原文链接:Qt实现表格树控件-支持多级表头

一、概述

之前写过一篇关于表格控件多级表头的文章,喜欢的话可以参考Qt实现表格控件-支持多级列表头、多级行表头、单元格合并、字体设置等。今天这篇文章带来了比表格更加复杂的控件-树控件多级表头实现。

在Qt中,表格控件包含有水平和垂直表头,但是常规使用模式下都是只能实现一级表头,而树控件虽然包含有了branch分支,这也间接的削弱了他自身的表头功能,细心的同学可能会发现Qt自带的QTreeView树控件只包含有水平表头,没有了垂直表头。

既然Qt自带的控件中没有这个功能,那么我们只能自己去实现了。

要实现多级表头功能方式也多种多样,之前就看到过几篇关于实现多级表头的文章,总体可以分为如下两种方式

  1. 表头使用一个表格来模拟
  2. 通过给表头自定义Model

今天这篇文章我们是通过方式2来实现多级表头。如效果图所示,实现的是一个树控件的多级表头,并且他还包含了垂直列头,实现这个控件所需要完成的代码量还是比较多的。本篇文章可以算作是一个开头吧,后续会逐步把关键功能的实现方式分享出来。

二、效果展示

三、实现方式

本篇文章中的控件看起来是一个树控件,但是他又具备了表格控件该有的一些特性,比如垂直表头、多级水平表头等等。要实现这样的树控件,我们有两个大的方向可以去考虑

  1. 重写表格控件,实现branch
  2. 表格控件+树控件

方式1重写表格控件实行branch的工作量是比较大的,而且需要把Qt原本的代码迁出来,工作量会比加大,个人选择了发你。

方式2是表格控件+树控件的实现方式,说白了就是表格控件提供水平和垂直表头,树控件提供内容展示,听起来好像没毛病,那么还等什么,直接干呗。

既然大方向定了,那么接下来可能就是一些细节问题的确定。

  1. 多级水平表头
  2. 垂直列头拖拽时,实现树控件行高同步变动
  3. 自绘branch

以上三个问题都是实现表格树控件时遇到的一些比较棘手的问题,后续会分别通过单独的文章来进行讲解,今天这篇文章也是我们的第一讲,怎么实现水平多级表头

四、多级表头

第一节我们也说了,实现多级表头我们使用重写model的方式来实现,接下来就是贴代码的时候。

1、数据源

经常重写model的同学对如下代码应该不陌生,对于继承自QAbstractItemModel的数据源肯定是需要重写该类的所有纯虚方法,包括所有间接父类的纯虚方法。

除此之外自定义model应该还需要提供一个可以合并单元格的方法,为什么呢?因为我们多级表头需要。比如说我们一级表头下有3个二级表头,那这就说明一级表头合并了3列,使得本身的3列数据变成一列。

class HHeaderModel : public QAbstractItemModel
{
struct ModelData //模型数据结构
{
QString text; ModelData() : text("")
{
}
}; Q_OBJECT public:
HHeaderModel(QObject * parent = 0);
~HHeaderModel(); public:
void setItem(int row, int col, const QString & text); QString item(int row, int col); void setSpan(int firstRow, int firstColumn, int rowSpanCount, int columnSpanCount);
const CellSpan& getSpan(int row, int column); public:
virtual QModelIndex index(int row, int column, const QModelIndex & parent) const override;
virtual QModelIndex parent(const QModelIndex & child) const override;
virtual int rowCount(const QModelIndex & parent) const override;
virtual int columnCount(const QModelIndex & parent) const override;
virtual QVariant data(const QModelIndex & index, int role) const override;
virtual Qt::ItemFlags flags(const QModelIndex & index) const override;
virtual bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole) override;
virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; private:
//找到对应的模型数据
ModelData * modelData(const QModelIndex & index) const; private:
//key rowNo, key colNo
QMap<int, QMap<int, ModelData *> > m_modelDataMap;
int m_iMaxCol; CellSpan m_InvalidCellSpan;
QList<CellSpan> m_cellSpanList;
};

以上便是model的头文件声明,其中重写父类的虚方法这里就不做过多说明,和平时重写其他数据源没有区别,这里多了重点说明下setSpan接口。

void HHeaderModel::setSpan(int firstRow, int firstColumn, int rowSpanCount, int columnSpanCount)
{
for (int row = firstRow; row < firstRow + rowSpanCount; ++row)
{
for (int col = firstColumn; col < firstColumn + columnSpanCount; ++col)
{
m_cellSpanList.append(CellSpan(row, col, rowSpanCount, columnSpanCount, firstRow, firstColumn));
}
}
} const CellSpan& HHeaderModel::getSpan(int row, int column)
{
for (QList<CellSpan>::const_iterator iter = m_cellSpanList.begin(); iter != m_cellSpanList.end(); ++iter)
{
if ((*iter).curRow == row && (*iter).curCol == column)
{
return *iter;
}
} return m_InvalidCellSpan;
}

setSpan接口存储了哪些列或者行被合并了,在绘制表格头时我们也可以根据这些信息来计算绘制的大小和位置。

2、表格

数据源介绍完,接下来看看表头视图类,这个类相对Model来说会复杂很多,主要也是根据Model中存储的合并信息来进行绘制。

由于这个类代码比价多,这里我就不贴头文件声明了,下面还是主要介绍下关键函数

a、paintEvent绘制函数

void HHeaderView::paintEvent(QPaintEvent * event)
{
QPainter painter(viewport());
QMultiMap<int, int> rowSpanList; int cnt = count();
int curRow, curCol; HHeaderModel * model = qobject_cast<HHeaderModel *>(this->model());
for (int row = 0; row < model->rowCount(QModelIndex()); ++row)
{
for (int col = 0; col < model->columnCount(QModelIndex()); ++col)
{
curRow = row;
curCol = col; QStyleOptionViewItemV4 opt = viewOptions(); QStyleOptionHeader header_opt;
initStyleOption(&header_opt); header_opt.textAlignment = Qt::AlignCenter;
// header_opt.icon = QIcon("./Resources/logo.ico");
QFont fnt;
fnt.setBold(true);
header_opt.fontMetrics = QFontMetrics(fnt); opt.fontMetrics = QFontMetrics(fnt);
QSize size = style()->sizeFromContents(QStyle::CT_HeaderSection, &header_opt, QSize(), this); // size.setHeight(25);
header_opt.position = QStyleOptionHeader::Middle; //判断当前行是否处于鼠标悬停状态
if (m_hoverIndex == model->index(row, col, QModelIndex()))
{
header_opt.state |= QStyle::State_MouseOver;
// header_opt.state |= QStyle::State_Active;
} opt.text = model->item(row, col);
header_opt.text = model->item(row, col); CellSpan span = model->getSpan(row, col); int rowSpan = span.rowSpan;
int columnSpan = span.colSpan; if (columnSpan > 1 && rowSpan > 1)
{
//单元格跨越多列和多行, 不支持,改为多行1列
continue;
/*header_opt.rect = QRect(sectionViewportPosition(logicalIndex(col)), row * size.height(), sectionSizes(col, columnSpan), rowSpan * size.height());
opt.rect = header_opt.rect;
col += columnSpan - 1; */
}
else if (columnSpan > 1)//单元格跨越多列
{
header_opt.rect = QRect(sectionViewportPosition(logicalIndex(col)), row * size.height(), sectionSizes(col, columnSpan), size.height());
opt.rect = header_opt.rect;
col += columnSpan - 1;
}
else if (rowSpan > 1)//单元格跨越多行
{
header_opt.rect = QRect(sectionViewportPosition(logicalIndex(col)), row * size.height(), sectionSize(logicalIndex(col)), size.height() * rowSpan);
opt.rect = header_opt.rect;
for (int i = row + 1; i <= rowSpan - 1; ++i)
{
rowSpanList.insert(i, col);
}
}
else
{
//正常的单元格
header_opt.rect = QRect(sectionViewportPosition(logicalIndex(col)), row * size.height(), sectionSize(logicalIndex(col)), size.height());
opt.rect = header_opt.rect;
} opt.state = header_opt.state; //opt.displayAlignment = Qt::AlignCenter; //opt.icon = QIcon("./Resources/logo.ico");
//opt.backgroundBrush = QBrush(QColor(255, 0, 0));
QMultiMap<int, int>::iterator it = rowSpanList.find(curRow, curCol);
if (it == rowSpanList.end())
{
//保存当前item的矩形
m_itemRectMap[curRow][curCol] = header_opt.rect;
itemDelegate()->paint(&painter, opt, model->index(curRow, curCol, QModelIndex())); painter.setPen(QColor("#e5e5e5"));
painter.drawLine(opt.rect.bottomLeft(), opt.rect.bottomRight());
}
else
{
//如果是跨越多行1列的情况,采用默认的paint
}
}
} //painter.drawLine(viewport()->rect().bottomLeft(), viewport()->rect().bottomRight());
}

b、列宽发生变化

void HHeaderView::onSectionResized(int logicalIndex, int oldSize, int newSize)
{
if (0 == newSize)
{
//过滤掉隐藏列导致的resize
viewport()->update();
return;
} static bool selfEmitFlag = false;
if (selfEmitFlag)
{
return;
} int minWidth = 99999;
QFontMetrics metrics(font());
//获取这列上最小的字体宽度,移动的长度不能大于最小的字体宽度
HHeaderModel * model = qobject_cast<HHeaderModel *> (this->model());
for (int i = 0; i < model->rowCount(QModelIndex()); ++i)
{
QString text = model->item(i, logicalIndex);
if (text.isEmpty())
continue; int textWidth = metrics.width(text);
if (minWidth > textWidth)
{
minWidth = textWidth;
}
} if (newSize < minWidth)
{
selfEmitFlag = true;
resizeSection(logicalIndex, oldSize);
selfEmitFlag = false;
} viewport()->update();
}

3、QStyledItemDelegate绘制代理

既然说到了自绘,那么有必要说下Qt自绘相关的一些东西。

a、paintEvent

paintEvent是QWidget提供的自绘函数,当界面刷新时该接口就会被调用。

b、复杂控件自绘

对于一些比较复杂的控件为了达到更好的定制型,Qt把paintEvent函数中的绘制过程进行了更为详细的切割,也可以让我们进行布局的重写。

比如今天说到的树控件,当paintEvent函数调用时,其实内部真正进行绘制的是如下3个函数,重写如下三个函数可以为我们带来更友好的定制性,并且很大程度上减轻了我们自己去实现的风险。

virtual void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const
virtual void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
void drawTree(QPainter *painter, const QRegion &region) const

c、QStyledItemDelegate绘制代理

为了更好的代码管理和接口抽象,Qt在处理一些超级变态的控件时提供了绘制代理这个类,即使在一些力度很小的绘制函数中依然是调用的绘制代理去绘图。

刚好我们今天说的这个表头绘制就是如此。如下代码所示,是一部分的HHeaderItemDelegate::paint函数展示,主要是针对表头排序进行了定制。

void HHeaderItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
{
int row = index.row();
int col = index.column(); const int textMargin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; QStyleOptionHeader header_opt;
header_opt.rect = option.rect;
header_opt.position = QStyleOptionHeader::Middle;
header_opt.textAlignment = Qt::AlignCenter; header_opt.state = option.state;
//header_opt.state |= QStyle::State_HasFocus;//QStyle::State_Enabled | QStyle::State_Horizontal | QStyle::State_None | QStyle::State_Raised; if (HHeaderView::instance->isItemPress(row, col))
{
header_opt.state |= QStyle::State_Sunken; //按钮按下效果
} // if ((QApplication::mouseButtons() && (Qt::LeftButton || Qt::RightButton)))
// header_opt.state |= QStyle::State_Sunken; painter->save();
QApplication::style()->drawControl(QStyle::CE_Header, &header_opt, painter);
painter->restore();
}

五、测试代码

把需要合并的列和行进行合并,即可达到多级表头的效果,如下是设置表格model合并接口展示。

horizontalHeaderModel->setSpan(0, 0, 1, 4);
horizontalHeaderModel->setSpan(0, 4, 1, 3);
horizontalHeaderModel->setSpan(0, 7, 1, 3); horizontalHeaderModel->setSpan(0, 10, 2, 1);
horizontalHeaderModel->setSpan(0, 11, 2, 1); //不支持跨越多行多列的情况
}

model设置完毕后只要把Model设置给QHeaderView类即可。

六、相关文章

值得一看的优秀文章:

  1. 财联社-产品展示
  2. 广联达-产品展示
  3. Qt定制控件列表
  4. 牛逼哄哄的Qt库

  1. Qt实现表格控件-支持多级列表头、多级行表头、单元格合并、字体设置等

  2. Qt高仿Excel表格组件-支持冻结列、冻结行、内容自适应和合并单元格

  3. 属性浏览器控件QtTreePropertyBrowser编译成动态库(设计师插件)

  4. 超级实用的属性浏览器控件--QtTreePropertyBrowser

  5. Qt之表格控件蚂蚁线

  6. QRowTable表格控件-支持hover整行、checked整行、指定列排序等

如果您觉得文章不错,不妨给个打赏,写作不易,感谢各位的支持。您的支持是我最大的动力,谢谢!!!

很重要--转载声明

  1. 本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords

  2. 如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。


Qt实现表格树控件-支持多级表头的更多相关文章

  1. Qt实现表格树控件-自绘树节点虚线

    目录 一.开心一刻 二.自绘树节点? 三.效果展示 四.实现思路 1.可扩展接口 2.函数重写 3.同步左侧表头 五.相关文章 原文链接:Qt实现表格树控件-自绘树节点虚线 一.开心一刻 一程序员第一 ...

  2. Qt实现表格控件-支持多级列表头、多级行表头、单元格合并、字体设置等

    目录 一.概述 二.效果展示 三.定制表头 1.重写数据源 2.重写QHeaderView 四.设置属性 五.相关文章 原文链接:Qt实现表格控件-支持多级列表头.多级行表头.单元格合并.字体设置等 ...

  3. 表格树控件QtTreePropertyBrowser编译成动态库(设计师插件)

    目录 一.回顾 二.动态库编译 1.命令行编译动态库和测试程序 2.vs工具编译动态库和测试程序 3.安装文档 4.测试文档 三.设计师插件编译 1.重写QDesignerCustomWidgetIn ...

  4. 超级实用的表格树控件--QtTreePropertyBrowser

    目录 一.源码下载 二.代码编译 1.intersect函数替换为intersected 2.移除UnicodeUTF8 3.QtGui模块拆分 4.Q_TYPENAME错误 5.qVariantVa ...

  5. Qt qml treeview 树控件

    qml并没有提供树控件,只能自己写了.model仍然用ListModel对象,弄成层级的就行.delegate必须用loader动态的增加子控件,如此而已. [先看效果] [下载] http://do ...

  6. QRowTable表格控件-支持hover整行、checked整行、指定列排序等

    目录 一.开心一刻 二.嘴一嘴 三.效果展示 四.浅谈实现 五.自定义数据源 1.data函数 2.flags函数 六.自定义视图 1.目的 2.问题分析 七.测试 八.相关文章 原文链接:QRowT ...

  7. QRowTable表格控件(五)-重写表头排序、支持第三次单击恢复默认排序

    目录 一.原生表格 二.效果展示 三.实现方式 1.排序列定制 2.排序交互修改 四.相关文章 原文链接:QRowTable表格控件(五)-重写表头排序.支持第三次单击恢复默认排序 一.原生表格 开发 ...

  8. 基于MVC4+EasyUI的Web开发框架经验总结(2)- 使用EasyUI的树控件构建Web界面

    最近花了不少时间在重构和进一步提炼我的Web开发框架上,力求在用户体验和界面设计方面,和Winform开发框架保持一致,而在Web上,我主要采用EasyUI的前端界面处理技术,走MVC的技术路线,在重 ...

  9. 使用EasyUI的树控件构建Web界面

    最近花了不少时间在重构和进一步提炼我的Web开发框架上,力求在用户体验和界面设计方面,和Winform开发框架保持一致,而在Web上,我主要采用EasyUI的前端界面处理技术,走MVC的技术路线,在重 ...

随机推荐

  1. 关于微信小程序中遇到的各种问题汇总(持续更新)

    1.关于 <input />标签容易忽略的问题: 使用<input />标签时容易忘记绑定bindblur()方法(输入框失去焦点时触发),因为用户用键盘输入时不一定会点击完成 ...

  2. HDU 1072 Nightmare 题解

    Nightmare Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total S ...

  3. 笔记7:Jquery知识

    jQuery 1 基本知识 jQuery 是一个 JavaScript 库.jQuery 极大地简化了 JavaScript 编程.其下载地址:http://jquery.com/download/ ...

  4. django中添加新的filter

    给模板传递了一个字典,却发现无法在模板中直接通过key获得value. 查阅资料后,这个问题可以通过添加自定义的filter来解决. 首先在app目录下创建一个templatetags目录,并在它的下 ...

  5. lf 前后端分离 (3) 中间建跨域

    一.关于中间建跨域 为了减少跨域代码冗余,采用中间件 from django.utils.deprecation import MiddlewareMixin class CorsMiddleware ...

  6. loadrunner12 Runtime Settings位置

  7. oracle中having与where的区别

    1.where 不能放在group by 的后面 2.HAVING 是跟GROUP BY 连在一起用的,放在GROUP BY 后面,此时的作用相当于WHERE 3.WHERE 后面的条件中不能有聚集函 ...

  8. 为什么说要搞定微服务架构,先搞定RPC框架

    今天开始聊一些微服务的实践,第一块,RPC框架的原理及实践,为什么说要搞定微服务架构,先搞定RPC框架呢? 一.需求缘起 服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现大公司跨团队 ...

  9. Codeforces Round #530 (Div. 2) F 线段树 + 树形dp(自下往上)

    https://codeforces.com/contest/1099/problem/F 题意 一颗n个节点的树上,每个点都有\(x[i]\)个饼干,然后在i节点上吃一个饼干的时间是\(t[i]\) ...

  10. 洛谷 P4053 [JSOI2007]建筑抢修

    传送门 思路 首先题意比较容易明白: n个建筑需要修复,只能同时修一个建筑,每个建筑修复需要t1时间,且必须在t2时间前修完,否则此建筑报废 问最多能修好多少个建筑 如果一个建筑在规定时间内没有修好的 ...