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

时间:2016-04-26 12:24:43

标签: windows winapi windows-10 dpi windows-10-desktop

Windows 8.1引入了为不同显示器设置不同DPI设置的功能。此功能称为"每监视器高DPI支持。"它在Windows 10中持续存在且has been further refined

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

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

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

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

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

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

使用Spy ++进行的调查证实,这只是一个标准的Win32对话框 - 没什么特别的。所有控件都是标准的Win32 SDK控件。它不是UWP应用程序,也没有自定义标题栏 - 它仍然具有WS_CAPTION样式。它由explorer.exe进程启动, 标记为每个监视器高DPI感知(使用Process Explorer和GetProcessDpiAwareness验证)。 This blog post确认已在Windows 10中重写了“运行”对话框和“命令提示符”以正确缩放(请参阅&#34; 命令shell等等。&#34;)。 “运行”对话框调整标题栏的大小是什么?

负责新式打开和保存对话框的Common Item Dialog API在从每个监视器高DPI感知的进程启动时也能正确扩展,如您在单击&#34;浏览&时所看到的#34; “运行”对话框中的按钮。 Task Dialog API也是如此,创建the odd situation where an app launches a dialog box with a different-size title bar。 (但是,旧的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 function一起使用,我可以强制窗口调整大小并重绘其非客户区域以响应WM_DPICHANGEDThis works fine to reduce the height of the title bar, or even remove it altogether, but it will never make it any taller。标题似乎在系统DPI确定的高度达到峰值。即使它有效,这也不是理想的解决方案,因为它对系统绘制的菜单栏或滚动条没有帮助......但至少它将是一个开始。其他想法?

*我知道这篇文章说"Note that the non-client area of a per monitor–DPI aware application is not scaled by Windows, and will appear proportionately smaller on a high DPI display."见上文为什么那是(1)错误和(2)不满意。我正在寻找除自定义绘制非客户区域之外的解决方法。

2 个答案:

答案 0 :(得分:26)

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

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

据我所知,没有这个API,就无法自动进行非客户区DPI扩展。

请注意,此API尚未最终确定,并且可能会在Windows 10周年更新中发布之前发生更改。当它成为最终版本时,请密切关注MSDN以获取官方文档。

答案 1 :(得分:7)

使用Windows 10 Creators Update(版本15063)中的 Per Monitor V2 DPI感知,您可以轻松解决此问题而无需EnableNonClientDpiScaling

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

<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>

参考文献: