Subclassing QWidget

When we cannot get the custom widget we need by setting properties, using a style sheet, or subclassing an existing widget, we can create the widget we need from scratch. In practice, we always create custom widgets by subclassing QWidget, since this provides a lot of behind-the-scenes convenience that we don't need or want to worry about, leaving us free to focus on what matters: the appearance and behavior of our custom widget.

In this section, we will look at two different custom widgets. The first, Frac-tionSlider, is a generic "range control"-type widget that might be used many times. The second, YPipeWidget, is an application-specific widget that may be needed in only one particular program.

Before we go into the details of these two widgets, we will first discuss painting in PyQt, and in particular the coordinate systems that are used by QPainter. A QPainter has two separate coordinate systems: a device (physical) coordinate system that matches the pixels in the widget's area, and a logical coordinate system. By default, the logical coordinate system is set to exactly match the physical coordinate system.

In fact, the physical coordinates are not necessarily pixels since they depend on the underlying paint device. This can be a QGLPixelBuffer (for 2D and 3D painting), a QImage, a QPicture, a QPixmap, a QPrinter (in which case the coordinates are points, 72"), a QSvgGenerator (introduced with Qt 4.3), or a QWidget.

In PyQt terminology the physical coordinate system is called the "viewport", and confusingly, the logical coordinate system is called the "window".

Figure 11.4 The viewport and window coordinate systems

In Figure 11.4, we have a physical widget size of 800 x 600. By calling setWin-dow(-60, -60, 120, 120) we can create a "window" with a top-left coordinate of (-60, -60), a width of 120, a height of 120, and centered at point (0, 0). The window's coordinate system is a logical coordinate system that QPainter automatically maps to the underlying physical device. After the setWindow() call, all our painting takes place using the logical (window) coordinate system.

In this case, the widget is rectangular, but our window has the same width and height. This means that the items we paint will be stretched out horizontally, since coordinates in the y-axis will be scaled by QPainter in the ratio 120:600 (1:5), whereas those in the x-axis will be scaled in the ratio 120:800 (1:6f).

For most widgets, a rectangular region works perfectly well, but in some cases—for example, if we really want our logical window to be square—we can change the viewport so that we operate on only a proportion of the widget's area.

This code, executed inside a widget's paintEvent(), changes the widget's viewport to be the largest centered square region that will fit. In the example earlier, this will produce a viewport of 600 x 600 pixels with no top or bottom margins, but with a 100-pixel margin on the left and on the right. The window will now be an exact square, and the aspect ratio of anything we paint in it will be preserved.

Table 11.1 Selected QWidget Methods

Syntax w.addAction(a)

w.hasFocus()

w.height()

w.restore-

Geometry(ba)

w.save-

Geometry()

w.setAccept-

Drops(b)

w.setAttrib-

w.setContext-

MenuPolicy(p)

w.setCursor(c)

w.setEnabled(b)

w.setFocus()

w.setFont(f)

w.setLayout(l)

w.setSize-

Policy(hp, vp)

w.setStyle-

Sheet(s)

w.setWindow-

Icon(i)

w.setWindow-

Title(s)

w.update()

w.update-

Geometry()

Description

Adds QAction a to QWidget w; useful for context menus Hides QWidget w; or deletes it if Qt.WA_DeleteOnClose is set Returns True if QWidget w has the keyboard focus Returns QWidget w^ height Hides QWidget w

Moves the top-level QWidget w to position (x, y)

Raises QWidget w to the top of the parent widget's stack

Returns QWidget w^ dimensions as a QRect

Restores top-level QWidget w^ geometry to that encoded in QByteArray ba

Returns a QByteArray that encodes QWidget w^ geometry

Sets whether QWidget w will accept drops depending on bool b

Sets Qt.WidgetAttribute wa on or off depending on bool b. The most common attribute used is Qt.WA_DeleteOnClose. Sets QWidget w^ context menu policy to policy p. Policies include Qt.NoContextMenu and Qt.ActionsContextMenu. Sets QWidget w^ cursor to c, a QCursor or a Qt.CursorShape Sets QWidget w to be enabled or disabled depending on b Gives the keyboard focus to QWidget w Sets QWidget w^ font to QFont f Sets QWidget w^ layout to QLayout l

Sets QWidget ws horizontal and vertical QSizePolicys to hp and vp

Sets QWidget ws style sheet to the CSS text in string s

Sets top-level QWidget ws icon to QIcon i

Sets top-level QWidget w^ title to string s

Shows top-level QWidget w modelessly. It can be shown modally by using setWindowModality(). Schedules a paint event for QWidget w

For non-top-level widgets, notifies any containing layouts that QWidget w's geometry may have changed Returns QWidget w's width

The main benefit of using a window is that it allows us to paint using logical coordinates. This is very convenient because it means that all the scaling that is needed—for example, when the user resizes the widget—is taken care of automatically by PyQt. This benefit also turns out to have a drawback: If we want to paint text, the text will be scaled along with everything else. For this reason, it is often easiest to work in physical (viewport) coordinates for custom widgets that paint text, and logical (window) coordinates otherwise. We show both approaches, with the FractionSlider using viewport coordinates and the YPipeWidget using window coordinates.

Example: A Fraction Slider

The FractionSlider is a widget that allows the user to choose a fraction between 0 and 1 inclusive; it is shown in Figure 11.5. We will allow programmers who use our slider to set a denominator in the range 3-60, and will emit value-Changed(int, int) signals (with the numerator and denominator) whenever the user changes the fraction. We provide both mouse and keyboard control, and we paint the entire widget ourselves. We also ensure that the widget's minimum size hint is always proportional to the size of the denominator, so that the widget cannot be resized to be too small to show the fraction texts.

Pyqt Widgets Sip

Figure 11.5 A dialog using a Fraction Slider We will begin by looking at the static data and the initializer. class FractionSlider(QWidget):

XMARGIN =12.0 YMARGIN =5.0 WSTRING = "999"

def_init_(self, numerator=0, denominator=10, parent=None):

super(FractionSlider, self)._init_(parent)

self._numerator = numerator self._denominator = denominator self.setFocusPolicy(Qt.WheelFocus)

self.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding,

QSizePolicy.Fixed))

9.1JL.1.11.1L 119111213141516 1616161616161616161616161616161616

Figure 11.5 A dialog using a Fraction Slider We will begin by looking at the static data and the initializer. class FractionSlider(QWidget):

XMARGIN =12.0 YMARGIN =5.0 WSTRING = "999"

def_init_(self, numerator=0, denominator=10, parent=None):

super(FractionSlider, self)._init_(parent)

self._numerator = numerator self._denominator = denominator self.setFocusPolicy(Qt.WheelFocus)

self.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding,

QSizePolicy.Fixed))

The XMARGIN and YMARGIN are used to give some horizontal and vertical spacing around the edges of the widget. The WSTRING is a string containing text that is the longest we could possibly need: two digits to display, and an extra digit to provide some margin.

We provide default values that start the widget off as showing zero tenths. We Size chose a focus policy of Qt.WheelFocus because that is the "strongest" one, which policies means that the widget will accept focus if tabbed to or clicked on, or if the 271« user uses the mouse wheel on it. We set the size policies for the horizontal and vertical directions. By doing this we help ensure that our widget will cooperate properly with the layout managers. Here we have said that in the horizontal direction, the widget can be shrunk to its minimum size but prefers to grow, and in the vertical direction the widget has a fixed size of whatever height its sizeHint() method returns.

def decimal(self):

return self._numerator / float(self._denominator)

def fraction(self):

return self._numerator, self._denominator

We provide two convenience methods for returning the value, the first returning a floating-point value and the second a pair of integers.

def setFraction(self, numerator, denominator=None): if denominator is not None: if 3 <= denominator <= 60:

self._denominator = denominator else:

raise ValueError, "denominator out of range" if 0 <= numerator <= self.__denominator:

self.__numerator = numerator else:

raise ValueError, "numerator out of range" self.update() self.updateGeometry()

This method can be used just to set the numerator, or to set both numerator and denominator. Once the fraction has been changed we call update() to schedule a paint event so that the gold triangle that marks the current fraction is repainted in the right place.

We also call updateGeometry(). This is to tell any layout manager that is responsible for this widget that the widget's geometry might have changed. This may appear strange—after all, we have changed only the fraction. But if we changed the denominator, the widget's size hint will have changed to allow for more (or less) fractions to be displayed. As a result, if there is a layout manager for the widget, it will recalculate its layout, asking the widget for its size hints and adjusting the layout if necessary.

We have chosen to deal with invalid values by raising exceptions. This is because setFraction() is normally called programmatically, and so should never be given out-of-range values in the normal run of things. An alternative approach would be to force the numerator and denominator to be within range: This approach is taken in the keyboard and mouse event handlers that give the widget its behavior, and it is to these that we now turn.

def mousePressEvent(self, event):

if event.button() == Qt.LeftButton: self.moveSlider(event.x()) event.accept() else:

QWidget.mousePressEvent(self, event)

If the user clicks the widget, we want to set the numerator to the nearest fraction. We could do the calculations in the mouse press event, but since we want to support dragging the gold triangle as well as clicking to move it, we have factored out this code into a separate moveSlider() method that takes the mouse's x coordinate as an argument. After changing the fraction, we accept the event, since we have handled it. If we did not handle the click (for example, if it was a right-click), we call the base class implementation, although this is not strictly necessary.

def moveSlider(self, x):

span = self.width() - (FractionSlider.XMARGIN * 2) offset = span - x + FractionSlider.XMARGIN

numerator = int(round(self._denominator * \

numerator = max(0, min(numerator, self._denominator))

if numerator != self.__numerator: self.__numerator = numerator self.emit(SIGNAL("valueChanged(int,int)"), self._numerator, self._denominator)

self.update()

We begin by calculating the "span" of the widget, excluding the horizontal margins. Then we find how far along the x-axis the mouse was clicked (or dragged) and calculate the numerator as a proportion of the widget's width. If the user clicked or dragged in the left margin area, we set the numerator to 0, and if they clicked or dragged in the right margin area, we set it to equal the denominator (so the fraction will be 1). If the numerator has changed from before, we set the instance variable accordingly and emit a signal announcing that the value has changed. We then call update() to schedule a paint event (to move the gold triangle).

Short- We have chosen to emit a Python non-short-circuit signal; we could just as circuit easily have made it a short-circuit signal by dropping the (int,int). It is also signals 131*»

possible to define signals using the_pyqtSignals_class attribute, although this is really useful only for custom widgets written in PyQt that are to be integrated with Qt Designer.*

def mouseMoveEvent(self, event): self.moveSlider(event.x())

This tiny method is all that we need to support dragging the gold triangle to change the fraction. This method is called only if the mouse is being dragged, that is, if the left mouse button is pressed at the same time the mouse is being moved. This is QWidget's standard behavior—we can have mouse move events generated for all mouse moves, regardless of the mouse buttons, by calling QWidget.setMouseTracking(True), if we wish.

def keyPressEvent(self, event): change = 0

change = -self._denominator elif event.key() in (Qt.Key_Up, Qt.Key_Right):

change = self._denominator if change:

numerator = self._numerator numerator += change numerator = max(0, min(numerator, self._denominator))

if numerator != self._numerator:

self._numerator = numerator self.emit(SIGNAL("valueChanged(int,int)"), self._numerator, self._denominator)

self.update() event.accept() else:

QWidget.keyPressEvent(self, event)

For keyboard support, we want Home to set the fraction to 0, End to set it to 1, up or right arrow keys to move to the next fraction up, and down or left arrow keys to move to the next fraction down. We have also set PageUp to move one-tenth of the way up and PageDown to move one-tenth of the way down.

★See the PyQt pyqt4ref.html documentation, under "Writing Qt Designer Plugins".

The code for ensuring that the numerator is in range, and for setting the instance variable, and so on, is identical to what we did in the mouse press event handler. And again we pass on unhandled key presses to the base class implementation—which does nothing, just as the base class mouse click handler does nothing.

def sizeHint(self):

return self.minimumSizeHint()

We have decided that the widget's preferred size is its minimum size. Strictly speaking we did not have to reimplement this method, but by doing so we make our intention clear. Thanks to the size policies we set in the initializer, the widget can grow horizontally to occupy as much horizontal space as is available.

def minimumSizeHint(self): font = QFont(self.font()) font.setPointSize(font.pointSize() - 1) fm = QFontMetricsF(font)

return QSize(fm.width(FractionSlider.WSTRING) * \ self._denominator,

A QFontMetricsF object is initialized by a QFont object, in this case the widget's default font.* This font is inherited from the widget's parent, which in turn inherits from its parent, and so on, with top-level widgets inheriting their fonts (and color schemes and other user settings) from the QApplication object, which itself takes them from the user preferences reported by the underlying windowing system. We can of course ignore the users' preferences and set an explicit font in any widget.

The QFontMetricsF object provides the real metrics, i.e., those of the font actually used—and this may be different from the font that was specified. For example, if the Helvetica font is used, it will almost certainly be found and used on Linux or Mac OS X, but on Windows, Ariel is likely to be used in its place. We have chosen to use a font size one less than the user's preferred font size to show the fractions, which is why we call setPointSize().

We set the widget's minimum width to be the width necessary to display all the fractions, assuming that each one is three digits wide, i.e., two digits plus some empty margin either side. The overall width is actually slightly less than this because we don't include the horizontal margins. We set the widget's minimum height to be four times the height of one character, i.e., enough vertical space for the fraction "segments" (the rectangles that signify each fraction), the vertical lines, the numerator, and the denominator. And just as for the width, the actual height is slightly less than this, because we only account for half of the vertical margin.

★PyQt also has a QFontMetrics class which gives integer rather than floating-point values. Similarly PyQt has QLine, QLineF, QPoint, QPointF, QPolygon, QPolygonF, QRect, QRectF, and some others.

Having implemented the key and mouse event handlers, set the size policies, and implemented the size hint methods, we have made the widget have appropriate behavior for user interaction and in relation to any layout manager that might be asked to lay out the widget. There is only one thing left to do: We must paint the widget when required to do so. The paintEvent() is rather long, so we will look at it in pieces.

def paintEvent(self, event=None): font = QFont(self.font()) font.setPointSize(font.pointSize() - 1) fm = QFontMetricsF(font) fracWidth = fm.width(FractionSlider.WSTRING) indent = fm.boundingRect("9").width() / 2.0 if not X11:

fracWidth *= 1.5 span = self.width() - (FractionSlider.XMARGIN * 2) value = self._numerator / float(self._denominator)

We begin by getting the font we want to use, as well as its font metrics. Then we calculate fracWidth, the width of one fraction, as well as an indent for each fraction's dividing line. The if statement is used to compensate for differences between the font metrics on the X Window System and other window systems such as Windows and Mac OS X. The span is the width of the widget excluding the horizontal margins, and the value is the floating-point value of the fraction.

The X11 Boolean variable is True if the underlying window system is the X Window System, as it normally is on Linux, BSD, Solaris, and similar, and False otherwise—for example, on Windows and Mac OS X. It was set at the beginning of the file, after the imports, using the following statement:

X11 = "qt_x11_wait_for_window_manager" in dir()

We could write it more clearly as:

import PyQt4.QtGui

X11 = hasattr(PyQt4.QtGui, "qt_x11_wait_for_window_manager")

These work because the PyQt4.QtGui.qt_x11_wait_for_window_manager() function exists only on systems that are using the X Window System. We used a similar technique for Mac OS X detection in Chapter 7.

painter = QPainter(self)

painter.setRenderHint(QPainter.Antialiasing)

painter.setRenderHint(QPainter.TextAntialiasing)

painter.setPen(self.palette().color(QPalette.Mid))

painter.setBrush(self.palette().brush(QPalette.AlternateBase))

painter.drawRect(self.rect())

We create a QPainter and set its render hints to give us antialiased drawing. Then we set the pen (which is used for shape outlines and for drawing text) and the brush (which is used for fills), and draw a rectangle over the entire widget. Because we used different shades for the pen and brush, this has the effect of giving the widget a border and a slightly indented look.

The QApplication object has a QPalette that contains colors for various purposes, such as text foreground and background colors, button colors, and so on. The colors are identified by their roles, such as QPalette.Text or QPalette.Highlight, although we have used rather more obscure roles in this example. There are, in fact, three sets of these colors, one for each of the widget states: "active", "disabled", and "inactive". Every QWidget also has a QPalette, with colors inherited from the QApplication palette—which in turn is initialized with colors from the underlying window system and therefore reflects the user's preferences. PyQt tries very hard to ensure that the colors in a palette work well together, providing good contrast, for example. As programmers, we are free to use whatever colors we like, but especially for standard requirements such as text colors and backgrounds, it is best to use the palette.

segColor = QColor(Qt.green).dark(120) segLineColor = segColor.dark() painter.setPen(segLineColor) painter.setBrush(segColor) painter.drawRect(FractionSlider.XMARGIN,

FractionSlider.YMARGIN, span, fm.height())

We create a dark green color for the segments, and an even darker green for the vertical lines that mark them out. Then we draw a rectangle that encompasses all the segments.

textColor = self.palette().color(QPalette.Text)

segWidth = span / self._denominator segHeight = fm.height() * 2

nRect = fm.boundingRect(FractionSlider.WSTRING)

x = FractionSlider.XMARGIN

yOffset = segHeight + fm.height()

Here, we set the text color to use based on the user's palette. Then we work out the width and height of each segment, and set an initial x position and a yOffset. The nRect is a rectangle large enough to contain a number with some left and right margin space.

for i in range(self._denominator + 1):

painter.setPen(segLineColor)

painter.drawLine(x, FractionSlider.YMARGIN, x, segHeight)

painter.setPen(textColor)

y = segHeight rect = QRectF(nRect)

rect.moveCenter(QPointF(x, y + fm.height() / 2.0)) painter.drawText(rect, Qt.AlignCenter, QString.number(i)) y = yOffset rect.moveCenter(QPointF(x, y + fm.height() / 2.0)) painter.drawText(rect, Qt.AlignCenter,

QString.number(self._denominator))

painter.drawLine(QPointF(rect.left() + indent, y), QPointF(rect.right() - indent, y))

In this loop we draw the vertical lines that mark out each segment, the numerator below each segment, and the denominator below each numerator, along with the dividing line between them. For the drawText() calls we provide a rectangle in which the text should be drawn, and by using Qt.AlignCenter, we ensure that the text is vertically and horizontally centered inside the specified rectangle. We use the same rectangle, but indented at the left and right, to calculate the end points of the dividing line, which we then draw. The y offsets are fixed for every line, numerator, and denominator, but the x offsets increase by one segment width after drawing each fraction.

y = FractionSlider.YMARGIN - 0.5 triangle = [QPointF(value * span, y), QPointF((value * span) + \

(2 * FractionSlider.XMARGIN), y), QPointF((value * span) + \

FractionSlider.XMARGIN, fm.height())] painter.setPen(Qt.yellow) painter.setBrush(Qt.darkYellow) painter.drawPolygon(QPolygonF(triangle))

At the end we draw the gold triangle that shows the user which fraction is selected. We specify polygons by providing a list of points. We don't have to duplicate the first point at the end, since if we use drawPolygon(), PyQt will automatically join the first and last points and fill the enclosed area. In the next chapter we will see more advanced drawing techniques, including the use of the very versatile QPainterPath class.

We have now completed the generic FractionSlider widget. It has both keyboard and mouse support, looks reasonable on all platforms, and interacts properly with the layout managers.

The QPainter class offers many more possibilities than we have needed for this widget, but in the next subsection we will see more of what can be done, including drawing unfilled polygons and polygons that use gradient fills. We will also see how to include other widgets inside a custom widget.

Example: A Flow-Mixing Widget

It is sometimes appropriate to create custom widgets for particular applications. For this example, we will assume that we have an application in which we are modeling the flow of fluid through a "Y'-shaped pipe, as depicted in Figure 11.6.

Figure 11.6 A YPipe widget

Figure 11.6 A YPipe widget

The widget must draw three gradient-filled polygons and three black outlines (to give a clear border to the pipe shapes), and must allow the user to set the left and right flows, as well as show the combined flow. We have chosen to provide spinboxes for the user to set the flows and to use a label to show the resultant flow. The advantages of using these built-in widgets include that we need concern ourselves only with their positioning and connections; we can leave PyQt to provide mouse and keyboard interaction and to display them properly. Another benefit is that we can paint our custom widget using a "window", that is, using logical rather than device (viewport) coordinates, and do not have to worry about scaled text because the text appears only in widgets that are overlaid on top of the custom widget, and are not affected by the window settings.

We will start by looking at the initializer, taking it in two parts. class YPipeWidget(QWidget):

def _init_(self, leftFlow=0, rightFlow=0, maxFlow=100, parent=None): super(YPipeWidget, self)._init_(parent)

self.leftSpinBox = QSpinBox(self) self.leftSpinBox,setRange(0, maxFlow) self.leftSpinBox.setValue(leftFlow) self.leftSpinBox.setSuffix(" l/s")

self.leftSpinBox.setAlignment(Qt.AlignRight|Qt.AlignVCenter) self.connect(self.leftSpinBox, SIGNAL("valueChanged(int)"), self.valueChanged)

After the super() call, we create the left spinbox, set some of its parameters, and connect it to a valueChanged() method that we will look at in a moment. Notice that we give the spinbox a parent of self (the YPipeWidget instance); this is because the spinbox will not be laid out, so no layout manager will reparent the spinbox to the parent widget for us. We have omitted the creation and setup of the right spinbox because the code is almost identical.

self.label = QLabel(self)

self.label.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken) self.label.setAlignment(Qt.AlignCenter) fm = QFontMetricsF(self.font()) self.label.setMinimumWidth(fm.width(" 999 l/s "))

self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,

QSizePolicy.Expanding)) self.setMinimumSize(self.minimumSizeHint()) self.valueChanged()

We create the label that we will use to show the combined flow, and set some of its properties. We give it a minimum width so that it will not resize disconcertingly—for example, if the flow rate changes between 9 and 10, or 99 and 100. We set the size policies of the YPipeWidget to expanding, which means that the widget wants to grow in both directions as much as possible. We also set the widget's minimum size to its minimum size hint, and call valueChanged() to give the label an initial value.

def valueChanged(self):

a = self.leftSpinBox.value() b = self.rightSpinBox.value() self.label.setText("%d l/s" % (a + b)) self.emit(SIGNAL("valueChanged"), a, b) self.update()

Whenever the user changes one of the flow spinboxes, this method is called. It updates the label, emits its own valueChanged Python signal, which any external widget could connect to, and schedules a repaint. The reason for the repaint is that the gradient fills are colored in proportion to the spinbox values.

def values(self):

return self.leftSpinBox.value(), self.rightSpinBox.value()

This method provides the flow spinbox values as a two-tuple.

def minimumSizeHint(self):

return QSize(self.leftSpinBox.width() * 3, self.leftSpinBox.height() * 5)

We have made the widget's minimum width and height proportional to the spinboxes. This ensures that the "Y" shape never becomes too small to be understandable.

def resizeEvent(self, event=None): fm = QFontMetricsF(self.font()) x = (self.width() - self.label.width()) / 2 y = self.height() - (fm.height() * 1.5) self.label.move(x, y) y = self.height() / 60.0

x = (self.width() / 4.0) - self.leftSpinBox.width() self.leftSpinBox.move(x, y) x = self.width() - (self.width() / 4.0) self.rightSpinBox.move(x, y)

The resize event is particularly important for widgets that contain other widgets and that do not have a layout. This is because we use this event to position the child widgets. A resize event is always called before a widget is first shown, so we automatically get the chance to position the child widgets before the widget is seen by the user for the first time.

The label is horizontally centered, and drawn near the bottom of the widget. (The y coordinates increase downward, so self.height() returns the greatest—bottommost—y value.) The two spinboxes are drawn near the top, 60 of the height below the least—topmost—y value, and 1 of the widget's width in from the left or right edge.

Because we have used QSpinBoxes and a QLabel, along with a couple of signal-slot connections, all the user interaction is taken care of, so we need to concern ourselves only with resizing and painting. Although the painting is simplified by having the spinboxes and label drawn by PyQt, it is still a little involved, so we will look at the paint event in pieces.

def paintEvent(self, event=None): LogicalSize = 100.0

def logicalFromPhysical(length, side): return (length / side) * LogicalSize fm = QFontMetricsF(self.font()) ymargin = (LogicalSize / 30.0) + \

logicalFromPhysical(self.leftSpinBox.height(), self.height())

logicalFromPhysical(fm.height() * 2, self.height()) width = LogicalSize / 4.0 cx, cy = LogicalSize / 2.0, LogicalSize / 3.0

ax, ay = cx - (2 * width), ymargin bx, by = cx - width, ay dx, dy = cx + width, ay ex, ey = cx + (2 * width), ymargin fx, fy = cx + (width / 2), cx + (LogicalSize / 24.0)

gx, gy = fx, ymax hx, hy = cx - (width / 2), ymax ix, iy = hx, fy

Rather than work in device (physical) coordinates and have to scale all the coordinates ourselves, we have created a logical coordinate system, with a top left of (0,0) and a width and height of 100 (LogicalSize). We have defined a tiny helper function used to calculate a y margin above which the spinboxes are drawn, and a maximum y below which the label is drawn.

Figure 11.7 The YPipe's coordinate points

As Figure 11.7 indicates, we do all our painting in terms of the points needed to draw the "Y" shape. For each point in the figure, we calculate an x coordinate and a y coordinate. For example, the top-left point is a, so its coordinates in the code are ax and ay. Most of the calculations are done in terms of point c, (cx, cy).

painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) side = min(self.width(), self.height()) painter.setViewport((self.width() - side) / 2,

(self.height() - side) / 2, side, side) painter.setWindow(0, 0, LogicalSize, LogicalSize)

We create the painter and set its viewport to be the largest centered square area that will fit inside its rectangle. We then set a window, that is, impose our own logical coordinate system, leaving PyQt to take care of transforming logical to physical coordinates.

painter.setPen(Qt.NoPen)

Table 11.2 Selected QPainter Methods (Excluding Drawing-Related Methods)

p.setMatrix(m) p.setRenderHint(h)

p.translate(dx, dy)

Description

Restores QPainter p's state to the last saved state Rotates QPainter p by int a°

Saves QPainter p's state, including its transformation matrix, pen, and brush

Scales QPainter p horizontally by float x and vertically by float y; 1.0 is unscaled, 0.5 is half size, 3.0 is three times the size

Sets QPainter p's transformation matrix to QMatrix m

Turns on the QPainter.RenderHint h. Hints include QPainter.Antialiasing, QPainter.TextAntialiasing, and QPainter.SmoothPixmapTransform. Constrains QPainter p's viewport (physical coordinates) to the rectangle with top-left corner at point (x, y), and with width w and height h; all the arguments are ints

Sets QPainter p's logical coordinate system to the rectangle with top-left corner at point (x, y), and with width w and height h; all the arguments are ints Shears QPainter p's coordinate system horizontally by float x and vertically by float y Moves QPainter p's coordinate system horizontally by int dx and vertically by int dy

We turn off the pen because we do not want an outline around the polygons we will draw for each part of the pipe. Instead, we will draw in the lines we want at the end of the paint event.

gradient = QLinearGradient(QPointF(0, 0), QPointF(0, 100)) gradient.setColorAt(0, Qt.white) a = self.leftSpinBox.value()

gradient.setColorAt(1, Qt.red if a != 0 else Qt.white)

painter.setBrush(QBrush(gradient))

painter.drawPolygon(

For the left part of the "Y" shape representing the left spinbox—the shape (a, b, c, i)—we use a linear color gradient going from white to red.

gradient = QLinearGradient(QPointF(0, 0), QPointF(0, 100)) gradient.setColorAt(0, Qt.white) b = self.rightSpinBox.value()

gradient.setColorAt(1, Qt.blue if b != 0 else Qt.white)

Table 11.3 Selected QPainter Drawing-Related Methods

p.drawConvex-Polygon(pl)

p.drawEllipse(r) p.drawImage(pt, i) p.drawLine(p1, p2)

p.drawPixmap(pt, px)

p.drawPoint(pt)

p.drawPolygon(pl)

p.drawPolyline(pl)

p.drawRect(r)

p.drawText(x, y, s) p.fillPath(pp, b) p.fillRect(r, b) p.setBrush(b) p.setPen(pn) p.setFont(f)

Description

Draws an arc on QPainter p in the circle bounded by QRect r, starting at angle int a°, and spanning 16°

Draws a chord on QPainter p in the circle bounded by QRect r, starting at angle int a°, and spanning 16°

Draws a convex polygon on QPainter p connecting the list of QPointsin pi, and connects the last point back to the first

Draws an ellipse on QPainter p bounded by QRect r; draws a circle if r is square

Draws Qlmage i at QPoint pt on QPainter p; different arguments allow drawing just part of the image Draws a line between QPoints pi and p2 on QPainter p. Many argument variations are possible; there are also drawLines() methods. Draws the QPainterPath pp on QPainter p

Draws a pie segment in the circle bounded by QRect r, starting at angle int a°, and spanning 16°

Draws QPixmap px at QPoint pt on QPainter p; different arguments allow drawing just part of the pixmap Draws QPoint pt on QPainter p; there are also draw-Points() methods

Draws a polygon on QPainter p connecting the iist of QPoints in pi, and connects the last point back to the first

Draws a polyline on QPainter p connecting the iist of QPoints in pi; does not connect the last point to the first

Draws a QRect r on QPainter p

Draws a rounded rectangle on QPainter p bounded by

QRect r, and using rounding factors ints x and y

Draws string s on QPainter p bounded by QRect r, and using the optional QTextOption o

Draws string s on QPainter p at point (x, y)

Fills QPainterPath pp with QBrush b on QPainter p

Fills QRect r with QBrush b on QPainter p

Sets the brush for filled shapes to QBrush b

Sets the pen for lines and outlines to QPen pn

Sets QPainter p's text font to QFont f painter.setBrush(QBrush(gradient)) painter.drawPolygon(

The right part—shape (d, e, f, c)—is very similar to the left part, only it uses a gradient going from white to blue.

color = QColor(Qt.white) else:

ashare = (a / (a + b)) * 255.0 bshare = 255.0 - ashare color = QColor(ashare, 0, bshare) gradient = QLinearGradient(QPointF(0, 0), QPointF(0, 100)) gradient.setColorAt(0, Qt.white) gradient.setColorAt(1, color) painter.setBrush(QBrush(gradient)) painter.drawPolygon(

QPolygon([cx, cy, fx, fy, gx, gy, hx, hy, ix, iy]))

The stem of the "Y"—shape (c, f, g, h, i)—is drawn with a linear gradient that goes from white to a red/blue color that is proportional to the left/right flow rates.

painter.setPen(Qt.black)

painter.drawPolyline(QPolygon([ax, ay, ix, iy, hx, hy])) painter.drawPolyline(QPolygon([gx, gy, fx, fy, ex, ey])) painter.drawPolyline(QPolygon([bx, by, cx, cy, dx, dy]))

We finish by drawing the lines that represent the sides of the pipe. The first line goes from a to i to h, marking out the left of the pipe, the second from g to f to e, marking out the right of the pipe, and the third, from b to c to d, marks the "V"-shaped part at the top.

Just like the built-in PyQt widgets, both the YPipeWidget and the Fraction-Slider can be used as top-level widgets, and this is particularly useful when developing and testing custom widgets. Both chap11/fractionslider.py and chap11/ypipewidget.py can be run as stand-alone programs because both have an if __name__ == "__main__": statement after the QWidget subclass, with code that creates a QApplication, and that creates and shows the custom widget.

Was this article helpful?

0 -2
Tuberminator

Tuberminator

The main focus of this report is to show how to get involved in video marketing on the run, how to rank quickly on YouTube and Google using FREE semi-automatic tools and services. QUICKLY AND FREE. I will show methods and techniques I use to rank my videos, as well as free resources and tools to make video clips, to get backlinks and free traffic.

Get My Free Ebook


Post a comment