【问题标题】:How to properly save window state in QML如何在 QML 中正确保存窗口状态
【发布时间】:2017-05-06 08:04:50
【问题描述】:

我阅读了 Qt 文档,查看了 SDK 提供的几个示例,我从源代码构建了 Qt Creator,以了解 Qt 开发人员是如何做到的......仍然没有运气。

我正在为 Windows 和 Mac 开发一个跨平台应用程序。在 Mac 方面,我基本上可以尝试我的任何解决方案,它们都可以完美运行(我想这要归功于 MacOS)。另一方面,在 Windows 上,我总是会发现某种错误或粗略的行为。

在我详细介绍之前,我的问题的根源在于支持具有不同分辨率的显示器的多个显示器环境。

简而言之我的两个主要解决方案:

  1. 由于我主要使用 QML 编写应用程序,因此我使用 ApplicationWindow 作为主窗口。我将ApplicationWindow 的状态保存在Settings 中。我的代码考虑了先前保存的位置是否仍然有效(例如,如果应用程序在监视器上时关闭,不再可用)......我必须这样做的唯一原因是因为 Windows 只会在“外层空间”中打开我的应用程序窗口(Mac 会自动处理)。如果我在其中一个监视器上关闭我的应用程序,然后我更改其他监视器的缩放因子,然后我重新打开我的应用程序,我的应用程序(在 Windows 上)会进入一个非常奇怪的状态。它在正确的显示器上打开,但它的比例太大了,UI 元素只是奇怪地浮动。如果我调整窗口大小,一切都会恢复正常。
  2. 我将我的 QML ApplicationWindow 暴露给 C++ 并放入 QWidget 容器中,然后我通过将其设置为 setCentralWidget 将其附加到 QMainWindow。通过这种方法,我可以访问saveGeometryrestoreGeometry,它们会自动处理多个显示器的定位,但我在 1. 中描述的缩放异常仍然存在。

有人解决了吗?在此先感谢您的帮助和提示

【问题讨论】:

  • 我对这个问题很熟悉......今晚晚些时候会写一些东西。
  • 谢谢@selbie,等不及了……我现在不太清楚。
  • 哇。我忘记回复了。圣诞假期的欢呼一定是太多了。希望我能回复。我们做的主要事情是在系统 DPI 感知模式下初始化进程并设置环境标志以阻止 Qt 尝试更改进程执行每个监视器的 DPI 感知。然后因为我们还使用了 QT_SCALE_FACTOR,所以我们编写了一些代码来将 Win32 屏幕坐标的坐标标准化为缩放的 QScreen。还有一点……我今晚会试着写一个答案。
  • @retif - 终于有时间写一个答案。哇,听起来真的很复杂。但它适用于绝对定位。

标签: qt qml qmainwindow qt5.6


【解决方案1】:

当我几个月前评论说我知道这些类型的问题时,@retif 要求我提供一篇文章。

TLDR

在 Windows 操作系统(尤其是在 Windows 10 上)处理 Qt Windows 的绝对定位问题时,最好使用系统 DPI 感知。当您试图获得最佳缩放时,从 Windows 坐标空间(在不同的 DPI 感知级别)到 Qt 坐标空间时,需要进行一些插值。

这是我在团队申请中所做的。

问题:

当有多个显示器和多个 DPI 分辨率需要处理时,很难对 Qt 窗口进行绝对定位。

我们的应用程序窗口从 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_SCALE_FACTOR 环境变量(基于主监视器 DPI)初始化 Qt,所以我们在转换到 Qt 的 QScreen 坐标空间以用于初始窗口放置时使用该值来缩放 Windows 坐标。

在单显示器场景下一切正常。只要两台显示器上的 DPI 相同,它甚至在多显示器场景下也能正常工作。但是在具有不同 DPI 的多台显示器的配置上,事情就发生了。如果由于屏幕更改或插入(或拔出)投影仪而不得不在非主监视器上弹出窗口,就会发生奇怪的事情。 Qt 窗口会出现在错误的位置。或者在某些情况下,窗口内的内容会不正确地缩放。当它确实起作用时,当定位在以不同 DPI 运行的类似大小的显示器上时,会出现缩放到一个 DPI 的窗口“太大”或“太小”的问题。

我的初步调查显示,Qt 的不同 QScreens 几何图形的坐标空间看起来不对劲。每个 QScreen 矩形的坐标是根据 QT_SCALE_FACTOR 缩放的,但是各个 QScreen 矩形的相邻轴没有对齐。例如一个 QScreen 矩形可能是 {0,0,2559,1439},但右侧的监视器将是 {3840,0,4920,1080}2560 &lt;= x &lt; 3840所在的地区发生了什么?因为我们基于 QT_SCALE_FACTOR 或 DPI 缩放 x 和 y 的代码依赖于位于 (0,0) 的主监视器并且所有监视器都具有相邻的坐标空间。如果我们的代码将假定的位置坐标缩放到另一个监视器上的某个东西,它可能会定位在一个奇怪的地方。

花了一段时间才意识到这本身不是 Qt 错误。只是 Qt 只是对 Windows 坐标空间进行了规范化,一开始就有这些奇怪的坐标空间间隙。

修复:

更好的解决方法是告诉 Qt 缩放到主监视器的 DPI 设置,并在系统感知 DPI 模式而不是每监视器感知 DPI 模式下运行进程。这样做的好处是可以让 Qt 正确缩放窗口,并且在主监视器上没有模糊或像素化,并让 Windows 在监视器更改时对其进行缩放。

一点背景。阅读 MSDN 上 High DPI programming 这部分的所有内容。很好的阅读。

这就是我们所做的。

如上所述保持QT_SCALE_FACTOR 的初始化。

然后我们将进程和 Qt 的初始化从每监视器 DPI 感知切换到系统感知 DPI。 system-dpi 的好处是它允许 Windows 自动将应用程序窗口缩放到预期的大小,因为监视器从它下面改变。 (所有 Windows API 的行为就像所有显示器都具有相同的 DPI)。如上所述,当 DPI 与主显示器不同时,Windows 会在后台进行位图拉伸。因此,在显示器切换上存在一个“模糊问题”需要解决。但它肯定比以前做得更好!

默认情况下,Qt 会尝试将进程初始化为每个监视器感知的应用程序。要强制它以 system-dpi 感知运行,请在 Qt 初始化之前的应用程序启动的早期调用 SetProcessDpiAwareness 并使用 PROCESS_SYSTEM_DPI_AWARE 的值。之后 Qt 将无法更改它。

只需切换到系统感知 dpi 即可解决许多其他问题。

最终错误修复:

因为我们将窗口定位在绝对位置(任务托盘中系统托盘图标的正上方),我们依靠 Windows APIShell_NotifyIconGetRect 为我们提供系统托盘的坐标。一旦我们知道系统托盘的偏移量,我们就会计算一个顶部/左侧位置,以便我们的窗口位于屏幕上。我们称这个职位为X1,Y1

但是,在 Windows 10 上从 Shell_NotifyIconGetRect 返回的坐标将始终是“每个显示器感知”的本机坐标,并且不会缩放到系统 DPI。使用PhysicalToLogicalPointForPerMonitorDPI 进行转换。此 API 在 Windows 7 上不存在,但不是必需的。如果您支持 Windows 7,请为此 API 使用 LoadLibraryGetProcAddress。如果 API 不存在,请跳过此步骤。使用PhysicalToLogicalPointForPerMonitorDPIX1,Y1 转换为系统感知的DPI 坐标,我们将调用X2,Y2

理想情况下,X2,Y2 被传递给 Qt 方法,例如 QQuickView::setPosition 但是....

因为我们使用 QT_SCALE_FACTOR 环境变量来让应用程序缩放主显示器 DPI,所以所有 QScreen 几何图形都将具有与 Windows 用作屏幕坐标系不同的标准化坐标。因此,如果QT_SCALE_FACTOR 环境变量不是1.0,则上面计算的X2,Y2 的最终窗口位置坐标不会映射到Qt 坐标中的预期位置

最终修复以计算 Qt 窗口的最终顶部/左侧位置。

  • 调用EnumDisplayMonitors 并枚举监视器列表。找到上面讨论的X2,Y2 所在的监视器。将 MONITORINFOEX.szDeviceMONITORINFOEX.rcMonitor 几何图形保存在名为 rect 的变量中

  • Call QGuiApplication::screens() 并枚举这些对象以查找其name() 属性与上一步中的MONITORINFOEX.szDevice 匹配的QScreen 实例。然后将这个QScreengeometry() 方法返回的QRect 保存到一个名为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

这只是屏幕坐标映射之间的基本插值。

那么最后的窗口定位就是在Qt的view对象上同时设置QScreen和XFinal,YFinal位置。

QPoint position(XFinal, YFinal);
pView->setScreen(pScreen);
pView->setPosition(position);

考虑的其他解决方案:

Qt 模式称为Qt::AA_EnableHighDpiScaling,可以在 QGuiApplication 对象上设置。它为您完成了上述大部分工作,除了它强制所有缩放为一个积分比例因子(1x、2x、3x 等......从不 1.5 或 1.75)。这对我们不起作用,因为在 DPI 设置为 150% 时将窗口缩放 2 倍看起来太大了。

【讨论】:

  • 没想到问题会这么大。谢谢你这么详细的回答。
  • 我也曾把这只兔子整过一次……谢谢你把它写下来。我最终做了Qt::AA_EnableHighDpiScaling,但正如你所说,这不是最好的解决方案。将尝试您的建议。
  • 我希望这会有所帮助。 Silex,我认为您可能不仅需要保存应用程序的几何图形,还需要保存屏幕几何图形,也许还需要保存窗口所在的 devicePixelRatio。当您的应用程序重新启动时,如果当前屏幕配置或 devicePixelRatio 在您保存后发生了变化,您应该丢弃这些设置(开始清理)或插入到新的主监视器。
猜你喜欢
  • 2015-10-04
  • 2022-11-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多