当前位置: 首页 > news >正文

电脑操作全记录:一键监控键盘鼠标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()

http://www.dtcms.com/a/488226.html

相关文章:

  • 青岛网站制作seo中国建设银行属于什么类型网站
  • 个人网站建设咨询电话九脉堂是做网站的
  • 阿里 建设网站七牛 wordpress 媒体
  • 计算机操作系统——文件元数据和索引节点(inode)
  • 香河家具城网站建设目标企业官方网站建设的流程
  • 新兴数据湖仓手册·从分层架构到数据湖仓架构(2025):数据仓库分层的概念与设计
  • 19手机网站沙井网站制作
  • 网站可做哪些服务上海有哪些互联网公司
  • 企业网站规划要求厂家在哪个app找
  • 查楼盘剩余房源的网站国际时事新闻
  • MySQL的配置
  • Xshell 8.0 自动化运维全场景实践:技术深度解析与实战指南
  • 扌们之 从诗文找数字4 2 1 再到某数字出现不出现
  • 深圳市建设工程造价管理站制作公司网页图片
  • 阿里巴巴网站优化怎么做免费的一级域名申请
  • 基于docker push原理进行tar镜像上传至harbor仓库(未分片)
  • RHCSA练习
  • 中铁建设集团门户网站网站深圳优化建设
  • H.265 RTP 打包与拆包重组详解
  • 建设网站情况说明范文php网站开发实战视频
  • 建网站需要用到什么软件陕西最新消息
  • 汕头网站建设方案开发可拖动网站
  • 收录网站查询建医疗网站步骤
  • H5网站开发工程师lnmp 网站开发总结
  • 二层虚拟专用网络技术详解1:VPWS在MPLS网络中的实现与应用
  • 免费网站后台模板下载字号 wordpress
  • 水果商城网站模板阿里巴巴logo的含义
  • QT6中QChart功能与应用
  • 人工智能简史(1)
  • 2025年--Lc188--931. 下降路径最小和(多维动态规划,矩阵)--Java版