使用deepseek完成的一个工具。
#灵感#创造游戏自动登录盒子#闲杂时光#1.填充账号密码到表格表格使用数据库保存。2.输入游戏路径.可搜索目前搜索不好用。3.点击开始后程序拉起游戏客户端4.获取游戏输入账号密码框登录按钮位置、5.将表格的数据填充填充完后自动点击登录。6.有日志记录当出现错误有提示。# -*- coding: utf-8 -*- QQ飞车 账号管理器 功能 - 账号表格序号/账号/密码/大区/状态双击编辑自动保存至 SQLite - 游戏路径查找/校验/保存 - 定时启动游戏指定时刻 - 自动记录控件特征账号框/密码框/登录按钮 - 启动游戏后自动填写账号密码并点击登录 import tkinter as tk from tkinter import ttk import tkinter.messagebox import sqlite3 import datetime import os import threading import time import sys from database import Database from log_panel import LogPanel import win32_helper as wh # Windows API 相关需安装 pywin32 try: import win32gui import win32con import win32process import psutil except ImportError as e: tk.messagebox.showerror(依赖缺失, f请先安装必要的库pip install pywin32 psutil\n错误详情{e}) sys.exit(1) class PrintLogger: 将 print 输出重定向到 LogPanel 的 log 方法 def __init__(self, log_panel, levelINFO): self.log_panel log_panel self.level level def write(self, message): if message and not message.isspace(): # 去掉末尾换行符每条 print 作为单独一条日志 self.log_panel.log(message.rstrip(), self.level) def flush(self): pass # 必须实现以满足 file-like 对象要求 class App(tk.Tk): def __init__(self): super().__init__() self.title(QQ飞车 账号管理器) self.geometry(950x600) # 初始化数据库 self.db Database(accounts.db) # 创建界面 self.create_widgets() # 重定向 print 输出到日志面板 self.log_printer PrintLogger(self.log_panel) sys.stdout self.log_printer self.load_data() # 加载保存的游戏路径 self.load_game_path() # 启动时钟更新 self.update_clock() # 用于编辑的临时控件 self.edit_widget None self.scheduled_after_id None # 定时器 ID # ---------- 配置存取 ---------- def load_game_path(self): path self.db.get_config(game_path) if path: self.game_path_var.set(path) def save_game_path(self, path): self.db.set_config(game_path, path) def clear_game_path(self): self.db.delete_config(game_path) self.game_path_var.set() # ---------- 界面构建 ---------- def create_widgets(self): # 主容器左右分栏 main_paned ttk.PanedWindow(self, orienttk.HORIZONTAL) main_paned.pack(filltk.BOTH, expandTrue, padx10, pady10) # 左侧面板原有所有内容 left_frame ttk.Frame(main_paned) main_paned.add(left_frame, weight5) # 左侧占较大比例 # 右侧面板日志面板 self.log_panel LogPanel(main_paned) main_paned.add(self.log_panel, weight1) # 右侧占较小比例 #... main_frame ttk.Frame(left_frame) main_frame.pack(filltk.BOTH, expandTrue, padx10, pady10) # ----- 表格区域 ----- table_frame ttk.Frame(main_frame) table_frame.pack(filltk.BOTH, expandTrue) scrollbar ttk.Scrollbar(table_frame, orienttk.VERTICAL) scrollbar.pack(sidetk.RIGHT, filltk.Y) columns (序号, 账号, 密码, 大区, 状态) self.tree ttk.Treeview( table_frame, columnscolumns, showheadings, yscrollcommandscrollbar.set ) self.tree.pack(sidetk.LEFT, filltk.BOTH, expandTrue) scrollbar.config(commandself.tree.yview) self.tree.heading(序号, text序号) self.tree.heading(账号, text账号) self.tree.heading(密码, text密码) self.tree.heading(大区, text大区) self.tree.heading(状态, text状态) self.tree.column(序号, width50, anchortk.CENTER) self.tree.column(账号, width180) self.tree.column(密码, width100) self.tree.column(大区, width120) self.tree.column(状态, width100) self.tree.bind(Double-1, self.on_double_click) self.tree.bind(Button-3, self.show_context_menu) self.context_menu tk.Menu(self, tearoff0) self.context_menu.add_command(label删除选中行, commandself.delete_selected) # ----- 第一行控制栏定时、时间、游戏启停、时钟----- control_frame1 ttk.Frame(main_frame) control_frame1.pack(filltk.X, pady(10, 5)) self.timer_var tk.BooleanVar() ttk.Checkbutton(control_frame1, text定时选中, variableself.timer_var).pack(sidetk.LEFT, padx5) ttk.Label(control_frame1, text时).pack(sidetk.LEFT, padx(10, 2)) self.hour_entry ttk.Entry(control_frame1, width4) self.hour_entry.pack(sidetk.LEFT) ttk.Label(control_frame1, text分).pack(sidetk.LEFT, padx(5, 2)) self.minute_entry ttk.Entry(control_frame1, width4) self.minute_entry.pack(sidetk.LEFT) ttk.Label(control_frame1, text秒).pack(sidetk.LEFT, padx(5, 2)) self.second_entry ttk.Entry(control_frame1, width4) self.second_entry.pack(sidetk.LEFT) self.start_btn ttk.Button(control_frame1, text启动游戏, commandself.on_start_game) self.start_btn.pack(sidetk.LEFT, padx(20, 5)) self.stop_btn ttk.Button(control_frame1, text关闭游戏, commandself.on_stop_game) self.stop_btn.pack(sidetk.LEFT, padx5) self.clock_label ttk.Label(control_frame1, text, font(TkDefaultFont, 10, bold)) self.clock_label.pack(sidetk.RIGHT, padx10) # ----- 第二行控制栏游戏路径相关----- control_frame2 ttk.Frame(main_frame) control_frame2.pack(filltk.X, pady(5, 0)) ttk.Label(control_frame2, text游戏路径:).pack(sidetk.LEFT, padx(0, 5)) self.game_path_var tk.StringVar() self.path_entry ttk.Entry(control_frame2, textvariableself.game_path_var, width45) self.path_entry.pack(sidetk.LEFT, filltk.X, expandTrue, padx(0, 5)) self.search_btn ttk.Button(control_frame2, text查找游戏, commandself.start_search_game) self.search_btn.pack(sidetk.LEFT, padx5) self.verify_btn ttk.Button(control_frame2, text校验游戏, commandself.verify_game_path) self.verify_btn.pack(sidetk.LEFT, padx5) self.record_btn ttk.Button(control_frame2, text记录控件, commandself.record_controls) self.record_btn.pack(sidetk.LEFT, padx5) self.clear_path_btn ttk.Button(control_frame2, text清除数据, commandself.on_clear_path) self.clear_path_btn.pack(sidetk.LEFT, padx5) self.searching False # ---------- 辅助功能 ---------- def update_clock(self): now datetime.datetime.now() time_str now.strftime(%Y-%m-%d %H:%M:%S) self.clock_label.config(textf北京时间{time_str}) self.after(1000, self.update_clock) def on_clear_path(self): self.clear_game_path() def on_stop_game(self): print(关闭游戏功能待实现) # ---------- 表格数据加载 ---------- def load_data(self): for item in self.tree.get_children(): self.tree.delete(item) rows self.db.get_all_accounts() for row in rows: self.tree.insert(, tk.END, iidstr(row[0]), values(row[0], row[1], ***, row[3], row[4])) # ---------- 双击编辑表格 ---------- def on_double_click(self, event): if self.edit_widget: self.cancel_edit() region_click self.tree.identify_region(event.x, event.y) if region_click ! cell: return column self.tree.identify_column(event.x) item self.tree.identify_row(event.y) if not item: return col_index int(column[1:]) - 1 if col_index not in (1, 2, 3, 4): return values self.tree.item(item, values) if col_index 2: # 密码列从数据库取真实值 cursor self.db.get_cursor() cursor.execute(SELECT password FROM accounts WHERE id?, (item,)) real_pwd cursor.fetchone()[0] current_value real_pwd else: current_value values[col_index] bbox self.tree.bbox(item, column) if not bbox: return x, y, width, height bbox if col_index in (3, 4): # 大区、状态使用下拉框 options [电信区, 联通区, 电信二区] if col_index 3 else [未登录, 登录中, 游戏中, 冻结] widget ttk.Combobox(self.tree, valuesoptions, statereadonly) widget.set(current_value) else: widget tk.Entry(self.tree, font(TkDefaultFont, 10)) widget.insert(0, current_value) widget.select_range(0, tk.END) widget.place(xx, yy, widthwidth, heightheight) widget.focus() widget.bind(Return, lambda e: self.save_edit(item, col_index)) widget.bind(Escape, lambda e: self.cancel_edit()) if col_index in (1, 2): widget.bind(FocusOut, lambda e: self.save_edit(item, col_index)) else: widget.bind(ComboboxSelected, lambda e: self.save_edit(item, col_index)) widget.bind(FocusOut, lambda e: self.save_edit(item, col_index)) self.edit_widget widget self.edit_item item self.edit_col col_index def save_edit(self, item, col_index): if not self.edit_widget: return new_value self.edit_widget.get().strip() self.cancel_edit() if not new_value: return field {1: account, 2: password, 3: region, 4: status}[col_index] self.db.update_account_field(int(item), field, new_value) # 刷新显示 row self.db.get_all_accounts() # 简单起见重载全部也可单独查询 # 这里为简洁重新加载整表 self.load_data() def cancel_edit(self): if self.edit_widget: self.edit_widget.destroy() self.edit_widget None def show_context_menu(self, event): item self.tree.identify_row(event.y) if item: self.tree.selection_set(item) self.context_menu.post(event.x_root, event.y_root) def delete_selected(self): selected self.tree.selection() if not selected: return item selected[0] self.db.delete_account(int(item)) self.tree.delete(item) print(f已删除账号 ID{item}) # ---------- 游戏路径查找/校验 ---------- def start_search_game(self): if self.searching: return self.searching True self.search_btn.config(statedisabled, text搜索中...) threading.Thread(targetself.search_game, daemonTrue).start() def search_game(self): target_exe QQSpeedLauncher.exe found_path None timeout 30 start_time time.time() drives [f{d}:\\ for d in [C, D] if os.path.exists(f{d}:\\)] for drive in drives: if found_path: break try: for root, dirs, files in os.walk(drive): if time.time() - start_time timeout: self.after(0, self.on_search_timeout) return if any(skip in root.lower() for skip in [windows, programdata, $recycle.bin]): continue if target_exe in files: full_path os.path.join(root, target_exe) if QQ飞车 in full_path: found_path full_path break except PermissionError: continue if found_path: self.after(0, self.on_search_complete, found_path) else: self.after(0, self.on_search_complete, None) def on_search_timeout(self): self.search_btn.config(statenormal, text查找游戏) self.searching False tk.messagebox.showwarning(搜索超时, 搜索时间超过30秒已自动停止。) def on_search_complete(self, path): self.search_btn.config(statenormal, text查找游戏) self.searching False if path: self.game_path_var.set(path) self.save_game_path(path) tk.messagebox.showinfo(查找成功, f已找到游戏路径\n{path}) else: tk.messagebox.showinfo(查找结果, 未找到 QQ飞车 游戏。) def verify_game_path(self): path self.game_path_var.get().strip() if not path: tk.messagebox.showwarning(校验失败, 游戏路径不能为空) return if not os.path.exists(path) or not os.path.isfile(path): tk.messagebox.showwarning(校验失败, 文件不存在或不是有效文件。) return if os.path.basename(path) ! QQSpeedLauncher.exe: tk.messagebox.showwarning(校验失败, 文件名必须是 QQSpeedLauncher.exe) return if QQ飞车 not in path: tk.messagebox.showwarning(校验失败, 路径中必须包含“QQ飞车”文件夹) return self.save_game_path(path) tk.messagebox.showinfo(校验成功, f游戏路径有效已保存\n{path}) # ---------- 记录控件特征 ---------- def record_controls(self): 自动查找游戏窗口并尝试智能识别账号框、密码框、登录按钮。 若识别成功则自动保存若失败则提示用户手动点击记录。 # 1. 通过进程名找到游戏窗口 pid None for proc in psutil.process_iter([name]): if proc.info[name] QQSpeed_loader.exe: pid proc.pid break if not pid: tk.messagebox.showerror(错误, 未找到游戏进程 (QQSpeed_loader.exe)请先启动游戏。) return hwnd wh.get_main_window_by_pid(pid) if not hwnd: tk.messagebox.showerror(错误, 无法获取游戏主窗口句柄。) return # 将窗口置顶便于观察非必须 wh.bring_window_to_front(hwnd) # 2. 智能识别控件 account_hwnd, password_hwnd, login_btn_hwnd wh.smart_detect_controls(hwnd) if account_hwnd and password_hwnd and login_btn_hwnd: # 自动保存 self._save_detected_controls(account_hwnd, password_hwnd, login_btn_hwnd) tk.messagebox.showinfo(成功, 已自动识别并保存控件特征。) else: # 自动识别失败提供手动选择或交互式记录 if tk.messagebox.askyesno(自动识别失败, 未能完全自动识别控件。是否进行手动记录\n(点击游戏窗口中的控件并按F2)): self._interactive_record(hwnd) else: tk.messagebox.showinfo(取消, 未保存控件特征。) def _save_detected_controls(self, account_hwnd, password_hwnd, login_btn_hwnd): 保存识别出的控件特征到数据库 acc_cls win32gui.GetClassName(account_hwnd) acc_txt win32gui.GetWindowText(account_hwnd) pw_cls win32gui.GetClassName(password_hwnd) pw_txt win32gui.GetWindowText(password_hwnd) btn_cls win32gui.GetClassName(login_btn_hwnd) btn_txt win32gui.GetWindowText(login_btn_hwnd) self.db.save_control_info(account_edit, Edit, acc_cls, acc_txt, , ) self.db.save_control_info(password_edit, Edit, pw_cls, pw_txt, , ) self.db.save_control_info(login_button, Button, btn_cls, btn_txt, , ) print(f保存控件特征: 账号框({acc_cls}, {acc_txt}), 密码框({pw_cls}, {pw_txt}), 登录按钮({btn_cls}, {btn_txt})) def _interactive_record(self, hwnd): 交互式手动记录用户依次点击控件并按 F2 pass def _show_record_prompt(self): pass def _on_f2_record(self, event, hwnd): pass def _finish_interactive_record(self): pass # ---------- 启动游戏与自动填写 ---------- def on_start_game(self): path self.game_path_var.get().strip() if not path or not os.path.exists(path): tk.messagebox.showerror(启动失败, 游戏路径无效请先设置路径。) return # 解析定时时间 if self.timer_var.get(): total_seconds self.parse_time_inputs() if total_seconds is None or total_seconds 0: tk.messagebox.showwarning(定时无效, 时间参数不正确将立即启动游戏。) self.launch_game(path) else: if self.scheduled_after_id: self.after_cancel(self.scheduled_after_id) self.scheduled_after_id self.after(total_seconds * 1000, self.launch_game, path) tk.messagebox.showinfo(定时启动, f游戏将在 {total_seconds} 秒后启动。) else: self.launch_game(path) def parse_time_inputs(self): try: h int(self.hour_entry.get().strip() or 0) m int(self.minute_entry.get().strip() or 0) s int(self.second_entry.get().strip() or 0) if h 0 or m 0 or s 0: return None return h * 3600 m * 60 s except ValueError: return None def launch_game(self, path): try: os.startfile(path) # 延迟6秒等待窗口初始化然后自动填写 self.after(6000, self.auto_fill_login) except Exception as e: tk.messagebox.showerror(启动失败, str(e)) def _fill_password(self, hwnd, password): win32gui.SetFocus(hwnd) time.sleep(0.1) # 清空可选发送 CtrlA 和 Delete # 这里可以调用 wh.send_key_combo ... wh.send_wm_char_string(hwnd,password) time.sleep(0.2) return True def auto_fill_login(self): 自动填写账号密码并点击登录 selected self.tree.selection() if not selected: print(未选中账号) return item selected[0] values self.tree.item(item, values) account values[1] cursor self.db.get_cursor() cursor.execute(SELECT password FROM accounts WHERE id?, (item,)) password cursor.fetchone()[0] # 获取游戏窗口 pid None for proc in psutil.process_iter([name]): if proc.info[name] QQSpeed_loader.exe: pid proc.pid break if not pid: print(未找到游戏进程) return hwnd wh.get_main_window_by_pid(pid) if not hwnd: print(未找到主窗口) return win32gui.SetForegroundWindow(hwnd) time.sleep(0.3) # 尝试从数据库读取控件特征 acc_info self.db.get_control_info(account_edit) pw_info self.db.get_control_info(password_edit) btn_info self.db.get_control_info(login_button) account_hwnd None password_hwnd None button_hwnd None if acc_info and pw_info and btn_info: acc_cls, acc_txt acc_info[1], acc_info[2] pw_cls, pw_txt pw_info[1], pw_info[2] btn_cls, btn_txt btn_info[1], btn_info[2] def find_by_saved(hwnd_child, lparam): nonlocal account_hwnd, password_hwnd, button_hwnd cls win32gui.GetClassName(hwnd_child) txt win32gui.GetWindowText(hwnd_child) if cls acc_cls and txt acc_txt: account_hwnd hwnd_child elif cls pw_cls and txt pw_txt: password_hwnd hwnd_child elif cls btn_cls and txt btn_txt: button_hwnd hwnd_child return True win32gui.EnumChildWindows(hwnd, find_by_saved, None) # 如果保存的特征没找到回退到动态查找 if not account_hwnd or not password_hwnd: print(使用动态查找...) edits [] def enum_edit(hwnd_child, lparam): if win32gui.GetClassName(hwnd_child) Edit: edits.append(hwnd_child) return True win32gui.EnumChildWindows(hwnd, enum_edit, None) if len(edits) 2: account_hwnd edits[0] password_hwnd edits[1] else: print(未找到输入框) return # 填写账号 if account_hwnd: win32gui.SendMessage(account_hwnd, win32con.WM_SETTEXT, 0, account) time.sleep(0.5) else: print(账号框未找到) return # 填写密码 # 5. 填写密码尝试多种方式 success self._fill_password(password_hwnd, password) if not success: print(❌ 密码填写失败自动填写终止) return print(f 已填入密码) time.sleep(0.2) # 点击登录 if button_hwnd: win32gui.PostMessage(button_hwnd, win32con.BM_CLICK, 0, 0) print(登录按钮已点击) else: # 动态查找登录按钮 def find_btn(hwnd_child, lparam): nonlocal button_hwnd if win32gui.GetClassName(hwnd_child) Button and win32gui.GetWindowText(hwnd_child) 登录: button_hwnd hwnd_child return False return True win32gui.EnumChildWindows(hwnd, find_btn, None) if button_hwnd: win32gui.PostMessage(button_hwnd, win32con.BM_CLICK, 0, 0) print(动态找到登录按钮并点击) else: print(未找到登录按钮) if __name__ __main__: app App() app.mainloop()account_manager.py# database.py import sqlite3 class Database: def __init__(self, db_pathaccounts.db): self.conn sqlite3.connect(db_path) self.create_tables() self.insert_sample_data() def get_cursor(self): return self.conn.cursor() def create_tables(self): cursor self.conn.cursor() # 账号表 cursor.execute( CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, account TEXT NOT NULL, password TEXT NOT NULL, region TEXT, status TEXT ) ) # 配置表保存游戏路径等 cursor.execute( CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value TEXT ) ) # 控件特征表 cursor.execute( CREATE TABLE IF NOT EXISTS control_info ( key TEXT PRIMARY KEY, control_type TEXT, class_name TEXT, window_text TEXT, automation_id TEXT, extra_info TEXT ) ) self.conn.commit() def insert_sample_data(self): cursor self.conn.cursor() cursor.execute(SELECT COUNT(*) FROM accounts) if cursor.fetchone()[0] 0: samples [ (user1example.com, pass123, 电信区, 未登录), (user2example.com, abc456, 联通区, 登录中), (test_user, test789, 电信二区, 游戏中), ] cursor.executemany( INSERT INTO accounts (account, password, region, status) VALUES (?, ?, ?, ?), samples ) self.conn.commit() # ---------- 账号表操作 ---------- def get_all_accounts(self): cursor self.conn.cursor() cursor.execute(SELECT id, account, password, region, status FROM accounts ORDER BY id) return cursor.fetchall() def update_account_field(self, account_id, field, value): cursor self.conn.cursor() cursor.execute(fUPDATE accounts SET {field}? WHERE id?, (value, account_id)) self.conn.commit() def get_account_password(self, account_id): cursor self.conn.cursor() cursor.execute(SELECT password FROM accounts WHERE id?, (account_id,)) row cursor.fetchone() return row[0] if row else None def delete_account(self, account_id): cursor self.conn.cursor() cursor.execute(DELETE FROM accounts WHERE id?, (account_id,)) self.conn.commit() # ---------- 配置表操作 ---------- def get_config(self, key): cursor self.conn.cursor() cursor.execute(SELECT value FROM config WHERE key?, (key,)) row cursor.fetchone() return row[0] if row else None def set_config(self, key, value): cursor self.conn.cursor() cursor.execute(INSERT OR REPLACE INTO config (key, value) VALUES (?, ?), (key, value)) self.conn.commit() def delete_config(self, key): cursor self.conn.cursor() cursor.execute(DELETE FROM config WHERE key?, (key,)) self.conn.commit() # ---------- 控件特征表操作 ---------- def save_control_info(self, key, control_type, class_name, window_text, automation_id, extra_info): cursor self.conn.cursor() cursor.execute( INSERT OR REPLACE INTO control_info (key, control_type, class_name, window_text, automation_id, extra_info) VALUES (?, ?, ?, ?, ?, ?) , (key, control_type, class_name, window_text, automation_id, extra_info)) self.conn.commit() def get_control_info(self, key): cursor self.conn.cursor() cursor.execute(SELECT control_type, class_name, window_text, automation_id, extra_info FROM control_info WHERE key?, (key,)) return cursor.fetchone()database.py# log_panel.py import tkinter as tk from tkinter import ttk, filedialog, messagebox import datetime class LogPanel(ttk.Frame): 独立的日志显示面板包含文本框和保存/清除按钮 def __init__(self, master, **kwargs): super().__init__(master, **kwargs) self.create_widgets() def create_widgets(self): # 日志显示区域带滚动条 text_frame ttk.Frame(self) text_frame.pack(filltk.BOTH, expandTrue) self.log_text tk.Text(text_frame, wraptk.WORD, font(Consolas, 9)) scrollbar ttk.Scrollbar(text_frame, orienttk.VERTICAL, commandself.log_text.yview) self.log_text.configure(yscrollcommandscrollbar.set) self.log_text.pack(sidetk.LEFT, filltk.BOTH, expandTrue) scrollbar.pack(sidetk.RIGHT, filltk.Y) # 按钮栏 btn_frame ttk.Frame(self) btn_frame.pack(filltk.X, pady(5, 0)) ttk.Button(btn_frame, text保存日志, commandself.save_log).pack(sidetk.LEFT, padx5) ttk.Button(btn_frame, text清除日志, commandself.clear_log).pack(sidetk.LEFT, padx5) def log(self, message, levelINFO): 向日志区域添加一条带时间戳的消息 timestamp datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) formatted f[{timestamp}] [{level}] {message}\n self.log_text.insert(tk.END, formatted) self.log_text.see(tk.END) # 自动滚动到底部 self.update_idletasks() def clear_log(self): 清空日志区域 self.log_text.delete(1.0, tk.END) def save_log(self): 弹出保存对话框将日志内容保存为文本文件 content self.log_text.get(1.0, tk.END).strip() if not content: messagebox.showinfo(提示, 日志内容为空无需保存。) return file_path filedialog.asksaveasfilename( defaultextension.txt, filetypes[(文本文件, *.txt), (所有文件, *.*)], title保存日志 ) if file_path: try: with open(file_path, w, encodingutf-8) as f: f.write(content) messagebox.showinfo(成功, f日志已保存至\n{file_path}) except Exception as e: messagebox.showerror(保存失败, f保存日志时出错\n{e}) def get_text_widget(self): 返回内部的Text控件供重定向使用 return self.log_textlog_panel.py# win32_helper.py Windows API 辅助函数 包含进程查找、窗口句柄获取、控件枚举、特征保存与自动填写 import win32gui import win32con import win32process import psutil import time # 需要在 win32_helper.py 顶部导入 ctypes import ctypes from ctypes import wintypes # 定义 SendInput 所需的结构 INPUT_KEYBOARD 1 KEYEVENTF_KEYUP 0x0002 KEYEVENTF_UNICODE 0x0004 def find_process_pid(proc_name): 根据进程名返回第一个匹配的 PID未找到返回 None for proc in psutil.process_iter([name]): try: if proc.info[name] and proc.info[name].lower() proc_name.lower(): return proc.pid except (psutil.NoSuchProcess, psutil.AccessDenied): pass return None def get_main_window_by_pid(pid): 通过 PID 获取主窗口句柄优先可见窗口 def callback(hwnd, hwnds): if win32gui.IsWindowVisible(hwnd): _, found_pid win32process.GetWindowThreadProcessId(hwnd) if found_pid pid: hwnds.append(hwnd) return True hwnds [] win32gui.EnumWindows(callback, hwnds) return hwnds[0] if hwnds else None def bring_window_to_front(hwnd): 将窗口置于前台 if win32gui.IsIconic(hwnd): win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) win32gui.SetForegroundWindow(hwnd) time.sleep(0.3) def get_child_controls(hwnd, class_filterNone): 枚举子窗口控件返回列表每个元素为 (hwnd, class_name, window_text) 若指定 class_filter则只返回该类名的控件 controls [] def enum_child(hwnd_child, lparam): cls win32gui.GetClassName(hwnd_child) if class_filter is None or cls class_filter: txt win32gui.GetWindowText(hwnd_child) controls.append((hwnd_child, cls, txt)) return True win32gui.EnumChildWindows(hwnd, enum_child, None) return controls def smart_detect_controls(hwnd): 智能识别账号框、密码框、登录按钮 返回 (account_hwnd, password_hwnd, login_btn_hwnd)未找到则为 None edit_controls get_child_controls(hwnd, class_filterEdit) button_controls get_child_controls(hwnd, class_filterButton) account_hwnd None password_hwnd None login_btn_hwnd None # 账号框优先选空文本或含“账号”字样否则第一个 Edit for hw, cls, txt in edit_controls: if txt or 账号 in txt or account in txt.lower(): account_hwnd hw break if not account_hwnd and edit_controls: account_hwnd edit_controls[0][0] # 密码框常见文本特征 M, Password, 密码 或空非账号框 for hw, cls, txt in edit_controls: if hw account_hwnd: continue if txt in (M, Password, 密码) or (txt and hw ! account_hwnd): password_hwnd hw break if not password_hwnd and len(edit_controls) 2: second edit_controls[1][0] if edit_controls[1][0] ! account_hwnd else edit_controls[0][0] password_hwnd second # 登录按钮匹配常见登录文本 login_texts [登录, Login, 进入游戏, 开始游戏] for hw, cls, txt in button_controls: if any(t in txt for t in login_texts): login_btn_hwnd hw break if not login_btn_hwnd and button_controls: login_btn_hwnd button_controls[0][0] return account_hwnd, password_hwnd, login_btn_hwnd def set_edit_text(hwnd, text): 向 Edit 控件发送文本 win32gui.SendMessage(hwnd, win32con.WM_SETTEXT, 0, text) time.sleep(0.1) def click_button(hwnd): 模拟点击按钮 win32gui.PostMessage(hwnd, win32con.BM_CLICK, 0, 0) def find_control_by_feature(hwnd, class_name, window_text): 根据类名和窗口文本查找子控件句柄返回第一个匹配项 for child_hwnd, cls, txt in get_child_controls(hwnd): if cls class_name and txt window_text: return child_hwnd return None class KEYBDINPUT(ctypes.Structure): _fields_ [ (wVk, wintypes.WORD), (wScan, wintypes.WORD), (dwFlags, wintypes.DWORD), (time, wintypes.DWORD), (dwExtraInfo, ctypes.POINTER(ctypes.c_ulong)) ] class INPUT(ctypes.Structure): class _INPUT(ctypes.Union): _fields_ [(ki, KEYBDINPUT)] _anonymous_ (_input,) _fields_ [(type, wintypes.DWORD), (_input, _INPUT)] def send_unicode_string(text): 使用 SendInput 发送 Unicode 字符串模拟真实键盘输入 user32 ctypes.windll.user32 inputs [] for ch in text: # 按下 inp_down INPUT(typeINPUT_KEYBOARD) inp_down.ki.wVk 0 inp_down.ki.wScan ord(ch) inp_down.ki.dwFlags KEYEVENTF_UNICODE inputs.append(inp_down) # 抬起 inp_up INPUT(typeINPUT_KEYBOARD) inp_up.ki.wVk 0 inp_up.ki.wScan ord(ch) inp_up.ki.dwFlags KEYEVENTF_UNICODE | KEYEVENTF_KEYUP inputs.append(inp_up) if inputs: arr (INPUT * len(inputs))(*inputs) user32.SendInput(len(arr), arr, ctypes.sizeof(INPUT)) def send_wm_char_string(hwnd, text): 逐个字符发送 WM_CHAR 消息 for ch in text: win32gui.PostMessage(hwnd, win32con.WM_CHAR, ord(ch), 0) time.sleep(0.02)win32_helper.py几个py文件需放在同目录下转载请注明出处谢谢