diff --git a/pyproject.toml b/pyproject.toml
index 0d2d275..dd552c9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "qpypack"
-version = "2.4.5"
+version = "2.4.6"
description = "A modern graphical packaging config tool for PyInstaller, Nuitka."
readme = "README.md"
authors = [
@@ -15,7 +15,7 @@ requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
- "Operating System :: Microsoft :: Windows",
+ "Operating System :: OS Independent",
]
dependencies = [
diff --git a/qpypack/main.py b/qpypack/main.py
index 569f04c..df89aee 100644
--- a/qpypack/main.py
+++ b/qpypack/main.py
@@ -31,7 +31,7 @@ from PySide6.QtGui import QFont, QDragEnterEvent, QDropEvent, QTextCursor, QIcon
from PySide6.QtSvg import QSvgRenderer
__app_name__ = "QPyPack"
-__version__ = "2.4.5"
+__version__ = "2.4.6"
__author__ = "QwejayHuang"
__company__ = "QwejayHuang"
__description__ = "自动化 Python 脚本打包构建工具"
@@ -75,7 +75,7 @@ def load_config():
'cpu_cores': str(os.cpu_count() or 2), 'upx_path': '',
'exclude_modules': '', 'out_mode': '0', 'custom_out_dir': '',
'sound_notify': 'True', 'auto_save_log': 'False',
- 'use_reqs_file': '', 'add_data_list': ''
+ 'use_reqs_file': '', 'add_data_list': '', 'custom_python_path': ''
}
try:
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
@@ -95,7 +95,8 @@ def load_config():
'sound_notify': 'True',
'auto_save_log': 'False',
'use_reqs_file': '',
- 'add_data_list': ''
+ 'add_data_list': '',
+ 'custom_python_path': ''
}
updated = False
for k, v in default_updates.items():
@@ -142,9 +143,10 @@ def extract_imports_via_ast(script_path, python_exe):
env = os.environ.copy()
env.pop("PYTHONHOME", None)
env.pop("PYTHONPATH", None)
- flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0)
- proc = subprocess.run([python_exe, "-c", code_snippet, script_path],
- capture_output=True, text=True, env=env, creationflags=flags)
+ kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "text": True, "env": env}
+ if os.name == 'nt': kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
+
+ proc = subprocess.run([python_exe, "-c", code_snippet, script_path], **kwargs)
m = re.search(r'__QPYPACK_RES__:(.*)', proc.stdout)
if m:
return set(json.loads(m.group(1).strip()))
@@ -262,6 +264,20 @@ def find_system_python():
for base in (user_profile, "C:\\", "D:\\"):
exe = os.path.join(base, c_dir, "python.exe")
if os.path.exists(exe): candidates.append(exe)
+ else:
+ unix_bases = [
+ "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin",
+ os.path.expanduser("~/.pyenv/shims"), os.path.expanduser("~/.local/bin")
+ ]
+ for base in unix_bases:
+ if os.path.exists(base):
+ try:
+ for f in os.listdir(base):
+ if f.startswith("python3") or f == "python":
+ exe = os.path.join(base, f)
+ if os.path.isfile(exe) and os.access(exe, os.X_OK):
+ candidates.append(exe)
+ except: pass
for cand in candidates:
cand = os.path.normpath(cand)
@@ -277,11 +293,9 @@ def find_system_python():
clean_env.pop("PYTHONPATH", None)
try:
- flags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
- proc = subprocess.run(
- [cand, "-V"], capture_output=True, env=clean_env,
- creationflags=flags, timeout=3
- )
+ kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "env": clean_env, "timeout": 3}
+ if os.name == 'nt': kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
+ proc = subprocess.run([cand, "-V"], **kwargs)
if proc.returncode == 0:
return cand
except:
@@ -290,6 +304,14 @@ def find_system_python():
return "python"
def get_python_executable():
+ try:
+ config = load_config()
+ custom_path = config['Settings'].get('custom_python_path', '').strip()
+ if custom_path and os.path.exists(custom_path) and os.path.isfile(custom_path):
+ return custom_path
+ except Exception:
+ pass
+
if getattr(sys, 'frozen', False) or '__compiled__' in globals():
return find_system_python()
@@ -712,6 +734,113 @@ class DropArea(QFrame):
self.sub_label.setText("智能解析工程依赖、静态附件及隐藏模块树")
+class PythonScannerThread(QThread):
+ scan_done = Signal(dict)
+
+ def run(self):
+ candidates = set()
+
+ for name in ('python', 'python3', 'pythonw'):
+ p = shutil.which(name)
+ if p: candidates.add(os.path.normpath(p))
+
+ if os.name == 'nt':
+ try:
+ for hive in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
+ try:
+ with winreg.OpenKey(hive, r"SOFTWARE\Python\PythonCore") as core_key:
+ for i in range(winreg.QueryInfoKey(core_key)[0]):
+ sub = winreg.EnumKey(core_key, i)
+ try:
+ with winreg.OpenKey(core_key, rf"{sub}\InstallPath") as pkey:
+ path = winreg.QueryValueEx(pkey, "")[0]
+ exe = os.path.join(path, "python.exe")
+ if os.path.exists(exe): candidates.add(os.path.normpath(exe))
+ except: pass
+ except: pass
+ except: pass
+
+ search_paths = [
+ os.environ.get("LOCALAPPDATA", "") + r"\Programs\Python",
+ os.environ.get("PROGRAMFILES", "") + r"\Python",
+ os.environ.get("PROGRAMFILES(X86)", "") + r"\Python",
+ r"C:\Python", r"C:\Program Files\Python", r"C:\Program Files (x86)\Python"
+ ]
+ for base in search_paths:
+ if base and os.path.exists(base):
+ try:
+ for d in os.listdir(base):
+ if d.lower().startswith("python"):
+ exe = os.path.join(base, d, "python.exe")
+ if os.path.exists(exe): candidates.add(os.path.normpath(exe))
+ except: pass
+
+ user_profile = os.environ.get("USERPROFILE", "")
+ if user_profile:
+ for c_dir in ("miniconda3", "anaconda3", "Miniconda3", "Anaconda3", ".conda"):
+ for base in (user_profile, "C:\\", "D:\\"):
+ base_dir = os.path.join(base, c_dir)
+ if os.path.exists(base_dir):
+ exe = os.path.join(base_dir, "python.exe")
+ if os.path.exists(exe): candidates.add(os.path.normpath(exe))
+ envs_dir = os.path.join(base_dir, "envs")
+ if os.path.exists(envs_dir):
+ try:
+ for env in os.listdir(envs_dir):
+ exe = os.path.join(envs_dir, env, "python.exe")
+ if os.path.exists(exe): candidates.add(os.path.normpath(exe))
+ except: pass
+ else:
+ unix_bases = [
+ "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin",
+ os.path.expanduser("~/.pyenv/shims"), os.path.expanduser("~/.local/bin")
+ ]
+ for base in unix_bases:
+ if os.path.exists(base):
+ try:
+ for f in os.listdir(base):
+ if f.startswith("python3") or f == "python":
+ exe = os.path.join(base, f)
+ if os.path.isfile(exe) and os.access(exe, os.X_OK):
+ candidates.add(os.path.normpath(exe))
+ except: pass
+
+ user_profile = os.environ.get("HOME", "")
+ if user_profile:
+ for c_dir in ("miniconda3", "anaconda3", "mambaforge"):
+ base_dir = os.path.join(user_profile, c_dir)
+ if os.path.exists(base_dir):
+ exe = os.path.join(base_dir, "bin", "python")
+ if os.path.exists(exe): candidates.add(os.path.normpath(exe))
+ envs_dir = os.path.join(base_dir, "envs")
+ if os.path.exists(envs_dir):
+ try:
+ for env in os.listdir(envs_dir):
+ exe = os.path.join(envs_dir, env, "bin", "python")
+ if os.path.exists(exe): candidates.add(os.path.normpath(exe))
+ except: pass
+
+ valid_pythons = {}
+
+ for cand in candidates:
+ if os.name == 'nt' and "WindowsApps" in cand:
+ try:
+ if os.path.getsize(cand) == 0: continue
+ except: continue
+ try:
+ clean_env = os.environ.copy()
+ clean_env.pop("PYTHONHOME", None)
+ clean_env.pop("PYTHONPATH", None)
+ kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "text": True, "env": clean_env, "timeout": 2}
+ if os.name == 'nt': kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
+ proc = subprocess.run([cand, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"], **kwargs)
+ if proc.returncode == 0:
+ ver = proc.stdout.strip()
+ valid_pythons[cand] = ver
+ except: pass
+ self.scan_done.emit(valid_pythons)
+
+
class SettingsPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
@@ -719,54 +848,87 @@ class SettingsPanel(QWidget):
self.upx_check = None
self.upx_path_container = None
- self.app_scroll_area = None
- self.form_dest = None
- self.form_perf = None
+ self.out_dir_container = None
self.setStyleSheet("""
- SettingsPanel { background-color: white; }
+ SettingsPanel { background-color: #f3f4f6; }
QLabel { color: #3c4043; font-size: 13px; font-weight: bold; background: transparent; }
- QComboBox, QLineEdit, QSpinBox { color: #3c4043; font-size: 13px; padding: 6px 10px; border: 1px solid #dadce0; border-radius: 6px; background: #fff; min-height: 24px; }
- QComboBox:hover, QLineEdit:hover, QSpinBox:hover { border-color: #bdc1c6; }
- QComboBox:focus, QLineEdit:focus, QSpinBox:focus { border-color: #1A73E8; }
- QGroupBox { border: 1px solid #e8eaed; border-radius: 8px; margin-top: 15px; padding-top: 15px; background-color: #f8f9fa; }
- QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left: 15px; top: 0px; color: #1A73E8; font-weight: bold; font-size: 13px; padding: 0 5px; background: transparent; }
- QCheckBox { font-size: 13px; color: #3c4043; spacing: 8px; background: transparent; }
- QCheckBox::indicator { width: 16px; height: 16px; border: 1px solid #bdc1c6; border-radius: 4px; background: white; }
- QCheckBox::indicator:checked { background: #1A73E8; border-color: #1A73E8; image: url("data:image/svg+xml;utf8,"); }
- QPushButton.ToolBtn { padding: 5px 12px; border: 1px solid #dadce0; border-radius: 6px; background: #f1f3f4; font-size: 12px; font-weight: bold; color: #5f6368;}
+ QComboBox, QLineEdit, QSpinBox {
+ color: #3c4043; font-size: 13px; padding: 8px 12px;
+ border: 1px solid #dadce0; border-radius: 6px; background: #f8f9fa; min-height: 20px;
+ }
+ QComboBox:hover, QLineEdit:hover, QSpinBox:hover { border-color: #bdc1c6; background: #ffffff; }
+ QComboBox:focus, QLineEdit:focus, QSpinBox:focus { border-color: #1A73E8; background: #ffffff; }
+ QCheckBox { font-size: 13px; color: #3c4043; spacing: 10px; background: transparent; }
+ QCheckBox::indicator { width: 18px; height: 18px; border: 1px solid #bdc1c6; border-radius: 4px; background: white; }
+ QCheckBox::indicator:hover { border-color: #1A73E8; }
+ QCheckBox::indicator:checked { background: #1A73E8; border-color: #1A73E8; image: url("data:image/svg+xml;utf8,"); }
+
+ /* Modern Card Style */
+ QFrame#SettingCard { background-color: #ffffff; border: 1px solid #e8eaed; border-radius: 12px; }
+ QFrame#SettingCard:hover { border: 1px solid #d2e3fc; }
+
+ QPushButton.ToolBtn { background: #f1f3f4; border: none; border-radius: 6px; padding: 8px 16px; color: #3c4043; font-weight: bold; }
QPushButton.ToolBtn:hover { background: #e8eaed; color: #202124; }
- QTabWidget::pane { border: 1px solid #e8eaed; border-radius: 8px; background: white; top: -1px; }
- QTabBar::tab { background: #f1f3f4; border: 1px solid #e8eaed; padding: 10px 20px; margin-right: 4px; border-top-left-radius: 8px; border-top-right-radius: 8px; color: #5f6368; font-weight: bold; font-size: 13px; }
- QTabBar::tab:selected { background: white; border-bottom-color: white; color: #1A73E8; }
- QTabBar::tab:hover:!selected { background: #e8eaed; }
+ QPushButton.ToolBtn:pressed { background: #dadce0; }
+
+ /* Main Tabs */
+ QTabWidget#MainTabWidget::pane { border: none; background: transparent; top: -1px; }
+ QTabBar#MainTabBar::tab { background: transparent; border: none; padding: 12px 24px; color: #5f6368; font-weight: bold; font-size: 14px; border-bottom: 3px solid transparent; margin-right: 4px; }
+ QTabBar#MainTabBar::tab:selected { color: #1A73E8; border-bottom: 3px solid #1A73E8; }
+ QTabBar#MainTabBar::tab:hover:!selected { color: #202124; background: rgba(0,0,0,0.03); border-radius: 6px; }
+
+ /* Sub Tabs (Pill Style) */
+ QTabWidget#SubTabWidget::pane { border: none; background: transparent; }
+ QTabBar#SubTabBar { alignment: left; }
+ QTabBar#SubTabBar::tab { background: transparent; border: 1px solid transparent; padding: 6px 18px; color: #5f6368; font-weight: bold; font-size: 13px; margin: 0 8px 12px 0; border-radius: 15px; }
+ QTabBar#SubTabBar::tab:selected { background: #e8f0fe; color: #1A73E8; border: 1px solid #d2e3fc; }
+ QTabBar#SubTabBar::tab:hover:!selected { background: #e8eaed; color: #202124; }
+
QListWidget { border: 1px solid #dadce0; border-radius: 6px; background: #fff; outline: none; font-size: 12px;}
- QListWidget::item { padding: 6px 10px; border-bottom: 1px solid #f1f3f4; color: #3c4043;}
+ QListWidget::item { padding: 8px 12px; border-bottom: 1px solid #f1f3f4; color: #3c4043;}
QListWidget::item:selected { background: #e8f0fe; color: #1a73e8; font-weight:bold; border-radius: 4px;}
""")
self.init_ui()
self.load_from_config()
+ self.scanner_thread = PythonScannerThread()
+ self.scanner_thread.scan_done.connect(self.populate_python_combo)
+ self.scanner_thread.start()
+
+ def _create_card(self, title_text=""):
+ card = QFrame()
+ card.setObjectName("SettingCard")
+ lay = QVBoxLayout(card)
+ lay.setContentsMargins(20, 20, 20, 20)
+ lay.setSpacing(12)
+ if title_text:
+ lbl = QLabel(title_text)
+ lbl.setStyleSheet("font-size: 15px; font-weight: 800; color: #1a73e8; margin-bottom: 6px;")
+ lay.addWidget(lbl)
+ return card, lay
+
def init_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(15)
- layout.setContentsMargins(20, 20, 20, 20)
+ layout.setContentsMargins(20, 10, 20, 20)
self.tabs = QTabWidget()
- self.tab_basic = QWidget()
- self.tab_env = QWidget()
- self.tab_meta = QWidget()
- self.tab_app = QWidget()
+ self.tabs.setObjectName("MainTabWidget")
+ self.tabs.tabBar().setObjectName("MainTabBar")
- self.tabs.addTab(self.tab_basic, get_svg_icon('package', "#1A73E8"), " 产物元数据")
- self.tabs.addTab(self.tab_env, get_svg_icon('python', "#1A73E8"), " 编译控制")
- self.tabs.addTab(self.tab_meta, get_svg_icon('info', "#1A73E8"), " 资源与沙盒")
- self.tabs.addTab(self.tab_app, get_svg_icon('settings', "#1A73E8"), " 软件设置")
+ self.tab_build = QWidget()
+ self.tab_pref = QWidget()
+ self.tab_about = QWidget()
+
+ self.tabs.addTab(self.tab_build, " 🎛️ 构建配置")
+ self.tabs.addTab(self.tab_pref, " ⚙️ 偏好设置")
+ self.tabs.addTab(self.tab_about, " ℹ️ 关于软件")
+
+ self.build_build_tab()
+ self.build_pref_tab()
+ self.build_about_tab()
- self.build_basic_tab()
- self.build_env_tab()
- self.build_meta_tab()
- self.build_app_tab()
layout.addWidget(self.tabs)
btn_lay = QHBoxLayout()
@@ -799,6 +961,28 @@ class SettingsPanel(QWidget):
layout.addLayout(btn_lay)
+ def populate_python_combo(self, py_dict):
+ current_text = self.python_path_combo.currentText()
+ self.python_path_combo.clear()
+ for path, ver in py_dict.items():
+ self.python_path_combo.addItem(f"{path} (Python {ver})", path)
+
+ if current_text:
+ clean_text = current_text
+ if " (Python " in clean_text:
+ clean_text = clean_text.split(" (Python ")[0].strip()
+ clean_text = os.path.normpath(clean_text).lower()
+
+ found = False
+ for idx in range(self.python_path_combo.count()):
+ item_path = self.python_path_combo.itemData(idx)
+ if item_path and os.path.normpath(item_path).lower() == clean_text:
+ self.python_path_combo.setCurrentIndex(idx)
+ found = True
+ break
+ if not found:
+ self.python_path_combo.setCurrentText(current_text)
+
def load_from_config(self):
config = load_config()
if 'Settings' in config:
@@ -813,6 +997,7 @@ class SettingsPanel(QWidget):
self.reqs_check.setChecked(s.getboolean('use_reqs', True))
self.pipreqs_check.setChecked(s.getboolean('use_pipreqs', True))
self.reqs_file_edit.setText(s.get('use_reqs_file', ''))
+ self.python_path_combo.setCurrentText(s.get('custom_python_path', ''))
if self.upx_check:
self.upx_check.setChecked(s.getboolean('upx', False))
@@ -856,6 +1041,12 @@ class SettingsPanel(QWidget):
s['use_reqs'] = str(self.reqs_check.isChecked())
s['use_pipreqs'] = str(self.pipreqs_check.isChecked())
s['use_reqs_file'] = self.reqs_file_edit.text().strip()
+
+ raw_py = self.python_path_combo.currentText().strip()
+ if " (Python " in raw_py:
+ raw_py = raw_py.split(" (Python ")[0].strip()
+ s['custom_python_path'] = raw_py
+
if self.upx_check:
s['upx'] = str(self.upx_check.isChecked())
s['upx_path'] = self.upx_path_edit.text().strip()
@@ -875,342 +1066,350 @@ class SettingsPanel(QWidget):
save_config(config)
- def build_basic_tab(self):
- scroll = QScrollArea()
- scroll.setWidgetResizable(True)
- scroll.setFrameShape(QFrame.Shape.NoFrame)
- scroll.setStyleSheet("background: transparent;")
+ def build_build_tab(self):
+ main_lay = QVBoxLayout(self.tab_build)
+ main_lay.setContentsMargins(0, 10, 0, 0)
- content = QWidget()
- content.setStyleSheet("background: white;")
- lay = QVBoxLayout(content)
- lay.setContentsMargins(15, 15, 15, 15)
+ self.sub_tabs = QTabWidget()
+ self.sub_tabs.setObjectName("SubTabWidget")
+ self.sub_tabs.tabBar().setObjectName("SubTabBar")
- grp_core = QGroupBox("核心参数")
- form_core = QFormLayout(grp_core)
- form_core.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
- form_core.setContentsMargins(15, 20, 15, 15)
- form_core.setVerticalSpacing(12)
+ tab_basic = QWidget()
+ tab_assets = QWidget()
+ tab_advanced = QWidget()
- self.name_edit = QLineEdit()
- self.name_edit.setPlaceholderText("留空则与入口脚本同名")
- form_core.addRow("输出名称:", self.name_edit)
+ self.sub_tabs.addTab(tab_basic, "基础设置")
+ self.sub_tabs.addTab(tab_assets, "资源与依赖")
+ self.sub_tabs.addTab(tab_advanced, "高级控制")
- self.icon_edit = QLineEdit()
- self.icon_preview = QLabel()
- self.icon_preview.setFixedSize(28, 28)
- self.icon_preview.setScaledContents(True)
- self.icon_edit.textChanged.connect(self.update_icon_preview)
- btn_icon = QPushButton("浏览...")
- btn_icon.setProperty("class", "ToolBtn")
- btn_icon.clicked.connect(self.select_icon)
- h_icon = QHBoxLayout()
- h_icon.addWidget(self.icon_edit)
- h_icon.addWidget(self.icon_preview)
- h_icon.addWidget(btn_icon)
- form_core.addRow("应用图标:", h_icon)
- lay.addWidget(grp_core)
-
- grp_mode = QGroupBox("打包模式")
- grid_mode = QGridLayout(grp_mode)
- grid_mode.setContentsMargins(15, 20, 15, 15)
- grid_mode.setVerticalSpacing(15)
+ lay_basic = QVBoxLayout(tab_basic)
+ lay_basic.setContentsMargins(0, 5, 0, 0)
- self.onefile_check = QCheckBox("单文件模式 (OneFile)")
- self.noconsole_check = QCheckBox("隐藏控制台 (GUI模式)")
-
- grid_mode.addWidget(self.onefile_check, 0, 0)
- grid_mode.addWidget(self.noconsole_check, 0, 1)
- lay.addWidget(grp_mode)
-
- self.grp_meta = QGroupBox("版本元数据 (Metadata)")
- meta_form = QFormLayout(self.grp_meta)
- meta_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
- meta_form.setContentsMargins(15, 20, 15, 15)
- meta_form.setVerticalSpacing(12)
-
- self.ver_ver = QLineEdit("1.0.0")
- self.ver_comp = QLineEdit("My Studio")
- self.ver_desc = QLineEdit("Python Executable")
- meta_form.addRow("版本序列:", self.ver_ver)
- meta_form.addRow("发行公司:", self.ver_comp)
- meta_form.addRow("文件描述:", self.ver_desc)
- lay.addWidget(self.grp_meta)
-
- lay.addStretch()
- scroll.setWidget(content)
- main_lay = QVBoxLayout(self.tab_basic)
- main_lay.setContentsMargins(0,0,0,0)
- main_lay.addWidget(scroll)
-
- def build_env_tab(self):
- scroll = QScrollArea()
- scroll.setWidgetResizable(True)
- scroll.setFrameShape(QFrame.Shape.NoFrame)
- scroll.setStyleSheet("background: transparent;")
-
- content = QWidget()
- content.setStyleSheet("background: white;")
- lay = QVBoxLayout(content)
- lay.setContentsMargins(15, 15, 15, 15)
-
- grp_engine = QGroupBox("编译器引擎")
- form_engine = QFormLayout(grp_engine)
- form_engine.setContentsMargins(15, 20, 15, 15)
- form_engine.setVerticalSpacing(12)
+ card1, c_lay1 = self._create_card("核心参数设定")
+ form1 = QFormLayout()
+ form1.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
+ form1.setSpacing(15)
self.engine_combo = QComboBox()
self.engine_combo.addItems(["PyInstaller", "Nuitka"])
self.engine_combo.currentIndexChanged.connect(self.on_engine_changed)
- form_engine.addRow("构建引擎选择:", self.engine_combo)
- lay.addWidget(grp_engine)
+
+ self.python_path_combo = QComboBox()
+ self.python_path_combo.setEditable(True)
+ self.python_path_combo.setPlaceholderText("留空自动探测当前系统默认 Python 解释器")
+ btn_python_path = QPushButton("浏览")
+ btn_python_path.setProperty("class", "ToolBtn")
+ btn_python_path.clicked.connect(self.select_python_path)
+ py_cont = QWidget(); h_py = QHBoxLayout(py_cont); h_py.setContentsMargins(0,0,0,0)
+ h_py.addWidget(self.python_path_combo); h_py.addWidget(btn_python_path)
- grp_modules = QGroupBox("精细化模块过滤")
- form_mod = QFormLayout(grp_modules)
- form_mod.setContentsMargins(15, 20, 15, 15)
- form_mod.setVerticalSpacing(12)
+ self.name_edit = QLineEdit()
+ self.name_edit.setPlaceholderText("默认自动使用入口代码文件同名")
- self.exclude_edit = QLineEdit()
- self.exclude_edit.setPlaceholderText("例如: tkinter, matplotlib (排除打包,减少体积)")
- form_mod.addRow("屏蔽指定模块:", self.exclude_edit)
- lay.addWidget(grp_modules)
+ self.icon_edit = QLineEdit()
+ self.icon_preview = QLabel()
+ self.icon_preview.setFixedSize(24, 24)
+ self.icon_preview.setScaledContents(True)
+ self.icon_edit.textChanged.connect(self.update_icon_preview)
+ btn_icon = QPushButton("浏览")
+ btn_icon.setProperty("class", "ToolBtn")
+ btn_icon.clicked.connect(self.select_icon)
+ icon_cont = QWidget(); h_icon = QHBoxLayout(icon_cont); h_icon.setContentsMargins(0,0,0,0)
+ h_icon.addWidget(self.icon_edit); h_icon.addWidget(self.icon_preview); h_icon.addWidget(btn_icon)
- grp_dep = QGroupBox("包与依赖分析机制")
- dep_lay = QVBoxLayout(grp_dep)
- dep_lay.setContentsMargins(15, 20, 15, 15)
- dep_lay.setSpacing(15)
+ form1.addRow("构建引擎:", self.engine_combo)
+ form1.addRow("Python 环境:", py_cont)
+ form1.addRow("输出名称:", self.name_edit)
+ form1.addRow("应用图标:", icon_cont)
+ c_lay1.addLayout(form1)
+
+ card2, c_lay2 = self._create_card("应用程序行为")
+ h_mode = QHBoxLayout()
+ self.onefile_check = QCheckBox("打包为单文件可执行程序 (OneFile)")
+ self.noconsole_check = QCheckBox("隐藏命令行黑框 (GUI窗口应用)")
+ h_mode.addWidget(self.onefile_check)
+ h_mode.addWidget(self.noconsole_check)
+ h_mode.addStretch()
+ c_lay2.addLayout(h_mode)
+
+ lay_basic.addWidget(card1)
+ lay_basic.addWidget(card2)
+ lay_basic.addStretch()
- grid_dep = QGridLayout()
- self.reqs_check = QCheckBox("启用依赖清单部署")
- self.pipreqs_check = QCheckBox("启用 pipreqs 深度代码分析")
+ scroll_assets = QScrollArea()
+ scroll_assets.setWidgetResizable(True)
+ scroll_assets.setFrameShape(QFrame.Shape.NoFrame)
+ scroll_assets.setStyleSheet("background: transparent;")
+ cont_assets = QWidget()
+ lay_assets = QVBoxLayout(cont_assets)
+ lay_assets.setContentsMargins(0, 5, 0, 0)
- grid_dep.addWidget(self.reqs_check, 0, 0)
- grid_dep.addWidget(self.pipreqs_check, 0, 1)
- dep_lay.addLayout(grid_dep)
+ card3, c_lay3 = self._create_card("环境隔离与包管理")
+ form2 = QFormLayout()
+ form2.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
+ form2.setSpacing(15)
+
+ self.pip_source_edit = QLineEdit()
+
+ self.reqs_file_edit = QLineEdit(); self.reqs_file_edit.setPlaceholderText("如不选,默认读取目录下的 requirements.txt")
+ btn_reqs = QPushButton("选择"); btn_reqs.setProperty("class", "ToolBtn"); btn_reqs.clicked.connect(self.select_reqs_file)
+ reqs_cont = QWidget(); h_reqs = QHBoxLayout(reqs_cont); h_reqs.setContentsMargins(0,0,0,0)
+ h_reqs.addWidget(self.reqs_file_edit); h_reqs.addWidget(btn_reqs)
+
+ self.hidden_edit = QLineEdit(); self.hidden_edit.setPlaceholderText("程序报错找不到模块时在此填入,逗号分隔 (如: pandas, PyQt5)")
+ btn_scan = QPushButton("AST扫描"); btn_scan.setProperty("class", "ToolBtn"); btn_scan.clicked.connect(self.auto_scan_hidden)
+ hid_cont = QWidget(); h_hid = QHBoxLayout(hid_cont); h_hid.setContentsMargins(0,0,0,0)
+ h_hid.addWidget(self.hidden_edit); h_hid.addWidget(btn_scan)
- form_dep = QFormLayout()
- form_dep.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
+ self.exclude_edit = QLineEdit(); self.exclude_edit.setPlaceholderText("排除无需打包的庞大模块,以减小体积 (如: tkinter, matplotlib)")
- self.reqs_file_edit = QLineEdit()
- self.reqs_file_edit.setPlaceholderText("留空则默认读取脚本目录下的 requirements.txt")
- btn_reqs_file = QPushButton("选择...")
- btn_reqs_file.setProperty("class", "ToolBtn")
- btn_reqs_file.clicked.connect(self.select_reqs_file)
- h_reqs = QHBoxLayout()
- h_reqs.addWidget(self.reqs_file_edit)
- h_reqs.addWidget(btn_reqs_file)
- form_dep.addRow("依赖清单文件:", h_reqs)
+ form2.addRow("PIP 镜像源:", self.pip_source_edit)
+ form2.addRow("依赖清单文件:", reqs_cont)
+ form2.addRow("强制包含 (防丢失):", hid_cont)
+ form2.addRow("强制排除 (减体积):", self.exclude_edit)
+ c_lay3.addLayout(form2)
- self.hidden_edit = QLineEdit()
- self.hidden_edit.setPlaceholderText("例如: pandas, PyQt5 (逗号分隔)")
- btn_scan = QPushButton("AST 扫描")
- btn_scan.setProperty("class", "ToolBtn")
- btn_scan.clicked.connect(self.auto_scan_hidden)
- h_hid = QHBoxLayout()
- h_hid.addWidget(self.hidden_edit)
- h_hid.addWidget(btn_scan)
- form_dep.addRow("隐式依赖:", h_hid)
- dep_lay.addLayout(form_dep)
- lay.addWidget(grp_dep)
+ c_lay3.addSpacing(5)
+ h_dep = QHBoxLayout()
+ self.venv_check = QCheckBox("启用虚拟沙盒 (推荐)")
+ self.reqs_check = QCheckBox("按清单同步")
+ self.pipreqs_check = QCheckBox("智能深度分析依赖")
+ h_dep.addWidget(self.venv_check)
+ h_dep.addWidget(self.reqs_check)
+ h_dep.addWidget(self.pipreqs_check)
+ h_dep.addStretch()
+ c_lay3.addLayout(h_dep)
- lay.addStretch()
+ card4, c_lay4 = self._create_card("附加静态资源 (双击列表条目可修改释放路径)")
+ self.add_data_list = QListWidget()
+ self.add_data_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
+ self.add_data_list.setFixedHeight(120)
+ self.add_data_list.itemDoubleClicked.connect(self.edit_resource)
+ c_lay4.addWidget(self.add_data_list)
- scroll.setWidget(content)
- main_lay = QVBoxLayout(self.tab_env)
- main_lay.setContentsMargins(0,0,0,0)
- main_lay.addWidget(scroll)
+ btn_res_lay = QHBoxLayout()
+ self.btn_add_file = QPushButton("添加文件"); self.btn_add_file.setProperty("class", "ToolBtn"); self.btn_add_file.clicked.connect(self.add_resource_files)
+ self.btn_add_dir = QPushButton("添加目录"); self.btn_add_dir.setProperty("class", "ToolBtn"); self.btn_add_dir.clicked.connect(self.add_resource_dir)
+ self.btn_del_res = QPushButton("删除选中"); self.btn_del_res.setProperty("class", "ToolBtn"); self.btn_del_res.clicked.connect(self.del_resource)
+ self.btn_clear_res = QPushButton("清空"); self.btn_clear_res.setProperty("class", "ToolBtn"); self.btn_clear_res.clicked.connect(self.clear_resource)
+ btn_res_lay.addWidget(self.btn_add_file); btn_res_lay.addWidget(self.btn_add_dir); btn_res_lay.addWidget(self.btn_del_res); btn_res_lay.addWidget(self.btn_clear_res); btn_res_lay.addStretch()
+ c_lay4.addLayout(btn_res_lay)
+
+ lay_assets.addWidget(card3)
+ lay_assets.addWidget(card4)
+ lay_assets.addStretch()
+
+ scroll_assets.setWidget(cont_assets)
+ lay_tabs_assets = QVBoxLayout(tab_assets)
+ lay_tabs_assets.setContentsMargins(0,0,0,0)
+ lay_tabs_assets.addWidget(scroll_assets)
- def build_meta_tab(self):
+ scroll_adv = QScrollArea()
+ scroll_adv.setWidgetResizable(True)
+ scroll_adv.setFrameShape(QFrame.Shape.NoFrame)
+ scroll_adv.setStyleSheet("background: transparent;")
+ cont_adv = QWidget()
+ lay_adv = QVBoxLayout(cont_adv)
+ lay_adv.setContentsMargins(0, 5, 0, 0)
+
+ card5, c_lay5 = self._create_card("可执行文件属性 (Windows 元数据)")
+ form3 = QFormLayout()
+ form3.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
+ form3.setSpacing(15)
+ self.ver_ver = QLineEdit("1.0.0")
+ self.ver_comp = QLineEdit("My Studio")
+ self.ver_desc = QLineEdit("Python Executable")
+ form3.addRow("版本序列:", self.ver_ver)
+ form3.addRow("发行公司:", self.ver_comp)
+ form3.addRow("文件描述:", self.ver_desc)
+ c_lay5.addLayout(form3)
+
+ card6, c_lay6 = self._create_card("深度编译优化")
+ form4 = QFormLayout()
+ form4.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
+ form4.setSpacing(15)
+ self.cores_spin = QSpinBox(); self.cores_spin.setRange(1, os.cpu_count() or 4); self.cores_spin.setValue(os.cpu_count() or 2)
+ form4.addRow("并发编译线程:", self.cores_spin)
+
+ self.upx_check = QCheckBox("挂载 UPX 极限压缩优化")
+ self.upx_check.toggled.connect(self.on_upx_toggled)
+ self.upx_path_edit = QLineEdit(); self.upx_path_edit.setPlaceholderText("输入UPX环境目录,留空则自动搜索")
+ btn_upx = QPushButton("选择"); btn_upx.setProperty("class", "ToolBtn"); btn_upx.clicked.connect(self.select_upx_path)
+ self.upx_path_container = QWidget(); h_upx = QHBoxLayout(self.upx_path_container); h_upx.setContentsMargins(0,0,0,0)
+ h_upx.addWidget(self.upx_path_edit); h_upx.addWidget(btn_upx)
+ self.upx_path_container.setVisible(False)
+
+ h_upx_row = QHBoxLayout()
+ h_upx_row.addWidget(self.upx_check)
+ h_upx_row.addWidget(self.upx_path_container)
+ form4.addRow("UPX 压缩引擎:", h_upx_row)
+ c_lay6.addLayout(form4)
+
+ lay_adv.addWidget(card5)
+ lay_adv.addWidget(card6)
+ lay_adv.addStretch()
+
+ scroll_adv.setWidget(cont_adv)
+ lay_tabs_adv = QVBoxLayout(tab_advanced)
+ lay_tabs_adv.setContentsMargins(0,0,0,0)
+ lay_tabs_adv.addWidget(scroll_adv)
+
+ main_lay.addWidget(self.sub_tabs)
+ self.on_engine_changed()
+
+ def build_pref_tab(self):
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent;")
content = QWidget()
- content.setStyleSheet("background: white;")
+ content.setStyleSheet("background: transparent;")
lay = QVBoxLayout(content)
- lay.setContentsMargins(15, 15, 15, 15)
+ lay.setContentsMargins(0, 5, 0, 15)
+ lay.setSpacing(15)
- grp_res = QGroupBox("附加资源归档 (支持双击修改目标路径)")
- res_lay = QVBoxLayout(grp_res)
- res_lay.setContentsMargins(15, 20, 15, 15)
- res_lay.setSpacing(10)
+ card1, lay1 = self._create_card("输出与归档策略")
+ self.out_mode_combo = QComboBox()
+ self.out_mode_combo.addItems(["默认策略 (直接保存在源脚本同级目录)", "独立归档 (保存到自定义的专属目录)"])
+ self.out_mode_combo.currentIndexChanged.connect(self.on_out_mode_changed)
- self.add_data_list = QListWidget()
- self.add_data_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
- self.add_data_list.setMinimumHeight(100)
- self.add_data_list.itemDoubleClicked.connect(self.edit_resource)
- res_lay.addWidget(self.add_data_list)
+ self.out_dir_edit = QLineEdit(); self.out_dir_edit.setPlaceholderText("选择具体的输出归档路径...")
+ btn_out_dir = QPushButton("浏览..."); btn_out_dir.setProperty("class", "ToolBtn"); btn_out_dir.clicked.connect(self.select_out_dir)
+ self.out_dir_container = QWidget(); h_out_dir = QHBoxLayout(self.out_dir_container); h_out_dir.setContentsMargins(0, 0, 0, 0)
+ h_out_dir.addWidget(self.out_dir_edit); h_out_dir.addWidget(btn_out_dir)
+ self.out_dir_container.setVisible(False)
- btn_res_lay = QHBoxLayout()
- btn_res_lay.setSpacing(8)
-
- self.btn_add_file = QPushButton("添加文件...")
- self.btn_add_file.setProperty("class", "ToolBtn")
- self.btn_add_file.clicked.connect(self.add_resource_files)
-
- self.btn_add_dir = QPushButton("添加文件夹...")
- self.btn_add_dir.setProperty("class", "ToolBtn")
- self.btn_add_dir.clicked.connect(self.add_resource_dir)
-
- self.btn_del_res = QPushButton("删除选中")
- self.btn_del_res.setProperty("class", "ToolBtn")
- self.btn_del_res.clicked.connect(self.del_resource)
-
- self.btn_clear_res = QPushButton("清空")
- self.btn_clear_res.setProperty("class", "ToolBtn")
- self.btn_clear_res.clicked.connect(self.clear_resource)
-
- btn_res_lay.addWidget(self.btn_add_file)
- btn_res_lay.addWidget(self.btn_add_dir)
- btn_res_lay.addWidget(self.btn_del_res)
- btn_res_lay.addWidget(self.btn_clear_res)
- btn_res_lay.addStretch()
-
- res_lay.addLayout(btn_res_lay)
- lay.addWidget(grp_res)
+ form1 = QFormLayout()
+ form1.setVerticalSpacing(15)
+ form1.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
+ form1.addRow("保存位置规则:", self.out_mode_combo)
+ form1.addRow("自定义归档区:", self.out_dir_container)
+ lay1.addLayout(form1)
+ lay.addWidget(card1)
- grp_env = QGroupBox("执行环境隔离")
- form_env = QFormLayout(grp_env)
- form_env.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
- form_env.setContentsMargins(15, 20, 15, 15)
- form_env.setVerticalSpacing(12)
+ card2, lay2 = self._create_card("自动化选项与运行反馈")
+ lay2.setSpacing(16)
- self.pip_source_edit = QLineEdit()
- form_env.addRow("PIP 镜像:", self.pip_source_edit)
-
- self.venv_check = QCheckBox("启用独立沙盒打包环境 (Venv)")
- form_env.addRow("", self.venv_check)
- lay.addWidget(grp_env)
+ self.concise_log_check = QCheckBox("开启静默排版 (自动过滤底层引擎产生的干扰性警告)")
+ self.auto_save_log_check = QCheckBox("异常捕获机制 (编译结束后,自动在输出目录落盘完整排错日志)")
+ self.auto_icon_check = QCheckBox("视觉资源嗅探 (自动识别并绑定同目录下的同名 ICO/SVG 图标)")
+ self.clean_all_check = QCheckBox("存储无痕释放 (任务完成后,自动销毁所有临时沙盒与构建缓存)")
+ self.sound_notify_check = QCheckBox("声音感官反馈 (任务结束或发生异常中断时,播放系统提示音)")
+ for chk in (self.concise_log_check, self.auto_save_log_check, self.auto_icon_check, self.clean_all_check, self.sound_notify_check):
+ lay2.addWidget(chk)
+
+ lay.addWidget(card2)
lay.addStretch()
scroll.setWidget(content)
- main_lay = QVBoxLayout(self.tab_meta)
+ main_lay = QVBoxLayout(self.tab_pref)
main_lay.setContentsMargins(0,0,0,0)
main_lay.addWidget(scroll)
- def build_app_tab(self):
- self.app_scroll_area = QScrollArea()
- self.app_scroll_area.setWidgetResizable(True)
- self.app_scroll_area.setFrameShape(QFrame.Shape.NoFrame)
- self.app_scroll_area.setStyleSheet("background: transparent;")
+ def build_about_tab(self):
+ main_lay = QVBoxLayout(self.tab_about)
+ main_lay.setContentsMargins(40, 20, 40, 20)
+ main_lay.setSpacing(15)
- content = QWidget()
- content.setStyleSheet("background: white;")
- lay = QVBoxLayout(content)
- lay.setContentsMargins(15, 15, 15, 15)
+ main_lay.addStretch(1)
- self.form_perf = QFormLayout()
- grp_perf = QGroupBox("编译资源限制与压缩优化")
- grp_perf.setLayout(self.form_perf)
- self.form_perf.setContentsMargins(15, 20, 15, 15)
- self.form_perf.setVerticalSpacing(12)
+ logo_lbl = QLabel()
+ icon_path = get_resource_path("icon.ico")
+ if os.path.exists(icon_path):
+ high_res_pixmap = QIcon(icon_path).pixmap(256, 256)
+ if not high_res_pixmap.isNull():
+ logo_pixmap = high_res_pixmap.scaled(110, 110, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
+ logo_lbl.setPixmap(logo_pixmap)
+ else:
+ logo_lbl.setPixmap(get_svg_pixmap('package', color="#1A73E8", size=110))
+ else:
+ logo_lbl.setPixmap(get_svg_pixmap('package', color="#1A73E8", size=110))
+
+ logo_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ main_lay.addWidget(logo_lbl)
- self.cores_spin = QSpinBox()
- self.cores_spin.setRange(1, os.cpu_count() or 4)
- self.cores_spin.setValue(os.cpu_count() or 2)
- self.form_perf.addRow("并发编译线程数:", self.cores_spin)
+ title_lbl = QLabel(__app_name__)
+ title_lbl.setStyleSheet("font-size: 36px; font-weight: 900; color: #202124; letter-spacing: -1px; margin-top: 10px;")
+ title_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ main_lay.addWidget(title_lbl)
- self.upx_check = QCheckBox("启用 PyInstaller UPX 压缩优化")
- self.upx_check.toggled.connect(self.on_upx_toggled)
- self.form_perf.addRow("", self.upx_check)
+ ver_lbl = QLabel(f"Version {__version__} · GPL-3.0")
+ ver_lbl.setStyleSheet("font-size: 14px; font-weight: bold; color: #1A73E8; margin-bottom: 5px;")
+ ver_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ main_lay.addWidget(ver_lbl)
- self.upx_path_edit = QLineEdit()
- self.upx_path_edit.setPlaceholderText("留空则在全局环境或当前目录下自动定位 UPX...")
- self.btn_upx_path = QPushButton("选择...")
- self.btn_upx_path.setProperty("class", "ToolBtn")
- self.btn_upx_path.clicked.connect(self.select_upx_path)
+ desc_lbl = QLabel(__description__)
+ desc_lbl.setStyleSheet("font-size: 14px; color: #5f6368;")
+ desc_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ main_lay.addWidget(desc_lbl)
- self.upx_path_container = QWidget()
- self.upx_path_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
- self.upx_path_container.setMinimumHeight(32)
- h_upx = QHBoxLayout(self.upx_path_container)
- h_upx.setContentsMargins(0, 0, 0, 0)
- h_upx.setSpacing(6)
- h_upx.addWidget(self.upx_path_edit)
- h_upx.addWidget(self.btn_upx_path)
- self.form_perf.addRow("自定义 UPX 目录:", self.upx_path_container)
- lay.addWidget(grp_perf)
+ main_lay.addSpacing(25)
- self.form_dest = QFormLayout()
- grp_dest = QGroupBox("构建产物保存路径")
- grp_dest.setLayout(self.form_dest)
- self.form_dest.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
- self.form_dest.setContentsMargins(15, 20, 15, 15)
- self.form_dest.setVerticalSpacing(12)
+ btn_lay = QHBoxLayout()
+ btn_lay.setSpacing(15)
+ btn_lay.setAlignment(Qt.AlignmentFlag.AlignCenter)
- self.out_mode_combo = QComboBox()
- self.out_mode_combo.addItems(["默认路径 (保存在源脚本所在同目录)", "保存到自定义输出目录"])
- self.out_mode_combo.currentIndexChanged.connect(self.on_out_mode_changed)
- self.form_dest.addRow("保存位置模式:", self.out_mode_combo)
+ def create_link_btn(text, url):
+ btn = QPushButton(text)
+ btn.setCursor(Qt.CursorShape.PointingHandCursor)
+ btn.setStyleSheet("""
+ QPushButton {
+ background-color: #f1f3f4;
+ color: #3c4043;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 20px;
+ font-size: 13px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background-color: #e8eaed;
+ color: #1A73E8;
+ }
+ QPushButton:pressed {
+ background-color: #dadce0;
+ }
+ """)
+ btn.clicked.connect(lambda: __import__('webbrowser').open(url))
+ return btn
+
+ btn_github = create_link_btn("🌍 GitHub 仓库", "https://github.com/qwejay/QPyPack")
+ btn_issue = create_link_btn("🐞 反馈与建议", "https://github.com/qwejay/QPyPack/issues")
+ btn_pypi = create_link_btn("📦 PyPI 主页", "https://pypi.org/project/qpypack/")
- self.out_dir_edit = QLineEdit()
- self.out_dir_edit.setPlaceholderText("选择自定义输出保存路径...")
- self.out_dir_edit.setMinimumWidth(150)
- self.btn_out_dir = QPushButton("浏览...")
- self.btn_out_dir.setFixedWidth(75)
- self.btn_out_dir.setProperty("class", "ToolBtn")
- self.btn_out_dir.clicked.connect(self.select_out_dir)
+ btn_lay.addWidget(btn_github)
+ btn_lay.addWidget(btn_issue)
+ btn_lay.addWidget(btn_pypi)
- self.out_dir_container = QWidget()
- self.out_dir_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
- self.out_dir_container.setMinimumHeight(32)
- h_out_dir = QHBoxLayout(self.out_dir_container)
- h_out_dir.setContentsMargins(0, 0, 0, 0)
- h_out_dir.setSpacing(6)
- h_out_dir.addWidget(self.out_dir_edit)
- h_out_dir.addWidget(self.btn_out_dir)
- self.form_dest.addRow("自定义目录路径:", self.out_dir_container)
- lay.addWidget(grp_dest)
-
- grp_app = QGroupBox("偏好习惯与通知反馈")
- grid_app = QGridLayout(grp_app)
- grid_app.setContentsMargins(15, 20, 15, 15)
- grid_app.setVerticalSpacing(15)
+ main_lay.addLayout(btn_lay)
- self.concise_log_check = QCheckBox("启用简洁日志模式 (自动屏蔽警告与垃圾调试信息)")
- self.clean_all_check = QCheckBox("编译结束后自动清理临时缓存文件目录")
- self.auto_icon_check = QCheckBox("自动匹配入口脚本同目录下的同名ico图标")
- self.sound_notify_check = QCheckBox("编译构建任务结束时发出声音反馈提示")
- self.auto_save_log_check = QCheckBox("打包结束后自动在目标输出目录导出运行日志 (.log)")
+ main_lay.addStretch(1)
- grid_app.addWidget(self.concise_log_check, 0, 0)
- grid_app.addWidget(self.clean_all_check, 1, 0)
- grid_app.addWidget(self.auto_icon_check, 2, 0)
- grid_app.addWidget(self.sound_notify_check, 3, 0)
- grid_app.addWidget(self.auto_save_log_check, 4, 0)
- lay.addWidget(grp_app)
-
- lay.addStretch()
-
- self.app_scroll_area.setWidget(content)
- main_lay = QVBoxLayout(self.tab_app)
- main_lay.setContentsMargins(0,0,0,0)
- main_lay.addWidget(self.app_scroll_area)
+ rights_lbl = QLabel(f"Copyright © {__company__}. All rights reserved.")
+ rights_lbl.setStyleSheet("font-size: 12px; color: #bdc1c6; font-weight: bold;")
+ rights_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ main_lay.addWidget(rights_lbl)
def on_engine_changed(self):
- if getattr(self, 'upx_check', None) is not None and getattr(self, 'form_perf', None) is not None:
+ if getattr(self, 'upx_check', None) is not None and getattr(self, 'upx_path_container', None) is not None:
engine = self.engine_combo.currentText()
is_pyi = (engine == "PyInstaller")
self.upx_check.setVisible(is_pyi)
- self.form_perf.setRowVisible(self.upx_path_container, is_pyi and self.upx_check.isChecked())
+ self.upx_path_container.setVisible(is_pyi and self.upx_check.isChecked())
def on_upx_toggled(self, checked):
- if getattr(self, 'form_perf', None) is not None:
- self.form_perf.setRowVisible(self.upx_path_container, checked)
- if checked and getattr(self, 'app_scroll_area', None) is not None:
- QTimer.singleShot(50, lambda: self.app_scroll_area.ensureWidgetVisible(self.upx_path_container))
+ if getattr(self, 'upx_path_container', None) is not None:
+ self.upx_path_container.setVisible(checked)
def on_out_mode_changed(self, index):
show_custom = (index == 1)
- if getattr(self, 'form_dest', None) is not None:
- self.form_dest.setRowVisible(self.out_dir_container, show_custom)
- if show_custom and getattr(self, 'app_scroll_area', None) is not None:
- QTimer.singleShot(50, lambda: self.app_scroll_area.ensureWidgetVisible(self.out_dir_container))
+ if getattr(self, 'out_dir_container', None) is not None:
+ self.out_dir_container.setVisible(show_custom)
def select_out_dir(self):
- d = QFileDialog.getExistingDirectory(self, "选择输出保存目录")
+ d = QFileDialog.getExistingDirectory(self, "选择输出归档目录")
if d:
self.out_dir_edit.setText(Path(d).resolve().as_posix())
@@ -1224,6 +1423,12 @@ class SettingsPanel(QWidget):
if f:
self.reqs_file_edit.setText(Path(f).resolve().as_posix())
+ def select_python_path(self):
+ exe_filter = "Executable (*.exe);;All Files (*)" if os.name == 'nt' else "All Files (*)"
+ f, _ = QFileDialog.getOpenFileName(self, "选择 Python 解释器", "", exe_filter)
+ if f:
+ self.python_path_combo.setCurrentText(Path(f).resolve().as_posix())
+
def update_icon_preview(self, path):
if path and Path(path).exists():
pixmap = QPixmap(path)
@@ -1358,7 +1563,7 @@ class PackingThread(QThread):
self._is_cancelled = True
if self.process:
try:
- if os.name == "nt": subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.process.pid)], capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
+ if os.name == "nt": subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.process.pid)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
else: self.process.kill()
except: pass
@@ -1386,7 +1591,7 @@ class PackingThread(QThread):
try:
if os.name == "nt":
subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.process.pid)],
- capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
else:
self.process.kill()
except:
@@ -1542,7 +1747,7 @@ class PackingThread(QThread):
script_posix = build_script_path.as_posix()
system_python_exe = get_python_executable()
- self.progress.emit(f"[Env] 宿主解释器路径: {system_python_exe}")
+ self.progress.emit(f"[Env] 构建调度宿主解释器路径: {system_python_exe}")
script_imports = set()
try:
@@ -1835,8 +2040,8 @@ class MainWindow(QMainWindow):
def init_style(self):
self.setWindowTitle(f"{__app_name__} {__version__} - {__author__}")
- self.setMinimumSize(610, 560)
- self.resize(610, 560)
+ self.setMinimumSize(680, 620)
+ self.resize(680, 620)
icon_path = get_resource_path("icon.ico")
if os.path.exists(icon_path):
@@ -2094,13 +2299,17 @@ class MainWindow(QMainWindow):
engine = sp.engine_combo.currentText()
version_file = None
- if engine == "PyInstaller" and sp.ver_ver.text().strip():
+ if engine == "PyInstaller" and os.name == "nt" and sp.ver_ver.text().strip():
try:
v_str = sp.ver_ver.text().strip()
v_nums = re.findall(r'\d+', v_str)
v_tuple = ",".join((v_nums + ['0', '0', '0', '0'])[:4])
- content = f'''VSVersionInfo(ffi=FixedFileInfo(filevers=({v_tuple}),prodvers=({v_tuple}),mask=0x3f,flags=0x0,OS=0x40004,fileType=0x1,subtype=0x0,date=(0,0)),kids=[StringFileInfo([StringTable('040904B0',[StringStruct('CompanyName','{sp.ver_comp.text()}'),StringStruct('FileDescription','{sp.ver_desc.text()}'),StringStruct('FileVersion','{v_str}'),StringStruct('ProductVersion','{v_str}'),StringStruct('OriginalFilename','{app_name}.exe')])])])'''
+ comp_escaped = sp.ver_comp.text().replace("'", "\\'")
+ desc_escaped = sp.ver_desc.text().replace("'", "\\'")
+ v_str_escaped = v_str.replace("'", "\\'")
+
+ content = f'''VSVersionInfo(ffi=FixedFileInfo(filevers=({v_tuple}),prodvers=({v_tuple}),mask=0x3f,flags=0x0,OS=0x40004,fileType=0x1,subtype=0x0,date=(0,0)),kids=[StringFileInfo([StringTable('040904B0',[StringStruct('CompanyName','{comp_escaped}'),StringStruct('FileDescription','{desc_escaped}'),StringStruct('FileVersion','{v_str_escaped}'),StringStruct('ProductVersion','{v_str_escaped}'),StringStruct('OriginalFilename','{app_name}.exe')])])])'''
version_file = Path(tempfile.gettempdir()) / f"qpypack_{app_name}_version.txt"
version_file.write_text(content, encoding='utf-8')
except: pass