关于Windows高DPI的一些简单总结

时间:2021-03-06 14:22:59
原文地址:http://www.cppblog.com/weiym/archive/2014/02/18/205841.aspx

      我们知道,关于高DPI的支持, Windows XP时代就开始有了, 那时关于高DPI的支持比较简单, 但是从Vista/Win7 到现在Win8 /Win8.1, Windows关于高DPI的支持已经发生了很大的变化, 下面我们依次简单介绍下。
      如果说以前XP时代我们还有理由不关注高DPI,  那么在移动设备时代和大显示器的高分辨率时代, 我们就没有理由不关注高DPI了, 比如Surface Pro的分辨率是1920x1080, 这种情况下如果系统我们不设置高DPI, 基本上就没法触摸和操作了,所以现在普通程序对高DPI的支持已经成为趋势了。
      什么DPI? 全称是dots per inch (DPI), 也就是每英寸的点数,在显示器上就是每英寸的像素个数,Window上一般默认是96 dpi 作为100% 的缩放比率, 但是要注意的是该值未必是真正的显示器物理值, 只是Windows里我们的一个参考标准。
      下面我们思考为什么DPI设置高了之后, 我们看到的字体会变大?因为系统字体是是以固定大小(宋体10号字,物理尺寸为(10/72)英寸)设计的, 当我们DPI设置高了之后 ,说明该字体要占有更多的像素(DPI是每英寸的点数,在显示器上就是每英寸的像素个数,DPI值变大,每英寸的像素个数变大,而字体单位是英寸,是不变的,英寸换算成像素后就会变大), 在屏幕分辨率不变的前提下, 看起来也就大了。所以如果我们设置高DPI,通常也意味着我们的显示器是高分辨率, 里面的字体看起来太小了, 我们需要提高DPI来把内容放大。
      那么我们的程序如何才能支持高DPI? 对于高DPI的支持, 不同操作系统有不同的方案。通常来说如果我们程序支持高DPI, 意味着我们要对绘画的内容进行相应的放大, 比如字体,图片和控件等。当然, 如果我们用的是系统字体(比如GetStockObject(DEFAULT_GUI_FONT)), 那么这种情况下我们不用操心, 因为系统会对该字体在高DPI时进行相应的放大; 如果我们是用CreateFont自己创建的字体, 那就要我们自己对该字体进行放大了。

下面我们看XP是如何对高DPI进行支持的?
      XP对高DPI的支持比较差劲, 大部分情况下就是字体的放大, 当然我们程序也可以通过GetDeviceCaps(hDC, LOGPIXELSX)获取DPI后自己对绘画的内容进行缩放。
关于Windows高DPI的一些简单总结

下面我们看Vista/Win7/Win8是如何对高DPI进行支持的?
      我们知道Vista/Win7我们可以禁止DWM(Desktop Window Manager), 该模式我们称之为Basic模式, 这种模式下的高DPI效果和XP一样。
      对于DWM没有禁掉的情况, Vista/Win7/Win8 对高DPI的支持又分为2种情况, 具体看下图: 
关于Windows高DPI的一些简单总结

      一种XP风格的高DPi支持, 这种方式我们上面讨论过了;
      还有一种是通过 DWM 虚拟化支持的 高DPI方式, 下面我们讨论下该方式:
      该种方式的高DPI支持是通过DWM的缩放实现的, 具体过程是这样的, 比如我们当前系统的DPI是200%, 我们程序运行时,系统会告诉你当前DPI仍然是96(100%), 所以我们程序会仍然按照100%的方式进行绘画, 但是但是系统给我们的坐标是根据DPI缩小过后(当前缩放200%但获取的还是100%的尺寸?)的(也就是我们对窗口调用GetWindowRect或是通过GetSystemMetrics(SM_CXSCREEN)得到的大小会比实际大小(在桌面上实际看到的窗口大小?)减半) , 当我们画完之后, DWM再对整个窗口进行200% 放大后画到屏幕上, 这样看起来我们的程序就自动支持高DPI了。
      这种方式看起来很美妙, 但是它也有缺点, 主要是经过缩放后的内容看起来会变模糊, 比如文字会有明显的锯齿。
      既然DWM虚拟化用户效果有时不是那么好, 那么我们很多时候可能会自己支持高DPI, 如何让我们的程序禁用该效果?事实上我们可以对每个进程对DWM虚拟化的支持进行设置和查询, 系统给我们提供了2个APi: SetProcessDPIAware  IsProcessDPIAware , 通过调用SetProcessDPIAware , 我们告诉系统不要对我们的程序进行DWM虚拟化。
      这里还有特殊情况也提一下: 我们在高DPI下通过窗口句柄取到的坐标信息是和目标程序是否支持DWM虚拟化相关联的, 我们对支持DWM虚拟化的程序窗口调用GetWindowRect, 取到的坐标也是经过DWM缩放后的坐标; 对禁用DWM虚拟化程序的窗口调用GetWindowRect, 取到的坐标则是没有经过缩放的原始坐标。
      最后我们再讨论下Win8.1 对高DPI的支持, WIn8.1对高DPi以3种方式支持 Process_DPI_Awareness : 
typedef enum _Process_DPI_Awareness { 
  Process_DPI_Unaware            = 0,
  Process_System_DPI_Aware       = 1,
  Process_Per_Monitor_DPI_Aware  = 2
} Process_DPI_Awareness;

下面我们依次讨论这3种方式:
      第一种Unaware, 该种方式是告诉系统, 我的程序不支持DPI aware, 请通过DWM虚拟化帮我们实现。 该方式和上面Win7/Win8对高DPI的支持的实现基本一样,主要区别是它通过GetWindowRect取到的坐标都是经过DWM缩放后的, 无论对方窗口是不是支持DWM虚拟化。
      第二种方式是System DPI aware, 该方式下告诉系统, 我的程序会在启动的显示器上自己支持DPI aware, 所以不需要对我进行DWM 虚拟化。 但是当我的程序被拖动到其他DPI不一样的显示器时, 请对我们先进行system DWM虚拟化缩放。
     第三种方式是Per Monitor DPI aware, 该方式是告诉系统, 请永远不要对我进行DWM虚拟化,我会自己针对不同的Monitor的DPi缩放比率进行缩放。

再介绍下相关API:
SetProcessDpiAwareness :设置当前进程对高DPi的支持方式
GetProcessDpiAwareness :查询某个进程对高DPI的支持方式
GetDpiForMonitor : 获取某个Monitor的DPI
WM_DPICHANGED :当某个程序窗口被拖到另外一个DPI的Monitor时收到

最后,简单总结下, 从上面我们可以看到微软在不同操作系统上对高DPI支持的改进线路,很多方面也体现了他们对老程序兼容性上的考虑, DWM虚拟化虽然很简单, 却丢失了用户体验。  

PS, 我在我机器上测试发现,桌面程序基本上只有微软自己的程序能做到在高DPI下完美支持, 其他大部分程序(即使如Chrome)也是通过DWM虚拟化实现的高DPI支持。当然现在WPF和Window store App基本上都是内置支持高DPI的。

统计下, 你们的程序支持高DPI吗?


-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
最后附上解决截图中窗口套索的思路:

1、禁止DWM缩放的飞秋

不管什么缩放比例,窗口大小始终不变,使用API Monitor可以探测到,飞秋在启动时调用了SetProcessDPIAware函数,告诉系统自己感知dpi,不需要系统进行DWM缩放。所以飞秋的窗口始终是100%显示比例时的窗口尺寸。

(1)飞秋套索系统窗口和QQ窗口是没问题的,后来通过API Monitor探测验证,系统窗口和QQ窗口都设置了DPIAware,即自己感知dpi,自己实现缩放,不需要系统进行DWM缩放。--》系统窗口和QQ窗口没有使用系统DWM缩放,使用GetWindowRect获取的他们的窗口坐标是当前看到的窗口原始坐标,即当前屏幕上看到的窗口大小,所以这些窗口套索没问题的。(自己禁用DWM缩放,不需要对禁用DWM缩放的程序窗口进行补偿)

(2)飞秋套索XX(我们的软件就叫XX吧)和imo窗口是有问题的,XX和imo窗口也有共同点,都没设置了DPIAware,即系统自动进行了DWM缩放。经观察,飞秋的套索框相对于XX和Imo窗口的实际大小要小。--》XX和imo窗口被系统进行了DWM缩放,对他们的窗口调用GetWindowRect比当前的显示比例下看到的窗口大小缩小了,所以要对XX和imo窗口的rect要进行放大补偿。(自己禁用DWM缩放,需要对使用了DWM缩放的程序窗口进行补偿)--》因为飞秋设置了DPAAware,指明不需要对飞秋的所有窗口进行DWM缩放,所以飞秋在截图模块绘制的套索窗口,没有进行放大,就是我们在屏幕上看到的大小。

2、禁止DWM缩放的QQ,在右键属性中勾选或者取消勾选“高DPI设置时禁用系统缩放”,对QQ没有任何影响,窗口都是放大的。使用API Monitor监测到,QQ在启动时也是调用了SetProcessDPIAware函数,将自己设置为自己感知DPI,不需要系统进行DWM缩放,QQ自己做了缩放。系统窗口也是类似的,也是自己做了对于DPI的缩放。

3、进行DWM缩放的XX

XX程序中没有做针对不同显示比例下的缩放,在显示比例大于100%时,由系统进行DWM索昂。经打印得知,在DWM缩放状态下,XX窗口中的WM_LBUTTONDOWN等消息中携带的光标坐标为当前缩放比下的DWM坐标,不是100%缩放比时的坐标。

(1)XX套索XX自己和imo,是没有问题的。XX和imo有个共同点,都是系统进行的DWM缩放,在调用GetWindowRect获取XX和imo窗口时,由于他们都进行了DWM缩放,所以获取的是DWM缩放后的坐标。我们在套索窗口时,使用Get来的坐标绘制(100%),系统会帮们放大2倍到200%显示到屏幕上(自己使用了DWM缩放,不需要对使用了DWM缩放的程序窗口进行补偿)

(2)XX套索QQ和系统窗口时有问题的,QQ和系统窗口有个共同点,都是自己实现缩放的,系统没有进行DWM缩放,所以在调用GetWindowRect获取QQ和系统窗口时,获取的是没有经过DWM缩放的原始坐标,是我们在屏幕上看到的窗口大小(已经放大后的大小)。用这个原始尺寸在我们的截图模块中去绘制套索窗口,因为我们XX的窗口使用了DWM缩放,会对绘制的进行DWM放大,相当于放大了2x2=4倍,所以补偿的方法是将目标窗口的坐标缩小1/2,这样经过系统DWM放大后,就是1倍的大小,就是我们看到的已经放大的窗口大小了。(自己使用了DWM缩放,需要对禁用了DWM缩放的程序窗口进行补偿)

4、最后发散一下,QQ为什么能做到完美兼容呢?(猜测其中的原因)

QQ是大厂商,估计是和微软有技术合作,对于win7以上系统的新的特性做了新的处理。QQ对windows的新的特性也不了解,应该是出现问题,告诉微软,让微软给出解决方案。比如通过API Monitor抓到QQ调用了DwmGetWindowAttribute函数,这个函数MSDN上都没有详细的说明,QQ是怎么知道这个函数的呢?当时探测时,将左边的所有函数都选上了,一部分一部分函数试,才知道QQ调用了DwmGetWindowAttribute函数。DwmGetWindowAttribute获取的是100%时的窗口尺寸,GetWindowRect获取的是经过DWM缩放后的窗口尺寸。

QQ是我们做im软件的标杆,正所谓一直被模仿,从未被超越!

有对比才有进步,有参考才有思路!