龙空技术网

Qt实现表格控件-支持多级列表头、多级行表头、单元格合并等

C加加Qt技术开发老杰 435

前言:

现在大家对“二级表头制作”都比较看重,看官们都需要分析一些“二级表头制作”的相关内容。那么小编同时在网摘上汇集了一些对于“二级表头制作””的相关内容,希望各位老铁们能喜欢,咱们快快来学习一下吧!

一、概述

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

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

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

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

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

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

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

想要学习更多开源事例的可以到openDesktop上去看看。

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

二、效果展示

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

三、定制表头

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

1、重写数据源

数据源就是为视图提供数据的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_OBJECTpublic:	// 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();}
2、重写QHeaderView

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

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

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);};

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

1、mousePressEvent鼠标按下

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

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

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

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

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

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

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

2、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;}

3、列大小改变

当手动拖拽列带下时,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);

Qt学习路线:Qt开发技术栈

资料领取:「链接」

标签: #二级表头制作 #vs中table控件怎么分行和列