From f0e20d80c7518ff14de0d14fdf1389c4380cf2fc Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Tue, 22 Sep 2026 10:51:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=20PEBI=20=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E5=8A=A8=E7=94=BB=E5=AF=BC=E5=87=BA=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将导出参数按帧设置、输出尺寸、输出文件和导出信息重新分组 - 使用单选项提供当前画面、720p、1080p和自定义输出尺寸 - 规范化当前视口及自定义尺寸,并保持自定义宽高独立编辑状态 - 在参数对话框中集成视频格式、保存位置和文件浏览 - 使用稳定扩展名识别格式,统一处理扩展名同步和绝对路径转换 - 集中校验目标目录、文件类型及覆盖确认,避免重复提示 - 简化结果窗口导出流程并保留快照身份复核 - 更新中文翻译资源及新增接口注释 --- Bin/Config/Lang/cn/nmNum_cn.qm | Bin 155847 -> 156947 bytes Bin/Config/Lang/cn/nmNum_cn.ts | 46 +- .../nmWxPebiResultAnimationExportDlg.h | 58 +- .../nmWxPebiResultAnimationExportDlg.cpp | 501 ++++++++++++++---- .../nmSubWxs/nmWxPebiResultViewWidget.cpp | 82 +-- 5 files changed, 492 insertions(+), 195 deletions(-) diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 22eb17ab5eeed4a1c1fa2c1d77224ada7c3859a8..b3ea695d27be687b0dfe6e240f7251272921de2a 100644 GIT binary patch delta 5853 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 IHMN=WUp=hcQ~&?~ 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 =