diff --git a/README.md b/README.md index fd22dd2..d1802a5 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ -# PyPack +# QPyPack -PyPack 是一个为 Python 开发者设计的图形化打包配置工具。它将复杂的命令行打包流程(支持 **PyInstaller**、**Nuitka** 和 **cx_Freeze**)转化为直观、简易的 GUI 界面操作,无需记忆繁琐的终端命令,即可快速生成可执行文件(.exe)。 +QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它将复杂的命令行打包流程(支持 **PyInstaller**、**Nuitka** 和 **cx_Freeze**)转化为直观、简易的 GUI 界面操作,无需记忆繁琐的终端命令,即可快速生成可执行文件(.exe)。

-image -image +image +image +

--- ## 核心设计:让打包变简单 -为了解决传统命令行打包参数多、依赖易冲突、配置繁琐的问题,PyPack 2.0 围绕“易用性”进行了重新设计: +为了解决传统命令行打包参数多、依赖易冲突、配置繁琐的问题,QPyPack 2.0 围绕“易用性”进行了重新设计: ### 1. 极简的交互流程 * **拖拽即装载**:无需手动输入路径,直接将您的 `.py` 源代码文件拖入软件窗口,即可自动识别并准备构建。 @@ -31,6 +32,14 @@ PyPack 是一个为 Python 开发者设计的图形化打包配置工具。它 --- ### Changelog +2.2 +* 更名为 QPyPack,规范目录结构for PyPI,优化内部沙盒与临时文件命名规范。 +* 超时守护 (Watchdog):引入看门狗机制,为关键打包步骤设置超时限制,防止因网络问题导致构建无限期挂起。 +* 依赖推导优化:pipreqs 原生支持国内镜像站重定向加速,并增加 15 秒超时自愈,失败时自动降级为本地纯静态 AST 解析。 +* 智能命名输出:自动解析脚本版本,预设输出文件名为 {Name}_{Version} 格式(例如:QPyPack_2.2.0.exe)。 +* 编译兼容扩展:Nuitka 编译模式新增对 PyQt6、PySide、NumPy、Matplotlib 等常用插件的自适应识别。 +* 细节打磨:改进日志高频刷新滚动体验,优化版本信息元数据抓取退级逻辑。 + 2.1 * Nuitka单文件打包解压路径修改为%LOCALAPPDATA% * 增加建构失败动画 @@ -51,11 +60,13 @@ PyPack 是一个为 Python 开发者设计的图形化打包配置工具。它 3. **开始构建**:返回主界面点击“开始构建”,并在下方展开的控制台中实时查看打包进度。 --- - -## 系统要求 - -* **支持系统**:Windows (推荐), macOS, Linux -* **Python 环境**:Python 3.8+ -* **基础依赖**:只需在您的本地系统提前安装好 `PyQt5` 即可运行: - ```bash - pip install PyQt5 +### 启动应用 +Python >=3.8 环境中执行以下命令启动程序。 +```bash +pip install qpypack +``` +```bash +qpypack +``` +### 或者使用打包好的二进制文件直接运行 +[GitHub Releases 下载链接](https://github.com/Qwejay/PyPack/releases) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8b948d7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["setuptools>=61.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "qpypack" +version = "2.2.0" +description = "A modern graphical packaging config tool for PyInstaller, Nuitka, and cx_Freeze." +readme = "README.md" +authors = [ + { name = "QwejayHuang" } +] +license = { text = "MIT" } +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", +] + +dependencies = [ + "PyQt5>=5.15.0", +] + +[project.urls] +Homepage = "https://github.com/qwejay/QPyPack" + +[project.scripts] +qpypack = "qpypack.main:main" + +[tool.setuptools.package-data] +qpypack = ["icon.ico"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..06e3fa7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyQt5==5.15.11 +pyqt5_sip==12.18.0 diff --git a/src/qpypack/__init__.py b/src/qpypack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/icon.ico b/src/qpypack/icon.ico similarity index 100% rename from icon.ico rename to src/qpypack/icon.ico diff --git a/main.py b/src/qpypack/main.py similarity index 90% rename from main.py rename to src/qpypack/main.py index 26b4323..da3e728 100644 --- a/main.py +++ b/src/qpypack/main.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -PyPack 2.1 - 自动化构建工具 -支持引擎: PyInstaller / Nuitka / cx_Freeze -""" + +__app_name__ = "QPyPack" +__version__ = "2.2.0" +__author__ = "QwejayHuang" +__company__ = "Qwesoft" +__description__ = "自动化 Python 脚本打包构建工具" import sys import os @@ -16,6 +18,7 @@ import stat import ast import locale import math +import threading from pathlib import Path os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false" @@ -32,7 +35,6 @@ from PyQt5.QtCore import Qt, QThread, pyqtSignal, QPropertyAnimation, QEasingCur from PyQt5.QtGui import QFont, QDragEnterEvent, QDropEvent, QTextCursor, QIcon, QPixmap, QPainter, QColor, QPen from PyQt5.QtSvg import QSvgRenderer -# ======================== Material Design 内置 SVG 引擎 ======================== MATERIAL_ICONS = { 'settings': 'M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z', 'refresh': 'M17.65,6.35C16.2,4.9,14.21,4,12,4c-4.42,0-7.99,3.58-7.99,8s3.57,8,7.99,8c3.73,0,6.84-2.55,7.73-6h-2.08 c-0.82,2.33-3.04,4-5.65,4c-3.31,0-6-2.69-6-6s2.69-6,6-6c1.66,0,3.14,0.69,4.22,1.78L13,11h7V4L17.65,6.35z', @@ -65,7 +67,6 @@ def get_svg_pixmap(name, color="#5F6368", size=64): return get_svg_icon(name, color, size).pixmap(size, size) -# ======================== 工具与配置 ======================== def get_stdlib_names(): libs = {'os', 'sys', 're', 'math', 'time', 'datetime', 'json', 'urllib', 'sqlite3', 'csv', 'subprocess', 'shutil', 'threading', 'multprocessing', 'queue', 'socket', @@ -106,7 +107,6 @@ def robust_rmtree(path: Path, retries=15, delay=0.8): return False -# ======================== 界面动效组件 ======================== class AnimatedButton(QPushButton): def __init__(self, text, parent=None): super().__init__(text, parent) @@ -148,13 +148,12 @@ class AnimatedButton(QPushButton): super().leaveEvent(event) class TargetIconWidget(QWidget): - """自定义图标渲染组件,接管无损抗锯齿与复合重绘效果""" def __init__(self, parent=None): super().__init__(parent) self.setFixedSize(200, 200) self.pixmap = None self.base_pixmap = None - self.file_pixmap = None # 用于专门记住当前加载文件的专属图标 + self.file_pixmap = None self.current_size = 88 self.is_building = False @@ -180,7 +179,6 @@ class TargetIconWidget(QWidget): self.burst_anim.setEasingCurve(QEasingCurve.OutQuad) self.burst_anim.valueChanged.connect(self._animate_burst) - # 抖动动画配置(用于构建失败) self.shake_offset = 0 self.shake_anim = QVariantAnimation(self) self.shake_anim.setDuration(500) @@ -213,7 +211,6 @@ class TargetIconWidget(QWidget): self.update() def start_building(self): - # 恢复之前保存的文件专属图标,覆盖掉可能残留的“成功”或“失败”图标 if getattr(self, 'file_pixmap', None) and not self.file_pixmap.isNull(): self.pixmap = self.file_pixmap self.current_size = 88 @@ -242,7 +239,7 @@ class TargetIconWidget(QWidget): def start_failure(self): self.stop_building() self.success_effect.setBlurRadius(40) - self.success_effect.setColor(QColor(217, 48, 37, 180)) # 红色警告发光 + self.success_effect.setColor(QColor(217, 48, 37, 180)) self.shake_anim.start() def reset(self): @@ -252,7 +249,7 @@ class TargetIconWidget(QWidget): self.burst_value = 0.0 self.shake_offset = 0 self.success_effect.setColor(QColor(0, 0, 0, 0)) - self.file_pixmap = None # 清空文件专属图标 + self.file_pixmap = None self.pixmap = self.base_pixmap self.current_size = 88 self.update() @@ -416,7 +413,6 @@ class DropArea(QFrame): if not pixmap: pixmap = get_svg_pixmap('package', color="#1A73E8", size=88) - # 使用 set_file_pixmap,避免覆盖 base_pixmap 导致复位失败 self.icon_widget.set_file_pixmap(pixmap, 88) self.label.setText(f"已加载:{filename}") @@ -766,7 +762,7 @@ class SettingsPanel(QWidget): if not script_path: return QMessageBox.warning(self, "提示", "请先在主界面加载脚本!") try: raw = Path(script_path).read_bytes() - if b"__CLOUDSYNC_ENC__" in raw[:1024]: return QMessageBox.critical(self, "错误", "脚本被云盘加密,请解密!") + if b"__CLOUDSYNC_ENC__" in raw[:1024]: return QMessageBox.warning(self, "错误", "脚本被云盘加密,请解密!") try: code = raw.decode('utf-8-sig') except: code = raw.decode('gbk', errors='ignore') @@ -784,7 +780,7 @@ class SettingsPanel(QWidget): hidden = [m for m in hidden if m not in STD_LIBS] self.hidden_edit.setText(','.join(hidden)) QMessageBox.information(self, "扫描完成", f"捕获到 {len(hidden)} 项依赖。") - except Exception as e: QMessageBox.critical(self, "错误", f"扫描失败: {e}") + except Exception as e: QMessageBox.warning(self, "错误", f"扫描失败: {e}") def add_resource(self): choice = QMessageBox.question(self, "添加资源", "Yes=文件, No=文件夹", QMessageBox.Yes | QMessageBox.No) @@ -832,13 +828,32 @@ class PackingThread(QThread): else: self.process.kill() except: pass - def run_cmd(self, cmd, cwd=None): + def run_cmd(self, cmd, cwd=None, timeout=None): if self._is_cancelled: return False + + timer = None + is_timeout = [False] + try: kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.STDOUT, "cwd": cwd, "text": True, "errors": "replace"} if os.name == 'nt': kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW self.process = subprocess.Popen(cmd, **kwargs) + + if timeout: + def kill_proc(): + is_timeout[0] = True + try: + if os.name == "nt": + subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.process.pid)], + capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW) + else: + self.process.kill() + except: + pass + timer = threading.Timer(timeout, kill_proc) + timer.start() + for line in self.process.stdout: if self._is_cancelled: self.process.terminate() @@ -846,10 +861,18 @@ class PackingThread(QThread): self.progress.emit(line.strip()) self.process.wait() + + if is_timeout[0]: + self.progress.emit(f"[提示] 该步骤执行超时 ({timeout}秒),已自动跳过并进入下一步...") + return False + return self.process.returncode == 0 except Exception as e: self.progress.emit(f"[系统异常] 进程执行失败: {e}") return False + finally: + if timer: + timer.cancel() def sanitize_script(self, orig_path: Path): try: @@ -858,8 +881,6 @@ class PackingThread(QThread): return None, False, "目标脚本被云盘锁定加密!" except Exception: pass - # 移除了所有重命名及生成 pypack_temp_ 前缀文件的死代码 - # 直接透传原始文件可防止 Nuitka 生成带有临时前缀的 DLL 触发 Windows 智能应用控制的启发式拦截 return orig_path, False, "" def run(self): @@ -884,7 +905,7 @@ class PackingThread(QThread): if self.params['use_venv']: self.progress.emit("[环境] 初始化虚拟隔离沙盒...") - self.venv_dir = Path(tempfile.mkdtemp(prefix="pypack_env_")).resolve() + self.venv_dir = Path(tempfile.mkdtemp(prefix="qpypack_env_")).resolve() if not self.run_cmd([get_python_executable(), "-m", "venv", self.venv_dir.as_posix()]): return self.finished.emit(False, "虚拟环境初始化失败。") python_exe = (self.venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")).as_posix() @@ -899,6 +920,8 @@ class PackingThread(QThread): self.run_cmd([python_exe, "-m", "pip", "install", "-q"] + core_pkgs + pip_args) req_success = False + + # 1. 优先尝试部署 requirements.txt if self.params.get('use_reqs'): req_file = script_dir / "requirements.txt" if req_file.exists(): @@ -914,16 +937,43 @@ class PackingThread(QThread): req_success = True temp_req.unlink(missing_ok=True) except Exception as e: self.progress.emit(f"[警告] {e}") + + # 2. requirements.txt 未能成功解决依赖,则启用 pipreqs(带镜像站原生参数及 15 秒超时自愈) if self.params.get('use_pipreqs') and not req_success: self.progress.emit("[依赖] 执行 pipreqs 深度推导...") self.run_cmd([python_exe, "-m", "pip", "install", "pipreqs", "-q"] + pip_args) temp_pipreqs = script_dir / "pypack_pipreqs_out.txt" - self.run_cmd([python_exe, "-m", "pipreqs.pipreqs", script_dir.as_posix(), "--encoding", "utf-8", "--force", "--savepath", temp_pipreqs.as_posix()]) - if temp_pipreqs.exists(): - if self.run_cmd([python_exe, "-m", "pip", "install", "-q", "-r", temp_pipreqs.as_posix()] + pip_args): req_success = True - temp_pipreqs.unlink(missing_ok=True) + + pypi_server = None + if pip_idx: + pypi_server = re.sub(r'/simple/?$', '/pypi', pip_idx.strip(), flags=re.I).rstrip('/') + + pipreqs_cmd = [ + python_exe, "-m", "pipreqs.pipreqs", + script_dir.as_posix(), + "--encoding", "utf-8", + "--force", + "--savepath", temp_pipreqs.as_posix() + ] + + if pypi_server: + pipreqs_cmd.extend(["--pypi-server", pypi_server]) + self.progress.emit(f"[依赖] pipreqs 已重定向至本地镜像接口: {pypi_server}") + + if self.run_cmd(pipreqs_cmd, timeout=15): + if temp_pipreqs.exists(): + if self.run_cmd([python_exe, "-m", "pip", "install", "-q", "-r", temp_pipreqs.as_posix()] + pip_args): + req_success = True + temp_pipreqs.unlink(missing_ok=True) + else: + self.progress.emit("[提示] pipreqs 深度解析超时或失败,转为本地 AST 解析...") + if temp_pipreqs.exists(): + try: temp_pipreqs.unlink() + except: pass + + # 3. 如果以上方案均未成功解决依赖,则采用本地纯静态 AST 降级解析兜底 if not req_success and (self.params.get('use_reqs') or self.params.get('use_pipreqs')): - self.progress.emit("[依赖] 执行 AST 语法树降级解析...") + self.progress.emit("[依赖] 正在启用 AST 本地语法树解析机制兜底...") try: code = build_script_path.read_text(encoding='utf-8', errors='ignore') third_party = set() @@ -949,18 +999,17 @@ class PackingThread(QThread): icon_path = Path(self.params['icon']).resolve().as_posix() if self.params.get('icon') else None if engine == "PyInstaller": - self.temp_workpath = Path(tempfile.mkdtemp(prefix="pypack_build_")).resolve() + self.temp_workpath = Path(tempfile.mkdtemp(prefix="qpypack_build_")).resolve() cmd = [python_exe, "-m", "PyInstaller", "--clean", "-y", f"--workpath={self.temp_workpath.as_posix()}", f"--name={app_name}"] + if self.params.get('onefile'): cmd.append("--onefile") - if os.name == "nt": - cmd.append(f"--windows-onefile-tempdir-spec=%LOCALAPPDATA%\\{app_name}_Env") else: - cmd.append("--standalone") + cmd.append("--onedir") - if self.params.get('noconsole'): cmd.append("--windows-console-mode=disable") + if self.params.get('noconsole'): + cmd.append("--noconsole") - if icon_path: cmd.extend(["--icon", icon_path]) sep = ';' if os.name == 'nt' else ':' @@ -998,7 +1047,6 @@ class PackingThread(QThread): cmd.append(f"--windows-icon-from-ico={icon_path}") elif sys.platform == "darwin" and icon_path.endswith(".icns"): cmd.append(f"--macos-app-icon={icon_path}") - # 使用精确的文件名作为目标部署位置,以避开 Nuitka '.' 校验致命错误 cmd.append(f"--include-data-files={icon_path}={Path(icon_path).name}") if os.name == "nt": @@ -1007,8 +1055,19 @@ class PackingThread(QThread): if self.params.get('ver_ver'): cmd.append(f"--file-version={self.params['ver_ver']}") try: - if 'PyQt5' in build_script_path.read_text(encoding='utf-8', errors='ignore'): + script_text = build_script_path.read_text(encoding='utf-8', errors='ignore') + if 'PyQt5' in script_text: cmd.append("--enable-plugin=pyqt5") + elif 'PyQt6' in script_text: + cmd.append("--enable-plugin=pyqt6") + elif 'PySide2' in script_text: + cmd.append("--enable-plugin=pyside2") + elif 'PySide6' in script_text: + cmd.append("--enable-plugin=pyside6") + elif 'numpy' in script_text: + cmd.append("--enable-plugin=numpy") + elif 'matplotlib' in script_text: + cmd.append("--enable-plugin=matplotlib") except: pass for imp in self.params.get('hidden_imports', '').split(','): @@ -1077,7 +1136,7 @@ class PackingThread(QThread): self.progress.emit("[清理] 抹除环境与沙盒...") for p in [self.venv_dir, self.temp_workpath, self.temp_out_dir]: if p and p.exists(): robust_rmtree(p) - + cwd = Path.cwd().resolve() app_name = self.params.get('app_name', 'app') robust_rmtree(cwd / "dist") @@ -1098,7 +1157,8 @@ class MainWindow(QMainWindow): self.init_ui() def init_style(self): - self.setWindowTitle("PyPack 2.1") + self.setWindowTitle(f"{__app_name__} {__version__} - {__author__}") + self.setMinimumSize(580, 520) self.resize(600, 560) @@ -1280,8 +1340,49 @@ class MainWindow(QMainWindow): except: pass self.script_path = path - self.settings_panel.name_edit.setText(Path(path).stem) + # 智能元数据预设逻辑 + app_name = Path(path).stem + version = "" + author = "My Studio" + desc = "Python Executable" + + try: + content = Path(path).read_text(encoding='utf-8', errors='ignore') + + # 解析版本 + v_match = re.search(r'^(?:__version__|VERSION|version)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) + if v_match: + version = v_match.group(1) + self.settings_panel.ver_ver.setText(version) + + # 解析发行公司(无company则降级尝试绑定author) + c_match = re.search(r'^(?:__company__|COMPANY)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) + if c_match: + author = c_match.group(1) + else: + a_match = re.search(r'^(?:__author__|AUTHOR)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) + if a_match: author = a_match.group(1) + self.settings_panel.ver_comp.setText(author) + + # 解析名称 + n_match = re.search(r'^(?:__title__|__app_name__|APP_NAME)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) + if n_match: + app_name = n_match.group(1) + + # 解析描述 + d_match = re.search(r'^(?:__description__|DESCRIPTION)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I) + if d_match: + desc = d_match.group(1) + self.settings_panel.ver_desc.setText(desc) + + except: pass + + # 拼装生成带版本后缀的默认文件名 + default_output_name = f"{app_name}_{version}" if version else app_name + self.settings_panel.name_edit.setText(default_output_name) + + # 图标自动配对 script_dir = Path(path).parent auto_icon = None @@ -1322,8 +1423,12 @@ class MainWindow(QMainWindow): version_file = None if engine == "PyInstaller" and sp.ver_ver.text().strip(): try: - content = f'''VSVersionInfo(ffi=FixedFileInfo(filevers=(1,0,0,0),prodvers=(1,0,0,0),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','{sp.ver_ver.text()}'),StringStruct('ProductVersion','{sp.ver_ver.text()}'),StringStruct('OriginalFilename','{app_name}.exe')])])])''' - version_file = Path(tempfile.gettempdir()) / f"pypack_{app_name}_version.txt" + 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')])])])''' + version_file = Path(tempfile.gettempdir()) / f"qpypack_{app_name}_version.txt" version_file.write_text(content, encoding='utf-8') except: pass @@ -1398,10 +1503,10 @@ class MainWindow(QMainWindow): def append_log(self, msg): self.log.append(msg) - self.log.moveCursor(QTextCursor.End) + self.log.ensureCursorVisible() -if __name__ == "__main__": +def main(): if hasattr(Qt, 'HighDpiScaleFactorRoundingPolicy'): QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) @@ -1413,3 +1518,6 @@ if __name__ == "__main__": win = MainWindow() win.show() sys.exit(app.exec_()) + +if __name__ == "__main__": + main() \ No newline at end of file