目录
什么是停靠窗口
在 Qt 中,停靠窗口 (dock window) 都是 QDockWidget 的实例,可以停靠在 QMainWindow 的中央部件 (central widget) 的上下左右四个区域,停靠的 QDockWidget 没有框架,有一个较小的标题栏;也可浮动出来作为独立窗口。
QDockWidget API 允许程序员控制停靠窗口移动、浮动和关闭的能力,以及它们可以放置的区域等。

相关文档:QDockWidget Class,QMainWindow Class
Qt 提供了很多相关示例,其中 Main Window 最为全面详细,我们今天就就着这个示例研究一下 QDockWidget 的用法及特性。

如何添加停靠窗口
先看一段代码关于如何创建停靠窗口,并将其添加到主窗口:
QDockWidget *dockWidget = new QDockWidget(tr("Dock Widget"), this);
dockWidget->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
dockWidget->setWidget(dockWidgetContents);
addDockWidget(Qt::LeftDockWidgetArea, dockWidget);QDockWidget::setWidget()
voidQDockWidget::setWidget(QWidget *widget)
QDockWidget 充当该 widget 的包装器。注意:调用这个函数之前必须为 widget 添加布局,否则它将不可见,而且如果此时 QDockWidget 已经可见了,那我们必须显式地 widget->show() 。
QMainWindow::addDockWidget()
voidQMainWindow::addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockwidget)
使用这个函数可以为 QMainWindow 在指定区域(上/下/左/右)添加一个停靠窗口。
设置停靠选项 (Options)

void QMainWindow::setDockOptions(QMainWindow::DockOptions options)
该函数可设置停靠窗口的一些行为。QMainWindow::DockOptions 是枚举类型,定义为:
enum DockOption {
AnimatedDocks = 0x01,
AllowNestedDocks = 0x02,
AllowTabbedDocks = 0x04,
ForceTabbedDocks = 0x08, // implies AllowTabbedDocks, !AllowNestedDocks
VerticalTabs = 0x10, // implies AllowTabbedDocks
GroupedDragging = 0x20 // implies AllowTabbedDocks
};
Q_ENUM(DockOption)
Q_DECLARE_FLAGS(DockOptions, DockOption)
Q_FLAG(DockOptions)默认值: AnimatedDocks | AllowTabbedDocks.
注意:这些选项仅控制如何在 QMainWindow 中放置停靠窗口,它们不会重新排列已有的停靠窗口以符合指定的选项。因此,应在停靠窗口添加到主窗口之前设置它们。 AnimatedDocks 和 VerticalTabs 选项除外,它们可以随时设置。
AnimatedDocks
默认设置该选项,对停靠窗口和工具栏的操作进行动画处理。当在主窗口上拖动停靠窗口或工具栏时,主窗口会调整其内容以指示窗口在放置时停靠的位置,这一过程以平滑的动画移动。若清除该选项则无动画效果。
同 animated 属性,查询及设置函数:
bool isAnimated() const
void setAnimated(bool enabled)
注意:如果主窗口有的 widgets 在调整大小或重新绘制自身时很缓慢,则该属性可能会被清除。
AllowNestedDocks
默认不设置该选项,每个停靠区域只能包含单行停靠窗口(即垂直方向的停靠区域只有一列,水平区域的停靠区域只有一行);若设置该属性,则停靠区域可以在任一方向拆分以包含更多停靠窗口(即垂直方向的停靠区域可以有多列,水平方向的停靠区域可以有多行)。
同 dockNestingEnabled 属性,查询及设置函数:
bool isDockNestingEnabled() const
void setDockNestingEnabled(bool enabled)
注意:仅在包含大量停靠窗口的应用程序中才需要设置该属性,它为用户组织主窗口提供了更多的自由。 然而由于有更多方法可以将停靠窗口放置在停靠区域中,也更复杂,且不太直观。
另外,很多人将这个属性称为“是否允许嵌套的一个属性”,我觉得这里"nested"翻译成嵌套并不合适,这个属性并没有允许停靠窗口嵌套布局,我觉得“巢状布局”更合适。
AllowTabbedDocks
若设置该选项,则允许用户以选项卡 (tab) 方式组织多个停靠窗口,即若将一个停靠窗口 dock1 拖动到另一个 dock2 的区域,会出现一个选项卡栏(QTabBar),每个停靠窗口都在一个单独的标签页里,dock1 为活动窗口。
ForceTabbedDocks
若设置该选项,则 AllowNestedDocks 无效,只能以选项卡 (tab) 方式组织多个停靠窗口。 换句话说,停靠窗口不能在停靠区域中彼此相邻放置。
VerticalTabs
若设置该选项,则暗示 AllowTabbedDocks,允许用户以选项卡 (tab) 方式组织多个停靠窗口,且垂直停靠区域的选项卡在垂直方向显示;若未设置此选项,则所有停靠区域都会在底部显示其选项卡。
另外可通过 void QMainWindow::setTabPosition() 为指定停靠区域设置选项卡位置,但VerticalTabs 选项优先。
GroupedDragging
若设置该选项,则暗示 AllowTabbedDocks,允许用户以选项卡 (tab) 方式组织多个停靠窗口,且在拖动停靠窗口时,将拖动所有带有标签的选项卡 。 但如果某些 QDockWidget 允许的区域有限制,则效果不佳。 (这个枚举值是在 Qt 5.6 中添加的。)
void MainWindow::setDockOptions()
{
DockOptions opts = 0;
QList<QAction*> actions = mainWindowMenu->actions();
if (actions.at(0)->isChecked())
opts |= AnimatedDocks;
if (actions.at(1)->isChecked())
opts |= AllowNestedDocks;
if (actions.at(2)->isChecked())
opts |= AllowTabbedDocks;
if (actions.at(3)->isChecked())
opts |= ForceTabbedDocks;
if (actions.at(4)->isChecked())
opts |= VerticalTabs;
if (actions.at(5)->isChecked())
opts |= GroupedDragging;
QMainWindow::setDockOptions(opts);
}设置窗口特性 (Features)

voidQDockWidget::setFeatures(QDockWidget::DockWidgetFeatures features)
该函数可以设置停靠窗口的一些特性,QDockWidget::DockWidgetFeatures 枚举定义为:
enum DockWidgetFeature {
DockWidgetClosable = 0x01,
DockWidgetMovable = 0x02,
DockWidgetFloatable = 0x04,
DockWidgetVerticalTitleBar = 0x08,
DockWidgetFeatureMask = 0x0f,
AllDockWidgetFeatures = DockWidgetClosable|DockWidgetMovable|DockWidgetFloatable, // ### Qt 6: remove
NoDockWidgetFeatures = 0x00,
Reserved = 0xff
};注意:QDockWidget::AllDockWidgetFeatures 已弃用,可指定为:QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable。这也是默认的特性:可关闭,可移动,可浮动。
- QDockWidget::DockWidgetVerticalTitleBar 在 QDockWidget 左侧显示一个垂直标题栏,这可用于增加垂直空间。
void ColorSwatch::changeClosable(bool on)
{ setFeatures(on ? features() | DockWidgetClosable : features() & ~DockWidgetClosable); }
void ColorSwatch::changeMovable(bool on)
{ setFeatures(on ? features() | DockWidgetMovable : features() & ~DockWidgetMovable); }
void ColorSwatch::changeFloatable(bool on)
{ setFeatures(on ? features() | DockWidgetFloatable : features() & ~DockWidgetFloatable); }设置可停靠区域
voidQDockWidget::setAllowedAreas(Qt::DockWidgetArea areas)
这个函数可以设置停靠窗口可以停靠的区域,Qt::DockWidgetArea 是一个枚举,给出了四个停靠区域:左边、右边、顶部和底部,默认是 Qt::AllDockWidgetAreas。
enum DockWidgetArea {
LeftDockWidgetArea = 0x1,
RightDockWidgetArea = 0x2,
TopDockWidgetArea = 0x4,
BottomDockWidgetArea = 0x8,
DockWidgetArea_Mask = 0xf,
AllDockWidgetAreas = DockWidgetArea_Mask,
NoDockWidgetArea = 0
};void ColorSwatch::allow(Qt::DockWidgetArea area, bool a)
{
Qt::DockWidgetAreas areas = allowedAreas();
areas = a ? areas | area : areas & ~area;
setAllowedAreas(areas);
}设置角落区域
void QMainWindow::setCorner(Qt::Corner corner, Qt::DockWidgetArea area)
文章最开始的 QMainWindow 的示意图中,有四个虚线标出的停靠区域重叠的角落,使用这个函数可以指定哪个停靠区域可以占据该区域。
Qt::Corner 是个枚举类型,定义:
enum Corner {
TopLeftCorner = 0x00000,
TopRightCorner = 0x00001,
BottomLeftCorner = 0x00002,
BottomRightCorner = 0x00003
};分割窗口
void QMainWindow::splitDockWidget(QDockWidget *first, QDockWidget *second, Qt::Orientation orientation)
将 first 停靠窗口所在的空间分成两部分,first 移动到第一部分,second 移动到第二部分。
orientation 指定空间的划分方式:Qt::Horizontal 将 second 停靠部件放置在 first 的右侧; Qt::Vertical 将 second 停靠部件放置在 first 下方。
注意:
- 如果 first 当前位于选项卡式停靠区域中,则 second 将作为新的标签页添加,因为单个标签页只能包含一个停靠窗口。
- Qt::LayoutDirection 会影响划分区域的两个部分中停靠窗口的顺序,当启用从右到左 (Qt::RightToLeft) 的布局方向时,停靠窗口的放置将被颠倒。
void ColorSwatch::splitInto(QAction *action)
{
ColorSwatch *target = findByName(mainWindow, action->text());
if (!target)
return;
const Qt::Orientation o = action->parent() == splitHMenu
? Qt::Horizontal : Qt::Vertical;
mainWindow->splitDockWidget(target, this, o);
}标签页显示 (tab)
void QMainWindow::tabifyDockWidget(QDockWidget *first, QDockWidget *second)
在主窗口中创建一个选项卡式停靠区域,将 first 和 second停靠窗口置于不同的标签页中,second 为当前活动窗口。
void ColorSwatch::tabInto(QAction *action)
{
if (ColorSwatch *target = findByName(mainWindow, action->text()))
mainWindow->tabifyDockWidget(target, this);
}
设置窗口标题栏
void QDockWidget::setTitleBarWidget(QWidget *widget)
将 widget 设置为停靠窗口的标题栏。如果widget 为0,则之前在停靠窗口上设置的自定义标题栏将被删除,但标题栏不会被删除,而将使用默认标题栏。
BlueTitleBar *titlebar = new BlueTitleBar(swatch);
swatch->setTitleBarWidget(titlebar);如果设置了标题栏,则 QDockWidget 在浮动时不会使用原生窗口装饰。
以下是实现自定义标题栏的一些技巧:
- 标题栏未明确处理的鼠标事件必须通过调用 QMouseEvent::ignore() 来忽略,从而将这些事件传递给父对象 QDockWidget 以通常的方式处理,比如拖动标题栏时移动、双击时停靠/取消停靠等。
- 当在 QDockWidget 上设置 DockWidgetVerticalTitleBar 时,标题栏会相应地重新定位。在 resizeEvent() 中,标题栏应该检查它应该采用的方向:
QDockWidget *dockWidget = qobject_cast<QDockWidget*>(parentWidget());
if (dockWidget->features() & QDockWidget::DockWidgetVerticalTitleBar) {
// I need to be vertical
} else {
// I need to be horizontal
}- 标题栏必须有一个有效的 QWidget::sizeHint() 和 QWidget::minimumSizeHint()。这些功能应考虑标题栏的当前方向。
QSize sizeHint() const override { return minimumSizeHint(); }
QSize BlueTitleBar::minimumSizeHint() const
{
QDockWidget *dw = qobject_cast<QDockWidget*>(parentWidget());
Q_ASSERT(dw != 0);
QSize result(leftPm.width() + rightPm.width(), centerPm.height());
if (dw->features() & QDockWidget::DockWidgetVerticalTitleBar)
result.transpose();
return result;
}- 虽然无法从停靠窗口中删除标题栏,但是通过将默认构造的 QWidget 设置为标题栏,可以实现类似的效果。
使用如上所示的 qobject_cast(),标题栏可以访问到其父对象 QDockWidget。因此,它可以执行停靠窗口的停靠、隐藏等操作。
保存停靠窗口状态
- QByteArray QMainWindow::saveState(int version = 0) const
- bool QMainWindow::restoreState(const QByteArray &state, int version = 0)
保存工具栏和停靠窗口的位置,包括使用 setCorner() 对角落区域的设置,以使下一次运行程序时能够恢复它们的状态。
void MainWindow::saveLayout()
{
...
QByteArray geo_data = saveGeometry();
QByteArray layout_data = saveState();
...
}
void MainWindow::loadLayout()
{
...
if (ok)
ok = restoreGeometry(geo_data);
if (ok)
ok = restoreState(layout_data);
...
}常见问题
- QDockWidget 是否必须在 QMainWindow 中使用?是否可在其他布局里添加 ?
可以,但这样它就和一个普通的 widget 没多大不一样,不过多一个浮动、停靠的功能,没有必要,而 QMainWindow 为 QDockWindow 增加了很多功能,QDockWindow 在 QMainWindow 中使用,才能发挥最大的作用。
- 如何约束停靠窗口的大小?
虽然 QDockWidget 继承自 QWidget,也继承了QWidget::setFixedSize(), QWidget::setMinimumSize(), QWidget::setMaximumSize() 等函数,但是QDockWidget 只是一个包装器,会根据是否停靠而变化,不应在 QDockWidget 本身上设置大小约束。自定义 sizeHint、minimumSize、maximumSize、sizePolicy 都应在它的 widget 中实现。QDockWidget 会尊重它们,调整自己的约束以包含框架和标题。
- 实现类似浏览器这种功能时,只需要以标签页(tab)方式组织停靠窗口,并不需要中央窗体,是否可以去除?
Qt 文档中有介绍到:
Note: Creating a main window without a central widget is not supported. You must have a central widget even if it is just a placeholder.
注意:不支持创建没有中央小部件的主窗口。 即使它只是一个占位符,您也必须有一个中央小部件。
但从 Qt 5.2 开始提供了 QWidget *QMainWindow::takeCentralWidget() 函数,可以将中央窗体从主窗口中移除。
QWidget* p = takeCentralWidget();
if(p) delete p;效果如下:

拓展阅读
本地示例源码路径:Qt安装目录\Qt5.12.11\Examples\Qt-5.12.11\widgets\mainwindows

该工程除了示例 Main Window 外,还有其他几个示例:
Application Example
该示例展示了如何实现带有菜单栏、工具栏和状态栏的标准 GUI 应用程序,该示例本身是一个围绕 QPlainTextEdit 构建的简单文本编辑器程序。
详见:Application Example | Qt Widgets

Dock Widgets Example
该示例展示了如何向应用程序添加停靠窗口,它还展示了如何使用 Qt 的富文本引擎。
详见:Dock Widgets Example | Qt Widgets

MDI Example
该示例展示了如何使用 Qt 的 QMdiArea 类实现多文档界面。

Menus Example
菜单示例演示了如何在主窗口应用程序中使用菜单。
菜单可以是菜单栏中的下拉菜单,也可以是独立的上下文菜单。 当用户单击相应的 item 或按下指定的快捷键时,菜单栏会显示下拉菜单。 上下文菜单通常由一些特殊的键盘键或右键单击来调用。

SDI Example
该示例展示了如何创建单文档界面,它使用多个顶级窗口来显示不同文本文件的内容。
