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



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM