缩放 non-client 区域(标题栏、菜单栏)以获得 per-monitor high-DPI 支持

Scaling the non-client area (title bar, menu bar) for per-monitor high-DPI support

Windows 8.1 引入了针对不同显示器设置不同 DPI 的功能。此功能被称为 "per-monitor high-DPI support." 它在 Windows 10.

中持续存在并且 has been further refined

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

另一方面,如果应用程序明确表示它支持 per-monitor high-DPI,则不会执行虚拟化,开发人员负责扩展。微软有相当全面的解释 here*, but for the benefit of a self-contained question, I'll summarize. First, you indicate support by setting <dpiAware>True/PM</dpiAware> in the manifest. This opts you into receiving WM_DPICHANGED messages, which tells you both the new DPI setting as well as a suggested new size and position for your window. It also allows you to call the GetDpiForMonitor function and obtain the actual DPI, without being lied to for compatibility reasons. Kenny Kerr has also written up a comprehensive tutorial.

我已经在一个小型 C++ 测试应用程序中成功完成了所有这些工作。这是很多样板文件,主要是项目设置,所以我认为在这里发布完整示例没有多大意义。如果您想对其进行测试,请按照 Kenny 的说明进行操作,this tutorial on MSDN, or download the official SDK sample。现在,客户区的文字看起来不错(因为我对WM_DPICHANGED的处理),但是因为不再进行虚拟化,所以non-client区域没有缩放。结果是 title/caption 栏和菜单栏的大小 错误 — 它们在 high-DPI 屏幕上不会变大:

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

没有答案——你唯一的选择是自定义绘制整个 window,包括 non-client区域。虽然这当然是可能的,而且确实是 UWP 应用程序(以前称为 Metro)所做的,例如 Windows 10 计算器,但对于使用许多 non-client 小部件并希望的桌面应用程序来说,这不是一个可行的选择看起来像本地人。

除此之外,这显然是错误的。 Custom-drawn 标题栏 不能 是获得正确行为的唯一方法,因为 Windows shell 团队已经做到了。不起眼的 运行 对话框的行为完全符合预期,当您在具有不同 DPI 的显示器之间拖动它时,可以正确调整客户端和 non-client 区域的大小:

使用 Spy++ 进行的调查证实这只是一个 bog-standard Win32 对话框——没什么特别的。所有控件都是标准的 Win32 SDK 控件。它不是 UWP 应用程序,也没有 custom-drawn 标题栏 — 它仍然具有 WS_CAPTION 样式。它由 explorer.exe 进程启动, 标记为 per-monitor high-DPI 可识别(使用 Process Explorer 和 GetProcessDpiAwareness 验证)。 This blog post 确认 运行 对话框和命令提示符都已在 Windows 10 中重写以正确缩放(参见“Command shells 等。 ")。 运行 对话框正在做什么来调整其标题栏的大小?

Common Item Dialog API, responsible for new-style Open and Save dialogs, also scales correctly when launched from a process that is per-monitor high-DPI aware, as you can see when clicking the "Browse" button from the Run dialog. Same thing for the Task Dialog API, creating the odd situation where an app launches a dialog box with a different-size title bar。 (但是,旧版 MessageBox API 尚未更新,并且表现出与我的测试应用程序相同的行为。)

如果shell团队在做,那一定是可以的。我就是无法想象负责designing/implementing的团队 per-monitor 忽略了 DPI 支持,以便为开发人员提供合理的方式来生成兼容的应用程序。像这样的功能需要开发人员支持,否则它们会损坏 out-of-the-box。甚至 WPF 应用程序也损坏了——Microsoft 的 Per-Monitor Aware WPF Sample 项目无法缩放 non-client 区域,导致标题栏大小错误。我不太喜欢阴谋论,但这有点像一种阻止桌面应用程序开发的营销举措。如果是这样,并且没有官方方式,我会接受依赖于未记录行为的答案。

说到未记录的行为,当在具有不同 DPI 设置的显示器之间拖动 运行 对话框时记录 window 消息显示它收到未记录的消息 0x02E1。这有点有趣,因为此消息 ID 恰好比记录的 WM_DPICHANGED 消息 (0x02E0) 大 1。但是,无论其 DPI-awareness 设置如何,我的测试应用程序都不会收到此消息。 (奇怪的是,仔细检查确实发现 Windows 稍微 增加了标题栏上 minimize/maximize/close 字形的大小,因为 window 移动到 high-DPI 监视器。它们仍然没有虚拟化时那么大,但比它用于未缩放的 system-DPI 应用程序的字形略大。)

到目前为止,我最好的想法是处理 WM_NCCALCSIZE message to adjust the size of the non-client area. By using the SWP_FRAMECHANGED flag with the SetWindowPos function, I can force the window to resize and redraw its non-client area in response to WM_DPICHANGED. This works fine to reduce the height of the title bar, or even remove it altogether, but it will never make it any taller。字幕似乎在系统 DPI 确定的高度达到峰值。即使它行得通,这不是理想的解决方案,因为它对 system-drawn 菜单栏或滚动条没有帮助……但至少这是一个开始。其他想法?

* 我知道这篇文章说 "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) 不令人满意。我正在寻找 custom-drawing non-client 区域以外的解决方法。

在任何 up-to-date Windows Insider 构建中(构建 >= 14342,SDK 版本# >= 14332)有一个 EnableNonClientDpiScaling API(它以 HWND 作为参数)这将为 top-level HWND 启用 non-client DPI 缩放。此功能要求 top-level window 在 per-monitor DPI-awareness 模式下为 运行。这个 API 应该从 window 的 WM_NCCREATE 处理程序调用。当在 top-level window 上调用此 API 时,其标题栏、top-level 滚动条、系统菜单和菜单栏将在应用程序的 DPI 更改时缩放 DPI(这可能发生在应用程序被移动到具有不同显示比例值的显示器或当 DPI 因其他原因(例如用户进行设置更改或 RDP 连接更改比例因子)而更改时)。

此 API 不支持缩放 child windows 的 non-client 区域,例如 child window 中的滚动条。

据我所知,没有这个 API。

就无法自动缩放 non-client 区域 DPI

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

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

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

参考文献:


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

来实现相同的目的
<add key="DpiAwareness" value="PerMonitorV2" />

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