跳转到内容
新建笔记

Qt 按钮控件:Widgets、QML 与 Designer 提升

Qt Widgets 的按钮基类是 QAbstractButton,实际常用控件包括 QPushButton、QRadioButton、QCheckBox 和 QToolButton。Qt Quick Controls 也有 AbstractButton,但它使用 QML 属性和信号,不能把两个体系的 API 放进同一张表后互相替用。

本文 C++ 示例的 --self-test 使用 assert,应在 Debug 构建或未定义 NDEBUG 的情况下运行;Release 构建若禁用了断言,退出成功不代表这些检查已经执行。

Qt 类继承关系原图,包含按钮、输入、容器、模型视图、事件和绘图等分类

QWindow 与三维图形、系列和数据代理的原关系图

两张原图保留作分类索引,覆盖范围比按钮更广,也包含不同模块的类。它们不是特定 Qt 新版本全部可用类型的清单;是否安装模块、继承关系和版本变化应查对应版本文档。尤其不能由图中都出现 UI 对象,就推断它们全都继承 QWidget。

Qt Widgets:QAbstractButton 的接口层次

跳转到“Qt Widgets:QAbstractButton 的接口层次”
分类名称用途与限制
互斥属性autoExclusive同一父控件下的可选按钮自动互斥;已加入 QButtonGroup 时按组管理
重复属性autoRepeat、autoRepeatDelay、autoRepeatInterval按住期间重复触发按钮信号,时间单位毫秒
状态属性checkable、checked、down是否可选、是否选中、是否显示按下;三者不是同一状态
内容属性text、icon、iconSize、shortcut文字、QIcon、图标尺寸和快捷键
公共槽click()、animateClick()立即模拟点击或带按下动画的点击
公共槽setChecked(bool)、toggle()、setIconSize(QSize)设置选中、切换状态和图标尺寸
信号clicked(bool)、pressed()、released()、toggled(bool)点击、按下、释放、选中变化
构造与析构QAbstractButton(QWidget *)、~QAbstractButton()这是抽象基类,不能直接 new QAbstractButton;使用具体子类或实现必要虚函数
受保护虚函数checkStateSet()、hitButton(QPoint)、nextCheckState()供自定义子类调整状态处理和命中检测,不能当成外部公共调用
受保护纯虚函数paintEvent(QPaintEvent *)自定义绘制入口,具体子类须提供实现

属性的访问方式仍分别是 setText()/text()、setIcon()/icon()、setIconSize()/iconSize()、setShortcut()/shortcut()、setAutoExclusive()/autoExclusive()、setAutoRepeat()/autoRepeat()、setAutoRepeatDelay()/autoRepeatDelay()、setAutoRepeatInterval()/autoRepeatInterval()、setCheckable()/isCheckable()、setChecked()/isChecked()、setDown()/isDown()。

程序调用 setChecked() 可能发出 toggled,却不会因此产生一次 clicked。setDown(true) 用于按下状态,也不等于执行一次点击。有关公共槽与受保护虚函数的正式区分,参见 QAbstractButton。

创建按钮、单选按钮与批量设置

跳转到“创建按钮、单选按钮与批量设置”

原片段在 YOLOWINDOW 中用 setGeometry(QRect(QPoint(100,100), QSize(200,50))) 放置按钮。这适合演示绝对位置;需要随窗口缩放的界面,通常使用布局。下面补齐 Qt 6 Widgets、C++17 程序,同时保留两个单选按钮、toggled 处理和按对象名启用按钮的用途。

#include <QApplication>
#include <QLabel>
#include <QPushButton>
#include <QRadioButton>
#include <QStringList>
#include <QVBoxLayout>
#include <QWidget>
#include <cassert>
static void setAllButtonsEnabled(QWidget &root, bool enabled) {
for (auto *button : root.findChildren<QPushButton *>())
button->setEnabled(enabled);
}
static void enableOnly(QWidget &root, const QStringList &names) {
for (auto *button : root.findChildren<QPushButton *>())
button->setEnabled(names.contains(button->objectName()));
}
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget window;
auto *layout = new QVBoxLayout(&window);
auto *first = new QPushButton("My Button", &window);
auto *repeat = new QPushButton("Hold to repeat", &window);
first->setObjectName("pushButton");
repeat->setObjectName("pushButton_2");
repeat->setAutoRepeat(true);
repeat->setAutoRepeatDelay(400);
repeat->setAutoRepeatInterval(50);
layout->addWidget(first);
layout->addWidget(repeat);
auto *option1 = new QRadioButton("Option 1", &window);
auto *option2 = new QRadioButton("Option 2", &window);
layout->addWidget(option1);
layout->addWidget(option2);
auto *status = new QLabel("Option 1", &window);
layout->addWidget(status);
for (auto *radio : {option1, option2}) {
QObject::connect(radio, &QRadioButton::toggled, &window,
[radio, status](bool checked) {
if (checked) status->setText(radio->text());
});
}
option1->setChecked(true);
int repeatCount = 0;
QObject::connect(repeat, &QPushButton::clicked, &window,
[&](bool) { status->setText(QString::number(++repeatCount)); });
if (app.arguments().contains("--self-test")) {
assert(option1->autoExclusive() && option2->autoExclusive());
option2->click();
assert(option2->isChecked() && !option1->isChecked());
assert(status->text() == "Option 2");
assert(repeat->autoRepeat() && repeat->autoRepeatDelay() == 400);
assert(repeat->autoRepeatInterval() == 50);
setAllButtonsEnabled(window, false);
assert(!first->isEnabled() && !repeat->isEnabled());
assert(option2->isEnabled());
enableOnly(window, {"pushButton_2"});
assert(!first->isEnabled() && repeat->isEnabled());
repeat->click();
assert(repeatCount == 1);
window.setEnabled(false);
repeat->setEnabled(true);
assert(!repeat->isEnabled());
return 0;
}
window.show();
return app.exec();
}

同一 QWidget 父对象下的 QRadioButton 默认自动互斥;需要多组、跨容器分组或稳定 ID 时,使用 QButtonGroup。这里用 lambda 捕获具体按钮,因此不需要从 sender() 猜测信号来源,也不需要仅为普通成员函数加入 Q_OBJECT。

findChildren<QPushButton *>() 默认递归查找后代,包括 QPushButton 的派生类,不包括 QRadioButton。若只找直接子对象,应传 Qt::FindDirectChildrenOnly;对象名也不是 Qt 强制唯一的 ID。上面的 enableOnly 对所有匹配名字生效,生产界面应保证命名规则明确。禁用共同祖先后,单独对子按钮 setEnabled(true) 不能让它有效;也不要把启用状态代替实际业务权限校验。QObject 子对象查找、QWidget enabled

autoRepeat 保留原例的 400 ms 延迟和 50 ms 间隔,但它表示重复点击,不是新增一个“长按一次”的信号。如果要求只在超过阈值后执行一次,并取消普通短按动作,需要另写计时和取消逻辑。上述 self-test 验证配置及普通程序点击;持续按压的真实事件时序应另测,不能仅因配置值正确就推断所有平台手感相同。

Qt Quick Controls:单独阅读 QML API

跳转到“Qt Quick Controls:单独阅读 QML API”

下表保留原第二张表的条目,并标明它属于 QML AbstractButton。此处示例范围是 Qt 6.10;click() 与 animateClick() 从 Qt 6.8 才提供。

分类名称正确理解
信号canceled()、clicked()、doubleClicked()取消、点击、双击
信号pressAndHold()、pressed()、released()长按、按下、释放;autoRepeat 开启时不发 pressAndHold
信号toggled()无 bool 参数;处理器内读 checked,不能照抄 Widgets 的 toggled(bool)
方法click()、animateClick()、toggle()前两者模拟点击,toggle 改变选中状态
属性action、autoExclusive关联 QML Action、同父项的自动互斥
属性autoRepeat、autoRepeatDelay、autoRepeatInterval是否重复、初次延迟、间隔
属性checkable、checked、down、text可选中、选中、视觉按下和文字
属性display、icon控制文字/图标显示方式及图标组合
图标属性icon.cache、icon.color、icon.height、icon.width、icon.name、icon.source缓存、颜色、尺寸、主题名称和 URL
指示器indicator、implicitIndicatorHeight、implicitIndicatorWidth指示器 Item 与其隐含尺寸
只读状态pressX、pressY、pressed最近按下坐标及当前物理按下状态,不能写 myButton.pressed = true

QML 的 onToggled 用于交互切换;需要响应 checked 属性的所有变化时用 onCheckedChanged。视觉 down 和物理 pressed 分开处理,原表的 pressed 赋值示例无效。参见 AbstractButton QML Type。

保存以下两个文件到同一个目录。MyButton.qml 定义组件:

import QtQuick
import QtQuick.Controls
Button {
id: control
objectName: "customButton"
checkable: true
text: checked ? "Selected" : "Select"
onToggled: console.log("checked:", checked)
}

main.qml 使用该组件:

import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 360
height: 200
visible: true
title: "QML button"
MyButton { anchors.centerIn: parent }
}

这些是普通 .qml 文件,可以由 Qt QML 运行器或项目加载。原命令 uic -o output.ui.qml input.ui 不会把 Widgets 界面转换为 QML;uic 读取 Widgets .ui XML 并生成相应代码,给输出文件换后缀不会改变界面体系。Qt Design Studio 的 .ui.qml 还带有设计工具的声明式格式约束,不应把含任意脚本的普通 QML 文件直接改名后等同使用。uic

Designer 的 Promote to:基类必须真实匹配

跳转到“Designer 的 Promote to:基类必须真实匹配”

提升控件用于让 Widgets Designer 中的占位控件在构建时成为自定义派生类,不会把控件变成 QML。步骤是先实现子类,再放置兼容基类控件,填写 Promoted class name 与真实头文件路径,Add 后 Promote。头文件名区分大小写的平台尤其需要严格匹配;项目也必须编译并链接子类实现。

Designer 提升控件对话框原图,基类字段为 QTableView

原文字用从 QTableWidget 继承的 Spreadsheet 举例,但截图的 Base class name 实际是 QTableView。若 Spreadsheet 继承 QTableWidget,就放置并提升 QTableWidget;若继承 QTableView,就按截图选 QTableView。不能只复制类名与头文件而忽略基类差异。子类通常需要接受 QWidget *parent 的构造方式。Designer 在没有自定义插件时显示的仍主要是占位基类外观,运行时才体现子类行为。Designer 自定义控件