429 lines
11 KiB
C++
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();
|
|
}
|