Trolltech | Documentation | Qt Quarterly | « Qt 3.1's SQL Sequel | Prize Puzzle Solution »

Trading Height for Width
by Jasmin Blanchette
Since the release of Qt 2.0, layouts have become an important part of Qt programming. Layouts relieve the programmer from having to specify the position of all of a form's child widgets, and usually result in more attractive forms. This article presents one problem that can arise with layouts, and for which no perfect solution exists: "height-for-width." It also presents the source code of a fish- and layout-friendly Aquarium widget.

The Minimum-Size Paradox

Qt's built-in widgets reimplement sizeHint() and minimumSizeHint() to help layout managers do their job. For example, an "OK" button might have a size hint of (40, 25), meaning that the layout should give it at least 40 pixels horizontally and 25 pixels vertically.

However, some kinds of widget have more advanced requirements. For example, here are screenshots of a QMenuBar at three different sizes:

Menubar

The screenshots clearly show that a wide QMenuBar doesn't need to be tall, and that a tall one doesn't need to be wide. QLabel with word-wrapping turned on shows the same behavior:

Alibaba-Diag

Screenshots #1 and #2 show that the label can be reduced in size to 102 pixels horizontally or to 55 pixels vertically, so long as there is enough space in the other direction. Screenshot #3 shows what happens when the label is squeezed down to its minimum height and minimum width.

We would like QLabel to tell the layout that screenshot #1 and screenshot #2 are acceptable but that screenshot #3 is not. The sizeHint() and minimumSizeHint() functions cannot do this, so Qt provides a complementary mechanism: height-for-width.

Every widget's QSizePolicy contains a boolean height-for-width flag that indicates whether or not the widget is able to trade width for height and height for width. The layout will call the virtual function QWidget::heightForWidth() as necessary to determine the desired height for a height-for-width widget with a given width.

For the QMenuBar shown earlier, we have these values:

This is all QLayout needs to avoid screenshot #3.

All of this is handled automatically by Qt, so you rarely need to intervene.

Trouble at the Top

Things start to get more complicated when height-for-width widgets are top-level widgets, because these are not managed by a layout class. While top-level QLabels, QMenuBars, or QTextEdits are rare, it is very common to have top-level forms made up of layouts that contain sublayouts that contain height-for-width QLabels, QMenuBars, or QTextEdits. The form inherits the layout behavior of its child widgets, which can lead to undesirable results. Here's an example Setup dialog:

Tele-Diag

While screenshots #1 and #2 look reasonable, screenshot #3 cuts off the buttons and some of the text. The best way to handle such forms is to allow the user to freely resize them. That's the approach taken by most modern applications, including Microsoft Visual Studio and Internet Explorer.

Before Qt 3.1, the layout classes set the form's minimum size to the minimum size hint, resulting in an undesirable "blocking" behavior. The solution was to add this line to the program:

    dialog->layout()->setResizeMode( QLayout::FreeResize );

In Qt 3.1, "free resize" is the default for layouts with height-for-width.

Writing Widgets with Height-for-Width

We will now write an Aquarium widget with height-for-width. The Aquarium widget contains a certain number of fish. For the fish to survive, they each need a certain amount of space. Unsurprisingly, this brings us back to height-for-width: A wide aquarium doesn't need to be deep, and a deep aquarium doesn't need to be wide.

Aqua1  Aqua2  Aqua3

The screenshots show how the layout adapts to the changing needs of the Aquarium widget each time we add a fish into it.

The Aquarium class inherits QFrame and reimplements three virtual functions. Its definition follows:

    class Aquarium : public QFrame
    {
        Q_OBJECT
    public:
        Aquarium( int numFish, QWidget *parent = 0, const char *name = 0 );

        virtual int heightForWidth( int width ) const;
        virtual QSize sizeHint() const;

    public slots:
        void setCapacity( int numFish );

    protected:
        virtual void drawContents( QPainter *painter );

        QPixmap fish;
        int capacity;
    };

The constructor accepts the capacity of the aquarium and sets the color, frame style, and size policy of the widget:
    Aquarium::Aquarium( int numFish, QWidget *parent, const char *name )
        : QFrame( parent, name ), fish( "fish.png" ),
          capacity( numFish )
    {
        setPalette( QPalette(QColor("light blue")) );
        setFrameStyle( Box | Raised );
        setSizePolicy( QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred, TRUE) );
    }

The TRUE argument to the QSizePolicy constructor means that the widget has height-for-width.

The heightForWidth() function is reimplemented from QWidget to compute the space based on the number of fish:

    int Aquarium::heightForWidth( int width ) const
    {
        return 10000 * capacity / QMAX( width, 1 );
    }

We assume that each fish requires a space of 10000 pixels, although this really depends on the species. We use QMAX() to avoid division by zero.

The sizeHint() function is reimplemented from QWidget to provide a decent default size for the widget:

    QSize Aquarium::sizeHint() const
    {
        int w = (int) ( 100 * sqrt(capacity) );
        return QSize( w, heightForWidth(w) );
    }

The setCapacity() function sets the number of fish:
    void Aquarium::setCapacity( int numFish )
    {
        if ( capacity != numFish ) {
            capacity = numFish;
            updateGeometry();
            update();
        }
    }

It calls QWidget::updateGeometry() to tell an eventual layout to adjust to the new situation. Well-behaved widgets always call updateGeometry() when their sizeHint(), minimumSizeHint(), or heightForWidth() changes. It also calls QWidget::update() to redraw the aquarium with the correct number of fish in it.

The drawContents() function is reimplemented from QFrame to draw the fish:

    void Aquarium::drawContents( QPainter *painter )
    {
        srand( capacity );
        QSize size( width() - fish.width(), height() - fish.height() );
        size = size.expandedTo( QSize(1, 1) );
        for ( int i = 0; i < capacity; i++ ) {
            int x = rand() % size.width();
            int y = rand() % size.height();
            painter->drawPixmap( x, y, fish );
        }
    }

The call to srand() ensures that the sequence of random numbers we get to position the fish repeats. This means that fish always appear in the same places for a given capacity.

The Tall and the Short of It

The base QWidget implementation of heightForWidth() returns 0 to signify that a widget's height and width are independent. This is appropriate for most QWidget subclasses. Classes like QLabel, QMenuBar, and QTextEdit reimplement heightForWidth().

Implementing a heightForWidth() function is not difficult. In the case of the Aquarium class, we reimplemented heightForWidth() as a monotonically decreasing function. But Qt's layout system makes no such assumption, so we can reimplement heightForWidth() as we like -- for example, to ensure that a widget maintains a constant aspect ratio.


This document is licensed under the Creative Commons Attribution-Share Alike 2.5 license.

Copyright © 2002 Trolltech. Trademarks Prize Puzzle Solution »