当前位置:首页>python>Python源码 | 用Python写出自己的python代码打包成exe工具!

Python源码 | 用Python写出自己的python代码打包成exe工具!

  • 2026-09-09 01:26:54
Python源码 | 用Python写出自己的python代码打包成exe工具!

Python代码打包成exe工具

大家好,我是灵感!

好长时间没更新了,我相信大部分人都需要写完python代码把它分享给自己的亲朋好友,但是你单独发代码他们可能运行不了,所以你需要把代码打包成exe文件,他们双击就能用你做的程序了,所以做了个可视化打包工具,以后也有可能继续更新版本,我把代码给你们自己改一下用也行。

今天就用代码实现一下,如果你们有能力,自己优化一下,也可以按自己的要求进行修改或者美化啥的!

今天我把程序的代码分享给大家,有兴趣的可以自己试试,能力有限可能不太美观,或者有一些小问题,也有可能对你们来说一点都看不上,如果真要是这样,那我希望你们自己搞一个,或者代码给你们了自己修改成自己喜欢的样子,然后再使用!

内容完全原创,所以希望你们用或者修改的时候给我留点关于我公众号的一些信息,谢谢大家!代码页面是这样的:

下面我把程序给你们简单的介绍一下:

也没什么好介绍的,就是个用Python写的打包工具,而且支持两种打包方式,最重要的是免费,下面看图片就知道了

其它地方你们自己测试,反正源码已经给你们了,按装号需要的Python和它的库就可以正常运行,你们只需要具备环境,然后就是复制,粘贴后运行代码就能看到上面介绍图片里的那个程序和页面!

完整版代码:

#!/usr/bin/env python3# -*- coding: utf-8 -*-"""灵感江Python打包exe工具 v1.0 - 精简美化版支持:PyInstaller 和 Nuitka 双引擎打包功能:UPX压缩、文件夹打包、多格式图标支持、居中进度弹窗、自动依赖扫描"""import osimport sysimport subprocessimport shutilimport threadingimport queueimport reimport tempfilefrom pathlib import Pathfrom typing import Listfrom dataclasses import dataclass, field, asdict# 设置控制台编码if sys.platform == 'win32':    os.environ['PYTHONIOENCODING'] = 'utf-8'try:    import tkinter as tk    from tkinter import ttk, filedialog, messagebox    GUI_AVAILABLE = Trueexcept ImportError:    GUI_AVAILABLE = False    print("错误: tkinter未安装")    sys.exit(1)# 尝试导入PIL用于图标转换try:    from PIL import Image    PIL_AVAILABLE = Trueexcept ImportError:    PIL_AVAILABLE = False# ============== 主题配置 ==============class Theme:    LIGHT = {        'name': 'light', 'bg_primary': '#f8fafc', 'bg_secondary': '#ffffff',        'bg_tertiary': '#f1f5f9', 'bg_card': '#ffffff', 'bg_input': '#ffffff',        'accent': '#6366f1', 'accent_hover': '#818cf8', 'accent_light': '#eef2ff',        'text_primary': '#1e293b', 'text_secondary': '#64748b', 'text_muted': '#94a3b8',        'text_success': '#22c55e', 'text_error': '#ef4444', 'text_warning': '#f59e0b',        'border': '#e2e8f0', 'btn_secondary': '#f1f5f9', 'btn_danger': '#ef4444',        'progress_bg': '#e2e8f0', 'progress_fg': '#6366f1',        'log_bg': '#1e293b', 'log_fg': '#94a3b8', 'log_success': '#4ade80',        'log_error': '#f87171', 'log_warning': '#fbbf24', 'log_info': '#60a5fa',        'overlay_bg': '#4a5568',    }    DARK = {        'name': 'dark', 'bg_primary': '#0f172a', 'bg_secondary': '#1e293b',        'bg_tertiary': '#334155', 'bg_card': '#1e293b', 'bg_input': '#0f172a',        'accent': '#818cf8', 'accent_hover': '#a5b4fc', 'accent_light': '#312e81',        'text_primary': '#f1f5f9', 'text_secondary': '#cbd5e1', 'text_muted': '#64748b',        'text_success': '#4ade80', 'text_error': '#f87171', 'text_warning': '#fbbf24',        'border': '#334155', 'btn_secondary': '#334155', 'btn_danger': '#ef4444',        'progress_bg': '#334155', 'progress_fg': '#818cf8',        'log_bg': '#0f172a', 'log_fg': '#94a3b8', 'log_success': '#4ade80',        'log_error': '#f87171', 'log_warning': '#fbbf24', 'log_info': '#60a5fa',        'overlay_bg': '#1a202c',    }# ============== 配置数据类 ==============@dataclassclass PackConfig:    python_path: str = ""    main_file: str = ""    output_name: str = ""    icon_path: str = ""    output_path: str = ""    packer: str = "pyinstaller"    one_file: bool = False    no_console: bool = True    speed_mode: bool = True    enable_upx: bool = False    upx_path: str = ""    add_folders: List[str] = field(default_factory=list)# ============== 依赖扫描器 ==============class DependencyScanner:    """自动扫描Python文件中的依赖库"""    # Python标准库模块列表(常见的一部分)    STDLIB_MODULES = {        # 内置模块        'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore',        'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect',        'builtins', 'bz2', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd',        'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall',        'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg',        'cProfile', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime',        'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'email',        'encodings', 'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', 'fileinput',        'fnmatch', 'fractions', 'ftplib', 'functools', 'gc', 'getopt', 'getpass',        'gettext', 'glob', 'graphlib', 'grp', 'gzip', 'hashlib', 'heapq', 'hmac',        'html', 'http', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io',        'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale',        'logging', 'lzma', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes',        'mmap', 'modulefinder', 'multiprocessing', 'netrc', 'nis', 'nntplib',        'numbers', 'operator', 'optparse', 'os', 'ossaudiodev', 'pathlib', 'pdb',        'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib',        'posix', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile',        'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib',        'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select', 'selectors',        'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr',        'socket', 'socketserver', 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics',        'string', 'stringprep', 'struct', 'subprocess', 'sunau', 'symtable', 'sys',        'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios',        'test', 'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token',        'tokenize', 'trace', 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo',        'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'uu', 'uuid', 'venv',        'warnings', 'wave', 'weakref', 'webbrowser', 'winreg', 'winsound', 'wsgiref',        'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib',        # 常见的标准库子模块        'tkinter.ttk', 'tkinter.filedialog', 'tkinter.messagebox', 'tkinter.colorchooser',        'tkinter.font', 'tkinter.scrolledtext', 'tkinter.simpledialog',        'http.client', 'http.server', 'http.cookies', 'http.cookiejar',        'urllib.request', 'urllib.parse', 'urllib.error', 'urllib.robotparser',        'email.mime', 'email.mime.text', 'email.mime.multipart', 'email.mime.base',        'xml.etree', 'xml.etree.ElementTree', 'xml.dom', 'xml.sax',        'json.decoder', 'json.encoder',        'collections.abc', 'collections.abc',        'concurrent.futures', 'concurrent.futures.thread', 'concurrent.futures.process',        'multiprocessing.pool', 'multiprocessing.process', 'multiprocessing.Queue',        'asyncio.coroutines', 'asyncio.events', 'asyncio.tasks', 'asyncio.queues',        'logging.handlers', 'logging.config',    }    # 需要特殊处理的第三方库映射(包名 -> 导入名)    PACKAGE_IMPORT_MAP = {        'cv2': 'cv2',        'opencv-python': 'cv2',        'opencv-contrib-python': 'cv2',        'PIL': 'PIL',        'pillow': 'PIL',        'sklearn': 'sklearn',        'scikit-learn': 'sklearn',        'skimage': 'skimage',        'scikit-image': 'skimage',        'bs4': 'bs4',        'beautifulsoup4': 'bs4',        'dateutil': 'dateutil',        'python-dateutil': 'dateutil',        'yaml': 'yaml',        'pyyaml': 'yaml',        'Crypto': 'Crypto',        'pycryptodome': 'Crypto',        'usb': 'usb',        'pyusb': 'usb',        'serial': 'serial',        'pyserial': 'serial',        'Image': 'PIL',        'sqlite3': None,  # 标准库    }    # 常见需要隐藏导入的库(自动添加)    COMMON_HIDDEN_IMPORTS = {        'cv2': ['cv2'],        'numpy': ['numpy', 'numpy.core', 'numpy.core._methods', 'numpy.lib', 'numpy.lib.format'],        'pandas': ['pandas', 'pandas._libs', 'pandas._libs.tslibs'],        'PIL': ['PIL', 'PIL._imagingtk', 'PIL._imaging'],        'sklearn': ['sklearn', 'sklearn.utils', 'sklearn.utils._cython_blas', 'sklearn.neighbors.typedefs'],        'torch': ['torch', 'torch._C'],        'tensorflow': ['tensorflow', 'tensorflow.python'],        'matplotlib': ['matplotlib', 'matplotlib.backends', 'matplotlib.backends.backend_tkagg'],        'requests': ['requests', 'urllib3'],        'flask': ['flask', 'jinja2', 'werkzeug', 'click'],        'django': ['django', 'django.core'],        'PyQt5': ['PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets'],        'PyQt6': ['PyQt6', 'PyQt6.QtCore', 'PyQt6.QtGui', 'PyQt6.QtWidgets'],        'PySide2': ['PySide2', 'PySide2.QtCore', 'PySide2.QtGui', 'PySide2.QtWidgets'],        'PySide6': ['PySide6', 'PySide6.QtCore', 'PySide6.QtGui', 'PySide6.QtWidgets'],        'pytz': ['pytz'],        'dateutil': ['dateutil', 'dateutil.relativedelta'],        'lxml': ['lxml', 'lxml.etree', 'lxml._elementpath'],        'bs4': ['bs4', 'bs4.builder', 'bs4.builder.html_parser'],        'selenium': ['selenium', 'selenium.webdriver'],        'serial': ['serial', 'serial.tools', 'serial.tools.list_ports'],        'usb': ['usb', 'usb.core', 'usb.util'],        'pynput': ['pynput', 'pynput.keyboard', 'pynput.mouse'],        'pygame': ['pygame', 'pygame.base', 'pygame.locals'],        'sounddevice': ['sounddevice'],        'soundfile': ['soundfile'],        'pyaudio': ['pyaudio', '_portaudio'],        'librosa': ['librosa', 'librosa.core', 'librosa.util'],        'scipy': ['scipy', 'scipy.sparse', 'scipy.sparse.csgraph', 'scipy.special', 'scipy.spatial'],    }    @classmethod    def scan_file(cls, file_path: str) -> set:        """扫描单个Python文件,提取所有导入的模块"""        imports = set()        try:            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:                content = f.read()            # 使用正则表达式匹配 import 语句            # 匹配: import xxx, import xxx as yyy, import xxx, yyy, zzz            import_pattern = r'^\s*import\s+([\w\s,\.]+)(?:\s+as\s+\w+)?\s*$'            # 匹配: from xxx import yyy            from_pattern = r'^\s*from\s+([\w\.]+)\s+import'            for line in content.split('\n'):                line = line.strip()                # 跳过注释                if line.startswith('#'):                    continue                # 匹配 from xxx import yyy                match = re.match(from_pattern, line)                if match:                    module = match.group(1).split('.')[0]  # 取顶层模块                    imports.add(module)                    continue                # 匹配 import xxx                match = re.match(import_pattern, line)                if match:                    modules = match.group(1)                    for mod in modules.split(','):                        mod = mod.strip().split('.')[0]  # 取顶层模块                        if mod and not mod.startswith('as'):                            imports.add(mod)        except Exception as e:            print(f"扫描文件失败 {file_path}: {e}")        return imports    @classmethod    def scan_project(cls, main_file: str) -> set:        """扫描整个项目目录下的所有Python文件"""        imports = set()        project_dir = Path(main_file).parent        # 扫描主文件        imports.update(cls.scan_file(main_file))        # 扫描项目目录下的所有 .py 文件        for py_file in project_dir.rglob('*.py'):            try:                imports.update(cls.scan_file(str(py_file)))            except BaseException:                pass        # 扫描附加文件夹        # 这里可以扩展,暂时只扫描主文件所在目录        return imports    @classmethod    def filter_third_party(cls, imports: set) -> list:        """过滤出第三方库(非标准库)"""        third_party = []        for mod in imports:            # 跳过标准库            if mod in cls.STDLIB_MODULES:                continue            # 跳过相对导入            if mod.startswith('.'):                continue            # 跳过本地模块(检查是否存在对应的 .py 文件)            # 这部分可能需要更智能的处理            third_party.append(mod)        return third_party    @classmethod    def get_hidden_imports(cls, main_file: str) -> list:        """获取需要隐藏导入的模块列表"""        imports = cls.scan_project(main_file)        third_party = cls.filter_third_party(imports)        hidden_imports = []        for mod in third_party:            # 检查是否有预定义的隐藏导入            if mod in cls.COMMON_HIDDEN_IMPORTS:                hidden_imports.extend(cls.COMMON_HIDDEN_IMPORTS[mod])            else:                # 添加基础模块名                hidden_imports.append(mod)        # 去重        hidden_imports = list(set(hidden_imports))        return hidden_importsclass IconConverter:    @staticmethod    def convert_to_ico(image_path: str) -> str:        """将PNG/JPG/WEBP等格式转换为ICO临时文件"""        if not PIL_AVAILABLE:            return None        try:            img = Image.open(image_path)            # 转换为RGBA模式            if img.mode != 'RGBA':                img = img.convert('RGBA')            # 调整大小为标准图标尺寸            sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]            icons = []            for size in sizes:                resized = img.resize(size, Image.Resampling.LANCZOS)                icons.append(resized)            # 创建临时ICO文件            temp_dir = tempfile.gettempdir()            temp_ico = os.path.join(temp_dir, f"packager_icon_{os.getpid()}.ico")            icons[0].save(temp_ico, format='ICO', sizes=[(i.width, i.height) for i in icons])            return temp_ico        except Exception as e:            print(f"图标转换失败: {e}")            return None# ============== 打包器基类 ==============class BasePacker:    def __init__(self, config, log_callback=None, progress_callback=None, status_callback=None):        self.config = config        self.log_callback = log_callback        self.progress_callback = progress_callback        self.status_callback = status_callback        self.process = None        self.cancelled = False    def log(self, message, level="INFO"):        if self.log_callback:            self.log_callback(message, level)    def update_progress(self, value, status=""):        if self.progress_callback:            self.progress_callback(value)        if self.status_callback and status:            self.status_callback(status)    def cancel(self):        self.cancelled = True        if self.process:            self.process.terminate()    def cleanup(self, project_dir):        for d in ['build', '__pycache__']:            p = Path(project_dir) / d            if p.exists():                try:                    shutil.rmtree(p)                except BaseException:                    pass        for f in Path(project_dir).glob("*.spec"):            try:                f.unlink()            except BaseException:                pass# ============== PyInstaller 打包器 ==============class PyInstallerPacker(BasePacker):    def check_install(self):        try:            import PyInstaller            self.log(f"✓ PyInstaller {PyInstaller.__version__}", "SUCCESS")            return True        except ImportError:            self.log("安装 PyInstaller...", "WARNING")            try:                python_exe = self.config.python_path or sys.executable                subprocess.run([python_exe, "-m", "pip", "install", "pyinstaller", "-q"],                               check=True, capture_output=True, timeout=180)                self.log("✓ PyInstaller 安装成功", "SUCCESS")                return True            except Exception as e:                self.log(f"✗ 安装失败: {e}", "ERROR")                return False    def build_args(self):        args = ['-y']        if self.config.one_file:            args.append('--onefile')        else:            args.append('--onedir')        if self.config.no_console:            args.append('--noconsole')        if self.config.output_name:            args.extend(['--name', self.config.output_name])        if self.config.icon_path:            args.extend(['--icon', self.config.icon_path])        if self.config.output_path:            args.extend(['--distpath', self.config.output_path])        if self.config.enable_upx and self.config.upx_path:            args.extend(['--upx-dir', self.config.upx_path])        for folder in self.config.add_folders:            if folder and Path(folder).exists():                folder_name = Path(folder).name                sep = ';' if sys.platform == 'win32' else ':'                args.extend(['--add-data', f"{folder}{sep}{folder_name}"])        if self.config.speed_mode:            args.extend(['--noupx', '-s'])            # 排除一些不必要的模块(但不排除tkinter,因为用户可能需要)            for mod in ['pytest', 'unittest', 'IPython', 'jupyter', 'numpy.f2py']:                args.extend(['--exclude-module', mod])        # 确保tkinter相关模块被包含        args.extend(['--hidden-import', 'tkinter'])        args.extend(['--hidden-import', 'tkinter.filedialog'])        args.extend(['--hidden-import', 'tkinter.messagebox'])        args.extend(['--hidden-import', 'tkinter.ttk'])        # 自动扫描并添加依赖库的隐藏导入        if self.config.main_file:            try:                self.log("📦 扫描项目依赖...", "INFO")                hidden_imports = DependencyScanner.get_hidden_imports(self.config.main_file)                if hidden_imports:                    self.log(f"✓ 发现依赖: {', '.join(hidden_imports[:10])}{'...'iflen(hidden_imports) > 10else''}",                             "SUCCESS")                    for mod in hidden_imports:                        args.extend(['--hidden-import', mod])                else:                    self.log("✓ 未发现额外依赖", "INFO")            except Exception as e:                self.log(f"⚠ 依赖扫描失败: {e}", "WARNING")        return args    def pack(self):        self.log("🚀 PyInstaller 打包模式", "INFO")        if not self.config.main_file:            self.log("✗ 未选择主文件", "ERROR")            return False        main_path = Path(self.config.main_file)        if not main_path.exists():            self.log(f"✗ 文件不存在", "ERROR")            return False        self.update_progress(5, "检查环境...")        if not self.check_install():            return False        project_dir = str(main_path.parent)        self.update_progress(10, "准备打包...")        python_exe = self.config.python_path or sys.executable        args = self.build_args()        cmd = [python_exe, "-m", "PyInstaller"] + args + [str(main_path)]        return self._run_command(cmd, project_dir)    def _run_command(self, cmd, cwd):        self.update_progress(20, "打包中...")        try:            env = os.environ.copy()            env['PYTHONIOENCODING'] = 'utf-8'            env['PYTHONUTF8'] = '1'            self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, env=env)            stages = [("Analyzing", "分析", 30), ("Processing", "处理", 45),                      ("Collecting", "收集", 60), ("Building", "构建", 75),                      ("completed successfully", "完成", 95)]            current_stage = 0            while True:                if self.cancelled:                    self.process.terminate()                    return False                try:                    line_bytes = self.process.stdout.readline()                    if not line_bytes:                        if self.process.poll() is not None:                            break                        continue                    try:                        line = line_bytes.decode('utf-8', errors='replace').rstrip()                    except BaseException:                        line = line_bytes.decode('gbk', errors='replace').rstrip()                    if line:                        for kw, desc, prog in stages:                            if kw.lower() in line.lower() and prog > current_stage:                                current_stage = prog                                self.update_progress(prog, f"{desc}...")                                break                        lvl = "INFO"                        ll = line.lower()                        if "error" in ll or "failed" in ll:                            lvl = "ERROR"                        elif "warning" in ll:                            lvl = "WARNING"                        elif "completed" in ll or "successfully" in ll:                            lvl = "SUCCESS"                        self.log(line, lvl)                except BaseException:                    break            self.process.wait()            if self.process.returncode == 0:                self.update_progress(100, "完成!")                if self.config.one_file:                    self.cleanup(cwd)                self.log("✓ 打包成功!", "SUCCESS")                main_path = Path(self.config.main_file)                output_dir = Path(self.config.output_path or main_path.parent) / 'dist'                output_name = self.config.output_name or main_path.stem                output_file = output_dir / f"{output_name}.exe" if self.config.one_file else output_dir / output_name / f"{output_name}.exe"                if output_file.exists():                    size = output_file.stat().st_size / (1024 * 1024)                    self.log(f"输出: {output_file} ({size:.1f}MB)", "SUCCESS")                return True            else:                self.log(f"✗ 打包失败", "ERROR")                return False        except Exception as e:            self.log(f"✗ 出错: {e}", "ERROR")            return False# ============== Nuitka 打包器 ==============class NuitkaPacker(BasePacker):    def check_install(self):        try:            result = subprocess.run([sys.executable, "-m", "nuitka", "--version"],                                    capture_output=True, text=True, timeout=10)            if result.returncode == 0:                self.log("✓ Nuitka 已安装", "SUCCESS")                return True        except BaseException:            pass        self.log("安装 Nuitka...", "WARNING")        try:            python_exe = self.config.python_path or sys.executable            subprocess.run([python_exe, "-m", "pip", "install", "nuitka", "-q"],                           check=True, capture_output=True, timeout=300)            self.log("✓ Nuitka 安装成功", "SUCCESS")            return True        except Exception as e:            self.log(f"✗ 安装失败: {e}", "ERROR")            return False    def build_args(self):        args = ['--follow-imports', '--assume-yes-for-downloads']        if self.config.one_file:            args.append('--onefile')        if self.config.no_console:            args.append('--disable-console')        if self.config.output_name:            args.extend(['-o', f"{self.config.output_name}.exe"])        if self.config.icon_path:            args.extend(['--windows-icon-from-ico', self.config.icon_path])        if self.config.output_path:            args.extend(['--output-dir', self.config.output_path])        if self.config.enable_upx and self.config.upx_path:            args.extend(['--upx-dir', self.config.upx_path])        for folder in self.config.add_folders:            if folder and Path(folder).exists():                args.extend(['--include-package-data', folder])        if self.config.speed_mode:            args.append('--lto=no')        return args    def pack(self):        self.log("🔥 Nuitka 编译模式", "INFO")        if not self.config.main_file:            self.log("✗ 未选择主文件", "ERROR")            return False        main_path = Path(self.config.main_file)        if not main_path.exists():            self.log(f"✗ 文件不存在", "ERROR")            return False        self.update_progress(5, "检查环境...")        if not self.check_install():            return False        project_dir = str(main_path.parent)        self.update_progress(10, "准备编译...")        python_exe = self.config.python_path or sys.executable        args = self.build_args()        cmd = [python_exe, "-m", "nuitka"] + args + [str(main_path)]        return self._run_command(cmd, project_dir)    def _run_command(self, cmd, cwd):        self.update_progress(15, "初始化编译环境...")        self.log("⚠ Nuitka编译可能需要10-30分钟,请耐心等待...", "WARNING")        self.log(f"命令: {' '.join(cmd[:5])}...", "INFO")        try:            env = os.environ.copy()            env['PYTHONIOENCODING'] = 'utf-8'            self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, env=env)            progress = 15            line_count = 0            while True:                if self.cancelled:                    self.process.terminate()                    return False                try:                    line_bytes = self.process.stdout.readline()                    if not line_bytes:                        if self.process.poll() is not None:                            break                        continue                    try:                        line = line_bytes.decode('utf-8', errors='replace').rstrip()                    except BaseException:                        line = line_bytes.decode('gbk', errors='replace').rstrip()                    line_count += 1                    # 更频繁地更新进度                    if line:                        ll = line.lower()                        # 根据输出内容更新进度                        if "compiling" in ll:                            progress = min(progress + 1, 70)                            self.update_progress(progress, "编译Python代码...")                        elif "linking" in ll:                            progress = max(progress, 80)                            self.update_progress(progress, "链接可执行文件...")                        elif "optimizing" in ll:                            progress = max(progress, 75)                            self.update_progress(progress, "优化代码...")                        elif "generating" in ll:                            progress = max(progress, 50)                            self.update_progress(progress, "生成中间代码...")                        elif "downloading" in ll:                            self.update_progress(20, "下载依赖组件...")                        elif "success" in ll or "completed" in ll:                            progress = max(progress, 95)                            self.update_progress(progress, "编译完成!")                        elif "error" in ll:                            self.log(line, "ERROR")                            continue                        elif line_count % 50 == 0:                            # 每50行更新一次进度,给用户反馈                            progress = min(progress + 0.5, 85)                            self.update_progress(progress, f"编译进行中... ({int(progress)}%)")                        # 确定日志级别                        lvl = "INFO"                        if "error" in ll:                            lvl = "ERROR"                        elif "warning" in ll:                            lvl = "WARNING"                        elif "success" in ll or "completed" in ll:                            lvl = "SUCCESS"                        # 只输出重要信息                        if any(kw in ll for kw in ["error", "warning", "success", "completed", "compiling", "linking"]):                            self.log(line, lvl)                except BaseException:                    break            self.process.wait()            if self.process.returncode == 0:                self.update_progress(100, "完成!")                self.log("✓ 编译成功!", "SUCCESS")                # 查找输出文件                main_path = Path(self.config.main_file)                output_dir = Path(self.config.output_path or main_path.parent)                output_name = self.config.output_name or main_path.stem                # Nuitka默认输出到当前目录的output目录                possible_outputs = [                    output_dir / f"{output_name}.exe",                    output_dir / f"{output_name}.onefile-build" / f"{output_name}.exe",                    Path(cwd) / f"{output_name}.exe",                    Path(cwd) / "output" / f"{output_name}.exe",                ]                for output_file in possible_outputs:                    if output_file.exists():                        size = output_file.stat().st_size / (1024 * 1024)                        self.log(f"输出: {output_file} ({size:.1f}MB)", "SUCCESS")                        break                return True            else:                self.log("✗ 编译失败,请检查错误信息", "ERROR")                return False        except Exception as e:            self.log(f"✗ 出错: {e}", "ERROR")            return False# ============== 检测器 ==============class PythonDetector:    @staticmethod    def find_python_interpreters():        interpreters = []        current = sys.executable        version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"        interpreters.append({'path': current, 'version': version, 'name': f"Python {version} (当前)", 'current': True})        if sys.platform == 'win32':            import glob            for pattern in [os.path.expandvars(r'%LOCALAPPDATA%\Programs\Python\Python*\python.exe'),                            os.path.expandvars(r'%PROGRAMFILES%\Python*\python.exe')]:                for path in glob.glob(pattern):                    if path != current and os.path.exists(path):                        try:                            result = subprocess.run([path, '--version'], capture_output=True, text=True, timeout=5)                            match = re.search(r'Python (\d+\.\d+\.\d+)', result.stdout + result.stderr)                            if match:                                interpreters.append(                                    {'path': path, 'version': match.group(1), 'name': f"Python {match.group(1)}",                                     'current': False})                        except BaseException:                            pass        seen = set()        return [i for i in interpreters if i['path'] not in seen and not seen.add(i['path'])]class UPXDetector:    @staticmethod    def find_upx():        search_paths = [Path(__file__).parent if '__file__' in dir() else Path.cwd(), Path.cwd(),                        Path(sys.argv[0]).parent if sys.argv else Path.cwd()]        for base_path in search_paths:            if not base_path.exists():                continue            for pattern in ['upx-*', 'upx', 'UPX*']:                for upx_dir in base_path.glob(pattern):                    if upx_dir.is_dir():                        upx_exe = upx_dir / ('upx.exe' if sys.platform == 'win32' else 'upx')                        if upx_exe.exists():                            return str(upx_dir), upx_dir.name            upx_exe = base_path / ('upx.exe' if sys.platform == 'win32' else 'upx')            if upx_exe.exists():                return str(base_path), base_path.name        return None, None# ============== GUI界面 ==============class PackagerGUI:    def __init__(self):        self.root = tk.Tk()        self.root.title("灵感江Python打包exe工具 v1.0")        # 固定窗口大小,确保所有内容完整显示        self.win_width = 580        self.win_height = 780        # 获取屏幕尺寸用于居中        screen_width = self.root.winfo_screenwidth()        screen_height = self.root.winfo_screenheight()        # 居中显示        x = (screen_width - self.win_width) // 2        y = (screen_height - self.win_height) // 2        self.root.geometry(f"{self.win_width}x{self.win_height}+{x}+{y}")        # 设置最小尺寸等于当前尺寸,禁止缩小        self.root.minsize(self.win_width, self.win_height)        self.root.maxsize(self.win_width, self.win_height)        self.theme_name = 'light'        self.theme = Theme.LIGHT        self.config = PackConfig()        self.packer = None        self.log_queue = queue.Queue()        self.python_interpreters = []        self.upx_path = None        self.is_packing = False        self.converted_icon_path = None  # 转换后的图标路径        self.progress_var = tk.DoubleVar(value=0)        self.status_var = tk.StringVar(value="就绪")        self.root.configure(bg=self.theme['bg_primary'])        self._create_ui()        self._process_log_queue()        self.root.after(100, self._auto_detect_python)        self.root.after(150, self._auto_detect_upx)    def _create_ui(self):        # 主框架 - 使用Canvas和Scrollbar实现滚动        self.main_container = tk.Frame(self.root, bg=self.theme['bg_primary'])        self.main_container.pack(fill=tk.BOTH, expand=True)        # 主内容框架        self.main_frame = tk.Frame(self.main_container, bg=self.theme['bg_primary'])        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=25, pady=20)        # 标题区域        self._create_header()        # 设置卡片(单列布局)        self._create_settings_card()        # 底部按钮区域        self._create_bottom_panel()        # 打包进度弹窗(居中小方块)        self._create_progress_popup()    def _create_header(self):        header = tk.Frame(self.main_frame, bg=self.theme['bg_primary'])        header.pack(fill=tk.X, pady=(0, 15))        # Logo和标题        left = tk.Frame(header, bg=self.theme['bg_primary'])        left.pack(side=tk.LEFT)        # Logo圆形        logo_canvas = tk.Canvas(left, width=42, height=42, bg=self.theme['bg_primary'],                                highlightthickness=0)        logo_canvas.pack(side=tk.LEFT, padx=(0, 12))        logo_canvas.create_oval(4, 4, 38, 38, fill=self.theme['accent'], outline='')        logo_canvas.create_text(21, 21, text="Py", font=('Segoe UI', 12, 'bold'), fill='white')        # 标题文字        title_frame = tk.Frame(left, bg=self.theme['bg_primary'])        title_frame.pack(side=tk.LEFT)        self.title_label = tk.Label(title_frame, text="灵感江Python打包exe工具",                                    font=('Microsoft YaHei UI', 15, 'bold'),                                    bg=self.theme['bg_primary'], fg=self.theme['text_primary'])        self.title_label.pack(anchor='w')        self.subtitle_label = tk.Label(title_frame, text="v1.0 精简美化版",                                       font=('Microsoft YaHei UI', 9),                                       bg=self.theme['bg_primary'], fg=self.theme['text_muted'])        self.subtitle_label.pack(anchor='w')        # 主题切换按钮        self.theme_btn = tk.Button(header, text="🌙", font=('Segoe UI', 14),                                   bg=self.theme['bg_tertiary'], fg=self.theme['text_primary'],                                   relief='flat', width=3, cursor='hand2',                                   command=self._toggle_theme)        self.theme_btn.pack(side=tk.RIGHT)    def _create_settings_card(self):        # 主卡片容器        card = tk.Frame(self.main_frame, bg=self.theme['bg_card'],                        highlightthickness=1, highlightbackground=self.theme['border'])        card.pack(fill=tk.BOTH, expand=True, pady=(0, 12))        # 内边距容器        content = tk.Frame(card, bg=self.theme['bg_card'])        content.pack(fill=tk.BOTH, expand=True, padx=18, pady=15)        # ===== 打包引擎 =====        row = tk.Frame(content, bg=self.theme['bg_card'])        row.pack(fill=tk.X, pady=5)        tk.Label(row, text="打包引擎", font=('Microsoft YaHei UI', 10), bg=self.theme['bg_card'],                 fg=self.theme['text_secondary'], width=10, anchor='w').pack(side=tk.LEFT)        engine_frame = tk.Frame(row, bg=self.theme['bg_card'])        engine_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)        self.packer_var = tk.StringVar(value="pyinstaller")        pyinstaller_rb = tk.Radiobutton(engine_frame, text="PyInstaller", variable=self.packer_var,                                        value="pyinstaller", font=('Microsoft YaHei UI', 10),                                        bg=self.theme['bg_card'], fg=self.theme['text_primary'],                                        selectcolor=self.theme['bg_tertiary'],                                        activebackground=self.theme['bg_card'])        pyinstaller_rb.pack(side=tk.LEFT)        nuitka_rb = tk.Radiobutton(engine_frame, text="Nuitka", variable=self.packer_var,                                   value="nuitka", font=('Microsoft YaHei UI', 10),                                   bg=self.theme['bg_card'], fg=self.theme['text_primary'],                                   selectcolor=self.theme['bg_tertiary'],                                   activebackground=self.theme['bg_card'])        nuitka_rb.pack(side=tk.LEFT, padx=(20, 0))        # ===== Python环境 =====        row = tk.Frame(content, bg=self.theme['bg_card'])        row.pack(fill=tk.X, pady=5)        tk.Label(row, text="Python", font=('Microsoft YaHei UI', 10), bg=self.theme['bg_card'],                 fg=self.theme['text_secondary'], width=10, anchor='w').pack(side=tk.LEFT)        self.python_combo = ttk.Combobox(row, font=('Microsoft YaHei UI', 10))        self.python_combo.pack(side=tk.LEFT, fill=tk.X, expand=True)        # 选择Python解释器按钮        tk.Button(row, text="选择", font=('Microsoft YaHei UI', 9), bg=self.theme['bg_tertiary'],                  fg=self.theme['text_primary'], relief='flat', padx=10, cursor='hand2',                  command=self._browse_python).pack(side=tk.RIGHT, padx=(8, 0))        # 刷新自动检测按钮        tk.Button(row, text="🔄", font=('Segoe UI', 9), bg=self.theme['bg_tertiary'],                  fg=self.theme['text_primary'], relief='flat', padx=8, cursor='hand2',                  command=self._auto_detect_python).pack(side=tk.RIGHT, padx=(8, 0))        # ===== 主文件 =====        self.main_file_var = tk.StringVar()        self._create_input_row(content, "*主文件", self.main_file_var, "选择",                               lambda: self._browse_file(self.main_file_var, [("Python文件", "*.py")]), True)        # ===== 输出名称 =====        self.output_name_var = tk.StringVar()        self._create_input_row(content, "输出名称", self.output_name_var, None, False)        # ===== 应用图标(支持多格式)=====        self.icon_path_var = tk.StringVar()        self.icon_entry = self._create_input_row(content, "应用图标", self.icon_path_var, "选择",                                                 lambda: self._browse_icon(), False, "可选可不选,不选自动默认图标")        # 图标格式提示        icon_hint = tk.Label(content, text="支持 .ico / .png / .jpg / .webp 格式",                             font=('Microsoft YaHei UI', 8), bg=self.theme['bg_card'],                             fg=self.theme['text_muted'])        icon_hint.pack(anchor='w', padx=(10, 0), pady=(0, 3))        # ===== 输出路径 =====        self.output_path_var = tk.StringVar()        self._create_input_row(content, "输出路径", self.output_path_var, "选择",                               lambda: self._browse_folder(self.output_path_var), False)        # ===== 选项 =====        options_frame = tk.Frame(content, bg=self.theme['bg_card'])        options_frame.pack(fill=tk.X, pady=(8, 5))        self.one_file_var = tk.BooleanVar(value=False)        self.no_console_var = tk.BooleanVar(value=True)        self.speed_mode_var = tk.BooleanVar(value=True)        tk.Checkbutton(options_frame, text="单文件", variable=self.one_file_var,                       font=('Microsoft YaHei UI', 10), bg=self.theme['bg_card'], fg=self.theme['text_primary'],                       selectcolor=self.theme['bg_tertiary'], activebackground=self.theme['bg_card'],                       highlightthickness=0).pack(side=tk.LEFT)        tk.Checkbutton(options_frame, text="隐藏控制台", variable=self.no_console_var,                       font=('Microsoft YaHei UI', 10), bg=self.theme['bg_card'], fg=self.theme['text_primary'],                       selectcolor=self.theme['bg_tertiary'], activebackground=self.theme['bg_card'],                       highlightthickness=0).pack(side=tk.LEFT, padx=(15, 0))        tk.Checkbutton(options_frame, text="极速模式", variable=self.speed_mode_var,                       font=('Microsoft YaHei UI', 10), bg=self.theme['bg_card'], fg=self.theme['text_primary'],                       selectcolor=self.theme['bg_tertiary'], activebackground=self.theme['bg_card'],                       highlightthickness=0).pack(side=tk.LEFT, padx=(15, 0))        # ===== 分隔线 =====        tk.Frame(content, bg=self.theme['border'], height=1).pack(fill=tk.X, pady=12)        # ===== UPX压缩 =====        upx_row = tk.Frame(content, bg=self.theme['bg_card'])        upx_row.pack(fill=tk.X, pady=5)        self.enable_upx_var = tk.BooleanVar(value=False)        tk.Checkbutton(upx_row, text="UPX压缩", variable=self.enable_upx_var,                       font=('Microsoft YaHei UI', 10), bg=self.theme['bg_card'], fg=self.theme['text_primary'],                       selectcolor=self.theme['bg_tertiary'], activebackground=self.theme['bg_card'],                       highlightthickness=0).pack(side=tk.LEFT)        self.upx_status_label = tk.Label(upx_row, text="检测中...",                                         font=('Microsoft YaHei UI', 9), bg=self.theme['bg_card'],                                         fg=self.theme['text_muted'])        self.upx_status_label.pack(side=tk.LEFT, padx=(10, 0))        # ===== 附加文件夹 =====        folder_label = tk.Label(content, text="附加文件夹", font=('Microsoft YaHei UI', 10),                                bg=self.theme['bg_card'], fg=self.theme['text_secondary'])        folder_label.pack(anchor='w', pady=(8, 3))        folder_row = tk.Frame(content, bg=self.theme['bg_card'])        folder_row.pack(fill=tk.X, pady=3)        self.folder_path_var = tk.StringVar()        tk.Entry(folder_row, textvariable=self.folder_path_var, font=('Microsoft YaHei UI', 10),                 bg=self.theme['bg_input'], fg=self.theme['text_primary'], relief='flat',                 highlightthickness=1, highlightbackground=self.theme['border']).pack(            side=tk.LEFT, fill=tk.X, expand=True)        tk.Button(folder_row, text="+", font=('Segoe UI', 12, 'bold'), bg=self.theme['accent'],                  fg='white', relief='flat', padx=12, cursor='hand2',                  command=self._add_folder).pack(side=tk.RIGHT, padx=(8, 0))        tk.Button(folder_row, text="📁", font=('Segoe UI', 10), bg=self.theme['bg_tertiary'],                  fg=self.theme['text_primary'], relief='flat', padx=10, cursor='hand2',                  command=lambda: self._browse_folder(self.folder_path_var)).pack(side=tk.RIGHT, padx=(8, 0))        # 文件夹列表        list_frame = tk.Frame(content, bg=self.theme['bg_card'])        list_frame.pack(fill=tk.BOTH, expand=True, pady=5)        self.folder_listbox = tk.Listbox(list_frame, font=('Microsoft YaHei UI', 9), height=3,                                         bg=self.theme['bg_input'], fg=self.theme['text_primary'],                                         selectbackground=self.theme['accent_light'], relief='flat',                                         highlightthickness=1, highlightbackground=self.theme['border'])        self.folder_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)        tk.Button(content, text="删除选中", font=('Microsoft YaHei UI', 9), bg=self.theme['btn_danger'],                  fg='white', relief='flat', padx=12, cursor='hand2',                  command=self._remove_folder).pack(anchor='w', pady=(5, 0))    def _create_input_row(self, parent, label, var, btn_text, btn_cmd, required=False, placeholder=None):        row = tk.Frame(parent, bg=self.theme['bg_card'])        row.pack(fill=tk.X, pady=5)        fg = self.theme['text_error'] if required else self.theme['text_secondary']        tk.Label(row, text=label, font=('Microsoft YaHei UI', 10), bg=self.theme['bg_card'],                 fg=fg, width=10, anchor='w').pack(side=tk.LEFT)        entry = tk.Entry(row, textvariable=var, font=('Microsoft YaHei UI', 10), bg=self.theme['bg_input'],                         fg=self.theme['text_primary'], relief='flat', highlightthickness=1,                         highlightbackground=self.theme['border'])        entry.pack(side=tk.LEFT, fill=tk.X, expand=True)        # 显示占位符        if placeholder:            entry.insert(0, placeholder)            entry.config(fg=self.theme['text_muted'])            def on_focus_in(event):                if entry.get() == placeholder:                    entry.delete(0, tk.END)                    entry.config(fg=self.theme['text_primary'])            def on_focus_out(event):                if not entry.get():                    entry.insert(0, placeholder)                    entry.config(fg=self.theme['text_muted'])            entry.bind('<FocusIn>', on_focus_in)            entry.bind('<FocusOut>', on_focus_out)        if btn_text:            tk.Button(row, text=btn_text, font=('Microsoft YaHei UI', 9), bg=self.theme['bg_tertiary'],                      fg=self.theme['text_primary'], relief='flat', padx=12, cursor='hand2',                      command=btn_cmd).pack(side=tk.RIGHT, padx=(8, 0))        return entry    def _create_bottom_panel(self):        # 按钮区域        btn_frame = tk.Frame(self.main_frame, bg=self.theme['bg_primary'])        btn_frame.pack(pady=(8, 0))        self.start_btn = tk.Button(btn_frame, text="🚀 开始打包", font=('Microsoft YaHei UI', 12, 'bold'),                                   bg=self.theme['bg_tertiary'], fg=self.theme['text_muted'],                                   relief='flat', padx=40, pady=12, cursor='arrow',                                   state=tk.DISABLED, command=self._start_pack)        self.start_btn.pack(side=tk.LEFT, padx=8)        self.cancel_btn = tk.Button(btn_frame, text="取消打包", font=('Microsoft YaHei UI', 11),                                    bg=self.theme['bg_tertiary'], fg=self.theme['text_muted'],                                    relief='flat', padx=25, pady=10, cursor='arrow',                                    state=tk.DISABLED, command=self._cancel_pack)        self.cancel_btn.pack(side=tk.LEFT, padx=8)        # 状态栏        self.status_bar = tk.Frame(self.main_frame, bg=self.theme['bg_tertiary'], height=30)        self.status_bar.pack(fill=tk.X, pady=(12, 0))        self.status_bar.pack_propagate(False)        self.footer = tk.Label(self.status_bar, text="⚠ 请先选择主文件", font=('Microsoft YaHei UI', 9),                               bg=self.theme['bg_tertiary'], fg=self.theme['text_warning'])        self.footer.pack(side=tk.LEFT, padx=12, pady=5)        tk.Label(self.status_bar, text="v1.0", font=('Microsoft YaHei UI', 9),                 bg=self.theme['bg_tertiary'], fg=self.theme['text_muted']).pack(side=tk.RIGHT, padx=12, pady=5)        # 初始化按钮状态        self._update_btn_state()    def _update_btn_state(self):        """更新按钮状态"""        main_file = self.main_file_var.get().strip()        has_main_file = bool(main_file and not main_file.startswith('点击选择'))        if self.is_packing:            # 打包进行中            self.start_btn.config(state=tk.DISABLED, bg=self.theme['bg_tertiary'],                                  fg=self.theme['text_muted'], cursor='arrow')            self.cancel_btn.config(state=tk.NORMAL, bg=self.theme['btn_danger'],                                   fg='white', cursor='hand2')        else:            # 非打包状态            self.cancel_btn.config(state=tk.DISABLED, bg=self.theme['bg_tertiary'],                                   fg=self.theme['text_muted'], cursor='arrow')            if has_main_file:                # 有主文件,可以打包                self.start_btn.config(state=tk.NORMAL, bg=self.theme['accent'],                                      fg='white', cursor='hand2')                self.footer.config(text="✅ 就绪", fg=self.theme['text_secondary'])            else:                # 没有主文件,不能打包                self.start_btn.config(state=tk.DISABLED, bg=self.theme['bg_tertiary'],                                      fg=self.theme['text_muted'], cursor='arrow')                self.footer.config(text="⚠ 请先选择主文件", fg=self.theme['text_warning'])    def _create_progress_popup(self):        """创建居中的小方块进度弹窗"""        # 半透明背景遮罩        self.overlay = tk.Frame(self.root, bg=self.theme['overlay_bg'])        # 居中的小方块容器        self.popup_frame = tk.Frame(self.overlay, bg=self.theme['bg_card'],                                    highlightthickness=1, highlightbackground=self.theme['border'])        # 弹窗标题        self.popup_header = tk.Frame(self.popup_frame, bg=self.theme['bg_card'])        self.popup_header.pack(fill=tk.X, padx=18, pady=(15, 10))        self.popup_title = tk.Label(self.popup_header, text="正在打包...",                                    font=('Microsoft YaHei UI', 12, 'bold'),                                    bg=self.theme['bg_card'], fg=self.theme['text_primary'])        self.popup_title.pack(side=tk.LEFT)        self.popup_percent = tk.Label(self.popup_header, text="0%",                                      font=('Microsoft YaHei UI', 12, 'bold'),                                      bg=self.theme['bg_card'], fg=self.theme['accent'])        self.popup_percent.pack(side=tk.RIGHT)        # 进度条        self.progress_container = tk.Frame(self.popup_frame, bg=self.theme['border'], height=10)        self.progress_container.pack(fill=tk.X, padx=18, pady=6)        self.progress_container.pack_propagate(False)        self.popup_progress = tk.Canvas(self.progress_container, height=10,                                        bg=self.theme['progress_bg'], highlightthickness=0)        self.popup_progress.pack(fill=tk.BOTH, expand=True, padx=1, pady=1)        # 状态文本        self.popup_status = tk.Label(self.popup_frame, text="准备中...",                                     font=('Microsoft YaHei UI', 10), bg=self.theme['bg_card'],                                     fg=self.theme['text_secondary'])        self.popup_status.pack(pady=(2, 10))        # 日志区域(小尺寸)        self.log_frame = tk.Frame(self.popup_frame, bg=self.theme['log_bg'])        self.log_frame.pack(fill=tk.BOTH, expand=True, padx=18, pady=(0, 15))        self.popup_log = tk.Text(self.log_frame, font=('Consolas', 9), bg=self.theme['log_bg'],                                 fg=self.theme['log_fg'], relief='flat', state=tk.DISABLED,                                 wrap=tk.WORD, padx=10, pady=8, height=8, width=45)        self.popup_log.pack(fill=tk.BOTH, expand=True)        self.popup_log.tag_configure('INFO', foreground=self.theme['log_info'])        self.popup_log.tag_configure('SUCCESS', foreground=self.theme['log_success'])        self.popup_log.tag_configure('ERROR', foreground=self.theme['log_error'])        self.popup_log.tag_configure('WARNING', foreground=self.theme['log_warning'])        # 结果显示区域(初始隐藏)        self.result_container = tk.Frame(self.popup_frame, bg=self.theme['bg_card'])        self.result_icon = tk.Label(self.result_container, text="", font=('Segoe UI', 36),                                    bg=self.theme['bg_card'])        self.result_icon.pack(pady=12)        self.result_text = tk.Label(self.result_container, text="", font=('Microsoft YaHei UI', 13, 'bold'),                                    bg=self.theme['bg_card'], fg=self.theme['text_primary'])        self.result_text.pack(pady=6)        self.result_detail = tk.Label(self.result_container, text="", font=('Microsoft YaHei UI', 10),                                      bg=self.theme['bg_card'], fg=self.theme['text_secondary'],                                      wraplength=300, justify='center')        self.result_detail.pack(pady=6)    def _show_progress_popup(self):        """显示进度弹窗"""        # 显示遮罩层        self.overlay.place(x=0, y=0, relwidth=1, relheight=1)        # 使用after确保遮罩层已经渲染        self.root.after(10, self._center_popup)        # 禁用开始按钮,启用取消按钮        self.start_btn.config(state=tk.DISABLED, bg=self.theme['bg_tertiary'])        self.cancel_btn.config(state=tk.NORMAL, bg=self.theme['btn_danger'], fg='white')    def _center_popup(self):        """将弹窗居中显示"""        self.root.update_idletasks()        # 获取窗口大小        overlay_width = self.overlay.winfo_width()        overlay_height = self.overlay.winfo_height()        # 设置弹窗大小        popup_width = 420        popup_height = 350        # 计算居中位置        x = (overlay_width - popup_width) // 2        y = (overlay_height - popup_height) // 2        # 放置弹窗        self.popup_frame.place(x=x, y=y, width=popup_width, height=popup_height)    def _hide_progress_popup(self):        """隐藏进度弹窗"""        self.popup_frame.place_forget()        self.overlay.place_forget()        # 重置按钮状态        self.start_btn.config(state=tk.NORMAL, bg=self.theme['accent'])        self.cancel_btn.config(state=tk.DISABLED, bg=self.theme['bg_tertiary'], fg=self.theme['text_muted'])        # 隐藏结果显示        self.result_container.pack_forget()        # 恢复进度元素显示        self.popup_header.pack(fill=tk.X, padx=18, pady=(15, 10))        self.popup_title.pack(side=tk.LEFT)        self.popup_percent.pack(side=tk.RIGHT)        self.progress_container.pack(fill=tk.X, padx=18, pady=6)        self.popup_status.pack(pady=(2, 10))        self.log_frame.pack(fill=tk.BOTH, expand=True, padx=18, pady=(0, 15))    def _show_result_in_popup(self, success, message, detail=""):        """在弹窗中显示结果"""        # 隐藏进度相关元素        self.popup_header.pack_forget()        self.progress_container.pack_forget()        self.popup_status.pack_forget()        self.log_frame.pack_forget()        # 显示结果        if success:            self.result_icon.config(text="✅", fg=self.theme['text_success'])            self.result_text.config(text="打包成功!", fg=self.theme['text_success'])        else:            self.result_icon.config(text="❌", fg=self.theme['text_error'])            self.result_text.config(text="打包失败", fg=self.theme['text_error'])        self.result_detail.config(text=detail)        self.result_container.pack(fill=tk.BOTH, expand=True, padx=18, pady=12)        # 禁用取消按钮        self.cancel_btn.config(state=tk.DISABLED, bg=self.theme['bg_tertiary'], fg=self.theme['text_muted'])        # 2秒后自动隐藏        self.root.after(2000, self._hide_progress_popup)    def _update_progress_ui(self, value):        self.progress_var.set(value)        self._draw_popup_progress()    def _update_status_ui(self, status):        self.status_var.set(status)        self.popup_status.config(text=status)        self.popup_percent.config(text=f"{int(self.progress_var.get())}%")    def _draw_popup_progress(self, event=None):        self.popup_progress.delete('all')        width = self.popup_progress.winfo_width()        height = self.popup_progress.winfo_height()        progress = self.progress_var.get()        fill_width = int(width * progress / 100)        self.popup_percent.config(text=f"{int(progress)}%")        if fill_width > 0:            self.popup_progress.create_rectangle(0, 0, fill_width, height,                                                 fill=self.theme['progress_fg'], outline='')    def _log_callback(self, message, level='INFO'):        self.log_queue.put((message, level))    def _process_log_queue(self):        try:            while True:                message, level = self.log_queue.get_nowait()                self.popup_log.config(state=tk.NORMAL)                self.popup_log.insert(tk.END, message + '\n', level)                self.popup_log.see(tk.END)                self.popup_log.config(state=tk.DISABLED)        except queue.Empty:            pass        self.root.after(100, self._process_log_queue)    # ============== 其他方法 ==============    def _auto_detect_python(self):        self.python_interpreters = PythonDetector.find_python_interpreters()        values = [f"{p['name']} - {p['path']}" for p in self.python_interpreters]        self.python_combo['values'] = values        if values:            for i, p in enumerate(self.python_interpreters):                if p.get('current'):                    self.python_combo.current(i)                    break            else:                self.python_combo.current(0)    def _auto_detect_upx(self):        upx_path, upx_name = UPXDetector.find_upx()        if upx_path:            self.upx_path = upx_path            self.upx_status_label.config(text=f"✅ {upx_name}", fg=self.theme['text_success'])        else:            self.upx_path = None            self.upx_status_label.config(text="未检测到", fg=self.theme['text_muted'])    def _toggle_theme(self):        if self.theme_name == 'light':            self.theme_name = 'dark'            self.theme = Theme.DARK            self.theme_btn.config(text="☀️")        else:            self.theme_name = 'light'            self.theme = Theme.LIGHT            self.theme_btn.config(text="🌙")        self._apply_theme()    def _apply_theme(self):        t = self.theme        self.root.configure(bg=t['bg_primary'])        self.main_frame.configure(bg=t['bg_primary'])        self.title_label.configure(bg=t['bg_primary'], fg=t['text_primary'])        self.subtitle_label.configure(bg=t['bg_primary'], fg=t['text_muted'])        self.theme_btn.configure(bg=t['bg_tertiary'], fg=t['text_primary'])        self.footer.configure(bg=t['bg_tertiary'], fg=t['text_secondary'])        self.status_bar.configure(bg=t['bg_tertiary'])        self.overlay.configure(bg=t['overlay_bg'])        self.popup_frame.configure(bg=t['bg_card'], highlightbackground=t['border'])        self.popup_header.configure(bg=t['bg_card'])        self.popup_title.configure(bg=t['bg_card'], fg=t['text_primary'])        self.popup_percent.configure(bg=t['bg_card'], fg=t['accent'])        self.progress_container.configure(bg=t['border'])        self.popup_progress.configure(bg=t['progress_bg'])        self.popup_status.configure(bg=t['bg_card'], fg=t['text_secondary'])        self.log_frame.configure(bg=t['log_bg'])        self.popup_log.configure(bg=t['log_bg'], fg=t['log_fg'])        self.result_container.configure(bg=t['bg_card'])        self.result_icon.configure(bg=t['bg_card'])        self.result_text.configure(bg=t['bg_card'])        self.result_detail.configure(bg=t['bg_card'], fg=t['text_secondary'])    def _browse_file(self, var, filetypes):        f = filedialog.askopenfilename(filetypes=filetypes)        if f:            var.set(f)            # 如果是主文件选择,更新按钮状态            if var == self.main_file_var:                self._update_btn_state()    def _browse_folder(self, var):        f = filedialog.askdirectory()        if f:            var.set(f)    def _browse_icon(self):        """选择图标文件,支持多种格式"""        filetypes = [            ("图标文件", "*.ico *.png *.jpg *.jpeg *.webp *.bmp *.gif"),            ("ICO 文件", "*.ico"),            ("PNG 文件", "*.png"),            ("JPG 文件", "*.jpg *.jpeg"),            ("WEBP 文件", "*.webp"),            ("所有文件", "*.*")        ]        f = filedialog.askopenfilename(filetypes=filetypes)        if f:            self.icon_path_var.set(f)    def _browse_python(self):        """手动选择Python解释器"""        if sys.platform == 'win32':            filetypes = [("Python解释器", "python.exe"), ("所有文件", "*.*")]        else:            filetypes = [("Python解释器", "python python3"), ("所有文件", "*.*")]        f = filedialog.askopenfilename(filetypes=filetypes, title="选择Python解释器")        if f:            # 获取Python版本            try:                result = subprocess.run([f, '--version'], capture_output=True, text=True, timeout=5)                match = re.search(r'Python (\d+\.\d+\.\d+)', result.stdout + result.stderr)                if match:                    version = match.group(1)                    display_text = f"Python {version} - {f}"                else:                    display_text = f            except BaseException:                display_text = f            # 更新下拉框            current_values = list(self.python_combo['values'])            if display_text not in current_values:                current_values.insert(0, display_text)                self.python_combo['values'] = current_values            self.python_combo.set(display_text)    def _add_folder(self):        folder = self.folder_path_var.get().strip()        if folder and Path(folder).exists() and folder not in self.folder_listbox.get(0, tk.END):            self.folder_listbox.insert(tk.END, folder)            self.folder_path_var.set('')    def _remove_folder(self):        sel = self.folder_listbox.curselection()        if sel:            self.folder_listbox.delete(sel[0])    def _get_config_from_ui(self):        py_text = self.python_combo.get()        python_path = py_text.split(' - ')[-1] if ' - ' in py_text else ''        # 处理图标转换        icon_path = self.icon_path_var.get()        # 检查是否是占位符文本        placeholder_texts = ['可选', '默认', '不选', '自动', '选择图标']        is_placeholder = any(txt in icon_path for txt in placeholder_texts) if icon_path else False        if is_placeholder or not icon_path:            icon_path = ''        elif icon_path:            ext = Path(icon_path).suffix.lower()            if ext != '.ico':                # 需要转换                if PIL_AVAILABLE:                    converted_icon = IconConverter.convert_to_ico(icon_path)                    if converted_icon:                        self.converted_icon_path = converted_icon                        icon_path = converted_icon                else:                    # PIL不可用,提示用户                    messagebox.showwarning('提示',                                           '选择的是非ICO格式图标,但PIL库未安装。\n图标将不会被应用到打包结果中。')        return PackConfig(            python_path=python_path,            main_file=self.main_file_var.get(),            output_name=self.output_name_var.get(),            icon_path=icon_path,            output_path=self.output_path_var.get(),            packer=self.packer_var.get(),            one_file=self.one_file_var.get(),            no_console=self.no_console_var.get(),            speed_mode=self.speed_mode_var.get(),            enable_upx=self.enable_upx_var.get() and self.upx_path is not None,            upx_path=self.upx_path or "",            add_folders=list(self.folder_listbox.get(0, tk.END)),        )    def _start_pack(self):        config = self._get_config_from_ui()        if not config.main_file:            messagebox.showerror('错误', '请选择主Python文件')            return        # 设置打包状态        self.is_packing = True        self._update_btn_state()        # 重置进度弹窗        self.result_container.pack_forget()        self.popup_log.config(state=tk.NORMAL)        self.popup_log.delete(1.0, tk.END)        self.popup_log.config(state=tk.DISABLED)        self.progress_var.set(0)        self.popup_percent.config(text="0%")        # 显示进度弹窗        self._show_progress_popup()        self.footer.config(text='🔄 打包中...')        # 选择打包器        if config.packer == 'nuitka':            self.packer = NuitkaPacker(config, self._log_callback,                                       lambda v: self.root.after(0, lambda: self._update_progress_ui(v)),                                       lambda s: self.root.after(0, lambda: self._update_status_ui(s)))        else:            self.packer = PyInstallerPacker(config, self._log_callback,                                            lambda v: self.root.after(0, lambda: self._update_progress_ui(v)),                                            lambda s: self.root.after(0, lambda: self._update_status_ui(s)))        def pack_thread():            try:                success = self.packer.pack()                self.root.after(0, lambda: self.progress_var.set(100))                self.root.after(0, self._draw_popup_progress)                if success:                    main_path = Path(config.main_file)                    output_dir = Path(config.output_path or main_path.parent) / 'dist'                    output_name = config.output_name or main_path.stem                    output_file = output_dir / f"{output_name}.exe"                    self.root.after(0, lambda: self.footer.config(text='✅ 打包完成'))                    self.root.after(0, lambda: self._show_result_in_popup(                        True, "打包成功!", f"输出: {output_file}"))                else:                    self.root.after(0, lambda: self.footer.config(text='❌ 打包失败'))                    self.root.after(0, lambda: self._show_result_in_popup(                        False, "打包失败", "请查看日志了解详情"))            except Exception as e:                self.root.after(0, lambda: self.footer.config(text='❌ 打包失败'))                self.root.after(0, lambda: self._show_result_in_popup(                    False, "打包失败", str(e)))            finally:                self.is_packing = False                self.root.after(0, self._update_btn_state)                self.packer = None                # 清理临时图标文件                if self.converted_icon_path and os.path.exists(self.converted_icon_path):                    try:                        os.remove(self.converted_icon_path)                    except BaseException:                        pass                    self.converted_icon_path = None        threading.Thread(target=pack_thread, daemon=True).start()    def _cancel_pack(self):        if self.packer:            self.packer.cancel()            self.is_packing = False            self._update_btn_state()            self._show_result_in_popup(False, "已取消", "打包已被用户取消")            self.footer.config(text='⏹️ 已取消')    def run(self):        self.root.mainloop()if __name__ == '__main__':    app = PackagerGUI()    app.run()
给粉丝朋友们的一封信:
最近开了流量主功能,就是发布的内容底下会有图片形式的广告,希望大家点击进去看看,(不想看,点击进去出来就行)就是我有钱赚了,虽然就十几块,几十块,但是可能够我租用服务器和域名为你们服务了,反正网站一直免费那也得需要点钱,可能对有些人来说不多,但是一直自己花钱买那些东西,一直免费还是会有点难处,也希望你们的支持,我这不是要把网站改成付费的意思,就是希望你们帮我挣点零钱租用所需东西为你们继续服务,谢谢大家!

记得给亲朋好友推荐一下此公众号,让更多的人享受免费,开源,破解资源!

最新文章

随机文章