initial commit
This commit is contained in:
66
client/gui/dialogs/AboutWindow.cpp
Normal file
66
client/gui/dialogs/AboutWindow.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
#include "dialogs/AboutWindow.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QHBoxLayout>
|
||||
#include <QDesktopServices>
|
||||
#include <QUrl>
|
||||
#include <QFont>
|
||||
|
||||
AboutWindow::AboutWindow(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("About");
|
||||
setFixedSize(400, 300);
|
||||
|
||||
// ===== 标题 =====
|
||||
titleLabel = new QLabel("Landscape Interactive Tool");
|
||||
QFont titleFont;
|
||||
titleFont.setPointSize(16);
|
||||
titleFont.setBold(true);
|
||||
titleLabel->setFont(titleFont);
|
||||
titleLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
// ===== 版本 =====
|
||||
versionLabel = new QLabel("Version: 1.0.0");
|
||||
versionLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
// ===== 作者 =====
|
||||
authorLabel = new QLabel("Author: 丁伟豪");
|
||||
authorLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
// ===== 描述 =====
|
||||
descLabel = new QLabel("An interactive tool for landscape visualization.\n"
|
||||
"Built with Qt.");
|
||||
descLabel->setAlignment(Qt::AlignCenter);
|
||||
descLabel->setWordWrap(true);
|
||||
|
||||
// // ===== GitHub 按钮 =====
|
||||
// githubButton = new QPushButton("GitHub");
|
||||
// connect(githubButton, &QPushButton::clicked, []() {
|
||||
// QDesktopServices::openUrl(QUrl("https://github.com/your_repo"));
|
||||
// });
|
||||
|
||||
// ===== 关闭按钮 =====
|
||||
closeButton = new QPushButton("Close");
|
||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::accept);
|
||||
|
||||
// ===== 按钮布局 =====
|
||||
QHBoxLayout* buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->addStretch();
|
||||
// buttonLayout->addWidget(githubButton);
|
||||
buttonLayout->addWidget(closeButton);
|
||||
|
||||
// ===== 主布局 =====
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->addWidget(titleLabel);
|
||||
layout->addWidget(versionLabel);
|
||||
layout->addWidget(authorLabel);
|
||||
layout->addSpacing(10);
|
||||
layout->addWidget(descLabel);
|
||||
layout->addStretch();
|
||||
layout->addLayout(buttonLayout);
|
||||
|
||||
setLayout(layout);
|
||||
}
|
||||
20
client/gui/dialogs/AboutWindow.h
Normal file
20
client/gui/dialogs/AboutWindow.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <QDialog>
|
||||
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
class AboutWindow : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AboutWindow(QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
QLabel* titleLabel;
|
||||
QLabel* versionLabel;
|
||||
QLabel* authorLabel;
|
||||
QLabel* descLabel;
|
||||
// QPushButton* githubButton;
|
||||
QPushButton* closeButton;
|
||||
};
|
||||
50
client/gui/dialogs/CancelableTaskDialog.cpp
Normal file
50
client/gui/dialogs/CancelableTaskDialog.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "dialogs/CancelableTaskDialog.h"
|
||||
|
||||
#include <QBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
|
||||
CancelableTaskDialog::CancelableTaskDialog(const QString& title,
|
||||
const QString& message,
|
||||
QWidget* parent)
|
||||
: QDialog(parent) {
|
||||
setWindowTitle(title);
|
||||
setModal(true);
|
||||
setMinimumWidth(420);
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(14, 14, 14, 14);
|
||||
root->setSpacing(10);
|
||||
|
||||
m_label = new QLabel(message, this);
|
||||
m_label->setWordWrap(true);
|
||||
root->addWidget(m_label);
|
||||
|
||||
m_bar = new QProgressBar(this);
|
||||
m_bar->setRange(0, 0); // 不定进度
|
||||
root->addWidget(m_bar);
|
||||
|
||||
auto* row = new QHBoxLayout();
|
||||
row->addStretch(1);
|
||||
m_btnCancel = new QPushButton(QStringLiteral("取消"), this);
|
||||
row->addWidget(m_btnCancel);
|
||||
root->addLayout(row);
|
||||
|
||||
connect(m_btnCancel, &QPushButton::clicked, this, &CancelableTaskDialog::onCancel);
|
||||
}
|
||||
|
||||
void CancelableTaskDialog::setMessage(const QString& message) {
|
||||
if (m_label) {
|
||||
m_label->setText(message);
|
||||
}
|
||||
}
|
||||
|
||||
void CancelableTaskDialog::onCancel() {
|
||||
if (m_canceled) {
|
||||
return;
|
||||
}
|
||||
m_canceled = true;
|
||||
emit canceled();
|
||||
}
|
||||
|
||||
35
client/gui/dialogs/CancelableTaskDialog.h
Normal file
35
client/gui/dialogs/CancelableTaskDialog.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
class QLabel;
|
||||
class QProgressBar;
|
||||
class QPushButton;
|
||||
|
||||
// 可复用的“长任务提示框”:显示提示文本 + 不定进度条 + 取消按钮。
|
||||
// - 任务本身由调用方启动(例如网络请求/后台线程)
|
||||
// - 调用方在取消时应中止任务,并调用 reject()/close()
|
||||
class CancelableTaskDialog final : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CancelableTaskDialog(const QString& title,
|
||||
const QString& message,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
void setMessage(const QString& message);
|
||||
bool wasCanceled() const { return m_canceled; }
|
||||
|
||||
signals:
|
||||
void canceled();
|
||||
|
||||
private slots:
|
||||
void onCancel();
|
||||
|
||||
private:
|
||||
QLabel* m_label = nullptr;
|
||||
QProgressBar* m_bar = nullptr;
|
||||
QPushButton* m_btnCancel = nullptr;
|
||||
bool m_canceled = false;
|
||||
};
|
||||
|
||||
252
client/gui/dialogs/FrameAnimationDialog.cpp
Normal file
252
client/gui/dialogs/FrameAnimationDialog.cpp
Normal file
@@ -0,0 +1,252 @@
|
||||
#include "dialogs/FrameAnimationDialog.h"
|
||||
|
||||
#include "core/animation/AnimationSampling.h"
|
||||
#include "core/workspace/ProjectWorkspace.h"
|
||||
|
||||
#include <QBoxLayout>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
|
||||
namespace {
|
||||
|
||||
QString resolvedImageAbsForFrame(const core::ProjectWorkspace& ws,
|
||||
const core::Project::Entity& e,
|
||||
int frame) {
|
||||
const QString rel = core::sampleImagePath(e.imageFrames, frame, e.imagePath);
|
||||
if (rel.isEmpty()) return {};
|
||||
const QString abs = QDir(ws.projectDir()).filePath(rel);
|
||||
return abs;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FrameAnimationDialog::FrameAnimationDialog(core::ProjectWorkspace& workspace,
|
||||
const QString& entityId,
|
||||
int startFrame,
|
||||
int endFrame,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_workspace(workspace)
|
||||
, m_entityId(entityId) {
|
||||
setWindowTitle(QStringLiteral("区间动画帧"));
|
||||
setModal(true);
|
||||
setMinimumSize(720, 420);
|
||||
|
||||
m_start = std::min(startFrame, endFrame);
|
||||
m_end = std::max(startFrame, endFrame);
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(12, 12, 12, 12);
|
||||
root->setSpacing(10);
|
||||
|
||||
m_title = new QLabel(this);
|
||||
m_title->setText(QStringLiteral("实体 %1 | 区间 [%2, %3]").arg(m_entityId).arg(m_start).arg(m_end));
|
||||
root->addWidget(m_title);
|
||||
|
||||
auto* mid = new QHBoxLayout();
|
||||
root->addLayout(mid, 1);
|
||||
|
||||
m_list = new QListWidget(this);
|
||||
m_list->setMinimumWidth(240);
|
||||
mid->addWidget(m_list, 0);
|
||||
|
||||
auto* right = new QVBoxLayout();
|
||||
mid->addLayout(right, 1);
|
||||
|
||||
m_preview = new QLabel(this);
|
||||
m_preview->setMinimumSize(320, 240);
|
||||
m_preview->setFrameShape(QFrame::StyledPanel);
|
||||
m_preview->setAlignment(Qt::AlignCenter);
|
||||
m_preview->setText(QStringLiteral("选择一帧"));
|
||||
right->addWidget(m_preview, 1);
|
||||
|
||||
auto* row = new QHBoxLayout();
|
||||
right->addLayout(row);
|
||||
m_btnReplace = new QPushButton(QStringLiteral("替换此帧…"), this);
|
||||
m_btnClear = new QPushButton(QStringLiteral("清除此帧(恢复默认)"), this);
|
||||
row->addWidget(m_btnReplace);
|
||||
row->addWidget(m_btnClear);
|
||||
|
||||
auto* row2 = new QHBoxLayout();
|
||||
right->addLayout(row2);
|
||||
m_btnImportFiles = new QPushButton(QStringLiteral("批量导入(多选图片)…"), this);
|
||||
m_btnImportFolder = new QPushButton(QStringLiteral("批量导入(文件夹)…"), this);
|
||||
row2->addWidget(m_btnImportFiles);
|
||||
row2->addWidget(m_btnImportFolder);
|
||||
row2->addStretch(1);
|
||||
|
||||
auto* closeRow = new QHBoxLayout();
|
||||
root->addLayout(closeRow);
|
||||
closeRow->addStretch(1);
|
||||
auto* btnClose = new QPushButton(QStringLiteral("关闭"), this);
|
||||
closeRow->addWidget(btnClose);
|
||||
|
||||
connect(btnClose, &QPushButton::clicked, this, &QDialog::accept);
|
||||
connect(m_list, &QListWidget::currentRowChanged, this, [this](int) { onSelectFrame(); });
|
||||
connect(m_btnReplace, &QPushButton::clicked, this, &FrameAnimationDialog::onReplaceCurrentFrame);
|
||||
connect(m_btnClear, &QPushButton::clicked, this, &FrameAnimationDialog::onClearCurrentFrame);
|
||||
connect(m_btnImportFiles, &QPushButton::clicked, this, &FrameAnimationDialog::onBatchImportFiles);
|
||||
connect(m_btnImportFolder, &QPushButton::clicked, this, &FrameAnimationDialog::onBatchImportFolder);
|
||||
|
||||
rebuildFrameList();
|
||||
if (m_list->count() > 0) {
|
||||
m_list->setCurrentRow(0);
|
||||
}
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::rebuildFrameList() {
|
||||
m_list->clear();
|
||||
if (!m_workspace.isOpen()) return;
|
||||
|
||||
const auto& ents = m_workspace.entities();
|
||||
const core::Project::Entity* hit = nullptr;
|
||||
for (const auto& e : ents) {
|
||||
if (e.id == m_entityId) {
|
||||
hit = &e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hit) return;
|
||||
|
||||
// 默认贴图(用于 UI 提示)
|
||||
m_defaultImageAbs.clear();
|
||||
if (!hit->imagePath.isEmpty()) {
|
||||
const QString abs = QDir(m_workspace.projectDir()).filePath(hit->imagePath);
|
||||
if (QFileInfo::exists(abs)) {
|
||||
m_defaultImageAbs = abs;
|
||||
}
|
||||
}
|
||||
|
||||
for (int f = m_start; f <= m_end; ++f) {
|
||||
bool hasCustom = false;
|
||||
for (const auto& k : hit->imageFrames) {
|
||||
if (k.frame == f) {
|
||||
hasCustom = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto* it = new QListWidgetItem(QStringLiteral("%1%2").arg(f).arg(hasCustom ? QStringLiteral(" *") : QString()));
|
||||
it->setData(Qt::UserRole, f);
|
||||
m_list->addItem(it);
|
||||
}
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onSelectFrame() {
|
||||
auto* it = m_list->currentItem();
|
||||
if (!it) return;
|
||||
const int f = it->data(Qt::UserRole).toInt();
|
||||
updatePreviewForFrame(f);
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::updatePreviewForFrame(int frame) {
|
||||
if (!m_workspace.isOpen()) return;
|
||||
const auto& ents = m_workspace.entities();
|
||||
const core::Project::Entity* hit = nullptr;
|
||||
for (const auto& e : ents) {
|
||||
if (e.id == m_entityId) {
|
||||
hit = &e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hit) return;
|
||||
|
||||
const QString abs = resolvedImageAbsForFrame(m_workspace, *hit, frame);
|
||||
if (abs.isEmpty() || !QFileInfo::exists(abs)) {
|
||||
m_preview->setText(QStringLiteral("无图像"));
|
||||
return;
|
||||
}
|
||||
QPixmap pm(abs);
|
||||
if (pm.isNull()) {
|
||||
m_preview->setText(QStringLiteral("加载失败"));
|
||||
return;
|
||||
}
|
||||
m_preview->setPixmap(pm.scaled(m_preview->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
bool FrameAnimationDialog::applyImageToFrame(int frame, const QString& absImagePath) {
|
||||
QImage img(absImagePath);
|
||||
if (img.isNull()) {
|
||||
return false;
|
||||
}
|
||||
if (img.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
return m_workspace.setEntityImageFrame(m_entityId, frame, img);
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onReplaceCurrentFrame() {
|
||||
auto* it = m_list->currentItem();
|
||||
if (!it) return;
|
||||
const int f = it->data(Qt::UserRole).toInt();
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
QStringLiteral("选择该帧图像"),
|
||||
QString(),
|
||||
QStringLiteral("Images (*.png *.jpg *.jpeg *.bmp *.webp);;All Files (*)"));
|
||||
if (path.isEmpty()) return;
|
||||
if (!applyImageToFrame(f, path)) {
|
||||
QMessageBox::warning(this, QStringLiteral("动画帧"), QStringLiteral("写入该帧失败。"));
|
||||
return;
|
||||
}
|
||||
rebuildFrameList();
|
||||
updatePreviewForFrame(f);
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onClearCurrentFrame() {
|
||||
auto* it = m_list->currentItem();
|
||||
if (!it) return;
|
||||
const int f = it->data(Qt::UserRole).toInt();
|
||||
if (!m_workspace.removeEntityImageFrame(m_entityId, f)) {
|
||||
return;
|
||||
}
|
||||
rebuildFrameList();
|
||||
updatePreviewForFrame(f);
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onBatchImportFiles() {
|
||||
const QStringList paths = QFileDialog::getOpenFileNames(
|
||||
this,
|
||||
QStringLiteral("选择逐帧动画图片(按文件名排序)"),
|
||||
QString(),
|
||||
QStringLiteral("Images (*.png *.jpg *.jpeg *.bmp *.webp);;All Files (*)"));
|
||||
if (paths.isEmpty()) return;
|
||||
QStringList sorted = paths;
|
||||
sorted.sort(Qt::CaseInsensitive);
|
||||
const int need = m_end - m_start + 1;
|
||||
const int count = std::min(need, static_cast<int>(sorted.size()));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
applyImageToFrame(m_start + i, sorted[i]);
|
||||
}
|
||||
rebuildFrameList();
|
||||
onSelectFrame();
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onBatchImportFolder() {
|
||||
const QString dir = QFileDialog::getExistingDirectory(this, QStringLiteral("选择逐帧动画图片文件夹"));
|
||||
if (dir.isEmpty()) return;
|
||||
QDir d(dir);
|
||||
d.setFilter(QDir::Files | QDir::Readable);
|
||||
d.setSorting(QDir::Name);
|
||||
const QStringList filters = {QStringLiteral("*.png"),
|
||||
QStringLiteral("*.jpg"),
|
||||
QStringLiteral("*.jpeg"),
|
||||
QStringLiteral("*.bmp"),
|
||||
QStringLiteral("*.webp")};
|
||||
const QStringList files = d.entryList(filters, QDir::Files, QDir::Name);
|
||||
if (files.isEmpty()) return;
|
||||
const int need = m_end - m_start + 1;
|
||||
const int count = std::min(need, static_cast<int>(files.size()));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
applyImageToFrame(m_start + i, d.filePath(files[i]));
|
||||
}
|
||||
rebuildFrameList();
|
||||
onSelectFrame();
|
||||
}
|
||||
|
||||
52
client/gui/dialogs/FrameAnimationDialog.h
Normal file
52
client/gui/dialogs/FrameAnimationDialog.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace core {
|
||||
class ProjectWorkspace;
|
||||
}
|
||||
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
|
||||
class FrameAnimationDialog final : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
FrameAnimationDialog(core::ProjectWorkspace& workspace,
|
||||
const QString& entityId,
|
||||
int startFrame,
|
||||
int endFrame,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onSelectFrame();
|
||||
void onReplaceCurrentFrame();
|
||||
void onClearCurrentFrame();
|
||||
void onBatchImportFiles();
|
||||
void onBatchImportFolder();
|
||||
|
||||
private:
|
||||
void rebuildFrameList();
|
||||
void updatePreviewForFrame(int frame);
|
||||
bool applyImageToFrame(int frame, const QString& absImagePath);
|
||||
|
||||
private:
|
||||
core::ProjectWorkspace& m_workspace;
|
||||
QString m_entityId;
|
||||
int m_start = 0;
|
||||
int m_end = 0;
|
||||
|
||||
QLabel* m_title = nullptr;
|
||||
QListWidget* m_list = nullptr;
|
||||
QLabel* m_preview = nullptr;
|
||||
QPushButton* m_btnReplace = nullptr;
|
||||
QPushButton* m_btnClear = nullptr;
|
||||
QPushButton* m_btnImportFiles = nullptr;
|
||||
QPushButton* m_btnImportFolder = nullptr;
|
||||
|
||||
QString m_defaultImageAbs;
|
||||
};
|
||||
|
||||
209
client/gui/dialogs/ImageCropDialog.cpp
Normal file
209
client/gui/dialogs/ImageCropDialog.cpp
Normal 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();
|
||||
}
|
||||
|
||||
34
client/gui/dialogs/ImageCropDialog.h
Normal file
34
client/gui/dialogs/ImageCropDialog.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QImage>
|
||||
#include <QRect>
|
||||
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
class ImageCropDialog final : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ImageCropDialog(const QString& imagePath, QWidget* parent = nullptr);
|
||||
|
||||
bool hasValidSelection() const;
|
||||
QRect selectedRectInImagePixels() const;
|
||||
|
||||
private slots:
|
||||
void onReset();
|
||||
void onOk();
|
||||
|
||||
private:
|
||||
void loadImageOrClose();
|
||||
void rebuildUi();
|
||||
|
||||
private:
|
||||
class CropView;
|
||||
CropView* m_view = nullptr;
|
||||
QPushButton* m_okButton = nullptr;
|
||||
|
||||
QString m_imagePath;
|
||||
QImage m_image;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user