> 技术文档 > Qt自定义图像显示控件(支持平移、缩放、横纵比自适应)

Qt自定义图像显示控件(支持平移、缩放、横纵比自适应)

 

         实现了基于Qt的图像查看器组件,主要包含两个核心类:ImageViewerWidget和ToolPanZoom。ImageViewerWidget作为图像显示控件,支持灰度图像和彩色图像显示,提供拖放文件、图像缩放和平移等功能。它通过转换矩阵数据为QImage实现可视化,并使用jet色彩映射增强显示效果。ToolPanZoom类实现了图像平移和缩放功能,通过鼠标事件处理实现交互式操作,并确保图像始终在可见区域内。关键技术点包括:1) 矩阵数据到QImage的转换算法;2) 基于变换矩阵的图像缩放和平移实现;3) 鼠标事件处理与坐标转换;4) 图像边界检查和限制逻辑。该组件可作为专业的图像处理工具的基础模块。

ImageViewerWidget.cpp

#include \"ImageViewerWidget.h\"#include #include #include#include #include#include #include #include #include\"Tools/ToolPanZoom.h\"#includeImageViewerWidget::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::infinity();image_Max = -std::numeric_limits::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 = 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, QVectorNANVEC){if (mat1 == nullptr)//一定要先判断是否为空,不为空再判断矩阵中是否有值return;QVector 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::infinity();image_Max = -std::numeric_limits::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  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 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_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(event));break;case QEvent::MouseMove:m_pToolManager->MouseMove(static_cast(event));break;case QEvent::MouseButtonRelease:m_pToolManager->MouseUp(static_cast(event));break;case QEvent::Wheel:if (toolType != EToolType::PanZoom && m_pToolManager)m_pToolManager->SetCurrentTool(EToolType::PanZoom);m_pToolManager->MouseWheel(static_cast(event));if (toolType != EToolType::PanZoom && m_pToolManager)m_pToolManager->SetCurrentTool(toolType);break;}}this->onPaintEvent(static_cast(event));}switch (event->type()){case QEvent::DragEnter: // 拖入时{isDropingFile = true;QDragEnterEvent* dragEvent = static_cast(event);if (dragEvent->mimeData()->hasUrls()) // 检查是否有文件{dragEvent->acceptProposedAction(); // 接受拖放return true; // 事件已处理}break;}case QEvent::Drop: // 放下时{QDropEvent* dropEvent = static_cast(event);const QMimeData* mimeData = dropEvent->mimeData();if (mimeData->hasUrls()) // 检查是否有文件{QList 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());emit updateImageData(image_data, image.width(), image.height(), 1.0);resetImageWidget();}catch (const std::exception& e) {qCritical() << \"Exception caught:\" <acceptProposedAction(); // 接受拖放isDropingFile = false;return true; // 事件已处理}break;}default:break;}this->onPaintEvent(static_cast(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(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\"< 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 #include #include #include \"ToolManager.h\"#include #include#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(m_pHookWidget); if (pWidget) { m_pImageViewerWidget = pWidget; } else return 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() <pos() - m_lastDragPos;m_lastDragPos = event->pos();// 获取当前变换后的图像边界QRectF imageRect = m_pImageViewerWidget->getImageRect();qDebug() << \"max\" << imageRect.x() << \" \" <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() zoomFactor();}if (translatedRect.top() > widgetRect.top()) {dy = widgetRect.top() - imageRect.top();dy /= m_pImageViewerWidget->zoomFactor();}if (translatedRect.bottom() zoomFactor();}// 应用限制后的平移m_pImageViewerWidget->m_transform.translate(dx, dy); m_pImageViewerWidget->updateShow();qDebug() <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  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.top()) {dy = widgetRect.top() - transformedRect.top();}if (transformedRect.bottom() m_transform.translate(dx / finalZoom, dy / finalZoom);}m_pImageViewerWidget->updateShow();}