一、背景
随着近些年屏幕设备的不断发展,各种显示设备的分辨率也越来越高,在尺寸保持基本不变的情况下,分辨率越高,设备的DPI也越高,清晰度也就越高。高DPI的设备给我们提供了更精细的画质,然而Windows上的大多数应用并没有适配高DPI的显示器,导致应用在这些设备显示模糊,体验非常差。
为了让应用在高DPI的设备上依然显示清晰,我们就需要对高DPI的设备进行适配。
二、基础概念
2.1 DPI是什么
DPI是Dots Per Inch的缩写,表示显示设备在每英寸上有多少个像素点。在开发过程中,我们会接触到两个DPI的概念:面板DPI和OS DPI。
面板DPI是设备真实的DPI,是一个固定的物理属性。
OS DPI是操作系统根据面板DPI校准后的一个标准化的DPI值。适配过程中所使用的DPI都是指的OS DPI。Windows平台中标准的OS DPI值一般有96 、120、144、192这四个值,用户可以在系统的控制面板里进行修改。
2.2 DPI和缩放比例的关系
现在我们已经知道DPI是个什么了,那么DPI又是怎样影响我们应用程序的界面的呢?
假设现在有两台显示器,显示器A:22寸,分辨率1920*1080,DPI=100。显示器B:22寸,分辨率3840*2160,DPI=200。显示器B的DPI是显示器A的2倍。现在显示一张分辨率为1920*1080的图片,在显示器A上,得到的效果如下:
图片是刚好撑满了整个显示器。
在显示器B上,得到的效果如下:
同样的图片,在显示器B上只占用了1/4面积的屏幕。
B的DPI是A的2倍,在默认处理下,实际的显示效果是:图片在A上显示的宽和高分别是B上的2倍。这个时候用户肯定不开心了,选择分辨率更高(通常也是DPI更高)的设备是为了显示更细腻而现在却是显示的内容越来越小。为了让高DPI上实现更好的显示效果。这个时候就我们在需要在高DPI的设备上放大我们的界面元素,在低DPI设备上缩小我们的界面元素,使用户在不同的设备上看到的内容的大小基本一致。
上面例子中我们是使用面板DPI来举的例子。面板DPI是不变的,OS DPI是可以通过设置改变的,并且应用中获取到的DPI是OS DPI。那么用户就可以通过改变OS DPI来实现缩放系统界面元素和应用界面。
Windows以DPI为96作为100%大小的基准。提供了120、144、192四个标准的DPI值。其各自对应了一个缩放比例。
2.3 有效分辨率
对于一个显示器,其物理分辨率和面板DPI是固定的,OS DPI决定了界面的缩放比例。DPI增大,应用界面是增大的,相反显示器的逻辑分辨率是在减小的。例如,3840*1920 DPI=192的显示器,对于应用而言,屏幕上的两个像素点变成了原来的一个像素点,所以其分辨率相当于变成了1920*1080,这个分辨率就称之为有效分辨率。
2.4 DPI Virtualization
对于Windows Vista之后的系统,当Windows检测到的程序不支持DPI缩放时(即非DPI Aware的程序),DWM会直接放大窗口。虚拟化之后的应用由于直接对像素进行缩放,所以界面会变得模糊。
2.5 应用DPI感知级别
前面提到了DWM会使用虚拟化技术来默认处理那些为适配DPI的应用,那么系统怎么判断这个应用有没有适配呢?系统是通过应用设置的DPI感知级别来判断应用是否适配了DPI。
三、适配步骤
了解了适配相关的基础概念之后,接下来开始对应用进行适配了。
在未对DPI进行适配之前,窗口、控件构建的大致过程如下:
适配的主要工作包括资源的适配和尺寸调整两个方面,调整之后的流程如下:
3.1 声明DPI感知级别
适配应用,首先我们需要声明我们的应用是DPI感知级别的。声明DPI感知有两种方法,一种方法是通过Manifest进行声明,
<assembly ...>
<asmv3:application>
<asmv3:windowsSettings ...>
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
通过设置dpiAware为true就声明应用支持DPI缩放。如果想要支持Per Monitor模式的DPI缩放,则需要设置dpiAware值为true/PM。这种方式是官方推荐的方式。需要注意的是,如果程序是一个DLL,则Manifest中的设置会被忽略。
另一种方式是通过SetProcessDPIAware函数进行设置,官方不推荐使用此方式,在两种特殊情况下可以优先考虑这种方式:1.应用本身是一个DLL。2.应用需要按照OS支持不同级别的适配。一般来说,优先考虑使用第一种方式。
3.2 确定DPI缩放因子
在控件和窗口创建之前需要先确定DPI缩放因子。
3.2.1 GetDeviceCaps
通过GetDeviceCaps可以获取到水平和垂直方向的DPI值,得到了DPI值,还需要明确支持的DPI缩放范围。Windows中,目前有96、120、144、192四种标准的DPI,当然用户也可以设置为其它DPI大小。实际适配时,支持96、120、144、192四种标准DPI即可,其它DPI则可以取相近的标准DPI。最后根据DPI就可以确定水平和垂直方向的缩放因子。
3.2.2 GetSystemMetrics
通过GetSystemMetrics可以得到显示器的分辨率,再根据前面得到的DPI值,就可以确定当前的有有效分辨率。如果有效分辨率过低(Windows推荐的有效分辨率不低于1024*720),可能会导致界面显示不全,这个时候就需要适当的缩小缩放因子或者提示用户。
3.3 资源适配
一般来说,DPI不同,界面的大小不同,需要的资源也就不同。
3.3.1 资源目录
每一种DPI都有一个对应的资源目录,资源在这些目录下采用相同的相对路径。例如一张test.png图片分辨适配1x(96 DPI)和1.5x(144 DPI)时,其对应的目录可以是res/xxxx/test.png和res@1.5x/xxxx/test.png。
3.3.2 图片资源
对于图片资源,优先使用对应目录下的资源。如果在对应目录下没有找到资源,则可以使用最其他DPI目录下的资源。使用其他DPI目录的图片资源时,在使用之前还需要先进行缩放处理,缩放的比例由相关的两个DPI的比值决定,以查找120 DPI下test.png为例。具体流程如下:
3.3.3 xml等构建UI的资源
一般来说,除非想在不同的DPI、有效分辨率下采用不同的布局方式,建议只使用一套xml资源。这样可以减少适配的工作量,同时避免维护多套xml带来的麻烦。
3.4 窗口和控件缩放
窗口和控件的缩放是DPI适配的主要工作。
3.4.1 xml适配
Windows版微信中,窗口和控件构建支持使用xml进行配置,对于xml构建的窗口和布局适配起来比较简单,工作量也比较小,只需要在读取xml的时候直接与缩放因子相乘就行了。为了使xml更灵活,xml中最好支持使用不同的单位进行配置。类似px,pt,dp这样的单位。
3.4.2 代码适配
对于代码中构建的窗口和布局,适配的工作量就要大一些了。这一部分主要工作就是将原先代码中动态设计算的尺寸与缩放因子相乘得到缩放后的尺寸。
对于代码中使用的常量尺寸,在定义的时候可以考虑#define而不是const常量。例如对于一个窗口宽度WND_WIDTH
#define WND_WIDTH 100 * dpi_x
而不是用
const int WND_WIDTH = 100 * dpi_x;
这样做的优点是如果应用考虑做到Per-Monitor级别的适配或者需要动态切换缩放比例时,这部分的代码可以直接使用而不需要再做修改了。
3.4.3 字体适配
Windows微信使用CreateFontIndirect创建字体,在创建的时候,只需要对lfHeight进行缩放就可以了。
3.4 测试
在开发的时候,一般都需要及时观察到适配的效果,这里简单的做法就是强制设定缩放因子为我们的需要测试的值就可以了。但是对于最终的显示效果还需要通过更改系统设置的缩放比例后观察的效果为准。
四、总结
对于Windows平台而言,未来高清设备一定会越来越多,所以适配DPI变得越来越重要。通过上面的步骤,能够满足大多数应用对于DPI适配的需求,但对于需要做到Per-Monitor级别的适配的应用,就还需要更多的工作以完成适配。
五、参考文档
https://msdn.microsoft.com/en-us/library/dn469266(v=vs.85).aspx#pixelated_text