Qt6/C++音乐播放器开发实战:从音频引擎到发布部署
2026/9/12 16:28:34 网站建设 项目流程

简介:这是一份面向QT C++初学者的音乐播放器实战代码包,围绕QMediaPlayer与QMediaPlaylist展开,覆盖播放、暂停、列表循环、音量控制等基本功能,适合正在学习Qt多媒体模块或需要课程设计的开发者直接参考。资源共8个文件,压缩包仅10KB,包含C++源文件、头文件、UI界面文件、工程配置pro文件及说明文档,代码结构简洁,附有个人注解,便于对照学习,也适合作为二次开发的基础。目前已有399人学习下载。项目采用标准Qt Widgets架构,mainwindow.h与mainwindow.cpp职责清晰,mainwindow.ui可快速调整播放器界面;借助该资源可以掌握在.pro中添加multimedia模块、初始化QMediaPlayer与QMediaPlaylist、通过信号槽绑定播放/暂停按钮、利用QListWidget展示歌曲并切换曲目、设置循环播放及音量调节等完整流程。附带的文本说明还指出了不支持中文路径等注意事项,能帮助新手少走弯路,是一份轻量但包含核心要点的入门示例,对理解Qt多媒体编程很有帮助。

1. 用 Qt/C++ 做音乐播放器,先解决播放内核问题

用 Qt/C++ 写一个音乐播放器,初学者最容易照着旧教程把 QMediaPlayer 拖进界面,然后编译时被 Qt 6 的接口改动打懵。Qt 5 里一个 QMediaPlayer 同时管理音量和播放列表,Qt 6 里 QMediaPlayer 只管播放控制,音频输出需要独立的 QAudioOutput,播放列表也不再默认提供。下面这套实现以 Qt 6.5 为例,先把本地文件播放跑通,再补播放列表、进度拖动、音量调节和部署发布。你不用先学完 Qt Multimedia 的所有类,只要理解这几个核心类怎么组合起来,就能自己扩展出覆盖常见需求的桌面播放器。这份路径对已经会写 C++ 但没接触过 Qt 界面的开发者,同样可以作为第一个 Qt 小工具来练手。

2. 播放引擎的最小骨架:QMediaPlayer 与 QAudioOutput 的分工

在 Qt 6 里播放一首 mp3,最少需要两个对象:QMediaPlayer 负责加载文件、播放、暂停和跳转,QAudioOutput 负责把声音交给系统音频设备并控制音量、静音。很多“点了播放没声音”的问题,不是文件坏了,是这两个对象没绑定,或者其中一个提前被销毁。

2.1 为什么 Qt 6 强制把音频输出拆出来

Qt 5 的 QMediaPlayer 把播放控制和音频输出耦合在一起,接口上简单,但想换一块声卡、想单独调整输出设备,都要绕回播放器本身。Qt 6 把这个边界拆开:QMediaPlayer 只处理媒体状态、播放位置和元数据,QAudioOutput 负责输出设备、音量比例和静音标志。好处是播放逻辑和渲染输出解耦,代价是刚上手的人容易漏掉setAudioOutput这一步。

如果你还在用 Qt 5.15.2 msvc2019_64,这套 Qt 6 代码需要做三处替换:player.setAudioOutput(&audioOutput)改成player.setVolume(80)player.setSource(QUrl::fromLocalFile(path))改成player.setMedia(QUrl::fromLocalFile(path)),音量范围从 0.0~1.0 改成整数 0~100。大量“同一段代码为什么编译不过”的问题,根源都是版本差异。

还有一个隐藏细节是生命周期。QAudioOutput 不能作为局部变量在 lambda 或构造函数栈上创建后立刻销毁,否则 QMediaPlayer 内部持有的音频输出引用失效,程序不会立刻报错,但声音就断了。这两个对象建议都做成 PlayerWindow 的成员变量,直到窗口关闭才释放。

2.2 最小可运行代码:打开文件就能出声

新建 Qt Widgets Application,main.cpp 内容如下:

#include <QApplication> #include <QMediaPlayer> #include <QAudioOutput> #include <QFileDialog> #include <QPushButton> #include <QVBoxLayout> #include <QWidget> #include <QDir> int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.setWindowTitle("Minimal Qt Player"); QMediaPlayer player; QAudioOutput audioOutput; player.setAudioOutput(&audioOutput); audioOutput.setVolume(0.8); QPushButton *openButton = new QPushButton("选择文件并播放"); QPushButton *pauseButton = new QPushButton("暂停/继续"); QVBoxLayout *layout = new QVBoxLayout(&window); layout->addWidget(openButton); layout->addWidget(pauseButton); QObject::connect(openButton, &QPushButton::clicked, &app, [&]() { QString path = QFileDialog::getOpenFileName( &window, "选择音频文件", QDir::homePath(), "音频文件 (*.mp3 *.wav *.flac *.m4a)"); if (path.isEmpty()) return; player.setSource(QUrl::fromLocalFile(path)); player.play(); }); QObject::connect(pauseButton, &QPushButton::clicked, &app, [&]() { if (player.playbackState() == QMediaPlayer::PlayingState) player.pause(); else player.play(); }); window.resize(320, 120); window.show(); return app.exec(); }

这段代码里四个关键点。setAudioOutput(&audioOutput)必须执行,漏掉这行,播放器用了默认的空输出,整个程序没有任何音频设备,播放状态正常但听不到声音。audioOutput.setVolume(0.8)是浮点比例,范围 0.0~1.0,不是 Qt 5 的整数逻辑。setSource(QUrl::fromLocalFile(path))要求传 QUrl,不能用普通 QString,带中文空格路径也不会有问题。暂停按钮通过playbackState()判断当前状态,再决定调用pause()还是play(),比维护一个布尔标志位可靠。

2.3 编译配置与两个典型编译错误

工程文件用 qmake 写法最少,新建 player.pro,内容如下:

QT += multimedia widgets CONFIG += c++17 TARGET = qt_player SOURCES += main.cpp

CMake 版本对应这样配置,适合用 CLion 或命令行构建的工程:

cmake_minimum_required(VERSION 3.16) project(qt_player LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia) qt_add_executable(qt_player main.cpp) target_link_libraries(qt_player Qt6::Widgets Qt6::Multimedia)

.pro和 CMake 都不能漏multimedia模块,少了这个组件,QMediaPlayer头文件都找不到。Qt Creator 里构建套件要和安装的编译器一致,MSVC 套件配 MinGW 的 Qt 或反过来,都会在链接阶段报一堆 undefined reference。我一般让新手直接装 Qt 6 的 mingw 版本配 Qt Creator,少碰环境变量。

| 报错信息 | 常见原因 | 处理方式 | |error: 'QAudioOutput' file not found| 工程文件没加 multimedia 模块 | .pro 里补QT += multimedia,CMake COMPONENTS 里补 Multimedia | |no member named 'setAudioOutput' in QMediaPlayer| 用 Qt 5 的头文件和库编译 Qt 6 代码 | 确认 qmake/cmake 选择的是 Qt 6,或按 Qt 5 API 改写 | |undefined reference to QMediaPlayer| 构建套件与 Qt 库架构不一致 | 在 Qt Creator 里重新选择匹配的构建套件,删除 build 目录重新编译 |

Qt 国内镜像下载安装包时注意选对包名,官方在线安装器里 Qt 6.5 的Qt Multimedia大概在Qt > Qt 6.5.3 > Additional Libraries分组下,只装 Qt Base 是编译不过的。

3. 播放列表:QListView + QStringListModel 管理歌曲队列

很多播放器项目直接使用 QListWidget 存放歌曲名,数据量小没问题,但你会遇到一个实际麻烦:QListWidget 的每个 item 只是字符串,要绑定完整路径只能往 Qt::UserRole 里塞数据,代码绕一圈。更直接的做法是让数据模型和视图分开。

3.1 为什么不直接用 QListWidget

QListWidget 是把数据存进视图内部,适合“写十行代码就不改了”的场景。QListView + QStringListModel 把歌曲列表和界面展示分开:文件名列表只是字符串数组,路径列表单独存一个 QStringList,两个列表通过同样的行号对应。后面要加双击切换、自动下一首、清空列表、刷新目录,都不用去碰视图内部数据。

播放器窗口类的头文件建议这样组织:

class PlayerWindow : public QWidget { Q_OBJECT public: explicit PlayerWindow(QWidget *parent = nullptr); private slots: void openFolder(); void onPlaylistDoubleClicked(const QModelIndex &index); void onMediaStatusChanged(QMediaPlayer::MediaStatus status); private: void loadFolder(const QString &folderPath); void playAt(int row); void playNext(); QMediaPlayer *m_player = nullptr; QAudioOutput *m_audioOutput = nullptr; QListView *m_playlistView = nullptr; QStringListModel *m_playlistModel = nullptr; QStringList m_trackNames; QStringList m_trackPaths; int m_currentRow = -1; };

m_trackNames 和 m_trackPaths 的行号一一对应,QStringListModel 只负责喂给视图显示,路径是播放逻辑的原始数据。这样构造函数里初始化代码的顺序就不会错:先 new QMediaPlayer 和 QAudioOutput,再 new QListView 和 QStringListModel。

3.2 遍历目录:QDir 过滤与加载到列表

在播放器界面里放一个“打开目录”按钮,下面这段代码负责扫描文件夹:

void PlayerWindow::openFolder() { QString folderPath = QFileDialog::getExistingDirectory( this, "选择歌曲目录", QDir::homePath()); if (folderPath.isEmpty()) return; loadFolder(folderPath); } void PlayerWindow::loadFolder(const QString &folderPath) { QDir dir(folderPath); QStringList filters; filters << "*.mp3" << "*.wav" << "*.flac" << "*.ogg" << "*.m4a"; // 只扫描当前目录;需要子目录用 QDirIterator,见下方说明 QFileInfoList fileList = dir.entryInfoList(filters, QDir::Files); m_trackNames.clear(); m_trackPaths.clear(); for (const QFileInfo &info : fileList) { m_trackNames << info.fileName(); m_trackPaths << info.absoluteFilePath(); } // 中文文件名按系统区域设置排序,避免拼音乱序 dir.setSorting(QDir::Name | QDir::LocaleAware); m_playlistModel->setStringList(m_trackNames); if (!m_trackPaths.isEmpty()) playAt(0); }

entryInfoList返回 QFileInfo 列表,fileName()拿显示名,absoluteFilePath()拿绝对路径,播放和显示各取所需。filters用的是 QDir::Files,只留文件,不包含子目录。setSorting(QDir::Name | QDir::LocaleAware)对中文文件名有实际效果,不加的话顺序可能乱。如果你想支持子目录递归,把entryInfoList换成QDirIterator(folderPath, filters, QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot),但要注意重名文件和目录层级导致的排序问题,简单播放器先用单目录更稳。

3.3 双击播放与 EndOfMedia 自动下一首

双击列表某一行,触发播放:

void PlayerWindow::playAt(int row) { if (row < 0 || row >= m_trackPaths.size()) return; m_currentRow = row; m_player->setSource(QUrl::fromLocalFile(m_trackPaths.at(row))); m_player->play(); QModelIndex index = m_playlistModel->index(row); m_playlistView->setCurrentIndex(index); m_playlistView->scrollTo(index); }

构造函数里连接双击信号:

connect(m_playlistView, &QListView::doubleClicked, this, &PlayerWindow::onPlaylistDoubleClicked);

onPlaylistDoubleClicked 槽里直接调用 playAt 即可:

void PlayerWindow::onPlaylistDoubleClicked(const QModelIndex &index) { playAt(index.row()); }

自动切歌靠 QMediaPlayer::mediaStatusChanged 信号。播放到文件末尾,媒体状态变成 EndOfMedia,这时触发下一首:

void PlayerWindow::onMediaStatusChanged(QMediaPlayer::MediaStatus status) { if (status == QMediaPlayer::EndOfMedia) playNext(); } void PlayerWindow::playNext() { if (m_trackPaths.isEmpty()) return; int next = m_currentRow + 1; if (next >= m_trackPaths.size()) next = 0; playAt(next); }

QMediaPlayer 的 MediaStatus 枚举里,和播放器业务最相关的几个状态:

| 枚举值 | 触发时机 | 需要处理的业务 | | LoadedMedia | setSource 后媒体加载完成 | 此时 durationChanged 才准确,可启用进度条 | | BufferingMedia | 网络流或大文件缓冲 | 进度条可以显示缓冲,但不需要弹错误 | | StalledMedia | 数据读取跟不上播放 | 不要把卡顿误判成崩溃 | | EndOfMedia | 当前文件播放完毕 | 自动切歌或停止 | | InvalidMedia | 文件损坏或格式不支持 | 配合 errorOccurred 弹提示 |

注意 EndOfMedia 对本地文件是稳定触发的;但如果播放过程中手动拖动进度条到末尾,某些 Qt 版本下不会进入 EndOfMedia,这种边界暂时不用管,先保证歌曲自然放完能切歌。

4. 进度条、音量、时间格式三个联动细节

播放器界面上最容易被忽略的是进度条和播放状态的相互影响。QMediaPlayer 每播一小段时间就发射 positionChanged,你如果无条件用它刷新 QSlider,用户在拖动进度条手柄时会被信号不断拉回去。

4.1 positionChanged 推,sliderMoved 拉

用两个方向的信号控制进度条,代码模式如下:

connect(m_player, &QMediaPlayer::durationChanged, this, [this](qint64 duration) { m_progressSlider->setRange(0, static_cast<int>(duration)); }); connect(m_player, &QMediaPlayer::positionChanged, this, [this](qint64 position) { if (!m_progressSlider->isSliderDown()) m_progressSlider->setValue(static_cast<int>(position)); }); connect(m_progressSlider, &QSlider::sliderMoved, this, [this](int position) { m_player->setPosition(position); });

durationChanged 把进度条范围设置为媒体总时长,单位是毫秒,int 足够容纳常规音频。positionChanged 刷新滑块位置,但用isSliderDown()挡住拖动过程,防止用户正在拖时滑块被拉走。sliderMoved 是用户拖动期间持续触发的信号,这里调用 setPosition 做跳转,松手后 positionChanged 恢复同步。

qint64 转 int 在这里有条件限制。一首歌时长 10 分钟,也就是 60 万毫秒,int 完全没问题。但如果是几十小时的长音频,qint64 给进度条 setRange,int 可能不够,我一般在工程里按秒计算,duration / 1000传给进度条,误差一秒以内,拖动时再setPosition(value * 1000)

显示当前时间位置的 QLabel 同样可以用 positionChanged 更新,配合一个毫秒转字符串的函数:

QString PlayerWindow::formatTime(qint64 ms) { qint64 totalSeconds = ms / 1000; int minutes = static_cast<int>(totalSeconds / 60); int seconds = static_cast<int>(totalSeconds % 60); return QString("%1:%2") .arg(minutes, 2, 10, QLatin1Char('0')) .arg(seconds, 2, 10, QLatin1Char('0')); }

.arg(minutes, 2, 10, QLatin1Char('0'))表示最少占两位,不足补 0,所以 09:05 这种显示格式不需要手动补零。

4.2 自定义进度条外观,不改 QSlider 默认样式

QSlider 默认样式在深色界面上很突兀,比较快的方案是直接用样式表换槽和手柄:

m_progressSlider->setStyleSheet(R"( QSlider::groove:horizontal { height: 4px; background: #d8d8d8; border-radius: 2px; } QSlider::sub-page:horizontal { background: #3a7afe; border-radius: 2px; } QSlider::handle:horizontal { width: 14px; margin: -5px 0; border-radius: 7px; background: #1c1c1c; } )");

sub-page指滑块左侧已经播放过的部分,groove是整条轨道,handlemargin: -5px 0让竖直方向向外扩展,把手柄中心对到轨道上。槽高 4px、手柄宽 14px 时,margin 设置成-(14-4)/2即 -5px,正好居中对齐。如果手柄看起来偏上或偏下,调 margin 的负值。

4.3 音量滑块和静音还原

音量滑块取值范围设成 0~100,然后换成 QAudioOutput 的浮点音量:

m_volumeSlider->setRange(0, 100); m_volumeSlider->setValue(80); connect(m_volumeSlider, &QSlider::valueChanged, this, [this](int value) { m_audioOutput->setVolume(value / 100.0); });

value / 100.0里的 100.0 是浮点字面量,保证整数除法不会发生,滑到 50 时得到 0.5 而不是 0。这里的 Qt 6 语义容易和 Qt 5 弄混,Qt 5 的 setVolume 用 0~100 整数,Qt 6 的 QAudioOutput::volume 用 0.0~1.0 浮点。

静音按钮建议直接调用 setMuted,不要用 setVolume(0) 模拟:

connect(m_muteButton, &QPushButton::clicked, this, [this]() { m_audioOutput->setMuted(!m_audioOutput->isMuted()); });

如果程序里同时有音量滑块和静音按钮,滑块 valueChanged 会把音量值写回 QAudioOutput,setMuted 单独控制静音标志,两者不冲突。

4.4 单曲循环和列表循环的控制

前面的 playNext 实现了列表循环,但要支持单曲循环,就得在 onMediaStatusChanged 里判断:

// 构造函数里保存一个状态或可配置项 m_loopMode = QMediaPlayer::Infinite; // 单曲循环

用 QMediaPlayer 自带的 setLoops 更直接,但和播放列表切歌逻辑混在一起容易乱。我习惯在 onMediaStatusChanged 里手动控制:

if (status == QMediaPlayer::EndOfMedia) { if (m_loopCurrent) { m_player->setPosition(0); m_player->play(); } else { playNext(); } }

单曲循环和自动下一首只需要在切歌分支前加一个判断。这样不会影响到播放列表的索引位置。

5. 发布给没有 Qt 的机器,以及三个高风险崩溃点

播放器写完,在 Qt Creator 里按运行没问题,不等于你拷贝 exe 到别的电脑能跑。Qt 程序发布要处理插件目录、运行库和多媒体后端依赖。

5.1 windeployqt 生成发布目录

打开 Qt 命令行工具,进入编译出的 release 目录,执行:

cd /d D:\build\qt_player\release D:\Qt\6.5.3\mingw_64\bin\windeployqt.exe qt_player.exe --release

windeployqt 会扫描 exe 依赖的 Qt DLL,并复制 platforms、styles、multimedia 等插件目录到 exe 旁边。如果你是 MSVC 套件编译的,建议加--compiler-runtime参数,它会带上 Visual C++ 运行库,等价于给目标机器安装 vc_redist。给客户交付时如果不想带翻译文件,用--no-translations可以减小体积;但后续要做 qt 国际化,翻译文件路径就靠这一层目录,不能随便删。

5.2 开发机上常见的 qt.qpa.plugin 报错

开发环境直接运行出错的场景,报错形如:

qt.qpa.plugin: Could not find the Qt platform plugin "windows"

这是因为程序没找到 plugins 目录下的 qwindows.dll。可以临时告诉程序插件路径:

set QT_QPA_PLATFORM_PLUGIN_PATH=D:\Qt\6.5.3\mingw_64\plugins

这只是定位问题的手段。发布机器上不能依赖这个环境变量,正确产物是 exe 旁边带一个 plugins 目录。部署后如果双击 exe 没反应,先检查 exe 同级目录下有没有plugins\platforms\qwindows.dll

5.3 三个崩溃点:生命周期、后端 DLL、槽函数返回类型

看 Qt 播放器崩溃,多数集中在三个位置。

第一个是 QMediaPlayer 或 QAudioOutput 被提前销毁。比如在按钮的 lambda 里新建临时对象播放,第一次点击播放正常,第二次点击时临时对象析构,音频输出失效并触发段错误。解决办法是把这两个类设为成员变量,指针初始化后在整个窗口生命周期内不释放。

第二个是媒体后端加载失败。Qt 6 在 Windows 上默认使用 FFmpeg 解码,发布目录中 multimedia 插件和 FFmpeg 相关 DLL 缺一不可。程序运行后播放列表正常但点击播放没反应并报 InvalidMedia,多半是后端 DLL 缺失。在 main.cpp 开头加一行qputenv("QT_DEBUG_PLUGINS", "1"),控制台会打印后端加载明细,发布前记得删掉。

第三个是槽函数和 connect 的签名不匹配。Qt 6 新语法下 lambda 返回类型如果带值,比如[this](int v) { return m_player->setPosition(v); },在某些重载场景下会因为返回值不一致导致编译失败或运行期行为异常。槽函数默认返回值会被忽略,保持 void 是更稳妥的写法。

部署完成后,用一台没有安装 Qt 的干净虚拟机验证最小集合:双击 exe、打开目录、播放、切歌、拖动进度、静音、关闭窗口。全程打开任务管理器观察进程退出是否干净。这一套走完,再考虑换肤、歌词、音频可视化这些附加功能;核心播放链路保持住,后面的扩展就不会推倒重来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询