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 zoom functionality based on the current mouse pointer so items present on the current mouse pointer zoom towards the center of the view.
Code for Zoom base don center of view area
void csGuiView::wheelEvent(QWheelEvent *event)
{
if ((event->modifiers()&Qt::ControlModifier) == Qt::ControlModifier
&& event->angleDelta().x() == 0)
{
QPoint pos = event->pos();
QPointF posf = this->mapToScene(pos);
double angle = event->angleDelta().y();
double scalingFactor;
if(angle > 0)
{
scalingFactor = 1 + ( angle / 360 * 0.1);
}else if (angle < 0)
{
scalingFactor = 1 - ( -angle / 360 * 0.1);
} else
{
scalingFactor = 1;
}
m_pvtData->m_scale = scalingFactor;
this->scale(scalingFactor, scalingFactor);
double w = this->viewport()->width();
double h = this->viewport()->height();
double wf = this->mapToScene(QPoint(w-1, 0)).x()
- this->mapToScene(QPoint(0,0)).x();
double hf = this->mapToScene(QPoint(0, h-1)).y()
- this->mapToScene(QPoint(0,0)).y();
double lf = posf.x() - pos.x() * wf / w;
double tf = posf.y() - pos.y() * hf / h;
/* try to set viewport properly */
this->ensureVisible(lf, tf, wf, hf, 0, 0);
QPointF newPos = this->mapToScene(pos);
this->ensureVisible(QRectF(QPointF(lf, tf) - newPos + posf,
QSizeF(wf, hf)), 0, 0);
}
if ((event->modifiers()&Qt::ControlModifier) != Qt::ControlModifier) {
QGraphicsView::wheelEvent(event);
}
event->accept();
}
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.