DPI 和与设备无关的像素

若要使用 Windows 图形进行有效编程,必须了解两个相关概念:

  • 每英寸点数 (DPI)
  • 与设备无关的像素 (DIP) 。

让我们从 DPI 开始。 这将需要一个短暂的绕道进入版式。 在版式中,类型的大小以称为 的单位度量。 1 分等于 1/72 英寸。

1 pt = 1/72 英寸

注意

这是点的桌面发布定义。 从历史上看,点的确切度量值各不相同。

例如,12 磅字体设计为适合 1/6 英寸 (12/72) 行文本。 显然,这并不意味着字体中的每个字符都是 1/6 英寸高。 事实上,某些字符可能高于 1/6 英寸。 例如,在许多字体中,字符 Å 比字体的名义高度高。 若要正确显示,字体在文本之间需要一些额外的空格。 此空间称为 前导

下图显示了 72 磅字体。 实线在文本周围显示一个 1 英寸高的边框。 虚线称为 基线。 字体中的大多数字符都位于基线上。 字体的高度包括基线上方的部分 (上升) ,以及低于基线的部分 (下降) 。 在此处显示的字体中,上升为 56 磅,下降为 16 磅。

显示 72 磅字体的插图。

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

12 磅 = 12/72 逻辑英寸 = 1/6 逻辑英寸 = 96/6 像素 = 16 像素

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

由于实际像素大小各不相同,因此在另一台监视器上可读的文本可能太小。 此外,用户有不同的偏好-有些人更喜欢较大的文本。 因此,Windows 允许用户更改 DPI 设置。 例如,如果用户将显示器设置为 144 DPI,则 72 磅字体的高度为 144 像素。 标准 DPI 设置为 100% (96 DPI) 、125% (120 DPI) 和 150% (144 DPI) 。 用户还可以应用自定义设置。 从 Windows 7 开始,DPI 是每用户设置。

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,请在清单中包含以下信息。

<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 大小
96 1 像素
120 1.25 像素
144 1.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 的转换使用以下公式。

DIPs = pixels / (DPI / USER_DEFAULT_SCREEN_DPI)

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

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 设置的替代方法:

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 感知

调整呈现器目标的大小

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

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.) 中的绘制窗口

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

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 而不是像素执行。

下一步

在 Direct2D 中使用颜色