简介:这是一份面向Qt开发者的QTableView多级表头实现资源,演示如何通过继承QHeaderView并重写paintSection、sectionSizeFromContents等关键方法,用QPainter逐级绘制文字与分隔线,从而在水平或垂直表头上呈现多层结构,满足复杂数据展示与分组需求。包体共12个文件,涵盖cpp、h、ui、pro等类型,既有自定义表头类与项委托实现,也有界面布局和工程配置,代码量紧凑,压缩包仅17KB,很适合需要掌握表格高级定制的中级Qt开发者。目前已有842人学习下载。资源内容不仅包含多级表头初始化示例,还说明了在按键槽函数中触发表头重绘、同步更新QAbstractItemModel等注意事项,能帮助读者快速理清QTableView、QHeaderView与数据模型之间的协作机制,并直接复用改造到自身项目中。
1. 多级表头不是排版问题:QTableView 的一行表头够用吗
做数据报表的同事往往第一反应是"用 QTableView 表头那一行多放几个分隔就行"。真把 Excel 里那种"大区/省份/城市"的三级头搬进 QT 项目,才发现 QTableView 的水平表头只认一行 section,跨列、分组、两级标题全得自己做。我在这上面连调了三天 paintEvent,中间一度怀疑是自己姿势不对,后来确认是官方 API 的边界限制。本文给出一套能直接抄的自绘方案:重写 QHeaderView::paintSection,用分组数据结构把一级标题画在两栏之上、二级标题画在每列之上,并把排序、列宽拖动、横向滚动这几个连带问题一起讲透。
2. 两条路线先摆出来:setSpan 合并还是直接重绘表头
2.1 先看官方给出的 setSpan:它到底能合并什么
不少教程把 QHeaderView::setSpan 说得神乎其神,好像一行代码就能搞定多级表头。它确实能合并表头区域,但合并的是"同一行里的相邻列",而不是"多出一行来装上级标题"。先看签名:
bool setSpan(int row, int column, int rowSpan, int columnSpan);典型调用是这样:
QHeaderView* header = tableView->horizontalHeader(); header->setSpan(0, 0, 1, 3); // 第0行、第0列起,纵向1行、横向跨3列参数含义很直白:row 是表头模型里的行号,column 是起始列,rowSpan 是纵向跨几行,columnSpan 是横向跨几列。问题就出在这个 rowSpan 上——QTableView 的水平表头在模型眼里只有一行。你用 QStandardItemModel 给表头造出多行结构是另一套玩法,但 QTableView 的 horizontalHeader 不会主动把两个 section 变成两行来显示。所以 setSpan(0, 0, 1, 3) 实际能产生的效果只是"让第 0、1、2 列的表头区域连在一起显示一个标题",等于把三个 section 的绘制合并成一次。
还有个更隐蔽的坑:setSpan 内部是把跨度信息通过 Qt::UserRole 写进 header model 的。代码走的是这样的通道:
// 伪代码,展示 setSpan 的实际行为 QModelIndex idx = header->model()->index(0, column); header->model()->setData(idx, QVariant::fromValue(QSize(columnSpan, rowSpan)), Qt::UserRole);如果你用的是 QAbstractTableModel,它的 setData 默认返回 false,这个 UserRole 根本写不进去,setSpan 调了等于没调。只有 QStandardItemModel 这种每个 item 都能自由存取 UserRole 的模型,setSpan 才能正常生效。这一点网上很多教程是不提的,等你复制完代码发现没反应,才会意识到问题出在模型实现上。
所以我的结论先放这:setSpan 适合"一行表头、部分列视觉上合并"的简单场景,撑不起真正的两级表头。真正要做的"组标题在上、子列名在下",得走另一条路。
2.2 自绘 QHeaderView:两级表头的关键点
两级表头的本质是:QTableView 水平表头这一行物理区域里,上半部分画分组标题,下半部分画子列标题。要做到这一点,就必须让 paintSection 自己决定每个 section 画什么、画到哪儿,而不是交给 QHeaderView 默认的"单行文本"逻辑。
重写 QHeaderView::paintSection 的核心思路有三个:
第一,把"分组配置"变成一份纯数据。每个分组记录自己覆盖的逻辑列区间(firstColumn 到 lastColumn)和一级标题文本。绘制时拿到当前 section 的逻辑列号,查一下它属于哪个组,再判断是不是这个组的第一个列——只有组内第一列才负责画一级标题,其他列只画二级标题,避免同一个组标题被重复绘制导致叠影。
第二,绘制坐标必须用 sectionViewportPosition 换算,不能用缓存或者一次性算好的固定坐标。因为 QHeaderView 支持拖动列、排序改变视觉顺序,甚至横向滚动时 section 相对 viewport 的位置会一直变。每次 paint 都现算,才能保证组标题永远覆盖在正确的列范围上。
第三,尺寸上要让表头物理高度足够显示两行。QHeaderView 默认按一行文本的高度给 section 分配空间,你在 paintSection 里画到下半部分时,高度不够就直接截断。常见做法是重写 sectionSizeFromContents 并配合 setMinimumHeight / setMaximumHeight,把表头高度顶到两行文字加边距的量。
自绘方案的优势很明显:不依赖模型的 UserRole 支持,不依赖 QStandardItemModel 这种特定模型,绘制和交互逻辑全在自己手里。缺点也明显——鼠标事件、排序箭头、列宽拖动的细节都要自己兜,这就是后面几章要展开的内容。
2.3 选型判断:什么业务选哪条路
两条路线的边界我来做个实际对比,你可以照着选:
| 能力 | setSpan 路线 | 自绘 QHeaderView |
|---|---|---|
| 一级跨列标题 | 支持,但只有一行表头可用 | 支持,天然两级 |
| 二级子列名+组标题同时存在 | 不支持 | 支持 |
| QAbstractTableModel 兼容 | 需要额外实现 UserRole 读写 | 完全兼容 |
| 列顺序拖动/排序后稳定性 | 排序后 span 容易错乱 | 每次绘制重新换算视觉坐标 |
| 代码量 | 少,几天内能跑通 | 中等,核心在 paintSection 和鼠标分流 |
| 后期维护成本 | 低,适合一次性报表 | 低,配置驱动后改结构不改代码 |
我一般这样判断:项目里只有"把若干列合并成一个总标题"的需求,比如时间维度"2025 年"底下只有一列数据,那 setSpan 够用。但如果是权限表、指标配置表、考试分析这类"组 A 底下有 5 列、组 B 底下有 3 列,每个子列还有自己的名字和排序逻辑",直接上自绘,省得以后为了补功能再重写一遍。
3. 把 MultiHeaderView 写出来:分组数据与 paintSection 实现
3.1 HeaderGroup 结构:先定义"组覆盖哪些列"
一切自绘工作的前提,是把表头结构变成程序能查询的数据。我定义的结构是这样:
#pragma once #include <QHeaderView> #include <QVector> struct HeaderGroup { int firstColumn = -1; // 本组覆盖的逻辑列起点 int lastColumn = -1; // 本组覆盖的逻辑列终点 QString title; // 一级标题文本 bool contains(int column) const { return column >= firstColumn && column <= lastColumn; } }; class MultiHeaderView : public QHeaderView { Q_OBJECT public: explicit MultiHeaderView(Qt::Orientation orientation, QWidget* parent = nullptr); void setGroups(const QVector<HeaderGroup>& groups); QVector<HeaderGroup> groups() const; protected: void paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const override; QSize sectionSizeFromContents(int logicalIndex) const override; signals: void groupSortRequested(int firstColumn, int lastColumn); void columnSortRequested(int column); private: HeaderGroup groupOf(int logicalIndex) const; QVector<HeaderGroup> m_groups; };这里有三个参数要特别说明。
firstColumn 和 lastColumn 存的是逻辑列号,不是视觉列号。逻辑列号是模型里的列索引,比如"姓名"是第 0 列,"年龄"是第 1 列。排序或拖动列之后,视觉顺序会变,但逻辑列号不变。绘制时我们要做的就是"把逻辑列号换算成当前视觉位置",所以结构里只存逻辑区间,坐标换算留在绘制函数里。
contains 方法用来查询某个逻辑列属于哪个组,是分组判断的入口。每次 paintSection 都会调用它,属于高频路径,保持这个函数只有两个整数比较,不要在里面做字符串匹配。
setGroups 是外部接口,接收完整的组配置。它内部要做两件事:保存 m_groups,然后触发一次 viewport 重绘。具体的表头高度调整也放在这里,后面会讲到。
3.2 paintSection:上半部分画组标题,下半部分画子列名
这是整个组件的核心,完整代码如下:
void MultiHeaderView::paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const { painter->save(); painter->fillRect(rect, palette().button()); painter->setPen(palette().color(QPalette::ButtonText)); const HeaderGroup group = groupOf(logicalIndex); const int half = height() / 2; // 只有组的第一个逻辑列负责画一级标题,避免重复绘制 if (logicalIndex == group.firstColumn) { QRect groupRect = rect; // 用视觉坐标算出组在 viewport 中的实际跨度 int startPos = sectionViewportPosition(group.firstColumn); int endPos = sectionViewportPosition(group.lastColumn) + sectionSize(group.lastColumn); groupRect.setLeft(startPos); groupRect.setRight(endPos - 1); groupRect.setHeight(half); // 跨列绘制必须裁剪到当前 section 可见区域 painter->setClipRect(rect); painter->drawText(groupRect, Qt::AlignCenter, group.title); // 一级标题底部分隔线 painter->setPen(QPen(palette().mid(), 1)); painter->drawLine(groupRect.left(), groupRect.bottom(), groupRect.right(), groupRect.bottom()); painter->setPen(palette().color(QPalette::ButtonText)); } // 下半部分:读取标准表头数据作为二级标题 QRect subRect = rect.adjusted(0, half, 0, 0); const QVariant title = model()->headerData( logicalIndex, orientation(), Qt::DisplayRole); painter->drawText(subRect, Qt::AlignCenter, title.toString()); // 二级标题之间的列分隔线 painter->setPen(palette().midlight().color()); painter->drawLine(subRect.right(), subRect.top(), subRect.right(), subRect.bottom()); painter->restore(); }这段代码有四个地方容易写错,逐个说。
一是 groupRect 的宽度。rect 永远是"当前这个 section 在 viewport 里占的矩形",它只覆盖一列。如果直接把组标题文本画到 rect 上,三列宽的组标题就被塞进一列里,不截断才怪。所以我用 sectionViewportPosition 分别取组起点和组终点的视觉 x 坐标,计算出整个组在 viewport 坐标系下的总宽度。这样文本天然居中在整个组上方。
二是 setClipRect(rect)。groupRect 可能比 rect 宽,直接 drawText 会画到相邻 section 的地盘上,而相邻 section 也会执行自己的 paintSection,后画的会把先画的盖掉,结果就是文字残缺。先裁剪再画,文字只在当前 section 可见范围内输出,既不会串画,也不会出现残影。
三是 half 的高度。height() / 2 是写死的一半对半分,如果你的表头需要"组标题比子列标题高",可以改成height() * 2 / 3或者存一个比例成员。但要注意:所有 section 共用同一个 height(),所以无论比例怎么定,每个 section 的上下分界线都是一条贯穿整个表头的直线,视觉上不会出现台阶。
四是二级标题的数据源直接用 model()->headerData。这样 MultiHeaderView 不额外存一份列名列表,模型里表头文本改了,表头自动跟着变,不用同步。
3.3 表头高度与刷新时机:别让两行压成一行
只重写 paintSection 还不够,QHeaderView 默认的高度是按单行文本算的。如果你不把表头物理高度撑起来,paintSection 里就算画了下半部分,也会因为绘图区域超出 section 实际高度而被 viewport 裁掉。我通常这样处理:
QSize MultiHeaderView::sectionSizeFromContents(int logicalIndex) const { // 表头每个 section 的高度 = 两行文字高度 + 上下边距 return QSize(sizeHint().width(), 2 * fontMetrics().height() + 10); } void MultiHeaderView::setGroups(const QVector<HeaderGroup>& groups) { m_groups = groups; const int headerHeightHint = 2 * fontMetrics().height() + 10; setMinimumHeight(headerHeightHint); setMaximumHeight(headerHeightHint); // 触发表头整体重绘,让新分组立即生效 viewport()->update(); }sectionSizeFromContents 的返回值只影响 QHeaderView 计算默认高度时的参考值,真正约束表头高度的是 setMinimumHeight 和 setMaximumHeight。注意这里用的是 fontMetrics().height(),如果你的表头字体通过 QSS 或 setFont 换过,一定要在设置字体之后再调用 setGroups,否则高度是按旧字体算的,换了字重或者字号变大就会截断。
还有刷新时机的问题。paintSection 是 QHeaderView 内部按 section 逐个调用的,QTableView 横向滚动时,水平表头会自动跟随滚动条更新可见区域,这部分不需要我们操心。真正需要主动刷新的是两个场景:分组配置变更(setGroups 里做)和列宽被拖动后(下一章详细说)。统一用 viewport()->update() 触发的是一次完整的表头区域重绘,代价不大,不要为了省那点绘制时间去做局部 update,容易漏画分隔线。
4. 接进 QTableView:排序、列宽与滚动的联动细节
4.1 模型侧:headerData 只返回二级标题就够了
自绘方案对模型的要求非常低,二级标题就是最普通的表头数据:
QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { if (section < m_columns.size()) return m_columns[section].title; } return QVariant(); }很多初次接触的人会误以为 MultiHeaderView 需要一个"二维 header model"来提供组标题和子标题两层数据。实际上组标题在 m_groups 里,子标题在模型的标准 headerData 里,各管各的,互不干扰。这也是自绘方案比 setSpan 干净的地方:setSpan 想让组标题生效,得让模型的 data/setData 配合 UserRole,等于把界面结构塞进业务模型;自绘方案里模型只管自己的列名,表头怎么分组是视图层的事。
把 MultiHeaderView 接进 QTableView 同样简单:
auto* header = new MultiHeaderView(Qt::Horizontal, tableView); tableView->setHorizontalHeader(header); QVector<HeaderGroup> groups; // ... 用上一章的配置填充 groups ... header->setGroups(groups);替换 horizontalHeader 后,QTableView 现有的网格、列宽、滚动机制全部照常工作,因为这些功能不依赖表头内部怎么画。
4.2 点击分流:组排序与单列排序的鼠标处理
两级表头带来的交互问题是:用户点击组标题区域时,意图多半是"整组数据排个序";点击子列标题区域时,才是传统的单列排序。QHeaderView 默认的 mouseReleaseEvent 只会触发单个 section 的排序逻辑,所以我们要自己拦截并分流。
这里有一个细节必须处理:QHeaderView 的鼠标事件里包含了列宽拖动的逻辑。如果我们在 press 阶段就把事件全截住,列宽拖动就废了。我在实际实现里维护两个状态:
void MultiHeaderView::mousePressEvent(QMouseEvent* event) { m_pressedColumn = -1; m_resizing = false; if (event->button() == Qt::LeftButton) { int logical = logicalIndexAt(event->pos()); int visualIdx = visualIndex(logical); if (visualIdx >= 0) { // 靠近列分隔线(2像素内),交给父类处理列宽拖动 int right = sectionViewportPosition(logical) + sectionSize(logical); if (qAbs(event->pos().x() - right) <= 2) { m_resizing = true; QHeaderView::mousePressEvent(event); return; } } m_pressedColumn = logical; m_pressedAtHalf = (event->pos().y() < height() / 2); return; // 不调用父类,排序时机压到 release 再决定 } QHeaderView::mousePressEvent(event); } void MultiHeaderView::mouseReleaseEvent(QMouseEvent* event) { if (m_resizing) { m_resizing = false; QHeaderView::mouseReleaseEvent(event); return; } if (m_pressedColumn >= 0 && event->button() == Qt::LeftButton && logicalIndexAt(event->pos()) == m_pressedColumn) { if (m_pressedAtHalf) { const HeaderGroup g = groupOf(m_pressedColumn); emit groupSortRequested(g.firstColumn, g.lastColumn); } else { emit columnSortRequested(m_pressedColumn); } m_pressedColumn = -1; return; } QHeaderView::mouseReleaseEvent(event); m_pressedColumn = -1; }注意几个边界条件。m_pressedColumn 初始是 -1,release 时先确认按下的那一刻和松开的那一刻落在同一个逻辑列,否则用户可能在按下后拖到了别的列,这种情况下不应该触发排序。m_resizing 用来标记本次按压是列宽拖动,避免把拖动结束误判成点击。只有既不在拖动、又落在表头文字区域、且列没变,才会发排序信号。
发信号之后,真正执行排序的是外部代码。典型的连接是这样:
// 使用 QSortFilterProxyModel 的典型写法 connect(header, &MultiHeaderView::columnSortRequested, this, [=](int column) { proxyModel->sort(column, Qt::AscendingOrder); header->setSortIndicator(column, Qt::AscendingOrder); }); connect(header, &MultiHeaderView::groupSortRequested, this, [=](int firstColumn, int lastColumn) { // 通常按组内第一列排序即可满足业务 proxyModel->sort(firstColumn, Qt::AscendingOrder); });组排序按组内第一列排,是最常见的业务约定。如果组内的排序优先级有特殊要求,比如"成绩"组想先按数学、再按语文,那就要在外面拼多级 sort role,不要在表头组件里做,表头只负责告诉外部"哪个组被点了"。
4.3 列宽拖动后的刷新策略
列宽被用户拖动后,组标题的视觉跨度变了,但 QHeaderView 不会自动知道这件事。paintSection 虽然每次绘制都会现算 sectionViewportPosition,可问题是拖动列宽并不会让所有相关 section 都自动重绘,尤其是同组里没有被拖动的那些列。
我最早踩的坑是只 update 被拖动的那个 section,结果组标题的右边界和子列右边界对不上,中间留了一条白缝。后来改成监听 sectionResized 信号,统一刷新整个表头:
connect(this, &QHeaderView::sectionResized, this, [this](int, int, int) { // 拖任何一列都全量重绘表头,保证组标题范围正确 viewport()->update(); });sectionResized 的参数(逻辑列号、旧宽、新宽)这里都用不上,因为刷新是整体性的。这样处理虽然一次重绘所有 section,但 QHeaderView 的 section 数量通常只有几十个,绘制开销完全可以忽略。
要特别提醒的是:在这个信号的槽函数里绝对不要再调用 resizeSection 去调整同组其他列的宽度。我曾经想让"组内列宽联动",结果 A 列变化触发 B 列变化,B 列变化又触发 sectionResized 更新 A 列,形成递归,程序直接卡死。如果真的要做联动,必须加一个m_resizingBatch布尔标志,进入联动逻辑时置位,槽函数里检测到该标志就跳过递归段,等整批 resize 结束后再统一更新。
5. 避坑记录:多级表头容易翻车的五个点
5.1 setSpan 写了没反应
现象:header->setSpan(0, 0, 1, 3)跑起来毫无效果,表头还是老样子,代码也执行到了,断点也打了,就是不变。
原因:setSpan 内部要把跨度信息写进模型,走的是 Qt::UserRole 通道。QAbstractTableModel 的 setData 默认返回 false,数据写不进去,合并自然不生效。另外还有一种情况:setSpan 在 setModel 之前调用,表头还没绑定模型,调用被静默忽略。
解决:如果坚持用 setSpan,要么换 QStandardItemModel 当模型,要么给模型实现 data/setData 对 Qt::UserRole 的读写。但既然已经决定做两级表头,直接走自绘路线把这个 API 抛到脑后,能省掉一大串为 UserRole 写的兼容代码。
5.2 一级标题被省略号截断
现象:组标题"2025 年第一季度数据"只显示出"2025 年…",而且拖动列宽时省略号位置还会变。
原因:paintSection 是按 section 逐列调用的,rect 只覆盖当前列。直接把整个组标题塞进当前列的 rect,宽度不够就会被省略号截断。
解决:计算组标题真实绘制范围时用 sectionViewportPosition 算出整个组在 viewport 坐标系下的宽度,然后用 setClipRect 限制输出区域。文本的 AlignCenter 是基于完整 groupRect 的,所以不会出现标题偏左或偏右的问题。
5.3 排序后组标题和子列错位
现象:点某一列排序后,组标题跑到别的列上方,或者组标题覆盖范围跟实际成员列对不上。
原因:HeaderGroup 存的是逻辑列号,但排序会改变视觉顺序。QHeaderView 默认的 section 绘制机制里,section 的位置由视觉索引决定,逻辑列 3 可能排在了最左边。如果你在绘制时直接用逻辑列当数组下标去取位置,必然错位。
解决:paintSection 里凡是涉及"画在哪"的地方,一律用 sectionViewportPosition(logicalIndex) 现算,不要缓存一个固定的像素坐标。如果开启了列拖动(setSectionsMovable(true)),还要监听 sectionMoved 信号触发表头重绘。业务允许的情况下,直接 setSectionsMovable(false),少一个变量就少一类 bug。
5.4 排序箭头叠在自绘文字上
现象:点击子列标题排序后,系统自带的上箭头/下箭头出现在单元格正中间,把二级标题文字挡住了一部分,看起来很脏。
原因:QHeaderView 默认在 paintSection 里画 sortIndicator,位置是当前 section 的中央。我们的自绘代码也在中央画文字,两边就叠在一起。
解决:在构造 MultiHeaderView 时调用 setSortIndicatorShown(false),关掉系统箭头。然后在自己的 paintSection 里,如果判断当前 section 就是排序列,就在组标题或子标题的右上角画一个小三角。箭头绘制代码不长,就是用 painter->drawLines 画两条线的事,但注意箭头颜色要在深色背景下也能看清,取 palette().color(QPalette::HighlightedText) 比较稳妥。
5.5 横向滚动表头闪断和重影
现象:拖动水平滚动条时,表头的组标题文字在滚动过程中出现残影,或者部分区域没擦干净。
原因:滚动发生时 QHeaderView 只重绘需要更新的 section,而组标题跨多个 section,绘制范围超出单个 section 的矩形,导致某些区域没有进入重绘清单。另外,如果代码里在 paintEvent 里做了整体绘制而不是在 paintSection 里做,滚动时新旧内容叠加,就会出现重影。
解决:坚持在 paintSection 实现,不要重写 paintEvent。滚动导致的可见区域变化,QHeaderView 会内部协调好每个 section 的更新范围。遇到个别 section 没重绘的情况,就用 sectionResized 里那套兜底策略,统一 viewport()->update() 全量重绘一次。全量重绘后,残影和错位基本都能解决。
6. 进阶:用配置驱动表头并快速过一遍交付检查
6.1 把表头结构改成配置:10 行代码从配置建组
写完 MultiHeaderView 之后,我做的第一件事就是把表头分组改成配置驱动。不再在代码里手写 HeaderGroup 列表,而是用一个贴近业务的配置结构:
struct HeaderCfg { QString group; // 组名 QStringList columns; // 组内子列名 }; // 这份配置直接从 JSON / XML / 数据库读取,改表头不用动界面代码 QVector<HeaderCfg> spec = { {"基本信息", {"姓名", "年龄", "状态"}}, {"考试成绩", {"语文", "数学", "英语"}} }; QVector<HeaderGroup> makeGroups(const QVector<HeaderCfg>& spec) { QVector<HeaderGroup> groups; int col = 0; for (const HeaderCfg& cfg : spec) { HeaderGroup g; g.firstColumn = col; g.lastColumn = col + cfg.columns.size() - 1; g.title = cfg.group; groups.append(g); // 子列标题仍然走 model->headerData,这里只负责分组区间 col += cfg.columns.size(); } return groups; }要注意的是分组覆盖的总列数必须和模型 columnCount 一致,否则会出现组标题画到不存在列上、或者部分列没有归属组的情况。我一般会在 setGroups 里做个断言:遍历 m_groups,检查最后一个组的 lastColumn 是否等于 model()->columnCount() - 1。配置驱动带来的收益很直接:业务方调整表头结构时,只改配置数据源,根本不碰绘制代码,出问题的概率一下子降下来。
6.2 交付前 10 分钟验证清单
我每次在模拟项目X里集成完这套表头,都会按下面的清单快速过一遍:
| 检查项 | 验证方法 | 通过标准 |
|---|---|---|
| 表头高度 | 全屏运行,切换系统字体 | 两级标题都完整显示,无截断 |
| 横向滚动 | 拖到最右再拖回来 | 组标题与子列无错位、无残影 |
| 点击分流 | 点组标题区域,再点子标题区域 | 组内整体排序/单列排序各司其职 |
| 排序箭头 | 分别点击上下两级触发排序 | 箭头出现在正确列且不遮挡文字 |
| 列宽拖动 | 把组内最窄的列拖到极限 | 组底线连续,分隔线不歪斜 |
| 高分屏缩放 | 缩放比例 125% / 150% 切换 | 表头文字和分隔线跟随缩放不错位 |
这六项里最容易出事的其实是最后一项高分屏缩放,因为 fontMetrics().height() 在缩放后会变化,如果 setGroups 里的高度计算只执行了一次,缩放后表头高度还是旧值,第二级标题就会被裁掉。我在高 DPI 场景下的习惯是重写 resizeEvent,在里面重新计算表头高度并调用 setMinimumHeight。
从那以后,我每次给 QTableView 加多级表头都强制走一遍这套流程:先画配置、再试点击分流、最后过清单。自绘组件不像官方 API 那样有成熟的兜底,边界条件全靠自己填,但填过一次之后,后面所有项目都能带着这套思路直接复用。希望帮到你。
本文还有配套的精品资源,点击获取