【Qt实战】实现图片缩放、平移与像素级查看功能
在Qt开发中,自定义QLabel实现图片的交互操作(缩放、平移、像素查看)是非常常见的需求,比如图片编辑器、医疗影像查看等场景。本文将详细讲解如何基于QLabel封装一个支持鼠标滚轮缩放、鼠标拖拽平移、像素RGB值显示的自定义控件,并优化代码结构与交互体验。
文章目录
- 【Qt实战】实现图片缩放、平移与像素级查看功能
-
- 一、效果展示
- 二、项目结构
- 三、核心代码实现
-
- [3.1 程序入口(main.cpp)](#3.1 程序入口(main.cpp))
- [3.2 主窗口头文件(QtTest.h)](#3.2 主窗口头文件(QtTest.h))
- [3.3 主窗口源文件(QtTest.cpp)](#3.3 主窗口源文件(QtTest.cpp))
- [3.4 自定义Label头文件(myLabel.h)](#3.4 自定义Label头文件(myLabel.h))
- [3.5 自定义Label源文件(myLabel.cpp)](#3.5 自定义Label源文件(myLabel.cpp))
- 四、优化点说明
-
- [4.1 代码结构优化](#4.1 代码结构优化)
- [4.2 交互体验优化](#4.2 交互体验优化)
- [4.3 性能优化](#4.3 性能优化)
- 五、核心原理讲解
-
- [5.1 坐标系统转换](#5.1 坐标系统转换)
- [5.2 缩放与平移的实现](#5.2 缩放与平移的实现)
- [5.3 像素级查看的实现](#5.3 像素级查看的实现)
- 六、扩展功能建议
- 七、总结
一、效果展示

功能说明:
- 点击"打开"按钮选择本地图片(png/bmp/jpeg等格式)
- 鼠标滚轮实现图片缩放,缩放时以鼠标位置为中心
- 鼠标左键拖拽实现图片平移
- 缩放至32倍以上显示像素网格,64倍以上显示像素RGB值
- 鼠标右键释放恢复初始状态
- 实时显示缩放比例与鼠标位置对应的像素坐标
二、项目结构
├── QtTest.h // 主窗口头文件
├── QtTest.cpp // 主窗口源文件
├── myLabel.h // 自定义Label头文件
├── myLabel.cpp // 自定义Label源文件
├── ui_QtTest.h // Qt设计师生成的界面文件(自动生成)
└── main.cpp // 程序入口
三、核心代码实现
3.1 程序入口(main.cpp)
cpp
#include "QtTest.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QtTest w;
w.show();
return a.exec();
}
3.2 主窗口头文件(QtTest.h)
cpp
#pragma once
#include <QtWidgets/QMainWindow>
#include "ui_QtTest.h"
#include "myLabel.h"
class QtTest : public QMainWindow
{
Q_OBJECT
public:
QtTest(QWidget *parent = nullptr);
~QtTest() override; // 建议使用override关键字
public slots:
void onaction_open(); // 打开图片槽函数
void onaction_exit(); // 退出程序槽函数
void onaction_UpdataZoom(float value); // 更新缩放比例显示
void onaction_UpdataPos(QPointF pos); // 更新像素坐标显示
private:
Ui::QtTestClass ui;
myLabel* m_label = nullptr; // 初始化指针,避免野指针
};
3.3 主窗口源文件(QtTest.cpp)
cpp
#include "QtTest.h"
#include <QFileDialog>
#include <QMessageBox> // 增加错误提示
QtTest::QtTest(QWidget* parent)
: QMainWindow(parent)
{
ui.setupUi(this);
this->setWindowTitle("图片像素查看器"); // 设置窗口标题
// 信号槽连接(建议使用新的lambda语法,更安全)
connect(ui.pushButtonExit, &QPushButton::clicked, this, &QtTest::onaction_exit);
connect(ui.pushButtonOpen, &QPushButton::clicked, this, &QtTest::onaction_open);
// 获取自定义Label控件
m_label = ui.labelFigure;
m_label->setContentsMargins(0, 0, 0, 0);
m_label->setMouseTracking(true); // 开启鼠标追踪(可选,用于实时显示坐标)
// 连接自定义信号
connect(m_label, &myLabel::signal_updataZoom, this, &QtTest::onaction_UpdataZoom);
connect(m_label, &myLabel::signal_updataPos, this, &QtTest::onaction_UpdataPos);
}
QtTest::~QtTest() = default; // 使用默认析构函数,更简洁
void QtTest::onaction_open()
{
// 打开文件对话框
QString strFile = QFileDialog::getOpenFileName(
this,
tr("打开图片文件"),
QDir::homePath(), // 默认路径为用户主目录,更友好
tr("Image Files (*.png *.bmp *.jpeg *.jpg);;All Files (*.*)"));
if (strFile.isEmpty()) // 判断是否选择文件
return;
QImage m_src(strFile);
if (m_src.isNull()) // 判断图片是否加载成功
{
QMessageBox::warning(this, tr("警告"), tr("图片加载失败,请选择有效图片!"));
return;
}
m_label->setImage(m_src);
}
void QtTest::onaction_exit()
{
this->close();
}
void QtTest::onaction_UpdataZoom(float value)
{
// 保留两位小数,显示更美观
ui.lineEditZoom->setText(QString::number(value, 'f', 2));
}
void QtTest::onaction_UpdataPos(QPointF pos)
{
// 像素坐标取整,显示更合理
ui.lineEditPosX->setText(QString::number(qRound(pos.x())));
ui.lineEditPosY->setText(QString::number(qRound(pos.y())));
}
3.4 自定义Label头文件(myLabel.h)
cpp
#pragma once // 防止重复包含
#include <QLabel>
#include <QImage>
#include <QMouseEvent>
#include <QWheelEvent>
// 自定义Label,支持图片缩放、平移、像素查看
class myLabel : public QLabel
{
Q_OBJECT // 必须添加,支持信号槽
public:
explicit myLabel(QWidget* parent = nullptr);
~myLabel() override = default;
// 设置要显示的图片
void setImage(QImage src);
// 放大/缩小图片(对外接口)
void onZoomInImage();
void onZoomOutImage();
signals:
// 缩放比例更新信号
void signal_updataZoom(float value);
// 像素坐标更新信号
void signal_updataPos(QPointF pos);
protected:
// 重写事件处理函数
void mousePressEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
void paintEvent(QPaintEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
private:
// 初始化参数
void Init();
// 坐标转换函数(核心)
QPointF convertMouse2LogicalDraw(const QPointF& ptInMouse);
QPointF convertLogicalDraw2Mouse(const QPointF& ptInDraw);
QPointF convertLogicalDraw2Pixcel(const QPointF& ptInDraw);
QPointF convertPixcel2LogicalDraw(const QPointF& ptInPixcel);
private:
// 图片相关
QImage m_ImageQ; // 原始图片
QRectF m_windowsPos; // 图片在绘制坐标系中的位置
// 缩放相关
float m_ZoomValue = 1.0f; // 当前缩放比例
float m_ZoomDafault = 1.0f;// 默认缩放比例(适配窗口)
float m_ScaleX = 1.0f; // X方向适配比例
float m_ScaleY = 1.0f; // Y方向适配比例
bool m_isFirstWheel = true;// 滚轮首次触发标记
// 平移相关
int m_XPtInterval = 0; // X方向平移量
int m_YPtInterval = 0; // Y方向平移量
QPoint m_OldPos; // 鼠标旧位置
QPointF m_drawPoint; // 绘制点
bool m_Pressed = false; // 鼠标是否按下
// 鼠标相关
QPoint m_mousePos; // 当前鼠标位置
// 像素网格相关
QVector<float> m_xInDraw; // X方向网格点
QVector<float> m_yInDraw; // Y方向网格点
};
3.5 自定义Label源文件(myLabel.cpp)
cpp
#include "myLabel.h"
#include <QPainter>
#include <QCursor>
#include <QMath>
myLabel::myLabel(QWidget* parent) : QLabel(parent)
{
Init();
// 设置背景透明(可选,根据需求)
this->setAttribute(Qt::WA_TranslucentBackground, false);
// 设置鼠标追踪(实时获取鼠标位置)
this->setMouseTracking(true);
}
void myLabel::Init()
{
// 重置所有参数,避免残留
m_XPtInterval = 0;
m_YPtInterval = 0;
m_ZoomValue = 1.0f;
m_Pressed = false;
m_ScaleX = 1.0f;
m_ScaleY = 1.0f;
m_isFirstWheel = true;
m_drawPoint = QPointF(0, 0);
m_xInDraw.clear();
m_yInDraw.clear();
m_ImageQ = QImage();
m_windowsPos = QRectF();
}
void myLabel::setImage(QImage src)
{
Init();
m_ImageQ = src;
update(); // 触发重绘
}
void myLabel::mousePressEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton) // 仅处理左键按下
{
m_OldPos = event->pos();
m_Pressed = true;
this->setCursor(Qt::ClosedHandCursor); // 按下时显示闭合手型,更直观
}
m_mousePos = event->pos();
// 发送当前鼠标位置对应的像素坐标
emit signal_updataPos(convertLogicalDraw2Pixcel(convertMouse2LogicalDraw(m_mousePos)));
QLabel::mousePressEvent(event); // 调用父类事件,保证事件传递
}
void myLabel::mouseReleaseEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton)
{
m_Pressed = false;
this->unsetCursor(); // 恢复默认光标
}
// 右键重置
if (event->button() == Qt::RightButton)
{
Init();
update();
}
QLabel::mouseReleaseEvent(event);
}
void myLabel::mouseMoveEvent(QMouseEvent* event)
{
if (m_Pressed && event->buttons() & Qt::LeftButton) // 仅处理左键拖拽
{
QPoint pos = event->pos();
// 计算平移增量
int xPtInterval = pos.x() - m_OldPos.x();
int yPtInterval = pos.y() - m_OldPos.y();
// 更新总平移量
m_XPtInterval += xPtInterval;
m_YPtInterval += yPtInterval;
m_drawPoint = QPointF(m_drawPoint.x() + xPtInterval, m_drawPoint.y() + yPtInterval);
// 更新旧位置
m_OldPos = pos;
update(); // 触发重绘
}
// 实时更新鼠标位置对应的像素坐标
m_mousePos = event->pos();
emit signal_updataPos(convertLogicalDraw2Pixcel(convertMouse2LogicalDraw(m_mousePos)));
QLabel::mouseMoveEvent(event);
}
void myLabel::mouseDoubleClickEvent(QMouseEvent* event)
{
// 双击可以添加自定义逻辑,比如重置缩放/居中图片
Init();
update();
QLabel::mouseDoubleClickEvent(event);
}
void myLabel::paintEvent(QPaintEvent* event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing); // 抗锯齿,绘制更平滑
painter.setRenderHint(QPainter::SmoothPixmapTransform); // 图片缩放平滑
// 无图片时,绘制父类默认样式
if (m_ImageQ.isNull())
{
QLabel::paintEvent(event);
return;
}
// ========== 1. 计算图片适配窗口的尺寸 ==========
int widgetW = this->width();
int widgetH = this->height();
int imgW = m_ImageQ.width();
int imgH = m_ImageQ.height();
// 保持图片宽高比,适配窗口
float scale = qMin((float)widgetW / imgW, (float)widgetH / imgH);
float showW = imgW * scale;
float showH = imgH * scale;
m_ScaleX = showW / imgW;
m_ScaleY = showH / imgH;
m_ZoomDafault = scale;
// ========== 2. 平移和缩放变换 ==========
// 平移到窗口中心,加上用户平移量
painter.translate(widgetW / 2 + m_XPtInterval, widgetH / 2 + m_YPtInterval);
// 缩放(基于适配后的比例)
painter.scale(m_ZoomValue / m_ScaleX, m_ZoomValue / m_ScaleY);
// ========== 3. 绘制图片 ==========
m_windowsPos = QRectF(-showW / 2.0, -showH / 2.0, showW, showH);
painter.drawImage(m_windowsPos, m_ImageQ);
// ========== 4. 缩放至32倍以上,绘制像素网格和RGB值 ==========
if (m_ZoomValue > 32.0f)
{
// 绘制像素网格线
QVector<QLineF> lines;
m_xInDraw.clear();
m_yInDraw.clear();
// 遍历图片像素,生成网格线(仅绘制可见区域)
for (int y = 0; y < imgH; y++)
{
QPointF ptDraw = convertPixcel2LogicalDraw(QPointF(0, y));
QPointF ptMouse = convertLogicalDraw2Mouse(ptDraw);
if (ptMouse.y() < 0 || ptMouse.y() > widgetH)
continue;
lines.append(QLineF(-showW / 2.0, ptDraw.y(), showW / 2.0, ptDraw.y()));
m_yInDraw.append(ptDraw.y());
}
for (int x = 0; x < imgW; x++)
{
QPointF ptDraw = convertPixcel2LogicalDraw(QPointF(x, 0));
QPointF ptMouse = convertLogicalDraw2Mouse(ptDraw);
if (ptMouse.x() < 0 || ptMouse.x() > widgetW)
continue;
lines.append(QLineF(ptDraw.x(), -showH / 2.0, ptDraw.x(), showH / 2.0));
m_xInDraw.append(ptDraw.x());
}
// 设置网格线颜色和宽度
painter.setPen(QPen(Qt::red, 0.5));
painter.drawLines(lines);
// 缩放至64倍以上,绘制像素RGB值
if (m_ZoomValue >= 64.0f)
{
QVector<QPointF> ptPlotB; // 黑色背景(文字白色)
QVector<QPointF> ptPlotW; // 白色背景(文字黑色)
QVector<QString> msgB;
QVector<QString> msgW;
// 遍历可见像素,计算RGB值
for (int i = 0; i < m_yInDraw.size(); i++)
{
for (int j = 0; j < m_xInDraw.size(); j++)
{
QPointF ptPix = convertLogicalDraw2Pixcel(QPointF(m_xInDraw[j], m_yInDraw[i]));
int x = qBound(0, qRound(ptPix.x()), imgW - 1); // 防止越界
int y = qBound(0, qRound(ptPix.y()), imgH - 1);
// 获取像素RGB值
QColor color = m_ImageQ.pixel(x, y);
int r = color.red();
int g = color.green();
int b = color.blue();
int grey = qRound(0.299 * r + 0.587 * g + 0.114 * b); // 灰度值
QString msg = QString("%1\n%2\n%3").arg(r).arg(g).arg(b);
QPointF ptPlot = QPointF(m_xInDraw[j], m_yInDraw[i]);
// 根据灰度值选择文字颜色
if (grey < 128)
{
ptPlotB.append(ptPlot);
msgB.append(msg);
}
else
{
ptPlotW.append(ptPlot);
msgW.append(msg);
}
}
}
// 恢复缩放,绘制文字(避免文字被缩放)
painter.save(); // 保存当前绘图状态
painter.scale(m_ScaleX / m_ZoomValue, m_ScaleY / m_ZoomValue);
QTextOption textOption(Qt::AlignHCenter | Qt::AlignVCenter);
textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
// 绘制白色文字(黑色背景)
painter.setPen(Qt::white);
for (int i = 0; i < ptPlotB.size(); i++)
{
QRectF textRect(ptPlotB[i].x() * m_ZoomValue / m_ScaleX,
ptPlotB[i].y() * m_ZoomValue / m_ScaleY,
m_ZoomValue / m_ScaleX,
m_ZoomValue / m_ScaleY);
painter.drawText(textRect, msgB[i], textOption);
}
// 绘制黑色文字(白色背景)
painter.setPen(Qt::black);
for (int i = 0; i < ptPlotW.size(); i++)
{
QRectF textRect(ptPlotW[i].x() * m_ZoomValue / m_ScaleX,
ptPlotW[i].y() * m_ZoomValue / m_ScaleY,
m_ZoomValue / m_ScaleX,
m_ZoomValue / m_ScaleY);
painter.drawText(textRect, msgW[i], textOption);
}
painter.restore(); // 恢复绘图状态
}
}
}
void myLabel::wheelEvent(QWheelEvent* event)
{
if (m_ImageQ.isNull())
{
QLabel::wheelEvent(event);
return;
}
// 获取滚轮增量(向上为正,向下为负)
int delta = event->angleDelta().y();
m_mousePos = event->pos();
// 计算鼠标位置对应的像素坐标
QPointF pixPos = convertLogicalDraw2Pixcel(convertMouse2LogicalDraw(m_mousePos));
// 检查是否在图片范围内
if (pixPos.x() < 0 || pixPos.x() >= m_ImageQ.width() ||
pixPos.y() < 0 || pixPos.y() >= m_ImageQ.height())
{
QLabel::wheelEvent(event);
return;
}
// 缩放限制:最大500倍,最小为默认适配比例
const float maxZoom = 500.0f;
if (delta > 0 && m_ZoomValue >= maxZoom)
{
QLabel::wheelEvent(event);
return;
}
if (delta < 0 && m_ZoomValue <= m_ZoomDafault)
{
// 恢复初始状态
Init();
update();
emit signal_updataZoom(m_ZoomValue);
QLabel::wheelEvent(event);
return;
}
// 计算新的缩放比例
float scaleFactor = delta > 0 ? 1.2f : 0.8f;
m_ZoomValue *= scaleFactor;
m_ZoomValue = qBound(m_ZoomDafault, m_ZoomValue, maxZoom); // 限制范围
emit signal_updataZoom(m_ZoomValue);
// 缩放时以鼠标位置为中心(核心逻辑)
QPointF ptZoom = convertLogicalDraw2Mouse(convertPixcel2LogicalDraw(pixPos));
m_XPtInterval -= (ptZoom.x() - m_mousePos.x());
m_YPtInterval -= (ptZoom.y() - m_mousePos.y());
update();
QLabel::wheelEvent(event);
}
void myLabel::onZoomInImage()
{
const float maxZoom = 500.0f;
if (m_ZoomValue >= maxZoom)
return;
m_ZoomValue *= 1.2f;
m_ZoomValue = qMin(m_ZoomValue, maxZoom);
emit signal_updataZoom(m_ZoomValue);
update();
}
void myLabel::onZoomOutImage()
{
m_ZoomValue *= 0.8f;
if (m_ZoomValue <= m_ZoomDafault)
{
Init();
emit signal_updataZoom(m_ZoomValue);
return;
}
emit signal_updataZoom(m_ZoomValue);
update();
}
// ========== 坐标转换函数(核心,需仔细理解) ==========
QPointF myLabel::convertMouse2LogicalDraw(const QPointF& ptInMouse)
{
float lx = (ptInMouse.x() - this->width() / 2.0f - m_XPtInterval) / (m_ZoomValue / m_ScaleX);
float ly = (ptInMouse.y() - this->height() / 2.0f - m_YPtInterval) / (m_ZoomValue / m_ScaleY);
return QPointF(lx, ly);
}
QPointF myLabel::convertLogicalDraw2Mouse(const QPointF& ptInDraw)
{
float lx = ptInDraw.x() * (m_ZoomValue / m_ScaleX) + m_XPtInterval + this->width() / 2.0f;
float ly = ptInDraw.y() * (m_ZoomValue / m_ScaleY) + m_YPtInterval + this->height() / 2.0f;
return QPointF(lx, ly);
}
QPointF myLabel::convertLogicalDraw2Pixcel(const QPointF& ptInDraw)
{
float px = (ptInDraw.x() - m_windowsPos.x()) / m_ScaleX;
float py = (ptInDraw.y() - m_windowsPos.y()) / m_ScaleY;
return QPointF(px, py);
}
QPointF myLabel::convertPixcel2LogicalDraw(const QPointF& ptInPixcel)
{
float px = ptInPixcel.x() * m_ScaleX + m_windowsPos.x();
float py = ptInPixcel.y() * m_ScaleY + m_windowsPos.y();
return QPointF(px, py);
}
四、优化点说明
4.1 代码结构优化
- 修复原代码错误:原自定义Label头文件重复粘贴了QtTest.cpp的代码,现已修正并重新组织myLabel.h的结构。
- 使用现代C++特性 :如
override关键字、默认析构函数、QString::number格式化输出等。 - 模块化拆分:将坐标转换、初始化、事件处理等逻辑拆分为独立函数,提高代码可读性和维护性。
- 添加注释:关键逻辑处添加详细注释,便于理解核心原理。
4.2 交互体验优化
- 鼠标光标优化 :鼠标按下时显示
ClosedHandCursor,拖拽时更直观;释放时恢复默认光标。 - 缩放中心优化:滚轮缩放时以鼠标位置为中心,符合用户操作习惯(原代码逻辑有瑕疵,现已修正)。
- 参数限制优化:添加缩放比例的上下限(最大500倍,最小为适配窗口比例),避免过度缩放。
- 错误处理优化:添加图片加载失败的提示框,判断文件是否为空,提升程序健壮性。
- 显示优化:像素坐标取整显示,缩放比例保留两位小数,RGB值绘制时添加抗锯齿,显示更美观。
4.3 性能优化
- 仅绘制可见区域:绘制像素网格时,只处理可见范围内的像素,减少不必要的计算。
- 绘图状态保存/恢复 :绘制RGB文字时使用
painter.save()和painter.restore(),避免影响其他绘制逻辑。 - 使用QVector存储数据:替代原始数组,提高内存管理效率。
五、核心原理讲解
5.1 坐标系统转换
整个功能的核心是四个坐标转换函数,涉及三个坐标系统:
- 鼠标坐标:以Label控件的左上角为原点,向右为X轴,向下为Y轴。
- 绘制坐标:以Label控件的中心为原点,经过平移和缩放后的坐标系统。
- 像素坐标:以图片的左上角为原点,对应图片的实际像素位置。
通过这四个转换函数,实现了鼠标位置与图片像素位置的一一对应,是缩放和平移的基础。
5.2 缩放与平移的实现
- 平移:鼠标左键拖拽时,计算鼠标移动的增量,累加到平移量中,并重绘图片。
- 缩放:滚轮事件中,根据滚轮增量调整缩放比例,同时调整平移量使缩放以鼠标位置为中心。
5.3 像素级查看的实现
当缩放比例超过32倍时,遍历图片的像素,绘制像素网格线;当缩放比例超过64倍时,计算每个像素的RGB值,并根据灰度值选择文字颜色,绘制在对应像素位置。
六、扩展功能建议
- 添加快捷键 :如
+键放大、-键缩小、ESC键重置。 - 支持图片旋转:添加旋转功能,处理不同方向的图片。
- 保存当前视图:将当前缩放和平移后的图片保存为新文件。
- 添加像素值复制:点击像素时复制RGB值到剪贴板。
- 优化大图片加载 :使用
QImageReader渐进式加载大图片,避免程序卡顿。
七、总结
本文通过自定义QLabel控件,实现了图片的缩放、平移和像素级查看功能,并对原代码进行了全面的优化,包括代码结构、交互体验和性能等方面。核心是理解Qt的坐标系统和绘图机制,以及事件处理的流程。该控件可直接集成到Qt项目中,适用于图片查看、像素分析等场景。