鱼C论坛

 找回密码
 立即注册
楼主: lightninng

[技术交流] PyQt5学习与交流

  [复制链接]
 楼主| 发表于 2015-4-23 19:20:59 | 显示全部楼层
本帖最后由 lightninng 于 2015-4-24 13:10 编辑

10 PyQt5中自定义插件
你是否曾经关注一个程序,并且想知道其独特的图形项目是怎么创造的?也许每个程序员都这么想过。但是当你查看你最喜爱的GUI库的组件目录时,却发现没有这个组件。工具包一般只提供最常有的组件,比如按钮,文本组件,滑块等,没有那个工具包可以提供所有的组件。
事实上有两种工具包,简单的工具包和复杂的工具包。FLTK工具包是一种简单的工具包,它只提供最基本的组件和呈现,程序员可以自己创建更复杂的组件。PyQt5是复杂的工具包,它有许多的组件,但是它并不提供专业化的组件。例如速度计组件,用来检测将要烧录的CD的容量(可以在nero这类程序中找到)。工具包也不具备通常的图表。
程序员必须自己创建这些组件,他们可以通过工具包提供的绘图工具来实现。有两种可能,程序员可以更改或增强一个现存的组件或者从零开始创建一个自定义组件。
自定烧录CD组件
这是一个我们经常在Nero,K3B或其他CD/DVD烧录软件中见到的组件。
  1. # -*- coding: utf-8 -*-
  2. """自定义组件示例"""
  3. import sys
  4. from PyQt5 import QtWidgets, QtCore, QtGui


  5. class Wight(QtWidgets.QLabel):
  6.     def __init__(self, parent):
  7.         super(Wight, self).__init__(parent)
  8.     # def __init__(self, parent):
  9.     #     QtWidgets.QLabel.__init__(self, parent)
  10.         self.setMinimumSize(1, 30)
  11.         self.num = [75, 150, 225, 300, 375, 450, 525, 600, 675]

  12.     def paintEvent(self, event):
  13.         paint = QtGui.QPainter()
  14.         paint.begin(self)

  15.         font = QtGui.QFont("Times New Roman", 7, QtGui.QFont.Light)
  16.         paint.setFont(font)

  17.         size = self.size()
  18.         w = size.width()
  19.         h = size.height()
  20.         cw = self.parent().cw
  21.         step = int(round(w/10.0))

  22.         till = int(((w/750.0)*cw))
  23.         full = int(((w/750.0)*700))

  24.         if cw >= 700:
  25.             paint.setPen(QtGui.QColor(255, 255, 255))
  26.             paint.setBrush(QtGui.QColor(255, 255, 184))
  27.             paint.drawRect(0, 0, full, h)
  28.             paint.setPen(QtGui.QColor(255, 175, 175))
  29.             paint.setBrush(QtGui.QColor(255, 175, 175))
  30.             paint.drawRect(full, 0, till-full, h)
  31.         else:
  32.             paint.setPen(QtGui.QColor(255, 255, 255))
  33.             paint.setBrush(QtGui.QColor(255, 255, 184))
  34.             paint.drawRect(0, 0, till, h)

  35.         pen = QtGui.QPen(QtGui.QColor(20, 20, 20), 1, QtCore.Qt.SolidLine)
  36.         paint.setPen(pen)
  37.         paint.setBrush(QtCore.Qt.NoBrush)
  38.         paint.drawRect(0, 0, w-1, h-1)

  39.         j = 0

  40.         for i in range(step, 10*step, step):
  41.             paint.drawLine(i, 0, i, 5)
  42.             metrics = paint.fontMetrics()
  43.             fw = metrics.width(str(self.num[j]))
  44.             paint.drawText(i-fw/2, h/2, str(self.num[j]))
  45.             j += 1

  46.         paint.end()


  47. class Burning(QtWidgets.QWidget):
  48.     def __init__(self):
  49.         super(Burning, self).__init__()
  50.         self.setWindowTitle("自定义组件演示程序")
  51.         self.setGeometry(300, 300, 300, 220)

  52.         self.cw = 75

  53.         self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
  54.         self.slider.setFocusPolicy(QtCore.Qt.NoFocus)
  55.         self.slider.setRange(1, 750)
  56.         self.slider.setValue(75)
  57.         self.slider.setGeometry(30, 40, 150, 30)
  58.         self.slider.valueChanged.connect(self.change_value)

  59.         self.wid = Wight(self)

  60.         h_box = QtWidgets.QHBoxLayout()
  61.         h_box.addWidget(self.wid)
  62.         v_box = QtWidgets.QVBoxLayout()
  63.         v_box.addStretch(1)
  64.         v_box.addLayout(h_box)
  65.         self.setLayout(v_box)

  66.     def change_value(self):
  67.         self.cw = self.slider.value()
  68.         self.wid.repaint()
  69. app = QtWidgets.QApplication(sys.argv)
  70. b = Burning()
  71. b.show()
  72. sys.exit(app.exec_())
复制代码
这个例子里,我们有一个QSlider和一个自定义组件。滑块控制自定义组件。这个组件图形化的显示一个媒体的总容量和我们可使用的空余空间。我们自定义组件的最小值为1,最大值为750。如果我们到达700值,我们开始用红色绘制。这一般表示超刻。
刻录组件放置在窗口的底部,这通过一个QHBoxLayout和一个QVBoxLayout完成。
class Wight(QtWidgets.QLabel):
    def __init__(self, parent):

        super(Wight, self).__init__(parent)
刻录组件基于QLabel组件。
  1. self.setMinimumSize(1, 30)
复制代码
我们改变组件的最小值(高度).缺省值对我们来说有点小。
  1. font = QtGui.QFont("Times New Roman", 7, QtGui.QFont.Light)
  2. paint.setFont(font)
复制代码
我们使用一个比缺省小一点的字体,这更符合我们的需要。
  1. size = self.size()
  2. w = size.width()
  3. h = size.height()
  4. cw = self.parent().cw
  5. step = int(round(w/10.0))

  6. till = int(((w/750.0)*cw))
  7. full = int(((w/750.0)*700))
复制代码
我们动态的绘制组件,窗口越大,刻录组件越大。锁定调整,这就是为什么我们必须计算组件的尺寸以便我们绘制自定义的组件。till参数确定绘制的total 尺寸。这个数值从滑块组件取得。是整个区域的比例。full参数确定我们将要用红色绘制的点。这里我们使用了浮点数,以保证精度。
实际的绘制由三步组成。我们绘制黄色或红色和黄色的矩形,然后我们绘制将组件分割成几部分的垂直线,最后,我们绘制标识媒体容量的数字。
  1. metrics = paint.fontMetrics()
  2. fw = metrics.width(str(self.num[j]))
  3. paint.drawText(i-fw/2, h/2, str(self.num[j]))
复制代码
我们使用字体矩阵来绘制文字。我们必须知道文本的宽带以便居中包围垂直线。
PS:说一下我在学这一节中遇到的两个问题
1、首先是关于Widget类的__init___()方法,我使用了super(),而不是像原教程中的
         def__init__(self, parent):
             QtWidgets.QLabel.__init__(self, parent)
原因之前已经说过,这里我刚开始的写法是:
   def __init__(self, parent):
       super(Wight, self).__init__(self, parent)
按我上面的写法报错:
TypeError: arguments did not match anyoverloaded call:
QLabel(QWidget parent=None, Qt.WindowFlags flags=0): argument 2 has unexpectedtype 'Burning'
QLabel(str,QWidget parent=None, Qt.WindowFlags flags=0): argument 1 has unexpected type'Wight'
根据错误提示,这里QLabel类有两种参数写法,第一种是直接(所属部件名,默认参数Qt.WindowFlags),第二种(字符串,所属部件名,默认参数Qt. WindowFlags),这里第二种方式中的字符串显示的是Label组件中显示的文字,显然我们不需要,按第一种方式,传入参数直接写上所属部件名就ok了,super(Wight, self).__init__(self, parent)
2、原文中的一个小问题cw =self.parent().cw这条语句中的()原文没有写,报错:
AttributeError:'builtin_function_or_method' object has no attribute 'cw'
可以看到提示builtin_function_or_method对象没有属性cw,可以看到Burning类继承的QtWidgets.QWidget类是有一个方法(builtin_function_or_method)叫作parent的,又看到,上面的语句size.width()推测PyQt中取属性可能都定义的相应的方法,加上括号程序运行成功。

                               
登录/注册后可看大图

截图:定义组件示例
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-24 13:50:28 | 显示全部楼层
本帖最后由 lightninng 于 2015-5-16 18:03 编辑

11 PyQt5做俄罗斯方块
11.1 俄罗斯方块简介
编写计算机游戏非常有挑战性,早晚有一天,一名程序员会希望编写一个计算机游戏。事实上,很多人因为玩游戏并希望创造自己的游戏而对编程产生兴趣的。编写计算机游戏可以大大的提高你的编程水平。
俄罗斯方块游戏是现有的最受欢迎的计算机游戏之一。游戏最初由一名俄罗斯程序员Alexey Pajitnov于1985年设计并编写。从那时开始,俄罗斯方块游戏的众多变种出现在几乎每种计算机平台上。甚至我的移动电话也有一个改版的俄罗斯方块游戏。
         俄罗斯方块也叫“掉落方块解迷游戏”。在这个游戏中,我们有七种不同的形状叫做tetrominoesS形、Z形、T形、L形、线形、反L形和方块。每个形状由四个小方块组成。形状从顶板上落下来。俄罗斯方块游戏的目标是移动并旋转形状以便将他们尽可能的组合起来。如果我们控制填充满了一行,这一行就会消失,并且我们的得分。直到方块顶到顶部游戏结束。
PyQt5被设计成用来编写程序的工具包。有其他的库是专门设计用来编写计算机游戏的。尽管如此,PyQt5和其他应用程序工具包也可以被用来编写游戏。
下面的例子是俄罗斯方块游戏的改版,随PyQt5的安装文件而存在。
我们没有为我们的俄罗斯方块使用图片。我们通过PyQt5编程工具包中的绘图API来绘制方块。每个计算机游戏中,都有数学模型,俄罗斯方块游戏也是。
11.2 游戏内部的设计
我们用QtCore.QBasicTimer()来创建一个游戏循环
绘制俄罗斯方块
图形一块一块的移动而不是一个像素一个像素的移动。
PS:以下第一版程序,这个版本是教程中的版本,我只针对信号槽(原版用的是pyqt4.5之前的信号槽)、还有一些不能运行通过的部分进行了修改
  1. # -*- coding: utf-8 -*-
  2. """俄罗斯方块第一版"""
  3. import sys
  4. import random
  5. from PyQt5 import QtWidgets, QtCore, QtGui


  6. class Tetris(QtWidgets.QMainWindow):
  7.     def __init__(self):
  8.         super(Tetris, self).__init__()

  9.         self.setWindowTitle("俄罗斯方块")
  10.         self.setGeometry(300, 300, 300, 682)

  11.         self.tetris_board = Board(self)
  12.         self.setCentralWidget(self.tetris_board)

  13.         self.status_bar = self.statusBar()
  14.         self.tetris_board.messages_to_satusbar.connect(self.status_bar.showMessage)

  15.         self.tetris_board.start()
  16.         self.center()

  17.     def center(self):
  18.         screen = QtWidgets.QDesktopWidget().screenGeometry()
  19.         size = self.geometry()
  20.         self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)


  21. class Board(QtWidgets.QFrame):
  22.     board_width = 10
  23.     board_height = 22
  24.     speed = 300
  25.     messages_to_satusbar = QtCore.pyqtSignal(str)

  26.     def __init__(self, parent):
  27.         super(Board, self).__init__(parent)
  28.         self.timer = QtCore.QBasicTimer()
  29.         self.is_waiting_after_line = False
  30.         self.cur_piece = Shape()
  31.         self.next_piece = Shape()
  32.         self.cur_x = 0
  33.         self.cur_y = 0
  34.         self.num_lines_moved = 0
  35.         self.board = []
  36.         self.setFocusPolicy(QtCore.Qt.StrongFocus)
  37.         self.is_started = False
  38.         self.is_paused = False
  39.         self.clear_board()
  40.         self.next_piece.set_random_shape()

  41.     def shape_at(self, x, y):
  42.         return self.board[int((y*Board.board_width) + x)]

  43.     def set_shape_at(self, x, y, shape):
  44.         self.board[int((y * Board.board_width) + x)] = shape

  45.     def square_width(self):
  46.         # print("width", self.contentsRect().width() / Board.board_width)
  47.         return self.contentsRect().width() / Board.board_width

  48.     def square_height(self):
  49.         # print("square_height", self.contentsRect().height() / Board.board_height)
  50.         return self.contentsRect().height() / Board.board_height

  51.     def start(self):
  52.         if self.is_paused:
  53.             return
  54.         self.is_started = True
  55.         self.num_lines_moved = 0
  56.         self.clear_board()
  57.         self.messages_to_satusbar.emit(str(self.num_lines_moved))
  58.         self.new_piece()
  59.         self.timer.start(Board.speed, self)

  60.     def pause(self):
  61.         if not self.is_started:
  62.             return
  63.         self.is_paused = not self.is_paused
  64.         if self.is_paused:
  65.             self.timer.stop()
  66.             self.messages_to_satusbar.emit("paused")
  67.         else:
  68.             self.timer.start(Board.speed, self)
  69.             self.messages_to_satusbar.emit(str(self.num_lines_moved))
  70.         self.update()

  71.     def paintEvent(self, event):
  72.         paint = QtGui.QPainter(self)
  73.         rect = self.contentsRect()
  74.         board_top = rect.bottom() - Board.board_height * self.square_height()
  75.         for i in range(Board.board_height):
  76.             for j in range(Board.board_width):
  77.                 shape = self.shape_at(j, Board.board_height - i - 1)
  78.                 if shape != Tetrominoes.NoShape:
  79.                     self.draw_square(paint, rect.left() + j*self.square_width(),
  80.                                      board_top + i*self.square_height(), shape)
  81.         if self.cur_piece.shape() != Tetrominoes.NoShape:
  82.             for i in range(4):
  83.                 x = self.cur_x + self.cur_piece.x(i)
  84.                 y = self.cur_y - self.cur_piece.y(i)
  85.                 self.draw_square(paint, rect.left() + x*self.square_width(),
  86.                                  board_top + (Board.board_height-y-1)*self.square_height(),
  87.                                  self.cur_piece.shape())

  88.     def keyPressEvent(self, event):
  89.         if not self.is_started or self.cur_piece.shape() == Tetrominoes.NoShape:
  90.             QtWidgets.QWidget.keyPressEvent(self, event)
  91.             return
  92.         key = event.key()
  93.         if key == QtCore.Qt.Key_P:
  94.             self.pause()
  95.             return
  96.         if self.is_paused:
  97.             return
  98.         elif key == QtCore.Qt.Key_Left:
  99.             self.try_move(self.cur_piece, self.cur_x - 1, self.cur_y)
  100.         elif key == QtCore.Qt.Key_Right:
  101.             self.try_move(self.cur_piece, self.cur_x + 1, self.cur_y)
  102.         elif key == QtCore.Qt.Key_Down:
  103.             self.try_move(self.cur_piece.rotated_right(), self.cur_x, self.cur_y)
  104.         elif key == QtCore.Qt.Key_Up:
  105.             self.try_move(self.cur_piece.rotated_left(), self.cur_x, self.cur_y)
  106.         elif key == QtCore.Qt.Key_Space:
  107.             self.drop_down()
  108.         elif key == QtCore.Qt.Key_D:
  109.             self.one_line_down()
  110.         else:
  111.             QtWidgets.QWidget.keyPressEvent(self, event)

  112.     def timerEvent(self, event):
  113.         if event.timerId() == self.timer.timerId():
  114.             if self.is_waiting_after_line:
  115.                 self.is_waiting_after_line = False
  116.                 self.new_piece()
  117.             else:
  118.                 self.one_line_down()
  119.         else:
  120.             QtWidgets.QFrame.timerEvent(self, event)

  121.     def clear_board(self):
  122.         for i in range(Board.board_height * Board.board_width):
  123.             self.board.append(Tetrominoes.NoShape)

  124.     def drop_down(self):
  125.         new_y = self.cur_y
  126.         while new_y > 0:
  127.             if not self.try_move(self.cur_piece, self.cur_x, new_y - 1):
  128.                 break
  129.             new_y -= 1
  130.         self.piece_dropped()

  131.     def one_line_down(self):
  132.         if not self.try_move(self.cur_piece, self.cur_x, self.cur_y - 1):
  133.             self.piece_dropped()

  134.     def piece_dropped(self):
  135.         for i in range(4):
  136.             x = self.cur_x + self.cur_piece.x(i)
  137.             y = self.cur_y - self.cur_piece.y(i)
  138.             self.set_shape_at(x, y, self.cur_piece.shape())
  139.         self.remove_full_lines()
  140.         if not self.is_waiting_after_line:
  141.             self.new_piece()

  142.     def remove_full_lines(self):
  143.         num_full_lines = 0
  144.         rows_to_remove = []
  145.         for i in range(Board.board_height):
  146.             n = 0
  147.             for j in range(Board.board_width):
  148.                 if not self.shape_at(j, i) == Tetrominoes.NoShape:
  149.                     n += 1
  150.             if n == 10:
  151.                 rows_to_remove.append(i)
  152.         rows_to_remove.reverse()
  153.         for m in rows_to_remove:
  154.             for k in range(m, Board.board_height):
  155.                 for l in range(Board.board_width):
  156.                     self.set_shape_at(l, k, self.shape_at(l, k + 1))
  157.         num_full_lines += len(rows_to_remove)
  158.         if num_full_lines > 0:
  159.             self.num_lines_moved += num_full_lines
  160.             self.messages_to_satusbar.emit(str(self.num_lines_moved))
  161.             self.is_waiting_after_line = True
  162.             self.cur_piece.set_shape(Tetrominoes.NoShape)
  163.             self.update()

  164.     def new_piece(self):
  165.         self.cur_piece = self.next_piece
  166.         self.next_piece.set_random_shape()
  167.         self.cur_x = Board.board_width / 2 + 1
  168.         self.cur_y = Board.board_height - 1 + self.cur_piece.min_y()
  169.         if not self.try_move(self.cur_piece, self.cur_x, self.cur_y):
  170.             self.cur_piece.set_shape(Tetrominoes.NoShape)
  171.             self.timer.stop()
  172.             self.is_started = False
  173.             self.messages_to_satusbar.emit("游戏结束")

  174.     def try_move(self, new_piece, new_x, new_y):
  175.         for i in range(4):
  176.             x = new_x + new_piece.x(i)
  177.             y = new_y - new_piece.y(i)
  178.             if x < 0 or x >= Board.board_width or y < 0 or y >= Board.board_height:
  179.                 return False
  180.             if self.shape_at(x, y) != Tetrominoes.NoShape:
  181.                 return False
  182.         self.cur_piece = new_piece
  183.         self.cur_x = new_x
  184.         self.cur_y = new_y
  185.         self.update()
  186.         return True

  187.     def draw_square(self, painter, x, y, shape):
  188.         color_table = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC, 0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
  189.         color = QtGui.QColor(color_table[shape])
  190.         painter.fillRect(x + 1, y + 1, self.square_width() - 2, self.square_height() - 2, color)
  191.         painter.setPen(color.lighter())
  192.         painter.drawLine(x, y + self.square_height() - 1, x, y)
  193.         painter.drawLine(x, y, x + self.square_width() - 1, y)
  194.         painter.setPen(color.darker())
  195.         painter.drawLine(x + 1, y + self.square_height() - 1,
  196.                          x + self.square_width() - 1, y + self.square_height() - 1)
  197.         painter.drawLine(x + self.square_width() - 1, y + self.square_height() - 1,
  198.                          x + self.square_width() - 1, y + 1)

  199. class Tetrominoes(object):
  200.     NoShape = 0
  201.     ZShape = 1
  202.     SShape = 2
  203.     LineShape = 3
  204.     TShape = 4
  205.     SquareShape = 5
  206.     LShape = 6
  207.     MirroredLShape = 7


  208. class Shape(object):
  209.     coords_table = (((0, 0),    (0, 0),     (0, 0),     (0, 0)),
  210.                     ((0, -1),   (0, 0),     (-1, 0),    (-1, 1)),
  211.                     ((0, -1),   (0, 0),     (1, 0),     (1, 1)),
  212.                     ((0, -1),   (0, 0),     (0, 1),     (0, 2)),
  213.                     ((-1, 0),   (0, 0),     (1, 0),     (0, 1)),
  214.                     ((0, 0),    (1, 0),     (0, 1),     (1, 1)),
  215.                     ((-1, -1),  (0, -1),    (0, 0),     (0, 1)),
  216.                     ((1, -1),   (0, -1),    (0, 0),     (0, 1)))

  217.     def __init__(self):
  218.         self.coords = [[0, 0] for i in range(4)]
  219.         self.piece_shape = Tetrominoes.NoShape
  220.         self.set_shape(Tetrominoes.NoShape)

  221.     def shape(self):
  222.         return self.piece_shape

  223.     def set_shape(self, shape):
  224.         table = Shape.coords_table[shape]
  225.         for i in range(4):
  226.             for j in range(2):
  227.                 self.coords[i][j] = table[i][j]
  228.         self.piece_shape = shape

  229.     def set_random_shape(self):
  230.         self.set_shape(random.randint(1, 7))

  231.     def x(self, index):
  232.         return self.coords[index][0]

  233.     def y(self, index):
  234.         return self.coords[index][1]

  235.     def set_x(self, index, x):
  236.         self.coords[index][0] = x

  237.     def set_y(self, index, y):
  238.         self.coords[index][1] = y

  239.     def min_x(self):
  240.         m = self.coords[0][0]
  241.         for i in range(4):
  242.             m = min(m, self.coords[i][0])
  243.         return m

  244.     def max_x(self):
  245.         m = self.coords[0][0]
  246.         for i in range(4):
  247.             m = max(m, self.coords[i][0])
  248.         return m

  249.     def min_y(self):
  250.         m = self.coords[0][1]
  251.         for i in range(4):
  252.             m = min(m, self.coords[i][1])
  253.         return m

  254.     def max_y(self):
  255.         m = self.coords[0][1]
  256.         for i in range(4):
  257.             m = max(m, self.coords[i][1])
  258.         return m

  259.     def rotated_left(self):
  260.         if self.piece_shape == Tetrominoes.SquareShape:
  261.             return self
  262.         result = Shape()
  263.         result.piece_shape = self.piece_shape
  264.         for i in range(4):
  265.             result.set_x(i, self.y(i))
  266.             result.set_y(i, -self.x(i))
  267.         return result

  268.     def rotated_right(self):
  269.         if self.piece_shape == Tetrominoes.SquareShape:
  270.             return self
  271.         result = Shape()
  272.         result.piece_shape = self.piece_shape
  273.         for i in range(4):
  274.             result.set_x(i, -self.y(i))
  275.             result.set_y(i, self.x(i))
  276.         return result

  277. app = QtWidgets.QApplication(sys.argv)
  278. tetris = Tetris()
  279. tetris.show()
  280. sys.exit(app.exec_())
复制代码

首先,说说修改一个小bug和关于信号槽的部分之前没有涉及到的内容:
1、Board类中的shape_at和set_shape_at方法中,对self.board进行了索引取值操作,但是给出的索引是用一个表达式,这个表达式计算出来的值是float型导致报错,观察一下,式中的变量均为int型,于是用int()函数强行把索引值转化为int型,原代码和改后代码如下
  1. def shape_at(self, x, y):
  2.         return self.board[(y*Board.board_width) + x]
  3. def shape_at(self, x, y):
  4.         return self.board[int((y*Board.board_width) + x)]
复制代码
2、关于PyQt5中的信号槽问题在前面的章节中我已经作过介绍,现在说一下信号槽的另外一个问题:怎么给槽函数传递参数?
其实主要就是3点:
(1)在创建信号变量时,按槽函数的传入参数顺序,将对应参数的类型传入QtCore.pyqtSignal(),渣语文见谅,不懂的请看程序中的做法:
  1. class Board(QtWidgets.QFrame):
  2.     board_width = 10
  3.     board_height = 22
  4.     speed = 300
  5.     messages_to_satusbar = QtCore.pyqtSignal(str)
复制代码
可以看到最后一句中我们指定要传入的参数为一个str类型的变量
(2)在用emit()方法发射信号时,将要传入槽函数的参数作为emit方法的参数传入,如这个程序中
  1.         if not self.try_move(self.cur_piece, self.cur_x, self.cur_y):
  2.             self.cur_piece.set_shape(Tetrominoes.NoShape)
  3.             self.timer.stop()
  4.             self.is_started = False
  5.             self.messages_to_satusbar.emit("游戏结束")
复制代码
当符合某种条件时,要在状态栏显示"游戏结束 ",将"游戏结束"这个字符串传入信号message_to_statusbar所对应的槽函数
(3)定义信号时定义的、信号发谢时传入的、以及槽函数定义中的变量类型,数量和顺序,必须一致,如这个程序中
  1. self.tetris_board.messages_to_satusbar.connect(self.status_bar.showMessage)
复制代码
这里的showMessage是QStatusBar自带的方法,我们看看它的这个方法的定义
  1. showMessage(...)
  2.       QStatusBar.showMessage(str, int msecs=0)
复制代码
可以看到,它可以传入两个参数,第一个参数就是str类型的参数,第二个默认参数可以不传,正好与上面信号的定义和发射相对应。
3、关于函数和变量的命名问题,python本身是有自己的规范的(请搜索PEP 8),在前面教程的代码中,我都将原作者所使用的小驼峰写法(即首单词字母小写,后续单词首字母大写,单词之间不用_隔开,如myPrincess)改成PEP 8的写法(如my_princess),所以在最后的章节中,我依然使用这样的写法(说老实话我也纠结,但为了一致我还是决定这样做),鱼油们在写自己的程序的时候请自行取舍
然后我们看原教程中对于这整个游戏代码的解释


评分

参与人数 1荣誉 +10 鱼币 +10 贡献 +10 收起 理由
~风介~ + 10 + 10 + 10 帅呆了~

查看全部评分

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-1 11:14:00 | 显示全部楼层
本帖最后由 lightninng 于 2015-5-16 18:15 编辑

我们对游戏作一些简化,以便于理解。游戏在启动后立刻开始。我们可以通过按'p'键暂停游戏。空格键将使俄罗斯方块立刻落到底部。游戏使用固定的速度,没有实现加速。游戏的分数是我们已经消掉的行数。
  1.         self.status_bar = self.statusBar()
  2.         self.setCentralWidget(self.tetris_board)
  3.         self.tetris_board.messages_to_satusbar.connect(self.status_bar.showMessage)
复制代码
我们创建一个状态栏用来显示信息。我们将显示三种可能的信息,已经消掉的行数,暂停的消息和游戏结束的消息。
  1.         self.cur_x = 0
  2.         self.cur_y = 0
  3.         self.num_lines_moved = 0
  4.         self.board = []
复制代码
在我们开始游戏之前,我们初始化一些重要的变量。self.board变量四从0到7的数字列表。它表示不同的图形的位置和面板上剩余的图形。
  1. for j in range(Board.board_width):
  2.                 shape = self.shape_at(j, Board.board_height - i - 1)
  3.                 if shape != Tetrominoes.NoShape:
  4.                     self.draw_square(paint, rect.left() + j*self.square_width(),
  5.                                      board_top + i*self.square_height(), shape)
复制代码
游戏的显示分成两步。第一步,我们绘制所有的图形,或已经掉落在底部的剩余的图形。所有的方块被保存在self.board列表变量中。我们通过使用shape_at()方法来访问它。
  1. if self.cur_piece.shape() != Tetrominoes.NoShape:
  2.             for i in range(4):
  3.                 x = self.cur_x + self.cur_piece.x(i)
  4.                 y = self.cur_y + self.cur_piece.y(i)
  5.                 self.draw_square(paint, rect.left() + x*self.square_width(),
  6.                                  board_top + (Board.board_height-y-1)*self.square_height(), self.cur_piece.shape())
复制代码
下一步是绘制正在掉落的当前块。
  1.         elif key == QtCore.Qt.Key_Left:
  2.             self.try_move(self.cur_piece, self.cur_x - 1, self.cur_y)
  3.         elif key == QtCore.Qt.Key_Right:
  4.             self.try_move(self.cur_piece, self.cur_x + 1, self.cur_y)
复制代码
在keyPressEvent我们检查按下的按键。如果我们按下了右方向键,我们就试着向右移动块。试着是因为块可能无法移动。
  1.     def try_move(self, new_piece, new_x, new_y):
  2.         for i in range(4):
  3.             x = new_x + new_piece.x(i)
  4.             y = new_y - new_piece.y(i)
  5.             if x < 0 or x >= Board.board_width or y < 0 or y >= Board.board_height:
  6.                 return False
  7.             if self.shape_at(x, y) != Tetrominoes.NoShape:
  8.                 return False
  9.         self.cur_piece = new_piece
  10.         self.cur_x = new_x
  11.         self.cur_y = new_y
  12.         self.update()
  13.         return True
复制代码
在try_move()方法中,我们尽力来移动我们的块,如果块在背板的边缘或者靠在其他的块上,我们返回假,否则我们将当前块放置在新的位置。
  1.     def timerEvent(self, event):
  2.         if event.timerId() == self.timer.timerId():
  3.             if self.is_waiting_after_line:
  4.                 self.is_waiting_after_line = False
  5.                 self.new_piece()
  6.             else:
  7.                 self.one_line_down()
  8.         else:
  9.             QtWidgets.QFrame.timerEvent(self, event)
复制代码
时间事件中,我们或者在上一个方块到达底部后创建一个新方块,或者将下落的方块向下移动一行。
  1.     def remove_full_lines(self):
  2.         num_full_lines = 0
  3.         rows_to_remove = []
  4.         for i in range(Board.board_height):
  5.             n = 0
  6.             for j in range(Board.board_width):
  7.                 if not self.shape_at(j, i) == Tetrominoes.NoShape:
  8.                     n += 1
  9.             if n == 10:
  10.                 rows_to_remove.append(i)
  11.         rows_to_remove.reverse()
  12.         ......
复制代码
如果方块到达了底部,我们调用removeFullLines()方法。首先我们找出所有的满行,然后我们移去他们,通过向下移动当前添满的行上的所有行来完成。注意,我们反转将要消去的行的顺序,否则它会工作不正常。这种情况我们使用简单的引力,这意味着块会浮动在缺口上面。
  1.     def new_piece(self):
  2.         self.cur_piece = self.next_piece
  3.         self.next_piece.set_random_shape()
  4.         self.cur_x = Board.board_width / 2 + 1
  5.         self.cur_y = Board.board_height - 1 + self.cur_piece.min_y()
  6.         if not self.try_move(self.cur_piece, self.cur_x, self.cur_y):
  7.             self.cur_piece.set_shape(Tetrominoes.NoShape)
  8.             self.timer.stop()
  9.             self.is_started = False
  10.             self.messages_to_satusbar.emit("游戏结束")
复制代码
newPiece()方法随机生成一个新的俄罗斯方块。如果方块无法进入它的初始位置,游戏结束。
  1. self.coords = [[0, 0] for i in range(4)]
复制代码
在生成之前,我们创建一个空的坐标列表,这个列表将会保存俄罗斯方块的坐标,例如这些元组(0, -1), (0, 0), (1, 0), (1, 1)表示一个S形。
当我们绘制当前掉落的块时,我们在self.curX,self.curY位置绘制。然后我们查找坐标表并绘制所有的四个方块。


然后说说我自己在输入时犯的一些错误:
1、运行程报错:QPainter::setPen: Painter not active,原因我是在定义paintEvent方法中的paint时,原文中是这样的paint = QtGui.QPainter(self),但我把括号中的self丢了。
       当时我的判断是Board类中的draw_square方法中,在进行绘制之前没有运行QPainter实例的begin()方法,当时的解决方法就是在第一句绘制语句之前加上painter.begin(self),另在绘制结束之后加上了painter.end(),程序可以跑通。
       至于两种方法的不同,请翻看9.1节中关于painter的解释,另外,当你使用上面的程序中的写法时,即painter.begin(self),请不要在绘制结束时调用painter.end(),否则,在程序会在绘制了一个方块后因为painter被结束而无法继续绘制,结果是不听的报错QPainter::setPen: Painter not active和QPainter::end: Painter not active, aborted,画面上也会只有一个方块往下落
2、在每个方块落下时,有时会出现落的位置不对(在高度上有错位,本来还能往下再落一格或者两格),有时候会出来落下来的方块会和已有的方块部分重叠的问题,原因是我把原文中101行和160行中的表达式里的-号写成了+号,改回后程序能以正常的形态运行。另外我做了两个小小的实验:
(1)只将160行的+号改回正确的-号的话,情况是这样的,board里存放的面板方块堆砌情况和你在屏幕上绘制的图像是不对应的,所以从视觉效果上会出现某个块落地之后上下的反转(因为-号变+号了嘛),还有就是新的块落下来的时候会穿过你从屏幕上看到的已有的块(因为board存放的情况和实际绘制的情况不符,图像上显示有方块的地方实际是没有方块的)
(2)只将101行的+号改为回正确的-号的话,由于piece_dropped方法负责的是方块落地之后对整个board的重建,所以方块落地时会上下颠倒(同样是由于-号变+号),但是这时的颠倒是因为在重绘board时,落地的块在放到整个board中时上下颠倒了,由于落地判断时用的是本身的块,而落地后用的是上下颠倒的块,所以会出现某些已经在的格子被落下来的块覆盖的现象,还有就是正方形的块落下来时会离它应该落在的位置高一格
(3)我出现的问题就是两都都搞反了,也就是会出现上面两种情况的混合体,囧死,我足足查了半天才找到问题所在
3、当落下的块造成了行消除时,会转瞬间出现一个黑块,虽然时间很短但是很碍眼,因为我在第98行的代码中搞丢了self.cur_piece后面的.shape()
当时我的解决方式是在draw_square方法中第一行加入这样的代码:
  1. if shape == Tetrominoes.NoShape:
  2.             return
复制代码
因为我判断这是由于落下的块为cur_piece,当块dropped之后cur_piece会被至为NoShape,所以在所在的位置会绘制颜色为0X000000(即黑色)的块,那么只要在绘制每个方格时不绘制为NoShape的方格即可。后来发现的问题所在,丢掉之后,if语句永远为真,所以在消行后cur_piece的NoShape也会被绘制,出现一个黑色的块。改回正确代码即可。
4、当我调整真个游戏主窗口的大小时,方块的绘制出现问题,因为我在第217行的代码中将self.square_width() - 2与 self.square_height() - 2,写反了,所以在绘制方块时,x轴和y轴交换了绘制,但是线条绘制正确,所以出现了颜色不在线条所限定的区域内
PS:最开始遇到这些问题的时候,我觉得可能是程序本身的问题(事实证明更多时候错的是自己),由于落的位置不对,我判断是try_move函数返回了不正确的值,但是知道bug的原因之后,一下想通了好多:首先,你在面板上看到的是自己绘制的图像,它不一定等于你想绘制的图像,比如你搞错的绘制的位置;其次,在敲代码时请万分小心,不然你将花费 大量的时间去为你的马虎买单

下接37#

评分

参与人数 1荣誉 +10 鱼币 +10 贡献 +5 收起 理由
~风介~ + 10 + 10 + 5 热爱鱼C^_^

查看全部评分

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-12 12:06:13 | 显示全部楼层
本帖最后由 lightninng 于 2015-5-12 12:07 编辑
~风介~ 发表于 2015-5-12 12:03
这是我见过的最好的PyQt5教程,没有之一!
主要内容都是人家的,自己加了些学习过程中遇到的问题而已
还是谢谢夸奖~~


想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-12 17:53:38 | 显示全部楼层
~风介~ 发表于 2015-5-12 12:38
感觉我个人没有写教程的耐心~

哈哈,那也不一定,性格也是会变的
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-12 22:17:48 | 显示全部楼层
~风介~ 发表于 2015-5-12 19:26
过个一两百年也许会变!

对了,问一个问题,我之前写这个贴子的时候是直接把图传到贴子里,后来有些图又重新做了修改,我上传了新的图到我的相册里,然后把之前在贴子里插入的图删除(但在附件里没有删除,因为编辑模式下好像看不到已经上传的图的列表),插入了相册里的图,结果问题来了,最开始上传的图都一起在那一层的最下面显示,比如3楼的最下面,和10楼的最下面,我该怎么把它们处理掉呢~~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-15 09:15:08 | 显示全部楼层
1102029952 发表于 2015-5-15 08:38
而且,图标必须是ico文件,貌似不是随便找个图片改扩展名就行

windows中的图标好像确实得要ico,不过PyQt中亲测png文件可用,似乎是需要有透明图层的文件类型~~

jpg和jpeg文件可能不行(未验证)~~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-15 13:06:07 | 显示全部楼层
1102029952 发表于 2015-5-15 11:34
在designer里做了ui,到Eric里编译成py文件了,然后再py里修改代码,似乎不能同步到ui文件里?比如py修改 ...

没用过designer,这个问题其实你可以自己试试

这个教程是基于纯手打代码的界面,designer设计的界面文件是.ui的后缀,它有两种使用方法:一是可以编译成py文件然后导入,二是可以直接用LoadUi方法导入。编译成py文件的界面代码和手码的代码应该没有多大区别,直接修改py文件的代码在运行时就能体现出来,前提是你是用import py文件名,这样的方式导入的界面;如果你用LoadUi方法直接导入.ui的文件的话,那应该需要再在designer中修改
以上均为我的想法,未经过验证,不过应该没什么问题
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-16 18:08:18 | 显示全部楼层
本帖最后由 lightninng 于 2015-5-31 22:31 编辑

11.3 改进它之前的工作
显然上面的俄罗斯方块游戏并不能让我们满意,它和我们想象中的东西还是有很大差距(好吧,我承认再做也做不出什么卵来,除非你做成这样http://www.bilibili.com/video/av2173943/),然而现阶段我还做不出上面的视频里面的东西,但是我们总想把自己做的东西(哪怕是别人的东西)变的更好不是么,至少我是这样。所以我开始对原版的程序吐槽:
1、  界面好low,只能用状态栏记录分数,啥信息都在状态栏,万一暂停的时候我想看分数肿么办,ok,关于界面要做到这样:
(1)      方块要更好看一些,最好是用到别人做好的素材,当然自己画也可以
(2)      分数显示,暂停状态和游戏结束不要都显示在状态栏,专门给它们找一个地方显示
2、  功能好像还不太全吧,至少和我小时候玩的不太一样,那么关于功能至少要加这些:
(1)      下一个要出现的块应该在屏幕的某个地方有显示
(2)      一次消除三行或者四行应该有分数奖励,另外咱们的分数能不要等于消除的行数么,看起来好少~~
(3)      当分数达到一定的高度应该提高游戏难度,即加快下落的速度
(4)      游戏结束之后不能重新开始,必须要关掉游戏再重新打开
既然要改进,那么至少我们应该搞清楚,它到底做了些什么,关于游戏的制作,甲鱼小哥的视频里也有些许提到打飞机(很可惜我忘了在哪一集了),让我们来引用 crossin 的打飞机教程中的一段话来说明我们玩的游戏代码到底做了什么。
11.3.1游戏的本质
你小时候有没有玩过这样一种玩具:一块硬纸,一面画着一只鸟,一面画着一个笼子。硬纸下粘上一根细棒。用手来回转动细棒,让硬纸的两面快速交替出现,就会看见鸟被关在了笼子里。这种现象被称为视觉暂留,又称余晖效应。人眼的性质使得光信号在进入之后,会保持一小段时间,这段时间大约是 0.1~0.4 秒。电影、动画便是利用这种现象得以实现,把一幅幅静态画面快速连续播放,形成看上去连续的活动画面。游戏能动起来的原因就是因为视觉暂留效果。
所以在每个游戏中都会有一个循环体,注释为“游戏主循环”,这就是游戏的主体部分。每次循环都相当于是一张静态的画面,程序一直运行,不停的重绘画面,画面就有了动态的效果。
与动画不同,游戏中不仅要把一幅幅画面播放出来,还需要处理玩家的操作与游戏中内容的交互。所以在这个循环体中,还要去接收玩家的输入,以及处理游戏中的各种逻辑判断、运动、碰撞等等。

       以上引用完毕,现在来让我们想想俄罗斯方块游戏都做了什么:
首先,在游戏进行过程中,会有一个块不停的落下,在下落的过程中,你可以使用键盘将其进行旋转、左右移动、往下落一行,以及直接落到底部等操作;然后,当它落到底部时,会判断是否有一行被铺满,如果铺满则将该行消去,并从上方生成一个新的块;最后,当新生成的块无法生成时(即块生成的位置被已经落下去的块占据),则游戏结束。

      看起来很简单吧,其实代码做了很多事:
首先,为了让块看起来不停的往下落,它需要控制块隔一段时间(这个间隔很小)就块的位置往下移动一格并刷新整个游戏画面(和上面我们说的一样),同时,为了能让键盘控制,它需要响应键盘操作,对不同的按键做出不同的反应并刷新整个游戏画面(它又出现了);然后,当它落到底部时,如果有一行被铺满,将将该行消去并刷新整个游戏画面(第三次出现),然后在上方出现一个新的块并刷新画面(我累了,后面不再标注它了);最后,当出现初始块的位置不能放下一个新的块时结束游戏
11.3.2教程中的俄罗斯方块
下面我们就教程中未解释的部分来说一下。
  1. class Tetris(QtWidgets.QMainWindow):
  2.     def __init__(self):
  3.         super(Tetris, self).__init__()
复制代码
我们先来看界面的部分,像往常以样我们先选择了QmainWindow类做为我们的基类设计了游戏的大框架Tetris
  1. class Board(QtWidgets.QFrame):
  2.         …
复制代码
然后用Qframe类作为基类设计了游戏显示的面板Board类
  1.         self.tetris_board = Board(self)
  2.         self.setCentralWidget(self.tetris_board)
复制代码
然后将Board类的tetris_boardsetCentralWidget方法设置它为tetris的中间部件,tetris_board的大小由tetris决定。
  1. self.timer = QtCore.QBasicTimer()
复制代码
在tetris_board中我们设置一个记时器,它负责按指定的时间间隔触发timerEvent,执行当前块落下一行,刷新界面这样的操作。
  1.         self.cur_piece = Shape()
  2.         self.next_piece = Shape()
  3.         self.cur_x = 0
  4.         self.cur_y = 0
  5.         self.num_lines_moved = 0
  6.         self.board = []
复制代码
cur_piece 和next_piece分别为当前块和下一个块,cur_x 和cur_y则记录当前块的位置,num_lines_moved记录已经消去的行数,board记录整个游戏区域内哪些格子有方块(以及它们的类型),当然这里的记录是不包含cur_piece,所以在绘制游戏画面时要同时将board记录的内容和cur_piece绘制到画面上
  1. board_width = 10
  2. board_height = 22
  3. speed = 300

  4.     def square_width(self):
  5.         return self.contentsRect().width() / Board.board_width

  6.     def square_height(self):
  7.         return self.contentsRect().height() / Board.board_height
复制代码
这段代码中,board_width和board_height是游戏的横向方格的数目和纵向方格数目,然后根据整个框体的大小,用square_width和square_height方法计算每个格子的长和宽。

  1. def drop_down(self):
  2.       …
  3.     def one_line_down(self):
  4.        …
  5.     def piece_dropped(self):
  6.         …
复制代码
这三个方法中:drop_down负责直接将当前块落到底部,它不停的尝试是否可以往下落一行,直到尝试失败调用piece_dropped方法,在每往下落一行时并不重新绘制,所以你看到的是它直接落到底部,;one_line_down负责将当前块往下落一行,如果尝试失败则调用piece_dropped方法;piece_dropped当调用到它时,表示当前块已经无法再移动了,将其放到board中,并调用new_piece生成新的cur_piece
  1. def paintEvent(self, event):
  2.         …
  3.     def draw_square(self, painter, x, y, shape):
  4.         …
复制代码
这两个方法负责对图像的绘制,心思比较细鱼油可能会发现timerEvent中并未直接调用paintEvent对画面进行更新,那我们来看看timerEvent调用的几个方法中是否有更新画面的操作,从timerEvent -> one_line_down -> try_move一路调用找下来,在try_move方法中发现了这样的语句
  1. def try_move(self, new_piece, new_x, new_y):
  2.         …
  3.         self.update()
复制代码
让我们来看看这个update()方法是干什么的,在 PyQt5的线上文档(http://pyqt.sourceforge.net/Docs/PyQt5/index.html)QFrame类的文档中并未发现update方法,于是找它的父类(Inherits),只有一个QWidget,找到了update方法,它是这么解释的(原版是英文,中文翻译来自这里http://www.kuqin.com/qtdocument/qwidget.html#update,它是Qt3.0.5的翻译,不过特性应该没有什么改变凑合看吧):
void QWidget::update ()
更新窗口部件,除非更新已经失效或者窗口部件被隐藏。
这个函数不会导致一个立刻的重新绘制——更正确的是,当Qt回到主事件回路中时,它规划了所要处理的绘制事件。这样允许Qt来优化得到比调用repaint()更快的速度和更少的闪烁。
几次调用update()的结果通常仅仅是一次paintEvent()调用。
Qt通常在paintEvent()调用之前擦除这个窗口部件的区域。仅仅只有在WRepaintNoErase窗口部件标记被设置的时候,窗口部件本身对绘制它所有的像素负有责任。
也就是说这个update()方法会调用paintEvent()方法,我们的画面将被重新绘制,代码看的仔细的鱼油应该发现了,游戏中的大部分移动都是调用了try_move方法,这使得游戏画面得以不停重绘,另外一个地方remove_full_lines()方法中因为与try_move没有什么关系,于是在对board的更新完成后调用了update()方法。
  1. class Shape(object):
  2.     coords_table = (…)
  3.        …
复制代码
游戏中代表块的类,也就是cur_piece的定义,俄罗斯方块的每种形状都是由四个方块组成,所以在Shape类中, coords_table保存的是每种形状中四个方块的相对位置,它的其它内置方法请鱼油自行研究。

以上,对于游戏的运行和教程中代码的解释全部完成,后面我们就要开始动手对游戏进行改进了

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-23 16:18:08 | 显示全部楼层
本帖最后由 lightninng 于 2021-11-20 14:48 编辑

答辩完成,现在就找工作了,决定赶紧把这个贴子完结了,加快了进度,解决了一些技术方面的细节,继续更贴
11.4 动手来改进
11.4.1 动手前的思考
想一想我要重新做的游戏和原游戏有什么不同:
1、外观:
(1)最好能有个菜单栏(不能比扫雷还差吧~!~,话说win7的扫雷似乎做的很好的样子,可以借鉴一些),主窗口为QMainWindow类,可以很方便的添加菜单栏
(2)除了主游戏面板,还得有一个显示下一个块的面板,一个显示分数的面板,以及相应的提示文字
(3)方块的样式要更好看一些,当然用自己找到的素材来填充应该是最先想到的方式
(4)游戏背景不能是原始的一片灰色,最好能自己设置成喜欢的图片
2、功能:
(1)主游戏面板每次产生新的块时,显示下一个块的面板要同步更新下一个块
(2)主游戏面板每次消掉新的行时,在分数面板上要显示更新后的分数
(3)在游戏结束,或者游戏正在进行时,可以通过菜单或者快捷键重新开始新的游戏
3、抽象:
对于我们来说,我们不想写重复的代码,那么需要思考一下,有没有模块是可以复用的,当然的想到,主游戏面板和显示下一个块的面板都会绘制方块到部件上,它们的draw_square方法是共用的,但是需要注意的是,两个面板中方块的长和宽的计算方式是不同的,主游戏面板中是根据矿体大小、行数和列数计算得到的,而显示下一个块的面板中是直接读取主面板的方块的长和宽,所以要把方块的长和宽用专门的方法封装起来(get_square_width和get_square_height),然后在draw_square方法中调用这两个方法获得方块的长和宽。
另外,对于俄罗斯方块的块原教程代码中是用Shape类来描述的,这部分我也想进行一些改进,这部分后面再完成
下面来看看和上面的各种问题相关的一些点。
11.4.2 PyQt5中的资源打包
在做游戏时,往往要用到自己的素材,包括图片,音频等等,我们可以直接把所有的素材放在同一文件夹中,或者同一文件夹的某个目录下,这样往往有两个问题:
1、程序打包时(常用的pyinstaller, py2exe, cxfreeze),必须将图片文件手动拷贝到正确的位置,打包后的程序图片才能显示正常
2、如果频繁的读取文件(当然有时候这种情况是因为自己代码写的不好),程序的速度会著的变慢,从打包之后的文件中调用会提高速度(这是我打包资源的初衷)。
PyQt中对于资源打包是有自己的方法的,这里我参考的是这篇贴之中的内容http://www.csdn123.com/html/topnews201408/58/14658.htm
先来看看我们的资源:七种不同颜色的方块,程序图标,背景图

                               
登录/注册后可看大图

截图:资源文件
打包的输入是这9个文件,输出是一个.py文件,打包步骤如下:
1、用QtDsigner创建qrc文件。打开QtDsigner,随意创建一个新的窗体,然后点击右下角资源浏览器中的铅笔
2、点击编辑资源窗口左下角的第一个按钮——新建资源文件,然后在弹出的文件对话框中,选择你的资源文件所在的目录,并输入你要创建的qrc文件的名字,我的qrc文件名叫作Tetris
3、点击第四个按钮——添加前缀,这里可以随意,我添加的是source,也可以不填(即无前缀)
4、点击第五个按钮——添加文件,在弹出的文件对话框中选择你所有的资源文件

5、点击确定后,便可以在你的资源所在的文件夹下看到生成的qrc文件了

                               
登录/注册后可看大图

截图:资源打包过程
6、将qrc文件转化为py文件,windows的用户运行cmd,然后进入qrc文件所在目录,然后输入命令pyrcc5 qrc_name.qrc -o py_name.py,这里qrc_name必须是你的qrc文件的文件名,py_name可以随意指定,它是转换之后的得到的py文件的文件名,我的命令是这样的:
pyrcc5Tetris.qrc -o tetris_file.py
当然最省事的方法就是建一个bat文件,把命令输进去,然后每次运行它就可以重新生成。最后我得到了一个名字为tetris_file.py的文件,打包完毕

最后说一说怎么在自己的pyqt代码中用到打包的文件,首先你要把你打包好的py文件放到你的代码同一目录下,然后在代码头部用import命令导入,然后在需要用到资源时,用”:文件路径\前缀\文件名”的形式调用它(千万不要忘了这个冒号),这里说说为什么最开始创建qrc文件时要放在资源同一目录,这个调用中文件路径指的就是资源文件相对于qrc文件所在路径,当它们在同一目录下时,文件路径可以直接省略。同理,当你不写前缀时,前缀也可以省略,这里我的前缀是source,所以我调用背景文件ground.png的路径名就应该是”:source\ground.png”。
11.4.3 背景图片
运行过教程代码的鱼油应该都会发现,教程完成的这个俄罗斯方块小游戏中,游戏面板中的方块大小是随着窗体的大小变化的,那么如果我们给它加上背景图片,就要求背景图片能跟随部件大小同时进行拉伸和缩放,如果仅仅简单的填充一个背景,那么背景图太小会用平铺的方式充满窗口,背景图太大只会显示左上角的部分。

下面是一个背景图自适应窗体大小的类:
  1. class CentralWidget(QWidget):
  2.     def __init__(self):
  3.         super(CentralWidget, self).__init__()

  4.         # 设置背景图案
  5.         self.background = QImage(r":source\ground.png")
  6.         self.setAutoFillBackground(True)

  7.     def resizeEvent(self, event):
  8.         # 重写resizeEvent, 使背景图案可以根据窗口大小改变
  9.         QWidget.resizeEvent(self, event)
  10.         palette = QPalette()
  11.         palette.setBrush(QPalette.Window, QBrush(self.background.scaled(event.size())))
  12.         self.setPalette(palette)
复制代码
在给一个QWidget部件加上背景,这里用的是setPalette方法,看过WeiY小哥教程的朋友应该知道,用部件的setStyleSheet方法可以完成对背景图版的设置。
  1.         self.background = QImage(r":source\ground.png")
复制代码
先将背景图片读取出来。
  1.         self.setAutoFillBackground(True)
复制代码
官方文档中关于autoFillBackground中有这么一句:如果启用该属性,Qt将在调用paintEvent前填充部件的背景。所以,若想设置背景图案这一句是必须的,另外需要注意的是当它与style sheet同时使用时,若style sheet已有一个有效的backgroundborder-image,这个属性将被禁用,也就是说当你使用了setStyleSheet设置了backgroundborder-image属性时,就无法应用setPalette方法来设置背景了。
  1.     def resizeEvent(self, event):
  2.         # 重写resizeEvent, 使背景图案可以根据窗口大小改变
  3.         QWidget.resizeEvent(self, event)
复制代码
由于我们要求窗体大小变化时,背景能随之进行变化,所以我们需要重新实现resizeEvent属性。这里必须先重载QWidget部件的resizeEvent,以提经常提到的操作。

  1.         palette = QPalette()
  2.         palette.setBrush(QPalette.Window, QBrush(self.background.scaled(event.size())))
复制代码
创建一个QPalette的实例,并设定它的绘图模式,当设置窗口背景时setBrush方法的第一个参数应为QtGui.QPalette.Window(这个参数的其它选项请看官方文档的QPalette类的介绍),第二个参数设为QBrush类,并载入图片,这里,我们调用QImage的scaled方法,将窗体更改后的大小(event.size())传入,得到了适应窗口大小的图片。另外,如果大家想直接在背景上绘制一整个颜色块,只需要用palette.setColor方法,如
  1. palette.setColor(QtGui.QPalette.Window,QtGui.QColor(0, 0, 0)
复制代码
  1.         self.setPalette(palette)
复制代码
最后我们将适应窗口大小的背景图绘制到部件上。


请到52楼查看后续章节


想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-25 19:12:53 | 显示全部楼层
wei_Y 发表于 2015-5-25 17:22
下次加点注释呗。。

好的,代码超过一定行数我会加注释的
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-25 20:37:23 | 显示全部楼层
shinima 发表于 2015-5-25 19:09
楼楼的帖子非常棒
现在网上很多教程是Qt4的, 很多函数名,变量名和Qt5都不太一样, 初学者就会很 ...

谢谢支持,欢迎交流~~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-27 22:05:12 | 显示全部楼层
kwen24 发表于 2015-5-27 19:33
留下脚印,谢谢楼主

欢迎多交流~~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-28 13:09:57 | 显示全部楼层
~风介~ 发表于 2015-5-12 19:26
过个一两百年也许会变!

原来是限时精华
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-28 19:02:29 | 显示全部楼层
本主题由 System 于 2015-5-16 21:00 解除限时精华~~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-29 00:02:18 | 显示全部楼层
小皮猪 发表于 2015-5-28 21:38
你好楼主,最近我在写一个pyqt5的程序,前台只有一个滚动条,这需要后台的程序根据磁盘的大小比例调节滚动 ...

滚动条是指拉动页面滚动的那个条么,我还没学到~~!
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-29 23:00:24 | 显示全部楼层
小皮猪 发表于 2015-5-29 20:50
哦,错了,应该是进度条,我研究会了,谢谢

好的,欢迎交流~~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-6-1 14:42:09 | 显示全部楼层
本帖最后由 lightninng 于 2021-11-20 14:47 编辑

11.4.4 方块颜色
显然直接用内置的fillRect方法绘制的方块太丑了,随便上网就能找到一大把好的素材,下面说说怎么把方块变的好看一些。分析了教程的代码,可以知道方块的绘制是用draw_squre方法完成的,看看改了之后的draw_squre代码。
  1.     def draw_square(self, x, y, square_type):
  2.         shape_table = [0x000000, QPixmap(":source\Red.png"),
  3.                        QPixmap(":source\Green.png"), QPixmap(":source\Blue.png"),
  4.                        QPixmap(":source\Yellow.png"), QPixmap(":source\Purple.png"),
  5.                        QPixmap(":source\Water_Green.png"), QPixmap(":source\Orange.png")]
  6.         color_table = [(0, 0, 0), (190, 110, 110), (160, 190, 110), (110, 140, 190),
  7.                        (165, 170, 180), (150, 130, 180), (100, 180, 190), (190, 135, 80)]

  8.         # 调用get_square_width方法和get_square_height方法获取方块的长和宽
  9.         square_width, square_height = self.get_square_width(), self.get_square_height()
  10.         painter = QPainter(self)

  11.         # 绘制方块内部
  12.         painter.drawPixmap(x + 1, y + 1, square_width - 2, square_height - 2, shape_table[square_type])

  13.         # 绘制方块边框
  14.         color = QColor(*color_table[square_type])
  15.         painter.setPen(color.lighter())
  16.         painter.drawLine(x, y, x + square_width - 1, y)
  17.         painter.drawLine(x, y, x, y + square_height - 1)
  18.         painter.setPen(color.darker())
  19.         painter.drawLine(x + square_width - 1, y + 1, x + square_width - 1, y + square_height - 1)
  20.         painter.drawLine(x + 1, y + square_height - 1, x + square_width - 1, y + square_height - 1)
复制代码
其实改动最大的只有一点就是我们把调用fillRect方法改为了调用drawPixmap,fillRect方法绘制一个矩形颜色块,而drawPixmap绘制一个pixmap图形。
  1.         shape_table = [0x000000, QPixmap(":source\Red.png"), ... ]
复制代码
首先我们将各种不同形状的俄罗斯方块的图片读取到shape_table中。
  1.         painter.drawPixmap(x + 1, y + 1, square_width - 2, square_height - 2, shape_table[square_type])
复制代码
然后调用drawPixmap方法以指定的位置和大小将pixmap对象绘制到部件上。
11.4.5 初步的成果
经过上面的讨论,我们初步写了一些代码,这里面包括自适应背景图的CentralWidget部件的一些代码,以及游戏主面板(GameBoard)和显示下一个块的面板(NextBoard)的基类(Board)的代码:
  1. from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QFrame, QLabel
  2. # from PyQt5.QtCore import
  3. from PyQt5.QtGui import QPixmap, QImage, QPainter, QPalette, QBrush, QColor
  4. import tetris_file


  5. class Tetris(QMainWindow):
  6.     def __init__(self):
  7.         super(Tetris, self).__init__()

  8.         self.resize(450, 660)
  9.         self.setCentralWidget(CentralWidget())  # 设置Board为中心部件看看效果


  10. class CentralWidget(QWidget):
  11.     def __init__(self):
  12.         super(CentralWidget, self).__init__()

  13.         # 设置背景图案
  14.         self.background = QImage(r":source\ground.png")
  15.         self.setAutoFillBackground(True)

  16.     def resizeEvent(self, event):
  17.         # 重写resizeEvent, 使背景图案可以根据窗口大小改变
  18.         QWidget.resizeEvent(self, event)
  19.         palette = QPalette()
  20.         palette.setBrush(QPalette.Window, QBrush(self.background.scaled(event.size())))
  21.         self.setPalette(palette)


  22. class Board(QFrame):
  23.     def __init__(self, parent=None):
  24.         super(Board, self).__init__(parent)

  25.         # 设置面板边框
  26.         self.setStyleSheet("border: 1px groove gray; border-radius: 5px; ")

  27.     def get_square_width(self):
  28.         return 100

  29.     def get_square_height(self):
  30.         return 100

  31.     def paintEvent(self, event):
  32.         # 重写panitEvent,绘制一个方块在Board上看看效果
  33.         self.draw_square(self.contentsRect().width()/2, self.contentsRect().height()/2, 1)

  34.     def draw_square(self, x, y, square_type):
  35.         shape_table = [0x000000, QPixmap(":source\Red.png"),
  36.                        QPixmap(":source\Green.png"), QPixmap(":source\Blue.png"),
  37.                        QPixmap(":source\Yellow.png"), QPixmap(":source\Purple.png"),
  38.                        QPixmap(":source\Water_Green.png"), QPixmap(":source\Orange.png")]
  39.         color_table = [(0, 0, 0), (190, 110, 110), (160, 190, 110), (110, 140, 190),
  40.                        (165, 170, 180), (150, 130, 180), (100, 180, 190), (190, 135, 80)]

  41.         # 调用get_square_width方法和get_square_height方法获取方块的长和宽
  42.         square_width, square_height = self.get_square_width(), self.get_square_height()
  43.         painter = QPainter(self)

  44.         # 绘制方块内部
  45.         painter.drawPixmap(x + 1, y + 1, square_width - 2, square_height - 2, shape_table[square_type])

  46.         # 绘制方块边框
  47.         color = QColor(*color_table[square_type])
  48.         painter.setPen(color.lighter())
  49.         painter.drawLine(x, y, x + square_width - 1, y)
  50.         painter.drawLine(x, y, x, y + square_height - 1)
  51.         painter.setPen(color.darker())
  52.         painter.drawLine(x + square_width - 1, y + 1, x + square_width - 1, y + square_height - 1)
  53.         painter.drawLine(x + 1, y + square_height - 1, x + square_width - 1, y + square_height - 1)


  54. class GameBoard(Board):
  55.     pass


  56. class NextBoard(Board):
  57.     pass


  58. class ScoreBoard(QLabel):
  59.     pass

  60. if __name__ == "__main__":
  61.     import sys
  62.     app = QApplication(sys.argv)
  63.     tetris = Tetris()
  64.     tetris.show()
  65.     sys.exit(app.exec_())
复制代码
为了看看我们的Board类中draw_square方法的效果,我们将其设为中心部件,并在它的paintEvent方法中调用draw_square方法绘制一个块,大小为100,100。然后可以再将CentralWidget类设为中心部件看看背景图的效果(请自行实现,改一个词而已)

                               
登录/注册后可看大图

截图:绘制方块和背景

有些鱼油喜欢先看效果,我把我用到的图片资源,打包好的tetris_files.py放到一个压缩文件里,需要的鱼油可以自行下载。

链接: http://pan.baidu.com/s/1mgnBsus 密码: rbab


2021.11.20
准备用PySide2重新实现,原因见1#楼最后面更新的说明

请到129#楼,查看后续章节

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 1 反对 0

使用道具 举报

 楼主| 发表于 2015-6-4 10:00:13 | 显示全部楼层

谢谢夸奖,欢迎多来python版交流~~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-6-4 23:07:41 | 显示全部楼层
小甲鱼 发表于 2015-6-4 12:42
测试回复上传图片。

我在这个贴子里回复也不能上传图片,在别的贴子里回复可以上传图片,下面我这个贴子里上点图片按钮的截图

                               
登录/注册后可看大图

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-4-20 03:34

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表