要始终以鼠标指针为中心进行缩放 - 鼠标指针的位置只需成为缩放的原点。
听起来很简单,但我在准备演示时有点吃力。 (我的线性代数不太好,抱歉。)不过,我终于让它运行起来了。
我的示例代码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();
}
这三行才是真正有趣的(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
这就是它的外观:
第一张图片是应用程序启动后的快照。
然后我指向红色矩形的中心并轻轻转动轮子。红色矩形按预期在鼠标指针周围增长。
1st 更新:
而且,这是直接使用屏幕坐标的更新版本(而不是将所有内容都转换为 NDC):
#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();
}
相关的三行没有太大变化——鼠标坐标直接应用于变换。
顺便说一句。我改变了渲染——它现在像我使用的一样缩放线宽
qPainter.setMatrix(_mat);
在Canvas::paintEvent() 中,而不是“手动”转换所有点。
在我指向蓝色矩形的中心并转动鼠标滚轮后,快照显示了应用程序:
2nd更新:
建议的矩阵操作也适用于 QGraphicsView:
#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();
}
使用QGraphicsView 可以简化代码,因为不需要渲染代码——它已经内置了。
由于我对QGraphicsView 还没有太多经验,所以另一个问题对我造成了很大的打击:QGraphicsView 能够在应用转换后自动[cg] 修复视图位置。就我而言,这会适得其反,因为显然我的转变和QGraphicsView 似乎在相反的方向上“拉动”。
因此,我吸取了今天的教训:QGrapicsView::setTransformationAnchor(QGraphicsView::NoAnchor)
有必要关闭这个(在我的情况下不是故意的)自动居中。
我发现另一个值得注意的细节是QGraphicsView::mapToScene(),它可用于方便地将小部件坐标(例如鼠标坐标)转换为场景空间。