Как правильно сохранить состояние окна в QML

Я прочитал документацию Qt, я проверил пару примеров, поставляемых с SDK, я собираю Qt Creator из исходного кода, чтобы посмотреть, как это делают разработчики Qt... до сих пор не повезло.

Я разрабатываю кроссплатформенное приложение для Windows и Mac. На стороне Mac я могу попробовать практически любое из моих решений, все они работают отлично (я думаю, это благодаря MacOS). С другой стороны, в Windows я всегда нахожу какую-то ошибку или схематичное поведение.

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

В двух словах мои два основных решения:

  1. Поскольку я пишу свое приложение в основном на QML, я использую ApplicationWindow для своего главного окна. Я сохраняю состояние моего ApplicationWindow в Settings. Мой код учитывает, действительна ли ранее сохраненная позиция (например, если приложение было закрыто, когда оно было на мониторе, который больше недоступен)... единственная причина, по которой я должен это сделать, потому что Windows просто открыть окно моего приложения в «космическом пространстве» (Mac обрабатывает это автоматически). Мое приложение (в Windows) переходит в действительно странное состояние, если я закрываю свое приложение на одном из мониторов, а затем изменяю коэффициент масштабирования других мониторов, а затем снова открываю свое приложение. Он открывается на правом мониторе, но сильно масштабируется, а элементы пользовательского интерфейса просто странно плавают. Если я изменю размер окна, все вернется в норму.
  2. Я представил свой QML ApplicationWindow для C++, помещенный в контейнер QWidget, который затем я прикрепил к QMainWindow, установив его как setCentralWidget. С помощью этого метода у меня есть доступ к saveGeometry и restoreGeometry, которые автоматически заботятся о позиционировании нескольких мониторов, но аномалия масштабирования, описанная в 1., все еще сохраняется.

Кто-нибудь решил это? Заранее спасибо за любую помощь и хин


person Silex    schedule 21.12.2016    source источник
comment
Я хорошо знаком с этой проблемой... Вечером что-нибудь напишу.   -  person selbie    schedule 21.12.2016
comment
Спасибо, @selbie, не могу дождаться... Сейчас я так мало знаю.   -  person Silex    schedule 21.12.2016
comment
ожидание @selbie   -  person retif    schedule 24.05.2018
comment
Ух ты. Я забыл ответить. Должно быть, это было слишком много праздничного настроения. Надеюсь, я смогу ответить. Главное, что мы сделали, — это инициализировали процесс в режиме с учетом системного DPI и установили флаг среды, чтобы заблокировать Qt от попыток изменить процесс для определения DPI для каждого монитора. Затем, поскольку мы также использовали QT_SCALE_FACTOR, мы написали некоторый код для нормализации координат экрана Win32 до масштабированного QScreen. Там еще немного... Постараюсь сегодня вечером написать ответ.   -  person selbie    schedule 25.05.2018
comment
@retif - наконец-то дошли руки написать ответ. Вау, звучит очень сложно. Но это работает, когда дело доходит до абсолютного позиционирования.   -  person selbie    schedule 01.06.2018


Ответы (1)


@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
comment
Я не ожидал, что проблема будет такой большой. Спасибо за такой подробный ответ. - person retif; 01.06.2018
comment
Я тоже один раз спускался с этим кроликом... спасибо, что записали. В итоге я сделал Qt::AA_EnableHighDpiScaling, но, как вы сказали, это не лучшее решение. Попробую то, что вы предложили. - person Silex; 01.06.2018
comment
Надеюсь, это поможет. Silex, я думаю, вам может понадобиться сохранить не только геометрию приложения, но и геометрию экрана и, возможно, devicePixelRatio, на котором было окно. Когда ваше приложение запускается, если текущая конфигурация экрана или devicePixelRatio изменились с момента сохранения, вы должны удалить эти настройки (начать с очистки) или интерполировать на новый основной монитор. - person selbie; 01.06.2018