脚本兼容性

优质
小牛编辑
139浏览
2023-12-01

尽管为 AutoHotkey 1.0 编写的许多脚本不需要修改就可以运行在 AutoHotkey 1.1 上,然而由于两种版本的根本区别可能使得某些功能运行不正常。因为最主要出现问题的差异只影响高级功能(例如 DllCall),大多数用户不需要担心。

AutoHotkey 1.1 也称为“AutoHotkey_L”,而 AutoHotkey 1.0 则表示“AutoHotkey Basic”。AutoHotkey_L 的一些旧版本使用 1.0.* 版本号,所以为了清晰,这里的帮助中使用名称而不是版本号来区分 AutoHotkey 的两种分支。

注:一些常见的问题是由对 Unicode 文本的支持引起的,可以通过使用 AutoHotkey_L 的 ANSI 版本来避免。

基本

高度影响:

  • 不再允许的一些语法错误
  • FileRead 可能返回损坏的二进制数据
  • 变量和函数名中不允许使用 [、] 或 ?

中度影响:

  • Transform 的 Unicode 子命令在 Unicode 版本中不再可用
  • 运行 AutoHotkey.ahk 来代替 AutoHotkey.ini
  • SetFormat, Integer, H 区分大小写
  • A_LastError 会被更多的命令修改
  • MsgBox 的逗号智能处理更加一致
  • Gui +Owner 替换额外的样式
  • 默认情况下会启用 GUI 的 DPI 缩放。
  • SoundSet 和 SoundGet 在 Vista 及更高版本中表现更佳
  • ~颚化符会影响自定义修饰键的工作方式
  • x & y:: 会使得 x::x up:: 都会在 x 释放时触发。

低度影响:

  • If var is type 默认忽略系统区域设置
  • GroupActivate 设置 ErrorLevel 和 GroupAdd 的 Label 作用的差异
  • Run 和 RunWait 解释 Target 的差异
  • Control-Z 不会被解释为文件结束
  • 兼容模式可能引起冲突
  • A_IsCompiled 总是只读的
  • 前导和尾随的 `t 序列不再被丢弃

高级

  • Unicode 和 ANSI 的比较
    • VarSetCapacity
    • DllCall
    • NumPut / NumGet
  • 指针大小

基本

语法错误

AutoHotkey_L 中不允许某些在 AutoHotkey Basic 中接受的语法错误。识别出这些错误后,其中大多数都很容易改正。用 AutoHotkey_L 运行脚本时下面的错误会立即被检测出来,必须改正后才能运行脚本:

  • 在每个命令和它的参数之间需要空格、tab 或逗号。例如,MsgBox< fooIf!foo 不会被允许。
  • Hotkey, IfSomething,当 Something 无效时不会被允许。

其他一些语法错误会在脚本运行时检测出来。这些情况会导致在退出当前线程前显示错误消息:

  • 常见:无法识别或不良格式的 GuiGui ShowGuiControl 选项。
  • 带有空的组名称的 GroupAdd。以前这个问题会让当前线程安静地退出。
  • Gui 选项 +LastFoundExist 不能和其他选项组合使用,因为这样会让它的行为与 +LastFound 一样。

当前无法检测但会在 AutoHotkey_L 出现问题的其他语法错误:

  • 通过 ( 进行自动连结具有更多选择性,所以类似 12(34) 这样无效的表达式不再起作用。

FileRead

在某些情况下 FileRead 会在代码页之间转换文本,因此可能输出错误的二进制数据。要避免此问题,请添加 *c 选项或使用 FileOpen 代替。

变量和函数名

字符 []? 被预留用于表达式,所以在变量名中使用时不再合法。因此,?(用于三元运算)的两边不再需要空格。另请参阅对象语法

可能或不能自动检测的错误:

  • 当脚本在表达式中的变量名使用这些字符时,一般脚本会运行而不显示错误,但这些字符可能被错误解释为运算符而不是变量名的一部分。
  • 如果这些字符用在双重解引(例如 Array%n%,其中 n 包含了上面的某个字符),则当脚本运行到计算双重解引时会显示错误消息。
  • 如果这些字符用在其他环境中(例如赋值的左边、命令的输入、输出变量的名称或在百分号之间),则会显示错误消息并阻止脚本运行。

Transform

在 AutoHotkey_L 的 Unicode 版本中一些 Transform 子命令被修改或不可用:

默认脚本

未指定脚本运行 AutoHotkey_L 时,默认加载 .ahk 文件而不是 .ini。这个文件的名称取决于当前执行程序的文件名。想了解更多细节, 请参阅 传递命令行参数到脚本.

SetFormat, Integer[Fast], H

使用大写字母 H 时, 十六进制数字中 A-F 也将为大写. AutoHotkey Basic 总是使用小写形式的数字。请参阅 SetFormat

A_LastError

现在后面的这些命令会设置 A_LastError 来辅助调试:FileAppend、FileRead、FileReadLine、FileDelete、FileCopy、FileMove、FileGetAttrib/Time/Size/Version、FileSetAttrib/Time、FileCreateDir、RegRead、RegWrite、RegDelete。使用这些命令的其中任何一个会覆盖 A_LastError 之前的值。

MsgBox

MsgBox 的智能逗号处理进行了改变,提高了灵活性及与其他所有命令的一致性。在大多数情况中,MsgBox 会按预想的方式执行。在某些罕见的情况下,依赖旧有怪癖的脚本可能观察到行为的变化。例如:

; 现在这样会被解释为表达式(选项)跟着文本(标题)
; 而不是含多个子表达式的单个表达式(文本):
MsgBox % x, y
; 添加小括号来强制使用旧式解释:
MsgBox % (x, y)

; 现在这样会显示空对话框而不是文本“0, Title”:
MsgBox 0, Title
; 这两个在 AutoHotkey_L 和 AutoHotkey Basic 的行为和预期一样:
MsgBox 0, Title, % ""   ; 显示空对话框
MsgBox 0`, Title        ; 显示文本“0, Title”

; 现在这样会显示空对话框而不是文本“, Title”:
MsgBox,, Title

Gui +Owner

对 Gui 应用 +Owner 选项还会移除 WS_CHILD 样式并设置 WS_POPUP 样式。这样会破坏在设置这些样式之后使用 +Owner 设置 Gui 的父窗口的脚本。

DPI 缩放

DPI 缩放 默认会在脚本的 GUI 中启用。当系统 DPI 设置不是 96(100%)时它会影响脚本。要禁用,请使用 Gui -DPIScale

Windows Vista 及更高版本中的 Sound 命令

SoundSetSoundGetSoundSetWaveVolumeSoundGetWaveVolume 已经改进了对 Windows Vista 及更高版本的支持。典型的行为改变包括:

  • 脚本会影响整个系统(通常希望如此)而不是仅脚本自身。
  • 设备编号方式的改变——每个输出设备或输入设备被视为独立的。

~颚化符和自定义组合键

从 v1.1.14 起,当一个按键作为修饰符用于组合键时颚化符前缀会影响该热键的工作方式。

自定义组合键和 Down/Up 热键

如果没有使用颚化符前缀,则对于自定义修饰键会同时定义按键按下和弹起热键,它们在该按键释放时都会触发。例如,x & y:: 会使得 x::x up:: 都会在 x 释放时触发,而以前 x:: 是不会触发。

If var is type

If var is type 只有在使用 StringCaseSense, Locale 时才不会忽略系统区域设置。

窗口组

GroupActivate 在没有找到要激活的窗口时设置 ErrorLevel 为 1,否则为 0。以前 ErrorLevel 会保持不变。

GroupAddLabel 参数应用于作为一个整体的窗口组而不是组中某个特殊的窗口规格。关于这个变更的讨论可以在论坛上找到。然而,不建议使用这个参数;相反地应该在调用 GroupActivate 后检查 ErrorLevel。

Run / RunWait

AutoHotkey_L 包含了一些对 RunRunWait 命令的 Target 参数的增强解释。这样能接受一些之前不起作用的情况,但在罕见的情况下也会影响 AutoHotkey Basic 中能执行的情况。新的行为如下:

  • 如果 Target 以引号开始,则到下一个引号之间的所有内容被视为要执行的操作,一般为可执行文件。
  • 否则首个以空格结束并且为一个现有文件或以 .exe, .bat, .com, .cmd 或 .hta 结尾的子字符串被视为操作. 这样就允许 .ahk、.vbs 或 .lnk 这样的文件类型接受参数,同时也允许像以前的版本那样不需要绝对路径运行“已知”的可执行文件(例如 wordpad.exe)。

Control-Z

Loop ReadFileReadLine 不再把字符 Control-Z(0x1A)解释为文件结束标记。任何的 Control-Z,即使某一个恰好出现在文件末尾,都原样加载。FileRead 已经忽略这个字符,所以不受这个问题的影响。

兼容模式

如果用于运行脚本的 EXE 文件属性中设置 兼容模式 为 Windows 95, 98/ME 或 NT4, 脚本可能无法正常运行. 这是由于兼容模式会把特定的 Windows 版本报告给应用程序,但 AutoHotkey_L 不支持这些版本。例如设置兼容模式为 Windows 95 或 98/ME 将使得 MsgBox %A_OSVersion% 报告 WIN_NT4.

A_IsCompiled

A_IsCompiled 当脚本未编译时被定义为空字符串。之前它会被保留为未定义,这意味着当脚本未编译时对它进行赋值(例如 A_IsCompiled := 1)是有效的。现在在所有的情况中它都被视为只读的内置变量。

转义的空白符

不再从每个参数的开始和末尾移除转义的空白字符(例如 `t`)。例如,StringReplace s, s, `t 现在是有效的,并会移除 s 中的所有 tab 字符。

Unicode 和 ANSI 的比较

脚本处理的每个文本值会被保存为一序列或一串字符。每个字符的数值和大小取决于所使用的AutoHotkey构建:UnicodeANSI。这些细节对于进行下列操作的脚本通常很重要:

  • 通过 DllCall 传递字符串到外部函数。
  • 通过 PostMessage/SendMessage 传递字符串。
  • 通过 NumPut/NumGet 直接操作字符串。
  • 使用 VarSetCapacity 来确保变量中能存储一定数目的字符。

为一种特殊格式设计的脚本使用错误的 AutoHotkey 版本运行时常常会遇到问题。例如,一些为 AutoHotkey Basic 编写的脚本能正常运行在 AutoHotkey_L 的 ANSI 版本中,但在 Unicode 版本中会失败。如果您不确定正在使用的版本,请运行下面的脚本:

MsgBox % A_IsUnicode ? "Unicode" : "ANSI"

ANSI:每个字符占用一个字节(8 位)。大于 127 的字符代码取决于系统的语言设置。

Unicode:每个字符占用两个字节(16 位)。字符代码在 UTF-16 格式中定义。

语义注:技术上,一些 Unicode 字符表示为两个 16 位代码单元,一起被称为“代理项对”。同样地,一些 ANSI 代码页(通常称为双字节字符集)含有一些双字节字符。然而,由于特殊的原因它们几乎都被视为两个单独的单元(为了简化而称为“字符”)。

VarSetCapacity

VarSetCapacity 设置一个变量占用的空间, 单位为字节. 要根据字符数设置变量的容量,必须同时考虑字符的大小。例如:

VarSetCapacity(ansi_var,    capacity_in_chars)
VarSetCapacity(unicode_var, capacity_in_chars * 2)
VarSetCapacity(native_var,  capacity_in_chars * (A_IsUnicode ? 2 : 1))
VarSetCapacity(native_var,  t_size(capacity_in_chars))  ; 请参阅下面

VarSetCapacity 的两种主要用途:

  1. 在通过逐步连接构造字符串时扩展变量以存储估计的字符数和提升性能。例如,VarSetCapacity(var, 1000) 允许使用 1000 字节,这样的容量在 AutoHotkey_L 的 Unicode 版本中只能保存 500 个字符。这样会影响性能,不过脚本应该能正常执行。
  2. 重设变量的大小来存储二进制结构。如果结构直接包含文本,则必须考虑文本的格式。这取决于结构自身,有时即使在 AutoHotkey_L 的 Unicode 版本中也会使用 ANSI 文本。如果变量太小,则脚本可能会崩溃或行为异常(取决于使用结构的方式)。

DllCall

使用 "Str" 类型时, 表示字符串使用当前版本原生的编码格式. 由于一些函数可能需要或返回特殊格式的字符串, 所以有时还需要使用下列的字符串格式:

 字符大小C / Win32 类型Encoding
WStr16-位wchar_t*, WCHAR*, LPWSTR, LPCWSTRUTF-16
AStr8-位char*, CHAR*, LPSTR, LPCSTRANSI (系统默认 ANSI 代码页)
Str--TCHAR*, LPTSTR, LPCTSTR相当于 Unicode 构建中的 WStr 或 ANSI 构建中的 AStr

如果 "Str" 或在当前版本中的等价类型用于参数中, 那么字符串或变量的地址被传递给函数, 否则创建一个期望格式的字符串临时副本进行传递. 一般地,“AStr”和“WStr”不应该用于函数会写入值的参数中。

注意: "AStr" 和 "WStr" 用于参数和函数的返回值同样是有效的.

一般而言, 如果脚本通过 DllCall 调用接受字符串参数的函数, 那么必须采取以下其中一种方式:

  1. 如果函数的 Unicode (W) 和 ANSI (A) 都可用, 那么为当前版本调用匹配的那个. 在下面的例子中, "DeleteFile" 在内部被称为 "DeleteFileA" 或 "DeleteFileW". 由于 "DeleteFile" 自身实际并不存在, DllCall 自动根据当前版本尝试适合的 "A" 或 "W".
    DllCall("DeleteFile", "Ptr", &filename)
    DllCall("DeleteFile", "Str", filename)

    在这个例子中, &filename 准确按原样传递字符串地址, 所以函数必须期望接受和 "Str" 类型相同编码格式的字符串. 注意在 AutoHotkey Basic 中 "UInt" 必须使用 "Ptr" 代替, 但得到的代码可能不兼容 64 位.

    注意: 如果无法根据名称准确找到指定的函数, 在 AutoHotkey_L 中不管指定哪个 DLL 都会在函数名称后添加 "A" 或 "W" 后缀查找. 然而, AutoHotkey Basic 则仅为 User32.dll, Kernel32.dll, ComCtl32.dll 或 Gdi32.dll 中的函数添加 "A" 后缀.

  2. 如果函数仅接受特殊类型的字符串作为输入, 那么脚本可能必须使用相应的字符串类型:
    DllCall("DeleteFileA", "AStr", filename)
    DllCall("DeleteFileW", "WStr", filename)
  3. 如果函数必须修改字符串 (非原生格式), 那么脚本必须如 上面 描述的那样分配缓存并且把缓存的地址传递给函数. 如果参数接受输入, 脚本还必须把输入字符串转换为适当的格式; 这可以用 StrPut 实现.

NumPut / NumGet

当使用 NumPut 或 NumGet 操作字符串时, 对于给定类型的字符串其偏移和类型都必须正确. 可以参考下面的代码:

;  8 位/ANSI   字符串:  size_of_char=1  type_of_char="Char"
; 16 位/UTF-16 字符串:  size_of_char=2  type_of_char="UShort"
nth_char := NumGet(var, (n-1)*size_of_char, type_of_char)
NumPut(nth_char, var, (n-1)*size_of_char, type_of_char)

如果 var 含有原生格式的字符串, 那么根据 A_IsUnicode 的值可以确定变量的值.

nth_char := NumGet(var, t_size(n-1), t_char())
NumPut(nth_char, var, t_size(n-1), t_char())

; 为了方便和清晰定义的函数:
t_char() {
    return A_IsUnicode ? "UShort" : "Char"
}
t_size(char_count=1) {
    return A_IsUnicode ? char_count*2 : char_count
}

指针大小

指针在 32 位版本 (包括 AutoHotkey Basic) 中是 4 个字节大小而在 64 位版本中是 8 个字节. 使用结构或 DllCall 的脚本可能需要为在两种平台上正常运行进行考虑. 受影响的特殊地方包括:

  • 含有一个或多个指针的结构字段的偏移计算.
  • 含有一个或多个指针的结构大小计算.
  • DllCall, NumPutNumGet 中使用的类型名称.

对于大小和偏移计算, 使用 A_PtrSize. 对于 DllCall, NumPut 和 NumGet, 使用适当的 Ptr 类型.

记住一个字段的偏移常常是在它之前所有字段的总大小. 同时注意句柄 (包括类似 HWND 和 HBITMAP 的类型) 实际上是指针类型.

/*
  typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;    // Ptr
    HANDLE hThread;
    DWORD  dwProcessId; // UInt (4 字节)
    DWORD  dwThreadId;
  } PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
*/
VarSetCapacity(pi, A_PtrSize*2 + 8) ; Ptr + Ptr + UInt + UInt
DllCall("CreateProcess", <为简短而省略>, "Ptr", &pi, <omitted>)
hProcess    := NumGet(pi, 0)         ; 默认为“Ptr”。
hThread     := NumGet(pi, A_PtrSize) ;
dwProcessId := NumGet(pi, A_PtrSize*2,     "UInt")
dwProcessId := NumGet(pi, A_PtrSize*2 + 4, "UInt")