From 76686236a9567228b523c7a3841474736655c6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AF=E8=BE=BE=E9=B8=AD?= <3443833799@qq.com> Date: Mon, 28 Apr 2025 14:15:08 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E8=87=AA=E5=8A=A8=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=EF=BC=8C=E5=88=A0=E6=8E=89=E4=BA=86=E5=8E=9F?= =?UTF-8?q?UI=E4=B8=AD=E2=80=9C=E6=9B=BF=E6=8D=A2=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E2=80=9D=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.py | 259 +-- chrome.png | Bin 0 -> 1175 bytes chrome_manager.py | 3809 +++++++++++++++++++++++++++------------------ 3 files changed, 2417 insertions(+), 1651 deletions(-) create mode 100644 chrome.png 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 0000000000000000000000000000000000000000..77e4b230b9881dead5c3b43bdfaaee3466276562 GIT binary patch literal 1175 zcmV;I1Zew-P)Px(QAtEWRA@uZT1!&fKoISgxktSRV5Wh9L>KSAOZRnqkI>(A?tKf#nPpJ67Vwloodp%*M!$tr?`3QfS1iiy^D-f*!#&_$OB3!?Na<(El zh*=wBH3Ik#l$Rh|^P&;J=sqD^GkwH+c}~=7M%GoYL#YH{xOfwE`7%EduKEZ3 zZ-cuON(BIJeFPHLCcxS2e6Y$vxKgQLjVMhwxi-dPWTUnEj#}Uf0J-&Lyff(qesJ`V ztW8yz0B?PI8CT`CNvv95zJNfh-Tbpi7(+ES7MoBd+s+FQoG#wh2(>j zO{($ZzMme!l!)S7CPSbkg=o_|K(hTZC_*YHi5oAbLr8X)@k9U^F5a|7ivW_>`N>4_ zLWx$Z$`7CRH$bgzHUq@f%f+DR|3m7{?kYwcKkeg`sIlLP5GPt%t#ERK7q#A3!_zj$ zbb>IYW7Nfe^mnG>V?gv)TN`UESi&}S2fk{38DN$Xq0eY0g0FO0q?OF(EfWK9r4VhaGOaVv${ zc(4gUg(O@HCKt=B0q~bqT9ry!deG=u&pcr0!XB!{Is(A^EFb-E z-UJ;!?)!%x#1W)t1SnRKyqzYDSo8~?)pk0hk&^>d#=(Y;)PSVcBHJJr_xez0E}S=V z+snx+1CTLE=nAEj*d`DGp(9!dxg*jJ^`^^Po{Q;=KbI32|C@&tB7Evw-bdU>eK%KW7O~P6iTcN#8)1{T7<= 22000 - + # 初始化系统托盘通知 try: if self.is_win11: @@ -226,16 +250,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 +273,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 +298,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 +520,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 +664,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 +686,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 +733,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 +755,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 +773,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 +795,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 +843,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 +877,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 +950,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 +1029,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 +1045,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 +1114,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 +1162,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 +1178,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 +1190,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 +1265,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 +1582,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 +1594,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 +1649,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 +1679,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 +1822,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 +1853,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 +1876,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 +1923,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 +1943,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 +1980,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 +2006,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 +2052,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 +2125,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 +2141,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 +2163,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 +2229,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 +2241,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 +2279,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 +2331,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 +2380,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 +2407,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 +2423,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 +2435,7 @@ def open_windows(self): # 不启用CDP,直接打开 subprocess.Popen(["start", "", shortcut], shell=True) time.sleep(0.05) # 只等待50毫秒 - + # 在所有窗口启动后,在后台清理临时文件 def cleanup_temp_files(): # 等待一小段时间再清理,确保所有窗口都已经启动 @@ -2070,31 +2446,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 +2483,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 +2509,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 +2517,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 +2705,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 +2823,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 +2866,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 +2916,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 +3043,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 +3077,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 +3093,48 @@ 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] 检测到非管理员权限,准备提示") + print( + f"[{time.time() - self.start_time:.3f}s] 检测到非管理员权限,准备提示" + ) + # 将管理员权限请求延迟显示,确保主窗口已完全显示 def show_admin_prompt(): - result = messagebox.askquestion("权限提示", "没有管理员权限可能无法正常访问某些窗口,是否以管理员身份重新启动?") - if result == 'yes': + result = messagebox.askquestion( + "权限提示", + "没有管理员权限可能无法正常访问某些窗口,是否以管理员身份重新启动?", + ) + if result == "yes": run_as_admin() self.root.destroy() + # 延迟更长时间显示,避免干扰用户 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] 开始预热窗口枚举...") 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 +3142,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 +3163,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 +3183,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 +3201,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 +3364,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 +3389,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 +3432,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 +3440,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 +3519,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 +3588,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 +3614,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 +3626,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 +3762,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 +3788,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 +3856,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 +3884,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 +3950,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 +3987,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 +4003,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 +4011,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 +4152,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 +4206,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 +4244,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 +4260,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 +4387,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 +4443,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 +4525,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 +4558,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 +4566,7 @@ def cut_text(self): self.current_text_widget.event_generate("<>") except: pass - + def copy_text(self): """复制文本""" if self.current_text_widget: @@ -4053,7 +4574,7 @@ def copy_text(self): self.current_text_widget.event_generate("<>") except: pass - + def paste_text(self): """粘贴文本""" if self.current_text_widget: @@ -4061,12 +4582,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 +4597,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 +4614,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 +4638,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 +4685,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 +4757,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 +4807,7 @@ def sync_popups(self): # 由于模拟点击可能较复杂,这里只记录日志 except Exception as e: print(f"同步单个弹窗出错: {str(e)}") - + except Exception as e: print(f"同步弹窗过程出错: {str(e)}") @@ -4255,7 +4816,7 @@ def setup_wheel_hook(self): if self.wheel_hook_id: # 如果已经有钩子,先卸载 self.unhook_wheel() - + # 定义钩子回调函数 def wheel_proc(nCode, wParam, lParam): try: @@ -4263,34 +4824,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 +4878,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 +4919,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 +5026,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 +5039,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 +5052,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 +5093,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 +5101,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 +5110,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 +5144,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 +5196,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 +5212,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 +5220,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 +5299,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 +5307,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 +5341,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 +5430,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 +5461,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 +5473,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 +5481,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 +5546,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 +5585,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 +5603,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 +5724,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() From 6b3433561f972c52cada1e70117be6eb5c0876a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AF=E8=BE=BE=E9=B8=AD?= <96913323+kiss-kedaya@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:25:56 +0800 Subject: [PATCH 2/5] Update chrome_manager.py --- chrome_manager.py | 57 +++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/chrome_manager.py b/chrome_manager.py index 70a2898..f9ea948 100644 --- a/chrome_manager.py +++ b/chrome_manager.py @@ -106,14 +106,28 @@ def __init__(self): # 存储进程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: @@ -3100,26 +3114,31 @@ def delayed_initialization(self): try: print(f"[{time.time() - self.start_time:.3f}s] 开始执行延迟初始化") - # 检查管理员权限(延迟检查) - if not is_admin(): + # 检查管理员权限(延迟检查),已确认则跳过 + 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] 开始预热窗口枚举...") From 4e566f4aea0ee23d4530100d999dabf39fc2835c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AF=E8=BE=BE=E9=B8=AD?= <96913323+kiss-kedaya@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:41:09 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20Colab=20=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E8=80=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ollama_Setup.ipynb | 333 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 Ollama_Setup.ipynb diff --git a/Ollama_Setup.ipynb b/Ollama_Setup.ipynb new file mode 100644 index 0000000..a3777a2 --- /dev/null +++ b/Ollama_Setup.ipynb @@ -0,0 +1,333 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [], + "gpuType": "T4", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + }, + "accelerator": "GPU" + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "source": [ + "# Run Ollama in Colab\n", + "---\n", + "\n", + "[![5aharsh/collama](https://raw.githubusercontent.com/5aharsh/collama/main/assets/banner.png)](https://github.com/5aharsh/collama)\n", + "\n", + "This is an example notebook which demonstrates how to run Ollama inside a Colab instance. With this you can run pretty much any small to medium sized models offerred by Ollama for free.\n", + "\n", + "For the list of available models check [models being offerred by Ollama](https://ollama.com/library).\n", + "\n", + "\n", + "## Before you proceed\n", + "---\n", + "\n", + "Since by default the runtime type of Colab instance is CPU based, in order to use LLM models make sure to change your runtime type to T4 GPU (or better if you're a paid Colab user). This can be done by going to **Runtime > Change runtime type**.\n", + "\n", + "While running your script be mindful of the resources you're using. This can be tracked at **Runtime > View resources**.\n", + "\n", + "## Running the notebook\n", + "---\n", + "\n", + "After configuring the runtime just run it with **Runtime > Run all**. And you can start tinkering around. This example uses [Llama 3.2](https://ollama.com/library/llama3.2) to generate a response from a prompted question using [LangChain Ollama Integration](https://python.langchain.com/docs/integrations/chat/ollama/)." + ], + "metadata": { + "id": "zyGk-87qnbWE" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Installing Dependencies\n", + "---\n", + "\n", + "1. `pciutils` is required by Ollama to detect the GPU type.\n", + "2. Installation of Ollama in the runtime instance will be taken care by `curl -fsSL https://ollama.com/install.sh | sh`\n", + "\n", + "\n" + ], + "metadata": { + "id": "B1S1YL6EnYBB" + } + }, + { + "cell_type": "code", + "source": [ + "!sudo apt-get update\n", + "!sudo apt-get install -y pciutils curl # 检测 GPU 必需\n", + "!curl -fsSL https://ollama.com/install.sh | sh" + ], + "metadata": { + "id": "YlVK9iG4AD5L", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "33b603d8-d095-4fec-bb9a-68b53fe747dc" + }, + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\u001b[33m\r0% [Working]\u001b[0m\r \rGet:1 https://cli.github.com/packages stable InRelease [3,917 B]\n", + "\u001b[33m\r0% [Connecting to archive.ubuntu.com] [Connecting to security.ubuntu.com (185.1\u001b[0m\r \rGet:2 https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64 InRelease [1,581 B]\n", + "\u001b[33m\r0% [Connecting to archive.ubuntu.com] [Connecting to security.ubuntu.com (185.1\u001b[0m\u001b[33m\r0% [Connecting to archive.ubuntu.com] [Connecting to security.ubuntu.com (185.1\u001b[0m\r \rGet:3 https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/ InRelease [3,632 B]\n", + "Get:4 https://cli.github.com/packages stable/main amd64 Packages [357 B]\n", + "Get:5 http://security.ubuntu.com/ubuntu jammy-security InRelease [129 kB]\n", + "Get:6 https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/ Packages [87.4 kB]\n", + "Get:7 https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64 Packages [2,436 kB]\n", + "Hit:8 http://archive.ubuntu.com/ubuntu jammy InRelease\n", + "Get:9 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [128 kB]\n", + "Get:10 https://r2u.stat.illinois.edu/ubuntu jammy InRelease [6,555 B]\n", + "Get:11 https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy InRelease [18.1 kB]\n", + "Hit:12 https://ppa.launchpadcontent.net/graphics-drivers/ppa/ubuntu jammy InRelease\n", + "Get:13 http://security.ubuntu.com/ubuntu jammy-security/main amd64 Packages [3,786 kB]\n", + "Get:14 http://archive.ubuntu.com/ubuntu jammy-backports InRelease [127 kB]\n", + "Hit:15 https://ppa.launchpadcontent.net/ubuntugis/ppa/ubuntu jammy InRelease\n", + "Get:16 https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy/main amd64 Packages [38.9 kB]\n", + "Get:17 https://r2u.stat.illinois.edu/ubuntu jammy/main amd64 Packages [2,930 kB]\n", + "Get:18 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages [4,160 kB]\n", + "Get:19 http://security.ubuntu.com/ubuntu jammy-security/restricted amd64 Packages [6,669 kB]\n", + "Get:20 http://security.ubuntu.com/ubuntu jammy-security/universe amd64 Packages [1,302 kB]\n", + "Get:21 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 Packages [1,615 kB]\n", + "Get:22 http://archive.ubuntu.com/ubuntu jammy-updates/restricted amd64 Packages [6,994 kB]\n", + "Get:23 http://archive.ubuntu.com/ubuntu jammy-backports/universe amd64 Packages [35.6 kB]\n", + "Get:24 http://archive.ubuntu.com/ubuntu jammy-backports/main amd64 Packages [84.0 kB]\n", + "Get:25 https://r2u.stat.illinois.edu/ubuntu jammy/main all Packages [9,855 kB]\n", + "Fetched 40.4 MB in 8s (5,241 kB/s)\n", + "Reading package lists... Done\n", + "Building dependency tree... Done\n", + "Reading state information... Done\n", + "128 packages can be upgraded. Run 'apt list --upgradable' to see them.\n", + "\u001b[1;33mW: \u001b[0mSkipping acquire of configured file 'main/source/Sources' as repository 'https://r2u.stat.illinois.edu/ubuntu jammy InRelease' does not seem to provide it (sources.list entry misspelt?)\u001b[0m\n", + "Reading package lists... Done\n", + "Building dependency tree... Done\n", + "Reading state information... Done\n", + "The following additional packages will be installed:\n", + " libpci3 pci.ids\n", + "The following NEW packages will be installed:\n", + " libpci3 pci.ids pciutils\n", + "0 upgraded, 3 newly installed, 0 to remove and 128 not upgraded.\n", + "Need to get 343 kB of archives.\n", + "After this operation, 1,581 kB of additional disk space will be used.\n", + "Get:1 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 pci.ids all 0.0~2022.01.22-1ubuntu0.1 [251 kB]\n", + "Get:2 http://archive.ubuntu.com/ubuntu jammy/main amd64 libpci3 amd64 1:3.7.0-6 [28.9 kB]\n", + "Get:3 http://archive.ubuntu.com/ubuntu jammy/main amd64 pciutils amd64 1:3.7.0-6 [63.6 kB]\n", + "Fetched 343 kB in 1s (274 kB/s)\n", + "debconf: unable to initialize frontend: Dialog\n", + "debconf: (No usable dialog-like program is installed, so the dialog based frontend cannot be used. at /usr/share/perl5/Debconf/FrontEnd/Dialog.pm line 78, <> line 3.)\n", + "debconf: falling back to frontend: Readline\n", + "debconf: unable to initialize frontend: Readline\n", + "debconf: (This frontend requires a controlling tty.)\n", + "debconf: falling back to frontend: Teletype\n", + "dpkg-preconfigure: unable to re-open stdin: \n", + "Selecting previously unselected package pci.ids.\n", + "(Reading database ... 121852 files and directories currently installed.)\n", + "Preparing to unpack .../pci.ids_0.0~2022.01.22-1ubuntu0.1_all.deb ...\n", + "Unpacking pci.ids (0.0~2022.01.22-1ubuntu0.1) ...\n", + "Selecting previously unselected package libpci3:amd64.\n", + "Preparing to unpack .../libpci3_1%3a3.7.0-6_amd64.deb ...\n", + "Unpacking libpci3:amd64 (1:3.7.0-6) ...\n", + "Selecting previously unselected package pciutils.\n", + "Preparing to unpack .../pciutils_1%3a3.7.0-6_amd64.deb ...\n", + "Unpacking pciutils (1:3.7.0-6) ...\n", + "Setting up pci.ids (0.0~2022.01.22-1ubuntu0.1) ...\n", + "Setting up libpci3:amd64 (1:3.7.0-6) ...\n", + "Setting up pciutils (1:3.7.0-6) ...\n", + "Processing triggers for man-db (2.10.2-1) ...\n", + "Processing triggers for libc-bin (2.35-0ubuntu3.8) ...\n", + "/sbin/ldconfig.real: /usr/local/lib/libur_loader.so.0 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libur_adapter_opencl.so.0 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libtbbbind.so.3 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libur_adapter_level_zero.so.0 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libtbbbind_2_0.so.3 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libtbbbind_2_5.so.3 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libtbb.so.12 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libumf.so.1 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libtbbmalloc_proxy.so.2 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libtcm_debug.so.1 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libtcm.so.1 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libhwloc.so.15 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libur_adapter_level_zero_v2.so.0 is not a symbolic link\n", + "\n", + "/sbin/ldconfig.real: /usr/local/lib/libtbbmalloc.so.2 is not a symbolic link\n", + "\n", + ">>> Installing ollama to /usr/local\n", + "\u001b[1m\u001b[31mERROR:\u001b[m This version requires zstd for extraction. Please install zstd and try again:\n", + " - Debian/Ubuntu: sudo apt-get install zstd\n", + " - RHEL/CentOS/Fedora: sudo dnf install zstd\n", + " - Arch: sudo pacman -S zstd\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Running Ollama\n", + "---\n", + "\n", + "In order to use Ollama it needs to run as a service in background parallel to your scripts. Becasue Jupyter Notebooks is built to run code blocks in sequence this make it difficult to run two blocks at the same time. As a workaround we will create a service using subprocess in Python so it doesn't block any cell from running.\n", + "\n", + "Service can be started by command `ollama serve`.\n", + "\n", + "`time.sleep(5)` adds some delay to get the Ollama service up before downloading the model." + ], + "metadata": { + "id": "fGEJwjTPoKWH" + } + }, + { + "cell_type": "code", + "source": [ + "import threading\n", + "import subprocess\n", + "import time\n", + "\n", + "def run_ollama_serve():\n", + " subprocess.Popen([\"ollama\", \"serve\"])\n", + "\n", + "thread = threading.Thread(target=run_ollama_serve)\n", + "thread.start()\n", + "time.sleep(5)" + ], + "metadata": { + "id": "Jh5CBAFxBYAC", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "8210b189-c23d-41ee-b14f-52b5830cdc1c" + }, + "execution_count": 3, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "Exception in thread Thread-4 (run_ollama_serve):\n", + "Traceback (most recent call last):\n", + " File \"/usr/lib/python3.12/threading.py\", line 1075, in _bootstrap_inner\n", + " self.run()\n", + " File \"/usr/lib/python3.12/threading.py\", line 1012, in run\n", + " self._target(*self._args, **self._kwargs)\n", + " File \"/tmp/ipykernel_3925/2856519456.py\", line 6, in run_ollama_serve\n", + " File \"/usr/lib/python3.12/subprocess.py\", line 1026, in __init__\n", + " self._execute_child(args, executable, preexec_fn, close_fds,\n", + " File \"/usr/lib/python3.12/subprocess.py\", line 1955, in _execute_child\n", + " raise child_exception_type(errno_num, err_msg, err_filename)\n", + "FileNotFoundError: [Errno 2] No such file or directory: 'ollama'\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Pulling Model\n", + "---\n", + "\n", + "Download the LLM model using `ollama pull llama3.2`.\n", + "\n", + "For other models check https://ollama.com/library" + ], + "metadata": { + "id": "WcBLqZfyoHg4" + } + }, + { + "cell_type": "code", + "source": [ + "!ollama pull llama3.2" + ], + "metadata": { + "id": "o2ghppmRDFny" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## And that's it!\n", + "---\n", + "\n", + "With this you should be able to freely play around with the models in your scripts. Following is an example using `langchain-ollama` to answer a simple prompt.\n", + "\n", + "If you have a use-case that can help out others feel free to add your notebook to [Collama](https://github.com/5aharsh/collama/fork)" + ], + "metadata": { + "id": "TYQJNeTuni_6" + } + }, + { + "cell_type": "code", + "source": [ + "!pip install langchain-ollama" + ], + "metadata": { + "id": "MbrT39oil6tK" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_ollama.llms import OllamaLLM\n", + "from IPython.display import Markdown\n", + "\n", + "template = \"\"\"Question: {question}\n", + "\n", + "Answer: Let's think step by step.\"\"\"\n", + "\n", + "prompt = ChatPromptTemplate.from_template(template)\n", + "\n", + "model = OllamaLLM(model=\"llama3.2\")\n", + "\n", + "chain = prompt | model\n", + "\n", + "display(Markdown(chain.invoke({\"question\": \"What's the length of hypotenuse in a right angled triangle\"})))" + ], + "metadata": { + "id": "9quBP56zDvpt" + }, + "execution_count": null, + "outputs": [] + } + ] +} \ No newline at end of file From 934c97f70e93b3df649c07440da8ff224288a975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AF=E8=BE=BE=E9=B8=AD?= <96913323+kiss-kedaya@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:52:42 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20Colab=20=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E8=80=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ollama_Setup.ipynb | 287 +++++++++++++++++++++------------------------ 1 file changed, 133 insertions(+), 154 deletions(-) diff --git a/Ollama_Setup.ipynb b/Ollama_Setup.ipynb index a3777a2..2c1eae9 100644 --- a/Ollama_Setup.ipynb +++ b/Ollama_Setup.ipynb @@ -5,6 +5,7 @@ "colab": { "provenance": [], "gpuType": "T4", + "toc_visible": true, "include_colab_link": true }, "kernelspec": { @@ -74,123 +75,39 @@ { "cell_type": "code", "source": [ - "!sudo apt-get update\n", - "!sudo apt-get install -y pciutils curl # 检测 GPU 必需\n", - "!curl -fsSL https://ollama.com/install.sh | sh" + "!apt-get update -qq\n", + "!apt-get install -y zstd" ], "metadata": { "id": "YlVK9iG4AD5L", "colab": { "base_uri": "https://localhost:8080/" }, - "outputId": "33b603d8-d095-4fec-bb9a-68b53fe747dc" + "outputId": "cd5fcee6-d4da-4577-d1ed-2a4926a72fa6" }, - "execution_count": 1, + "execution_count": 5, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ - "\u001b[33m\r0% [Working]\u001b[0m\r \rGet:1 https://cli.github.com/packages stable InRelease [3,917 B]\n", - "\u001b[33m\r0% [Connecting to archive.ubuntu.com] [Connecting to security.ubuntu.com (185.1\u001b[0m\r \rGet:2 https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64 InRelease [1,581 B]\n", - "\u001b[33m\r0% [Connecting to archive.ubuntu.com] [Connecting to security.ubuntu.com (185.1\u001b[0m\u001b[33m\r0% [Connecting to archive.ubuntu.com] [Connecting to security.ubuntu.com (185.1\u001b[0m\r \rGet:3 https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/ InRelease [3,632 B]\n", - "Get:4 https://cli.github.com/packages stable/main amd64 Packages [357 B]\n", - "Get:5 http://security.ubuntu.com/ubuntu jammy-security InRelease [129 kB]\n", - "Get:6 https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/ Packages [87.4 kB]\n", - "Get:7 https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64 Packages [2,436 kB]\n", - "Hit:8 http://archive.ubuntu.com/ubuntu jammy InRelease\n", - "Get:9 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [128 kB]\n", - "Get:10 https://r2u.stat.illinois.edu/ubuntu jammy InRelease [6,555 B]\n", - "Get:11 https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy InRelease [18.1 kB]\n", - "Hit:12 https://ppa.launchpadcontent.net/graphics-drivers/ppa/ubuntu jammy InRelease\n", - "Get:13 http://security.ubuntu.com/ubuntu jammy-security/main amd64 Packages [3,786 kB]\n", - "Get:14 http://archive.ubuntu.com/ubuntu jammy-backports InRelease [127 kB]\n", - "Hit:15 https://ppa.launchpadcontent.net/ubuntugis/ppa/ubuntu jammy InRelease\n", - "Get:16 https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy/main amd64 Packages [38.9 kB]\n", - "Get:17 https://r2u.stat.illinois.edu/ubuntu jammy/main amd64 Packages [2,930 kB]\n", - "Get:18 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages [4,160 kB]\n", - "Get:19 http://security.ubuntu.com/ubuntu jammy-security/restricted amd64 Packages [6,669 kB]\n", - "Get:20 http://security.ubuntu.com/ubuntu jammy-security/universe amd64 Packages [1,302 kB]\n", - "Get:21 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 Packages [1,615 kB]\n", - "Get:22 http://archive.ubuntu.com/ubuntu jammy-updates/restricted amd64 Packages [6,994 kB]\n", - "Get:23 http://archive.ubuntu.com/ubuntu jammy-backports/universe amd64 Packages [35.6 kB]\n", - "Get:24 http://archive.ubuntu.com/ubuntu jammy-backports/main amd64 Packages [84.0 kB]\n", - "Get:25 https://r2u.stat.illinois.edu/ubuntu jammy/main all Packages [9,855 kB]\n", - "Fetched 40.4 MB in 8s (5,241 kB/s)\n", - "Reading package lists... Done\n", - "Building dependency tree... Done\n", - "Reading state information... Done\n", - "128 packages can be upgraded. Run 'apt list --upgradable' to see them.\n", - "\u001b[1;33mW: \u001b[0mSkipping acquire of configured file 'main/source/Sources' as repository 'https://r2u.stat.illinois.edu/ubuntu jammy InRelease' does not seem to provide it (sources.list entry misspelt?)\u001b[0m\n", + "W: Skipping acquire of configured file 'main/source/Sources' as repository 'https://r2u.stat.illinois.edu/ubuntu jammy InRelease' does not seem to provide it (sources.list entry misspelt?)\n", "Reading package lists... Done\n", "Building dependency tree... Done\n", "Reading state information... Done\n", - "The following additional packages will be installed:\n", - " libpci3 pci.ids\n", "The following NEW packages will be installed:\n", - " libpci3 pci.ids pciutils\n", - "0 upgraded, 3 newly installed, 0 to remove and 128 not upgraded.\n", - "Need to get 343 kB of archives.\n", - "After this operation, 1,581 kB of additional disk space will be used.\n", - "Get:1 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 pci.ids all 0.0~2022.01.22-1ubuntu0.1 [251 kB]\n", - "Get:2 http://archive.ubuntu.com/ubuntu jammy/main amd64 libpci3 amd64 1:3.7.0-6 [28.9 kB]\n", - "Get:3 http://archive.ubuntu.com/ubuntu jammy/main amd64 pciutils amd64 1:3.7.0-6 [63.6 kB]\n", - "Fetched 343 kB in 1s (274 kB/s)\n", - "debconf: unable to initialize frontend: Dialog\n", - "debconf: (No usable dialog-like program is installed, so the dialog based frontend cannot be used. at /usr/share/perl5/Debconf/FrontEnd/Dialog.pm line 78, <> line 3.)\n", - "debconf: falling back to frontend: Readline\n", - "debconf: unable to initialize frontend: Readline\n", - "debconf: (This frontend requires a controlling tty.)\n", - "debconf: falling back to frontend: Teletype\n", - "dpkg-preconfigure: unable to re-open stdin: \n", - "Selecting previously unselected package pci.ids.\n", - "(Reading database ... 121852 files and directories currently installed.)\n", - "Preparing to unpack .../pci.ids_0.0~2022.01.22-1ubuntu0.1_all.deb ...\n", - "Unpacking pci.ids (0.0~2022.01.22-1ubuntu0.1) ...\n", - "Selecting previously unselected package libpci3:amd64.\n", - "Preparing to unpack .../libpci3_1%3a3.7.0-6_amd64.deb ...\n", - "Unpacking libpci3:amd64 (1:3.7.0-6) ...\n", - "Selecting previously unselected package pciutils.\n", - "Preparing to unpack .../pciutils_1%3a3.7.0-6_amd64.deb ...\n", - "Unpacking pciutils (1:3.7.0-6) ...\n", - "Setting up pci.ids (0.0~2022.01.22-1ubuntu0.1) ...\n", - "Setting up libpci3:amd64 (1:3.7.0-6) ...\n", - "Setting up pciutils (1:3.7.0-6) ...\n", - "Processing triggers for man-db (2.10.2-1) ...\n", - "Processing triggers for libc-bin (2.35-0ubuntu3.8) ...\n", - "/sbin/ldconfig.real: /usr/local/lib/libur_loader.so.0 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libur_adapter_opencl.so.0 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libtbbbind.so.3 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libur_adapter_level_zero.so.0 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libtbbbind_2_0.so.3 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libtbbbind_2_5.so.3 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libtbb.so.12 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libumf.so.1 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libtbbmalloc_proxy.so.2 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libtcm_debug.so.1 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libtcm.so.1 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libhwloc.so.15 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libur_adapter_level_zero_v2.so.0 is not a symbolic link\n", - "\n", - "/sbin/ldconfig.real: /usr/local/lib/libtbbmalloc.so.2 is not a symbolic link\n", - "\n", - ">>> Installing ollama to /usr/local\n", - "\u001b[1m\u001b[31mERROR:\u001b[m This version requires zstd for extraction. Please install zstd and try again:\n", - " - Debian/Ubuntu: sudo apt-get install zstd\n", - " - RHEL/CentOS/Fedora: sudo dnf install zstd\n", - " - Arch: sudo pacman -S zstd\n" + " zstd\n", + "0 upgraded, 1 newly installed, 0 to remove and 125 not upgraded.\n", + "Need to get 603 kB of archives.\n", + "After this operation, 1,695 kB of additional disk space will be used.\n", + "Get:1 http://archive.ubuntu.com/ubuntu jammy/main amd64 zstd amd64 1.4.8+dfsg-3build1 [603 kB]\n", + "Fetched 603 kB in 2s (362 kB/s)\n", + "Selecting previously unselected package zstd.\n", + "(Reading database ... 121874 files and directories currently installed.)\n", + "Preparing to unpack .../zstd_1.4.8+dfsg-3build1_amd64.deb ...\n", + "Unpacking zstd (1.4.8+dfsg-3build1) ...\n", + "Setting up zstd (1.4.8+dfsg-3build1) ...\n", + "Processing triggers for man-db (2.10.2-1) ...\n" ] } ] @@ -211,6 +128,40 @@ "id": "fGEJwjTPoKWH" } }, + { + "cell_type": "code", + "source": [ + "!curl -fsSL https://ollama.com/install.sh | sh" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "5mFKcxwgc7Jn", + "outputId": "d2c0f55d-cb79-4dec-99f7-66194b15ff9d" + }, + "execution_count": 6, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + ">>> Cleaning up old version at /usr/local/lib/ollama\n", + ">>> Installing ollama to /usr/local\n", + ">>> Downloading ollama-linux-amd64.tar.zst\n", + "######################################################################## 100.0%\n", + ">>> Creating ollama user...\n", + ">>> Adding ollama user to video group...\n", + ">>> Adding current user to ollama group...\n", + ">>> Creating ollama systemd service...\n", + "\u001b[1m\u001b[31mWARNING:\u001b[m systemd is not running\n", + ">>> NVIDIA GPU installed.\n", + ">>> The Ollama API is now available at 127.0.0.1:11434.\n", + ">>> Install complete. Run \"ollama\" from the command line.\n" + ] + } + ] + }, { "cell_type": "code", "source": [ @@ -218,38 +169,27 @@ "import subprocess\n", "import time\n", "\n", - "def run_ollama_serve():\n", - " subprocess.Popen([\"ollama\", \"serve\"])\n", + "def start_ollama():\n", + " subprocess.Popen([\"ollama\", \"serve\"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n", "\n", - "thread = threading.Thread(target=run_ollama_serve)\n", - "thread.start()\n", - "time.sleep(5)" + "threading.Thread(target=start_ollama, daemon=True).start()\n", + "time.sleep(5) # 等几秒让服务起来\n", + "print(\"Ollama 服务已启动!\")" ], "metadata": { "id": "Jh5CBAFxBYAC", "colab": { "base_uri": "https://localhost:8080/" }, - "outputId": "8210b189-c23d-41ee-b14f-52b5830cdc1c" + "outputId": "3b2b124c-03f3-4210-95e3-c1c466ff806a" }, - "execution_count": 3, + "execution_count": 14, "outputs": [ { "output_type": "stream", - "name": "stderr", + "name": "stdout", "text": [ - "Exception in thread Thread-4 (run_ollama_serve):\n", - "Traceback (most recent call last):\n", - " File \"/usr/lib/python3.12/threading.py\", line 1075, in _bootstrap_inner\n", - " self.run()\n", - " File \"/usr/lib/python3.12/threading.py\", line 1012, in run\n", - " self._target(*self._args, **self._kwargs)\n", - " File \"/tmp/ipykernel_3925/2856519456.py\", line 6, in run_ollama_serve\n", - " File \"/usr/lib/python3.12/subprocess.py\", line 1026, in __init__\n", - " self._execute_child(args, executable, preexec_fn, close_fds,\n", - " File \"/usr/lib/python3.12/subprocess.py\", line 1955, in _execute_child\n", - " raise child_exception_type(errno_num, err_msg, err_filename)\n", - "FileNotFoundError: [Errno 2] No such file or directory: 'ollama'\n" + "Ollama 服务已启动!\n" ] } ] @@ -257,12 +197,12 @@ { "cell_type": "markdown", "source": [ - "## Pulling Model\n", + "## 拉取模型\n", "---\n", "\n", - "Download the LLM model using `ollama pull llama3.2`.\n", + "使用 `ollama pull huihui_ai/qwen3.5-abliterated:9b` 下载 LLM 模型。\n", "\n", - "For other models check https://ollama.com/library" + "其他模型请查看 https://ollama.com/library" ], "metadata": { "id": "WcBLqZfyoHg4" @@ -271,23 +211,33 @@ { "cell_type": "code", "source": [ - "!ollama pull llama3.2" + "!ollama pull huihui_ai/qwen3.5-abliterated:9b" ], "metadata": { - "id": "o2ghppmRDFny" + "id": "o2ghppmRDFny", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "4d55c525-3316-40ec-9f2c-ec8b7c01f8de" }, - "execution_count": null, - "outputs": [] + "execution_count": 16, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\u001b[?2026h\u001b[?25l\u001b[1G\u001b[?25h\u001b[?2026l\n" + ] + } + ] }, { "cell_type": "markdown", "source": [ - "## And that's it!\n", - "---\n", - "\n", - "With this you should be able to freely play around with the models in your scripts. Following is an example using `langchain-ollama` to answer a simple prompt.\n", + "就是这样!\n", + "这样,您就可以在脚本中自由地使用模型了。下面是一个使用 langchain-ollama 回答简单提示的示例。\n", "\n", - "If you have a use-case that can help out others feel free to add your notebook to [Collama](https://github.com/5aharsh/collama/fork)" + "如果您有可以帮助他人的用例,请随时将您的笔记本添加到 Collama 中。" ], "metadata": { "id": "TYQJNeTuni_6" @@ -296,38 +246,67 @@ { "cell_type": "code", "source": [ - "!pip install langchain-ollama" + "!pip install ollama" ], "metadata": { - "id": "MbrT39oil6tK" + "id": "MbrT39oil6tK", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "f840830f-e454-4343-aad4-4984e5ce1d34" }, - "execution_count": null, - "outputs": [] + "execution_count": 11, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Requirement already satisfied: ollama in /usr/local/lib/python3.12/dist-packages (0.6.1)\n", + "Requirement already satisfied: httpx>=0.27 in /usr/local/lib/python3.12/dist-packages (from ollama) (0.28.1)\n", + "Requirement already satisfied: pydantic>=2.9 in /usr/local/lib/python3.12/dist-packages (from ollama) (2.12.3)\n", + "Requirement already satisfied: anyio in /usr/local/lib/python3.12/dist-packages (from httpx>=0.27->ollama) (4.12.1)\n", + "Requirement already satisfied: certifi in /usr/local/lib/python3.12/dist-packages (from httpx>=0.27->ollama) (2026.2.25)\n", + "Requirement already satisfied: httpcore==1.* in /usr/local/lib/python3.12/dist-packages (from httpx>=0.27->ollama) (1.0.9)\n", + "Requirement already satisfied: idna in /usr/local/lib/python3.12/dist-packages (from httpx>=0.27->ollama) (3.11)\n", + "Requirement already satisfied: h11>=0.16 in /usr/local/lib/python3.12/dist-packages (from httpcore==1.*->httpx>=0.27->ollama) (0.16.0)\n", + "Requirement already satisfied: annotated-types>=0.6.0 in /usr/local/lib/python3.12/dist-packages (from pydantic>=2.9->ollama) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.41.4 in /usr/local/lib/python3.12/dist-packages (from pydantic>=2.9->ollama) (2.41.4)\n", + "Requirement already satisfied: typing-extensions>=4.14.1 in /usr/local/lib/python3.12/dist-packages (from pydantic>=2.9->ollama) (4.15.0)\n", + "Requirement already satisfied: typing-inspection>=0.4.2 in /usr/local/lib/python3.12/dist-packages (from pydantic>=2.9->ollama) (0.4.2)\n" + ] + } + ] }, { "cell_type": "code", "source": [ - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_ollama.llms import OllamaLLM\n", - "from IPython.display import Markdown\n", - "\n", - "template = \"\"\"Question: {question}\n", - "\n", - "Answer: Let's think step by step.\"\"\"\n", + "import ollama\n", "\n", - "prompt = ChatPromptTemplate.from_template(template)\n", - "\n", - "model = OllamaLLM(model=\"llama3.2\")\n", - "\n", - "chain = prompt | model\n", - "\n", - "display(Markdown(chain.invoke({\"question\": \"What's the length of hypotenuse in a right angled triangle\"})))" + "response = ollama.chat(model='huihui_ai/qwen3.5-abliterated:9b', messages=[\n", + " {'role': 'user', 'content': '你好,测试一下是否正常!'},\n", + "])\n", + "print(response['message']['content'])" ], "metadata": { - "id": "9quBP56zDvpt" + "id": "9quBP56zDvpt", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "0040d2a6-8d0d-4fe2-d41d-2fb0586a5104" }, - "execution_count": null, - "outputs": [] + "execution_count": 17, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "你好!👋 收到你的“信号”,看来一切运行正常!\n", + "\n", + "随时准备为你效劳~ 今天想先试试什么?💡\n", + "是闲聊、查资料,还是处理个什么小任务?😊\n" + ] + } + ] } ] } \ No newline at end of file From 65b644d60ffcab68cf85f8a73fbb0d541a3a864f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AF=E8=BE=BE=E9=B8=AD?= <96913323+kiss-kedaya@users.noreply.github.com> Date: Mon, 16 Mar 2026 06:16:34 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20Colab=20=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E8=80=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ollama_Setup.ipynb | 210 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 203 insertions(+), 7 deletions(-) diff --git a/Ollama_Setup.ipynb b/Ollama_Setup.ipynb index 2c1eae9..3b77fe4 100644 --- a/Ollama_Setup.ipynb +++ b/Ollama_Setup.ipynb @@ -181,9 +181,9 @@ "colab": { "base_uri": "https://localhost:8080/" }, - "outputId": "3b2b124c-03f3-4210-95e3-c1c466ff806a" + "outputId": "114efb79-e1d6-4c3d-ceef-9072d5f743ca" }, - "execution_count": 14, + "execution_count": 42, "outputs": [ { "output_type": "stream", @@ -292,18 +292,214 @@ "colab": { "base_uri": "https://localhost:8080/" }, - "outputId": "0040d2a6-8d0d-4fe2-d41d-2fb0586a5104" + "outputId": "ddc51fa8-cf5d-4d6d-8a92-71cb7623ecd1" }, - "execution_count": 17, + "execution_count": 21, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ - "你好!👋 收到你的“信号”,看来一切运行正常!\n", + "你好!👋\n", "\n", - "随时准备为你效劳~ 今天想先试试什么?💡\n", - "是闲聊、查资料,还是处理个什么小任务?😊\n" + "一切正常,状态在线!📊\n", + "\n", + "看来我们的“连接”很稳定~ 不管是处理任务、查资料,还是随便闲聊,我都是“满血”状态~🔋\n", + "\n", + "请随时吩咐,今天想先聊点什么?😄\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "!wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n", + "!dpkg -i cloudflared-linux-amd64.deb" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "vZz0BFFhfYRN", + "outputId": "85f65995-1700-430e-8255-3ebe1b36a8c3" + }, + "execution_count": 31, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "--2026-03-15 22:06:07-- https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n", + "Resolving github.com (github.com)... 20.205.243.166\n", + "Connecting to github.com (github.com)|20.205.243.166|:443... connected.\n", + "HTTP request sent, awaiting response... 302 Found\n", + "Location: https://github.com/cloudflare/cloudflared/releases/download/2026.3.0/cloudflared-linux-amd64.deb [following]\n", + "--2026-03-15 22:06:07-- https://github.com/cloudflare/cloudflared/releases/download/2026.3.0/cloudflared-linux-amd64.deb\n", + "Reusing existing connection to github.com:443.\n", + "HTTP request sent, awaiting response... 302 Found\n", + "Location: https://release-assets.githubusercontent.com/github-production-release-asset/106867604/ec689fe1-d727-4ebd-bbc3-5967730ab54e?sp=r&sv=2018-11-09&sr=b&spr=https&se=2026-03-15T23%3A01%3A44Z&rscd=attachment%3B+filename%3Dcloudflared-linux-amd64.deb&rsct=application%2Foctet-stream&skoid=96c2d410-5711-43a1-aedd-ab1947aa7ab0&sktid=398a6654-997b-47e9-b12b-9515b896b4de&skt=2026-03-15T22%3A01%3A38Z&ske=2026-03-15T23%3A01%3A44Z&sks=b&skv=2018-11-09&sig=lgmxxJOMKU86%2BHgtXP2p9R6LyqUYcTXSCOFtVccTkPI%3D&jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmVsZWFzZS1hc3NldHMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwia2V5Ijoia2V5MSIsImV4cCI6MTc3MzYxNDE2OCwibmJmIjoxNzczNjEyMzY4LCJwYXRoIjoicmVsZWFzZWFzc2V0cHJvZHVjdGlvbi5ibG9iLmNvcmUud2luZG93cy5uZXQifQ.TnCK22qFBimNM-rjZKxG22oBFJkBRw3dE_xsNB3BTZc&response-content-disposition=attachment%3B%20filename%3Dcloudflared-linux-amd64.deb&response-content-type=application%2Foctet-stream [following]\n", + "--2026-03-15 22:06:08-- https://release-assets.githubusercontent.com/github-production-release-asset/106867604/ec689fe1-d727-4ebd-bbc3-5967730ab54e?sp=r&sv=2018-11-09&sr=b&spr=https&se=2026-03-15T23%3A01%3A44Z&rscd=attachment%3B+filename%3Dcloudflared-linux-amd64.deb&rsct=application%2Foctet-stream&skoid=96c2d410-5711-43a1-aedd-ab1947aa7ab0&sktid=398a6654-997b-47e9-b12b-9515b896b4de&skt=2026-03-15T22%3A01%3A38Z&ske=2026-03-15T23%3A01%3A44Z&sks=b&skv=2018-11-09&sig=lgmxxJOMKU86%2BHgtXP2p9R6LyqUYcTXSCOFtVccTkPI%3D&jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmVsZWFzZS1hc3NldHMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwia2V5Ijoia2V5MSIsImV4cCI6MTc3MzYxNDE2OCwibmJmIjoxNzczNjEyMzY4LCJwYXRoIjoicmVsZWFzZWFzc2V0cHJvZHVjdGlvbi5ibG9iLmNvcmUud2luZG93cy5uZXQifQ.TnCK22qFBimNM-rjZKxG22oBFJkBRw3dE_xsNB3BTZc&response-content-disposition=attachment%3B%20filename%3Dcloudflared-linux-amd64.deb&response-content-type=application%2Foctet-stream\n", + "Resolving release-assets.githubusercontent.com (release-assets.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...\n", + "Connecting to release-assets.githubusercontent.com (release-assets.githubusercontent.com)|185.199.108.133|:443... connected.\n", + "HTTP request sent, awaiting response... 200 OK\n", + "Length: 19308942 (18M) [application/octet-stream]\n", + "Saving to: ‘cloudflared-linux-amd64.deb.1’\n", + "\n", + "cloudflared-linux-a 100%[===================>] 18.41M --.-KB/s in 0.03s \n", + "\n", + "2026-03-15 22:06:08 (530 MB/s) - ‘cloudflared-linux-amd64.deb.1’ saved [19308942/19308942]\n", + "\n", + "(Reading database ... 121899 files and directories currently installed.)\n", + "Preparing to unpack cloudflared-linux-amd64.deb ...\n", + "Unpacking cloudflared (2026.3.0) over (2026.3.0) ...\n", + "Setting up cloudflared (2026.3.0) ...\n", + "Processing triggers for man-db (2.10.2-1) ...\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "import subprocess\n", + "import threading\n", + "import time\n", + "import socket\n", + "import re # 用于从日志提取 URL\n", + "\n", + "def start_cloudflare_tunnel(port=11434):\n", + " print(\"启动 Cloudflare Tunnel(后台模式)...\")\n", + "\n", + " # 检查端口是否已开(Ollama 就绪)\n", + " timeout = 60\n", + " start = time.time()\n", + " while time.time() - start < timeout:\n", + " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n", + " result = sock.connect_ex(('127.0.0.1', port))\n", + " sock.close()\n", + " if result == 0:\n", + " print(\"Ollama 端口已就绪,开始创建隧道...\")\n", + " break\n", + " time.sleep(1)\n", + " else:\n", + " print(\"超时:Ollama 未启动,请先运行 !ollama serve &\")\n", + " return\n", + "\n", + " # cloudflared 命令:加 --http-host-header 修复 403,加 --loglevel debug 看日志\n", + " cmd = [\n", + " \"cloudflared\", \"tunnel\",\n", + " \"--url\", f\"http://localhost:{port}\",\n", + " \"--http-host-header\", \"localhost:11434\",\n", + " \"--loglevel\", \"debug\"\n", + " ]\n", + "\n", + " # 用 Popen 后台启动,捕获 stdout/stderr\n", + " process = subprocess.Popen(\n", + " cmd,\n", + " stdout=subprocess.PIPE,\n", + " stderr=subprocess.STDOUT,\n", + " text=True,\n", + " bufsize=1,\n", + " universal_newlines=True\n", + " )\n", + "\n", + " # 实时读取日志,提取 URL 并打印\n", + " url_found = False\n", + " for line in iter(process.stdout.readline, ''):\n", + " line = line.strip()\n", + " if line:\n", + " print(line) # 打印所有日志,便于调试\n", + "\n", + " # 匹配 trycloudflare.com URL\n", + " match = re.search(r'(https?://[^\\s]+\\.trycloudflare\\.com)', line)\n", + " if match and not url_found:\n", + " public_url = match.group(1)\n", + " print(\"\\n\" + \"=\"*60)\n", + " print(f\"🎉 Cloudflare Tunnel 后台启动成功!公共 URL: {public_url}\")\n", + " print(f\"测试模型列表: {public_url}/api/tags\")\n", + " print(f\"OpenAI 兼容 Base URL: {public_url}/v1\")\n", + " print(\"隧道已作为后台进程运行,不会阻塞 cell。\")\n", + " print(\"=\"*60 + \"\\n\")\n", + " url_found = True\n", + "\n", + " # 可选:如果想在找到 URL 后继续打印日志或 break\n", + " # if url_found: break # 注释掉这行以持续看日志\n", + "\n", + " # 等待进程结束(实际不会结束,除非手动杀)\n", + " process.wait()\n", + "\n", + "# 用线程启动隧道(daemon=True 让它随 runtime 结束)\n", + "thread = threading.Thread(target=start_cloudflare_tunnel, daemon=True)\n", + "thread.start()\n", + "\n", + "print(\"隧道线程已启动!你可以继续执行其他 cell。\")\n", + "print(\"等 10-60 秒,看上面日志中是否出现 URL。\")\n", + "time.sleep(60)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "UqlsBsR7hayD", + "outputId": "d6a141e5-babf-4afa-b9a1-5aaefdbb105e" + }, + "execution_count": 43, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "启动 Cloudflare Tunnel(后台模式)...隧道线程已启动!你可以继续执行其他 cell。\n", + "等 10-60 秒,看上面日志中是否出现 URL。\n", + "\n", + "Ollama 端口已就绪,开始创建隧道...\n", + "2026-03-15T22:13:48Z INF Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee, are subject to the Cloudflare Online Services Terms of Use (https://www.cloudflare.com/website-terms/), and Cloudflare reserves the right to investigate your use of Tunnels for violations of such terms. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps\n", + "2026-03-15T22:13:48Z INF Requesting new quick Tunnel on trycloudflare.com...\n", + "2026-03-15T22:13:53Z INF +--------------------------------------------------------------------------------------------+\n", + "2026-03-15T22:13:53Z INF | Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): |\n", + "2026-03-15T22:13:53Z INF | https://adequate-certificate-recent-benchmark.trycloudflare.com |\n", + "\n", + "============================================================\n", + "🎉 Cloudflare Tunnel 后台启动成功!公共 URL: https://adequate-certificate-recent-benchmark.trycloudflare.com\n", + "测试模型列表: https://adequate-certificate-recent-benchmark.trycloudflare.com/api/tags\n", + "OpenAI 兼容 Base URL: https://adequate-certificate-recent-benchmark.trycloudflare.com/v1\n", + "隧道已作为后台进程运行,不会阻塞 cell。\n", + "============================================================\n", + "\n", + "2026-03-15T22:13:53Z INF +--------------------------------------------------------------------------------------------+\n", + "2026-03-15T22:13:53Z INF Cannot determine default configuration path. No file [config.yml config.yaml] in [~/.cloudflared ~/.cloudflare-warp ~/cloudflare-warp /etc/cloudflared /usr/local/etc/cloudflared]\n", + "2026-03-15T22:13:53Z INF Version 2026.3.0 (Checksum 4a9e50e6d6d798e90fcd01933151a90bf7edd99a0a55c28ad18f2e16263a5c30)\n", + "2026-03-15T22:13:53Z INF GOOS: linux, GOVersion: go1.24.13, GoArch: amd64\n", + "2026-03-15T22:13:53Z INF Settings: map[ha-connections:1 http-host-header:localhost:11434 loglevel:debug protocol:quic url:http://localhost:11434]\n", + "2026-03-15T22:13:53Z INF cloudflared will not automatically update if installed by a package manager.\n", + "2026-03-15T22:13:53Z INF Generated Connector ID: 3973d056-105c-4faf-b7ff-216433445fb4\n", + "2026-03-15T22:13:53Z DBG Fetched protocol: quic\n", + "2026-03-15T22:13:53Z INF Initial protocol quic\n", + "2026-03-15T22:13:53Z INF ICMP proxy will use 172.28.0.12 as source for IPv4\n", + "2026-03-15T22:13:53Z INF ICMP proxy will use ::1 in zone lo as source for IPv6\n", + "2026-03-15T22:13:53Z DBG edge discovery: looking up edge SRV record domain=_v2-origintunneld._tcp.argotunnel.com event=0\n", + "2026-03-15T22:13:53Z DBG edge discovery: resolved edge addresses addresses=[\"198.41.192.27\",\"198.41.192.167\",\"198.41.192.47\",\"198.41.192.37\",\"198.41.192.67\",\"198.41.192.107\",\"198.41.192.57\",\"198.41.192.77\",\"198.41.192.7\",\"198.41.192.227\",\"2606:4700:a0::5\",\"2606:4700:a0::3\",\"2606:4700:a0::8\",\"2606:4700:a0::7\",\"2606:4700:a0::4\",\"2606:4700:a0::2\",\"2606:4700:a0::10\",\"2606:4700:a0::6\",\"2606:4700:a0::1\",\"2606:4700:a0::9\"] event=0\n", + "2026-03-15T22:13:53Z DBG edge discovery: resolved edge addresses addresses=[\"198.41.200.193\",\"198.41.200.113\",\"198.41.200.73\",\"198.41.200.233\",\"198.41.200.13\",\"198.41.200.63\",\"198.41.200.33\",\"198.41.200.23\",\"198.41.200.53\",\"198.41.200.43\",\"2606:4700:a8::5\",\"2606:4700:a8::2\",\"2606:4700:a8::4\",\"2606:4700:a8::1\",\"2606:4700:a8::9\",\"2606:4700:a8::7\",\"2606:4700:a8::3\",\"2606:4700:a8::6\",\"2606:4700:a8::8\",\"2606:4700:a8::10\"] event=0\n", + "2026-03-15T22:13:53Z INF ICMP proxy will use 172.28.0.12 as source for IPv4\n", + "2026-03-15T22:13:53Z INF ICMP proxy will use ::1 in zone lo as source for IPv6\n", + "2026-03-15T22:13:53Z DBG edge discovery: looking up edge SRV record domain=_v2-origintunneld._tcp.argotunnel.com event=0\n", + "2026-03-15T22:13:53Z INF Starting metrics server on 127.0.0.1:20241/metrics\n", + "2026-03-15T22:13:53Z DBG edge discovery: resolved edge addresses addresses=[\"198.41.192.27\",\"198.41.192.167\",\"198.41.192.47\",\"198.41.192.37\",\"198.41.192.67\",\"198.41.192.107\",\"198.41.192.57\",\"198.41.192.77\",\"198.41.192.7\",\"198.41.192.227\",\"2606:4700:a0::4\",\"2606:4700:a0::10\",\"2606:4700:a0::5\",\"2606:4700:a0::9\",\"2606:4700:a0::6\",\"2606:4700:a0::3\",\"2606:4700:a0::1\",\"2606:4700:a0::8\",\"2606:4700:a0::2\",\"2606:4700:a0::7\"] event=0\n", + "2026-03-15T22:13:53Z DBG edge discovery: resolved edge addresses addresses=[\"198.41.200.73\",\"198.41.200.193\",\"198.41.200.23\",\"198.41.200.233\",\"198.41.200.63\",\"198.41.200.13\",\"198.41.200.33\",\"198.41.200.53\",\"198.41.200.43\",\"198.41.200.113\",\"2606:4700:a8::7\",\"2606:4700:a8::1\",\"2606:4700:a8::5\",\"2606:4700:a8::6\",\"2606:4700:a8::4\",\"2606:4700:a8::8\",\"2606:4700:a8::3\",\"2606:4700:a8::10\",\"2606:4700:a8::2\",\"2606:4700:a8::9\"] event=0\n", + "2026-03-15T22:13:53Z DBG edge discovery: giving new address to connection connIndex=0 event=0 ip=198.41.200.73\n", + "2026-03-15T22:13:53Z DBG Tunnel connection options connIndex=0 event=0 features=[\"support_quic_eof\",\"management_logs\",\"allow_remote_config\",\"serialized_headers\",\"support_datagram_v2\"] ip=198.41.200.73\n", + "2026-03-15T22:13:53Z INF Tunnel connection curve preferences: [X25519MLKEM768 CurveP256] connIndex=0 event=0 ip=198.41.200.73\n", + "2026/03/15 22:13:53 failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB). See https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes for details.\n", + "2026-03-15T22:13:53Z DBG Updating DNS local resolver: 127.0.0.11:53\n", + "2026-03-15T22:13:53Z DBG Received transport parameters: MaxUDPPayloadSize=1396, MaxIdleTimeout=5s, MaxDatagramFrameSize=16383 connIndex=0 event=0 ip=198.41.200.73\n", + "2026-03-15T22:13:53Z DBG Registering tunnel connection connIndex=0 event=0 ip=198.41.200.73 protocol=quic\n", + "2026-03-15T22:13:53Z DBG QUIC MTU updated to 1314 connIndex=0 event=0 ip=198.41.200.73\n", + "2026-03-15T22:13:53Z DBG QUIC MTU updated to 1355 connIndex=0 event=0 ip=198.41.200.73\n", + "2026-03-15T22:13:53Z DBG QUIC MTU updated to 1375 connIndex=0 event=0 ip=198.41.200.73\n", + "2026-03-15T22:13:54Z INF Registered tunnel connection connIndex=0 connection=0aadd666-4a29-4c35-8a33-88b557747c66 event=0 ip=198.41.200.73 location=sin02 protocol=quic\n", + "2026-03-15T22:14:04Z DBG GET https://adequate-certificate-recent-benchmark.trycloudflare.com/v1/models HTTP/1.1 connIndex=0 content-length=0 event=1 headers={\"Accept\":[\"application/json\"],\"Accept-Encoding\":[\"gzip\"],\"Authorization\":[\"Bearer ollama-local\"],\"Cdn-Loop\":[\"cloudflare; loops=1; subreqs=1\"],\"Cf-Connecting-Ip\":[\"171.37.25.194\"],\"Cf-Ew-Via\":[\"15\"],\"Cf-Ipcountry\":[\"CN\"],\"Cf-Ray\":[\"9dcede76f12edcee-SIN\"],\"Cf-Visitor\":[\"{\\\"scheme\\\":\\\"https\\\"}\"],\"Cf-Warp-Tag-Id\":[\"3973d056-105c-4faf-b7ff-216433445fb4\"],\"Cf-Worker\":[\"trycloudflare.com\"],\"Content-Type\":[\"application/json\"],\"User-Agent\":[\"OpenAI/JS 4.104.0\"],\"X-Forwarded-For\":[\"171.37.25.194\"],\"X-Forwarded-Proto\":[\"https\"],\"X-Stainless-Arch\":[\"x64\"],\"X-Stainless-Lang\":[\"js\"],\"X-Stainless-Os\":[\"Windows\"],\"X-Stainless-Package-Version\":[\"4.104.0\"],\"X-Stainless-Retry-Count\":[\"0\"],\"X-Stainless-Runtime\":[\"node\"],\"X-Stainless-Runtime-Version\":[\"v22.21.1\"],\"X-Stainless-Timeout\":[\"60\"]} host=adequate-certificate-recent-benchmark.trycloudflare.com ingressRule=0 originService=http://localhost:11434 path=/v1/models\n", + "2026-03-15T22:14:04Z DBG 200 OK connIndex=0 content-length=130 event=1 ingressRule=0 originService=http://localhost:11434\n" ] } ]