一、实例演示
头文件:
#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)。
- 用于鼠标交互,比如点选、框选图像的某个区域。