QTableView表格控件区域选择-自绘选择区域

news2024/11/17 19:08:25

一、概述

最近优化了一个小功能,主要是模仿excel相关的操作,觉得还挺不错的,因此在这里进行了整理,分享给有需要的朋友。今天主要是说一下区域选择这项功能,Qt自带的表格控件是具有区域选择功能的,但是他并不美观,不能支持我们自定义边框色和一些细节上的调整。

今天博主就来讲解下自己是怎么自定义这个区域选择功能的。

主要使用的方式还是自绘,下面先来看下效果,是不是你想要的。

二、效果展示

如下图所示,是一个自绘选择区域的效果展示,除此之外demo中还有一些其他的效果,但不是本篇文章所要讲述的内容。

本篇文章的重点就是讲述怎么实现区域选择框绘制

三、实现思路

看过效果图之后,接下来开始分析怎么绘制矩形选择框。下面以问题的形式来进行分析,这样更有利于理解。

那么先来思考如下几个很问题

  1. 怎么确定绘制区域
  2. 怎么确定绘制的边框
  3. 谁去绘制更好

以上三个问题搞懂了,那么今天的主要内容也就差不多了。

1、绘制区域

学习Qt的第一步便是看帮助文档,不得不说Qt的帮助文档那是做的相当好,非常齐全。既然如此那还等什么,直接打开Qt 助手看看如下几个类都有哪些信号把。

QTableView

//QAbstractItemView
void activated(const QModelIndex &index)
void clicked(const QModelIndex &index)
void doubleClicked(const QModelIndex &index)
void entered(const QModelIndex &index)
void iconSizeChanged(const QSize &size)
void pressed(const QModelIndex &index)
void viewportEntered()

QTableView是表格控件基类,我们的表格也是基于这个控件进行开发。再看这个类的包含的信号(其中都是他的父窗口信号),对于本小结开始提出的3个问题好像没有特别大的作用。那么我们继续往下看,看看他的数据存储类。

QStandardItemModel

void itemChanged(QStandardItem *item)

//parent QAbstractItemModel

void columnsAboutToBeInserted(const QModelIndex &parent, int first, int last)
void columnsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationColumn)
void columnsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void columnsInserted(const QModelIndex &parent, int first, int last)
void columnsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int column)
void columnsRemoved(const QModelIndex &parent, int first, int last)
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int> ())
void headerDataChanged(Qt::Orientation orientation, int first, int last)
void layoutAboutToBeChanged(const QList<QPersistentModelIndex> &parents = QList<QPersistentModelIndex> (), QAbstractItemModel::LayoutChangeHint hint = QAbstractItemModel::NoLayoutChangeHint)
void layoutChanged(const QList<QPersistentModelIndex> &parents = QList<QPersistentModelIndex> (), QAbstractItemModel::LayoutChangeHint hint = QAbstractItemModel::NoLayoutChangeHint)
void modelAboutToBeReset()
void modelReset()
void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
void rowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row)
void rowsRemoved(const QModelIndex &parent, int first, int last)

QStandardItemModel便是QTableView的数据模型了,一眼扫过好像都是模型数据发生变化了的一些信号。这个时候发现M和V好像没有我们需要的东西,Qt不会真这么挫吧。答案当然是“否”,仔细翻阅Qt的帮助文档就会发现QAbstractItemView类可以返回一个selectionModel,看其名字好像是我们需要的东西。

QItemSelectionModel * selectionModel() const

随继续翻阅帮助文档,我们得到以下信息

void currentChanged(const QModelIndex &current, const QModelIndex &previous)
void currentColumnChanged(const QModelIndex &current, const QModelIndex &previous)
void currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
void modelChanged(QAbstractItemModel *model)
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)

哈哈哈,果然找到了我们需要的信号,看信号名称就知道,当前项发生变化时触发,然后我们就可以去统计哪些项被选中。

到这里,我们的第一个问题就算回答了,我们可以通过selectionModel的selectionChanged信号来统计可能需要绘制border的单元格。

//连接信号
connect(m_pVew->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ExcTableWidget::SelectionChanged);

2、绘制边框

信号连接上后,开始处理信号。

思路大致是这样的:

  1. 使用gridCell记录所有的单元格
  2. 循环遍历选中的单元格
  3. 判断当前单元格哪个边是需要绘制的
  4. 结果存储于gridPosints结构中

判断逻辑也比较简单,逻辑比较简单,可以直接看代码。这里我举一个例子,比如说是否需要绘制左border,那么就是需要看这个cell左边是否有cell,或者自己已经是第一列。

gridPosints是QMap<QModelIndex, QVector>类型,键存储单元格索引,值存储4个边的状态(是否需要绘制)

void ExcTableWidget::SelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
	QModelIndexList indexs = m_pVew->selectionModel()->selectedIndexes();

	qDebug() << indexs;

	int row = GetModel()->rowCount();
	int column = GetModel()->columnCount();

	QVector<QVector<bool>> gridCell(row, QVector<bool>(column));

	for each (const QModelIndex & index in indexs)
	{
		gridCell[index.row()][index.column()] = true;
	}

	QMap<QModelIndex, DrawTypes> datas;
	QMap<QModelIndex, QVector<GridPoint>> gridPosints;
	for each (const QModelIndex & index in indexs)
	{
		DrawTypes types;
		bool topLine = true, rightLine = true, bottomLine = true, leftLine = true;
		if (index.row() == 0)
		{
			types |= TOP;
		}
		else
		{
			int aboveCell = index.row() - 1;
			if (gridCell[aboveCell][index.column()] == false)
			{
				types |= TOP;
			}
			else
			{
				topLine = false;
			}
		}

		if (index.column() == GetModel()->columnCount() - 1)
		{
			types |= RIGHT;
		}
		else
		{
			int rightCell = index.column() + 1;
			if (gridCell[index.row()][rightCell] == false)
			{
				types |= RIGHT;
			}
			else
			{
				rightLine = false;
			}
		}

		if (index.row() == GetModel()->rowCount() - 1)
		{
			types |= BOTTOM;
		}
		else
		{
			int beloveCell = index.row() + 1;
			if (gridCell[beloveCell][index.column()] == false)
			{
				types |= BOTTOM;
			}
			else
			{
				bottomLine = false;
			}
		}

		if (index.column() == 0)
		{
			types |= LEFT;
		}
		else
		{
			int leftCell = index.column() - 1;
			if (gridCell[index.row()][leftCell] == false)
			{
				types |= LEFT;
			}
			else
			{
				leftLine = false;
			}
		}

		datas[index] = types;

		gridPosints[index].push_back({ TOP, topLine });
		gridPosints[index].push_back({ RIGHT, rightLine });
		gridPosints[index].push_back({ BOTTOM, bottomLine });
		gridPosints[index].push_back({ LEFT, leftLine });
	}

	m_pVew->SetCellDatas(gridPosints);
	SelectStyle * style = m_pVew->GetDelegate();
	style->SetCellDatas(datas);

	m_pVew->update();
}

到这里,我们的第二个问题就算回答了,我们需要绘制边框的单元格总算是计算出来了。

3、绘制

数据都有了,绘制还会远吗?

接下来继续往下看,Qt提供的绘制逻辑机制还是很强大滴,我们可以通过以下方式重绘

1、重写QStyledItemDelegate

QStyledItemDelegate是绘图代理,大多数的绘制操作最终都会在这里被执行,看参数就知道每一个cell绘制时都会来这里。

virtual void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const override;

但是这里有一个问题,那就是这个函数可绘制的区域问题,只能在这个cell里边绘制,如果绘制在border上将会被覆盖,不信看如下堆栈。

绘图代理QStyledItemDelegate的paint函数是被QTableView的paintEvent函数进行回调。

既然绘图代理中绘制cell项时不能绘制到cell外边去,那么刚好,我们可以在这里进行选择区域的填充

void SelectStyle::DrawSelected(QPainter * painter, const QRect & rect, const QModelIndex & index) const
{
	if (m_indexs.contains(index) == false)
	{
		return;
	}

	painter->save();

	QPen pen = painter->pen();
	pen.setWidth(1);
	pen.setColor(m_color);
	painter->setPen(pen);

	painter->fillRect(rect, QColor(100, 0, 0, 100));

	painter->restore();
}

填充完选择区域后,接下来便是绘制选择区域的border。

2、重写paintEvent
看了函数调用堆栈后,大家心里应该也比较清楚QTableView是怎么绘制的了吧。既然绘制代理不能完成需求,那么我们就只能在paintEvent这座大山中进行绘制。

这里需要注意一点就是,我们需要先试用QTableView本身的paintEvent把原有的绘制走一遍,保证界面上的信息都是全的,然后在执行我们自己的定制代码。

如下图所示,父类的paintEvent函数执行完毕后,我们绘制了border边线

之前在selectionModel的selectionChanged信号中,我们已经获取到了需要绘制border的cell信息,下面绘制时只需要根据缓存数据绘制即可,看这代码很长,但速度杠杠滴。

void FreezeTableView::paintEvent(QPaintEvent * event)
{
	QTableView::paintEvent(event);

	//绘制网格线
	QPainter painter(viewport());
	painter.save();
	QPen pen = painter.pen();
	pen.setWidth(1);
	pen.setColor(m_pSelectBorder->GetLineColor());
	painter.setPen(pen);

	for (auto iter = m_indexs.begin(); iter != m_indexs.end(); ++iter)
	{
		QModelIndex index = iter.key();
		QVector<GridPoint> cellTyeps = iter.value();
		QRect rect = visualRect(index);
		QRect tmpRect = rect;
		tmpRect.adjust(-1, -1, 1, 1);
		if (index.column() == 0)
		{
			tmpRect.adjust(1, 0, 0, 0);
		}
		if (index.row() == 0)
		{ 
			tmpRect.adjust(0, 1, 0, 0);
		}

		for (int i = 0; i < cellTyeps.size(); ++i)
		{
			const GridPoint & point = cellTyeps.at(i);

			if (point.type == TOP && point.line)
			{
				painter.drawLine(tmpRect.topLeft(), tmpRect.topRight());
			}
			if (point.type == RIGHT && point.line)
			{
				painter.drawLine(tmpRect.topRight(), tmpRect.bottomRight());
			}
			if (point.type == BOTTOM && point.line)
			{
				painter.drawLine(tmpRect.bottomLeft(), tmpRect.bottomRight());
			}
			if (point.type == LEFT && point.line)
			{
				painter.drawLine(tmpRect.topLeft(), tmpRect.bottomLeft());
			}
		}
	}

	for (auto iter = m_indexsBorder.begin(); iter != m_indexsBorder.end(); ++iter)
	{
		QModelIndexList indexs = iter.key();
		for each (const QModelIndex & index in indexs)
		{
			QRect rect = visualRect(index);
			rect.adjust(-1, -1, 0, 0);
			if (index.column() == 0)
			{
				rect.adjust(1, 0, 0, 0);
			}
			if (index.row() == 0)
			{
				rect.adjust(0, 1, 0, 0);
			}
			painter.setPen(iter.value());
			painter.drawRect(rect);
		}
	}

	painter.restore();
}

有了以上核心代码,自绘选择区域的功能基本上也就可以实现了。

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

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

相关文章

【Linux技术专题系列】「必备基础知识」一起探索(su、sudo等相关身份提权/身份切换机制)

内容简介 Linux技术专题&#xff0c;主要介绍相关&#xff0c;针对于Linux开发和知识体系的必备开发技能以及基础常识&#xff0c;是开发者必备的知识体系和方法论。 什么是用户、用户组 Linux用户 Linux系统是一个多用户多任务的分时操作系统&#xff0c;任何一个要使用系统资…

如何制作mp3音频文件?分享制作mp3的简单方法

MP3 对每个人来说都不是一个陌生的名字&#xff0c;因为它是全球排名第一的主流音乐格式。听过上千首MP3歌曲&#xff0c;你知道自己可以制作MP3文件吗&#xff1f;如果答案是否定的&#xff0c;那么您很幸运能找到这篇文章&#xff0c;因为它将通过向您介绍一个免费且简单的解…

产险精算GLM案例2

这是北美产险精算学会CAS网站上的一个案例&#xff0c;对案例略作修改后进行验证。 原始数据是一个简单的分组数据&#xff1a; ClassAOITerrExposureClaims1Low1762Medium1108443High11791054Low2130625Medium2126826High21291207Low3143848Medium31261019High34046 &#x…

从入门到项目实战 - Vue 列表渲染

Vue 列表渲染上一节&#xff1a;《Vue 条件渲染 》| 下一节&#xff1a;《Vue 数据的单向和双向绑定 》jcLee95 邮箱 &#xff1a;291148484163.com CSDN 主页&#xff1a;https://blog.csdn.net/qq_28550263?spm1001.2101.3001.5343 本文地址&#xff1a;https://blog.c…

【Linux技术专题系列】「必备基础知识」一起探索(用户、用户组与文件权限)

内容简介 Linux技术专题&#xff0c;主要介绍相关&#xff0c;针对于Linux开发和知识体系的必备开发技能以及基础常识&#xff0c;是开发者必备的知识体系和方法论。 什么是用户、用户组 Linux用户 Linux系统是一个多用户多任务的分时操作系统&#xff0c;任何一个要使用系统资…

【OpenFeign】【源码+图解】【五】创建FeignClient接口的代理(上)

【OpenFeign】【源码图解】【四】FeignClient实例工具类ReflectiveFeign 目录6. 创建FeignClient接口的代理6.1 收集方法的元数据6.1.1 方法上基础信息6.1.2 方法所在类的注解信息6.1.3 方法上的注解信息6.1.3.1 uri6.1.3.2 produces6.1.3.3 consumes6.1.3.4 headersRequestMap…

认识 TEE OS

了解 TEE OS 关于本文&#xff0c;是一篇会议纪要&#xff0c;会议主题是《从 Linux Kernel 角度看 TEE》&#xff0c;主讲人是周贺贺。它适用于嵌入式系统开发/驱动开发/内核设计/安全业务设计从业者&#xff0c;目的是让自己掌握 TEE 基本概念&#xff0c;知道大系统软件架构…

目标检测之YOLOv1算法分析

网络结构 卷积层池化层全连接层 输入448∗448448*448448∗448大小的图片 输出7∗7∗307*7*307∗7∗30的张量 30维张量包括20个对象的概率&#xff0c;2个bounding box的置信度及其位置&#xff08;一个bounding box位置信息需要四个值&#xff0c;两个bounding box总需要8个…

【C/C++】排序讲解,C语言实现各种排序

这篇文章会从思路到实现到分析时间空间复杂度&#xff0c;一次性搞懂各种排序 有帮助的话点个赞收藏一下不迷路啊 如果对时间空间复杂度还不熟悉的请去看 时间 空间复杂度 本文章不会讲堆排序&#xff0c;这部分内容会马上单写一篇博客介绍&#xff0c;和堆的和一些更复杂的问题…

C++基础之核心3

C核心编程 本阶段主要针对C面向对象编程技术做详细讲解&#xff0c;探讨C中的核心和精髓。 1 内存分区模型 C程序在执行时&#xff0c;将内存大方向划分为4个区域 代码区&#xff1a;存放函数体的二进制代码&#xff0c;由操作系统进行管理的全局区&#xff1a;存放全局变量…

海格里斯HEGERLS标准解析|夹抱式四向穿梭车医用行业现代物流解决方案

众所周知&#xff0c;随着当前电商、医用、新零售等领域的快速发展&#xff0c;各大中小企业对于存储的要求越来越高&#xff0c;为让仓储货架、仓储设备、仓储配件等更具有行业的适配性&#xff0c;传统固定的穿梭车已不能满足对不同尺寸料箱的处理。为此&#xff0c;河北沃克…

二叉树题型

目录 二叉数遍历迭代法 1.1前序遍历 1.2中序遍历 1.3后续遍历 二叉树最小深度 二叉树所有路径 中序后序构造二叉树 验证二叉搜素树 二叉数遍历迭代法 1.1前序遍历 前序遍历顺序&#xff1a;根—左—右&#xff1b; 解法1&#xff1a;用栈来进行中间过程处理&#xf…

Babel和devServer | Webpack

文章目录Babel和devServerbabelbabel命令行使用babel-loaderVue源码的打包VSCode对SFC文件的支持vue-loaderdevServerBabel和devServer babel babel命令行使用 babel-loader Vue源码的打包 VSCode对SFC文件的支持 vue-loader devServer

一键可以轻松替换人物背景图,效果出乎意料的好(附 Python 代码)

最近发现 BackgroundMattingV2 项目的一些使用上的小缺陷&#xff0c;但是他却可以做到头发丝精细的抠图效果。我将项目稍微魔改了一下&#xff0c;让他在可以选择单一图片的基础上&#xff0c;可以把抠好的图片贴在自定义的背景图上&#xff0c;这样就可以让照片中的人物&…

使用Tomcat时出现Access Error: 404 -- Not Found的解决办法

当时出现这种情况很迷惑&#xff0c;错误提示如下 突然想到大二上学模电的时候安装过multisim&#xff0c;这个软件的某些不必要的服务占用了8080端口&#xff08;恼&#xff09;&#xff0c;出现这种情况应该是和Tomcat默认的端口冲突了。 于是就有了解决思路&#xff1a; …

stm32f407VET6 系统学习 day03 通用同步异步收发器

1.同步串行通信 同步通信发送端和接收端必须用共同的时钟源才能保持它们之间的准确同步。同步传输时&#xff0c;每个字 符没有起始位和停止位&#xff0c;它不是用起始位来标志字符的开始&#xff0c;而是用一串特定的二进制序列&#xff0c;称为 同步字符&#xff0c;去通知接…

[思维模式-14]:《复盘》-2- “知”篇 - 复盘之道

目录 前言&#xff1a; 一、U型学习法&#xff1a;复盘的学习机理 &#xff08;1&#xff09; 回顾、评估 &#xff08;2&#xff09;分析、反思 &#xff08;3&#xff09;萃取、提炼 &#xff08;4&#xff09;转化、应用 二、复盘与PDCA既有区别&#xff0c;也有联系…

solr 安装和使用

Solr是基于ApacheLucene构建的流行、快速、开源的企业搜索平台 Solr具有高度可靠性、可扩展性和容错性&#xff0c;提供分布式索引、复制和负载平衡查询、自动故障切换和恢复、集中配置等功能。Solr为许多世界上最大的互联网站点提供搜索和导航功能 环境准备 linux centos7 ja…

如何用iDesktop快速制作一幅研究区概况图

目录前言数据准备成果展示制作步骤前言 研究区概况图能直观展示研究区域的地理位置&#xff0c;在许多研究展示与论文撰写中必不可少。本文将以成都市为例&#xff0c;利用SuperMap桌面产品iDesktop快速制作一幅研究区概况图。 数据准备 四川省行政区划矢量数据&#xff08;…

【语音处理】LQ/QR噪声估计器研究(Matlab代码实现)

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