v2.2.0: Rename repository to QPyPack and release updates

Update
This commit is contained in:
QwejayHuang committed 2026-08-07 01:39:22 +08:00
1 parent 93d1cbf3fb
commit 74d8f4405b
6 files changed
+206 -53

No files matched your search

+24 -13
View File
@@ -1,16 +1,17 @@
# PyPack
# QPyPack
PyPack 是一个为 Python 开发者设计的图形化打包配置工具。它将复杂的命令行打包流程(支持 **PyInstaller**、**Nuitka** 和 **cx_Freeze**)转化为直观、简易的 GUI 界面操作,无需记忆繁琐的终端命令,即可快速生成可执行文件(.exe)。
QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它将复杂的命令行打包流程(支持 **PyInstaller**、**Nuitka** 和 **cx_Freeze**)转化为直观、简易的 GUI 界面操作,无需记忆繁琐的终端命令,即可快速生成可执行文件(.exe)。
<p align="center">
<img width="904" height="887" alt="image" src="https://github.com/user-attachments/assets/abe4c3ea-ccfe-41ab-9dd5-c99a882d69d4" />
<img width="904" height="887" alt="image" src="https://github.com/user-attachments/assets/7a9ceed9-e070-4036-aabc-3b37bc13e036" />
<img width="904" height="887" alt="image" src="https://github.com/user-attachments/assets/0f7831a2-68fc-460c-a5a2-75b39e36a7af" />
<img width="904" height="887" alt="image" src="https://github.com/user-attachments/assets/ce50d3a0-95df-4773-bd56-763701bfd16a" />
</p>
---
## 核心设计:让打包变简单
为了解决传统命令行打包参数多、依赖易冲突、配置繁琐的问题,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)
+32
View File
@@ -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"]
+2
View File
@@ -0,0 +1,2 @@
PyQt5==5.15.11
pyqt5_sip==12.18.0
View File
Whitespace-only changes.
View File
File renamed without changes.

Before

Width:  |  Height:  |  Size: 551 KiB

After

Width:  |  Height:  |  Size: 551 KiB

+148 -40
View File
@@ -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()