共用方式為


Windows 上的高 DPI 桌面應用程式開發

此內容是以想要更新傳統型應用程式的開發人員為目標,以動態方式處理顯示縮放比例(每英吋點或 DPI)變更,讓其應用程式在轉譯的任何顯示器上變得清晰。

首先,如果您要從頭開始建立新的 Windows 應用程式,強烈建議您建立 通用 Windows 平台 (UWP) 應用程式。 UWP 應用程式會自動且動態地調整其執行的每個顯示器。

使用舊版 Windows 程式設計技術的計算機應用程式(原始 Win32 程式設計、Windows Forms、Windows Presentation Framework (WPF)等) 在沒有額外的開發人員工作的情況下,無法自動處理 DPI 縮放比例。 如果沒有這類工作,許多常見的使用案例中,應用程式看起來會模糊或大小不正確。 本檔提供更新傳統型應用程式以正確轉譯的相關內容和資訊。

顯示縮放比例和 DPI

隨著顯示技術的進展,顯示面板製造商在其面板上的每一個實體空間單位中封裝了越來越多的圖元。 這導致現代顯示面板的每英吋點數(DPI)遠高於以往的點數。 過去,大多數顯示器每一線性英寸的實體空間有96像素(96 DPI):在 2017 年,已可供使用近 300 DPI 或更新版本的顯示器。

大部分舊版傳統型 UI 架構都有內建的假設,即顯示 DPI 不會在程式的存留期內變更。 此假設已不再成立,顯示 DPIs 通常會在應用程式程式的存留期內變更數次。 顯示縮放比例/DPI 變更的一些常見案例如下:

  • 多個監視器設定,其中每個顯示器都有不同的縮放比例,而且應用程式會從一個顯示器移至另一個顯示器(例如 4K 和 1080p 顯示器)
  • 對接和取消停駐高 DPI 膝上型電腦與低 DPI 外部顯示器(反之亦然)
  • 連線 從高 DPI 膝上型電腦/平板電腦到低 DPI 裝置的遠端桌面 (反之亦然)
  • 當應用程式執行時,讓顯示縮放比例設定變更

在這些案例中,UWP 應用程式會自動重新繪製新的 DPI。 根據預設,而且沒有額外的開發人員工作,傳統型應用程式不會。 未執行這項額外工作以回應 DPI 變更的桌面應用程式,可能會對使用者顯示模糊或大小不正確。

DPI 感知模式

傳統型應用程式必須告訴 Windows 是否支援 DPI 縮放比例。 根據預設,系統會考慮傳統型應用程式 DPI 未察覺和點陣圖延展其視窗。 藉由設定下列其中一種可用的 DPI 感知模式,應用程式可以明確地告訴 Windows 他們想要如何處理 DPI 縮放比例:

DPI 未察覺

DPI 不知道應用程式會以固定 DPI 值 96(100%)呈現。 每當這些應用程式在顯示比例大於 96 DPI 的畫面上執行時,Windows 會將應用程式點陣圖延展至預期的實體大小。 這會導致應用程式看起來模糊。

系統 DPI 感知

系統 DPI 感知的電腦應用程式通常會在使用者登入時收到主要連線監視器的 DPI。 在初始化期間,他們會使用該系統 DPI 值適當地配置 UI(重設大小控件、選擇字型大小、載入資產等)。 因此,系統 DPI 感知應用程式不會由 Windows 在顯示以該單一 DPI 轉譯時縮放 DPI 縮放(點陣圖縮放)。 當應用程式移至具有不同縮放比例的顯示器,或顯示縮放比例變更時,Windows 會點陣圖縮放應用程式的視窗,使其看起來模糊。 實際上,系統 DPI 感知桌面應用程式只會以單一顯示器縮放比例清晰呈現,每當 DPI 變更時就會變得模糊。

每一監視器和個別監視器 (V2) DPI 感知

建議更新傳統型應用程式以使用每一監視器 DPI 感知模式,以便每當 DPI 變更時立即正確轉譯。 當應用程式向 Windows 回報它想要在此模式中執行時,Windows 不會在 DPI 變更時點陣圖延展應用程式,而是將WM_DPICHANGED傳送至應用程式視窗。 然後,應用程式必須完全負責處理新 DPI 的大小調整。 傳統型應用程式所使用的大部分 UI 架構(Windows 通用控件(comctl32)、Windows Forms、Windows Presentation Framework 等) 不支援自動 DPI 縮放比例,要求開發人員調整和重新調整視窗本身的內容大小。

每個監視器有兩個版本的感知,應用程式可以自行註冊為:第 1 版和第 2 版(PMv2)。 將進程註冊為在 PMv2 感知模式中執行,會導致:

  1. 當 DPI 變更時收到通知的應用程式(最上層和子 HWND)
  2. 應用程式看到每個顯示器的原始圖元
  3. 應用程式永遠不會由 Windows 縮放位圖
  4. 自動非工作區(視窗 標題、滾動條等)依 Windows 的 DPI 縮放比例
  5. Win32 對話框 (從 CreateDialog) 自動由 Windows 縮放的 DPI
  6. 通用控件中主題繪製的點陣圖資產(複選框、按鈕背景等)會自動以適當的 DPI 縮放比例轉譯

在個別監視器 v2 感知模式中執行時,應用程式會在 DPI 變更時收到通知。 如果應用程式不會針對新的 DPI 自行重設大小,則應用程式 UI 會顯示太小或太大(視先前和新 DPI 值的差異而定)。

注意

個別監視器 V1 (PMv1) 感知非常有限。 建議應用程式使用 PMv2。

下表顯示應用程式在不同案例下呈現的方式:

DPI 感知模式 引進的 Windows 版本 應用程式的 DPI 檢視 DPI 變更時的行為
知道 N/A 所有顯示器都是96 DPI 點陣圖延展 (模糊)
系統 Vista 所有顯示器都有相同的 DPI(啟動目前使用者工作階段時主要顯示器的 DPI) 點陣圖延展 (模糊)
每一監視器 8.1 應用程式視窗主要位於的顯示器 DPI
  • 最上層 HWND 會收到 DPI 變更的通知
  • 沒有任何UI元素的 DPI 縮放比例。

每部監視器 V2 Windows 10 Creators Update (1703) 應用程式視窗主要位於的顯示器 DPI
  • 最上層 子 HWND 會收到 DPI 變更通知

自動 DPI 縮放比例:
  • 非工作區
  • 通用控制項中的主題繪製位圖 (comctl32 V6)
  • 對話框 (CreateDialog

每部監視器 (V1) DPI 感知

每部監視器 V1 DPI 感知模式 (PMv1) 是透過 Windows 8.1 引進的。 此 DPI 感知模式非常有限,只提供下列功能。 建議傳統型應用程式使用 Windows 10 1703 或更新版本支援的個別監視器 v2 感知模式。

每個監視器感知的初始支援僅提供下列應用程式:

  1. 最上層 HWND 會收到 DPI 變更的通知,並提供新的建議大小
  2. Windows 不會點陣圖延展應用程式 UI
  3. 應用程式會以實體像素顯示所有顯示器(請參閱虛擬化)

在 Windows 10 1607 或更新版本上,PMv1 應用程式也可以在WM_NCCREATE期間呼叫 EnableNonClientDpiScaling ,要求 Windows 正確調整視窗的非工作區。

依UI架構/技術支援的每個監視器 DPI 縮放比例

下表顯示從 Windows 10 1703 起,各種 Windows UI 架構所提供的每個監視器 DPI 感知支援層級:

Framework / Technology 支援 OS 版本 處理的 DPI 縮放比例 深入閱讀
通用 Windows 平台 (UWP) 完整 1607 UI 架構 通用 Windows 平台 (UWP)
Raw Win32/Common Controls V6 (comctl32.dll)
  • 傳送至所有 HWND 的 DPI 變更通知訊息
  • 主題繪製的資產在通用控件中正確轉譯
  • 對話框的自動 DPI 縮放比例
1703 應用程式 GitHub 範例
Windows Forms 某些控制件的自動個別監視器 DPI 縮放比例有限 1703 UI 架構 Windows Forms 中的高 DPI 支援
Windows Presentation Framework (WPF) 原生 WPF 應用程式會將裝載在其他架構和其他架構中裝載的 WPF 縮放比例不會自動調整 1607 UI 架構 GitHub 範例
GDI None N/A 應用程式 請參閱 GDI 高 DPI 縮放比例
GDI+ None N/A 應用程式 請參閱 GDI 高 DPI 縮放比例
MFC None N/A 應用程式 N/A

更新現有的應用程式

若要更新現有的傳統型應用程式以正確處理 DPI 縮放比例,它必須更新,以便至少更新其 UI 的重要部分以回應 DPI 變更。

大部分傳統型應用程式都會以系統 DPI 感知模式執行。 系統 DPI 感知應用程式通常會調整為主要顯示器的 DPI(系統匣在 Windows 作業階段啟動時所在的顯示器)。 當 DPI 變更時,Windows 會點陣圖延展這些應用程式的 UI,這通常會導致這些應用程式的 UI 變得模糊。 更新系統 DPI 感知應用程式以變成個別監視器 DPI 感知時,處理 UI 配置的程式代碼必須更新,如此一來,它不僅會在應用程式初始化期間執行,而且每當收到 DPI 變更通知時(在 Win32 的情況下為WM_DPICHANGED )。 這通常牽涉到重新瀏覽程序代碼中的任何假設,即 UI 只需要調整一次。

此外,在 Win32 程式設計的情況下,許多 Win32 API 沒有任何 DPI 或顯示內容,因此它們只會傳回相對於系統 DPI 的值。 透過程式代碼進行 grep 來尋找其中一些 API,並將其取代為 DPI 感知變體,這非常有用。 具有 DPI 感知變體的一些常見 API 包括:

單一 DPI 版本 每一監視器版本
GetSystemMetrics GetSystemMetricsForDpi
AdjustWindowRectEx AdjustWindowRectExForDpi
SystemParametersInfo SystemParametersInfoForDpi
GetDpiForMonitor GetDpiForWindow

在程式代碼基底中搜尋硬式編碼大小是個好主意,假設有固定 DPI,以正確說明 DPI 縮放比例的程式代碼取代它們。 以下是包含上述所有建議的範例:

範例:

下列範例顯示建立子 HWND 的簡化 Win32 案例。 對 CreateWindow 的呼叫會假設應用程式執行於 96 DPI (USER_DEFAULT_SCREEN_DPI 常數),而且按鈕的大小和位置都不會在較高的 DPIs 上正確:

case WM_CREATE: 
{ 
    // Add a button 
    HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",  
        WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,  
        50,  
        50,  
        100,  
        50,  
        hWnd, (HMENU)NULL, NULL, NULL); 
} 

下列更新的程式代碼顯示:

  1. 視窗建立程式碼 DPI 會調整其父視窗 DPI 之子 HWND 的位置和大小
  2. 重新調整子 HWND 的位置及重設大小,以回應 DPI 變更
  3. 已移除硬式編碼大小,並以回應 DPI 變更的程式代碼取代
#define INITIALX_96DPI 50 
#define INITIALY_96DPI 50 
#define INITIALWIDTH_96DPI 100 
#define INITIALHEIGHT_96DPI 50 

// DPI scale the position and size of the button control 
void UpdateButtonLayoutForDpi(HWND hWnd) 
{ 
    int iDpi = GetDpiForWindow(hWnd); 
    int dpiScaledX = MulDiv(INITIALX_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI); 
    int dpiScaledY = MulDiv(INITIALY_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI); 
    int dpiScaledWidth = MulDiv(INITIALWIDTH_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI); 
    int dpiScaledHeight = MulDiv(INITIALHEIGHT_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI); 
    SetWindowPos(hWnd, hWnd, dpiScaledX, dpiScaledY, dpiScaledWidth, dpiScaledHeight, SWP_NOZORDER | SWP_NOACTIVATE); 
} 
 
... 
 
case WM_CREATE: 
{ 
    // Add a button 
    HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",  
        WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON, 
        0, 
        0, 
        0, 
        0, 
        hWnd, (HMENU)NULL, NULL, NULL); 
    if (hWndChild != NULL) 
    { 
        UpdateButtonLayoutForDpi(hWndChild); 
    } 
} 
break; 
 
case WM_DPICHANGED: 
{ 
    // Find the button and resize it 
    HWND hWndButton = FindWindowEx(hWnd, NULL, NULL, NULL); 
    if (hWndButton != NULL) 
    { 
        UpdateButtonLayoutForDpi(hWndButton); 
    } 
} 
break; 

更新系統 DPI 感知應用程式時,一些常見的步驟如下:

  1. 使用應用程式指令清單(或其他方法,視所使用的UI架構而定),將進程標示為個別監視器 DPI 感知 (V2)。
  2. 讓UI設定邏輯可重複使用,並將其移出應用程式初始化程式代碼,以便在發生 DPI 變更時重複使用它(在 Windows (Win32) 程式設計的情況下WM_DPICHANGED)。
  3. 使任何假設 DPI 敏感數據 (DPI/fonts/sizes/etc.) 的程式代碼都不需要更新。 在程式初始化時快取字型大小和 DPI 值是很常見的做法。 更新應用程式以變成個別監視器 DPI 感知時,每當遇到新的 DPI 時,都必須重新評估 DPI 敏感數據。
  4. 發生 DPI 變更時,重載 (或重新點陣化) 新 DPI 的任何點陣化,或選擇性地將目前載入的資產延展至正確的大小。
  5. Grep 適用於非個別監視器 DPI 感知的 API,並將其取代為個別監視器 DPI 感知 API(如果適用)。 範例:將 GetSystemMetrics 取代為 GetSystemMetricsForDpi。
  6. 在多顯示器/多 DPI 系統上測試您的應用程式。
  7. 對於無法更新為正確 DPI 縮放比例的應用程式中的任何最上層視窗,請使用混合模式 DPI 縮放比例(如下所述)允許系統對這些最上層視窗進行位圖延展。

混合模式 DPI 縮放比例 (子進程 DPI 縮放比例)

更新應用程式以支援個別監視器 DPI 感知時,有時可能會變得不切實際或不可能一次更新應用程式中的每個視窗。 這可能是因為更新和測試所有UI所需的時間和精力,或因為您沒有執行所需的所有UI程式代碼(如果您的應用程式可能載入第三方UI)。 在這些情況下,Windows 可讓您在原始 DPI 感知模式中執行一些應用程式視窗(最上層),同時專注您的時間和精力,以更新 UI 更重要的部分,以輕鬆瞭解每個監視器感知的世界。

下圖說明其外觀:您會更新圖例中的主要應用程式 UI(圖例中的「主視窗」),以使用個別監視器 DPI 感知執行,同時以現有模式執行其他視窗(「次要視窗」)。

differences in dpi scaling between awareness modes

在 Windows 10 年度更新版 (1607) 之前,程式的 DPI 感知模式是全進程屬性。 從 Windows 10 年度更新版開始,現在可以為每個 最上層 視窗設定此屬性。 ( 視窗必須繼續符合其父系的縮放大小。最上層視窗定義為沒有父系的視窗。 這通常是具有最小化、最大化和關閉按鈕的「一般」視窗。 子進程 DPI 感知的案例是讓次要 UI 由 Windows 縮放(點陣圖延展),同時將時間和資源放在更新主要 UI 上。

若要啟用子進程 DPI 感知,請在任何視窗建立呼叫之前和之後呼叫 SetThreadDpiAwarenessContext 建立的視窗將會與您透過 SetThreadDpiAwarenessContext 設定的 DPI 感知相關聯。 使用第二個呼叫來還原目前線程的 DPI 感知。

雖然使用子進程 DPI 縮放可讓您依賴 Windows 來執行應用程式的一些 DPI 縮放比例,但它會增加應用程式的複雜度。 請務必瞭解此方法的缺點,以及所引進複雜度的性質。 如需子進程 DPI 感知的詳細資訊,請參閱 混合模式 DPI 縮放比例和 DPI 感知 API。

測試您的變更

更新應用程式以成為個別監視器 DPI 感知之後,請務必驗證應用程式在混合 DPI 環境中正確回應 DPI 變更。 要測試的一些細節包括:

  1. 在不同 DPI 值的顯示之間來回移動應用程式視窗
  2. 在顯示不同的 DPI 值時啟動您的應用程式
  3. 在應用程式執行時變更監視器的縮放比例
  4. 變更您用來做為主要顯示器的顯示器、 註銷 Windows,然後在重新登入之後重新測試您的應用程式。 這對於尋找使用硬式編碼大小/維度的程式代碼特別有用。

常見的陷阱 (Win32)

不使用WM_DPICHANGED中提供的建議矩形

當 Windows 傳送應用程式視窗WM_DPICHANGED訊息時,此訊息會包含建議的矩形,您應該用來調整視窗的大小。 您的應用程式必須使用這個矩形自行重設大小,因為這將會:

  1. 在顯示之間拖曳時,確定滑鼠游標會停留在視窗的相同相對位置
  2. 防止應用程式窗口進入遞歸 DPI 變更迴圈,其中一個 DPI 變更會觸發後續 DPI 變更,這會觸發另一個 DPI 變更。

如果您有應用程式特定需求,導致您無法使用 Windows 在WM_DPICHANGED訊息中提供的建議矩形,請參閱 WM_GETDPISCALEDSIZE。 此訊息可用來為 Windows 提供您想要的大小,一旦發生 DPI 變更,仍可避免上述問題。

缺乏虛擬化的相關文件

當 HWND 或進程以 DPI 未察覺或系統 DPI 感知的形式執行時,它可以由 Windows 縮放。 發生這種情況時,Windows 會將 DPI 敏感性資訊從某些 API 縮放並轉換成呼叫線程的座標空間。 例如,如果 DPI 不知道線程在高 DPI 顯示器上執行時查詢螢幕大小,Windows 會將提供給應用程式的答案虛擬化,就像螢幕在 96 DPI 單位中一樣。 或者,當系統 DPI 感知線程與目前使用者會話啟動時所使用的顯示器互動時,如果 HWND 在原始 DPI 縮放比例下執行,Windows 會將一些 API 呼叫縮放到 HWND 將使用的座標空間。

當您正確將傳統型應用程式更新為 DPI 縮放比例時,可能很難知道哪些 API 呼叫可以根據線程內容傳回虛擬化值;這項資訊目前尚未由 Microsoft 記載。 請注意,如果您從 DPI-unaware 或 system-DPI 感知線程內容呼叫任何系統 API,則傳回值可能會虛擬化。 因此,請確定您的線程是在與螢幕或個別窗口互動時所預期的 DPI 內容中執行。 當您使用 SetThreadDpiAwarenessContext 暫時變更線程的 DPI 內容時,請務必在完成時還原舊內容,以避免在應用程式中其他地方造成不正確的行為。

許多 Windows API 沒有 DPI 內容

許多舊版 Windows API 不會在其介面中包含 DPI 或 HWND 內容。 因此,開發人員通常需要執行其他工作來處理任何 DPI 敏感性資訊的縮放比例,例如大小、點或圖示。 例如,使用 LoadIcon 的開發人員必須點圖延展載入圖示,或使用替代 API 來為適當的 DPI 載入正確大小的圖示,例如 LoadImage

強制重設全進程 DPI 感知

一般而言,進程初始化之後,無法變更進程的 DPI 感知模式。 不過,如果您嘗試中斷視窗樹狀結構中所有 HWND 具有相同 DPI 感知模式的需求,Windows 可以強制變更程式的 DPI 感知模式。 在所有版本的 Windows 上,從 Windows 10 1703 開始,HWND 樹狀結構中不可能以不同的 DPI 感知模式執行不同的 HWND。 如果您嘗試建立中斷此規則的子父關聯性,則可以重設整個程式的 DPI 感知。 這可以由下列方式觸發:

  1. CreateWindow 呼叫,其中傳入父視窗的 DPI 感知模式與呼叫線程不同。
  2. SetParent 呼叫,其中兩個視窗與不同的 DPI 感知模式相關聯。

下表顯示如果您嘗試違反此規則,會發生什麼情況:

作業 Windows 8.1 Windows 10 (1607 和更早版本) Windows 10 (1703 和更新版本)
CreateWindow (In-Proc) N/A 子繼承 (混合模式) 子繼承 (混合模式)
CreateWindow (Cross-Proc) 強制重設 (呼叫端的程式) 子繼承 (混合模式) 強制重設 (呼叫端的程式)
SetParent (In-Proc) N/A 強制重設 (目前行程) 失敗 (ERROR_INVALID_STATE)
SetParent (Cross-Proc) 強制重設 (子視窗的行程) 強制重設 (子視窗的行程) 強制重設 (子視窗的行程)

高 DPI API 參考

混合模式 DPI 縮放比例和 DPI 感知 API。