update
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#include "dialogs/AnimationSchemeSettingsDialog.h"
|
||||
|
||||
#include "core/domain/Project.h"
|
||||
#include "core/workspace/ProjectWorkspace.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QCheckBox>
|
||||
|
||||
AnimationSchemeSettingsDialog::AnimationSchemeSettingsDialog(Mode mode, core::ProjectWorkspace& workspace,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_mode(mode)
|
||||
, m_workspace(workspace) {
|
||||
setModal(true);
|
||||
setMinimumWidth(360);
|
||||
if (mode == Mode::Create) {
|
||||
setWindowTitle(QStringLiteral("新建动画方案"));
|
||||
} else {
|
||||
setWindowTitle(QStringLiteral("动画方案设置"));
|
||||
}
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(12, 12, 12, 12);
|
||||
auto* form = new QFormLayout();
|
||||
form->setSpacing(8);
|
||||
root->addLayout(form);
|
||||
|
||||
m_name = new QLineEdit(this);
|
||||
m_name->setPlaceholderText(QStringLiteral("显示名称"));
|
||||
form->addRow(QStringLiteral("名称"), m_name);
|
||||
|
||||
m_frames = new QSpinBox(this);
|
||||
m_frames->setRange(1, 1000000);
|
||||
m_frames->setValue(600);
|
||||
form->addRow(QStringLiteral("总帧数"), m_frames);
|
||||
|
||||
m_fps = new QSpinBox(this);
|
||||
m_fps->setRange(1, 240);
|
||||
m_fps->setValue(std::max(1, m_workspace.project().fps()));
|
||||
form->addRow(QStringLiteral("帧率"), m_fps);
|
||||
|
||||
m_loop = new QCheckBox(QStringLiteral("循环播放"), this);
|
||||
m_loop->setChecked(true);
|
||||
form->addRow(QString(), m_loop);
|
||||
|
||||
auto* box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
root->addWidget(box);
|
||||
connect(box, &QDialogButtonBox::accepted, this, &AnimationSchemeSettingsDialog::accept);
|
||||
connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
void AnimationSchemeSettingsDialog::setEditTarget(const QString& animationId) {
|
||||
m_editId = animationId;
|
||||
if (const core::Project::Animation* a = m_workspace.project().findAnimationById(animationId)) {
|
||||
m_name->setText(a->name);
|
||||
m_frames->setValue(std::max(1, a->lengthFrames));
|
||||
m_fps->setValue(std::max(1, a->fps));
|
||||
m_loop->setChecked(a->loop);
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationSchemeSettingsDialog::accept() {
|
||||
const QString nm = m_name->text().trimmed();
|
||||
const int frames = m_frames->value();
|
||||
const int fps = m_fps->value();
|
||||
const bool loop = m_loop->isChecked();
|
||||
|
||||
if (m_mode == Mode::Create) {
|
||||
if (!m_workspace.createAnimationScheme(nm, frames, loop, fps, &m_createdNewId)) {
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("新建失败"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (m_editId.isEmpty() || !m_workspace.project().findAnimationById(m_editId)) {
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("无效的动画方案"));
|
||||
return;
|
||||
}
|
||||
if (!m_workspace.updateAnimationSchemeSettings(m_editId, nm, frames, loop, fps)) {
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("保存失败"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
QDialog::accept();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
namespace core {
|
||||
class ProjectWorkspace;
|
||||
}
|
||||
|
||||
class QCheckBox;
|
||||
class QLineEdit;
|
||||
class QSpinBox;
|
||||
|
||||
class AnimationSchemeSettingsDialog final : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class Mode { Create, Edit };
|
||||
|
||||
AnimationSchemeSettingsDialog(Mode mode, core::ProjectWorkspace& workspace, QWidget* parent = nullptr);
|
||||
|
||||
void setEditTarget(const QString& animationId);
|
||||
|
||||
QString createdNewId() const { return m_createdNewId; }
|
||||
|
||||
private:
|
||||
void accept() override;
|
||||
|
||||
Mode m_mode = Mode::Create;
|
||||
core::ProjectWorkspace& m_workspace;
|
||||
QString m_editId;
|
||||
QString m_createdNewId;
|
||||
|
||||
QLineEdit* m_name = nullptr;
|
||||
QSpinBox* m_frames = nullptr;
|
||||
QSpinBox* m_fps = nullptr;
|
||||
QCheckBox* m_loop = nullptr;
|
||||
};
|
||||
@@ -10,13 +10,13 @@
|
||||
|
||||
namespace {
|
||||
|
||||
QPushButton* makeAlgoButton(const QString& title, const QString& subtitle, QWidget* parent) {
|
||||
QPushButton* makeAlgoButton(const QString& title, QWidget* parent) {
|
||||
auto* btn = new QPushButton(parent);
|
||||
btn->setCheckable(false);
|
||||
btn->setCursor(Qt::PointingHandCursor);
|
||||
btn->setMinimumHeight(86);
|
||||
btn->setMinimumHeight(48);
|
||||
btn->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
btn->setText(title + QStringLiteral("\n") + subtitle);
|
||||
btn->setText(title);
|
||||
btn->setStyleSheet(
|
||||
"QPushButton { text-align: left; padding: 10px 12px; border: 1px solid palette(mid); border-radius: 8px; }"
|
||||
"QPushButton:hover { border-color: palette(highlight); }");
|
||||
@@ -29,8 +29,8 @@ BlackholeResolveDialog::BlackholeResolveDialog(const QString& blackholeName, QWi
|
||||
: QDialog(parent),
|
||||
m_blackholeName(blackholeName) {
|
||||
setModal(true);
|
||||
setMinimumSize(560, 420);
|
||||
setWindowTitle(QStringLiteral("黑洞解决"));
|
||||
setMinimumSize(400, 320);
|
||||
setWindowTitle(QStringLiteral("修复 · %1").arg(m_blackholeName));
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
m_pages = new QStackedWidget(this);
|
||||
@@ -51,25 +51,13 @@ void BlackholeResolveDialog::buildSelectPage() {
|
||||
layout->setContentsMargins(8, 8, 8, 8);
|
||||
layout->setSpacing(12);
|
||||
|
||||
auto* title = new QLabel(QStringLiteral("第 1 步:选择黑洞解决算法"), m_pageSelect);
|
||||
auto* sub = new QLabel(QStringLiteral("当前黑洞:%1").arg(m_blackholeName), m_pageSelect);
|
||||
title->setStyleSheet("font-size: 18px; font-weight: 600;");
|
||||
sub->setStyleSheet("color: palette(mid);");
|
||||
auto* title = new QLabel(QStringLiteral("方式"), m_pageSelect);
|
||||
title->setStyleSheet("font-size: 16px; font-weight: 600;");
|
||||
layout->addWidget(title);
|
||||
layout->addWidget(sub);
|
||||
|
||||
auto* btnCopy = makeAlgoButton(
|
||||
QStringLiteral("复制背景其他区域"),
|
||||
QStringLiteral("进入画布拖动取样框,直观选择复制来源。"),
|
||||
m_pageSelect);
|
||||
auto* btnOriginal = makeAlgoButton(
|
||||
QStringLiteral("使用原始背景"),
|
||||
QStringLiteral("撤销黑洞显示,恢复抠图前背景区域。"),
|
||||
m_pageSelect);
|
||||
auto* btnModel = makeAlgoButton(
|
||||
QStringLiteral("模型补全(SDXL Inpaint)"),
|
||||
QStringLiteral("输入提示词,自动补全缺失区域;可预览后再决定是否接受。"),
|
||||
m_pageSelect);
|
||||
auto* btnCopy = makeAlgoButton(QStringLiteral("复制背景"), m_pageSelect);
|
||||
auto* btnOriginal = makeAlgoButton(QStringLiteral("原始背景"), m_pageSelect);
|
||||
auto* btnModel = makeAlgoButton(QStringLiteral("模型补全"), m_pageSelect);
|
||||
layout->addWidget(btnCopy);
|
||||
layout->addWidget(btnOriginal);
|
||||
layout->addWidget(btnModel);
|
||||
@@ -100,12 +88,8 @@ void BlackholeResolveDialog::buildDetailPage() {
|
||||
layout->setSpacing(10);
|
||||
|
||||
m_detailTitle = new QLabel(m_pageDetail);
|
||||
m_detailTitle->setStyleSheet("font-size: 18px; font-weight: 600;");
|
||||
m_detailHint = new QLabel(m_pageDetail);
|
||||
m_detailHint->setWordWrap(true);
|
||||
m_detailHint->setStyleSheet("color: palette(mid);");
|
||||
m_detailTitle->setStyleSheet("font-size: 15px; font-weight: 600;");
|
||||
layout->addWidget(m_detailTitle);
|
||||
layout->addWidget(m_detailHint);
|
||||
|
||||
m_algoDetails = new QStackedWidget(m_pageDetail);
|
||||
|
||||
@@ -120,10 +104,7 @@ void BlackholeResolveDialog::buildDetailPage() {
|
||||
auto* pLay = new QVBoxLayout(panel);
|
||||
pLay->setSpacing(8);
|
||||
|
||||
auto* tip = new QLabel(
|
||||
QStringLiteral("说明:点击“应用”后进入画布拖动模式。\n在画布中拖动青色取样框,松开鼠标即可将该区域复制到黑洞位置并自动移除黑洞。"),
|
||||
panel);
|
||||
tip->setWordWrap(true);
|
||||
auto* tip = new QLabel(QStringLiteral("应用后在画布拖取样框。"), panel);
|
||||
tip->setStyleSheet("color: palette(mid);");
|
||||
pLay->addWidget(tip);
|
||||
cLay->addWidget(panel);
|
||||
@@ -135,17 +116,9 @@ void BlackholeResolveDialog::buildDetailPage() {
|
||||
{
|
||||
auto* oLay = new QVBoxLayout(m_originalDetail);
|
||||
oLay->setSpacing(8);
|
||||
auto* desc = new QLabel(
|
||||
QStringLiteral("该方案不会改动背景像素文件,只会将黑洞切换为不显示,从而恢复原始背景区域。"),
|
||||
m_originalDetail);
|
||||
auto* desc = new QLabel(QStringLiteral("仅隐藏黑洞,不改动背景文件。"), m_originalDetail);
|
||||
desc->setWordWrap(true);
|
||||
auto* note = new QLabel(
|
||||
QStringLiteral("适用场景:当前黑洞区域无需二次修补,只需恢复抠图前背景;应用后黑洞会自动移除。"),
|
||||
m_originalDetail);
|
||||
note->setWordWrap(true);
|
||||
note->setStyleSheet("color: palette(mid);");
|
||||
oLay->addWidget(desc);
|
||||
oLay->addWidget(note);
|
||||
oLay->addStretch(1);
|
||||
}
|
||||
|
||||
@@ -161,8 +134,8 @@ void BlackholeResolveDialog::buildDetailPage() {
|
||||
pLay->setSpacing(8);
|
||||
|
||||
m_promptEdit = new QPlainTextEdit(panel);
|
||||
m_promptEdit->setPlainText(QStringLiteral("This is part of a Chinese painting; please complete the background for me, following the style of the other parts."));
|
||||
m_promptEdit->setMinimumHeight(90);
|
||||
m_promptEdit->setPlaceholderText(QStringLiteral("提示词(可选)"));
|
||||
m_promptEdit->setMinimumHeight(72);
|
||||
pLay->addWidget(m_promptEdit);
|
||||
|
||||
mLay->addWidget(panel);
|
||||
@@ -175,8 +148,8 @@ void BlackholeResolveDialog::buildDetailPage() {
|
||||
layout->addWidget(m_algoDetails, 1);
|
||||
|
||||
auto* btns = new QDialogButtonBox(m_pageDetail);
|
||||
auto* btnBack = btns->addButton(QStringLiteral("上一步"), QDialogButtonBox::ActionRole);
|
||||
auto* btnApply = btns->addButton(QStringLiteral("应用"), QDialogButtonBox::AcceptRole);
|
||||
auto* btnBack = btns->addButton(QStringLiteral("返回"), QDialogButtonBox::ActionRole);
|
||||
auto* btnApply = btns->addButton(QStringLiteral("确定"), QDialogButtonBox::AcceptRole);
|
||||
auto* btnCancel = btns->addButton(QDialogButtonBox::Cancel);
|
||||
connect(btnBack, &QPushButton::clicked, this, [this]() {
|
||||
m_pages->setCurrentWidget(m_pageSelect);
|
||||
@@ -191,16 +164,13 @@ void BlackholeResolveDialog::buildDetailPage() {
|
||||
void BlackholeResolveDialog::enterAlgorithmPage(Algorithm algo) {
|
||||
m_selectedAlgorithm = algo;
|
||||
if (algo == Algorithm::CopyBackgroundRegion) {
|
||||
m_detailTitle->setText(QStringLiteral("第 2 步:复制背景其他区域"));
|
||||
m_detailHint->setText(QStringLiteral("准备进入画布拖动取样框模式。"));
|
||||
m_detailTitle->setText(QStringLiteral("复制背景"));
|
||||
m_algoDetails->setCurrentWidget(m_copyDetail);
|
||||
} else if (algo == Algorithm::UseOriginalBackground) {
|
||||
m_detailTitle->setText(QStringLiteral("第 2 步:使用原始背景"));
|
||||
m_detailHint->setText(QStringLiteral("确认后将切换为原始背景显示。"));
|
||||
m_detailTitle->setText(QStringLiteral("原始背景"));
|
||||
m_algoDetails->setCurrentWidget(m_originalDetail);
|
||||
} else {
|
||||
m_detailTitle->setText(QStringLiteral("第 2 步:模型补全(SDXL Inpaint)"));
|
||||
m_detailHint->setText(QStringLiteral("输入提示词(可选),点击应用后将生成预览。"));
|
||||
m_detailTitle->setText(QStringLiteral("模型补全"));
|
||||
m_algoDetails->setCurrentWidget(m_modelDetail);
|
||||
}
|
||||
m_pages->setCurrentWidget(m_pageDetail);
|
||||
|
||||
@@ -34,7 +34,6 @@ private:
|
||||
QWidget* m_pageDetail = nullptr;
|
||||
|
||||
QLabel* m_detailTitle = nullptr;
|
||||
QLabel* m_detailHint = nullptr;
|
||||
QStackedWidget* m_algoDetails = nullptr;
|
||||
|
||||
QWidget* m_copyDetail = nullptr;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#include "dialogs/EntityFinalizeDialog.h"
|
||||
|
||||
#include "widgets/CompactNumericSpinBox.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
EntityFinalizeDialog::EntityFinalizeDialog(QWidget* parent)
|
||||
@@ -18,7 +19,7 @@ EntityFinalizeDialog::EntityFinalizeDialog(QWidget* parent)
|
||||
m_name->setPlaceholderText(QStringLiteral("例如:entity-1、人物、树、建筑…"));
|
||||
form->addRow(QStringLiteral("名称"), m_name);
|
||||
|
||||
m_userScale = new QDoubleSpinBox(this);
|
||||
m_userScale = new gui::CompactDoubleSpinBox(this);
|
||||
m_userScale->setDecimals(3);
|
||||
m_userScale->setRange(0.01, 50.0);
|
||||
m_userScale->setSingleStep(0.05);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "dialogs/EntityIntroPopup.h"
|
||||
|
||||
#include "core/image/ImageDecodeConfig.h"
|
||||
#include "core/image/ImageFileLoader.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
@@ -95,7 +98,8 @@ void EntityIntroPopup::setContent(const core::EntityIntroContent& content) {
|
||||
auto* lab = new QLabel(m_imagesHost);
|
||||
lab->setAlignment(Qt::AlignCenter);
|
||||
if (!abs.isEmpty() && QFileInfo::exists(abs)) {
|
||||
QPixmap pm(abs);
|
||||
QPixmap pm = QPixmap::fromImage(
|
||||
core::image_file::loadImageLimited(abs, core::image_decode::kPreviewMaxPixels));
|
||||
if (!pm.isNull()) {
|
||||
if (pm.width() > kMaxThumb || pm.height() > kMaxThumb) {
|
||||
pm = pm.scaled(kMaxThumb, kMaxThumb, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
|
||||
@@ -1,29 +1,45 @@
|
||||
#include "dialogs/FrameAnimationDialog.h"
|
||||
|
||||
#include "core/animation/AnimationSampling.h"
|
||||
#include "core/image/ImageDecodeConfig.h"
|
||||
#include "core/image/ImageFileLoader.h"
|
||||
#include "core/net/ModelServerClient.h"
|
||||
#include "core/workspace/ProjectWorkspace.h"
|
||||
#include "dialogs/CancelableTaskDialog.h"
|
||||
|
||||
#include <QBoxLayout>
|
||||
#include <QAbstractItemView>
|
||||
#include <QBuffer>
|
||||
#include <QColor>
|
||||
#include <QColorDialog>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFormLayout>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QIODevice>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
#include <QUrl>
|
||||
|
||||
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;
|
||||
const QByteArray png = ws.entityImagePngAt(e.id, frame);
|
||||
if (png.isEmpty()) return {};
|
||||
return QStringLiteral("pngb64:") + QString::fromLatin1(png.toBase64());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -56,6 +72,7 @@ FrameAnimationDialog::FrameAnimationDialog(core::ProjectWorkspace& workspace,
|
||||
|
||||
m_list = new QListWidget(this);
|
||||
m_list->setMinimumWidth(240);
|
||||
m_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
mid->addWidget(m_list, 0);
|
||||
|
||||
auto* right = new QVBoxLayout();
|
||||
@@ -71,18 +88,27 @@ FrameAnimationDialog::FrameAnimationDialog(core::ProjectWorkspace& workspace,
|
||||
auto* row = new QHBoxLayout();
|
||||
right->addLayout(row);
|
||||
m_btnReplace = new QPushButton(QStringLiteral("替换此帧…"), this);
|
||||
m_btnClear = 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);
|
||||
m_btnImportFiles = new QPushButton(QStringLiteral("导入图片…"), this);
|
||||
m_btnImportFolder = new QPushButton(QStringLiteral("导入文件夹…"), this);
|
||||
row2->addWidget(m_btnImportFiles);
|
||||
row2->addWidget(m_btnImportFolder);
|
||||
row2->addStretch(1);
|
||||
|
||||
auto* row3 = new QHBoxLayout();
|
||||
right->addLayout(row3);
|
||||
m_btnAiGenerate = new QPushButton(QStringLiteral("AI 生成逐帧动画…"), this);
|
||||
row3->addWidget(m_btnAiGenerate);
|
||||
auto* aiHint = new QLabel(QStringLiteral("写入选中区间"), this);
|
||||
aiHint->setStyleSheet(QStringLiteral("color: #777;"));
|
||||
row3->addWidget(aiHint);
|
||||
row3->addStretch(1);
|
||||
|
||||
auto* closeRow = new QHBoxLayout();
|
||||
root->addLayout(closeRow);
|
||||
closeRow->addStretch(1);
|
||||
@@ -95,6 +121,7 @@ FrameAnimationDialog::FrameAnimationDialog(core::ProjectWorkspace& workspace,
|
||||
connect(m_btnClear, &QPushButton::clicked, this, &FrameAnimationDialog::onClearCurrentFrame);
|
||||
connect(m_btnImportFiles, &QPushButton::clicked, this, &FrameAnimationDialog::onBatchImportFiles);
|
||||
connect(m_btnImportFolder, &QPushButton::clicked, this, &FrameAnimationDialog::onBatchImportFolder);
|
||||
connect(m_btnAiGenerate, &QPushButton::clicked, this, &FrameAnimationDialog::onGenerateAiFrames);
|
||||
|
||||
rebuildFrameList();
|
||||
if (m_list->count() > 0) {
|
||||
@@ -118,16 +145,11 @@ void FrameAnimationDialog::rebuildFrameList() {
|
||||
|
||||
// 默认贴图(用于 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) {
|
||||
const auto spriteFrames = m_workspace.entitySpriteFrames(m_entityId);
|
||||
for (const auto& k : spriteFrames) {
|
||||
if (k.frame == f) {
|
||||
hasCustom = true;
|
||||
break;
|
||||
@@ -159,38 +181,33 @@ void FrameAnimationDialog::updatePreviewForFrame(int frame) {
|
||||
if (!hit) return;
|
||||
|
||||
const QString abs = resolvedImageAbsForFrame(m_workspace, *hit, frame);
|
||||
if (abs.isEmpty() || !QFileInfo::exists(abs)) {
|
||||
if (abs.isEmpty()) {
|
||||
m_preview->setText(QStringLiteral("无图像"));
|
||||
return;
|
||||
}
|
||||
QPixmap pm(abs);
|
||||
if (pm.isNull()) {
|
||||
QImage loaded;
|
||||
if (abs.startsWith(QStringLiteral("pngb64:"))) {
|
||||
const QByteArray b64 = abs.mid(QStringLiteral("pngb64:").size()).toLatin1();
|
||||
const QByteArray raw = QByteArray::fromBase64(b64);
|
||||
loaded.loadFromData(raw, "PNG");
|
||||
} else {
|
||||
if (!QFileInfo::exists(abs)) {
|
||||
m_preview->setText(QStringLiteral("无图像"));
|
||||
return;
|
||||
}
|
||||
loaded = core::image_file::loadImageLimited(abs, core::image_decode::kPreviewMaxPixels);
|
||||
}
|
||||
if (loaded.isNull()) {
|
||||
m_preview->setText(QStringLiteral("加载失败"));
|
||||
return;
|
||||
}
|
||||
QPixmap pm = QPixmap::fromImage(loaded);
|
||||
m_preview->setPixmap(pm.scaled(m_preview->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
bool FrameAnimationDialog::applyImageToFrame(int frame, const QString& absImagePath) {
|
||||
// Qt 默认的 image allocation limit 较小,超大分辨率图可能会被拒绝。
|
||||
// 这里提高 limit,并对极端大图按像素数上限自动缩放,避免 OOM。
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QImageReader::setAllocationLimit(1024); // MB
|
||||
#endif
|
||||
QImageReader reader(absImagePath);
|
||||
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));
|
||||
}
|
||||
}
|
||||
QImage img = reader.read();
|
||||
QImage img =
|
||||
core::image_file::loadImageLimited(absImagePath, core::image_decode::kWorkspaceMaxPixels);
|
||||
if (img.isNull()) {
|
||||
return false;
|
||||
}
|
||||
@@ -200,6 +217,93 @@ bool FrameAnimationDialog::applyImageToFrame(int frame, const QString& absImageP
|
||||
return m_workspace.setEntityImageFrame(m_entityId, frame, img);
|
||||
}
|
||||
|
||||
QVector<int> FrameAnimationDialog::targetFramesForWrite(int requestedCount) const {
|
||||
QVector<int> frames;
|
||||
if (m_list) {
|
||||
const auto selected = m_list->selectedItems();
|
||||
frames.reserve(selected.size());
|
||||
for (auto* it : selected) {
|
||||
if (it) frames.push_back(it->data(Qt::UserRole).toInt());
|
||||
}
|
||||
std::sort(frames.begin(), frames.end());
|
||||
frames.erase(std::unique(frames.begin(), frames.end()), frames.end());
|
||||
}
|
||||
if (!frames.isEmpty()) {
|
||||
if (requestedCount > 0 && frames.size() > requestedCount) {
|
||||
frames.resize(requestedCount);
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
const int count = requestedCount > 0 ? std::min(requestedCount, m_end - m_start + 1)
|
||||
: (m_end - m_start + 1);
|
||||
frames.reserve(count);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
frames.push_back(m_start + i);
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
QByteArray FrameAnimationDialog::currentEntityPngBytes(QString* outError) const {
|
||||
if (outError) outError->clear();
|
||||
if (!m_workspace.isOpen()) {
|
||||
if (outError) *outError = QStringLiteral("未打开项目。");
|
||||
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) {
|
||||
if (outError) *outError = QStringLiteral("未找到当前实体。");
|
||||
return {};
|
||||
}
|
||||
|
||||
int sourceFrame = m_start;
|
||||
if (m_list && m_list->currentItem()) {
|
||||
sourceFrame = m_list->currentItem()->data(Qt::UserRole).toInt();
|
||||
}
|
||||
|
||||
const QString abs = resolvedImageAbsForFrame(m_workspace, *hit, sourceFrame);
|
||||
if (abs.isEmpty()) {
|
||||
if (outError) *outError = QStringLiteral("当前实体没有可用的角色贴图。");
|
||||
return {};
|
||||
}
|
||||
|
||||
QImage img;
|
||||
if (abs.startsWith(QStringLiteral("pngb64:"))) {
|
||||
const QByteArray b64 = abs.mid(QStringLiteral("pngb64:").size()).toLatin1();
|
||||
const QByteArray raw = QByteArray::fromBase64(b64);
|
||||
img.loadFromData(raw, "PNG");
|
||||
} else {
|
||||
if (!QFileInfo::exists(abs)) {
|
||||
if (outError) *outError = QStringLiteral("当前实体没有可用的角色贴图。");
|
||||
return {};
|
||||
}
|
||||
img = core::image_file::loadImageLimited(abs, core::image_decode::kWorkspaceMaxPixels);
|
||||
}
|
||||
if (img.isNull()) {
|
||||
if (outError) *outError = QStringLiteral("角色贴图读取失败。");
|
||||
return {};
|
||||
}
|
||||
if (img.format() != QImage::Format_ARGB32 && img.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
img = img.convertToFormat(QImage::Format_ARGB32);
|
||||
}
|
||||
|
||||
QByteArray png;
|
||||
QBuffer buf(&png);
|
||||
if (!buf.open(QIODevice::WriteOnly) || !img.save(&buf, "PNG")) {
|
||||
if (outError) *outError = QStringLiteral("角色贴图编码 PNG 失败。");
|
||||
return {};
|
||||
}
|
||||
return png;
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onReplaceCurrentFrame() {
|
||||
auto* it = m_list->currentItem();
|
||||
if (!it) return;
|
||||
@@ -222,15 +326,17 @@ void FrameAnimationDialog::onReplaceCurrentFrame() {
|
||||
}
|
||||
if (hit) {
|
||||
// 是否已有精确关键帧
|
||||
for (const auto& k : hit->imageFrames) {
|
||||
const auto spriteFrames = m_workspace.entitySpriteFrames(m_entityId);
|
||||
for (const auto& k : spriteFrames) {
|
||||
if (k.frame == f + 1) {
|
||||
hasExplicitKeyAtFPlus1 = true;
|
||||
}
|
||||
}
|
||||
prevRelPathForF = core::sampleImagePath(hit->imageFrames, f, hit->imagePath);
|
||||
prevRelPathForFPlus1 = core::sampleImagePath(hit->imageFrames, f + 1, hit->imagePath);
|
||||
prevRelPathForF = m_workspace.entityImagePathAt(m_entityId, f);
|
||||
prevRelPathForFPlus1 = m_workspace.entityImagePathAt(m_entityId, f + 1);
|
||||
}
|
||||
}
|
||||
(void)prevRelPathForF;
|
||||
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
@@ -255,14 +361,12 @@ void FrameAnimationDialog::onReplaceCurrentFrame() {
|
||||
}
|
||||
|
||||
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;
|
||||
const QVector<int> frames = targetFramesForWrite(0);
|
||||
for (int f : frames) {
|
||||
m_workspace.removeEntityImageFrame(m_entityId, f);
|
||||
}
|
||||
rebuildFrameList();
|
||||
updatePreviewForFrame(f);
|
||||
onSelectFrame();
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onBatchImportFiles() {
|
||||
@@ -274,10 +378,10 @@ void FrameAnimationDialog::onBatchImportFiles() {
|
||||
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()));
|
||||
const QVector<int> targets = targetFramesForWrite(static_cast<int>(sorted.size()));
|
||||
const int count = std::min(static_cast<int>(targets.size()), static_cast<int>(sorted.size()));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
applyImageToFrame(m_start + i, sorted[i]);
|
||||
applyImageToFrame(targets[i], sorted[i]);
|
||||
}
|
||||
rebuildFrameList();
|
||||
onSelectFrame();
|
||||
@@ -296,12 +400,213 @@ void FrameAnimationDialog::onBatchImportFolder() {
|
||||
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()));
|
||||
const QVector<int> targets = targetFramesForWrite(static_cast<int>(files.size()));
|
||||
const int count = std::min(static_cast<int>(targets.size()), static_cast<int>(files.size()));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
applyImageToFrame(m_start + i, d.filePath(files[i]));
|
||||
applyImageToFrame(targets[i], d.filePath(files[i]));
|
||||
}
|
||||
rebuildFrameList();
|
||||
onSelectFrame();
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onGenerateAiFrames() {
|
||||
const QVector<int> targetFrames = targetFramesForWrite(0);
|
||||
const int frameCount = static_cast<int>(targetFrames.size());
|
||||
if (frameCount <= 0) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("当前区间无有效帧。"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString imageErr;
|
||||
const QByteArray characterPng = currentEntityPngBytes(&imageErr);
|
||||
if (characterPng.isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
imageErr.isEmpty() ? QStringLiteral("无法读取当前实体贴图。") : imageErr);
|
||||
return;
|
||||
}
|
||||
|
||||
QDialog cfg(this);
|
||||
cfg.setWindowTitle(QStringLiteral("AI 生成实体动画"));
|
||||
cfg.setModal(true);
|
||||
auto* root = new QVBoxLayout(&cfg);
|
||||
root->setContentsMargins(14, 14, 14, 14);
|
||||
root->setSpacing(10);
|
||||
|
||||
auto* form = new QFormLayout();
|
||||
root->addLayout(form);
|
||||
|
||||
auto* promptEdit = new QLineEdit(QStringLiteral("walk cycle"), &cfg);
|
||||
form->addRow(QStringLiteral("动画提示词"), promptEdit);
|
||||
|
||||
auto* bgRow = new QWidget(&cfg);
|
||||
auto* bgLayout = new QHBoxLayout(bgRow);
|
||||
bgLayout->setContentsMargins(0, 0, 0, 0);
|
||||
auto* bgEdit = new QLineEdit(QStringLiteral("#00FF00"), bgRow);
|
||||
auto* bgBtn = new QPushButton(QStringLiteral("选择…"), bgRow);
|
||||
bgLayout->addWidget(bgEdit, 1);
|
||||
bgLayout->addWidget(bgBtn);
|
||||
form->addRow(QStringLiteral("生成背景色"), bgRow);
|
||||
|
||||
auto* frameLabel = new QLabel(QString::number(frameCount), &cfg);
|
||||
form->addRow(QStringLiteral("帧数"), frameLabel);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &cfg);
|
||||
buttons->button(QDialogButtonBox::Ok)->setText(QStringLiteral("开始生成"));
|
||||
buttons->button(QDialogButtonBox::Cancel)->setText(QStringLiteral("取消"));
|
||||
root->addWidget(buttons);
|
||||
|
||||
connect(bgBtn, &QPushButton::clicked, &cfg, [bgEdit, &cfg]() {
|
||||
const QColor initial(bgEdit->text().trimmed());
|
||||
const QColor c = QColorDialog::getColor(initial.isValid() ? initial : QColor(0, 255, 0),
|
||||
&cfg,
|
||||
QStringLiteral("选择纯色背景"));
|
||||
if (c.isValid()) {
|
||||
bgEdit->setText(c.name(QColor::HexRgb).toUpper());
|
||||
}
|
||||
});
|
||||
connect(buttons, &QDialogButtonBox::accepted, &cfg, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &cfg, &QDialog::reject);
|
||||
|
||||
if (cfg.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
const QString prompt = promptEdit->text().trimmed();
|
||||
if (prompt.isEmpty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("动画提示词不能为空。"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString base;
|
||||
const QByteArray env = qgetenv("MODEL_SERVER_URL");
|
||||
base = env.isEmpty() ? QStringLiteral("http://127.0.0.1:8000") : QString::fromUtf8(env);
|
||||
|
||||
auto* client = new core::ModelServerClient(this);
|
||||
client->setBaseUrl(QUrl(base));
|
||||
|
||||
QString immediateErr;
|
||||
QNetworkReply* reply = client->animateCharacterSequenceAsync(
|
||||
characterPng,
|
||||
prompt,
|
||||
QStringLiteral("blur, distortion, extra limbs, bad hands, text, watermark"),
|
||||
bgEdit->text(),
|
||||
40,
|
||||
frameCount,
|
||||
25,
|
||||
8.0,
|
||||
768,
|
||||
-1,
|
||||
&immediateErr);
|
||||
if (!reply) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
immediateErr.isEmpty() ? QStringLiteral("无法发起后端请求。") : immediateErr);
|
||||
client->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
auto* task = new CancelableTaskDialog(
|
||||
QStringLiteral("AI 生成实体动画"),
|
||||
QStringLiteral("正在生成 PNG 序列。该过程可能耗时较久,请稍候……"),
|
||||
this);
|
||||
task->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
connect(task, &CancelableTaskDialog::canceled, this, [reply, task]() {
|
||||
if (reply) reply->abort();
|
||||
if (task) task->reject();
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, task, client, frameCount, targetFrames]() {
|
||||
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QByteArray raw = reply->readAll();
|
||||
const auto netErr = reply->error();
|
||||
const QString netErrStr = reply->errorString();
|
||||
|
||||
reply->deleteLater();
|
||||
client->deleteLater();
|
||||
if (task) task->close();
|
||||
|
||||
if (netErr != QNetworkReply::NoError) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
QStringLiteral("网络错误:%1").arg(netErrStr));
|
||||
return;
|
||||
}
|
||||
if (httpStatus != 200) {
|
||||
QString detail;
|
||||
const QJsonDocument jerr = QJsonDocument::fromJson(raw);
|
||||
if (jerr.isObject()) {
|
||||
detail = jerr.object().value(QStringLiteral("detail")).toString();
|
||||
}
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
detail.isEmpty() ? QStringLiteral("后端返回 HTTP %1。").arg(httpStatus)
|
||||
: QStringLiteral("后端错误(HTTP %1):%2").arg(httpStatus).arg(detail));
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonDocument jd = QJsonDocument::fromJson(raw);
|
||||
if (!jd.isObject()) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("响应不是 JSON。"));
|
||||
return;
|
||||
}
|
||||
const QJsonObject obj = jd.object();
|
||||
if (!obj.value(QStringLiteral("success")).toBool()) {
|
||||
const QString err = obj.value(QStringLiteral("error")).toString();
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
err.isEmpty() ? QStringLiteral("生成失败。") : err);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonArray frames = obj.value(QStringLiteral("frames")).toArray();
|
||||
if (frames.isEmpty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("后端未返回 PNG 帧。"));
|
||||
return;
|
||||
}
|
||||
|
||||
int applied = 0;
|
||||
const int count = std::min(frameCount, static_cast<int>(frames.size()));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const QJsonObject fobj = frames.at(i).toObject();
|
||||
const QString b64 = fobj.value(QStringLiteral("image_b64")).toString();
|
||||
if (b64.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QImage img;
|
||||
img.loadFromData(QByteArray::fromBase64(b64.toLatin1()), "PNG");
|
||||
if (img.isNull()) {
|
||||
continue;
|
||||
}
|
||||
if (img.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
if (i < targetFrames.size() && m_workspace.setEntityImageFrame(m_entityId, targetFrames[i], img)) {
|
||||
++applied;
|
||||
}
|
||||
}
|
||||
|
||||
rebuildFrameList();
|
||||
if (m_list && m_list->count() > 0) {
|
||||
m_list->setCurrentRow(0);
|
||||
}
|
||||
onSelectFrame();
|
||||
|
||||
if (applied <= 0) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("生成成功,但写入项目失败。"));
|
||||
return;
|
||||
}
|
||||
if (applied < frameCount) {
|
||||
QMessageBox::information(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
QStringLiteral("已写入 %1/%2 帧;部分帧解析或保存失败。").arg(applied).arg(frameCount));
|
||||
} else {
|
||||
QMessageBox::information(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
QStringLiteral("已写入 %1 帧。").arg(applied));
|
||||
}
|
||||
});
|
||||
|
||||
task->show();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
namespace core {
|
||||
class ProjectWorkspace;
|
||||
@@ -27,11 +28,14 @@ private slots:
|
||||
void onClearCurrentFrame();
|
||||
void onBatchImportFiles();
|
||||
void onBatchImportFolder();
|
||||
void onGenerateAiFrames();
|
||||
|
||||
private:
|
||||
void rebuildFrameList();
|
||||
void updatePreviewForFrame(int frame);
|
||||
bool applyImageToFrame(int frame, const QString& absImagePath);
|
||||
QVector<int> targetFramesForWrite(int requestedCount) const;
|
||||
QByteArray currentEntityPngBytes(QString* outError) const;
|
||||
|
||||
private:
|
||||
core::ProjectWorkspace& m_workspace;
|
||||
@@ -46,6 +50,7 @@ private:
|
||||
QPushButton* m_btnClear = nullptr;
|
||||
QPushButton* m_btnImportFiles = nullptr;
|
||||
QPushButton* m_btnImportFolder = nullptr;
|
||||
QPushButton* m_btnAiGenerate = nullptr;
|
||||
|
||||
QString m_defaultImageAbs;
|
||||
};
|
||||
|
||||
@@ -1,64 +1,122 @@
|
||||
#include "dialogs/ImageCropDialog.h"
|
||||
|
||||
#include "core/image/ImageDecodeConfig.h"
|
||||
#include "core/image/ImageFileLoader.h"
|
||||
|
||||
#include <QBoxLayout>
|
||||
#include <QCursor>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QImageReader>
|
||||
#include <QResizeEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QtMath>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
# define CROP_LOCAL_POS_M(E) (E)->position()
|
||||
# define CROP_LOCAL_POS_W(E) (E)->position()
|
||||
#else
|
||||
# define CROP_LOCAL_POS_M(E) (E)->localPos()
|
||||
# define CROP_LOCAL_POS_W(E) (E)->posF()
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kMinCropSidePx = 2;
|
||||
/// 相对「适应窗口」倍率的乘子;范围极大以便实质任意缩放(仍避免数值溢出)
|
||||
constexpr qreal kZoomMulMin = 1e-12;
|
||||
constexpr qreal kZoomMulMax = 1e12;
|
||||
constexpr qreal kHandleViewPx = 7.0;
|
||||
|
||||
enum class HitPart : quint8 {
|
||||
None,
|
||||
Inside,
|
||||
N,
|
||||
Ne,
|
||||
E,
|
||||
Se,
|
||||
S,
|
||||
Sw,
|
||||
W,
|
||||
Nw,
|
||||
/// 平移视图(选区外左键拖动)
|
||||
PanView
|
||||
};
|
||||
|
||||
QRect clampRectToBounds(QRect r, const QRect& bounds) {
|
||||
r = r.normalized();
|
||||
if (r.width() < kMinCropSidePx || r.height() < kMinCropSidePx) {
|
||||
return {};
|
||||
}
|
||||
if (r.left() < bounds.left()) {
|
||||
r.moveLeft(bounds.left());
|
||||
}
|
||||
if (r.top() < bounds.top()) {
|
||||
r.moveTop(bounds.top());
|
||||
}
|
||||
if (r.right() > bounds.right()) {
|
||||
r.moveRight(bounds.right());
|
||||
}
|
||||
if (r.bottom() > bounds.bottom()) {
|
||||
r.moveBottom(bounds.bottom());
|
||||
}
|
||||
if (r.width() < kMinCropSidePx || r.height() < kMinCropSidePx) {
|
||||
return {};
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ImageCropDialog::CropView final : public QWidget {
|
||||
public:
|
||||
explicit CropView(QWidget* parent = nullptr)
|
||||
: QWidget(parent) {
|
||||
setMouseTracking(true);
|
||||
setFocusPolicy(Qt::WheelFocus);
|
||||
setMinimumSize(480, 320);
|
||||
}
|
||||
|
||||
void setImage(const QImage& img) {
|
||||
m_image = img;
|
||||
m_selection = {};
|
||||
m_zoom = 1.0;
|
||||
m_pan = QPointF();
|
||||
m_selImage = m_image.isNull() ? QRect() : QRect(QPoint(0, 0), m_image.size());
|
||||
m_dragPart = HitPart::None;
|
||||
updateGeometry();
|
||||
update();
|
||||
}
|
||||
|
||||
bool hasSelection() const { return !m_selection.isNull() && m_selection.width() > 0 && m_selection.height() > 0; }
|
||||
bool hasSelection() const {
|
||||
return !m_image.isNull() && m_selImage.width() >= kMinCropSidePx && m_selImage.height() >= kMinCropSidePx;
|
||||
}
|
||||
|
||||
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;
|
||||
return m_selImage.normalized().intersected(QRect(0, 0, m_image.width(), m_image.height()));
|
||||
}
|
||||
|
||||
void resetSelection() {
|
||||
m_selection = {};
|
||||
if (!m_image.isNull()) {
|
||||
m_selImage = QRect(QPoint(0, 0), m_image.size());
|
||||
} else {
|
||||
m_selImage = {};
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* e) override {
|
||||
QWidget::resizeEvent(e);
|
||||
update();
|
||||
}
|
||||
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), palette().window());
|
||||
@@ -69,84 +127,362 @@ protected:
|
||||
return;
|
||||
}
|
||||
|
||||
const auto map = viewToImageTransform();
|
||||
qreal eff = 0;
|
||||
QPointF tl;
|
||||
computeLayout(eff, tl);
|
||||
|
||||
const QTransform imgToView = imageToViewTransform(eff, tl);
|
||||
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
p.setTransform(map);
|
||||
p.setTransform(imgToView);
|
||||
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);
|
||||
const QRectF selView = imgToView.mapRect(QRectF(m_selImage));
|
||||
const QRect sel = selView.toAlignedRect().intersected(rect());
|
||||
|
||||
// 上
|
||||
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);
|
||||
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);
|
||||
p.setPen(QPen(QColor(255, 255, 255, 230), 2));
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawRect(sel);
|
||||
|
||||
const qreal hsz = kHandleViewPx;
|
||||
const QPointF c[4] = {
|
||||
selView.topLeft(),
|
||||
selView.topRight(),
|
||||
selView.bottomRight(),
|
||||
selView.bottomLeft(),
|
||||
};
|
||||
p.setPen(QPen(QColor(255, 255, 255, 240), 1));
|
||||
p.setBrush(QColor(255, 255, 255, 220));
|
||||
for (const QPointF& pt : c) {
|
||||
p.drawRect(QRectF(pt.x() - hsz * 0.5, pt.y() - hsz * 0.5, hsz, hsz));
|
||||
}
|
||||
}
|
||||
|
||||
void wheelEvent(QWheelEvent* e) override {
|
||||
if (m_image.isNull()) {
|
||||
e->ignore();
|
||||
return;
|
||||
}
|
||||
qreal effOld = 0;
|
||||
QPointF tlOld;
|
||||
computeLayout(effOld, tlOld);
|
||||
|
||||
const QPointF cursor = CROP_LOCAL_POS_W(e);
|
||||
const QPointF imgPt((cursor.x() - tlOld.x()) / effOld, (cursor.y() - tlOld.y()) / effOld);
|
||||
|
||||
const qreal steps = e->angleDelta().y() / 120.0;
|
||||
const qreal factor = std::pow(1.1, steps);
|
||||
const qreal newZoom = std::clamp(m_zoom * factor, kZoomMulMin, kZoomMulMax);
|
||||
if (qFuzzyCompare(newZoom, m_zoom)) {
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
m_zoom = newZoom;
|
||||
|
||||
qreal effNew = 0;
|
||||
QPointF tlIgnored;
|
||||
computeLayout(effNew, tlIgnored);
|
||||
|
||||
const QPointF tlNew(cursor.x() - imgPt.x() * effNew, cursor.y() - imgPt.y() * effNew);
|
||||
const qreal iw = m_image.width();
|
||||
const qreal ih = m_image.height();
|
||||
const qreal vw = width();
|
||||
const qreal vh = height();
|
||||
const QPointF centerOff((vw - iw * effNew) / 2.0, (vh - ih * effNew) / 2.0);
|
||||
m_pan = tlNew - centerOff;
|
||||
|
||||
e->accept();
|
||||
update();
|
||||
}
|
||||
|
||||
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();
|
||||
qreal eff = 0;
|
||||
QPointF tl;
|
||||
computeLayout(eff, tl);
|
||||
const QPointF viewPos = CROP_LOCAL_POS_M(e);
|
||||
const QPointF img = viewToImage(viewPos, eff, tl);
|
||||
|
||||
m_dragPart = hitTest(img, eff);
|
||||
if (m_dragPart == HitPart::None) {
|
||||
m_dragPart = HitPart::PanView;
|
||||
}
|
||||
if (m_dragPart == HitPart::PanView) {
|
||||
m_pressView = viewPos;
|
||||
m_panAtPress = m_pan;
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
return;
|
||||
}
|
||||
m_pressImg = img;
|
||||
m_startSel = m_selImage;
|
||||
updateCursorForHover(viewPos, eff, tl);
|
||||
}
|
||||
|
||||
void mouseMoveEvent(QMouseEvent* e) override {
|
||||
if (!m_dragging) {
|
||||
if (m_image.isNull()) {
|
||||
return;
|
||||
}
|
||||
const QPoint cur = e->position().toPoint();
|
||||
m_selection = QRect(m_anchor, cur).normalized();
|
||||
update();
|
||||
qreal eff = 0;
|
||||
QPointF tl;
|
||||
computeLayout(eff, tl);
|
||||
const QPointF viewPos = CROP_LOCAL_POS_M(e);
|
||||
|
||||
if ((e->buttons() & Qt::LeftButton) && m_dragPart == HitPart::PanView) {
|
||||
m_pan = m_panAtPress + (viewPos - m_pressView);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e->buttons() & Qt::LeftButton) && m_dragPart != HitPart::None && m_dragPart != HitPart::PanView) {
|
||||
const QPointF img = viewToImage(viewPos, eff, tl);
|
||||
applyDrag(m_dragPart, m_pressImg, img, m_startSel);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
updateCursorForHover(viewPos, eff, tl);
|
||||
}
|
||||
|
||||
void mouseReleaseEvent(QMouseEvent* e) override {
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
m_dragPart = HitPart::None;
|
||||
unsetCursor();
|
||||
}
|
||||
m_dragging = false;
|
||||
update();
|
||||
QWidget::mouseReleaseEvent(e);
|
||||
}
|
||||
|
||||
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;
|
||||
void computeLayout(qreal& outEffScale, QPointF& outTopLeft) const {
|
||||
const qreal vw = width();
|
||||
const qreal vh = height();
|
||||
const qreal iw = m_image.width();
|
||||
const qreal ih = m_image.height();
|
||||
if (iw <= 0 || ih <= 0 || vw <= 1 || vh <= 1) {
|
||||
outEffScale = 1.0;
|
||||
outTopLeft = QPointF();
|
||||
return;
|
||||
}
|
||||
const qreal fit = std::min(vw / iw, vh / ih);
|
||||
const qreal eff = fit * m_zoom;
|
||||
outEffScale = eff;
|
||||
outTopLeft = m_pan + QPointF((vw - iw * eff) / 2.0, (vh - ih * eff) / 2.0);
|
||||
}
|
||||
|
||||
QTransform imageToViewTransform(qreal eff, const QPointF& tl) const {
|
||||
QTransform t;
|
||||
t.translate(offsetX, offsetY);
|
||||
t.scale(s, s);
|
||||
t.translate(tl.x(), tl.y());
|
||||
t.scale(eff, eff);
|
||||
return t;
|
||||
}
|
||||
|
||||
QPointF viewToImage(const QPointF& viewPt, qreal eff, const QPointF& tl) const {
|
||||
return QPointF((viewPt.x() - tl.x()) / eff, (viewPt.y() - tl.y()) / eff);
|
||||
}
|
||||
|
||||
qreal marginImage(qreal eff) const {
|
||||
return std::max<qreal>(kMinCropSidePx, kHandleViewPx / std::max(eff, 1e-6));
|
||||
}
|
||||
|
||||
HitPart hitTest(const QPointF& img, qreal eff) const {
|
||||
const qreal iw = m_image.width();
|
||||
const qreal ih = m_image.height();
|
||||
const QRectF boundsF(0, 0, iw, ih);
|
||||
if (!boundsF.contains(img)) {
|
||||
return HitPart::PanView;
|
||||
}
|
||||
const qreal m = marginImage(eff);
|
||||
const QRect r = m_selImage.normalized();
|
||||
|
||||
const bool onN = std::abs(img.y() - r.top()) <= m && img.x() >= r.left() - m && img.x() <= r.right() + m;
|
||||
const bool onS = std::abs(img.y() - r.bottom()) <= m && img.x() >= r.left() - m && img.x() <= r.right() + m;
|
||||
const bool onW = std::abs(img.x() - r.left()) <= m && img.y() >= r.top() - m && img.y() <= r.bottom() + m;
|
||||
const bool onE = std::abs(img.x() - r.right()) <= m && img.y() >= r.top() - m && img.y() <= r.bottom() + m;
|
||||
|
||||
const bool cnw = std::abs(img.x() - r.left()) <= m && std::abs(img.y() - r.top()) <= m;
|
||||
const bool cne = std::abs(img.x() - r.right()) <= m && std::abs(img.y() - r.top()) <= m;
|
||||
const bool cse = std::abs(img.x() - r.right()) <= m && std::abs(img.y() - r.bottom()) <= m;
|
||||
const bool csw = std::abs(img.x() - r.left()) <= m && std::abs(img.y() - r.bottom()) <= m;
|
||||
|
||||
if (cnw) {
|
||||
return HitPart::Nw;
|
||||
}
|
||||
if (cne) {
|
||||
return HitPart::Ne;
|
||||
}
|
||||
if (cse) {
|
||||
return HitPart::Se;
|
||||
}
|
||||
if (csw) {
|
||||
return HitPart::Sw;
|
||||
}
|
||||
if (onN && !onW && !onE) {
|
||||
return HitPart::N;
|
||||
}
|
||||
if (onS && !onW && !onE) {
|
||||
return HitPart::S;
|
||||
}
|
||||
if (onW && !onN && !onS) {
|
||||
return HitPart::W;
|
||||
}
|
||||
if (onE && !onN && !onS) {
|
||||
return HitPart::E;
|
||||
}
|
||||
const QRect fullImage(0, 0, m_image.width(), m_image.height());
|
||||
if (r.contains(img.toPoint())) {
|
||||
// 选区为整图时没有「可平移的选区」,框内左键用于平移视图
|
||||
if (r.normalized() == fullImage.normalized()) {
|
||||
return HitPart::PanView;
|
||||
}
|
||||
return HitPart::Inside;
|
||||
}
|
||||
return HitPart::PanView;
|
||||
}
|
||||
|
||||
void updateCursorForHover(const QPointF& viewPt, qreal eff, const QPointF& tl) {
|
||||
if (m_dragPart != HitPart::None) {
|
||||
return;
|
||||
}
|
||||
const QPointF img = viewToImage(viewPt, eff, tl);
|
||||
const HitPart h = hitTest(img, eff);
|
||||
switch (h) {
|
||||
case HitPart::PanView:
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
break;
|
||||
case HitPart::Inside:
|
||||
setCursor(Qt::SizeAllCursor);
|
||||
break;
|
||||
case HitPart::N:
|
||||
setCursor(Qt::SizeVerCursor);
|
||||
break;
|
||||
case HitPart::S:
|
||||
setCursor(Qt::SizeVerCursor);
|
||||
break;
|
||||
case HitPart::W:
|
||||
setCursor(Qt::SizeHorCursor);
|
||||
break;
|
||||
case HitPart::E:
|
||||
setCursor(Qt::SizeHorCursor);
|
||||
break;
|
||||
case HitPart::Nw:
|
||||
case HitPart::Se:
|
||||
setCursor(Qt::SizeFDiagCursor);
|
||||
break;
|
||||
case HitPart::Ne:
|
||||
case HitPart::Sw:
|
||||
setCursor(Qt::SizeBDiagCursor);
|
||||
break;
|
||||
default:
|
||||
unsetCursor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void applyDrag(HitPart part, const QPointF& pressImg, const QPointF& curImg, const QRect& start) {
|
||||
const QRect bounds(0, 0, m_image.width(), m_image.height());
|
||||
QRect r = start.normalized();
|
||||
const int L = r.left();
|
||||
const int T = r.top();
|
||||
const int R = r.right();
|
||||
const int B = r.bottom();
|
||||
|
||||
auto clampX = [&](int x) { return std::clamp(x, bounds.left(), bounds.right()); };
|
||||
auto clampY = [&](int y) { return std::clamp(y, bounds.top(), bounds.bottom()); };
|
||||
|
||||
switch (part) {
|
||||
case HitPart::Inside: {
|
||||
const QPoint delta = (curImg - pressImg).toPoint();
|
||||
r.translate(delta);
|
||||
r = clampRectToBounds(r, bounds);
|
||||
break;
|
||||
}
|
||||
case HitPart::N: {
|
||||
int nt = clampY(int(std::round(curImg.y())));
|
||||
nt = std::min(nt, B - kMinCropSidePx);
|
||||
r = QRect(L, nt, R - L + 1, B - nt + 1);
|
||||
break;
|
||||
}
|
||||
case HitPart::S: {
|
||||
int nb = clampY(int(std::round(curImg.y())));
|
||||
nb = std::max(nb, T + kMinCropSidePx);
|
||||
r = QRect(L, T, R - L + 1, nb - T + 1);
|
||||
break;
|
||||
}
|
||||
case HitPart::W: {
|
||||
int nl = clampX(int(std::round(curImg.x())));
|
||||
nl = std::min(nl, R - kMinCropSidePx);
|
||||
r = QRect(nl, T, R - nl + 1, B - T + 1);
|
||||
break;
|
||||
}
|
||||
case HitPart::E: {
|
||||
int nr = clampX(int(std::round(curImg.x())));
|
||||
nr = std::max(nr, L + kMinCropSidePx);
|
||||
r = QRect(L, T, nr - L + 1, B - T + 1);
|
||||
break;
|
||||
}
|
||||
case HitPart::Nw: {
|
||||
int nl = clampX(int(std::round(curImg.x())));
|
||||
int nt = clampY(int(std::round(curImg.y())));
|
||||
nl = std::min(nl, R - kMinCropSidePx);
|
||||
nt = std::min(nt, B - kMinCropSidePx);
|
||||
r = QRect(QPoint(nl, nt), QPoint(R, B)).normalized();
|
||||
break;
|
||||
}
|
||||
case HitPart::Ne: {
|
||||
int nr = clampX(int(std::round(curImg.x())));
|
||||
int nt = clampY(int(std::round(curImg.y())));
|
||||
nr = std::max(nr, L + kMinCropSidePx);
|
||||
nt = std::min(nt, B - kMinCropSidePx);
|
||||
r = QRect(QPoint(L, nt), QPoint(nr, B)).normalized();
|
||||
break;
|
||||
}
|
||||
case HitPart::Se: {
|
||||
int nr = clampX(int(std::round(curImg.x())));
|
||||
int nb = clampY(int(std::round(curImg.y())));
|
||||
nr = std::max(nr, L + kMinCropSidePx);
|
||||
nb = std::max(nb, T + kMinCropSidePx);
|
||||
r = QRect(QPoint(L, T), QPoint(nr, nb)).normalized();
|
||||
break;
|
||||
}
|
||||
case HitPart::Sw: {
|
||||
int nl = clampX(int(std::round(curImg.x())));
|
||||
int nb = clampY(int(std::round(curImg.y())));
|
||||
nl = std::min(nl, R - kMinCropSidePx);
|
||||
nb = std::max(nb, T + kMinCropSidePx);
|
||||
r = QRect(QPoint(nl, T), QPoint(R, nb)).normalized();
|
||||
break;
|
||||
}
|
||||
case HitPart::PanView:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
m_selImage = clampRectToBounds(r, bounds);
|
||||
if (m_selImage.isEmpty()) {
|
||||
m_selImage = start;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QImage m_image;
|
||||
bool m_dragging = false;
|
||||
QPoint m_anchor;
|
||||
QRect m_selection;
|
||||
QRect m_selImage;
|
||||
double m_zoom = 1.0;
|
||||
QPointF m_pan;
|
||||
|
||||
HitPart m_dragPart = HitPart::None;
|
||||
QPointF m_pressImg;
|
||||
QRect m_startSel;
|
||||
QPointF m_pressView;
|
||||
QPointF m_panAtPress;
|
||||
};
|
||||
|
||||
ImageCropDialog::ImageCropDialog(const QString& imagePath, QWidget* parent)
|
||||
@@ -160,34 +496,30 @@ ImageCropDialog::ImageCropDialog(const QString& imagePath, QWidget* parent)
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
if (!core::image_file::probeImagePixelSize(m_imagePath, &m_sourceLogicalSize)) {
|
||||
m_sourceLogicalSize = QSize();
|
||||
}
|
||||
m_image = reader.read();
|
||||
m_image =
|
||||
core::image_file::loadImageLimited(m_imagePath, core::image_decode::kPreviewMaxPixels);
|
||||
if (m_image.isNull()) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
if (!m_sourceLogicalSize.isValid() || m_sourceLogicalSize.width() <= 0 ||
|
||||
m_sourceLogicalSize.height() <= 0) {
|
||||
m_sourceLogicalSize = m_image.size();
|
||||
}
|
||||
}
|
||||
|
||||
void ImageCropDialog::rebuildUi() {
|
||||
auto* root = new QVBoxLayout(this);
|
||||
|
||||
auto* hint = new QLabel(QStringLiteral("拖拽选择裁剪区域(不选则使用整张图)。"), this);
|
||||
auto* hint = new QLabel(
|
||||
QStringLiteral(
|
||||
"默认已框选整张图。滚轮可连续缩放;拖拽边或角调整裁剪,框内拖拽移动选区;选区外或留白处左键拖动平移视图。「重置」恢复整图选区。"
|
||||
" 若已链接 libvips,超大图用流式解码与预览;否则回退 Qt。确定裁剪后坐标按原图逻辑像素对齐。"),
|
||||
this);
|
||||
hint->setWordWrap(true);
|
||||
root->addWidget(hint);
|
||||
|
||||
m_view = new CropView(this);
|
||||
@@ -196,7 +528,7 @@ void ImageCropDialog::rebuildUi() {
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
m_okButton = buttons->button(QDialogButtonBox::Ok);
|
||||
auto* resetBtn = new QPushButton(QStringLiteral("重置选择"), this);
|
||||
auto* resetBtn = new QPushButton(QStringLiteral("重置"), this);
|
||||
buttons->addButton(resetBtn, QDialogButtonBox::ActionRole);
|
||||
|
||||
connect(resetBtn, &QPushButton::clicked, this, &ImageCropDialog::onReset);
|
||||
@@ -213,7 +545,14 @@ QRect ImageCropDialog::selectedRectInImagePixels() const {
|
||||
if (!m_view) {
|
||||
return {};
|
||||
}
|
||||
return m_view->selectionInImagePixels();
|
||||
const QRect inPreview = m_view->selectionInImagePixels();
|
||||
if (!inPreview.isValid() || m_image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
if (m_sourceLogicalSize.isValid() && m_sourceLogicalSize != m_image.size()) {
|
||||
return core::image_decode::mapRectBetweenSizes(inPreview, m_image.size(), m_sourceLogicalSize);
|
||||
}
|
||||
return inPreview;
|
||||
}
|
||||
|
||||
void ImageCropDialog::onReset() {
|
||||
@@ -225,4 +564,3 @@ void ImageCropDialog::onReset() {
|
||||
void ImageCropDialog::onOk() {
|
||||
accept();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QDialog>
|
||||
#include <QImage>
|
||||
#include <QRect>
|
||||
#include <QSize>
|
||||
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
@@ -30,5 +31,7 @@ private:
|
||||
|
||||
QString m_imagePath;
|
||||
QImage m_image;
|
||||
/// read() 前 reader.size() 得到的原图逻辑尺寸(与 m_image 可能为缩放预览不一致)
|
||||
QSize m_sourceLogicalSize;
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ static QScrollArea* wrapScroll(QWidget* child, QWidget* parent) {
|
||||
InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent)
|
||||
: QDialog(parent) {
|
||||
setModal(true);
|
||||
setMinimumSize(860, 520);
|
||||
setMinimumSize(720, 480);
|
||||
setWindowTitle(title);
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
@@ -49,7 +49,7 @@ InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent
|
||||
|
||||
auto* btns = new QDialogButtonBox(this);
|
||||
btns->addButton(QStringLiteral("取消"), QDialogButtonBox::RejectRole);
|
||||
btns->addButton(QStringLiteral("接受并写回"), QDialogButtonBox::AcceptRole);
|
||||
btns->addButton(QStringLiteral("写回"), QDialogButtonBox::AcceptRole);
|
||||
connect(btns, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(btns, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
root->addWidget(btns);
|
||||
|
||||
Reference in New Issue
Block a user