feat:更新ImageView,支持多个图片显示
This commit is contained in:
parent
6626ee33e5
commit
57fe539c64
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
#include <QGraphicsView>
|
#include <QGraphicsView>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QPair>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
#include <Qt>
|
#include <Qt>
|
||||||
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
@ -12,14 +15,23 @@ class QGraphicsPixmapItem;
|
|||||||
class QGraphicsScene;
|
class QGraphicsScene;
|
||||||
class QMouseEvent;
|
class QMouseEvent;
|
||||||
class QPinchGesture;
|
class QPinchGesture;
|
||||||
|
class QTabBar;
|
||||||
class QWheelEvent;
|
class QWheelEvent;
|
||||||
|
|
||||||
// 可缩放、可平移的图片显示控件。
|
// 可缩放、可平移的图片显示控件。
|
||||||
|
//
|
||||||
|
// 支持两种显示模式:
|
||||||
|
// 1) 单图模式:setImage()/loadImage() 直接显示一张未命名图片,标题栏不显示;
|
||||||
|
// 2) 多图模式:setImages()/setNamedImage() 传入 [名字, 图片] 列表,
|
||||||
|
// 控件顶部用标签页显示名字,下方显示当前选中的图片。
|
||||||
class ImageView : public QGraphicsView
|
class ImageView : public QGraphicsView
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// 一组“名字 + 图片”,对应多图模式下的一个标签页。
|
||||||
|
using NamedImage = QPair<QString, QImage>;
|
||||||
|
|
||||||
explicit ImageView(QWidget* parent = nullptr);
|
explicit ImageView(QWidget* parent = nullptr);
|
||||||
~ImageView() override;
|
~ImageView() override;
|
||||||
|
|
||||||
@ -28,6 +40,21 @@ public:
|
|||||||
bool loadImage(const QString& filePath);
|
bool loadImage(const QString& filePath);
|
||||||
void clearImage();
|
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;
|
bool hasImage() const;
|
||||||
QImage image() const;
|
QImage image() const;
|
||||||
|
|
||||||
@ -49,6 +76,8 @@ public:
|
|||||||
signals:
|
signals:
|
||||||
void imageChanged();
|
void imageChanged();
|
||||||
void zoomRatioChanged(double ratio);
|
void zoomRatioChanged(double ratio);
|
||||||
|
// 多图模式下当前标签页发生变化。
|
||||||
|
void currentImageIndexChanged(int index);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool viewportEvent(QEvent* event) override;
|
bool viewportEvent(QEvent* event) override;
|
||||||
@ -67,9 +96,23 @@ private:
|
|||||||
void updateZoomRatioAt(double ratio, const QPoint& viewportAnchor);
|
void updateZoomRatioAt(double ratio, const QPoint& viewportAnchor);
|
||||||
void repositionZoomLabel();
|
void repositionZoomLabel();
|
||||||
|
|
||||||
|
// 显示单张图片;空图片等价于清空当前画面。
|
||||||
|
void updatePixmap(const QImage& image);
|
||||||
|
// 清空画面,保留多图列表。
|
||||||
|
void resetScene();
|
||||||
|
// 按 m_images 刷新标签页标题与可见性,并调整视口边距。
|
||||||
|
void refreshTabBar();
|
||||||
|
// 切换到指定标签页并显示对应图片。
|
||||||
|
void showImageAt(int index);
|
||||||
|
|
||||||
QGraphicsScene* m_scene;
|
QGraphicsScene* m_scene;
|
||||||
QGraphicsPixmapItem* m_pixmapItem;
|
QGraphicsPixmapItem* m_pixmapItem;
|
||||||
QLabel* m_zoomLabel;
|
QLabel* m_zoomLabel;
|
||||||
|
QTabBar* m_tabBar;
|
||||||
|
QVector<NamedImage> m_images;
|
||||||
|
int m_currentImageIndex;
|
||||||
|
int m_tabBarHeight;
|
||||||
|
bool m_multiImageMode;
|
||||||
QImage m_image;
|
QImage m_image;
|
||||||
double m_zoomRatio;
|
double m_zoomRatio;
|
||||||
double m_minimumZoomRatio;
|
double m_minimumZoomRatio;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "ImageView.h"
|
#include "ImageView.h"
|
||||||
|
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
|
#include <QFont>
|
||||||
#include <QGestureEvent>
|
#include <QGestureEvent>
|
||||||
#include <QGraphicsPixmapItem>
|
#include <QGraphicsPixmapItem>
|
||||||
#include <QGraphicsScene>
|
#include <QGraphicsScene>
|
||||||
@ -8,6 +9,7 @@
|
|||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPinchGesture>
|
#include <QPinchGesture>
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
|
#include <QTabBar>
|
||||||
#include <QtWidgets/QScrollBar>
|
#include <QtWidgets/QScrollBar>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QWheelEvent>
|
#include <QWheelEvent>
|
||||||
@ -19,6 +21,29 @@ constexpr double kDefaultMinimumZoom = 0.02;
|
|||||||
constexpr double kDefaultMaximumZoom = 20.0;
|
constexpr double kDefaultMaximumZoom = 20.0;
|
||||||
constexpr double kZoomStep = 1.25;
|
constexpr double kZoomStep = 1.25;
|
||||||
const QColor kDefaultBackgroundColor(37, 38, 42);
|
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)
|
ImageView::ImageView(QWidget* parent)
|
||||||
@ -26,6 +51,10 @@ ImageView::ImageView(QWidget* parent)
|
|||||||
m_scene(new QGraphicsScene(this)),
|
m_scene(new QGraphicsScene(this)),
|
||||||
m_pixmapItem(nullptr),
|
m_pixmapItem(nullptr),
|
||||||
m_zoomLabel(new QLabel(this)),
|
m_zoomLabel(new QLabel(this)),
|
||||||
|
m_tabBar(new EqualWidthTabBar(this)),
|
||||||
|
m_currentImageIndex(-1),
|
||||||
|
m_tabBarHeight(0),
|
||||||
|
m_multiImageMode(false),
|
||||||
m_zoomRatio(1.0),
|
m_zoomRatio(1.0),
|
||||||
m_minimumZoomRatio(kDefaultMinimumZoom),
|
m_minimumZoomRatio(kDefaultMinimumZoom),
|
||||||
m_maximumZoomRatio(kDefaultMaximumZoom),
|
m_maximumZoomRatio(kDefaultMaximumZoom),
|
||||||
@ -57,6 +86,33 @@ ImageView::ImageView(QWidget* parent)
|
|||||||
m_zoomLabel->setStyleSheet(QStringLiteral(
|
m_zoomLabel->setStyleSheet(QStringLiteral(
|
||||||
"QLabel { color: white; background: transparent; border: none; }"));
|
"QLabel { color: white; background: transparent; border: none; }"));
|
||||||
updateZoomLabel();
|
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;
|
ImageView::~ImageView() = default;
|
||||||
@ -68,19 +124,11 @@ void ImageView::setImage(const QImage& image)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_image = image;
|
m_multiImageMode = false;
|
||||||
if (!m_pixmapItem) {
|
m_images.clear();
|
||||||
m_pixmapItem = m_scene->addPixmap(QPixmap::fromImage(m_image));
|
m_currentImageIndex = -1;
|
||||||
} else {
|
refreshTabBar();
|
||||||
m_pixmapItem->setPixmap(QPixmap::fromImage(m_image));
|
updatePixmap(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::setImage(const QPixmap& pixmap)
|
void ImageView::setImage(const QPixmap& pixmap)
|
||||||
@ -100,15 +148,116 @@ bool ImageView::loadImage(const QString& filePath)
|
|||||||
|
|
||||||
void ImageView::clearImage()
|
void ImageView::clearImage()
|
||||||
{
|
{
|
||||||
m_image = QImage();
|
m_multiImageMode = false;
|
||||||
m_scene->clear();
|
m_images.clear();
|
||||||
m_pixmapItem = nullptr;
|
m_currentImageIndex = -1;
|
||||||
m_scene->setSceneRect(QRectF());
|
refreshTabBar();
|
||||||
m_zoomRatio = 1.0;
|
resetScene();
|
||||||
m_needsInitialFit = false;
|
}
|
||||||
updateZoomLabel();
|
|
||||||
viewport()->update();
|
void ImageView::setImages(const QVector<NamedImage>& images)
|
||||||
emit imageChanged();
|
{
|
||||||
|
// 图片集合刷新后尽量停留在同一名字的标签页上。
|
||||||
|
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
|
bool ImageView::hasImage() const
|
||||||
@ -208,6 +357,99 @@ Qt::Alignment ImageView::imageAlignment() const
|
|||||||
return m_imageAlignment;
|
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)
|
bool ImageView::viewportEvent(QEvent* event)
|
||||||
{
|
{
|
||||||
if (event->type() == QEvent::Gesture)
|
if (event->type() == QEvent::Gesture)
|
||||||
@ -270,6 +512,10 @@ void ImageView::handlePinchGesture(QPinchGesture* gesture)
|
|||||||
void ImageView::resizeEvent(QResizeEvent* event)
|
void ImageView::resizeEvent(QResizeEvent* event)
|
||||||
{
|
{
|
||||||
QGraphicsView::resizeEvent(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) {
|
if (m_needsInitialFit && hasImage() && viewport()->width() > 0 && viewport()->height() > 0) {
|
||||||
m_needsInitialFit = false;
|
m_needsInitialFit = false;
|
||||||
resetView();
|
resetView();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user