为每个显示器的高DPI支持缩放非客户端区域(标题栏,菜单栏)

Windows 8.1引入了针对不同显示器具有不同DPI设置的function。 此function被称为“每监视器高DPI支持”。 它坚持并在Windows 10中进一步完善 。

如果应用程序没有select( 不识别DPI或识别高DPI),DWM会自动将其扩展到适当的DPI。 大多数应用程序属于这两个类别之一,包括与Windows捆绑在一起的大多数应用程序( 例如记事本)。 在我的testing系统中,高DPI显示器设置为150%(144 DPI),而普通显示器设置为系统DPI(100%显示,96 DPI)。 因此,在高DPI屏幕上打开其中一个应用程序(或将其拖到那里)时,虚拟化将会启动,放大所有内容,但也使其变得难以置信的模糊。

另一方面,如果应用程序明确指出它支持每个监视器的高DPI,则不执行虚拟化,开发人员负责缩放。 微软在这里有一个相当全面的解释* ,但为了一个自包含的问题,我将总结。 首先,您通过在清单中设置<dpiAware>True/PM</dpiAware>来指示支持。 这使您接收WM_DPICHANGED消息 ,它告诉您新的DPI设置以及为您的窗口build议新的大小和位置。 它还允许您调用GetDpiForMonitor函数并获取实际的 DPI,而不会出于兼容性原因而撒谎。 肯尼·克尔还写了一个全面的教程 。

我已经在一个小小的C ++testing应用程序中成功完成了这一切。 这是很多的样板,主要是项目设置,所以我在这里发表一个完整的例子没有多less意义。 如果您想对其进行testing,请遵循Kenny的说明, MSDN上的本教程或下载官方SDK示例 。 现在,客户区域中的文本看起来不错(因为我处理了WM_DPICHANGED ),但是由于虚拟化不再执行,所以没有缩放非客户区域。 结果是标题/标题栏和菜单栏的大小错误 – 在高DPI屏幕上它们不会变大:

所以问题是,如何让窗口的非客户区域扩展到新的DPI?
无论您是创build自己的窗口类还是使用对话框,它们在这方面都具有相同的行为。

有人build议 ,没有答案 – 你唯一的select是自定义绘制整个窗口,包括非客户区。 虽然这当然是可能的,事实上UWP应用程序(以前称为Metro)的应用程序(如Windows 10计算器)对于使用许多非客户端小部件并希望看起来是本机的桌面应用程序来说不是一个可行的select。

除此之外,这是明显的错误。 自定义绘制的标题栏不能是获得正确行为的唯一方法,因为Windowsshell团队已经完成了它。 卑鄙的“运行”对话框的行为与预期的完全相同,在不同DPI的监视器之间拖曳时,可以正确调整客户端和非客户端区域的大小:

Spy ++的调查证实这只是一个标准的Win32对话框 – 没有什么奇特的。 所有的控件都是标准的Win32 SDK控件。 它不是UWP应用程序,也不是自定义标题栏 – 它仍然具有WS_CAPTION风格。 它由explorer.exe进程启动,该进程标记为每个监视器可识别的DPI(使用Process Explorer和GetProcessDpiAwareness进行validation)。 这篇博客文章确认,Windows 10中的“运行”对话框和“命令提示符”都已被重写,可以正确扩展(请参阅“ 命令shell ”)。 什么是运行对话框来调整其标题栏?

负责新样式“打开”和“保存”对话框的Common Item Dialog API在从每个监视器识别出高DPI的进程启动时也可以正确缩放,如同从“运行”对话框中单击“浏览”button时所看到的。 Task Dialog API也是如此 ,创造了一个奇怪的情况,即应用程序启动一个不同大小的标题栏的对话框 。 (但是,传统的MessageBox API尚未更新,并且与我的testing应用程序具有相同的行为。)

如果壳牌团队正在这样做,那就必须有可能。 我无法想象,负责devise/实现每个监视器DPI支持的团队忽视了为开发人员提供合理的方式来生成兼容的应用程序。 像这样的function需要开发人员的支持,或者他们是开箱即用的。 即使是WPF应用程序也被破坏 – 微软的Per-Monitor Aware WPF示例项目无法扩展非客户端区域,导致标题栏大小错误。 我对阴谋论不太了解,但是这种营销行为的气味阻碍了桌面应用程序的开发。 如果是这样,并没有官方的方式,我会接受依赖无证行为的答案。

说到未logging的行为,当“运行”对话框在具有不同DPI设置的监视器之间拖动时logging窗口消息,表明它收到未logging的消息0x02E1 。 这有点令人感兴趣,因为这个消息ID正好比logging的WM_DPICHANGED消息( 0x02E0 )大一个。 不过,我的testing应用程序不会收到此消息,不pipe其DPI感知设置如何。 (奇怪的是,仔细检查发现,当窗口移动到高DPI监视器上时,Windows 略微增加了标题栏上最小化/最大化/closures字形的大小,但它们仍然不像虚拟化时那么大,但是它们比用于未缩放系统-DPI应用程序的字形略大。)

到目前为止,我最好的想法是处理WM_NCCALCSIZE消息来调整非客户区的大小。 通过在SetWindowPos函数中使用SWP_FRAMECHANGED标志,我可以强制窗口resize并重绘其非客户区以响应WM_DPICHANGED 。 这可以很好地减less标题栏的高度,甚至完全删除它,但它永远不会使它更高 。 标题似乎在由系统DPI确定的高度达到峰值。 即使它工作,这也不是理想的解决scheme,因为它不会帮助系统绘制的菜单栏或滚动条…但至less这将是一个开始。 其他想法?

*我知道这篇文章写道: “请注意,每台显示器的DPI感知应用程序的非客户端区域不会被Windows缩放,并且会在较高的DPI显示器上按比例缩小。” 见上文为什么是(1)错误和(2)不满意。 我正在寻找一个解决scheme,而不是自定义绘制非客户端区域。

Solutions Collecting From Web of "为每个显示器的高DPI支持缩放非客户端区域(标题栏,菜单栏)"

在任何最新的Windows Insider构建版本(build> = 14342,SDK版本#> = 14332)中,都有一个EnableNonClientDpiScaling API(以HWND为参数),可以为顶级HWND启用非客户端DPI缩放。 此功能要求顶级窗口以每台显示器的DPI感知模式运行。 应该从该窗口的WM_NCCREATE处理程序调用此API。 在顶级窗口调用此API时,当应用程序的DPI更改时,其标题栏,顶级滚动条,系统菜单和菜单栏将DPI缩放(当应用程序移动到具有不同显示缩放的显示器时,可能会发生这种情况值或当DPI由于其他原因(如用户进行设置更改或RDP连接更改比例因子)而发生更改时。

此API不支持缩放子窗口的非客户端区域,例如子窗口中的滚动条。

据我所知,没有这个API,没有办法让非客户区DPI自动扩展。

请注意,此API尚未完成,并且可能会在Windows 10周年更新发布之前更改。 当它成为最终的时候,请留意MSDN的官方文档。

通过Windows 10创建者更新(版本15063)中的每个监视器V2 DPI感知,您可以在不使用EnableNonClientDpiScaling情况下轻松解决此问题。

要启用Per Monitor V2 DPI感知,同时仍然支持较旧的Windows 10版本上的旧的Per Monitor DPI感知以及旧版Windows的Windows 8.1和DPI感知,请使您的应用程序如下所示:

 <assembly ...> <!-- ... ---> <asmv3:application> <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> <dpiAware>True/PM</dpiAware> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness> </asmv3:windowsSettings> </asmv3:application> </assembly> 

参考文献:

  • 桌面应用程序开发人员在Windows 10创作者更新中的高DPI改进 – 视频
  • 应用程序清单 – 参考