缩放非客户区域(标题栏,菜单栏)以获得每个监视器的高DPI支持

Cod*_*ray 36 windows winapi dpi windows-10 windows-10-desktop

Windows 8.1引入了为不同监视器提供不同DPI设置的功能.此功能称为"每监视器高DPI支持".它在Windows 10中持续存在并得到进一步完善.

如果应用程序没有选择加入(,DPI不知道或高DPI意识),它将由DWM自动扩展到正确的DPI.大多数应用程序属于这两个类别之一,包括与Windows捆绑在一起的大多数实用程序(例如,记事本).在我的测试系统上,高DPI监视器设置为150%标度(144 DPI),而普通监视器设置为系统DPI(100%标度,96 DPI).因此,当您在高DPI屏幕上打开其中一个应用程序(或将其拖到那里)时,虚拟化会启动,放大所有内容,但也会让它变得非常模糊.

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

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

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

已经表明,有没有答案,那你唯一的选择是自定义绘制整个窗口,包括非客户区.虽然这当然是可能的,实际上UWP应用程序(以前称为Metro)的功能,例如Windows 10计算器,对于使用许多非客户端小部件并希望看起来像原生的桌面应用程序来说,它不是一个可行的选择.

除此之外,它显然是错误的.自定义绘制的标题栏不能是获得正确行为的唯一方法,因为Windows shell团队已经完成了它.简洁的"运行"对话框的行为完全符合预期,当您在具有不同DPI的监视器之间拖动时,正确调整客户端和非客户端区域的大小:

使用Spy ++进行的调查证实,这只是一个标准的Win32对话框 - 没什么特别的.所有控件都是标准的Win32 SDK控件.它不是一个UWP应用程序,也没有自定义绘制标题栏 - 它仍然具有WS_CAPTION风格.它是由explorer.exe进程,它推出标记为每个监视器高DPI感知(使用Process Explorer和GetProcessDpiAwareness验证).此博客文章确认已在Windows 10中重写了"运行"对话框和"命令提示符"以正确扩展(请参阅" 命令shell等 ").Run对话框用于调整其标题栏的大小是什么?

负责新式打开和保存对话框的Common Item Dialog API在从每个监视器高DPI感知的进程启动时也可以正确扩展,如您在Run对话框中单击"Browse"按钮时所看到的.同样的事情任务对话框API,创建奇数情况下的应用程序启动一个对话框,以不同大小的标题栏.(但是,传统的MessageBox API尚未更新,并且与我的测试应用程序具有相同的行为.)

如果shell团队正在这样做,那么它必须是可能的.我无法想象负责设计/实现每个监视器DPI支持的团队忽略了为开发人员提供合理的方式来生成兼容的应用程序.这样的功能需要开发人员的支持,或者它们是开箱即用的.甚至WPF应用程序都被破坏了 - 微软的Per-Monitor Aware WPF Sample项目无法扩展非客户区域,导致标题栏的大小错误.我对阴谋理论并不多,但这种气氛有助于阻止桌面应用程序开发.如果是这样,并且没有官方方式,我会接受依赖于无证行为的答案.

说到未记录的行为,在具有不同DPI设置的监视器之间拖动"运行"对话框时记录窗口消息表明它接收到未记录的消息,0x02E1.这有点令人感兴趣,因为此消息ID恰好比记录的WM_DPICHANGED消息(0x02E0)大一个.但是,我的测试应用程序永远不会收到此消息,无论其DPI感知设置如何.(奇怪的是,仔细检查确实显示当窗口移动到高DPI监视器上时,Windows 稍微增加了标题栏上最小化/最大化/关闭字形的大小.它们仍然没有虚拟化时那么大,但它们比用于未缩放的系统DPI应用程序的字形略大.)

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

*我知道这篇文章说"请注意,每个监视器-DPI感知应用程序的非客户端区域不会被Windows缩放,并且在高DPI显示器上会显得比例较小." 见上文为什么(1)错误和(2)不满意.我正在寻找除自定义绘制非客户区域之外的解决方法.

pet*_*lts 26

在任何最新的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以获取官方文档.

  • 谢谢,这很棒!标题栏,系统菜单等都可以正确缩放,唯一的问题是上下文菜单(`CreatePopupMenu`)不受影响,并且在低DPI监视器上显得过大.你知道是否有一个技巧可以让它们扩展吗? (2认同)
  • 已提供 [EnableNonClientDpiScaling](https://msdn.microsoft.com/en-us/library/windows/desktop/mt748621(v=vs.85).aspx) 的初步文档。 (2认同)

Mar*_*ryl 8

通过Windows 10 Creators Update(版本15063)中的Per Monitor 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>
Run Code Online (Sandbox Code Playgroud)

参考文献:

  • 我假设`app.config`中的`<add>`标签实际上最终修改了清单 - 这个问题和答案是通用的 - 它们与.NET无关. (3认同)