如何在 QML 中正确保存 window 状态
How to properly save window state in QML
我阅读了 Qt 文档,检查了 SDK 提供的几个示例,我从源代码构建了 Qt Creator 以查看 Qt 开发人员是如何做到的......仍然没有运气。
我正在为 Windows 和 Mac 开发跨平台应用程序。在 Mac 方面,我基本上可以尝试我的任何解决方案,所有解决方案都可以完美运行(我想这要归功于 MacOS)。另一方面,在 Windows 上,我总是发现某种错误或粗略的行为。
在我进入更多细节之前,我的问题的根源是支持具有不同分辨率的多个监视器环境。
简而言之,我的两个主要解决方案:
- 因为我主要用 QML 编写我的应用程序,所以我主要使用
ApplicationWindow
window。我将 ApplicationWindow
的状态保存在 Settings
中。我的代码考虑了之前保存的位置是否仍然有效(例如,如果应用程序在监视器上关闭时关闭,不再可用)......我必须这样做的唯一原因是 Windows 只会在 "outer space" 中打开我应用程序的 window(Mac 会自动处理)。如果我在其中一个监视器上关闭我的应用程序,然后我更改另一个监视器缩放因子,然后我重新打开我的应用程序,我的应用程序(在 Windows 上)会进入一个非常奇怪的状态。它在右侧的显示器上打开,但它的缩放比例过大,UI 元素只是奇怪地浮动。如果我调整 window 的大小,一切都会恢复正常。
- 我将我的 QML
ApplicationWindow
暴露给放入 QWidget 容器中的 C++,然后我通过将其设置为 setCentralWidget
附加到 QMainWindow
。通过这种方法,我可以访问 saveGeometry
和 restoreGeometry
,它们会自动处理多显示器定位,但我在 1. 中描述的缩放异常仍然存在。
有人解决了吗?在此先感谢您的帮助和 hin
当我评论说我几个月前就知道这些类型的问题时,@retif 要求我提供一篇文章。
TLDR
在 Windows OS 上使用 Qt Windows 处理绝对定位问题时 - 特别是在 Windows 10 上,最好使用 system-DPI意识。当您试图获得最佳缩放比例时,从 Windows 坐标 spaces(在不同的 DPI 感知级别)到 Qt 坐标 spaces 时需要进行一些插值。
这是我在团队申请中所做的。
问题:
当有多个显示器和多个 DPI 分辨率需要应对时,Qt Window 的绝对定位真的很难。
我们的应用程序 window "pops up" 来自 Windows 任务托盘图标(或 Mac 上的菜单栏图标)。
原始代码将采用 Windows 托盘图标的屏幕坐标位置,并以此为参考来计算 window.
的位置
在应用程序启动时,在初始化 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 scale" 并告诉 Qt 匹配它。它有一个令人愉快的效果,让 Qt 计算所有本地缩放,而不是像 Windows 在非 DPI 感知应用程序中那样的位图拉伸。
因为我们使用 QT_SCALE_FACTOR
环境变量(基于主监视器 DPI)初始化 Qt,所以我们在转换为 Qt 的 QScreen 坐标时使用该值来缩放 Windows 坐标 space 对于这个初始 window 展示位置。
在单显示器场景中一切正常。只要两台显示器上的 DPI 相同,它甚至可以在多显示器场景中正常工作。但是在具有不同 DPI 的多个显示器的配置上,事情就开始了。如果 window 由于屏幕变化或投影仪插入(或拔出)而不得不在非主显示器上弹出,就会发生奇怪的事情。 Qt windows 会出现在错误的位置。或者在某些情况下,window 中的内容将无法正确缩放。当它确实起作用时,当放置在类似尺寸的显示器上时 "too big" 或 "too small" 会出现 windows 缩放到一个 DPI 的问题 运行ning 在不同的 DPI .
我的初步调查显示,Qt 的坐标 space 用于不同的 QScreens 几何形状看起来不对。每个 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) 并且所有监视器都具有相邻坐标 spaces。如果我们的代码将假设的位置坐标缩放到另一台显示器上的某物,它可能会定位在一个奇怪的地方。
花了一段时间才意识到这本身并不是 Qt 错误。只是 Qt 只是对 Windows 坐标 space 进行标准化,这些坐标 space 开始时有这些奇怪的坐标 space 间隙。
修正:
更好的解决方法是告诉 Qt 缩放到主监视器的 DPI 设置,并且 运行 进程在系统感知 DPI 模式下而不是每个监视器感知 DPI 模式下。这样做的好处是让 Qt 正确缩放 window 并且不会在主监视器上出现模糊或像素化,并让 Windows 在监视器更改时缩放它。
一些背景知识。在 MSDN 上阅读 High DPI programming 本节中的所有内容。不错的读物。
这是我们所做的。
保留 QT_SCALE_FACTOR
的初始化,如上所述。
然后我们将进程和 Qt 的初始化从每个监视器 DPI 感知切换到系统感知 DPI。 system-dpi 的好处是它允许 Windows 自动缩放应用程序 windows 到预期的大小,因为显示器从它下面改变。 (所有 Windows API 就好像所有显示器都具有相同的 DPI)。如上所述,当 DPI 与主监视器不同时,Windows 正在后台进行位图拉伸。所以有一个 "blurriness issue" 来应对显示器切换。但它肯定比以前做的更好!
默认情况下,Qt 会尝试将进程初始化为每个监视器感知的应用程序。要在系统 dpi 感知中强制它为 运行,请在 Qt 初始化之前在应用程序启动的早期调用 SetProcessDpiAwareness
并使用值 PROCESS_SYSTEM_DPI_AWARE
。以后Qt就改不了了
只需切换到系统感知 dpi 即可解决一系列其他问题。
最终错误修复:
因为我们将 window 定位在 绝对位置 (任务托盘中系统托盘图标的正上方),我们依赖于 Windows API,Shell_NotifyIconGetRect 给我们系统托盘的坐标。一旦我们知道了系统托盘的偏移量,我们就会计算出一个 top/left 的位置来定位我们的 window 在屏幕上的位置。我们称这个位置为 X1,Y1
但是,从 Shell_NotifyIconGetRect
在 Windows 10 返回的坐标将始终是 "per-monitor aware" 本机坐标,不会缩放到系统 DPI。使用 PhysicalToLogicalPointForPerMonitorDPI 进行转换。 API 在 Windows 7 上不存在,但不需要。如果您支持Windows,则此API使用LoadLibrary
和GetProcAddress
7. 如果API不存在,则跳过此步骤。使用 PhysicalToLogicalPointForPerMonitorDPI
将 X1,Y1
转换为系统感知的 DPI 坐标,我们将调用 X2,Y2
.
理想情况下,X2,Y2 被传递给 Qt 方法,例如 QQuickView::setPosition
但是....
因为我们使用 QT_SCALE_FACTOR
环境变量来让应用程序缩放主监视器 DPI,所以所有 QScreen 几何图形都将具有不同于 Windows 用作屏幕的标准化坐标坐标系。因此,如果 QT_SCALE_FACTOR
环境变量不是 1.0
,那么上面计算的 X2,Y2
的最终 windows 位置坐标将不会映射到 Qt 坐标中的预期位置
获得 Qt window 计算的最终 top/left 位置的最终修复。
调用 EnumDisplayMonitors 并枚举监视器列表。找到上面讨论的 X2,Y2
所在的监视器。在名为 rect
的变量中保存 MONITORINFOEX.szDevice
和 MONITORINFOEX.rcMonitor
几何图形
Call QGuiApplication::screens()
并枚举这些对象,找到name()
属性与上一步的MONITORINFOEX.szDevice
匹配的QScreen实例。然后把这个QScreen
的geometry()
方法返回的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
这只是屏幕坐标映射之间的基本插值。
那么最后的window定位就是在Qt的视图对象上同时设置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)。这对我们不起作用,因为在 150% 的 DPI 设置下 window 的 2 倍缩放看起来太大了。
我阅读了 Qt 文档,检查了 SDK 提供的几个示例,我从源代码构建了 Qt Creator 以查看 Qt 开发人员是如何做到的......仍然没有运气。
我正在为 Windows 和 Mac 开发跨平台应用程序。在 Mac 方面,我基本上可以尝试我的任何解决方案,所有解决方案都可以完美运行(我想这要归功于 MacOS)。另一方面,在 Windows 上,我总是发现某种错误或粗略的行为。
在我进入更多细节之前,我的问题的根源是支持具有不同分辨率的多个监视器环境。
简而言之,我的两个主要解决方案:
- 因为我主要用 QML 编写我的应用程序,所以我主要使用
ApplicationWindow
window。我将ApplicationWindow
的状态保存在Settings
中。我的代码考虑了之前保存的位置是否仍然有效(例如,如果应用程序在监视器上关闭时关闭,不再可用)......我必须这样做的唯一原因是 Windows 只会在 "outer space" 中打开我应用程序的 window(Mac 会自动处理)。如果我在其中一个监视器上关闭我的应用程序,然后我更改另一个监视器缩放因子,然后我重新打开我的应用程序,我的应用程序(在 Windows 上)会进入一个非常奇怪的状态。它在右侧的显示器上打开,但它的缩放比例过大,UI 元素只是奇怪地浮动。如果我调整 window 的大小,一切都会恢复正常。 - 我将我的 QML
ApplicationWindow
暴露给放入 QWidget 容器中的 C++,然后我通过将其设置为setCentralWidget
附加到QMainWindow
。通过这种方法,我可以访问saveGeometry
和restoreGeometry
,它们会自动处理多显示器定位,但我在 1. 中描述的缩放异常仍然存在。
有人解决了吗?在此先感谢您的帮助和 hin
当我评论说我几个月前就知道这些类型的问题时,@retif 要求我提供一篇文章。
TLDR
在 Windows OS 上使用 Qt Windows 处理绝对定位问题时 - 特别是在 Windows 10 上,最好使用 system-DPI意识。当您试图获得最佳缩放比例时,从 Windows 坐标 spaces(在不同的 DPI 感知级别)到 Qt 坐标 spaces 时需要进行一些插值。
这是我在团队申请中所做的。
问题:
当有多个显示器和多个 DPI 分辨率需要应对时,Qt Window 的绝对定位真的很难。
我们的应用程序 window "pops up" 来自 Windows 任务托盘图标(或 Mac 上的菜单栏图标)。
原始代码将采用 Windows 托盘图标的屏幕坐标位置,并以此为参考来计算 window.
的位置在应用程序启动时,在初始化 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 scale" 并告诉 Qt 匹配它。它有一个令人愉快的效果,让 Qt 计算所有本地缩放,而不是像 Windows 在非 DPI 感知应用程序中那样的位图拉伸。
因为我们使用 QT_SCALE_FACTOR
环境变量(基于主监视器 DPI)初始化 Qt,所以我们在转换为 Qt 的 QScreen 坐标时使用该值来缩放 Windows 坐标 space 对于这个初始 window 展示位置。
在单显示器场景中一切正常。只要两台显示器上的 DPI 相同,它甚至可以在多显示器场景中正常工作。但是在具有不同 DPI 的多个显示器的配置上,事情就开始了。如果 window 由于屏幕变化或投影仪插入(或拔出)而不得不在非主显示器上弹出,就会发生奇怪的事情。 Qt windows 会出现在错误的位置。或者在某些情况下,window 中的内容将无法正确缩放。当它确实起作用时,当放置在类似尺寸的显示器上时 "too big" 或 "too small" 会出现 windows 缩放到一个 DPI 的问题 运行ning 在不同的 DPI .
我的初步调查显示,Qt 的坐标 space 用于不同的 QScreens 几何形状看起来不对。每个 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) 并且所有监视器都具有相邻坐标 spaces。如果我们的代码将假设的位置坐标缩放到另一台显示器上的某物,它可能会定位在一个奇怪的地方。
花了一段时间才意识到这本身并不是 Qt 错误。只是 Qt 只是对 Windows 坐标 space 进行标准化,这些坐标 space 开始时有这些奇怪的坐标 space 间隙。
修正:
更好的解决方法是告诉 Qt 缩放到主监视器的 DPI 设置,并且 运行 进程在系统感知 DPI 模式下而不是每个监视器感知 DPI 模式下。这样做的好处是让 Qt 正确缩放 window 并且不会在主监视器上出现模糊或像素化,并让 Windows 在监视器更改时缩放它。
一些背景知识。在 MSDN 上阅读 High DPI programming 本节中的所有内容。不错的读物。
这是我们所做的。
保留 QT_SCALE_FACTOR
的初始化,如上所述。
然后我们将进程和 Qt 的初始化从每个监视器 DPI 感知切换到系统感知 DPI。 system-dpi 的好处是它允许 Windows 自动缩放应用程序 windows 到预期的大小,因为显示器从它下面改变。 (所有 Windows API 就好像所有显示器都具有相同的 DPI)。如上所述,当 DPI 与主监视器不同时,Windows 正在后台进行位图拉伸。所以有一个 "blurriness issue" 来应对显示器切换。但它肯定比以前做的更好!
默认情况下,Qt 会尝试将进程初始化为每个监视器感知的应用程序。要在系统 dpi 感知中强制它为 运行,请在 Qt 初始化之前在应用程序启动的早期调用 SetProcessDpiAwareness
并使用值 PROCESS_SYSTEM_DPI_AWARE
。以后Qt就改不了了
只需切换到系统感知 dpi 即可解决一系列其他问题。
最终错误修复:
因为我们将 window 定位在 绝对位置 (任务托盘中系统托盘图标的正上方),我们依赖于 Windows API,Shell_NotifyIconGetRect 给我们系统托盘的坐标。一旦我们知道了系统托盘的偏移量,我们就会计算出一个 top/left 的位置来定位我们的 window 在屏幕上的位置。我们称这个位置为 X1,Y1
但是,从 Shell_NotifyIconGetRect
在 Windows 10 返回的坐标将始终是 "per-monitor aware" 本机坐标,不会缩放到系统 DPI。使用 PhysicalToLogicalPointForPerMonitorDPI 进行转换。 API 在 Windows 7 上不存在,但不需要。如果您支持Windows,则此API使用LoadLibrary
和GetProcAddress
7. 如果API不存在,则跳过此步骤。使用 PhysicalToLogicalPointForPerMonitorDPI
将 X1,Y1
转换为系统感知的 DPI 坐标,我们将调用 X2,Y2
.
理想情况下,X2,Y2 被传递给 Qt 方法,例如 QQuickView::setPosition
但是....
因为我们使用 QT_SCALE_FACTOR
环境变量来让应用程序缩放主监视器 DPI,所以所有 QScreen 几何图形都将具有不同于 Windows 用作屏幕的标准化坐标坐标系。因此,如果 QT_SCALE_FACTOR
环境变量不是 1.0
X2,Y2
的最终 windows 位置坐标将不会映射到 Qt 坐标中的预期位置
获得 Qt window 计算的最终 top/left 位置的最终修复。
调用 EnumDisplayMonitors 并枚举监视器列表。找到上面讨论的
X2,Y2
所在的监视器。在名为rect
的变量中保存 Call QGuiApplication::screens()
并枚举这些对象,找到name()
属性与上一步的MONITORINFOEX.szDevice
匹配的QScreen实例。然后把这个QScreen
的geometry()
方法返回的QRect
保存到一个叫做qRect
的变量中。将 QScreen 保存到一个名为pScreen
的指针变量中
MONITORINFOEX.szDevice
和 MONITORINFOEX.rcMonitor
几何图形
将 X2,Y2
转换为 XFinal,YFinal
的最后一步是此算法:
XFinal = (X2 - rect.left) * qRect.width
------------------------------- + qRect.left
rect.width
YFinal = (Y2 - rect.top) * qRect.height
------------------------------- + qRect.top
rect.height
这只是屏幕坐标映射之间的基本插值。
那么最后的window定位就是在Qt的视图对象上同时设置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)。这对我们不起作用,因为在 150% 的 DPI 设置下 window 的 2 倍缩放看起来太大了。