来源:AutoHotkey Community - Screen scaling, DPI, and making scripts work in different computers

原作者:Descolada。本文为中文移植和扩展整理版,完整覆盖原帖关于 DPI、屏幕缩放、多显示器、跨电脑坐标适配、ImageSearch 和 FindText 跨 DPI 的核心内容;示例代码按本站习惯改写为 AHK v1 风格,并补充 AHK v1 实战建议。原帖基于 AHK v2,若需要对照 v2 原始代码,请查看上方来源链接。

为什么同一个脚本换电脑就失效

很多 AHK 脚本在自己电脑上运行正常,换到别人电脑就点偏、找图失败、窗口移动位置不对。常见原因不是 AHK 写错了,而是目标电脑的屏幕分辨率、缩放比例、DPI、多显示器布局不同。

例如你在 150% 缩放的笔记本上记录了计算器按钮的坐标,然后把脚本放到 100% 缩放的外接显示器上运行,同样的坐标就可能点到另一个位置。找图、找色、PixelSearch、ImageSearch、FindText、MouseMove、Click、WinMove 都可能受影响。

这篇文章主要解决三个问题:

  • DPI、缩放比例和屏幕分辨率到底有什么区别。
  • AHK 脚本为什么在多显示器、不同缩放比例下坐标会错。
  • 如何写出更不依赖 DPI 的坐标、点击、找图和 FindText 脚本。

DPI 和分辨率不是一回事

DPI 是 dots per inch 的缩写,意思是每英寸多少个点。在 Windows 显示设置里,100% 缩放通常对应 96 DPI。150% 缩放对应 144 DPI,200% 缩放对应 192 DPI。

注意:DPI 不是屏幕分辨率,也不等于显示器真实物理像素数量。分辨率描述的是 Windows 用多少像素输出画面,例如 1920x1080;DPI/缩放描述的是同样大小的内容要用多少像素显示。

Windows 主要处理的是“逻辑英寸”,而不是真实物理英寸。很多显示器不会准确报告物理尺寸,投影仪这类设备的物理尺寸还会随着距离变化,所以 Windows 更关心逻辑 DPI。

字体也能帮助理解这个概念:1pt 定义为 1/72 逻辑英寸。一个 72pt 的字体,在 96 DPI 的屏幕上大约是 96 像素高;在 150% 缩放,也就是 144 DPI 的屏幕上,大约就是 144 像素高。

分辨率和 DPI 的区别

分辨率决定画面有多少像素可用。比如物理屏幕是 1920x1080,却把显示分辨率设置成 960x540,那么画面质量会明显下降,因为用来显示内容的像素少了。

DPI/缩放决定同样大小的内容用多少像素来画。DPI 越高,同样一段文字、同样一个按钮会使用更多像素,看起来更清晰,也更适合高分辨率小尺寸屏幕。

原帖举了一个很直观的例子:同样物理尺寸的两块屏幕,一块是 1200x800,另一块是 600x400。如果 1200x800 的屏幕使用 200% 缩放,那么同样大小的界面元素会拥有更高像素密度,看起来更清晰。

另一个例子:一台 14 英寸 1920x1080 的笔记本默认 150% 缩放,也就是 144 DPI。如果改成 1600x900 + 125% 缩放,界面大小可能看起来差不多,因为 1920/150 约等于 1600/125,但画面会更模糊,因为实际输出像素变少了。

DPI Awareness:程序的 DPI 感知模式

Windows 程序并不都用同一种方式处理 DPI。常见有三类:

  • DPI unaware:程序不知道 DPI,认为永远是 100% 缩放。Windows 会帮它拉伸,所以容易变模糊。
  • DPI system-aware:程序启动时按主显示器 DPI 绘制。启动后拖到其它缩放比例的显示器上,Windows 可能继续拉伸。
  • DPI per-monitor aware:程序能感知每个显示器的 DPI,窗口移动到不同显示器时可以重新适配。

AHK 脚本默认通常更接近 system-aware。多显示器缩放不一致时,AHK 内置坐标、窗口移动、鼠标移动就可能和你肉眼看到的位置不一致。

在 AHK v1 中,可以在脚本开头设置当前线程为 per-monitor DPI aware:

#Requires AutoHotkey v1.1

; DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3
; 放在脚本开头,尽量在创建 GUI 或执行坐标操作之前调用。
DllCall("SetThreadDpiAwarenessContext", "Ptr", -3, "Ptr")

这可以改善 WinMove、MouseMove、Click 等函数在多显示器环境下的坐标表现。但它不是万能解法,因为你记录的窗口/客户区坐标本身仍然可能来自某个特定 DPI。

多显示器下 WinMove 为什么会偏

假设主屏是 1920x1080、150% 缩放,也就是 DPI 144;副屏是 3440x1440、100% 缩放,也就是 DPI 96。AHK 的 A_ScreenWidth 和 A_ScreenHeight 通常只反映主显示器分辨率,比如 1920 和 1080。

如果你想把计算器移动到第二块屏幕的 x100 y100,直觉上可能写:

WinMove, Calculator,, % A_ScreenWidth + 100, 100

 

但在不同 DPI 的多显示器上,这个位置可能不对。原帖中用下面这个比例修正后位置才正确:

; 主屏 DPI 144,副屏 DPI 96 时的手动换算思路
WinMove, Calculator,, % A_ScreenWidth * 144 / 96 + 100, 100

这个例子说明:手动换算很快会变复杂,尤其是三块显示器、不同主屏、不同 DPI、动态拖动窗口时。更好的方向是让脚本进入 per-monitor DPI aware,并在需要时获取目标窗口所在显示器的 DPI。

获取指定窗口所在显示器的 DPI

原帖提供了 WinGetDpi 函数,用 MonitorFromWindow 找到窗口所在显示器,再用 Shcore.dll 的 GetDpiForMonitor 获取 DPI。下面是 AHK v1 版本:

WinGetDpi(WinTitle := "A")
{
    hwnd := WinExist(WinTitle)
    if (!hwnd)
        return 96

    ; MONITOR_DEFAULTTONEAREST = 2
    hMonitor := DllCall("MonitorFromWindow", "Ptr", hwnd, "UInt", 2, "Ptr")
    if (!hMonitor)
        return 96

    ; MDT_EFFECTIVE_DPI = 0
    dpiX := 0, dpiY := 0
    hr := DllCall("Shcore.dll\GetDpiForMonitor"
        , "Ptr", hMonitor
        , "Int", 0
        , "UInt*", dpiX
        , "UInt*", dpiY)

    return dpiX ? dpiX : 96
}

如果你的坐标是在 DPI 144 的显示器上记录的,那么在当前窗口所在显示器执行点击时,可以这样换算:

CoordMode, Mouse, Client

baseDpi := 144
targetDpi := WinGetDpi("A")

x := Round(67 * targetDpi / baseDpi)
y := Round(494 * targetDpi / baseDpi)

Click, %x%, %y%

这表示:原始坐标 67,494 是在 144 DPI 下记录的。当前窗口如果在 96 DPI 显示器上,就自动缩小到 96/144;如果仍在 144 DPI 显示器上,则保持不变。

把 DPI 换算封装起来

每次都写 x * 当前DPI / 记录DPI 会很烦,所以可以封装成函数。下面是 AHK v1 简化版 DPI 工具函数,适合日常坐标点击使用。

DpiScale(value, fromDpi, toDpi := "")
{
    if (toDpi = "")
        toDpi := WinGetDpi("A")
    return Round(value * toDpi / fromDpi)
}

DpiClick(x, y, fromDpi := 96, winTitle := "A")
{
    dpi := WinGetDpi(winTitle)
    sx := Round(x * dpi / fromDpi)
    sy := Round(y * dpi / fromDpi)
    Click, %sx%, %sy%
}

DpiMouseMove(x, y, fromDpi := 96, winTitle := "A")
{
    dpi := WinGetDpi(winTitle)
    sx := Round(x * dpi / fromDpi)
    sy := Round(y * dpi / fromDpi)
    MouseMove, %sx%, %sy%
}

使用示例:

; 假设这个坐标是在 144 DPI,也就是 150% 缩放时记录的
CoordMode, Mouse, Client
DpiClick(67, 494, 144, "A")

原帖的 Dpi.ahk 和 WindowSpyDpi 工具思路类似:WindowSpyDpi 自身是 per-monitor aware,并把坐标归一化到标准 DPI;DPI 库再根据当前窗口 DPI 把坐标换算回正确位置。

使用高标准 DPI 减少精度损失

如果从高 DPI 缩放到 96 DPI,坐标会有取整误差。例如 1 个逻辑单位换算后可能不是整数。原帖提到可以使用更大的“标准 DPI”,比如 960,减少精度损失。

简单理解:不要一定把所有坐标都归一到 96,可以归一到更大的基准值。这样中间换算更精细,最后再 Round 到实际像素。

DPI 工具函数会不会变慢

DPI 封装函数肯定比原生 Click、MouseMove 慢,因为它多了获取窗口、获取显示器、计算比例等步骤。但正常自动化里几乎感觉不到,除非你每秒调用几千次。

如果你的脚本里有高频循环,比如游戏循环、图像识别循环、毫秒级鼠标移动,建议先把 DPI 算出来缓存起来,不要每次循环都调用 WinGetDpi。

; 高频循环前先缓存 DPI
targetDpi := WinGetDpi("A")
baseDpi := 144

Loop, 1000
{
    x := Round(67 * targetDpi / baseDpi)
    y := Round(494 * targetDpi / baseDpi)
    ; 这里不要每次都重新 WinGetDpi
}

DPI Awareness 可能影响已有脚本和 GUI

调用 SetThreadDpiAwarenessContext 后,脚本的 DPI 感知方式会改变。它可能让已有硬编码坐标失效,也可能影响 AHK GUI、菜单、ToolTip 的显示大小。

如果你的脚本要创建 GUI,而 GUI 又不想自己处理 DPI,最好在创建 GUI 前谨慎设置 DPI 上下文。原帖也提到,如果启用了 per-monitor DPI aware,ToolTip 和 GUI 可能看起来更小,因为 Windows 不再自动帮你拉伸。

; 有些场景下,创建 GUI 前可以切回 system-aware
; DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2
DllCall("SetThreadDpiAwarenessContext", "Ptr", -2, "Ptr")

Gui, Add, Text,, 这是一个普通 GUI
Gui, Show

跨 DPI 的 ImageSearch

ImageSearch 在不同 DPI 下最容易失效,因为图片会被缩放、抗锯齿、字体渲染、图标资源替换等因素影响。原帖提到可以用 ImageSearch 的 *w 和 *h 参数按当前 DPI 缩放搜索图片,但这并不总是可靠。

基本思路是:如果图片是在 216 DPI,也就是 225% 缩放下截取的,而当前窗口是 144 DPI,就按 144/216 缩放搜索图。

; ImageSearch 跨 DPI 思路示例
; sourceDpi 是截图素材的 DPI,targetDpi 是当前窗口所在显示器 DPI

sourceDpi := 216
targetDpi := WinGetDpi("A")

; 原图尺寸假设为 100x100,按 DPI 比例缩放搜索尺寸
w := Round(100 * targetDpi / sourceDpi)
h := Round(100 * targetDpi / sourceDpi)

CoordMode, Pixel, Screen
ImageSearch, outX, outY, 0, 0, A_ScreenWidth, A_ScreenHeight, *150 *w%w% *h%h% Calculator_icon_225.png

if (ErrorLevel = 0)
    MouseMove, %outX%, %outY%
else
    MsgBox, 没有找到图片。

这里的 *150 是容差示例。实际使用时,跨 DPI 搜索往往需要较高容差,但容差越高也越容易误识别。

为什么 FindText 更适合跨 DPI

原帖也提到,FindText 在跨 DPI 图像搜索时通常更容易、更宽容。我的建议更明确一点:如果你的需求是长期维护的找图找字,优先考虑 FindText;ImageSearch 依赖图片文件,跨 DPI 和跨电脑时更容易失效。

FindText 支持缩放参数,可以按当前 DPI 和素材 DPI 的比例调整。下面是思路示例,具体 FindText 文本请用你的 FindText 工具重新生成。

; FindText 跨 DPI 思路示例
; 需要提前 #Include FindText.ahk

CoordMode, Pixel, Screen

wTitle := "ahk_exe chrome.exe"
WinActivate, %wTitle%
WinWaitActive, %wTitle%
WinGetPos, wX, wY, wW, wH, %wTitle%

sourceDpi := 216
targetDpi := WinGetDpi(wTitle)
zoom := targetDpi / sourceDpi

Text := "|......这里放你的 FindText 特征文本......"

if (ok := FindText(X, Y, wX, wY, wX + wW, wY + wH, 0.3, 0.2, Text,,,,,,,, zoom, zoom))
    FindText().MouseTip(X, Y)
else
    MsgBox, 没有找到目标。

如果你用的是图片文件形式的 FindText,也可以按类似方式设置缩放比例。站内 FindText 相关内容可以参考:

DPI 与 PixelSearch、游戏脚本

原帖后续回复里也有人问到游戏和 PixelSearch。这里补充一下本站角度的判断:如果只是分辨率不同,但游戏内 UI 没有按 Windows 缩放变化,DPI 可能不是主要问题;如果 Windows 缩放、游戏分辨率、窗口模式、无边框窗口、UI 缩放都在变,那坐标和搜索区域就必须重新设计。

游戏脚本里常见的稳定路线是:

  • 优先固定游戏分辨率和窗口模式,减少变量。
  • 坐标按窗口客户区比例保存,而不是按屏幕绝对坐标保存。
  • PixelSearch 区域也按窗口宽高比例计算。
  • 找图需求优先用 FindText 或 WinCapture + FindText。
  • 如果 UI 本身会缩放,素材也要按缩放比例处理。

例如把坐标保存为客户区比例,而不是固定像素:

; 把点击点保存为窗口客户区比例
; 假设目标按钮在客户区宽度 30%、高度 70% 的位置

WinGetPos, wx, wy, ww, wh, A

x := Round(ww * 0.30)
y := Round(wh * 0.70)

CoordMode, Mouse, Client
Click, %x%, %y%

这种写法不一定解决所有 DPI 问题,但对“不同分辨率同布局”的窗口很有用。

DPI 和缩放比例速查表

DPI Scale Windows 缩放
96 1 100%
120 1.25 125%
144 1.5 150%
168 1.75 175%
192 2 200%
216 2.25 225%
240 2.5 250%
288 3 300%
336 3.5 350%

获取窗口 DPI Awareness

如果你想知道目标窗口本身是 DPI unaware、system-aware 还是 per-monitor aware,可以用 GetWindowDpiAwarenessContext 和 GetAwarenessFromDpiAwarenessContext。

GetWindowDpiAwareness(WinTitle := "A")
{
    hwnd := WinExist(WinTitle)
    if (!hwnd)
        return ""

    ctx := DllCall("GetWindowDpiAwarenessContext", "Ptr", hwnd, "Ptr")
    awareness := DllCall("GetAwarenessFromDpiAwarenessContext", "Ptr", ctx, "Int")

    ; 0 = DPI unaware
    ; 1 = system-aware
    ; 2 = per-monitor aware
    return awareness
}

aw := GetWindowDpiAwareness("A")
MsgBox, 当前窗口 DPI Awareness:%aw%

这对截图识别很有帮助。DPI unaware 窗口可能由 Windows 拉伸,GetDCEx 或 PrintWindow 获取到的可能是原始 DPI;system-aware 窗口取决于程序启动时的 DPI;per-monitor aware 程序可能会动态根据显示器 DPI 重新绘制。

总结

DPI 问题的核心不是“坐标乘一个固定比例”这么简单。你需要先判断:目标窗口在哪个显示器上,窗口所在显示器 DPI 是多少,坐标是在哪个 DPI 下记录的,目标程序自己是否 DPI aware,窗口布局是否会随大小变化。

日常 AHK v1 自动化可以按这个顺序处理:

  1. 尽量避免屏幕绝对坐标,优先用窗口客户区坐标。
  2. 能用控件、UIA、浏览器控制,就不要依赖坐标。
  3. 必须坐标点击时,记录坐标对应的 DPI,并按当前窗口 DPI 换算。
  4. 找图优先使用 FindText,ImageSearch 跨 DPI 时要谨慎。
  5. 多显示器环境下,尽量启用 per-monitor DPI aware,并测试每块屏幕。

如果你只是自己电脑用,硬编码坐标也许够了;但如果脚本要发给别人、换电脑、换显示器、换缩放比例,DPI 适配就必须提前考虑。

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