QML 让开发者能构建出色的用户界面,且不破坏架构。
其中,Anchor Layout 是最爱的功能。相比 Widget 的传统布局(如 QVBoxLayout),锚点布局提供了更大的灵活性:能直观地将元素相对于另一个元素定位(如右侧、上方),或设置等宽等。这种直观性在传统 Widget 中较难实现。
因此,我们可以为 QtWidgets 创建一个 AnchorLayout 类。
一、什么是锚布局?
Qt 的布局类 QVBoxLayout 、 QHBoxLayout 、 QGridLayout 和 QFormLayout 使用某种逻辑在容器窗口的子窗口之间分配可用空间。
另一方面,锚点布局将容器及其内容视为 UI 元素,每个元素都有不可见的锚线,如下所示:

然后,我们设置 UI 元素,使一个 UI 元素的锚线“锚定”到另一个 UI 元素的锚线上。当然,只要这两个 UI 元素是兄弟元素或共享容器包含关系,这种方法就有效。
除了能够将线段“锚定”在一起之外,我们还可以通过指定边距 (margin) 在它们之间建立空间。当我们将一个项目锚定在另一个项目上时,我们还可以指定边距。这样我们就可以在 UI 元素之间引入空间。

例如:我们可以说,将蓝色 UI 元素的左边缘锚定到红色 UI 元素的右边缘。且两者之间有 10 像素的间隙。

我们对锚布局的思考方式与我们对 QLayout 的思考方式不同。
当我们思考“锚点布局”时,我们的想法很像我们在桌子上摆放物品时的想法。将一个物品放在另一个物品的右侧,另一个物品的顶部,或者将这个物品放在桌子的最右侧,将那个物品放在最左侧,依此类推。
锚点布局比传统的 QLayout 更容易理解。这并不是说传统布局不好。如果能提供混合搭配和使用两种布局的选项,那就更好了。
通过 AnchorLayout 类来实现这个功能。如果你只想下载并使用代码,可以从这里 git-pull 一份代码: https://github.com/pnudupa/anchorlayout
二、示例
2.1 简单示例
假设我想将 QFrame 放置在 QWidget 中,以便它占据整个容器,并留出一些边距。
int main(int argc, char **argv) {
QApplication a(argc, argv);
QWidget container;
QFrame *frame = new QFrame(&container);
frame->setFrameStyle(QFrame::Box);
/************************************
QML实现...
QWidget {
id: container
QFrame {
anchors.fill: container
anchors.margins: 50
}
}
************************************/
AnchorLayout::get(frame)
->fill(AnchorLayout::get(&container))
->setMargins(50);
container.resize(400, 400); container.show();
return a.exec();
}
执行时,我们将得到这样的输出。

当然,这也可以通过 QVBoxLayout 或 QHBoxLayout 来实现。我们不会与现有的布局竞争,只是想将 QML 中 Anchor Layout 的强大功能引入 QtWidgets 世界。
2.2 右下象限中的 Frame
假设我们想要在容器的右下象限中显示窗口,并在周围留出 20 像素的边距,就像这样。

当然,使用 QGridLayout 可以实现这一点,但如何确保正确的位置是一个问题。为了达到这个效果,我们必须有效地调整 Frame 的 sizeHint() 和 sizePolicy() 。
使用 AnchorLayout 类,一切就这么简单。注意我们是如何借用 QML 中的锚线概念的。
int main(int argc, char **argv) {
QApplication a(argc, argv);
QWidget container;
QFrame *frame = new QFrame(&container);
frame->setFrameStyle(QFrame::Box);
/************************************
QML实现...
QWidget {
id: container
QFrame {
anchors.left: container.horizontalCenter
anchors.leftMargin: 20
anchors.top: container.verticalCenter
anchors.topMargin: 20
anchors.right: container.right
anchors.rightMargin: 20
anchors.bottom: container.bottom
anchors.bottomMargin: 20
}
}
************************************/
AnchorLayout *containerLayout = AnchorLayout::get(&container);
AnchorLayout *frameLayout = AnchorLayout::get(frame);
frameLayout->left()
->anchorTo(containerLayout->horizontalCenter())
->setMargin(20);
frameLayout->top()
->anchorTo(containerLayout->verticalCenter())
->setMargin(20);
frameLayout->right()
->anchorTo(containerLayout->right())
->setMargin(20);
frameLayout->bottom()
->anchorTo(containerLayout->bottom())
->setMargin(20);
container.resize(400, 400);
container.show();
return a.exec();
}
每个 AnchorLayout 实例都有 left() 、 right() 、 top() 、 bottom() 、 horizontalCenter() 和 verticalCenter()、 AnchorLine 。这些线可以与其他布局的 AnchorLine 重合。
将窗口放置在容器中,只需将其锚点布局的锚线相互锚定即可。当锚线移动时,它们会带动其所代表的窗口的边缘一起移动。这样,通过拉动锚线可以有效地放置和调整窗口的大小。
2.3 将两个 Frame 放在中心,中间用空格隔开
假设想要将两个 Frame 并排放置在容器的中心,这样
- 每个 Frame 的宽度为容器宽度的35%
- 每个 Frame 的高度为容器高度的50%
像这样:

再次强调,这样做的目的是确保在调整容器大小时,容器的宽度和高度比必须保持一致。

以下是我们使用 AnchorLayout 类来实现这一点的方法。
int main(int argc, char **argv) {
QApplication a(argc, argv);
QWidget container;
QLabel *frame1 = new QLabel(&container);
frame1->setFrameStyle(QFrame::Box);
frame1->setText("One");
frame1->setAlignment(Qt::AlignCenter);
QLabel *frame2 = new QLabel(&container);
frame2->setFrameStyle(QFrame::Box);
frame2->setText("Two");
frame2->setAlignment(Qt::AlignCenter);
AnchorLayout *containerLayout = AnchorLayout::get(&container);
AnchorLayout *frame1Layout = AnchorLayout::get(frame1);
frame1Layout->left()
->anchorTo(containerLayout->customLine(Qt::Vertical,0.15))
->setMargin(-5);
frame1Layout->right()
->anchorTo(containerLayout->horizontalCenter())
->setMargin(5);
frame1Layout->top()
->anchorTo(containerLayout->customLine(Qt::Horizontal,0.25));
frame1Layout->bottom()
->anchorTo(containerLayout->customLine(Qt::Horizontal,-0.25));
AnchorLayout *frame2Layout = AnchorLayout::get(frame2);
frame2Layout->right()
->anchorTo(containerLayout->customLine(Qt::Vertical,-0.15))
->setMargin(-5);
frame2Layout->left()
->anchorTo(containerLayout->horizontalCenter())
->setMargin(5);
frame2Layout->top()
->anchorTo(containerLayout->customLine(Qt::Horizontal,0.25));
frame2Layout->bottom()
->anchorTo(containerLayout->customLine(Qt::Horizontal,-0.25));
container.resize(600, 400);
container.show();
return a.exec();
}
注意 customLine() 方法的使用。除了 left() 、 top() 、 right() 、 bottom() 、 horizontalCenter() 和 verticalCenter() 锚线之外,我们还可以创建自定义锚线。锚线可以是水平线,也可以是垂直线。它们显示在距布局边缘特定距离的位置。例如
- 距离 0.25 的 Qt::HorizontalLine 是一条距离布局左侧 25% 宽度的水平线。
- 距离 -0.25 的 Qt::HorizontalLine 是一条距离布局右侧 25% 宽度的水平线。
与所有 AnchorLine 一样,只要方向匹配,自定义 AnchorLines 也可以用来锚定另一个锚线。另外请注意,我们能够轻松地使用边距在 Frame 之间添加空间。
2.4 一个Frame粘在顶部,另一个粘在底部
假设想要实现这一点。

请注意,“一”和“二”比之前更高了。它们实际上相当于容器高度的 75%。第一个 Frame 与顶部对齐,而第二个 Frame 与底部对齐。还要注意边距和间距。实现此效果的代码与我们之前编写的代码几乎相同,需要修改的部分如下:
int main(int argc, char **argv) {
...
frame1Layout->top()
->anchorTo(containerLayout->top())->setMargin(10);
frame1Layout->bottom()
->anchorTo(containerLayout->customLine(Qt::Horizontal, -0.25))
->setMargin(-10);
...
frame2Layout->top()
->anchorTo(containerLayout->customLine(Qt::Horizontal, 0.25))
->setMargin(-10);
frame2Layout->bottom()->anchorTo(containerLayout->bottom())->setMargin(10);
...
}
虽然用传统布局实现这样的功能是可行的,但难度相当大。你必须考虑各种拉伸系数、对齐方式、尺寸提示和策略。而锚点布局则让这一切变得非常简单。
2.5 典型的应用程序用户界面
典型的应用程序 UI 由一个中央文档区域及其周围的其他窗口组成。

尽管 QMainWindow 确实为我们提供了这种构造,但让我们看看如何使用 AnchorsLayout 自己构建这样的布局。
首先,我们来明确一下每个区域的比例、比例和固定大小
- 菜单栏区域始终位于顶部,占据窗口的整个宽度。它的高度固定为 30 像素。
- 状态栏区域始终位于底部,占据窗口的整个宽度。它的高度固定为 30 像素。
- 工具箱和配置框区域位于左侧和右侧;占据窗口宽度的大约 20%。
- 剩余空间由文档区域占用。
int main(int argc, char **argv) {
QApplication a(argc, argv);
auto createFrame = [](const QString &text, QWidget *parent) {
QLabel *label = new QLabel(parent);
label->setFrameStyle(QFrame::Box);
label->setText(text);
label->setAlignment(Qt::AlignCenter);
return label;
};
QWidget container;
QWidget *documentArea = createFrame("document-area", &container);
QWidget *menuBarArea = createFrame("menu-bar-area", &container);
QWidget *toolBoxArea = createFrame("tool-box-area", &container);
QWidget *configBoxArea = createFrame("config-box-area", &container);
QWidget *statusBarArea = createFrame("status-bar-area", &container);
AnchorLayout *containerLayout = AnchorLayout::get(&container);
AnchorLayout *menuBarLayout = AnchorLayout::get(menuBarArea);
menuBarLayout->left()
->anchorTo(containerLayout->left());
menuBarLayout->right()
->anchorTo(containerLayout->right());
menuBarLayout->top()
->anchorTo(containerLayout->top());
menuBarArea->setFixedHeight(30);
AnchorLayout *statusBarLayout = AnchorLayout::get(statusBarArea);
statusBarLayout->left()
->anchorTo(containerLayout->left());
statusBarLayout->right()
->anchorTo(containerLayout->right());
statusBarLayout->bottom()
->anchorTo(containerLayout->bottom());
statusBarArea->setFixedHeight(30);
AnchorLayout *configBoxLayout = AnchorLayout::get(configBoxArea);
configBoxLayout->right()
->anchorTo(containerLayout->right());
configBoxLayout->top()
->anchorTo(menuBarLayout->bottom())
->setMargin(2);
configBoxLayout->bottom()
->anchorTo(statusBarLayout->top())
->setMargin(2);
configBoxLayout->left()
->anchorTo(containerLayout->customLine(Qt::Vertical,-0.2));
AnchorLayout *toolBoxLayout = AnchorLayout::get(toolBoxArea);
toolBoxLayout->left()
->anchorTo(containerLayout->left());
toolBoxLayout->top()
->anchorTo(menuBarLayout->bottom())
->setMargin(2);
toolBoxLayout->bottom()
->anchorTo(statusBarLayout->top())
->setMargin(2);
toolBoxLayout->right()
->anchorTo(containerLayout->customLine(Qt::Vertical,0.2));
AnchorLayout *documentAreaLayout = AnchorLayout::get(documentArea);
documentAreaLayout->left()
->anchorTo(toolBoxLayout->right())
->setMargin(2);
documentAreaLayout->top()
->anchorTo(toolBoxLayout->top());
documentAreaLayout->right()
->anchorTo(configBoxLayout->left())
->setMargin(2);
documentAreaLayout->bottom()
->anchorTo(configBoxLayout->bottom());
container.resize(600, 400);
container.show();
return a.exec();
}
注意,我们能够轻松地将区域彼此相对放置。另请注意,我们能够使用 setFixedHeight() 指定一些硬编码的大小。一旦我们确定了区域的放置位置,现在就可以使用传统的 QLayout 或 AnchorLayout 在其中放置窗口了。
三、实现原理:AnchorLayout 不继承 QLayout
AnchorLayout 类不继承 QLayout 。它利用事件过滤器查找调整大小事件并重新计算锚线的位置。然后,每条锚线会将其位置级联到与其连接的其他锚线。整个 Widget UI 会以递归方式进行放置和大小调整。这也意味着我们可以在现有布局之上使用 AnchorLayout 。当您想将一个 Widget 堆叠在另一个 Widget 之上时,这尤其有用,因为 Widget 可能已经有一个布局,但仍希望控制其位置。
四、堆叠示例:以 Qt 的 QMainWindow 为例

现在,假设我们想将一个日历窗口悬停在主窗口的顶部,使其恰好位于状态栏区域的上方,但水平居中。像这样:

现有的主窗口布局不提供任何功能来容纳此类临时窗口。但使用 AnchorLayout ,我们可以实现如下功能。
MainWindow::MainWindow(const CustomSizeHintMap &customSizeHints,
QWidget *parent, Qt::WindowFlags flags)
: QMainWindow(parent, flags) {
// ......
AnchorLayout *mainWindowLayout = AnchorLayout::get(this);
AnchorLayout *statusBarLayout = AnchorLayout::get(this->statusBar());
QCalendarWidget *calendarWidget = new QCalendarWidget(this);
calendarWidget->setGridVisible(true);
AnchorLayout *calendarLayout = AnchorLayout::get(calendarWidget);
calendarLayout->bottom()
->anchorTo(statusBarLayout->top())
->setMargin(1);
calendarLayout->horizontalCenter()
->anchorTo(mainWindowLayout->horizontalCenter());
calendarWidget->setFixedSize(350, 200);
}
如果我们希望日历窗口稍微与状态栏区域重叠,使得日历的底部边缘一直延伸到状态栏高度的 50%,该怎么办?

我们只需要对代码进行很小的修改。
calendarLayout->bottom()->anchorTo(statusBarLayout->verticalCenter());
在这个例子中,我们显示的是一个日历窗口。但在实际应用中,我们完全有可能在每次选择要打开的文件时,都希望在状态栏附近显示一个临时窗口,就像这样显示加载进度。我们希望这个临时窗口在主窗口的边界内可见,但位置要合适。
1137

被折叠的 条评论
为什么被折叠?



