简介:这是一份基于 Visual Studio 集成开发环境与 Qt 图形界面框架开发的视频播放器练习项目,主要面向正在学习 Qt 界面编程、想熟悉工程配置与构建流程的初学者,也可作为入门课程设计或自学实验的参考。项目实现了播放、暂停以及从文件夹中选取视频资源后点击播放的基础交互,麻雀虽小但结构完整,适合用来理解窗口部件布局、按钮信号槽和资源文件加载等基本用法。资源压缩包共 45 个文件,大小约 25.22MB;内容以 C++ 源码和界面描述文件为核心,包含源文件、头文件、界面文件、资源集合文件,同时带有工程解决方案和项目配置,编译生成的执行文件、目标文件、程序数据库文件、编译日志等中间产物,以及界面配套的按钮图标、背景图片和测试视频素材,既有可运行程序,也有中间构建输出,能直观看到从源码到最终程序的完整过程。已有 392 人学习浏览,作者也说明这是 VS+Qt 练习作品,读者可重点借鉴工程组织方式、界面设计思路和播放相关槽函数写法,并在现有框架上继续扩展功能。
1. 基于 VS—QT 的播放器:为什么值得亲手做一版
接到一个桌面端需求,要在一个已有系统里内嵌一个能播本地音视频、能拖进度条、之后还想接入自定义渲染的播放器,最省心的组合其实就是 VS—QT。用 Visual Studio 管理工程和调试,Qt 提供界面组件与多媒体框架,播放器的核心播放循环只需要很少代码,就能把「能跑的播放器」变成「能进产品形态的播放器」。真正劝退多数人的不是解码原理,而是环境那一步。这篇文章就按「把环境跑通 → 选对播放框架 → 写完整功能 → 发布验证」的顺序落一遍,适合要在 Windows 桌面交付播放能力、又不想被底层编解码细节拖住的开发者。先说结论:播放器本身没那么玄学,坑多半集中在版本匹配和信号联动上,这两处我会专门展开。
2. 把 Qt 工具链接进 Visual Studio:版本匹配、配置与第一个工程
2.1 为什么要选 MSVC 版 Qt:编译器不一致会直接翻车
Visual Studio 里写 C++ 用的是 MSVC 编译器,而 Qt 的库分 MinGW 版和 MSVC 版,这两者的二进制 ABI 完全不兼容。你装了 MinGW 版 Qt,VS 编译出来的 .obj 链接 Qt 的 .lib 时就会报一堆符号不匹配或者直接链接失败,这是新手环境搭建里最常见的一次翻车。所以在 Qt 在线安装器里选组件时,认准带 MSVC 的包,比如MSVC 2019 64-bit、MSVC 2022 64-bit,而不是带 MinGW 字样的组件。
版本匹配的大原则是:Qt 包的编译器标识要和 VS 用的平台工具集对应。VS2019 用的是 v142 工具集,VS2022 用的是 v143。低版本 VS 连高版本 MSVC 编译的 Qt 库通常没问题,反过来高版本 VS 去连老 Qt 包里的工具集会让你折腾半天。下面这张表可以直接当参考:
| Visual Studio 版本 | 平台工具集 | Qt 安装包中的编译器标识 |
|---|---|---|
| VS2019 | v142 | msvc2019_64 |
| VS2022 | v143 | msvc2022_64 |
老项目还在用 Qt 5.15 LTS 的话,注意在线安装器里 5.15 只提供 msvc2019 和 msvc2019_64,VS2022 用它通常也能编译过,但建议别混得太杂。我一般会先在本地固定一个「VS2022 + msvc2022_64 的 Qt」组合作为默认,其他版本只在一台机器上跑,避免不同机器构建结果不一致。
2.2 在 VS 里安装 Qt 扩展并配置 Qt Versions
环境搭建的第二步是在 VS 里装 Qt Visual Studio Tools 扩展。打开「扩展 → 管理扩展」,在联机搜索框里输入 Qt,找到 Qt Visual Studio Tools 安装,装完重启 VS,菜单栏里就会出现「扩展 → Qt VS Tools」。这一步没什么黑匣子,但不装扩展,VS 就不会自动帮你处理 .ui 文件编译、moc、资源文件这些 Qt 的构建环节。
装完扩展还不算完,必须告诉扩展我本机的 Qt 装在哪。打开「扩展 → Qt VS Tools → Qt Versions」,点新增,路径要指到 Qt 的 MSVC 目录,比如C:\Qt\6.x.x\msvc2019_64,不要指到 Qt 的根目录。版本号那一栏扩展会自动识别。配置完成后,在 Qt VS Tools 的默认版本下拉里要能看到刚加进去的版本,如果下拉是空的,说明路径指错了。
这里有个容易漏的细节:Qt 安装器默认只装几项,你得在组件列表里找到对应的 MSVC 那一项并勾上,同时把 Qt Multimedia 相关模块一并选上。播放器后面要用到的 QMediaPlayer、QAudioOutput、QVideoWidget 都依赖这些模块,不勾的话,工程能建出来,但代码一引用就报「未找到 Qt 多媒体头文件」。
2.3 新建第一个 Widgets 工程:模块、平台与验证代码
创建工程时,在 VS 的新建项目向导里如果能看到「Qt Widgets Application」模板,说明扩展已经生效。没有的话,回头检查 2.2 这步。向导会让你勾选模块,至少要选上 Core、Gui、Widgets、Multimedia、MultimediaWidgets,前四个是 Qt 程序的地基,最后一个负责视频渲染控件。
平台这里有个默认坑:向导会生成 x86 和 x64 两个平台配置,默认调试可能是 Win32。现在的 Qt 安装包大多只有 64 位库,你应该直接在配置管理器里把活动平台切到 x64,或者删掉 Win32 配置。切换后编译一下,再用下面这段代码验证环境是否真的通了:
#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("VS-QT 播放器环境验证"); label.resize(320, 120); label.show(); return app.exec(); }这段代码不涉及多媒体,只验证 VS、Qt 库、moc 和头文件搜索路径这一整条编译链路有没有连通。编译运行后能看到窗口弹出,说明工具链没问题。如果这一步报「无法打开包括文件 QApplication」或者链接时找不到 Qt6Widgets.lib,基本可以确定是 Qt Versions 的路径没配好,或者平台切换到了 x86。
环境配好之后,别急着写播放器,先用一个空 Qt Widgets 工程把目标平台固定成 Release x64 构建一次,确认 release 版也能跑。Debug 能过、Release 崩的场景在 Qt 里不少见,后面发布环节全靠 release 版,所以这一下能省后面很多事。
3. 播放核心选型:官方多媒体框架的边界与最小播放骨架
3.1 QMediaPlayer 到底能做什么、不能做什么
播放器最核心的部分不是界面,是解码、同步和渲染。Qt 官方给的答案是 QMediaPlayer 这套 API:一个类负责把媒体的加载、播放、暂停、跳转统一管理起来,配合 QAudioOutput 出声音、QVideoWidget 出画面。在 Windows 上,它底层会通过平台多媒体后端去解码,也就是说具体能播什么格式,取决于后端支持哪些封装和解码器。
对常见需求来说,这个边界很清晰:mp4、mkv、wmv 这类常见的,以及 mp3、wav、flac 这类音频,基本都能播。但如果你碰到特定编码的 flv、老旧 rmvb,或者需要内嵌字幕轨道做精细处理,QMediaPlayer 就可能力不从心,表现是打不开或者有声音没画面。它也不是为低延迟设计的,你要做直播推流、毫秒级同步,就不要指望这一层。
所以我的选型原则是:先确认需求是在「播放」这个层面结束,还是要在「帧」这个层面继续。前者用官方组件,后者老老实实上 FFmpeg。多数桌面播放器需求其实属于前者,包括给企业系统做培训视频播放、给工具软件加演示录像回放,这些用 QMediaPlayer 不仅代码少,而且和 Qt 的界面生命周期天然匹配。
3.2 用 QMediaPlayer 跑通最小可播放程序
最小可播放程序的关键是处理好 Qt 6 的 API 变化。Qt 5 里音频输出直接挂在 QMediaPlayer 上,Qt 6 则拆出一个 QAudioOutput 对象,必须用 setAudioOutput 把它挂上去,否则会出现「画面正常、没有声音」的诡异现象。完整的最小程序长这样:
#include <QApplication> #include <QMediaPlayer> #include <QAudioOutput> #include <QVideoWidget> #include <QUrl> int main(int argc, char *argv[]) { QApplication app(argc, argv); QMediaPlayer player; QAudioOutput audio; // Qt 6 中音频输出独立成对象 QVideoWidget video; player.setAudioOutput(&audio); player.setVideoOutput(&video); video.resize(960, 540); video.show(); // 用 QUrl::fromLocalFile 包装本地路径,不要直接传 QString player.setSource(QUrl::fromLocalFile("D:/sample/test.mp4")); player.play(); return app.exec(); }注意 setSource 入参是 QUrl,如果你的文件路径是D:/sample/test.mp4,QUrl::fromLocalFile 会自动把分隔符和编码处理妥当。如果偷懒直接 player.setSource("D:/sample/test.mp4"),路径里有中文字符或者空格时,会有概率解析成相对 URL,表现出来就是文件明明存在却打不开。
player.play() 是非阻塞调用,调用完立刻返回,真正的播放发生在事件循环里。视频播放的时长、画面比例变化这些信息都需要事件循环把信号送回来,所以 main 里最后必须 app.exec()。在这个程序里,你可以不在构造时指定文件,而是把 setSource 放到某个按钮的槽函数里,这就是后面 UI 化的基础。
3.3 什么时候换 FFmpeg:代价不在解码,在同步
当需求变成推流、逐帧算法、自定义滤镜或精确同步时,官方组件就兜不住了。常见做法是引入 FFmpeg 做解码,把解码出来的 QVideoFrame 或者原始 AVFrame 丢给 QOpenGLWidget 渲染,音频再走 QAudioOutput。这个方案的开发量不是加一个依赖那么简单,你要自己处理解码线程、音频时钟、视频时钟、丢帧策略和颜色空间转换,任何一个环节没对齐,音画不同步的锅就来了。
如果只是为了某一类特殊格式,临时切换到 FFmpeg 后端可能是更值得的中间路线。较新的 Qt 6 版本在 Windows 上提供了 FFmpeg 多媒体后端选项,安装 Qt 时将它作为一个模块勾选,播放器的格式覆盖就能扩展不少。这个方向对多数项目都够用,又能保留官方 API 的简单性。选择哪种,取决于你有没有看到「仍需在帧层面做算法」这条硬需求,没有就别急着上 FFmpeg。
4. 把播放器功能做全:进度同步、倍速、音量与控制布局
4.1 播放器窗口结构:预览区、控制条与文件打开
把第 3 章的最小程序扩展成完整播放器,核心是画出一个可用的界面骨架。我一般用 QVideoWidget 作为主预览区,底部放一个控制条,控制条里放打开文件按钮、播放暂停按钮、进度滑块和时间标签。用代码构建布局和用 .ui 文件拖控件效果一致,区别只在于 .ui 方式控件通过 ui->xxx 访问,代码方式直接持有成员变量。
播放器的打开文件动作通常接 QFileDialog,这一步不能忽略的是文件过滤器的写法。下面这段代码可以直接作为 PlayerWindow 类的成员函数使用,注意打开新文件前先 stop,避免出现播放状态残留。
void PlayerWindow::openFile() { QString filePath = QFileDialog::getOpenFileName( this, "选择媒体文件", QString(), "视频 (*.mp4 *.mkv *.avi *.wmv);;音频 (*.mp3 *.wav *.flac)"); if (filePath.isEmpty()) { return; // 用户取消了选择,不改变当前播放 } player.stop(); // 先复位上一次的播放状态 player.setSource(QUrl::fromLocalFile(filePath)); player.play(); }getOpenFileName 的过滤器分号前是显示文本,分号后是实际匹配的模式,多个后缀用空格隔开。filter 里写 *.mkv 不代表播放器就能播 mkv,这只是对话框帮你筛选文件名,真正决定能不能播的还是第 3 章说的后端。如果用户选了一个后端不支持的格式,播放器会进入媒体错误状态,后面避坑章节会讲怎么看错误消息。
4.2 进度条与时间标签:一组信号如何做到不互相打架
进度条联动是播放器里最容易写出「进度条一跳一跳」或者「拖动时往回弹」的地方。QMediaPlayer 会在播放过程中高频发出 positionChanged,而用户拖动滑块又会触发 sliderMoved,两组信号交汇时会产生竞争。处理办法是让滑块只有在用户未按住时才跟随播放器位置。
durationChanged 决定滑块的活动范围。注意这个信号什么时候来不确定,所以要把 setRange 放在信号回调里,而不是构造时硬编码。下面是一组可直接照用的构造代码:
// PlayerWindow 构造函数中,完成布局与信号连接 connect(&player, &QMediaPlayer::durationChanged, this, [=](qint64 duration) { slider->setRange(0, static_cast<int>(duration)); }); connect(&player, &QMediaPlayer::positionChanged, this, [=](qint64 pos) { if (!slider->isSliderDown()) { // 用户没按住滑块时才回写 slider->setValue(static_cast<int>(pos)); } timeLabel->setText(formatTime(pos) + " / " + formatTime(player.duration())); }); connect(slider, &QSlider::sliderMoved, this, [=](int value) { player.setPosition(value); // 拖动时直接跳转 });关键逻辑是 isSliderDown() 的判断。没有这个判断,用户拖动滑块时,播放器的 positionChanged 会不断把滑块拉回原位置,表现出来就是滑块像在抗拒你的手。而有了这个判断后,用户松手后下一次 positionChanged 又会把滑块对齐到实际播放位置,逻辑就完整了。
时间标签的格式函数 formatTime 建议写成QString::asprintf("%02d:%02d", (ms/60000), (ms%60000)/1000)的形式。当视频长度超过一小时,这个简单格式会变成 90:00 而不是 1:30:00,如果产品要支持长视频,你可以补一个小时位判断,但不要为了这个细节毁掉初版进度条逻辑。
4.3 倍速、音量与快捷键:三个常用参数的设置边界
播放器的交互细节里,倍速和音量是最容易被人试出来的功能。倍速通过 setPlaybackRate 实现,传 1.0 是正常速度,0.5 是半速,2.0 是两倍速。我建议 UI 上不要把范围拉太大,0.5 到 2.0 就够了,超过 2.0 之后很多文件的音画会肉眼可见地不同步,而且音频音调变化会很奇怪。
音量的正确姿势在 Qt 6 里是操作 QAudioOutput 对象,而不是直接调 player 的方法。setVolume 的范围是 0 到 100,静音用 setMuted(true)。一个容易踩的细节是:新建 QAudioOutput 时默认音量是 1.0(也就是 100),所以不要想当然认为初始是静音。
快捷键属于播放器「有没有质感」的分界线。下面这段代码把空格绑定到播放暂停,左右方向键绑定到快退快进 10 秒:
// 快捷键:空格 播放/暂停,方向键左/右 快退/快进 10 秒 QShortcut *spaceKey = new QShortcut(QKeySequence(Qt::Key_Space), this); connect(spaceKey, &QShortcut::activated, this, [=]() { if (player.playbackState() == QMediaPlayer::PlayingState) { player.pause(); } else { player.play(); } }); QShortcut *leftKey = new QShortcut(QKeySequence(Qt::Key_Left), this); connect(leftKey, &QShortcut::activated, this, [=]() { player.setPosition(player.position() - 10000); // 毫秒 }); QShortcut *rightKey = new QShortcut(QKeySequence(Qt::Key_Right), this); connect(rightKey, &QShortcut::activated, this, [=]() { player.setPosition(player.position() + 10000); });setPosition 跳转会清空播放缓冲,频繁触发会出现短暂卡顿,所以一般不做逐帧的按住连续跳,只做单击跳 10 秒就够了。如果你要按住方向键连续快进,需要自己加一个定时器合并跳转请求,否则会出现「越快越卡」的体验。这里还有一个没提到的小习惯:把播放器停到视频尾部后,再按播放键不会自动回到开头,需要监听媒体状态里的 EndOfMedia 信号,在里面主动 setPosition(0) 再 play,这是很多初版播放器漏掉的细节。
5. VS—QT 播放器开发避坑:5 个高频问题的现象、原因与处理
5.1 编译报「无法打开包括文件 QApplication」
- 现象:新建工程后按 F5,编译直接失败,提示找不到 QApplication 头文件,或者链接阶段报无法打开 Qt6Widgets.lib。
- 原因:绝大多数情况是 Qt VS Tools 扩展里没有配置 Qt Versions,或者路径指向了 Qt 根目录而不是 msvc 子目录,VS 不知道该去哪找头文件和库文件。少数情况是工程的平台选成了 Win32,而本机只有 64 位 Qt 库。
- 解决:打开「扩展 → Qt VS Tools → Qt Versions」,重新新增路径并指向
C:\Qt\6.x.x\msvc2019_64这类带编译器标识的目录,确认默认版本变成有效值。然后把工程活动平台切到 x64,重新生成。
5.2 能听到声音但画面黑屏
- 现象:文件可以正常播放,音频正常输出,但 QVideoWidget 区域一直黑着不渲染画面。
- 原因:两种情况。一是 setVideoOutput 没有真正把视频输出连到视频控件上,Qt 6 里没有这个连接就默认丢帧不渲染;二是显卡驱动或者硬件渲染通道问题,某些 Windows 机器上 OpenGL 后端初始化失败,画面就一直黑着。
- 解决:先检查代码里是否调用过 player.setVideoOutput(videoWidget),确认之后再加一句 qDebug() 打印 player.error() 和 mediaStatus 的值,看播放器有没有报告解码错误。排除代码问题后,换一台机器或者更新显卡驱动验证;在较新的 Qt 6 上也可以切换官方提供的 FFmpeg 多媒体后端再试。
5.3 中文路径和空格路径打不开
- 现象:文件是英文名时一切正常,换成中文名或带空格的路径,播放器没有任何反应,甚至直接进入媒体错误状态。
- 原因:把本地路径字符串直接传给 setSource 时,QUrl 会按相对 URL 去解释,中文字符和空格在 URL 规范里需要转义,没转义就解析失败。代码文件本身的编码问题还会叠加一层乱码。
- 解决:任何本地文件统一走 QUrl::fromLocalFile(path)。这个静态方法会把路径按本地文件语义转成正确的 QUrl,空格、中文都会处理,不要自己拼字符串。如果播放目录来自配置文件,还要确保读取配置文件时用正确的编码,常见做法是统一按 UTF-8 读入。
5.4 连续打开多个文件后内存只增不减
- 现象:播放器运行一整天,每开一个新视频就发现任务管理器里内存占用上涨,而且关掉视频窗口也没有回落。
- 原因:通常是 QMediaPlayer 对象被反复 new,且旧对象没有被释放。Qt 的对象树机制只有在父对象销毁时才会级联 delete,如果你每次 openFile 都
player = new QMediaPlayer(this)并且没有主动 deleteLater,旧播放器累积的解码缓冲就会一直占着内存。 - 解决:让 QMediaPlayer 成为 PlayerWindow 的成员对象,而不是每次打开文件新建。如果确实要换播放器实例,先 stop(),再调用 deleteLater()。还有一个需要注意的场景:切换源之前不 stop,整个缓冲会带着旧文件的临时数据,看起来也是内存不降,所以先圣战写法是 stop() 在前、setSource 在后。
5.5 开发机能跑,部署到别的机器报缺 DLL
- 现象:把 Release 版的 exe 拷贝到另一台没装 Qt 的机器上,双击直接弹窗提示找不到 Qt6Core.dll、Qt6Multimedia.dll 或其他 DLL。
- 原因:Qt 不是纯静态库,exe 运行时需要一堆 Qt 动态库和插件。如果只拷了 exe 而没有拷贝 plugins 目录,程序连 platform 插件都加载不到,窗口都弹不出来。
- 解决:用 Qt 自带的 windeployqt 工具做部署,把需要的 DLL 和插件目录自动拷到 exe 旁边,并确认 platforms 目录和多媒体插件目录都在。为了确认拷全了,可以临时把开发机上的 Qt 路径从 PATH 里去掉再运行,让程序只能用 exe 旁边的 DLL,这样能暴露依赖缺失。
6. 发布 VS—QT 播放器:打包、验证与自定义渲染的扩展点
播放器写完了,最后一公里是把它交付出去。常见做法是用 Qt 的 windeployqt 完成依赖收集,命令长这样:
C:\Qt\6.x.x\msvc2019_64\bin\windeployqt.exe --release --no-translations D:\build\myPlayer\release\myPlayer.exe执行完检查 exe 所在目录,至少应出现 Qt6Core.dll、Qt6Gui.dll、Qt6Widgets.dll、Qt6Multimedia.dll,以及 platforms 目录里的 qwindows.dll 和多媒体插件目录。缺了任何一项,换台干净机器就会当场翻车。如果目标机器没装 Visual C++ 运行库,release 版还会报找不到 VCRUNTIME140.dll,解决方案是把 VC 运行库安装包放进部署目录,让用户先装运行库。
验证这一步别偷懒。我一般会把整个部署目录压成一个压缩包,在一台干净机器上解压运行,从打开文件到拖动进度条到切换倍速全部过一遍。只验证开发机能跑是不够的,播放器这东西对运行环境的敏感度远高于普通表单程序,硬件解码、显卡驱动都会影响最终表现。
如果你未来要在播放器上做逐帧分析、截图或者自定义滤镜,Qt 6 提供了一个干净的扩展点:继承 QAbstractVideoSink 重写 present 方法,就可以拿到每一帧的 QVideoFrame。到时候播放器的职责仍然是播放,但你等于在播放链路上加了一个自定义出口,不用改成 FFmpeg 方案也能做不少事情。我自己的习惯是,初版永远先接官方多媒体组件跑通全流程,等到确实出现「官方这套接不住」的需求,再决定要不要往底层换;这套流程走下来,踩过最深的坑反而是最开始版本匹配那一步。希望这篇能帮到你少走几步弯路。
本文还有配套的精品资源,点击获取