229 lines
7.3 KiB
C++
229 lines
7.3 KiB
C++
#include "dialogs/ImageCropDialog.h"
|
||
|
||
#include <QBoxLayout>
|
||
#include <QDialogButtonBox>
|
||
#include <QLabel>
|
||
#include <QMouseEvent>
|
||
#include <QPainter>
|
||
#include <QPushButton>
|
||
#include <QImageReader>
|
||
#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() {
|
||
// 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<double>(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();
|
||
}
|
||
|