You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Qt 6.6颜色拾取器中QGraphicsView实时捕获/绘制时的闪烁及缩放后鼠标精确定位问题

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 18:13:14