原文连接:QTableView表格控件区域选择-自绘选择区域浏览器
陪完客户回到家,朦胧之中,看到我妈正在拖地,我掏出200块塞到我妈手里,说道:妈,给你点零花钱,别让我媳妇知道。缓存
我妈接过钱,大吼:你是否是又喝酒了?ide
我:嘘,你怎么知道的?函数
老妈:你看清楚了,我是你媳妇,还有。这200块钱是哪来的,说!我:啊……学习
最近优化了一个小功能,主要是模仿excel相关的操做,以为还挺不错的,所以在这里进行了整理,分享给有须要的朋友。今天主要是说一下区域选择这项功能,Qt自带的表格控件是具备区域选择功能的,可是他并不美观,不能支持咱们自定义边框色和一些细节上的调整。字体
今天博主就来说解下本身是怎么自定义这个区域选择功能的。优化
主要使用的方式仍是自绘,下面先来看下效果,是否是你想要的。this
以下图所示,是一个自绘选择区域的效果展现,除此以外demo中还有一些其余的效果,但不是本篇文章所要讲述的内容。spa
本篇文章的重点就是讲述怎么实现区域选择框绘制
看过效果图以后,接下来开始分析怎么绘制矩形选择框。下面以问题的形式来进行分析,这样更有利于理解。
那么先来思考以下几个很问题
以上三个问题搞懂了,那么今天的主要内容也就差很少了。
学习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);
信号链接上后,开始处理信号。
思路大体是这样的:
判断逻辑也比较简单,逻辑比较简单,能够直接看代码。这里我举一个例子,好比说是否须要绘制左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(); }
到这里,咱们的第二个问题就算回答了,咱们须要绘制边框的单元格总算是计算出来了。
数据都有了,绘制还会远吗?
接下来继续往下看,Qt提供的绘制逻辑机制仍是很强大滴,咱们能够经过如下方式重绘
一、重写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。
二、重写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(); }
有了以上核心代码,自绘选择区域的功能基本上也就能够实现了。
值得一看的优秀文章:
![]() |
![]() |
很重要--转载声明