使用鼠标滚轮以鼠标位置为中心进行QGraphicsView缩放

32

我有一个应用程序,在屏幕中央有一个QGraphicsView窗口。我想通过鼠标滚轮缩放视图。

目前,我已经重新实现了QGraphicsView并重写了鼠标滚动函数,以便它不会像默认情况下一样滚动图片。

void MyQGraphicsView::wheelEvent(QWheelEvent *event)
{
    if(event->delta() > 0)
    {
        emit mouseWheelZoom(true);
    }
    else
    {
        emit mouseWheelZoom(false);
    }
}

当我滚动时,通过一个信号来发出真假值,如果鼠标滚轮向前则为 true ,向后则为 false 。

然后,我将这个信号连接到处理 GUI 的类中的一个槽(缩放函数 见下文)中。现在,基本上我认为我的缩放函数根本不是最好的方法,我看到有些人使用重写的 wheelevent 函数来设置比例尺,但我并没有找到完整的答案。

因此,我没有采用那种方法,而是做了另外一种方法,但它并不完美,所以我正在寻求对其进行微调或者使用 wheel event 函数的比例尺工作示例。

我在构造函数中将 m_zoom_level 初始化为 0

void Display::zoomfunction(bool zoom)
{
    QMatrix matrix;

    if(zoom && m_zoom_level < 500)
    {
        m_zoom_level = m_zoom_level + 10;
        ui->graphicsView->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
        matrix.scale(m_zoom_level, m_zoom_level);

        ui->graphicsView->setMatrix(matrix);
        ui->graphicsView->scale(1,-1);
    }
    else if(!zoom)
    {
        m_zoom_level = m_zoom_level - 10;
        ui->graphicsView->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
        matrix.scale(m_zoom_level, m_zoom_level);

        ui->graphicsView->setMatrix(matrix);
        ui->graphicsView->scale(1,-1);
    }
}

正如您在上面看到的,我正在使用QMatrix进行缩放,并将其设置为Graphicsview并将转换锚点设置为鼠标下方,但有时它并不完美地工作,如果我滚动很多次,它就会开始仅缩小(我认为这与int循环或其他事情有关)。

正如我所说,对此的帮助或一个好的缩放鼠标下的示例将是很棒的。


3
能够得到一个明确的答案会很好,因为有太多类似这样的问题,每个问题都有不同的答案,而且大多数看起来只是部分解决了,然后就被搁置了...这肯定不是太复杂了,更多的只是正确缩放和如何应用它的问题。 - AngryDuck
13个回答

59
这种缩放有点棘手。让我分享一下我自己用来做这件事的类。
标题:
#include <QObject>
#include <QGraphicsView>

/*!
 * This class adds ability to zoom QGraphicsView using mouse wheel. The point under cursor
 * remains motionless while it's possible.
 *
 * Note that it becomes not possible when the scene's
 * size is not large enough comparing to the viewport size. QGraphicsView centers the picture
 * when it's smaller than the view. And QGraphicsView's scrolls boundaries don't allow to
 * put any picture point at any viewport position.
 *
 * When the user starts scrolling, this class remembers original scene position and
 * keeps it until scrolling is completed. It's better than getting original scene position at
 * each scrolling step because that approach leads to position errors due to before-mentioned
 * positioning restrictions.
 *
 * When zommed using scroll, this class emits zoomed() signal.
 *
 * Usage:
 *
 *   new Graphics_view_zoom(view);
 *
 * The object will be deleted automatically when the view is deleted.
 *
 * You can set keyboard modifiers used for zooming using set_modified(). Zooming will be
 * performed only on exact match of modifiers combination. The default modifier is Ctrl.
 *
 * You can change zoom velocity by calling set_zoom_factor_base().
 * Zoom coefficient is calculated as zoom_factor_base^angle_delta
 * (see QWheelEvent::angleDelta).
 * The default zoom factor base is 1.0015.
 */
class Graphics_view_zoom : public QObject {
  Q_OBJECT
public:
  Graphics_view_zoom(QGraphicsView* view);
  void gentle_zoom(double factor);
  void set_modifiers(Qt::KeyboardModifiers modifiers);
  void set_zoom_factor_base(double value);

private:
  QGraphicsView* _view;
  Qt::KeyboardModifiers _modifiers;
  double _zoom_factor_base;
  QPointF target_scene_pos, target_viewport_pos;
  bool eventFilter(QObject* object, QEvent* event);

signals:
  void zoomed();
};

来源:

#include "Graphics_view_zoom.h"
#include <QMouseEvent>
#include <QApplication>
#include <QScrollBar>
#include <qmath.h>

Graphics_view_zoom::Graphics_view_zoom(QGraphicsView* view)
  : QObject(view), _view(view)
{
  _view->viewport()->installEventFilter(this);
  _view->setMouseTracking(true);
  _modifiers = Qt::ControlModifier;
  _zoom_factor_base = 1.0015;
}

void Graphics_view_zoom::gentle_zoom(double factor) {
  _view->scale(factor, factor);
  _view->centerOn(target_scene_pos);
  QPointF delta_viewport_pos = target_viewport_pos - QPointF(_view->viewport()->width() / 2.0,
                                                             _view->viewport()->height() / 2.0);
  QPointF viewport_center = _view->mapFromScene(target_scene_pos) - delta_viewport_pos;
  _view->centerOn(_view->mapToScene(viewport_center.toPoint()));
  emit zoomed();
}

void Graphics_view_zoom::set_modifiers(Qt::KeyboardModifiers modifiers) {
  _modifiers = modifiers;

}

void Graphics_view_zoom::set_zoom_factor_base(double value) {
  _zoom_factor_base = value;
}

bool Graphics_view_zoom::eventFilter(QObject *object, QEvent *event) {
  if (event->type() == QEvent::MouseMove) {
    QMouseEvent* mouse_event = static_cast<QMouseEvent*>(event);
    QPointF delta = target_viewport_pos - mouse_event->pos();
    if (qAbs(delta.x()) > 5 || qAbs(delta.y()) > 5) {
      target_viewport_pos = mouse_event->pos();
      target_scene_pos = _view->mapToScene(mouse_event->pos());
    }
  } else if (event->type() == QEvent::Wheel) {
    QWheelEvent* wheel_event = static_cast<QWheelEvent*>(event);
    if (QApplication::keyboardModifiers() == _modifiers) {
      if (wheel_event->orientation() == Qt::Vertical) {
        double angle = wheel_event->angleDelta().y();
        double factor = qPow(_zoom_factor_base, angle);
        gentle_zoom(factor);
        return true;
      }
    }
  }
  Q_UNUSED(object)
  return false;
}

使用示例:

Graphics_view_zoom* z = new Graphics_view_zoom(ui->graphicsView);
z->set_modifiers(Qt::NoModifier);

谢谢,这个很好用,但是在编译时出现了一些错误:“QMouseEvent* mouse_event = static_cast<QMouseEvent*>(event);”,我不得不改成“QMouseEvent* mouse_event = (QMouseEvent *) event;”。除此之外,一切都很好,谢谢。你介意解释一下一些硬编码的值是干什么的吗?这样我就可以明确它在做什么和为什么了,再次感谢,我会尽快接受的。 - AngryDuck
这不是将视图重新定位到鼠标指向的位置,而是缩放鼠标光标吗? - Ben
提示:使用 item->setTransformationMode(Qt::SmoothTransformation);:对我来说,它修复了在使用拖动模式 view->setDragMode(QGraphicsView::ScrollHandDrag); 时的重绘故障。此外,它将使用 AA,在缩放时有助于消除大像素。 - ForeverLearning
这个解决方案非常棒。有人能帮我解决如何使用_mouseEvent->pos()获取鼠标下的原始图像位置吗? - Anonymous
1
谢谢您,太棒了!距离上次编辑已经6年了,但现在复制粘贴后它可以直接运行。 - michalmonday
显示剩余2条评论

30

以下是使用PyQt的解决方案:

def wheelEvent(self, event):
    """
    Zoom in or out of the view.
    """
    zoomInFactor = 1.25
    zoomOutFactor = 1 / zoomInFactor

    # Save the scene pos
    oldPos = self.mapToScene(event.pos())

    # Zoom
    if event.angleDelta().y() > 0:
        zoomFactor = zoomInFactor
    else:
        zoomFactor = zoomOutFactor
    self.scale(zoomFactor, zoomFactor)

    # Get the new position
    newPos = self.mapToScene(event.pos())

    # Move scene to old position
    delta = newPos - oldPos
    self.translate(delta.x(), delta.y())

25

您可以简单地使用内置功能AnchorUnderMouseAnchorViewCenter来保持焦点在鼠标下或中心位置。

在Qt 5.7中,这对我起作用。

void SceneView::wheelEvent(QWheelEvent *event)
    {
        if (event->modifiers() & Qt::ControlModifier) {
            // zoom
            const ViewportAnchor anchor = transformationAnchor();
            setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
            int angle = event->angleDelta().y();
            qreal factor;
            if (angle > 0) {
                factor = 1.1;
            } else {
                factor = 0.9;
            }
            scale(factor, factor);
            setTransformationAnchor(anchor);
        } else {
            QGraphicsView::wheelEvent(event);
        }
    }

3
这就是真正的解决方案,让Qt来完成工作,而不是重复造轮子。伟大的贡献! - ypnos
3
@ypnos 这里使用了很巧妙的双关语,重新发明车轮 :) - mlvljr
我尝试了很多其他的答案,但我发现这个是正确的,谢谢! - Phymin
这个方法完美地运行,为什么不是最受欢迎的答案呢!或许在构造函数中添加完整的类定义以显示启用鼠标跟踪会更方便。 - Ashley Duncan

15

这是适用于我的Python版本。它来自于@Stefan Reinhardt和@rengel的答案组合。

class MyQGraphicsView(QtGui.QGraphicsView):

def __init__ (self, parent=None):
    super(MyQGraphicsView, self).__init__ (parent)

def wheelEvent(self, event):
    # Zoom Factor
    zoomInFactor = 1.25
    zoomOutFactor = 1 / zoomInFactor

    # Set Anchors
    self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
    self.setResizeAnchor(QtGui.QGraphicsView.NoAnchor)

    # Save the scene pos
    oldPos = self.mapToScene(event.pos())

    # Zoom
    if event.delta() > 0:
        zoomFactor = zoomInFactor
    else:
        zoomFactor = zoomOutFactor
    self.scale(zoomFactor, zoomFactor)

    # Get the new position
    newPos = self.mapToScene(event.pos())

    # Move scene to old position
    delta = newPos - oldPos
    self.translate(delta.x(), delta.y())

是的,这些设置锚点解决了问题。 - Melroy van den Berg
3
Qt5:event.delta() -> event.angleDelta().y() - user136036
PySide6: @user136036 需要将 change + QPointF 强制转换为 QPoint,以便调用 self.mapToScene。 - John

6
以下是上述解决方案的精简版本;仅包括您需要放入滚动事件中的代码。 在我的测试中,这可以与或不带滚动条完美运行 ;)
void MyGraphicsView::wheelEvent(QWheelEvent* pWheelEvent)
{
    if (pWheelEvent->modifiers() & Qt::ControlModifier)
    {
        // Do a wheel-based zoom about the cursor position
        double angle = pWheelEvent->angleDelta().y();
        double factor = qPow(1.0015, angle);

        auto targetViewportPos = pWheelEvent->pos();
        auto targetScenePos = mapToScene(pWheelEvent->pos());

        scale(factor, factor);
        centerOn(targetScenePos);
        QPointF deltaViewportPos = targetViewportPos - QPointF(viewport()->width() / 2.0, viewport()->height() / 2.0);
        QPointF viewportCenter = mapFromScene(targetScenePos) - deltaViewportPos;
        centerOn(mapToScene(viewportCenter.toPoint()));

        return;
    }

6

有点晚了,但是我今天用Pyside走了同样的路线,应该是一样的...

这种方法“非常简单”,尽管花费了我一些时间... 首先将所有锚点设置为NoAnchor,然后获取鼠标滚轮事件的位置,在场景中进行映射, 通过这个值对场景进行平移、缩放,最后再将其平移回来:

def wheelEvent(self, evt):
    #Remove possible Anchors
    self.widget.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
    self.widget.setResizeAnchor(QtGui.QGraphicsView.NoAnchor)
    #Get Scene Pos
    target_viewport_pos = self.widget.mapToScene(evt.pos())
    #Translate Scene
    self.widget.translate(target_viewport_pos.x(),target_viewport_pos.y())
    # ZOOM
    if evt.delta() > 0:
        self._eventHandler.zoom_ctrl(1.2)
    else:
        self._eventHandler.zoom_ctrl(0.83333)
    # Translate back
    self.widget.translate(-target_viewport_pos.x(),-target_viewport_pos.y())

这是为我的目的工作的唯一解决方案。 我认为这也是最合乎逻辑的解决方案...


5
经过多次尝试,似乎这个方法可以。问题在于 QGraphicsViewtransform 与其滚动位置无关,因此 QGraphicsView::mapToScene(const QPoint&) const 的行为取决于滚动位置和变换。我必须查看 mapToScene 的源代码才能理解这一点。
有了这个想法后,以下是可行的方法:记住鼠标指向的场景点,进行缩放,将该场景点映射到鼠标坐标,然后调整滚动条以使该点位于鼠标下方:
void ZoomGraphicsView::wheelEvent(QWheelEvent* event)
{
   const QPointF p0scene = mapToScene(event->pos());

   qreal factor = std::pow(1.01, event->delta());
   scale(factor, factor);

   const QPointF p1mouse = mapFromScene(p0scene);
   const QPointF move = p1mouse - event->pos(); // The move
   horizontalScrollBar()->setValue(move.x() + horizontalScrollBar()->value());
   verticalScrollBar()->setValue(move.y() + verticalScrollBar()->value());
}

我实际上根本没有使用scrollbr,所以不幸的是,最后一个重置位置的部分不起作用。我不喜欢重新发布,但我已经实现了一些东西,起初我认为它有效,但实际上并不是,我觉得这值得一个新问题,不知道你是否有任何想法?http://stackoverflow.com/questions/21134446/zooming-a-qgraphicsview-under-the-mouse-looking-for-a-definitive-answer-to-this - AngryDuck
这对我来说没起作用。不过,Veslem的答案确实有效。 - Melroy van den Berg

4
更平滑的缩放
void StatusView::wheelEvent(QWheelEvent * event)
{
    const QPointF p0scene = mapToScene(event->pos());

    qreal factor = qPow(1.2, event->delta() / 240.0);
    scale(factor, factor);

    const QPointF p1mouse = mapFromScene(p0scene);
    const QPointF move = p1mouse - event->pos(); // The move
    horizontalScrollBar()->setValue(move.x() + horizontalScrollBar()->value());
    verticalScrollBar()->setValue(move.y() + verticalScrollBar()->value());

}

2

PyQt的回答很好,这里提供一个C++函数,以防将来有人需要。

原始答案:Original Answer

void CanvasView::zoomAt(const QPoint &centerPos, double factor)
{
    //QGraphicsView::AnchorUnderMouse uses ::centerOn() in it's implement, which must need scroll.
    //transformationAnchor() default is AnchorViewCenter, you need set NoAnchor while change transform, 
    //and combine all transform change will work more effective
    QPointF targetScenePos = mapToScene(centerPos);
    ViewportAnchor oldAnchor = this->transformationAnchor();
    setTransformationAnchor(QGraphicsView::NoAnchor);

    QTransform matrix = transform();
    matrix.translate(targetScenePos.x(), targetScenePos.y())
            .scale(factor, factor)
            .translate(-targetScenePos.x(), -targetScenePos.y());
    setTransform(matrix);

    setTransformationAnchor(oldAnchor);
}

void CanvasView::wheelEvent(QWheelEvent *event)
{
    if(event->modifiers().testFlag(Qt::ControlModifier))
    {
        double angle = event->angleDelta().y();

        double factor = qPow(1.0015, angle);    //smoother zoom
        zoomAt(event->pos(), factor);
        return;
    }

    QGraphicsView::wheelEvent(event);
}

围绕点的缩放矩阵公式:围绕点旋转,与缩放相同。


2
最初的回答:

简单示例:

class CGraphicsVew : public QGraphicsView
{
    Q_OBJECT

protected:
    void wheelEvent(QWheelEvent *event)
    {
        qreal deltaScale = 1;
        deltaScale += event->delta() > 0 ? 0.1 : -0.1;
        setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
        scale(deltaScale, deltaScale);
    }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接