电脑操作全记录:一键监控键盘鼠标U盘
第一部分:
功能概述
该代码实现了一个计算机操作记录器,用于监控并记录键盘、鼠标动作以及U盘插拔事件。所有操作会被实时记录到UTF-8编码的日志文件中,便于后续审计或分析。
核心功能模块
键盘事件监控
- 记录按键按下(
on_press
)和释放(on_release
)动作 - 特殊按键(如功能键)会被记录为键名(如
Key.esc
) - 按下ESC键可主动停止记录
鼠标事件监控
- 记录点击动作(
on_click
),区分按下/释放状态和左右键 - 记录滚轮滚动(
on_scroll
)及方向/幅度 - 记录移动轨迹(
on_move
),通过0.5秒节流控制日志量
外设监控(U盘插拔检测)
- 通过Windows消息机制监听
WM_DEVICECHANGE
事件 - 识别设备类型(
DBT_DEVTYP_VOLUME
)和驱动器号 - 区分设备接入(
DBT_DEVICEARRIVAL
)与移除(DBT_DEVICEREMOVECOMPLETE
)
技术实现要点
系统级监控
- 使用
pynput
库捕获HID输入事件 - 通过
ctypes
调用Windows API实现设备监控窗口 - 定义
WNDCLASS
结构体处理设备变更消息
日志记录优化
- 强制UTF-8编码避免中文乱码
- 时间戳精确到秒级(
%Y-%m-%d %H:%M:%S
) - 鼠标坐标转为整数值避免浮点格式
类型安全处理
- 动态补全
wintypes
未定义的Windows类型 - 明确定义
LPARAM
/WPARAM
为32位整数 - 使用
Structure
类严格匹配C语言结构体
典型日志输出示例
2023-08-20 14:30:15 - 键盘按下: Key.cmd
2023-08-20 14:30:16 - 鼠标按下: Button.left 在位置 (120, 450)
2023-08-20 14:30:17 - 设备接入: 驱动器 D:
2023-08-20 14:30:18 - 鼠标滚轮: 在位置 (300, 200),滚动量 (0, 1)
以下是具体代码(复制直接可用):
OperationRecord.bat(ANSI):
@echo off
:: 定义当前文件夹路径(使用脚本所在目录)
set "CURRENT_DIR=%~dp0":: 定义脚本路径和日志路径(日志文件生成在当前文件夹下,命名为 OperationRecord_log.txt)
set "PY_SCRIPT=%CURRENT_DIR%OperationRecord.py"
set "LOG_FILE=%CURRENT_DIR%OperationRecord_log.txt":: 输出当前运行时间到日志
echo ============================================== >> %LOG_FILE%
echo 脚本运行时间:%date% %time% >> %LOG_FILE%
echo ============================================== >> %LOG_FILE%:: 调用 Python 运行脚本,并将输出/报错写入日志(同时在窗口显示)
python "%PY_SCRIPT%" >> %LOG_FILE% 2>&1
:: 提示运行完成,按任意键关闭窗口
echo.
echo 脚本运行完成!日志已保存至:%LOG_FILE%
pause
OperationRecord.py:
import time
import logging
import ctypes
import threading
from ctypes import wintypes
from pynput import keyboard, mouse
from datetime import datetime# 配置日志记录:核心添加 encoding='utf-8' 确保UTF-8编码
logging.basicConfig(filename='computer_operations.log',level=logging.INFO,format='%(asctime)s - %(message)s',datefmt='%Y-%m-%d %H:%M:%S',encoding='utf-8' # 关键修改:指定日志文件编码为UTF-8
)# 补充定义所需的Windows类型
if not hasattr(wintypes, 'HCURSOR'):wintypes.HCURSOR = ctypes.c_void_p
if not hasattr(wintypes, 'HICON'):wintypes.HICON = ctypes.c_void_p
if not hasattr(wintypes, 'HBRUSH'):wintypes.HBRUSH = ctypes.c_void_p
if not hasattr(wintypes, 'LPCWSTR'):wintypes.LPCWSTR = ctypes.c_wchar_p
if not hasattr(wintypes, 'HWND'):wintypes.HWND = ctypes.c_void_p
if not hasattr(wintypes, 'HINSTANCE'):wintypes.HINSTANCE = ctypes.c_void_p
if not hasattr(wintypes, 'HMENU'):wintypes.HMENU = ctypes.c_void_p
if not hasattr(wintypes, 'ATOM'):wintypes.ATOM = ctypes.c_uint16
if not hasattr(wintypes, 'LPARAM'):wintypes.LPARAM = ctypes.c_long # 明确指定为32位整数
if not hasattr(wintypes, 'WPARAM'):wintypes.WPARAM = ctypes.c_ulong # 明确指定为32位无符号整数# 定义WNDCLASS结构
class WNDCLASS(ctypes.Structure):_fields_ = [("style", ctypes.c_uint),("lpfnWndProc", ctypes.CFUNCTYPE(ctypes.c_long, wintypes.HWND, ctypes.c_uint, wintypes.WPARAM, wintypes.LPARAM)),("cbClsExtra", ctypes.c_int),("cbWndExtra", ctypes.c_int),("hInstance", wintypes.HINSTANCE),("hIcon", wintypes.HICON),("hCursor", wintypes.HCURSOR),("hbrBackground", wintypes.HBRUSH),("lpszMenuName", wintypes.LPCWSTR),("lpszClassName", wintypes.LPCWSTR)]# Windows API相关常量
WM_DEVICECHANGE = 0x0219
DBT_DEVICEARRIVAL = 0x8000
DBT_DEVICEREMOVECOMPLETE = 0x8004
DBT_DEVTYP_VOLUME = 0x00000002class DEV_BROADCAST_VOLUME(ctypes.Structure):_fields_ = [("dbcv_size", wintypes.DWORD),("dbcv_devicetype", wintypes.DWORD),("dbcv_reserved", wintypes.DWORD),("dbcv_unitmask", wintypes.DWORD),("dbcv_flags", wintypes.WORD)]class OperationRecorder:def __init__(self):self.start_time = datetime.now()self.keyboard_listener = Noneself.mouse_listener = Noneself.device_monitor_thread = Noneself.is_recording = Falseself.hwnd = Nonedef on_press(self, key):"""键盘按下事件处理"""try:logging.info(f"键盘按下: {key.char}")except AttributeError:logging.info(f"键盘按下: {key}")def on_release(self, key):"""键盘释放事件处理"""try:logging.info(f"键盘释放: {key.char}")except AttributeError:logging.info(f"键盘释放: {key}")# 按ESC键停止记录if key == keyboard.Key.esc:self.stop_recording()return Falsedef on_click(self, x, y, button, pressed):"""鼠标点击事件处理"""action = "按下" if pressed else "释放"# 修正:将x/y转为整数,避免浮点格式(如100.0)logging.info(f"鼠标{action}: {button} 在位置 ({int(x)}, {int(y)})")def on_scroll(self, x, y, dx, dy):"""鼠标滚轮事件处理"""# 修正:将x/y转为整数logging.info(f"鼠标滚轮: 在位置 ({int(x)}, {int(y)}),滚动量 ({dx}, {dy})")def on_move(self, x, y):"""鼠标移动事件处理"""current_time = time.time()if not hasattr(self, 'last_move_time') or current_time - self.last_move_time > 0.5:# 修正:将x/y转为整数logging.info(f"鼠标移动到: ({int(x)}, {int(y)})")self.last_move_time = current_timedef _get_drive_letter(self, unitmask):"""将设备掩码转换为驱动器字母"""drive_letters = []for i in range(26): # A-Zif unitmask & (1 << i):drive_letters.append(f"{chr(ord('A') + i)}:")return ", ".join(drive_letters)def _device_monitor(self):"""设备监控线程,监听U盘插拔事件"""# 加载user32.dll并定义函数原型user32 = ctypes.WinDLL('user32', use_last_error=True)# 定义RegisterClassW函数原型user32.RegisterClassW.argtypes = [ctypes.POINTER(WNDCLASS)]user32.RegisterClassW.restype = wintypes.ATOM# 定义CreateWindowExW函数原型user32.CreateWindowExW.argtypes = [wintypes.DWORD, # dwExStylewintypes.LPCWSTR, # lpClassNamewintypes.LPCWSTR, # lpWindowNamewintypes.DWORD, # dwStylectypes.c_int, # xctypes.c_int, # yctypes.c_int, # nWidthctypes.c_int, # nHeightwintypes.HWND, # hWndParentwintypes.HMENU, # hMenuwintypes.HINSTANCE, # hInstancectypes.c_void_p # lpParam]user32.CreateWindowExW.restype = wintypes.HWND# 定义DefWindowProcW函数原型(关键修改)user32.DefWindowProcW.argtypes = [wintypes.HWND,ctypes.c_uint,wintypes.WPARAM,wintypes.LPARAM]user32.DefWindowProcW.restype = ctypes.c_long# 定义消息处理函数def wndproc(hwnd, msg, wparam, lparam):try:if msg == WM_DEVICECHANGE:# 处理设备变化消息if wparam == DBT_DEVICEARRIVAL:# 设备插入try:dev_broadcast = ctypes.cast(lparam, ctypes.POINTER(DEV_BROADCAST_VOLUME)).contentsif dev_broadcast.dbcv_devicetype == DBT_DEVTYP_VOLUME:drive = self._get_drive_letter(dev_broadcast.dbcv_unitmask)logging.info(f"U盘插入: 驱动器 {drive}")except:pass # 忽略非卷设备消息elif wparam == DBT_DEVICEREMOVECOMPLETE:# 设备拔出try:dev_broadcast = ctypes.cast(lparam, ctypes.POINTER(DEV_BROADCAST_VOLUME)).contentsif dev_broadcast.dbcv_devicetype == DBT_DEVTYP_VOLUME:drive = self._get_drive_letter(dev_broadcast.dbcv_unitmask)logging.info(f"U盘拔出: 驱动器 {drive}")except:pass # 忽略非卷设备消息except Exception as e:logging.error(f"消息处理错误: {e}")# 调用默认窗口过程,确保参数类型正确return user32.DefWindowProcW(ctypes.cast(hwnd, wintypes.HWND),ctypes.c_uint(msg),wintypes.WPARAM(wparam),wintypes.LPARAM(lparam))# 注册窗口类wc = WNDCLASS()wc.style = 0wc.lpfnWndProc = ctypes.CFUNCTYPE(ctypes.c_long, wintypes.HWND, ctypes.c_uint, wintypes.WPARAM, wintypes.LPARAM)(wndproc)wc.cbClsExtra = 0wc.cbWndExtra = 0wc.hInstance = ctypes.windll.kernel32.GetModuleHandleW(None)wc.hIcon = Nonewc.hCursor = Nonewc.hbrBackground = ctypes.cast(0, wintypes.HBRUSH) # 默认背景wc.lpszMenuName = Nonewc.lpszClassName = "DeviceMonitorClass"# 注册窗口类class_atom = user32.RegisterClassW(ctypes.byref(wc))if not class_atom:err_code = ctypes.get_last_error()logging.error(f"窗口类注册失败,错误代码: {err_code}")return# 创建窗口self.hwnd = user32.CreateWindowExW(0, # dwExStylewc.lpszClassName, # lpClassName"Device Monitor", # lpWindowName0, # dwStyle0, 0, 0, 0, # 位置和大小None, # 父窗口None, # 菜单wc.hInstance, # 实例句柄None # 参数)if not self.hwnd:err_code = ctypes.get_last_error()logging.error(f"窗口创建失败,错误代码: {err_code}")return# 消息循环msg = wintypes.MSG()while self.is_recording:if user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1):user32.TranslateMessage(ctypes.byref(msg))user32.DispatchMessageW(ctypes.byref(msg))time.sleep(0.1)def start_recording(self):"""开始记录操作"""self.is_recording = Truestart_msg = f"开始记录操作,时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}"# 【修改1】移除print,仅保留日志记录(后台无控制台,print无效且可能报错)logging.info(start_msg)# 创建监听器self.keyboard_listener = keyboard.Listener(on_press=self.on_press,on_release=self.on_release)self.mouse_listener = mouse.Listener(on_click=self.on_click,on_scroll=self.on_scroll,on_move=self.on_move)# 启动设备监控线程self.device_monitor_thread = threading.Thread(target=self._device_monitor, daemon=True)# 启动监听器和监控线程self.keyboard_listener.start()self.mouse_listener.start()self.device_monitor_thread.start()# 【修改2】移除print提示(后台无控制台,用户看不到)# 保持程序运行:用无阻塞循环替代原while+sleep,避免后台占用过多资源while self.is_recording:time.sleep(1)def stop_recording(self):"""停止记录操作"""if not self.is_recording:returnself.is_recording = Falseend_time = datetime.now()duration = end_time - self.start_time# 停止监听器if self.keyboard_listener:self.keyboard_listener.stop()if self.mouse_listener:self.mouse_listener.stop()# 销毁设备监控窗口if self.hwnd:ctypes.windll.user32.DestroyWindow(self.hwnd)end_msg = f"停止记录操作,时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')},记录时长: {duration}"# 【修改3】移除print,仅保留日志记录logging.info(end_msg)if __name__ == "__main__":recorder = OperationRecorder()try:recorder.start_recording()except Exception as e:# 【修改4】移除print,仅保留日志报错logging.error(f"发生错误: {e}")
第二部分:
功能模块说明
OperationReplayer 类
该工具用于解析和重现计算机操作日志,支持真实鼠标/键盘控制模拟,主要功能包括操作日志解析、GUI界面控制、速度调节和强制终止。
核心功能
日志解析与操作存储
- 通过正则表达式匹配日志中的时间戳和操作内容
- 计算相对时间差以确定操作执行顺序
- 分类存储键盘按下/释放事件
图形用户界面
- 主窗口包含日志显示区、控制按钮、速度调节滑块
- 实时展示键盘输入和鼠标移动轨迹
- 状态栏显示当前播放状态
操作重现控制
- 多线程执行操作序列以避免界面冻结
- 支持调整播放速度(0.1x~5x)
- 强制停止功能可立即终止所有模拟操作
安全机制
- 禁用pyautogui的故障安全模式(需手动终止)
- 自动释放可能被按下的修饰键(Shift/Ctrl/Alt)
- 窗口状态恢复功能保证异常终止后界面可用
技术实现
鼠标控制模拟
pyautogui.moveTo(x, y) # 精确控制鼠标位置
pyautogui.click() # 模拟点击操作
键盘事件处理
pyautogui.keyDown(key) # 模拟按键按下
pyautogui.keyUp(key) # 模拟按键释放
时间同步逻辑
relative_time = (event_time - start_time).total_seconds()
time.sleep(delay / play_speed) # 根据速度系数调整等待时间
使用场景
- 自动化测试:重复用户操作路径
- 教学演示:还原特定操作流程
- 行为分析:可视化研究用户交互模式
注意事项
- 需管理员权限运行(涉及系统输入控制)
- 播放期间避免手动操作键鼠
- 强制停止后建议检查按键状态
该工具通过高精度时间控制和真实输入模拟,实现了操作过程的帧级还原,适用于需要精确重现交互场景的各类应用场景。
以下是具体代码(复制直接可用):
OperationRetrospection.bat(ANSI):
@echo off
:: 定义当前文件夹路径(使用脚本所在目录)
set "CURRENT_DIR=%~dp0":: 定义脚本路径和日志路径(日志文件生成在当前文件夹下,命名为 OperationRetrospection_log.txt)
set "PY_SCRIPT=%CURRENT_DIR%OperationRetrospection.py"
set "LOG_FILE=%CURRENT_DIR%OperationRetrospection_log.txt":: 输出当前运行时间到日志
echo ============================================== >> %LOG_FILE%
echo 脚本运行时间:%date% %time% >> %LOG_FILE%
echo ============================================== >> %LOG_FILE%:: 调用 Python 运行脚本,并将输出/报错写入日志(同时在窗口显示)
python "%PY_SCRIPT%" >> %LOG_FILE% 2>&1
:: 提示运行完成,按任意键关闭窗口
echo.
echo 脚本运行完成!日志已保存至:%LOG_FILE%
pause
OperationRetrospection.py:
import re
import time
import tkinter as tk
from tkinter import scrolledtext, messagebox
import threading
from datetime import datetime, timedelta
import pyautogui # 用于模拟真实键鼠操作# 初始化pyautogui,禁用故障安全(按Ctrl+Alt+Del可强制停止)
pyautogui.FAILSAFE = False
# 设置pyautogui操作延迟(避免操作过快,单位:秒)
pyautogui.PAUSE = 0.01class OperationReplayer:def __init__(self, log_file="computer_operations.log"):self.log_file = log_fileself.operations = [] # 存储解析后的操作self.is_playing = Falseself.play_speed = 1.0 # 播放速度倍数self.offset_time = 0 # 时间偏移(用于计算相对时间)# 创建GUI窗口self.root = tk.Tk()self.root.title("操作过程复原(真实鼠标控制)")self.root.geometry("800x600")# 记录窗口初始状态(用于恢复)self.window_initial_state = (self.root.winfo_x(), self.root.winfo_y(), 800, 600)# 创建界面组件self.create_widgets()# 解析日志文件self.parse_log()def create_widgets(self):# 日志显示区域self.log_text = scrolledtext.ScrolledText(self.root, wrap=tk.WORD)self.log_text.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)self.log_text.config(state=tk.DISABLED)# 控制区域control_frame = tk.Frame(self.root)control_frame.pack(padx=10, pady=5, fill=tk.X)self.play_btn = tk.Button(control_frame, text="开始播放", command=self.toggle_play)self.play_btn.pack(side=tk.LEFT, padx=5)self.stop_btn = tk.Button(control_frame, text="强制停止", command=self.force_stop)self.stop_btn.pack(side=tk.LEFT, padx=5)self.speed_label = tk.Label(control_frame, text="播放速度:")self.speed_label.pack(side=tk.LEFT, padx=5)self.speed_scale = tk.Scale(control_frame, from_=0.1, to=5.0, resolution=0.1, orient=tk.HORIZONTAL, length=200)self.speed_scale.set(1.0)self.speed_scale.pack(side=tk.LEFT, padx=5)self.speed_scale.bind("<Motion>", self.update_speed)self.status_label = tk.Label(control_frame, text="状态: 就绪", anchor=tk.E)self.status_label.pack(side=tk.RIGHT, padx=5)# 操作展示区域self.display_frame = tk.Frame(self.root, bg="white", bd=2, relief=tk.SUNKEN)self.display_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)self.keyboard_display = tk.Label(self.display_frame, text="键盘输入将显示在这里 | 已开启真实键鼠控制", bg="white", anchor=tk.W, justify=tk.LEFT,font=("SimHei", 12), fg="red")self.keyboard_display.pack(padx=10, pady=5, fill=tk.X)self.mouse_canvas = tk.Canvas(self.display_frame, bg="lightgray")self.mouse_canvas.pack(padx=10, pady=5, fill=tk.BOTH, expand=True)self.mouse_pointer = self.mouse_canvas.create_oval(0, 0, 10, 10, fill="red")self.event_log = scrolledtext.ScrolledText(self.display_frame, height=5, wrap=tk.WORD)self.event_log.pack(padx=10, pady=5, fill=tk.X)self.event_log.config(state=tk.DISABLED)def update_speed(self, event):"""更新播放速度"""self.play_speed = self.speed_scale.get()def force_stop(self):"""强制停止所有操作 + 恢复UI窗口"""self.is_playing = False# 恢复窗口显示(关键:从隐藏状态变回正常)self.root.deiconify()# 恢复窗口初始大小和位置x, y, w, h = self.window_initial_stateself.root.geometry(f"{w}x{h}+{x}+{y}")# 更新按钮和状态self.play_btn.config(text="开始播放")self.status_label.config(text="状态: 已强制停止")self.log_event("操作被强制停止")# 释放所有可能被按下的键pyautogui.keyUp('shift')pyautogui.keyUp('ctrl')pyautogui.keyUp('alt')def parse_log(self):"""解析日志文件(保持原逻辑不变)"""try:with open(self.log_file, 'r', encoding='utf-8') as f:lines = f.readlines()log_pattern = r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) - (.*)$'start_time = Nonefor line in lines:match = re.match(log_pattern, line.strip())if match:time_str, content = match.groups()event_time = datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S')if not start_time:start_time = event_timeself.offset_time = event_timerelative_time = (event_time - start_time).total_seconds()op_type = ""if content.startswith("键盘按下:"):op_type = "key_press"elif content.startswith("键盘释放:"):op_type = "key_release"elif content.startswith("鼠标按下:"):op_type = "mouse_press"elif content.startswith("鼠标释放:"):op_type = "mouse_release"elif content.startswith("鼠标移动到:"):op_type = "mouse_move"elif content.startswith("鼠标滚轮:"):op_type = "mouse_scroll"elif content.startswith("U盘插入:") or content.startswith("U盘拔出:"):op_type = "usb_event"elif content.startswith("开始记录操作") or content.startswith("停止记录操作"):op_type = "system_event"self.operations.append({'time': event_time,'relative_time': relative_time,'type': op_type,'content': content})self.log_text.config(state=tk.NORMAL)self.log_text.insert(tk.END, f"成功解析日志文件: {self.log_file}\n")self.log_text.insert(tk.END, f"记录开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}\n")self.log_text.insert(tk.END, f"总操作数: {len(self.operations)}\n")self.log_text.insert(tk.END, "提示:播放时UI将隐藏,按ESC键可强制恢复窗口!\n\n")self.log_text.config(state=tk.DISABLED)except Exception as e:messagebox.showerror("解析错误", f"无法解析日志文件: {str(e)}")self.log_text.config(state=tk.NORMAL)self.log_text.insert(tk.END, f"解析错误: {str(e)}\n")self.log_text.config(state=tk.DISABLED)def toggle_play(self):"""切换播放/暂停 + 控制UI显示/隐藏"""if self.is_playing:# 暂停状态:恢复窗口显示self.is_playing = Falseself.root.deiconify() # 显示窗口self.play_btn.config(text="继续播放")self.status_label.config(text="状态: 已暂停")else:# 开始播放:先确认 + 隐藏窗口confirm = messagebox.askyesno("确认播放", "播放时UI将隐藏,按ESC键可强制恢复!建议关闭重要程序后继续!")if not confirm:returnself.is_playing = Trueself.play_btn.config(text="暂停")self.status_label.config(text="状态: 播放中")# 关键:隐藏UI窗口(使用withdraw()完全隐藏,而非最小化)self.root.withdraw()# 启动播放线程threading.Thread(target=self.play_operations, daemon=True).start()# 启动ESC监听线程(播放中按ESC恢复窗口)threading.Thread(target=self.listen_esc_key, daemon=True).start()def listen_esc_key(self):"""新增:监听ESC键,播放中按ESC可恢复UI窗口"""while self.is_playing:# 检测ESC键是否按下(优化:用pyautogui.isPressed避免阻塞)if pyautogui.isPressed('esc'):self.force_stop() # 触发强制停止(自动恢复窗口)breaktime.sleep(0.1) # 降低检测频率,减少资源占用def log_event(self, message):"""在事件日志中添加信息(保持原逻辑不变)"""self.event_log.config(state=tk.NORMAL)self.event_log.insert(tk.END, f"{datetime.now().strftime('%H:%M:%S')} - {message}\n")self.event_log.see(tk.END)self.event_log.config(state=tk.DISABLED)def update_keyboard_display(self, content):"""更新键盘输入显示(保持原逻辑不变)"""key_info = content.replace("键盘按下:", "").replace("键盘释放:", "").strip()if key_info.startswith("Key."):key = key_info.split(".")[1].upper()display_text = f"【真实键盘】特殊键: {key} ({content.split(':')[0]})"else:display_text = f"【真实键盘】输入: {key_info} ({content.split(':')[0]})"self.keyboard_display.config(text=display_text)def update_mouse_position(self, content):"""更新鼠标位置 + 控制真实鼠标移动(保持原逻辑不变)"""match = re.search(r'\((\d+), (\d+)\)', content)if match:x, y = map(int, match.groups())# 控制真实鼠标移动pyautogui.moveTo(x, y, duration=0.05 / self.play_speed)# 更新画布模拟显示canvas_width = self.mouse_canvas.winfo_width() or 600canvas_height = self.mouse_canvas.winfo_height() or 400scaled_x = min(int(x * canvas_width / 1920), canvas_width - 10)scaled_y = min(int(y * canvas_height / 1080), canvas_height - 10)self.mouse_canvas.coords(self.mouse_pointer, scaled_x, scaled_y, scaled_x + 10, scaled_y + 10)def play_operations(self):"""播放操作过程 + 播放结束恢复UI"""if not self.operations:# 无操作记录时,先恢复窗口再提示self.root.deiconify()messagebox.showinfo("提示", "没有可播放的操作记录")self.toggle_play()returnstart_time = time.time()prev_relative_time = 0for op in self.operations:if not self.is_playing:break# 计算等待时间time_diff = op['relative_time'] - prev_relative_timewait_time = time_diff / self.play_speedtime.sleep(wait_time)prev_relative_time = op['relative_time']# 执行真实键鼠操作if op['type'] == 'key_press':key = op['content'].replace("键盘按下:", "").strip()if key.startswith("Key."):key = key.split(".")[1]pyautogui.keyDown(key)self.update_keyboard_display(op['content'])self.log_event(op['content'])elif op['type'] == 'key_release':key = op['content'].replace("键盘释放:", "").strip()if key.startswith("Key."):key = key.split(".")[1]pyautogui.keyUp(key)self.update_keyboard_display(op['content'])self.log_event(op['content'])elif op['type'] == 'mouse_move':self.update_mouse_position(op['content'])self.log_event(op['content'])elif op['type'] == 'mouse_press':self.update_mouse_position(op['content'])if "左键" in op['content']:pyautogui.mouseDown(button='left')elif "右键" in op['content']:pyautogui.mouseDown(button='right')elif "中键" in op['content']:pyautogui.mouseDown(button='middle')self.log_event(op['content'])elif op['type'] == 'mouse_release':self.update_mouse_position(op['content'])if "左键" in op['content']:pyautogui.mouseUp(button='left')elif "右键" in op['content']:pyautogui.mouseUp(button='right')elif "中键" in op['content']:pyautogui.mouseUp(button='middle')self.log_event(op['content'])elif op['type'] == 'mouse_scroll':if "向上" in op['content']:pyautogui.scroll(1)elif "向下" in op['content']:pyautogui.scroll(-1)self.log_event(op['content'])elif op['type'] == 'usb_event':self.log_event(op['content'])# U盘事件提示:播放中窗口隐藏,需先恢复窗口再弹窗self.root.deiconify()self.root.after(0, lambda msg=op['content']: messagebox.showinfo("设备事件", msg))# 弹窗后重新隐藏窗口(若仍在播放)if self.is_playing:self.root.withdraw()# 更新日志显示(窗口隐藏时不影响,恢复后可见)self.log_text.config(state=tk.NORMAL)self.log_text.insert(tk.END, f"{op['time'].strftime('%H:%M:%S')} - {op['content']}\n")self.log_text.see(tk.END)self.log_text.config(state=tk.DISABLED)# 播放结束:恢复窗口显示self.root.deiconify()# 释放残留按键 + 更新状态pyautogui.keyUp('shift')pyautogui.keyUp('ctrl')pyautogui.keyUp('alt')self.is_playing = Falseself.play_btn.config(text="开始播放")self.status_label.config(text="状态: 播放结束")self.log_event("操作播放完成")def run(self):"""运行GUI主循环(保持原逻辑不变)"""self.root.mainloop()if __name__ == "__main__":# 运行前检查依赖(无控制台环境下,用tkinter弹窗提示,而非print)try:import pyautoguiexcept ImportError:# 先创建临时tk窗口显示依赖提示(避免无控制台时无法输出)temp_root = tk.Tk()temp_root.withdraw() # 隐藏临时窗口messagebox.showerror("依赖缺失", "请先安装pyautogui库:\n1. 打开CMD命令行\n2. 执行命令:pip install pyautogui")temp_root.destroy()exit()replayer = OperationReplayer()replayer.run()