# python_pyside6_learning **Repository Path**: jiahui1230/python_pyside6_learning ## Basic Information - **Project Name**: python_pyside6_learning - **Description**: github链接:https://github.com/Wanderson-Magalhaes/Curso_PySide6_Em_Portugues youtube:https://www.youtube.com/watch?v=_qOnJ7-tJcM&list=PLfQ7GQSrl0_ung_Wt0PpgOICqA8k6dr3i&index=2 - **Primary Language**: Python - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 4 - **Created**: 2021-10-19 - **Last Updated**: 2024-01-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # python_pyside6_learning >主要目的:搞清楚使用pyside6-Qt Designer(widgets) 设计gui的从0到1的大致思路。 [gitee仓库](https://gitee.com/jiahui1230/python_pyside6_learning.git) ## 介绍 ### 这是练习pyside6创建UI的仓库, 教程来自于youtube的up主:Wanderson. - [github链接](https://github.com/Wanderson-Magalhaes/Curso_PySide6_Em_Portugues) - [youtube](https://www.youtube.com/watch?v=_qOnJ7-tJcM&list=PLfQ7GQSrl0_ung_Wt0PpgOICqA8k6dr3i&index=2) - [pyside6链接](https://pypi.org/project/PySide6/) - [pyside6文档](https://doc.qt.io/qtforpython/PySide6/QtWidgets/index.html) ### 相似界面设计效果,使用Qt Creator(QML)设计gui的教程视频 Python, Qt Quick - MODERN GUI / FLAT STYLE / BANK CONCEPT - EDIT [ FREE ON GITHUB NOW ] - [第1课](https://www.youtube.com/watch?v=pD0UeD7S27s&ab_channel=Wanderson) - [第2课](https://www.youtube.com/watch?v=VMZyJfCYF4s&t=56s&ab_channel=Wanderson) - [第3课](https://www.youtube.com/watch?v=TY2vtsp2QAs&ab_channel=Wanderson) - [第4课](https://www.youtube.com/watch?v=JhPXOcmXf8I&ab_channel=Wanderson) - [第5课](https://www.youtube.com/watch?v=P3DMJPCN5yg&list=RDCMUCy1fv5dh3wQEem1nFAUBJzw&index=2&ab_channel=Wanderson) - [第6课]() - [第7课]() - [第8课]() - [第9课]() - [第10课]() - [第11课]() - [第12课]() - [第13课]() ### 其他界面链接 - [PyOneDark](https://github.com/Wanderson-Magalhaes/PyOneDark_Qt_Widgets_Modern_GUI) ![img.png](doc/image/img00.png) - [Bank Gui](https://github.com/Wanderson-Magalhaes/App_Bank_GUI_v01_PySide_Or_PyQt_Qt_Quick_QML) ![img.png](doc/image/img0.png) ## 软件架构 软件架构说明 ## 安装教程 1、使用pycharm下载pyside6, 安装pyside6 package 2. xxxx 3. xxxx ## 学习笔记 ### 第1课:部署 `python3.9` + `pyside6` 开发环境 - `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` ### 第2课:创建主窗口 @2021.10.19、2021.10.23 1. 创建组织工程文件夹目录 - 根目录 - `main.py`:启动程序 - `qt_core.py`:qt库导入管理程序 - `gui folder`:UI设计文件存放处 - `windows folder`:窗口设计 - `main_window`:主窗口设计文件夹 - `ui_main_window.py`: 用代码拼出UI主界面 2. 程序注释 添加脚本开头的共同注释部分(增加中文翻译), 也可以使用 `PyCharm` 自带的创建py文件自动加抬头注释。 ```python # /////////////////////////////////////////////////////////////// # # 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许可证是有限制的,我建议你在官方网站上阅读它们 # /////////////////////////////////////////////////////////////// ``` 3. `pyside6`运行的最小框架 - `qt_core.py`:增加其他程序需要调用qt库的所有功能。 ```python from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * ``` - `main.py`: app 主程序,创建`MainWindow`类,继承自`QMainWindow`。 ```python # 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界面代码 ```python # 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的界面。 ```python # 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()) ``` 5. `ui_main_window.py`代码增加:实现典型UI布局设计: menu(左侧) + content(右侧) ![img.png](doc/image/img.png) [QFrame画布的作用:主要控制边框样式,继承自QWidget](https://www.cnblogs.com/mosewumo/p/12518429.html) 主要思路: - 创建一个中央画布`central_frame` - 创建主布局`main_layout`,设定中央画布中的控件布局方式是水平(H,推荐)/垂直(V)布局 - 创建一个左侧菜单画布`left_menu`,同时创建一个右侧内容画布`content`,通过CCS格式,设定其背景颜色,便于区分不同画布 - 调用主布局增加窗口的方法 `main_layout.addWidget`,将左右侧画布依次加到主布局中 - 设置父类中央窗口`parent.setCentralWidgets`是中央画布`central_frame` ![img.png](doc/image/img1.png) 美化思路: - 在主布局中,边缘间隔`setContentsMargins`设定成0,左右画布中间间隔`setSpacing`也设置成0 ![img.png](doc/image/img2.png) - 左菜单栏最大宽度(水平布局)/高度(垂直布局) `setMaximumWidth/setMaximumHeight`,设定成50 - 水平布局 ![img.png](doc/image/img3.png) ```python # 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.png](doc/image/img4.png) ```python # 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控件 [堆栈窗口控件, 中文教程参考](https://www.bilibili.com/video/BV154411n79k?p=82) - 1:增加 `Top Bar` - 先创建顶部工具栏画面 `Top Bar`,设置固定尺寸以及背景色和前景色。 - 再设定 `content` 画面的布局方式为垂直布局,设置内容间隔和控件之间的间隔值为0 - 再将 `Top Bar` 画面增加到 `content_layout` 布局中。 - 上述步骤显示效果(`content`画布加上 `Top Bar`画布): ![img.png](doc/image/img5.png) - 2:增加 `pages` - 创建堆栈窗口控件 `QS tackedWidget` 实例 `self.pages` ;可实现页面切换(索引)效果,比选项卡控件 `QTabWidget` 操作的自由度更高(切换控件可以自由定制,通过页面索引控制页面切换)。 - 将此控件实例增加到 `content_layout`布局中 - 补充上个课程中 `left_menu` 的最小尺寸宽度设定 - 显示效果: ![img.png](doc/image/img6.png) - 3:模仿 `Top Bar`,增加一个 `Bottom Bar` - 复制 `# TOP BAR` 段,修改为 bottom_bar - 将此控件实例 `self.bootom_bar` 增加到 `content_layout`布局中 - 显示效果: ![img.png](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` 依次放入上述水平布局中。 - ```python # 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.png](doc/image/img8.png) - 5: 在 `pages` 画布中,添加多个 `page` (借助 `QStackedWidget` 堆栈窗口控件实现,使用 `Qt-Designer` 自动生成相对应的 `python` 代码) - `Qt-Designer` 路径:`...envs\venv_new_hmi\Lib\site-packages\PySide6\designer.exe` - `PyCharm` IDE中将此应用添加外部工具中,便于后续使用。 - `PyCharm` IDE中添加 uic工具。 ![img.png](doc/image/img9.png) - 新建文件页面中,在Widget一栏展开后,才能找到 `QStackedWidget` - 自动生成python代码功能测试; - 生成python代码报错:uic在Pyside根目录下,需要再创建一层bin目录,将uic.exe 放入 bin中。 - [缺少uic的处理办法2](https://blog.csdn.net/zqx119/article/details/107831530) - 新建 ui_pages.ui 文件(`QStackedWidget`), 保存到 `gui\pages` 文件夹中 - 右键增加pages - 在每页上添加垂直布局以及label控件,并调整对齐方式为中心对齐,修改堆栈窗口名称为 `application_pages`。 ![img.png](doc/image/img10.png) - 使用 `uic.exe` 生成 `ui_pages.py`文件 - ... \envs\venv_new_hmi\Lib\site-packages\PySide6\bin\uic.exe ui_pages.ui -o ui_pages.py ![img.png](doc/image/img11.png) - 在 前面创建的 `self.pages` 后面,继续添加 ui设计文件中类的实例化 `self.ui_pages` - 使用 `self.ui_pages.setupUi` 方法,与 `self.pages` 连接起来。 - 最后效果: ![img.png](doc/image/img12.png) ### 第4课:构建左侧菜单,使用 `QPushButton` > 在第三课的基础上,继续在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.png](doc/image/img13.png) - 2、在顶部/底部画面中增加按钮 - 依然先规定/创建 布局方式为垂直布局, - 在创建 几个按钮, - 然后将按钮对象依次添加到垂直布局中。 - 最后,限定只有画布的背景色是红色,其他子部件的背景色默认,前提是限定前指定对象名称;[Qt-CSS样式表含义参考](https://www.cnblogs.com/liming19680104/p/13929838.html) ```python self.left_menu_top_frame.setObjectName("left_menu_top_frame") self.left_menu_top_frame.setStyleSheet("#left_menu_top_frame { background-color: red; }") ``` - 效果: ![img.png](doc/image/img14.png) ### 第5课:左菜单toggle按钮的点击事件:展开和折叠左侧菜单,学习使用动画 `animation`,做出动态效果 - 1、在`main.py` 中,定义按钮的响应事件 - 获取ui中左菜单的宽度 - 设计展开和缩回的目标宽度值变化逻辑:如果进入点击事件时,按钮是缩回状态,那么目标宽度就是展开时的宽度,反之就是缩回状态的宽度。 - 借助 `QPropertyAnimation` 功能,定义一个属性更改动画 - 开始值是当前菜单某个属性值(宽度) - 结束值是动画变化的目标值(目标宽度) - 设置持续时间(Duration) - 设置值缓慢变化的曲线类型(EasingCurve) - 开始动画 -效果 ![img.png](doc/image/img15.png) ### 第6课:自定义按钮控件,第一节: `PyPushButton`,继承自 `QPushButton`,修改按钮显示图标。 - 1、自定义 `PyPushButton` 类, 继承自 `QPushButton` - 设定控件大小,颜色响应等默认参数 - 存档在 `gui/widgets` 中 - 2、在 `ui_main_window.py` 文件中,修改按钮控件为 `PyPushButton` 的实例。 - 初始化实例时,通过 `text` 形参设置按钮的显示名称,`is_active` 设置当前控件为激活状态。 - 3、效果 ![img.png](doc/image/img16.png) ### 第7课:自定义按钮控件,第二节: 增加自定义的图标 - 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.png](doc/image/img17.png) ### 第8课:添加自定义按钮控件的单击事件,并创建一些简单的示例。 - 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.png](doc/image/img22.png) - 3、效果 ![img.png](doc/image/img18.png) ## 打包EXE笔记:使用 `pyinstaller` 打包 `python+pyside6` 程序 ### 1、实现步骤 - 单目录模式运行(-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`文件中,设置打包时一并将资源文件复制到指定目录下。 ```python datas=[('.\\*.ico','.\\'),('gui\\images\\icons','gui\\images\\icons')], # 增加程序图标以及控件图标文件 ``` - 效果 ![img.png](doc/image/img19.png) ### 2、使用图形界面打包 - 第一步下载安装pip包,-i可以指定下载源(如清华源): ```commandline pip install auto-py-to-exe -i https://pypi.tuna.tsinghua.edu.cn/simple ``` - 安装过程 ![img.png](doc/image/img20.png) - 单目录EXE输出,启动效果: ![img.png](doc/image/img21.png) ### 3、参考资料 - [单目录模式解释](https://www.cnblogs.com/cocoonink/p/13858062.html) - [spec 规格文件解释](https://www.cnblogs.com/cocoonink/p/13858083.html) - [知乎 打包教程链接](https://zhuanlan.zhihu.com/p/370914926) - [CSDN Pyinstaller 打包发布经验总结](https://blog.csdn.net/weixin_42052836/article/details/82315118) ## 其他阅读:不推荐使用 `单文件模式` 的原因(从参考资料中copy) >之所以强调这一点,并不是因为单文件模式存在什么无法解决的问题。如果你非常清楚该模式的运行机制,并且在写代码的时候小心避开这些坑的话,那么所有问题都是可以避免的。但实际上,可以说 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 命令行开关来强制使用特定的目录,但是鉴于最终用户有哪些目录不在程序员控制范围内,通常还是应该避免使用它。 我们可以自己写一个程序来验证: ```python 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 机制的一个重要原因。 >如果你的脚本需要引用辅助文件路径的话,那么一种可能的形式如下: ```python 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') ``` >上述代码并不是唯一可行的代码,或许也不是最简洁的,但是你应当明白了,要正确处理该过程并不是轻而易举的事情。很多用户之所以出错又找不到问题,就是因为他们根本不清楚临时目录这回事,也不知道上哪里去找这些文件。如果使用单目录模式的话,那么文件在哪里是可以直接看到的,出现问题的可能性就小多了,即使有问题也很容易排查。这就是我为什么强烈推荐用户不要使用单文件模式的原因————除了看起来比较清爽之外,单文件模式基本上没有其他好处,而且它带来的麻烦比这一点好处要多太多了。 >除此之外,单文件模式还带来了其他一些负面效应: >因为有临时目录和解压文件这个过程,所以单文件模式的程序启动速度会比较慢。对于稍大的程序,这个延迟是肉眼可以感觉到的; 如果你的程序运行到一半崩溃了,那么临时目录将没有机会被删除。日积月累的话,可能会在临时目录下遗留一大堆 _MEIxxxx 目录,占用大量磁盘空间。 或许对你来说上面这两个问题并不是特别重要,但知道它们的存在还是有好处的。 ## 继续练习:增加组件到`pages`中 ### 组件1: ## 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request ## 特技 1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md 2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) 3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) 6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)