“模板按钮按住时,另一动作才能执行”包含一个持续状态和一个瞬时触发。通常只需把动作连接一次,并在动作到达时读取当前状态;不必每次按下、释放都创建或拆除连接。
原场景的动作保持不变:切到 workspaceWidget 的索引 4,显示 threeButtons,隐藏 twoButtons。索引 4 指第五页,执行前必须保证该页已经存在;页面身份也可以按 [[10-knowledge/computing/software-development/application-development/desktop/qt/widgets/stacked-pages|堆叠页面]] 中的方法用指针管理。
先明确“按住”还是“选中”
跳转到“先明确“按住”还是“选中””QAbstractButton::isDown() 表示按钮当前处于按下状态;isChecked() 表示可选中按钮的选中状态。这两种交互不同:如果需求是先启用某种模板模式,再点击动作,就让模板按钮可选中并检查 isChecked();如果确实要求按住期间执行,才检查 isDown()。Qt:按钮状态
单个鼠标按住一个控件时,Qt 会把后续鼠标事件交给该次按压的接收控件,直到按钮释放。因此,“用同一个鼠标一直按住 A,再正常点击 B”不能作为默认可行的操作方式。下面保留严格的按住条件,并用 F6 快捷键触发动作:鼠标按住模板按钮的同时按 F6。另一种输入设备或业务调用也可以触发相同动作,但应按真实设备行为验证。Qt:鼠标事件与自动抓取
一次连接,读取真实状态
跳转到“一次连接,读取真实状态”以下完整程序建立五页和两组按钮。动作只有在模板按钮可用、仍按住、原按钮组仍显示时才执行,防止已经切换后的重复触发。正常启动可以交互使用;--check 模式用 Qt Test 生成模板按钮的按压与释放,并直接调用动作按钮的 click() 验证业务条件。测试不冒称模拟了同一鼠标同时点击两颗按钮。
#include <QApplication>#include <QKeySequence>#include <QLabel>#include <QPushButton>#include <QShortcut>#include <QStackedWidget>#include <QVBoxLayout>#include <QWidget>#include <QtTest/QTest>#include <cassert>
class Panel final : public QWidget {public: QPushButton *templateButton = nullptr; QPushButton *actionButton = nullptr; QStackedWidget *workspaceWidget = nullptr; QWidget *twoButtons = nullptr; QWidget *threeButtons = nullptr; int transitions = 0;
Panel() { auto *layout = new QVBoxLayout(this); templateButton = new QPushButton("Hold template; press F6"); templateButton->setAutoRepeat(false); layout->addWidget(templateButton); workspaceWidget = new QStackedWidget; for (int i = 0; i < 5; ++i) workspaceWidget->addWidget(new QLabel(QString("Page %1").arg(i))); layout->addWidget(workspaceWidget); twoButtons = new QWidget; auto *two = new QVBoxLayout(twoButtons); two->addWidget(new QPushButton("Other action")); actionButton = new QPushButton("Conditional action (F6)"); two->addWidget(actionButton); layout->addWidget(twoButtons); threeButtons = new QWidget; auto *three = new QVBoxLayout(threeButtons); for (int i = 1; i <= 3; ++i) three->addWidget(new QPushButton(QString("Next %1").arg(i))); layout->addWidget(threeButtons); auto *resetButton = new QPushButton("Reset"); layout->addWidget(resetButton); auto *shortcut = new QShortcut(QKeySequence(Qt::Key_F6), this); QObject::connect(shortcut, &QShortcut::activated, actionButton, &QPushButton::click); QObject::connect(actionButton, &QPushButton::clicked, this, [this] { performAction(); }); QObject::connect(resetButton, &QPushButton::clicked, this, [this] { resetPage(); }); resetPage(); } void resetPage() { workspaceWidget->setCurrentIndex(0); threeButtons->hide(); twoButtons->show(); } void performAction() { if (!templateButton->isEnabled() || !templateButton->isDown() || !twoButtons->isVisible()) return; workspaceWidget->setCurrentIndex(4); threeButtons->show(); twoButtons->hide(); ++transitions; }};
int main(int argc, char *argv[]) { QApplication app(argc, argv); Panel panel; panel.resize(360, 360); panel.show(); app.processEvents(); if (app.arguments().contains("--check")) { panel.actionButton->click(); assert(panel.transitions == 0); QTest::mousePress(panel.templateButton, Qt::LeftButton); assert(panel.templateButton->isDown()); panel.actionButton->click(); assert(panel.transitions == 1); assert(panel.workspaceWidget->currentIndex() == 4); assert(panel.threeButtons->isVisible() && !panel.twoButtons->isVisible()); panel.actionButton->click(); assert(panel.transitions == 1); QTest::mouseRelease(panel.templateButton, Qt::LeftButton); panel.resetPage(); panel.actionButton->click(); assert(panel.transitions == 1); QTest::mousePress(panel.templateButton, Qt::LeftButton); panel.templateButton->setEnabled(false); panel.actionButton->click(); assert(panel.transitions == 1); return 0; } return app.exec();}保存为 main.cpp,用 Debug 构建保留断言:
cmake_minimum_required(VERSION 3.20)project(ConditionalActions LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)find_package(Qt6 REQUIRED COMPONENTS Widgets Test)add_executable(conditional_actions main.cpp)target_link_libraries(conditional_actions PRIVATE Qt6::Widgets Qt6::Test)这里依赖 Qt Test 是为了同一份示例能运行按压断言。应用中的业务连接仍是普通 clicked 连接;如果移除测试分支,也可以去掉 Test 模块和头文件。
原来四类方案各有什么边界
跳转到“原来四类方案各有什么边界”| 方案 | 适合的用途 | 必须明确的边界 |
|---|---|---|
成员状态或直接读取 isDown() | 一个持续条件控制一次动作 | 直接读取避免维护第二份可能失步的按压标记;GUI 状态在 GUI 线程读取 |
| 事件过滤器维护状态 | 需要处理自定义输入、取消、拖动等低层动作 | 只看鼠标按下/释放会遗漏键盘、禁用、失焦和取消;观察时不要吞掉按钮默认事件 |
| 按状态连接和断开 | 确实要控制某条连接的存在 | 保存对应 QMetaObject::Connection,避免重复连接或误断其他逻辑;断开不保证撤回已排队调用 |
QStateMachine | 多个业务状态和合法迁移共同约束动作 | 建立显式状态与取消路径;信号迁移经状态机事件处理,不能把它假设成任意时刻都同步更新的布尔值 |
原来的局部 bool templateButtonPressed 配合多个 [=] lambda 不能共享可变状态:默认捕获的副本不能在非 mutable lambda 中赋值;加上 mutable 后也只是各自修改各自的副本。改成 [&] 若引用构造函数的局部变量,构造返回后会悬空。成员变量必须由持续存活的对象管理,并补全所有清除路径;这就是本例优先读取按钮状态的原因。
如果动作采用排队连接,还要定义条件是“发出请求时满足”还是“实际执行时仍满足”。本例使用 GUI 线程内的直接触发,检查的是执行时的状态。远程命令、工作线程通知或真正的安全联锁,应使用明确的业务状态和请求协议,不能把界面按钮外观作为唯一依据。Qt:连接与断开、Qt:状态机框架