42

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,而不会出于兼容性原因而被欺骗。Kenny Kerr 还编写了一个综合教程

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

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

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

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

使用 Spy++ 进行的调查证实这只是一个标准的 Win32 对话框——没什么特别的。所有的控件都是标准的 Win32 SDK 控件。它不是 UWP 应用程序,他们也没有自定义绘制标题栏——它仍然具有WS_CAPTION样式。它由 explorer.exe 进程启动,该进程标记为 per-monitor 高 DPI 感知(通过 Process Explorer 和 GetProcessDpiAwareness 验证)。这篇博文确认运行对话框和命令提示符都已在 Windows 10 中重写以正确扩展(请参阅“命令 shell 等”)。运行对话框在做什么来调整其标题栏的大小?

Common Item Dialog API负责新型打开和保存对话框,当从每个监视器高 DPI 感知的进程启动时,它也可以正确缩放,正如您在“运行”对话框中单击“浏览”按钮时所看到的那样。Task Dialog API也是如此,创造了一个奇怪的情况,即应用程序启动一个带有不同大小标题栏的对话框。(但是,旧的 MessageBox API 尚未更新,并且表现出与我的测试应用程序相同的行为。)

如果外壳团队正在这样做,它必须是可能的。我只是无法想象负责设计/实现每个监视器 DPI 支持的团队忽略了为开发人员提供一种合理的方式来生成兼容的应用程序。像这样的功能需要开发人员的支持,或者它们是开箱即用的。甚至 WPF 应用程序都损坏了——Microsoft 的Per-Monitor Aware WPF 示例项目无法缩放非客户区,导致标题栏的大小错误。我不太喜欢阴谋论,但这闻起来像是一种阻止桌面应用程序开发的营销举措。如果是这样,并且没有官方方式,我将接受依赖于无证行为的答案。

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

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

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

4

2 回答 2

26

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

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

据我所知,如果没有此 API,就无法自动调整非客户区 DPI。

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

于 2016-06-03T22:30:19.890 回答
9

借助Windows 10 创意者更新(内部版本 15063)中的Per Monitor V2 DPI 感知,您可以轻松解决此问题,而无需EnableNonClientDpiScaling.

要启用Per Monitor V2 DPI 感知,同时仍支持旧Windows 10 版本和 Windows 8.1 上的旧Per Monitor DPI 感知以及旧版本 Windows 上的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>

参考:


请注意,在面向 .NET 4.7 及更高版本的 WinForms 中,您可以通过添加

<add key="DpiAwareness" value="PerMonitorV2" />

<System.Windows.Forms.ApplicationConfigurationSection>标记在app.config. 我假设最后,此更改会修改目标二进制文件中的清单,如上所述。

于 2017-04-05T14:38:22.887 回答