Trolltech | Documentation | Qt Quarterly | « Mandatory Fields

Not Your Standard Pie Menu
by Andreas Aardal Hanssen
Pie menus are great fun to work with. They come in various shapes, sizes, and colors, often tailored to fit the specific application. They are especially frequent in games and in specialized applications. Because all items are available at an equal distance from the mouse pointer origin, pie menus are much more efficient to use than standard menus.

[Download Source Code]

Normal pie menus are circular, with action items represented as pie slices. When a pie menu pops up, the mouse pointer is automatically positioned at the center of the pie.

Standard-Pie     Hexagon  Pie-Example

The QtPieMenu component, available as a Qt Solution, makes it easy to add pie menus to your applications. The default look and feel is shown above on the left. Other looks are possible by subclassing QtPieMenu and reimplementing a few virtual functions.

In this article, we show how to create the "sector" pie menu shown above on the right. When a pie sector is selected, it is extended with sub-items. The main benefit of this new shape is that every mouse gesture consists of a straight movement of the cursor from the center of the pie and out toward a leaf item.

Our sector menu requires us to reimplement four functions: indexAt(), generateMask(), reposition(), and paintEvent(). Everything else is handled by the QtPieMenu base class. Let's start with the class definition:

    class SectorMenu : public QtPieMenu
    {
        Q_OBJECT
    public:
        SectorMenu(const QString &title, QWidget *parent);
    
        int indexAt(const QPoint &pos);
    
    protected:
        void generateMask(QBitmap *mask);
        void reposition();
        void paintEvent(QPaintEvent *event);
        int menuDepth() const;
    
    private:
        int startAngle;
        int arcLength;
    };
    

We declare the four functions we need to reimplement, a helper function called menuDepth(), and two member variables. The variables will be used to lay out and draw the pie sectors.

    SectorMenu::SectorMenu(const QString &title,
                           QWidget *parent, const char *name)
        : QtPieMenu(title, parent, name)
    {
        int depth = menuDepth();
        setInnerRadius(depth * 50);
        setOuterRadius((depth + 1) * 50);
        setFixedSize(2 * outerRadius() + 1,
                     2 * outerRadius() + 1);
    }
    

In the constructor, we determine the menu's inner and outer radii. The root menu has a fixed inner radius of 0 and an outer radius of 50 pixels; first-level submenus have an inner radius of 50 pixels and an outer radius of 100; second-level submenus have an inner radius of 100 pixels and an outer radius of 150; and so on.

    int SectorMenu::menuDepth() const
    {
        const QObject *pie = this;
        int depth = 0;
    
        while (pie->parent()
               && pie->parent()->inherits("SectorMenu")) {
            pie = pie->parent();
            ++depth;
        }
        return depth;
    }
    

The menuDepth() function returns the menu's depth. The root menu has a depth of 0, the submenus have a depth of 1, the sub-submenus have a depth of 2, and so on.

    void SectorMenu::generateMask(QBitmap *mask)
    {
        if (menuDepth() == 0) {
            startAngle = 45 * 16;
            arcLength = 360 * 16;    
        } else {
            SectorMenu *parentPie = (SectorMenu *)parent();
            for (int i = 0; i < parentPie->count(); ++i) {
                if (parentPie->subMenuAt(i) == this) {
                    arcLength = parentPie->arcLength
                                / parentPie->count();
                    startAngle = (45 * 16) + (arcLength * i);
                    break;
                }
            }
        }
        // more follows
    

QtPieMenu calls generateMask() just before a menu is shown for the first time. The mask specifies which pixels belong to the menu; the other pixels are made transparent.

If the menu is a root menu, the startAngle and arcLength member variables are set to 45 and 360 degrees, respectively. The values are multiplied by 16 because QPainter expresses angles as sixteenths of a degree.

If the menu is a submenu, we iterate through the parent menu's submenus and determine the start angle and arc length of the submenu.

        QPainter painter(mask);
        painter.setPen(color1);
        painter.setBrush(color1);
        painter.drawPie(0, 0,
                        outerRadius() * 2, outerRadius() * 2,
                        startAngle, arcLength);
    
        if (innerRadius() > 0) {
            QPoint center = rect().center();
            painter.setPen(color0);
            painter.setBrush(color0);
            painter.drawPie(center.x() - innerRadius(),
                            center.y() - innerRadius(),
                            innerRadius() * 2,
                            innerRadius() * 2,
                            0, 360 * 16);
        }
    }
    

Next, we draw the mask with one big pie for the outer radius, and one small pie for the inner radius, based on startAngle and arcLength. The mask will exactly cover the section of the pie menu that belongs to this menu.

Pie-Diagram1

The pie menu in the diagram above is composed of three widgets, made visible here using black rectangles. To make things clearer, here are screenshots of each widget:

Pie-Diagram2    Pie-Diagram3    Pie-Diagram4

Now, we just have two remaining functions to review: indexAt() and paintEvent().

    int SectorMenu::indexAt(const QPoint &pos)
    {
        if (count() == 0)
            return -1;
    
        int sliceSize = arcLength / count();
        if (sliceSize == 0)
            return -1;
    
        int angle = (int)((angleAt(pos) * 360.0 * 16.0)
                          / (2.0 * M_PI));
        if (angle - startAngle < 0)
            angle += 16 * 360;
        angle -= startAngle;
    
        int sector = angle / sliceSize;
        if (sector < 0 || sector >= count())
            return -1;
        return sector;
    }
    

The indexAt() function is reimplemented to return the index of the item located at a certain screen position (or -1 if there is no item there). It then calculates the angle of the point, and determines which sector (if any) is under this point. The angleAt() function is provided by the QtPieMenu base class.

Our indexAt() function can safely ignore the menu's inner and outer radii, because QtPieMenu only calls indexAt() on points that are part of the mask we generated.

    void SectorMenu::reposition()
    {
        if (menuDepth() > 0) {
            SectorMenu *parentPie = (SectorMenu *)parent();
            QPoint center = parentPie->geometry().center();
            move(center.x() - outerRadius(),
                 center.y() - outerRadius());
        }
    }
    

When a submenu is opened, QtPieMenu positions it just outside the sector that triggered it. In our implementation, the submenu widget is centered on top of the parent widget.

    void SectorMenu::paintEvent(QPaintEvent *)
    {
        QPainter painter(this);
        QFontMetrics metrics(font());
        QPoint center = rect().center();
        int sliceSize = arcLength / count();
    
        for (int i = 0; i < count(); ++i) {
            if (i == highlightedItem()) {
                painter.setPen(
                        colorGroup().highlightedText());
                painter.setBrush(colorGroup().highlight());
            } else {
                painter.setPen(colorGroup().foreground());
                painter.setBrush(colorGroup().background());
            }
            painter.drawPie(0, 0,
                            outerRadius() * 2,
                            outerRadius() * 2,
                            startAngle + sliceSize * i,
                            sliceSize);
            // more follows
    

Finally, we're ready to draw the pie menu. For every item, we draw one pie sector using QPainter::drawPie().

          double rad = (innerRadius() + outerRadius())
                          / 2.0;
            double slice = sliceSize * M_PI / (360.0 * 8.0);
            double angle = startAngle * M_PI / (360.0 * 8.0);
            angle += slice * (i + 0.5);
    
            int x = (int)(cos(angle) * rad);
            int y = (int)(-sin(angle) * rad);
    
            painter.drawText(center.x() + x
                             - metrics.width(itemText(i)) / 2,
                             center.y() + y, itemText(i));
        }
    }
    

Then we draw the text. We calculate the center point of each sector, and draw the item text centered on this point.

Our implementation of paintEvent() is very simple, and this article only covers drawing of the sectors and text. The full source code contains code for drawing icons as well.


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

Copyright © 2004 Trolltech Trademarks Qt Quarterly