diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm
index 22eb17ab..b3ea695d 100644
Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ
diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts
index 326c447e..f48a2b14 100644
--- a/Bin/Config/Lang/cn/nmNum_cn.ts
+++ b/Bin/Config/Lang/cn/nmNum_cn.ts
@@ -2884,20 +2884,41 @@ Reason: %1
nmWxPebiResultAnimationExportDlg
Export Animation导出动画
+ Frame Settings帧设置
Frame rate帧率
FPS FPS
- Start frame起始帧
- End frame结束帧
- Sampling interval采样间隔
- Resolution分辨率
- Current view当前视口
- Custom自定义
- Output size输出尺寸
+ Start time起始时间
+ End time结束时间
+ Frame step帧步长
+ Output Size输出尺寸
+ Current image size当前画面尺寸
+ Custom size自定义尺寸
+ Output File输出文件
+ File format文件格式
+ Save location保存位置
+ Browse...浏览...
+ Save Result Animation保存结果动画
+ MP4 Video (*.mp4)MP4 视频 (*.mp4)
+ AVI Video (*.avi)AVI 视频 (*.avi)
+ QuickTime Video (*.mov)QuickTime 视频 (*.mov)
+ MPEG Video (*.mpg)MPEG 视频 (*.mpg)
+ WebM Video (*.webm)WebM 视频 (*.webm)
+ Matroska Video (*.mkv)Matroska 视频 (*.mkv)
+ Windows Media Video (*.wmv)Windows Media 视频 (*.wmv)
+ Flash Video (*.flv)Flash 视频 (*.flv)
+ Export Information导出信息
Output frames输出帧数
+ %1 frames%1 帧
Estimated duration预计时长
%1 h%1 h
%1 s%1 秒
The animation export settings are invalid.动画导出参数无效。
+ The selected video format is unsupported.不支持所选视频格式。
+ Please select an output file.请选择输出文件。
+ The selected video file extension does not match the selected format.所选视频文件的扩展名与文件格式不一致。
+ The selected output path is a directory.所选输出路径是文件夹。
+ The selected output directory does not exist.所选输出目录不存在。
+ The selected file already exists. Replace it?所选文件已存在,是否替换?
nmWxPebiResultControlBar
@@ -2959,17 +2980,6 @@ Reason: %1
Export Animation导出动画
Another result animation export is already in progress.已有结果动画正在导出。
Unable to read the result animation timeline.无法读取结果动画时间轴。
- Save Result Animation保存结果动画
- MP4 Video (*.mp4)MP4 视频 (*.mp4)
- AVI Video (*.avi)AVI 视频 (*.avi)
- QuickTime Video (*.mov)QuickTime 视频 (*.mov)
- MPEG Video (*.mpg)MPEG 视频 (*.mpg)
- WebM Video (*.webm)WebM 视频 (*.webm)
- Matroska Video (*.mkv)Matroska 视频 (*.mkv)
- Windows Media Video (*.wmv)Windows Media 视频 (*.wmv)
- Flash Video (*.flv)Flash 视频 (*.flv)
- The selected video file extension is unsupported.不支持所选视频文件扩展名。
- The selected file already exists. Replace it?所选文件已存在,是否替换?
The displayed result changed before export started.开始导出前,当前显示结果已发生变化。
Unable to freeze the current result display.无法冻结当前结果显示状态。
Exporting result animation...正在导出结果动画...
diff --git a/Include/nmNum/nmSubWxs/nmWxPebiResultAnimationExportDlg.h b/Include/nmNum/nmSubWxs/nmWxPebiResultAnimationExportDlg.h
index 068285f4..7aa0fec9 100644
--- a/Include/nmNum/nmSubWxs/nmWxPebiResultAnimationExportDlg.h
+++ b/Include/nmNum/nmSubWxs/nmWxPebiResultAnimationExportDlg.h
@@ -6,13 +6,17 @@
#include
#include
+#include
#include
+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 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 按输出帧数和帧率计算的预计视频时长。 */
diff --git a/Src/nmNum/nmSubWxs/nmWxPebiResultAnimationExportDlg.cpp b/Src/nmNum/nmSubWxs/nmWxPebiResultAnimationExportDlg.cpp
index c0fa047f..ad9ad104 100644
--- a/Src/nmNum/nmSubWxs/nmWxPebiResultAnimationExportDlg.cpp
+++ b/Src/nmNum/nmSubWxs/nmWxPebiResultAnimationExportDlg.cpp
@@ -1,13 +1,24 @@
#include "nmWxPebiResultAnimationExportDlg.h"
+#include "nmFFmpegVideoWriter.h"
+#include "ZxBaseUtil.h"
+
+#include
#include
#include
#include
+#include
+#include
+#include
#include
-#include
+#include
#include
#include
+#include
+#include
#include
+#include
+#include
#include
#include
#include
@@ -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(
- 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(nOutputFrameCount) /
static_cast(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);
diff --git a/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp
index 3ff7c2ff..88a3417d 100644
--- a/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp
+++ b/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp
@@ -16,8 +16,6 @@
#include
#include
#include
-#include
-#include
#include
#include
#include
@@ -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 =