DPI 和设备无关的像素
若要使用 Windows 图形进行有效编程,必须了解两个相关概念:
- 每英寸点数 (DPI)
- 与设备无关的像素(DIP)。
让我们从 DPI 开始。 这需要短暂的排版。 在版式中,类型的大小以 磅单位进行度量。 一磅等于一英寸的 1/72。
- 1 pt = 1/72 英寸
注意
这是点的桌面发布定义。 从历史上看,点的确切度量值各不相同。
例如,12 磅字体设计为适合 1/6 英寸(12/72)文本行。 显然,这并不意味着字体中的每个字符都只有 1/6 英寸高。 事实上,某些字符可能高于 1/6”。 例如,在许多字体中,字符 Å 比字体的名义高度高。 若要正确显示,字体需要文本之间的一些额外空间。 此空间称为前导 。
下图显示了一个 72 磅字体。 实线显示文本周围的 1 英寸高边界框。 虚线称为 基线。 字体中的大多数字符都位于基线上。 字体的高度包括基线上方的部分(上升)和基线下方的部分(下降)。 在此处显示的字体中,上升为56分,下降为16磅。
但是,当涉及到计算机显示器时,测量文本大小是有问题的,因为像素的大小并不相同。 像素的大小取决于两个因素:显示分辨率和监视器的物理大小。 因此,物理英寸不是有用的度量值,因为物理英寸和像素之间没有固定的关系。 相反,字体以逻辑 单位 度量。 72 磅字体定义为一个逻辑英寸高。 逻辑英寸随后转换为像素。 多年来,Windows 使用以下转换:一个逻辑英寸等于 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 中执行以下步骤。
- 在 项目 菜单上,单击 属性。
- 在左窗格中,展开 配置属性,展开 清单工具,然后单击 输入和输出。
- 在“其他清单文件 文本框中,键入清单文件的名称,然后单击 ”确定“。
通过将程序标记为 DPI 感知,可以告知 DWM 不要缩放应用程序窗口。 现在,如果创建 500 × 500 窗口,无论用户的 DPI 设置如何,该窗口都将占用 500 × 500 像素。
GDI 和 DPI
GDI 绘图以像素为单位测量。 这意味着,如果程序标记为 DPI 感知,并且你要求 GDI 绘制一个 200 × 100 个矩形,生成的矩形将在屏幕上宽度为 200 像素和 100 像素高。 但是,GDI 字号将缩放为当前的 DPI 设置。 换而言之,如果创建 72 磅字体,则字体大小为 96 像素,为 96 DPI,但 144 像素为 144 DPI。 下面是使用 GDI 以 144 DPI 呈现的 72 磅字体。
如果应用程序感知 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 的基值定义为设置为 96 USER_DEFAULT_SCREEN_DPI
。 若要确定缩放系数,请采用 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 PixelsToDipsY(T y)
{
return static_cast<float>(y) / g_DPIScale;
}
如果未使用 Direct2D,则下面是获取 DPI 设置的替代方法:
void InitializeDPIScale(HWND hwnd)
{
HDC hdc = GetDC(hwnd);
g_DPIScaleX = (float)GetDeviceCaps(hdc, LOGPIXELSX) / USER_DEFAULT_SCREEN_DPI;
g_DPIScaleY = (float)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 方法返回呈现目标的大小(而不是像素),这是计算布局的适当单位。 有一个密切相关的方法,ID2D1RenderTarget::GetPixelSize,以物理像素为单位返回大小。 对于 HWND 呈现目标,此值与 getClientRect返回的大小匹配。 但请记住,绘图是在 DIP 中执行的,而不是像素。
下一个
在 Direct2D 中使用颜色