🔥 本文专栏:Qt
🌸作者主页:努力努力再努力wz



💪 今日博客励志语录 :
你可以焦虑结果,但不要放弃积累;因为真正把你带出去的,始终是后者。
思维导图

引入
在此前的学习中,我们知道,对于一个 Qt Widgets 程序来说,其最终呈现给用户的执行结果,通常是一个可视化并且能够进行交互的图形化界面。这个图形化界面由窗口以及窗口内部的各种可视化组件共同构成,例如按钮、标签、输入框等。
在 Qt Widgets 体系中,按钮、标签、输入框等能够直接显示在界面上的组件,通常都直接或间接继承自 QWidget 类。对于 QWidget 类,我们已经比较熟悉了:它为界面组件提供了基本的可视化能力,例如位置、大小等几何属性;同时,它还在 QObject 事件机制的基础上,提供了针对可视化组件的事件处理能力,包括鼠标事件处理函数、键盘事件处理函数以及绘制事件处理函数等,例如 mousePressEvent()、keyPressEvent()、paintEvent() 等。
具体来说,QObject 类提供了 event() 虚函数,该函数是 Qt 对象接收事件的统一入口。当 Qt 将某个事件分发给具体对象时,本质上就是调用该对象的 event() 函数。而 QWidget 类重写了 event() 函数,它会根据接收到的事件类型,进一步调用对应的事件处理函数,例如 mousePressEvent()、keyPressEvent()、paintEvent() 等,从而使界面组件具备处理用户操作以及更新自身显示内容的能力。
text
QObject
└── 提供 event(QEvent *event) 虚函数
└── 形成 Qt 对象接收事件的统一入口
QWidget
└── 重写 event()
└── 针对与界面组件有关的事件类型,
进一步调用 mousePressEvent()、keyPressEvent()、
paintEvent()、resizeEvent() 等具体事件处理函数
具体组件类
└── 继承 QWidget 的事件处理能力
└── 根据自身功能进一步处理交互逻辑
因此,构建一个图形化界面的过程,本质上就是实例化出所需要的可视化组件对象,为这些对象设置相应的属性,并通过父子关系将不同组件组织起来。例如,可以将按钮、标签等组件设置为窗口对象的子组件,使它们显示在窗口内部,并随着父组件共同构成最终呈现给用户的图形化界面。
在此前的内容中,我们已经详细介绍了 QWidget 这一层所提供的通用属性,以及修改这些属性的相关接口。认识了组件所具备的基础可视化能力之后,接下来便可以进一步学习 QWidget 的具体子类,也就是实际开发中经常使用的各种界面组件。
只有了解这些组件各自能够提供的功能、相关属性以及交互方式,我们才能更加得心应手地设计和编辑图形化界面。而本文首先要介绍的,就是图形化界面中最常见的一类交互组件------按钮类。
从 QAbstractButton 到 QPushButton:图标属性、快捷键与事件触发机制
按钮类概述:QAbstractButton 与具体按钮组件
接下来,我们便正式进入按钮类的学习。
首先需要明确的是,在 Qt 中,按钮并不是只由某一个类来进行描述。由于不同按钮在功能以及外观上存在差异,因此 Qt 提供了多个具体的按钮类,例如 QPushButton、QRadioButton 和 QCheckBox 等。
其中,QPushButton 表示普通的命令按钮,通常以带有文本的矩形按钮形式显示;QRadioButton 表示单选按钮,通常由一个圆形选择标记以及对应文本构成;QCheckBox 则表示复选框,通常由一个方形选择标记以及对应文本构成,用于描述可以独立选中或取消选中的选项。
虽然这些按钮类在具体功能以及外观表现上存在差异,但是它们仍然具有一些共同的能力。例如,按钮可以显示文本或图标,可以记录自身当前是否处于按下状态;对于能够被选中的按钮,还可以记录自身当前是否处于选中状态;同时,当用户对按钮进行按下、释放或点击等操作时,按钮还可以发出对应的信号。
为了统一封装这些按钮所共有的属性和交互行为,Qt 提供了 QAbstractButton 类。QAbstractButton 继承自 QWidget,并在 QWidget 已有可视化能力和事件处理能力的基础上,进一步封装了按钮组件所共有的功能。
例如,QAbstractButton 类提供了用于描述按钮文本内容的 text 属性、用于描述可选中按钮是否处于选中状态的 checked 属性,以及用于描述按钮当前是否处于按下状态的 down 属性。同时,该类还提供了一系列与按钮操作相关的信号,例如 pressed()、released()、clicked() 和 toggled() 等。其中,我们此前已经接触过的 clicked() 信号,便是在 QAbstractButton 这一层提供的。
不过,需要注意的是,QAbstractButton 并不能被直接实例化,因为它是一个抽象类。所谓抽象类,就是包含至少一个纯虚函数,从而不能直接创建对象的类。在 QAbstractButton 中,paintEvent() 函数被声明为纯虚函数,这意味着 QAbstractButton 只负责提供按钮组件共有的属性、状态以及交互行为,而按钮最终应当以什么样的外观显示在界面上,则需要由其具体子类来完成。
因此,QPushButton、QRadioButton 和 QCheckBox 等具体按钮类,都会继承 QAbstractButton 所提供的公共能力,并在此基础上实现各自不同的外观和功能特点。
接下来,我们首先认识最常见的一种按钮组件------QPushButton 类。
QPushButton 类与图标属性
接下来,我们首先认识最常见的一种按钮组件------QPushButton 类。
QPushButton 表示普通的命令按钮,通常表现为一个带有文本标签的矩形按钮。与标签等主要用于显示内容的组件不同,QPushButton 更重要的作用是接收用户操作,并在用户点击按钮后触发对应的业务逻辑。例如,在一个登录界面中,用户点击"登录"按钮后,程序便可以执行账号验证逻辑;在一个窗口界面中,用户点击"关闭"按钮后,程序便可以关闭当前窗口。
因此,在使用 QPushButton 时,我们最常关注的就是按钮的点击交互逻辑。此前我们已经接触过 clicked() 信号,当用户完成一次按钮点击操作时,按钮便会发出该信号,程序可以将其连接到对应的槽函数,从而执行具体逻辑。
需要注意的是,clicked() 信号并不是由 QPushButton 类单独提供的,而是继承自其父类 QAbstractButton。QPushButton 作为一种具体的按钮组件,继承了 QAbstractButton 所提供的公共属性和交互能力,并在此基础上呈现为普通命令按钮的外观。
虽然 QPushButton 主要用于响应用户的点击操作,但是这并不意味着我们不需要关注它的外观属性。在实际使用图形化程序时,我们经常能够看到按钮内部除了显示文本之外,还会显示一个小图标,用于更加直观地说明按钮所代表的功能。例如,保存按钮中可能会显示保存图标,删除按钮中可能会显示垃圾桶图标。
对于 QPushButton 来说,我们同样可以为其设置需要显示的图标。不过,图标属性并不是 QPushButton 类单独具有的属性,而是由 QAbstractButton 类统一提供的公共属性。因此,不仅 QPushButton 可以显示图标,QRadioButton、QCheckBox 等其他继承自 QAbstractButton 的按钮组件,同样也可以设置图标。
要为按钮设置图标,首先需要准备一个图片资源文件。对于 Qt 程序来说,我们可以通过 Qt 资源系统将图片文件引入项目中。具体来说,可以在项目中创建一个 .qrc 资源文件,该文件采用 XML 格式,用于描述程序所需要使用的资源文件,以及这些资源在 Qt 资源系统中的虚拟路径前缀。
例如,可以在 .qrc 文件中添加一个图片资源:
xml
<RCC>
<qresource prefix="/icons">
<file>N.jpg</file>
</qresource>
</RCC>
其中,prefix 用于描述资源路径的前缀,而 <file> 标签中填写的是需要引入项目中的图片文件。当该资源被加入项目之后,程序便可以通过资源路径 :/icons/N.jpg 来访问对应的图片。
当项目被构建时,Qt 的 rcc 工具会读取 .qrc 文件,并生成对应的资源代码。这些资源代码中会保存图片等资源文件的数据,并最终随着程序一同被编译和链接。因此,在程序运行时,便可以直接通过资源路径访问已经被嵌入程序中的图片资源,而不需要额外依赖磁盘上的原始图片文件。
准备好图片资源之后,我们便可以定义一个 QIcon 对象,并将对应的资源路径传递给该对象:
cpp
QIcon icon(":/icons/N.jpg");
QIcon 类用于描述程序中需要使用的图标资源。创建好 QIcon 对象之后,便可以调用按钮提供的 setIcon() 接口,将该图标设置给按钮:
cpp
ui->pushButton->setIcon(icon);
当然,也可以直接将 QIcon 对象作为参数传递给 setIcon() 接口:
cpp
ui->pushButton->setIcon(QIcon(":/icons/N.jpg"));
这里的 setIcon() 接口同样不是由 QPushButton 类单独提供的,而是继承自 QAbstractButton 类。调用该接口之后,本质上就是将对应的图标资源设置为当前按钮的 icon 属性。当按钮需要显示在界面上或者重新进行绘制时,Qt 便会根据按钮当前的图标属性、状态以及界面样式,将文本和图标一同显示在按钮内部。
除了设置按钮所显示的图标之外,我们还可以设置图标在按钮中的显示大小。QAbstractButton 类提供了 setIconSize() 接口,该接口接收一个 QSize 对象,用于描述图标显示时所使用的宽度和高度:
cpp
ui->pushButton->setIcon(QIcon(":/icons/N.jpg"));
ui->pushButton->setIconSize(QSize(32, 32));
在上述代码中,按钮所显示的图标尺寸被设置为宽度 32 像素、高度 32 像素。需要注意的是,如果我们没有主动调用 setIconSize() 接口,那么按钮图标的默认显示尺寸通常由当前界面样式决定,而不是直接采用图片文件原本的大小。
因此,对于 QPushButton 来说,它不仅能够通过 clicked() 信号响应用户的点击操作,还可以通过继承自 QAbstractButton 的 icon 属性,为按钮设置更加直观的图标显示效果。通过文本、图标以及点击交互逻辑的组合,我们便可以构建出更加清晰且便于用户操作的按钮组件。

QPushButton 的快捷键属性
认识了按钮类的图标属性之后,接下来我们再来认识按钮类所提供的快捷键属性。
对于按钮来说,用户最常见的一种交互方式,就是通过鼠标点击按钮,从而触发对应的功能逻辑。例如,用户可以点击"保存"按钮完成文件保存,点击"登录"按钮完成登录验证。不过,在实际的图形化界面程序中,除了使用鼠标进行操作之外,程序通常还会提供快捷键,使用户能够通过键盘更加高效地完成某些操作。
例如,在编辑文本时,我们通常可以通过 Ctrl+C 和 Ctrl+V 快速完成复制和粘贴操作,而不需要每次都通过鼠标右键菜单选择对应功能。因此,快捷键的作用,本质上就是为用户提供一种更加高效的交互方式。
对于 QPushButton 来说,我们同样可以为按钮设置快捷键。需要注意的是,快捷键属性并不是由 QPushButton 类单独提供的,而是由其父类 QAbstractButton 统一提供的公共属性。因此,QPushButton 可以直接调用 setShortcut() 接口,为当前按钮绑定一个快捷键。
例如,下面的代码为按钮绑定了 Ctrl+S 快捷键:
cpp
ui->pushButton_save->setShortcut(
QKeySequence(Qt::CTRL | Qt::Key_S)
);
其中,QKeySequence 类用于描述一个能够作为快捷键使用的按键序列。对于当前示例来说,Qt::CTRL 表示 Ctrl 修饰键,Qt::Key_S 表示键盘上的 S 键,二者通过按位或运算符 | 组合起来,便表示 Ctrl+S 这一快捷键组合。
在 Qt 中,我们也可以通过字符串的方式描述快捷键,例如:
cpp
ui->pushButton_save->setShortcut(QKeySequence("Ctrl+S"));
其中,对于字母按键来说,字符串中的大小写并不会影响快捷键含义,因此 "Ctrl+S" 与 "Ctrl+s" 表示的是同一个快捷键。不过,相比字符串形式,使用 Qt 提供的枚举值进行描述,可以在代码书写错误时更早地暴露问题。例如,如果将 Qt::Key_S 错写为不存在的枚举值,程序会在编译阶段直接报错。
当用户输入与按钮绑定的快捷键时,按钮会产生类似于被点击的效果,并最终发出 clicked() 信号。因此,我们仍然可以通过信号与槽机制,为按钮定义具体需要执行的交互逻辑:
cpp
connect(ui->pushButton_save, &QPushButton::clicked, this, []() {
// 执行保存操作
});
键盘输入如何进入 Qt 程序
一提到快捷键,就需要联系到此前认识过的焦点概念。对于一个图形化界面程序来说,屏幕上可能会同时存在多个窗口,例如浏览器窗口、文本编辑器窗口以及当前运行的 Qt 程序窗口。
对于鼠标点击来说,鼠标事件中会包含鼠标所在位置的信息,因此窗口系统可以根据鼠标坐标判断用户当前点击的是哪一个窗口。但是,键盘输入本身并不包含用于定位目标窗口的坐标信息。例如,用户按下字母 A 或组合键 Ctrl+S 时,仅凭这些按键信息,并不能判断该输入应当交给浏览器窗口,还是交给 Qt 程序窗口。
因此,键盘输入需要依赖当前的激活窗口来确定接收目标。
所谓激活窗口,就是当前正在与用户进行键盘交互的顶层窗口。通常情况下,用户可以通过鼠标点击某个窗口,使该窗口成为当前激活窗口。随后产生的键盘输入,便会首先进入该窗口所属的 GUI 程序。
例如,当桌面上同时存在浏览器窗口和 Qt 程序窗口时,如果用户点击了 Qt 程序窗口,那么该窗口便会成为当前激活窗口。此时,用户后续产生的键盘输入就会进入 Qt 程序,而不是进入浏览器窗口。
从整体上看,用户产生键盘输入之后,可以将其处理过程抽象理解为:
text
用户按下键盘按键
↓
键盘硬件产生输入信号
↓
操作系统获取并整理该键盘输入
↓
窗口系统根据当前激活窗口,
确定该键盘输入应当交给哪个 GUI 窗口
↓
如果当前激活窗口属于 Qt 程序,
键盘输入便进入 Qt 程序
↓
Qt 将底层输入转换为自身能够处理的事件
↓
Qt 根据输入类型进行后续分发
需要注意的是,这里描述的是一条抽象化的输入处理链路。对于我们当前学习按钮快捷键来说,重点并不是深入分析不同操作系统底层如何实现键盘输入处理,而是理解:键盘输入首先进入当前激活窗口,之后才由 Qt 在窗口内部决定如何处理这次输入。
激活窗口与焦点组件
键盘输入进入 Qt 程序之后,还需要区分两个概念:激活窗口 和焦点组件。
激活窗口描述的是窗口级别的输入目标,它决定键盘输入首先进入哪一个 GUI 窗口;而焦点组件描述的是窗口内部的输入目标,它决定普通键盘输入进入窗口之后,由哪一个具体组件进行处理。
例如,假设当前 Qt 程序窗口中存在一个输入框和一个按钮,并且用户已经点击了输入框:
text
Qt 程序窗口:当前激活窗口
QLineEdit: 当前获得键盘焦点
QPushButton:窗口中的普通按钮
此时,如果用户输入普通字符,例如字母 A,则这次键盘输入会先进入当前激活的 Qt 窗口,然后再由 Qt 分发给当前获得焦点的输入框,最终由输入框处理并显示该字符。
text
用户输入字符 A
↓
键盘输入进入当前激活的 Qt 窗口
↓
Qt 将普通键盘事件分发给当前获得焦点的 QLineEdit
↓
QLineEdit 处理该键盘事件
↓
输入框中显示字符 A
因此,可以将二者的区别总结为:
text
激活窗口:
决定键盘输入首先进入哪一个 GUI 窗口
焦点组件:
决定普通键盘输入进入窗口之后,
由窗口内部的哪一个组件进行处理
按钮快捷键的注册与触发
普通键盘输入通常会交给当前获得焦点的组件处理,但是按钮快捷键的触发过程并不完全相同。
当我们调用:
cpp
ui->pushButton_save->setShortcut(QKeySequence("Ctrl+S"));
本质上就是将 Ctrl+S 这一按键组合与保存按钮的快捷操作关联起来。Qt 内部会记录已经注册的快捷键,以及该快捷键所关联的对象。
这一过程可以抽象理解为:
text
已注册快捷键 关联对象
Ctrl+S 保存按钮
假设当前窗口中存在一个输入框和一个保存按钮,并且当前焦点位于输入框中:
text
Qt 程序窗口: 当前激活窗口
QLineEdit: 当前获得键盘焦点
QPushButton: 绑定了 Ctrl+S 快捷键
此时,用户输入普通字符,仍然会由输入框处理。但是,当用户按下 Ctrl+S 时,即使保存按钮本身没有获得键盘焦点,只要该快捷键在当前窗口中处于可用状态,Qt 依然可以识别该快捷键,并触发保存按钮对应的操作。
text
用户按下 Ctrl+S
↓
键盘输入进入当前激活的 Qt 窗口
↓
Qt 检查当前按键组合是否与已经注册的快捷键匹配
↓
发现 Ctrl+S 与保存按钮绑定的快捷键匹配
↓
Qt 触发保存按钮对应的快捷操作
↓
按钮产生类似被点击的效果
↓
按钮发出 clicked() 信号
↓
执行与 clicked() 信号连接的槽函数
由此可见,按钮快捷键并不要求按钮本身提前获得键盘焦点。按钮能够被快捷键触发,是因为 Qt 的快捷键机制建立了按键组合与按钮操作之间的关联。
这也说明,按钮快捷键并不是一套独立于按钮点击之外的业务逻辑,而是另一种触发按钮点击行为的方式。用户通过鼠标点击按钮和通过键盘输入快捷键,最终都可以复用与 clicked() 信号连接的槽函数逻辑。
快捷键冲突与 ShortcutOverride 事件
在一般情况下,当 Qt 发现用户输入的按键组合与某个按钮已经绑定的快捷键匹配时,便可以触发该按钮对应的操作。例如,我们为保存按钮绑定 Ctrl+S 快捷键:
cpp
ui->pushButton_save->setShortcut(QKeySequence("Ctrl+S"));
当用户按下 Ctrl+S 时,Qt 会触发保存按钮的快捷操作,使按钮产生类似被点击的效果,并最终发出 clicked() 信号,从而执行与该信号连接的槽函数逻辑。
但是,在某些特殊场景中,当前获得键盘焦点的组件可能也希望处理相同的按键组合。例如,界面中存在一个"保存整个文件"的按钮,该按钮绑定了 Ctrl+S 快捷键;与此同时,当前获得键盘焦点的是一个自定义编辑组件,而该组件希望将 Ctrl+S 用于"保存当前编辑区域中的临时内容"。
此时,同一个按键组合便对应了两种可能的处理逻辑:
text
保存按钮:
Ctrl+S 用于保存整个文件
当前焦点组件:
Ctrl+S 用于保存当前编辑区域中的临时内容
为了处理这种快捷键冲突,当 Qt 发现用户输入的按键组合可能触发已经注册的快捷键时,并不会立即触发与该快捷键绑定的按钮操作。在真正触发按钮之前,Qt 会先向当前获得键盘焦点的组件发送一个 ShortcutOverride 事件,用于询问该组件是否希望自行处理这组按键。
这一过程可以理解为:
text
用户按下 Ctrl+S
↓
键盘输入进入当前激活的 Qt 窗口
↓
Qt 发现 Ctrl+S 与保存按钮绑定的快捷键匹配
↓
在真正触发保存按钮之前,
Qt 先向当前焦点组件发送 ShortcutOverride 事件
↓
当前焦点组件是否希望自行处理 Ctrl+S?
如果当前焦点组件没有接受 ShortcutOverride 事件,说明它不需要优先处理这组按键,Qt 便会继续触发保存按钮的快捷操作。
text
当前焦点组件没有接受 ShortcutOverride
↓
Qt 向保存按钮分发 QShortcutEvent
↓
事件进入 QAbstractButton::event()
↓
QAbstractButton 识别到这是快捷键触发事件
↓
调用 animateClick()
↓
按钮产生类似被点击的效果
↓
按钮发出 clicked() 信号
↓
执行保存逻辑
需要注意的是,在这条路径中,保存按钮接收到的并不是普通的键盘输入事件,而是快捷键触发事件 QShortcutEvent。该事件会进入按钮继承自 QAbstractButton 的 event() 函数,随后由按钮执行快捷键对应的点击过程。
而如果当前焦点组件接受了 ShortcutOverride 事件,则说明它希望自行处理本次 Ctrl+S 输入。此时,Qt 便不会继续触发保存按钮所绑定的快捷操作,而是将这次输入作为普通键盘事件交给当前焦点组件处理。
text
当前焦点组件接受 ShortcutOverride
↓
Qt 不再触发保存按钮的快捷操作
↓
Ctrl+S 作为普通键盘事件交给当前焦点组件
↓
事件进入当前组件的 event()
↓
进一步调用 keyPressEvent()
↓
执行当前组件自身的 Ctrl+S 处理逻辑
例如,我们可以自定义一个输入框,使其在获得键盘焦点时优先处理 Ctrl+S:
cpp
class InterceptLineEdit : public QLineEdit
{
Q_OBJECT
public:
explicit InterceptLineEdit(QWidget *parent = nullptr)
: QLineEdit(parent)
{
}
signals:
void localSaveTriggered();
protected:
bool event(QEvent *e) override
{
if (e->type() == QEvent::ShortcutOverride)
{
auto *keyEvent = static_cast<QKeyEvent *>(e);
if (keyEvent->key() == Qt::Key_S &&
keyEvent->modifiers() == Qt::ControlModifier)
{
// 当前输入框希望自行处理 Ctrl+S,
// 不再继续触发保存按钮绑定的快捷操作。
e->accept();
return true;
}
}
// 其他事件仍然交给 QLineEdit 原本的处理逻辑。
return QLineEdit::event(e);
}
void keyPressEvent(QKeyEvent *e) override
{
if (e->key() == Qt::Key_S &&
e->modifiers() == Qt::ControlModifier)
{
// 执行当前输入框自身的局部保存逻辑。
emit localSaveTriggered();
return;
}
// 其他普通按键仍然按照输入框原本的方式处理。
QLineEdit::keyPressEvent(e);
}
};
在上述代码中,自定义输入框分别重写了 event() 函数和 keyPressEvent() 函数。虽然这两个函数都与用户输入的 Ctrl+S 有关,但是它们处理的并不是同一个阶段的事件。
在 event() 函数中,当前输入框处理的是 ShortcutOverride 事件。该阶段的作用并不是立即执行保存逻辑,而是决定当前输入框是否要优先处理 Ctrl+S。当输入框判断出当前按键组合为 Ctrl+S 后,调用 e->accept(),便表示当前输入框希望自行处理这次输入,不再触发保存按钮绑定的快捷操作。
cpp
e->accept();
return true;
这里,e->accept() 表示当前组件接受了这次快捷键覆盖请求;而 return true 表示当前这个 ShortcutOverride 事件已经由自定义输入框处理完成。对于其他没有进行特殊处理的事件,则继续通过:
cpp
return QLineEdit::event(e);
交给父类原本的事件处理逻辑,从而保证输入框原有的普通输入、绘制以及焦点处理等功能不会受到影响。
当输入框接受了 ShortcutOverride 事件之后,Qt 会将这次 Ctrl+S 继续作为普通键盘输入交给当前输入框。此时,该事件便会进入输入框的 keyPressEvent() 函数,而我们可以在该函数中真正执行输入框自身对于 Ctrl+S 的处理逻辑:
cpp
void keyPressEvent(QKeyEvent *e) override
{
if (e->key() == Qt::Key_S &&
e->modifiers() == Qt::ControlModifier)
{
emit localSaveTriggered();
return;
}
QLineEdit::keyPressEvent(e);
}
因此,event() 和 keyPressEvent() 在这里分别承担了不同的职责:
text
event():
处理 ShortcutOverride 事件,
决定当前输入框是否要优先处理 Ctrl+S
keyPressEvent():
当输入框获得这次按键的处理权之后,
真正执行自身对于 Ctrl+S 的处理逻辑
将整个过程总结起来,就是:
text
┌─ 当前焦点组件接受 ShortcutOverride
用户按下 Ctrl+S │ ↓
↓ │ Qt 不再触发保存按钮
Qt 发现快捷键可能匹配 │ ↓
↓ │ 普通 QKeyEvent
ShortcutOverride ──────┤ ↓
│ 焦点组件的 keyPressEvent()
│
└─ 当前焦点组件不接受 ShortcutOverride
↓
QShortcutEvent
↓
绑定快捷键的按钮
↓
QAbstractButton::event()
↓
animateClick()
↓
clicked()
所以,ShortcutOverride 本身并不是用于真正执行业务逻辑的事件,而是用于决定当前按键组合最终应当由谁处理的中间判定事件。如果当前焦点组件接受了该事件,这次输入便会继续作为普通键盘事件由焦点组件处理;如果当前焦点组件没有接受该事件,Qt 便会按照快捷键触发流程,向与该快捷键关联的按钮分发 QShortcutEvent,最终触发按钮的 clicked() 信号。
QRadioButton 单选按钮类
在认识了 QPushButton 类之后,接下来我们再来认识另一种常见的按钮组件------QRadioButton,即单选按钮类。
QRadioButton 通常由一个圆形选择指示器以及对应的文本内容构成。与 QPushButton 主要用于触发某项操作不同,QRadioButton 的核心功能主要围绕按钮是否处于选中状态展开。当某个单选按钮被选中时,其圆形选择指示器会呈现对应的选中效果,例如内部出现一个实心圆点,从而直观地向用户表示当前选择的选项。
需要注意的是,用于描述按钮是否处于选中状态的 checked 属性,并不是由 QRadioButton 类单独提供的,而是封装在其父类 QAbstractButton 中。因此,QPushButton 实际上也继承了该属性以及对应的操作接口。
不过,普通的 QPushButton 默认只是一个命令按钮,用户点击之后通常只会触发相应的功能逻辑,并不会保持选中状态。只有调用 setCheckable(true) 接口,将其设置为可选中按钮之后,用户点击该按钮时,Qt 才会自动切换其 checked 状态。
cpp
ui->pushButton->setCheckable(true);
此时,用户第一次点击按钮,按钮的 checked 状态会变为 true;再次点击按钮,其 checked 状态又会变为 false。
text
第一次点击:checked = true
第二次点击:checked = false
第三次点击:checked = true
而对于 QRadioButton 来说,选中状态本身就是其最核心的功能,因此它本身就是可选中按钮。当用户点击某个单选按钮时,Qt 会自动将该按钮的 checked 状态设置为 true,并通过外观变化将当前选中状态展示出来。
此外,QRadioButton 默认还具有自动排他性。在默认情况下,属于同一个父组件的多个单选按钮会构成一个排他选择范围,同一时间只能有一个按钮处于选中状态。当用户选择其中一个按钮之后,原本被选中的其他按钮会自动取消选中。
例如,在用户注册界面中,可以定义"男""女""其他"三个单选按钮,并将它们都设置为当前窗口对象的子组件:
text
Widget
├── QRadioButton:男
├── QRadioButton:女
└── QRadioButton:其他
由于这三个单选按钮具有相同的父组件,因此在默认情况下,它们之间具有排他关系:
text
选择"男":
男被选中,女和其他未选中
选择"女":
女被选中,男自动取消选中
选择"其他":
其他被选中,男和女自动取消选中
因此,QRadioButton 非常适合用于描述"多个选项中只能选择一个"的交互场景,例如性别选择、支付方式选择等。
下面通过一个简单示例,使用三个单选按钮完成性别选择,并通过一个标签显示用户当前选择的结果:
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 将"男"设置为默认选中项
ui->radioButton_male->setChecked(true);
// 同步显示初始默认选项
ui->label->setText("当前你的选择是:" + ui->radioButton_male->text());
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_radioButton_male_clicked()
{
ui->label->setText("当前你的选择是:" + ui->radioButton_male->text());
}
void Widget::on_radioButton_female_clicked()
{
ui->label->setText("当前你的选择是:" + ui->radioButton_female->text());
}
void Widget::on_radioButton_other_clicked()
{
ui->label->setText("当前你的选择是:" + ui->radioButton_other->text());
}

在上述代码中,我们首先在构造函数中调用 setChecked(true) 接口,将"男"这一单选按钮设置为默认选中项。setChecked() 接口用于由程序主动修改按钮当前的选中状态,因此,当程序界面刚刚显示出来时,"男"按钮便已经处于选中状态。
需要注意的是,调用 setChecked(true) 只是主动修改了按钮的选中状态,并不等同于用户点击了该按钮。因此,在当前通过 clicked() 信号修改标签内容的代码中,程序初始化时还需要主动设置一次标签文本,使其能够正确显示默认选项。
之后,当用户点击"女"或"其他"等单选按钮时,Qt 会自动修改对应按钮的 checked 状态,并按照默认排他规则,将原本被选中的按钮修改为未选中状态。随后,对应的 clicked() 信号被触发,槽函数获取当前按钮显示的文本内容,并将用户的选择结果更新到标签中。
这里使用 clicked() 信号,是因为当前示例关注的是:用户点击了哪个选项之后,程序应当更新界面中的提示信息。
除了 setChecked() 之外,QAbstractButton 还提供了 setCheckable() 接口。该接口用于设置按钮是否具备可选中能力。对于一个可选中的按钮来说,当用户点击该按钮时,Qt 会自动修改其 checked 状态;而当按钮不具备可选中能力时,用户点击它便不会再使其进入选中状态。
例如,对于普通的 QPushButton,可以通过下面的代码使其具备选中状态切换能力:
cpp
ui->pushButton->setCheckable(true);
而如果之后不再希望该按钮保持选中状态,则可以调用:
cpp
ui->pushButton->setCheckable(false);
当 setCheckable(false) 被调用之后,该按钮将不再具备可选中能力,其 checked 状态也会被重置为 false。此后,用户再次点击该按钮时,Qt 也不会再自动切换其选中状态。
可以将这几个接口的职责总结为:
text
setCheckable(bool):
设置按钮是否具备可选中能力
setChecked(bool):
程序主动设置按钮当前是否处于选中状态
checked:
描述按钮当前是否已经被选中
不过,对于 QRadioButton 来说,它本身就是用于描述选项选择状态的按钮,因此通常不需要调用 setCheckable(false) 关闭其可选中能力。否则,该单选按钮便失去了最核心的使用意义。
如果某个单选选项只是暂时不允许用户选择,更合适的做法是调用继承自 QWidget 的 setEnabled() 接口,将该按钮设置为不可用状态:
cpp
ui->radioButton_other->setEnabled(false);
当按钮被禁用之后,其通常会呈现为不可操作的外观,并且用户无法再通过正常点击操作选择该选项。
因此,QRadioButton 的核心特点可以总结为:它是一种围绕选中状态展开的按钮组件,通常用于表示多个选项中的单一选择结果;它本身具备可选中能力,用户点击后 Qt 会自动更新其 checked 状态;在默认情况下,同一父组件下的多个单选按钮还具有排他关系,同一时间只能有一个按钮处于选中状态。
toggled() 信号
除了使用 clicked() 信号响应用户的点击操作之外,QRadioButton 还可以使用继承自 QAbstractButton 的 toggled(bool checked) 信号,对按钮选中状态的变化进行处理。
需要注意的是,toggled() 信号关注的并不是用户是否点击了按钮,而是按钮的 checked 属性是否发生了变化。当按钮从未选中状态变为选中状态时,会发出 toggled(true) 信号;当按钮从选中状态变为未选中状态时,则会发出 toggled(false) 信号。如果按钮的选中状态没有发生变化,那么该信号也不会被触发。
text
false -> true:发出 toggled(true)
true -> false:发出 toggled(false)
状态未变化: 不发出 toggled() 信号
对于 QRadioButton 来说,由于同一排他范围内的多个单选按钮同一时间只能有一个处于选中状态,因此当用户切换选项时,通常会同时引起两个按钮的状态变化。
例如,假设初始状态下"男"按钮已经被选中:
text
初始状态:
男:checked = true
女:checked = false
当用户点击"女"按钮之后:
text
状态变化:
男:true -> false,发出 toggled(false)
女:false -> true,发出 toggled(true)
因此,如果我们为多个单选按钮的 toggled() 信号都绑定了槽函数,那么用户切换选项时,原本被取消选中的按钮和新选中的按钮都可能触发对应槽函数。
此时,在槽函数中就不能直接修改标签文本,而应当首先根据 toggled() 信号传递进来的 checked 参数,判断当前按钮是否变为了选中状态。只有当 checked 为 true 时,才说明该按钮是用户当前选择的结果,此时再更新标签内容。
cpp
void Widget::on_radioButton_male_toggled(bool checked)
{
if (checked)
{
ui->label->setText("当前你的选择是:" + ui->radioButton_male->text());
}
}
void Widget::on_radioButton_female_toggled(bool checked)
{
if (checked)
{
ui->label->setText("当前你的选择是:" + ui->radioButton_female->text());
}
}
void Widget::on_radioButton_other_toggled(bool checked)
{
if (checked)
{
ui->label->setText("当前你的选择是:" + ui->radioButton_other->text());
}
}
在上述代码中,当某个单选按钮由未选中状态变为选中状态时,其对应槽函数会接收到 checked == true,并将标签文本修改为当前选项;而当某个按钮因为排他关系被自动取消选中时,其对应槽函数虽然同样会被触发,但是由于此时 checked == false,因此不会修改标签文本。
此外,toggled() 信号不仅会因为用户点击按钮而触发。当程序主动调用 setChecked() 接口修改按钮的选中状态,并且该状态确实发生变化时,同样也会触发 toggled() 信号。
例如:
cpp
ui->radioButton_male->setChecked(true);
如果"男"按钮原本处于未选中状态,那么调用上述代码后,其 checked 属性会从 false 变为 true,从而发出 toggled(true) 信号;如果该按钮原本已经处于选中状态,那么由于状态没有发生变化,因此不会再次发出 toggled() 信号。
因此,clicked() 和 toggled() 的关注点并不相同:
text
clicked():
关注用户是否完成了一次点击操作
toggled(bool checked):
关注按钮的 checked 属性是否发生变化
在当前性别选择示例中,如果我们只希望在用户点击某个选项后更新标签内容,那么使用 clicked() 信号即可完成需求;而如果希望无论选中状态是由用户点击引起,还是由程序调用 setChecked() 主动修改引起,都能够同步更新界面内容,那么使用 toggled(bool checked) 信号会更加合适。
QCheckBox 复选框类
在认识了 QRadioButton 单选按钮类之后,接下来我们再来认识另一种常见的可选中按钮组件------QCheckBox,即复选框类。
QCheckBox 通常由一个方形选择指示器以及对应的文本内容构成。当复选框处于选中状态时,其方形选择指示器会呈现对应的勾选效果,从而向用户表示当前选项已经被选择。
text
[ ] 篮球
[✓] 篮球
与 QRadioButton 类似,QCheckBox 的核心功能同样围绕按钮是否处于选中状态展开。用于描述按钮当前是否被选中的 checked 属性,仍然是由其父类 QAbstractButton 提供的公共属性。由于 QCheckBox 本身就是可选中按钮,因此,当用户点击复选框时,Qt 会自动切换其 checked 状态:如果按钮原本未被选中,则点击后变为选中;如果按钮原本已经被选中,则再次点击后变为未选中。
text
第一次点击:
checked:false -> true
第二次点击:
checked:true -> false
不过,QCheckBox 与 QRadioButton 之间存在一个非常重要的区别:多个复选框默认彼此独立,不具有排他性。
对于 QRadioButton 来说,同一排他范围内的多个单选按钮通常只能选择其中一个。例如,在性别选择场景中,用户不能同时选择"男"和"女"。而对于 QCheckBox 来说,用户选中某一个复选框,并不会导致其他复选框自动取消选中。因此,QCheckBox 更适合表示用户可以同时选择多个结果,或者某个独立功能是否开启的场景。
例如,在一个兴趣爱好选择界面中,可以定义"篮球""音乐""编程"三个复选框:
text
请选择你的兴趣爱好:
[ ] 篮球
[ ] 音乐
[ ] 编程
由于一个用户可以同时具有多个兴趣爱好,因此使用 QCheckBox 非常合适。用户可以同时选择多个选项:
text
[✓] 篮球
[✓] 音乐
[ ] 编程
在这个过程中,当用户点击"篮球"复选框时,Qt 只会切换"篮球"自身的 checked 状态,而不会修改"音乐"和"编程"复选框的状态。
除此之外,QCheckBox 也可以单独使用,用于描述某个独立功能是否开启。例如:
text
[✓] 我已阅读并同意用户协议
[ ] 记住密码
[ ] 开机自动启动
这些选项是否被选中,通常都不会影响其他选项,因此同样适合使用复选框进行描述。
使用复选框完成多项选择
下面通过一个简单示例,使用三个复选框完成兴趣爱好选择,并通过一个标签显示用户当前已经选择的结果。
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
ui->label->setText("当前你的选择是:");
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_checkBox_basketball_toggled(bool)
{
QString result = "当前你的选择是:";
if (ui->checkBox_basketball->isChecked())
result += "篮球 ";
if (ui->checkBox_music->isChecked())
result += "音乐 ";
if (ui->checkBox_programming->isChecked())
result += "编程 ";
ui->label->setText(result);
}
void Widget::on_checkBox_music_toggled(bool)
{
QString result = "当前你的选择是:";
if (ui->checkBox_basketball->isChecked())
result += "篮球 ";
if (ui->checkBox_music->isChecked())
result += "音乐 ";
if (ui->checkBox_programming->isChecked())
result += "编程 ";
ui->label->setText(result);
}
void Widget::on_checkBox_programming_toggled(bool)
{
QString result = "当前你的选择是:";
if (ui->checkBox_basketball->isChecked())
result += "篮球 ";
if (ui->checkBox_music->isChecked())
result += "音乐 ";
if (ui->checkBox_programming->isChecked())
result += "编程 ";
ui->label->setText(result);
}
在上述代码中,我们分别为三个复选框的 toggled(bool checked) 信号绑定了对应的槽函数。该信号的触发条件是按钮的 checked 属性发生变化:当复选框由未选中状态变为选中状态时,会发出 toggled(true) 信号;当复选框由选中状态变为未选中状态时,则会发出 toggled(false) 信号。
text
false -> true:发出 toggled(true)
true -> false:发出 toggled(false)
状态未变化: 不发出 toggled() 信号
在当前示例中,当任意一个复选框的选中状态发生变化时,我们都需要重新统计当前所有已经被选中的兴趣选项。因此,槽函数中并没有只处理当前触发信号的复选框,而是通过 isChecked() 接口,分别查询三个复选框当前是否处于选中状态。
其中,isChecked() 接口用于查询当前按钮是否已经被选中,并返回一个 bool 类型的结果:
text
按钮当前被选中: isChecked() 返回 true
按钮当前未被选中: isChecked() 返回 false
例如,当用户同时选择"篮球"和"编程"时:
text
[✓] 篮球
[ ] 音乐
[✓] 编程
此时,各个复选框调用 isChecked() 得到的结果为:
text
checkBox_basketball->isChecked() -> true
checkBox_music->isChecked() -> false
checkBox_programming->isChecked() -> true
最终,标签中显示的内容为:
text
当前你的选择是:篮球 编程
这里之所以需要查询多个复选框当前的选中状态,是因为 QCheckBox 默认允许用户同时选择多个选项。也就是说,当前选择结果并不一定由某一个单独按钮决定,而可能由多个已经被选中的复选框共同构成。
需要注意的是,在上述槽函数定义中,toggled(bool) 信号虽然会传递当前按钮变化后的选中状态,但是由于我们需要重新统计所有复选框的整体选择结果,因此并没有直接使用该参数。函数参数位置只保留类型 bool,而没有为其命名,表示该槽函数能够接收信号传递的布尔值,但函数内部并不需要直接使用它。
QCheckBox 与 QRadioButton 的区别
至此,我们可以将 QCheckBox 和 QRadioButton 的使用场景进行一个简单对比:
text
QRadioButton:
表示多个选项中只能选择一个
默认具有排他性
例如:性别选择、支付方式、配送方式
QCheckBox:
表示某个选项是否被选择
多个复选框默认互不排斥
例如:兴趣爱好、功能开关、用户协议
例如,性别选择通常适合使用单选按钮:
text
(●) 男
( ) 女
( ) 其他
因为用户在这组选项中通常只能选择一个结果。
而兴趣爱好选择则适合使用复选框:
text
[✓] 篮球
[✓] 音乐
[ ] 编程
因为用户可以同时选择多个结果。
因此,虽然 QRadioButton 和 QCheckBox 都围绕 checked 属性展开,但是二者所表达的交互语义并不相同:单选按钮强调一组结果中的唯一选择,而复选框强调每一个选项都可以独立进行选择或取消选择。
使用 QButtonGroup 管理复选框之间的排他关系
虽然 QCheckBox 默认用于描述彼此独立、可以同时选择的选项,但是从技术角度来说,我们也可以通过 QButtonGroup 将多个复选框组织为一个具有排他关系的按钮组,使同一时间只能有一个复选框处于选中状态。
例如,假设在支付界面中定义了三个复选框:
text
请选择支付方式:
[ ] 微信支付
[ ] 支付宝
[ ] 银行卡
如果不进行额外处理,那么用户可以同时勾选多个支付方式:
text
[✓] 微信支付
[✓] 支付宝
[ ] 银行卡
但是,实际业务中一次支付通常只能选择一种支付方式。因此,如果仍然希望使用 QCheckBox 来实现该界面,就可以使用 QButtonGroup 对这些复选框进行逻辑分组。
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QButtonGroup>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QButtonGroup *paymentGroup = new QButtonGroup(this);
paymentGroup->addButton(ui->checkBox_wechat, 0);
paymentGroup->addButton(ui->checkBox_alipay, 1);
paymentGroup->addButton(ui->checkBox_bankCard, 2);
// 设置按钮组具有排他性
paymentGroup->setExclusive(true);
// 设置默认支付方式
ui->checkBox_wechat->setChecked(true);
ui->label->setText("当前支付方式:微信支付");
connect(paymentGroup, &QButtonGroup::idClicked,
this, [=](int id) {
switch (id)
{
case 0:
ui->label->setText("当前支付方式:微信支付");
break;
case 1:
ui->label->setText("当前支付方式:支付宝");
break;
case 2:
ui->label->setText("当前支付方式:银行卡");
break;
}
});
}
Widget::~Widget()
{
delete ui;
}

在上述代码中,我们首先创建了一个 QButtonGroup 对象,并将三个复选框加入该按钮组中:
cpp
paymentGroup->addButton(ui->checkBox_wechat, 0);
paymentGroup->addButton(ui->checkBox_alipay, 1);
paymentGroup->addButton(ui->checkBox_bankCard, 2);
addButton() 接口的第二个参数表示当前按钮在按钮组中对应的整数编号。因此,在当前示例中,各个复选框与编号之间的关系为:
text
微信支付复选框 -> id = 0
支付宝复选框 -> id = 1
银行卡复选框 -> id = 2
随后,我们调用:
cpp
paymentGroup->setExclusive(true);
将当前按钮组设置为排他按钮组。当按钮组具有排他性之后,用户选中组内某一个复选框时,Qt 会自动取消组内原本被选中的其他复选框,从而保证同一时间只能存在一个选中的支付方式。
需要注意的是,QButtonGroup 的 exclusive 属性默认值就是 true。也就是说,将多个可选中按钮加入同一个 QButtonGroup 之后,即使没有显式调用 setExclusive(true),该按钮组默认也具有排他性。在示例中显式调用该接口,主要是为了让代码能够更加清晰地表达当前按钮组的设计意图。
对于排他按钮组,通常还应当主动设置一个默认选中项:
cpp
ui->checkBox_wechat->setChecked(true);
这样,程序界面刚刚显示出来时,就已经存在一个有效的默认支付方式。
在当前示例中,我们还使用了 QButtonGroup 提供的 idClicked(int id) 信号:
cpp
connect(paymentGroup, &QButtonGroup::idClicked,
this, [=](int id) {
switch (id)
{
case 0:
ui->label->setText("当前支付方式:微信支付");
break;
case 1:
ui->label->setText("当前支付方式:支付宝");
break;
case 2:
ui->label->setText("当前支付方式:银行卡");
break;
}
});
当按钮组中的某个按钮被用户点击时,QButtonGroup 会发出 idClicked(int id) 信号,并将被点击按钮对应的编号传递给槽函数。因此,我们可以根据不同的 id,判断用户当前点击的是哪一种支付方式,并统一更新标签中所显示的内容。
例如:
text
用户点击"支付宝"
↓
paymentGroup 发出 idClicked(1)
↓
槽函数进入 case 1
↓
标签显示:当前支付方式:支付宝
相比于分别为每一个复选框绑定单独的 clicked() 槽函数,QButtonGroup::idClicked() 可以在一个统一的槽函数中处理整组按钮的点击逻辑。当一组按钮所完成的业务功能比较相似时,这种处理方式会更加方便。
程序运行后的选择效果如下:
text
初始状态:
[✓] 微信支付
[ ] 支付宝
[ ] 银行卡
用户点击"支付宝"之后:
[ ] 微信支付
[✓] 支付宝
[ ] 银行卡
用户点击"银行卡"之后:
[ ] 微信支付
[ ] 支付宝
[✓] 银行卡
多个 QButtonGroup 之间的关系
一个界面中可以同时创建多个 QButtonGroup。不同按钮组之间互不影响,每一个按钮组只负责管理加入自身的按钮之间的状态关系。
例如,在一个订单提交界面中,用户既需要选择支付方式,也需要选择配送方式:
text
支付方式:
[ ] 微信支付
[ ] 支付宝
[ ] 银行卡
配送方式:
[ ] 快递配送
[ ] 到店自提
此时,可以分别创建两个排他按钮组:
cpp
QButtonGroup *paymentGroup = new QButtonGroup(this);
paymentGroup->addButton(ui->checkBox_wechat);
paymentGroup->addButton(ui->checkBox_alipay);
paymentGroup->addButton(ui->checkBox_bankCard);
paymentGroup->setExclusive(true);
QButtonGroup *deliveryGroup = new QButtonGroup(this);
deliveryGroup->addButton(ui->checkBox_express);
deliveryGroup->addButton(ui->checkBox_selfPickup);
deliveryGroup->setExclusive(true);
此时,排他关系只发生在各自按钮组内部:
text
支付方式组:
[✓] 微信支付
[ ] 支付宝
[ ] 银行卡
配送方式组:
[✓] 快递配送
[ ] 到店自提
用户在支付方式组中选择"支付宝",只会取消支付方式组内部原本被选中的"微信支付",并不会影响配送方式组中已经选中的"快递配送"。
text
用户选择"支付宝"之后:
支付方式组:
[ ] 微信支付
[✓] 支付宝
[ ] 银行卡
配送方式组:
[✓] 快递配送
[ ] 到店自提
因此,可以将多个按钮组之间的关系理解为:
text
paymentGroup:
只管理支付方式选项之间的排他关系
deliveryGroup:
只管理配送方式选项之间的排他关系
不同 QButtonGroup 之间:
彼此独立,互不影响
小结
QCheckBox 是一种围绕选中状态展开的复选框组件。它通常由方形选择指示器和文本内容构成,用户点击复选框之后,Qt 会自动切换其 checked 状态。
与 QRadioButton 不同,多个 QCheckBox 默认彼此独立,不具有排他性。因此,它既可以单独用于表示某个功能是否开启,也可以用于表示用户能够同时选择多个结果的业务场景,例如兴趣爱好、权限配置、记住密码等。
当复选框的选中状态发生变化时,会发出 toggled(bool checked) 信号;而我们可以通过 isChecked() 接口查询某个复选框当前是否处于选中状态,从而统计用户当前已经选择的所有内容。
如果确实需要使多个复选框之间具有排他关系,则可以通过 QButtonGroup 对它们进行逻辑分组。不同的 QButtonGroup 之间彼此独立,每一个按钮组只管理自身内部按钮之间的状态关系。
不过,从界面语义和用户使用习惯来说,如果某个业务场景本身就是"多个选项中只能选择一个",通常直接使用 QRadioButton 会更加合适;而 QCheckBox 更适合描述彼此独立、允许同时选择的多个选项。

结语
那么这就是本篇文章的全部内容,我会持续更新,希望你能够多多关注,如果本文有帮助到你的话,还请三连加关注,你的支持就是我创作的最大动力!
