Page List

Sunday, January 22, 2012

Qt Tutorial #1-10 Smooth As Silk

This is from Qt Tutorial #1-10 Smooth as Silk


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''
    PyQt4 conversion of Qt Tutorial 10
    
    In this example, we introduce painting in a pixmap to remove flickering. 
    We also add a force control. 
    
    The differences from 109_battle.pyw are:
        1. CannonField 
            - added a 'force' attribute similar to 'angle'
            - implemented 'flicker-free' drawing technique
              using a pixmap (see original tutorial for full explanation)
              
        2. MyWidget - added a 'force' widget and appropriate
                      event handling
                       
    
    NOTES:
    =====
    The original C++ code defines a private interface for a 
    static method, cannonRect() which is implemented in the
    CannonField class. The intent is to create a single copy
    of a rectangle having the same dimensions as the painted
    on screen cannon.
    
    In the following code, the same behaviour
    is implemented using a class attribute 'cRect' and a private
    instance method, '_cannonRect()'. The first call to the method
    creates the rectangle and assigns it to 'CannonField.cRect'
    Future method calls return the same rectangle, 'CannonField.cRect'.
    with the result that only one 'drawing' rectangle is created.
    
    BEHAVIOUR:
    =========
    The flicker has gone and we have a force control.
            
last modified: 2012-01-20 jg
ref: 
    http://doc.trolltech.com/3.3/tutorial1-10.html
'''
import sys
from PyQt4.QtGui import (QApplication, QWidget, QPushButton, QFont,
                         QVBoxLayout, QGridLayout, QLCDNumber, QSlider,
                         QColor, QPainter, QSizePolicy, QPixmap)
from PyQt4.QtCore import (Qt, pyqtSlot, pyqtSignal, qWarning, QRect)


class LCDRange(QWidget):
    '''
        A two digit QLCDNumber and QSlider widget.
    '''
    def __init__(self, parent=None):
        super(LCDRange, self).__init__(parent)

        lcd = QLCDNumber(2, self);

        self.slider = QSlider(Qt.Horizontal, self);
        self.slider.setRange(0, 99);
        self.slider.setValue(0);

        self.slider.valueChanged.connect(lcd.display)

        layout = QVBoxLayout()
        layout.addWidget(lcd)
        layout.addWidget(self.slider)
        self.setLayout(layout)

        # set the widget focus to the 'slider' object
        self.setFocusProxy(self.slider)

    def value(self):
        return self.slider.value()

    @pyqtSlot(int)
    def setValue(self, value):
        self.slider.setValue(value)

    # set the 'slider' range, if min and max values are not
    # between 0 and 99, print a warning message and leave
    # the slider values as they were
    def setRange(self, minVal, maxVal):
        if (minVal < 0 or maxVal > 99 or minVal > maxVal) :
            qWarning("LCDRange.setRange({0},{1})\n"
                     "\tRange must be 0..99\n"
                     "\tand minVal must not be greater than maxVal".format(minVal, maxVal))
            return

        self.slider.setRange(minVal, maxVal)

class CannonField(QWidget):
    cRect = None    # class attribute, only one can exist

    def __init__(self, parent=None):
        super(QWidget, self).__init__(parent)
        self.setObjectName('cannonField')
        self.ang = 45
        self.f = 0      # force

        # set background colour
        pal = self.palette()
        pal.setColor(self.backgroundRole(), QColor(250, 250, 200))
        self.setPalette(pal)
        self.setAutoFillBackground(True)

    # define the screen area containing the cannon
    # using a 'private' method
    def _cannonRect(self):
        if not CannonField.cRect:   # create once
            r = QRect(0, 0, 50, 50)
            r.moveBottomLeft(self.rect().bottomLeft())
            return r
        else:   # already defined so return existing rectangle
            return CannonField.cRect

    # a custom signal
    angleChanged = pyqtSignal(int, name="angleChanged")
    def setAngle(self, degrees):
        if degrees < 5: degrees = 5
        elif degrees > 70: degrees = 70
        elif self.ang == degrees: return

        self.ang = degrees
        self.repaint(self._cannonRect())
        self.angleChanged.emit(self.ang)

    # handle the force of the cannon shot
    forceChanged = pyqtSignal(int, name="forceChanged")
    def setForce(self, newton):
        if newton < 0: newton = 0
        elif self.f == newton: return

        self.f = newton
        self.forceChanged.emit(self.f)

    def paintEvent(self, event):
        # only need to repaint the cannon
        if not(event.rect().intersects(self._cannonRect())):
            return

        # get the area used by the cannon and create
        # a temporary pixmap to avoid flickering while painting
        # all the painting is done in the pixmap and then it is
        # added in one shot
        cr = self._cannonRect()
        pix = QPixmap(cr.size())
        pix.fill(self, cr.topLeft())

        p = QPainter(pix)
        p.setBrush(Qt.blue)     # brush colour for filling object
        p.setPen(Qt.NoPen)      # no special edges

        # set QPainter's origin (0,0) coords to the bottom-left
        p.translate(0, pix.height() - 1)

        # draw a quarter circle in the bottom left corner
        p.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16)

        # rotate counter-clockwise 'ang' degrees around the origin
        # and draw the cannon's barrel
        p.rotate(-self.ang)
        p.drawRect(QRect(33, -4, 15, 8))
        p.end()

        # paint the pixmap on the screen
        p.begin(self)
        p.drawPixmap(cr.topLeft(), pix)

    def sizePolicy(self):
        return QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

class MyWidget(QWidget):
    def __init__(self, parent=None, name=''):
        super(MyWidget, self).__init__(parent)
        if name:
            self.setObjectName(name)

        quitBtn = QPushButton('&Quit', self)
        quitBtn.setFont(QFont("Times", 18, QFont.Bold))
        quitBtn.clicked.connect(QApplication.instance().quit)

        angle = LCDRange(self)
        angle.setRange(5, 70)

        # add LCDRange widget to handle force
        force = LCDRange(self)
        force.setRange(10, 50)

        cannonField = CannonField(self)
        angle.slider.valueChanged.connect(cannonField.setAngle)
        cannonField.angleChanged.connect(angle.setValue)

        # add event handling for 'force'
        force.slider.valueChanged.connect(cannonField.setForce)
        cannonField.forceChanged.connect(force.setValue)

        grid = QGridLayout()
        grid.addWidget(quitBtn, 0, 0)
        grid.addWidget(cannonField, 1, 1)
        grid.setColumnStretch(1, 10)

        # add the angle and force widgets 
        leftBox = QVBoxLayout()
        grid.addLayout(leftBox, 1, 0)
        leftBox.addWidget(angle)
        leftBox.addWidget(force)
        self.setLayout(grid)

        angle.setValue(60)
        force.setValue(25)
        angle.setFocus()    # give the LCDRange object keyboard focus

def main():
    app = QApplication(sys.argv)    # required

    w = MyWidget()
    w.setGeometry(100, 100, 500, 355)
    w.show()

    sys.exit(app.exec_())   # start main event loop, exit when app closed

if __name__ == '__main__':
    main()