文章目录
- 一、概述(Readme)
-
- 主要功能
- [1.1 环境要求](#1.1 环境要求)
- [1.2 如何使用](#1.2 如何使用)
- 二、详解
-
- [2.1 架构设计:解耦视图、逻辑与控制](#2.1 架构设计:解耦视图、逻辑与控制)
- [2.2 核心技术实现细节](#2.2 核心技术实现细节)
-
- [2.21 异步执行与 UI 响应:`QThread` 与信号/槽机制](#2.21 异步执行与 UI 响应:
QThread
与信号/槽机制) - [2.22 Conda 环境集成:`subprocess` 与正则表达式](#2.22 Conda 环境集成:
subprocess
与正则表达式) - [2.23 资源文件打包的通用解决方案](#2.23 资源文件打包的通用解决方案)
- [2.24 动态命令生成与输出路径管理](#2.24 动态命令生成与输出路径管理)
- [2.25 UI/UX 优化:主题切换与状态管理](#2.25 UI/UX 优化:主题切换与状态管理)
- [2.21 异步执行与 UI 响应:`QThread` 与信号/槽机制](#2.21 异步执行与 UI 响应:
- 三、改进与完善
一、概述(Readme)
🟢适用场景: 使用conda
虚拟环境
🟢项目地址 :EasyPack
🟢App 截图:
pyinstaller -F --paths=E:\anaconda3\envs\yt_dlp_env\Lib\site-packages --python=E:\anaconda3\envs\yt_dlp_env\python.exe --noconsole --icon=wx2.ico --name=Downloader test.py
这是一句常用的 pyinstaller打包的命令,但每次都手动输入这一长串命令,很是麻烦。
本项目最初是使用 Tkinter 实现的一个简单的打包程序。
现在,它已经演进为一个功能较为丰富、界面美观的 PyQt6 应用,旨在解决直接使用 pyinstaller
命令行时参数复杂、配置繁琐的问题。
此工具特别为使用 Conda 进行环境管理的 Python 开发者设计,能够自动检测并列出您的 Conda 虚拟环境,极大地简化了路径配置过程。您只需通过几次点击,即可完成对 Python 脚本的打包,无需记忆复杂的命令。
主要功能
- 美观的图形用户界面:采用 PyQt6 构建,提供清爽的左右分栏布局,配置区和日志区一目了然。
- 主题切换:内置亮色和暗色两套主题,可根据个人喜好一键切换。
- 深度 Conda 集成 :
- 自动扫描并列出所有本地 Conda 虚拟环境。
- 选择环境后,自动填充该环境的 Python 解释器路径和
site-packages
路径。
- 智能依赖检查 :在构建开始前,自动检查所选环境中是否安装了
pyinstaller
,如果未安装,会提示用户一键安装。 - 全面的打包选项 :
- 基本选项 :支持单文件 (
-F
) 和单目录 (-D
) 模式切换、自定义程序名称 (--name
)、添加图标 (--icon
)、隐藏控制台 (--noconsole
) 等常用功能。 - 高级选项 :支持通过图形界面添加附加数据文件/目录 (
--add-data
)、添加隐藏的模块导入 (--hidden-import
) 等。
- 基本选项 :支持单文件 (
- 实时日志输出:在独立的线程中执行打包命令,构建过程的日志会实时显示在右侧的日志窗口中,界面不会卡顿。
- 统一的输出管理 :所有 PyInstaller 生成的文件(
build
目录、dist
目录、.spec
文件)都会被统一整理到源脚本同级目录下的output
文件夹中,保持项目根目录的整洁。 - 便捷操作:打包成功后,"打开输出目录"按钮会被激活,可以一键直达生成的可执行文件所在的位置。
1.1 环境要求
在运行此程序之前,请确保您的系统满足以下条件:
- Python: 3.8 或更高版本。
- PyQt6 :
pip install PyQt6
- Conda : 已安装 Anaconda 或 Miniconda,并且
conda
命令已添加到系统的环境变量 (PATH) 中,可以在命令行中直接调用。
1.2 如何使用
直接下载已经打包好的exe文件即可,或者克隆本仓库。
打包步骤:
- 选择脚本:点击 "Python 脚本" 右侧的 "浏览..." 按钮,选择您想要打包的主 Python 文件。程序名称通常会自动根据文件名填充。
- 选择环境:在 "Conda 环境" 下拉菜单中,选择您的项目所使用的虚拟环境。选择后,相关的模块路径会自动填充。
- 自定义选项 :
- 根据需要修改程序名称或添加图标。
- 选择打包模式(单文件或单目录)。
- 在 "高级打包选项" 中添加额外的数据文件或需要强制包含的库。
- 预览命令:在右侧的 "最终命令预览" 窗口中检查即将执行的命令是否符合预期。
- 开始构建:点击右下角的 "开始构建" 按钮。
- 查看日志:在 "构建日志" 窗口中观察实时输出。
- 完成 :
- 如果构建成功,日志中会以绿色字体显示成功信息,并且 "打开目录" 按钮会变为可用状态。
- 如果构建失败,日志中会以红色字体显示失败信息,您可以根据日志排查问题。
二、详解
2.1 架构设计:解耦视图、逻辑与控制
应用分解为三个独立的 Python 模块。
-
ui_components.py
- 视图层 (View)- 技术栈: PyQt6
- 职责 : 完全负责 UI 的构建、布局和样式。它包含一个
PyInstallerGUI
类,该类继承自QMainWindow
,并定义了所有的窗口控件(QWidget
)、布局管理器(QLayout
)和样式表(QSS)。此模块不包含任何业务逻辑,只通过信号(Signal)对外暴露用户交互事件。
-
builder.py
- 逻辑/工作层 (Worker)- 技术栈 :
QObject
,QThread
,subprocess
- 职责 : 处理所有耗时和后台任务。核心是
BuildWorker
类,它继承自QObject
,其run
方法内封装了执行 PyInstaller 命令的逻辑。此类被设计为在独立的QThread
中运行,以防止 UI 主线程阻塞。它通过信号向控制层报告进度和结果。
- 技术栈 :
-
main.py
- 主控层 (Controller)- 技术栈 :
QApplication
- 职责 : 程序的入口和"胶水层"。它负责实例化
QApplication
和视图PyInstallerGUI
,创建并管理QThread
和BuildWorker
。其核心任务是连接视图层发出的用户操作信号和逻辑层执行任务的槽(Slot),并处理逻辑层返回的信号。
- 技术栈 :
2.2 核心技术实现细节
2.21 异步执行与 UI 响应:QThread
与信号/槽机制
GUI 应用的主线程负责渲染界面和响应用户输入。任何耗时操作(如文件 I/O、网络请求、子进程执行)如果在此线程中进行,都会导致界面冻结。PyInstaller 的打包过程可能持续数分钟,因此必须异步执行。
实现步骤:
-
创建
BuildWorker
:BuildWorker
类继承QObject
,使其能够利用 Qt 的元对象系统,特别是信号/槽机制。打包命令的执行逻辑被封装在run
方法中。python# builder.py class BuildWorker(QObject): progress_updated = pyqtSignal(str) # 定义信号,用于发送日志 finished = pyqtSignal(int) # 定义信号,用于报告完成状态 def __init__(self, command, python_executable): super().__init__() self.command = command # ... def run(self): # 使用 subprocess.Popen 启动 PyInstaller 进程 process = subprocess.Popen(...) # 逐行读取子进程的输出 for line in iter(process.stdout.readline, ''): self.progress_updated.emit(line) # 发射信号,将日志发送出去 process.wait() self.finished.emit(process.returncode) # 发射完成信号
-
线程管理与通信 : 在
main.py
中,当用户触发构建时:- 实例化
QThread
和BuildWorker
。 - 调用
worker.moveToThread(thread)
,此操作将worker
对象的所有事件处理(包括其槽函数的执行)都转移到了thread
所代表的线程中。 - 连接信号与槽。将
worker
的信号连接到view
中更新 UI 的方法上。 - 启动线程。调用
thread.start()
,这会触发thread
的started
信号,我们将其连接到worker.run
方法,从而在新线程中开始执行打包任务。
python# main.py def start_build(self): # ... self.build_thread = QThread() self.build_worker = BuildWorker(command, python_exe) self.build_worker.moveToThread(self.build_thread) # 连接 worker 的信号到 view 的槽 self.build_worker.progress_updated.connect(self.view.log_to_console) self.build_worker.finished.connect(self.on_build_finished) # 当线程启动时,执行 worker 的 run 方法 self.build_thread.started.connect(self.build_worker.run) # 当 worker 完成后,安全退出线程 self.build_worker.finished.connect(self.build_thread.quit) self.build_thread.finished.connect(self.build_thread.deleteLater) self.build_thread.start()
- 实例化
通过这种方式,打包任务在独立的线程中运行,并通过信号机制将状态安全地传递给主线程以更新 UI,保证了界面的流畅性。
2.22 Conda 环境集成:subprocess
与正则表达式
为了实现 Conda 环境的自动发现,程序需要执行 conda env list
命令并解析其输出。
实现细节:
- 使用
subprocess.run
函数以阻塞方式执行conda env list
。这是一个轻量级操作,可以安全地在主线程中执行(仅在程序启动时调用一次)。 - 设置
capture_output=True
和text=True
来捕获命令的文本输出。 - 使用
creationflags=subprocess.CREATE_NO_WINDOW
(仅 Windows) 来防止执行conda
命令时弹出黑色的控制台窗口。 - 通过正则表达式
re.compile(r"^(\S+)\s+\*?\s+(.+)$")
匹配输出中的每一行,提取环境名称和绝对路径,并存入字典中。
python
# builder.py -> get_conda_envs()
proc = subprocess.run(
["conda", "env", "list"],
capture_output=True, text=True, check=True, encoding='utf-8',
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
)
当用户在 UI 下拉框中选择一个环境后,程序会根据这个字典找到对应的环境路径,并自动拼接出该环境下 python.exe
和 Lib/site-packages
的路径,填充到相应的输入框中。
2.23 资源文件打包的通用解决方案
在 GUI 应用中,图标、配置文件等资源在开发时和打包后的路径是不同的。PyInstaller 打包后会将 --add-data
指定的文件解压到一个临时的 _MEIPASS
目录。必须编写一个与环境无关的路径解析函数。
实现:
python
# ui_components.py
def resource_path(relative_path):
""" 获取资源的绝对路径,兼容开发和打包环境 """
try:
# PyInstaller 打包后会创建一个临时目录,并将其路径存入 sys._MEIPASS
base_path = sys._MEIPASS
except AttributeError:
# 在开发环境中,使用常规的相对路径
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
在代码中所有需要加载本地资源的地方(如此工具自身的窗口图标),都通过 resource_path()
函数来获取路径。
python
# 加载窗口图标
app_icon_path = resource_path("app_icon.ico")
self.setWindowIcon(QIcon(app_icon_path))
同时,在打包此工具自身时,必须在 pyinstaller
命令中明确指出要包含此资源文件:
bash
pyinstaller --add-data "app_icon.ico;." main.py
--add-data
的格式为 源路径;目标目录
(Windows 用 ;
,Linux/macOS 用 :
),.
表示打包后的根目录。
2.24 动态命令生成与输出路径管理
为了提供灵活的打包选项并保持项目目录整洁,PyInstaller 命令是在用户交互时动态构建的。
实现 :
在 ui_components.py
中,get_pyinstaller_command
方法负责从各个 UI 控件(如 QLineEdit
, QCheckBox
)中获取当前值,并逐步拼接成一个完整的命令行字符串。
一个关键的改进是输出路径的统一管理 。通过 PyInstaller 的 --distpath
, --workpath
, 和 --specpath
参数,可以将所有生成物重定向到一个统一的 output
目录下。
python
# ui_components.py -> get_pyinstaller_command()
# 基于主脚本路径,定义所有输出路径
script = self.script_path_edit.text()
base_output_dir = os.path.join(os.path.dirname(script), "output")
dist_path = os.path.join(base_output_dir, "dist")
build_path = os.path.join(base_output_dir, "build")
spec_path = base_output_dir
# 将路径参数添加到命令列表中
command.extend([
f'--distpath="{dist_path}"',
f'--workpath="{build_path}"',
f'--specpath="{spec_path}"'
])
这不仅使用户的项目目录保持干净,也使得"打开目录"功能可以精确定位到 output/dist
文件夹。
2.25 UI/UX 优化:主题切换与状态管理
为了提升用户体验,实现了一些 UI 层的优化。
-
主题切换 : 定义了两套 QSS (Qt Style Sheets) 字符串:
LIGHT_THEME
和DARK_THEME
。通过连接"黑暗模式"复选框的toggled
信号到一个槽函数apply_theme
,可以调用self.setStyleSheet()
方法,实现整个应用主题的一键切换。 -
状态管理 : 在构建开始时,必须禁用所有配置相关的控件,以防止用户在打包过程中修改参数。这通过一个
set_build_state(is_building)
方法实现,该方法会遍历所有输入控件并设置其setEnabled(not is_building)
状态。同时,"开始构建"和"取消构建"按钮的可用性也会相应地切换。
三、改进与完善
- 自定义打包命令:pyinstaller还有其它选项,可以加一个自定义命令部分或者添加更多GUI选项
- 附加数据的bug:先添加附加资源(如果需要)后设置其它的选项是ok的,否则会崩溃(
已修正
) - 加一个软件教程页面,说明各种选项具体作用和注意点等
- 语言切换
有空了写吧。