@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()));
上面的代码采用主监视器“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 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 初始化之前在应用程序启动SetProcessDpiAwareness
时使用值非常早的调用。PROCESS_SYSTEM_DPI_AWARE
之后 Qt 将无法更改它。
只需切换到系统感知 dpi 即可解决许多其他问题。
最终错误修复:
因为我们将窗口定位在一个绝对位置(任务托盘中系统托盘图标的正上方),所以我们依靠 Windows API Shell_NotifyIconGetRect来为我们提供系统托盘的坐标。一旦我们知道系统托盘的偏移量,我们就会计算一个顶部/左侧位置,以便我们的窗口位于屏幕上。让我们称这个位置X1,Y1
但是,Shell_NotifyIconGetRect
在 Windows 10 上返回的坐标将始终是“每个显示器感知”的本机坐标,而不是缩放到系统 DPI。使用PhysicalToLogicalPointForPerMonitorDPI进行转换。此 API 在 Windows 7 上不存在,但不是必需的。如果您支持 Windows 7,请将此 API使用LoadLibrary
和GetProcAddress
。如果该 API 不存在,请跳过此步骤。用于PhysicalToLogicalPointForPerMonitorDPI
转换X1,Y1
为系统感知的 DPI 坐标,我们将调用X2,Y2
.
理想情况下,X2,Y2 被传递给 Qt 方法,如QQuickView::setPosition
But....
因为我们使用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()
并枚举这些对象以查找其name()
属性与MONITORINFOEX.szDevice
上一步中的 相匹配的 QScreen 实例。然后将this 方法QRect
返回的值保存到一个名为. 将 QScreen 保存到一个名为的指针变量中geometry()
QScreen
qRect
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);
考虑的其他解决方案:
可以在 QGuiApplication 对象上设置称为Qt::AA_EnableHighDpiScaling的 Qt 模式。它为您完成了上述大部分工作,除了它强制所有缩放为一个积分比例因子(1x、2x、3x 等......从不 1.5 或 1.75)。这对我们不起作用,因为在 DPI 设置为 150% 时将窗口缩放 2 倍看起来太大了。