先说两句
最近偷懒了很久没有学习,对于自己的懒惰实在是看不下了,所以决定随便学点什么。但是最近没什么学习的方向,想起之前学过一点点Qt,但是没有学完,所有打算先把Qt学完,说不定以后能用上,随便记录一下学习笔记。我自己是一个自制力比较差的人,但是有点强迫症,如果了笔记没写完,会非常不舒服,所有以后以后把学的东西都记一下笔记,能一定程度上约束一下自己。突然想起之前写的shell脚本笔记还有sed
命令还没学完,emmm…下次再补。
Qt简介
Qt是一个跨平台的C++开发库,主要用来开发图形用户界面程序。Qt还存在Python、Ruby、Perl等脚本语言的绑定,也就是说可以使用脚本语言开发基于 Qt 的程序。开源社区就是这样,好东西就会被派生扩展,到处使用,越来越壮大。
Qt支持的操作系统有很多,例如通用操作系统Windows、Linux、Unix,智能手机系统Android、iOS、WinPhone,嵌入式系统QNX、VxWorks等等。
上面是从C语言中文网上抄的Qt简介,附上Qt的官网,感兴趣的小伙伴可以去看看。
Qt包含很多C++的类,但是头文件代码里的注释基本没有,2333…,所以只能去看Qt官网上的文档:https://doc.qt.io/qt-5/classes.html,里面有所有类的说明。因为类太多了,所以后面关于类的成员函数就不解析了,小伙伴们有不清楚的地方可以去官网上查。
环境搭建
我自己是在linuxmint系统下用VScode搭建的,具体参考这篇博客,其他系统下搭建环境的小伙伴也可以参考一下。
pro文件配置
pro文件只要用于配置Qt项目的编译,具体配置方式抄参考了一下大佬的这篇文章
常用配置项
注释 : 注释是从一行的
#
开始,到这一行的结束。QT +=
: 这个是添加QT项目需要的模块的,若项目中要排除某个模块,也可用QT -=
配置项。TEMPLATE =
: 这个配置项确定qmake
为这个应用程序生成哪种makefile
。有下面五种形式可供选择:app
: 建立一个应用程序的makefile,这个是默认值,若模块项未指定,将默认使用此项;lib
: 建立一个库的makefile;vcapp
: 建立一个应用程序的VisualStudio项目文件;vclib
: 建立一个库的VisualStudio项目文件;subdirs
: 这是一个特殊的模板,可以创建一个可进入特定目录并为一个项目文件生成makefile,此makfile可以调用make;
TARGET =
: 这个配置项用来指定最后生成的目标应用程序的名称。CONFIG +=
: 用来告诉qmake关于应用程序的配置信息,使用+=表示在现有的配置上添加,这样会更安全。比如,CONFIG += qt warn_on release
其具体的意义为:qt
: 告诉qmake此程序是使用qt来连编的。即qmake在连接、为编译添加所需包含路径时会考虑qt的库;warn_on
: 告诉qmake要将编译器设置为输出警告信息形式;release
: 告诉qmake应用程序必须被连编为一个可发布的应用程序。开发过程中,也可以使用debug
;
UIC_DIR +=
: 用来指定uic
命令,将.ui
文件转化为ui_*.h
文件存放的目录。RCC_DIR +=
: 用来指定rcc
命令,将.qrc
文件转换成qrc_*.h
文件存放的目录。MOC_DIR +=
: 用来指定moc
命令,将含有Q_OBJECT
的头文件转换成标准.h
文件存放的目录。OBJECTS_DIR +=
: 用来指定目标文件obj
的存放目录。DEPENDPATH +=
: 用来指定工程的依赖路径。INCLUDEPATH +=
: 用来指定工程所需要的头文件。CODECFORSRC +=
: 用来指定源文件的编码格式。FORMS +=
: 用来指定工程中的ui
文件。HEADERS +=
: 用来指定工程中所包含的头文件。SOURCES +=
: 用来指定工程中包含的源文件。RESOURCES +=
: 用来指定工程中所包含的资源文件。LIBS +=
: 用来指定引入的lib
文件的路径,一般会在前面加下参数-L
,根据不同的版本可以分为两种形式:1
2Release: LIBS += -L folder_Path # release版本引入的lib文件
Debug: LIBS += -L folder_Path # debug版本引入的lib文件DEFINES +=
: 用来定义编译选项。DESTDIR +=
: 用来指定目标的生成路径。跨平台处理信息也要写在pro文件中。 其示例如下:
1
2
3
4
5
6
7
8
9win32
{
....
}
unix
{
...
}
pro文件样例
下面是大佬给的一个pro文件样例:
1 | # 添加QT依赖的库 |
GUI
Qt的很多GUI控件都是继承自QWidget,所以使用Qt的GUI一般都需要加上widgets
和gui
这两个库,pro项目文件样例如下:
1 | # 添加widgets和gui库 |
QWidget
QWidget是Qt的窗口类,下面是一段样例代码:
1 |
|
上面的代码中,创建了一个标题是Hello World
的窗口。执行app.exec();
后,会进入一个处理消息的死循环,app
会捕获系统消息,并传递给widget
窗口。这就是鼠标点击后能拖动窗口的原因,app
将鼠标的点击、移动等消息传递给了widget
,widget
作出了对应的响应,窗口移动了位置。
控件
Qt里有很多控件,大部分都是继承自Qwidget
,所以控件也可以看作是一个窗口,这里列举一些常用的控件(抄大佬的这篇博客):
控件 | 类名 | 描述 |
---|---|---|
标签 | QLabel | 显示一个文本或图像。 |
按钮 | QPushButton | 用户可以点击的一个按钮,用来触发某个操作。 |
输入框 | QLineEdit | 用户可以在其中输入文本的一个输入框。 |
复选框 | QCheckBox | 用户可以勾选或取消的一个复选框。 |
单选按钮 | QRadioButton | 用户可以选择其中一个选项的一组单选按钮。 |
数字微调框 | QSpinBox | 用于选择一个数值的微调框。 |
滑动条 | QSlider | 用户可以通过滑动来选择数值的一个滑动条。 |
列表框 | QListWidget | 用于显示一组列表项的一个列表框。 |
组合框 | QComboBox | 类似于下拉菜单的一个组合框,用户可以选择其中一个选项。 |
多行文本框 | QTextEdit | 用户可以在其中编辑多行文本的一个文本编辑框。 |
日期和时间编辑框 | QDateTimeEdit | 用于选择日期和时间的一个日期和时间编辑框。 |
由于控件种类太多了,这里只介绍一下按钮QPushButton
和输入框QLineEdit
这两种控件,其他控件的很多性质和接口都比较相似,小伙伴们可以自己尝试,我就偷懒不写了。
按钮-QPushButton
下面是一个在窗口中添加一个按钮的样例:
1 |
|
上面的代码会创建一个窗口,窗口中包含一个按钮。在Qt中,QPushButton
是QWidget
的子类,所以按钮也可以看作是一个窗口。
Qt里的窗口可以存在父子关系,上面的代码中,button.setParent(&widget);
的作用是将button
按钮的父窗口设置成widget
,设置后button
按钮才会显示在widget
窗口里面;如果不设置,button
按钮会显示成一个独立的窗口,与widget
窗口同级。
一个窗口在执行show()
成员函数时,会将已经添加的子窗口也一起显示,如果是子窗口是在父窗口执行show()
之后添加的,则子窗口不会显示,子窗口也需要执行show()
才能显示出来。比如,去掉上面代码中的button.show()
之后,虽然之后将button
按钮添加到了父窗口widget
里,但是widget
窗口里的button
按钮不显示。如果在widget
窗口执行show()
之前,将button
按钮添加到了父窗口widget
里,也就是在widget.show()
之前执行button.setParent(&widget)
,则即使button
按钮不执行show()
,也会在父窗口widget
执行show()
的时候一起显示出来。
输入框-QLineEdit
下面是一个输入框的样例代码:
1 |
|
上面的代码向窗口中添加了两个输入框,一个用来输入用户名,一个用来输入密码。用户名输入框设置了一个completer
,可以将输入的字符串与completer
的字符串列表进行匹配,显示匹配成功的字符串。密码输入框设置了设置回显模式为密码模式,可以将输入的字符显示为●
。
坐标体系
Qt里的Qwidget
可以使用setGeometry()
来设置控件的位置坐标,位置坐标是相对与父窗口的左上角计算的,下面是一个例子:
1 | QWidget widget; |
上面的例子中,setGeometry()
有4个参数,前两个参数是button
相对父窗口的坐标ax,ay,后面两个是button
的宽度和高度aw,ah。
layout
当一个窗口里控件很多时,使用setGeometry()
来设置很麻烦,而且无法随着窗口的大小变化而调整。使用layout(布局)就可以很方便的解决这个问题,他们负责一组控件的几何管理。上面输入框-QLineEdit里的样例代码,使用的QVBoxLayout
就是一种layout,可以将控件在垂直方向上排列,使得name_input
输入框在password_input
输入框上面,如果不使用layout,则两个输入框会重叠在一起。(以下内容大部分抄的大佬的这篇博客)
简述
Qt布局系统提供了一种简单而强大的方法,可以在控件内自动排列子控件,以确保它们充分利用可用空间。Qt包含一组布局管理类,用于描述控件在应用程序用户界面中的布局方式。当控件的可用空间发生变化时,这些layout会自动定位和调整控件的大小,确保它们的排列一致并且用户界面作为一个整体仍然可用。
所有QWidget
子类都可以使用layout来管理它们的子类。QWidget::setLayout()
函数可以为一个控件设置layout。 当以这种方式在窗口上设置layout时,它负责以下任务:
- 布置子控件。
- 最高层窗口可感知的默认大小。
- 最高层窗口可感知的最小大小。
- 调整大小的处理。
- 当内容改变的时候自动更新:
- 字体大小、文本或者子控件的其它内容。
- 隐藏或者显示子控件。
- 移除一些子控件。
常用layout
为控件提供良好布局的最简单方法是使用Qt内置的布局管理器:QHBoxLayout
、QVBoxLayout
、QGridLayout
和QFormLayout
。这些类从QLayout
继承,而QLayout
又从QObject
(而不是QWidget
)派生。他们负责一组控件的几何管理。要创建更复杂的布局,可以将布局管理器相互嵌套。
QHBoxLayout
:从左到右在水平行中布置控件。QVBoxLayout
:在垂直列中从上到下布置控件。QGridLayout
:在二维网格中布置控件,控件可以占用多个单元格。QFormLayout
:把控件按照标签-输入框的形式排列在两列。
为layout添加控件
将控件添加到一个layout时,布局过程如下:
- 所有控件最初将根据它们的
QWidget::sizePolicy()
和QWidget::sizeHint()
分配一定数量的空间。 - 如果任何控件设置了拉伸系数,并且其值大于零,那么它们将按其拉伸因子的比例分配空间(如下伸展因素所述)。
- 如果任何控件的拉伸系数设置为零,它们只会在没有其他控件需要空间的情况下获得更多空间。其中,空间首先分配给具有扩展大小策略的控件。
- 任何控件被分配的空间的大小如果小于它们的最小大小(如果未指定最小尺寸,则为最小尺寸提示),它们就会被按它们所需要的最小大小分配空间。(如果控件的伸展因素是它们的决定因素,它们不必有最小大小或者最小大小的提示。)
- 任何控件被分配的空间的大小如果大于它们的最大大小,它们就会被按它们所需要的最大大小分配空间。(如果控件的伸展因素是它们的决定因素,它们不必有最大大小。)
伸展因素
控件通常是在没有伸展因素设置的情况下被生成的。当它们被布置到一个layout中时,控件会被根据它们的QWidget::sizePolicy()
或者它们的最小大小的提示中大的那一个分配给整个空间的一部分。伸展因素是用来根据控件互相的比例来改变它们所被分配的空间。
如果使用一个QHBoxLayout
来布置没有伸展参数设置的三个控件,则我们就会得到像下面这样的布局:
如果我们给每个控件设置一个伸展因素,它们就会被按比例布置(但是不能小于最小大小的提示),以下是按1:3:2设置的:
简单的demo
布局中常用的方法有addWidget()
和addLayout()
,addWidget()
方法用于向layout中加入需要布局的控件,addLayout()
方法用于向layout中加入子布局。
1 | void addWidget( |
下面是一个样例:
1 |
|
主窗口
抄了大佬的这篇博客。
主窗口QMainWindow
是一个为用户提供主窗口程序的类,包含一个菜单栏(menu bar)、多个工具栏(tool bars)、多个浮动窗口(dock widgets)、一个状态栏(status bar)及一个中心区域(central widget),主窗口是许多应用程序的基础,如文本编辑器,图片编辑器等。
下面是一个使用QMainWindow
的样例:
1 |
|
运行结果如下图:
菜单栏
一个主窗口最多只有一个菜单栏。通过QMainWindow
类的menuBar()
函数可以获取主窗口菜单栏指针,如果当前窗口没有菜单栏,该函数会自动创建一个。
Qt 并没有专门的菜单项类,只是使用一个QAction
类,抽象出公共的动作。当我们把QAction
对象添加到菜单,就显示成一个菜单项,添加到工具栏,就显示成一个工具按钮。用户可以通过点击菜单项、点击工具栏按钮、点击快捷键来激活这个动作。
工具栏
主窗口可以有多个工具栏,通常采用一个菜单对应一个工具栏的的方式,也可根据需要进行工具栏的划分。
调用QMainWindowd
对象的成员函数addToolBar()
会创建一个新的工具栏,并且返回该工具栏的指针。通过QToolBar
类的addAction()
函数可以添加插入属于工具栏的项,工具栏上添加项也是用QAction
。
工具栏是一个可移动的窗口,它的可停靠区域由QToolBar
的allowAreas
决定,包括以下可用值:
Qt::LeftToolBarArea
:停靠在左侧Qt::RightToolBarArea
:停靠在右侧Qt::TopToolBarArea
:停靠在顶部Qt::BottomToolBarArea
:停靠在底部Qt::AllToolBarAreas
:以上四个位置都可停靠
使用setAllowedAreas()
函数指定停靠区域,使用setFloatable()
函数可以设置工具栏是否可以浮动。
状态栏
一个QMainWindow
的程序最多只有一个状态栏。QMainWindow
中可以有多个的部件都使用add…名字的函数,而只有一个的部件,就直接使用获取部件的函数,如menuBar。同理状态栏也提供了一个获取状态栏的函数statusBar()
,没有就自动创建一个并返回状态栏的指针,状态栏可以使用addWidget()
接口来添加内容。
浮动窗口
即QDockWidget
,也称为铆接部件,可以有多个。主窗口可以使用addDockWidget()
成员函数添加浮动窗口。
中心区域
除了以上几个部件占用的区域外,剩下的区域都是中心区域,中心区域只有一个,使用setCentralWidget()
函数设置中心区域。
系统托盘图标
系统托盘图标并不是QMainWindow
里的内容,Qt 中使用QSystemTrayIcon
类来创建系统托盘图标,可以使用QSystemTrayIcon
的setIcon()
成员函数设置图标,使用setContextMenu()
设置鼠标右键菜单。
对话框
抄了大佬的这篇博客。
对话框是GUI程序中不可或缺的组成部分。⼀些不适合在主窗⼝实现的功能组件可以设置在对话框中。对话框通常是⼀个顶层窗口,出现在程序最上层,⽤于实现短期任务或者简洁的用户交互。对话框主要可以分为模态对话框和⾮模态对话框。
Qt中的对话框类为QDialog
,是QWidget
的子类,QWidget
的各种属性方法,在QDialog
也同样适用。
模态对话框
模态对话框的特点是:显示后无法与父窗口进行交互,是⼀种阻塞式的对话框。在 Qt 中使用QDialog::exec()
函数调用。模态对话框适用于必须依赖用户选择的场合,⽐如消息显示,文件选择,打印设置等。
1 | QDialog model_dialog; |
非模态对话框
非模态对话框的特点是:显示后独立存在,可以同时与父窗口进行交互,是⼀种非阻塞式对话框,使用QDialog::show()
函数调用。⾮模态对话框⼀般在堆上创建,这是因为如果创建在栈上时,当函数运行结束后,弹出的⾮模态对话框会被释放。⾮模态对话框适用于特殊功能设置的场合,⽐如查找操作,属性设置等。
Qt 可以通过设置Qt:WA_DeleteOnClose
属性,实现在对话框在关闭的时候被delete
掉,避免内存泄漏。
1 | QDialog *modaless_dialog = new QDialog(); |
Qt常用的对话框
Qt 提供了多种可复复用的对话框类型,即Qt标准对话框。Qt标准对话框全部继承于QDialog
类。常用标准对话框如下:
QMessageBox
:消息对话框QFileDialog
:文件对话框QColorDialog
:颜色对话框QFontDialog
:字体对话框QInputDialog
:输入对话框
消息对话框
消息对话框主要用于为用户提示重要消息,强制用户进行选择操作。QMessageBox
提供了多种静态方法来快速显示不同类型的消息框:
- information:显示一个信息消息框
1
QMessageBox::information(nullptr, "信息消息框", "假装这是一条信息消息");
- warning:显示一个警告消息框
1
QMessageBox::warning(nullptr, "警告消息框", "假装这是一条警告消息");
- critical:显示一个严重错误消息框
1
QMessageBox::critical(nullptr, "错误消息框", "假装这是一条错误消息");
- question:显示一个问题消息框,允许用户做出选择
1
2
3
4
5
6
7int ret = QMessageBox::question(nullptr, "问题消息框", "假装这是一个问题");
if (ret == QMessageBox::Yes) {
qDebug() << "return yes";
}
else {
qDebug() << "return no";
}
文件对话框
文件对话框QFiledialog
是一个模态对话框,主要用于让用户可以浏览文件系统、并选择文件或目录。
- 打开一个文件,返回文件的绝对路径
1
QString file_path = QFileDialog::getOpenFileName();
- 打开多个文件,返回文件的绝对路径列表
1
QStringList file_path_list = QFileDialog::getOpenFileNames();
- 打开目录,返回目录的绝对路径
1
QString dir_path = QFileDialog::getExistingDirectory();
- 保存文件,返回文件的绝对路径
1
QString file_path = QFileDialog::getSaveFileName();
颜色对话框
颜色对话框QColorDialog
是一个模态对话框,用于让用户选择颜色。
1 | QColor color = QColorDialog::getColor(); |
字体对话框
字体对话框QFontDialog
也是一个模态对话框,用于让用户选择字体的样式、大小、粗细等属性。
1 | bool flag = false; |
输入对话框
输入对话框QInputDialog
也是一个模态对话框,主要用于让用户输入文本、数字或选择列表中的一个选项。
- 浮点数输入对话框
1
double ret = QInputDialog::getDouble(nullptr, "输入对话框", "输入浮点数");
- 整数输入对话框
1
int ret = QInputDialog::getInt(nullptr, "输入对话框", "输入整数");
- 选项列表输入对话框
1
2QString ret = QInputDialog::getItem(nullptr, "输入对话框", "选择选项",
{"选项_1", "选项_2", "选项_3"});
画板
抄了大佬的这篇博客。
QPainter
是 Qt 中用于进行绘图操作的类。它提供了各种绘制函数,可以在不同的绘图设备上进行绘制,如窗口、图像、打印机等。
以下是QPainter
类的一些常用属性和方法:
begin(QPaintDevice *device)
: 在给定的绘图设备上开始绘制操作。end()
: 结束绘制操作。drawText(const QRectF &rectangle, const QString &text)
: 绘制指定矩形区域内的文本。drawImage(const QRectF &target, const QImage &image, const QRectF &source)
: 在目标矩形区域内绘制源图像的一部分。setPen(const QPen &pen)
: 设置绘制的画笔样式。setBrush(const QBrush &brush)
: 设置绘制的画刷样式。setFont(const QFont &font)
: 设置绘制的字体样式。translate(const QPointF &offset)
: 将绘图坐标原点平移指定的偏移量。scale(qreal sx, qreal sy)
: 沿着x轴和y轴方向对绘图进行缩放。rotate(qreal angle)
: 以原点为中心,按照给定的角度旋转绘图。save()
: 保存当前的绘图状态,包括画笔、画刷、字体等设置。restore()
: 恢复上一次保存的绘图状态。
这些方法和属性只是QPainter
类的一部分,还有其他许多功能可以用于绘制不同的图形和效果。可以根据需要在QPainter
文档中进一步了解更多细节。
下面是一个简单的示例,演示了如何使用QPainter
在窗口上进行绘制:
1 |
|
在上述示例中,我们创建了一个自定义的QWidget
派生类my_painter
,并重写了paintEvent()
函数。在paintEvent()
函数中,我们创建了一个QPainter
对象,将其关联到窗口上,并使用一些绘制函数,在窗口的矩形区域内绘制图形。最后,我们创建了一个my_painter
对象并显示窗口,绘制的图形将在窗口中心显示。运行的结果如下:
这只是一个简单的示例,你可以根据需要使用其他绘图函数和属性来绘制更复杂的图形和效果。
事件
抄了大佬的这篇博客
事件定义
事件(event)是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标,敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件是在对用户操作做出响应的时候发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
事件与信号槽
一般来说,使用 Qt 编程时,我们并不会把主要精力放在事件上,因为在 Qt 中,需要我们关心的事件总会发出一个信号。比如,我们关心的是QPushButton
的鼠标点击,但我们不需要关心这个鼠标点击事件,而是关心它的clicked()
信号。
- 信号槽:
signal
由具体对象发出,然后会马上交给由connect
函数连接的slot
进行处理。 - 事件:Qt 使用一个事件队列对所有发出的事件进行维护,当新的事件产生时,会被追加到事件队列的尾部,前一个事件完成后,取出后面的事件进行处理。但是,必要的时候,Qt 的事件也是可以不进入事件队列,而是直接处理的。并且,事件还可以使用“事件过滤器”进行过滤。
总的来说,如果我们使用组件,我们关心的是信号槽;如果我们自定义组件,我们关心的是事件。因为我们可以通过事件来改变组件的默认操作。比如,如果我们要自定义一个QPushButton
,那么我们就需要重写它的鼠标点击事件和键盘处理事件,并且在恰当的时候发出clicked()
信号。
事件循环
我们在 main 函数里面创建了一个QApplication
对象,然后调用了它的exec()
函数。其实,这个函数就是开始 Qt 的事件循环。在执行exec()
函数之后,程序将进入事件循环来监听应用程序的事件。
事件处理函数
当事件发生时,Qt 将创建一个事件对象。Qt 的所有事件都继承于QEvent
类。在事件对象创建完毕后,Qt 将这个事件对象传递给 QObject 的event()
函数。event()
函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler) 。
例如在所有组件的父类 QWidget 中,定义了很多事件处理函数 ,如keyPressEvent()
、keyReleaseEvent()
、mouseDoubleClickEvent()
、mouseMoveEvent ()
、mousePressEvent()
、mouseReleaseEvent()
等。这些函数都是protected virtual
的,也就是说,我们应该在子类中重写这些函数。下面是一个重写事件处理函数的例子:
1 |
|
上面的例子中,我们定义了一个QWidget
的子类event_widget
,重写了一些事件处理函数。
事件接受与忽略
前面的例子中我们在重写事件处理函数时,都会调用父类对应的事件处理函数。这在某种程度上说,是把事件向上传递给父类去响应,也就是说,我们在子类中“忽略”了这个事件。
我们可以把 Qt 的事件传递看成链状:如果子类没有处理这个事件,就会继续向其他类传递。其实,Qt 的事件对象都有一个accept()
函数和ignore()
函数。正如它们的名字,前者用来告诉 Qt,事件处理函数“接收”了这个事件,不要再传递;后者则告诉 Qt,事件处理函数“忽略”了这个事件,需要继续传递,寻找另外的接受者。在事件处理函数中,可以使用isAccepted()
来查询这个事件是不是已经被接收了。
事实上,我们很少使用accept()
和ignore()
函数,而是像上面的示例一样,如果希望忽略事件,只要调用父类的响应函数即可。
Qt 中的事件大部分是protected
的,因此,重写的函数必定存在着其父类中的响应函数,这个方法是可行的。为什么要这么做呢?因为我们无法确认父类中的这个处理函数没有操作,如果我们在子类中直接忽略事件,Qt 不会再去寻找其他的接受者,那么父类的操作也就不能进行,这可能会有潜在的危险。
在一个情形下,我们必须使用accept()
和ignore()
函数,那就是在窗口关闭的时候。如果在窗口关闭时需要有个询问对话框,那么就需要这么去写:
1 | /* 窗口关闭事件处理 */ |
这样,我们经过询问之后才能正常退出程序。
event()函数
事件对象创建完毕后,Qt 将这个事件对象传递给QObject
的event()
函数。event()
函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。
event()
函数主要用于事件的分发,所以,如果希望在事件分发之前做一些操作,那么,就需要注意这个event()
函数了。为了达到这种目的,我们可以重写event()
函数。
例如,如果希望在窗口中的 tab 键按下时将焦点移动到下一组件,而不是让具有焦点的组件处理,那么就可以继承 QWidget ,并重写它的event()
函数,以达到这个目的:
1 | bool MyWidget::event(QEvent *event) |
event()
函数接受一个 QEvent 对象,也就是需要这个函数进行转发的对象。为了进行转发,必定需要有一系列的类型判断,这就可以调用 QEvent 的type()
函数,其返回值是QEvent::Type
类型的枚举。
我们处理过自己需要的事件后,可以直接return
回去,对于其他我们不关心的事件,需要调用父类的event()
函数继续转发,否则这个组件就只能处理我们定义的事件了。
event()
函数返回值是bool
类型,如果传入的事件已被识别并且处理,返回true
,否则返回false
。如果返回值是true
,QApplication 会认为这个事件已经处理完毕,会继续处理事件队列中的下一事件;如果返回值是false
,QApplication 会尝试寻找这个事件的下一个处理函数。
event()
函数的返回值和事件的accept()
和ignore()
函数不同。accept()
和ignore()
函数用于不同的事件处理器之间的沟通,例如判断这一事件是否处理;event()
函数的返回值主要是通知 QApplication 的notify()
函数是否处理下一事件。
为了更加明晰这一点,我们来看看 QWidget 的event()
函数是如何定义的:
1 | bool QWidget::event(QEvent *event) |
QWidget 的event()
函数使用一个巨大的 switch 来判断 QEvent 的 type,并且分发给不同的事件处理函数。在事件处理函数之后,使用这个事件的isAccepted()
方法,获知这个事件是不是被接受,如果没有被接受则event()
函数立即返回false
,否则返回true
。
另外一个必须重写event()
函数的情形是有自定义事件的时候。如果程序中有自定义事件,则必须重写event()
函数以便将自定义事件进行分发,否则自定义事件永远也不会被调用。
事件过滤器
Qt 创建了 QEvent 事件对象之后,会调用 QObject 的event()
函数做事件的分发。有时候,可能需要在调用event()函数之前做一些另外的操作,比如,对话框上某些组件可能并不需要响应回车按下的事件,此时,就需要重新定义组件的event()
函数。如果组件很多,就需要重写很多次event()
函数,这显然没有效率。为此,可以使用一个事件过滤器,来判断是否需要调用event()
函数。
QOjbect 有一个eventFilter()
函数,用于建立事件过滤器。这个函数的签名如下:
1 | virtual bool QObject::eventFilter(QObject * watched, QEvent * event) |
如果 watched 对象安装了事件过滤器,这个函数会被调用并进行事件过滤,然后才轮到组件进行事件处理。在重写这个函数时,如果需要过滤掉某个事件,例如停止对这个事件的响应,需要返回true。
1 | /* 重载消息过滤器 */ |
上面的例子中为 event_filter_t 建立了一个事件过滤器。为了过滤某个组件上的事件,首先需要判断这个对象是哪个组件,然后判断这个事件的类型。
例如,我不想让 textEdit 组件处理键盘事件,于是就首先找到这个组件,如果这个事件是键盘事件,则直接返回true
,也就是过滤掉了这个事件。对于其他组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。
在创建了过滤器之后,下面要做的是安装这个过滤器。安装过滤器需要调用installEventFilter()
函数。这个函数的签名如下:
1 | void QObject::installEventFilter(QObject* filterObj) |
这个函数是 QObject 的一个函数,因此可以安装到任何 QObject 的子类,并不仅仅是 UI 组件。这个函数接收一个 QObject 对象,调用了这个函数安装事件过滤器的组件会调用 filterObj 定义的eventFilter()
函数。
例如,textField.installEventFilter(obj)
,则如果有事件发送到 textField 组件是,会先调用obj->eventFilter()
函数,然后才会调用textField.event()
。
当然,你也可以把事件过滤器安装到 QApplication 上面,这样就可以过滤所有的事件,已获得更大的控制权。不过,这样做的后果就是会降低事件分发的效率。
如果一个组件安装了多个过滤器,则最后一个安装的会最先调用,类似于堆栈的行为。
注意:如果你在事件过滤器中delete
了某个接收组件,务必将返回值设为true
。否则,Qt 还是会将事件分发给这个接收组件,从而导致程序崩溃。
事件过滤器和被安装的组件必须在同一线程,否则,过滤器不起作用。另外,如果在 install 之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
事件的调用最终都会调用 QCoreApplication 的notify()
函数,因此,最大的控制权实际上是重写 QCoreApplication 的notify()
函数。由此可以看出,Qt 的事件处理实际上是分层五个层次:
- 重定义事件处理函数
- 重定义 event()函数
- 为单个组件安装事件过滤器
- 为 QApplication 安装事件过滤器
- 重定义 QCoreApplication 的
notify()
函数
这几个层次的控制权是逐层增大的。
自定义事件
Qt 允许创建自己的事件类型,这在多线程的程序中尤其有用,当然,也可以用在单线程的程序中,作为一种对象间通讯的机制。那么,为什么需要使用事件,而不是使用信号槽呢?主要原因是,事件的分发既可以是同步的,又可以是异步的,而函数的调用或者说是槽的回调总是同步的。事件的另外一个好处是,它可以使用过滤器。
Qt 中的自定义事件很简单,同其他类似的库的使用很相似,都是要继承一个类进行扩展。在 Qt 中,你需要继承的类是QEvent
。
继承 QEvent 类,你需要提供一个QEvent::Type
类型的参数,作为自定义事件的类型值。这里的 QEvent::Type 类型是 QEvent 里面定义的一个 enum,因此,你是可以传递一个 int 的。重要的是,你的事件类型不能和已经存在的 type 值重复,否则会有不可预料的错误发生!因为系统会将你的事件当做系统事件进行派发和调用。
在 Qt 中,系统将保留0 - 999
的值,也就是说,你的事件 type 要大于999. 具体来说,你的自定义事件的 type 要在 QEvent::User 和 QEvent::MaxUser 的范围之间。其中,QEvent::User 值是1000,QEvent::MaxUser 的值是65535。从这里知道,你最多可以定义64536个事件,相信这个数字已经足够大了!
但是,即便如此,也只能保证用户自定义事件不能覆盖系统事件,并不能保证自定义事件之间不会被覆盖。为了解决这个问题,Qt 提供了一个函数:registerEventType()
,用于自定义事件的注册。该函数签名如下:
1 | static int QEvent::registerEventType(int hint = -1); |
函数是 static 的,因此可以使用 QEvent 类直接调用。函数接受一个 int 值,其默认值为-1,返回值是创建的这个 Type 类型的值。如果 hint 是合法的,不会发生任何覆盖,则会返回这个值;如果hint不合法,系统会自动分配一个合法值并返回。因此,使用这个函数即可完成 type 值的指定。这个函数是线程安全的,因此不必另外添加同步。
你可以在 QEvent 子类中添加自己的事件所需要的数据,然后进行事件的发送。Qt 中提供了两种发送方式:
static bool QCoreApplication::sendEvent(QObjecy receiver, QEvent event)
:事件被
QCoreApplication 的 notify()函数直接发送给 receiver 对象,返回值是事件处理函数的返回值。使用这个函数必须要在栈上创建事件对象,例如:
1 | QMouseEvent event(QEvent::MouseButtonPress, pos, 0, 0, 0); |
static bool QCoreApplication::postEvent(QObject receiver, QEvent event)
:事件被
QCoreApplication 追加到事件列表的最后,并等待处理,该函数将事件追加后会立即返回,并且注意,该函数是线程安全的。另外一点是,使用这个函数必须要在堆上创建对象,例如:
1 | QApplication::postEvent(object, new MyEvent(QEvent::registerEventType(2048))); |
这个对象不需要手动 delete,Qt 会自动 delete 掉!因此,如果在 post 事件之后调用 delete,程序可能会崩溃。另外,postEvent()函数还有一个重载的版本,增加一个优先级参数,具体请参见API。通过调用 sendPostedEvent()函数可以让已提交的事件立即得到处理。
如果要处理自定义事件,可以重写 QObject 的customEvent()
函数,该函数接收一个 QEvent 对象作为参数,也可以像前面介绍的重写event()
函数的方法去重写这个函数,这两种办法都是可行的。下面是一个使用自定义信号的例子:
1 |
|
信号和槽
抄了大佬的这篇博客。
简介
信号槽是QT中用于对象间通信的一种机制,也是QT的核心机制。在GUI编程中,我们经常需要在改变一个组件的同时,通知另一个组件做出响应。
早期,对象间的通信采用回调来实现。回调实际上是利用函数指针来实现,当我们希望某件事发生时处理函数能够获得通知,就需要将回调函数的指针传递给处理函数,这样处理函数就会在合适的时候调用回调函数。回调有两个明显的缺点:
- 它们不是类型安全的,我们无法保证处理函数传递给回调函数的参数都是正确的。
- 回调函数和处理函数紧密耦合,源于处理函数必须知道哪一个函数被回调。
在QT中,我们有回调技术之外的选择,也即是信号槽机制。所谓的信号与槽,其实都是函数。当特定事件被触发时将发送一个信号,而与该信号建立的连接槽,则可以接收到该信号并做出反应。
QT组件预定义了很多信号和槽,而在GUI编程中,我们习惯于继承那些组件,继承后添加我们自己的槽,以便以我们的方式来处理信号。槽和普通的C++成员函数几乎是一样的,它可以是虚函数,可以被重载,可以是共有、私有或是保护的,也同样可以被其他成员函数调用。它的函数参数也可以是任意类型的。唯一不同的是:槽还可以和信号连接在一起。
与回调不同,信号槽机制是类型安全的。这体现在信号的函数签名与槽的函数签名必须匹配上,才能够发生信号的传递。实际上,槽的参数个数可以比信号的参数个数少,因为槽能够忽略信号形参中多出来的参数。信号和槽是松耦合的:发出信号的类不关心哪些类将接收它的信号。QT的槽能够接收到信号的参数并调用,信号和槽都可以有任意个数的参数,它们都是类型安全的。
样例分析
首先我们要知道的是,所有继承自QObject
或者它的子类(如QWidget
)都可以包含信号槽。我们写的类也要继承自QObject
(或其子类)。所有包含了信号和槽的类都必须在声明的上部含有Q_OBJECT
宏。
下面是一个样例,一个定义了信号的类和一个定义了槽函数的类:
1 |
|
1 |
|
在这个信号类中,我们使用Qt的signals
保留字定义了一个信号函数signal_fun()
,signal_fun()
的代码会由 Qt 的 moc 工具自动生成,开发人员一定不能在自己的C++代码中实现它。反之,槽应该由开发人员来实现,在槽函数里可以使用sender()
来获取信号的发送方。需要注意的是,必须在 pro 工程文件里,使用HEADERS
添加定义了信号或槽函数类的头文件,如果只是使用INCLUDEPATH
添加头文件的路径,Qt 不会调用 moc 生成代码。
可以使用QObject::connect()
函数连接信号和槽,该函数指定了信号发送方、信号函数、信号接收方、槽函数等信息,函数的格式如下:
1 | QObject::connect( |
最后,我们可以使用 Qt 的emit
关键字发送信号,下面是一个使用connect()
和emit
的简单样例:
1 |
|
上面的例子中,信号函数有两个参数,而槽函数只有一个参数,信息函数的第二个参数会被槽函数忽略。这个例子展示了对象之间通信的一种方式。对象间可以一起工作,而不需要知道彼此的任何信息。为了达到通信的目的,只需要将它们连接起来,而这只需要通过调用QObject::connect()
函数指定一些简单信息就好。
连接
要把信号成功连接到槽,它们的参数必须具有相同的顺序和相同的类型,或者允许信号的参数比槽多,槽会自动忽略掉多出来的参数而进行调用。
一个信号可以连接多个槽
使用QObject::connect
可以把一个信号连接到多个槽,而当信号发射时,将按声明联系时的顺序依次调用槽。
1 | my_signal sign; |
多个信号可以连接同一个槽
同样的,可以让多个信号连接到同一个槽上,而且其中的任意一个信号的发送,都会调用了那个槽。
1 | my_signal sign_1, sign_2; |
一个信号可以和另外一个信号相连接
当发送第一个信号的时候,也会把第二个信号发送出去。
1 | my_signal sign_1, sign_2; |
连接可以被移除
1 | /* 移除sign.signal_fun()与slot.slot_fun()之间的连接 */ |
实际上当对象被delete时,其关联的所有连接都会失效,QT会自动移除和这个对象的所有连接。