Unterstützung für die Sensibilisierung pro Monitor für Visual Studio-Extender

In Versionen vor Visual Studio 2019 wurde der Kontext für die DPI-Sensibilisierung auf systemfähig festgelegt, anstatt die DPI-Fähigkeit pro Monitor (PMA) zu berücksichtigen. Die Ausführung des Systembewusstseins führte zu einer beeinträchtigten visuellen Erfahrung (z. B. verschwommenen Schriftarten oder Symbolen), wenn Visual Studio monitorübergreifend mit unterschiedlichen Skalierungsfaktoren oder remote auf Computern mit unterschiedlichen Anzeigekonfigurationen (z. B. unterschiedlicher Windows-Skalierung) gerendert werden musste.

Der DPI-Sensibilisierungskontext von Visual Studio 2019 wird als PMA festgelegt, wenn die Umgebung unterstützen, sodass Visual Studio entsprechend der Konfiguration der Anzeige gerendert werden kann, in der sie gehostet wird, anstatt eine einzelne systemdefinierte Konfiguration. Letztendlich wird eine stets scharfe Benutzeroberfläche für Oberflächenbereiche übersetzt, die den PMA-Modus unterstützen.

Weitere Informationen zu den begriffen und allgemeinen Szenarien, die in diesem Dokument behandelt werden, finden Sie in der Windows-Dokumentation zur Entwicklung von Desktopanwendungen mit hohem DPI-Wert.

Schnellstart

  • Stellen Sie sicher, dass Visual Studio im PMA-Modus ausgeführt wird (siehe Aktivieren von PMA)

  • Überprüfen, ob Ihre Erweiterung in einer Reihe gängiger Szenarien ordnungsgemäß funktioniert (Siehe Testen ihrer Erweiterungen auf PMA-Probleme)

  • Wenn Sie Probleme finden, können Sie die in diesem Dokument erläuterten Strategien/Empfehlungen verwenden, um diese Probleme zu diagnostizieren und zu beheben. Sie müssen ihrem Projekt auch das neue NuGet-Paket "Microsoft.VisualStudio.DpiAwareness " hinzufügen, um auf die erforderlichen APIs zuzugreifen.

PMA aktivieren

Um PMA in Visual Studio zu aktivieren, müssen die folgenden Anforderungen erfüllt sein:

Sobald diese Anforderungen erfüllt sind, aktiviert Visual Studio automatisch den PMA-Modus im gesamten Prozess.

Hinweis

Windows Forms-Inhalte in Visual Studio (z. B. Eigenschaftenbrowser) unterstützen PMA nur, wenn Visual Studio 2019, Version 16.1 oder höher, installiert ist.

Testen ihrer Erweiterungen auf PMA-Probleme

Visual Studio unterstützt offiziell die Frameworks WPF, Windows Forms, Win32 und HTML/JS. Wenn Visual Studio in den PMA-Modus versetzt wird, verhält sich jeder UI-Stapel anders. Daher wird unabhängig vom Benutzeroberflächenframework empfohlen, dass ein Testdurchlauf durchgeführt wird, um sicherzustellen, dass die gesamte Benutzeroberfläche mit dem PMA-Modus kompatibel ist.

Es wird empfohlen, die folgenden allgemeinen Szenarien zu überprüfen:

  • Ändern des Skalierungsfaktors einer einzelnen Monitorumgebung während der Ausführung der Anwendung.

    In diesem Szenario wird getestet, dass die Benutzeroberfläche auf die dynamische Windows-DPI-Änderung reagiert.

  • Andocken/Abdocken eines Laptops, auf dem ein angeschlossener Monitor auf den primären Monitor festgelegt ist und der angeschlossene Monitor einen anderen Skalierungsfaktor hat als der Laptop, während die Anwendung ausgeführt wird.

    In diesem Szenario wird getestet, dass die Benutzeroberfläche auf die Änderung der Anzeige-DPI reagiert, sowie die Behandlung von Displays, die dynamisch hinzugefügt oder entfernt werden.

  • Mehrere Monitore mit unterschiedlichen Skalierungsfaktoren haben und die Anwendung zwischen ihnen verschieben.

    In diesem Szenario wird getestet, dass die Benutzeroberfläche auf die Änderung der Anzeige-DPI reagiert.

  • Remoting in einen Computer, wenn die lokalen und Remotecomputer unterschiedliche Skalierungsfaktoren für den primären Monitor haben.

    In diesem Szenario wird getestet, dass die Benutzeroberfläche auf die dynamische Windows-DPI-Änderung reagiert.

Ein guter vorläufiger Test, ob ihre Benutzeroberfläche Probleme haben könnte, besteht darin, ob der Code die Klassen Microsoft.VisualStudio.Utilities.DpiHelper, Microsoft.VisualStudio.PlatformUI.DpiHelper oder VsUI::CDpiHelper verwendet. Diese alten DpiHelper-Klassen unterstützen nur den System-DPI-Grad und funktionieren nicht immer ordnungsgemäß, wenn der Prozess PMA ist.

Die typische Verwendung dieser DpiHelpers sieht wie folgt aus:

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);

Im vorherigen Beispiel wird ein Rechteck, das die logischen Grenzen eines Fensters darstellt, in Geräteeinheiten konvertiert, sodass es an die systemeigene Methode MonitorFromPoint übergeben werden kann, die Gerätekoordinaten erwartet, um einen genauen Monitorzeiger zurückzugeben.

Klassen von Problemen

Wenn der PMA-Modus für Visual Studio aktiviert ist, kann die Benutzeroberfläche Probleme auf verschiedene gängige Weise replizieren. Die meisten, wenn nicht alle, dieser Probleme können in den unterstützten Ui-Frameworks von Visual Studio auftreten. Darüber hinaus können diese Probleme auch auftreten, wenn ein Teil der Benutzeroberfläche in Szenarien für die DPI-Skalierung im gemischten Modus gehostet wird (weitere Informationen finden Sie in der Windows-Dokumentation).

Win32-Fenstererstellung

Beim Erstellen von Fenstern mit CreateWindow() oder CreateWindowEx() besteht ein gängiges Muster darin, das Fenster an Koordinaten (0,0) (die obere/linke Ecke der primären Anzeige) zu erstellen, und verschieben Sie es dann an die endgültige Position. Dies kann jedoch dazu führen, dass das Fenster eine geänderte DPI-Nachricht oder ein Ereignis auslöst, was andere UI-Nachrichten oder Ereignisse wiederholen und schließlich zu unerwünschtem Verhalten oder Rendering führen kann.

WPF-Elementplatzierung

Beim Verschieben von WPF-Elementen mithilfe der alten Microsoft.VisualStudio.Utilities.Dpi.DpiHelper-Koordinaten werden die oberen linken Koordinaten möglicherweise nicht ordnungsgemäß berechnet, wenn sich Elemente auf einem nicht primären DPI befinden.

Serialisierung von UI-Elementgrößen oder -positionen

Wenn die Größe oder Position der Benutzeroberfläche (wenn sie als Geräteeinheiten gespeichert wird) bei einem anderen DPI-Kontext wiederhergestellt wird als bei dem, wo sie gespeichert wurde, wird sie falsch positioniert und angepasst. Dies geschieht, da Geräteeinheiten über eine inhärente DPI-Beziehung verfügen.

Falsche Skalierung

Ui-Elemente, die auf dem primären DPI-Wert erstellt wurden, werden jedoch ordnungsgemäß skaliert, wenn sie zu einer Anzeige mit einem anderen DPI-Wert verschoben werden, und ihre Inhalte sind zu groß oder zu klein.

Falsche Begrenzung

Ähnlich wie beim Skalierungsproblem berechnen UI-Elemente ihre Grenzen korrekt im primären DPI-Kontext. Wenn sie jedoch zu einem nicht primären DPI-Wert verschoben werden, werden die neuen Grenzen nicht ordnungsgemäß berechnet. Daher ist das Inhaltsfenster im Vergleich zur Host-UI zu klein oder zu groß, was zu leerem Platz oder Zuschneiden führt.

Ziehen und Ablegen

Wann immer innerhalb von DPI-Szenarien im gemischten Modus (z. B. unterschiedliche UI-Elemente, die in verschiedenen DPI-Sensibilisierungsmodi gerendert werden), können Drag- und Drop-Koordinaten falsch berechnet werden, was dazu führt, dass die endgültige Ablageposition falsch ist.

Out-of-Process-Ui

Einige Ui-Elemente werden außerhalb des Prozesses erstellt, und wenn sich der erstellung externe Prozess in einem anderen DPI-Sensibilisierungsmodus befindet als Visual Studio, kann dies zu allen vorherigen Renderingproblemen führen.

Windows Forms-Steuerelemente, -Bilder oder -Layouts wurden falsch gerendert

Nicht alle Windows Forms-Inhalte unterstützen den PMA-Modus. Daher wird möglicherweise ein Renderingproblem mit falschen Layouts oder Skalierungen angezeigt. Eine mögliche Lösung in diesem Fall ist das explizite Rendern von Windows Forms-Inhalten in "System Aware" DpiAwarenessContext (siehe Erzwingen eines Steuerelements in einen bestimmten DpiAwarenessContext).

Windows Forms-Steuerelemente oder Fenster, die nicht angezeigt werden

Eine der Standard Ursachen für dieses Problem ist, dass Entwickler versuchen, ein Steuerelement oder Fenster mit einem DpiAwarenessContext in ein Fenster mit einem anderen DpiAwarenessContext zu aktualisieren.

Die folgenden Bilder zeigen die aktuellen Standardeinschränkungen des Windows-Betriebssystems in übergeordneten Fenstern:

A screenshot of the correct parenting behavior

Hinweis

Sie können dieses Verhalten ändern, indem Sie das Threadhostingverhalten festlegen (siehe Dpi_Hosting_Behavior Enumeration).

Wenn Sie daher die Beziehung zwischen nicht unterstützten Modi festlegen, schlägt dies fehl, und das Steuerelement oder das Fenster wird möglicherweise nicht wie erwartet gerendert.

Diagnostizieren von Problemen

Es gibt viele Faktoren, die beim Identifizieren von PMA-bezogenen Problemen berücksichtigt werden sollten:

  • Erwartet die Benutzeroberfläche oder API logische oder Gerätewerte?

    • WPF-Ui und APIs verwenden in der Regel Wahrheitswerte (aber nicht immer)
    • Win32 UI und APIs verwenden in der Regel Gerätewerte
  • Woher stammen die Werte?

    • Wenn Werte von einer anderen Benutzeroberfläche oder API empfangen werden, werden Geräte- oder Wahrheitswerte übergeben.
    • Wenn Sie Werte aus mehreren Quellen empfangen, verwenden/erwarten sie alle die gleichen Wertetypen, oder müssen Konvertierungen gemischt und übereinstimmen?
  • Werden UI-Konstanten verwendet, und in welcher Form befinden sie sich?

  • Befindet sich der Thread im richtigen DPI-Kontext für die empfangenen Werte?

    Die Änderungen zum Aktivieren des gemischten DPI-Hostings sollten in der Regel Codepfade in den richtigen Kontext einfügen. Die Arbeit außerhalb der Standard Nachrichtenschleife oder des Ereignisflusses kann jedoch im falschen DPI-Kontext ausgeführt werden.

  • Überschreiten Werte DPI-Kontextgrenzen?

    Drag &drop ist eine häufige Situation, in der Koordinaten DPI-Kontexte überschreiten können. Fenster versucht, das richtige Zu tun, aber in einigen Fällen muss die Hostbenutzeroberfläche möglicherweise Konvertierungsaufgaben ausführen, um übereinstimmende Kontextgrenzen sicherzustellen.

PMA NuGet-Paket

Die neuen DpiAwarness-Bibliotheken finden Sie im NuGet-Paket "Microsoft.VisualStudio.DpiAwareness ".

Mit den folgenden Tools können Sie PMA-bezogene Probleme in einigen der verschiedenen von Visual Studio unterstützten UI-Stapel debuggen.

Snoop

Snoop ist ein XAML-Debuggingtool mit zusätzlichen Funktionen, über die die integrierten Visual Studio-XAML-Tools nicht verfügen. Darüber hinaus muss Snoop Visual Studio nicht aktiv debuggen, um die WPF-Benutzeroberfläche anzuzeigen und zu optimieren. Die beiden Standard Möglichkeiten, wie Snoop für die Diagnose von PMA-Problemen nützlich sein kann, besteht darin, logische Platzierungskoordinaten oder Größengrenzen zu validieren, und zum Überprüfen der Benutzeroberfläche hat den richtigen DPI-Wert.

Visual Studio-XAML-Tools

Wie Snoop können die XAML-Tools in Visual Studio die Diagnose von PMA-Problemen unterstützen. Sobald ein wahrscheinlicher Schuldiger gefunden wurde, können Sie Haltepunkte festlegen und das Fenster "Visuelle Live-Struktur" sowie die Debugfenster verwenden, um UI-Grenzen und aktuelle DPI-Werte zu prüfen.

Strategien zum Beheben von PMA-Problemen

Ersetzen von DpiHelper-Aufrufen

In den meisten Fällen führt das Beheben von UI-Problemen im PMA-Modus dazu, Aufrufe in verwaltetem Code in der alten Microsoft.VisualStudio.Utilities.Dpi.DpiHelper- und Microsoft.VisualStudio.PlatformUI.DpiHelper-Klasse durch Aufrufe der neuen Microsoft.VisualStudio.Utilities.DpiAwareness-Hilfsklasse zu ersetzen.

// 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));

Bei systemeigenem Code müssen Aufrufe der alten VsUI::CDpiHelper-Klasse durch Aufrufe der neuen VsUI::CDpiAwareness-Klasse ersetzt werden.

// 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);

Die neuen DpiAwareness- und CDpiAwareness-Klassen bieten dieselben Komponentenkonvertierungshilfsprogramme wie die DpiHelper-Klassen, erfordern jedoch einen zusätzlichen Eingabeparameter: das UI-Element, das als Verweis für den Konvertierungsvorgang verwendet werden soll. Es ist wichtig zu beachten, dass die Bildskalierungshilfsprogramme nicht in den neuen DpiAwareness/CDpiAwareness-Hilfsern vorhanden sind und bei Bedarf stattdessen der ImageService verwendet werden sollte.

Die verwaltete DpiAwareness-Klasse bietet Hilfsprogramme für WPF-Visualisierungen, Windows Forms-Steuerelemente und Win32 HWNDs und HMONITORs (beide in Form von IntPtrs), während die systemeigene CDpiAwareness-Klasse HWND- und HMONITOR-Hilfsprogramme bietet.

Windows Forms-Dialogfelder, Fenster oder Steuerelemente, die im falschen DpiAwarenessContext angezeigt werden

Auch nach einem erfolgreichen Übergeordneten von Fenstern mit unterschiedlichen DpiAwarenessContexts (aufgrund des Standardverhaltens von Windows) werden Benutzern möglicherweise weiterhin Skalierungsprobleme angezeigt, da Fenster mit unterschiedlichen DpiAwarenessContexts unterschiedlich skaliert werden. Daher werden Benutzern möglicherweise Ausrichtungs-/Verschwommene Text- oder Bildprobleme auf der Benutzeroberfläche angezeigt.

Die Lösung besteht darin, den richtigen DpiAwarenessContext-Bereich für alle Fenster und Steuerelemente in der Anwendung festzulegen.

Dialogfelder für den gemischten Modus der obersten Ebene (TLMM)

Beim Erstellen von Fenstern der obersten Ebene wie modale Dialogfelder ist es wichtig, sicherzustellen, dass sich der Thread vor dem Erstellen des Fensters (und seines Handles) im richtigen Zustand befindet. Der Thread kann mithilfe des CDpiScope-Hilfsprogramms in systemeigener oder der DpiAwareness.EnterDpiScope-Hilfsprogramm in verwalteter Weise in den Systembewusstsein versetzt werden. (TLMM sollte in der Regel für Nicht-WPF-Dialogfelder/Windows verwendet werden.)

Gemischter Modus auf untergeordneter Ebene (CLMM)

Standardmäßig erhalten untergeordnete Fenster den aktuellen Thread-DPI-Sensibilisierungskontext, wenn er ohne ein übergeordnetes Element erstellt wurde, oder den DPI-Sensibilisierungskontext des übergeordneten Elements, wenn er mit einem übergeordneten Element erstellt wurde. Um ein untergeordnetes Element mit einem anderen DPI-Bewusstseinskontext als dem übergeordneten Element zu erstellen, kann der Thread in den gewünschten DPI-Bewusstseinskontext eingefügt werden. Anschließend kann das untergeordnete Element ohne übergeordnetes Element erstellt und manuell in das übergeordnete Fenster analysiert werden.

CLMM-Probleme

Die meisten Funktionen der UI-Berechnung, die als Teil der Standard Messagingschleife oder Ereigniskette ausgeführt werden, sollten bereits im richtigen DPI-Bewusstseinskontext ausgeführt werden. Wenn jedoch Koordinaten- oder Größenberechnungen außerhalb dieser Standard Workflows durchgeführt werden (z. B. während einer Leerlaufzeitaufgabe oder außerhalb des UI-Threads), kann der aktuelle DPI-Sensibilisierungskontext falsch sein, was zu Fehlern bei der Benutzeroberfläche führt oder zu Fehlern bei der Größenanpassung führt. Wenn der Thread in den richtigen Zustand für die Benutzeroberfläche versetzt wird, wird das Problem im Allgemeinen behoben.

Abmelden von CLMM

Wenn ein Nicht-WPF-Toolfenster migriert wird, um PMA vollständig zu unterstützen, muss es sich von CLMM abmelden. Dazu muss eine neue Schnittstelle implementiert werden: 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);
};

Bei verwalteten Sprachen befindet sich der beste Ort zum Implementieren dieser Schnittstelle in derselben Klasse, die von Microsoft.VisualStudio.Shell.ToolWindowPane abgeleitet ist. Für C++ befindet sich der beste Ort zum Implementieren dieser Schnittstelle in derselben Klasse, die IVsWindowPane aus vsshell.h implementiert.

Der von der Mode-Eigenschaft auf der Schnittstelle zurückgegebene Wert ist ein __VSDPIMODE (und wird in ein verwaltetes Objekt umgewandelt):

enum __VSDPIMODE
{
    VSDM_Unaware    = 0x01,
    VSDM_System     = 0x02,
    VSDM_PerMonitor = 0x03,
}
  • Unwissend bedeutet, dass das Toolfenster 96 DPI verarbeiten muss. Windows übernimmt die Skalierung für alle anderen DPIs. Dies führt dazu, dass Inhalte leicht verschwommen sind.
  • Das System bedeutet, dass das Toolfenster den DPI-Wert für den primären DPI-Wert der Anzeige verarbeiten muss. Jede Anzeige mit einem passenden DPI-Wert sieht scharf aus, aber wenn sich der DPI-Wert während der Sitzung unterscheidet oder ändert, behandelt Windows die Skalierung und wird leicht verschwommen.
  • PerMonitor bedeutet, dass das Toolfenster alle DPIs auf allen Displays verarbeiten muss und wann immer sich der DPI-Wert ändert.

Hinweis

Visual Studio unterstützt nur perMonitorV2-Sensibilisierung, sodass der PerMonitor-Enumerationswert in den Windows-Wert von DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 übersetzt wird.

Erzwingen eines Steuerelements in einen bestimmten DpiAwarenessContext

Ältere Ui, die nicht aktualisiert wird, um den PMA-Modus zu unterstützen, benötigen möglicherweise weiterhin kleinere Optimierungen, um zu funktionieren, während Visual Studio im PMA-Modus ausgeführt wird. Eine solche Lösung besteht darin, sicherzustellen, dass die Benutzeroberfläche im richtigen DpiAwarenessContext erstellt wird. Um die Benutzeroberfläche in einen bestimmten DpiAwarenessContext zu erzwingen, können Sie einen DPI-Bereich mit dem folgenden Code eingeben:

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(...);
}

Hinweis

Das Erzwingen von DpiAwarenessContext funktioniert nur für WPF-Ui- und WPF-Dialogfelder auf oberster Ebene. Beim Erstellen einer WPF-Benutzeroberfläche, die in Toolfenstern oder Designern gehostet werden soll, sobald der Inhalt in die WPF-UI-Struktur eingefügt wird, wird sie in den aktuellen Prozess DpiAwarenessContext konvertiert.

Bekannte Probleme

Windows Forms

Um die neuen Szenarien mit gemischtem Modus zu optimieren, hat Windows Forms die Erstellung von Steuerelementen und Fenstern geändert, wenn das übergeordnete Element nicht explizit festgelegt wurde. Zuvor verwendeten Steuerelemente ohne ein explizites übergeordnetes Element ein internes "Parkfenster" als temporäres übergeordnetes Element für das steuerelement oder das Fenster, das erstellt wird.

Vor .NET 4.8 gab es ein einzelnes "Parking Window", das seinen DpiAwarenessContext aus dem aktuellen Thread-DPI-Bewusstseinskontext zur Erstellungszeit des Fensters abruft. Jedes nicht analysierte Steuerelement erbt denselben DpiAwarenessContext wie das Parkfenster, wenn der Steuerpunkt des Steuerelements erstellt wird und vom Anwendungsentwickler zum endgültigen/erwarteten übergeordneten Element des Steuerelements analysiert wird. Dies würde zeitbasierte Fehler verursachen, wenn das "Parkfenster" einen höheren DpiAwarenessContext als das letzte übergeordnete Fenster hatte.

Ab .NET 4.8 gibt es jetzt ein "Parkfenster" für jeden dpiAwarenessContext, der gefunden wurde. Der andere Hauptunterschied besteht darin, dass der für das Steuerelement verwendete DpiAwarenessContext beim Erstellen des Steuerelements zwischengespeichert wird, nicht beim Erstellen des Handles. Dies bedeutet, dass das allgemeine Endverhalten identisch ist, aber kann das, was als zeitbasiertes Problem verwendet wird, zu einem konsistenten Problem machen. Außerdem erhält der Anwendungsentwickler ein deterministisches Verhalten zum Schreiben des UI-Codes und zur korrekten Bereichsdefinition.