autohotkey2.0 脚本运行机制

一个记录鼠标点击事件的代码

#Requires AutoHotkey v2.0
CoordMode "Mouse", "Screen"
CoordMode "ToolTip", "Screen"

; 热键绑定:F8 开始录制,F9 结束并保存,Esc 退出脚本
F8::StartRecord()
F9::StopRecord()
Esc::ExitApp

; 全局状态
global isRecording := false
global records := []

; 启动录制
StartRecord() {
    global isRecording, records
    records := []
    isRecording := true
    ToolTip("⏺️ 正在录制鼠标点击... (按 F9 保存并结束)`n已捕获: 0 步", 20, 20)
}

; 停止录制并保存至 mouse.txt
StopRecord() {
    global isRecording, records
    if !isRecording {
        ToolTip()
        return
    }

    isRecording := false
    ToolTip()

    if (records.Length = 0) {
        MsgBox("未检测到任何鼠标点击动作,未保存文件。", "提示", "Icon!")
        return
    }

    outputFile := "mouse.txt"
    fileContent := ""

    ; 逐行生成 x,y,间隔时间
    for index, item in records {
        if (index < records.Length) {
            ; 计算与下一次点击的真实时间间隔 (毫秒)
            interval := records[index + 1].tick - item.tick
        } else {
            ; 最后一次点击没有后续点击,默认设为 1000 毫秒
            interval := 1000
        }

        fileContent .= item.x . "," . item.y . "," . interval . "`r`n"
    }

    ; 写入文件(UTF-8 编码,覆盖旧文件)
    try {
        if FileExist(outputFile)
            FileDelete(outputFile)
        FileAppend(fileContent, outputFile, "UTF-8")
        MsgBox("录制成功!共记录 " . records.Length . " 步,已保存至 " . outputFile, "完成", "Iconi")
    } catch as err {
        MsgBox("写入文件失败: " . err.Message, "错误", "IconX")
    }
}

; 仅在录制状态下监听鼠标左键点击
#HotIf isRecording
~*LButton:: {
    global records
    ; 获取屏幕绝对坐标与当前时间戳
    MouseGetPos(&mX, &mY)
    records.Push({x: mX, y: mY, tick: A_TickCount})

    ; 更新悬浮提示
    ToolTip(
        "⏺️ 正在录制鼠标点击... (按 F9 保存并结束)`n" .
        "已捕获: " . records.Length . " 步`n" .
        "最新坐标: (" . mX . ", " . mY . ")",
        20, 20
    )
}
#HotIf


实现要点与逻辑说明

  • 透传点击(~*LButton

  • 前缀 ~ 确保录制时不拦截正常的鼠标左键功能,系统和应用能正常响应点击。

  • 前缀 * 允许在按住 Shift/Ctrl 等修饰键时依然能触发录制。

  • 高精度时间差

  • 每次点击时记录系统的微秒计时器 A_TickCount

  • \(N\) 步的间隔时间计算公式为:records[N+1].tick - records[N].tick

  • 最后一笔点击由于没有“下一次点击”,默认赋予 1000ms 的安全等待时间。

  • 条件热键(#HotIf isRecording

  • 仅在按 F8 开启录制后才会挂载鼠标钩子,录制结束后不占用系统监听资源。

#HotIf isRecording,这个语句是什么意思,介绍鼠标钩子的用法

#HotIf isRecording 的含义

#HotIf 是 AutoHotkey v2 中用于实现条件热键(Context-Sensitive Hotkeys)的指令。

  • 语法作用:它用于指定一个条件表达式。在其下方定义的所有热键,仅在表达式结果为 true(真)时生效
  • 边界范围#HotIf 的约束力会一直向下延伸,直到遇到下一个 #HotIf 指令(若单独写一行 #HotIf 则表示清除条件,使后续热键全局生效)。
  • 代码实例解析
#HotIf isRecording
~*LButton:: {
    ; 只有当 isRecording 为 true 时,按下鼠标左键才会触发此处的逻辑
}
#HotIf ; 闭合条件,恢复默认的全局状态

在未开启录制(isRecording := false)时,系统不会对鼠标左键做任何额外拦截或计算,左键完全由操作系统默认调度,避免了常驻监听带来的性能开销与误触风险。


什么是鼠标钩子(Mouse Hook)

鼠标钩子底层基于 Windows 系统的 API SetWindowsHookEx(特别是低级鼠标钩子 WH_MOUSE_LL)。

当操作系统产生鼠标事件(如移动、按键按下/弹起、滚轮滚动)时,系统会在把事件投递给目标窗口之前,优先通知 AHK 的钩子函数。AHK 借此决定:

  1. 获取参数:获取光标当前所在的绝对坐标、时间戳、所属窗口句柄等。
  2. 事件透传:让鼠标事件继续传递给原本要接收的程序(如使用了 ~ 前缀)。
  3. 事件拦截/重定向:吃掉该事件(阻断输入)或转换为其他按键。

AHK v2 中鼠标钩子的典型用法

1. 显式启用鼠标钩子

通常定义了鼠标热键时 AHK 会自动加载钩子;如果需要强制常驻加载(例如用于记录鼠标历史按键 KeyHistory),可显式调用函数:

InstallMouseHook() ; 安装底层鼠标钩子

2. 核心修饰符搭配

符号 名称 作用与示例
~ 透传修饰符 不阻断原生事件。如 ~LButton::,在执行 AHK 逻辑的同时,目标窗口依然能正常收到点击。
* 通配符 忽略修饰键。无论是否同时按住了 Shift、Ctrl 或 Alt,*LButton:: 均可触发。
` 符号 名称
--- --- ---
~ 透传修饰符 不阻断原生事件。如 ~LButton::,在执行 AHK 逻辑的同时,目标窗口依然能正常收到点击。
* 通配符 忽略修饰键。无论是否同时按住了 Shift、Ctrl 或 Alt,*LButton:: 均可触发。
强制钩子 防止死循环。强制走钩子而不是 RegisterHotkey,避免自身发送的按键递归触发自身。
Up 按键弹起 监听释放瞬间。如 LButton Up::

3. 鼠标高级事件监听(侧键、滚轮、手势)

; 1. 拦截鼠标侧键并改写
XButton1::Send "^c"  ; 侧下键变复制
XButton2::Send "^v"  ; 侧上键变粘贴

; 2. 结合滚轮实现音量调节(仅在任务栏上方滚动时生效)
#HotIf MouseIsOver("ahk_class Shell_TrayWnd")
WheelUp::SoundSetVolume "+2"
WheelDown::SoundSetVolume "-2"
#HotIf

MouseIsOver(winTitle) {
    MouseGetPos ,, &win
    return WinExist(winTitle . " ahk_id " . win)
}


使用鼠标钩子的避坑指南

  1. 回调耗时严控
  • Windows 对低级钩子(WH_MOUSE_LL)有严格的超时限制(通常为几十毫秒)。
  • 鼠标热键触发的代码中切勿执行耗时较长的同步阻塞操作(如大文件读写、超长 Sleep),否则会导致整个系统的鼠标出现卡顿、掉帧甚至钩子被系统强制移除。
  1. 管理员权限与 UAC 限制
  • 低权限脚本无法拦截或穿透高权限(管理员身份运行)窗口的鼠标事件。如果目标软件是以管理员身份运行的,AHK 脚本也必须以管理员身份启动。
  1. 坐标系匹配
  • 钩子捕获的原始数据是屏幕绝对坐标,在配合 MouseGetPosMouseClick 处理时,务必注意脚本顶部的 CoordMode 设置,保持基准统一。

为什么不把#hotif isRecording 写到StartRecord函数内? 整个调用链条

不能把 #HotIf 写在 StartRecord() 函数内部,核心原因在于:#HotIf 是以 # 开头的“预处理/静态指令”(Directive),而不是在代码运行到该行时才执行的“运行时语句”。


根本原因:指令(Directive)vs 语句(Statement)

  1. 解析时机不同
  • 指令(以 # 开头):在脚本刚启动加载(编译/语法解析阶段)时就由解释器静态处理完毕。它用于告诉 AHK 解释器:“下面这几行热键定义,在系统触发时需要挂载一个条件判断”。
  • 函数体内部代码:只有在被显式调用(如按下 F8)时才会动态运行
  1. 位置决定作用域
  • #HotIf 的作用范围是纯文本从上到下的物理位置,而非函数调用的逻辑时间。如果在函数内部写 #HotIf,AHK v2 会直接抛出语法错误或产生未定义的作用域混乱。

完整的生命周期与调用链条

整个机制分为两个阶段:启动加载期(静态绑定)按键触发期(动态求值)

阶段一:脚本启动与加载(解析阶段)

[启动脚本]
   │
   ▼
[解析器自上而下扫描源码]
   │
   ├─► 遇到 F8::StartRecord() ──► 注册全局常规热键
   │
   ├─► 遇到 #HotIf isRecording ──► 记录条件标记:后续热键需绑定 isRecording 条件
   │
   ├─► 遇到 ~*LButton:: ─────────► 安装底层鼠标钩子(WH_MOUSE_LL),并挂上条件检测器
   │
   └─► 遇到 #HotIf ──────────────► 清除条件标记,恢复全局模式
   │
   ▼
[初始化全局变量] isRecording := false
   │
   ▼
[进入空闲监听循环](此时鼠标左键点击,条件为 false,完全透传,不触发热键函数)


阶段二:用户交互与运行时调用链(执行阶段)

1. 用户按下 F8(启动录制)
用户物理按下 F8
   │
   ▼
操作系统捕获键盘事件 ──► 投递给 AHK
   │
   ▼
AHK 执行 StartRecord()
   │
   ├─► 清空 records 数组
   ├─► 变更状态:isRecording := true  <─── (仅改变布尔值,不修改任何钩子结构)
   └─► 弹出 ToolTip 提示

2. 用户点击鼠标左键(录制过程)
用户物理按下鼠标左键
   │
   ▼
Windows 操作系统内核产生底层事件 (WM_LBUTTONDOWN)
   │
   ▼
操作系统优先调用 AHK 的低级鼠标钩子函数 (WH_MOUSE_LL)
   │
   ▼
AHK 钩子引擎检索当前热键列表:
   │
   ├─► 匹配到 ~*LButton 定义受 #HotIf isRecording 约束
   │
   ├─► 动态求值:isRecording 当前是 true 还是 false?
   │      │
   │      ├─► [如果为 false] ──► AHK 忽略此热键,直接把事件放行给被点击的软件
   │      │
   │      └─► [当前为 true] ──► 判定条件满足!
   │
   ▼
【AHK 执行 ~*LButton 函数体】:
   ├─► MouseGetPos 获取坐标
   ├─► records.Push 记录时间戳与坐标
   └─► 更新 ToolTip 计数
   │
   ▼
因带有 ~ 修饰符,AHK 把鼠标事件继续放行给目标软件(目标软件正常响应点击)

3. 用户按下 F9(停止录制)
用户物理按下 F9 ──► 执行 StopRecord()
   │
   ├─► 变更状态:isRecording := false
   ├─► 计算每一步的实际时间间隔 (Interval)
   ├─► 写入 mouse.txt 文件
   └─► ToolTip() 销毁悬浮提示


如果非要在函数内动态开关热键,该怎么写?

如果你希望在 StartRecord() 运行时才去动态创建/开启鼠标热键,不应使用 #HotIf 指令,而必须使用 AHK v2 的 HotIf / Hotkey 内置函数

; 动态函数方式(无需全局变量与 #HotIf 指令)
StartRecord() {
    global records := []
    
    ; 动态挂载鼠标左键热键
    Hotkey("~*LButton", OnMouseClick, "On")
    ToolTip("⏺️ 录制已启动...", 20, 20)
}

StopRecord() {
    ; 动态卸载鼠标左键热键
    Hotkey("~*LButton", "Off")
    ToolTip()
    ; ...保存逻辑...
}

OnMouseClick(thisHotkey) {
    MouseGetPos(&mX, &mY)
    records.Push({x: mX, y: mY, tick: A_TickCount})
}

  • #HotIf isRecording:声明式(推荐,代码最简洁,由 AHK 底层在每次点击时自动判断变量)。
  • Hotkey("~*LButton", ...):编程式(在函数内部通过 API 显式开启/关闭钩子)。
posted @ 2026-08-17 08:29  立体风  阅读(7)  评论(0)    收藏  举报