来源出处:本文整理自 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 同时有 FunctionAFunctionW: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()

如果要做更高精度的性能测试,可以用 QueryPerformanceCounterQueryPerformanceFrequency

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 就猜。把 GetLastErrorFormatMessage 封装好,排查效率会高很多。

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 位整数。掌握这些模式后,再看仓库里的其它函数,就会顺很多。

声明:站内资源为整理优化好的代码上传分享与学习研究,如果是开源代码基本都会标明出处,方便大家扩展学习路径。请不要恶意搬运,破坏站长辛苦整理维护的劳动成果。本站为爱好者分享站点,所有内容不作为商业行为。如若本站上传内容侵犯了原著者的合法权益,请联系我们进行删除下架。