27
votes

I have an application with a QGraphicsView window in the middle of the screen. I want to be able to zoom in and out using a mouse wheel scroll.

Currently I have re-implemented QGraphicsView and overriden the mouse scroll function so that it doesn't scroll the image (like it does by default).

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

so when I scroll, I'm emitting a signal true if mouse wheel forward false if mouse wheel back.

I have then connected this signal to a slot (zoom function see below) in the class that handles my GUI stuff. Now basically I think my zoom function just isn't the best way to do it at all I have seen some examples of people using the overriden wheelevent function to set scales but I couldn't really find a complete answer.

So instead I have done this but it's not perfect by any means so I'm looking for this to be tweaked a bit or for a working example using scale in the wheel event function.

I initialize m_zoom_level to 0 in the constructor.

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);
    }
}

As you can see above I'm using a QMatrix and scaling that and setting it to the Graphicsview and setting the transformation anchor to under mouse, but its just not working perfectly sometimes if I'm scrolling loads it will just start to zoom in only (which I think is to do with the int looping over or something).

As I said help with this or a good example of scale under mouse would be great.

13
It would be nice to just get a definitive answer to this question because there's too many questions like this all with different answers and most only seem half worked out before they were just left alone ... surely this isnt too complicated its more just the issue of correct scaling and how to apply it ..AngryDuck

13 Answers

47
votes

Such zooming is a bit tricky. Let me share my own class for doing that.

Header:

#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();
};

Source:

#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;
}

Usage example:

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

Here is a solution using 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())
15
votes

Here's the python version works for me. Comes from the combination of answers from @Stefan Reinhardt and @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())
11
votes

You can simply use builtin functionality AnchorUnderMouse or AnchorViewCenter to maintain focus under mouse or in the center. This works for me in 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);
        }
    }
6
votes

Here's a condensed version of the solution above; with just the code you need to put into the wheel event. This works with/without scroll bars in my testing, perfectly ;)

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;
    }
5
votes

After much frustration, this seems to work. The issue seems to be that the QGraphicsView's transform has nothing to do with its scroll position, so the behavior of QGraphicsView::mapToScene(const QPoint&) const depends on both the scroll position and the transform. I had to look at the source for mapToScene to understand this.

With that in mind, here's what worked: remember the scene point the mouse is pointing to, scale, map that scene point to mouse coordinates, then adjust the scroll bars to make that point wind up under the mouse:

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());
}
5
votes

It's a bit late but i walked through the same today only with Pyside, but should be the same...

The approach is "very simple", altough costed me a bit time... First set all Anchors to NoAnchor, then take the point of the wheelevent, map it to the scene, translate the scene by this value, scale and finally translate it back:

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())

This was the only solution that worked for my purpose. IMHO it is also the most logical solution...

3
votes

Smoother zoom

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());

}
1
votes

On Mac OS, the solutions cited here sometimes fail when using QGraphicsView::setTransformationAnchor(AnchorUnderMouse):

1 - Qt doesn't update lastMouseMoveScenePoint when the windows doesn't have focus. Because of that zoom is performed using the mouse position when it lost focus, and not the current one. (https://bugreports.qt.io/browse/QTBUG-73033)

2 - Qt sometimes stops propagating mouse move events when switching windows using mission control, so zoom also misbehaves like in #1. (https://bugreports.qt.io/browse/QTBUG-73067). I made this video where Chips are not highlighted the second time I clicked the window because mouseMoveEvent is not called. I know that it is not a bug in my application because this is the 40000 chips example provided by Qt. I posted the workaround for this issue here.

3 - setInteractive(false) can not be used with AnchorUnderMouse because mouse position used as centre of transformation is not updated: https://bugreports.qt.io/browse/QTBUG-60672

It seems that Qt SDK was not well tested for mouse move events in uncommon scenarios like zooming with the mouse wheel.

1
votes

Simple example:

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);
    }
};
1
votes

PyQt answered work well, here provide a c++ function, in case someone need in future.

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);
}

Scale around point matrix formula:rotate around point, which is same with scale.

0
votes

Combining @veslam:s solution with the Smooth Zoom code from QT Wiki (https://wiki.qt.io/Smooth_Zoom_In_QGraphicsView) seems to work very well:

Source:

QGraphicsViewMap::QGraphicsViewMap(QWidget *parent) : QGraphicsView(parent)
{
    setTransformationAnchor(QGraphicsView::NoAnchor);
    setResizeAnchor(QGraphicsView::NoAnchor);
}

void QGraphicsViewMap::wheelEvent(QWheelEvent* event)
{
    wheelEventMousePos = event->pos();

    int numDegrees = event->delta() / 8;
    int numSteps = numDegrees / 15; // see QWheelEvent documentation
    _numScheduledScalings += numSteps;
    if (_numScheduledScalings * numSteps < 0) // if user moved the wheel in another direction, we reset previously scheduled scalings
        _numScheduledScalings = numSteps;

    QTimeLine *anim = new QTimeLine(350, this);
    anim->setUpdateInterval(20);

    connect(anim, SIGNAL (valueChanged(qreal)), SLOT (scalingTime(qreal)));
    connect(anim, SIGNAL (finished()), SLOT (animFinished()));
    anim->start();
 }

void QGraphicsViewMap::scalingTime(qreal x)
{
    QPointF oldPos = mapToScene(wheelEventMousePos);

    qreal factor = 1.0+ qreal(_numScheduledScalings) / 300.0;
    scale(factor, factor);

    QPointF newPos = mapToScene(wheelEventMousePos);
    QPointF delta = newPos - oldPos;
    this->translate(delta.x(), delta.y());
}

void QGraphicsViewMap::animFinished()
{
    if (_numScheduledScalings > 0)
        _numScheduledScalings--;
    else
        _numScheduledScalings++;

    sender()->~QObject();
}

Header:

class QGraphicsViewMap : public QGraphicsView
{
    Q_OBJECT

private:
    qreal _numScheduledScalings = 0;
    QPoint wheelEventMousePos;
public:
    explicit QGraphicsViewMap(QWidget *parent = 0);

signals:

public slots:
    void wheelEvent(QWheelEvent* event);
    void scalingTime(qreal x);
    void animFinished();
};
0
votes

void GraphicsView::wheelEvent(QWheelEvent* event)
{
    switch (event->modifiers()) {
    case Qt::ControlModifier:
        if (event->angleDelta().x() != 0)
            QAbstractScrollArea::horizontalScrollBar()->setValue(QAbstractScrollArea::horizontalScrollBar()->value() - (event->delta()));
        else
            QAbstractScrollArea::verticalScrollBar()->setValue(QAbstractScrollArea::verticalScrollBar()->value() - (event->delta()));
        break;
    case Qt::ShiftModifier:
        QAbstractScrollArea::horizontalScrollBar()->setValue(QAbstractScrollArea::horizontalScrollBar()->value() - (event->delta()));
        break;
    case Qt::NoModifier:
        if (abs(event->delta()) == 120) {
            if (event->delta() > 0)
                zoomIn();
            else
                zoomOut();
        }
        break;
    default:
        QGraphicsView::wheelEvent(event);
        return;
    }
    event->accept();
}

const double zoomFactor = 1.5;

void GraphicsView::zoomIn()
{
    scale(zoomFactor, zoomFactor);
}

void GraphicsView::zoomOut()
{
    scale(1.0 / zoomFactor, 1.0 / zoomFactor);
}