From 57fe539c64fff7c56afae76a7324a411a2125743 Mon Sep 17 00:00:00 2001 From: YangSn Date: Wed, 21 Oct 2026 14:02:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0ImageView,=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E4=B8=AA=E5=9B=BE=E7=89=87=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ImageView/Inc/ImageView.h | 43 ++++++ ImageView/Src/ImageView.cpp | 290 +++++++++++++++++++++++++++++++++--- 2 files changed, 311 insertions(+), 22 deletions(-) diff --git a/ImageView/Inc/ImageView.h b/ImageView/Inc/ImageView.h index db48093..5a32103 100644 --- a/ImageView/Inc/ImageView.h +++ b/ImageView/Inc/ImageView.h @@ -2,7 +2,10 @@ #include #include +#include #include +#include +#include #include class QLabel; @@ -12,14 +15,23 @@ class QGraphicsPixmapItem; class QGraphicsScene; class QMouseEvent; class QPinchGesture; +class QTabBar; class QWheelEvent; // 可缩放、可平移的图片显示控件。 +// +// 支持两种显示模式: +// 1) 单图模式:setImage()/loadImage() 直接显示一张未命名图片,标题栏不显示; +// 2) 多图模式:setImages()/setNamedImage() 传入 [名字, 图片] 列表, +// 控件顶部用标签页显示名字,下方显示当前选中的图片。 class ImageView : public QGraphicsView { Q_OBJECT public: + // 一组“名字 + 图片”,对应多图模式下的一个标签页。 + using NamedImage = QPair; + explicit ImageView(QWidget* parent = nullptr); ~ImageView() override; @@ -28,6 +40,21 @@ public: bool loadImage(const QString& filePath); void clearImage(); + // 多图模式:整体替换为指定的一组 [名字, 图片]。 + // 若原有当前页的名字仍在新列表中,则继续保持该页;否则显示第一张有图的页。 + void setImages(const QVector& images); + // 多图模式:新增或更新一张命名图片,其余图片与当前页保持不变。 + void setNamedImage(const QString& name, const QImage& image); + // 退出多图模式,清空所有命名图片。 + void clearImages(); + + bool isMultiImageMode() const; + int imageCount() const; + QString imageName(int index) const; + QImage namedImage(const QString& name) const; + int currentImageIndex() const; + void setCurrentImageIndex(int index); + bool hasImage() const; QImage image() const; @@ -49,6 +76,8 @@ public: signals: void imageChanged(); void zoomRatioChanged(double ratio); + // 多图模式下当前标签页发生变化。 + void currentImageIndexChanged(int index); protected: bool viewportEvent(QEvent* event) override; @@ -67,9 +96,23 @@ private: void updateZoomRatioAt(double ratio, const QPoint& viewportAnchor); void repositionZoomLabel(); + // 显示单张图片;空图片等价于清空当前画面。 + void updatePixmap(const QImage& image); + // 清空画面,保留多图列表。 + void resetScene(); + // 按 m_images 刷新标签页标题与可见性,并调整视口边距。 + void refreshTabBar(); + // 切换到指定标签页并显示对应图片。 + void showImageAt(int index); + QGraphicsScene* m_scene; QGraphicsPixmapItem* m_pixmapItem; QLabel* m_zoomLabel; + QTabBar* m_tabBar; + QVector m_images; + int m_currentImageIndex; + int m_tabBarHeight; + bool m_multiImageMode; QImage m_image; double m_zoomRatio; double m_minimumZoomRatio; diff --git a/ImageView/Src/ImageView.cpp b/ImageView/Src/ImageView.cpp index 6ea9568..9e60d9a 100644 --- a/ImageView/Src/ImageView.cpp +++ b/ImageView/Src/ImageView.cpp @@ -1,6 +1,7 @@ #include "ImageView.h" #include +#include #include #include #include @@ -8,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +21,29 @@ 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) @@ -26,6 +51,10 @@ ImageView::ImageView(QWidget* 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), @@ -57,6 +86,33 @@ ImageView::ImageView(QWidget* parent) 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; @@ -68,19 +124,11 @@ void ImageView::setImage(const QImage& image) 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(); + m_multiImageMode = false; + m_images.clear(); + m_currentImageIndex = -1; + refreshTabBar(); + updatePixmap(image); } void ImageView::setImage(const QPixmap& pixmap) @@ -100,15 +148,116 @@ bool ImageView::loadImage(const QString& filePath) void ImageView::clearImage() { - 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(); + 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 @@ -208,6 +357,99 @@ 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) @@ -270,6 +512,10 @@ void ImageView::handlePinchGesture(QPinchGesture* gesture) 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();