为什么会出现High DPI问题

首先解释什么是DPI。DPI是Dots Per Inch缩写,表示每英寸点数,这里的点(Dot)指什么?对面向PC、手机、Pad编程的程序员来说,它等同像素(Pixel),于是DPI就等同PPI。
当前流行的DPI是多少,让以两款电脑为例有个直观认识。一商务笔记本,13.3×8.3英寸屏,最大分辨率1920×1080,由它可算出水平、垂直DPI大概是144(1920/13.3)、130(1080/8.3)。苹果的一款MacBook Air,13.3×8.3英寸屏,最大分辨率1440×900,它水平、垂直DPI分别是108、108。
接下说苹果营销出的一个概念:Retina屏(视网膜屏)。单从DPI这个参数来说,Retina屏是指它的DPI超过人眼能识别极限的最高DPI,我们不关心这个DPI是不是真的超过人眼分辨极限,但不得不承认的是,Retina屏的DPI的确是基本做到了普通屏的2倍。举个例子,当前一款MacBook Pro,13.3×8.3英寸屏,最大分辨率2560×1600,那它的水平、垂直DPI到达了192,基本是同期Air的两倍。
现在来讨论同一数量像素下在Air和Pro分别是多少英寸。假设有一个按钮,程序设定它是360×360像素,那它在Air的实际尺寸是3.33×3.33英寸/8.46cm,在Pro则是1.87×1.87英寸/4.76cm。——到此对High DPI问题会有初步认识了:由于不同DPI,使得同一App在不同显示器上显示时会出现很不一样的物理尺寸。就拿这两款苹果笔记本,Air上看去正常的按钮,换Retina上就小很多。
以上讨论是电脑屏,手机、Pad屏也一样存在DPI。例如iPhone6,对角线是4.7英寸(屏尺寸大概是4.09×2.3),它的水平、垂直DPI都是326。仍然以360×360按钮为例,在iPhone6上则变成1.1×1.1英寸(2.8cm)。以下表格小结了同一像素数在不同设备上的物理尺寸。

High DPI问题根源是用户在意的是物理单位表示的尺寸,程序员关注的是像素表示的尺寸,而关联物理单位和像素的DPI在各设备不同,使得要能让同一App在这些设备都能正常使用的话须进行额外处理。

解决High DPI方法:放大倍数(hdpi_scale)
让思考这样一个问题,对同一个按钮,如何让在MacBook Air、MacBook Pro看去是同一物理尺寸?很快能想到办法,按钮的像素数在Pro时是Air像素数乘上2。办法虽然简单,但的确是各操作系统在使用的解决High DPI方法。
那放大倍数到底是多少,有些是操作系统决定,像iOS,它规定iPhone 5/6是2。Android、Windows则由程序员自个定,不过为缩放图像简单,真正到显示时倍数要取整数。以下是Android定义的density(基本等同hdpi_scale)、DPI值之间关系,它用了一个等式:DPI = density * 160。

density是浮点数,转换到hdpi_scale可用四舍五入法,即小数部分>=0.4时向上取整,否则向下取整。

对Windows系统一般取2,一来是考虑到它的DPI相比手机来说不是很大,二来Windows有个High DPI-unaware概念,对属于High DPI-unaware类型App,它会默认用2去放大。接下就让说说High DPI-unaware。Windows把App分为两类:High DPI-aware和High DPI-unaware,App可调用SetProcessDpiAwareness(Win8.1开始出现)这个API让自个归属到哪种类型。

High DPI-unaware是告诉系统,我这App不支持高清屏,如遇上高清屏请通过DWM(Desktop Window Manager)虚拟化帮我去实现。此种方式的高DPI支持具体过程是这样的,比如当前系统的DPI是192(200%),程序运行时,系统会告诉你当前DPI仍然是96(100%),所以程序会仍然按照96的方式进行绘画,而且调用系统API时,得到的坐标、窗口尺寸都是按96来的。当我们画完之后,DWM再对整个窗口进行200%放大后画到屏幕上,这样看起来程序就自动支持高DPI了。 这种方式对那些非高清屏上开发却依旧要在清高清屏运行的App很有用,像在XP、Win7开发的App,正因为如此,App默认的工作方式是High DPI-unaware。
这种方式看起来很美妙,但有严重缺点,经过放大后的内容看起来会变模糊,比如文字会有明显锯齿。既然DWM虚拟化用户效果不怎么好,那么很多时候就得向操作系统宣称自个能支持高DPI,请直接告诉我真实DPI,至于遇到的屏是192还是108,我自个去处理,这种类型的App就是High DPI-aware。
虽然不叫DWM,iOS也有类似这样功能的模块,它也可把App分为那两类。不过由于DWM会导致图像变模糊这个致命缺点,往后程序必将都是High DPI-aware。
回到方法的源头,这会引入两个概念,配置尺寸和显示尺寸。

  • 配置尺寸(Config Size):定义控件时使用的尺寸。设定值时参考一条规则:能满足最小屏幕分辨率的DPI。
  • 显示尺寸(Display Size):控件最终显示出来的尺寸。是配置尺寸乘上放大倍数,在值上,放大倍数等于1时就是配置尺寸。

QT 如何支持高清屏,视网膜屏幕

原文链接:Morten Johan S?rvig – Retina display support for Mac OS, iOS and X11

Qt 5.0中添加了对于retina显示的基本支持。即将到来的Qt 5.1中提供了新的API和缺陷修复,对于这一问题进行了改进。Qt 4.8也获得了良好的支持,我们反向移植了一些Qt 5的补丁。

尽管这些实现的努力和Mac以及iOS程序员最为相关,但是来看一看其它平台是如何处理高DPI显示这一问题,也是很有趣的。这里主要有两种方式:

  • 基于DPI缩放——Win32 GDIKDE。在这种方式中,应用程序在全物理设备分辨率下工作,使用系统提供的一个DPI设定或者缩放因子,用于缩放布局。字体通常会被操作系统自动缩放(只要您使用点数(point)而不是像素(pixel)来指定字体大小)
  • 另一种意义的像素。在这种方式中,应用程序并不知道物理解析度(在任何程度上)。物理像素被逻辑像素替代:
平台/API 逻辑的 物理的
HTML CSS像素 设备像素
Apple 像素
Android 密度无关像素(dp) (屏幕)像素
Direct2D 设备无关像素(DIP) 物理像素
Qt(过去) 像素 像素
Qt(现在) 设备无关像素 设备像素

在历史上,Qt已经支持基于DPI缩放的物理像素这一方式。在2009年时,对于Windows上的高DPI值的支持已经有所改进。Qt布局对于增加的DPI并没有考虑。现在Qt 5添加了对于“新增像素”这一缩放类型的支持。

(还有其它的高DPI实现么?欢迎大家在评论中进行指正。)

Mac OS X的高DPI支持

OS X上高DPI模式的关键是,以前绝大多数几何信息都是通过物理像素给定的,现在却是设备无关点。这包括桌面几何信息(例如15英寸的Retina MacBook Pro是1440×900而不是全分辨率2880×1800)、窗口几何信息和事件坐标。CoreGraphics绘制引擎知道全分辨率并且针对这一解析度生成输出。例如,对于普通屏幕和高DPI屏幕(其它参数都相同的情况下),一个100×100的窗口在屏幕上占用的区域是一样的。在高DPI屏幕的窗口后端存储包含了200×200像素。

这种模式的主要收益是向后兼容性以及自由的高DPI矢量图形。对于底层情况不了解的应用程序可以简单地像以前一样工作在相同的几何情况下,并且保留写死的像素值。同时,他们还可以使用如文本这样的矢量图形,而不用做任何修改。光栅图形引擎不能获得自动改进,但这是可以实现的。不好的一点是在代码中使用点和像素的时候,有不可避免的坐标系统混淆。

点到像素的缩放因子总是2x。在改变屏幕分辨率的时候,这一情况也是真的——点和像素总是被一个值同时缩放。当使用“More Space”进行缩放的时候,应用程序将会被渲染到一个大的后端存储,这个后端存储会被再缩小到物理屏幕解析度上。

在Mac OS上缩放用户界面解析度

如果您手里没有Retina硬件,在使用外部显示器的时候,有一种仿真模式还是很有用的。打开显示器(Displays)属性并且选择一种HiDPI模式。(如果没有,请查看stackoverflow上的这个问题。)

为OS X应用程序启用高DPI

高DPI模式是通过Info.Plist文件中的这些键值控制的:

<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>
<string>True</string>

qmake将会为您添加这些内容。(严格意义上说,它将会只添加NSPrincipalClass,NSHighResolutionCapable是可选的并且默认值为true)。

如果NSHighResolutionCapable被设置为false,或者缺少这些键值,那么应用程序将会被按“普通”解析度渲染然后放大。这样的结果看起来很糟糕并且应该避免,特别是因为高DPI模式是非常向后兼容的,并且应用程序可以获得很多高DPI支持而不用做任何修改。

缩放的Qt Creator

高DPI的Qt Creator

(除了一个更新“模式”图标的补丁之外,没有其它修改了。)

Qt的实现细节

Mac OS 10.8(10.7是非正式的?)添加了对高DPI的Retina显示的支持。Qt 4免费获得这一支持,因为它使用的是CoreGraphics绘制引擎。

Qt 5使用的是光栅绘制引擎并且Qt通过缩放绘图器变换(transform)实现了高DPI矢量的绘制。HITheme同时为Qt 4和5提供了高DPI的Mac风格。在Qt 5的Fusion风格中,对于高DPI模式的支持也已经修改好了。

OpenGL是一种基于设备像素的API并且对于高DPI模式也仍然如此。在NSView中有一个flag可以用来开启或者禁用2x缩放——Qt在所有情况下都可以设置它。Shaders运行在设备像素中。

Qt Quick 1是构建于QGraphicsView之上的,它是一个QWidget并且通过QPainter获得对于高DPI的支持。

Qt Quick 2是基于Scene Graph(和OpenGL),已经更新了高DPI的支持。Qt Quick控件(也就是以前的Desktop Component)也已经更新了在高DPI模式下的渲染,其中包括距离场(distance field)文本渲染。(译者注:关于距离场,可以参考Yoann Lopes – Text Rendering in the QML Scene Graph以及iKDE上的译文。)

这里的卖点是应用程序开发人员不需要关心这些,您只需要在设备无关像素的空间里舒适地开发,Qt和/或OS会为您处理那些复杂的事情。但有一个例外,光栅内容(raster content)——需要提供高DPI光栅内容,并且应用程序代码需要正确处理这些内容。

窗口部件和绘图器

QPainter代码绝大多数情况下都和原来一样。我们来看看绘制渐变(gradient)的代码:

QRect destinationRect = ...
QGradient gradient = ...
painter.fillRect(rect, QBrush(gradient));

在高DPI显示器上,这个渐变在屏幕上的大小还是一样的,但是被填充了更多的(设备)像素。

绘制一个像素映射(pixmap)也是类似的:

QRect destinationRect = ...
QPixmap pixmap = ...
painter.drawPixmap(destinationRect, pixmap);

为了避免在高DPI显示器上出现缩放失真,像素映射必须包含足够的像素:两倍于destinationRect的宽和高。应用程序可以直接提供它们,也可以使用QIcon来管理不同的解析度:

QRect destinationRect = ...
QIcon icon = ...
painter.drawPixmap(destinationRect, icon.pixmap(destinationRect.size()));

QIcon::pixmap()已经被修改了,可以在高DPI系统中返回一个更大的像素映射。这种行为的改变会破坏现有的代码,所以它是由AA_UseHighDpiPixmaps这个应用程序属性来控制的:

qApp->setAttribute(Qt::AA_UseHighDpiPixmaps);

在Qt 5.1中这个属性默认值是关闭的,但在未来的Qt发布中它很有可能默认为打开。

极端情况和devicePixelRatio

Qt的窗口部件有一些极端情况。在理想情况下,它一直使用QIcon,并且在绘制的时候会使用正确的像素映射,但是实际情况是Qt API经常直接生成和使用像素映射。当像素映射的大小被用来计算布局的几何信息时,会发生错误——如果一个像素映射已经是高分辨率的,那么在屏幕上它就不应该再占用更多的空间。

通过使用QPixmap::devicePixelRatio(),就能让200×200的像素映射实际占据100×100的设备无关像素。由QIcon::pixmap()返回的像素映射中devicePixelRatio已经设置好了。

例如QLabel就是一个“像素映射消费者”:

QPixmap pixmap2x = ...
pixmap2x.setDevicePixelRatio(2.0);
QLabel *label = ...
label->setPixmap(pixmap2x);

然后QLabel会除以devicePixelRatio来获得布局的大小:

QSize layoutSize = pixmap.size() / pixmap.devicePixelRatio();

与此类似的几种情况在Qt中都已经修复,并且应用程序代码在启用AA_UseHighDpiPixmaps之前也需要做类似处理。

下面几个Qt类中都提供了devicePixelRatio()的读写函数:

注释
QWindow::devicePixelRatio() 推荐使用的读写函数
QScreen::devicePixelRatio()
QGuiApplication::devicePixelRatio() 如果没有QWindow指针,请使用这个
QImage::[set]devicePixelRatio()
QPixmap::[set]devicePixelRatio()

文本

字体大小还可以和原来一样,会在高DPI显示中产生类似的大小(但会有一点小问题)。字体的像素大小是设备无关的像素大小。您在高DPI显示中永远不会得到太小的文本。

QGLWidget

OpenGL是在设备像素空间中工作的。例如,传递给glViewport的宽和高应该是设备像素。QGLWidget::resizeGL()中的宽和高也是设备像素的。

不管怎样,QGLWidget::width()实际上就是QWidget::width(),它返回的是设备无关像素的值。如果需要,用它来乘以widget->windowHandle()->devicePixelRatio()可以解决很多问题。

Qt Quick 2和控件

Qt Quick 2和Qt Quick控件可以直接使用。因为窗口部件的坐标是设备无关像素的。Qt Quick也有几个和光栅相关的极端情况,因为QML的Image元素是通过URL来指定图像源的,这样就避免了像素映射的传递。

Qt Quick控件

还有一个例外是OpenGL着色器(shader),它运行在设备像素空间中并且可以看到全分辨率。在通常情况下这没有什么问题,我们应该知道的一件重要的事情是,鼠标坐标是设备无关像素的,也许需要被转换成设备像素。

运行中的着色器效果实例

管理高解析度的光栅内容

正如我们所看到的,在缩放的情况下,光栅内容看起来会不够好,所以必须提供高解析度的内容。作为应用程序开发人员,您有两个选项:(请忽略“什么都不做”选项)

  • 使用高解析度版本替换现有光栅内容
  • 另外提供一份高解析度内容

第一个选项很简单,因为每个资源只有一个版本。可是您也许会发现(或者您的设计师会告诉您)像图标这样的资源只有在它被创建的那个特定解析度下看起来才最好。为了解决这个问题,Qt沿用了“@2x”这种图像文件名的方案:

foo.png
foo@2x.png

这样高解析度的内容和原来的一一对应。在需要的时候,“@2x”的版本会被QML的Image元素以及QIcon自动加载。

Image { source = “foo.png” }
QIcon icon(“foo.png”)

(对于QIcon请记住使用AA_UseHighDpiPixmaps)

试验性的跨平台的高解析度支持

QPA允许我们相对容易的完成跨平台的实现。Qt现在把这一问题分为三层:

  • 应用程序层(应用程序代码和使用QPA类的Qt代码)
  • QPA层(QWindow、QScreen、QBackingStore)
  • 平台插件层(QPlatform*子类)

简化一下,应用程序层是在设备无关像素空间中工作的,并不知道设备像素。平台插件是在设备像素空间中工作的,并不知道设备无关像素。QPA层在两者之间,基于一个由环境变量QT_HIGHDPI_SCALE_FACTOR指定的缩放因子进行转换。

实际上,这个情况还会更复杂一些,各层之间会有泄露的事情发生,并且在Mac和iOS下还会有一些例外情况。

代码在github上。最后是XCB下的Qt Creator的截屏:

DPI缩放的Qt Creator

QT_HIGDPI_SCALE_FACTOR=2缩放的Qt Creator