├── .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 |
3 |
4 |
5 | Python GUI packaging tool
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 | 
30 |
31 | 
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 | 
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 |
3 |
4 |
5 | 强大易用的 Python 图形界面打包工具
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 | 
28 |
29 | 
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 | 
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 |
--------------------------------------------------------------------------------