Thursday, November 21, 2013

Understanding the QWidget layout flow

When layouts in a UI are not behaving as expected or performance is poor, it can be helpful to have a mental model of the layout process in order to know where to start debugging.  For web browsers there are some good resources which provide a description of the process at different levels. The layout documentation for Qt describes the various layout facilities that are available but I haven't found a detailed description of the flow, so this is my attempt to explain what happens when a layout is triggered that ultimately ends up with the widgets being resized and repositioned appropriately.

  1. A widget's contents are modified in some way that require a layout update. Such changes can include:
    • Changes to the content of the widget (eg. the text in a label, content margins being altered)
    • Changes to the sizePolicy() of the widget
    • Changes to the layout() of the widget, such as new child widgets being added or removed
  2. The widget calls QWidget::updateGeometry() which then performs several steps to trigger a layout:
    1. It invalidates any cached size information for the QWidgetItem associated with the widget in the parent layout.
    2. It recursively climbs up the widget tree (first to the parent widget, then the grandparent and so on), invalidating that widget's layout. The process stops when we reach a widget that is a top level window or doesn't have its own layout - we'll call this widget the top-level widget, though it might not actually be a window.
    3. If the top-level widget is not yet visible, then the process stops and layout is deferred until the widget is due to be shown.
    4. If the top-level widget is shown, a LayoutRequest event is posted asynchronously to the top-level widget, so a layout will be performed on the next pass through the event loop.
    5. If multiple layout requests are posted to the same top-level widget during a pass through the event loop, they will get compressed into a single layout request. This is similar to the way that multiple QWidget::update() requests are compressed into a single paint event.
  3. The top-level widget receives the LayoutRequest event on the next pass through the event loop. This can then be handled in one of two ways:
    1. If the widget has a layout, the layout will intercept the LayoutRequest event using an event filter and handle it by calling QLayout::activate()
    2. If the widget does not have a layout, it may handle the LayoutRequest event itself and manually set the geometry of its children.
  4. When the layout is activated, it first sets the fixed, minimum and/or maximum size constraints of the widget depending on QLayout::sizeConstraint(), using the values calculated by QLayout::minimumSize(), maximumSize() and sizeHint(). These functions will recursively proceed down the layout tree to determine the constraints for each item and produce a final size constraint for the whole layout.  This may or may not alter the current size of the widget.
  5. The layout is then asked to resize its contents to fit the current size of the widget using QLayout::setGeometry(widget->size()). The specific implementation of the layout - whether it is a box layout, grid layout or something else then lays out its child items to fit this new size.
  6. For each item in the layout, the QLayoutItem::setGeometry() implementation will typically ask the item for various size parameters (minimum size, maximum size, size hint, height for width) and then decide upon a final size and position for the item. It will then invoke QLayoutItem::setGeometry() to update the position and size of the widget.
  7. If the layout item is itself a layout or a widget, steps 5-6 proceed recursively down the tree, updating all of the items whose constraints have been modified.
A layout update is an expensive operation, so there are a number of steps taken to avoid unnecessary re-layouts:
  • Multiple layout update requests submitted in a single pass through the event loop are coalesced into a single update
  • Layout updates for widgets that are not visible and layouts that are not enabled are deferred until the widget is shown or the layout is re-enabled
  • The QLayoutItem::setGeometry() implementations will typically check whether the current and new geometry differ or whether they have been invalidated in some way before performing an update. This prunes parts of the widget tree from the layout process which have not been altered.
  • The QWidgetItem associated with a widget in a layout caches information which is expensive to calculate, such as sizeHint(). This cached data is then returned until the widget invalidates it using QWidget::updateGeometry()

Given this flow, there are a few things to bear in mind to avoid unexpected behaviour:
  • Qt provides multiple ways to set constraints such as fixed and minimum sizes.
    • Using QWidget::setFixedSize(), setMinimumSize() or setMaximumSize(). This is simple and available whether you control the widget or not.
    • Implementing the sizeHint() and minimumSizeHint() functions and using QWidget::setSizePolicy() to determine how these hints are handled by the layouts. If you control the widget, it is almost always preferable to use sizePolicy() together with the layout hints.
  • The layout management documentation suggests that handling LayoutRequest events in QWidget::event() is an alternative to implementing a custom layout. A potential problem with this is that LayoutRequest events are delivered asynchronously on the next pass through the event loop. If your widget is likely to update its own geometry in response to the LayoutRequest event then this can trigger layout flicker where several passes through the event loop occur before the layout process is fully finished. Each of the intermediate stages will flicker on screen briefly, as the event loop may process a paint event on each pass as well as the layout update, which looks poor. So if you need a custom layout, subclassing QLayout/QLayoutItem is the recommended approach unless you're sure that your widget will always be used as a top-level widget.

Post a Comment