1. 程式人生 > >Qt Mac OS、iOS和X11的Retina顯示支援

Qt Mac OS、iOS和X11的Retina顯示支援

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是1440x900而不是全解析度2880×1800)、視窗幾何資訊和事件座標。CoreGraphics繪製引擎知道全解析度並且針對這一解析度生成輸出。例如,對於普通螢幕和高DPI螢幕(其它引數都相同的情況下),一個100x100的視窗在螢幕上佔用的區域是一樣的。在高DPI螢幕的視窗後端儲存包含了200x200畫素。

這種模式的主要收益是向後相容性以及自由的高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(),就能讓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顯示中永遠不會得到太小的文字。

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
[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

 

QT_HIGDPI_SCALE_FACTOR