├── PythonEXE_Maker ├── icon.png ├── iogo.png ├── __pycache__ │ ├── dialogs.cpython-311.pyc │ ├── widgets.cpython-311.pyc │ └── converters.cpython-311.pyc ├── widgets.py ├── dialogs.py ├── converters.py └── main.py └── README.md /PythonEXE_Maker/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeahhe365/PythonEXE_Maker/HEAD/PythonEXE_Maker/icon.png -------------------------------------------------------------------------------- /PythonEXE_Maker/iogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeahhe365/PythonEXE_Maker/HEAD/PythonEXE_Maker/iogo.png -------------------------------------------------------------------------------- /PythonEXE_Maker/__pycache__/dialogs.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeahhe365/PythonEXE_Maker/HEAD/PythonEXE_Maker/__pycache__/dialogs.cpython-311.pyc -------------------------------------------------------------------------------- /PythonEXE_Maker/__pycache__/widgets.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeahhe365/PythonEXE_Maker/HEAD/PythonEXE_Maker/__pycache__/widgets.cpython-311.pyc -------------------------------------------------------------------------------- /PythonEXE_Maker/__pycache__/converters.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeahhe365/PythonEXE_Maker/HEAD/PythonEXE_Maker/__pycache__/converters.cpython-311.pyc -------------------------------------------------------------------------------- /PythonEXE_Maker/widgets.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QLabel, QMessageBox 2 | from PyQt5.QtCore import Qt, pyqtSignal 3 | 4 | 5 | class DropArea(QLabel): 6 | """拖放区域,用于拖入 .py 文件""" 7 | file_dropped = pyqtSignal(str) 8 | 9 | def __init__(self, parent=None): 10 | super().__init__(parent) 11 | self.setAcceptDrops(True) 12 | self.setText("拖入 .py 文件") 13 | self.setAlignment(Qt.AlignCenter) 14 | self.setStyleSheet(""" 15 | QLabel { 16 | border: 2px dashed #aaa; 17 | min-height: 80px; 18 | font-size: 14px; 19 | color: #555; 20 | padding: 10px; 21 | } 22 | QLabel:hover { 23 | border-color: #777; 24 | } 25 | """) 26 | 27 | def dragEnterEvent(self, event): 28 | """检测拖入文件是否为.py,符合则允许释放""" 29 | if event.mimeData().hasUrls() and any(url.toLocalFile().endswith('.py') for url in event.mimeData().urls()): 30 | event.acceptProposedAction() 31 | else: 32 | event.ignore() 33 | 34 | def dropEvent(self, event): 35 | """释放拖拽文件后,若是.py文件则发送信号""" 36 | paths = [ 37 | url.toLocalFile() for url in event.mimeData().urls() 38 | if url.toLocalFile().endswith(".py") 39 | ] 40 | if paths: 41 | for path in paths: 42 | self.file_dropped.emit(path) 43 | else: 44 | QMessageBox.warning(self, "警告", "请拖放 Python 文件 (.py) 到窗口中。") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PythonEXE Maker 2 | 3 |  4 | 5 | 6 | **PythonEXE Maker** 是一个开源且免费的工具,旨在将 Python 脚本转换为独立的可执行文件(EXE)。通过友好的图形用户界面,用户可以轻松配置转换参数,管理多个转换任务,并自定义生成的 EXE 文件的各种属性,如图标、版本信息等。 7 | 8 | 9 | ## 特性 10 | 11 | - **拖拽支持**:直接将 `.py` 文件拖入程序窗口,快速添加转换任务。 12 | - **批量转换**:一次性转换多个 Python 脚本为 EXE 文件。 13 | - **自定义设置**: 14 | - 选择转换模式(GUI 模式或命令行模式)。 15 | - 指定输出目录。 16 | - 设置 EXE 文件名称。 17 | - 添加自定义图标(支持 `.png` 和 `.ico` 格式,`.png` 会自动转换为 `.ico`)。 18 | - 配置文件版本信息和版权信息。 19 | - 指定额外的隐藏导入模块和附加 PyInstaller 参数。 20 | - **任务管理**:实时查看每个转换任务的进度和状态。 21 | - **日志查看**:详细的转换日志,方便排查问题。 22 | - **可定制的“关于”对话框**:内嵌项目 Logo,展示项目信息。 23 | - **依赖检查**:程序启动时自动检查并提示安装必要的依赖库。 24 | 25 | ## 截图 26 | 27 | ### 主界面 28 | 29 |  30 | 31 | ### 转换任务管理 32 | 33 |  34 | 35 | ### 日志查看 36 | 37 |  38 | 39 | ### 关于对话框 40 | 41 |  42 | 43 | *请将上述截图添加到项目的 `screenshots` 文件夹中,并根据需要调整图片路径。* 44 | 45 | ## 安装 46 | 47 | ### 前提条件 48 | 49 | - **操作系统**:Windows 50 | - **Python 版本**:Python 3.6 及以上 51 | - **依赖库**: 52 | - [PyQt5](https://pypi.org/project/PyQt5/) 53 | - [Pillow](https://pypi.org/project/Pillow/) 54 | - [PyInstaller](https://pypi.org/project/PyInstaller/) 55 | 56 | ### 安装步骤 57 | 58 | 1. **克隆仓库** 59 | 60 | ```bash 61 | git clone https://github.com/yeahhe365/PythonEXE_Maker.git 62 | cd PythonEXE_Maker 63 | ``` 64 | 65 | 2. **创建虚拟环境(可选)** 66 | 67 | ```bash 68 | python -m venv venv 69 | source venv/bin/activate # 对于 Windows 用户使用 venv\Scripts\activate 70 | ``` 71 | 72 | 3. **安装依赖** 73 | 74 | ```bash 75 | pip install -r requirements.txt 76 | ``` 77 | 78 | *如果没有 `requirements.txt` 文件,请手动安装依赖:* 79 | 80 | ```bash 81 | pip install PyQt5 Pillow PyInstaller 82 | ``` 83 | 84 | ## 使用说明 85 | 86 | 1. **运行程序** 87 | 88 | ```bash 89 | python PythonEXE_Maker_1.1.py 90 | ``` 91 | 92 | 2. **配置转换参数** 93 | 94 | - **转换模式**:选择生成的 EXE 是带控制台(命令行模式)还是不带控制台(GUI 模式)。 95 | - **输出目录**:指定生成的 EXE 文件的存放位置,默认为源文件所在目录。 96 | - **EXE 信息**: 97 | - **EXE 名称**:设置生成的 EXE 文件名称,默认为源文件同名。 98 | - **图标文件**:选择一个图标文件用于 EXE,支持 `.png` 和 `.ico` 格式。 99 | - **文件版本**:设置 EXE 文件的版本号(格式:X.X.X.X)。 100 | - **版权信息**:设置 EXE 文件的版权信息。 101 | - **高级设置**: 102 | - **额外模块**:输入需要隐藏导入的模块名称,多个模块以逗号分隔。 103 | - **附加参数**:输入 PyInstaller 的其他命令行参数。 104 | 105 | 3. **添加转换任务** 106 | 107 | - **拖拽文件**:将 `.py` 文件直接拖入程序窗口的拖放区域。 108 | - **浏览文件**:点击“浏览文件”按钮,选择要转换的 Python 脚本。 109 | 110 | 4. **开始转换** 111 | 112 | - 点击“开始转换”按钮,程序将开始转换选中的 Python 脚本。 113 | - 转换过程中可以在“任务管理”选项卡中查看每个任务的进度和状态。 114 | - 转换日志可以在“日志”选项卡中查看详细信息。 115 | 116 | 5. **取消转换** 117 | 118 | - 在转换过程中,可以点击“取消转换”按钮,停止所有正在进行的转换任务。 119 | 120 | ## 贡献 121 | 122 | 欢迎任何形式的贡献!您可以通过以下方式参与: 123 | 124 | - **提交 Issue**:在 [GitHub Issues](https://github.com/yeahhe365/PythonEXE_Maker/issues) 中提交您的问题或建议。 125 | - **提交 Pull Request**:Fork 本仓库,进行修改后提交 Pull Request,我们会尽快审核。 126 | - **捐赠支持**:如果您觉得这个项目对您有帮助,可以通过 [请开发者喝咖啡](https://b23.tv/Sni5cax) 来支持我。 127 | 128 | ## 许可证 129 | 130 | 本项目采用 [MIT 许可证](LICENSE) 进行许可。您可以自由地使用、修改和分发本项目的代码,但需要保留原作者的版权声明和许可说明。 131 | 132 | ## 联系我们 133 | 134 | - **作者**:yeahhe365 135 | - **GitHub**:[https://github.com/yeahhe365/PythonEXE_Maker](https://github.com/yeahhe365/PythonEXE_Maker) 136 | - **官方网站**:[https://www.yeahhe.online/](https://www.yeahhe.online/) 137 | - **论坛主页**:[LINUXDO 论坛](https://www.linuxdo.com/users/yeahhe) 138 | - **支持链接**:[请开发者喝咖啡](https://b23.tv/Sni5cax) 139 | -------------------------------------------------------------------------------- /PythonEXE_Maker/dialogs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import webbrowser 3 | from PyQt5.QtWidgets import ( 4 | QDialog, QVBoxLayout, QTextBrowser, QTextEdit 5 | ) 6 | from PyQt5.QtGui import QFont, QIcon 7 | from PyQt5.QtCore import QSize 8 | 9 | 10 | class ManualDialog(QDialog): 11 | """使用说明对话框""" 12 | def __init__(self, parent=None): 13 | super().__init__(parent) 14 | self.setWindowTitle("使用说明") 15 | self.setFixedSize(800, 600) 16 | 17 | # 对话框样式示例(也可移到 main 的全局样式中统一管理) 18 | self.setStyleSheet(""" 19 | QDialog { 20 | background-color: #FFFFFF; 21 | } 22 | QTextBrowser { 23 | border: 1px solid #CCC; 24 | border-radius: 4px; 25 | padding: 8px; 26 | font-size: 14px; 27 | } 28 | """) 29 | 30 | layout = QVBoxLayout() 31 | self.text_browser = QTextBrowser() 32 | self.text_browser.setFont(QFont("Arial", 14)) 33 | self.text_browser.setHtml(self.manual_text()) 34 | layout.addWidget(self.text_browser) 35 | self.setLayout(layout) 36 | 37 | @staticmethod 38 | def manual_text(): 39 | return """ 40 |
本程序用于将 Python 脚本转换为可执行文件 (EXE)。以下是使用步骤:
42 |注意事项:
50 |更多信息:
57 |版本:1.1.0
106 |作者:yeahhe365
107 |这是一个开源免费工具,用于将 Python 脚本转换为可执行文件。
108 |如有问题或建议,欢迎在 GitHub 提交 issue 或查看源代码:
109 |https://github.com/yeahhe365/PythonEXE_Maker
110 |感谢您的使用与支持!
111 | """ 112 | 113 | 114 | class LogViewerDialog(QDialog): 115 | """查看日志文件的对话框""" 116 | def __init__(self, parent=None, log_path="app.log"): 117 | super().__init__(parent) 118 | self.setWindowTitle("查看日志文件") 119 | self.setFixedSize(800, 600) 120 | 121 | self.setStyleSheet(""" 122 | QDialog { 123 | background-color: #FFFFFF; 124 | } 125 | QTextEdit { 126 | border: 1px solid #CCC; 127 | border-radius: 4px; 128 | padding: 8px; 129 | font-size: 12px; 130 | } 131 | """) 132 | 133 | layout = QVBoxLayout() 134 | self.text_edit = QTextEdit() 135 | self.text_edit.setReadOnly(True) 136 | self.text_edit.setFont(QFont("Courier New", 10)) 137 | layout.addWidget(self.text_edit) 138 | self.setLayout(layout) 139 | self.load_log(log_path) 140 | 141 | def load_log(self, log_path): 142 | """加载并显示日志文件内容""" 143 | if os.path.exists(log_path): 144 | try: 145 | with open(log_path, 'r', encoding='utf-8') as f: 146 | self.text_edit.setPlainText(f.read()) 147 | except Exception as e: 148 | self.text_edit.setPlainText(f"无法读取日志文件: {e}") 149 | else: 150 | self.text_edit.setPlainText("日志文件不存在。") -------------------------------------------------------------------------------- /PythonEXE_Maker/converters.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import logging 5 | 6 | from PyQt5.QtCore import QRunnable, pyqtSignal, QObject 7 | 8 | # 若要转换图标,需要尝试导入 Pillow 9 | try: 10 | from PIL import Image 11 | except ImportError: 12 | Image = None 13 | 14 | 15 | class WorkerSignals(QObject): 16 | """定义 Worker 线程的信号""" 17 | status_updated = pyqtSignal(str) # 用于传递状态信息字符串 18 | progress_updated = pyqtSignal(int) # 用于更新进度条 19 | conversion_finished = pyqtSignal(str, int) # (exe_path, exe_size) 20 | conversion_failed = pyqtSignal(str) # 传递错误信息 21 | 22 | 23 | class ConvertRunnable(QRunnable): 24 | """执行转换任务的 Runnable 类(配合 QThreadPool 使用)""" 25 | 26 | def __init__(self, script_path, convert_mode, output_dir, exe_name, icon_path, 27 | file_version, copyright_info, extra_library, additional_options): 28 | super().__init__() 29 | self.script_path = script_path 30 | self.convert_mode = convert_mode 31 | self.output_dir = output_dir 32 | self.exe_name = exe_name 33 | self.icon_path = icon_path 34 | self.file_version = file_version 35 | self.copyright_info = copyright_info 36 | self.extra_library = extra_library 37 | self.additional_options = additional_options 38 | 39 | self.signals = WorkerSignals() 40 | self._is_running = True 41 | 42 | def run(self): 43 | """线程池执行入口""" 44 | version_file_path = None 45 | try: 46 | script_dir = os.path.dirname(self.script_path) 47 | exe_name = self.exe_name or os.path.splitext(os.path.basename(self.script_path))[0] 48 | output_dir = self.output_dir or script_dir 49 | 50 | if not self.ensure_pyinstaller(): 51 | return 52 | 53 | # 准备 PyInstaller 命令参数 54 | options = self.prepare_pyinstaller_options(exe_name, output_dir) 55 | 56 | # 处理图标(如是PNG则自动转ICO) 57 | if self.icon_path: 58 | icon_file = self.handle_icon(script_dir) 59 | if icon_file: 60 | options.append(f'--icon={icon_file}') 61 | 62 | # 生成版本信息文件 63 | if self.file_version or self.copyright_info: 64 | version_file_path = self.create_version_file(exe_name, script_dir) 65 | if version_file_path: 66 | options.append(f'--version-file={version_file_path}') 67 | 68 | self.update_status("开始转换...") 69 | success = self.run_pyinstaller(options) 70 | 71 | if success: 72 | # 检查生成的exe文件 73 | exe_path = os.path.join(output_dir, exe_name + '.exe') 74 | if os.path.exists(exe_path): 75 | exe_size = os.path.getsize(exe_path) // 1024 76 | self.signals.conversion_finished.emit(exe_path, exe_size) 77 | self.update_status(f"转换成功! EXE 文件位于: {exe_path} (大小: {exe_size} KB)") 78 | else: 79 | error_message = "转换完成,但未找到生成的 EXE 文件。" 80 | self.update_status(error_message) 81 | self.signals.conversion_failed.emit(error_message) 82 | else: 83 | error_message = "转换失败,请查看上面的错误信息。" 84 | self.update_status(error_message) 85 | self.signals.conversion_failed.emit(error_message) 86 | 87 | except Exception as e: 88 | error_message = f"转换过程中出现异常: {e}" 89 | self.update_status(error_message) 90 | self.signals.conversion_failed.emit(error_message) 91 | 92 | finally: 93 | # 任务结束 94 | self._is_running = False 95 | self.cleanup_files(version_file_path) 96 | 97 | def stop(self): 98 | """停止转换任务""" 99 | self._is_running = False 100 | 101 | def update_status(self, message: str): 102 | """更新转换状态(日志 + UI)""" 103 | logging.info(message) 104 | self.signals.status_updated.emit(message) 105 | 106 | def ensure_pyinstaller(self) -> bool: 107 | """确保本机已安装 PyInstaller,如未安装则尝试安装""" 108 | try: 109 | subprocess.run([sys.executable, '-m', 'PyInstaller', '--version'], 110 | check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 111 | self.update_status("已检测到 PyInstaller。") 112 | return True 113 | except subprocess.CalledProcessError: 114 | self.update_status("未检测到 PyInstaller,正在尝试安装...") 115 | try: 116 | subprocess.check_call([sys.executable, "-m", "pip", "install", "pyinstaller"]) 117 | self.update_status("PyInstaller 安装成功。") 118 | return True 119 | except subprocess.CalledProcessError as e: 120 | self.update_status(f"安装 PyInstaller 失败: {e}") 121 | return False 122 | 123 | def prepare_pyinstaller_options(self, exe_name: str, output_dir: str) -> list: 124 | """准备 PyInstaller 命令行参数""" 125 | options = ['--onefile', '--clean'] 126 | options.append('--console' if self.convert_mode == "命令行模式" else '--windowed') 127 | 128 | if self.extra_library: 129 | hidden_imports = [lib.strip() for lib in self.extra_library.split(',') if lib.strip()] 130 | options += [f'--hidden-import={lib}' for lib in hidden_imports] 131 | 132 | if self.additional_options: 133 | options += self.additional_options.strip().split() 134 | 135 | options += ['--distpath', output_dir, '-n', exe_name] 136 | return options 137 | 138 | def handle_icon(self, script_dir: str) -> str: 139 | """处理图标:.png -> .ico 转换""" 140 | if not Image: 141 | self.update_status("Pillow 库未安装,无法转换 PNG 图标。请安装 Pillow 或使用 ICO 图标。") 142 | return "" 143 | 144 | lower_icon = self.icon_path.lower() 145 | if lower_icon.endswith('.png'): 146 | self.update_status("检测到 PNG 图标,正在转换为 ICO 格式...") 147 | try: 148 | img = Image.open(self.icon_path) 149 | ico_path = os.path.join(script_dir, 'icon_converted.ico') 150 | img.save(ico_path, format='ICO', 151 | sizes=[(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)]) 152 | self.update_status("图标转换成功。") 153 | return ico_path 154 | except Exception as e: 155 | self.update_status(f"PNG 转 ICO 失败: {e}") 156 | return "" 157 | elif lower_icon.endswith('.ico'): 158 | return self.icon_path 159 | else: 160 | self.update_status("不支持的图标格式,仅支持 .png 和 .ico 格式。") 161 | return "" 162 | 163 | def create_version_file(self, exe_name: str, script_dir: str) -> str: 164 | """生成版本信息文件""" 165 | try: 166 | from PyInstaller.utils.win32.versioninfo import ( 167 | VSVersionInfo, FixedFileInfo, StringFileInfo, StringTable, StringStruct, 168 | VarFileInfo, VarStruct 169 | ) 170 | except ImportError as e: 171 | self.update_status(f"导入PyInstaller版本信息类失败: {e}") 172 | return "" 173 | 174 | version_numbers = self.file_version.split('.') if self.file_version else ['1', '0', '0', '0'] 175 | if len(version_numbers) != 4 or not all(num.isdigit() for num in version_numbers): 176 | version_numbers = ['1', '0', '0', '0'] 177 | 178 | # 构造版本信息 179 | version_info = VSVersionInfo( 180 | ffi=FixedFileInfo( 181 | filevers=tuple(map(int, version_numbers)), 182 | prodvers=tuple(map(int, version_numbers)), 183 | mask=0x3f, 184 | flags=0x0, 185 | OS=0x40004, 186 | fileType=0x1, 187 | subtype=0x0, 188 | date=(0, 0) 189 | ), 190 | kids=[ 191 | StringFileInfo( 192 | [ 193 | StringTable( 194 | '040904E4', 195 | [ 196 | StringStruct('CompanyName', ''), 197 | StringStruct('FileDescription', exe_name), 198 | StringStruct('FileVersion', '.'.join(version_numbers)), 199 | StringStruct('InternalName', f'{exe_name}.exe'), 200 | StringStruct('LegalCopyright', self.copyright_info), 201 | StringStruct('OriginalFilename', f'{exe_name}.exe'), 202 | StringStruct('ProductName', exe_name), 203 | StringStruct('ProductVersion', '.'.join(version_numbers)) 204 | ] 205 | ) 206 | ] 207 | ), 208 | VarFileInfo([VarStruct('Translation', [0x0409, 0x04B0])]) 209 | ] 210 | ) 211 | 212 | version_file_path = os.path.join(script_dir, 'version_info.txt') 213 | try: 214 | with open(version_file_path, 'w', encoding='utf-8') as vf: 215 | vf.write(version_info.__str__()) 216 | self.update_status("生成版本信息文件。") 217 | return version_file_path 218 | except Exception as e: 219 | self.update_status(f"版本信息文件生成失败: {e}") 220 | return "" 221 | 222 | def run_pyinstaller(self, options: list) -> bool: 223 | """调用 PyInstaller 执行转换""" 224 | cmd = [sys.executable, '-m', 'PyInstaller'] + options + [self.script_path] 225 | self.update_status(f"执行命令: {' '.join(cmd)}") 226 | try: 227 | process = subprocess.Popen( 228 | cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True 229 | ) 230 | 231 | for line in process.stdout: 232 | if not self._is_running: 233 | process.terminate() 234 | self.update_status("转换已被用户取消。") 235 | return False 236 | line = line.strip() 237 | self.update_status(line) 238 | # 简易进度估计 239 | if "Analyzing" in line: 240 | self.signals.progress_updated.emit(30) 241 | elif "Collecting" in line: 242 | self.signals.progress_updated.emit(50) 243 | elif "Building" in line: 244 | self.signals.progress_updated.emit(70) 245 | elif "completed successfully" in line.lower(): 246 | self.signals.progress_updated.emit(100) 247 | 248 | process.stdout.close() 249 | process.wait() 250 | 251 | return process.returncode == 0 252 | except Exception as e: 253 | self.update_status(f"转换过程中出现异常: {e}") 254 | return False 255 | 256 | def cleanup_files(self, version_file_path: str): 257 | """清理临时文件(版本信息、转换后的ico等)""" 258 | script_dir = os.path.dirname(self.script_path) 259 | 260 | # 删除版本信息文件 261 | if version_file_path and os.path.exists(version_file_path): 262 | try: 263 | os.remove(version_file_path) 264 | self.update_status("删除版本信息文件。") 265 | except Exception as e: 266 | self.update_status(f"无法删除版本信息文件: {e}") 267 | 268 | # 若原图标是 png,则删除临时生成的 ico 269 | if self.icon_path and self.icon_path.lower().endswith('.png'): 270 | ico_path = os.path.join(script_dir, 'icon_converted.ico') 271 | if os.path.exists(ico_path): 272 | try: 273 | os.remove(ico_path) 274 | self.update_status("删除临时 ICO 文件。") 275 | except Exception as e: 276 | self.update_status(f"无法删除临时 ICO 文件: {e}") -------------------------------------------------------------------------------- /PythonEXE_Maker/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import logging 5 | import webbrowser 6 | 7 | from PyQt5.QtWidgets import ( 8 | QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, 9 | QLabel, QPushButton, QFileDialog, QMessageBox, QTextEdit, QLineEdit, 10 | QDialog, QProgressBar, QGroupBox, QMenuBar, QAction, QStatusBar, QListWidget, 11 | QListWidgetItem, QSplitter, QScrollArea, QFrame, QTabWidget, QComboBox 12 | ) 13 | from PyQt5.QtGui import QFont, QIcon, QColor 14 | from PyQt5.QtCore import Qt, QThreadPool, QSize 15 | 16 | # 引入我们在其它模块里定义的类和函数 (假设本地已有) 17 | from converters import ConvertRunnable 18 | from dialogs import ManualDialog, AboutDialog, LogViewerDialog 19 | from widgets import DropArea 20 | 21 | # ======= 日志配置 ======= 22 | logging.basicConfig( 23 | level=logging.INFO, 24 | format='%(asctime)s [%(levelname)s] %(message)s', 25 | handlers=[ 26 | logging.FileHandler("app.log", mode='w', encoding='utf-8'), 27 | logging.StreamHandler(sys.stdout) 28 | ] 29 | ) 30 | 31 | 32 | class MainWindow(QMainWindow): 33 | """主窗口:包含主要的UI和逻辑""" 34 | def __init__(self): 35 | super().__init__() 36 | self.setWindowTitle("PythonEXE Maker") 37 | self.setGeometry(100, 100, 1300, 900) 38 | self.setFont(QFont("Arial", 11)) 39 | 40 | # 存放脚本路径 41 | self.script_paths = [] 42 | # 线程池 43 | self.thread_pool = QThreadPool() 44 | # 转换任务列表 45 | self.tasks = [] 46 | # 每个脚本对应的任务UI元素 47 | self.task_widgets = {} 48 | 49 | # 在此属性中存储“附加文件”的路径 50 | self.extra_file_path = None 51 | 52 | # 初始化UI 53 | self.init_ui() 54 | # 应用全局样式表(若需要美化UI,可在这里调用 self.apply_global_stylesheet()) 55 | # self.apply_global_stylesheet() 56 | 57 | # 检查并更新“开始转换”按钮的可用状态 58 | self.update_start_button_state() 59 | 60 | def init_ui(self): 61 | # 创建中央部件 62 | central_widget = QWidget() 63 | main_layout = QVBoxLayout(central_widget) 64 | 65 | # ============ 菜单栏 ============ 66 | self.init_menu() 67 | 68 | # ============ 左右分割(QSplitter) ============ 69 | splitter = QSplitter(Qt.Horizontal) 70 | 71 | # -------- 左侧设置区域 -------- 72 | left_widget = QWidget() 73 | left_layout = QVBoxLayout(left_widget) 74 | left_layout.addWidget(self.init_settings_group()) 75 | left_layout.addLayout(self.init_button_group()) 76 | splitter.addWidget(left_widget) 77 | 78 | # -------- 右侧Tab标签页(任务管理、日志) -------- 79 | self.tab_widget = QTabWidget() 80 | self.tab_widget.setTabPosition(QTabWidget.North) 81 | 82 | # 1) “任务管理”选项卡 83 | task_tab = QWidget() 84 | task_tab_layout = QVBoxLayout(task_tab) 85 | 86 | # ------ 脚本管理区 ------ 87 | script_group = QGroupBox("脚本管理") 88 | script_layout = QVBoxLayout() 89 | 90 | # 拖拽区与“浏览文件”按钮 91 | drop_browse_layout = QHBoxLayout() 92 | self.drop_area = DropArea(self) # 自定义拖拽控件(在 widgets.py 中) 93 | self.drop_area.file_dropped.connect(self.add_script_path) 94 | drop_browse_layout.addWidget(self.drop_area) 95 | 96 | browse_button = QPushButton("浏览文件") 97 | browse_button.setToolTip("点击选择要转换的 Python 文件,可多选。") 98 | # 如果有 Material Icon,可在此设置 browse_button.setIcon(...) 99 | browse_button.setFixedHeight(60) 100 | browse_button.clicked.connect(self.browse_files) 101 | drop_browse_layout.addWidget(browse_button) 102 | 103 | script_layout.addLayout(drop_browse_layout) 104 | 105 | # 脚本列表 106 | self.script_list = QListWidget() 107 | self.script_list.setToolTip("已选择的 Python 脚本列表,双击可移除。") 108 | self.script_list.itemDoubleClicked.connect(self.remove_script) 109 | script_layout.addWidget(self.script_list) 110 | 111 | script_group.setLayout(script_layout) 112 | task_tab_layout.addWidget(script_group) 113 | 114 | # ------ 任务进度区域 ------ 115 | task_progress_group = QGroupBox("转换任务进度") 116 | task_progress_layout = QVBoxLayout(task_progress_group) 117 | 118 | self.task_area = QScrollArea() 119 | self.task_area.setWidgetResizable(True) 120 | self.task_container = QWidget() 121 | self.task_layout = QVBoxLayout(self.task_container) 122 | self.task_layout.setAlignment(Qt.AlignTop) 123 | self.task_area.setWidget(self.task_container) 124 | task_progress_layout.addWidget(self.task_area) 125 | 126 | task_progress_group.setLayout(task_progress_layout) 127 | task_tab_layout.addWidget(task_progress_group) 128 | 129 | self.tab_widget.addTab(task_tab, "任务管理") 130 | 131 | # 2) “日志”选项卡 132 | log_tab = QWidget() 133 | log_tab_layout = QVBoxLayout(log_tab) 134 | 135 | self.status_text_edit = QTextEdit() 136 | self.status_text_edit.setReadOnly(True) 137 | self.status_text_edit.setFont(QFont("Courier New", 10)) 138 | log_tab_layout.addWidget(self.status_text_edit) 139 | 140 | self.progress_bar = QProgressBar() 141 | self.progress_bar.setRange(0, 100) 142 | self.progress_bar.hide() 143 | log_tab_layout.addWidget(self.progress_bar) 144 | 145 | self.status_bar = QStatusBar() 146 | log_tab_layout.addWidget(self.status_bar) 147 | 148 | self.tab_widget.addTab(log_tab, "日志") 149 | 150 | splitter.addWidget(self.tab_widget) 151 | splitter.setSizes([500, 800]) 152 | 153 | main_layout.addWidget(splitter) 154 | self.setCentralWidget(central_widget) 155 | 156 | # 设置窗口图标(如有需要) 157 | script_dir = os.path.dirname(os.path.abspath(__file__)) 158 | icon_path = os.path.join(script_dir, 'icon.png') 159 | if os.path.exists(icon_path): 160 | self.setWindowIcon(QIcon(icon_path)) 161 | else: 162 | logging.warning(f"图标文件未找到: {icon_path}") 163 | 164 | def init_menu(self): 165 | """初始化菜单栏""" 166 | menubar = self.menuBar() 167 | 168 | # 文件菜单 169 | file_menu = menubar.addMenu('文件') 170 | exit_action = QAction('退出', self) 171 | exit_action.triggered.connect(self.close) 172 | file_menu.addAction(exit_action) 173 | 174 | # 帮助菜单 175 | help_menu = menubar.addMenu('帮助') 176 | manual_action = QAction('使用说明', self) 177 | manual_action.triggered.connect(self.show_manual) 178 | help_menu.addAction(manual_action) 179 | 180 | about_action = QAction('关于', self) 181 | about_action.triggered.connect(self.show_about) 182 | help_menu.addAction(about_action) 183 | 184 | github_action = QAction('开源地址', self) 185 | github_action.triggered.connect(lambda: webbrowser.open("https://github.com/yeahhe365/PythonEXE_Maker")) 186 | help_menu.addAction(github_action) 187 | 188 | website_action = QAction('官方网站', self) 189 | website_action.triggered.connect(lambda: webbrowser.open("https://www.yeahhe.online/")) 190 | help_menu.addAction(website_action) 191 | 192 | forum_action = QAction('LINUXDO 论坛主页', self) 193 | forum_action.triggered.connect(lambda: webbrowser.open("https://www.linuxdo.com/users/yeahhe")) 194 | help_menu.addAction(forum_action) 195 | 196 | support_action = QAction('请开发者喝咖啡', self) 197 | support_action.triggered.connect(self.open_bilibili_link) 198 | help_menu.addAction(support_action) 199 | 200 | # 日志菜单 201 | log_menu = menubar.addMenu('日志') 202 | view_log_action = QAction('查看日志文件', self) 203 | view_log_action.triggered.connect(self.view_log_file) 204 | log_menu.addAction(view_log_action) 205 | 206 | def init_settings_group(self) -> QGroupBox: 207 | """初始化基本设置、EXE信息和高级设置的组""" 208 | settings_group = QGroupBox("基本设置") 209 | settings_layout = QGridLayout() 210 | 211 | # 转换模式 212 | mode_label = QLabel("转换模式:") 213 | self.mode_combo = QComboBox() 214 | self.mode_combo.addItems(["GUI 模式", "命令行模式"]) 215 | self.mode_combo.setToolTip("选择生成的 EXE 是带控制台(命令行模式)还是不带控制台(GUI 模式)。") 216 | settings_layout.addWidget(mode_label, 0, 0) 217 | settings_layout.addWidget(self.mode_combo, 0, 1) 218 | 219 | # 输出目录 220 | output_label = QLabel("输出目录:") 221 | self.output_edit = QLineEdit() 222 | self.output_edit.setPlaceholderText("默认与源文件同目录") 223 | self.output_edit.setToolTip("设置生成 EXE 文件的输出目录。为空则默认放在源文件同目录下。") 224 | 225 | output_button = QPushButton("浏览") 226 | output_button.setToolTip("选择输出目录。") 227 | output_button.clicked.connect(self.browse_output_dir) 228 | 229 | output_h_layout = QHBoxLayout() 230 | output_h_layout.addWidget(self.output_edit) 231 | output_h_layout.addWidget(output_button) 232 | 233 | settings_layout.addWidget(output_label, 1, 0) 234 | settings_layout.addLayout(output_h_layout, 1, 1) 235 | 236 | # EXE 信息 237 | exe_info_group = QGroupBox("EXE 信息") 238 | exe_info_layout = QGridLayout() 239 | 240 | name_label = QLabel("EXE 名称:") 241 | self.name_edit = QLineEdit() 242 | self.name_edit.setPlaceholderText("默认与源文件同名") 243 | self.name_edit.setToolTip("设置生成的 EXE 文件名称。") 244 | exe_info_layout.addWidget(name_label, 0, 0) 245 | exe_info_layout.addWidget(self.name_edit, 0, 1) 246 | 247 | icon_label = QLabel("图标文件:") 248 | self.icon_edit = QLineEdit() 249 | self.icon_edit.setPlaceholderText("可选,支持 .png 和 .ico") 250 | self.icon_edit.setToolTip("选择一个图标文件用于 EXE。若为 PNG,将自动转换为 ICO。") 251 | icon_button = QPushButton("浏览") 252 | icon_button.setToolTip("选择图标文件。") 253 | icon_button.clicked.connect(self.browse_icon_file) 254 | 255 | icon_h_layout = QHBoxLayout() 256 | icon_h_layout.addWidget(self.icon_edit) 257 | icon_h_layout.addWidget(icon_button) 258 | exe_info_layout.addWidget(icon_label, 1, 0) 259 | exe_info_layout.addLayout(icon_h_layout, 1, 1) 260 | 261 | version_label = QLabel("文件版本:") 262 | self.version_edit = QLineEdit() 263 | self.version_edit.setPlaceholderText("1.0.0.0") 264 | self.version_edit.setToolTip("设置 EXE 文件的版本号(X.X.X.X)。") 265 | exe_info_layout.addWidget(version_label, 2, 0) 266 | exe_info_layout.addWidget(self.version_edit, 2, 1) 267 | 268 | copyright_label = QLabel("版权信息:") 269 | self.copyright_edit = QLineEdit() 270 | self.copyright_edit.setToolTip("设置 EXE 文件的版权信息。") 271 | exe_info_layout.addWidget(copyright_label, 3, 0) 272 | exe_info_layout.addWidget(self.copyright_edit, 3, 1) 273 | 274 | exe_info_group.setLayout(exe_info_layout) 275 | settings_layout.addWidget(exe_info_group, 2, 0, 1, 2) 276 | 277 | # 高级设置 278 | advanced_settings_group = QGroupBox("高级设置") 279 | advanced_settings_layout = QGridLayout() 280 | 281 | # 额外模块 282 | library_label = QLabel("额外模块:") 283 | self.library_edit = QLineEdit() 284 | self.library_edit.setPlaceholderText("隐藏导入的模块,逗号分隔") 285 | self.library_edit.setToolTip("输入需要隐藏导入的模块名称(多个用逗号分隔)。") 286 | advanced_settings_layout.addWidget(library_label, 0, 0) 287 | advanced_settings_layout.addWidget(self.library_edit, 0, 1) 288 | 289 | # 用按钮来选择需要打包的“附加文件” 290 | extra_file_label = QLabel("附加文件:") 291 | self.select_file_button = QPushButton("选择文件") 292 | self.select_file_button.setToolTip("点击选择一个要与脚本一起打包的文件。将自动生成 --add-data 参数。") 293 | self.select_file_button.clicked.connect(self.choose_extra_file) 294 | 295 | advanced_settings_layout.addWidget(extra_file_label, 1, 0) 296 | advanced_settings_layout.addWidget(self.select_file_button, 1, 1) 297 | 298 | advanced_settings_group.setLayout(advanced_settings_layout) 299 | settings_layout.addWidget(advanced_settings_group, 3, 0, 1, 2) 300 | 301 | settings_group.setLayout(settings_layout) 302 | return settings_group 303 | 304 | def init_button_group(self) -> QHBoxLayout: 305 | """初始化【开始转换】【取消转换】按钮""" 306 | button_layout = QHBoxLayout() 307 | 308 | self.start_button = QPushButton("开始转换") 309 | self.start_button.setEnabled(False) 310 | self.start_button.setToolTip("开始将所选 Python 脚本转换为 EXE。") 311 | self.start_button.clicked.connect(self.start_conversion) 312 | 313 | self.cancel_button = QPushButton("取消转换") 314 | self.cancel_button.setEnabled(False) 315 | self.cancel_button.setToolTip("取消正在进行的转换任务。") 316 | self.cancel_button.clicked.connect(self.cancel_conversion) 317 | 318 | button_layout.addWidget(self.start_button) 319 | button_layout.addWidget(self.cancel_button) 320 | return button_layout 321 | 322 | def add_script_path(self, path: str): 323 | """添加脚本路径到列表中""" 324 | if path not in self.script_paths: 325 | self.script_paths.append(path) 326 | self.script_list.addItem(QListWidgetItem(path)) 327 | self.append_status(f"已添加脚本: {path}") 328 | self.update_start_button_state() 329 | 330 | def browse_files(self): 331 | """浏览并添加 Python 脚本文件""" 332 | script_paths, _ = QFileDialog.getOpenFileNames(self, "选择 Python 文件", "", "Python Files (*.py)") 333 | if script_paths: 334 | added = False 335 | for script_path in script_paths: 336 | if script_path not in self.script_paths: 337 | self.script_paths.append(script_path) 338 | self.script_list.addItem(QListWidgetItem(script_path)) 339 | self.append_status(f"已添加脚本: {script_path}") 340 | added = True 341 | if added: 342 | self.update_start_button_state() 343 | 344 | def remove_script(self, item: QListWidgetItem): 345 | """移除选中的脚本路径""" 346 | path = item.text() 347 | if path in self.script_paths: 348 | self.script_paths.remove(path) 349 | self.script_list.takeItem(self.script_list.row(item)) 350 | self.append_status(f"已移除脚本: {path}") 351 | self.update_start_button_state() 352 | 353 | def update_start_button_state(self): 354 | """根据是否有脚本,更新“开始转换”按钮状态""" 355 | self.start_button.setEnabled(bool(self.script_paths)) 356 | 357 | def browse_output_dir(self): 358 | """浏览并设置输出目录""" 359 | output_dir = QFileDialog.getExistingDirectory(self, "选择输出目录") 360 | if output_dir: 361 | self.output_edit.setText(output_dir) 362 | 363 | def browse_icon_file(self): 364 | """浏览并设置图标文件""" 365 | icon_path, _ = QFileDialog.getOpenFileName(self, "选择图标文件", "", "Image Files (*.ico *.png)") 366 | if icon_path: 367 | self.icon_edit.setText(icon_path) 368 | 369 | def choose_extra_file(self): 370 | """选择附加文件并保存路径""" 371 | file_path, _ = QFileDialog.getOpenFileName(self, "选择附加文件", "", "所有文件 (*.*)") 372 | if file_path: 373 | self.extra_file_path = file_path 374 | self.append_status(f"已选择附加文件: {file_path}") 375 | 376 | def start_conversion(self): 377 | """开始转换所有选中的脚本""" 378 | if not self.script_paths: 379 | QMessageBox.warning(self, "警告", "请先选择至少一个 Python 脚本。") 380 | return 381 | 382 | convert_mode = self.mode_combo.currentText() 383 | output_dir = self.output_edit.text().strip() or None 384 | exe_name = self.name_edit.text().strip() or None 385 | icon_path = self.icon_edit.text().strip() or None 386 | file_version = self.version_edit.text().strip() or None 387 | copyright_info = self.copyright_edit.text().strip() 388 | extra_library = self.library_edit.text().strip() or None 389 | 390 | # 如果文件版本号不为空,但格式不正确,则提示 391 | if file_version and not self.validate_version(file_version): 392 | QMessageBox.warning(self, "警告", "文件版本号格式不正确,应为 X.X.X.X (如 1.0.0.0)。") 393 | return 394 | 395 | # ------------------- 396 | # 关键修复:去掉额外引号,并使用 --add-data=SRC;DEST (Windows) / SRC:DEST (其他) 397 | # ------------------- 398 | additional_options = None 399 | if self.extra_file_path: 400 | separator = ';' if os.name == 'nt' else ':' 401 | # 不要外层的引号,避免 PyInstaller 解析出错 402 | additional_options = f'--add-data={self.extra_file_path}{separator}.' 403 | 404 | # 禁用相关UI 405 | self.toggle_ui_elements(False) 406 | # 清空日志 407 | self.status_text_edit.clear() 408 | self.append_status("开始转换...") 409 | self.progress_bar.show() 410 | self.status_bar.showMessage("转换中...") 411 | 412 | self.tasks = [] 413 | self.task_widgets = {} 414 | 415 | # 清空任务进度区域 416 | for i in reversed(range(self.task_layout.count())): 417 | w = self.task_layout.itemAt(i).widget() 418 | if w: 419 | w.setParent(None) 420 | 421 | # 为每个脚本创建转换任务 422 | for script_path in self.script_paths: 423 | task_widget = self.create_task_widget(script_path) 424 | self.task_layout.addWidget(task_widget['widget']) 425 | self.task_widgets[script_path] = task_widget 426 | 427 | runnable = ConvertRunnable( 428 | script_path=script_path, 429 | convert_mode=convert_mode, 430 | output_dir=output_dir, 431 | exe_name=exe_name, 432 | icon_path=icon_path, 433 | file_version=file_version, 434 | copyright_info=copyright_info, 435 | extra_library=extra_library, 436 | additional_options=additional_options 437 | ) 438 | # 信号连接:把脚本路径一起传过去以区分不同任务 439 | runnable.signals.status_updated.connect( 440 | lambda msg, sp=script_path: self.update_status(msg, sp) 441 | ) 442 | runnable.signals.progress_updated.connect( 443 | lambda val, sp=script_path: self.update_progress(val, sp) 444 | ) 445 | runnable.signals.conversion_finished.connect( 446 | lambda exe, size, sp=script_path: self.conversion_finished(exe, size, sp) 447 | ) 448 | runnable.signals.conversion_failed.connect( 449 | lambda err, sp=script_path: self.conversion_failed(err, sp) 450 | ) 451 | 452 | self.thread_pool.start(runnable) 453 | self.tasks.append(runnable) 454 | 455 | self.cancel_button.setEnabled(True) 456 | 457 | def cancel_conversion(self): 458 | """取消所有正在进行的转换任务""" 459 | if hasattr(self, 'tasks') and self.tasks: 460 | for task in self.tasks: 461 | task.stop() 462 | self.append_status("已请求取消转换任务。") 463 | self.status_bar.showMessage("取消转换...") 464 | self.cancel_button.setEnabled(False) 465 | # 因为主动取消,这里直接调用 conversion_complete 来恢复UI 466 | self.conversion_complete() 467 | 468 | def conversion_finished(self, exe_path: str, exe_size: int, script_path: str): 469 | """处理单个脚本转换完成的情况""" 470 | self.append_status(f"转换成功! EXE 文件位于: {exe_path} (大小: {exe_size} KB)") 471 | task_widget = self.task_widgets.get(script_path) 472 | if task_widget: 473 | task_widget['status'].setText(f"转换成功! 文件: {exe_path} ({exe_size} KB)") 474 | task_widget['progress'].setValue(100) 475 | # 若所有任务都结束,则执行收尾 476 | if all(not getattr(task, '_is_running', False) for task in self.tasks): 477 | self.conversion_complete() 478 | 479 | def conversion_failed(self, error_message: str, script_path: str): 480 | """处理单个脚本转换失败的情况""" 481 | self.append_status(f"{error_message}") 482 | task_widget = self.task_widgets.get(script_path) 483 | if task_widget: 484 | task_widget['status'].setText(f"{error_message}") 485 | task_widget['progress'].setValue(0) 486 | # 若所有任务都结束,则执行收尾 487 | if all(not getattr(task, '_is_running', False) for task in self.tasks): 488 | self.conversion_complete() 489 | 490 | def conversion_complete(self): 491 | """所有转换任务完成或取消后的处理""" 492 | self.toggle_ui_elements(True) 493 | self.progress_bar.hide() 494 | self.status_bar.showMessage("转换完成。") 495 | self.tasks = [] 496 | 497 | def validate_version(self, version: str) -> bool: 498 | """验证版本号格式 (X.X.X.X)""" 499 | parts = version.split('.') 500 | return len(parts) == 4 and all(part.isdigit() for part in parts) 501 | 502 | def toggle_ui_elements(self, enabled: bool): 503 | """启用或禁用与任务相关的 UI""" 504 | self.start_button.setEnabled(enabled and bool(self.script_paths)) 505 | self.mode_combo.setEnabled(enabled) 506 | self.output_edit.setEnabled(enabled) 507 | self.name_edit.setEnabled(enabled) 508 | self.icon_edit.setEnabled(enabled) 509 | self.version_edit.setEnabled(enabled) 510 | self.library_edit.setEnabled(enabled) 511 | self.drop_area.setEnabled(enabled) 512 | self.script_list.setEnabled(enabled) 513 | self.select_file_button.setEnabled(enabled) 514 | if enabled: 515 | self.cancel_button.setEnabled(False) 516 | 517 | def append_status(self, text: str): 518 | """在日志文本框中追加状态信息,并更新状态栏""" 519 | logging.info(text) 520 | if "" in text: 521 | self.status_text_edit.setTextColor(QColor('red')) 522 | else: 523 | self.status_text_edit.setTextColor(QColor('black')) 524 | self.status_text_edit.append(text) 525 | self.status_bar.showMessage(text) 526 | 527 | def update_status(self, status: str, script_path: str): 528 | """更新特定脚本的状态""" 529 | self.append_status(f"[{os.path.basename(script_path)}] {status}") 530 | task_widget = self.task_widgets.get(script_path) 531 | if task_widget: 532 | task_widget['log'].append(status) 533 | 534 | def update_progress(self, value: int, script_path: str): 535 | """更新特定脚本的进度条""" 536 | task_widget = self.task_widgets.get(script_path) 537 | if task_widget: 538 | task_widget['progress'].setValue(value) 539 | 540 | def show_manual(self): 541 | """显示“使用说明”对话框""" 542 | manual_dialog = ManualDialog(self) 543 | manual_dialog.exec_() 544 | 545 | def show_about(self): 546 | """显示“关于”对话框""" 547 | about_dialog = AboutDialog(self) 548 | about_dialog.exec_() 549 | 550 | def open_bilibili_link(self): 551 | """打开支持开发者的链接""" 552 | webbrowser.open("https://b23.tv/Sni5cax") 553 | 554 | def view_log_file(self): 555 | """查看日志文件""" 556 | log_path = os.path.abspath("app.log") 557 | if os.path.exists(log_path): 558 | log_viewer = LogViewerDialog(self, log_path) 559 | log_viewer.exec_() 560 | else: 561 | QMessageBox.warning(self, "警告", "日志文件不存在。") 562 | 563 | def create_task_widget(self, script_path: str) -> dict: 564 | """ 565 | 创建一个转换任务在UI上的显示小部件: 566 | - 脚本名 567 | - 进度条 568 | - 状态文字 569 | - 简要日志 570 | """ 571 | widget = QFrame() 572 | widget.setFrameShape(QFrame.StyledPanel) 573 | layout = QHBoxLayout(widget) 574 | 575 | script_label = QLabel(os.path.basename(script_path)) 576 | script_label.setFixedWidth(200) 577 | layout.addWidget(script_label) 578 | 579 | progress = QProgressBar() 580 | progress.setRange(0, 100) 581 | progress.setValue(0) 582 | progress.setFixedWidth(200) 583 | layout.addWidget(progress) 584 | 585 | status = QLabel("等待中...") 586 | status.setWordWrap(True) 587 | layout.addWidget(status) 588 | 589 | log = QTextEdit() 590 | log.setReadOnly(True) 591 | log.setFont(QFont("Courier New", 10)) 592 | log.setFixedHeight(50) 593 | log.setToolTip("本任务的转换日志片段") 594 | layout.addWidget(log) 595 | 596 | return { 597 | 'widget': widget, 598 | 'script_label': script_label, 599 | 'progress': progress, 600 | 'status': status, 601 | 'log': log 602 | } 603 | 604 | def closeEvent(self, event): 605 | """关闭窗口前,尝试停止所有任务""" 606 | if hasattr(self, 'tasks') and self.tasks: 607 | for task in self.tasks: 608 | task.stop() 609 | self.tasks = [] 610 | event.accept() 611 | 612 | 613 | if __name__ == "__main__": 614 | app = QApplication(sys.argv) 615 | 616 | script_dir = os.path.dirname(os.path.abspath(__file__)) 617 | icon_path = os.path.join(script_dir, 'icon.png') 618 | if os.path.exists(icon_path): 619 | app.setWindowIcon(QIcon(icon_path)) 620 | 621 | window = MainWindow() 622 | window.show() 623 | sys.exit(app.exec_()) --------------------------------------------------------------------------------