跳转到内容
新建笔记

Qt 控件的继承与组合:避免重复 QObject 基类

QPushButton 已经通过 QAbstractButton 继承了 QWidget。再声明 class AbstractPropertyItem : public QWidget, public QPushButton,会引入两条 QWidget、QObject 基类路径。普通成员访问和基类转换会产生二义性,Qt 的元对象工具也不支持这种重复 QObject 基类设计。

若对象本身就是一个按钮,直接继承 QPushButton;若对象是包含按钮、文字等元素的一行属性编辑器,继承 QWidget 并组合子控件。下面给出这两种方式的完整 Qt 6 / C++17 示例。

重复基类的问题在哪里

跳转到“重复基类的问题在哪里”

这里不是“内存布局随机混乱”。C++ 对重复的非虚基类子对象有明确规则,问题是一个对象里确实存在两份基类子对象,调用者没有说明要走哪条路径。原声明也不是必须画成左右对称菱形才会出问题;一条直接继承路径和一条间接路径就足以重复。

以下是用于观察编译诊断的完整翻译单元,可单独保存为 invalid.cpp 并只做编译检查;它预期编译失败,不加入后面的可运行应用。

invalid.cpp
#include <QPushButton>
#include <QWidget>
class InvalidItem : public QWidget, public QPushButton {};
void demonstrate(InvalidItem& item) {
item.show(); // ambiguous: two QWidget base subobjects
}

不要把普通 C++ 菱形继承的“把公共基类改成虚继承”直接套到 Qt 类层次。Qt 明确规定:moc 处理多重继承时,第一条基类路径必须提供 QObject,只有第一条基类路径可以派生自 QObject,并且不支持 QObject 的虚继承。moc 的多重继承限制

这仍然允许一个 QWidget 子类实现不继承 QObject 的普通 C++ 接口。下面的 Resettable 只定义重置操作,AbstractPropertyItem 把 QWidget 放在第一基类,保留唯一 QObject 路径。

组合属性行与直接派生按钮

跳转到“组合属性行与直接派生按钮”

AbstractPropertyItem 拥有一个 QPushButton 和计数标签,通过 getButton() 暴露按钮入口。按钮点击连接到该属性行的槽,更新计数后发出自定义 activated(int) 信号。ButtonPropertyItem 则直接作为按钮使用,它已经拥有 QWidget 的显示、尺寸和事件功能。

main.cpp
#include <QApplication>
#include <QDebug>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
class Resettable {
public:
virtual ~Resettable() = default;
virtual void reset() = 0;
};
class AbstractPropertyItem : public QWidget, public Resettable {
Q_OBJECT
public:
explicit AbstractPropertyItem(QWidget* parent = nullptr)
: QWidget(parent), button_(new QPushButton("Click Me", this)),
countLabel_(new QLabel("Clicks: 0", this)) {
auto* layout = new QVBoxLayout(this);
layout->addWidget(button_);
layout->addWidget(countLabel_);
connect(button_, &QPushButton::clicked,
this, &AbstractPropertyItem::onButtonClicked);
}
QPushButton* getButton() const { return button_; }
int clickCount() const { return clickCount_; }
void reset() override {
clickCount_ = 0;
countLabel_->setText("Clicks: 0");
}
signals:
void activated(int total);
private slots:
void onButtonClicked() {
++clickCount_;
countLabel_->setText(QString("Clicks: %1").arg(clickCount_));
qDebug() << "Button clicked!" << clickCount_;
emit activated(clickCount_);
}
private:
QPushButton* button_;
QLabel* countLabel_;
int clickCount_ = 0;
};
class ButtonPropertyItem : public QPushButton {
Q_OBJECT
public:
explicit ButtonPropertyItem(QWidget* parent = nullptr)
: QPushButton("Click Me", parent) {
connect(this, &QPushButton::clicked,
this, &ButtonPropertyItem::onButtonClicked);
}
int clickCount() const { return clickCount_; }
private slots:
void onButtonClicked() {
++clickCount_;
setText(QString("Button clicks: %1").arg(clickCount_));
qDebug() << "Button clicked!" << clickCount_;
}
private:
int clickCount_ = 0;
};
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QWidget window;
auto* layout = new QVBoxLayout(&window);
auto* property = new AbstractPropertyItem(&window);
auto* button = new ButtonPropertyItem(&window);
auto* resetButton = new QPushButton("Reset property row", &window);
layout->addWidget(property);
layout->addWidget(button);
layout->addWidget(resetButton);
QObject::connect(resetButton, &QPushButton::clicked,
property, &AbstractPropertyItem::reset);
window.setWindowTitle("Composition and button inheritance");
window.resize(360, 240);
window.show();
return app.exec();
}
#include "main.moc"
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(widget_composition LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
add_executable(widget-example main.cpp)
target_link_libraries(widget-example PRIVATE Qt6::Widgets)

把 main.cpp 和 CMakeLists.txt 放在同一目录构建。两个带 Q_OBJECT 的类都在这个源文件中,末尾包含 main.moc,由 CMake 的 AUTOMOC 生成。点击属性行中的按钮会同步改变标签并发出 activated;点击第二个按钮会改变按钮自身文字;重置只作用于属性行。

布局和所有权跟着组合关系走

跳转到“布局和所有权跟着组合关系走”

new QVBoxLayout(this) 已经把布局安装到当前 QWidget,不需要再调用一遍 setLayout(layout)。按钮和标签以属性行为父对象,属性行销毁时会自动释放这些子对象。getButton() 返回的是借用指针:只在属性行和按钮都仍存活时使用;把它长期保存在别处时,应考虑 QPointer<QPushButton> 等生命周期观察方式。QObject 对象树

Resettable 只供普通 C++ 多态使用。本例可以通过 Resettable& 调用 reset(),但并没有把这个接口注册成可供 qobject_cast<Resettable*> 查询的 Qt 接口;需要后一种能力时,另行定义接口标识并使用 Q_DECLARE_INTERFACE、Q_INTERFACES。

继承描述“这个对象是什么”,组合描述“这个对象包含什么”。如果还需要让组合中的内容区分别使用滚动区或堆叠窗口,见不同类型的内容区;构造函数和 parent 的关系见构造顺序与父对象。