来源: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 自动化可以按这个顺序处理:
- 尽量避免屏幕绝对坐标,优先用窗口客户区坐标。
- 能用控件、UIA、浏览器控制,就不要依赖坐标。
- 必须坐标点击时,记录坐标对应的 DPI,并按当前窗口 DPI 换算。
- 找图优先使用 FindText,ImageSearch 跨 DPI 时要谨慎。
- 多显示器环境下,尽量启用 per-monitor DPI aware,并测试每块屏幕。
如果你只是自己电脑用,硬编码坐标也许够了;但如果脚本要发给别人、换电脑、换显示器、换缩放比例,DPI 适配就必须提前考虑。

评论(0)