原文链接:Qt实现表格控件-支持多级列表头、多级行表头、单元格合并、字体设置等

一、概述

最近在研究QTableView支持多级表头的事情,百度了下网上资料还是挺多的。实现的方式总的来说有2种,效果都还不错,最主要是搞懂其中的原理,做到以不变应万变。

实现多级表头的方式有以下两种方案

  1. 行表头和列表头都是用一个表格去模拟
  2. 重写QHeadView

以上两种方式都可以实现多级表头,各有利弊,并且已经有人投入项目使用。

我个人还是比较偏向于第二种方式,因为这样我们才可以更好的了解Qt的底层,了解Qt的绘图机制,并且这样实现的效率也是比较高的,而且合理一些,比较可控(个人理解)。

后来我在网上找到了一个哥们写的控件,项目名字叫做RbTableHeaderView,挺不错的,可以实现我们要的功能,但是效果还是差一些,如果需要更友好的交互效果,那么还需要在继续完善这个demo。

今天闲来无事,找到了一个开源的网站,上边好多Qt的库,虽然有一些是很早以前的东西,但是也很值得我们去学习。为什么会提到这个网站呢?因为这个网站上就有我们要的这个多级表头事例,和上边提到的那个哥们的事例不谋而合。

想要学习更多开源事例的可以到openDesktop上去看看。还有我自己收录的牛逼哄哄的Qt库

下面我们就来讲解这个多节表头的实现方式,代码比较简单,主要是大家理解下这个实现方式,可以加以扩展。

后续的文章中我会在写一篇关于树控件多级表头的事例,这里先把文章名称挂载这里,后续发布后就可以看到--Qt实现表格树控件-支持多级表头

二、效果展示

多级表头的效果下图所示,很糙粗的一个demo,大家将就着看吧。

三、定制表头

定制表头我们主要是要重写2个东西,分别是数据源QAbstractTableModel和表头QHeaderView

1、重写数据源

数据源就是为视图提供数据的model,我们的所有显示的内容数据都来自这个model。

对于外部程序填充数据时和往常使用同样的方式

  1. for (int i = 0; i < 10; i++)
  2. {
  3. QList<QStandardItem*> items;
  4. for (int j = 0; j < 8; j++)
  5. {
  6. items.append(new QStandardItem(QString("item(%1, %2)").arg(i).arg(j)));
  7. }
  8. dataModel->appendRow(items);
  9. }

重写了这个数据源后,我们主要是为了完成data的返回数据过程,View最关心的就是这个接口

  1. class RbTableHeaderModel : public QAbstractTableModel
  2. {
  3. Q_OBJECT
  4. public:
  5. // override
  6. virtual QVariant data(const QModelIndex &index, int role) const;
  7. private:
  8. // properties
  9. int row_count_prop;
  10. int column_count_prop;
  11. // inherent features
  12. RbTableHeaderItem* root_item;
  13. };

下面就是data的函数实现,是不是大失所望,所有的额操作好像被封装到RbTableHeaderItem这个节点中去了。

  1. QVariant RbTableHeaderModel::data(const QModelIndex & index, int role) const
  2. {
  3. if (!index.isValid())
  4. return QVariant();
  5. if (index.row() >= row_count_prop || index.row() < 0 || index.column() >= column_count_prop || index.column() < 0)
  6. return QVariant();
  7. RbTableHeaderItem * item = static_cast<RbTableHeaderItem *>(index.internalPointer());
  8. return item->data(role);
  9. }

RbTableHeaderItem结构表示了一个单元格,而且他还维护了所有的表格cell子节点。

注意看下面index的构造,把index和RbTableHeaderItem这个结构绑定在了一起。

index中的很多数据也都存储在了RbTableHeaderItem这个结构中,后续我们在讲视图的时候大家就会发现了。

Model也就这么多内动了,View才是我们的重头戏。

  1. QModelIndex RbTableHeaderModel::index(int row, int column, const QModelIndex & parent) const
  2. {
  3. RbTableHeaderItem * parentItem;
  4. if (!parent.isValid())
  5. parentItem = root_item; // parent item is always the root_item on table model
  6. else
  7. parentItem = static_cast<RbTableHeaderItem*>(parent.internalPointer()); // no effect
  8. RbTableHeaderItem * childItem = parentItem->child(row, column);
  9. if (!childItem)
  10. childItem = parentItem->insertChild(row, column);
  11. return
  12. createIndex(row, column, childItem);
  13. return QModelIndex();
  14. }

2、重写QHeaderView

重写表头时,公有接口用于设置单元格行高、列宽、背景色和前景色的,单元格合并等。

保护接口都是重写父类的方法,在合适的实际会被框架进行调用。

inherent features注释下的方法是自己封装的方法,方便其他函数调用。

  1. class RbTableHeaderView : public QHeaderView
  2. {
  3. void setRowHeight(int row, int rowHeight);
  4. void setColumnWidth(int col, int colWidth);
  5. void setSpan(int row, int column, int rowSpanCount, int columnSpanCount);
  6. void setCellBackgroundColor(const QModelIndex & index, const QColor &);
  7. void setCellForegroundColor(const QModelIndex & index, const QColor &);
  8. protected:
  9. // override
  10. virtual void mousePressEvent(QMouseEvent * event);
  11. virtual void paintSection(QPainter * painter, const QRect & rect, int logicalIndex) const;
  12. protected Q_SLOTS:
  13. void onSectionResized(int logicalIdx, int oldSize, int newSize);
  14. Q_SIGNALS:
  15. void sectionPressed(int from, int to);
  16. };

下面我们分析几个比较重要的函数

1、mousePressEvent鼠标按下

当鼠标按下时mousePressEvent函数被触发,然后我们需要去计算那个单元格被按下了,并通知视图,让视图去选择某些cell集合。

这个函数的处理逻辑会比较负责一些,这个dmeo做的有问题,这里我就不按照demo中的代码来讲解了。

首先我们还是得根据自己的需求来实现这个鼠标按下事件,对于大多数的程序来说,可能都是鼠标按下时,选中视图中的单元格集合,那么我们这里也就按照这个思路来分析。

如下图所示,我们在程序加载过程中,给表头头设置了合并属性,对于合并了的表格项,他们对象的index中都是存储了红色文字信息的。

当我们点击了某一个item时,程序就需要去判断是否点击了这个大的合并sell,然后去选择tableview视图上的cell集合。

就是这么简单,但是实现起来还是有一定难度的。

思路就到这里了,具体逻辑大家可以去思考下。

2、paintSection绘制函数

UI上真正的绘制函数其实就是paintSection函数,当这个函数回调的时候,我们只需要在程序给定的区域内绘制上文本即可,那么问题来了,这个区域是这么计算出来的,既然我们要合并了列和行,那么每一个区域的大小应该都是不一样的。

分析的一点都没错,这个区域的大小Qt已经帮我们留好了接口--sectionSizeFromContents

我们只需要重写这个函数即可,根据我们之前保存的index上合并列和行的数据进行计算,计算出一个合适的区域,然后把值返回即可。

  1. QSize RbTableHeaderView::sectionSizeFromContents(int logicalIndex) const
  2. {
  3. const RbTableHeaderModel * tblModel = qobject_cast<const RbTableHeaderModel*>(this->model());
  4. const int OTN = orientation();
  5. const int LEVEL_CNT = (OTN == Qt::Horizontal) ? tblModel->rowCount() : tblModel->columnCount();
  6. QSize siz = QHeaderView::sectionSizeFromContents(logicalIndex);
  7. for (int i = 0; i < LEVEL_CNT; ++i)
  8. {
  9. QModelIndex cellIndex = (OTN == Qt::Horizontal) ? tblModel->index(i, logicalIndex) : tblModel->index(logicalIndex, i);
  10. QModelIndex colSpanIdx = columnSpanIndex(cellIndex);
  11. QModelIndex rowSpanIdx = rowSpanIndex(cellIndex);
  12. siz = cellIndex.data(Qt::SizeHintRole).toSize();
  13. if (colSpanIdx.isValid())
  14. {
  15. int colSpanFrom = colSpanIdx.column();
  16. int colSpanCnt = colSpanIdx.data(COLUMN_SPAN_ROLE).toInt();
  17. int colSpanTo = colSpanFrom + colSpanCnt - 1;
  18. siz.setWidth(columnSpanSize(colSpanIdx.row(), colSpanFrom, colSpanCnt));
  19. if (OTN == Qt::Vertical) i = colSpanTo;
  20. }
  21. if (rowSpanIdx.isValid())
  22. {
  23. int rowSpanFrom = rowSpanIdx.row();
  24. int rowSpanCnt = rowSpanIdx.data(ROW_SPAN_ROLE).toInt();
  25. int rowSpanTo = rowSpanFrom + rowSpanCnt - 1;
  26. siz.setHeight(rowSpanSize(rowSpanIdx.column(), rowSpanFrom, rowSpanCnt));
  27. if (OTN == Qt::Horizontal) i = rowSpanTo;
  28. }
  29. }
  30. return siz;
  31. }

3、列大小改变

当手动拖拽列带下时,onSectionResized槽函数会被调用,然后我们需要在这个函数中把相邻的列头大小进行重新设置。

  1. void RbTableHeaderView::onSectionResized(int logicalIndex, int oldSize, int newSize)
  2. {
  3. for (int i = 0; i < LEVEL_CNT; ++i)
  4. {
  5. QSize cellSize = cellIndex.data(Qt::SizeHintRole).toSize();
  6. // set position of cell
  7. if (OTN == Qt::Horizontal)
  8. {
  9. sectionRect.setTop(rowSpanSize(logicalIndex, 0, i));
  10. cellSize.setWidth(newSize);
  11. }
  12. else
  13. {
  14. sectionRect.setLeft(columnSpanSize(logicalIndex, 0, i));
  15. cellSize.setHeight(newSize);
  16. }
  17. tblModel->setData(cellIndex, cellSize, Qt::SizeHintRole);
  18. QRect rToUpdate(sectionRect);
  19. rToUpdate.setWidth(viewport()->width() - sectionRect.left());
  20. rToUpdate.setHeight(viewport()->height() - sectionRect.top());
  21. viewport()->update(rToUpdate.normalized());
  22. }
  23. }

大致的实现思路就是这样的,由于核心实现代码逻辑比较长,大多数的代码我只保留了关键的执行步骤。

四、设置属性

下面这一大堆代码看似很长,其实很好理解,就是调用我们封装好的控件进行设置

  1. 第一段设置了水平表头合并和内容
  2. 第二段设置了垂直表头合并和内容
  3. 第三段设置水平表头行高
  4. 第四段设置了垂直表头列宽和行高
  5. 第五段设置水平和垂直表头可点击
  6. 第六段设置水平和垂直表头背景色
  1. hHead->setSpan(0, 0, 3, 0);
  2. hHead->setSpan(0, 1, 2, 2);
  3. hHead->setSpan(1, 3, 2, 0);
  4. hModel->setData(hModel->index(0, 0), QStringLiteral("一级表头"), Qt::DisplayRole);
  5. hModel->setData(hModel->index(0, 1), QStringLiteral("一级表头"), Qt::DisplayRole);
  6. hModel->setData(hModel->index(2, 1), QStringLiteral("二级表头"), Qt::DisplayRole);
  7. hModel->setData(hModel->index(2, 2), QStringLiteral("二级表头"), Qt::DisplayRole);
  8. hModel->setData(hModel->index(0, 3), QStringLiteral("一级表头"), Qt::DisplayRole);
  9. hModel->setData(hModel->index(1, 3), QStringLiteral("二级表头"), Qt::DisplayRole);
  10. vHead->setSpan(0, 0, 0, 3);
  11. vHead->setSpan(1, 0, 3, 0);
  12. vHead->setSpan(1, 1, 2, 0);
  13. vModel->setData(vModel->index(0, 0), QStringLiteral("一级表头"), Qt::DisplayRole);
  14. vModel->setData(vModel->index(1, 0), QStringLiteral("一级表头"), Qt::DisplayRole);
  15. vModel->setData(vModel->index(1, 1), QStringLiteral("二级表头"), Qt::DisplayRole);
  16. vModel->setData(vModel->index(3, 1), QStringLiteral("二级表头"), Qt::DisplayRole);
  17. vModel->setData(vModel->index(1, 2), QStringLiteral("三级表头"), Qt::DisplayRole);
  18. vModel->setData(vModel->index(2, 2), QStringLiteral("三级表头"), Qt::DisplayRole);
  19. vModel->setData(vModel->index(3, 2), QStringLiteral("三级表头"), Qt::DisplayRole);
  20. hHead->setRowHeight(0, 30);
  21. hHead->setRowHeight(1, 30);
  22. hHead->setRowHeight(2, 30);
  23. vHead->setRowHeight(0, 30);
  24. vHead->setRowHeight(1, 30);
  25. vHead->setRowHeight(2, 30);
  26. vHead->setColumnWidth(0, 50);
  27. vHead->setColumnWidth(1, 50);
  28. vHead->setColumnWidth(2, 50);
  29. hHead->setSectionsClickable(true);
  30. vHead->setSectionsClickable(true);
  31. hHead->setCellBackgroundColor(hModel->index(0, 0), 0xcfcfcf);
  32. hHead->setCellBackgroundColor(hModel->index(0, 1), 0xcfcfcf);
  33. vHead->setCellBackgroundColor(vModel->index(0, 0), Qt::cyan);
  34. vHead->setCellBackgroundColor(vModel->index(1, 0), 0xcfcfcf);

这个demo网上也可以下载到,或者去我文章开头留下的开源库地址去拿也行。

现在的CSDN不敢恭维了,代码也没地方放了。代码如果下载不到也可以留言我来发。。。

五、相关文章

  1. Qt实现表格树控件-支持多级表头
  2. Qt实现高仿excel表格-可执行文件(源码不开放)
  3. Qt高仿Excel表格组件-支持冻结列、冻结行、内容自适应和合并单元格
  4. 牛逼哄哄的Qt库
  5. openDesktop
如果您觉得文章不错,不妨给个打赏,写作不易,感谢各位的支持。您的支持是我最大的动力,谢谢!!!

很重要--转载声明

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

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


Qt实现表格控件-支持多级列表头、多级行表头、单元格合并、字体设置等的更多相关文章

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

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

  2. javascript - 可编辑表格控件 支持全键盘操作(无JS框架)

    项目中经常会用到表格编辑控件,网上也有不少,但是确实没有完全符合我要求的, 自己写一个吧! 1.该控件支持 数据显示列,文本编辑列,选择列,下拉列,索引列,删除列 六种列类型 2.支持全键盘操作,自定 ...

  3. Qt之表格控件蚂蚁线

    一.蚂蚁线 摘自互动百科:在图像影像软件中表示选区的动态虚线,因为虚线闪烁的样子像是一群蚂蚁在跑,所以俗称蚂蚁线.在Poshop,After Effect等软件中比较常见. 背景:用过excel的同学 ...

  4. devexpress表格控件gridcontrol图片列,按钮列,时间列等特殊列的实现

    1.项目中经常会在表格中插入按钮列,图片列,表格列一些非文本的特殊列.如何在devexpress表格控件gridcontrol中实现呢?以下列举一个实现添加图片列,按钮列,时间列,按钮列,开关列的示例 ...

  5. qt tableWidget 表格控件使用

    //创建表格头 (灰色冻结状态的) QStringList header; header<<"Time"<<" ID "<< ...

  6. Qt实现表格树控件-支持多级表头

    目录 一.概述 二.效果展示 三.实现方式 四.多级表头 1.数据源 2.表格 3.QStyledItemDelegate绘制代理 五.测试代码 六.相关文章 原文链接:Qt实现表格树控件-支持多级表 ...

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

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

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

    目录 一.概述 二.效果展示 三.实现思路 1.冻结行.冻结列 2.行高自适应 3.蚂蚁线 四.测试代码 1.添加表格数据 2.设置冻结行.列 3.行高.列宽 4.单元格背景色 5.单元格文字 6.其 ...

  9. QRowTable表格控件(二)-红涨绿跌

    目录 一.开心一刻 二.概述 三.效果展示 四.任务需求 五.指定列排序 六.排序 七.列对其方式 八.相关文章 原文链接:QRowTable表格控件(二)-红涨绿跌 一.开心一刻 一天,五娃和六娃去 ...

随机推荐

  1. [Android] 环境优化配置Android Studio发展NDK

    ======================================================== 作者:qiujuer 博客:blog.csdn.net/qiujuer 站点:www. ...

  2. WPF ContextMenu 在MVVM模式中绑定 Command及使用CommandParameter传参

    原文:WPF ContextMenu 在MVVM模式中绑定 Command及使用CommandParameter传参 ContextMenu无论定义在.cs或.xaml文件中,都不继承父级的DataC ...

  3. https://www.jianshu.com/p/4da29fa310d2

    wampserver显示红色.橙色的解决方案   拿笔的小鑫 关注 2016.10.15 14:38* 字数 2643 阅读 5083评论 0喜欢 3赞赏 1 </br></br&g ...

  4. WPF 用代码调用dynamic resource动态更改背景 - CSDN博客

    原文:WPF 用代码调用dynamic resource动态更改背景 - CSDN博客 一般dynamic resoource通常在XAML里调用,如下范例: <Button Click=&qu ...

  5. UWP使用AppService向另一个UWP客户端应用程序提供服务

    原文:UWP使用AppService向另一个UWP客户端应用程序提供服务 在上篇里,我使用的是寄宿在WPF上的WCF进行两个程序间的通信,在解决问题的同时,我的同事也在思考能否使用UWP来做这件事.于 ...

  6. UWP开发学习笔记3

    获取可视化状态列表 private void Button_Click(object sender, RoutedEventArgs e) { //获取CheckBox控件可视化树中的子元素数量 in ...

  7. Win10的UWP之标题栏的返回键(二)

    原文:Win10的UWP之标题栏的返回键(二) 关于Win10的UWP的返回键的第二种处理的方法,是介于标题栏的强行修改,不是像上期的那样直接调用系统内置的API. - - - - - - - - - ...

  8. ML:吴恩达 机器学习 课程笔记(Week3~4)

    Logistic Regression Regularization Neural Networks: Representation

  9. 纯CSS3创意loading文字特效

    快速使用Romanysoft LAB的技术实现 HTML 开发Mac OS App,并销售到苹果应用商店中.   <HTML开发Mac OS App 视频教程> 土豆网同步更新:http: ...

  10. Qt:移动无边框窗体(使用Windows的SendMessage)

    移动无边框窗体的代码网上很多,其原理都是一样的,但是是有问题的,我这里只是对其修正一下 网上的代码仅仅实现了两个事件 void EditDialog::mousePressEvent(QMouseEv ...