11_常用控件速查(上):QPushButton、QLabel、QLineEdit、QComboBox

常用控件速查(上):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
)

这里仍然使用前几章的 QApplicationQWidget 和布局管理器。四个控件都属于 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 的对象树会依次析构它们,因此这里不需要手工 deleteQFormLayout 被放进 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 独有的,QToolButtonQCheckBox 等按钮类也共享这套机制。

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 clickedpressedreleasedtoggled 怎么选

信号 何时发出 适合做什么
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::TextSelectableByMouseQLabel 能识别一部分 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 只处理一行文本;地址、备注、日志等需要多行时应使用 QTextEditQPlainTextEdit。它的关键不是"拿到字符串",而是区分输入是否有效、变化来自程序还是用户,以及何时提交。

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 时触发,但受输入状态限制 提交、失焦校验

如果设置了 QValidatorinputMask,用户按 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();

验证器会对每次编辑返回三种状态:InvalidIntermediateAcceptableIntermediate 很重要,例如用户刚输入第一个字符时,虽然尚未达到两字符要求,但仍应允许他继续输入;因此不要简单地把"尚未完整"理解为"非法"。提交前用 hasAcceptableInput() 统一判断即可。

校验器改善交互,但不能替代业务校验。用户名是否被占用、手机号是否已注册、权限是否允许,都必须在业务层或服务端再次验证。

5.4 可编辑与只读

cpp 复制代码
lineEdit->setReadOnly(true);

readOnlyenabled 是两个不同概念。

  • 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》

相关推荐
Quz16 分钟前
QML 按键事件之方向控制
qt
Demon--hx23 分钟前
多态实现原理
开发语言·c++
水饺编程1 小时前
第5章,[Win32 章节] :绘制填充区域
c语言·c++·windows·visual studio
渡我白衣3 小时前
深入理解 Transformer:Transformer 究竟是什么?
java·linux·开发语言·c++·人工智能·深度学习·transformer
「QT(C++)开发工程师」11 小时前
C++ 11 常用for循环
开发语言·c++
我还记得那天11 小时前
0 初识C++
开发语言·c++
「QT(C++)开发工程师」13 小时前
C++ std::move 详解
开发语言·c++
Littlehero_12114 小时前
QT自定义控件之内嵌报表(4)(源码开源)
开发语言·qt