Double Buffering
Double buffering is a technique that can be used to provide a snappier user interface and to eliminate flicker. Flicker occurs when the same pixel is painted multiple times with different colors in a very short period of time. If this occurs for only one pixel, it isn't a problem, but if it occurs for lots of pixels at the same time, it can be distracting for the user.
When Qt generates a paint event, it first erases the widget using the palette's background color. Then, in paintEvent(), the widget only needs to paint the pixels that are not the same color as the background. This two-step approach is very convenient, because it means we can simply paint what we need on the widget without worrying about the other pixels.
Unfortunately, the two-step approach is also a major source of flicker. For example, if the user resizes the widget, the widget is first cleared in its entirety, and then the pixels are painted. The flicker is even worse if the window system shows the contents of the window as it is resized, because then the widget is repeatedly erased and painted.
Figure 5.7. Resizing a widget that has no provision against flicker
The WStaticContents flag used to implement the IconEditor widget is one solution to this problem, but it can only be used for widgets whose content is independent of the size of the widget. Such widgets are rare. Most widgets tend to stretch their contents to consume all the available space. They need to be completely repainted when they are resized. We can still avoid flicker, but the solution is slightly more complicated.
The first rule to avoid flicker is to construct the widget with the WNoAutoErase flag. This flag tells Qt not to erase the widget before a paint event. The old pixels are then left unchanged, and any newly revealed pixels are undefined.
Figure 5.8. Resizing a WNoAutoErase widget
When using WNoAutoErase, it is important that the paint handler sets all the pixels explicitly. Any pixel that is not set in the paint event will keep its previous value, which isn't necessarily the background color.
The second rule to avoid flicker is to paint every pixel just once. The easiest way to implement this requirement is to draw the whole widget in an offscreen pixmap and to copy the pixmap onto the widget in one go. Using this approach, it doesn't matter if some pixels are painted multiple times because the painting takes place off-screen. This is double buffering.
Adding double buffering to a custom widget to eliminate flicker is straightforward. Suppose the original paint event handler looks like this:
void MyWidget::paintEvent(QPaintEvent *) { QPainter painter(this); drawMyStuff(&painter); }
The double-buffered version looks like this:
void MyWidget::paintEvent(QPaintEvent *event) { static QPixmap pixmap; QRect rect = event->rect(); QSize newSize = rect.size().expandedTo(pixmap.size()); pixmap.resize(newSize); pixmap.fill(this, rect.topLeft()); QPainter painter (&pixmap, this); painter.translate(-rect.x(), -rect.y()); drawMyStuff(&painter); bitBlt(this, rect.x(), rect.y(), &pixmap, 0, 0, rect.width(), rect.height()); }
First, we resize a QPixmap to be at least as large as the bounding rectangle of the region to repaint. (A "region" is very often either a rectangle or an L-shaped area, but it can be arbitrarily complex.) We make the QPixmap a static variable to avoid repeatedly allocating and deallocating it. For the same reason, we never shrink the QPixmap; the calls to QSize::expandedTo() and QPixmap::resize() ensure that it is always large enough. After resizing, we fill the QPixmap with the widget's erase color or background pixmap using QPixmap::fill(). The second argument to fill() specifies which point in the widget the QPixmap's top-left pixel corresponds to. (This makes a difference if the widget is to be erased using a pixmap instead of a uniform color.)
The QPixmap class is similar to both QImage and QWidget. Like a QImage, it stores an image, but the color depth and possibly the colormap are aligned with the display, rather like a hidden QWidget. If the window system is running in 8-bit mode, all widgets and pixmaps are restricted to 256 colors, and Qt automatically maps 24-bit color specifications onto 8-bit colors. (Qt's color allocation strategy is controlled by calling QApplication::setColorSpec().)
Next, we create a QPainter to operate on the pixmap. By passing the this pointer to the constructor, we tell QPainter to adopt some of the widget's settings, such as its font. We translate the painter to paint the correct rectangle into the pixmap, before we perform the drawing using the QPainter as usual.
Finally, we copy the pixmap to the widget using the bitBlt() global function, whose name stands for "bit-block transfer".
Double buffering is not only useful for avoiding flicker. It is beneficial if the widget's rendering is complex and needed repeatedly. We can then store a pixmap permanently with the widget, always ready for the next paint event, and copy the pixmap to the widget whenever we receive a paint event. It is especially helpful when we want to do small modifications, such as drawing a rubber band, without recomputing the whole widget's rendering over and over.
We will round off this chapter by reviewing the Plotter custom widget. This widget uses double buffering, and also demonstrates some other aspects of Qt programming, including keyboard event handling, manual layout, and coordinate systems.
The Plotter widget displays one or more curves specified as vectors of coordinates. The user can draw a rubber band on the image, and the Plotter will |zoom in on the area enclosed by the rubber band. The user draws the rubber band by clicking a point on the graph, dragging the mouse to another position with the left mouse button held down, and releasing the mouse button.
Figure 5.9. Zooming in on the Plotter widget
The user can zoom in repeatedly by drawing a rubber band multiple times, zooming out using the Zoom Out button, and then zooming back in using the Zoom In button. The Zoom In and Zoom Out buttons appear the first time they become available, so that they don't clutter the display if the user doesn't zoom the graph.
The Plotter widget can hold the data for any number of curves. It also maintains a stack of PlotSettings, each of which corresponds to a particular zoom level.
Let's review the class, starting with plotter.h:
#ifndef PLOTTER_H #define PLOTTER_H #include #include #include #include class QToolButton; class PlotSettings; typedef std::vector CurveData;
We include the standard and header files. We don't import all the std namespace's symbols into the global namespace, because it's bad style to do this in a header file.
We define CurveData as a synonym for std::vector. We will store a curve's points as successive pairs of x and y values in the vector. For example, the curve defined by the points (0, 24), (1, 44), (2,89) is represented by the vector [0, 24, 1, 44, 2, 89].
class Plotter : public QWidget { Q_OBJECT public: Plotter(QWidget *parent = 0, const char *name = 0, WFlags flags = 0); void setPlotSettings(const PlotSettings &settings); void setCurveData(int id, const CurveData &data); void clearCurve(int id); QSize minimumSizeHint() const; QSize sizeHint() const; public slots: void zoomIn(); void zoomOut();
We provide three public functions for setting up the plot, and two public slots for zooming in and out. We also reimplement minimumSizeHint() and sizeHint() from QWidget.
protected: void paintEvent(QPaintEvent *event); void resizeEvent(QResizeEvent *event); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void keyPressEvent(QKeyEvent *event); void wheelEvent(QWheelEvent *event);
In the protected section of the class, we declare all the QWidget event handlers that we need to reimplement.
private: void updateRubberBandRegion(); void refreshPixmap(); void drawGrid(QPainter *painter); void drawCurves(QPainter *painter); enum { Margin = 40 }; QToolButton *zoomInButton; QToolButton *zoomOutButton; std::map curveMap; std::vector zoomStack; int curZoom; bool rubberBandIsShown; QRect rubberBandRect; QPixmap pixmap; };
In the private section of the class, we declare a constant, a few functions for painting the widget, and several member variables. The Margin constant is used to provide some spacing around the graph.
Among the member variables is pixmap of type QPixmap. This variable holds a copy of the whole widget's rendering, identical to what is shown on screen. The plot is always drawn onto this off-screen pixmap first; then the pixmap is copied onto the widget.
class PlotSettings { public: PlotSettings(); void scroll(int dx, int dy); void adjust(); double spanX() const { return maxX - minX; } double spanY() const { return maxY - minY; } double minX; double maxX; int numXTicks; double minY; double maxY; int numYTicks; private: void adjustAxis(double &min, double &max, int &numTicks); }; #endif
The PlotSettings class specifies the range of the x and y axes and the number of ticks for these axes. Figure 5.10 shows the correspondence between a PlotSettings object and the scales on a Plotter widget.
Figure 5.10. PlotSettings's member variables
By convention, numXTicks and numYTicks are off by one; if numXTicks is 5, Plotter will actually draw 6 tick marks on the x axis. This simplifies the calculations later on.
Now let's review the implementation file:
#include #include #include #include using namespace std; #include "plotter.h"
We include the expected header files and import all the std namespace's symbols into the global namespace.
Plotter::Plotter(QWidget *parent, const char *name, WFlags flags) : QWidget(parent, name, flags | WNoAutoErase) { setBackgroundMode(PaletteDark); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setFocusPolicy(StrongFocus); rubberBandIsShown = false; zoomInButton = new QToolButton(this); zoomInButton->setIconSet(QPixmap::fromMimeSource("zoomin.png")); zoomInButton->adjustSize(); connect(zoomInButton, SIGNAL(clicked()), this, SLOT(zoomIn())); zoomOutButton = new QToolButton(this); zoomOutButton->setIconSet( QPixmap::fromMimeSource("zoomout.png")); zoomOutButton->adjustSize(); connect(zoomOutButton, SIGNAL(clicked()), this, SLOT(zoomOut())); setPlotSettings(PlotSetting()); }
The Plotter has a flags parameter in addition to parent and name. This parameter is simply passed on to the base class constructor, along with WNoAutoErase. The parameter is especially useful for widgets that are likely to be used as stand-alone windows, because it allows the user of the class to configure the window frame and title bar.
The setBackgroundMode() call tells QWidget to use the "dark" component of the palette as the color for erasing the widget, instead of the "background" component. Although we pass the WNoAutoErase flag to the base class constructor, Qt still needs a default color that it may use to fill any newly revealed pixels when the widget is resized to a larger size, before paintEvent() even has the chance to paint the new pixels. Since the background of the Plotter widget will be dark, it makes sense to paint these pixels dark.
The setSizePolicy() call sets the widget's size policy to QSizePolicy::Expanding in both directions. This tells any layout manager that is responsible for the widget that the widget is especially willing to grow, but can also shrink. This setting is typical for widgets that can take up a lot of screen space. The default is QSizePolicy::Preferred in both directions, which means that the widget prefers to be the size of its size hint, but it can be shrunk down to its minimum size hint or expanded indefinitely if necessary.
The setFocusPolicy() call makes the widget accept focus by clicking or by pressing Tab. When the Plotter has focus, it will receive events for key presses. The Plotter widget understands a few keys: + to zoom in, - to zoom out, and the arrow keys to scroll up, down, left, and right.
Figure 5.11. Scrolling the Plotter widget
Still in the constructor, we create two QToolButtons, each with an icon. These buttons allow the user to navigate through the zoom stack. The button's icons are stored in an image collection. Any application that uses the Plotter widget will need this entry in its .pro file:
IMAGES += images/zoomin.png images/zoomout.png
The calls to adjustSize() on the buttons sets their sizes to be that of their size hints.
The call to setPlotSettings() at the end does the rest of the initialization.
void Plotter::setPlotSettings(const PlotSettings &settings) { zoomStack.resize(1); zoomStack[0] = settings; curZoom = 0; zoomInButton->hide(); zoomOutButton->hide(); refreshPixmap(); }
The setPlotSettings() function is used to specify the PlotSettings to use for displaying the plot. It is called by the Plotter constructor, and can be called by users of the class. The plotter starts out at its default zoom level. Each time the user zooms in, a new PlotSettings instance is created and put onto the zoom stack.
The zoom stack is represented by two member variables:
- zoomStack holds the different zoom settings as a vector.
- curZoom holds the current PlotSettings's index in the zoomStack.
After a call to setPlotSettings(), the zoom stack contains only one entry, and the Zoom In and Zoom Out buttons are hidden. These buttons will not be shown until we call show() on them in the zoomIn() and zoomOut() slots. (Normally, it is sufficient to call show() on the top-level widget to show all the children. But when we explicitly call hide() on a child widget, it is hidden until we call show() on it.)
The call to refreshPixmap() is necessary to update the display. Usually, we would call update(), but here we do things slightly differently because we want to keep a QPixmap up to date at all times. After regenerating the pixmap, refreshPixmap() calls update() to copy the pixmap onto the widget.
void Plotter::zoomOut() { if (curZoom > 0) { --curZoom; zoomOutButton->setEnabled(curZoom > 0); zoomInButton->setEnabled(true); zoomInButton->show(); refreshPixmap(); } }
The zoomOut() slot zooms out if the graph is zoomed in. It decrements the current zoom level and enables the Zoom Out button depending on whether the graph can be zoomed out any more or not. The Zoom In button is enabled and shown, and the display is updated with a call to refreshPixmap().
void Plotter::zoomIn() { if (curZoom > (int)zoomStack.size() - 1) { ++curZoom; zoomInButton->setEnabled( curZoom < (int)zoomStack.size() - 1); zoomOutButton->setEnabled(true); zoomOutButton->show(); refreshPixmap(); } }
If the user has previously zoomed in and then out again, the PlotSettings for the next zoom level will be in the zoom stack, and we can zoom in. (Otherwise, it is still possible to zoom in using a rubber band.)
The slot increments curZoom to move one level deeper into the zoom stack, sets the Zoom In button enabled or disabled depending on whether it's possible to zoom in any further, and enables and shows the Zoom Out button. Again, we call refreshPixmap() to make the plotter use the latest zoom settings.
void Plotter::setCurveData(int id, const CurveData &data) { curveMap[id] = data; refreshPixmap(); }
The setCurveData() function sets the curve data for a given curve ID. If a curve with the same ID already exists in the plotter, it is replaced with the new curve data;otherwise, the new curve is simply inserted. The curves are stored in the curveMap member variable of type map.
Again, we call our own refreshPixmap() function, rather than update(), to update the display.
void Plotter::clearCurve(int id) { curveMap.erase(id); refreshPixmap(); }
The clearCurve() function removes a curve from curveMap.
QSize Plotter::minimumSizeHint() const { return QSize(4 * Margin, 4 * Margin); }
The minimumSizeHint() function is similar to sizeHint(); just as sizeHint() specifies a widget's ideal size, minimumSizeHint() specifies a widget's ideal minimum size. A layout never resizes a widget below its minimum size hint.
The value we return is 160 x 160 to allow for the margin on all four sides and some space for the plot itself. Below that size, the plot would be too small to be useful.
QSize Plotter::sizeHint() const { return QSize(8 * Margin, 6 * Margin); }
In sizeHint(), we return an "ideal" size in proportion to the margin and with a pleasing 4:3 aspect ratio.
This finishes the review of the Plotter's public functions and slots. Now let's review the protected event handlers.
void Plotter::paintEvent(QPaintEvent *event) { QMemArray rects = event->region().rects(); for (int i = 0; i < (int)rects.size(); ++i) bitBlt(this, rects[i].topLeft(), &pixmap, rects[i]); QPainter painter(this); if (rubberBandIsShown) { painter.setPen(colorGroup().light()); painter.drawRect(rubberBandRect.normalize()); } if (hasFocus()) { style().drawPrimitive(QStyle::PE_FocusRect, &painter, rect(), colorGroup(), QStyle::Style_FocusAtBorder, colorGroup().dark()); } }
Normally, paintEvent() is the place where we perform all the drawing. But here all the plot drawing is done beforehand in refreshPixmap(), so we can render the entire plot simply by copying the pixmap onto the widget.
The call to QRegion::rect() returns an array of QRects that define the region to repaint. We use bitBlt() to copy each rectangular area from the pixmap to the widget. The bitBlt() global function has the following syntax:
bitBlt(dest, destPos, source, sourceRect);
where source is the source widget or pixmap, sourceRect is the rectangle in the source that should be copied, dest is the destination widget or pixmap, and destPos is the top-left position in the destination.
Figure 5.12. Copying arbitrary rectangles to and from pixmaps and widgets
It would have been equally correct to call bitBlt() just once on the region's bounding rectangle, as we did in a previous code snippet (p. 113). However, because we call update() to erase and redraw the rubber band repeatedly in the mouse event handlers (as we will see shortly), and the rubber band outline is basically four tiny rectangles (two 1-pixel-wide rectangles and two 1-pixel-high rectangles), we gain some speed by breaking the region down into its constituent rectangles and calling bitBlt() for each rectangle.
Once the plot is shown on screen, we draw the rubber band and the focus rectangle on top of it. For the rubber band, we use the "light" component from the widget's current color group as the pen color to ensure good contrast with the "dark" background. Notice that we draw directly on the widget, leaving the off-screen pixmap untouched. The focus rectangle is drawn using the widget style's drawPrimitive() function with PE_FocusRect as its first argument.
The QWidget::style() function returns the widget style to use to draw the widget. In Qt, a widget style is a subclass of QStyle. The built-in styles include QWindowStyle, QWindowsXPStyle, QMotifStyle, and QMacStyle. Each of these styles reimplements the virtual functions in QStyle to perform the drawing in the correct way for the platform the style is emulating. The drawPrimitive() function is one of these functions; it draws "primitive elements" like panels, buttons, and focus rectangles. The widget style is usually the same for all widgets in an application (QApplication::style()), but it can be overriden on a per-widget basis using QWidget::setStyle().
By subclassing QStyle, it is possible to define a custom style. This can be done to give a distinctive look at an application or a suite of applications. While it is generally advisable to use the target platform's native look and feel, Qt offers a lot of flexibility if you want to be adventurous.
Qt's built-in widgets rely almost exclusively on QStyle to paint themselves. This is why they look like native widgets on all platforms supported by Qt. Custom widgets can be made style-aware either by using QStyle to paint themselves or by using built-in Qt widgets as child widgets. For Plotter, we use both approaches: The focus rectangle is drawn using QStyle, and the Zoom In and Zoom Out buttons are built-in Qt widgets.
void Plotter::resizeEvent(QResizeEvent *) { int x = width() - (zoomInButton->width() + zoomOutButton->width() + 10); zoomInButton->move(x, 5); zoomOutButton->move(x + zoomInButton->width() + 5, 5); refreshPixmap(); }
Whenever the Plotter widget is resized, Qt generates a "resize" event. Here, we reimplement resizeEvent() to place the Zoom In and Zoom Out buttons at the top right of the Plotter widget.
We move the Zoom In button and the Zoom Out button to be side by side, separated by a 5-pixel gap and with a 5-pixel offset from the top and right edges of the parent widget.
If we wanted the buttons to stay rooted to the top-left corner, whose coordinates are (0, 0), we would simply have moved them there in the Plotter constructor. But we want to track the top-right corner, whose coordinates depend on the size of the widget. Because of this, it's necessary to reimplement resizeEvent() and to set the position there.
We didn't set any positions for the buttons in the Plotter constructor. This isn't an issue, since Qt always generates a resize event before a widget is shown for the first time.
An alternative to reimplementing resizeEvent() and laying out the child widgets manually would have been to use a layout manager (for example, QGridLayout). However, it would have been a little more complicated and would have consumed more resources. When we write widgets from scratch as we are doing here, laying out our child widgets manually is usually the right approach.
At the end, we call refreshPixmap() to redraw the pixmap at the new size.
void Plotter::mousePressEvent(QMouseEvent *event) { if (event->button() == LeftButton) { rubberBandIsShown = true; rubberBandRect.setTopLeft(event->pos()); rubberBandRect.setBottomRight(event->pos()); updateRubberBandRegion(); setCursor(crossCursor); } }
When the user presses the left mouse button, we start displaying a rubber band. This involves setting rubberBandIsShown to true, initializing the rubberBandRect member variable with the current mouse pointer position, scheduling a paint event to paint the rubber band, and changing the mouse cursor to have a crosshair shape.
Qt provides two mechanisms for controlling the mouse cursor's shape:
- QWidget::setCursor() sets the cursor shape to use when the mouse hovers over a particular widget. If no cursor is set for a widget, the parent widget's cursor is used. The default for top-level widgets is an arrow cursor.
- QApplication::setOverrideCursor() sets the cursor shape for the entire application, overriding the cursors set by individual widgets until restoreOverrideCursor() is called.
In Chapter 4, we called QApplication::setOverrideCursor() with waitCursor to change the application's cursor to the standard wait cursor.
void Plotter::mouseMoveEvent(QMouseEvent *event) { if (event->state() & LeftButton) { updateRubberBandRegion(); rubberBandRect.setBottomRight(event->pos()); updateRubberBandRegion(); } }
When the user moves the mouse cursor while holding the left button, we call updateRubberBandRegion() to schedule a paint event to repaint the area where the rubber band was, we update rubberBandRect to account for the mouse move, and we call updateRubberBandRegion() a second time to repaint the area where the rubber band has moved to. This effectively erases the rubber band and redraws it at the new coordinates.
The rubberBandRect variable is of type QRect. A QRect can be defined either as an (x, y, w, h) quadruplewhere (x, y) is the position of the top-left corner and w x h is the size of the rectangeor as a top-left and a bottom-right coordinate pair. Here, we have used the coordinate pair representation. We set the point where the user clicked the first time as the top-left corner and the current mouse position as the bottom-right corner.
If the user moves the mouse upward or leftward, it's likely that rubberBandRect's nominal bottom-right corner will end up above or to the left of its top-left corner. If this occurs, the QRect will have a negative width or height. QRect has a normalize() function that a adjusts the top-left and bottom-right coordinates to obtain a nonnegative width and height.
void Plotter::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == LeftButton) { rubberBandIsShown = false; updateRubberBandRegion(); unsetCursor(); QRect rect = rubberBandRect.normalize(); if (rect.width() < 4 || rect.height() < 4) return; rect.moveBy(-Margin, -Margin); PlotSettings prevSettings = zoomStack[curZoom]; PlotSettings settings; double dx = prevSettings.spanX() / (width() - 2 * Margin); double dy = prevSettings.spanY() / (height() - 2 * Margin); settings.minX = prevSettings.minX + dx * rect.left(); settings.maxX = prevSettings.minX + dx * rect.right(); settings.minY = prevSettings.maxY - dy * rect.bottom(); settings.maxY = prevSettings.maxY - dy * rect.top(); settings.adjust(); zoomStack.resize(curZoom + 1); zoomStack.push_back(settings); zoomIn(); } }
When the user releases the left mouse button, we erase the rubber band and restore the standard arrow cursor. If the rubber band is at least 4 x 4, we perform the zoom. If the rubber band is smaller than that, it's likely that the user clicked the widget by mistake or to give it focus, so we do nothing.
The code to perform the zoom is a bit complicated. This is because we deal with two coordinate systems at the same time: widget coordinates and plotter coordinates. Most of the work we perform here is to convert the rubberBandRect from widget coordinates to plotter coordinates.
Once we have done the conversion, we call PlotSettings::adjust() to round the numbers and find a sensible number of ticks for each axis.
Figure 5.13. Converting the rubber band from widget to plotter coordinates
Figure 5.14. Adjusting plotter coordinates and zooming in on the rubber band
Then we perform the zoom. The zoom is achieved by pushing the new PlotSettings that we have just calculated on top of the zoom stack and calling zoomIn() to do the job.
void Plotter::keyPressEvent(QKeyEvent * event) { switch (event->key()) { case Key_Plus: zoomIn(); break; case Key_Minus: zoomOut(); break; case Key_Left: zoomStack[curZoom].scroll(-1, 0); refreshPixmap(); break; case Key_Right: zoomStack[curZoom].scroll(+1, 0); refreshPixmap(); break; case Key_Down: zoomStack[curZoom].scroll(0, -1); refreshPixmap(); break; case Key_Up: zoomStack[curZoom].scroll(0, +1); refreshPixmap(); break; default: QWidget::keyPressEvent(event); } }
When the user presses a key and the Plotter widget has focus, the keyPressEvent() function is called. We reimplement it here to respond to six keys: +, -, Up, Down, Left, and Right. If the user pressed a key that we are not handling, we call the base class implementation. For simplicity, we ignore the Shift, Ctrl, and Alt modifier keys, which are available through QKeyEvent::state().
void Plotter::wheelEvent(QWheelEvent *event) { int numDegrees = event->delta() / 8; int numTicks = numDegrees / 15; if (event->orientation() == Horizontal) zoomStack[curZoom].scroll(numTicks, 0); else zoomStack[curZoom].scroll(0, numTicks); refreshPixmap(); }
Wheel events occur when a mouse wheel is turned. Most mice only provide a vertical wheel, but some also have a horizontal wheel. Qt supports both kinds of wheels. Wheel events go to the widget that has the focus. The delta() function returns the distance the wheel was rotated in eighths of a degree. Mice typically work in steps of 15 degrees.
The most common use of the wheel mouse is to scroll a scroll bar. When we subclass QScrollView (covered in Chapter 6) to provide scroll bars, QScrollView handles the wheel mouse events automatically, so we don't so we reimplement wheelEvent() ourselves. Qt classes like QListView, QTable, and QTexEdit that inherit QScrollView also support wheel events without needing additional code.
This finishes the implementation of the event handlers. Now let's review the private functions.
void Plotter::updateRubberBandRegion() { QRect rect = rubberBandRect.normalize(); update(rect.left(), rect.top(), rect.width(), 1); update(rect.left(), rect.top(), 1, rect.height()); update(rect.left(), rect.bottom(), rect.width(), 1); update(rect.right(), rect.top(), 1, rect.height()); }
The updateRubberBand() function is called from mousePressEvent(), mouseMoveEvent(), and mouseReleaseEvent() to erase or redraw the rubber band. It consists of four calls to update() that schedule a paint event for the four small rectangular areas that are covered by the rubber band.
void Plotter::refreshPixmap() { pixmap.resize(size()); pixmap.fill(this, 0, 0); QPainter painter(&pixmap, this); drawGrid(&painter); drawCurves(&painter); update(); }
The refreshPixmap() function redraws the plot onto the off-screen pixmap and updates the display.
We resize the pixmap to have the same size as the widget and fill it with the widget's erase color. This color is the "dark" component of the palette, because of the call to setBackgroundMode() in the Plotter constructor.
Then we create a QPainter to draw on the pixmap and call drawGrid() and drawCurves() to perform the drawing. At the end, we call update() to schedule a paint event for the whole widget. The pixmap is copied to the widget in the paintEvent() function (p. 121).
void Plotter::drawGrid(QPainter *painter) { QRect rect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin); PlotSettings settings = zoomStack[curZoom]; QPen quiteDark = colorGroup().dark().light(); QPen light = colorGroup().light(); for (int i = 0; i <= settings.numXTicks; ++i) { int x = rect.left() + (i * (rect.width() - 1) / settings.numXTicks); double label = settings.minX + (i * settings.spanX() / settings.numXTicks); painter->setPen(quiteDark); painter->drawLine(x, rect.top(), x, rect.bottom()); painter->setPen(light); painter->drawLine(x, rect.bottom(), x, rect.bottom() + 5); painter->drawText(x - 50, rect.bottom() + 5, 100, 15, AlignHCenter | AlignTop, QString::number(label)); } for (int j = 0; j <= settings.numYTicks; ++j) { int y = rect.bottom() - (j * (rect.height() - 1) / settings.numYTicks); double label = settings.minY + (j * settings.spanY() / settings.numYTicks); painter->setPen(quiteDark); painter->drawLine(rect.left(), y, rect.right(), y); painter->setPen(light); painter->drawLine(rect.left() - 5, y, rect.left(), y); painter->drawText(rect.left() - Margin, y - 10, Margin - 5, 20, AlignRight | AlignVCenter, QString::number(label)); } painter->drawRect(rect); }
The drawGrid() function draws the grid behind the curves and the axes.
The first for loop draws the grid's vertical lines and the ticks along the x axis. The second for loop draws the grid's horizontal lines and the ticks along the y axis. The drawText() function is used to draw the numbers corresponding to the tick mark on both axes.
The calls to drawText() have the following syntax:
painter.drawText(x, y, w, h, alignment, text);
where (x, y, w, h) define a rectangle, alignment the position of the text within that rectangle, and text the text to draw.
void Plotter::drawCurves(QPainter *painter) { static const QColor colorForIds[6] = { red, green, blue, cyan, magenta, yellow }; PlotSettings settings = zoomStack[curZoom]; QRect rect(Margin, Margin, width() - 2 * Margin, height() - 2 * Margin); painter->setClipRect(rect.x() + 1, rect.y() + 1, rect.width() - 2, rect.height() - 2); map::const_iterator it = curveMap.begin(); while (it != curveMap.end()) { int id = (*it).first; const CurveData &data = (*it).second; int numPoints = 0; int maxPoints = data.size() / 2; QPointArray points(maxPoints); for (int i = 0; i < maxPoints; ++i) { double dx = data[2 * i] - settings.minX; double dy = data[2 * i + 1] - settings.minY; double x = rect.left() + (dx * (rect.width() - 1) / settings.spanX()); double y = rect.bottom() - (dy * (rect.height() - 1) / settings.spanY()); if (fabs(x) < 32768 && fabs(y) < 32768) { points[numPoints] = QPoint((int)x, (int)y); ++numPoints; } } points.truncate(numPoints); painter->setPen(colorForIds[(uint)id % 6]); painter->drawPolyline(points); ++it; } }
The drawCurves() function draws the curves on top of the grid. We start by calling setClipRect() to set the QPainter's clip region to the rectangle that contains the curves (excluding the margins). QPainter will then ignore drawing operations on pixels outside the area.
Next, we iterate over all the curves, and for each curve, we iterate over the (x, y) coordinate pairs that constitute it. The first member of the iterator's value gives us the ID of the curve and the second member gives us the curve data.
The inner part of the for loop converts a coordinate pair from plotter coordinates to widget coordinates and stores it in the points variable, provided that it lies within reasonable bounds. If the user zooms in a lot, we could easily end up with numbers that cannot be represented as 16-bit signed integers, leading to incorrect rendering by some window systems.
Once we have converted all the points of a curve to widget coordinates, we set the pen color for the curve (using one of a set of predefined colors) and call drawPolyline() to draw a line that goes through all the curve's points.
This is the complete Plotter class. All that remains are a few functions in PlotSettings.
PlotSettings::PlotSettings() { minX = 0.0; maxX = 10.0; numXTicks = 5; minY = 0.0; maxY = 10.0; numYTicks = 5; }
The PlotSettings constructor initializes both axes to the range 0 to 10 with 5 tick marks.
void PlotSettings::scroll(int dx, int dy) { double stepX = spanX() / numXTicks; minX += dx * stepX; maxX += dx * stepX; double stepY = spanY() / numYTicks; minY += dy * stepY; maxY += dy * stepY; }
The scroll() function increments (or decrements) minX, maxX, minY, and maxY by the interval between two ticks times a given number. This function is used to implement scrolling in Plotter::keyPressEvent().
void PlotSettings::adjust() { adjustAxis(minX, maxX, numTicks); adjustAxis(minY, maxY, numYTicks); }
The adjust() function is called from mouseReleaseEvent() to round the minX, maxX, minY, and maxY values to "nice" values and to determine the number of ticks appropriate for each axis. The private function adjustAxis() does its work one axis at a time.
void PlotSettings::adjustAxis(double &min, double &max, int &numTicks) { const int MinTicks = 4; double grossStep = (max - min) / MinTicks; double step = pow(10, floor(log10(grossStep))); if (5 * step < grossStep) step *= 5; else if (2 * step < grossStep) step *= 2; numTicks = (int) (ceil(max / step) - floor(min / step)); min = floor(min / step) * step; max = ceil(max / step) * step; }
The adjustAxis() function converts its min and max parameters into "nice" numbers and sets its numTicks parameter to the number of ticks it calculates to be appropriate for the given [min, max] range. Because adjustAxis() needs to modify the actual variables (minX, maxX, numXTicks, etc.) and not just copies, its parameters are non-const references.
Most of the code in adjustAxis() simply attempts to determine an appropriate value for the interval between two ticks (the "step"). To obtain nice numbers along the axis, we must select the step with care. For example, a step value of 3.8 would lead to an axis with multiples of 3.8, which is difficult for people to relate to. For axes labelled in decimal notation, "nice" step values are numbers of the form 10n, 2.10n, or 5.10n.
We start by computing the "gross step", a kind of maximum for the step value. Then we find the corresponding number of the form 10n that is smaller than or equal to the gross step. We do this by taking the decimal logarithm of the gross step, then rounding that value down to a whole number, then raising 10 to the power of this rounded number. For example, if the gross step is 236, we compute log 236 = 2.37291...; then we round it down to 2 and obtain 102 = 100 as the candidate step value of the form 10n.
Once we have the first candidate step value, we can use it to calculate the other two candidates: 2.10n and 5.10n. For the example above, the two other candidates are 200 and 500. The 500 candidate is larger than the gross step, so we can't use it. But 200 is smaller than 236, so we use 200 for the step size in this example.
It's fairly easy to derive numTicks, min, and max from the step value. The new min value is obtained by rounding the original min down to the nearest multiple of the step, and the new max value is obtained by rounding up to the nearest multiple of the step. The new numTicks is the number of intervals between the the rounded min and max values. For example, if min is 240 and max is 1184 upon entering the function, the new range becomes [200, 1200], with 5 tick marks.
This algorithm will give suboptimal results in some cases. A more sophisticated algorithm is described in Paul S. Heckbert's article "Nice Numbers for Graph Labels" published in Graphics Gems (ISBN 0-12-286166-3). Also of interest is the Qt Quarterly article "Fast and Flicker-Free", available online at http://doc.trolltech.com/qq/qq06-flicker-free.html, which presents some more ideas for eliminating flicker.
This chapter has brought us to the end of Part I. It has explained how to customize an existing Qt widget and how to build a widget from the ground up using QWidget as the base class. We have already seen how to compose a widget from existing widgets in Chapter 2, and we will explore the theme further in Chapter 6.
At this point, we know enough to write complete GUI applications using Qt. In Part II, we will explore Qt in greater depth, so that we can make full use of Qt's power.