缩放非客户区(标题栏、菜单栏)以支持每个显示器的高 DPI [英] Scaling the non-client area (title bar, menu bar) for per-monitor high-DPI support

查看:48
本文介绍了缩放非客户区(标题栏、菜单栏)以支持每个显示器的高 DPI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Windows 8.1 引入了针对不同显示器进行不同 DPI 设置的功能.此功能称为每个显示器的高 DPI 支持".它持续存在,

另一方面,如果应用程序明确指出它支持每台显示器的高 DPI,则不会执行任何虚拟化,并且开发人员负责扩展.微软有一个相当全面的解释

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

使用 Spy++ 进行的调查证实这只是一个标准的 Win32 对话框——没什么特别的.所有的控件都是标准的 Win32 SDK 控件.它不是 UWP 应用,也没有自定义绘制标题栏——它仍然具有 WS_CAPTION 样式.它由 explorer.exe 进程启动,该进程标记为每个显示器的高 DPI 感知(通过 Process Explorer 和 GetProcessDpiAwareness 验证).这篇博文证实运行对话框和命令提示符都已在 Windows 10 中重写以正确缩放(请参阅Command shells et al.").运行"对话框如何调整其标题栏的大小?

通用项对话框 API 负责新样式的打开和保存对话框,当从每个监视器高 DPI 感知的进程启动时,也可以正确缩放,正如您在运行"对话框中单击浏览"按钮时所见.任务对话框 API,创建 应用启动带有不同大小标题栏的对话框的奇怪情况.(但是,旧版 MessageBox API 尚未更新,并且表现出与我的测试应用相同的行为.)

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

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

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

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

解决方案

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

Windows 8.1 introduced the ability to have different DPI settings for different monitors. This feature is known as "per-monitor high-DPI support." It persists and has been further refined in Windows 10.

If an application does not opt in (i.e., is either DPI-unaware or high-DPI aware), it will be automatically scaled up by DWM to the proper DPI. Most applications fall into one of these two categories, including most of the utilities bundled with Windows (e.g., Notepad). On my test system, the high-DPI monitor is set to 150% scale (144 DPI), while the normal monitor is set to the system DPI (100% scale, 96 DPI). Therefore, when you open one of these applications on the high-DPI screen (or drag it over there), virtualization kicks in, magnifying everything, but also making it incredibly blurry.

On the other hand, if an application explicitly indicates that it supports per-monitor high-DPI, then no virtualization is performed and the developer is responsible for scaling. Microsoft has a fairly comprehensive explanation 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.

I've gotten all of this going successfully in a little C++ test app. It's a lot of boilerplate and mostly project settings, so I don't see much point in posting a full example here. If you want to test it out, either follow Kenny's instructions, this tutorial on MSDN, or download the official SDK sample. Now, the text in the client area looks good (because of my handling of WM_DPICHANGED), but because virtualization is no longer performed, there is no scaling of the non-client area. The result is that the title/caption bar and the menu bar are the wrong size—they do not get larger on the high-DPI screen:

So the question is, how do I get the non-client area of the window to scale to the new DPI?
It doesn't matter whether you create your own window class or use a dialog—they have identical behavior in this respect.

It has been suggested that there is no answer—that your only choice is to custom draw the entire window, including the non-client area. While this is certainly possible, and indeed what UWP apps (those previously known as Metro) do, like the Windows 10 Calculator, it is not a workable option for desktop applications that use many non-client widgets and hope to look native.

Aside from that, it is demonstrably false. Custom-drawn title bars cannot be the only way of getting the correct behavior, since the Windows shell team has done it. The humble Run dialog behaves exactly as expected, properly resizing both the client and non-client areas as you drag it between monitors with different DPIs:

Investigation with Spy++ confirms this is just a bog-standard Win32 dialog—nothing fancy. All of the controls are standard Win32 SDK controls. It is not a UWP app, nor have they custom-drawn the title bar—it still has the WS_CAPTION style. It is launched by the explorer.exe process, which is marked as per-monitor high-DPI aware (verified with Process Explorer and GetProcessDpiAwareness). This blog post confirms that both the Run dialog and the Command Prompt have been rewritten in Windows 10 to scale correctly (see "Command shells et al."). What is the Run dialog doing to resize its title bar?

The 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. (The legacy MessageBox API has not been updated, however, and exhibits the same behavior as my test app.)

If the shell team is doing it, it has to be possible. I just cannot imagine that the team responsible for designing/implementing per-monitor DPI support neglected to provide a reasonable way for developers to produce compatible applications. Features like this require developer support, or they are broken out-of-the-box. Even WPF applications are broken—Microsoft's Per-Monitor Aware WPF Sample project fails to scale the non-client area, resulting in a title bar that is the wrong size. I'm not much for conspiracy theories, but this smells of a marketing move to discourage desktop app development. If so, and there is no official way, I'll accept answers that rely on undocumented behavior.

Speaking of undocumented behavior, logging window messages when the Run dialog is dragged between monitors with different DPI settings shows that it receives an undocumented message, 0x02E1. This is somewhat interesting because this message ID is exactly one greater than the documented WM_DPICHANGED message (0x02E0). My test app never gets this message, though, regardless of its DPI-awareness settings. (Curiously, close inspection does reveal that Windows slightly increases the size of the minimize/maximize/close glyphs on the title bar as the window moves onto the high-DPI monitor. They're still not as big as they are when they are virtualized, but they're slightly bigger than the glyphs that it uses for unscaled system-DPI applications.)

So far, my best idea has been to handle the 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. The caption seems to peak out at the height determined by the system DPI. Even if it worked, this wouldn't be the ideal solution, because it wouldn't help with the system-drawn menu bar or scroll bars…but at least it would be a start. Other ideas?

* I know that this article says "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." See above for why that is (1) wrong and (2) unsatisfactory. I'm looking for a workaround other than custom-drawing the non-client area.

解决方案

In any up-to-date Windows Insider builds (build >= 14342, SDK version# >= 14332) there is an EnableNonClientDpiScaling API (which takes an HWND as its argument) that will enable non-client DPI scaling for top-level HWNDs. This functionality requires that the top-level window be running in per-monitor DPI-awareness mode. This API should be called from the WM_NCCREATE handler for the window. When this API is called on a top-level window, its caption bar, top-level scrollbars, system menu and menubar will DPI scale when the application’s DPI changes (this can happen when the app is moved to a display with a different display scaling value or when the DPI changes for other reasons such as the user making a settings change or when an RDP connection changes the scale factor).

This API does not support scaling non-client area for child windows, such as scroll bars in a child window.

To my knowledge there is no way to have non-client area DPI scale automatically without this API.

Note that this API has not yet been finalized, and it may change prior to being released in the Windows 10 Anniversary update. Keep your eye on MSDN for official documentation when it becomes final.

这篇关于缩放非客户区(标题栏、菜单栏)以支持每个显示器的高 DPI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆