吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1128|回复: 13
上一主题 下一主题
收起左侧

[Python 原创] 跨平台打包工具PyPackTool_Qt6

[复制链接]
跳转到指定楼层
楼主
wcj6376 发表于 2026-8-6 15:45 回帖奖励
上次因为关于里面有QQ号码,一不小心被删除了,这次进行了改进优化。实测对几个含大型依赖库的py项目一次性即可自动打包成功。
• 支持在线获取github源码
• 增加打包进度彩色显示
• 增加语法检查与修复
• 增加在数据文件处运行py功能
• 增加大小预估
🔧 优化改进
-----------
• 优化为异步秒启动
• 完善约九种打包器

• 完善 PyQt6 完整界面
源码地址:https://github.com/wcj6376/PyPackTool/blob/main/PyPackTool.py
通过网盘分享的文件:PyPackTool_Qt6.exe
链接: https://pan.baidu.com/s/1MmZcqF6gL6LhcMbmapqiiQ 提取码: 52pj

qt6.jpg (88.99 KB, 下载次数: 1)

qt6.jpg

免费评分

参与人数 4吾爱币 +7 热心值 +4 收起 理由
beizi1754 + 2 + 1 我很赞同!
helian147 + 1 + 1 谢谢@Thanks!
一丝风 + 1 + 1 我很赞同!
zhanglei1371 + 3 + 1 目前最强py打包工具

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| wcj6376 发表于 2026-8-6 15:47 |楼主
本帖最后由 wcj6376 于 2026-8-11 09:21 编辑

源码很长此为第一部分

!/usr/bin/env python3

-- coding: utf-8 --

"""Python脚本打包工具 - PyQt6完整版 """
import sys, json, time, ast, shutil, subprocess, threading, multiprocessing, io
import tempfile, webbrowser, fnmatch, glob, ctypes, platform as sys_platform
import urllib.request, zipfile, traceback
import textwrap
from pathlib import Path
from datetime import datetime
from collections import Counter
import os, re, stat,unicodedata,textwrap,difflib,io,functools,psutil,datetime
from contextlib import redirect_stdout, redirect_stderr
from PyQt6.QtCore import
from PyQt6.QtGui import

from PyQt6.QtWidgets import *
from PIL import Image
try:
from PyQt6.QtMultimedia import QMediaPlayer
from PyQt6.QtMultimediaWidgets import QVideoWidget
MEDIA_AVAILABLE = True
except (ImportError, AttributeError):
MEDIA_AVAILABLE = False
VERSION = "8.1.0"
BUILD_DATE = datetime.datetime.now().strftime("%Y-%m-%d")
AUTHOR = "wcj6376"

==================== 镜像源 ====================

MIRRORS = [
"https://pypi.tuna.tsinghua.edu.cn/simple",
"https://mirrors.aliyun.com/pypi/simple/",
"https://pypi.mirrors.ustc.edu.cn/simple/"
]
MIRROR = MIRRORS[0]

==================== 常量定义 ====================

STANDARD_LIBS = frozenset({
'abc','argparse','array','ast','asyncio','atexit','base64','bdb','binascii',
'bisect','builtins','bz2','calendar','cgi','cgitb','chunk','cmath','cmd',
'code','codecs','codeop','collections','colorsys','compileall','concurrent',
'configparser','contextlib','contextvars','copy','copyreg','csv','ctypes',
'dataclasses','datetime','dbm','decimal','difflib','dis','email','encodings',
'enum','faulthandler','fnmatch','fractions','ftplib','functools','fcntl','gc',
'getopt','getpass','gettext','glob','graphlib','gzip','hashlib','heapq',
'hmac','html','http','idlelib','imaplib','imghdr','importlib','inspect',
'io','ipaddress','itertools','json','keyword','linecache','locale','logging',
'lzma','mailbox','mailcap','marshal','math','mimetypes','mmap','modulefinder','msvcrt',
'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','types','typing',
'unicodedata','unittest','urllib','uu','uuid','venv','warnings','wave',
'weakref','webbrowser','winreg','wsgiref','xml','xmlrpc','zipapp','zipfile',
'zipimport','zlib','_thread','future','zoneinfo','tomllib','typing_extensions'
})
MODULE_TO_PACKAGE = {
'cv2':'opencv-python','PIL':'Pillow','skimage':'scikit-image','sklearn':'scikit-learn',
'bs4':'beautifulsoup4','yaml':'PyYAML','Image':'Pillow','ImageDraw':'Pillow','docx':'python-docx',
'pyautogui':'PyAutoGUI','wx':'wxPython','qtpy':'QtPy','PySide2':'PySide2','clr': 'pythonnet',
'PySide6':'PySide6','PyQt5':'PyQt5','PyQt6':'PyQt6','dateutil':'python-dateutil',
'dotenv':'python-dotenv','jwt':'PyJWT','lxml':'lxml','OpenGL':'PyOpenGL',
'redis':'redis','requests':'requests','selenium':'selenium','sqlalchemy':'SQLAlchemy',
'matplotlib':'matplotlib','numpy':'numpy','pandas':'pandas','scipy':'scipy',
'torch':'torch','tensorflow':'tensorflow','flask':'Flask','django':'Django',
'fastapi':'fastapi','tornado':'tornado','aiohttp':'aiohttp','grpc':'grpcio',
'protobuf':'protobuf','pydantic':'pydantic','typer':'typer','rich':'rich',
'click':'click','jinja2':'Jinja2','markupsafe':'MarkupSafe','werkzeug':'Werkzeug',
'itsdangerous':'itsdangerous','LibreHardwareMonitor': 'PyLibreHardwareMonitor',
'win32api':'pywin32','win32con':'pywin32','win32gui':'pywin32','win32ui':'pywin32',
'win32file':'pywin32','win32process':'pywin32','win32security':'pywin32',
'win32service':'pywin32','win32net':'pywin32','win32event':'pywin32',
'win32pipe':'pywin32','win32clipboard':'pywin32','win32console':'pywin32',
'win32profile':'pywin32','win32cred':'pywin32','win32crypt':'pywin32',
'win32job':'pywin32','win32ras':'pywin32','win32timezone':'pywin32',
'win32wnet':'pywin32','win32com':'pywin32','win32com.client':'pywin32',
'win32com.server':'pywin32','pywin32':'pywin32','dde':'pywin32','odbc':'pywin32',
'win32help':'pywin32','win32inet':'pywin32','win32mail':'pywin32',
'win32mapi':'pywin32','win32pdh':'pywin32','win32print':'pywin32',
'win32trace':'pywin32','win32transaction':'pywin32','win32evtlog':'pywin32',
'win32perf':'pywin32','win32ts':'pywin32','win32usb':'pywin32','win32verstamp':'pywin32'
}
EXCLUDE_PACKAGES = frozenset({
'_pytest','astroid','asttokens','autopep8','backcall','black','build',
'charset_normalizer','coverage','Cython','cython','debugpy','decorator','distribute',
'executing','fancycompleter','flake8','ipykernel','ipython','ipywidgets','isort',
'jedi','jupyter','jupyter_client','jupyter_core','jupyterlab','matplotlib-inline',
'mccabe','mock','module','mypy','mypy_extensions','mypyc','nbconvert','nbformat',
'nose','notebook','packaging','pathspec','pdbpp','pep517','pip','pkg_resources',
'platformdirs','pluggy','prompt_toolkit','ptpython','pure_eval','py','pycodestyle',
'pyflakes','pygments','pyi_hooks','pyi_hooks_contrib','pyinstaller',
'pyinstaller-hooks-contrib','pylint','pyproject_hooks','pywin32_ctypes',
'pytest','pywin32','qtconsole','stack_data','test','tests','tox','traitlets',
'typed_ast','typeshed_client','unittest2','venv','virtualenv','wcwidth','wheel',
'wmctrl','win32','win32con','yaml','pyyaml'
})
BASELINE_EXCLUDE = frozenset({

--- 测试框架 ---

'pytest', '_pytest', 'py', 'nose', 'nose2', 'mock', 'unittest2', 'tox',
'coverage', 'hypothesis', 'pytest_cov', 'pytest_xdist',
# --- 代码质量 / 静态检查 ---
'pylint', 'astroid', 'flake8', 'pycodestyle', 'pyflakes', 'mccabe',
'black', 'isort', 'autopep8', 'yapf', 'bandit', 'pydocstyle',
'mypy', 'mypy_extensions', 'mypyc', 'typed_ast', 'pathspec',
# --- 交互式解释器 / Notebook(PyInstaller 分析重灾区)---
'IPython', 'ipython', 'ipykernel', 'ipywidgets', 'jupyter',
'jupyter_client', 'jupyter_core', 'jupyterlab', 'jupyterlab_server',
'notebook', 'nbconvert', 'nbformat', 'nbclient', 'qtconsole',
'traitlets', 'jedi', 'parso', 'prompt_toolkit', 'pygments', 'wcwidth',
'backcall', 'pickleshare', 'matplotlib_inline', 'stack_data',
'executing', 'asttokens', 'pure_eval', 'debugpy', 'pydevd',
# --- 打包 / 构建工具链 ---
'PyInstaller', 'pyinstaller', 'cx_Freeze', 'nuitka', 'py2exe', 'py2app',
'pkg_resources', 'pip', 'wheel', 'build', 'pep517',
'pyproject_hooks', 'virtualenv', 'pipenv', 'poetry', 'twine',
'setuptools_scm', 'distutils_hack', '_distutils_hack',
# --- 文档工具 ---
'sphinx', 'docutils', 'alabaster', 'snowballstemmer', 'imagesize',
'sphinxcontrib',
# --- 重型科学计算(未被实际 import 时排除)---
'scipy', 'matplotlib', 'sympy', 'numba', 'llvmlite', 'torch',
'tensorflow', 'keras', 'sklearn', 'scikit_learn', 'seaborn', 'plotly',
'bokeh', 'statsmodels', 'h5py', 'tables', 'numexpr', 'bottleneck',
'pyarrow', 'fastparquet', 'dask', 'xarray', 'sqlalchemy',
# --- GUI 框架(互斥,未被 import 时排除,否则体积暴涨)---
'PyQt5', 'PyQt6', 'PySide2', 'PySide6', 'qtpy', 'wx', 'kivy', 'pygame',
'PyQt5_sip', 'PyQt6_sip', 'shiboken2', 'shiboken6',
# --- 网络 / Web 框架 ---
'selenium', 'scrapy', 'django', 'flask', 'tornado', 'aiohttp', 'fastapi',
# --- 标准库中的冗余部分 ---
'lib2to3', 'pydoc_data', 'idlelib', 'turtledemo', 'tkinter.test',
'test', 'tests', 'distutils.command.bdist_msi',
})

RUNTIME_SAFE_KEEP = frozenset({
'six', 'typing_extensions', 'typing-extensions', 'packaging',
'zipp', 'importlib_metadata', 'importlib-metadata',
'importlib_resources', 'importlib-resources',
'pytz', 'tzdata', 'tzlocal', 'python_dateutil', 'python-dateutil',
'dateutil', 'certifi', 'idna', 'urllib3',
'charset_normalizer', 'charset-normalizer',
'et_xmlfile', 'et-xmlfile', 'attrs', 'attr',
'pyparsing', 'setuptools', 'setuptools_rust', 'cffi', 'pycparser',
})
DEPENDENCY_MAP = {
'requests': ['urllib3', 'certifi', 'idna', 'charset_normalizer'],
'openpyxl': ['et_xmlfile'],
'pandas': ['numpy', 'pytz', 'python-dateutil', 'six', 'tzdata'],
'python-dateutil': ['six'],
'dateutil': ['six'],
'docx': ['lxml', 'typing_extensions', 'python-docx'],
'python-docx': ['lxml', 'typing_extensions', 'python-docx'],
'matplotlib': ['numpy', 'pillow', 'kiwisolver', 'pyparsing'],
'scikit-learn': ['numpy', 'scipy', 'joblib', 'threadpoolctl'],
'sklearn': ['numpy', 'scipy', 'joblib', 'threadpoolctl'],
'Image': ['pillow'],
'opencv': ['opencv-python','numpy'],
'cv2': ['opencv-python','numpy'],
'GPUtil': ['setuptools', 'nvidia-ml-py'],
'win32api': ['pywin32'],
'win32con': ['pywin32'],
'win32gui': ['pywin32'],
'win32com': ['pywin32'],
}
FILTER_MODULES = frozenset({
'PyInstaller', 'module', 'yaml', 'pyyaml',
'unittest', 'test', 'tests', 'pythonwin',
'pytest', 'nose', 'mock', 'tox', 'coverage',
'pylint', 'flake8', 'black', 'mypy', 'isort', 'autopep8',
'jupyter', 'ipython', 'notebook', 'ipykernel',
'build', 'packaging', 'pep517', 'pyproject_hooks',
'pyinstaller', 'pyi_hooks_contrib', 'pyi_hooks','pyinstaller-hooks-contrib',
'pywin32_ctypes', 'pip', 'wheel', 'pkg_resources', 'distribute',
})
NEVER_PACK = frozenset({
'pyinstaller', 'pyi_hooks_contrib', 'pyi_hooks','pyinstaller-hooks-contrib',
'pywin32_ctypes', 'pip', 'wheel', 'pkg_resources', 'distribute',
})
def get_startupinfo():
"""获取启动信息,隐藏控制台窗口"""
if sys.platform == 'win32':
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.wShowWindow = subprocess.SW_HIDE
return si
return None

def get_exe_directory():
if getattr(sys, 'frozen', False) or os.environ.get('NUITKA_ONEFILE_PARENT') is not None:
return os.path.dirname(os.path.abspath(sys.argv[0]))
return os.path.dirname(os.path.abspath(file))
APP_BASE_PATH = get_exe_directory()

def _get_real_python():
"""获取真实的Python解释器路径"""
import os
import subprocess
import sys

源码模式

if not getattr(sys, 'frozen', False):
    return sys.executable

def is_same_file(path1, path2):
    """判断两个路径是否指向同一文件(跨平台)"""
    try:
        return os.path.samefile(path1, path2)
    except:
        return os.path.abspath(path1) == os.path.abspath(path2)

def is_valid_python(path):
    """检查是否是有效的Python且不是当前exe"""
    if not path or not os.path.exists(path):
        return False
    if is_same_file(path, sys.executable):
        return False
    try:
        result = subprocess.run(
            [path, '--version'],
            capture_output=True, text=True, timeout=2,
            startupinfo=get_startupinfo()
        )
        return result.returncode == 0 and ('Python' in result.stdout or 'Python' in result.stderr)
    except:
        return False
    if sys.platform == 'win32':
        try:
            result = self._run_hidden(['py', '-c', 'import sys; print(sys.executable)'],
                                      capture_output=True, text=True, timeout=5,
                                      startupinfo=get_startupinfo())
            if result.returncode == 0 and result.stdout.strip():
                py_path = result.stdout.strip()
                if is_valid_python(py_path):
                    return py_path
        except Exception:
            pass
    for py_name in ['python3', 'python']:
        try:
            result = self._run_hidden([py_name, '-c', 'import sys; print(sys.executable)'],
                                      capture_output=True, text=True, timeout=5,
                                      startupinfo=get_startupinfo())
            if result.returncode == 0 and result.stdout.strip():
                py_path = result.stdout.strip()
                if is_valid_python(py_path):
                    return py_path
        except Exception:
            pass
    if sys.platform == 'win32':
        username = os.environ.get('USERNAME', '')
        default_paths = [
            r'C:\Python312\python.exe',
            r'C:\Python311\python.exe',
            r'C:\Python310\python.exe',
            r'C:\Python39\python.exe',
            rf'C:\Users\{username}\AppData\Local\Programs\Python\Python312\python.exe',
            rf'C:\Users\{username}\AppData\Local\Programs\Python\Python311\python.exe',
            rf'C:\Users\{username}\AppData\Local\Programs\Python\Python310\python.exe',
        ]
    elif sys.platform == 'darwin':
        default_paths = [
            '/usr/local/bin/python3',
            '/usr/bin/python3',
            '/opt/homebrew/bin/python3',
        ]
    else:
        default_paths = [
            '/usr/bin/python3',
            '/usr/local/bin/python3',
            '/usr/bin/python',
        ]
    for p in default_paths:
        if is_valid_python(p):
            return p
    if hasattr(sys, '_MEIPASS'):
        meipass = sys._MEIPASS
        search_dir = os.path.dirname(meipass)
        for root, dirs, files in os.walk(search_dir):
            for f in files:
                if f.lower() in ('python.exe', 'python3.exe', 'python'):
                    p = os.path.join(root, f)
                    if is_valid_python(p):
                        return p
            if len(root) > len(search_dir) + 100:
                break
    import shutil
    for cmd in ['python3', 'python']:
        p = shutil.which(cmd)
        if p and is_valid_python(p):
            return p
    return None

def get_short_path(path):
"""获取 Windows 短路径(8.3格式),仅当路径包含空格时使用"""
if sys.platform != 'win32' or not path:
return path
if ' ' not in path:
return path
try:
import ctypes
GetShortPathName = ctypes.windll.kernel32.GetShortPathNameW
buffer_len = GetShortPathName(path, None, 0)
if buffer_len == 0:
return path
buffer = ctypes.create_unicode_buffer(buffer_len)
GetShortPathName(path, buffer, buffer_len)
return buffer.value if buffer.value else path
except Exception:
return path

def get_cache_dir():
"""获取可写的缓存目录"""
exe_dir = get_exe_directory()
if os.access(exe_dir, os.W_OK):
return exe_dir
import tempfile
fallback = os.path.join(tempfile.gettempdir(), "PyPackTool")
os.makedirs(fallback, exist_ok=True)
return fallback

def load_dep_map():
"""加载依赖映射表(独立配置文件,不影响原有缓存)"""
global DEPENDENCY_MAP
if os.path.exists(DEP_MAP_FILE):
try:
import re
import ast
with open(DEP_MAP_FILE, 'r', encoding='utf-8-sig') as f:
content = f.read()
new_map = {}
for line in content.split('\n'):
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, val = line.split('=', 1)
key = key.strip()
val = val.strip()
try:
deps = ast.literal_eval(val)
if isinstance(deps, list):
new_map[key] = deps
except:

降级解析:提取所有引号内的内容

                    deps = re.findall(r'"([^"]+)"', val)
                    if deps:
                        new_map[key] = deps
        if new_map:
            DEPENDENCY_MAP = new_map
            return
    except Exception as e:
        pass
# ===== 不存在则用源码中的 DEPENDENCY_MAP 创建 =====
try:
    with open(DEP_MAP_FILE, 'w', encoding='utf-8-sig') as f:
        for mod, deps in DEPENDENCY_MAP.items():
            deps_str = ', '.join([f'"{d}"' for d in deps])
            f.write(f'{mod} = [{deps_str}]\n')
except Exception as e:
    pass

def load_cache():
"""加载缓存 - 优先内存"""
global _memory_cache
if _memory_cache is not None:
return _memory_cache
try:
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, 'r', encoding='utf-8-sig') as f:
_memory_cache = json.load(f)
return _memory_cache
except:
pass
_memory_cache = {}
return _memory_cache

def save_cache(cache):
"""保存缓存到文件并更新内存"""
global _memory_cache
_memory_cache = cache
try:
with open(CACHE_FILE, 'w', encoding='utf-8-sig') as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
except:
pass

def now_str():
"""返回当前时间的字符串格式 HH:MM:SS.mmm"""
import datetime
now = datetime.datetime.now()
return now.strftime("%H:%M:%S") + f".{now.microsecond//1000:03d}"

def pip_install(python_exe, package, env=None, timeout=300, quiet=False):
"""
安装包,失败时自动切换镜像源
返回: (success, result)
"""
global MIRROR
if env is None:
env = {'PATH': os.environ.get('PATH', '')}
if sys.platform == 'win32':
env['SYSTEMROOT'] = os.environ.get('SYSTEMROOT', '')
cmd = [python_exe, '-m', 'pip', 'install', package]
if quiet:
cmd.append('-q')
cmd.append('--no-warn-script-location')
for mirror in MIRRORS:
try:
cmd_with_mirror = cmd + ['-i', mirror]
result = subprocess.run(
cmd_with_mirror,
capture_output=True, text=True,
env=env,
timeout=timeout
)
if result.returncode == 0:
MIRROR = mirror

===== 新增:安装 pywin32 后自动激活 =====

            if package.lower() == 'pywin32' and sys.platform == 'win32':
                try:
                    subprocess.run(
                        [python_exe, '-c', 'import pywin32_postinstall; pywin32_postinstall.install()'],
                        capture_output=True, text=True,
                        env=env,
                        timeout=30
                    )
                except Exception as e:
                    pass  # 激活失败不影响安装成功返回
            return True, result
    except subprocess.TimeoutExpired:
        continue
    except Exception:
        continue
return False, None

DEP_MAP_FILE = os.path.join(get_exe_directory(), ".dep_map.toml")

===== 全局内存缓存 =====

_memory_cache = None

class PythonInstallWorker(QThread):
"""后台静默安装 Python """
finished_signal = pyqtSignal(bool, str)
def init(self):
super().init()
self._cancel = False
self._progress = 0

def cancel(self):
    self._cancel = True

def run(self):
    import urllib.request
    import ssl
    import tempfile
    import os
    import subprocess
    import time
    import sys
    try:
        version = "3.12.10"
        temp_dir = tempfile.gettempdir()
        has_python, version_str = check_python_installed()
        if has_python:
            self.finished_signal.emit(True, f"Python {version_str} 已安装")
            return
        if sys.platform == 'win32':
            filename = f"python-{version}-amd64.exe"
            mirrors = [
                f"https://www.python.org/ftp/python/{version}/python-{version}-amd64.exe",
                f"https://mirrors.tuna.tsinghua.edu.cn/python/{version}/python-{version}-amd64.exe",
                f"https://mirrors.aliyun.com/python/{version}/python-{version}-amd64.exe",
                f"https://mirrors.ustc.edu.cn/python/{version}/python-{version}-amd64.exe",
            ]
        elif sys.platform == 'darwin':
            filename = f"python-{version}-macos11.pkg"
            mirrors = [
                f"https://www.python.org/ftp/python/{version}/python-{version}-macos11.pkg",
                f"https://mirrors.tuna.tsinghua.edu.cn/python/{version}/python-{version}-macos11.pkg",
            ]
        else:
            self.finished_signal.emit(False, "Linux请使用包管理器安装Python")
            return
        installer_path = os.path.join(temp_dir, filename)
        ssl_context = ssl.create_default_context()
        ssl_context.check_hostname = False
        ssl_context.verify_mode = ssl.CERT_NONE
        downloaded = False
        last_error = ""
        for i, url in enumerate(mirrors):
            if self._cancel:
                self.finished_signal.emit(False, "已取消")
                return
            try:
                self.finished_signal.emit(False, f"下载中... 尝试镜像 {i+1}/{len(mirrors)}")
                req = urllib.request.Request(url, headers={
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
                })
                with urllib.request.urlopen(req, context=ssl_context, timeout=60) as response:
                    total_size = int(response.headers.get('content-length', 0))
                    downloaded_size = 0
                    if os.path.exists(installer_path):
                        if total_size > 0 and os.path.getsize(installer_path) == total_size:
                            downloaded = True
                            break
                    with open(installer_path, 'wb') as f:
                        while not self._cancel:
                            chunk = response.read(8192)
                            if not chunk:
                                break
                            f.write(chunk)
                            downloaded_size += len(chunk)
                            if total_size > 0:
                                progress = int(downloaded_size * 100 / total_size)
                                if progress != self._progress:
                                    self._progress = progress
                                    self.finished_signal.emit(False, f"下载中... {progress}%")
                if os.path.exists(installer_path):
                    file_size = os.path.getsize(installer_path)
                    if file_size > 5 * 1024 * 1024:  
                        downloaded = True
                        self.finished_signal.emit(False, f"下载完成 ({file_size // 1024 // 1024}MB)")
                        break
                    else:
                        os.remove(installer_path)
                        last_error = f"文件太小 ({file_size} bytes)"
            except Exception as e:
                last_error = str(e)
                continue
        if self._cancel:
            self.finished_signal.emit(False, "已取消")
            return
        if not downloaded:
            self.finished_signal.emit(False, f"下载失败: {last_error}")
            return
        self.finished_signal.emit(False, "安装中... 请稍候")
        if sys.platform == 'win32':
            cmd = [
                installer_path,
                '/quiet',
                'InstallAllUsers=1',
                'PrependPath=1',
                'Include_doc=0',
                'Include_tcltk=0',
                'Include_test=0',
                'Include_tools=0',
                'Include_pip=1',
                'Include_setuptools=1',
                'Include_symbols=0',
                'Include_debug=0',
                'InstallLauncherAllUsers=1',
            ]
        elif sys.platform == 'darwin':
            cmd = ['sudo', 'installer', '-pkg', installer_path, '-target', '/']
        else:
            self.finished_signal.emit(False, "不支持的操作系统")
            return
        process = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
        )
        timeout_count = 0
        while process.poll() is None:
            if self._cancel:
                process.terminate()
                self.finished_signal.emit(False, "已取消")
                return
            timeout_count += 1
            if timeout_count > 600:  
                process.terminate()
                self.finished_signal.emit(False, "安装超时")
                return
            if timeout_count % 30 == 0:
                progress = min(95, 50 + timeout_count // 6)
                self.finished_signal.emit(False, f"安装中... {progress}%")
            self.msleep(500)
        try:
            os.remove(installer_path)
        except:
            pass
        if process.returncode == 0:
            self.finished_signal.emit(False, "验证安装...")
            time.sleep(3)  
            for attempt in range(5):
                has_python, version_str = check_python_installed()
                if has_python:
                    self.finished_signal.emit(True, f"Python {version_str} 安装成功!")
                    return
                time.sleep(2)
            self.finished_signal.emit(False, "安装完成但验证失败,请手动安装")
        else:
            self.finished_signal.emit(False, f"安装失败 (返回码: {process.returncode})")
    except Exception as e:
        import traceback
        self.finished_signal.emit(False, f"错误: {str(e)}")

def check_python_installed():
"""检测系统是否安装了 Python"""
import subprocess
import sys
import os
import shutil
import glob
IS_FROZEN = getattr(sys, 'frozen', False)

def is_valid_system_python(path):
    if not path or not os.path.exists(path):
        return False
    if IS_FROZEN:
        try:
            if os.path.samefile(path, sys.executable):
                return False
        except:
            if path.lower() == sys.executable.lower():
                return False
    path_lower = path.lower()
    if '_mei' in path_lower or 'onefile_' in path_lower or 'nuitka' in path_lower:
        return False
    try:
        if sys.platform == 'win32':
            result = subprocess.run(
                [path, '--version'],
                capture_output=True, text=True, timeout=3,
                creationflags=subprocess.CREATE_NO_WINDOW
            )
        else:
            result = subprocess.run(
                [path, '--version'],
                capture_output=True, text=True, timeout=3
            )
        if result.returncode == 0:
            output = result.stdout + result.stderr
            return 'Python' in output
    except:
        pass
    return False

def get_python_version(path):
    """获取Python版本"""
    try:
        if sys.platform == 'win32':
            result = subprocess.run(
                [path, '--version'],
                capture_output=True, text=True, timeout=3,
                creationflags=subprocess.CREATE_NO_WINDOW
            )
        else:
            result = subprocess.run(
                [path, '--version'],
                capture_output=True, text=True, timeout=3
            )
        if result.returncode == 0:
            output = result.stdout.strip() or result.stderr.strip()
            return output
    except:
        pass
    return None
if sys.platform == 'win32':
    try:
        result = subprocess.run(
            ['py', '--version'],
            capture_output=True, text=True, timeout=3,
            creationflags=subprocess.CREATE_NO_WINDOW
        )
        if result.returncode == 0:
            version = result.stdout.strip() or result.stderr.strip()
            return True, version
    except:
        pass
    try:
        result = subprocess.run(
            ['py', '-c', 'import sys; print(sys.executable)'],
            capture_output=True, text=True, timeout=3,
            creationflags=subprocess.CREATE_NO_WINDOW
        )
        if result.returncode == 0:
            py_path = result.stdout.strip()
            if py_path and is_valid_system_python(py_path):
                version = get_python_version(py_path)
                if version:
                    return True, version
    except:
        pass
    try:
        result = subprocess.run(
            ['where', 'python'],
            capture_output=True, text=True, timeout=3,
            creationflags=subprocess.CREATE_NO_WINDOW
        )
        if result.returncode == 0:
            for line in result.stdout.strip().split('\n'):
                py_path = line.strip()
                if is_valid_system_python(py_path):
                    version = get_python_version(py_path)
                    if version:
                        return True, version
    except:
        pass
    try:
        import winreg
        for root in [winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE]:
            try:
                key = winreg.OpenKey(root, r'Software\Python\PythonCore')
                i = 0
                while True:
                    try:
                        version_key = winreg.EnumKey(key, i)
                        if version_key.startswith('3.'):
                            try:
                                install_key = winreg.OpenKey(key, fr'{version_key}\InstallPath')
                                install_path, _ = winreg.QueryValueEx(install_key, '')
                                if install_path:
                                    py_path = os.path.join(install_path, 'python.exe')
                                    if os.path.exists(py_path) and is_valid_system_python(py_path):
                                        version = get_python_version(py_path)
                                        if version:
                                            return True, version
                            except:
                                pass
                        i += 1
                    except WindowsError:
                        break
            except:
                pass
    except:
        pass
for cmd in ['python3', 'python']:
    py_path = shutil.which(cmd)
    if py_path and is_valid_system_python(py_path):
        version = get_python_version(py_path)
        if version:
            return True, version
if sys.platform == 'win32':
    username = os.environ.get('USERNAME', '')
    search_patterns = [
        r'C:\Python3*',
        r'C:\Python3*\python.exe',
        rf'C:\Users\{username}\AppData\Local\Programs\Python\Python3*\python.exe',
        r'C:\Program Files\Python3*\python.exe',
        r'C:\Program Files (x86)\Python3*\python.exe',
    ]
    for pattern in search_patterns:
        for path in glob.glob(pattern):
            if os.path.isfile(path) and path.endswith('python.exe'):
                if is_valid_system_python(path):
                    version = get_python_version(path)
                    if version:
                        return True, version
            elif os.path.isdir(path):
                exe_path = os.path.join(path, 'python.exe')
                if os.path.exists(exe_path) and is_valid_system_python(exe_path):
                    version = get_python_version(exe_path)
                    if version:
                        return True, version
elif sys.platform == 'darwin':
    paths_to_check = [
        '/usr/local/bin/python3',
        '/usr/bin/python3',
        '/opt/homebrew/bin/python3',
        '/Library/Frameworks/Python.framework/Versions/3.*/bin/python3',
    ]
    for pattern in paths_to_check:
        for path in glob.glob(pattern):
            if os.path.exists(path) and is_valid_system_python(path):
                version = get_python_version(path)
                if version:
                    return True, version
else:
    paths_to_check = [
        '/usr/bin/python3',
        '/usr/local/bin/python3',
        '/usr/bin/python',
        '/opt/python3/bin/python3',
    ]
    for path in paths_to_check:
        if os.path.exists(path) and is_valid_system_python(path):
            version = get_python_version(path)
            if version:
                return True, version
return False, None

def ensure_python_on_startup():
"""启动时检测 Python"""
import sys
import os
if not getattr(sys, 'frozen', False):
return True
has_python, version = check_python_installed()
if has_python:
print(f"[Main] 检测到系统Python: {version}")
return True
print("[Main] 未检测到系统Python,后台静默安装...")
if sys.platform == "win32":
try:
import ctypes
if not ctypes.windll.shell32.IsUserAnAdmin():
print("[Main] 警告: 非管理员模式,安装可能失败")
except:
pass
from PyQt6.QtWidgets import QApplication
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
app.setStyle('Fusion')
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QProgressBar, QPushButton
from PyQt6.QtCore import QTimer, Qt
tip_widget = QWidget(None, Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
tip_widget.setWindowTitle("安装 Python")
tip_widget.setFixedSize(380, 140)
tip_widget.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
layout = QVBoxLayout(tip_widget)
layout.setSpacing(8)
status_label = QLabel("⏳ 正在后台静默安装 Python 3.12.10...")
status_label.setStyleSheet("font-size: 13px; font-weight: bold;")
status_label.setWordWrap(True)
layout.addWidget(status_label)
progress_bar = QProgressBar()
progress_bar.setRange(0, 100)
progress_bar.setValue(0)
progress_bar.setTextVisible(True)
layout.addWidget(progress_bar)
btn_layout = QHBoxLayout()
btn_layout.addStretch()
hide_btn = QPushButton("最小化到托盘")
hide_btn.setFixedWidth(100)
btn_layout.addWidget(hide_btn)
cancel_btn = QPushButton("取消")
cancel_btn.setFixedWidth(60)
btn_layout.addWidget(cancel_btn)
layout.addLayout(btn_layout)
tip_widget.show()
worker = PythonInstallWorker()
install_success = False

def on_progress(success, msg):
    nonlocal install_success
    if success:
        install_success = True
        status_label.setText("✅ " + msg)
        progress_bar.setValue(100)
        hide_btn.setEnabled(False)
        cancel_btn.setEnabled(False)
        cancel_btn.setText("重启")
        cancel_btn.clicked.connect(lambda: restart_app())
        QTimer.singleShot(3000, tip_widget.close)
    else:
        if "下载" in msg or "安装" in msg:
            status_label.setText("⏳ " + msg)
            import re
            match = re.search(r'(\d+)%', msg)
            if match:
                progress_bar.setValue(int(match.group(1)))
        else:
            status_label.setText("⏳ " + msg)
            cancel_btn.setText("关闭")
            cancel_btn.clicked.connect(tip_widget.close)

def on_hide():
    """最小化到托盘或隐藏窗口"""
    tip_widget.hide()
    print("[Main] Python安装已在后台继续...")

def on_cancel():
    """取消安装"""
    if worker.isRunning():
        worker.cancel()
        worker.wait()
    tip_widget.close()

def restart_app():
    """重启程序"""
    import subprocess
    import sys
    tip_widget.close()
    subprocess.Popen([sys.executable] + sys.argv)
    sys.exit(0)
worker.finished_signal.connect(on_progress)
hide_btn.clicked.connect(on_hide)
cancel_btn.clicked.connect(on_cancel)
worker.start()
return True

def patch_subprocess_hide_window():
""" 所有 subprocess 调用默认隐藏 cmd 窗口(Windows)"""
import subprocess
import sys
if sys.platform == "win32":
_orig_run = subprocess.run
_orig_popen = subprocess.Popen
_orig_call = subprocess.call
_orig_check_call = subprocess.check_call
_orig_check_output = subprocess.check_output
CREATE_NO_WINDOW = 0x08000000
def _patched_run(*args, kwargs):
if 'creationflags' not in kwargs:
kwargs['creationflags'] = CREATE_NO_WINDOW
return _orig_run(*args, *kwargs)
def _patched_popen(
args,
kwargs):
if 'creationflags' not in kwargs:
kwargs['creationflags'] = CREATE_NO_WINDOW
return _orig_popen(*args, kwargs)
def _patched_call(*args, *kwargs):
if 'creationflags' not in kwargs:
kwargs['creationflags'] = CREATE_NO_WINDOW
return _orig_call(
args,
kwargs)
def _patched_check_call(*args, kwargs):
if 'creationflags' not in kwargs:
kwargs['creationflags'] = CREATE_NO_WINDOW
return _orig_check_call(*args, *kwargs)
def _patched_check_output(
args,
kwargs):
if 'creationflags' not in kwargs:
kwargs['creationflags'] = CREATE_NO_WINDOW
return _orig_check_output(*args, **kwargs)
subprocess.run = _patched_run
subprocess.Popen = _patched_popen
subprocess.call = _patched_call
subprocess.check_call = _patched_check_call
subprocess.check_output = _patched_check_output

def show_msg(parent, title, text, timeout=0, buttons='ok'):
"""
通用消息框(PyQt6)调用示例:
show_msg(self, "提示", "操作完成")
show_msg(self, "完成", "打包成功!", 3)
if show_msg(self, "确认", "是否继续?", 3, 'yes_no'):
self.do_continue()   # 点击"是" 或 超时 → 执行
else:
print("取消")        # 点击"否" → 跳过
if show_msg(self, "提示", "确定要删除吗?", 5, 'ok_cancel'):
self.do_delete()     # 点击"确定" 或 超时 → 执行
else:
print("取消删除")
show_msg(None, "后台任务", "完成!", 2)
QMessageBox.critical(self, "错误", "打包失败!")
"""
from PyQt6.QtWidgets import QMessageBox
from PyQt6.QtCore import QTimer
btn_map = {
'ok': QMessageBox.StandardButton.Ok,
'ok_cancel': QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
'yes_no': QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
}
msg = QMessageBox(parent)
msg.setWindowTitle(title)
msg.setText(text)
msg.setIcon(QMessageBox.Icon.Information)
msg.setStandardButtons(btn_map.get(buttons, QMessageBox.StandardButton.Ok))
if timeout > 0:
remaining = timeout
msg.setText(f"{text}\n\n⏱️ {remaining}秒后自动...")
timer = QTimer(msg)
def update():
nonlocal remaining
remaining -= 1
if remaining > 0:
msg.setText(f"{text}\n\n⏱️ {remaining}秒后自动...")
else:
timer.stop()
timer.timeout.connect(update)
timer.start(1000)
QTimer.singleShot(timeout * 1000, msg.accept)
result = msg.exec()
if buttons == 'ok':
return True
elif buttons == 'ok_cancel':
return result == QMessageBox.StandardButton.Ok
elif buttons == 'yes_no':
return result == QMessageBox.StandardButton.Yes
return False

class AnalyzeUsedThread(QThread):
"""后台执行 _analyze_used 的纯计算部分(无UI操作)"""
finished = pyqtSignal(list, list, list, bool)  # result, real_imports, extra_deps, uses_tkinter
error = pyqtSignal(str)
def init(self, script_path):
super().init()
self.script_path = script_path

def run(self):
    try:
        with open(self.script_path, 'r', encoding='utf-8-sig', errors='replace') as f:
            source = f.read()
    except Exception as e:
        self.error.emit(str(e))
        self.finished.emit([], [], [], False)
        return
    imports = set()
    uses_tkinter = False
    # ===== 1. AST 解析所有 import =====
    try:
        tree = ast.parse(source)
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    mod = alias.name.split('.')[0]
                    if mod == 'tkinter':
                        uses_tkinter = True
                        imports.add('tk')
                    elif mod == 'tk':
                        imports.add('tk')
                    elif mod not in STANDARD_LIBS:
                        imports.add(mod)
            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    mod = node.module.split('.')[0]
                    if mod == 'tkinter':
                        uses_tkinter = True
                        imports.add('tk')
                    elif mod == 'tk':
                        imports.add('tk')
                    elif mod not in STANDARD_LIBS:
                        imports.add(mod)
    except Exception:
        import re
        for line in source.split('\n'):
            line = line.strip()
            if line.startswith('import ') or line.startswith('from '):
                if 'tkinter' in line:
                    uses_tkinter = True
                    imports.add('tk')
                else:
                    match = re.match(r'(?:from\s+(\S+)\s+import|import\s+(\S+))', line)
                    if match:
                        mod = match.group(1) or match.group(2)
                        if mod:
                            mod = mod.split('.')[0]
                            if mod not in STANDARD_LIBS:
                                imports.add(mod)
    # ===== 2. 过滤(使用全局 FILTER_MODULES) =====
    result = []
    for mod in sorted(imports):
        mod_clean = mod.split('==')[0].split(' ')[0].strip()
        if mod_clean and mod_clean not in FILTER_MODULES:
            if mod_clean not in result:
                result.append(mod_clean)
    # ===== 3. 确保 tk 在结果中 =====
    if uses_tkinter and 'tk' not in result:
        result.append('tk')
    # ===== 4. 自动补充隐式依赖(使用全局 DEPENDENCY_MAP) =====
    real_imports = result.copy()
    extra_deps = set()
    for mod, deps in DEPENDENCY_MAP.items():
        if mod in result:
            for dep in deps:
                if dep not in result:
                    result.append(dep)
                    extra_deps.add(dep)
    self.finished.emit(result, real_imports, list(extra_deps), uses_tkinter)

class PythonHighlighter(QSyntaxHighlighter):
"""Python 语法高亮器"""
def init(self, parent=None):
super().init(parent)
keyword_format = QTextCharFormat()
keyword_format.setForeground(QColor("#569CD6"))
keyword_format.setFontWeight(QFont.Weight.Bold)
keywords = [
'def', 'class', 'import', 'from', 'return', 'if', 'elif', 'else', 'for',
'while', 'try', 'except', 'finally', 'with', 'as', 'pass', 'break',
'continue', 'lambda', 'yield', 'assert', 'raise', 'del', 'global',
'nonlocal', 'True', 'False', 'None', 'and', 'or', 'not', 'is', 'in',
'async', 'await', 'init', 'name', 'main', 'file'
]
self.rules = []
for kw in keywords:
pattern = QRegularExpression(r'\b' + kw + r'\b')
self.rules.append((pattern, keyword_format))
string_format = QTextCharFormat()
string_format.setForeground(QColor("#CE9178"))
self.rules.append((QRegularExpression(r'"[^"\](\.[^"\])"'), string_format))
self.rules.append((QRegularExpression(r"'[^'\]
(\.[^'\])'"), string_format))
comment_format = QTextCharFormat()
comment_format.setForeground(QColor("#6A9955"))
self.rules.append((QRegularExpression(r'#.*'), comment_format))
number_format = QTextCharFormat()
number_format.setForeground(QColor("#B5CEA8"))
self.rules.append((QRegularExpression(r'\b[0-9]+\b'), number_format))

def highlightBlock(self, text):
    for pattern, fmt in self.rules:
        iterator = pattern.globalMatch(text)
        while iterator.hasNext():
            match = iterator.next()
            self.setFormat(match.capturedStart(), match.capturedLength(), fmt)

class CodePreviewDialog(QDialog):
"""代码修复预览对话框 - 左右双栏对比"""
skip_preview = False
def init(self, parent, original_content, new_content, changes, file_path, backup_path):
super().init(parent)
self.setWindowTitle("代码修复预览")
self.setModal(True)
screen = QApplication.primaryScreen()
screen_geometry = screen.availableGeometry()
screen_width = screen_geometry.width()
screen_height = screen_geometry.height()
window_width = int(screen_width 0.9)
window_height = int(screen_height
0.8)
self.setMinimumSize(int(screen_width 0.5), int(screen_height 0.5))
self.resize(window_width, window_height)
self.original_content = original_content
self.new_content = new_content
self.changes = changes
self.file_path = file_path
self.backup_path = backup_path
self.left_modified = False
self.right_modified = False
self._setup_ui()
self._show_diff()
self.setStyleSheet("")

def _setup_ui(self):
    layout = QVBoxLayout(self)
    # 顶部信息
    info = QHBoxLayout()
    info.addWidget(QLabel(f"📄 文件: {os.path.basename(self.file_path)}"))
    info.addStretch()
    info.addWidget(QLabel(f"📝 修改: {len(self.changes)} 处"))
    layout.addLayout(info)
    splitter = QSplitter(Qt.Orientation.Horizontal)
    left_widget = QWidget()
    left_layout = QVBoxLayout(left_widget)
    left_title = QLabel("🔴 原始代码")
    left_title.setStyleSheet("font-weight: bold; color: #c0392b; font-size: 12px;")
    left_layout.addWidget(left_title)
    self.left_edit = QPlainTextEdit()
    self.left_edit.setFont(QFont("Consolas", 11))
    self.left_edit.setStyleSheet("""
        QPlainTextEdit {
            background-color: #fdf2f2;
            color: #1a0000;
            border: 2px solid #e74c3c;
            border-radius: 6px;
            padding: 8px;
            selection-background-color: #ffcccc;
        }
    """)
    self.left_edit.textChanged.connect(lambda: self._on_edit("left"))
    left_layout.addWidget(self.left_edit)
    splitter.addWidget(left_widget)
    right_widget = QWidget()
    right_layout = QVBoxLayout(right_widget)
    right_title = QLabel("🟢 修复后代码")
    right_title.setStyleSheet("font-weight: bold; color: #27ae60; font-size: 12px;")
    right_layout.addWidget(right_title)
    self.right_edit = QPlainTextEdit()
    self.right_edit.setFont(QFont("Consolas", 11))
    self.right_edit.setStyleSheet("""
        QPlainTextEdit {
            background-color: #f0faf0;
            color: #001a00;
            border: 2px solid #2ecc71;
            border-radius: 6px;
            padding: 8px;
            selection-background-color: #a8e6cf;
        }
    """)
    self.right_edit.textChanged.connect(lambda: self._on_edit("right"))
    right_layout.addWidget(self.right_edit)
    splitter.addWidget(right_widget)
    splitter.setSizes([450, 450])
    layout.addWidget(splitter, stretch=1)
    layout.addWidget(QLabel("📋 修改详情:"))
    self.changes_list = QListWidget()
    self.changes_list.setMaximumHeight(120)
    self.changes_list.setStyleSheet("""
        QListWidget {
            background-color: #f8f9fa;
            color: #212529;
            border: 1px solid #dee2e6;
            border-radius: 4px;
            font-size: 11px;
        }
        QListWidget::item {
            padding: 4px 8px;
            border-bottom: 1px solid #f0f0f0;
        }
        QListWidget::item:selected {
            background-color: #007bff;
            color: white;
        }
    """)
    for change in self.changes:
        self.changes_list.addItem(change)
    layout.addWidget(self.changes_list)
    btn_layout = QHBoxLayout()
    btn_layout.setSpacing(8)
    left_btns = QHBoxLayout()
    self.save_left_btn = QPushButton("💾 保存左侧")
    self.save_left_btn.setEnabled(False)
    self.save_left_btn.setStyleSheet("background: #e74c3c; color: white; font-weight: bold; padding: 6px 14px; border-radius: 4px;")
    self.save_left_btn.clicked.connect(lambda: self._save_side("left"))
    left_btns.addWidget(self.save_left_btn)
    self.revert_left_btn = QPushButton("↩️ 还原左侧")
    self.revert_left_btn.setStyleSheet("background: #95a5a6; color: white; padding: 6px 14px; border-radius: 4px;")
    self.revert_left_btn.clicked.connect(lambda: self._revert_side("left"))
    left_btns.addWidget(self.revert_left_btn)
    btn_layout.addLayout(left_btns)
    btn_layout.addStretch()
    right_btns = QHBoxLayout()
    self.save_right_btn = QPushButton("💾 保存右侧")
    self.save_right_btn.setEnabled(False)
    self.save_right_btn.setStyleSheet("background: #27ae60; color: white; font-weight: bold; padding: 6px 14px; border-radius: 4px;")
    self.save_right_btn.clicked.connect(lambda: self._save_side("right"))
    right_btns.addWidget(self.save_right_btn)
    self.revert_right_btn = QPushButton("↩️ 还原右侧")
    self.revert_right_btn.setStyleSheet("background: #95a5a6; color: white; padding: 6px 14px; border-radius: 4px;")
    self.revert_right_btn.clicked.connect(lambda: self._revert_side("right"))
    right_btns.addWidget(self.revert_right_btn)
    btn_layout.addLayout(right_btns)
    btn_layout.addStretch()
    self.btn_apply = QPushButton("✅ 应用修复")
    self.btn_apply.setStyleSheet("background: #2ecc71; color: white; font-weight: bold; padding: 8px 24px; border-radius: 6px; font-size: 12px;")
    self.btn_apply.clicked.connect(self.accept)
    btn_layout.addWidget(self.btn_apply)
    self.btn_cancel = QPushButton("❌ 取消")
    self.btn_cancel.setStyleSheet("background: #e74c3c; color: white; padding: 8px 24px; border-radius: 6px;")
    self.btn_cancel.clicked.connect(self.reject)
    btn_layout.addWidget(self.btn_cancel)
    layout.addLayout(btn_layout)
    bottom_info = QHBoxLayout()
    bottom_info.addWidget(QLabel("💡 备份文件 (.bak.py) 将保留,可手动恢复"))
    bottom_info.addStretch()
    layout.addLayout(bottom_info)
    self.status_label = QLabel("就绪")
    self.status_label.setStyleSheet("border: 1px solid #ddd; padding: 2px 8px; background: #f8f9fa; border-radius: 4px;")
    layout.addWidget(self.status_label)
    self._update_status()

def _show_diff(self):
    self.left_edit.setPlainText(self.original_content)
    self.right_edit.setPlainText(self.new_content)

def _on_edit(self, side):
    if side == "left":
        self.left_modified = True
        self.save_left_btn.setEnabled(True)
    else:
        self.right_modified = True
        self.save_right_btn.setEnabled(True)
    self._update_status()

def _save_side(self, side):
    content = self.left_edit.toPlainText() if side == "left" else self.right_edit.toPlainText()
    with open(self.backup_path, 'w', encoding='utf-8-sig') as f:
        f.write(content)
    if side == "left":
        self.left_modified = False
        self.save_left_btn.setEnabled(False)
    else:
        self.right_modified = False
        self.save_right_btn.setEnabled(False)
    self._update_status()
    self.safe_log(f"✅ 已保存{'左侧' if side == 'left' else '右侧'}到备份")

def _revert_side(self, side):
    if side == "left":
        self.left_edit.setPlainText(self.original_content)
        self.left_modified = False
        self.save_left_btn.setEnabled(False)
    else:
        self.right_edit.setPlainText(self.new_content)
        self.right_modified = False
        self.save_right_btn.setEnabled(False)
    self._update_status()

def _update_status(self):
    self.status_label.setText(f"左侧: {'已修改' if self.left_modified else '未修改'} | 右侧: {'已修改' if self.right_modified else '未修改'}")

def _log(self, msg):
    if self.parent() and hasattr(self.parent(), 'safe_log'):
        self.parent().safe_log(msg)

def accept(self):
    """用户确认应用修复"""
    self.safe_log(f"✅ 修复已应用,备份保留: {os.path.basename(self.backup_path)}")
    super().accept()

def reject(self):
    """从备份还原"""
    if os.path.exists(self.backup_path):
        try:
            shutil.copy2(self.backup_path, self.file_path)
            self.safe_log(f"↩️ 已从备份还原: {os.path.basename(self.file_path)}")
            self.safe_log(f"💡 备份保留: {os.path.basename(self.backup_path)}")
        except Exception as e:
            self.safe_log(f"⚠️ 还原失败: {e}")
    super().reject()

class CodeCompareDialog(QDialog):
"""增强版代码对比 - 4窗口 + 函数列表 + 同步滚动 + 模糊匹配 + 互覆盖"""
def init(self, parent, left_file, right_file):
super().init(parent)
self.setWindowTitle(f"代码对比 - {os.path.basename(left_file)} ↔ {os.path.basename(right_file)}")
self.resize(1200, 800)
self.setMinimumSize(800, 600)
self.setModal(True)
self.left_file = left_file
self.right_file = right_file
self.view_mode = "func"
self.current_func = None
self.sync_mode = False
self.left_modified = False
self.right_modified = False
self.highlight_active = False
self.fuzzy_match_enabled = False
self._load_files()
self._extract_functions()
self._setup_ui()
self._display_func_mode()
self._update_status()
self.sync_mode = True
self._bind_sync()
self.sync_btn.setText("🔗 滚动 (开)")
self.sync_btn.setStyleSheet("background: #2ecc71; color: white;")
if self.common_funcs:
self._select_func(self.common_funcs[0])

def _load_files(self):
    with open(self.left_file, 'r', encoding='utf-8-sig') as f:
        self.left_lines = f.readlines()
    with open(self.right_file, 'r', encoding='utf-8-sig') as f:
        self.right_lines = f.readlines()
    self.left_original = self.left_lines.copy()
    self.right_original = self.right_lines.copy()

def _extract_functions(self):
    self.left_funcs = {}
    self.right_funcs = {}

    def extract(lines, func_dict):
        i = 0
        while i < len(lines):
            line = lines[i].strip()
            if line.startswith('def '):
                match = re.match(r'def\s+(\w+)\s*\(', line)
                if match:
                    name = match.group(1)
                    start = i
                    j = i + 1
                    indent = len(lines[i]) - len(lines[i].lstrip())
                    while j < len(lines):
                        if lines[j].strip():
                            curr_indent = len(lines[j]) - len(lines[j].lstrip())
                            if curr_indent <= indent and lines[j].strip().startswith('def '):
                                break
                        j += 1
                    end = j - 1
                    body = ''.join(lines[start:end+1])
                    func_dict[name] = {'start': start, 'end': end, 'body': body}
                    i = j
                else:
                    i += 1
            else:
                i += 1
    extract(self.left_lines, self.left_funcs)
    extract(self.right_lines, self.right_funcs)
    self.common_funcs = sorted(set(self.left_funcs.keys()) & set(self.right_funcs.keys()))

def showEvent(self, event):
    """窗口显示时调整大小"""
    super().showEvent(event)
    try:
        screen = QApplication.primaryScreen().availableGeometry()
        width = int(screen.width() * 0.8)
        height = int(screen.height() * 0.8)
        self.resize(width, height)
        self.setMinimumSize(int(screen.width() * 0.6), int(screen.height() * 0.4))
    except:
        pass

def _setup_ui(self):
    layout = QVBoxLayout(self)
    # 工具栏
    toolbar = QWidget()
    tb = QHBoxLayout(toolbar)
    tb.setContentsMargins(0,0,0,0)
    self.func_mode_btn = QPushButton("函数")
    self.func_mode_btn.setStyleSheet("background: #2ecc71; color: white;")
    self.func_mode_btn.clicked.connect(self._switch_to_func_mode)
    tb.addWidget(self.func_mode_btn)
    self.line_mode_btn = QPushButton("逐行")
    self.line_mode_btn.setStyleSheet("background: #95a5a6; color: white;")
    self.line_mode_btn.clicked.connect(self._switch_to_line_mode)
    tb.addWidget(self.line_mode_btn)
    tb.addWidget(QLabel("|"))
    # ===== 模糊匹配开关 =====
    self.fuzzy_match_cb = QCheckBox("🔍 模糊")
    self.fuzzy_match_cb.setChecked(False)
    self.fuzzy_match_cb.setToolTip("开启后,精确匹配不到的函数会尝试相似度匹配")
    self.fuzzy_match_cb.stateChanged.connect(self._refresh_func_mode)
    tb.addWidget(self.fuzzy_match_cb)
    tb.addWidget(QLabel("|"))
    # ===== 互覆盖按钮 =====
    self.copy_left_to_right_btn = QPushButton("左→右")
    self.copy_left_to_right_btn.setStyleSheet("background: #e74c3c; color: white;")
    self.copy_left_to_right_btn.setToolTip("将左侧选中的行覆盖到右侧对应位置")
    self.copy_left_to_right_btn.clicked.connect(self._copy_selected_left_to_right)
    tb.addWidget(self.copy_left_to_right_btn)
    self.copy_right_to_left_btn = QPushButton("右→左")
    self.copy_right_to_left_btn.setStyleSheet("background: #3498db; color: white;")
    self.copy_right_to_left_btn.setToolTip("将右侧选中的行覆盖到左侧对应位置")
    self.copy_right_to_left_btn.clicked.connect(self._copy_selected_right_to_left)
    tb.addWidget(self.copy_right_to_left_btn)
    tb.addWidget(QLabel("|"))
    self.highlight_btn = QPushButton("高亮")
    self.highlight_btn.clicked.connect(self._on_highlight_clicked)
    self.highlight_btn.setStyleSheet("background: #e67e22; color: white;")
    self.highlight_btn.setToolTip("在当前模式下高亮显示差异")
    tb.addWidget(self.highlight_btn)
    # === 清除高亮按钮(点击后变色) ===
    self.clear_highlight_btn = QPushButton("清除")
    self.clear_highlight_btn.clicked.connect(self._clear_highlights)
    self.clear_highlight_btn.setStyleSheet("background: #95a5a6; color: white;")
    self.clear_highlight_btn.setToolTip("清除所有差异高亮")
    tb.addWidget(self.clear_highlight_btn)
    tb.addWidget(QLabel("|"))
    self.sync_btn = QPushButton("🔗 滚动")
    self.sync_btn.clicked.connect(self._toggle_sync)
    self.sync_btn.setStyleSheet("background: #3498db; color: white;")
    tb.addWidget(self.sync_btn)
    tb.addStretch()
    self.save_left_btn = QPushButton("💾 存左")
    self.save_left_btn.setEnabled(False)
    self.save_left_btn.clicked.connect(lambda: self._save_side("left"))
    tb.addWidget(self.save_left_btn)
    self.revert_left_btn = QPushButton("↩️ 还左")
    self.revert_left_btn.clicked.connect(lambda: self._revert_side("left"))
    tb.addWidget(self.revert_left_btn)
    self.save_right_btn = QPushButton("💾 存右")
    self.save_right_btn.setEnabled(False)
    self.save_right_btn.clicked.connect(lambda: self._save_side("right"))
    tb.addWidget(self.save_right_btn)
    self.revert_right_btn = QPushButton("↩️ 还右")
    self.revert_right_btn.clicked.connect(lambda: self._revert_side("right"))
    tb.addWidget(self.revert_right_btn)
    tb.addWidget(QLabel("|"))
    self.export_btn = QPushButton("导出")
    self.export_btn.clicked.connect(self._export_report)
    self.export_btn.setStyleSheet("background: #1abc9c; color: white;")
    tb.addWidget(self.export_btn)
    self.reload_btn = QPushButton("加载")
    self.reload_btn.clicked.connect(self._reload_files)
    self.reload_btn.setStyleSheet("background: #f39c12; color: white;")
    tb.addWidget(self.reload_btn)
    self.select_btn = QPushButton("选择")
    self.select_btn.clicked.connect(self._select_files)
    self.select_btn.setStyleSheet("background: #9b59b6; color: white;")
    tb.addWidget(self.select_btn)
    layout.addWidget(toolbar)
    # 主分割:左侧函数列表 + 右侧4窗口
    main_splitter = QSplitter(Qt.Orientation.Horizontal)
    # 左侧函数列表
    left_frame = QWidget()
    left_layout = QVBoxLayout(left_frame)
    left_layout.addWidget(QLabel("📋 函数列表"))
    self.search_edit = QLineEdit()
    self.search_edit.setPlaceholderText("搜索函数...")
    self.search_edit.textChanged.connect(self._filter_func_list)
    left_layout.addWidget(self.search_edit)
    self.func_list = QListWidget()
    self.func_list.itemClicked.connect(self._on_func_clicked)
    left_layout.addWidget(self.func_list)
    main_splitter.addWidget(left_frame)
    main_splitter.setSizes([200, 800])
    # 右侧4窗口
    right_widget = QWidget()
    right_layout = QVBoxLayout(right_widget)
    right_layout.setContentsMargins(0,0,0,0)
    top_bottom = QSplitter(Qt.Orientation.Vertical)
    # 预览区
    preview_splitter = QSplitter(Qt.Orientation.Horizontal)
    left_preview_frame = QFrame()
    left_preview_frame.setFrameStyle(QFrame.Shape.StyledPanel)
    left_preview_layout = QVBoxLayout(left_preview_frame)
    left_preview_layout.addWidget(QLabel("📄 源文件 (预览)"))
    self.left_preview = QPlainTextEdit()
    self.left_preview.setReadOnly(True)
    self.left_preview.setFont(QFont("Consolas", 10))
    self.left_preview.setStyleSheet("background: #1e1e1e; color: #d4d4d4;")
    PythonHighlighter(self.left_preview.document())
    left_preview_layout.addWidget(self.left_preview)
    preview_splitter.addWidget(left_preview_frame)
    right_preview_frame = QFrame()
    right_preview_frame.setFrameStyle(QFrame.Shape.StyledPanel)
    right_preview_layout = QVBoxLayout(right_preview_frame)
    right_preview_layout.addWidget(QLabel("📄 编译文件 (预览)"))
    self.right_preview = QPlainTextEdit()
    self.right_preview.setReadOnly(True)
    self.right_preview.setFont(QFont("Consolas", 10))
    self.right_preview.setStyleSheet("background: #1e1e1e; color: #d4d4d4;")
    PythonHighlighter(self.right_preview.document())
    right_preview_layout.addWidget(self.right_preview)
    preview_splitter.addWidget(right_preview_frame)
    preview_splitter.setSizes([400,400])
    top_bottom.addWidget(preview_splitter)
    # 编辑区
    edit_splitter = QSplitter(Qt.Orientation.Horizontal)
    left_edit_frame = QFrame()
    left_edit_frame.setFrameStyle(QFrame.Shape.StyledPanel)
    left_edit_layout = QVBoxLayout(left_edit_frame)
    self.left_edit_title = QLabel("✏️ 源函数: 未选中")
    left_edit_layout.addWidget(self.left_edit_title)
    self.left_edit = QPlainTextEdit()
    self.left_edit.setFont(QFont("Consolas", 10))
    self.left_edit.setStyleSheet("background: #2b2b2b; color: #d4d4d4;")
    self.left_edit.textChanged.connect(lambda: self._on_edit("left"))
    PythonHighlighter(self.left_edit.document())
    left_edit_layout.addWidget(self.left_edit)
    edit_splitter.addWidget(left_edit_frame)
    right_edit_frame = QFrame()
    right_edit_frame.setFrameStyle(QFrame.Shape.StyledPanel)
    right_edit_layout = QVBoxLayout(right_edit_frame)
    self.right_edit_title = QLabel("✏️ 编译函数: 未选中")
    right_edit_layout.addWidget(self.right_edit_title)
    self.right_edit = QPlainTextEdit()
    self.right_edit.setFont(QFont("Consolas", 10))
    self.right_edit.setStyleSheet("background: #2b2b2b; color: #d4d4d4;")
    self.right_edit.textChanged.connect(lambda: self._on_edit("right"))
    PythonHighlighter(self.right_edit.document())
    right_edit_layout.addWidget(self.right_edit)
    edit_splitter.addWidget(right_edit_frame)
    edit_splitter.setSizes([400,400])
    top_bottom.addWidget(edit_splitter)
    top_bottom.setSizes([400,400])
    right_layout.addWidget(top_bottom)
    main_splitter.addWidget(right_widget)
    layout.addWidget(main_splitter, stretch=1)
    # 状态栏
    status_frame = QFrame()
    status_frame.setFrameStyle(QFrame.Shape.StyledPanel)
    status_layout = QHBoxLayout(status_frame)
    self.status_label = QLabel("就绪")
    status_layout.addWidget(self.status_label)
    status_layout.addStretch()
    self.status_func = QLabel("函数: 无")
    status_layout.addWidget(self.status_func)
    layout.addWidget(status_frame)

def _refresh_func_mode(self):
    """刷新函数模式(切换模糊匹配后重新显示)"""
    if self.view_mode == "func":
        self._display_func_mode()

def _find_matching_functions(self, left_name, right_names):
    """找到匹配的右侧函数名(优先精确匹配)"""
    import difflib
    if left_name in right_names:
        return left_name, "精确匹配"
    if not self.fuzzy_match_cb.isChecked():
        return None, None
    lower_map = {name.lower(): name for name in right_names}
    if left_name.lower() in lower_map:
        return lower_map[left_name.lower()], "忽略大小写"
    left_clean = left_name.strip('_')
    for right_name in right_names:
        right_clean = right_name.strip('_')
        if left_clean == right_clean:
            return right_name, "去前后缀"
        if left_clean.endswith(right_clean) or right_clean.endswith(left_clean):
            return right_name, "包含匹配"
    matches = difflib.get_close_matches(left_name, right_names, n=1, cutoff=0.7)
    if matches:
        return matches[0], f"相似度 {difflib.SequenceMatcher(None, left_name, matches[0]).ratio():.2f}"
    return None, None

def _count_diff_lines(self, left_text, right_text):
    """统计两个文本的差异行数"""
    import difflib
    differ = difflib.SequenceMatcher(None, left_text.splitlines(), right_text.splitlines())
    added = 0
    removed = 0
    for tag, i1, i2, j1, j2 in differ.get_opcodes():
        if tag == 'replace':
            added += j2 - j1
            removed += i2 - i1
        elif tag == 'delete':
            removed += i2 - i1
        elif tag == 'insert':
            added += j2 - j1
    return added, removed

def _get_display_funcs(self):
    """获取当前显示的匹配函数列表"""
    result = []
    left_names = list(self.left_funcs.keys())
    right_names = list(self.right_funcs.keys())
    for name in left_names:
        matched, _ = self._find_matching_functions(name, right_names)
        if matched:
            result.append(name)
    return sorted(result)

def _get_selected_range(self, edit_widget):
    """获取选中区域的行范围"""
    cursor = edit_widget.textCursor()
    start = cursor.selectionStart()
    end = cursor.selectionEnd()
    if start == end:
        return None, None
    doc = edit_widget.document()
    start_block = doc.findBlock(start)
    end_block = doc.findBlock(end)
    return start_block.blockNumber(), end_block.blockNumber()

def _copy_selected_left_to_right(self):
    """将左侧选中的行覆盖到右侧对应位置"""
    start, end = self._get_selected_range(self.left_edit)
    if start is None:
        show_msg(self, "提示", "请先在左侧选中要覆盖的行",1)
        return
    cursor = self.left_edit.textCursor()
    selected_text = cursor.selectedText()
    reply = QMessageBox.question(
        self, "确认覆盖",
        f"将左侧选中的 {end - start + 1} 行覆盖到右侧对应位置?\n\n右侧原有内容将被替换!",
        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
    )
    if reply != QMessageBox.StandardButton.Yes:
        return
    right_cursor = self.right_edit.textCursor()
    right_cursor.setPosition(self.right_edit.document().findBlockByNumber(start).position())
    right_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
    for _ in range(end - start):
        right_cursor.movePosition(QTextCursor.MoveOperation.Down, QTextCursor.MoveMode.KeepAnchor)
        right_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
    right_cursor.insertText(selected_text)
    self.right_modified = True
    self.save_right_btn.setEnabled(True)
    self._update_status()
    if self.parent() and hasattr(self.parent(), 'safe_log'):
        self.parent().safe_log(f"✅ 已从左侧覆盖 {end - start + 1} 行到右侧")

def _copy_selected_right_to_left(self):
    """将右侧选中的行覆盖到左侧对应位置"""
    start, end = self._get_selected_range(self.right_edit)
    if start is None:
        show_msg(self, "提示", "请先在右侧选中要覆盖的行",1)
        return
    cursor = self.right_edit.textCursor()
    selected_text = cursor.selectedText()
    reply = QMessageBox.question(
        self, "确认覆盖",
        f"将右侧选中的 {end - start + 1} 行覆盖到左侧对应位置?\n\n左侧原有内容将被替换!",
        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
    )
    if reply != QMessageBox.StandardButton.Yes:
        return
    left_cursor = self.left_edit.textCursor()
    left_cursor.setPosition(self.left_edit.document().findBlockByNumber(start).position())
    left_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
    for _ in range(end - start):
        left_cursor.movePosition(QTextCursor.MoveOperation.Down, QTextCursor.MoveMode.KeepAnchor)
        left_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
    left_cursor.insertText(selected_text)
    self.left_modified = True
    self.save_left_btn.setEnabled(True)
    self._update_status()
    if self.parent() and hasattr(self.parent(), 'safe_log'):
        self.parent().safe_log(f"✅ 已从右侧覆盖 {end - start + 1} 行到左侧")

def _populate_func_list(self):
    self.func_list.clear()
    keyword = self.search_edit.text().strip().lower()
    display_funcs = self._get_display_funcs()
    for name in display_funcs:
        if keyword == "" or keyword in name.lower():
            self.func_list.addItem(name)

def _on_func_clicked(self, item):
    func_name = item.text()
    right_names = list(self.right_funcs.keys())
    matched, _ = self._find_matching_functions(func_name, right_names)
    if matched:
        self._select_func(func_name, matched)
    else:
        self._select_func(func_name, None)

def _select_func(self, left_name, right_name=None):
    """选择并显示函数对比"""
    self.current_func = left_name
    if right_name is None:
        right_name = left_name
        matched, _ = self._find_matching_functions(left_name, list(self.right_funcs.keys()))
        if matched:
            right_name = matched
    self.left_edit_title.setText(f"✏️ 源函数: {left_name}")
    self.right_edit_title.setText(f"✏️ 编译函数: {right_name}")
    self.left_edit.clear()
    self.right_edit.clear()
    if left_name in self.left_funcs:
        self.left_edit.setPlainText(self.left_funcs[left_name]['body'])
    if right_name in self.right_funcs:
        self.right_edit.setPlainText(self.right_funcs[right_name]['body'])
    self.left_modified = False
    self.right_modified = False
    self.save_left_btn.setEnabled(False)
    self.save_right_btn.setEnabled(False)
    self._update_status()
    if self.highlight_active:
        self._highlight_diffs()

def _display_func_mode(self):
    self.view_mode = "func"
    self.func_mode_btn.setStyleSheet("background: #2ecc71; color: white;")
    self.line_mode_btn.setStyleSheet("background: #95a5a6; color: white;")
    self.left_preview.clear()
    self.right_preview.clear()
    left_names = list(self.left_funcs.keys())
    right_names = list(self.right_funcs.keys())
    matched_pairs = []
    used_right = set()
    for name in left_names:
        matched, match_type = self._find_matching_functions(name, right_names)
        if matched and matched not in used_right:
            matched_pairs.append((name, matched, match_type))
            used_right.add(matched)
        elif matched and matched in used_right:
            matched_pairs.append((name, None, "已被其他函数匹配"))
    for name, matched, match_type in matched_pairs:
        if matched:
            left_body = self.left_funcs[name]['body']
            right_body = self.right_funcs[matched]['body']
            left_num = len(left_body.splitlines())
            right_num = len(right_body.splitlines())
            added, removed = self._count_diff_lines(left_body, right_body)
            diff_info = f" [+{added} -{removed}]"
            label = f"函数: {name} ↔ {matched}{diff_info}"
            if match_type != "精确匹配":
                label += f" [{match_type}]"
            self.left_preview.appendPlainText(f"\n{'='*60}\n{label}\n行数: {left_num}\n{'='*60}")
            self.left_preview.appendPlainText(left_body)
            self.right_preview.appendPlainText(f"\n{'='*60}\n{label}\n行数: {right_num}\n{'='*60}")
            self.right_preview.appendPlainText(right_body)
        else:
            self.left_preview.appendPlainText(f"\n{'='*60}\n函数: {name} (无匹配)\n{'='*60}")
            self.left_preview.appendPlainText(self.left_funcs[name]['body'])
            self.right_preview.appendPlainText(f"\n{'='*60}\n函数: {name} (无匹配)\n{'='*60}")
    self._populate_func_list()
    matched_count = len([p for p in matched_pairs if p[1] is not None])
    total_left = len(left_names)
    self.status_label.setText(f"匹配: {matched_count}/{total_left} 个函数")
    if self.highlight_active:
        self._highlight_diffs()

def _switch_to_func_mode(self):
    if self.view_mode != "func":
        self._display_func_mode()
        if self.current_func:
            right_names = list(self.right_funcs.keys())
            matched, _ = self._find_matching_functions(self.current_func, right_names)
            if matched:
                self._select_func(self.current_func, matched)
            else:
                self._select_func(self.current_func, None)

def _display_line_mode(self):
    self.view_mode = "line"
    self.func_mode_btn.setStyleSheet("background: #95a5a6; color: white;")
    self.line_mode_btn.setStyleSheet("background: #2ecc71; color: white;")
    self.left_preview.clear()
    self.right_preview.clear()
    differ = difflib.SequenceMatcher(None, self.left_lines, self.right_lines)
    for tag, i1, i2, j1, j2 in differ.get_opcodes():
        if tag == 'equal':
            for i in range(i1, i2):
                self.left_preview.appendPlainText(self.left_lines[i].rstrip())
                self.right_preview.appendPlainText(self.right_lines[j1 + (i-i1)].rstrip())
        elif tag == 'replace':
            for i in range(i1, i2):
                self.left_preview.appendPlainText("  " + self.left_lines[i].rstrip())
            for j in range(j1, j2):
                self.right_preview.appendPlainText("  " + self.right_lines[j].rstrip())
        elif tag == 'delete':
            for i in range(i1, i2):
                self.left_preview.appendPlainText("- " + self.left_lines[i].rstrip())
        elif tag == 'insert':
            for j in range(j1, j2):
                self.right_preview.appendPlainText("+ " + self.right_lines[j].rstrip())
    # 逐行模式下也支持高亮
    if self.highlight_active:
        self._highlight_diffs()

def _switch_to_func_mode(self):
    if self.view_mode != "func":
        self._display_func_mode()
        if self.current_func:
            self._select_func(self.current_func)

def _switch_to_line_mode(self):
    if self.view_mode != "line":
        self._display_line_mode()

def _on_edit(self, side):
    if side == "left":
        self.left_modified = True
        self.save_left_btn.setEnabled(True)
    else:
        self.right_modified = True
        self.save_right_btn.setEnabled(True)
    self._update_status()
    if self.highlight_active:
        self.highlight_active = False
        self.highlight_btn.setStyleSheet("background: #e67e22; color: white;")

def _save_side(self, side):
    if side == "left":
        content = self.left_edit.toPlainText()
        if self.current_func and self.current_func in self.left_funcs:
            old = self.left_funcs[self.current_func]
            new_lines = content.splitlines(True)
            if new_lines and not new_lines[-1].endswith('\n'):
                new_lines[-1] += '\n'
            self.left_lines[old['start']:old['end']+1] = new_lines
            with open(self.left_file, 'w', encoding='utf-8-sig') as f:
                f.writelines(self.left_lines)
            self.left_funcs[self.current_func]['body'] = content
            self.left_funcs[self.current_func]['end'] = old['start'] + len(new_lines) - 1
            self.left_modified = False
            self.save_left_btn.setEnabled(False)
            self._refresh_preview()
    else:
        content = self.right_edit.toPlainText()
        if self.current_func and self.current_func in self.right_funcs:
            old = self.right_funcs[self.current_func]
            new_lines = content.splitlines(True)
            if new_lines and not new_lines[-1].endswith('\n'):
                new_lines[-1] += '\n'
            self.right_lines[old['start']:old['end']+1] = new_lines
            with open(self.right_file, 'w', encoding='utf-8-sig') as f:
                f.writelines(self.right_lines)
            self.right_funcs[self.current_func]['body'] = content
            self.right_funcs[self.current_func]['end'] = old['start'] + len(new_lines) - 1
            self.right_modified = False
            self.save_right_btn.setEnabled(False)
            self._refresh_preview()
    self._update_status()

def _revert_side(self, side):
    if side == "left" and self.current_func in self.left_funcs:
        old = self.left_funcs[self.current_func]
        original = ''.join(self.left_original[old['start']:old['end']+1])
        self.left_edit.setPlainText(original.rstrip())
        self.left_funcs[self.current_func]['body'] = original.rstrip()
        self.left_lines[old['start']:old['end']+1] = self.left_original[old['start']:old['end']+1].copy()
        self.left_modified = False
        self.save_left_btn.setEnabled(False)
    elif side == "right" and self.current_func in self.right_funcs:
        old = self.right_funcs[self.current_func]
        original = ''.join(self.right_original[old['start']:old['end']+1])
        self.right_edit.setPlainText(original.rstrip())
        self.right_funcs[self.current_func]['body'] = original.rstrip()
        self.right_lines[old['start']:old['end']+1] = self.right_original[old['start']:old['end']+1].copy()
        self.right_modified = False
        self.save_right_btn.setEnabled(False)
    self._update_status()
    if self.view_mode == "func":
        self._refresh_preview()
    if self.highlight_active:
        self._highlight_diffs()

def _refresh_preview(self):
    if self.view_mode != "func":
        return
    self.left_preview.clear()
    self.right_preview.clear()
    left_names = list(self.left_funcs.keys())
    right_names = list(self.right_funcs.keys())
    for name in left_names:
        matched, match_type = self._find_matching_functions(name, right_names)
        if matched:
            left_body = self.left_funcs[name]['body']
            right_body = self.right_funcs[matched]['body']
            left_num = len(left_body.splitlines())
            right_num = len(right_body.splitlines())
            label = f"函数: {name} ↔ {matched}"
            if match_type != "精确匹配":
                label += f" [{match_type}]"
            self.left_preview.appendPlainText(f"\n{'='*60}\n{label}\n行数: {left_num}\n{'='*60}")
            self.left_preview.appendPlainText(left_body)
            self.right_preview.appendPlainText(f"\n{'='*60}\n{label}\n行数: {right_num}\n{'='*60}")
            self.right_preview.appendPlainText(right_body)
        else:
            self.left_preview.appendPlainText(f"\n{'='*60}\n函数: {name} (无匹配)\n{'='*60}")
            self.left_preview.appendPlainText(self.left_funcs[name]['body'])
            self.right_preview.appendPlainText(f"\n{'='*60}\n函数: {name} (无匹配)\n{'='*60}")

def _on_highlight_clicked(self):
    """高亮差异按钮点击 - 切换高亮状态"""
    if self.highlight_active:
        self._clear_highlights()
        self.highlight_active = False
        self.highlight_btn.setStyleSheet("background: #e67e22; color: white;")
    else:
        self._highlight_diffs()
        self.highlight_active = True
        self.highlight_btn.setStyleSheet("background: #e67e22; color: white; font-weight: bold; border: 2px solid #ff6b00;")
    self.clear_highlight_btn.setStyleSheet("background: #95a5a6; color: white;")

def _clear_highlights(self):
    self.left_edit.setExtraSelections([])
    self.right_edit.setExtraSelections([])
    self.status_label.setText("已清除高亮")
    self.highlight_active = False
    self.highlight_btn.setStyleSheet("background: #e67e22; color: white;")
    self.clear_highlight_btn.setStyleSheet("background: #f44336; color: white; font-weight: bold;")
    QTimer.singleShot(300, lambda: self.clear_highlight_btn.setStyleSheet("background: #95a5a6; color: white;"))

def _highlight_diffs(self):
    """高亮差异 - 使用 ExtraSelections"""
    if self.view_mode == "func":
        self._highlight_func_diff()
    else:
        self._highlight_line_diff()

def _highlight_func_diff(self):
    """函数模式下的高亮 - 只高亮当前选中的函数"""
    if not self.current_func:
        return
    right_name = self.current_func
    matched, _ = self._find_matching_functions(self.current_func, list(self.right_funcs.keys()))
    if matched:
        right_name = matched
    if self.current_func not in self.left_funcs or right_name not in self.right_funcs:
        return
    left_body = self.left_funcs[self.current_func]['body'].splitlines()
    right_body = self.right_funcs[right_name]['body'].splitlines()
    differ = difflib.SequenceMatcher(None, left_body, right_body)
    left_selections = []
    right_selections = []
    for tag, i1, i2, j1, j2 in differ.get_opcodes():
        if tag == 'equal':
            continue
        if tag in ('delete', 'replace'):
            for i in range(i1, i2):
                if i < self.left_edit.document().blockCount():
                    cursor = self.left_edit.textCursor()
                    cursor.setPosition(self.left_edit.document().findBlockByNumber(i).position())
                    cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
                    fmt = QTextCharFormat()
                    fmt.setBackground(QColor("#5a2a2a"))
                    selection = QTextEdit.ExtraSelection()
                    selection.cursor = cursor
                    selection.format = fmt
                    left_selections.append(selection)
        if tag in ('insert', 'replace'):
            for j in range(j1, j2):
                if j < self.right_edit.document().blockCount():
                    cursor = self.right_edit.textCursor()
                    cursor.setPosition(self.right_edit.document().findBlockByNumber(j).position())
                    cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
                    fmt = QTextCharFormat()
                    fmt.setBackground(QColor("#2a5a2a"))
                    selection = QTextEdit.ExtraSelection()
                    selection.cursor = cursor
                    selection.format = fmt
                    right_selections.append(selection)
    self.left_edit.setExtraSelections(left_selections)
    self.right_edit.setExtraSelections(right_selections)
    if left_selections or right_selections:
        self.status_label.setText(f"✅ 差异: 左侧 {len(left_selections)} 处, 右侧 {len(right_selections)} 处")
        self.status_func.setText(f"函数: {self.current_func} | 差异: L{len(left_selections)} R{len(right_selections)}")

def _highlight_line_diff(self):
    """逐行模式下的高亮 - 高亮全文件差异"""
    differ = difflib.SequenceMatcher(None, self.left_lines, self.right_lines)
    left_selections = []
    right_selections = []
    for tag, i1, i2, j1, j2 in differ.get_opcodes():
        if tag == 'equal':
            continue
        if tag in ('delete', 'replace'):
            for i in range(i1, i2):
                if i < self.left_edit.document().blockCount():
                    cursor = self.left_edit.textCursor()
                    cursor.setPosition(self.left_edit.document().findBlockByNumber(i).position())
                    cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
                    fmt = QTextCharFormat()
                    fmt.setBackground(QColor("#5a2a2a"))
                    selection = QTextEdit.ExtraSelection()
                    selection.cursor = cursor
                    selection.format = fmt
                    left_selections.append(selection)
        if tag in ('insert', 'replace'):
            for j in range(j1, j2):
                if j < self.right_edit.document().blockCount():
                    cursor = self.right_edit.textCursor()
                    cursor.setPosition(self.right_edit.document().findBlockByNumber(j).position())
                    cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
                    fmt = QTextCharFormat()
                    fmt.setBackground(QColor("#2a5a2a"))
                    selection = QTextEdit.ExtraSelection()
                    selection.cursor = cursor
                    selection.format = fmt
                    right_selections.append(selection)
    self.left_edit.setExtraSelections(left_selections)
    self.right_edit.setExtraSelections(right_selections)
    if left_selections or right_selections:
        self.status_label.setText(f"✅ 差异: 左侧 {len(left_selections)} 处, 右侧 {len(right_selections)} 处")

def _toggle_sync(self):
    self.sync_mode = not self.sync_mode
    if self.sync_mode:
        self._bind_sync()
        self.sync_btn.setText("🔗 滚动 (开)")
        self.sync_btn.setStyleSheet("background: #2ecc71; color: white;")
    else:
        self._unbind_sync()
        self.sync_btn.setText("🔗 滚动 (关)")
        self.sync_btn.setStyleSheet("background: #3498db; color: white;")

def _bind_sync(self):
    all_texts = [self.left_preview, self.right_preview, self.left_edit, self.right_edit]
    for text in all_texts:
        text.wheelEvent = lambda e, t=text: self._on_wheel(e, t)

def _unbind_sync(self):
    all_texts = [self.left_preview, self.right_preview, self.left_edit, self.right_edit]
    for text in all_texts:
        text.wheelEvent = None

def _on_wheel(self, event, source):
    if not self.sync_mode:
        return
    delta = event.angleDelta().y()
    if delta == 0:
        return
    current = source.verticalScrollBar().value()
    new_pos = current - delta // 4
    all_texts = [self.left_preview, self.right_preview, self.left_edit, self.right_edit]
    for text in all_texts:
        text.verticalScrollBar().setValue(new_pos)
    event.accept()

def _reload_files(self):
    self._load_files()
    self._extract_functions()
    if self.view_mode == "func":
        self._display_func_mode()
        if self.current_func:
            matched, _ = self._find_matching_functions(self.current_func, list(self.right_funcs.keys()))
            if matched:
                self._select_func(self.current_func, matched)
            else:
                self._select_func(self.current_func, None)
    else:
        self._display_line_mode()
    self.left_modified = False
    self.right_modified = False
    self._update_status()

def _select_files(self):
    left = QFileDialog.getOpenFileName(self, "选择源文件", "", "Python文件 (*.py)")[0]
    if not left:
        return
    right = QFileDialog.getOpenFileName(self, "选择编译文件", "", "Python文件 (*.py)")[0]
    if not right:
        return
    self.left_file = left
    self.right_file = right
    self.setWindowTitle(f"代码对比 - {os.path.basename(left)} ↔ {os.path.basename(right)}")
    self._reload_files()

def _export_report(self):
    path = QFileDialog.getSaveFileName(self, "导出报告", "", "文本文件 (*.txt)")[0]
    if not path:
        return
    report = ["=" * 60]
    report.append("代码对比报告")
    report.append(f"源文件: {os.path.basename(self.left_file)}")
    report.append(f"编译文件: {os.path.basename(self.right_file)}")
    report.append(f"时间: {datetime.now()}")
    report.append("=" * 60)
    left_names = list(self.left_funcs.keys())
    right_names = list(self.right_funcs.keys())
    matched_count = 0
    report.append("")
    report.append("📊 函数匹配统计:")
    report.append("-" * 40)
    for name in left_names:
        matched, match_type = self._find_matching_functions(name, right_names)
        if matched:
            matched_count += 1
            left_body = self.left_funcs[name]['body']
            right_body = self.right_funcs[matched]['body']
            ratio = difflib.SequenceMatcher(None, left_body, right_body).ratio() * 100
            added, removed = self._count_diff_lines(left_body, right_body)
            status = f"相似度: {ratio:.1f}%  [+{added} -{removed}]"
            if match_type != "精确匹配":
                status += f" [{match_type}]"
            report.append(f"  {name} ↔ {matched}")
            report.append(f"    {status}")
    report.append("")
    report.append(f"匹配率: {matched_count}/{len(left_names)} ({matched_count/len(left_names)*100:.1f}%)" if left_names else "0/0")
    with open(path, 'w', encoding='utf-8-sig') as f:
        f.write('\n'.join(report))
    self.status_label.setText(f"✅ 报告已导出: {os.path.basename(path)}")

def _filter_func_list(self):
    self._populate_func_list()

def _update_status(self):
    self.status_label.setText(f"函数: {self.current_func or '无'} | 左侧修改: {'是' if self.left_modified else '否'} | 右侧修改: {'是' if self.right_modified else '否'}")
    self.status_func.setText(f"函数: {self.current_func or '无'}")

class SystemMonitorThread(QThread):
"""系统资源监控线程"""
status_updated = pyqtSignal(float, float, float, float, str)
def init(self):
super().init()
self._is_running = True
self._interval = 3000
self._lhm_computer = None
self._init_lhm()

def _init_lhm(self):
    """初始化 LibreHardwareMonitor(Windows 最佳方案)"""
    if sys.platform != 'win32':
        return
    try:
        import clr
        # 尝试多种路径找 dll
        dll_paths = [
            'LibreHardwareMonitorLib.dll',
            './LibreHardwareMonitorLib.dll',
            '../LibreHardwareMonitorLib.dll',
            os.path.join(os.path.dirname(__file__), 'LibreHardwareMonitorLib.dll'),
            os.path.join(get_exe_directory(), 'LibreHardwareMonitorLib.dll'),
            os.path.join(get_exe_directory(), 'tools', 'LibreHardwareMonitorLib.dll'),
        ]
        dll_loaded = False
        for dll_path in dll_paths:
            if os.path.exists(dll_path):
                clr.AddReference(dll_path)
                dll_loaded = True
                break
        if not dll_loaded:
            return
        from LibreHardwareMonitor.Hardware import Computer, HardwareType, SensorType
        self._lhm_computer = Computer()
        self._lhm_computer.IsCpuEnabled = True
        self._lhm_computer.IsGpuEnabled = True
        self._lhm_computer.IsMotherboardEnabled = True
        self._lhm_computer.Open()
    except Exception:
        self._lhm_computer = None

def _get_temp_lhm(self):
    """通过 LibreHardwareMonitor 获取温度(返回 float)"""
    if not self._lhm_computer:
        return None
    try:
        from LibreHardwareMonitor.Hardware import HardwareType, SensorType
        for hardware in self._lhm_computer.Hardware:
            if hardware.HardwareType == HardwareType.Cpu:
                hardware.Update()
                for sensor in hardware.Sensors:
                    if (sensor.SensorType == SensorType.Temperature and 
                        'package' in sensor.Name.lower()):
                        val = sensor.Value
                        if val is not None and -10 < val < 120:
                            return float(val)
                for sensor in hardware.Sensors:
                    if sensor.SensorType == SensorType.Temperature:
                        val = sensor.Value
                        if val is not None and -10 < val < 120:
                            return float(val)
    except Exception:
        pass
    return None

def _get_temp_psutil(self):
    """通过 psutil 获取温度(Linux/macOS 有效,返回 float)"""
    try:
        import psutil
        temps = psutil.sensors_temperatures()
        if not temps:
            return None
        priority_keys = ['coretemp', 'k10temp', 'zenpower', 'cpu_thermal', 
                         'acpitz', 'pch_skylake']
        for key in priority_keys:
            if key in temps:
                entries = temps[key]
                if entries:
                    for entry in entries:
                        if entry.current and 10 < entry.current < 120:
                            return float(entry.current)
        for name, entries in temps.items():
            if entries:
                for entry in entries:
                    if entry.current and 10 < entry.current < 120:
                        return float(entry.current)
    except Exception:
        pass
    return None

def _get_temp_wmic(self):
    """通过 wmic/powershell 命令获取温度(Windows 备用,返回 float)"""
    if sys.platform != 'win32':
        return None
    try:
        import subprocess
        # 优先用 powershell(Win11 24H2 已移除 wmic)
        ps_cmd = (
            'Get-CimInstance -Namespace root/wmi '
            '-ClassName MSAcpi_ThermalZoneTemperature | '
            'Select-Object -ExpandProperty CurrentTemperature'
        )
        result = subprocess.run(
            ['powershell', '-NoProfile', '-Command', ps_cmd],
            capture_output=True, text=True, timeout=5,
            creationflags=subprocess.CREATE_NO_WINDOW
        )
        lines = [l.strip() for l in result.stdout.split('\n') 
                 if l.strip().replace('.', '').isdigit()]
        if lines:
            temp_k = float(lines[0]) / 10.0
            temp_c = temp_k - 273.15
            if -10 < temp_c < 120:
                return float(temp_c)
        # fallback 到 wmic(旧系统)
        result = subprocess.run(
            ['wmic', r'/namespace:\\root\wmi',
             'PATH', 'MSAcpi_ThermalZoneTemperature', 
             'GET', 'CurrentTemperature'],
            capture_output=True, text=True, timeout=5,
            creationflags=subprocess.CREATE_NO_WINDOW
        )
        lines = [l.strip() for l in result.stdout.split('\n') 
                 if l.strip().replace('.', '').isdigit()]
        if lines:
            temp_k = float(lines[0]) / 10.0
            temp_c = temp_k - 273.15
            if -10 < temp_c < 120:
                return float(temp_c)
    except Exception:
        pass
    return None

def _get_temperature(self):
    """获取温度:按优先级尝试多种方案"""
    temp = self._get_temp_lhm()
    if temp is not None:
        return f"{temp:.1f}°C"
    temp = self._get_temp_psutil()
    if temp is not None:
        return f"{temp:.1f}°C"
    temp = self._get_temp_wmic()
    if temp is not None:
        return f"{temp:.1f}°C"
    return ""

def _get_resource_color(self, percent, is_temp=False):
    """获取资源颜色(三色:绿→橙→红)"""
    if is_temp:
        if percent > 80:
            return "#F44336"
        elif percent > 65:
            return "#FF9800"
        else:
            return "#4CAF50"
    else:
        if percent > 80:
            return "#F44336"
        elif percent > 60:
            return "#FF9800"
        else:
            return "#4CAF50"

def run(self):
    try:
        import psutil
    except ImportError:
        return
    while self._is_running:
        try:
            cpu_percent = psutil.cpu_percent(interval=1)
            memory = psutil.virtual_memory()
            temp_str = self._get_temperature()
            self.status_updated.emit(
                cpu_percent,
                memory.percent,
                memory.used / (1024 ** 3),
                memory.total / (1024 ** 3),
                temp_str
            )
        except Exception:
            pass
        if self._is_running:
            self.msleep(self._interval)

def stop(self):
    self._is_running = False
    self.wait(1000)
    if self._lhm_computer:
        try:
            self._lhm_computer.Close()
        except Exception:
            pass

class PackThread(QThread):
progress_signal = pyqtSignal(int)
log_signal = pyqtSignal(str)
finished_signal = pyqtSignal(bool, str)
def init(self, config):
super().init()
self.config = config
self._is_running = True
self.process = None
self._mock_progress = 0
self._mock_timer = None
self._real_progress_received = False
self._output_buffer = []  

def run(self):
    process = None
    try:
        self.log_signal.emit("🚀 开始打包...")
        cmd = self._build_command()
        process = self._popen_hidden(
            cmd, 
            stdout=subprocess.PIPE, 
            stderr=subprocess.STDOUT,
            text=True, 
            encoding="utf-8", 
            errors="replace", 
            startupinfo=get_startupinfo(),
            bufsize=1,
            universal_newlines=True
        )
        self.process = process
        self._real_progress_received = False
        self._mock_progress = 0
        self._start_mock_progress()
        import select
        import sys
        while self._is_running and process.poll() is None:
            try:
                if sys.platform == 'win32':
                    # Windows使用timeout轮询
                    import time
                    time.sleep(0.1)
                    line = process.stdout.readline()
                    if line:
                        self._process_output_line(line)
                else:
                    import select
                    if select.select([process.stdout], [], [], 0.1)[0]:
                        line = process.stdout.readline()
                        if line:
                            self._process_output_line(line)
            except Exception as e:
                if self._is_running:
                    self.log_signal.emit(f"读取输出异常: {e}")
                break
            if not self._is_running:
                break
        if self._is_running:
            for line in process.stdout:
                if not self._is_running:
                    break
                self._process_output_line(line)
        if process.poll() is None:
            process.wait(timeout=30)
        returncode = process.returncode if process.poll() is not None else -1
        self._stop_mock_progress()
        if returncode == 0:
            self.progress_signal.emit(100)
            self.finished_signal.emit(True, "打包完成!")
        else:
            self.finished_signal.emit(False, f"返回码: {returncode}")
    except subprocess.TimeoutExpired:
        self._stop_mock_progress()
        self.finished_signal.emit(False, "打包超时")
    except Exception as e:
        self._stop_mock_progress()
        self.finished_signal.emit(False, str(e))
    finally:
        self._cleanup_process(process)

def _process_output_line(self, line):
    """处理输出行"""
    if not line:
        return
    line = line.rstrip()
    if line:
        self.log_signal.emit(line)
        p = self._parse_progress(line)
        if p is not None:
            if not self._real_progress_received:
                self._real_progress_received = True
                self._stop_mock_progress()
            self.progress_signal.emit(p)

def _start_mock_progress(self):
    """启动模拟进度"""
    self._mock_progress = 0
    self._real_progress_received = False
    self._mock_timer = QTimer()
    self._mock_timer.timeout.connect(self._update_mock_progress)
    self._mock_timer.start(2000)  

def _update_mock_progress(self):
    """更新模拟进度(更慢的增长)"""
    if self._real_progress_received:
        self._stop_mock_progress()
        return
    if self._mock_progress < 5: 
        self._mock_progress += 1
        self.progress_signal.emit(self._mock_progress)

def _stop_mock_progress(self):
    """停止模拟进度"""
    if self._mock_timer:
        self._mock_timer.stop()
        self._mock_timer.deleteLater()
        self._mock_timer = None

def _cleanup_process(self, process):
    """彻底清理进程"""
    if process:
        try:
            if process.poll() is None:
                process.terminate()
                try:
                    process.wait(timeout=3)
                except subprocess.TimeoutExpired:
                    process.kill()
                    process.wait(timeout=2)
            if process.stdout:
                process.stdout.close()
            if process.stderr:
                process.stderr.close()
        except Exception as e:
            pass
        finally:
            self.process = None
            import gc
            gc.collect()

def stop(self):
    """停止打包"""
    self._is_running = False
    self._stop_mock_progress()
    if self.process:
        try:
            if sys.platform == 'win32':
                self.process.terminate()
                try:
                    self.process.wait(timeout=3)
                except subprocess.TimeoutExpired:
                    self.process.kill()
            else:
                self.process.terminate()
                try:
                    self.process.wait(timeout=3)
                except subprocess.TimeoutExpired:
                    self.process.kill()
        except Exception as e:
            pass
        finally:
            self.process = None

qt.jpg (108.43 KB, 下载次数: 0)

qt.jpg
推荐
 楼主| wcj6376 发表于 2026-8-11 20:18 |楼主
本帖最后由 wcj6376 于 2026-8-11 22:02 编辑

源码地址:https://github.com/wcj6376/PyPackTool     更新下载exe地址通过网盘分享的文件:PyPackTool
链接: https://pan.baidu.com/s/1q1-bB4vOTQ10lygeScuXww 提取码: 52pj及部分所需工具解压到同目录下的tools即可  也可以自行下载  
一、快速开始
─────────────────
1. 选择要打包的 Python 脚本文件
2. 设置打包参数(输出目录、图标等)
3. 点击「开始打包」按钮
4. 等待打包完成
5. 在输出目录找到生成的 exe 文件
二、详细步骤
─────────────────
1&#65039;&#8419; 选择脚本文件
&#8226; 点击「选择文件」按钮
&#8226; 选择您的 .py 脚本文件
&#8226; 支持 Python 3.8+ 版本
&#8226; 支持拖拽文件到界面
2&#65039;&#8419; 配置打包参数
&#8226; 输出目录:设置打包文件保存位置
&#8226; 应用程序名称:设置生成的 exe 名称
&#8226; 图标文件:可选,支持 .ico 格式
&#8226; 额外文件:添加需要打包的资源文件
3&#65039;&#8419; 高级选项
&#8226; 打包模式:单文件/多文件
&#8226; 控制台窗口:显示/隐藏
&#8226; UPX 压缩:减小文件体积
&#8226; 依赖管理:自动/手动
4&#65039;&#8419; 开始打包
&#8226; 点击「开始打包」按钮
&#8226; 查看打包进度和日志
&#8226; 打包完成后自动打开输出目录
三、常见问题
─────────────────
&#10067; 打包后程序无法运行
&#8226; 检查 Python 环境是否完整
&#8226; 确认所有依赖已正确安装
&#8226; 查看打包日志寻找错误
&#10067; 打包文件体积过大
&#8226; 使用 UPX 压缩选项
&#8226; 检查是否包含不必要的文件
&#8226; 使用多文件模式
&#10067; 中文显示乱码
&#8226; 确保使用 UTF-8 编码
&#8226; 在代码中添加编码声明
&#8226; 检查文件编码格式
3#
 楼主| wcj6376 发表于 2026-8-6 20:28 |楼主
本帖最后由 wcj6376 于 2026-8-7 10:05 编辑


如果发现自动排除后打包提示缺失依赖库可以去掉勾选排除重新打包即可,只是体积偏大,。优化附属依赖为可配置,方便大家自行拓展配置一次永久有效。可解决体积过大或者缺失依赖报错问题。

33.jpg (48.01 KB, 下载次数: 0)

33.jpg
4#
wuyands 发表于 2026-8-6 23:03
感谢分享
5#
Huanxian 发表于 2026-8-7 08:36
这个还是不错的,这个教程对我很有帮助,
6#
sohuso 发表于 2026-8-7 08:50
&#10060; 打包失败: 返回码: 1
在win10下选择Linux平台打包报错
7#
Monitor 发表于 2026-8-7 08:57
一直提示

---------------------------
错误
---------------------------
请先安装 PyInstaller: pip install pyinstaller
---------------------------
确定   
---------------------------


可我已经手动安装成功了,并且重启过了
另外Python后面识别到版本是“3.10.6 | 未安装”,这个未安装指的是什么?
8#
 楼主| wcj6376 发表于 2026-8-7 09:36 |楼主

你要安装在选定的Python环境里面,多个Python建议 绝对路径xxxxx\python.exe -m pip install xxx
9#
chicheng99 发表于 2026-8-7 10:22
感谢分享,留个脚印后面应该能用上
10#
bb1222663901 发表于 2026-8-7 17:06
尝试尝试,看看咋样
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-14 12:07

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表