Qt Widgets 之 QDockWidget(停靠窗口)

目录

什么是停靠窗口

如何添加停靠窗口

QDockWidget::setWidget()

QMainWindow::addDockWidget()

设置停靠选项 (Options)

AnimatedDocks

AllowNestedDocks

AllowTabbedDocks

ForceTabbedDocks

VerticalTabs

GroupedDragging

设置窗口特性 (Features)

设置可停靠区域

设置角落区域

分割窗口

标签页显示 (tab)

设置窗口标题栏

保存停靠窗口状态

常见问题

拓展阅读

Application Example

Dock Widgets Example

MDI Example

SDI Example 


什么是停靠窗口

在 Qt 中,停靠窗口 (dock window) 都是 QDockWidget 的实例,可以停靠在 QMainWindow 的中央部件 (central widget) 的上下左右四个区域,停靠的 QDockWidget 没有框架,有一个较小的标题栏;也可浮动出来作为独立窗口。

QDockWidget API 允许程序员控制停靠窗口移动、浮动和关闭的能力,以及它们可以放置的区域等。

相关文档:QDockWidget ClassQMainWindow 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 areaQDockWidget *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 cornerQt::DockWidgetArea area)

文章最开始的 QMainWindow 的示意图中,有四个虚线标出的停靠区域重叠的角落,使用这个函数可以指定哪个停靠区域可以占据该区域。

Qt::Corner 是个枚举类型,定义:

    enum Corner {
        TopLeftCorner = 0x00000,
        TopRightCorner = 0x00001,
        BottomLeftCorner = 0x00002,
        BottomRightCorner = 0x00003
    };

分割窗口

void QMainWindow::splitDockWidget(QDockWidget *firstQDockWidget *secondQt::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 *firstQDockWidget *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 类实现多文档界面。

详见:MDI Example | Qt Widgets

菜单示例演示了如何在主窗口应用程序中使用菜单。

菜单可以是菜单栏中的下拉菜单,也可以是独立的上下文菜单。 当用户单击相应的 item 或按下指定的快捷键时,菜单栏会显示下拉菜单。 上下文菜单通常由一些特殊的键盘键或右键单击来调用。

详见:Menus Example | Qt Widgets

SDI Example 

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

详见:SDI Example | Qt Widgets


版权声明:本文为SummerPa原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。