PAYATHON 2026

如何避免 pynput 自动连点干扰鼠标左键按住状态的检测?

数据小许

自动连点器需要单独记录实际鼠标的按下和松开事件,并忽略模拟点击产生的事件。只把检测方式换成 GetAsyncKeyState(),无法解决输入来源混在一起的问题。

原代码中,模拟点击不会直接把 clicking 改为 False;只有 toggled() 会修改它。根据描述,更可能是鼠标状态判断变为假,导致点击循环暂停。

为什么模拟点击会干扰检测?

rat.click(mouse.Button.left, 1) 包含一次按下和一次松开。它发送的松开事件可能影响随后读取到的按键状态,即使你的手指仍然压着鼠标。pynput 的 click 实现也是先调用 press(),再调用 release()。

GetAsyncKeyState(0x01) & 0x8000 != 0 的位运算写法没有问题,高位表示按键当前是否按下。但这个接口不会告诉调用者状态来自实际操作还是注入事件。Microsoft 文档中的“physical mouse buttons”说明的是左右键交换后的按钮映射,不能据此认为它会自动排除模拟输入。GetAsyncKeyState 文档

如何区分实际操作和模拟点击?

在 Windows 上,可以给 mouse.Listener 配置 win32_event_filter,检查底层鼠标事件的 data.flags。

LLMHF_INJECTED 的值为 0x00000001,用于标记注入事件。过滤掉这些事件后,用其余左键按下和松开事件维护一个独立状态。MSLLHOOKSTRUCT 文档

过滤函数返回 False 只会阻止事件进入该监听器的普通回调,模拟点击仍可传给目标程序。不要在这里使用 suppress=True 或 suppress_event(),它们会抑制系统事件。pynput 事件过滤说明

修改后的代码

下面的代码沿用原代码的 F6:松开 F6 时切换启用状态,实际按住鼠标左键时连点,松开 Esc 时退出。切换操作放在 on_release 中,可以避免长按快捷键时因键盘自动重复而反复切换。

import threading
from pynput import mouse, keyboard

TOGGLE_KEY = keyboard.Key.f6
LLMHF_INJECTED = 0x00000001

# 分别记录启用状态、实际左键状态和退出状态。
enabled = threading.Event()
left_held = threading.Event()
stopping = threading.Event()
controller = mouse.Controller()


def mouse_event_filter(msg, data):
    # 只忽略监听器中的注入事件,不阻止系统接收点击。
    return not bool(data.flags & LLMHF_INJECTED)


def on_click(x, y, button, pressed):
    if button == mouse.Button.left:
        if pressed:
            left_held.set()
        else:
            left_held.clear()


def on_key_release(key):
    if key == keyboard.Key.esc:
        stopping.set()
        return False

    if key == TOGGLE_KEY:
        if enabled.is_set():
            enabled.clear()
        else:
            enabled.set()
        print("Autoclicker:", enabled.is_set())


def clicker():
    while not stopping.is_set():
        if enabled.is_set() and left_held.is_set():
            controller.click(mouse.Button.left, 1)
            stopping.wait(0.01)
        else:
            stopping.wait(0.005)


worker = threading.Thread(target=clicker, daemon=True)

with mouse.Listener(
    on_click=on_click,
    win32_event_filter=mouse_event_filter,
) as mouse_listener, keyboard.Listener(
    on_release=on_key_release,
) as keyboard_listener:
    worker.start()
    try:
        keyboard_listener.join()
    finally:
        stopping.set()
        worker.join()

如果希望使用描述中的 = 键,把快捷键定义改为:

TOGGLE_KEY = keyboard.KeyCode.from_char('=')

使用时需要注意什么?

  • 这个过滤方案适用于 Windows,以及支持 win32_event_filter 的 pynput 后端。它依赖事件携带正确的注入标记,不能作为鉴别任意输入来源是否来自硬件的机制。
  • 启动程序时先松开左键,再重新按下。示例通过监听后续事件维护状态,不会恢复监听开始前的按住状态。
  • 过滤会忽略所有带 LLMHF_INJECTED 标记的鼠标事件,包括其他程序注入的事件。
  • 鼠标松开与工作线程检查状态之间有很短的时间窗口,边界处可能仍会完成一次已经开始的点击。0.01 秒只是等待间隔,不保证精确达到每秒 100 次点击。
  • 如果粘贴出的代码就是实际文件内容,还必须先修复缩进,否则 Python 会在运行点击逻辑之前报错。

验证时依次检查:关闭状态下按住左键不连点;启用后按住左键持续连点;松开左键停止;再次按下可以继续。这样可以确认模拟松开事件没有再清除实际左键状态。

备注:内容仅供参考。