我正在构建一个具有放大和缩小功能的图像查看器小部件,包括对平移的支持(单击并拖动以移动图像)。
目前,放大后直接缩小可以正常工作,图像会返回到起始位置。但是,当我放大、将图像平移到不同位置,然后缩小时,图像不会返回到原来的中心位置。
演示视频: https://i.imgur.com/xgt9V2H.gif
黑色区域是视图背景,不应该是可见的。
我正在寻求有关缩小时正确重新定位图像所需的计算的帮助,特别是在这部分代码中:
if (factor < 1.0) // Zooming out { ... }
。
最小可重现示例:
#include <QtWidgets>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsPixmapItem>
class ZoomGraphicsView : public QGraphicsView
{
Q_OBJECT
public:
ZoomGraphicsView()
{
scene = new QGraphicsScene(this);
setScene(scene);
// Basic setup - explicitly disable scrollbars
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setRenderHint(QPainter::SmoothPixmapTransform);
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
setFrameShape(QFrame::NoFrame);
setTransformationAnchor(QGraphicsView::NoAnchor);
setResizeAnchor(QGraphicsView::NoAnchor);
setBackgroundBrush(Qt::black);
setAlignment(Qt::AlignCenter);
setOptimizationFlag(QGraphicsView::DontAdjustForAntialiasing);
// Initialize pixmap item
pixmapItem = new QGraphicsPixmapItem();
scene->addItem(pixmapItem);
// Initialize base scale
baseScale = 1.0;
currentScale = 1.0;
isZooming = false;
}
void setImage(const QImage& image)
{
QPixmap newPixmap = QPixmap::fromImage(image);
if (newPixmap.isNull())
return;
// Update pixmap
pixmapItem->setPixmap(newPixmap);
if (firstImage)
{
resetView();
firstImage = false;
}
// Update scene rect to match the viewport
scene->setSceneRect(viewport()->rect());
}
protected:
void wheelEvent(QWheelEvent* event) override
{
if (pixmapItem->pixmap().isNull())
return;
isZooming = true;
// Store cursor position relative to scene
QPointF mousePosScene = mapToScene(event->position().toPoint());
QPointF mousePosCurrent = event->position();
// Calculate zoom factor
double factor = pow(1.5, event->angleDelta().y() / 240.0);
double newScale = currentScale * factor;
// Handle zoom out specifically
if (factor < 1.0) // Zooming out
{
if (newScale < baseScale)
{
resetView();
isZooming = false;
event->accept();
return;
}
// 1. Get the current positions before any transformation
QRectF viewRect = viewport()->rect();
QPointF mousePos = event->position();
QPointF mousePosScene = mapToScene(mousePos.toPoint());
// 2. Calculate the view center and the distance from mouse to center
QPointF viewCenter = mapToScene(viewRect.center().x(), viewRect.center().y());
QPointF mouseOffset = mousePosScene - viewCenter;
// 3. Apply the new scale
QTransform newTransform;
newTransform.scale(newScale, newScale);
setTransform(newTransform);
// 4. Calculate how much the mouse point moved after scaling
QPointF newMousePosScene = mapToScene(mousePos.toPoint());
QPointF deltaPos = newMousePosScene - mousePosScene;
// 5. Adjust the view to maintain the mouse position
translate(deltaPos.x(), deltaPos.y());
// 6. Calculate the scaled image bounds
QRectF imageRect = pixmapItem->boundingRect();
QRectF scaledImageRect = QRectF(imageRect.topLeft() * newScale, imageRect.size() * newScale);
// 7. Ensure the view stays within bounds
QPointF currentCenter = mapToScene(viewRect.center().x(), viewRect.center().y());
QPointF newCenter = currentCenter;
// 8. Apply bounds to keep image filling the view
if (scaledImageRect.width() < viewRect.width())
newCenter.setX(scaledImageRect.center().x());
else
{
qreal minX = viewRect.width() / 2.0;
qreal maxX = scaledImageRect.width() - minX;
newCenter.setX(qBound(minX, currentCenter.x(), maxX));
}
if (scaledImageRect.height() < viewRect.height())
newCenter.setY(scaledImageRect.center().y());
else
{
qreal minY = viewRect.height() / 2.0;
qreal maxY = scaledImageRect.height() - minY;
newCenter.setY(qBound(minY, currentCenter.y(), maxY));
}
// 9. Update to the bounded position
centerOn(newCenter);
currentScale = newScale;
qDebug() << "\nviewRect: " << viewRect;
qDebug() << "mousePos: " << mousePos;
qDebug() << "mousePosScene: " << mousePosScene;
qDebug() << "viewCenter: " << viewCenter;
qDebug() << "mouseOffset: " << mouseOffset;
qDebug() << "newMousePosScene:" << newMousePosScene;
qDebug() << "deltaPos: " << deltaPos;
qDebug() << "currentCenter: " << currentCenter;
qDebug() << "newCenter: " << newCenter;
qDebug() << "imageRect: " << imageRect;
qDebug() << "scaledImageRect: " << scaledImageRect;
qDebug() << "factor: " << factor;
qDebug() << "newScale: " << newScale;
}
else // Zooming in
{
if (newScale > 40.0)
{
isZooming = false;
event->accept();
return;
}
// Update scale
currentScale = newScale;
// Apply new transform
QTransform newTransform = transform();
newTransform.scale(factor, factor);
setTransform(newTransform);
// Calculate new scene position under mouse after scaling
QPointF newMousePosScene = mapToScene(mousePosCurrent.toPoint());
QPointF offset = newMousePosScene - mousePosScene;
// Adjust view position to keep mouse point stable
translate(offset.x(), offset.y());
}
currentScale = newScale;
isZooming = false;
event->accept();
}
void mousePressEvent(QMouseEvent* event) override
{
if (event->button() == Qt::LeftButton)
{
isPanning = true;
lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
event->accept();
}
}
void mouseMoveEvent(QMouseEvent* event) override
{
if (isPanning)
{
QPointF delta = mapToScene(event->pos()) - mapToScene(lastMousePos);
QPointF newCenter = mapToScene(viewport()->rect().center()) - delta;
centerOn(newCenter);
lastMousePos = event->pos();
event->accept();
}
}
void mouseReleaseEvent(QMouseEvent* event) override
{
if (event->button() == Qt::LeftButton)
{
isPanning = false;
setCursor(Qt::ArrowCursor);
event->accept();
}
}
void resizeEvent(QResizeEvent* event) override
{
QGraphicsView::resizeEvent(event);
if (!pixmapItem->pixmap().isNull())
{
resetView();
scene->setSceneRect(viewport()->rect());
}
}
// Override to block automatic scrolling only during zoom
void scrollContentsBy(int dx, int dy) override
{
if (!isZooming)
QGraphicsView::scrollContentsBy(dx, dy);
}
private:
QGraphicsScene* scene;
QGraphicsPixmapItem* pixmapItem;
bool firstImage = true;
bool isPanning = false;
bool isZooming = false;
QPoint lastMousePos;
qreal baseScale;
qreal currentScale;
void resetView()
{
// Reset transform
setTransform(QTransform());
// Calculate scale to fit view
QRectF viewRect = viewport()->rect();
QRectF imageRect = pixmapItem->boundingRect();
qreal scaleX = viewRect.width() / imageRect.width();
qreal scaleY = viewRect.height() / imageRect.height();
baseScale = qMin(scaleX, scaleY);
currentScale = baseScale;
// Center the pixmap in the view
pixmapItem->setPos((viewRect.width() - imageRect.width() * baseScale) / 2.0,
(viewRect.height() - imageRect.height() * baseScale) / 2.0);
// Apply base scale
QTransform transform;
transform.scale(baseScale, baseScale);
setTransform(transform);
}
};
class Widget: public QWidget
{
Q_OBJECT
public:
Widget()
{
ZoomGraphicsView* view = new ZoomGraphicsView;
QHBoxLayout* layout = new QHBoxLayout(this);
layout->addWidget(view);
QFile file("test.png");
file.open(QIODevice::ReadOnly);
QImage image;
image.load(&file, "PNG");
file.close();
view->setFixedSize(image.size());
view->setImage(image);
}
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
您想要的是确保场景的左上角和右下角点分别位于视图的左上角和右下角或视图之外。
wheelEvent
和mouseMoveEvent
可以使用相同的私有方法(下面称为centerScene
)。它应该保证您永远看不到场景的外部部分。
当我们这样做时,我们可以简化您为放大/缩小而创建的代码。我没有深入分析你的长片段,但你似乎有点挣扎。
放大/缩小通常会向下,以便固定光标下的点,使用这个简单的三步技巧:
只要
centerScene
不决定场景必须移动,这使得光标下很容易有一个固定点。
这导致调整后的
wheelEvent
和 mouseMoveEvent
方法 + 附加 centerScene
:
void wheelEvent(QWheelEvent* event) override
{
if (pixmapItem->pixmap().isNull())
return;
// Store cursor position relative to scene
QPointF mousePos = event->position(),
mousePosCurrent = mapToScene(mousePos.x(), mousePos.y());
// Calculate zoom factor
double factor = pow(1.5, event->angleDelta().y() / 240.0);
double newScale = currentScale * factor;
if (newScale < 1.) { // Do not allow zooming out (in absolute)
factor /= newScale;
newScale = 1.;
}
// Zoom centered on the mouse cursor
QTransform t;
t.translate(mousePosCurrent.x(), mousePosCurrent.y());
t.scale(factor, factor);
t.translate(-mousePosCurrent.x(), -mousePosCurrent.y());
setTransform(t, true);
currentScale = newScale;
centerScene();
event->accept();
}
void mouseMoveEvent(QMouseEvent* event) override
{
if (isPanning)
{
QPointF delta = mapToScene(event->pos()) - mapToScene(lastMousePos);
QTransform t;
t.translate(delta.x(), delta.y());
setTransform(t, true);
centerScene();
lastMousePos = event->pos();
event->accept();
}
}
void centerScene()
{ // First check if the top left corner is in the middle of the view.
QPointF topLeft = mapFromScene(0, 0);
if (topLeft.x() > 0 || topLeft.y() > 0) {
QTransform t;
t.translate(
topLeft.x() > 0 ? -topLeft.x() / currentScale : 0,
topLeft.y() > 0 ? -topLeft.y() / currentScale : 0
);
setTransform(t, true);
}
else { // If not, then check if perhaps the bottom right corner is.
QPointF bottomRight = mapFromScene(width(), height());
if (bottomRight.x() < width() || bottomRight.y() < height()) {
QTransform t;
t.translate(
bottomRight.x() < width() ? -(bottomRight.x() - width()) / currentScale : 0,
bottomRight.y() < height() ? -(bottomRight.y() - height()) / currentScale : 0
);
setTransform(t, true);
}
}
}
注意:
centerScene
依赖于取消当前缩放(如果有)时缩小停止的事实。