Qt 6.6颜色拾取器中QGraphicsView实时捕获/绘制时的闪烁及缩放后鼠标精确定位问题
问题描述
问题1:无缩放时画面闪烁/残影
我在做基于QWidget和QGraphicsView的颜色拾取器时,碰到了个头疼的问题:当QGraphicsView没有缩放、移动窗口并绘制捕获的屏幕画面时,画面会出现类似闪烁或者残影的抖动。我已经试了一堆优化手段,但都没解决:
- 把QTimer改成
Qt::PreciseTimer - 定时器间隔设成0
- 用BitBlt代替Qt自带的窗口抓取
- 在DIBSection里缓存QImage的数据(就是
ensureCaptureSurface那部分) - 给QGraphicsView设了
SmartViewportUpdate的视口更新模式 - 关掉了
QPainter::SmoothPixmapTransform的渲染提示
问题2:缩放后鼠标定位不准
当QGraphicsView放大后,像素被拉得很大,鼠标快速移动的时候会直接跳过好几个像素,根本没法精准点到想要的那个像素。我试过用鼠标钩子拦截输入,然后用SendInput改光标位置,但这样又加了新的闪烁;后来用ClipCursor,闪烁少了点,但光标移动起来特别不自然,像卡壳了一样。
解决方案建议
先搞定问题1:画面闪烁/抖动
1. 调整视口更新策略
你现在用的是SmartViewportUpdate,我之前做类似实时渲染工具时,换成FullViewportUpdate反而解决了残影问题——有时候局部更新会因为Qt的智能判断漏掉一些区域,导致画面残留。另外可以手动确认视口的双缓冲开启:
viewport()->setAttribute(Qt::WA_DoubleBuffered, true);
2. 优化捕获与更新逻辑
0间隔定时器强制刷新不仅占CPU,还容易导致过度绘制。不如改成按需刷新:只在鼠标移动、窗口位置变化或屏幕内容更新时,再触发捕获和绘制。
另外,捕获到图像后复用QPixmap对象,不要每次新建:
// 初始化时创建一次 QPixmap m_cachedPixmap; // 捕获到图像后更新 m_cachedPixmap = QPixmap::fromImage(m_captureImage); m_pixmapItem->setPixmap(m_cachedPixmap); // 只刷新图像区域 m_scene->update(m_pixmapItem->boundingRect());
3. 给窗口加属性减少系统干扰
做置顶小工具时,这几个属性能减少系统层面的绘制问题:
setAttribute(Qt::WA_OpaquePaintEvent); // 窗口完全不透明时启用 setAttribute(Qt::WA_NoSystemBackground, false); setWindowFlags(windowFlags() | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); setAttribute(Qt::WA_DisableHighDpiScaling); // 自己控制高DPI适配,避免Qt自动缩放干扰
4. 尝试OpenGL硬件加速
如果机器支持OpenGL,把QGraphicsView的视口换成QOpenGLWidget,硬件渲染能大幅降低闪烁:
m_gridView->setViewport(new QOpenGLWidget()); m_gridView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
再解决问题2:缩放后的鼠标精确定位
1. 用Qt坐标映射替代钩子拦截
别再用钩子改光标位置了,Qt的坐标映射完全能搞定。核心思路是将全局鼠标坐标转成场景坐标,再按缩放比例对齐到原始像素,最后让窗口平滑跟随:
// 拿到全局鼠标位置 QPoint globalMousePos = QCursor::pos(); // 转成视图视口坐标 QPoint viewPos = m_gridView->mapFromGlobal(globalMousePos); // 转成场景坐标 QPointF scenePos = m_gridView->mapToScene(viewPos); // 计算原始像素在场景中的尺寸 qreal pixelSize = 1.0 / m_currentScale; // 对齐到最近的原始像素 qreal alignedX = qRound(scenePos.x() / pixelSize) * pixelSize; qreal alignedY = qRound(scenePos.y() / pixelSize) * pixelSize; QPointF alignedScenePos(alignedX, alignedY); // 转成全局坐标,平滑移动窗口 QPoint targetWindowPos = m_gridView->mapToGlobal(m_gridView->mapFromScene(alignedScenePos)); QPropertyAnimation* anim = new QPropertyAnimation(this, "pos"); anim->setDuration(50); // 调整这个值控制平滑度 anim->setEndValue(targetWindowPos); anim->start(QAbstractAnimation::DeleteWhenStopped);
2. 加平滑插值让移动更自然
如果直接对齐像素太生硬,可以用缓动插值优化:
// 类成员变量 QPointF m_smoothPos; const qreal smoothFactor = 0.3; // 0.2~0.5之间,越小越平滑 // 每次计算完alignedScenePos后 m_smoothPos = m_smoothPos * (1 - smoothFactor) + alignedScenePos * smoothFactor; // 用平滑后的位置更新窗口 QPoint targetPos = m_gridView->mapToGlobal(m_gridView->mapFromScene(m_smoothPos)); move(targetPos);
3. 临时关闭系统鼠标加速
Windows自带的鼠标加速会放大快速移动时的像素跳跃,可以临时关闭,退出时恢复:
// 保存原鼠标速度 int oldMouseSpeed; SystemParametersInfo(SPI_GETMOUSESPEED, 0, &oldMouseSpeed, 0); // 设置为最低速度 SystemParametersInfo(SPI_SETMOUSESPEED, 0, (PVOID)1, SPIF_SENDCHANGE); // 退出时恢复 SystemParametersInfo(SPI_SETMOUSESPEED, 0, (PVOID)oldMouseSpeed, SPIF_SENDCHANGE);
相关代码实现
Main.cpp
#include "ColorPicker.h" static bool createColorPicker(WPARAM wParam, LPARAM lParam) { if (wParam == WM_KEYDOWN && ((KBDLLHOOKSTRUCT*)lParam)->vkCode == VK_F4) new ColorPicker(); return true; } int main(int argc, char *argv[]) { QApplication app(argc, argv); qApp->setQuitOnLastWindowClosed(false); setInputHooks(); setKeyboardHookCallback(new QObject(), createColorPicker); new ColorPicker(); return app.exec(); }
ColorPicker.h
#pragma once #include <QtWidgets> inline HHOOK g_keyboardHook = NULL; inline HHOOK g_mouseHook = NULL; inline QMap<QObject*, std::function<bool(WPARAM, LPARAM)>> g_keyboardHookCallbacks; inline QMap<QObject*, std::function<bool(WPARAM, LPARAM)>> g_mouseHookCallbacks; inline LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode != HC_ACTION) return CallNextHookEx(g_keyboardHook, nCode, wParam, lParam); for (const auto& callback : g_keyboardHookCallbacks.values()) { if (!callback(wParam, lParam)) return 1; // Block the event if any callback returns false } return CallNextHookEx(g_keyboardHook, nCode, wParam, lParam); } inline LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode != HC_ACTION) return CallNextHookEx(g_mouseHook, nCode, wParam, lParam); MSLLHOOKSTRUCT* mouseStruct = (MSLLHOOKSTRUCT*)lParam; for (const auto& callback : g_mouseHookCallbacks.values()) { if (!callback(wParam, lParam)) return 1; // Block the event if any callback returns false } return CallNextHookEx(g_mouseHook, nCode, wParam, lParam); } template<typename T> static void setKeyboardHookCallback(T* obj, bool(T::*callback)(WPARAM, LPARAM)) { QObject::connect(obj, &QObject::destroyed, [obj] { g_keyboardHookCallbacks.remove(obj); }); g_keyboardHookCallbacks[obj] = ([obj, callback](WPARAM wParam, LPARAM lParam) { return (obj->*callback)(wParam, lParam); }); } template<typename T> static void setMouseHookCallback(T* obj, bool(T::*callback)(WPARAM, LPARAM)) { QObject::connect(obj, &QObject::destroyed, [obj] { g_mouseHookCallbacks.remove(obj); }); g_mouseHookCallbacks[obj] = ([obj, callback](WPARAM wParam, LPARAM lParam) { return (obj->*callback)(wParam, lParam); }); } inline void setKeyboardHookCallback(QObject* obj, std::function<bool(WPARAM, LPARAM)> callback) { QObject::connect(obj, &QObject::destroyed, [obj] { g_keyboardHookCallbacks.remove(obj); }); g_keyboardHookCallbacks[obj] = callback; } inline void setInputHooks() { QSettings settings("HKEY_CURRENT_USER\\Control Panel\\Desktop", QSettings::NativeFormat); settings.setValue("LowLevelHooksTimeout", 100); settings.sync(); g_keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, 0); g_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, NULL, 0); if (g_keyboardHook == NULL || g_mouseHook == NULL) MessageBox(NULL, L"Failed to set input hook", L"Error", MB_ICONERROR); } class GridView : public QGraphicsView { public: QGraphicsScene* m_scene = nullptr; QPixmap m_pixmap; // Pixmap set on the pixmapItem QGraphicsPixmapItem* m_pixmapItem; QGraphicsItemGroup* m_gridGroup; bool m_enabled = false; bool m_gridEnabled = false; QRect m_sceneRect; QPoint m_lastMousePos; qreal m_baseScale = 1.0; qreal m_currentScale = 1.0; GridView(QWidget* p = nullptr); void setPixmap(const QPixmap& pixmap); void setGraphicsViewEnabled(); void setGridEnabled(); void resetView(); void centerScene(); void updateGrid(); void wheelEvent(QWheelEvent* event) override; }; class ColorPicker : public QWidget { Q_OBJECT public: const int GRID_WIDTH = 256; const int GRID_HEIGHT = 256; QTimer* m_captureScreenTimer = nullptr; GridView* m_gridView = nullptr; QPointF m_cursorAccum; QPoint m_lastCursorPos; QSize m_captureSize; qreal m_captureDpr = 0.0; HDC m_captureDC = nullptr; HBITMAP m_captureBitmap = nullptr; HGDIOBJ m_captureOldBitmap = nullptr; QImage m_captureImage; ColorPicker(); ~ColorPicker(); void destroyCaptureSurface(); bool ensureCaptureSurface(); void lockCursor(); void unlockCursor(); void updateOverlayPosition(const QPoint& pt); void captureScreenTimerTimeout(); bool keyboardProcHookCallback(WPARAM wParam, LPARAM lParam); bool mouseProcHookCallback(WPARAM wParam, LPARAM lParam); };
ColorPicker.cpp
#include "ColorPicker.h" #include <fstream> #include <limits> #include <sstream> static std::ofstream& getLog() { static std::ofstream log("D:\\colorpicker_debug.log", std::ios::trunc); return log; } void logCaptureState(const QPoint& cursorPos, const QPoint& windowPos, const QSize& captureSize, qreal dpr) { static QPoint lastCursorPos(std::numeric_limits<int>::min(), std::numeric_limits<int>::min()); static QPoint lastWindowPos(std::numeric_limits<int>::min(), std::numeric_limits<int>::min()); static QSize lastCaptureSize; static qreal lastDpr = 0.0; if (cursorPos == lastCursorPos && windowPos == lastWindowPos && captureSize == lastCaptureSize && dpr == lastDpr) return; lastCursorPos = cursorPos; lastWindowPos = windowPos; lastCaptureSize = captureSize; lastDpr = dpr; auto& log = getLog(); log << "CAPTURE cursorPos=(" << cursorPos.x() << "," << cursorPos.y() << ")" << " winPos=(" << windowPos.x() << "," << windowPos.y() << ")" << " captureSize=(" << captureSize.width() << "x" << captureSize.height() << ")" << " dpr=" << dpr << '\n'; } GridView::GridView(QWidget* p) : QGraphicsView(p) { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setRenderHint(QPainter::SmoothPixmapTransform, false); setViewportUpdateMode(QGraphicsView::SmartViewportUpdate); setFrameShape(QFrame::NoFrame); setTransformationAnchor(QGraphicsView::NoAnchor); setResizeAnchor(QGraphicsView::NoAnchor); setBackgroundBrush(Qt::NoBrush); setAlignment(Qt::AlignLeft | Qt::AlignTop); setOptimizationFlag(QGraphicsView::DontAdjustForAntialiasing); setOptimizationFlag(QGraphicsView::DontSavePainterState); setContentsMargins(0, 0, 0, 0); viewport()->setContentsMargins(0, 0, 0, 0); setMouseTracking(true); m_scene = new QGraphicsScene(this); m_scene->installEventFilter(this); m_pixmapItem = new QGraphicsPixmapItem(); m_scene->addItem(m_pixmapItem); m_gridGroup = new QGraphicsItemGroup(); m_scene->addItem(m_gridGroup); m_gridGroup->setZValue(1); // Ensure grid is above image // 原代码此处有截断,保留现有内容 }
备注:内容来源于stack exchange,提问作者Niele

