web-dev-qa-db-fra.com

QGraphicsView Zoom avant et arrière sous la position de la souris à l'aide de la molette de la souris

J'ai une application avec une fenêtre QGraphicsView au milieu de l'écran. Je veux pouvoir zoomer et dézoomer à l'aide du défilement de la souris. 

Actuellement, j'ai ré-implémenté QGraphicsView et redéfini la fonction de défilement de la souris afin qu'elle ne défile pas l'image (comme c'est le cas par défaut). 

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

ainsi, lorsque je fais défiler, j'émets un signal vrai si la molette de la souris est fausse si elle est reculée.

J'ai ensuite connecté ce signal à un emplacement (fonction de zoom voir ci-dessous ) de la classe qui gère mes fonctions d'interface graphique. Maintenant, fondamentalement, je pense que ma fonction de zoom n'est tout simplement pas la meilleure façon de le faire. J'ai vu quelques exemples de personnes utilisant la fonction d'annulation de la roue pour régler des échelles, mais je ne pouvais pas vraiment trouver une réponse complète.

Au lieu de cela, j’ai fait cela, mais ce n’est pas parfait du tout. Je cherche donc à peaufiner légèrement ce problème ou à utiliser un exemple concret utilisant scale dans la fonction d’événement wheel.

J'initialise m_zoom_level à 0 dans le constructeur.

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

Comme vous pouvez le voir ci-dessus, j'utilise une variable QMatrix, que je redimensionne et redéfinis dans la vue Graphics et que je mets l'ancre de transformation sous la souris. Toutefois, cela ne fonctionne pas parfaitement. (ce qui, je pense, est lié au bouclage int ou autre chose). 

Comme je le disais, aider avec ceci ou un bon exemple d'échelle sous la souris serait formidable.

22
AngryDuck

Un tel zoom est un peu délicat. Permettez-moi de partager ma propre classe pour le faire.

Entête:

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

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

Exemple d'utilisation:

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

Voici une solution utilisant 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())
18
rengel

Voici la version python qui fonctionne pour moi. Provient de la combinaison des réponses de @Stefan Reinhardt et @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())
9
veslam

Après beaucoup de frustration, cela semble fonctionner. Le problème semble être que la QGraphicsView de transform n'a rien à voir avec sa position de défilement. Le comportement de QGraphicsView::mapToScene(const QPoint&) const dépend donc à la fois de la position de défilement et de la transformation. Je devais regarder la source de mapToScene pour comprendre cela.

Dans cet esprit, voici ce qui a fonctionné: souvenez-vous du point de la scène sur lequel pointe la souris, mettez à l'échelle, mappez ce point de la scène sur ses coordonnées, puis ajustez les barres de défilement pour que ce point se déroule sous la souris:

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

Vous pouvez simplement utiliser la fonctionnalité intégrée AnchorUnderMouse ou AnchorViewCenter pour maintenir le focus sous la souris ou au centre de la souris . Cela fonctionne pour moi dans 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);
        }
    }
5
Alex Maystrenko

Voici une version condensée de la solution ci-dessus; avec juste le code que vous devez mettre dans l'événement de roue. Cela fonctionne avec/sans barres de défilement dans mes tests, parfaitement;)

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
cmaughan

Il est un peu tard Mais je n’ai traversé la même chose aujourd’hui qu’avec Pyside, mais ça devrait être pareil ...

L’approche est "très simple", bien que cela m’ait coûté un peu de temps ......... Réglez d’abord tous les ancres sur NoAnchor, puis prenez le point de la roue, mappez-le sur la scène, traduisez la scène par cette valeur. , redimensionne et finalement traduit à nouveau:

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

C’est la seule solution qui a fonctionné pour mon but . À mon humble avis, c’est aussi la solution la plus logique ...

5
Stefan Reinhardt

Zoom plus lisse

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

}
3
e.n.shirokov

La combinaison de la solution @veslam: s avec le code Smooth Zoom de QT Wiki ( https://wiki.qt.io/Smooth_Zoom_In_QGraphicsView ) semble très bien fonctionner:

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

Entête:

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

Exemple simple:

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

Sous Mac OS, les solutions citées ici échouent parfois lors de l'utilisation de QGraphicsView :: setTransformationAnchor (AnchorUnderMouse):

1 - Qt ne met pas à jour lastMouseMoveScenePoint lorsque la fenêtre n’a pas le focus. En raison de ce zoom est effectué en utilisant la position de la souris quand il a perdu la mise au point, et non celle actuelle. ( https://bugreports.qt.io/browse/QTBUG-73033 )

2 - Qt arrête parfois de propager les événements de déplacement de la souris lors du changement de fenêtre à l’aide du contrôle de la mission. Le zoom se comporte donc de la même manière que dans # 1. ( https://bugreports.qt.io/browse/QTBUG-73067 ). J'ai créé cette video où les puces ne sont pas mises en surbrillance la deuxième fois que j'ai cliqué sur la fenêtre, car mouseMoveEvent n'est pas appelé. Je sais que ce n’est pas un bogue dans mon application car c’est l’exemple des 40000 puces fourni par Qt. J'ai posté la solution de contournement pour ce problème ici .

3 - setInteractive (false) ne peut pas être utilisé avec AnchorUnderMouse car la position de la souris utilisée comme centre de transformation n'est pas mise à jour: https://bugreports.qt.io/browse/QTBUG-60672

Il semble que Qt SDK n’ait pas été correctement testé pour les événements de déplacement de la souris dans des scénarios inhabituels tels que le zoom avec la molette de la souris.

0
Adriel Jr