如何在QML中正确保存窗口状态

Sil*_*lex 6 qt qml qmainwindow qt5.6

我阅读了Qt Documentations,我查看了SDK提供的一些示例,我从源代码构建了Qt Creator,看看Qt开发者是如何做到的......仍然没有运气.

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

在我进入更多细节之前,我的问题的根源是支持具有不同分辨率的监视器的多个监视器环境.

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

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

有人解决了吗?在此先感谢任何帮助和欣赏

sel*_*bie 5

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

TLDR

在 Windows 操作系统上处理 Qt Windows 的绝对定位问题时 - 特别是在 Windows 10 上,最好使用系统 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()));    
Run Code Online (Sandbox Code Playgroud)

上面的代码采用主监视器“DPI 比例”并告诉 Qt 进行匹配。它具有让 Qt 本地计算所有缩放而不是像 Windows 在非 DPI 感知应用程序中所做的位图拉伸的令人愉快的效果。

因为我们使用QT_SCALE_FACTOR环境变量(基于主监视器 DPI)初始化 Qt,所以在转换为 Qt 的 QScreen 坐标空间时,我们使用该值来缩放 Windows 坐标以进行初始窗口放置。

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

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

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

修复:

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

一点背景。阅读MSDN上高 DPI 编程这一节中的所有内容。很好的阅读。

这就是我们所做的。

保持上述的初始化QT_SCALE_FACTOR

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

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

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

最终错误修复:

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

但是,从Shell_NotifyIconGetRectWindows 10 上返回的坐标将始终是“每显示器感知”本机坐标,而不是缩放到系统 DPI。使用PhysicalToLogicalPointForPerMonitorDPI进行转换。此 API 在 Windows 7 上不存在,但不需要。如果您支持 Windows 7,请使用LoadLibraryGetProcAddress用于此 API。如果该 API 不存在,请跳过此步骤。使用PhysicalToLogicalPointForPerMonitorDPI转换X1,Y1为系统感知DPI协调wel'll通话X2,Y2

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

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

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

  • 调用EnumDisplayMonitors并枚举监视器列表。找到X2,Y2上面讨论过的显示器。保存关闭MONITORINFOEX.szDevice,以及在MONITORINFOEX.rcMonitor几何中一个叫做变量rect

  • Call QGuiApplication::screens()并枚举这些对象以查找其name()属性与MONITORINFOEX.szDevice上一步中的匹配的QScreen 实例。然后将this 方法QRect返回的值保存到一个名为. 将 QScreen 保存到一个名为的指针变量中geometry()QScreenqRectpScreen

转换X2,Y2为的最后一步XFinal,YFinal是这个算法:

XFinal  =       (X2 - rect.left) * qRect.width
                -------------------------------  + qRect.left
                           rect.width

YFinal  =       (Y2 - rect.top) * qRect.height
                -------------------------------  + qRect.top
                           rect.height
Run Code Online (Sandbox Code Playgroud)

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

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

QPoint position(XFinal, YFinal);
pView->setScreen(pScreen);
pView->setPosition(position);
Run Code Online (Sandbox Code Playgroud)

考虑的其他解决方案:

可以在 QGuiApplication 对象上设置名为Qt::AA_EnableHighDpiScaling的 Qt 模式。它为您完成了上述大部分工作,除了它强制所有缩放比例为整数比例因子(1x、2x、3x 等...永远不会是 1.5 或 1.75)。这对我们不起作用,因为在 DPI 设置为 150% 的情况下,窗口的 2 倍缩放看起来太大了。