From 621d752a8d5f09f9b4ce5be0114d16cafe75c137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com> Date: Tue, 7 Apr 2026 18:54:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BE=AE=E4=BF=A1=E5=A4=9A?= =?UTF-8?q?=E5=BC=80=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CleanDesktopOrganizer.spec | 4 +- Readme.md | 5 + main.py | 2 +- ui/__pycache__/dock.cpython-313.pyc | Bin 95258 -> 95621 bytes ui/dock.py | 6 + ui/updater.py | 101 ++++++++---- ui/wechat_multi.py | 234 ++++++++++++++++++++++++++++ 7 files changed, 317 insertions(+), 35 deletions(-) create mode 100644 Readme.md create mode 100644 ui/wechat_multi.py diff --git a/CleanDesktopOrganizer.spec b/CleanDesktopOrganizer.spec index c5f88d0..62aab32 100644 --- a/CleanDesktopOrganizer.spec +++ b/CleanDesktopOrganizer.spec @@ -34,7 +34,7 @@ exe = EXE( debug=False, bootloader_ignore_signals=False, strip=False, - upx=True, + upx=False, upx_exclude=[], runtime_tmpdir=None, console=False, @@ -43,5 +43,5 @@ exe = EXE( target_arch=None, codesign_identity=None, entitlements_file=None, - icon=['logo.png'], + icon=['logo.ico'], ) diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..1e6bbe8 --- /dev/null +++ b/Readme.md @@ -0,0 +1,5 @@ +# 编译命令 +python -m PyInstaller CleanDesktopOrganizer.spec + +# 清除缓存重新打包命令 +python -m PyInstaller CleanDesktopOrganizer.spec --clean diff --git a/main.py b/main.py index 2865d52..d8145ce 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ from ui.ball import FloatBall, BALL_SIZE import ui.theme as theme from db import database -__VERSION__ = "0.0.2" +__VERSION__ = "0.0.3" # ===================== 打包兼容核心函数 ===================== def get_resource_path(relative_path): diff --git a/ui/__pycache__/dock.cpython-313.pyc b/ui/__pycache__/dock.cpython-313.pyc index 833556de3d86a3d8884fc358d0484893972ec055..372abebef15f9a315065b8253de4ad859d2c6b38 100644 GIT binary patch delta 4910 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 K>Aa?%lKulJ+N;C> diff --git a/ui/dock.py b/ui/dock.py index af3126d..e866c57 100644 --- a/ui/dock.py +++ b/ui/dock.py @@ -507,6 +507,7 @@ class PanelWindow(QWidget): 每项: (tooltip, qtawesome_icon, callback) 排除重启/退出。""" return [ + ("微信多开", "fa5b.weixin", self._open_wechat_multi), ("管理员运行 CMD", "fa5s.terminal", self._open_admin_cmd), ("管理员运行 PowerShell", "fa5b.windows", self._open_admin_powershell), ("打开默认浏览器", "fa5s.globe", self._open_default_browser), @@ -913,6 +914,11 @@ class PanelWindow(QWidget): import webbrowser webbrowser.open("https://www.baidu.com") + def _open_wechat_multi(self): + from ui.wechat_multi import WechatMultiDialog + dlg = WechatMultiDialog(self) + dlg.exec() + def _toggle_theme(self): theme.set_theme("light" if theme.name() == "dark" else "dark") self._apply_theme() diff --git a/ui/updater.py b/ui/updater.py index 4c4777a..cfda538 100644 --- a/ui/updater.py +++ b/ui/updater.py @@ -16,7 +16,6 @@ UPDATE_CHECK_URL = "https://api.yunzer.cn/api/softwareupgrade/check?code=niumaso def _current_version() -> str: - """从 main 模块取当前版本号,避免循环导入。""" try: import importlib m = importlib.import_module("__main__") @@ -35,13 +34,12 @@ def _is_newer(latest: str, current: str) -> bool: class _CheckWorker(QThread): - result = pyqtSignal(dict) # 成功:返回 data 字段 + result = pyqtSignal(dict) error = pyqtSignal(str) def run(self): try: import json - import urllib.request with urllib.request.urlopen(UPDATE_CHECK_URL, timeout=10) as resp: body = json.loads(resp.read().decode()) if body.get("code") == 200: @@ -53,59 +51,90 @@ class _CheckWorker(QThread): class _DownloadWorker(QThread): - progress = pyqtSignal(int) # 0-100 - finished = pyqtSignal(str) # 下载完成,返回临时文件路径 + progress = pyqtSignal(int) + finished = pyqtSignal(str) error = pyqtSignal(str) - def __init__(self, url: str): + def __init__(self, url: str, dest: str): super().__init__() - self._url = url + self._url = url + self._dest = dest def run(self): try: - tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".exe") - tmp.close() - dst = tmp.name - def _reporthook(count, block_size, total_size): if total_size > 0: pct = min(100, int(count * block_size * 100 / total_size)) self.progress.emit(pct) - urllib.request.urlretrieve(self._url, dst, _reporthook) + urllib.request.urlretrieve(self._url, self._dest, _reporthook) self.progress.emit(100) - self.finished.emit(dst) + self.finished.emit(self._dest) except Exception as e: self.error.emit(str(e)) def _replace_and_restart(new_exe: str): """ - 写一个批处理脚本:等待当前进程退出 → 覆盖 exe → 启动新版。 - 仅在打包为 exe 时执行真正替换;开发环境直接启动下载文件。 + onedir 模式:只替换 exe 本身,dll 等文件不变。 + 用 PowerShell 后台脚本等待原进程退出后替换并重启,完全无窗口。 """ - current_exe = sys.executable if getattr(sys, "frozen", False) else None - - if current_exe: - bat = tempfile.NamedTemporaryFile(delete=False, suffix=".bat", mode="w", encoding="gbk") - bat.write(f"""@echo off -ping 127.0.0.1 -n 3 >nul -move /y "{new_exe}" "{current_exe}" -start "" "{current_exe}" -del "%~f0" -""") - bat.close() - subprocess.Popen(["cmd", "/c", bat.name], creationflags=subprocess.CREATE_NO_WINDOW) - else: - # 开发环境:直接运行下载的 exe + if not getattr(sys, "frozen", False): subprocess.Popen([new_exe]) + QApplication.quit() + return + current_exe = sys.executable + pid = os.getpid() + + ps_script = f""" +$pid_target = {pid} +$src = '{new_exe.replace(chr(92), chr(92)*2)}' +$dst = '{current_exe.replace(chr(92), chr(92)*2)}' + +# 等待原进程退出,最多 30 秒 +$waited = 0 +while ((Get-Process -Id $pid_target -ErrorAction SilentlyContinue) -and $waited -lt 30) {{ + Start-Sleep -Milliseconds 500 + $waited += 0.5 +}} + +# 重试覆盖,最多 10 次 +$ok = $false +for ($i = 0; $i -lt 10; $i++) {{ + try {{ + Copy-Item -Path $src -Destination $dst -Force + $ok = $true + break + }} catch {{ + Start-Sleep -Milliseconds 800 + }} +}} + +if ($ok) {{ + Remove-Item $src -Force -ErrorAction SilentlyContinue + Start-Process $dst +}} +""" + + tmp = tempfile.NamedTemporaryFile( + delete=False, suffix=".ps1", mode="w", encoding="utf-8" + ) + tmp.write(ps_script) + tmp.close() + + subprocess.Popen( + [ + "powershell", "-WindowStyle", "Hidden", + "-NonInteractive", "-ExecutionPolicy", "Bypass", + "-File", tmp.name, + ], + creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW, + ) QApplication.quit() class Updater(QObject): - """对外接口:调用 check() 即可。""" - def __init__(self, parent=None): super().__init__(parent) self._parent_widget = parent @@ -141,6 +170,7 @@ class Updater(QObject): if ret != QMessageBox.StandardButton.Yes: return + self._url = url self._download(url) def _on_check_error(self, err: str): @@ -148,12 +178,19 @@ class Updater(QObject): QMessageBox.warning(self._parent_widget, "检查更新失败", f"无法连接更新服务器:\n{err}") def _download(self, url: str): + # 下载到原 exe 同目录,避免跨盘 move 失败 + if getattr(sys, "frozen", False): + dest_dir = os.path.dirname(sys.executable) + else: + dest_dir = tempfile.gettempdir() + dest = os.path.join(dest_dir, "_update_new.exe") + self._progress = QProgressDialog("正在下载更新...", "取消", 0, 100, self._parent_widget) self._progress.setWindowTitle("下载更新") self._progress.setMinimumDuration(0) self._progress.setValue(0) - self._dl_worker = _DownloadWorker(url) + self._dl_worker = _DownloadWorker(url, dest) self._dl_worker.progress.connect(self._progress.setValue) self._dl_worker.finished.connect(self._on_download_done) self._dl_worker.error.connect(self._on_download_error) diff --git a/ui/wechat_multi.py b/ui/wechat_multi.py new file mode 100644 index 0000000..8c462ce --- /dev/null +++ b/ui/wechat_multi.py @@ -0,0 +1,234 @@ +""" +微信多开对话框 +""" +import os +import subprocess +import tempfile + +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QSpinBox, QFileDialog, QTextEdit, QWidget +) +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QIcon +import ui.theme as theme +from db import database + +_DB_KEY_PATH = "wechat_multi_path" +_DB_KEY_COUNT = "wechat_multi_count" + +_DEFAULT_PATHS = [ + r"D:\Softwares\Tencent\Weixin\Weixin.exe", + r"C:\Program Files\Tencent\WeChat\WeChat.exe", + r"C:\Program Files (x86)\Tencent\WeChat\WeChat.exe", +] + +_USAGE = """使用说明: + +1. 填写微信 Weixin.exe 的完整路径。 + 点击右侧「浏览」按钮可以直接选择文件。 + +2. 设置多开数量(建议不超过 5 个, + 数量过多可能导致电脑卡顿)。 + +3. 点击「开始多开」,程序会依次启动 + 对应数量的微信进程。 + +4. 每个微信实例需要单独登录账号。 + +注意:微信官方不支持多开,使用本功能 +请自行承担相关风险。 +""" + + +def _detect_wechat() -> str: + """尝试自动检测微信路径。""" + saved = database.get_setting(_DB_KEY_PATH, "") + if saved and os.path.isfile(saved): + return saved + for p in _DEFAULT_PATHS: + if os.path.isfile(p): + return p + return "" + + +class WechatMultiDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("微信多开") + self.setMinimumWidth(460) + self.setWindowFlags( + Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint + ) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self._drag_pos = None + self._build() + self._apply_theme() + + def _build(self): + saved_path = _detect_wechat() + saved_count = int(database.get_setting(_DB_KEY_COUNT, "2")) + + root = QVBoxLayout(self) + root.setContentsMargins(0, 0, 0, 0) + + self._card = QWidget() + self._card.setObjectName("wechat_card") + card_layout = QVBoxLayout(self._card) + card_layout.setContentsMargins(20, 16, 20, 16) + card_layout.setSpacing(12) + + # 标题栏 + title_row = QHBoxLayout() + lbl_title = QLabel("微信多开") + lbl_title.setStyleSheet("font-size:14px; font-weight:bold;") + close_btn = QPushButton("✕") + close_btn.setFixedSize(24, 24) + close_btn.setStyleSheet("border:none; background:transparent; font-size:13px;") + close_btn.clicked.connect(self.reject) + title_row.addWidget(lbl_title) + title_row.addStretch() + title_row.addWidget(close_btn) + card_layout.addLayout(title_row) + + # 路径 + card_layout.addWidget(QLabel("微信程序路径(Weixin.exe):")) + path_row = QHBoxLayout() + self._path_edit = QLineEdit(saved_path) + self._path_edit.setPlaceholderText("请输入或浏览 Weixin.exe 路径") + browse_btn = QPushButton("浏览") + browse_btn.setFixedWidth(56) + browse_btn.clicked.connect(self._browse) + path_row.addWidget(self._path_edit) + path_row.addWidget(browse_btn) + card_layout.addLayout(path_row) + + # 数量 + count_row = QHBoxLayout() + count_row.addWidget(QLabel("多开数量:")) + self._spin = QSpinBox() + self._spin.setRange(2, 9) + self._spin.setValue(saved_count) + self._spin.setFixedWidth(70) + count_row.addWidget(self._spin) + count_row.addStretch() + card_layout.addLayout(count_row) + + # 使用说明 + usage = QTextEdit() + usage.setReadOnly(True) + usage.setPlainText(_USAGE) + usage.setFixedHeight(150) + usage.setStyleSheet("font-size:11px; border-radius:6px;") + card_layout.addWidget(usage) + + # 按钮行 + btn_row = QHBoxLayout() + btn_row.addStretch() + self._start_btn = QPushButton("开始多开") + self._start_btn.setFixedHeight(32) + self._start_btn.setMinimumWidth(90) + self._start_btn.clicked.connect(self._start) + cancel_btn = QPushButton("取消") + cancel_btn.setFixedHeight(32) + cancel_btn.setMinimumWidth(70) + cancel_btn.clicked.connect(self.reject) + btn_row.addWidget(self._start_btn) + btn_row.addWidget(cancel_btn) + card_layout.addLayout(btn_row) + + root.addWidget(self._card) + + def _apply_theme(self): + t = theme.current() + is_dark = theme.name() == "dark" + self._card.setStyleSheet(f""" + QWidget#wechat_card {{ + background: {t['panel_bg']}; + border-radius: 10px; + border: 1px solid {t['panel_border']}; + }} + QLabel {{ color: {t['search_color']}; background: transparent; }} + QLineEdit {{ + background: {t['search_bg']}; + border: 1px solid {t['search_border']}; + border-radius: 5px; + color: {t['search_color']}; + padding: 4px 8px; + }} + QSpinBox {{ + background: {t['search_bg']}; + border: 1px solid {t['search_border']}; + border-radius: 5px; + color: {t['search_color']}; + padding: 2px 4px; + }} + QTextEdit {{ + background: {t['search_bg']}; + color: {t['search_color']}; + border: 1px solid {t['search_border']}; + }} + QPushButton {{ + border: 1px solid {t['search_border']}; + border-radius: 5px; + color: {t['btn_color']}; + background: {t['search_bg']}; + font-size: 12px; + padding: 0 10px; + }} + QPushButton:hover {{ background: {t['header_hover']}; }} + """) + + def _browse(self): + path, _ = QFileDialog.getOpenFileName( + self, "选择 Weixin.exe", "", "可执行文件 (*.exe)" + ) + if path: + self._path_edit.setText(path) + + def _start(self): + exe = self._path_edit.text().strip() + count = self._spin.value() + + if not exe: + self._path_edit.setPlaceholderText("⚠ 请先填写路径") + return + if not os.path.isfile(exe): + self._path_edit.setStyleSheet( + self._path_edit.styleSheet() + "border-color: red;" + ) + return + + # 保存设置 + database.set_setting(_DB_KEY_PATH, exe) + database.set_setting(_DB_KEY_COUNT, str(count)) + + # 写临时 bat,连续 start 多次 + lines = ["@echo off"] + for _ in range(count): + lines.append(f'start "" "{exe}"') + bat_content = "\r\n".join(lines) + "\r\n" + + tmp = tempfile.NamedTemporaryFile( + delete=False, suffix=".bat", mode="w", encoding="gbk" + ) + tmp.write(bat_content) + tmp.close() + + subprocess.Popen( + ["cmd", "/c", tmp.name], + creationflags=subprocess.CREATE_NO_WINDOW + ) + self.accept() + + # 无边框拖动 + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft() + + def mouseMoveEvent(self, event): + if self._drag_pos and event.buttons() & Qt.MouseButton.LeftButton: + self.move(event.globalPosition().toPoint() - self._drag_pos) + + def mouseReleaseEvent(self, event): + self._drag_pos = None