跳转到内容
新建笔记

Qt 条件动作:在按钮状态满足时切换页面

“模板按钮按住时,另一动作才能执行”包含一个持续状态和一个瞬时触发。通常只需把动作连接一次,并在动作到达时读取当前状态;不必每次按下、释放都创建或拆除连接。

原场景的动作保持不变:切到 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:状态机框架