#include "dialogs/ImageCropDialog.h" #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() { m_image = QImage(m_imagePath); 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(); }