跳转到内容
新建笔记

Qt 按钮组:互斥选中、ID 与路由条目

两个可选按钮需要“选中一个就取消另一个”时,可以放进 QButtonGroup。实际类名不是 QGroupButton;它继承 QObject,是逻辑分组,不绘制外框,也不负责排版。

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

选中状态、按钮 ID 与对象归属

跳转到“选中状态、按钮 ID 与对象归属”

普通 QPushButton 默认不可选中,先设 setCheckable(true)。按钮组默认互斥,但不会自动挑选初始按钮;需要默认选择时,加入组后明确调用 setChecked(true)。互斥组中再次点击已选按钮,不会把整组选成“全部未选”。QButtonGroup

接口或概念用途与边界
addButton(button, id)、removeButton(button)管理组成员关系;不是重新设置 QWidget 父对象,移出组也不会删除按钮
button(id)、id(button)、setId(button, id)ID 与按钮之间映射;应用应为每个成员分配不同的正整数 ID
checkedButton()、checkedId()查询选中成员;没有选择时分别为 nullptr、-1
buttons()枚举成员,可据此逐个设置 enabled
setExclusive(false)允许多个可选按钮同时选中;例如一组独立开关
QGroupBox / 布局需要可见分组框或排版时另用这些对象

自动 ID 从 -2 开始取负数,-1 保留为“没有对应按钮”。QButtonGroup 没有 QWidget 的 setEnabled();批量禁用应遍历按钮,或禁用它们共同的可视父容器。组的 QObject 父对象负责组自身的寿命,按钮的 QWidget 父对象仍负责按钮寿命。

保留 PyQt5 示例:两个 QPushButton 互斥

跳转到“保留 PyQt5 示例:两个 QPushButton 互斥”

保存为 button_group.py,安装 PyQt5 后运行。这里明确使用 PyQt5 的 buttonClicked[int] 重载和 exec_();不能不加区分地复制成 Qt 6 C++ 写法。--self-test 分支检查默认选择、切换、重复点击和组成员移除。

import sys
from PyQt5.QtWidgets import (
QApplication, QWidget, QPushButton, QButtonGroup, QVBoxLayout
)
class ExampleApp(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("QButtonGroup example")
self.resize(300, 200)
self.button1 = QPushButton("Button 1", self)
self.button2 = QPushButton("Button 2", self)
self.button_group = QButtonGroup(self)
self.button_group.setExclusive(True)
layout = QVBoxLayout(self)
for number, button in enumerate((self.button1, self.button2), 1):
button.setCheckable(True)
self.button_group.addButton(button, number)
layout.addWidget(button)
self.button1.setChecked(True)
self.last_clicked_id = None
self.button_group.buttonClicked[int].connect(self.record_click)
def record_click(self, number):
self.last_clicked_id = number
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ExampleApp()
if "--self-test" in sys.argv:
assert window.button_group.checkedId() == 1
window.button2.click()
assert window.last_clicked_id == 2
assert window.button2.isChecked() and not window.button1.isChecked()
window.button2.click()
assert window.button_group.checkedId() == 2
window.button_group.removeButton(window.button1)
assert window.button_group.button(1) is None
assert window.button1.parent() is window
print("PyQt5 button-group checks passed")
else:
window.show()
sys.exit(app.exec_())

Qt 6:路由条目、checked 样式与统一信号

跳转到“Qt 6:路由条目、checked 样式与统一信号”

原来的 RoutesItem 片段保留“条目携带文件名、选中时改变边框、由组统一处理点击”的设计,下面补齐为一个 Qt 6 Widgets、C++17 程序。保存为 main.cpp,构建时启用 CMake AUTOMOC。自定义类声明 Q_OBJECT,供 qobject_cast<RoutesItem *> 使用;文件末尾的 main.moc 对应此文件名。

#include <QApplication>
#include <QAbstractButton>
#include <QButtonGroup>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <cassert>
class RoutesItem final : public QPushButton {
Q_OBJECT
public:
RoutesItem(const QString &file, QWidget *parent = nullptr)
: QPushButton(parent), file_(file) {
setCheckable(true);
setStyleSheet(
"QPushButton { border-bottom: 1px solid gray;"
"border-top: 1px solid gray; border-radius: 0; padding: 0; }"
"QPushButton:checked { background-color: lightgray; border: none;"
"border-bottom: 1px solid rgb(70,70,70);"
"border-left: 10px solid rgb(70,70,70); border-radius: 0; }"
"QLabel { background-color: transparent; }");
auto *label = new QLabel(file, this);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
auto *layout = new QVBoxLayout(this);
layout->addWidget(label);
setAccessibleName(file);
setMinimumHeight(48);
}
QString fileName() const { return file_; }
private:
QString file_;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget window;
auto *layout = new QVBoxLayout(&window);
auto *first = new RoutesItem("route-a.json", &window);
auto *second = new RoutesItem("route-b.json", &window);
auto *group = new QButtonGroup(&window);
group->addButton(first, 1);
group->addButton(second, 2);
first->setChecked(true);
layout->addWidget(first);
layout->addWidget(second);
auto *status = new QLabel(first->fileName(), &window);
layout->addWidget(status);
int lastId = -1;
QObject::connect(group, &QButtonGroup::idClicked, &window,
[&](int id) { lastId = id; });
QObject::connect(group, &QButtonGroup::buttonClicked, &window,
[status](QAbstractButton *button) {
if (auto *item = qobject_cast<RoutesItem *>(button))
status->setText(item->fileName());
});
if (app.arguments().contains("--self-test")) {
assert(group->checkedId() == 1);
second->click();
assert(lastId == 2 && status->text() == "route-b.json");
assert(group->checkedButton() == second && !first->isChecked());
second->click();
assert(second->isChecked());
group->setId(first, 3);
assert(group->button(3) == first && group->button(1) == nullptr);
for (auto *button : group->buttons()) button->setEnabled(false);
assert(!first->isEnabled() && !second->isEnabled());
group->removeButton(first);
assert(first->parentWidget() == &window && group->id(first) == -1);
return 0;
}
window.show();
return app.exec();
}
#include "main.moc"

Qt 5 旧代码常用 QOverload<int>::of(&QButtonGroup::buttonClicked) 选择整数重载;Qt 6 对应写法是 &QButtonGroup::idClicked。按钮指针信号仍叫 buttonClicked,两者不要混写。连接时提供上下文对象,也让窗口销毁后连接自动失效。

checked 样式依赖可选中状态,不等于 hover。内部 QLabel 设为鼠标透明,让按下事件落到按钮;文字可访问名称也单独设置。使用 Designer 的 scrollAreaWidgetContents 时可以把它替换为这里的 window 容器,但不需要把未知 ui 成员或省略号复制进独立程序。

原互斥条目来源:CSDN 原文,首次发布于 2025-04-18。页面切换中的 ID 映射另见 布局参数与页面切换。