两个可选按钮需要“选中一个就取消另一个”时,可以放进 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 sysfrom 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_OBJECTpublic: 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 成员或省略号复制进独立程序。