在QGraphicsView上实时捕获和绘制时出现闪烁
我在使用Qt 6.6开发一个颜色选择器小部件,QWidget 和 QGraphicsView,遇到了两个问题:
问题1)
当 QGraphicsView 没有缩放、窗口被移动且图像被绘制时,它会出现闪烁。我不确定这是否可以称为闪烁、鬼影,还是其他什么,我找不到导致图像像这样抖动的原因:
https://i.imgur.com/NCCFeiq.gif
为尽量提高捕获速度,我尝试了以下几种做法:
- 将
QTimerTimerType设置为Qt::PreciseTimer - 将定时器间隔设置为0
- 在Qt的窗口抓取上使用
BitBlt - 将
QImage位缓存到DIBSection(ensureCaptureSurface) - 将
QGraphicsViewviewportUpdateMode设置为SmartViewportUpdate - 将
QGraphicsView设置为RenderHint(QPainter::SmoothPixmapTransform, false);
问题2)
当 GrapchisView 放大时,我想让鼠标或窗口移动得慢一些,因为像素变大,鼠标快速移动时会跳跃像素,无法精确地移动到某个具体像素上:
https://i.imgur.com/m4mjNx9.gif
为实现这一点,我在 mouseProcHookCallback 函数中返回false,使位于 CALLBACK MouseProc 的鼠标钩子返回1;这阻止了输入消息继续传递。随后我尝试使用 SendInput 来操作光标位置,如上面的GIF所示,这也会导致闪烁,可能是问题1 的闪烁再加上我修改光标位置的尝试所致。
我还尝试使用Windows API的 ClipCursor,它降低了闪烁,但光标移动仍然看起来很怪,显得不自然。
即使在发布模式下测试且不做任何日志记录,两个问题依然存在。
我该如何停止这些闪烁问题?
(注:我尝试在这里添加这两个GIF,但失败了,请管理员帮忙修复吗?)
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
setScene(m_scene);
};
void GridView::setPixmap(const QPixmap& pixmap)
{
QSize oldSize = m_pixmap.size();
m_pixmap = pixmap;
m_pixmapItem->setPixmap(pixmap);
if (oldSize != pixmap.size())
{
QRectF bounds = m_pixmapItem->boundingRect();
m_scene->setSceneRect(bounds);
centerOn(bounds.center());
}
}
void GridView::setGraphicsViewEnabled()
{
if (m_enabled)
resetView();
else if (m_gridEnabled)
updateGrid();
m_enabled = !m_enabled;
}
void GridView::setGridEnabled()
{
m_gridEnabled = !m_gridEnabled;
if (m_gridEnabled)
{
if (m_currentScale > 3.0)
{
m_gridGroup->setVisible(true);
updateGrid();
}
}
else
{
m_gridGroup->setVisible(false);
delete m_gridGroup->childItems().first();
}
}
void GridView::resetView()
{
// Reset transform
setTransform(QTransform());
// Calculate scale to fit view
QRectF viewRect = viewport()->rect();
QRectF imageRect = m_pixmapItem->boundingRect();
qreal scaleX = viewRect.width() / imageRect.width();
qreal scaleY = viewRect.height() / imageRect.height();
m_baseScale = qMin(scaleX, scaleY);
m_currentScale = m_baseScale;
// Center the pixmap in the view
m_pixmapItem->setPos((viewRect.width() - imageRect.width() * m_baseScale) / 2.0,
(viewRect.height() - imageRect.height() * m_baseScale) / 2.0);
// Apply base scale
QTransform transform;
transform.scale(m_baseScale, m_baseScale);
setTransform(transform);
// Reset grid visibility
m_gridGroup->setVisible(m_currentScale > 3.0);
}
void GridView::centerScene()
{
// First check if the top left corner is in the middle of the view.
QPointF topLeft = mapFromScene(0, 0);
if (topLeft.x() > 0 || topLeft.y() > 0)
{
QTransform t;
t.translate(
topLeft.x() > 0 ? -topLeft.x() / m_currentScale : 0,
topLeft.y() > 0 ? -topLeft.y() / m_currentScale : 0
);
setTransform(t, true);
}
else
{
// If not, then check if perhaps the bottom right corner is.
QPointF bottomRight = mapFromScene(width(), height());
if (bottomRight.x() < width() || bottomRight.y() < height())
{
QTransform t;
t.translate(
bottomRight.x() < width() ? - (bottomRight.x() - width()) / m_currentScale : 0,
bottomRight.y() < height() ? -(bottomRight.y() - height()) / m_currentScale : 0
);
setTransform(t, true);
}
}
QPolygonF point = mapToScene(viewport()->rect());
if (!point.isEmpty())
m_sceneRect = QRect(point[0].toPoint(), point[2].toPoint());
}
void GridView::updateGrid()
{
if (!m_gridEnabled)
return;
// Clear existing grid
while (!m_gridGroup->childItems().isEmpty()) {
delete m_gridGroup->childItems().first();
}
// Get visible area in scene coordinates
QRectF visibleRect = mapToScene(viewport()->rect()).boundingRect();
QRectF imageRect = m_pixmapItem->boundingRect();
QRectF gridRect = visibleRect.intersected(imageRect);
// Calculate grid spacing in scene coordinates
qreal gridSpacing = 1.0; // One pixel in image coordinates
QPen gridPen(QColor(200, 200, 200, 255), 0); // Width 0 for single pixel line
// Calculate grid line positions
qreal startX = std::ceil(gridRect.left() / gridSpacing) * gridSpacing;
qreal startY = std::ceil(gridRect.top() / gridSpacing) * gridSpacing;
// Draw vertical lines
for (qreal x = startX; x <= gridRect.right(); x += gridSpacing)
{
QGraphicsLineItem* line = new QGraphicsLineItem(x, gridRect.top(), x, gridRect.bottom());
line->setPen(gridPen);
m_gridGroup->addToGroup(line);
}
// Draw horizontal lines
for (qreal y = startY; y <= gridRect.bottom(); y += gridSpacing)
{
QGraphicsLineItem* line = new QGraphicsLineItem(gridRect.left(), y, gridRect.right(), y);
line->setPen(gridPen);
m_gridGroup->addToGroup(line);
}
}
void GridView::wheelEvent(QWheelEvent* event)
{
if (m_pixmapItem->pixmap().isNull() || !m_enabled)
return QGraphicsView::wheelEvent(event);
// Store cursor position relative to scene
QPointF mousePos = event->position(),
mousePosCurrent = mapToScene(mousePos.x(), mousePos.y());
// Calculate zoom factor
double factor = pow(1.5, event->angleDelta().y() / 240.0);
double newScale = m_currentScale * factor;
if (newScale < 1.) { // Do not allow zooming out (in absolute)
factor /= newScale;
newScale = 1.;
}
else if (newScale > 50.) { // Do not allow zoom > 50x
factor = factor * 50. / newScale;
newScale = 50.;
}
// Zoom centered on the mouse cursor
QTransform t;
t.translate(mousePosCurrent.x(), mousePosCurrent.y());
t.scale(factor, factor);
t.translate(-mousePosCurrent.x(), -mousePosCurrent.y());
setTransform(t, true);
m_currentScale = newScale;
centerScene();
// Update grid based on zoom level
if (m_gridEnabled)
{
m_gridGroup->setVisible(m_currentScale > 3.0);
if (m_currentScale > 3.0)
updateGrid();
}
event->accept();
}
ColorPicker::ColorPicker() : QWidget()
{
setWindowFlags(Qt::FramelessWindowHint|Qt::WindowStaysOnTopHint);
setKeyboardHookCallback(this, &ColorPicker::keyboardProcHookCallback);
setMouseHookCallback(this, &ColorPicker::mouseProcHookCallback);
QWidget* widget = new QWidget(this);
widget->setFixedSize(GRID_WIDTH + 4, GRID_HEIGHT + 5);
widget->setStyleSheet("background-color: red;");
widget->move(0, 0);
m_gridView = new GridView(this);
m_gridView->setGraphicsViewEnabled();
m_gridView->setGridEnabled();
m_gridView->setFixedSize(GRID_WIDTH, GRID_HEIGHT);
m_gridView->move(2, 3);
m_captureScreenTimer = new QTimer(this);
m_captureScreenTimer->setTimerType(Qt::PreciseTimer);
m_captureScreenTimer->setInterval(0);
m_captureScreenTimer->start();
connect(m_captureScreenTimer, &QTimer::timeout, this, &ColorPicker::captureScreenTimerTimeout);
m_lastCursorPos = QCursor::pos();
HWND hwnd = (HWND)winId();
LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_LAYERED | WS_EX_TRANSPARENT);
SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA);
SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);
ensureCaptureSurface();
show();
}
ColorPicker::~ColorPicker()
{
if (m_captureScreenTimer != nullptr)
m_captureScreenTimer->stop();
destroyCaptureSurface();
unlockCursor();
}
void ColorPicker::destroyCaptureSurface()
{
m_captureImage = QImage();
if (m_captureDC != nullptr && m_captureOldBitmap != nullptr)
{
SelectObject(m_captureDC, m_captureOldBitmap);
m_captureOldBitmap = nullptr;
}
if (m_captureBitmap != nullptr)
{
DeleteObject(m_captureBitmap);
m_captureBitmap = nullptr;
}
if (m_captureDC != nullptr)
{
DeleteDC(m_captureDC);
m_captureDC = nullptr;
}
m_captureSize = QSize();
m_captureDpr = 0.0;
}
bool ColorPicker::ensureCaptureSurface()
{
qreal dpr = m_gridView->devicePixelRatioF();
QSize captureSize(qRound(256 * dpr), qRound(256 * dpr));
if (captureSize.isEmpty())
return false;
if (m_captureBitmap != nullptr && captureSize == m_captureSize && dpr == m_captureDpr)
return true;
destroyCaptureSurface();
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = captureSize.width();
bmi.bmiHeader.biHeight = -captureSize.height();
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* dibBits = nullptr;
m_captureBitmap = CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, &dibBits, nullptr, 0);
if (m_captureBitmap == nullptr || dibBits == nullptr)
{
destroyCaptureSurface();
return false;
}
m_captureDC = CreateCompatibleDC(nullptr);
if (m_captureDC == nullptr)
{
destroyCaptureSurface();
return false;
}
m_captureOldBitmap = SelectObject(m_captureDC, m_captureBitmap);
m_captureImage = QImage(static_cast<uchar*>(dibBits), captureSize.width(), captureSize.height(), captureSize.width() * 4, QImage::Format_ARGB32);
m_captureImage.setDevicePixelRatio(dpr);
m_captureSize = captureSize;
m_captureDpr = dpr;
return !m_captureImage.isNull();
}
void ColorPicker::lockCursor()
{
POINT pt;
GetCursorPos(&pt);
RECT rc = { pt.x, pt.y, pt.x + 1, pt.y + 1 };
ClipCursor(&rc);
}
void ColorPicker::unlockCursor()
{
ClipCursor(NULL);
}
void ColorPicker::updateOverlayPosition(const QPoint& cursorPos)
{
QPoint newPos = QPoint(cursorPos.x() - (size().width() / 2) + 9,
cursorPos.y() - (size().height() / 2));
if (pos() != newPos)
move(newPos);
}
bool ColorPicker::keyboardProcHookCallback(WPARAM wParam, LPARAM lParam)
{
KBDLLHOOKSTRUCT* kbStruct = (KBDLLHOOKSTRUCT*)lParam;
if (wParam == WM_KEYDOWN && kbStruct->vkCode == VK_ESCAPE)
{
close();
deleteLater();
return false;
}
return true;
}
bool ColorPicker::mouseProcHookCallback(WPARAM wParam, LPARAM lParam)
{
MSLLHOOKSTRUCT* mouseStruct = (MSLLHOOKSTRUCT*)lParam;
short delta = HIWORD(mouseStruct->mouseData);
QPoint pt(mouseStruct->pt.x, mouseStruct->pt.y);
if (wParam == WM_MOUSEMOVE)
{
auto& log = getLog();
qreal scale = m_gridView->m_currentScale;
if (scale > 1.0)
{
// Accept our own injected moves
if (mouseStruct->flags & LLMHF_INJECTED)
{
log << "INJECTED pt=(" << pt.x() << "," << pt.y() << ")"
<< " flags=0x" << std::hex << mouseStruct->flags << std::dec
<< " lastPos=(" << m_lastCursorPos.x() << "," << m_lastCursorPos.y() << ")"
<< " -> accepted\n";
updateOverlayPosition(pt);
m_lastCursorPos = pt;
return true;
}
// Compute delta from where we last placed the cursor
QPoint rawDelta = pt - m_lastCursorPos;
m_cursorAccum += QPointF(rawDelta) / scale;
int dx = (int)m_cursorAccum.x();
int dy = (int)m_cursorAccum.y();
m_cursorAccum -= QPointF(dx, dy);
QPoint newPos = m_lastCursorPos + QPoint(dx, dy);
std::ostringstream line;
line << "REAL pt=(" << pt.x() << "," << pt.y() << ")"
<< " flags=0x" << std::hex << mouseStruct->flags << std::dec
<< " lastPos=(" << m_lastCursorPos.x() << "," << m_lastCursorPos.y() << ")"
<< " rawDelta=(" << rawDelta.x() << "," << rawDelta.y() << ")"
<< " scale=" << scale
<< " dx=" << dx << " dy=" << dy
<< " accum=(" << m_cursorAccum.x() << "," << m_cursorAccum.y() << ")";
if (dx != 0 || dy != 0)
{
line << " -> SendInput to (" << newPos.x() << "," << newPos.y() << ")";
}
line << " -> BLOCKED\n";
log << line.str();
if (dx != 0 || dy != 0)
{
INPUT input = {};
input.type = INPUT_MOUSE;
input.mi.dx = (LONG)(newPos.x() * 65535.0 / (GetSystemMetrics(SM_CXSCREEN) - 1));
input.mi.dy = (LONG)(newPos.y() * 65535.0 / (GetSystemMetrics(SM_CYSCREEN) - 1));
input.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE;
SendInput(1, &input, sizeof(INPUT));
}
lockCursor();
QTimer::singleShot(1000, this, &ColorPicker::unlockCursor);
return false;
}
else
{
m_lastCursorPos = pt;
m_cursorAccum = QPointF(0, 0);
updateOverlayPosition(pt);
}
}
else if (wParam == WM_MOUSEWHEEL && (GetKeyState(VK_CONTROL) & 0x8000))
{
QMetaObject::invokeMethod(this, [this, delta, pt]()
{
QPoint angleDelta(0, delta);
QPointF pos = m_gridView->mapFromGlobal(pt);
QWheelEvent* wheelEvent = new QWheelEvent(pos, m_gridView->mapToGlobal(pos), QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false);
QCoreApplication::postEvent(m_gridView->viewport(), wheelEvent);
}, Qt::QueuedConnection);
return false;
}
return true;
}
void ColorPicker::captureScreenTimerTimeout()
{
if (!ensureCaptureSurface())
return;
QPoint cursorPos = m_lastCursorPos;
logCaptureState(cursorPos, pos(), m_captureSize, m_captureDpr);
HDC hdcScreen = GetDC(nullptr);
if (hdcScreen == nullptr)
return;
BitBlt(m_captureDC, 0, 0, m_captureSize.width(), m_captureSize.height(), hdcScreen,
cursorPos.x() - m_captureSize.width() / 2,
cursorPos.y() - m_captureSize.height() / 2,
SRCCOPY);
ReleaseDC(nullptr, hdcScreen);
m_gridView->setPixmap(QPixmap::fromImage(m_captureImage));
}
解决方案
禁用Windows窗口边框和标题栏时可能会出现闪烁问题。这种方法对我有帮助:
void mouseMoveEvent(QMouseEvent *event)override{
BaseClass::mouseMoveEvent(event);
setUpdatesEnabled(false);
move(...);
setUpdatesEnabled(true);
}
};
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。