简介这是一份面向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 Applicationmain.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 c17 TARGET qt_player SOURCES main.cppCMake 版本对应这样配置适合用 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 multimediaCMake 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 为什么不直接用 QListWidgetQListWidget 是把数据存进视图内部适合“写十行代码就不改了”的场景。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_castint(duration)); }); connect(m_player, QMediaPlayer::positionChanged, this, [this](qint64 position) { if (!m_progressSlider-isSliderDown()) m_progressSlider-setValue(static_castint(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 给进度条 setRangeint 可能不够我一般在工程里按秒计算duration / 1000传给进度条误差一秒以内拖动时再setPosition(value * 1000)。显示当前时间位置的 QLabel 同样可以用 positionChanged 更新配合一个毫秒转字符串的函数QString PlayerWindow::formatTime(qint64 ms) { qint64 totalSeconds ms / 1000; int minutes static_castint(totalSeconds / 60); int seconds static_castint(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是整条轨道handle的margin: -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 会把音量值写回 QAudioOutputsetMuted 单独控制静音标志两者不冲突。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 --releasewindeployqt 会扫描 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_PATHD:\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、打开目录、播放、切歌、拖动进度、静音、关闭窗口。全程打开任务管理器观察进程退出是否干净。这一套走完再考虑换肤、歌词、音频可视化这些附加功能核心播放链路保持住后面的扩展就不会推倒重来。本文还有配套的精品资源点击获取