210 lines
6.3 KiB
C++
210 lines
6.3 KiB
C++
#include "dialogs/ImageCropDialog.h"
|
||
|
||
#include <QBoxLayout>
|
||
#include <QDialogButtonBox>
|
||
#include <QLabel>
|
||
#include <QMouseEvent>
|
||
#include <QPainter>
|
||
#include <QPushButton>
|
||
#include <QtMath>
|
||
|
||
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();
|
||
}
|
||
|