百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

zhezhongyun 2025-06-09 07:21 34 浏览

一、概述

最近看到一个比较炫酷的表格效果,冻结表格列功能。经常使用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虚函数,这个函数里边对单元格进行了绘制。

【领QT开发教程学习资料,点击下方链接莬费领取↓↓,先码住不迷路~】Qt开发(视频教程+文档+代码+项目实战)

很重要:我们的绘制函数一定不要忘记调用原来的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);

【领QT开发教程学习资料,点击下方链接莬费领取↓↓,先码住不迷路~】「链接」

相关推荐

Python入门学习记录之一:变量_python怎么用变量

写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...

python变量命名规则——来自小白的总结

python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...

Python入门学习教程:第 2 章 变量与数据类型

2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...

绘制学术论文中的“三线表”具体指导

在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...

Python基础语法知识--变量和数据类型

学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...

一文搞懂 Python 中的所有标点符号

反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...

Python变量类型和运算符_python中变量的含义

别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...

从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序

在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...

Python中下划线 ‘_’ 的用法,你知道几种

Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...

解锁Shell编程:变量_shell $变量

引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...

一文学会Python的变量命名规则!_python的变量命名有哪些要求

目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...

更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for

src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...

C++第五课:变量的命名规则_c++中变量的命名规则

变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....

Rust编程-核心篇-不安全编程_rust安全性

Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...

探秘 Python 内存管理:背后的神奇机制

在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...