共用方式為


Visual Studio 擴充器的個別監視器感知支援

Visual Studio 2019 之前的版本已將其 DPI 感知內容設定為系統感知,而不是個別監視器 DPI 感知(PMA)。 在系統感知中執行會導致視覺體驗降低(例如模糊字型或圖示),每當 Visual Studio 必須在不同縮放比例的監視器之間轉譯,或從遠端轉譯到具有不同顯示組態的機器(例如不同的 Windows 縮放比例)。

當visual Studio 2019的 DPI 感知內容設定為 PMA 時,當 環境支援 時,可讓Visual Studio 根據裝載位置的顯示器設定來轉譯,而不是單一系統定義的組態。 最終會轉譯成支援 PMA 模式之介面區域的一律清晰 UI。

如需本文件涵蓋之詞彙和整體案例的詳細資訊,請參閱 Windows 上的高 DPI 桌面應用程式開發檔。

快速入門

  • 確定 Visual Studio 以 PMA 模式執行(請參閱 啟用 PMA

  • 驗證擴充功能可在一組常見案例中正確運作(請參閱 測試您的擴充功能以瞭解 PMA 問題

  • 如果您發現問題,您可以使用本文件中討論的策略/建議來診斷並修正這些問題。 您也必須將新的 Microsoft.VisualStudio.DpiAwareness NuGet 套件新增至專案,以存取必要的 API。

啟用 PMA

若要在 Visual Studio 中啟用 PMA,必須符合下列需求:

  • Windows 10 2018 年 4 月更新版(v1803、RS4) 或更新版本
  • .NET Framework 4.8 RTM 或更新版本
  • 已開啟 [針對具有不同圖元密度的螢幕優化轉譯] 選項的 Visual Studio 2019

符合這些需求之後,Visual Studio 會自動跨進程啟用 PMA 模式。

注意

只有在 Visual Studio 2019 16.1 版或更新版本時,Visual Studio 中的 Windows Forms 內容才支援 PMA。

測試您的擴充功能是否有 PMA 問題

Visual Studio 正式支援 WPF、Windows Forms、Win32 和 HTML/JS UI 架構。 當 Visual Studio 進入 PMA 模式時,每個 UI 堆疊的行為會不同。 因此,不論UI架構為何,建議執行測試階段,以確保所有UI都符合PMA模式。

建議您驗證下列常見案例:

  • 在應用程式執行時變更單一監視器環境的縮放比例。

    此案例可協助測試UI回應動態 Windows DPI 變更。

  • 停駐/取消停駐附加監視器的膝上型計算機,其中附加監視器設定為主要監視器,且附加監視器在應用程式執行時具有與膝上型電腦不同的縮放比例。

    此案例可協助測試UI正在響應顯示 DPI 變更,以及處理動態新增或移除的顯示。

  • 有多個具有不同縮放比例的監視器,並在兩者之間移動應用程式。

    此案例可協助測試UI回應顯示 DPI 變更

  • 當本機和遠端計算機對主要監視器有不同的縮放比例時,遠端處理到計算機。

    此案例可協助測試UI回應動態 Windows DPI 變更。

UI 是否可能有問題的初步測試是程式代碼是否使用 Microsoft.VisualStudio.Utilities.Dpi.DpiHelper、Microsoft.VisualStudio.PlatformUI.DpiHelper 或 VsUI::CDpiHelper 類別。 這些舊的 DpiHelper 類別僅支援系統 DPI 感知,而且在程式為 PMA 時,不會一律正常運作。

這些 DpiHelpers 的一般用法看起來會像這樣:

Point screenTopRight = logicalBounds.TopRight.LogicalToDeviceUnits();

POINT screenIntTopRight = new POINT
{
    x = (int)screenTopRIght.X,
    y = (int)screenTopRIght.Y
}

// Declared via P/Invoke
IntPtr monitor = MonitorFromPoint(screenIntTopRight, MONITOR_DEFAULTTONEARST);

在上一個範例中,代表窗口邏輯界限的矩形會轉換成裝置單位,以便將它傳遞至預期裝置座標的原生方法 MonitorFromPoint,以便傳回精確的監視器指標。

問題的類別

針對 Visual Studio 啟用 PMA 模式時,UI 可能會以數種常見方式復寫問題。 大部分的問題,如果不是全部,都可能發生在任何 Visual Studio 支援的 UI 架構中。 此外,在混合模式 DPI 縮放案例中裝載 UI 時,也會發生這些問題(請參閱 Windows 以深入瞭解)。

Win32 視窗建立

使用 CreateWindow() 或 CreateWindowEx() 建立視窗時,常見的模式是在座標 (0,0) (主要顯示器的左上角)建立視窗,然後將它移至其最終位置。 不過,這樣做可能會導致視窗觸發 DPI 變更的訊息或事件,這可能會重新嘗試其他 UI 訊息或事件,並最終導致不想要的行為或轉譯。

WPF 專案放置

使用舊版 Microsoft.VisualStudio.Utilities.Dpi.DpiHelper 移動 WPF 元素時,每當元素位於非主要 DPI 時,可能不會正確計算左上方座標。

UI 元素大小或位置的串行化

當UI大小或位置(如果儲存為裝置單位)還原時,其 DPI 內容與儲存所在的不同 DPI 內容,則會正確定位及重設大小。 這是因為裝置單位具有固有的 DPI 關聯性。

調整不正確

在主要 DPI 上建立的 UI 元素會正確縮放,不過,當移至具有不同 DPI 的顯示器時,它們不會重新調整,而且其內容太大或太小。

不正確的周框

與縮放問題類似,UI 元素會在主要 DPI 內容上正確計算其界限,但是移至非主要 DPI 時,它們不會正確計算新的界限。 因此,相較於主控UI,內容視窗太小或太大,這會導致空白空間或裁剪。

拖放

每當在混合模式 DPI 案例內時(例如,以不同 DPI 感知模式呈現的不同 UI 元素),拖放座標可能會誤算,導致最終置放位置最終不正確。

跨進程UI

有些UI是跨進程建立的,如果建立外部進程與Visual Studio不同 DPI 感知模式,這可能會引入任何先前的轉譯問題。

Windows Forms 控件、影像或版面配置轉譯不正確

並非所有 Windows Forms 內容都支援 PMA 模式。 因此,您可能會看到轉譯問題與不正確的版面配置或調整。 在此情況下,可能的解決方法是在「系統感知」DpiAwarenessContext 中明確呈現 Windows Forms 內容(請參閱 強制控件進入特定的 DpiAwarenessContext)。

Windows Forms 控制件或未顯示的視窗

此問題的其中一個主要原因是開發人員嘗試將具有一個 DpiAwarenessContext 的控件或視窗重新父代為具有不同 DpiAwarenessContext 的視窗。

下列影像顯示父系視窗中目前的 預設 Windows 作業系統限制:

A screenshot of the correct parenting behavior

注意

您可以藉由設定線程裝載行為來變更此行為(請參閱 Dpi_Hosting_Behavior列舉)。

因此,如果您在不支援的模式之間設定父子關聯性,它將會失敗,而且控件或視窗可能無法如預期呈現。

診斷問題

識別 PMA 相關問題時需要考慮許多因素:

  • UI 或 API 是否預期邏輯或裝置值?

    • WPF UI 和 API 通常會使用邏輯值(但不一定)
    • Win32 UI 和 API 通常會使用裝置值
  • 值來自何處?

    • 如果從其他 UI 或 API 接收值,則會傳遞裝置或邏輯值。
    • 如果從多個來源接收值,它們是否全都使用/預期相同的值類型,或轉換是否需要混合和比對?
  • 使用中的UI常數及其形式為何?

  • 線程是否在正確的 DPI 內容中接收其值?

    啟用混合 DPI 裝載的變更通常應該將程式代碼路徑放在正確的內容中,不過,在主要訊息迴圈之外完成的工作,或事件流程可能會在錯誤的 DPI 內容中執行。

  • 值是否跨越 DPI 內容界限?

    拖放是座標可以跨 DPI 內容的常見情況。 Window 會嘗試執行正確的動作,但在某些情況下,主機 UI 可能需要執行轉換工作,以確保符合內容界限。

PMA NuGet 套件

您可以在 Microsoft.VisualStudio.DpiAwareness NuGet 套件中找到 新的 DpiAwarness 連結庫。

下列工具可協助偵錯 Visual Studio 所支援之一些不同 UI 堆疊的 PMA 相關問題。

探聽

Snoop 是 XAML 偵錯工具,具有內建 Visual Studio XAML 工具沒有的一些額外功能。 此外,Snoop 不需要主動偵錯 Visual Studio,就能檢視及調整其 WPF UI。 Snoop 可用來診斷 PMA 問題的兩個主要方式,是驗證邏輯放置座標或大小界限,以及驗證 UI 具有正確的 DPI。

Visual Studio XAML 工具

如同 Snoop,Visual Studio 中的 XAML 工具可協助診斷 PMA 問題。 找到可能罪魁禍首之後,您可以設定斷點,並使用 [即時可視化樹狀結構] 視窗以及偵錯視窗來檢查 UI 界限和目前的 DPI。

修正 PMA 問題的策略

取代 DpiHelper 呼叫

在大部分情況下,修正 PMA 模式中的 UI 問題會歸結為將 Managed 程式代碼中的呼叫取代為舊 Microsoft.VisualStudio.Utilities.Dpi.DpiHelper 和 Microsoft.VisualStudio.PlatformUI.DpiHelper 類別的呼叫,並呼叫新的 Microsoft.VisualStudio.Utilities.DpiAwareness 協助程序類別。

// Remove this kind of use:
Point deviceTopLeft = new Point(window.Left, window.Top).LogicalToDeviceUnits();

// Replace with this use:
Point deviceTopLeft = window.LogicalToDevicePoint(new Point(window.Left, window.Top));

針對原生程序代碼,這需要將舊 VsUI::CDpiHelper 類別的呼叫取代為對新 VsUI::CDpiAwareness 類別的呼叫。

// Remove this kind of use:
int cx = VsUI::DpiHelper::LogicalToDeviceUnitsX(m_cxS);
int cy = VsUI::DpiHelper::LogicalToDeviceUnitsY(m_cyS);

// Replace with this use:
int cx = m_cxS;
int cy = m_cyS;
VsUI::CDpiAwareness::LogicalToDeviceUnitsX(m_hwnd, &cx);
VsUI::CDpiAwareness::LogicalToDeviceUnitsY(m_hwnd, &cy);

新的 DpiAwareness 和 CDpiAwareness 類別提供與 DpiHelper 類別相同的單元轉換協助程式,但需要額外的輸入參數:UI 元素作為轉換作業的參考。 請務必注意,影像縮放協助程式不存在於新的 DpiAwareness/CDpiAwareness 協助程式中,如有需要, 則應該改用 ImageService

Managed DpiAwareness 類別提供 WPF 視覺效果、Windows Forms 控件和 Win32 HWND 和 HMONITOR 的協助程式(兩者都是以 IntPtrs 的形式),而原生 CDpiAwareness 類別則提供 HWND 和 HMONITOR 協助程式。

在錯誤的 DpiAwarenessContext 中顯示的 Windows Forms 對話框、視窗或控件

即使在具有不同 DpiAwarenessContexts 的視窗成功父代之後(因為 Windows 默認行為),使用者仍可能會看到縮放問題,因為具有不同 DpiAwarenessContexts 的視窗會以不同的比例調整。 因此,使用者可能會在UI上看到對齊/模糊的文字或影像問題。

解決方案是針對應用程式中的所有視窗和控件設定正確的 DpiAwarenessContext 範圍。

最上層混合模式 (TLMM) 對話框

建立最上層視窗,例如強制回應對話時,請務必確定線程在建立視窗之前處於正確的狀態(及其句柄)。 線程可以使用原生的 CDpiScope 協助程式或 Managed 中的 DpiAwareness.EnterDpiScope 協助程式,將線程放入系統感知中。 (TLMM 通常應該用於非 WPF 對話框/視窗上。

子層級混合模式 (CLMM)

根據預設,如果建立時沒有父系,或父系的 DPI 感知內容建立時,子視窗會收到目前的線程 DPI 感知內容。 若要建立具有與其父系不同 DPI 感知內容的子系,線程可以放入所需的 DPI 感知內容中。 然後可以建立子系,而不使用父代,並手動重新父代至父視窗。

CLMM 問題

大部分在主要傳訊迴圈或事件鏈結中發生的UI計算工作都應該已在正確的 DPI 感知內容中執行。 不過,如果在這些主要工作流程之外完成座標或重設大小計算(例如在閑置時間工作期間或關閉 UI 線程,則目前的 DPI 感知內容可能不正確,導致 UI 錯置或重設大小問題。 讓線程進入UI工作的正確狀態通常會修正問題。

退出退出 CLMM

如果要將非 WPF 工具視窗移轉至完全支援 PMA,則必須退出退出 CLMM。 若要這樣做,必須實作新的介面:IVsDpiAware。

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IVsDpiAware
{
    [ComAliasName("Microsoft.VisualStudio.Shell.Interop.VSDPIMode")]
    uint Mode {get;}
}
IVsDpiAware : public IUnknown
{
    public:
        HRRESULT STDMETHODCALLTYPE get_Mode(__RCP__out VSDPIMODE *dwMode);
};

針對 Managed 語言,實作此介面的最佳位置是衍生自 Microsoft.VisualStudio.Shell.ToolWindowPane 的相同類別。 對於 C++,實作此介面的最佳位置是與從 vsshell.h 實作 IVsWindowPane 的相同類別中。

介面上Mode屬性所傳回的值是__VSDPIMODE (並在 Managed 中轉換成 uint):

enum __VSDPIMODE
{
    VSDM_Unaware    = 0x01,
    VSDM_System     = 0x02,
    VSDM_PerMonitor = 0x03,
}
  • 不知道表示工具視窗需要處理 96 DPI,Windows 會處理所有其他 DPIs 的縮放比例。 導致內容稍微模糊。
  • 系統表示工具視窗必須處理主要顯示器 DPI 的 DPI。 任何具有相符 DPI 的顯示器看起來都清晰,但如果 DPI 在會話期間不同或變更,Windows 會處理縮放比例,而且會稍微模糊。
  • PerMonitor 表示工具視窗必須處理所有顯示器上的所有 DPIs,以及每當 DPI 變更時。

注意

Visual Studio 僅支援 PerMonitorV2 感知,因此 PerMonitor 列舉值會轉譯為 DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 的 Windows 值。

強制控件進入特定的 DpiAwarenessContext

未更新以支援 PMA 模式的舊版 UI,在 Visual Studio 以 PMA 模式執行時,仍可能需要稍微調整才能運作。 其中一個這類修正程序牽涉到確定 UI 是在正確的 DpiAwarenessContext 中建立。 若要強制 UI 進入特定的 DpiAwarenessContext,您可以使用下列程式代碼輸入 DPI 範圍:

using (DpiAwareness.EnterDpiScope(DpiAwarenessContext.SystemAware))
{
    Form form = new MyForm();
    form.ShowDialog();
}
void MyClass::ShowDialog()
{
    VsUI::CDpiScope dpiScope(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
    HWND hwnd = ::CreateWindow(...);
}

注意

強制 DpiAwarenessContext 僅適用於非 WPF UI 和最上層 WPF 對話方塊。 建立要裝載於工具視窗或設計工具內的 WPF UI 時,一旦內容插入 WPF UI 樹狀結構,就會轉換成目前的處理程式 DpiAwarenessContext。

已知問題

Windows Forms

為了針對新的混合模式案例進行優化,Windows Forms 會在未明確設定其父系時,變更其建立控件和視窗的方式。 稍早,沒有明確父系的控件會使用內部「停車視窗」做為所建立控件或視窗的暫存父代。

在 .NET 4.8 之前,有一個「停車視窗」可從視窗建立時間的目前線程 DPI 感知內容取得其 DpiAwarenessContext。 在建立控件的句柄時,任何未父系控件都會繼承與停車視窗相同的 DpiAwarenessContext,並且會由應用程式開發人員重新父代給最終/預期的父系。 如果「停車視窗」的 DpiAwarenessContext 高於最終父視窗,這會導致計時型失敗。

自 .NET 4.8 起,現在每個遇到 DpiAwarenessContext 的「停車視窗」。 另一個主要差異是,建立控件時會快取用於控件的 DpiAwarenessContext,而不是在建立句柄時快取。 這表示整體結束行為相同,但可以將過去的時間型問題轉換成一致的問題。 它也會讓應用程式開發人員更確定性的行為來撰寫其 UI 程式代碼,並將其範圍正確界定。