Painter Paths Example

Image non disponible

The QPainterPath class provides a container for painting operations, enabling graphical shapes to be constructed and reused.

A painter path is an object composed of a number of graphical building blocks (such as rectangles, ellipses, lines, and curves), and can be used for filling, outlining, and clipping. The main advantage of painter paths over normal drawing operations is that complex shapes only need to be created once, but they can be drawn many times using only calls to QPainter::drawPath().

The example consists of two classes:

  • The RenderArea class which is a custom widget displaying a single painter path.

  • The Window class which is the applications main window displaying several RenderArea widgets, and allowing the user to manipulate the painter paths' filling, pen, color and rotation angle.

First we will review the Window class, then we will take a look at the RenderArea class.

Window Class Definition

The Window class inherits QWidget, and is the applications main window displaying several RenderArea widgets, and allowing the user to manipulate the painter paths' filling, pen, color and rotation angle.

 
Sélectionnez
class Window : public QWidget
{
    Q_OBJECT

public:
    Window();

private slots:
    void fillRuleChanged();
    void fillGradientChanged();
    void penColorChanged();

We declare three private slots to respond to user input regarding filling and color: fillRuleChanged(), fillGradientChanged() and penColorChanged().

When the user changes the pen width and the rotation angle, the new value is passed directly on to the RenderArea widgets using the QSpinBox::valueChanged() signal. The reason why we must implement slots to update the filling and color, is that QComboBox doesn't provide a similar signal passing the new value as argument; so we need to retrieve the new value, or values, before we can update the RenderArea widgets.

 
Sélectionnez
private:
    void populateWithColors(QComboBox *comboBox);
    QVariant currentItemData(QComboBox *comboBox);

We also declare a couple of private convenience functions: populateWithColors() populates a given QComboBox with items corresponding to the color names Qt knows about, and currentItemData() returns the current item for a given QComboBox.

 
Sélectionnez
    QList<RenderArea*> renderAreas;
    QLabel *fillRuleLabel;
    QLabel *fillGradientLabel;
    QLabel *fillToLabel;
    QLabel *penWidthLabel;
    QLabel *penColorLabel;
    QLabel *rotationAngleLabel;
    QComboBox *fillRuleComboBox;
    QComboBox *fillColor1ComboBox;
    QComboBox *fillColor2ComboBox;
    QSpinBox *penWidthSpinBox;
    QComboBox *penColorComboBox;
    QSpinBox *rotationAngleSpinBox;
};

Then we declare the various components of the main window widget. We also declare a convenience constant specifying the number of RenderArea widgets.

Window Class Implementation

In the Window constructor, we define the various painter paths and create corresponding RenderArea widgets which will render the graphical shapes:

 
Sélectionnez
Window::Window()
{
    QPainterPath rectPath;
    rectPath.moveTo(20.0, 30.0);
    rectPath.lineTo(80.0, 30.0);
    rectPath.lineTo(80.0, 70.0);
    rectPath.lineTo(20.0, 70.0);
    rectPath.closeSubpath();

We construct a rectangle with sharp corners using the QPainterPath::moveTo() and QPainterPath::lineTo() functions.

QPainterPath::moveTo() moves the current point to the point passed as argument. A painter path is an object composed of a number of graphical building blocks, i.e. subpaths. Moving the current point will also start a new subpath (implicitly closing the previously current path when the new one is started). The QPainterPath::lineTo() function adds a straight line from the current point to the given end point. After the line is drawn, the current point is updated to be at the end point of the line.

We first move the current point starting a new subpath, and we draw three of the rectangle's sides. Then we call the QPainterPath::closeSubpath() function which draws a line to the beginning of the current subpath. A new subpath is automatically begun when the current subpath is closed. The current point of the new path is (0, 0). We could also have called QPainterPath::lineTo() to draw the last line as well, and then explicitly start a new subpath using the QPainterPath::moveTo() function.

QPainterPath also provide the QPainterPath::addRect() convenience function, which adds a given rectangle to the path as a closed subpath. The rectangle is added as a clockwise set of lines. The painter path's current position after the rect has been added is at the top-left corner of the rectangle.

 
Sélectionnez
    QPainterPath roundRectPath;
    roundRectPath.moveTo(80.0, 35.0);
    roundRectPath.arcTo(70.0, 30.0, 10.0, 10.0, 0.0, 90.0);
    roundRectPath.lineTo(25.0, 30.0);
    roundRectPath.arcTo(20.0, 30.0, 10.0, 10.0, 90.0, 90.0);
    roundRectPath.lineTo(20.0, 65.0);
    roundRectPath.arcTo(20.0, 60.0, 10.0, 10.0, 180.0, 90.0);
    roundRectPath.lineTo(75.0, 70.0);
    roundRectPath.arcTo(70.0, 60.0, 10.0, 10.0, 270.0, 90.0);
    roundRectPath.closeSubpath();

Then we construct a rectangle with rounded corners. As before, we use the QPainterPath::moveTo() and QPainterPath::lineTo() functions to draw the rectangle's sides. To create the rounded corners we use the QPainterPath::arcTo() function.

QPainterPath::arcTo() creates an arc that occupies the given rectangle (specified by a QRect or the rectangle's coordinates), beginning at the given start angle and extending the given degrees counter-clockwise. Angles are specified in degrees. Clockwise arcs can be specified using negative angles. The function connects the current point to the starting point of the arc if they are not already connected.

 
Sélectionnez
    QPainterPath ellipsePath;
    ellipsePath.moveTo(80.0, 50.0);
    ellipsePath.arcTo(20.0, 30.0, 60.0, 40.0, 0.0, 360.0);

We also use the QPainterPath::arcTo() function to construct the ellipse path. First we move the current point starting a new path. Then we call QPainterPath::arcTo() with starting angle 0.0 and 360.0 degrees as the last argument, creating an ellipse.

Again, QPainterPath provides a convenience function ( QPainterPath::addEllipse()) which creates an ellipse within a given bounding rectangle and adds it to the painter path. If the current subpath is closed, a new subpath is started. The ellipse is composed of a clockwise curve, starting and finishing at zero degrees (the 3 o'clock position).

 
Sélectionnez
    QPainterPath piePath;
    piePath.moveTo(50.0, 50.0);
    piePath.arcTo(20.0, 30.0, 60.0, 40.0, 60.0, 240.0);
    piePath.closeSubpath();

When constructing the pie chart path we continue to use a combination of the mentioned functions: First we move the current point, starting a new subpath. Then we create a line from the center of the chart to the arc, and the arc itself. When we close the subpath, we implicitly construct the last line back to the center of the chart.

 
Sélectionnez
    QPainterPath polygonPath;
    polygonPath.moveTo(10.0, 80.0);
    polygonPath.lineTo(20.0, 10.0);
    polygonPath.lineTo(80.0, 30.0);
    polygonPath.lineTo(90.0, 70.0);
    polygonPath.closeSubpath();

Constructing a polygon is equivalent to constructing a rectangle.

QPainterPath also provide the QPainterPath::addPolygon() convenience function which adds the given polygon to the path as a new subpath. Current position after the polygon has been added is the last point in polygon.

 
Sélectionnez
    QPainterPath groupPath;
    groupPath.moveTo(60.0, 40.0);
    groupPath.arcTo(20.0, 20.0, 40.0, 40.0, 0.0, 360.0);
    groupPath.moveTo(40.0, 40.0);
    groupPath.lineTo(40.0, 80.0);
    groupPath.lineTo(80.0, 80.0);
    groupPath.lineTo(80.0, 40.0);
    groupPath.closeSubpath();

Then we create a path consisting of a group of subpaths: First we move the current point, and create a circle using the QPainterPath::arcTo() function with starting angle 0.0, and 360 degrees as the last argument, as we did when we created the ellipse path. Then we move the current point again, starting a new subpath, and construct three sides of a square using the QPainterPath::lineTo() function.

Now, when we call the QPainterPath::closeSubpath() function the last side is created. Remember that the QPainterPath::closeSubpath() function draws a line to the beginning of the current subpath, i.e the square.

QPainterPath provide a convenience function, QPainterPath::addPath() which adds a given path to the path that calls the function.

 
Sélectionnez
    QPainterPath textPath;
    QFont timesFont("Times", 50);
    timesFont.setStyleStrategy(QFont::ForceOutline);
    textPath.addText(10, 70, timesFont, tr("Qt"));

When creating the text path, we first create the font. Then we set the font's style strategy which tells the font matching algorithm what type of fonts should be used to find an appropriate default family. QFont::ForceOutline forces the use of outline fonts.

To construct the text, we use the QPainterPath::addText() function which adds the given text to the path as a set of closed subpaths created from the supplied font. The subpaths are positioned so that the left end of the text's baseline lies at the specified point.

 
Sélectionnez
    QPainterPath bezierPath;
    bezierPath.moveTo(20, 30);
    bezierPath.cubicTo(80, 0, 50, 50, 80, 80);

To create the Bezier path, we use the QPainterPath::cubicTo() function which adds a Bezier curve between the current point and the given end point with the given control point. After the curve is added, the current point is updated to be at the end point of the curve.

In this case we omit to close the subpath so that we only have a simple curve. But there is still a logical line from the curve's endpoint back to the beginning of the subpath; it becomes visible when filling the path as can be seen in the applications main window.

 
Sélectionnez
    QPainterPath starPath;
    starPath.moveTo(90, 50);
    for (int i = 1; i < 5; ++i) {
        starPath.lineTo(50 + 40 * std::cos(0.8 * i * M_PI),
                        50 + 40 * std::sin(0.8 * i * M_PI));
    }
    starPath.closeSubpath();

The final path that we construct shows that you can use QPainterPath to construct rather complex shapes using only the previous mentioned QPainterPath::moveTo(), QPainterPath::lineTo() and QPainterPath::closeSubpath() functions.

 
Sélectionnez
    renderAreas.push_back(new RenderArea(rectPath));
    renderAreas.push_back(new RenderArea(roundRectPath));
    renderAreas.push_back(new RenderArea(ellipsePath));
    renderAreas.push_back(new RenderArea(piePath));
    renderAreas.push_back(new RenderArea(polygonPath));
    renderAreas.push_back(new RenderArea(groupPath));
    renderAreas.push_back(new RenderArea(textPath));
    renderAreas.push_back(new RenderArea(bezierPath));
    renderAreas.push_back(new RenderArea(starPath));

Now that we have created all the painter paths that we need, we create a corresponding RenderArea widget for each. In the end, we make sure that the number of render areas is correct using the Q_ASSERT() macro.

 
Sélectionnez
    fillRuleComboBox = new QComboBox;
    fillRuleComboBox->addItem(tr("Odd Even"), Qt::OddEvenFill);
    fillRuleComboBox->addItem(tr("Winding"), Qt::WindingFill);

    fillRuleLabel = new QLabel(tr("Fill &Rule:"));
    fillRuleLabel->setBuddy(fillRuleComboBox);

Then we create the widgets associated with the painter paths' fill rule.

There are two available fill rules in Qt: The Qt::OddEvenFill rule determine whether a point is inside the shape by drawing a horizonta