原文连接:Qt实现表格控件-支持多级列表头、多级行表头、单元格合并、字体设置等app
最近在研究QTableView支持多级表头的事情,百度了下网上资料仍是挺多的。实现的方式总的来讲有2种,效果都还不错,最主要是搞懂其中的原理,作到以不变应万变。框架
实现多级表头的方式有如下两种方案ide
以上两种方式均可以实现多级表头,各有利弊,而且已经有人投入项目使用。函数
我我的仍是比较偏向于第二种方式,由于这样咱们才能够更好的了解Qt的底层,了解Qt的绘图机制,而且这样实现的效率也是比较高的,并且合理一些,比较可控(我的理解)。学习
后来我在网上找到了一个哥们写的控件,项目名字叫作RbTableHeaderView,挺不错的,能够实现咱们要的功能,可是效果仍是差一些,若是须要更友好的交互效果,那么还须要在继续完善这个demo。字体
今天闲来无事,找到了一个开源的网站,上边好多Qt的库,虽然有一些是很早之前的东西,可是也很值得咱们去学习。为何会提到这个网站呢?由于这个网站上就有咱们要的这个多级表头事例,和上边提到的那个哥们的事例不谋而合。网站
想要学习更多开源事例的能够到openDesktop上去看看。还有我本身收录的牛逼哄哄的Qt库this
下面咱们就来说解这个多节表头的实现方式,代码比较简单,主要是你们理解下这个实现方式,能够加以扩展。spa
后续的文章中我会在写一篇关于树控件多级表头的事例,这里先把文章名称挂载这里,后续发布后就能够看到--Qt实现表格树控件-支持多级表头
多级表头的效果下图所示,很糙粗的一个demo,你们将就着看吧。
定制表头咱们主要是要重写2个东西,分别是数据源QAbstractTableModel和表头QHeaderView
数据源就是为视图提供数据的model,咱们的全部显示的内容数据都来自这个model。
对于外部程序填充数据时和往常使用一样的方式
for (int i = 0; i < 10; i++) { QList<QStandardItem*> items; for (int j = 0; j < 8; j++) { items.append(new QStandardItem(QString("item(%1, %2)").arg(i).arg(j))); } dataModel->appendRow(items); }
重写了这个数据源后,咱们主要是为了完成data的返回数据过程,View最关心的就是这个接口
class RbTableHeaderModel : public QAbstractTableModel { Q_OBJECT public: // override virtual QVariant data(const QModelIndex &index, int role) const; private: // properties int row_count_prop; int column_count_prop; // inherent features RbTableHeaderItem* root_item; };
下面就是data的函数实现,是否是大失所望,全部的额操做好像被封装到RbTableHeaderItem这个节点中去了。
QVariant RbTableHeaderModel::data(const QModelIndex & index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() >= row_count_prop || index.row() < 0 || index.column() >= column_count_prop || index.column() < 0) return QVariant(); RbTableHeaderItem * item = static_cast<RbTableHeaderItem *>(index.internalPointer()); return item->data(role); }
RbTableHeaderItem结构表示了一个单元格,并且他还维护了全部的表格cell子节点。
注意看下面index的构造,把index和RbTableHeaderItem这个结构绑定在了一块儿。
index中的不少数据也都存储在了RbTableHeaderItem这个结构中,后续咱们在讲视图的时候你们就会发现了。
Model也就这么多内动了,View才是咱们的重头戏。
QModelIndex RbTableHeaderModel::index(int row, int column, const QModelIndex & parent) const { RbTableHeaderItem * parentItem; if (!parent.isValid()) parentItem = root_item; // parent item is always the root_item on table model else parentItem = static_cast<RbTableHeaderItem*>(parent.internalPointer()); // no effect RbTableHeaderItem * childItem = parentItem->child(row, column); if (!childItem) childItem = parentItem->insertChild(row, column); return createIndex(row, column, childItem); return QModelIndex(); }
重写表头时,公有接口用于设置单元格行高、列宽、背景色和前景色的,单元格合并等。
保护接口都是重写父类的方法,在合适的实际会被框架进行调用。
inherent features
注释下的方法是本身封装的方法,方便其余函数调用。
class RbTableHeaderView : public QHeaderView { void setRowHeight(int row, int rowHeight); void setColumnWidth(int col, int colWidth); void setSpan(int row, int column, int rowSpanCount, int columnSpanCount); void setCellBackgroundColor(const QModelIndex & index, const QColor &); void setCellForegroundColor(const QModelIndex & index, const QColor &); protected: // override virtual void mousePressEvent(QMouseEvent * event); virtual void paintSection(QPainter * painter, const QRect & rect, int logicalIndex) const; protected Q_SLOTS: void onSectionResized(int logicalIdx, int oldSize, int newSize); Q_SIGNALS: void sectionPressed(int from, int to); };
下面咱们分析几个比较重要的函数
一、mousePressEvent鼠标按下
当鼠标按下时mousePressEvent函数被触发,而后咱们须要去计算那个单元格被按下了,并通知视图,让视图去选择某些cell集合。
这个函数的处理逻辑会比较负责一些,这个dmeo作的有问题,这里我就不按照demo中的代码来说解了。
首先咱们仍是得根据本身的需求来实现这个鼠标按下事件,对于大多数的程序来讲,可能都是鼠标按下时,选中视图中的单元格集合,那么咱们这里也就按照这个思路来分析。
以下图所示,咱们在程序加载过程当中,给表头头设置了合并属性,对于合并了的表格项,他们对象的index中都是存储了红色文字信息的。
当咱们点击了某一个item时,程序就须要去判断是否点击了这个大的合并sell,而后去选择tableview视图上的cell集合。
就是这么简单,可是实现起来仍是有必定难度的。
思路就到这里了,具体逻辑你们能够去思考下。
二、paintSection绘制函数
UI上真正的绘制函数其实就是paintSection函数,当这个函数回调的时候,咱们只须要在程序给定的区域内绘制上文本便可,那么问题来了,这个区域是这么计算出来的,既然咱们要合并了列和行,那么每个区域的大小应该都是不同的。
分析的一点都没错,这个区域的大小Qt已经帮咱们留好了接口--sectionSizeFromContents
咱们只须要重写这个函数便可,根据咱们以前保存的index上合并列和行的数据进行计算,计算出一个合适的区域,而后把值返回便可。
QSize RbTableHeaderView::sectionSizeFromContents(int logicalIndex) const { const RbTableHeaderModel * tblModel = qobject_cast<const RbTableHeaderModel*>(this->model()); const int OTN = orientation(); const int LEVEL_CNT = (OTN == Qt::Horizontal) ? tblModel->rowCount() : tblModel->columnCount(); QSize siz = QHeaderView::sectionSizeFromContents(logicalIndex); for (int i = 0; i < LEVEL_CNT; ++i) { QModelIndex cellIndex = (OTN == Qt::Horizontal) ? tblModel->index(i, logicalIndex) : tblModel->index(logicalIndex, i); QModelIndex colSpanIdx = columnSpanIndex(cellIndex); QModelIndex rowSpanIdx = rowSpanIndex(cellIndex); siz = cellIndex.data(Qt::SizeHintRole).toSize(); if (colSpanIdx.isValid()) { int colSpanFrom = colSpanIdx.column(); int colSpanCnt = colSpanIdx.data(COLUMN_SPAN_ROLE).toInt(); int colSpanTo = colSpanFrom + colSpanCnt - 1; siz.setWidth(columnSpanSize(colSpanIdx.row(), colSpanFrom, colSpanCnt)); if (OTN == Qt::Vertical) i = colSpanTo; } if (rowSpanIdx.isValid()) { int rowSpanFrom = rowSpanIdx.row(); int rowSpanCnt = rowSpanIdx.data(ROW_SPAN_ROLE).toInt(); int rowSpanTo = rowSpanFrom + rowSpanCnt - 1; siz.setHeight(rowSpanSize(rowSpanIdx.column(), rowSpanFrom, rowSpanCnt)); if (OTN == Qt::Horizontal) i = rowSpanTo; } } return siz; }
三、列大小改变
当手动拖拽列带下时,onSectionResized槽函数会被调用,而后咱们须要在这个函数中把相邻的列头大小进行从新设置。
void RbTableHeaderView::onSectionResized(int logicalIndex, int oldSize, int newSize) { for (int i = 0; i < LEVEL_CNT; ++i) { QSize cellSize = cellIndex.data(Qt::SizeHintRole).toSize(); // set position of cell if (OTN == Qt::Horizontal) { sectionRect.setTop(rowSpanSize(logicalIndex, 0, i)); cellSize.setWidth(newSize); } else { sectionRect.setLeft(columnSpanSize(logicalIndex, 0, i)); cellSize.setHeight(newSize); } tblModel->setData(cellIndex, cellSize, Qt::SizeHintRole); QRect rToUpdate(sectionRect); rToUpdate.setWidth(viewport()->width() - sectionRect.left()); rToUpdate.setHeight(viewport()->height() - sectionRect.top()); viewport()->update(rToUpdate.normalized()); } }
大体的实现思路就是这样的,因为核心实现代码逻辑比较长,大多数的代码我只保留了关键的执行步骤。
下面这一大堆代码看似很长,其实很好理解,就是调用咱们封装好的控件进行设置
hHead->setSpan(0, 0, 3, 0); hHead->setSpan(0, 1, 2, 2); hHead->setSpan(1, 3, 2, 0); hModel->setData(hModel->index(0, 0), QStringLiteral("一级表头"), Qt::DisplayRole); hModel->setData(hModel->index(0, 1), QStringLiteral("一级表头"), Qt::DisplayRole); hModel->setData(hModel->index(2, 1), QStringLiteral("二级表头"), Qt::DisplayRole); hModel->setData(hModel->index(2, 2), QStringLiteral("二级表头"), Qt::DisplayRole); hModel->setData(hModel->index(0, 3), QStringLiteral("一级表头"), Qt::DisplayRole); hModel->setData(hModel->index(1, 3), QStringLiteral("二级表头"), Qt::DisplayRole); vHead->setSpan(0, 0, 0, 3); vHead->setSpan(1, 0, 3, 0); vHead->setSpan(1, 1, 2, 0); vModel->setData(vModel->index(0, 0), QStringLiteral("一级表头"), Qt::DisplayRole); vModel->setData(vModel->index(1, 0), QStringLiteral("一级表头"), Qt::DisplayRole); vModel->setData(vModel->index(1, 1), QStringLiteral("二级表头"), Qt::DisplayRole); vModel->setData(vModel->index(3, 1), QStringLiteral("二级表头"), Qt::DisplayRole); vModel->setData(vModel->index(1, 2), QStringLiteral("三级表头"), Qt::DisplayRole); vModel->setData(vModel->index(2, 2), QStringLiteral("三级表头"), Qt::DisplayRole); vModel->setData(vModel->index(3, 2), QStringLiteral("三级表头"), Qt::DisplayRole); hHead->setRowHeight(0, 30); hHead->setRowHeight(1, 30); hHead->setRowHeight(2, 30); vHead->setRowHeight(0, 30); vHead->setRowHeight(1, 30); vHead->setRowHeight(2, 30); vHead->setColumnWidth(0, 50); vHead->setColumnWidth(1, 50); vHead->setColumnWidth(2, 50); hHead->setSectionsClickable(true); vHead->setSectionsClickable(true); hHead->setCellBackgroundColor(hModel->index(0, 0), 0xcfcfcf); hHead->setCellBackgroundColor(hModel->index(0, 1), 0xcfcfcf); vHead->setCellBackgroundColor(vModel->index(0, 0), Qt::cyan); vHead->setCellBackgroundColor(vModel->index(1, 0), 0xcfcfcf);
这个demo网上也能够下载到,或者去我文章开头留下的开源库地址去拿也行。
如今的CSDN不敢恭维了,代码也没地方放了。代码若是下载不到也能够留言我来发。。。
![]() |
![]() |
很重要--转载声明