随着便携设备和高DPI显示器的普及,Windows自身对高DPI的支持也在不断更迭。对应用程序而言,如何与操作系统配合,实现DPI适配以达到最佳的显示效果愈发重要,我们平日常用的应用软件也陆续支持了DPI适配。本文将从概念原理、Windows DPI支持发展开始描述,并给出DPI适配方案的设计与实现(NTAssassin),最后演示实现案例(AlleyWind)效果。
假设在一个19英寸的显示器上显示10像素x20像素的文字。若分辨率为1920x1080尚可正常阅读,但如果这是3840x2160分辨率的4K高分屏,便小到难以看清,相当于只有原来的1/4。此时我们则使用DPI来进行缩放调节,将该文字调整到对应高分辨率下的大小进行显示,如20像素x40像素,获得合适的视觉效果。
DPI(Dots Per Inch,每英寸点数)用于描述图像每英寸中含有多少像素点,故等大的图像,DPI越高,包含的像素点数越多,图像内容越丰富,看起来便越清晰。
在Windows下,屏幕单位长度中有多少像素点由实际屏幕物理大小与分辨率决定,而常说的DPI则成了缩放的参考值。如默认DPI(系统缩放设置为100%)为常量96,系统缩放设置为125%时对应DPI为96x125%=120。系统本身与应用程序均以此DPI作参照,实现缩放。
Win10中缩放相关设置
了解背景以后,看似只要将各长度对应DPI进行等比缩放就能实现适配,比如原本需要绘制100x200单位矩形,在125%缩放比例下绘制125x250单位即可,并且系统已经有内置的缩放机制供默认使用。然而实际情况往往复杂得多,从Windows对DPI支持不断的更迭便可看出,主要有以下问题:
l 系统缩放容易导致应用程序模糊
若使用系统自带的DPI适配(DWM Scaling),应用程序一般无需做任何更改。系统始终告知应用程序DPI设置为默认值96(100%),但应用程序进行显示相关操作时,系统内部将为其自动应用DPI缩放。如同位图缩放,这样的缩放容易导致应用程序显示模糊,尤其是字体。下图是MSDN中对比示意:
MSDN 混合DPI缩放模式下,应用程序/UI框架缩放与系统缩放对比
以下是AlleyWind应用程序系统缩放(模糊)与应用程序适配DPI缩放(不模糊)的对比,系统缩放明显模糊、浑厚,应用程序缩放则显得清晰、锐利:
系统缩放(125%)
应用程序适配DPI(125%)
l 若应用程序自己控制缩放,界面编程繁复
需要应用程序在进行绘制时,自己将所有要绘制的内容全部按DPI进行缩放,并且依赖代码进行计算和控制。如今不论B/S还是C/S,我们都努力将界面与代码抽离开,而不是用代码去实现UI。
l 需要响应DPI变化,实时调节缩放
目前绝大多数支持DPI适配的常用软件,在DPI更改时都能正确调整布局,不需重新启动才生效。随着高DPI显示器的普及,这样的场景不再仅仅局限于用户手动更改系统DPI设置这样少见甚至不必考虑的场合——窗口游走于不同DPI显示器之间也会出现。所以比起在开局一次性缩放,如今更需要设计成能灵活响应DPI变化的模式。
l Windows XP
早在Windows XP,便提供了缩放设置选项供用户设置。应用程序可以通过API获取当前DPI,补足系统缩放的缺陷。
Windows XP中DPI设置
使用GetDeviceCaps获取传入DC的DPI设置,LOGPIXELSX与LOGPIXELSY对应X与Y方向的DPI,目前二者始终相等,默认值(缩放为100%)是96(Windows SDK里定义为USER_DEFAULT_SCREEN_DPI)。
此时,面临上述三个问题,并且系统缩放效果不佳。
l Windows Vista与Windows 7
随着DWM的引入,应用程序的系统缩放由其实现(DWM Scaling),系统缩放效果相较于XP好了很多。并且开始增加相关DPI函数,供应用程序自己实现缩放。
可以通过清单文件或者新增的SetProcessDPIAware函数设置应用程序DPI缩放模式,告诉系统本程序已考虑了DPI缩放问题,不要应用系统缩放。增加IsProcessDPIAware函数以获取此项设置。
Windows 7开始,DPI设置成为了每用户独立的设置。至此,虽然仍面临上述三个问题,但系统缩放效果得以改善,并且应用程序可以选择使用系统缩放还是自己实现。
l Windows 8.1
支持Per-Monitor DPI缩放模式,不同显示器可以拥有不同DPI,新增GetDpiForMonitor函数获取特定显示器的DPI设置。由于多了DPI模式,原SetProcessDPIAware和IsProcessDPIAware的TRUE和FALSE已无法准确指明,并且相关DPI变更消息机制也应运而生。
可以使用清单文件或者新增的SetProcessDpiAwareness函数设置应用程序DPI缩放模式,告诉系统本程序使用Per-Monitor DPI缩放模式。与之前相同,系统不再为应用程序进行缩放,但当DPI变更时,操作系统会通知应用程序,让应用程序适时调整,实现窗口在不同DPI下来去自如。新增的GetProcessDpiAwareness函数可以获取此项设置。
除了新增上述函数支持Per-Monitor模式的设置,还新增了窗口消息以落实该机制。当顶层窗口DPI变更时,如用户修改了DPI设置,或窗口移动到了不同DPI的显示器上,操作系统会为其下发WM_DPICHANGED消息,告诉窗口新的DPI与建议调整的位置和大小。看似提供了近乎保姆级别支持,但该消息只为顶层窗口派发,顶层窗口内部仍需自己进行计算和缩放子窗口。
至此,上述问题中DPI变化的场景得以有机会优雅地面对,多显示器不同DPI的场景需要得以满足。
l Windows 10 1607
DPI缩放模式设置从进程独立变为线程独立——从此同一个进程的不同线程可以拥有不同的DPI缩放模式。看似能让应用程序更自由地控制DPI缩放,但随之而来的又是一对新API——SetThreadDpiAwarenessContext与GetThreadDpiAwarenessContext。此外还新加入了GetAwarenessFromDpiAwarenessContext、GetDpiForWindow、GetDpiForSystem、GetSystemMetricsForDpi、SystemParametersInfoForDpi等函数供应用程序使用,看上去似乎更便捷,但考虑兼容性后应该就不会有这样的错觉了。还增加EnableNonClientDpiScaling函数为顶层窗口提供非客户区DPI缩放支持,这个很快便被即将到来的Per-Monitor v2模式吞并。
此外,微软还为自家的UWP与WPF提供了原生支持。至此,相比Windows 8.1变更也不少,但未必可圈可点。可以结合实际情况考虑,沿用之前的模式。
l Windows 10 1703
随Win10 1703而来的,是Per-Monitor v2 DPI缩放模式,Win32应用程序DPI适配也终于完善。不出意外,又引入了新的DPI缩放模式读写API,SetProcessDpiAwarenessContext与GetProcessDpiAwarenessContext,与1607新增针对线程DPI模式的函数共用相同一套枚举值,函数命名也相似。
相较于Win8.1的Per-Monitor (v1),Per-Monitor v2可谓众望所归:
n WM_DPICHANGED不仅发送给顶层窗口,也发送给其子窗口,还新增WM_DPICHANGED_BEFOREPARENT、WM_DPICHANGED_AFTERPARENT消息满足子窗口时序需要,新增WM_GETDPISCALEDSIZE消息满足非线性缩放需要
n 增加或改进系统对话框、菜单与通用控件(ComCtl32.dll V6.0)对DPI的支持和响应
n 系统自动缩放非客户区,不需调用EnableNonClientDpiScaling
n 主题资源显示自动缩放
有了上述支持,经典Win32应用程序甚至只需在清单文件中声明或一行“SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);”设置DPI缩放模式为Per-Monitor v2,即可完美适配DPI,并响应DPI的变化。因为对话框及其菜单、内部的Win32通用控件都能自动响应并进行DPI缩放,应用程序无需干预。当然也可以使用SetDialogDpiChangeBehavior、SetDialogControlDpiChangeBehavior这样的函数修改默认行为,但一般少有需要,因为实际效果已经很理想。随着Win32通用控件对高DPI的适配,Windows Forms也在1703中提供了有限的支持。
至此,Windows基本完善对高DPI的支持,此版本具有重大意义。从系统层面解决了上述各问题,并满足相关使用场景,应用程序生态环境基本完善。从系统和应用程序角度上而言,通用控件原生支持了DPI适配,直接赋予Win32程序DPI适配的能力,甚至简单到应用程序开发者开启一个开关。UWP、WPF、Windows Forms、Direct2D、Win32等框架支持也顺势健全。
l Windows 10 1803与1809
1703引入全新模式一举完善DPI的支持之后,1803与1809更多的是修订与补充,这里归并到一起简要描述。
1803主要引入新的API让同一线程的不同窗口可以具有不同DPI缩放模式,相关函数有如SetThreadDpiHostingBehavior、GetThreadDpiHostingBehavior、GetWindowDpiHostingBehavior。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-6-25 16:34
被Ratin编辑
,原因: 勘误