diff --git a/build.py b/build.py
index af4528e..83a17c3 100644
--- a/build.py
+++ b/build.py
@@ -5,33 +5,48 @@
import json
from typing import Dict
+
def check_and_install_packages(packages_with_versions: Dict[str, str]):
"""检查并安装指定版本的包"""
print("正在检查并安装必要的依赖包...")
-
+
for package, version in packages_with_versions.items():
try:
# 尝试导入包来检查是否已安装
- __import__(package.replace('-', '_').replace('.', '_'))
+ __import__(package.replace("-", "_").replace(".", "_"))
print(f"✓ {package} 已安装")
except ImportError:
print(f"正在安装 {package}=={version}...")
try:
# 对于win11toast包特殊处理,忽略安装失败
- if package == 'win11toast':
+ if package == "win11toast":
try:
subprocess.run(
- [sys.executable, "-m", "pip", "install", f"{package}=={version}"],
- check=False
+ [
+ sys.executable,
+ "-m",
+ "pip",
+ "install",
+ f"{package}=={version}",
+ ],
+ check=False,
)
print(f"✓ {package}=={version} 安装成功")
except:
- print(f"! {package} 安装失败,但这不会影响程序核心功能,继续打包")
+ print(
+ f"! {package} 安装失败,但这不会影响程序核心功能,继续打包"
+ )
else:
# 其他包正常安装
subprocess.run(
- [sys.executable, "-m", "pip", "install", f"{package}=={version}"],
- check=True
+ [
+ sys.executable,
+ "-m",
+ "pip",
+ "install",
+ f"{package}=={version}",
+ ],
+ check=True,
)
print(f"✓ {package}=={version} 安装成功")
except subprocess.CalledProcessError as e:
@@ -39,29 +54,31 @@ def check_and_install_packages(packages_with_versions: Dict[str, str]):
print(f" 尝试安装不指定版本的 {package}...")
try:
subprocess.run(
- [sys.executable, "-m", "pip", "install", package],
- check=True
+ [sys.executable, "-m", "pip", "install", package], check=True
)
print(f"✓ {package} 安装成功")
except subprocess.CalledProcessError as e2:
print(f"! 安装 {package} 失败: {str(e2)}")
- if package == 'win11toast':
+ if package == "win11toast":
print(f" {package} 安装失败,但这不会影响程序核心功能")
else:
return False
return True
+
def create_notification_alternative():
"""创建替代win11toast的通知实现文件"""
print("正在创建通知替代实现...")
-
+
# 在dist目录下创建一个替代的通知模块
- notification_dir = os.path.join('dist', 'win11toast')
+ notification_dir = os.path.join("dist", "win11toast")
if not os.path.exists(notification_dir):
os.makedirs(notification_dir)
-
+
# 创建__init__.py
- with open(os.path.join(notification_dir, '__init__.py'), 'w', encoding='utf-8') as f:
+ with open(
+ os.path.join(notification_dir, "__init__.py"), "w", encoding="utf-8"
+ ) as f:
f.write('''
# 替代win11toast的简易实现
import ctypes
@@ -78,39 +95,44 @@ def notify(title, message, **kwargs):
"""显示一个Windows通知,使用MessageBox替代"""
toast(title, message, **kwargs)
''')
-
+
# 创建空的__pycache__目录以避免警告
- pycache_dir = os.path.join(notification_dir, '__pycache__')
+ pycache_dir = os.path.join(notification_dir, "__pycache__")
if not os.path.exists(pycache_dir):
os.makedirs(pycache_dir)
-
+
print("通知替代实现创建完成")
+
def get_installed_packages() -> Dict[str, str]:
"""获取当前已安装的包版本信息"""
result = {}
try:
- output = subprocess.check_output([sys.executable, "-m", "pip", "freeze"]).decode('utf-8')
- for line in output.split('\n'):
- if '==' in line:
- package, version = line.strip().split('==', 1)
+ output = subprocess.check_output(
+ [sys.executable, "-m", "pip", "freeze"]
+ ).decode("utf-8")
+ for line in output.split("\n"):
+ if "==" in line:
+ package, version = line.strip().split("==", 1)
result[package] = version
except Exception as e:
print(f"获取已安装包信息时出错: {str(e)}")
return result
+
def write_requirements_file(packages_with_versions: Dict[str, str]):
"""生成requirements.txt文件"""
print("正在生成requirements.txt文件...")
- with open('requirements.txt', 'w', encoding='utf-8') as f:
+ with open("requirements.txt", "w", encoding="utf-8") as f:
for package, version in packages_with_versions.items():
f.write(f"{package}=={version}\n")
print("requirements.txt文件已生成")
+
def create_manifest_file():
"""创建应用程序清单文件,请求管理员权限"""
print("正在创建应用程序清单文件...")
- manifest_content = '''
+ manifest_content = """
@@ -119,44 +141,45 @@ def create_manifest_file():
-'''
-
- with open('app.manifest', 'w', encoding='utf-8') as f:
+"""
+
+ with open("app.manifest", "w", encoding="utf-8") as f:
f.write(manifest_content)
print("应用程序清单文件已创建")
+
def create_spec_file(sv_ttk_path: str):
"""创建PyInstaller spec文件"""
print("正在创建PyInstaller spec文件...")
-
+
# 这里列出所有需要的隐藏导入
hidden_imports = [
- 'sv_ttk',
- 'keyboard',
- 'mouse',
- 'win32gui',
- 'win32process',
- 'win32con',
- 'win32api',
- 'win32com.client',
- 'json',
- 'requests',
- 'math',
- 'ctypes',
- 'threading',
- 'time',
- 'webbrowser',
- 're',
- 'traceback',
- 'wmi',
- 'pythoncom',
- 'concurrent.futures',
- 'winreg',
- 'win11toast' # 总是包含win11toast,即使安装失败也不影响
+ "sv_ttk",
+ "keyboard",
+ "mouse",
+ "win32gui",
+ "win32process",
+ "win32con",
+ "win32api",
+ "win32com.client",
+ "json",
+ "requests",
+ "math",
+ "ctypes",
+ "threading",
+ "time",
+ "webbrowser",
+ "re",
+ "traceback",
+ "wmi",
+ "pythoncom",
+ "concurrent.futures",
+ "winreg",
+ "win11toast", # 总是包含win11toast,即使安装失败也不影响
]
-
+
# 创建spec文件内容
- spec_content = f'''# -*- mode: python ; coding: utf-8 -*-
+ spec_content = f"""# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['chrome_manager.py'],
@@ -166,6 +189,7 @@ def create_spec_file(sv_ttk_path: str):
('app.ico', '.'),
(r'{sv_ttk_path}', 'sv_ttk'),
('README.md', '.'),
+ ('chrome.png', '.'),
('settings.json', '.') if os.path.exists('settings.json') else None,
],
hiddenimports={hidden_imports},
@@ -200,38 +224,50 @@ def create_spec_file(sv_ttk_path: str):
uac_uiaccess=False,
disable_windowed_traceback=False,
)
-'''
-
- with open('chrome_manager.spec', 'w', encoding='utf-8') as f:
+"""
+
+ with open("chrome_manager.spec", "w", encoding="utf-8") as f:
f.write(spec_content)
print("PyInstaller spec文件已创建")
+
def find_sv_ttk_path():
try:
import sv_ttk
+
return os.path.dirname(sv_ttk.__file__)
except ImportError:
print("未找到sv_ttk模块,请先安装")
return None
+
def ensure_icon_exists():
- if not os.path.exists('app.ico'):
+ if not os.path.exists("app.ico"):
print("警告: 未找到app.ico文件,将使用默认图标")
# 可以考虑生成一个简单的图标或从网络下载一个
try:
# 尝试从Windows系统中复制一个默认图标
- shutil.copy(os.path.join(os.environ['SystemRoot'], 'System32', 'shell32.dll'), 'temp_icon.dll')
- subprocess.run(['powershell', '-Command',
- "(New-Object -ComObject Shell.Application).NameSpace(0).ParseName('temp_icon.dll').GetLink.GetIconLocation() | Out-File -FilePath 'app.ico'"],
- check=True)
- os.remove('temp_icon.dll')
+ shutil.copy(
+ os.path.join(os.environ["SystemRoot"], "System32", "shell32.dll"),
+ "temp_icon.dll",
+ )
+ subprocess.run(
+ [
+ "powershell",
+ "-Command",
+ "(New-Object -ComObject Shell.Application).NameSpace(0).ParseName('temp_icon.dll').GetLink.GetIconLocation() | Out-File -FilePath 'app.ico'",
+ ],
+ check=True,
+ )
+ os.remove("temp_icon.dll")
except Exception as e:
print(f"生成默认图标失败: {str(e)}")
print("将使用PyInstaller默认图标")
+
def ensure_settings_exists():
"""确保settings.json文件存在"""
- if not os.path.exists('settings.json'):
+ if not os.path.exists("settings.json"):
print("正在创建默认settings.json文件...")
default_settings = {
"shortcut_path": "",
@@ -239,30 +275,35 @@ def ensure_settings_exists():
"icon_dir": "",
"screen_selection": "",
"sync_shortcut": None,
- "window_position": {"x": 100, "y": 100}
+ "window_position": {"x": 100, "y": 100},
}
- with open('settings.json', 'w', encoding='utf-8') as f:
+ with open("settings.json", "w", encoding="utf-8") as f:
json.dump(default_settings, f, ensure_ascii=False, indent=4)
print("默认settings.json文件已创建")
+
def modify_chrome_manager_for_win11toast():
"""修改chrome_manager.py中的通知相关代码,添加简单的try-except处理"""
print("检查chrome_manager.py是否需要修改通知实现...")
-
+
try:
- with open('chrome_manager.py', 'r', encoding='utf-8') as f:
+ with open("chrome_manager.py", "r", encoding="utf-8") as f:
content = f.read()
-
+
# 如果已经有错误处理,则不需要修改
- if "try:" in content and "from win11toast import notify, toast" in content and "except ImportError:" in content:
+ if (
+ "try:" in content
+ and "from win11toast import notify, toast" in content
+ and "except ImportError:" in content
+ ):
print("chrome_manager.py已包含通知错误处理")
return
-
+
# 查找win11toast导入行
if "from win11toast import notify, toast" in content:
# 替换成带错误处理的版本
original = "from win11toast import notify, toast"
- replacement = '''# 添加通知错误处理
+ replacement = """# 添加通知错误处理
try:
from win11toast import notify, toast
except ImportError:
@@ -270,13 +311,13 @@ def modify_chrome_manager_for_win11toast():
def toast(title, message, **kwargs):
pass
def notify(title, message, **kwargs):
- pass'''
-
+ pass"""
+
modified_content = content.replace(original, replacement)
-
- with open('chrome_manager.py', 'w', encoding='utf-8') as f:
+
+ with open("chrome_manager.py", "w", encoding="utf-8") as f:
f.write(modified_content)
-
+
print("成功添加通知错误处理到chrome_manager.py")
else:
print("未找到win11toast导入行,跳过修改")
@@ -284,6 +325,7 @@ def notify(title, message, **kwargs):
print(f"修改chrome_manager.py失败: {str(e)}")
print("继续打包过程...")
+
def show_success_message():
print("\n")
print("─────────────────────────────────────────────────────")
@@ -298,6 +340,7 @@ def show_success_message():
print("─────────────────────────────────────────────────────")
print("\n")
+
def show_failure_message(error_msg="未知错误"):
print("\n")
print("─────────────────────────────────────────────────────")
@@ -312,77 +355,80 @@ def show_failure_message(error_msg="未知错误"):
print("─────────────────────────────────────────────────────")
print("\n")
+
def build():
"""打包程序"""
print("\n===== 开始打包Chrome多窗口管理器 V2.0 =====\n")
-
+
# 修改chrome_manager.py添加简单的错误处理
modify_chrome_manager_for_win11toast()
-
+
# 需要的包和版本列表
required_packages = {
- 'pyinstaller': '6.12.0',
- 'sv-ttk': '2.6.0',
- 'keyboard': '0.13.5',
- 'mouse': '0.7.1',
- 'pywin32': '309',
- 'wmi': '1.5.1',
- 'requests': '2.32.3',
- 'pillow': '11.1.0',
- 'win11toast': '0.32', # 包含win11toast但允许安装失败
+ "pyinstaller": "6.12.0",
+ "sv-ttk": "2.6.0",
+ "keyboard": "0.13.5",
+ "mouse": "0.7.1",
+ "pywin32": "309",
+ "wmi": "1.5.1",
+ "requests": "2.32.3",
+ "pillow": "11.1.0",
+ "win11toast": "0.32", # 包含win11toast但允许安装失败
}
-
+
# 获取当前已安装的包
installed_packages = get_installed_packages()
-
+
# 更新为实际安装的版本
for package in required_packages:
if package in installed_packages:
required_packages[package] = installed_packages[package]
-
+
# 检查并安装必要的包
if not check_and_install_packages(required_packages):
print("安装必要的包失败,尝试继续打包...")
-
+
# 创建requirements.txt文件
write_requirements_file(required_packages)
-
+
# 确保其他必要文件存在
ensure_icon_exists()
ensure_settings_exists()
-
+
# 创建清单文件
create_manifest_file()
-
+
# 查找sv_ttk路径
sv_ttk_path = find_sv_ttk_path()
if not sv_ttk_path:
print("无法找到sv_ttk模块,打包终止")
show_failure_message("未找到sv_ttk模块")
return False
-
+
# 创建spec文件
create_spec_file(sv_ttk_path)
-
+
# 清理旧的构建文件
print("正在清理旧的构建文件...")
- if os.path.exists('build'):
- shutil.rmtree('build')
- if os.path.exists('dist'):
- shutil.rmtree('dist')
-
+ if os.path.exists("build"):
+ shutil.rmtree("build")
+ if os.path.exists("dist"):
+ shutil.rmtree("dist")
+
# 运行PyInstaller
print("\n正在打包程序...")
try:
# 当使用 .spec 文件时,不应在命令行传递 --clean 或 --noupx 等选项
# 这些选项应在 spec 文件内配置,或由脚本本身处理(如清理目录)
- subprocess.run(['pyinstaller', 'chrome_manager.spec'], check=True)
+ subprocess.run(["pyinstaller", "chrome_manager.spec"], check=True)
print("\n打包完成!程序文件在dist文件夹中。")
-
+
# 复制额外文件到dist目录
- if not os.path.exists(os.path.join('dist', 'settings.json')) and os.path.exists('settings.json'):
- shutil.copy('settings.json', os.path.join('dist', 'settings.json'))
-
+ if not os.path.exists(os.path.join("dist", "settings.json")) and os.path.exists(
+ "settings.json"
+ ):
+ shutil.copy("settings.json", os.path.join("dist", "settings.json"))
+
show_success_message()
return True
except subprocess.CalledProcessError as e:
@@ -390,10 +436,11 @@ def build():
show_failure_message(error_msg)
return False
+
if __name__ == "__main__":
try:
success = build()
except Exception as e:
show_failure_message(str(e))
finally:
- input("\n按回车键退出...")
\ No newline at end of file
+ input("\n按回车键退出...")
diff --git a/chrome.png b/chrome.png
new file mode 100644
index 0000000..77e4b23
Binary files /dev/null and b/chrome.png differ
diff --git a/chrome_manager.py b/chrome_manager.py
index 4466846..f9ea948 100644
--- a/chrome_manager.py
+++ b/chrome_manager.py
@@ -21,6 +21,9 @@
import webbrowser
import sv_ttk
import win32security
+import random
+from PIL import Image, ImageDraw, ImageFont
+
# 添加通知错误处理
try:
from win11toast import notify, toast
@@ -28,8 +31,11 @@
# 简单的空函数替代
def toast(title, message, **kwargs):
pass
+
def notify(title, message, **kwargs):
pass
+
+
import re
import socket
import traceback
@@ -38,19 +44,22 @@ def notify(title, message, **kwargs):
import concurrent.futures
import random
+
# 添加滚轮钩子所需的结构体定义
class POINT(ctypes.Structure):
_fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]
+
class MSLLHOOKSTRUCT(ctypes.Structure):
_fields_ = [
("pt", POINT),
("mouseData", wintypes.DWORD),
("flags", wintypes.DWORD),
("time", wintypes.DWORD),
- ("dwExtraInfo", ctypes.POINTER(wintypes.ULONG))
+ ("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
]
+
def is_admin():
# 检查是否具有管理员权限
try:
@@ -58,28 +67,32 @@ def is_admin():
except:
return False
+
def run_as_admin():
# 以管理员权限重新运行程序
- ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
+ ctypes.windll.shell32.ShellExecuteW(
+ None, "runas", sys.executable, " ".join(sys.argv), None, 1
+ )
+
class ChromeManager:
def __init__(self):
"""初始化"""
# 记录启动时间用于性能分析
self.start_time = time.time()
-
+
# 默认设置值
self.show_chrome_tip = True # 是否显示Chrome后台运行提示
-
+
# 加载设置
self.settings = self.load_settings()
-
+
self.enable_cdp = True # 始终开启CDP
-
+
# 从设置中读取是否显示Chrome提示的设置
- if 'show_chrome_tip' in self.settings:
- self.show_chrome_tip = self.settings['show_chrome_tip']
-
+ if "show_chrome_tip" in self.settings:
+ self.show_chrome_tip = self.settings["show_chrome_tip"]
+
# 滚轮钩子相关参数
self.wheel_hook_id = None
self.wheel_hook_proc = None
@@ -92,132 +105,157 @@ def __init__(self):
self.shortcut_to_pid = {}
# 存储进程ID和窗口编号的映射关系
self.pid_to_number = {}
-
- if not is_admin():
- if messagebox.askyesno("权限不足", "需要管理员权限才能运行同步功能。\n是否以管理员身份重新启动程序?"):
- run_as_admin()
- sys.exit()
-
+
+ # 加载设置
+ self.settings = self.load_settings()
+
+ # 检查是否已确认管理员权限,不再弹窗
+ self.admin_confirmed = self.settings.get("admin_confirmed", False)
+
+ # 检查管理员权限,未确认时才弹窗
+ if not self.admin_confirmed and not is_admin():
+ try:
+ # 只用原有messagebox.askyesno弹窗
+ user_confirmed = messagebox.askyesno(
+ "权限不足",
+ "需要管理员权限才能运行同步功能。\n是否以管理员身份重新启动程序?",
+ )
+ if user_confirmed:
+ # 用户点击"是",以后不再提示
+ self.settings["admin_confirmed"] = True
+ self.save_settings()
+ run_as_admin()
+ sys.exit()
+ except Exception as e:
+ print(f"管理员权限弹窗异常: {str(e)}")
# 确保settings.json文件存在
- if not os.path.exists('settings.json'):
- with open('settings.json', 'w', encoding='utf-8') as f:
+ if not os.path.exists("settings.json"):
+ with open("settings.json", "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=4)
-
+
self.root = tk.Tk()
self.root.title("NoBiggie社区Chrome多窗口管理器 V2.0")
-
+
# 先隐藏主窗口,避免闪烁
self.root.withdraw()
-
+
# 随机数字输入相关配置 - 移动到root创建之后
self.random_min_value = tk.StringVar(value="1000")
self.random_max_value = tk.StringVar(value="2000")
self.random_overwrite = tk.BooleanVar(value=True)
self.random_delayed = tk.BooleanVar(value=False)
-
+
try:
icon_path = os.path.join(os.path.dirname(__file__), "app.ico")
if os.path.exists(icon_path):
self.root.iconbitmap(icon_path)
except Exception as e:
print(f"设置图标失败: {str(e)}")
-
+
# 设置固定的窗口大小
self.window_width = 700
self.window_height = 360
self.root.geometry(f"{self.window_width}x{self.window_height}")
self.root.resizable(False, False)
-
+
# 设置关闭事件处理
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
-
+
# 加载主题
sv_ttk.set_theme("light")
print(f"[{time.time() - self.start_time:.3f}s] 主题加载完成")
-
+
# 仅保存/加载窗口位置,不包括大小
last_position = self.load_window_position()
if last_position:
try:
# 直接使用返回的位置信息
- self.root.geometry(f"{self.window_width}x{self.window_height}{last_position}")
+ self.root.geometry(
+ f"{self.window_width}x{self.window_height}{last_position}"
+ )
except Exception as e:
print(f"应用窗口位置时出错: {e}")
-
+
self.window_list = None
self.windows = []
self.master_window = None
self.screens = [] # 初始化屏幕列表
-
+
# 从设置加载所有路径
- self.shortcut_path = self.settings.get('shortcut_path', '')
- self.cache_dir = self.settings.get('cache_dir', '')
- self.icon_dir = self.settings.get('icon_dir', '')
- self.screen_selection = self.settings.get('screen_selection', '')
-
+ self.shortcut_path = self.settings.get("shortcut_path", "")
+ self.cache_dir = self.settings.get("cache_dir", "")
+ self.icon_dir = self.settings.get("icon_dir", "")
+ self.screen_selection = self.settings.get("screen_selection", "")
+
print("初始化加载设置:", self.settings) # 调试输出
-
+
self.path_entry = None
-
+
# 初始化快捷键相关属性
self.shortcut_hook = None
- self.current_shortcut = self.settings.get('sync_shortcut', None)
+ self.current_shortcut = self.settings.get("sync_shortcut", None)
if self.current_shortcut:
self.set_shortcut(self.current_shortcut)
-
+
self.shell = win32com.client.Dispatch("WScript.Shell")
self.select_all_var = tk.StringVar(value="全部选择")
-
+
self.is_syncing = False
self.sync_button = None
self.mouse_hook_id = None
self.keyboard_hook = None
self.hook_thread = None
- self.user32 = ctypes.WinDLL('user32', use_last_error=True)
+ self.user32 = ctypes.WinDLL("user32", use_last_error=True)
self.sync_windows = []
-
+
self.chrome_drivers = {}
-
+
# 调试端口映射 - 将窗口号映射到调试端口
self.debug_ports = {}
# 基础调试端口
self.base_debug_port = 9222
-
+
self.DWMWA_BORDER_COLOR = 34
self.DWM_MAGIC_COLOR = 0x00FF0000
-
+
self.popup_mappings = {}
-
+
self.popup_monitor_thread = None
-
+
self.mouse_threshold = 3
self.last_mouse_position = (0, 0)
self.last_move_time = 0
self.move_interval = 0.016
-
+
# 创建样式
self.create_styles()
-
+
# 创建界面
self.create_widgets()
-
+
# 更新树形视图样式
self.update_treeview_style()
-
+
# 窗口尺寸已在初始化时固定,无需再次调整
# 在初始化时设置进程缓解策略
- PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON = 0x100000000000
+ PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON = (
+ 0x100000000000
+ )
ctypes.windll.kernel32.SetProcessMitigationPolicy(
0, # ProcessSignaturePolicy
- ctypes.byref(ctypes.c_ulonglong(PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON)),
- ctypes.sizeof(ctypes.c_ulonglong)
+ ctypes.byref(
+ ctypes.c_ulonglong(
+ PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON
+ )
+ ),
+ ctypes.sizeof(ctypes.c_ulonglong),
)
# 检测Windows版本
self.win_ver = sys.getwindowsversion()
self.is_win11 = self.win_ver.build >= 22000
-
+
# 初始化系统托盘通知
try:
if self.is_win11:
@@ -226,16 +264,22 @@ def __init__(self):
else:
# Windows 10使用win32gui通知
self.hwnd = win32gui.GetForegroundWindow()
- self.notification_flags = win32gui.NIF_ICON | win32gui.NIF_INFO | win32gui.NIF_TIP
-
+ self.notification_flags = (
+ win32gui.NIF_ICON | win32gui.NIF_INFO | win32gui.NIF_TIP
+ )
+
# 加载app.ico图标
try:
icon_path = os.path.join(os.path.dirname(__file__), "app.ico")
if os.path.exists(icon_path):
# 加载应用程序图标
icon_handle = win32gui.LoadImage(
- 0, icon_path, win32con.IMAGE_ICON,
- 0, 0, win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
+ 0,
+ icon_path,
+ win32con.IMAGE_ICON,
+ 0,
+ 0,
+ win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE,
)
else:
# 使用默认图标
@@ -243,16 +287,16 @@ def __init__(self):
except Exception as e:
print(f"加载托盘图标失败: {str(e)}")
icon_handle = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
-
+
self.notify_id = (
- self.hwnd,
+ self.hwnd,
0,
self.notification_flags,
win32con.WM_USER + 20,
icon_handle,
- "Chrome多窗口管理器"
+ "Chrome多窗口管理器",
)
-
+
# 先注册托盘图标
try:
win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self.notify_id)
@@ -268,208 +312,221 @@ def __init__(self):
self.context_menu.add_command(label="粘贴", command=self.paste_text)
self.context_menu.add_separator()
self.context_menu.add_command(label="全选", command=self.select_all_text)
-
+
# 保存当前焦点的文本框引用
self.current_text_widget = None
# 添加CDP WebSocket连接池
- #self.ws_connections = {}
- #self.ws_lock = threading.Lock()
- #self.scroll_sync_enabled = True # 添加滚轮同步控制标志
+ # self.ws_connections = {}
+ # self.ws_lock = threading.Lock()
+ # self.scroll_sync_enabled = True # 添加滚轮同步控制标志
# 安排延迟初始化
- self.root.after(100, self.delayed_initialization)
+ self.root.after(100, self.delayed_initialization)
print(f"[{time.time() - self.start_time:.3f}s] __init__ 完成, 已安排延迟初始化")
def create_styles(self):
style = ttk.Style()
-
- default_font = ('Microsoft YaHei UI', 9)
-
- style.configure('Small.TEntry',
- padding=(4, 0),
- font=default_font
- )
-
- style.configure('TButton', font=default_font)
- style.configure('TLabel', font=default_font)
- style.configure('TEntry', font=default_font)
- style.configure('Treeview', font=default_font)
- style.configure('Treeview.Heading', font=default_font)
- style.configure('TLabelframe.Label', font=default_font)
- style.configure('TNotebook.Tab', font=default_font)
-
+
+ default_font = ("Microsoft YaHei UI", 9)
+
+ style.configure("Small.TEntry", padding=(4, 0), font=default_font)
+
+ style.configure("TButton", font=default_font)
+ style.configure("TLabel", font=default_font)
+ style.configure("TEntry", font=default_font)
+ style.configure("Treeview", font=default_font)
+ style.configure("Treeview.Heading", font=default_font)
+ style.configure("TLabelframe.Label", font=default_font)
+ style.configure("TNotebook.Tab", font=default_font)
+
# 链接样式
- style.configure('Link.TLabel',
- foreground='#0d6efd',
- cursor='hand2',
- font=('Microsoft YaHei UI', 9, 'underline')
+ style.configure(
+ "Link.TLabel",
+ foreground="#0d6efd",
+ cursor="hand2",
+ font=("Microsoft YaHei UI", 9, "underline"),
)
-
+
def update_treeview_style(self):
"""更新Treeview组件的样式,此方法应在window_list初始化后调用"""
if self.window_list:
- self.window_list.tag_configure("master",
- background="#0d6efd",
- foreground="white")
+ self.window_list.tag_configure(
+ "master", background="#0d6efd", foreground="white"
+ )
def create_widgets(self):
"""创建界面元素"""
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.X, padx=10, pady=5)
-
+
upper_frame = ttk.Frame(main_frame)
upper_frame.pack(fill=tk.X)
-
+
arrange_frame = ttk.LabelFrame(upper_frame, text="自定义排列")
arrange_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(3, 0))
-
+
manage_frame = ttk.LabelFrame(upper_frame, text="窗口管理")
manage_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
-
+
# 创建两行按钮区域
button_rows = ttk.Frame(manage_frame)
button_rows.pack(fill=tk.X)
-
+
# 第一行:基本操作按钮
first_row = ttk.Frame(button_rows)
first_row.pack(fill=tk.X)
-
- ttk.Button(first_row, text="导入窗口", command=self.import_windows, style='Accent.TButton').pack(side=tk.LEFT, padx=2)
- select_all_label = ttk.Label(first_row, textvariable=self.select_all_var, style='Link.TLabel')
+
+ ttk.Button(
+ first_row,
+ text="导入窗口",
+ command=self.import_windows,
+ style="Accent.TButton",
+ ).pack(side=tk.LEFT, padx=2)
+ select_all_label = ttk.Label(
+ first_row, textvariable=self.select_all_var, style="Link.TLabel"
+ )
select_all_label.pack(side=tk.LEFT, padx=5)
- select_all_label.bind('', self.toggle_select_all)
- ttk.Button(first_row, text="自动排列", command=self.auto_arrange_windows).pack(side=tk.LEFT, padx=2)
- ttk.Button(first_row, text="关闭选中", command=self.close_selected_windows).pack(side=tk.LEFT, padx=2)
-
+ select_all_label.bind("", self.toggle_select_all)
+ ttk.Button(first_row, text="自动排列", command=self.auto_arrange_windows).pack(
+ side=tk.LEFT, padx=2
+ )
+ ttk.Button(
+ first_row, text="关闭选中", command=self.close_selected_windows
+ ).pack(side=tk.LEFT, padx=2)
+
self.sync_button = ttk.Button(
first_row,
text="▶ 开始同步",
command=self.toggle_sync,
- style='Accent.TButton'
+ style="Accent.TButton",
)
self.sync_button.pack(side=tk.LEFT, padx=5)
-
+
# 添加设置按钮
ttk.Button(
- first_row,
- text="🔗 设置",
- command=self.show_settings_dialog,
- width=8
+ first_row, text="🔗 设置", command=self.show_settings_dialog, width=8
).pack(side=tk.LEFT, padx=2)
-
+
list_frame = ttk.Frame(manage_frame)
list_frame.pack(fill=tk.BOTH, expand=True, pady=(2, 0))
-
+
# 创建窗口列表
- self.window_list = ttk.Treeview(list_frame,
+ self.window_list = ttk.Treeview(
+ list_frame,
columns=("select", "number", "title", "master", "hwnd"),
- show="headings",
- height=4,
- style='Accent.Treeview'
+ show="headings",
+ height=4,
+ style="Accent.Treeview",
)
self.window_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
-
+
self.window_list.heading("select", text="选择")
self.window_list.heading("number", text="窗口序号")
self.window_list.heading("title", text="页面标题")
self.window_list.heading("master", text="主控")
self.window_list.heading("hwnd", text="")
-
+
self.window_list.column("select", width=50, anchor="center")
self.window_list.column("number", width=60, anchor="center")
self.window_list.column("title", width=260)
self.window_list.column("master", width=50, anchor="center")
self.window_list.column("hwnd", width=0, stretch=False) # 隐藏hwnd列
-
+
self.window_list.tag_configure("master", background="lightblue")
-
- self.window_list.bind('', self.on_click)
-
+
+ self.window_list.bind("", self.on_click)
+
# 添加右键菜单功能
self.window_list_menu = tk.Menu(self.root, tearoff=0)
- self.window_list_menu.add_command(label="关闭此窗口", command=self.close_selected_window)
- self.window_list.bind('', self.show_window_list_menu)
-
+ self.window_list_menu.add_command(
+ label="关闭此窗口", command=self.close_selected_window
+ )
+ self.window_list.bind("", self.show_window_list_menu)
+
# 添加滚动条
- scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.window_list.yview)
+ scrollbar = ttk.Scrollbar(
+ list_frame, orient=tk.VERTICAL, command=self.window_list.yview
+ )
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.window_list.configure(yscrollcommand=scrollbar.set)
-
+
params_frame = ttk.Frame(arrange_frame)
params_frame.pack(fill=tk.X, padx=5, pady=2)
-
+
left_frame = ttk.Frame(params_frame)
left_frame.pack(side=tk.LEFT, padx=(0, 5))
right_frame = ttk.Frame(params_frame)
right_frame.pack(side=tk.LEFT)
-
+
ttk.Label(left_frame, text="起始X坐标").pack(anchor=tk.W)
- self.start_x = ttk.Entry(left_frame, width=8, style='Small.TEntry')
+ self.start_x = ttk.Entry(left_frame, width=8, style="Small.TEntry")
self.start_x.pack(fill=tk.X, pady=(0, 2))
self.start_x.insert(0, "0")
self.setup_right_click_menu(self.start_x)
-
+
ttk.Label(left_frame, text="窗口宽度").pack(anchor=tk.W)
- self.window_width = ttk.Entry(left_frame, width=8, style='Small.TEntry')
+ self.window_width = ttk.Entry(left_frame, width=8, style="Small.TEntry")
self.window_width.pack(fill=tk.X, pady=(0, 2))
self.window_width.insert(0, "500")
self.setup_right_click_menu(self.window_width)
-
+
ttk.Label(left_frame, text="水平间距").pack(anchor=tk.W)
- self.h_spacing = ttk.Entry(left_frame, width=8, style='Small.TEntry')
+ self.h_spacing = ttk.Entry(left_frame, width=8, style="Small.TEntry")
self.h_spacing.pack(fill=tk.X, pady=(0, 2))
self.h_spacing.insert(0, "0")
self.setup_right_click_menu(self.h_spacing)
-
+
ttk.Label(right_frame, text="起始Y坐标").pack(anchor=tk.W)
- self.start_y = ttk.Entry(right_frame, width=8, style='Small.TEntry')
+ self.start_y = ttk.Entry(right_frame, width=8, style="Small.TEntry")
self.start_y.pack(fill=tk.X, pady=(0, 2))
self.start_y.insert(0, "0")
self.setup_right_click_menu(self.start_y)
-
+
ttk.Label(right_frame, text="窗口高度").pack(anchor=tk.W)
- self.window_height = ttk.Entry(right_frame, width=8, style='Small.TEntry')
+ self.window_height = ttk.Entry(right_frame, width=8, style="Small.TEntry")
self.window_height.pack(fill=tk.X, pady=(0, 2))
self.window_height.insert(0, "400")
self.setup_right_click_menu(self.window_height)
-
+
ttk.Label(right_frame, text="垂直间距").pack(anchor=tk.W)
- self.v_spacing = ttk.Entry(right_frame, width=8, style='Small.TEntry')
+ self.v_spacing = ttk.Entry(right_frame, width=8, style="Small.TEntry")
self.v_spacing.pack(fill=tk.X, pady=(0, 2))
self.v_spacing.insert(0, "0")
self.setup_right_click_menu(self.v_spacing)
-
+
for widget in left_frame.winfo_children() + right_frame.winfo_children():
if isinstance(widget, ttk.Entry):
widget.pack_configure(pady=(0, 2))
-
+
bottom_frame = ttk.Frame(arrange_frame)
bottom_frame.pack(fill=tk.X, padx=5, pady=2)
-
+
row_frame = ttk.Frame(bottom_frame)
row_frame.pack(side=tk.LEFT)
ttk.Label(row_frame, text="每行窗口数").pack(anchor=tk.W)
- self.windows_per_row = ttk.Entry(row_frame, width=8, style='Small.TEntry')
+ self.windows_per_row = ttk.Entry(row_frame, width=8, style="Small.TEntry")
self.windows_per_row.pack(pady=(2, 0))
self.windows_per_row.insert(0, "5")
self.setup_right_click_menu(self.windows_per_row)
-
- ttk.Button(bottom_frame, text="自定义排列",
+
+ ttk.Button(
+ bottom_frame,
+ text="自定义排列",
command=self.custom_arrange_windows,
- style='Accent.TButton'
+ style="Accent.TButton",
).pack(side=tk.RIGHT, pady=(15, 0))
-
+
bottom_frame = ttk.Frame(self.root)
bottom_frame.pack(fill=tk.X, padx=10, pady=(5, 0))
-
+
self.tab_control = ttk.Notebook(bottom_frame)
self.tab_control.pack(side=tk.LEFT, fill=tk.X, expand=True)
-
+
# 打开窗口标签
open_window_tab = ttk.Frame(self.tab_control)
self.tab_control.add(open_window_tab, text="打开窗口")
-
+
# 简化布局结构,移除多余的嵌套frame
numbers_frame = ttk.Frame(open_window_tab)
numbers_frame.pack(fill=tk.X, padx=10, pady=10) # 统一顶部边距
@@ -477,166 +534,143 @@ def create_widgets(self):
self.numbers_entry = ttk.Entry(numbers_frame, width=20)
self.numbers_entry.pack(side=tk.LEFT, padx=5)
self.setup_right_click_menu(self.numbers_entry)
-
+
settings = self.load_settings()
- if 'last_window_numbers' in settings:
- self.numbers_entry.insert(0, settings['last_window_numbers'])
-
- self.numbers_entry.bind('', lambda e: self.open_windows())
-
+ if "last_window_numbers" in settings:
+ self.numbers_entry.insert(0, settings["last_window_numbers"])
+
+ self.numbers_entry.bind("", lambda e: self.open_windows())
+
ttk.Button(
numbers_frame,
text="打开窗口",
command=self.open_windows,
- style='Accent.TButton'
+ style="Accent.TButton",
).pack(side=tk.LEFT, padx=5)
-
+
# 添加示例文字
ttk.Label(numbers_frame, text="示例: 1-5 或 1,3,5").pack(side=tk.LEFT, padx=5)
-
+
# 批量打开网页标签
url_tab = ttk.Frame(self.tab_control)
self.tab_control.add(url_tab, text="批量打开网页")
-
+
url_frame = ttk.Frame(url_tab)
url_frame.pack(fill=tk.X, padx=10, pady=10) # 统一边距
ttk.Label(url_frame, text="网址:").pack(side=tk.LEFT)
self.url_entry = ttk.Entry(url_frame, width=20)
self.url_entry.pack(side=tk.LEFT, padx=5)
self.url_entry.insert(0, "www.google.com")
-
- self.url_entry.bind('', lambda e: self.batch_open_urls())
-
+
+ self.url_entry.bind("", lambda e: self.batch_open_urls())
+
ttk.Button(
- url_frame,
- text="批量打开",
+ url_frame,
+ text="批量打开",
command=self.batch_open_urls,
- style='Accent.TButton' # 设置蓝色风格
+ style="Accent.TButton", # 设置蓝色风格
).pack(side=tk.LEFT, padx=5)
-
+
# 添加几个常用网站快速打开按钮
twitter_button = ttk.Button(
- url_frame,
- text="Twitter",
+ url_frame,
+ text="Twitter",
command=lambda: self.set_quick_url("https://twitter.com"),
- style='Quick.TButton', # 使用自定义样式
- width=8
+ style="Quick.TButton", # 使用自定义样式
+ width=8,
)
twitter_button.pack(side=tk.LEFT, padx=2)
-
+
discord_button = ttk.Button(
- url_frame,
- text="Discord",
+ url_frame,
+ text="Discord",
command=lambda: self.set_quick_url("https://discord.com/channels/@me"),
- style='Quick.TButton', # 使用自定义样式
- width=8
+ style="Quick.TButton", # 使用自定义样式
+ width=8,
)
discord_button.pack(side=tk.LEFT, padx=2)
-
+
gmail_button = ttk.Button(
- url_frame,
+ url_frame,
text="Gmail",
command=lambda: self.set_quick_url("https://mail.google.com"),
- style='Quick.TButton', # 使用自定义样式
- width=8
+ style="Quick.TButton", # 使用自定义样式
+ width=8,
)
gmail_button.pack(side=tk.LEFT, padx=2)
-
+
# 标签页管理标签
tab_manage_tab = ttk.Frame(self.tab_control)
self.tab_control.add(tab_manage_tab, text="标签页管理")
-
+
tab_manage_frame = ttk.Frame(tab_manage_tab)
tab_manage_frame.pack(fill=tk.X, padx=10, pady=10)
-
+
ttk.Button(
tab_manage_frame,
text="仅保留当前标签页",
command=self.keep_only_current_tab,
width=20,
- style='Accent.TButton' # 应用蓝色风格
+ style="Accent.TButton", # 应用蓝色风格
).pack(side=tk.LEFT, padx=5)
-
+
ttk.Button(
tab_manage_frame,
text="仅保留新标签页",
command=self.keep_only_new_tab,
width=20,
- style='Accent.TButton' # 应用蓝色风格
+ style="Accent.TButton", # 应用蓝色风格
).pack(side=tk.LEFT, padx=5)
-
+
# 添加随机数字输入标签
random_number_tab = ttk.Frame(self.tab_control)
self.tab_control.add(random_number_tab, text="批量文本输入")
-
+
# 简化界面,只添加两个按钮
buttons_frame = ttk.Frame(random_number_tab)
buttons_frame.pack(fill=tk.X, padx=10, pady=10)
-
+
ttk.Button(
buttons_frame,
text="随机数字输入",
command=self.show_random_number_dialog,
width=20,
- style='Accent.TButton' # 应用蓝色风格
+ style="Accent.TButton", # 应用蓝色风格
).pack(side=tk.LEFT, padx=10)
-
+
ttk.Button(
buttons_frame,
text="指定文本输入",
command=self.show_text_input_dialog,
width=20,
- style='Accent.TButton' # 应用蓝色风格
+ style="Accent.TButton", # 应用蓝色风格
).pack(side=tk.LEFT, padx=10)
-
-
# 批量创建环境标签
env_create_tab = ttk.Frame(self.tab_control)
self.tab_control.add(env_create_tab, text="批量创建环境")
-
+
# 统一框架布局
input_row = ttk.Frame(env_create_tab)
input_row.pack(fill=tk.X, padx=10, pady=10) # 统一边距
-
+
# 环境编号
ttk.Label(input_row, text="创建编号:").pack(side=tk.LEFT)
self.env_numbers = ttk.Entry(input_row, width=20)
self.env_numbers.pack(side=tk.LEFT, padx=5)
self.setup_right_click_menu(self.env_numbers)
-
+
# 创建按钮
ttk.Button(
- input_row,
- text="开始创建",
+ input_row,
+ text="开始创建",
command=self.create_environments,
- style='Accent.TButton' # 设置蓝色风格
+ style="Accent.TButton", # 设置蓝色风格
).pack(side=tk.LEFT, padx=5)
-
+
# 示例文字
ttk.Label(input_row, text="示例: 1-5,7,9-12").pack(side=tk.LEFT, padx=5)
-
- # 替换图标标签页
- icon_tab = ttk.Frame(self.tab_control)
- self.tab_control.add(icon_tab, text="替换图标")
-
- icon_frame = ttk.Frame(icon_tab)
- icon_frame.pack(fill=tk.X, padx=10, pady=10) # 统一边距
-
- ttk.Label(icon_frame, text="窗口编号:").pack(side=tk.LEFT)
- self.icon_window_numbers = ttk.Entry(icon_frame, width=20)
- self.icon_window_numbers.pack(side=tk.LEFT, padx=5)
-
- ttk.Button(
- icon_frame,
- text="替换图标",
- command=self.set_taskbar_icons,
- style='Accent.TButton' # 设置蓝色风格
- ).pack(side=tk.LEFT, padx=5)
-
- # 示例文字
- ttk.Label(icon_frame, text="示例: 1-5,7,9-12").pack(side=tk.LEFT, padx=5)
-
+
# 底部按钮框架 - 在所有标签页设置完成后添加
footer_frame = ttk.Frame(self.root)
footer_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=5)
@@ -644,16 +678,19 @@ def create_widgets(self):
# 添加左侧的超链接
donate_frame = ttk.Frame(footer_frame)
donate_frame.pack(side=tk.LEFT)
-
+
donate_label = ttk.Label(
- donate_frame,
+ donate_frame,
text="铸造一个看上去没什么用的NFT 0.1SOL(其实就是打赏啦 😁)",
cursor="hand2",
- foreground="black"
+ foreground="black",
# 移除字体设置,使用系统默认字体
)
donate_label.pack(side=tk.LEFT)
- donate_label.bind("", lambda e: webbrowser.open("https://truffle.wtf/project/Devilflasher"))
+ donate_label.bind(
+ "",
+ lambda e: webbrowser.open("https://truffle.wtf/project/Devilflasher"),
+ )
author_frame = ttk.Frame(footer_frame)
author_frame.pack(side=tk.RIGHT)
@@ -663,48 +700,42 @@ def create_widgets(self):
ttk.Label(author_frame, text=" ").pack(side=tk.LEFT)
twitter_label = ttk.Label(
- author_frame,
- text="Twitter",
- cursor="hand2",
- font=("Arial", 9)
+ author_frame, text="Twitter", cursor="hand2", font=("Arial", 9)
)
twitter_label.pack(side=tk.LEFT)
- twitter_label.bind("", lambda e: webbrowser.open("https://x.com/DevilflasherX"))
+ twitter_label.bind(
+ "", lambda e: webbrowser.open("https://x.com/DevilflasherX")
+ )
ttk.Label(author_frame, text=" ").pack(side=tk.LEFT)
telegram_label = ttk.Label(
- author_frame,
- text="Telegram",
- cursor="hand2",
- font=("Arial", 9)
+ author_frame, text="Telegram", cursor="hand2", font=("Arial", 9)
)
telegram_label.pack(side=tk.LEFT)
- telegram_label.bind("", lambda e: webbrowser.open("https://t.me/devilflasher0"))
+ telegram_label.bind(
+ "", lambda e: webbrowser.open("https://t.me/devilflasher0")
+ )
def toggle_select_all(self, event=None):
- #切换全选状态
+ # 切换全选状态
try:
items = self.window_list.get_children()
if not items:
return
-
-
+
current_text = self.select_all_var.get()
-
-
+
if current_text == "全部选择":
-
for item in items:
self.window_list.set(item, "select", "√")
- else:
-
+ else:
for item in items:
self.window_list.set(item, "select", "")
-
+
# 更新按钮状态
self.update_select_all_status()
-
+
except Exception as e:
print(f"切换全选状态失败: {str(e)}")
@@ -716,16 +747,18 @@ def update_select_all_status(self):
if not items:
self.select_all_var.set("全部选择")
return
-
+
# 检查是否全部选中
- selected_count = sum(1 for item in items if self.window_list.set(item, "select") == "√")
-
+ selected_count = sum(
+ 1 for item in items if self.window_list.set(item, "select") == "√"
+ )
+
# 根据选中数量设置按钮文本
if selected_count == len(items):
self.select_all_var.set("取消全选")
else:
self.select_all_var.set("全部选择")
-
+
except Exception as e:
print(f"更新全选状态失败: {str(e)}")
@@ -736,7 +769,7 @@ def on_click(self, event):
if region == "cell":
column = self.window_list.identify_column(event.x)
item = self.window_list.identify_row(event.y)
-
+
if column == "#1": # 选择列
current = self.window_list.set(item, "select")
self.window_list.set(item, "select", "" if current == "√" else "√")
@@ -754,14 +787,14 @@ def set_master_window(self, item):
if self.is_syncing:
self.stop_sync()
# 确保按钮状态更新
- self.sync_button.configure(text="▶ 开始同步", style='Accent.TButton')
+ self.sync_button.configure(text="▶ 开始同步", style="Accent.TButton")
self.is_syncing = False
# 显示通知
self.show_notification("同步已关闭", "切换主控窗口,同步已停止")
-
+
# 清除其他窗口的主控状态和标题
for i in self.window_list.get_children():
- values = self.window_list.item(i)['values']
+ values = self.window_list.item(i)["values"]
if values and len(values) >= 5:
hwnd = int(values[4])
title = values[2]
@@ -776,42 +809,45 @@ def set_master_window(self, item):
try:
# 使用 LoadLibrary 显式加载 dwmapi.dll
dwmapi = ctypes.WinDLL("dwmapi.dll")
-
+
# 定义参数类型
DWMWA_BORDER_COLOR = 34
color = ctypes.c_uint(0) # 默认颜色
-
+
# 恢复默认边框颜色
dwmapi.DwmSetWindowAttribute(
hwnd,
DWMWA_BORDER_COLOR,
ctypes.byref(color),
- ctypes.sizeof(ctypes.c_int)
+ ctypes.sizeof(ctypes.c_int),
)
-
+
# 强制刷新窗口
win32gui.SetWindowPos(
hwnd,
0,
- 0, 0, 0, 0,
- win32con.SWP_NOMOVE |
- win32con.SWP_NOSIZE |
- win32con.SWP_NOZORDER |
- win32con.SWP_FRAMECHANGED
+ 0,
+ 0,
+ 0,
+ 0,
+ win32con.SWP_NOMOVE
+ | win32con.SWP_NOSIZE
+ | win32con.SWP_NOZORDER
+ | win32con.SWP_FRAMECHANGED,
)
except Exception as e:
print(f"重置窗口边框颜色失败: {str(e)}")
self.window_list.set(i, "master", "")
self.window_list.item(i, tags=())
-
+
# 设置新的主控窗口
- values = self.window_list.item(item)['values']
+ values = self.window_list.item(item)["values"]
self.master_window = int(values[4])
-
+
# 设置主控标记和蓝色背景
self.window_list.set(item, "master", "√")
self.window_list.item(item, tags=("master",))
-
+
# 修改窗口标题和边框颜色
title = values[2]
if not "[主控]" in title and not "★" in title:
@@ -821,29 +857,32 @@ def set_master_window(self, item):
try:
# 加载 dwmapi.dll
dwmapi = ctypes.WinDLL("dwmapi.dll")
-
+
# 设置窗口边框颜色为红色
color = ctypes.c_uint(0x0000FF) # 红色 (BGR格式)
dwmapi.DwmSetWindowAttribute(
self.master_window,
34, # DWMWA_BORDER_COLOR
ctypes.byref(color),
- ctypes.sizeof(ctypes.c_int)
+ ctypes.sizeof(ctypes.c_int),
)
-
+
# 强制刷新窗口
win32gui.SetWindowPos(
self.master_window,
0,
- 0, 0, 0, 0,
- win32con.SWP_NOMOVE |
- win32con.SWP_NOSIZE |
- win32con.SWP_NOZORDER |
- win32con.SWP_FRAMECHANGED
+ 0,
+ 0,
+ 0,
+ 0,
+ win32con.SWP_NOMOVE
+ | win32con.SWP_NOSIZE
+ | win32con.SWP_NOZORDER
+ | win32con.SWP_FRAMECHANGED,
)
except Exception as e:
print(f"设置主控窗口边框颜色失败: {str(e)}")
-
+
except Exception as e:
print(f"设置主控窗口失败: {str(e)}")
@@ -852,49 +891,62 @@ def toggle_sync(self, event=None):
if not self.window_list.get_children():
messagebox.showinfo("提示", "请先导入窗口!")
return
-
+
# 获取选中的窗口
selected = []
for item in self.window_list.get_children():
if self.window_list.set(item, "select") == "√":
selected.append(item)
-
+
if not selected:
messagebox.showinfo("提示", "请选择要同步的窗口!")
return
-
+
# 检查主控窗口
- master_items = [item for item in self.window_list.get_children()
- if self.window_list.set(item, "master") == "√"]
-
+ master_items = [
+ item
+ for item in self.window_list.get_children()
+ if self.window_list.set(item, "master") == "√"
+ ]
+
if not master_items:
# 如果没有主控窗口,设置第一个选中的窗口为主控
self.set_master_window(selected[0])
-
+
# 切换同步状态
if not self.is_syncing:
try:
self.start_sync(selected)
- self.sync_button.configure(text="■ 停止同步", style='Accent.TButton')
+ self.sync_button.configure(text="■ 停止同步", style="Accent.TButton")
self.is_syncing = True
print("同步已开启")
# 使用after方法异步显示通知
- self.root.after(10, lambda: self.show_notification("同步已开启", "Chrome多窗口同步功能已启动"))
+ self.root.after(
+ 10,
+ lambda: self.show_notification(
+ "同步已开启", "Chrome多窗口同步功能已启动"
+ ),
+ )
except Exception as e:
print(f"开启同步失败: {str(e)}")
# 确保状态正确
self.is_syncing = False
- self.sync_button.configure(text="▶ 开始同步", style='Accent.TButton')
+ self.sync_button.configure(text="▶ 开始同步", style="Accent.TButton")
# 重新显示错误消息
messagebox.showerror("错误", str(e))
else:
try:
self.stop_sync()
- self.sync_button.configure(text="▶ 开始同步", style='Accent.TButton')
+ self.sync_button.configure(text="▶ 开始同步", style="Accent.TButton")
self.is_syncing = False
print("同步已停止")
# 使用after方法异步显示通知
- self.root.after(10, lambda: self.show_notification("同步已关闭", "Chrome多窗口同步功能已停止"))
+ self.root.after(
+ 10,
+ lambda: self.show_notification(
+ "同步已关闭", "Chrome多窗口同步功能已停止"
+ ),
+ )
except Exception as e:
print(f"停止同步失败: {str(e)}")
@@ -912,64 +964,76 @@ def show_toast():
message,
duration="short",
app_id="Chrome多开管理工具",
- on_dismissed=lambda x: None # 忽略关闭回调
+ on_dismissed=lambda x: None, # 忽略关闭回调
)
except Exception:
pass
-
+
threading.Thread(target=show_toast).start()
except TypeError:
# 如果上面的方法失败,尝试使用另一种调用方式
def show_toast_alt():
try:
- self.notify_func({
- "title": title,
- "message": message,
- "duration": "short",
- "app_id": "Chrome多开管理工具",
- "on_dismissed": lambda x: None
- })
+ self.notify_func(
+ {
+ "title": title,
+ "message": message,
+ "duration": "short",
+ "app_id": "Chrome多开管理工具",
+ "on_dismissed": lambda x: None,
+ }
+ )
except Exception:
pass
-
+
threading.Thread(target=show_toast_alt).start()
else:
# Windows 10 使用win32gui通知
try:
# 确保托盘图标已注册
- if not hasattr(self, 'notify_id'):
+ if not hasattr(self, "notify_id"):
self.hwnd = win32gui.GetForegroundWindow()
- self.notification_flags = win32gui.NIF_ICON | win32gui.NIF_INFO | win32gui.NIF_TIP
-
+ self.notification_flags = (
+ win32gui.NIF_ICON | win32gui.NIF_INFO | win32gui.NIF_TIP
+ )
+
# 加载app.ico图标
try:
- icon_path = os.path.join(os.path.dirname(__file__), "app.ico")
+ icon_path = os.path.join(
+ os.path.dirname(__file__), "app.ico"
+ )
if os.path.exists(icon_path):
# 加载应用程序图标
icon_handle = win32gui.LoadImage(
- 0, icon_path, win32con.IMAGE_ICON,
- 0, 0, win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
+ 0,
+ icon_path,
+ win32con.IMAGE_ICON,
+ 0,
+ 0,
+ win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE,
)
else:
# 使用默认图标
- icon_handle = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
+ icon_handle = win32gui.LoadIcon(
+ 0, win32con.IDI_APPLICATION
+ )
except Exception as e:
print(f"加载托盘图标失败: {str(e)}")
icon_handle = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
-
+
self.notify_id = (
- self.hwnd,
+ self.hwnd,
0,
self.notification_flags,
win32con.WM_USER + 20,
icon_handle,
- "Chrome多窗口管理器"
+ "Chrome多窗口管理器",
)
win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self.notify_id)
# 获取当前图标句柄
icon_handle = self.notify_id[4]
-
+
# 准备通知数据
notify_data = (
self.hwnd,
@@ -979,9 +1043,9 @@ def show_toast_alt():
icon_handle,
"Chrome多窗口管理器", # 托盘提示
message, # 通知内容
- 1000, # 1秒 = 1000毫秒
- title, # 通知标题
- win32gui.NIIF_INFO # 通知类型
+ 1000, # 1秒 = 1000毫秒
+ title, # 通知标题
+ win32gui.NIIF_INFO, # 通知类型
)
# 显示通知
win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, notify_data)
@@ -995,55 +1059,61 @@ def start_sync(self, selected_items):
# 确保主控窗口存在
if not self.master_window:
raise Exception("未设置主控窗口")
-
+
# 清除之前可能的同步状态
- if hasattr(self, 'is_sync') and self.is_sync:
+ if hasattr(self, "is_sync") and self.is_sync:
self.stop_sync()
time.sleep(0.2) # 等待资源清理
-
+
# 初始化同步状态变量
self.is_sync = True
self.popup_windows = [] # 储存所有弹出窗口
self.last_mouse_position = (0, 0)
self.last_move_time = time.time()
-
+
# 保存选中的窗口列表,并按编号排序
self.sync_windows = []
window_info = []
-
+
# 收集所有选中的窗口
for item in selected_items:
- values = self.window_list.item(item)['values']
+ values = self.window_list.item(item)["values"]
if values and len(values) >= 5:
number = int(values[1])
hwnd = int(values[4])
if hwnd != self.master_window: # 排除主控窗口
window_info.append((number, hwnd))
-
+
# 按编号排序
window_info.sort(key=lambda x: x[0])
-
+
# 保存所有同步窗口的句柄
self.sync_windows = [hwnd for _, hwnd in window_info]
-
+
# 检查是否存在有效的同步窗口
if not self.sync_windows:
- messagebox.showwarning("警告", "没有可同步的窗口,请至少选择两个窗口(一个主控,一个被控)")
+ messagebox.showwarning(
+ "警告", "没有可同步的窗口,请至少选择两个窗口(一个主控,一个被控)"
+ )
self.is_sync = False
return
-
+
# 启动键盘和鼠标钩子
- if not hasattr(self, 'hook_thread') or not self.hook_thread or not self.hook_thread.is_alive():
+ if (
+ not hasattr(self, "hook_thread")
+ or not self.hook_thread
+ or not self.hook_thread.is_alive()
+ ):
self.hook_thread = threading.Thread(target=self.message_loop)
self.hook_thread.daemon = True
self.hook_thread.start()
-
+
try:
# 设置键盘和鼠标钩子
keyboard.hook(self.on_keyboard_event)
mouse.hook(self.on_mouse_event)
print("已设置键盘和鼠标钩子")
-
+
# 尝试安装低级滚轮钩子,但不强制要求成功
if self.use_wheel_hook:
try:
@@ -1058,29 +1128,45 @@ def start_sync(self, selected_items):
print(f"设置钩子失败: {str(e)}")
self.stop_sync()
raise Exception(f"无法设置输入钩子: {str(e)}")
-
+
# 更新按钮状态
- self.sync_button.configure(text="■ 停止同步", style='Accent.TButton')
-
+ self.sync_button.configure(text="■ 停止同步", style="Accent.TButton")
+
# 启动插件窗口监控线程
self.popup_monitor_thread = threading.Thread(target=self.monitor_popups)
self.popup_monitor_thread.daemon = True
self.popup_monitor_thread.start()
-
- print(f"已启动同步,主控窗口: {self.master_window}, 同步窗口: {self.sync_windows}")
-
+
+ print(
+ f"已启动同步,主控窗口: {self.master_window}, 同步窗口: {self.sync_windows}"
+ )
+
# 添加:将所有窗口设置为置顶
for hwnd in self.sync_windows:
try:
# 设置窗口为置顶
- win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
- win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
+ win32gui.SetWindowPos(
+ hwnd,
+ win32con.HWND_TOPMOST,
+ 0,
+ 0,
+ 0,
+ 0,
+ win32con.SWP_NOMOVE | win32con.SWP_NOSIZE,
+ )
# 取消置顶(但保持在所有窗口前面)
- win32gui.SetWindowPos(hwnd, win32con.HWND_NOTOPMOST, 0, 0, 0, 0,
- win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
+ win32gui.SetWindowPos(
+ hwnd,
+ win32con.HWND_NOTOPMOST,
+ 0,
+ 0,
+ 0,
+ 0,
+ win32con.SWP_NOMOVE | win32con.SWP_NOSIZE,
+ )
except Exception as e:
print(f"设置窗口 {hwnd} 置顶失败: {str(e)}")
-
+
# 添加:将主窗口设置为活动窗口
try:
# 确保主窗口可见
@@ -1090,7 +1176,7 @@ def start_sync(self, selected_items):
print(f"已激活主窗口: {self.master_window}")
except Exception as e:
print(f"激活主窗口失败: {str(e)}")
-
+
except Exception as e:
self.stop_sync() # 确保清理资源
messagebox.showerror("错误", f"开启同步失败: {str(e)}")
@@ -1106,11 +1192,11 @@ def on_mouse_event(self, event):
try:
if self.is_sync:
current_window = win32gui.GetForegroundWindow()
-
+
# 检查是否是主控窗口或其插件窗口
is_master = current_window == self.master_window
master_popups = self.get_chrome_popups(self.master_window)
-
+
# 检查是否是主窗口的弹出窗口之一
is_popup = False
if not is_master and current_window in master_popups:
@@ -1118,59 +1204,70 @@ def on_mouse_event(self, event):
# 确保这个弹出窗口在我们的同步列表中
if current_window not in self.popup_windows:
self.popup_windows.append(current_window)
-
+
# 只有当当前窗口是主控窗口或其弹出窗口时才处理事件
# 这样可以防止其他窗口控制同步
if is_master or is_popup:
# 获取鼠标位置
x, y = mouse.get_position()
-
+
# 获取当前窗口的矩形区域
current_rect = win32gui.GetWindowRect(current_window)
-
+
# 检查鼠标是否在当前窗口范围内
mouse_in_window = (
- x >= current_rect[0] and x <= current_rect[2] and
- y >= current_rect[1] and y <= current_rect[3]
+ x >= current_rect[0]
+ and x <= current_rect[2]
+ and y >= current_rect[1]
+ and y <= current_rect[3]
)
-
+
# 只有当鼠标在窗口范围内时才进行同步
if not mouse_in_window:
return
-
+
# 对于移动事件进行优化
if isinstance(event, mouse.MoveEvent):
# 改进的移动事件节流策略
current_time = time.time()
- if not hasattr(self, 'move_interval'):
+ if not hasattr(self, "move_interval"):
self.move_interval = 0.01 # 10ms节流间隔
-
+
# 更精细的移动阈值控制
- if not hasattr(self, 'mouse_threshold'):
+ if not hasattr(self, "mouse_threshold"):
self.mouse_threshold = 2 # 像素移动阈值
-
+
# 时间节流:忽略过于频繁的移动事件
- if current_time - getattr(self, 'last_move_time', 0) < self.move_interval:
+ if (
+ current_time - getattr(self, "last_move_time", 0)
+ < self.move_interval
+ ):
return
-
+
# 距离节流:忽略过小的移动
- last_pos = getattr(self, 'last_mouse_position', (event.x, event.y))
+ last_pos = getattr(
+ self, "last_mouse_position", (event.x, event.y)
+ )
dx = abs(event.x - last_pos[0])
dy = abs(event.y - last_pos[1])
if dx < self.mouse_threshold and dy < self.mouse_threshold:
return
-
+
# 更新上次位置和时间
self.last_mouse_position = (event.x, event.y)
self.last_move_time = current_time
-
+
# 计算当前窗口的相对坐标
- rel_x = (x - current_rect[0]) / max((current_rect[2] - current_rect[0]), 1)
- rel_y = (y - current_rect[1]) / max((current_rect[3] - current_rect[1]), 1)
-
+ rel_x = (x - current_rect[0]) / max(
+ (current_rect[2] - current_rect[0]), 1
+ )
+ rel_y = (y - current_rect[1]) / max(
+ (current_rect[3] - current_rect[1]), 1
+ )
+
# 使用线程池批量处理事件分发
sync_tasks = []
-
+
# 同步到其他窗口
for hwnd in self.sync_windows:
try:
@@ -1182,152 +1279,313 @@ def on_mouse_event(self, event):
target_popups = self.get_chrome_popups(hwnd)
# 检查当前窗口是否为弹出类型的浮动窗口
- style = win32gui.GetWindowLong(current_window, win32con.GWL_STYLE)
+ style = win32gui.GetWindowLong(
+ current_window, win32con.GWL_STYLE
+ )
is_floating = (style & win32con.WS_POPUP) != 0
current_title = win32gui.GetWindowText(current_window)
-
+
if is_floating and target_popups:
# 按照相对位置和窗口标题匹配浮动窗口
best_match = None
- min_diff = float('inf')
- current_size = (current_rect[2] - current_rect[0], current_rect[3] - current_rect[1])
-
+ min_diff = float("inf")
+ current_size = (
+ current_rect[2] - current_rect[0],
+ current_rect[3] - current_rect[1],
+ )
+
for popup in target_popups:
# 获取目标弹出窗口信息
popup_rect = win32gui.GetWindowRect(popup)
- popup_style = win32gui.GetWindowLong(popup, win32con.GWL_STYLE)
+ popup_style = win32gui.GetWindowLong(
+ popup, win32con.GWL_STYLE
+ )
popup_title = win32gui.GetWindowText(popup)
-
+
# 检查是否也是浮动窗口
if (popup_style & win32con.WS_POPUP) == 0:
continue
-
+
# 计算窗口大小差异
- popup_size = (popup_rect[2] - popup_rect[0], popup_rect[3] - popup_rect[1])
- size_diff = abs(current_size[0] - popup_size[0]) + abs(current_size[1] - popup_size[1])
-
+ popup_size = (
+ popup_rect[2] - popup_rect[0],
+ popup_rect[3] - popup_rect[1],
+ )
+ size_diff = abs(
+ current_size[0] - popup_size[0]
+ ) + abs(current_size[1] - popup_size[1])
+
# 计算标题相似度
- title_sim = self.title_similarity(current_title, popup_title)
-
+ title_sim = self.title_similarity(
+ current_title, popup_title
+ )
+
# 综合评分
diff = size_diff * (2.0 - title_sim)
-
+
if diff < min_diff:
min_diff = diff
best_match = popup
-
+
target_hwnd = best_match if best_match else hwnd
else:
# 按照相对位置匹配
best_match = None
- min_diff = float('inf')
+ min_diff = float("inf")
for popup in target_popups:
popup_rect = win32gui.GetWindowRect(popup)
- master_rect = win32gui.GetWindowRect(current_window)
+ master_rect = win32gui.GetWindowRect(
+ current_window
+ )
# 计算相对位置差异
- master_rel_x = master_rect[0] - win32gui.GetWindowRect(self.master_window)[0]
- master_rel_y = master_rect[1] - win32gui.GetWindowRect(self.master_window)[1]
- popup_rel_x = popup_rect[0] - win32gui.GetWindowRect(hwnd)[0]
- popup_rel_y = popup_rect[1] - win32gui.GetWindowRect(hwnd)[1]
-
- diff = abs(master_rel_x - popup_rel_x) + abs(master_rel_y - popup_rel_y)
+ master_rel_x = (
+ master_rect[0]
+ - win32gui.GetWindowRect(
+ self.master_window
+ )[0]
+ )
+ master_rel_y = (
+ master_rect[1]
+ - win32gui.GetWindowRect(
+ self.master_window
+ )[1]
+ )
+ popup_rel_x = (
+ popup_rect[0]
+ - win32gui.GetWindowRect(hwnd)[0]
+ )
+ popup_rel_y = (
+ popup_rect[1]
+ - win32gui.GetWindowRect(hwnd)[1]
+ )
+
+ diff = abs(master_rel_x - popup_rel_x) + abs(
+ master_rel_y - popup_rel_y
+ )
if diff < min_diff:
min_diff = diff
best_match = popup
target_hwnd = best_match if best_match else hwnd
-
+
if not target_hwnd:
continue
-
+
# 获取目标窗口尺寸
target_rect = win32gui.GetWindowRect(target_hwnd)
-
+
# 计算目标坐标 - 保护除以零
client_x = int((target_rect[2] - target_rect[0]) * rel_x)
client_y = int((target_rect[3] - target_rect[1]) * rel_y)
lparam = win32api.MAKELONG(client_x, client_y)
-
+
# 使用PostMessage代替SendMessage提高性能
# 处理滚轮事件
if isinstance(event, mouse.WheelEvent):
try:
wheel_delta = int(event.delta)
- if keyboard.is_pressed('ctrl'):
- if wheel_delta > 0:
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, win32con.VK_CONTROL, 0)
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, 0xBB, 0) # VK_OEM_PLUS
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, 0xBB, 0)
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, win32con.VK_CONTROL, 0)
+ if keyboard.is_pressed("ctrl"):
+ if wheel_delta > 0:
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYDOWN,
+ win32con.VK_CONTROL,
+ 0,
+ )
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYDOWN,
+ 0xBB,
+ 0,
+ ) # VK_OEM_PLUS
+ win32gui.PostMessage(
+ target_hwnd, win32con.WM_KEYUP, 0xBB, 0
+ )
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYUP,
+ win32con.VK_CONTROL,
+ 0,
+ )
else:
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, win32con.VK_CONTROL, 0)
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, 0xBD, 0) # VK_OEM_MINUS
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, 0xBD, 0)
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, win32con.VK_CONTROL, 0)
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYDOWN,
+ win32con.VK_CONTROL,
+ 0,
+ )
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYDOWN,
+ 0xBD,
+ 0,
+ ) # VK_OEM_MINUS
+ win32gui.PostMessage(
+ target_hwnd, win32con.WM_KEYUP, 0xBD, 0
+ )
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYUP,
+ win32con.VK_CONTROL,
+ 0,
+ )
else:
# 获取滚轮方向和绝对值
abs_delta = abs(wheel_delta)
scroll_up = wheel_delta > 0
-
+
# 主要使用PageUp/PageDown键来实现更大的滚动幅度
# 对于小幅度滚动,使用箭头键;对于大幅度滚动,使用Page键
-
+
# 根据滚动大小决定策略,微调使同步窗口滚动幅度更接近主窗口
if abs_delta <= 1:
# 对于小幅度滚动,减少到2次箭头键
- vk_code = win32con.VK_UP if scroll_up else win32con.VK_DOWN
+ vk_code = (
+ win32con.VK_UP
+ if scroll_up
+ else win32con.VK_DOWN
+ )
for _ in range(2):
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, vk_code, 0)
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, vk_code, 0)
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYDOWN,
+ vk_code,
+ 0,
+ )
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYUP,
+ vk_code,
+ 0,
+ )
elif abs_delta <= 3:
# 对于中等幅度滚动,使用一次Page键但减少额外的箭头键
- page_vk = win32con.VK_PRIOR if scroll_up else win32con.VK_NEXT # Page Up/Down
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, page_vk, 0)
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, page_vk, 0)
-
+ page_vk = (
+ win32con.VK_PRIOR
+ if scroll_up
+ else win32con.VK_NEXT
+ ) # Page Up/Down
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYDOWN,
+ page_vk,
+ 0,
+ )
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYUP,
+ page_vk,
+ 0,
+ )
+
# 额外只增加1次箭头键,减少之前的额外按键
- vk_code = win32con.VK_UP if scroll_up else win32con.VK_DOWN
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, vk_code, 0)
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, vk_code, 0)
+ vk_code = (
+ win32con.VK_UP
+ if scroll_up
+ else win32con.VK_DOWN
+ )
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYDOWN,
+ vk_code,
+ 0,
+ )
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYUP,
+ vk_code,
+ 0,
+ )
else:
# 对于大幅度滚动,减少Page键系数
- page_count = min(int(abs_delta * 0.4), 2) # 系数从0.6降到0.4,最多减少到2次
- page_vk = win32con.VK_PRIOR if scroll_up else win32con.VK_NEXT
-
+ page_count = min(
+ int(abs_delta * 0.4), 2
+ ) # 系数从0.6降到0.4,最多减少到2次
+ page_vk = (
+ win32con.VK_PRIOR
+ if scroll_up
+ else win32con.VK_NEXT
+ )
+
for _ in range(page_count):
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, page_vk, 0)
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, page_vk, 0)
-
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYDOWN,
+ page_vk,
+ 0,
+ )
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYUP,
+ page_vk,
+ 0,
+ )
+
# 移除额外的箭头键调整
-
+
except Exception as e:
print(f"处理滚轮事件失败: {str(e)}")
continue
-
+
# 处理鼠标点击
elif isinstance(event, mouse.ButtonEvent):
if event.event_type == mouse.DOWN:
if event.button == mouse.LEFT:
- win32gui.PostMessage(target_hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lparam)
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_LBUTTONDOWN,
+ win32con.MK_LBUTTON,
+ lparam,
+ )
elif event.button == mouse.RIGHT:
- win32gui.PostMessage(target_hwnd, win32con.WM_RBUTTONDOWN, win32con.MK_RBUTTON, lparam)
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_RBUTTONDOWN,
+ win32con.MK_RBUTTON,
+ lparam,
+ )
elif event.button == mouse.MIDDLE: # 添加中键支持
- win32gui.PostMessage(target_hwnd, win32con.WM_MBUTTONDOWN, win32con.MK_MBUTTON, lparam)
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_MBUTTONDOWN,
+ win32con.MK_MBUTTON,
+ lparam,
+ )
elif event.event_type == mouse.UP:
if event.button == mouse.LEFT:
- win32gui.PostMessage(target_hwnd, win32con.WM_LBUTTONUP, 0, lparam)
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_LBUTTONUP,
+ 0,
+ lparam,
+ )
elif event.button == mouse.RIGHT:
- win32gui.PostMessage(target_hwnd, win32con.WM_RBUTTONUP, 0, lparam)
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_RBUTTONUP,
+ 0,
+ lparam,
+ )
elif event.button == mouse.MIDDLE: # 添加中键支持
- win32gui.PostMessage(target_hwnd, win32con.WM_MBUTTONUP, 0, lparam)
-
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_MBUTTONUP,
+ 0,
+ lparam,
+ )
+
# 处理鼠标移动 - 减少移动事件传递,仅对实质性移动做处理
elif isinstance(event, mouse.MoveEvent):
- win32gui.PostMessage(target_hwnd, win32con.WM_MOUSEMOVE, 0, lparam)
-
+ win32gui.PostMessage(
+ target_hwnd, win32con.WM_MOUSEMOVE, 0, lparam
+ )
+
except Exception as e:
error_msg = str(e)
# 减少错误日志输出频率
- if not hasattr(self, 'last_error_time') or time.time() - self.last_error_time > 5:
+ if (
+ not hasattr(self, "last_error_time")
+ or time.time() - self.last_error_time > 5
+ ):
print(f"同步到窗口 {hwnd} 失败: {error_msg}")
self.last_error_time = time.time()
except Exception as e:
@@ -1338,11 +1596,11 @@ def on_keyboard_event(self, event):
try:
if self.is_sync:
current_window = win32gui.GetForegroundWindow()
-
+
# 检查是否是主控窗口或其插件窗口
is_master = current_window == self.master_window
master_popups = self.get_chrome_popups(self.master_window)
-
+
# 检查是否是主窗口的弹出窗口之一
is_popup = False
if not is_master and current_window in master_popups:
@@ -1350,41 +1608,50 @@ def on_keyboard_event(self, event):
# 确保这个弹出窗口在我们的同步列表中
if current_window not in self.popup_windows:
self.popup_windows.append(current_window)
-
+
# 只有当当前窗口是主控窗口或其弹出窗口时才处理事件
# 这样可以防止其他窗口控制同步
if is_master or is_popup:
# 获取鼠标位置
x, y = mouse.get_position()
-
+
# 获取当前窗口的矩形区域
current_rect = win32gui.GetWindowRect(current_window)
-
+
# 检查鼠标是否在当前窗口范围内
mouse_in_window = (
- x >= current_rect[0] and x <= current_rect[2] and
- y >= current_rect[1] and y <= current_rect[3]
+ x >= current_rect[0]
+ and x <= current_rect[2]
+ and y >= current_rect[1]
+ and y <= current_rect[3]
)
-
+
# 只有当鼠标在窗口范围内时才进行同步
if not mouse_in_window:
return
-
+
# 获取实际的输入目标窗口
input_hwnd = win32gui.GetFocus()
-
+
# 同步到其他窗口 - 键盘事件限流
current_time = time.time()
- if not hasattr(self, 'last_key_time') or current_time - self.last_key_time > 0.01:
+ if (
+ not hasattr(self, "last_key_time")
+ or current_time - self.last_key_time > 0.01
+ ):
self.last_key_time = current_time
else:
# 对于连续的相同按键,适当限流,减少重复输入
- if hasattr(self, 'last_key') and self.last_key == event.name and event.event_type == keyboard.KEY_DOWN:
+ if (
+ hasattr(self, "last_key")
+ and self.last_key == event.name
+ and event.event_type == keyboard.KEY_DOWN
+ ):
return
-
+
# 记录最后一个按键
self.last_key = event.name
-
+
# 同步到其他窗口
for hwnd in self.sync_windows:
try:
@@ -1396,17 +1663,29 @@ def on_keyboard_event(self, event):
target_popups = self.get_chrome_popups(hwnd)
# 按照相对位置匹配
best_match = None
- min_diff = float('inf')
+ min_diff = float("inf")
for popup in target_popups:
popup_rect = win32gui.GetWindowRect(popup)
master_rect = win32gui.GetWindowRect(current_window)
# 计算相对位置差异
- master_rel_x = master_rect[0] - win32gui.GetWindowRect(self.master_window)[0]
- master_rel_y = master_rect[1] - win32gui.GetWindowRect(self.master_window)[1]
- popup_rel_x = popup_rect[0] - win32gui.GetWindowRect(hwnd)[0]
- popup_rel_y = popup_rect[1] - win32gui.GetWindowRect(hwnd)[1]
-
- diff = abs(master_rel_x - popup_rel_x) + abs(master_rel_y - popup_rel_y)
+ master_rel_x = (
+ master_rect[0]
+ - win32gui.GetWindowRect(self.master_window)[0]
+ )
+ master_rel_y = (
+ master_rect[1]
+ - win32gui.GetWindowRect(self.master_window)[1]
+ )
+ popup_rel_x = (
+ popup_rect[0] - win32gui.GetWindowRect(hwnd)[0]
+ )
+ popup_rel_y = (
+ popup_rect[1] - win32gui.GetWindowRect(hwnd)[1]
+ )
+
+ diff = abs(master_rel_x - popup_rel_x) + abs(
+ master_rel_y - popup_rel_y
+ )
if diff < min_diff:
min_diff = diff
best_match = popup
@@ -1414,82 +1693,142 @@ def on_keyboard_event(self, event):
if not target_hwnd:
continue
-
+
# 检测组合键状态
modifiers = 0
modifier_keys = {
- 'ctrl': {'pressed': keyboard.is_pressed('ctrl'), 'vk': win32con.VK_CONTROL, 'flag': win32con.MOD_CONTROL},
- 'alt': {'pressed': keyboard.is_pressed('alt'), 'vk': win32con.VK_MENU, 'flag': win32con.MOD_ALT},
- 'shift': {'pressed': keyboard.is_pressed('shift'), 'vk': win32con.VK_SHIFT, 'flag': win32con.MOD_SHIFT}
+ "ctrl": {
+ "pressed": keyboard.is_pressed("ctrl"),
+ "vk": win32con.VK_CONTROL,
+ "flag": win32con.MOD_CONTROL,
+ },
+ "alt": {
+ "pressed": keyboard.is_pressed("alt"),
+ "vk": win32con.VK_MENU,
+ "flag": win32con.MOD_ALT,
+ },
+ "shift": {
+ "pressed": keyboard.is_pressed("shift"),
+ "vk": win32con.VK_SHIFT,
+ "flag": win32con.MOD_SHIFT,
+ },
}
# 处理修饰键和组合键
for mod_name, mod_info in modifier_keys.items():
- if mod_info['pressed']:
+ if mod_info["pressed"]:
# 按下修饰键
if event.event_type == keyboard.KEY_DOWN:
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, mod_info['vk'], 0)
-
- modifiers |= mod_info['flag']
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYDOWN,
+ mod_info["vk"],
+ 0,
+ )
+
+ modifiers |= mod_info["flag"]
# 处理 Ctrl+组合键的特殊情况
- if modifier_keys['ctrl']['pressed'] and event.name in ['a', 'c', 'v', 'x', 'z']:
+ if modifier_keys["ctrl"]["pressed"] and event.name in [
+ "a",
+ "c",
+ "v",
+ "x",
+ "z",
+ ]:
vk_code = ord(event.name.upper())
if event.event_type == keyboard.KEY_DOWN:
# 发送组合键序列
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, vk_code, 0)
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, vk_code, 0)
-
+ win32gui.PostMessage(
+ target_hwnd, win32con.WM_KEYDOWN, vk_code, 0
+ )
+ win32gui.PostMessage(
+ target_hwnd, win32con.WM_KEYUP, vk_code, 0
+ )
+
# 对于这些特殊组合键,直接处理完毕
continue
-
+
# 处理普通按键
- if event.name in ['enter', 'backspace', 'tab', 'esc', 'space',
- 'up', 'down', 'left', 'right',
- 'home', 'end', 'page up', 'page down', 'delete',
- 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12']:
+ if event.name in [
+ "enter",
+ "backspace",
+ "tab",
+ "esc",
+ "space",
+ "up",
+ "down",
+ "left",
+ "right",
+ "home",
+ "end",
+ "page up",
+ "page down",
+ "delete",
+ "f1",
+ "f2",
+ "f3",
+ "f4",
+ "f5",
+ "f6",
+ "f7",
+ "f8",
+ "f9",
+ "f10",
+ "f11",
+ "f12",
+ ]:
vk_map = {
- 'enter': win32con.VK_RETURN,
- 'backspace': win32con.VK_BACK,
- 'tab': win32con.VK_TAB,
- 'esc': win32con.VK_ESCAPE,
- 'space': win32con.VK_SPACE,
- 'up': win32con.VK_UP,
- 'down': win32con.VK_DOWN,
- 'left': win32con.VK_LEFT,
- 'right': win32con.VK_RIGHT,
- 'home': win32con.VK_HOME,
- 'end': win32con.VK_END,
- 'page up': win32con.VK_PRIOR,
- 'page down': win32con.VK_NEXT,
- 'delete': win32con.VK_DELETE,
- 'f1': win32con.VK_F1,
- 'f2': win32con.VK_F2,
- 'f3': win32con.VK_F3,
- 'f4': win32con.VK_F4,
- 'f5': win32con.VK_F5,
- 'f6': win32con.VK_F6,
- 'f7': win32con.VK_F7,
- 'f8': win32con.VK_F8,
- 'f9': win32con.VK_F9,
- 'f10': win32con.VK_F10,
- 'f11': win32con.VK_F11,
- 'f12': win32con.VK_F12
+ "enter": win32con.VK_RETURN,
+ "backspace": win32con.VK_BACK,
+ "tab": win32con.VK_TAB,
+ "esc": win32con.VK_ESCAPE,
+ "space": win32con.VK_SPACE,
+ "up": win32con.VK_UP,
+ "down": win32con.VK_DOWN,
+ "left": win32con.VK_LEFT,
+ "right": win32con.VK_RIGHT,
+ "home": win32con.VK_HOME,
+ "end": win32con.VK_END,
+ "page up": win32con.VK_PRIOR,
+ "page down": win32con.VK_NEXT,
+ "delete": win32con.VK_DELETE,
+ "f1": win32con.VK_F1,
+ "f2": win32con.VK_F2,
+ "f3": win32con.VK_F3,
+ "f4": win32con.VK_F4,
+ "f5": win32con.VK_F5,
+ "f6": win32con.VK_F6,
+ "f7": win32con.VK_F7,
+ "f8": win32con.VK_F8,
+ "f9": win32con.VK_F9,
+ "f10": win32con.VK_F10,
+ "f11": win32con.VK_F11,
+ "f12": win32con.VK_F12,
}
vk_code = vk_map[event.name]
-
+
# 发送按键消息
if event.event_type == keyboard.KEY_DOWN:
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, vk_code, 0)
+ win32gui.PostMessage(
+ target_hwnd, win32con.WM_KEYDOWN, vk_code, 0
+ )
else:
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, vk_code, 0)
+ win32gui.PostMessage(
+ target_hwnd, win32con.WM_KEYUP, vk_code, 0
+ )
else:
# 处理普通字符
if len(event.name) == 1:
vk_code = win32api.VkKeyScan(event.name[0]) & 0xFF
if event.event_type == keyboard.KEY_DOWN:
# 直接发送字符消息,更有效
- win32gui.PostMessage(target_hwnd, win32con.WM_CHAR, ord(event.name[0]), 0)
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_CHAR,
+ ord(event.name[0]),
+ 0,
+ )
continue
else:
continue
@@ -1497,18 +1836,29 @@ def on_keyboard_event(self, event):
# 释放修饰键 - 仅在按键弹起时释放
if event.event_type == keyboard.KEY_UP:
for mod_name, mod_info in modifier_keys.items():
- if mod_info['pressed']:
- win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, mod_info['vk'], 0)
-
+ if mod_info["pressed"]:
+ win32gui.PostMessage(
+ target_hwnd,
+ win32con.WM_KEYUP,
+ mod_info["vk"],
+ 0,
+ )
+
except Exception as e:
# 限制错误日志输出频率
- if not hasattr(self, 'last_key_error_time') or time.time() - self.last_key_error_time > 5:
+ if (
+ not hasattr(self, "last_key_error_time")
+ or time.time() - self.last_key_error_time > 5
+ ):
print(f"同步键盘事件到窗口 {hwnd} 失败: {str(e)}")
self.last_key_error_time = time.time()
-
+
except Exception as e:
# 限制错误日志输出频率
- if not hasattr(self, 'last_keyboard_error_time') or time.time() - self.last_keyboard_error_time > 5:
+ if (
+ not hasattr(self, "last_keyboard_error_time")
+ or time.time() - self.last_keyboard_error_time > 5
+ ):
print(f"处理键盘事件失败: {str(e)}")
self.last_keyboard_error_time = time.time()
@@ -1517,21 +1867,21 @@ def stop_sync(self):
try:
# 标记同步状态为False
self.is_sync = False
-
+
# 卸载低级滚轮钩子
self.unhook_wheel()
-
+
# 保存当前快捷键设置,用于后续恢复
current_shortcut = None
- if hasattr(self, 'current_shortcut'):
+ if hasattr(self, "current_shortcut"):
current_shortcut = self.current_shortcut
-
+
# 保存当前快捷键钩子
shortcut_hook = None
- if hasattr(self, 'shortcut_hook'):
+ if hasattr(self, "shortcut_hook"):
shortcut_hook = self.shortcut_hook
self.shortcut_hook = None # 临时清除引用,避免被unhook_all移除
-
+
# 移除同步相关的键盘钩子,但保留快捷键钩子
try:
# 不使用 keyboard.unhook_all(),而是有选择地移除
@@ -1540,44 +1890,46 @@ def stop_sync(self):
print("已移除同步相关的键盘钩子")
except Exception as e:
print(f"移除键盘钩子失败: {str(e)}")
-
+
# 移除鼠标钩子
try:
mouse.unhook_all()
print("已移除鼠标钩子")
except Exception as e:
print(f"移除鼠标钩子失败: {str(e)}")
-
+
# 等待线程结束
- if hasattr(self, 'hook_thread') and self.hook_thread:
+ if hasattr(self, "hook_thread") and self.hook_thread:
try:
if self.hook_thread.is_alive():
self.hook_thread.join(timeout=0.5)
except Exception as e:
print(f"等待消息循环线程结束失败: {str(e)}")
self.hook_thread = None
-
+
# 等待弹出窗口监控线程结束
- if hasattr(self, 'popup_monitor_thread') and self.popup_monitor_thread:
+ if hasattr(self, "popup_monitor_thread") and self.popup_monitor_thread:
try:
if self.popup_monitor_thread.is_alive():
self.popup_monitor_thread.join(timeout=0.5)
except Exception as e:
print(f"等待弹出窗口监控线程结束失败: {str(e)}")
self.popup_monitor_thread = None
-
+
# 重置关键数据结构
self.popup_windows = []
self.sync_popups = {}
self.sync_windows = []
-
+
# 更新按钮状态 - 需要检查按钮是否存在
- if hasattr(self, 'sync_button') and self.sync_button:
+ if hasattr(self, "sync_button") and self.sync_button:
try:
- self.sync_button.configure(text="▶ 开始同步", style='Accent.TButton')
+ self.sync_button.configure(
+ text="▶ 开始同步", style="Accent.TButton"
+ )
except Exception as e:
print(f"更新按钮状态失败: {str(e)}")
-
+
# 重新设置快捷键 - 确保快捷键在停止同步后仍然有效
if current_shortcut:
try:
@@ -1585,17 +1937,19 @@ def stop_sync(self):
print(f"已恢复快捷键设置: {current_shortcut}")
except Exception as e:
print(f"恢复快捷键失败: {str(e)}")
-
+
# 提示用户
print("同步已停止")
-
+
except Exception as e:
print(f"停止同步出错: {str(e)}")
traceback.print_exc()
# 确保按钮恢复正常状态
try:
- if hasattr(self, 'sync_button') and self.sync_button:
- self.sync_button.configure(text="▶ 开始同步", style='Accent.TButton')
+ if hasattr(self, "sync_button") and self.sync_button:
+ self.sync_button.configure(
+ text="▶ 开始同步", style="Accent.TButton"
+ )
except:
pass
@@ -1603,29 +1957,31 @@ def on_closing(self):
# 窗口关闭事件
try:
# 停止同步
- if hasattr(self, 'is_sync') and self.is_sync:
+ if hasattr(self, "is_sync") and self.is_sync:
self.stop_sync()
-
+
# 保存设置
self.save_settings()
# 保存窗口位置
self.save_window_position()
-
+
# 移除系统托盘图标
- if not self.is_win11 and hasattr(self, 'notify_id'):
+ if not self.is_win11 and hasattr(self, "notify_id"):
try:
win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, self.notify_id)
print("已移除系统托盘图标")
except Exception as e:
print(f"移除系统托盘图标失败: {str(e)}")
-
+
# 关闭所有Chrome窗口
- if hasattr(self, 'close_all_windows') and messagebox.askyesno("确认", "关闭所有Chrome窗口?"):
+ if hasattr(self, "close_all_windows") and messagebox.askyesno(
+ "确认", "关闭所有Chrome窗口?"
+ ):
self.close_all_windows()
-
+
# 销毁主窗口
self.root.destroy()
-
+
except Exception as e:
print(f"关闭程序时出错: {str(e)}")
self.root.destroy()
@@ -1638,23 +1994,23 @@ def auto_arrange_windows(self):
was_syncing = self.is_syncing
if was_syncing:
self.stop_sync()
-
+
# 获取选中的窗口并按编号排序
selected = []
for item in self.window_list.get_children():
if self.window_list.set(item, "select") == "√":
- values = self.window_list.item(item)['values']
+ values = self.window_list.item(item)["values"]
if values and len(values) >= 5:
- number = int(values[1])
+ number = int(values[1])
hwnd = int(values[4])
selected.append((number, hwnd, item))
-
+
if not selected:
messagebox.showinfo("提示", "请先选择要排列的窗口!")
return
-
+
print(f"选中了 {len(selected)} 个窗口")
-
+
# 按编号升序排序
selected.sort(key=lambda x: x[0])
print("窗口排序结果:")
@@ -1664,43 +2020,43 @@ def auto_arrange_windows(self):
# 获取选中的屏幕信息
screen_selection = self.screen_selection
print(f"当前选择的屏幕: {screen_selection}")
-
+
# 更新屏幕列表
screen_names = self.update_screen_list()
-
+
# 找到选中的屏幕索引
screen_index = 0 # 默认使用第一个屏幕
for i, name in enumerate(screen_names):
if name == screen_selection:
screen_index = i
break
-
+
if screen_index >= len(self.screens):
messagebox.showerror("错误", "请选择有效的屏幕!")
- return
-
+ return
+
# 获取屏幕尺寸
screen = self.screens[screen_index]
- screen_rect = screen['work_rect'] # 使用工作区而不是完整显示区
+ screen_rect = screen["work_rect"] # 使用工作区而不是完整显示区
print(f"屏幕工作区: {screen_rect}")
# 计算屏幕尺寸
screen_width = screen_rect[2] - screen_rect[0]
screen_height = screen_rect[3] - screen_rect[1]
print(f"屏幕尺寸: {screen_width}x{screen_height}")
-
+
# 计算最佳布局
count = len(selected)
cols = int(math.sqrt(count))
if cols * cols < count:
cols += 1
rows = (count + cols - 1) // cols
-
+
# 计算窗口大小
width = screen_width // cols
height = screen_height // rows
print(f"窗口布局: {rows}行 x {cols}列, 窗口大小: {width}x{height}")
-
+
# 创建位置映射(从左到右,从上到下)
positions = []
for i in range(count):
@@ -1710,55 +2066,69 @@ def auto_arrange_windows(self):
y = screen_rect[1] + row * height
positions.append((x, y))
print(f"位置 {i}: ({x}, {y})")
-
+
# 应用窗口位置
for i, (number, hwnd, _) in enumerate(selected):
try:
x, y = positions[i]
print(f"移动窗口 {number} (句柄: {hwnd}) 到位置 ({x}, {y})")
-
+
# 确保窗口可见并移动到指定位置
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
-
+
# 先设置窗口样式确保可以移动
style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
style |= win32con.WS_SIZEBOX | win32con.WS_SYSMENU
win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE, style)
-
+
# 移动窗口
win32gui.MoveWindow(hwnd, x, y, width, height, True)
-
+
# 强制重绘
win32gui.UpdateWindow(hwnd)
print(f"窗口 {number} 移动成功")
-
+
except Exception as e:
print(f"移动窗口 {number} (句柄: {hwnd}) 失败: {str(e)}")
continue
-
+
print("窗口排列完成")
-
+
# 添加:将所有排列的窗口置顶
for _, hwnd, _ in selected:
try:
# 设置窗口为置顶
- win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
- win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
+ win32gui.SetWindowPos(
+ hwnd,
+ win32con.HWND_TOPMOST,
+ 0,
+ 0,
+ 0,
+ 0,
+ win32con.SWP_NOMOVE | win32con.SWP_NOSIZE,
+ )
# 取消置顶(但保持在所有窗口前面)
- win32gui.SetWindowPos(hwnd, win32con.HWND_NOTOPMOST, 0, 0, 0, 0,
- win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
+ win32gui.SetWindowPos(
+ hwnd,
+ win32con.HWND_NOTOPMOST,
+ 0,
+ 0,
+ 0,
+ 0,
+ win32con.SWP_NOMOVE | win32con.SWP_NOSIZE,
+ )
except Exception as e:
print(f"设置窗口 {hwnd} 置顶失败: {str(e)}")
-
+
# 找到主窗口并激活
master_hwnd = None
for item in self.window_list.get_children():
if self.window_list.set(item, "master") == "√":
- values = self.window_list.item(item)['values']
+ values = self.window_list.item(item)["values"]
if values and len(values) >= 5:
master_hwnd = int(values[4])
break
-
+
# 如果找到主窗口,将其设为活动窗口
if master_hwnd:
try:
@@ -1769,11 +2139,11 @@ def auto_arrange_windows(self):
print(f"已激活主窗口: {master_hwnd}")
except Exception as e:
print(f"激活主窗口失败: {str(e)}")
-
+
# 如果之前在同步,重新开启同步
if was_syncing:
self.start_sync([item for _, _, item in selected])
-
+
except Exception as e:
print(f"自动排列失败: {str(e)}")
messagebox.showerror("错误", f"自动排列失败: {str(e)}")
@@ -1785,19 +2155,19 @@ def custom_arrange_windows(self):
was_syncing = self.is_syncing
if was_syncing:
self.stop_sync()
-
+
selected = []
for item in self.window_list.get_children():
if self.window_list.set(item, "select") == "√":
- values = self.window_list.item(item)['values']
+ values = self.window_list.item(item)["values"]
if values and len(values) >= 5:
hwnd = int(values[4])
selected.append((item, hwnd))
-
+
if not selected:
messagebox.showinfo("提示", "请选择要排列的窗口!")
return
-
+
try:
# 获取参数
start_x = int(self.start_x.get())
@@ -1807,48 +2177,62 @@ def custom_arrange_windows(self):
h_spacing = int(self.h_spacing.get())
v_spacing = int(self.v_spacing.get())
windows_per_row = int(self.windows_per_row.get())
-
+
# 排列窗口
for i, (item, hwnd) in enumerate(selected):
row = i // windows_per_row
col = i % windows_per_row
-
+
x = start_x + col * (width + h_spacing)
y = start_y + row * (height + v_spacing)
-
+
# 确保窗口可见并移动到指定位置
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
win32gui.MoveWindow(hwnd, x, y, width, height, True)
-
+
# 保存参数
self.save_settings()
-
+
except ValueError:
messagebox.showerror("错误", "请输入有效的数字参数!")
except Exception as e:
messagebox.showerror("错误", f"排列窗口失败: {str(e)}")
-
+
# 添加:将所有排列的窗口置顶
for _, hwnd in selected:
try:
# 设置窗口为置顶
- win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
- win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
+ win32gui.SetWindowPos(
+ hwnd,
+ win32con.HWND_TOPMOST,
+ 0,
+ 0,
+ 0,
+ 0,
+ win32con.SWP_NOMOVE | win32con.SWP_NOSIZE,
+ )
# 取消置顶(但保持在所有窗口前面)
- win32gui.SetWindowPos(hwnd, win32con.HWND_NOTOPMOST, 0, 0, 0, 0,
- win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
+ win32gui.SetWindowPos(
+ hwnd,
+ win32con.HWND_NOTOPMOST,
+ 0,
+ 0,
+ 0,
+ 0,
+ win32con.SWP_NOMOVE | win32con.SWP_NOSIZE,
+ )
except Exception as e:
print(f"设置窗口 {hwnd} 置顶失败: {str(e)}")
-
+
# 找到主窗口并激活
master_hwnd = None
for item in self.window_list.get_children():
if self.window_list.set(item, "master") == "√":
- values = self.window_list.item(item)['values']
+ values = self.window_list.item(item)["values"]
if values and len(values) >= 5:
master_hwnd = int(values[4])
break
-
+
# 如果找到主窗口,将其设为活动窗口
if master_hwnd:
try:
@@ -1859,11 +2243,11 @@ def custom_arrange_windows(self):
print(f"已激活主窗口: {master_hwnd}")
except Exception as e:
print(f"激活主窗口失败: {str(e)}")
-
+
# 添加:如果之前在同步,重新开启同步
if was_syncing:
self.start_sync([item for item, _ in selected])
-
+
except Exception as e:
messagebox.showerror("错误", f"排列窗口失败: {str(e)}")
@@ -1871,37 +2255,37 @@ def load_settings(self) -> dict:
# 加载设置
settings = {}
try:
- if os.path.exists('settings.json'):
- with open('settings.json', 'r', encoding='utf-8') as f:
+ if os.path.exists("settings.json"):
+ with open("settings.json", "r", encoding="utf-8") as f:
settings = json.load(f)
-
+
# 加载是否显示Chrome提示的设置
- if 'show_chrome_tip' in settings:
- self.show_chrome_tip = settings['show_chrome_tip']
+ if "show_chrome_tip" in settings:
+ self.show_chrome_tip = settings["show_chrome_tip"]
except Exception as e:
print(f"加载设置失败: {str(e)}")
-
+
return settings
def save_settings(self):
# 保存设置
try:
# 确保信息是最新的
- self.settings['shortcut_path'] = self.shortcut_path
- self.settings['cache_dir'] = self.cache_dir
- self.settings['icon_dir'] = self.icon_dir
- if hasattr(self, 'current_shortcut') and self.current_shortcut:
- self.settings['sync_shortcut'] = self.current_shortcut
- if hasattr(self, 'screen_selection'):
- self.settings['screen_selection'] = self.screen_selection
-
+ self.settings["shortcut_path"] = self.shortcut_path
+ self.settings["cache_dir"] = self.cache_dir
+ self.settings["icon_dir"] = self.icon_dir
+ if hasattr(self, "current_shortcut") and self.current_shortcut:
+ self.settings["sync_shortcut"] = self.current_shortcut
+ if hasattr(self, "screen_selection"):
+ self.settings["screen_selection"] = self.screen_selection
+
# 保存是否显示Chrome提示的设置
- self.settings['show_chrome_tip'] = self.show_chrome_tip
-
+ self.settings["show_chrome_tip"] = self.show_chrome_tip
+
# 保存排列参数
self.settings.update(self.get_arrange_params())
-
- with open('settings.json', 'w', encoding='utf-8') as f:
+
+ with open("settings.json", "w", encoding="utf-8") as f:
json.dump(self.settings, f, ensure_ascii=False, indent=4)
print(f"保存设置成功,包括 show_chrome_tip = {self.show_chrome_tip}")
except Exception as e:
@@ -1909,48 +2293,48 @@ def save_settings(self):
def get_arrange_params(self):
return {
- 'start_x': self.start_x.get(),
- 'start_y': self.start_y.get(),
- 'window_width': self.window_width.get(),
- 'window_height': self.window_height.get(),
- 'h_spacing': self.h_spacing.get(),
- 'v_spacing': self.v_spacing.get(),
- 'windows_per_row': self.windows_per_row.get()
+ "start_x": self.start_x.get(),
+ "start_y": self.start_y.get(),
+ "window_width": self.window_width.get(),
+ "window_height": self.window_height.get(),
+ "h_spacing": self.h_spacing.get(),
+ "v_spacing": self.v_spacing.get(),
+ "windows_per_row": self.windows_per_row.get(),
}
def load_arrange_params(self):
# 加载排列参数
settings = self.load_settings()
- if 'arrange_params' in settings:
- params = settings['arrange_params']
+ if "arrange_params" in settings:
+ params = settings["arrange_params"]
self.start_x.delete(0, tk.END)
- self.start_x.insert(0, params.get('start_x', '0'))
+ self.start_x.insert(0, params.get("start_x", "0"))
self.start_y.delete(0, tk.END)
- self.start_y.insert(0, params.get('start_y', '0'))
+ self.start_y.insert(0, params.get("start_y", "0"))
self.window_width.delete(0, tk.END)
- self.window_width.insert(0, params.get('window_width', '500'))
+ self.window_width.insert(0, params.get("window_width", "500"))
self.window_height.delete(0, tk.END)
- self.window_height.insert(0, params.get('window_height', '400'))
+ self.window_height.insert(0, params.get("window_height", "400"))
self.h_spacing.delete(0, tk.END)
- self.h_spacing.insert(0, params.get('h_spacing', '0'))
+ self.h_spacing.insert(0, params.get("h_spacing", "0"))
self.v_spacing.delete(0, tk.END)
- self.v_spacing.insert(0, params.get('v_spacing', '0'))
+ self.v_spacing.insert(0, params.get("v_spacing", "0"))
self.windows_per_row.delete(0, tk.END)
- self.windows_per_row.insert(0, params.get('windows_per_row', '5'))
+ self.windows_per_row.insert(0, params.get("windows_per_row", "5"))
def parse_window_numbers(self, numbers_str: str) -> List[int]:
# 解析窗口编号字符串
if not numbers_str.strip():
return list(range(1, 49)) # 如果为空,返回所有编号
-
+
result = []
# 分割逗号分隔的部分
- parts = numbers_str.split(',')
+ parts = numbers_str.split(",")
for part in parts:
part = part.strip()
- if '-' in part:
+ if "-" in part:
# 处理范围,如 "1-5"
- start, end = map(int, part.split('-'))
+ start, end = map(int, part.split("-"))
result.extend(range(start, end + 1))
else:
# 处理单个数字
@@ -1961,48 +2345,48 @@ def open_windows(self):
"""打开Chrome窗口,依次打开但速度更快"""
# 获取快捷方式目录
shortcut_dir = self.shortcut_path
-
+
if not shortcut_dir:
messagebox.showinfo("提示", "请先在设置中设置快捷方式目录!")
return
-
+
if not os.path.exists(shortcut_dir):
messagebox.showerror("错误", "快捷方式目录不存在!")
return
-
+
# 获取用户设置的路径
abs_path = os.path.abspath(os.path.normpath(shortcut_dir))
if not os.path.isdir(abs_path):
messagebox.showerror("路径错误", "指定的路径不是一个有效目录")
return
-
+
# 快速验证路径可访问性
if not os.access(abs_path, os.R_OK):
messagebox.showerror("权限不足", "程序没有该目录的读取权限")
return
-
+
# 打开窗口逻辑
numbers = self.numbers_entry.get()
-
+
if not numbers:
messagebox.showwarning("警告", "请输入窗口编号!")
return
-
+
try:
window_numbers = self.parse_window_numbers(numbers)
-
+
# 清空现有调试端口映射
self.debug_ports.clear()
-
+
# 临时文件列表,用于最后清理
temp_files = []
-
+
for num in window_numbers:
shortcut = os.path.join(abs_path, f"{num}.lnk")
if not os.path.exists(shortcut):
print(f"警告: 快捷方式不存在: {shortcut}")
continue
-
+
# 如果启用了CDP,添加远程调试参数
if self.enable_cdp:
# 获取快捷方式信息
@@ -2010,21 +2394,25 @@ def open_windows(self):
target = shortcut_obj.TargetPath
args = shortcut_obj.Arguments
working_dir = shortcut_obj.WorkingDirectory
-
+
# 为每个窗口分配一个唯一的调试端口
debug_port = 9222 + int(num)
-
+
# 将窗口号和调试端口的映射保存到字典中
self.debug_ports[num] = debug_port
-
+
# 设置调试端口参数
if "--remote-debugging-port=" in args:
# 替换已有的调试端口参数
- new_args = re.sub(r'--remote-debugging-port=\d+', f'--remote-debugging-port={debug_port}', args)
+ new_args = re.sub(
+ r"--remote-debugging-port=\d+",
+ f"--remote-debugging-port={debug_port}",
+ args,
+ )
else:
# 添加新的调试端口参数
new_args = f"{args} --remote-debugging-port={debug_port}"
-
+
# 创建临时快捷方式
temp_shortcut = os.path.join(abs_path, f"temp_{num}.lnk")
temp_obj = self.shell.CreateShortCut(temp_shortcut)
@@ -2033,10 +2421,10 @@ def open_windows(self):
temp_obj.WorkingDirectory = working_dir
temp_obj.IconLocation = shortcut_obj.IconLocation
temp_obj.Save()
-
+
# 记录临时文件
temp_files.append(temp_shortcut)
-
+
# 确保临时文件创建成功
if os.path.exists(temp_shortcut):
# 启动临时快捷方式
@@ -2049,7 +2437,9 @@ def open_windows(self):
print(f"启动窗口 {num} 失败: {str(e)}")
else:
# 如果临时文件创建失败,尝试直接启动原始快捷方式
- print(f"警告: 临时快捷方式创建失败,直接启动原始快捷方式: {shortcut}")
+ print(
+ f"警告: 临时快捷方式创建失败,直接启动原始快捷方式: {shortcut}"
+ )
try:
subprocess.Popen(["start", "", shortcut], shell=True)
time.sleep(0.1)
@@ -2059,7 +2449,7 @@ def open_windows(self):
# 不启用CDP,直接打开
subprocess.Popen(["start", "", shortcut], shell=True)
time.sleep(0.05) # 只等待50毫秒
-
+
# 在所有窗口启动后,在后台清理临时文件
def cleanup_temp_files():
# 等待一小段时间再清理,确保所有窗口都已经启动
@@ -2070,31 +2460,31 @@ def cleanup_temp_files():
os.remove(temp_file)
except:
pass # 忽略删除失败
-
+
# 启动清理线程,不阻塞主线程
cleanup_thread = threading.Thread(target=cleanup_temp_files)
cleanup_thread.daemon = True # 设为守护线程,程序退出时自动结束
cleanup_thread.start()
-
+
# 调试输出当前所有的端口映射,方便排查
print("窗口号到调试端口的映射:")
for window_num, port in self.debug_ports.items():
print(f"窗口 {window_num} -> 端口 {port}")
-
+
# 保存当前使用的窗口编号到设置
try:
# 重新加载设置,确保获取最新的设置
settings = self.load_settings()
- settings['last_window_numbers'] = numbers
+ settings["last_window_numbers"] = numbers
self.settings = settings # 更新当前实例中的设置
-
+
# 保存设置到文件
- with open('settings.json', 'w', encoding='utf-8') as f:
+ with open("settings.json", "w", encoding="utf-8") as f:
json.dump(settings, f, ensure_ascii=False, indent=4)
print(f"成功保存窗口编号: {numbers}")
except Exception as e:
print(f"保存窗口编号设置失败: {str(e)}")
-
+
except Exception as e:
messagebox.showerror("错误", f"打开窗口失败: {str(e)}")
@@ -2107,21 +2497,21 @@ def get_shortcut_number(self, shortcut_path):
name_without_ext = os.path.splitext(file_name)[0]
if name_without_ext.isdigit():
return name_without_ext
-
+
# 如果文件名不是纯数字,则尝试从参数中提取数据目录
shortcut = self.shell.CreateShortCut(shortcut_path)
cmd_line = shortcut.Arguments
-
- if '--user-data-dir=' in cmd_line:
- data_dir = cmd_line.split('--user-data-dir=')[1].strip('"')
+
+ if "--user-data-dir=" in cmd_line:
+ data_dir = cmd_line.split("--user-data-dir=")[1].strip('"')
# 注意:这里不再假设数据目录名就是数字
# 但为了向后兼容性,我们仍然检查是否为数字
base_name = os.path.basename(data_dir)
if base_name.isdigit():
return base_name
-
+
return None
-
+
except Exception as e:
print(f"获取快捷方式编号失败: {str(e)}")
return None
@@ -2133,7 +2523,7 @@ def import_windows(self):
# 清空列表
for item in self.window_list.get_children():
self.window_list.delete(item)
-
+
# 创建进度对话框
progress_dialog = tk.Toplevel(self.root)
progress_dialog.title("加载窗口") # 修改标题
@@ -2141,138 +2531,186 @@ def import_windows(self):
progress_dialog.resizable(False, False)
progress_dialog.transient(self.root) # 设置为主窗口的临时窗口
progress_dialog.grab_set() # 模态对话框
-
+
# 设置图标
try:
if os.path.exists("app.ico"):
progress_dialog.iconbitmap("app.ico")
except Exception as e:
print(f"设置图标失败: {str(e)}")
-
+
# 保持对话框在顶层
- progress_dialog.attributes('-topmost', True)
-
+ progress_dialog.attributes("-topmost", True)
+
# 居中对话框
self.center_window(progress_dialog)
-
+
# 添加进度标签 - 只保留一个简单的说明
- progress_label = ttk.Label(progress_dialog, text="正在加载窗口...", font=("微软雅黑", 10))
+ progress_label = ttk.Label(
+ progress_dialog, text="正在加载窗口...", font=("微软雅黑", 10)
+ )
progress_label.pack(pady=(15, 10))
-
+
# 不再显示状态标签 (删除status_label)
-
+
# 添加进度条
- progress_bar = ttk.Progressbar(progress_dialog, mode="indeterminate", length=250)
+ progress_bar = ttk.Progressbar(
+ progress_dialog, mode="indeterminate", length=250
+ )
progress_bar.pack(pady=10)
progress_bar.start(10) # 开始动画
-
+
# 添加取消按钮
- cancel_btn = ttk.Button(progress_dialog, text="取消", command=progress_dialog.destroy)
+ cancel_btn = ttk.Button(
+ progress_dialog, text="取消", command=progress_dialog.destroy
+ )
cancel_btn.pack(pady=5)
-
+
# 在后台线程中进行窗口导入操作
import_thread_active = [True] # 使用列表作为可变引用
-
+
def import_thread():
try:
# 初始化COM环境,必须在线程中使用WMI之前调用
pythoncom.CoInitialize()
-
+
windows = []
-
+ hwnd_map = {}
+
# 使用WMI搜索Chrome进程
def search_chrome_processes():
c = wmi.WMI()
chrome_processes = []
# 不再更新进度文字
-
+
for process in c.Win32_Process():
if not import_thread_active[0]:
return [] # 如果取消了,立即返回
-
+
# 检查ExecutablePath是否为None
- if process.ExecutablePath is not None and "chrome.exe" in process.ExecutablePath.lower():
+ if (
+ process.ExecutablePath is not None
+ and "chrome.exe" in process.ExecutablePath.lower()
+ ):
cmd_line = process.CommandLine
- if cmd_line and '--user-data-dir=' in cmd_line:
+ if cmd_line and "--user-data-dir=" in cmd_line:
chrome_processes.append(process)
-
+
return chrome_processes
-
+
# 获取Chrome进程
chrome_processes = search_chrome_processes()
total_processes = len(chrome_processes)
-
+
if not import_thread_active[0]:
return # 如果已取消,不继续处理
-
+
# 不再更新进度文字
-
+
# 处理每个Chrome进程
for index, process in enumerate(chrome_processes):
if not import_thread_active[0]:
return # 如果已取消,不继续处理
-
+
try:
pid = process.ProcessId
cmd_line = process.CommandLine
-
+
# 不再更新进度文字
-
- if '--user-data-dir=' in cmd_line:
+
+ if "--user-data-dir=" in cmd_line:
# 先检查这个进程是否有可见的Chrome窗口
def find_window_for_process(pid):
result = []
-
+
def enum_callback(hwnd, process_windows):
if win32gui.IsWindowVisible(hwnd):
- _, win_pid = win32process.GetWindowThreadProcessId(hwnd)
+ _, win_pid = (
+ win32process.GetWindowThreadProcessId(
+ hwnd
+ )
+ )
if win_pid == pid:
title = win32gui.GetWindowText(hwnd)
- if title and not title.startswith("Chrome 传递"):
+ if title and not title.startswith(
+ "Chrome 传递"
+ ):
process_windows.append(hwnd)
-
+
process_windows = []
win32gui.EnumWindows(enum_callback, process_windows)
return process_windows
-
+
# 获取该进程的窗口列表
chrome_windows = find_window_for_process(pid)
-
+
# 如果没有可见窗口,跳过这个进程
# 这有助于避免处理后台或扩展进程
if not chrome_windows:
continue
-
+
# 从命令行中提取用户数据目录路径
- data_dir = re.search(r'--user-data-dir="?([^"]+)"?', cmd_line)
+ data_dir = re.search(
+ r'--user-data-dir="?([^"]+)"?', cmd_line
+ )
if data_dir:
data_path = data_dir.group(1)
-
+
# 尝试找到对应的快捷方式和编号
window_num = None
-
+
# 1. 首先尝试从快捷方式目录查找与此用户数据目录匹配的快捷方式
shortcut_dir = self.shortcut_path
if shortcut_dir and os.path.exists(shortcut_dir):
for shortcut_file in os.listdir(shortcut_dir):
- if shortcut_file.endswith('.lnk'):
- shortcut_path = os.path.join(shortcut_dir, shortcut_file)
+ if shortcut_file.endswith(".lnk"):
+ shortcut_path = os.path.join(
+ shortcut_dir, shortcut_file
+ )
try:
- shortcut_obj = self.shell.CreateShortCut(shortcut_path)
- shortcut_args = shortcut_obj.Arguments
-
+ shortcut_obj = (
+ self.shell.CreateShortCut(
+ shortcut_path
+ )
+ )
+ shortcut_args = (
+ shortcut_obj.Arguments
+ )
+
# 检查是否为同一数据目录
- if '--user-data-dir=' in shortcut_args:
- shortcut_data_dir = re.search(r'--user-data-dir="?([^"]+)"?', shortcut_args)
- if shortcut_data_dir and self.normalize_path(shortcut_data_dir.group(1)) == self.normalize_path(data_path):
+ if (
+ "--user-data-dir="
+ in shortcut_args
+ ):
+ shortcut_data_dir = re.search(
+ r'--user-data-dir="?([^"]+)"?',
+ shortcut_args,
+ )
+ if (
+ shortcut_data_dir
+ and self.normalize_path(
+ shortcut_data_dir.group(
+ 1
+ )
+ )
+ == self.normalize_path(
+ data_path
+ )
+ ):
# 找到匹配的快捷方式,从文件名提取编号
- shortcut_name = os.path.splitext(shortcut_file)[0]
+ shortcut_name = (
+ os.path.splitext(
+ shortcut_file
+ )[0]
+ )
if shortcut_name.isdigit():
- window_num = int(shortcut_name)
+ window_num = int(
+ shortcut_name
+ )
break
except Exception as e:
print(f"读取快捷方式失败: {str(e)}")
-
+
# 2. 如果未找到匹配的快捷方式,则尝试从数据目录名称中提取(向后兼容)
if window_num is None:
try:
@@ -2281,90 +2719,114 @@ def enum_callback(hwnd, process_windows):
window_num = int(base_name)
except:
pass
-
+
# 3. 如果仍未找到编号,则创建一个临时编号
if window_num is None:
# 生成一个大于1001的临时编号,避免与用户自定义编号冲突
window_num = 1001 + len(windows)
- print(f"未能确定窗口编号,使用临时编号: {window_num},用户数据目录: {data_path}")
-
+ print(
+ f"未能确定窗口编号,使用临时编号: {window_num},用户数据目录: {data_path}"
+ )
+
# 注意:这里不再需要重复查找窗口,因为我们已经在前面找到了窗口
# 使用第一个窗口
hwnd = chrome_windows[0]
title = win32gui.GetWindowText(hwnd)
- windows.append({
- 'hwnd': hwnd,
- 'title': title,
- 'number': window_num
- })
+ windows.append(
+ {
+ "hwnd": hwnd,
+ "title": title,
+ "number": window_num,
+ }
+ )
print(f"添加窗口: 编号={window_num}, 标题={title}")
+ hwnd_map[window_num] = hwnd
except:
continue
-
+
# 按窗口编号排序(升序)
- windows.sort(key=lambda w: w['number'])
-
+ windows.sort(key=lambda w: w["number"])
+
# 导入完成,更新UI
def update_ui():
if not import_thread_active[0]:
return # 如果已取消,不更新UI
-
+
# 填充列表
for window in windows:
- self.window_list.insert("", "end", values=("", f"{window['number']}", window['title'], "", window['hwnd']))
-
+ self.window_list.insert(
+ "",
+ "end",
+ values=(
+ "",
+ f"{window['number']}",
+ window["title"],
+ "",
+ window["hwnd"],
+ ),
+ )
+
# 更新端口映射
- self.debug_ports = {w['number']: 9222 + w['number'] for w in windows}
-
+ self.debug_ports = {
+ w["number"]: 9222 + w["number"] for w in windows
+ }
+
# 关闭进度对话框 - 不显示完成文字,直接变进度条状态
progress_bar.stop()
progress_bar.config(mode="determinate", value=100)
-
+
# 0.3秒后关闭对话框 - 减少等待时间,但还是给用户一点完成的视觉反馈
progress_dialog.after(300, progress_dialog.destroy)
-
+
# 显示导入结果 - 只在没有找到窗口时显示提示
if not windows:
# 延迟显示消息框,确保进度对话框已关闭
- self.root.after(400, lambda: messagebox.showinfo("导入结果", "未找到任何Chrome窗口"))
+ self.root.after(
+ 400,
+ lambda: messagebox.showinfo(
+ "导入结果", "未找到任何Chrome窗口"
+ ),
+ )
else:
# 只在控制台打印结果,不再向用户显示
print(f"成功导入 {len(windows)} 个窗口")
-
+
# 在主线程中更新UI
if import_thread_active[0]:
progress_dialog.after(0, update_ui)
-
+ # 自动为导入的窗口设置图标
+ self.apply_icons_to_chrome_windows(hwnd_map)
+
except Exception as import_error:
# 修复变量作用域问题 - 将异常保存到局部变量
error_message = str(import_error)
print(f"导入窗口线程内部错误: {error_message}")
-
+
# 在主线程中关闭对话框并显示错误
def show_error_message():
if progress_dialog.winfo_exists():
progress_dialog.destroy()
messagebox.showerror("错误", f"导入窗口失败: {error_message}")
-
+
progress_dialog.after(0, show_error_message)
-
+
finally:
# 清理COM环境
try:
pythoncom.CoUninitialize()
except:
pass
-
+
# 取消按钮的事件处理
def on_cancel():
import_thread_active[0] = False
progress_dialog.destroy()
-
+
cancel_btn.config(command=on_cancel)
-
+
# 启动导入线程
threading.Thread(target=import_thread, daemon=True).start()
-
+
except Exception as e:
print(f"导入窗口失败: {str(e)}")
messagebox.showerror("错误", f"导入窗口失败: {str(e)}")
@@ -2375,40 +2837,40 @@ def enum_window_callback(self, hwnd, windows):
# 检查窗口是否可见
if not win32gui.IsWindowVisible(hwnd):
return
-
+
# 获取窗口标题
title = win32gui.GetWindowText(hwnd)
if not title:
return
-
+
# 检查是否是Chrome窗口
if " - Google Chrome" in title:
# 提取窗口编号
number = None
if title.startswith("[主控]"):
title = title[4:].strip() # 移除[主控]标记
-
+
# 从进程命令行参数中获取窗口编号
try:
_, pid = win32process.GetWindowThreadProcessId(hwnd)
- handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ, False, pid)
+ handle = win32api.OpenProcess(
+ win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ,
+ False,
+ pid,
+ )
if handle:
cmd_line = win32process.GetModuleFileNameEx(handle, 0)
win32api.CloseHandle(handle)
-
+
# 从路径中提取编号
if "\\Data\\" in cmd_line:
number = int(cmd_line.split("\\Data\\")[-1].split("\\")[0])
except:
pass
-
+
if number is not None:
- windows.append({
- 'hwnd': hwnd,
- 'title': title,
- 'number': number
- })
-
+ windows.append({"hwnd": hwnd, "title": title, "number": number})
+
except Exception as e:
print(f"枚举窗口失败: {str(e)}")
@@ -2418,141 +2880,49 @@ def close_selected_windows(self):
for item in self.window_list.get_children():
if self.window_list.set(item, "select") == "√":
selected.append(item)
-
+
if not selected:
messagebox.showinfo("提示", "请先选择要关闭的窗口!")
return
-
+
try:
for item in selected:
# 从values中获取hwnd
- hwnd = int(self.window_list.item(item)['values'][4])
+ hwnd = int(self.window_list.item(item)["values"][4])
try:
# 检查窗口是否还存在
if win32gui.IsWindow(hwnd):
win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
except:
pass # 忽略已关闭窗口的错误
-
+
# 移除自动导入,改为手动从列表中删除项目
for item in selected:
self.window_list.delete(item)
-
+
# 重置全选按钮状态为"全部选择"
self.select_all_var.set("全部选择")
-
+
# 显示Chrome后台运行提示(如果启用)
if self.show_chrome_tip:
self.show_chrome_settings_tip()
-
- except Exception as e:
- print(f"关闭窗口失败: {str(e)}") # 只打印错误,不显示错误对话框
- def set_taskbar_icons(self):
- # 设置独立任务栏图标
- # 从设置中获取目录信息
- settings = self.load_settings()
- shortcut_dir = self.shortcut_path
- icon_dir = settings.get('icon_dir', '')
-
- if not shortcut_dir:
- messagebox.showinfo("提示", "请先在设置中设置快捷方式目录!")
- return
-
- if not os.path.exists(shortcut_dir):
- messagebox.showerror("错误", "快捷方式目录不存在!")
- return
-
- if not icon_dir:
- messagebox.showinfo("提示", "请先在设置中设置图标目录!")
- return
-
- if not os.path.exists(icon_dir):
- messagebox.showerror("错误", "图标目录不存在!")
- return
-
- # 确认操作
- choice = messagebox.askyesnocancel("选择操作", "选择要执行的操作:\n是 - 设置自定义图标\n否 - 恢复原始设置\n取消 - 不执行任何操作")
- if choice is None: # 用户点击取消
- return
-
- try:
- shell = win32com.client.Dispatch("WScript.Shell")
- modified_count = 0
-
- # 获取要修改的窗口编号列表
- window_numbers = self.parse_window_numbers(self.icon_window_numbers.get())
-
- if choice: # 设置自定义图标
- # 确保图标目录存在
- if not os.path.exists(icon_dir):
- os.makedirs(icon_dir)
-
- # 修改指定的快捷方式
- for i in window_numbers:
- shortcut_path = os.path.join(shortcut_dir, f"{i}.lnk")
- if not os.path.exists(shortcut_path):
- continue
-
- # 修改快捷方式
- shortcut = shell.CreateShortCut(shortcut_path)
-
- # 设置自定义图标
- icon_path = os.path.join(icon_dir, f"{i}.ico")
- if os.path.exists(icon_path):
- shortcut.IconLocation = icon_path
- # 保存修改
- shortcut.save()
- modified_count += 1
-
- messagebox.showinfo("成功", f"已成功修改 {modified_count} 个快捷方式的图标!")
- else: # 恢复原始设置
- chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
- if not os.path.exists(chrome_path):
- chrome_path = r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
-
- # 获取Chrome数据目录
- chrome_data_dir = settings.get('cache_dir', 'D:\\chrom duo\\Data')
-
- # 恢复指定的快捷方式
- for i in window_numbers:
- shortcut_path = os.path.join(shortcut_dir, f"{i}.lnk")
- if not os.path.exists(shortcut_path):
- continue
-
- # 修改快捷方式
- shortcut = shell.CreateShortCut(shortcut_path)
-
- # 恢复默认图标
- shortcut.IconLocation = f"{chrome_path},0"
-
- # 恢复原始启动参数
- original_args = f'--user-data-dir="{chrome_data_dir}\\{i}"'
- shortcut.TargetPath = chrome_path
- shortcut.Arguments = original_args
-
- # 保存修改
- shortcut.save()
- modified_count += 1
-
- messagebox.showinfo("成功", f"已成功恢复 {modified_count} 个快捷方式的原始设置!")
-
except Exception as e:
- messagebox.showerror("错误", f"操作失败: {str(e)}")
+ print(f"关闭窗口失败: {str(e)}") # 只打印错误,不显示错误对话框
def batch_open_urls(self):
"""批量打开网页,使用直接的命令行方式打开URL"""
try:
# 获取输入的网址
- url = self.url_entry.get()
+ url = self.url_entry.get()
if not url:
messagebox.showwarning("警告", "请输入要打开的网址!")
return
-
+
# 确保 URL 格式正确
- if not url.startswith(('http://', 'https://')):
- url = 'https://' + url
-
+ if not url.startswith(("http://", "https://")):
+ url = "https://" + url
+
# 获取选中的窗口
selected_windows = []
for item in self.window_list.get_children():
@@ -2560,111 +2930,125 @@ def batch_open_urls(self):
try:
# 获取窗口编号和标题
# values列表: ["", "编号", "标题", "", hwnd]
- window_values = self.window_list.item(item)['values']
+ window_values = self.window_list.item(item)["values"]
window_num = int(window_values[1]) # 获取窗口编号
- window_title = str(window_values[2]) if len(window_values) > 2 else ""
+ window_title = (
+ str(window_values[2]) if len(window_values) > 2 else ""
+ )
hwnd = int(window_values[-1]) if len(window_values) > 4 else 0
-
+
# 调试输出
- print(f"选择了窗口: {window_title} (编号: {window_num}, 句柄: {hwnd})")
+ print(
+ f"选择了窗口: {window_title} (编号: {window_num}, 句柄: {hwnd})"
+ )
selected_windows.append(window_num)
except (ValueError, IndexError) as e:
print(f"解析窗口信息出错: {str(e)}")
# 忽略无法识别编号的窗口
-
+
if not selected_windows:
messagebox.showwarning("警告", "请先选择要操作的窗口!")
return
-
+
# 调试输出
print(f"选择的窗口编号: {selected_windows}")
-
+
# 验证快捷方式目录是否存在
shortcut_dir = self.shortcut_path
if not shortcut_dir or not os.path.exists(shortcut_dir):
messagebox.showerror("错误", "快捷方式目录不存在,请在设置中配置!")
return
-
+
# 查找Chrome路径
chrome_path = self.find_chrome_path()
if not chrome_path:
messagebox.showerror("错误", "未找到Chrome安装路径!")
return
-
+
# 创建WScript.Shell对象(如果尚未创建)
- if not hasattr(self, 'shell') or self.shell is None:
+ if not hasattr(self, "shell") or self.shell is None:
self.shell = win32com.client.Dispatch("WScript.Shell")
-
+
# 为每个选中的窗口直接启动Chrome并打开指定URL
success_count = 0
for window_num in selected_windows:
try:
# 通过快捷方式获取用户数据目录路径
shortcut_path = os.path.join(shortcut_dir, f"{window_num}.lnk")
-
+
# 检查快捷方式是否存在
if not os.path.exists(shortcut_path):
- print(f"警告: 窗口 {window_num} 的快捷方式不存在: {shortcut_path}")
+ print(
+ f"警告: 窗口 {window_num} 的快捷方式不存在: {shortcut_path}"
+ )
continue
-
+
# 从快捷方式中获取用户数据目录
try:
shortcut_obj = self.shell.CreateShortCut(shortcut_path)
cmd_line = shortcut_obj.Arguments
-
+
# 提取user-data-dir参数
- if '--user-data-dir=' in cmd_line:
- user_data_dir = re.search(r'--user-data-dir="?([^"]+)"?', cmd_line)
+ if "--user-data-dir=" in cmd_line:
+ user_data_dir = re.search(
+ r'--user-data-dir="?([^"]+)"?', cmd_line
+ )
if user_data_dir:
user_data_dir = user_data_dir.group(1)
else:
- print(f"警告: 无法从快捷方式提取用户数据目录: {shortcut_path}")
+ print(
+ f"警告: 无法从快捷方式提取用户数据目录: {shortcut_path}"
+ )
continue
else:
# 尝试使用旧的方式(向后兼容)
- user_data_dir = os.path.join(self.cache_dir, str(window_num))
+ user_data_dir = os.path.join(
+ self.cache_dir, str(window_num)
+ )
if not os.path.exists(user_data_dir):
- print(f"警告: 窗口 {window_num} 的用户数据目录不存在: {user_data_dir}")
+ print(
+ f"警告: 窗口 {window_num} 的用户数据目录不存在: {user_data_dir}"
+ )
continue
except Exception as e:
print(f"警告: 读取快捷方式失败: {str(e)}")
continue
-
+
# 使用subprocess.list形式构建命令,避免路径引号问题
cmd_list = [
chrome_path,
- f'--user-data-dir={user_data_dir}',
+ f"--user-data-dir={user_data_dir}",
]
-
+
# 如果启用了CDP,添加调试端口参数
if self.enable_cdp:
debug_port = 9222 + window_num
- cmd_list.insert(1, f'--remote-debugging-port={debug_port}')
-
+ cmd_list.insert(1, f"--remote-debugging-port={debug_port}")
+
# 添加URL
cmd_list.append(url)
-
+
# 打印命令以便调试
print(f"执行命令: {' '.join(cmd_list)}")
-
+
# 使用不带shell的方式启动进程,避免命令行解析问题
subprocess.Popen(cmd_list)
-
+
success_count += 1
print(f"成功在窗口 {window_num} 打开URL: {url}")
-
+
# 短暂延迟,避免同时打开太多窗口导致系统过载
time.sleep(0.1)
-
+
except Exception as e:
print(f"打开URL失败 (窗口 {window_num}): {str(e)}")
-
+
# 移除通知提示,操作成功或失败都不再提示
# if success_count > 0:
# self.show_notification("成功", f"成功为 {success_count} 个窗口打开了网页!")
# else:
# messagebox.showerror("失败", "批量打开网页失败!")
-
+
except Exception as e:
messagebox.showerror("错误", f"批量打开网页失败: {str(e)}")
@@ -2673,28 +3057,32 @@ def find_chrome_path(self):
common_paths = [
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
- r"C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe"
+ r"C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe",
]
-
+
# 替换用户名
- username = os.environ.get('USERNAME', '')
- common_paths = [p.replace('%USERNAME%', username) for p in common_paths]
-
+ username = os.environ.get("USERNAME", "")
+ common_paths = [p.replace("%USERNAME%", username) for p in common_paths]
+
# 检查常见路径
for path in common_paths:
if os.path.exists(path):
return path
-
+
# 如果找不到,尝试从注册表获取
try:
import winreg
- key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe")
+
+ key = winreg.OpenKey(
+ winreg.HKEY_LOCAL_MACHINE,
+ r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe",
+ )
chrome_path, _ = winreg.QueryValueEx(key, None)
if os.path.exists(chrome_path):
return chrome_path
except:
pass
-
+
# 如果以上方法都失败,返回None
return None
@@ -2703,12 +3091,12 @@ def run(self):
try:
# 确保窗口快速显示
print(f"[{time.time() - self.start_time:.3f}s] 开始显示窗口...")
- self.root.deiconify() # 显示窗口
- self.root.attributes('-topmost', True) # 先设置为置顶
- self.root.update() # 强制刷新UI
- self.root.attributes('-topmost', False) # 取消置顶
+ self.root.deiconify() # 显示窗口
+ self.root.attributes("-topmost", True) # 先设置为置顶
+ self.root.update() # 强制刷新UI
+ self.root.attributes("-topmost", False) # 取消置顶
print(f"[{time.time() - self.start_time:.3f}s] 窗口显示完成")
-
+
# 启动主循环
self.root.mainloop()
except Exception as e:
@@ -2719,39 +3107,53 @@ def run(self):
try:
messagebox.showerror("程序错误", error_message)
except:
- pass
-
+ pass
+
def delayed_initialization(self):
"""延迟执行可能耗时的初始化操作"""
try:
print(f"[{time.time() - self.start_time:.3f}s] 开始执行延迟初始化")
-
- # 检查管理员权限(延迟检查)
- if not is_admin():
- print(f"[{time.time() - self.start_time:.3f}s] 检测到非管理员权限,准备提示")
- # 将管理员权限请求延迟显示,确保主窗口已完全显示
+
+ # 检查管理员权限(延迟检查),已确认则跳过
+ if not self.settings.get("admin_confirmed", False) and not is_admin():
+ print(
+ f"[{time.time() - self.start_time:.3f}s] 检测到非管理员权限,准备提示"
+ )
+
def show_admin_prompt():
- result = messagebox.askquestion("权限提示", "没有管理员权限可能无法正常访问某些窗口,是否以管理员身份重新启动?")
- if result == 'yes':
- run_as_admin()
- self.root.destroy()
- # 延迟更长时间显示,避免干扰用户
+ try:
+ user_confirmed = messagebox.askyesno(
+ "权限提示",
+ "没有管理员权限可能无法正常访问某些窗口,是否以管理员身份重新启动?",
+ )
+ if user_confirmed:
+ self.settings["admin_confirmed"] = True
+ self.save_settings()
+ run_as_admin()
+ self.root.destroy()
+ except Exception as e:
+ print(f"管理员权限弹窗异常: {str(e)}")
+
self.root.after(1500, show_admin_prompt)
else:
- print(f"[{time.time() - self.start_time:.3f}s] 已是管理员权限")
-
+ print(
+ f"[{time.time() - self.start_time:.3f}s] 已是管理员权限或已确认不再询问"
+ )
+
# 预热窗口枚举 (这个操作可能比较慢)
print(f"[{time.time() - self.start_time:.3f}s] 开始预热窗口枚举...")
try:
# 注意:这里不实际填充列表,只做枚举测试
- windows = []
+ windows = []
win32gui.EnumWindows(self.enum_window_callback, windows)
print(f"[{time.time() - self.start_time:.3f}s] 预热窗口枚举完成")
except Exception as e:
- print(f"[{time.time() - self.start_time:.3f}s] 预热窗口枚举失败: {str(e)}")
-
+ print(
+ f"[{time.time() - self.start_time:.3f}s] 预热窗口枚举失败: {str(e)}"
+ )
+
# 其他可能耗时的初始化可以放在这里
-
+
print(f"[{time.time() - self.start_time:.3f}s] 所有延迟初始化任务完成")
except Exception as e:
print(f"[{time.time() - self.start_time:.3f}s] 延迟初始化出错: {str(e)}")
@@ -2759,15 +3161,15 @@ def show_admin_prompt():
def load_window_position(self):
# 从 settings.json 加载窗口位置
try:
- position = self.settings.get('window_position')
+ position = self.settings.get("window_position")
if position:
# 检查是否只包含位置信息(以+开头)
- if position.startswith('+'):
+ if position.startswith("+"):
return position # 直接返回位置信息
-
+
# 处理包含尺寸的旧格式("widthxheight+x+y")
- if 'x' in position and '+' in position:
- parts = position.split('+')
+ if "x" in position and "+" in position:
+ parts = position.split("+")
if len(parts) >= 3:
return f"+{parts[1]}+{parts[2]}" # 只返回位置部分
return None
@@ -2780,19 +3182,19 @@ def save_window_position(self):
try:
# 获取窗口当前位置
geometry = self.root.geometry()
-
+
# 提取位置信息 (x和y坐标)
- position_parts = geometry.split('+')
+ position_parts = geometry.split("+")
if len(position_parts) >= 3:
x_pos = position_parts[1]
y_pos = position_parts[2]
position = f"+{x_pos}+{y_pos}" # 只保存位置信息
-
+
# 保存到设置
- self.settings['window_position'] = position
-
+ self.settings["window_position"] = position
+
# 写入文件
- with open('settings.json', 'w', encoding='utf-8') as f:
+ with open("settings.json", "w", encoding="utf-8") as f:
json.dump(self.settings, f, ensure_ascii=False, indent=4)
except Exception as e:
print(f"保存窗口位置失败: {str(e)}")
@@ -2800,16 +3202,17 @@ def save_window_position(self):
def get_chrome_popups(self, chrome_hwnd):
"""改进的插件窗口检测,支持网页触发的钱包插件和网页浮动层"""
popups = []
+
def enum_windows_callback(hwnd, _):
try:
if not win32gui.IsWindowVisible(hwnd):
return
-
+
class_name = win32gui.GetClassName(hwnd)
title = win32gui.GetWindowText(hwnd)
_, chrome_pid = win32process.GetWindowThreadProcessId(chrome_hwnd)
_, popup_pid = win32process.GetWindowThreadProcessId(hwnd)
-
+
# 检查是否是Chrome相关窗口
if popup_pid == chrome_pid:
# 检查窗口类型
@@ -2817,123 +3220,160 @@ def enum_windows_callback(hwnd, _):
# 检查是否是扩展程序相关窗口,放宽检测条件
style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
ex_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
-
+
# 扩展窗口的特征
is_popup = (
- "扩展程序" in title or
- "插件" in title or
- "OKX" in title or # 常见钱包名称
- "MetaMask" in title or # 常见钱包名称
- "钱包" in title or
- "Wallet" in title or
- win32gui.GetParent(hwnd) == chrome_hwnd or
- (style & win32con.WS_POPUP) != 0 or
- (style & win32con.WS_CHILD) != 0 or
- (ex_style & win32con.WS_EX_TOOLWINDOW) != 0 or
- (ex_style & win32con.WS_EX_DLGMODALFRAME) != 0 # 对话框样式窗口
+ "扩展程序" in title
+ or "插件" in title
+ or "OKX" in title # 常见钱包名称
+ or "MetaMask" in title # 常见钱包名称
+ or "钱包" in title
+ or "Wallet" in title
+ or win32gui.GetParent(hwnd) == chrome_hwnd
+ or (style & win32con.WS_POPUP) != 0
+ or (style & win32con.WS_CHILD) != 0
+ or (ex_style & win32con.WS_EX_TOOLWINDOW) != 0
+ or (ex_style & win32con.WS_EX_DLGMODALFRAME)
+ != 0 # 对话框样式窗口
)
-
+
# 获取窗口位置和大小,钱包插件通常较小
rect = win32gui.GetWindowRect(hwnd)
width = rect[2] - rect[0]
height = rect[3] - rect[1]
-
+
# 钱包插件窗口通常不会特别大
- is_wallet_size = (width < 800 and height < 800 and width > 200 and height > 200)
-
+ is_wallet_size = (
+ width < 800
+ and height < 800
+ and width > 200
+ and height > 200
+ )
+
# 网页浮动层通常是较小的弹窗
is_floating_layer = (
- (style & win32con.WS_POPUP) != 0 and
- (width < 600 and height < 600) and
- hwnd != chrome_hwnd
+ (style & win32con.WS_POPUP) != 0
+ and (width < 600 and height < 600)
+ and hwnd != chrome_hwnd
)
-
+
if is_popup or is_wallet_size or is_floating_layer:
# 增加额外判断,如果窗口很像钱包弹窗,即使不满足其他条件也捕获
if hwnd != chrome_hwnd and hwnd not in popups:
- if self.is_likely_wallet_popup(hwnd, chrome_hwnd) or is_floating_layer:
+ if (
+ self.is_likely_wallet_popup(hwnd, chrome_hwnd)
+ or is_floating_layer
+ ):
popups.append(hwnd)
- print(f"识别到可能的钱包插件窗口或网页浮动层: {title} (句柄: {hwnd})")
+ print(
+ f"识别到可能的钱包插件窗口或网页浮动层: {title} (句柄: {hwnd})"
+ )
elif is_popup:
popups.append(hwnd)
-
+
except Exception as e:
print(f"枚举窗口失败: {str(e)}")
-
+
win32gui.EnumWindows(enum_windows_callback, None)
return popups
-
+
def is_likely_wallet_popup(self, hwnd, parent_hwnd):
"""检查窗口是否可能是钱包弹出窗口或网页浮动层"""
try:
# 常见钱包和浮层关键词
keywords = [
- "钱包", "okx", "metamask", "token", "connect", "wallet", "sign",
- "signature", "transaction", "登录", "connect", "eth", "web3", "链接", "连接",
- "确认", "confirm", "cancel", "取消", "dialog", "弹出层", "浮层", "modal",
- "popup", "alert", "提示", "通知", "message", "消息"
+ "钱包",
+ "okx",
+ "metamask",
+ "token",
+ "connect",
+ "wallet",
+ "sign",
+ "signature",
+ "transaction",
+ "登录",
+ "connect",
+ "eth",
+ "web3",
+ "链接",
+ "连接",
+ "确认",
+ "confirm",
+ "cancel",
+ "取消",
+ "dialog",
+ "弹出层",
+ "浮层",
+ "modal",
+ "popup",
+ "alert",
+ "提示",
+ "通知",
+ "message",
+ "消息",
]
-
+
# 检查窗口标题
title = win32gui.GetWindowText(hwnd).lower()
for keyword in keywords:
if keyword.lower() in title:
return True
-
+
# 尝试获取窗口内部的文本 (使用WM_GETTEXT消息)
buffer_size = 1024
buffer = ctypes.create_unicode_buffer(buffer_size)
try:
- ctypes.windll.user32.SendMessageW(hwnd, win32con.WM_GETTEXT, buffer_size, ctypes.byref(buffer))
+ ctypes.windll.user32.SendMessageW(
+ hwnd, win32con.WM_GETTEXT, buffer_size, ctypes.byref(buffer)
+ )
text = buffer.value.lower()
for keyword in keywords:
if keyword.lower() in text:
return True
except:
pass
-
+
# 检查窗口尺寸和样式特征
rect = win32gui.GetWindowRect(hwnd)
width = rect[2] - rect[0]
height = rect[3] - rect[1]
-
+
# 获取Chrome主窗口位置
parent_rect = win32gui.GetWindowRect(parent_hwnd)
-
+
# 检查窗口是否在Chrome窗口内或附近
is_near_chrome = (
- rect[0] >= parent_rect[0] - 100 and
- rect[1] >= parent_rect[1] - 100 and
- rect[2] <= parent_rect[2] + 100 and
- rect[3] <= parent_rect[3] + 100
+ rect[0] >= parent_rect[0] - 100
+ and rect[1] >= parent_rect[1] - 100
+ and rect[2] <= parent_rect[2] + 100
+ and rect[3] <= parent_rect[3] + 100
)
-
+
# 检查窗口样式
style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
ex_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
-
+
# 弹出窗口特征
has_popup_style = (
- (style & win32con.WS_POPUP) != 0 or
- (ex_style & win32con.WS_EX_TOPMOST) != 0 or
- (ex_style & win32con.WS_EX_TOOLWINDOW) != 0
+ (style & win32con.WS_POPUP) != 0
+ or (ex_style & win32con.WS_EX_TOPMOST) != 0
+ or (ex_style & win32con.WS_EX_TOOLWINDOW) != 0
)
-
+
# 检测是否为网页浮动层 (往往会有z-index较高,且有特定样式)
is_floating_layer = (
- has_popup_style and
- is_near_chrome and
- (200 <= width <= 600 and 100 <= height <= 600)
+ has_popup_style
+ and is_near_chrome
+ and (200 <= width <= 600 and 100 <= height <= 600)
)
-
+
# 综合判断
return (
- ((300 <= width <= 600 and 300 <= height <= 800) and # 典型钱包窗口尺寸
- has_popup_style and
- is_near_chrome) or
- is_floating_layer
- )
-
+ (300 <= width <= 600 and 300 <= height <= 800) # 典型钱包窗口尺寸
+ and has_popup_style
+ and is_near_chrome
+ ) or is_floating_layer
+
except Exception as e:
print(f"判断钱包窗口或浮动层失败: {str(e)}")
return False
@@ -2943,24 +3383,24 @@ def monitor_popups(self):
last_check_time = time.time()
last_error_time = 0
error_count = 0
-
+
# 钱包插件窗口同步历史
wallet_popup_history = {}
-
+
print("启动弹窗监控线程...")
-
+
while self.is_sync:
try:
# 优化CPU使用率
time.sleep(0.1) # 更快速的检查以捕获快速弹出的钱包窗口
-
+
# 每500毫秒执行一次完整检查
current_time = time.time()
if current_time - last_check_time < 0.5:
continue
-
+
last_check_time = current_time
-
+
# 检查主窗口是否有效
if not self.master_window or not win32gui.IsWindow(self.master_window):
if current_time - last_error_time > 10:
@@ -2968,35 +3408,41 @@ def monitor_popups(self):
last_error_time = current_time
self.stop_sync()
break
-
+
# 获取主窗口的弹出窗口
current_popups = self.get_chrome_popups(self.master_window)
-
+
# 检查是否有新增弹出窗口或关闭的弹出窗口
- new_popups = [popup for popup in current_popups if popup not in self.popup_windows]
- closed_popups = [popup for popup in self.popup_windows if popup not in current_popups]
-
+ new_popups = [
+ popup for popup in current_popups if popup not in self.popup_windows
+ ]
+ closed_popups = [
+ popup for popup in self.popup_windows if popup not in current_popups
+ ]
+
has_changes = False
-
+
# 处理新的弹出窗口,特别注意钱包插件窗口
for popup in new_popups:
try:
if self.is_sync and win32gui.IsWindow(popup):
# 获取窗口标题
title = win32gui.GetWindowText(popup)
-
+
# 检查是否是钱包插件窗口
- is_wallet = self.is_likely_wallet_popup(popup, self.master_window)
-
+ is_wallet = self.is_likely_wallet_popup(
+ popup, self.master_window
+ )
+
if is_wallet:
print(f"发现钱包插件窗口: {title}")
# 记录钱包窗口信息用于后续处理
wallet_popup_history[popup] = {
- 'detected_time': time.time(),
- 'title': title,
- 'synced': False
+ "detected_time": time.time(),
+ "title": title,
+ "synced": False,
}
-
+
# 将弹出窗口添加到同步列表
if popup not in self.popup_windows:
self.popup_windows.append(popup)
@@ -3005,7 +3451,7 @@ def monitor_popups(self):
if current_time - last_error_time > 10:
print(f"处理新弹窗时出错: {str(e)}")
last_error_time = current_time
-
+
# 清理已关闭的弹出窗口
for popup in closed_popups:
if popup in self.popup_windows:
@@ -3013,71 +3459,77 @@ def monitor_popups(self):
if popup in wallet_popup_history:
del wallet_popup_history[popup]
has_changes = True
-
+
# 同步处理钱包窗口和其他弹出窗口
if has_changes:
self.sync_popups()
-
+
# 定期尝试同步钱包插件窗口,即使没有检测到变化
# 这有助于处理某些难以检测的网页触发钱包窗口
for hwnd, info in list(wallet_popup_history.items()):
- if (not info.get('synced') and
- current_time - info.get('detected_time', 0) > 0.5 and
- win32gui.IsWindow(hwnd)):
+ if (
+ not info.get("synced")
+ and current_time - info.get("detected_time", 0) > 0.5
+ and win32gui.IsWindow(hwnd)
+ ):
# 尝试强制同步钱包窗口
try:
self.sync_specific_popup(hwnd)
- info['synced'] = True
+ info["synced"] = True
print(f"强制同步钱包窗口: {info['title']}")
except Exception as e:
if current_time - last_error_time > 10:
print(f"强制同步钱包窗口失败: {str(e)}")
last_error_time = current_time
-
+
# 清理无效的历史记录
for hwnd in list(wallet_popup_history.keys()):
- if not win32gui.IsWindow(hwnd) or current_time - wallet_popup_history[hwnd]['detected_time'] > 60:
+ if (
+ not win32gui.IsWindow(hwnd)
+ or current_time - wallet_popup_history[hwnd]["detected_time"]
+ > 60
+ ):
del wallet_popup_history[hwnd]
-
+
except Exception as e:
error_count += 1
-
+
# 限制错误日志频率
if current_time - last_error_time > 10:
print(f"弹出窗口监控异常: {str(e)}")
last_error_time = current_time
-
+
# 防止过多错误导致CPU占用过高
if error_count > 100:
print("错误次数过多,停止弹窗监控")
break
-
+
time.sleep(1) # 出错后等待一段时间
-
+
print("弹窗监控线程已结束")
-
+
def sync_specific_popup(self, popup_hwnd):
"""单独同步特定的弹出窗口(特别是钱包插件窗口)"""
try:
if not win32gui.IsWindow(popup_hwnd):
return
-
+
# 获取窗口位置
popup_rect = win32gui.GetWindowRect(popup_hwnd)
popup_x = popup_rect[0]
popup_y = popup_rect[1]
popup_width = popup_rect[2] - popup_rect[0]
popup_height = popup_rect[3] - popup_rect[1]
-
+
# 获取主窗口位置
master_rect = win32gui.GetWindowRect(self.master_window)
master_x = master_rect[0]
master_y = master_rect[1]
-
+
# 计算相对位置(相对于主窗口左上角)
relative_x = popup_x - master_x
relative_y = popup_y - master_y
-
+
# 确保在其他浏览器窗口中也能看到弹出窗口
for hwnd in self.sync_windows:
try:
@@ -3086,59 +3538,67 @@ def sync_specific_popup(self, popup_hwnd):
sync_rect = win32gui.GetWindowRect(hwnd)
sync_x = sync_rect[0]
sync_y = sync_rect[1]
-
+
# 计算新位置(相对于同步窗口)
new_x = sync_x + relative_x
new_y = sync_y + relative_y
-
+
# 检查同步窗口的弹出窗口
sync_popups = self.get_chrome_popups(hwnd)
-
+
# 查找匹配的弹出窗口,使用标题和大小作为匹配依据
target_title = win32gui.GetWindowText(popup_hwnd)
matching_popup = None
-
+
for sync_popup in sync_popups:
if win32gui.IsWindow(sync_popup):
sync_popup_title = win32gui.GetWindowText(sync_popup)
sync_popup_rect = win32gui.GetWindowRect(sync_popup)
- sync_popup_width = sync_popup_rect[2] - sync_popup_rect[0]
- sync_popup_height = sync_popup_rect[3] - sync_popup_rect[1]
-
+ sync_popup_width = (
+ sync_popup_rect[2] - sync_popup_rect[0]
+ )
+ sync_popup_height = (
+ sync_popup_rect[3] - sync_popup_rect[1]
+ )
+
# 如果标题相似且尺寸相近,认为是匹配的窗口
- title_similarity = self.title_similarity(target_title, sync_popup_title)
+ title_similarity = self.title_similarity(
+ target_title, sync_popup_title
+ )
size_match = (
- abs(sync_popup_width - popup_width) < 50 and
- abs(sync_popup_height - popup_height) < 50
+ abs(sync_popup_width - popup_width) < 50
+ and abs(sync_popup_height - popup_height) < 50
)
-
+
if title_similarity > 0.5 or size_match:
matching_popup = sync_popup
break
-
+
# 移动匹配的弹出窗口
if matching_popup:
win32gui.SetWindowPos(
- matching_popup,
- win32con.HWND_TOP,
- new_x, new_y,
- popup_width, popup_height,
- win32con.SWP_NOACTIVATE
+ matching_popup,
+ win32con.HWND_TOP,
+ new_x,
+ new_y,
+ popup_width,
+ popup_height,
+ win32con.SWP_NOACTIVATE,
)
-
+
except Exception as e:
print(f"同步特定弹窗失败: {str(e)}")
-
+
except Exception as e:
print(f"同步特定弹窗出错: {str(e)}")
-
+
def title_similarity(self, title1, title2):
"""计算两个窗口标题之间的相似度
-
+
Args:
title1: 第一个窗口标题
title2: 第二个窗口标题
-
+
Returns:
float: 0到1之间的相似度分数,1表示完全匹配
"""
@@ -3147,23 +3607,23 @@ def title_similarity(self, title1, title2):
return 1.0
if not title1 or not title2:
return 0.0
-
+
# 转换为小写以进行不区分大小写的比较
title1 = title1.lower()
title2 = title2.lower()
-
+
# 计算Jaccard相似度
set1 = set(title1)
set2 = set(title2)
-
+
# 计算交集和并集的大小
intersection_size = len(set1.intersection(set2))
union_size = len(set1.union(set2))
-
+
# 避免除以零
if union_size == 0:
return 1.0
-
+
return intersection_size / union_size
def show_shortcut_dialog(self):
@@ -3173,11 +3633,11 @@ def show_shortcut_dialog(self):
dialog.title("设置同步功能快捷键")
dialog.geometry("300x150")
dialog.resizable(False, False)
-
+
# 使对话框模态
dialog.transient(self.root)
dialog.grab_set()
-
+
# 设置图标
try:
icon_path = os.path.join(os.path.dirname(__file__), "app.ico")
@@ -3185,137 +3645,133 @@ def show_shortcut_dialog(self):
dialog.iconbitmap(icon_path)
except Exception as e:
print(f"设置图标失败: {str(e)}")
-
+
# 当前快捷键显示
current_label = ttk.Label(dialog, text=f"当前快捷键: {self.current_shortcut}")
current_label.pack(pady=10)
-
+
# 快捷键输入框
shortcut_var = tk.StringVar(value="点击下方按钮开始录制快捷键...")
shortcut_label = ttk.Label(dialog, textvariable=shortcut_var)
shortcut_label.pack(pady=5)
-
+
# 记录按键状态
keys_pressed = set()
recording = False
on_key_event = None # 在外部声明,方便后续引用
-
+
def start_recording():
# 开始录制快捷键
nonlocal recording, on_key_event
recording = True
keys_pressed.clear()
shortcut_var.set("请按下快捷键组合...")
- record_btn.configure(state='disabled')
-
+ record_btn.configure(state="disabled")
+
# 定义按键事件处理函数
def on_key_event_handler(e):
if not recording:
return
if e.event_type == keyboard.KEY_DOWN:
keys_pressed.add(e.name)
- shortcut_var.set('+'.join(sorted(keys_pressed)))
+ shortcut_var.set("+".join(sorted(keys_pressed)))
elif e.event_type == keyboard.KEY_UP:
if e.name in keys_pressed:
keys_pressed.remove(e.name)
- if not keys_pressed:
+ if not keys_pressed:
stop_recording()
-
+
# 保存引用以便后续取消钩子
on_key_event = on_key_event_handler
-
+
# 只为录制添加临时钩子
keyboard.hook(on_key_event)
-
+
def stop_recording():
# 停止录制快捷键
nonlocal recording
recording = False
-
+
# 移除录制时添加的临时钩子,而不是所有钩子
keyboard.unhook(on_key_event)
-
+
# 不再需要重新设置当前快捷键,保持原状
- record_btn.configure(state='normal')
-
+ record_btn.configure(state="normal")
+
# 录制按钮
- record_btn = ttk.Button(
- dialog,
- text="开始录制",
- command=start_recording
- )
+ record_btn = ttk.Button(dialog, text="开始录制", command=start_recording)
record_btn.pack(pady=10)
-
+
def save_shortcut():
# 保存快捷键设置
new_shortcut = shortcut_var.get()
- if new_shortcut and new_shortcut != "点击下方按钮开始录制快捷键..." and new_shortcut != "请按下快捷键组合...":
+ if (
+ new_shortcut
+ and new_shortcut != "点击下方按钮开始录制快捷键..."
+ and new_shortcut != "请按下快捷键组合..."
+ ):
try:
# 设置新快捷键
self.set_shortcut(new_shortcut)
-
+
# 保存到设置文件
settings = self.load_settings()
- settings['sync_shortcut'] = new_shortcut
- with open('settings.json', 'w', encoding='utf-8') as f:
+ settings["sync_shortcut"] = new_shortcut
+ with open("settings.json", "w", encoding="utf-8") as f:
json.dump(settings, f, ensure_ascii=False, indent=4)
-
+
messagebox.showinfo("成功", f"快捷键已设置为: {new_shortcut}")
dialog.destroy()
except Exception as e:
messagebox.showerror("错误", f"设置快捷键失败: {str(e)}")
else:
messagebox.showwarning("警告", "请先录制快捷键!")
-
+
# 保存按钮
ttk.Button(
- dialog,
- text="保存",
- style='Accent.TButton',
- command=save_shortcut
+ dialog, text="保存", style="Accent.TButton", command=save_shortcut
).pack(pady=5)
-
+
# 确保关闭对话框时停止录制
- dialog.protocol("WM_DELETE_WINDOW", lambda: [stop_recording(), dialog.destroy()])
-
+ dialog.protocol(
+ "WM_DELETE_WINDOW", lambda: [stop_recording(), dialog.destroy()]
+ )
+
# 居中显示对话框
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
- dialog.geometry(f'{width}x{height}+{x}+{y}')
+ dialog.geometry(f"{width}x{height}+{x}+{y}")
def set_shortcut(self, shortcut):
# 设置快捷键
try:
# 只清除之前的快捷键钩子,而不是所有钩子
- if hasattr(self, 'shortcut_hook') and self.shortcut_hook:
+ if hasattr(self, "shortcut_hook") and self.shortcut_hook:
keyboard.remove_hotkey(self.shortcut_hook)
self.shortcut_hook = None
-
+
# 设置新的快捷键
if shortcut:
# 保存当前快捷键字符串,即使添加热键失败也能保留
self.current_shortcut = shortcut
-
+
# 添加新的热键钩子
self.shortcut_hook = keyboard.add_hotkey(
- shortcut,
- self.toggle_sync,
- suppress=True,
- trigger_on_release=True
+ shortcut, self.toggle_sync, suppress=True, trigger_on_release=True
)
print(f"快捷键 {shortcut} 设置成功")
-
+
# 保存到设置文件
settings = self.load_settings()
- settings['sync_shortcut'] = shortcut
- with open('settings.json', 'w', encoding='utf-8') as f:
+ settings["sync_shortcut"] = shortcut
+ with open("settings.json", "w", encoding="utf-8") as f:
json.dump(settings, f, ensure_ascii=False, indent=4)
-
+
return True
-
+
except Exception as e:
print(f"设置快捷键失败: {str(e)}")
# 不重置current_shortcut,即使失败也保留当前值
@@ -3325,19 +3781,22 @@ def update_screen_list(self):
"""更新屏幕列表,返回屏幕名称列表"""
try:
screens = []
+
def callback(hmonitor, hdc, lprect, lparam):
try:
# 获取显示器信息
monitor_info = win32api.GetMonitorInfo(hmonitor)
screen_name = f"屏幕 {len(screens) + 1}"
- if monitor_info['Flags'] & 1: # MONITORINFOF_PRIMARY
+ if monitor_info["Flags"] & 1: # MONITORINFOF_PRIMARY
screen_name += " (主)"
- screens.append({
- 'name': screen_name,
- 'rect': monitor_info['Monitor'],
- 'work_rect': monitor_info['Work'],
- 'monitor': hmonitor
- })
+ screens.append(
+ {
+ "name": screen_name,
+ "rect": monitor_info["Monitor"],
+ "work_rect": monitor_info["Work"],
+ "monitor": hmonitor,
+ }
+ )
except Exception as e:
print(f"处理显示器信息失败: {str(e)}")
return True
@@ -3348,49 +3807,64 @@ def callback(hmonitor, hdc, lprect, lparam):
ctypes.c_ulong,
ctypes.c_ulong,
ctypes.POINTER(wintypes.RECT),
- ctypes.c_longlong
+ ctypes.c_longlong,
)
# 创建回调函数
callback_function = MONITORENUMPROC(callback)
# 枚举显示器
- if ctypes.windll.user32.EnumDisplayMonitors(0, 0, callback_function, 0) == 0:
+ if (
+ ctypes.windll.user32.EnumDisplayMonitors(0, 0, callback_function, 0)
+ == 0
+ ):
# EnumDisplayMonitors 失败,尝试使用备用方法
try:
# 获取虚拟屏幕范围
- virtual_width = win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN)
- virtual_height = win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN)
+ virtual_width = win32api.GetSystemMetrics(
+ win32con.SM_CXVIRTUALSCREEN
+ )
+ virtual_height = win32api.GetSystemMetrics(
+ win32con.SM_CYVIRTUALSCREEN
+ )
virtual_left = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN)
virtual_top = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN)
# 获取主屏幕信息
- primary_monitor = win32api.MonitorFromPoint((0, 0), win32con.MONITOR_DEFAULTTOPRIMARY)
+ primary_monitor = win32api.MonitorFromPoint(
+ (0, 0), win32con.MONITOR_DEFAULTTOPRIMARY
+ )
primary_info = win32api.GetMonitorInfo(primary_monitor)
# 添加主屏幕
- screens.append({
- 'name': "屏幕 1 (主)",
- 'rect': primary_info['Monitor'],
- 'work_rect': primary_info['Work'],
- 'monitor': primary_monitor
- })
+ screens.append(
+ {
+ "name": "屏幕 1 (主)",
+ "rect": primary_info["Monitor"],
+ "work_rect": primary_info["Work"],
+ "monitor": primary_monitor,
+ }
+ )
# 尝试获取第二个屏幕
try:
second_monitor = win32api.MonitorFromPoint(
- (virtual_left + virtual_width - 1,
- virtual_top + virtual_height // 2),
- win32con.MONITOR_DEFAULTTONULL
+ (
+ virtual_left + virtual_width - 1,
+ virtual_top + virtual_height // 2,
+ ),
+ win32con.MONITOR_DEFAULTTONULL,
)
if second_monitor and second_monitor != primary_monitor:
second_info = win32api.GetMonitorInfo(second_monitor)
- screens.append({
- 'name': "屏幕 2",
- 'rect': second_info['Monitor'],
- 'work_rect': second_info['Work'],
- 'monitor': second_monitor
- })
+ screens.append(
+ {
+ "name": "屏幕 2",
+ "rect": second_info["Monitor"],
+ "work_rect": second_info["Work"],
+ "monitor": second_monitor,
+ }
+ )
except:
pass
@@ -3401,23 +3875,25 @@ def callback(hmonitor, hdc, lprect, lparam):
# 如果仍然没有找到屏幕,使用基本方案
screen_width = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
screen_height = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
- screens.append({
- 'name': "屏幕 1 (主)",
- 'rect': (0, 0, screen_width, screen_height),
- 'work_rect': (0, 0, screen_width, screen_height),
- 'monitor': None
- })
+ screens.append(
+ {
+ "name": "屏幕 1 (主)",
+ "rect": (0, 0, screen_width, screen_height),
+ "work_rect": (0, 0, screen_width, screen_height),
+ "monitor": None,
+ }
+ )
# 按照屏幕位置排序(从左到右)
- screens.sort(key=lambda x: x['rect'][0])
-
+ screens.sort(key=lambda x: x["rect"][0])
+
# 保存屏幕信息
self.screens = screens
-
+
# 返回屏幕名称列表
- screen_names = [screen['name'] for screen in screens]
+ screen_names = [screen["name"] for screen in screens]
return screen_names
-
+
except Exception as e:
print(f"获取屏幕列表失败: {str(e)}")
return ["主屏幕"] # 返回默认值
@@ -3427,56 +3903,64 @@ def create_environments(self):
try:
# 从设置中获取目录信息
settings = self.load_settings()
- cache_dir = settings.get('cache_dir', '')
+ cache_dir = settings.get("cache_dir", "")
shortcut_dir = self.shortcut_path
numbers = self.env_numbers.get().strip()
-
+
if not all([cache_dir, shortcut_dir, numbers]):
- messagebox.showwarning("警告", "请先在设置中填写缓存目录和快捷方式目录!")
+ messagebox.showwarning(
+ "警告", "请先在设置中填写缓存目录和快捷方式目录!"
+ )
return
-
+
# 确保目录存在
os.makedirs(cache_dir, exist_ok=True)
os.makedirs(shortcut_dir, exist_ok=True)
-
+
# 查找chrome可执行文件
chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
if not os.path.exists(chrome_path):
- chrome_path = r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
-
+ chrome_path = (
+ r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
+ )
+
if not os.path.exists(chrome_path):
messagebox.showerror("错误", "未找到Chrome安装路径!")
return
-
+
# 创建WScript.Shell对象
shell = win32com.client.Dispatch("WScript.Shell")
-
+
# 解析窗口编号
window_numbers = self.parse_window_numbers(numbers)
-
+
# 批量创建环境
for i in window_numbers:
# 创建数据目录 - 使用纯数字命名
data_dir_name = str(i) # 改回纯数字命名
-
+
# 使用os.path.join创建路径,然后统一转换为正斜杠格式
data_dir = os.path.join(cache_dir, data_dir_name)
- data_dir = data_dir.replace('\\', '/') # 统一使用正斜杠
-
+ data_dir = data_dir.replace("\\", "/") # 统一使用正斜杠
+
os.makedirs(data_dir, exist_ok=True)
-
+
# 创建快捷方式 - 仍然使用数字命名以便识别和分配端口
shortcut_path = os.path.join(shortcut_dir, f"{i}.lnk")
shortcut = shell.CreateShortCut(shortcut_path)
shortcut.TargetPath = chrome_path
- shortcut.Arguments = f'--user-data-dir="{data_dir}"' # 使用统一的正斜杠格式
+ shortcut.Arguments = (
+ f'--user-data-dir="{data_dir}"' # 使用统一的正斜杠格式
+ )
shortcut.WorkingDirectory = os.path.dirname(chrome_path)
shortcut.WindowStyle = 1 # 正常窗口
shortcut.IconLocation = f"{chrome_path},0"
shortcut.save()
-
- messagebox.showinfo("成功", f"已成功创建 {len(window_numbers)} 个Chrome环境!")
-
+
+ messagebox.showinfo(
+ "成功", f"已成功创建 {len(window_numbers)} 个Chrome环境!"
+ )
+
except Exception as e:
messagebox.showerror("错误", f"创建环境失败: {str(e)}")
@@ -3485,31 +3969,33 @@ def setup_hotkey_message_handler(self):
try:
# 获取窗口句柄
hwnd = int(self.root.winfo_id())
-
+
# 在这里我们添加额外的保障,确保快捷键设置有效
- if hasattr(self, 'current_shortcut') and self.current_shortcut:
+ if hasattr(self, "current_shortcut") and self.current_shortcut:
# 重新确认快捷键有效性
- if hasattr(self, 'shortcut_hook') and not self.shortcut_hook:
+ if hasattr(self, "shortcut_hook") and not self.shortcut_hook:
# 如果快捷键被清除,重新设置
self.set_shortcut(self.current_shortcut)
print(f"已重新设置快捷键: {self.current_shortcut}")
-
+
# 使用定时器检查热键状态
def check_hotkey():
try:
- if self.current_shortcut and keyboard.is_pressed(self.current_shortcut):
+ if self.current_shortcut and keyboard.is_pressed(
+ self.current_shortcut
+ ):
# 确保不会重复触发
keyboard.release(self.current_shortcut)
# 在主线程中执行toggle_sync
self.root.after(0, self.toggle_sync)
-
+
# 额外打印调试信息
print(f"检测到快捷键 {self.current_shortcut} 被按下")
except Exception as e:
print(f"检查热键状态失败: {str(e)}")
-
+
# 尝试恢复快捷键设置
- if hasattr(self, 'current_shortcut') and self.current_shortcut:
+ if hasattr(self, "current_shortcut") and self.current_shortcut:
try:
self.set_shortcut(self.current_shortcut)
print(f"已尝试恢复快捷键: {self.current_shortcut}")
@@ -3520,10 +4006,10 @@ def check_hotkey():
if not self.root.winfo_exists():
return
self.root.after(100, check_hotkey)
-
+
# 启动检查
check_hotkey()
-
+
except Exception as e:
print(f"设置热键消息处理失败: {str(e)}")
@@ -3536,7 +4022,7 @@ def show_settings_dialog(self):
settings_dialog.resizable(False, False)
settings_dialog.transient(self.root)
settings_dialog.grab_set()
-
+
# 设置图标
try:
icon_path = os.path.join(os.path.dirname(__file__), "app.ico")
@@ -3544,134 +4030,139 @@ def show_settings_dialog(self):
settings_dialog.iconbitmap(icon_path)
except Exception as e:
print(f"设置图标失败: {str(e)}")
-
+
# 创建内容和按钮的主框架
main_frame = ttk.Frame(settings_dialog)
main_frame.pack(fill=tk.BOTH, expand=True)
-
+
# 创建内容框架
content_frame = ttk.Frame(main_frame)
content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
-
+
# 目录设置框架
settings_frame = ttk.LabelFrame(content_frame, text="目录设置", padding=10)
settings_frame.pack(fill=tk.X, pady=5)
-
+
# 加载当前设置
settings = self.load_settings()
-
+
# 快捷方式目录
shortcut_frame = ttk.Frame(settings_frame)
shortcut_frame.pack(fill=tk.X, pady=5)
ttk.Label(shortcut_frame, text="谷歌多开快捷方式目录:").pack(side=tk.LEFT)
- shortcut_path_var = tk.StringVar(value=self.shortcut_path or settings.get('shortcut_path', ''))
+ shortcut_path_var = tk.StringVar(
+ value=self.shortcut_path or settings.get("shortcut_path", "")
+ )
shortcut_path_entry = ttk.Entry(shortcut_frame, textvariable=shortcut_path_var)
shortcut_path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.setup_right_click_menu(shortcut_path_entry)
ttk.Button(
shortcut_frame,
text="浏览",
- command=lambda: shortcut_path_var.set(filedialog.askdirectory(initialdir=shortcut_path_var.get() or os.getcwd()))
+ command=lambda: shortcut_path_var.set(
+ filedialog.askdirectory(
+ initialdir=shortcut_path_var.get() or os.getcwd()
+ )
+ ),
).pack(side=tk.LEFT)
-
+
# 缓存存放目录
cache_frame = ttk.Frame(settings_frame)
cache_frame.pack(fill=tk.X, pady=5)
ttk.Label(cache_frame, text="谷歌多开缓存存放目录:").pack(side=tk.LEFT)
- cache_dir_var = tk.StringVar(value=self.cache_dir or settings.get('cache_dir', ''))
+ cache_dir_var = tk.StringVar(
+ value=self.cache_dir or settings.get("cache_dir", "")
+ )
cache_dir_entry = ttk.Entry(cache_frame, textvariable=cache_dir_var)
cache_dir_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.setup_right_click_menu(cache_dir_entry)
ttk.Button(
cache_frame,
text="浏览",
- command=lambda: cache_dir_var.set(filedialog.askdirectory(initialdir=cache_dir_var.get() or os.getcwd()))
+ command=lambda: cache_dir_var.set(
+ filedialog.askdirectory(initialdir=cache_dir_var.get() or os.getcwd())
+ ),
).pack(side=tk.LEFT)
-
+
# 快捷方式图标资源目录
icon_frame = ttk.Frame(settings_frame)
icon_frame.pack(fill=tk.X, pady=5)
ttk.Label(icon_frame, text="快捷方式图标资源目录:").pack(side=tk.LEFT)
- icon_dir_var = tk.StringVar(value=self.icon_dir or settings.get('icon_dir', ''))
+ icon_dir_var = tk.StringVar(value=self.icon_dir or settings.get("icon_dir", ""))
icon_dir_entry = ttk.Entry(icon_frame, textvariable=icon_dir_var)
icon_dir_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.setup_right_click_menu(icon_dir_entry)
ttk.Button(
icon_frame,
text="浏览",
- command=lambda: icon_dir_var.set(filedialog.askdirectory(initialdir=icon_dir_var.get() or os.getcwd()))
+ command=lambda: icon_dir_var.set(
+ filedialog.askdirectory(initialdir=icon_dir_var.get() or os.getcwd())
+ ),
).pack(side=tk.LEFT)
-
+
# 功能设置
function_frame = ttk.LabelFrame(content_frame, text="功能设置", padding=10)
function_frame.pack(fill=tk.X, pady=5)
-
+
# 屏幕选择
screen_frame = ttk.Frame(function_frame)
screen_frame.pack(fill=tk.X, pady=5)
ttk.Label(screen_frame, text="屏幕选择:").pack(side=tk.LEFT)
-
+
# 更新屏幕列表
screen_options = self.update_screen_list()
if not screen_options:
screen_options = ["主屏幕"]
-
- screen_var = tk.StringVar(value=settings.get('screen_selection', ''))
+
+ screen_var = tk.StringVar(value=settings.get("screen_selection", ""))
screen_combo = ttk.Combobox(
- screen_frame,
- textvariable=screen_var,
- width=15,
- state="readonly"
+ screen_frame, textvariable=screen_var, width=15, state="readonly"
)
screen_combo.pack(side=tk.LEFT, padx=5)
- screen_combo['values'] = screen_options
-
+ screen_combo["values"] = screen_options
+
# 如果之前选过屏幕且还在列表中,则选中它
if screen_var.get() and screen_var.get() in screen_options:
screen_combo.set(screen_var.get())
# 否则默认选择第一个屏幕
elif screen_options:
screen_combo.current(0)
-
+
# 快捷键设置
shortcut_frame = ttk.Frame(function_frame)
shortcut_frame.pack(fill=tk.X, pady=5)
ttk.Label(shortcut_frame, text="快捷键设置:").pack(side=tk.LEFT)
shortcut_button = ttk.Button(
- shortcut_frame,
- text="设置快捷键",
- command=self.show_shortcut_dialog
+ shortcut_frame, text="设置快捷键", command=self.show_shortcut_dialog
)
shortcut_button.pack(side=tk.LEFT, padx=5)
-
+
# 底部按钮框架
button_frame = ttk.Frame(settings_dialog)
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=10)
-
+
cancel_button = ttk.Button(
- button_frame,
- text="取消",
- command=settings_dialog.destroy
+ button_frame, text="取消", command=settings_dialog.destroy
)
cancel_button.pack(side=tk.RIGHT, padx=5)
-
+
save_button = ttk.Button(
button_frame,
text="保存",
- style='Accent.TButton',
+ style="Accent.TButton",
command=lambda: self.save_settings_dialog(
settings_dialog,
shortcut_path_var.get(),
cache_dir_var.get(),
icon_dir_var.get(),
- screen_var.get()
- )
+ screen_var.get(),
+ ),
)
save_button.pack(side=tk.RIGHT, padx=5)
-
+
# 居中显示
self.center_window(settings_dialog)
-
+
# 为对话框中所有文本框添加右键菜单支持
def add_right_click_to_all_entries(parent):
"""为所有文本框添加右键菜单"""
@@ -3680,50 +4171,52 @@ def add_right_click_to_all_entries(parent):
self.setup_right_click_menu(child)
elif child.winfo_children():
add_right_click_to_all_entries(child)
-
+
# 在对话框创建完成后应用右键菜单
- settings_dialog.after(100, lambda: add_right_click_to_all_entries(settings_dialog))
+ settings_dialog.after(
+ 100, lambda: add_right_click_to_all_entries(settings_dialog)
+ )
def save_settings_dialog(self, dialog, shortcut_path, cache_dir, icon_dir, screen):
"""保存设置对话框中的设置"""
try:
print("保存前设置:", self.load_settings()) # 调试输出
-
+
# 更新当前实例变量,确保在本次会话中立即生效
self.shortcut_path = shortcut_path
self.cache_dir = cache_dir
self.icon_dir = icon_dir
self.screen_selection = screen
self.enable_cdp = True # 始终开启CDP
-
+
# 准备新设置
new_settings = {
- 'shortcut_path': shortcut_path,
- 'cache_dir': cache_dir,
- 'icon_dir': icon_dir,
- 'screen_selection': screen,
- 'enable_cdp': True # 始终开启CDP
+ "shortcut_path": shortcut_path,
+ "cache_dir": cache_dir,
+ "icon_dir": icon_dir,
+ "screen_selection": screen,
+ "enable_cdp": True, # 始终开启CDP
}
-
+
# 加载现有设置(不覆盖窗口等其他设置)
settings = self.load_settings()
settings.update(new_settings) # 更新设置
-
+
# 直接写入文件
- with open('settings.json', 'w', encoding='utf-8') as f:
+ with open("settings.json", "w", encoding="utf-8") as f:
json.dump(settings, f, ensure_ascii=False, indent=4)
-
+
# 如果页面上有路径输入框,更新它
- if hasattr(self, 'path_entry') and self.path_entry is not None:
+ if hasattr(self, "path_entry") and self.path_entry is not None:
self.path_entry.delete(0, tk.END)
self.path_entry.insert(0, shortcut_path)
-
+
print("保存后设置:", settings) # 调试输出
-
+
# 显示成功消息
messagebox.showinfo("成功", "设置已保存!")
dialog.destroy()
-
+
except Exception as e:
messagebox.showerror("错误", f"保存设置失败: {e}")
print(f"保存设置失败: {e}")
@@ -3732,37 +4225,37 @@ def center_window(self, window):
"""将窗口居中显示在屏幕上"""
# 先隐藏窗口,以便计算尺寸
window.withdraw()
-
+
# 更新窗口尺寸
window.update_idletasks()
-
+
# 获取屏幕尺寸
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
-
+
# 获取窗口尺寸
window_width = window.winfo_width()
window_height = window.winfo_height()
-
+
# 确保窗口尺寸正确
if window_width < 100 or window_height < 100:
# 使用窗口请求的尺寸
geometry = window.geometry()
- if 'x' in geometry and '+' in geometry:
- size_part = geometry.split('+')[0]
- if 'x' in size_part:
- parts = size_part.split('x')
+ if "x" in geometry and "+" in geometry:
+ size_part = geometry.split("+")[0]
+ if "x" in size_part:
+ parts = size_part.split("x")
if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
window_width = int(parts[0])
window_height = int(parts[1])
-
+
# 计算居中位置
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
-
+
# 设置窗口位置
window.geometry(f"{window_width}x{window_height}+{x}+{y}")
-
+
# 显示窗口
window.deiconify()
@@ -3770,13 +4263,13 @@ def keep_only_current_tab(self):
"""仅保留当前标签页,关闭所有选中窗口的其它标签页(高性能版)"""
# 立即显示视觉反馈
self.root.config(cursor="wait") # 修改光标为等待状态
-
+
# 获取选中的窗口
selected = []
try:
for item in self.window_list.get_children():
if self.window_list.set(item, "select") == "√":
- values = self.window_list.item(item)['values']
+ values = self.window_list.item(item)["values"]
if len(values) >= 5:
hwnd = int(values[4])
window_num = int(values[1])
@@ -3786,109 +4279,124 @@ def keep_only_current_tab(self):
self.root.config(cursor="") # 恢复光标
messagebox.showerror("错误", f"获取选中窗口失败: {str(e)}")
return
-
+
if not selected:
self.root.config(cursor="") # 恢复光标
messagebox.showinfo("提示", "请先选择要操作的窗口!")
return
-
+
# 如果debug_ports为空,尝试重建
- if not hasattr(self, 'debug_ports') or not self.debug_ports:
+ if not hasattr(self, "debug_ports") or not self.debug_ports:
print("未找到调试端口映射,尝试重建...")
- self.debug_ports = {window_num: 9222 + window_num for window_num, _ in selected}
-
+ self.debug_ports = {
+ window_num: 9222 + window_num for window_num, _ in selected
+ }
+
# 使用ThreadPoolExecutor在后台处理所有标签页操作
# 不再暂停同步功能,两者可以同时运行
def process_tabs():
try:
# 并行获取所有窗口的标签信息
port_to_tabs = {}
-
+
def get_tabs(window_data):
window_num, _ = window_data
if window_num in self.debug_ports:
port = self.debug_ports[window_num]
try:
# 使用更短的超时时间提高响应速度
- response = requests.get(f"http://localhost:{port}/json", timeout=0.5)
+ response = requests.get(
+ f"http://localhost:{port}/json", timeout=0.5
+ )
if response.status_code == 200:
tabs = response.json()
- page_tabs = [tab for tab in tabs if tab.get('type') == 'page']
+ page_tabs = [
+ tab for tab in tabs if tab.get("type") == "page"
+ ]
if len(page_tabs) > 1: # 如果只有一个标签页则不处理
return port, page_tabs, window_num
except Exception as e:
print(f"获取窗口{window_num}的标签页失败: {str(e)}")
return None
-
+
# 并行获取所有窗口的标签页
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
futures = []
for window_data in selected:
futures.append(executor.submit(get_tabs, window_data))
-
+
# 立即处理结果,不等待所有任务完成
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
port, tabs, window_num = result
port_to_tabs[port] = (tabs, window_num)
-
+
# 如果没有可操作的标签页,立即结束并恢复光标
if not port_to_tabs:
self.root.after(0, lambda: self.root.config(cursor=""))
return
-
+
# 准备并行关闭请求
close_requests = []
-
+
for port, (tabs, window_num) in port_to_tabs.items():
keep_tab = tabs[0] # 始终保留第一个标签
to_close = []
for tab in tabs:
- if tab.get('id') != keep_tab.get('id'):
- to_close.append((port, tab.get('id')))
+ if tab.get("id") != keep_tab.get("id"):
+ to_close.append((port, tab.get("id")))
close_requests.extend(to_close)
-
+
# 并行执行所有关闭请求
def close_tab(request):
port, tab_id = request
try:
- requests.get(f"http://localhost:{port}/json/close/{tab_id}", timeout=0.5)
+ requests.get(
+ f"http://localhost:{port}/json/close/{tab_id}", timeout=0.5
+ )
return True
except Exception as e:
print(f"关闭标签页失败: {str(e)}")
return False
-
+
# 使用更大的线程池来加速处理
with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor:
- futures = [executor.submit(close_tab, req) for req in close_requests]
+ futures = [
+ executor.submit(close_tab, req) for req in close_requests
+ ]
for future in concurrent.futures.as_completed(futures):
future.result() # 仅为了确保所有请求完成
-
+
# 操作完成后立即恢复光标
self.root.after(0, lambda: self.root.config(cursor=""))
-
+
except Exception as e:
print(f"处理标签页时出错: {str(e)}")
traceback.print_exc()
# 确保UI状态恢复
self.root.after(0, lambda: self.root.config(cursor=""))
- self.root.after(0, lambda: messagebox.showerror("错误", f"处理标签页时出错: {str(e)}"))
-
+ self.root.after(
+ 0,
+ lambda e=e: messagebox.showerror(
+ "错误", f"处理标签页时出错: {str(e)}"
+ ),
+ )
+
# 启动后台线程处理,不阻塞UI
threading.Thread(target=process_tabs, daemon=True).start()
-
+
def keep_only_new_tab(self):
"""仅保留新标签页,关闭所有选中窗口的其它标签页(高性能版)"""
# 立即显示视觉反馈
self.root.config(cursor="wait") # 修改光标为等待状态
-
+
# 获取选中的窗口
selected = []
try:
for item in self.window_list.get_children():
if self.window_list.set(item, "select") == "√":
- values = self.window_list.item(item)['values']
+ values = self.window_list.item(item)["values"]
if len(values) >= 5:
hwnd = int(values[4])
window_num = int(values[1])
@@ -3898,47 +4406,55 @@ def keep_only_new_tab(self):
self.root.config(cursor="") # 恢复光标
messagebox.showerror("错误", f"获取选中窗口失败: {str(e)}")
return
-
+
if not selected:
self.root.config(cursor="") # 恢复光标
messagebox.showinfo("提示", "请先选择要操作的窗口!")
return
-
+
# 如果debug_ports为空,尝试重建
- if not hasattr(self, 'debug_ports') or not self.debug_ports:
+ if not hasattr(self, "debug_ports") or not self.debug_ports:
print("未找到调试端口映射,尝试重建...")
- self.debug_ports = {window_num: 9222 + window_num for window_num, _ in selected}
-
+ self.debug_ports = {
+ window_num: 9222 + window_num for window_num, _ in selected
+ }
+
# 使用ThreadPoolExecutor在后台处理所有标签页操作
# 不再暂停同步功能,两者可以同时运行
def process_tabs():
try:
# 并行获取所有窗口的标签信息
window_tabs = {}
-
+
def get_tabs(window_data):
window_num, _ = window_data
if window_num in self.debug_ports:
port = self.debug_ports[window_num]
try:
# 使用更短的超时时间提高响应速度
- response = requests.get(f"http://localhost:{port}/json", timeout=0.5)
+ response = requests.get(
+ f"http://localhost:{port}/json", timeout=0.5
+ )
if response.status_code == 200:
tabs = response.json()
- page_tabs = [tab.get('id') for tab in tabs if tab.get('type') == 'page']
+ page_tabs = [
+ tab.get("id")
+ for tab in tabs
+ if tab.get("type") == "page"
+ ]
if page_tabs:
return port, page_tabs, window_num
except Exception as e:
print(f"获取窗口{window_num}的标签页失败: {str(e)}")
return None
-
+
# 并行获取所有窗口的标签页
valid_ports = []
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
futures = []
for window_data in selected:
futures.append(executor.submit(get_tabs, window_data))
-
+
# 立即处理结果,不等待所有任务完成
for future in concurrent.futures.as_completed(futures):
result = future.result()
@@ -3946,63 +4462,81 @@ def get_tabs(window_data):
port, tabs, window_num = result
window_tabs[port] = (tabs, window_num)
valid_ports.append(port)
-
+
# 如果没有可操作的标签页,立即结束并恢复光标
if not valid_ports:
self.root.after(0, lambda: self.root.config(cursor=""))
return
-
+
# 并行为所有窗口创建新标签页
created_tabs = {}
-
+
def create_new_tab(port_data):
port, window_num = port_data
try:
- requests.put(f"http://localhost:{port}/json/new?chrome://newtab/", timeout=0.5)
+ requests.put(
+ f"http://localhost:{port}/json/new?chrome://newtab/",
+ timeout=0.5,
+ )
return port, window_num, True
except Exception as e:
print(f"为窗口 {window_num} 创建新标签页失败: {str(e)}")
return port, window_num, False
-
+
# 并行创建新标签页
- port_to_window = {port: window_num for port, (_, window_num) in window_tabs.items()}
+ port_to_window = {
+ port: window_num for port, (_, window_num) in window_tabs.items()
+ }
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
- futures = [executor.submit(create_new_tab, (port, port_to_window[port])) for port in valid_ports]
+ futures = [
+ executor.submit(create_new_tab, (port, port_to_window[port]))
+ for port in valid_ports
+ ]
for future in concurrent.futures.as_completed(futures):
port, window_num, success = future.result()
if success:
created_tabs[window_num] = port
-
+
# 并行关闭原有标签页
def close_old_tabs(port_data):
port, tabs, window_num = port_data
for tab_id in tabs:
try:
- requests.get(f"http://localhost:{port}/json/close/{tab_id}", timeout=0.5)
+ requests.get(
+ f"http://localhost:{port}/json/close/{tab_id}",
+ timeout=0.5,
+ )
except Exception as e:
print(f"关闭窗口 {window_num} 的标签页失败: {str(e)}")
-
+
# 只有在成功创建了新标签页的窗口才关闭旧标签页
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
futures = []
for window_num, port in created_tabs.items():
tabs, _ = window_tabs[port]
- futures.append(executor.submit(close_old_tabs, (port, tabs, window_num)))
-
+ futures.append(
+ executor.submit(close_old_tabs, (port, tabs, window_num))
+ )
+
# 等待所有关闭操作完成
for future in concurrent.futures.as_completed(futures):
future.result()
-
+
# 操作完成后立即恢复光标
self.root.after(0, lambda: self.root.config(cursor=""))
-
+
except Exception as e:
print(f"处理标签页时出错: {str(e)}")
traceback.print_exc()
# 确保UI状态恢复
self.root.after(0, lambda: self.root.config(cursor=""))
- self.root.after(0, lambda: messagebox.showerror("错误", f"处理标签页时出错: {str(e)}"))
-
+ self.root.after(
+ 0,
+ lambda e=e: messagebox.showerror(
+ "错误", f"处理标签页时出错: {str(e)}"
+ ),
+ )
+
# 启动后台线程处理,不阻塞UI
threading.Thread(target=process_tabs, daemon=True).start()
@@ -4010,18 +4544,24 @@ def set_quick_url(self, url_template):
"""设置快捷网址模板到URL输入框"""
# 清空现有内容
self.url_entry.delete(0, tk.END)
-
+
# 根据不同的模板设置不同的URL组合
if url_template == "x.com" or url_template == "https://twitter.com":
self.url_entry.insert(0, "x.com")
- elif url_template == "discord.com/app" or url_template == "https://discord.com/channels/@me":
+ elif (
+ url_template == "discord.com/app"
+ or url_template == "https://discord.com/channels/@me"
+ ):
self.url_entry.insert(0, "discord.com/app")
- elif url_template == "mail.google.com" or url_template == "https://mail.google.com":
+ elif (
+ url_template == "mail.google.com"
+ or url_template == "https://mail.google.com"
+ ):
self.url_entry.insert(0, "mail.google.com")
else:
# 对于其他URL,直接使用传入的值
self.url_entry.insert(0, url_template)
-
+
# 自动触发批量打开网页
self.batch_open_urls()
@@ -4037,7 +4577,7 @@ def show_context_menu(self, event):
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
-
+
def cut_text(self):
"""剪切文本"""
if self.current_text_widget:
@@ -4045,7 +4585,7 @@ def cut_text(self):
self.current_text_widget.event_generate("<>")
except:
pass
-
+
def copy_text(self):
"""复制文本"""
if self.current_text_widget:
@@ -4053,7 +4593,7 @@ def copy_text(self):
self.current_text_widget.event_generate("<>")
except:
pass
-
+
def paste_text(self):
"""粘贴文本"""
if self.current_text_widget:
@@ -4061,12 +4601,14 @@ def paste_text(self):
self.current_text_widget.event_generate("<>")
except:
pass
-
+
def select_all_text(self):
"""全选文本"""
if self.current_text_widget:
try:
- if isinstance(self.current_text_widget, (tk.Entry, ttk.Entry, ttk.Combobox)):
+ if isinstance(
+ self.current_text_widget, (tk.Entry, ttk.Entry, ttk.Combobox)
+ ):
self.current_text_widget.select_range(0, tk.END)
self.current_text_widget.icursor(tk.END)
elif isinstance(self.current_text_widget, tk.Text):
@@ -4074,11 +4616,11 @@ def select_all_text(self):
self.current_text_widget.mark_set(tk.INSERT, tk.END)
except:
pass
-
+
def setup_right_click_menu(self, widget):
"""为文本框设置右键菜单"""
- widget.bind('', self.show_context_menu)
-
+ widget.bind("", self.show_context_menu)
+
def show_window_list_menu(self, event):
"""显示窗口列表的右键菜单"""
try:
@@ -4091,14 +4633,14 @@ def show_window_list_menu(self, event):
self.window_list_menu.post(event.x_root, event.y_root)
except Exception as e:
print(f"显示右键菜单失败: {str(e)}")
-
+
def close_selected_window(self):
"""关闭右键菜单选中的窗口"""
try:
- if hasattr(self, 'right_clicked_item') and self.right_clicked_item:
+ if hasattr(self, "right_clicked_item") and self.right_clicked_item:
item = self.right_clicked_item
# 从values中获取hwnd
- values = self.window_list.item(item)['values']
+ values = self.window_list.item(item)["values"]
if values and len(values) > 4:
hwnd = int(values[4])
# 检查窗口是否存在
@@ -4115,43 +4657,46 @@ def close_selected_window(self):
def sync_popups(self):
"""同步主窗口的弹出窗口到所有同步窗口,改进对网页浮动层的处理"""
try:
- if not self.is_sync or not self.master_window or not win32gui.IsWindow(self.master_window):
+ if (
+ not self.is_sync
+ or not self.master_window
+ or not win32gui.IsWindow(self.master_window)
+ ):
return
-
+
# 获取主窗口的所有弹出窗口
master_popups = self.get_chrome_popups(self.master_window)
if not master_popups:
return
-
+
# 获取主窗口位置
master_rect = win32gui.GetWindowRect(self.master_window)
master_x = master_rect[0]
master_y = master_rect[1]
-
+
# 针对每个主窗口的弹出窗口进行同步
for popup in master_popups:
try:
if not win32gui.IsWindow(popup):
continue
-
+
# 获取弹出窗口位置和大小
popup_rect = win32gui.GetWindowRect(popup)
popup_width = popup_rect[2] - popup_rect[0]
popup_height = popup_rect[3] - popup_rect[1]
-
+
# 检查窗口样式,确定是否为网页浮动层
style = win32gui.GetWindowLong(popup, win32con.GWL_STYLE)
ex_style = win32gui.GetWindowLong(popup, win32con.GWL_EXSTYLE)
-
- is_floating_layer = (
- (style & win32con.WS_POPUP) != 0 and
- (popup_width < 600 and popup_height < 600)
+
+ is_floating_layer = (style & win32con.WS_POPUP) != 0 and (
+ popup_width < 600 and popup_height < 600
)
-
+
# 计算相对于主窗口的位置
rel_x = popup_rect[0] - master_x
rel_y = popup_rect[1] - master_y
-
+
# 同步到所有其他窗口
for hwnd in self.sync_windows:
if hwnd != self.master_window and win32gui.IsWindow(hwnd):
@@ -4159,47 +4704,71 @@ def sync_popups(self):
sync_rect = win32gui.GetWindowRect(hwnd)
sync_x = sync_rect[0]
sync_y = sync_rect[1]
-
+
# 获取该窗口的所有弹出窗口
sync_popups = self.get_chrome_popups(hwnd)
-
+
# 寻找可能匹配的弹出窗口
target_title = win32gui.GetWindowText(popup)
best_match = None
best_score = 0
-
+
# 对网页浮动层和其他弹出窗口应用不同的匹配策略
if is_floating_layer:
# 为浮动层寻找相似的大小和位置
for sync_popup in sync_popups:
if not win32gui.IsWindow(sync_popup):
continue
-
- sync_style = win32gui.GetWindowLong(sync_popup, win32con.GWL_STYLE)
-
+
+ sync_style = win32gui.GetWindowLong(
+ sync_popup, win32con.GWL_STYLE
+ )
+
# 检查是否同样是弹出样式
if (sync_style & win32con.WS_POPUP) == 0:
continue
-
+
# 对于网页浮动层,主要基于尺寸和位置相似度匹配
sync_rect = win32gui.GetWindowRect(sync_popup)
sync_width = sync_rect[2] - sync_rect[0]
sync_height = sync_rect[3] - sync_rect[1]
-
+
# 尺寸相似度
- size_match = 1.0 - min(1.0, (abs(sync_width - popup_width) / max(popup_width, 1) +
- abs(sync_height - popup_height) / max(popup_height, 1)) / 2)
-
+ size_match = 1.0 - min(
+ 1.0,
+ (
+ abs(sync_width - popup_width)
+ / max(popup_width, 1)
+ + abs(sync_height - popup_height)
+ / max(popup_height, 1)
+ )
+ / 2,
+ )
+
# 相对位置相似度
sync_rel_x = sync_rect[0] - sync_x
sync_rel_y = sync_rect[1] - sync_y
- pos_match = 1.0 - min(1.0, (abs(sync_rel_x - rel_x) + abs(sync_rel_y - rel_y)) /
- max(sync_rect[2] - sync_rect[0] + sync_rect[3] - sync_rect[1], 1))
-
+ pos_match = 1.0 - min(
+ 1.0,
+ (
+ abs(sync_rel_x - rel_x)
+ + abs(sync_rel_y - rel_y)
+ )
+ / max(
+ sync_rect[2]
+ - sync_rect[0]
+ + sync_rect[3]
+ - sync_rect[1],
+ 1,
+ ),
+ )
+
# 综合得分,对于浮动层位置更重要
score = size_match * 0.4 + pos_match * 0.6
-
- if score > best_score and score > 0.6: # 提高匹配阈值
+
+ if (
+ score > best_score and score > 0.6
+ ): # 提高匹配阈值
best_score = score
best_match = sync_popup
else:
@@ -4207,37 +4776,48 @@ def sync_popups(self):
for sync_popup in sync_popups:
if not win32gui.IsWindow(sync_popup):
continue
-
+
sync_title = win32gui.GetWindowText(sync_popup)
# 计算标题相似度
- similarity = self.title_similarity(target_title, sync_title)
-
+ similarity = self.title_similarity(
+ target_title, sync_title
+ )
+
# 获取窗口大小相似度
sync_rect = win32gui.GetWindowRect(sync_popup)
sync_width = sync_rect[2] - sync_rect[0]
sync_height = sync_rect[3] - sync_rect[1]
- size_match = min(1.0, 1.0 - (abs(sync_width - popup_width) + abs(sync_height - popup_height)) /
- max(popup_width + popup_height, 1))
-
+ size_match = min(
+ 1.0,
+ 1.0
+ - (
+ abs(sync_width - popup_width)
+ + abs(sync_height - popup_height)
+ )
+ / max(popup_width + popup_height, 1),
+ )
+
# 计算总匹配分数
score = similarity * 0.7 + size_match * 0.3
if score > best_score and score > 0.5:
best_score = score
best_match = sync_popup
-
+
# 如果找到匹配的弹出窗口,调整其位置
if best_match:
# 计算新位置
new_x = sync_x + rel_x
new_y = sync_y + rel_y
-
+
# 设置窗口位置
win32gui.SetWindowPos(
best_match,
win32con.HWND_TOP,
- new_x, new_y,
- popup_width, popup_height,
- win32con.SWP_NOACTIVATE
+ new_x,
+ new_y,
+ popup_width,
+ popup_height,
+ win32con.SWP_NOACTIVATE,
)
elif is_floating_layer:
# 如果是浮动层但没找到匹配项,尝试通过模拟点击关闭和重新打开的方式同步
@@ -4246,7 +4826,7 @@ def sync_popups(self):
# 由于模拟点击可能较复杂,这里只记录日志
except Exception as e:
print(f"同步单个弹窗出错: {str(e)}")
-
+
except Exception as e:
print(f"同步弹窗过程出错: {str(e)}")
@@ -4255,7 +4835,7 @@ def setup_wheel_hook(self):
if self.wheel_hook_id:
# 如果已经有钩子,先卸载
self.unhook_wheel()
-
+
# 定义钩子回调函数
def wheel_proc(nCode, wParam, lParam):
try:
@@ -4263,34 +4843,47 @@ def wheel_proc(nCode, wParam, lParam):
if wParam == win32con.WM_MOUSEWHEEL and self.is_sync:
# 获取当前窗口
current_window = win32gui.GetForegroundWindow()
-
+
# 检查是否为主控窗口
is_master_window = current_window == self.master_window
-
+
# 获取主窗口的弹出窗口
master_popups = self.get_chrome_popups(self.master_window)
-
+
# 判断是否为主窗口的插件
is_master_plugin = current_window in master_popups
-
+
# 如果不是主控窗口也不是主窗口插件,直接放行事件
if not is_master_window and not is_master_plugin:
- return ctypes.windll.user32.CallNextHookEx(self.wheel_hook_id, nCode, wParam, ctypes.cast(lParam, ctypes.c_void_p))
-
+ return ctypes.windll.user32.CallNextHookEx(
+ self.wheel_hook_id,
+ nCode,
+ wParam,
+ ctypes.cast(lParam, ctypes.c_void_p),
+ )
+
# 获取窗口层次结构信息
try:
# 获取窗口类名和标题
window_class = win32gui.GetClassName(current_window)
window_title = win32gui.GetWindowText(current_window)
-
+
# 获取窗口样式
- style = win32gui.GetWindowLong(current_window, win32con.GWL_STYLE)
- ex_style = win32gui.GetWindowLong(current_window, win32con.GWL_EXSTYLE)
-
+ style = win32gui.GetWindowLong(
+ current_window, win32con.GWL_STYLE
+ )
+ ex_style = win32gui.GetWindowLong(
+ current_window, win32con.GWL_EXSTYLE
+ )
+
# 获取窗口进程ID
- _, process_id = win32process.GetWindowThreadProcessId(current_window)
- _, master_process_id = win32process.GetWindowThreadProcessId(self.master_window)
-
+ _, process_id = win32process.GetWindowThreadProcessId(
+ current_window
+ )
+ _, master_process_id = win32process.GetWindowThreadProcessId(
+ self.master_window
+ )
+
# 获取位置和尺寸
rect = win32gui.GetWindowRect(current_window)
width = rect[2] - rect[0]
@@ -4304,31 +4897,40 @@ def wheel_proc(nCode, wParam, lParam):
master_process_id = 0
width = 0
height = 0
-
+
# 检查是否为无法用键盘控制滚动的特殊窗口
is_uncontrollable_window = False
if "Chrome_RenderWidgetHostHWND" in window_class:
is_uncontrollable_window = True
-
+
# 检查是否与Chrome相关
is_chrome_window = (
- "Chrome_" in window_class or
- "Chromium_" in window_class
+ "Chrome_" in window_class or "Chromium_" in window_class
)
-
+
# 检查是否是插件窗口
is_plugin_window = is_master_plugin
-
+
if is_master_plugin:
- print(f"识别到主窗口插件: {window_title}, 句柄: {current_window}")
-
+ print(
+ f"识别到主窗口插件: {window_title}, 句柄: {current_window}"
+ )
+
# 检查Ctrl键状态 - 如果按下Ctrl则不拦截事件(保留缩放功能)
- ctrl_pressed = ctypes.windll.user32.GetKeyState(win32con.VK_CONTROL) & 0x8000 != 0
+ ctrl_pressed = (
+ ctypes.windll.user32.GetKeyState(win32con.VK_CONTROL) & 0x8000
+ != 0
+ )
if ctrl_pressed and is_chrome_window:
print("检测到Ctrl键按下,不拦截滚轮事件(保留缩放功能)")
# 不拦截,让事件继续传递给Chrome处理缩放
- return ctypes.windll.user32.CallNextHookEx(self.wheel_hook_id, nCode, wParam, ctypes.cast(lParam, ctypes.c_void_p))
-
+ return ctypes.windll.user32.CallNextHookEx(
+ self.wheel_hook_id,
+ nCode,
+ wParam,
+ ctypes.cast(lParam, ctypes.c_void_p),
+ )
+
# 只处理Chrome相关窗口且不是无法控制的特殊窗口
if is_chrome_window and not is_uncontrollable_window:
# 防止过于频繁触发
@@ -4336,68 +4938,93 @@ def wheel_proc(nCode, wParam, lParam):
if current_time - self.last_wheel_time < self.wheel_threshold:
# 阻止事件继续传递(返回1)
return 1
-
+
self.last_wheel_time = current_time
-
+
# 从MSLLHOOKSTRUCT结构体中获取滚轮增量
- wheel_delta = ctypes.c_short(lParam.contents.mouseData >> 16).value
-
+ wheel_delta = ctypes.c_short(
+ lParam.contents.mouseData >> 16
+ ).value
+
# 标准化滚轮增量
normalized_delta = self.normalize_wheel_delta(wheel_delta)
-
+
# 只同步到其他同步窗口,不包括主窗口自身
windows_to_sync = self.sync_windows
-
+
# 获取鼠标位置
mouse_x, mouse_y = lParam.contents.pt.x, lParam.contents.pt.y
- print(f"拦截滚轮事件: 窗口={current_window}, 类型={'主窗口' if is_master_window else '主窗口插件' if is_master_plugin else '其他'}, wheel_delta={wheel_delta}")
-
+ print(
+ f"拦截滚轮事件: 窗口={current_window}, 类型={'主窗口' if is_master_window else '主窗口插件' if is_master_plugin else '其他'}, wheel_delta={wheel_delta}"
+ )
+
# 如果是插件窗口,同步到其他窗口,但允许原始事件继续传递
if is_plugin_window:
# 向同步窗口发送模拟滚动
if windows_to_sync:
- print(f"主窗口插件滚轮事件,同步到其他{len(windows_to_sync)}个窗口")
- self.sync_specified_windows_scroll(normalized_delta, windows_to_sync)
-
+ print(
+ f"主窗口插件滚轮事件,同步到其他{len(windows_to_sync)}个窗口"
+ )
+ self.sync_specified_windows_scroll(
+ normalized_delta, windows_to_sync
+ )
+
# 允许原始事件继续传递,这样插件窗口本身可以正常滚动
print("允许插件窗口原始滚轮事件继续传递")
- return ctypes.windll.user32.CallNextHookEx(self.wheel_hook_id, nCode, wParam, ctypes.cast(lParam, ctypes.c_void_p))
-
+ return ctypes.windll.user32.CallNextHookEx(
+ self.wheel_hook_id,
+ nCode,
+ wParam,
+ ctypes.cast(lParam, ctypes.c_void_p),
+ )
+
# 主窗口:拦截原始事件,向同步窗口发送模拟滚动
else:
# 包括主窗口在内的所有窗口
all_windows = [self.master_window] + self.sync_windows
print(f"主窗口滚轮事件,同步到所有{len(all_windows)}个窗口")
- self.sync_specified_windows_scroll(normalized_delta, all_windows)
+ self.sync_specified_windows_scroll(
+ normalized_delta, all_windows
+ )
# 拦截原始滚轮事件
return 1
-
+
# 其他消息或非Chrome窗口,继续传递事件
- return ctypes.windll.user32.CallNextHookEx(self.wheel_hook_id, nCode, wParam, ctypes.cast(lParam, ctypes.c_void_p))
-
+ return ctypes.windll.user32.CallNextHookEx(
+ self.wheel_hook_id,
+ nCode,
+ wParam,
+ ctypes.cast(lParam, ctypes.c_void_p),
+ )
+
except Exception as e:
print(f"滚轮钩子处理出错: {str(e)}")
# 异常情况下继续传递事件
- return ctypes.windll.user32.CallNextHookEx(self.wheel_hook_id, nCode, wParam, ctypes.cast(lParam, ctypes.c_void_p))
-
+ return ctypes.windll.user32.CallNextHookEx(
+ self.wheel_hook_id,
+ nCode,
+ wParam,
+ ctypes.cast(lParam, ctypes.c_void_p),
+ )
+
# 创建钩子回调函数
self.wheel_hook_proc = ctypes.WINFUNCTYPE(
ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.POINTER(MSLLHOOKSTRUCT)
)(wheel_proc)
-
+
try:
# 安装钩子 - 修复整数溢出错误,直接使用0而不是GetModuleHandle(None)
self.wheel_hook_id = ctypes.windll.user32.SetWindowsHookExW(
win32con.WH_MOUSE_LL,
self.wheel_hook_proc,
0, # 直接使用0替代win32api.GetModuleHandle(None)
- 0
+ 0,
)
-
+
if not self.wheel_hook_id:
error = ctypes.windll.kernel32.GetLastError()
raise Exception(f"安装滚轮钩子失败,错误码: {error}")
-
+
except Exception as e:
print(f"安装滚轮钩子时出错: {str(e)}")
# 确保标记为None,以便其他部分代码知道钩子未成功安装
@@ -4418,12 +5045,12 @@ def unhook_wheel(self):
finally:
self.wheel_hook_id = None
self.wheel_hook_proc = None
-
+
def normalize_wheel_delta(self, delta, is_plugin=False):
"""标准化滚轮增量值 - 使用适中的缩放系数"""
# 检查是否可能来自触控板(通常有小数或不规则值)
abs_delta = abs(delta)
-
+
# 使用适中的缩放系数,不区分窗口类型
if abs_delta < 40: # 很小的值,可能是精确触控板
normalized = delta * 0.20 # 适中系数
@@ -4431,7 +5058,7 @@ def normalize_wheel_delta(self, delta, is_plugin=False):
normalized = delta * 0.25 # 适中系数
else: # 标准鼠标滚轮
normalized = delta * 0.30 # 适中系数
-
+
# 保持方向一致,但标准化大小
direction = 1 if delta > 0 else -1
# 标准增量设为中等值,从120降至50
@@ -4444,40 +5071,40 @@ def sync_specified_windows_scroll(self, normalized_delta, window_list):
# 确定滚动方向和大小
is_scroll_up = normalized_delta > 0
abs_delta = abs(normalized_delta)
-
+
# 遍历所有需要同步的窗口
for hwnd in window_list:
try:
if not win32gui.IsWindow(hwnd):
continue
-
+
# 根据滚动大小决定使用不同的按键组合
if abs_delta < 40: # 小幅度滚动
key = win32con.VK_UP if is_scroll_up else win32con.VK_DOWN
repeat = max(1, min(int(abs_delta / 20), 2))
-
+
for _ in range(repeat):
win32gui.PostMessage(hwnd, win32con.WM_KEYDOWN, key, 0)
win32gui.PostMessage(hwnd, win32con.WM_KEYUP, key, 0)
-
+
elif abs_delta < 80: # 中等幅度滚动
# 使用Page键
key = win32con.VK_PRIOR if is_scroll_up else win32con.VK_NEXT
win32gui.PostMessage(hwnd, win32con.WM_KEYDOWN, key, 0)
win32gui.PostMessage(hwnd, win32con.WM_KEYUP, key, 0)
-
+
else: # 大幅度滚动
# 使用多个Page键
key = win32con.VK_PRIOR if is_scroll_up else win32con.VK_NEXT
repeat = min(int(abs_delta / 100) + 1, 2)
-
+
for _ in range(repeat):
win32gui.PostMessage(hwnd, win32con.WM_KEYDOWN, key, 0)
win32gui.PostMessage(hwnd, win32con.WM_KEYUP, key, 0)
-
+
except Exception as e:
print(f"向窗口 {hwnd} 发送滚动事件失败: {str(e)}")
-
+
except Exception as e:
print(f"同步滚动出错: {str(e)}")
@@ -4485,7 +5112,7 @@ def sync_all_windows_scroll(self, normalized_delta):
"""同步所有窗口的滚动 - 设置适中的滚动幅度"""
# 遍历所有窗口,包括主窗口
all_windows = [self.master_window] + self.sync_windows
-
+
# 调用指定窗口滚动函数
self.sync_specified_windows_scroll(normalized_delta, all_windows)
@@ -4493,7 +5120,7 @@ def normalize_path(self, path):
"""标准化路径格式,统一使用正斜杠,便于比较"""
if not path:
return ""
- return os.path.normpath(path).lower().replace('\\', '/')
+ return os.path.normpath(path).lower().replace("\\", "/")
def input_random_number(self):
"""在选中的窗口中输入随机数字"""
@@ -4502,32 +5129,32 @@ def input_random_number(self):
selected_windows = []
for item in self.window_list.get_children():
if self.window_list.set(item, "select") == "√":
- hwnd = int(self.window_list.item(item)['values'][-1])
+ hwnd = int(self.window_list.item(item)["values"][-1])
selected_windows.append(hwnd)
-
+
if not selected_windows:
messagebox.showwarning("警告", "请先选择要操作的窗口!")
return
-
+
# 获取范围值
min_str = self.random_min_value.get().strip()
max_str = self.random_max_value.get().strip()
-
+
if not min_str or not max_str:
messagebox.showwarning("警告", "请输入有效的范围值!")
return
-
+
# 确定是整数还是小数
- is_float = '.' in min_str or '.' in max_str
-
+ is_float = "." in min_str or "." in max_str
+
try:
if is_float:
min_val = float(min_str)
max_val = float(max_str)
# 获取小数位数
decimal_places = max(
- len(min_str.split('.')[-1]) if '.' in min_str else 0,
- len(max_str.split('.')[-1]) if '.' in max_str else 0
+ len(min_str.split(".")[-1]) if "." in min_str else 0,
+ len(max_str.split(".")[-1]) if "." in max_str else 0,
)
decimal_places = min(decimal_places, 10) # 最多10位小数
else:
@@ -4536,40 +5163,48 @@ def input_random_number(self):
except ValueError:
messagebox.showerror("错误", "请输入有效的数字范围!")
return
-
+
# 获取选项
overwrite = self.random_overwrite.get()
delayed = self.random_delayed.get()
-
- print(f"准备为{len(selected_windows)}个窗口生成随机数 (范围: {min_val}-{max_val}, 覆盖: {overwrite}, 延迟: {delayed})")
-
+
+ print(
+ f"准备为{len(selected_windows)}个窗口生成随机数 (范围: {min_val}-{max_val}, 覆盖: {overwrite}, 延迟: {delayed})"
+ )
+
# 为每个选中的窗口输入随机数
for hwnd in selected_windows:
# 为每个窗口单独生成一个随机数
if is_float:
# 生成随机小数,最多10位小数
- random_number = round(random.uniform(min_val, max_val), decimal_places)
+ random_number = round(
+ random.uniform(min_val, max_val), decimal_places
+ )
# 转为字符串,保留指定小数位
random_text = f"{random_number:.{decimal_places}f}"
# 去除尾部多余的0
- if '.' in random_text:
- random_text = random_text.rstrip('0').rstrip('.') if '.' in random_text else random_text
+ if "." in random_text:
+ random_text = (
+ random_text.rstrip("0").rstrip(".")
+ if "." in random_text
+ else random_text
+ )
else:
random_number = random.randint(min_val, max_val)
random_text = str(random_number)
-
+
print(f"窗口 {hwnd} 的随机数: {random_text}")
-
+
# 激活窗口
try:
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.1) # 等待窗口获得焦点
-
+
# 如果选择覆盖原有内容,先全选文本
if overwrite:
- keyboard.press_and_release('ctrl+a')
+ keyboard.press_and_release("ctrl+a")
time.sleep(0.05)
-
+
# 输入随机数
if delayed:
# 模拟真人输入,逐字输入
@@ -4580,11 +5215,11 @@ def input_random_number(self):
else:
# 直接输入整个字符串
keyboard.write(random_text)
-
+
time.sleep(0.2) # 等待短暂时间再处理下一个窗口
except Exception as e:
print(f"向窗口 {hwnd} 输入随机数时出错: {str(e)}")
-
+
except Exception as e:
messagebox.showerror("错误", f"输入随机数时出错: {str(e)}")
@@ -4596,7 +5231,7 @@ def show_random_number_dialog(self):
dialog.transient(self.root)
dialog.grab_set()
dialog.resizable(False, False)
-
+
# 设置图标
try:
icon_path = os.path.join(os.path.dirname(__file__), "app.ico")
@@ -4604,78 +5239,77 @@ def show_random_number_dialog(self):
dialog.iconbitmap(icon_path)
except Exception as e:
print(f"设置对话框图标失败: {str(e)}")
-
+
# 居中显示
self.center_window(dialog)
-
+
# 主框架
main_frame = ttk.Frame(dialog, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
-
+
# 范围输入区域
range_frame = ttk.LabelFrame(main_frame, text="数字范围", padding=10)
range_frame.pack(fill=tk.X, pady=(0, 10))
-
+
range_inner_frame = ttk.Frame(range_frame)
range_inner_frame.pack(fill=tk.X)
-
+
ttk.Label(range_inner_frame, text="最小值:").pack(side=tk.LEFT)
- min_entry = ttk.Entry(range_inner_frame, width=10, textvariable=self.random_min_value)
+ min_entry = ttk.Entry(
+ range_inner_frame, width=10, textvariable=self.random_min_value
+ )
min_entry.pack(side=tk.LEFT, padx=(5, 15))
self.setup_right_click_menu(min_entry)
-
+
ttk.Label(range_inner_frame, text="最大值:").pack(side=tk.LEFT)
- max_entry = ttk.Entry(range_inner_frame, width=10, textvariable=self.random_max_value)
+ max_entry = ttk.Entry(
+ range_inner_frame, width=10, textvariable=self.random_max_value
+ )
max_entry.pack(side=tk.LEFT, padx=5)
self.setup_right_click_menu(max_entry)
-
+
# 选项区域
options_frame = ttk.LabelFrame(main_frame, text="输入选项", padding=10)
options_frame.pack(fill=tk.X, pady=(0, 15))
-
+
options_inner_frame = ttk.Frame(options_frame)
options_inner_frame.pack(fill=tk.X)
-
+
overwrite_var = tk.BooleanVar(value=True)
-
+
overwrite_check = ttk.Checkbutton(
- options_inner_frame,
- text="覆盖原有内容",
- variable=self.random_overwrite
+ options_inner_frame, text="覆盖原有内容", variable=self.random_overwrite
)
overwrite_check.pack(anchor=tk.W, pady=5)
-
+
delayed_check = ttk.Checkbutton(
- options_inner_frame,
- text="模拟人工输入(逐字输入并添加延迟)",
- variable=self.random_delayed
+ options_inner_frame,
+ text="模拟人工输入(逐字输入并添加延迟)",
+ variable=self.random_delayed,
)
delayed_check.pack(anchor=tk.W)
-
+
# 按钮区域
buttons_frame = ttk.Frame(main_frame)
buttons_frame.pack(fill=tk.X)
-
- ttk.Button(
- buttons_frame,
- text="取消",
- command=dialog.destroy,
- width=10
- ).pack(side=tk.RIGHT, padx=5)
-
+
+ ttk.Button(buttons_frame, text="取消", command=dialog.destroy, width=10).pack(
+ side=tk.RIGHT, padx=5
+ )
+
ttk.Button(
buttons_frame,
text="开始输入",
command=lambda: self.run_random_input(dialog),
- style='Accent.TButton',
- width=10
+ style="Accent.TButton",
+ width=10,
).pack(side=tk.RIGHT, padx=5)
-
+
def run_random_input(self, dialog):
"""执行随机数输入操作并关闭对话框"""
dialog.destroy()
self.input_random_number()
-
+
def show_text_input_dialog(self):
"""显示指定文本输入对话框"""
dialog = tk.Toplevel(self.root)
@@ -4684,7 +5318,7 @@ def show_text_input_dialog(self):
dialog.transient(self.root)
dialog.grab_set()
dialog.resizable(False, False)
-
+
# 设置图标
try:
icon_path = os.path.join(os.path.dirname(__file__), "app.ico")
@@ -4692,33 +5326,33 @@ def show_text_input_dialog(self):
dialog.iconbitmap(icon_path)
except Exception as e:
print(f"设置对话框图标失败: {str(e)}")
-
+
# 居中显示
self.center_window(dialog)
-
+
# 主框架
main_frame = ttk.Frame(dialog, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
-
+
# 文本文件选择区域
file_frame = ttk.LabelFrame(main_frame, text="文本文件", padding=10)
file_frame.pack(fill=tk.X, pady=(0, 10))
-
+
file_path_var = tk.StringVar()
file_path_entry = ttk.Entry(file_frame, textvariable=file_path_var, width=40)
file_path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
self.setup_right_click_menu(file_path_entry)
-
+
def browse_file():
filepath = filedialog.askopenfilename(
title="选择文本文件",
- filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
+ filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
)
if filepath:
file_path_var.set(filepath)
# 预览文本文件内容
try:
- with open(filepath, 'r', encoding='utf-8') as f:
+ with open(filepath, "r", encoding="utf-8") as f:
preview_text = "\n".join(f.read().splitlines()[:10])
if len(f.read().splitlines()) > 10:
preview_text += "\n..."
@@ -4726,98 +5360,88 @@ def browse_file():
preview.insert(tk.END, preview_text)
except Exception as e:
messagebox.showerror("错误", f"读取文件失败: {str(e)}")
-
- ttk.Button(
- file_frame,
- text="浏览...",
- command=browse_file
- ).pack(side=tk.RIGHT)
-
+
+ ttk.Button(file_frame, text="浏览...", command=browse_file).pack(side=tk.RIGHT)
+
# 文本预览区域
preview_frame = ttk.LabelFrame(main_frame, text="文件内容预览", padding=10)
preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
-
+
preview = tk.Text(preview_frame, height=6, width=50, wrap=tk.WORD)
preview.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
-
- preview_scrollbar = ttk.Scrollbar(preview_frame, orient=tk.VERTICAL, command=preview.yview)
+
+ preview_scrollbar = ttk.Scrollbar(
+ preview_frame, orient=tk.VERTICAL, command=preview.yview
+ )
preview_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
preview.configure(yscrollcommand=preview_scrollbar.set)
-
+
# 输入方式选择
input_method_frame = ttk.Frame(main_frame)
input_method_frame.pack(fill=tk.X, pady=(0, 10))
-
+
input_method = tk.StringVar(value="sequential")
-
+
ttk.Radiobutton(
input_method_frame,
text="顺序输入",
variable=input_method,
- value="sequential"
+ value="sequential",
).pack(side=tk.LEFT, padx=(0, 15))
-
+
ttk.Radiobutton(
- input_method_frame,
- text="随机输入",
- variable=input_method,
- value="random"
+ input_method_frame, text="随机输入", variable=input_method, value="random"
).pack(side=tk.LEFT)
-
+
# 选项区域
options_frame = ttk.Frame(main_frame)
options_frame.pack(fill=tk.X, pady=(0, 10))
-
+
overwrite_var = tk.BooleanVar(value=True)
-
+
overwrite_check = ttk.Checkbutton(
- options_frame,
- text="覆盖原有内容",
- variable=overwrite_var
+ options_frame, text="覆盖原有内容", variable=overwrite_var
)
overwrite_check.pack(side=tk.LEFT, padx=(0, 10))
-
+
# 按钮区域
buttons_frame = ttk.Frame(main_frame)
buttons_frame.pack(fill=tk.X)
-
- ttk.Button(
- buttons_frame,
- text="取消",
- command=dialog.destroy,
- width=10
- ).pack(side=tk.RIGHT, padx=5)
-
+
+ ttk.Button(buttons_frame, text="取消", command=dialog.destroy, width=10).pack(
+ side=tk.RIGHT, padx=5
+ )
+
ttk.Button(
buttons_frame,
text="开始输入",
command=lambda: self.execute_text_input(
- dialog,
- file_path_var.get(),
- input_method.get(),
- overwrite_var.get(),
- False # 永远不使用延迟输入
+ dialog,
+ file_path_var.get(),
+ input_method.get(),
+ overwrite_var.get(),
+ False, # 永远不使用延迟输入
),
- style='Accent.TButton',
- width=10
+ style="Accent.TButton",
+ width=10,
).pack(side=tk.RIGHT, padx=5)
-
+
def execute_text_input(self, dialog, file_path, input_method, overwrite, delayed):
"""执行文本输入操作"""
if not file_path:
messagebox.showwarning("警告", "请选择文本文件!")
return
-
+
if not os.path.exists(file_path):
messagebox.showerror("错误", "文件不存在!")
return
-
+
# 关闭对话框
dialog.destroy()
-
+
# 调用文本输入功能
self.input_text_from_file(file_path, input_method, overwrite, delayed)
-
+
def input_text_from_file(self, file_path, input_method, overwrite, delayed):
"""从文件输入文本到选中的窗口"""
try:
@@ -4825,30 +5449,30 @@ def input_text_from_file(self, file_path, input_method, overwrite, delayed):
selected_windows = []
for item in self.window_list.get_children():
if self.window_list.set(item, "select") == "√":
- hwnd = int(self.window_list.item(item)['values'][-1])
+ hwnd = int(self.window_list.item(item)["values"][-1])
selected_windows.append(hwnd)
-
+
if not selected_windows:
messagebox.showwarning("警告", "请先选择要操作的窗口!")
return
-
+
# 读取文本文件
try:
- with open(file_path, 'r', encoding='utf-8') as f:
+ with open(file_path, "r", encoding="utf-8") as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
except UnicodeDecodeError:
# 尝试其它编码
try:
- with open(file_path, 'r', encoding='gbk') as f:
+ with open(file_path, "r", encoding="gbk") as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
except Exception as e:
messagebox.showerror("错误", f"读取文件失败: {str(e)}")
return
-
+
if not lines:
messagebox.showwarning("警告", "文本文件为空!")
return
-
+
# 准备文本行
if input_method == "random":
# 为每个窗口随机选择一行
@@ -4856,11 +5480,11 @@ def input_text_from_file(self, file_path, input_method, overwrite, delayed):
# 如果窗口数量大于文本行数,循环使用
if len(selected_windows) > len(lines):
lines = lines * (len(selected_windows) // len(lines) + 1)
-
+
# 确保文本行至少与窗口数量一样多
while len(lines) < len(selected_windows):
lines.extend(lines)
-
+
# 输入进度窗口
progress_dialog = tk.Toplevel(self.root)
progress_dialog.title("文本输入")
@@ -4868,7 +5492,7 @@ def input_text_from_file(self, file_path, input_method, overwrite, delayed):
progress_dialog.transient(self.root)
progress_dialog.grab_set()
progress_dialog.resizable(False, False)
-
+
# 设置图标
try:
icon_path = os.path.join(os.path.dirname(__file__), "app.ico")
@@ -4876,57 +5500,61 @@ def input_text_from_file(self, file_path, input_method, overwrite, delayed):
progress_dialog.iconbitmap(icon_path)
except Exception as e:
print(f"设置进度对话框图标失败: {str(e)}")
-
+
self.center_window(progress_dialog)
-
+
progress_label = ttk.Label(progress_dialog, text="正在准备输入...")
progress_label.pack(pady=(20, 10))
-
- progress_bar = ttk.Progressbar(progress_dialog, mode='determinate', length=350)
+
+ progress_bar = ttk.Progressbar(
+ progress_dialog, mode="determinate", length=350
+ )
progress_bar.pack(pady=(0, 20))
-
+
progress_dialog.update()
-
+
try:
# 为每个窗口输入文本
for i, hwnd in enumerate(selected_windows):
# 更新进度
progress = int((i / len(selected_windows)) * 100)
- progress_bar['value'] = progress
+ progress_bar["value"] = progress
text_line = lines[i % len(lines)]
- progress_label.config(text=f"正在输入 ({i+1}/{len(selected_windows)}): {text_line[:30]}...")
+ progress_label.config(
+ text=f"正在输入 ({i + 1}/{len(selected_windows)}): {text_line[:30]}..."
+ )
progress_dialog.update()
-
+
try:
# 激活窗口
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.1) # 等待窗口获得焦点
-
+
# 如果选择覆盖原有内容,先全选文本
if overwrite:
- keyboard.press_and_release('ctrl+a')
+ keyboard.press_and_release("ctrl+a")
time.sleep(0.05)
-
+
# 输入文本 - 直接输入整个字符串
keyboard.write(text_line)
-
+
time.sleep(0.2) # 等待短暂时间再处理下一个窗口
except Exception as e:
print(f"向窗口 {hwnd} 输入文本时出错: {str(e)}")
continue
-
+
# 完成后更新进度
- progress_bar['value'] = 100
+ progress_bar["value"] = 100
progress_label.config(text="输入完成!")
progress_dialog.update()
-
+
# 短暂延迟后关闭进度窗口
self.root.after(1000, progress_dialog.destroy)
-
+
except Exception as e:
progress_dialog.destroy()
messagebox.showerror("错误", f"输入文本时出错: {str(e)}")
-
+
except Exception as e:
messagebox.showerror("错误", f"操作失败: {str(e)}")
@@ -4937,35 +5565,37 @@ def show_chrome_settings_tip(self):
tip_dialog.geometry("420x255")
tip_dialog.transient(self.root)
tip_dialog.grab_set()
-
+
# 设置为模态对话框
tip_dialog.focus_set()
-
+
# 提示信息
- tip_text = "如果窗口关闭后,Chrome仍在后台运行(右下角系统托盘区域里有多个chrome图标),请批量在浏览器设置页面取消后台运行:\n\n1. 批量打开Chrome浏览器\n2. 在地址栏输入:chrome://settings/system,或者进入设置-系统\n3. 找到\"关闭 Google Chrome 后继续运行后台应用\"选项\n4. 关闭该选项"
-
- tip_label = ttk.Label(tip_dialog, text=tip_text, justify=tk.LEFT, wraplength=380)
+ tip_text = '如果窗口关闭后,Chrome仍在后台运行(右下角系统托盘区域里有多个chrome图标),请批量在浏览器设置页面取消后台运行:\n\n1. 批量打开Chrome浏览器\n2. 在地址栏输入:chrome://settings/system,或者进入设置-系统\n3. 找到"关闭 Google Chrome 后继续运行后台应用"选项\n4. 关闭该选项'
+
+ tip_label = ttk.Label(
+ tip_dialog, text=tip_text, justify=tk.LEFT, wraplength=380
+ )
tip_label.pack(pady=20, padx=20)
-
+
# 不再显示的选项
dont_show_var = tk.BooleanVar(value=False)
dont_show_check = ttk.Checkbutton(
- tip_dialog,
- text="下次不再显示",
- variable=dont_show_var
+ tip_dialog, text="下次不再显示", variable=dont_show_var
)
dont_show_check.pack(pady=10)
-
+
# 确定按钮
def on_ok():
if dont_show_var.get():
self.show_chrome_tip = False
self.save_tip_settings()
tip_dialog.destroy()
-
- ok_button = ttk.Button(tip_dialog, text="确定", command=on_ok, style='Accent.TButton')
+
+ ok_button = ttk.Button(
+ tip_dialog, text="确定", command=on_ok, style="Accent.TButton"
+ )
ok_button.pack(pady=10)
-
+
# 居中显示
self.center_window(tip_dialog)
@@ -4974,16 +5604,16 @@ def save_tip_settings(self):
try:
# 强制设置为False - 确保选择"下次不再显示"后永远不再显示
self.show_chrome_tip = False
-
+
# 直接设置当前实例的设置
- self.settings['show_chrome_tip'] = False
-
+ self.settings["show_chrome_tip"] = False
+
# 立即保存到settings.json
- with open('settings.json', 'w', encoding='utf-8') as f:
+ with open("settings.json", "w", encoding="utf-8") as f:
json.dump(self.settings, f, ensure_ascii=False, indent=4)
-
+
print(f"成功保存Chrome提示设置: show_chrome_tip = {self.show_chrome_tip}")
-
+
except Exception as e:
print(f"保存提示设置失败: {str(e)}")
messagebox.showerror("设置保存失败", f"无法保存提示设置: {str(e)}")
@@ -4992,14 +5622,120 @@ def load_settings(self) -> dict:
# 加载设置
settings = {}
try:
- if os.path.exists('settings.json'):
- with open('settings.json', 'r', encoding='utf-8') as f:
+ if os.path.exists("settings.json"):
+ with open("settings.json", "r", encoding="utf-8") as f:
settings = json.load(f)
except Exception as e:
print(f"加载设置失败: {str(e)}")
-
+
return settings
+ def generate_color_icon(self, window_number, size=48):
+ """生成带数字的彩色图标"""
+ try:
+ if not hasattr(self, "icon_dir") or not self.icon_dir:
+ self.icon_dir = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "icons"
+ )
+ if not os.path.exists(self.icon_dir):
+ try:
+ os.makedirs(self.icon_dir, exist_ok=True)
+ except Exception as e:
+ print(f"创建图标目录失败: {str(e)}")
+
+ random.seed(window_number)
+ r = random.randint(30, 220)
+ g = random.randint(30, 220)
+ b = random.randint(30, 220)
+ img = Image.new("RGBA", (size, size), color=(0, 0, 0, 0))
+ draw = ImageDraw.Draw(img)
+ bg_image_path = os.path.join(os.path.dirname(__file__), "chrome.png")
+ if os.path.exists(bg_image_path):
+ bg_image = Image.open(bg_image_path).resize((size, size))
+ img.paste(bg_image, (0, 0))
+ ellipse_width = size * 0.85
+ ellipse_height = size * 0.5
+ ellipse_left = (size - ellipse_width) / 2
+ ellipse_top = (size - ellipse_height) / 2 + 12
+ ellipse_right = ellipse_left + ellipse_width
+ ellipse_bottom = ellipse_top + ellipse_height
+ draw.ellipse(
+ (ellipse_left, ellipse_top, ellipse_right, ellipse_bottom),
+ fill=(r, g, b, 255),
+ )
+ try:
+ font_size = 24
+ font_path = os.path.join(os.environ["WINDIR"], "Fonts", "Arial.ttf")
+ if os.path.exists(font_path):
+ font = ImageFont.truetype(font_path, font_size)
+ else:
+ font = ImageFont.truetype("Arial", font_size)
+ except Exception as font_error:
+ print(f"加载字体失败: {str(font_error)}")
+ font = ImageFont.load_default()
+ font_size = 24
+ text = str(window_number)
+ try:
+ if hasattr(font, "getbbox"):
+ bbox = font.getbbox(text)
+ text_width = bbox[2] - bbox[0]
+ text_height = bbox[3] - bbox[1]
+ elif hasattr(draw, "textsize"):
+ text_width, text_height = draw.textsize(text, font=font)
+ else:
+ text_width = font_size * len(text) * 0.6
+ text_height = font_size
+ x = (size - text_width) / 2
+ y = (size - text_height) / 2 + 10
+ text_color = (255, 255, 255, 255)
+ draw.text((x, y), text, fill=text_color, font=font)
+ except Exception as text_error:
+ print(f"绘制文本失败: {str(text_error)}")
+ draw.text(
+ (size // 4, size // 4), text, fill=(255, 255, 255, 255), font=font
+ )
+ icon_path = os.path.join(self.icon_dir, f"chrome_icon_{window_number}.ico")
+ os.makedirs(os.path.dirname(icon_path), exist_ok=True)
+ try:
+ img.save(icon_path, format="ICO")
+ except Exception as save_error:
+ print(f"保存图标失败: {str(save_error)}")
+ png_path = os.path.join(
+ self.icon_dir, f"chrome_icon_{window_number}.png"
+ )
+ img.save(png_path, format="PNG")
+ icon_path = png_path
+ return icon_path
+ except Exception as e:
+ print(f"生成图标失败: {str(e)}")
+ return None
+
+ def set_chrome_icon(self, hwnd, icon_path):
+ """为Chrome窗口设置自定义图标"""
+ try:
+ big_icon = win32gui.LoadImage(
+ 0, icon_path, win32con.IMAGE_ICON, 32, 32, win32con.LR_LOADFROMFILE
+ )
+ small_icon = win32gui.LoadImage(
+ 0, icon_path, win32con.IMAGE_ICON, 16, 16, win32con.LR_LOADFROMFILE
+ )
+ win32gui.SendMessage(hwnd, win32con.WM_SETICON, win32con.ICON_BIG, big_icon)
+ win32gui.SendMessage(
+ hwnd, win32con.WM_SETICON, win32con.ICON_SMALL, small_icon
+ )
+ return True
+ except Exception as e:
+ print(f"设置图标失败: {str(e)}")
+ return False
+
+ def apply_icons_to_chrome_windows(self, hwnd_map):
+ """为所有打开的Chrome窗口应用自定义图标"""
+ for number, hwnd in hwnd_map.items():
+ icon_path = self.generate_color_icon(number)
+ if icon_path:
+ self.set_chrome_icon(hwnd, icon_path)
+
+
if __name__ == "__main__":
try:
app = ChromeManager()
@@ -5007,13 +5743,15 @@ def load_settings(self) -> dict:
except Exception as e:
# 确保错误被显示出来
import traceback
+
error_message = f"程序出现错误:\n{str(e)}\n\n{traceback.format_exc()}"
print(error_message)
try:
# 尝试使用tkinter显示错误
from tkinter import messagebox
+
messagebox.showerror("程序错误", error_message)
except:
# 如果tkinter也失败了,尝试命令行保持窗口
print("\n按任意键退出...")
- input()
\ No newline at end of file
+ input()