1. 程式人生 > >支援 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 應用開發

支援 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 應用開發

Windows 10 自 1703 開始引入第二代的多屏 DPI 機制(PerMonitor V2),而 WPF 框架可以支援此第二代的多屏 DPI 機制。

本文將介紹 WPF 框架利用第二代多屏 DPI 機制進行高 DPI 適配的方法。同時,也介紹低版本的 WPF 或者低版本的作業系統下如何做相容。

新增應用程式清單檔案

在你現有 WPF 專案的主專案中需要新增兩個檔案以支援第二代的多屏 DPI 機制。

  • app.manifest (決定性檔案)
  • app.config (修復 Bug, .NET Framework 4.6.2 及以上可忽略)

專案中新增的兩個檔案
▲ 專案中新增的兩個檔案

預設情況下,app.config 在你建立 WPF 專案的時候就會存在,而 app.manifest 則不是。如果你的專案中已經存在這兩個檔案,就不需要添加了。

如果你沒有 app.config,如何新增?

開啟專案屬性,然後在屬性中選擇 .NET Framework 的版本,無論你選擇哪個,app.config 都會自動為你新增。

選擇 .NET Framework 版本以便新增 app.config 檔案

當然,正統的方法是跟下面的 app.manifest 的新增方法相同,你會在下面看到 Visual Studio 新建項中 app.manifest 和 app.config 檔案是挨在一起的。

如果你沒有 app.manifest,如何新增?

新建檔案的時候選擇應用程式清單檔案(應用程式配置檔案就在旁邊)
▲ 新建檔案的時候選擇應用程式清單檔案(應用程式配置檔案就在旁邊)

瞭解 WPF 清單檔案中的 DPI 感知設定

DpiAware

在你打開了 app.manifest 檔案後,找到以下程式碼,然後取消註釋:

<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
    DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need 
    to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should 
    also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->
<!-- <application xmlns="urn:schemas-microsoft-com:asm.v3"> <windowsSettings> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware> </windowsSettings> </application> -->

上面這一段程式碼是普通的 DPI 感知的清單設定,開啟後獲得系統 DPI 感知級別(System DPI Awareness)。

如果要開啟 Per-Monitor DPI 感知,將上面的 true 改成 true/pm(pm 表示 per-monitor)。

不過這只是相容性的設計而已,感謝老版本的系統使用字串包含的方式,於是可以老版本的系統可以相容新的 DPI 感知值:

  • 什麼都不填
    • 如果你額外也沒做什麼 DPI 相關的操作,那麼就是 Unaware。
  • 包含 true 字串
    • 當前程序設定為系統級 DPI 感知(System DPI Awareness)。
  • 包含 false 字串
    • 在 Windows Vista / 7 / 8 中,與什麼都不填的效果是一樣的。
  • 包含 true/pm 字串
    • 在 Windows Vista / 7 / 8 中,當前程序設定為系統級 DPI 感知(System DPI Awareness)。
    • 在 Windows 8.1 / 10 中,當前程序設定為螢幕級 DPI 感知(Per-Monitor DPI Awareness)。
  • 包含 per monitor 字串
    • 在 Windows Vista / 7 / 8 中,與什麼都不填的效果是一樣的。
    • 在 Windows 8.1 / 10 中,當前程序設定為螢幕級 DPI 感知(Per-Monitor DPI Awareness)。
  • 其他任何字串
    • 在 Windows Vista / 7 / 8 中,與什麼都不填的效果是一樣的。

說明一下,SetProcessDpiAwareness 是新 API,要求的最低系統版本是 Windows 8.1,呼叫這個才能指定為 Per-Monitor 的 DPI 感知。而 SetProcessDPIAware 是 Vista 開始引入的老 API,沒有引數可以傳。

DpiAwareness

<asmv3:application>
  <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
    <dpiAwareness>PerMonitorV2, unaware</dpiAwareness>
  </asmv3:windowsSettings>
</asmv3:application>

注意:只有 Windows 10 (1607) 及以上版本才會識別此節點的 DPI 設定。如果你設定了 dpiAwareness 節點,那麼 dpiAware 就會被忽略。

建議你可以兩個節點都指定,這樣既可以使用到 Windows 10 1607 的新特性,又可以相容老版本的 Windows 作業系統。

dpiAwareness 節點支援設定一個或多個 DPI 感知級別,用逗號分隔。如果你指定了多個,那麼作業系統會從第一個開始識別,如果能識別就使用,否則會找第二個。用這種方式,未來的應用可以指定當前系統不支援的 DPI 感知級別。

鑑於此,在目前 Windows 7 還大行其道的今天,為了相容,dpiAwarenessdpiAware 都設定是比較靠譜的。

dpiAwareness 節點目前支援的值有:

  • 什麼都不設定
    • dpiAware 節點的結果來
  • 整個逗號分隔的序列都沒有能識別的 DPI 感知級別
    • 如果你額外也沒做什麼 DPI 相關的操作,那麼就是 Unaware。
  • 第一個能識別的感知級別是 system
    • 當前程序設定為系統級 DPI 感知(System DPI Awareness)。
  • 第一個能識別的感知級別是 permonitor
    • 當前程序設定為螢幕級 DPI 感知(Per-Monitor DPI Awareness)。
  • 第一個能識別的感知級別是 permonitorv2
    • 當前程序設定為第二代螢幕級 DPI 感知(Per-Monitor V2 DPI Awareness)。
    • 僅在 Windows 10 (1703) 及以上版本才可被識別

使 WPF 程式支援 Per-Monitor V2 級 DPI 感知

前面我們分析 App.Manifest 檔案中 DPI 的設定後,幾乎得到一個資訊,dpiAwaredpiAwareness 都是要設定的,除非以後絕大多數使用者的系統版本都到達 Windows 10 (1607) 及以上。

以下是推薦的 DPI 感知級別設定:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <!-- The combination of below two tags have the following effect : 
         1. Per-Monitor for >= Windows 10 Anniversary Update
         2. System < Windows 10 Anniversary Update -->
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
  </windowsSettings>
</application>

需要注意:

  1. 你的 .NET Framework 版本必須在 4.6.2 以上才建議這麼設定,否則不建議開啟 Per-Monitor 的 DPI 感知;
  2. 系統版本在 Windows 10 (1703) 或以上,V2 的感知級別才會生效,否則就是第一個版本。

額外的,如果你的 .NET Framework 版本在 .NET Framework 4.6.2 以下,但作業系統在 Windows 10 及以上,你還需要修改 App.config 檔案(在 <configuration /> 節點)。

<runtime>
  <AppContextSwitchOverrides value = "Switch.System.Windows.DoNotScaleForDpiChanges=false"/>
</runtime>

注意:

  1. 這個值要設為 false。(微軟官方吐槽:Yes, set it to false. Double negative FTW!)
  2. AppContextSwitchOverrides 不能被設定兩次,如果一已經設定了其他值,需要用分號分隔多個值。

特別說明,當面向 .NET Framework 4.6.2 以下版本編譯,但運行於 Windows 10 (1607) 和以上版本時,只需要新增 Switch.System.Windows.DoNotScaleForDpiChanges=false 即可讓 WPF 程式處理 Dpi Change 訊息,此時 WPF 程式就像高版本的 .NET Framework 中一樣能夠正常處理多屏下的 DPI 縮放。

以上,劃重點 你並不需要編譯為高版本的 .NET Framework 即可獲得 Per-Monitor 的 DPI 縮放支援

WPF 程式在特殊清單設定下的效果

dpiAwareness 不設定,dpiAware 節點設定為 true/pm

100% DPI -->
100% DPI
▲ 100% DPI

150% DPI
▲ 150% DPI

注意到標題欄(非客戶區)沒有縮放,而 WPF 區域(客戶區)清晰地縮放了。

dpiAwareness 不設定,dpiAware 節點設定為 true

100% DPI -->
100% DPI
▲ 100% DPI

150% DPI
▲ 150% DPI

注意到標題欄(非客戶區)被縮放了,而 WPF 區域(客戶區)被 DPI 虛擬化進行了點陣圖拉伸(模糊)。

dpiAwareness 不設定,dpiAware 節點設定為 true/pm12345

此時,WPF 程式無法啟動!!!而你只需要減少一位數字,例如寫成 true/pm1234 即可成功啟動,效果跟 true 是一樣的,注意效果 不是 true/pm。也就是說,/pm 並沒有顯示出它的含義來。額外的,如果設為 false 但後面跟隨那麼長的字串,WPF 程式是可以啟動的。

dpiAwareness 設定為 PerMonitorV2

150% DPI
▲ 150% DPI

注意到標題欄(非客戶區)被縮放了,而 WPF 區域(客戶區)也能清晰地縮放(僅 Windows 10 1703 及以上系統才是這個效果)。

低版本 .NET Framework 和 低版本 Windows 下的 WPF DPI 縮放

由於 Windows 8.1 作業系統使用者存量不多,主要是 Windows 7 和 Windows 10。所以我們要麼相容完全不支援 Per-Monitor 的 Windows 7,要麼使用具有新特性的 Windows 10 即可獲得最佳的開發成本平衡。使用以上的 DPI 縮放方法足以讓你的 WPF 應用在任何一個 .NET Framework 版本下獲得針對螢幕的 DPI 清晰縮放(Per-Monitor DPI Awareness)。

參考資料