A common way to draw a rubber band is to use the NOT (or the XOR) math- ematical operator, which replaces each pixel value on the rubber band rect- angle with the opposite bit pattern. Here’s a new version ofupdateRubber- BandRegion()that does this:
void Plotter::updateRubberBandRegion() { QPainter painter(this); painter.setRasterOp(NotROP); painter.drawRect(rubberBandRect.normalize()); }
ThesetRasterOp()call sets the painter’s raster operation to beNotROP. In the original version, we kept the default value,CopyROP, which toldQPainterto simply copy the new value over the original.
When we call updateRubberBandRegion() a second time with the same coordinates, the original pixels are restored, since two NOTs cancel each other out.
The advantage of using NOT is that it’s easy to implement and it eliminates the need to keep a copy of the covered areas. But it isn’t generally applica- ble. For example, if we draw text instead of a rubber band, the text could become very hard to read. Also, NOT doesn’t always produce good contrast; for example, medium gray stays medium gray. Finally, NOT isn’t supported on Mac OS X.
Another approach is to render the rubber band as an animated dotted line. This is often used in image manipulation programs, because it provides good contrast no matter what colors are found in the image. To do this in Qt, the trick is to reimplementQObject::timerEvent()to erase the rubber band and then repaint it but starting drawing the dots at a slightly different offset each time, producing the illusion of movement.
void Plotter::refreshPixmap() {
pixmap.resize(size()); pixmap.fill(this, 0, 0);
QPainter painter(&pixmap, this); drawGrid(&painter);
}
TherefreshPixmap()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 tosetBackgroundMode()in thePlotterconstructor.
Then we create a QPainter to draw on the pixmap and call drawGrid() and
drawCurves()to perform the drawing. At the end, we callupdate()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); }
Double Buffering 129 The firstforloop draws the grid’s vertical lines and the ticks along thexaxis. The secondforloop draws the grid’s horizontal lines and the ticks along the
yaxis. ThedrawText()function is used to draw the numbers corresponding to the tick mark on both axes.
The calls todrawText()have the following syntax:
painter.drawText(x, y, w, h, alignment, text);
where (x,y,w,h) define a rectangle,alignmentthe position of the text within
that rectangle, andtextthe 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<int, CurveData>::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; } }
ThedrawCurves()function draws the curves on top of the grid. We start by callingsetClipRect()to set theQPainter’s clip region to the rectangle that con- tains the curves (excluding the margins).QPainterwill then ignore drawing operations on pixels outside the area.
coordinate pairs that constitute it. The member of the iterator’s value gives us the ID of the curve and thesecondmember gives us the curve data. The inner part of theforloop converts a coordinate pair from plotter coordi- nates to widget coordinates and stores it in thepointsvariable, 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; }
ThePlotSettingsconstructor 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; }
Thescroll()function increments (or decrements)minX,maxX,minY, andmaxYby the interval between two ticks times a given number. This function is used to implement scrolling inPlotter::keyPressEvent().
void PlotSettings::adjust() {
adjustAxis(minX, maxX, numXTicks); adjustAxis(minY, maxY, numYTicks); }
Theadjust()function is called from mouseReleaseEvent()to round the minX,
maxX,minY, andmaxYvalues 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.
Double Buffering 131
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 itsnumTicksparameter to the number of ticks it calculates to be appropriate for the given [min,max] range. BecauseadjustAxis()needs to modify the actual variables (minX,maxX,numXTicks, etc.) and not just copies, its parameters are non-const references.
Most of the code inadjustAxis()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 form10 , 2·n 10 , or 5·n 10 .n
We start by computing the “gross step”, a kind of maximum for the step value. Then we find the corresponding number of the form10 that is smaller thann 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 form10 .n
Once we have the first candidate step value, we can use it to calculate the other two candidates: 2·10 and 5·n 10 . For the example above, the two othern 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 derivenumTicks,min, andmaxfrom the step value. The newmin
value is obtained by rounding the originalmindown to the nearest multiple of the step, and the newmaxvalue is obtained by rounding up to the nearest multiple of the step. The newnumTicksis the number of intervals between the the roundedminandmaxvalues. For example, ifminis 240 andmaxis 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 sophisti- cated algorithm is described in Paul S. Heckbert’s article “Nice Numbers for
interest is theQt Quarterlyarticle “Fast and Flicker-Free”, available online athttp://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 usingQWidgetas 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.