1. 程式人生 > >Windows 下的高 DPI 應用開發(UWP / WPF / Windows Forms / Win32)

Windows 下的高 DPI 應用開發(UWP / WPF / Windows Forms / Win32)

本文將介紹 Windows 系統中高 DPI 開發的基礎知識。由於涉及到座標轉換,這種轉換經常發生在計算的不知不覺中;所以無論你使用哪種 Windows 下的 UI 框架進行開發,你都需要了解這些內容,以免不斷踩坑。

Windows 高 DPI 應用開發課件

各種不同的 Windows 桌面 UI 框架

微軟主推的 Windows 桌面 UI 框架有:

  • UWP
  • WPF
  • Windows Forms
  • Win32 與 C++
  • DirectX

後兩者實際上並不是 UI 框架,是 UI 框架的底層不同實現。當然你單純憑藉 Win32 和 DirectX 去開發 GUI 應用也沒有人攔你,只不過如果你試圖只用 Win32 和 DirectX 而不進行各種 UI 元件封裝的話,最終會非常痛苦的。

UWP 只支援 Windows 10(當然也分不同的小版本,相容起來有些痛苦)。

WPF 和 Windows Forms 的最新版本只支援 Windows 7 SP1 及以上系統。如果要支援 Windows 7 和更早的系統,你需要降低 .NET Framework 的版本至 4.6.2 及以下;如果要 XP 支援,還需要到 4.0 及以下。

對普通使用者而言的 DPI 級別

DPI 值有兩種:系統 DPI (System DPI) 和螢幕 DPI (Monitor DPI)。自 Windows Vista 開始引入系統 DPI 概念,自 Windows 8.1 開始引入螢幕 DPI 概念。

在 Windows Vista / 7 / 8 中,作業系統提供了真正的 DPI 的設定:

Windows 7 的 DPI 設定 ▲ Windows 7 的 DPI 設定(控制面板 -> 外觀與個性化 -> 顯示

這裡的設定改的就是系統的 DPI 值。

Windows 7 中還額外提供了傳統 Windows XP 風格 DPI 縮放比例的選項(此選項在 Windows 8 之後就刪掉了),這也是在修改 DPI 值,只不過可以選擇非 1/4 整數倍的 DPI 值。

自定義 DPI 設定 ▲ 自定義 DPI 設定

自 Windows 8.1 開始,作業系統開始可以設定不同螢幕的 DPI 值了:

Windows 10 中的多個螢幕選擇 ▲ Windows 10 中的多個螢幕選擇

Windows 10 中針對每個螢幕的 DPI 設定 ▲ Windows 10 中針對每個螢幕的 DPI 設定

如果使用者在設定中更改了系統 DPI 值或螢幕 DPI 值,那麼 Windows 系統會提示需要登出才會應用修改。

對於 Windows 8.1 以下的系統,登出是必要的。因為系統 DPI 值如果不登出就不會改變,應用需要在系統重新登入後有了新的 DPI 值時才會正常根據新的系統 DPI 值進行渲染。否則就是系統進行的點陣圖縮放。

對於 Windows 8.1 及以上的系統,登出通常也是必要的。雖然螢幕 DPI 值已經更新,並且已嚮應用視窗傳送了 Dpi Change 訊息,但系統 DPI 值依然沒變。應用必須處理 Dpi Change 訊息才會正常渲染。如果應用不支援螢幕 DPI 感知,那麼使用的就是系統 DPI 值,於是一樣的會被系統進行點陣圖縮放。

但事情到 Windows 10 (1803) 之後,事情又有了轉機。現在,你可以通過在設定中開啟一個開關,使得無需登出,只要重新開啟應用即可讓此應用獲取到最新的系統 DPI 的值。

Windows 10 (1803) 中新增的“不模糊”設定項

方法是:開啟“設定” -> “系統” -> “顯示器” -> “高階縮放設定”,在“高階縮放設定”上,開啟“允許 Windows 嘗試修復應用,使其不模糊”。

額外的,對於 Windows 8.1 及以上的系統,系統 DPI 值等於主屏在系統啟動時的螢幕 DPI 值。

對 Windows 應用而言的 DPI 感知級別(Dpi Awareness)

Windows 的 DPI 感知級別經過歷代升級,已經有四種了。

  1. 無感知 (Unaware)
    • DPI 值就是一個常量 96。
    • 如果在系統中設定縮放,那麼就會採用點陣圖拉伸(會模糊)。
  2. 系統級感知 (System DPI Awareness)
    • Vista 系統引入
    • DPI 值在系統啟動後就固定下來,所有顯示器上的應用共用這一個 DPI 值。
    • 如果在系統設定中修改了 DPI,那麼就會採用點陣圖拉伸(會模糊)。
  3. 螢幕級感知 (Per-Monitor DPI Awareness)
    • 隨 Windows 8.1 引入
    • 應用的 DPI 值會隨著所在螢幕的不同而改變。
    • 當多個螢幕 DPI 不一樣,而應用從一個螢幕切換到另一個螢幕的時候,應用會收到 DPI 改變的訊息
    • 只有應用的頂層 HWND 會收到 DPI 改變訊息
  4. 螢幕級感知第二代 (Per-Monitor V2 DPI Awareness)
    • 隨 Windows 10 (1703) 引入
    • 應用的 DPI 值會隨著所在螢幕的不同而改變。
    • 當多個螢幕 DPI 不一樣,而應用從一個螢幕切換到另一個螢幕的時候,應用會收到 DPI 改變的訊息
    • 應用的頂層和子 HWND 都會收到 DPI 改變訊息
    • 以下 UI 元素也會在 DPI 改變時縮放
      • 非客戶區(Non-client Area)
      • 系統通用控制元件中的點陣圖(comctl32V6)

在 Windows 10 19H1 中(對現在來說還是預覽版),可以直接在工作管理員中檢視到程序的 DPI Awareness:

在工作管理員中檢視 DPI Awareness ▲ 在工作管理員中檢視 DPI Awareness

方法是在工作管理員中 Details 的標題欄右鍵,選擇列,然後找到 DPI Awareness。

可以看到,目前僅檔案資源管理器是 Per-Monitor V2 的。

不同 UI 框架對 DPI 的支援情況

UWP

UWP 當然支援最新的各種 DPI 感知級別,而且是完全支援。

WPF

WPF 的最新版支援最新的 DPI 感知級別,不過依然有限制:

Native WPF applications will DPI scale WPF hosted in other frameworks and other frameworks hosted in WPF do not automatically scale

即原生 WPF 應用支援 DPI 縮放,在其他 UI 框架中的 WPF 也支援 DPI 縮放;但是 WPF 中嵌入的其他 UI 框架不支援自動 DPI 縮放。

WPF 第一個版本(隨 .NET Framework 3.5 釋出)就已支援系統級 DPI 感知。

.NET Framework 4.6.2 開始的 WPF 才開始支援螢幕級 DPI 感知。而 Per-Monitor V1 和 Per-Monitor V2 的支援在作業系統級別是相容的,所以只需要修改 WPF 中的應用程式清單即可相容第二代螢幕級 DPI 感知。

Windows Forms

Windows Forms 也是在 .NET Framework 4.7 才開始支援螢幕級 DPI 感知的。不過部分控制元件不支援自動隨螢幕 DPI 切換。

其他 UI 框架

原生 Win32 是支援最新 DPI 感知的,其他如 GDI/GDI+/MFC 等都不支援,除非開發者手工編寫。

混合 DPI 感知級別

當專案足夠大的時候,一個或幾個專案成員可能很難了解所有的視窗邏輯。讓一個程序的所有視窗開啟 DPI 縮放對應用的高 DPI 遷移來說比較困難。不過好在我們可以開啟混合 DPI 縮放。

Windows 10 (1604) 開始引入頂級視窗(Top-level Window)級別的 DPI 感知,而 Windows 10 (1703) 開始引入每一個 HWND 的 DPI 感知,包括頂級視窗和非頂級視窗。這裡的頂級視窗指的是沒有父級的視窗,指的是 Parent,而不是 Owner。

在建立一個視窗的前後分別呼叫 SetThreadDpiAwarenessContext 函式可以讓建立的這個視窗具有單獨的 DPI 感知級別。前一次是為了讓視窗在建立時有一個對此執行緒的新的 DPI 感知級別,而後一次呼叫是恢復此執行緒的 DPI 感知級別。

微軟的 Office 系列就是典型的使用了混合 DPI 感知級別的應用。在以下實驗中,我組成了一個 96 DPI 的主屏和 144 DPI 的副屏,先在 96 DPI 的螢幕上截一張圖,再將視窗移動到 144 DPI 的螢幕中再截一張圖。

Microsoft PowerPoint 使用的是系統 DPI 感知級別:

96 DPI 下的主介面 ▲ 96 DPI 下的主介面

144 DPI 下的主介面 ▲ 144 DPI 下的主介面

你可以通過點開圖片檢視原圖來比較這兩幅圖在原圖尺寸下的模糊程度。

Microsoft PowerPoint 的演示頁面使用的是螢幕 DPI 感知級別:

96 DPI 下的演示頁面 ▲ 96 DPI 下的演示頁面

144 DPI 下的演示頁面 ▲ 144 DPI 下的演示頁面

可以看到,演示頁面在多屏 DPI 下是沒有產生縮放的模糊,即採用了螢幕 DPI 感知級別。

而以上的主介面和演示頁面屬於同一個程序。

只有一個 PowerPoint 程序 ▲ 只有一個 PowerPoint 程序

DPI 相關的 Windows API 的遷移

  • GetSystemMetrics -> GetSystemMetricsForDpi
  • AdjustWindowRectEx -> AdjustWindowRectExForDpi
  • SystemParametersInfo -> SystemParametersInfoForDpi
  • GetDpiForMonitor -> GetDpiForWindow

參考資料