You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用线程后宏录制回放变慢?Tkinter+pynput技术求助

宏录制软件回放速度异常问题

我正在开发一款宏录制软件,用于记录鼠标移动、点击和键盘输入。用Tkinter构建GUI,pynput捕获输入。GUI包含两个按钮:回放录制内容、启动/停止录制(按下录制按钮后需再次点击停止)。

因为pynput的监听循环会导致Tkinter GUI冻结,所以用线程让GUI在pynput运行时保持可用,但现在遇到的问题是:停止录制后回放时,速度只有录制时的0.5倍,怀疑是线程导致time.time()精度不足,不知道怎么解决。

期望:GUI不冻结,pynput功能正常运行。


主GUI代码(main.py)

import threading
from tkinter import *
from tkinter.ttk import *

from macro import startRecord, stopRecord, playRec

# Window Setup
window = Tk()
window.title("MacroRecorder")
window.geometry("350x200")

def startRecordingAndChangeImg():
    global stopBtn
    recordBtn.pack_forget()
    stopBtn = Button(window, text="stop", command=stopRecordingAndChangeTxt)
    stopBtn.pack(side=RIGHT, padx=50)
    threading.Thread(target=startRecord).start()

def stopRecordingAndChangeImg():
    global recordBtn
    stopBtn.pack_forget()
    recordBtn = Button(window, text="record", command=startRecordingAndChangeTxt)
    recordBtn.pack(side=RIGHT, padx=50)
    stopRecord()

# Play Button
playBtn = Button(window, text="play", command=playRec)
playBtn.pack(side=LEFT, padx=50)

# Record Button
recordBtn = Button(window, text="record", command=startRecordingAndChangeTxt)
recordBtn.pack(side=RIGHT, padx=50)


window.mainloop()

宏功能代码(macro.py)

from pynput import mouse, keyboard
from pynput.mouse import Button
from pynput.keyboard import Key
import time
import os
import json

mouseControl = mouse.Controller()
keyboardControl = keyboard.Controller()

def startRecord():
    global start_time
    global mouse_listener
    global keyboard_listener
    global macroEvents
    macroEvents = {'events': []}
    start_time = time.time()
    mouse_listener = mouse.Listener(on_move=on_move, on_click=on_click, on_scroll=on_scroll)
    keyboard_listener = keyboard.Listener(on_press=on_press, on_release=on_release)
    print('record started')
    with mouse_listener, keyboard_listener:
        mouse_listener.join()
        keyboard_listener.join()

def stopRecord():
    mouse_listener.stop()
    keyboard_listener.stop()
    json_macroEvents = json.dumps(macroEvents, indent=4)
    with open(os.path.join("C:/Users/***/Desktop/autre/python project/macro-recorder/data/test.json"), "w") as macroRecord:
        macroRecord.write(json_macroEvents)
    print('record stopped')


def on_move(x, y):
    global start_time
    macroEvents["events"].append({'type': 'cursorMove', 'x': x, 'y': y, 'timestamp': time.time() - start_time})
    start_time = time.time()


# 其他监听事件(on_click、on_scroll、on_press、on_release)采用相同逻辑,每次都会重置计时器为0

def playRec():
    print('record playing')
    for i in range(len(macroEvents["events"])):
        time.sleep(macroEvents["events"][i]["timestamp"])
        if macroEvents["events"][i]["type"] == "cursorMove":
            mouseControl.position = (macroEvents["events"][i]["x"], macroEvents["events"][i]["y"])
        elif macroEvents["events"][i]["type"] == "leftClickEvent":
            if macroEvents["events"][i]["pressed"] == True:
                mouseControl.press(Button.left)
            else:
                mouseControl.release(Button.left)
        elif macroEvents["events"][i]["type"] == "rightClickEvent":
            if macroEvents["events"][i]["pressed"] == True:
                mouseControl.press(Button.right)
            else:
                mouseControl.release(Button.right)
        elif macroEvents["events"][i]["type"] == "middleClickEvent":
            if macroEvents["events"][i]["pressed"] == True:
                mouseControl.press(Button.middle)
            else:
                mouseControl.release(Button.middle)
        elif macroEvents["events"][i]["type"] == "scrollEvent":
            mouseControl.scroll(macroEvents["events"][i]["dx"], macroEvents["events"][i]["dy"])
        elif macroEvents["events"][i]["type"] == "keyboardEvent":
            keyToPress = macroEvents["events"][i]["key"] if 'Key.' not in macroEvents["events"][i]["key"] else special_keys[macroEvents["events"][i]["key"]]
            if macroEvents["events"][i]["pressed"] == True:
                keyboardControl.press(keyToPress)
            else:
                keyboardControl.release(keyToPress)

问题原因及解决方法

核心问题

线程不会导致time.time()精度不足(该函数在Windows下精度约1ms,Linux下更高),真正的问题出在时间记录和回放的逻辑上:

  • 录制时每次触发事件就重置start_time,记录的是两次事件的间隔;
  • 回放时直接对每个事件sleep记录的间隔,但time.sleep()和事件处理本身都有执行开销,这些开销累加后会导致整体回放速度变慢。

修复步骤

  1. 记录绝对时间戳
    只在录制开始时初始化一次start_time,每个事件记录相对于录制开始的绝对时间差,不再每次重置计时器:

    def startRecord():
        global start_time
        # ... 其他代码不变
        start_time = time.time()  # 仅录制开始时初始化
    
    def on_move(x, y):
        global start_time
        elapsed = time.time() - start_time  # 记录相对于录制开始的绝对时间差
        macroEvents["events"].append({'type': 'cursorMove', 'x': x, 'y': y, 'timestamp': elapsed})
        # 移除start_time = time.time()这行代码
    

    其他事件回调(on_click、on_scroll等)做同样修改。

  2. 精确计算回放等待时间
    回放时跟踪当前已流逝的时间,计算每个事件需要等待的时长,避免累加误差:

    def playRec():
        print('record playing')
        if not macroEvents["events"]:
            return
        start_play = time.time()
        for event in macroEvents["events"]:
            # 计算需要等待的时间:事件的绝对时间差 - 已播放的时间
            wait_time = event["timestamp"] - (time.time() - start_play)
            if wait_time > 0:
                time.sleep(wait_time)
            # 执行事件操作(以下代码不变)
            if event["type"] == "cursorMove":
                mouseControl.position = (event["x"], event["y"])
            elif event["type"] == "leftClickEvent":
                if event["pressed"]:
                    mouseControl.press(Button.left)
                else:
                    mouseControl.release(Button.left)
            # ... 其他事件处理逻辑
    
  3. 补充缺失的特殊按键映射
    代码中special_keys未定义,需在macro.py中添加:

    special_keys = {
        'Key.space': Key.space,
        'Key.enter': Key.enter,
        'Key.backspace': Key.backspace,
        'Key.shift': Key.shift,
        # 根据需要补充其他特殊按键
    }
    
  4. 优化线程设置
    给录制线程设置daemon=True,关闭GUI时线程自动退出:

    threading.Thread(target=startRecord, daemon=True).start()
    

额外建议

  • 录制鼠标移动时,可设置最小间隔(比如只记录间隔超过10ms的移动事件),减少事件数量,提升回放性能;
  • 回放时也将playRec放到线程中执行,避免GUI因sleep阻塞。

内容的提问来源于stack exchange,提问作者LOUDO

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 03:47:33