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 借此决定:
- 获取参数:获取光标当前所在的绝对坐标、时间戳、所属窗口句柄等。
- 事件透传:让鼠标事件继续传递给原本要接收的程序(如使用了
~前缀)。 - 事件拦截/重定向:吃掉该事件(阻断输入)或转换为其他按键。
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)
}
使用鼠标钩子的避坑指南
- 回调耗时严控
- Windows 对低级钩子(
WH_MOUSE_LL)有严格的超时限制(通常为几十毫秒)。 - 鼠标热键触发的代码中切勿执行耗时较长的同步阻塞操作(如大文件读写、超长
Sleep),否则会导致整个系统的鼠标出现卡顿、掉帧甚至钩子被系统强制移除。
- 管理员权限与 UAC 限制
- 低权限脚本无法拦截或穿透高权限(管理员身份运行)窗口的鼠标事件。如果目标软件是以管理员身份运行的,AHK 脚本也必须以管理员身份启动。
- 坐标系匹配
- 钩子捕获的原始数据是屏幕绝对坐标,在配合
MouseGetPos或MouseClick处理时,务必注意脚本顶部的CoordMode设置,保持基准统一。
为什么不把#hotif isRecording 写到StartRecord函数内? 整个调用链条
不能把 #HotIf 写在 StartRecord() 函数内部,核心原因在于:#HotIf 是以 # 开头的“预处理/静态指令”(Directive),而不是在代码运行到该行时才执行的“运行时语句”。
根本原因:指令(Directive)vs 语句(Statement)
- 解析时机不同:
- 指令(以
#开头):在脚本刚启动加载(编译/语法解析阶段)时就由解释器静态处理完毕。它用于告诉 AHK 解释器:“下面这几行热键定义,在系统触发时需要挂载一个条件判断”。 - 函数体内部代码:只有在被显式调用(如按下 F8)时才会动态运行。
- 位置决定作用域:
#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 显式开启/关闭钩子)。

浙公网安备 33010602011771号