
簡介這是一份面向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、打開目錄、播放、切歌、拖動進度、靜音、關閉窗口。全程打開任務管理器觀察進程退出是否干凈。這一套走完再考慮換膚、歌詞、音頻可視化這些附加功能核心播放鏈路保持住后面的擴展就不會推倒重來。本文還有配套的精品資源點擊獲取