Qt自定义图像显示控件(支持平移、缩放、横纵比自适应)
实现了基于Qt的图像查看器组件,主要包含两个核心类:ImageViewerWidget和ToolPanZoom。ImageViewerWidget作为图像显示控件,支持灰度图像和彩色图像显示,提供拖放文件、图像缩放和平移等功能。它通过转换矩阵数据为QImage实现可视化,并使用jet色彩映射增强显示效果。ToolPanZoom类实现了图像平移和缩放功能,通过鼠标事件处理实现交互式操作,并确保图像始终在可见区域内。关键技术点包括:1) 矩阵数据到QImage的转换算法;2) 基于变换矩阵的图像缩放和平移实现;3) 鼠标事件处理与坐标转换;4) 图像边界检查和限制逻辑。该组件可作为专业的图像处理工具的基础模块。
ImageViewerWidget.cpp
#include "ImageViewerWidget.h"
#include <QPainter>
#include <QWheelEvent>
#include<QDebug>
#include <QMouseEvent>
#include<QHboxLayout>
#include <QtMath>
#include <QDragEnterEvent>
#include <QMimeData>
#include"Tools/ToolPanZoom.h"
#include<Tools/ToolManager.h>ImageViewerWidget::ImageViewerWidget(QWidget *parent): QWidget(parent),m_scale(1.0)
{ setMouseTracking(true);//m_imageWidget2->setFixedSize(m_imageWidget->width(), m_imageWidget->height());setAcceptDrops(true);installEventFilter(this);//m_pToolManager = new ToolManager(this);jetcolorinit(0, 0);m_imageWidget = new QWidget(this);m_imageWidget->installEventFilter(this);
}void ImageViewerWidget::setToolManager(ToolManager* pToolManager)
{m_pToolManager = pToolManager;m_pToolManager->AddTool(new ToolPanZoom(), this);m_pToolManager->SetCurrentTool(EToolType::PanZoom);
}ImageViewerWidget::~ImageViewerWidget()
{if (image_data){delete[] image_data;image_data = nullptr;}delete m_imageWidget;
}void ImageViewerWidget::setImage(const QImage& pixmap)
{m_pixmap = pixmap;resetZoom();
}void ImageViewerWidget::setGray8Image(const QImage& pixmap)
{format = ImageDataFormat::IMAGE_8UC1;M_scale = 1.0;//数据image_Min = std::numeric_limits<float>::infinity();image_Max = -std::numeric_limits<float>::infinity();M_width = pixmap.width();//根据行进行设置M_height = pixmap.height();//根据列进行设置imageRatio = (qreal)M_width / M_height;m_pixmap = pixmap;resetZoom();update();
}void ImageViewerWidget::jetcolorinit(float max, float min)
{int s;float component;if (max == 0 && min == 0)component = 1;elsecomponent = abs(max - min);//QColor(255, 101, 101)for (s = 0; s < max * 256; s++){jet[s][0] = 255;jet[s][1] = 101;jet[s][2] = 101;}qDebug() << "max" << max << "min" << min << "component" << component << "max*255" << max * 255;for (s = 0; s < 32; s++) {jet[(int)(s * component + max * 256)][0] = 128 + 4 * s;jet[(int)s][1] = 0;jet[(int)s][2] = 0;}jet[(int)(32 * component + max * 256)][0] = 255;jet[(int)(32 * component + max * 256)][1] = 0;jet[(int)(32 * component + max * 256)][2] = 0;for (s = 0; s < 63; s++) {jet[(int)((33 + s) * component + max * 256)][0] = 255;jet[(int)((33 + s) * component + max * 256)][1] = 4 + 4 * s;jet[(int)((33 + s) * component + max * 256)][2] = 0;}jet[(int)(96 * component + max * 256)][0] = 254;jet[(int)(96 * component + max * 256)][1] = 255;jet[(int)(96 * component + max * 256)][2] = 2;for (s = 0; s < 62; s++) {jet[(int)((97 + s) * component + max * 256)][0] = 250 - 4 * s;jet[(int)((97 + s) * component + max * 256)][1] = 255;jet[(int)((97 + s) * component + max * 256)][2] = 6 + 4 * s;}jet[(int)(159 * component + max * 256)][0] = 1;jet[(int)(159 * component + max * 256)][1] = 255;jet[(int)(159 * component + max * 256)][2] = 254;for (s = 0; s < 64; s++) {jet[(int)((160 + s) * component + max * 256)][0] = 0;jet[(int)((160 + s) * component + max * 256)][1] = 252 - (s * 4);jet[(int)((160 + s) * component + max * 256)][2] = 255;}for (s = 0; s < 32; s++) {jet[(int)((224 + s) * component + max * 256)][0] = 0;jet[(int)((224 + s) * component + max * 256)][1] = 0;jet[(int)((224 + s) * component + max * 256)][2] = 252 - 4 * s;}for (int s = (255 * component + max * 256); s < 256; s++){jet[s][0] = 0;jet[s][1] = 0;jet[s][2] = 128;}
}void ImageViewerWidget::convertMatToQImage(double** mat, int m_width, int m_height, float min, float max)
{qDebug() << "m_width" << m_width << "m_height" << m_height << "min" << min << max;m_pixmap = QImage(m_width, m_height, QImage::Format_RGB32);double vlie = (double)256 / (abs(max - min));m_pixmap.fill(255);// Set the color table (used to translate colour indexes to qRgb values)// Copy input MatM_rangefor (int i = 0; i < m_height; i++){for (int j = 0; j < m_width; j++){if (std::isnan(mat[i][j])){m_pixmap.setPixelColor(j, i, QColor(255, 255, 255));}else{int num = abs((mat[i][j] - min) * vlie);if (num >= 255)num = 255;m_pixmap.setPixelColor(j, i, QColor(jet[255 - num][0],jet[255 - num][1], jet[255 - num][2]));}}}// Set the color table (used to translate colour indexes to qRgb values)
}void ImageViewerWidget::setimagedata(double** mat1, int width, int height, float scale, QVector<QPoint>NANVEC)
{if (mat1 == nullptr)//一定要先判断是否为空,不为空再判断矩阵中是否有值return;QVector<double> local_vec;//for (int i = 0; i < NANVEC.size(); i++){local_vec.push_back(mat1[NANVEC[i].x()][NANVEC[i].y()]);mat1[NANVEC[i].x()][NANVEC[i].y()] = NAN;}M_scale = scale;image_data = mat1;NAN_Vec = NANVEC;//数据image_Min = std::numeric_limits<float>::infinity();image_Max = -std::numeric_limits<float>::infinity();M_width = width;//根据行进行设置M_height = height;//根据列进行设置imageRatio = (qreal)M_width / M_height;resetZoom();//if (ratio_image > ratio_widget) {// XY_unit_scale = (float)m_imageWidget->width() / M_width;//}//else {// XY_unit_scale = (float)m_imageWidget->height() / M_height;//}////int i, j;float z;for (i = 1; i <= M_height; i++){int index = 0;for (j = 1; j <= M_width; j++){z = image_data[i - 1][j - 1] * Z_unit_scale;if (!std::isnan(z)){if (z > image_Max)image_Max = z;if (z < image_Min)image_Min = z;}}}convertMatToQImage(image_data, M_width, M_height, image_Min / Z_unit_scale, image_Max / Z_unit_scale);for (int i = 0; i < NANVEC.size(); i++){mat1[NANVEC[i].x()][NANVEC[i].y()] = local_vec.at(i);}//emit Send_Jet_Range(image_Min, image_Max, graph3D->axisY()->labelFormat());update();
}QImage ImageViewerWidget::image() const
{return m_pixmap;
}QRectF ImageViewerWidget::getImageRect() const
{auto pt1 = imageToWindow(QPointF(0, 0));auto pt2 = imageToWindow(QPointF(M_width-1, M_height-1));QRectF result;result.setX(pt1.x());result.setY(pt1.y());result.setWidth(pt2.x() - pt1.x() + 1);result.setHeight(pt2.y() - pt1.y() + 1);return result;
}QRectF ImageViewerWidget::getImageWidgetRect() const
{return m_imageWidget->rect();
}int ImageViewerWidget::getImageWidth() const
{return M_width;
}int ImageViewerWidget::getImageHeight() const
{return M_height;
}bool ImageViewerWidget::isimageValid() const
{if (M_width == 0 || M_height == 0)return false;return true;
}void ImageViewerWidget::resetZoom()
{if (isimageValid()) {m_transform.reset();m_transform.scale(m_scale, m_scale);m_transform_init = m_transform;}
}double ImageViewerWidget::zoomFactor() const
{return m_scale * m_zoomFactor;
}void ImageViewerWidget::onPaintEvent(QPaintEvent *event)
{Q_UNUSED(event);QPainter painter(m_imageWidget);painter.setRenderHint(QPainter::SmoothPixmapTransform);painter.fillRect(rect(), QColor(240, 240, 240));if (!m_pixmap.isNull()){if (format == ImageDataFormat::IMAGE_8UC1){if (m_bHighlightSaturation){QImage tmp = m_pixmap.convertToFormat(QImage::Format_ARGB32);for (int y = 0; y < M_height; ++y) {const uchar* line = m_pixmap.constScanLine(y); // 每行首地址[^3^]for (int x = 0; x < M_width; ++x) {if (line[x] >= m_uSaturationThreshold){tmp.setPixelColor(QPoint(x, y), Qt::red);}}}painter.setWorldTransform(m_transform);QRectF source(0.0, 0.0, M_width, M_height);painter.drawImage(0, 0, tmp, Qt::ColorOnly);}else{painter.setWorldTransform(m_transform);QRectF source(0.0, 0.0, M_width, M_height);painter.drawImage(0, 0, m_pixmap, Qt::ColorOnly);}}else if (format == ImageDataFormat::IMAGE_64FC1){painter.setWorldTransform(m_transform);QRectF source(0.0, 0.0, M_width, M_height);painter.drawImage(0, 0, m_pixmap, Qt::ColorOnly);} }else {QImage imag = QImage(100, 100, QImage::Format_RGB32);imag.fill(QColor(255, 255, 255));QPainter mypainter(this);mypainter.setRenderHint(QPainter::Antialiasing);QPen pen;pen.setColor(QColor(0, 0, 120));pen.setWidth(2);mypainter.setPen(pen);QRectF target(0, 0, this->width(), this->height());QRectF source(0.0, 0.0, 100, 100);mypainter.drawImage(target, imag, source, Qt::ColorOnly);return QWidget::paintEvent(event);}//QTransform tmpTrans;//tmpTrans.reset();//painter.setWorldTransform(tmpTrans);//painter.setPen(QPen(Qt::red, 2, Qt::DashLine)); // 红色虚线,宽度2px//painter.drawRect(rect().adjusted(1, 1, -1, -1)); // 向内缩进1px避免溢出return QWidget::paintEvent(event);
}void ImageViewerWidget::mousePressEvent(QMouseEvent* event)
{if (!m_pixmap.isNull()){if (event->button() == Qt::RightButton){//// 右键点击获取像素坐标//QPoint pixelPos = windowToImage(event->pos());//if (m_pixmap.rect().contains(pixelPos)) {// emit pixelSelected(pixelPos);//}}}
}void ImageViewerWidget::mouseMoveEvent(QMouseEvent* event)
{
}void ImageViewerWidget::mouseReleaseEvent(QMouseEvent *event)
{
}void ImageViewerWidget::wheelEvent(QWheelEvent *event)
{
}QPoint ImageViewerWidget::windowToImage(const QPoint& windowPos) const
{QTransform invTransform = m_transform.inverted();return invTransform.map(windowPos);
}QPointF ImageViewerWidget::windowToImage(const QPointF &windowPos) const
{QTransform invTransform = m_transform.inverted();return invTransform.map(windowPos);
}QPointF ImageViewerWidget::imageToWindow(const QPointF& imagePos) const
{return m_transform.map(imagePos);
}QPoint ImageViewerWidget::imageToWindow(const QPoint& imagePos) const
{return m_transform.map(imagePos);
}void ImageViewerWidget::updateShow()
{update();
}void ImageViewerWidget::setImageCursor(const QCursor& cursor)
{setCursor(cursor);
}void ImageViewerWidget::resizeEvent(QResizeEvent* event)
{Q_UNUSED(event);if (!m_pixmap.isNull()) {resetZoom();}
}bool ImageViewerWidget::eventFilter(QObject* watched, QEvent* event)
{auto toolType = EToolType::DefaultNull; if (m_pToolManager && m_pToolManager->CurrentTool()){toolType = m_pToolManager->CurrentTool()->GetType();}if (!m_pixmap.isNull()){if (watched == m_imageWidget){if (m_pToolManager){switch (event->type()){case QEvent::MouseButtonPress:m_pToolManager->MouseDown(static_cast<QMouseEvent*>(event));break;case QEvent::MouseMove:m_pToolManager->MouseMove(static_cast<QMouseEvent*>(event));break;case QEvent::MouseButtonRelease:m_pToolManager->MouseUp(static_cast<QMouseEvent*>(event));break;case QEvent::Wheel:if (toolType != EToolType::PanZoom && m_pToolManager)m_pToolManager->SetCurrentTool(EToolType::PanZoom);m_pToolManager->MouseWheel(static_cast<QWheelEvent*>(event));if (toolType != EToolType::PanZoom && m_pToolManager)m_pToolManager->SetCurrentTool(toolType);break;}}this->onPaintEvent(static_cast<QPaintEvent*>(event));}switch (event->type()){case QEvent::DragEnter: // 拖入时{isDropingFile = true;QDragEnterEvent* dragEvent = static_cast<QDragEnterEvent*>(event);if (dragEvent->mimeData()->hasUrls()) // 检查是否有文件{dragEvent->acceptProposedAction(); // 接受拖放return true; // 事件已处理}break;}case QEvent::Drop: // 放下时{QDropEvent* dropEvent = static_cast<QDropEvent*>(event);const QMimeData* mimeData = dropEvent->mimeData();if (mimeData->hasUrls()) // 检查是否有文件{QList<QUrl> urlList = mimeData->urls();for (const QUrl& url : urlList){QString filePath = url.toLocalFile(); // 获取文件路径qDebug() << "Dropped file:" << filePath;//emit fileDropped(filePath); // 发射信号QPixmap pixmap;try {pixmap.load(filePath);QImage image = pixmap.toImage().convertToFormat(QImage::Format_Grayscale8);setGray8Image(image);//convertToImageData(image);//setimagedata(image_data, image.width(), image.height(), 1.0, QVector<QPoint>());emit updateImageData(image_data, image.width(), image.height(), 1.0);resetImageWidget();}catch (const std::exception& e) {qCritical() << "Exception caught:" << e.what();}}dropEvent->acceptProposedAction(); // 接受拖放isDropingFile = false;return true; // 事件已处理}break;}default:break;}this->onPaintEvent(static_cast<QPaintEvent*>(event));}return QWidget::eventFilter(watched, event);
}bool ImageViewerWidget::isInImageWidget(QPoint pt)
{return m_imageWidget->rect().contains(pt);
}void ImageViewerWidget::convertToImageData(QImage& image)
{const int img_width = image.width();const int img_height = image.height();const int totalPixels = img_width * img_height;if (image_data){delete[] image_data;image_data = nullptr;}image_data = new double* [img_height];// 遍历像素并转换for (int y = 0; y < img_height; ++y) {image_data[y] = new double[img_width];const uchar* scanLine = image.constScanLine(y); // 获取一行像素数据for (int x = 0; x < img_width; ++x) {double pixelValue = static_cast<double>(scanLine[x]);image_data[y][x] = pixelValue;}}
}void ImageViewerWidget::Set_Range_Max(float max, float Min)
{if (image_data == nullptr)return;jetcolorinit(max, Min);convertMatToQImage(image_data, M_width, M_height, image_Min, image_Max);//max = abs(max - Min);//qDebug()<<"Max"<<max<<"Min"<<Min;QLinearGradient gr;gr.setColorAt(0.0, QColor(0, 0, 255));gr.setColorAt(1 - Min, QColor(0, 0, 255));gr.setColorAt(1 - Min + max / 4, QColor(0, 255, 255));gr.setColorAt(1 - Min + (max / 4) * 2, QColor(0, 255, 0));gr.setColorAt(1 - Min + (max / 4) * 3, QColor(255, 255, 0));gr.setColorAt(1 - Min + max, QColor(255, 0, 0));gr.setColorAt(1 - Min + max + 0.01, QColor(255, 101, 101, 255));gr.setColorAt(1.0, QColor(255, 101, 101, 255));emit UpdateSurfaceColorMap(gr);
}void ImageViewerWidget::resetImageWidget()
{if (isimageValid()){canvasRatio = (qreal)width() / height();if (imageRatio > canvasRatio) {// 以宽度为基准,图像宽占满canvasqreal height = std::round(width() / imageRatio);targetRect = QRectF(0, (this->height() - height) / 2, width(), height);m_scale = (double)width() / getImageWidth();}else {// 以高度为基准,图像高占满canvasqreal width = std::round(height() * imageRatio);targetRect = QRectF((this->width() - width) / 2, 0, width, height());m_scale = (double)height() / getImageHeight();}m_imageWidget->setGeometry(targetRect.x(),targetRect.y(),targetRect.width(), targetRect.height());resetZoom();update();}
}
ToolPanZoom.cpp
#include "ToolPanZoom.h"
#include <QMouseEvent>
#include <QWheelEvent>
#include <QMessageBox>
#include "ToolManager.h"
#include <QtMath>
#include<QDebug>
#include "ImageViewerWidget.h"ToolPanZoom::ToolPanZoom(ToolManager* pCmdMgr/* = nullptr*/): ITool(pCmdMgr),m_isDragging(false)
{type = EToolType::DrawMask;m_zoomTimer.start();
}bool ToolPanZoom::IsEnable()
{if (nullptr == m_pImageViewerWidget){ImageViewerWidget* pWidget = qobject_cast<ImageViewerWidget*>(m_pHookWidget);if (pWidget){m_pImageViewerWidget = pWidget;}elsereturn false;}return true;
}void ToolPanZoom::MouseDown(QMouseEvent* event)
{if (IsEnable() && m_pImageViewerWidget->isInImageWidget(event->pos())){if (event->button() == Qt::LeftButton) {m_lastDragPos = event->pos();m_isDragging = true;m_pImageViewerWidget->setImageCursor(Qt::ClosedHandCursor);}}
}void ToolPanZoom::MouseMove(QMouseEvent* event)
{if (IsEnable() && m_pImageViewerWidget->isInImageWidget(event->pos())){if (m_pImageViewerWidget->isViewLocked()){return;}if (m_isDragging) {qDebug() << "mouse move in";// 计算移动距离QPoint delta = event->pos() - m_lastDragPos;m_lastDragPos = event->pos();// 获取当前变换后的图像边界QRectF imageRect = m_pImageViewerWidget->getImageRect();qDebug() << "max" << imageRect.x() << " " << imageRect.y();QRectF widgetRect = m_pImageViewerWidget->getImageWidgetRect();// 计算平移后的位置qreal dx = delta.x() / m_pImageViewerWidget->zoomFactor();qreal dy = delta.y() / m_pImageViewerWidget->zoomFactor();// 检查平移后是否会显示图像内部空白QRectF translatedRect = imageRect.translated(dx, dy);// 限制平移范围if (translatedRect.left() > widgetRect.left()) {dx = widgetRect.left() - imageRect.left();dx /= m_pImageViewerWidget->zoomFactor();}if (translatedRect.right() < widgetRect.right()) {dx = widgetRect.right() - imageRect.right();dx /= m_pImageViewerWidget->zoomFactor();}if (translatedRect.top() > widgetRect.top()) {dy = widgetRect.top() - imageRect.top();dy /= m_pImageViewerWidget->zoomFactor();}if (translatedRect.bottom() < widgetRect.bottom()) {dy = widgetRect.bottom() - imageRect.bottom();dy /= m_pImageViewerWidget->zoomFactor();}// 应用限制后的平移m_pImageViewerWidget->m_transform.translate(dx, dy);m_pImageViewerWidget->updateShow();qDebug() << "mouse move out";}}
}void ToolPanZoom::MouseUp(QMouseEvent* event)
{if (event->button() == Qt::LeftButton && m_isDragging) {m_isDragging = false;m_pImageViewerWidget->setImageCursor(Qt::ArrowCursor);}
}void ToolPanZoom::MouseWheel(QWheelEvent* event)
{if (IsEnable() && m_pImageViewerWidget->isInImageWidget(event->pos())){if (m_zoomTimer.elapsed() > 30) {m_zoomTimer.restart();// 获取鼠标位置作为缩放中心QPointF mousePos = event->position();// 计算缩放因子double angle = event->angleDelta().y();double factor = qPow(1.0015, angle);scaleImage(factor, mousePos);}}
}void ToolPanZoom::KeyDown(QKeyEvent* event)
{/*switch (event->key()){case Qt::Key_Space:break;default:break;}*/
}void ToolPanZoom::scaleImage(double factor, const QPointF& center)
{// 限制缩放范围double newZoomFactor = m_pImageViewerWidget->m_zoomFactor * factor;if (newZoomFactor < 1.0)newZoomFactor = 1.0;if (newZoomFactor > 100) {return;}m_pImageViewerWidget->m_zoomFactor = newZoomFactor;double finalZoom = m_pImageViewerWidget->zoomFactor();// 获取当前图像和窗口的矩形QRectF widgetRect = m_pImageViewerWidget->getImageWidgetRect();if (!center.isNull()) {// 计算当前鼠标位置的图片坐标QPointF imagePos = m_pImageViewerWidget->windowToImage(center.toPoint());// 重置变换矩阵并应用新缩放m_pImageViewerWidget->m_transform.reset();m_pImageViewerWidget->m_transform.scale(m_pImageViewerWidget->zoomFactor(), m_pImageViewerWidget->zoomFactor());// 计算缩放后鼠标应该对应的窗口坐标QPointF targetWindowPos = center;// 计算当前鼠标实际会对应的窗口坐标QPointF actualWindowPos = m_pImageViewerWidget->m_transform.map(imagePos);// 计算需要调整的平移量QPointF delta = targetWindowPos - actualWindowPos;if(newZoomFactor > 1.0)m_pImageViewerWidget->m_transform.translate(delta.x() / finalZoom, delta.y() / finalZoom);// 检查缩放后图像边界是否在widget内部//QRectF transformedRect = m_pImageViewerWidget->m_transform.mapRect(imageRect);QRectF transformedRect = m_pImageViewerWidget->getImageRect();qreal dx = 0, dy = 0;if (transformedRect.left() > widgetRect.left()) {dx = widgetRect.left() - transformedRect.left();}if (transformedRect.right() < widgetRect.right()) {dx = widgetRect.right() - transformedRect.right();}if (transformedRect.top() > widgetRect.top()) {dy = widgetRect.top() - transformedRect.top();}if (transformedRect.bottom() < widgetRect.bottom()) {dy = widgetRect.bottom() - transformedRect.bottom();}m_pImageViewerWidget->m_transform.translate(dx / finalZoom, dy / finalZoom);}m_pImageViewerWidget->updateShow();
}