Qt 5.0中添加了对于retina显示的基本支持。即将到来的Qt 5.1中提供了新的API和缺陷修复,对于这一问题进行了改进。Qt 4.8也获得了良好的支持,我们反向移植了一些Qt 5的补丁。
尽管这些实现的努力和Mac以及iOS程序员最为相关,但是来看一看其它平台是如何处理高DPI显示这一问题,也是很有趣的。这里主要有两种方式:
基于DPI缩放——Win32 GDI和KDE。在这种方式中,应用程序在全物理设备分辨率下工作,使用系统提供的一个DPI设定或者缩放因子,用于缩放布局。字体通常会被操作系统自动缩放(只要您使用点数(point)而不是像素(pixel)来指定字体大小)
另一种意义的像素。在这种方式中,应用程序并不知道物理解析度(在任何程度上)。物理像素被逻辑像素替代:
平台/API | 逻辑的 | 物理的 |
---|---|---|
HTML | CSS像素 | 设备像素 |
Apple | 点 | 像素 |
Android | 密度无关像素(dp) | (屏幕)像素 |
Direct2D | 设备无关像素(DIP) | 物理像素 |
Qt(过去) | 像素 | 像素 |
Qt(现在) | 设备无关像素 | 设备像素 |
在历史上,Qt已经支持基于DPI缩放的物理像素这一方式。在2009年时,对于Windows上的高DPI值的支持已经有所改进。Qt布局对于增加的DPI并没有考虑。现在Qt 5添加了对于“新增像素”这一缩放类型的支持。
(还有其它的高DPI实现么?欢迎大家在评论中进行指正。)
OS X上高DPI模式的关键是,以前绝大多数几何信息都是通过物理像素给定的,现在却是设备无关点。这包括桌面几何信息(例如15英寸的Retina MacBook Pro是1440x900而不是全分辨率2880×1800)、窗口几何信息和事件坐标。CoreGraphics绘制引擎知道全分辨率并且针对这一解析度生成输出。例如,对于普通屏幕和高DPI屏幕(其它参数都相同的情况下),一个100x100的窗口在屏幕上占用的区域是一样的。在高DPI屏幕的窗口后端存储包含了200x200像素。
这种模式的主要收益是向后兼容性以及自由的高DPI矢量图形。对于底层情况不了解的应用程序可以简单地像以前一样工作在相同的几何情况下,并且保留写死的像素值。同时,他们还可以使用如文本这样的矢量图形,而不用做任何修改。光栅图形引擎不能获得自动改进,但这是可以实现的。不好的一点是在代码中使用点和像素的时候,有不可避免的坐标系统混淆。
点到像素的缩放因子总是2x。在改变屏幕分辨率的时候,这一情况也是真的——点和像素总是被一个值同时缩放。当使用“More Space”进行缩放的时候,应用程序将会被渲染到一个大的后端存储,这个后端存储会被再缩小到物理屏幕解析度上。
在Mac OS上缩放用户界面解析度
如果您手里没有Retina硬件,在使用外部显示器的时候,有一种仿真模式还是很有用的。打开显示器(Displays)属性并且选择一种HiDPI模式。(如果没有,请查看stackoverflow上的这个问题。)
高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
(除了一个更新“模式”图标的补丁之外,没有其它修改了。)
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发布中它很有可能默认为打开。
Qt的窗口部件有一些极端情况。在理想情况下,它一直使用QIcon,并且在绘制的时候会使用正确的像素映射,但是实际情况是Qt API经常直接生成和使用像素映射。当像素映射的大小被用来计算布局的几何信息时,会发生错误——如果一个像素映射已经是高分辨率的,那么在屏幕上它就不应该再占用更多的空间。
通过使用QPixmap::devicePixelRatio(),就能让200x200的像素映射实际占据100x100的设备无关像素。由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显示中永远不会得到太小的文本。
OpenGL是在设备像素空间中工作的。例如,传递给glViewport的宽和高应该是设备像素。QGLWidget::resizeGL()中的宽和高也是设备像素的。
不管怎样,QGLWidget::width()实际上就是QWidget::width(),它返回的是设备无关像素的值。如果需要,用它来乘以widget->windowHandle()->devicePixelRatio()可以解决很多问题。
Qt Quick 2和Qt Quick控件可以直接使用。因为窗口部件的坐标是设备无关像素的。Qt Quick也有几个和光栅相关的极端情况,因为QML的Image元素是通过URL来指定图像源的,这样就避免了像素映射的传递。
Qt Quick控件
还有一个例外是OpenGL着色器(shader),它运行在设备像素空间中并且可以看到全分辨率。在通常情况下这没有什么问题,我们应该知道的一件重要的事情是,鼠标坐标是设备无关像素的,也许需要被转换成设备像素。
运行中的着色器效果实例
正如我们所看到的,在缩放的情况下,光栅内容看起来会不够好,所以必须提供高解析度的内容。作为应用程序开发人员,您有两个选项:(请忽略“什么都不做”选项)
使用高解析度版本替换现有光栅内容
另外提供一份高解析度内容
第一个选项很简单,因为每个资源只有一个版本。可是您也许会发现(或者您的设计师会告诉您)像图标这样的资源只有在它被创建的那个特定解析度下看起来才最好。为了解决这个问题,Qt沿用了“@2x”这种图像文件名的方案:
foo.png [email protected]
这样高解析度的内容和原来的一一对应。在需要的时候,“@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