目录
● namespace Ui { class Widget; }
● Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
2.文本编辑器TextEdit、显示牌Label和下拉列表Combo Box
前言
项目源码以及图标在我的资源里免费下载!
前面更新完C++基础后,正式进入到Qt的学习,学习Qt需要有C++基础(C++基础(一)、C++基础(二))。Qt有太多太多的控件和各种封装好了的API,从头讲起显然不明智,讲完也学不到东西,所以直接从项目入手,熟悉Qt开发的流程以及遇到陌生的控件,该如何通过帮助文档达到我们想要的效果,前面有些C++的知识在要用的时候也会补充,不妨跟着我,一步一步搓出属于自己的记事本项目。本课程会从0开始,手把手带你熟悉Qt的开发流程,每讲一个Qt的类,我都会先写一些示例代码,再根据最终需要的效果给出最终代码,跟着我一步一步做,你也可以做出属于你自己的记事本,源码可以从我的资源直接下载,希望大家关注支持一下,后续还会同步更新Qt项目的学习笔记。
最终效果如图所示:
我们仿照Windows系统自带的记事本,做出了类似的记事本。
项目概述
1.功能介绍
● 支持文本创建,打开,保存,关闭的功能
● UI样式美化
● 添加打开、保存快捷键
● 底部显示行列号及文本字符编码
● Ctrl加鼠标滚轮支持字体放大缩小
2.工程概述
我们新建一个Qt工程:
然后一路默认来到Details这里:
在Qt中,创建 "MainWindow" 与 "Widget" 项目的主要区别在于他们的用途和功能范围:
1. MainWindow:这是一个包含完整菜单栏、工具栏和状态栏的主窗口应用程序框架。它适合于更复杂的应用程序,需要这些额外的用户界面元素来提供丰富的功能和交互。
2. Widget:这通常是一个简单的窗口,没有内置的菜单栏、工具栏或状态栏。它适合于更简单或专用的应用程序,不需要复杂的用户界面组件。(明显选择Widget比较合适,记事本不需要太多的菜单栏)
后面一路默认即可,就可以看到默认生成的文件:
● QApplication
来看main.cpp文件
在Qt应用程序中, QApplication a(argc, argv); 这行代码的作用是创建一个 QApplication 类的实例。这是几乎每个Qt应用程序必须做的第一步,因为它负责管理应用程序的许多核心功能。 下表总结了 QApplication类在Qt框架中的主要功能和职责:
QApplication 是Qt应用程序的核心,它为应用程序提供了必要的环境和框架,确保GUI组件能够正常工作并响应用户的操作。
简而言之, QApplication a(argc, argv); 用于初始化Qt应用程序的环境,设置事件循环,并准备应用程序处理GUI事件。
● return a.exec()
在Qt应用程序中, QApplication::exec() 函数是用来启动应用程序的事件循环的。当你调用这个函数时,它会开始处理和分发事件,如用户的点击、键盘输入等。这个函数会一直运行,直到事件循环结束,通常是因为调用了 QApplication::quit() 函数或者关闭了应用程序的主窗口。简而言之,exec() 是Qt程序中的主循环,负责监听和响应事件,保持应用程序运行直到用户决定退出。(有点像我们平时写stm32项目里的while循环,把其他模块都封装好函数后统一在while循环里调用)
● namespace Ui { class Widget; }
来看widge.h文件:
在Qt框架中, namespace Ui { class Widget; } 是一种常见的用法,通常出现在使用Qt Designer设计GUI时自动生成的代码中。这里的 Ui 是一个命名空间,而 class Widget 是一个前向声明,它声明了一个名为 Widget 的类。这种做法允许你在 .cpp 源文件中引用由Qt Designer创建的UI界面,而不需要在头文件中包含完整的UI类定义。这种分离的方法有助于减少编译依赖性并保持代码的清晰和组织。在你的源文件中,你会创建一个 Ui::Widget 类型的对象来访问和操作UI组件。
● Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
来看widget.cpp文件:
代码 : QWidget(parent) 是初始化列表,用于调用基类 QWidget 的构造函数,并将 parent 传递给它。 ui(new Ui::Widget) 是初始化类内部的 ui 成员变量,这是通过 new 关键字动态分配的。Ui::Widget 是由Qt Designer工具生成的,用于处理用户界面。这种方式允许将用户界面的设计与后端逻辑代码分离,有助于提高代码的可维护性和可读性。
实现UI界面
本小节先把所有的UI界面用Qt Designer工具画出来,然后试着运行一下(由于还没写代码,所以按键等控件肯定不会有响应,这就需要用到信号与槽,这是Qt中最重要的,等画完UI界面就会讲)。
1.按键QPushButton 和布局Layou
再拖入两个按键,修改一下显示的字符和对象名称,运行结果如下(字符显示不全可以把按键拉大一点):
选中三个按键,然后点击水平布局(Horizontal Layout),就可以很好的排列在一起,并且包含关系也很清晰明了:
此时选中hLayoutButton,放大缩小的时候发现按键的宽度也在跟着变,这显然不是我们想要的,这时候又引入了弹簧控件(Spacers):
我们想给按键的水平布局添加一个背景色,可以用到widget控件,拉到合适大小后,注意:选中hLayoutButton,将按键水平布局拉到新添加的widget里:
现在可以修改背景色了,右键widgetUp,选择更改样式表:
然后点击添加颜色右边的箭头,选择背景颜色:
选择完后运行一下试试(真不错):
2.文本编辑器TextEdit、显示牌Label和下拉列表Combo Box
稍微熟悉了Qt Designer的使用,现在我们把整体的UI界面做出来,先加入一个文本编辑器,既然是记事本,这个肯定是要用的:
稍微调整一下位置和大小,让widgetUp顶住Widget的左上角,依照最终效果,再在TextEdit的下面添加一个Label和Combo Box,同样放在一个新加入的widget里,用于设置背景色:
同样的选中Label和Combo Box,再点击水平布局,这样就比较紧凑了。
3.最终布局以及按键美化(Style Sheet)
选中这三个(Ctrl+鼠标可以单独选择)后点击垂直布局:
垂直布局之后是这样的:
调整一下垂直布局的大小(垂直布局包含了本文编辑器以及我们自己加入的两个widget),把它铺满整个网点(背后的幕布),观察到我们新加入的两个widget都太小了,导致我们的按键和标签等都看不到了,可以通过设置widget的最小高度来解决:
下面的widgetDown同理,设置最小高度为30:
注意:hLayout(包含Label和Combo Box)可能会消失,可以调整完后删除再重新拖入,最终效果如图:
点击垂直布局,修改间距为0:
运行试试:
差不多有个雏形了。但是我放大缩小运行的窗口时,我的UI界面并不会跟着变,所以在widget.cpp里加上这么一句:
接下来我们来做按键美化,这时候就需要开始查看帮助文档了,Qt有太多太多控件了,死记硬背是不可能的,帮助文档里有各种示例代码可以直接拿来用:
点击The Style Sheet Syntax(The Style Sheet Syntax),往下找就能看到如何改变按键的样式,有很多,这里讲不完,只讲要用到的,你也可以去网上看别人开源的代码,把别人如何更改样式表记录下来,有自己的代码库。比如:
QPushButton:hover 当鼠标悬停在按键上方时,你可以指定显示什么颜色。我这里以红色为例子,右键按键,改变样式表,直接把代码复制上去:
当我们鼠标悬停在按键上面时,就会显示红色的字体,下面我直接给出最终效果要用到的代码,并附上说明:
QPushButton { color: black; background-color: white }//默认状态下白底黑字
QPushButton:hover { color: red }//悬停时红字
QPushButton:pressed { color: blue }//按下时蓝字
知道了这些,我们就可以引入图标了,可以去iconfont-阿里巴巴矢量图标库 寻找图标,我直接把我自己用的图标给大家,可以在我的资源里下载,icon文件夹就是图标。
① 添加程序左上角的记事本图标
回到编辑,右键选择添加新文件:
然后选择Qt里的Resource File,一路默认会出现以下界面:
现在代码文件夹里新建一个icon文件夹,里面存放图标:
跟着我这样做:
Add Files添加icon文件夹里的所有文件,结果如下:
回到Qt Designer,点击最大的widget,在下面的属性列表里可以修改窗口的标题为 MyNoteBook,再在下方找到windowIcon,点击可以选择图标:
选择icon里的记事本图标:
运行看一下:
② 添加按键图标(打开、保存、关闭)
右键想要添加图标的按键,点击改变样式表,然后点击添加资源的border-image:
选择图标,我想让它默认是蓝色,悬停的时候是黑色,按下去是灰色,话不多说:
添加完后出现这么一行代码,也就是说,这行代码就是我们要显示的图标,那么把这行代码放在我们之前改变按键样式的{}里,不就能实现不同按键状态显示不同图标了吗?
最终代码如下(其他两个按键同理):
QPushButton { border-image: url(:/icon/dakai.png); }
QPushButton:hover { border-image: url(:/icon/dakai.1.png); }
QPushButton:pressed { border-image: url(:/icon/dakai.2.png); }
点击OK后看到图标太小了,先把之前写的文本删掉:
然后设置三个按键的最小高度和宽度为45,在属性列表那里设置,结果如下:
4.解决UI美化遗留问题
运行一下看似很完美了,但还有一个问题:
当我放大缩小时,右下角的控件并不会跟着窗口移动,那还是要我们显式的绑定它们和布局。点击hLayout,将它铺满widgetDown,然后在左右都加上弹簧,其中右边的弹簧设置成Fixed,宽度和高度为20x20。
来到widget.cpp文件:
我们需要设置ui(即Qt Designer)里面的widgetDown和它包含的水平布局hLayout相关联。之前我们是设置最大的Widget和垂直布局相关联,这个也是类似的,知道可以这么用。之前的代码this就表示最大的Widg。
还有一种方法比较简单粗暴,选择hLayout的布局,然后打破布局:
然后点击widgetDown再点击水平布局:
这样就可以把 ui->widgetDown->setLayout(ui->hLayout); 这行代码删掉,然后调整一下大小,有些同学可能会被弹簧挤压到左边,按照我这样选择:
点击widgetDown,往下找到高亮那一行,选择左边到右边。
运行一下(两种方法运行效果一样,第二种方法打破布局就不需要代码):
完美了,UI界面的设计到此为止,后面将会引入Qt最重要的概念,信号与槽,其实也不会很难,难的是各个控件改用什么函数来实现我们想要的功能?这就需要我们频繁的查看帮助文档了,在我们设计UI界面的时候,我们可以看到每个控件都属于哪个类,我们在帮助文档里搜索这个类,就能看到许多示例代码。不知道你有没有一步步跟着我做到这里,快去做出你自己的记事本界面吧。
信号与槽
界面上已经有按键了,怎么操作才能让用户按下按键后有操作上的反应呢?
在 Qt 中,信号和槽机制是一种非常强大的事件通信机制。这是一个重要的概念,特别是对于初学者来说,理解它对于编写 Qt 程序至关重要。
概要
1. 信号 (Signals):是由对象在特定事件发生时发出的消息。例如,QPushButton 有一个 clicked() 信号,当用户点击按钮时发出。
2. 槽 (Slots):是用来响应信号的方法。一个槽可以是任何函数,当其关联的信号被发出时,该槽函数将被调用。
3. 连接信号和槽:使用 QObject::connect() 方法将信号连接到槽。当信号发出时,关联的槽函数会自动执行。
Qt的信号和槽机制是其事件处理系统的核心。这种机制允许对象之间的通信,而不需要它们知道对方的 具体实现。以下是Qt信号和槽的几种常见连接方式的简要概述:
这些方式各有优劣,选择哪种方式取决于具体的应用场景、代码风格以及个人偏好。例如,直接使用 QObject::connect 是最通用的方式,而使用Lambda表达式可以在同一位置编写信号处理逻辑,提高代码的可读性。使用函数指针的方式则在编译时提供更好的类型检查。自动连接通常在使用 Qt Designer 设计UI时比较方便。(如果有需要,可以在评论一下,我再专门出一篇博客讲讲信号与槽的四种方式和要注意的点)
按键QPushButton设置信号与槽
新建一个Qt文件,进入Qt Designer,拖一个按键进来,右键点击转到槽:
默认的信号就是按键的 clicked() 信号,包含了按下和松开两个事件(很重要的概念),点击ok后会跳转到widget.cpp文件,并且帮我们自动绑定和生成了槽函数:
并且在widget.h里添加了槽函数的声明:
我们试着在槽函数里打印一下信息:
了解到这里已经够用了。
补充:QDebug
QDebug 是 Qt 框架中用于输出调试信息的一个类。它提供了一种方便的方式来输出文本到标准输出(通常是控制台),这对于调试 Qt 应用程序非常有用。 QDebug 类可以与 Qt 的信号和槽机制一起使用,使得在响应各种事件时能够输出有用的调试信息。
使用 QDebug 的一个典型方式是通过 qDebug() 函数,它返回一个 QDebug 对象。然后,可以使用流操作符 << 来输出各种数据类型(用法和之前的std::cout << "Hello"; 一样)。例如:
#include <QDebug>
qDebug() << "This is a debug message";
int value = 10;
qDebug() << "The value is" << value;
文件操作类QFile
QFile 是 Qt 框架中用于文件处理的一个类。它提供了读取和写入文件的功能,支持文本和二进制文件。
QFile 继承自 QIODevice ,因此它可以像其他IO设备一样使用。(学过linux系统编程的同学对文件的操作应该比较熟悉,我们要打开一个文件,读出里面的内容显示在文本编辑器 TextEdit 上,保存的时候要把文本编辑器的内容写入文件里;涉及文件的打开、读取、写入、关闭和光标移动等操作)
QFile示例
新建一个测试工程,新建两个按键,一个为"读取文件",一个为"写入文件",并生成槽函数,然后在工程路径下新建一个txt文本,内容为"This is a file, open by Qt",然后开始写代码。
在帮助文档搜索QFile:
帮助文档十分详细,有时间还是自己去翻一翻。点击跳转后往下一点就能看到怎么使用,比如直接传入文件路径打开文件:
上面只是教大家试着去使用帮助文档,下面的代码我就不一一告诉大家是怎么找的了,比较重要的我会提一下。
widget.cpp
#include "widget.h"
#include "ui_widget.h"
//使用QFile类需要包含的头文件
#include <QFile>
#include <QDebug>
/*----------------------------*/
//这部分是自动生成的构造函数和析构函数
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
/*----------------------------*/
//读取文件的槽函数
void Widget::on_btnRead_clicked()
{
// 1. 打开文件
QFile file("D:/QT/test1.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Failed to open file!";
return;
}
// 2. 读取文件
int size = file.size();
char *contex = new char[size + 1]; // 分配足够的内存,+1 是为了存储 '\0'
contex[size] = '\0'; // 手动添加字符串结束符
if (file.read(contex, size) == -1) {
qDebug() << "Failed to read file!";
delete[] contex; // 释放内存
file.close();
return;
}
// 3. 显示
qDebug() << contex;
// 4. 关闭文件并释放内存
file.close();
delete[] contex; // 释放内存
}
//写入文件的槽函数
void Widget::on_btnWrite_clicked()
{
//1.打开
QFile file("D:/QT/test6.txt");
file.open(QIODevice::WriteOnly | QIODevice::Text);
//2.写入
file.write("write by QT");
//3.关闭
file.close();
}
传递给open()的QIODevice::Text标志告诉Qt将windows风格的行终止符(“\r\n”)转换为c++风格的行终止符(“\n”)。还是比较简单的,注意文件写入这里,我们一开始没有创建test6.txt文件,但我们是写入文件,点击按键后会自动帮我们生成文本文件。
我们怎么知道我们要用的类,比如上面的QFile类需要包含什么头文件?
当报错的时候点击类名,然后按下Alt+回车键:
就可以知道需要的头文件,另外可以通过Ctrl+鼠标左键跳转到类的定义(更多还是使用帮助文档)。
QTextStream
QTextStream 的主要特性成一个表格。请看下表:
QTextStream 是一个功能强大的类,用于处理文本数据,特别是在需要考虑字符编码和文本格式化的情况下。通过这些特性,它提供了一种灵活而强大的方式来读写和操作文本。(我们需要根据不同的编码方式来打开一个文本,因此引入QTextStream)
QTextStream示例
新建一个测试工程,新建两个按键,新建两个按键,一个为"Stream读取",一个为"Stream写入",并生成槽函数,然后看看帮助文档:
上面是一行一行读取文件和写入文件的示例,我们直接来写代码。
widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QFile>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_StreamRead_clicked()
{
QFile file("D:/QT/test.txt");
file.open(QIODevice::ReadOnly | QIODevice::Text);
//构造函数的入参为QIODevice *device,QFile的基类是QFileDevice,它的基类是QIODevice
//是C++多态的用法
QTextStream in(&file);
in.setCodec("UTF-8");
//如果没读到末尾就一直读
while(!in.atEnd())
{
//处理大文件的时候一行一行读出来
QString context = in.readLine();
qDebug() << context;
}
file.close();
}
void Widget::on_StreamWrite_clicked()
{
QFile file("D:/QT/test10.txt");
file.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream out(&file);
//直接通过操作符来写入字符串
out << "StreamWriteByQT";
file.close();
}
同样的,写入文件的时候如果文件不存在,会帮我们自动生成。
文件选择对话框QFileDialog
实现选择任意文件
前面我们学习了如何对文件进行操作,但是文件的路径我们都是写死的,我们想要实现点击打开的时候,像Windows系统自带的记事本一样,跳出一个文件选择对话框,因此引入 QFileDialog 类。我们先查看一下帮助文档。
看帮助文档来学习一个类或者控件是最快的,它会贴心的给你代码示例已经某些参数的功能。我们话不多说直接来写代码。
widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QFileDialog>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
//打开文件选择对话框
void Widget::on_btnFileDialog_clicked()
{
/* 返回值为QString类型,它返回用户选择的现有文件的路径。如果用户按下Cancel,则返回空字符串。
* tr("Open Text File"):这是对话框的标题,表示你要打开一个文本文件。
* "D:/Qt":这是对话框打开的初始目录。
* tr("Text Files (*.txt)"):这是文件过滤器,确保对话框中只显示 .txt 文件。
*/
QString fileName = QFileDialog::getOpenFileName(this, tr("Open Text File"), "D:/Qt", tr("Text Files (*.txt)"));
qDebug() << fileName;
}
运行一下:
成功得到选择文件的路径,那结合我们之前的文件操作,把得到的路径传入 setFileName 函数就好了,不就可以实现打开任意txt文件了吗。
实现保存文件
查看帮助文档,看到有个函数为 getSaveFileName 的,看看帮助文档的代码示例:
看起来和选择文件的用法差不多,新建一个按键,转到它的槽函数:
void Widget::on_btnSave_clicked()
{
/* 注意:
* 运行这个函数并不会真的保存文件,而是返回你要保存的文件名(第三个参数设置默认名)
* 还记得QFile类的写入文件吗?写入文件的时候文件不存在会帮我们自动生成
* 因此这个函数配合QFile的写入函数操作就能实现保存文件的功能
*/
QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"), "D:/Qt", tr("Text Files (*.txt)"));
}
实现记事本的文件打开功能以及优化
基本功能代码实现
回到我们的记事本项目,给打开按键生成一个槽函数,我们直接来写代码(这里并不是最终的代码,我会根据思路一步一步优化,源码在文章开头下载):
void Widget::on_btnOpen_clicked()
{
//选择txt文件
QString fileName = QFileDialog::getOpenFileName(this,
tr("Open Text File"), "D:/QT", tr("Text Files (*.txt)"));
//每次选择完文本后把TextEdit控件清空,不然新打开的文本会追加在原先文本的后面
ui->textEdit->clear();
//打开
file.setFileName(fileName);
if(!file.open(QIODevice::ReadWrite | QIODevice::Text))
{
qDebug() << "open file error";
}
//这里是设置窗口的标题为文件名加上后面的字符串
this->setWindowTitle(fileName + "-MyNoteBook");
//读取文件
QTextStream in(&file);
in.setCodec("UTF-8");
while(!in.atEnd())
{
QString context = in.readLine();
//如果使用setText,那么每读一行都会把之前的覆盖,最终只会显示最后一行
//ui->textEdit->setText(context);
//使用追加的方式,就能完整的显示文件
ui->textEdit->append(context);
}
}
ui指向了最大的 Widget 控件,所以我们可以操作里面的 textEdit 控件,在帮助文档搜索文本编辑器所属的类 QTextEdit 。
运行一下:
注意:只能打开UTF-8编码的文件,后续会使用下拉列表(Combo Box)实现对打开的文件进行不同的编码。
字符编码引入
在 Qt 中,QTextStream 常用的字符编码主要包括以下几种:
其实只需要一个控件,它可以提供多种选择,然后把选择传入之前的 setCodec 函数即可,因此引入下面的控件。
QComboBox
QComboBox 是 Qt 框架中用于创建下拉列表的一个控件。它允许用户从一组选项中选择一个选项,并可以配置为可编辑,使用户能够在其中输入文本。QComboBox 提供了一系列方法来添加、删除和修改列表中的项,支持通过索引或文本检索项,并可以通过信号和槽机制来响应用户的选择变化。该控件广泛应用于需要从多个选项中进行选择的用户界面场景,例如表单和设置界面。
我们新建一个工程来玩一下这个控件,双击 Combo Box ,点击+号就可以添加选项:
在帮助文档搜索QCombo Box,看看有什么信号:
现在我们来使用连接信号与槽最常用的方式,就是 connect 函数:
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
//槽函数加上这句说明
public slots:
//自定义的槽函数
void oncurrentIndexChanged(int index);
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//使用connect函数,谁发送信号?发送什么信号?谁接收信号?槽函数是哪个?
connect(ui->comboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(oncurrentIndexChanged(int)));
}
Widget::~Widget()
{
delete ui;
}
void Widget::oncurrentIndexChanged(int index)
{
//当索引值改变后槽函数被调用,打印索引值(从0开始)
qDebug() << index;
//可以直接获得当前显示的文本
qDebug() << ui->comboBox->currentText();
}
解决字符编码
既然有编码选项给我们,那我们直接把得到的文本传给 setCodec 函数就好了,直接来写代码:
记事本项目的打开按键槽函数(最终代码):
void Widget::on_btnOpen_clicked()
{
//选择txt文件
QString fileName = QFileDialog::getOpenFileName(this,
tr("Open File"), "D:/QT", tr("Text Files (*.txt)"));
//每次选择完文本后把控件清空,不然新打开的文本会追加在原先文本的后面
ui->textEdit->clear();
//打开
file.setFileName(fileName);
if(!file.open(QIODevice::ReadWrite | QIODevice::Text))
{
qDebug() << "open file error";
}
this->setWindowTitle(fileName + "-MyNoteBook");
//读取文件
QTextStream in(&file);
//in.setCodec("UTF-8");
/* setCodec接收一个const char *类型的字符串
* str.toStdString()返回std::string类型
* 再用c_str()转化为const char *型
*/
QString str = ui->comboBox->currentText();
const char *c_str = str.toStdString().c_str();
in.setCodec(c_str);
while(!in.atEnd())
{
QString context = in.readLine();
ui->textEdit->append(context);
}
}
Combo Box控件的槽函数(最终代码):
//当我打开一个文件后,修改窗口右下角的编码方式,想立刻得到这个编码下的文本
void Widget::oncurrentIndexChanged()
{
/* 思路:
* 清空文本,再根据选择的编码方式重新读文件,显示在文本编辑器上
*/
ui->textEdit->clear();
if(file.isOpen())
{
QTextStream in(&file);
//链式调用,之前字符串类型的转化可以简化成这一句
in.setCodec(ui->comboBox->currentText().toStdString().c_str());
//由于没有关闭文件,光标是在文件的末尾,直接读读不到数据,因此将光标归位
file.seek(0);
while(!in.atEnd())
{
QString context = in.readLine();
ui->textEdit->append(context);
}
}
}
成功解决字符编码问题:
添加行列显示
使用QTextEdit的cursorPositionChanged信号(光标改变时发出信号),当光标发生移动时候刷新Label的显示(默认为L:0,C:0),代码比较简单,我先给出示例代码:
// 1. 在构造函数中添加信号与槽的连接
// 当 textEdit 中的光标位置发生变化时,触发 onCursorPositionChanged 槽函数
connect(ui->textEdit, SIGNAL(cursorPositionChanged()), this, SLOT(onCursorPositionChanged()));
// 槽函数:当光标位置发生变化时调用
void Widget::onCursorPositionChanged()
{
// 获取 textEdit 中当前的光标对象
QTextCursor cursor = ui->textEdit->textCursor();
// 获取光标所在的行号(blockNumber 返回从 0 开始的行号,因此需要 +1)
QString blockNum = QString::number(cursor.blockNumber() + 1);
// 获取光标所在的列号(columnNumber 返回从 0 开始的列号,因此需要 +1)
QString columnNum = QString::number(cursor.columnNumber() + 1);
// 格式化显示信息,例如 "L:1,C:5" 表示第 1 行第 5 列
const QString labelmes = "L:" + blockNum + ",C:" + columnNum;
// 将格式化后的信息显示在 label 控件上
ui->label->setText(labelmes);
}
实现当前行高亮
实现策略:
● 获取当前行的光标位置,使用的信号和获取行列值是一样的
● 通过ExtraSelection来配置相关属性,在当前行设置该属性
实现该功能,需要用到一个API:
QList extraSelections;
void setExtraSelections(const QList &extraSelections);
既然是在文本编辑器进行操作,在帮助文档搜索QTextEdit:
很明显这正是我们需要的,由于也是接收光标改变的信号,所以也写在和行列值显示同一个槽函数里。
光标移动槽函数(最终代码):
// 槽函数:响应光标位置变化的信号
void Widget::onCursorPositionChanged()
{
// --- 第一部分:更新标签显示行号和列号 ---
QTextCursor cursor = ui->textEdit->textCursor();
// 注意:blockNumber() 返回从 0 开始的行号,需要 +1 转换为自然行号
QString blockNum = QString::number(cursor.blockNumber() + 1);
// 注意:columnNumber() 返回从 0 开始的列号,需要 +1 转换为自然列号
QString columnNum = QString::number(cursor.columnNumber() + 1);
// 格式化显示内容(例如:"L:3,C:12")
const QString labelmes = "L:" + blockNum + ",C:" + columnNum;
ui->label->setText(labelmes); // 更新标签控件显示
// --- 第二部分:设置当前行高亮 ---
QList<QTextEdit::ExtraSelection> extraSelections;
QTextEdit::ExtraSelection ext;
// 1. 获取当前行的光标对象
// 注意:直接使用当前光标对象,而不是创建新的,确保操作当前正确位置
ext.cursor = cursor;
// 2. 设置背景色
// 注意:颜色可根据需求调整(例如 Qt::yellow 更醒目)
QBrush qBrush(Qt::lightGray);
ext.format.setBackground(qBrush);
// 3. 关键配置:设置全行选中属性
// 注意:没有此属性将无法整行高亮(只高亮光标所在位置的背景)
ext.format.setProperty(QTextFormat::FullWidthSelection, true);
// 4. 将配置添加到高亮列表
extraSelections.append(ext);
// 5. 应用高亮设置到 textEdit
// 注意:这会覆盖之前的所有额外选区,如果有多个高亮需求需要合并设置
ui->textEdit->setExtraSelections(extraSelections);
}
这里要说明的是,QTextEdit的setExtraSelections方法接受一个列表(QList),允许同时设置多个高亮区域。即使当前只需要一个,也需要以列表的形式传入,因为API设计如此。当前行背景色被我设置成了浅灰色。
QList<QTextEdit::ExtraSelection> extraSelections; 这行代码创建了一个列表,可以理解成数组,但它有数组和链表的特性;这个列表每一项都是 ExtraSelection 。正如上面说的,可以同时设置多个高亮区域,所以需要用到列表,但我只需要设置一行。对于没用过的函数的入参,使用帮助文档就能快速上手。
实现记事本的文件保存功能以及优化
回到记事本项目,保存文件的功能代码比较简单,都是我们之前讲过的控件,我直接给出代码,实现的功能为:如果文件是新的文件,点击保存按键后会跳出文件选择对话框;如果我事先打开一个文件,然后添加了新的内容,再点击保存按键,它会直接帮我保存,不弹出文件选择对话框。
记事本项目的保存按键槽函数(最终代码):
// 保存按钮点击事件的槽函数
void Widget::on_btnSave_clicked()
{
// 判断文件是否已打开
if(!file.isOpen())
{
// 文件未打开时弹出保存对话框
QString fileName = QFileDialog::getSaveFileName(
this,
tr("Save File"), // 对话框标题
"D:/QT", // 默认目录
tr("Text Files (*.txt *.doc)") // 文件过滤器
);
// 设置文件名
file.setFileName(fileName);
if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
qDebug() << "open file error"; // 错误日志
return; // 关键点:打开失败必须返回,避免后续操作崩溃
}
// 更新窗口标题显示文件名
this->setWindowTitle(fileName + "-MyNoteBook");
}
else
{
//为啥这样写往下看看,有详细的解释。
file.resize(0);
}
// 创建文本流对象关联文件
QTextStream out(&file);
// 设置编码(从下拉框获取编码名称)
out.setCodec(ui->comboBox->currentText().toStdString().c_str());
// 获取文本框内容
QString context = ui->textEdit->toPlainText();
// 将内容写入文件
out << context;
}
调用文本编辑器控件的 toPlainText 函数就可以得到文本编辑框的内容。
我在跟着课程做的时候,发现他最终实现的保存功能有bug,假设我事先打开了一个文件,里面的内容为 aaa ,我这时候加入新的内容 bbb ,点击保存后确实不会弹出文件选择对话框,但是打开保存后的文件,发现文件的内容变成了 aaaaaabbb 。也就是说点击保存的时候它会将文本编辑器上的全部文本追加到原先的文件里边,而不是把我新加入的内容追加到原先的文件里。
因此如果文件是打开状态下,进入else分支,执行 file.resize(0); 这行代码,它会将文件的大小变成0,也就是清空之后再把文本编辑器的内容全写入到文件里,这样就能实现我们想要的效果。
实现记事本的文件关闭功能以及优化
关闭就很简单了,先给出最简的代码,后面再给出优化:
void Widget::on_btnClose_clicked()
{
ui->textEdit->clear();
if(file.isOpen())
{
file.close();
this->setWindowTitle("MyNoteBook");
}
}
非常简单哈,但我们还可以对关闭做些处理,比如弹出一个询问窗口(防误触)
消息对话框QMessageBox
帮助文档搜索QMessageBox:
我直接给出代码了,比较简单:
记事本项目的关闭按键槽函数(最终代码):
void Widget::on_btnClose_clicked()
{
// 创建标准警告消息框
int ret = QMessageBox::warning(this,
tr("My NoteBook"), // 对话框标题
tr("当前文件有未保存的修改!\n" // 更明确的提示
"是否要保存更改?"), // 分句更符合中文习惯
QMessageBox::Save | // 保存按钮
QMessageBox::Discard |// 不保存按钮
QMessageBox::Cancel, // 取消按钮
QMessageBox::Save); // 默认选中保存按钮
switch (ret) {
case QMessageBox::Save:
// 用户选择保存:调用保存按钮的槽函数
on_btnSave_clicked(); // 注意:如果保存失败,此处可能需要额外处理
break;
case QMessageBox::Discard:
// 用户选择不保存:清空编辑区并关闭文件
ui->textEdit->clear(); // 清除文本内容
if(file.isOpen()) {
file.close(); // 关闭文件
this->setWindowTitle("MyNoteBook"); // 重置窗口标题
// 建议:同时清空文件名变量(如果代码中有相关变量)
}
break;
case QMessageBox::Cancel:
// 用户选择取消:不执行任何操作
// 此处不需要写代码,自动中断关闭流程
break;
}
}
当点击Save后,会调用保存按键的槽函数,Discard就是直接关闭文件,忽略这次的编辑,Cancel就是防止误触,不进行任何操作。
实现快捷键功能
快捷键实现文件的打开和保存
在 Qt 中实现快捷键功能通常涉及到 QShortcut 类的使用。我们在帮助文档搜索这个类:
可以看到功能还是挺少的,信号也只有两个,那我们肯定是捕捉第一个信号,因为比较简单,所以我直接写代码了,实现的结果为:按下Ctrl+O快捷键,调用打开按键的槽函数,按下Ctrl+S快捷键,调用保存按键的槽函数:
//在构造函数里
// 1. 创建快捷键对象
// - 参数1: 快捷键组合(使用 tr 标记翻译上下文)
// - 参数2: 父对象(this)确保自动内存管理
QShortcut *shortcutOpen = new QShortcut(QKeySequence(tr("Ctrl+O", "File|Open")), this);
QShortcut *shortcutSave = new QShortcut(QKeySequence(tr("Ctrl+S", "File|Save")), this);
// 2. 连接快捷键信号到槽函数
// 使用 lambda 表达式直接转发到已有按钮的点击事件
connect(shortcutOpen, &QShortcut::activated, [=]() {
on_btnOpen_clicked(); // 触发打开按钮的点击逻辑
});
connect(shortcutSave, &QShortcut::activated, [=]() {
on_btnSave_clicked(); // 触发保存按钮的点击逻辑
});
十分简单,这里连接信号与槽又是一种新的方式,使用Lambda表达式,由于是匿名函数,因此只有三个参数,直接调用我们写好的按键槽函数就行了。
快捷键实现字体的放大和缩小
既然是要改变字体大小,那肯定是调用文本编辑器的某些函数,直接给出代码,通过Ctrl+=放大字体,Ctrl+-缩小字体:
QShortcut *shortcutZoomIn = new QShortcut(QKeySequence(tr("Ctrl+=", "File|ZoomIn")), this);
QShortcut *shortcutZoomOut = new QShortcut(QKeySequence(tr("Ctrl+-", "File|ZoomOut")), this);
connect(shortcutZoomIn, &QShortcut::activated,[=](){
zoomIn();
});
connect(shortcutZoomOut, &QShortcut::activated,[=](){
zoomOut();
});
//函数声明在头文件里声明
void Widget::zoomIn()
{
//获得TextEdit的当前字体信息
QFont font = ui->textEdit->font();
//获得当前字体的大小
int fontsize = font.pointSize();
if(fontsize == -1) return;
//改变大小并设置字体大小
int newfontsize = fontsize+1;
font.setPointSize(newfontsize);
ui->textEdit->setFont(font);
}
void Widget::zoomOut()
{
//获得TextEdit的当前字体信息
QFont font = ui->textEdit->font();
//获得当前字体的大小
int fontsize = font.pointSize();
if(fontsize == -1) return;
//改变大小并设置字体大小
int newfontsize = fontsize-1;
font.setPointSize(newfontsize);
ui->textEdit->setFont(font);
}
轻松实现。
事件
讲到这里我们的记事本项目其实已经十分完善了,我们再实现最后一个功能,那就是Ctrl键加上鼠标滚轮来实现字体的放大和缩小,并且补充一下事件的知识点,这是一个很重要的概念。
众所周知Qt是一个基于C++的框架,主要用来开发带窗口的应用程序。我们使用的基于窗口的应用程序都是基于事件,其目的主要是用来实现回调(因为只有这样程序的效率才是最高的)。所以在Qt框架内部为我们提供了一些列的事件处理机制,当窗口事件产生之后,事件会经过: 事件派发 -> 事件过滤->事件分发->事件处理几个阶段。Qt窗口中对于产生的一系列事件都有默认的处理动作,如果我们有特殊需求就需要在合适的阶段重写事件的处理动作,比如信号与槽就是一种
事件(event)是由系统或者 Qt 本身在不同的场景下发出的。当用户按下/移动鼠标、敲下键盘,或者是窗口关闭/大小发生变化/隐藏或显示都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如鼠标/键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
每一个Qt应用程序都对应一个唯一的 QApplication 应用程序对象,然后调用这个对象的 exec() 函数,这样Qt框架内部的事件检测就开始了( 程序将进入事件循环来监听应用程序的事件)。
在Widget.h里,它是继承于QWidget的,我们打开帮助文档,点击Protected Functions就可以看到很多可以重写的事件:
在头文件里添加protected:属性,就能重写事件,如下操作:
重写事件实现Ctrl+鼠标滚轮放大缩小字体
话不多说,跟着我来操作,右键项目,新建一个Class文件:
默认生成的类有缺陷,我们需要和基类产生关联,代码如下:
mytextedit.h(最终代码):
#ifndef MYTEXTEDIT_H
#define MYTEXTEDIT_H
#include <QTextEdit>
#include <QWheelEvent>
///
/// \brief 自定义文本编辑控件类
/// 继承自 QTextEdit,扩展了以下功能:
/// 1. Ctrl+滚轮缩放字体
/// 2. 自定义键盘事件处理
///
class MyTextEdit : public QTextEdit
{
public:
/// \brief 构造函数
/// \param parent 父控件指针
///
/// 需要显式传递父控件指针的原因:
/// 1. Qt 对象树管理:父控件负责子控件内存回收
/// 2. 确保控件在界面布局中正确显示层级关系
/// 3. 符合 Qt 标准控件构造规范
MyTextEdit(QWidget *parent);
protected:
/// \brief 重写滚轮事件(实现字体缩放功能)
void wheelEvent(QWheelEvent *e) override;
/// \brief 重写键盘按下事件(跟踪 Ctrl 键状态)
void keyPressEvent(QKeyEvent *e) override;
/// \brief 重写键盘释放事件(跟踪 Ctrl 键状态)
void keyReleaseEvent(QKeyEvent *e) override;
private:
/// \brief Ctrl 键按下状态标志
///
/// 用于实现组合键功能(如 Ctrl+滚轮缩放时判断是否按住 Ctrl):
/// - true : Ctrl 键被按下
/// - false : Ctrl 键未被按下
bool KeyCtrlPressed = false; // 建议使用 false 初始化更符合 bool 类型语义
};
#endif // MYTEXTEDIT_H
mytextedit.cpp(最终代码):
#include "mytextedit.h"
#include <QWheelEvent>
#include <QDebug>
///
/// \brief 构造函数
/// \param parent 父控件指针
///
/// 显式调用基类 QTextEdit 的构造函数
/// - 确保父控件能正确管理本控件生命周期
/// - 继承 QTextEdit 的所有默认行为
MyTextEdit::MyTextEdit(QWidget *parent) : QTextEdit(parent)
{
// 此处可添加初始化代码(如默认字体设置等)
}
///
/// \brief 滚轮事件处理(实现Ctrl+滚轮字体缩放)
/// \param e 滚轮事件对象
///
/// 功能逻辑:
/// 1. 当Ctrl键按下时,执行字体缩放
/// 2. 未按下Ctrl键时保持默认滚动行为
void MyTextEdit::wheelEvent(QWheelEvent *e)
{
if(KeyCtrlPressed == 1) // 检查Ctrl键状态
{
// 获取滚轮滚动方向(正值向上,负值向下)
if(e->angleDelta().y() > 0) // 滚轮向上滚动
{
zoomIn(); // 放大字体(基类方法,默认步长+1)
}
else if(e->angleDelta().y() < 0) // 滚轮向下滚动
{
zoomOut(); // 缩小字体(基类方法,默认步长-1)
}
e->accept(); // 标记事件已处理(阻止继续传递)
}
else // 未按下Ctrl键时
{
QTextEdit::wheelEvent(e); // 调用基类默认处理(滚动内容)
}
}
///
/// \brief 键盘按下事件处理(跟踪Ctrl键状态)
/// \param e 键盘事件对象
///
/// 注意:需调用基类实现以保证正常文本输入处理
void MyTextEdit::keyPressEvent(QKeyEvent *e)
{
if(e->key() == Qt::Key_Control) // 检测Ctrl键按下
{
KeyCtrlPressed = 1; // 设置状态标志
}
QTextEdit::keyPressEvent(e); // 必须调用基类实现
}
///
/// \brief 键盘释放事件处理(跟踪Ctrl键状态)
/// \param e 键盘事件对象
///
/// 注意:需调用基类实现以保证正常文本输入处理
void MyTextEdit::keyReleaseEvent(QKeyEvent *e)
{
if(e->key() == Qt::Key_Control) // 检测Ctrl键释放
{
KeyCtrlPressed = 0; // 清除状态标志
}
QTextEdit::keyReleaseEvent(e); // 必须调用基类实现
}
至此,该项目就完成了,如果有可以改进的地方,欢迎在评论区留言,完整代码在文章开头下载,希望大家关注支持一下,后续还会更新质量更高的文章。