feat:更新ImageView,支持多个图片显示
This commit is contained in:
parent
6626ee33e5
commit
57fe539c64
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user