mirror of
https://github.com/maicss/PyQt5-Chinese-tutorial.git
synced 2025-01-08 17:06:18 +08:00
本次更新完工,撒花~~
This commit is contained in:
parent
45180f79bf
commit
c6a2deb5e0
@ -33,5 +33,5 @@ PyQt5中文教程,翻译自 [zetcode](http://zetcode.com/gui/pyqt5/)
|
||||
|
||||
- 菜单和工具栏 【新增】 右键菜单 子菜单 勾选菜单
|
||||
- 事件和信号 【新增】 事件对象
|
||||
|
||||
- 绘图 【新增】 贝塞尔曲线
|
||||
|
||||
|
@ -11,6 +11,6 @@
|
||||
* [控件(2)](控件2.md)
|
||||
* [拖拽](拖拽.md)
|
||||
* [绘图](绘图.md)
|
||||
* [自定义控件](自定义控件.md)
|
||||
* [自定义组件](自定义组件.md)
|
||||
* [俄罗斯方块游戏](俄罗斯方块游戏.md)
|
||||
|
||||
|
802
original/俄罗斯方块游戏.md
Normal file
802
original/俄罗斯方块游戏.md
Normal file
@ -0,0 +1,802 @@
|
||||
Tetris in PyQt5
|
||||
|
||||
In this chapter, we will create a Tetris game clone.
|
||||
|
||||
Tetris
|
||||
|
||||
The Tetris game is one of the most popular computer games ever created. The original game was designed and programmed by a Russian programmer Alexey Pajitnov in 1985. Since then, Tetris is available on almost every computer platform in lots of variations.
|
||||
|
||||
Tetris is called a falling block puzzle game. In this game, we have seven different shapes called tetrominoes: an S-shape, a Z-shape, a T-shape, an L-shape, a Line-shape, a MirroredL-shape, and a Square-shape. Each of these shapes is formed with four squares. The shapes are falling down the board. The object of the Tetris game is to move and rotate the shapes so that they fit as much as possible. If we manage to form a row, the row is destroyed and we score. We play the Tetris game until we top out.
|
||||
|
||||
Tetrominoes
|
||||
Figure: Tetrominoes
|
||||
PyQt5 is a toolkit designed to create applications. There are other libraries which are targeted at creating computer games. Nevertheless, PyQt5 and other application toolkits can be used to create simple games.
|
||||
|
||||
Creating a computer game is a good way for enhancing programming skills.
|
||||
|
||||
The development
|
||||
|
||||
We do not have images for our Tetris game, we draw the tetrominoes using the drawing API available in the PyQt5 programming toolkit. Behind every computer game, there is a mathematical model. So it is in Tetris.
|
||||
|
||||
Some ideas behind the game:
|
||||
|
||||
We use a QtCore.QBasicTimer() to create a game cycle.
|
||||
The tetrominoes are drawn.
|
||||
The shapes move on a square by square basis (not pixel by pixel).
|
||||
Mathematically a board is a simple list of numbers.
|
||||
The code consists of four classes: Tetris, Board, Tetrominoe and Shape. The Tetris class sets up the game. The Board is where the game logic is written. The Tetrominoe class contains names for all tetris pieces and the Shape class contains the code for a tetris piece.
|
||||
|
||||
tetris.py
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
ZetCode PyQt5 tutorial
|
||||
|
||||
This is a Tetris game clone.
|
||||
|
||||
Author: Jan Bodnar
|
||||
Website: zetcode.com
|
||||
Last edited: August 2017
|
||||
"""
|
||||
|
||||
from PyQt5.QtWidgets import QMainWindow, QFrame, QDesktopWidget, QApplication
|
||||
from PyQt5.QtCore import Qt, QBasicTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QPainter, QColor
|
||||
import sys, random
|
||||
|
||||
class Tetris(QMainWindow):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.initUI()
|
||||
|
||||
|
||||
def initUI(self):
|
||||
'''initiates application UI'''
|
||||
|
||||
self.tboard = Board(self)
|
||||
self.setCentralWidget(self.tboard)
|
||||
|
||||
self.statusbar = self.statusBar()
|
||||
self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)
|
||||
|
||||
self.tboard.start()
|
||||
|
||||
self.resize(180, 380)
|
||||
self.center()
|
||||
self.setWindowTitle('Tetris')
|
||||
self.show()
|
||||
|
||||
|
||||
def center(self):
|
||||
'''centers the window on the screen'''
|
||||
|
||||
screen = QDesktopWidget().screenGeometry()
|
||||
size = self.geometry()
|
||||
self.move((screen.width()-size.width())/2,
|
||||
(screen.height()-size.height())/2)
|
||||
|
||||
|
||||
class Board(QFrame):
|
||||
|
||||
msg2Statusbar = pyqtSignal(str)
|
||||
|
||||
BoardWidth = 10
|
||||
BoardHeight = 22
|
||||
Speed = 300
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.initBoard()
|
||||
|
||||
|
||||
def initBoard(self):
|
||||
'''initiates board'''
|
||||
|
||||
self.timer = QBasicTimer()
|
||||
self.isWaitingAfterLine = False
|
||||
|
||||
self.curX = 0
|
||||
self.curY = 0
|
||||
self.numLinesRemoved = 0
|
||||
self.board = []
|
||||
|
||||
self.setFocusPolicy(Qt.StrongFocus)
|
||||
self.isStarted = False
|
||||
self.isPaused = False
|
||||
self.clearBoard()
|
||||
|
||||
|
||||
def shapeAt(self, x, y):
|
||||
'''determines shape at the board position'''
|
||||
|
||||
return self.board[(y * Board.BoardWidth) + x]
|
||||
|
||||
|
||||
def setShapeAt(self, x, y, shape):
|
||||
'''sets a shape at the board'''
|
||||
|
||||
self.board[(y * Board.BoardWidth) + x] = shape
|
||||
|
||||
|
||||
def squareWidth(self):
|
||||
'''returns the width of one square'''
|
||||
|
||||
return self.contentsRect().width() // Board.BoardWidth
|
||||
|
||||
|
||||
def squareHeight(self):
|
||||
'''returns the height of one square'''
|
||||
|
||||
return self.contentsRect().height() // Board.BoardHeight
|
||||
|
||||
|
||||
def start(self):
|
||||
'''starts game'''
|
||||
|
||||
if self.isPaused:
|
||||
return
|
||||
|
||||
self.isStarted = True
|
||||
self.isWaitingAfterLine = False
|
||||
self.numLinesRemoved = 0
|
||||
self.clearBoard()
|
||||
|
||||
self.msg2Statusbar.emit(str(self.numLinesRemoved))
|
||||
|
||||
self.newPiece()
|
||||
self.timer.start(Board.Speed, self)
|
||||
|
||||
|
||||
def pause(self):
|
||||
'''pauses game'''
|
||||
|
||||
if not self.isStarted:
|
||||
return
|
||||
|
||||
self.isPaused = not self.isPaused
|
||||
|
||||
if self.isPaused:
|
||||
self.timer.stop()
|
||||
self.msg2Statusbar.emit("paused")
|
||||
|
||||
else:
|
||||
self.timer.start(Board.Speed, self)
|
||||
self.msg2Statusbar.emit(str(self.numLinesRemoved))
|
||||
|
||||
self.update()
|
||||
|
||||
|
||||
def paintEvent(self, event):
|
||||
'''paints all shapes of the game'''
|
||||
|
||||
painter = QPainter(self)
|
||||
rect = self.contentsRect()
|
||||
|
||||
boardTop = rect.bottom() - Board.BoardHeight * self.squareHeight()
|
||||
|
||||
for i in range(Board.BoardHeight):
|
||||
for j in range(Board.BoardWidth):
|
||||
shape = self.shapeAt(j, Board.BoardHeight - i - 1)
|
||||
|
||||
if shape != Tetrominoe.NoShape:
|
||||
self.drawSquare(painter,
|
||||
rect.left() + j * self.squareWidth(),
|
||||
boardTop + i * self.squareHeight(), shape)
|
||||
|
||||
if self.curPiece.shape() != Tetrominoe.NoShape:
|
||||
|
||||
for i in range(4):
|
||||
|
||||
x = self.curX + self.curPiece.x(i)
|
||||
y = self.curY - self.curPiece.y(i)
|
||||
self.drawSquare(painter, rect.left() + x * self.squareWidth(),
|
||||
boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
|
||||
self.curPiece.shape())
|
||||
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
'''processes key press events'''
|
||||
|
||||
if not self.isStarted or self.curPiece.shape() == Tetrominoe.NoShape:
|
||||
super(Board, self).keyPressEvent(event)
|
||||
return
|
||||
|
||||
key = event.key()
|
||||
|
||||
if key == Qt.Key_P:
|
||||
self.pause()
|
||||
return
|
||||
|
||||
if self.isPaused:
|
||||
return
|
||||
|
||||
elif key == Qt.Key_Left:
|
||||
self.tryMove(self.curPiece, self.curX - 1, self.curY)
|
||||
|
||||
elif key == Qt.Key_Right:
|
||||
self.tryMove(self.curPiece, self.curX + 1, self.curY)
|
||||
|
||||
elif key == Qt.Key_Down:
|
||||
self.tryMove(self.curPiece.rotateRight(), self.curX, self.curY)
|
||||
|
||||
elif key == Qt.Key_Up:
|
||||
self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)
|
||||
|
||||
elif key == Qt.Key_Space:
|
||||
self.dropDown()
|
||||
|
||||
elif key == Qt.Key_D:
|
||||
self.oneLineDown()
|
||||
|
||||
else:
|
||||
super(Board, self).keyPressEvent(event)
|
||||
|
||||
|
||||
def timerEvent(self, event):
|
||||
'''handles timer event'''
|
||||
|
||||
if event.timerId() == self.timer.timerId():
|
||||
|
||||
if self.isWaitingAfterLine:
|
||||
self.isWaitingAfterLine = False
|
||||
self.newPiece()
|
||||
else:
|
||||
self.oneLineDown()
|
||||
|
||||
else:
|
||||
super(Board, self).timerEvent(event)
|
||||
|
||||
|
||||
def clearBoard(self):
|
||||
'''clears shapes from the board'''
|
||||
|
||||
for i in range(Board.BoardHeight * Board.BoardWidth):
|
||||
self.board.append(Tetrominoe.NoShape)
|
||||
|
||||
|
||||
def dropDown(self):
|
||||
'''drops down a shape'''
|
||||
|
||||
newY = self.curY
|
||||
|
||||
while newY > 0:
|
||||
|
||||
if not self.tryMove(self.curPiece, self.curX, newY - 1):
|
||||
break
|
||||
|
||||
newY -= 1
|
||||
|
||||
self.pieceDropped()
|
||||
|
||||
|
||||
def oneLineDown(self):
|
||||
'''goes one line down with a shape'''
|
||||
|
||||
if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
|
||||
self.pieceDropped()
|
||||
|
||||
|
||||
def pieceDropped(self):
|
||||
'''after dropping shape, remove full lines and create new shape'''
|
||||
|
||||
for i in range(4):
|
||||
|
||||
x = self.curX + self.curPiece.x(i)
|
||||
y = self.curY - self.curPiece.y(i)
|
||||
self.setShapeAt(x, y, self.curPiece.shape())
|
||||
|
||||
self.removeFullLines()
|
||||
|
||||
if not self.isWaitingAfterLine:
|
||||
self.newPiece()
|
||||
|
||||
|
||||
def removeFullLines(self):
|
||||
'''removes all full lines from the board'''
|
||||
|
||||
numFullLines = 0
|
||||
rowsToRemove = []
|
||||
|
||||
for i in range(Board.BoardHeight):
|
||||
|
||||
n = 0
|
||||
for j in range(Board.BoardWidth):
|
||||
if not self.shapeAt(j, i) == Tetrominoe.NoShape:
|
||||
n = n + 1
|
||||
|
||||
if n == 10:
|
||||
rowsToRemove.append(i)
|
||||
|
||||
rowsToRemove.reverse()
|
||||
|
||||
|
||||
for m in rowsToRemove:
|
||||
|
||||
for k in range(m, Board.BoardHeight):
|
||||
for l in range(Board.BoardWidth):
|
||||
self.setShapeAt(l, k, self.shapeAt(l, k + 1))
|
||||
|
||||
numFullLines = numFullLines + len(rowsToRemove)
|
||||
|
||||
if numFullLines > 0:
|
||||
|
||||
self.numLinesRemoved = self.numLinesRemoved + numFullLines
|
||||
self.msg2Statusbar.emit(str(self.numLinesRemoved))
|
||||
|
||||
self.isWaitingAfterLine = True
|
||||
self.curPiece.setShape(Tetrominoe.NoShape)
|
||||
self.update()
|
||||
|
||||
|
||||
def newPiece(self):
|
||||
'''creates a new shape'''
|
||||
|
||||
self.curPiece = Shape()
|
||||
self.curPiece.setRandomShape()
|
||||
self.curX = Board.BoardWidth // 2 + 1
|
||||
self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
|
||||
|
||||
if not self.tryMove(self.curPiece, self.curX, self.curY):
|
||||
|
||||
self.curPiece.setShape(Tetrominoe.NoShape)
|
||||
self.timer.stop()
|
||||
self.isStarted = False
|
||||
self.msg2Statusbar.emit("Game over")
|
||||
|
||||
|
||||
|
||||
def tryMove(self, newPiece, newX, newY):
|
||||
'''tries to move a shape'''
|
||||
|
||||
for i in range(4):
|
||||
|
||||
x = newX + newPiece.x(i)
|
||||
y = newY - newPiece.y(i)
|
||||
|
||||
if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
|
||||
return False
|
||||
|
||||
if self.shapeAt(x, y) != Tetrominoe.NoShape:
|
||||
return False
|
||||
|
||||
self.curPiece = newPiece
|
||||
self.curX = newX
|
||||
self.curY = newY
|
||||
self.update()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def drawSquare(self, painter, x, y, shape):
|
||||
'''draws a square of a shape'''
|
||||
|
||||
colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
|
||||
0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
|
||||
|
||||
color = QColor(colorTable[shape])
|
||||
painter.fillRect(x + 1, y + 1, self.squareWidth() - 2,
|
||||
self.squareHeight() - 2, color)
|
||||
|
||||
painter.setPen(color.lighter())
|
||||
painter.drawLine(x, y + self.squareHeight() - 1, x, y)
|
||||
painter.drawLine(x, y, x + self.squareWidth() - 1, y)
|
||||
|
||||
painter.setPen(color.darker())
|
||||
painter.drawLine(x + 1, y + self.squareHeight() - 1,
|
||||
x + self.squareWidth() - 1, y + self.squareHeight() - 1)
|
||||
painter.drawLine(x + self.squareWidth() - 1,
|
||||
y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)
|
||||
|
||||
|
||||
class Tetrominoe(object):
|
||||
|
||||
NoShape = 0
|
||||
ZShape = 1
|
||||
SShape = 2
|
||||
LineShape = 3
|
||||
TShape = 4
|
||||
SquareShape = 5
|
||||
LShape = 6
|
||||
MirroredLShape = 7
|
||||
|
||||
|
||||
class Shape(object):
|
||||
|
||||
coordsTable = (
|
||||
((0, 0), (0, 0), (0, 0), (0, 0)),
|
||||
((0, -1), (0, 0), (-1, 0), (-1, 1)),
|
||||
((0, -1), (0, 0), (1, 0), (1, 1)),
|
||||
((0, -1), (0, 0), (0, 1), (0, 2)),
|
||||
((-1, 0), (0, 0), (1, 0), (0, 1)),
|
||||
((0, 0), (1, 0), (0, 1), (1, 1)),
|
||||
((-1, -1), (0, -1), (0, 0), (0, 1)),
|
||||
((1, -1), (0, -1), (0, 0), (0, 1))
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.coords = [[0,0] for i in range(4)]
|
||||
self.pieceShape = Tetrominoe.NoShape
|
||||
|
||||
self.setShape(Tetrominoe.NoShape)
|
||||
|
||||
|
||||
def shape(self):
|
||||
'''returns shape'''
|
||||
|
||||
return self.pieceShape
|
||||
|
||||
|
||||
def setShape(self, shape):
|
||||
'''sets a shape'''
|
||||
|
||||
table = Shape.coordsTable[shape]
|
||||
|
||||
for i in range(4):
|
||||
for j in range(2):
|
||||
self.coords[i][j] = table[i][j]
|
||||
|
||||
self.pieceShape = shape
|
||||
|
||||
|
||||
def setRandomShape(self):
|
||||
'''chooses a random shape'''
|
||||
|
||||
self.setShape(random.randint(1, 7))
|
||||
|
||||
|
||||
def x(self, index):
|
||||
'''returns x coordinate'''
|
||||
|
||||
return self.coords[index][0]
|
||||
|
||||
|
||||
def y(self, index):
|
||||
'''returns y coordinate'''
|
||||
|
||||
return self.coords[index][1]
|
||||
|
||||
|
||||
def setX(self, index, x):
|
||||
'''sets x coordinate'''
|
||||
|
||||
self.coords[index][0] = x
|
||||
|
||||
|
||||
def setY(self, index, y):
|
||||
'''sets y coordinate'''
|
||||
|
||||
self.coords[index][1] = y
|
||||
|
||||
|
||||
def minX(self):
|
||||
'''returns min x value'''
|
||||
|
||||
m = self.coords[0][0]
|
||||
for i in range(4):
|
||||
m = min(m, self.coords[i][0])
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def maxX(self):
|
||||
'''returns max x value'''
|
||||
|
||||
m = self.coords[0][0]
|
||||
for i in range(4):
|
||||
m = max(m, self.coords[i][0])
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def minY(self):
|
||||
'''returns min y value'''
|
||||
|
||||
m = self.coords[0][1]
|
||||
for i in range(4):
|
||||
m = min(m, self.coords[i][1])
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def maxY(self):
|
||||
'''returns max y value'''
|
||||
|
||||
m = self.coords[0][1]
|
||||
for i in range(4):
|
||||
m = max(m, self.coords[i][1])
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def rotateLeft(self):
|
||||
'''rotates shape to the left'''
|
||||
|
||||
if self.pieceShape == Tetrominoe.SquareShape:
|
||||
return self
|
||||
|
||||
result = Shape()
|
||||
result.pieceShape = self.pieceShape
|
||||
|
||||
for i in range(4):
|
||||
|
||||
result.setX(i, self.y(i))
|
||||
result.setY(i, -self.x(i))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def rotateRight(self):
|
||||
'''rotates shape to the right'''
|
||||
|
||||
if self.pieceShape == Tetrominoe.SquareShape:
|
||||
return self
|
||||
|
||||
result = Shape()
|
||||
result.pieceShape = self.pieceShape
|
||||
|
||||
for i in range(4):
|
||||
|
||||
result.setX(i, -self.y(i))
|
||||
result.setY(i, self.x(i))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
app = QApplication([])
|
||||
tetris = Tetris()
|
||||
sys.exit(app.exec_())
|
||||
The game is simplified a bit so that it is easier to understand. The game starts immediately after it is launched. We can pause the game by pressing the p key. The Space key will drop the tetris piece instantly to the bottom. The game goes at constant speed, no acceleration is implemented. The score is the number of lines that we have removed.
|
||||
|
||||
self.tboard = Board(self)
|
||||
self.setCentralWidget(self.tboard)
|
||||
An instance of the Board class is created and set to be the central widget of the application.
|
||||
|
||||
self.statusbar = self.statusBar()
|
||||
self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)
|
||||
We create a statusbar where we will display messages. We will display three possible messages: the number of lines already removed, the paused message, or the game over message. The msg2Statusbar is a custom signal that is implemented in the Board class. The showMessage() is a built-in method that displays a message on a statusbar.
|
||||
|
||||
self.tboard.start()
|
||||
This line initiates the game.
|
||||
|
||||
class Board(QFrame):
|
||||
|
||||
msg2Statusbar = pyqtSignal(str)
|
||||
...
|
||||
A custom signal is created with pyqtSignal. The msg2Statusbar is a signal that is emitted when we want to write a message or the score to the statusbar.
|
||||
|
||||
BoardWidth = 10
|
||||
BoardHeight = 22
|
||||
Speed = 300
|
||||
These are Board's class variables. The BoardWidth and the BoardHeight define the size of the board in blocks. The Speed defines the speed of the game. Each 300 ms a new game cycle will start.
|
||||
|
||||
...
|
||||
self.curX = 0
|
||||
self.curY = 0
|
||||
self.numLinesRemoved = 0
|
||||
self.board = []
|
||||
...
|
||||
In the initBoard() method we initialize some important variables. The self.board variable is a list of numbers from 0 to 7. It represents the position of various shapes and remains of the shapes on the board.
|
||||
|
||||
def shapeAt(self, x, y):
|
||||
'''determines shape at the board position'''
|
||||
|
||||
return self.board[(y * Board.BoardWidth) + x]
|
||||
The shapeAt() method determines the type of a shape at a given block.
|
||||
|
||||
def squareWidth(self):
|
||||
'''returns the width of one square'''
|
||||
|
||||
return self.contentsRect().width() // Board.BoardWidth
|
||||
The board can be dynamically resized. As a consequence, the size of a block may change. The squareWidth() calculates the width of the single square in pixels and returns it. The Board.BoardWidth is the size of the board in blocks.
|
||||
|
||||
def pause(self):
|
||||
'''pauses game'''
|
||||
|
||||
if not self.isStarted:
|
||||
return
|
||||
|
||||
self.isPaused = not self.isPaused
|
||||
|
||||
if self.isPaused:
|
||||
self.timer.stop()
|
||||
self.msg2Statusbar.emit("paused")
|
||||
|
||||
else:
|
||||
self.timer.start(Board.Speed, self)
|
||||
self.msg2Statusbar.emit(str(self.numLinesRemoved))
|
||||
|
||||
self.update()
|
||||
The pause() method pauses the game. It stops the timer and displays a message on the statusbar.
|
||||
|
||||
def paintEvent(self, event):
|
||||
'''paints all shapes of the game'''
|
||||
|
||||
painter = QPainter(self)
|
||||
rect = self.contentsRect()
|
||||
...
|
||||
The painting happens in the paintEvent() method. The QPainter is responsible for all low-level painting in PyQt5.
|
||||
|
||||
for i in range(Board.BoardHeight):
|
||||
for j in range(Board.BoardWidth):
|
||||
shape = self.shapeAt(j, Board.BoardHeight - i - 1)
|
||||
|
||||
if shape != Tetrominoe.NoShape:
|
||||
self.drawSquare(painter,
|
||||
rect.left() + j * self.squareWidth(),
|
||||
boardTop + i * self.squareHeight(), shape)
|
||||
The painting of the game is divided into two steps. In the first step, we draw all the shapes, or remains of the shapes that have been dropped to the bottom of the board. All the squares are remembered in the self.board list variable. The variable is accessed using the shapeAt() method.
|
||||
|
||||
if self.curPiece.shape() != Tetrominoe.NoShape:
|
||||
|
||||
for i in range(4):
|
||||
|
||||
x = self.curX + self.curPiece.x(i)
|
||||
y = self.curY - self.curPiece.y(i)
|
||||
self.drawSquare(painter, rect.left() + x * self.squareWidth(),
|
||||
boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
|
||||
self.curPiece.shape())
|
||||
The next step is the drawing of the actual piece that is falling down.
|
||||
|
||||
elif key == Qt.Key_Right:
|
||||
self.tryMove(self.curPiece, self.curX + 1, self.curY)
|
||||
In the keyPressEvent() method we check for pressed keys. If we press the right arrow key, we try to move the piece to the right. We say try because the piece might not be able to move.
|
||||
|
||||
elif key == Qt.Key_Up:
|
||||
self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)
|
||||
The Up arrow key will rotate the falling piece to the left.
|
||||
|
||||
elif key == Qt.Key_Space:
|
||||
self.dropDown()
|
||||
The Space key will drop the falling piece instantly to the bottom.
|
||||
|
||||
elif key == Qt.Key_D:
|
||||
self.oneLineDown()
|
||||
Pressing the d key, the piece will go one block down. It can be used to accellerate the falling of a piece a bit.
|
||||
|
||||
def timerEvent(self, event):
|
||||
'''handles timer event'''
|
||||
|
||||
if event.timerId() == self.timer.timerId():
|
||||
|
||||
if self.isWaitingAfterLine:
|
||||
self.isWaitingAfterLine = False
|
||||
self.newPiece()
|
||||
else:
|
||||
self.oneLineDown()
|
||||
|
||||
else:
|
||||
super(Board, self).timerEvent(event)
|
||||
In the timer event, we either create a new piece after the previous one was dropped to the bottom or we move a falling piece one line down.
|
||||
|
||||
def clearBoard(self):
|
||||
'''clears shapes from the board'''
|
||||
|
||||
for i in range(Board.BoardHeight * Board.BoardWidth):
|
||||
self.board.append(Tetrominoe.NoShape)
|
||||
The clearBoard() method clears the board by setting Tetrominoe.NoShape at each block of the board.
|
||||
|
||||
def removeFullLines(self):
|
||||
'''removes all full lines from the board'''
|
||||
|
||||
numFullLines = 0
|
||||
rowsToRemove = []
|
||||
|
||||
for i in range(Board.BoardHeight):
|
||||
|
||||
n = 0
|
||||
for j in range(Board.BoardWidth):
|
||||
if not self.shapeAt(j, i) == Tetrominoe.NoShape:
|
||||
n = n + 1
|
||||
|
||||
if n == 10:
|
||||
rowsToRemove.append(i)
|
||||
|
||||
rowsToRemove.reverse()
|
||||
|
||||
|
||||
for m in rowsToRemove:
|
||||
|
||||
for k in range(m, Board.BoardHeight):
|
||||
for l in range(Board.BoardWidth):
|
||||
self.setShapeAt(l, k, self.shapeAt(l, k + 1))
|
||||
|
||||
numFullLines = numFullLines + len(rowsToRemove)
|
||||
...
|
||||
If the piece hits the bottom, we call the removeFullLines() method. We find out all full lines and remove them. We do it by moving all lines above the current full line to be removed one line down. Notice that we reverse the order of the lines to be removed. Otherwise, it would not work correctly. In our case we use a naive gravity. This means that the pieces may be floating above empty gaps.
|
||||
|
||||
def newPiece(self):
|
||||
'''creates a new shape'''
|
||||
|
||||
self.curPiece = Shape()
|
||||
self.curPiece.setRandomShape()
|
||||
self.curX = Board.BoardWidth // 2 + 1
|
||||
self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
|
||||
|
||||
if not self.tryMove(self.curPiece, self.curX, self.curY):
|
||||
|
||||
self.curPiece.setShape(Tetrominoe.NoShape)
|
||||
self.timer.stop()
|
||||
self.isStarted = False
|
||||
self.msg2Statusbar.emit("Game over")
|
||||
The newPiece() method creates randomly a new tetris piece. If the piece cannot go into its initial position, the game is over.
|
||||
|
||||
def tryMove(self, newPiece, newX, newY):
|
||||
'''tries to move a shape'''
|
||||
|
||||
for i in range(4):
|
||||
|
||||
x = newX + newPiece.x(i)
|
||||
y = newY - newPiece.y(i)
|
||||
|
||||
if x < 0 or x &g;= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
|
||||
return False
|
||||
|
||||
if self.shapeAt(x, y) != Tetrominoe.NoShape:
|
||||
return False
|
||||
|
||||
self.curPiece = newPiece
|
||||
self.curX = newX
|
||||
self.curY = newY
|
||||
self.update()
|
||||
|
||||
return True
|
||||
In the tryMove() method we try to move our shapes. If the shape is at the edge of the board or is adjacent to some other piece, we return False. Otherwise we place the current falling piece to a new position.
|
||||
|
||||
class Tetrominoe(object):
|
||||
|
||||
NoShape = 0
|
||||
ZShape = 1
|
||||
SShape = 2
|
||||
LineShape = 3
|
||||
TShape = 4
|
||||
SquareShape = 5
|
||||
LShape = 6
|
||||
MirroredLShape = 7
|
||||
The Tetrominoe class holds names of all possible shapes. We have also a NoShape for an empty space.
|
||||
|
||||
The Shape class saves information about a tetris piece.
|
||||
|
||||
class Shape(object):
|
||||
|
||||
coordsTable = (
|
||||
((0, 0), (0, 0), (0, 0), (0, 0)),
|
||||
((0, -1), (0, 0), (-1, 0), (-1, 1)),
|
||||
...
|
||||
)
|
||||
...
|
||||
The coordsTable tuple holds all possible coordinate values of our tetris pieces. This is a template from which all pieces take their coordinate values.
|
||||
|
||||
self.coords = [[0,0] for i in range(4)]
|
||||
Upon creation we create an empty coordinates list. The list will save the coordinates of the tetris piece.
|
||||
|
||||
Coordinates
|
||||
Figure: Coordinates
|
||||
The above image will help understand the coordinate values a bit more. For example, the tuples (0, -1), (0, 0), (-1, 0), (-1, -1) represent a Z-shape. The diagram illustrates the shape.
|
||||
|
||||
def rotateLeft(self):
|
||||
'''rotates shape to the left'''
|
||||
|
||||
if self.pieceShape == Tetrominoe.SquareShape:
|
||||
return self
|
||||
|
||||
result = Shape()
|
||||
result.pieceShape = self.pieceShape
|
||||
|
||||
for i in range(4):
|
||||
|
||||
result.setX(i, self.y(i))
|
||||
result.setY(i, -self.x(i))
|
||||
|
||||
return result
|
||||
The rotateLeft() method rotates a piece to the left. The square does not have to be rotated. That is why we simply return the reference to the current object. A new piece is created and its coordinates are set to the ones of the rotated piece.
|
||||
|
||||
Tetris
|
||||
Figure: Tetris
|
||||
This was a Tetris game in PyQt5.
|
205
original/自定义组件.md
Normal file
205
original/自定义组件.md
Normal file
@ -0,0 +1,205 @@
|
||||
Custom widgets in PyQt5
|
||||
|
||||
PyQt5 has a rich set of widgets. However, no toolkit can provide all widgets that programmers might need in their applications. Toolkits usually provide only the most common widgets like buttons, text widgets, or sliders. If there is a need for a more specialised widget, we must create it ourselves.
|
||||
|
||||
Custom widgets are created by using the drawing tools provided by the toolkit. There are two basic possibilities: a programmer can modify or enhance an existing widget or he can create a custom widget from scratch.
|
||||
|
||||
Burning widget
|
||||
|
||||
This is a widget that we can see in Nero, K3B, or other CD/DVD burning software.
|
||||
|
||||
customwidget.py
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
ZetCode PyQt5 tutorial
|
||||
|
||||
In this example, we create a custom widget.
|
||||
|
||||
Author: Jan Bodnar
|
||||
Website: zetcode.com
|
||||
Last edited: August 2017
|
||||
"""
|
||||
|
||||
from PyQt5.QtWidgets import (QWidget, QSlider, QApplication,
|
||||
QHBoxLayout, QVBoxLayout)
|
||||
from PyQt5.QtCore import QObject, Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QPainter, QFont, QColor, QPen
|
||||
import sys
|
||||
|
||||
class Communicate(QObject):
|
||||
|
||||
updateBW = pyqtSignal(int)
|
||||
|
||||
|
||||
class BurningWidget(QWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.initUI()
|
||||
|
||||
|
||||
def initUI(self):
|
||||
|
||||
self.setMinimumSize(1, 30)
|
||||
self.value = 75
|
||||
self.num = [75, 150, 225, 300, 375, 450, 525, 600, 675]
|
||||
|
||||
|
||||
def setValue(self, value):
|
||||
|
||||
self.value = value
|
||||
|
||||
|
||||
def paintEvent(self, e):
|
||||
|
||||
qp = QPainter()
|
||||
qp.begin(self)
|
||||
self.drawWidget(qp)
|
||||
qp.end()
|
||||
|
||||
|
||||
def drawWidget(self, qp):
|
||||
|
||||
MAX_CAPACITY = 700
|
||||
OVER_CAPACITY = 750
|
||||
|
||||
font = QFont('Serif', 7, QFont.Light)
|
||||
qp.setFont(font)
|
||||
|
||||
size = self.size()
|
||||
w = size.width()
|
||||
h = size.height()
|
||||
|
||||
step = int(round(w / 10))
|
||||
|
||||
|
||||
till = int(((w / OVER_CAPACITY) * self.value))
|
||||
full = int(((w / OVER_CAPACITY) * MAX_CAPACITY))
|
||||
|
||||
if self.value >= MAX_CAPACITY:
|
||||
|
||||
qp.setPen(QColor(255, 255, 255))
|
||||
qp.setBrush(QColor(255, 255, 184))
|
||||
qp.drawRect(0, 0, full, h)
|
||||
qp.setPen(QColor(255, 175, 175))
|
||||
qp.setBrush(QColor(255, 175, 175))
|
||||
qp.drawRect(full, 0, till-full, h)
|
||||
|
||||
else:
|
||||
|
||||
qp.setPen(QColor(255, 255, 255))
|
||||
qp.setBrush(QColor(255, 255, 184))
|
||||
qp.drawRect(0, 0, till, h)
|
||||
|
||||
|
||||
pen = QPen(QColor(20, 20, 20), 1,
|
||||
Qt.SolidLine)
|
||||
|
||||
qp.setPen(pen)
|
||||
qp.setBrush(Qt.NoBrush)
|
||||
qp.drawRect(0, 0, w-1, h-1)
|
||||
|
||||
j = 0
|
||||
|
||||
for i in range(step, 10*step, step):
|
||||
|
||||
qp.drawLine(i, 0, i, 5)
|
||||
metrics = qp.fontMetrics()
|
||||
fw = metrics.width(str(self.num[j]))
|
||||
qp.drawText(i-fw/2, h/2, str(self.num[j]))
|
||||
j = j + 1
|
||||
|
||||
|
||||
class Example(QWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.initUI()
|
||||
|
||||
|
||||
def initUI(self):
|
||||
|
||||
OVER_CAPACITY = 750
|
||||
|
||||
sld = QSlider(Qt.Horizontal, self)
|
||||
sld.setFocusPolicy(Qt.NoFocus)
|
||||
sld.setRange(1, OVER_CAPACITY)
|
||||
sld.setValue(75)
|
||||
sld.setGeometry(30, 40, 150, 30)
|
||||
|
||||
self.c = Communicate()
|
||||
self.wid = BurningWidget()
|
||||
self.c.updateBW[int].connect(self.wid.setValue)
|
||||
|
||||
sld.valueChanged[int].connect(self.changeValue)
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addWidget(self.wid)
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addStretch(1)
|
||||
vbox.addLayout(hbox)
|
||||
self.setLayout(vbox)
|
||||
|
||||
self.setGeometry(300, 300, 390, 210)
|
||||
self.setWindowTitle('Burning widget')
|
||||
self.show()
|
||||
|
||||
|
||||
def changeValue(self, value):
|
||||
|
||||
self.c.updateBW.emit(value)
|
||||
self.wid.repaint()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
ex = Example()
|
||||
sys.exit(app.exec_())
|
||||
In our example, we have a QSlider and a custom widget. The slider controls the custom widget. This widget shows graphically the total capacity of a medium and the free space available to us. The minimum value of our custom widget is 1, the maximum is OVER_CAPACITY. If we reach value MAX_CAPACITY, we begin drawing in red colour. This normally indicates overburning.
|
||||
|
||||
The burning widget is placed at the bottom of the window. This is achieved using one QHBoxLayout and one QVBoxLayout.
|
||||
|
||||
class BurningWidget(QWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
The burning widget it based on the QWidget widget.
|
||||
|
||||
self.setMinimumSize(1, 30)
|
||||
We change the minimum size (height) of the widget. The default value is a bit small for us.
|
||||
|
||||
font = QFont('Serif', 7, QFont.Light)
|
||||
qp.setFont(font)
|
||||
We use a smaller font than the default one. This better suits our needs.
|
||||
|
||||
size = self.size()
|
||||
w = size.width()
|
||||
h = size.height()
|
||||
|
||||
step = int(round(w / 10))
|
||||
|
||||
|
||||
till = int(((w / OVER_CAPACITY) * self.value))
|
||||
full = int(((w / OVER_CAPACITY) * MAX_CAPACITY))
|
||||
We draw the widget dynamically. The greater is the window, the greater is the burning widget and vice versa. That is why we must calculate the size of the widget onto which we draw the custom widget. The till parameter determines the total size to be drawn. This value comes from the slider widget. It is a proportion of the whole area. The full parameter determines the point where we begin to draw in red colour.
|
||||
|
||||
The actual drawing consists of three steps. We draw the yellow or the red and yellow rectangle. Then we draw the vertical lines which divide the widget into several parts. Finally, we draw the numbers which indicate the capacity of the medium.
|
||||
|
||||
metrics = qp.fontMetrics()
|
||||
fw = metrics.width(str(self.num[j]))
|
||||
qp.drawText(i-fw/2, h/2, str(self.num[j]))
|
||||
We use font metrics to draw the text. We must know the width of the text in order to center it around the vertical line.
|
||||
|
||||
def changeValue(self, value):
|
||||
|
||||
self.c.updateBW.emit(value)
|
||||
self.wid.repaint()
|
||||
When we move the slider, the changeValue() method is called. Inside the method, we send a custom updateBW signal with a parameter. The parameter is the current value of the slider. The value is later used to calculate the capacity of the Burning widget to be drawn. The custom widget is then repainted.
|
||||
|
||||
The burning widget
|
||||
Figure: The burning widget
|
||||
In this part of the PyQt5 tutorial, we created a custom widget.
|
121
俄罗斯方块游戏.md
121
俄罗斯方块游戏.md
@ -4,6 +4,8 @@
|
||||
|
||||
## Tetris
|
||||
|
||||
> 译注:称呼:方块是由四个小方格组成的
|
||||
|
||||
俄罗斯方块游戏是世界上最流行的游戏之一。是由一名叫Alexey Pajitnov的俄罗斯程序员在1985年制作的,从那时起,这个游戏就风靡了各个游戏平台。
|
||||
|
||||
俄罗斯方块归类为下落块迷宫游戏。游戏有7个基本形状:S、Z、T、L、反向L、直线、方块,每个形状都由4个方块组成,方块最终都会落到屏幕底部。所以玩家通过控制形状的左右位置和旋转,让每个形状都以合适的位置落下,如果有一行全部被方块填充,这行就会消失,并且得分。游戏结束的条件是有形状接触到了屏幕顶部。
|
||||
@ -27,8 +29,9 @@ PyQt5是专门为创建图形界面产生的,里面一些专门为制作游戏
|
||||
- 模型的运动是以小块为基础单位的,不是按像素
|
||||
- 从数学意义上来说,模型就是就是一串数字而已
|
||||
|
||||
代码由四个类组成:Tetris, Board, Tetrominoe和Shape。Tetris类创建游戏,Board是游戏逻辑。Tetrominoe包含了所有的砖块,Shape是所有砖块的代码。
|
||||
```
|
||||
代码由四个类组成:Tetris, Board, Tetrominoe和Shape。Tetris类创建游戏,Board是游戏主要逻辑。Tetrominoe包含了所有的砖块,Shape是所有砖块的代码。
|
||||
|
||||
```python
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@ -37,16 +40,15 @@ ZetCode PyQt5 tutorial
|
||||
|
||||
This is a Tetris game clone.
|
||||
|
||||
author: Jan Bodnar
|
||||
website: zetcode.com
|
||||
last edited: January 2015
|
||||
Author: Jan Bodnar
|
||||
Website: zetcode.com
|
||||
Last edited: August 2017
|
||||
"""
|
||||
|
||||
import sys, random
|
||||
from PyQt5.QtWidgets import QMainWindow, QFrame, QDesktopWidget, QApplication
|
||||
from PyQt5.QtCore import Qt, QBasicTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QPainter, QColor
|
||||
|
||||
import sys, random
|
||||
|
||||
class Tetris(QMainWindow):
|
||||
|
||||
@ -57,6 +59,7 @@ class Tetris(QMainWindow):
|
||||
|
||||
|
||||
def initUI(self):
|
||||
'''initiates application UI'''
|
||||
|
||||
self.tboard = Board(self)
|
||||
self.setCentralWidget(self.tboard)
|
||||
@ -73,6 +76,7 @@ class Tetris(QMainWindow):
|
||||
|
||||
|
||||
def center(self):
|
||||
'''centers the window on the screen'''
|
||||
|
||||
screen = QDesktopWidget().screenGeometry()
|
||||
size = self.geometry()
|
||||
@ -95,6 +99,7 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def initBoard(self):
|
||||
'''initiates board'''
|
||||
|
||||
self.timer = QBasicTimer()
|
||||
self.isWaitingAfterLine = False
|
||||
@ -111,22 +116,31 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def shapeAt(self, x, y):
|
||||
'''determines shape at the board position'''
|
||||
|
||||
return self.board[(y * Board.BoardWidth) + x]
|
||||
|
||||
|
||||
def setShapeAt(self, x, y, shape):
|
||||
'''sets a shape at the board'''
|
||||
|
||||
self.board[(y * Board.BoardWidth) + x] = shape
|
||||
|
||||
|
||||
def squareWidth(self):
|
||||
'''returns the width of one square'''
|
||||
|
||||
return self.contentsRect().width() // Board.BoardWidth
|
||||
|
||||
|
||||
def squareHeight(self):
|
||||
'''returns the height of one square'''
|
||||
|
||||
return self.contentsRect().height() // Board.BoardHeight
|
||||
|
||||
|
||||
def start(self):
|
||||
'''starts game'''
|
||||
|
||||
if self.isPaused:
|
||||
return
|
||||
@ -143,6 +157,7 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def pause(self):
|
||||
'''pauses game'''
|
||||
|
||||
if not self.isStarted:
|
||||
return
|
||||
@ -161,6 +176,7 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def paintEvent(self, event):
|
||||
'''paints all shapes of the game'''
|
||||
|
||||
painter = QPainter(self)
|
||||
rect = self.contentsRect()
|
||||
@ -188,6 +204,7 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
'''processes key press events'''
|
||||
|
||||
if not self.isStarted or self.curPiece.shape() == Tetrominoe.NoShape:
|
||||
super(Board, self).keyPressEvent(event)
|
||||
@ -225,6 +242,7 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def timerEvent(self, event):
|
||||
'''handles timer event'''
|
||||
|
||||
if event.timerId() == self.timer.timerId():
|
||||
|
||||
@ -239,12 +257,14 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def clearBoard(self):
|
||||
'''clears shapes from the board'''
|
||||
|
||||
for i in range(Board.BoardHeight * Board.BoardWidth):
|
||||
self.board.append(Tetrominoe.NoShape)
|
||||
|
||||
|
||||
def dropDown(self):
|
||||
'''drops down a shape'''
|
||||
|
||||
newY = self.curY
|
||||
|
||||
@ -259,12 +279,14 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def oneLineDown(self):
|
||||
'''goes one line down with a shape'''
|
||||
|
||||
if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
|
||||
self.pieceDropped()
|
||||
|
||||
|
||||
def pieceDropped(self):
|
||||
'''after dropping shape, remove full lines and create new shape'''
|
||||
|
||||
for i in range(4):
|
||||
|
||||
@ -279,6 +301,7 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def removeFullLines(self):
|
||||
'''removes all full lines from the board'''
|
||||
|
||||
numFullLines = 0
|
||||
rowsToRemove = []
|
||||
@ -315,6 +338,7 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def newPiece(self):
|
||||
'''creates a new shape'''
|
||||
|
||||
self.curPiece = Shape()
|
||||
self.curPiece.setRandomShape()
|
||||
@ -331,6 +355,7 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def tryMove(self, newPiece, newX, newY):
|
||||
'''tries to move a shape'''
|
||||
|
||||
for i in range(4):
|
||||
|
||||
@ -352,6 +377,7 @@ class Board(QFrame):
|
||||
|
||||
|
||||
def drawSquare(self, painter, x, y, shape):
|
||||
'''draws a square of a shape'''
|
||||
|
||||
colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
|
||||
0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
|
||||
@ -405,10 +431,13 @@ class Shape(object):
|
||||
|
||||
|
||||
def shape(self):
|
||||
'''returns shape'''
|
||||
|
||||
return self.pieceShape
|
||||
|
||||
|
||||
def setShape(self, shape):
|
||||
'''sets a shape'''
|
||||
|
||||
table = Shape.coordsTable[shape]
|
||||
|
||||
@ -420,26 +449,37 @@ class Shape(object):
|
||||
|
||||
|
||||
def setRandomShape(self):
|
||||
'''chooses a random shape'''
|
||||
|
||||
self.setShape(random.randint(1, 7))
|
||||
|
||||
|
||||
def x(self, index):
|
||||
'''returns x coordinate'''
|
||||
|
||||
return self.coords[index][0]
|
||||
|
||||
|
||||
def y(self, index):
|
||||
'''returns y coordinate'''
|
||||
|
||||
return self.coords[index][1]
|
||||
|
||||
|
||||
def setX(self, index, x):
|
||||
'''sets x coordinate'''
|
||||
|
||||
self.coords[index][0] = x
|
||||
|
||||
|
||||
def setY(self, index, y):
|
||||
'''sets y coordinate'''
|
||||
|
||||
self.coords[index][1] = y
|
||||
|
||||
|
||||
def minX(self):
|
||||
'''returns min x value'''
|
||||
|
||||
m = self.coords[0][0]
|
||||
for i in range(4):
|
||||
@ -449,6 +489,7 @@ class Shape(object):
|
||||
|
||||
|
||||
def maxX(self):
|
||||
'''returns max x value'''
|
||||
|
||||
m = self.coords[0][0]
|
||||
for i in range(4):
|
||||
@ -458,6 +499,7 @@ class Shape(object):
|
||||
|
||||
|
||||
def minY(self):
|
||||
'''returns min y value'''
|
||||
|
||||
m = self.coords[0][1]
|
||||
for i in range(4):
|
||||
@ -467,6 +509,7 @@ class Shape(object):
|
||||
|
||||
|
||||
def maxY(self):
|
||||
'''returns max y value'''
|
||||
|
||||
m = self.coords[0][1]
|
||||
for i in range(4):
|
||||
@ -476,6 +519,7 @@ class Shape(object):
|
||||
|
||||
|
||||
def rotateLeft(self):
|
||||
'''rotates shape to the left'''
|
||||
|
||||
if self.pieceShape == Tetrominoe.SquareShape:
|
||||
return self
|
||||
@ -492,6 +536,7 @@ class Shape(object):
|
||||
|
||||
|
||||
def rotateRight(self):
|
||||
'''rotates shape to the right'''
|
||||
|
||||
if self.pieceShape == Tetrominoe.SquareShape:
|
||||
return self
|
||||
@ -514,33 +559,39 @@ if __name__ == '__main__':
|
||||
sys.exit(app.exec_())
|
||||
```
|
||||
游戏很简单,所以也就很好理解。程序加载之后游戏也就直接开始了,可以用P键暂停游戏,空格键让方块直接落到最下面。游戏的速度是固定的,并没有实现加速的功能。分数就是游戏中消除的行数。
|
||||
|
||||
```
|
||||
self.tboard = Board(self)
|
||||
self.setCentralWidget(self.tboard)
|
||||
```
|
||||
创建了一个Board类的实例,并设置为应用的中心组件。
|
||||
|
||||
```
|
||||
self.statusbar = self.statusBar()
|
||||
self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)
|
||||
```
|
||||
创建一个statusbar来显示三种信息:消除的行数,游戏暂停状态或者游戏结束状态。msg2Statusbar是一个自定义的信号,用在(和)Board类(交互),showMessage()方法是一个内建的,用来在statusbar上显示信息的方法。
|
||||
创建一个`statusbar`来显示三种信息:消除的行数,游戏暂停状态或者游戏结束状态。`msg2Statusbar`是一个自定义的信号,用在(和)Board类(交互),`showMessage()`方法是一个内建的,用来在statusbar上显示信息的方法。
|
||||
|
||||
```
|
||||
self.tboard.start()
|
||||
```
|
||||
This line initiates the game.
|
||||
初始化游戏:
|
||||
|
||||
```
|
||||
class Board(QFrame):
|
||||
|
||||
msg2Statusbar = pyqtSignal(str)
|
||||
...
|
||||
```
|
||||
创建了一个自定义信号msg2Statusbar,当我们想往statusbar里显示信息的时候,发出这个信号就行了。
|
||||
创建了一个自定义信号`msg2Statusbar`,当我们想往`statusbar`里显示信息的时候,发出这个信号就行了。
|
||||
|
||||
```
|
||||
BoardWidth = 10
|
||||
BoardHeight = 22
|
||||
Speed = 300
|
||||
```
|
||||
这些是Board类的变量。BoardWidth和BoardHeight分别是board的宽度和高度。Speed是游戏的速度,每300ms出现一个新的方块。
|
||||
这些是`Board`类的变量。`BoardWidth`和`BoardHeight`分别是board的宽度和高度。`Speed`是游戏的速度,每300ms出现一个新的方块。
|
||||
|
||||
```
|
||||
...
|
||||
self.curX = 0
|
||||
@ -549,17 +600,20 @@ self.numLinesRemoved = 0
|
||||
self.board = []
|
||||
...
|
||||
```
|
||||
在initBoard()里初始化了一些重要的变量。The self.board定义了方块的形状和位置,取值范围是0-7。
|
||||
在`initBoard()`里初始化了一些重要的变量。`self.board`定义了方块的形状和位置,取值范围是0-7。
|
||||
|
||||
```
|
||||
def shapeAt(self, x, y):
|
||||
return self.board[(y * Board.BoardWidth) + x]
|
||||
```
|
||||
shapeAt()决定了board里方块的的种类。
|
||||
`shapeAt()`决定了board里方块的的种类。
|
||||
|
||||
```
|
||||
def squareWidth(self):
|
||||
return self.contentsRect().width() // Board.BoardWidth
|
||||
```
|
||||
board的大小可以动态的改变。所以方格的大小也应该随之变化。squareWidth()计算并返回每个块应该占用多少像素--也即Board.BoardWidth。
|
||||
board的大小可以动态的改变。所以方格的大小也应该随之变化。`squareWidth()`计算并返回每个块应该占用多少像素--也即`Board.BoardWidth`。
|
||||
|
||||
```
|
||||
def pause(self):
|
||||
'''pauses game'''
|
||||
@ -579,7 +633,8 @@ def pause(self):
|
||||
|
||||
self.update()
|
||||
```
|
||||
pause()方法用来暂停游戏,停止计时并在statusbar上显示一条信息。
|
||||
`pause()`方法用来暂停游戏,停止计时并在`statusbar`上显示一条信息。
|
||||
|
||||
```
|
||||
def paintEvent(self, event):
|
||||
'''paints all shapes of the game'''
|
||||
@ -589,6 +644,7 @@ def paintEvent(self, event):
|
||||
...
|
||||
```
|
||||
渲染是在paintEvent()方法里发生的`QPainter`负责PyQt5里所有低级绘画操作。
|
||||
|
||||
```
|
||||
for i in range(Board.BoardHeight):
|
||||
for j in range(Board.BoardWidth):
|
||||
@ -599,7 +655,8 @@ for i in range(Board.BoardHeight):
|
||||
rect.left() + j * self.squareWidth(),
|
||||
boardTop + i * self.squareHeight(), shape)
|
||||
```
|
||||
渲染游戏分为两步。第一步是先画出所有已经落在最下面的的图,这些保存在self.board里。可以使用shapeAt()查看这个这个变量。
|
||||
渲染游戏分为两步。第一步是先画出所有已经落在最下面的的图,这些保存在`self.board`里。可以使用`shapeAt()`查看这个这个变量。
|
||||
|
||||
```
|
||||
if self.curPiece.shape() != Tetrominoe.NoShape:
|
||||
|
||||
@ -612,26 +669,31 @@ if self.curPiece.shape() != Tetrominoe.NoShape:
|
||||
self.curPiece.shape())
|
||||
```
|
||||
第二步是画出更在下落的方块。
|
||||
|
||||
```
|
||||
elif key == Qt.Key_Right:
|
||||
self.tryMove(self.curPiece, self.curX + 1, self.curY)
|
||||
```
|
||||
在keyPressEvent()方法获得用户按下的按键。如果按下的是右方向键,就尝试把方块向右移动,说尝试是因为有可能到边界不能移动了。
|
||||
在`keyPressEvent()`方法获得用户按下的按键。如果按下的是右方向键,就尝试把方块向右移动,说尝试是因为有可能到边界不能移动了。
|
||||
|
||||
```
|
||||
elif key == Qt.Key_Up:
|
||||
self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)
|
||||
```
|
||||
上方向键是把方块向左旋转一下
|
||||
|
||||
```
|
||||
elif key == Qt.Key_Space:
|
||||
self.dropDown()
|
||||
```
|
||||
空格键会直接把方块放到底部
|
||||
|
||||
```
|
||||
elif key == Qt.Key_D:
|
||||
self.oneLineDown()
|
||||
```
|
||||
D键是加速一下下下路速度。
|
||||
D键是加速一次下落速度。
|
||||
|
||||
```
|
||||
def tryMove(self, newPiece, newX, newY):
|
||||
|
||||
@ -652,7 +714,8 @@ def tryMove(self, newPiece, newX, newY):
|
||||
self.update()
|
||||
return True
|
||||
```
|
||||
tryMove()是尝试移动方块的方法。如果方块已经到达board的边缘或者遇到了其他方块,就返回False。否则就把方块下落到想要
|
||||
`tryMove()`是尝试移动方块的方法。如果方块已经到达board的边缘或者遇到了其他方块,就返回False。否则就把方块下落到想要
|
||||
|
||||
```
|
||||
def timerEvent(self, event):
|
||||
|
||||
@ -668,13 +731,15 @@ def timerEvent(self, event):
|
||||
super(Board, self).timerEvent(event)
|
||||
```
|
||||
在计时器事件里,要么是等一个方块下落完之后创建一个新的方块,要么是让一个方块直接落到底(move a falling piece one line down)。
|
||||
|
||||
```
|
||||
def clearBoard(self):
|
||||
|
||||
for i in range(Board.BoardHeight * Board.BoardWidth):
|
||||
self.board.append(Tetrominoe.NoShape)
|
||||
```
|
||||
clearBoard()方法通过Tetrominoe.NoShape清空broad。
|
||||
`clearBoard(`)方法通过`Tetrominoe.NoShape`清空`broad`。
|
||||
|
||||
```
|
||||
def removeFullLines(self):
|
||||
|
||||
@ -703,7 +768,8 @@ def removeFullLines(self):
|
||||
numFullLines = numFullLines + len(rowsToRemove)
|
||||
...
|
||||
```
|
||||
如果方块碰到了底部,就调用removeFullLines()方法,找到所有能消除的行消除它们。消除的具体动作就是把符合条件的行消除掉之后,再把它上面的行下降一行。注意移除满行的动作是倒着来的,因为我们是按照重力来表现游戏的,如果不这样就有可能出现有些方块浮在空中的现象。
|
||||
如果方块碰到了底部,就调用`removeFullLines()`方法,找到所有能消除的行消除它们。消除的具体动作就是把符合条件的行消除掉之后,再把它上面的行下降一行。注意移除满行的动作是倒着来的,因为我们是按照重力来表现游戏的,如果不这样就有可能出现有些方块浮在空中的现象。
|
||||
|
||||
```
|
||||
def newPiece(self):
|
||||
|
||||
@ -719,7 +785,8 @@ def newPiece(self):
|
||||
self.isStarted = False
|
||||
self.msg2Statusbar.emit("Game over")
|
||||
```
|
||||
newPiece()方法是用来创建形状随机的方块。如果随机的方块不能正确的出现在预设的位置,游戏结束。
|
||||
`newPiece()`方法是用来创建形状随机的方块。如果随机的方块不能正确的出现在预设的位置,游戏结束。
|
||||
|
||||
```
|
||||
class Tetrominoe(object):
|
||||
|
||||
@ -732,9 +799,10 @@ class Tetrominoe(object):
|
||||
LShape = 6
|
||||
MirroredLShape = 7
|
||||
```
|
||||
Tetrominoe类保存了所有方块的形状。我们还定义了一个NoShape的空形状。
|
||||
`Tetrominoe`类保存了所有方块的形状。我们还定义了一个`NoShape`的空形状。
|
||||
|
||||
Shape类保存类方块内部的信息。
|
||||
|
||||
```
|
||||
class Shape(object):
|
||||
|
||||
@ -746,6 +814,7 @@ class Shape(object):
|
||||
...
|
||||
```
|
||||
coordsTable元组保存了所有的方块形状的组成。是一个构成方块的坐标模版。
|
||||
|
||||
```
|
||||
self.coords = [[0,0] for i in range(4)]
|
||||
```
|
||||
@ -755,7 +824,7 @@ self.coords = [[0,0] for i in range(4)]
|
||||
|
||||
![coordinates](./images/11-coordinates.png)
|
||||
|
||||
上面的图片可以帮助我们更好的理解坐标值的意义。比如元组(0, -1), (0, 0), (-1, 0), (-1, -1)代表了一个Z形状的方块。这个图表就描绘了这个形状。
|
||||
上面的图片可以帮助我们更好的理解坐标值的意义。比如元组`(0, -1), (0, 0), (-1, 0), (-1, -1)`代表了一个Z形状的方块。这个图表就描绘了这个形状。
|
||||
```
|
||||
def rotateLeft(self):
|
||||
|
||||
@ -772,6 +841,8 @@ def rotateLeft(self):
|
||||
|
||||
return result
|
||||
```
|
||||
rotateLeft()方法向右旋转一个方块。正方形的方块就没必要旋转,就直接返回了。其他的是返回一个新的,能表示这个形状旋转了的坐标。
|
||||
`rotateLeft()`方法向右旋转一个方块。正方形的方块就没必要旋转,就直接返回了。其他的是返回一个新的,能表示这个形状旋转了的坐标。
|
||||
|
||||
程序展示:
|
||||
|
||||
![Tetris](./images/11-tetris.png)
|
90
绘图.md
90
绘图.md
@ -371,7 +371,7 @@ qp.setPen(pen)
|
||||
|
||||
`QBrush`也是图像的一个基本元素。是用来填充一些物体的背景图用的,比如矩形,椭圆,多边形等。有三种类型:预定义、渐变和纹理。
|
||||
|
||||
```
|
||||
```python
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@ -470,4 +470,90 @@ qp.drawRect(10, 15, 90, 60)
|
||||
|
||||
程序展示:
|
||||
|
||||
![brushes](./images/9-brushes.png)
|
||||
![brushes](./images/9-brushes.png)
|
||||
|
||||
## 贝塞尔曲线
|
||||
|
||||
噩梦可以使用PyQt5的`QPainterPath`创建贝塞尔曲线。绘画路径是由许多构建图形的对象,具体表现就是一些线的形状,比如矩形,椭圆,线和曲线。
|
||||
|
||||
```python
|
||||
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
ZetCode PyQt5 tutorial
|
||||
|
||||
This program draws a Bézier curve with
|
||||
QPainterPath.
|
||||
|
||||
Author: Jan Bodnar
|
||||
Website: zetcode.com
|
||||
Last edited: August 2017
|
||||
"""
|
||||
|
||||
from PyQt5.QtWidgets import QWidget, QApplication
|
||||
from PyQt5.QtGui import QPainter, QPainterPath
|
||||
from PyQt5.QtCore import Qt
|
||||
import sys
|
||||
|
||||
class Example(QWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.initUI()
|
||||
|
||||
|
||||
def initUI(self):
|
||||
|
||||
self.setGeometry(300, 300, 380, 250)
|
||||
self.setWindowTitle('Bézier curve')
|
||||
self.show()
|
||||
|
||||
|
||||
def paintEvent(self, e):
|
||||
|
||||
qp = QPainter()
|
||||
qp.begin(self)
|
||||
qp.setRenderHint(QPainter.Antialiasing)
|
||||
self.drawBezierCurve(qp)
|
||||
qp.end()
|
||||
|
||||
|
||||
def drawBezierCurve(self, qp):
|
||||
|
||||
path = QPainterPath()
|
||||
path.moveTo(30, 30)
|
||||
path.cubicTo(30, 30, 200, 350, 350, 30)
|
||||
|
||||
qp.drawPath(path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
ex = Example()
|
||||
sys.exit(app.exec_())
|
||||
```
|
||||
|
||||
这个示例中,我们画出了一个贝塞尔曲线。
|
||||
|
||||
```python
|
||||
path = QPainterPath()
|
||||
path.moveTo(30, 30)
|
||||
path.cubicTo(30, 30, 200, 350, 350, 30)
|
||||
```
|
||||
|
||||
用`QPainterPath`路径创建贝塞尔曲线。使用`cubicTo()`方法生成,分别需要三个点:起始点,控制点和终止点。
|
||||
|
||||
```python
|
||||
qp.drawPath(path)
|
||||
|
||||
```
|
||||
|
||||
`drawPath()`绘制最后的图像。
|
||||
|
||||
程序展示:
|
||||
|
||||
![Bézier curve](./images/9-Bézier%20curve.png)
|
||||
|
@ -1,13 +1,14 @@
|
||||
# 自定义控件
|
||||
|
||||
PyQt5有丰富的组件,但是还是满足不了所有开发者的所有需求的,PyQt5只提供了基本的组件,像按钮,文本,滑块等。如果你还需要其他的模块,应该尝试自己去自定义一些。
|
||||
PyQt5有丰富的组件,但是肯定满足不了所有开发者的所有需求,PyQt5只提供了基本的组件,像按钮,文本,滑块等。如果你还需要其他的模块,应该尝试自己去自定义一些。
|
||||
|
||||
自定义组件使用绘画工具创建,有两个基本方式:根据已有的创建或改进;通过自己绘图创建。
|
||||
|
||||
## Burning widget
|
||||
|
||||
这个组件我们会在Nero,K3B,或者其他CD/DVD烧录软件中见到。
|
||||
```
|
||||
|
||||
```python
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@ -16,17 +17,16 @@ ZetCode PyQt5 tutorial
|
||||
|
||||
In this example, we create a custom widget.
|
||||
|
||||
author: Jan Bodnar
|
||||
website: zetcode.com
|
||||
last edited: January 2015
|
||||
Author: Jan Bodnar
|
||||
Website: zetcode.com
|
||||
Last edited: August 2017
|
||||
"""
|
||||
|
||||
import sys
|
||||
from PyQt5.QtWidgets import (QWidget, QSlider, QApplication,
|
||||
QHBoxLayout, QVBoxLayout)
|
||||
from PyQt5.QtCore import QObject, Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QPainter, QFont, QColor, QPen
|
||||
|
||||
import sys
|
||||
|
||||
class Communicate(QObject):
|
||||
|
||||
@ -62,6 +62,9 @@ class BurningWidget(QWidget):
|
||||
|
||||
|
||||
def drawWidget(self, qp):
|
||||
|
||||
MAX_CAPACITY = 700
|
||||
OVER_CAPACITY = 750
|
||||
|
||||
font = QFont('Serif', 7, QFont.Light)
|
||||
qp.setFont(font)
|
||||
@ -70,13 +73,13 @@ class BurningWidget(QWidget):
|
||||
w = size.width()
|
||||
h = size.height()
|
||||
|
||||
step = int(round(w / 10.0))
|
||||
step = int(round(w / 10))
|
||||
|
||||
|
||||
till = int(((w / 750.0) * self.value))
|
||||
full = int(((w / 750.0) * 700))
|
||||
till = int(((w / OVER_CAPACITY) * self.value))
|
||||
full = int(((w / OVER_CAPACITY) * MAX_CAPACITY))
|
||||
|
||||
if self.value >= 700:
|
||||
if self.value >= MAX_CAPACITY:
|
||||
|
||||
qp.setPen(QColor(255, 255, 255))
|
||||
qp.setBrush(QColor(255, 255, 184))
|
||||
@ -119,10 +122,12 @@ class Example(QWidget):
|
||||
|
||||
|
||||
def initUI(self):
|
||||
|
||||
OVER_CAPACITY = 750
|
||||
|
||||
sld = QSlider(Qt.Horizontal, self)
|
||||
sld.setFocusPolicy(Qt.NoFocus)
|
||||
sld.setRange(1, 750)
|
||||
sld.setRange(1, OVER_CAPACITY)
|
||||
sld.setValue(75)
|
||||
sld.setGeometry(30, 40, 150, 30)
|
||||
|
||||
@ -155,25 +160,30 @@ if __name__ == '__main__':
|
||||
ex = Example()
|
||||
sys.exit(app.exec_())
|
||||
```
|
||||
本例中,我们使用了`QSlider`和一个自定义组件,由进度条控制。显示的有物体(也就是CD/DVD)的总容量和剩余容量。进度条的范围是1~750。如果值达到了700,就显示为红色,代表了烧毁了的意思。
|
||||
本例中,我们使用了`QSlider`和一个自定义组件,由进度条控制。显示的有物体(也就是CD/DVD)的总容量和剩余容量。进度条的范围是1~750。如果值达到了700(OVER_CAPACITY),就显示为红色,代表了烧毁了的意思。
|
||||
|
||||
烧录组件在窗口的底部,这个组件是用`QHBoxLayout`和`QVBoxLayout`组成的。
|
||||
```
|
||||
|
||||
```python
|
||||
class BurningWidget(QWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
```
|
||||
烧录组件是基于`QWidget`组件做的。
|
||||
|
||||
基于`QWidget`组件。
|
||||
|
||||
```
|
||||
self.setMinimumSize(1, 30)
|
||||
```
|
||||
修改组件进度条的高度,默认的有点小了。
|
||||
修改组件进度条的高度,默认的有点小。
|
||||
|
||||
```
|
||||
font = QFont('Serif', 7, QFont.Light)
|
||||
qp.setFont(font)
|
||||
```
|
||||
使用比默认更小一点的字体,这样更配。
|
||||
|
||||
```
|
||||
size = self.size()
|
||||
w = size.width()
|
||||
@ -188,12 +198,14 @@ full = int(((w / 750.0) * 700))
|
||||
动态的渲染组件,随着窗口的大小而变化,这就是我们计算窗口大小的原因。最后一个参数决定了组件的最大范围,进度条的值是由窗口大小按比例计算出来的。最大值的地方填充的是红色。注意这里使用的是浮点数,能提高计算和渲染的精度。
|
||||
|
||||
绘画由三部分组成,黄色或红色区域和黄色矩形,然后是分割线,最后是添上代表容量的数字。
|
||||
|
||||
```
|
||||
metrics = qp.fontMetrics()
|
||||
fw = metrics.width(str(self.num[j]))
|
||||
qp.drawText(i-fw/2, h/2, str(self.num[j]))
|
||||
```
|
||||
这里使用字体去渲染文本。必须要知道文本的宽度,这样才能让文本的中间点正好落在竖线上。
|
||||
|
||||
```
|
||||
def changeValue(self, value):
|
||||
|
Loading…
x
Reference in New Issue
Block a user