DPI 和 DIP

本文介绍物理像素和设备独立像素(DIP)之间的差异,以及 WIN2D 中处理 DPI(每英寸点数)的方式。

Win2D 的设计方式使许多应用可以忽略此区别,因为它提供了合理的默认行为,在低 DPI 设备和高 DPI 设备上运行时,这些行为将执行正确的操作。 如果你的应用有更特殊的需求,或者你对“合理的默认设置”有不同看法,请继续阅读以了解具体细节……

什么是 DPI?

DPI 是“每英寸点数”的缩写。 这是输出显示器(如计算机监视器或手机屏幕)像素密度的近似度量值。 DPI 越高,构成显示画面的点就越多、越小。

DPI 只是近似度量值,因为并非所有显示硬件都可以依赖来报告准确的信息。 某些计算机监视器根本不向操作系统报告 DPI,或者用户可能已将系统配置为使用与实际硬件不同的 DPI 进行呈现(例如,更改 UI 文本元素的大小)。 应用程序可以使用 DPI 来决定内容应绘制为多大,但不应将其作为显示器尺寸的精确物理测量值。

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

什么是像素?

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

像素的物理大小可能因一个显示器而异。 当计算机连接到尺寸较大但分辨率较低的显示器或外接显示屏时,像素会显得相当大;而在屏幕只有几英寸、分辨率为 1080p 的手机上,像素则非常小。

在 Win2D 中,每当看到使用整数数据类型指定位置或大小的 API(或包含整数的 BitmapSize 等结构)时,这意味着 API 以像素单位运行。

大多数 Win2D API 使用 DIP,而不是像素。

什么是 DIP?

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

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

pixels = dips * dpi / 96

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

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

在 Win2D 中,每当看到使用浮点数据类型指定位置或大小的 API(或包含浮点值的 Vector2 或 Size 等结构)时,这意味着 API 在 DIP 中运行。

若要在 DIP 和像素之间进行转换,请使用 ConvertDipsToPixels(Single, CanvasDpiRounding)ConvertPixelsToDips(Int32) 方法。

具有 DPI 的 Win2D 资源

包含位图图像的所有 Win2D 资源也有关联的 DPI 属性:

所有其他资源类型都独立于 DPI。 例如,单个 CanvasDevice 实例可用于在多个具有不同 DPI 的控件或渲染目标上进行绘制,因此该设备本身没有自己的 DPI。

同样, CanvasCommandList 没有 DPI,因为它包含矢量绘图指令,而不是位图图像。 DPI 仅在光栅化过程中生效,即当命令列表绘制到渲染目标或控件(它们确实具有 DPI)时。

控制 DPI

Win2D 控件(CanvasControlCanvasVirtualControlCanvasAnimatedControl)自动使用与应用正在运行的显示相同的 DPI。 这与 XAML 和其他Windows 运行时 API 使用的坐标系匹配。

如果 DPI 发生变化(例如应用被移动到不同的显示器),控件将引发 CreateResources 事件并传递一个 DpiChangedCanvasCreateResourcesReason。 应用应通过重新创建依赖于控件 DPI 的任何资源(如 rendertargets)来响应此事件。

渲染目标 DPI

可作为绘制目标的对象(这不仅包括 CanvasRenderTarget,还包括类似渲染目标的类型 CanvasSwapChainCanvasImageSource)都有各自的 DPI,但与控件不同,这些类型并不直接连接到显示器,因此 Win2D 无法自动确定 DPI 应该是多少。 如果要绘制到稍后将复制到屏幕的呈现目标,则可能希望呈现目标使用与屏幕相同的 DPI,但如果出于其他目的(例如生成要上传到网站的图像)绘制,则默认的 96 DPI 会更合适。

为了简化这两种使用模式,Win2D 提供了两种类型的构造函数重载:

CanvasRenderTarget(ICanvasResourceCreator, width, height, dpi)
CanvasRenderTarget(ICanvasResourceCreatorWithDpi, width, height)

ICanvasResourceCreator 接口由 CanvasDevice 以及 Win2D 控件实现。 由于设备本身没有其自身的特定 DPI,因此从该设备创建渲染目标时,必须显式指定 DPI。

例如,若要创建默认 DPI 呈现目标,其中 DIP 和像素将始终相同:

const float defaultDpi = 96;
var rtWithFixedDpi = new CanvasRenderTarget(canvasDevice, width, height, defaultDpi);

ICanvasResourceCreatorWithDpi通过添加 DPI 属性扩展了 ICanvasResourceCreator。 此接口由 Win2D 控件实现,并可轻松创建呈现目标,该呈现目标将自动继承与从中创建的控件相同的 DPI:

var rtWithSameDpiAsDisplay = new CanvasRenderTarget(canvasControl, width, height);

位图 DPI

CanvasBitmap与呈现目标不同,不会自动从控件继承 DPI。 创建和加载位图的方法包含用于显式指定 DPI 的重载,但如果省略此项,无论当前显示配置如何,位图 DPI 默认为 96。

位图与其他类型不同的原因是它们是输入数据的来源,而不是将绘制到其上的输出。 因此,对位图来说,关键的不是输出最终显示位置的 DPI,而是源图像的 DPI,而这与当前的显示设置完全无关。

如果你加载一个比如 100x100 的默认 DPI 位图,然后将它绘制到某个渲染目标上,那么该位图会从 96 DPI 下的 100 个 DIP(即 100 像素)缩放为目标渲染目标 DPI 下的 100 个 DIP(如果该渲染目标是高 DPI,则这可能对应更多像素)。 生成的图像大小将始终为 100 DIP(因此不会出现令人不快的布局意外),但如果低 DPI 源位图被缩放到更高的 DPI 目标,可能会出现一些模糊。

为了获得最高 DPI 的清晰度,某些应用程序可能希望在不同的分辨率下提供多个位图图像集,并在加载时选择最接近目标控件 DPI 的版本。 其他应用可能更喜欢只提供高 DPI 位图,并允许 Win2D 在较低的 DPI 显示器上运行时缩小这些位图(纵向缩减通常比纵向扩展更好)。 在任一情况下,都可以将位图 DPI 指定为参数 LoadAsync(ICanvasResourceCreator, String, Single)

请注意,某些位图文件格式包含自己的 DPI 元数据,但 Win2D 会忽略此元数据,因为它通常设置不正确。 相反,加载位图时必须显式指定 DPI。

CanvasDrawingSession DPI

CanvasDrawingSession 从其所绘制到的任何控件、渲染目标、交换链等继承其 DPI。

默认情况下,所有绘图操作都在 DIP 中运行。 如果希望以像素为单位工作,可以通过属性更改 Units 此值。

效果 DPI

图像效果管道从其效果正被绘制到的 CanvasDrawingSession 继承其 DPI。 在内部,效果处理始终以像素为单位运行。 参数值(如大小或位置)在 DIP 中指定,但在进行任何实际图像操作之前,这些单位将转换为像素。

当使用与目标绘制会话 DPI 不同的位图作为效果源图像时,会在位图和效果之间自动插入一个内部的 DpiCompensationEffect。 这会缩放位图以匹配目标 DPI,这通常正是您想要的。 如果这不是你想要的,你可以插入自己的 DpiCompensationEffect 实例来自定义行为。

注释

如果实现自定义效果,请考虑应用等效的 DPI 处理方案,以确保与内置 Win2D 效果一起使用时的行为一致。

合成 API

Microsoft.Graphics.Canvas.Composition API 在低于 Win2D XAML 控件的水平运行,因此它们不会尝试代表你自动处理 DPI。 由你决定偏好使用哪种单位,并设置必要的变换作为组合视觉树的一部分来实现这一点。

组合 API,例如 CreateDrawingSurface 始终以像素单位指定大小。 使用 Win2D 在合成图面上绘制时,可以在调用 CreateDrawingSession(CompositionDrawingSurface, Rect, Single) 时指定所需的任意 DPI。 通过返回的 CanvasDrawingSession 执行的所有绘图操作都将相应地按比例放大或缩小。

如何测试 DPI 处理

测试应用处理正确显示 DPI 的最简单方法是在Windows 10或Windows 11上运行,并在应用运行时更改显示比例:

  1. 右键单击桌面背景,然后选择 “显示设置”。
  2. “缩放与布局”下,更改缩放比例百分比(例如,从 100% 更改为 150%)。
  3. 你的应用会收到 DPI 更改通知,应自动调整。

还可以通过连接具有不同 DPI 设置的监视器并在它们之间拖动应用窗口进行测试。