1. 项目概述
作为一个长期从事多媒体开发的工程师,我经常需要处理视频字幕相关的任务。市面上的商业字幕工具要么功能臃肿,要么缺乏必要的定制化能力。于是我用Qt C++开发了一个轻量级的字幕制作工具,核心功能包括:
- 基于Qt Quick的现代化字幕编辑界面
- 精确到帧的时间轴控制系统
- 通过FFmpeg实现视频与字幕的合成输出
- 支持SRT、ASS等常见字幕格式的导入导出
这个工具特别适合需要批量处理字幕的影视工作者,或者想要学习Qt多媒体开发的程序员。整个项目采用MVVM架构,界面逻辑与业务逻辑完全分离,代码结构清晰易扩展。
2. 开发环境准备
2.1 工具链配置
开发环境需要以下组件:
-
Qt 5.15+:建议使用官方在线安装器,勾选以下组件:
- Qt Creator
- Qt 5.15.x (MinGW 64-bit)
- Qt Quick Controls 2
- Qt Multimedia
-
FFmpeg:下载shared版本的Windows预编译包,配置环境变量:
bash复制# 示例:在.pro文件中添加lib路径 LIBS += -L$$PWD/ffmpeg/lib -lavcodec -lavformat -lavutil INCLUDEPATH += $$PWD/ffmpeg/include -
可选工具:
- Visual Studio Code(用于QML调试)
- Git(版本控制)
注意:FFmpeg路径中不要包含中文或空格,否则可能导致链接失败。在Windows下建议将库文件放在项目根目录的ffmpeg子文件夹中。
2.2 项目创建步骤
-
在Qt Creator中创建新项目:
- 选择"Qt Quick Application - Empty"
- 项目名称:SubtitleEditor
- 勾选"Create form"选项
-
修改.pro文件关键配置:
qmake复制
QT += quick multimedia widgets qml CONFIG += c++11 DEFINES += QT_DEPRECATED_WARNINGS -
添加资源文件:
- 创建resources.qrc
- 将QML文件放入/qml目录
- 添加图标等静态资源
3. 核心模块实现
3.1 字幕数据模型设计
在src/subtitlemanager.h中定义核心数据结构:
cpp复制class SubtitleManager : public QObject {
Q_OBJECT
public:
struct SubtitleItem {
int id;
QString text;
qint64 startTime; // 毫秒
qint64 endTime;
QColor color;
QFont font;
};
explicit SubtitleManager(QObject *parent = nullptr);
Q_INVOKABLE void addSubtitle(const QString &text, qint64 start, qint64 end);
Q_INVOKABLE void removeSubtitle(int id);
Q_INVOKABLE QVariantList getSubtitles() const;
signals:
void subtitlesChanged();
private:
QList<SubtitleItem> m_subtitles;
int m_nextId = 0;
};
关键点说明:
- 使用QObject派生类实现数据模型,便于QML调用
- Q_INVOKABLE标记的方法可以在QML中直接调用
- QVariantList作为跨C++/QML的数据传输格式
3.2 QML界面架构
3.2.1 主界面布局
Main.qml采用SplitView实现可调整布局:
qml复制import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
ApplicationWindow {
id: root
visible: true
width: 1280
height: 720
SplitView {
anchors.fill: parent
orientation: Qt.Horizontal
// 视频预览区域
VideoPlayer {
id: videoPlayer
Layout.minimumWidth: 600
}
// 字幕编辑区域
SubtitleEditor {
id: editor
Layout.minimumWidth: 400
}
}
}
3.2.2 时间轴实现
Timeline.qml使用Canvas绘制时间刻度:
qml复制Canvas {
id: canvas
property real scaleFactor: 1.0
property real currentPosition: 0
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
// 绘制时间刻度
ctx.strokeStyle = "#333"
ctx.lineWidth = 1
var step = 50 * scaleFactor
for (var x = 0; x < width; x += step) {
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, 15)
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
onClicked: {
currentPosition = mouse.x / width * videoPlayer.duration
}
}
}
3.3 FFmpeg视频合成
3.3.1 封装FFmpeg调用
在ffmpegwrapper.cpp中实现视频合成:
cpp复制bool FFmpegWrapper::burnSubtitles(const QString &videoPath,
const QString &subtitlePath,
const QString &outputPath)
{
QStringList args;
args << "-i" << videoPath
<< "-vf" << QString("subtitles=%1").arg(subtitlePath)
<< "-c:a" << "copy"
<< "-y" << outputPath;
QProcess ffmpeg;
ffmpeg.start("ffmpeg", args);
if (!ffmpeg.waitForStarted()) {
qWarning() << "FFmpeg启动失败";
return false;
}
ffmpeg.waitForFinished(-1);
return ffmpeg.exitCode() == 0;
}
3.3.2 字幕文件生成
实现SRT格式导出:
cpp复制void SubtitleManager::exportToSrt(const QString &filename) {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
return;
QTextStream out(&file);
for (const auto &item : m_subtitles) {
out << item.id << "\n"
<< formatTime(item.startTime) << " --> " << formatTime(item.endTime) << "\n"
<< item.text << "\n\n";
}
}
QString SubtitleManager::formatTime(qint64 ms) {
int hours = ms / 3600000;
int minutes = (ms % 3600000) / 60000;
int seconds = (ms % 60000) / 1000;
int msec = ms % 1000;
return QString("%1:%2:%3,%4")
.arg(hours, 2, 10, QLatin1Char('0'))
.arg(minutes, 2, 10, QLatin1Char('0'))
.arg(seconds, 2, 10, QLatin1Char('0'))
.arg(msec, 3, 10, QLatin1Char('0'));
}
4. 关键问题与解决方案
4.1 时间同步问题
现象:视频播放与字幕显示不同步
解决方案:
- 使用QtMultimedia的MediaPlayer作为基准时钟
- 实现自定义的时序控制器:
cpp复制class TimelineController : public QObject {
Q_OBJECT
public:
explicit TimelineController(QObject *parent = nullptr);
public slots:
void updatePosition(qint64 position) {
foreach (const auto &item, m_manager->getSubtitles()) {
auto subtitle = item.value<SubtitleManager::SubtitleItem>();
if (position >= subtitle.startTime && position <= subtitle.endTime) {
emit showSubtitle(subtitle.text);
break;
}
}
}
private:
SubtitleManager *m_manager;
};
4.2 性能优化
问题:大量字幕项导致界面卡顿
优化措施:
- 实现虚拟列表:只渲染可视区域内的字幕项
- 使用WorkerScript处理耗时操作:
qml复制WorkerScript {
id: subtitleWorker
source: "SubtitleProcessor.js"
onMessage: {
// 处理完成后的回调
}
}
function processSubtitles() {
subtitleWorker.sendMessage({subtitles: model});
}
5. 扩展功能实现
5.1 字幕样式编辑器
qml复制ColumnLayout {
ColorPicker {
title: "文字颜色"
currentColor: subtitleItem.color
onColorSelected: subtitleItem.color = color
}
FontComboBox {
currentFont: subtitleItem.font
onFontSelected: subtitleItem.font = font
}
Slider {
title: "阴影模糊"
value: subtitleItem.shadowRadius
onValueChanged: subtitleItem.shadowRadius = value
}
}
5.2 批量导出功能
cpp复制void BatchExporter::startExport(const QString &templateStr) {
QThreadPool::globalInstance()->start([this, templateStr]() {
for (int i = 0; i < m_items.count(); ++i) {
QString outputPath = templateStr.arg(i);
FFmpegWrapper::burnSubtitles(m_videoPath,
m_items[i].subtitlePath,
outputPath);
emit progressChanged(i * 100 / m_items.count());
}
});
}
6. 实际使用建议
-
快捷键配置:
- F5:播放/暂停
- Ctrl+←/→:逐帧调整
- Ctrl+S:快速保存
-
调试技巧:
bash复制# 启用QML调试 qmlscene --qtquick2.debug Main.qml -
部署注意事项:
- 打包时包含以下DLL:
- Qt5Core.dll
- Qt5Quick.dll
- Qt5Multimedia.dll
- avcodec-58.dll
- avformat-58.dll
- 打包时包含以下DLL:
我在实际开发中发现,将字幕渲染分为预处理和实时渲染两个阶段可以显著提升性能。预处理阶段计算所有字幕的位置和样式,实时渲染阶段只处理当前可见的字幕项。这种方法在4K视频编辑时尤其有效。
