QWidget 提供 Qt Widgets 界面的基础能力;QFrame 在此基础上增加标准框线的形状、阴影和线宽控制。需要普通容器时可以用 QWidget,需要框线或分隔线时可以用 QFrame。两者都能承载布局,也都可能成为顶级窗口。
本文 C++ 示例的 --self-test 使用 assert,应在 Debug 构建或未定义 NDEBUG 的情况下运行;Release 构建若禁用了断言,退出成功不代表这些检查已经执行。
继承关系与适用范围
跳转到“继承关系与适用范围”QFrame 继承 QWidget,因此保留事件、焦点、子控件、布局和样式表能力。QWidget 的“基础控件”范围是 Qt Widgets,不能扩大为所有 Qt 用户界面对象:例如 QQuickItem 和 QWindow 属于不同的类型体系。
| 原笔记比较项 | QWidget | QFrame |
|---|---|---|
| 基础能力与主要用途 | 通用 Widgets 窗口、容器或自定义控件的基础类 | 继承 QWidget,额外提供标准框线接口 |
| 边框 | 可用样式表或自定义绘制;窗口装饰还受系统管理 | 可用 setFrameShape()、setFrameShadow()、线宽接口 |
| 背景 | 可用样式表;使用调色板填充时还要考虑 autoFillBackground 等绘制设置 | 继承相同机制;“是 QFrame”本身不保证填充某种背景 |
| 布局与子控件 | 支持 | 支持;框线占用的区域与内容区域需要区分 |
| 滚动 | 本身没有自动滚动条,可组合 QScrollArea | 本身没有自动滚动条;QAbstractScrollArea 才在其上扩展滚动区域 |
| 标题栏 | 是否为窗口以及窗口标志决定系统装饰 | 同样可以是窗口,不存在“一定没有标题栏”的限制 |
| 信号槽、样式表、事件过滤、焦点 | 继承 QObject 能力,并提供 QWidget 相关接口 | 继承同样能力 |
QFrame::Box 是框形状,QFrame::Raised 是视觉阴影类型;“阴影”不是系统窗口投影。实际线宽、颜色和外观还受到当前样式影响,不应把某台电脑的截图当成跨平台像素规范。参见 QFrame 与 QWidget。
同时运行普通容器和带框容器
跳转到“同时运行普通容器和带框容器”下面将原来的两个独立入口合为一个完整程序。两个容器都没有 QWidget 父对象,因此都作为窗口显示;向布局添加按钮后,按钮会成为对应容器的子控件。new QVBoxLayout(&container) 已安装顶层布局,无须再对同一对象调用 setLayout()。
将代码保存为 main.cpp,用 Qt 6 Widgets、C++17 构建。传入 --self-test 可执行关系与配置检查后退出。
#include <QApplication>#include <QFrame>#include <QPushButton>#include <QVBoxLayout>#include <QWidget>#include <cassert>
static void addButtons(QWidget &container) { auto *layout = new QVBoxLayout(&container); layout->addWidget(new QPushButton("Button 1")); layout->addWidget(new QPushButton("Button 2"));}
int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget plain; plain.setWindowTitle("QWidget container"); QFrame framed; framed.setWindowTitle("QFrame container"); framed.setFrameShape(QFrame::Box); framed.setFrameShadow(QFrame::Raised); framed.setLineWidth(2); addButtons(plain); addButtons(framed); plain.resize(260, 160); framed.resize(260, 160);
if (app.arguments().contains("--self-test")) { assert(plain.isWindow() && framed.isWindow()); assert(framed.frameShape() == QFrame::Box); assert(framed.frameShadow() == QFrame::Raised); assert(framed.lineWidth() == 2); for (QWidget *container : {&plain, static_cast<QWidget *>(&framed)}) { assert(container->layout()->count() == 2); for (int i = 0; i < 2; ++i) assert(container->layout()->itemAt(i)->widget()->parentWidget() == container); } QWidget child(&framed); assert(!child.isWindow()); return 0; } plain.show(); framed.show(); return app.exec();}无父对象通常意味着独立窗口;指定 Qt::Window 等窗口标志也能改变带父对象控件的窗口行为。因此判断时使用 isWindow(),不要只用“指针是否有 parent”替代所有窗口规则。
组合时常见的边界
跳转到“组合时常见的边界”容器有框线不代表已经有布局,也不代表已经有滚动条。需要长内容时,再选择 QScrollArea;需要可拖动分隔条时选择 QSplitter;需要切换整页时选择 QStackedWidget。这些类各自管理不同的交互,不应仅靠加边框来替代。
布局的边距与 QFrame 的框宽也不是同一项设置。调整 contentsMargins 前先确定希望改变的是框内留白、控件之间的间距,还是框线本身;详见 布局参数与页面切换。