Qt实现表格控件

news2024/11/19 1:23:09

一、概述

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

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

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

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

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

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

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

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

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

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

二、效果展示

多级表头的效果下图所示,很糙粗的一个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_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();
}

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

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

四、设置属性

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

  1. 第一段设置了水平表头合并和内容
  2. 第二段设置了垂直表头合并和内容
  3. 第三段设置水平表头行高
  4. 第四段设置了垂直表头列宽和行高
  5. 第五段设置水平和垂直表头可点击
  6. 第六段设置水平和垂直表头背景色
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);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/112698.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络空间安全——利用 CVE-2017-0213 提权

利用 CVE-2017-0213 提权 VE-2017-0213 是一个比较冷门的COM 类型混淆 (Type Confusion)漏洞。巧妙的利用该漏洞&#xff0c;可以实现本地的提权。该漏洞由著名的Google Project zero 发现。 下面就简单演示一下利用CVE-2017-0213漏洞简单提权&#xff0c; 首先下载CVE-2017…

【环境搭建】RocketMQ集群搭建

前置条件及效果图 条件&#xff1a; 两台服务器&#xff0c;个人是两台腾讯云服务器(其中嫖的朋友一个)&#xff1b; 版本&#xff1a; rocketmq-version:4.4.0rocketmq-console(mq控制台)Java&#xff1a;1.8maven:3.6.3 集群模式选择&#xff1a; 单master 这种方式风险…

【Django】第三课 基于Django超市订单管理系统开发

概念 本文在上一文之上&#xff0c;针对管理员&#xff0c;经理&#xff0c;普通员工身份的用户操作供应商管理模块功能。 功能实现 供应商管理模块属于业务功能&#xff0c;这里管理员不具备操作权限&#xff0c;而经理具备与供应商之间谈合作的实际需要&#xff0c;因此经…

Linux | 进程理解,fork | 进程地址空间

文章目录冯诺依曼体系结构的理解为什么要有内存的存在&#xff1f;操作系统的管理进程的理解系统调用接口进程的查看fork进程状态Linux进程具体的状态孤儿进程总结进程优先级怎样修改优先级&#xff1f;进程其他概念进程抢占进程地址空间利用代码验证地址区域验证堆区和栈区的增…

python3GUI--音乐播放器(精简版)By:PyQt5(附下载地址)

文章目录一&#xff0e;前言二&#xff0e;预览1.主界面2.歌单页3.歌词页4.播放列表5.mini6.设置三&#xff0e;心得1.解耦2.体验优化3.歌词显示四&#xff0e;总结一&#xff0e;前言 传送门&#xff1a; 1.python3GUI–打造一款音乐播放器By:PyQt5&#xff08;附下载地址&am…

LD_PRELOAD劫持

在前面UUCTF的uploadinject题&#xff0c;遇到了 LD_PRELOAD劫持&#xff0c;之前没遇见过&#xff0c;刚好借此机会学一学。不能小瞧这个变量&#xff0c;它甚至可以弹shell&#xff0c;绕过disable_functions&#xff0c;非常危险。下面来介绍一下这个变量&#xff0c;以及怎…

XDocReport使用入门

XDocReport 简介 XDocReport是GitHub上根据麻省理工学院许可证开源的Wrod导出框架。XDocReport可以根据ODT、Doc、Docx文档模板通过模板引擎语法&#xff08;Freemarker、Velocity&#xff09;转换为另外一种格式文档&#xff08;Doc、Docx、XHTML、PDF&#xff09;。 XDocR…

防沉迷管理系统

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 1、用户模块 1.1注册&#xff1a;用户通过注册生产账号&#xff0c;并在数据库存储数据 1.2登录&#xff1a;用户登录后…

k8s学习-CKA真题-k8s升级(kubeadm、kubelet、kubectl等)

目录题目解析命令准备工作升级组件升级kubectl、kubelet收尾结果killer 模拟环境题目解析解题参考题目 解析 结合博主当前环境&#xff0c;调整后题目为&#xff1a; 现有的 Kubernetes 集权正在运行的版本是 1.23.6&#xff0c;仅将主节点上的所有 kubernetes 控制面板和组件…

【语音处理】基于加权压力匹配方法(WPMM)的声音系统研究(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

【小程序】案例 - 本地生活(列表页面)

1. 演示页面效果以及主要功能 页面导航并传参 上拉触底时加载下一页数据 下拉刷新列表数据 2. 列表页面的 API 接口 以分页的形式&#xff0c;加载指定分类下商铺列表的数据&#xff1a; 接口地址 https://www.escook.cn/categories/:cate_id/shops URL 地址中的 :cate…

博泰应宜伦:智能汽车上攻时刻,需要“国家级”平台登场

作者 | 张祥威 编辑 | 王博汽车智能化转型的道路上&#xff0c;有个问题可能并非杞人忧天&#xff0c;而是值得整个行业警醒的。那就是&#xff1a; 中国的智能汽车发展&#xff0c;是否会被国外“卡脖子”&#xff1f; 卡脖子的担忧&#xff0c;其实也可以理解为&#xff0c;中…

【Linux】shell及其运行原理

目录1.什么是shell2.shell的功能3.shell的感性理解4.为什么不安装图形化界面1.什么是shell shell &#xff1a; 操作系统内核的外壳 通常来讲&#xff0c;计算机硬件是由运算器、控制器、存储器、输入/输出设备等硬件共同组成的&#xff0c;而让各种硬件设备各司其职且能协同运…

【基础强训】day3

一、选择题 &#x1f4a6;第1题&#xff1a; 以下程序的输出结果是&#xff08;&#xff09; #include <stdio.h> main() { char a[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, *p; int i; i 8; p a i; printf("%s\n", p - 3); } A 6 B 6789 C 6 D 789 B 先定义…

阳了之后,python实用工具之:疫情信息快速查看

嗨害大家好鸭&#xff01;我是小熊猫&#xff01;&#xff08;阳了个阳版&#xff09; &#xff08;先给大家消消毒&#xff09; 不好意思&#xff0c;很久没给大家更新了… 主要是小熊猫我不小心阳了… 大家要多注意自己的身体健康 多喝点热水 这个真的是个好东西 在家里稍…

深度学习—00入门 神经网络 pytorch

1、深度学习简介 深度学习是机器学习的一个分支&#xff0c;简单来说就是通过人工神经网络&#xff0c;强行在业务的 输入 和 输出 之间&#xff0c;暴力耦合一个出一个数学模型。 1.1 深度学习特点 1、由于是暴力耦合出来的模型&#xff0c;自然模型可解释性很差&#xff0c…

STM32 cubeMX配置OLED

文章目录前言一、OLED的接线二、cubeMX的配置三、OLED的驱动代码四、OLED的使用总结前言 本篇文章主要介绍OLED的操作和使用。 一、OLED的接线 OLED一共有四根线这里我使用的是IIC的OLED显示屏。 VCC----3.3V或者5V GND-----GND SDA-----PB7 SCL-----PB6 二、cubeMX的配置 …

TFT-LCD屏幕触摸校准

TFT-LCD屏幕触摸校准 触摸屏幕分类 1.电阻屏 电阻屏的主要部分是一块与显示器表面配合非常好的电阻薄膜屏&#xff0c;这是一种多层的复合薄膜&#xff0c;由一层玻璃或有机玻璃作为基层&#xff0c;表面涂有一层秀明的导电层&#xff0c;上面再盖有一层外表硬化处理、光滑防…

通达信量化接口需要被程序化执行吗?

其实通达信量化接口其实是量化交易模型的需要被执行的一种方式&#xff0c;但如果你交易者的策略模型采用比较中低频的交易执行方式&#xff0c;比如每天只交易一次&#xff0c;甚至每周或每月才轮动一次。那么小编认为通达信量化接口建议是否程序化执行也是一个非常重要的指标…

UI自动化测试-selenium元素定位

在使用Selenium和WebDriver进行UI自动化测试时&#xff0c;我们首先需要对元素定位&#xff0c;那么如何来定位元素呢&#xff1f; HTML 在进行元素定位之前&#xff0c;我们要对html代码有所了解。 <div classs_form><div classs_form_wrapper><div idlg>…