#include "ImageView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr double kDefaultMinimumZoom = 0.02; constexpr double kDefaultMaximumZoom = 20.0; constexpr double kZoomStep = 1.25; const QColor kDefaultBackgroundColor(37, 38, 42); // 标签栏:把可用宽度平均分给每个标签页,使标签页铺满控件宽度。 // QTabBar::expanding 只按各标签的 sizeHint 比例分配,无法保证等宽,故在此重写。 class EqualWidthTabBar : public QTabBar { public: explicit EqualWidthTabBar(QWidget* parent = nullptr) : QTabBar(parent) { setExpanding(false); } protected: QSize tabSizeHint(int index) const override { QSize hint = QTabBar::tabSizeHint(index); const int tabCount = count(); if (tabCount > 0 && width() > 0) { hint.setWidth(width() / tabCount); } return hint; } }; } ImageView::ImageView(QWidget* parent) : QGraphicsView(parent), m_scene(new QGraphicsScene(this)), m_pixmapItem(nullptr), m_zoomLabel(new QLabel(this)), m_tabBar(new EqualWidthTabBar(this)), m_currentImageIndex(-1), m_tabBarHeight(0), m_multiImageMode(false), m_zoomRatio(1.0), m_minimumZoomRatio(kDefaultMinimumZoom), m_maximumZoomRatio(kDefaultMaximumZoom), m_imageAlignment(Qt::AlignCenter), m_panning(false), m_pinching(false), m_needsInitialFit(false), m_pinchStartZoomRatio(1.0) { setScene(m_scene); setBackgroundBrush(kDefaultBackgroundColor); setFrameShape(QFrame::NoFrame); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); setTransformationAnchor(QGraphicsView::AnchorUnderMouse); setResizeAnchor(QGraphicsView::AnchorViewCenter); setAlignment(m_imageAlignment); setDragMode(QGraphicsView::NoDrag); setCursor(Qt::OpenHandCursor); // 手势由 QGraphicsView 的视口接收,支持触摸屏双指捏合缩放。 viewport()->setAttribute(Qt::WA_AcceptTouchEvents, true); viewport()->grabGesture(Qt::PinchGesture); m_zoomLabel->setObjectName(QStringLiteral("imageViewZoomLabel")); m_zoomLabel->setAlignment(Qt::AlignCenter); m_zoomLabel->setMinimumWidth(58); m_zoomLabel->setAttribute(Qt::WA_TransparentForMouseEvents); m_zoomLabel->setStyleSheet(QStringLiteral( "QLabel { color: white; background: transparent; border: none; }")); updateZoomLabel(); // 标签栏固定贴在控件顶部,多图模式下才显示。 m_tabBar->setObjectName(QStringLiteral("imageViewTabBar")); m_tabBar->setDrawBase(false); m_tabBar->setExpanding(false); m_tabBar->setMovable(false); m_tabBar->setUsesScrollButtons(false); m_tabBar->setFocusPolicy(Qt::NoFocus); m_tabBar->setStyleSheet(QStringLiteral( "QTabBar::tab { background: #25262A; color: #EFF1F5; padding: 6px 18px;" " border: 1px solid #4A4C52; border-bottom: none; }" "QTabBar::tab:selected { background: #3A3C42; }" "QTabBar::tab:hover { background: #31333A; }")); // 标签字体在控件默认字体基础上放大一倍。 QFont tabFont = m_tabBar->font(); if (tabFont.pointSizeF() > 0.0) { tabFont.setPointSizeF(tabFont.pointSizeF() * 2.0); } else if (tabFont.pixelSize() > 0) { tabFont.setPixelSize(tabFont.pixelSize() * 2); } m_tabBar->setFont(tabFont); m_tabBar->hide(); connect(m_tabBar, &QTabBar::currentChanged, this, [this](int index) { showImageAt(index); }); } ImageView::~ImageView() = default; void ImageView::setImage(const QImage& image) { if (image.isNull()) { clearImage(); return; } m_multiImageMode = false; m_images.clear(); m_currentImageIndex = -1; refreshTabBar(); updatePixmap(image); } void ImageView::setImage(const QPixmap& pixmap) { setImage(pixmap.toImage()); } bool ImageView::loadImage(const QString& filePath) { QImage loaded; if (!loaded.load(filePath)) { return false; } setImage(loaded); return true; } void ImageView::clearImage() { m_multiImageMode = false; m_images.clear(); m_currentImageIndex = -1; refreshTabBar(); resetScene(); } void ImageView::setImages(const QVector& images) { // 图片集合刷新后尽量停留在同一名字的标签页上。 const QString previousName = (m_currentImageIndex >= 0 && m_currentImageIndex < m_images.size()) ? m_images.at(m_currentImageIndex).first : QString(); m_images = images; m_multiImageMode = true; m_currentImageIndex = -1; if (!m_images.isEmpty()) { int index = 0; for (int i = 0; i < m_images.size(); ++i) { if (!previousName.isEmpty() && m_images.at(i).first == previousName) { index = i; break; } } // 停留在无图页上时改显示第一张有图的页,避免切换后画面空白。 if (m_images.at(index).second.isNull()) { for (int i = 0; i < m_images.size(); ++i) { if (!m_images.at(i).second.isNull()) { index = i; break; } } } m_currentImageIndex = index; } refreshTabBar(); if (m_currentImageIndex >= 0) { showImageAt(m_currentImageIndex); } else { resetScene(); } } void ImageView::setNamedImage(const QString& name, const QImage& image) { QVector images = m_images; bool replaced = false; for (int i = 0; i < images.size(); ++i) { if (images.at(i).first == name) { images[i].second = image; replaced = true; break; } } if (!replaced) { images.append(NamedImage(name, image)); } setImages(images); } void ImageView::clearImages() { clearImage(); } bool ImageView::isMultiImageMode() const { return m_multiImageMode; } int ImageView::imageCount() const { return m_multiImageMode ? m_images.size() : (hasImage() ? 1 : 0); } QString ImageView::imageName(int index) const { if (!m_multiImageMode || index < 0 || index >= m_images.size()) { return QString(); } return m_images.at(index).first; } QImage ImageView::namedImage(const QString& name) const { if (!m_multiImageMode) { return QString() == name ? m_image : QImage(); } for (const NamedImage& image : m_images) { if (image.first == name) { return image.second; } } return QImage(); } int ImageView::currentImageIndex() const { return m_multiImageMode ? m_currentImageIndex : (hasImage() ? 0 : -1); } void ImageView::setCurrentImageIndex(int index) { if (!m_multiImageMode) { return; } showImageAt(index); } bool ImageView::hasImage() const { return !m_image.isNull(); } QImage ImageView::image() const { return m_image; } double ImageView::zoomRatio() const { return m_zoomRatio; } void ImageView::setZoomRatio(double ratio) { updateZoomRatio(ratio, true); } void ImageView::zoomIn(double factor) { if (factor <= 1.0) { factor = kZoomStep; } updateZoomRatio(m_zoomRatio * factor, true); } void ImageView::zoomOut(double factor) { if (factor <= 1.0) { factor = kZoomStep; } updateZoomRatio(m_zoomRatio / factor, true); } void ImageView::resetView() { if (!hasImage() || !m_pixmapItem) { return; } const double previousRatio = m_zoomRatio; resetTransform(); fitInView(m_pixmapItem, Qt::KeepAspectRatio); const double fittedRatio = qBound(m_minimumZoomRatio, transform().m11(), m_maximumZoomRatio); resetTransform(); scale(fittedRatio, fittedRatio); m_zoomRatio = fittedRatio; centerOn(m_pixmapItem); updateZoomLabel(); if (!qFuzzyCompare(previousRatio, m_zoomRatio)) { emit zoomRatioChanged(m_zoomRatio); } } void ImageView::setZoomRange(double minimum, double maximum) { if (minimum <= 0.0 || maximum < minimum) { return; } m_minimumZoomRatio = minimum; m_maximumZoomRatio = maximum; updateZoomRatio(m_zoomRatio, true); } double ImageView::minimumZoomRatio() const { return m_minimumZoomRatio; } double ImageView::maximumZoomRatio() const { return m_maximumZoomRatio; } void ImageView::setImageAlignment(Qt::Alignment alignment) { if (!alignment) { alignment = Qt::AlignCenter; } m_imageAlignment = alignment; setAlignment(m_imageAlignment); if (hasImage()) { viewport()->update(); } } Qt::Alignment ImageView::imageAlignment() const { return m_imageAlignment; } void ImageView::updatePixmap(const QImage& image) { if (image.isNull()) { resetScene(); return; } m_image = image; if (!m_pixmapItem) { m_pixmapItem = m_scene->addPixmap(QPixmap::fromImage(m_image)); } else { m_pixmapItem->setPixmap(QPixmap::fromImage(m_image)); } m_scene->setSceneRect(QRectF(QPointF(0.0, 0.0), QSizeF(m_image.size()))); m_needsInitialFit = true; if (viewport()->width() > 0 && viewport()->height() > 0) { m_needsInitialFit = false; resetView(); } emit imageChanged(); } void ImageView::resetScene() { m_image = QImage(); m_scene->clear(); m_pixmapItem = nullptr; m_scene->setSceneRect(QRectF()); m_zoomRatio = 1.0; m_needsInitialFit = false; updateZoomLabel(); viewport()->update(); emit imageChanged(); } void ImageView::refreshTabBar() { if (!m_tabBar) { return; } m_tabBar->blockSignals(true); while (m_tabBar->count() > m_images.size()) { m_tabBar->removeTab(m_tabBar->count() - 1); } for (int i = 0; i < m_images.size(); ++i) { if (i < m_tabBar->count()) { m_tabBar->setTabText(i, m_images.at(i).first); } else { m_tabBar->addTab(m_images.at(i).first); } } if (m_currentImageIndex >= 0 && m_currentImageIndex < m_tabBar->count()) { m_tabBar->setCurrentIndex(m_currentImageIndex); } // 是否显示标签栏由多图模式和图组内容决定,而不是 isVisible(); // 这样窗口尚未显示时也能正确预留视口边距。 const bool showTabBar = m_multiImageMode && !m_images.isEmpty(); m_tabBar->setVisible(showTabBar); m_tabBar->blockSignals(false); const int topMargin = showTabBar ? m_tabBar->sizeHint().height() : 0; if (topMargin != m_tabBarHeight) { m_tabBarHeight = topMargin; // 视口让出标签栏高度,图片显示在标签栏下方。 setViewportMargins(0, m_tabBarHeight, 0, 0); } if (showTabBar) { m_tabBar->setGeometry(0, 0, width(), m_tabBarHeight); m_tabBar->raise(); } } void ImageView::showImageAt(int index) { if (!m_multiImageMode || index < 0 || index >= m_images.size()) { return; } if (m_tabBar && m_tabBar->currentIndex() != index) { m_tabBar->blockSignals(true); m_tabBar->setCurrentIndex(index); m_tabBar->blockSignals(false); } const bool indexChanged = (m_currentImageIndex != index); m_currentImageIndex = index; updatePixmap(m_images.at(index).second); if (indexChanged) { emit currentImageIndexChanged(index); } } bool ImageView::viewportEvent(QEvent* event) { if (event->type() == QEvent::Gesture) { return handleGestureEvent(static_cast(event)); } return QGraphicsView::viewportEvent(event); } bool ImageView::handleGestureEvent(QGestureEvent* event) { QGesture* gesture = event->gesture(Qt::PinchGesture); if (!gesture) { return false; } QPinchGesture* pinchGesture = static_cast(gesture); handlePinchGesture(pinchGesture); event->accept(pinchGesture); return true; } void ImageView::handlePinchGesture(QPinchGesture* gesture) { if (!gesture) { return; } if (gesture->state() == Qt::GestureStarted) { // 双指手势开始后停止单指模拟产生的鼠标拖拽。 m_pinching = true; m_panning = false; m_pinchStartZoomRatio = m_zoomRatio; setCursor(Qt::OpenHandCursor); } if (hasImage() && (gesture->state() == Qt::GestureStarted || gesture->state() == Qt::GestureUpdated)) { const double scaleFactor = gesture->totalScaleFactor(); if (qIsFinite(scaleFactor) && scaleFactor > 0.0) { // 使用累计缩放比例,避免连续事件带来的比例漂移。 updateZoomRatioAt(m_pinchStartZoomRatio * scaleFactor, gesture->centerPoint().toPoint()); } } if (gesture->state() == Qt::GestureFinished || gesture->state() == Qt::GestureCanceled) { m_pinching = false; setCursor(Qt::OpenHandCursor); } } void ImageView::resizeEvent(QResizeEvent* event) { QGraphicsView::resizeEvent(event); if (m_tabBar && m_tabBarHeight > 0) { m_tabBar->setGeometry(0, 0, width(), m_tabBarHeight); m_tabBar->raise(); } if (m_needsInitialFit && hasImage() && viewport()->width() > 0 && viewport()->height() > 0) { m_needsInitialFit = false; resetView(); } repositionZoomLabel(); } void ImageView::wheelEvent(QWheelEvent* event) { if (!hasImage() || event->angleDelta().y() == 0) { event->ignore(); return; } const double factor = event->angleDelta().y() > 0 ? kZoomStep : 1.0 / kZoomStep; updateZoomRatio(m_zoomRatio * factor, true); event->accept(); } void ImageView::mousePressEvent(QMouseEvent* event) { if (m_pinching) { event->accept(); return; } if (event->button() == Qt::LeftButton && hasImage()) { m_panning = true; m_lastMousePosition = event->pos(); setCursor(Qt::ClosedHandCursor); event->accept(); return; } QGraphicsView::mousePressEvent(event); } void ImageView::mouseMoveEvent(QMouseEvent* event) { if (m_pinching) { event->accept(); return; } if (m_panning) { const QPoint delta = event->pos() - m_lastMousePosition; m_lastMousePosition = event->pos(); horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x()); verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y()); event->accept(); return; } QGraphicsView::mouseMoveEvent(event); } void ImageView::mouseReleaseEvent(QMouseEvent* event) { if (m_pinching) { event->accept(); return; } if (event->button() == Qt::LeftButton && m_panning) { m_panning = false; setCursor(Qt::OpenHandCursor); event->accept(); return; } QGraphicsView::mouseReleaseEvent(event); } void ImageView::mouseDoubleClickEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton && hasImage()) { resetView(); event->accept(); return; } QGraphicsView::mouseDoubleClickEvent(event); } void ImageView::updateZoomLabel() { if (!hasImage()) { m_zoomLabel->setText(QStringLiteral("--")); } else { m_zoomLabel->setText(QStringLiteral("%1%") .arg(m_zoomRatio * 100.0, 0, 'f', 1)); } m_zoomLabel->adjustSize(); repositionZoomLabel(); } void ImageView::updateZoomRatio(double ratio, bool keepCenter) { if (!hasImage()) { return; } ratio = qBound(m_minimumZoomRatio, ratio, m_maximumZoomRatio); if (qFuzzyCompare(ratio, m_zoomRatio) && !transform().isIdentity()) { return; } const QPoint viewportCenter = viewport()->rect().center(); const QPointF sceneCenter = mapToScene(viewportCenter); resetTransform(); scale(ratio, ratio); m_zoomRatio = ratio; if (keepCenter) { centerOn(sceneCenter); } updateZoomLabel(); emit zoomRatioChanged(m_zoomRatio); } void ImageView::updateZoomRatioAt(double ratio, const QPoint& viewportAnchor) { if (!hasImage()) { return; } ratio = qBound(m_minimumZoomRatio, ratio, m_maximumZoomRatio); if (qFuzzyCompare(ratio, m_zoomRatio) && !transform().isIdentity()) { return; } // 缩放前后保持双指中心对应同一个图像位置。 const QPointF sceneAnchor = mapToScene(viewportAnchor); resetTransform(); scale(ratio, ratio); m_zoomRatio = ratio; const QPointF mappedAnchor = mapFromScene(sceneAnchor); const QPointF viewportDelta = mappedAnchor - viewportAnchor; horizontalScrollBar()->setValue( horizontalScrollBar()->value() + qRound(viewportDelta.x())); verticalScrollBar()->setValue( verticalScrollBar()->value() + qRound(viewportDelta.y())); updateZoomLabel(); emit zoomRatioChanged(m_zoomRatio); } void ImageView::repositionZoomLabel() { if (!m_zoomLabel) { return; } const int margin = 8; const int scrollBarHeight = horizontalScrollBar()->isVisible() ? horizontalScrollBar()->height() : 0; m_zoomLabel->move(margin, height() - m_zoomLabel->height() - margin - scrollBarHeight); m_zoomLabel->raise(); }