QTableView表格控件区域选择-自绘选择区域
原文链接:QTableView表格控件区域选择-自绘选择区域
一、开心一刻
陪完客户回到家,朦胧之中,看到我妈正在拖地,我掏出200块塞到我妈手里,说道:妈,给你点零花钱,别让我媳妇知道。
我妈接过钱,大吼:你是不是又喝酒了?
我:嘘,你怎么知道的?
老妈:你看清楚了,我是你媳妇,还有。这200块钱是哪来的,说!我:啊……
二、概述
最近优化了一个小功能,主要是模仿excel相关的操作,觉得还挺不错的,因此在这里进行了整理,分享给有需要的朋友。今天主要是说一下区域选择这项功能,Qt自带的表格控件是具有区域选择功能的,但是他并不美观,不能支持我们自定义边框色和一些细节上的调整。
今天博主就来讲解下自己是怎么自定义这个区域选择功能的。
主要使用的方式还是自绘,下面先来看下效果,是不是你想要的。
三、效果展示
如下图所示,是一个自绘选择区域的效果展示,除此之外demo中还有一些其他的效果,但不是本篇文章所要讲述的内容。
本篇文章的重点就是讲述怎么实现区域选择框绘制

四、实现思路
看过效果图之后,接下来开始分析怎么绘制矩形选择框。下面以问题的形式来进行分析,这样更有利于理解。
那么先来思考如下几个很问题
- 怎么确定绘制区域
- 怎么确定绘制的边框
- 谁去绘制更好
以上三个问题搞懂了,那么今天的主要内容也就差不多了。
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 ¤t, const QModelIndex &previous)
void currentColumnChanged(const QModelIndex ¤t, const QModelIndex &previous)
void currentRowChanged(const QModelIndex ¤t, 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、绘制边框
信号连接上后,开始处理信号。
思路大致是这样的:
- 使用gridCell记录所有的单元格
- 循环遍历选中的单元格
- 判断当前单元格哪个边是需要绘制的
- 结果存储于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();
}
有了以上核心代码,自绘选择区域的功能基本上也就可以实现了。
五、相关文章
值得一看的优秀文章:
![]() |
![]() |
很重要--转载声明
本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords
如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。
QTableView表格控件区域选择-自绘选择区域的更多相关文章
- Qt实现表格控件-支持多级列表头、多级行表头、单元格合并、字体设置等
目录 一.概述 二.效果展示 三.定制表头 1.重写数据源 2.重写QHeaderView 四.设置属性 五.相关文章 原文链接:Qt实现表格控件-支持多级列表头.多级行表头.单元格合并.字体设置等 ...
- QRowTable表格控件(二)-红涨绿跌
目录 一.开心一刻 二.概述 三.效果展示 四.任务需求 五.指定列排序 六.排序 七.列对其方式 八.相关文章 原文链接:QRowTable表格控件(二)-红涨绿跌 一.开心一刻 一天,五娃和六娃去 ...
- 【案例分享】在 React 框架中使用 SpreadJS 纯前端表格控件
[案例分享]在 React 框架中使用 SpreadJS 纯前端表格控件 本期葡萄城公开课,将由国电联合动力技术有限公司,资深前端开发工程师——李林慧女士,与大家在线分享“在 React 框架中使用 ...
- Qt之表格控件蚂蚁线
一.蚂蚁线 摘自互动百科:在图像影像软件中表示选区的动态虚线,因为虚线闪烁的样子像是一群蚂蚁在跑,所以俗称蚂蚁线.在Poshop,After Effect等软件中比较常见. 背景:用过excel的同学 ...
- QRowTable表格控件-支持hover整行、checked整行、指定列排序等
目录 一.开心一刻 二.嘴一嘴 三.效果展示 四.浅谈实现 五.自定义数据源 1.data函数 2.flags函数 六.自定义视图 1.目的 2.问题分析 七.测试 八.相关文章 原文链接:QRowT ...
- QRowTable表格控件(四)-效率优化之-优化数据源
目录 一.开心一刻 二.问题分析 三.重写数据源 1.自己存储数据 2.重写data接口 四.比较 五.相关文章 原文链接:QRowTable表格控件(四)-效率优化之-优化数据源 一.开心一刻 一程 ...
- 基于纯前端类Excel表格控件实现在线损益表应用
财务报表也称对外会计报表,是会计主体对外提供的反映企业或预算单位一定时期资金.利润状况的会计报表,由资产负债表.损益表.现金流量表或财务状况变动表.附表和附注构成.财务报表是财务报告的主要部分,不包括 ...
- 最好的Angular2表格控件
现在市面上有大量的JavaScript数据表格控件,包括开源的第三方的和自产自销的.可以说Wijmo的Flexgrid是目前适应Angular 2的最好的表格控件. Angular 2数据表格基本要求 ...
- Silverlight项目笔记5:Oracle归档模式引起的异常&&表格控件绑定按钮
1.Oracle归档模式产生日志文件引起数据库异常 连接数据库失败,提示监听错误,各种检查监听配置文件,删除再添加监听,无果. sqlplus下重启数据库数据库依然无果,期间碰到多个错误提示: ORA ...
随机推荐
- Hive —— 安装部署
一.安装Hive 1.1 下载并解压 下载所需版本的Hive,这里我下载版本为cdh5.15.2.下载地址:http://archive.cloudera.com/cdh5/cdh/5/ # 下载后进 ...
- javascript函数详解
//函数的两种声明方式 //在同一个<script>标签中,函数的调用和声明位置可以没有先后的顺序,因为在同一个标签中,都是等加载到内存中,然后在运行 //但是如果是在两个script标枪 ...
- spring boot使用log4j2将日志写入mysql数据库
log4j2官方例子在spring boot中报错而且还是用的是org.apache.commons.dbcp包 我给改了一下使用org.apache.commons.dbcp2包 1.log4j2. ...
- html手机自适应屏幕
<meta name="viewport" content="height=device-width, initial-scale=1.0, maximum-sca ...
- ajax请求中 两种csrftoken的发送方法
通过ajax的方式发送两个数据进行加法运算 html页面 <body> <h3>index页面 </h3> <input type="text&qu ...
- WebSocket+Netty构建web聊天程序
WebSocket 传统的浏览器和服务器之间的交互模式是基于请求/响应的模式,虽然可以使用js发送定时任务让浏览器在服务器中拉取但是弊端很明显,首先就是不等避免的延迟,其次就是频繁的请求,让服务器的压 ...
- mysql中id值被重置的情况
MySQL中,如果你为一张使用了innodb引擎的表指定了一auto_increment列,那么这张表会有一个auto_increment计数器,专门记录当前auto_increment的相关值,用来 ...
- Asp.Net url参数加密存在特殊符号处理方法
Url出现了有+,空格,/,?,%,#,&,=等特殊符号的时候,服务器端无法获得正确的参数值,解决办法. 使用System.Web.HttpUtility.UrlEncode()方法将这些字符 ...
- linux服务器无telnet等测试工具,测试http+json服务连通性
1. 问题描述: 1.公司内部服务器需要通过http接口方式访问另一公司内部接口服务器. 2.申请信息安全开通访问权限,但是只能开通到服务器+端口号,例如:192.168.1:8080,无ping权限 ...
- 从四个属性的角度来理解C语言的指针也许会更好理解
文章会在文末更新! 关于指针是什么,很多教材已经作出了定义,大多数都会定义为"存放变量内存地址的变量".从这句话中我觉得除了让我知道这个定义有11个字以外,其他就没什么用了.我个人 ...