跳转到内容
新建笔记

Qt QWidget 与 QFrame:窗口、容器和框线

QWidget 提供 Qt Widgets 界面的基础能力;QFrame 在此基础上增加标准框线的形状、阴影和线宽控制。需要普通容器时可以用 QWidget,需要框线或分隔线时可以用 QFrame。两者都能承载布局,也都可能成为顶级窗口。

本文 C++ 示例的 --self-test 使用 assert,应在 Debug 构建或未定义 NDEBUG 的情况下运行;Release 构建若禁用了断言,退出成功不代表这些检查已经执行。

QFrame 继承 QWidget,因此保留事件、焦点、子控件、布局和样式表能力。QWidget 的“基础控件”范围是 Qt Widgets,不能扩大为所有 Qt 用户界面对象:例如 QQuickItem 和 QWindow 属于不同的类型体系。

原笔记比较项QWidgetQFrame
基础能力与主要用途通用 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 前先确定希望改变的是框内留白、控件之间的间距,还是框线本身;详见 布局参数与页面切换。