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
如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。
上一篇: 用函数模拟简单的购物车(Python)
下一篇: 从“n!末尾有多少个0”谈起