Разработка классических приложений с высоким уровнем DPI в Windows

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

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

Классические приложения с использованием старых технологий программирования Windows (необработанное программирование Win32, Windows Forms, Windows Presentation Framework (WPF) и т. д.) не удается автоматически обрабатывать масштабирование DPI без дополнительной работы разработчика. Без такой работы приложения будут отображаться размытыми или неправильно размерами во многих распространенных сценариях использования. В этом документе содержатся контекст и сведения о том, что связано с обновлением классического приложения для правильной отрисовки.

Коэффициент масштабирования отображения и DPI

По мере развития технологии отображения производители панелей отображения упаковали все большее количество пикселей в каждую единицу физического пространства на своих панелях. Это привело к тому, что точки на дюйм (DPI) современных панелей дисплея значительно выше, чем они исторически были. В прошлом большинство дисплеев имели 96 пикселей на линейный дюйм физического пространства (96 DPI); в 2017 году отображаются почти 300 DPI или более поздних версий.

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

  • Установки с несколькими мониторами, в которых каждый дисплей имеет другой коэффициент масштабирования, и приложение перемещается из одного дисплея в другой (например, 4K и 1080p)
  • Закрепление и отключение ноутбука с высоким уровнем DPI с внешним дисплеем с низким уровнем DPI (или наоборот)
  • Подключение через удаленный рабочий стол с высокой нагрузкой на ноутбук или планшет с высоким уровнем DPI на устройство с низким уровнем DPI (или наоборот)
  • Изменение параметров коэффициента отображения во время работы приложений

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

Режим осведомленности о DPI

Классические приложения должны сообщить Windows, если они поддерживают масштабирование DPI. По умолчанию система считает, что DPI классических приложений не знает и растягивает их окна. Задав один из следующих доступных режимов осведомленности О DPI, приложения могут явно сообщить Windows, как они хотят обрабатывать масштабирование DPI:

DPI не знает

Отрисовка неузнанных приложений DPI составляет 96 (100%). Всякий раз, когда эти приложения выполняются на экране с масштабом отображения больше 96 DPI, Windows растянет растровое изображение приложения до ожидаемого физического размера. Это приводит к тому, что приложение отображается размытым.

Осведомленность о DPI системы

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

Осведомленность о DPI для каждого монитора и на мониторе (V2)

Рекомендуется обновлять классические приложения для использования режима осведомленности о DPI на мониторе, что позволяет им немедленно отображаться правильно при изменении DPI. Когда приложение сообщает Windows, что он хочет запустить в этом режиме, Windows не будет растягивать растровое изображение приложения при изменении DPI, а не отправлять WM_DPICHANGED в окно приложения. После этого приложение несет полную ответственность за изменение размера для нового DPI. Большинство платформ пользовательского интерфейса, используемых классическими приложениями (общие элементы управления Windows (comctl32), Windows Forms, Windows Presentation Framework и т. д.) не поддерживают автоматическое масштабирование DPI, требуя от разработчиков изменять размер и изменять положение содержимого своих окон.

Существует две версии осведомленности per-Monitor о том, что приложение может зарегистрировать себя как: версия 1 и версия 2 (PMv2). Регистрация процесса в режиме осведомленности PMv2 приводит к следующим результатам:

  1. Приложение уведомляется при изменении DPI (как верхнего уровня, так и дочерних HWND)
  2. Приложение, отображающее необработанные пиксели каждого дисплея
  3. Приложение никогда не масштабируется по растровой карте Windows
  4. Автоматическая не клиентская область (окно подпись, полосы прокрутки и т. д.) Масштабирование DPI в Windows
  5. Диалоговые окна Win32 (из CreateDialog) автоматически масштабируются по windows
  6. Растровые ресурсы нарисованных тем в общих элементах управления (проверка boxes, фона кнопок и т. д.) автоматически отрисовываются в соответствующем коэффициенте масштабирования DPI.

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

Примечание.

Осведомленность о каждом мониторе версии 1 (PMv1) очень ограничена. Рекомендуется использовать PMv2 для приложений.

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

Режим осведомленности о DPI Появилась версия Windows Представление DPI приложения Поведение при изменении DPI
Знают Н/П Все отображение — 96 DPI Растяжение растровых изображений (размытое)
Системные Vista Все отображения имеют один и тот же DPI (DPI основного дисплея во время запуска текущего сеанса пользователя) Растяжение растровых изображений (размытое)
На монитор 8.1 DPI отображения, на котором находится окно приложения в основном
  • HWND верхнего уровня уведомляется об изменении DPI
  • Нет масштабирования DPI элементов пользовательского интерфейса.

Для каждого монитора версии 2 Windows 10 Creators Update (1703) DPI отображения, на котором находится окно приложения в основном
  • HWND верхнего уровня и дочерних HWND уведомляются об изменении DPI

Автоматическое масштабирование DPI:
  • Не клиентская область
  • Растровые изображения нарисованных тем в общих элементах управления (comctl32 V6)
  • Диалоги (CreateDialog)

Осведомленность о DPI на мониторе (V1)

Режим осведомленности о DPI для каждого монитора версии 1 (PMv1) появился в Windows 8.1. Этот режим осведомленности о DPI очень ограничен и предоставляет только перечисленные ниже функциональные возможности. Рекомендуется, чтобы классические приложения использовали режим осведомленности per-Monitor версии 2, поддерживаемый в Windows 10 1703 или более поздней версии.

Начальная поддержка осведомленности для каждого монитора предоставляет только следующие приложения:

  1. HWND верхнего уровня уведомляются об изменении DPI и предоставляют новый предлагаемый размер.
  2. Windows не будет растягивать пользовательский интерфейс приложения
  3. Приложение видит все отображаемые в физических пикселях (см. виртуализацию)

В Windows 10 1607 или более поздней версии приложения PMv1 также могут вызывать EnableNonClientDpiScaling во время WM_NCCREATE, чтобы запросить, чтобы Windows правильно масштабировал не клиентскую область окна.

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

В таблице ниже показан уровень поддержки осведомленности о DPI для каждого монитора, предоставляемой различными платформами пользовательского интерфейса Windows по состоянию на Windows 10 1703:

Платформа / технология Поддержка Версия ОС Масштабирование DPI, обработанное Дополнительные материалы
Универсальная платформа Windows (UWP) Полностью 1607 Платформа пользовательского интерфейса Универсальная платформа Windows (UWP)
Необработанные элементы управления Win32/Common Controls V6 (comctl32.dll)
  • Уведомления об изменении DPI, отправленные всем HWND
  • Ресурсы, нарисованные темой, правильно отображаются в общих элементах управления
  • Автоматическое масштабирование DPI для диалоговых окон
1703 Приложение Пример GitHub
Windows Forms Ограниченное автоматическое масштабирование DPI на монитор для некоторых элементов управления 1703 Платформа пользовательского интерфейса Поддержка высокого DPI в Windows Forms
Windows Presentation Foundation (WPF) Собственные приложения WPF будут масштабировать WPF DPI, размещенные в других платформах и других платформах, размещенных в WPF, не масштабируются автоматически. 1607 Платформа пользовательского интерфейса Пример GitHub
GDI нет Н/П Приложение См. масштабирование GDI с высоким уровнем DPI
GDI+ нет Н/П Приложение См. масштабирование GDI с высоким уровнем DPI
MFC нет Н/П Приложение Н/П

Обновление существующих приложений

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

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

Кроме того, в случае программирования Win32 многие API Win32 не имеют никакого DPI или контекста отображения, поэтому они будут возвращать только значения относительно системного DPI. Это может быть полезно, чтобы понять код, чтобы найти некоторые из этих API и заменить их вариантами с поддержкой DPI. Ниже приведены некоторые распространенные API с поддержкой DPI:

Одна версия 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 кода создания окна, масштабируемого положения и размера дочернего HWND для DPI родительского окна
  2. Реагирование на изменение DPI путем изменения положения и изменения размера дочернего HWND
  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. Помечайте процесс в соответствии с параметром DPI (V2) с помощью манифеста приложения (или другого метода в зависимости от используемой платформы пользовательского интерфейса).
  2. Сделайте логику макета пользовательского интерфейса повторно используемым и переместите его из кода инициализации приложения, чтобы его можно было повторно использовать при изменении DPI (WM_DPICHANGED в случае программирования Windows (Win32).
  3. Отмените любой код, предполагающий, что данные, конфиденциальные для DPI (DPI/fonts/sizes/etc.) никогда не нужно обновлять. Это очень распространенная практика кэширования размеров шрифтов и значений DPI при инициализации процесса. При обновлении приложения для каждого монитора с учетом DPI данные, конфиденциальные данные DPI должны быть переоценены при обнаружении нового DPI.
  4. При изменении DPI перезагрузите (или повторно растеризовать) все растровые ресурсы для нового DPI или, при необходимости, растровое изображение растяните загруженные в данный момент ресурсы до правильного размера.
  5. Grep для API, которые не знают DPI для каждого монитора, и замените их API с поддержкой DPI для каждого монитора (где это применимо). Пример. Замените GetSystemMetrics на GetSystemMetricsForDpi.
  6. Протестируйте приложение в системе с несколькими дисплеями и несколькими DPI.
  7. Для всех окон верхнего уровня в приложении, которые не удается обновить до правильного масштабирования DPI, используйте масштабирование DPI в смешанном режиме (описано ниже), чтобы разрешить растяжение растровых изображений этих окон верхнего уровня системой.

Масштабирование DPI в смешанном режиме (масштабирование DPI в подпроцессе)

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

Ниже приведена иллюстрация того, как это может выглядеть: вы обновляете основной пользовательский интерфейс приложения ("Главное окно" на рисунке) для запуска с осведомленностью о DPI для каждого монитора при запуске других окон в существующем режиме ("Дополнительное окно").

differences in dpi scaling between awareness modes

До юбилейного обновления Windows 10 (1607) режим осведомленности о DPI процесса был свойством на уровне процесса. Начиная с юбилейного обновления Windows 10, это свойство теперь можно задать для каждого окна верхнего уровня . (Дочерние окна должны продолжать соответствовать размеру масштабирования родительского элемента.) Окно верхнего уровня определяется как окно без родительского элемента. Обычно это обычное окно с свертыванием, развертыванием и закрытием кнопок. Сценарий, в котором осведомленность о DPI подпроцессе предназначена для того, чтобы дополнительный пользовательский интерфейс масштабировался Windows (растровое изображение растянуто) при фокусе времени и ресурсов на обновлении основного пользовательского интерфейса.

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

При использовании масштабирования DPI подпроцесса вы можете полагаться на Windows, чтобы выполнить некоторые из масштабирования 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 взаимодействует с дисплеем в другом DPI, отличном от того, что использовался при запуске сеанса текущего пользователя, Windows будет масштабировать некоторые вызовы API в пространство координат, которое HWND будет использовать, если бы он работал на исходном коэффициенте масштабирования DPI.

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

Многие API Windows не имеют контекста DPI

Многие устаревшие API Windows не включают контекст DPI или HWND в составе интерфейса. В результате разработчикам часто приходится выполнять дополнительную работу для обработки масштабирования любой конфиденциальной информации DPI, например размеров, точек или значков. Например, разработчики, использующие LoadIcon , должны либо растровое изображение растянутые значки, либо использовать альтернативные API для загрузки значков правильного размера для соответствующего DPI, например LoadImage.

Принудительный сброс осведомленности о DPI на уровне процесса

Как правило, режим осведомленности о DPI процесса не может быть изменен после инициализации процесса. Однако Windows может принудительно изменить режим осведомленности о DPI процесса, если вы пытаетесь нарушить требование, что все HWND в дереве окон имеют один и тот же режим осведомленности о DPI. Во всех версиях Windows, начиная с Windows 10 1703, невозможно использовать разные HWND в дереве HWND в разных режимах осведомленности о DPI. Если вы пытаетесь создать связь дочернего родителя, которая нарушает это правило, можно сбросить осведомленность о DPI всего процесса. Это может быть вызвано следующими способами:

  1. Вызов CreateWindow, в котором переданное в родительском окне, отличается от режима осведомленности о DPI, отличном от вызывающего потока.
  2. Вызов SetParent, в котором две окна связаны с разными режимами осведомленности О DPI.

В следующей таблице показано, что происходит, если вы пытаетесь нарушить это правило:

Операция Windows 8.1 Windows 10 (1607 и более ранние версии) Windows 10 (1703 и более поздние версии)
CreateWindow (in-Proc) Н/П Дочерний наследует (смешанный режим) Дочерний наследует (смешанный режим)
CreateWindow (Cross-Proc) Принудительный сброс (процесса вызывающего объекта) Дочерний наследует (смешанный режим) Принудительный сброс (процесса вызывающего объекта)
SetParent (In-Proc) Н/П Принудительный сброс (текущего процесса) Сбой (ERROR_INVALID_STATE)
SetParent (Cross-Proc) Принудительный сброс (процесса дочернего окна) Принудительный сброс (процесса дочернего окна) Принудительный сброс (процесса дочернего окна)

Справочник по API высокого уровня DPI

API масштабирования и DPI в смешанном режиме.