简介:这是一份基于 QT C++ 编写的简易音乐播放器源码工程,面向刚接触 Qt 桌面开发或多媒体模块的初学者,帮助理解音乐播放器的基础功能与实现流程。压缩包共8个文件,包含 cpp 源文件、头文件、ui 界面文件以及 pro 工程配置等,整体仅10KB,结构紧凑、便于快速阅读。资源已有 399 人学习下载。代码实现了播放、暂停、列表循环等核心功能,并附带个人注解;在工程中通过引入 multimedia 模块,使用 QMediaPlayer 与 QMediaPlaylist 管理音频播放与播放列表,借助 QWidget、QLayout 设计控制界面,再以信号槽连接按钮操作,同时展示了音量滑动条、歌曲列表切换等交互细节。对于希望快速上手 Qt 多媒体编程的开发者而言,这份资源提供了可直接运行学习的完整示例,既能用于课程设计,也可作为扩展开发智能播放器的起点。
1. 用 QT C++ 写音乐播放器的第一步:先辨认谁是播放器,谁是界面
拿 Qt 和 C++ 写一个音乐播放器,最常见的偏差是直奔按钮和样式表,最后得到一个看起来能操作、但文件拖进去毫无反应的工具。真正决定这个项目成败的,是解码、输出、进度回传这一条链路,而不是 QPushButton 摆放得是否整齐。
下面这条路径围绕“基础功能”展开:能打开本地音频、能播放/暂停/拖动、能显示播放进度和音量、能切到下一首,再把发布到另一台机器上的收尾问题一起处理。实现时主选 Qt 6 的新接口,也会指出 Qt 5.15 里对应的旧写法和迁移注意点。
如果你已经会用 Qt Widgets 摆窗口,但对 QMediaPlayer、QAudioOutput、QMediaPlaylist 这些类之间的边界还比较模糊,这篇文章正好帮你在动手前把架构决策定下来,而不是边写边改。
2. 组装播放器骨架:QMediaPlayer 和 QAudioOutput 的最小链路
2.1 为什么播放器要拆成两个类:Qt 5 到 Qt 6 的接口变化
很多老式 Qt 教程里,音乐播放器的初始化代码长这样:
// Qt 5.14 之前的旧写法,现在不推荐 player = new QMediaPlayer(this); player->setMedia(QUrl::fromLocalFile(filePath)); // setMedia 在 Qt 5.15 后被标记为弃用 player->setVolume(60); // 音量挂在播放器上 player->play();这段代码在 Qt 5.15 里还能编译,但切换到 Qt 6 后,setMedia()和setVolume(int)直接消失。Qt 6 把“播放状态机”和“音频输出”拆成两个对象:QMediaPlayer负责媒体源、播放状态、播放位置、元数据这些信号和数据;真正把解码后的音频数据送到系统声卡的职责,交给QAudioOutput。
拆分带来的实际收益是:音量调节、静音、音频设备切换从播放器里剥离后,UI 层可以独立控制输出,播放器本身不需要知道声卡长什么样。如果以后要接可视化均衡器或多路输出,这个边界会省很多事。
两个版本的核心对应关系如下:
| 能力 | Qt 5.15 旧写法 | Qt 6.x 新写法 |
|---|---|---|
| 加载本地文件 | player->setMedia(QUrl(...)) | player->setSource(QUrl(...)) |
| 关联音频输出 | 不需要 | player->setAudioOutput(&output) |
| 设置音量 | player->setVolume(0~100) | output->setVolume(0.0~1.0) |
| 静音 | player->setMuted(true) | output->setMuted(true) |
2.2 最小可播放代码:先让一个文件响起来
在 Qt 6 项目里,构造函数中最少需要这样几条:
// PlayerWindow 构造函数内部 QAudioOutput *audioOutput = new QAudioOutput(this); // 输出设备 QMediaPlayer *player = new QMediaPlayer(this); // 播放器主体 player->setAudioOutput(audioOutput); // 先绑定输出,再加载媒体 audioOutput->setVolume(0.6f); // 范围是 0.0 ~ 1.0 player->setSource(QUrl::fromLocalFile("D:/music/测试.mp3"));这里有两个参数要点:一是setAudioOutput()必须在setSource()之前完成,否则play()虽然能调用,但没有任何声音到达声卡;二是QAudioOutput::setVolume()的参数是浮点数,而不是旧版那个 0~100 的整数,UI 上如果放的是QSlider,需要自己把滑块值换算成 0.0~1.0。
之所以不紧接着调用player->play(),是因为本地文件还没缓冲完时执行play(),某些后端会直接忽略。更稳妥的做法是等mediaStatusChanged状态变成BufferedMedia再播放:
connect(player, &QMediaPlayer::mediaStatusChanged, this, &PlayerWindow::handleMediaStatus); void PlayerWindow::handleMediaStatus(QMediaPlayer::MediaStatus status) { if (status == QMediaPlayer::LoadedMedia || status == QMediaPlayer::BufferedMedia) { player->play(); } }提示:
play()之后马上读playbackState(),多数情况下得到的还是StoppedState,这不代表播放失败,只是后端还没来得及切换状态。
2.3 用 QFileDialog 把本地音频文件接进播放链路
基础功能必须有“打开文件”入口。下面的代码负责从磁盘选择音频文件,并把路径交给播放器:
void PlayerWindow::openAudioFile() { QString filePath = QFileDialog::getOpenFileName( this, tr("选择音频文件"), QStandardPaths::writableLocation(QStandardPaths::MusicLocation), tr("音频文件 (*.mp3 *.wav *.flac *.ogg *.m4a)")); if (filePath.isEmpty()) return; player->setSource(QUrl::fromLocalFile(filePath)); }QFileDialog 的第一个参数是父窗口,第四个参数是过滤器,它只影响对话框里能选的文件类型,不影响解码能力。这里故意不在函数末尾调用play(),而是依靠上面mediaStatusChanged里的逻辑自动播放,避免用户双击一个较大的 flac 文件后界面卡住。
QUrl::fromLocalFile()是必须的:本地路径如果包含中文、空格或 Windows 盘符,直接丢给QUrl构造函数很可能会丢失分割符。从QUrl再取回路径时用toLocalFile(),不要用toString(),后者拿到的字符串仍带file:///前缀。
3. 给播放器接进度条与音量:QSlider 的三种连线方式
3.1 进度条的单位是毫秒,不是秒
把QSlider放在窗口底部,左边一个当前时间,右边一个总时长。播放器侧需要两个信号配合:
connect(player, &QMediaPlayer::durationChanged, this, &PlayerWindow::onDurationChanged); connect(player, &QMediaPlayer::positionChanged, this, &PlayerWindow::onPositionChanged);对应的两个槽:
void PlayerWindow::onDurationChanged(qint64 duration) { ui->progressSlider->setRange(0, static_cast<int>(duration)); ui->totalTimeLabel->setText(msToText(duration)); } void PlayerWindow::onPositionChanged(qint64 pos) { if (!ui->progressSlider->isSliderDown()) { // 用户拖拽时不要刷新 ui->progressSlider->setValue(static_cast<int>(pos)); } ui->currentTimeLabel->setText(msToText(pos)); }这里的单位是毫秒,qint64是 Qt 统一的 64 位整数类型。QSlider::setRange()接收 int,所以直接static_cast<int>(duration)截断;对本地音乐文件来说,常见时长不会超过 int 上限,这个转换是安全的。
isSliderDown()判断滑块是否正被鼠标按住。如果不加这个判断,用户拖动进度条的过程中,positionChanged不断把滑块拉回原处,看起来就是“拖不动”。加上之后,拖动行为优先,松手后进度条自然回到新位置。
时间格式化函数单独抽出来:
QString PlayerWindow::msToText(qint64 ms) { QTime t(0, 0, 0); t = t.addMSecs(ms); return t.toString("mm:ss"); }超过一小时的音频建议改成toString("hh:mm:ss"),否则 90 分钟的文件会显示成 90:00,语义不对。
3.2 拖动进度条:用 sliderMoved,别用 valueChanged
拖动进度条触发转跳,正确连接是:
connect(ui->progressSlider, &QSlider::sliderMoved, this, &PlayerWindow::onSliderMoved); void PlayerWindow::onSliderMoved(int positionMs) { player->setPosition(positionMs); }sliderMoved只在用户拖动时发射,程序内部setValue()不会触发它。反过来如果连接valueChanged,会碰到一个隐蔽的循环:程序把进度回写到滑块时触发valueChanged,槽里再调setPosition,positionChanged又回写滑块,状态互相追赶,进度条就会抖动或漂移。用sliderMoved把“用户输入”和“程序回写”两条路径彻底分开。
setPosition()的参数同样是毫秒,QSlider 的值域已经和播放位置单位统一,不需要额外换算。
3.3 音量滑块与 Qt 5.15 的兼容写法
音量放在界面上做成一个 0~100 的 QSlider,Qt 6 的映射代码是:
connect(ui->volumeSlider, &QSlider::valueChanged, this, &PlayerWindow::onVolumeChanged); void PlayerWindow::onVolumeChanged(int value) { audioOutput->setVolume(value / 100.0f); }注意 Qt 6 的setVolume接收 0.0~1.0,所以滑块必须除以 100。如果项目还在用 Qt 5.15.2 这种带 msvc2019_64 的环境,根本没有QAudioOutput,就得退回旧接口:
// Qt 5.15.2 分支 player->setVolume(ui->volumeSlider->value()); // 旧接口直接用 0~100在代码里可以用 Qt 版本宏做条件编译,也可以把两类播放器对象都封装到自己的MediaEngine类里。条件编译虽然简单,但会让界面代码出现大量分支,我一般建议只在升级过渡期使用,最终还是要统一到 Qt 6 接口上。
4. 播放列表与封面:QMediaPlaylist 消失后的 Qt 6 处理方式
4.1 Qt 5.15 里 QMediaPlaylist 的典型用法
如果你搜到的参考资料基于 Qt 5.15,播放列表通常这么写:
// Qt 5.15 中的播放列表 QMediaPlaylist *playlist = new QMediaPlaylist(this); playlist->addMedia(QUrl::fromLocalFile(filePath)); playlist->setPlaybackMode(QMediaPlaylist::Loop); player->setPlaylist(playlist); playlist->setCurrentIndex(0);QMediaPlaylist在 Qt 5.15 用得顺手,是因为它自带几种切歌模式,无需自己维护索引:
| PlaybackMode | 行为 |
|---|---|
Sequential | 顺序播放,播完最后一个停止 |
Loop | 列表循环,播完最后一个回到第一个 |
CurrentItemOnce | 当前这曲播完就停,不自动切歌 |
CurrentItemInLoop | 单曲循环当前文件 |
问题在于 Qt 6 中QMediaPlaylist和相关接口被整体移除。如果你的学习资料只讲 QMediaPlaylist,换到新版本后连编译都过不去。这不是“接口改名”,是把老 API 直接废弃掉,移动端和桌面端的媒体框架在 Qt 6 里重新统一过了。
4.2 Qt 6 下手写播放列表:QStringList 与模运算
Qt 6 官方不提供内置播放列表类,通常做法是自己维护一个QStringList和当前索引:
// PlayerWindow 内部状态 QStringList trackList; int currentIndex = -1; void PlayerWindow::appendToPlaylist(const QString &filePath) { trackList << filePath; if (currentIndex == -1) { currentIndex = 0; player->setSource(QUrl::fromLocalFile(trackList.at(currentIndex))); } } void PlayerWindow::playNext() { if (trackList.isEmpty()) return; currentIndex = (currentIndex + 1) % trackList.size(); player->setSource(QUrl::fromLocalFile(trackList.at(currentIndex))); }% trackList.size()是实现列表循环的关键:索引越界时自动绕回 0。上一首的方向相反,需要写成(currentIndex - 1 + trackList.size()) % trackList.size(),加一个size()是为了避免负数取模得到负索引。
列表模式下还要处理“当前音频播完自动下一首”:
connect(player, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status) { if (status == QMediaPlayer::EndOfMedia) playNext(); });这个 lambda 里的EndOfMedia状态只在媒体自然播放结束时出现,手动stop()不会触发,所以不会出现“用户暂停一下结果自动跳了”的误判。
4.3 用 QMediaMetaData 读标题和封面,不再手写 ID3 解析
播放器界面如果想显示“歌手 - 歌名”和封面,常用的做法是直接用播放器自带的元数据解析:
connect(player, &QMediaPlayer::metaDataChanged, this, &PlayerWindow::onMetaDataChanged); void PlayerWindow::onMetaDataChanged() { QString title = player->metaData(QMediaMetaData::Title).toString(); QString artist = player->metaData(QMediaMetaData::AlbumArtist).toString(); if (title.isEmpty()) title = QFileInfo(player->source().toString()).completeBaseName(); ui->titleLabel->setText( artist.isEmpty() ? title : artist + " - " + title); QVariant coverVariant = player->metaData(QMediaMetaData::CoverArtImage); if (coverVariant.canConvert<QImage>()) { QImage cover = qvariant_cast<QImage>(coverVariant); ui->coverLabel->setPixmap( QPixmap::fromImage( cover.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation))); } }metaData()的返回值是QVariant,必须根据键名转成对应类型。标题和歌手用toString(),封面则先判canConvert<QImage>(),防止拿到空QVariant时直接转换导致崩溃。
封面图的坑主要在 flac 文件上:很多 flac 并不内嵌封面,或者内嵌的是非 QImage 能直接识别的格式,此时canConvert返回 false,这是正常的。界面里保留一张默认占位图即可。字段名本身在两代 Qt 里基本稳定,QMediaMetaData::Title和QMediaMetaData::AlbumArtist在 5.15 与 6.x 都能用。
如果后面要做 Qt 国际化,这个界面里所有tr()包裹的字符串都可以交给 Qt 语言家处理;中文字符串直接裸露写死会给翻译阶段增加成本。
5. 播放器交付前:信号日志验证与 windeployqt 发布两个技巧
5.1 把关键信号打成日志,缩短定位“没声音”的时间
开发过程中最难受的是点播放没反应,又不知道断在哪一环。我会在初始化连接后面顺手加两组调试输出:
connect(player, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status) { qDebug() << "MediaStatus:" << status; }); connect(player, &QMediaPlayer::errorOccurred, this, [this](QMediaPlayer::Error error, const QString &errorString) { qDebug() << "Error code:" << error << errorString; });以播放 mp3 为例,正常状态下日志应依次出现LoadingMedia、LoadedMedia、BufferedMedia,最后是EndOfMedia。如果卡在LoadingMedia不动,多半是文件路径问题;如果能看到BufferedMedia却听不到声音,就去检查audioOutput是否绑定、系统音量是否被静音。
errorOccurred在 Qt 5.15 中已经存在,参数里的QMediaPlayer::Error是枚举,errorString会带解码器或后端的具体报错。日志里这两个值一起打,比单独看界面状态有用得多。
5.2 用 windeployqt 发布,别再手动拷 DLL
在 Windows 上把播放器交给别人之前,Release 构建目录里通常只有 exe。需要手动拷贝 Qt 的 DLL 和插件是常见的发布错误。标准处理是用 Qt 自带的部署工具:
# 在 Qt 安装目录的命令行环境中执行 windeployqt D:\build\release\PlayerApp.exe命令没有特殊参数,执行后它会自动把 Qt6Core、Qt6Gui、Qt6Multimedia、Qt6Widgets 以及音视频插件拷贝到 exe 所在目录,并生成platforms子目录。
发布后最常见的报错是:
qt.qpa.plugin: Could not find the Qt platform plugin "windows"这表示platforms目录不在 exe 同级目录下。处理办法是把platforms/qwindows.dll放到 exe 旁的 platforms 目录里,而不是照网上说的去设置QT_QPA_PLATFORM_PLUGIN_PATH环境变量——环境变量只解决本机临时调试,换一台机器还是会复发。
完整部署后,建议把整个目录复制到纯英文路径下再运行一次。中文用户名或中文目录在某些环境下会引入编码转换问题,虽然 Qt 6 改善明显,但播放器在真实用户手里不如直接在干净环境里验一轮。
本文还有配套的精品资源,点击获取