Featured image of post Windows DPI 深入探究

Windows DPI 深入探究

深入介绍 DPI 和显示缩放等关键概念

核心概念

什么是 DPI

每英寸点数 (DPI) 是显示器的线性英寸中像素数的物理测量值。 DPI 取决于显示器分辨率和尺寸;DPI 越高,构成显示器的点越多、越小。较高的分辨率或较小的尺寸会导致较高的 DPI,较低的分辨率或较大的尺寸会导致较低的 DPI。 当显示器具有较高的 DPI 时,像素较小且更靠近,因此用户界面 (UI) 和其他显示内容看起来比预期的要小。

DPI 值 96 被视为中性默认值。

什么是像素

像素是一个彩色点。 计算机图形中的图像由排列在二维网格中的许多像素组成。 可以将像素视为生成所有图像的原子。

像素的物理大小因显示器而异。 当计算机连接到大尺寸但分辨率较低的显示器或外部显示器时,像素可能相当大,但在具有 1080p 的手机上,只有几英寸宽,像素很小。

像素的大小取决于两个因素:显示分辨率和监视器的物理大小。 因此,物理英寸不是有用的度量值,因为物理英寸和像素之间没有固定的关系。相反,字体以 逻辑 单位度量。 72 磅字体定义为一个逻辑英寸高。 逻辑英寸随后转换为像素。 多年来,Windows 使用以下转换:1 个逻辑英寸等于 96 像素。 使用此比例系数,72 磅字体呈现为 96 像素高。 12 磅字体高 16 像素。

此比例系数描述为每英寸 96 点 (DPI) 。 术语点派生自打印,其中物理墨点放在纸上。 对于计算机显示器,说每逻辑英寸 96 像素会更准确,但术语 DPI 停滞不前。

由于实际像素大小各不相同,因此在另一台监视器上可读的文本可能太小。 此外,Windows 允许用户更改 DPI 设置。 例如,如果用户将显示器设置为 144 DPI,则 72 磅字体的高度为 144 像素。 标准 DPI 设置为 100% (96 DPI) 、125% (120 DPI) 和 150% (144 DPI) 。

什么是 DIP

与设备无关的像素(DIP)。 这是一个虚拟化单元,可能与物理像素相同、更大或小于物理像素。

像素和 DIP 之间的比率由 DPI 确定:

1
pixels = dips * dpi / 96

当 DPI 为 96 时,像素和 DIP 相同。 使用更高的 DPI 时,单个 DIP 可能对应于多个像素 (或部分像素,在 DPI 不是 96) 的确切倍数的常见情况下。

大多数Windows 运行时 API(包括 Win2D)使用 DIP 而不是像素。 这样做的优点是,无论在什么显示器上运行应用,图形的物理大小都保持大致相同。 例如,如果应用指定一个按钮的宽度为 100 DIP,则当在高 DPI 设备(如手机或 4k 监视器)上运行时,此按钮将自动缩放为宽度超过 100 像素,因此它仍然是用户可单击的合理大小。 另一方面,如果以像素为单位指定了按钮大小,则这种高 DPI 显示器上的按钮大小会显得非常小,因此应用必须执行更多工作才能针对每种屏幕以不同的方式调整布局。

什么是 Windows 比例系数

Windows 通过指示应用程序(包括 Windows 桌面 shell)按比例系数调整内容的大小,确保所有内容以可用且一致的大小显示在屏幕上。 这个数字取决于显示器 DPI 以及影响用户对显示器的感知的其他因素。 几乎所有台式机显示器和目前大多数笔记本电脑显示器都在 95-110 DPI 范围内;对于这些设备,无需缩放,并且 Windows 将比例系数设置为 100%。 但是,许多新设备(特别是在高端笔记本电脑和平板电脑市场)具有超过 200 DPI 的更高显示。 对于这些设备,Windows 设置了更高的比例系数,以确保用户体验的舒适性和可视性。

DPI 感知模式

Windows 桌面应用大致分为两类:DPI 感知应用和非 DPI 感知应用。 DPI 感知应用在应用程序启动期间会主动告知 Windows 它们能够自我缩放以便在高 DPI 显示器上正常工作。但是,如果某个应用程序不具备 DPI 感知能力并在高 DPI 显示器上运行,则 Windows 会通过对应用程序输出应用位图缩放来缩放该应用。 这可以确保该应用程序在高 DPI 显示器上以正确的大小显示。 在大多数情况下,这可以使应用程序保持清晰且可正常使用,但在某些情况下,位图缩放会导致应用程序不够清晰,并呈现略微模糊的外观。

桌面应用程序必须告知 Windows 它们是否支持 DPI 缩放。 默认情况下,系统将桌面应用程序 DPI 视为无法感知,并对其窗口进行位图拉伸。

无法感知 DPI

无法感知 DPI 的应用程序以固定 DPI 值 96 (100%) 进行呈现。 每当这些应用程序在显示比例大于 96 DPI 的屏幕上运行时,Windows 会将应用程序位图拉伸到预期的物理大小。 这会导致应用程序显示模糊。

系统 DPI 感知

可感知系统 DPI 的桌面应用程序通常在用户登录时接收主连接监视器的 DPI。 在初始化期间,它们使用该系统 DPI 值来适当地设置 UI 布局(调整控件大小、选择字号、加载资产等)。 因此,在以单个 DPI 呈现的显示器上,可感知系统 DPI 的应用程序不会由 Windows 进行 DPI 缩放(位图拉伸)。 当应用程序移动到具有不同缩放因子的显示器时,或者如果显示比例因子发生其他更改,Windows 将对应用程序的窗口进行位图缩放,使其显示模糊。 实际上,可感知系统 DPI 的桌面应用程序仅以单个显示缩放因子清晰呈现,每当 DPI 发生更改时,这些应用程序都会变得模糊。

高DPI问题

显示缩放因子 & DPI

随着显示技术的进步,显示面板制造商在其面板上的每个物理空间单元中都封装了越来越多的像素。 这导致新式显示面板的每英寸点数 (DPI) 远高于历史上的点数。 过去,大多数显示器每线性英寸的物理空间有 96 个像素 (96 DPI):2017 年,具有近 300 DPI 或更高 DPI 的显示器随处可见。

大多数旧版桌面 UI 框架都有内置的假设,即显示 DPI 在进程的生存期内不会改变。 这种假设不再成立,在应用程序进程的整个生存期内,显示 DPI 通常会更改多次。 显示缩放因子/DPI 更改的一些常见场景如下:

  • 多监视器设置,其中每个显示器具有不同的缩放因子,应用程序从一个显示器移动到另一个显示器(如 4K 和 1080p 显示器)
  • 将高 DPI 笔记本电脑与低 DPI 外部显示器连接和分离(反之亦然)
  • 通过远程桌面从高 DPI 笔记本电脑/平板电脑连接到低 DPI 设备(反之亦然)
  • 在应用程序运行时更改显示缩放因子设置

DPI 缩放比例

DPI比例因子
96100
120125
144150
192200

DWM 缩放

如果程序未考虑 DPI,则高 DPI 设置中可能显示以下缺陷:

  • 已剪裁的 UI 元素。
  • 布局不正确。
  • 像素化的位图和图标。
  • 鼠标坐标不正确,可能会影响命中测试、拖放等。

为了确保较旧的程序在高 DPI 设置下工作,DWM 实现了有用的回退。 如果程序未标记为 DPI 感知,DWM 将缩放整个 UI 以匹配 DPI 设置。 例如,在 144 DPI 下,UI 按 150% 缩放,包括文本、图形、控件和窗口大小。 如果程序创建一个 500 × 500 窗口,则窗口实际上显示为 750 × 750 像素,并且窗口的内容会相应地缩放。

此行为意味着较旧的程序在高 DPI 设置下“只工作”。 但是,缩放也会导致一些模糊的外观,因为缩放是在绘制窗口后应用的。

DPI 感知应用程序

若要避免 DWM 缩放,程序可以将自身标记为 DPI 感知。 这会告知 DWM 不执行任何自动 DPI 缩放。 所有新应用程序都应设计为 DPI 感知,因为 DPI 感知可改善 UI 在较高 DPI 设置下的外观。

程序通过其应用程序清单声明自己的 DPI 感知。 清单只是描述 DLL 或应用程序的 XML 文件。 清单通常嵌入到可执行文件中,尽管它可以作为单独的文件提供。 清单包含 DLL 依赖项、请求的权限级别以及程序设计用于的 Windows 版本等信息。

若要声明程序可识别 DPI,请在清单中包含以下信息。

1
2
3
4
5
6
7
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
  <asmv3:application>
    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
      <dpiAware>true</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

此处显示的列表只是部分清单,但 Visual Studio 链接器会自动为你生成清单的其余部分。 若要在项目中包括部分清单,请在 Visual Studio 中执行以下步骤。

  1. 在“ 项目 ”菜单上,单击“ 属性”。
  2. 在左窗格中,依次展开“ **配置属性”**和“ 清单工具”,然后单击“ 输入和输出”。
  3. 在“ 其他清单文件 ”文本框中,键入清单文件的名称,然后单击“ 确定”。

通过将程序标记为 DPI 感知,可以告知 DWM 不要缩放应用程序窗口。 现在,如果创建 500 × 500 窗口,则无论用户的 DPI 设置如何,该窗口都将占用 500 × 500 像素。

GDI 和 DPI

GDI 绘图以像素为单位测量。 这意味着,如果程序标记为 DPI 感知,并且你要求 GDI 绘制一个 200 × 100 矩形,生成的矩形将在屏幕上为 200 像素宽,100 像素高。 但是,GDI 字体大小将缩放为当前 DPI 设置。 换句话说,如果创建 72 磅字体,则字体大小将为 96 像素(96 DPI),但在 144 DPI 时为 144 像素。 下面是使用 GDI 以 144 DPI 呈现的 72 磅字体。

显示 gdi 中 dpi 字体缩放的示意图。

如果应用程序可识别 DPI,并且使用 GDI 进行绘图,请缩放所有绘图坐标以匹配 DPI。

Direct2D 和 DPI

Direct2D 会自动执行缩放以匹配 DPI 设置。 在 Direct2D 中,坐标以称为 设备无关像素 (DIP) 单位进行测量。 DIP 定义为 逻辑 英寸的 1/96。 在 Direct2D 中,所有绘图操作都在 DIP 中指定,然后缩放到当前 DPI 设置。

DPI 设置DIP 大小
961 像素
1201.25 像素
1441.5 像素

例如,如果用户的 DPI 设置为 144 DPI,并且你要求 Direct2D 绘制一个 200 × 100 矩形,则矩形将为 300 × 150 个物理像素。 此外,DirectWrite以 DIP 而不是磅为单位衡量字号。 若要创建 12 磅字体,请指定 16 个 DIP (12 磅 = 1/6 逻辑英寸 = 96/6 个 DIP) 。 在屏幕上绘制文本时,Direct2D 会将 DIP 转换为物理像素。 此系统的优点是无论当前 DPI 设置如何,文本和绘图的度量单位都是一致的。

注意:鼠标和窗口坐标仍以物理像素表示,而不是 DIP。 例如,如果处理 WM_LBUTTONDOWN 消息,则鼠标按下位置以物理像素为单位。 若要在该位置绘制点,必须将像素坐标转换为 DIP。

将物理像素转换为 DIP

DPI 的基值定义为 USER_DEFAULT_SCREEN_DPI 设置为 96。 若要确定比例系数,请取 DPI 值并除以 USER_DEFAULT_SCREEN_DPI

从物理像素到 DIP 的转换使用以下公式。

1
DIPs = pixels / (DPI / USER_DEFAULT_SCREEN_DPI)

若要获取 DPI 设置,请调用 GetDpiForWindow 函数。 DPI 作为浮点值返回。 计算两个轴的缩放系数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
float g_DPIScale = 1.0f;

void InitializeDPIScale(HWND hwnd)
{
    float dpi = GetDpiForWindow(hwnd);
    g_DPIScale = dpi / USER_DEFAULT_SCREEN_DPI;
}

template <typename T>
float PixelsToDipsX(T x)
{
    return static_cast<float>(x) / g_DPIScale;
}

template <typename T>
float PixelsToDips(T y)
{
    return static_cast<float>(y) / g_DPIScale;
}

如果不使用 Direct2D,下面是获取 DPI 设置的替代方法:

1
2
3
4
5
6
7
void InitializeDPIScale(HWND hwnd)
{
    HDC hdc = GetDC(hwnd);
    g_DPIScaleX = GetDeviceCaps(hdc, LOGPIXELSX) / USER_DEFAULT_SCREEN_DPI;
    g_DPIScaleY = GetDeviceCaps(hdc, LOGPIXELSY) / USER_DEFAULT_SCREEN_DPI;
    ReleaseDC(hwnd, hdc);
}

对于桌面应用,建议使用 GetDpiForWindow;对于通用 Windows 平台 (UWP) 应用,请使用 DisplayInformation::LogicalDpi。 尽管我们不建议这样做,但可以使用 SetProcessDpiAwarenessContext 以编程方式设置默认 DPI 感知。 一旦在进程中创建了一个窗口 (HWND) ,就不再支持更改 DPI 感知模式。 如果要以编程方式设置进程默认 DPI 感知模式,则必须在创建任何 HWND 之前调用相应的 API。 有关详细信息,请参阅 设置进程的默认 DPI 感知

调整呈现器目标的大小

如果窗口的大小发生更改,则必须调整呈现目标的大小以匹配。 在大多数情况下,还需要更新布局并重新绘制窗口。 下面的代码演示了这些步骤。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void MainWindow::Resize()
{
    if (pRenderTarget != NULL)
    {
        RECT rc;
        GetClientRect(m_hwnd, &rc);

        D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom);

        pRenderTarget->Resize(size);
        CalculateLayout();
        InvalidateRect(m_hwnd, NULL, FALSE);
    }
}

GetClientRect 函数获取工作区的新大小(以物理像素为单位), (而不是 DIP) 。 ID2D1HwndRenderTarget::Resize 方法更新呈现器目标的大小(也以像素为单位指定)。 InvalidateRect 函数通过将整个工作区添加到窗口的更新区域来强制重新绘制。 (请参阅模块 1.) 中的绘制窗口

随着窗口的增大或缩小,通常需要重新计算所绘制的对象的位置。 例如,在圆形程序中,必须更新半径和中心点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void MainWindow::CalculateLayout()
{
    if (pRenderTarget != NULL)
    {
        D2D1_SIZE_F size = pRenderTarget->GetSize();
        const float x = size.width / 2;
        const float y = size.height / 2;
        const float radius = min(x, y);
        ellipse = D2D1::Ellipse(D2D1::Point2F(x, y), radius, radius);
    }
}

ID2D1RenderTarget::GetSize 方法以 DIP (而不是像素) 返回呈现器目标的大小,这是用于计算布局的适当单位。 有一个密切相关的方法 ID2D1RenderTarget::GetPixelSize,它以物理像素为单位返回大小。 对于 HWND 呈现目标,此值与 GetClientRect 返回的大小匹配。 但请记住,绘制以 DIP 而不是像素执行。

如何判断应用程序是否不具备 DPI 感知能力

使用进程浏览器工具确定应用是否具备 DPI 感知能力。 图 1 进程资源管理器显示此实用工具正在使用,其中启用了 DPI 感知列。 (默认情况下,进程资源管理器不显示 DPI 感知列。若要打开此列,请单击“视图”菜单,单击“选择列”,选中“DPI 感知”框,单击“确定”。)标题为 感知的列会告诉你某个特定进程是否感知到 DPI。

process explorer - sysinternals

图 1:进程浏览器

Windows 区分三类应用程序。

表 1:DPI 感知应用

DPI 感知示例行为
无法感知Mmc.exe(Microsoft 管理控制台及其插件)Windows 位图根据连接到系统的任何高 DPI 显示器缩放应用程序;可以按 125% 和 150% 的缩放因子模糊显示应用程序。
系统感知Office 应用应用程序在启动时按系统 DPI 进行缩放(通常与主显示器 DPI 相同);Windows 将应用缩放到与此不匹配的任何显示器。
按监视器感知(Per-Monitor DPI 感知)Internet Explorer 11应用程序根据显示器 DPI 动态缩放自身。

如何测试 DPI 处理

测试应用是否对更改显示 DPI 执行正确操作的最简单方法是在Windows 10或Windows 11上运行,并在应用运行时更改显示设置:

  • 右键单击桌面背景,然后选择“显示设置”
  • 移动标记为“更改文本、应用和其他项目的大小”的滑块
  • 单击“应用”按钮
  • 选择“稍后注销”

如果没有Windows 10或Windows 11,还可以使用 Windows 模拟器进行测试。 在 Visual Studio 工具栏中,将“本地计算机”设置更改为“模拟器”,然后使用“更改分辨率”图标在以下两者之间切换模拟显示:

  • 100% (DPI = 96)
  • 140% (DPI = 134.4)
  • 180% (DPI = 172.8)

总结

关于 Qt 高分辨率的适配,通常比较复杂,了解这些前置知识,在一定程度上会有帮助。

https://learn.microsoft.com/zh-cn/windows/apps/develop/win2d/dpi-and-dips

https://learn.microsoft.com/zh-cn/windows-hardware/manufacture/desktop/fixing-blurry-text-in-windows-for-it-professionals?view=windows-11

https://learn.microsoft.com/zh-cn/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows

https://learn.microsoft.com/zh-cn/windows/win32/learnwin32/dpi-and-device-independent-pixels

Licensed under CC BY-NC-SA 4.0