#include "dialogs/ImageCropDialog.h" #include #include #include #include #include #include #include #include class ImageCropDialog::CropView final : public QWidget { public: explicit CropView(QWidget* parent = nullptr) : QWidget(parent) { setMouseTracking(true); setMinimumSize(480, 320); } void setImage(const QImage& img) { m_image = img; m_selection = {}; updateGeometry(); update(); } bool hasSelection() const { return !m_selection.isNull() && m_selection.width() > 0 && m_selection.height() > 0; } QRect selectionInImagePixels() const { if (m_image.isNull() || !hasSelection()) { return {}; } const auto map = viewToImageTransform(); // selection 是 view 坐标;映射到 image 像素坐标 const QRectF selF = QRectF(m_selection).normalized(); bool invertible = false; const QTransform inv = map.inverted(&invertible); if (!invertible) { return {}; } const QPointF topLeftImg = inv.map(selF.topLeft()); const QPointF bottomRightImg = inv.map(selF.bottomRight()); // 使用 floor/ceil,避免因为取整导致宽高变 0 const int left = qFloor(std::min(topLeftImg.x(), bottomRightImg.x())); const int top = qFloor(std::min(topLeftImg.y(), bottomRightImg.y())); const int right = qCeil(std::max(topLeftImg.x(), bottomRightImg.x())); const int bottom = qCeil(std::max(topLeftImg.y(), bottomRightImg.y())); QRect r(QPoint(left, top), QPoint(right, bottom)); r = r.normalized().intersected(QRect(0, 0, m_image.width(), m_image.height())); return r; } void resetSelection() { m_selection = {}; update(); } protected: void paintEvent(QPaintEvent*) override { QPainter p(this); p.fillRect(rect(), palette().window()); if (m_image.isNull()) { p.setPen(palette().text().color()); p.drawText(rect(), Qt::AlignCenter, QStringLiteral("无法加载图片")); return; } const auto map = viewToImageTransform(); p.setRenderHint(QPainter::SmoothPixmapTransform, true); p.setTransform(map); p.drawImage(QPoint(0, 0), m_image); p.resetTransform(); if (hasSelection()) { // 避免 CompositionMode_Clear 在某些平台/样式下表现异常: // 用“围绕选区画四块遮罩”的方式实现高亮裁剪区域。 const QRect sel = m_selection.normalized().intersected(rect()); const QColor shade(0, 0, 0, 120); // 上 p.fillRect(QRect(0, 0, width(), sel.top()), shade); // 下 p.fillRect(QRect(0, sel.bottom(), width(), height() - sel.bottom()), shade); // 左 p.fillRect(QRect(0, sel.top(), sel.left(), sel.height()), shade); // 右 p.fillRect(QRect(sel.right(), sel.top(), width() - sel.right(), sel.height()), shade); p.setPen(QPen(QColor(255, 255, 255, 220), 2)); p.drawRect(sel); } } void mousePressEvent(QMouseEvent* e) override { if (m_image.isNull() || e->button() != Qt::LeftButton) { return; } m_dragging = true; m_anchor = e->position().toPoint(); m_selection = QRect(m_anchor, m_anchor); update(); } void mouseMoveEvent(QMouseEvent* e) override { if (!m_dragging) { return; } const QPoint cur = e->position().toPoint(); m_selection = QRect(m_anchor, cur).normalized(); update(); } void mouseReleaseEvent(QMouseEvent* e) override { if (e->button() != Qt::LeftButton) { return; } m_dragging = false; update(); } private: QTransform viewToImageTransform() const { // 让图片按比例 fit 到 view 中居中显示 const QSizeF viewSize = size(); const QSizeF imgSize = m_image.size(); const qreal sx = viewSize.width() / imgSize.width(); const qreal sy = viewSize.height() / imgSize.height(); const qreal s = std::min(sx, sy); const qreal drawW = imgSize.width() * s; const qreal drawH = imgSize.height() * s; const qreal offsetX = (viewSize.width() - drawW) / 2.0; const qreal offsetY = (viewSize.height() - drawH) / 2.0; QTransform t; t.translate(offsetX, offsetY); t.scale(s, s); return t; } private: QImage m_image; bool m_dragging = false; QPoint m_anchor; QRect m_selection; }; ImageCropDialog::ImageCropDialog(const QString& imagePath, QWidget* parent) : QDialog(parent), m_imagePath(imagePath) { setWindowTitle(QStringLiteral("裁剪图片")); setModal(true); resize(900, 600); loadImageOrClose(); rebuildUi(); } void ImageCropDialog::loadImageOrClose() { // Qt 默认的 image allocation limit 较小(常见为 256MB),超大分辨率图会被拒绝。 // 这里用 QImageReader 并提高 limit;同时对极端大图按像素数上限自动缩放,避免 OOM。 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QImageReader::setAllocationLimit(1024); // MB #endif QImageReader reader(m_imagePath); reader.setAutoTransform(true); const QSize sz = reader.size(); if (sz.isValid()) { constexpr qint64 kMaxPixels = 120LL * 1000LL * 1000LL; // 120MP const qint64 pixels = qint64(sz.width()) * qint64(sz.height()); if (pixels > kMaxPixels) { const double s = std::sqrt(double(kMaxPixels) / std::max(1.0, double(pixels))); const int nw = std::max(1, int(std::lround(sz.width() * s))); const int nh = std::max(1, int(std::lround(sz.height() * s))); reader.setScaledSize(QSize(nw, nh)); } } m_image = reader.read(); if (m_image.isNull()) { reject(); } } void ImageCropDialog::rebuildUi() { auto* root = new QVBoxLayout(this); auto* hint = new QLabel(QStringLiteral("拖拽选择裁剪区域(不选则使用整张图)。"), this); root->addWidget(hint); m_view = new CropView(this); m_view->setImage(m_image); root->addWidget(m_view, 1); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); m_okButton = buttons->button(QDialogButtonBox::Ok); auto* resetBtn = new QPushButton(QStringLiteral("重置选择"), this); buttons->addButton(resetBtn, QDialogButtonBox::ActionRole); connect(resetBtn, &QPushButton::clicked, this, &ImageCropDialog::onReset); connect(buttons, &QDialogButtonBox::accepted, this, &ImageCropDialog::onOk); connect(buttons, &QDialogButtonBox::rejected, this, &ImageCropDialog::reject); root->addWidget(buttons); } bool ImageCropDialog::hasValidSelection() const { return m_view && m_view->hasSelection(); } QRect ImageCropDialog::selectedRectInImagePixels() const { if (!m_view) { return {}; } return m_view->selectionInImagePixels(); } void ImageCropDialog::onReset() { if (m_view) { m_view->resetSelection(); } } void ImageCropDialog::onOk() { accept(); }