基于传统技术开发的 Windows 桌面应用,在高分辨率的显示设备上表现得“惨不忍睹”。随着高分辨率显示设备的普及,所有桌面应用程序的开发人员,都需要关注自己的软件在不同的 DPI 上的表现。
1 应用程序感知 DPI 变化
在 Windows 2000 之前,大部分大部分开发人员对显示器分辨率的关注点是如何让自己的程序在低分辨率的显示器上表现正常,因为过低的分辨率会导致窗口界面显示不完整。随着垂直分辨率低于 768 的显示设备逐步被淘汰,为 Windows XP 和 Windows 7 开发软件的程序员在很长一段时间都不需要考虑显示器的分辨率问题。但是近几年,高分辨率显示器开始迅速普及,Windows 8 或 Windows 10 的桌面程序需要再次应对高分辨率显示设备的挑战。
1.1 DPI 感知的类型
为了让应用程序在不同 DPI (Dots-Per-Inchs)显示设备上都表现正常,需要感知显示设备的分辨率变化。随着 Windows 的发展,对 DPI 感知也经历了一系列技术更新。从 DPI 无感知,到 Windows 8.1 开始支持 Per-Monitor,再到 Windows 10 开始支持的 Per-Monitor V2,应用程序感知 DPI 变化,并动态调整窗口显示的方法也越来越简单。
1.1.1 DPI 无感知
传统的 Windows 总是以 96 DPI 显示窗口系统,此时系统的显示缩放比例就是 100%。当用户调整显示缩放比例的时候,实际上调整得是显示器的分辨率(DPI),比如缩放比例 125% 对应的是 120 DPI,150% 缩放比例对应的是 144 DPI,200% 对应的 DPI 是 192。对 DPI 无感知的应用程序,在高 DPI 的情况下会显示一个非常小的窗口,有些情况下会小到看不清楚窗口内容。Windows 系统在高 DPI 的时候,会提示优化应用程序的显示效果,对 DPI 无感知的程序来说,这种“优化”就是用拉伸的方式放大窗口内容。但是这种放大是基于光栅位图的缩放,不是基于矢量技术的缩放,通常会导致窗口显示模糊,尤其是文字的边缘轮廓模糊,人眼看起来非常不舒服。
1.1.2 Per-Monitor
从 Windows 8.1 开始,操作系统为应用程序增加了一种感知系统 DPI 变化的能力,就是 Per-Monitor。当显示设备的 DPI 发生变化的时候,对于使用了 Per-Monitor 技术的应用程序,系统不再做窗口显示的拉伸放大,而是向程序的顶层窗口发送 WM _ DPICHANGED 消息,让应用程序根据变化调整自己。
Per-Monitor 技术的限制性主要是开发人员的应用不方便,顶层窗口在收到 WM _ DPICHANGED 消息的时候,不仅要负责计算所有的子窗口的位置,还需要在窗口创建时的 WM NCCREATE 消息处理中调用 `EnableNonClientDpiScaling` 这个 API,让系统帮忙处理非客户去的正确缩放。
1.1.3 Per-Monitor V2
从 Windows 10 1703 开始,操作系统开始支持 Per-Monitor V2 级别的感知,它比 Per-Monitor 具有更多的感知模式,比如在不同显示分辨率的两个显示器之间拖动窗口的时候,也能收到 WM _ DPICHANGED 消息。另外,Per-Monitor V2 不仅向顶层窗口发送 WM _ DPICHANGED 消息,还向所有的子窗口发送 WM _ DPICHANGED 消息,这就大大减少了主窗口控件调整的复杂度。除此之外,Per-Monitor V2 还自动处理非客户去的正确缩放,对公用对话框(比如文件选择对话框,颜色对话框)也做了正确的缩放处理。
1.2 让应用程序感知 DPI 变化
1.2.1 API 调用
让桌面应用程序支持 DPI 变化有两种方法,一种是采用编程方式在程序初始化的时候调用某个 API 告知操作系统本程序的 DPI 感知能力,另一种是使用程序清单文件(manifest),本节介绍第一种方法。有这种功能的 API 有两个,功能上是等效的。第一个是 `SetProcessDpiAwareness` ,通过 `value` 参数通知操作系统本程序的 DPI 感知级别。使用这个 API 需要包含 shellscalingapi.h 头文件,并导入 Shcore.lib 库,其原型如下:
HRESULT SetProcessDpiAwareness(PROCESS_DPI_AWARENESS value);
`PROCESS_DPI_AWARENESS` 有三个值可选: