增加微信多开功能
This commit is contained in:
parent
3051f6ca49
commit
621d752a8d
@ -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'],
|
||||
)
|
||||
|
||||
5
Readme.md
Normal file
5
Readme.md
Normal file
@ -0,0 +1,5 @@
|
||||
# 编译命令
|
||||
python -m PyInstaller CleanDesktopOrganizer.spec
|
||||
|
||||
# 清除缓存重新打包命令
|
||||
python -m PyInstaller CleanDesktopOrganizer.spec --clean
|
||||
2
main.py
2
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):
|
||||
|
||||
Binary file not shown.
@ -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()
|
||||
|
||||
@ -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._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)
|
||||
|
||||
234
ui/wechat_multi.py
Normal file
234
ui/wechat_multi.py
Normal file
@ -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
|
||||
Loading…
Reference in New Issue
Block a user