Utils/ImageView/Src/ImageView.cpp

675 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "ImageView.h"
#include <QEvent>
#include <QFont>
#include <QGestureEvent>
#include <QGraphicsPixmapItem>
#include <QGraphicsScene>
#include <QLabel>
#include <QMouseEvent>
#include <QPinchGesture>
#include <QResizeEvent>
#include <QTabBar>
#include <QtWidgets/QScrollBar>
#include <QStyle>
#include <QWheelEvent>
#include <QtMath>
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<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
{
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<QGestureEvent*>(event));
}
return QGraphicsView::viewportEvent(event);
}
bool ImageView::handleGestureEvent(QGestureEvent* event)
{
QGesture* gesture = event->gesture(Qt::PinchGesture);
if (!gesture)
{
return false;
}
QPinchGesture* pinchGesture = static_cast<QPinchGesture*>(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();
}