书名:PyQt编程快速上手——Python GUI开发从入门到实践
ISBN:978-7-115-60866-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 任路顺
责任编辑 胡俊英
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e60866”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。
PyQt是一个创建GUI应用程序的工具包,是Python编程语言和Qt库的成功融合。本书旨在通过深入浅出的讲解和简明的程序示例教读者掌握PyQt的开发技巧。
本书分为10章,从PyQt的安装和基础知识讲起,陆续介绍了基础控件(如标签控件、消息框、文本框、按钮等)、高级控件(如组合框、滚动条、容器控件及各类视图等)、窗口(如属性、坐标、事件等)、Qt Designer(如安装与配置、编辑模式等)、PyQt高级应用(如数据库、多线程、动画、音视频、网页交互等)、图形视图框架(如图元、场景、视图、事件传递等)、打包(如PyInstaller、Nuitka等)等内容。此外,本书还通过两个开发实例(可视化爬虫软件和《经典贪吃蛇》游戏)带领读者巩固了书中介绍的相关知识点。
本书内容简洁实用、实操性强,适合对Python编程及GUI开发感兴趣的读者阅读。本书有配套的读者交流群(QQ群:747114397),为大家答疑解惑。
如果说要快速开发一个桌面程序,我首先想到的开发工具就是Python + PyQt,理由很明确:开发速度快、功能强大而且界面美观。PyQt提供了丰富的类和函数,能够让我们快速实现各种各样的功能。它是一个跨平台的工具包,几乎可以运行在所有主流的操作系统上,包括Windows、Linux和macOS。PyQt不仅拥有Qt的强大功能,而且在开发速度上至少比用Qt开发快一倍。PyQt绝对是GUI桌面程序开发的一件“神器”!
我是在2017年首次接触PyQt的,当时觉得开发桌面程序非常有意思,而且能够给自己做一些小工具,成就感满满。之后我开始系统地查阅并学习有关PyQt的知识,当时的资料还是非常少的,所以学习之路异常艰辛。2019年,我决定开始在博客上写一些关于PyQt的文章,分享我对PyQt的理解。在写这些文章的同时,我对PyQt的理解更加深入,使用它也更加顺手。之后博客的浏览量逐渐增加,文章得到了很多读者的反馈与肯定,这让我对PyQt的感情更深了,我开始更加坚定地使用和推广PyQt。
我会在本书中分享自己所知道的有关PyQt的知识和经验,让读者能够快速入门PyQt并且掌握其开发技巧,也希望在看本书的你能够和我一样领略PyQt的魅力!学完并掌握本书知识点后,你就能上手开发各式各样的桌面程序了。
本书共10章,章节内容和顺序经过精心设计,力求能让读者循序渐进地掌握PyQt开发的基础知识和技巧。各章内容概要如下。
第1章介绍PyQt的安装方法,解释PyQt的程序入口代码,并对布局管理器、信号和槽机制进行详细的讲解。另外还会教大家如何使用在线文档。
第2章介绍在编写PyQt程序时经常会用到的基础控件,针对每一个控件,结合实例进行讲解。
第3章介绍PyQt中的高级控件,让大家进一步了解PyQt,从而编写出功能强大的桌面程序。
第4章深入介绍窗口的各种属性和事件函数的用法,还会介绍主窗口类QMainWindow的用法。
第5章介绍使用Qt Designer快速设计界面。
第6章涉及许多高级功能,例如在PyQt中使用数据库,编写多线程代码来处理复杂耗时的程序逻辑,以及使用QSS来美化自己设计的程序界面等。
第7章介绍图形视图框架的基础知识及其用法。
第8章介绍PyInstaller和Nuitka的实战打包技巧,带领读者一起解决常见的打包问题。
第9章介绍用PyQt开发一款可视化爬虫软件,详细介绍如何将界面和爬虫代码结合起来。
第10章介绍用PyQt开发一款《经典贪吃蛇》游戏,帮助读者巩固学到的图形视图框架知识。
读者在阅读书中代码时,会看到一些带有编号的注释,笔者会在代码解释部分讲解这些注释所指向的代码行或代码片段。下面举一个例子。
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.label = QLabel('你好世界!')
self.btn = QPushButton('改变文本')
self.btn.clicked.connect(self.change_text) # 1
v_layout = QVBoxLayout() #注释2开始
v_layout.addWidget(self.label)
v_layout.addWidget(self.btn)
self.setLayout(v_layout) #注释2结束
def change_text(self): # 3
print('槽函数启动')
self.label.setText('你好PyQt!')
代码解释:
#1 笔者会在这里解释注释#1所指向的代码行。
#2 笔者会在这里解释注释#2所指向的代码片段。
#3 笔者会在这里解释注释#3所指向的函数,就是解释函数中的代码。
除了上面这一约定外,本书第2章开始会默认省略导入代码和程序启动代码,除非特意写出。这样做是为了防止代码冗余。了解这两点代码阅读约定会更有助于读者理解本书内容。
书中所有的示例代码都已放入资源包中,在开始阅读前读者可以先从异步社区网站把代码下载下来。
由于笔者水平有限,书中难免存在一些不妥之处,恳请广大读者批评指正,读者可以直接发邮件与笔者联系,邮箱是louasure@126.com。
另外,关于本书还设有专门的读者交流QQ群(群号:747114397),欢迎各位读者加入!笔者在CSDN博客(昵称“la_vie_est_belle”)上也会更新与PyQt相关的文章,欢迎读者访问交流。
首先感谢我的爸爸妈妈,没有你们的栽培,我是不会前进到这一步的。接着,要感谢美丽可爱的方玲,谢谢你的陪伴和支持。最后,感谢自己,想对自己说:“谢谢你的坚持,未来一定会更好。”
欢迎同笔者一起来体验这场有意思的PyQt之旅。希望读完本书的你在编程能力上能够更上一层楼,更有把握地将自己脑海中的想法变成现实。PyQt,你值得拥有!
任路顺
2023年1月
本书由异步社区出品,社区(https://www.epubit.com)为您提供后续服务。
您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e60866”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。
本书为读者提供源代码。读者可在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者、译者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“发表勘误”,输入错误信息,单击“提交勘误”按钮即可,如下图所示。本书的作者和编辑会对您提交的错误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区投稿(直接访问www.epubit.com/contribute即可)。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。
异步社区
微信服务号
在本章,我们首先会学习如何在相应系统上安装PyQt,接着会深入学习编写每一个PyQt窗口所必需的程序入口代码,还会学习频繁用到的布局管理器以及信号和槽的知识点。布局、信号和槽是编写每个PyQt窗口所需要用到的基础知识,也是PyQt的核心内容。只有理解了它们才能够编写出功能更好、更丰富的程序。
之后,我们还会了解如何使用文档,以便快速找到相应方法的解释。读者在明白如何使用文档之后,说不定就可以快捷上手PyQt编程了。现在就让我们一起进入PyQt的世界吧!
PyQt的安装步骤非常简单。在本节中,笔者会介绍如何在Windows、macOS以及Ubuntu这3个系统上安装PyQt。
本书所使用的Python的版本为3.9.11,PyQt的版本为5.15.4,并且会使用PyCharm编辑器编写PyQt程序。另外,本书不会介绍Python的基础语法知识,读者如果对Python基础语法还不够熟悉的话,可以先去知乎(昵称“la vie”)或者B站(昵称“快乐的代码蛋”)上观看笔者录制的Python教学视频,其中包括如何下载PyCharm编辑器的教程。
在Windows系统上,我们可以直接使用“pip install pyqt5= =5.15.4”命令安装PyQt,如图1-1所示。
图1-1 在Windows系统上安装PyQt
如果安装速度太慢或出现ReadTimeoutError错误,可以转为从国内源安装,将上述命令修改如下。
pip install pyqt5==5.15.4 -i https://pypi.tuna.tsinghua.edu.cn/simple
在macOS系统上,我们同样可以使用 pip 命令安装PyQt,在终端中执行“pip3 install pyqt5= =5.15.4”命令即可,如图1-2所示。
图1-2 在macOS系统上安装PyQt
在Ubuntu系统上,我们可以使用“sudo apt-get install python3-pyqt5”命令安装PyQt,如图1-3所示。
图1-3 在Ubuntu系统上安装PyQt
PyQt安装完毕后,我们在Python命令提示符窗口中执行“import PyQt5”命令(注意大小写)来导入PyQt 5库,如果没有出现任何错误提示,则表明安装成功,如图1-4所示。
图1-4 验证安装
麻雀虽小,五脏俱全。本节介绍的窗口程序虽然简单,但能够让我们了解很多知识点,给后续的PyQt程序编写打下良好的基础。
通过示例代码1-1我们能创建出一个非常简单的PyQt窗口,而这段代码就是常见的PyQt程序入口。
示例代码1-1
import sys
from PyQt5.QtWidgets import *
if __name__ == '__main__':
app = QApplication([]) # 1
label = QLabel('Hello, PyQt!') # 2
label.show() # 3
sys.exit(app.exec()) # 4
运行结果如图1-5所示。
图1-5 简单的PyQt窗口
代码解释:
#1 通过app = QApplication([])语句实例化一个QApplication对象,该对象的作用是接收一个列表类型的值,其实就是用来接收命令行参数的。由于该程序不会与命令行“打交道”,所以直接传入空列表[]即可。如果程序需要接收命令行参数,则可以传入sys.argv。
#2 通过label = QLabel('Hello, PyQt!')语句实例化一个QLabel控件,我们通常用它来显示文本或图片。在这行代码中,我们用它来显示文本。在实例化QLabel控件时,可以直接传入文本,也可以先实例化,再调用setText()方法来设置,代码如下所示。
label= QLabel()
label.setText('Hello, PyQt!')
#3 因为控件默认都是隐藏的,所以要调用show()方法将其显示出来。
#4 通过app.exec()可以让PyQt程序运行起来,而当用户正常关闭窗口时,app.exec()会返回数值0,将其传给sys.exit(),从而让Python解释器正常退出。
在本小节中,如果碰到不理解的地方完全没有关系,先记住可以理解的部分,往下慢慢看,懂的就会越来越多了。比如先记住QLabel控件的用法,知道这个控件是干什么的,以及如何使用它的setText()方法等。
在PyQt中,一个控件可以看作一个窗口。
读者可能发现app对象还有exec_()方法,那是因为在Python 2中exec是关键字,所以为了不引起冲突,PyQt官方起初就编写了带下画线的exec_()。不过exec在Python 3中已不再是关键字,所以直接调用exec()不会有任何问题。
我们可以直接在字符串中加上HTML代码来修改文本样式,示例代码1-2通过HTML的<h1>标签修改文本大小。
示例代码1-2
import sys
from PyQt5.QtWidgets import *
if __name__ == '__main__':
app = QApplication([])
label = QLabel()
label.setText('<h1>Hello, PyQt!</h1>')
label.show()
sys.exit(app.exec())
运行结果如图1-6所示。
图1-6 加入HTML代码1
也可以在读取HTML文件后,调用setText()方法将其内容显示到窗口上。新建一个名为test.html的文件,并在其中输入以下内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello, PyQt!</title>
</head>
<body>
<h1>I love PyQt.</h1>
</body>
</html>
现在我们在示例代码1-2的基础上进行修改,将文件中的HTML代码内容显示到窗口上,详见示例代码1-3。
示例代码1-3
import sys
from PyQt5.QtWidgets import *
if __name__ == '__main__':
app = QApplication([])
label = QLabel()
with open('test.html', 'r', encoding='utf-8') as f:
label.setText(f.read())
label.show()
sys.exit(app.exec())
运行结果如图1-7所示。
图1-7 加入HTML代码2
除了可以使用HTML代码来修改文本样式,我们也可以通过PyQt自身提供的类(比如QFont)来实现相同的效果。当然,还可以使用QSS(Qt Style Sheets,Qt样式表)来实现。这两种方法在后续章节都会讲解到。
PyQt程序的代码不应该全部挤在程序入口处,因为我们要用到的控件可能不止一个。如果在程序入口处实例化多个控件对象,就会出现多个窗口,详见示例代码1-4。
示例代码1-4
import sys
from PyQt5.QtWidgets import *
if __name__ == '__main__':
app = QApplication([])
label_1 = QLabel('Label 1')
label_2 = QLabel('Label 2')
label_1.show()
label_2.show()
sys.exit(app.exec())
运行结果如图1-8所示。
图1-8 多个窗口
很明显,这不是我们想要的。我们希望能够在一个窗口上同时显示两个QLabel控件。为了实现这种需求,也为了让代码更好管理,我们通常会在一个类中编写窗口,详见示例代码1-5。
示例代码1-5
import sys
from PyQt5.QtWidgets import *
class Window(QWidget): # 1
def __init__(self):
super(Window, self).__init__()
label_1 = QLabel('Label 1', self) #注释2开始
label_2 = QLabel('Label 2', self) #注释2结束
if __name__ == '__main__':
app = QApplication([])
window = Window() #注释3开始
window.show() #注释3结束
sys.exit(app.exec())
代码解释:
#1 让编写的Window类继承QWidget。大家可以把QWidget看成一个空白的窗口,而我们要做的就是往里面添加控件。
#2 在Window类中,我们实例化两个QLabel控件对象。在该程序中,QLabel除了接收一个字符串,还指明了一个父类实例对象self,这样QLabel控件就能够显示在窗口上。
#3 在程序入口处,我们实例化Window对象,并调用show()方法将窗口显示出来。
当对一个窗口调用show()方法之后,窗口中的控件会一同显示出来,不必再调用自身的show()方法。
运行结果如图1-9所示。
图1-9 在类中编写窗口
图1-9所示的窗口中同时出现了“Label 1”和“Label 2”两个文本,但它们是重合的,原因在于我们没有对两个QLabel控件进行布局。在1.3节,我们将会学习使用布局管理器来让窗口更加整洁有序。
PyQt窗口就像一间房子,我们会往房子里面放各种家具,而在窗口中,我们会放很多的控件。在布局时,我们不会把家具乱放,不过要怎么放才更加好看、有序呢?接下来就让我们一起来看一下如何布局窗口上的各个控件。
最简单的布局方式就是使用move()方法来规定各个控件在窗口中的位置。在使用这个方法前,我们先来简单了解一下PyQt的坐标体系,如图1-10所示。
图1-10 PyQt的坐标体系
不管是窗口还是控件,它们的坐标原点(以及锚点)都在左上角,而且向右为x轴正方向,向下为y轴正方向。也就是说,如果我们要调用move()方法把一个QLabel控件放在坐标为(50, 100)的窗口位置上,其实就是规定QLabel控件左上角在窗口上的位置,如图1-11所示。
图1-11 QLabel控件在(50, 100)坐标处
我们现在可以实际操作一下,示例代码1-6通过move()方法把QLabel放在了窗口上的其他位置。
示例代码1-6
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(200, 200) # 1
label_1 = QLabel('Label 1', self)
label_2 = QLabel('Label 2', self)
label_1.move(-20, 0) #注释2开始
label_2.move(50, 100) #注释2结束
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-12所示。
图1-12 QLabel控件的位置
代码解释:
#1 调用resize()方法将窗口大小设置为宽200像素,长200像素。
#2 调用move()方法分别设置两个QLabel控件的位置。这里将label_1控件对象设置在了坐标为(−20, 0)的位置上。之前说过,窗口的左上角为坐标原点(0, 0),QLabel控件的坐标为(−20, 0)也就意味着控件还往原点左侧移动了一些,这样的话一部分文本就会被遮住。
使用move()方法可以快速进行布局,但是当控件数量很多时,该方法就不再方便了。因为我们要计算很多个坐标,而且万一其中一个控件的位置要改变,就可能会影响其他所有控件的位置,牵一发而动全身的方法是不推荐的。另外,使用move()方法还有一个弊端——坐标都是固定的。也就是说,当我们拉伸窗口时,控件的位置固定不变,并不能够自适应。为了解决这些问题,我们就需要用到PyQt中的布局管理器。
所谓垂直布局(Vertical Layout),就是指将控件从上到下垂直进行摆放,我们可以用QVBoxLayout这个布局管理器来实现,如图1-13所示。
图1-13 垂直布局
现在我们在程序中垂直布局两个QLabel控件,详见示例代码1-7。
示例代码1-7
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
username = QLabel('Username:') #注释1开始
password = QLabel('Password:') #注释1结束
v_layout = QVBoxLayout() #注释2开始
v_layout.addWidget(username)
v_layout.addWidget(password)
self.setLayout(v_layout) #注释2结束
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-14所示。
图1-14 控件垂直布局
代码解释:
#1 实例化两个QLabel控件,将其文本设置为“Username:”和“Password:”。
#2 实例化一个垂直布局管理器,并通过addWidget()方法将两个控件依次添加到布局中。接着调用窗口的setLayout()方法将垂直布局方式设置为窗口的整体布局。
在垂直布局中,先添加的控件位于后添加的控件上方。
布局管理器被设置到窗口上,被添加到布局管理器中的各个控件也会自然而然地显示到窗口上,也就是我们可以不必再在实例化的时候给这些控件指定父类实例对象self。
水平布局(Horizontal Layout)就是指将控件从左到右依次摆放,控件都是水平对齐的,我们可以用QHBoxLayout这个布局管理器来实现,如图1-15所示。
图1-15 水平布局
现在我们在程序中水平布局一个QLabel控件和一个QLineEdit控件,详见示例代码1-8。
示例代码1-8
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
username_label = QLabel('Username:') #注释1开始
username_line = QLineEdit() #注释1结束
h_layout = QHBoxLayout() #注释2开始
h_layout.addWidget(username_label)
h_layout.addWidget(username_line)
self.setLayout(h_layout) #注释2结束
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-16所示。
图1-16 控件水平布局
代码解释:
#1 除了QLabel控件,我们还添加了QLineEdit控件,它是一个单行文本输入框,在这里用于输入账号。
#2 实例化一个水平布局管理器并调用addWidget()方法将QLabel控件和QLineEdit控件添加到布局中。接着通过窗口的setLayout()方法将水平布局方式设置为窗口的整体布局。
在水平布局中,先添加的控件位于后添加的控件左侧。
表单布局管理器就是指将控件按照表单的样式进行布局,比如可以将控件以一行两列的形式进行布局。表单布局管理器通常用来设置文本型控件和输入型控件(比如QLabel和QLineEdit)的布局,通常左列控件为文本型控件,右列控件为输入型控件。使用该布局管理器可以帮助我们快速实现一个登录界面,详见示例代码1-9。
示例代码1-9
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
username_label = QLabel('Username:') # 注释1开始
password_label = QLabel('Password:')
username_line = QLineEdit()
password_line = QLineEdit() # 注释1结束
f_layout = QFormLayout() # 注释2开始
f_layout.addRow(username_label, username_line)
f_layout.addRow(password_label, password_line)
self.setLayout(f_layout) # 注释2结束
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-17所示。
图1-17 表单布局
代码解释:
#1 实例化两个QLabel控件和两个QLineEdit控件,这两个QLineEdit控件分别用来输入账号和密码。
#2 实例化一个表单布局管理器,然后调用addRow()方法添加QLabel控件和QLineEdit控件。这样username_label和username_line就处在第一行,password_label和password_line就处在第二行。左列控件为QLabel,右列控件为QLineEdit。
使用网格布局(Grid Layout)管理器时,我们可以把窗口想象成是带有网格的,如图1-18所示,而这些网格都有相应的坐标。
图1-18 窗口网格
从图1-18中我们可以得到以下信息。
(1)username_label文本控件被放到了第1行、第1列,网格坐标为(0, 0)。
(2)username_line输入框控件被放到了第1行、第2列,网格坐标为(0, 1)。
(3)password_label文本控件被放到了第2行、第1列,网格坐标为(1, 0)。
(4)password_line输入框控件被放到了第2行、第2列,网格坐标为(1, 1)。
网格坐标只在网格布局管理器中使用,与之前讲的窗口坐标无关。另外,网格坐标是用0表示第1行或者第1列的。
示例代码1-10实现了图1-18中的网格布局。
示例代码1-10
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
username_label = QLabel('Username:')
password_label = QLabel('Password:')
username_line = QLineEdit()
password_line = QLineEdit()
g_layout = QGridLayout() # 注释1开始
g_layout.addWidget(username_label, 0, 0)
g_layout.addWidget(username_line, 0, 1)
g_layout.addWidget(password_label, 1, 0)
g_layout.addWidget(password_line, 1, 1)
self.setLayout(g_layout) # 注释1结束
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
代码解释:
#1 网格布局管理器同样有addWidget()方法,我们在调用该方法时还需要传入控件的网格坐标。运行结果跟图1-18所示的结果一样。
布局管理器除了可以添加控件,还可以添加子布局。我们现在拆分图1-18中的登录界面,将其分成多个子布局,如图1-19和图1-20所示。
图1-19 一个垂直布局(V)中包含两个水平布局(H)
图1-20 一个水平布局(H)中包含两个垂直布局(V)
通过图1-19我们可以看出,username_label和username_line使用水平布局,password_label和password_line也使用水平布局,这两个水平布局被包含在一个垂直布局中,即在QVBoxLayout中添加了两个QHBoxLayout。
通过图1-20我们可以看出,username_label和password_label使用垂直布局,username_line和password_line也使用垂直布局,这两个垂直布局被包含在一个水平布局中,即在QHBoxLayout中添加了两个QVBoxLayout。
示例代码1-11实现了图1-19中的布局嵌套方式。
示例代码1-11
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
username_label = QLabel('Username:')
password_label = QLabel('Password:')
username_line = QLineEdit()
password_line = QLineEdit()
v_layout = QVBoxLayout() # 注释1开始
h1_layout = QHBoxLayout()
h2_layout = QHBoxLayout()
h1_layout.addWidget(username_label)
h1_layout.addWidget(username_line)
h2_layout.addWidget(password_label)
h2_layout.addWidget(password_line)
v_layout.addLayout(h1_layout)
v_layout.addLayout(h2_layout)
self.setLayout(v_layout) # 注释1结束
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
代码解释:
#1 实例化一个垂直布局管理器和两个水平布局管理器。h1_layout水平布局管理器中添加了username_label和username_line控件,h2_layout水平布局管理器中则添加了password_label和password_line控件。最后v_layout垂直布局管理器调用addLayout()方法依次添加两个水平布局管理器,使它们从上到下垂直排列。
大家可以自行实现图1-20中的布局方式,源码参见资源包中的示例代码1-12。到这里,我们一共已经认识了4种布局管理器并明白了如何进行布局嵌套,这几种布局管理器各有特点,不过常使用的还是QVBoxLayout和QHBoxLayout这两种。
PyQt的一个核心知识点我们已经了解了,那现在跟随笔者再去看一下另一个核心知识点——信号和槽机制。
信号和槽机制作为PyQt中各个对象之间的通信基础,其重要程度不言而喻。只有了解了这个机制的用法,我们才能写出一个功能完善的PyQt程序。
其实这个机制非常好理解,我们拿红绿灯来做个类比。
当红灯信号发射后,行人就会停下;当绿灯信号发射后,行人就会前进。我们用red和green来表示信号,用stop()和go()函数来表示行人的动作,这两个函数也被称为槽函数。也就是说,当red信号发射后,stop()槽函数就会被调用;当green信号发射后,go()槽函数会被调用。不过信号和槽只有在连接之后才可以起作用,连接方式如图1-21所示。
在图1-21中,widget就是PyQt中的控件对象,signal就是控件对象拥有的信号,connect()方法用于连接信号和槽,而slot是槽函数名称。我们参考上面的红绿灯例子,了解代码中的连接方式:
traffic_light.red.connect(stop)
traffic_light.green.connect(go)
图1-21 信号和槽的连接方式
red信号和stop()槽函数进行连接,green信号和go()槽函数进行连接,只有这样连接后,发射的信号才可以调用相应的槽函数。总结起来就一句话:连接后,信号发射,槽函数“启动”。
在connect()方法中传入的是函数名。
我们可以在很多窗口上看到“Start”按钮,单击之后文本从“Start”变成了“Stop”,示例代码1-13通过信号和槽机制实现了这种功能。
示例代码1-13
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.btn = QPushButton('Start', self) #注释1开始
self.btn.clicked.connect(self.change_text) #注释1结束
def change_text(self): # 2
if self.btn.text() == 'Start'
self.btn.setText('Stop')
else:
self.btn.setText('Start')
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-22所示。
图1-22 改变按钮文本
代码解释:
#1 实例化一个QPushButton按钮控件之后,我们将按钮的clicked信号与自定义的change_text()槽函数连接起来。
#2 在槽函数中,我们首先通过text()方法获取到当前单击按钮的文本,如果是“Start”,就调用setText()方法将按钮的文本修改为“Stop”。而如果文本是“Stop”,就将其修改为“Start”。
因为要在槽函数中使用btn对象,所以应该在类的初始化函数__init__()中将btn设置为成员变量,也就是self.btn。当然我们也可以直接通过sender()方法获取到当前发射信号的控件对象,代码如下所示。
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
btn = QPushButton('Start', self)
btn.clicked.connect(self.change_text)
def change_text(self):
btn = self.sender()
if btn.text() == 'Start':
btn.setText('Stop')
else:
btn.setText('Start')
... # 程序入口代码不变
每个控件都有相应的内置信号,比如QPushButton控件有clicked、pressed、released等内置信号。当然我们也可以给控件或窗口自定义一个信号,笔者会在1.4.6小节中讲解。
信号是可以传值的,比如QLineEdit控件有一个textChanged信号,它会在输入框中的文本发生改变时被发射,并且会携带当前的文本。图1-23所示为官方文档中对该信号的解释,示例代码1-14演示了该信号的使用方法。
图1-23 textChanged信号
示例代码1-14
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(180, 30)
line = QLineEdit(self)
line.textChanged.connect(self.show_text)
def show_text(self, text): # 1
print(text)
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-24所示。
图1-24 控制台输出
代码解释:
#1 show_text()槽函数有一个text参数,textChanged信号携带的值会传给这个参数。运行程序,每当修改输入框中的文本时,控制台都会将修改后的文本输出。
如果信号(比如clicked信号)无法传值,而我们想要让它连接一个带参数的槽函数,这时候要怎么做呢?答案是使用lambda匿名函数。示例代码1-15演示了如何通过lambda匿名函数让clicked信号连接setText()这个带参数的槽函数。
示例代码1-15
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
btn = QPushButton('Start', self)
btn.clicked.connect(lambda: btn.setText('Stop')) # 1
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
代码解释:
#1 如果我们把此处的信号和槽连接代码更改成“btn.clicked.connect(btn.setText ('Stop'))”,那么程序就会报错,如图1-25所示。
图1-25 报错截图
在信号和槽连接时,我们必须往connect()方法中传入一个可调用对象,也就是传入函数名,不带括号。如果带了括号,就表示我们传入了函数的执行结果。setText('Stop')方法执行后返回None,信号跟None连接明显不合理。如果要将setText()用作和clicked信号连接的槽函数,就必须使用lambda匿名函数把setText()方法“包装”一下,以返回一个可调用对象。
一个信号可以连接多个槽函数,也就是信号只用发射一次,就可以调用多个槽函数。示例代码1-16是在示例代码1-13的基础上修改得到的,该示例代码将按钮的clicked信号与两个槽函数进行了连接。
示例代码1-16
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.btn = QPushButton('Start', self)
self.btn.clicked.connect(self.change_text) #注释1开始
self.btn.clicked.connect(self.change_size) #注释1结束
def change_text(self):
if self.btn.text() == 'Start':
self.btn.setText('Stop')
else:
self.btn.setText('Start')
def change_size(self): # 2
self.btn.resize(150, 30)
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-26所示。
图1-26 一个信号连接两个槽函数
代码解释:
#1 clicked信号连接了change_text()槽函数和change_size()槽函数。
#2 change_size()槽函数用于改变按钮的尺寸。
QPushButton除了有clicked信号,还有pressed信号和released信号。pressed信号是在按钮被“按下”那一刻发射,而released信号则是在按钮被“松开”后发射。“按下”和“松开”其实就构成了一次单击,也就会发射clicked信号。现在我们将pressed信号和released信号用在示例代码1-17中。
示例代码1-17
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.btn = QPushButton('Start', self)
self.btn.pressed.connect(self.change_text) #注释1开始
self.btn.released.connect(self.change_text) #注释1结束
def change_text(self):
if self.btn.text() == 'Start':
self.btn.setText('Stop')
else:
self.btn.setText('Start')
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-27所示。
图1-27 “按下”和“松开”后的按钮文本
代码解释:
#1 将pressed信号和released信号都跟change_text()槽函数连接起来。那么当按钮被“按下”(不“松开”)时,槽函数就会被调用,按钮文本从“Start”变成了“Stop”。当“松开”按钮后,槽函数再次被调用,按钮文本从“Stop”变回了“Start”。
信号不仅可以跟槽函数连接,还可以跟信号连接,详见示例代码1-18。
示例代码1-18
import sys
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.btn = QPushButton('Start', self)
self.btn.pressed.connect(self.btn.released) #注释1开始
self.btn.released.connect(self.change_text) #注释1结束
def change_text(self):
if self.btn.text() == 'Start':
self.btn.setText('Stop')
else:
self.btn.setText('Start')
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
代码解释:
#1 我们将pressed信号同released信号进行连接,而released信号则跟槽函数进行连接。当按钮被“按下”(不“松开”)后,pressed信号发射,released信号也会马上跟着发射,槽函数就会被执行,改变按钮的文本。当按钮被“松开”后,released信号再次发射,槽函数再次被调用。运行结果跟图1-26所示的一样。
在PyQt中,各个控件内置的信号已经能够让我们实现许多功能需求,但是如果想要更加个性化的功能,我们还得借助自定义信号来实现。本小节会详细介绍如何自定义信号,并通过自定义信号进行传值。
本小节的知识点相对于之前的部分要稍微难一些,如果对前面的信号和槽知识点还不是很熟悉,可以先跳过这一部分,等可以熟练地连接信号和槽后再来看。
自定义信号是通过pyqtSignal来创建的,接下来通过示例代码1-19来演示自定义信号的创建过程。
示例代码1-19
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import*
class Window(QWidget):
my_signal = pyqtSignal() # 1
def __init__(self):
super(Window, self).__init__()
self.my_signal.connect(self.my_slot) # 2
def my_slot(self):
print(self.width())
print(self.height())
def mousePressEvent(self, event): # 3
self.my_signal.emit()
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-28所示。
图1-28 控制台输出结果
代码解释:
#1 实例化一个pyqtSignal对象。
#2 将自定义信号与my_slot()槽函数连接。
#3 mousePressEvent()是鼠标按下事件函数,每当鼠标被按下时,该事件函数就会被执行(4.3节会详细讲解窗口事件)。my_siganl信号调用emit()方法将自己发射出去,这样my_slot()槽函数就会被执行,输出窗口的宽和高。
如果想要获取鼠标指针在窗口上的x坐标和y坐标,可以通过信号将坐标值发送过来,详见示例代码1-20。
示例代码1-20
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Window(QWidget):
my_signal = pyqtSignal(int, int) # 1
def __init__(self):
super(Window, self).__init__()
self.my_signal.connect(self.my_slot)
def my_slot(self, x, y): # 2
print(x)
print(y)
def mousePressEvent(self, event): # 3
x = event.pos().x()
y = event.pos().y()
self.my_signal.emit(x, y)
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec())
运行结果如图1-29所示。
图1-29 控制台输出结果
代码解释:
#1 要通过自定义信号传值,我们必须在实例化pyqtSignal对象时明确要传递的值的类型。由于x坐标和y坐标都是整型值,因此要给pyqtSignal传入两个int。
#2 槽函数也要稍做修改,需要增加两个参数,分别用于接收x坐标和y坐标。
#3 现在我们需要在鼠标按下事件中获取鼠标指针的x和y坐标,并通过emit()方法将其随信号一同发射出去。
除了整型值,我们还可以让自定义信号携带其他类型(包括Python语言所支持的值类型和PyQt自定义的数据类型)的值,详见表1-1。
表1-1 自定义信号可携带的值类型
值 类 型 |
实例化方式 |
---|---|
整型 |
pyqtSignal(int) |
浮点型 |
pyqtSignal(float) |
复数 |
pyqtSignal(complex) |
字符型 |
pyqtSignal(str) |
布尔型 |
pyqtSignal(bool) |
列表 |
pyqtSignal(list) |
元组 |
pyqtSignal(tuple) |
字典 |
pyqtSignal(dict) |
集合 |
pyqtSignal(set) |
QSize |
pyqtSignal(QSize) |
QPoint |
pyqtSignal(QPoint) |
信号和槽机制就暂时讲到这里,希望大家能够花时间理解相关内容,这会让你在后续的PyQt程序编写中事半功倍。现在让我们进入本章的最后一个部分,即学会使用文档。
PyQt的官方文档的内容不是很详细,比如我们在其中就找不到各个控件的用法解释。由于PyQt是Python版本的Qt,所以我们完全可以去看C++版本Qt的官方文档。
编程语言的设计思想是相通的,只是语法会有一些区别。所以不要担心,只要你会使用Python,那 C++版本的文档就是可以看懂的。不管是Python还是 C++,我们在编写 PyQt或Qt代码时所使用的类名和方法名都是一样的,只不过两种语言对方法的调用方式和解释有所不同。
PyQt提供了Qt Assistant文档助手工具,它是一个桌面版的文档查询软件,提供了一些快速查找的功能。由于文档是会经常更新的,所以笔者认为还是直接查阅在线文档比较好。
在本小节,笔者会拿C++版本和Python版本的代码片段做对比,好让大家能看懂文档上的C++代码。如果读者已经学过C++,可以直接跳过本小节。
C++版本
QLabel *label = new QLabel(this);
label->setText("Hello World");
Python版本
label = QLabel(self)
label.setText("Hello World")
从这两段代码中我们可以发现,不管是C++还是Python,代码中所使用的类名和方法名都是一样的,所以将C++代码转换成Python代码是一件非常简单的事情,反之亦然。
当文档在解释一个方法时,比如QWidget类的resize()方法,我们会看到这样的C++代码片段:void QWidget::resize(int w, int h)。
这段C++代码告诉我们resize()方法是属于QWidget这个类的,类名前面的void表示该方法不会返回任何值。如果把void改成QString,就表示该方法会返回一个字符串。int w和int h表示我们需要往该方法中传入2个整型值。将上述代码转换成Python代码的话就是:QWidget. resize(w, h)。
接下来,我们来看官方文档中对QLabel控件的实例化方法QLabel()的解释,如图1-30所示。
图1-30 文档解释
根据文档提示,我们可以往该方法中传入3个参数:第一个是文本字符串(const表示常量);第二个是QWidget类型的父类对象,nullptr相当于 Python中的None,表示该参数默认为空;第三个是Qt.WindowFlags类型的值。QLabel()实例化方法的Python版本如下所示。
QLabel.QLabel(text, parent=None, f=Qt.WindowFlags())
打开在线文档后,我们可以看到页面上罗列出了所有的类。可以按“Ctrl+F”快捷键进行搜索,比方说我们要查阅QLineEdit控件的用法,在搜索框中输入“QLineEdit”后,就会在页面直接定位到它,如图1-31所示。
图1-31 搜索QLineEdit
单击“QLineEdit”进入QLineEdit控件介绍页面,往下滑动页面找到“Detailed Description”,查看官方对该控件的介绍,在这部分我们能够学习到该控件的常见用法,如图1-32所示。
图1-32 控件介绍
“Properties”部分罗列了QLineEdit控件拥有的各个属性。在“Public Functions”部分,我们可以看到该控件常用的一些方法,而在“Signals”部分则可以看到该控件拥有的信号。各个属性、方法和信号文本都设置了超链接,如果对其有疑问,直接单击对应超链接就可以跳转到解释部分。
如果没有在上面几个部分中找到想要的内容,我们可以单击“List of all members, including inherited members”超链接,如图1-33所示。跳转后的页面上会罗列出QLineEdit控件所有的属性、方法和信号。有些时候,如果我们忘了某个方法的用处,可以直接前往这个页面,然后按“Ctrl+F”快捷键查找相关方法。
图1-33 显示所有的属性、方法和信号
单击某个方法(比如setTextMargins()方法)的超链接后,就可以看到这个方法的使用介绍,如图1-34所示。从介绍中我们得知setTextMargins()方法可以用来设置输入框中的文本边距,文档中还提供了与其相关的textMargins()方法,它用来获取输入框中的文本边距。
图1-34 setTextMargins()方法的使用介绍
本章介绍了PyQt程序入口的写法、各个布局管理器以及信号和槽机制,也介绍了如何看懂和使用C++版本的Qt官方文档。
我们可以直接调用move()方法设置控件在窗口上的位置,但如果要让控件在窗口上自适应,建议使用布局管理器。布局管理器包括垂直布局管理器、水平布局管理器、表单布局管理器和网格布局管理器。布局管理器对象可以通过addWidget()方法添加控件。如果要在布局中嵌入另外的布局则可以调用addLayout()。
信号和槽机制是本章的重点,信号可以和一个或多个槽函数进行连接,也可以和一个或多个信号进行连接。每个控件都有各自的信号,会在条件满足时发射,比如按钮有一个clicked信号,它会在按钮被单击时发射。当PyQt内置的信号无法满足需求时,我们可以使用pyqtSignal自定义信号,并通过它传递各种类型的值。
我们在编写代码时如果遇到一些疑问,比如对某个控件或者某个方法不熟悉,应该去查阅官方文档。官方文档是我们坚实的后盾,不写代码时也可以多去看看。
本章的内容非常重要,掌握好本章的内容能让我们在接下来的学习中事半功倍。
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e60866”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。