鱼C论坛

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

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

  [复制链接]
 楼主| 发表于 2015-4-18 12:47:41 | 显示全部楼层
本帖最后由 lightninng 于 2015-5-16 16:54 编辑

8 PyQt5中的拖放操作
在这一章中,我们将介绍PyQt5中的拖放操作。
在计算机图形用户界面中,拖放操作是指点击一个虚拟对象并把它拖到一个不同的位置或其他的虚拟对象上的操作(或支持的行为).一般来说,它可以用来调用多种操作,或创建两个抽象对象间的特定类型的关联。(Wikipedia)
拖放功能是图形用户接口的一个明显的特征,拖放操作允许用户直观的组合事物。
一般来说,我们可以拖放两种东西,数据和图形对象。如果我们将一幅图像从一个应用程序拖到另一个,我们拖放的是二进制数据。如果我们拖起一个火狐的标签并将它放到另一个地方,我们拖放的是图形组件。
8.1 一个简单的拖放功能示例
第一个例子中,我们有一个QLineEdit和一个QPushButton。我们将从行编辑组件中拖动无格式的文本并将它放到按钮组件上。
# -*- coding: utf-8 -*-
"""拖放功能示例"""
import sys
from PyQt5 import QtWidgets


class Button(QtWidgets.QPushButton):
    def __init__(self, title, parent):
        super(Button, self).__init__(title, parent)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat('text/plain'):
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        self.setText(event.mimeData().text())


class DragDrop(QtWidgets.QDialog):
    def __init__(self):
        super(DragDrop, self).__init__()

        self.setWindowTitle("拖放功能演示程序")
        self.resize(280, 150)
        edit = QtWidgets.QLineEdit("", self)
        edit.move(30, 65)
        edit.setDragEnabled(True)
        button = Button("按钮", self)
        button.move(170, 65)

        screen = QtWidgets.QDesktopWidget().screenGeometry()
        size = self.geometry()
        self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)

app = QtWidgets.QApplication(sys.argv)
dd = DragDrop()
dd.show()
sys.exit(app.exec_())
class Button(QtWidgets.QPushButton):
    def __init__(self, title, parent):
        super(Button, self).__init__(title, parent)
为了将文本放到QPushButton组件上,我们必须写一些方法,所以我们通过继承QPushButton组件来创建我们自己的按钮类。
self.setAcceptDrops(True)
我们允许QPushButton的放置事件(即将其它对象拖放到其上的操作)。
def dragEnterEvent(self, event):
        if event.mimeData().hasFormat('text/plain'):
            event.accept()
        else:
            event.ignore()
首先我们重写dragEnterEvent()方法,我们通知我们将要收到的数据类型,这里是无格式文本。
def dropEvent(self, event):
        self.setText(event.mimeData().text())
通过重写dropEvent()方法,我们定义我们收到drop事件后如何操作,这里我们改变按钮组件显示的文本。
edit = QtWidgets.QLineEdit("", self)
edit.setDragEnabled(True)
QLineEdit组件有内置的拖动操作,我们所要作的就是调用setDragEnabled()方法来将其设置为可用。

                               
登录/注册后可看大图

截图:拖放功能示例
8.2 拖放按钮部件
以下的例子中我们将示范如何拖放一个按钮部件。
# -*- coding: utf-8 -*-
"""拖放按钮示例"""
import sys
from PyQt5 import QtWidgets, QtCore, QtGui


class Button(QtWidgets.QPushButton):
    def __init__(self, title, parent):
        super(Button, self).__init__(title, parent)

    def mouseMoveEvent(self, event):
        if event.buttons() != QtCore.Qt.RightButton:
            return
        mime_data = QtCore.QMimeData()
        drag = QtGui.QDrag(self)
        drag.setMimeData(mime_data)
        drag.setHotSpot(event.pos()-self.rect().topLeft())
        drop_action = drag.exec_(QtCore.Qt.MoveAction)
        if drop_action == QtCore.Qt.MoveAction:
            self.close()

    def mousePressEvent(self, event):
        QtWidgets.QPushButton.mousePressEvent(self, event)
        if event.button() == QtCore.Qt.LeftButton:
            print("按下")


class DragButton(QtWidgets.QDialog):
    def __init__(self):
        super(DragButton, self).__init__()
        self.setWindowTitle("拖放按钮演示程序")
        self.resize(280, 150)
        self.setAcceptDrops(True)
        self.button = Button("关闭", self)
        self.button.move(100, 65)

        screen = QtWidgets.QDesktopWidget().screenGeometry()
        size = self.geometry()
        self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)

    def dragEnterEvent(self, event):
        event.accept()

    def dropEvent(self, event):
        position = event.pos()
        button = Button("关闭", self)
        button.move(position)
        button.show()
        event.setDropAction(QtCore.Qt.MoveAction)
        event.accept()
app = QtWidgets.QApplication(sys.argv)
db = DragButton()
db.show()
sys.exit(app.exec_())
在我们的例子里,主窗口中有一个QPushButton,如果我们鼠标左键点击按钮,会在终端上打印”按下”,如果我们右键点击并拖动按钮,我们对按钮组件执行拖放操作。
class Button(QtWidgets.QPushButton):
    def __init__(self, title, parent):
        super(Button, self).__init__(title, parent)
我们创建一个继承QPushButton的按钮类。我们同时重写QPushButton的两个方法mouseMoveEvent()mousePressEvent()。mouseMoveEvent()方法是拖放操作开始的位置。
if event.button() == QtCore.Qt.LeftButton:
print("按下")
这里我们设定只对鼠标右键的拖放操作做出响应,对鼠标左键操作保留给点击按钮。
mime_data = QtCore.QMimeData()
drag = QtGui.QDrag(self)
drag.setMimeData(mime_data)
drag.setHotSpot(event.pos()-self.rect().topLeft())
这里我们创建一个QDrag对象。
drop_action = drag.exec_(QtCore.Qt.MoveAction)
拖动对象的exec_ ()方法开始拖放操作。如果我们完成一次移动放置操作,我们要销毁按钮组件。技术上来讲,我们在当前位置销毁一个组件,并在新位置重新创建它。
PS:原文中(即PyQt4中), 这里的方法并不是exec_()而是start(),运行起来右键拖动按钮提示:AttributeError:'QDrag' object has no attribute 'start',打开idle,输入
>>> from PyQt5 import QtGui
>>> help(QtGui.QDrag)
在帮助信息中搜索MoveAction(因为这里drag.start()方法的参数是MoveAction类型),找到下面的信息
exec_(...)
    QDrag.exec_(Qt.DropActions supportedActions=Qt.MoveAction) -> Qt.DropAction
    QDrag.exec_(Qt.DropActions, Qt.DropAction) -> Qt.DropAction
start()换为exec_()运行成功。
def mousePressEvent(self, event):
    QtWidgets.QPushButton.mousePressEvent(self, event)
    if event.button() == QtCore.Qt.LeftButton:
        print("按下")
如果我们点击鼠标左键,就在控制台打印'press'。注意,这里我们调用了父类的mousePressEvent()方法,否则我们无法看到鼠标被按下的效果。
友情提示:这一点由其重要,在重写事件方法时,继承父类的方法可以使部件能像以前一样工作,只是在你定义的功能部分替换为了你所写的功能,通常我们都需要这样做,这一点在多个贴子中均有提到。
position = event.pos()
button = Button("关闭", self)
button.move(position)
button.show()
在dropEvent()方法中包含了当我们释放鼠标按键并且结束放置后的操作的代码。在我们的例子里,我们在鼠标指针的当前位置创建了一个新的按钮组件。
event.setDropAction(QtCore.Qt.MoveAction)
event.accept()
我们指定释放操作的类型。这里是移动操作。

                               
登录/注册后可看大图

截图:拖放按钮示例
PS:有几点教程中没有提到的语句,我百度了一下Qt的拖放相关的文章,总结一下:

mime_data = QtCore.QMimeData()
drag = QtGui.QDrag(self)
drag.setMimeData(mime_data)
这段代码其实是定义了Button类的拖放属性,首先定义一个QMimeData类的示例用于存存储在拖放过程中的信息,可以将需要的信息通过QMimeData传到被拖放到的部件处;然后定义了一个QDrag类,并用setMimeData()方法设定其将拖放信息存放在mime_data中。

drag.setHotSpot(event.pos()-self.rect().topLeft())

最后这一句,在本例中其实没用,注释掉之后,程序运行如常,经过百度,这个方法其实是配合下面两条语句使用的:
pixmap = QtGui.QPixmap(r" sample.png")
drag.setPixmap(pixmap)
先创建一个QPixmap类的实例存放一个图片,然后设定拖放过程中在鼠标所在位置显示该图片,然后再调用setHotSpot()方法设定显示图片的位置,参数应该是一个QtCore.QPoint类,比如drag.setHotSpot(QtCore.QPoint(64,64)),设置鼠标在pixmap图片以左上角为(0, 0)时(64,64)的位置。
教程的作者只是给我们展示了两个例子,但是这两个例子可以清楚的看到拖放的细节(当然这些细节的发现要靠自己查资料来发现)。首先,对于拖放操作的接受对象我们需要调用setAcceptDrops()方法设定它可以接受把其它对象拖动到上面的操作;其次,对于拖放操作的接受对象我们需要重写它的dragEnterEvent()和dropEvent()两个方法,来设定当有对象拖放到其上时,需要做出的响应;最后,对于我们要拖放的对象要在它内部描述清楚拖放操作过程中的一些细节(如拖放过程中显示图片,响应鼠标的哪个按键的拖动操作)。

最后,大家可以试一下,分别把这几条语句注释掉,程序会发生什么改变。
1   if event.buttons() != QtCore.Qt.RightButton:
           Return
2   dragEnterEvent函数中的event.accept()
3   dropEvent函数中的event.accept()

评分

参与人数 2荣誉 +15 鱼币 +15 贡献 +3 收起 理由
电脑大师/大兵 + 5 + 5 + 3 鱼C有你更精彩^_^
~风介~ + 10 + 10 感谢楼主无私奉献!

查看全部评分

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

使用道具 举报

 楼主| 发表于 2015-4-20 22:55:01 | 显示全部楼层
本帖最后由 lightninng 于 2015-5-14 00:44 编辑

9 PyQt5中的绘图
当我们想要改变或者加强已经存在的组件时,或者我们通过涂画自定义组件时,需要用的绘图。为了进行绘图,我们要使用PyQt5工具集提供的绘图API。
         绘图通过paintEvent()方法实现。绘图的代码放置在QPainter对象的begin() end()方法之间。
9.1 绘制文本
我们首先将一些Unicode文本绘制在窗口的客户区域。
# -*- coding: utf-8 -*-
"""绘制文本示例"""
import sys
from PyQt5 import QtWidgets, QtGui, QtCore


class DrawText(QtWidgets.QWidget):
    def __init__(self):
        super(DrawText, self).__init__()

        self.setWindowTitle("绘制文本演示程序")
        self.setGeometry(300, 300, 250, 150)

        self.text = "遥远的东方有一条江\n它的名字就叫长江\n遥远的东方有一条河\n它的名字就叫黄河"

    def paintEvent(self, event):
        paint = QtGui.QPainter()
        paint.begin(self)
        paint.setPen(QtGui.QColor(168, 34, 3))
        paint.setFont(QtGui.QFont("STLiti", 20))
        paint.drawText(event.rect(), QtCore.Qt.AlignCenter, self.text)
        paint.end()

app = QtWidgets.QApplication(sys.argv)
dt = DrawText()
dt.show()
sys.exit(app.exec_())
这个例子里,我们用汉语输出了一些文本。文本在水平和垂直方向都是居中的。
def paintEvent(self, event):
通过一个绘图事件实现绘图。
paint = QtGui.QPainter()
paint.begin(self)
...
paint.end()
QPainter类负责所有的底层绘图。所有在begin()和end()方法之间的绘图方法。
PS:这里begin方法必须将参数加上(这里的参数应该是表示你要在哪个部件上绘图),另外,如果你在paint定义时直接将self作为参数传入,则不需要再调用begin方法(否则将报错QPainter::begin: Painter already active),那么这段代码就变成 了这样
paint = QtGui.QPainter(self)
...
paint.end()
当然,你不调用end方法程序也能句跑通,副作用未发现,所以,我的理解是,只有当你需要用同一个QPainter去在不同的部件上绘制图像时,才会调用end方法,然后在新的部件上绘制时在将begin方法的参数设为另一个部件(这段话未验证)。
paint.setPen(QtGui.QColor(168, 34, 3))
paint.setFont(QtGui.QFont("STLiti", 20))
这里我们定义用来输出文字的画笔和字体。
paint.drawText(event.rect(), QtCore.Qt.AlignCenter, self.text)
drawText()方法实际上在窗口绘制文字。

                               
登录/注册后可看大图

截图:绘制文本示例
9.2 绘制散点
点是可被绘制的最简单的图形对象。是窗口的一个小点。
# -*- coding: utf-8 -*-
"""绘制散点示例"""
import sys
from PyQt5 import QtWidgets, QtGui, QtCore


class DrawPoints(QtWidgets.QWidget):
    def __init__(self):
        super(DrawPoints, self).__init__()

        self.setWindowTitle("绘制散点演示程序")
        self.setGeometry(300, 300, 600, 300)

    def paintEvent(self, event):
        import math
        paint = QtGui.QPainter()
        paint.begin(self)
        paint.setPen(QtCore.Qt.red)
        size = self.size()
        for i in range(0, size.width(), 3):
            x = i
            y = math.sqrt(300**2-(x-300)**2)
            paint.drawPoint(x, y)
        paint.end()
app = QtWidgets.QApplication(sys.argv)
dp = DrawPoints()
dp.show()
sys.exit(app.exec_())
这个例子里,我们在窗口中用点绘制了一个半圆形。
paint.setPen(QtCore.Qt.red)
我们设置画笔为红色,预定义的颜色常量。
size = self.size()
每次改变窗口的尺寸,就会生成一个绘图事件。我们通过size() 方法获取当前的窗口尺寸。
paint.drawPoint(x, y)
我们通过drawPoint()方法画点。

                               
登录/注册后可看大图

截图:绘制散点示例
PS:简单说一下,
for i in range(0, size.width(), 3):
    x = i
这个窗口的左上角,坐标为(0, 0),这里的点我们选择x以3为步长(为了能让点看得更清楚),y就是按圆的公式按x的坐标计算的。


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

使用道具 举报

 楼主| 发表于 2015-4-22 13:41:09 | 显示全部楼层
本帖最后由 lightninng 于 2015-4-23 19:18 编辑

9.3 绘图工具QPen
QPen是一个用来画直线,曲线和矩形的轮廓,椭圆,多边形或其他图形的基本绘图对象。
# -*- coding: utf-8 -*-
"""QPen示例"""
import sys
from PyQt5 import QtWidgets, QtGui, QtCore


class Brush(QtWidgets.QWidget):
    def __init__(self):
        super(Brush, self).__init__()

        self.setWindowTitle("QPen演示程序")
        self.setGeometry(300, 300, 280, 270)

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

        pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine)

        paint.setPen(pen)
        paint.drawLine(20, 40, 250, 40)

        pen.setStyle(QtCore.Qt.DashLine)
        paint.setPen(pen)
        paint.drawLine(20, 80, 250, 80)

        pen.setStyle(QtCore.Qt.DashDotLine)
        paint.setPen(pen)
        paint.drawLine(20, 120, 250, 120)

        pen.setStyle(QtCore.Qt.DotLine)
        paint.setPen(pen)
        paint.drawLine(20, 160, 250, 160)

        pen.setStyle(QtCore.Qt.DashDotDotLine)
        paint.setPen(pen)
        paint.drawLine(20, 200, 250, 200)

        pen.setStyle(QtCore.Qt.CustomDashLine)
        pen.setDashPattern([1, 4, 5, 4])
        paint.setPen(pen)
        paint.drawLine(20, 240, 250, 240)

        paint.end()
app = QtWidgets.QApplication(sys.argv)
ps = Brush()
ps.show()
sys.exit(app.exec_())
这个例子里我们绘制六条直线。这些线通过六种不同的笔类型绘制,其中有五种是预定义的笔类型,我们也可以自定义笔类型,最后的一根直线使用了自定义的笔类型。
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine)
我们创建一个Q画笔对象,颜色是黑色,宽度设置为2像素,这样我们可以看到不同的笔类型间的区别。QtCore.Qt.SolidLine是一个预定义的笔类型。
pen.setStyle(QtCore.Qt.CustomDashLine)
         pen.setDashPattern([1, 4, 5, 4])
         paint.setPen(pen)
这里我们定义一个自定义画笔类型。我们设置QtCore.Qt.CustomDashLine画笔类型并且调用setDashPattern()方法。数字列表定义一个类型,必须有偶数个数字,奇数位置定义一个破折号,偶数位置定义一个空白,数字越大,空白或者破折号就越长。我们的例子是1像素的破折,4像素的空白,5像素的破折,4像素的空白。

                               
登录/注册后可看大图

截图:QPen示例


9.4 绘图中的颜色
颜色是通过红,绿,蓝亮度值的组合所表现的对象。正确的RGB值是从0到255,我们可以通过不同的方法来定义一种颜色。最常见的是RGB的十进制或十六进制表示。我们也可以使用包含红,绿,蓝和alpha的RGBA值来表示。这里我们增加一些关于透明度的额外信息,alpha值为255表示完全不透明,0 表示完全透明,比如颜色完全不可见。
# -*- coding: utf-8 -*-
"""绘图中的颜色示例"""
import sys
from PyQt5 import QtWidgets, QtCore, QtGui


class Colors(QtWidgets.QWidget):
    def __init__(self):
        super(Colors, self).__init__()

        self.setWindowTitle("绘图中的颜色演示程序")
        self.setGeometry(300, 300, 350, 300)

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

        paint.setBrush(QtGui.QColor(255, 0, 0, 0))
        paint.drawRect(10, 15, 90, 60)

        paint.setBrush(QtGui.QColor(255, 0, 0, 160))
        paint.drawRect(130, 15, 90, 60)

        paint.setBrush(QtGui.QColor(255, 0, 0, 255))
        paint.drawRect(250, 15, 90, 60)

        paint.setBrush(QtGui.QColor(0, 255, 2, 80))
        paint.drawRect(10, 105, 90, 60)

        paint.setBrush(QtGui.QColor(0, 255, 0, 160))
        paint.drawRect(130, 105, 90, 60)

        paint.setBrush(QtGui.QColor(0, 255, 0, 255))
        paint.drawRect(250, 105, 90, 60)

        paint.setBrush(QtGui.QColor(0, 0, 255, 80))
        paint.drawRect(10, 195, 90, 60)

        paint.setBrush(QtGui.QColor(0, 0, 255, 160))
        paint.drawRect(130, 195, 90, 60)

        paint.setBrush(QtGui.QColor(0, 0, 255, 255))
        paint.drawRect(250, 195, 90, 60)

        paint.end()

app = QtWidgets.QApplication(sys.argv)
c = Colors()
c.show()
sys.exit(app.exec_())
这个例子里,我们画了9个有颜色的长方形。第一行显示了具有不同alpha值的红色。
paint.setBrush(QtGui.QColor(255, 0, 0, 0))
paint.drawRect(10, 15, 90, 60)
这里我们定义刷子并绘制一个矩形。刷子是用来绘制形状背景的基本绘图对象,drawRect()方法接受四个参数,头两个是坐标轴的x,y值。第三个和第四个参数是矩形的宽和高。这个方法使用当前画笔和当前刷子绘制一个矩形。
PS:这里QtGui.QColor(255,0, 0, 255)也可以将参数中的最后一个参数(也就是alpha,透明度)省略,默认值是255,即完全不透明。

                               
登录/注册后可看大图

图:绘图中的颜色示例

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

使用道具 举报

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

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


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

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

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

        size = self.size()
        w = size.width()
        h = size.height()
        cw = self.parent().cw
        step = int(round(w/10.0))

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

        if cw >= 700:
            paint.setPen(QtGui.QColor(255, 255, 255))
            paint.setBrush(QtGui.QColor(255, 255, 184))
            paint.drawRect(0, 0, full, h)
            paint.setPen(QtGui.QColor(255, 175, 175))
            paint.setBrush(QtGui.QColor(255, 175, 175))
            paint.drawRect(full, 0, till-full, h)
        else:
            paint.setPen(QtGui.QColor(255, 255, 255))
            paint.setBrush(QtGui.QColor(255, 255, 184))
            paint.drawRect(0, 0, till, h)

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

        j = 0

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

        paint.end()


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

        self.cw = 75

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

        self.wid = Wight(self)

        h_box = QtWidgets.QHBoxLayout()
        h_box.addWidget(self.wid)
        v_box = QtWidgets.QVBoxLayout()
        v_box.addStretch(1)
        v_box.addLayout(h_box)
        self.setLayout(v_box)

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

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

till = int(((w/750.0)*cw))
full = int(((w/750.0)*700))
我们动态的绘制组件,窗口越大,刻录组件越大。锁定调整,这就是为什么我们必须计算组件的尺寸以便我们绘制自定义的组件。till参数确定绘制的total 尺寸。这个数值从滑块组件取得。是整个区域的比例。full参数确定我们将要用红色绘制的点。这里我们使用了浮点数,以保证精度。
实际的绘制由三步组成。我们绘制黄色或红色和黄色的矩形,然后我们绘制将组件分割成几部分的垂直线,最后,我们绘制标识媒体容量的数字。
metrics = paint.fontMetrics()
fw = metrics.width(str(self.num[j]))
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之前的信号槽)、还有一些不能运行通过的部分进行了修改
# -*- coding: utf-8 -*-
"""俄罗斯方块第一版"""
import sys
import random
from PyQt5 import QtWidgets, QtCore, QtGui


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

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

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

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

        self.tetris_board.start()
        self.center()

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


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

    def __init__(self, parent):
        super(Board, self).__init__(parent)
        self.timer = QtCore.QBasicTimer()
        self.is_waiting_after_line = False
        self.cur_piece = Shape()
        self.next_piece = Shape()
        self.cur_x = 0
        self.cur_y = 0
        self.num_lines_moved = 0
        self.board = []
        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.is_started = False
        self.is_paused = False
        self.clear_board()
        self.next_piece.set_random_shape()

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

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

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

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

    def start(self):
        if self.is_paused:
            return
        self.is_started = True
        self.num_lines_moved = 0
        self.clear_board()
        self.messages_to_satusbar.emit(str(self.num_lines_moved))
        self.new_piece()
        self.timer.start(Board.speed, self)

    def pause(self):
        if not self.is_started:
            return
        self.is_paused = not self.is_paused
        if self.is_paused:
            self.timer.stop()
            self.messages_to_satusbar.emit("paused")
        else:
            self.timer.start(Board.speed, self)
            self.messages_to_satusbar.emit(str(self.num_lines_moved))
        self.update()

    def paintEvent(self, event):
        paint = QtGui.QPainter(self)
        rect = self.contentsRect()
        board_top = rect.bottom() - Board.board_height * self.square_height()
        for i in range(Board.board_height):
            for j in range(Board.board_width):
                shape = self.shape_at(j, Board.board_height - i - 1)
                if shape != Tetrominoes.NoShape:
                    self.draw_square(paint, rect.left() + j*self.square_width(),
                                     board_top + i*self.square_height(), shape)
        if self.cur_piece.shape() != Tetrominoes.NoShape:
            for i in range(4):
                x = self.cur_x + self.cur_piece.x(i)
                y = self.cur_y - self.cur_piece.y(i)
                self.draw_square(paint, rect.left() + x*self.square_width(),
                                 board_top + (Board.board_height-y-1)*self.square_height(), 
                                 self.cur_piece.shape())

    def keyPressEvent(self, event):
        if not self.is_started or self.cur_piece.shape() == Tetrominoes.NoShape:
            QtWidgets.QWidget.keyPressEvent(self, event)
            return
        key = event.key()
        if key == QtCore.Qt.Key_P:
            self.pause()
            return
        if self.is_paused:
            return
        elif key == QtCore.Qt.Key_Left:
            self.try_move(self.cur_piece, self.cur_x - 1, self.cur_y)
        elif key == QtCore.Qt.Key_Right:
            self.try_move(self.cur_piece, self.cur_x + 1, self.cur_y)
        elif key == QtCore.Qt.Key_Down:
            self.try_move(self.cur_piece.rotated_right(), self.cur_x, self.cur_y)
        elif key == QtCore.Qt.Key_Up:
            self.try_move(self.cur_piece.rotated_left(), self.cur_x, self.cur_y)
        elif key == QtCore.Qt.Key_Space:
            self.drop_down()
        elif key == QtCore.Qt.Key_D:
            self.one_line_down()
        else:
            QtWidgets.QWidget.keyPressEvent(self, event)

    def timerEvent(self, event):
        if event.timerId() == self.timer.timerId():
            if self.is_waiting_after_line:
                self.is_waiting_after_line = False
                self.new_piece()
            else:
                self.one_line_down()
        else:
            QtWidgets.QFrame.timerEvent(self, event)

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

    def drop_down(self):
        new_y = self.cur_y
        while new_y > 0:
            if not self.try_move(self.cur_piece, self.cur_x, new_y - 1):
                break
            new_y -= 1
        self.piece_dropped()

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

    def piece_dropped(self):
        for i in range(4):
            x = self.cur_x + self.cur_piece.x(i)
            y = self.cur_y - self.cur_piece.y(i)
            self.set_shape_at(x, y, self.cur_piece.shape())
        self.remove_full_lines()
        if not self.is_waiting_after_line:
            self.new_piece()

    def remove_full_lines(self):
        num_full_lines = 0
        rows_to_remove = []
        for i in range(Board.board_height):
            n = 0
            for j in range(Board.board_width):
                if not self.shape_at(j, i) == Tetrominoes.NoShape:
                    n += 1
            if n == 10:
                rows_to_remove.append(i)
        rows_to_remove.reverse()
        for m in rows_to_remove:
            for k in range(m, Board.board_height):
                for l in range(Board.board_width):
                    self.set_shape_at(l, k, self.shape_at(l, k + 1))
        num_full_lines += len(rows_to_remove)
        if num_full_lines > 0:
            self.num_lines_moved += num_full_lines
            self.messages_to_satusbar.emit(str(self.num_lines_moved))
            self.is_waiting_after_line = True
            self.cur_piece.set_shape(Tetrominoes.NoShape)
            self.update()

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

    def try_move(self, new_piece, new_x, new_y):
        for i in range(4):
            x = new_x + new_piece.x(i)
            y = new_y - new_piece.y(i)
            if x < 0 or x >= Board.board_width or y < 0 or y >= Board.board_height:
                return False
            if self.shape_at(x, y) != Tetrominoes.NoShape:
                return False
        self.cur_piece = new_piece
        self.cur_x = new_x
        self.cur_y = new_y
        self.update()
        return True

    def draw_square(self, painter, x, y, shape):
        color_table = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC, 0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
        color = QtGui.QColor(color_table[shape])
        painter.fillRect(x + 1, y + 1, self.square_width() - 2, self.square_height() - 2, color)
        painter.setPen(color.lighter())
        painter.drawLine(x, y + self.square_height() - 1, x, y)
        painter.drawLine(x, y, x + self.square_width() - 1, y)
        painter.setPen(color.darker())
        painter.drawLine(x + 1, y + self.square_height() - 1, 
                         x + self.square_width() - 1, y + self.square_height() - 1)
        painter.drawLine(x + self.square_width() - 1, y + self.square_height() - 1, 
                         x + self.square_width() - 1, y + 1)

class Tetrominoes(object):
    NoShape = 0
    ZShape = 1
    SShape = 2
    LineShape = 3
    TShape = 4
    SquareShape = 5
    LShape = 6
    MirroredLShape = 7


class Shape(object):
    coords_table = (((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.piece_shape = Tetrominoes.NoShape
        self.set_shape(Tetrominoes.NoShape)

    def shape(self):
        return self.piece_shape

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

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

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

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

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

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

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

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

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

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

    def rotated_left(self):
        if self.piece_shape == Tetrominoes.SquareShape:
            return self
        result = Shape()
        result.piece_shape = self.piece_shape
        for i in range(4):
            result.set_x(i, self.y(i))
            result.set_y(i, -self.x(i))
        return result

    def rotated_right(self):
        if self.piece_shape == Tetrominoes.SquareShape:
            return self
        result = Shape()
        result.piece_shape = self.piece_shape
        for i in range(4):
            result.set_x(i, -self.y(i))
            result.set_y(i, self.x(i))
        return result

app = QtWidgets.QApplication(sys.argv)
tetris = Tetris()
tetris.show()
sys.exit(app.exec_())
首先,说说修改一个小bug和关于信号槽的部分之前没有涉及到的内容:
1、Board类中的shape_at和set_shape_at方法中,对self.board进行了索引取值操作,但是给出的索引是用一个表达式,这个表达式计算出来的值是float型导致报错,观察一下,式中的变量均为int型,于是用int()函数强行把索引值转化为int型,原代码和改后代码如下
def shape_at(self, x, y):
        return self.board[(y*Board.board_width) + x]
def shape_at(self, x, y):
        return self.board[int((y*Board.board_width) + x)]
2、关于PyQt5中的信号槽问题在前面的章节中我已经作过介绍,现在说一下信号槽的另外一个问题:怎么给槽函数传递参数?
其实主要就是3点:
(1)在创建信号变量时,按槽函数的传入参数顺序,将对应参数的类型传入QtCore.pyqtSignal(),渣语文见谅,不懂的请看程序中的做法:
class Board(QtWidgets.QFrame):
    board_width = 10
    board_height = 22
    speed = 300
    messages_to_satusbar = QtCore.pyqtSignal(str)
可以看到最后一句中我们指定要传入的参数为一个str类型的变量
(2)在用emit()方法发射信号时,将要传入槽函数的参数作为emit方法的参数传入,如这个程序中
        if not self.try_move(self.cur_piece, self.cur_x, self.cur_y):
            self.cur_piece.set_shape(Tetrominoes.NoShape)
            self.timer.stop()
            self.is_started = False
            self.messages_to_satusbar.emit("游戏结束")
当符合某种条件时,要在状态栏显示"游戏结束 ",将"游戏结束"这个字符串传入信号message_to_statusbar所对应的槽函数
(3)定义信号时定义的、信号发谢时传入的、以及槽函数定义中的变量类型,数量和顺序,必须一致,如这个程序中
self.tetris_board.messages_to_satusbar.connect(self.status_bar.showMessage)
这里的showMessage是QStatusBar自带的方法,我们看看它的这个方法的定义
showMessage(...)
      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'键暂停游戏。空格键将使俄罗斯方块立刻落到底部。游戏使用固定的速度,没有实现加速。游戏的分数是我们已经消掉的行数。
        self.status_bar = self.statusBar()
        self.setCentralWidget(self.tetris_board)
        self.tetris_board.messages_to_satusbar.connect(self.status_bar.showMessage)
我们创建一个状态栏用来显示信息。我们将显示三种可能的信息,已经消掉的行数,暂停的消息和游戏结束的消息。
        self.cur_x = 0
        self.cur_y = 0
        self.num_lines_moved = 0
        self.board = []
在我们开始游戏之前,我们初始化一些重要的变量。self.board变量四从0到7的数字列表。它表示不同的图形的位置和面板上剩余的图形。
for j in range(Board.board_width):
                shape = self.shape_at(j, Board.board_height - i - 1)
                if shape != Tetrominoes.NoShape:
                    self.draw_square(paint, rect.left() + j*self.square_width(),
                                     board_top + i*self.square_height(), shape)
游戏的显示分成两步。第一步,我们绘制所有的图形,或已经掉落在底部的剩余的图形。所有的方块被保存在self.board列表变量中。我们通过使用shape_at()方法来访问它。
if self.cur_piece.shape() != Tetrominoes.NoShape:
            for i in range(4):
                x = self.cur_x + self.cur_piece.x(i)
                y = self.cur_y + self.cur_piece.y(i)
                self.draw_square(paint, rect.left() + x*self.square_width(),
                                 board_top + (Board.board_height-y-1)*self.square_height(), self.cur_piece.shape())
下一步是绘制正在掉落的当前块。
        elif key == QtCore.Qt.Key_Left:
            self.try_move(self.cur_piece, self.cur_x - 1, self.cur_y)
        elif key == QtCore.Qt.Key_Right:
            self.try_move(self.cur_piece, self.cur_x + 1, self.cur_y)
在keyPressEvent我们检查按下的按键。如果我们按下了右方向键,我们就试着向右移动块。试着是因为块可能无法移动。
    def try_move(self, new_piece, new_x, new_y):
        for i in range(4):
            x = new_x + new_piece.x(i)
            y = new_y - new_piece.y(i)
            if x < 0 or x >= Board.board_width or y < 0 or y >= Board.board_height:
                return False
            if self.shape_at(x, y) != Tetrominoes.NoShape:
                return False
        self.cur_piece = new_piece
        self.cur_x = new_x
        self.cur_y = new_y
        self.update()
        return True
在try_move()方法中,我们尽力来移动我们的块,如果块在背板的边缘或者靠在其他的块上,我们返回假,否则我们将当前块放置在新的位置。
    def timerEvent(self, event):
        if event.timerId() == self.timer.timerId():
            if self.is_waiting_after_line:
                self.is_waiting_after_line = False
                self.new_piece()
            else:
                self.one_line_down()
        else:
            QtWidgets.QFrame.timerEvent(self, event)
时间事件中,我们或者在上一个方块到达底部后创建一个新方块,或者将下落的方块向下移动一行。
    def remove_full_lines(self):
        num_full_lines = 0
        rows_to_remove = []
        for i in range(Board.board_height):
            n = 0
            for j in range(Board.board_width):
                if not self.shape_at(j, i) == Tetrominoes.NoShape:
                    n += 1
            if n == 10:
                rows_to_remove.append(i)
        rows_to_remove.reverse()
        ......
如果方块到达了底部,我们调用removeFullLines()方法。首先我们找出所有的满行,然后我们移去他们,通过向下移动当前添满的行上的所有行来完成。注意,我们反转将要消去的行的顺序,否则它会工作不正常。这种情况我们使用简单的引力,这意味着块会浮动在缺口上面。
    def new_piece(self):
        self.cur_piece = self.next_piece
        self.next_piece.set_random_shape()
        self.cur_x = Board.board_width / 2 + 1
        self.cur_y = Board.board_height - 1 + self.cur_piece.min_y()
        if not self.try_move(self.cur_piece, self.cur_x, self.cur_y):
            self.cur_piece.set_shape(Tetrominoes.NoShape)
            self.timer.stop()
            self.is_started = False
            self.messages_to_satusbar.emit("游戏结束")
newPiece()方法随机生成一个新的俄罗斯方块。如果方块无法进入它的初始位置,游戏结束。
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方法中第一行加入这样的代码:
if shape == Tetrominoes.NoShape:
            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:03:50 | 显示全部楼层
lightninng 发表于 2015-5-1 11:14
我们对游戏作一些简化,以便于理解。游戏在启动后立刻开始。我们可以通过按'p'键暂停游戏。空格键将使俄罗 ...

这是我见过的最好的PyQt5教程,没有之一!
想知道小甲鱼最近在做啥?请访问 -> 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 12:38:14 | 显示全部楼层
lightninng 发表于 2015-5-12 12:06
主要内容都是人家的,自己加了些学习过程中遇到的问题而已
还是谢谢夸奖~~

感觉我个人没有写教程的耐心~
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

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

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

使用道具 举报

发表于 2015-5-12 19:26:13 | 显示全部楼层
lightninng 发表于 2015-5-12 17:53
哈哈,那也不一定,性格也是会变的

过个一两百年也许会变!
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

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

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

使用道具 举报

发表于 2015-5-15 08:38:06 | 显示全部楼层
lightninng 发表于 2015-4-1 23:59
2初次使用PyQt5工具包编程         这一部分我们将学习一些基本的功能。这里的讲解很详细,就像我们和孩子 ...

而且,图标必须是ico文件,貌似不是随便找个图片改扩展名就行
想知道小甲鱼最近在做啥?请访问 -> 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 11:34:45 | 显示全部楼层
lightninng 发表于 2015-5-15 09:15
windows中的图标好像确实得要ico,不过PyQt中亲测png文件可用,似乎是需要有透明图层的文件类型~~

jpg ...

在designer里做了ui,到Eric里编译成py文件了,然后再py里修改代码,似乎不能同步到ui文件里?比如py修改一个标题,如果不能的化岂不是所有的ui设计都要在designer里,py只是做后台?求扫盲
想知道小甲鱼最近在做啥?请访问 -> 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教程中的俄罗斯方块
下面我们就教程中未解释的部分来说一下。
class Tetris(QtWidgets.QMainWindow):
    def __init__(self):
        super(Tetris, self).__init__()
我们先来看界面的部分,像往常以样我们先选择了QmainWindow类做为我们的基类设计了游戏的大框架Tetris
class Board(QtWidgets.QFrame):
        …
然后用Qframe类作为基类设计了游戏显示的面板Board类
        self.tetris_board = Board(self)
        self.setCentralWidget(self.tetris_board)
然后将Board类的tetris_boardsetCentralWidget方法设置它为tetris的中间部件,tetris_board的大小由tetris决定。
self.timer = QtCore.QBasicTimer()
在tetris_board中我们设置一个记时器,它负责按指定的时间间隔触发timerEvent,执行当前块落下一行,刷新界面这样的操作。
        self.cur_piece = Shape()
        self.next_piece = Shape()
        self.cur_x = 0
        self.cur_y = 0
        self.num_lines_moved = 0
        self.board = []
cur_piece 和next_piece分别为当前块和下一个块,cur_x 和cur_y则记录当前块的位置,num_lines_moved记录已经消去的行数,board记录整个游戏区域内哪些格子有方块(以及它们的类型),当然这里的记录是不包含cur_piece,所以在绘制游戏画面时要同时将board记录的内容和cur_piece绘制到画面上
board_width = 10
board_height = 22
speed = 300
…
    def square_width(self):
        return self.contentsRect().width() / Board.board_width

    def square_height(self):
        return self.contentsRect().height() / Board.board_height
这段代码中,board_width和board_height是游戏的横向方格的数目和纵向方格数目,然后根据整个框体的大小,用square_width和square_height方法计算每个格子的长和宽。
def drop_down(self):
      …
    def one_line_down(self):
       …
    def piece_dropped(self):
        …
这三个方法中:drop_down负责直接将当前块落到底部,它不停的尝试是否可以往下落一行,直到尝试失败调用piece_dropped方法,在每往下落一行时并不重新绘制,所以你看到的是它直接落到底部,;one_line_down负责将当前块往下落一行,如果尝试失败则调用piece_dropped方法;piece_dropped当调用到它时,表示当前块已经无法再移动了,将其放到board中,并调用new_piece生成新的cur_piece
 def paintEvent(self, event):
        …
    def draw_square(self, painter, x, y, shape):
        …
这两个方法负责对图像的绘制,心思比较细鱼油可能会发现timerEvent中并未直接调用paintEvent对画面进行更新,那我们来看看timerEvent调用的几个方法中是否有更新画面的操作,从timerEvent -> one_line_down -> try_move一路调用找下来,在try_move方法中发现了这样的语句
def try_move(self, new_piece, new_x, new_y):
        …
        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()方法。
class Shape(object):
    coords_table = (…)
       …
游戏中代表块的类,也就是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 背景图片
运行过教程代码的鱼油应该都会发现,教程完成的这个俄罗斯方块小游戏中,游戏面板中的方块大小是随着窗体的大小变化的,那么如果我们给它加上背景图片,就要求背景图片能跟随部件大小同时进行拉伸和缩放,如果仅仅简单的填充一个背景,那么背景图太小会用平铺的方式充满窗口,背景图太大只会显示左上角的部分。

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

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

    def resizeEvent(self, event):
        # 重写resizeEvent, 使背景图案可以根据窗口大小改变
        QWidget.resizeEvent(self, event)
        palette = QPalette()
        palette.setBrush(QPalette.Window, QBrush(self.background.scaled(event.size())))
        self.setPalette(palette)
在给一个QWidget部件加上背景,这里用的是setPalette方法,看过WeiY小哥教程的朋友应该知道,用部件的setStyleSheet方法可以完成对背景图版的设置。
        self.background = QImage(r":source\ground.png")
先将背景图片读取出来。
        self.setAutoFillBackground(True)
官方文档中关于autoFillBackground中有这么一句:如果启用该属性,Qt将在调用paintEvent前填充部件的背景。所以,若想设置背景图案这一句是必须的,另外需要注意的是当它与style sheet同时使用时,若style sheet已有一个有效的backgroundborder-image,这个属性将被禁用,也就是说当你使用了setStyleSheet设置了backgroundborder-image属性时,就无法应用setPalette方法来设置背景了。
    def resizeEvent(self, event):
        # 重写resizeEvent, 使背景图案可以根据窗口大小改变
        QWidget.resizeEvent(self, event)
由于我们要求窗体大小变化时,背景能随之进行变化,所以我们需要重新实现resizeEvent属性。这里必须先重载QWidget部件的resizeEvent,以提经常提到的操作。
        palette = QPalette()
        palette.setBrush(QPalette.Window, QBrush(self.background.scaled(event.size())))
创建一个QPalette的实例,并设定它的绘图模式,当设置窗口背景时setBrush方法的第一个参数应为QtGui.QPalette.Window(这个参数的其它选项请看官方文档的QPalette类的介绍),第二个参数设为QBrush类,并载入图片,这里,我们调用QImage的scaled方法,将窗体更改后的大小(event.size())传入,得到了适应窗口大小的图片。另外,如果大家想直接在背景上绘制一整个颜色块,只需要用palette.setColor方法,如
palette.setColor(QtGui.QPalette.Window,QtGui.QColor(0, 0, 0)
        self.setPalette(palette)
最后我们将适应窗口大小的背景图绘制到部件上。


请到52楼查看后续章节


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

使用道具 举报

发表于 2015-5-25 17:22:30 | 显示全部楼层
下次加点注释呗。。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2015-5-25 19:09:35 | 显示全部楼层
楼楼的帖子非常棒
现在网上很多教程是Qt4的, 很多函数名,变量名和Qt5都不太一样, 初学者就会很困扰.
这里代码打上去都可以直接运行, 非常感动, 教程写的也很棒
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-1-12 19:01

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

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