这里记录一种基于PyQT进行GUI工具开发的项目结构设计及开发、发布工作流,用于快速构建简单且具备一定可拓展性、便于维护的GUI工具并进行打包发布。
开发环境及主要依赖:
- Python 3.9.6
- PyQt 5.15.10
- PyQtFluentWidgets 1.5.1
PyQtFluentWidgets主要用于对PyQt的原生组件进行样式美化,并提供新的组件以及程序设置配置方式。
1.项目结构设计
1.1.结构说明
项目使用基于MVVM架构改进的MVVM-C架构,与MVVM架构基本类似,如下图:
各层职责如下:
- Model:与MVC/MVP/MVVM中职责相同,用于定义数据结构,同时提供数据交互相关的业务逻辑实现方法。
- View:负责前端布局的初始化及释放,同时响应用户交互事件。
- View Model:与MVVM架构的中心类职责类似,负责:
- 从Model层获取数据
- 同步数据到Model中
- 向View层提供格式化数据以及事件处理接口
- 向View层通知数据改变
- 定义业务逻辑
- Controllers:用于整合、串联多个相同功能模块的View Models,负责不同视图间的跳转以及数据流转。
1.2.项目结构
基于此所实现的项目结构如下:
Demo
├─ bin ───────────────── 打包解释器
├─ builds ───────────────── 打包临时目录
├─ dist ───────────────── 打包产物目录
├─ env ───────────────── 开发环境
├─ resource ───────────────── 静态资源目录
│ ├─ img ───────────────── 图像资源目录
│ ├─ qss ───────────────── QSS样式文件目录
│ ├─ ui ───────────────── ui文件目录
│ └─ resource.qrc ───────────────── 资源清单文件
├─ src ───────────────── 源码目录
│ ├─ commons ───────────────── 业务组件目录
│ ├─ components ───────────────── UI组件目录
│ ├─ config ───────────────── 配置文件
│ ├─ controllers ───────────────── 控制器
│ ├─ models ───────────────── 数据模型
│ ├─ view_models ───────────────── 视图模型
│ ├─ views ───────────────── 视图
│ ├─ windows ───────────────── 窗体
│ ├─ __init__.py
│ └─ main.py ───────────────── 主入口
└─ build.ps1 ───────────────── 打包文件
其中bin、builds、dist目录为打包发布相关,将在下一节展开说明,本节暂不涉及。
resource文件夹中的静态资源及qrc资源清单
项目源码应当存放于src目录下,目录内各部分内容为:
- commons:存放相对独立的功能组件,以及工具所依赖的各种基本配置服务,如log、项目配置、信号总线等。
- components:存放在PyQT库基础上进行二次开发的自定义组件。
- config:存放基于QConfig类实例关联的json配置文件,一般仅创建文件夹即可,内部文件会由项目自动生成。
- controllers:控制器,用于整合视图模型,组成完整的功能模块。
- models:数据模型,用于定义功能所需数据模型及数据交互功能。
- view_models:视图模型,用于将视图与数据模型进行双向绑定,并实现基本的基础功能接口。
- views:视图,用于定义前端展示样式,通常由PyUIc工具生成。
- windows:窗体,用于将各控制器进行组装,组成完整程序。
- main.py:主入口,定义App级别配置,实例化窗体。
2.设置
config.py文件存放于src/commons目录内,通过继承QConfig
类并定义类属性来声明程序所需要的各种设置项。QConfig
类引用自qfluentwidgets库,如不使用该组件,可跳过本节,使用原生的QSetting
类进行实现。
"""
@File: config.py
@Description:
@Author:
"""
from qfluentwidgets import QConfig
class Config(QConfig):
""" Config of application """
demo_steeing = ConfigItem("main", "demo", "")
cfg = Config()
qconfig.load('config/config.json', cfg)
程序执行后会在config/config.json
路径内生成config.json,以json的格式实时同步设置变更。
详细使用请见:https://pyqt-fluent-widgets.readthedocs.io/zh-cn/latest/settings.html
3.打包流程及原理
本节内容主要参考:
GitHub - skywind3000/PyStand: :rocket: Python Standalone Deploy Environment !!
https://www.zhihu.com/question/48776632/answer/2336654649
传统使用PyInstaller进行打包的时候,会将当前开发环境下的解释器以及安装的所有依赖库全打进去,造成包体偏大以及安装目录混乱的问题。当使用-F参数打包为单文件并运行时,还额外增加了临时文件自解压、删除的开销,使得执行效率异常低下。
为了解决上述问题,这里参考上述文章及项目对手工打包进行介绍:
3.1.原理
在Python 3.5之后,官方在发布新版本时会发布一个嵌入式Python包,这些包很小,解压出来仅包含一些Python解释器执行所必须的文件。
当需要执行Python程序时,只通过批处理或加壳等方式调用这个嵌入式环境中的python.exe或python3.dll,同样,当需要使用依赖、引用第三方库时,将需要使用的依赖所在文件夹引入执行器的环境变量即可。
3.2.制作打包环境
3.2.1.替换解释器
下载PyStand:Release 20240619 - v1.1.2 · skywind3000/PyStand,解压后如图:
下载开发对应版本embeddable python解释器(此处以3.9.6为例):Windows embeddable package (64-bit)
删除PyStand目录下runtime文件夹内内容,将解压后的embeddable包文件替换进去:
替换后运行PyStand文件夹内的PyStand.exe,正常运行并显示Hello窗体即为制作成功。
验证完成后,将PyStand.exe
及PyStand.int
文件更名为工具名称(以ReportMaker为例)。
最后将PyStand目录下所有内容移动至项目文件夹/bin
目录下,作为项目打包所依赖的基础解释器
3.2.2.修改入口文件
此处入口文件修改仅为当前项目结构下示例,其他项目使用时需根据实际情况进行调整
修改src/main.py
,定义main方法作为程序主入口方法:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from window.mainWindow import Window
def main():
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec_()
if __name__ == '__main__':
main()
修改bin/ReportMaker.int
内容如下:
import sys, os
os.chdir(os.path.dirname(__file__))
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = r'.\site-packages\PyQt5\Qt5\plugins'
sys.path.append(os.path.abspath('script'))
sys.path.append(os.path.abspath('script.egg'))
import main
main.main()
这里主要是将后续打包的源码加入环境变量,而后引入源码的main模块并执行里面的main()
方法。
3.2.3.编写打包脚本
为了便于进行打包操作,应当将整个流程编写为一个脚本,当需要打包时执行即可。
编写src/build.ps1
$CONFIG_PATH="./builds/script.egg"
# resource打包
Copy-Item -Path "./resource/img" -Destination "./builds/resource/img" -Recurse
Copy-Item -Path "./resource/qss" -Destination "./builds/resource/qss" -Recurse
mkdir "./builds/resource/temp"
Compress-Archive -Path .\builds\resource\* -DestinationPath .\builds\resource.zip -Force
Move-Item -Path "./builds/resource.zip" -Destination "./dist/resource.zip"
# script打包更名
Compress-Archive -Path .\src\* -DestinationPath .\builds\script.zip -Force
$TRUE_FALSE=(Test-Path $CONFIG_PATH)
if($TRUE_FALSE -eq "True")
{
remove-Item -Recurse -Force $CONFIG_PATH
}
Rename-Item -Path .\builds\script.zip -NewName "script.egg"
Copy-Item -Path "./builds/script.egg" -Destination "./dist/script.egg"
# 文件合并
if((Test-Path "./builds/bin/") -eq "False")
{
mkdir "./builds/bin/"
}
Copy-Item -Path "./bin/" -Destination "./builds/bin/" -Recurse
Copy-Item -Path "./ReportMaker.lnk" -Destination "./builds/ReportMaker.lnk" -Recurse
Copy-Item -Path "./CHANGELOG.json" -Destination "./builds/CHANGELOG.json" -Recurse
Move-Item -Path "./builds/script.egg" -Destination "./builds/bin/script.egg"
Start-Sleep -s 30
# 打包归档
Compress-Archive -Path .\builds\* -DestinationPath .\dist\ReportMaker.zip -Force
# 环境清理
Get-ChildItem ./builds/* -Include *.* -Recurse | Remove-Item -Recurse
Remove-Item ./builds/* -Recurse
Write-Output "Done"
脚本需使用powershell运行,实际项目使用时可根据需求进行修改。
3.3.裁剪依赖
实际进行项目开发时会使用虚拟环境进行依赖管理,在该项目中我们将虚拟环境创建至env文件夹内:
python -m venv env
进入虚拟环境并安装依赖后,在env\Lib\site-packages
路径下可以找到对应的依赖文件夹,将依赖文件夹内需要的依赖粘贴至bin\site-packages
目录即可。
当然,将整个文件夹内的依赖文件全部粘贴是不合适的,因为其中会包含很多程序实际运行中所不需要的库,这时我们就需要对bin\site-packages
目录内的依赖进行裁剪,在保证程序正常执行的情况下删除掉不需要的依赖从而尽量缩小软件体积。
裁剪依赖这一步可能会耗费比较多的时间,但是通过一步步手动裁剪不仅可以在一定程度上帮助你理清依赖库之间的调用关系,还可以了解各种第三方库的一些内部实现。
3.4.开始打包
到这一步,再进行打包操作就十分简单了,只需要使用powershell运行项目中的build.ps1脚本,然后前往dist
文件夹获取打包产物即可