原文連結: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
如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。