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


    原文链接:QTableView表格控件区域选择-自绘选择区域

    一、开心一刻

    陪完客户回到家,朦胧之中,看到我妈正在拖地,我掏出200块塞到我妈手里,说道:妈,给你点零花钱,别让我媳妇知道。

    我妈接过钱,大吼:你是不是又喝酒了?

    我:嘘,你怎么知道的?

    老妈:你看清楚了,我是你媳妇,还有。这200块钱是哪来的,说!我:啊……

    二、概述

    最近优化了一个小功能,主要是模仿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();
    }
    

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

    五、相关文章

    1. Qt实现表格控件-支持多级列表头、多级行表头、单元格合并、字体设置等

    2. Qt高仿Excel表格组件-支持冻结列、冻结行、内容自适应和合并单元格

    3. 属性浏览器控件QtTreePropertyBrowser编译成动态库(设计师插件)

    4. 超级实用的属性浏览器控件--QtTreePropertyBrowser

    5. Qt之表格控件蚂蚁线

    6. QRowTable表格控件-支持hover整行、checked整行、指定列排序等

    7. QRowTable表格控件(二)-红涨绿跌


    值得一看的优秀文章:

    1. 财联社-产品展示
    2. 广联达-产品展示
    3. Qt定制控件列表
    4. 牛逼哄哄的Qt库

    如果您觉得文章不错,不妨给个打赏,写作不易,感谢各位的支持。您的支持是我最大的动力,谢谢!!!




    很重要--转载声明

    1. 本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords

    2. 如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。


  • 相关阅读:
    JS获取四位年份和2位年份
    notebook 快捷键
    发表文章不需要版面费的期刊
    命题演算、集合论和布尔代数之间的关系是什么?
    炒作套路
    如何理解佛经中“黄叶止啼”的故事
    期货之为什么要注册仓单!逼空是什么鬼!
    反证法与归谬法的区别
    感恩的含义!告诉你什么是感?什么是恩?人生必读!
    悖论的本质
  • 原文地址:https://www.cnblogs.com/swarmbees/p/11280122.html
Copyright © 2020-2023  润新知