Journal Articles

CVu Journal Vol 16, #6 - Dec 2004 + Programming Topics
Browse in : All > Journals > CVu > 166 (12)
All > Topics > Programming (877)
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: Writing Custom Widgets in Qt

Author: Administrator

Date: 03 December 2004 13:16:10 +00:00 or Fri, 03 December 2004 13:16:10 +00:00

Summary: 

In the fourth installment of our series on cross-platform GUI programming with the Qt C++ toolkit, we are going to write a custom widget using Qt. The widget in question is a "scribble" widget (see Figure 1) - that is, the drawing area of a simple paint program

Body: 

In the fourth installment of our series on cross-platform GUI programming with the Qt C++ toolkit, we are going to write a custom widget using Qt. The widget in question is a "scribble" widget (see Figure 1) - that is, the drawing area of a simple paint program. The user can draw by moving the mouse pointer while holding down the left mouse button.

The Scribble Widget

Figure 1. The Scribble Widget

Writing a custom widget using Qt isn't much different from writing an application's main window (C Vu Volume 16 No 3) or a dialog (C Vu Volume 16 No 4). It also involves deriving from a Qt base class, reimplementing some virtual functions, and connecting signals to slots. The main difference is that we also need to handle low-level events (also called "messages") such as paint events and mouse events to give the widget its look and feel.

The Scribble Class Definition

We'll start by looking at the definition of the Scribble class:

#ifndef SCRIBBLE_H
#define SCRIBBLE_H
#include <qimage.h>
#include <qwidget.h>

class Scribble : public QWidget {
public:
  Scribble(QWidget *parent = 0);
  QSize sizeHint() const;
  void setPixmap(const QPixmap &pixmap);
  QPixmap pixmap() const { return m_pixmap; }
  void setPenColor(const QColor &color);
  QColor penColor() const { return m_color; }
protected:
  void mousePressEvent(QMouseEvent *event);
  void mouseMoveEvent(QMouseEvent *event);
  void paintEvent(QPaintEvent *event);
private:
  QColor m_color;
  QPixmap m_pixmap;
  QPoint m_prevPos;
};
#endif

The Scribble class inherits from QWidget, the base class for all widgets and windows. Scribble provides public access functions, three protected event handles, and some private variables.

The m_color data member holds the current pen colour. The m_pixmap member holds the image that the user is drawing. The m_prevPos member will be explained later; just ignore it for the moment.

The protected event handles are virtual functions inherited from QWidget that are called whenever the widget receives an event. Events are sent by the window system whenever some condition occurs. For example, if the user presses a key while the widget has the keyboard focus, the window system dispatches a "key press" event that the widget can handle by reimplementing QWidget::keyPressEvent(). The Scribble widget is interested in three kinds of event: "mouse press", "mouse move" and "paint" events.

The Scribble Class Implementation

We will now go through the implementation of the Scribble class, starting with the constructor:

Scribble::Scribble(QWidget *parent)
    : QWidget(parent) {
  m_color = black;
  m_pixmap.resize(480, 320);
  m_pixmap.fill(0xFFFFFF);
  setWFlags(WStaticContents);
}

The constructor takes a parent widget and passes it on to the base class constructor. If parent is a null pointer, the widget is a window in its own right; otherwise the widget is displayed within the parent's area.

In the constructor body we initialize the m_color and m_pixmap data members to default values. The pen colour is set to black; the pixmap is initialized to size 480 × 320 and filled with white (0xFFFFFF). Finally we set the WStaticContents flag on the widget, telling Qt that the widget's content doesn't scale when the widget is resized, but rather it stays rooted in the top-left corner. This simple trick lets Qt optimize drawing and reduce flicker drastically.

QSize Scribble::sizeHint() const {
  return m_pixmap.size();
}

The sizeHint() function is reimplemented from QWidget. It should return the ideal size of a widget. Layout managers take this into account when assigning screen positions to widgets. Here we return the size of the pixmap (480 × 320 by default) as the ideal size for the widget.

void Scribble::setPixmap(const QPixmap
                                    &pixmap) {
  m_pixmap = pixmap;
  update();
  updateGeometry();
}

The setPixmap() function sets the pixmap which the user can draw on. Notice that we call update() and updateGeometry() in addition to assigning the new pixmap to m_pixmap. The call to update() tells Qt to repaint the widget, ensuring that the new pixmap is shown straight away. The call to updateGeometry() tells the layout manager responsible for this widget (if any) that the sizeHint() might have changed.

void Scribble::setPenColor(const QColor
                                     &color) {
  m_color = color;
}

The setPenColor() function sets the current pen colour. This time we don't need to call update() because the operation doesn't affect the screen rendering of the widget (it only affects pixels that the user will draw in the future). We don't need to call updateGeometry() either because m_color isn't used when computing the size hint.

void Scribble::mousePressEvent(QMouseEvent
                                     *event) {
  if(event->button() == LeftButton)
    m_prevPos = event->pos();
}

The mousePressEvent() function is called whenever the user presses a mouse button while the mouse pointer is located on the widget. The event parameter gives additional information, such as the button that was pressed (button()) and the screen position of the mouse cursor when the button was pressed (pos()). If the user pressed the left button, we store the mouse position in m_prevPos for later use.

void Scribble::mouseMoveEvent(QMouseEvent
                                     *event) {
  if(event->state() & LeftButton) {
    QPainter painter(&m_pixmap);
    painter.setPen(QPen(m_color, 3));
    painter.drawLine(m_prevPos, event->pos());

    QRect rect(m_prevPos, event->pos());
    rect = rect.normalize();
    update(rect.x() - 1, rect.y() - 1,
           rect.width() + 2,
           rect.height() + 2);

    m_prevPos = event->pos();
  }
}

The mouseMoveEvent() function is called continuously when the user moves the mouse pointer while holding down a mouse button. The typical sequence of events is one "mouse press" event when the user presses a button, then a series of "mouse move" events that describe the path taken by the mouse pointer, and finally a "mouse release" event when the user releases the button.

We check if the left button is one of the buttons that are currently pressed. If this is the case we update m_pixmap and repaint the widget using update().

We create a QPainter to draw on the pixmap. We set the pen to have the correct colour (m_color) and a thickness of 3 pixels. Then we draw a line from the previous mouse position (m_prevPos) to the new mouse position (event->pos()).

QPainter is the entrance door to Qt's paint engine. It provides functions to draw all sorts of geometric shapes (rectangles, circles, pie sections, Bezier curves, etc.) and supports transformations such as rotating and scaling. A QPainter object can be used to draw on a pixmap, a widget, a vector diagram or a printer.

Once we're done updating the pixmap we must update the on-screen version. The reductionist approach would be to call update() with no argument and be done with it; this would tell Qt to redraw the entire widget area, a somewhat expensive operation. Instead we compute the bounding rectangle for the line segment we just drew and pass it to update().

At the end of the function, we update m_prevPos so that the next "mouse move" event will prolong the line segment we just drew.

void Scribble::paintEvent(QPaintEvent *event) {
  QPainter painter(this);
  painter.drawPixmap(0, 0, m_pixmap);
}

The paintEvent() function is called whenever the widget must be repainted. This can occur if the widget was temporarily obscured by another window and then made visible again, or as a result of calling update(). Here we simply draw the pixmap onto the widget.

At this point you might wonder why we bother drawing on a pixmap then transfer the pixmap onto the widget. Couldn't we draw directly on the widget instead, eliminating the need for m_pixmap? The answer is no. This is because we can't rely on the window system to keep a copy of the widget's pixels if the window is obscured or minimized. A well-behaved widget must implement paintEvent() and be able to redraw itself entirely at any moment.

The Application's Main Window

We are done implementing the custom widget. To make it useful, we need a window around it, with a "Pen Color..." button and a "Quit" button. Here's the class definition:

#ifndef WINDOW_H
#define WINDOW_H

#include <qwidget.h>
class Scribble;

class Window : public QWidget {
  Q_OBJECT
public:
  Window(QWidget *parent = 0);
private slots:
  void choosePenColor();
private:
  Scribble *m_scribble;
};

#endif

We can call the class Window because it will be the only window in the application. The class has one slot, choosePenColor(), which pops up a colour dialog.

Window::Window(QWidget *parent)
    : QWidget(parent) {
  m_scribble = new Scribble(this);
  m_scribble->setSizePolicy(
                QSizePolicy::Expanding,
                QSizePolicy::Expanding);

  QPushButton *penColorButton =
        new QPushButton(tr("Pen Color..."),
                        this);
  QPushButton *quitButton =
        new QPushButton(tr("Quit"), this);

  connect(penColorButton, SIGNAL(clicked()),
          this, SLOT(choosePenColor()));
  connect(quitButton, SIGNAL(clicked()),
          this, SLOT(close()));

  QGridLayout *layout = new QGridLayout(this);
  layout->setMargin(10);
  layout->setSpacing(5);

  layout->addMultiCellWidget(m_scribble, 0, 2,
                             0, 0);
  layout->addWidget(penColorButton, 0, 1);
  layout->addWidget(quitButton, 1, 1);

  setCaption(tr("Scribble"));
}

In the constructor we create three child widgets (the scribble area and two push buttons), connect the "Pen Color..." button to the choosePenColor() slot, connect the "Quit" button to the window's close() slot, and put the child widgets in a grid layout. Figure 2 shows how the child widgets are laid out in the grid cells.

Grid Layout of Child Widgets

Figure 2. Grid Layout of Child Widgets

void Window::choosePenColor() {
  QColor color =
       QColorDialog::getColor(
                m_scribble->penColor(), this);
  if(color.isValid())
    m_scribble->setPenColor(color);
}

When the user clicks "Pen Color...", we pop up a QColorDialog that allows the user to select a pen colour. We pass the old pen colour to the dialog as the initial value.

This is all the code we need in Window. To complete the application, we need a main() function:

int main(int argc,
         char *argv[]){
  QApplication
       app(argc, argv);
  Window win;
  app.setMainWidget(
                 &win);
  win.show();
  return app.exec();
}

That's it! One of Qt's striking features is how easy it is to create custom widgets. In fact all of Qt's built-in widgets (e.g. QPushButton and QColorDialog) are implemented using the techniques described in this article. While with other toolkits writing custom widgets is considered an advanced topic, in Qt it is so easy that it is taught straight away to beginners as an introduction to the Qt way of thinking.

The Colour Dialog

Figure 3. The Colour Dialog

Notes: 

More fields may be available via dynamicdata ..