├── .github ├── CONTRIBUTING.md └── dependabot.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── README_zh.md ├── dev_scripts ├── build.py ├── check_funcs.py ├── clear_cache.py ├── path_constants.py └── pyside6_tools_script.py ├── docs ├── ROADMAP.md └── source │ └── images │ ├── Py2exe-GUI_v0.1.0_screenshot.png │ ├── Py2exe-GUI_v0.2.0_screenshot.png │ ├── Py2exe-GUI_v0.3.0_mainwindow_screenshot.png │ ├── Py2exe-GUI_v0.3.1_mainwindow_screenshot_en.png │ ├── gplv3-127x51.png │ └── py2exe-gui_logo_big.png ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── src ├── Py2exe-GUI.py └── py2exe_gui ├── Constants ├── __init__.py ├── app_constants.py ├── packaging_constants.py ├── python_env_constants.py └── runtime_info.py ├── Core ├── __init__.py ├── packaging.py ├── packaging_task.py ├── subprocess_tool.py └── validators.py ├── Resources ├── COMPILED_RESOURCES.py ├── Icons │ ├── Py2exe-GUI_icon_72px.ico │ ├── Py2exe-GUI_icon_72px.png │ ├── Python-logo-cyan-72px.png │ ├── Python_128px.png │ ├── conda-icon_200px.png │ ├── conda-icon_72px.png │ ├── poetry-icon_128px.png │ └── pyinstaller-icon-console_128px.png ├── Texts │ ├── About_en.md │ ├── About_zh_CN.md │ ├── pyinstaller_options_en.yaml │ └── pyinstaller_options_zh_CN.yaml ├── i18n │ ├── zh_CN.qm │ └── zh_CN.ts └── resources.qrc ├── Utilities ├── __init__.py ├── open_qfile.py ├── platform_specific_funcs.py ├── python_env.py └── qobject_tr.py ├── Widgets ├── __init__.py ├── add_data_widget.py ├── arguments_browser.py ├── center_widget.py ├── dialog_widgets.py ├── main_window.py ├── multi_item_edit_widget.py ├── pyenv_combobox.py ├── pyinstaller_option_widget.py └── subprocess_widget.py ├── __init__.py └── __main__.py /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Code Contribution Guidelines 2 | 3 | > 注:简体中文版在本文下方。 4 | 5 | Thank you very much for your willingness to contribute to the Py2exe-GUI project, please read the following and follow 6 | some ground rules in order to keep the project in good shape for rapid development. 7 | 8 | ## Before Writing Code 9 | 10 | If you have any thoughts that involve more than a few dozen lines of code, I highly recommend you to submit an issue first talking about the stuff you want to implement. It's a good idea to discuss whether we want to do it before you put a lot of effort into it, and it also ensures that we're not duplicating work. 11 | 12 | ## Coding 13 | 14 | ### Prepare the development environment 15 | 16 | Development environment is more complicated than use environment, ensure that you have installed Python 3.11+ and [Poetry](https://python-poetry.org/docs/#installation). Then use Poetry to create and install the development environment: 17 | 18 | ```shell 19 | cd Py2exe-GUI 20 | poetry install --with dev --extras "AddOns" 21 | ``` 22 | 23 | You also need to install a git hook via [pre-commit](https://pre-commit.com/): 24 | 25 | ```shell 26 | pre-commit install 27 | ``` 28 | 29 | ### Code Style 30 | 31 | - In general, new and modified code should be close to the style of the original code. Try to make the code readable. 32 | - Please format the code using [Black](https://black.readthedocs.io/en/stable/) to ensure that all code styles are consistent with the project. 33 | - Please add sufficient type annotations to the code for your code. This allows the code to pass [mypy](https://mypy.readthedocs.io/en/stable/) checks without reporting errors. 34 | - Please add docstrings or comments to modules, classes, functions/methods, properties, etc. to ensure clarity and readability. 35 | 36 | ### Documentation 37 | 38 | If you implement some new features, you should consider adding the appropriate documentation to the `docs/` directory, or modifying `README.md` as appropriate to elaborate. 39 | 40 | ### Tests 41 | 42 | The Py2exe-GUI project does not have any tests at the moment due to my limited programming skills as well as the fact that automated tests are not easily implemented for GUI programs. If you want to add some test code, please create `tests/` directory in the root directory of the whole project and put the test code in it. 43 | 44 | ## Pull Request 45 | 46 | Do a pull request to the `main` branch of [muziing/Py2exe-GUI](https://github.com/muziing/Py2exe-GUI), and I will review the code and give feedbacks as soon as possible. 47 | 48 | ----- 49 | 50 | # 代码贡献指南 51 | 52 | 非常感谢你愿意为 Py2exe-GUI 项目提供贡献,请阅读以下内容,遵守一些基本规则,以便项目保持良好状态快速发展。 53 | 54 | ## 开始编码之前 55 | 56 | 如果你准备对代码进行超过数十行的修改或新增,我强烈建议你先提交一个 issue,谈谈你想实现的东西。在投入很多精力之前,我们应该先讨论一下是否要这样做,也可以确保我们不重复工作。 57 | 58 | ## 编写代码 59 | 60 | ### 准备开发环境 61 | 62 | 开发环境相较于使用环境较为复杂,确保已经安装 Python 3.11+ 和 [Poetry](https://python-poetry.org/docs/#installation),然后通过 Poetry 创建和安装开发环境: 63 | 64 | ```shell 65 | cd Py2exe-GUI 66 | poetry install --with dev --extras "AddOns" 67 | ``` 68 | 69 | > 如果你在国内使用 PyPI 源速度较慢,可以考虑取消注释 `pyproject.toml` 文件中的 `[[tool.poetry.source]]` 70 | > 小节,启用国内镜像站。但注意不要将修改后的 `poetry.lock` 文件提交到 git 中。 71 | 72 | 还需要通过 [pre-commit](https://pre-commit.com/) 安装 git 钩子: 73 | 74 | ```shell 75 | pre-commit install 76 | ``` 77 | 78 | ### 代码风格 79 | 80 | - 总体来讲,新增和修改的代码应接近原有代码的风格。尽量使代码有良好的可读性。 81 | - 请使用 [Black](https://black.readthedocs.io/en/stable/) 格式化代码,确保所有代码风格与项目一致。开发环境中已经安装了 Black,配置使用方法可以参考[这篇文章](https://muzing.top/posts/a29e4743/)。 82 | - 请为代码添加充分的[类型注解](https://muzing.top/posts/84a8da1c/),并能通过 [mypy](https://mypy.readthedocs.io/en/stable/) 检查不报错。 83 | - 请为模块、类、函数/方法、属性等添加 docstring 或注释,确保含义清晰易读。 84 | 85 | ### 文档 86 | 87 | 如果你实现了一些新功能,应考虑在 `docs/` 目录下添加相应的文档,或适当修改 `README.md` 来加以阐述。 88 | 89 | ### 测试 90 | 91 | 由于我的编程水平有限、GUI 程序不易实现自动化测试等原因,Py2exe-GUI 项目暂无测试。如果你想添加一些测试代码,请在整个项目的根目录下创建 `tests/` 目录,并将测试代码置于其中。 92 | 93 | ## 拉取请求 94 | 95 | 新建一个指向 [muziing/Py2exe-GUI](https://github.com/muziing/Py2exe-GUI) 的 `main` 分支的拉取请求,我将尽快 review 代码并给出反馈。 96 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "pip" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyCharm IDE 132 | .idea/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Visual Studio Code Editor 141 | .vscode/ 142 | 143 | # line-profiler 144 | *.lprof 145 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: false 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: check-toml 8 | - id: check-yaml 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | 12 | - repo: https://github.com/psf/black-pre-commit-mirror 13 | rev: 25.1.0 14 | hooks: 15 | - id: black 16 | args: ["--config", "pyproject.toml"] 17 | 18 | - repo: https://github.com/pycqa/isort 19 | rev: 6.0.1 20 | hooks: 21 | - id: isort 22 | args: [--settings-path, "./pyproject.toml"] 23 | 24 | - repo: https://github.com/astral-sh/ruff-pre-commit 25 | rev: v0.11.10 26 | hooks: 27 | - id: ruff 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Py2exe-GUI Logo 3 |

4 | 5 |

Python GUI packaging tool

6 | 7 |

8 | GitHub Repo stars 9 | Python Version 10 | PyPI Version 11 | PyPI Downloads 12 |

13 |

14 | PySide Version 15 | Ruff 16 | Code style: black 17 | Checked with mypy 18 |

19 | 20 |

21 | English | 简体中文 22 |

23 | 24 | ## Introduction 25 | 26 | Py2exe-GUI is an assist tool based on [PySide6](https://doc.qt.io/qtforpython/index.html), designed to provide a 27 | complete yet easy-to-use GUI for [PyInstaller](https://pyinstaller.org/). 28 | 29 | ![Screenshot](https://raw.githubusercontent.com/muziing/Py2exe-GUI/main/docs/source/images/Py2exe-GUI_v0.3.1_mainwindow_screenshot_en.png) 30 | 31 | ![Screenshot](https://raw.githubusercontent.com/muziing/Py2exe-GUI/main/docs/source/images/Py2exe-GUI_v0.2.0_screenshot.png) 32 | 33 | It has the following features: 34 | 35 | - Fully graphical interface, easy to use. 36 | - All options of PyInstaller will be supported. 37 | - You can invoke any local Python interpreter with its corresponding environment, eliminating the need to reinstall it in each interpreter environment to be packaged. 38 | - Cross-platform, supports Windows, Linux and macOS. 39 | 40 | ## How to install 41 | 42 | > Note: Py2exe-GUI is still in the early stages of development, and the distributions provided are *beta versions*. 43 | > Installation methods may change frequently, so be sure to check these instructions often. 44 | 45 | ### Option A: Install with `pip` 46 | 47 | First, install PyInstaller in the Python interpreter environment which to be packaged: 48 | 49 | ```shell 50 | pip install pyinstaller # Must be installed in your project environment 51 | ``` 52 | 53 | Then install Py2exe-GUI with `pip`: 54 | 55 | ```shell 56 | pip install py2exe-gui # Can be installed into any environment 57 | ``` 58 | 59 | Run: 60 | 61 | ```shell 62 | py2exe-gui 63 | ``` 64 | 65 | You can run py2exe-gui as a package if running it as a script doesn't work: 66 | 67 | ```shell 68 | python -m py2exe_gui # `_`, not `-` 69 | ``` 70 | 71 | ### Option B: Run through source code 72 | 73 | For those who like to try it out or are in desperate need of the latest bug fixes, you can run it through the repository source code: 74 | 75 | 1. Download the [latest main branching source code](https://codeload.github.com/muziing/Py2exe-GUI/zip/refs/heads/main). 76 | 77 | 2. Unzip it and go to the directory. Launch a terminal to create and activate the virtual environment: 78 | 79 | ```shell 80 | python -m venv venv # create a virtual environment (Windows) 81 | .\venv\Scripts\activate.ps1 # and activate it (Windows, PowerShell) 82 | ``` 83 | 84 | ```shell 85 | python3 -m venv venv # create a virtual environment (Linux/macOS) 86 | source venv/bin/activate # and activate it (Linux/macOS) 87 | ``` 88 | 89 | 3. Install dependencies and run the program. 90 | 91 | ```shell 92 | pip install -r requirements.txt 93 | python ./src/Py2exe-GUI.py 94 | ``` 95 | 96 | ## Contributing 97 | 98 | Py2exe-GUI is a free and open source software and anyone is welcome to contribute to its development. 99 | 100 | If you encounter any problems while using it (including bugs, typos, etc.), or if you have suggestions for new features, you can open an [issue](https://github.com/muziing/Py2exe-GUI/issues/new). 101 | 102 | If you have the willingness and ability to contribute code, please read the [contribution guidance](.github/CONTRIBUTING.md) for more details. 103 | 104 | ## License 105 | 106 | ![GPLv3](https://raw.githubusercontent.com/muziing/Py2exe-GUI/main/docs/source/images/gplv3-127x51.png) 107 | 108 | Py2exe-GUI is licensed under the GPLv3 open source license, see the [LICENSE](LICENSE) file for details. 109 | 110 | There is one exception: if your project uses Py2exe-GUI only as a packaging tool, and your final distribution does not contain Py2exe-GUI's source code or binaries, then your project is not restricted by the GPLv3 restrictions and can still be distributed as closed-source commercial software. 111 | 112 | ```text 113 | Py2exe-GUI 114 | Copyright (C) 2022-2024 muzing 115 | 116 | This program is free software: you can redistribute it and/or modify 117 | it under the terms of the GNU General Public License as published by 118 | the Free Software Foundation, either version 3 of the License, or 119 | (at your option) any later version. 120 | 121 | This program is distributed in the hope that it will be useful, 122 | but WITHOUT ANY WARRANTY; without even the implied warranty of 123 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 124 | GNU General Public License for more details. 125 | 126 | You should have received a copy of the GNU General Public License 127 | along with this program. If not, see . 128 | ``` 129 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |

2 | Py2exe-GUI Logo 3 |

4 | 5 |

强大易用的 Python 图形界面打包工具

6 | 7 |

8 | GitHub Repo stars 9 | Python Version 10 | PyPI Version 11 | PyPI Downloads 12 |

13 |

14 | PySide Version 15 | Ruff 16 | Code style: black 17 | Checked with mypy 18 |

19 |

20 | English | 简体中文 21 |

22 | 23 | ## 简介 24 | 25 | Py2exe-GUI 是一个基于 [PySide6](https://doc.qt.io/qtforpython/index.html) 开发的辅助工具,旨在为 [PyInstaller](https://pyinstaller.org/) 提供完整易用的图形化界面,方便用户进行 Python 项目的打包。 26 | 27 | ![界面截图](https://raw.githubusercontent.com/muziing/Py2exe-GUI/main/docs/source/images/Py2exe-GUI_v0.3.0_mainwindow_screenshot.png) 28 | 29 | ![界面截图](https://raw.githubusercontent.com/muziing/Py2exe-GUI/main/docs/source/images/Py2exe-GUI_v0.2.0_screenshot.png) 30 | 31 | 有如下特性: 32 | 33 | - 完全图形化界面,易用。 34 | - 将会支持 PyInstaller 的全部选项。 35 | - 可以调用本地任一 Python 解释器与对应环境,无需在每个待打包的解释器环境中重复安装。 36 | - 跨平台,支持 Windows、Linux、MacOS。 37 | 38 | ## 如何安装 39 | 40 | > 注意:Py2exe-GUI 尚处早期开发阶段,提供的分发版本均为*beta-测试版*。安装方式也可能频繁变化,注意经常查阅此使用说明。 41 | 42 | ### 方式1:通过 `pip` 安装 43 | 44 | 首先在待打包的 Python 解释器环境中安装 PyInstaller: 45 | 46 | ```shell 47 | pip install pyinstaller # 必须在你的项目环境中安装 48 | ``` 49 | 50 | 然后通过 pip 安装 Py2exe-GUI: 51 | 52 | ```shell 53 | pip install py2exe-gui # 可以安装至任何环境 54 | ``` 55 | 56 | 运行 57 | 58 | ```shell 59 | py2exe-gui 60 | ``` 61 | 62 | 如果以脚本形式运行失败,还可以尝试作为 Python 包运行: 63 | 64 | ```shell 65 | python -m py2exe_gui # 注意连字符为_ 66 | ``` 67 | 68 | ### 方式2:通过仓库源码运行 69 | 70 | 对于喜欢尝鲜或急需最新 bug 修复的用户,可以通过仓库源码运行: 71 | 72 | 1. 下载[最新 main 分支源码](https://codeload.github.com/muziing/Py2exe-GUI/zip/refs/heads/main) 73 | 74 | 2. 解压后进入目录,启动命令行/终端,创建并激活虚拟环境: 75 | 76 | ```shell 77 | python -m venv venv # 创建虚拟环境(Windows) 78 | .\venv\Scripts\activate.ps1 # 激活虚拟环境(Windows PowerShell) 79 | ``` 80 | 81 | ```shell 82 | python3 -m venv venv # 创建虚拟环境(Linux/macOS) 83 | source venv/bin/activate # 激活虚拟环境(Linux/macOS) 84 | ``` 85 | 86 | 3. 安装依赖、运行程序: 87 | 88 | ```shell 89 | pip install -r requirements.txt # 安装依赖项 90 | python ./src/Py2exe-GUI.py # 运行 91 | ``` 92 | 93 | ## 贡献 94 | 95 | Py2exe-GUI 是一个自由的开源软件,欢迎任何人为其开发贡献力量。 96 | 97 | 如果你在使用时遇到任何问题(包括 bug、界面错别字等),或者提议新增实用功能,可以提交一个 [issue](https://github.com/muziing/Py2exe-GUI/issues/new)。 98 | 99 | 如果你有能力有想法贡献代码,请阅读[贡献指南](.github/CONTRIBUTING.md)了解更多详情。 100 | 101 | ## 开源许可 102 | 103 | ![GPLv3](https://raw.githubusercontent.com/muziing/Py2exe-GUI/main/docs/source/images/gplv3-127x51.png) 104 | 105 | Py2exe-GUI 采用 GPLv3 开源许可证,详情请参见 [LICENSE](LICENSE) 文件。 106 | 107 | 但有一个例外:如果你的项目仅使用 Py2exe-GUI 作为打包工具,而最终发布的软件中并不包含 Py2exe-GUI 的源码或二进制文件,那么你的项目不会受到 GPLv3 的限制,仍可作为闭源商业软件发布。 108 | 109 | ```text 110 | Py2exe-GUI 111 | Copyright (C) 2022-2024 Muzing 112 | 113 | This program is free software: you can redistribute it and/or modify 114 | it under the terms of the GNU General Public License as published by 115 | the Free Software Foundation, either version 3 of the License, or 116 | (at your option) any later version. 117 | 118 | This program is distributed in the hope that it will be useful, 119 | but WITHOUT ANY WARRANTY; without even the implied warranty of 120 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 121 | GNU General Public License for more details. 122 | 123 | You should have received a copy of the GNU General Public License 124 | along with this program. If not, see . 125 | ``` 126 | -------------------------------------------------------------------------------- /dev_scripts/build.py: -------------------------------------------------------------------------------- 1 | """用于构建项目的脚本""" 2 | 3 | __all__ = [ 4 | "export_requirements", 5 | "build_py2exe_gui", 6 | ] 7 | 8 | import subprocess 9 | 10 | from dev_scripts.check_funcs import ( 11 | check_license_statement, 12 | check_mypy, 13 | check_pre_commit, 14 | check_version_num, 15 | ) 16 | from dev_scripts.clear_cache import clear_pycache, clear_pyinstaller_dist 17 | from dev_scripts.path_constants import PROJECT_ROOT, SRC_PATH 18 | 19 | 20 | def export_requirements() -> int: 21 | """将项目依赖项导出至 requirements.txt 中 22 | 23 | :return: poetry export 命令返回值 24 | """ 25 | 26 | poetry_export_cmd = [ 27 | "poetry", 28 | "export", 29 | "--without-hashes", 30 | "-o", 31 | PROJECT_ROOT / "requirements.txt", 32 | "--format=requirements.txt", 33 | ] 34 | 35 | try: 36 | result = subprocess.run(poetry_export_cmd) 37 | except subprocess.SubprocessError as e: 38 | print(f"poetry export 进程错误:{e}") 39 | raise e 40 | else: 41 | print( 42 | "已将当前项目依赖导出至 requirements.txt," 43 | f"poetry export 返回码:{result.returncode}" 44 | ) 45 | return result.returncode 46 | 47 | 48 | def build_py2exe_gui() -> None: 49 | """构建项目的总函数""" 50 | 51 | if check_version_num() + check_license_statement() == 0: 52 | # 准备工作 53 | clear_pyinstaller_dist(SRC_PATH) 54 | clear_pycache(SRC_PATH) 55 | # compile_resources() 56 | # export_requirements() 57 | print(f"pre-commit 检查完毕,返回码:{check_pre_commit()}。") 58 | print(f"mypy 检查完毕,返回码:{check_mypy()}。") 59 | 60 | # 正式构建 61 | try: 62 | result = subprocess.run(["poetry", "build"], check=True) 63 | except subprocess.SubprocessError as e: 64 | print(f"Poetry build 失败:{e}") 65 | raise 66 | else: 67 | print(f"Poetry build 完毕,返回码:{result.returncode}。") 68 | finally: 69 | # 清理 70 | pass 71 | else: 72 | print("有未通过的检查项,不进行构建") 73 | 74 | 75 | if __name__ == "__main__": 76 | # export_requirements() 77 | build_py2exe_gui() 78 | -------------------------------------------------------------------------------- /dev_scripts/check_funcs.py: -------------------------------------------------------------------------------- 1 | """各类检查函数""" 2 | 3 | __all__ = [ 4 | "check_license_statement", 5 | "check_version_num", 6 | "check_pre_commit", 7 | "check_mypy", 8 | ] 9 | 10 | import subprocess 11 | import tomllib 12 | import warnings 13 | 14 | from dev_scripts.path_constants import ( 15 | COMPILED_RESOURCES, 16 | PROJECT_ROOT, 17 | SRC_PATH, 18 | SRC_PKG_PATH, 19 | ) 20 | from py2exe_gui import Constants as py2exe_gui_Constants 21 | from py2exe_gui import __version__ as py2exe_gui__version__ 22 | 23 | 24 | def check_license_statement() -> int: 25 | """检查源代码文件中是否都包含了许可声明 26 | 27 | :return: 0-所有源文件都包含许可声明;1-存在缺失许可声明的源文件 28 | """ 29 | 30 | license_statement = "# Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html\n" 31 | source_file_list = list(SRC_PATH.glob("**/*.py")) 32 | source_file_list.remove(COMPILED_RESOURCES) # 排除RCC编译工具自动生成的.py文件 33 | check_pass = 0 34 | 35 | print("开始检查源码中许可声明情况...") 36 | 37 | for file in source_file_list: 38 | with open(file, encoding="utf-8") as f: 39 | if license_statement not in f.read(): 40 | warning_mes = f"Source code file {file} lacks a license statement." 41 | warnings.warn(warning_mes, Warning, stacklevel=2) 42 | else: 43 | check_pass += 1 44 | 45 | if check_pass == len(source_file_list): 46 | print("许可声明检查完毕,所有源码文件都包含许可声明。") 47 | return 0 48 | else: 49 | print("许可声明检查完毕,部分源码文件缺失许可声明,请检查。") 50 | return 1 51 | 52 | 53 | def check_version_num() -> int: 54 | """检查各部分声明的版本号是否一致 55 | 56 | :return: 0-各处版本一致;1-存在版本不一致情况 57 | """ 58 | 59 | print("正在检查各处版本号是否一致...") 60 | 61 | app_constant_version = py2exe_gui_Constants.app_constants.AppConstant.VERSION 62 | package_version = py2exe_gui__version__ 63 | with open(PROJECT_ROOT / "pyproject.toml", "rb") as ppj_toml_file: 64 | ppj_dict = tomllib.load(ppj_toml_file) 65 | ppj_version = ppj_dict["tool"]["poetry"]["version"] 66 | 67 | if ppj_version == app_constant_version == package_version: 68 | print(f"版本号检查完毕,均为 {ppj_version}。") 69 | return 0 70 | else: 71 | warning_mes = ( 72 | """版本号不一致!\n""" 73 | + f"""pyproject.toml................{ppj_version}\n""" 74 | + f"""__version__...................{package_version}\n""" 75 | + f"""Constants.AppConstant.........{app_constant_version}\n""" 76 | ) 77 | warnings.warn(warning_mes, stacklevel=1) 78 | return 1 79 | 80 | 81 | def check_pre_commit() -> int: 82 | """调用已有的 pre-commit 检查工具进行检查 83 | 84 | 如果首次调用返回值不为0,可能已经进行了一定的自动修复,需要再运行第二次检查返回值 85 | 86 | :return: pre-commit 进程返回码 87 | """ 88 | 89 | pre_commit_run_cmd = ["pre-commit", "run", "--all-files"] 90 | print("开始进行第一次 pre-commit 检查...") 91 | result_1 = subprocess.run(pre_commit_run_cmd) 92 | if result_1.returncode != 0: 93 | print("开始进行第二次 pre-commit 检查...") 94 | result_2 = subprocess.run(pre_commit_run_cmd) 95 | if result_2.returncode != 0: 96 | warnings.warn("pre-commit 进程返回码非 0,建议检查", stacklevel=1) 97 | return result_2.returncode 98 | else: 99 | print("pre-commit 检查完成,所有项目通过。") 100 | return 0 101 | 102 | 103 | def check_mypy() -> int: 104 | """调用mypy进行静态代码分析""" 105 | 106 | mypy_cmd = ["mypy", SRC_PKG_PATH, "--config-file", PROJECT_ROOT / "pyproject.toml"] 107 | print("开始运行 mypy 检查...") 108 | try: 109 | result = subprocess.run(mypy_cmd) 110 | except subprocess.CalledProcessError as e: 111 | warnings.warn(f"mypy 检查失败,错误信息:{e}", stacklevel=1) 112 | return e.returncode 113 | else: 114 | print("mypy 检查运行完毕。") 115 | return result.returncode 116 | 117 | 118 | if __name__ == "__main__": 119 | check_license_statement() 120 | check_version_num() 121 | check_pre_commit() 122 | check_mypy() 123 | -------------------------------------------------------------------------------- /dev_scripts/clear_cache.py: -------------------------------------------------------------------------------- 1 | """各种清理函数,如清理 Python 编译缓存、PyInstaller 打包中间文件与输出文件等""" 2 | 3 | __all__ = [ 4 | "clear_pyinstaller_dist", 5 | "clear_pycache", 6 | ] 7 | 8 | import os 9 | from pathlib import Path 10 | from shutil import rmtree 11 | 12 | from dev_scripts.path_constants import SRC_PATH 13 | 14 | 15 | def clear_pyinstaller_dist(src_path: Path) -> None: 16 | """清理开发过程中测试运行时的打包中间文件及结果文件 17 | 18 | :param src_path: Py2exe-GUI.py 运行目录 19 | """ 20 | 21 | dist_path = src_path / "dist" 22 | build_path = src_path / "build" 23 | spec_path_list = list(src_path.glob("*.spec")) 24 | 25 | if dist_path.exists(): 26 | rmtree(dist_path) 27 | if build_path.exists(): 28 | rmtree(build_path) 29 | if spec_path_list: 30 | for spec_file in spec_path_list: 31 | os.remove(spec_file) 32 | 33 | print("Pyinstaller dist and cache all cleaned.") 34 | 35 | 36 | def clear_pycache(src_path: Path) -> None: 37 | """清理给定路径下的所有 `.pyc` `.pyo` 文件与 `__pycache__` 目录 38 | 39 | ref: https://stackoverflow.com/a/41386937 40 | 41 | :param src_path: 源码 src 目录路径 42 | """ 43 | 44 | [p.unlink() for p in src_path.rglob("*.py[co]")] 45 | [p.rmdir() for p in src_path.rglob("__pycache__")] 46 | print("PyCache all cleaned.") 47 | 48 | 49 | if __name__ == "__main__": 50 | clear_pyinstaller_dist(SRC_PATH) 51 | clear_pycache(SRC_PATH) 52 | -------------------------------------------------------------------------------- /dev_scripts/path_constants.py: -------------------------------------------------------------------------------- 1 | """开发脚本中使用的相对路径常量 2 | 3 | 所有脚本应以项目根目录为工作目录运行 4 | """ 5 | 6 | from pathlib import Path 7 | 8 | PROJECT_ROOT = Path(__file__).parent.parent # 项目根目录 9 | SRC_PATH = PROJECT_ROOT / "src" # 源码目录 10 | SRC_PKG_PATH = SRC_PATH / "py2exe_gui" # 包目录 11 | RESOURCES_PATH = SRC_PKG_PATH / "Resources" # 静态资源文件目录 12 | COMPILED_RESOURCES = RESOURCES_PATH / "COMPILED_RESOURCES.py" # 编译静态资源文件 13 | WIDGETS_PATH = SRC_PKG_PATH / "Widgets" # 控件目录 14 | README_FILE_LIST = [ 15 | PROJECT_ROOT / "README.md", 16 | PROJECT_ROOT / "README_zh.md", 17 | ] # README 文件列表 18 | -------------------------------------------------------------------------------- /dev_scripts/pyside6_tools_script.py: -------------------------------------------------------------------------------- 1 | """开发脚本,便于调用 PySide6 提供的各种工具程序""" 2 | 3 | __all__ = [ 4 | "compile_resources", 5 | "gen_ts", 6 | "gen_qm", 7 | ] 8 | 9 | import subprocess 10 | 11 | from dev_scripts.path_constants import ( 12 | COMPILED_RESOURCES, 13 | PROJECT_ROOT, 14 | RESOURCES_PATH, 15 | SRC_PKG_PATH, 16 | WIDGETS_PATH, 17 | ) 18 | 19 | 20 | def compile_resources() -> int: 21 | """调用 RCC 工具编译静态资源 22 | 23 | :return: rcc 进程返回码 24 | """ 25 | 26 | compiled_file_path = COMPILED_RESOURCES 27 | qrc_file_path = RESOURCES_PATH / "resources.qrc" 28 | cmd = [ 29 | "pyside6-rcc", 30 | "-o", 31 | compiled_file_path, 32 | qrc_file_path, 33 | ] 34 | 35 | try: 36 | result = subprocess.run(cmd, cwd=PROJECT_ROOT, check=True) 37 | except subprocess.SubprocessError as e: 38 | print(f"RCC 编译进程错误:{e}") 39 | raise e 40 | else: 41 | print(f"已完成静态资源文件编译,RCC 返回码:{result.returncode}。") 42 | return result.returncode 43 | 44 | 45 | def gen_ts(lang: str = "zh_CN") -> int: 46 | """调用 lupdate 工具分析源码,生成 .ts 文本翻译文件 47 | 48 | :param lang: 目标翻译语言代码 49 | :return: lupdate 返回码 50 | """ 51 | 52 | source = [*list(WIDGETS_PATH.glob("**/*.py")), SRC_PKG_PATH / "__main__.py"] 53 | target = RESOURCES_PATH / "i18n" / f"{lang.replace('-', '_')}.ts" 54 | cmd = ["pyside6-lupdate", *source, "-ts", target] 55 | 56 | try: 57 | result = subprocess.run(cmd, cwd=PROJECT_ROOT, check=True) 58 | except subprocess.SubprocessError as e: 59 | print(f"lupdate 进程错误:{e}") 60 | raise 61 | else: 62 | print(f"已完成文本翻译文件生成,lupdate 返回码:{result.returncode}。") 63 | return result.returncode 64 | 65 | 66 | def gen_qm(lang: str = "zh_CN") -> int: 67 | """调用 lrelease 工具编译.ts 文本翻译文件 68 | 69 | :param lang: 目标翻译语言代码 70 | :return: lrelease 返回码 71 | """ 72 | 73 | source = RESOURCES_PATH / "i18n" / f"{lang.replace('-', '_')}.ts" 74 | target = RESOURCES_PATH / "i18n" / f"{lang.replace('-', '_')}.qm" 75 | cmd = ["pyside6-lrelease", source, "-qm", target] 76 | 77 | try: 78 | result = subprocess.run(cmd, cwd=PROJECT_ROOT, check=True) 79 | except subprocess.SubprocessError as e: 80 | print(f"lrelease 进程错误:{e}") 81 | raise 82 | else: 83 | print(f"已完成文本翻译文件编译,lrelease 返回码:{result.returncode}。") 84 | return result.returncode 85 | 86 | 87 | if __name__ == "__main__": 88 | # compile_resources() 89 | gen_ts("zh_CN") 90 | # gen_qm("zh_CN") 91 | -------------------------------------------------------------------------------- /docs/ROADMAP.md: -------------------------------------------------------------------------------- 1 | # 开发待办事项 2 | 3 | 此文档用于记录灵感,内容格式较为随意。其中的大部分功能将会逐渐实现,也有部分可能会被删去。 4 | 5 | 如果你对 Py2exe-GUI 的新功能有建议,欢迎[提交 issue](https://github.com/muziing/Py2exe-GUI/issues/new)。 6 | 7 | ## 控件 8 | 9 | - [ ] PyInstaller 子进程窗口 `SubProcessDlg` 10 | - [x] 将子进程的输出与状态显示至单独的对话框 11 | - [x] 增加多功能按钮 12 | - [x] 关闭窗口时中断子进程、清除输出 13 | - [x] 处理不能正确显示子进程错误的问题(会被“打包完成”遮盖) 14 | - [ ] 增加「将输出导出到日志文件」功能 15 | - [ ] 增加简单高亮功能 16 | - [x] 命令浏览器 17 | - [x] 显示将传递给 PyInstaller 的选项列表 18 | - [x] 高亮提示 19 | - [x] 以终端命令格式显示完整命令,并添加续行符 20 | - [ ] 导出为脚本功能,根据运行时平台导出 bash、PowerShell 等格式脚本 21 | - [x] 添加资源文件窗口 22 | - [x] `--add-data`、 `--add-binary` 23 | - [x] `--paths`、`--hidden-import` 等可多次调用的选项 24 | - [x] 模仿 Windows “编辑环境变量” 窗口,左侧条目,右侧添加删除编辑等按钮 25 | - [x] Python 解释器选择器 26 | - [x] 文件浏览对话框选择解释器可执行文件 27 | - [x] 处理解释器验证器返回结果,异常时弹出对话框要求用户自行检查确认 28 | - [x] ComboBox 中列出各解释器,与全局变量 `ALL_PY_ENVs` 联动 29 | - [ ] 右键菜单,可以将现有的环境 pin(固定),并保存到缓存文件中,后续启动时自动加载 30 | - [ ] 用户自定义选项输入框 31 | - [ ] 允许用户自行输入选项,添加到选项列表中 32 | - [x] ToolTip 提示,对应 PyInstaller 文档,提供完整帮助信息 33 | - [x] `PyInstaller` 选项参数详解表格(界面细节待优化) 34 | - [x] 主窗口状态栏显示软件版本 35 | - [ ] 「一键调试」模式,自动选择 `--onedir`、`--console`、`--debug` 等利于调试的选项 36 | - [ ] 用户设置窗口:若干个选项卡 37 | - [ ] 界面语言 38 | - [ ] PyInstaller 选项 39 | - [ ] 导入/导出选项 40 | - [ ] 插件(比如 Pillow 是否已安装、UPX 是否可用等) 41 | - [ ] 创建虚拟环境向导窗口,通过若干个步骤引导用户创建新的 venv 环境与安装依赖 42 | - [ ] 简洁视图(仅包含常用选项)/详细视图(包含所有 PyInstaller 可用选项) 切换 43 | 44 | ## 环境管理 45 | 46 | - [x] 能够使用系统中的其他 Python 环境来进行打包 47 | - [x] 创建「解释器环境类」,保存解释器路径等信息 48 | - [x] 识别 系统解释器/venv/Poetry/conda 等 49 | - [x] 将解释器环境保存在全局变量 `ALL_PY_ENVs` 中 50 | - [ ] 识别是否已安装 PyInstaller,未安装则提供「一键安装」 51 | - [ ] 创建新的虚拟环境 52 | - [ ] 使用 venv 创建新的虚拟环境 53 | - [ ] 查找用户的 `requirements.txt` 文件,分析依赖需求 54 | - [ ] 如未找到需求描述文件,则通过 [pipreqs](https://github.com/bndr/pipreqs) 分析生成 55 | - [ ] 安装依赖项与 PyInstaller 56 | 57 | ## 打包 58 | 59 | - [x] 选项参数获取 60 | - [x] 将参数拼接成完整调用命令 61 | - [x] 使用枚举值控制参数 62 | - [x] 优化拼接代码 63 | - [x] 调用 `PyInstaller` 子进程 64 | - [x] 使用 `QProcess` 替代 `subprocess` 以解决界面卡死问题 65 | - [x] 使用 Python 解释器直接运行命令,而不是 `PyInstaller.exe` 66 | - [x] 优化子进程相关代码,增强异常处理 67 | - [ ] 打包任务 68 | - [x] 创建打包任务,保存所有选项 69 | - [ ] 导出打包任务(json 或 yaml 格式)与加载打包任务(与 [*Auto-py-to-exe*](https://github.com/brentvollebregt/auto-py-to-exe) 兼容?) 70 | - [ ] 创建 [`.spec` 文件](https://pyinstaller.org/en/stable/man/pyi-makespec.html) 71 | 72 | ## 界面 73 | 74 | - [x] 实现跨平台功能 75 | - [x] 获取当前运行平台,保存至全局变量 `RUNTIME_INFO.platform` 中 76 | - [x] 定制各平台特有功能 77 | - [x] 使用 `qrc` 管理静态资源 78 | - [x] 翻译与国际化 79 | - [x] Qt 提供的界面文本自动翻译(待优化) 80 | - [x] 自实现的不同语言下功能差异,如“打开PyInstaller文档”指向不同的链接等 81 | 82 | ## 用户设置 83 | 84 | - [ ] 在用户家目录中创建配置文件夹与配置文件(YAML 格式),用于保存用户设置 85 | - [ ] 设置条目: 86 | - [ ] 界面语言 87 | - [ ] 是否使用 `--clean` `--noconfirm` 选项(默认自动使用) 88 | - [ ] 脚本导出格式(默认与当前平台对应,如 Windows 则为 PowerShell) 89 | 90 | ## 应用程序级 91 | 92 | - [x] 解决相对引用与作为包运行问题 93 | - [ ] 缓存目录 94 | - [ ] (?) 将用户使用过的 Python 环境保存到缓存文件中存储,下次启动时自动加载 95 | - [ ] `logging` 日志记录 Py2exe-GUI 的运行过程 96 | 97 | ## 美化 98 | 99 | - [ ] QSS 与美化 100 | - [ ] 动画效果 101 | - [ ] (?) 使用组件库 102 | 103 | ## 构建与分发 104 | 105 | 平台: 106 | 107 | - [ ] Windows 发行版 108 | - [ ] 创建[版本资源文件](https://muzing.gitbook.io/pyinstaller-docs-zh-cn/usage#bu-huo-windows-ban-ben-shu-ju) 109 | - [ ] Linux 发行版 110 | 111 | 分发方式: 112 | 113 | - [x] PyPI 114 | - [x] GitHub Releases 115 | - [ ] Arch Linux AUR 116 | - [ ] Ubuntu PPA 117 | 118 | ## 可选依赖 119 | 120 | - [ ] 在 PyPI 上提供“完整版”的发行,包含以下所有可选依赖项 121 | - [x] “普通版”也要有能力检测用户是否已经安装了某个/些可选依赖项并能协同工作 122 | - [ ] [Pillow](https://python-pillow.org/) 123 | - [x] 更精确可靠的图标文件格式识别(根据图片二进制内容判断,而不只是文件扩展名) 124 | - [ ] 在主窗口工具栏提供图像格式转换功能,将其他格式转换为平台对应的图标格式 125 | - [ ] [pipreqs](https://github.com/bndr/pipreqs):作为后备选项,分析用户项目的依赖项 126 | - [ ] [UPX](https://upx.github.io/) 127 | - [ ] [仅限 Windows 平台](https://muzing.gitbook.io/pyinstaller-docs-zh-cn/usage#shi-yong-upx) 128 | - [ ] 设法添加到运行时的环境变量 PATH 中 129 | - [ ] 或者,PyInstaller 命令中自动添加 `--upx-dir` 选项 130 | -------------------------------------------------------------------------------- /docs/source/images/Py2exe-GUI_v0.1.0_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/docs/source/images/Py2exe-GUI_v0.1.0_screenshot.png -------------------------------------------------------------------------------- /docs/source/images/Py2exe-GUI_v0.2.0_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/docs/source/images/Py2exe-GUI_v0.2.0_screenshot.png -------------------------------------------------------------------------------- /docs/source/images/Py2exe-GUI_v0.3.0_mainwindow_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/docs/source/images/Py2exe-GUI_v0.3.0_mainwindow_screenshot.png -------------------------------------------------------------------------------- /docs/source/images/Py2exe-GUI_v0.3.1_mainwindow_screenshot_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/docs/source/images/Py2exe-GUI_v0.3.1_mainwindow_screenshot_en.png -------------------------------------------------------------------------------- /docs/source/images/gplv3-127x51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/docs/source/images/gplv3-127x51.png -------------------------------------------------------------------------------- /docs/source/images/py2exe-gui_logo_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/docs/source/images/py2exe-gui_logo_big.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "py2exe-gui" 3 | version = "0.3.2" 4 | description = "GUI for PyInstaller, based on PySide6" 5 | keywords = ["PyInstaller", "GUI", "PySide6"] 6 | authors = ["Muzing "] 7 | license = "GPL-3.0-or-later" 8 | readme = ["README.md", "README_zh.md"] 9 | repository = "https://github.com/muziing/Py2exe-GUI" 10 | exclude = ["src/py2exe_gui/Resources/*"] 11 | include = ["src/py2exe_gui/Resources/COMPILED_RESOURCES.py"] 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Operating System :: Microsoft :: Windows", 15 | "Operating System :: POSIX :: Linux", 16 | "Operating System :: MacOS" 17 | ] 18 | 19 | [tool.poetry.urls] 20 | "Bug Tracker" = "https://github.com/muziing/Py2exe-GUI/issues" 21 | 22 | [tool.poetry.scripts] 23 | py2exe-gui = 'py2exe_gui.__main__:main' 24 | 25 | #[[tool.poetry.source]] 26 | #name = "tsinghua_mirror" 27 | #url = "https://pypi.tuna.tsinghua.edu.cn/simple/" 28 | #priority = "default" 29 | 30 | [tool.poetry.dependencies] 31 | python = ">=3.9.0, <3.14" 32 | PySide6-Essentials = "^6.2.0" 33 | pyyaml = ">6.0.0" 34 | 35 | Pillow = { version = ">10.0", optional = true } 36 | #pipreqs = { version = ">0.5.0", optional = true } 37 | 38 | [tool.poetry.extras] 39 | AddOns = ["Pillow", "pipreqs"] 40 | 41 | [tool.poetry.group.dev] 42 | optional = true 43 | 44 | [tool.poetry.group.dev.dependencies] 45 | pre-commit = ">3.5.0" 46 | black = ">24.4.0" 47 | isort = ">5.13.0" 48 | ruff = ">=0.5.0" 49 | mypy = ">1.10.0" 50 | pyinstaller = ">6.7.0" 51 | types-pyyaml = ">6.0.12.12" 52 | line-profiler = ">4.1.2" 53 | 54 | [tool.black] 55 | line-length = 88 56 | target-version = ["py311"] 57 | extend-exclude = "COMPILED_RESOURCES\\.py" 58 | 59 | [tool.isort] 60 | profile = "black" 61 | line_length = 88 62 | py_version = 311 63 | skip_glob = ["src/py2exe_gui/Resources/*"] 64 | 65 | [tool.mypy] 66 | python_version = "3.12" 67 | warn_return_any = true 68 | ignore_missing_imports = true 69 | check_untyped_defs = true 70 | exclude = ["COMPILED_RESOURCES\\.py$"] 71 | 72 | [tool.ruff] 73 | extend-exclude = ["COMPILED_RESOURCES.py"] 74 | 75 | [tool.ruff.lint] 76 | select = [ 77 | # Pyflakes 78 | "F", 79 | # pyupgrade 80 | "UP", 81 | # flake8-bugbear 82 | "B", 83 | # flake8-simplify 84 | "SIM", 85 | ] 86 | ignore = ["F401", "F403"] 87 | 88 | 89 | [build-system] 90 | requires = ["poetry-core>=1.0.0"] 91 | build-backend = "poetry.core.masonry.api" 92 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyside6-essentials >= 6.2.0 ; python_version >= "3.8" and python_version < "3.13" 2 | pyyaml>=6.0.0 ; python_version >= "3.8" and python_version < "3.13" 3 | -------------------------------------------------------------------------------- /src/Py2exe-GUI.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """程序入口脚本 5 | 由于整个程序作为单一 Python 包发布,直接运行 py2exe_gui.__main__.py 会导致相对导入错误 6 | 需要在包外留有这个显式的入口模块来提供“通过运行某个 .py 文件启动程序”功能和 PyInstaller 打包入口脚本 7 | 8 | Py2exe-GUI 启动方式: 9 | python Py2exe-GUI.py 10 | 或 11 | python -m py2exe_gui 12 | """ 13 | 14 | from py2exe_gui.__main__ import main 15 | 16 | main() 17 | -------------------------------------------------------------------------------- /src/py2exe_gui/Constants/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """各类常量、枚举值与全局变量""" 5 | 6 | from .app_constants import APP_URLs, AppConstant 7 | from .packaging_constants import PyInstOpt 8 | from .python_env_constants import PyEnvType 9 | from .runtime_info import RUNTIME_INFO, Platform, get_platform 10 | -------------------------------------------------------------------------------- /src/py2exe_gui/Constants/app_constants.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """应用程序级常量与部分全局变量""" 5 | 6 | __all__ = [ 7 | "APP_URLs", 8 | "AppConstant", 9 | ] 10 | 11 | from .runtime_info import RUNTIME_INFO 12 | 13 | APP_URLs = { 14 | "HOME_PAGE": "https://github.com/muziing/Py2exe-GUI", 15 | "BugTracker": "https://github.com/muziing/Py2exe-GUI/issues", 16 | "Pyinstaller_doc": "https://pyinstaller.org/", 17 | } 18 | 19 | if RUNTIME_INFO.language_code == "zh_CN": 20 | APP_URLs["Pyinstaller_doc"] = "https://muzing.gitbook.io/pyinstaller-docs-zh-cn/" 21 | 22 | 23 | class AppConstant: 24 | """应用程序级的常量""" 25 | 26 | NAME = "Py2exe-GUI" 27 | VERSION = "0.3.2" 28 | AUTHORS = ["Muzing "] 29 | LICENSE = "GPL-3.0-or-later" 30 | HOME_PAGE = APP_URLs["HOME_PAGE"] 31 | -------------------------------------------------------------------------------- /src/py2exe_gui/Constants/packaging_constants.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """PyInstaller 打包相关的常量""" 5 | 6 | __all__ = [ 7 | "PyInstOpt", 8 | ] 9 | 10 | import enum 11 | 12 | 13 | @enum.unique 14 | class PyInstOpt(enum.IntFlag): 15 | """PyInstaller 命令行选项枚举类""" 16 | 17 | script_path = enum.auto() 18 | icon_path = enum.auto() 19 | FD = enum.auto() 20 | console = enum.auto() 21 | out_name = enum.auto() 22 | add_data = enum.auto() 23 | add_binary = enum.auto() 24 | hidden_import = enum.auto() 25 | clean = enum.auto() 26 | -------------------------------------------------------------------------------- /src/py2exe_gui/Constants/python_env_constants.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """Python 环境相关常量""" 5 | 6 | __all__ = [ 7 | "PyEnvType", 8 | ] 9 | 10 | import enum 11 | 12 | 13 | @enum.unique 14 | class PyEnvType(enum.IntFlag): 15 | """Python 解释器(环境)类型,如系统解释器、venv 虚拟环境等""" 16 | 17 | system = enum.auto() # 系统解释器 18 | venv = enum.auto() # venv 虚拟环境 https://docs.python.org/3/library/venv.html 19 | poetry = enum.auto() # Poetry 环境 https://python-poetry.org/ 20 | conda = enum.auto() # conda 环境 https://docs.conda.io/en/latest/ 21 | unknown = enum.auto() # 未知 22 | -------------------------------------------------------------------------------- /src/py2exe_gui/Constants/runtime_info.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """运行时信息,存储于全局变量 `RUNTIME_INFO` 中 5 | 6 | 目前包括运行时平台(操作系统)、运行时语言(本地化)、运行时捆绑状态(是否已经被 PyInstaller 打包) 7 | """ 8 | 9 | __all__ = [ 10 | "Platform", 11 | "get_platform", 12 | "RuntimeInfo", 13 | "RUNTIME_INFO", 14 | ] 15 | 16 | import enum 17 | import sys 18 | from locale import getdefaultlocale 19 | from typing import NamedTuple, Optional 20 | 21 | 22 | @enum.unique 23 | class Platform(enum.Enum): 24 | """运行平台相关的常量 25 | 26 | 由于 enum.StrEnum 在 Python 3.11 才新增,为保持对低版本的支持,暂不使用 27 | """ 28 | 29 | windows = "Windows" 30 | linux = "Linux" 31 | macos = "macOS" 32 | others = "others" 33 | 34 | 35 | def get_platform() -> Platform: 36 | """辅助函数,用于获取当前运行的平台 37 | 38 | :return: platform 39 | """ 40 | 41 | if sys.platform.startswith("win32"): 42 | return Platform.windows 43 | elif sys.platform.startswith("linux"): 44 | return Platform.linux 45 | elif sys.platform.startswith("darwin"): 46 | return Platform.macos 47 | else: 48 | return Platform.others 49 | 50 | 51 | class RuntimeInfo(NamedTuple): 52 | """运行时信息数据结构类""" 53 | 54 | platform: Platform # 运行平台,Windows、macOS、Linux或其他 55 | language_code: Optional[str] # 语言环境,zh-CN、en-US 等 56 | is_bundled: bool # 是否在已被 PyInstaller 捆绑的冻结环境中运行 57 | 58 | 59 | # 虽然 locale.getdefaultlocale() 函数已被废弃[https://github.com/python/cpython/issues/90817], 60 | # 但仍然是目前唯一能在 Windows 平台正确获取语言编码的方式[https://github.com/python/cpython/issues/82986] 61 | # 当 Python 更新修复了这一问题后,将迁移至 locale.getlocale() 62 | language_code = getdefaultlocale()[0] # noqa 63 | 64 | # 判断当前是在普通 Python 环境中运行,还是已被 PyInstaller 捆绑/打包 65 | # https://pyinstaller.org/en/stable/runtime-information.html#run-time-information 66 | if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): 67 | is_bundled = True 68 | else: 69 | is_bundled = False 70 | 71 | # 全局变量 RUNTIME_INFO 72 | RUNTIME_INFO = RuntimeInfo(get_platform(), language_code, is_bundled) 73 | -------------------------------------------------------------------------------- /src/py2exe_gui/Core/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """本 package 主要包含处理 PyInstaller 参数、子进程等功能(后端)的类与函数""" 5 | 6 | from .packaging import Packaging 7 | from .packaging_task import PackagingTask 8 | from .validators import FilePathValidator, InterpreterValidator 9 | -------------------------------------------------------------------------------- /src/py2exe_gui/Core/packaging.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块包含实际执行打包子进程的类 `Packaging`""" 5 | 6 | __all__ = ["Packaging"] 7 | 8 | from pathlib import Path 9 | from typing import Optional 10 | 11 | from PySide6 import QtCore 12 | 13 | from ..Constants.packaging_constants import PyInstOpt 14 | from .subprocess_tool import SubProcessTool 15 | 16 | 17 | class Packaging(QtCore.QObject): 18 | """执行打包子进程的类,负责拼接命令选项、设置子进程工作目录、启动子进程等。 19 | 不负责输入参数的检查,输入参数检查由 PackagingTask 对象进行 20 | """ 21 | 22 | # 自定义信号 23 | args_settled = QtCore.Signal(list) # 所有选项完成设置,直接将命令行参数传出 24 | 25 | def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: 26 | """ 27 | :param parent: 父控件对象 28 | """ 29 | 30 | super().__init__(parent) 31 | 32 | self.args_dict: dict = dict.fromkeys(PyInstOpt, "") 33 | self._args: list[str] = [] # PyInstaller 命令 34 | self.subprocess: SubProcessTool = SubProcessTool("", parent=self) 35 | 36 | @QtCore.Slot(tuple) 37 | def set_pyinstaller_args(self, arg: tuple[PyInstOpt, str]) -> None: 38 | """解析传递来的PyInstaller运行参数,并添加至命令参数字典 39 | 40 | :param arg: 运行参数 41 | """ 42 | 43 | arg_key, arg_value = arg 44 | if isinstance(arg_key, PyInstOpt): 45 | self.args_dict[arg_key] = arg_value 46 | self._add_pyinstaller_args() 47 | self._set_subprocess_working_dir() 48 | 49 | def set_python_path(self, python_path: str) -> None: 50 | """为内部管理的 SubProcessTool 设置新的 Python 程序路径 51 | 52 | :param python_path: Python 可执行文件路径 53 | """ 54 | 55 | self.subprocess.set_program(python_path) 56 | 57 | def _add_pyinstaller_args(self) -> None: 58 | """将命令参数字典中的参数按顺序添加到PyInstaller命令参数列表中""" 59 | 60 | self._args = [] # 避免重复添加 61 | 62 | self._args.append(self.args_dict[PyInstOpt.script_path]) 63 | 64 | if self.args_dict[PyInstOpt.icon_path]: 65 | self._args.extend(["--icon", self.args_dict[PyInstOpt.icon_path]]) 66 | 67 | if self.args_dict[PyInstOpt.add_data]: 68 | for item in self.args_dict[PyInstOpt.add_data]: 69 | self._args.extend(["--add-data", f"{item[0]}:{item[1]}"]) 70 | 71 | if self.args_dict[PyInstOpt.add_binary]: 72 | for item in self.args_dict[PyInstOpt.add_binary]: 73 | self._args.extend(["--add-binary", f"{item[0]}:{item[1]}"]) 74 | 75 | if self.args_dict[PyInstOpt.FD]: 76 | self._args.append(self.args_dict[PyInstOpt.FD]) 77 | 78 | if self.args_dict[PyInstOpt.console]: 79 | self._args.append(self.args_dict[PyInstOpt.console]) 80 | 81 | if self.args_dict[PyInstOpt.hidden_import]: 82 | for item in self.args_dict[PyInstOpt.hidden_import]: 83 | self._args.extend(["--hidden-import", item]) 84 | 85 | if self.args_dict[PyInstOpt.out_name]: 86 | self._args.extend(["--name", self.args_dict[PyInstOpt.out_name]]) 87 | 88 | if self.args_dict[PyInstOpt.clean]: 89 | self._args.append(self.args_dict[PyInstOpt.clean]) 90 | 91 | self.args_settled.emit(self._args) 92 | 93 | def _set_subprocess_working_dir(self) -> None: 94 | """设置子进程工作目录""" 95 | 96 | script_path = self.args_dict[PyInstOpt.script_path] 97 | # 工作目录设置为脚本所在目录 98 | self.subprocess.set_working_dir(str(Path(script_path).parent)) 99 | 100 | def run_packaging_process(self) -> None: 101 | """使用给定的参数启动打包子进程""" 102 | 103 | # 从 Python 内启动 Pyinstaller, 104 | # 参见 https://pyinstaller.org/en/stable/usage.html#running-pyinstaller-from-python-code 105 | cmd = [ 106 | "-c", 107 | f"import PyInstaller.__main__;PyInstaller.__main__.run({self._args})", 108 | ] 109 | 110 | self.subprocess.set_arguments(cmd) 111 | self.subprocess.start_process( 112 | time_out=500, mode=QtCore.QIODeviceBase.OpenModeFlag.ReadOnly 113 | ) 114 | -------------------------------------------------------------------------------- /src/py2exe_gui/Core/packaging_task.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块主要包含打包任务类 `PackagingTask`,用于进行选项值检查和保存最终使用的选项 5 | 6 | `PackagingTask.using_option` 是一个数据类型为 dict[PyInstOpt, Any] 的字典,存储着当前确认使用的所有选项 7 | """ 8 | 9 | __all__ = ["PackagingTask"] 10 | 11 | from pathlib import Path 12 | from typing import Any, Optional 13 | 14 | from PySide6 import QtCore 15 | 16 | from ..Constants import PyInstOpt 17 | from ..Utilities import PyEnv 18 | from .validators import FilePathValidator 19 | 20 | 21 | class PackagingTask(QtCore.QObject): 22 | """打包任务类,处理用户输入 23 | 24 | 接收来自界面的用户输入操作并处理,将结果反馈给界面和实际执行打包子进程的 `Packaging` 对象 25 | 在实例属性 `pyenv` 中保存目前设置的 Python 环境; 26 | 在实例属性 `using_option` 中保存目前设置的所有参数值; 27 | """ 28 | 29 | # 自定义信号 30 | option_set = QtCore.Signal(tuple) # 用户输入选项通过了验证,已设置为打包选项; 31 | # option_set 实际类型为 tuple[PyInstOpt, Any] 32 | option_error = QtCore.Signal(PyInstOpt) # 用户输入选项有误,需要进一步处理 33 | ready_to_pack = QtCore.Signal(bool) # 是否已经可以运行该打包任务 34 | 35 | def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: 36 | """ 37 | :param parent: 父控件对象 38 | """ 39 | 40 | super().__init__(parent) 41 | 42 | # 保存打包时使用的 Python 环境 43 | self.pyenv: Optional[PyEnv] = None 44 | 45 | # 保存所有参数值,None 表示未设置 46 | self.using_option: dict[PyInstOpt, Any] = {value: None for value in PyInstOpt} 47 | # self.using_option = { 48 | # PyInstOpt.script_path: Path, 49 | # PyInstOpt.icon_path: Path, 50 | # PyInstOpt.FD: bool, 51 | # PyInstOpt.console: str, 52 | # PyInstOpt.out_name: str, 53 | # PyInstOpt.add_data: list[tuple[Path, str]], 54 | # PyInstOpt.add_binary: list[tuple[Path, str]], 55 | # PyInstOpt.hidden_import: list[str], 56 | # PyInstOpt.clean: bool, 57 | # } 58 | 59 | @QtCore.Slot(tuple) 60 | def on_opt_selected(self, option: tuple[PyInstOpt, Any]) -> None: 61 | """槽函数,处理用户在界面选择的打包选项,进行有效性验证并保存 62 | 63 | :param option: 选项,应为二元素元组,且其中的第一项为 `PyInstOpt` 枚举值 64 | :raise TypeError: 如果传入的 option 参数第一项不是有效的 PyInstOpt 成员,则抛出类型错误 65 | """ 66 | 67 | arg_key, arg_value = option 68 | 69 | # 进行有效性验证,有效则保存并发射option_set信号,无效则发射option_error信号 70 | if arg_key == PyInstOpt.script_path: 71 | script_path = Path(arg_value) 72 | if FilePathValidator.validate_script(script_path): 73 | self.using_option[PyInstOpt.script_path] = script_path 74 | self.ready_to_pack.emit(True) 75 | self.option_set.emit(option) 76 | # 输出名默认与脚本名相同 77 | self.using_option[PyInstOpt.out_name] = script_path.stem 78 | self.option_set.emit((PyInstOpt.out_name, script_path.stem)) 79 | else: 80 | self.ready_to_pack.emit(False) 81 | self.option_error.emit(arg_key) 82 | 83 | elif arg_key == PyInstOpt.icon_path: 84 | icon_path = Path(arg_value) 85 | if FilePathValidator.validate_icon(icon_path): 86 | self.using_option[PyInstOpt.icon_path] = icon_path 87 | self.option_set.emit(option) 88 | else: 89 | self.option_error.emit(arg_key) 90 | 91 | elif isinstance(arg_key, PyInstOpt): 92 | # 其他不需要进行检查的选项,直接保存与发射完成设置信号 93 | self.using_option[arg_key] = arg_value 94 | self.option_set.emit(option) 95 | 96 | else: 97 | raise TypeError(f"'{arg_key}' is not a instance of {PyInstOpt}.") 98 | -------------------------------------------------------------------------------- /src/py2exe_gui/Core/subprocess_tool.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块包含辅助 QProcess 使用的工具类 `SubProcessTool` 5 | 6 | 待考量:是否有必要使用此类,还是仅需使用其他技巧创建单例QProcess、自行处理信号 7 | """ 8 | 9 | __all__ = ["SubProcessTool"] 10 | 11 | from collections.abc import Sequence 12 | from enum import IntEnum 13 | from pathlib import Path 14 | from typing import Optional, Union 15 | from warnings import warn 16 | 17 | from PySide6.QtCore import QIODeviceBase, QObject, QProcess, Signal 18 | 19 | from ..Utilities.open_qfile import qba_to_str 20 | 21 | 22 | class SubProcessTool(QObject): 23 | """辅助 QProcess 使用的工具类,将所有直接对子进程进行的操作都封装在此类中""" 24 | 25 | # 自定义信号,类型为 tuple[SubProcessTool.OutputType, str] 26 | output = Signal(tuple) 27 | 28 | # output_types 29 | class OutputType(IntEnum): 30 | """枚举值:输出类型""" 31 | 32 | STATE = 1 33 | STDOUT = 2 34 | STDERR = 3 35 | STARTED = 4 36 | FINISHED = 5 37 | ERROR = 6 38 | 39 | def __init__( 40 | self, 41 | program: str, 42 | *, 43 | parent: Optional[QObject] = None, 44 | arguments: Sequence[str] = (), 45 | working_directory: str = "./", 46 | ) -> None: 47 | """ 48 | :param parent: 父对象 49 | :param program: 待运行的子进程 50 | :param arguments: 运行参数 51 | :param working_directory: 子进程工作目录 52 | """ 53 | 54 | super().__init__(parent) 55 | 56 | self.program: str = program 57 | self._arguments: Sequence[str] = arguments 58 | self._working_directory: str = working_directory 59 | self._process: Optional[QProcess] = None 60 | self.exit_code: int = 0 61 | self.exit_status: QProcess.ExitStatus = QProcess.ExitStatus.NormalExit 62 | 63 | def _connect_signals(self) -> None: 64 | """连接信号""" 65 | 66 | self._process.stateChanged.connect(self._handle_state) # type: ignore 67 | self._process.readyReadStandardOutput.connect(self._handle_stdout) # type: ignore 68 | self._process.readyReadStandardError.connect(self._handle_stderr) # type: ignore 69 | self._process.started.connect(self._process_started) # type: ignore 70 | self._process.finished.connect(self._process_finished) # type: ignore 71 | self._process.errorOccurred.connect(self._handle_error) # type: ignore 72 | 73 | def start_process( 74 | self, 75 | *, 76 | mode: QIODeviceBase.OpenModeFlag = QIODeviceBase.OpenModeFlag.ReadWrite, 77 | time_out: int = 1000, 78 | ) -> bool: 79 | """创建并启动子进程,有阻塞 80 | 81 | :param mode: 设备打开的模式 82 | :param time_out: 启动进程超时时间(单位为毫秒) 83 | :return: 是否成功启动 84 | """ 85 | 86 | if self._process is None: # 防止在子进程运行结束前重复启动 87 | self._process = QProcess(self) 88 | self._connect_signals() 89 | self._process.setWorkingDirectory(self._working_directory) 90 | self._process.start(self.program, self._arguments, mode) 91 | return self._process.waitForStarted( 92 | time_out 93 | ) # 阻塞,直到成功启动子进程或超时 94 | return False 95 | 96 | def abort_process(self, timeout: int = 5000) -> bool: 97 | """尝试中止子进程,超时后杀死子进程。若子进程没有运行,则什么都不做。 98 | 99 | :param timeout: 超时时间,单位为毫秒 100 | :return: 子进程是否已结束 101 | """ 102 | 103 | if self._process: 104 | self._process.terminate() 105 | is_finished = self._process.waitForFinished( 106 | timeout 107 | ) # 阻塞,直到进程终止或超时 108 | if not is_finished: 109 | self._process.kill() # 超时后杀死子进程 110 | return is_finished 111 | else: 112 | # 如果子进程没有运行,则认为已结束 113 | return True 114 | 115 | def set_program(self, program: str) -> None: 116 | """设置子进程程序 117 | 118 | :param program: 程序名称 119 | """ 120 | 121 | self.program = program 122 | 123 | def set_arguments(self, arguments: Sequence[str]) -> None: 124 | """设置子进程参数 125 | 126 | :param arguments: 参数列表 127 | """ 128 | 129 | self._arguments = arguments 130 | 131 | def set_working_dir(self, work_dir: Union[str, Path]) -> bool: 132 | """设置子进程工作目录 133 | 134 | :param work_dir: 工作目录 135 | :return: 是否设置成功 136 | """ 137 | 138 | working_dir = Path(work_dir) 139 | if working_dir.is_dir(): 140 | self._working_directory = str(working_dir.absolute()) 141 | return True 142 | else: 143 | return False 144 | 145 | def _process_started(self) -> None: 146 | """处理子进程开始的槽""" 147 | 148 | self.output.emit((self.OutputType.STARTED, "started")) 149 | 150 | def _process_finished(self, code: int, status: QProcess.ExitStatus) -> None: 151 | """处理子进程结束的槽 152 | 153 | :param code: 退出码 154 | :param status: 退出状态 155 | """ 156 | 157 | self.exit_code = code 158 | self.exit_status = status 159 | self.output.emit((self.OutputType.FINISHED, str(code))) 160 | self._process = None 161 | 162 | def _handle_stdout(self) -> None: 163 | """处理标准输出的槽""" 164 | 165 | if self._process: 166 | data = self._process.readAllStandardOutput() 167 | stdout = qba_to_str(data) 168 | self.output.emit((self.OutputType.STDOUT, stdout)) 169 | 170 | def _handle_stderr(self) -> None: 171 | """处理标准错误的槽""" 172 | 173 | if self._process: 174 | data = self._process.readAllStandardError() 175 | stderr = qba_to_str(data) 176 | self.output.emit((self.OutputType.STDERR, stderr)) 177 | 178 | def _handle_state(self, state: QProcess.ProcessState) -> None: 179 | """将子进程运行状态转换为易读形式 180 | 181 | :param state: 进程运行状态 182 | """ 183 | 184 | states = { 185 | QProcess.ProcessState.NotRunning: "The process is not running.", 186 | QProcess.ProcessState.Starting: "The process is starting...", 187 | QProcess.ProcessState.Running: "The process is running...", 188 | } 189 | state_name = states[state] 190 | self.output.emit((self.OutputType.STATE, state_name)) 191 | 192 | def _handle_error(self, error: QProcess.ProcessError) -> None: 193 | """处理子进程错误 194 | 195 | :param error: 子进程错误类型 196 | """ 197 | 198 | process_error = { 199 | QProcess.ProcessError.FailedToStart: "The process failed to start.", 200 | QProcess.ProcessError.Crashed: "The process has crashed.", 201 | QProcess.ProcessError.Timedout: "The process has timed out.", 202 | QProcess.ProcessError.WriteError: "A write error occurred in the process.", 203 | QProcess.ProcessError.ReadError: "A read error occurred in the process", 204 | QProcess.ProcessError.UnknownError: "An unknown error has occurred in the process.", 205 | } 206 | error_type = process_error[error] 207 | 208 | if self._process: 209 | self.abort_process(0) 210 | self.output.emit((self.OutputType.ERROR, error_type)) 211 | warn(error_type, category=RuntimeWarning, stacklevel=3) 212 | self._process = None 213 | -------------------------------------------------------------------------------- /src/py2exe_gui/Core/validators.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块包含数个验证器类,用于验证用户输入是否有效 5 | 6 | `FilePathValidator.validate_script()` 用于验证用户给定的路径是否为有效的 Python 脚本; 7 | `FilePathValidator.validate_icon()` 用于验证用户给定的图标文件是否有效; 8 | `InterpreterValidator.validate()` 用于验证用户给定的路径是否为有效的 Python 解释器可执行文件; 9 | """ 10 | 11 | __all__ = [ 12 | "FilePathValidator", 13 | "InterpreterValidator", 14 | ] 15 | 16 | import os 17 | import subprocess 18 | import warnings 19 | from importlib import util as importlib_util 20 | from pathlib import Path 21 | from typing import Union 22 | 23 | from ..Constants import RUNTIME_INFO, Platform 24 | 25 | 26 | class FilePathValidator: 27 | """根据给定的路径验证文件的有效性""" 28 | 29 | @classmethod 30 | def validate_script(cls, script_path: Union[str, Path]) -> bool: 31 | """验证脚本路径是否有效 32 | 33 | :param script_path: 脚本路径 34 | :return: 脚本路径是否有效 35 | """ 36 | 37 | path = Path(script_path) 38 | 39 | return path.exists() and path.is_file() and os.access(script_path, os.R_OK) 40 | 41 | @classmethod 42 | def validate_icon(cls, icon_path: Union[str, Path]) -> bool: 43 | """验证图标路径是否有效 44 | 45 | :param icon_path: 图标路径 46 | :return: 图标是否有效 47 | """ 48 | 49 | path = Path(icon_path) 50 | 51 | if not (path.exists() and path.is_file() and os.access(icon_path, os.R_OK)): 52 | return False 53 | 54 | if importlib_util.find_spec("PIL") is not None: 55 | # 如果安装了可选依赖 Pillow,则进行更精确的判断 56 | from PIL import Image, UnidentifiedImageError 57 | 58 | try: 59 | with Image.open(path) as img: 60 | img_format: str = img.format 61 | except UnidentifiedImageError: 62 | return False 63 | else: 64 | if img_format is None: 65 | return False 66 | elif RUNTIME_INFO.platform == Platform.windows: 67 | return img_format == "ICO" 68 | elif RUNTIME_INFO.platform == Platform.macos: 69 | return img_format == "ICNS" 70 | 71 | else: 72 | # 若未安装 Pillow,则简单地用文件扩展名判断 73 | if RUNTIME_INFO.platform == Platform.windows: 74 | return path.suffix == ".ico" 75 | elif RUNTIME_INFO.platform == Platform.macos: 76 | return path.suffix == ".icns" 77 | 78 | # 如果以上所有检查项均通过,默认返回 True 79 | return True 80 | 81 | 82 | class InterpreterValidator: 83 | """验证给定的可执行文件是否为有效的Python解释器""" 84 | 85 | @classmethod 86 | def validate(cls, itp_path: Union[str, Path, os.PathLike[str]]) -> bool: 87 | """验证 `path` 是否指向有效的Python解释器 88 | 89 | :param itp_path: 文件路径 90 | :return: 是否有效 91 | """ 92 | 93 | path = Path(itp_path).absolute() 94 | 95 | if not (path.exists() and path.is_file() and os.access(path, os.X_OK)): 96 | return False 97 | 98 | # 尝试将该文件作为Python解释器运行 99 | subprocess_args = [str(path), "-c", "import sys"] 100 | try: 101 | subprocess.run(args=subprocess_args, check=True) 102 | except subprocess.SubprocessError: 103 | return False 104 | except OSError as e: 105 | warnings.warn( 106 | f"Failed to start the Python interpreter validation subprocess for {path}: {e}", 107 | Warning, 108 | stacklevel=2, 109 | ) 110 | return False 111 | else: 112 | return True 113 | -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Icons/Py2exe-GUI_icon_72px.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/src/py2exe_gui/Resources/Icons/Py2exe-GUI_icon_72px.ico -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Icons/Py2exe-GUI_icon_72px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/src/py2exe_gui/Resources/Icons/Py2exe-GUI_icon_72px.png -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Icons/Python-logo-cyan-72px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/src/py2exe_gui/Resources/Icons/Python-logo-cyan-72px.png -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Icons/Python_128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/src/py2exe_gui/Resources/Icons/Python_128px.png -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Icons/conda-icon_200px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/src/py2exe_gui/Resources/Icons/conda-icon_200px.png -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Icons/conda-icon_72px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/src/py2exe_gui/Resources/Icons/conda-icon_72px.png -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Icons/poetry-icon_128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/src/py2exe_gui/Resources/Icons/poetry-icon_128px.png -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Icons/pyinstaller-icon-console_128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/src/py2exe_gui/Resources/Icons/pyinstaller-icon-console_128px.png -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Texts/About_en.md: -------------------------------------------------------------------------------- 1 | # About Py2exe-GUI 2 | 3 | Py2exe-GUI is a graphical cross-platform packaging tool based on [PyInstaller](https://pyinstaller.org/) that enables easy packaging of Python code into executable files. 4 | 5 | This program is **open source software**: all source code is hosted on [GitHub](https://github.com/muziing/Py2exe-GUI) and made available for distribution through [PyPI](https://pypi.org/project/py2exe-gui/). 6 | 7 | This program is **free software**: you can redistribute it and/or modify it under the terms of the [GNU General Public License](https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license) as published by the Free Software Foundation, either version 3 of the License, or any later version. 8 | 9 | Copyright © 2022-2024 [Muzing \](https://muzing.top/about) 10 | -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Texts/About_zh_CN.md: -------------------------------------------------------------------------------- 1 | ## 关于 Py2exe-GUI 2 | 3 | Py2exe-GUI 是一个基于 [PyInstaller](https://pyinstaller.org/) 的图形化跨平台打包工具,便于将 Python 代码打包为可执行文件。 4 | 5 | 本程序为**开源软件**:所有源代码托管于 [GitHub](https://github.com/muziing/Py2exe-GUI),并通过 [PyPI](https://pypi.org/project/py2exe-gui/) 提供分发。 6 | 7 | 本程序为**自由软件**:在自由软件联盟发布的 [*GNU 通用公共许可协议(第3版或更新的版本)*](https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license) 的约束下,你可以对其进行再发布和/或修改。 8 | 9 | 版权所有 © 2022-2024 [Muzing \](https://muzing.top/about) 10 | -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Texts/pyinstaller_options_en.yaml: -------------------------------------------------------------------------------- 1 | options: 2 | 3 | # Options 4 | - option: "-h, --help" 5 | description: "show this help message and exit" 6 | platform: [all] 7 | 8 | - option: "-v, --version" 9 | description: "Show program version info and exit." 10 | platform: [all] 11 | 12 | - option: "--distpath DIR" 13 | description: "Where to put the bundled app (default: ./dist)" 14 | platform: [all] 15 | 16 | - option: "--workpath WORKPATH" 17 | description: "Where to put all the temporary work files, .log, .pyz and etc. (default: ./build)" 18 | platform: [all] 19 | 20 | - option: "-y, --noconfirm" 21 | description: "Replace output directory (default: SPECPATH/dist/SPECNAME) without asking for confirmation" 22 | platform: [all] 23 | 24 | - option: "--upx-dir UPX_DIR" 25 | description: "Path to UPX utility (default: search the execution path)" 26 | platform: [all] 27 | 28 | - option: "--clean" 29 | description: "Clean PyInstaller cache and remove temporary files before building." 30 | platform: [all] 31 | 32 | - option: "--log-level LEVEL" 33 | description: "Amount of detail in build-time console messages. LEVEL may be one of TRACE, DEBUG, INFO, WARN, DEPRECATION, ERROR, FATAL (default: INFO). Also settable via and overrides the PYI_LOG_LEVEL environment variable." 34 | platform: [all] 35 | 36 | 37 | # What To Generate 38 | - option: "-D, --onedir" 39 | description: "Create a one-folder bundle containing an executable (default)" 40 | platform: [all] 41 | 42 | - option: "-F, --onefile" 43 | description: "Create a one-file bundled executable." 44 | platform: [all] 45 | 46 | - option: "--specpath DIR" 47 | description: "Folder to store the generated spec file (default: current directory)" 48 | platform: [all] 49 | 50 | - option: "-n NAME, --name NAME" 51 | description: "Name to assign to the bundled app and spec file (default: first script’s basename)" 52 | platform: [all] 53 | 54 | - option: "--contents-directory CONTENTS_DIRECTORY" 55 | description: "For onedir builds only, specify the name of the directory in which all supporting files (i.e. everything except the executable itself) will be placed in. Use “.” to re-enable old onedir layout without contents directory." 56 | platform: [all] 57 | 58 | 59 | # What To Bundle, Where To Search 60 | - option: "--add-data SOURCE:DEST" 61 | description: "Additional data files or directories containing data files to be added to the application. The argument value should be in form of “source:dest_dir”, where source is the path to file (or directory) to be collected, dest_dir is the destination directory relative to the top-level application directory, and both paths are separated by a colon (:). To put a file in the top-level application directory, use . as a dest_dir. This option can be used multiple times." 62 | platform: [all] 63 | 64 | - option: "--add-binary SOURCE:DEST" 65 | description: "Additional binary files to be added to the executable. See the --add-data option for the format. This option can be used multiple times." 66 | platform: [all] 67 | 68 | - option: "-p DIR, --paths DIR" 69 | description: "A path to search for imports (like using PYTHONPATH). Multiple paths are allowed, separated by ':', or use this option multiple times. Equivalent to supplying the pathex argument in the spec file." 70 | platform: [all] 71 | 72 | - option: "--hidden-import MODULENAME, --hiddenimport MODULENAME" 73 | description: "Name an import not visible in the code of the script(s). This option can be used multiple times." 74 | platform: [all] 75 | 76 | - option: "--collect-submodules MODULENAME" 77 | description: "Collect all submodules from the specified package or module. This option can be used multiple times." 78 | platform: [all] 79 | 80 | - option: "--collect-data MODULENAME, --collect-datas MODULENAME" 81 | description: "Collect all data from the specified package or module. This option can be used multiple times." 82 | platform: [all] 83 | 84 | - option: "--collect-binaries MODULENAME" 85 | description: "Collect all binaries from the specified package or module. This option can be used multiple times." 86 | platform: [all] 87 | 88 | - option: "--collect-all MODULENAME" 89 | description: "Collect all submodules, data files, and binaries from the specified package or module. This option can be used multiple times." 90 | platform: [all] 91 | 92 | - option: "--copy-metadata PACKAGENAME" 93 | description: "Copy metadata for the specified package. This option can be used multiple times." 94 | platform: [all] 95 | 96 | - option: "--recursive-copy-metadata PACKAGENAME" 97 | description: "Copy metadata for the specified package and all its dependencies. This option can be used multiple times." 98 | platform: [all] 99 | 100 | - option: "--additional-hooks-dir HOOKSPATH" 101 | description: "An additional path to search for hooks. This option can be used multiple times." 102 | platform: [all] 103 | 104 | - option: "--runtime-hook RUNTIME_HOOKS" 105 | description: "Path to a custom runtime hook file. A runtime hook is code that is bundled with the executable and is executed before any other code or module to set up special features of the runtime environment. This option can be used multiple times." 106 | platform: [all] 107 | 108 | - option: "--exclude-module EXCLUDES" 109 | description: "Optional module or package (the Python name, not the path name) that will be ignored (as though it was not found). This option can be used multiple times." 110 | platform: [all] 111 | 112 | - option: "--splash IMAGE_FILE" 113 | description: "(EXPERIMENTAL) Add an splash screen with the image IMAGE_FILE to the application. The splash screen can display progress updates while unpacking." 114 | platform: [all] 115 | 116 | 117 | # How To Generate 118 | - option: "-d {all,imports,bootloader,noarchive}, --debug {all,imports,bootloader,noarchive}" 119 | description: "Provide assistance with debugging a frozen application. This argument may be provided multiple times to select several of the following options. - all: All three of the following options. - imports: specify the -v option to the underlying Python interpreter, causing it to print a message each time a module is initialized, showing the place (filename or built-in module) from which it is loaded. See https://docs.python.org/3/using/cmdline.html#id4. - bootloader: tell the bootloader to issue progress messages while initializing and starting the bundled app. Used to diagnose problems with missing imports. - noarchive: instead of storing all frozen Python source files as an archive inside the resulting executable, store them as files in the resulting output directory." 120 | platform: [all] 121 | 122 | - option: "--python-option PYTHON_OPTION" 123 | description: "Specify a command-line option to pass to the Python interpreter at runtime. Currently supports “v” (equivalent to “–debug imports”), “u”, “W ”, “X ”, and “hash_seed=”. For details, see the section “Specifying Python Interpreter Options” in PyInstaller manual." 124 | platform: [all] 125 | 126 | - option: "-s, --strip" 127 | description: "Apply a symbol-table strip to the executable and shared libs (not recommended for Windows)" 128 | platform: [all] 129 | 130 | - option: "--noupx" 131 | description: "Do not use UPX even if it is available (works differently between Windows and *nix)" 132 | platform: [all] 133 | 134 | - option: "--upx-exclude FILE" 135 | description: "Prevent a binary from being compressed when using upx. This is typically used if upx corrupts certain binaries during compression. FILE is the filename of the binary without path. This option can be used multiple times." 136 | platform: [all] 137 | 138 | 139 | # Windows And Mac Os X Specific Options 140 | - option: "-c, --console, --nowindowed" 141 | description: "Open a console window for standard i/o (default). On Windows this option has no effect if the first script is a ‘.pyw’ file." 142 | platform: [Windows, macOS] 143 | 144 | - option: "-w, --windowed, --noconsole" 145 | description: "Windows and Mac OS X: do not provide a console window for standard i/o. On Mac OS this also triggers building a Mac OS .app bundle. On Windows this option is automatically set if the first script is a ‘.pyw’ file. This option is ignored on *NIX systems." 146 | platform: [Windows, macOS] 147 | 148 | - option: "-i , --icon " 149 | description: "FILE.ico: apply the icon to a Windows executable. FILE.exe,ID: extract the icon with ID from an exe. FILE.icns: apply the icon to the .app bundle on Mac OS. If an image file is entered that isn’t in the platform format (ico on Windows, icns on Mac), PyInstaller tries to use Pillow to translate the icon into the correct format (if Pillow is installed). Use “NONE” to not apply any icon, thereby making the OS show some default (default: apply PyInstaller’s icon). This option can be used multiple times." 150 | platform: [Windows, macOS] 151 | 152 | - option: "--disable-windowed-traceback" 153 | description: "Disable traceback dump of unhandled exception in windowed (noconsole) mode (Windows and macOS only), and instead display a message that this feature is disabled." 154 | platform: [Windows, macOS] 155 | 156 | 157 | # Windows Specific Options 158 | - option: "--version-file FILE" 159 | description: "Add a version resource from FILE to the exe." 160 | platform: [Windows] 161 | 162 | - option: "-m , --manifest " 163 | description: "Add manifest FILE or XML to the exe." 164 | platform: [Windows] 165 | 166 | - option: "-r RESOURCE, --resource RESOURCE" 167 | description: "Add or update a resource to a Windows executable. The RESOURCE is one to four items, FILE[,TYPE[,NAME[,LANGUAGE]]]. FILE can be a data file or an exe/dll. For data files, at least TYPE and NAME must be specified. LANGUAGE defaults to 0 or may be specified as wildcard * to update all resources of the given TYPE and NAME. For exe/dll files, all resources from FILE will be added/updated to the final executable if TYPE, NAME and LANGUAGE are omitted or specified as wildcard *. This option can be used multiple times." 168 | platform: [Windows] 169 | 170 | - option: "--uac-admin" 171 | description: "Using this option creates a Manifest that will request elevation upon application start." 172 | platform: [Windows] 173 | 174 | - option: "--uac-uiaccess" 175 | description: "Using this option allows an elevated application to work with Remote Desktop." 176 | platform: [Windows] 177 | 178 | - option: "--hide-console {hide-late,minimize-late,hide-early,minimize-early}" 179 | description: "Windows only: in console-enabled executable, have bootloader automatically hide or minimize the console window if the program owns the console window (i.e., was not launched from an existing console window)." 180 | platform: [Windows] 181 | 182 | 183 | # Mac Os Specific Options 184 | - option: "--argv-emulation" 185 | description: "Enable argv emulation for macOS app bundles. If enabled, the initial open document/URL event is processed by the bootloader and the passed file paths or URLs are appended to sys.argv." 186 | platform: [macOS] 187 | 188 | - option: "--osx-bundle-identifier BUNDLE_IDENTIFIER" 189 | description: "Mac OS .app bundle identifier is used as the default unique program name for code signing purposes. The usual form is a hierarchical name in reverse DNS notation. For example: com.mycompany.department.appname (default: first script’s basename)" 190 | platform: [macOS] 191 | 192 | - option: "--target-architecture ARCH, --target-arch ARCH" 193 | description: "Target architecture (macOS only; valid values: x86_64, arm64, universal2). Enables switching between universal2 and single-arch version of frozen application (provided python installation supports the target architecture). If not target architecture is not specified, the current running architecture is targeted." 194 | platform: [macOS] 195 | 196 | - option: "--codesign-identity IDENTITY" 197 | description: "Code signing identity (macOS only). Use the provided identity to sign collected binaries and generated executable. If signing identity is not provided, ad- hoc signing is performed instead." 198 | platform: [macOS] 199 | 200 | - option: "--osx-entitlements-file FILENAME" 201 | description: "Entitlements file to use when code-signing the collected binaries (macOS only)." 202 | platform: [macOS] 203 | 204 | 205 | # Rarely Used Special Options 206 | - option: "--runtime-tmpdir PATH" 207 | description: "Where to extract libraries and support files in onefile-mode. If this option is given, the bootloader will ignore any temp-folder location defined by the run-time OS. The _MEIxxxxxx-folder will be created here. Please use this option only if you know what you are doing." 208 | platform: [all] 209 | 210 | - option: "--bootloader-ignore-signals" 211 | description: "Tell the bootloader to ignore signals rather than forwarding them to the child process. Useful in situations where for example a supervisor process signals both the bootloader and the child (e.g., via a process group) to avoid signalling the child twice." 212 | platform: [all] 213 | -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/Texts/pyinstaller_options_zh_CN.yaml: -------------------------------------------------------------------------------- 1 | options: 2 | 3 | # 选项 4 | - option: "-h, --help" 5 | description: "显示此帮助信息并退出。" 6 | platform: [all] 7 | 8 | - option: "-v, --version" 9 | description: "显示程序版本信息并退出。" 10 | platform: [all] 11 | 12 | - option: "--distpath DIR" 13 | description: "捆绑应用程序的放置位置(默认值:`./dist`)。" 14 | platform: [all] 15 | 16 | - option: "--workpath WORKPATH" 17 | description: "放置所有临时工作文件、`.log`、`.pyz` 等的位置(默认值:`./build`)。" 18 | platform: [all] 19 | 20 | - option: "-y, --noconfirm" 21 | description: "覆盖输出目录中的原有内容(默认值:`SPECPATH/dist/SPECNAME`),不请求确认。" 22 | platform: [all] 23 | 24 | - option: "--upx-dir UPX_DIR" 25 | description: "UPX 组件的位置(默认值:搜索可执行文件路径,即环境变量中的 `PATH`)。" 26 | platform: [all] 27 | 28 | - option: "--clean" 29 | description: "在构建之前,清理 PyInstaller 缓存并删除临时文件。" 30 | platform: [all] 31 | 32 | - option: "--log-level LEVEL" 33 | description: "编译时控制台信息的详细程度。LEVEL 可以是 `TRACE`、`DEBUG`、`INFO`、`WARN`、`DEPRECATION`、`ERROR`、`FATAL` 之一(默认值:`INFO`)。也可以通过 `PYI_LOG_LEVEL` 环境变量进行覆盖设置。" 34 | platform: [all] 35 | 36 | 37 | # 生成什么 38 | - option: "-D, --onedir" 39 | description: "创建包含一个可执行文件的单文件夹捆绑包(默认值)。" 40 | platform: [all] 41 | 42 | - option: "-F, --onefile" 43 | description: "创建单文件捆绑的可执行文件。" 44 | platform: [all] 45 | 46 | - option: "--specpath DIR" 47 | description: "存储生成的 spec 文件的文件夹(默认值:当前目录)。" 48 | platform: [all] 49 | 50 | - option: "-n NAME, --name NAME" 51 | description: "为捆绑的应用程序和 spec 文件指定的名称(默认值:第一个脚本的名称)。" 52 | platform: [all] 53 | 54 | - option: "--contents-directory CONTENTS_DIRECTORY" 55 | description: "仅适用于单文件夹构建。指定存放所有支持文件(即除可执行文件本身外的所有文件)的目录名称。使用 `.` 来重新启用旧的 onedir 布局,但不包含内容目录。" 56 | platform: [all] 57 | 58 | 59 | # 捆绑什么,在何处搜索 60 | - option: "--add-data SOURCE:DEST" 61 | description: "要添加到应用程序中的附加数据文件或包含数据文件的目录。参数值的形式应为 \"source:dest_dir\",其中 source 是要收集的文件(或目录)的路径,dest_dir 是相对于应用程序顶层目录的目标目录,两个路径之间用冒号(`:`)分隔。要将文件放入应用程序顶层目录,使用 `.` 作为 dest_dir。该选项可多次使用。" 62 | platform: [all] 63 | 64 | - option: "--add-binary SOURCE:DEST" 65 | description: "要添加到可执行文件中的其他二进制文件。格式参考 `--add-data` 选项。该选项可多次使用。" 66 | platform: [all] 67 | 68 | - option: "-p DIR, --paths DIR" 69 | description: "搜索导入的路径(如使用 PYTHONPATH)。允许使用多个路径,以 `:` 分隔,或多次使用该选项。相当于在 spec 文件中提供 pathex 参数。" 70 | platform: [all] 71 | 72 | - option: "--hidden-import MODULENAME, --hiddenimport MODULENAME" 73 | description: "指明脚本中不可见的导入关系。该选项可多次使用。" 74 | platform: [all] 75 | 76 | - option: "--collect-submodules MODULENAME" 77 | description: "收集指定软件包或模块的所有子模块。该选项可多次使用。" 78 | platform: [all] 79 | 80 | - option: "--collect-data MODULENAME, --collect-datas MODULENAME" 81 | description: "收集指定软件包或模块的所有数据。该选项可多次使用。" 82 | platform: [all] 83 | 84 | - option: "--collect-binaries MODULENAME" 85 | description: "收集指定软件包或模块的所有二进制文件。该选项可多次使用。" 86 | platform: [all] 87 | 88 | - option: "--collect-all MODULENAME" 89 | description: "收集指定软件包或模块的所有子模块、数据文件和二进制文件。该选项可多次使用。" 90 | platform: [all] 91 | 92 | - option: "--copy-metadata PACKAGENAME" 93 | description: "复制指定包的元数据。该选项可多次使用。" 94 | platform: [all] 95 | 96 | - option: "--recursive-copy-metadata PACKAGENAME" 97 | description: "复制指定包及其所有依赖项的元数据。该选项可多次使用。" 98 | platform: [all] 99 | 100 | - option: "--additional-hooks-dir HOOKSPATH" 101 | description: "用于搜索钩子的附加路径。该选项可多次使用。" 102 | platform: [all] 103 | 104 | - option: "--runtime-hook RUNTIME_HOOKS" 105 | description: "自定义运行时钩子文件的路径。运行时钩子是与可执行文件捆绑在一起的代码,在其他代码或模块之前执行,以设置运行时环境的特殊功能。该选项可多次使用。" 106 | platform: [all] 107 | 108 | - option: "--exclude-module EXCLUDES" 109 | description: "将被忽略(就像没有被找到一样)的可选模块或软件包(Python 名称,不是路径名)。该选项可多次使用。" 110 | platform: [all] 111 | 112 | - option: "--splash IMAGE_FILE" 113 | description: "(实验性功能)为应用程序添加一个带有 IMAGE_FILE 图像的闪屏。闪屏可以在解压缩时显示进度更新。" 114 | platform: [all] 115 | 116 | 117 | # 如何生成 118 | - option: "-d {all,imports,bootloader,noarchive}, --debug {all,imports,bootloader,noarchive}" 119 | description: "辅助调试冻结应用程序。该参数可以提供多次以选择下面多个选项。- all:以下所有三个选项。- imports:向底层 Python 解释器指定 -v 选项,使其在每次初始化模块时打印一条信息,显示模块的加载位置(文件名或内置模块)。参考命令行选项 -v。- bootloader:告知 bootloader 在初始化和启动捆绑应用程序时发布进度消息,用于诊断导入丢失的问题。- noarchive:不将所有冻结的 Python 源文件作为压缩归档存储在生成的可执行文件中,而是将它们作为文件存储在生成的输出目录中。" 120 | platform: [all] 121 | 122 | - option: "--python-option PYTHON_OPTION" 123 | description: "指定运行时传递给 Python 解释器的命令行选项。目前支持 “v”(相当于 “–debug imports”)、“u”、“W ”、“X ” 与 “hash_seed=”。详情参阅指定 Python 解释器选项。" 124 | platform: [all] 125 | 126 | - option: "-s, --strip" 127 | description: "对可执行文件和共享库应用符号条带表(symbol-table strip)。不建议在 Windows 环境下使用。" 128 | platform: [all] 129 | 130 | - option: "--noupx" 131 | description: "即使有可用的 UPX 也不要使用。在 Windows 和 *nix 下效果有所不同。" 132 | platform: [all] 133 | 134 | - option: "--upx-exclude FILE" 135 | description: "防止二进制文件在使用 upx 时被压缩。如果 upx 在压缩过程中破坏了某些二进制文件,通常可以使用此功能。FILE 是二进制文件不含路径的文件名。该选项可多次使用。" 136 | platform: [all] 137 | 138 | 139 | # Windows 与 macOS 专用选项 140 | - option: "-c, --console, --nowindowed" 141 | description: "为标准 i/o 打开一个控制台窗口(默认选项)。在 Windows 中,如果第一个脚本是 ‘.pyw’ 文件,则此选项无效。" 142 | platform: [Windows, macOS] 143 | 144 | - option: "-w, --windowed, --noconsole" 145 | description: "不提供用于标准 i/o 的控制台窗口。在 macOS 上,这也会触发构建一个 .app 捆绑程序。在 Windows 系统中,如果第一个脚本是 ‘.pyw’ 文件,则会自动设置该选项。在 *NIX 系统上,该选项将被忽略。" 146 | platform: [Windows, macOS] 147 | 148 | - option: "-i , --icon " 149 | description: "FILE.ico:将图标应用于 Windows 可执行文件。FILE.exe,ID:从一个 exe 文件中提取带有 ID 的图标。FILE.icns:将图标应用到 macOS 的 .app 捆绑程序中。如果输入的图像文件不是对应平台的格式(Windows 为 ico,Mac 为 icns),PyInstaller 会尝试使用 Pillow 将图标翻译成正确的格式(如果安装了 Pillow)。使用 “NONE” 不应用任何图标,从而使操作系统显示默认图标(默认值:使用 PyInstaller 的图标)。该选项可多次使用。" 150 | platform: [Windows, macOS] 151 | 152 | - option: "--disable-windowed-traceback" 153 | description: "禁用窗口(noconsole)模式下未处理异常的回溯转储,并显示禁用此功能的信息。" 154 | platform: [Windows, macOS] 155 | 156 | 157 | # Windows 专用选项 158 | - option: "--version-file FILE" 159 | description: "将 FILE 中的版本资源添加到 exe 中。" 160 | platform: [Windows] 161 | 162 | - option: "-m , --manifest " 163 | description: "将 manifest FILE 或 XML 添加到 exe 中。" 164 | platform: [Windows] 165 | 166 | - option: "-r RESOURCE, --resource RESOURCE" 167 | description: "为 Windows 可执行文件添加或更新资源。RESOURCE 包含一到四个条目:FILE[,TYPE[,NAME[,LANGUAGE]]]。FILE 可以是数据文件或 exe/dll。对于数据文件,则必须指定 TYPE 和 NAME。LANGUAGE 默认为 0,也可以指定为通配符 `*`,以更新给定 TYPE 和 NAME 的所有资源。对于 exe/dll 文件,如果省略 TYPE、NAME 和 LANGUAGE 或将其指定为通配符 `*`,则 FILE 中的所有资源都将添加/更新到最终可执行文件中。该选项可多次使用。" 168 | platform: [Windows] 169 | 170 | - option: "--uac-admin" 171 | description: "使用该选项可创建一个 Manifest,在应用程序启动时请求提升权限。" 172 | platform: [Windows] 173 | 174 | - option: "--uac-uiaccess" 175 | description: "使用此选项,可让提升后的应用程序与远程桌面协同工作。" 176 | platform: [Windows] 177 | 178 | - option: "--hide-console {hide-late,minimize-late,hide-early,minimize-early}" 179 | description: "在启用控制台的可执行文件中,如果程序有控制台窗口(即,不是从一个现有的控制台窗口启动的),bootloader 会自动隐藏或最小化控制台窗口。" 180 | platform: [Windows] 181 | 182 | 183 | # macOS 专用选项 184 | - option: "--argv-emulation" 185 | description: "启用 macOS 应用程序捆绑包的 argv 仿真。如果启用,初始打开文档/URL 事件将由 bootloader 处理,并将传递的文件路径或 URL 附加到 sys.argv。" 186 | platform: [macOS] 187 | 188 | - option: "--osx-bundle-identifier BUNDLE_IDENTIFIER" 189 | description: "macOS `.app` 捆绑标识符用于代码签名的唯一程序名称。通常的形式是以反向 DNS 记法表示的分层名称。例如:com.mycompany.department.appname。(默认值:第一个脚本的名称)" 190 | platform: [macOS] 191 | 192 | - option: "--target-architecture ARCH, --target-arch ARCH" 193 | description: "目标架构。有效值:`x86_64`、`arm64`、`universal2`。启用冻结应用程序在 universal2 和 single-arch version 之间的切换(前提是 Python 安装支持目标架构)。如果为指定目标架构,则以当前运行的架构为目标。" 194 | platform: [macOS] 195 | 196 | - option: "--codesign-identity IDENTITY" 197 | description: "代码签名身份。使用提供的身份对收集的二进制文件和生成的可执行文件进行签名。如果未提供签名标识,则会执行临时签名。" 198 | platform: [macOS] 199 | 200 | - option: "--osx-entitlements-file FILENAME" 201 | description: "在对收集的二进制文件进行代码签名时使用的权限文件。" 202 | platform: [macOS] 203 | 204 | 205 | # 罕用的特殊选项 206 | - option: "--runtime-tmpdir PATH" 207 | description: "在单文件模式下提取库和支持文件的位置。如果给定此选项,bootloader 将忽略运行时操作系统定义的任何临时文件夹位置。将在此处创建 `_MEIxxxxxx` 文件夹。请仅在你知道自己在做什么的情况下使用该选项。" 208 | platform: [all] 209 | 210 | - option: "--bootloader-ignore-signals" 211 | description: "告知 bootloader 忽略信号,而不是将其转发给子进程。例如,在监督进程同时向 bootloader 和子进程发出信号(如,通过进程组)以避免向子进程发出两次信号的情况下就很有用。" 212 | platform: [all] 213 | -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/i18n/zh_CN.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muziing/Py2exe-GUI/4e3eeb0de09a0719f6be49f43c1207ea799b40aa/src/py2exe_gui/Resources/i18n/zh_CN.qm -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/i18n/zh_CN.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AboutDlg 6 | 7 | 8 | About 9 | 关于 10 | 11 | 12 | 13 | Can't open the About document, try to reinstall this program. 14 | 无法打开关于文档,请尝试重新获取本程序。 15 | 16 | 17 | 18 | AddDataWindow 19 | 20 | 21 | Add Files 22 | 添加文件 23 | 24 | 25 | 26 | &New 27 | 新建(&N) 28 | 29 | 30 | 31 | &Browse File 32 | 浏览文件(&B) 33 | 34 | 35 | 36 | Browse &Folder 37 | 浏览目录(&F) 38 | 39 | 40 | 41 | &Delete 42 | 删除(&D) 43 | 44 | 45 | 46 | &OK 47 | 确认(&O) 48 | 49 | 50 | 51 | &Cancel 52 | 取消(&C) 53 | 54 | 55 | 56 | ArgumentsBrowser 57 | 58 | 59 | Copy 60 | 复制 61 | 62 | 63 | 64 | Export 65 | 导出 66 | 67 | 68 | 69 | CenterWidget 70 | 71 | 72 | Python entry script path 73 | Python 入口脚本路径 74 | 75 | 76 | 77 | 78 | Browse 79 | 浏览 80 | 81 | 82 | 83 | Python script: 84 | 待打包脚本: 85 | 86 | 87 | 88 | App Name: 89 | 项目名称: 90 | 91 | 92 | 93 | Bundled app name 94 | 打包的应用程序名称 95 | 96 | 97 | 98 | One Folder/ One File: 99 | 单目录/单文件: 100 | 101 | 102 | 103 | One Folder 104 | 打包至单个目录 105 | 106 | 107 | 108 | One File 109 | 打包至单个文件 110 | 111 | 112 | 113 | 114 | Add Data Files 115 | 添加数据文件 116 | 117 | 118 | 119 | 120 | Add Binary Files 121 | 添加二进制文件 122 | 123 | 124 | 125 | Hidden Import 126 | 隐式导入 127 | 128 | 129 | 130 | Clean 131 | 清理 132 | 133 | 134 | 135 | Bundle! 136 | 打包! 137 | 138 | 139 | 140 | Bundling into one folder. 141 | 将打包至单个目录中。 142 | 143 | 144 | 145 | Bundling into one file. 146 | 将打包至单个文件中。 147 | 148 | 149 | 150 | Add data files updated. 151 | 添加数据文件已更新。 152 | 153 | 154 | 155 | Add binary files updated. 156 | 添加二进制文件已更新。 157 | 158 | 159 | 160 | Hidden import updated. 161 | 隐式导入已更新。 162 | 163 | 164 | 165 | Clean cache and remove temporary files before building. 166 | 构建前将清除缓存与临时目录。 167 | 168 | 169 | 170 | Will not delete cache and temporary files. 171 | 不会删除缓存与临时文件。 172 | 173 | 174 | 175 | Opened script path: 176 | 打开脚本路径: 177 | 178 | 179 | 180 | The app name has been set to: 181 | 已将项目名设置为: 182 | 183 | 184 | 185 | 186 | Error 187 | 错误 188 | 189 | 190 | 191 | The selection is not a valid Python script file, please reselect it! 192 | 选择的不是有效的Python脚本文件,请重新选择! 193 | 194 | 195 | 196 | Warning 197 | 警告 198 | 199 | 200 | 201 | Pyinstaller doesn't seem to be installed in this Python environment, still continue? 202 | 在该 Python 环境中似乎没有安装 Pyinstaller,是否仍要继续? 203 | 204 | 205 | 206 | The selection is not a valid Python interpreter, please reselect it! 207 | 选择的不是有效的Python解释器,请重新选择! 208 | 209 | 210 | 211 | IconFileDlg 212 | 213 | 214 | Icon Files (*.ico *.icns) 215 | 图标文件 (*.ico *.icns) 216 | 217 | 218 | 219 | All Files (*) 220 | 所有文件 (*) 221 | 222 | 223 | 224 | App Icon 225 | 图标 226 | 227 | 228 | 229 | Icon File 230 | 图标文件 231 | 232 | 233 | 234 | Open 235 | 打开 236 | 237 | 238 | 239 | Cancel 240 | 取消 241 | 242 | 243 | 244 | InterpreterFileDlg 245 | 246 | 247 | Python Interpreter 248 | Python 解释器 249 | 250 | 251 | 252 | Executable File 253 | 可执行文件 254 | 255 | 256 | 257 | Python Interpreter (python.exe) 258 | Python 解释器 (python.exe) 259 | 260 | 261 | 262 | Executable Files (*.exe) 263 | 可执行文件 (*.exe) 264 | 265 | 266 | 267 | 268 | All Files (*) 269 | 所有文件 (*) 270 | 271 | 272 | 273 | Python Interpreter (python3*) 274 | Python 解释器 (python3*) 275 | 276 | 277 | 278 | MainApp 279 | 280 | 281 | Ready. 282 | 就绪。 283 | 284 | 285 | 286 | MainWindow 287 | 288 | 289 | &File 290 | 文件(&F) 291 | 292 | 293 | 294 | Import Config From JSON File 295 | 从 JSON 文件中导入配置 296 | 297 | 298 | 299 | Export Config To JSON File 300 | 导出配置至 JSON 文件 301 | 302 | 303 | 304 | &Settings 305 | 设置(&S) 306 | 307 | 308 | 309 | &Exit 310 | 退出(&E) 311 | 312 | 313 | 314 | &Help 315 | 帮助(&H) 316 | 317 | 318 | 319 | PyInstaller Documentation 320 | PyInstaller 文档 321 | 322 | 323 | 324 | PyInstaller Options Details 325 | PyInstaller 选项详情 326 | 327 | 328 | 329 | Report Bugs 330 | 报告 Bug 331 | 332 | 333 | 334 | &About 335 | 关于(&A) 336 | 337 | 338 | 339 | About This Program 340 | 关于此程序 341 | 342 | 343 | 344 | About &Qt 345 | 关于 &Qt 346 | 347 | 348 | 349 | MultiItemEditWindow 350 | 351 | 352 | &New 353 | 新建(&N) 354 | 355 | 356 | 357 | &Delete 358 | 删除(&D) 359 | 360 | 361 | 362 | &OK 363 | 确认(&O) 364 | 365 | 366 | 367 | &Cancel 368 | 取消(&C) 369 | 370 | 371 | 372 | MultiPkgEditWindow 373 | 374 | 375 | &Browse packages 376 | 浏览包(&B) 377 | 378 | 379 | 380 | PkgBrowserDlg 381 | 382 | 383 | Installed Packages 384 | 已安装的包 385 | 386 | 387 | 388 | Name 389 | 这里专指软件包的名称 390 | 包名 391 | 392 | 393 | 394 | Version 395 | 版本 396 | 397 | 398 | 399 | PyinstallerOptionTable 400 | 401 | 402 | Option 403 | 选项 404 | 405 | 406 | 407 | Description 408 | 描述 409 | 410 | 411 | 412 | ScriptFileDlg 413 | 414 | 415 | Python Script (*.py *.pyw) 416 | Python 脚本文件 (*.py *.pyw) 417 | 418 | 419 | 420 | All Files (*) 421 | 所有文件 (*) 422 | 423 | 424 | 425 | Python Entry File 426 | Python入口文件 427 | 428 | 429 | 430 | Python File 431 | Python 文件 432 | 433 | 434 | 435 | Open 436 | 打开 437 | 438 | 439 | 440 | Cancel 441 | 取消 442 | 443 | 444 | 445 | SubProcessDlg 446 | 447 | 448 | Done! 449 | 此处专指PyInstaller运行完成。 450 | 打包完成! 451 | 452 | 453 | 454 | 455 | Open Dist 456 | 打开输出位置 457 | 458 | 459 | 460 | Execution ends, but an error occurs and the exit code is 461 | 运行结束,但有错误发生,退出码为 462 | 463 | 464 | 465 | 466 | 467 | Cancel 468 | 取消 469 | 470 | 471 | 472 | PyInstaller Error! 473 | PyInstaller 错误! 474 | 475 | 476 | 477 | PyInstaller subprocess output: 478 | PyInstaller 子进程输出: 479 | 480 | 481 | 482 | Please check if you have installed the correct version of PyInstaller or not. 483 | 请检查是否已经安装正确版本的 PyInstaller。 484 | 485 | 486 | 487 | 488 | Close 489 | 关闭 490 | 491 | 492 | 493 | WinMacCenterWidget 494 | 495 | 496 | App icon: 497 | 应用图标: 498 | 499 | 500 | 501 | Path to icon file 502 | 图标路径 503 | 504 | 505 | 506 | Browse 507 | 浏览 508 | 509 | 510 | 511 | Open a console window for standard I/O 512 | 为标准I/O启用终端 513 | 514 | 515 | 516 | Terminal will be enabled. 517 | 将为打包程序的 stdio 启用终端。 518 | 519 | 520 | 521 | Terminal will not be enabled. 522 | 不会为打包程序的 stdio 启用终端。 523 | 524 | 525 | 526 | Opened icon path: 527 | 打开的图标路径: 528 | 529 | 530 | 531 | Error 532 | 错误 533 | 534 | 535 | 536 | The selection is not a valid icon file, please re-select it! 537 | 选择的不是有效的图标文件,请重新选择! 538 | 539 | 540 | 541 | -------------------------------------------------------------------------------- /src/py2exe_gui/Resources/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icons/Py2exe-GUI_icon_72px.png 5 | Icons/Python_128px.png 6 | Icons/Python-logo-cyan-72px.png 7 | Icons/pyinstaller-icon-console_128px.png 8 | Icons/poetry-icon_128px.png 9 | Icons/conda-icon_72px.png 10 | 11 | 12 | i18n/zh_CN.qm 13 | 14 | 15 | Texts/About_zh_CN.md 16 | Texts/pyinstaller_options_zh_CN.yaml 17 | 18 | 19 | Texts/About_en.md 20 | Texts/pyinstaller_options_en.yaml 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/py2exe_gui/Utilities/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """公共基础功能类与函数""" 5 | 6 | from .open_qfile import QtFileOpen 7 | from .platform_specific_funcs import * 8 | from .python_env import ALL_PY_ENVs, PyEnv 9 | from .qobject_tr import QObjTr 10 | -------------------------------------------------------------------------------- /src/py2exe_gui/Utilities/open_qfile.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """将使用QFile读写文件包装成 Python 的 `with open() as` 风格 5 | 6 | 暂时仅实现了文本文件的读取方法 7 | """ 8 | 9 | __all__ = [ 10 | "qba_to_str", 11 | "QtFileOpen", 12 | ] 13 | __author__ = "Muzing" 14 | 15 | import locale 16 | import os 17 | import pathlib 18 | import warnings 19 | from sys import getdefaultencoding 20 | from typing import Optional, Union 21 | 22 | from PySide6.QtCore import QByteArray, QFile, QFileInfo, QIODevice 23 | 24 | 25 | def qba_to_str(qba: QByteArray, encoding: str = getdefaultencoding()) -> str: 26 | """将 QByteArray 转换为 str 27 | 28 | :param qba: QByteArray 字节串对象 29 | :param encoding: 使用何种编码方式解码,默认值为 sys.getdefaultencoding() 30 | :return: str 字符串 31 | """ 32 | 33 | return qba.data().decode(encoding=encoding) 34 | 35 | 36 | class QtFileOpen: 37 | """通过 QFile 读写文件的上下文管理器, 38 | 使与 Python 的 "with open() as" 语句风格统一 39 | 40 | 使用举例: 41 | 42 | with QtFileOpen("./test.txt", "rt", encoding="utf-8") as f: 43 | print(f.read()) 44 | """ 45 | 46 | def __init__( 47 | self, 48 | file: Union[str, bytes, os.PathLike[str]], 49 | mode: str = "r", 50 | encoding: Optional[str] = None, 51 | ): 52 | """ 53 | :param file: 文件路径 54 | :param mode: 打开模式(暂时只支持文本读取) 55 | :param encoding: 文本文件编码,留空则自动处理 56 | """ 57 | 58 | # 预处理文件路径 59 | file_path = self.deal_path(file) 60 | 61 | # 分析模式是否合法、返回正确的 FileIo 类实例 62 | # https://docs.python.org/zh-cn/3/library/functions.html#open 63 | if "b" not in mode: 64 | # 文本模式 65 | self.io_obj = PyQTextFileIo(file_path, mode, encoding) 66 | else: 67 | # 二进制模式(暂不支持) 68 | # self.io_obj = PyQByteFileIo(file, mode) 69 | raise ValueError("暂不支持该模式") 70 | 71 | def __enter__(self): 72 | return self.io_obj 73 | 74 | def __exit__(self, exc_type, exc_val, exc_tb): 75 | self.io_obj.close() 76 | 77 | @staticmethod 78 | def deal_path(path: Union[str, bytes, os.PathLike[str]]) -> str: 79 | """预处理文件路径,统一成 posix 风格的字符串 80 | 81 | 因为 QFile 无法识别 pathlib.Path类型的路径、 82 | 无法处理 Windows 下 \\ 风格的路径字符串,所以需要归一化处理 83 | 84 | :param path: 文件路径 85 | :return: 使用正斜杠(/)的路径字符串 86 | """ 87 | 88 | # 若路径以字节串传入,则先处理成字符串 89 | if isinstance(path, bytes): 90 | path = path.decode("utf-8") 91 | elif isinstance(path, QByteArray): 92 | path = qba_to_str(path) 93 | 94 | return str(pathlib.Path(path).as_posix()) 95 | 96 | 97 | class PyQTextFileIo: 98 | """将 QFile 中处理文本文件读写的部分封装成 Python的 io 风格 99 | 100 | 目前只支持读取,不支持写入 101 | """ 102 | 103 | def __init__(self, file: str, mode: str, encoding: Optional[str] = None): 104 | """ 105 | :param file: 文件路径,已经由 `QtFileOpen` 进行过预处理 106 | :param mode: 打开模式 107 | :param encoding: 文本编码 108 | """ 109 | 110 | self._detect_error(file) 111 | self._file = QFile(file) 112 | 113 | if encoding is not None: 114 | self.encoding = encoding 115 | else: 116 | # 用户未指定编码,则使用当前平台默认编码 117 | self.encoding = locale.getencoding() 118 | 119 | self.mode = self._parse_mode(mode) 120 | self._file.open(self.mode) 121 | 122 | @staticmethod 123 | def _detect_error(input_file: str) -> None: 124 | """检查传入的文件是否存在错误,如有则抛出对应的异常 125 | 126 | :param input_file: 文件路径 127 | :raise IsADirectoryError: 传入的文件路径实际是目录时抛出此异常 128 | :raise FileNotFoundError: 传入的文件路径不存在时抛出此异常 129 | """ 130 | 131 | file_info = QFileInfo(input_file) 132 | if file_info.isDir(): 133 | raise IsADirectoryError(f"File '{input_file}' is a directory.") 134 | if not file_info.exists(): 135 | raise FileNotFoundError(f'File "{input_file}" not found.') 136 | 137 | @staticmethod 138 | def _parse_mode(py_mode: str) -> QIODevice.OpenModeFlag: 139 | """解析文件打开模式,将 Python open() 风格转换至 QIODevice.OpenModeFlag 140 | 141 | https://docs.python.org/zh-cn/3/library/functions.html#open 142 | https://doc.qt.io/qt-6/qiodevicebase.html#OpenModeFlag-enum 143 | 144 | :param py_mode: Python风格的文件打开模式字符串,如"r"、"w"、"r+"、"x"等。 145 | :return: QIODevice.OpenModeFlag 枚举类的成员 146 | :raise ValueError: 传入模式错误时抛出 147 | """ 148 | 149 | qt_mode: QIODevice.OpenModeFlag = QIODevice.OpenModeFlag.Text 150 | 151 | if "r" not in py_mode and "w" not in py_mode and "+" not in py_mode: 152 | raise ValueError(f"Mode must have 'r', 'w', or '+'; got '{py_mode}'.") 153 | 154 | if "r" in py_mode and "+" not in py_mode: 155 | qt_mode = qt_mode | QIODevice.OpenModeFlag.ReadOnly 156 | # 暂不支持写入 157 | elif "w" in py_mode: 158 | qt_mode = qt_mode | QIODevice.OpenModeFlag.WriteOnly 159 | elif "+" in py_mode: 160 | qt_mode = qt_mode | QIODevice.OpenModeFlag.ReadWrite 161 | 162 | if "x" in py_mode: 163 | qt_mode = qt_mode | QIODevice.OpenModeFlag.NewOnly 164 | 165 | return qt_mode 166 | 167 | def readable(self) -> bool: 168 | """当前文件是否可读 169 | 170 | :return: isReadable 171 | """ 172 | 173 | return self._file.isReadable() 174 | 175 | def read(self, size: int = -1) -> str: 176 | """模仿 `io.TextIOBase.read()` 的行为,读取流中的文本。 177 | 178 | 从流中读取至多 `size` 个字符并以单个 str 的形式返回。 如果 size 为负值或 None,则读取至 EOF。 179 | https://docs.python.org/3/library/io.html#io.TextIOBase.read 180 | 181 | :param size: 读取的字符数,负值或 None 表示一直读取直到 EOF 182 | :return: 文件中读出的文本内容 183 | """ 184 | 185 | if not self.readable(): 186 | raise PermissionError(f"File '{self._file.fileName()}' is not Readable.") 187 | 188 | if size < 0 or size is None: 189 | # 读取文件,并将 QByteArray 转为 str 190 | text = qba_to_str(self._file.readAll(), encoding=self.encoding) 191 | else: 192 | # 已知问题:性能太差 193 | # PySide6.QtCore.QIODevice.read(maxlen) 以字节而非字符方式计算长度,行为不一致 194 | # 而 QTextStream 对字符编码支持太差,许多编码并不支持 195 | text_all = qba_to_str(self._file.readAll(), self.encoding) 196 | text = text_all[0:size] # 性能太差 197 | 198 | return text 199 | 200 | def readline(self, size: int = -1, /) -> str: 201 | """模仿 `io.TextIOBase.readline()` 的行为,读取文件中的一行。 202 | 203 | https://docs.python.org/3/library/io.html#io.TextIOBase.readline 204 | 205 | :param size: 如果指定了 size,最多将读取 size 个字符。 206 | :return: 单行文本 207 | """ 208 | 209 | if not self.readable(): 210 | raise PermissionError(f"File '{self._file.fileName()}' is not Readable.") 211 | 212 | if self._file.atEnd(): 213 | warnings.warn( 214 | f"Trying to read a line at the end of the file '{self._file.fileName()}'.", 215 | Warning, 216 | stacklevel=1, 217 | ) 218 | return "" 219 | else: 220 | if size == 0: 221 | return "" 222 | else: 223 | line = qba_to_str(self._file.readLine(), self.encoding) 224 | if size < 0: 225 | return line 226 | else: 227 | return line[0:size] 228 | 229 | def readlines(self, hint: int = -1, /) -> list[str]: 230 | """模仿 `io.IOBase.readlines()` 的行为,返回由所有行组成的字符串列表。 231 | 232 | Known issue: slower than `readline()` 233 | https://docs.python.org/3/library/io.html#io.IOBase.readlines 234 | 235 | :param hint: 要读取的字符数 236 | :return: 文本内容所有行组成的列表 237 | """ 238 | 239 | if not self.readable(): 240 | raise PermissionError(f"File '{self._file.fileName()}' is not Readable.") 241 | 242 | if hint <= 0 or hint is None: 243 | temp = qba_to_str(self._file.readAll(), self.encoding) 244 | all_lines = temp.splitlines(keepends=True) 245 | else: 246 | all_lines = [] 247 | char_num = 0 248 | while char_num <= hint and not self._file.atEnd(): 249 | new_line = self.readline() 250 | all_lines.append(new_line) 251 | char_num += len(new_line) 252 | 253 | return all_lines 254 | 255 | def close(self) -> None: 256 | """关闭打开的文件对象""" 257 | 258 | self._file.close() 259 | -------------------------------------------------------------------------------- /src/py2exe_gui/Utilities/platform_specific_funcs.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """一些因操作系统不同而在具体实现上有差异的功能函数 5 | 6 | 注意,由于开发者没有苹果电脑,所有 macOS 功能均未经过验证 7 | """ 8 | 9 | __all__ = [ 10 | "open_dir_in_explorer", 11 | "get_sys_python", 12 | "get_user_config_dir", 13 | "get_user_cache_dir", 14 | "get_venv_python", 15 | ] 16 | 17 | import subprocess 18 | import warnings 19 | from pathlib import Path 20 | from typing import Union 21 | 22 | from ..Constants import RUNTIME_INFO, Platform 23 | 24 | 25 | def open_dir_in_explorer(dir_path: Union[str, Path]) -> None: 26 | """在操作系统文件资源管理器中打开指定目录 27 | 28 | :param dir_path: 待打开的目录路径 29 | :raise RuntimeError: 当前操作系统不受支持时抛出 30 | """ 31 | 32 | try: 33 | if RUNTIME_INFO.platform == Platform.windows: 34 | import os # fmt: skip 35 | os.startfile(dir_path) # type: ignore 36 | elif RUNTIME_INFO.platform == Platform.linux: 37 | subprocess.call(["xdg-open", dir_path]) 38 | elif RUNTIME_INFO.platform == Platform.macos: 39 | subprocess.call(["open", dir_path]) 40 | else: 41 | raise RuntimeError("Current OS is not supported.") 42 | except OSError as e: 43 | warnings.warn( 44 | f"Error occurred while trying to open directory in explorer: {e}", 45 | RuntimeWarning, 46 | stacklevel=2, 47 | ) 48 | 49 | 50 | def get_sys_python() -> str: 51 | """获取系统默认 Python 解释器的可执行文件位置 52 | 53 | :return: Python 可执行文件路径 54 | :raise RuntimeError: 当前操作系统不受支持时抛出 55 | :raise FileNotFoundError: 未找到 Python 解释器抛出 56 | """ 57 | 58 | if RUNTIME_INFO.platform == Platform.windows: 59 | cmd = ["powershell.exe", "(Get-Command python).Path"] # PowerShell 60 | elif RUNTIME_INFO.platform in (Platform.linux, Platform.macos): 61 | cmd = ["which", "python3"] 62 | else: 63 | raise RuntimeError("Current OS is not supported.") 64 | 65 | try: 66 | result = subprocess.run(cmd, capture_output=True, text=True) 67 | python_path = result.stdout.strip() 68 | except subprocess.CalledProcessError as e: 69 | warnings.warn( 70 | f"Error occurred while trying to get system default Python interpreter: {e.output}", 71 | RuntimeWarning, 72 | stacklevel=2, 73 | ) 74 | raise 75 | 76 | if not Path(python_path).exists(): 77 | raise FileNotFoundError(f"Python interpreter not found: {python_path}") 78 | 79 | return python_path 80 | 81 | 82 | def get_user_config_dir() -> Path: 83 | """获取当前平台用户配置文件目录路径 84 | 85 | :return: 用户配置文件目录路径 86 | :raise RuntimeError: 当前操作系统不受支持时抛出 87 | """ 88 | 89 | if RUNTIME_INFO.platform == Platform.windows: 90 | return Path.home() / "AppData" / "Roaming" 91 | elif RUNTIME_INFO.platform == Platform.linux: 92 | return Path.home() / ".config" 93 | elif RUNTIME_INFO.platform == Platform.macos: 94 | return Path.home() / "Library" / "Application Support" 95 | else: 96 | raise RuntimeError("Current OS is not supported.") 97 | 98 | 99 | def get_user_cache_dir() -> Path: 100 | """获取当前平台用户缓存或数据文件目录路径 101 | 102 | :return: 用户缓存目录路径 103 | :raise RuntimeError: 当前操作系统不受支持时抛出 104 | """ 105 | 106 | if RUNTIME_INFO.platform == Platform.windows: 107 | return Path.home() / "AppData" / "Local" 108 | elif RUNTIME_INFO.platform == Platform.linux: 109 | return Path.home() / ".cache" 110 | elif RUNTIME_INFO.platform == Platform.macos: 111 | return Path.home() / "Library" / "Caches" 112 | else: 113 | raise RuntimeError("Current OS is not supported.") 114 | 115 | 116 | def get_venv_python(root_path: Union[str, Path]) -> Path: 117 | """获取当前平台 venv 虚拟环境中 Python 解释器的可执行文件位置 118 | 119 | :param root_path: venv 虚拟环境根路径 120 | :return: Python 可执行文件路径 121 | :raise RuntimeError: 当前操作系统不受支持时抛出 122 | """ 123 | 124 | root = Path(root_path) 125 | 126 | if RUNTIME_INFO.platform == Platform.windows: 127 | return root / "Scripts" / "python.exe" 128 | elif RUNTIME_INFO.platform in (Platform.linux, Platform.macos): 129 | return root / "bin" / "python3" 130 | else: 131 | raise RuntimeError("Current OS is not supported.") 132 | -------------------------------------------------------------------------------- /src/py2exe_gui/Utilities/python_env.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块主要包含 Python 解释器环境类 `PyEnv` 与全局变量 `ALL_PY_ENVs` 5 | 6 | 用于存储 Python 环境的相关信息,如解释器可执行文件路径、Python 版本、已安装的包等 7 | """ 8 | 9 | __all__ = [ 10 | "PyEnv", 11 | "ALL_PY_ENVs", 12 | ] 13 | 14 | import json 15 | import subprocess 16 | from pathlib import Path 17 | from typing import Optional, Union 18 | 19 | from ..Constants import PyEnvType 20 | from ..Utilities import get_sys_python 21 | 22 | 23 | class PyEnv: 24 | """Python 解释器环境类,存储某个 Python 解释器对应的环境中的各种信息,如 25 | 解释器可执行文件路径、Python 版本、已安装的包等 26 | 27 | 静态方法: 28 | `PyEnv.get_py_version()` 用于获取 Python 版本号,返回 "3.11.7" 形式的字符串; 29 | `PyEnv.get_installed_packages()` 用于获取已安装的包,返回一个包含包名和版本的列表; 30 | `PyEnv.infer_type()` 用于推断 Python 解释器类型,返回 PyEnvType 枚举类的成员; 31 | """ 32 | 33 | def __init__( 34 | self, 35 | executable_path: Union[str, Path], 36 | type_: Optional[PyEnvType] = PyEnvType.unknown, 37 | ) -> None: 38 | """ 39 | :param executable_path: Python 可执行文件路径 40 | :param type_: Python 解释器类型,PyEnvType 枚举类的成员。传入显式的 None 则会触发自动识别。 41 | """ 42 | 43 | self.__exe_path = str(Path(executable_path).absolute()) 44 | 45 | # 懒加载,如果没有请求访问,则不会运行耗时的get操作;一旦运行过,将结果缓存到这些私有属性中,下次可直接使用 46 | self.__pyversion: Optional[str] = None 47 | self.__installed_packages: Optional[list[dict]] = None 48 | 49 | if type_ is None: 50 | # type_ 为 None 表示特殊含义“待推断” 51 | self.__type = self.infer_type(self.__exe_path) 52 | else: 53 | self.__type = type_ 54 | 55 | def __repr__(self) -> str: 56 | return f"PyEnv object (executable_path={self.__exe_path}, type={self.__type})" 57 | 58 | @property 59 | def exe_path(self) -> str: 60 | """Python 可执行文件路径(实例属性,只读)""" 61 | 62 | return self.__exe_path 63 | 64 | @property 65 | def pyversion(self) -> str: 66 | """Python 版本(实例属性,只读)""" 67 | 68 | if self.__pyversion is None: 69 | self.__pyversion = self.get_py_version(self.__exe_path) 70 | return self.__pyversion 71 | 72 | @property 73 | def installed_packages(self) -> list[dict]: 74 | """已安装的包列表(实例属性,只读) 75 | 76 | 对于每个 PyEnv 实例,首次访问此属性耗时会很长 77 | 78 | :return: 包列表,形如 [{'name': 'aiohttp', 'version': '3.9.1'}, {'name': 'aiosignal', 'version': '1.3.1'}, ...] 79 | """ 80 | 81 | if self.__installed_packages is None: 82 | self.__installed_packages = self.get_installed_packages(self.__exe_path) 83 | return self.__installed_packages 84 | 85 | @property 86 | def type(self) -> PyEnvType: 87 | """Python 解释器类型(实例属性,只读)""" 88 | 89 | return self.__type 90 | 91 | @staticmethod 92 | def get_py_version(executable_path: Union[str, Path]) -> str: 93 | """获取Python解释器的版本,以形如 "3.11.7" 的字符串形式返回 94 | 95 | 由于需要运行子进程,此函数耗时较长,应尽量减少调用次数 96 | 97 | :param executable_path: Python 可执行文件路径 98 | :return: Version of the Python interpreter, such as "3.11.7". 99 | """ 100 | 101 | cmd = [ 102 | f"{executable_path}", 103 | "-c", 104 | "import platform;print(platform.python_version(), end='')", 105 | ] 106 | try: 107 | result = subprocess.run(cmd, capture_output=True, text=True) 108 | version = result.stdout 109 | except subprocess.CalledProcessError as e: 110 | raise RuntimeError(f"Failed to get Python version: {e.output}") from e 111 | return version 112 | 113 | @staticmethod 114 | def get_installed_packages(executable_path: Union[str, Path]) -> list[dict]: 115 | """获取该 Python 环境中已安装的包信息 116 | 117 | 由于需要运行子进程与解析json,此函数耗时极长,应尽量减少调用次数 118 | 119 | :param executable_path: Python 解释器可执行文件路径 120 | :return: 包列表,形如 [{'name': 'aiohttp', 'version': '3.9.1'}, {'name': 'aiosignal', 'version': '1.3.1'}, ...] 121 | """ 122 | 123 | cmd = [ 124 | f"{executable_path}", 125 | "-m", 126 | "pip", 127 | "list", 128 | "--format", 129 | "json", 130 | "--disable-pip-version-check", 131 | "--no-color", 132 | "--no-python-version-warning", 133 | ] 134 | 135 | try: 136 | # 运行 pip list 命令,获取输出 137 | result = subprocess.run(cmd, capture_output=True, text=True) 138 | pip_list = result.stdout 139 | except subprocess.CalledProcessError as e: 140 | raise RuntimeError(f"Failed to get installed packages: {e.output}") from e 141 | except Exception as e: 142 | raise RuntimeError(f"An error occurred: {e}") from e 143 | 144 | try: 145 | # json 解析 146 | installed_packages: list[dict] = json.loads(pip_list) 147 | except json.decoder.JSONDecodeError as e: 148 | raise RuntimeError(f"Failed to parse installed packages: {e}") from e 149 | 150 | return installed_packages 151 | 152 | @staticmethod 153 | def infer_type(executable_path: Union[str, Path]) -> PyEnvType: 154 | """推断 Python 环境类型,如 system venv Poetry Conda 等 155 | 156 | 暂时仅通过解析解释器绝对路径,字符串正则匹配来判断,有待改进 157 | 158 | :param executable_path: Python 可执行文件路径 159 | :return: PyEnvType 枚举类的成员 160 | """ 161 | 162 | # 确保路径为 POSIX 风格的绝对路径 163 | exe_path = Path(executable_path).absolute().as_posix() 164 | 165 | # # 使用正则表达式匹配来检测不同的环境类型 166 | # regexes = [ 167 | # (r"^.*venv.*", PyEnvType.venv), 168 | # (r"^.*pypoetry.*", PyEnvType.poetry), 169 | # (r"^.*conda.*", PyEnvType.conda), 170 | # ( 171 | # re.escape(f"{Path(get_sys_python()).absolute().as_posix()}"), 172 | # PyEnvType.system 173 | # )] 174 | # for regex, env_type in regexes: 175 | # match = re.search(regex, str(__exe_path)) 176 | # if match: 177 | # return env_type 178 | 179 | # 简单的字符串 in 方法,效率应该远高于正则匹配 180 | match_pair = [ 181 | ("venv", PyEnvType.venv), 182 | ("pypoetry", PyEnvType.poetry), 183 | ("conda", PyEnvType.conda), 184 | (f"{Path(get_sys_python()).absolute().as_posix()}", PyEnvType.system), 185 | ] 186 | 187 | for patten, env_type in match_pair: 188 | if patten in exe_path: 189 | return env_type 190 | 191 | # 如果没有匹配到,返回 unknown 192 | return PyEnvType.unknown 193 | 194 | def pkg_installed(self, package_name: str) -> bool: 195 | """检查特定软件包是否已安装 196 | 197 | :param package_name: 待检索的软件包名称 198 | :return: 是否已安装 199 | """ 200 | 201 | return any(pkg["name"] == package_name for pkg in self.installed_packages) 202 | 203 | def install_package(self, package_name: str) -> int: 204 | """安装特定软件包 205 | 206 | :param package_name: 待安装的软件包名称 207 | :return: pip install 命令返回值 208 | :raise RuntimeError: pip install 错误 209 | """ 210 | 211 | cmd = [ 212 | self.__exe_path, 213 | "-m", 214 | "pip", 215 | "install", 216 | "--upgrade", 217 | package_name, 218 | ] 219 | 220 | try: 221 | # 运行 pip install 命令,安装指定模块 222 | result = subprocess.run(cmd, capture_output=True, text=True) 223 | except subprocess.CalledProcessError as e: 224 | raise RuntimeError(f"Failed to install package: {e.output}") from e 225 | except Exception as e: 226 | raise RuntimeError(f"An error occurred: {e}") from e 227 | else: 228 | return result.returncode 229 | 230 | 231 | # 全局变量,保存所有已创建的 PyEnv 实例 232 | ALL_PY_ENVs: list[PyEnv] = [] 233 | -------------------------------------------------------------------------------- /src/py2exe_gui/Utilities/qobject_tr.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """自实现 QObject.tr() 方法,使得 PyCharm 等静态检查工具不再报错。""" 5 | 6 | __all__ = [ 7 | "QObjTr", 8 | ] 9 | 10 | from typing import Optional 11 | 12 | from PySide6.QtCore import QCoreApplication 13 | 14 | 15 | class QObjTr: 16 | """利用 Python 多继承机制,为任何需要Qt翻译的类提供类方法 `tr()`。 17 | 18 | 只能通过类名调用(形如 CLASSNAME.tr()),不能通过实例调用(形如 self.tr())!对于有继承关系的控件, 19 | 通过子类的实例调用 tr() 只能得到子类方法中涉及的字符串,而在父类中涉及的字符串都会丢失。 20 | 21 | 假设有名为 MainWindow 的类,继承自 PySide6.QtWidgets.QMainWindow,需要在其实例属性中设置带翻译的文本, 22 | 那么应当这样做: 23 | 24 | 1. 将 MainWindow 同时继承自 QObjTr 和 QMainWindow:`class MainWindow(QObjTr, QMainWindow): ...` 25 | 2. 在实例属性中,凡需要翻译的字符串,使用 MainWindow.tr()包裹,比如:`MainWindow.tr("&File")` 26 | """ 27 | 28 | @classmethod 29 | def tr(cls, msg: str, disambiguation: Optional[str] = None, n: int = -1) -> str: 30 | """Returns a translated version of `msg` 31 | 32 | **Note: Can only be used as `CLASSNAME.tr()`, not as `self.tr()`!** 33 | 34 | **注意:只能通过类名调用,不能通过实例调用!否则会在涉及到继承的控件中失效。** 35 | 36 | Wrap `QCoreApplication.translate()` to `QObject.tr()`.This will make PyCharm 37 | happy. Now you can use `CLASSNAME.tr()` freely. For more details, see: 38 | 39 | 40 | 41 | :param msg: Origin messages 42 | :param disambiguation: Disambiguate identical text, see Qt Docs for details 43 | :param n: Handle plural forms, see Qt Docs for details 44 | :return: Translated messages 45 | """ 46 | 47 | return QCoreApplication.translate(cls.__name__, msg, disambiguation, n) 48 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """本 package 中包含所有控件类,集中处理界面(前端)相关功能""" 5 | 6 | from .main_window import MainWindow 7 | from .subprocess_widget import SubProcessDlg 8 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/add_data_widget.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块主要包含用于提供 PyInstaller `--add-data` 和 `--add-binary` 功能的窗口类 `AddDataWindow`""" 5 | 6 | __all__ = ["AddDataWindow"] 7 | 8 | from pathlib import Path 9 | from typing import Optional 10 | 11 | from PySide6.QtCore import QItemSelectionModel, Qt, Signal, Slot 12 | from PySide6.QtGui import QIcon, QPixmap 13 | from PySide6.QtWidgets import ( 14 | QFileDialog, 15 | QHBoxLayout, 16 | QHeaderView, 17 | QPushButton, 18 | QTableWidget, 19 | QTableWidgetItem, 20 | QVBoxLayout, 21 | QWidget, 22 | ) 23 | 24 | from ..Utilities import QObjTr 25 | 26 | 27 | class AddDataWindow(QObjTr, QWidget): 28 | """用于提供 PyInstaller --add-data 和 --add-binary 功能的窗口""" 29 | 30 | # 类型别名 31 | # 数据条目,第一项为在文件系统中的路径,第二项为捆绑后环境中的路径 32 | data_item = tuple[Path, str] 33 | 34 | # 自定义信号 35 | data_selected = Signal(list) # 用户在添加数据窗口完成所有编辑后,提交的信号 36 | 37 | def __init__(self, parent: Optional[QWidget] = None) -> None: 38 | """ 39 | :param parent: 父控件对象 40 | """ 41 | 42 | super().__init__(parent) 43 | 44 | # 工作目录,应由主界面提供,默认为入口脚本所在目录;各种文件处理将以此作为相对路径起点 45 | self._work_dir: Path = Path(".").absolute() 46 | 47 | # 浏览系统文件/目录的文件对话框 48 | self.data_browse_dlg = QFileDialog(self) 49 | self.data_dir_browse_dlg = QFileDialog(self) 50 | 51 | # 条目表格,双列,每行为一条添加的数据 52 | self.item_table = QTableWidget(self) 53 | 54 | # 各种编辑功能按键 55 | self.new_btn = QPushButton(self) 56 | self.browse_btn = QPushButton(self) 57 | self.browse_dir_btn = QPushButton(self) 58 | self.delete_btn = QPushButton(self) 59 | 60 | # 整个窗口的确认和取消键 61 | self.ok_btn = QPushButton(self) 62 | self.cancel_btn = QPushButton(self) 63 | 64 | self._setup_ui() 65 | self._setup_layout() 66 | self._connect_slots() 67 | 68 | def _setup_ui(self) -> None: 69 | """处理 UI 内容""" 70 | 71 | self.setWindowTitle(AddDataWindow.tr("Add Files")) 72 | self.setMinimumWidth(550) 73 | self.setWindowIcon(QIcon(QPixmap(":/Icons/Py2exe-GUI_icon_72px"))) 74 | 75 | self.data_browse_dlg.setFileMode(QFileDialog.FileMode.ExistingFile) 76 | self.data_dir_browse_dlg.setFileMode(QFileDialog.FileMode.Directory) 77 | 78 | self.item_table.setColumnCount(2) 79 | self.item_table.setRowCount(0) 80 | self.item_table.setHorizontalHeaderLabels(["SOURCE", "DEST"]) 81 | self.item_table.horizontalHeader().setSectionResizeMode( 82 | 0, QHeaderView.ResizeMode.Stretch 83 | ) 84 | self.item_table.horizontalHeader().setSectionResizeMode( 85 | 1, QHeaderView.ResizeMode.ResizeToContents 86 | ) 87 | 88 | self.new_btn.setText(AddDataWindow.tr("&New")) 89 | self.browse_btn.setText(AddDataWindow.tr("&Browse File")) 90 | self.browse_dir_btn.setText(AddDataWindow.tr("Browse &Folder")) 91 | self.delete_btn.setText(AddDataWindow.tr("&Delete")) 92 | 93 | self.ok_btn.setText(AddDataWindow.tr("&OK")) 94 | self.cancel_btn.setText(AddDataWindow.tr("&Cancel")) 95 | 96 | # noinspection DuplicatedCode 97 | def _setup_layout(self) -> None: 98 | """构建与设置布局管理器""" 99 | 100 | btn_group_box = QVBoxLayout() 101 | btn_group_box.addWidget(self.new_btn) 102 | btn_group_box.addWidget(self.browse_btn) 103 | btn_group_box.addWidget(self.browse_dir_btn) 104 | btn_group_box.addWidget(self.delete_btn) 105 | btn_group_box.addStretch(10) 106 | 107 | upper_box = QHBoxLayout() 108 | upper_box.addWidget(self.item_table) 109 | upper_box.addLayout(btn_group_box) 110 | 111 | lower_box = QHBoxLayout() 112 | lower_box.addStretch(10) 113 | lower_box.addWidget(self.ok_btn) 114 | lower_box.addWidget(self.cancel_btn) 115 | 116 | main_layout = QVBoxLayout() 117 | main_layout.addLayout(upper_box) 118 | main_layout.addLayout(lower_box) 119 | 120 | self.setLayout(main_layout) 121 | 122 | # noinspection DuplicatedCode 123 | def _connect_slots(self) -> None: 124 | """构建各槽函数、连接信号""" 125 | 126 | @Slot() 127 | def handle_new_btn() -> None: 128 | """“新建”按钮槽函数""" 129 | 130 | row_count = self.item_table.rowCount() 131 | source_item = QTableWidgetItem("") 132 | source_item.setFlags( 133 | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable 134 | ) 135 | self.item_table.setRowCount(row_count + 1) 136 | self.item_table.setItem(row_count, 0, source_item) 137 | self.item_table.setItem(row_count, 1, QTableWidgetItem(".")) 138 | self.item_table.setCurrentCell( 139 | row_count, 0, QItemSelectionModel.SelectionFlag.Select 140 | ) # 自动选中新建行的第一列 141 | 142 | @Slot() 143 | def handle_delete_btn() -> None: 144 | """“删除”按钮槽函数""" 145 | 146 | self.item_table.removeRow(self.item_table.currentRow()) 147 | 148 | @Slot() 149 | def handel_browse_btn() -> None: 150 | """“浏览文件”按钮槽函数""" 151 | 152 | if ( 153 | self.item_table.currentRow() == -1 154 | or self.item_table.currentColumn() == 1 155 | ): 156 | # 用户未选中任何条目或选择的是DEST列,不做任何响应 157 | return 158 | 159 | self.data_browse_dlg.open() 160 | 161 | @Slot() 162 | def handle_browse_dir_btn() -> None: 163 | """“浏览文件夹”按钮槽函数""" 164 | 165 | if ( 166 | self.item_table.currentRow() == -1 167 | or self.item_table.currentColumn() == 1 168 | ): 169 | # 用户未选中任何条目或选择的是DEST列,不做任何响应 170 | return 171 | 172 | self.data_dir_browse_dlg.open() 173 | 174 | @Slot(str) 175 | def handle_browse_selected(file_path: str) -> None: 176 | """处理文件对话框获取到用户打开文件/目录的槽函数 177 | 178 | :param file_path: 用户打开的文件/目录路径 179 | """ 180 | 181 | path = Path(file_path) 182 | current_row = self.item_table.currentRow() 183 | 184 | # SOURCE 列设置为操作系统绝对路径 185 | source_item = QTableWidgetItem(str(path.absolute())) 186 | source_item.setFlags( 187 | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable 188 | ) 189 | self.item_table.setItem(current_row, 0, source_item) 190 | 191 | # DEST 列设置为与入口脚本的相对路径或表示顶层目录的"." 192 | if path.is_dir(): 193 | try: 194 | dest_item = QTableWidgetItem(str(path.relative_to(self._work_dir))) 195 | except ValueError: 196 | dest_item = QTableWidgetItem(str(path.name)) 197 | self.item_table.setItem(current_row, 1, dest_item) 198 | else: 199 | self.item_table.setItem(current_row, 1, QTableWidgetItem(".")) 200 | 201 | @Slot() 202 | def handle_ok_btn() -> None: 203 | """“确定”按钮槽函数""" 204 | 205 | # 删掉所有空白行 206 | row = 0 207 | while row < self.item_table.rowCount(): 208 | if self.item_table.item(row, 0).text() == "": 209 | self.item_table.removeRow(row) 210 | else: 211 | row += 1 212 | 213 | self.data_selected.emit(self._submit()) 214 | self.close() 215 | 216 | self.new_btn.clicked.connect(handle_new_btn) 217 | self.delete_btn.clicked.connect(handle_delete_btn) 218 | self.browse_btn.clicked.connect(handel_browse_btn) 219 | self.browse_dir_btn.clicked.connect(handle_browse_dir_btn) 220 | self.data_browse_dlg.fileSelected.connect(handle_browse_selected) 221 | self.data_dir_browse_dlg.fileSelected.connect(handle_browse_selected) 222 | 223 | self.ok_btn.clicked.connect(handle_ok_btn) 224 | self.cancel_btn.clicked.connect(self.close) 225 | 226 | def _submit(self) -> list[data_item]: 227 | """将当前界面上的所有配置项转换为 data_item 列表,准备提交给主界面和打包流使用 228 | 229 | :return: `data-item` 列表 230 | """ 231 | 232 | all_data_item_list = [] 233 | for row in range(self.item_table.rowCount()): 234 | path = Path(self.item_table.item(row, 0).text()) 235 | dest = self.item_table.item(row, 1).text() 236 | all_data_item_list.append((path, dest)) 237 | 238 | return all_data_item_list 239 | 240 | def set_work_dir(self, work_dir_path: Path) -> None: 241 | """设置工作目录并更新相关文件对话框路径起点 242 | 243 | :param work_dir_path: 新的工作目录路径 244 | """ 245 | 246 | self._work_dir = work_dir_path 247 | self.data_browse_dlg.setDirectory(str(work_dir_path)) 248 | self.data_dir_browse_dlg.setDirectory(str(work_dir_path)) 249 | 250 | def load_data_item_list(self, all_data_item_list: list[data_item]) -> None: 251 | """从 `all_data_item_list` 列表加载待添加的数据文件至界面控件 252 | 253 | :param all_data_item_list: 保存数据条目的列表 254 | """ 255 | 256 | self.item_table.setRowCount(len(all_data_item_list)) 257 | for row, data_item in enumerate(all_data_item_list): 258 | source_item = QTableWidgetItem(str(data_item[0].absolute())) 259 | source_item.setFlags( 260 | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable 261 | ) 262 | self.item_table.setItem(row, 0, source_item) 263 | self.item_table.setItem(row, 1, QTableWidgetItem(data_item[1])) 264 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/arguments_browser.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块主要包含用于在界面上预览显示 PyInstaller 命令选项的 `ArgumentsBrowser` 类""" 5 | 6 | __all__ = [ 7 | "get_line_continuation", 8 | "ArgumentsBrowser", 9 | ] 10 | 11 | from typing import Optional 12 | 13 | from PySide6.QtGui import QContextMenuEvent 14 | from PySide6.QtWidgets import QMenu, QTextBrowser, QWidget 15 | 16 | from ..Constants import RUNTIME_INFO, Platform 17 | from ..Utilities import QObjTr 18 | 19 | # 一组适合浅色背景的颜色 20 | colors = ["#FD6D5A", "#FEB40B", "#6DC354", "#994487", "#518CD8", "#443295"] 21 | 22 | 23 | def get_line_continuation() -> str: 24 | """获取当前运行平台对应的命令行续行符 25 | 26 | :return: line continuation character 27 | """ 28 | 29 | # 各平台的命令行续行符 30 | line_continuation_text = {"shell": "\\", "cmd": "^", "powershell": "`"} 31 | 32 | if Platform.windows == RUNTIME_INFO.platform: 33 | return line_continuation_text["powershell"] 34 | else: 35 | return line_continuation_text["shell"] 36 | 37 | 38 | def wrap_font_tag(raw_text: str, color: str, **kwargs): 39 | """辅助函数,用于为字符添加标签包裹,属性可通过可变关键字参数传入 40 | 41 | :param raw_text: 原始字符文本 42 | :param color: 颜色 43 | """ 44 | 45 | attributes = [f"color='{color}'"] 46 | attributes.extend(f"{k}='{v}'" for k, v in kwargs.items()) 47 | attributes_text = " ".join(attr for attr in attributes if attr) # 筛选出非空的属性 48 | tag_attributes = " " + attributes_text + " " if attributes_text else "" 49 | return f"{raw_text}" 50 | 51 | 52 | class ArgumentsBrowser(QObjTr, QTextBrowser): 53 | """针对命令行参数列表特别优化的文本浏览器""" 54 | 55 | def __init__(self, parent: Optional[QWidget] = None) -> None: 56 | """ 57 | :param parent: 父控件对象 58 | """ 59 | 60 | super().__init__(parent) 61 | 62 | # 右键菜单 63 | self.context_menu = QMenu(self) 64 | copy_action = self.context_menu.addAction(ArgumentsBrowser.tr("Copy")) 65 | copy_action.triggered.connect(self._handle_copy_action) 66 | export_action = self.context_menu.addAction(ArgumentsBrowser.tr("Export")) 67 | export_action.triggered.connect(self._handle_export_action) 68 | 69 | def contextMenuEvent(self, event: QContextMenuEvent) -> None: 70 | """重写右键菜单事件 71 | 72 | :param event: 事件 73 | """ 74 | 75 | self.context_menu.exec(event.globalPos()) 76 | 77 | def _handle_copy_action(self) -> None: 78 | """处理复制事件""" 79 | 80 | self.selectAll() 81 | self.copy() 82 | 83 | def _handle_export_action(self) -> None: 84 | """处理导出事件""" 85 | 86 | # TODO 实现导出至 PowerShell/Bash 脚本 87 | pass 88 | 89 | def enrich_args_text(self, args_list: list[str]) -> None: 90 | """对参数进行一定高亮美化后显示 91 | 92 | :param args_list: 参数列表 93 | """ 94 | 95 | # 不间断换行 96 | line_continuation = get_line_continuation() + "
" + (" " * 4) 97 | 98 | # 首个参数一定为待打包的 Python 脚本名 99 | enriched_arg_texts: list[str] = [wrap_font_tag(args_list[0], color=colors[4])] 100 | 101 | for arg in args_list[1:]: 102 | if arg.startswith("--") or arg.startswith("-"): 103 | # 添加换行,便于阅读与复制导出脚本 104 | enriched_arg_texts.append(line_continuation) 105 | enriched_arg_texts.append(wrap_font_tag(arg, color=colors[1])) 106 | else: 107 | enriched_arg_texts.append(arg) 108 | 109 | self.setText("pyinstaller " + " ".join(enriched_arg_texts)) 110 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/center_widget.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块主要包含主界面中央控件类 5 | 6 | CenterWidget 是主要类,定义了各种界面控件及其大部分信号与槽功能 7 | 8 | WinMacCenterWidget 继承自 CenterWidget,额外添加了仅 Windows 和 macOS 平台才支持的 9 | PyInstaller 功能所对应的控件及其槽函数 10 | """ 11 | 12 | __all__ = ["CenterWidget", "WinMacCenterWidget"] 13 | 14 | from pathlib import Path 15 | 16 | from PySide6 import QtCore 17 | from PySide6.QtWidgets import ( 18 | QButtonGroup, 19 | QCheckBox, 20 | QGridLayout, 21 | QHBoxLayout, 22 | QLabel, 23 | QLineEdit, 24 | QMainWindow, 25 | QMessageBox, 26 | QPushButton, 27 | QRadioButton, 28 | QVBoxLayout, 29 | QWidget, 30 | ) 31 | 32 | from ..Constants import PyInstOpt 33 | from ..Core import InterpreterValidator 34 | from ..Utilities import ALL_PY_ENVs, PyEnv, QObjTr 35 | from .add_data_widget import AddDataWindow 36 | from .arguments_browser import ArgumentsBrowser 37 | from .dialog_widgets import ( 38 | IconFileDlg, 39 | InterpreterFileDlg, 40 | PkgBrowserDlg, 41 | ScriptFileDlg, 42 | ) 43 | from .multi_item_edit_widget import MultiPkgEditWindow 44 | from .pyenv_combobox import PyEnvComboBox 45 | from .pyinstaller_option_widget import PyinstallerOptionTable 46 | 47 | 48 | class CenterWidget(QObjTr, QWidget): 49 | """主界面的中央控件 50 | 51 | 此类为可以实例化的基类,适用于所有平台。 52 | 有专有功能的平台应继承此类,并重写对应方法来添加相关控件与功能。 53 | """ 54 | 55 | # 自定义信号 56 | option_selected = QtCore.Signal(tuple) # 用户通过界面控件选择选项后发射此信号 57 | # option_selected 实际类型为 tuple[PyinstallerArgs, str] 58 | 59 | def __init__(self, parent: QMainWindow) -> None: 60 | """ 61 | :param parent: 父控件对象,应为主程序主窗口 62 | """ 63 | 64 | super().__init__(parent) 65 | 66 | self.parent_widget = parent 67 | 68 | # 显示 PyInstaller 选项详细描述的控件 69 | self.pyinstaller_option_table = PyinstallerOptionTable() 70 | 71 | # 展示已安装的包 72 | self.pkg_browser_dlg = PkgBrowserDlg() 73 | 74 | # 待打包的入口脚本 75 | self.script_path_label = QLabel() 76 | self.script_file_dlg = ScriptFileDlg() 77 | self.script_browse_btn = QPushButton() 78 | self.script_path_le = QLineEdit() 79 | 80 | # Python 解释器选择 81 | self.pyenv_combobox = PyEnvComboBox() 82 | self.pyenv_browse_btn = QPushButton() 83 | self.itp_dlg = InterpreterFileDlg() 84 | 85 | # 打包后输出的项目名称 86 | self.project_name_label = QLabel() 87 | self.project_name_le = QLineEdit() 88 | 89 | # 输出至单目录/单文件 90 | self.fd_label = QLabel() 91 | self.one_dir_btn = QRadioButton() 92 | self.one_file_btn = QRadioButton() 93 | self.fd_group = QButtonGroup() 94 | 95 | # 添加数据与二进制文件 96 | self.add_data_btn = QPushButton() 97 | self.add_data_dlg = AddDataWindow() 98 | self.data_item_list: list[AddDataWindow.data_item] = [] 99 | self.add_binary_btn = QPushButton() 100 | self.add_binary_dlg = AddDataWindow() 101 | self.binary_item_list: list[AddDataWindow.data_item] = [] 102 | 103 | # 添加隐式导入 104 | self.hidden_import_btn = QPushButton() 105 | self.hidden_import_dlg = MultiPkgEditWindow(self.pkg_browser_dlg) 106 | self.hidden_import_list: list[str] = [] 107 | 108 | # 清理缓存与临时文件 109 | self.clean_checkbox = QCheckBox() 110 | 111 | # 预览生成的PyInstaller打包指令 112 | self.pyinstaller_args_browser = ArgumentsBrowser() 113 | 114 | # 打包按钮 115 | self.run_packaging_btn = QPushButton() 116 | 117 | self._setup_ui() 118 | self._connect_slots() 119 | self._set_layout() 120 | 121 | def _setup_ui(self) -> None: 122 | """设置各种控件的属性""" 123 | 124 | self.script_path_label.setText(CenterWidget.tr("Python script:")) 125 | self.script_path_le.setReadOnly(True) 126 | self.script_path_le.setPlaceholderText( 127 | CenterWidget.tr("Python entry script path") 128 | ) 129 | self.script_browse_btn.setText(CenterWidget.tr("Browse")) 130 | self.script_browse_btn.setFixedWidth(80) 131 | 132 | self.pyenv_browse_btn.setText(CenterWidget.tr("Browse")) 133 | self.pyenv_browse_btn.setFixedWidth(80) 134 | 135 | self.project_name_label.setText(CenterWidget.tr("App Name:")) 136 | self.project_name_le.setPlaceholderText(CenterWidget.tr("Bundled app name")) 137 | 138 | self.fd_label.setText(CenterWidget.tr("One Folder/ One File:")) 139 | self.one_dir_btn.setText(CenterWidget.tr("One Folder")) 140 | self.one_dir_btn.setChecked(True) # 默认值 141 | self.one_file_btn.setText(CenterWidget.tr("One File")) 142 | self.fd_group.addButton(self.one_dir_btn, 0) 143 | self.fd_group.addButton(self.one_file_btn, 1) 144 | 145 | self.add_data_btn.setText(CenterWidget.tr("Add Data Files")) 146 | self.add_binary_btn.setText(CenterWidget.tr("Add Binary Files")) 147 | self.add_data_dlg.setWindowTitle(CenterWidget.tr("Add Data Files")) 148 | self.add_binary_dlg.setWindowTitle(CenterWidget.tr("Add Binary Files")) 149 | 150 | self.hidden_import_btn.setText(CenterWidget.tr("Hidden Import")) 151 | self.hidden_import_dlg.setWindowTitle("Hidden Import") 152 | 153 | self.clean_checkbox.setText(CenterWidget.tr("Clean")) 154 | self.clean_checkbox.setChecked(False) 155 | self.pyinstaller_args_browser.setMaximumHeight(80) 156 | 157 | self.run_packaging_btn.setText(CenterWidget.tr("Bundle!")) 158 | self.run_packaging_btn.setEnabled(False) 159 | 160 | # 将 PyInstaller 选项详情设置成各控件的 ToolTip 161 | if self.pyinstaller_option_table.option_dict: 162 | opt = self.pyinstaller_option_table.option_dict 163 | # TODO: 解绑文本文档中的option字符和此处opt的键 164 | self.project_name_label.setToolTip(opt["-n NAME, --name NAME"]) 165 | self.one_dir_btn.setToolTip(opt["-D, --onedir"]) 166 | self.one_file_btn.setToolTip(opt["-F, --onefile"]) 167 | self.add_data_btn.setToolTip(opt["--add-data SOURCE:DEST"]) 168 | self.add_binary_btn.setToolTip(opt["--add-binary SOURCE:DEST"]) 169 | self.hidden_import_btn.setToolTip( 170 | opt["--hidden-import MODULENAME, --hiddenimport MODULENAME"] 171 | ) 172 | self.clean_checkbox.setToolTip(opt["--clean"]) 173 | 174 | # noinspection DuplicatedCode 175 | def _connect_slots(self) -> None: 176 | """定义、连接信号与槽""" 177 | 178 | @QtCore.Slot(str) 179 | def handle_script_file_selected(file_path: str) -> None: 180 | """脚本文件完成选择的槽函数 181 | 182 | :param file_path: 脚本文件路径 183 | """ 184 | 185 | self.option_selected.emit((PyInstOpt.script_path, file_path)) 186 | 187 | @QtCore.Slot() 188 | def handle_pyenv_change() -> None: 189 | """处理用户通过选择不同的 Python 解释器时的响应""" 190 | 191 | current_pyenv = self.pyenv_combobox.get_current_pyenv() 192 | 193 | # 首次调用 current_pyenv.installed_packages 为重大性能热点,优化时应首先考虑 194 | self.pkg_browser_dlg.load_pkg_list(current_pyenv.installed_packages) 195 | 196 | @QtCore.Slot() 197 | def handle_project_name_selected() -> None: 198 | """输出程序名称完成输入的槽""" 199 | 200 | project_name: str = self.project_name_le.text() 201 | self.option_selected.emit((PyInstOpt.out_name, project_name)) 202 | 203 | @QtCore.Slot(int) 204 | def handle_one_fd_selected(btn_id: int) -> None: 205 | """选择输出至单文件/单目录的槽 206 | 207 | :param btn_id: `fd_group` 按钮组中按钮的 id 208 | """ 209 | 210 | if btn_id == 0: 211 | self.option_selected.emit((PyInstOpt.FD, "--onedir")) 212 | self.show_status_msg(CenterWidget.tr("Bundling into one folder.")) 213 | elif btn_id == 1: 214 | self.option_selected.emit((PyInstOpt.FD, "--onefile")) 215 | self.show_status_msg(CenterWidget.tr("Bundling into one file.")) 216 | 217 | @QtCore.Slot() 218 | def handle_add_data_btn_clicked() -> None: 219 | """用户在界面点击添加数据按钮的槽函数""" 220 | 221 | self.add_data_dlg.load_data_item_list(self.data_item_list) 222 | self.add_data_dlg.show() 223 | 224 | @QtCore.Slot(list) 225 | def handle_add_data_selected(data_item_list: list) -> None: 226 | """用户完成了添加数据操作的槽函数""" 227 | 228 | self.data_item_list = data_item_list 229 | self.show_status_msg(CenterWidget.tr("Add data files updated.")) 230 | self.option_selected.emit((PyInstOpt.add_data, data_item_list)) 231 | 232 | @QtCore.Slot() 233 | def handle_add_binary_btn_clicked() -> None: 234 | """用户在界面点击添加二进制文件按钮的槽函数""" 235 | 236 | self.add_binary_dlg.load_data_item_list(self.binary_item_list) 237 | self.add_binary_dlg.show() 238 | 239 | @QtCore.Slot(list) 240 | def handle_add_binary_selected(binary_item_list: list) -> None: 241 | """用户完成了添加二进制文件操作的槽函数""" 242 | 243 | self.binary_item_list = binary_item_list 244 | self.show_status_msg(CenterWidget.tr("Add binary files updated.")) 245 | self.option_selected.emit((PyInstOpt.add_binary, binary_item_list)) 246 | 247 | @QtCore.Slot() 248 | def handle_hidden_import_btn_clicked() -> None: 249 | """点击隐式导入按钮的槽函数""" 250 | 251 | self.hidden_import_dlg.show() 252 | 253 | @QtCore.Slot(list) 254 | def handle_hidden_import_selected(hidden_import_list: list[str]) -> None: 255 | """用户完成了隐式导入编辑操作的槽函数 256 | 257 | :param hidden_import_list: 隐式导入项列表 258 | """ 259 | 260 | self.hidden_import_list = hidden_import_list 261 | self.show_status_msg(CenterWidget.tr("Hidden import updated.")) 262 | self.option_selected.emit((PyInstOpt.hidden_import, hidden_import_list)) 263 | 264 | @QtCore.Slot(bool) 265 | def handle_clean_selected(selected: bool) -> None: 266 | """选择了清理缓存复选框的槽 267 | 268 | :param selected: 是否勾选了 clean 复选框 269 | """ 270 | 271 | if selected: 272 | self.option_selected.emit((PyInstOpt.clean, "--clean")) 273 | self.show_status_msg( 274 | CenterWidget.tr( 275 | "Clean cache and remove temporary files before building." 276 | ) 277 | ) 278 | else: 279 | self.option_selected.emit((PyInstOpt.clean, "")) 280 | self.show_status_msg( 281 | CenterWidget.tr("Will not delete cache and temporary files.") 282 | ) 283 | 284 | # 连接信号与槽 285 | # 入口脚本文件 286 | self.script_browse_btn.clicked.connect(self.script_file_dlg.open) 287 | self.script_file_dlg.fileSelected.connect(handle_script_file_selected) 288 | 289 | # 添加与选择 Python 解释器 290 | self.pyenv_browse_btn.clicked.connect(self.itp_dlg.open) 291 | self.itp_dlg.fileSelected.connect(self._handle_itp_file_selected) 292 | handle_pyenv_change() # 显式调用一次,为默认项设置相关内容 293 | self.pyenv_combobox.currentIndexChanged.connect(handle_pyenv_change) 294 | 295 | # 项目名称 296 | self.project_name_le.editingFinished.connect(handle_project_name_selected) 297 | 298 | # 单目录/单文件模式 299 | self.fd_group.idClicked.connect(handle_one_fd_selected) 300 | 301 | # 添加数据文件与二进制文件 302 | self.add_data_btn.clicked.connect(handle_add_data_btn_clicked) 303 | self.add_data_dlg.data_selected.connect(handle_add_data_selected) 304 | self.add_binary_btn.clicked.connect(handle_add_binary_btn_clicked) 305 | self.add_binary_dlg.data_selected.connect(handle_add_binary_selected) 306 | 307 | # 隐式导入 308 | self.hidden_import_btn.clicked.connect(handle_hidden_import_btn_clicked) 309 | self.hidden_import_dlg.items_selected.connect(handle_hidden_import_selected) 310 | 311 | # 清理 312 | self.clean_checkbox.toggled.connect(handle_clean_selected) 313 | 314 | # noinspection DuplicatedCode 315 | def _set_layout(self) -> None: 316 | """设置布局管理器""" 317 | 318 | self.main_layout = QVBoxLayout() 319 | 320 | script_layout = QGridLayout() 321 | script_layout.addWidget(self.script_path_label, 0, 0, 1, 2) 322 | script_layout.addWidget(self.script_path_le, 1, 0) 323 | script_layout.addWidget(self.script_browse_btn, 1, 1) 324 | 325 | pyenv_layout = QHBoxLayout() 326 | pyenv_layout.addWidget(self.pyenv_combobox) 327 | pyenv_layout.addWidget(self.pyenv_browse_btn) 328 | 329 | name_layout = QVBoxLayout() 330 | name_layout.addWidget(self.project_name_label) 331 | name_layout.addWidget(self.project_name_le) 332 | 333 | fd_layout = QGridLayout() 334 | fd_layout.addWidget(self.fd_label, 0, 0, 1, 2) 335 | fd_layout.addWidget(self.one_dir_btn, 1, 0) 336 | fd_layout.addWidget(self.one_file_btn, 1, 1) 337 | 338 | add_btn_layout = QHBoxLayout() 339 | add_btn_layout.addWidget(self.add_data_btn) 340 | add_btn_layout.addWidget(self.add_binary_btn) 341 | 342 | self.main_layout.addSpacing(10) 343 | self.main_layout.addLayout(script_layout) 344 | self.main_layout.addLayout(pyenv_layout) 345 | self.main_layout.addStretch(10) 346 | self.main_layout.addLayout(name_layout) 347 | self.main_layout.addStretch(10) 348 | self.main_layout.addLayout(fd_layout) 349 | self.main_layout.addStretch(10) 350 | self.main_layout.addLayout(add_btn_layout) 351 | self.main_layout.addStretch(10) 352 | self.main_layout.addWidget(self.hidden_import_btn) 353 | self.main_layout.addStretch(10) 354 | self.main_layout.addWidget(self.clean_checkbox) 355 | self.main_layout.addStretch(10) 356 | self.main_layout.addWidget(self.pyinstaller_args_browser) 357 | self.main_layout.addWidget(self.run_packaging_btn) 358 | 359 | self.setLayout(self.main_layout) 360 | 361 | @QtCore.Slot(tuple) 362 | def handle_option_set(self, option: tuple[PyInstOpt, str]) -> None: 363 | """处理option_set信号的槽,根据已经成功设置的选项调整界面 364 | 365 | :param option: 选项键值对 366 | """ 367 | 368 | option_key, option_value = option 369 | 370 | if option_key == PyInstOpt.script_path: 371 | script_path = Path(option_value) 372 | self.script_path_le.setText(script_path.name) 373 | self.show_status_msg( 374 | CenterWidget.tr("Opened script path: ") 375 | + f"{str(script_path.absolute())}" 376 | ) 377 | self.add_data_dlg.set_work_dir(script_path.parent) 378 | self.add_binary_dlg.set_work_dir(script_path.parent) 379 | 380 | elif option_key == PyInstOpt.out_name: 381 | self.project_name_le.setText(option_value) 382 | self.show_status_msg( 383 | CenterWidget.tr("The app name has been set to:") + f"{option_value}" 384 | ) 385 | 386 | @QtCore.Slot(PyInstOpt) 387 | def handle_option_error(self, option: PyInstOpt) -> None: 388 | """处理option_error信号的槽,重置设置失败的选项对应的界面,并向用户发出警告 389 | 390 | :param option: 选项 391 | """ 392 | 393 | # 清空重置该项的输入控件,并弹出警告窗口,等待用户重新输入 394 | if option == PyInstOpt.script_path: 395 | self.script_file_dlg.close() 396 | # 警告对话框 397 | result = QMessageBox.critical( 398 | self, 399 | CenterWidget.tr("Error"), 400 | CenterWidget.tr( 401 | "The selection is not a valid Python script file, " 402 | "please reselect it!" 403 | ), 404 | QMessageBox.StandardButton.Cancel, 405 | QMessageBox.StandardButton.Ok, 406 | ) 407 | if result == QMessageBox.StandardButton.Cancel: 408 | self.script_path_le.clear() 409 | self.project_name_le.clear() 410 | elif result == QMessageBox.StandardButton.Ok: 411 | self.script_file_dlg.exec() 412 | 413 | @QtCore.Slot(bool) 414 | def handle_ready_to_pack(self, ready: bool) -> None: 415 | """处理 ready_to_pack 信号的槽 416 | 417 | :param ready: 是否可以进行打包 418 | """ 419 | 420 | self.run_packaging_btn.setEnabled(ready) 421 | 422 | @QtCore.Slot(str) 423 | def _handle_itp_file_selected(self, file_path: str) -> None: 424 | """Python解释器文件完成选择的槽函数 425 | 426 | 首先对用户选择的文件进行有效性判断: 427 | 若有效则以此创建新的 PyEnv 对象、设置到下拉框中并选中; 428 | 若无效则弹出错误警告对话框,要求重新选择文件; 429 | 430 | :param file_path: 选择的解释器文件路径 431 | """ 432 | 433 | itp_path = Path(file_path).absolute() 434 | 435 | # 首先判断用户选择的路径是否已在当前存储的列表中,如在则不添加直接返回 436 | for pyenv in ALL_PY_ENVs: 437 | if str(itp_path) == pyenv.exe_path: 438 | return 439 | 440 | if InterpreterValidator.validate(itp_path): 441 | # 用户选择的解释器有效 442 | 443 | new_pyenv = PyEnv(itp_path, type_=None) 444 | 445 | # 但在该环境中没有安装 PyInstaller,询问用户是否继续操作 446 | if not new_pyenv.pkg_installed("pyinstaller"): 447 | # TODO 自实现 MessageBox,包含"仍要使用"、"取消"、"尝试安装PyInstaller"三个按钮 448 | result = QMessageBox.warning( 449 | self, 450 | CenterWidget.tr("Warning"), 451 | CenterWidget.tr( 452 | "Pyinstaller doesn't seem to be installed in this Python " 453 | "environment, still continue?" 454 | ), 455 | QMessageBox.StandardButton.Ok, 456 | QMessageBox.StandardButton.Cancel, 457 | ) 458 | if result == QMessageBox.StandardButton.Cancel: 459 | return 460 | 461 | # TODO 将 ALL_PY_ENVs.append() 和 pyenv_combobox.addItem() 统一管理 462 | ALL_PY_ENVs.append(new_pyenv) 463 | new_item = self.pyenv_combobox.gen_item(new_pyenv, len(ALL_PY_ENVs) - 1) 464 | self.pyenv_combobox.addItem(*new_item) 465 | self.pyenv_combobox.setCurrentIndex(self.pyenv_combobox.count() - 1) 466 | 467 | else: 468 | # 用户选择的解释器无效 469 | self.itp_dlg.close() 470 | result = QMessageBox.critical( 471 | self, 472 | CenterWidget.tr("Error"), 473 | CenterWidget.tr( 474 | "The selection is not a valid Python interpreter, " 475 | "please reselect it!" 476 | ), 477 | QMessageBox.StandardButton.Cancel, 478 | QMessageBox.StandardButton.Ok, 479 | ) 480 | if result == QMessageBox.StandardButton.Ok: 481 | self.itp_dlg.exec() 482 | 483 | def show_status_msg(self, msg: str) -> None: 484 | """向父控件(QMainWindow)的状态栏显示信息 485 | 486 | :param msg: 要在状态栏显示的信息 487 | """ 488 | 489 | self.parent_widget.statusBar().showMessage(msg) 490 | 491 | 492 | class WinMacCenterWidget(CenterWidget): 493 | """包含 Windows 与 MacOS 特有功能的主界面中央控件 494 | 495 | 适用于所有平台功能的控件已由基类 `CenterWidget` 提供,此类仅实现平台特有功能对应的控件 496 | """ 497 | 498 | def __init__(self, parent=QMainWindow): 499 | """ 500 | :param parent: 父控件对象,应为主程序主窗口 501 | """ 502 | 503 | # 应用图标 504 | self.icon_path_label = QLabel() 505 | self.icon_file_dlg = IconFileDlg() 506 | self.icon_browse_btn = QPushButton() 507 | self.icon_path_le = QLineEdit() 508 | 509 | # 是否为stdio启用终端 510 | self.console_checkbox = QCheckBox() 511 | 512 | super().__init__(parent) 513 | 514 | def _setup_ui(self) -> None: 515 | """设置各种控件的属性""" 516 | 517 | super()._setup_ui() 518 | 519 | self.icon_path_label.setText(WinMacCenterWidget.tr("App icon:")) 520 | self.icon_path_le.setReadOnly(True) 521 | self.icon_path_le.setPlaceholderText(WinMacCenterWidget.tr("Path to icon file")) 522 | self.icon_browse_btn.setText(WinMacCenterWidget.tr("Browse")) 523 | self.icon_browse_btn.setFixedWidth(80) 524 | 525 | self.console_checkbox.setText( 526 | WinMacCenterWidget.tr("Open a console window for standard I/O") 527 | ) 528 | self.console_checkbox.setChecked(True) # 默认值 529 | 530 | # 将 PyInstaller 选项详情设置成各控件的 ToolTip 531 | if self.pyinstaller_option_table.option_dict: 532 | opt = self.pyinstaller_option_table.option_dict 533 | self.icon_path_label.setToolTip( 534 | opt[ 535 | '-i , ' 536 | "--icon ' 538 | ] 539 | ) 540 | self.console_checkbox.setToolTip(opt["-c, --console, --nowindowed"]) 541 | 542 | def _connect_slots(self) -> None: 543 | """定义、连接信号与槽""" 544 | 545 | super()._connect_slots() 546 | 547 | @QtCore.Slot(str) 548 | def handle_icon_file_selected(file_path: str) -> None: 549 | """图标文件完成选择的槽函数 550 | 551 | :param file_path: 图标路径 552 | """ 553 | 554 | self.option_selected.emit((PyInstOpt.icon_path, file_path)) 555 | 556 | @QtCore.Slot(bool) 557 | def handle_console_selected(console: bool) -> None: 558 | """选择打包的程序是否为stdio启用终端的槽 559 | 560 | :param console: 是否启用终端 561 | """ 562 | 563 | if console: 564 | self.option_selected.emit((PyInstOpt.console, "--console")) 565 | self.show_status_msg(WinMacCenterWidget.tr("Terminal will be enabled.")) 566 | else: 567 | self.option_selected.emit((PyInstOpt.console, "--windowed")) 568 | self.show_status_msg( 569 | WinMacCenterWidget.tr("Terminal will not be enabled.") 570 | ) 571 | 572 | # 图标文件 573 | self.icon_browse_btn.clicked.connect(self.icon_file_dlg.open) 574 | self.icon_file_dlg.fileSelected.connect(handle_icon_file_selected) 575 | 576 | # 是否启用 stdio 终端 577 | self.console_checkbox.toggled.connect(handle_console_selected) 578 | 579 | # noinspection DuplicatedCode 580 | def _set_layout(self) -> None: 581 | """设置布局管理器""" 582 | 583 | super()._set_layout() 584 | 585 | icon_layout = QGridLayout() 586 | icon_layout.addWidget(self.icon_path_label, 0, 0, 1, 2) 587 | icon_layout.addWidget(self.icon_path_le, 1, 0) 588 | icon_layout.addWidget(self.icon_browse_btn, 1, 1) 589 | 590 | self.main_layout.insertWidget(7, self.console_checkbox) 591 | self.main_layout.addStretch(10) 592 | self.main_layout.insertLayout(8, icon_layout) 593 | 594 | @QtCore.Slot(tuple) 595 | def handle_option_set(self, option: tuple[PyInstOpt, str]) -> None: 596 | """处理option_set信号的槽,根据已经成功设置的选项调整界面 597 | 598 | :param option: 选项键值对 599 | """ 600 | 601 | super().handle_option_set(option) 602 | 603 | option_key, option_value = option 604 | 605 | if option_key == PyInstOpt.script_path: 606 | # 根据入口脚本路径,重新设置图标文件对话框的初始路径 607 | script_path = Path(option_value) 608 | self.icon_file_dlg.setDirectory(str(script_path.parent.absolute())) 609 | 610 | elif option_key == PyInstOpt.icon_path: 611 | icon_path = Path(option_value) 612 | self.icon_path_le.setText(icon_path.name) 613 | msg = ( 614 | WinMacCenterWidget.tr("Opened icon path: ") 615 | + f"{str(icon_path.absolute())}" 616 | ) 617 | self.show_status_msg(msg) 618 | 619 | @QtCore.Slot(PyInstOpt) 620 | def handle_option_error(self, option: PyInstOpt) -> None: 621 | """处理option_error信号的槽,重置设置失败的选项对应的界面,并向用户发出警告 622 | 623 | :param option: 选项 624 | """ 625 | 626 | super().handle_option_error(option) 627 | 628 | if option == PyInstOpt.icon_path: 629 | self.icon_file_dlg.close() 630 | result = QMessageBox.critical( 631 | self.parent_widget, 632 | WinMacCenterWidget.tr("Error"), 633 | WinMacCenterWidget.tr( 634 | "The selection is not a valid icon file, please re-select it!" 635 | ), 636 | QMessageBox.StandardButton.Cancel, 637 | QMessageBox.StandardButton.Ok, 638 | ) 639 | if result == QMessageBox.StandardButton.Cancel: 640 | self.icon_path_le.clear() 641 | elif result == QMessageBox.StandardButton.Ok: 642 | self.icon_file_dlg.exec() 643 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/dialog_widgets.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块集中处理数个对话框(QDialog)控件""" 5 | 6 | __all__ = [ 7 | "ScriptFileDlg", 8 | "IconFileDlg", 9 | "InterpreterFileDlg", 10 | "AboutDlg", 11 | "PkgBrowserDlg", 12 | ] 13 | 14 | import warnings 15 | from typing import Optional 16 | 17 | from PySide6.QtCore import Qt 18 | from PySide6.QtGui import QIcon, QPixmap 19 | from PySide6.QtWidgets import ( 20 | QDialog, 21 | QFileDialog, 22 | QHeaderView, 23 | QMessageBox, 24 | QTableWidget, 25 | QTableWidgetItem, 26 | QVBoxLayout, 27 | QWidget, 28 | ) 29 | 30 | from ..Constants import RUNTIME_INFO, Platform 31 | from ..Utilities import QObjTr, QtFileOpen 32 | 33 | 34 | class ScriptFileDlg(QObjTr, QFileDialog): 35 | """用于获取入口脚本文件的对话框""" 36 | 37 | def __init__(self, parent: Optional[QWidget] = None) -> None: 38 | """ 39 | :param parent: 父控件对象 40 | """ 41 | 42 | super().__init__(parent) 43 | 44 | self.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) 45 | self.setViewMode(QFileDialog.ViewMode.Detail) 46 | self.setNameFilters( 47 | ( 48 | ScriptFileDlg.tr("Python Script (*.py *.pyw)"), 49 | ScriptFileDlg.tr("All Files (*)"), 50 | ) 51 | ) 52 | self.setFileMode(QFileDialog.FileMode.ExistingFiles) 53 | self.setLabelText( 54 | QFileDialog.DialogLabel.FileName, ScriptFileDlg.tr("Python Entry File") 55 | ) 56 | self.setLabelText( 57 | QFileDialog.DialogLabel.FileType, ScriptFileDlg.tr("Python File") 58 | ) 59 | self.setLabelText(QFileDialog.DialogLabel.Accept, ScriptFileDlg.tr("Open")) 60 | self.setLabelText(QFileDialog.DialogLabel.Reject, ScriptFileDlg.tr("Cancel")) 61 | 62 | 63 | class IconFileDlg(QObjTr, QFileDialog): 64 | """用于获取应用图标文件的对话框""" 65 | 66 | def __init__(self, parent: Optional[QWidget] = None) -> None: 67 | """ 68 | :param parent: 父控件对象 69 | """ 70 | 71 | super().__init__(parent) 72 | 73 | self.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) 74 | self.setViewMode(QFileDialog.ViewMode.Detail) 75 | self.setNameFilters( 76 | ( 77 | IconFileDlg.tr("Icon Files (*.ico *.icns)"), 78 | IconFileDlg.tr("All Files (*)"), 79 | ) 80 | ) 81 | self.setFileMode(QFileDialog.FileMode.ExistingFile) 82 | self.setLabelText(QFileDialog.DialogLabel.FileName, IconFileDlg.tr("App Icon")) 83 | self.setLabelText(QFileDialog.DialogLabel.FileType, IconFileDlg.tr("Icon File")) 84 | self.setLabelText(QFileDialog.DialogLabel.Accept, IconFileDlg.tr("Open")) 85 | self.setLabelText(QFileDialog.DialogLabel.Reject, IconFileDlg.tr("Cancel")) 86 | 87 | 88 | class InterpreterFileDlg(QObjTr, QFileDialog): 89 | """用于获取 Python 解释器可执行文件的对话框""" 90 | 91 | def __init__(self, parent: Optional[QWidget] = None) -> None: 92 | """ 93 | :param parent: 父控件对象 94 | """ 95 | 96 | super().__init__(parent) 97 | 98 | self.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) 99 | self.setViewMode(QFileDialog.ViewMode.Detail) 100 | self.setFileMode(QFileDialog.FileMode.ExistingFile) 101 | self.setLabelText( 102 | QFileDialog.DialogLabel.FileName, 103 | InterpreterFileDlg.tr("Python Interpreter"), 104 | ) 105 | self.setLabelText( 106 | QFileDialog.DialogLabel.FileType, InterpreterFileDlg.tr("Executable File") 107 | ) 108 | 109 | if RUNTIME_INFO.platform == Platform.windows: 110 | self.setNameFilters( 111 | ( 112 | InterpreterFileDlg.tr("Python Interpreter (python.exe)"), 113 | InterpreterFileDlg.tr("Executable Files (*.exe)"), 114 | InterpreterFileDlg.tr("All Files (*)"), 115 | ) 116 | ) 117 | else: 118 | self.setNameFilters( 119 | ( 120 | InterpreterFileDlg.tr("Python Interpreter (python3*)"), 121 | InterpreterFileDlg.tr("All Files (*)"), 122 | ) 123 | ) 124 | 125 | 126 | class AboutDlg(QObjTr, QMessageBox): 127 | """用于显示关于信息的对话框""" 128 | 129 | def __init__(self, parent: Optional[QWidget] = None) -> None: 130 | """ 131 | :param parent: 父控件对象 132 | """ 133 | 134 | super().__init__(parent) 135 | 136 | self._about_text: str = "" 137 | self.setWindowTitle(AboutDlg.tr("About")) 138 | self.setStandardButtons(QMessageBox.StandardButton.Ok) 139 | self.setTextFormat(Qt.TextFormat.MarkdownText) 140 | self.setText(self.about_text) 141 | self.setIconPixmap(QPixmap(":/Icons/Py2exe-GUI_icon_72px")) 142 | 143 | @property 144 | def about_text(self) -> str: 145 | """返回本程序的关于信息文本 146 | 147 | :return: 关于信息 148 | """ 149 | 150 | try: 151 | # 因使用qrc/rcc系统,所以使用Qt风格读取文本文件 152 | with QtFileOpen(":/Texts/About_Text", encoding="utf-8") as about_file: 153 | self._about_text = about_file.read() 154 | except OSError as e: 155 | warnings.warn( 156 | f"Cannot open About document: {e}", RuntimeWarning, stacklevel=1 157 | ) 158 | self._about_text = AboutDlg.tr( 159 | "Can't open the About document, try to reinstall this program." 160 | ) 161 | 162 | return self._about_text 163 | 164 | 165 | class PkgBrowserDlg(QObjTr, QDialog): 166 | """浏览已安装的所有包的对话框""" 167 | 168 | def __init__(self, parent: Optional[QWidget] = None) -> None: 169 | """ 170 | :param parent: 父控件对象 171 | """ 172 | 173 | super().__init__(parent) 174 | 175 | self.pkg_list: list[tuple[str, str]] = [] # [("black", "23.12.1"), ...] 176 | self.pkg_table = QTableWidget(self) 177 | 178 | self._setup_ui() 179 | 180 | def _setup_ui(self) -> None: 181 | """处理 UI""" 182 | 183 | self.setWindowTitle(PkgBrowserDlg.tr("Installed Packages")) 184 | self.setWindowIcon(QIcon(QPixmap(":/Icons/Python_128px"))) 185 | 186 | self.pkg_table.setColumnCount(2) 187 | self.pkg_table.setHorizontalHeaderLabels( 188 | [PkgBrowserDlg.tr("Name"), PkgBrowserDlg.tr("Version")] 189 | ) 190 | self.pkg_table.horizontalHeader().setSectionResizeMode( 191 | 0, QHeaderView.ResizeMode.Stretch 192 | ) 193 | self.pkg_table.horizontalHeader().setSectionResizeMode( 194 | 1, QHeaderView.ResizeMode.Stretch 195 | ) 196 | 197 | main_layout = QVBoxLayout() 198 | main_layout.setSpacing(0) 199 | main_layout.setContentsMargins(0, 0, 0, 0) 200 | main_layout.addWidget(self.pkg_table) 201 | self.setLayout(main_layout) 202 | 203 | def load_pkg_list(self, pkg_list: list[dict[str, str]]) -> None: 204 | """从后端加载包数据,存储到实例属性 pkg_list 中,并更新界面 205 | 206 | self.pkg_list 形如 [("black", "23.12.1"), ...] 207 | 208 | :param pkg_list: 已安装的包列表,形如 [{"name": "black", "version": "23.12.1"}, {}, ...] 209 | """ 210 | 211 | package_lists = [] 212 | for pkg in pkg_list: 213 | pkg_name = pkg["name"] 214 | pkg_version = pkg["version"] 215 | package_lists.append((pkg_name, pkg_version)) 216 | self.pkg_list = package_lists 217 | self._pkg_table_update() 218 | 219 | def _pkg_table_update(self) -> None: 220 | """更新包列表控件显示内容""" 221 | 222 | self.pkg_table.setRowCount(len(self.pkg_list)) 223 | for row, pkg in enumerate(self.pkg_list): 224 | self.pkg_table.setItem(row, 0, QTableWidgetItem(pkg[0])) 225 | self.pkg_table.setItem(row, 1, QTableWidgetItem(pkg[1])) 226 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/main_window.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块主要包含 `MainWindow` 主窗口控件,定义并配置了一个有工具栏、中央控件、状态栏的主窗口 5 | 6 | 仅包含控件(前端界面)部分,不包含打包任务等(后端)功能 7 | """ 8 | 9 | __all__ = ["open_url", "MainWindow"] 10 | 11 | from PySide6.QtCore import QUrl 12 | from PySide6.QtGui import QDesktopServices, QIcon, QPixmap 13 | from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QMenuBar, QStatusBar 14 | 15 | from ..Constants import RUNTIME_INFO, APP_URLs, AppConstant, Platform 16 | from ..Utilities.qobject_tr import QObjTr 17 | from .center_widget import CenterWidget, WinMacCenterWidget 18 | from .dialog_widgets import AboutDlg 19 | 20 | 21 | def open_url(url: str) -> None: 22 | """辅助函数,在系统默认浏览器中打开 `url` 23 | 24 | :param url: 待打开的URL 25 | """ 26 | 27 | QDesktopServices.openUrl(QUrl(url)) 28 | 29 | 30 | class MainWindow(QObjTr, QMainWindow): 31 | """ 32 | 主界面主窗口 33 | """ 34 | 35 | def __init__(self, *args, **kwargs) -> None: 36 | super().__init__(*args, **kwargs) 37 | 38 | self.center_widget: CenterWidget 39 | if RUNTIME_INFO.platform in (Platform.windows, Platform.macos): 40 | self.center_widget = WinMacCenterWidget(self) 41 | else: 42 | self.center_widget = CenterWidget(self) 43 | 44 | self.menu_bar = QMenuBar(self) 45 | self.status_bar = QStatusBar(self) 46 | 47 | self.setCentralWidget(self.center_widget) 48 | self.setMenuBar(self.menu_bar) 49 | self.setStatusBar(self.status_bar) 50 | 51 | self._setup() 52 | 53 | def _setup(self) -> None: 54 | """设置主窗口""" 55 | 56 | self.setWindowTitle("Py2exe-GUI") 57 | self.setMinimumSize(350, 430) 58 | # self.resize(800, 600) 59 | self.setWindowIcon(QIcon(QPixmap(":/Icons/Py2exe-GUI_icon_72px"))) 60 | 61 | self._setup_menu_bar() 62 | self._setup_status_bar() 63 | 64 | def _setup_menu_bar(self) -> None: 65 | """配置主窗口菜单栏""" 66 | 67 | file_menu = self.menu_bar.addMenu(MainWindow.tr("&File")) 68 | file_menu.addAction( 69 | MainWindow.tr("Import Config From JSON File") 70 | ) # 暂时只为占位 71 | file_menu.addAction(MainWindow.tr("Export Config To JSON File")) # 暂时只为占位 72 | file_menu.addSeparator() 73 | file_menu.addAction(MainWindow.tr("&Settings")) # 暂时只为占位 74 | file_menu.addSeparator() 75 | file_menu.addAction(MainWindow.tr("&Exit"), self.close) 76 | 77 | help_menu = self.menu_bar.addMenu(MainWindow.tr("&Help")) 78 | 79 | help_menu.addAction( 80 | MainWindow.tr("PyInstaller Documentation"), 81 | lambda: open_url(APP_URLs["Pyinstaller_doc"]), 82 | ) 83 | help_menu.addAction( 84 | MainWindow.tr("PyInstaller Options Details"), 85 | self.center_widget.pyinstaller_option_table.show, 86 | ) 87 | help_menu.addSeparator() 88 | help_menu.addAction( 89 | MainWindow.tr("Report Bugs"), lambda: open_url(APP_URLs["BugTracker"]) 90 | ) 91 | 92 | about_menu = self.menu_bar.addMenu(MainWindow.tr("&About")) 93 | about_menu.addAction(MainWindow.tr("About This Program"), AboutDlg(self).exec) 94 | about_menu.addAction(MainWindow.tr("About &Qt"), QApplication.aboutQt) 95 | 96 | def _setup_status_bar(self) -> None: 97 | """配置主窗口状态栏""" 98 | 99 | # 在最右侧固定显示版本信息 100 | version_label = QLabel("V" + AppConstant.VERSION, self.status_bar) 101 | self.status_bar.insertPermanentWidget(0, version_label) 102 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/multi_item_edit_widget.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块包含一组供用户编辑多个文本条目的控件,用于实现 PyInstaller 的 --hidden-import 等可以多次调用的选项 5 | 6 | `MultiItemEditWindow` 是最主要的类,提供一个左侧有条目展示与编辑、右侧有删减条目按钮的窗口 7 | 8 | `MultiPkgEditWindow` 继承自 `MultiItemEditWindow`,多了一个浏览当前 Python 环境中已安装 Python 包的功能 9 | """ 10 | 11 | __all__ = [ 12 | "MultiItemEditWindow", 13 | "MultiPkgEditWindow", 14 | ] 15 | 16 | from typing import Optional 17 | 18 | from PySide6.QtCore import Qt, Signal, Slot 19 | from PySide6.QtGui import QIcon, QPixmap 20 | from PySide6.QtWidgets import ( 21 | QHBoxLayout, 22 | QListWidget, 23 | QListWidgetItem, 24 | QPushButton, 25 | QVBoxLayout, 26 | QWidget, 27 | ) 28 | 29 | from ..Utilities import QObjTr 30 | from .dialog_widgets import PkgBrowserDlg 31 | 32 | 33 | class MultiItemEditWindow(QObjTr, QWidget): 34 | """用于添加多个条目的窗口控件,实现如 --hidden-import、--collect-submodules 等功能""" 35 | 36 | # 用户在添加条目窗口完成所有编辑后,提交的信号.完整数据类型为 list[str] 37 | items_selected = Signal(list) 38 | 39 | def __init__(self, parent: Optional[QWidget] = None) -> None: 40 | """ 41 | :param parent: 父控件对象 42 | """ 43 | 44 | super().__init__(parent) 45 | 46 | # 条目列表控件 47 | self.item_list_widget = QListWidget(self) 48 | 49 | # 可选中且可编辑 50 | self._QListWidgetItem_flag = ( 51 | Qt.ItemFlag.ItemIsEnabled 52 | | Qt.ItemFlag.ItemIsSelectable 53 | | Qt.ItemFlag.ItemIsEditable 54 | ) 55 | 56 | # 编辑功能按键 57 | self.new_btn = QPushButton(self) 58 | self.delete_btn = QPushButton(self) 59 | 60 | # 整个窗口的确认和取消键 61 | self.ok_btn = QPushButton(self) 62 | self.cancel_btn = QPushButton(self) 63 | 64 | self._setup_ui() 65 | self._setup_layout() 66 | self._connect_slots() 67 | 68 | def _setup_ui(self) -> None: 69 | """处理 UI""" 70 | 71 | self.setWindowIcon(QIcon(QPixmap(":/Icons/Py2exe-GUI_icon_72px"))) 72 | 73 | self.new_btn.setText(MultiItemEditWindow.tr("&New")) 74 | self.delete_btn.setText(MultiItemEditWindow.tr("&Delete")) 75 | 76 | self.ok_btn.setText(MultiItemEditWindow.tr("&OK")) 77 | self.cancel_btn.setText(MultiItemEditWindow.tr("&Cancel")) 78 | 79 | # noinspection DuplicatedCode 80 | def _setup_layout(self) -> None: 81 | """构建与设置布局管理器""" 82 | 83 | btn_group_boxlayout = QVBoxLayout() 84 | self.btn_group_boxlayout = btn_group_boxlayout 85 | btn_group_boxlayout.addWidget(self.new_btn) 86 | btn_group_boxlayout.addWidget(self.delete_btn) 87 | btn_group_boxlayout.addStretch(10) 88 | 89 | upper_box = QHBoxLayout() 90 | upper_box.addWidget(self.item_list_widget) 91 | upper_box.addLayout(btn_group_boxlayout) 92 | 93 | lower_box = QHBoxLayout() 94 | lower_box.addStretch(10) 95 | lower_box.addWidget(self.ok_btn) 96 | lower_box.addWidget(self.cancel_btn) 97 | 98 | main_layout = QVBoxLayout() 99 | main_layout.addLayout(upper_box) 100 | main_layout.addLayout(lower_box) 101 | 102 | self.setLayout(main_layout) 103 | 104 | def _connect_slots(self) -> None: 105 | """构建各槽函数、连接信号""" 106 | 107 | @Slot() 108 | def handle_new_btn() -> None: 109 | """新建按钮点击的槽函数""" 110 | 111 | new_item = QListWidgetItem("") 112 | new_item.setFlags(self._QListWidgetItem_flag) 113 | self.item_list_widget.addItem(new_item) 114 | self.item_list_widget.editItem(new_item) 115 | 116 | @Slot() 117 | def handle_delete_btn() -> None: 118 | """删除按钮点击的槽函数""" 119 | 120 | self.item_list_widget.takeItem(self.item_list_widget.currentRow()) 121 | 122 | @Slot() 123 | def handle_ok_btn() -> None: 124 | """确定按钮点击的槽函数,将用户编辑好的条目列表以信号方式传出,并自身关闭窗口""" 125 | 126 | self.items_selected.emit(self._submit()) 127 | self.close() 128 | 129 | self.new_btn.clicked.connect(handle_new_btn) 130 | self.delete_btn.clicked.connect(handle_delete_btn) 131 | 132 | self.cancel_btn.clicked.connect(self.close) 133 | self.ok_btn.clicked.connect(handle_ok_btn) 134 | 135 | def _submit(self) -> list[str]: 136 | """将控件界面内容整理为字符串列表,准备提交 137 | 138 | 会自动删去空白行 139 | 140 | :return: 条目列表 141 | """ 142 | 143 | item_list = [] 144 | 145 | for row in range(self.item_list_widget.count()): 146 | item_text = self.item_list_widget.item(row).text() 147 | if item_text == "": 148 | self.item_list_widget.takeItem(row) 149 | else: 150 | item_list.append(item_text) 151 | 152 | return item_list 153 | 154 | def load_items(self, items: list[str]) -> None: 155 | """从给定的条目列表加载界面控件,用于打开先前已保存过的条目 156 | 157 | :param items: 条目列表 158 | """ 159 | 160 | self.item_list_widget.clear() 161 | 162 | for item in items: 163 | list_widget_item = QListWidgetItem(item) 164 | list_widget_item.setFlags(self._QListWidgetItem_flag) 165 | self.item_list_widget.addItem(list_widget_item) 166 | 167 | 168 | class MultiPkgEditWindow(MultiItemEditWindow): 169 | """用于添加多个Python模块条目的窗口控件,实现如 --hidden-import 等选项 170 | 171 | 相比MultiItemEditWindow,主要是多了一个浏览当前 Python 环境中所有已安装第三方包 172 | 并将包名作为条目添加的功能 173 | """ 174 | 175 | def __init__( 176 | self, pkg_browser_dlg: PkgBrowserDlg, parent: Optional[QWidget] = None 177 | ) -> None: 178 | """ 179 | :param pkg_browser_dlg: PkgBrowserDlg 实例,用于显示已安装包的列表 180 | :param parent: 父控件对象 181 | """ 182 | 183 | # TODO 新增一种可以由用户选中的已安装包浏览器,并替代目前的 PkgBrowserDlg 184 | 185 | self.browse_pkg_button = QPushButton() 186 | self.pkg_browser_dlg = pkg_browser_dlg 187 | super().__init__(parent) 188 | 189 | def _setup_ui(self) -> None: 190 | """处理 UI""" 191 | 192 | super()._setup_ui() 193 | self.browse_pkg_button.setText(MultiPkgEditWindow.tr("&Browse packages")) 194 | 195 | def _setup_layout(self) -> None: 196 | """构建与设置布局管理器""" 197 | 198 | super()._setup_layout() 199 | self.btn_group_boxlayout.insertWidget(2, self.browse_pkg_button) 200 | 201 | def _connect_slots(self) -> None: 202 | """构建各槽函数、连接信号""" 203 | 204 | super()._connect_slots() 205 | self.browse_pkg_button.clicked.connect(self.pkg_browser_dlg.exec) 206 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/pyenv_combobox.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """本模块主要包含用于选择 Python 解释器环境的下拉框控件 `PyEnvComboBox`""" 5 | 6 | __all__ = ["PyEnvComboBox"] 7 | 8 | import sys 9 | from typing import Optional 10 | 11 | from PySide6.QtCore import QSize 12 | from PySide6.QtGui import QIcon 13 | from PySide6.QtWidgets import QComboBox, QWidget 14 | 15 | from ..Constants import RUNTIME_INFO, PyEnvType 16 | from ..Utilities import ALL_PY_ENVs, PyEnv, get_sys_python 17 | 18 | 19 | class PyEnvComboBox(QComboBox): 20 | """用于选择解释器环境的下拉框""" 21 | 22 | def __init__(self, parent: Optional[QWidget] = None) -> None: 23 | """ 24 | :param parent: 父控件对象 25 | """ 26 | 27 | super().__init__(parent) 28 | 29 | self.setIconSize(QSize(18, 18)) 30 | self.setMinimumHeight(24) # 确保图标显示完整 31 | self._add_default_item() 32 | 33 | def _add_default_item(self) -> None: 34 | """添加默认解释器环境条目""" 35 | 36 | if not RUNTIME_INFO.is_bundled: 37 | # 在非 PyInstaller 捆绑环境中,第一项为当前用于运行 Py2exe-GUI 的 Python 环境 38 | default_pyenv = PyEnv(sys.executable, None) 39 | else: 40 | # 若已由 PyInstaller 捆绑成冻结应用程序,则第一项为系统 Python 环境 41 | default_pyenv = PyEnv(get_sys_python(), PyEnvType.system) 42 | 43 | ALL_PY_ENVs.append(default_pyenv) 44 | self.addItem(*self.gen_item(default_pyenv, 0)) 45 | 46 | @staticmethod 47 | def gen_item(pyenv: PyEnv, index: int) -> tuple: 48 | """根据传入的 Python 环境,生成一个适用于 QComboBox.addItem() 参数的三元素元组 49 | 50 | :param pyenv: Python 解释器环境 51 | :param index: 该环境在全局变量 ALL_PY_ENVs 中的索引值 52 | :return: (icon, text, data) 53 | :raise ValueError: PyEnv 类型无效时抛出 54 | """ 55 | 56 | if not isinstance(pyenv.type, PyEnvType): 57 | raise ValueError( 58 | f'Current PyEnv type "{pyenv.type}" is not instance of "PyEnvType"' 59 | ) 60 | 61 | data = index 62 | version = pyenv.pyversion 63 | 64 | icon_map = { 65 | PyEnvType.system: QIcon(":/Icons/Python_128px"), 66 | PyEnvType.venv: QIcon(":/Icons/Python_cyan"), 67 | PyEnvType.poetry: QIcon(":/Icons/Poetry"), 68 | PyEnvType.conda: QIcon(":/Icons/Conda"), 69 | PyEnvType.unknown: QIcon(":/Icons/Python_128px"), 70 | } 71 | 72 | text_map = { 73 | PyEnvType.system: f"Python {version} (System)", 74 | PyEnvType.venv: f"Python {version} (venv)", 75 | PyEnvType.poetry: f"Poetry [Python {version}]", 76 | PyEnvType.conda: f"Conda [Python {version}]", 77 | PyEnvType.unknown: f"Python {version}", 78 | } 79 | # TODO 根据路径为环境命名,如 "Poetry (Py2exe-GUI) [Python 3.11.7]" 80 | 81 | icon = icon_map.get(pyenv.type, QIcon(":/Icons/Python_128px")) 82 | text = text_map.get(pyenv.type, f"Python {version}") 83 | 84 | return icon, text, data 85 | 86 | def get_current_pyenv(self) -> PyEnv: 87 | """通过当前选中的条目索引值,从全局变量 `ALL_PY_ENVs` 中获取对应的 Python 环境 88 | 89 | :return: 当前在下拉框中选定的 Python 环境 90 | """ 91 | 92 | current_pyenv: PyEnv = ALL_PY_ENVs[self.currentData()] 93 | 94 | return current_pyenv 95 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/pyinstaller_option_widget.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """本模块主要用于加载、解析、界面显示 PyInstaller 选项的详细描述 5 | 6 | `load_pyinst_options()` 函数用于从数据文件中读取并解析 PyInstaller 命令选项信息,按运行时平台筛选后返回; 7 | `PyinstallerOptionTable` 类是用于显示 PyInstaller 命令行选项的表格控件窗口,界面有待进一步优化 8 | """ 9 | 10 | __all__ = [ 11 | "load_pyinst_options", 12 | "PyinstallerOptionTable", 13 | ] 14 | 15 | import warnings 16 | 17 | import yaml 18 | from PySide6.QtGui import QPixmap 19 | from PySide6.QtWidgets import QHeaderView, QTableWidget, QTableWidgetItem 20 | 21 | from ..Constants import RUNTIME_INFO 22 | from ..Utilities import QObjTr, QtFileOpen 23 | 24 | 25 | def load_pyinst_options() -> dict[str, str]: 26 | """从数据文件中读取并解析 PyInstaller 命令选项信息,按运行时平台筛选后返回 27 | 28 | 若加载失败,则抛出警告、返回空字典 29 | 由于涉及QRC资源文件读取与遍历,耗时稍长,应尽量减少此函数调用次数 30 | 31 | :return: 选项信息字典,{option: description} 32 | """ 33 | 34 | try: 35 | with QtFileOpen(":/Texts/PyInstaller_Options", encoding="utf-8") as option_file: 36 | option_file_text = option_file.read() 37 | except OSError as e: 38 | warnings.warn( 39 | f"Failed to load PyInstaller Options: {e}", RuntimeWarning, stacklevel=1 40 | ) 41 | return dict() 42 | 43 | try: 44 | # 优先使用性能更高的 CLoader 进行解析 45 | opt_data = yaml.load(option_file_text, Loader=yaml.CLoader) 46 | except AttributeError: 47 | # 如果没有可用的 C 扩展,则使用纯 Python 解析 48 | # https://pyyaml.org/wiki/PyYAMLDocumentation 49 | opt_data = yaml.load(option_file_text, Loader=yaml.Loader) 50 | 51 | option_dict = { 52 | option["option"]: option["description"] 53 | for option in opt_data["options"] 54 | if ( 55 | option["platform"] == ["all"] 56 | or RUNTIME_INFO.platform.value in option["platform"] 57 | ) 58 | } 59 | 60 | return option_dict 61 | 62 | 63 | class PyinstallerOptionTable(QObjTr, QTableWidget): 64 | """用于显示 PyInstaller 命令行选项的表格控件""" 65 | 66 | def __init__(self) -> None: 67 | super().__init__() 68 | 69 | # 设置界面 70 | self.setWindowTitle("PyInstaller 命令选项") 71 | self.setMinimumSize(700, 430) 72 | self.setWindowIcon(QPixmap(":/Icons/PyInstaller")) 73 | self.setColumnCount(2) 74 | self.setHorizontalHeaderLabels( 75 | [ 76 | PyinstallerOptionTable.tr("Option"), 77 | PyinstallerOptionTable.tr("Description"), 78 | ] 79 | ) 80 | self.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) 81 | 82 | # 存储选项信息的字典 83 | self.option_dict = load_pyinst_options() 84 | self._set_option_items() 85 | 86 | def _set_option_items(self) -> None: 87 | """加载选项信息、为表格控件中填充条目""" 88 | 89 | # 填充条目 90 | self.setRowCount(len(self.option_dict)) 91 | for index, (option, description) in enumerate(self.option_dict.items()): 92 | self.setItem(index, 0, QTableWidgetItem(option)) 93 | self.setItem(index, 1, QTableWidgetItem(description)) 94 | -------------------------------------------------------------------------------- /src/py2exe_gui/Widgets/subprocess_widget.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """此模块主要包含用于呈现 PyInstaller 进程运行状态和输出的控件 `SubProcessDlg`""" 5 | 6 | __all__ = ["SubProcessDlg"] 7 | 8 | from PySide6.QtCore import Slot 9 | from PySide6.QtGui import QCloseEvent 10 | from PySide6.QtWidgets import ( 11 | QDialog, 12 | QLabel, 13 | QPushButton, 14 | QTextBrowser, 15 | QVBoxLayout, 16 | QWidget, 17 | ) 18 | 19 | from ..Core.subprocess_tool import SubProcessTool 20 | from ..Utilities import QObjTr 21 | 22 | 23 | class SubProcessDlg(QObjTr, QDialog): 24 | """用于显示子进程信息的对话框""" 25 | 26 | def __init__(self, parent: QWidget) -> None: 27 | """ 28 | :param parent: 父控件对象,必须为 MainApp 类 29 | """ 30 | 31 | super().__init__(parent) 32 | 33 | self.info_label = QLabel(self) 34 | self.text_browser = QTextBrowser(self) # 用于显示子进程输出内容 35 | # 可用于“取消”“打开输出位置”等的多功能按钮 36 | self.multifunction_btn = QPushButton(self) 37 | self._setup() 38 | 39 | def _setup(self) -> None: 40 | """配置子进程信息对话框""" 41 | 42 | self.setWindowTitle("PyInstaller") 43 | self.setMinimumWidth(500) 44 | self.setModal(True) 45 | 46 | # 布局管理器 47 | main_layout = QVBoxLayout() 48 | main_layout.addWidget(self.info_label) 49 | main_layout.addWidget(self.text_browser) 50 | main_layout.addWidget(self.multifunction_btn) 51 | self.setLayout(main_layout) 52 | 53 | @Slot(tuple) 54 | def handle_output( 55 | self, subprocess_output: tuple[SubProcessTool.OutputType, str] 56 | ) -> None: 57 | """处理子进程的输出 58 | 59 | :param subprocess_output: 子进程输出,应为二元素元组,第一项为 SubProcessTool.OutputType 60 | :raise ValueError: 子进程输出的类型不正确 61 | """ 62 | 63 | output_type, output_text = subprocess_output 64 | 65 | if output_type == SubProcessTool.OutputType.STATE: 66 | self.info_label.setText(output_text) 67 | if output_text == "The process is running...": 68 | self.multifunction_btn.setText(SubProcessDlg.tr("Cancel")) 69 | 70 | elif ( 71 | output_type == SubProcessTool.OutputType.STDOUT 72 | or output_type == SubProcessTool.OutputType.STDERR 73 | ): 74 | self.text_browser.append(output_text) 75 | 76 | elif output_type == SubProcessTool.OutputType.FINISHED: 77 | if output_text == "0": 78 | self.info_label.setText(SubProcessDlg.tr("Done!")) 79 | self.multifunction_btn.setText(SubProcessDlg.tr("Open Dist")) 80 | else: 81 | self.info_label.setText( 82 | SubProcessDlg.tr( 83 | "Execution ends, but an error occurs and the exit code is" 84 | ) 85 | + f"{output_text}" 86 | ) 87 | self.multifunction_btn.setText(SubProcessDlg.tr("Cancel")) 88 | 89 | elif output_type == SubProcessTool.OutputType.ERROR: 90 | self.info_label.setText(SubProcessDlg.tr("PyInstaller Error!")) 91 | self.text_browser.append( 92 | SubProcessDlg.tr("PyInstaller subprocess output:") + f"{output_text}" 93 | ) 94 | self.text_browser.append( 95 | SubProcessDlg.tr( 96 | "Please check if you have installed " 97 | "the correct version of PyInstaller or not." 98 | ) 99 | ) 100 | self.multifunction_btn.setText(SubProcessDlg.tr("Close")) 101 | 102 | elif not isinstance(output_type, SubProcessTool.OutputType): 103 | raise ValueError(f"Unsupported output type: {output_type}") 104 | 105 | def closeEvent(self, event: QCloseEvent) -> None: 106 | """重写关闭事件,进行收尾清理 107 | 108 | :param event: 关闭事件 109 | """ 110 | 111 | # 显式发送一次 finished 信号,外部接收到此信号后应主动中断 PyInstaller 进程 112 | self.finished.emit(-1) 113 | 114 | self.text_browser.clear() 115 | super().closeEvent(event) 116 | -------------------------------------------------------------------------------- /src/py2exe_gui/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """Py2exe-GUI is an assist tool based on PySide6, 5 | designed to provide a complete yet easy-to-use GUI for PyInstaller. 6 | 7 | HomePage: https://github.com/muziing/Py2exe-GUI 8 | """ 9 | 10 | __version__ = "0.3.2" 11 | -------------------------------------------------------------------------------- /src/py2exe_gui/__main__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html 2 | # For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license 3 | 4 | """Py2exe-GUI 软件包入口 5 | 6 | 主要包含 `MainApp` 类,将前端界面和后端功能在此结合。 7 | 包含一个名为 `main()` 的入口函数 8 | """ 9 | 10 | import sys 11 | from pathlib import Path 12 | 13 | from PySide6.QtCore import QTranslator, Slot 14 | from PySide6.QtGui import QCloseEvent 15 | from PySide6.QtWidgets import QApplication 16 | 17 | from .Constants import RUNTIME_INFO, PyInstOpt 18 | from .Core import Packaging, PackagingTask 19 | from .Resources import COMPILED_RESOURCES # noqa 20 | from .Utilities import open_dir_in_explorer 21 | from .Widgets import MainWindow, SubProcessDlg 22 | 23 | 24 | class MainApp(MainWindow): 25 | """应用主程序""" 26 | 27 | def __init__(self, *args, **kwargs) -> None: 28 | super().__init__(*args, **kwargs) 29 | 30 | self.packaging_task = PackagingTask(self) 31 | self.packager = Packaging(self) 32 | self.subprocess_dlg = SubProcessDlg(self) 33 | 34 | self._connect_slots() 35 | 36 | self.status_bar.showMessage(MainApp.tr("Ready.")) 37 | 38 | # def show(self): 39 | # """仅供分析启动性能使用,切勿取消注释!!! 40 | # """ 41 | # 42 | # super().show() 43 | # sys.exit() 44 | 45 | def _connect_slots(self) -> None: 46 | """连接各种信号与槽""" 47 | 48 | self._connect_run_pkg_btn_slot() 49 | self._connect_mul_btn_slot(self.subprocess_dlg) 50 | 51 | self.center_widget.option_selected.connect(self.packaging_task.on_opt_selected) 52 | self.packaging_task.option_set.connect(self.packager.set_pyinstaller_args) 53 | self.packaging_task.option_set.connect(self.center_widget.handle_option_set) 54 | self.packaging_task.option_error.connect(self.center_widget.handle_option_error) 55 | self.packaging_task.ready_to_pack.connect( 56 | self.center_widget.handle_ready_to_pack 57 | ) 58 | self.packager.args_settled.connect( 59 | self.center_widget.pyinstaller_args_browser.enrich_args_text 60 | ) 61 | self.packager.subprocess.output.connect(self.subprocess_dlg.handle_output) 62 | 63 | # 用户关闭子进程对话框时中止打包进程 64 | self.subprocess_dlg.finished.connect( 65 | lambda: self.packager.subprocess.abort_process(2000) 66 | ) 67 | 68 | def _connect_run_pkg_btn_slot(self): 69 | @Slot() 70 | def handle_run_pkg_btn_clicked() -> None: 71 | """“运行打包”按钮的槽函数""" 72 | 73 | # 将当前选择的 Python 解释器作为打包使用的解释器 74 | current_pyenv = self.center_widget.pyenv_combobox.get_current_pyenv() 75 | self.packaging_task.pyenv = current_pyenv 76 | self.packager.set_python_path(current_pyenv.exe_path) 77 | 78 | # 先显示对话框窗口,后运行子进程,确保调试信息/错误信息能被直观显示 79 | self.subprocess_dlg.show() 80 | self.packager.run_packaging_process() 81 | 82 | self.center_widget.run_packaging_btn.clicked.connect(handle_run_pkg_btn_clicked) 83 | 84 | def _connect_mul_btn_slot(self, subprocess_dlg): 85 | @Slot() 86 | def handle_mul_btn_clicked() -> None: 87 | """处理子进程窗口多功能按钮点击信号的槽""" 88 | 89 | btn_text = self.subprocess_dlg.multifunction_btn.text() 90 | if btn_text == SubProcessDlg.tr("Cancel"): 91 | self.packager.subprocess.abort_process() 92 | self.subprocess_dlg.close() 93 | elif btn_text == SubProcessDlg.tr("Open Dist"): 94 | script_path: Path = self.packaging_task.using_option[ 95 | PyInstOpt.script_path 96 | ] 97 | dist_path = script_path.parent / "dist" 98 | open_dir_in_explorer(dist_path) 99 | elif btn_text == SubProcessDlg.tr("Close"): 100 | self.subprocess_dlg.close() 101 | 102 | subprocess_dlg.multifunction_btn.clicked.connect(handle_mul_btn_clicked) 103 | 104 | def closeEvent(self, event: QCloseEvent) -> None: 105 | """重写关闭事件,进行收尾清理 106 | 107 | :param event: 关闭事件 108 | """ 109 | 110 | # self.packager.subprocess.abort_process(3000) # 不会访问到此行 111 | super().closeEvent(event) 112 | 113 | 114 | def main() -> None: 115 | """应用程序主入口函数,便于 Poetry 由此函数级入口构建启动脚本""" 116 | 117 | app = QApplication(sys.argv) 118 | 119 | # TODO 翻译机制待优化 120 | translator = QTranslator() 121 | if RUNTIME_INFO.language_code == "zh_CN": 122 | translator.load(":/i18n/zh_CN.qm") 123 | app.installTranslator(translator) 124 | 125 | window = MainApp() 126 | window.show() 127 | sys.exit(app.exec()) 128 | 129 | 130 | if __name__ == "__main__": 131 | main() 132 | --------------------------------------------------------------------------------