书名:Python Qt GUI与数据可视化编程
ISBN:978-7-115-51416-5
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 王维波 栗宝鹃 张晓东
责任编辑 杨海玲
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书介绍在Python中使用PyQt5和其他模块进行GUI和数据可视化编程的方法。第一部分介绍PyQt5设计GUI程序的基本框架,包括GUI应用程序的基本结构、窗体UI可视化设计与窗体业务逻辑的设计、信号与槽的特点和使用等。第二部分介绍GUI程序设计中一些主要功能模块的使用,包括基本界面组件、事件处理、数据库、绘图、多媒体等。第三部分先介绍使用PyQtChart和PyQtDataVisualization进行二维和三维数据可视化设计的方法,再介绍将Matplotlib嵌入PyQt5 GUI应用程序窗口界面中进行数据可视化的编程方法。通过研读本书,读者可以掌握使用PyQt5、PyQtChart、Matplotlib等模块进行GUI应用程序和数据可视化设计的方法。
本书适合具有Python 编程基础,并想通过Python设计GUI应用程序或在GUI应用程序中实现数据可视化的读者阅读和参考。
Python作为一个开源的解释型编程软件,在教学、科研、实际项目中用得越来越多。Python易学易用,程序资源丰富,在编程解决一些科学计算问题时比较实用,但是Python自带的Tkinter包设计GUI程序的功能比较弱,无法设计专业的GUI应用程序。
Qt C++类库是一套广泛使用的跨平台GUI设计类库,PyQt5是Qt5 C++类库的Python绑定,使用PyQt5在Python里编程,可以将Python丰富的科学计算、图形显示等功能与PyQt5的GUI设计功能结合起来,开发出比较专业的Python GUI应用程序,便于对研究成果进行有效的集成与展示。
目前,介绍Python编程的书很多,但是专门介绍PyQt5 GUI编程的书很少。本书介绍两个主题:一个是使用PyQt5进行GUI应用程序设计,另一个是使用PyQtChart、PyQtDataVisualization和Matplotlib在GUI程序的窗口界面上嵌入数据可视化功能。这两个主题都是非常实用的,可以将研究成果集成为一个GUI应用程序,进行交互式操作和结果展示。
本书介绍在Python中使用PyQt5、PyQtChart、Matplotlib等进行GUI应用程序设计和数据可视化编程的方法,全书的内容分为三部分。
第一部分是PyQt5开发基础,包括第1章和第2章。
第1章介绍Python、Qt、PyQt5的特点和安装方法,在Windows中建立开发环境。
第2章介绍使用PyQt5开发GUI应用程序的基本框架原理,包括GUI应用程序的基本结构、使用可视化设计UI窗体时开发GUI程序的流程和框架、信号与槽的使用方法等。掌握了第2章的内容就掌握了PyQt5开发GUI应用程序的框架性原理,再学习第二部分和第三部分就很容易了。
第二部分是GUI应用程序设计,从第3章至第11章。
这部分介绍GUI应用程序设计中常用的一些功能模块的编程使用方法,包括常用界面组件的使用、Model/View结构、事件处理、对话框和多窗口设计、数据库、绘图、文件读写和操作、多媒体、多语言界面和Qt样式表定制界面等。
这部分的内容根据PyQt5和Python各自的特点做了取舍,总体的原则就是对GUI程序设计中必需的,而Python中没有或功能不强的模块进行介绍。例如,Python虽然有自带的数据库、多媒体、文件读写功能模块,但是功能不如PyQt5的相应模块,或不易与PyQt5的GUI程序的窗口界面结合使用,因此本书就介绍PyQt5的数据库、多媒体、文件读写功能模块。而Python自带的多线程编程功能已经比较全,且不涉及用户界面,因此本书就不介绍PyQt5的多线程编程功能。Python有很多功能强大的第三方网络功能模块,因此没有必要介绍PyQt5的网络编程功能。
第三部分是数据可视化编程,从第12章至第14章。
Chart和Data Visualization模块是Qt C++类库的一部分,分别用于二维图表绘制和三维数据可视化,但是PyQt5中没有这两个模块,需要单独安装PyQtChart包和PyQtDataVisualization包。第 12 章介绍使用PyQtChart模块绘制各种二维图表的编程方法,第13章介绍使用PyQtDataVisualization模块绘制三维柱状图、三维散点图和三维曲面图的编程方法。
Matplotlib是Python中应用最广泛的数据可视化模块,但是一般介绍Matplotlib数据可视化的书很少详细介绍将Matplotlib嵌入GUI窗口上的编程方法。第14章专门介绍Matplotlib与PyQt5结合,嵌入GUI程序中实现数据可视化的编程方法,这是在编写集成化的Python GUI应用程序时经常遇到的,是非常实用的功能。
本书使用的编程语言是Python,但是本书并不会介绍Python语言基础,需要读者对Python编程有一定的了解,特别是对Python的面向对象编程原理要比较熟悉。如果读者对Python不够熟悉,需要参考专门介绍Python编程基础的书,学会Python后再来学习本书。
本书的内容虽然用到Qt的IDE,即Qt Creator,但是并不需要编写任何C++语言程序,所以读者无须具有C++语言基础。当然,如果读者有C++语言基础,或者对Qt C++编程比较熟悉,对阅读本书的内容是非常有帮助的。
学习本书应从第一部分开始。第1章介绍本书用到的各个软件及其安装,搭建开发环境。第2章是本书的基础和重点内容,介绍了PyQt5 GUI应用程序的基本代码框架、基于UI窗体可视化设计的GUI应用程序的设计流程和工具软件pyuic5的使用、UI与窗体业务逻辑分离设计的原理、Qt的核心技术信号与槽的使用方法、Qt Creator中管理和使用资源文件,以及通过工具软件pyrcc5将资源文件转换为Python程序的方法。第2章还创建了3个单窗口项目模板,本书的大部分示例都是基于这几个项目模板创建的。
掌握了第2章的内容就掌握了用PyQt5设计GUI程序的技术框架,剩下的就是PyQt5中用于GUI应用程序设计的各种类的使用了。
第二部分介绍PyQt5 GUI程序设计中各个技术模块的使用方法,包括常用界面组件、Model/View结构、事件处理、对话框与多窗口设计、数据库、绘图、文件、多媒体等,读者可以根据自己的需要学习或查阅相应章节。第11章有两个新的技术点不在第2章介绍的技术框架内,分别是多语言界面设计方法和Qt样式表定制界面方法。
第三部分介绍数据可视化设计方法。PyQtChart和PyQtDataVisualization是Qt C++类库相应模块的Python绑定,分别用于二维图表和三维数据可视化设计,其内容的介绍比较全面。另外由于Matplotlib在Python数据可视化中应用广泛,第14章专门介绍将Matplotlib嵌入GUI窗体上实现交互式数据可视化的设计方法,包括主要的技术点和一些常用二维图和三维图的编程使用方法。
PyQtChart、PyQtDataVisualization与Matplotlib的某些功能是重合的,但它们各有千秋,读者可根据自己的需要和熟悉的内容选择学习和使用。如果读者熟悉Qt C++类库中的二维图表和三维数据可视化模块的使用,就参阅第12章和第13章;如果读者熟悉Matplotlib的使用,就参阅第14章。
本书的示例程序都是在64位Windows 7系统里开发和测试的。在开始编写本书时使用的是Qt 5.11和PyQt 5.11,完成本书初稿时已经发布了Qt 5.12和PyQt 5.12,由于Qt 5.12是一个LTS(Long Term Supported)版本,于是又用Qt 5.12和PyQt 5.12对全书内容和程序进行了检查、修改和测试。
本书使用的各个软件或Python包的版本分别是Python 3.7.0、Qt 5.12.0、PyQt 5.12、PyQtChart 5.12、PyQtDataVisualization 5.12、Matplotlib 3.0.0。
读者在拿到本书进行阅读和学习时,这些软件肯定已经有更新的发布版本了。读者在构建开发环境时使用最新的软件版本即可,不必与本书使用的软件版本完全一致,因为这些软件在大的版本序列里基本上是向下兼容的。
本书提供所有示例源程序的下载,读者可以到人民邮电出版社异步社区搜索到本书后,根据提示下载本书的示例程序资源。本书提供两套示例源程序,使用目的不同。
一套是具有全部源码的程序,包括Qt项目、UI窗体、Python程序等,其中的Python主程序可以直接运行,显示示例运行结果。读者可以使用这套源程序测试和查看示例运行结果,并查看已设计好的UI窗体和Python程序文件。
另外一套是只有UI窗体的不完整程序,包括Qt项目、UI窗体、Python程序框架,其中的Python程序文件只有基本框架,没有功能实现代码。这套程序是为了便于读者使用已经设计好的UI窗体,根据书上介绍的内容和过程,在Python程序框架里自己编写程序,逐步实现功能。之所以保留UI窗体,是因为UI窗体的可视化设计是个比较耗时间的过程,读者如果自己从头开始设计UI窗体,难以保证所有组件的名称和属性与示例的一致,在Python编程实现业务功能时容易出现问题。
作者一贯认为UI窗体的可视化设计不是学习编程的重点,窗体界面的创建能用可视化设计解决的就不要用代码。一般情况下,做过几个示例后很快就可以掌握UI窗体可视化设计的方法,所以,学习编程的重点是各种界面组件和功能类的接口函数、信号的灵活使用,以实现程序的业务逻辑功能。
本书编写和运行Python程序使用Python 3.7自带的软件IDLE,对于Python程序有如下的约定。
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
本书提供源代码下载。要获得以上配套资源,请在异步社区本书页面中点击 ,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/ submission即可)。
如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
第1章 开发环境安装
第2章 PyQt5 GUI程序框架
本书介绍如何在Python中使用PyQt5进行图形用户界面(Graphical User Interface,GUI)应用程序开发,使用的编程语言是Python,构建开发环境需要安装的软件有Python 3、Qt 5和PyQt5。
本章介绍各个软件的功能特点、安装和基本使用方法,以及构建本书介绍内容所需的开发环境。本书所有程序都是在64位Windows 7平台上开发的,但由于Python和Qt都是跨平台的,因此所介绍的内容在Linux等平台上也是适用的。
Python是由Guido van Rossum在1989年开发,然后在1991年初发布的。Python是一种跨平台的解释型语言,它功能强大,简单易学,具有面向对象编程的功能。Python是完全开源的软件,具有开放的特性,能很方便地将其他语言(尤其是C/C++)的类库封装为Python的模块来使用。
由于Python语言的特点,以及其开源和开放的特性,吸引了编程社区为Python开发了很多实用且功能强大的包(package),例如用于矩阵处理和线性代数计算的NumPy,用于科学计算的SciPy,用于数据分析的Pandas,用于数据可视化的Matplotlib等,这使得Python在科学计算、数据分析、数据可视化、神经网络、人工智能、Web编程等各方面得到了广泛的应用,逐渐成为一种主流的编程语言。
Python是一个完全开源的软件,从官网上可以下载最新版本的Python安装文件。Python 3和Python 2是不兼容的,本书就不考虑Python 2了,直接下载最新的发布版本Python 3.7.0。
Python是跨平台的,有Windows、Linux、macOS等各种平台的安装文件。本书的示例程序都是在64位的Windows 7平台上开发的,所以下载64位Windows平台的离线安装文件。Python的安装过程与一般的Windows程序安装过程一样,在一个安装向导里完成安装过程。
安装向导的第一步如图1-1所示。在此窗口里勾选“Add Python 3.7 to PATH”,会自动将安装后的Python的两个文件夹路径添加到Windows系统的环境变量PATH里,这样就可以在Windows的cmd窗口里直接执行Python的一些工具程序,如python.exe、pyuic5.exe等。
在图1-1中点击“Customize installation”进行定制安装,出现的窗口如图1-2所示,在此窗口中勾选所有选项。其中,pip默认是不勾选的,一定要勾选此选项。pip是Python的包管理工具程序,使用pip可以很方便地下载和安装各种第三方的Python包,包括后面用到的PyQt5、PyQtChart等,都需要通过pip安装。
图1-1 Python安装向导第一步
图1-2 Python安装向导第二步
继续按照向导提示完成安装。这里设置Python安装到“D:\Python37”目录下,这个目录下有Python的主程序文件python.exe和pythonw.exe。
文件夹“D:\Python37\Scripts”下存放的是Python的一些工具软件,如pip.exe和pip3.exe。在安装其他一些第三方模块或工具软件后,可执行文件都安装到此目录下,例如安装PyQt5之后,会在此目录下增加3个可执行文件。
路径“D:\Python37”和“D:\Python37\Scripts”会被安装程序自动添加到Windows系统的PATH环境变量里,这两个目录下的文件就可以在Windows的cmd窗口里直接执行。如果在安装的第一步(图1-1)中没有勾选“Add Python 3.7 to PATH”,那么这两个路径不会自动添加到PATH环境变量里,需要在安装后手动添加。
Python安装后有一个交互式操作环境IDLE,其运行时界面如图1-3所示。在此交互式操作环境里,可以执行Python的各种语句。
在图1-3窗口的“File”菜单下,点击“New File”,可以打开一个文件编辑器,在这个编辑器里可以编写Python程序,然后保存为后缀为“.py”的文件。例如,在图1-4的窗口中简单地输入了两行语句,然后保存为文件hello.py。
点击图1-4文件编辑器的菜单项“Run”→“Run Module”,或直接按快捷键F5执行此程序,在交互式窗口里就会输出运行结果。
IDLE的功能比较简单,不像其他一些Python IDE(如Eric、PyCharm)功能那么强大,但是基本的程序编辑和调试功能是具备的。IDLE对于初学者来说简单易用,编写和调试规模不大的程序是够用的,因此本书就使用IDLE作为Python开发环境。
IDLE的文件编辑器有以下一些常用的快捷键非常有用。
图1-3 Python自带的IDLE交互式操作环境
图1-4 Python程序文件编辑器
因为Python源程序是采用缩进确定代码段的,排版时为减少缩进空格数和缩进层级,本书设置TAB为3个空格(点击IDLE的“Options”→“Configure IDLE”菜单项进行设置),并且在程序中也基本不使用try...except和try...finally等语句块。
IDLE也具有程序调试功能。在文件编辑器中打开需要调试的源程序文件,通过鼠标右键快捷菜单在当前行设置或取消断点。在IDLE交互环境中,点击“Debug”→“Debugger”菜单项,出现如图1-5所示的调试控制窗口。按F5开始运行程序后,就进入调试状态。在调试状态下,使用图1-5窗口上的“Go”“Step”“Over”等按钮进行程序调试。程序调试的方法与一般IDE的程序调试方法类似,这里就不详细介绍了。
图1-5 程序调试控制窗口
本书不对Python语言基础做介绍,假定读者已熟悉Python语言编程的基本方法,掌握了Python中类的使用方法。如果读者对Python的基本编程不熟悉,需要找一本专门介绍Python编程基础的书学习后再来学习本书的内容。
除了Python自带的IDLE,还有许多其他用于Python编程的IDE,如PyCharm、Eric等。本书的示例程序都用IDLE编程和调试,如果读者习惯于使用其他的IDE,也可以使用自己习惯的编程环境。因为Python是解释型语言,无须编译,所以无论使用哪个IDE都可以实现Python程序的编写和运行。
Python的一大特点就是有大量的包(package)可供使用,而且都是开源的。PyPI(Python Package Index)网站就是Python程序资源的集散地,在这个网站上可以查找、下载、发布Python包。
在Windows的cmd窗口里使用pip3指令可以直接从PyPI网站下载包并安装。例如,SIP是一个用于将C++库转换为Python扩展模块的工具软件,这种扩展模块称为C++库的Python绑定(binding)。要安装SIP只需在Windows的cmd窗口里执行如下的指令:
pip3 install sip
这条指令中的pip3就是“D:\Python37\Scripts”目录下的程序pip3.exe;install是指令参数,表示安装,相应地,卸载用uninstall;sip是需要安装的包的名称。
执行这条指令时,pip3会自动链接到PyPI网站上,查找最新版本的SIP,如果找到就自动下载并安装。成功安装后,在“D:\Python37\Lib\site-packages”目录下会出现SIP相关的子目录和文件,该目录下存放的都是安装的Python包。
如果要卸载已安装的SIP,执行下面的指令即可。
pip3 uninstall sip
如果直接链接国外的PyPI服务器速度比较慢,可以在pip3指令中指定使用镜像服务器。例如,使用清华的镜像服务器安装SIP的指令是:
pip3 install –i https://pypi.tuna.tsinghua.edu.cn/simple sip
Qt是一个跨平台的应用程序C++开发类库,支持Windows、Linux、macOS等各种桌面平台,也支持iOS、Android等移动平台,还支持各种嵌入式系统,是应用非常广泛的跨平台C++开发类库。
Qt最早是由挪威的Haavard Nord和Eirik Chambe-Eng在1991年开始开发的,在1994年发布,并成立了一家名为Trolltech的公司。Trolltech公司在2008年被诺基亚公司收购,2012年,Qt被Digia公司收购,2014年从Digia公司拆分出来成立了独立的Qt公司,专门进行Qt的开发、维护和商业推广。
Qt的许可类型分为商业许可和开源许可,开源许可的Qt就已经包含非常丰富的功能模块,可用于Qt学习和一般的应用程序开发。
在Python中使用PyQt5编写程序可以只安装PyQt5,而不必安装Qt的开发环境。但是为了使用Qt的IDE(即Qt Creator)的一些功能如UI窗体可视化设计、Qt类库帮助信息查找、资源文件管理等,安装Qt是有必要的。
从Qt官网可以下载最新版本的Qt软件。Qt分为商业版和社区版,社区版就是具有开源许可协议的免费版本。Qt的版本更新比较快,本书使用的是Qt 5.12.0,这是一个LTS(Long Term Supported)版本。
下载的Windows平台的Qt离线安装文件是一个可执行文件,运行文件就可以开始安装。安装过程与一般的Windows应用程序安装过程一样,按照向导进行操作即可。
在设置安装路径时,选择安装到“D:\Qt\Qt5.12.0”目录下,当然也可以安装在其他路径下。设置为这个路径,是为了在后面需要讲到Qt的路径时使用此绝对路径。
在安装过程中会出现如图 1-6 所示的安装模块选项设置页面,在这个页面里选择需要安装的模块。“Qt 5.12.0”节点下面是Qt的功能模块,包括用于不同编译器和平台的模块,这些模块如下。
图1-6 Qt安装模块选项设置
“Tools”节点下面是一些工具软件,这些软件如下。
安装完成后,在Windows“开始”菜单里建立的“Qt 5.12.0”程序组内容如图1-7所示。程序组中的一个主要程序是Qt Creator 4.8.0(Enterprise),它是用于开发Qt程序的IDE,是Qt的主要工具软件。
根据选择安装的编译器模块会建立相应的几个子分组,例如,在图1-7中有两个版本的编译器模块,即MinGW 7.3.0(64-bit)和MSVC 2015(64-bit),每个分组下面都有以下3个工具软件。
图1-7 安装后“开始”菜单里的“Qt 5.12.0”程序组
本书对使用的Qt Creator做了一些设置:一是将界面语言设置为英语,因为初始的汉语界面中某些词汇翻译得并不恰当,使用英语界面会更准确一些;二是设置文件命名规则,取消默认的全小写命名文件规则。
启动Qt Creator后,点击Qt Creator菜单栏的“Tools”→“Options…”菜单项打开如图1-8所示的选项设置对话框。
界面语言设置:点击对话框左侧Environment分组后,在Interface页面将界面语言设置为English,设置主题为Flat Light。更改语言和主题后需要重新启动Qt Creator才会生效。
文件命名规则设置:在图1-8所示的C++分组的File Naming页,取消“Lower case file names”选项。默认是勾选此项的,即自动命名的文件名采用全小写字母。
在本书里用Qt Creator只需要进行窗体可视化设计、生成槽函数代码框架、查阅Qt帮助文档,而不需要使用Qt Creator编写任何C++程序。Qt Creator的具体使用在下一章详细介绍。
图1-8 Qt Creator的选项设置对话框
Python语言功能很强,但是Python自带的GUI开发库Tkinter功能很弱,难以开发出专业的GUI。好在Python语言的开放性,很容易将其他语言(特别是C/C++)的类库封装为Python绑定,而Qt是非常优秀的C++ GUI类库,所以就有了PyQt。
PyQt是Qt C++类库的Python绑定,PyQt5对应于Qt5类库。Qt推出新的版本后,PyQt就会推出跟进的版本,例如针对Qt 5.12.0就有PyQt 5.12。使用PyQt5可以充分利用Qt的应用程序开发框架和功能丰富的类设计GUI程序。PyQt主要有以下一些优点。
PyQt5是Riverbank公司的产品,分为开源版本和商业版本,开源版本就包含全部的功能。Riverbank公司不仅开发了PyQt5,还开发了PyQtChart、PyQtDataVisualization、PyQt3D、SIP等软件包。可以在Riverbank公司网站上下载这些软件包的源代码,在PyPI网站上也可以找到这些软件包,所以可以使用pip3指令直接安装。
在PyPI网站上可以找到最新版本的PyQt5,直接用下面的指令安装PyQt5。
pip3 install PyQt5
直接连接PyPI服务器可能速度比较慢,可以使用镜像网站安装,例如使用清华大学镜像网站的指令是:
pip3 install –i https://pypi.tuna.tsinghua.edu.cn/simple PyQt5
这条指令正确执行后就会安装PyQt5,并且会自动安装依赖的包SIP。SIP是一个将C/C++库转换为Python绑定的工具,SIP本来是为了开发PyQt而开发的,现在也可以用于将任何C/C++库转换为Python绑定。
安装PyQt5之后,在“D:\Python37\Scripts”目录下增加了pylupdate5.exe、pyrcc5.exe和pyuic5.exe这3个用于PyQt5的可执行程序,如图1-9所示。这3个可执行程序的作用分别如下。
图1-9 安装PyQt5之后的Scripts子目录下的可执行文件
路径“D:\Python37\Scripts”被添加到了Windows的PATH环境变量里,所以这些程序在cmd窗口里可以直接运行。
若想要卸载PyQt5,就执行下面的指令:
pip3 uninstall PyQt5
在安装了PyQt5之后,可以在IDLE中开启代码提示功能,并且添加PyQt5的所有模块,这样在编写程序时,IDLE就具有一定的代码提示功能。
首先编辑目录“D:\Python37\Lib\idlelib”下的文件config-extensions.def,修改[AutoComplete]部分:
[AutoComplete]
enable=True
popupwait= 20
这表示开启自动提示功能,提示开启的延迟时间是20毫秒。
然后再编辑同一目录下的文件autocomplete.py,在文件的import部分导入PyQt5的各个模块:
import os
import string
import sys
##添加需要自动提示的模块
import PyQt5.QtWidgets
import PyQt5.QtCore
import PyQt5.QtGui
import PyQt5.QtSql
import PyQt5.QtMultimedia
import PyQt5.QtMultimediaWidgets
import PyQt5.QtChart
import PyQt5.QtDataVisualization
这样就将 PyQt5 的各个常用模块以及第 12 章和第 13 章要单独安装的PyQtChart和PyQtDataVisualization加入了可提示模块列表。
开启和设置自动提示功能后,在IDLE中使用PyQt5各模块中的类时就会有代码提示功能。但是IDLE的代码提示功能比较弱,不如PyCharm、Eric等专业IDE软件。
本章介绍PyQt5编写GUI程序的基本原理和主要技术点,包括GUI应用程序的基本框架、UI Designer可视化设计窗体的方法、窗体文件如何转换为Python文件并使用和Qt的信号与槽技术的使用方法等。掌握了本章的内容,就掌握了PyQt5设计GUI程序的框架性原理,再学习后面的各章内容就基本上是学习各种类的使用方法了。
本节先通过一个简单的示例程序介绍PyQt5 GUI应用程序的基本框架。
启动Python自带的编程和交互式环境IDLE,点击“File”→“New File”菜单项,打开一个文件编辑窗口,在此窗口中输入下面的程序,并保存为文件demo2_1Hello.py,此文件保存在随书示例Demo2_1目录下。
## demo2_1Hello.py
## 使用PyQt5,纯代码化创建一个简单的GUI程序
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
app = QtWidgets.QApplication(sys.argv) #创建app,用QApplication类
widgetHello = QtWidgets.QWidget() #创建窗体,用QWidget类
widgetHello.resize(280,150) #设置窗体的宽度和高度
widgetHello.setWindowTitle("Demo2_1") #设置窗体的标题文字
LabHello = QtWidgets.QLabel(widgetHello) #创建标签,父容器为widgetHello
LabHello.setText("Hello World,PyQt5") #设置标签文字
font = QtGui.QFont() #创建字体对象font,用QFont类
font.setPointSize(12) #设置字体大小
font.setBold(True) #设置为粗体
LabHello.setFont(font) #设置为标签LabHello的字体
size=LabHello.sizeHint() #获取LabHello的合适大小,返回值是QSize类对象
LabHello.setGeometry(70, 60, size.width(), size.height())
widgetHello.show() #显示对话框
sys.exit(app.exec_()) #应用程序运行
程序输入完成后,在程序编辑器窗口中点击“Run”→“Run Module”菜单项,或直接按快捷键F5就可以运行程序,会出现图2-1所示的窗口。
图2-1 文件demo2_1Hello.py运行结果窗口
这是一个典型的GUI应用程序。观察文件demo2_1Hello.py的代码,并结合程序中的注释,可以看出此程序的基本工作原理。
(1)首先导入了PyQt5包中的一些模块,包括QtCore,QtGui,QtWidgets,其中每个模块都包含了一些类。
(2)用下面的语句创建了一个应用程序。
app = QtWidgets.QApplication(sys.argv)
这里用到了QtWidgets模块中的QApplication类。QApplication是管理GUI应用程序的控制流程和设置的类,这里创建的应用程序对象是app。
(3)使用QtWidgets模块中的QWidget类创建了窗体对象widgetHello,然后调用QWidget类的resize()函数设置窗体大小,调用setWindowTitle()函数设置窗体标题。
(4) 使用QtWidgets模块中的QLabel类创建了一个标签对象LabHello,创建LabHello的语句是:
LabHello = QtWidgets.QLabel(widgetHello)
这里将widgetHello作为参数传递给QLabel的构造函数,实际是指定widgetHello作为LabHello的父容器,这样标签LabHello才会显示在窗体widgetHello上。
后面的代码用QLabel的接口函数setText()设置标签的文字,又创建了一个QFont对象用于设置标签的字体,还设置了标签在窗体上的位置和大小。
(5)窗体显示和程序运行。
窗体widgetHello和文字标签LabHello创建并设置好各种属性后,就显示窗体并运行应用程序,即程序中的最后两行语句:
widgetHello.show()
sys.exit(app.exec_())
这里的窗体widgetHello是应用程序的主窗体,应用程序运行后开始消息管理。
这个示例程序演示了使用PyQt5的一些类创建GUI程序的基本过程。首先需要用QApplication类创建一个应用程序实例,然后创建一个窗体(窗体类主要有QWidget、QDialog、QMainWindow),再创建界面组件(例如一个QLabel组件)并在窗体上显示,最后是显示窗体并开始应用程序的消息循环。这个程序虽然功能很简单,只显示了一个带标签的窗口,关闭窗口还需要点击窗口右上角的关闭按钮,但它已经是一个标准的GUI应用程序。
提示 从上面的程序中可以看出,PyQt5中的类都是以大写字母Q开头命名的,如QWidget、QApplication、QLabel等,这样的命名规则很容易将PyQt5的类与其他的类或变量区分开来。
示例Demo2_1用PyQt5的一些类创建了一个简单的GUI应用程序,窗体及窗体上的标签对象的创建和属性设置都完全由代码完成。显然这种纯代码方式构造UI的方式是比较麻烦的,特别是在窗体上组件比较多、层次比较复杂的时候,纯代码方式构造界面的工作量和难度可想而知。
Qt提供了可视化界面设计工具Qt Designer,以及Qt Creator中内置的UI Designer。可视化地设计UI窗体可以大大提高GUI应用程序开发的工作效率。
本节通过示例Demo2_2演示如何用UI Designer可视化设计UI窗体,然后转换为Python程序,再构建为Python的GUI程序。主要工作步骤如下。
(1)在UI Designer中可视化设计窗体。
(2)用工具软件pyuic5将窗体文件(.ui文件)转换为Python程序文件。
(3)使用转换后的窗体的Python类构建GUI应用程序。
在Qt Creator中点击菜单项“File”→“New File or Project...”,在出现的对话框里选择“Qt”分组里的“Qt Designer Form”(如图2-2所示),这将创建一个单纯的窗体文件(.ui文件)。
在图2-2的对话框中点击“Choose...”按钮后,出现如图2-3所示的窗体模板选择界面。窗体模板主要有以下3种。
图2-2 新建窗体对话框
图2-3 选择Widget模板
在图2-3的界面上选择Widget模板。点击“Next”按钮后,在出现的对话框里设置文件名为FormHello.ui,文件保存到Demo2_2的目录下,再根据向导提示完成创建即可。创建了窗体后就可以在Qt Creator内置的UI Designer里可视化设计窗体,图2-4是在窗体上放置了标签和按钮,并设置好各种属性后的界面。
图2-4的UI Designer窗口有以下一些功能区域。
主窗口上方有窗体设计模式和布局管理工具栏,最左侧还有一个工具栏,这些功能在后面详细介绍Qt Creator的使用时再具体介绍。
在设计窗体上用鼠标点选一个组件,在属性编辑器里会显示其各种属性,并且可以修改其属性。例如,图2-5是选中窗体上放置的标签组件后属性编辑器的内容。
图2-4 在Qt Creator里可视化设计窗体
图2-5 界面组件的属性编辑器
图2-5展示的属性编辑器的最上方显示的文字“LabHello: QLabel”表示这个组件是一个QLabel类的组件,objectName是LabHello。属性编辑器的内容分为两列,其中Property列是属性的名称,Value列是属性的值。属性又分为多个组,实际上表示了类的继承关系,例如在图2-5中,可以看出QLabel的继承关系是QObject→QWidget→QFrame→QLabel。
objectName是组件的对象名称,界面上的每个组件都需要一个唯一的对象名称,以便被引用。界面上的组件的命名应该遵循一定的法则,具体使用什么样的命名法则根据个人习惯而定,主要目的是便于区分和记忆,也要便于与普通变量相区分。
设置组件属性的值只需在属性编辑器里进行修改即可,例如设置LabHello的text属性为“Hello, by UI Designer”,只需如图2-5所示那样修改text属性的值即可。
表2-1是所设计的窗体,以及窗体上的标签和按钮的主要属性的设置。
表2-1 窗体以及各组件的主要属性设置
objectName |
类名称 |
属性设置 |
备注 |
---|---|---|---|
FormHello |
QWidget |
windowTitle="Demo2_2" |
设置窗体的标题栏显示文字 |
btnClose |
QPushButton |
Text="关闭" |
设置按钮的显示文字 |
LabHello |
QLabel |
Text="Hello, by UI Designer" |
设置标签显示文字和字体 |
窗体设计完成后,将这个窗体保存为文件FormHello.ui。
提示 一般情况下,保存的.ui文件名与窗体的objectName名称一致,这样通过文件名就可以直接知道窗体的名称。
窗体文件FormHello.ui实际上是一个XML文件,它记录了窗体上各组件的属性以及位置分布。FormHello.ui的XML文件内容不必去深入研究,它是由UI Designer根据可视化设计的窗体自动生成的。使用IDLE的文件编辑器就可以打开FormHello.ui文件,下面是FormHello.ui文件的内容。
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FormHello</class>
<widget class="QWidget" name="FormHello">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>283</width>
<height>156</height>
</rect>
</property>
<property name="windowTitle">
<string>Demo2_2</string>
</property>
<widget class="QLabel" name="LabHello">
<property name="geometry">
<rect>
<x>50</x>
<y>40</y>
<width>189</width>
<height>16</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Hello, by UI Designer</string>
</property>
</widget>
<widget class="QPushButton" name="btnClose">
<property name="geometry">
<rect>
<x>100</x>
<y>90</y>
<width>75</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>关闭</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
使用UI Designer设计好窗体并保存为文件FormHello.ui后,要在Python里使用这个窗体,需要使用PyQt5的工具软件pyuic5.exe将这个ui文件编译转换为对应的Python语言程序文件。
pyuic5.exe程序位于Python安装目录的Scripts子目录下,如“D:\Python37\Scripts”,这个路径在安装Python时被自动添加到了系统的PATH环境变量里,所以可以直接执行pyuic5命令。
在Windows的cmd窗口里用cd指令切换到文件FormHello.ui所在的目录,然后用pyuic5指令编译转换为Python文件。例如,假设文件FormHello.ui保存在目录“G:\PyQt5Book\Demo\chap02\demo2_2”下,依次执行下面的指令:
cd G:\PyQt5Book\Demo\chap02\demo2_2
pyuic5 –o ui_FormHello.py FormHello.ui
其中,pyuic5的作用是将文件FormHello.ui编译后输出为文件ui_FormHello.py。编译输出的文件名可以任意指定,在原来的文件名前面加“ui_”是个人命名习惯,表明ui_FormHello.py 文件是从FormHello.ui文件转换来的。
为了避免重复地在cmd窗口里输入上述指令,可以创建一个文件uic.bat保存到项目Demo2_2的目录下。bat文件是Windows的批处理文件,uic.bat文件的内容只有一条语句,如下:
pyuic5 -o ui_FormHello.py FormHello.ui
在Windows资源管理器里双击uic.bat文件就会执行该文件里的语句,也就是将文件FormHello.ui编译为ui_FormHello.py。
编译后在FormHello.ui文件所在的同目录下生成了一个文件ui_FormHello.py,用IDLE的文件编辑器打开这个文件,其内容如下:
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'FormHello.ui'
#
# Created by: PyQt5 UI code generator 5.12
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_FormHello(object):
def setupUi(self, FormHello):
FormHello.setObjectName("FormHello")
FormHello.resize(283, 156)
self.LabHello = QtWidgets.QLabel(FormHello)
self.LabHello.setGeometry(QtCore.QRect(50, 40, 189, 16))
font = QtGui.QFont()
font.setPointSize(12)
font.setBold(True)
font.setWeight(75)
self.LabHello.setFont(font)
self.LabHello.setObjectName("LabHello")
self.btnClose = QtWidgets.QPushButton(FormHello)
self.btnClose.setGeometry(QtCore.QRect(100, 90, 75, 23))
self.btnClose.setObjectName("btnClose")
self.retranslateUi(FormHello)
QtCore.QMetaObject.connectSlotsByName(FormHello)
def retranslateUi(self, FormHello):
_translate = QtCore.QCoreApplication.translate
FormHello.setWindowTitle(_translate("FormHello", "Demo2_2"))
self.LabHello.setText(_translate("FormHello",
"Hello, by UI Designer"))
self.btnClose.setText(_translate("FormHello", "关闭"))
分析这个文件的代码,可以发现这个文件实际上定义了一个类Ui_FormHello,仔细分析一下这段代码,可以发现其原理和功能。
(1)Ui_FormHello类的父类是object,而不是QWidget。
(2)Ui_FormHello类定义了一个函数setupUi(),其接口定义为:
def setupUi(self, FormHello)
其传入的参数有两个,其中self是函数自己,Python中的self类似于C++语言中的this;FormHello是一个传入的参数,而不是在Ui_FormHello类里定义的一个变量。
setupUi()函数的前两行语句是:
FormHello.setObjectName("FormHello")
FormHello.resize(283, 156)
所以,FormHello是窗体,是一个QWidget对象,其名称就是在UI Designer里设计的窗体的objectName。但是这个FormHello不是在类Ui_FormHello里创建的,而是作为一个参数传入的。
(3)创建了一个QLabel类型的对象LabHello,创建的语句是:
self.LabHello = QtWidgets.QLabel(FormHello)
LabHello定义为Ui_FormHello类的一个公共属性(类似于C++的公共变量),它的父容器是FormHello,所以LabHello在窗体FormHello上显示。后面的语句又设置了LabHello的显示位置、大小,以及字体属性。
提示
在Python语言中,类的接口包括属性(attribute)和方法(method),属性又分为类属性和类的实例属性。Python的类属性类似于C++中类的静态变量,类的实例属性类似于C++中类的成员变量。Qt C++中的属性是指用Q_PROPERTY宏定义了读写函数的类的接口元素,类似于Python中用@property修饰符定义了读写函数的实例属性。
不管是否为属性定义了读写函数,Python类中的实例属性都可以当作一个变量来访问。在本书中,为了与定义了读写函数的属性区分开来,也为了明确概念,将自定义类中的实例数据型属性(也就是类似于C++类中的成员变量)有时也称为变量,特别是一些简单类型的数据属性。
(4)创建了一个QPushButton类型的对象btnClose,创建的语句是
self.btnClose = QtWidgets.QPushButton(FormHello)
btnClose也是Ui_FormHello类的一个公共属性,它的父容器是FormHello,所以在窗体上显示。
(5)setupUi()函数的倒数第二行调用了Ui_FormHello类里定义的另外一个函数retranslateUi(),这个函数设置了窗体的标题、标签LabHello的文字、按钮btnClose的标题。实际上,retranslateUi()函数集中设置了窗体上所有的字符串,利于实现软件的多语言界面。
(6)setupUi()函数的最后一行语句用于窗体上各组件的信号与槽函数的自动连接,在后面介绍信号与槽时再详细解释其功能。
所以,经过pyuic5编译后,FormHello.ui文件转换为一个对应的Python的类定义文件ui_FormHello.py,类的名称是Ui_FormHello。有如下的特点和功能。
(1)Ui_FormHello.py文件里的类名称Ui_FormHello与FormHello.ui文件里窗体的objectName有关,是在窗体的objectName名称前面加“Ui_”自动生成的。
(2)Ui_FormHello类的函数setupUi()用于窗体的初始化,它创建了窗体上的所有组件并设置其属性。
(3)Ui_FormHello类并不创建窗体FormHello,窗体FormHello是由外部传入的,作为所有界面组件的父容器。
注意
ui_FormHello.py文件只是定义了一个类Ui_FormHello,这个文件并不能直接运行,而是需要在其他地方编程使用这个文件里定义的类Ui_FormHello。
将窗体UI文件FormHello.ui编译转换为Python的类定义文件ui_FormHello.py后,就可以使用其中的类Ui_FormHello创建GUI应用程序。编写一个程序文件appMain1.py,它演示了使用Ui_FormHello类创建GUI应用程序的基本框架,其代码如下:
## appMain1.py
## 使用ui_FormHello.py文件中的类Ui_FormHello创建app
import sys
from PyQt5 import QtWidgets
import ui_FormHello
app = QtWidgets.QApplication(sys.argv)
baseWidget=QtWidgets.QWidget() #创建窗体的基类QWidget的实例
ui =ui_FormHello.Ui_FormHello()
ui.setupUi(baseWidget) #以baseWidget作为传递参数,创建完整窗体
baseWidget.show()
##ui.LabHello.setText("Hello,被程序修改") #可以修改窗体上标签的文字
sys.exit(app.exec_())
分析上面的代码,可以了解GUI程序创建和运行的过程。
(1)首先用QApplication类创建了应用程序实例app。
(2)创建了一个QWidget类的对象baseWidget,它是基本的QWidget窗体,没有做任何设置。
(3)使用ui_FormHello模块中的类Ui_FormHello创建了一个对象ui。
(4)调用了Ui_FormHello类的setupUi()函数,并且将baseWidget作为参数传入:
ui.setupUi(baseWidget)
根据前面的分析,Ui_FormHello类的setupUi()函数只创建窗体上的其他组件,而作为容器的窗体是靠外部传入的,这里的baseWidget就是作为一个基本的QWidget窗体传入的。执行这条语句后,就在窗体baseWidget上创建了标签和按钮。
(5)显示窗体,使用的语句是:
baseWidget.show()
注意,这里不能使用ui.show(),因为ui是Ui_FormHello类的对象,而Ui_FormHello的父类是object,根本就不是Qt的窗体界面类。
程序运行后的结果窗口如图2-6所示,这就是在UI Designer里设计的窗体。这个程序只是简单地实现了窗体的显示,“关闭”按钮并不能关闭窗口,在后面介绍信号与槽时再实现其功能。
那么现在有个问题,窗体上的标签、按钮对象如何访问呢?例如,若需要修改标签的显示文字,该如何修改呢?
分析一下程序,窗体上的标签对象LabHello是在Ui_FormHello类里定义的公共属性,所以在程序里可以通过ui对象访问LabHello。
对appMain1.py文件稍作修改,在baseWidget.show()语句后加入一条语句,如下(省略了前后的语句):
baseWidget.show()
ui.LabHello.setText("Hello,被程序修改")
再运行appMain1.py,结果窗口如图2-7所示,说明上面修改标签文字的语句是有效的。在上面的修改标签文字的语句中,不能将ui替换为baseWidget,即下面的语句是错误的:
baseWidget.LabHello.setText("Hello,被程序修改") #错误的
这是因为baseWidget是QWidget类型的对象,它只是LabHello的父容器,并没有定义公共属性LabHello,所以运行时会出错。而ui是Ui_FormHello类的实例对象,窗体上的所有界面组件都是ui的实例属性。因此,访问窗体上的界面组件只能通过ui对象。
图2-6 appMain1.py运行结果窗口
图2-7 程序中访问窗体的标签对象,修改了其显示文字
分析前面的程序appMain1.py,虽然它实现了对Ui_FormHello类的使用,生成了GUI程序,但是它是存在一些缺陷的,原因在于appMain1.py完全是一个过程化的程序。它创建了Ui_FormHello类的对象ui,通过这个对象可以访问界面上的所有组件,所以,ui可以用于界面交互,获取界面输入,将结果输出到界面上。程序创建的baseWidget是QWidget类的对象,它不包含任何处理逻辑,而仅仅是为了调用ui.setupUi()函数时作为一个传入的参数。一般的程序是从界面上读取输入数据,经过业务处理后再将结果输出到界面上,那么这些业务处理的代码放在哪里呢?
appMain1.py的应用程序框架只适合测试单个窗体的UI设计效果,也就是仅能显示窗体。若要基于UI窗体设计更多的业务逻辑,由于appMain1.py是一个过程化的程序,难以实现业务逻辑功能的有效封装。
界面与业务逻辑分离的设计方法不是唯一的,这里介绍两种方法,一种是多继承方法,另一种是单继承方法。
Python的面向对象编程支持使用多继承,编写一个程序appMain2.py,代码如下:
## appMain2.py 多继承方法
import sys
from PyQt5.QtWidgets import QWidget, QApplication
from ui_FormHello import Ui_FormHello
class QmyWidget(QWidget,Ui_FormHello):
def __init__(self, parent=None):
super().__init__(parent) #调用父类构造函数,创建QWidget窗体
self.Lab="多重继承的QmyWidget" #新定义的一个变量
self.setupUi(self) #self是QWidget窗体,可作为参数传给setupUi()
self.LabHello.setText(self.Lab)
if __name__ == "__main__":
app = QApplication(sys.argv) #创建app
myWidget=QmyWidget()
myWidget.show()
myWidget.btnClose.setText("不关闭了")
sys.exit(app.exec_())
这个程序的运行结果如图2-8所示。分析这段代码,可以发现它的实现原理。
图2-8 程序appMain2.py运行结果
(1)采用多继承的方式定义了一个类QmyWidget,称这个类为窗体的业务逻辑类,它的父类是QWidget和Ui_FormHello。
(2)在这个类的构造函数中,首先用函数super()获取父类,并执行父类的构造函数,代码是:
super().__init__(parent)
在多继承时,使用super()得到的是第一个基类,在这里就是QWidget。所以,执行这条语句后,self就是一个QWidget对象。
(3)调用setupUi()函数创建UI窗体,即
self.setupUi(self)
因为QmyWidget的基类包括Ui_FormHello类,所以可以调用Ui_FormHello类的setupUi()函数。同时,经过前面调用父类的构造函数,self是一个QWidget对象,可以作为参数传递给setupUi()函数,正好作为各组件的窗体容器。
通过这样的多继承,Ui_FormHello类中定义的窗体上的所有界面组件对象就变成了新定义的类QmyWidget的公共属性,可以直接访问这些界面组件。例如,在QmyWidget类的构造函数里通过下面的语句设置了界面上的标签的显示文字:
self.Lab="多重继承的QmyWidget" #新定义的一个属性
self.LabHello.setText(self.Lab)
在应用程序创建QmyWidget类的实例对象myWidget后,通过下面的语句设置了界面上按钮的显示文字:
myWidget.btnClose.setText("不关闭了")
这种多继承方式有其优点,也有其缺点,表现为以下两方面。
(1)界面上的组件都成为窗体业务逻辑类QmyWidget的公共属性,外界可以直接访问。优点是访问方便,缺点是过于开放,不符合面向对象严格封装的设计思想。
(2)界面上的组件与QmyWidget类里新定义的属性混合在一起了,不便于区分。例如,在构造函数中有这样一条语句:
self.LabHello.setText(self.Lab)
其中,self.LabHello是窗体上的标签对象,而self.Lab是QmyWidget类里新定义的一个属性。如果没有明确的加以区分的命名规则,当窗体上的界面组件较多,且窗体业务逻辑类里定义的属性也很多时,就难以区分哪个属性是界面上的组件,哪个属性是在业务逻辑类里新定义的,这样是不利于界面与业务逻辑分离的。
针对多继承存在的一些问题,改用单继承的方法,编写另一个程序appMain.py,其代码如下:
## appMain.py 单继承方法,能更好地进行界面与逻辑的分离
import sys
from PyQt5.QtWidgets import QWidget, QApplication
from ui_FormHello import Ui_FormHello
class QmyWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent) #调用父类构造函数,创建QWidget窗体
self.__ui=Ui_FormHello() #创建UI对象
self.__ui.setupUi(self) #构造UI
self.Lab="单继承的QmyWidget"
self.__ui.LabHello.setText(self.Lab)
def setBtnText(self, aText):
self.__ui.btnClose.setText(aText)
if __name__ == "__main__":
app = QApplication(sys.argv) #创建app,用QApplication类
myWidget=QmyWidget()
myWidget.show()
myWidget.setBtnText("间接设置")
sys.exit(app.exec_())
这个程序的运行结果如图2-9所示。分析这段代码,可以看到以下几点。
图2-9 程序appMain.py运行结果
(1)新定义的窗体业务逻辑类QmyWidget只有一个基类QWidget。
(2)在QmyWidget的构造函数中,首先调用父类(也就是QWidget)的构造函数,这样self就是一个QWidget对象。
(3)显式地创建了一个Ui_FormHello类的私有属性self.__ui,即
self.__ui=Ui_FormHello() #创建UI对象
私有属性self.__ui包含了可视化设计的UI窗体上的所有组件,所以,只有通过self.__ui才可以访问窗体上的组件,包括调用其创建界面组件的setupUi()函数。
提示
Python语言的类定义通过命名规则来限定元素对外的可见性,属性或方法名称前有两个下划线表示是私有的,一个下划线表示模块内可见,没有下划线的就是公共的。
(4)由于self.__ui是QmyWidget类的私有属性,因此在应用程序中创建的QmyWidget对象myWidget不能直接访问myWidget.__ui,也就无法直接访问窗体上的界面组件。
为了访问窗体上的组件,可以在QmyWidget类里定义接口函数,例如函数setBtnText()用于设置窗体上按钮的文字。在应用程序里创建QmyWidget对象的实例myWidget,通过调用setBtnText()函数间接修改界面上按钮的文字,即
myWidget.setBtnText("间接设置")
仔细观察和分析这种单继承的方式,发现它有如下特点。
(1)可视化设计的窗体对象被定义为QmyWidget类的一个私有属性self.__ui,在QmyWidget类的内部对窗体上的组件的访问都通过这个属性实现,而外部无法直接访问窗体上的对象,这更符合面向对象封装隔离的设计思想。
(2)窗体上的组件不会与QmyWidget里定义的属性混淆。例如,下面的语句:
self.__ui.LabHello.setText(self.Lab)
self.__ui.LabHello表示窗体上的标签对象LabHello,它是self.__ui的一个属性;self.Lab是QmyWidget类里定义的一个属性。这样,窗体上的对象和QmyWidget类里新定义的属性不会混淆,有利于界面与业务逻辑的分离。
(3)当然,也可以定义界面对象为公共属性,即创建界面对象时用下面的语句:
self.ui=Ui_FormHello()
这里的ui就是个公共属性,在类的外部也可以通过属性ui直接访问界面上的组件。为了简化程序,在本书后面的示例程序中,都定义界面对象为公共属性self.ui。
对比多继承方法和单继承方法,可以发现单继承方法更有利于界面与业务逻辑分离。实际上,在Qt C++应用程序中默认就是采用的单继承方法,对Qt C++应用程序比较清楚的读者就很容易理解其工作原理了。
本书使用这种单继承和界面独立封装的方式,在后面的示例程序中都采用这种单继承的应用程序框架。
在这个示例中,窗口上虽然放置了一个按钮并显示“关闭”,但是运行时点击这个按钮并不能关闭窗口,这是因为我们还没有编写任何代码。这个示例只是为了演示如何在UI Designer里可视化设计UI窗体,再编译转换为对应的Python类,然后使用PyQt5里相关的类创建GUI应用程序的过程,以及GUI程序的框架和工作原理,下一节再重点介绍如何编写代码实现窗体的功能。
信号与槽(Signals/Slots)是Qt编程的基础,也是Qt的一大特色。因为有了信号与槽的编程机制,在Qt中处理界面组件的交互操作时变得比较直观和简单。
信号(Signal)就是在特定情况下被发射(emit)的一种通告,例如一个PushButton按钮最常见的信号就是鼠标单击时发射的clicked()信号,一个ComboBox最常见的信号是选择的项变化时发射的CurrentIndexChanged()信号。GUI程序设计的主要内容就是对界面上各组件发射的特定信号进行响应,只需要知道什么情况下发射了哪些信号,然后合理地去响应和处理这些信号就可以了。
槽(Slot)就是对信号响应的函数。槽实质上是一个函数,它可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数会被自动执行。Qt的类一般都有一些内建(build-in)的槽函数,例如QWidget有一个槽函数close(),其功能是关闭窗口。如果将一个PushButton按钮的clicked()信号与窗体的close()槽函数关联,那么点击按钮时就会关闭窗口。
本节通过一个完整的示例Demo2_3介绍信号与槽的使用方法,示例的主程序appMain.py的运行结果如图2-10所示。界面上的所有功能都是可以操作的。
图2-10 示例Demo2_3的主程序appMain.py运行结果窗体
这个示例的设计包含了PyQt5 GUI 应用程序设计的完整过程,以及涉及的一些关键技术问题,本节将逐步展开讲解这些问题。
(1)在UI Designer里设计窗体的布局,使界面上的各个组件合理地分布,并且随窗体大小变化而自动调整大小和位置。
(2) 在UI Designer里设计窗体时,设置组件的某个内建信号与窗体上其他组件的内建槽函数关联。
(3)在与UI窗体对应的业务逻辑类里,设计窗体组件内建信号的响应槽函数,并且与组件的信号关联起来。
(4)信号与槽设计和关联时的各种情况的处理方法。
详细地研究和实现这个示例后,基本上就掌握了用PyQt5编写GUI程序的完整流程。
Qt Creator是Qt的IDE,它可以管理、编译和调试Qt的C++项目。本节将用Qt Creator创建一个C++应用程序项目,主要是为了用Qt Creator内置的UI Designer可视化设计窗体,方便提取组件的信号并创建信号的槽函数原型,不需要编写C++程序,也无须对项目进行编译。
启动Qt Creator,创建一个名为QtApp的C++ GUI应用程序项目,步骤如下。
(1)点击Qt Creator的菜单项“File”→“New File or Project…”,出现如图2-11所示的对话框。在此对话框里选择Project类型为Application,中间的模板里选择Qt Widgets Application,这是常见的GUI应用程序项目。
(2)在图2-11的对话框中点击“Choose…”按钮,出现如图2-12所示的新建项目向导。在此对话框中,设置项目名称为QtApp,点击“Browse…”按钮选择示例Demo2_3所在的文件夹,例如“G:\PyQt5Book\Demo\chap02\ demo2_3”,这样创建的项目将自动保存在“G:\PyQt5Book\Demo\ chap02\demo2_3\QtApp”文件夹下。
图2-11 Qt Creator的新建项目对话框,选择新建Qt Widgets Application项目
(3)继续下一步,在出现选择编译工具的页面选择一个 Desktop Qt 5.12.0 MinGW 64-bit即可,因为不需要编译项目,选择哪一个都可以。
(4)继续下一步,出现如图2-13所示的创建UI窗体的界面,这个窗体将作为应用程序的主窗体。本示例的目的是创建一个对话框,所以选择基类QDialog,新窗体的类名称就使用默认的Dialog,这将自动创建3个文件,即Dialog.h、Dialog.cpp和Dialog.ui。注意一定要勾选“Generate form”旁边的复选框,否则不会创建文件Dialog.ui,也就不能进行窗体的可视化设计了。
图2-12 创建项目的名称设置为QtApp
图2-13 创建UI窗体的设置,选择基类QDialog
完成向导创建项目QtApp后,Qt Creator的界面如图2-14所示。Qt Creator的界面非常简洁,本书不需要用它来编写C++程序,而只是用于窗体可视化设计和槽函数原型生成,所以用到的功能较少。
窗体左侧是项目的文件管理目录树,与窗体相关的文件有以下3个。
主窗体左侧的工具栏上是一些功能按钮,“Edit”按钮用于切换到如图2-14所示的项目文件管理界面,“Design”按钮用于在有ui文件被打开时,切换到UI Designer设计界面,“Help”按钮用于切换到内置的Qt Assistant界面查看Qt的帮助文档。还有其他一些用于项目编译、调试和运行的按钮,本书未用到。
图2-14 Qt Creator项目管理与代码文件编辑器
该Qt项目的所有文件存放在Demo2_3目录的子目录“\QtApp”下,其中QtApp.pro是Qt项目文件。在Qt Creator里再次打开QtApp.pro文件时,就可以打开这个项目。
在图2-14的项目文件管理目录树上,双击文件Dialog.ui可以打开内置的UI Designer对窗体进行可视化设计,界面如图2-15所示。图中显示的是本示例已经设计好的窗体,在界面设计中使用了布局管理功能。窗体中间的文本框是一个PlainTextEdit组件,在组件面板的Input Widgets分组里。
图2-15 在Qt Creator内置的UI Designer里进行窗体可视化设计
在界面可视化设计时,对于需要在窗体业务逻辑类里访问的界面组件,修改其objectName,例如各个按钮、需要读取输入的编辑框、需要显示结果的标签等,以便在程序里加以区分。对于不需要在程序里访问的界面组件则无须修改其objectName,例如用于界面上组件分组的GroupBox、Frame、布局等,UI Designer自动命名即可。
对图2-15所设计窗体的主要组件的命名、属性设置如表2-2所示。
表2-2 Dialog.ui中各个组件的相关设置
对象名 |
类名称 |
属性设置 |
功能 |
---|---|---|---|
Dialog |
QDialog |
windowTitle=“Demo2-3信号与槽” |
窗体的类名称是Dialog,objectName不要修改 |
textEdit |
QPlainTextEdit |
Text=“PyQt5 编程指南\nPython 和 Qt.” |
用于显示文字,可编辑 |
chkBoxUnder |
QCheckBox |
Text=“Underline” |
设置字体的下划线特性 |
chkBoxItalic |
QCheckBox |
Text=“Italic” |
设置字体的斜体特性 |
chkBoxBold |
QCheckBox |
Text=“Bold” |
设置字体的粗体特性 |
radioBlack |
QRadioButton |
Text=“Black” |
设置字体颜色为黑色 |
radioRed |
QRadioButton |
Text=“Red” |
设置字体颜色为红色 |
radioBlue |
QRadioButton |
Text=“Blue” |
设置字体颜色为蓝色 |
btnClear |
QPushButton |
Text=“清空” |
清空文本框的内容 |
btnOK |
QPushButton |
Text=“确定” |
返回确定,并关闭窗口 |
btnClose |
QPushButton |
Text=“退出” |
退出程序 |
对于界面组件的属性设置,需要注意以下两点。
(1)objectName是窗体上的组件的实例名称,界面上的每个组件需要有一个唯一的objectName,程序里访问界面组件时都通过其objectName进行访问,自动生成的槽函数名称与objectName有关。所以,组件的objectName需要在设计程序之前设置好,设置好之后一般不再改动。若程序设计好之后再改动objectName,涉及的代码需要进行相应的改动。
(2)窗体的objectName是窗体的类名称,也就是利用向导新建窗体时设置的名称,在UI Designer里一般不修改窗体的objectName。
Qt的窗体设计具有布局(layout)功能。所谓布局,就是界面上的组件的排列方式。使用布局管理功能可以使组件有规则地分布,并且随着窗体大小变化自动地调整大小和相对位置。布局管理是GUI设计的必备技巧,下面逐步讲解如何实现图2-15的窗体。
为了将界面上的各个组件的分布设计得更加美观,经常使用一些容器类组件,如GroupBox、TabWidget、Frame等。例如,将3个CheckBox(复选框)组件放置在一个GroupBox组件里,这个GroupBox组件就是这3个复选框的容器,移动这个GroupBox就会同时移动其中的3个CheckBox。
图2-16是设计图2-15所示的窗体的前期阶段。在窗体上放置了两个GroupBox组件,其中在groupBox1里放置3个CheckBox组件,在groupBox2里放置3个RadioButton按钮。图2-16右侧Object Inspector里显示了界面上各组件之间的层次关系。
Qt为窗体设计提供了丰富的布局管理功能,在UI Designer里,组件面板里有Layouts和Spacers两个分组,在窗体上方的工具栏里有布局管理的按钮(如图2-17所示)。
图2-16 窗体上组件的放置与层次关系
图2-17 用于布局可视化设计的组件面板和工具栏
组件面板里Layouts和Spacers这两个分组里的布局组件的功能如表2-3所示。
表2-3 组件面板上用于布局的组件
布局组件 |
功能 |
---|---|
Vertical Layout |
垂直布局,组件自动在垂直方向上分布 |
Horizontal Layout |
水平布局,组件自动在水平方向上分布 |
Grid Layout |
网格状布局,网格状布局大小改变时,每个网格的大小都改变 |
Form Layout |
窗体布局,与网格状布局类似,但是只有最右侧的一列网格会改变大小 |
Horizontal Spacer |
一个用于水平分隔的空格 |
Vertical Spacer |
一个用于垂直分隔的空格 |
使用组件面板里的布局组件设计布局时,先拖放一个布局组件到窗体上,例如在设计图2-17窗体下方的3个按钮的布局时,先放一个Horizontal Layout到窗体上,布局组件会以红色矩形框显示。再向布局组件里拖放3个PushButton和两个Horizontal Spacer,就可以得到图2-17中3个按钮的水平布局效果。
每个布局还有layoutTopMargin、layoutBottomMargin、layoutLeftMargin、layoutRightMargin这 4 个属性用于调整布局边框与内部组件之间的上、下、左、右的边距大小。
在设计窗体的上方有一个工具栏,用于使界面进入不同的设计状态,以及进行布局设计,工具栏上各按钮的功能如表2-4所示。
表2-4 内置的UI Designer工具栏各按钮的功能
按钮及快捷键 |
功能 |
---|---|
Edit Widget (F3) |
界面设计进入编辑状态,也就是正常的设计状态 |
Edit Signals/Slots(F4) |
进入信号与槽的可视化设计状态 |
Edit Buddies |
进入伙伴关系编辑状态,可以设置一个Label与一个组件成为伙伴关系 |
Edit Tab Order |
进入Tab顺序编辑状态,Tab顺序是指在键盘上按Tab键时,输入焦点在界面各组件之间跳动的顺序 |
Lay Out Horizontally (Ctrl+H) |
将窗体上所选组件水平布局 |
Lay Out Vertically (Ctrl+L) |
将窗体上所选组件垂直布局 |
Lay Out Horizontally in Splitter |
将窗体上所选组件用一个分割条进行水平分割布局 |
Lay Out Vertically in Splitter |
将窗体上所选组件用一个分割条进行垂直分割布局 |
Lay Out in a Form Layout |
将窗体上所选组件按窗体布局 |
Lay Out in a Grid |
将窗体上所选组件按网格状布局 |
Break Layout |
解除窗体上所选组件的布局,也就是打散现有的布局 |
Adjust Size(Ctrl+J) |
自动调整所选组件的大小 |
使用工具栏上的布局设计按钮时,只需在窗体上选中需要设计布局的组件,然后点击某个布局按钮即可。在窗体上选择组件时同时按住Ctrl键,可以实现组件多选。选择某个容器类组件,相当于选择了其内部的所有组件。例如,在图2-16的窗体中,选中groupBox1,然后单击“Lay Out Horizontally”工具栏按钮,就可以对groupBox1内的3个复选框水平布局。
在图2-16的窗体上,使groupBox1里的3个复选框水平布局,groupBox2里的3个RadioButton按钮水平布局,下方放置的 3 个按钮水平布局,窗体上又放置一个PlainTextEdit组件。现在如果改变groupBox1、groupBox2或按钮的水平布局的大小,其内部组件会自动改变大小,但是如果改变窗体大小,界面上的各组件却并不会自动改变大小。
还需为窗体指定一个总的布局。选中窗体(即不选择任何组件),单击工具栏上的“Lay Out Vertically”按钮,使4个组件垂直分布。这样布局后,当窗体大小改变时,各个组件都会自动改变大小,且当窗体纵向增大时,只有中间的文本框增大,其他3个布局组件不增大。最终设计好的窗体的组件布局如图2-18所示,从图中可以清楚地看出组件的层次关系,以及布局的设置。
图2-18 设计好的窗体的组件布局与层次关系
在窗体可视化设计布局时,要善于利用水平和垂直空格组件,善于设置组件的最大、最小宽度或高度属性,善于设置布局的layoutStretch等属性来达到布局效果。
提示
窗体可视化设计和布局就是放置组件并合理地布局,设计过程如同拼图一般,设计的经验多了自然就熟悉了,所以本书后面的示例一般不会花篇幅来描述界面可视化设计的具体实现过程,读者看本书示例源程序里的窗体UI文件即可。
在UI Designer工具栏上单击“Edit Buddies”按钮可以进入伙伴关系编辑状态,例如设计一个窗体时,进入伙伴关系编辑状态之后如图2-19所示。
伙伴关系(Buddy)是指界面上一个Label和一个具有输入焦点的组件相关联,在图2-19的伙伴关系编辑状态,单击一个Label,按住鼠标左键,然后拖向一个组件,就建立了Label和组件之间的伙伴关系。
伙伴关系是为了在程序运行时,在窗体上用快捷键快速将输入焦点切换到某个组件上。例如,在图2-19的界面上,设定“姓名”标签的text属性为“姓 名(&N)”,其中符号“&”用来指定快捷字符,界面上并不显示“&”。这里指定快捷字母为N,那么程序运行时,如果用户按下Alt+N,输入焦点就会快速切换到“姓名”标签关联的文本框内。
在UI Designer工具栏上单击“Edit Tab Order”按钮进入Tab顺序编辑状态(如图2-20所示)。Tab顺序是指在程序运行时,按下键盘上的Tab键时输入焦点的移动顺序。一个好的用户界面,在按Tab键时焦点应该以合理的顺序在界面上移动。
进入Tab顺序编辑状态后,在界面上会显示具有Tab顺序的组件的Tab顺序编号,依次按希望的顺序单击组件,就可以重排Tab顺序了。没有输入焦点的组件是没有Tab顺序的,例如Label组件。
图2-19 编辑伙伴关系
图2-20 Tab顺序编辑状态
Qt的界面组件都是从QWidget继承而来的,都支持信号与槽的功能。每个类都有一些内建的信号和槽函数,例如QPushButton按钮类常用的信号是clicked(),在按钮被单击时发射此信号。QDialog是对话框类,它有以下3个内建的槽函数。
这3个槽函数都可以关闭对话框,但是表示的对话框的返回值不同,关于对话框的显示和返回值在6.2节详细介绍。在图2-17的对话框上,我们希望将“确定”按钮与对话框的accept()槽函数关联,将“退出”按钮与对话框的close()槽函数关联。
可以在UI Designer里使用可视化的方式实现信号与槽函数的关联。在UI Designer里单击上方工具栏里的“Edit Signals/Slots”按钮,窗体进入信号与槽函数编辑状态,如图2-21所示。
鼠标点选“确定”按钮,再按住鼠标左键拖动到窗体的空白区域后释放左键,这时出现如图2-22所示的关联设置对话框。此对话框左边的列表框里显示了btnOK的信号,选择clicked(),右边的列表框里显示了Dialog的槽函数,选择accept(),然后单击“OK”按钮。同样的方法可以将btnClose的clicked()信号与Dialog的close()槽函数关联。
图2-21 窗体进入Signals/Slots编辑状态(已设置好关联)
图2-22 信号与槽关联编辑对话框
提示
在图2-22的右边列表框中没有close()函数,需要勾选下方的“Show signals and slots inherited from QWidget”才会出现close()函数。
设置好这两个按钮的信号与槽关联之后,在窗体下方的Signals Slots 编辑器里就显示了这两个关联(如图2-23所示)。实际上,可以直接在Signals Slots 编辑器进行某个组件的内建信号与其他组件的内建槽函数关联。
图2-23 信号与槽关联编辑器
在完成上一步的窗体可视化设计后,就可以将窗体文件Dialog.ui编译转换为相应的Python类定义文件,并编写PyQt5 GUI应用程序,测试程序运行效果。
在这个示例中,我们对PyQt5 GUI应用程序的文件组成做了一个统一的规划。示例Demo2_3的文件夹下的文件组成如图2-24所示。各个文件或文件夹的作用如下。
图2-24 示例Demo2_3的文件夹下的文件组成
用鼠标右键单击文件uic.bat(注意不要双击,双击是执行此文件),在快捷菜单里点击“编辑”会显示此文件的内容。uic.bat文件的内容如下:
echo off
rem 将子目录 QtApp 下的.ui文件复制到当前目录下
copy .\QtApp\Dialog.ui Dialog.ui
rem 用pyuic5编译.ui文件
pyuic5 -o ui_Dialog.py Dialog.ui
uic.bat文件是指令批处理文件,运行批处理文件就相当于在Windows的cmd窗口里顺序执行文件里的操作指令。文件中的“rem”表示注释行,这个文件主要执行了以下两条指令。
将操作指令编写为批处理文件uic.bat后,双击此文件就可以执行这些指令,避免了在cmd窗口里重复键入指令的麻烦。
运行批处理文件uic.bat后,将得到窗体UI文件Dialog.ui编译后的窗体界面定义文件ui_Dialog.py,下面是这个文件的完整内容:
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(337, 318)
font = QtGui.QFont()
font.setFamily("宋体")
font.setPointSize(11)
font.setBold(True)
font.setWeight(75)
Dialog.setFont(font)
self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
self.verticalLayout.setContentsMargins(11, 11, 11, 9)
self.verticalLayout.setSpacing(6)
self.verticalLayout.setObjectName("verticalLayout")
self.groupBox1 = QtWidgets.QGroupBox(Dialog)
self.groupBox1.setTitle("")
self.groupBox1.setObjectName("groupBox1")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.groupBox1)
self.horizontalLayout_2.setContentsMargins(11, 11, 11, 11)
self.horizontalLayout_2.setSpacing(6)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.chkBoxUnder = QtWidgets.QCheckBox(self.groupBox1)
self.chkBoxUnder.setObjectName("chkBoxUnder")
self.horizontalLayout_2.addWidget(self.chkBoxUnder)
self.chkBoxItalic = QtWidgets.QCheckBox(self.groupBox1)
self.chkBoxItalic.setObjectName("chkBoxItalic")
self.horizontalLayout_2.addWidget(self.chkBoxItalic)
self.chkBoxBold = QtWidgets.QCheckBox(self.groupBox1)
self.chkBoxBold.setChecked(True)
self.chkBoxBold.setObjectName("chkBoxBold")
self.horizontalLayout_2.addWidget(self.chkBoxBold)
self.verticalLayout.addWidget(self.groupBox1)
self.groupBox2 = QtWidgets.QGroupBox(Dialog)
self.groupBox2.setTitle("")
self.groupBox2.setObjectName("groupBox2")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.groupBox2)
self.horizontalLayout_3.setContentsMargins(11, 11, 11, 11)
self.horizontalLayout_3.setSpacing(6)
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.radioBlack = QtWidgets.QRadioButton(self.groupBox2)
self.radioBlack.setChecked(True)
self.radioBlack.setObjectName("radioBlack")
self.horizontalLayout_3.addWidget(self.radioBlack)
self.radioRed = QtWidgets.QRadioButton(self.groupBox2)
self.radioRed.setObjectName("radioRed")
self.horizontalLayout_3.addWidget(self.radioRed)
self.radioBlue = QtWidgets.QRadioButton(self.groupBox2)
self.radioBlue.setChecked(False)
self.radioBlue.setObjectName("radioBlue")
self.horizontalLayout_3.addWidget(self.radioBlue)
self.verticalLayout.addWidget(self.groupBox2)
self.textEdit = QtWidgets.QPlainTextEdit(Dialog)
font = QtGui.QFont()
font.setPointSize(20)
font.setBold(True)
font.setWeight(75)
self.textEdit.setFont(font)
self.textEdit.setObjectName("textEdit")
self.verticalLayout.addWidget(self.textEdit)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setContentsMargins(-1, 10, -1, 10)
self.horizontalLayout.setSpacing(6)
self.horizontalLayout.setObjectName("horizontalLayout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.btnClear = QtWidgets.QPushButton(Dialog)
self.btnClear.setObjectName("btnClear")
self.horizontalLayout.addWidget(self.btnClear)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem1)
self.btnOK = QtWidgets.QPushButton(Dialog)
self.btnOK.setObjectName("btnOK")
self.horizontalLayout.addWidget(self.btnOK)
self.btnClose = QtWidgets.QPushButton(Dialog)
self.btnClose.setObjectName("btnClose")
self.horizontalLayout.addWidget(self.btnClose)
self.verticalLayout.addLayout(self.horizontalLayout)
self.retranslateUi(Dialog)
self.btnOK.clicked.connect(Dialog.accept)
self.btnClose.clicked.connect(Dialog.close)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", " Demo2-3信号与槽"))
self.chkBoxUnder.setText(_translate("Dialog", "Underline"))
self.chkBoxItalic.setText(_translate("Dialog", "Italic"))
self.chkBoxBold.setText(_translate("Dialog", "Bold"))
self.radioBlack.setText(_translate("Dialog", "Black"))
self.radioRed.setText(_translate("Dialog", "Red"))
self.radioBlue.setText(_translate("Dialog", "Blue"))
self.textEdit.setPlainText(_translate("Dialog", "PyQt5 编程指南\n"
"Python 和 Qt"))
self.btnClear.setText(_translate("Dialog", "清空"))
self.btnOK.setText(_translate("Dialog", "确定"))
self.btnClose.setText(_translate("Dialog", "退出"))
这个文件定义了一个Python类Ui_Dialog,在2.2.2节已经分析了这种类的基本构成,它主要完成两个任务:界面创建,信号与槽函数的关联。
(1)界面创建
setupUi()函数创建窗体上的各个组件,包括布局管理组件。布局管理也是通过相应的类实现的,例如,groupBox1组件内部是3个CheckBox组件的水平布局,相关代码是(省略了中间一些属性设置的代码行):
self.groupBox1 = QtWidgets.QGroupBox(Dialog)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.groupBox1)
self.chkBoxUnder = QtWidgets.QCheckBox(self.groupBox1)
self.horizontalLayout_2.addWidget(self.chkBoxUnder)
self.chkBoxItalic = QtWidgets.QCheckBox(self.groupBox1)
self.horizontalLayout_2.addWidget(self.chkBoxItalic)
self.chkBoxBold = QtWidgets.QCheckBox(self.groupBox1)
self.horizontalLayout_2.addWidget(self.chkBoxBold)
第2行代码创建了一个水平布局horizontalLayout_2,其父容器是groupBox1。
创建CheckBox组件时指定父容器为groupBox1,然后添加到水平布局horizontalLayout_2里。这样依次添加的3个CheckBox组件就在groupBox1里水平分布了。同样,其他的布局管理的代码也是类似的。
分析这些代码可以发现代码化创建窗体组件和布局管理的编程方法。可视化设计的窗体最后其实也都是转换为代码来执行的,但显然,没几个人愿意手工编写这样的代码来创建窗体。但是我们需要知道这些代码的原理,一般情况下尽量用UI Designer可视化设计窗体,在必须手工编写代码创建界面时再编写代码,例如在后面要讲到的混合方式创建界面的时候。
(2)信号与槽的关联
在setupUi()函数的最后有3行这样的语句:
self.btnOK.clicked.connect(Dialog.accept)
self.btnClose.clicked.connect(Dialog.close)
QtCore.QMetaObject.connectSlotsByName(Dialog)
其中,第1行将界面上的按钮btnOK的clicked()信号与窗体对象Dialog的accept()槽函数关联起来;第2行将按钮btnClose的clicked()信号与Dialog的close()槽函数关联起来;第3行的作用在后面解释。
信号与槽函数关联使用connect()函数,语句如下:
sender.signalName.connect(receiver.slotName)
其中:
所以,对于在图2-23的Signals Slots编辑器里可视化设置的关联,setupUi()函数将自动生成信号与槽关联的语句。
提示
在本书后面的示例程序中,将不会再显示.ui文件编译后生成的Python文件的内容,因为这个文件就是可视化设计的UI窗体的代码实现,代码多且没有显示的意义。如果需要分析某种界面效果的代码化构建方法,可以自行分析此文件代码。
按照2.2节介绍的界面与业务逻辑分离且界面独立封装的方式定义一个类QmyDialog,并保存为文件myDialog.py。文件代码如下:
##与UI窗体类对应的业务逻辑类
import sys
from PyQt5.QtWidgets import QApplication, QDialog
from ui_Dialog import Ui_Dialog
class QmyDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent) #调用父类构造函数,创建窗体
self.ui=Ui_Dialog() #创建UI对象
self.ui.setupUi(self) #构造UI
if __name__ == "__main__": #用于当前窗体测试
app = QApplication(sys.argv) #创建GUI应用程序
form=QmyDialog() #创建窗体
form.show()
sys.exit(app.exec_())
这个文件有窗体测试程序,运行此文件时,就会执行文件后面部分的程序,其功能是创建应用程序和窗体,并运行应用程序。
现在运行程序myDialog.py就会出现所设计的窗体,点击窗体上的“确定”和“退出”按钮可以关闭窗体并退出程序,说明这两个按钮的功能实现了。这是因为在QmyDialog类的构造函数中,创建了窗体类的实例对象self.ui,并调用了其setupUi()函数,即下面这两行语句:
self.ui=Ui_Dialog() #创建UI对象
self.ui.setupUi(self) #构造UI
而Ui_Dialog的setupUi()函数实现了这两个按钮的信号与窗体相关槽函数的关联,所以点击按钮的操作起作用了。
程序myDialog.py可以当作主程序直接运行,但是建议单独编写一个主程序文件appMain.py,此文件的代码如下:
## GUI应用程序主程序
import sys
from PyQt5.QtWidgets import QApplication
from myDialog import QmyDialog
app = QApplication(sys.argv) #创建GUI应用程序
mainform=QmyDialog() #创建主窗体
mainform.show() #显示主窗体
sys.exit(app.exec_())
appMain.py的功能是创建应用程序和主窗体,然后显示主窗体,并开始运行应用程序。它将myDialog.py文件的测试运行部分单独拿出来作为一个文件。当一个应用程序有多个窗体,并且窗体之间有数据传递时,appMain.py负责创建应用程序的主窗体并运行起来,这样使整个应用程序的结构更清晰。
注意
为了避免混淆,我们在命名文件时,将文件名与文件内的类名称区分开来,将Python中针对UI窗体创建的业务逻辑类的名称与UI窗体名称区分开来,这与Eric中的命名方法不同。
例如,UI文件Dialog.ui中的窗体名称是Dialog,文件Dialog.ui编译后生成的Python类名称是固定的Ui_Dialog,我们设置保存为文件ui_Dialog.py,而Eric会自动保存为文件Ui_Dialog.py。
针对文件ui_Dialog.py中的类Ui_Dialog创建的窗体业务逻辑类是可以自由命名的,我们将类命名为QmyDialog,保存为文件myDialog.py。而在Eric中创建的业务逻辑类默认的文件名是Dialog.py,类名称也是Dialog,这对于初学者容易造成混淆。
下面为窗体上的“清空”按钮编写槽函数,首先要找到应该使用该按钮的那个信号。在Qt Creator中打开本示例的QtApp项目,再打开窗体Dialog.ui,选中“清空”按钮,点击右键调出其快捷菜单,在菜单中点击“Go to slot...”菜单项,会打开如图2-25所示的Go to slot对话框。
这个对话框显示了所选组件类的所有可用信号。“清空”按钮是一个QPushButton类的按钮,所以图2-25显示的是QPushButton类及其所有父类的信号。按钮最常用的信号是clicked(),就是点击按钮时发射的信号。在图2-25中选择clicked()信号,然后点击“OK”按钮,这样会在QtApp项目的Dialog.cpp文件里生成下面这样一个C++槽函数框架:
void DialogText::on_btnClear_clicked()
{
}
按快捷键F4会在Dialog.h和Dialog.cpp文件之间切换,在Dialog.h文件里可看到自动生成的这个槽函数的C++原型定义:
void on_btnClear_clicked();
我们并不需要编写任何C++程序,而只需要自动生成的这个槽函数名称。复制此函数名称,在myDialog.py文件的QmyDialog类里定义一个同名的函数并编写代码:
def on_btnClear_clicked(self):
self.ui.textEdit.clear()
现在若运行myDialog.py文件,会发现“清空”按钮可用了,它会将文本框里的内容全部清除。
同样,在UI Designer里可视化设计窗体时,选中“Bold”复选框,打开其Go to slot对话框(如图2-26所示),这里显示了QCheckBox类的所有信号。
其中的toggled(bool)信号在复选框的状态变化时发射,复选框的勾选状态作为参数传递给函数,点击“OK”按钮后生成其C++槽函数原型为:
void on_chkBoxBold_toggled(bool checked);
在myDialog.py文件的QmyDialog类里定义一个同名函数,并且具有相同类型的参数,代码如下:
def on_chkBoxBold_toggled(self,checked):
font=self.ui.textEdit.font()
font.setBold(checked) #参数checked表示勾选状态
self.ui.textEdit.setFont(font)
现在若运行myDialogText.py文件,会发现“Bold”复选框可用了,会使文本框的字体在粗体和正常之间切换。
图2-25 QPushButton类按钮的Go to slot对话框
图2-26 QCheckBox类组件的Go to slot对话框
同样,在窗体可视化设计时,选中“Underline”复选框,打开其Go to slot对话框如图2-26所示。在对话框里不选择toggled(bool)信号,而是选择clicked()信号。而且要注意,还有一个带参数的clicked(bool)信号,它会将点击复选框时的勾选状态当作一个参数传递给槽函数。现在暂时用clicked()信号,对于同名而参数不同的overload型信号的处理在后面讨论。
“Underline”复选框的clicked()信号的C++槽函数原型是:
void on_chkBoxUnder_clicked();
在myDialog.py文件的QmyDialog类里定义一个同名函数并编写代码。现在QmyDialog类的完整代码如下:
import sys
from PyQt5.QtWidgets import QApplication, QDialog
from ui_Dialog import Ui_Dialog
class QmyDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent) #调用父类构造函数,创建窗体
self.ui=Ui_Dialog() #创建UI对象
self.ui.setupUi(self) #构造UI
##==== 由connectSlotsByName() 自动与组件的信号关联的槽函数=====
def on_btnClear_clicked(self): ##"清空"按钮
self.ui.textEdit.clear()
def on_chkBoxBold_toggled(self,checked): ## "Bold" 复选框
font=self.ui.textEdit.font()
font.setBold(checked) #参数checked表示勾选状态
self.ui.textEdit.setFont(font)
def on_chkBoxUnder_clicked(self): ## "Underline"复选框
checked=self.ui.chkBoxUnder.isChecked() #读取勾选状态
font=self.ui.textEdit.font()
font.setUnderline(checked)
self.ui.textEdit.setFont(font)
现在运行文件myDialog.py,“Underline”复选框的功能也可用了。
在QmyDialog类里定义了3个函数,这3个函数与相应界面组件的信号关联起来了,实现了信号与槽的关联。但是,在QmyDialog类的构造函数里并没有添加任何代码进行信号与槽的关联,而Ui_Dialog类也没有做任何修改(在UI Designer里可视化生成槽函数框架时,并不会对Dialog.ui做任何修改,所以无须重新编译Dialog.ui),这些信号与槽的关联是如何实现的呢?
秘密在于ui_Dialog.py文件中的Ui_Dialog.setupUi()函数的最后一行语句
QtCore.QMetaObject.connectSlotsByName(Dialog)
使用了Qt的元对象(QMetaObject),它会搜索Dialog窗体上的所有从属组件,将匹配的信号和槽函数关联起来,它假设槽函数的名称是:
on_<object name>_<signal name>(<signal parameters>)
在组件的Go to slot对话框里,选择一个信号后生成的槽函数名称就是符合这个命名规则的。所以,如果在UI Designer里通过可视化设计自动生成槽函数框架,然后复制函数名到Python程序里,这样的槽函数就可以和组件的信号自动关联,而不用逐个手工编写关联的语句。
不符合这样的命名规则的函数不能自动与信号关联,即使非常小的改动,例如函数on_btnClear_clicked()改为on_btnClear_clicked2(),也不能与组件的信号自动关联。
提示 要在Qt Creator中通过Go to slot对话框为一个UI窗体上的组件自动生成槽函数框架,UI窗体文件必须是在一个Qt GUI项目里打开的,一个.ui文件有对应的.h和.cpp文件。像示例Demo2_2里那样只有一个独立的.ui文件是不能生成槽函数框架的。使用Qt的独立软件Qt Designer只能设计UI窗体,没有Go to slot对话框,不能生成槽函数框架,这就是为什么我们使用Qt Creator内置的UI Designer,而不使用独立的Qt Designer的原因。
在图2-26的QCheckBox类组件的Go to slot对话框中,有两个名称为clicked的信号,一个是不带参数的clicked()信号,“Underline”复选框使用这个信号生成槽函数是可以自动关联的;另一个是带参数的clicked(bool)信号,它将复选框的当前勾选状态作为参数传递给槽函数。这种名称相同但参数个数或类型不同的信号就是overload型信号。
对于窗体上的“Italic”复选框,在其Go to slot对话框中选择clicked(bool)信号生成槽函数原型,用相应的函数名在QmyDialog类中定义一个函数,代码如下:
def on_chkBoxItalic_clicked(self,checked):
font=self.ui.textEdit.font()
font.setItalic(checked)
self.ui.textEdit.setFont(font)
我们“以为”这个槽函数会和chkBoxItalic的clicked(bool)信号自动关联,运行文件myDialog.py,却发现点击“Italic”复选框时,程序出现异常直接退出了!为什么会出现这种情况呢?
这是因为有两个不同类型参数的clicked信号,connectSlotsByName()函数进行信号与槽函数的关联时会使用一个默认的信号,对 QCheckBox来说,默认使用的是不带参数的clicked()信号。而现在定义的函数on_chkBoxItalic_clicked(self, checked)是需要传递进来一个参数的,程序运行到on_chkBoxItalic_clicked(self, checked)函数时,无法给它传递一个参数checked,所以发生了异常。
要解决这个问题,需要使用@pyqtSlot修饰符,用这个修饰符将函数的参数类型声明清楚。将on_chkBoxItalic_clicked()函数的代码修改为如下形式:
@pyqtSlot(bool) ##修饰符指定参数类型,用于overload型的信号
def on_chkBoxItalic_clicked(self,checked):
font=self.ui.textEdit.font()
font.setItalic(checked)
self.ui.textEdit.setFont(font)
这样使用@pyqtSlot修饰符声明函数参数类型后,connectSlotsByName()函数就会自动使用clicked(bool)信号与这个槽函数关联,运行就没有问题了。
注意
对于非默认的overload型信号,槽函数必须使用修饰符@pyqtSlot声明函数参数类型。如果两种参数的overload型信号都要关联槽函数,那么两个槽函数名必须不同名,且在关联时要做设置(具体的设置方法在2.4节的示例里介绍)。
很多情况下也需要手工编写代码进行信号与槽的关联,例如在图2-18的窗体上,希望将设置颜色的3个RadioButton按钮的clicked()信号与同一个槽函数关联。
在QmyDialog类里定义一个新的函数do_setTextColor(),并且在构造函数里进行关联,添加这些功能后的myDialog.py的完整代码如下:
import sys
from PyQt5.QtWidgets import QApplication, QDialog
from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt, pyqtSlot
from ui_Dialog import Ui_Dialog
class QmyDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent) #调用父类构造函数
self.ui=Ui_Dialog() #创建UI对象
self.ui.setupUi(self) #构造UI
self.ui.radioBlack.clicked.connect(self.do_setTextColor)
self.ui.radioRed.clicked.connect(self.do_setTextColor)
self.ui.radioBlue.clicked.connect(self.do_setTextColor)
##==== 由connectSlotsByName() 自动与组件的信号关联的槽函数====
def on_btnClear_clicked(self): ##"清空" 按钮
self.ui.textEdit.clear()
def on_chkBoxBold_toggled(self,checked): ##"Bold"复选框
font=self.ui.textEdit.font()
font.setBold(checked) #函数参数checked表示勾选状态
self.ui.textEdit.setFont(font)
def on_chkBoxUnder_clicked(self): ##"Underline"复选框
checked=self.ui.chkBoxUnder.isChecked() #读取勾选状态
font=self.ui.textEdit.font()
font.setUnderline(checked)
self.ui.textEdit.setFont(font)
@pyqtSlot(bool) ##修饰符指定参数类型,用于overload型的信号
def on_chkBoxItalic_clicked(self,checked): #"Italic"复选框
font=self.ui.textEdit.font()
font.setItalic(checked)
self.ui.textEdit.setFont(font)
##=========自定义槽函数========
def do_setTextColor(self): ##设置文本颜色
plet=self.ui.textEdit.palette() #获取 palette
if (self.ui.radioBlack.isChecked()):
plet.setColor(QPalette.Text, Qt.black) #black
elif (self.ui.radioRed.isChecked()):
plet.setColor(QPalette.Text, Qt.red) #red
elif (self.ui.radioBlue.isChecked()):
plet.setColor(QPalette.Text, Qt.blue) #blue
self.ui.textEdit.setPalette(plet) #设置palette
if __name__ == "__main__": ##用于当前窗体测试
app = QApplication(sys.argv) #创建GUI应用程序
form=QmyDialog() #创建窗体
form.show()
sys.exit(app.exec_())
代码里用到了QPalette、Qt、pyqtSlot等类或函数,所以需要用import语句从相应的模块导入。
自定义的函数do_setTextColor()读取3个RadioButton按钮的选中状态,哪个按钮被选中就设置这个按钮文本框里文本的颜色为相应的颜色。do_setTextColor()的代码涉及的具体操作现在暂时不解释,第3章会介绍常用界面组件的使用。
在QmyDialog的构造函数中增加了下面3条语句:
self.ui.radioBlack.clicked.connect(self.do_setTextColor)
self.ui.radioRed.clicked.connect(self.do_setTextColor)
self.ui.radioBlue.clicked.connect(self.do_setTextColor)
这样就将3个RadioButton按钮的clicked()信号与同一个槽函数do_setTextColor()关联起来了,实现了信号与槽函数的关联。
提示 为了与connectSlotsByName()自动关联的槽函数区别,本书中自定义槽函数的函数名一律使用“do_”作为前缀。当然,这只是个人习惯的命名规则。
现在运行程序myDialog.py,就会发现3个设置颜色的RadioButton按钮都可以用了,整个窗体的所有功能都实现了。
在PyQt5中,信号与槽的使用有如下一些特点。
2.3节的示例使用的信号都是类的内建信号,在自定义类中还可以自定义信号。使用自定义信号在程序的对象之间传递信息是非常方便的,例如在多窗体应用程序中,通过信号与槽在窗体之间传递数据。
使用PyQt5.QtCore.pyqtSignal()为一个类定义新的信号。要自定义信号,类必须是QObject类的子类。pyqtSignal()的句法是:
pyqtSignal(types[, name[, revision=0[, arguments=[]]]])
信号可以带有参数types,后面的参数都是一些可选项,基本不使用。
信号需要定义为类属性,这样定义的信号是未绑定(unbound)信号。当创建类的实例后,PyQt5会自动将类的实例与信号绑定,这样就生成了绑定的(bound)信号。这与Python语言从类的函数生成绑定的方法的机制是一样的。
一个绑定的信号(也就是类的实例对象的信号)具有connect()、disconnect()和emit()这3个函数,分别用于关联槽函数、断开与槽函数的关联、发射信号。
下面是示例Demo2_4目录下的程序human.py的完整代码,这个程序演示了自定义信号的使用,以及信号与槽的使用中一些功能的实现方法。
## 自定义信号与槽的演示
import sys
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal
class Human(QObject):
##定义一个带str类型参数的信号
nameChanged = pyqtSignal(str)
## overload型信号有两种参数,一种是int,另一种是str
ageChanged = pyqtSignal([int],[str])
def __init__(self,name='Mike',age=10,parent=None):
super().__init__(parent)
self.setAge(age)
self.setName(name)
def setAge(self,age):
self.__age= age
self.ageChanged.emit(self.__age) #发射int参数信号
if age<=18:
ageInfo="你是 少年"
elif (18< age <=35):
ageInfo="你是 年轻人"
elif (35< age <=55):
ageInfo="你是 中年人"
elif (55< age <=80):
ageInfo="您是 老人"
else:
ageInfo="您是 寿星啊"
self.ageChanged[str].emit(ageInfo) #发射str参数信号
def setName(self,name):
self.__name = name
self.nameChanged.emit(self.__name)
class Responsor(QObject):
@pyqtSlot(int)
def do_ageChanged_int(self,age):
print("你的年龄是:"+str(age))
@pyqtSlot(str)
def do_ageChanged_str(self,ageInfo):
print(ageInfo)
## @pyqtSlot(str)
def do_nameChanged(self, name):
print("Hello,"+name)
if __name__ == "__main__": ##测试程序
print("**创建对象时**")
boy=Human("Boy",16)
resp=Responsor()
boy.nameChanged.connect(resp.do_nameChanged)
## overload的信号,两个槽函数不能同名,关联时需要给信号加参数区分
boy.ageChanged.connect(resp.do_ageChanged_int) #默认参数,int型
boy.ageChanged[str].connect(resp.do_ageChanged_str) #str型参数
print("\n **建立关联后**")
boy.setAge(35) #发射两个ageChanged 信号
boy.setName("Jack") #发射nameChanged信号
boy.ageChanged[str].disconnect(resp.do_ageChanged_str) #断开关联
print("\n **断开ageChanged[str]的关联后**")
boy.setAge(10) #发射两个ageChanged 信号
(1)信号的定义
定义的类Human是从QObject继承而来的,它定义了两个信号,两个信号都需要定义为类的属性。nameChanged信号是带有一个str类型参数的信号,定义为:
nameChanged = pyqtSignal(str)
ageChanged信号是具有两种类型参数的overload型的信号,信号的参数类型可以是int,也可以是str。ageChanged信号定义为:
ageChanged = pyqtSignal([int],[str])
(2)信号的发射
通过信号的emit()函数发射信号。在类的某个状态发生变化,需要通知外部发生了这种变化时,发射相应的信号。如果信号关联了一个槽函数,就会执行槽函数,如果信号没有关联槽函数,就不会产生任何动作。
例如在Human.setName()函数中,当变量self.__name发生变化时发射nameChanged信号,并且传递参数,即
self.nameChanged.emit(self.__name)
变量self.__name作为信号的参数,关联的槽函数可以从参数中获得当前信号的名称,从而进行相应的处理。
Human.setAge()函数中发射了两次ageChanged信号,但是使用了不同的参数,分别是int型参数和str型参数,即
self.ageChanged.emit(self.__age) #int参数信号
self.ageChanged[str].emit(ageInfo) #str参数信号
(3)信号与槽的关联
另外定义的一个类Responsor也是从QObject继承而来的,它定义了三个函数,分别用于与Human类实例对象的信号建立关联。
因为信号ageChanged有两种参数类型,要与两种参数的ageChanged信号都建立关联,两个槽函数的名称必须不同,所以定义的两个槽函数名称分别是do_ageChanged_int和do_ageChanged_str。
需要在创建类的具体实例后再进行信号与槽的关联,所以,程序在测试部分先创建两个具体的对象。
boy=Human("Boy",16)
resp=Responsor()
如果一个信号的名称是唯一的,即不是overload型信号,那么关联时无须列出信号的参数,例如,nameChanged信号的连接为:
boy.nameChanged.connect(resp.do_nameChanged)
对于overload型的信号,定义信号时的第一个位置的参数是默认参数。例如,ageChanged信号的定义是:
ageChanged = pyqtSignal([int],[str])
所以,ageChanged信号的默认参数就是int型。默认参数的信号关联无须标明参数类型,所以有:
boy.ageChanged.connect(resp.do_ageChanged_int) #默认参数,int型
但是,对于另外一个非默认参数,必须在信号关联时在信号中注明参数,即
boy.ageChanged[str].connect(resp.do_ageChanged_str) #str型参数
(4)@pyqtSlot修饰符的作用
在PyQt5中,任何一个函数都可以作为槽函数,但有时也需要使用@pyqtSlot修饰符说明函数的参数类型,以使信号与槽之间能正确关联。
@pyqtSlot()修饰符用于声明槽函数的参数类型,例如在2.3节的示例中,为了使函数on_chkBoxItalic_clicked(self,checked)与窗体上chkBoxItalic复选框的clicked(bool)信号自动建立关联,就使用了@pyqtSlot(bool)进行修饰。
在本例Responsor类的 3 个槽函数前的@pyqtSlot()修饰符都可以被注释掉,不影响程序的运行结果。因为overload型信号的两个槽函数名称不同,在建立关联时也指定了参数类型。
(5)断开信号与槽的关联
在程序中可以使用disconnect()函数断开信号与槽的关联,例如,程序中用下面的代码断开了一个关联。
boy.ageChanged[str].disconnect(resp.do_ageChanged_str) #断开关联
运行程序human.py,在Python Shell中显示如下的运行结果:
**创建对象时**
**建立关联后**
你的年龄是:35
你是 年轻人
Hello,Jack
**断开ageChanged[str]的关联后**
你的年龄是:10
从运行结果中可以看到:
通过这两节的示例讲解,信号与槽使用中涉及的一些用法基本都介绍了。信号与槽机制是非常好用的,特别是为GUI程序各对象之间的信息传递提供了很方便的处理方法,但是在PyQt5中使用信号与槽时也要注意以下问题。
(1)对于PyQt5中的类的内建overload型信号,一般只为其中一种信号编写槽函数。例如QCheckBox组件有clicked()和clicked(bool)两种信号,可以有针对性地只选择其中一种参数类型的信号编写槽函数。如果使用的overload型信号不是默认参数类型的信号,那么槽函数还需要使用@pyqtSlot()修饰符声明参数类型。
(2)在自定义信号时,尽量不要定义overload型信号。因为Python的某些类型转换为C++的类型时,对于C++来说可能是同一种类型的参数,例如,若定义一个overload型的信号:
valueChanged = pyqtSignal([dict], [list])
dict和list在Python中是不同的数据类型,但是转换为C++后可能就是相同的数据类型了,这可能会出现问题。
到目前为止,我们已经介绍了窗体可视化设计的PyQt5 GUI应用程序框架,以及Qt核心技术信号与槽的使用方法,熟悉窗体可视化设计和常用界面组件使用的读者完全可以开始编写自己的GUI应用程序了。但还有一个技术点需要解决,就是资源文件的使用。例如,在示例Demo2_3的窗体上(图2-10)的按钮都没有设置图标,而图标的使用是GUI程序不可或缺的一项功能。
本节介绍如何在PyQt5 GUI应用中使用资源文件,包括如何在Qt Creator中创建和管理资源文件,如何在窗体可视化设计时为按钮设置图标,以及如何将Qt的资源文件通过pyrcc5工具软件编译为Python文件。
本节仍然结合2.4节的程序文件human.py创建一个示例Demo2_5,示例运行时界面如图2-27所示。通过这个示例可以掌握资源文件的使用方法,加深对GUI应用程序的设计流程、窗体布局可视化设计、自定义信号与槽函数的使用等内容的理解。
图2-27 示例Demo2_5运行时界面
掌握这个应用程序的设计方法后,就基本掌握了PyQt5设计GUI应用程序的基本流程和关键技术了,再继续学习就是学习更多的PyQt5的类的使用方法了。就如同你已经精通了英语语法,剩下的只是增加单词量的问题了。
在Demo2_5的项目目录下新建一个Qt Widgets Application项目QtApp,创建窗体时选择窗体基类为QWidget,新建窗体类的名称设置为默认的Widegt。创建项目后,对窗体Widget.ui 进行可视化设计,设计好的窗体如图2-28所示。
该窗体在设计时采用了布局管理方法。“年龄设置”分组框用的是组件面板Container分组里的GroupBox组件,其内部组件按网格状布局;“姓名设置”分组框里的组件也按网格状布局;最下方的按钮和多个空格组件使用了组件面板Container分组里的Frame组件作为容器,采用水平布局。窗口的主布局采用垂直布局。
窗体上所有组件的层次关系如图2-29所示,图2-29还显示了各个组件的objectName及其所属的类。用于设置年龄的是一个QSlider组件,在属性编辑器里设置其minimum属性为0,maximum属性为100。
图2-28 可视化设计完成的窗体Widget.ui
图2-29 窗体上的组件的层次关系、objectName及其所属类
在Qt Creator里单击“File”→“New File or Project…”菜单项,在新建文件与项目对话框里选择“Qt Resource File”,然后按照向导的指引设置资源文件的文件名,并添加到当前项目里。
本项目创建的资源文件名为res.qrc。在项目文件目录树里,会自动创建一个与Headers、Sources、Forms并列的Resources文件组,在Resources组里有res.qrc节点。在res.qrc节点上点击鼠标右键,在弹出的快捷菜单中选择“Open in Editor”打开资源文件编辑器(如图2-30所示)。
资源文件最主要的功能是存储图标和图片文件,以便在程序里使用。在资源文件里首先建一个前缀(Prefix),例如icons,方法是在图2-30窗口右下方的功能区单击“Add”按钮下的“Add Prefix”,设置一个前缀名为icons,前缀就是资源的分组。
然后再单击“Add”按钮下的“Add Files”选择图标文件。如果所选的图标文件不在本项目的子目录里,会提示复制文件到项目下的子目录里。在QtApp项目的目录下建一个子目录 \images,将所有图标文件放置在这个文件夹里。在图2-30的前缀和图标文件目录已设置的情况下,如果要在代码里使用其中的app.ico图标文件,其引用名称是“:/icons/images/app.ico”。
将图标导入到资源文件里后,就可以在窗体设计时使用图标了。例如,在图2-28中要设置“关闭”按钮的图标,在属性编辑器中有icon属性,点击右侧下拉菜单中的“Choose Resource...”,就可以在项目的资源文件里为按钮选择图标了(如图2-31所示)。
图2-30 资源文件编辑
图2-31 为按钮在资源文件里选择图标
在Qt Creator里设计的资源文件要在Python程序里使用,需要使用pyrcc5.exe工具软件将资源文件res.qrc编译为一个对应的Python文件res_rc.py。在Demo2_5目录下执行编译的指令如下:
pyrcc5 .\QtApp\res.qrc -o res_rc.py
该指令将\QtApp目录下的res.qrc进行编译,输出文件res_rc.py到Demo2_5目录下,编译后的资源文件名必须是原文件名后面加“_rc”。
不能先将文件QtApp\res.qrc复制到Demo2_5目录下之后再编译,因为res.qrc需要查找其子目录 \images下的文件,复制后相对位置变化了,编译时会找不到图标文件。
同样还需要编译窗体文件Widget.ui。于是在Demo2_5目录下建一个批处理文件uic.bat,用于执行这两条编译指令,文件uic.bat内容如下:
echo off
rem 将子目录 QtApp 下的.ui文件复制到当前目录下,并且编译
copy .\QtApp\Widget.ui \Widget.ui
pyuic5 -o ui_Widget.py \Widget.ui
rem 编译并复制资源文件
pyrcc5 .\QtApp\res.qrc -o res_rc.py
双击执行这个批处理文件,就同时编译了窗体文件和资源文件。
可以打开资源文件res.qrc编译后的文件res_rc.py,查看其内容。res_rc.py文件里存储了图标的十六进制编码数据,以及相关的管理代码。
窗体文件编译后的文件是ui_Widget.py,由于这个窗体使用了资源文件,在此文件的最后自动加入了一行import语句,即
import res_rc
文件ui_Widget.py里定义了一个类Ui_Widget,其setupUi()函数是构建窗体界面的代码。如果有兴趣研究代码化构建窗体界面的原理,或需要参考其中的代码,例如布局管理的代码、使用图标的代码,可以查看此文件的内容。
将示例Demo2_4创建的文件human.py复制到本示例目录下。采用单继承方法设计一个窗体业务逻辑类QmyWidget,保存为文件myWidget.py,该文件的完整内容如下:
import sys
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtGui import QIcon
from ui_Widget import Ui_Widget
from human import Human
class QmyWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent) #调用父类构造函数
self.ui=Ui_Widget() #创建UI对象
self.ui.setupUi(self) #构造UI
self.boy=Human("Boy",16)
self.boy.nameChanged.connect(self.do_nameChanged)
self.boy.ageChanged.connect(self.do_ageChanged_int)
self.boy.ageChanged[str].connect(self.do_ageChanged_str)
##=====由connectSlotsByName() 自动与组件的信号关联的槽函数=====
def on_sliderSetAge_valueChanged(self,value):
self.boy.setAge(value)
def on_btnSetName_clicked(self):
hisName=self.ui.editNameInput.text()
self.boy.setName(hisName)
##=======自定义槽函数=======
def do_nameChanged(self,name):
self.ui.editNameHello.setText("Hello,"+name)
@pyqtSlot(int)
def do_ageChanged_int(self,age):
self.ui.editAgeInt.setText(str(age))
@pyqtSlot(str)
def do_ageChanged_str(self,info):
self.ui.editAgeStr.setText(info)
if __name__ == "__main__": ##用于当前窗体测试
app = QApplication(sys.argv)
icon = QIcon(":/icons/images/app.ico")
app.setWindowIcon(icon)
form=QmyWidget()
form.show()
sys.exit(app.exec_())
QmyWidget的构造函数创建了一个Human类的实例self.boy,并且将其3个信号分别与3个自定义槽函数关联,这3个自定义槽函数的功能是在窗体界面上显示相关信息。
QmyWidget类还定义了两个可以由connectSlotsByName()自动创建连接的槽函数。一个是界面组件sliderSetAge的valueChanged(int)信号的槽函数,其函数名称是on_sliderSetAge_ valueChanged(),这个函数名是在UI Designer里用Go to slot对话框自动生成的(生成方法参见2.3.7节)。窗体上的组件sliderSetAge的滑块滑动时触发执行这个槽函数,其响应代码self.boy.setAge (value)又会使self.boy发射两个ageChanged()信号,与其关联的两个自定义槽函数do_ageChanged_int()和do_ageChanged_str()会被执行,从而在窗体上显示信息。另一个是组件btnSetName的clicked()信号的槽函数on_btnSetName_clicked(),其执行过程类似。
可以为应用程序设置一个图标,这样,应用程序的每个窗体将自动使用这个图标作为窗体的图标。在myWidget.py文件的测试程序部分添加设置应用程序图标的代码:
app = QApplication(sys.argv) #创建GUI应用程序
icon = QIcon(":/icons/images/app.ico")
app.setWindowIcon(icon)
就是从资源文件里提取了一个图标作为应用程序的图标。当然,也可以使用QWidget的setWindowIcon()函数为一个窗体单独设置图标。
安装PyQt5时不会安装完整的类库帮助文档,PyQt5的在线Reference Guide提供了PyQt5使用中的一些关键技术问题的说明,但是关于具体的某个类的信息并不完整,不如Qt官网上的帮助文档信息全面。
要离线获取一个类的详细帮助信息,可以使用Qt Creator的帮助窗口。例如,在Qt Creator的帮助窗口里搜索QSpinBox,其资料页面如图2-32所示,这里有对QSpinBox类的简单说明和主要特性的示例代码,列出了其所有的属性、类型定义、公共接口函数、公共槽函数、信号等,并且可以查看每一项的详细资料。
图2-32 在Qt Creator的帮助窗口查找类的详细信息
Qt类库包含的类很多,具体到某个特定的类,其属性、接口函数、信号也很多,不可能全部介绍或列出来。对任何一种编程语言来说,其自带的帮助文档的信息都是最全面最准确的,学习时要善于查找帮助信息。
PyQt5安装后虽然没有Qt Creator里那样详细的类库帮助文档,但是可以通过Python的一些基本指令获取类或函数的内置帮助信息。例如,dir()指令可以显示一个类的所有接口信息;help()指令可以显示一个类的详细接口定义或一个函数的原型定义。
例如,要在Python Shell里查看QSpinBox的帮助信息,可执行下面的指令:
>>> from PyQt5.QtWidgets import QSpinBox
>>> dir(QSpinBox)
指令dir(QSpinBox)会列出QSpinBox的所有属性和方法的名称,包括所有从父类继承的属性和方法。
>>> help(QSpinBox)
指令help(QSpinBox)会更详细地列出QSpinBox类的所有属性和方法,它会先列出QSpinBox类里新定义的属性和方法,然后依次列出父类的属性和方法。接口函数(即方法)会显示输入输出参数定义。
help()指令也可以显示一个方法的函数原型(如QSpinBox.setValue()函数)的帮助信息:
>>> help(QSpinBox.setValue)
Help on built-in function setValue:
setValue(...)
setValue(self, int)
其中的最后一行表示setValue()函数需要一个int类型的输入参数,没有返回值。self是Python中所有类的接口函数的第一个参数,不看作函数参数。
>>> help (QSpinBox.value)
Help on built-in function value:
value(...)
value(self) -> int
上面显示的是QSpinBox.value()函数的帮助信息,最后一行表示value()函数返回一个int类型的数据,没有输入参数。
PyQt5的内置帮助信息虽然不详细、查阅不方便,但是可以提供最准确的信息,特别是在函数的输入输出参数定义上。对于某些类或函数,Qt C++类库中的定义和PyQt5中的定义有差异,应该以PyQt5的定义为准。
PyQt5是Qt C++类库的一个Python绑定,它包含了很多模块,在PyQt5安装后的目录“D:\Python37\Lib\site-packages\PyQt5”里可以看到所有模块的文件。在前面的示例程序中已经用到了QtWidgets、QtCore、QtGui等模块,PyQt5中常用的几个模块如表2-5所示。
表2-5 PyQt5中常用的模块
PyQt5模块名 |
主要功能 |
包含的类示例 |
---|---|---|
QtCore |
提供核心的非GUI功能的类,包括常用的名称空间Qt |
QFile、QDir、QTimer等Qt中的非界面组件类 |
QtGui |
提供GUI设计中用于窗口系统集成、事件处理、绘图等功能的类 |
QIcon、QFont、QPixMap、QCloseEvent、QPalette、QPainter等GUI底层实现类 |
QtWidgets |
提供GUI设计中所有窗体显示的类,包括各种窗体、标准对话框、按钮、文本框等组件 |
QMainWindow、QWidget、QDialog等窗体 |
QtMultimedia |
提供音频、视频、摄像头操作的类 |
QCamera、QAudioInput、QMedaiPlayer等 |
QtMultimediaWidgets |
提供多媒体窗体显示的类 |
QCameraViewfinder、QVideoWidget等 |
QtSql |
提供SQL数据库驱动、数据查询和操作的类 |
QSqlDatabase、QSqlQuery、QSqlRecord等 |
在Python程序里用到某个PyQt5的类时,需要用import语句导入这个类,例如在前面的示例程序中用过这样的导入语句:
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import pyqtSlot, pyqtSignal
from PyQt5.QtGui import QIcon
因为Qt的类一般都以大写字母Q开头作为类名,与Python自带的类或其他程序包的类有很好的区分度,所以一般导入具体的类,然后在程序里直接使用这个类。
尽量不要使用类似于这样的导入语句:
from PyQt5.QtWidgets import *
这样虽然可以导入PyQt5.QtWidgets中的所有类并且直接使用,但是会导入很多不需要用到的类,这可能使程序运行变慢。
对于一个具体的类,如何知道它属于哪个模块呢?例如,对于类QPalette,如何知道它属于哪个模块,从而使用正确的import语句呢?
Qt C++的类库也是以模块组织的,Qt C++类库中的模块与PyQt5中的模块基本是对应的,可以在Qt Creator的帮助页面查找一个类的详细资料来查到其属于哪个模块。例如,QPalette类的帮助信息的基本描述如图2-33所示,其中有一行是:
图2-33 Qt帮助文档里QPalette类的基本描述
qmake: QT += gui
这表明在Qt C++类库中,QPalette是属于gui模块的,那么在PyQt5中对应的模块就是PyQt5.QtGui,所以导入语句应该是:
from PyQt5.QtGui import QPalette
Qt帮助文档中qmake语句常见的描述与PyQt5模块的对应关系如表2-6所示。
表2-6 Qt帮助文档里的qmake描述与PyQt5模块的对应关系
Qt帮助中qmake描述 |
对应的PyQt5模块 |
示例导入语句 |
---|---|---|
QT += core |
QtCore |
from PyQt5.QtCore import QDateTime |
QT += gui |
QtGui |
from PyQt5.QtGui import QIcon |
QT += widgets |
QtWidgets |
from PyQt5.QtWidgets import QFileDialog |
QT += multimedia |
QtMultimedia |
from PyQt5.QtMultimedia import QAudioInput |
QT += multimediawidgets |
QtMultimediaWidgets |
from PyQt5.QtMultimediaWidgets import QVideoWidget |
QT += sql |
QtSql |
from PyQt5.QtSql import QSqlQuery |
PyQt5中大部分类的接口函数,以及每个函数的输入输出参数定义与Qt C++类库中的是一致的,所以在Qt Creator中查询帮助信息就可以知道类的接口或一个函数的输入输出参数。
但是有少量PyQt5的类或接口函数与Qt C++类库中的是不一样的。例如,对于QDataStream类,Qt C++类库中使用流操作符“>>”和“<<”实现各种类型数据的输入和输出,但是PyQt5中的QDataStream类没有这两个流操作符,而是定义了很多接口函数进行各种数据的输入和输出(详见9.3节)。
另外,有少量函数的接口在PyQt5和Qt C++中的定义不一样。例如,QFileDialog类的getOpenFileName()在Qt C++中的函数原型(省略了输入参数)是:
QString getOpenFileName(…);
而用help()指令查看的PyQt5中的函数原型(省略了输入参数)是:
getOpenFileName(…) -> Tuple[str, str]
getOpenFileName()函数在Qt C++和PyQt5中的输入参数相同,所以上面都省略了输入参数的显示。但是在Qt C++中,getOpenFileName()函数只返回一个选择的文件名,而在PyQt5中,getOpenFileName()返回一个Tuple类型的数据,第一个str类型数据是选择的文件名,第二个str类型数据是使用的文件过滤器。如果直接按照Qt C++中的函数原型在Python中使用QFileDialog.getOpenFileName()函数就会出现问题。
在Qt C++类库和PyQt5之间存在差异的类和接口函数并不多,但如果不知道这些差异,按照Qt C++类库的接口定义来使用PyQt5中的相应类或函数就会出现问题。例如,只根据Qt帮助文档里的函数原型使用PyQt5中的类或函数,或者是熟悉Qt C++类库使用的读者根据经验使用这些有差异的类或函数。
下面是整理的本书示例程序或使用PyQt5过程中遇到过的有差异的类或函数,这不是覆盖整个PyQt5的清单,不全面,但是可以让读者遇到此类问题时避免落入陷阱耗费时间。下面整理的内容只是列出了这些有差异的类或函数,并做简单说明,至于具体的差异之处,书中示例程序中涉及的地方会有具体说明。读者在用到以下这些类或函数时,也可以查阅Qt C++帮助文档和PyQt5内置帮助信息来明确这些差异之处。
(1)QDataStream类:接口函数存在较大差异,Qt C++中使用流操作符“>>”和“<<”,PyQt5中使用大量的接口函数替代流操作符。
(2)QFileDialog类:三个类函数getOpenFileName()、getOpenFileNames()、getSaveFileName()的返回数据有差异。Qt C++中只返回文件名或文件名列表,而PyQt5中返回的是一个Tuple类型的数据,第一个元素是文件名或文件名列表,第二个元素是使用的文件名过滤器。
(3)QFontDialog类:类函数getFont()的输入参数、返回数据有差异。
(4)QInputDialog类:getText()、getInt()等类函数返回数据有差异。
(5)QMediaRecorder类:supportedAudioSampleRates()函数返回数据有差异。
C++是强制类型定义的语言,Python是动态数据类型语言,而且两种语言之间的数据类型有一些差异。例如对于字符串数据,Python有内建的str类型,而Qt C++中使用QString类。
Qt C++类库转换为PyQt5后,某些Qt C++中的数据类型与Python中的数据类型存在对应关系,知道这些常见的对应关系后,就可以根据Qt Creator里查到的Qt C++函数原型迅速知道Python中的函数原型,从而正确使用这些函数。
Qt C++的名称空间(namespace)Qt包含大量的枚举类型的定义,例如,表示预定义颜色的枚举类型:
enum Qt::GlobalColor
其部分枚举值有Qt::white、Qt::black、Qt::red、Qt::blue等。
PyQt5.QtCore模块中的类Qt对应于Qt C++类库中的名称空间Qt,这些枚举类型常量都通过类属性访问,例如预定义颜色常量Qt.white、Qt.red等。
在Qt C++中,也经常在类里定义枚举类型,例如QPalette类定义的用于表示颜色角色的枚举类型:
enum QPalette::ColorRole
其部分枚举值有QPalette::Window、QPalette::Text等。
在PyQt5中,对应的枚举类型就是QPalette.ColorRole,而这些枚举类型常量作为类属性访问,也就是QPalette.Window、QPalette.Text等。
PyQt5中没有QString类型,Qt C++中的QString会被自动转换为Python的str类型,例如,C++中的一个函数返回值是QString类型:
QString QFileDialog::getExistingDirectory(…);
在PyQt5中的返回值就是str类型:
getExistingDirectory(…) -> str
由于返回结果是Python的str类型,不能使用QString的接口函数对返回结果进行处理,而应该使用Python的str类型的接口函数。
在Qt C++中用QList<type>定义类型为type的数据列表,而在Python中有内建的list数据类型,所以,Qt C++中的QList<type>在PyQt5中对应的是list[type]数据。例如,Qt C++中用于表示字符串列表的是QStringList类,在PyQt5中没有这个类,而是转换为list[str]数据。
例如,Qt C++中QFileDialog.getOpenFileNames()函数用于返回选择的多个文件的列表,其C++函数原型定义(省略了输入参数)是:
QStringList getOpenFileNames(…);
而在PyQt5的内置帮助信息显示的函数原型(省略了输入参数)是:
getOpenFileNames(…) -> Tuple[List[str], str]
其返回数据是Tuple类型,第一个数据List[str]是选择的文件名称字符串列表,第二个str数据是使用的文件过滤器。所以,这里还存在Qt C++与PyQt5函数参数不一致的问题。
既然返回的结果是list[str],就应该用Python的list数据处理的方法,例如:
fileList,flt=QFileDialog.getOpenFileNames(self,"选择多个文件",
"", "Images(*.jpg)")
if (len(fileList)<1): #fileList是字符串列表
return
for i in range(len(fileList)):
print(fileList[i])
本书的示例程序大部分只有一个窗体,并且采用可视化方法设计窗体UI文件。窗体的常用基类是QWidget、QDialog和QMainWindow,示例Demo2_2中使用的是基于QDialog的窗体,示例Demo2_5中使用的是基于QWidget的窗体。
为了便于读者创建新的示例项目进行编程练习,我们创建了3个单窗体GUI应用程序模板,存放在随书示例程序的“\AppTemplates”目录下。这个目录下有以下3个模板项目文件夹。
dialogApp项目是主窗体基于QDialog的项目模板,项目目录下的文件及其子目录\QtApp里的文件如图2-34所示。
图2-34 dialogApp模板目录下文件(上)及其子目录\QtApp下的文件(下)
子目录\QtApp里是一个Qt GUI应用程序项目,\QtApp\images目录下是Qt项目资源文件用到的一些图标和图片文件。Qt项目文件是QtApp.pro,窗体文件是Dialog.ui。使用Qt Creator打开项目QtApp.pro后的主要工作是可视化设计窗体文件Dialog.ui,利用Go to slot对话框为界面上的组件的信号生成槽函数框架,以便复制槽函数名。
dialogApp项目目录下的批处理文件uic.bat用于编译UI窗体文件和资源文件,双击此文件就可以完成编译,分别生成文件ui_Dialog.py和res_rc.py。uic.bat文件的内容如下:
echo off
rem 将子目录QtApp下的.ui文件复制到当前目录下,并且编译
copy .\QtApp\Dialog.ui Dialog.ui
pyuic5 -o ui_Dialog.py Dialog.ui
rem 编译并复制资源文件
pyrcc5 .\QtApp\res.qrc -o res_rc.py
文件myDialog.py是与Dialog.ui对应的窗体业务逻辑类QmyDialog所在的文件,这个文件的内容如下:
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtWidgets import QApplication, QDialog
##from PyQt5.QtCore import pyqtSlot,pyqtSignal,Qt
##from PyQt5.QtWidgets import
##from PyQt5.QtGui import
##from PyQt5.QtSql import
##from PyQt5.QtMultimedia import
##from PyQt5.QtMultimediaWidgets import
from ui_Dialog import Ui_Dialog
class QmyDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent) #调用父类构造函数,创建窗体
self.ui=Ui_Dialog() #创建UI对象
self.ui.setupUi(self) #构造UI
## ============自定义功能函数=============================
## ===========事件处理函数=============================
## ========由connectSlotsByName()自动关联的槽函数=========
## ==========自定义槽函数================================
## ============窗体测试程序 =============================
if __name__ == "__main__": ##用于当前窗体测试
app = QApplication(sys.argv) #创建GUI应用程序
form=QmyDialog() #创建窗体
form.show()
sys.exit(app.exec_())
这个文件里定义了窗体业务逻辑类QmyDialog,在构造函数里已经有创建窗体的代码。QmyDialog的代码分为几部分,分别用于添加各种函数代码。
文件 myDialog.py 里还有窗体测试程序,所以可以直接运行 myDialog.py 文件以测试QmyDialog类的功能。
文件appMain.py 是将文件myDialog.py中的窗体测试部分的程序单独拿出来作为一个文件。在具有多个窗体的GUI项目里,appMain.py文件的代码创建主窗体然后运行应用程序。
widgetApp项目是主窗体基于QWidget的项目模板,项目目录下的文件及其子目录\QtApp里的文件如图2-35所示。
图2-35 widgetApp模板目录下文件(上)及其子目录\QtApp下的文件(下)
子目录\QtApp里是一个Qt GUI应用程序项目,Qt项目文件是QtApp.pro,窗体UI文件是Widget.ui。
widgetApp项目目录下的批处理文件uic.bat用于编译UI窗体文件和资源文件,双击此文件就可以完成编译,分别生成文件ui_Widget.py和res_rc.py。uic.bat文件的内容如下。
echo off
rem 将子目录QtApp下的.ui文件复制到当前目录下,并且编译
copy .\QtApp\Widget.ui Widget.ui
pyuic5 -o ui_Widget.py Widget.ui
rem 编译并复制资源文件
pyrcc5 .\QtApp\res.qrc -o res_rc.py
文件myWidget.py 是与Widget.ui对应的窗体业务逻辑类QmyWidget所在的文件,这个文件的内容如下。
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtWidgets import QApplication, QWidget
##from PyQt5.QtCore import pyqtSlot,pyqtSignal,Qt
##from PyQt5.QtWidgets import
##from PyQt5.QtGui import
##from PyQt5.QtSql import
##from PyQt5.QtMultimedia import
##from PyQt5.QtMultimediaWidgets import
from ui_Widget import Ui_Widget
class QmyWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent) #调用父类构造函数,创建窗体
self.ui=Ui_Widget() #创建UI对象
self.ui.setupUi(self) #构造UI
## ==============自定义功能函数============================
## ==============事件处理函数===========================
## ==========由connectSlotsByName()自动关联的槽函数========
## =============自定义槽函数==============================
## ============窗体测试程序 ==============================
if __name__ == "__main__": ##用于当前窗体测试
app = QApplication(sys.argv) #创建GUI应用程序
form=QmyWidget() #创建窗体
form.show()
sys.exit(app.exec_())
mainWindowApp项目是主窗体基于QMainWindow的项目模板,项目目录下的文件及其子目录\QtApp里的文件如图2-36所示。
图2-36 mainWindowApp模板目录下文件(上)及其子目录\QtApp下的文件(下)
子目录\QtApp里的Qt项目的窗体文件是MainWindow.ui,用Qt Creator可视化设计窗体MainWindow.ui并生成界面组件的槽函数框架。
项目目录下的批处理文件uic.bat用于编译UI窗体文件和资源文件,双击此文件就可以完成编译,分别生成文件ui_MainWindow.py和res_rc.py。uic.bat文件的内容如下。
echo off
rem 将子目录QtApp下的.ui文件复制到当前目录下,并且编译
copy .\QtApp\MainWindow.ui MainWindow.ui
pyuic5 -o ui_MainWindow.py MainWindow.ui
rem 编译并复制资源文件
pyrcc5 .\QtApp\res.qrc -o res_rc.py
文件myMainWindow.py 是与窗体MainWindow.ui对应的窗体业务逻辑类QmyMainWindow所在的文件,这个文件的内容如下:
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
##from PyQt5.QtCore import pyqtSlot,pyqtSignal,Qt
##from PyQt5.QtWidgets import
##from PyQt5.QtGui import
##from PyQt5.QtSql import
##from PyQt5.QtMultimedia import
##from PyQt5.QtMultimediaWidgets import
from ui_MainWindow import Ui_MainWindow
class QmyMainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent) #调用父类构造函数,创建窗体
self.ui=Ui_MainWindow() #创建UI对象
self.ui.setupUi(self) #构造UI
## ==============自定义功能函数============================
## ==============事件处理函数===========================
## ==========由connectSlotsByName()自动关联的槽函数========
## =============自定义槽函数==============================
## ============窗体测试程序 ===============================
if __name__ == "__main__": ##用于当前窗体测试
app = QApplication(sys.argv) #创建GUI应用程序
form=QmyMainWindow() #创建窗体
form.show()
sys.exit(app.exec_())
本书的示例程序都使用IDLE编写和运行,这3个项目模板中的uic.bat完全控制了窗体UI文件和资源文件到Python文件的编译过程,当UI文件多于一个,或UI文件名不同于模板中的UI文件名时,直接修改uic.bat文件的内容即可。
PyQt5应用程序的开发主要有两项工作内容:一项是窗体的UI设计,这主要在UI Designer里可视化设计完成;另一项是对应的窗体业务逻辑类的功能实现,也就是在 3 个项目模板的myDialog.py、myWidget.py和myMainWindow.py文件里编写功能实现代码。
读者在学习本书时,如果要自己完成完整示例,可以从这3个项目模板中直接复制一个作为自己的项目,然后可视化设计UI窗体,在业务逻辑类里编写功能实现代码。
本书提供了两套示例源程序,其中一套是具有全部源码的程序,包括Qt项目、UI窗体、Python程序等,实现窗体业务逻辑操作的Python程序文件可以直接运行出结果。使用这套源程序可以查看示例运行结果,查看已设计好的UI窗体,也可以查看Python程序文件中的功能实现代码。
另一套是不完整的程序,包括Qt项目、UI窗体和Python程序文件,但是实现窗体业务逻辑操作的Python程序文件只有基本代码框架,而没有功能实现代码。这套程序是为了便于读者使用已经设计好的UI窗体,根据书上的介绍内容和过程,在Python程序框架里自己编写程序,逐步实现功能。之所以保留UI窗体,是因为UI窗体的可视化设计是个比较耗时间的过程,读者如果自己从头开始设计UI窗体,难以保证所有组件的名称和属性与示例的一致,而使用已经设计好的UI窗体进行编程学习就可以避免这些问题,将学习的重点放在类的各种接口属性和函数的使用,以及业务逻辑功能的实现上。
本书的示例程序都使用IDLE编写和运行,而没有使用另一种常用的Python编程IDE软件Eric。项目模板中使用批处理文件uic.bat可以让读者更清楚掌握UI文件、Qt资源文件到Python文件的编译过程。另外,Eric有以下一些问题。
但Eric也有其优势,例如代码导航功能比较好,代码提示和自动完成功能较强,这些是IDLE的劣势。熟悉Eric的读者可以为本书的示例项目创建一个Eric项目保存到示例项目的文件夹下,然后添加示例的程序文件,将Qt Creator、批处理文件uic.bat和Eric结合起来用,具体的用法如下。
这样可以综合各个软件和方法的优点。例如,将示例项目Demo2_3的整个目录复制为示例Demo2_6,然后在示例根目录下创建一个Eric6的项目文件EricProject,将根目录下的所有文件都添加到这个Eric6项目,然后就可以在Eric6软件里编辑、调试和运行程序了。图2-37是在Eric6里管理示例项目Demo2_6的界面。
图2-37 在Eric6里管理示例Demo2_6
由于本书的示例程序一般不是太复杂,代码行数不多,因此使用IDLE来编程和运行测试。熟悉Eric6,或对使用Eric6管理项目感兴趣的读者可以使用Eric6来编程。