Qt QGraphicsView 实时截图和缩放时如何减少闪烁?
结论
这段代码可能有两处抖动来源,而且它们可能同时发生。首先,间隔为 0 的定时器会持续截图并刷新 QGraphicsView,导致截图、窗口移动和界面绘制都频繁执行。其次,代码拦截真实鼠标移动后又通过 SendInput 注入新位置。注入事件会再次进入 hook,并与 ClipCursor 一起影响光标的运动过程。仅凭描述和代码无法确定 GIF 中每一帧闪烁的具体原因,建议先分别排查这两条路径。
先降低截图与重绘频率
Qt::PreciseTimer 只影响定时器的触发时机,不代表屏幕截图和绘制能以更高、更稳定的帧率完成。间隔设为 0 时,定时器会在事件循环空闲时尽快触发。每次触发都会调用 BitBlt,随后执行 QGraphicsView::setPixmap()。即使鼠标没有移动,代码也会重复捕获和刷新。
可以先把更新频率限制在一个明确的范围,例如每秒 60 次:
m_captureScreenTimer->setInterval(16); // 约 60 次/秒
然后记录上次捕获的位置或区域,只在位置变化时调用 BitBlt 和 setPixmap()。先暂停移动取色窗口,观察截图更新时是否仍然闪烁;再暂停截图,只移动窗口。通过这两组测试,可以判断抖动来自截图刷新还是窗口移动。
如果降低频率后仍有局部闪烁,可以临时将 QGraphicsView 的 viewportUpdateMode 改为 QGraphicsView::FullViewportUpdate 做对照。这个设置可能增加绘制开销,适合用于排查,未必是最终的最佳设置。
核对截图坐标与 DPI
代码通过 devicePixelRatioF() 将 256 乘以 DPR,得到截图位图尺寸。不过,GetCursorPos() 返回的屏幕坐标、BitBlt 的源坐标、窗口坐标和 Qt 坐标是否处于同一坐标空间,需要进一步确认。高 DPI 或多显示器环境中,逻辑像素和物理像素可能不一致,导致截图区域偏移或抖动。
建议记录并对照目标显示器、DPR、GetCursorPos() 坐标、m_captureSize、BitBlt 的源坐标,以及取色窗口的屏幕位置。同时确认进程的 DPI awareness,以及 Qt 与 WinAPI 屏幕坐标之间的转换方式。多显示器环境还要检查屏幕坐标原点和负坐标,不能只根据主屏幕尺寸推算位置。
SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE) 的效果取决于 Windows 版本和使用的捕获路径。在目标系统上应单独验证截图是否包含取色器窗口,不要预设 BitBlt 一定会排除它。
避免在鼠标 hook 中模拟慢速光标
当前缩放逻辑会拦截真实的 WM_MOUSEMOVE,按 m_currentScale 缩小位移,再通过 SendInput 注入新位置。注入事件会再次触发低级鼠标 hook。代码虽然通过 LLMHF_INJECTED 放行,但真实移动和注入移动仍会交织在同一个全局输入流程中。每次真实移动还会调用 lockCursor(),并安排一秒后解锁,这也可能导致光标受限、回弹或移动不自然。
取色精度通常不需要依赖改变系统光标的实际速度。可以保留真实光标位置,用它计算取色器中的采样位置。例如,累计真实位移,再按缩放比例换算为图像像素:
m_cursorAccum += QPointF(rawDelta) / scale;
int dx = static_cast<int>(m_cursorAccum.x());
int dy = static_cast<int>(m_cursorAccum.y());
m_cursorAccum -= QPointF(dx, dy);
将算出的图像坐标用于选择像素或移动取景区域。这样可以分别处理鼠标实际移动和放大图像上的精细取样,无需再通过 SendInput 让系统光标追赶模拟位置。
把界面操作放回 Qt 事件循环
鼠标 hook 回调中直接调用 updateOverlayPosition()、lockCursor(),以及在键盘 hook 中创建或关闭窗口,会让 WinAPI 输入回调与 Qt 界面操作耦合。hook 回调应尽快返回。需要更新 QWidget 时,先将坐标复制到局部变量,再通过队列调用把操作交给 Qt 线程。避免在 hook 中等待绘制、执行耗时工作或持续注入输入。
如果仍需拦截输入并注入事件,应单独实现并验证注入事件过滤、递归处理和边界坐标换算。当前的绝对坐标换算按主屏幕尺寸计算,多显示器桌面还需额外处理。ClipCursor 也应在明确的生命周期内配对释放,避免多个延迟解锁任务互相干扰。
建议的排查顺序
- 暂时关闭鼠标拦截、
SendInput和ClipCursor,只保留截图与图像显示。 - 将定时器间隔改为约 16 毫秒;截图区域未变化时,跳过捕获和刷新。
- 分别测试窗口静止时的截图更新,以及暂停截图时的窗口移动。
- 核对 DPR、屏幕坐标和
BitBlt源区域,重点检查高 DPI 与多显示器环境。 - 恢复缩放取样时,只将真实鼠标位移映射到图像像素,先不要改变系统光标运动。
备注:内容仅供参考。
