理解windows DPI 以及开发过程中的迷惑问题

一、DPI定义

DPI全称是dots per inch, 也就是每英寸的点数,在显示器上就是每英寸的像素个数,Window上一般默认是96 dpi 作为100% 的缩放比率, 但是要注意的是该值未必是真正的显示器物理值, 只是Windows里我们的一个参考标准。

二、为什么设置DPI会使我们看到的程序变大

简单来说我们可以直接理解为系统的机制,我们暂时不讨论程序对DPI的适配,只讨论系统在设置125%以后程序会放大的原理,如果打个比方的话就像系统使用个放大镜一样让我们的程序看起来变大了,但是实际上程序还是那么大,只不过视觉上看起来按照设置的DPI比例放大了而已。

如果讨论程序对DPI的适配,也就是说,程序感知到了桌面的dpi变为125%,那么程序在感知到桌面的dpi变化之后,程序把自身大小调成125%的大小,这个改变是与系统无关的,程序只是感知到系统DPI的变化,从而自己按照系统DPI去适配自己程序放大后的效果。这个是程序级别的放大,所以如果程序本身对系统DPI有所感知,我们看到的125%分辨率下的程序就不是简单的模糊放大,可能是实际上的像素点变多(也有程序感知到系统DPI为125%以后却还保持自己的程序界面为100%的大小,例如生死狙击桌面版本)。

举个例子理解,在Win10系统下我们原生DPI100%,然后去量一个窗口的大小为100*100,如果此时我们把DPI设置为200%,我们会发现视觉上窗口变大了两倍,但是实际上窗口细节是模糊的,这就是因为系统使用了DWM Desktop Window  Manager)虚拟化机制去帮助程序适配高分辨率。

DPI比例与每英寸点数对应关系:

 

屏幕比例

每英寸点数

100%

96

125%

120

150%

144

175

192

 

注:以上的每英寸点数为代码中关于DPI问题经常使用到的数字

 

三、不同系统下的DWM机制

对于系统dpi的改善,可以分为三个时间段

1. WindowsXP阶段:

对高DPI的支持比较差劲, 大部分情况下就是字体的放大, 当然我们程序也可以通过GetDeviceCaps(hDC, LOGPIXELSX)获取DPI后自己对绘画的内容进行缩放。因为现在已经用不到了,不作过多讨论。

2.Windows Vista/Windows 7/Windows 8阶段:

Windows Vista / 7 / 8 中,操作系统提供了真正的 DPI 的设置:

Windows 7 中还额外提供了传统 Windows XP 风格 DPI 缩放比例的选项(此选项在 Windows 8 之后就删掉了),这也是在修改 DPI 值,只不过可以选择非1/4 整数倍的 DPI 值。

以上系统下我们可以禁止DWM, 该模式我们称之为Basic模式, 这种模式下的DPI效果和XP一样。如何禁止:在服务管理器中找Desktop Window Manager Session Manager 这个选项就可以禁止但是在不禁止DWM模式,我们就可以看到以上已经讨论过的放大镜效果了

 3.Windows 8.1以后阶段

Windows 10 中的多个屏幕选择

▲ Windows 10 中的多个屏幕选择

Windows 10 中针对每个屏幕的 DPI 设置

▲ Windows 10 中针对每个屏幕的 DPI 设置

 

如果用户在设置中更改了系统 DPI 值或屏幕 DPI 值,那么 Windows 系统会提示需要注销才会应用修改

对于 Windows 8.1 以下的系统,注销是必要的。因为系统 DPI 值如果不注销就不会改变,应用需要在系统重新登录后有了新的 DPI 值时才会正常根据新的系统 DPI 值进行渲染。否则就是系统进行的位图缩放。

对于 Windows 8.1 及以上的系统,注销通常也是必要的。虽然屏幕 DPI 值已经更新,并且已向应用窗口发送了 Dpi Change 消息,但系统 DPI 值依然没变。应用必须处理 Dpi Change 消息才会正常渲染。如果应用不支持屏幕 DPI 感知(后边讨论),那么使用的就是系统 DPI 值,于是一样的会被系统进行位图缩放(放大镜)。

 

但事情到 Windows 10 (1803)  之后,事情又有了转机。现在,你可以通过在设置中打开一个开关,使得无需注销,只要重新打开应用即可让此应用获取到最新的系统 DPI 的值。

Windows 10 (1803) 中新增的不模糊设置项

方法是:打开设置” ­> “系统” ­> “显示器” ­> “高级缩放设置,在高级缩放设置上,打开允许 Windows 尝试修复应用,使其不模糊。额外的,对于 Windows 8.1 及以上的系统,系统 DPI 值等于主屏在系统启动时的屏幕 DPI 值。

四、对 Windows 应用而言的 DPI 感知级别(Dpi Awareness

上面已经说过系统对于DPI的操作,我们再讨论一下应用程序,对于系统dpi改变后的适配方式。现在Windows 的程序 DPI 感知级别经过历代升级,主要是有四种级别。

 

1.DPI Unaware

顾名思义,就是程序对系统的DPI不进行感知,对于应用程序本身,永远是96DPI,但是在系统级别的DPI改变时,系统直接对该感知级别的程序进行DWM放大,就是视觉上模糊的位图扩大,坐标位置还是不变。这是操作系统进行的操作。简单理解就是应用程序告诉系统,本程序对于DPI改变没有适配,你操作系统帮我给用户适配一下吧。这种感知级别的程序对于调用系统的API获取系统的DPI时,获取到的永远都是100%,而且调用相关的系统API时例如GetWindowRect去获取其他窗口的Rect时,获取到的数据无论窗口实际的大小为多少,获取的永远都是该窗口在100%系统分辨率下所在的位置以及长度与宽度。

2.System DPI Awareness

这个级别的程序,在系统启动的时候,系统所设置的DPI程序会去识别这个DPI,然后根据这个DPI来选择自己程序编好的DPI进行识别,这个是程序层面的DPI适配,在初始化期间,他们使用该系统DPI值适当地布置其UI(大小控制,选择字体大小,加载资源等)。这种适配不是模糊的看起来扩大,而是程序真真切切的变大了,这是程序级别的操作。当然有些程序获取到了系统的DPI以后,他可能不随系统的DPI去设置自己程序窗口的DPI,反而让自己的程序在125%的情况下依然显示的大小是100%的大小,可以去看一下生死狙击微端登录这一款游戏。

但是假设我们是双屏的屏幕,一个主屏幕是125%,另一个屏幕是150%,然后程序启动感受到了125%的桌面DPI,然后初始化的时候使用程序125%的初始化方式,但是当程序从125%的屏幕移动到150%的屏幕时候,程序不会再进行150%的初始化,而是让系统对其进行模糊的位图放大,这个放大就是操作系统层面的。

所以来说这个级别的程序只是在初始化的时候去获取一下DPI然后根据自己程序的情况去适配系统的DPI

3.Per­Monitor

此模式下的程序是告诉系统,不要对我使用DWM放大,当系统DPI更改时,给程序发送WM_DPICHANGED消息,应用程序自己负责为DPI处理自身的大小调整。桌面应用程序使用的大多数UI框架(Windows通用控件(comctl32),Windows窗体,Windows Presentation Framework等)都不支持自动DPI缩放,需要开发人员自行调整其窗口内容的大小并重新放置其内容。

4.Per­Monitor (V2)

Per—Monitor的模式相同,主要差别为以下几点:

  1. DPI更改时(顶级HWND和子HWND通知应用程序
  2. 该应用程序看到每个显示器的原始像素
  3. Windows永远不会缩放应用程序
  4. Windows自动缩放非客户区域(窗口标题,滚动条等)
  5. Win32对话框(来自CreateDialog)由Windows自动缩放DPI
  6. 公共控件(复选框,按钮背景等)中以主题绘制的位图资产将以适当的DPI比例因子自动呈现

Per­Monitor v2模式下运行时,当DPI更改时,将通知应用程序。如果应用程序没有为新的DPI调整大小,则应用程序UI将显得太小或太大(取决于先前DPI值和新DPI值的不同)。

对于应用程序的DPI感知级别可以在Win10下的任务管理器中查看

打开任务管理器,右键点击窗口栏,就可以弹出程序显示情况,勾选DPI感知,就可以在详细里面看到每个程序的DPI感知模式。

其他更深入的知识请移步MSDN High DPI Desktop Application Development on Windows

 

五、如何设置程序的DPI感知以及相关的API使用

理解了上面程序的DPI感知级别,我们再讲一下如何去设置程序的DPI感知级别,主要有以下两个办法:(方法名是我在MSDN上复制过来的,翻译过来很生硬,大家自行理解)

  1. through an application manifest setting
  2. programmatically through an API call

 

在此主要解释一下第二种方法,使用windows提供的API进行设置,就是以编程方式设置程序的DPI感知,虽然Windows不建议我们这么做,但是在我们的开发过程中,这些API还是挺实用的,以及其他相关程序DPI感知行为的API解释。

这些API有三个版本的迭代,以SetProcessDpiAwarenessContext系列为例

 

API

Windows

最低支持

版本

DPI Unaware

System DPI Aware

Per Monitor D

SetProcessDpiAware

Windows

Vista

N/A

SetProcessDpiAware()

N/A

SetProcessDpiAwareness

Windows

8.1

PROCESS_DPI_UNAWARE

PROCESS_DPI_SYSTEM_DPI_AWARE

PROCESS_DPI

SetProcessDpiAwarenessContext

Windows

10,version 1607

DPI_AWARENESS_CONTEXT_UNAWARE

DPI_AWARENESS_CONTEXT_SYSTEM_AWARE

DPI_AWARENE

SetProcessDpiAwareness ()函数的参数枚举:

SetProcessDpiAwarenessContext()函数的参数枚举:

DPI系列的相关函数还有获取程序DPI感知等,详细可去MSDN上查看

六、日常DPI相关开发时容易遇到的问题

1、在自身程序为DPI不感知的情况下,调用系统API去获取DPI时,获取到的数据永远是100%的DPI

包括获取其他窗口大小,都是100%的情况。这一系列函数包括但不限于:

- BOOL GetWindowRect(HWND hWnd,LPRECT lpRect); 

看图中的例子可以想到该博主为什么获取到的Rect还要去乘一下系统缩放大小,因为他的程序是不感知系统DPI的级别,所以调用这个API需要去乘一下当前系统的缩放大小才能获取到真正实际的Rect。但是如果你的程序是DPI感知的情况,获取到的Rect就是实际上的Rect,不需要再去乘以系统的缩放大小了。

 

- int GetDeviceCaps(HDC hdc, int nlndex);

使用这个API去获取系统dpi像素点,这个跟上面的解释是一样的。

- GetDpiForSystem();

看一下这个函数的MSDN就可以明白

其他相关的一系列:GetDpiForWindowGetSystemDpiForProcess等等。

所以说当自己的程序为不感知系统DPI的情况下,我们调用系统提供的相关API都是获取不到正确的,如果想要获取到正确的DPI我们其实有两种方案:

1.通过读取注册表键值去获取

这种方法的局限就是你需要重启电脑才能获取到。键值位置如下图。

2.通过逻辑宽度与物理宽度比值计算获取桌面DPI

但是这个函数在程序DPI感知的情况下获取的大小永远是1,代码如下,只适用于当前DPI不感知的程序去获取当前系统DPI。win10下不需要重启就能获取到。

double GetDesktopDpiByCalcu()
{
	double dDpi = 0.0;
	// 获取窗口当前显示的监视器
	// 使用桌面的句柄.
	HWND hWnd = GetDesktopWindow();
	HMONITOR hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);

	// 获取监视器逻辑宽度与高度
	MONITORINFOEX miex;
	miex.cbSize = sizeof(miex);
	GetMonitorInfo(hMonitor, &miex);
	int cxLogical = (miex.rcMonitor.right - miex.rcMonitor.left);
	int cyLogical = (miex.rcMonitor.bottom - miex.rcMonitor.top);

	// 获取监视器物理宽度与高度
	DEVMODE dm;
	dm.dmSize = sizeof(dm);
	dm.dmDriverExtra = 0;
	EnumDisplaySettings(miex.szDevice, ENUM_CURRENT_SETTINGS, &dm);
	int cxPhysical = dm.dmPelsWidth;
	int cyPhysical = dm.dmPelsHeight;

	// 缩放比例计算  实际上使用任何一个即可
	double horzScale = ((double)cxPhysical / (double)cxLogical);
	double vertScale = ((double)cyPhysical / (double)cyLogical);
	double x = floor(horzScale * 100.00f + 0.5) / 100.00f;
	double y = floor(vertScale * 100.00f + 0.5) / 100.00f;
	if (x == y)//计算有偏差保留小数点后3位进行比较
	{
		dDpi = x;
	}

	return dDpi;
}

2、在自身程序为DPI感知的情况下,调用系统API去获取DPI可以获取到正确的

所以说你可以调用之前标题五中所讲的方式去设置自己程序的DPI感知级别。最简单的就是SetProcessDPIAware,使用这个函数以后,再去调用以上系统API就能获取到正确的系统DPI,但是Windows好像不太建议这么去设置自己程序的DPI。

 

 

 

 

 

 

 

 

 

 

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值