• No results found

Customizing Qt Widgets

Subclassing QWidget

Integrating Custom Widgets with Qt Designer

Double Buffering

This chapter explains how to create custom widgets using Qt. Custom widgets can be created by subclassing an existing Qt widget or by subclassingQWidget

directly. We will demonstrate both approaches, and we will also see how to integrate a custom widget withQt Designerso that it can be used just like a built-in Qt widget. We will round off the chapter by presenting a custom wid- get that uses a powerful technique for eliminating flicker: double buffering.

Customizing Qt Widgets

In some cases, we find that a Qt widget requires more customization than is possible by setting its properties inQt Designeror by calling its functions. A simple and direct solution is to subclass the relevant widget class and adapt it to suit our needs.

Figure 5.1. TheHexSpinBoxwidget

In this section, we will develop a hexadecimal spin box to show how this works.

QSpinBoxonly supports decimal integers, but by subclassing it’s quite easy to make it accept and display hexadecimal values.

#ifndef HEXSPINBOX_H #define HEXSPINBOX_H

#include <qspinbox.h>

class HexSpinBox : public QSpinBox {

protected:

QString mapValueToText(int value); int mapTextToValue(bool *ok); };

#endif

TheHexSpinBox inherits most of its functionality from QSpinBox. It provides a typical constructor and reimplements two virtual functions fromQSpinBox. Since the class doesn’t define its own signals and slots, it doesn’t need theQ_ OBJECTmacro.

#include <qvalidator.h>

#include "hexspinbox.h"

HexSpinBox::HexSpinBox(QWidget *parent, const char *name) : QSpinBox(parent, name)

{

QRegExp regExp("[0-9A-Fa-f]+");

setValidator(new QRegExpValidator(regExp, this)); setRange(0, 255);

}

The user can modify a spin box’s current value either by clicking its up and down arrows or by typing a value into the spin box’s line editor. In the latter case, we want to restrict the user’s input to legitimate hexadecimal numbers. To achieve this, we use aQRegExpValidatorthat accepts one or more characters

from the ranges ‘0’ to ‘9’, ‘A’ to ‘F’, and ‘a’ to ‘f ’. We also set the default range to be 0 to 255 (0x00 to 0xFF), which is more appropriate for a hexadecimal spin box thanQSpinBox’s default of 0 to 99.

QString HexSpinBox::mapValueToText(int value) {

return QString::number(value, 16).upper(); }

ThemapValueToText()function converts an integer value to a string. QSpinBox

calls it to update the editor part of the spin box when the user presses the spin box’s up or down arrows. We use the static functionQString::number()with a second argument of 16 to convert the value to lower-case hexadecimal, and callQString::upper()on the result to make it upper-case.

int HexSpinBox::mapTextToValue(bool *ok) {

return text().toInt(ok, 16); }

ThemapTextToValue()function performs the reverse conversion, from a string to an integer value. It is called byQSpinBoxwhen the user types a value into the editor part of the spin box and pressesEnter. We use theQString::toInt()

Customizing Qt Widgets 99 function to attempt to convert the current text (returned byQSpinBox::text()) to an integer value, again using base 16.

If the conversion is successful,toInt()sets*oktotrue; otherwise, it sets it to

false. This behavior happens to be exactly whatQSpinBoxexpects.

We have now finished the hexadecimal spin box. Customizing other Qt wid- gets follows the same pattern: Pick a suitable Qt widget, subclass it, and reim- plement some virtual functions to change its behavior. This technique is com- mon in Qt programming; in fact, we have already used it in Chapter 4 when we subclassedQTableand reimplementedcreateEditor()andendEdit().

Subclassing QWidget

Most custom widgets are simply a combination of existing widgets, whether they are built-in Qt widgets or other custom widgets such as HexSpinBox. Custom widgets that are built by composing existing widgets can usually be developed inQt Designer:

• Create a new form using the “Widget” template.

• Add the necessary widgets to the form, then lay them out.

• Set up the signals and slots connections and add any necessary code (either in a.ui.hfile or in a subclass) to provide the desired behavior. Naturally, this can also be done entirely in code. Whichever approach is taken, the resulting class inherits directly fromQWidget.

If the widget has no signals and slots of its own and doesn’t reimplement any virtual functions, it is even possible to simply assemble the widget by aggregating existing widgets without a subclass. That’s the approach we used in Chapter 1 to create the Age application, with aQHBox, aQSpinBox, and

aQSlider. Even so, we could just as easily have subclassedQHBoxand created

theQSpinBoxandQSliderin the subclass’s constructor.

When none of Qt’s widgets are suitable for the task at hand, and when there’s no way to combine or adapt existing widgets to obtain the desired result, we can still create the widget we want. This is achieved by subclassingQWidget

and reimplementing a few event handlers to paint the widget and to respond to mouse clicks. This approach gives us complete freedom to define and control both the appearance and the behavior of our widget. Qt’s built-in widgets, likeQLabel,QPushButton, andQTable, are implemented this way. If they didn’t

exist in Qt, it would still be possible to create them ourselves using the public functions provided byQWidgetin a totally platform-independent manner.

To demonstrate how to write a custom widget using this approach, we will create theIconEditorwidget shown in Figure 5.2. TheIconEditoris a widget

that could be used in an icon editing program. Let’s begin by reviewing the header file.

#include <qimage.h> #include <qwidget.h>

class IconEditor : public QWidget {

Q_OBJECT

Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor) Q_PROPERTY(QImage iconImage READ iconImage WRITE setIconImage) Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor)

public:

IconEditor(QWidget *parent = 0, const char *name = 0);

void setPenColor(const QColor &newColor); QColor penColor() const { return curColor; } void setZoomFactor(int newZoom);

int zoomFactor() const { return zoom; } void setIconImage(const QImage &newImage);

const QImage &iconImage() const { return image; } QSize sizeHint() const;

TheIconEditorclass uses theQ_PROPERTY()macro to declare three custom prop- erties:penColor,iconImage, andzoomFactor. Each property has a type, a “read” function, and a “write” function. For example, thepenColorproperty is of type

QColor and can be read and written using thepenColor() andsetPenColor()

functions.

Figure 5.2. TheIconEditorwidget

When we make use of the widget inQt Designer, custom properties appear inQt Designer’s property editor below the properties inherited fromQWidget. Properties may be of any type supported byQVariant. TheQ_OBJECTmacro is necessary for classes that define properties.

protected:

void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void paintEvent(QPaintEvent *event);

Subclassing QWidget 101

private:

void drawImagePixel(QPainter *painter, int i, int j); void setImagePixel(const QPoint &pos, bool opaque);

QColor curColor; QImage image; int zoom; };

#endif

IconEditorreimplements three protected functions fromQWidgetand has a few private functions and variables. The three private variables hold the values of the three properties.

The implementation file begins with#includedirectives and theIconEditor’s constructor:

#include <qpainter.h>

#include "iconeditor.h"

IconEditor::IconEditor(QWidget *parent, const char *name) : QWidget(parent, name, WStaticContents)

{ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); curColor = black; zoom = 8; image.create(16, 16, 32); image.fill(qRgba(0, 0, 0, 0)); image.setAlphaBuffer(true); }

The constructor has some subtle aspects such as thesetSizePolicy()call and theWStaticContentsflag. We will discuss them shortly.

The zoom factor is set to 8, meaning that each pixel in the icon will be rendered as an 8 × 8 square. The pen color is set to black; theblacksymbol is a prede- fined value in theQtclass (QObject’s base class).

The icon data is stored in theimage member variable and can be accessed through thesetIconImage()andiconImage()functions. An icon editor program would typically callsetIconImage()when the user opens an icon file andicon- Image()to retrieve the icon when the user wants to save it.

Theimagevariable is of typeQImage. We initialize it to 16 × 16 pixels and 32-bit depth, clear the image data, and enable the alpha buffer.

TheQImageclass stores an image in a hardware-independent fashion. It can be set to use a 1-bit, 8-bit, or 32-bit depth. An image with 32-bit depth uses 8 bits for each of the red, green, and blue components of a pixel. The remaining 8 bits store the pixel’s alpha component—that is, its opacity. For example, a pure red color’s red, green, blue, and alpha components have the values 255, 0, 0, and 255. In Qt, this color can be specified as

QRgb red = qRgb(255, 0, 0);

QRgbis simply a typedef for unsigned int, and qRgb()and qRgba()are inline functions that combine their arguments into one 32-bit integer value. It is also possible to write

QRgb red = 0xFFFF0000;

where the firstFF corresponds to the alpha component and the secondFF to the red component. In theIconEditor constructor, we fill theQImage with a transparent color by using 0 as the alpha component.

Qt provides two types for storing colors:QRgbandQColor. WhileQRgbis only a typedef used inQImageto store 32-bit pixel data,QColoris a class with many useful functions and is widely used in Qt to store colors. In the IconEditor

widget, we only use QRgb when dealing with the QImage; we use QColor for everything else, including thepenColorproperty.

QSize IconEditor::sizeHint() const {

QSize size = zoom * image.size(); if (zoom >= 3)

size += QSize(1, 1); return size;

}

ThesizeHint()function is reimplemented fromQWidgetand returns the ideal size of a widget. Here, we take the image size multiplied by the zoom factor, with one extra pixel in each direction to accommodate a grid if the zoom factor is 3 or more. (We don’t show a grid if the zoom factor is 2 or 1, because the grid would hardly leave any room for the icon’s pixels.)

A widget’s size hint is mostly useful in conjunction with layouts. Qt’s layout managers try as much as possible to respect a widget’s size hint when they lay out a form’s child widgets. ForIconEditorto be a good layout citizen, it must report a credible size hint.

In addition to the size hint, widgets have a size policy that tells the layout system whether they like to be stretched and shrunk. By callingsetSizePol- icy()in the constructor withQSizePolicy::Minimumas horizontal and vertical policies, we tell any layout manager that is responsible for this widget that the widget’s size hint is really its minimum size. In other words, the widget can be stretched if required, but it should never shrink below the size hint. This can be overridden inQt Designerby setting the widget’ssizePolicyproperty. The meaning of the various size policies is explained in Chapter 6 (Layout Management).

void IconEditor::setPenColor(const QColor &newColor) {

curColor = newColor; }

Subclassing QWidget 103 ThesetPenColor()function sets the current pen color. The color will be used for newly drawn pixels.

void IconEditor::setIconImage(const QImage &newImage) { if (newImage != image) { image = newImage.convertDepth(32); image.detach(); update(); updateGeometry(); } }

ThesetIconImage() function sets the image to edit. We callconvertDepth()

to make the image 32-bit if it isn’t already. Elsewhere in the code, we will assume that the image data is stored as 32-bitQRgbvalues.

We also calldetach()to take a deep copy of the data stored in the image. This is necessary because the image data might be stored in ROM.QImage tries to save time and memory by copying the image data only when explicitly requested to do so. This optimization is calledexplicit sharingand is discussed withQMemArray<T>in the “Pointer-Based Containers” section of Chapter 11. After setting theimagevariable, we callQWidget::update()to force a repainting of the widget using the new image. Next, we callQWidget::updateGeometry()

to tell any layout that contains the widget that the widget’s size hint has changed. The layout will then automatically adapt to the new size hint.

void IconEditor::setZoomFactor(int newZoom) { if (newZoom < 1) newZoom = 1; if (newZoom != zoom) { zoom = newZoom; update(); updateGeometry(); } }

ThesetZoomFactor()function sets the zoom factor for the image. To prevent division by zero later, we correct any value below 1. Again, we callupdate()

andupdateGeometry()to repaint the widget and to notify any managing layout about the size hint change.

ThepenColor(),iconImage(), andzoomFactor()functions are implemented as inline functions in the header file.

We will now review the code for thepaintEvent() function. This function is

IconEditor’s most important function. It is called whenever the widget needs repainting. The default implementation inQWidgetdoes nothing, leaving the widget blank.

is an event handler. Qt has many other event handlers, each of which corresponds to a different type of event. Chapter 7 covers event processing in depth.

There are many situations when a paint event is generated andpaintEvent()

is called:

• When a widget is shown for the first time, the system automatically generates a paint event to force the widget to paint itself.

• When a widget is resized, the system automatically generates a paint event.

• If the widget is obscured by another window and then revealed again, a paint event is generated for the area that was hidden (unless the window system stored the area).

We can also force a paint event by callingQWidget::update()orQWidget::re- paint(). The difference between these two functions is thatrepaint()forces an immediate repaint, whereasupdate()simply schedules a paint event for when Qt next processes events. (Both functions do nothing if the widget isn’t visible on screen.) Ifupdate()is called multiple times, Qt compresses the consecutive paint events into a single paint event to avoid flicker. InIconEditor, we always useupdate().

Here’s the code:

void IconEditor::paintEvent(QPaintEvent *) {

QPainter painter(this);

if (zoom >= 3) {

painter.setPen(colorGroup().foreground()); for (int i = 0; i <= image.width(); ++i) painter.drawLine(zoom * i, 0,

zoom * i, zoom * image.height()); for (int j = 0; j <= image.height(); ++j)

painter.drawLine(0, zoom * j,

zoom * image.width(), zoom * j); }

for (int i = 0; i < image.width(); ++i) { for (int j = 0; j < image.height(); ++j) drawImagePixel(&painter, i, j); }

}

We start by constructing aQPainterobject on the widget. If the zoom factor is 3 or more, we draw the horizontal and vertical lines that form the grid using theQPainter::drawLine()function.

A call toQPainter::drawLine()has the following syntax:

Subclassing QWidget 105 where (x1,y1) is the position of one end of the line and (x2,y2) is the position of the other end. There is also an overloaded version of the function that takes twoQPoints instead of fourints.

The top-left pixel of a Qt widget is located at position (0, 0), and the bottom- right pixel is located at (width()+-- 1,height()+-- 1). This is similar to the conven- tional Cartesian coordinate system, but upside down, and makes a lot of sense in GUI programming. It is perfectly possible to changeQPainter’s coordinate system by using transformations, such as translation, scaling, rotation, and shearing. This is covered in Chapter 8 (2D and 3D Graphics).

(0, 0)

(width()+-- 1, height()+-- 1) (x1, y1)

(x2, y

2)

Figure 5.3. Drawing a line usingQPainter

Before we calldrawLine()on theQPainter, we set the line’s color usingsetPen(). We could hard-code a color, like black or gray, but a better approach is to use the widget’s palette.

Every widget is equipped with a palette that specifies which colors should be used for what. For example, there is a palette entry for the background color of widgets (usually light gray) and one for the color of text on that background (usually black). By default, a widget’s palette adopts the window system’s color scheme. By using colors from the palette, we ensure thatIconEditorrespects the user’s preferences.

A widget’s palette consists of three color groups: active, inactive, and disabled. Which color group should be used depends on the widget’s current state:

• The active color group is used for widgets in the currently active window. • The inactive color group is used for widgets in the other windows. • The disabled color group is used for disabled widgets in any window. TheQWidget::palette() function returns the widget’s palette as a QPalette

object. The color groups are available throughQPalette’sactive(),inactive(), and disabled() functions, and are of type QColorGroup. For convenience,

QWidget::colorGroup()returns the correct color group for the current state of the widget, so we rarely need to access the palette directly.

The paintEvent() function finishes by drawing the image itself, using the

IconEditor::drawImagePixel() function to draw each of the icon’s pixels as filled squares.

QColor color; QRgb rgb = image.pixel(i, j); if (qAlpha(rgb) == 0) color = colorGroup().base(); else color.setRgb(rgb); if (zoom >= 3) { painter->fillRect(zoom * i + 1, zoom * j + 1, zoom - 1, zoom - 1, color); } else {

painter->fillRect(zoom * i, zoom * j, zoom, zoom, color); }

}

ThedrawImagePixel()function draws a zoomed pixel using aQPainter. Thei

andjparameters are pixel coordinates in theQImage—not in the widget. (If the zoom factor is 1, the two coordinate systems coincide exactly.) If the pixel is transparent (its alpha component is 0), we use the current color group’s “base” color (typically white) to draw the pixel; otherwise, we use the pixel’s color in the image. Then we callQPainter::fillRect()to draw a filled square. If the grid is shown, the square is reduced by one pixel in both directions to avoid painting over the grid.

(0, 0)

(width()+-- 1, height()+-- 1) w

h (x, y)

Figure 5.4. Drawing a rectangle usingQPainter

The call toQPainter::fillRect()has the following syntax: painter->fillRect(x, y, w, h, brush);

where (x,y) is the position of the top-left corner of the rectangle,w×his the size of the rectangle, andbrushspecifies the color to fill with and the fill pattern to use. By passing aQColoras the brush, we obtain a solid fill pattern.

void IconEditor::mousePressEvent(QMouseEvent *event) {

if (event->button() == LeftButton) setImagePixel(event->pos(), true); else if (event->button() == RightButton)

Subclassing QWidget 107

setImagePixel(event->pos(), false); }

When the user presses a mouse button, the system generates a “mouse press” event. By reimplementingQWidget::mousePressEvent(), we can respond to this event and set or clear the image pixel under the mouse cursor.

If the user pressed the left mouse button, we call the private functionsetIm- agePixel()withtrueas the second argument, telling it to set the pixel to the current pen color. If the user pressed the right mouse button, we also callset- ImagePixel(), but passfalseto clear the pixel.

void IconEditor::mouseMoveEvent(QMouseEvent *event) {

if (event->state() & LeftButton) setImagePixel(event->pos(), true); else if (event->state() & RightButton) setImagePixel(event->pos(), false); }

ThemouseMoveEvent()handles “mouse move” events. By default, these events

are only generated when the user is holding down a button. It is possible to change this behavior by calling QWidget::setMouseTracking(), but we don’t

need to do so for this example.

Just as pressing the left or right mouse button sets or clears a pixel, keeping it pressed and hovering over a pixel is also enough to set or clear a pixel. Since