Zoom functionality using Qt

前端 未结 1 1800
一生所求
一生所求 2020-12-21 13:47

Current implementation, zooms towards the center of View so items present in the top left corner or the current mouse pointer is not visible when we Zoom it.

I want

相关标签:
1条回答
  • 2020-12-21 14:32

    To zoom always centered at mouse pointer – the position of mouse pointer just has to become the origin of scaling.

    It sounds that simple but I struggled a bit to prepare a demonstration. (I'm not that good in linear algebra, sorry.) However, I finally got it running.

    My sample code testQWidget-Zoom.cc:

    #include <vector>
    #include <QtWidgets>
    
    // class for widget to demonstrate zooming
    class Canvas: public QWidget {
      // types:
      private:
        struct Geo {
          QRectF rect; QColor color;
          Geo(const QRectF &rect, const QColor &color):
            rect(rect), color(color)
          { }
        };
      // variables:
      private:
        bool _initDone : 1; // flag: true ... sample geo created
        std::vector<Geo> _scene; // contents to render
        QMatrix _mat; // view matrix
      // methods:
      public: 
        // constructor.
        Canvas(): QWidget(), _initDone(false) { }
        // destructor.
        virtual ~Canvas() = default;
        // disabled:
        Canvas(const Canvas&) = delete;
        Canvas& operator=(const Canvas&) = delete;
      private:
        // initializes sample geo
        void init()
        {
          if (_initDone) return;
          _initDone = true;
          // build scene (with NDC i.e. view x/y range: [-1, 1])
          _scene.emplace_back(Geo(QRectF(-1.0f, -1.0f, 2.0f, 2.0f), QColor(0x000000u)));
          _scene.emplace_back(Geo(QRectF(-0.2f, -0.2f, 0.4f, 0.4f), QColor(0x00ff00u)));
          _scene.emplace_back(Geo(QRectF(-0.8f, -0.8f, 0.4f, 0.4f), QColor(0xff0000u)));
          _scene.emplace_back(Geo(QRectF(-0.8f, 0.4f, 0.4f, 0.4f), QColor(0x0000ffu)));
          _scene.emplace_back(Geo(QRectF(0.4f, 0.4f, 0.4f, 0.4f), QColor(0xff00ffu)));
          _scene.emplace_back(Geo(QRectF(0.4f, -0.8f, 0.4f, 0.4f), QColor(0xffff00u)));
          // get initial scaling
          const int wView = width(), hView = height();
          _mat.scale(wView / 2, hView / 2);
          _mat.translate(1, 1);
        }
      protected:
        virtual void paintEvent(QPaintEvent *pQEvent) override
        {
          init();
          // render
          QPainter qPainter(this);
    #if 0 // This scales line width as well:
          qPainter.setMatrix(_mat);
          for (const Geo &geo : _scene) {
            qPainter.setPen(geo.color);
            qPainter.drawRect(geo.rect);
          }
    #else // This transforms only coordinates:
          for (const Geo &geo : _scene) {
            qPainter.setPen(geo.color);
            QRectF rect(geo.rect.topLeft() * _mat, geo.rect.bottomRight() * _mat);
            qPainter.drawRect(rect);
          }
    #endif // 0
        }
        virtual void wheelEvent(QWheelEvent *pQEvent) override
        {
          //qDebug() << "Wheel Event:"
          //qDebug() << "mouse pos:" << pQEvent->pos();
          // pos() -> virtual canvas
          bool matInvOK = false;
          QMatrix matInv = _mat.inverted(&matInvOK);
          if (!matInvOK) {
            qDebug() << "View matrix not invertible!";
            return;
          }
          QPointF posNDC
            = QPointF(pQEvent->pos().x(), pQEvent->pos().y()) * matInv;
          //qDebug() << "mouse pos (NDC):" << posNDC;
          float delta = 1.0f + pQEvent->angleDelta().y() / 1200.0f;
          //qDebug() << "angleDelta:" << pQEvent->angleDelta().y();
          //qDebug() << "scale factor:" << delta;
          _mat.translate(posNDC.x(), posNDC.y()); // origin to spot
          _mat.scale(delta, delta); // scale
          _mat.translate(-posNDC.x(), -posNDC.y()); // spot to origin
          update();
          pQEvent->accept();
        }
    };
    
    
    int main(int argc, char **argv)
    {
      QApplication app(argc, argv);
      Canvas canvas;
      canvas.resize(512, 512);
      canvas.show();
      // runtime loop
      return app.exec();
    }
    

    and these three lines are the actual interesting ones (in Canvas::wheelEvent()):

          _mat.translate(posNDC.x(), posNDC.y()); // origin to spot
          _mat.scale(delta, delta); // scale
          _mat.translate(-posNDC.x(), -posNDC.y()); // spot to origin
    

    And this is how it looks:

    The first image is a snapshot of the application just after starting it.

    Then I pointed into the center of the red rectangle and turned the wheel slightly. The red rectangle grew around the mouse pointer as intended.


    1st Update:

    And, this is the updated version which uses screen coordinates directly (instead of converting everything to NDCs):

    #include <vector>
    #include <QtWidgets>
    
    // class for widget to demonstrate zooming
    class Canvas: public QWidget {
      // types:
      private:
        struct Geo {
          QRectF rect; QColor color;
          Geo(const QRectF &rect, const QColor &color):
            rect(rect), color(color)
          { }
        };
      // variables:
      private:
        bool _initDone : 1; // flag: true ... sample geo created
        std::vector<Geo> _scene; // contents to render
        QMatrix _mat; // view matrix
      // methods:
      public: 
        // constructor.
        Canvas(): QWidget(), _initDone(false) { }
        // destructor.
        virtual ~Canvas() = default;
        // disabled:
        Canvas(const Canvas&) = delete;
        Canvas& operator=(const Canvas&) = delete;
      private:
        // initializes sample geo
        void init()
        {
          if (_initDone) return;
          _initDone = true;
          const int wView = width(), hView = height();
          // build scene (with NDC i.e. view x/y range: [-1, 1])
          _scene.emplace_back(Geo(QRectF(-1.0f, -1.0f, 2.0f, 2.0f), QColor(0x000000u)));
          _scene.emplace_back(Geo(QRectF(-0.2f, -0.2f, 0.4f, 0.4f), QColor(0x00ff00u)));
          _scene.emplace_back(Geo(QRectF(-0.8f, -0.8f, 0.4f, 0.4f), QColor(0xff0000u)));
          _scene.emplace_back(Geo(QRectF(-0.8f, 0.4f, 0.4f, 0.4f), QColor(0x0000ffu)));
          _scene.emplace_back(Geo(QRectF(0.4f, 0.4f, 0.4f, 0.4f), QColor(0xff00ffu)));
          _scene.emplace_back(Geo(QRectF(0.4f, -0.8f, 0.4f, 0.4f), QColor(0xffff00u)));
          // scale geometry to screen coordinates
          QMatrix mat;
          mat.scale(wView / 2, hView / 2);
          mat.translate(1, 1);
          for (Geo &geo : _scene) {
            geo.rect = QRectF(geo.rect.topLeft() * mat, geo.rect.bottomRight() * mat);
          }
        }
      protected:
        virtual void paintEvent(QPaintEvent *pQEvent) override
        {
          init();
          // render
          QPainter qPainter(this);
          qPainter.setMatrix(_mat);
          for (const Geo &geo : _scene) {
            qPainter.setPen(geo.color);
            qPainter.drawRect(geo.rect);
          }
        }
        virtual void wheelEvent(QWheelEvent *pQEvent) override
        {
          //qDebug() << "Wheel Event:";
          //qDebug() << "mouse pos:" << pQEvent->pos();
          float delta = 1.0f + pQEvent->angleDelta().y() / 1200.0f;
          //qDebug() << "angleDelta:" << pQEvent->angleDelta().y();
          //qDebug() << "scale factor:" << delta;
          _mat.translate(pQEvent->pos().x(), pQEvent->pos().y()); // origin to spot
          _mat.scale(delta, delta); // scale
          _mat.translate(-pQEvent->pos().x(), -pQEvent->pos().y()); // spot to origin
          update();
          pQEvent->accept();
        }
    };
    
    int main(int argc, char **argv)
    {
      QApplication app(argc, argv);
      Canvas canvas;
      canvas.resize(256, 256);
      canvas.show();
      // runtime loop
      return app.exec();
    }
    

    The relevant three lines didn't change much – the mouse coordinates are applied directly to transformation.

    Btw. I changed the rendering – it now scales line width as well as I used

          qPainter.setMatrix(_mat);
    

    in Canvas::paintEvent() instead of transforming all points "manually".

    The snapshot shows the application after I pointed into the center of the blue rectangle and turned the mouse wheel:


    2nd Update:

    The suggested matrix manipulation works in a QGraphicsView as well:

    #include <QtWidgets>
    
    // class for widget to demonstrate zooming
    class Canvas: public QGraphicsView {
      // methods:
      public: 
        // constructor.
        Canvas() = default;
        // destructor.
        virtual ~Canvas() = default;
        // disabled:
        Canvas(const Canvas&) = delete;
        Canvas& operator=(const Canvas&) = delete;
    
      protected:
    
        virtual void wheelEvent(QWheelEvent *pQEvent) override
        {
          //qDebug() << "Wheel Event:";
          // pos() -> virtual canvas
          QPointF pos = mapToScene(pQEvent->pos());
          //qDebug() << "mouse pos:" << pos;
          // scale from wheel angle
          float delta = 1.0f + pQEvent->angleDelta().y() / 1200.0f;
          //qDebug() << "angleDelta:" << pQEvent->angleDelta().y();
          //qDebug() << "scale factor:" << delta;
          // modify transform matrix
          QTransform xform = transform();
          xform.translate(pos.x(), pos.y()); // origin to spot
          xform.scale(delta, delta); // scale
          xform.translate(-pos.x(), -pos.y()); // spot to origin
          setTransform(xform);
          //qDebug() << "transform:" << xform;
          // force update
          update();
          pQEvent->accept();
        }
    };
    
    QRectF toScr(QWidget *pQWidget, float x, float y, float w, float h)
    {
      const int wView = pQWidget->width(), hView = pQWidget->height();
      const int s = wView < hView ? wView : hView;
      return QRectF(
        (0.5f * x + 0.5f) * s, (0.5f * y + 0.5f) * s,
        0.5f * w * s, 0.5f * h * s);
    }
    
    int main(int argc, char **argv)
    {
      QApplication app(argc, argv);
      // setup GUI
      Canvas canvas;
      canvas.setTransformationAnchor(QGraphicsView::NoAnchor);
      canvas.resize(256, 256);
      canvas.show();
      // prepare scene
      QGraphicsScene qGScene;
      qGScene.addRect(toScr(canvas.viewport(), -1.0f, -1.0f, 2.0f, 2.0f), QColor(0x000000u));
      qGScene.addRect(toScr(canvas.viewport(), -0.2f, -0.2f, 0.4f, 0.4f), QColor(0x00ff00u));
      qGScene.addRect(toScr(canvas.viewport(), -0.8f, -0.8f, 0.4f, 0.4f), QColor(0xff0000u));
      qGScene.addRect(toScr(canvas.viewport(), -0.8f, 0.4f, 0.4f, 0.4f), QColor(0x0000ffu));
      qGScene.addRect(toScr(canvas.viewport(), 0.4f, 0.4f, 0.4f, 0.4f), QColor(0xff00ffu));
      qGScene.addRect(toScr(canvas.viewport(), 0.4f, -0.8f, 0.4f, 0.4f), QColor(0xffff00u));
      canvas.setScene(&qGScene);
      // runtime loop
      return app.exec();
    }
    

    Using a QGraphicsView simplifies code as no rendering code is needed – it's already built-in.

    As I have not (yet) much experience with QGraphicsView, another issue hit me quite hard: The QGraphicsView is able to fix the view position automati[cg]ally after a transformation has been applied. In my case, this was rather counter-productive as obviously my transformation and the QGraphicsView seemed to "pull" in opposite directions.

    Hence, I've learnt my lesson of the day: QGrapicsView::setTransformationAnchor(QGraphicsView::NoAnchor) is necessary to switch off this (in my case not-intended) auto-centering.

    The other detail I find worth to notice is QGraphicsView::mapToScene() which can be used to conveniently convert widget coordinates (e.g. mouse coordinates) to scene space.

    0 讨论(0)
提交回复
热议问题