@retif попросил меня предоставить рецензию, когда я прокомментировал, что знал об этих типах проблем несколько месяцев назад.
TLDR
При решении проблемы абсолютного позиционирования с Qt Windows в ОС Windows, особенно в Windows 10, лучше всего использовать знание системного DPI. Некоторая интерполяция требуется при переходе от координатных пространств Windows (с разными уровнями осведомленности о DPI) к координатным пространствам Qt, когда вы пытаетесь получить наилучшее масштабирование.
Вот что я сделал в приложении моей команды.
Проблема:
Действительно сложно выполнить абсолютное позиционирование окна Qt, когда есть несколько мониторов и несколько разрешений DPI, с которыми нужно бороться.
Окно нашего приложения «выскакивает» из значка панели задач Windows (или значка строки меню на Mac).
Исходный код брал координаты экрана Windows для значка в трее и использовал их в качестве ссылки для вычисления положения окна.
При запуске приложения, до инициализации Qt, мы установили переменную среды QT_SCALE_FACTOR
как значение с плавающей запятой (systemDPI/96.0)
. Образец кода:
HDC hdc = GetDC(nullptr);
unsigned int dpi = ::GetDeviceCaps(hdc, LOGPIXELSX);
stringstream dpiScaleFactor;
dpiScaleFactor << (dpi / 96.0);
qputenv("QT_SCALE_FACTOR", QByteArray::fromStdString(dpiScaleFactor.str()));
Приведенный выше код берет «шкалу DPI» основных мониторов и сообщает Qt, чтобы она соответствовала ей. Приятный эффект заключается в том, что Qt позволяет вычислять все масштабирование изначально, а не растягивать растровое изображение, как это делает Windows в приложении, не поддерживающем DPI.
Поскольку мы инициализируем Qt, используя переменную среды QT_SCALE_FACTOR
(на основе DPI основного монитора), мы использовали это значение для масштабирования координат Windows при преобразовании в координатное пространство Qt QScreen для этого начального размещения окна.
Все работало нормально на сценариях с одним монитором. Он отлично работал даже в сценариях с несколькими мониторами, если DPI на обоих мониторах был одинаковым. Но в конфигурациях из нескольких мониторов с разными DPI все получилось. Если окно должно было всплывать на неосновном мониторе в результате смены экрана или подключения (или отключения) проектора, происходили странные вещи. Окна Qt появлялись бы в неправильном положении. Или в некоторых случаях содержимое внутри окна масштабировалось неправильно. И когда это действительно срабатывало, возникала проблема «слишком большой» или «слишком маленькой» окна, масштабированного до одного DPI при расположении на мониторе аналогичного размера, работающем с другим DPI.
Мое первоначальное исследование показало, что координатное пространство Qt для различных геометрий QScreens выглядело не так. Координаты каждого прямоугольника QScreen были масштабированы на основе QT_SCALE_FACTOR, но соседние оси отдельных прямоугольников QScreen не были выровнены. например Один прямоугольник QScreen может быть {0,0,2559,1439}
, но монитор справа будет {3840,0,4920,1080}
. Что случилось с регионом, где 2560 <= x < 3840
? Потому что наш код, который масштабировал x и y на основе QT_SCALE_FACTOR или DPI, полагался на то, что основной монитор находится в (0,0), а все мониторы имеют смежные координатные пространства. Если бы наш код масштабировал предполагаемую координату положения до чего-то на другом мониторе, он мог бы оказаться в странном месте.
Потребовалось некоторое время, чтобы понять, что это не ошибка Qt как таковая. Просто Qt просто нормализует координатное пространство Windows, которое изначально имеет эти странные пробелы в координатном пространстве.
Исправление:
Лучшее решение состоит в том, чтобы сказать Qt, чтобы он масштабировался до настройки DPI основного монитора и запускал процесс в системном режиме DPI, а не в режиме DPI для каждого монитора. Преимущество этого заключается в том, что Qt позволяет масштабировать окно правильно, без размытия или пикселизации на основном мониторе, а также позволяет Windows масштабировать его при изменении монитора.
Немного предыстории. Прочитайте все в этом разделе Высокий Программирование DPI в MSDN. Хорошее чтение.
Вот что мы сделали.
Сохранил инициализацию QT_SCALE_FACTOR
, как описано выше.
Затем мы переключили инициализацию нашего процесса и Qt с поддержки DPI для каждого монитора на DPI с учетом системы. Преимущество system-dpi заключается в том, что он позволяет Windows автоматически масштабировать окна приложений до ожидаемого размера, когда мониторы из-под него меняются. (Все API-интерфейсы Windows действуют так, как будто все мониторы имеют одинаковый DPI). Как обсуждалось выше, Windows делает растяжение растрового изображения под капотом, когда DPI отличается от основного монитора. Таким образом, возникает «проблема размытости», с которой приходится бороться при переключении мониторов. Но это точно лучше, чем то, что было раньше!
По умолчанию Qt попытается инициализировать процесс для приложения, поддерживающего работу с каждым монитором. Чтобы заставить его работать с учетом системного dpi, вызовите SetProcessDpiAwareness
со значением PROCESS_SYSTEM_DPI_AWARE
очень рано при запуске приложения до инициализации Qt. После этого Qt не сможет его изменить.
Простое переключение на System-aware dpi устранило множество других проблем.
Окончательное исправление ошибки:
Поскольку мы размещаем наше окно в абсолютной позиции (непосредственно над значком системного трея на панели задач), мы полагаемся на Windows API,Shell_NotifyIconGetRect, чтобы получить координаты системного трея. И как только мы узнаем смещение панели задач, мы вычисляем верхнее/левое положение для положения нашего окна на экране. Назовем эту позицию X1,Y1
Однако координаты, возвращаемые из Shell_NotifyIconGetRect
в Windows 10, всегда будут исходными координатами «с учетом каждого монитора» и не масштабируются до системного DPI. Используйте PhysicalToLogicalPointForPerMonitorDPI перерабатывать. Этого API нет в Windows 7, но он и не нужен. Используйте LoadLibrary
и GetProcAddress
для этого API, если вы поддерживаете Windows 7. Если API не существует, просто пропустите этот шаг. Используйте PhysicalToLogicalPointForPerMonitorDPI
для преобразования X1,Y1
в системную координату DPI, которую мы будем называть X2,Y2
.
В идеале X2,Y2 передаются в методы Qt, такие как QQuickView::setPosition
Но....
Поскольку мы использовали переменную среды QT_SCALE_FACTOR
, чтобы заставить приложение масштабировать DPI основного монитора, все геометрии QScreen имели бы нормализованные координаты, которые отличались бы от того, что Windows использовала в качестве системы координат экрана. Таким образом, окончательная координата положения окна X2,Y2
, вычисленная выше, не будет отображаться в ожидаемую позицию в координатах Qt, если переменная окружения QT_SCALE_FACTOR
будет чем-то иным, кроме 1.0
Окончательное исправление для расчета конечной верхней/левой позиции окна Qt.
Вызовите EnumDisplayMonitors и перечислить список мониторов. Найдите монитор, на котором находится X2,Y2
, о котором говорилось выше. Сохраните MONITORINFOEX.szDevice
, а также MONITORINFOEX.rcMonitor
геометрию в переменной с именем rect
.
Call QGuiApplication::screens()
и перечислите эти объекты, чтобы найти экземпляр QScreen, чье свойство name()
соответствует MONITORINFOEX.szDevice
на предыдущем шаге. А затем сохраните QRect
, возвращенное методом geometry()
этого QScreen
, в переменную с именем qRect
. Сохраните QScreen в переменной указателя с именем pScreen
Последний шаг преобразования X2,Y2
в XFinal,YFinal
— это следующий алгоритм:
XFinal = (X2 - rect.left) * qRect.width
------------------------------- + qRect.left
rect.width
YFinal = (Y2 - rect.top) * qRect.height
------------------------------- + qRect.top
rect.height
Это просто базовая интерполяция между отображениями экранных координат.
Затем окончательное позиционирование окна заключается в установке обеих позиций QScreen и XFinal,YFinal в объекте представления Qt.
QPoint position(XFinal, YFinal);
pView->setScreen(pScreen);
pView->setPosition(position);
Рассматриваются другие решения:
Существует режим Qt, который называется Qt::AA_EnableHighDpiScaling, который можно задать для объекта QGuiApplication. Он делает многое из вышеперечисленного за вас, за исключением того, что он заставляет все масштабирование быть интегральным коэффициентом масштабирования (1x, 2x, 3x и т. д., никогда 1,5 или 1,75). Это не сработало для нас, потому что 2-кратное масштабирование окна при настройке DPI 150% выглядело слишком большим.
person
selbie
schedule
01.06.2018