当前位置:   article > 正文

彻底搞懂 Windows 显示器 DPI 及其相关功能(一、高分辨率介绍)_显示器dpi

显示器dpi

目录

​编辑

一、前言

二、高分辨率(High DPI)

1. 显示比例系数和 DPI

2. DPI 感知模式(DPI Awareness Mode)

(1)未感知 DPI(DPI Unaware)

(2)系统 DPI 感知(System DPI Awareness)

(3)每显示器和每显示器 (V2) DPI 感知(Per-Monitor and Per-Monitor (V2) DPI Awareness)

(4)每个显示器 (V1) DPI 感知(Per Monitor (V1) DPI Awareness)

(5)用户界面框架/技术对每个显示器 DPI 的缩放支持(Per Monitor DPI Scaling Support by UI Framework / Technology)

三、更新现有应用程序

Example:

混合模式 DPI 缩放(子进程 DPI 缩放):

测试更改


一、前言

              系列文章:

  1. 彻底搞懂 Windows 显示器 DPI 及其相关功能(一、高分辨率介绍)
  2. 彻底搞懂 Windows 显示器 DPI 及其相关功能(二、缩放比例感知介绍)
  3. 彻底搞懂 Windows 显示器 DPI 及其相关功能(三、Delphi 如何适用)
  4. Delphi 程序例子(自动感知及显示器相关功能演示)

        目前随着4K显示器的逐步普及,各种高分辨率的显示器层出不穷,最标准典型的4K分辨率显示器,显示分辨率是3840 x 2160,2K显示器的分辨率为:1920 X 1080。那么除了分辨率,显示器还有缩放比例,显示方向等等,如何判断和设置呢?特别是缩放比例,感觉好像理解了,但是程序做出来又似乎不能平滑、完美的适应4K等高分辨率显示器,本文将详细介绍高分辨率显示器的这些属性,让你彻底了解其实现方式,以便你的程序能够适应不同的显示器。

二、高分辨率(High DPI

        对希望更新桌面应用程序以动态处理显示比例因子(每英寸点数,或 DPI)变化的开发人员,使他们的应用程序能够在任何显示器上清晰呈现。

        首先,如果您要从头开始创建一个新的 Windows 应用程序,强烈建议您创建一个通用 Windows 平台 (UWP) 应用程序。UWP 应用程序会自动动态缩放,以适应其运行的每个显示屏。

        使用旧版 Windows 编程技术(原始 Win32 编程、Windows 窗体、Windows 演示框架 (WPF) 等)的桌面应用程序无法自动处理 DPI 缩放,开发人员需要额外的工作。如果不做这些工作,在许多常见的使用场景中,应用程序就会显得模糊或大小不正确。本节将介绍了更新桌面应用程序以正确呈现所涉及的上下文和信息。

1. 显示比例系数和 DPI

        随着显示技术的不断进步,显示面板制造商在面板上的每单位物理空间中嵌入了越来越多的像素。这使得现代显示面板的每英寸点数 (DPI) 远高于历史水平。过去,大多数显示器每线性英寸物理空间只有 96 个像素(96 DPI);而到了 2017 年,近 300 DPI 或更高的显示器已随处可见。

        大多数传统的桌面用户界面框架都有一个内置假设,即在程序的生命周期内,显示器的 DPI 不会发生变化。这种假设已不再适用,因为在应用程序进程的整个生命周期中,显示屏 DPI 通常会发生多次变化。显示比例因子/DPI 发生变化的一些常见情况包括:

  • 多显示器设置,每台显示器都有不同的比例系数,应用程序从一台显示器移动到另一台显示器(如 4K 和 1080p 显示器)
  • 将高 DPI 笔记本电脑与低 DPI 外接显示器对接或分离(反之亦然)
  • 通过远程桌面从高 DPI 笔记本电脑/平板电脑连接到低 DPI 设备(反之亦然)
  • 在应用程序运行时更改显示比例因子设置

        在这些情况下,UWP 应用程序会根据新的 DPI 自动重绘。默认情况下,如果开发人员不做额外的工作,桌面应用程序就不会这样做。如果桌面应用程序不做响应 DPI 变化的额外工作,用户可能会看到模糊或大小不正确的图形。

2. DPI 感知模式(DPI Awareness Mode)

        桌面应用程序必须告知 Windows 它们是否支持 DPI 缩放。默认情况下,系统会认为桌面应用程序不支持 DPI,并对其窗口进行位图拉伸。通过设置以下可用的 DPI 感知模式之一,应用程序可以明确告诉 Windows 它们希望如何处理 DPI 缩放:

(1)未感知 DPI(DPI Unaware)

        未感知 DPI 的应用程序以 96(100%)的固定 DPI 值呈现。只要在显示比例大于 96 DPI 的屏幕上运行这些应用程序,Windows 就会将应用程序位图拉伸到预期的物理尺寸。这将导致应用程序显示模糊。

(2)系统 DPI 感知(System DPI Awareness)

        可感知系统 DPI 的桌面应用程序通常会在用户登录时接收主连接显示器的 DPI。在初始化过程中,它们会使用该系统 DPI 值对用户界面进行适当布局(调整控件大小、选择字体大小、加载资源等)。因此,在以该单一 DPI 渲染的显示器上,Windows 不会对系统 DPI 感知应用程序进行 DPI 缩放(位图拉伸)。当应用程序被移动到具有不同比例因子的显示器上时,或者如果显示器比例因子发生其他变化,Windows 将对应用程序的窗口进行位图缩放,从而使它们看起来模糊不清。实际上,系统 DPI 感知桌面应用程序只能在单一显示比例系数下清晰呈现,只要 DPI 发生变化就会变得模糊

(3)每显示器和每显示器 (V2) DPI 感知(Per-Monitor and Per-Monitor (V2) DPI Awareness)

        可能的话,建议将桌面应用程序更新为使用每显示器 DPI 感知模式,以便在 DPI 发生变化时立即正确呈现。当应用程序向 Windows 报告要在此模式下运行时,Windows 不会在 DPI 发生变化时对应用程序进行位图拉伸,而是向应用程序窗口发送 WM_DPICHANGED。应用程序完全有责任根据新的 DPI 自行调整大小。桌面应用程序使用的大多数 UI 框架(Windows 常用控件 (comctl32)、Windows 窗体、Windows 演示框架等)都不支持自动 DPI 缩放,这就要求开发人员自己调整窗口内容的大小和位置。

        应用程序可将自己注册为两种版本的 “每监视器感知”:版本 1 和版本 2 (PMv2)。将进程注册为以 PMv2 感知模式运行会导致以下结果:

  1. 当 DPI 发生变化时通知应用程序(包括顶层和子 HWND)
  2. 应用程序看到每个显示器的原始像素
  3. 应用程序永远不会被 Windows 按位图缩放
  4. Windows 自动对非客户端区域(窗口标题、滚动条等)进行 DPI 缩放
  5. Win32 对话框(来自 CreateDialog)可由 Windows 自动进行 DPI 缩放
  6. 常用控件(复选框、按钮背景等)中的主题绘制位图资产自动以适当的 DPI 比例因子呈现

        在 Per-Monitor v2 Awareness 模式下运行时,应用程序会在其 DPI 发生变化时收到通知。如果应用程序没有根据新的 DPI 调整自身大小,应用程序的用户界面就会显得过小或过大(取决于新旧 DPI 值的差异)。

对每显示器 V1 (PMv1) 的感知非常有限。建议应用程序使用 PMv2。

下表显示了应用程序在不同情况下的呈现方式:

DPI Awareness ModeWindows版本程序的 DPI 视图DPI 更改后的行为
UnawareN/A所有显示均为 96 DPI位图拉伸(模糊)
SystemVista所有显示器都具有相同的 DPI(当前用户会话启动时主显示器的 DPI)位图拉伸(模糊)
Per-Monitor8.1应用程序窗口以来主所在显示屏的 DPI
  • 顶层 HWND 会收到 DPI 更改通知
  • 不对任何用户界面元素进行 DPI 缩放。
Per-Monitor V2Windows 10 Creators Update (1703)应用程序窗口以来主所在显示屏的 DPI
  • 顶层和子 HWND 会收到 DPI 更改通知

自动缩放 DPI:

  • 非客户端区域
  • 常用控件中的主题绘制位图(comctl32 V6)
  • 对话框(创建对话框)

(4)每个显示器 (V1) DPI 感知(Per Monitor (V1) DPI Awareness)

        Windows 8.1 引入了每显示器 V1 DPI 感知模式 (PMv1)。这种 DPI 感知模式非常有限,只能提供以下功能。建议桌面应用程序使用 Windows 10 1703 或更高版本支持的 Per-Monitor v2 感知模式。

        最初支持的每显示器感知仅为应用程序提供了以下功能:

  1. 顶层 HWND 会收到 DPI 更改通知,并提供新的建议尺寸
  2. Windows 不会对应用程序 UI 进行位图拉伸
  3. 应用程序以物理像素显示所有显示器(请参阅虚拟化)

        在 Windows 10 1607 或更高版本中,PMv1 应用程序还可以在 WM_NCCREATE 期间调用 EnableNonClientDpiScaling,以请求 Windows 正确缩放窗口的非客户端区域。

(5)用户界面框架/技术对每个显示器 DPI 的缩放支持(Per Monitor DPI Scaling Support by UI Framework / Technology)

        下表显示了各种 Windows UI 框架在 Windows 10 1703 中提供的每显示器 DPI 感知支持级别:

框架/技术支持OS版本DPI缩放方式更多参考
Universal Windows Platform (UWP)Full1607UI frameworkUniversal Windows Platform (UWP)
Raw Win32/Common Controls V6 (comctl32.dll)
  • 向所有 HWND 发送 DPI 更改通知消息
  • 在常用控件中正确呈现主题绘制
  • 自动缩放对话框的 DPI
1703ApplicationGitHub Sample
Windows Forms对某些控件进行有限的按显示器 DPI 自动缩放1703UI frameworkHigh DPI Support in Windows Forms
Windows Presentation Framework (WPF)本地 WPF 应用程序将 DPI 缩放其他框架中托管的 WPF 和 WPF 中托管的其他框架不会自动缩放1607UI frameworkGitHub Sample
GDINoneN/AApplicationSee GDI High-DPI Scaling
GDI+NoneN/AApplicationSee GDI High-DPI Scaling
MFCNoneN/AApplicationN/A

三、更新现有应用程序

        为了更新现有桌面应用程序以正确处理 DPI 缩放,需要对其进行更新,至少要更新用户界面的重要部分以响应 DPI 变化。

        大多数桌面应用程序都在系统 DPI 感知模式下运行。系统 DPI 感知应用程序通常会根据主显示屏(启动 Windows 会话时系统托盘所在的显示屏)的 DPI 进行缩放。当 DPI 发生变化时,Windows 将对这些应用程序的用户界面进行位图拉伸,这通常会导致它们模糊不清。在将系统 DPI 感知应用程序更新为按显示器 DPI 感知时,需要更新处理 UI 布局的代码,使其不仅在应用程序初始化时执行,而且在收到 DPI 更改通知(Win32 为 WM_DPICHANGED)时也执行。这通常需要重新审视代码中关于用户界面只需缩放一次的假设。

        此外,在 Win32 编程中,许多 Win32 API 没有任何 DPI 或显示上下文,因此它们只会返回相对于系统 DPI 的值。在代码中搜索这些 API 并将其替换为支持 DPI 的变体可能会有所帮助。具有 DPI 感知变体的一些常见 API 包括:

        在代码库中搜索假定 DPI 恒定的硬编码大小,用正确考虑 DPI 缩放的代码替换它们也是一个好主意。下面是一个包含所有这些建议的示例:

Example:

        下面的示例展示了创建子 HWND 的简化 Win32 案例。对 CreateWindow 的调用假定应用程序以 96 DPI(USER_DEFAULT_SCREEN_DPI 常量)运行,在更高的 DPI 下,按钮的大小和位置都不会正确:

  1. case WM_CREATE:
  2. {
  3. // Add a button
  4. HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",
  5. WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
  6. 50,
  7. 50,
  8. 100,
  9. 50,
  10. hWnd, (HMENU)NULL, NULL, NULL);
  11. }

更新后的代码如下所示:

  1. 窗口创建代码 DPI 根据父窗口的 DPI 调整子 HWND 的位置和大小
  2. 通过调整子 HWND 的位置和大小来响应 DPI 变化
  3. 删除硬编码大小,代之以响应 DPI 变化的代码
  1. #define INITIALX_96DPI 50
  2. #define INITIALY_96DPI 50
  3. #define INITIALWIDTH_96DPI 100
  4. #define INITIALHEIGHT_96DPI 50
  5. // DPI scale the position and size of the button control
  6. void UpdateButtonLayoutForDpi(HWND hWnd)
  7. {
  8. int iDpi = GetDpiForWindow(hWnd);
  9. int dpiScaledX = MulDiv(INITIALX_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
  10. int dpiScaledY = MulDiv(INITIALY_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
  11. int dpiScaledWidth = MulDiv(INITIALWIDTH_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
  12. int dpiScaledHeight = MulDiv(INITIALHEIGHT_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
  13. SetWindowPos(hWnd, hWnd, dpiScaledX, dpiScaledY, dpiScaledWidth, dpiScaledHeight, SWP_NOZORDER | SWP_NOACTIVATE);
  14. }
  15. ...
  16. case WM_CREATE:
  17. {
  18. // Add a button
  19. HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",
  20. WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
  21. 0,
  22. 0,
  23. 0,
  24. 0,
  25. hWnd, (HMENU)NULL, NULL, NULL);
  26. if (hWndChild != NULL)
  27. {
  28. UpdateButtonLayoutForDpi(hWndChild);
  29. }
  30. }
  31. break;
  32. case WM_DPICHANGED:
  33. {
  34. // Find the button and resize it
  35. HWND hWndButton = FindWindowEx(hWnd, NULL, NULL, NULL);
  36. if (hWndButton != NULL)
  37. {
  38. UpdateButtonLayoutForDpi(hWndButton);
  39. }
  40. }
  41. break;

更新系统 DPI 感知应用程序时,需要遵循以下一些常见步骤:

  1. 使用应用程序清单(或其他方法,取决于所使用的用户界面框架)将进程标记为每显示器 DPI 感知 (V2)。
  2. 使用户界面布局逻辑可重复使用,并将其移出应用程序初始化代码,以便在 DPI 发生变化(在 Windows (Win32) 编程中为 WM_DPICHANGED)时可重复使用。
  3. 取消任何假定 DPI 敏感数据(DPI/字体/大小等)永远不需要更新的代码。在进程初始化时缓存字体大小和 DPI 值是一种非常普遍的做法。在更新应用程序以实现每显示器 DPI 感知时,只要遇到新的 DPI,就必须重新评估 DPI 敏感数据。
  4. 当 DPI 发生变化时,应针对新的 DPI 重新加载(或重新光栅化)任何位图资产,或者将当前加载的位图资产拉伸到正确的大小。
  5. 搜索未感知每显示器 DPI 的 API,并将其替换为感知每显示器 DPI 的 API(如适用)。例如:将 GetSystemMetrics 替换为 GetSystemMetricsForDpi。
  6. 在多显示器/多 DPI 系统上测试应用程序。
  7. 对于应用程序中无法更新以正确缩放 DPI 的任何顶级窗口,请使用混合模式 DPI 缩放(如下所述),以允许系统对这些顶级窗口进行位图拉伸。

混合模式 DPI 缩放(子进程 DPI 缩放):

        在更新应用程序以支持每显示器 DPI 感知时,一次性更新应用程序中的每个窗口有时会变得不切实际或不可能。这可能仅仅是因为更新和测试所有用户界面所需的时间和精力,或者因为您并不拥有需要运行的所有用户界面代码(如果您的应用程序可能加载第三方用户界面)。在这种情况下,Windows 提供了一种方法,可以让您在原始 DPI 感知模式下运行部分应用程序窗口(仅顶层),同时集中时间和精力更新 UI 中更重要的部分,从而轻松进入按显示器感知的世界。

        下面是一个示例:您可以更新您的主应用程序用户界面(图中的 “主窗口”),使其在按显示器 DPI 感知模式下运行,同时以现有模式(“辅助窗口”)运行其他窗口。

        在 Windows 10 周年更新(1607)之前,进程的 DPI 感知模式是整个进程的属性。从 Windows 10 周年更新开始,现在可以为每个顶级窗口设置该属性(子窗口必须继续与父窗口的缩放尺寸相匹配)。(顶级窗口被定义为没有父窗口的窗口。这通常是一个带有最小化、最大化和关闭按钮的 “常规 ”窗口。子进程 DPI 意识的目的是在您集中时间和资源更新主用户界面时,由 Windows 对二级用户界面进行缩放(位图拉伸)。

        要启用子进程 DPI 意识,请在任何窗口创建调用前后调用 SetThreadDpiAwarenessContext。创建的窗口将与通过 SetThreadDpiAwarenessContext设置的 DPI 意识相关联。使用第二个调用可恢复当前线程的 DPI 感知。

        虽然使用子进程 DPI 缩放可以让您依靠 Windows 为应用程序完成部分 DPI 缩放,但它会增加应用程序的复杂性。您必须了解这种方法的缺点及其带来的复杂性。有关子进程 DPI 感知的更多信息,请参阅混合模式 DPI 缩放和 DPI 感知 API

测试更改

        在更新应用程序以实现对每显示器 DPI 的感知后,验证应用程序在混合 DPI 环境中是否能正确响应 DPI 变化非常重要。需要测试的一些细节包括

  1. 在不同 DPI 值的显示器之间来回移动应用程序窗口
  2. 在不同 DPI 值的显示器上启动应用程序
  3. 在应用程序运行时更改显示器的比例因子
  4. 更改作为主显示器的显示器,退出 Windows,然后在重新登录后重新测试应用程序。这对查找使用硬编码大小/尺寸的代码特别有用。

本节完。下一节:彻底搞懂 Windows 显示器 DPI 及其相关功能(二、缩放比例感知介绍)

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号