#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
PyQt4 conversion of Qt Tutorial 11
In this example we introduce a timer to implement animated shooting.
The differences from 110_battle.pyw are:
1. CannonField
- added class attribute 'bRect' defining cannon barrel size
- initialise a timer and appropriate handler
- added methods shoot(), _moveShot(), _shotRect()
- refactor paintEvent(), extracting out paintCannon() and
paintShot()
2. MyWidget - added a 'Shoot' button and event handler
NOTES:
=====
BEHAVIOUR:
=========
The cannon can shoot, but there's nothing to shoot at.
last modified: 2012-01-20 jg
ref:
http://doc.trolltech.com/3.3/tutorial1-11.html
'''
import sys
from math import (cos, sin)
from PyQt4.QtGui import (QApplication, QWidget, QPushButton, QFont,
QVBoxLayout, QGridLayout, QLCDNumber, QSlider,
QColor, QPainter, QSizePolicy, QPixmap,
QHBoxLayout)
from PyQt4.QtCore import (Qt, pyqtSlot, pyqtSignal, qWarning, QRect, QTimer,
QPoint)
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
bRect = QRect(33, -4, 15, 8) # class attribute, cannon barrel definition
def __init__(self, parent=None):
super(QWidget, self).__init__(parent)
self.setObjectName('cannonField')
self.ang = 45
self.f = 0 # force
# 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)
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
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.x() > self.width()) or (shotR.y() > self.height()):
self.autoShootTimer.stop()
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
# 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):
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)
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)
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)
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()
Sunday, January 22, 2012
Qt Tutorial #1-11 Giving it a Shot
This is based on Qt Tutorial #1-11 Giving it a Shot
Labels:
Qt Tutorial 1