惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

腾讯CDC
The Cloudflare Blog
IT之家
IT之家
V
V2EX
雷峰网
雷峰网
MyScale Blog
MyScale Blog
P
Proofpoint News Feed
Stack Overflow Blog
Stack Overflow Blog
博客园 - Franky
Engineering at Meta
Engineering at Meta
S
SegmentFault 最新的问题
GbyAI
GbyAI
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
小众软件
小众软件
博客园 - 叶小钗
Blog — PlanetScale
Blog — PlanetScale
C
Check Point Blog
A
About on SuperTechFans
B
Blog
月光博客
月光博客
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI

博客园 - 立体风

autohotkey2.0 Send keys参数分析 autohotkey2.0 盲从模式 windows 10 下 vscode 编写编译一个 nana c++ 库的示例程序 vscode 插件 cmake-tools 输出内容解读 Claude Code 处理中文乱码问题 python 启动器命令 py 答惑 windows 下 python 3.11 的版本问题 pip 安装 老版本 pytorch 的一个容易迷惑的错误 sentence transformer 例子及说明 Sentence Transformers 介绍 Jina Reranker 替代方案:改用 ModelScope 模型 Webnovel Writer 6.2.1 项目分析 windows 10 LTSC 版 安装 Terminal windows wsl2 安装 gentoo 的步骤 windows 10 LTSC版 打开 wsl2 autohotkey 提示词 编译安装最新版 perl rust 做到“快并且安全” 的逻辑思路 rust 和 go 语言的核心目的 用 vim 查看文件的格式和编码 throughput 和 efficient 词义 python 以字符数量分割文档 (三) python 以字符数量分割文档 (二) python 以字符数量分割文档 (一) GNU Bash 参考手册(中文版)(六) GNU Bash 参考手册(中文版)(五) GNU Bash 参考手册(中文版)(四) GNU Bash 参考手册(中文版)(三) GNU Bash 参考手册(中文版)(二) GNU Bash 参考手册(中文版)(一)
autohotkey2.0 脚本运行机制
立体风 · 2026-08-17 · via 博客园 - 立体风

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

#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 显式开启/关闭钩子)。