feat:更新ImageView,支持多个图片显示

This commit is contained in:
YangSn 2026-10-21 14:02:32 +08:00
parent 6626ee33e5
commit 57fe539c64
2 changed files with 311 additions and 22 deletions

View File

@ -2,7 +2,10 @@
#include <QGraphicsView>
#include <QImage>
#include <QPair>
#include <QPixmap>
#include <QString>
#include <QVector>
#include <Qt>
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<QString, QImage>;
explicit ImageView(QWidget* parent = nullptr);
~ImageView() override;
@ -28,6 +40,21 @@ public:
bool loadImage(const QString& filePath);
void clearImage();
// 多图模式:整体替换为指定的一组 [名字, 图片]。
// 若原有当前页的名字仍在新列表中,则继续保持该页;否则显示第一张有图的页。
void setImages(const QVector<NamedImage>& 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<NamedImage> m_images;
int m_currentImageIndex;
int m_tabBarHeight;
bool m_multiImageMode;
QImage m_image;
double m_zoomRatio;
double m_minimumZoomRatio;

View File

@ -1,6 +1,7 @@
#include "ImageView.h"
#include <QEvent>
#include <QFont>
#include <QGestureEvent>
#include <QGraphicsPixmapItem>
#include <QGraphicsScene>
@ -8,6 +9,7 @@
#include <QMouseEvent>
#include <QPinchGesture>
#include <QResizeEvent>
#include <QTabBar>
#include <QtWidgets/QScrollBar>
#include <QStyle>
#include <QWheelEvent>
@ -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<NamedImage>& 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<NamedImage> 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();