目录
1. 安装
2. designer简介
3. 联合ui界面写逻辑
4. 打包你的程序让没有python环境的朋友也能愉快的玩耍
5. 选修-如何优雅的应对耗时的(阻塞)的程序
pip install PyQt5
pip install PyQt5-tools
两个包安装好之后可以在Anaconda3\Lib\site-packages\qt5_applications\Qt\bin
路径下找到designer.exe。
这个东东就是我们大显身手的工具了。
我们选择Main Window后点击创建
左侧框框里就是各种各样的控件,例如按钮控件Push Button,复选框控件Check Box,文本输入控件Plain Text Edit。你可以直接拖动你想要的控件到主界面上。如果你想编辑某个控件具有的属性(字体/大小/启用/禁用等),你首先需要选中需要调整的对象,一种方法是你在主窗口直接点击这个控件,那么右侧的属性编辑器将作用于这个控件;另一种方法是在右侧的对象查看器里通过结构关系来找到这个对象的名字,然后点击对象名来定位。
我们来简单尝试一下,拖一个按钮和一个文本框到主界面(CTRL + R预览)。
好了你已经学会设计程序UI的90%了,接下来就是按找你的需求,去找合适的控件,然后调整控件的位置/属性让整个程序的布局不那么难看。这里我修改了一下按钮的名字,假设我要实现这样一个需求,当我点击的这个按钮之后,按钮上方的文本框将会显示5秒之后的时间。
然后我们把我们设计好的这个窗体保存下来,保存输出的是一个以.ui后缀结尾的文件。
有小可爱可能就问了,.ui和我.py有何关系,我们可是py工程师。
别急,我们打开cmd,cd到保存的.ui文件的路径,假设我保存的这个文件叫做ui.ui,进入到这个文件所在的路径之后,我们执行以下命令:
pyuic5 -o ui.py ui.ui
你会发现生成了一个ui.py文件,打开看看,里面是我们熟悉的py代码没错了。
它的内容也是比较容易理解的,对一些控件设置属性尺寸等等参数。
不过看着东西有点多,眼花缭乱有没有。没关系,我们其实完全不用care这个文件,只用知道这个文件可以用来展示我们需要的ui界面,拿来用就行了。
在第2节中我们生成了一个ui.py文件,并且知道了这个文件就是我们程序主要的ui文件,但是当你运行这个文件你会发现没有任何的output(那当然了,在这个文件里只有一个类的定义,并没有实例以及调用)。
实际开发过程中,程序的开发需求可能是经常更新变化的,有时候这里添个文本框,那里又加个按钮,如刚刚见到的,由designer转换过来的代码是冗长的,我们若是通过在这个转换后的代码里定位要改的ui无异于大海捞针。聪明的你当然能想到,我们依旧可以通过designer来修改ui,然后重新转换py文件对旧文件进行覆盖即可,这也就意味着,由.ui文件转换来的.py文件可能是会被时时覆盖的,这就意味着如果你手动改写了ui.py文件,这些改动没有人为记录下来的话,那么下一次你通过designer生成的新设计文件将会完全覆盖旧的文件,这些改动就会丢失掉。所以,机智的做法是,ui.py文件,除了由命令行生成之外,不要再对他有任何的改写操作。当然,读取还是要的,不然生成它干啥。
好了,我们现在在ui.py同级目录新建一个server.py文件,写入如下代码:
import sys
from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication
class MyWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.setupUi(self)
if __name__ == '__main__':
app = QApplication(sys.argv)
myWin = MyWindow()
myWin.show()
sys.exit(app.exec_())
运行server.py,你会发现你刚才通过designer设计出来的用户界面就弹出来啦。
我们回过头来理一下我们目前的项目结构。首先最开始是一个ui.ui文件,它是由designer设计好了ui界面之后保存生成的,我们也可以通过designer重新打开这个文件并进行更新修改,为了方便更新设计,这个文件我们基本上是要保留在项目里的。然后我们通过一行命令转换除了ui.ui的py版本ui.py,在server.py里面我们引用了ui.py定义的类。然后我们要做的就是要server.py写我们工具的逻辑了。
再强调一下,ui.py文件只需要由ui.ui转换而来,除此之外,不需要对其有任何的修改,因为这个文件可能时时丢失。
好了,接下来我们要干正事了,刚才我假设了我们要实现的需求是当我点击按钮之后,按钮上方的文本框要显示5秒之后的时间来着。
要让文本框显示5秒之后的时间,首先我们要知道这个文本框在整个项目里是哪个对象,你可以通过designer选中这个文本框,右侧就会显示这个对象的名字。我这里没有进行修改,所以文本框是默认的名字plainTextEdit,在实际的开发过程中,当然要像给变量取名一样认真对待它的名字。
好了,我们回到server.py,在py里,self加上这里的名字就是我们的目标对象了。我们尝试在__init__方法里键入self.plainTextEdit,然后再键入一个.,可以看到,pycharm的自动补全为我们展示这个对象具有的方法。这些方法的名字也很直观有没有,我们完全就可以顾名思义。例如toPlainText()转换为普通的文本,insertPlainText()插入普通文本。
我们尝试对文本框的内容进行更改。
import sys
from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication
class MyWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.setupUi(self)
self.plainTextEdit.setPlainText('我是新的内容')
if __name__ == '__main__':
app = QApplication(sys.argv)
myWin = MyWindow()
myWin.show()
sys.exit(app.exec_())
运行。
成功。但是,显然这样不符合我们的要求呀,我做这个东西当然不能是通过改程序代码来实现不同的展示文本是吧,当然也要通过这个运行的程序本身来实现变化,比如点击那个按钮,文本框的文本变化,这才是做这个玩意的初衷。
那么,我们就要实现这样一个流程了,点击按钮,文本框的文本变化。实际上,这是一个事件,也就是按钮的click事件,当按钮被按下时,执行文本框的文本修改程序。
改写代码如下:
import sys
from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication
class MyWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.setupUi(self)
self.pushButton.clicked.connect(self.change_text)
def change_text(self):
self.plainTextEdit.setPlainText('我是你按了按钮之后才出来的')
if __name__ == '__main__':
app = QApplication(sys.argv)
myWin = MyWindow()
myWin.show()
sys.exit(app.exec_())
运行,那么首先弹出来的程序界面文本框里是没有内容的,当按钮被点击时,文本框就出现了新的内容,这里就不展示了。
我们就可以根据这样的套路来编写很多很多的事件了,要注意connect里面的参数必须得是一个函数,不带括号那种!你也可以为它传入匿名函数。
现在,我们来把刚才想要的程序逻辑完整的写一写,点击按钮后,文本框显示5s后的时间。
import sys
import datetime
from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication
class MyWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.setupUi(self)
self.pushButton.clicked.connect(self.change_text)
def change_text(self):
self.plainTextEdit.setPlainText((datetime.datetime.now() + datetime.timedelta(seconds=5)).strftime('%Y-%m-%d %H:%M:%S'))
if __name__ == '__main__':
app = QApplication(sys.argv)
myWin = MyWindow()
myWin.show()
sys.exit(app.exec_())
读者可运行代码自行验证。
打包的话推荐建立一个新的虚拟环境,在旧的环境里打包的话,比如你之前为了研究网络爬虫,下载了requests
包,而实际上,我们这个项目根本不需要这个扩展包,而当你在原来的环境下打包,那么也会把这个用于网络爬虫的包给打包进来,当然,这是打包多了,对程序的正确运行是没有影响的,只是程序的体积可能会出人意料的大。
打开命令行窗口,执行如下命令(注:如果没有配置Anaconda的环境变量,那么需要打开Anaconda的shell)
conda create -n pyqt_demo python=3.8 % 建立一个名为pyqt_demo的虚拟环境,环境配置的python版本是3.8 %
conda activate pyqt_demo % 激活我们刚刚建立的环境 %
好了,现在我们已经进入了一个新的环境里,这个环境除了自带的包,是没有其他扩展包的,也包括我们一开始下载的Pyqt5包,就好比是你刚刚安装好Python的时候,所以我们要在这个环境里重新下载一下这些包。
在刚刚激活了虚拟环境的命令行中,我们使用pip进行下载
pip install Pyqt5
pip install pyinstaller
现在我们进入我们刚刚编写的程序在的路径,执行以下代码就可以开始打包了:
pyinstaller -F server.py
执行完之后你会发现多了一大堆额外的文件,大部分是没用的,你甚至可以删掉,找到一个dist文件夹,里面就是你想要的东西了。运行一下看看:
嗯,心满意足了。
不对,怎么有个黑黑的框,对,程序运行的时候会默认打开一个命令行窗口,这个命令行窗口会显示在程序里你人工打印的信息或者报错信息,方便调试。
但是显然如果是要发布的程序,那么这个命令行窗口就显得碍眼了。如何把它去掉呢,很简单,我们给刚才的打包代码加点料。
pyinstaller -F server.py --windowed
好了,去运行看看重新生成的程序吧,愉快的玩耍吧!
学完以上四节之后,应对基本的开发需求是完全没问题了的。
本小节的内容涵盖Pyqt5的多线程和自定义信号,这两块内容能帮助你的程序更加的优雅。
好了,我们还是关注刚才那个例子,我们想要点击按钮之后,显示5s后的时间。刚才我们的做法是,给按钮的点击事件绑定了一个函数,这个函数更改文本框的内容,更改的内容是我们计算出来的5秒后的时间,我们怎么计算的呢,通过datetime模块,我们取得当前的时间然后加上5s钟,那就是5s后的时间了!现在,我们要另辟蹊径,我们换一条路子,当点击了按钮之后,我们让程序休眠5s,然后再输出当前时间!这样是不是也输出了5s后的时间,好了,说做就做,我们改代码去!
import sys
import datetime
import time
from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication
class MyWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.setupUi(self)
self.pushButton.clicked.connect(self.change_text)
def change_text(self):
time.sleep(5)
self.plainTextEdit.setPlainText((datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
if __name__ == '__main__':
app = QApplication(sys.argv)
myWin = MyWindow()
myWin.show()
sys.exit(app.exec_())
可以看到,按钮绑定的change_text的函数已经被我们改写了,里面加入了time.slepp(5),然后输出当前的时间。
现在我们运行一下看看效果。
界面也还是没变化的,我们点击一下按钮看看。这时候,文本框并没有一下子就跳出结果了,咦,我刚才点到这个按钮了吗?怎么好像没点到?咦,我怎么点不了最小化窗口了,咦,怎么关也关不掉这个窗口了,正当你一筹莫展以为死机的时候,文本框突然弹出了内容,一切恢复了~
这时候有人可能要指出这种计算5秒后时间的方法有点憨批,确实确实,不过我们要关注的点并不是这个需求的实现方式上,这一种方式模拟的是这样一种情景,当你触发某个事件,程序要去做一些耗时的事情,比如我要去数据库取数据,数据库查询可能比较耗时间,这个事件程序就要等着直到有返回结果,或者点击了按钮之后,程序要执行大量的计算,总而言之,按钮点击事件被触发之后,不能立即得到结果。由于整个程序处于同一个线程中,那么程序在执行这个耗时操作的时候,其实会把一切都卡死,简单点来说,程序的代码是一步一步执行的,执行完第一行才能执行第二行,然后第三行,如果第三行的代码很耗时,那么就会一直卡在第三行,我们能看到的影响是,程序会处于一个假死(未响应)状态,5s后,程序执行完了耗时的逻辑,重新恢复。那么这样的情况给用户的体验当然是十分不好的,下面我们就给出一个方法来优雅的解决这种状况。
概括点来讲,把耗时的操作放到另一个线程中(线程的概念这里就不介绍了,想要详细了解的自行了解),按钮绑定的事件是启动这一个线程。
首先,从PyQt5.QtCore
导入QThread
和pyqtSignal
,QThread
是专用于Pyqt5的线程处理模块,pyqtSignal
用于在线程与线程之前传递消息。
实践一下,注意我们这里写一个新的线程的话,要继承QThread这个类,然后重写run方法,注意一定得是run方法,当这个线程启动的时候,它启动的是就是这个线程的run方法。这个线程的做了这样一件事,首先休眠5s钟,5s钟后通过我们定义的result_return_sign信号向主线程传递一个值,这个值就是当前的时间。
class TestThread(QThread):
result_return_sign = pyqtSignal(str)
def run(self):
time.sleep(5)
self.result_return.emit(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
好了,现在我们要在主线程里初始化这个线程,然后调用这个线程,然后还要绑定这个线程的信号处理,也就是当这个线程传出来某个信号,我们要根据这个信号做点什么。当前情景下,我们收到的次线程的信号内容就是5s后的时间,那么这里我们就是接收信号值然后把信号值放到文本框上去。
完整的代码如下:
import sys
import datetime
import time
from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import QThread, pyqtSignal
class TestThread(QThread):
result_return_sign = pyqtSignal(str) # 定义一个字符串类型的变量,根据需求还可以int, bool等等
def run(self):
time.sleep(5) # 休息5秒
self.result_return_sign.emit(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) # 利用信号向外传递当前时间
class MyWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.setupUi(self)
self.pushButton.clicked.connect(self.start_new_thread) # 按钮绑定的事件是一个启动一个新线程的方法
def start_new_thread(self):
self.new_thread = TestThread() # 实例化线程
self.new_thread.result_return_sign.connect(lambda text: self.change_text(text)) # 为线程的result_return_sign绑定一个回调函数(定义当这个信号发生/传来时要做什么)
self.new_thread.start() # 启动线程,这里会自动调用线程的run方法,所以在定义线程的时候一定要重写的是run方法
def change_text(self, text):
self.plainTextEdit.setPlainText(text)
if __name__ == '__main__':
app = QApplication(sys.argv)
myWin = MyWindow()
myWin.show()
sys.exit(app.exec_())
现在运行一下程序,你会发现在点击了按钮之后,程序窗口并不会假死了,你可以任意缩放窗口或者做其他事情。我们再加点料,把程序整的像样一点,比如点击了按钮之后,按钮要变成不可用直到结果返回后。
import sys
import datetime
import time
from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import QThread, pyqtSignal
class TestThread(QThread):
result_return_sign = pyqtSignal(str) # 定义一个字符串类型的变量,根据需求还可以int, bool等等
def run(self):
time.sleep(5) # 睡觉睡5秒
self.result_return_sign.emit(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) # 利用信号向外传递当前时间
class MyWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.setupUi(self)
self.pushButton.clicked.connect(self.start_new_thread) # 按钮绑定的事件是一个启动一个新线程的方法
def start_new_thread(self):
self.statusbar.showMessage('程序正在计算中...') # 左下角状态显示程序运行状态
self.pushButton.setEnabled(False) # 禁用按钮
self.new_thread = TestThread() # 实例化线程
self.new_thread.result_return_sign.connect(lambda text: self.change_text(text)) # 为线程的result_return_sign绑定一个回调函数(定义当这个信号发生/传来时要做什么)
self.new_thread.start() # 启动线程,这里会自动调用线程的run方法,所以在定义线程的时候一定要重写的是run方法
def change_text(self, text):
self.plainTextEdit.setPlainText(text)
self.statusbar.showMessage('程序计算完成!')
self.pushButton.setEnabled(True) # 重新启用按钮
if __name__ == '__main__':
app = QApplication(sys.argv)
myWin = MyWindow()
myWin.show()
sys.exit(app.exec_())
好了,愉快的玩耍吧~