如何让图像在放大、平移、缩小后回到原来的居中位置?

问题描述 投票:0回答:1

我正在构建一个具有放大和缩小功能的图像查看器小部件,包括对平移的支持(单击并拖动以移动图像)。

目前,放大后直接缩小可以正常工作,图像会返回到起始位置。但是,当我放大、将图像平移到不同位置,然后缩小时,图像不会返回到原来的中心位置。

演示视频: https://i.imgur.com/xgt9V2H.gif

演示GIF:
enter image description here

黑色区域是视图背景,不应该是可见的。

我正在寻求有关缩小时正确重新定位图像所需的计算的帮助,特别是在这部分代码中:

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();
}
c++ qt qgraphicsview qgraphicsscene qgraphicspixmapitem
1个回答
0
投票

您想要的是确保场景的左上角和右下角点分别位于视图的左上角和右下角或视图之外。

wheelEvent
mouseMoveEvent
可以使用相同的私有方法(下面称为
centerScene
)。它应该保证您永远看不到场景的外部部分。

当我们这样做时,我们可以简化您为放大/缩小而创建的代码。我没有深入分析你的长片段,但你似乎有点挣扎。
放大/缩小通常会向下,以便固定光标下的点,使用这个简单的三步技巧:

  1. 通过平移将原点移动到固定点。
  2. 缩放。
  3. 做相反的翻译。

只要

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
依赖于取消当前缩放(如果有)时缩小停止的事实。
如果您允许这样做,您将需要实现一些功能,例如将图像与小部件的中心对齐。

© www.soinside.com 2019 - 2024. All rights reserved.