Win2D 官方文章系列翻译 - DPI (每英寸点数)和 DIPs(设备独立像素)

时间:2021-02-07 16:58:57

本文为个人博客备份文章,原文地址:

http://validvoid.net/win2d-dpi-dips/

本文旨在解释物理像素与设备独立像素(DIPs, device independent pixels)之间的区别,以及 Win2D 如何处理 DPI (dots per inch/每英寸点数)。

Win2D 被设计为可以自行感知设备环境,以便在各种高低不同 DPI 的设备上呈现正确的视觉表现,故多数应用可以忽略 DIPs 和 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 的倍数)。

包括 Win2D,大部分 Windows 运行时 API 以 DIP 而非像素作为基准进行操作。这样有利于保证无论应用运行在怎样的显示设备上,图形都已近似相等的物理大小进行呈现。例如应用指定了一个 100 DIP 宽的按钮,当应用运行在手机或 4k 显示器等高 DPI 设备上时,该按钮的物理像素宽度会自动缩放到大于 100 像素,以便用户能够进行点击。而如果按钮的大小以像素为单位进行设置,在此类高 DPI 显示设备上,按钮就会变得出奇的小,应用也就得花费更多功夫为不同类型的屏幕进行布局调整。

在 Win2D 中,当你看到 API 使用浮点数据类型(或一个 Vector2 或 Size 这样封装浮点值成员的结构)指定了位置或大小都意味着 API 在以 DIP 为单位进行操作。

要在 DIP 和像素之间进行转换,可以使用 ConvertDipsToPixels(Single, CanvasDpiRounding) 和 ConvertPixelsToDips(Int32) 这两个方法。

拥有 DPI 的 Win2D 资源

所有包含位图图像的 Win2D 资源都有一个 DPI 属性:

其它所有资源都是独立于 DPI 的。例如一个 CanvasDevice (画布设备)实例可被用于绘制多种不同 DPI 的控件或渲染目标(rendertarget),因此设备本身没有 DPI 属性。

与之相似,CanvasCommandList (画布命令列表)也没有 DPI 属性,因为它包含了矢量绘图指令而非位图。 当命令列表被绘制到一个渲染目标(rendertarget)或控件时,DPI 才在栅格化过程中发挥作用。

控件 DPI

Win2D 控件(CanvasControlCanvasVirtualControl 和 CanvasAnimatedControl)在应用当前运行的显示设备中自动使用相同的 DPI。这与 XAML、CoreWindow 以及其它 Windows 运行时 API 所用的坐标系一致。

如果 DPI 发生改变(例如应用移动到了另一个显示设备),控件会触发 CreateResources事件并传递一个值为 DpiChanged 的 CanvasCreateResourcesReason 类型枚举参数。应用应该在事件响应中重新创建任何依赖于控件 DPI 的资源(比如 rendertargets)。

Rendertarget (渲染目标) DPI

可作为绘制目标的对象(除了 CanvasRenderTarget 还有 CanvasSwapChain 和CanvasImageSource 等类 rendertarget 类型)自身也有 DPI 属性。但不同于控件,这些类型并不直接关联于显示设备,所以 Win2D 无法自动为其指定最佳 DPI。如果你将内容绘制到一个稍后将输出到屏幕的渲染目标(rendertarget) ,你应该使用与屏幕相同的 DPI;而如果你是为其它目的进行绘制(例如生成图片用于上传到网站),则推荐使用默认的 96 DPI。

为了方便此类使用需求, Win2D 提供了两种构造函数重载:

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

CanvasDevice 和 Win2D 控件均实现了 ICanvasResourceCreator 接口。因为设备本身并没有指定任何 DPI,故你在创建一个渲染目标(rendertarget)时必须显式指定 DPI。例如,创建一个 DIP 与像素总是相等的默认 DPI 渲染目标:

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

ICanvasResourceCreatorWithDpi 接口通过添加一个 DPI 属性扩展了 ICanvasResourceCreator 接口。Win2D 控件实现了这一接口,从而简化了创建一个从来源控件自动继承 DPI 的渲染目标(rendertarget)的流程:

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

位图 DPI

不同于渲染目标(rendertarget), CanvasBitmap 并不会自动从来源控件继承 DPI 值。创建和加载位图的方法包含多个可以显式指定 DPI 的重载,但如果你不手动指定 DPI 值,则位图 DPI 会无视当前显示设备的配置而自动设置为默认的 96。

位图之所以不同于其它类型,是由于它们是输入数据源,而非用于绘制的输出目标。所以对于位图而言,关键在于输入图像的 DPI 而非输出目标的 DPI。输入图像的 DPI 与当前显示设备的 DPI 设置是完全无关的。

如果你有一张尺寸为 100x100 ,DPI 默认的位图,把它绘制到一个渲染目标(rendertarget),位图会从 96 DPI 的 100 DIP 大小(也就是 100 像素)缩放到渲染目标 DPI 的 100 DIP 大小(如果渲染目标的 DPI 大于位图原 DPI,则输出像素会大于 100 像素)。最后的结果图像总是会保持 100 DIP 的尺寸(所以也不会有任何意料外的布局异常),但如果源位图 DPI 较低,缩放后的高 DPI 目标图像会模糊。

为保证高 DPI 情况下的最佳清晰度,某些应用可能希望为不同分辨率提供多套位图图像,并在加载时选择与目标控件 DPI 最匹配的资源。而另外一些应用则选择只封装高 DPI 位图,在运行于低 DPI 显示设备时,让 Win2D 完成缩小工作(缩小后的效果总比放大要好)。上述两种情况中,位图 DPI 均可作为 LoadAsync(ICanvasResourceCreator, String, Single) 方法的参数进行指定。

注意某些位图文件格式本身自带 DPI 元数据,但考虑到这些元数据经常不准确,Win2D 在加载时会忽略它们。相对地,在加载位图时,DPI 必须显式指定。

CanvasDrawingSession DPI

CanvasDrawingSession 从作为其绘制目标的任意控件、渲染目标(rendertarget)、swapchain 等继承 DPI。

默认情况下,所有绘图操作都是基于 DIP 完成的。如果你想使用像素进行操作,可以通过更改 Units 属性实现。

如何测试应用的 DPI 应对情况

测试你的应用是否正确应对 DPI 变化最简单的办法就是在 Windows 10 环境下运行你的应用,然后在运行期间更改显示设置:

  • 右键单击桌面背景,选择 显示选项
  • 拖动 更改文本、应用和其它项目大小 滑块
  • 单击 '应用' 按钮
  • 选择 '稍后注销'

如果你没有 Windows 10 环境,你也可以通过 Windows 模拟器进行测试。在 Visual Studio 工具栏上,将调试目标从 本机 修改为 模拟器,然后点击模拟器上修改分辨率的按钮在以下模拟分辨率之间进行切换:

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