发表回复 
DllCall的使用简介
2016-06-07, 01 : 31 (这个帖子最后修改于: 2018-08-16 13 : 01 by feiyue.)
DllCall的使用简介

;--------------------------------
; DllCall的使用简介 By FeiYue
;--------------------------------

DllCall是AHK的一个强大功能,用来调用Dll文件中的函数,例如标准的WinAPI函数。
许多新手可能觉得DllCall很复杂、很难用,于是对强大的WinAPI函数不敢上手,
只能羡慕那些高手们调用WinAPI实现各种功能。其实DllCall的使用并不难,下面我就
为大家拨开DllCall的神秘面纱,其实你也能简单学会!(下面都以WinAPI函数为例)

DllCall调用的格式为:
Result := DllCall("[DllFile\]Function" [, Type1,Arg1, Type2,Arg2, "Cdecl ReturnType"])

我们都用过AHK的内置函数或自己写的函数,比如:Pos:=InStr("abc123", "abc")
假如一个Dll文件中也有一个InStr函数,而且那个函数也需要两个字符串参数,怎么调用呢?
调用的格式为:Pos:=DllCall("Dll文件\InStr", "Str","abc123", "Str","abc", "Int")

我们比较一下区别:
AHK函数的函数名InStr,相当于DllCall调用中的 DllCall("Dll文件\InStr" 部分;
AHK函数的两个参数,在DllCall调用中每个参数前面都伴随了一个说明参数类型的参数;
最后DllCall调用的尾部还多出了一个说明函数返回类型的参数。
函数名部分很容易照搬使用,函数返回类型也只有两三种比较容易,只有参数类型有点难度。

要学会并自由使用DllCall需要进修三步:

第一步,要学会准确设定参数类型,这是重点、难点。

第二步,要学会用VarSetCapacity()分配内存。当我们使用一个新变量a并赋值时,
AHK自动为a变量申请了一块内存,但大小是尽量小的,不由我们掌控。
因此我们可以利用VarSetCapacity(a,字节数)来自己申请指定大小的内存。

要了解&为取变量内存地址的操作符,及用NumGet()、NumPut()读写内存数据,
获取内存地址及读写内存数据都相当于C语言的指针操作。

第三步,了解字符串在内存中的编码状态,要学会用StrGet()、StrPut()转换字符串编码。
利用StrPut可以将原生编码(由AHK是ANSI版还是Unicode版决定的)
的字符串转换为目标编码,StrGet读取目标编码转换为当前的原生编码。

要明白AHK将数字也保存为字符串格式。我们把数字赋值给变量,例如a:=1,
假如把1的数值直接保存在a变量对应的内存地址中,那么就要区分变量
是占1个字节(Char),还是2个字节(Short),还是4个字节(Int),
还是8个字节(Int64),这样的话,每个变量使用前都必须先定义它的数据类型
(指明占用字节数)了,比如C语言就要用“int a;”定义一个4字节的变量。
而AHK作为脚本程序,为了方便用户免声明任意使用变量,所以AHK把数字都
在内存中保存为字符串格式,因为字符串的末尾都有“\0”作为结束标记。
(Ansi编码“\0”为1个字节值0,Unicode编码“\0”为2个字节值0)

第二、三步看帮助文件就很容易懂了,第一步我慢慢讲解,引导大家深入理解参数类型。

一、为什么每个参数前面都要设定参数类型?

1、DllCall调用的大概流程是:DllCall首先把Dll文件整个读取到AHK进程的私有内存中,
然后通过函数名字符串找到对应的函数入口地址,然后把参数一个一个压入AHK进程的栈中
(每个参数占A_PtrSize个字节,32位系统中要传入64位的值时占A_PtrSize*2个字节),
然后跳转到函数的入口地址,控制权交给了入口地址的机器码手中。机器码会从栈中
把参数的数值一个一个读取回来,然后执行自身的代码。机器码执行完毕后,会把返回值
写入通用寄存器EAX中,然后把控制权归还给AHK,然后AHK从通用寄存器EAX中读取返回值。

2、我们知道AHK脚本是动态解析的,AHK函数的参数都用逗号作为分隔符隔开,不用考虑参数
的数据类型,而DllCall调用函数,要把参数的数值连续压入栈中,中间可是没有分隔符的,
如果每个参数前面不设定参数类型,各个参数怎么准确分隔开就有点困难。

如果默认采用每个参数都是A_PtrSize个字节的整型数据,那么有两种情况不能解决:
一是在Win32位系统中如果需要输入一个8字节的整型参数,必须标记为Int64类型;
二是如果要输入4字节的Float或8字节的Double类型的浮点数,必须标记为浮点类型,
因为浮点数在内存中的保存形式与一般的整型数不同,浮点数不能以整型数的形式压入栈中。

3、编写WinAPI函数的程序员不会为了省事便来个默认约定,该用地址型就地址型
(这在AHK中对应Ptr类型,会根据AHK自身是32位版还是64位版自动为4字节或8字节),
该用整型就整型(这在AHK中对应Int占用4字节),该用浮点型就浮点型(对应Float),
然后函数发布时会声明各个参数的参数类型(表面上五花八门,实际上是为了说明
每个参数在栈中占用A_PtrSize*N个字节,是否为浮点数,或者该参数是否为返回值)。

DllCall调用时就要按照WinAPI函数的声明,在每个参数前面加上合适的参数类型,
如果参数类型错误,机器码从栈中读取参数时就会错位读到错误的值,容易造成崩溃。

小结:
AHK的数据类型看起来很复杂,但只在操作数据结构(NumPut及NumGet),
和获取返回值(一是整个函数的返回值,二是星号类型的参数)时才有用,
对于传入的参数没什么用。因为每个参数传入系统的栈时至少占用A_PtrSize个字节,
所以AHK数据类型不够A_PtrSize个字节时,会先把此类型扩充为UPtr类型再传递数值。
也就是说("char",-1)与("short",-1)与("int",-1)与("UPtr",-1)是一样的。

简单来看,所有传入的参数都使用"Ptr"类型是可以的(太简单了!),
只要注意两种例外情况:一是32位系统中传入64位的值Int64,二是传入浮点数Float。

所以,先看看WinAPI的参数类型声明,简单判断一下是否是Int64、Long Long
这些明确的8字节类型采用Int64类型,再看看是否是浮点数类型,用AHK的
4字节Float类型或8字节Double类型,其他都用Ptr即可(用int、uint也是等效的)。

二、为什么AHK的参数类型不只采用Ptr和Float、Int64三种?

根据前面讲的,调用Dll文件中的InStr函数的例子写成下面的参数类型也不错:
Pos:=DllCall("Dll文件\InStr", "Ptr",&(s1:="abc123"), "Ptr",&(s2:="abc"), "Int")
为什么AHK要把地址类型(Ptr)分解成了Ptr类型、Str类型和*类型三种呢?

因为Ptr类型只是死板地传递数值,没有多余动作。而Str类型和*类型都有神奇的
调用前后的内部自动转换操作,可以很方便地通过参数接收返回结果,所以很好用。

三、Str字符串类型的好处。

1、首先我们要认识到字符串在内存中是以什么形式保存的。
在AHK中我们用一对双引号包围的一些文字来表示字符串,比如"abc123"。
其实AHK在解析的时候还是会把它赋值给某个变量的,例如 a:="abc123"。
a变量代表一块内存,字符串保存到这块内存中变成了一个一个的数值,
称为字符编码。最初发明编程的人,由于仅使用英文26个字母和数字、
英文符号,加起来不到256个,所以规定了一套ASCII编码,每个字母、
数字等的编码都占1个字节。

后来要推广到所有国家,仅用英文字母肯定不够,所以各国都推出了
自己国家的编码,例如中国的汉字,1个汉字就用两个字节来编码。
通过各国编码时采用不同的码区,综合起来就形成了Ansi编码,
即英文的字母和数字、符号还是用1个字节的ASCII码,其他国家
的编码可能用一个或两个字节来编码。

由于编码越来越多,也比较乱,微软为了大一统,推出了一种Unicode
编码,即所有的字符都用两个字节来编码,并囊括了所有国家的文字。

目前Ansi编码和Unicode编码都很常用,因此WinAPI处理字符串时,
基本上同时提供两套API函数,分别以A和W结尾,对应两种编码。

AHK把除了对象以外的变量都保存为字符串,比如a:="123",a:=123,在内存中都保存为
字符串形式。怎么查看字符串的编码值呢?我们知道“&a”是获取a变量的内存首地址,
*是读取内存地址的1字节值的操作符,我们运行下面的代码看看效果:
-----------------------------
a:=12, p:=&a, n1:=*p, n2:=*(p+1), n3:=*(p+2), n4:=*(p+3), n5:=*(p+4), n6:=*(p+5)
MsgBox, %n1% %n2% %n3% %n4% %n5% %n6% ;-- 显示结果为:49 0 50 0 0 0
-----------------------------
这些数字代表什么含义呢?1的ASCII值为49,2的ASCII值为50,由于我的AHK是Unicode
版本的,Unicode版本的原生字符串(AHK中可用的)都是用两字节表示任何字符编码,
所以49 0占两个字节,50 0也占两个字节,最后两个字节0 0表示字符串的结尾\0字符。
如果AHK是ANSI版本的,原生字符串就是ANSI编码,英文和英文标点符号都占一个字节,
而汉字等语言的编码一个字占两个字节,字符串的结尾用一个字节0表示结束\0字符。

2、WinAPI函数读取字符串时要注意编码匹配。
前面说了,字符串参数压入栈中的是字符串的内存首地址,也就是"Ptr",&a这种形式。
但是假如WinAPI函数的参数需要ANSI编码的字符串,而AHK版本为Unicode编码怎么办?
使用原生编码显然错误,这时有两种方法,一种是手动转换编码,利用StrPut()把
Unicode的编码转换成ANSI编码保存到b变量的内存地址中,然后用"Ptr",&b传递参数。
另一种方法就是利用AHK提供的AStr参数类型,它会在调用前自动把参数的原生字符
串在临时变量的内存中转为ANSI编码并把临时变量的内存首地址压入栈中。
还有一个WStr参数类型,可以在调用前自动把ANSI编码的原生字符串转换为Unicode
编码,再把临时变量的内存首地址压入栈中。当然,如果原生变量与AStr/WStr指定
的一致,就不用转换,直接把a变量的内存首地址压入栈中,等效于"Ptr",&a 。

AHK采用了更聪明的方法确保原生编码符合WinAPI的需求,因为WinAPI为了适应两种
字符串编码,大多数函数都有A/W结尾的两个版本(如DeleteFileA、DeleteFileW),
AHK读取函数名称时如果找不到DeleteFile,会自动根据自身是ANSI编码还是Unicode
编码在函数名称后面加A或W,如果WinAPI准备了这两种版本的,就刚好智能匹配了。

由于AHK有这种智能匹配机制,所以一般用原生的Str类型(或Ptr,&a)就行了。
用Str的好处,一是可以直接采用字符串(比如"Str","abc123"),对于变量也不用
取地址&。另一个更重要的好处是,调用结束后,会更新对应变量的字符串长度。

3、Str类型可以更新变量的字符串长度,这对于通过变量地址返回字符串很好用。
由于AHK是自动管理内存的,变量占用的内存经常变动,需要增大内存时就要动态
申请内存然后把旧的内容拷贝过去,把变量的地址设到新的内存地址上,而字符串
的内存大小体现在字符串的长度上,所以AHK内部标记了每个字符串变量的长度。
AHK自身对字符串的改变操作,比如赋值、替换等都会自动调整这个长度标记。
而调用WinAPI中的函数,由于控制权不在AHK手中,发生了什么它也不知道,如果
原来的字符串为a:="abc123",但是如果WinAPI内部操作在末尾添加了"456\0"
(或者把a的内存内容改为了"xyz\0"),实际上a:="abc123456"(或者a:="xyz"),
而用b:=a,或者MsgBox, %a%来读取a的值时,AHK内部没有更新a的长度,还认为
字符串长度为6,就会造成错误。Str形式会更新字符串长度,而Ptr形式不会更新。

注1:如果不需要自动转换编码,那么Ptr,&a等效于Str,a。如果需要返回字符串,
Ptr,&a形式可以用VarSetCapacity(a,-1)或者StrGet(&a)两种方式手动更新长度。

注2:Astr和Wstr可能传入的是转换编码后的临时变量的地址,如果需要返回字符串,
WinAPI修改的也可能是临时地址中的内容,不能体现在参数的变量所在的内存地址中来,
所以如果需要返回字符串,就不能使用自动转换编码的Astr和Wstr,而要用Str或者
Ptr类型,因为这两种类型,压入栈中的地址就是变量的内存首地址。
如果WinAPI参数需要的编码不同于AHK的原生编码,需要手动用StrPut()转换成目标编码。
如果WinAPI返回的字符串编码与AHK原生编码不同,需要手动用StrGet()转换成原生编码。

四、*类型用于从参数获取函数返回数值。

1、WinAPI通过参数传入的内存地址可以返回多个数值,类似于ByRef类型。
WinAPI把某个数值保存到某个内存地址中并占几个字节(比如占1个字节对应
Char类型,4字节对应Int类型,8字节对应Int64类型),AHK不直接把参数
变量的内存地址通过"Ptr",&a传给WinAPI,而是通过Char*(CharP同义)
传递一个临时内存地址给WinAPI函数,函数把数值写入这个临时内存地址,
函数返回后,AHK自动从这个临时内存地址读取1个字节的数值到变量a,
这样就实现了通过传递临时地址的*参数来返回数值结果。虽然*类型一般用于
返回值,但如果这个Char*,a后面的a值也要作为输入值对WinAPI有用,AHK会在
临时内存地址中把a的值存入这个地址,注意char限定了仅写入1个字节的数值。

2、用Ptr代替*类型不可取。
如果用 "Ptr",&a 传递变量的内存地址给函数来接收返回数值可不可行呢?
首先考虑传递的地址中如果先需要一个输入值,这时要自己手动采用NumPut()写入
到地址&a中。假如我们设置a:=1,它不是已经是数值了吗,怎么还要NumPut()呢?
因为AHK内部把数值变量也都保存为字符串,所以a的内存首地址中保存的是1的
字符串编码,即 Asc("1")==>49,所以必须自己手动NumPut(1,a,"char")。
函数返回后,虽然WinAPI函数确实把返回数值写入到&a地址中了,但是我们要读取
出来的其实是字符串表示的数值,这才能用于AHK中,于是又要手动NumGet()读取。

五、利用Ptr类型输入数据结构。

Ptr类型只是简单传递了变量的内存地址给函数,它没有str、*类型那么
多的内部智能转换操作,它主要用于传递一个数据结构的地址给函数。
函数的参数往往需要特定的数据结构,因为只要得到这个结构的首地址,
按照这个结构的约定格式,就能用内存首地址加偏移获取各部分的数据了。

我们一般先用 VarSetCapacity(a,100) 申请一块内存,然后使用
NumPut() 按WinAPI约定的数据结构手动把数值写入a变量内存对应的
地址中,数据结构设定好后,再把&a地址传入函数。调用结束后,
还可以手动使用 NumGet() 从a变量的数据结构中读取需要的值。
NumPut()、NumGet()都是AHK对内存的指针操作,&取变量内存地址也是指针。

WinAPI的读写都是对内存地址的操作,所以在调用前一般要先用VarSetCapacity()
申请足够的内存,避免WinAPI乱写内存覆盖了有用的数据。

六、其他说明:

1、返回类型:WinAPI通过函数的返回值可以返回1个数值。通过寄存器EAX返回,
DllCall读取寄存器的数值到函数返回变量,这时由返回类型指定读取的字节数,
返回类型一般是地址型Ptr或者整型Int两种,比较特殊的是Str返回类型,
AHK会把返回的数值看做字符串的内存首地址,并复制字符串到返回变量中。

2、调用约定:C语言写的函数,返回类型前一般要添加"Cdecl",而WinAPI
使用标准调用形式则不用添加。若C函数编译时指定了使用标准调用也不用。

"C"调用约定是栈的平衡由调用者来完成,调用者压入了多个参数到栈中,
最后栈顶指针的恢复要由调用者来做。而标准调用则要函数自己来恢复,掉用者
只管压栈不管恢复。所以如果调用C函数不加上"Cdecl",默认使用标准调用,
栈的平衡无法完成,多次调用后会耗尽栈资源。

3、U前缀:指示使用无符号的类型,这对于输入数值没有意义(int64除外),
因为输入时指定char和uchar,写入内存的都是同样的数值,但对于WinAPI
的输出数值,即 函数返回类型 和 *类型 就有意义了,AHK内部读取的值
可能不同(类似于用NumGet()读取)。

4、Windows数据类型对应于AHK参数类型的简单判断。(懂了上面的就不难了)

简单数值的WinAPI参数绝大部分对应Int类型,比如:DWORD、LONG、BOOL、
COLORREF。带64的、LONGLONG对应Int64。浮点类型对应Float、Double。
对于输入类型U前缀不重要,因此UInt写成Int也没关系。

内存地址的WinAPI参数对应于AHK的三种形式:一般WinAPI声明中的各种句柄
(H开头的)、带LP或P开头的、带PTR的都是指针,即地址类型,一般对应Ptr类型。
但是带STR的指针则对应Str类型更方便些(用Ptr可行但稍麻烦,参看上面的说明)。
如果是用于输出结果的指针就对应*类型(用Ptr就不可取,参看上面的说明)。

查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
[+] 2用户表示感谢feiyue
2017-07-11, 02 : 04
RE: DllCall的使用简介
DllCall的使用简介已经更新,应该更容易懂了。
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
[+] 1用户表示感谢feiyue
2017-08-07, 09 : 21
RE: DllCall的使用简介
写得再多,不如一句话一个实例说明。
当然能编就已经不错了。

梦幻软件天堂
人生R棋随1出招借力打力多而合1扬C避D奇L断金
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2017-12-16, 19 : 06 (这个帖子最后修改于: 2017-12-16 19 : 57 by feiyue.)
RE: DllCall的使用简介
下面我举个简单的例子吧,请对照前面讲的来理解。
GetUserName是一个WinApi函数,用于获得当前windows登录的用户名。
我们先看看MSDN网站的权威声明:
https://msdn.microsoft.com/en-us/library...s.85).aspx
----------------------------------
BOOL WINAPI GetUserName(
_Out_ LPTSTR lpBuffer,
_Inout_ LPDWORD lpnSize
);
DLL | Advapi32.dll
Unicode and ANSI names | GetUserNameW (Unicode) and GetUserNameA (ANSI)
----------------------------------
从上面的声明我们知道,这个函数位于Advapi32.dll库文件中,
它在库文件中有两个版本名称,分别是GetUserNameW处理Unicode字符串,
GetUserNameA处理ANSI字符串。

我前面说过,WinApi凡是牵涉到字符串的函数,大多都提供了A和W结尾的两个
版本的函数供用户使用,AHK可以根据自身是ANSI版还是Unicode版,在找不到
函数时会智能在函数名末尾添加A或W,从而智能找到刚好匹配的WinApi函数。
所以,AHK调用的第一部分为:DllCall("Advapi32.dll\GetUserName" 就好了。

当然因为我的AHK为Unicode版,我直接使用GetUserNameW也行,AHK能够
直接找到这个函数名,就不会在末尾添加A或W再尝试了。

如果直接使用GetUserNameA行不行呢?由于AHK能够直接找到这个函数名,
同样不会在末尾添加A或W再尝试了。这样该函数在处理输入输出时,内部默认
都是ANSI编码的字符串,所以对于输入字符串,我们需要将AHK原生的Unicode
编码字符串用StrPut转换成ANSI编码字符串,再把字符串首地址传给该函数,
对于输出字符串,需要用StrGet将返回的字符串首地址转换为Unicode字符串。

废话不多说,我们再看看它的参数。它有两个参数,我们一一分析。

第一个参数 “_Out_ LPTSTR lpBuffer”,用于返回Windows登录的用户名字符串。
这个参数是输出的,以LP开头的都是指针,也就是字符串首地址,毫无疑问,
我们用AHK的地址类型“Ptr”作为参数类型是可以的。我前面说过,对于含有STR
的指针,使用“Str”类型会更方便些,下面会详细分析。

第二个参数 “_Inout_ LPDWORD lpnSize”,用于设置处理的字符串长度。
这个参数既是输入一个长度最大值,又是输出结果的长度值,以LP开头的都是指针,
毫无疑问,我们用AHK的地址类型“Ptr”作为参数类型是可以的。但是我前面说过,
对于输出数值的地址参数,使用“*”类型可以自动转换,即输入时,将AHK常规的
字符串型数字,用NumPut写入一个临时内存地址,返回时,从临时内存地址用
NumGet读取数值转换为字符串型数字,供AHK常规使用。所以这个参数用
“Ptr”类型不妥,使用“Int*”则刚刚好。使用无符号的“UInt*”也可以,从临时内存
读取数值时,有符号的Int类型,因为32位二进制中第一位表示正负符号,只有31位
表示数值,所以最大范围为2147483647 (0x7FFFFFFF),而无符号的UInt类型
表示的数值最大范围为4294967295 (0xFFFFFFFF),范围翻了一倍。但是对于
我们这个函数要返回的登录用户名字符串长度而言,长度只有几十字节,所以
没必要用UInt。不过Windows的DWORD类型其实对应于无符号的“UInt”。

参数类型搞清楚了,就可以写出调用格式了:
DllCall("Advapi32.dll\GetUserName", "Str",name, "Int*",size)
最后用消息框显示结果:
MsgBox, % "登录用户名:" name "`n字符串长度:" size
这样就可以了吗?

不行。我前面说过,WinApi都是对内存地址的操作,所以在调用前一般要先用
VarSetCapacity()申请足够的内存,避免WinAPI乱写内存覆盖了有用的数据。
这个函数把返回用户名字符串写入name变量的内存地址,但是我们的AHK的变量
初始都为空的,内部合法占用的内存为0字节,明显不够WinApi写入的。为了可靠
地让WinApi操作合法的内存,我们用VarSetCapacity(name,100)手动申请100
字节的内存给name变量合法占用,因为100字节内存对于ANSI编码的英文可以
写入99个字母(加上末尾的“\0”字符),对于Unicode字符可以写入49个字符,
应该够用了。所以最后完整的调用代码为:
--------------------------------
size:=100, VarSetCapacity(name, size)
DllCall("Advapi32.dll\GetUserName", "Str",name, "Int*",size)
MsgBox, % "登录用户名:`t" name "`n`n字符串长度:`t" size
--------------------------------
这个函数其实有个返回类型“BOOL”,用于返回函数调用是否成功,对应于
AHK的Int类型,由于我们不需要该返回值,所以可以忽略返回类型参数那部分。

最后我们再假设WinApi只有GetUserNameA这一个函数,而我们的AHK又是
Unicode版的情况。对于输入字符串参数,比如 a:="abc",我们只要使用
"AStr",a 即可让AHK帮我们自动转换为临时变量的ANSI字符串传给WinApi。
但是这个函数需要从参数的地址返回字符串,所以不能使用“AStr”类型,需要
手动转换,可以这样做:
--------------------------------
a:="abc"
VarSetCapacity(b, StrLen(a)*2+100) ;-- 6字节不够用
StrPut(a, &b, "CP0")
--------------------------------
这样b变量的内存地址就有了ANSI编码的字符串内容,然后:
--------------------------------
DllCall("Advapi32.dll\GetUserNameA", "Ptr",&b, "Int*",100)
--------------------------------
把b变量的内存首地址传给WinApi,最后读取WinApi的返回结果时
还要把ANSI编码的结果转换回Unicode编码供AHK使用:
--------------------------------
name:=StrGet(&b,"CP0")
MsgBox, % "登录用户名:`t" name
--------------------------------
这样就大功告成了。虽然麻烦点,但是对于WinApi库函数中没有与
AHK本身Unicode还是ANSI版匹配的情况,只好这样手动解决。

由于例子比较简单,没有牵涉到WinApi参数常见的传入数据结构。
其实构造数据结构很简单,先用 VarSetCapacity(a,16) 申请内存,
再用 NumPut(写入数值, a, 偏移字节, "Int") 根据WinApi的数据结构
要求写入数值到正确的偏移位置,然后传入 "Ptr",&a 给WinApi即可。

查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
[+] 1用户表示感谢feiyue
2017-12-16, 20 : 47
RE: DllCall的使用简介
感谢feiyue大神的讲解!非常有用!
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2018-08-16, 13 : 12 (这个帖子最后修改于: 2018-08-16 13 : 13 by feiyue.)
RE: DllCall的使用简介
我对AHK参数类型的等效性有了新的理解,因此使用WinApi更简单了!Smile
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2019-05-14, 08 : 53 (这个帖子最后修改于: 2019-05-15 07 : 36 by 火冷.)
RE: DllCall的使用简介
4字节和8字节的区别,都是根据Windows来判断吗?
那ahk的U32和U64有什么区别?
A_PtrSize是根据ahk的版本来区分的吧?有点迷糊。

数据类型转dllcall的类型,加长度,写了个初步的函数,待完善
代码: (全选)
win2dllcall(tp) {
    objApi := {"WORD":["Short",2]
        ,"DWORD":["Int",4]
        ,"SIZE_T":["Ptr",4]
        ,"BOOL":["Int",4]
        ,"LONG":["Int",4]
        ,"Int64":["Int64",8]
        ,"LONGLONG":["Int64",8]
        ,"Float":["Float",4]
        ,"DOUBLE":["Double",8]
        ,"CHAR":["Char",1]
        ,"BYTE":["Byte",1]
    }
    If objApi.HasKey(tp)
        arrThis := objApi[tp].Clone()
    Else If (str ~= "STR$") ;STR结尾
        arrThis := ["Str",4]
    Else If (str ~= "^LP") ;LP开头
        arrThis := ["Ptr",4]
    return arrThis
}
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2019-05-14, 10 : 55
RE: DllCall的使用简介
https://docs.microsoft.com/en-us/windows...mprocesses
像这种数组的就更没概念了,上手不易
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2019-05-15, 07 : 09
RE: DllCall的使用简介
https://docs.microsoft.com/en-us/windows...versionexa
我用以下代码,为什么失败了呢,求指教。
VarSetCapacity(struct, 156, 0)
MsgBox(DllCall("GetVersionEx", "Ptr",&struct))
MsgBox(NumGet(struct, 0, "Int"))
MsgBox(NumGet(struct, 4, "Int"))
MsgBox(NumGet(struct, 8, "Int"))
MsgBox(NumGet(struct, 12, "Int"))
MsgBox(NumGet(struct, 16, "Int"))
MsgBox(NumGet(struct, 20, "Char"))
MsgBox(NumGet(struct, 148, "Short"))
MsgBox(NumGet(struct, 150, "Short"))
MsgBox(NumGet(struct, 152, "Short"))
MsgBox(NumGet(struct, 154, "Byte"))
MsgBox(NumGet(struct, 155, "Byte"))
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2019-05-15, 15 : 33 (这个帖子最后修改于: 2019-05-15 16 : 45 by feiyue.)
RE: DllCall的使用简介
(2019-05-14 08 : 53)火冷 提到:  4字节和8字节的区别,都是根据Windows来判断吗?
那ahk的U32和U64有什么区别?
A_PtrSize是根据ahk的版本来区分的吧?有点迷糊。

4字节和8字节的区别,准确来说是根据ahk的U32和U64版本来判断。
为让新手容易理解,前文说地址值(指针)占的字节数在64位系统中
占8字节,其实在64位系统的兼容32位模式中,是按32位系统计算的,
即字节数为4。所以科学的做法是按A_PtrSize来判断地址值的位数。
而A_PtrSize在AHK的U32版中为4, 在AHK的U64位版中为8。
AHK的U32版可以用在32位系统和64位系统的兼容32位模式中,
AHK的U64版只能用于64位系统。

=================

(2019-05-14 10 : 55)火冷 提到:  https://docs.microsoft.com/en-us/windows...mprocesses
像这种数组的就更没概念了,上手不易

BOOL WINAPI EnumProcesses(
_Out_ DWORD * pProcessIds,
_In_ DWORD CB,
_Out_ DWORD * pBytesReturned
);

这种返回数组结构的WinApi,其实并不难,
第一个参数是输出用的(_Out_),一个数组结构的指针,
所以我们需要先分配一点内存,然后传递内存首地址给它
VarSetCapacity(a, 100*4, 0) 然后 Ptr,&a
第二个参数是输入用的(_In_),说明数组的大小,所以我们这里是 int,100
第三个参数是输出用的(_Out_),通过一个地址输出Api返回的数组字节数。
返回数值一般用*号类型,所以用 "int*",ReturnSize 即可。用法如下:

VarSetCapacity(a, 100*4, 0)
DllCall("psapi.dll\EnumProcesses", Ptr,&a, int,100, "int*", ReturnSize)
Loop, % ReturnSize//4
PID:=NumGet(a, 4*(A_Index-1), "uint")

=================

(2019-05-15 07 : 09)火冷 提到:  https://docs.microsoft.com/en-us/windows...versionexa
我用以下代码,为什么失败了呢,求指教。

你看GetVersionEx函数介绍的时候不够仔细,明明说了要填充结构的第一个成员,但你没有做。
【如果为OSVERSIONINFO或 OSVERSIONINFOEX结构的dwOSVersionInfoSize成员 指定了无效值,则该函数将失败。】
应该这样:
size:=5*4+128*1+3*2+2*1 ; =>156
VarSetCapacity(struct, size, 0), NumPut(size, struct, "uint")
MsgBox(DllCall("GetVersionEx", "Ptr",&struct))
MsgBox(NumGet(struct, 0, "UInt"))
MsgBox(NumGet(struct, 4, "UInt"))
MsgBox(NumGet(struct, 8, "UInt"))
MsgBox(NumGet(struct, 12, "UInt"))
MsgBox(NumGet(struct, 16, "UInt"))
MsgBox(StrGet(&struct+20, 128, "CP0"))
MsgBox(NumGet(struct, 148, "UShort"))
MsgBox(NumGet(struct, 150, "UShort"))
MsgBox(NumGet(struct, 152, "UShort"))
MsgBox(NumGet(struct, 154, "Byte"))
MsgBox(NumGet(struct, 155, "Byte"))

/*
typedef struct _OSVERSIONINFOEXA {
DWORD dwOSVersionInfoSize;
DWORD dwMajorVersion;
DWORD dwMinorVersion;
DWORD dwBuildNumber;
DWORD dwPlatformId;
CHAR szCSDVersion[128];
WORD wServicePackMajor;
WORD wServicePackMinor;
WORD wSuiteMask;
BYTE wProductType;
BYTE wReserved;
} OSVERSIONINFOEXA, *POSVERSIONINFOEXA, *LPOSVERSIONINFOEXA;
*/
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
[+] 1用户表示感谢feiyue
2019-05-15, 22 : 54
RE: DllCall的使用简介
非常感谢回复!!!
请问EnumProcesses参数的in和out在哪里看呢?网页上没找到。是看下面说明得出的?这个示例正常运行。
而下面的代码,运行还是出错了,dllcall返回0。麻烦再看看原因

size := 156
VarSetCapacity(struct, size, 0)
NumPut(size, struct, "UInt")
MsgBox(DllCall("GetVersionEx", "Ptr",&struct))
MsgBox(NumGet(struct, 0, "UInt"))
MsgBox(NumGet(struct, 4, "UInt"))
MsgBox(NumGet(struct, 8, "UInt"))
MsgBox(NumGet(struct, 12, "Int"))
MsgBox(NumGet(struct, 16, "Int"))
MsgBox(NumGet(struct, 20, "Char"))
MsgBox(StrGet(&struct+20, 128, "CP0"))
MsgBox(NumGet(struct, 150, "Short"))
MsgBox(NumGet(struct, 152, "Short"))
MsgBox(NumGet(struct, 154, "Byte"))
MsgBox(NumGet(struct, 155, "Byte"))
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2019-05-16, 07 : 34
RE: DllCall的使用简介
Win7 64,AHK32,入栈和出栈每次是4字节吗?
又感觉入栈和出栈和AHK无关,是不是应该是8字节呢?
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2019-05-16, 16 : 01 (这个帖子最后修改于: 2019-05-16 17 : 50 by feiyue.)
RE: DllCall的使用简介
请问EnumProcesses参数的in和out在哪里看呢? 百度查询,很多都有详细的。

OSVERSIONINFOEXA 结构是 Ansi 版的,用于 GetVersionExA 函数,所以应该这样:

size:=5*4+128*1+3*2+2*1 ; =>156
VarSetCapacity(struct, size, 0), NumPut(size, struct, "uint")
MsgBox(DllCall("GetVersionExA", "Ptr",&struct))
MsgBox(NumGet(struct, i:=0, "UInt"))
MsgBox(NumGet(struct, i+=4, "UInt"))
MsgBox(NumGet(struct, i+=4, "UInt"))
MsgBox(NumGet(struct, i+=4, "UInt"))
MsgBox(NumGet(struct, i+=4, "UInt"))
MsgBox(StrGet(&struct+(i+=4), 128, "CP0"))
MsgBox(NumGet(struct, i+=128, "UShort"))
MsgBox(NumGet(struct, i+=2, "UShort"))
MsgBox(NumGet(struct, i+=2, "UShort"))
MsgBox(NumGet(struct, i+=2, "Byte"))
MsgBox(NumGet(struct, i+=1, "Byte"))

如果用 DllCall("GetVersionEx", "Ptr",&struct),找不到函数会根据AHK的Unicode版自动添加W,成为 GetVersionExW,
而 OSVERSIONINFOEXW 结构是 Unicode 版的,其中预留的128个字符空间占据 128*2 字节,因此自适应的写法是:

size := 5*4+128*(A_isUnicode?2:1)+3*2+2*1
VarSetCapacity(struct, size, 0), NumPut(size, struct, "uint")
MsgBox(DllCall("GetVersionEx", "Ptr",&struct))
MsgBox(NumGet(struct, i:=0, "UInt"))
MsgBox(NumGet(struct, i+=4, "UInt"))
MsgBox(NumGet(struct, i+=4, "UInt"))
MsgBox(NumGet(struct, i+=4, "UInt"))
MsgBox(NumGet(struct, i+=4, "UInt"))
MsgBox(StrGet(&struct+(i+=4), 128))
MsgBox(NumGet(struct, i+=128*(A_isUnicode?2:1), "UShort"))
MsgBox(NumGet(struct, i+=2, "UShort"))
MsgBox(NumGet(struct, i+=2, "UShort"))
MsgBox(NumGet(struct, i+=2, "Byte"))
MsgBox(NumGet(struct, i+=1, "Byte"))

================

你可以用下面的代码测试一下, Win7 64,AHK32,入栈和出栈是不是4字节:

DllCall(RegisterCallback("TheFunc", "C F"), int,1, int,2, int,3, int,4, "CDecl")

TheFunc(params*) {

; 返回1,2则为入栈和出栈为4字节
MsgBox % Format("0x{:08X}",NumGet(params+0, "int"))
. ", " Format("0x{:08X}",NumGet(params+4, "int"))

; 返回1,2则为入栈和出栈为8字节
MsgBox % Format("0x{:016X}",NumGet(params+0, "int64"))
. ", " Format("0x{:016X}",NumGet(params+8, "int64"))

}
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
[+] 1用户表示感谢feiyue
2019-05-17, 07 : 55
RE: DllCall的使用简介
非常感谢在入门的时候有你的解答!!!少踩了很多坑!希望其他学习的人也能看到这个帖子。
再请教下抓取软件的消息号这块有研究吗?比如我手动进行了操作,想查看消息号是多少,
这样以后可以用PostMessage的方法来操作会更稳定,这块一直没找到好的教程
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2019-05-17, 09 : 43
RE: DllCall的使用简介
帮助文件中就有 Winspector Spy 的操作指南,已经说得很清楚了,不明白的百度搜索有更详细的使用教程。
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
2019-06-03, 05 : 43 (这个帖子最后修改于: 2019-06-03 05 : 54 by feiyue.)
RE: DllCall的使用简介

DllCall的使用指南(第三版)

1、参数类型其实很简单:
根据AHK自身是32位版或者64位版,AHK在调用操作系统的Dll时也对应
32位版或者64位版。如果使用非系统的Dll,也要调用相对应的版本。
32位的Dll编译时,函数的每个参数默认占4字节,而64位的Dll编译时,
函数的每个参数默认占8字节,所以AHK的DllCall调用函数时每个参数
默认是按A_PtrSize字节来发送给函数的,也就是将类型扩充为Ptr类型。
所以初学者先把参数类型全部设为Ptr即可,然后根据WinAPI函数的参数
声明考虑例外情况或更合适的情况:
(1)如果AHK为32位版,但是参数声明是:Int64、long long这种明显
是8字节的情况,必须用AHK的int64类型,或用两个参数顶替1个。
(2)如果参数声明是浮点数,必须用AHK4字节的float或8字节的double。
(3)如果参数声明是:Int、DWORD这种明显是4字节的情况,使用AHK的
int类型显得更合适些,但是AHK传参时仍然是按Ptr类型来执行的。

2、数据结构其实很简单:
数据结构是一块连续内存,内部按各个声明的类型写入数值,所以AHK
先要用VarSetCapacity(a,100)申请一块内存给变量,&a为内存首地址,
然后用NumPut(1,&a+0,"int")写入数值到&a+偏移的内存地址中。
AHK也可使用等效的NumPut(1,a,0,"int"),隐藏&a防止初学者使用错误。
然后将&a内存首地址传入函数。最后再用NumGet()读取数据结构中的值。

3、使用字符串其实很简单:
WinAPI中的函数如果参数声明是字符串,那么我们就要传递字符串的
内存首地址给它,这时就要考虑字符串的编码匹配问题。根据AHK自身
是Ansi版或Unicode版,AHK内部FileRead、WinGetTitle获取的字符串,
或者定义的字符串变量a:="abc",都是使用对应的原生编码。
AHK内部提供了Ansi和Unicode转换的函数:StrPut()和StrGet(),
StrPut()将原生编码写入为指定编码的内存编码序列,
StrGet()将指定编码的内存编码序列读取为原生编码。
WinAPI函数声明中有的指明了仅支持Ansi编码,但大部分都提供了两个
版本的函数,以A结尾的支持Ansi编码,以W结尾的支持Unicode编码。
(1)AHK读取Dll中的函数可以指明A或W后缀,也可以不带后缀,AHK
自动根据自身是Ansi或Unicode版尝试添加A或W来查找函数,智能匹配。
(2)如果估计函数参数与AHK的Ansi或Unicode版匹配,参数类型使用
Str即可(也可用Ptr,&a),如果估计不匹配,参数类型可以使用
AStr或者WStr,让AHK自动帮我们转换编码再传递内存首地址给函数。
(3)如果需要通过参数返回字符串,如果编码不匹配,这时不能使用
AStr或者WStr,只能用Ptr,&a,然后用StrGet()转码读取。如果不匹配
还要输入字符串,必须手动StrPut()转码到新变量b,然后传递Ptr,&b。
如果编码匹配,最好使用Str,若用Ptr,&a,则还需StrGet()不转码读取。

4、WinAPI由参数返回数值很简单:
前面说的第一种由参数的数据结构的内存首地址来返回数据。
前面说的第二种由参数的字符串的内存首地址来返回字符串。
现在说的第三种由参数的传入的内存地址来返回一个数值。
这是WinAPI的参数声明为_out_和*类型时的操作方式。
(1)AHK可以用Ptr,&a传递一个内存地址,WinAPI函数将数值写入
&a的地址中,但是AHK使用的是字符串形式的数字,还要用NumGet()
读取&a内存地址中的二进制数值为字符串数值,手动读取比较麻烦。
(2)AHK提供了*类型来自动读取参数返回的数值,它传递一个临时
内存地址给函数,函数返回再读取临时内存地址的值给参数的变量。
所以参数返回数值的情况应该用*类型替换原来傻瓜设置的Ptr类型。

5、还需注意的事项也很简单:
(1)由于WinAPI操作的内存地址往往是参数传递的,所以我们构造
数据结构,或者由参数返回字符串之前,都要用VarSetCapacity()
先申请内存再传递内存首地址,避免WinAPI函数乱读写非法内存。
(2)如果调用C语言写的函数,需要指明CDecl调用约定,WinAPI的
函数大多数是标准调用,所以调用约定通常省略。
(3)对于无符号类型前缀U,对于输入没有意义,对于输出数值的
函数返回值或者*类型就有意义了,读取的数值范围大一些。

DllCall的知识点就是这么多,是不是很简单呢。

查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
发表回复 


论坛跳转:


联系我们 | Autohotkey 中文站 | 回到顶部 | 回到正文区 | 精简(归档)模式 | RSS 聚合