Utils/ImageView/Src/ImageView.cpp

429 lines
11 KiB
C++

#include "ImageView.h"
#include <QEvent>
#include <QGestureEvent>
#include <QGraphicsPixmapItem>
#include <QGraphicsScene>
#include <QLabel>
#include <QMouseEvent>
#include <QPinchGesture>
#include <QResizeEvent>
#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);
}
ImageView::ImageView(QWidget* parent)
: QGraphicsView(parent),
m_scene(new QGraphicsScene(this)),
m_pixmapItem(nullptr),
m_zoomLabel(new QLabel(this)),
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();
}
ImageView::~ImageView() = default;
void ImageView::setImage(const QImage& image)
{
if (image.isNull()) {
clearImage();
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::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_image = QImage();
m_scene->clear();
m_pixmapItem = nullptr;
m_scene->setSceneRect(QRectF());
m_zoomRatio = 1.0;
m_needsInitialFit = false;
updateZoomLabel();
viewport()->update();
emit imageChanged();
}
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;
}
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_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();
}