Sunday, January 22, 2012

Qt Tutorial #1-12 Hanging in the air

This is from Qt Tutorial #1-12 Hanging in the Air the Way Bricks Don't As the file was getting rather large, decided to split the classes across 3 files.



LCDRange

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

'''
    PyQt4 conversion of Qt Tutorial 12
    
    Added a widget label.
    
    The differences from 111_battle.pyw are:
            - modified initialiser to accept a string to label the widget
            - added methods text() and setText() to get and set the
              widget's label
            - added a main() for testing
                  
last modified: 2012-01-21 jg
ref: 
    http://doc.trolltech.com/3.3/tutorial1-12.html
'''

from PyQt4.QtGui import (QWidget, QLabel, QLCDNumber, QSlider, QVBoxLayout)
from PyQt4.QtCore import (Qt, pyqtSlot)

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

        self.label = QLabel(wlabel)
        self.label.setAlignment(Qt.AlignCenter)

        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)
        layout.addWidget(self.label)
        self.setLayout(layout)

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

    def text(self):
        return self.label.text()

    def setText(self, txt):
        self.label.setText(txt)

    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)

def main():
    import sys
    from PyQt4.QtGui import (QApplication)
    app = QApplication(sys.argv)    # required

    w = LCDRange(wlabel="ANGLE")
    w.setGeometry(100, 100, 500, 355)
    w.show()

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

if __name__ == '__main__':
    main()

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

'''
    PyQt4 conversion of Qt Tutorial 12
    
    The differences from 111_battle.pyw are:              
            - added hit() and miss() signals
            - added newTarget() and targetRect() methods
            - added new attribute 'target' to hold the target's center point
            - modified paintEvent() and added paintTarget()
            - added a main() for testing
                                   
    NOTES:
    =====
    The orignal C++ code set up the 'firstTarget' variable as a static
    method variable within the newTarget() method. As the variable is
    only accessed once, it's been setup here as a class attribute.
    
last modified: 2012-01-21 jg
ref: 
    http://doc.trolltech.com/3.3/tutorial1-12.html
'''

from math import (cos, sin)
from random import (seed, randrange)
from PyQt4.QtGui import (QWidget, QColor, QPainter, QSizePolicy, QPixmap)
from PyQt4.QtCore import (Qt, pyqtSignal, pyqtSlot, QRect, QTimer, QPoint, QTime)

class CannonField(QWidget):
    cRect = None                     # class attribute, only one can exist
    bRect = QRect(33, -4, 15, 8)     # class attribute, cannon barrel definition
    firstTarget = True               # first time we're creating a target

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

        # add timer to handle shooting
        self.autoShootTimer = QTimer(self)
        self.autoShootTimer.timeout.connect(self._moveShot)

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

        self.newTarget()    # create a target

    # custom signals
    angleChanged = pyqtSignal(int, name="angleChanged")
    forceChanged = pyqtSignal(int, name="forceChanged")
    missed = pyqtSignal(name="missed")
    hit = pyqtSignal(name="hit")

    def newTarget(self):
        if CannonField.firstTarget:
            CannonField.firstTarget = False
            midnight = QTime(0, 0, 0)
            seed(midnight.secsTo(QTime.currentTime()))

        r = self._targetRect()
        self.target = QPoint(200 + randrange(0, 190),
                             10 + randrange(0, 255))
        self.repaint(r.unite(self._targetRect()))

    def shoot(self):
        ''' Shoots a 'shot' unless one is in the air. '''
        if self.autoShootTimer.isActive(): return

        self.timerCount = 0
        self.shoot_ang = self.ang
        self.shoot_f = self.f
        self.autoShootTimer.start(50)

    # 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

    @pyqtSlot()
    def _moveShot(self):
        # moves the 'shot' every 50 milliseconds when the
        # timer is fired in shoot()
        r = self._shotRect()
        self.timerCount += 1
        shotR = self._shotRect()

        if shotR.intersects(self._targetRect()):
            self.autoShootTimer.stop()
            self.hit.emit()
        elif (shotR.x() > self.width()) or (shotR.y() > self.height()):
            self.autoShootTimer.stop()
            self.missed.emit()
        else:
            r = r.unite(shotR)
        self.repaint(r)

    def _shotRect(self):
        # identifies where the 'shot' is on the screen and returns
        # its bounding rectangle
        gravity = 4
        time = self.timerCount / 4.0
        velocity = self.shoot_f
        radians = self.shoot_ang * 3.14159265 / 180

        velx = velocity * cos(radians)
        vely = velocity * sin(radians)
        x0 = (CannonField.bRect.right() + 5) * cos(radians)
        y0 = (CannonField.bRect.right() + 5) * sin(radians)
        x = x0 + velx * time
        y = y0 + vely * time - 0.5 * gravity * time * time

        r = QRect(0, 0, 6, 6)
        r.moveCenter(QPoint(int(x), self.height() - 1 - int(y)))
        return r

    def _targetRect(self):
        r = QRect(0, 0, 20, 10)
        r.moveCenter(QPoint(self.target.x(), self.height() - 1 - self.target.y()))
        return r

    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
    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):
        updateR = event.rect()
        p = QPainter(self)

        if updateR.intersects(self._cannonRect()):
            self.paintCannon(p)
        if self.autoShootTimer.isActive() and updateR.intersects(self._shotRect()):
            self.paintShot(p)
        if updateR.intersects(self._targetRect()):
            self.paintTarget(p)

    def paintTarget(self, p):
        p.setBrush(Qt.red)
        p.setPen(Qt.black)
        p.drawRect(self._targetRect())

    def paintShot(self, p):
        p.setBrush(Qt.black)
        p.setPen(Qt.NoPen)
        p.drawRect(self._shotRect())

    def paintCannon(self, p):
        cr = self._cannonRect()
        pix = QPixmap(cr.size())
        pix.fill(self, cr.topLeft())

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

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

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

        # rotate counter-clockwise 'ang' degrees around the origin
        # and draw the cannon's barrel
        tmp.rotate(-self.ang)
        tmp.drawRect(CannonField.bRect)
        tmp.end()

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

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

def main():
    import sys
    from PyQt4.QtGui import (QApplication)
    app = QApplication(sys.argv)    # required

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

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

if __name__ == '__main__':
    main()

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

'''
    PyQt4 conversion of Qt Tutorial 12
    
    In this example, we extend our LCDRange class to include a text label. 
    We also provide something to shoot at.
    
    The differences from 111_battle.pyw are:
        1. Refactored LCDRange and CannonField, moving them to separate modules.
           Makes them easier to modify and test
        2. MyWdiget
            - modified calls to LCDRange to provide widget names
                           
    BEHAVIOUR:
    =========
    The LCDRange widgets look a bit strange - the built-in layout management in 
    QVBox gives the labels too much space and the rest not enough. 
    We'll fix that in the next chapter.

            
last modified: 2012-01-21 jg
ref: 
    http://doc.trolltech.com/3.3/tutorial1-12.html
'''
import sys
from PyQt4.QtGui import (QApplication, QWidget, QPushButton, QFont,
                         QVBoxLayout, QGridLayout, QHBoxLayout)
from t112_lcdrange import (LCDRange)
from t112_cannon import (CannonField)

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(wlabel="ANGLE")
        angle.setRange(5, 70)

        # add LCDRange widget to handle force
        force = LCDRange(wlabel="FORCE")
        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)

        shootBtn = QPushButton('&Shoot', self)
        shootBtn.setFont(QFont("Times", 18, QFont.Bold))
        shootBtn.clicked.connect(cannonField.shoot)

        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)

        # add the 'shoot' button
        topBox = QHBoxLayout()
        grid.addLayout(topBox, 0, 1)
        topBox.addWidget(shootBtn)
        topBox.addStretch(1)

        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()