Anchor Layout for Widgets

I love QML. It’s a software developer’s ticket to build truly amazing user interfaces while at the same time not compromising on software architecture. Its simply fantastic!

One of the features of QML UI that I love the most is Anchor Layouts. Coming from the widgets world I knew QVBoxLayout, QHBoxLayout, QGridLayout and QFormLayout. These layouts help place and resize contents of a widget in a predefined fashion. They were the perfect set of layouting tools until I discovered Anchor Layouts. Now, whenever I have to go back to writing widgets UI; I feel that lack of Anchor Layouts in Widgets world more than ever. I have gotten used to placing one UI element to the right-of, left-of, above, below of another UI element and so on. I like being able to say that this UI element should be as wide as the other element, but placed on top. You can’t do that with widgets as straight-forwardly as you can do it in QML. So, I decided to build an AnchorLayout class for use with QtWidgets. This blog post is all about that.

For the uninformed: what is an Anchor Layout?

Qt’s layout classes QBoxLayout, QGridLayout and QFormLayout distributes space available in a container widget among its child widgets using some logic. Anchor Layouts on the other hand think of a container and its contents as a UI elements with each of them having invisible anchor lines as follows

We then setup UI elements such that one UI element’s anchor-line is “anchored” to another UI element’s anchor-line. Of-course, this works as long as the UI elements in question are either siblings OR share a container-contained relationship.

In addition to being able to “anchor” lines on each other, we can also establish a space between them, by specifying a margin. When we anchor one item on another, we can in addition to that also say that we need a margin. That way we can introduce space between UI elements.

For example: we could say, anchor the left of blue UI element to the right of a red UI element.

We could further say that while we want the left of the blue UI element to stand to the right of the red UI element, we want a gap of 10 pixels between them.

The way we think anchor-layouts is distinct from the way we think QLayouts. When we think “anchor-layouts” we think pretty much like how we would think when we arrange objects on our desk. Place one item to the right of the other, top of the other OR place this item to the extreme right of the table, that one to the extreme left and so on.

Anchor Layouts are more natural to think than traditional QLayouts. That is not to say that traditional layouts are bad. Having an option to use both is better.

I have implemented an AnchorLayout class that offers this functionality. If you just want to download and use the code, git-pull a copy of the code from here: https://github.com/pnudupa/anchorlayout

Simple Example

First let’s look at the results. Suppose I wanted to place a QFrame within a QWidget such that it would occupy the entire container, by leaving behind some margin.

int main(int argc, char **argv)
{
    QApplication a(argc, argv);

    QWidget container;

    QFrame *frame = new QFrame(&container);
    frame->setFrameStyle(QFrame::Box);

    AnchorLayout *containerLayout = AnchorLayout::get(&container);
    AnchorLayout *frameLayout = AnchorLayout::get(frame);
    frameLayout->fill(containerLayout)->setMargins(50);

    container.resize(400, 400);
    container.show();

    return a.exec();
}

When executed, we will get an output like this.

Surely this can be accomplished using a QVBoxLayout or QHBoxLayout as well. We are not competing with existing layouts. Only trying to bring the power of Anchor Layouts in QML to the QtWidgets world.

Show the frame in the bottom right quadrant

Suppose we wanted to show the frame in the bottom right quadrant of the container leaving a 20 pixel margin around, like this.

This is of course possible with QGridLayout, but somehow getting the placement right will be an issue. We will have to play around with sizeHint(), sizePolicy() of the frame effectively to achieve this result. You see, we want the frame to be in the bottom right quadrant, leaving a 20 pixel margin around even when the widget is resized.

With the AnchorLayout class, it is as simple as this. Notice how we borrow the anchor-lines concept from QML into this.

int main(int argc, char **argv)
{
    QApplication a(argc, argv);

    QWidget container;

    QFrame *frame = new QFrame(&container);
    frame->setFrameStyle(QFrame::Box);

    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();
}

Each AnchorLayout instance has left(), right(), top(), bottom(), horizontalCenter() and verticalCenter() AnchorLines. These lines can be made to coincide with AnchorLines of other layouts.

Placing widgets in a container is simply a matter of anchoring their anchor-lines of their anchor-layouts with each other. As the anchor lines move, they take the edges of the widgets they represent with them. This way the pull-and-push of anchor lines effectively places and resizes the widgets.

Place two frames in the center, separated by a space

Suppose wanted to place two frames next to each other, in the center of a container such that

  • The width of each frame is 35% of the width of the container
  • The height of each frame is 50% of the height of the container

Like this:

Once again, the idea is to ensure that width and height ratios vis-a-vis the container must remain consistent when the container is resized.

Here is how we would use the AnchorLayout class to make this happen.

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();
}

Notice the use of customLine() method. In addition to left(), top(), right(), bottom(), horizontalCenter() and verticalCenter() anchor lines, we can create custom anchor lines. Anchor Lines are either horizontal or vertical lines. They show up at a particular distance from the layout’s edge. For example

  • A Qt::HorizontalLine at 0.25 distance is a horizontal line that is 25% of the width from the left of the layout.
  • A Qt::HorizontalLine at -0.25 distance is a horizontal line that is 25% of the width from the right of the layout

Like all AnchorLines, custom AnchorLines can be used to anchor another another anchor-line on it as long as the orientations match. Also note how we are able to easily introduce space in-between the frames using margins.

We want one one of the frames to stick to the top, the other to the bottom.

Suppose want to achieve this.

Notice that “one” and “two” are taller than before. They are actually 75% as tall as the container. While frame one is aligned to the top, frame two is aligned to the bottom. But note also the margin and spacing. The code for accomplishing this result is almost similar to what we have written before. I have highlighted the changes.

int main(int argc, char **argv)
{
    QApplication a(argc, argv);

    QWidget container;

    QLabel *frame1 = new QLabel(&container);
    // ....

    QLabel *frame2 = new QLabel(&container);
    // ....

    AnchorLayout *containerLayout = AnchorLayout::get(&container);

    AnchorLayout *frame1Layout = AnchorLayout::get(frame1);
    // ....    
    frame1Layout->top()
                ->anchorTo(containerLayout->top())
                ->setMargin(10);
    frame1Layout->bottom()
                ->anchorTo(containerLayout->customLine(Qt::Horizontal,-0.25))
                ->setMargin(-10);

    AnchorLayout *frame2Layout = AnchorLayout::get(frame2);
    // ....
    frame2Layout->top()
                ->anchorTo(containerLayout->customLine(Qt::Horizontal,0.25))
                ->setMargin(-10);
    frame2Layout->bottom()
                ->anchorTo(containerLayout->bottom())
                ->setMargin(10);

    // ....
}

Accomplishing something like this with traditional layouts, though possible, would be quite tricky. You have to play around with stretch factors, alignments, size hints and policies. Anchor Layout just makes all this very straight forward.

Typical Application UI

A typical application UI consists of a central document area, surrounded by other widgets.

Although a main-window does indeed provide us with this construct, lets look at how we can construct such a layout by ourselves using AnchorsLayout.

Firstly, let us spell out the ratio, proportions and fixed sizes of each area

  • The menu-bar area is always along the top, occupying the full width of the window. It has a fixed height of 30 pixels.
  • The status-bar-area is always along the bottom, occupying the full width of the window. It also has a fixed height of 30 pixels.
  • The tool-box and config-box areas are to the left and right; occupying roughly 20% of the window width.
  • The remaining space is occupied by the document-area.
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();
}

Notice how we are able to easily place areas relative to each other. Notice also how we are able to use setFixedHeight() to specify some hardcoded sizes. Once we have sorted out the placement of areas, we can now go ahead and place widgets within them using traditional QLayouts or using AnchorLayouts.

AnchorLayout does not inherit QLayout

The AnchorLayout class I have implemented does not inherit QLayout. It makes use of event-filters to look up resize events and recalculates placement of anchor-lines. Each anchor-line then cascades its placement to other anchor-lines connected to it. Recursively the entire widget UI is placed and sized. This also means that we can use an AnchorLayout on top of existing layouts. This is especially useful when you want to stack a widget on top of another widget, which may already have a layout, but still want to have control over its placement.

Example: Let’s take Qt’s QMainWindow sample application.

Now, lets say we want to hover a calendar widget on top of the main-window such that it comes exactly above the status-bar area, but horizontally centered. Like this:

Existing main-window layouts do not provide us any facility to accommodate temporary widgets like this. But with AnchorLayout, we can do something like this

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);
}

What if we wanted for the calendar widget to slightly overlap the status bar area, such that the bottom edge of the calendar goes all the way to 50% of the status-bar height?

We just need to make a really small code change.

    calendarLayout->bottom()
            ->anchorTo(statusBarLayout->verticalCenter());

In this example, we are showing a calendar widget. But in a real world application, it is perfectly possible that whenever we select a file to open, we want to show a temporary widget near the status bar like this which shows load-progress. We want for this temporary widget to be visible within the boundary of the main-window; but placed appropriately.

Surely there are plenty of real-world use cases for using an Anchor Layout in Widgets UI. If you want to take the Anchor Layout for a spin, please pull a copy of the sample code from here: https://github.com/pnudupa/anchorlayout.git

Leave a Reply

Your email address will not be published. Required fields are marked *