Qt实战:实现图像的缩放、移动、标记及保存

发布于:2025-09-11 ⋅ 阅读:(23) ⋅ 点赞:(0)

一、实例演示

头文件:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

protected:
    virtual void paintEvent(QPaintEvent *event) override;
    virtual void mousePressEvent(QMouseEvent *event) override;
    virtual void mouseMoveEvent(QMouseEvent *event) override;
    virtual void mouseReleaseEvent(QMouseEvent *event) override;
    virtual void wheelEvent(QWheelEvent *event) override;
    virtual void keyPressEvent(QKeyEvent *event) override;

private:
    void OnSavePixmapWithRectangle(const QString& strFileName);
    QRect GetPixmapDrawRect() const;
    QPointF ToPixmapCoord(const QPoint& widgetPos) const; // 窗口坐标 -> pixmap坐标
    QPointF ToWidgetCoord(const QPointF& pixmapPos) const; // pixmap坐标 -> 窗口坐标

private:
    bool m_bIsDrawing;
    QPoint m_startPoint;
    QPoint m_currentPoint;

    double m_dScaleFactor;

    bool m_bIsPanning;
    QPoint m_panOffset;
    QPoint m_lastPanPoint;

    QVector<QRectF> m_rectanglesVec;

    QPixmap m_pixmap;
};
#endif // MAINWINDOW_H

源文件:

#include "main_window.h"

#include <QPainter>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QWidget(parent)
    , m_bIsDrawing(false)
    , m_dScaleFactor(1.0)
    , m_bIsPanning(false)
{
    this->setWindowTitle("图像操作");
    
		if (!m_pixmap.load("2025-09-10_19-32-15.png")) {
        qDebug() << "加载失败";
    }
}

MainWindow::~MainWindow()
{
}

void MainWindow::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::SmoothPixmapTransform, true);

    // 获取目标区域
    QRect targetRect = GetPixmapDrawRect();
    painter.drawPixmap(targetRect, m_pixmap);

    // 绘制已有矩形
    painter.setPen(QPen(Qt::red, 2));
    for (const QRectF& rect : m_rectanglesVec) {
        QPointF p1 = ToWidgetCoord(rect.topLeft());
        QPointF p2 = ToWidgetCoord(rect.bottomRight());
        painter.drawRect(QRectF(p1, p2).normalized());
    }

    // 绘制临时矩形
    if (m_bIsDrawing && !m_pixmap.isNull()) {
        painter.setPen(QPen(Qt::red, 2));
        painter.drawRect(QRect(m_startPoint, m_currentPoint).normalized());
    }

    QWidget::paintEvent(event);
}

void MainWindow::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton && !m_pixmap.isNull()) {
        // 保存鼠标起点(窗口坐标)
        m_startPoint = event->pos();
        m_currentPoint = m_startPoint;
        m_bIsDrawing = true;
    } else if (event->button() == Qt::RightButton) {
        m_bIsPanning = true;
        m_lastPanPoint = event->pos();
    }
}


void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
    if (m_bIsDrawing) {
        m_currentPoint = event->pos();

        update(); // 触发重绘,显示临时矩形
    } else if (m_bIsPanning) {
        QPoint delta = event->pos() - m_lastPanPoint;
        m_panOffset += delta;
        m_lastPanPoint = event->pos();

        update();
    }
}

void MainWindow::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton &&m_bIsDrawing) {
        m_currentPoint = event->pos();
        m_bIsDrawing = false;

        // 转换到pixmap坐标
        QPointF p1 = ToPixmapCoord(m_startPoint);
        QPointF p2 = ToPixmapCoord(m_currentPoint);
        QRectF rectInPixmap(p1, p2);

        // 保存矩形(pixmap坐标)
        m_rectanglesVec.append(rectInPixmap.normalized());
    } else if (event->button() == Qt::RightButton) {
        m_bIsPanning = false;
    }
}

void MainWindow::wheelEvent(QWheelEvent *event)
{
    if (event->buttons() == Qt::NoButton) {
        if (event->angleDelta().y() > 0) {
            m_dScaleFactor *= 1.1; // 放大 10%
        } else {
            m_dScaleFactor *= 0.9; // 缩小 10%
        }

        // 限制范围
        if (m_dScaleFactor < 0.1) m_dScaleFactor = 0.1;
        if (m_dScaleFactor > 10.0) m_dScaleFactor = 10.0;

        update();
    }
}

void MainWindow::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_R) {
        m_dScaleFactor = 1.0;
        m_panOffset = QPoint(0, 0);
        m_rectanglesVec.clear();

        update();
    } else if (event->key() == Qt::Key_S) {
        QString strFileName = QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss") + ".png";
        this->OnSavePixmapWithRectangle(strFileName);
    }
}

void MainWindow::OnSavePixmapWithRectangle(const QString& strFileName)
{
    if (m_pixmap.isNull()) {
        return;
    }

    // 拷贝一份原图
    QPixmap resultPixmap = m_pixmap.copy();

    // 画矩形
    QPainter painter(&resultPixmap);
    painter.setPen(QPen(Qt::red, 2));
    for(const QRectF &rect : m_rectanglesVec) {
        painter.drawRect(rect.normalized());
    }

    resultPixmap.save(strFileName, "PNG");
}

QRect MainWindow::GetPixmapDrawRect() const
{
    QSize lableSize = this->size();
    QPixmap scalePixmap = m_pixmap.scaled(lableSize * m_dScaleFactor, Qt::KeepAspectRatio, Qt::SmoothTransformation);

    // 计算居中位置
    int posX = (lableSize.width() - scalePixmap.width()) / 2;
    int posY = (lableSize.height()- scalePixmap.height()) / 2;

    // 加上平移的偏移量
    posX += m_panOffset.x();
    posY += m_panOffset.y();

    return QRect(posX, posY, scalePixmap.width(), scalePixmap.height());
}

QPointF MainWindow::ToPixmapCoord(const QPoint& widgetPos) const
{
    QRect targetRect = GetPixmapDrawRect();
    double scaleX = double(m_pixmap.width()) / targetRect.width();
    double scaleY = double(m_pixmap.height()) / targetRect.height();
    QPointF offset = widgetPos - targetRect.topLeft();

    return QPointF(offset.x() * scaleX, offset.y() * scaleY);
}

QPointF MainWindow::ToWidgetCoord(const QPointF& pixmapPos) const
{
    QRect targetRect = GetPixmapDrawRect();
    double scaleX = double(m_pixmap.width()) / targetRect.width();
    double scaleY = double(m_pixmap.height()) / targetRect.height();

    return QPointF(targetRect.left() + pixmapPos.x() / scaleX, targetRect.top() + pixmapPos.y() / scaleY);
}

输出结果:
在这里插入图片描述

二、实例分析

代码细节解析:

QRect MainWindow::GetPixmapDrawRect() const
{
    QSize lableSize = this->size();
    QPixmap scalePixmap = m_pixmap.scaled(lableSize * m_dScaleFactor, Qt::KeepAspectRatio, Qt::SmoothTransformation);

    // 计算居中位置
    int posX = (lableSize.width() - scalePixmap.width()) / 2;
    int posY = (lableSize.height()- scalePixmap.height()) / 2;

    // 加上平移的偏移量
    posX += m_panOffset.x();
    posY += m_panOffset.y();

    return QRect(posX, posY, scalePixmap.width(), scalePixmap.height());
}

根据窗口大小、缩放因子和平移偏移量,计算出 pixmap 在窗口里应该绘制的位置和大小矩形区域。

QSize lableSize = this->size();

获取当前窗口的大小(也就是 QWidget 的宽高)。

QPixmap scalePixmap = m_pixmap.scaled(lableSize * m_dScaleFactor,
                                      Qt::KeepAspectRatio,
                                      Qt::SmoothTransformation);
  • 将原始 m_pixmap 按照缩放因子 m_dScaleFactor 缩放。
  • Qt::KeepAspectRatio 保证缩放后宽高比不变。
  • Qt::SmoothTransformation 让缩放更平滑(但性能稍差)。
  • scalePixmap 是临时变量,只是为了获取缩放后的宽高。
int posX = (lableSize.width() - scalePixmap.width()) / 2;
int posY = (lableSize.height()- scalePixmap.height()) / 2;
  • 让图片在窗口中居中显示
  • 比如窗口宽 800,而图像宽 600,那么 (800-600)/2 = 100,图像会从 x=100 开始绘制。
posX += m_panOffset.x();
posY += m_panOffset.y();
  • 考虑用户平移(拖拽)操作的偏移量。
  • m_panOffset 保存了用户右键拖拽的累计位移。
return QRect(posX, posY, scalePixmap.width(), scalePixmap.height());

最终返回一个矩形区域,告诉 paintEvent:图像应该画在窗口的哪个位置、多大尺寸。

参数含义:
QRect(int x, int y, int width, int height)

  • x → 矩形左上角的 X 坐标
  • y → 矩形左上角的 Y 坐标
  • scalePixmap.width()→ 矩形的宽度
  • scalePixmap.height() → 矩形的高度

所以这一句等价于:

  • 位置:(x, y)(图片绘制的左上角坐标)
  • 大小:scalePixmap.width() × scalePixmap.height()

坐标示意图(窗口、targetRect、图片居中效果):
在这里插入图片描述
上述代码就是表示把图像绘制到 targetRect 这个矩形里。而我们之前算 x、y 的目的,就是为了让这个矩形正好居中对齐。

换句话说:

  • 窗口区域是整个 QWidget 的大小。
  • targetRect 是图像要显示的位置和大小。
  • Qt 会把图像绘制到这个 targetRect 中。

代码细节解析:

QPointF MainWindow::ToPixmapCoord(const QPoint& widgetPos) const
{
    QRect targetRect = GetPixmapDrawRect();
    double scaleX = double(m_pixmap.width()) / targetRect.width();
    double scaleY = double(m_pixmap.height()) / targetRect.height();
    QPointF offset = widgetPos - targetRect.topLeft();

    return QPointF(offset.x() * scaleX, offset.y() * scaleY);
}
  • 输入:widgetPos —— 一个点,位于窗口/控件(QWidget)的坐标系中,比如鼠标点击的位置。
  • 输出:QPointF —— 对应到原始 QPixmap(未经缩放、绘制的图像)的坐标。

这样做的意义是,如果你把一张图缩放、居中绘制到窗口上,用户点击了窗口的某个位置,你能知道他点到的其实是图像上的哪个像素点。

QRect targetRect = GetPixmapDrawRect();
  • 获取绘制 QPixmap 时实际在窗口中的矩形区域。
  • 因为图片可能被缩放、居中显示,所以它在窗口中所占的位置不一定等于窗口大小。
double scaleX = double(m_pixmap.width()) / targetRect.width();
double scaleY = double(m_pixmap.height()) / targetRect.height();

计算缩放比例:

  • 原图宽度 ÷ 绘制宽度 = X 方向的缩放比例。
  • 原图高度 ÷ 绘制高度 = Y 方向的缩放比例。

举例:原图是 1920×1080,绘制时缩放成 960×540,那么 scaleX = 2.0,scaleY = 2.0。

QPointF offset = widgetPos - targetRect.topLeft();
  • 计算点相对于绘制区域左上角的偏移量。
  • 因为 targetRect 可能居中,所以不能直接拿 widgetPos,当图像偏移后必须减掉偏移。
return QPointF(offset.x() * scaleX, offset.y() * scaleY);

把偏移量乘以缩放比例,得到在原图坐标系中的点。

例如:鼠标点在绘制区域的 (100, 50) 像素处,scaleX = 2.0, scaleY = 2.0,那么在原图中的位置就是 (200, 100)。
如下:是窗口大小、pixmap 绘制区域、坐标转换流程(widgetPos → offset → 原图坐标)的示意图
在这里插入图片描述
代码细节解析:

QPointF MainWindow::ToWidgetCoord(const QPointF& pixmapPos) const 
{
    QRect targetRect = GetPixmapDrawRect();  

    double scaleX = double(m_pixmap.width()) / targetRect.width();  
    double scaleY = double(m_pixmap.height()) / targetRect.height();  

    return QPointF(
        targetRect.left() + pixmapPos.x() / scaleX,  
        targetRect.top()  + pixmapPos.y() / scaleY
    );  
}

背景场景:
在 paintEvent 里,你会用 drawPixmap(targetRect, m_pixmap) 把一张 原始大小的图像(m_pixmap) 缩放后绘制到窗口的某个区域(targetRect)。
这就导致一个问题:

  • pixmap 原始坐标系(图像像素坐标,范围 0~m_pixmap.width(), 0~m_pixmap.height())
  • widget 坐标系(绘制在窗口上的坐标,范围是 targetRect 的区域)

如果你要在图像上绘制标记(比如十字、点、框),就必须把图像坐标(pixmapPos)转换到 widget 上的绘制坐标。这个函数就是干这个的。

QRect targetRect = GetPixmapDrawRect();

targetRect 是图像绘制在窗口上的矩形区域。

double scaleX = double(m_pixmap.width()) / targetRect.width();
double scaleY = double(m_pixmap.height()) / targetRect.height();

计算 缩放比例。scaleX 表示:窗口绘制区域的 1 像素对应多少个原始图像像素。比如原图是 2000px,显示区域是 1000px,那么 scaleX = 2000 / 1000 = 2.0。

return QPointF(
    targetRect.left() + pixmapPos.x() / scaleX,
    targetRect.top()  + pixmapPos.y() / scaleY
);
  • pixmapPos.x() / scaleX:把图像坐标缩小到窗口绘制区域的比例。
  • targetRect.left() + …:加上绘制区域的偏移(因为 targetRect 可能不是窗口的 (0,0))。

最终得到 widget 坐标系下的点。

使用举例:
原图大小:2000x1000;targetRect(显示区域):(100, 50, 1000, 500);用户想标记图像上 (400, 200) 的点。

scaleX = 2000 / 1000 = 2.0
scaleY = 1000 / 500  = 2.0
widgetX = 100 + 400 / 2.0 = 300
widgetY = 50  + 200 / 2.0 = 150

结果:原图的 (400,200) 转换成窗口上的 (300,150),就可以直接在 paintEvent 用 drawEllipse(QPointF(300,150), 5,5) 画出来。

典型用途:

  • 在 缩放/居中绘制的图片 上叠加标记(点、框、线)。
  • 让用户点击窗口坐标,反推到图像坐标(需要写反函数 ToPixmapCoord)。
  • 用于鼠标交互,比如点选、框选图像的某个区域。

网站公告

今日签到

点亮在社区的每一天
去签到