# 如何管理窗口

AutoHotkey 可以做的最简单和最有用的事情之一是允许您创建操作窗口的键盘快捷键(热键). 脚本可以激活, 关闭, 最小化, 最大化, 恢复, 隐藏, 显示或移动几乎任何窗口. 这是通过调用适当的 Win 函数来完成的, 通过标题或其他一些条件来指定窗口:

    Run "notepad.exe"
    WinWait "Untitled - Notepad"
    WinActivate "Untitled - Notepad"
    WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2, "Untitled - Notepad"

这个例子应该打开一个新的记事本窗口, 然后移动它来填充主屏幕的一部分(宽度的 ¼, 和高度的 ½). 要了解如何试用, 请参阅如何运行示例代码.

由于不是本篇文档的重点, 我们不会在这里详细讨论用于操作窗口的许多函数. 例如, 要最小化窗口而不是激活它, 请将 `WinActivate` 替换为 `WinMinimize`. 有关可以操作窗口或检索信息的函数列表, 请参阅 Win 函数.

本教程的大部分内容都是关于如何 识别 要在哪个窗口上进行操作, 因为这通常是最麻烦的部分. 例如, 上面的例子有一些问题:

- 窗口的标题没有必要重复.
- 该标题仅适用于 UI 语言设置为英语的系统.
- 它可能会移动一个现有的无标题记事本窗口, 而不是新的.
- 如果由于某种原因没有出现匹配的窗口, 脚本将无限期地停止运行.

在介绍了一些基本知识之后, 我们将逐个解决这些问题.

提示: AutoHotkey 自带一个名为 Window Spy 的脚本, 可以用来确认窗口的标题, 类和进程名. 当仅通过标题识别窗口不可行的时候, 通常会使用类名和进程名. 您可以在脚本的托盘菜单或 AutoHotkey Dash 中找到 Window Spy.

## 标题匹配

在通过标题指定窗口时, 有几件事需要知道:

- 窗口标题始终区分大小写, 除非使用 RegEx 匹配模式和 i) 修饰符.
- 默认情况下, 函数需要窗口标题的子字符串. 例如, "Notepad" 可以同时匹配 "Untitled - Notepad" 和 "C:\A\B.ahk - Notepad2". SetTitleMatchMode 可用于使函数期望前缀, 精确匹配或 RegEx 模式.
- 默认情况下, 隐藏窗口将被忽略(WinShow 除外). DetectHiddenWindows 可以用来改变这一点.

有关详情, 请参阅匹配行为.

## 活动窗口

要引用活动窗口, 请使用字母 "A" 代替窗口标题. 例如, 这将最小化活动窗口:

    WinMinimize "A"

## 最后找到窗口

当 WinWait, WinExist, WinActive, WinWaitActive 或 WinWaitNotActive 找到一个匹配的窗口时, 他们将其设置为最后找到的窗口. 大多数窗口函数允许省略窗口标题(和相关参数), 在这种情况下, 默认为最后找到的窗口. 例如:

    Run "notepad.exe"
    WinWait "Untitled - Notepad"
    WinActivate
    WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2

这样可以避免重复窗口标题, 从而节省了一些时间, 如果需要更改窗口标题, 则可以更容易地更新脚本, 并且可能使代码更易于阅读. 它确保脚本每次都在同一个窗口上操作, 即使有多个匹配的窗口, 或者在 "找到的" 窗口后窗口标题发生了变化, 也使脚本更加可靠. 它还使脚本更有效地执行, 但不是很多.

## 窗口类

窗口类是一组属性, 用作创建窗口的模板. 通常, 窗口类的名称与应用程序或窗口的用途有关. 当窗口存在时, 窗口的类永远不会改变, 因此当通过标题进行标识是不切实际或不可能的时候, 我们经常可以使用它来标识窗口.

例如, 我们可以使用窗口的类, 而不是窗口标题 "Untitled - Notepad", 在这种情况下, 不管 系统语言是什么, 它恰好是 "Notepad":

    Run "notepad.exe"
    WinWait "ahk_class Notepad"
    WinActivate
    WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2

窗口类通过使用单词 "ahk_class" 来区别于标题, 如上面所示. 若要组合多个条件, 请先列出窗口标题. 例如: "Untitled ahk_class Notepad".

相关: ahk_class

## 进程名称/路径

可以由创建它们的进程来识别窗口, 方法是使用单词 "ahk_exe" 后跟进程名称或路径. 例如:

    Run "notepad.exe"
    WinWait "ahk_exe notepad.exe"
    WinActivate
    WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2

相关: ahk_exe

## Process ID (PID)

每个进程都有一个 ID 号, 该 ID 号在进程退出之前保持唯一. 我们可以使用这个来使我们的 Notepad 示例更加可靠, 确保它忽略除新进程创建的记事本窗口外的任何记事本窗口:

    Run "notepad.exe",,, &notepad_pid
    WinWait "ahk_pid " notepad_pid
    WinActivate
    WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2

我们需要连续三个逗号; 其中两个只是跳过 Run 函数的未使用的 WorkingDir 和 Options 参数, 因为我们想要的(OutputVarPID) 是第四个参数.

Ampersand(&) 是 引用操作符. 这用于 通过引用 将 `notepad_pid` 变量传递给 Run 函数(换句话说, 传递变量本身而不是它的值), 允许函数为变量分配一个新值. 然后 `notepad_pid` 成为实际进程 ID 的占位符.

字符串 "ahk_pid " 与 notepad_pid 变量所包含的进程 ID 相连接, 只需将它们按顺序写入, 并用空格分隔. 结果是一个类似 "ahk_pid 1040" 的字符串, 但是这个数字是不可预测的.

如果新进程可能创建多个窗口, 则可以通过用空格分隔窗口标题和其他条件来组合它们. 窗口标题必须始终排在前面. 例如: `"Untitled ahk_class Notepad ahk_pid " notepad_pid`.

相关: ahk_pid

## 窗口 ID(HWND)

每个窗口都有一个唯一的 ID 号, 直到窗口被销毁. 在编程术语中, 这被称为 "窗口句柄" 或 HWND. 尽管不像使用 最后找到的窗口 那样方便, 但窗口的 ID 可以存储在一个变量中, 以便脚本可以通过您选择的名称引用它, 即使标题发生了变化. 一次只能有一个 最后找到的窗口, 但是可以使用尽可能多的窗口 id 来创建变量名(也可以使用) 数组).

窗口 ID 由 WinWait, WinExist 或 WinActive 返回, 也可以来自其他来源. 可以重写记事本示例以利用这一点:

    Run "notepad.exe"
    notepad_id := WinWait("Untitled - Notepad")
    WinActivate notepad_id
    WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2, notepad_id

这将 WinWait 的返回值赋给变量 "notepad_id". 换句话说, 当 WinWait 找到窗口时, 它生成窗口的 ID 作为结果, 然后脚本将这个结果存储在变量中. "notepad_id" 只是我为这个例子创建的一个名称; 您可以使用任何对您有意义的变量名(在某些限制条件下).

注意, 我在窗口标题周围添加了圆括号, 紧跟 着函数名. 在函数调用语句中可以省略括号(也就是说, 函数调用在一行的最开始), 但在这种情况下, 您无法获得函数的返回值.

脚本还可以保留变量 `notepad_id` 以供以后使用, 例如关闭或重新激活窗口或将其移动到其他地方.

相关: ahk_id

## Timeout

默认情况下, WinWait 将无限期地等待匹配的窗口出现. 您可以通过托盘图标打开脚本的主窗口来确定是否发生了这种情况(除非您已禁用它). 默认情况下, 窗口通常打开 ListLines. 如果 WinWait 仍在等待, 它将出现在行列表的最底部, 后面跟着它开始等待的秒数. 除非从"视图"菜单中选择"刷新", 否则数字不会更新.

试着运行这个例子并像上面描述的那样打开主窗口:

    WinWait "Untitled - Notpad"  ; (故意输错)

如果脚本卡住等待窗口, 您通常需要退出或重新加载脚本来解除卡住. 为了防止这种情况发生(或再次发生), 您可以使用 WinWait 的timeout 参数. 例如, 这将等待最多 5 秒的窗口出现:

    Run "notepad.exe",,, &notepad_pid
    if WinWait("ahk_pid " notepad_pid,, 5)
    {
        WinActivate
        WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2
    }

If 语句下面的 block 只有在 WinWait 找到匹配的窗口时才会执行. 如果超时, 则跳过该块, 并在结束大括号之后继续执行(如果它之后有任何代码).

注意, 当我们想在表达式中使用函数的结果时(比如 if 语句的条件) "WinWait" 后面的括号是必需的. 您可以将函数调用本身视为函数结果的替代品. 例如, 如果 WinWait 在超时之前找到一个匹配, 结果是非零. `if 1` 将执行 If 语句下面的块, 而 `if 0` 将跳过它.

另一种书写方法是, 如果等待超时, 则提前返回(换句话说, abort). 例如:

    Run "notepad.exe",,, &notepad_pid
    if !WinWait("ahk_pid " notepad_pid,, 5)
        return
    WinActivate
    WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2

通过应用逻辑非运算符(`!` 或 `not`) 来反转结果. 如果 WinWait 超时, 则结果为 0. `!0` 的结果是 1, 所以当 WinWait 超时时, if 语句执行 `return`.

WinWait 的结果实际上是窗口的 ID(如上所述), 如果超时则为零. 如果你还想通过 ID 引用窗口, 你可以将结果赋值给一个变量, 而不是直接在 If 语句中使用它:

    Run "notepad.exe",,, &notepad_pid
    notepad_id := WinWait("ahk_pid " notepad_pid,, 5)
    if notepad_id
    {
        WinActivate notepad_id
        WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2, notepad_id
    }

为了避免重复变量名, 你可以同时将结果赋值给一个变量, 并检查它是否为非零(true):

    Run "notepad.exe",,, &notepad_pid
    if notepad_id := WinWait("ahk_pid " notepad_pid,, 2)
    {
        WinActivate notepad_id
        WinMove 0, 0, A_ScreenWidth/4, A_ScreenHeight/2, notepad_id
    }

在这种情况下, 注意不要混淆 `:=`(赋值) 和 `=` 或 `==`(比较). 例如, `if myvar := 0` 分配一个新值, 并且每次都给出相同的结果(false), 而 `if myvar = 0` 则将先前分配的值与 0 进行比较.

## 表达式(数学运算等.)

当您想要移动一个窗口时, 相对于它之前的位置或大小移动它或调整它的大小通常是有用的, 这可以通过使用 WinGetPos 函数来检索. 例如, 通过按住 `RCtrl` 并按方向键, 下面的热键可以在每个方向上移动活动窗口 10 个像素:

    >^Left::    MoveActiveWindowBy(-10,   0)
    >^Right::   MoveActiveWindowBy(+10,   0)
    >^Up::      MoveActiveWindowBy(  0, -10)
    >^Down::    MoveActiveWindowBy(  0, +10)

    MoveActiveWindowBy(x, y) {
        WinExist "A"  ; 使活动窗口成为 "上次找到的窗口"
        WinGetPos &current_x, &current_y
        WinMove current_x + x, current_y + y
    }

该示例定义了一个函数来避免代码多次重复. `x` 和 `y` 成为每个热键中指定的两个数字的占位符. WinGetPos 将当前位置存储在 `current_x` 和 `current_y` 中, 然后将其添加到 `x` 和 `y` 中.

像这样的简单表达式应该看起来相当熟悉. 有关详情, 请参阅表达式; 但是要注意, 有很多细节你可能不需要马上学习.

dftg