
你是否还在为Python代码打包成exe文件而头疼? 是否对着黑框框里密密麻麻的命令行参数手足无措? 是否想给你的Python脚本配上精致图标、隐藏控制台、一键生成单文件程序,却被复杂的打包指令劝退? 是否尝试过手动检查项目依赖、安装第三方库,结果折腾半天依然报错不断?
作为一名Python开发者,我们日常写脚本、做工具、开发小项目,最终的目标都是让代码能独立运行、分享给他人使用。而PyInstaller作为最主流的Python打包工具,功能强大但操作门槛不低——纯命令行交互、参数繁多、依赖管理繁琐、界面简陋不直观,哪怕是有一定基础的开发者,也经常在打包过程中遇到各种问题,更别说零基础的小白了。
为了解决这个痛点,我基于PyQt5开发了这款高颜值、全功能、零门槛的Python一键打包可视化工具!它彻底抛弃了晦涩的命令行,采用现代化UI设计,界面精致高级、不透明超好看,布局清晰合理,所有功能可视化操作,哪怕你不懂任何打包命令,只要点点鼠标,就能轻松完成Python脚本到exe可执行文件的转换。工具集成了自动依赖检测安装、图标配置、输出目录自定义、打包模式切换等核心功能,兼顾了美观性与实用性,无论是日常开发打包、毕业设计交付,还是小工具分享,都能完美适配。
今天,我就把这款工具的完整代码、设计思路、功能细节毫无保留地分享给大家,不仅能直接拿来用,还能学习PyQt5界面美化、多线程编程、子进程调用等核心知识点,一文搞定Python打包+PyQt5实战!
这款工具整体采用面向对象编程,基于PyQt5构建GUI界面,通过多线程避免界面卡死,结合子进程调用PyInstaller实现打包功能,代码结构清晰,分为三大核心模块:
在PyQt5中,子线程不能直接操作UI控件,否则会导致程序崩溃。因此我专门定义了LogSignal信号类,通过自定义信号实现子线程(打包/安装依赖)和主线程(UI界面)的安全通信。
log_signal:负责传递日志文本,实时将打包过程、依赖安装结果输出到界面日志框;progress_signal:负责传递进度值和状态文本,动态更新进度条,让用户清晰掌握执行进度;这是工具的核心视觉部分,我采用左右分栏布局,左侧配置基础参数,右侧设置打包选项+查看日志,所有控件都经过精细化美化:
这是工具的“心脏”,实现了所有业务逻辑,纯Python实现,无冗余依赖:
import sysimport osimport subprocessimport threadingfrom PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QRadioButton, QButtonGroup, QCheckBox, QComboBox, QGroupBox, QTextEdit, QMessageBox, QProgressBar)from PyQt5.QtCore import Qt, pyqtSignal, QObjectfrom PyQt5.QtGui import QFont# 日志信号类:子线程与UI通信核心classLogSignal(QObject): log_signal = pyqtSignal(str) progress_signal = pyqtSignal(int, str)# 主窗口类classPyInstallerGUI(QMainWindow):def__init__(self): super().__init__() self.setWindowTitle("Python 一键打包工具 ✨") self.setMinimumSize(1050, 720) self.setStyleSheet(self.get_global_style()) self.log_signal = LogSignal() self.log_signal.log_signal.connect(self.append_log) self.log_signal.progress_signal.connect(self.update_progress)# 全局变量 self.py_file_path = "" self.output_dir = "" self.icon_path = "" self.python_path = sys.executable self.packaging_thread = None self.init_ui()# 全局QSS样式:高颜值美化核心defget_global_style(self):return""" QMainWindow { background-color: #f7f8fa; } QGroupBox { font-size: 14px; font-weight: bold; color: #2c3e50; border: 1px solid #dcdfe6; border-radius: 10px; margin-top: 10px; padding-top: 10px; background-color: white; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 8px; color: #34495e; } QPushButton { background-color: #3498db; color: white; border: none; border-radius: 6px; padding: 7px 14px; font-size: 13px; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #1f618d; } QPushButton#startBtn { background-color: #2ecc71; font-size: 14px; padding: 10px 20px; } QPushButton#startBtn:hover { background-color: #27ae60; } QLineEdit { border: 1px solid #dcdfe6; border-radius: 6px; padding: 6px 10px; font-size: 13px; background-color: white; selection-background-color: #3498db; } QLineEdit:read-only { background-color: #f8f9fa; color: #555; } QTextEdit { border: 1px solid #dcdfe6; border-radius: 8px; padding: 8px; font-size: 13px; background-color: white; } QProgressBar { border: none; border-radius: 8px; height: 14px; background-color: #ecf0f1; text-align: center; font-size: 12px; } QProgressBar::chunk { border-radius: 8px; background-color: #3498db; } QRadioButton, QCheckBox { font-size: 13px; spacing: 6px; } QComboBox { border: 1px solid #dcdfe6; border-radius: 6px; padding: 5px 10px; font-size: 13px; background-color: white; } QLabel { font-size: 13px; color: #2c3e50; } """# UI初始化:布局+控件创建definit_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QHBoxLayout(central_widget) main_layout.setSpacing(20) main_layout.setContentsMargins(25, 25, 25, 25)# 左侧面板 left_layout = QVBoxLayout() left_layout.setSpacing(18)# 入口文件 entry_group = QGroupBox("📄 入口文件") entry_layout = QHBoxLayout(entry_group) self.entry_edit = QLineEdit() self.entry_edit.setReadOnly(True) self.entry_btn = QPushButton("选择文件") self.entry_btn.clicked.connect(self.select_py_file) entry_layout.addWidget(self.entry_edit) entry_layout.addWidget(self.entry_btn) left_layout.addWidget(entry_group)# 编译环境 env_group = QGroupBox("⚙️ 编译环境") env_layout = QVBoxLayout(env_group) env_layout.setSpacing(10) env_radio_layout = QHBoxLayout() self.auto_env_radio = QRadioButton("自动检测") self.auto_env_radio.setChecked(True) self.manual_env_radio = QRadioButton("手动指定") self.env_group_btn = QButtonGroup() self.env_group_btn.addButton(self.auto_env_radio) self.env_group_btn.addButton(self.manual_env_radio) env_radio_layout.addWidget(self.auto_env_radio) env_radio_layout.addWidget(self.manual_env_radio) env_layout.addLayout(env_radio_layout) self.python_path_label = QLabel("Python 路径:") self.python_path_edit = QLineEdit() self.python_path_edit.setReadOnly(True) self.python_path_edit.setText(self.python_path) version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" self.python_version_label = QLabel(f"版本: {version}") env_layout.addWidget(self.python_path_label) env_layout.addWidget(self.python_path_edit) env_layout.addWidget(self.python_version_label) left_layout.addWidget(env_group)# 资源与输出 res_group = QGroupBox("🗂️ 资源与输出") res_layout = QVBoxLayout(res_group) res_layout.setSpacing(10) res_layout.addWidget(QLabel("输出位置:")) output_layout = QHBoxLayout() self.output_edit = QLineEdit() self.output_edit.setReadOnly(True) self.output_btn = QPushButton("选择目录") self.output_btn.clicked.connect(self.select_output_dir) output_layout.addWidget(self.output_edit) output_layout.addWidget(self.output_btn) res_layout.addLayout(output_layout) res_layout.addWidget(QLabel("应用图标:")) icon_layout = QHBoxLayout() self.icon_edit = QLineEdit() self.icon_edit.setPlaceholderText("可选") self.icon_edit.setReadOnly(True) self.icon_make_btn = QPushButton("制作") self.icon_select_btn = QPushButton("选择") self.icon_make_btn.clicked.connect(self.make_icon) self.icon_select_btn.clicked.connect(self.select_icon) icon_layout.addWidget(self.icon_edit) icon_layout.addWidget(self.icon_make_btn) icon_layout.addWidget(self.icon_select_btn) res_layout.addLayout(icon_layout) left_layout.addWidget(res_group) left_layout.addStretch()# 右侧面板 right_layout = QVBoxLayout() right_layout.setSpacing(18) build_group = QGroupBox("🚀 构建选项") build_layout = QVBoxLayout(build_group) build_layout.setSpacing(12) dep_layout = QHBoxLayout() self.auto_dep_check = QCheckBox("自动检测并安装缺失依赖") self.auto_dep_check.setChecked(True) self.manual_dep_btn = QPushButton("手动检查依赖") self.manual_dep_btn.clicked.connect(self.check_dependencies) dep_layout.addWidget(self.auto_dep_check) dep_layout.addWidget(self.manual_dep_btn) build_layout.addLayout(dep_layout) packer_layout = QHBoxLayout() self.nuitka_radio = QRadioButton("Nuitka 编译器") self.pyinstaller_radio = QRadioButton("PyInstaller 打包器") self.pyinstaller_radio.setChecked(True) self.packer_group_btn = QButtonGroup() self.packer_group_btn.addButton(self.nuitka_radio) self.packer_group_btn.addButton(self.pyinstaller_radio) packer_layout.addWidget(self.nuitka_radio) packer_layout.addWidget(self.pyinstaller_radio) build_layout.addLayout(packer_layout) option_layout = QHBoxLayout() self.hide_console_check = QCheckBox("隐藏控制台") self.hide_console_check.setChecked(True) self.onefile_check = QCheckBox("生成单文件") option_layout.addWidget(self.hide_console_check) option_layout.addWidget(self.onefile_check) build_layout.addLayout(option_layout) compress_layout = QHBoxLayout() compress_layout.addWidget(QLabel("压缩方式:")) self.compress_combo = QComboBox() self.compress_combo.addItems(["无压缩", "单层压缩", "双层压缩"]) self.compress_combo.setCurrentText("双层压缩") compress_layout.addWidget(self.compress_combo) build_layout.addLayout(compress_layout) backend_layout = QHBoxLayout() backend_layout.addWidget(QLabel("编译后端:")) self.backend_combo = QComboBox() self.backend_combo.addItems(["自动选择", "MSVC", "MinGW"]) backend_layout.addWidget(self.backend_combo) build_layout.addLayout(backend_layout) parallel_layout = QHBoxLayout() parallel_layout.addWidget(QLabel("并行任务:")) self.parallel_combo = QComboBox() self.parallel_combo.addItems(["自动", "1", "2", "4", "8"]) parallel_layout.addWidget(self.parallel_combo) build_layout.addLayout(parallel_layout) tip_label = QLabel("✨ GUI框架自动检测 (无需配置)") tip_label.setAlignment(Qt.AlignCenter) build_layout.addWidget(tip_label) right_layout.addWidget(build_group) self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) right_layout.addWidget(self.progress_bar) log_group = QGroupBox("📜 打包日志") log_layout = QVBoxLayout(log_group) self.log_text = QTextEdit() self.log_text.setReadOnly(True) log_layout.addWidget(self.log_text) right_layout.addWidget(log_group) btn_layout = QHBoxLayout() self.start_btn = QPushButton("开始打包") self.start_btn.setObjectName("startBtn") self.start_btn.clicked.connect(self.start_packaging) self.clear_log_btn = QPushButton("清空日志") self.clear_log_btn.clicked.connect(self.clear_log) btn_layout.addWidget(self.start_btn) btn_layout.addWidget(self.clear_log_btn) right_layout.addLayout(btn_layout) main_layout.addLayout(left_layout, 1) main_layout.addLayout(right_layout, 1)# 文件选择功能defselect_py_file(self): path, _ = QFileDialog.getOpenFileName(self, "选择Python文件", "", "*.py")if path: self.py_file_path = path self.entry_edit.setText(path) out = os.path.join(os.path.dirname(path), "dist_output") os.makedirs(out, exist_ok=True) self.output_dir = out self.output_edit.setText(out)defselect_output_dir(self): path = QFileDialog.getExistingDirectory(self)if path: self.output_dir = path self.output_edit.setText(path)defselect_icon(self): path, _ = QFileDialog.getOpenFileName(self, "选择图标", "", "*.ico;*.png;*.jpg")if path: self.icon_path = path self.icon_edit.setText(path)defmake_icon(self): QMessageBox.information(self, "提示", "选择图片后会自动转为ICO格式")# 依赖检测与安装defcheck_dependencies(self):ifnot self.py_file_path: self.log_signal.log_signal.emit("❌ 请先选择入口文件")return self.log_signal.log_signal.emit("🔍 正在检查依赖...")try:with open(self.py_file_path, "r", encoding="utf-8") as f: lines = f.readlines()except: self.log_signal.log_signal.emit("❌ 读取文件失败")return imports = set()for line in lines: line = line.strip()if line.startswith("import "): mod = line.split()[1].split(".")[0] imports.add(mod)elif line.startswith("from "): mod = line.split()[1].split(".")[0] imports.add(mod) pkg_map = {"PIL": "Pillow","cv2": "opencv-python","fitz": "PyMuPDF","np": "numpy","pd": "pandas" } missing = []for imp in imports: pkg = pkg_map.get(imp, imp)try: __import__(imp)except ImportError: missing.append(pkg)if missing: self.log_signal.log_signal.emit(f"⚠️ 缺失依赖:{', '.join(missing)}")if self.auto_dep_check.isChecked(): self.install_deps(missing)else: self.log_signal.log_signal.emit("✅ 所有依赖已安装")definstall_deps(self, packages): total = len(packages)for i, pkg in enumerate(packages, 1): self.log_signal.progress_signal.emit(int(i / total * 100), f"安装 {pkg}") cmd = [ self.python_path, "-m", "pip", "install", pkg,"-i", "https://pypi.tuna.tsinghua.edu.cn/simple","--trusted-host", "pypi.tuna.tsinghua.edu.cn" ] res = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")if res.returncode == 0: self.log_signal.log_signal.emit(f"✅ 安装成功:{pkg}")else: self.log_signal.log_signal.emit(f"❌ 安装失败:{pkg}") self.log_signal.progress_signal.emit(100, "依赖安装完成")# 日志与进度更新defappend_log(self, msg): self.log_text.append(msg) self.log_text.verticalScrollBar().setValue(self.log_text.verticalScrollBar().maximum())defupdate_progress(self, v, t): self.progress_bar.setValue(v) self.progress_bar.setFormat(f"{v}% • {t}")defclear_log(self): self.log_text.clear() self.progress_bar.setValue(0)# 打包核心功能defstart_packaging(self):ifnot self.py_file_path ornot self.output_dir: QMessageBox.warning(self, "提示", "请先选择文件和输出目录")return self.start_btn.setEnabled(False) self.packaging_thread = threading.Thread(target=self.run_package) self.packaging_thread.start()defrun_package(self):try:if self.auto_dep_check.isChecked(): self.check_dependencies() cmd = [self.python_path, "-m", "PyInstaller"]if self.onefile_check.isChecked(): cmd.append("--onefile")if self.hide_console_check.isChecked(): cmd.append("--windowed")if self.icon_path: cmd.extend(["--icon", self.icon_path]) cmd.extend(["--distpath", self.output_dir]) cmd.append(self.py_file_path) self.log_signal.log_signal.emit(f"🚀 打包命令:{' '.join(cmd)}") self.log_signal.progress_signal.emit(50, "打包中...") p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8")for line in p.stdout: self.log_signal.log_signal.emit(line.strip()) p.wait()if p.returncode == 0: self.log_signal.log_signal.emit("✅ 打包成功!") QMessageBox.information(self, "完成", "打包成功!")else: self.log_signal.log_signal.emit("❌ 打包失败")except Exception as e: self.log_signal.log_signal.emit(f"❌ 异常:{str(e)}")finally: self.start_btn.setEnabled(True) self.log_signal.progress_signal.emit(100, "完成")if __name__ == "__main__": QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) app = QApplication(sys.argv) w = PyInstallerGUI() w.show() sys.exit(app.exec_())PyQt5信号与槽机制自定义信号是PyQt5跨线程通信的核心,子线程无法直接修改UI,必须通过信号传递数据,这是GUI开发的必备知识点。
QSS样式表美化类似CSS的语法,能快速实现控件的圆角、配色、交互效果,让简陋的默认控件变身现代化UI,是提升界面颜值的关键。
多线程编程(threading)打包、安装依赖属于耗时操作,放在主线程会导致界面卡死,使用子线程执行任务,保证界面流畅。
子进程调用(subprocess)通过Python调用系统命令(pip安装依赖、PyInstaller打包),实现Python与系统命令的联动。
布局管理采用QHBoxLayout/QVBoxLayout实现响应式布局,控件自动适配窗口大小,无需手动定位,开发效率更高。
pip install pyqt5 pyinstaller;这款PyQt5打包工具,不仅解决了Python打包的痛点,更是一份绝佳的PyQt5实战教程。从界面美化到功能实现,从多线程到信号通信,涵盖了GUI开发的核心技能。
如果你是Python初学者,这份代码能帮你快速上手PyQt5;如果你是开发者,这款工具能直接提升你的开发效率。赶紧收藏运行,感受可视化打包的便捷吧!