QPushButton 已经通过 QAbstractButton 继承了 QWidget。再声明 class AbstractPropertyItem : public QWidget, public QPushButton,会引入两条 QWidget、QObject 基类路径。普通成员访问和基类转换会产生二义性,Qt 的元对象工具也不支持这种重复 QObject 基类设计。
若对象本身就是一个按钮,直接继承 QPushButton;若对象是包含按钮、文字等元素的一行属性编辑器,继承 QWidget 并组合子控件。下面给出这两种方式的完整 Qt 6 / C++17 示例。
重复基类的问题在哪里
跳转到“重复基类的问题在哪里”这里不是“内存布局随机混乱”。C++ 对重复的非虚基类子对象有明确规则,问题是一个对象里确实存在两份基类子对象,调用者没有说明要走哪条路径。原声明也不是必须画成左右对称菱形才会出问题;一条直接继承路径和一条间接路径就足以重复。
以下是用于观察编译诊断的完整翻译单元,可单独保存为 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 的显示、尺寸和事件功能。
#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_OBJECTpublic: 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_OBJECTpublic: 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"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 的关系见构造顺序与父对象。