【发布时间】:2016-08-20 06:42:47
【问题描述】:
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 中重写了运行对话框和命令提示符以正确扩展(请参阅“Command shells et al.”)。 “运行”对话框如何调整其标题栏的大小?
Common Item Dialog API,负责新式的打开和保存对话框,当从每个监视器高 DPI 感知的进程启动时,也可以正确缩放,正如您在单击“运行”中的“浏览”按钮时所看到的那样对话。 Task Dialog API 也一样,创建 the odd situation where an app launches a dialog box with a different-size title bar。 (但是,旧版 MessageBox API 尚未更新,并且表现出与我的测试应用相同的行为。)
如果 shell 团队正在这样做,那一定是可能的。 我无法想象负责设计/实现每个监视器 DPI 支持的团队忽略了为开发人员提供合理的方式来实现产生兼容的应用程序。像这样的功能需要开发人员的支持,或者它们是开箱即用的。甚至 WPF 应用程序都损坏了——Microsoft 的 Per-Monitor Aware WPF Sample 项目无法缩放非客户区,导致标题栏的大小错误。我不太喜欢阴谋论,但这闻起来像是一种阻止桌面应用程序开发的营销举措。如果是这样,并且没有官方方式,我将接受依赖于无证行为的答案。
谈到未记录的行为,当在具有不同 DPI 设置的监视器之间拖动“运行”对话框时记录窗口消息表明它收到了未记录的消息 0x02E1。这有点有趣,因为此消息 ID 正好比记录的 WM_DPICHANGED 消息 (0x02E0) 大一。但是,无论其 DPI 感知设置如何,我的测试应用程序都不会收到此消息。 (奇怪的是,仔细检查确实发现,当窗口移动到高 DPI 显示器上时,Windows 略微增加了标题栏上最小化/最大化/关闭字形的大小。它们仍然没有那么大就像它们被虚拟化时一样,但它们比它用于未缩放的系统 DPI 应用程序的字形略大。)
到目前为止,我最好的想法是处理WM_NCCALCSIZE 消息以调整非客户区的大小。通过使用带有SetWindowPos function 的SWP_FRAMECHANGED 标志,我可以强制窗口调整大小并重绘其非客户区以响应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 确定的高度达到峰值。即使它有效,这也不是理想的解决方案,因为它对系统绘制的菜单栏或滚动条没有帮助……但至少这是一个开始。其他想法?
* 我知道这篇文章说 "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) 不令人满意。我正在寻找一种解决方法,而不是自定义绘制非客户区。
【问题讨论】:
-
我猜这不是不将
WM_DPICHANGED传递给DefWindowProc这么简单的事情吗? -
@Jonathan 不幸的是没有。按照惯例,我的测试应用程序将所有消息传递给
DefWindowProc(WM_PAINT除外),这样就可以处理未记录的消息。 -
@Barmak 我猜它没有自定义绘制标题栏,因为它具有
WS_CAPTION样式。我想它可能会使用WM_NCCALCSIZE来删除系统绘制的标题栏并绘制自己的标题栏。是的,我已经通过重新下载最新版本的 Visual Studio 工具集尝试了所有这些。绝对设置为 v140,并专门针对最新版本的 Windows 10 (10586)。 -
其实我错了。我不知道如何在 Windows 10 中制作一个自定义绘制标题栏,使关闭/最大/最小按钮更大。因此 Run-Dialog 的标题栏可能不是自定义绘制(如果那将是另一个谜案例)
-
我用这个method 启动了运行对话框。似乎 Run-Dialog 使用来自父进程的清单。如果我的进程是 per-monitor-dpi-aware,那么 Run-Dialog 也是 per-monitor-dpi-aware,但标题栏故障与我自己的进程相同。如果您从开始菜单启动 Run-Dialog,则 Explorer.exe 将成为父进程,并且 Run-Dialog 使用 Explorer.exe 中的清单。查看Explorer.exe的代码,它似乎有自己的特殊DPI标志:
Explorer (对其他程序不起作用)
标签: windows winapi windows-10 dpi windows-10-desktop