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

83 阅读22分钟

常用控件速查(上):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_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 后即可构建运行:

#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 最小使用

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

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 文本、换行、富文本边界与控制内容位置

auto *hintLabel = new QLabel(tr("密码至少包含 8 个字符。"), this);
hintLabel->setWordWrap(true);
hintLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);

statusLabel->setText(tr("正在连接服务器..."));

当文本会随着窗口宽度变化时,使用 setWordWrap(true)。需要让用户复制错误信息时,使用 Qt::TextSelectableByMouseQLabel 能识别一部分 HTML 富文本,但业务输入不应直接拼进 HTML;至少要用 toHtmlEscaped() 转义,避免文本被当作标签解释。

const QString userName = nameEdit->text();
label->setText(tr("欢迎,<b>%1</b>").arg(userName.toHtmlEscaped()));

常用的还有设置文本内容位置setAlignment()

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

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():标签和输入框的键盘关联

auto *nameLabel = new QLabel(tr("&昵称:"), this);
nameLabel->setBuddy(nameEdit);

& 标出助记键。用户按 Alt+N 时,焦点会进入 nameEdit。这是桌面端表单的细节优势:不要只为鼠标用户设计。

五、QLineEdit:单行输入、校验与提交时机

QLineEdit 只处理一行文本;地址、备注、日志等需要多行时应使用 QTextEditQPlainTextEdit。它的关键不是“拿到字符串”,而是区分输入是否有效、变化来自程序还是用户,以及何时提交。

5.1 常用属性

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()

connect(searchEdit, &QLineEdit::textEdited, this, &SearchWidget::requestSuggestions);

connect(nameEdit, &QLineEdit::returnPressed, this, &ProfileWidget::saveProfile);

returnPressed() 不等于任何时候按下 Enter 都会发出。设置校验器或输入掩码后,输入必须是可接受的,信号才会发出。

5.3 QValidator:让“是否有效”成为控件状态

本章示例把正则校验器安装到 nameEdit 上:

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 可编辑与只读

lineEdit->setReadOnly(true);

readOnlyenabled 是两个不同概念。

  • setReadOnly(true):用户不能修改,但仍可以获得焦点、选择和复制文本;
  • setEnabled(false):控件整体进入禁用状态,通常不能接受正常交互。

5.4 程序回填时如何避免触发联动?

QSignalBlocker blocker(nameEdit);
nameEdit->setText(profile.name);

QSignalBlocker 适合处理一次性的程序回填场景;它不是解决信号循环依赖的万能手段。

六、QComboBox:显示文本与业务值分开保存

QComboBox 将“用户看见的条目”与“程序需要的值”分开保存,这是它比一组硬编码 if 更可靠的原因。

6.1 添加条目与读取数据

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 消除歧义:

connect(cityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, &ProfileWidget::updateSaveButton);

不要写旧式字符串连接:

// 不推荐:编译器无法检查拼写和参数签名。
connect(cityCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSaveButton()));

6.3 可编辑下拉框不是普通输入框的替代品

cityCombo->setEditable(true);
cityCombo->setInsertPolicy(QComboBox::NoInsert);
cityCombo->setCompleter(completer);

设为可编辑后,QComboBox 内部会持有一个 QLineEdit,用户既能输入又能选择。适合“常用值 + 允许自定义值”的场景,例如标签、服务器地址、历史目录;如果只有自由输入需求,直接使用 QLineEdit 更简单。可编辑模式下,明确设置 InsertPolicy,避免用户的临时输入意外进入候选列表。

6.4 查找数据findData()

在实际业务中经常存在反向需求:已经有一个 cityCode = "shanghai",如何让 ComboBox 选中它?

这时候可以这样使用:

const int index = cityCombo->findData("shanghai");
if (index >= 0) {
    cityCombo->setCurrentIndex(index);
}

通过查找该数据项的索引,如果不存在那么说明没有这个数据。否则表示找到,可以继续使用索引执行自己的业务。

6.5 清空Clear()与统计Count()

combo->count();
combo->clear();
combo->removeItem(index);

这三个是QCombobox最常用的接口,分别是:统计当前数量、清空下拉项、删除指定下拉项。

七、信号槽:三种应该掌握的连接写法

信号槽并不是控件专属机制,但控件是最常见的发送者。connect() 的前两个参数描述“谁发出什么”,后两个参数描述“谁接收、执行什么”。

7.1 成员函数槽:业务逻辑的默认选择

connect(saveButton, &QPushButton::clicked, this, &ProfileWidget::saveProfile);

这适合可命名、可测试、可能增长的业务动作。由于函数指针在编译期参与类型检查,信号参数可以比槽函数参数多,但槽函数不能要求更多参数。例如 clicked(bool) 可连接到无参数的 saveProfile()

7.2 Lambda:短小的界面绑定

connect(notifyButton, &QPushButton::toggled, this,
        [this](bool checked) {
            statusLabel->setText(checked ? tr("通知开启") : tr("通知关闭"));
        });

这里的第三个参数 this 称为上下文对象。当 ProfileWidget 析构时,这条连接会自动断开,Lambda 不会继续访问已经销毁的 statusLabel。不要省略上下文对象后再捕获裸指针;那会让生命周期判断变得困难。

7.3 重载信号:用 qOverload 说清楚签名

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++ 回调指针”

当你写:

connect(saveButton, &QPushButton::clicked, this, &ProfileWidget::saveProfile);

新式函数指针连接利用 C++ 类型系统进行编译期检查;

对于 Qt 的元对象信号槽机制,连接关系会被 Qt 的元对象系统记录和管理。真正发射信号时,MOC 生成的信号函数会进入 QMetaObject::activate();它遍历匹配的连接,并按连接类型直接调用或投递事件。

可以先把它理解成:

connect(...)
  -> 记录连接关系(sender + signal -> receiver + slot)

emit clicked(...)
  -> MOC 生成的 clicked 函数
  -> QMetaObject::activate(...)
  -> 找到连接
  -> 直接调用槽,或向接收者线程投递调用

emit 只是一个便于阅读的宏。它不是一次独立的运行时操作;去掉 emit,信号函数调用依然成立。真正让 Qt 能分发信号的是 MOC 生成的元对象代码和 QMetaObject::activate()

8.2 QPushButton:点击先落到 QAbstractButton

QPushButton 的鼠标交互大部分来自基类 QAbstractButton。一条简化路径是:

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() 里立刻画字

label->setText(tr("保存成功"));

其简化过程是:

QLabel::setText()
  -> 保存新的文本与文本格式信息
  -> 重新计算 sizeHint / 文本布局所需信息
  -> updateGeometry() 通知父布局:建议尺寸可能变了
  -> update() 请求一次重绘
  -> 事件循环稍后投递 Paint 事件
  -> QLabel::paintEvent() 按当前 QStyle 绘制文本或图片

这就是为什么一次连续的 setText() 不会马上逐像素绘制多次:Qt 通常把重绘请求合并,等事件循环有机会处理绘制事件。也因此,不要为了“马上显示状态文字”在 GUI 线程里调用耗时任务。耗时任务会占住事件循环,绘制事件同样无法执行;正确方向是把耗时工作移出 GUI 线程,并以信号报告结果。

8.4 QLineEdit:用户编辑与程序设值有意分流

QLineEdit 的文本编辑细节由内部的 QLineControl 协作完成。概念上可以分为两条路:

程序:setText("Alice")
  -> 更新文本状态
  -> textChanged("Alice")
  -> 不发 textEdited(...)

用户:keyPressEvent(QKeyEvent)
  -> QLineControl 处理按键、光标、选择区、验证器
  -> 文本确实变化
  -> textEdited(newText)
  -> textChanged(newText)

这就是 textEdited() 存在的理由:程序回填输入框时,不应被误判为用户正在键入。内部还要处理撤销/重做、输入法预编辑、剪贴板粘贴、输入掩码和验证器,因此不建议通过重写 keyPressEvent() 来自己维护文本;优先使用公开信号、验证器和 QLineEdit API。

8.5 QComboBox:条目不是简单的字符串数组

addItem(text, userData) 看起来像把一行字符串加入列表,实际 QComboBox 通过内部模型保存条目数据,并使用弹出的列表视图呈现选择项。简化链路如下:

addItem("北京", "beijing")
  -> 内部模型新增一行
  -> DisplayRole 保存“北京”,UserRole 保存“beijing”

用户在弹出列表选中一行
  -> 视图的当前项改变
  -> QComboBox 更新 currentIndex / 显示文本
  -> currentIndexChanged(index)
  -> currentTextChanged(text)
  -> activated(index)(仅用户操作语义)

这也解释了两条实践建议:列表很少时用 addItem() 足够;当条目来自数据库、需要排序过滤、条目数很大或多个视图共享同一份数据时,应学习后续章节的 Model/View,改用 setModel() 提供数据模型。

九、把界面状态和业务状态分开

下面这种写法很容易在小项目里出现:

if (cityCombo->currentText() == "北京") {
    // 业务分支
}

问题是:显示文本会被翻译、会改名,也可能由用户输入。优先读取 currentData(),并在保存时把控件状态转换为业务数据:

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() 的返回值:

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()。如果回填数据期间不希望触发界面联动,可缩小屏蔽范围:

QSignalBlocker blocker(nameEdit);
nameEdit->setText(profile.name);

需要 #include <QSignalBlocker>。不要长期阻塞整个窗口的信号,更不要用它掩盖循环依赖;优先让槽函数能安全处理重复状态,或改用只响应用户操作的 textEdited()

10.4 为什么下拉框收到的索引是 -1

-1 表示当前没有有效条目。常见原因是列表尚未添加数据、调用了 setCurrentIndex(-1),或模型被清空。读取 currentData() 前要允许空值;表单场景可像本章示例那样放一个“请选择”占位项,并以空业务值表示未选择。

10.5 为什么状态标签不马上更新?

如果你在设置文本后立刻进行了耗时计算、同步网络请求或循环,事件循环没有机会执行绘制。不要用 repaint() 硬顶;应把耗时任务移至工作线程,完成后发射信号回到 GUI 线程更新 QLabel。第三阶段会完整讲解这条路径。

十一、四个控件常用API速查表

控件API用途
QPushButtonsetText()设置文字
setIcon()设置图标
setEnabled()启用/禁用
setCheckable()设置可选中
setChecked()设置选中状态
isChecked()获取状态
setDefault()设置 Dialog 默认按钮
QLabelsetText()设置文本
setPixmap()设置图片
setWordWrap()自动换行
setAlignment()内容对齐
setBuddy()关联输入控件
QLineEdittext()获取文本
setText()设置文本
setPlaceholderText()占位提示
setReadOnly()只读
setMaxLength()最大长度
setValidator()输入校验
hasAcceptableInput()判断是否有效
QComboBoxaddItem()添加项目
currentText()当前显示文本
currentData()当前业务数据
findData()根据业务数据查找
setCurrentIndex()设置当前项
clear()清空
count()项目数量
setEditable()允许编辑

总结

四个控件可形成一个完整的最小交互闭环:QLabel 展示说明和结果,QLineEdit 收集单行输入,QComboBox 提供稳定的候选值,QPushButton 发起命令或维护二元状态。使用时优先掌握四件事:用布局安放控件;用属性限制可见行为;用新式 connect() 连接信号槽;在提交边界把控件状态转换为业务数据。

从源码视角看,控件 API 并非直接“画界面”或“调用函数”:按钮和输入框先处理事件、更新内部状态,再经 MOC 生成的信号函数与 QMetaObject::activate() 找到连接;标签把重绘请求交给事件循环;下拉框则借助模型和视图保存、展示条目。理解这条主线后,后续面对更多控件时,就不再只是背 API。

下一章将继续补齐另一组高频控件:数值输入、范围选择、进度反馈与布尔选项。


下一篇预告:《12_常用控件速查(下):QSpinBox、QSlider、QProgressBar、QCheckBox》