Поддержка осведомленности по монитору для расширителей Visual Studio

Версии до Visual Studio 2019 имели контекст осведомленности о DPI для системы, а не для каждого монитора с учетом DPI (PMA). Работа в системе осведомленности привела к снижению визуального интерфейса (например, размытых шрифтов или значков) всякий раз, когда Visual Studio должен был отображаться на мониторах с различными коэффициентами масштабирования или удаленными на компьютерах с различными конфигурациями отображения (например, с разными масштабированием Windows).

Контекст осведомленности О DPI Visual Studio 2019 задается как PMA, когда поддержка среды его, что позволяет Visual Studio отображать в соответствии с конфигурацией дисплея, где размещено, а не одной системной конфигурацией. В конечном счете преобразование в всегда четкий пользовательский интерфейс для областей поверхностей, поддерживающих режим PMA.

Дополнительные сведения о терминах и общем сценарии, описанных в этом документе, см. в документации по классическим приложениям высокого уровня DPI в Windows.

Быстрое начало

  • Убедитесь, что Visual Studio работает в режиме PMA (см. раздел "Включение PMA")

  • Проверка правильности работы расширения в наборе распространенных сценариев (см. статью " Тестирование расширений для проблем PMA")

  • При поиске проблем можно использовать стратегии и рекомендации, описанные в этом документе, для диагностики и устранения этих проблем. Вам также потребуется добавить в проект новый пакет NuGet Microsoft.VisualStudio.DpiAwareness для доступа к необходимым API.

Включение PMA

Чтобы включить PMA в Visual Studio, необходимо выполнить следующие требования:

После выполнения этих требований Visual Studio автоматически включает режим PMA в процессе.

Примечание.

Содержимое Windows Forms в Visual Studio (например, браузер свойств) поддерживает PMA только в том случае, если у вас есть Visual Studio 2019 версии 16.1 или более поздней версии.

Тестирование расширений для проблем с PMA

Visual Studio официально поддерживает платформы пользовательского интерфейса WPF, Windows Forms, Win32 и HTML/JS. Если Visual Studio помещается в режим PMA, каждый стек пользовательского интерфейса работает по-разному. Поэтому независимо от платформы пользовательского интерфейса рекомендуется выполнить тестовый проход, чтобы убедиться, что весь пользовательский интерфейс соответствует режиму PMA.

Рекомендуется проверить следующие распространенные сценарии:

  • Изменение коэффициента масштабирования среды единого монитора во время работы приложения.

    Этот сценарий помогает проверить, что пользовательский интерфейс отвечает на динамическое изменение DPI Windows.

  • Закрепление или отсоединение ноутбука, в котором подключенный монитор установлен на основной, и подключенный монитор имеет другой коэффициент масштабирования, чем ноутбук во время работы приложения.

    Этот сценарий помогает проверить, что пользовательский интерфейс отвечает на изменение DPI отображения, а также обработку отображения динамически добавляются или удаляются.

  • Наличие нескольких мониторов с различными коэффициентами масштабирования и перемещение приложения между ними.

    Этот сценарий помогает проверить, что пользовательский интерфейс отвечает на изменение DPI отображения

  • Удаленное взаимодействие с компьютером, когда локальные и удаленные компьютеры имеют различные коэффициенты масштабирования для основного монитора.

    Этот сценарий помогает проверить, что пользовательский интерфейс отвечает на динамическое изменение DPI Windows.

Хороший предварительный тест на наличие проблем с пользовательским интерфейсом заключается в том, использует ли код классы 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, который ожидает координаты устройства для возврата точного указателя монитора.

Классы проблем

Если режим PMA включен для Visual Studio, пользовательский интерфейс может реплика проблемы с несколькими распространенными способами. Большинство из этих проблем могут возникать в любой из поддерживаемых платформ пользовательского интерфейса Visual Studio. Кроме того, эти проблемы также могут возникать, когда часть пользовательского интерфейса размещается в сценариях масштабирования DPI в смешанном режиме (см. документацию По Windows, чтобы узнать больше).

Создание окна Win32

При создании окон с помощью CreateWindow() или CreateWindowEx() обычно создается окно с координатами (0,0) (верхний или левый угол основного дисплея), а затем переместите его в окончательное положение. Однако это может привести к тому, что окно активирует измененное сообщение или событие DPI, которое может извлечь другие сообщения пользовательского интерфейса или события и в конечном итоге привести к нежелательному поведению или отрисовке.

Размещение элементов WPF

При перемещении элементов WPF с помощью старых элементов Microsoft.VisualStudio.Utilities.Dpi.DpiHelper координаты в левом верхнем углу могут быть неправильно вычисляются, когда элементы находятся в не первичном DPI.

Сериализация размеров элементов пользовательского интерфейса или позиций

При восстановлении размера пользовательского интерфейса (при сохранении в качестве единиц устройства) в другом контексте DPI, отличном от того, что он был сохранен, он будет размещен неправильно и размер. Это происходит из-за того, что единицы устройств имеют присущие связи DPI.

Неправильное масштабирование

Элементы пользовательского интерфейса, созданные на первичном DPI, будут правильно масштабироваться, однако при перемещении на дисплей с другим DPI они не масштабируются и их содержимое слишком велико или слишком мало.

Неправильное ограничивающее

Аналогично проблеме масштабирования элементы пользовательского интерфейса правильно вычисляют границы в основном контексте DPI, однако при перемещении в не первичный DPI они не будут правильно вычислять новые границы. Таким образом, окно содержимого слишком мало или слишком велико по сравнению с пользовательским интерфейсом размещения, что приводит к пустому пробелу или вырезке.

Перетащите

Всякий раз, когда в сценариях DPI смешанного режима (например, различные элементы пользовательского интерфейса отрисовки в разных режимах осведомленности о DPI), перетаскивание координат может быть неправильно вычислено, что приводит к неправильной позиции окончательного падения.

Внепроцессный пользовательский интерфейс

Некоторый пользовательский интерфейс создается вне процесса и если создание внешнего процесса находится в другом режиме осведомленности о DPI, чем Visual Studio, это может привести к любым из предыдущих проблем с отрисовкой.

Элементы управления Windows Forms, изображения или макеты, отрисованные неправильно

Не все содержимое Windows Forms поддерживают режим PMA. В результате может возникнуть проблема с отрисовкой с неправильными макетами или масштабированием. Возможное решение в этом случае заключается в явном отображении содержимого Windows Forms в DpiAwarenessContext (см. ссылку Force a control in a specific DpiAwarenessContext).

Элементы управления Windows Forms или окна не отображаются

Одной из основных причин этой проблемы является попытка разработчиков повторно использовать элемент управления или окно с одним DpiAwarenessContext в окно с другим DpiAwarenessContext.

На следующих изображениях показаны текущие ограничения операционной системы Windows по умолчанию в родительских окнах:

A screenshot of the correct parenting behavior

Примечание.

Это поведение можно изменить, задав поведение размещения потоков (см. Dpi_Hosting_Behavior перечисление).

В результате, если установить отношение "родительский-дочерний" между неподдерживаемых режимов, он завершится ошибкой, а элемент управления или окно может не отображаться должным образом.

Диагностика проблем

Существует множество факторов, которые следует учитывать при выявлении проблем, связанных с PMA:

  • Ожидает ли пользовательский интерфейс или API логических значений или значений устройств?

    • Пользовательский интерфейс и API WPF обычно используют логические значения (но не всегда)
    • Пользовательский интерфейс и API Win32 обычно используют значения устройств
  • Откуда идут значения?

    • При получении значений из другого пользовательского интерфейса или API он передает устройства или логические значения.
    • Если при получении значений из нескольких источников все они используют или ожидают одинаковых типов значений или должны быть смешанными и соответствующими?
  • Используются ли константы пользовательского интерфейса и какая форма они находятся?

  • Является ли поток в правильном контексте DPI для значений, которые он получает?

    Изменения, необходимые для включения размещения смешанного DPI, обычно должны помещать пути кода в правильный контекст, однако работа, выполняемая вне основного цикла сообщений или потока событий, может выполняться в неправильном контексте DPI.

  • Пересекают ли значения границ контекста DPI?

    Перетаскивание — это распространенная ситуация, когда координаты могут пересекать контексты DPI. Окно пытается выполнить правильные действия, но в некоторых случаях пользовательский интерфейс узла может потребоваться выполнить преобразование, чтобы обеспечить соответствие границ контекста.

Пакет NuGet PMA

Новые библиотеки DpiAwarness можно найти в пакете NuGet Microsoft.VisualStudio.DpiAwareness .

Следующие средства помогут отладить проблемы, связанные с PMA, в некоторых различных стеках пользовательского интерфейса, поддерживаемых Visual Studio.

Snoop

Snoop — это средство отладки XAML с некоторыми дополнительными функциями, которые не имеют встроенных средств XAML Visual Studio. Кроме того, Snoop не требует активной отладки Visual Studio, чтобы иметь возможность просматривать и настраивать пользовательский интерфейс WPF. Двумя основными способами Snoop может быть полезно для диагностики проблем PMA является проверка координат логического размещения или границ размера, а для проверки пользовательского интерфейса имеет правильный DPI.

Инструменты Visual Studio XAML

Как и Snoop, средства XAML в Visual Studio могут помочь диагностировать проблемы с PMA. После обнаружения вероятного виновника можно задать точки останова и использовать окно "Динамическое визуальное дерево", а также окна отладки для проверки границ пользовательского интерфейса и текущего DPI.

Стратегии устранения проблем PMA

Замена вызовов DpiHelper

В большинстве случаев устранение проблем пользовательского интерфейса в режиме PMA сводится к замене вызовов в управляемом коде на старый вспомогательный класс 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, но требуют дополнительного входного параметра: элемент пользовательского интерфейса, используемый в качестве ссылки для операции преобразования. Важно отметить, что вспомогательные средства масштабирования изображений не существуют в новых вспомогательных программах DpiAwareness/CDpiAwareness, и при необходимости следует использовать службу ImageService .

Управляемый класс DpiAwareness предлагает вспомогательные средства для визуальных элементов WPF, элементов управления Windows Forms и Win32 HWND и HMONITORs (как в виде IntPtrs), в то время как собственный класс CDpiAwareness предлагает вспомогательные средства HWND и HMONITOR.

Диалоговые окна, окна или элементы управления Windows Forms, отображаемые в неправильном DpiAwarenessContext

Даже после успешного родительства окон с различными DpiAwarenessContexts (из-за поведения Windows по умолчанию) пользователи могут по-прежнему видеть проблемы с масштабированием в виде окон с различными DpiAwarenessContexts по-разному. В результате пользователи могут видеть проблемы выравнивания или размытого текста или изображения в пользовательском интерфейсе.

Решение заключается в том, чтобы задать правильный область DpiAwarenessContext для всех окон и элементов управления в приложении.

Диалоговые окна смешанного режима верхнего уровня (TLMM)

При создании окон верхнего уровня, таких как модальные диалоги, важно убедиться, что поток находится в правильном состоянии до создания окна (и его дескриптора). Поток можно поместить в системную осведомленность с помощью вспомогательного средства CDpiScope в собственном коде или вспомогательного средства DpiAwareness.EnterDpiScope в управляемом. (TLMM обычно следует использовать в диалоговых окнах, отличных от WPF/windows.)

Смешанный режим дочернего уровня (CLMM)

По умолчанию дочерние окна получают текущий контекст осведомленности О DPI потока при создании без родительского элемента или контекста осведомленности о DPI родительского объекта при создании с родительским элементом. Чтобы создать дочерний объект с другим контекстом осведомленности о DPI, отличном от родительского, поток можно поместить в нужный контекст осведомленности DPI. Затем дочерний элемент можно создать без родительского и вручную перепарированного в родительское окно.

Проблемы со средой CLMM

Большая часть вычислений пользовательского интерфейса, которая происходит в рамках основного цикла обмена сообщениями или цепочки событий, уже должна выполняться в правильном контексте осведомленности DPI. Однако если вычисления координат или размеров выполняются за пределами этих основных рабочих процессов (например, во время задачи простоя или вне потока пользовательского интерфейса, то текущий контекст осведомленности DPI может быть неправильным, что приводит к неправильному месту или неправильному размеру пользовательского интерфейса. При вводе потока в правильное состояние для пользовательского интерфейса обычно устранена проблема.

Отказ от 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);
};

Для управляемых языков лучше всего реализовать этот интерфейс в том же классе, который является производным от Microsoft.VisualStudio.Shell.ToolWindowPane. Для C++лучше всего реализовать этот интерфейс в том же классе, который реализует IVsWindowPane из vsshell.h.

Значение, возвращаемое свойством Mode в интерфейсе, является __VSDPIMODE (и приведение к управляемому объекту 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 преобразуется в значение Windows DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2.

Принудительное добавление элемента управления в определенный DpiAwarenessContext

Устаревший пользовательский интерфейс, который не обновляется для поддержки режима PMA, может по-прежнему потребовать дополнительных настроек для работы во время работы Visual Studio в режиме PMA. Одно из таких исправлений заключается в том, чтобы пользовательский интерфейс был создан в правильном DpiAwarenessContext. Чтобы принудительно перевести пользовательский интерфейс в определенный 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, и диалоговых окнах WPF верхнего уровня. При создании пользовательского интерфейса WPF, который должен размещаться в окнах инструментов или конструкторах, как только содержимое вставляется в дерево пользовательского интерфейса WPF, оно преобразуется в текущий процесс DpiAwarenessContext.

Известные проблемы

Windows Forms

Для оптимизации для новых сценариев смешанного режима Windows Forms изменили способ создания элементов управления и окон, когда их родитель не был явно задан. Ранее элементы управления без явного родительского элемента использовали внутреннее окно парковки в качестве временного родителя для создаваемого элемента управления или окна.

До .NET 4.8 существовал один "Окно парковки", который получает DpiAwarenessContext из текущего контекста осведомленности DPI потока во время создания окна. Любой непарентный элемент управления наследует тот же DpiAwarenessContext, что и окно парковки при создании дескриптора элемента управления и будет повторно преобразовано в окончательный или ожидаемый родительский элемент разработчиком приложения. Это приведет к сбоям на основе времени, если окно парковки было выше DpiAwarenessContext, чем окончательное родительское окно.

По состоянию на .NET 4.8 теперь существует "окно парковки" для каждого обнаруженного DpiAwarenessContext. Другим основным отличием является то, что DpiAwarenessContext, используемый для элемента управления, кэшируется при создании элемента управления, а не при создании дескриптора. Это означает, что общее поведение конца совпадает, но может превратить то, что раньше было проблемой на основе времени, в согласованную проблему. Он также дает разработчику приложения более детерминированное поведение для написания кода пользовательского интерфейса и правильной области.