Qt高仿Excel表格組件-支持凍結列、凍結行、內容自適應和合並單元格


原文鏈接:Qt高仿Excel表格組件-支持凍結列、凍結行、內容自適應和合並單元格

一、概述

最近看到一個比較炫酷的表格效果,凍結表格列功能。經常使用excel的人應該都使用過這個功能,當我們想把一些重要的信息一直固定在界面上時,就得使用凍結行或者凍結列的功能。

之前我也做過類似的凍結列的功能,而且Qt的源碼中也有類似的demo。

對Qt比較熟悉的人應該都知道,Qt的安裝包里可以為我們安裝很多的Qt使用事例,都非常不錯,很值得學習。我個人也是經常會去學習其中的東西,建議大家沒事也多看看。

Qt自帶的有一個事例程序,工程名字就做frozencolumn,這個功能就演示了怎么去實現凍結列的功能,思路非常不錯。於是,我也借鑒這個想法,做了好幾個復雜控件,都是使用這個思路來實現的效果,后續陸續放出

像標題說的那樣,本篇文章我們不僅僅是實現凍結列的功能,除此之外,凍結行、內容自適應行高,單元格合並這些我們都要需要完成。

二、效果展示

下面這張圖展示了凍結行、凍結列效果。gif圖有點兒長,可以花點兒時間看完,確認是自己想要的效果,再繼續往下看。

三、實現思路

1、凍結行、凍結列

Qt中的demofrozencolumn是怎么干的

既然Qt中已經幫我們是想了凍結列的功能,那么久先來分析下這個demo吧。

實現列凍結,也就是說在拖動水平滾動條的時候,第一列永遠顯示在窗口上。 怎么做到這個效果呢?Qt給的解決辦法很簡單,我們只需要把兩個視圖疊加在一起,上層這個視圖只顯示第一列,下層的視圖是全顯示,然后拖動的時候我們只需要正常拖動下層的這個視圖即可。

是不是很簡單呢。Qt封裝的控件,接口都很齊全,我們只需要使用connect把相關的變化綁定起來即可。

setModel(model);
frozenTableView = new QTableView(this);

init();

//connect the headers and scrollbars of both tableviews together
connect(horizontalHeader(),&QHeaderView::sectionResized, this,
      &FreezeTableWidget::updateSectionWidth);
connect(verticalHeader(),&QHeaderView::sectionResized, this,
      &FreezeTableWidget::updateSectionHeight);

connect(frozenTableView->verticalScrollBar(), &QAbstractSlider::valueChanged,
      verticalScrollBar(), &QAbstractSlider::setValue);
connect(verticalScrollBar(), &QAbstractSlider::valueChanged,
      frozenTableView->verticalScrollBar(), &QAbstractSlider::setValue);

上述代碼是從Qt5.7.1_vs2013版本中復制出來的。

看到了吧,就是這么簡單。

下面就是我們自己封裝凍結列、凍結行的講解,思路參考Qt的。

我們自己的高仿Excel表格

既然Qt都這么干了,我們還有什么理由不這么干呢?

話不多說,直接開干,既然要凍結列和行,那我們至少還需要在添加2個上層視圖,以固定列和行。

第一個版本的結構就是這樣的,多了2個上層視圖。

QTableView * m_pFrozenLeftTopView;
QTableView * m_pFrozenRowView;

等程序做好后,發現一個問題,上層2個用來凍結列和凍結行的視圖,永遠只有一個是工作正常的,2個上層視圖疊加的區域總是出現問題。

要么水平滾動正常,要么垂直滾動正常

思考了很久,為什么呢?也想了很多辦法去解決這個問題,最后還是決定,在添加一個視圖到行和列視圖重疊的區域,因為這么做最簡單。

至於為什么,大家可以自己想想,這里我就不做結束了,語言不是特別好描述,感覺自己也描述不清楚,囧。。。。

最后呢,我們的上層視圖列表會像下面這樣,從名字應該也可以看到他們分別是干什么的。

QTableView * m_pFrozenLeftTopView;
QTableView * m_pFrozenRowView;
QTableView * m_pFrozenColumnView;

然后就是構造函數了,負責同步他們之間的狀態

//沒有布局 因此必須把父窗口帶上
m_pFrozenLeftTopView = new QTableView(this);
m_pFrozenColumnView = new QTableView(this);
m_pFrozenRowView = new QTableView(this);

init();

connect(horizontalHeader(), &QHeaderView::sectionResized, this,
	&FreezeTableView::updateSectionWidth);
connect(verticalHeader(), &QHeaderView::sectionResized, this,
	&FreezeTableView::updateSectionHeight);

//垂直視圖垂直滾動條  -> 垂直滾動條
connect(m_pFrozenColumnView->verticalScrollBar(), &QAbstractSlider::valueChanged,
	verticalScrollBar(), &QAbstractSlider::setValue);
//垂直滾動條  -> 垂直視圖垂直滾動條
connect(verticalScrollBar(), &QAbstractSlider::valueChanged,
	m_pFrozenColumnView->verticalScrollBar(), &QAbstractSlider::setValue);

//水平滾動條  -> 水平視圖水平滾動條
connect(horizontalScrollBar(), &QAbstractSlider::valueChanged,
	m_pFrozenRowView->horizontalScrollBar(), &QAbstractSlider::setValue);
connect(m_pFrozenRowView->horizontalScrollBar(), &QAbstractSlider::valueChanged,
	horizontalScrollBar(), &QAbstractSlider::setValue);

connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &FreezeTableView::updateSelections);
connect(m_pFrozenColumnView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FreezeTableView::updateSelections);
connect(m_pFrozenRowView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FreezeTableView::updateSelections);
connect(m_pFrozenLeftTopView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FreezeTableView::updateSelections);

4個視圖上的當前選中項維護是一個比較費勁的操作,我這里設置了每個視圖都只能選中一個單元格,然后其他視圖單元格被選中的時候,清空其他三個視圖上的當前選中項。

當某一個視圖被點擊時,updateSelections槽就會被處罰。然后根據參數中被選中項,獲取點擊的單元格行號和列號,依次拿到被點擊了的視圖,接着清空其他沒有被點擊的視圖當前選中項。

void FreezeTableView::updateSelections(const QItemSelection & selected, const QItemSelection &)
{
	if (selected.isEmpty())
	{
		return;
	}
	QModelIndex index = selected.indexes().at(0);
	int row = index.row();
	int column = index.column();
	if (row < m_iRowFrozen
		&& column < m_iColumnFrozen)//左上
	{
		clearSelection();
		m_pFrozenRowView->clearSelection();
		m_pFrozenColumnView->selectionModel()->select(selected, QItemSelectionModel::Select | QItemSelectionModel::Clear);
	}
	else if (row >= m_iRowFrozen
		&& column < m_iColumnFrozen)//左下
	{
		clearSelection();
		m_pFrozenRowView->clearSelection();
		m_pFrozenLeftTopView->selectionModel()->select(selected, QItemSelectionModel::Select | QItemSelectionModel::Clear);
	}
	else if (row >= m_iRowFrozen
		&& column >= m_iColumnFrozen)//右下
	{
		m_pFrozenColumnView->clearSelection();
		m_pFrozenRowView->selectionModel()->select(selected, QItemSelectionModel::Select | QItemSelectionModel::Clear);
		m_pFrozenLeftTopView->clearSelection();
	}
	else if (row < m_iRowFrozen
		&& column >= m_iColumnFrozen)//右上視圖點擊
	{
		selectionModel()->select(selected, QItemSelectionModel::Select | QItemSelectionModel::Clear);
		m_pFrozenColumnView->clearSelection();
		m_pFrozenLeftTopView->clearSelection();
	}
}

大概思路就講這么多了,其他一些細節的實現大家可以自行完成,有困難可以找我。

2、行高自適應

編輯單元格內容時,行高自適應。

直接調用我封裝好的類,ResizeRowHeightEnable接口,參數傳遞true即可。

代碼比較簡單,一看應該都可以明白。是用過表格resizeRowToContents這個接口自適應行高。

void ExcTableWidget::ResizeRowHeightEnable(bool enable)
{
	if (enable)
	{
		connect(m_pModel, &QStandardItemModel::itemChanged, m_pVew, [this](QStandardItem * item){
			m_pVew->resizeRowToContents(item->row());
		}, Qt::UniqueConnection);
	}
	else
	{
		m_pModel->disconnect(m_pVew);
	}
}

3、螞蟻線

螞蟻線這個工我之前在另一篇文章中有講過,需要重寫一個繪圖代理QStyledItemDelegate,然后設置給表格控件。

可以參考Qt之表格控件螞蟻線這篇文章。

下面是這個繪圖代理的頭文件,其中有幾個public接口,主要是用於設置螞蟻線的樣式,和是否啟用螞蟻線的。

其中比較重要的接口是paint虛函數,這個函數里邊對單元格進行了繪制。

很重要:我們的繪制函數一定不要忘記調用原來的paint函數,否則單元格的其他樣式都會丟失

class SelectStyle : public QStyledItemDelegate
{
	Q_OBJECT

public:
	SelectStyle(QObject * parent = nullptr) : QStyledItemDelegate(parent), m_bAntLine(false), m_iOffset(0), m_color(0, 132, 255){}
	~SelectStyle(){}

public:
	void GoStepAntLine(bool);

	void SetLineColor(const QColor & color);
	void SetLineType(bool dash);

protected:
	virtual void paint(QPainter * painter
		, const QStyleOptionViewItem & option
		, const QModelIndex & index) const override;

private:
	void DrawBorderRect(QPainter * painter, const QRect & rect, bool firstColumn) const;
	void DrawDashRect(QPainter * painter, const QRect & rect, bool firstColumn) const;

protected:
	bool m_bAntLine;
	bool m_bDashState;
	int m_iOffset;
	QColor m_color;
};

四、測試代碼

1、添加表格數據

QFile file(":/grades.txt");
if (file.open(QFile::ReadOnly)) {
	QString line = file.readLine(200);
	QStringList list = line.simplified().split(',');
	tableView->SetHeaderLabels(list);

	QStringList lines; 
	while (file.canReadLine()) {
		line = file.readLine(200);
		lines.append(line);
	}
	file.close();

	int i = 1;
	int row = 0;
	while (i-- > 0)
	{
		for each (const QString & line in lines)
		{
			if (!line.startsWith('#') && line.contains(',')) {
				list = line.simplified().split(',');
				for (int col = 0; col < list.length(); ++col){
					tableView->SetItemData(row, col, list.at(col));
				}
				++row;
			}
		}
	}
}

2、設置凍結行、列

//測試凍結列
tableView->SetFrozen(2, 2);

3、行高、列寬

//測試行高
tableView->SetRowHight(2, 100);

//測試列寬
tableView->SetColumnWidth(1, 200);

4、單元格背景色

	//設置單元格文本顏色   第一行前五列字體為紅色
	tableView->SetItemForegroundColor(0, 0, Qt::red);
	tableView->SetItemForegroundColor(0, 1, Qt::red);
	tableView->SetItemForegroundColor(0, 2, Qt::red);
	tableView->SetItemForegroundColor(0, 3, Qt::red);
	tableView->SetItemForegroundColor(0, 4, Qt::red);

5、單元格文字

//設置單元格背景色	第二行前五列背景色為紅色
tableView->SetItemBackgroundColor(1, 0, Qt::red);
tableView->SetItemBackgroundColor(1, 1, Qt::red);
tableView->SetItemBackgroundColor(1, 2, Qt::red);
tableView->SetItemBackgroundColor(1, 3, Qt::red);
tableView->SetItemBackgroundColor(1, 4, Qt::red);

//設置單元格文本對齊方式 第三行前五列文字居中
tableView->SetTextAlignment(2, 0, Qt::AlignCenter);
tableView->SetTextAlignment(2, 1, Qt::AlignCenter);
tableView->SetTextAlignment(2, 2, Qt::AlignCenter);
tableView->SetTextAlignment(2, 3, Qt::AlignCenter);
tableView->SetTextAlignment(2, 4, Qt::AlignCenter);

//設置單元格文本對齊方式 第四行前五列文字字體
QFont font;
font.setBold(true);//加粗
font.setPixelSize(18);//18像素
font.setItalic(true);//斜體
font.setFamily(QString("Microsoft Yahei"));
font.setUnderline(true);//是否有下划線
font.setStrikeOut(true);
tableView->SetItemFont(3, 0, font);
tableView->SetItemFont(3, 1, font);
tableView->SetItemFont(3, 2, font);
tableView->SetItemFont(3, 3, font);
tableView->SetItemFont(3, 4, font);

6、其他相關測試

//合並單元格
tableView->SetSpan(5, 5, 2, 2);

//自適應第一行高度
tableView->ResizeRowHeight(0);

//高度自適應  盡量放在大量數據填充完 修改數據階段啟用
tableView->ResizeRowHeightEnable(true);

//選擇框顏色和樣式
tableView->SetFoucsLine(Qt::red, false);
tableView->SetMotionLine(true);

五、相關文章

  1. 屬性瀏覽器控件QtTreePropertyBrowser編譯成動態庫(設計師插件)

  2. 超級實用的屬性瀏覽器控件--QtTreePropertyBrowser

  3. Qt之表格控件螞蟻線

  4. 測試程序:Qt實現高仿excel表格-可執行文件(源碼不開放)


Qt自帶的demo中有一個demo程序,源碼工程叫做frozencolumn。

這個控件就是依賴於這個二次開發的,當然了已經被我封裝成了一個控件,對外暴露的都是借口,用戶不在需要關心內容的實現邏輯了。只需要調用幾個接口,就可以達到這樣的效果。

后續還會依次放出其他復雜控件:

  1. Qt實現表格樹控件-支持多級表頭
  2. Qt實現表格控件-支持多級列表頭、多級行表頭、單元格合並、字體設置等

如果您覺得文章不錯,不妨給個 打賞,寫作不易,感謝各位的支持。您的支持是我最大的動力,謝謝!!!




很重要--轉載聲明

  1. 本站文章無特別說明,皆為原創,版權所有,轉載時請用鏈接的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords

  2. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。



免責聲明!

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



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