Files
Chrome-Manager/chrome_manager.py
T

1752 lines
75 KiB
Python

import tkinter as tk
from tkinter import ttk, messagebox
import os
import subprocess
import win32gui
import win32process
import win32con
import win32api
import win32com.client
import json
from typing import List, Dict, Optional
import math
import ctypes
from ctypes import wintypes
import threading
import time
import sys
import keyboard
import mouse
import webbrowser
import sv_ttk
def is_admin():
# 检查是否具有管理员权限
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
def run_as_admin():
# 以管理员权限重新运行程序
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
class ChromeManager:
def __init__(self):
if not is_admin():
if messagebox.askyesno("权限不足", "需要管理员权限才能运行同步功能。\n是否以管理员身份重新启动程序?"):
run_as_admin()
sys.exit()
self.root = tk.Tk()
self.root.title("NoBiggie社区Chrome多窗口管理器 V1.0")
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)}")
last_position = self.load_window_position()
if last_position:
self.root.geometry(last_position)
sv_ttk.set_theme("light") # 使用 light 主题
self.window_list = None # 先初始化为 None
self.windows = []
self.master_window = None
self.shortcut_path = self.load_settings().get('shortcut_path', '')
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.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.shortcut_hook = None
self.current_shortcut = None
# 从设置中加载快捷键
settings = self.load_settings()
if 'sync_shortcut' in settings:
self.set_shortcut(settings['sync_shortcut'])
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# 创建界面
self.create_widgets()
self.create_styles()
self.root.update()
current_width = self.root.winfo_width()
current_height = self.root.winfo_height()
self.root.geometry(f"{current_width}x{current_height}")
self.root.resizable(False, False)
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)
if self.window_list:
self.window_list.tag_configure("master",
background="#0d6efd",
foreground='white'
)
# 链接样式
style.configure('Link.TLabel',
foreground='#0d6efd',
cursor='hand2',
font=('Microsoft YaHei UI', 9, 'underline')
)
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_frame = ttk.Frame(manage_frame)
button_frame.pack(fill=tk.X)
ttk.Button(button_frame, text="导入窗口", command=self.import_windows, style='Accent.TButton').pack(side=tk.LEFT, padx=2)
select_all_label = ttk.Label(button_frame, textvariable=self.select_all_var, style='Link.TLabel')
select_all_label.pack(side=tk.LEFT, padx=5)
select_all_label.bind('<Button-1>', self.toggle_select_all)
ttk.Button(button_frame, text="自动排列", command=self.auto_arrange_windows).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="关闭选中", command=self.close_selected_windows).pack(side=tk.LEFT, padx=2)
self.sync_button = ttk.Button(
button_frame,
text="▶ 开始同步",
command=self.toggle_sync,
style='Accent.TButton'
)
self.sync_button.pack(side=tk.LEFT, padx=5)
ttk.Button(
button_frame,
text="快捷键",
command=self.show_shortcut_dialog,
style='Accent.TButton'
).pack(side=tk.LEFT, padx=5)
# 在 button_frame 中添加屏幕选择下拉框
screen_frame = ttk.Frame(button_frame)
screen_frame.pack(side=tk.LEFT, padx=2)
ttk.Label(screen_frame, text="屏幕:").pack(side=tk.LEFT)
# 创建屏幕选择下拉框
self.screen_var = tk.StringVar()
self.screen_combo = ttk.Combobox(
screen_frame,
textvariable=self.screen_var,
width=8,
state="readonly"
)
self.screen_combo.pack(side=tk.LEFT)
# 获取并设置屏幕列表
self.update_screen_list()
list_frame = ttk.Frame(manage_frame)
list_frame.pack(fill=tk.BOTH, expand=True, pady=2)
# 创建窗口列表
self.window_list = ttk.Treeview(list_frame,
columns=("select", "number", "title", "master", "hwnd"),
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=40, anchor="center")
self.window_list.column("number", width=40, anchor="center")
self.window_list.column("title", width=300)
self.window_list.column("master", width=40, anchor="center")
self.window_list.column("hwnd", width=0, stretch=False) # 隐藏hwnd列
self.window_list.tag_configure("master", background="lightblue")
self.window_list.bind('<Button-1>', self.on_click)
# 添加滚动条
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.pack(fill=tk.X, pady=(0, 2))
self.start_x.insert(0, "0")
ttk.Label(left_frame, text="窗口宽度").pack(anchor=tk.W)
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")
ttk.Label(left_frame, text="水平间距").pack(anchor=tk.W)
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")
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.pack(fill=tk.X, pady=(0, 2))
self.start_y.insert(0, "0")
ttk.Label(right_frame, text="窗口高度").pack(anchor=tk.W)
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")
ttk.Label(right_frame, text="垂直间距").pack(anchor=tk.W)
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")
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.pack(pady=(2, 0))
self.windows_per_row.insert(0, "5")
ttk.Button(bottom_frame, text="自定义排列",
command=self.custom_arrange_windows,
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="打开窗口")
input_frame = ttk.Frame(open_window_tab)
input_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(input_frame, text="快捷方式目录:").pack(side=tk.LEFT)
self.path_entry = ttk.Entry(input_frame)
self.path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.path_entry.insert(0, self.shortcut_path)
numbers_frame = ttk.Frame(input_frame)
numbers_frame.pack(pady=5, padx=10, fill=tk.X)
ttk.Label(numbers_frame, text="窗口编号:").pack(side=tk.LEFT)
self.numbers_entry = ttk.Entry(numbers_frame)
self.numbers_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
settings = self.load_settings()
if 'last_window_numbers' in settings:
self.numbers_entry.insert(0, settings['last_window_numbers'])
self.numbers_entry.bind('<Return>', lambda e: self.open_windows())
ttk.Button(
numbers_frame,
text="打开窗口",
command=self.open_windows
).pack(side=tk.LEFT)
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=5, pady=5)
ttk.Label(url_frame, text="网址:").pack(side=tk.LEFT)
self.url_entry = ttk.Entry(url_frame)
self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.url_entry.insert(0, "www.google.com")
self.url_entry.bind('<Return>', lambda e: self.batch_open_urls())
ttk.Button(url_frame, text="批量打开", command=self.batch_open_urls).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=5, pady=5)
ttk.Label(icon_frame, text="图标目录:").pack(side=tk.LEFT)
self.icon_path_entry = ttk.Entry(icon_frame)
self.icon_path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
ttk.Label(icon_frame, text="窗口编号:").pack(side=tk.LEFT, padx=(10, 0))
self.icon_window_numbers = ttk.Entry(icon_frame, width=15)
self.icon_window_numbers.pack(side=tk.LEFT, padx=(0, 5))
ttk.Label(icon_frame, text="示例: 1-5,7,9-12").pack(side=tk.LEFT)
ttk.Button(icon_frame, text="替换图标", command=self.set_taskbar_icons).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)
author_frame = ttk.Frame(footer_frame)
author_frame.pack(side=tk.RIGHT)
ttk.Label(author_frame, text="Compiled by Devilflasher").pack(side=tk.LEFT)
ttk.Label(author_frame, text=" ").pack(side=tk.LEFT)
twitter_label = ttk.Label(
author_frame,
text="Twitter",
cursor="hand2",
font=("Arial", 9)
)
twitter_label.pack(side=tk.LEFT)
twitter_label.bind("<Button-1>", 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)
)
telegram_label.pack(side=tk.LEFT)
telegram_label.bind("<Button-1>", 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:
for item in items:
self.window_list.set(item, "select", "")
# 更新按钮状态
self.update_select_all_status()
except Exception as e:
print(f"切换全选状态失败: {str(e)}")
def update_select_all_status(self):
# 更新全选状态
try:
# 获取所有项目
items = self.window_list.get_children()
if not items:
self.select_all_var.set("全部选择")
return
# 检查是否全部选中
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)}")
def on_click(self, event):
# 处理点击事件
try:
region = self.window_list.identify_region(event.x, event.y)
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 "√")
# 更新全选按钮状态
self.update_select_all_status()
elif column == "#4": # 主控列
self.set_master_window(item)
except Exception as e:
print(f"处理点击事件失败: {str(e)}")
def set_master_window(self, item):
# 设置主控窗口
if not item:
return
try:
# 清除其他窗口的主控状态和标题
for i in self.window_list.get_children():
values = self.window_list.item(i)['values']
if values and len(values) >= 5:
hwnd = int(values[4])
title = values[2]
if title.startswith("[主控]"):
new_title = title.replace("[主控]", "").strip()
win32gui.SetWindowText(hwnd, new_title)
# 恢复默认边框颜色
try:
ctypes.windll.dwmapi.DwmSetWindowAttribute(
hwnd,
self.DWMWA_BORDER_COLOR,
ctypes.byref(ctypes.c_int(0)),
ctypes.sizeof(ctypes.c_int)
)
except:
pass
self.window_list.set(i, "master", "")
self.window_list.item(i, tags=())
# 设置新的主控窗口
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 title.startswith("[主控]"):
new_title = f"[主控] {title}"
win32gui.SetWindowText(self.master_window, new_title)
try:
# 设置红色边框
ctypes.windll.dwmapi.DwmSetWindowAttribute(
self.master_window,
self.DWMWA_BORDER_COLOR,
ctypes.byref(ctypes.c_int(0x000000FF)),
ctypes.sizeof(ctypes.c_int)
)
except:
pass
except Exception as e:
print(f"设置主控窗口失败: {str(e)}")
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") == "√"]
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.is_syncing = True
print("同步已开启")
except Exception as e:
print(f"开启同步失败: {str(e)}")
# 确保状态正确
self.is_syncing = False
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.is_syncing = False
print("同步已停止")
except Exception as e:
print(f"停止同步失败: {str(e)}")
def start_sync(self, selected_items):
try:
# 确保主控窗口存在
if not self.master_window:
raise Exception("未设置主控窗口")
# 保存选中的窗口列表,并按编号排序
self.sync_windows = []
window_info = []
# 收集所有选中的窗口
for item in selected_items:
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.hook_thread:
self.is_syncing = True
self.hook_thread = threading.Thread(target=self.message_loop)
self.hook_thread.daemon = True
self.hook_thread.start()
keyboard.hook(self.on_keyboard_event)
mouse.hook(self.on_mouse_event)
# 更新按钮状态
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}")
except Exception as e:
self.stop_sync() # 确保清理资源
print(f"开启同步失败: {str(e)}")
raise e
def message_loop(self):
# 消息循环
while self.is_syncing:
time.sleep(0.001)
def on_mouse_event(self, event):
try:
if self.is_syncing:
current_window = win32gui.GetForegroundWindow()
# 检查是否是主控窗口或其插件窗口
is_master = current_window == self.master_window
master_popups = self.get_chrome_popups(self.master_window)
is_popup = current_window in master_popups
if is_master or is_popup:
# 对于移动事件进行优化
if isinstance(event, mouse.MoveEvent):
# 检查移动距离和时间间隔
current_time = time.time()
if current_time - self.last_move_time < self.move_interval:
return
dx = abs(event.x - self.last_mouse_position[0])
dy = abs(event.y - self.last_mouse_position[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
# 获取鼠标位置
x, y = mouse.get_position()
# 获取当前窗口的相对坐标
current_rect = win32gui.GetWindowRect(current_window)
rel_x = (x - current_rect[0]) / (current_rect[2] - current_rect[0])
rel_y = (y - current_rect[1]) / (current_rect[3] - current_rect[1])
# 同步到其他窗口
for hwnd in self.sync_windows:
try:
# 确定目标窗口
if is_master:
target_hwnd = hwnd
else:
# 查找对应的扩展程序窗口
target_popups = self.get_chrome_popups(hwnd)
# 按照相对位置匹配
best_match = None
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)
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)
# 处理滚轮事件
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)
else:
win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, 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:
vk_code = win32con.VK_UP if wheel_delta > 0 else win32con.VK_DOWN
vk_code = win32con.VK_UP if wheel_delta > 0 else win32con.VK_DOWN
repeat_count = min(abs(wheel_delta) * 3, 6)
for _ in range(repeat_count):
win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, vk_code, 0)
win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, vk_code, 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)
elif event.button == mouse.RIGHT:
win32gui.PostMessage(target_hwnd, win32con.WM_RBUTTONDOWN, win32con.MK_RBUTTON, lparam)
elif event.event_type == mouse.UP:
if event.button == mouse.LEFT:
win32gui.PostMessage(target_hwnd, win32con.WM_LBUTTONUP, 0, lparam)
elif event.button == mouse.RIGHT:
win32gui.PostMessage(target_hwnd, win32con.WM_RBUTTONUP, 0, lparam)
# 处理鼠标移动
elif isinstance(event, mouse.MoveEvent):
win32gui.PostMessage(target_hwnd, win32con.WM_MOUSEMOVE, 0, lparam)
except Exception as e:
print(f"同步到窗口 {target_hwnd} 失败: {str(e)}")
continue
except Exception as e:
print(f"处理鼠标事件失败: {str(e)}")
def on_keyboard_event(self, event):
# 改进的键盘事件处理
try:
if self.is_syncing:
current_window = win32gui.GetForegroundWindow()
# 检查是否是主控窗口或其插件窗口
is_master = current_window == self.master_window
master_popups = self.get_chrome_popups(self.master_window)
is_popup = current_window in master_popups
if is_master or is_popup:
# 获取实际的输入目标窗口
input_hwnd = win32gui.GetFocus()
# 同步到其他窗口
for hwnd in self.sync_windows:
try:
# 确定目标窗口
if is_master:
target_hwnd = hwnd
else:
# 查找对应的扩展程序窗口
target_popups = self.get_chrome_popups(hwnd)
# 按照相对位置匹配
best_match = None
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)
if diff < min_diff:
min_diff = diff
best_match = popup
target_hwnd = best_match if best_match else hwnd
if not target_hwnd:
continue
# 处理 Ctrl 组合键
if keyboard.is_pressed('ctrl'):
# 发送 Ctrl 按下
win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, win32con.VK_CONTROL, 0)
# 处理常用组合键
if event.name in ['a', 'c', 'v', 'x']:
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_KEYUP, win32con.VK_CONTROL, 0)
continue
# 处理普通按键
if event.name in ['enter', 'backspace', 'tab', 'esc', 'space',
'up', 'down', 'left', 'right', # 添加左右键
'home', 'end', 'page up', 'page down', 'delete']:
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
}
vk_code = vk_map[event.name]
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)
continue
else:
continue
# 发送按键消息
if event.event_type == keyboard.KEY_DOWN:
win32gui.PostMessage(target_hwnd, win32con.WM_KEYDOWN, vk_code, 0)
else:
win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, vk_code, 0)
# 释放组合键
if keyboard.is_pressed('ctrl'):
win32gui.PostMessage(target_hwnd, win32con.WM_KEYUP, win32con.VK_CONTROL, 0)
except Exception as e:
print(f"同步到窗口 {target_hwnd} 失败: {str(e)}")
except Exception as e:
print(f"处理键盘事件失败: {str(e)}")
def stop_sync(self):
# 停止同步
try:
self.is_syncing = False
# 移除键盘钩子
if self.keyboard_hook:
keyboard.unhook(self.keyboard_hook)
self.keyboard_hook = None
# 移除鼠标钩子
if self.mouse_hook_id:
mouse.unhook(self.mouse_hook_id)
self.mouse_hook_id = None
# 等待监控线程结束
if self.hook_thread and self.hook_thread.is_alive():
self.hook_thread.join(timeout=1.0)
# 清理资源(保留主窗口设置)
self.sync_windows.clear()
self.popup_mappings.clear()
# 清理调试端口映射
self.debug_ports.clear()
# 重置鼠标状态
self.last_mouse_position = (0, 0)
self.last_move_time = 0
# 更新按钮状态
if self.sync_button:
self.sync_button.configure(text="▶ 开始同步", style='Accent.TButton')
except Exception as e:
print(f"停止同步失败: {str(e)}")
def on_closing(self):
# 窗口关闭事件
try:
self.stop_sync()
# 清理快捷键
if self.shortcut_hook:
keyboard.clear_all_hotkeys()
keyboard.unhook_all()
self.shortcut_hook = None
self.save_settings()
except Exception as e:
print(f"程序关闭时出错: {str(e)}")
finally:
self.root.destroy()
def auto_arrange_windows(self):
"""自动排列窗口"""
try:
# 先停止同步
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']
if values and len(values) >= 5:
number = int(values[1])
hwnd = int(values[4])
selected.append((number, hwnd, item))
if not selected:
messagebox.showinfo("提示", "请先选择要排列的窗口!")
return
# 按编号正序排序
selected.sort(key=lambda x: x[0])
# 获取选中的屏幕信息
screen_index = self.screen_combo.current()
if screen_index < 0 or screen_index >= len(self.screens):
messagebox.showerror("错误", "请选择有效的屏幕!")
return
screen = self.screens[screen_index]
screen_rect = screen['work_rect'] # 使用工作区而不是完整显示区
# 计算屏幕尺寸
screen_width = screen_rect[2] - screen_rect[0]
screen_height = screen_rect[3] - screen_rect[1]
# 计算最佳布局
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
# 创建位置映射(从左到右,从上到下)
positions = []
for i in range(count):
row = i // cols
col = i % cols
x = screen_rect[0] + col * width
y = screen_rect[1] + row * height
positions.append((x, y))
# 应用窗口位置
for i, (_, hwnd, _) in enumerate(selected):
try:
x, y = positions[i]
# 确保窗口可见并移动到指定位置
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)
except Exception as e:
print(f"移动窗口 {hwnd} 失败: {str(e)}")
continue
# 如果之前在同步,重新开启同步
if was_syncing:
self.start_sync([item for _, _, item in selected])
except Exception as e:
messagebox.showerror("错误", f"自动排列失败: {str(e)}")
def custom_arrange_windows(self):
# 自定义排列窗口
try:
# 先停止同步
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") == "√":
selected.append(item)
if not selected:
messagebox.showinfo("提示", "请选择要排列的窗口!")
return
try:
# 获取参数
start_x = int(self.start_x.get())
start_y = int(self.start_y.get())
width = int(self.window_width.get())
height = int(self.window_height.get())
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 in enumerate(selected):
values = self.window_list.item(item)['values']
if values and len(values) >= 5: # 确保有足够的值
hwnd = int(values[4])
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)}")
# 如果之前在同步,重新开启同步
if was_syncing:
self.start_sync(selected)
except Exception as e:
messagebox.showerror("错误", f"排列窗口失败: {str(e)}")
def load_settings(self) -> dict:
# 加载设置
try:
with open('settings.json', 'r') as f:
return json.load(f)
except:
return {}
def save_settings(self):
# 保存设置
try:
settings = {
'shortcut_path': self.path_entry.get(),
'window_position': self.root.geometry(),
'last_window_numbers': self.numbers_entry.get(), # 添加保存窗口编号
'arrange_params': {
'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()
},
'sync_shortcut': self.current_shortcut
}
with open('settings.json', 'w', encoding='utf-8') as f:
json.dump(settings, f, ensure_ascii=False, indent=4)
except Exception as e:
print(f"保存设置失败: {str(e)}")
def load_arrange_params(self):
# 加载排列参数
settings = self.load_settings()
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_y.delete(0, tk.END)
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_height.delete(0, tk.END)
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.v_spacing.delete(0, tk.END)
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'))
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(',')
for part in parts:
part = part.strip()
if '-' in part:
# 处理范围,如 "1-5"
start, end = map(int, part.split('-'))
result.extend(range(start, end + 1))
else:
# 处理单个数字
result.append(int(part))
return sorted(list(set(result))) # 去重并排序
def open_windows(self):
# 打开Chrome窗口
path = self.path_entry.get()
numbers = self.numbers_entry.get()
if not path or not numbers:
messagebox.showwarning("警告", "请输入快捷方式路径和窗口编号!")
return
try:
window_numbers = self.parse_window_numbers(numbers)
for num in window_numbers:
shortcut = os.path.join(path, f"{num}.lnk")
if os.path.exists(shortcut):
subprocess.Popen(["start", "", shortcut], shell=True)
time.sleep(0.5) # 添加延时确保按顺序打开
else:
messagebox.showwarning("警告", f"快捷方式不存在: {shortcut}")
# 保存路径
self.save_settings()
except Exception as e:
messagebox.showerror("错误", f"打开窗口失败: {str(e)}")
def get_shortcut_number(self, shortcut_path):
# 从快捷方式中获取窗口编号
handle = None
try:
# 获取目标进程
shortcut = self.shell.CreateShortCut(shortcut_path)
cmd_line = shortcut.Arguments
# 打开进程获取命令行
handle = win32api.OpenProcess(
win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ,
False,
self.pid
)
# 处理命令行
if '--user-data-dir=' in cmd_line:
data_dir = cmd_line.split('--user-data-dir=')[1].strip('"')
number = os.path.basename(data_dir)
return number
return None
except Exception as e:
print(f"获取快捷方式编号失败: {str(e)}")
return None
finally:
# 确保句柄被关闭
if handle:
try:
win32api.CloseHandle(handle)
except Exception as e:
print(f"关闭进程句柄失败: {str(e)}")
def import_windows(self):
# 导入当前打开的Chrome窗口
try:
# 清空列表
for item in self.window_list.get_children():
self.window_list.delete(item)
def callback(hwnd, windows):
if win32gui.IsWindowVisible(hwnd):
_, pid = win32process.GetWindowThreadProcessId(hwnd)
try:
handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ, False, pid)
path = win32process.GetModuleFileNameEx(handle, 0)
win32api.CloseHandle(handle)
if "chrome.exe" in path.lower():
title = win32gui.GetWindowText(hwnd)
if title and not title.startswith("Chrome 传递"):
# 为每个窗口分配一个调试端口
port = self.base_debug_port + len(windows)
self.debug_ports[hwnd] = port
windows.append((title, hwnd))
except Exception as e:
print(f"获取进程信息失败: {str(e)}")
windows = []
win32gui.EnumWindows(callback, windows)
# 反转windows列表,这样就会按照相反的顺序添加到列表中
windows.reverse()
# 添加到列表
for i, (title, hwnd) in enumerate(windows, 1):
self.window_list.insert("", "end", values=("", f"{i}", title, "", hwnd))
except Exception as e:
messagebox.showerror("错误", f"导入窗口失败: {str(e)}")
def enum_window_callback(self, hwnd, windows):
# 枚举窗口回调函数
try:
# 检查窗口是否可见
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)
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
})
except Exception as e:
print(f"枚举窗口失败: {str(e)}")
def close_selected_windows(self):
# 关闭选中的窗口
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
try:
for item in selected:
# 从values中获取hwnd
hwnd = int(self.window_list.item(item)['values'][4])
try:
# 检查窗口是否还存在
if win32gui.IsWindow(hwnd):
win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
except:
pass # 忽略已关闭窗口的错误
# 等待窗口关闭后刷新列表
time.sleep(0.5)
self.import_windows()
except Exception as e:
print(f"关闭窗口失败: {str(e)}") # 只打印错误,不显示错误对话框
def set_taskbar_icons(self):
# 设置独立任务栏图标
if not self.path_entry.get():
messagebox.showinfo("提示", "请先设置快捷方式目录!")
return
if not os.path.exists(self.path_entry.get()):
messagebox.showerror("错误", "快捷方式目录不存在!")
return
# 确认操作
choice = messagebox.askyesnocancel("选择操作", "选择要执行的操作:\n是 - 设置自定义图标\n否 - 恢复原始设置\n取消 - 不执行任何操作")
if choice is None: # 用户点击取消
return
try:
data_dir = self.path_entry.get()
icon_dir = self.icon_path_entry.get()
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(data_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"
# 恢复指定的快捷方式
for i in window_numbers:
shortcut_path = os.path.join(data_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="D:\\chrom duo\\Data\\{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)}")
def batch_open_urls(self):
# 批量打开网页
try:
# 获取输入的网址
url = self.url_entry.get()
if not url:
messagebox.showwarning("警告", "请输入要打开的网址!")
return
# 确保 URL 格式正确
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
# 获取选中的窗口
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])
selected_windows.append(hwnd)
if not selected_windows:
messagebox.showwarning("警告", "请先选择要操作的窗口!")
return
# 在每个选中的窗口中打开网页
for hwnd in selected_windows:
try:
# 激活窗口
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.1)
# 打开新标签页
keyboard.press_and_release('ctrl+t')
time.sleep(0.1)
# 输入网址并回车
keyboard.write(url)
time.sleep(0.1)
keyboard.press_and_release('enter')
time.sleep(0.2)
except Exception as e:
print(f"在窗口 {hwnd} 打开URL失败: {str(e)}")
messagebox.showinfo("成功", "批量打开网页完成!")
except Exception as e:
messagebox.showerror("错误", f"批量打开网页失败: {str(e)}")
def run(self):
"""运行程序"""
self.root.mainloop()
def load_window_position(self):
# 从 settings.json 加载窗口位置
try:
settings = self.load_settings()
return settings.get('window_position')
except:
return None
def save_window_position(self):
# 保存窗口位置到 settings.json
try:
position = self.root.geometry()
settings = self.load_settings()
settings['window_position'] = position
with open('settings.json', 'w', encoding='utf-8') as f:
json.dump(settings, f, ensure_ascii=False, indent=4)
except Exception as e:
print(f"保存窗口位置失败: {str(e)}")
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:
# 检查窗口类型
if "Chrome_WidgetWin_1" in class_name:
# 检查是否是扩展程序相关窗口,放宽检测条件
style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
ex_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
# 扩展窗口的特征
is_popup = (
"扩展程序" in title or
"插件" 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 # 添加工具窗口检查
)
if is_popup:
popups.append(hwnd)
except Exception as e:
print(f"枚举窗口失败: {str(e)}")
win32gui.EnumWindows(enum_windows_callback, None)
return popups
def monitor_popups(self):
# 监控插件窗口变化
while self.is_syncing:
try:
self.sync_chrome_popups()
except:
pass
time.sleep(0.1)
def show_shortcut_dialog(self):
# 显示快捷键设置对话框
# 创建对话框
dialog = tk.Toplevel(self.root)
dialog.title("设置同步功能快捷键")
dialog.geometry("300x150")
dialog.resizable(False, False)
# 使对话框模态
dialog.transient(self.root)
dialog.grab_set()
# 当前快捷键显示
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
def start_recording():
# 开始录制快捷键
nonlocal recording
recording = True
keys_pressed.clear()
shortcut_var.set("请按下快捷键组合...")
record_btn.configure(state='disabled')
def on_key_event(e):
if not recording:
return
if e.event_type == keyboard.KEY_DOWN:
keys_pressed.add(e.name)
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:
stop_recording()
keyboard.hook(on_key_event)
def stop_recording():
# 停止录制快捷键
nonlocal recording
recording = False
keyboard.unhook_all()
# 重新设置当前快捷键
if self.current_shortcut:
self.set_shortcut(self.current_shortcut)
record_btn.configure(state='normal')
# 录制按钮
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 != "请按下快捷键组合...":
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:
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="保存",
command=save_shortcut
).pack(pady=5)
# 确保关闭对话框时停止录制
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}')
def set_shortcut(self, shortcut):
# 设置快捷键
try:
# 先完全清理所有快捷键和钩子
keyboard.unhook_all()
keyboard.clear_all_hotkeys()
if self.shortcut_hook:
self.shortcut_hook = None
if shortcut:
# 添加错误重试机制
max_retries = 3
for attempt in range(max_retries):
try:
# 注册新快捷键
self.shortcut_hook = keyboard.add_hotkey(
shortcut,
self.toggle_sync,
suppress=True,
trigger_on_release=True # 在按键释放时触发,避免卡键
)
self.current_shortcut = shortcut
print(f"快捷键 {shortcut} 设置成功")
break
except Exception as e:
print(f"第 {attempt + 1} 次设置快捷键失败: {str(e)}")
if attempt == max_retries - 1:
raise
time.sleep(0.5) # 重试前等待
except Exception as e:
print(f"设置快捷键失败: {str(e)}")
self.shortcut_hook = None
# 尝试恢复之前的快捷键
if self.current_shortcut and self.current_shortcut != shortcut:
try:
self.shortcut_hook = keyboard.add_hotkey(
self.current_shortcut,
self.toggle_sync,
suppress=True,
trigger_on_release=True
)
except:
self.current_shortcut = None
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
screen_name += " (主屏幕)"
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
# 定义回调函数类型
MONITORENUMPROC = ctypes.WINFUNCTYPE(
ctypes.c_bool,
ctypes.c_ulong,
ctypes.c_ulong,
ctypes.POINTER(wintypes.RECT),
ctypes.c_longlong
)
# 创建回调函数
callback_function = MONITORENUMPROC(callback)
# 枚举显示器
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_left = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN)
virtual_top = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN)
# 获取主屏幕信息
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
})
# 尝试获取第二个屏幕
try:
second_monitor = win32api.MonitorFromPoint(
(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
})
except:
pass
except Exception as e:
print(f"备用方法失败: {str(e)}")
if not screens:
# 如果仍然没有找到屏幕,使用基本方案
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.sort(key=lambda x: x['rect'][0])
# 更新下拉框选项
self.screen_combo['values'] = [screen['name'] for screen in screens]
self.screens = screens # 保存屏幕信息
# 默认选择主屏幕
for i, screen in enumerate(screens):
if '主屏幕' in screen['name']:
self.screen_combo.current(i)
break
else:
self.screen_combo.current(0)
# 打印调试信息
print("检测到的屏幕:")
for screen in screens:
print(f"名称: {screen['name']}")
print(f"位置: {screen['rect']}")
print(f"工作区: {screen['work_rect']}")
print("---")
except Exception as e:
print(f"获取屏幕列表失败: {str(e)}")
# 使用最基本的方案
try:
screen_width = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
screen_height = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
screens = [{
'name': "主屏幕",
'rect': (0, 0, screen_width, screen_height),
'work_rect': (0, 0, screen_width, screen_height),
'monitor': None
}]
self.screen_combo['values'] = [screen['name'] for screen in screens]
self.screens = screens
self.screen_combo.current(0)
except Exception as e2:
print(f"基本方案也失败了: {str(e2)}")
if __name__ == "__main__":
app = ChromeManager()
app.run()