We have two distinct drawing methods at our disposal.

In immediate mode Qt will call a given function whenever we (re)draw. By overriding that function (QWidget::paintEvent()), we determine what gets drawn. In retained mode we prepare a container of the objects we wish to draw, and we implement for each object its own drawing function.

Let’s look at incremental examples for drawing and interacting with objects using both methods.

1. Immediate Mode

1.1. Create a Window

Creating a window in which to draw is as simple as instantiating a QWidget.

int main(int argc, char * argv[])
{
    QApplication app(argc, argv);
    QWidget w;
    w.show();
    return app.exec();
}

With that code we get a drawing window.

The QApplication object controls the event loop. After creating a QWidget and displaying it, we pass control to QApplication. Window events (resize, mouse clicks, etc) are now sent to the QWidget object. By default these do nothing, hence the empty drawing surface.

QWidget
Figure 1. QWidget

1.2. Override QWidget::paintEvent()

To draw we need to override QWidget::paintEvent(). We do so in a class Hello_Segment that we derive from QWidget.

Qt’s imaging model requires that we instantiate a QPainter object and initialize it with the instance of QWidget — this in our case. This model is very convenient because it makes it possible to reuse drawing routines on more than one imaging device. By just changing the object passed to QPainter during its construction, we can prepare a PDF output by painting to a QPrinter instance, or we can prepare an SVG file by painting to a QSvgGenerator.

int main(int argc, char * argv[])
{
    QApplication app(argc, argv);
    Hello_Segment hs;
    hs.show();
    return app.exec();
}
class Hello_Segment : public QWidget
{
public:
    Hello_Segment(QWidget * parent = 0)
        : QWidget(parent)
    {}

protected:
    virtual QSize sizeHint() const { return QSize(200, 200); }
    virtual void paintEvent(QPaintEvent * /*event*/)
    {
        QPainter painter(this);
        painter.setPen(QPen(Qt::black, 2.5));
        const QPointF A( 50.0,  50.0);
        const QPointF B(150.0, 150.0);
        painter.drawLine(A, B);
    }
};

The rendering operations, QPainter::setPen() and QPainter::drawLine(), are self explanatory. A more subtle issue are Qt’s geometric classes. Qt provides both QPoint and QPointF, the former uses integer coordinates and the latter floating point (double-precision) coordinates.

We obtain the following window. Observe that we have also overridden QWidget::sizeHint() to return a QSize object. The QWidget is now created at the given dimensions.

OneLine
Figure 2. Drawing a line on a QWidget

Observe that the origin is at the top left corner of the window. This placement facilitates computations when laying out text alongside a drawing.

1.3. Handle Keyboard Events

To handle a keyboard event, we override QWidget::keyPressEvent(QKeyEvent * event). If the user presses the 'P' key, a PDF file of the drawing will be saved. If the user presses 'S', an SVG file is saved. We isolate the QPainter rendering to a function render(QPaintDevice * device) and call that function three times, to render a raster image on the screen using paintEvent(..), as well as to render PDF and SVG vector images.

    virtual void paintEvent(QPaintEvent * /*event*/)
    {
        render(this);
    }
    void render(QPaintDevice * device)
    {
        QPainter painter(device);
        painter.setPen(QPen(Qt::black, 2.5));
        QPointF A( 50.0,  50.0);
        QPointF B(150.0, 150.0);
        painter.drawLine(A, B);
    }

    virtual void keyPressEvent(QKeyEvent * event)
    {
        QPrinter printer;
        QSvgGenerator svgGenerator;
        switch(event->key()) {
        case Qt::Key_P:
            printer.setOutputFormat(QPrinter::PdfFormat);
            printer.setOutputFileName("Hello_Segment.pdf");
            render(&printer);
            QMessageBox::information(this, "PDF", "Document saved to PDF file", QMessageBox::Ok);
            break;
        case Qt::Key_S:
            svgGenerator.setFileName("Hello_Segment.svg");
            render(&svgGenerator);
            QMessageBox::information(this, "SVG", "Document saved to SVG file", QMessageBox::Ok);
            break;
        default:
            QWidget::keyPressEvent(event);
        }
    }

Here is the SVG file we obtain.

Hello Segment
Figure 3. Handling a keyboard event

1.4. Change Window Scale

We have so far been using screen coordinates. If sizeHint() returns QSize(500,400), whatever we draw must fit in a rectangle from the origin in a rectangle of that size (in the first quadrant, although it is reflected since the origin is at the top).

Often we want to decouple the user coordinates from the screen coordinates. Our screen may be of size 300x200 in the first quadrant, as in the following code, but the rectangle we want to draw is of size 12x8, centered at the origin. The function QTransform interpolate_and_yflip(const QRectF & from, const QRectF & to) computes the transformation needed from the rectangle from to the rectangle to. We pass rectangles using the Qt type QRectF. As with points, Qt provides two types QRectF and QRect, the first for double-precision floating point numbers and the latter for integer coordinates.

class Hello_Segment : public QWidget
{
public:
    Hello_Segment(QWidget * parent = 0)
        : QWidget(parent)
    {}

protected:
    virtual QSize sizeHint() const { return QSize(300, 200); }
    virtual void paintEvent(QPaintEvent * /*event*/)
    {
        render(this);
    }
    virtual void render(QPaintDevice * device)
    {
        QPainter painter(device);
        painter.setPen(QPen(Qt::black, 0.5));

        const QRectF window(QPointF(-6.0, -4.0), QPointF(6.0, 4.0));
        const QRectF device_window(0, 0, device->width(), device->height());
        QTransform T = interpolate_and_yflip(window, device_window);
        painter.setTransform( T );

        QPointF A(-4.0, -3.0);
        QPointF B( 4.0,  3.0);
        painter.drawLine(A, B);
    }
    QTransform interpolate_and_yflip(const QRectF & from, const QRectF & to)
    {
        QTransform T;
        T.translate( to.bottomLeft().x(), to.bottomLeft().y() ); // third: translate from origin
        T.scale(to.width() / from.width(), - to.height() / from.height() ); // second: scale
        T.translate( - from.bottomLeft().x(), - from.topLeft().y() ); // first: translate to origin
        return T;
    }
scale window
Figure 4. Setting up user coordinates

Our rendering still has a weakness. If we stretch the window vertically

scale window stretch vertically
Figure 5. Stretching the window vertically

or if we stretch it horizontally

scale window stretch horizontally
Figure 6. Stretching the window horizontally

the aspect ratio of the image will change.

1.5. Defining a Refined QWidget

We now define a child of QWidget that will encapsulate some useful and common behavior. If we change the aspect ratio of a drawing window, we would like the aspect ratio of the image to remain 1:1. We would like to maintain aspect ratios while centering the image maximally within the frame.

As an example, if we render a circle, a line, and some text in a square window,

a Refined QWidget
Figure 7. A Refined QWidget

and then stretch the window vertically, the circle remains a circle and the slope of the line will not change.

a Refined QWidget stretch vertically
Figure 8. The aspect ratio remains unchange when scaling the window vertically.

Likewise, we would like the aspect ratio of the image not to change if the window is stretched horizontally.

a Refined QWidget stretch horizontally
Figure 9. The aspect ratio remains unchanged when scaling the window horizontally.

Let’s start by looking at the code from the client side. Our Hello_Segment class is now a child of the yet to be defined RefinedQWidget. Since we may or may not want to move the origin from the top left to the bottom left corner, we provide the ability to choose between the two options by passing either INTERPOLATE_AND_YFLIP or INTERPOLATE_AND_DO_NOT_YFLIP in the initialization.

RefinedQWidget will have two pure virtual functions:

    virtual QRectF getUserRectF()const=0;
    virtual void render(QPaintDevice * device)=0;

The definition of Hello_Segment::render() will provide the drawing routines. Notice that, as before, we pass a QPaintDevice* to render() to make it possible to produce PDF and SVG media easily. Hello_Segment::getUserRectF() will define the user coordinates by returning a QRectF.

class Hello_Segment : public RefinedQWidget
{
public:
    Hello_Segment(QWidget * parent = 0)
//        : RefinedQWidget(parent, INTERPOLATE_AND_YFLIP)
        : RefinedQWidget(parent, INTERPOLATE_AND_DO_NOT_YFLIP)
    {}

protected:
    virtual QRectF getUserRectF() const
    {
        return QRectF(QPointF(0.0, 0.0), QPointF(100.0, 100.0));
    }
    virtual void render(QPaintDevice * device)
    {
        QPainter painter(device);
        painter.setPen(QPen(Qt::black, 1.0));
        painter.setRenderHint(QPainter::Antialiasing, true);
        // userToWidgetTransform is updated only when the QWidget is resized.
        painter.setTransform(userToWidgetTransform);

        painter.drawEllipse(QPointF(50.0,50.0), 40.0, 40.0);
        painter.drawText(QPointF(30.0, 30.0), QString("Hello"));

        painter.setPen(QPen(Qt::black, 5.0));
        QPointF A(10.0, 30.0);
        QPointF B(90.0, 40.0);
        painter.drawLine(A, B);
    }
};

#endif // HELLO_SEGMENT_H

Naturally we want the user-coordinates to widget-coordinates transformation to be computed only when necessary, which is when the widget is resized. Here Qt makes it convenient: RefinedQWidget::resizeEvent() will also be called at initialization.

RefinedQWidget only needs to use QTransform userToWidgetTransform, but we also maintain QTransform userToWidgetTransform as a protected member variable. The latter is not used by RefinedQWidget, but it will be needed by derived classes to handle mouse events.

// RefinedQWidget is responsible for centering the userRectF.
class RefinedQWidget : public QWidget
{
public:
    RefinedQWidget(QWidget * parent = 0,
                   YFlip _yflip = INTERPOLATE_AND_YFLIP)
        : QWidget(parent), yflip(_yflip)
    {}
protected:
    YFlip yflip;
    QTransform userToWidgetTransform;
    QTransform widgetToUserTransform;

    virtual QSize sizeHint() const { return QSize(200, 200); }

    // Classes derived from RefinedQWidget must define the user window..
    virtual QRectF getUserRectF()const=0;

    virtual void paintEvent(QPaintEvent * /*event*/)
    {
        render(this);
    }
    // Classes derived from RefinedQWidget must also define what to render.
    virtual void render(QPaintDevice * device)=0;

    virtual void resizeEvent(QResizeEvent *)
    {
        userToWidgetTransform = getUserToWidgetTransform(this);
        widgetToUserTransform = userToWidgetTransform.inverted();
    }

    QTransform getUserToWidgetTransform(QPaintDevice * device)
    {
        const QRectF device_window(0, 0, device->width(), device->height());

        QRectF w2 = get_centered_window(getUserRectF(), device_window);

        QTransform T;
        switch(yflip) {
        case INTERPOLATE_AND_YFLIP:
            T = interpolate_and_yflip(w2, device_window);
            break;
        case INTERPOLATE_AND_DO_NOT_YFLIP:
            T = interpolate_and_do_not_yflip(w2, device_window);
            break;
        }
        return T;
    }
    QTransform interpolate_and_yflip(const QRectF & from, const QRectF & to)
    {
        QTransform T;
        T.translate( to.bottomLeft().x(), to.bottomLeft().y() ); // third: translate from origin
        T.scale(to.width() / from.width(), - to.height() / from.height() ); // second: scale
        T.translate( - from.bottomLeft().x(), - from.topLeft().y() ); // first: translate to origin
        return T;
    }
    QTransform interpolate_and_do_not_yflip(const QRectF & from, const QRectF & to)
    {
        QTransform T;
        T.translate( to.bottomLeft().x(), to.bottomLeft().y() ); // third: translate from origin
        T.scale(to.width() / from.width(), to.height() / from.height() ); // second: scale
        T.translate( - from.bottomLeft().x(), - from.bottomLeft().y() ); // first: translate to origin
        return T;
    }
    QRectF get_centered_window(const QRectF & w /*input*/, const QRectF & d /*reference*/)
    {
        const qreal w_ar = w.width()/w.height();
        const qreal d_ar = d.width()/d.height();

        if(d_ar > w_ar) {
            qreal new_w_width = w.height() * d_ar;
            return QRectF( w.topLeft().x() - (new_w_width - w.width()) / 2.0 , w.topLeft().y(),
                           new_w_width, w.height() );
        }
        else {
            qreal new_w_height = w.width() / d_ar;
            return QRectF( w.topLeft().x(), w.topLeft().y() - (new_w_height - w.height()) / 2.0,
                           w.width(), new_w_height );
        }
    }

    virtual void keyPressEvent(QKeyEvent * event)
    {
        QPrinter printer;
        QSvgGenerator svgGenerator;
        switch(event->key()) {
        case Qt::Key_P:
            printer.setOutputFormat(QPrinter::PdfFormat);
            printer.setOutputFileName("Hello_Segment.pdf");
            userToWidgetTransform = getUserToWidgetTransform(&printer);
            render(&printer);
            userToWidgetTransform = getUserToWidgetTransform(this);
            QMessageBox::information(this, "Print", "Document printed", QMessageBox::Ok);
            break;
        case Qt::Key_S:
            svgGenerator.setFileName("Hello_Segment.svg");
            userToWidgetTransform = getUserToWidgetTransform(&svgGenerator);
            render(&svgGenerator);
            userToWidgetTransform = getUserToWidgetTransform(this);
            QMessageBox::information(this, "SVG", "Document saved to SVG file", QMessageBox::Ok);
            break;
        default:
            QWidget::keyPressEvent(event);
        }
    }
};

#endif // REFINEDQWIDGET_H

1.6. Descendants of QPaintDevice

Let’s stop for a moment and take a look at the three classes we have used that are derived from QPaintDevice.

Whenever we initialize a QPainter, we use the function signature:

    QPainter(QPaintDevice * device)

and so it is instructive to look at a tiny snippet of the Qt class diagram:

QPaintDeviceChildren
Figure 10. Descendants of QPaintDevice

1.7. Implementing a Timer

Often the image requires updating even when there are no user events. The display may for example include a clock.

timer
Figure 11. Defining timer events

An instance of QBasicTimer will generate events to our QWidget instance. These events will in turn be handled by timerEvent(QTimerEvent *). In that function we simply trigger an update to the image by invoking QWidget::update(). Since the render() function needs access to a time variable, we initialize a QElapsedTimer instance and call QElapsedTimer::elapsed() in render().

class Hello_Segment : public RefinedQWidget
{
public:
    Hello_Segment(QWidget * parent = 0)
        : RefinedQWidget(parent)
    {
        basic_timer.start(5, this); // interval between drawings
        elapsed_timer.start();      // time since start
    }

protected:
    virtual QRectF getUserRectF() const
    {
        return QRectF(QPointF(-1.0, -1.0), QPointF(1.0, 1.0));
    }
    virtual void paintEvent(QPaintEvent * /*event*/)
    {
        render(this);
    }
    virtual void render(QPaintDevice * device)
    {
        QPainter painter(device);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setPen(QPen(Qt::black, 0));
        painter.setTransform(getUserToWidgetTransform(device));

        const QPointF O( 0.0,  0.0);
        qreal minutes = - elapsed_timer.elapsed() / 60.0;
        qreal hours   = minutes / 12.0;
        const qreal hour_length = 0.6;
        const qreal minute_length = 0.9;
        QPointF H(  hour_length * std::cos(hours),     hour_length * std::sin(hours));
        QPointF M(minute_length * std::cos(minutes), minute_length * std::sin(minutes));
        painter.drawLine(O, H);
        painter.drawLine(O, M);
    }
    virtual void timerEvent(QTimerEvent * /*event*/)
    {
        update();
    }
private:
    QBasicTimer basic_timer;
    QElapsedTimer elapsed_timer;
};

1.8. Adding a Push Button

Adding a push button will let us discuss a central feature of Qt’s user interface model: signals and slots. The mechanics of inserting a push button widget are straight-forward and quite similar to the traditional approach in many user interface libraries. A QVBoxLayout captures a vertical box layout and holds two widget objects: an instance of Hello_Segment and an instance of QPushButton. Creating an independent QWidget instance and setting its layout to the instance of QVBoxLayout thus defined is all we need to add a push button.

We now need a method for handling the button events to some receiver. Qt provides a set of static functions QObject::connect(…​) for this objective. The connection is concise, lucid, and efficient. A call to QObject::connect with the object sending the user events (button) and the signal that that object will issue when triggered (QPushButton::released()), as well as the receiving object (an instance of Hello_Segment) and the function that needs to be called to receive the input event (Hello_Segment::toggle_circle()) suffices for the setup. Whenever the user releases the push button, the released() function will in turn trigger a call to the toggle_circle() in our widget. For an example we toggle adding a circle around the clock. The drawback is that the call to QObject::connect depends on two macros SIGNAL and SLOT. These two macros in turn mean that we cannot roll our own compile-then-link steps but must rely on Qt’s qmake to compile the project (or else call Qt’s moc, meta object compiler, ourselves). More seriously, it becomes possible for the signal-to-slot connection not to match our code, leaving it possible for a user event not to be processed.

int main(int argc, char * argv[])
{
    QApplication app(argc, argv);

    QVBoxLayout * vLayout = new QVBoxLayout;

    Hello_Segment * hs = new Hello_Segment;
    vLayout->addWidget(hs);

    QPushButton * button = new QPushButton("Toggle &Circle");
    vLayout->addWidget(button);

    QObject::connect(button, SIGNAL(released()), hs, SLOT(toggle_circle()));

    QWidget * widget = new QWidget;
    widget->setLayout(vLayout);
    widget->show();

    return app.exec();
}

To define a slot, or a function that will receive a signal, we define block public slots in a child of QObject. The toggle_circle() function will now be called whenever the button is released.

class Hello_Segment : public RefinedQWidget
{
    Q_OBJECT
public:
    Hello_Segment(QWidget * parent = 0)
        : RefinedQWidget(parent),
          draw_circle(false)
    {
        timer.start(5, this);
        elapsed_timer.start();
    }

public slots:
    void toggle_circle()
    {
        draw_circle = !draw_circle;
    }

protected:
    virtual QRectF getUserRectF() const
    {
        return QRectF(QPointF(-1.0, -1.0), QPointF(1.0, 1.0));
    }
    virtual void paintEvent(QPaintEvent * /*event*/)
    {
        render(this);
    }
    virtual void render(QPaintDevice * device)
    {
        QPainter painter(device);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setPen(QPen(Qt::black, 0));
        painter.setTransform(getUserToWidgetTransform(device));

        const QPointF O( 0.0,  0.0);
        qreal minutes = - elapsed_timer.elapsed() / 60.0;
        qreal hours   = minutes / 12.0;
        const qreal hour_length = 0.6;
        const qreal minute_length = 0.9;
        QPointF H(  hour_length * std::cos(hours),     hour_length * std::sin(hours));
        QPointF M(minute_length * std::cos(minutes), minute_length * std::sin(minutes));
        painter.drawLine(O, H);
        painter.drawLine(O, M);

        if(draw_circle)
            painter.drawEllipse(QRectF(-1,-1,2,2));
    }
    virtual void timerEvent(QTimerEvent * /*event*/)
    {
        update();
    }

1.9. Scribble Circles

Let’s look at the three central functions for drawing on a QWidget.

void QWidget::mousePressEvent(QMouseEvent * event);
void QWidget::mouseReleaseEvent(QMouseEvent * event);
void QWidget::mouseMoveEvent(QMouseEvent * event);

These three functions are triggered when the mouse button is pressed or released, and when the mouse coordinates change.

We will insert a small circles at every mouse movement, which will help us illustrate the density of events obtained on a given machine.

Hello Segment
Figure 12. Drawing circles

In the following code we use the widget_to_world variable that we have so far maintained (but did not use) in RefinedQWidget. Notice that in this case we save our objects (the centers of the circles) using world-coordinates. Since a conversion from world to widget is needed at each render() event, we may very well find it advantageous to cache the widget coordinates instead of (or in addition to) the world coordinates.

    virtual void render(QPaintDevice * device)
    {
        QPainter painter(device);
        painter.setRenderHint(QPainter::Antialiasing, true);
        // userToWidgetTransform is updated only when the QWidget is resized.
        painter.setPen(QPen(Qt::black, 0.01));
        painter.setTransform(userToWidgetTransform);

        painter.drawRoundedRect(QRectF(QPointF(-0.99,-0.99),
                                       QSizeF(1.98, 1.98)), 0.05, 0.05);

        typedef vector<QPointF>::const_iterator Point_Iterator;
        for(Point_Iterator p = points.begin(); p != points.end(); ++p)
            painter.drawEllipse(*p, 0.02, 0.02);
    }

    void mousePressEvent(QMouseEvent * /*event*/)
    {
        mouse_is_pressed = true;
    }
    void mouseMoveEvent(QMouseEvent * event)
    {
        if(mouse_is_pressed) {
            QTransform widget_to_world = userToWidgetTransform.inverted();
            QPointF p = widget_to_world.map(event->localPos()); // Qt 5
            // QPointF p = widget_to_world.map(event->posF()); // Qt 4
            points.push_back(p);
            update();
        }
    }
    void mouseReleaseEvent(QMouseEvent * /*event*/)
    {
        mouse_is_pressed = false;
    }

    bool mouse_is_pressed;
    vector<QPointF> points;

1.10. Draw a Segment in S2

Qt’s model also makes it easy to draw in other than Euclidean geometry. An introduction to visualization in spherical geometry or in hyperbolic geometry is rather outside the scope of this introduction. Here we use components of a spherical geometry library, which will make our task particularly easy.

First we define the user interface. We start by observing that the points on the boundary of the unit circle do belong to spherical geometry. In an orthogonal projection of the sphere these points correspond to points on the great circle parallel to the view plane.

It is unnecessary to expect the user to aim precisely on a point on the boundary of the circle. We map the points outside the circle to their nearest point on the boundary. This is the objective of the normalization step calc_spherical_point in the code below.

As we will see in the arcball interface in the next section, manipulation of these points is particularly useful to rotate 3D objects around an axis orthogonal to the view plane.

The following three figures illustrate the three cases of spherical segments.

Segment S2 1
Figure 13. Both segment endpoints are inside the unit circle.
Segment S2 2
Figure 14. One endpoint is outside the unit circle.
Segment S2 3
Figure 15. Both endpoints are outside the unit circle.

In the code below we first map from widget to user coordinates, then we use calc_spherical_point to compute the spherical point. The if statement normalizes so we can capture inputs outside the sphere.

class Segment_S2_Widget : public RefinedQWidget
{
public:
    Segment_S2_Widget(QWidget * parent = 0)
        : RefinedQWidget(parent)
    {}

protected:
    virtual QSize sizeHint() const { return QSize(500, 500); }

    virtual QRectF getUserRectF() const
    {
        return QRectF( QPointF(-1.1, -1.1), QPointF(1.1, 1.1) );
    }

    virtual void render(QPaintDevice * device)
    {
        QPainter painter(device);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setTransform( userToWidgetTransform );

        painter.setPen(QPen(Qt::lightGray, 0.02));
        painter.drawEllipse(QPointF(0.0,0.0), 1.0, 1.0);

        painter.setPen(QPen(Qt::black, 0.02));
        if(!P0.isNull()) {
            QPainterPath path;
            path.moveTo(P0);
            for(qreal f=0.0; f<1.0; f+=0.01) {
                Point_S2d p = interpolate(P0_S2d, P1_S2d, f);
                path.lineTo(p.x(), p.y());
            }
            path.lineTo(P1);
            painter.drawPath(path);
        }
    }

    // Use QMouseEvent.localPos() with Qt5.
    // Use QMouseEvent.posF() with Qt4, if needed.

    void mousePressEvent(QMouseEvent * event)
    {
        P0 = P1 = widgetToUserTransform.map(event->localPos());
        P0_S2d = P1_S2d = calc_spherical_point(P0);
        update();
    }
    void mouseMoveEvent(QMouseEvent * event)
    {
        P1 = widgetToUserTransform.map(event->localPos());
        P1_S2d = calc_spherical_point(P1);
        update();
    }
    void mouseReleaseEvent(QMouseEvent * event)
    {
        P1 = widgetToUserTransform.map(event->localPos());
        update();
    }
    Point_S2d calc_spherical_point(const QPointF & P)
    {
        qreal x = P.x(); qreal y = P.y();
        qreal r = x*x + y*y;
        qreal z;
        if(r > 1.0) {
            x /= sqrt(r);
            y /= sqrt(r);
            z = 0.0;
        }
        else
            z = std::sqrt(1.0 - r);
        return Point_S2d(x, y, z);
    }
    QPointF P0;
    QPointF P1;
    Point_S2d P0_S2d;
    Point_S2d P1_S2d;
};

1.11. An Arcball Interface

We now revise the class Segment_S2_Widget into a class Arcball_Widget. We will in turn derive from Arcball_Widget a class Tetrahedron that draws a wireframe tetrahedron.

arcball tetra
Figure 16. Using a spherical segment for an arcball interface

The spherical segment is drawn during dragging to illustrate the rotation. Recall that the arcball interface manipulates a 3D object using a spherical segment by applying a rotation about the axis orthogonal to the segment’s plane at twice the angle. Thus a rotation defined by half a full circle (defined by two antipodal points) performs a full turn rotation.

class Arcball_Widget : public RefinedQWidget
{
public:
    Arcball_Widget(QWidget * parent = 0)
        : RefinedQWidget(parent), dragging(false)
    {}

protected:
    virtual QSize sizeHint() const { return QSize(550, 500); }

    virtual QRectF getUserRectF() const
    {
        return QRectF( QPointF(-1.1, -1.1), QPointF(1.1, 1.1) );
    }

    virtual void render(QPaintDevice * device)
    {
        QPainter painter(device);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setTransform( userToWidgetTransform );

        if(dragging) {
            painter.setPen(QPen(Qt::lightGray, 0.02));
            painter.drawEllipse(QPointF(0.0,0.0), 1.0, 1.0);

            painter.setPen(QPen(Qt::black, 0.02));

            QPainterPath path;
            path.moveTo(P0);
            for(qreal f=0.0; f<1.0; f+=0.01) {
                Point_S2d p = interpolate(P0_S2d, P1_S2d, f);
                path.lineTo(p.x(), p.y());
            }
            path.lineTo(P1);
            painter.drawPath(path);
        }
    }

    void mousePressEvent(QMouseEvent * event)
    {
        dragging = true;

        P0 = P1 = widgetToUserTransform.map(event->localPos());
        P0_S2d = P1_S2d = calc_spherical_point(P0);
        update();
    }
    void mouseMoveEvent(QMouseEvent * event)
    {
        P1 = widgetToUserTransform.map(event->localPos());
        P1_S2d = calc_spherical_point(P1);
        update();
    }
    void mouseReleaseEvent(QMouseEvent * event)
    {
        dragging = false;

        P1 = widgetToUserTransform.map(event->localPos());
        update();
    }
    Point_S2d calc_spherical_point(const QPointF & P)
    {
        qreal x = P.x(); qreal y = P.y();
        qreal r = x*x + y*y;
        qreal z;
        if(r > 1.0) {
            x /= sqrt(r);
            y /= sqrt(r);
            z = 0.0;
        }
        else
            z = std::sqrt(1.0 - r);
        return Point_S2d(x, y, z);
    }
    QPointF P0;
    QPointF P1;
    Point_S2d P0_S2d;
    Point_S2d P1_S2d;
    bool dragging;
};
class Tetra : public Arcball_Widget
{
public:
    Tetra(QWidget * parent = 0)
        : Arcball_Widget(parent)
    {
        pnts.push_back( Point_S2d( 0.4,   0.4,   0.4) );
        pnts.push_back( Point_S2d(-0.4,   0.4,  -0.4) );
        pnts.push_back( Point_S2d(-0.4,  -0.4,   0.4) );
        pnts.push_back( Point_S2d( 0.4,  -0.4,  -0.4) );

        edges.push_back( make_pair(0,1) );
        edges.push_back( make_pair(1,2) );
        edges.push_back( make_pair(0,2) );
        edges.push_back( make_pair(0,3) );
        edges.push_back( make_pair(1,3) );
        edges.push_back( make_pair(2,3) );
    }
protected:
    virtual void render(QPaintDevice * device)
    {
        Arcball_Widget::render(device);
        Transformation_S2d rot = calc_current_rotation();
        rot = rot * accumulation_of_previous_rotations;

        QPainter painter(device);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setPen(QPen(Qt::black, 0.02));
        painter.setTransform( userToWidgetTransform );

        vector<Point_S2d> pt(4);
        typedef vector<Point_S2d>::const_iterator PcIt;
        typedef vector<Point_S2d>::iterator PIt;
        PcIt pi=pnts.begin();
        PIt pj=pt.begin();

        while(pi!=pnts.end())
        {
            *pj = rot.transform(*pi);
            ++pi; ++pj;
        }

        typedef vector<pair<int, int> >::const_iterator EIt;
        for(EIt ei=edges.begin(); ei!=edges.end(); ++ei)
        {
            painter.drawLine( QPointF(pt[ei->first].x(), pt[ei->first].y()),
                              QPointF(pt[ei->second].x(), pt[ei->second].y()) );
        }

        for(PcIt pj=pt.begin(); pj!=pt.end(); ++pj)
        {
            painter.drawEllipse(QPointF(pj->x(), pj->y()), 0.04, 0.04);
        }
    }

    void mouseReleaseEvent(QMouseEvent * event)
    {
        Arcball_Widget::mouseReleaseEvent(event);

        accumulation_of_previous_rotations = calc_current_rotation() * accumulation_of_previous_rotations;

        P0 = P1;
        P0_S2d = P1_S2d;

        update();
    }

    Transformation_S2d calc_current_rotation()
    {
        double qx, qy, qz;
        cross_product(P0_S2d.x(), P0_S2d.y(), P0_S2d.z(),
                      P1_S2d.x(), P1_S2d.y(), P1_S2d.z(),
                      qx, qy, qz);
        double qw = inner_product(P1_S2d, P0_S2d);
        Quaternion_d q(qw, qx, qy, qz);
        Transformation_S2d rot(q);
        return rot;
    }

    vector<Point_S2d> pnts;
    vector<pair<int,int> > edges;
    Transformation_S2d accumulation_of_previous_rotations;
};

1.12. Input QPolygonF in E2

We conclude this section on immediate rendering using Qt by illustrating how to detect a keyboard modifier key event presed during a mouse press event. To draw an n-sided polygon, the user clicks on the endpoints without a modifier key. To signal entering the last vertex, the user clicks the control key. As the code shows, this is detected in the condition if(event→modifiers() != Qt::ControlModifier).

class Input_Polygon : public RefinedQWidget
{
public:
    Input_Polygon(QWidget * parent = 0)
        : RefinedQWidget(parent)
    {}

protected:
    static int randc() // return a random number from 0..255
    {
        return  int( double(qrand())/double(RAND_MAX) * 256.0 );
    }
    virtual QSize sizeHint() const { return QSize(500, 500); }
    virtual QRectF getUserRectF() const
    {
        return QRectF( QPointF(-4.0, -4.0), QPointF(4.0, 4.0) );
    }

    void mousePressEvent(QMouseEvent * event)
    {
        if(current_polygon.size() == 0) {
            // We insert two points and update the second/last in mouseMoveEvent().
            current_polygon.push_back(widgetToUserTransform.map(event->localPos()));
            current_polygon.push_back(widgetToUserTransform.map(event->localPos()));
            setMouseTracking(true);
        }
        else {
            QPointF last = widgetToUserTransform.map(event->localPos());
            if(event->modifiers() != Qt::ControlModifier)
                // Control not pressed ==> insert new point
                current_polygon.push_back( last );
            else {
                // Control pressed ==> final update of last new point
                current_polygon[current_polygon.size() - 1] = widgetToUserTransform.map(event->localPos());
                setMouseTracking(false);

                // Save to container.
                QColor c(randc(), randc(), randc(), 127);
                polygons.push_back( std::make_pair(current_polygon, c) );

                current_polygon.clear();
            }
        }
        update();
    }

    void mouseMoveEvent(QMouseEvent * event)
    {
        if(current_polygon.size() >= 2)
            current_polygon[current_polygon.size() - 1] = widgetToUserTransform.map(event->localPos());
        update();
    }

    virtual void paintEvent(QPaintEvent * /*event*/)
    {
        render(this);
    }
    void drawAxes(QPainter& painter)
    {
        const QRectF window(getUserRectF());
        const QRectF device_window(rect());
        QRectF w2 = get_centered_window(window, device_window);
        painter.drawLine(QPointF(w2.left(),0), QPointF(w2.right(),0)); // X
        painter.drawLine(QPointF(0,w2.bottom()), QPointF(0,w2.top())); // Y
        painter.drawEllipse( QRectF(QPointF(-1,-1), QSize(2,2)) ); // unit circle
    }
    virtual void render(QPaintDevice * device)
    {
        QPainter painter(device);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setTransform( userToWidgetTransform );
        painter.setPen(QPen(Qt::black, 0.02));

        drawAxes(painter);

        painter.setPen(QPen(Qt::black, 0.02));
        painter.setBrush(QColor(255, 255, 0, 127));
        painter.drawPolygon(current_polygon);

        typedef PC_Container::const_iterator It;
        for(It i = polygons.begin(); i != polygons.end(); ++i) {
            painter.setBrush(i->second);
            painter.drawPolygon(i->first);
        }
    }

    QPolygonF current_polygon;

    typedef QVector<std::pair<QPolygonF, QColor> > PC_Container;

    PC_Container polygons;
};

The following image illustrates a sample drawing.

input QPolygonF in E2
Figure 17. Drawing a polygon in E2

2. Retained Mode

In immediate mode we were concerned with creating the drawing primitives, such as class Tetrahedron, as well as overriding QWidget::paintEvent() to perform the painting. Whenever a suitable object, such as QPolygonF, was already defined in Qt, we ensured that paintEvent() painted the relevant objects. But what if we’d rather avoid creating new objects and preferred to customize from a predefined set of objects? If all our objects are, for example, ellipses and line segments, we would rather simply create instances of these objects without having to define how they are drawn. That behavior would be already defined.

Retained mode rendering delegates the rendering to objects. Each object must know how to draw itself. We construct a scene that consists of the objects to be drawn and a view that consists of their rendering. The scene is an instance of QGraphicsScene; the view is an instance of QGraphicsView; and the drawing objects are instances of descendants of the (abstract) QGraphicsItem. Qt provides concrete children of QGraphicsItem. For example, QGraphicsEllipseItem captures an ellipse and QGraphicsPathItem captures a polyline.

2.1. Draw a Rectangle

To see the different programming style in retained mode, we look at a trivial example of rendering a rectangle. QGraphicsScene makes available the function QGraphicsScene::addItem(…​), as well as the simpler functions QGraphicsScene::addRect(…​) and QGraphicsScene::addEllipse(…​). Once a rectangle is added, rendering the scene will include rendering that rectangle. The view is initialized with a pointer to the scene, and the boundary of the region to be rendering by that view is defined.

int main(int argc, char * argv[])
{
    QApplication app(argc, argv);

    QGraphicsScene scene;
    scene.addRect(QRectF(QPointF(0,0), QPointF(300,200)));

    QGraphicsView view(&scene);
    view.setSceneRect(QRectF(QPointF(-10,-10), QPointF(310,210)));
    view.show();

    return app.exec();
}

We already see that centering the view within the widget while maintaining the aspect ratio is behavior that we obtain for free.

draw rect
Figure 18. Rendering a rectangle in a view
draw rect stretched vertically
Figure 19. Stretching the widget vertically
draw rect stretched horizontally
Figure 20. Stretching the widget horizontally

2.2. Input QPointF in E2

Another example will illustrate the reduced code base achievable with retained mode rendering. We derive a class MyGraphicsView from QGraphicsView and maintain two member variables.

    QPointF point;
    bool mouseIsPressed;

In the three mouse event functions for press/move/release we use QGraphicsView::mapToScene(const QPoint& point) to convert from widget coordinates to world coordinates. The current point is rendered while it’s dragged. It is then added to the scene when the mouse is released.

    void mousePressEvent(QMouseEvent * mouseEvent)
    {
        point = mapToScene(mouseEvent->pos());
        mouseIsPressed = true;
        update();
    }
    void mouseMoveEvent(QMouseEvent * mouseEvent)
    {
        point = mapToScene(mouseEvent->pos());
        update();
    }
    void mouseReleaseEvent(QMouseEvent * mouseEvent)
    {
        point = mapToScene(mouseEvent->pos());
        QGraphicsEllipseItem * ellipse = new QGraphicsEllipseItem(draggedRect());
        ellipse->setPen(QPen(Qt::black, 0.1));
        scene()->addItem(ellipse);
        mouseIsPressed = false;
        update();
    }
input QPointF in E2
Figure 21. Entering a set of points

2.3. Event-Reacting Items

Consider rendering two dependent items. For example, suppose we wish to draw a point in the complex plane and its conjugate. If the user manipulates either of the two points, the relationship between the two points should remain invariant.

complex and conjugate
Figure 22. Tying a complex number to its conjugate

We derive a class Node from QGraphicsEllipseItem, which is itself a child of QGraphicsItem. Node will capture the complex point as well as its conjugate. The function QGraphicsItem::itemChange(GraphicsItemChange change, …​) will react to a Node being dragged and will propagate the motion to the connected Node by testing whether change == QGraphicsItem::ItemPositionChange.

class Node : public QGraphicsEllipseItem
{
public:
    Node(bool _is_control,
         const QColor & c,
         const QString & t,
         bool _is_x_constrained);

protected:
    virtual QVariant itemChange(GraphicsItemChange change, const QVariant & value);
    virtual void paint(QPainter * painter, const QStyleOptionGraphicsItem * option,  QWidget * widget);

private:
    static bool update_underway;

    bool is_control;
    QColor color;
    QString title;

    bool is_x_constrained;

    static const double radius;
};

2.4. Draw a Graph

We conclude with an example of using signals and slots in a child of QGraphicsScene. Our objective is to maintain a graph when the nodes are moved.

mini graph
Figure 23. Maintaining a graph

We again override QGraphicsItem::itemChange, but this time we emit a signal node_has_moved() from a Node object.

QVariant Node::itemChange(GraphicsItemChange change, const QVariant & value)
{
    // Use neither ItemPositionChange nor ItemScenePositionHasChanged, but:
    if(change == QGraphicsItem::ItemPositionChange)
        emit(node_has_moved());

    return QGraphicsItem::itemChange(change, value);
}

The signal will be caught by the function MyGraphicsScene::node_has_moved(). That function maintains the lines representing the edges in the graph.

Node *
MyGraphicsScene::insert_node(const QPointF & pos)
{
    Node * node = new Node;
    addItem(node);
    node->setPos(pos);
    QObject::connect(node, SIGNAL(node_has_moved()), SLOT(node_has_moved()));
    return node;
}

// public slots:
void
MyGraphicsScene::node_has_moved(void)
{
    line12->setLine(QLineF(node1->pos(), node2->pos()));
    line23->setLine(QLineF(node2->pos(), node3->pos()));
    update();
}