Добавляем поддержку HiDPI в приложения на WinForms

В настоящее время всё больше людей приобретают мониторы, либо ноутбуки, оснащённые дисплеями с большой плотностью пикселей (HiDPI-дисплеи), поэтому для корректной работы современные приложения должны поддерживать работу на них в полной мере.

Введение

К HiDPI относятся любые дисплеи со значением PPI (пикселей на дюйм) 144 и выше (при стандартном значении 96).

В большинстве фреймворков для построения приложений с графическим интерфейсом поддержка масштабирования в зависимости от размеров DPI экрана, на котором отображается окно, реализована «из коробки». Microsoft же предлагали всем использовать исключительно универсальные приложения Windows (UWP), либо WPF, заявляя, что лишь они полностью поддерживают данную технологию. Многих это по понятным причинам не устраивало ибо существует огромное количество уже написанных и прекрасно функционирующих программ, использующих Windows Forms, которые переписывать практически с нуля никто не собирается.

В итоге поддержка полноценного автоматического масштабирования элементов управления для экранов с высокой плотностью пикселей на Windows Forms появилась в пакете .NET Framework лишь начиная с версии 4.7.1, однако она является не полной и требующей ручной доработки.

Активируем стандартную поддержку

Полная поддержка экранов с высокой плотностью пикселей присутствует в ОС Microsoft Windows 10. В более старых её либо нет вовсе, либо она реализована лишь частично.

В главном манифесте приложения, в compatibility::application::supportedOS, явно укажем поддержку всех современных версий системы (в противном случае приложение будет считать, что запущено в Windows 8.1 из-за слоя совместимости Win32 API и не сможет использовать поддержку данной технологии):

<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
    <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
    <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
    <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
    <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
    <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>

Теперь для активации HiDPI в режиме per monitor добавим в главный конфигурационный файл приложения app.config следующие строки:

<System.Windows.Forms.ApplicationConfigurationSection>
    <add key="DpiAwareness" value="PerMonitorV2"/>
</System.Windows.Forms.ApplicationConfigurationSection>

В свойствах проекта изменим тулчейн сборки на Microsoft .NET Framework 4.7.1 или более позднюю версию, согласимся с предложением Visual Studio на конвертацию и подтвердим осуществление данного действия, согласившись с возможными последствиями.

Пересоберём проект и запустим получившийся результат на мониторе с высокой плотностью пикселей. Форма должна автоматически масштабироваться.

Масштабируем составные элементы управления

Как мы и говорили в самом начале статьи, поддержка автоматического масштабирования в Windows Forms получилась не полной и некоторые сложные элементы управления, а именно ListView, DataGridView, StatusBar и некоторые другие, не будут корректно изменять свои размеры в зависимости от установленного на текущем экране значения DPI, поэтому нам придётся исправить это самостоятельно.

Создадим новый файл с исходным кодом и добавим его корректное в пространство имён проекта. Добавим отдельный статический класс с перегруженным методом ScaleControlElements:

public static void ScaleControlElements(DataGridView ScaleSource, SizeF ScaleFactor)
{
    foreach (DataGridViewColumn Column in ScaleSource.Columns)
    {
        Column.Width = (int)Math.Round(Column.Width * ScaleFactor.Width);
    }
}

public static void ScaleControlElements(ListView ScaleSource, SizeF ScaleFactor)
{
    foreach (ColumnHeader Column in ScaleSource.Columns)
    {
        Column.Width = (int)Math.Round(Column.Width * ScaleFactor.Width);
    }
}

Этот метод изменяет размеры колонок внутри таких контролов, как ListView и DataGridView, в зависимости от текущего значения ScaleFactor, которое, в свою очередь, зависит от DPI текущего дисплея.

Теперь на каждой форме, содержащей проблемные элементы управления, переопределим метод ScaleControl:

protected override void ScaleControl(SizeF ScalingFactor, BoundsSpecified Bounds)
{
    base.ScaleControl(ScalingFactor, Bounds);
    ScaleControlElements(ControlName, ScalingFactor);
}

Сначала мы вызываем базовый метод base.ScaleControl, чтобы произвести автоматическое масштабирование средствами среды, а затем произведём собственное для составных контролов (ControlName).

Масштабируем графику

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

Таким образом, нам необходимо иметь несколько версий каждого растрового изображения на форме. В настоящее время это:

  • 1x — стандартное изображение;
  • 2x — двойное разрешение;
  • 3x — тройное разрешение.

Например если у нас есть файл foo-bar.png с разрешением 100*100 пикселей, то мы должны создать также foo-barx2.png (200*200 px), а также foo-barx3.png (300*300 px).

Как и в прошлый раз, на каждой форме, содержащей растровые изображения, переопределим метод ScaleControl:

protected override void ScaleControl(SizeF ScalingFactor, BoundsSpecified Bounds)
{
    base.ScaleControl(ScalingFactor, Bounds);
    if (CompareFloats(Math.Max(ScalingFactor.Width, ScalingFactor.Height), 2.0f))
    {
        // Требуются изображения 2x...
        Load2xImages();
    }
}

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

Написание этих методов выходит за рамки данной статьи.

Заключение

Теперь после доработки и пересборки нашего приложения, оно будет выглядеть корректно на любых конфигурациях оборудования.

Более того, т.к. мы задали режим per monitor, при наличии нескольких экранов с разным значением PPI и перемещении окон между данными ними, они будут автоматически масштабироваться в большую, либо меньшую сторону.

Литература

При написании данной статьи использовалась литература из следующих источников: