feat: 完善 PEBI 结果动画导出配置

- 将导出参数按帧设置、输出尺寸、输出文件和导出信息重新分组
- 使用单选项提供当前画面、720p、1080p和自定义输出尺寸
- 规范化当前视口及自定义尺寸,并保持自定义宽高独立编辑状态
- 在参数对话框中集成视频格式、保存位置和文件浏览
- 使用稳定扩展名识别格式,统一处理扩展名同步和绝对路径转换
- 集中校验目标目录、文件类型及覆盖确认,避免重复提示
- 简化结果窗口导出流程并保留快照身份复核
- 更新中文翻译资源及新增接口注释
feature/nmNum-postprocessing-view-20260910
lh 1 month ago
parent 6b4d4e4121
commit f0e20d80c7

Binary file not shown.

@ -2884,20 +2884,41 @@ Reason: %1</source>
<context>
<name>nmWxPebiResultAnimationExportDlg</name>
<message><source>Export Animation</source><translation>导出动画</translation></message>
<message><source>Frame Settings</source><translation>帧设置</translation></message>
<message><source>Frame rate</source><translation>帧率</translation></message>
<message><source> FPS</source><translation> FPS</translation></message>
<message><source>Start frame</source><translation>起始帧</translation></message>
<message><source>End frame</source><translation>结束帧</translation></message>
<message><source>Sampling interval</source><translation>采样间隔</translation></message>
<message><source>Resolution</source><translation>分辨率</translation></message>
<message><source>Current view</source><translation>当前视口</translation></message>
<message><source>Custom</source><translation>自定义</translation></message>
<message><source>Output size</source><translation>输出尺寸</translation></message>
<message><source>Start time</source><translation>起始时间</translation></message>
<message><source>End time</source><translation>结束时间</translation></message>
<message><source>Frame step</source><translation>帧步长</translation></message>
<message><source>Output Size</source><translation>输出尺寸</translation></message>
<message><source>Current image size</source><translation>当前画面尺寸</translation></message>
<message><source>Custom size</source><translation>自定义尺寸</translation></message>
<message><source>Output File</source><translation>输出文件</translation></message>
<message><source>File format</source><translation>文件格式</translation></message>
<message><source>Save location</source><translation>保存位置</translation></message>
<message><source>Browse...</source><translation>浏览...</translation></message>
<message><source>Save Result Animation</source><translation>保存结果动画</translation></message>
<message><source>MP4 Video (*.mp4)</source><translation>MP4 视频 (*.mp4)</translation></message>
<message><source>AVI Video (*.avi)</source><translation>AVI 视频 (*.avi)</translation></message>
<message><source>QuickTime Video (*.mov)</source><translation>QuickTime 视频 (*.mov)</translation></message>
<message><source>MPEG Video (*.mpg)</source><translation>MPEG 视频 (*.mpg)</translation></message>
<message><source>WebM Video (*.webm)</source><translation>WebM 视频 (*.webm)</translation></message>
<message><source>Matroska Video (*.mkv)</source><translation>Matroska 视频 (*.mkv)</translation></message>
<message><source>Windows Media Video (*.wmv)</source><translation>Windows Media 视频 (*.wmv)</translation></message>
<message><source>Flash Video (*.flv)</source><translation>Flash 视频 (*.flv)</translation></message>
<message><source>Export Information</source><translation>导出信息</translation></message>
<message><source>Output frames</source><translation>输出帧数</translation></message>
<message><source>%1 frames</source><translation>%1 帧</translation></message>
<message><source>Estimated duration</source><translation>预计时长</translation></message>
<message><source>%1 h</source><translation>%1 h</translation></message>
<message><source>%1 s</source><translation>%1 秒</translation></message>
<message><source>The animation export settings are invalid.</source><translation>动画导出参数无效。</translation></message>
<message><source>The selected video format is unsupported.</source><translation>不支持所选视频格式。</translation></message>
<message><source>Please select an output file.</source><translation>请选择输出文件。</translation></message>
<message><source>The selected video file extension does not match the selected format.</source><translation>所选视频文件的扩展名与文件格式不一致。</translation></message>
<message><source>The selected output path is a directory.</source><translation>所选输出路径是文件夹。</translation></message>
<message><source>The selected output directory does not exist.</source><translation>所选输出目录不存在。</translation></message>
<message><source>The selected file already exists. Replace it?</source><translation>所选文件已存在,是否替换?</translation></message>
</context>
<context>
<name>nmWxPebiResultControlBar</name>
@ -2959,17 +2980,6 @@ Reason: %1</source>
<message><source>Export Animation</source><translation>导出动画</translation></message>
<message><source>Another result animation export is already in progress.</source><translation>已有结果动画正在导出。</translation></message>
<message><source>Unable to read the result animation timeline.</source><translation>无法读取结果动画时间轴。</translation></message>
<message><source>Save Result Animation</source><translation>保存结果动画</translation></message>
<message><source>MP4 Video (*.mp4)</source><translation>MP4 视频 (*.mp4)</translation></message>
<message><source>AVI Video (*.avi)</source><translation>AVI 视频 (*.avi)</translation></message>
<message><source>QuickTime Video (*.mov)</source><translation>QuickTime 视频 (*.mov)</translation></message>
<message><source>MPEG Video (*.mpg)</source><translation>MPEG 视频 (*.mpg)</translation></message>
<message><source>WebM Video (*.webm)</source><translation>WebM 视频 (*.webm)</translation></message>
<message><source>Matroska Video (*.mkv)</source><translation>Matroska 视频 (*.mkv)</translation></message>
<message><source>Windows Media Video (*.wmv)</source><translation>Windows Media 视频 (*.wmv)</translation></message>
<message><source>Flash Video (*.flv)</source><translation>Flash 视频 (*.flv)</translation></message>
<message><source>The selected video file extension is unsupported.</source><translation>不支持所选视频文件扩展名。</translation></message>
<message><source>The selected file already exists. Replace it?</source><translation>所选文件已存在,是否替换?</translation></message>
<message><source>The displayed result changed before export started.</source><translation>开始导出前,当前显示结果已发生变化。</translation></message>
<message><source>Unable to freeze the current result display.</source><translation>无法冻结当前结果显示状态。</translation></message>
<message><source>Exporting result animation...</source><translation>正在导出结果动画...</translation></message>

@ -6,13 +6,17 @@
#include <QDialog>
#include <QSize>
#include <QString>
#include <QVector>
class QButtonGroup;
class QComboBox;
class QLabel;
class QLineEdit;
class QRadioButton;
class QSpinBox;
/** @brief 编辑当前动态结果的动画导出参数,不选择目标文件。 */
/** @brief 编辑当前动态结果的动画导出参数和目标视频文件。 */
class NM_SUB_WXS_EXPORT nmWxPebiResultAnimationExportDlg
: public QDialog
{
@ -32,17 +36,23 @@ public:
/** @brief 返回用户确认后的参数;仅在对话框Accepted后使用。 */
nmPebiResultAnimationExportSettings getSettings() const;
/** @brief 返回用户确认后的绝对目标文件路径;仅在对话框Accepted后使用。 */
QString getTargetFilePath() const;
private slots:
/** @brief 保证结束帧不早于起始帧,并刷新采样上限和摘要。 */
void onFrameRangeChanged();
/** @brief 应用当前视口或固定分辨率,并切换自定义输入可用状态。 */
void onResolutionPresetChanged(int nIndex);
/** @brief 切换输出尺寸模式并刷新自定义输入可用状态和摘要。 */
void onResolutionPresetChanged(int nPresetId);
/** @brief 在输入完成后把自定义宽高向上规范为偶数。 */
void normalizeCustomDimensions();
/** @brief 按当前参数刷新输出帧数和预计时长。 */
void updateSummary();
/** @brief 规范化并严格校验参数,成功后接受对话框。 */
/** @brief 按稳定扩展名同步已选路径,并清除旧错误。 */
void onVideoFormatChanged(int nIndex);
/** @brief 使用当前格式打开不负责覆盖确认的系统保存窗口。 */
void browseTargetFile();
/** @brief 规范化并严格校验参数和目标文件,成功后接受对话框。 */
void acceptSettings();
private:
@ -65,17 +75,37 @@ private:
void createUi();
/** @brief 将小时值及其零基帧下标填入起止帧下拉框。 */
void populateFrameItems();
/** @brief 填入正式支持的视频格式及其稳定扩展名。 */
void populateVideoFormats();
/** @brief 从控件收集一份不改变界面状态的规范化参数。 */
nmPebiResultAnimationExportSettings collectSettings() const;
/** @brief 返回当前尺寸模式对应的最终偶数输出尺寸。 */
QSize getSelectedOutputSize() const;
/** @brief 返回当前格式下拉项保存的不带点扩展名。 */
QString getSelectedVideoExtension() const;
/** @brief 返回当前格式对应的单一系统文件过滤器。 */
QString getSelectedVideoFilter() const;
/** @brief 按当前格式追加或替换路径扩展名。 */
QString normalizeTargetFilePath(const QString& sPath) const;
/** @brief 返回用户目录下与当前格式匹配的建议文件路径。 */
QString getSuggestedTargetFilePath() const;
/** @brief 更新只读路径框,并同步完整路径Tooltip。 */
void setTargetFilePath(const QString& sPath);
/** @brief 显示一条阻止导出的对话框内错误。 */
void showError(const QString& sError);
/** @brief 清空并隐藏旧错误。 */
void clearError();
/** @brief 将像素尺寸限制到2~8192并向上修正为偶数。 */
static int normalizeEvenDimension(int nValue);
/** @brief 各零基帧下标对应的时间,单位为小时。 */
QVector<double> m_vecFrameTimesHours;
/** @brief 打开对话框时冻结的QVTK视口像素尺寸。 */
QSize m_oCurrentViewportSize;
/** @brief 打开对话框时完成回退和偶数规范化的QVTK视口像素尺寸。 */
QSize m_oEffectiveCurrentViewportSize;
/** @brief 用户确认后保存的规范化导出参数。 */
nmPebiResultAnimationExportSettings m_oAcceptedSettings;
/** @brief 用户确认覆盖后保存的绝对目标视频路径。 */
QString m_sAcceptedTargetFilePath;
/** @brief 目标视频帧率输入框。 */
QSpinBox* m_pFpsSpinBox;
/** @brief 导出闭区间的起始帧下拉框。 */
@ -84,12 +114,24 @@ private:
QComboBox* m_pEndFrameCombo;
/** @brief 原始结果帧采样间隔输入框。 */
QSpinBox* m_pSamplingIntervalSpinBox;
/** @brief 当前视口、固定尺寸和自定义尺寸的选择框。 */
QComboBox* m_pResolutionCombo;
/** @brief 管理四种互斥输出尺寸模式及其稳定枚举ID。 */
QButtonGroup* m_pResolutionButtonGroup;
/** @brief 使用冻结后的有效当前画面尺寸。 */
QRadioButton* m_pCurrentSizeRadioButton;
/** @brief 使用固定1280x720输出尺寸。 */
QRadioButton* m_p720pRadioButton;
/** @brief 使用固定1920x1080输出尺寸。 */
QRadioButton* m_p1080pRadioButton;
/** @brief 使用用户输入并规范化后的自定义尺寸。 */
QRadioButton* m_pCustomSizeRadioButton;
/** @brief 输出宽度输入框,界面最终显示偶数像素值。 */
QSpinBox* m_pWidthSpinBox;
/** @brief 输出高度输入框,界面最终显示偶数像素值。 */
QSpinBox* m_pHeightSpinBox;
/** @brief 显示正式视频格式并以itemData保存稳定扩展名。 */
QComboBox* m_pVideoFormatCombo;
/** @brief 显示浏览窗口选定的只读目标文件路径。 */
QLineEdit* m_pTargetPathEdit;
/** @brief 按当前帧范围和采样间隔计算的输出帧数。 */
QLabel* m_pOutputFrameCountLabel;
/** @brief 按输出帧数和帧率计算的预计视频时长。 */

@ -1,13 +1,24 @@
#include "nmWxPebiResultAnimationExportDlg.h"
#include "nmFFmpegVideoWriter.h"
#include "ZxBaseUtil.h"
#include <QButtonGroup>
#include <QComboBox>
#include <QCoreApplication>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QIcon>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QRadioButton>
#include <QSizePolicy>
#include <QSpinBox>
#include <QVBoxLayout>
#include <QVariant>
@ -21,6 +32,22 @@ QString exportDialogIconPath(const QString& sFileName)
"/Res/Icon/" + sFileName;
}
QLabel* createRightAlignedLabel(const QString& sText, QWidget* pParent)
{
QLabel* pLabel = new QLabel(sText, pParent);
pLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
return pLabel;
}
QString formatPixelSize(int nWidth, int nHeight)
{
QString sText = QString::number(nWidth);
sText.append(' ');
sText.append(QChar(0x00D7));
sText.append(QString::fromLatin1(" %1 px").arg(nHeight));
return sText;
}
}
nmWxPebiResultAnimationExportDlg::nmWxPebiResultAnimationExportDlg(
@ -29,21 +56,38 @@ nmWxPebiResultAnimationExportDlg::nmWxPebiResultAnimationExportDlg(
QWidget* pParent)
: QDialog(pParent),
m_vecFrameTimesHours(vecFrameTimesHours),
m_oCurrentViewportSize(oCurrentViewportSize),
m_pFpsSpinBox(nullptr),
m_pStartFrameCombo(nullptr),
m_pEndFrameCombo(nullptr),
m_pSamplingIntervalSpinBox(nullptr),
m_pResolutionCombo(nullptr),
m_pResolutionButtonGroup(nullptr),
m_pCurrentSizeRadioButton(nullptr),
m_p720pRadioButton(nullptr),
m_p1080pRadioButton(nullptr),
m_pCustomSizeRadioButton(nullptr),
m_pWidthSpinBox(nullptr),
m_pHeightSpinBox(nullptr),
m_pVideoFormatCombo(nullptr),
m_pTargetPathEdit(nullptr),
m_pOutputFrameCountLabel(nullptr),
m_pEstimatedDurationLabel(nullptr),
m_pErrorLabel(nullptr)
{
int nCurrentWidth = oCurrentViewportSize.width();
int nCurrentHeight = oCurrentViewportSize.height();
if(nCurrentWidth <= 0 || nCurrentHeight <= 0) {
nCurrentWidth = 1280;
nCurrentHeight = 720;
}
m_oEffectiveCurrentViewportSize = QSize(
normalizeEvenDimension(nCurrentWidth),
normalizeEvenDimension(nCurrentHeight));
createUi();
populateFrameItems();
onResolutionPresetChanged(m_pResolutionCombo->currentIndex());
populateVideoFormats();
setTargetFilePath(getSuggestedTargetFilePath());
onResolutionPresetChanged(Resolution_CurrentView);
onFrameRangeChanged();
}
@ -53,6 +97,11 @@ nmWxPebiResultAnimationExportDlg::getSettings() const
return m_oAcceptedSettings;
}
QString nmWxPebiResultAnimationExportDlg::getTargetFilePath() const
{
return m_sAcceptedTargetFilePath;
}
void nmWxPebiResultAnimationExportDlg::onFrameRangeChanged()
{
const int nStartIndex = m_pStartFrameCombo->currentIndex();
@ -71,31 +120,9 @@ void nmWxPebiResultAnimationExportDlg::onFrameRangeChanged()
}
void nmWxPebiResultAnimationExportDlg::onResolutionPresetChanged(
int nIndex)
int nPresetId)
{
Q_UNUSED(nIndex);
const ResolutionPreset ePreset = static_cast<ResolutionPreset>(
m_pResolutionCombo->itemData(
m_pResolutionCombo->currentIndex()).toInt());
int nWidth = m_oCurrentViewportSize.width();
int nHeight = m_oCurrentViewportSize.height();
if(ePreset == Resolution_720p) {
nWidth = 1280;
nHeight = 720;
} else if(ePreset == Resolution_1080p) {
nWidth = 1920;
nHeight = 1080;
}
if(nWidth <= 0 || nHeight <= 0) {
nWidth = 1280;
nHeight = 720;
}
const bool bCustom = ePreset == Resolution_Custom;
if(!bCustom) {
m_pWidthSpinBox->setValue(normalizeEvenDimension(nWidth));
m_pHeightSpinBox->setValue(normalizeEvenDimension(nHeight));
}
const bool bCustom = nPresetId == Resolution_Custom;
m_pWidthSpinBox->setEnabled(bCustom);
m_pHeightSpinBox->setEnabled(bCustom);
updateSummary();
@ -117,28 +144,112 @@ void nmWxPebiResultAnimationExportDlg::updateSummary()
collectSettings();
const int nOutputFrameCount = oSettings.getOutputFrameCount();
m_pOutputFrameCountLabel->setText(
QString::number(nOutputFrameCount));
tr("%1 frames").arg(nOutputFrameCount));
const double dDuration = oSettings.m_nFramesPerSecond > 0
? static_cast<double>(nOutputFrameCount) /
static_cast<double>(oSettings.m_nFramesPerSecond)
: 0.0;
m_pEstimatedDurationLabel->setText(
tr("%1 s").arg(QString::number(dDuration, 'g', 6)));
m_pErrorLabel->clear();
tr("%1 s").arg(QString::number(dDuration, 'g', 4)));
clearError();
}
void nmWxPebiResultAnimationExportDlg::onVideoFormatChanged(int nIndex)
{
Q_UNUSED(nIndex);
if(m_pTargetPathEdit != nullptr &&
!m_pTargetPathEdit->text().isEmpty()) {
setTargetFilePath(normalizeTargetFilePath(
m_pTargetPathEdit->text()));
}
clearError();
}
void nmWxPebiResultAnimationExportDlg::browseTargetFile()
{
const QString sExtension = getSelectedVideoExtension();
if(sExtension.isEmpty()) {
showError(tr("The selected video format is unsupported."));
return;
}
const QString sInitialPath = m_pTargetPathEdit->text().isEmpty()
? getSuggestedTargetFilePath() : m_pTargetPathEdit->text();
const QFileInfo oInitialFileInfo(sInitialPath);
QFileDialog oFileDialog(this, tr("Save Result Animation"));
oFileDialog.setAcceptMode(QFileDialog::AcceptSave);
oFileDialog.setFileMode(QFileDialog::AnyFile);
oFileDialog.setNameFilter(getSelectedVideoFilter());
oFileDialog.setDefaultSuffix(sExtension);
oFileDialog.setOption(QFileDialog::DontConfirmOverwrite, true);
oFileDialog.setDirectory(oInitialFileInfo.absolutePath());
oFileDialog.selectFile(oInitialFileInfo.fileName());
if(oFileDialog.exec() != QDialog::Accepted ||
oFileDialog.selectedFiles().isEmpty()) {
return;
}
const QString sTargetPath = normalizeTargetFilePath(
oFileDialog.selectedFiles().first());
const QFileInfo oTargetFileInfo(sTargetPath);
setTargetFilePath(oTargetFileInfo.absoluteFilePath());
ZxBaseUtil::setLastDir(oTargetFileInfo.absolutePath());
clearError();
}
void nmWxPebiResultAnimationExportDlg::acceptSettings()
{
// 接受前再规范化一次,覆盖按钮点击与焦点事件顺序差异。
normalizeCustomDimensions();
// 只有自定义模式需要把编辑值回写为编码器最终使用的偶数尺寸。
if(m_pCustomSizeRadioButton->isChecked()) {
normalizeCustomDimensions();
}
const nmPebiResultAnimationExportSettings oSettings =
collectSettings();
if(!oSettings.isValid(m_vecFrameTimesHours.size())) {
m_pErrorLabel->setText(
tr("The animation export settings are invalid."));
showError(tr("The animation export settings are invalid."));
return;
}
if(m_pTargetPathEdit->text().isEmpty()) {
showError(tr("Please select an output file."));
return;
}
const QString sExtension = getSelectedVideoExtension();
if(sExtension.isEmpty()) {
showError(tr("The selected video format is unsupported."));
return;
}
const QString sTargetPath = QFileInfo(normalizeTargetFilePath(
m_pTargetPathEdit->text())).absoluteFilePath();
const QFileInfo oTargetFileInfo(sTargetPath);
if(oTargetFileInfo.suffix().compare(
sExtension, Qt::CaseInsensitive) != 0) {
showError(tr("The selected video file extension does not match "
"the selected format."));
return;
}
if(oTargetFileInfo.isDir()) {
showError(tr("The selected output path is a directory."));
return;
}
if(!QDir(oTargetFileInfo.absolutePath()).exists()) {
showError(tr("The selected output directory does not exist."));
return;
}
setTargetFilePath(sTargetPath);
if(oTargetFileInfo.exists() &&
QMessageBox::question(
this, tr("Export Animation"),
tr("The selected file already exists. Replace it?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes) {
return;
}
m_oAcceptedSettings = oSettings;
m_sAcceptedTargetFilePath = sTargetPath;
accept();
}
@ -146,75 +257,167 @@ void nmWxPebiResultAnimationExportDlg::createUi()
{
setWindowTitle(tr("Export Animation"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setMinimumWidth(430);
setMinimumWidth(460);
QVBoxLayout* pMainLayout = new QVBoxLayout(this);
pMainLayout->setContentsMargins(12, 10, 12, 10);
pMainLayout->setSpacing(8);
QGridLayout* pGrid = new QGridLayout();
pGrid->setHorizontalSpacing(8);
pGrid->setVerticalSpacing(6);
int nRow = 0;
QGroupBox* pFrameGroup = new QGroupBox(tr("Frame Settings"), this);
QGridLayout* pFrameGrid = new QGridLayout(pFrameGroup);
pFrameGrid->setHorizontalSpacing(8);
pFrameGrid->setVerticalSpacing(6);
pFrameGrid->setColumnStretch(1, 1);
const QSizePolicy oFieldSizePolicy(
QSizePolicy::Expanding, QSizePolicy::Fixed);
m_pFpsSpinBox = new QSpinBox(this);
m_pFpsSpinBox = new QSpinBox(pFrameGroup);
m_pFpsSpinBox->setRange(1, 5000);
m_pFpsSpinBox->setValue(15);
m_pFpsSpinBox->setSuffix(tr(" FPS"));
pGrid->addWidget(new QLabel(tr("Frame rate"), this), nRow, 0);
pGrid->addWidget(m_pFpsSpinBox, nRow++, 1, 1, 2);
m_pFpsSpinBox->setSizePolicy(oFieldSizePolicy);
pFrameGrid->addWidget(
createRightAlignedLabel(tr("Frame rate"), pFrameGroup), 0, 0);
pFrameGrid->addWidget(m_pFpsSpinBox, 0, 1);
m_pStartFrameCombo = new QComboBox(this);
pGrid->addWidget(new QLabel(tr("Start frame"), this), nRow, 0);
pGrid->addWidget(m_pStartFrameCombo, nRow++, 1, 1, 2);
m_pStartFrameCombo = new QComboBox(pFrameGroup);
m_pStartFrameCombo->setSizePolicy(oFieldSizePolicy);
pFrameGrid->addWidget(
createRightAlignedLabel(tr("Start time"), pFrameGroup), 1, 0);
pFrameGrid->addWidget(m_pStartFrameCombo, 1, 1);
m_pEndFrameCombo = new QComboBox(this);
pGrid->addWidget(new QLabel(tr("End frame"), this), nRow, 0);
pGrid->addWidget(m_pEndFrameCombo, nRow++, 1, 1, 2);
m_pEndFrameCombo = new QComboBox(pFrameGroup);
m_pEndFrameCombo->setSizePolicy(oFieldSizePolicy);
pFrameGrid->addWidget(
createRightAlignedLabel(tr("End time"), pFrameGroup), 2, 0);
pFrameGrid->addWidget(m_pEndFrameCombo, 2, 1);
m_pSamplingIntervalSpinBox = new QSpinBox(this);
m_pSamplingIntervalSpinBox = new QSpinBox(pFrameGroup);
m_pSamplingIntervalSpinBox->setRange(1, 1);
pGrid->addWidget(new QLabel(tr("Sampling interval"), this), nRow, 0);
pGrid->addWidget(m_pSamplingIntervalSpinBox, nRow++, 1, 1, 2);
m_pResolutionCombo = new QComboBox(this);
m_pResolutionCombo->addItem(tr("Current view"), Resolution_CurrentView);
m_pResolutionCombo->addItem(QString::fromLatin1("1280 x 720"),
Resolution_720p);
m_pResolutionCombo->addItem(QString::fromLatin1("1920 x 1080"),
Resolution_1080p);
m_pResolutionCombo->addItem(tr("Custom"), Resolution_Custom);
pGrid->addWidget(new QLabel(tr("Resolution"), this), nRow, 0);
pGrid->addWidget(m_pResolutionCombo, nRow++, 1, 1, 2);
m_pWidthSpinBox = new QSpinBox(this);
m_pHeightSpinBox = new QSpinBox(this);
m_pSamplingIntervalSpinBox->setSizePolicy(oFieldSizePolicy);
pFrameGrid->addWidget(
createRightAlignedLabel(tr("Frame step"), pFrameGroup), 3, 0);
pFrameGrid->addWidget(m_pSamplingIntervalSpinBox, 3, 1);
pMainLayout->addWidget(pFrameGroup);
QGroupBox* pOutputSizeGroup = new QGroupBox(tr("Output Size"), this);
QGridLayout* pOutputSizeGrid = new QGridLayout(pOutputSizeGroup);
pOutputSizeGrid->setHorizontalSpacing(6);
pOutputSizeGrid->setVerticalSpacing(6);
m_pResolutionButtonGroup = new QButtonGroup(this);
m_pCurrentSizeRadioButton = new QRadioButton(
tr("Current image size") + QString::fromLatin1(" ") +
formatPixelSize(m_oEffectiveCurrentViewportSize.width(),
m_oEffectiveCurrentViewportSize.height()),
pOutputSizeGroup);
m_p720pRadioButton = new QRadioButton(
formatPixelSize(1280, 720), pOutputSizeGroup);
m_p1080pRadioButton = new QRadioButton(
formatPixelSize(1920, 1080), pOutputSizeGroup);
m_pCustomSizeRadioButton = new QRadioButton(
tr("Custom size"), pOutputSizeGroup);
m_pResolutionButtonGroup->addButton(
m_pCurrentSizeRadioButton, Resolution_CurrentView);
m_pResolutionButtonGroup->addButton(
m_p720pRadioButton, Resolution_720p);
m_pResolutionButtonGroup->addButton(
m_p1080pRadioButton, Resolution_1080p);
m_pResolutionButtonGroup->addButton(
m_pCustomSizeRadioButton, Resolution_Custom);
m_pCurrentSizeRadioButton->setChecked(true);
pOutputSizeGrid->addWidget(
m_pCurrentSizeRadioButton, 0, 1, 1, 5, Qt::AlignLeft);
pOutputSizeGrid->addWidget(
m_p720pRadioButton, 1, 1, 1, 5, Qt::AlignLeft);
pOutputSizeGrid->addWidget(
m_p1080pRadioButton, 2, 1, 1, 5, Qt::AlignLeft);
pOutputSizeGrid->addWidget(m_pCustomSizeRadioButton, 3, 1, Qt::AlignLeft);
m_pWidthSpinBox = new QSpinBox(pOutputSizeGroup);
m_pHeightSpinBox = new QSpinBox(pOutputSizeGroup);
m_pWidthSpinBox->setRange(2, 8192);
m_pHeightSpinBox->setRange(2, 8192);
m_pWidthSpinBox->setSingleStep(2);
m_pHeightSpinBox->setSingleStep(2);
QHBoxLayout* pCustomSizeLayout = new QHBoxLayout();
pCustomSizeLayout->setContentsMargins(0, 0, 0, 0);
pCustomSizeLayout->setSpacing(6);
pCustomSizeLayout->addWidget(m_pWidthSpinBox);
pCustomSizeLayout->addWidget(new QLabel(QString::fromLatin1("x"), this));
pCustomSizeLayout->addWidget(m_pHeightSpinBox);
pGrid->addWidget(new QLabel(tr("Output size"), this), nRow, 0);
pGrid->addLayout(pCustomSizeLayout, nRow++, 1, 1, 2);
m_pOutputFrameCountLabel = new QLabel(this);
pGrid->addWidget(new QLabel(tr("Output frames"), this), nRow, 0);
pGrid->addWidget(m_pOutputFrameCountLabel, nRow++, 1, 1, 2);
m_pEstimatedDurationLabel = new QLabel(this);
pGrid->addWidget(new QLabel(tr("Estimated duration"), this), nRow, 0);
pGrid->addWidget(m_pEstimatedDurationLabel, nRow++, 1, 1, 2);
pGrid->setColumnStretch(1, 1);
pMainLayout->addLayout(pGrid);
m_pWidthSpinBox->setMinimumWidth(90);
m_pWidthSpinBox->setMaximumWidth(110);
m_pHeightSpinBox->setMinimumWidth(90);
m_pHeightSpinBox->setMaximumWidth(110);
m_pWidthSpinBox->setValue(m_oEffectiveCurrentViewportSize.width());
m_pHeightSpinBox->setValue(m_oEffectiveCurrentViewportSize.height());
QLabel* pMultiplyLabel = new QLabel(
QString(QChar(0x00D7)), pOutputSizeGroup);
pMultiplyLabel->setAlignment(Qt::AlignCenter);
QLabel* pPixelLabel = new QLabel(
QString::fromLatin1("px"), pOutputSizeGroup);
pOutputSizeGrid->addWidget(m_pWidthSpinBox, 3, 2);
pOutputSizeGrid->addWidget(pMultiplyLabel, 3, 3, Qt::AlignCenter);
pOutputSizeGrid->addWidget(m_pHeightSpinBox, 3, 4);
pOutputSizeGrid->addWidget(pPixelLabel, 3, 5, Qt::AlignLeft);
// 两侧等权留白使完整尺寸选项块居中,块内控件仍保持左对齐。
pOutputSizeGrid->setColumnStretch(0, 1);
pOutputSizeGrid->setColumnStretch(6, 1);
pMainLayout->addWidget(pOutputSizeGroup);
QGroupBox* pOutputFileGroup = new QGroupBox(
tr("Output File"), this);
QGridLayout* pOutputFileGrid = new QGridLayout(pOutputFileGroup);
pOutputFileGrid->setHorizontalSpacing(8);
pOutputFileGrid->setVerticalSpacing(6);
pOutputFileGrid->setColumnStretch(1, 1);
m_pVideoFormatCombo = new QComboBox(pOutputFileGroup);
m_pVideoFormatCombo->setSizePolicy(oFieldSizePolicy);
pOutputFileGrid->addWidget(
createRightAlignedLabel(
tr("File format"), pOutputFileGroup), 0, 0);
pOutputFileGrid->addWidget(m_pVideoFormatCombo, 0, 1, 1, 2);
m_pTargetPathEdit = new QLineEdit(pOutputFileGroup);
m_pTargetPathEdit->setReadOnly(true);
m_pTargetPathEdit->setSizePolicy(oFieldSizePolicy);
QPushButton* pBrowseButton = new QPushButton(
tr("Browse..."), pOutputFileGroup);
pBrowseButton->setMinimumWidth(72);
pOutputFileGrid->addWidget(
createRightAlignedLabel(
tr("Save location"), pOutputFileGroup), 1, 0);
pOutputFileGrid->addWidget(m_pTargetPathEdit, 1, 1);
pOutputFileGrid->addWidget(pBrowseButton, 1, 2);
pMainLayout->addWidget(pOutputFileGroup);
QGroupBox* pInformationGroup = new QGroupBox(
tr("Export Information"), this);
QGridLayout* pInformationGrid = new QGridLayout(pInformationGroup);
pInformationGrid->setHorizontalSpacing(8);
pInformationGrid->setVerticalSpacing(6);
m_pOutputFrameCountLabel = new QLabel(pInformationGroup);
m_pEstimatedDurationLabel = new QLabel(pInformationGroup);
m_pOutputFrameCountLabel->setTextInteractionFlags(
Qt::TextSelectableByMouse);
m_pEstimatedDurationLabel->setTextInteractionFlags(
Qt::TextSelectableByMouse);
pInformationGrid->addWidget(
createRightAlignedLabel(
tr("Output frames"), pInformationGroup), 0, 1);
pInformationGrid->addWidget(
m_pOutputFrameCountLabel, 0, 2, Qt::AlignLeft);
pInformationGrid->addWidget(
createRightAlignedLabel(
tr("Estimated duration"), pInformationGroup), 1, 1);
pInformationGrid->addWidget(
m_pEstimatedDurationLabel, 1, 2, Qt::AlignLeft);
// 标签列和数值列作为一个整体居中,保留内部左右对齐关系。
pInformationGrid->setColumnStretch(0, 1);
pInformationGrid->setColumnStretch(3, 1);
pMainLayout->addWidget(pInformationGroup);
m_pErrorLabel = new QLabel(this);
m_pErrorLabel->setStyleSheet("QLabel { color: #a32626; }");
m_pErrorLabel->setWordWrap(true);
m_pErrorLabel->hide();
pMainLayout->addWidget(m_pErrorLabel);
QDialogButtonBox* pButtonBox = new QDialogButtonBox(
@ -228,7 +431,7 @@ void nmWxPebiResultAnimationExportDlg::createUi()
pCancelButton->setIconSize(QSize(16, 16));
pOkButton->setMinimumWidth(72);
pCancelButton->setMinimumWidth(72);
pMainLayout->addWidget(pButtonBox);
pMainLayout->addWidget(pButtonBox, 0, Qt::AlignRight);
connect(m_pStartFrameCombo, SIGNAL(currentIndexChanged(int)),
this, SLOT(onFrameRangeChanged()));
@ -238,8 +441,12 @@ void nmWxPebiResultAnimationExportDlg::createUi()
this, SLOT(updateSummary()));
connect(m_pFpsSpinBox, SIGNAL(valueChanged(int)),
this, SLOT(updateSummary()));
connect(m_pResolutionCombo, SIGNAL(currentIndexChanged(int)),
connect(m_pResolutionButtonGroup, SIGNAL(buttonClicked(int)),
this, SLOT(onResolutionPresetChanged(int)));
connect(m_pVideoFormatCombo, SIGNAL(currentIndexChanged(int)),
this, SLOT(onVideoFormatChanged(int)));
connect(pBrowseButton, SIGNAL(clicked()),
this, SLOT(browseTargetFile()));
connect(m_pWidthSpinBox, SIGNAL(valueChanged(int)),
this, SLOT(updateSummary()));
connect(m_pHeightSpinBox, SIGNAL(valueChanged(int)),
@ -270,6 +477,28 @@ void nmWxPebiResultAnimationExportDlg::populateFrameItems()
}
}
void nmWxPebiResultAnimationExportDlg::populateVideoFormats()
{
// itemData只保存稳定扩展名,界面翻译不会参与格式识别。
m_pVideoFormatCombo->addItem(
tr("MP4 Video (*.mp4)"), QString::fromLatin1("mp4"));
m_pVideoFormatCombo->addItem(
tr("AVI Video (*.avi)"), QString::fromLatin1("avi"));
m_pVideoFormatCombo->addItem(
tr("QuickTime Video (*.mov)"), QString::fromLatin1("mov"));
m_pVideoFormatCombo->addItem(
tr("MPEG Video (*.mpg)"), QString::fromLatin1("mpg"));
m_pVideoFormatCombo->addItem(
tr("WebM Video (*.webm)"), QString::fromLatin1("webm"));
m_pVideoFormatCombo->addItem(
tr("Matroska Video (*.mkv)"), QString::fromLatin1("mkv"));
m_pVideoFormatCombo->addItem(
tr("Windows Media Video (*.wmv)"), QString::fromLatin1("wmv"));
m_pVideoFormatCombo->addItem(
tr("Flash Video (*.flv)"), QString::fromLatin1("flv"));
m_pVideoFormatCombo->setCurrentIndex(0);
}
nmPebiResultAnimationExportSettings
nmWxPebiResultAnimationExportDlg::collectSettings() const
{
@ -283,13 +512,107 @@ nmWxPebiResultAnimationExportDlg::collectSettings() const
m_pEndFrameCombo->currentIndex()).toInt();
oSettings.m_nSamplingInterval =
m_pSamplingIntervalSpinBox->value();
oSettings.m_nOutputWidth =
normalizeEvenDimension(m_pWidthSpinBox->value());
oSettings.m_nOutputHeight =
normalizeEvenDimension(m_pHeightSpinBox->value());
const QSize oOutputSize = getSelectedOutputSize();
oSettings.m_nOutputWidth = oOutputSize.width();
oSettings.m_nOutputHeight = oOutputSize.height();
return oSettings;
}
QSize nmWxPebiResultAnimationExportDlg::getSelectedOutputSize() const
{
const int nPresetId = m_pResolutionButtonGroup == nullptr
? Resolution_CurrentView :
m_pResolutionButtonGroup->checkedId();
if(nPresetId == Resolution_720p) {
return QSize(1280, 720);
}
if(nPresetId == Resolution_1080p) {
return QSize(1920, 1080);
}
if(nPresetId == Resolution_Custom) {
return QSize(
normalizeEvenDimension(m_pWidthSpinBox->value()),
normalizeEvenDimension(m_pHeightSpinBox->value()));
}
return m_oEffectiveCurrentViewportSize;
}
QString nmWxPebiResultAnimationExportDlg::getSelectedVideoExtension() const
{
if(m_pVideoFormatCombo == nullptr ||
m_pVideoFormatCombo->currentIndex() < 0) {
return QString();
}
const QString sExtension = m_pVideoFormatCombo->itemData(
m_pVideoFormatCombo->currentIndex()).toString().toLower();
return nmFFmpegVideoWriter::isSupportedExtension(sExtension)
? sExtension : QString();
}
QString nmWxPebiResultAnimationExportDlg::getSelectedVideoFilter() const
{
return m_pVideoFormatCombo == nullptr
? QString() : m_pVideoFormatCombo->currentText();
}
QString nmWxPebiResultAnimationExportDlg::normalizeTargetFilePath(
const QString& sPath) const
{
const QString sExtension = getSelectedVideoExtension();
if(sPath.isEmpty() || sExtension.isEmpty()) {
return QString();
}
QString sNormalizedPath = QDir::fromNativeSeparators(sPath);
const QString sExistingExtension =
QFileInfo(sNormalizedPath).suffix().toLower();
if(sExistingExtension.isEmpty()) {
if(!sNormalizedPath.endsWith('.')) {
sNormalizedPath.append('.');
}
sNormalizedPath.append(sExtension);
} else if(nmFFmpegVideoWriter::isSupportedExtension(
sExistingExtension)) {
sNormalizedPath.chop(sExistingExtension.length());
sNormalizedPath.append(sExtension);
} else {
sNormalizedPath.append(
QString::fromLatin1(".%1").arg(sExtension));
}
return QDir::cleanPath(sNormalizedPath);
}
QString nmWxPebiResultAnimationExportDlg::getSuggestedTargetFilePath() const
{
QString sDirectory = ZxBaseUtil::getLastDir();
if(sDirectory.isEmpty() || !QDir(sDirectory).exists()) {
sDirectory = QDir::homePath();
}
return QDir(sDirectory).absoluteFilePath(
QString::fromLatin1("PEBI-result.%1").arg(
getSelectedVideoExtension()));
}
void nmWxPebiResultAnimationExportDlg::setTargetFilePath(
const QString& sPath)
{
const QString sDisplayPath = QDir::toNativeSeparators(sPath);
m_pTargetPathEdit->setText(sDisplayPath);
m_pTargetPathEdit->setToolTip(sDisplayPath);
}
void nmWxPebiResultAnimationExportDlg::showError(const QString& sError)
{
m_pErrorLabel->setText(sError);
m_pErrorLabel->show();
}
void nmWxPebiResultAnimationExportDlg::clearError()
{
m_pErrorLabel->clear();
m_pErrorLabel->hide();
}
int nmWxPebiResultAnimationExportDlg::normalizeEvenDimension(int nValue)
{
nValue = qBound(2, nValue, 8192);

@ -16,8 +16,6 @@
#include <QAtomicInt>
#include <QCoreApplication>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
@ -101,55 +99,6 @@ private:
bool m_bAcquired;
};
QString videoSaveFilters()
{
QStringList listFilters;
listFilters << QCoreApplication::translate(
"nmWxPebiResultViewWidget", "MP4 Video (*.mp4)")
<< QCoreApplication::translate(
"nmWxPebiResultViewWidget", "AVI Video (*.avi)")
<< QCoreApplication::translate(
"nmWxPebiResultViewWidget", "QuickTime Video (*.mov)")
<< QCoreApplication::translate(
"nmWxPebiResultViewWidget", "MPEG Video (*.mpg)")
<< QCoreApplication::translate(
"nmWxPebiResultViewWidget", "WebM Video (*.webm)")
<< QCoreApplication::translate(
"nmWxPebiResultViewWidget", "Matroska Video (*.mkv)")
<< QCoreApplication::translate(
"nmWxPebiResultViewWidget", "Windows Media Video (*.wmv)")
<< QCoreApplication::translate(
"nmWxPebiResultViewWidget", "Flash Video (*.flv)");
return listFilters.join(QString::fromLatin1(";;"));
}
QString extensionFromFilter(const QString& sFilter)
{
const QStringList listExtensions =
nmFFmpegVideoWriter::getSupportedExtensions();
for(int nIndex = 0; nIndex < listExtensions.size(); ++nIndex) {
const QString& sExtension = listExtensions[nIndex];
if(sFilter.contains(QString::fromLatin1("*.%1").arg(sExtension))) {
return sExtension;
}
}
return QString::fromLatin1("mp4");
}
QString normalizedVideoTargetPath(const QString& sPath,
const QString& sSelectedExtension)
{
if(sPath.isEmpty()) {
return QString();
}
const QString sExistingExtension = QFileInfo(sPath).suffix().toLower();
if(sExistingExtension.isEmpty()) {
return sPath + QString::fromLatin1(".%1").arg(sSelectedExtension);
}
return nmFFmpegVideoWriter::isSupportedExtension(sExistingExtension)
? sPath : QString();
}
}
nmWxPebiResultViewWidget::nmWxPebiResultViewWidget(
@ -379,34 +328,7 @@ void nmWxPebiResultViewWidget::onExportAnimationRequested()
return;
}
QString sSelectedFilter = videoSaveFilters().section(
QString::fromLatin1(";;"), 0, 0);
const QString sSelectedPath = QFileDialog::getSaveFileName(
this, tr("Save Result Animation"),
QDir::home().filePath(QString::fromLatin1("PEBI-result.mp4")),
videoSaveFilters(), &sSelectedFilter);
if(sSelectedPath.isEmpty()) {
return;
}
const QString sTargetPath = normalizedVideoTargetPath(
sSelectedPath, extensionFromFilter(sSelectedFilter));
if(sTargetPath.isEmpty()) {
QMessageBox::warning(
this, tr("Export Animation"),
tr("The selected video file extension is unsupported."));
return;
}
if(QFileInfo(sSelectedPath).suffix().isEmpty() &&
QFileInfo(sTargetPath).exists() &&
QMessageBox::question(
this, tr("Export Animation"),
tr("The selected file already exists. Replace it?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes) {
return;
}
// 参数和文件窗口都可能形成嵌套事件循环,开始前重新核对完整显示身份。
// 参数窗口内的文件浏览也会形成嵌套事件循环,开始前重新核对完整显示身份。
if(m_pDisplayedSnapshot.data() != pFrozenSnapshot.data() ||
m_pSceneController->getSnapshotBindingRevision() !=
nFrozenBindingRevision ||
@ -433,7 +355,7 @@ void nmWxPebiResultViewWidget::onExportAnimationRequested()
oRequest.m_sPropertyId = sFrozenPropertyId;
oRequest.m_oDisplaySettings = oDisplaySettings;
oRequest.m_oExportSettings = oSettingsDialog.getSettings();
oRequest.m_sTargetFilePath = QFileInfo(sTargetPath).absoluteFilePath();
oRequest.m_sTargetFilePath = oSettingsDialog.getTargetFilePath();
oRequest.m_bScalarBarVisible =
m_pSceneController->isScalarBarVisible();
oRequest.m_bScaleGridVisible =

Loading…
Cancel
Save