【Qt实战】实现图片缩放、平移与像素级查看功能

【Qt实战】实现图片缩放、平移与像素级查看功能

在Qt开发中,自定义QLabel实现图片的交互操作(缩放、平移、像素查看)是非常常见的需求,比如图片编辑器、医疗影像查看等场景。本文将详细讲解如何基于QLabel封装一个支持鼠标滚轮缩放、鼠标拖拽平移、像素RGB值显示的自定义控件,并优化代码结构与交互体验。

文章目录

一、效果展示

功能说明:

  1. 点击"打开"按钮选择本地图片(png/bmp/jpeg等格式)
  2. 鼠标滚轮实现图片缩放,缩放时以鼠标位置为中心
  3. 鼠标左键拖拽实现图片平移
  4. 缩放至32倍以上显示像素网格,64倍以上显示像素RGB值
  5. 鼠标右键释放恢复初始状态
  6. 实时显示缩放比例与鼠标位置对应的像素坐标

二、项目结构

复制代码
├── 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 代码结构优化

  1. 修复原代码错误:原自定义Label头文件重复粘贴了QtTest.cpp的代码,现已修正并重新组织myLabel.h的结构。
  2. 使用现代C++特性 :如override关键字、默认析构函数、QString::number格式化输出等。
  3. 模块化拆分:将坐标转换、初始化、事件处理等逻辑拆分为独立函数,提高代码可读性和维护性。
  4. 添加注释:关键逻辑处添加详细注释,便于理解核心原理。

4.2 交互体验优化

  1. 鼠标光标优化 :鼠标按下时显示ClosedHandCursor,拖拽时更直观;释放时恢复默认光标。
  2. 缩放中心优化:滚轮缩放时以鼠标位置为中心,符合用户操作习惯(原代码逻辑有瑕疵,现已修正)。
  3. 参数限制优化:添加缩放比例的上下限(最大500倍,最小为适配窗口比例),避免过度缩放。
  4. 错误处理优化:添加图片加载失败的提示框,判断文件是否为空,提升程序健壮性。
  5. 显示优化:像素坐标取整显示,缩放比例保留两位小数,RGB值绘制时添加抗锯齿,显示更美观。

4.3 性能优化

  1. 仅绘制可见区域:绘制像素网格时,只处理可见范围内的像素,减少不必要的计算。
  2. 绘图状态保存/恢复 :绘制RGB文字时使用painter.save()painter.restore(),避免影响其他绘制逻辑。
  3. 使用QVector存储数据:替代原始数组,提高内存管理效率。

五、核心原理讲解

5.1 坐标系统转换

整个功能的核心是四个坐标转换函数,涉及三个坐标系统:

  1. 鼠标坐标:以Label控件的左上角为原点,向右为X轴,向下为Y轴。
  2. 绘制坐标:以Label控件的中心为原点,经过平移和缩放后的坐标系统。
  3. 像素坐标:以图片的左上角为原点,对应图片的实际像素位置。

通过这四个转换函数,实现了鼠标位置与图片像素位置的一一对应,是缩放和平移的基础。

5.2 缩放与平移的实现

  1. 平移:鼠标左键拖拽时,计算鼠标移动的增量,累加到平移量中,并重绘图片。
  2. 缩放:滚轮事件中,根据滚轮增量调整缩放比例,同时调整平移量使缩放以鼠标位置为中心。

5.3 像素级查看的实现

当缩放比例超过32倍时,遍历图片的像素,绘制像素网格线;当缩放比例超过64倍时,计算每个像素的RGB值,并根据灰度值选择文字颜色,绘制在对应像素位置。

六、扩展功能建议

  1. 添加快捷键 :如+键放大、-键缩小、ESC键重置。
  2. 支持图片旋转:添加旋转功能,处理不同方向的图片。
  3. 保存当前视图:将当前缩放和平移后的图片保存为新文件。
  4. 添加像素值复制:点击像素时复制RGB值到剪贴板。
  5. 优化大图片加载 :使用QImageReader渐进式加载大图片,避免程序卡顿。

七、总结

本文通过自定义QLabel控件,实现了图片的缩放、平移和像素级查看功能,并对原代码进行了全面的优化,包括代码结构、交互体验和性能等方面。核心是理解Qt的坐标系统和绘图机制,以及事件处理的流程。该控件可直接集成到Qt项目中,适用于图片查看、像素分析等场景。

相关推荐
我命由我123452 小时前
Python Flask 开发问题:ImportError: cannot import name ‘Markup‘ from ‘flask‘
开发语言·后端·python·学习·flask·学习方法·python3.11
wjs20242 小时前
Go 语言指针
开发语言
wuguan_2 小时前
C#:多态函数重载、态符号重载、抽象、虚方法
开发语言·c#
小信啊啊2 小时前
Go语言数组与切片的区别
开发语言·后端·golang
计算机学姐2 小时前
基于php的摄影网站系统
开发语言·vue.js·后端·mysql·php·phpstorm
全栈陈序员2 小时前
【Python】基础语法入门(二十)——项目实战:从零构建命令行 To-Do List 应用
开发语言·人工智能·python·学习
我不是程序猿儿2 小时前
【C#】ScottPlot的Refresh()
开发语言·c#
Neolnfra2 小时前
渗透测试标准化流程
开发语言·安全·web安全·http·网络安全·https·系统安全