c7391.htm
来自「GUI Programming with Python」· HTM 代码 · 共 962 行 · 第 1/2 页
HTM
962 行
light-gray pen. The distance is computed by letting the <TTCLASS="FUNCTION">range</TT> function use the height of the widget divided by the number of rows we want as a stepsize.</P><P>If you wish to use several different painter objects, you might want to use the <TTCLASS="FUNCTION">begin()</TT> and <TTCLASS="FUNCTION">end()</TT> methods of the <TTCLASS="CLASSNAME">QPainter</TT> class. In normal use, as here, the <TTCLASS="FUNCTION">begin()</TT> function is called when the <TTCLASS="CLASSNAME">QPainter</TT> is created, and <TTCLASS="FUNCTION">end()</TT> when it is destroyed. However, because the reference goes out of scope, <TTCLASS="FUNCTION">end()</TT> is called automatically, so you won't have to call <TTCLASS="FUNCTION">end()</TT> yourself.</P><PRECLASS="PROGRAMLISTING"> def text(self): return QString(str(self.count)) </PRE><P>The function <TTCLASS="FUNCTION">text()</TT> simply returns a <TTCLASS="CLASSNAME">QString</TT> object containing the last plotted value. We will use this to set the caption of the chart window.</P><PRECLASS="PROGRAMLISTING"> def update(self, count): """ Called periodically by a timer to update the count. """ self.count = count h = self.height() w = self.width() p = QPainter(self) p.setBackgroundColor(QColor("white")) p.setBrush(QColor("black")) if self.col >= w: self.col = w # move one pixel to the left pixmap = QPixmap(w, h) pixmap.fill(QColor("white")) bitBlt(pixmap, 0, 0, self, BARWIDTH, 0, w - BARWIDTH, h) bitBlt(self, 0, 0, pixmap, 0, 0, w, h) for i in range(1, h, h/5): p.setPen(QColor("lightgray")) p.drawLine(self.col - BARWIDTH , i, w, i) else: self.col += BARWIDTH y = float(self.scale) * float(self.count) # to avoid ZeroDivisionError if y == 0: y = 1 # Draw gradient minV = 255 H = 0 S = 255 vStep = float(float(128)/float(y)) for i in range(y): color = QColor() color.setHsv(H, S, 100 + int(vStep * i)) p.setPen(QPen(color)) p.drawLine(self.col - BARWIDTH, h-i, self.col, h-i) </PRE><P>The <TTCLASS="FUNCTION">update()</TT> function is where the real meat of the charting pixmap is. It draws a gradiented bar that scrolls left when the right side is reached (that is, if the current column has arrived at or gone beyond the width of the pixmap).</P><P>The scrolling is done by creating a new, empty <TTCLASS="CLASSNAME">QPixmap</TT> and blitting the right hand part of the old pixmap onto it. When writing this code, I noticed that you cannot blit a pixmap onto itself. So, after we've created a pixmap that contains the old pixmap minus the first few vertical lines, we blit it back, and add the grid to the now empty right hand side of the pixmap.</P><P>The height of the bar we want to draw is computed by multiplying the value (<TTCLASS="VARNAME">self.count</TT>) with the scale of the chart. If the result is 0, we make it 1.</P><P>We draw the bar in steps, with each step having a subtly differing color from the one before it. The color gradient is determined by going along the <SPAN><ICLASS="EMPHASIS">value</I></SPAN> range of a hue-saturation-value color model. Value determines darkness, with 0 being completely dark, and 255 completely light. We don't use the complete range, but step directly from 100 (fairly dark) to 228 (quite bright). The step is computed by dividing the value range we want (128) by the height of the bar. Every bar is going from 100 to 228.</P><P>Then we step through the computed height of the bar, drawing a horizontal line with the length of the bar thickness — <TTCLASS="VARNAME">BARWIDTH</TT>.</P><P>Computing gradients is fairly costly, but it is still possible to type comfortably when this chart is running: a testimony to the efficient design of <TTCLASS="CLASSNAME">QPainter</TT>. If your needs are more complicated, then <TTCLASS="CLASSNAME">QPainter</TT> offers a host of sophisticated drawing primitives (and not so primitives, like shearing, scaling, resizing and the drawing of quad beziers).</P><P>The <TTCLASS="CLASSNAME">TypoGraph</TT> is completely generic: it draws a nicely gradiented graph of any values that you feed the update function. There's some testing code included that uses a simple timer to update the chart with a random value.</P><DIVCLASS="MEDIAOBJECT"><P><DIVCLASS="CAPTION"><P>A stand-alone chart</P></DIV></P></DIV><P>More application-specific is the <TTCLASS="CLASSNAME">TypoMeter</TT> widget, which keeps track of all open <SPANCLASS="APPLICATION">Kalam</SPAN> documents, and shows the right chart for the currently active document.</P><PRECLASS="PROGRAMLISTING">class TypoMeter(QWidget): def __init__(self, docmanager, workspace, w, h, *args): apply(QWidget.__init__, (self,) + args) self.docmanager = docmanager self.workspace = workspace self.resize(w, h) self.setMinimumSize(w,h) self.setMaximumSize(w,h) self.h = h self.w = w self.connect(self.docmanager, PYSIGNAL("sigNewDocument"), self.addGraph) self.connect(self.workspace, PYSIGNAL("sigViewActivated"), self.changeGraph) self.graphMap = {} self.addGraph(self.docmanager.activeDocument(), self.workspace.activeWindow()) self.timer = QTimer(self) self.connect(self.timer, SIGNAL("timeout()"), self.updateGraph) self.timer.start(FIVE_SECONDS, FALSE) </PRE><P>In order to implement this feature, some new signals had to be added to the document manager and the workspace classes. Note also the use of the <TTCLASS="CLASSNAME">QTimer</TT> class. A timer is created with the current object as its parent; a slot is connected to the <TTCLASS="FUNCTION">timeout()</TT> signal, and the timer is started with a certain interval. The <TTCLASS="VARNAME">FALSE</TT> parameter means that the timer is supposed to keep running, instead of firing once, when the timeout is reached.</P><PRECLASS="PROGRAMLISTING"> def addGraph(self, document, view): self.currentGraph = TypoGraph(0, self.h, self.w) self.graphMap[document] = (self.currentGraph, 0) self.currentDocument = document def changeGraph(self, view): self.currentGraph = self.graphMap[view.document()][0] self.currentDocument = view.document() bitBlt(self, 0, 0, self.currentGraph, 0, 0, self.w, self.h) def updateGraph(self): prevCount = self.graphMap[self.currentDocument][1] newCount = self.currentDocument.text().length() self.graphMap[self.currentDocument] = (self.currentGraph, newCount) delta = newCount - prevCount if delta < 0: delta = 0 # no negative productivity self.currentGraph.update(delta) bitBlt(self, 0, 0, self.currentGraph, 0, 0, self.w, self.h) self.setCaption(self.currentGraph.text()) </PRE><P>The actual keeping track of the type-rate is done in this class, not in the <TTCLASS="CLASSNAME">TypoChart</TT> class. In making good use of Python's ability to form tuples on the fly, a combination of the <TTCLASS="CLASSNAME">TypoChart</TT> instance and the last count is kept in a dictionary, indexed by the document.</P><P>Using the last count and the current length of the text, the <SPAN><ICLASS="EMPHASIS">delta</I></SPAN> (the difference) is computed and fed to the chart. This updates the chart, and the chart is then blitted onto the widget — a <TTCLASS="CLASSNAME">QWidget</TT> is a paintdevice, after all.</P><PRECLASS="PROGRAMLISTING"> def paintEvent(self, ev): p = QPainter(self) bitBlt(self, 0, 0, self.currentGraph, 0, 0, self.w, self.h)class TestWidget(QWidget): def __init__(self, *args): apply(QWidget.__init__, (self,) + args) self.setGeometry(10, 10, 50, 250) self.pixmap = TypoGraph(0, self.width(), self.height()) self.timer = self.startTimer(100) def paintEvent(self, ev): bitBlt(self, 0, 0, self.pixmap, 0, 0, self.width(), self.height()) def timerEvent(self, ev): self.pixmap.update(whrandom.randrange(0, 300)) bitBlt(self, 0, 0, self.pixmap, 0, 0, self.width(), self.height())if __name__ == '__main__': a = QApplication(sys.argv) QObject.connect(a,SIGNAL('lastWindowClosed()'),a,SLOT('quit()')) w = TestWidget() a.setMainWidget(w) w.show() a.exec_loop() </PRE><P>Finally, this is some testing code, not for the <TTCLASS="CLASSNAME">TypoMeter</TT> class, which can only work together with <SPANCLASS="APPLICATION">Kalam</SPAN>, but for the <TTCLASS="CLASSNAME">TypoChart</TT> class. It is difficult to use the unit testing framework from <AHREF="c5064.htm">Chapter 14</A> here— after all, in the case of graphics work, the proof of the pudding is in the eating, and it's difficult to assert things about pixels on the screen.</P><P>The code to show the type-o-meter on screen is interesting, since it shows how you can destructively delete a widget. The <TTCLASS="CLASSNAME">QAction</TT> that provides the menu option "show type-o-meter" is a toggle action, and changing the toggle emits the <TTCLASS="FUNCTION">toggled(bool)</TT> signal. This is connected to the following function (in <TTCLASS="FILENAME">kalamapp.py</TT>: </P><PRECLASS="PROGRAMLISTING"> def slotSettingsTypometer(self, toggle): if toggle: self.typowindow = TypoMeter(self.docManager, self.workspace, 100, 100, self, "type-o-meter", Qt.WType_TopLevel or Qt.WDestructiveClose) self.typowindow.setCaption("Type-o-meter") self.typowindow.show() else: self.typowindow.close(TRUE) </PRE><P>Destroying this popup-window is important, because you don't want to waste processing power on a widget that still exists and is merely hidden. The character picker popup we will create in the next section will be hidden, not destroyed.</P></DIV></DIV></DIV><DIVCLASS="NAVFOOTER"><HRALIGN="LEFT"WIDTH="100%"><TABLESUMMARY="Footer navigation table"WIDTH="100%"BORDER="0"CELLPADDING="0"CELLSPACING="0"><TR><TDWIDTH="33%"ALIGN="left"VALIGN="top"><AHREF="x7388.htm"ACCESSKEY="P">Prev</A></TD><TDWIDTH="34%"ALIGN="center"VALIGN="top"><AHREF="book1.htm"ACCESSKEY="H">Home</A></TD><TDWIDTH="33%"ALIGN="right"VALIGN="top"><AHREF="x7601.htm"ACCESSKEY="N">Next</A></TD></TR><TR><TDWIDTH="33%"ALIGN="left"VALIGN="top">Conclusion</TD><TDWIDTH="34%"ALIGN="center"VALIGN="top"><AHREF="p4627.htm"ACCESSKEY="U">Up</A></TD><TDWIDTH="33%"ALIGN="right"VALIGN="top">QCanvas</TD></TR></TABLE></DIV></BODY></HTML>
⌨️ 快捷键说明
复制代码Ctrl + C
搜索代码Ctrl + F
全屏模式F11
增大字号Ctrl + =
减小字号Ctrl + -
显示快捷键?