本文通过Python3+PyQt5实现《python Qt Gui 快速编程》这本书的千足蛇动画程序,采用QGraphicsView,QGraphicsScene,QGraphicsItem,这个程序包含有多个文本,图片和框的页面。有些图形类在PyQt5已过时,所以本代码改动幅度比较大。本文实现复杂形状动画内容,一种通过项自身定时器,另一种采用窗口超时处理程序来实现项目移动碰撞等功能。
1,通过自身定时器实现,完整代码如下:
#!/usr/bin/env python3
import math
import random
import sys
from PyQt5.QtCore import (QTimer, QPointF, QRectF, Qt)
from PyQt5.QtWidgets import (QApplication, QDialog,
QGraphicsItem, QGraphicsScene, QGraphicsView, QHBoxLayout,
QPushButton, QSlider,QVBoxLayout)
from PyQt5.QtGui import (QBrush, QColor,QPainter,QPainterPath,QPolygonF)
SCENESIZE = 500
INTERVAL = 1
class Head(QGraphicsItem):
Rect = QRectF(-30, -20, 60, 40)
def __init__(self, color, angle, position):
super(Head, self).__init__()
self.color = color
self.angle = angle
self.setPos(position)
def boundingRect(self):
return Head.Rect
def shape(self):
path = QPainterPath()
path.addEllipse(Head.Rect)
return path
def paint(self, painter, option, widget=None):
painter.setPen(Qt.NoPen)
painter.setBrush(QBrush(self.color))
painter.drawEllipse(Head.Rect)
if option.levelOfDetailFromTransform(self.transform()) > 0.5: # Outer eyes
painter.setBrush(QBrush(Qt.yellow))
painter.drawEllipse(-12, -19, 8, 8)
painter.drawEllipse(-12, 11, 8, 8)
if option.levelOfDetailFromTransform(self.transform())> 0.8: # Inner eyes
painter.setBrush(QBrush(Qt.darkBlue))
painter.drawEllipse(-12, -19, 4, 4)
painter.drawEllipse(-12, 11, 4, 4)
if option.levelOfDetailFromTransform(self.transform()) > 0.9: # Nostrils
painter.setBrush(QBrush(Qt.white))
painter.drawEllipse(-27, -5, 2, 2)
painter.drawEllipse(-27, 3, 2, 2)
def advance(self, phase):
if phase == 0:
angle = self.angle
while True:
flipper = 1
angle += random.random() * random.choice((-1, 1))
offset = flipper * random.random()
x = self.x() + (offset * math.sin(math.radians(angle)))
y = self.y() + (offset * math.cos(math.radians(angle)))
if 0 <= x <= SCENESIZE and 0 <= y <= SCENESIZE:
break
else:
flipper = -1 if flipper == 1 else 1
self.angle = angle
self.position = QPointF(x, y)
else:
self.setRotation(random.random() * random.choice((-1, 1)))
self.setPos(self.position)
if self.scene():
for item in self.scene().collidingItems(self):
if isinstance(item, Head):
self.color.setRed(min(255, self.color.red() + 1))
else:
item.color.setBlue(min(255, item.color.blue() + 1))
class Segment(QGraphicsItem):
def __init__(self, color, offset, parent):
super(Segment, self).__init__(parent)
self.color = color
self.rect = QRectF(offset, -20, 30, 40)
self.path = QPainterPath()
self.path.addEllipse(self.rect)
x = offset + 15
y = -20
self.path.addPolygon(QPolygonF([QPointF(x, y),
QPointF(x - 5, y - 12), QPointF(x - 5, y)]))
self.path.closeSubpath()
y = 20
self.path.addPolygon(QPolygonF([QPointF(x, y),
QPointF(x - 5, y + 12), QPointF(x - 5, y)]))
self.path.closeSubpath()
self.change = 1
self.angle = 0
def boundingRect(self):
return self.path.boundingRect()
def shape(self):
return self.path
def paint(self, painter, option, widget=None):
painter.setPen(Qt.NoPen)
painter.setBrush(QBrush(self.color))
if option.levelOfDetailFromTransform(self.transform()) < 0.9:
painter.drawEllipse(self.rect)
else:
painter.drawPath(self.path)
def advance(self, phase):
if phase == 0:
matrix = self.transform()
matrix.reset()
self.setTransform(matrix)
self.angle += self.change * random.random()
if self.angle > 4.5:
self.change = -1
self.angle -= 0.00001
elif self.angle < -4.5:
self.change = 1
self.angle += 0.00001
elif phase == 1:
self.setRotation(self.angle)
class MainForm(QDialog):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
self.running = False
self.scene = QGraphicsScene(self)
self.scene.setSceneRect(0, 0, SCENESIZE, SCENESIZE)
self.scene.setItemIndexMethod(QGraphicsScene.NoIndex)
self.view = QGraphicsView()
self.view.setRenderHint(QPainter.Antialiasing)
self.view.setScene(self.scene)
self.view.setFocusPolicy(Qt.NoFocus)
zoomSlider = QSlider(Qt.Horizontal)
zoomSlider.setRange(5, 200)
zoomSlider.setValue(100)
self.pauseButton = QPushButton("Pa&use")
quitButton = QPushButton("&Quit")
quitButton.setFocusPolicy(Qt.NoFocus)
layout = QVBoxLayout()
layout.addWidget(self.view)
bottomLayout = QHBoxLayout()
bottomLayout.addWidget(self.pauseButton)
bottomLayout.addWidget(zoomSlider)
bottomLayout.addWidget(quitButton)
layout.addLayout(bottomLayout)
self.setLayout(layout)
zoomSlider.valueChanged[int].connect(self.zoom)
quitButton.clicked.connect(self.accept)
self.populate()
self.startTimer(INTERVAL)
self.setWindowTitle("Multipedes")
def zoom(self, value):
factor = value / 100.0
matrix=self.view.transform()
matrix.reset()
matrix.scale(factor, factor)
self.view.setTransform(matrix)
def pauseOrResume(self):
self.running = not self.running
self.pauseButton.setText("Pa&use" if self.running else "Res&ume")
def populate(self):
red, green, blue = 0, 150, 0
for i in range(random.randint(6, 10)):
angle = random.randint(0, 360)
offset = random.randint(0, SCENESIZE // 2)
half = SCENESIZE / 2
x = half + (offset * math.sin(math.radians(angle)))
y = half + (offset * math.cos(math.radians(angle)))
color = QColor(red, green, blue)
head = Head(color, angle, QPointF(x, y))
color = QColor(red, green + random.randint(10, 60), blue)
offset = 25
segment = Segment(color, offset, head)
for j in range(random.randint(3, 7)):
offset += 25
segment = Segment(color, offset, segment)
head.setRotation(random.randint(0, 360))
self.scene.addItem(head)
self.running = True
def timerEvent(self, event):
if not self.running:
return
dead = set()
items = self.scene.items()
if len(items) == 0:
self.populate()
return
heads = set()
for item in items:
if isinstance(item, Head):
heads.add(item)
if item.color.red() == 255 and random.random() > 0.75:
dead.add(item)
if len(heads) == 1:
dead = heads
del heads
while dead:
item = dead.pop()
self.scene.removeItem(item)
del item
self.scene.advance()
app = QApplication(sys.argv)
form = MainForm()
rect = QApplication.desktop().availableGeometry()
form.resize(int(rect.width() * 0.75), int(rect.height() * 0.9))
form.show()
app.exec_()
2,通过窗口超时处理程序实现,完整代码如下:
#!/usr/bin/env python3
import math
import random
import sys
from PyQt5.QtCore import (QTimer, QPointF, QRectF, Qt)
from PyQt5.QtWidgets import (QApplication, QDialog,
QGraphicsItem, QGraphicsScene, QGraphicsView, QHBoxLayout,
QPushButton, QSlider,QVBoxLayout)
from PyQt5.QtGui import (QBrush, QColor,QPainter,QPainterPath,QPolygonF)
SCENESIZE = 500
INTERVAL = 1
Running = False
class Head(QGraphicsItem):
Rect = QRectF(-30, -20, 60, 40)
def __init__(self, color, angle, position):
super(Head, self).__init__()
self.color = color
self.angle = angle
self.setPos(position)
self.timer = QTimer()
self.timer.timeout.connect(self.timeout)
self.timer.start(INTERVAL)
def boundingRect(self):
return Head.Rect
def shape(self):
path = QPainterPath()
path.addEllipse(Head.Rect)
return path
def paint(self, painter, option, widget=None):
painter.setPen(Qt.NoPen)
painter.setBrush(QBrush(self.color))
painter.drawEllipse(Head.Rect)
if option.levelOfDetailFromTransform(self.transform()) > 0.5: # Outer eyes
painter.setBrush(QBrush(Qt.yellow))
painter.drawEllipse(-12, -19, 8, 8)
painter.drawEllipse(-12, 11, 8, 8)
if option.levelOfDetailFromTransform(self.transform()) > 0.8: # Inner eyes
painter.setBrush(QBrush(Qt.darkBlue))
painter.drawEllipse(-12, -19, 4, 4)
painter.drawEllipse(-12, 11, 4, 4)
if option.levelOfDetailFromTransform(self.transform()) > 0.9: # Nostrils
painter.setBrush(QBrush(Qt.white))
painter.drawEllipse(-27, -5, 2, 2)
painter.drawEllipse(-27, 3, 2, 2)
def timeout(self):
if not Running:
return
angle = self.angle
while True:
flipper = 1
angle += random.random() * random.choice((-1, 1))
offset = flipper * random.random()
x = self.x() + (offset * math.sin(math.radians(angle)))
y = self.y() + (offset * math.cos(math.radians(angle)))
if 0 <= x <= SCENESIZE and 0 <= y <= SCENESIZE:
break
else:
flipper = -1 if flipper == 1 else 1
self.angle = angle
self.setRotation(random.random() * random.choice((-1, 1)))
self.setPos(QPointF(x, y))
if self.scene():
for item in self.scene().collidingItems(self):
if isinstance(item, Head):
self.color.setRed(min(255, self.color.red() + 1))
else:
item.color.setBlue(min(255, item.color.blue() + 1))
class Segment(QGraphicsItem):
def __init__(self, color, offset, parent):
super(Segment, self).__init__(parent)
self.color = color
self.rect = QRectF(offset, -20, 30, 40)
self.path = QPainterPath()
self.path.addEllipse(self.rect)
x = offset + 15
y = -20
self.path.addPolygon(QPolygonF([QPointF(x, y),
QPointF(x - 5, y - 12), QPointF(x - 5, y)]))
self.path.closeSubpath()
y = 20
self.path.addPolygon(QPolygonF([QPointF(x, y),
QPointF(x - 5, y + 12), QPointF(x - 5, y)]))
self.path.closeSubpath()
self.change = 1
self.angle = 0
self.timer = QTimer()
self.timer.timeout.connect(self.timeout)
self.timer.start(INTERVAL)
def boundingRect(self):
return self.path.boundingRect()
def shape(self):
return self.path
def paint(self, painter, option, widget=None):
painter.setPen(Qt.NoPen)
painter.setBrush(QBrush(self.color))
if option.levelOfDetailFromTransform(self.transform()) < 0.9:
painter.drawEllipse(self.rect)
else:
painter.drawPath(self.path)
def timeout(self):
if not Running:
return
matrix = self.transform()
matrix.reset()
self.setTransform(matrix)
self.angle += self.change * random.random()
if self.angle > 4.5:
self.change = -1
self.angle -= 0.00001
elif self.angle < -4.5:
self.change = 1
self.angle += 0.00001
self.setRotation(self.angle)
class MainForm(QDialog):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
self.scene = QGraphicsScene(self)
self.scene.setSceneRect(0, 0, SCENESIZE, SCENESIZE)
self.scene.setItemIndexMethod(QGraphicsScene.NoIndex)
self.view = QGraphicsView()
self.view.setRenderHint(QPainter.Antialiasing)
self.view.setScene(self.scene)
self.view.setFocusPolicy(Qt.NoFocus)
zoomSlider = QSlider(Qt.Horizontal)
zoomSlider.setRange(5, 200)
zoomSlider.setValue(100)
self.pauseButton = QPushButton("Pa&use")
quitButton = QPushButton("&Quit")
quitButton.setFocusPolicy(Qt.NoFocus)
layout = QVBoxLayout()
layout.addWidget(self.view)
bottomLayout = QHBoxLayout()
bottomLayout.addWidget(self.pauseButton)
bottomLayout.addWidget(zoomSlider)
bottomLayout.addWidget(quitButton)
layout.addLayout(bottomLayout)
self.setLayout(layout)
zoomSlider.valueChanged[int].connect(self.zoom)
self.pauseButton.clicked.connect(self.pauseOrResume)
quitButton.clicked.connect(self.accept)
self.populate()
self.startTimer(INTERVAL)
self.setWindowTitle("Multipedes")
def zoom(self, value):
factor = value / 100.0
matrix=self.view.transform()
matrix.reset()
matrix.scale(factor, factor)
self.view.setTransform(matrix)
def pauseOrResume(self):
global Running
Running = not Running
self.pauseButton.setText("Pa&use" if Running else "Res&ume")
def populate(self):
red, green, blue = 0, 150, 0
for i in range(random.randint(6, 10)):
angle = random.randint(0, 360)
offset = random.randint(0, SCENESIZE // 2)
half = SCENESIZE / 2
x = half + (offset * math.sin(math.radians(angle)))
y = half + (offset * math.cos(math.radians(angle)))
color = QColor(red, green, blue)
head = Head(color, angle, QPointF(x, y))
color = QColor(red, green + random.randint(10, 60), blue)
offset = 25
segment = Segment(color, offset, head)
for j in range(random.randint(3, 7)):
offset += 25
segment = Segment(color, offset, segment)
head.setRotation(random.randint(0, 360))
self.scene.addItem(head)
global Running
Running = True
def timerEvent(self, event):
if not Running:
return
dead = set()
items = self.scene.items()
if len(items) == 0:
self.populate()
return
heads = set()
for item in items:
if isinstance(item, Head):
heads.add(item)
if item.color.red() == 255 and random.random() > 0.75:
dead.add(item)
if len(heads) == 1:
dead = heads
del heads
while dead:
item = dead.pop()
self.scene.removeItem(item)
del item
app = QApplication(sys.argv)
form = MainForm()
rect = QApplication.desktop().availableGeometry()
form.resize(int(rect.width() * 0.75), int(rect.height() * 0.9))
form.show()
app.exec_()