Qt Widgets的锚点布局

仓库地址:https://github.com/pnudupa/anchorlayout

QML 让开发者能构建出色的用户界面,且不破坏架构。
其中,​​Anchor Layout​​ 是最爱的功能。相比 Widget 的传统布局(如 QVBoxLayout),锚点布局提供了更大的灵活性:能直观地将元素相对于另一个元素定位(如右侧、上方),或设置等宽等。这种直观性在传统 Widget 中较难实现。
因此,我们可以为 QtWidgets 创建一个 AnchorLayout 类。

一、什么是锚布局?

Qt 的布局类 QVBoxLayoutQHBoxLayoutQGridLayoutQFormLayout 使用某种逻辑在容器窗口的子窗口之间分配可用空间。
另一方面,锚点布局将容器及其内容视为 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 像素的边距,就像这样。
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/03a0d031452942398e4254af302453f6.png
在这里插入图片描述
当然,使用 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() 指定一些硬编码的大小。一旦我们确定了区域的放置位置,现在就可以使用传统的 QLayoutAnchorLayout 在其中放置窗口了。

三、实现原理: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()); 

在这个例子中,我们显示的是一个日历窗口。但在实际应用中,我们完全有可能在每次选择要打开的文件时,都希望在状态栏附近显示一个临时窗口,就像这样显示加载进度。我们希望这个临时窗口在主窗口的边界内可见,但位置要合适。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值