PyQt5基本用法&注意事项小结
前言
PyQt5可以说是Python中比较重要,也比较热门的GUI模块,依托Qt的实力而具有丰富的积累。就我目前的了解来看,和Python自带的GUI模块Tkinter相比,PyQt的重要优势是附带的图形界面工具Designer可以帮助开发者高效建立图形界面,以及更丰富的部件和信号发送-处理机制。综合起来,使用体验和Tkinter相比是要好很多的。
当然,网上关于PyQt的教程已经很多了,我暂时还是在短暂地接触上手的阶段,没有原创的经验。这里摘录一些比较基础、常用的使用示例,我个人遇到的一些问题,以及对应的解决方案。因为来源太多,很难一一记录,这里声明,除有特别说明的以外,本文的示例代码和解决方案都并非原创。
基础使用和代码示例
这里简单介绍PyQt及其GUI工具的安装和使用。
基础使用
首先是安装,使用pip install PyQt5
安装即可。(PyQt模块体积略大)
安装完成后,在Python安装目录/Scripts/
目录下的designer.exe
即为自带的pyqt-tools图形界面工具。开启时似乎会伴有Shell界面,但是在之后关闭也没有影响。界面如图。
选择Mainwindow或其他默认窗口后,Designer会创建.ui文件以记录图形界面。可以看到,通过Designer,可以非常方便地完成添加、修改部件尺寸,修改部件的其他属性等操作。这确保了GUI编程的效率。自然,有人更喜欢直接使用代码编写GUI界面,网上也有不少教程是这么做的。我个人比较赞同一个观点,就是要把GUI和逻辑分离。
保存后,需要使用PyQt自带的工具/scripts/pyuic.exe
将转成Python代码,如:
1 | pyuic5 -o output.py MainScreen.ui |
可以看到,output.py
将MainScreen封装成了一个重写的类。要调用主窗口类,并将主窗口实例化时,需要另写一个main.py
来提供入口。以下是常见的形式:
1 | import sys |
对于上述代码,有必要仔细区分MainWindow
和MainUI
。对组件进行操作时,一般调用MainUI
;而对窗口进行操作时,一般调用MainWindow
。人为清除窗口时,有MainWindow.destroy()
和MainWindow.hide()
两种方法。经测试,只调用destroy()
时,运行不会自动停止;而所有窗口都调用了hide()
方法时,运行就会中止。因为没有研究过官方文档,所以这一点还有待讨论。
部件交互
这里总结部件交互的基本方式稍作总结。与Tkinter的回调形式不同,PyQt(Qt)采用的是信号-槽的信息传递方式,其途径是Sender-Signal
到Receiver-Slot
,即信号源部件发出特定信号(可以传递参数),而对信号接受部件的槽(如方法)。这为GUI部件之间的信息传递提供了便利。
一些常用部件有固定的常规信号,如Button。可以通过以下代码将按钮点击信号绑定到方法:
1 | MainUI.Button1.clicked.connect(func) |
要实现一些部件的非常规交互方式,其中一种方式是重写该部件的事件和信号发送的相关方法。如对QPlainTextEdit部件的重写:
1 | from PyQt5 import QtCore, QtGui, QtWidgets |
可以看到,这段代码通过QtCore.pyqtSignal()
定义了一个待发送的信号,并确定了QPlainTextEdit对鼠标中键点击的响应,相当于修改了事件的过滤器。其中的mouseReleaseEvent
和QtCore.Qt.MidButton
都可以根据需要进行修改,这里只是一个简单的例子。
部分的信号-槽可以在Designer中添加。无奈的是,上述方式似乎不能通过Designer实现,每次修改GUI并重新生成后都不得不重新补上重写的类。不知道是否有更好的解决方案。
而RadioButton和CheckButton有更简洁的交互方式,可以直接调用它们的属性,如RadioButton1.isChecked()
和CheckButton1.isChecked()
。
对于从文本框获取文本的需求,因为PyQt的类似部件很多,不同文本框部件的获取文本的方法名又不尽相同,还是要参考Qt文档。常用的部件如TextEdit是GetText()
,而纯文本常用的PlainTextEdit采用的是toPlainText()
。修改文本也是如此,如PlainTextEdit采用的是setPlainText()
,而其他的部件,包括Lable,多采用setText()
。
其他常用功能
这里罗列一些常用的基本功能。
设置图标
程序图标设置通过QIcon和窗体的方法实现,如:
1 | icon = QtGui.QIcon() |
最后两个参数是图标的模式,我没有测试过具体作用。
基本对话框
PyQt提供了几种基本的对话框,即information、warning、fatal、question、about。其具体使用如:
1 | result = QMessageBox.information(MainWindow, "OK", "Configuration file loaded!", QMessageBox.Ok, QMessageBox.Ok) |
这是一种调用默认弹窗静态方法的用法。其中information
指明类型,各参数依次是父对象、窗口标题、正文内容、对话框使用的默认按钮和默认焦点所在的按钮。可以使用多个按钮,用“|”分割。这里用户的选择作为返回值赋给result,类型是QMessageBox.Ok等。下面是一个about
窗口的用例:
1 | QMessageBox.about(MainWindow, 'About', ShowText) |
这里的about
窗口只能使用默认的确认键。另外,如果设置了程序图标,那么该窗口也会在左侧显示放大的图标。
这些默认弹窗基本可以满足一般需求,但是如果要求更复杂的交互,就需要另外设计了。Designer提供了相应的模板。
使用样式
PyQt支持各类HTML标签和CSS样式。也就是说,可以在文本中使用HTML标签和style
来调整文本。如:
1 | style = 'font-family: Calibri; font-style: italic; font-size: 12px; font-weight: bold; color: red' |
另外惊喜的是,经测试,img标签同样是可用的,所以也可以用这种方式插入图片。
注意事项
方便归方便,在上手的过程中,我还是遇到了各种意外的情况,这里稍作总结。
窗体显示
PyQt窗体貌似默认以低分辨率的模式运行,而这会给布局效果带来灾难性的影响。可以使用下面这行代码解决这一问题:
1 | QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) |
值得注意的是,这里的顺序似乎是很关键的。另外,对于字体无法显示完全,也有一些其他的解决方案,如:
1 | font = QtGui.QFont("Arial") |
这是通过使用便于自适应的Arial字体,并按比例调节字体大小来解决字体显示问题的。这里只是以主窗体为例,其他的部件同样可以重新设置字体。
部件布局
PyQt的部件一定要使用自适应的布局!这是我经过两天的痛苦摸索之后得出的结论。事实证明,PyQt的窗口在不同机器上的运行结果可能完全不同,而其中可能的原因是对绝对大小的定义的差异,以及对窗口伸展性定义的差异。下面是一个惨不忍睹的窗口效果的例子。(图源:Toyomu的机器)
而PyQt提供四种基本布局方式,即水平并列、垂直并列、表格布局、表单布局,其具体模式都如字面意思,可以结合需要使用。在Designer中,这些布局方式都很容易设置。另外,要注意对整个窗体,即Designer中默认的centralwidget
选用布局,以便部件随窗体大小变化而调整。
可以看到,通过布局的参数调节,可以设置边距、比例等,达到预期的效果。
使用Pyinstaller打包
在简单的程序中,使用Pyinstaller打包并不会带来问题。然而,当程序依赖于资源文件,如图片时,选择用Pyinstaller打包成单文件的exe就会带来问题。首先是,必须将资源文件一并打包。参考网上的其他教程可知,可以通过一般的命令使用Pyinstaller后,修改生成的.spec文件中的设置,添加资源文件夹,如:
1 | datas = [('res', 'res')] |
其中,元组中的第一个参数是资源文件夹的当前路径,第二个参数指定的是打包后释放文件时的所在路径。修改完成后,通过.spec文件打包即可:
1 | pyinstaller -F -w example.spec |
另一方面,通过一般方式,如相对路径引用、os模块的绝对路径获取,都会得到单文件程序本体所在的路径。然而,Pyinstaller打包的单文件程序的实际操作是,将包中各部分文件释放到以_MEIPASS开头的临时目录中,包括资源文件夹,对本体路径的引用是无效的。所幸,Pyinstaller打包的单文件程序中,会在sys加入sys._MEIPASS
这一属性,以便获取实际路径,即临时文件的所在目录。如果要兼顾调试和实际使用,可以定义一个函数:
1 | def GetAbsolutePath(filename): |
这样,就可以保证引用正确了。