- python_pyside6_learning
- 介绍
- 这是练习pyside6创建UI的仓库, 教程来自于youtube的up主:Wanderson.
- 相似界面设计效果,使用Qt Creator(QML)设计gui的教程视频
- 其他界面链接
- 软件架构
- 安装教程
- 学习笔记
- 第1课:部署 `python3.9` + `pyside6` 开发环境
- 第2课:创建主窗口 @2021.10.19、2021.10.23
- 第3课:右侧`content`画面中,创建小部件和页面(堆栈窗口控件) @2021.10.23, 24
- 第4课:构建左侧菜单,使用 `QPushButton`
- 第5课:左菜单toggle按钮的点击事件:展开和折叠左侧菜单,学习使用动画 `animation`,做出动态效果
- 第6课:自定义按钮控件,第一节: `PyPushButton`,继承自 `QPushButton`,修改按钮显示图标。
- 第7课:自定义按钮控件,第二节: 增加自定义的图标
- 第8课:添加自定义按钮控件的单击事件,并创建一些简单的示例。
- 打包EXE笔记:使用 `pyinstaller` 打包 `python+pyside6` 程序
- 1、实现步骤
- 2、使用图形界面打包
- 3、参考资料
- 其他阅读:不推荐使用 `单文件模式` 的原因(从参考资料中copy)
- 继续练习:增加组件到`pages`中
- 组件1:
- 参与贡献
- 特技
python_pyside6_learning
主要目的:搞清楚使用pyside6-Qt Designer(widgets) 设计gui的从0到1的大致思路。
gitee仓库
介绍 这是练习pyside6创建UI的仓库, 教程来自于youtube的up主:Wanderson.- github链接
- youtube
- pyside6链接
- pyside6文档
Python, Qt Quick - MODERN GUI / FLAT STYLE / BANK ConCEPT - EDIT [ FREE ON GITHUB NOW ]
- 第1课
- 第2课
- 第3课
- 第4课
- 第5课
- 第6课
- 第7课
- 第8课
- 第9课
- 第10课
- 第11课
- 第12课
- 第13课
- PyOneDark
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UO7xR4xy-1639301893694)(doc/image/img00.png)] - Bank Gui
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RRW4IZM2-1639301893702)(doc/image/img0.png)]
软件架构说明
安装教程1、使用pycharm下载pyside6, 安装pyside6 package
- xxxx
- xxxx
- python3.9 虚拟环境 venv_pyside6 创建
- 1、在 PyCharm 命令终端中,输入: conda create venv_pyside6 python==3.9
- 2、激活虚拟环境: conda activate venv_pyside6; 使用pycharm ide需要手动切换python解释器为此虚拟环境
- 3、查看已有的虚拟环境: conda env list; PyCharm 切换命令终端为 Command prompt, 可以看到括号内的环境为刚切换的虚拟环境。
- 在 Command prompt 中,安装库
- 1、安装 pyside6, 输入: pip install pyside6
- 2、安装 pyinstaller, 输入: pip install pyinstaller
- 创建组织工程文件夹目录
- 根目录
- main.py:启动程序
- qt_core.py:qt库导入管理程序
- gui folder:UI设计文件存放处
- windows folder:窗口设计
- main_window:主窗口设计文件夹
- ui_main_window.py: 用代码拼出UI主界面
- main_window:主窗口设计文件夹
- windows folder:窗口设计
- 程序注释
添加脚本开头的共同注释部分(增加中文翻译), 也可以使用 PyCharm 自带的创建py文件自动加抬头注释。
# /// # # BY: Author name # PROJECT MADE WITH: Qt Designer and PySide6 # V: 1.0.0 # # This project can be used freely for all uses, as long as they maintain the # respective credits only in the Python scripts, any information in the visual # interface (GUI) can be modified without any implication. # # There are limitations on Qt licenses if you want to use your products # commercially, I recommend reading them on the official website: # https://doc.qt.io/qtforpython/licenses.html # # 这个项目可以被所有用户自由使用,只要他们只在Python脚本中保留各自的原作者信息, # 可视界面(GUI)中的任何信息都可以被修改而不需要任何暗示。 # 如果你想在商业上使用你的产品,Qt许可证是有限制的,我建议你在官方网站上阅读它们 # ///
- pyside6运行的最小框架
- qt_core.py:增加其他程序需要调用qt库的所有功能。
from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import *
- main.py: app 主程序,创建MainWindow类,继承自QMainWindow。
# import MODULES
import sys
import os
# import QT CORE
from qt_core import *
# MAIN WINDOW
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Python3.9 PySide6 Learning")
# SHOW APP
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("icon.ico"))
window = MainWindow()
# window.show() # 展示我们的app 窗口,或者在MainWindow()构造函数中写self.show()
sys.exit(app.exec())
4.工程文件结构优化
- 使用独立的py文件ui_main_window.py,定义UI_MainWindow类,并且定义setup_ui方法。在setup_ui方法中在使用pyside6创建的UI界面类
- 在上述py文件中编写(使用 pyside6 创建&layout一系列控件)或复制来自 Qt Designer自动生成的UI界面代码
# import QT CORE
from qt_core import *
# MAIN WINDOW
class UI_MainWindow(object):
def setup_ui(self, parent):
'''parent: 一般由 main.py文件中MainWindow类的实例self变量作为父类,目的是UI_MainWindow实例做mai文件中的UI界面。
通常,在main.py,这样调用setup_ui方法
# SETUP MAIN WINDOW
self.ui = UI_MainWindow()
self.ui.setup_ui(self)
'''
if not parent.objectName(): # 此判断,经测试,一定会进入
parent.setObjectName("MainWindow") #TODO(wangjh-xm): 暂时不清楚setObjectName的作用效果。
# print('test')
# SET INITIAL PARAMETERS
# ///
parent.resize(1200, 720) # 重新设启动时的界面大小
parent.setMinimumSize(960, 540) # 设置手动调整的最小尺寸
- main.py文件中的调用方式
- 导入UI_MainWindow类,并生成self.ui实例变量。通过 self.ui.setup_ui(self) 让self.ui作为MainWindow实例变量self的界面。
# import MODULES
import sys
import os
# import QT CORE
from qt_core import *
# import MAIN WINDOW
from gui.windows.main_window.ui_main_window import UI_MainWindow
# MAIN WINDOW
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Python3.9 PySide6 Learning")
# SETUP MAIN WINDOW
self.ui = UI_MainWindow()
self.ui.setup_ui(self)
# SHOW APP
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("icon.ico"))
window = MainWindow()
# window.show() # 展示我们的app 窗口,或者在MainWindow()构造函数中写self.show()
sys.exit(app.exec())
- ui_main_window.py代码增加:实现典型UI布局设计: menu(左侧) + content(右侧)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2mVWMYGA-1639301893704)(doc/image/img.png)]
Qframe画布的作用:主要控制边框样式,继承自QWidget
主要思路:
- 创建一个中央画布central_frame
- 创建主布局main_layout,设定中央画布中的控件布局方式是水平(H,推荐)/垂直(V)布局
- 创建一个左侧菜单画布left_menu,同时创建一个右侧内容画布content,通过CCS格式,设定其背景颜色,便于区分不同画布
- 调用主布局增加窗口的方法 main_layout.addWidget,将左右侧画布依次加到主布局中
- 设置父类中央窗口parent.setCentralWidgets是中央画布central_frame
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-36JaigBH-1639301893705)(doc/image/img1.png)]
美化思路: - 在主布局中,边缘间隔setContentsMargins设定成0,左右画布中间间隔setSpacing也设置成0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FGuQXj1g-1639301893706)(doc/image/img2.png)] - 左菜单栏最大宽度(水平布局)/高度(垂直布局) setMaximumWidth/setMaximumHeight,设定成50
- 水平布局
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yxu1UpaS-1639301893707)(doc/image/img3.png)]
- 水平布局
# import QT CORE
from qt_core import *
# MAIN WINDOW
class UI_MainWindow(object):
def setup_ui(self, parent):
'''parent: 一般由 main.py文件中MainWindow类的实例self变量作为父类,目的是UI_MainWindow实例做mai文件中的UI界面。
通常,在main.py,这样调用setup_ui方法
# SETUP MAIN WINDOW
self.ui = UI_MainWindow()
self.ui.setup_ui(self)
'''
if not parent.objectName(): # 此判断,经测试,一定会进入
parent.setObjectName("MainWindow") # TODO(wangjh-xm): 暂时不清楚setObjectName的作用效果。
# print('test')
# SET INITIAL PARAMETERS
# ///
parent.resize(1200, 720) # 重新设启动时的界面大小
parent.setMinimumSize(960, 540) # 设置手动调整的最小尺寸
# CREATE CENTRAL WIDGET
self.central_frame = Qframe()
# CREATE MAIN LAYOUT
self.main_layout = QHBoxLayout(self.central_frame)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
# LEFT MANU
self.left_menu = Qframe()
self.left_menu.setStyleSheet("background-color: #44475a")
self.left_menu.setMaximumWidth(50)
# CONTENT
self.content = Qframe()
self.content.setStyleSheet("background-color: #282a36")
# ADD WIDGETS TO APP
self.main_layout.addWidget(self.left_menu)
self.main_layout.addWidget(self.content)
# SET CENTRAL
parent.setCentralWidget(self.central_frame)
- 垂直布局
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-avqLDdFZ-1639301893709)(doc/image/img4.png)]
# import QT CORE
from qt_core import *
# MAIN WINDOW
class UI_MainWindow(object):
def setup_ui(self, parent):
'''parent: 一般由 main.py文件中MainWindow类的实例self变量作为父类,目的是UI_MainWindow实例做mai文件中的UI界面。
通常,在main.py,这样调用setup_ui方法
# SETUP MAIN WINDOW
self.ui = UI_MainWindow()
self.ui.setup_ui(self)
'''
if not parent.objectName(): # 此判断,经测试,一定会进入
parent.setObjectName("MainWindow") # TODO(wangjh-xm): 暂时不清楚setObjectName的作用效果。
# print('test')
# SET INITIAL PARAMETERS
# ///
parent.resize(1200, 720) # 重新设启动时的界面大小
parent.setMinimumSize(960, 540) # 设置手动调整的最小尺寸
# CREATE CENTRAL WIDGET
self.central_frame = Qframe()
# CREATE MAIN LAYOUT
self.main_layout = QVBoxLayout(self.central_frame)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
# LEFT MANU
self.left_menu = Qframe()
self.left_menu.setStyleSheet("background-color: #44475a")
self.left_menu.setMaximumHeight(50)
# CONTENT
self.content = Qframe()
self.content.setStyleSheet("background-color: #282a36")
# ADD WIDGETS TO APP
self.main_layout.addWidget(self.left_menu)
self.main_layout.addWidget(self.content)
# SET CENTRAL
parent.setCentralWidget(self.central_frame)
第3课:右侧content画面中,创建小部件和页面(堆栈窗口控件) @2021.10.23, 24
在第二课的基础上,继续在ui_main_window.py文件中,为 content 画面增加UI控件
堆栈窗口控件, 中文教程参考
-
1:增加 Top Bar
- 先创建顶部工具栏画面 Top Bar,设置固定尺寸以及背景色和前景色。
- 再设定 content 画面的布局方式为垂直布局,设置内容间隔和控件之间的间隔值为0
- 再将 Top Bar 画面增加到 content_layout 布局中。
- 上述步骤显示效果(content画布加上 Top Bar画布):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gyy29A1Q-1639301893715)(doc/image/img5.png)]
-
2:增加 pages
- 创建堆栈窗口控件 QS tackedWidget 实例 self.pages ;可实现页面切换(索引)效果,比选项卡控件 QTabWidget 操作的自由度更高(切换控件可以自由定制,通过页面索引控制页面切换)。
- 将此控件实例增加到 content_layout布局中
- 补充上个课程中 left_menu 的最小尺寸宽度设定
- 显示效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5IuJIrbF-1639301893724)(doc/image/img6.png)]
-
3:模仿 Top Bar,增加一个 Bottom Bar
- 复制 # TOP BAR 段,修改为 bottom_bar
- 将此控件实例 self.bootom_bar 增加到 content_layout布局中
- 显示效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qhyqr0Ht-1639301893725)(doc/image/img7.png)]
-
4:在 Top/Bottom Bar 画布中,增加 Label控件。
- 在 top_bar 代码的后面,新增 QLabel 控件实例 top_label_left
- 新增 QSpacerItem 临时间隔区控件,目的是将顶部的左右2个 QLabel分在2侧。
- 新增 右边的 QLabel 标签控件 top_label_right, 并设置字体风格。
- 最后,新增 top_bar 画面中的布局规格 self.top_bar_layout:
- 1、水平布局 QHBoxLayout(self.top_bar) , 并且设置边缘间隔 Margins 为合适的距离,10。
- 2、将上述 左右 QLabel 以及中间的 QSpacerItem 依次放入上述水平布局中。
-
# ADD TO TOP BAR LAYOUT self.top_bar_layout.addWidget(self.top_label_left) self.top_bar_layout.addItem(self.top_spacer) # 注意 spacer 是Item,所以用addItem self.top_bar_layout.addWidget(self.top_label_right)
-
- 同样的,参考 top_bar 的 label 添加方法,新增 label 到 bottom_bar
- 效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YMPMTce8-1639301893726)(doc/image/img8.png)]
-
5: 在 pages 画布中,添加多个 page (借助 QStackedWidget 堆栈窗口控件实现,使用 Qt-Designer 自动生成相对应的 python 代码)
- Qt-Designer 路径:...envsvenv_new_hmiLibsite-packagesPySide6designer.exe
- PyCharm IDE中将此应用添加外部工具中,便于后续使用。
- PyCharm IDE中添加 uic工具。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nTs2j5Vo-1639301893727)(doc/image/img9.png)]
- 新建文件页面中,在Widget一栏展开后,才能找到 QStackedWidget
- 自动生成python代码功能测试;
- 生成python代码报错:uic在Pyside根目录下,需要再创建一层bin目录,将uic.exe 放入 bin中。
- 缺少uic的处理办法2
- 新建 ui_pages.ui 文件(QStackedWidget), 保存到 guipages 文件夹中
- 右键增加pages
- 在每页上添加垂直布局以及label控件,并调整对齐方式为中心对齐,修改堆栈窗口名称为 application_pages。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KUJiAIlL-1639301893729)(doc/image/img10.png)] - 使用 uic.exe 生成 ui_pages.py文件
- … envsvenv_new_hmiLibsite-packagesPySide6binuic.exe ui_pages.ui -o ui_pages.py
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wu3PQY27-1639301893730)(doc/image/img11.png)]
- … envsvenv_new_hmiLibsite-packagesPySide6binuic.exe ui_pages.ui -o ui_pages.py
- 在 前面创建的 self.pages 后面,继续添加 ui设计文件中类的实例化 self.ui_pages
- 使用 self.ui_pages.setupUi 方法,与 self.pages 连接起来。
- 最后效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHxFL6WO-1639301893731)(doc/image/img12.png)]
- Qt-Designer 路径:...envsvenv_new_hmiLibsite-packagesPySide6designer.exe
在第三课的基础上,继续在ui_main_window.py文件中,为 左侧菜单self.left_menu 画面增加UI控件。
思路总结如下,在左侧菜单frame画面中,增加垂直布局,然后在布局中依次增加顶部画面frame、底部画面frame、最底部标签label。
然后,在顶部/底部frame画面中,增加垂直布局,在布局中依次增加pushButton按钮。顶部与底部之间,增加spacer间隔项分割。
总结手动创建控件的思路就是:先创建frame画布;然后规定画布中的布局方式为水平/垂直...布局;在布局中,添加需要增加的控件,如pushButton、label等,
画面/子控件之间可以使用spacer间隔隔开。
-
1、增加最底层的4个大控件
- 规定 left_menu 的布局方式是垂直布局,注意将布局与左菜单画面绑定起来的语句是self.left_menu_layout = QVBoxLayout(self.left_menu)。
- 依次新建顶部画面菜单 self.left_menu_top_frame、中间分隔项self.left_menu_spacer、底部画面菜单self.left_menu_bottom_frame以及 版本标签self.left_menu_label_version。
- 效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-arcnM6up-1639301893733)(doc/image/img13.png)]
-
2、在顶部/底部画面中增加按钮
- 依然先规定/创建 布局方式为垂直布局,
- 在创建 几个按钮,
- 然后将按钮对象依次添加到垂直布局中。
- 最后,限定只有画布的背景色是红色,其他子部件的背景色默认,前提是限定前指定对象名称;Qt-CSS样式表含义参考
self.left_menu_top_frame.setObjectName("left_menu_top_frame")
self.left_menu_top_frame.setStyleSheet("#left_menu_top_frame { background-color: red; }")
- 效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mdtf2Q4F-1639301893734)(doc/image/img14.png)]
第5课:左菜单toggle按钮的点击事件:展开和折叠左侧菜单,学习使用动画 animation,做出动态效果- 1、在main.py 中,定义按钮的响应事件
- 获取ui中左菜单的宽度
- 设计展开和缩回的目标宽度值变化逻辑:如果进入点击事件时,按钮是缩回状态,那么目标宽度就是展开时的宽度,反之就是缩回状态的宽度。
- 借助 QPropertyAnimation 功能,定义一个属性更改动画
- 开始值是当前菜单某个属性值(宽度)
- 结束值是动画变化的目标值(目标宽度)
- 设置持续时间(Duration)
- 设置值缓慢变化的曲线类型(EasingCurve)
- 开始动画
-效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q9UgnWRM-1639301893735)(doc/image/img15.png)]
- 1、自定义 PyPushButton 类, 继承自 QPushButton
- 设定控件大小,颜色响应等默认参数
- 存档在 gui/widgets 中
- 2、在 ui_main_window.py 文件中,修改按钮控件为 PyPushButton 的实例。
- 初始化实例时,通过 text 形参设置按钮的显示名称,is_active 设置当前控件为激活状态。
- 3、效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FrjNOZKy-1639301893736)(doc/image/img16.png)]
- 1、设计/寻找合适的图标
- 格式 .svg
- 存档在 gui/images/icons 中
- 自定义图标导入方法 draw_icon(self,qp,image,color), 写死图标搜索路径就在gui/images/icons下
- 重构 paintEvent(self, event) 方法,使用 QPainter、QRect ,调用 draw_icon 方法
- 2、在 ui_main_window.py 文件中,按钮初始化形参中调用 icon_path形参,填入图标路径(仅填图标名以及后缀)
- 3、效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2iz9iydg-1639301893738)(doc/image/img17.png)]
-
1、添加按钮的单击事件
- 使用按键控件的clicked.connect 方法,连接槽函数。实现显示 pages 的不同页面
- 增加 reset_selection方法,复位按钮被激活效果,实现点击哪个按钮,只那个按钮会有选中的效果。
-
2、使用QT Designer,在主页中添加单行文本框 QLineEdit 和 按钮 PushButton
- 删除page1中的标签控件,先增加画布控件Qframe
- 设置画布最小/大尺寸:500*70
- 右键画布,选择布局对齐:水平居中
- 往画布中添加 标签和按钮控件,并设置标签与按钮控件布局为 栅格布局
- 设置画布布局方式为 垂直布局
- 修改画布layout中的margin, 9->0
- 修改标签与按钮的样式表
- 在标签属性placehoderText中,填写文本框提示性文字。
- 修改按钮的最小尺寸:120*36,最大尺寸高度:36
- 在 main.py 文件中, 增加 单行文本框的字符串,以及按钮单击事件。
- 在ui_pages.ui中,增加输入完文本行后,按enter键直接发送文本,需要重新uic
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mPNtWJxh-1639301893739)(doc/image/img22.png)]
- 删除page1中的标签控件,先增加画布控件Qframe
-
3、效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CzcverL1-1639301893740)(doc/image/img18.png)]
- 单目录模式运行(-D/–onedir, 默认、推荐模式)
- 在命令终端中,输入: pyinstaller -D main.py,执行完之后,到dist文件夹中找到 main.exe 运行。
- 添加icon程序图标,在 pyinstaller 生成的 spec 规格文件中,exe命令内容中添加 icon='icon.ico'
- 取消exe运行时控制台的显示,在 spec 文件中,exe命令内容中修改 console=True -> console=False
- 输入终端命令:pyinstaller main.spec, 测试新生成的 main.exe ,然后把icon文件复制一份到与 main.exe 同目录下,观察图片和控制台是否成功设置。
- 单文件模式(-F/–onefile)
- pyinstaller -F main.py
- 单目录模式运行时,图标文件资源打包设置(优点:不需要使用冻结路径的方式)
- 在 spec文件中,设置打包时一并将资源文件复制到指定目录下。
datas=[('.\*.ico','.\'),('gui\images\icons','gui\images\icons')], # 增加程序图标以及控件图标文件
- 效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtcmN6E8-1639301893741)(doc/image/img19.png)]
- 第一步下载安装pip包,-i可以指定下载源(如清华源):
pip install auto-py-to-exe -i https://pypi.tuna.tsinghua.edu.cn/simple
-
安装过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0h3edO8d-1639301893741)(doc/image/img20.png)] -
单目录EXE输出,启动效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Vz954AP-1639301893742)(doc/image/img21.png)]
- 单目录模式解释
- spec 规格文件解释
- 知乎 打包教程链接
- CSDN Pyinstaller 打包发布经验总结
之所以强调这一点,并不是因为单文件模式存在什么无法解决的问题。如果你非常清楚该模式的运行机制,并且在写代码的时候小心避开这些坑的话,那么所有问题都是可以避免的。但实际上,可以说 PyInstaller 的用户 99% 都达不到这个要求,而只要你写的程序有点规模的话,几乎无一例外会踩到坑里。基于这种考虑,我从来不推荐用户使用单文件模式。如果你认真看过本文,并非常肯定自己能避开下面提到的问题,那么请使用单文件模式无妨。否则,还是老老实实的使用默认模式吧。
有个问题你不妨考虑一下:我们把程序编译成了单一的可执行文件,但是从上面的单目录模式结果可以知道,要让程序运行还需要其他很多的辅助文件,此外我们自己也可以添加数据文件(–add-data)和二进制文件(–add-binary),那么这些文件哪里去了?你如何访问这些文件?
这才是秘密所在!本质上,Python 是解释程序,而不是 native 的编译程序,它并不能真正产生出真正单一的可执行文件。PyInstaller 这里变了个小戏法,如果我们使用单文件模式的话,那么 PyInstaller 生成的实际上类似于 WinZIP/WinRAR 生成的自动解压程序。它需要先把所有文件解压到一个临时目录(通常名为_MEIxxxx,xxxx是随机数字),再从临时目录加载解释器和附属文件。程序运行完毕后,如果一切正常,那么它会把临时目录再悄悄删除掉。
为了让这个过程顺利执行,PyInstaller 会对运行时的 Python 解释器做一些修改,特别是下面两个变量:
sys.frozen 如果你直接运行 Python 脚本的话,那么该变量是不存在的。但 PyInstaller 则会设置它为 True(不论单目录还是单文件模式)。因此,你可以用它来判断程序是手工运行的,还是通过 PyInstaller 生成的可执行文件运行的;
sys._MEIPASS 如果使用单文件模式,该变量包含了 PyInstaller 自动创建的临时目录名。你可以用 --runtime-tmpdir 命令行开关来强制使用特定的目录,但是鉴于最终用户有哪些目录不在程序员控制范围内,通常还是应该避免使用它。
我们可以自己写一个程序来验证:
import sys
import os
print('__file__:', __file__)
print('sys.executable:', sys.executable)
print('sys.argv[0]:', sys.argv[0])
print('os.getcwd():', os.getcwd())
print('sys.frozen:', getattr(sys, 'frozen', False))
print('sys._MEIPASS:', getattr(sys, '_MEIPASS', None))
input('Press any key to exit...')
把该脚本编译到单文件模式,然后执行。注意,先不要按任何键(否则程序退出,临时目录就不存在了),然后根据输出结果,可以到资源管理器中找到对应的临时目录:
单文件模式临时目录
你可以看到临时目录包含了运行输出所需的各种辅助文件,除了主程序.EXE 之外。仔细分析一下,我们也能明白为什么单文件模式下容易出错了。尽管 PyInstaller 努力使得各种输出和直接运行脚本的结果尽可能相似,但差别还是很明显的:
file 指向的脚本名不变,但该文件已经不存在于磁盘上了。这使得依赖于 file 去解析相对文件位置的代码非常容易出错。这也是绝大多数错误的来源,请务必注意!
sys.executable 不再指向 Python.exe,而是指向生成的文件位置了。如果你使用该变量判断系统库位置的话,那么也请小心;
os.getcwd() 指向执行文件的位置(双击运行的话是这样,但如果从命令行启动的话则未必)。但请注意,你添加的数据/二进制文件并非位于此目录,而是在临时目录上,不明白这一点的话,也很容易出现找不到文件的问题。
需要说明的是,上述问题不只存在于你自己写的代码里。有相当多的库没有考虑到在 PyInstaller 打包后下执行的场景,它们在使用这些变量的时候很有可能会出问题。事实上这也是 PyInstaller 添加 Runtime Hook 机制的一个重要原因。
如果你的脚本需要引用辅助文件路径的话,那么一种可能的形式如下:
if getattr(sys, 'frozen', False):
tmpdir = getattr(sys, '_MEIPASS', None)
if tmpdir:
filepath = os.path.join(tmpdir, 'README.txt')
else:
filepath = os.path.join(os.getcwd(), 'README.txt')
else:
filepath = os.path.join(os.path.dirname(__file__), 'README.txt')
上述代码并不是唯一可行的代码,或许也不是最简洁的,但是你应当明白了,要正确处理该过程并不是轻而易举的事情。很多用户之所以出错又找不到问题,就是因为他们根本不清楚临时目录这回事,也不知道上哪里去找这些文件。如果使用单目录模式的话,那么文件在哪里是可以直接看到的,出现问题的可能性就小多了,即使有问题也很容易排查。这就是我为什么强烈推荐用户不要使用单文件模式的原因————除了看起来比较清爽之外,单文件模式基本上没有其他好处,而且它带来的麻烦比这一点好处要多太多了。
除此之外,单文件模式还带来了其他一些负面效应:
继续练习:增加组件到pages中 组件1: 参与贡献因为有临时目录和解压文件这个过程,所以单文件模式的程序启动速度会比较慢。对于稍大的程序,这个延迟是肉眼可以感觉到的;
如果你的程序运行到一半崩溃了,那么临时目录将没有机会被删除。日积月累的话,可能会在临时目录下遗留一大堆 _MEIxxxx 目录,占用大量磁盘空间。
或许对你来说上面这两个问题并不是特别重要,但知道它们的存在还是有好处的。
- Fork 本仓库
- 新建 Feat_xxx 分支
- 提交代码
- 新建 Pull Request
- 使用 Readme_XXX.md 来支持不同的语言,例如 Readme_en.md, Readme_zh.md
- Gitee 官方博客 blog.gitee.com
- 你可以 https://gitee.com/explore 这个地址来了解 Gitee 上的优秀开源项目
- GVP 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
- Gitee 官方提供的使用手册 https://gitee.com/help
- Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 https://gitee.com/gitee-stars/



