initial commit

This commit is contained in:
2026-04-07 20:55:30 +08:00
commit 81d1fb7856
84 changed files with 11929 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
#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();
}