跳转到内容
新建笔记

Qt 内容区类型设计:基类指针、虚工厂与模板

基类成员声明为 QWidget *contentArea 后,这个成员的静态类型不会随派生类改变。它可以指向实际类型为 QScrollArea、QStackedWidget 或其他 QWidget 子类的对象;具体对象决定运行时行为,指针的静态类型决定编译时能直接调用哪些接口。

选择方案之前先确认两件事:每种派生类的内容类型是否固定,以及内容控件是否会在运行中更换。下面保留 QWidget 基类指针、虚工厂和模板三种思路,同时明确构造时机与对象所有权。示例使用 C++17 和 Qt 6 Widgets。

基类构造函数不能调用到派生类的工厂

跳转到“基类构造函数不能调用到派生类的工厂”

原例在 BaseWidget 构造函数中调用虚函数 createContentArea(),期待进入 ScrollWidget 的重写并创建 QScrollArea。这个期待不成立:执行基类构造函数时,虚调用分派到正在构造的基类层级,不会进入尚未构造的派生层级。析构期间也有相应限制。构造和析构期间的虚调用

下面是独立、完整的 C++ 程序。duringConstruction 保存 base,对象构造完成后再调用 kind() 才得到 derived。把基类工厂改为纯虚函数,再从基类构造函数调用它,也不是修复方法。

virtual-dispatch.cpp
#include <iostream>
class BaseProbe {
public:
BaseProbe() : duringConstruction(kind()) {}
virtual ~BaseProbe() = default;
virtual const char* kind() const { return "base"; }
const char* duringConstruction;
};
class DerivedProbe final : public BaseProbe {
public:
const char* kind() const override { return "derived"; }
};
int main() {
DerivedProbe value;
std::cout << value.duringConstruction << ' ' << value.kind() << '\n';
}

修复时可以让派生类在自己的构造函数体里创建并安装控件,也可以把创建策略作为参数注入基类,或在完整对象构造后统一初始化。下面使用第一种思路:基类只创建布局,final 派生类在自身构造函数体调用 initializeContent()。此时工厂会分派到该派生类的实现;final 明确禁止再向下派生并误以为下一层工厂也已可用。

一份基类指针,加上有边界的安装接口

跳转到“一份基类指针,加上有边界的安装接口”

基类持有 QPointer<QWidget>,对外仍以 QWidget* 提供通用接口。QPointer 是对象存活状态的观察者,不负责释放对象;对象树才负责控件所有权。内容安装接口要求新控件已由当前窗口直接拥有,统一更新布局并删除旧控件,避免派生类只改指针却漏改布局。

ScrollWidget 通过 qobject_cast<QScrollArea*> 取得专用接口。转换失败会返回空指针,因此不能仅因变量名称叫 contentArea 就认为任何向下转换都安全。模板版本则在编译时固定 ContentType,无需向下转换。

main.cpp
#include <QApplication>
#include <QLabel>
#include <QPointer>
#include <QScrollArea>
#include <QStackedWidget>
#include <QVBoxLayout>
#include <QWidget>
#include <stdexcept>
#include <type_traits>
class BaseWidget : public QWidget {
Q_OBJECT
public:
explicit BaseWidget(QWidget* parent = nullptr)
: QWidget(parent), layout_(new QVBoxLayout(this)) {}
QWidget* contentArea() const { return content_.data(); }
protected:
virtual QWidget* createContentArea() { return new QWidget(this); }
void initializeContent() {
if (content_) {
throw std::logic_error("content already initialized");
}
replaceContent(createContentArea());
}
void replaceContent(QWidget* next) {
if (!next || next->parentWidget() != this) {
throw std::invalid_argument("content must be a direct child");
}
if (next == content_) {
return;
}
if (content_) {
QWidget* previous = content_.data();
layout_->removeWidget(previous);
delete previous;
}
content_ = next;
layout_->addWidget(next);
}
private:
QVBoxLayout* layout_;
QPointer<QWidget> content_;
};
class ScrollWidget final : public BaseWidget {
Q_OBJECT
public:
explicit ScrollWidget(QWidget* parent = nullptr) : BaseWidget(parent) {
initializeContent();
auto* area = scrollArea();
if (!area) {
throw std::logic_error("scroll factory returned a wrong type");
}
area->setWidgetResizable(true);
area->setWidget(new QLabel("Scroll content"));
}
QScrollArea* scrollArea() const {
return qobject_cast<QScrollArea*>(contentArea());
}
protected:
QWidget* createContentArea() override { return new QScrollArea(this); }
};
class StackedWidget final : public BaseWidget {
Q_OBJECT
public:
explicit StackedWidget(QWidget* parent = nullptr) : BaseWidget(parent) {
initializeContent();
auto* area = stackedArea();
if (!area) {
throw std::logic_error("stack factory returned a wrong type");
}
area->addWidget(new QLabel("Page one"));
area->addWidget(new QLabel("Page two"));
area->setCurrentIndex(1);
}
QStackedWidget* stackedArea() const {
return qobject_cast<QStackedWidget*>(contentArea());
}
protected:
QWidget* createContentArea() override { return new QStackedWidget(this); }
};
class FlexibleWidget final : public BaseWidget {
public:
using BaseWidget::BaseWidget;
void useScrollArea() { replaceContent(new QScrollArea(this)); }
void useStackedArea() { replaceContent(new QStackedWidget(this)); }
};
template <typename ContentType = QWidget>
class BaseWidgetTemplate : public QWidget {
static_assert(std::is_base_of_v<QWidget, ContentType>,
"ContentType must derive from QWidget");
public:
explicit BaseWidgetTemplate(QWidget* parent = nullptr)
: QWidget(parent), content_(new ContentType(this)) {
auto* layout = new QVBoxLayout(this);
layout->addWidget(content_);
}
ContentType* contentArea() const { return content_; }
private:
ContentType* content_;
};
using TemplateScrollWidget = BaseWidgetTemplate<QScrollArea>;
using TemplateStackedWidget = BaseWidgetTemplate<QStackedWidget>;
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QWidget window;
auto* layout = new QVBoxLayout(&window);
layout->addWidget(new ScrollWidget(&window));
layout->addWidget(new StackedWidget(&window));
auto* typed = new TemplateScrollWidget(&window);
typed->contentArea()->setWidgetResizable(true);
typed->contentArea()->setWidget(new QLabel("Template scroll content"));
layout->addWidget(typed);
window.setWindowTitle("Typed content areas");
window.resize(420, 420);
window.show();
return app.exec();
}
#include "main.moc"
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(typed_content_area 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)
add_executable(virtual-dispatch virtual-dispatch.cpp)
set_target_properties(virtual-dispatch PROPERTIES AUTOMOC OFF)

把三个文件放在同一目录构建。virtual-dispatch 打印 base derived;widget-example 显示滚动内容、第二个堆叠页面和模板创建的滚动内容。QScrollArea::setWidget() 接管传入的内容控件,QStackedWidget::addWidget() 把页面交给堆叠窗口管理,所以这里无需另外删除这些标签。QScrollArea 内容所有权、QStackedWidget 页面所有权

更换内容时,旧指针也必须退出使用

跳转到“更换内容时,旧指针也必须退出使用”

FlexibleWidget 演示运行时切换所需的接口。依次调用 useScrollArea()、useStackedArea() 时,旧控件会从布局移除并销毁,新的内容区成为布局中的唯一控件。任何先前保存的普通 QWidget* 或 QScrollArea* 都不能再使用;指向旧对象的 QPointer 会自动清空。QPointer

本例采用同步删除,适合构造过程或确定旧控件当前没有执行事件处理的调用位置。如果切换发生在旧控件自己的事件处理栈上,应设计延后销毁流程,使用 deleteLater() 并处理旧控件隐藏、布局移除和稍后释放的阶段,不能机械替换一行删除语句就认为所有生命周期问题都解决了。

同样,不要在派生类再声明一个同名的 QScrollArea *contentArea,希望它“覆盖”基类成员;那会得到两份不同成员。static_cast 向下转换也不会验证实际类型,只有已建立并维护类型约束时才可使用。

模板适合固定类型,但有 moc 边界

跳转到“模板适合固定类型,但有 moc 边界”
设计类型信息放在哪里适用情况
QWidget* 加检查后的专用访问器对象的实际类型与类的约束基类需要统一布局,不同派生类使用不同内容控件
派生类构造体调用虚工厂当前正在构造的最终派生类共享安装流程,需要集中创建策略;不能在基类构造体提前调用
BaseWidgetTemplate<ContentType>模板参数内容类型编译时固定,需要直接调用专用接口
明确的运行时替换接口安装函数和调用方生命周期约定一个对象确实需要切换不同类型的内容区

BaseWidgetTemplate 没有 Q_OBJECT,可以使用 QWidget 已有的功能;moc 不支持在类模板里使用 Q_OBJECT,因此模板本身不能按这个方式声明自己的 Qt 信号、属性和元对象功能。需要这些功能时,可以抽出非模板 QObject 基类或非模板组合层,再将类型相关部分交给模板。moc 的模板限制

模板还要求 ContentType 能用 QWidget* 父参数构造;is_base_of 只检查继承关系,不保证构造函数可用。单纯保持 QWidget* 也不会额外“改善 Qt 事件系统集成”,它主要提供一个统一静态接口。实际事件能力来自对象本身的类和 Qt 对象关系。

继续阅读:构造顺序与父对象、继承与组合。