4.2. Painting Widgets

Although visible changes to the widget can happen at various times during the life of your application, you should paint only during a paintEvent(). The drawing you do in paintEvent() is done with the QPainter class. It offers pixel addressing, drawing primitives, text drawing and other, more advanced functions. Widget drawing needs to be done efficiently to provide a smooth, understandable GUI, and mechanisms are provided by Qt for doing so.

4.2.1. When Painting Occurs

The paintEvent() method is called automatically when

  • Your widget is shown for the first time.

  • After a window has been moved to reveal some part (or all) of the widget.

  • The window in which the widget lies is restored after being minimized.

  • The window in which the widget lies is resized.

  • The user switches from another desktop to the desktop on which the widget's window lies.

You can generate paint events manually by calling QWidget::update(). QWidget::update() erases the widget before generating the paint event. You can pass arguments to update(), which can restrict painting only to areas (rectangles, in particular) that need it. The two equivalent forms of the method are QWidget::update (int x, int y, int width, int height) and QWidget::update (QRect rectangle), where x and y give the upper-left corner of the rectangle, and width and height are obvious. Because update() places a paint event into the event queue, no painting occurs until the current method exits and control returns to the event handler. This is a good thing because other events may be waiting there to be processed, and events need to be processed in a timely manner for the GUI to operate smoothly.

You can also invoke painting of the widget by calling QWidget::repaint (int x, int y, int width, int height, bool erase) (or one of several convenience-method forms), where all the arguments mean the same as in the case of the update() method, and erase tells repaint whether to erase the rectangle before painting it. repaint() calls paintEvent() directly. It does not place a paint event into the event queue, so use this method with care. If you try to call repaint() repeatedly from a simple loop to create an animation, for example, the animation will be drawn, but the rest of your user interface will be unresponsive because the events corresponding to mouse button clicks, keyboard presses, and so on will be waiting in the queue. Even if you are not performing a task as potentially time-consuming as animation, it is generally better to use update() to help keep your GUI alive.

If you paint something on your widget outside the paintEvent(), you still need to include the logic and commands necessary to paint that same thing in paintEvent(). Otherwise, the painting you did would disappear the next time the widget is updated.

4.2.2. Repainting Efficiently

I mentioned earlier in this chapter that update() and repaint() may take arguments describing the rectangle that needs to be updated. The description of this rectangle is passed to paintEvent() through the QPaintEvent class, which is the argument to paintEvent().

For the rectangle information to be useful, you must specifically take advantage of it in your paintEvent(). Unless the painting you do will always be simple and quick, you should take the time to repaint only the rectangle that is requested in the QPaintEvent. There are ways to improve upon this for more difficult repainting tasks, which will be covered in Chapter 9.

4.2.3. Painting Your Widget with QPainter

QPainter is responsible for all the drawing you do with Qt. It is used to draw on widgets and offscreen buffers (pixmaps) and to generate Postscript output for printing. Specifically, QPainter draws on one of the objects derived from QPaintDevice: QWidget, QPixmap, QPrinter, and QPicture.

4.2.4. Recording Drawing Commands with QPicture

QPicture is used for recording drawing commands. The commands can then be "played back" onto another paint device (a widget, a pixmap, or a printer). To make printing easy, you could, in your reimplementation of paintEvent(), record all your drawing commands in a QPicture, and then play them back onto the widget. With the drawing commands still saved in QPicture, you could respond to a print command by replaying the QPicture onto a QPrinter. This is useful only in the simplest cases because

  • Often, the printer output will not be the same as the screen output (consider a typical text editor, for example).

  • For complex enough output, the extra time spent recording and replaying in paintEvent() will incur an unacceptable performance hit (which might be the case with an image-manipulation program).

4.2.5. A Simple Widget

Listings 4.2–4.4 give the code for a simple widget called KXOSquare. This widget draws an X or an O inside a square (see Figure 4.1).

The first time this widget paints itself, it draws a black box around at its border (the box is drawn just inside the widget's borders, actually). When you click the widget with the left mouse button, it draws a blue X inside. When you click the widget with the right mouse button, it draws a red O.

Let's take a look at the class declaration. In Listing 4.2 the widget is, as all widgets are, derived from the class QWidget. The state of the widget is described by one of three enum values: None (the initial state), X, or O. The widget paints itself to reflect its state in the reimplemented method paintEvent(). The state of the widget may be changed by calling the method newState(). This method has been declared as a slot so that the widget may respond to signals in a convenient way to change a widget's state. The widget emits the signal changeRequest() whenever the user clicks the square. It is up to the programmer using this class to use the signal appropriately. In this example, the function main() does the simplest thing and connects the signal to the newState() slot.


Figure 4.1. KXOSquare draws a blue X or a red O in response to mouse clicks.



Example 4.2. kxosquare.cpp is the Class Declaration for KXOSquare, a Widget that Draws an X or an O in a Square

   1 
   2 1: #ifndef __KXOSQUARE_H__
   3 2: #define __KXOSQUARE_H__
   4 3:
   5 4:
   6 5: #include <qwidget.h>
   7 6:
   8 7:
   9 8: /**
  10 9:  * KXOSquare
  11 10:  * Draws a square in one of three states: empty, with an X inside,
  12 11:  *  or with an O inside.
  13 12:  **/
  14 13: class KXOSquare : public QWidget
  15 14: {
  16 15:  Q_OBJECT
  17 16:
  18 17:  public:
  19 18:   enum State {None=0, X=1, O=2};
  20 19: 
  21 20:   /**
  22 21:    * Create the widget.
  23 22:    **/
  24 23:   KXOSquare (QWidget *parent, const char *name=0);
  25 24:
  26 25:   public slots:
  27 26:   /**
  28 27:    * Change the state of the widget to @p state.
  29 28:    **/
  30 29:     void newState (State state);
  31 30:
  32 31:  signals:
  33 32:   /**
  34 33:    * The user has requested that the state be changed to @p state
  35 34:    *  by clicking on the square.
  36 35:    **/
  37 36:     void changeRequest (State state);
  38 37:
  39 38:  protected:
  40 39:     /**
  41 40:      * Draw the widget.
  42 41:      **/
  43 42:     void paintEvent (QPaintEvent *);
  44 43:
  45 44:     /**
  46 45:      * Process mouse clicks.
  47 46:      **/
  48 47:     void mousePressEvent (QMouseEvent *);
  49 48:
  50 49:  private:
  51 50:   State thestate;
  52 51: };
  53 52:
  54 53: #endif

Note

The executable is named kxosquaretest because this is a common way to indicate that the application exists only to test or demonstrate a widget and is not a full-fledged KDE application. I will use this form throughout the book.

Listing 4.3 shows that class definition for the KXOSquare widget.


Example 4.3. kxosquare.cpp: Class Definition for the KXOSquare Widget

   1 
   2 1: #include <qpainter.h>
   3 2:
   4 3: #include "kxosquare.moc"
   5 4:
   6 5: KXOSquare::KXOSquare (QWidget *parent, const char *name=0) :
   7 6:   QWidget (parent, name)
   8 7: {
   9 8:   thestate = None;
  10 9: }
  11 10:
  12 11: void
  13 12: KXOSquare::paintEvent (QPaintEvent *)
  14 13: {
  15 14:   QPainter qpainter (this);
  16 15:
  17 16:   qpainter.drawRect (rect());
  18 17:
  19 18:   switch (thestate)
  20 19:     {
  21 20:     case X:
  22 21:       qpainter.setPen (QPen (Qt::blue, 3));
  23 22:       qpainter.drawLine (rect().x(), rect().y(),
  24 23:           rect().x()+rect().width(), rect().y()+rect().height());
  25 24:       qpainter.drawLine (rect().x(), rect().y()+rect().height(),
  26 25:           rect().x()+rect().width(), rect().y());
  27 26:       break;
  28 27:     case O:
  29 28:       qpainter.setPen (QPen (Qt::red, 3));
  30 29:       qpainter.drawEllipse (rect());
  31 30:       break;
  32 31:     }
  33 32: }
  34 33:
  35 34: void
  36 35: KXOSquare::mousePressEvent (QMouseEvent *mouseevent)
  37 36: {
  38 37:   switch (mouseevent->button())
  39 38:     {
  40 39:     case Qt::LeftButton:
  41 40:       emit changeRequest (X);
  42 41:       break;
  43 42:     case Qt::RightButton:
  44 43:       emit changeRequest (O);
  45 44:       break;
  46 46:
  47 47: }
  48 48:
  49 49: void
  50 50: KXOSquare::newState (State state)
  51 51: {
  52 52:   thestate = state;
  53 53:   update();
  54 54: }

KXOSquare::paintEvent() (lines 11-32) shows a somewhat typical usage of QPainter in a paintEvent(). The QPainter object is created with this as its paint device (see line 14), meaning that it will draw on the KXOSquare widget. The argument, of type QPaintEvent *, is ignored.

Note

Only because the items being drawn are so simple and can be rendered very quickly do you repaint the entire widget. To save time, you should paint only the rectangle specified in the QPaintEvent argument when the widget is complex.

You should do all of your painting inside paintEvent(). Since paint events are sometimes generated by the windowing system and sometimes by your application, you can be sure when paintEvent() will be called. If you make changes to the state of the widget in other methods and do your painting in paintEvent() based on the current state of the widget, then your program's logic will be simpler.

Line 22 draws the black bounding box using the QPainter method drawRect(). Other QPainter methods are also demonstrated:

  • setPen (QPen qpen)

    Sets the color used to draw lines and figure edges.

  • drawLine (int x1, int y1, int x2, int y2)

    Draws a line from the point (x1, y1) to the point (x2, y2).

  • drawEllipse (QRect rect)

    Draws an ellipse that just fits inside the rectangle, rect (that is, the ellipse is tangent to all four sides of the rectangle).

The first QPen, defined by QPen (Qt::blue, 3) in line 21, is blue with a width of 3 pixels. The other QPen, defined in line 28, is red with a width of 3 pixels.

The next listing, Listing 4.4, presents a short main() function that creates and shows the widget. You can compile the whole program with the command


   1 
   2 g++  kxosquare.cpp main.cpp -I$KDEDIR/include
   3 
   4        I/usr/include/qt -L$KDEDIR/lib -lkdecore -lkdeui -o kxosquaretest

The option -o kxosuaretest tells g++ to create an executable with name kxosquaretest.


Example 4.4. main.cpp Contains a main() Function that Can Be Used to Test the Widget KXOSquare

   1 
   2 1: #include <kapp.h>
   3 2:
   4 3: #include "kxosquare.h"
   5 4:
   6 5: int
   7 6: main (int argc, char *argv[])
   8 7: {
   9 8:   KApplication kapplication (argc, argv, "kxosquaretest");
  10 9:   KXOSquare *kxosquare = new KXOSquare (0);
  11 10:
  12 11:   kapplication.setMainWidget (kxosquare);
  13 12:   kxosquare->connect ( kxosquare, SIGNAL (changeRequest (State)),
  14 13:                SLOT (newState (State)) );
  15 14:
  16 15:   kxosquare->show();
  17 16:   return kapplication.exec();
  18 17: }

The signal changeRequest() takes a variable of type KXOSquare * as its second argument because, as you will see next, this provides added flexibility. Notice, however, that in main(), line 11 of main.cpp, I call


   1 
   2 kxosquare.connect ( kxosquare,
   3                       SIGNAL (changeRequest (KXOSquare::State, KXOSquare *)),
   4                       SLOT (newState (KXOSquare::State)) );

The signal and slot don't have the same arguments. In this case that's just fine. It is acceptable for the slot to have fewer arguments than the signal as long as the arguments that are retained match. The following forms are not acceptable:


   1 
   2   connect ( pwidget1, SIGNAL (mysignal (int, char)),
   3             pwidget2, SLOT (myslot (char)) );
   4 
   5   connect ( pwidget1, SIGNAL (mysignal (int, char)),
   6             pwidget2, SLOT (myslot (int, char, char)) );
   7 

The following are acceptable:


   1 
   2   connect ( pwidget1, SIGNAL (mysignal (int, char)),
   3             pwidget2, SLOT (myslot (int, char)) );
   4 
   5   connect ( pwidget1, SIGNAL (mysignal (int, char)),
   6             pwidget2, SLOT (myslot (int)) );