常用控件速查(上):QPushButton、QLabel、QLineEdit、QComboBox
引言
从本章开始,我们进入 Qt Widgets 最常见的一层:控件。按钮负责发起动作,标签负责展示信息,单行编辑框负责收集输入,下拉框负责在有限选项中做选择。它们看似简单,却正好覆盖了桌面软件中最典型的交互闭环:显示 -> 输入 -> 选择 -> 提交 -> 反馈。
本文面向 Qt 6 Widgets。学习顺序刻意安排为:先运行一个完整示例,再分别认识四个控件的常用 API 和信号,最后沿着一次交互追踪到源码层面的调用。读完后应能:
- 用 C++ 创建并布局四个控件;
- 区分"程序修改"和"用户操作"触发的信号;
- 用成员槽、Lambda、
qOverload三种方式安全连接信号槽; - 用
text()、currentData()等 API 读取控件状态; - 从公开 API 追踪到事件处理、信号发射与槽调用的大致路径;
- 避免把控件指针、业务状态和界面状态混在一起。

图 1:四个控件都直接或间接继承 QWidget,但它们背后协作的对象不同。初学时只需操作公开类;读源码时再理解虚线所示的内部对象。
一、先会用:一个可运行的"用户资料"小窗口
先不把注意力分散到多个小片段。下面用一个小窗口贯穿全文:用户输入昵称、选择城市,点击"保存",状态标签给出反馈;"接收通知"按钮则展示可选中按钮的用法。
1.1 CMake 配置
新建一个 Qt Widgets 项目,CMakeLists.txt 最小内容如下:
cmake
cmake_minimum_required(VERSION 3.21)
project(widget_controls_demo LANGUAGES CXX)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_standard_project_setup()
qt_add_executable(widget_controls_demo
main.cpp
)
target_link_libraries(widget_controls_demo PRIVATE Qt6::Widgets)
set_target_properties(widget_controls_demo PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)
这里仍然使用前几章的 QApplication、QWidget 和布局管理器。四个控件都属于 Qt6::Widgets,不需要分别链接额外模块。
1.2 完整代码
把下面代码保存为 main.cpp 后即可构建运行:
cpp
#include <QApplication>
#include <QComboBox>
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QVBoxLayout>
#include <QWidget>
class ProfileWidget : public QWidget
{
Q_OBJECT
public:
explicit ProfileWidget(QWidget *parent = nullptr)
: QWidget(parent)
{
setWindowTitle(tr("用户资料"));
resize(420, 250);
auto *titleLabel = new QLabel(tr("创建你的资料"), this);
titleLabel->setStyleSheet("font-size: 20px; font-weight: 600;");
nameEdit = new QLineEdit(this);
nameEdit->setPlaceholderText(tr("2 到 12 个中文、字母或数字"));
nameEdit->setClearButtonEnabled(true);
nameEdit->setMaxLength(12);
auto *nameValidator = new QRegularExpressionValidator(
QRegularExpression("[A-Za-z0-9\\u4e00-\\u9fa5]{2,12}"), this);
nameEdit->setValidator(nameValidator);
cityCombo = new QComboBox(this);
cityCombo->addItem(tr("请选择城市"), QString());
cityCombo->addItem(tr("北京"), "beijing");
cityCombo->addItem(tr("上海"), "shanghai");
cityCombo->addItem(tr("深圳"), "shenzhen");
notifyButton = new QPushButton(tr("接收通知"), this);
notifyButton->setCheckable(true);
notifyButton->setToolTip(tr("点击切换通知订阅状态"));
saveButton = new QPushButton(tr("保存"), this);
saveButton->setDefault(true);
saveButton->setEnabled(false);
statusLabel = new QLabel(tr("请填写昵称并选择城市"), this);
statusLabel->setWordWrap(true);
statusLabel->setStyleSheet("color: #526a7d;");
auto *formLayout = new QFormLayout;
formLayout->addRow(tr("昵称:"), nameEdit);
formLayout->addRow(tr("城市:"), cityCombo);
formLayout->addRow(QString(), notifyButton);
auto *rootLayout = new QVBoxLayout(this);
rootLayout->addWidget(titleLabel);
rootLayout->addLayout(formLayout);
rootLayout->addWidget(saveButton);
rootLayout->addWidget(statusLabel);
// 用户每次编辑昵称或切换城市时,重新判断保存按钮是否可用。
connect(nameEdit, &QLineEdit::textChanged,
this, &ProfileWidget::updateSaveButton);
connect(cityCombo, qOverload<int>(&QComboBox::currentIndexChanged),
this, &ProfileWidget::updateSaveButton);
connect(notifyButton, &QPushButton::toggled, this,
[this](bool checked) {
statusLabel->setText(checked
? tr("已开启通知;继续填写资料后保存。")
: tr("通知已关闭;继续填写资料后保存。"));
});
connect(saveButton, &QPushButton::clicked,
this, &ProfileWidget::saveProfile);
}
private slots:
void updateSaveButton()
{
const bool nameIsAcceptable = nameEdit->hasAcceptableInput();
const bool cityWasSelected = !cityCombo->currentData().toString().isEmpty();
saveButton->setEnabled(nameIsAcceptable && cityWasSelected);
}
void saveProfile()
{
const QString name = nameEdit->text();
const QString cityCode = cityCombo->currentData().toString();
const bool wantsNotifications = notifyButton->isChecked();
statusLabel->setText(
tr("已保存:%1,城市代码:%2,通知:%3")
.arg(name, cityCode,
wantsNotifications ? tr("开启") : tr("关闭")));
}
private:
QLineEdit *nameEdit = nullptr;
QComboBox *cityCombo = nullptr;
QPushButton *notifyButton = nullptr;
QPushButton *saveButton = nullptr;
QLabel *statusLabel = nullptr;
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
ProfileWidget window;
window.show();
return app.exec();
}
这个例子里控件都是 ProfileWidget 的子对象。窗口析构时,QObject 的对象树会依次析构它们,因此这里不需要手工 delete。QFormLayout 被放进 rootLayout,而 rootLayout 安装在窗口上,也会由布局体系接管。
如果你把类声明和实现拆到
profile_widget.h/.cpp,并让 CMake 的 AUTOMOC 自动运行,就不需要在.cpp末尾写#include "main.moc"。这个包含仅适合本章为了便于复制而写在单文件中的Q_OBJECT类。
二、四个控件各自负责什么
| 控件 | 主要职责 | 最常见的读取 API | 典型用户信号 |
|---|---|---|---|
QPushButton |
发起一个命令,或切换一个二元状态 | isChecked() |
clicked(bool)、toggled(bool) |
QLabel |
显示只读文本、图片、富文本 | text() |
通常不承担业务交互 |
QLineEdit |
输入与编辑一行文本 | text() |
textEdited(const QString &)、returnPressed() |
QComboBox |
从候选项中选择一个值 | currentData()、currentText() |
activated(int)、currentIndexChanged(int) |
不要把这张表理解为"每个控件只用一个函数"。它只是帮助你先建立职责边界:按钮表示动作,标签表示结果,编辑框表示自由输入,下拉框表示有限选择。
三、QPushButton:把用户动作变成命令
QPushButton 继承自 QAbstractButton。因此 clicked()、pressed()、released()、toggled() 等大多数行为并不是 QPushButton 独有的,QToolButton、QCheckBox 等按钮类也共享这套机制。
3.1 最小使用
cpp
auto *deleteButton = new QPushButton(tr("删除"), this);
deleteButton->setEnabled(false); // 当前不可点击
deleteButton->setToolTip(tr("删除当前记录"));
auto *saveButton = new QPushButton(tr("保存"), this);
saveButton->setIcon(QIcon(":/icons/save.svg"));
connect(deleteButton, &QPushButton::clicked, this, &ProfileWidget::deleteCurrentRecord);
按钮的显示文字由构造函数或 setText() 指定;
setEnabled(false) 会同时让按钮呈现禁用外观并拒绝用户输入。不要只在槽函数里判断"能不能执行",而把本就不允许的动作禁用掉,用户体验更清晰。
QPushButton 不只是文字按钮,也可以同时显示图标和文字。在桌面应用中,常见做法是用SetIcon()图标强化动作含义,同时保留文字保证可理解性。
3.2 clicked、pressed、released、toggled 怎么选
| 信号 | 何时发出 | 适合做什么 |
|---|---|---|
pressed() |
按钮刚被按下 | 需要立即视觉或临时反馈的场景 |
released() |
鼠标或键盘操作释放 | 少见;通常优先用 clicked() |
clicked(bool checked) |
完成一次点击;也可由 click() 触发 |
提交、打开窗口、执行命令 |
toggled(bool checked) |
可选中状态发生改变 | 订阅开关、显示/隐藏面板 |
普通命令按钮使用 clicked();有持续状态的按钮先 setCheckable(true),再监听 toggled(bool)。clicked(bool) 的参数是当前选中状态;对不可选中按钮,它始终是 false。
cpp
auto *previewButton = new QPushButton(tr("预览"), this);
previewButton->setCheckable(true);
connect(previewButton, &QPushButton::toggled, this,
[this](bool enabled) {
previewPanel->setVisible(enabled);
});
3.3 默认按钮不是"自动保存"
setDefault(true) 用于设置 QDialog 中的默认按钮。
当对话框存在默认按钮时,用户按 Enter/Return 可以激活该按钮。它不意味着窗口一打开就会调用槽函数,也不意味着每次输入都会自动保存。 需要注意:
- default 按钮机制主要针对 QDialog;
- 普通 QWidget 不应把 setDefault() 当作通用的 Enter 快捷键机制;
- 如果只是希望响应 Enter,应根据场景使用 QLineEdit::returnPressed()、QShortcut 等机制。
四、QLabel:只读信息的展示出口
QLabel 是展示控件。它适合标题、字段名、说明、状态、图标与缩略图;它不是输入控件,也不适合承担复杂富文本编辑。
4.1 文本、换行、富文本边界与控制内容位置
cpp
auto *hintLabel = new QLabel(tr("密码至少包含 8 个字符。"), this);
hintLabel->setWordWrap(true);
hintLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
statusLabel->setText(tr("正在连接服务器..."));
当文本会随着窗口宽度变化时,使用 setWordWrap(true)。需要让用户复制错误信息时,使用 Qt::TextSelectableByMouse。QLabel 能识别一部分 HTML 富文本,但业务输入不应直接拼进 HTML;至少要用 toHtmlEscaped() 转义,避免文本被当作标签解释。
cpp
const QString userName = nameEdit->text();
label->setText(tr("欢迎,<b>%1</b>").arg(userName.toHtmlEscaped()));
常用的还有设置文本内容位置setAlignment()
cpp
auto *label = new QLabel(tr("处理中..."), this);
label->setAlignment(Qt::AlignCenter);
Qt::AlignLeft //左对齐
Qt::AlignRight //右对齐
Qt::AlignHCenter //横向居中对齐
Qt::AlignTop //顶部对齐
Qt::AlignVCenter //垂直居中对齐
Qt::AlignBottom //底部对齐
QLabel 的"控件大小"和"文字绘制位置"是两个概念。setAlignment() 控制的是内容在 QLabel 内部如何对齐。
4.2 图片与高 DPI
显示图片时使用 QPixmap:
cpp
auto *avatarLabel = new QLabel(this);
QPixmap avatar(":/images/avatar.png");
avatarLabel->setPixmap(avatar);
avatarLabel->setScaledContents(false);
不要一上来就 setScaledContents(true)。它会强制把图片拉伸到标签大小,容易造成比例失真。保持原比例缩放时,应根据目标大小先调用 pixmap.scaled(..., Qt::KeepAspectRatio, Qt::SmoothTransformation);图标、头像等资源还应准备高分辨率版本,让 Qt 在高 DPI 屏幕上选择合适的像素密度。
4.3 setBuddy():标签和输入框的键盘关联
cpp
auto *nameLabel = new QLabel(tr("&昵称:"), this);
nameLabel->setBuddy(nameEdit);
& 标出助记键。用户按 Alt+N 时,焦点会进入 nameEdit。这是桌面端表单的细节优势:不要只为鼠标用户设计。
五、QLineEdit:单行输入、校验与提交时机
QLineEdit 只处理一行文本;地址、备注、日志等需要多行时应使用 QTextEdit 或 QPlainTextEdit。它的关键不是"拿到字符串",而是区分输入是否有效、变化来自程序还是用户,以及何时提交。
5.1 常用属性
cpp
passwordEdit->setPlaceholderText(tr("请输入密码"));
passwordEdit->setEchoMode(QLineEdit::Password);
passwordEdit->setClearButtonEnabled(true);
passwordEdit->setMaxLength(64);
placeholderText 是提示,不是默认值,调用 text() 时不会得到它。密码框只影响屏幕显示,不会加密内存中的字符串,也不会替你安全传输密码。
5.2 三个高频信号:不要混用
| 信号 | 程序调用 setText() |
用户编辑 | 常见用途 |
|---|---|---|---|
textChanged() |
会触发 | 会触发 | 实时同步、实时校验 |
textEdited() |
不会触发 | 会触发 | 只关心用户输入 |
editingFinished() |
不因 setText() 直接触发 |
焦点离开或 Enter 时触发,但受输入状态限制 | 提交、失焦校验 |
如果设置了 QValidator 或 inputMask,用户按 Enter 时,只有输入达到 Acceptable 状态才会发出 returnPressed() 和 editingFinished();另外,单纯失去焦点且内容没有变化时,不会重复发出 editingFinished()。
本章示例用 textChanged() 启用保存按钮,因为昵称无论来自用户输入还是从已有资料回填,都应重新计算按钮状态。搜索建议、撤销栈统计这类只应由人操作触发的逻辑,则更适合 textEdited()。
cpp
connect(searchEdit, &QLineEdit::textEdited, this, &SearchWidget::requestSuggestions);
connect(nameEdit, &QLineEdit::returnPressed, this, &ProfileWidget::saveProfile);
returnPressed() 不等于任何时候按下 Enter 都会发出。设置校验器或输入掩码后,输入必须是可接受的,信号才会发出。
5.3 QValidator:让"是否有效"成为控件状态
本章示例把正则校验器安装到 nameEdit 上:
cpp
auto *validator = new QRegularExpressionValidator(
QRegularExpression("[A-Za-z0-9\\u4e00-\\u9fa5]{2,12}"), this);
nameEdit->setValidator(validator);
const bool ok = nameEdit->hasAcceptableInput();
验证器会对每次编辑返回三种状态:Invalid、Intermediate、Acceptable。Intermediate 很重要,例如用户刚输入第一个字符时,虽然尚未达到两字符要求,但仍应允许他继续输入;因此不要简单地把"尚未完整"理解为"非法"。提交前用 hasAcceptableInput() 统一判断即可。
校验器改善交互,但不能替代业务校验。用户名是否被占用、手机号是否已注册、权限是否允许,都必须在业务层或服务端再次验证。
5.4 可编辑与只读
cpp
lineEdit->setReadOnly(true);
readOnly 和 enabled 是两个不同概念。
setReadOnly(true):用户不能修改,但仍可以获得焦点、选择和复制文本;setEnabled(false):控件整体进入禁用状态,通常不能接受正常交互。
5.4 程序回填时如何避免触发联动?
cpp
QSignalBlocker blocker(nameEdit);
nameEdit->setText(profile.name);
QSignalBlocker 适合处理一次性的程序回填场景;它不是解决信号循环依赖的万能手段。
六、QComboBox:显示文本与业务值分开保存
QComboBox 将"用户看见的条目"与"程序需要的值"分开保存,这是它比一组硬编码 if 更可靠的原因。
6.1 添加条目与读取数据
cpp
cityCombo->addItem(tr("北京"), "beijing");
cityCombo->addItem(tr("上海"), "shanghai");
const QString displayName = cityCombo->currentText();
const QString cityCode = cityCombo->currentData().toString();
第一个参数是显示文本,第二个参数保存在 Qt::UserRole 位置的 QVariant。界面未来从"北京"改成"北京市"时,业务代码仍然拿到稳定的 beijing,不需要跟着改字符串判断。
如果业务数据本身就是枚举,可直接保存枚举值,但跨边界传递前要考虑 QVariant 是否注册了该类型。入门项目中,字符串代码通常已经足够清晰。
6.2 信号的两个语义层次
| 信号 | 程序调用 setCurrentIndex() 是否发出 |
用户选择是否发出 | 推荐用途 |
|---|---|---|---|
currentIndexChanged(int) |
是 | 是 | 所有状态同步,例如刷新依赖字段 |
currentTextChanged(const QString &) |
是 | 是 | 只关心可见文本时 |
activated(int) |
否 | 是,即使重复点当前项也会发出 | 用户明确确认了一次选择 |
highlighted(int) |
否 | 用户在弹出列表中移动高亮项 | 预览,不宜做提交 |
可以把这三个信号简单理解成:
currentIndexChanged():状态变了currentTextChanged():显示文本变了activated():用户主动选了一次
QComboBox 有不少重载信号。Qt 6 中推荐用 qOverload 消除歧义:
cpp
connect(cityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, &ProfileWidget::updateSaveButton);
不要写旧式字符串连接:
cpp
// 不推荐:编译器无法检查拼写和参数签名。
connect(cityCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSaveButton()));
6.3 可编辑下拉框不是普通输入框的替代品
cpp
cityCombo->setEditable(true);
cityCombo->setInsertPolicy(QComboBox::NoInsert);
cityCombo->setCompleter(completer);
设为可编辑后,QComboBox 内部会持有一个 QLineEdit,用户既能输入又能选择。适合"常用值 + 允许自定义值"的场景,例如标签、服务器地址、历史目录;如果只有自由输入需求,直接使用 QLineEdit 更简单。可编辑模式下,明确设置 InsertPolicy,避免用户的临时输入意外进入候选列表。
6.4 查找数据findData()
在实际业务中经常存在反向需求:已经有一个 cityCode = "shanghai",如何让 ComboBox 选中它?
这时候可以这样使用:
cpp
const int index = cityCombo->findData("shanghai");
if (index >= 0) {
cityCombo->setCurrentIndex(index);
}
通过查找该数据项的索引,如果不存在那么说明没有这个数据。否则表示找到,可以继续使用索引执行自己的业务。
6.5 清空Clear()与统计Count()
scss
combo->count();
combo->clear();
combo->removeItem(index);
这三个是QCombobox最常用的接口,分别是:统计当前数量、清空下拉项、删除指定下拉项。
七、信号槽:三种应该掌握的连接写法
信号槽并不是控件专属机制,但控件是最常见的发送者。connect() 的前两个参数描述"谁发出什么",后两个参数描述"谁接收、执行什么"。
7.1 成员函数槽:业务逻辑的默认选择
cpp
connect(saveButton, &QPushButton::clicked, this, &ProfileWidget::saveProfile);
这适合可命名、可测试、可能增长的业务动作。由于函数指针在编译期参与类型检查,信号参数可以比槽函数参数多,但槽函数不能要求更多参数。例如 clicked(bool) 可连接到无参数的 saveProfile()。
7.2 Lambda:短小的界面绑定
cpp
connect(notifyButton, &QPushButton::toggled, this,
[this](bool checked) {
statusLabel->setText(checked ? tr("通知开启") : tr("通知关闭"));
});
这里的第三个参数 this 称为上下文对象 。当 ProfileWidget 析构时,这条连接会自动断开,Lambda 不会继续访问已经销毁的 statusLabel。不要省略上下文对象后再捕获裸指针;那会让生命周期判断变得困难。
7.3 重载信号:用 qOverload 说清楚签名
cpp
connect(cityCombo, qOverload<int>(&QComboBox::activated),
this, [this](int index) {
qDebug() << "用户选择了" << cityCombo->itemData(index);
});
有重载时,&QComboBox::activated 本身不够明确。qOverload<int> 告诉编译器要选 int 版本。对于本章四个控件,QComboBox 是最常遇到这种情况的类。

图 2:connect() 通常在构造函数中建立一次;之后每次用户交互,控件事件处理会更新状态、发射信号,并根据连接关系调用槽函数。
7.4 AutoConnection 的初步理解
不指定连接类型时,connect() 默认使用 Qt::AutoConnection:如果发送者发射信号的线程与接收者所在的线程相同,槽通常直接调用;如果不同,Qt 会把调用投递到接收者线程的事件队列。本章所有控件和窗口都在 GUI 线程,所以不需要显式指定连接类型。
后续的多线程章节会深入队列连接。现在只记住一条边界:只能在 GUI 线程创建和操作 QWidget。 即使槽函数通过跨线程信号到达,也必须让真正的界面更新在 GUI 线程执行。
八、从 API 到源码:控件是怎样把操作送进槽函数的
这一节不要求你立刻下载 Qt 源码逐行阅读。目标是建立一张稳定的调用地图:公开 API 改了什么状态,用户事件从哪里进入,信号大致在哪一层发出。
不同 Qt 6 小版本的私有类名和行号可能变化,下面使用的是 Qt 6 系列的简化调用链;公开语义保持稳定。
8.1 connect() 做的不是"保存一个 C++ 回调指针"
当你写:
cpp
connect(saveButton, &QPushButton::clicked, this, &ProfileWidget::saveProfile);
新式函数指针连接利用 C++ 类型系统进行编译期检查;
对于 Qt 的元对象信号槽机制,连接关系会被 Qt 的元对象系统记录和管理。真正发射信号时,MOC 生成的信号函数会进入 QMetaObject::activate();它遍历匹配的连接,并按连接类型直接调用或投递事件。
可以先把它理解成:
text
connect(...)
-> 记录连接关系(sender + signal -> receiver + slot)
emit clicked(...)
-> MOC 生成的 clicked 函数
-> QMetaObject::activate(...)
-> 找到连接
-> 直接调用槽,或向接收者线程投递调用
emit 只是一个便于阅读的宏。它不是一次独立的运行时操作;去掉 emit,信号函数调用依然成立。真正让 Qt 能分发信号的是 MOC 生成的元对象代码和 QMetaObject::activate()。
8.2 QPushButton:点击先落到 QAbstractButton
QPushButton 的鼠标交互大部分来自基类 QAbstractButton。一条简化路径是:
text
QApplication 交付 QMouseEvent
-> QAbstractButton::mousePressEvent()
-> 按钮进入 down 状态,发射 pressed()
-> QAbstractButton::mouseReleaseEvent()
-> 命中按钮区域时调用 click()
-> 若 checkable,更新 checked 状态并发射 toggled(...)
-> 发射 released()
-> 发射 clicked(checked)
-> QMetaObject::activate()
-> ProfileWidget::saveProfile()
因此,setCheckable(true) 并不是 QPushButton 额外加一个布尔字段那么简单:它会改变 click() 时的状态切换和信号序列。也能解释为什么"订阅开关"更适合 toggled(),而"保存"更适合 clicked()。
程序调用 button->click() 时,也会走按钮的点击语义并发出相关信号;直接调用业务槽函数则绕过了按钮状态与信号。测试"保存业务"时可直接测业务函数;测试"按钮是否正确连接"时应触发 click() 或用 QTest 模拟用户输入。
8.3 QLabel:设置文本后,不是在 setText() 里立刻画字
cpp
label->setText(tr("保存成功"));
其简化过程是:
text
QLabel::setText()
-> 保存新的文本与文本格式信息
-> 重新计算 sizeHint / 文本布局所需信息
-> updateGeometry() 通知父布局:建议尺寸可能变了
-> update() 请求一次重绘
-> 事件循环稍后投递 Paint 事件
-> QLabel::paintEvent() 按当前 QStyle 绘制文本或图片
这就是为什么一次连续的 setText() 不会马上逐像素绘制多次:Qt 通常把重绘请求合并,等事件循环有机会处理绘制事件。也因此,不要为了"马上显示状态文字"在 GUI 线程里调用耗时任务。耗时任务会占住事件循环,绘制事件同样无法执行;正确方向是把耗时工作移出 GUI 线程,并以信号报告结果。
8.4 QLineEdit:用户编辑与程序设值有意分流
QLineEdit 的文本编辑细节由内部的 QLineControl 协作完成。概念上可以分为两条路:
text
程序:setText("Alice")
-> 更新文本状态
-> textChanged("Alice")
-> 不发 textEdited(...)
用户:keyPressEvent(QKeyEvent)
-> QLineControl 处理按键、光标、选择区、验证器
-> 文本确实变化
-> textEdited(newText)
-> textChanged(newText)
这就是 textEdited() 存在的理由:程序回填输入框时,不应被误判为用户正在键入。内部还要处理撤销/重做、输入法预编辑、剪贴板粘贴、输入掩码和验证器,因此不建议通过重写 keyPressEvent() 来自己维护文本;优先使用公开信号、验证器和 QLineEdit API。
8.5 QComboBox:条目不是简单的字符串数组
addItem(text, userData) 看起来像把一行字符串加入列表,实际 QComboBox 通过内部模型保存条目数据,并使用弹出的列表视图呈现选择项。简化链路如下:
text
addItem("北京", "beijing")
-> 内部模型新增一行
-> DisplayRole 保存"北京",UserRole 保存"beijing"
用户在弹出列表选中一行
-> 视图的当前项改变
-> QComboBox 更新 currentIndex / 显示文本
-> currentIndexChanged(index)
-> currentTextChanged(text)
-> activated(index)(仅用户操作语义)
这也解释了两条实践建议:列表很少时用 addItem() 足够;当条目来自数据库、需要排序过滤、条目数很大或多个视图共享同一份数据时,应学习后续章节的 Model/View,改用 setModel() 提供数据模型。
九、把界面状态和业务状态分开
下面这种写法很容易在小项目里出现:
cpp
if (cityCombo->currentText() == "北京") {
// 业务分支
}
问题是:显示文本会被翻译、会改名,也可能由用户输入。优先读取 currentData(),并在保存时把控件状态转换为业务数据:
cpp
Profile profile;
profile.name = nameEdit->text().trimmed();
profile.cityCode = cityCombo->currentData().toString();
profile.notificationsEnabled = notifyButton->isChecked();
profileService.save(profile);
statusLabel->setText(tr("保存成功"));
控件是界面的短期状态容器,不应成为业务模型本身。
把转换集中在 saveProfile()、loadProfile() 等边界函数中,后续接入数据库、网络或单元测试时会轻松得多。
十、常见问题与排查顺序
10.1 为什么槽函数没有执行?
按这个顺序检查:控件指针是否有效;connect() 是否执行过;信号是否真的发出;控件是否被禁用;槽函数签名是否匹配;发送者或接收者是否已经析构。可以临时在槽函数首行加 qDebug(),也可以保存 connect() 的返回值:
cpp
const auto connection = connect(saveButton, &QPushButton::clicked,
this, &ProfileWidget::saveProfile);
Q_ASSERT(connection);
使用新式函数指针连接时,绝大多数签名问题会在编译期暴露;这正是它优于旧式 SIGNAL/SLOT 宏的重要原因。
10.2 为什么 QLineEdit 输入不了内容?
先检查是否被 setReadOnly(true) 或 setEnabled(false);再检查验证器和输入掩码。尤其是验证器正则写得过严时,用户可能连第一个字符都无法输入。把规则临时移除或改为允许 Intermediate 的形式,再观察问题是否消失。
10.3 为什么设置 setText() 后又触发了逻辑?
因为 setText() 会发出 textChanged()。如果回填数据期间不希望触发界面联动,可缩小屏蔽范围:
cpp
QSignalBlocker blocker(nameEdit);
nameEdit->setText(profile.name);
需要 #include <QSignalBlocker>。不要长期阻塞整个窗口的信号,更不要用它掩盖循环依赖;优先让槽函数能安全处理重复状态,或改用只响应用户操作的 textEdited()。
10.4 为什么下拉框收到的索引是 -1?
-1 表示当前没有有效条目。常见原因是列表尚未添加数据、调用了 setCurrentIndex(-1),或模型被清空。读取 currentData() 前要允许空值;表单场景可像本章示例那样放一个"请选择"占位项,并以空业务值表示未选择。
10.5 为什么状态标签不马上更新?
如果你在设置文本后立刻进行了耗时计算、同步网络请求或循环,事件循环没有机会执行绘制。不要用 repaint() 硬顶;应把耗时任务移至工作线程,完成后发射信号回到 GUI 线程更新 QLabel。第三阶段会完整讲解这条路径。
十一、四个控件常用API速查表
| 控件 | API | 用途 |
|---|---|---|
| QPushButton | setText() |
设置文字 |
setIcon() |
设置图标 | |
setEnabled() |
启用/禁用 | |
setCheckable() |
设置可选中 | |
setChecked() |
设置选中状态 | |
isChecked() |
获取状态 | |
setDefault() |
设置 Dialog 默认按钮 | |
| QLabel | setText() |
设置文本 |
setPixmap() |
设置图片 | |
setWordWrap() |
自动换行 | |
setAlignment() |
内容对齐 | |
setBuddy() |
关联输入控件 | |
| QLineEdit | text() |
获取文本 |
setText() |
设置文本 | |
setPlaceholderText() |
占位提示 | |
setReadOnly() |
只读 | |
setMaxLength() |
最大长度 | |
setValidator() |
输入校验 | |
hasAcceptableInput() |
判断是否有效 | |
| QComboBox | addItem() |
添加项目 |
currentText() |
当前显示文本 | |
currentData() |
当前业务数据 | |
findData() |
根据业务数据查找 | |
setCurrentIndex() |
设置当前项 | |
clear() |
清空 | |
count() |
项目数量 | |
setEditable() |
允许编辑 |
总结
四个控件可形成一个完整的最小交互闭环:QLabel 展示说明和结果,QLineEdit 收集单行输入,QComboBox 提供稳定的候选值,QPushButton 发起命令或维护二元状态。使用时优先掌握四件事:用布局安放控件;用属性限制可见行为;用新式 connect() 连接信号槽;在提交边界把控件状态转换为业务数据。
从源码视角看,控件 API 并非直接"画界面"或"调用函数":按钮和输入框先处理事件、更新内部状态,再经 MOC 生成的信号函数与 QMetaObject::activate() 找到连接;标签把重绘请求交给事件循环;下拉框则借助模型和视图保存、展示条目。理解这条主线后,后续面对更多控件时,就不再只是背 API。
下一章将继续补齐另一组高频控件:数值输入、范围选择、进度反馈与布尔选项。
下一篇预告:《12_常用控件速查(下):QSpinBox、QSlider、QProgressBar、QCheckBox》