来源出处:本文整理自 AutoHotkey 官方论坛 jNizM 的帖子 WinApi, DllCalls & AHK,以及其维护的 GitHub 示例仓库 AHK_DllCall_WinAPI。原帖主要是 WinAPI 函数目录和链接列表,本文按 AHK66 读者习惯重新整理为“能读、能查、能照着改”的中文说明。
站内延伸阅读:如果你刚接触 DllCall,建议先看 DllCall 入门:AHK 调用 Windows API 的最小示例;需要查更多封装示例,可以看 WinApi、DllCalls封装成函数的收集;涉及窗口消息时,再配合 OnMessage 入门:监听 Windows 消息能做什么 和 窗口控制怎么选:标题、类名、进程名、PID、句柄 一起读。
AHK 已经封装了大量常用命令,例如 WinGet、FileCopy、MouseMove、Clipboard、Run、Process 等。但 Windows 本身还有更底层、更庞大的 API。AHK 通过 DllCall() 可以直接调用这些 Windows API,于是很多“AHK 没有现成命令”的事情,也能通过系统 DLL 完成。
不过,DllCall 的难点不在于“能不能调用”,而在于你是否看得懂官方函数原型:参数是什么类型、字符串该用 A 还是 W、结构体要分配多少字节、返回值失败时怎么查错误、32 位和 64 位指针要怎么处理。本文先把这些核心概念讲清楚,再列出几个最常用的 WinAPI 示例。
一、DllCall 到底在做什么
一个 Windows API 通常位于某个 DLL 文件里,例如:
user32.dll:窗口、鼠标、键盘、消息、菜单等用户界面相关 API。kernel32.dll:进程、线程、文件、时间、内存、系统信息等基础 API。gdi32.dll:绘图、字体、位图等 GDI 相关 API。advapi32.dll:注册表、服务、安全、权限等 API。
AHK 调用一个 API 的基本形式是:
#Requires AutoHotkey v1.1
ret := DllCall("DLL名称\函数名", "参数类型1", 参数1, "参数类型2", 参数2, "返回类型")
例如获取当前脚本进程 ID:
pid := DllCall("kernel32.dll\GetCurrentProcessId", "UInt")
MsgBox, % "当前进程 ID:" pid
这类无参数 API 最适合入门:不需要结构体,不需要指针,不需要缓冲区,只要知道返回值类型即可。
二、从 C 函数原型翻译成 AHK
微软文档里的函数通常长这样:
DWORD WINAPI GetCurrentProcessId(void);
翻译到 AHK 时,只需要关注三件事:
- 函数在哪个 DLL:这里是
kernel32.dll。 - 有没有参数:
void表示没有参数。 - 返回值是什么:
DWORD通常对应 AHK 的UInt。
所以 AHK 写法就是:
DllCall("kernel32.dll\GetCurrentProcessId", "UInt")
如果你不知道某个 C 类型对应 AHK 哪个类型,可以先记住这些最常见的映射:
| Windows / C 类型 | AHK DllCall 类型 | 说明 |
|---|---|---|
| BOOL | Int 或 UInt | 0 表示失败,非 0 表示成功 |
| DWORD / UINT | UInt | 32 位无符号整数 |
| int | Int | 32 位有符号整数 |
| ULONGLONG / UInt64 | UInt64 | 64 位无符号整数 |
| LARGE_INTEGER | Int64 | 64 位整数,常用于高精度计时 |
| HANDLE / HWND / HMODULE / 指针 | Ptr | 指针大小随 32/64 位变化 |
| LPCTSTR / LPTSTR / 字符串指针 | Str 或 Ptr | 直接传字符串用 Str,缓冲区用 Ptr |
三、返回 BOOL 的函数:SetCursorPos
SetCursorPos 位于 user32.dll,用于把鼠标移动到屏幕坐标。微软函数原型大致是:
BOOL WINAPI SetCursorPos(
int X,
int Y
);
AHK 写法:
SetCursorPos(X, Y)
{
if !DllCall("user32.dll\SetCursorPos", "Int", X, "Int", Y, "UInt")
return DllCall("kernel32.dll\GetLastError")
return 1
}
SetCursorPos(750, 500)
这里有一个很重要的习惯:如果 Windows API 返回 BOOL,通常 0 表示失败。失败时不要只说“没反应”,可以立刻调用 GetLastError 获取错误码。
四、结构体入门:GetCursorPos 和 POINT
很多 API 不会直接返回所有结果,而是要求你传入一个结构体指针,由系统把结果写到结构体里。GetCursorPos 就是典型例子。
C 原型大致是:
BOOL WINAPI GetCursorPos(
LPPOINT lpPoint
);
POINT 结构体包含两个 int:
typedef struct tagPOINT {
LONG x;
LONG y;
} POINT;
AHK 里没有 C 结构体语法,所以要用 VarSetCapacity 分配内存,再用 NumGet 按偏移读取。
GetCursorPos()
{
VarSetCapacity(POINT, 8, 0)
if !DllCall("user32.dll\GetCursorPos", "Ptr", &POINT, "UInt")
return DllCall("kernel32.dll\GetLastError")
return { x: NumGet(POINT, 0, "Int")
, y: NumGet(POINT, 4, "Int") }
}
pos := GetCursorPos()
MsgBox, % "X=" pos.x "`nY=" pos.y
这个例子能理解,DllCall 就过了第一道坎:VarSetCapacity 是准备一块内存,&POINT 是把这块内存的地址传给 API,NumGet 是从这块内存里按类型读回结果。
五、字符串缓冲区:GetComputerName
有些 API 要求你传入一个字符串缓冲区,让系统把文本写进去。比如获取计算机名:
GetComputerName()
{
size := 32
VarSetCapacity(buf, size * (A_IsUnicode ? 2 : 1), 0)
if !DllCall("kernel32.dll\GetComputerName", "Ptr", &buf, "UInt*", size, "UInt")
return DllCall("kernel32.dll\GetLastError")
return StrGet(&buf)
}
MsgBox, % GetComputerName()
这里的 UInt* 表示把变量地址传给 API,API 可以修改这个变量。很多 Windows API 的“长度参数”都是这种写法:调用前告诉系统缓冲区多大,调用后系统可能把实际长度写回来。
字符串相关 API 还要注意 A/W 版本。很多 Windows API 同时有 FunctionA 和 FunctionW:A 是 ANSI,W 是 Unicode。AHK Unicode 版通常更适合调用 W 版,或者直接让 AHK 根据函数名自动选择。
六、错误处理:GetLastError 和 FormatMessage
只拿到错误码还不够直观,FormatMessage 可以把错误码转成系统错误文本。jNizM 仓库里有一个典型封装:
FormatMessage(MessageId)
{
size := 2024
VarSetCapacity(buf, size, 0)
if !DllCall("kernel32.dll\FormatMessage"
, "UInt", 0x1000 ; FORMAT_MESSAGE_FROM_SYSTEM
, "Ptr", 0
, "UInt", MessageId
, "UInt", 0x0800 ; LANG_SYSTEM_DEFAULT
, "Ptr", &buf
, "UInt", size
, "UInt*", 0)
return DllCall("kernel32.dll\GetLastError")
return StrGet(&buf)
}
DeleteFileApi(FileName)
{
if !DllCall("kernel32.dll\DeleteFile", "Str", FileName, "UInt")
return FormatMessage(DllCall("kernel32.dll\GetLastError"))
return 1
}
MsgBox, % DeleteFileApi("C:\Temp\TestFile.txt")
写 DllCall 时,错误处理非常重要。否则你只能看到“失败”,却不知道是路径不存在、权限不足、参数类型错了,还是缓冲区太小。
七、64 位整数:GetTickCount64 和高精度计时
GetTickCount64 返回系统启动后的毫秒数,返回值是 64 位无符号整数:
GetTickCount64()
{
return DllCall("kernel32.dll\GetTickCount64", "UInt64")
}
MsgBox, % GetTickCount64()
如果要做更高精度的性能测试,可以用 QueryPerformanceCounter 和 QueryPerformanceFrequency:
DllCall("kernel32.dll\QueryPerformanceFrequency", "Int64*", F)
DllCall("kernel32.dll\QueryPerformanceCounter", "Int64*", S)
Loop, 1000000
i++
DllCall("kernel32.dll\QueryPerformanceCounter", "Int64*", E)
MsgBox, % "耗时秒数:" (E - S) / F
这里的 Int64* 表示传入一个 64 位整数变量的地址,让 API 把结果写回来。
八、复杂结构体:GlobalMemoryStatusEx
GlobalMemoryStatusEx 可以获取系统内存状态。它使用 MEMORYSTATUSEX 结构体,结构体总大小为 64 字节,并且第一个字段必须先写入结构体大小。
GlobalMemoryStatusEx()
{
VarSetCapacity(MEMORYSTATUSEX, 64, 0)
NumPut(64, MEMORYSTATUSEX, 0, "UInt")
if !DllCall("kernel32.dll\GlobalMemoryStatusEx", "Ptr", &MEMORYSTATUSEX, "UInt")
return DllCall("kernel32.dll\GetLastError")
return { MemoryLoad: NumGet(MEMORYSTATUSEX, 4, "UInt")
, TotalPhys: NumGet(MEMORYSTATUSEX, 8, "UInt64")
, AvailPhys: NumGet(MEMORYSTATUSEX, 16, "UInt64") }
}
mem := GlobalMemoryStatusEx()
MsgBox, % "内存使用率:" mem.MemoryLoad "%`n总物理内存:" Round(mem.TotalPhys/1024/1024) " MB`n可用物理内存:" Round(mem.AvailPhys/1024/1024) " MB"
这类 API 的关键是看懂结构体字段顺序和字段类型。偏移量不是随便写的:第一个 UInt 占 4 字节,下一个 UInt 再占 4 字节,然后 UInt64 每个占 8 字节。
九、带枚举返回值:GetDriveType
GetDriveType 返回磁盘类型编号。原始返回值是数字,封装时可以转成易读文本:
GetDriveType(path, convert := 0)
{
static DriveType := { 0: "DRIVE_UNKNOWN"
, 1: "DRIVE_NO_ROOT_DIR"
, 2: "DRIVE_REMOVABLE"
, 3: "DRIVE_FIXED"
, 4: "DRIVE_REMOTE"
, 5: "DRIVE_CDROM"
, 6: "DRIVE_RAMDISK" }
ret := DllCall("kernel32.dll\GetDriveType", "Str", path, "UInt")
return convert ? DriveType[ret] : ret
}
MsgBox, % GetDriveType("C:\")
MsgBox, % GetDriveType("C:\", 1)
很多 Windows API 都会返回类似枚举值。封装函数时建议把数字转成文本,这样文章、日志和调试输出都更容易读。
十、jNizM 仓库里哪些分类值得看
原帖列了很多分类。对 AHK 用户来说,不建议从头到尾硬啃,应该按需求看:
| 分类 | 适合解决的问题 | 代表函数 |
|---|---|---|
| Cursor / Mouse Input | 鼠标坐标、鼠标限制、双击时间、捕获状态 | GetCursorPos、SetCursorPos、ClipCursor |
| Keyboard Input | 键盘布局、输入阻塞、代码页 | BlockInput、GetKeyboardLayout |
| File / Directory | 文件复制、删除、属性、临时目录、当前目录 | CopyFile、DeleteFile、GetTempPath |
| Error Handling | 失败后获取错误码和错误文本 | GetLastError、FormatMessage |
| System Information | 系统目录、用户名、版本、高精度计时 | GetComputerName、QueryPerformanceCounter |
| Memory Management | 系统内存状态、内存大小 | GlobalMemoryStatusEx |
| Process and Thread | 进程 ID、线程 ID、处理器信息、SleepEx | GetCurrentProcessId、GetCurrentThreadId |
| Time | 系统时间、本地时间、启动时间 | GetLocalTime、GetTickCount64 |
| Volume Management | 磁盘类型、卷信息 | GetDriveType |
仓库地址:https://github.com/jNizM/AHK_DllCall_WinAPI
十一、写 DllCall 时最容易踩的坑
1. Ptr 不要写成 UInt
句柄、窗口句柄、结构体地址、缓冲区地址,在现代 AHK v1 脚本里优先用 Ptr。这样 32 位和 64 位都能适配。老代码里常见 UInt,在 64 位环境可能出问题。
2. 字符串不要乱用编码
如果函数有 A/W 两个版本,Unicode 环境优先考虑 W 版。传缓冲区时,要确认字节数和字符数的区别。中文乱码、字符串截断,很多都和这里有关。
3. 结构体大小必须正确
结构体里字段顺序、类型和对齐都要匹配。像 MEMORYSTATUSEX 这种结构体,还必须先把结构体大小写入第一个字段。
4. 失败时先查 GetLastError
不要只看返回 0 就猜。把 GetLastError 和 FormatMessage 封装好,排查效率会高很多。
5. 先写小封装,再写业务脚本
建议把每个 API 封装成一个 AHK 函数,例如 GetCursorPos()、GetDriveType()。业务脚本只调用封装函数,不要在业务逻辑里到处散落长长的 DllCall。
十二、什么时候该用 DllCall
DllCall 适合这些场景:
- AHK 自带命令没有提供对应功能。
- 需要更底层的窗口、进程、文件、系统信息能力。
- 需要调用第三方 DLL。
- 需要精确控制结构体、句柄、系统消息或内存。
但如果 AHK 已经有稳定命令,就不必强行 DllCall。例如普通文件复制用 FileCopy 就够了,普通鼠标移动用 MouseMove 就够了。DllCall 的价值是补足 AHK 命令之外的能力,而不是把所有命令都重写一遍。
结语
jNizM 的原帖和仓库最有价值的地方,不是“列出了多少 API”,而是给了大量 AHK v1 调用 WinAPI 的可运行模板。学习 DllCall 时,不建议先背完整 Windows API,而是从几个典型模式开始:无参数返回值、BOOL 返回值、字符串缓冲区、结构体指针、错误处理、64 位整数。掌握这些模式后,再看仓库里的其它函数,就会顺很多。

评论(0)