實(shí)戰(zhàn):從音頻引擎到發(fā)布部署)
簡介這是一份面向QT C初學(xué)者的音樂播放器實(shí)戰(zhàn)代碼包圍繞QMediaPlayer與QMediaPlaylist展開覆蓋播放、暫停、列表循環(huán)、音量控制等基本功能適合正在學(xué)習(xí)Qt多媒體模塊或需要課程設(shè)計(jì)的開發(fā)者直接參考。資源共8個(gè)文件壓縮包僅10KB包含C源文件、頭文件、UI界面文件、工程配置pro文件及說明文檔代碼結(jié)構(gòu)簡潔附有個(gè)人注解便于對(duì)照學(xué)習(xí)也適合作為二次開發(fā)的基礎(chǔ)。目前已有399人學(xué)習(xí)下載。項(xiàng)目采用標(biāo)準(zhǔn)Qt Widgets架構(gòu)mainwindow.h與mainwindow.cpp職責(zé)清晰mainwindow.ui可快速調(diào)整播放器界面借助該資源可以掌握在.pro中添加multimedia模塊、初始化QMediaPlayer與QMediaPlaylist、通過信號(hào)槽綁定播放/暫停按鈕、利用QListWidget展示歌曲并切換曲目、設(shè)置循環(huán)播放及音量調(diào)節(jié)等完整流程。附帶的文本說明還指出了不支持中文路徑等注意事項(xiàng)能幫助新手少走彎路是一份輕量但包含核心要點(diǎn)的入門示例對(duì)理解Qt多媒體編程很有幫助。1. 用 Qt/C 做音樂播放器先解決播放內(nèi)核問題用 Qt/C 寫一個(gè)音樂播放器初學(xué)者最容易照著舊教程把 QMediaPlayer 拖進(jìn)界面然后編譯時(shí)被 Qt 6 的接口改動(dòng)打懵。Qt 5 里一個(gè) QMediaPlayer 同時(shí)管理音量和播放列表Qt 6 里 QMediaPlayer 只管播放控制音頻輸出需要獨(dú)立的 QAudioOutput播放列表也不再默認(rèn)提供。下面這套實(shí)現(xiàn)以 Qt 6.5 為例先把本地文件播放跑通再補(bǔ)播放列表、進(jìn)度拖動(dòng)、音量調(diào)節(jié)和部署發(fā)布。你不用先學(xué)完 Qt Multimedia 的所有類只要理解這幾個(gè)核心類怎么組合起來就能自己擴(kuò)展出覆蓋常見需求的桌面播放器。這份路徑對(duì)已經(jīng)會(huì)寫 C 但沒接觸過 Qt 界面的開發(fā)者同樣可以作為第一個(gè) Qt 小工具來練手。2. 播放引擎的最小骨架QMediaPlayer 與 QAudioOutput 的分工在 Qt 6 里播放一首 mp3最少需要兩個(gè)對(duì)象QMediaPlayer 負(fù)責(zé)加載文件、播放、暫停和跳轉(zhuǎn)QAudioOutput 負(fù)責(zé)把聲音交給系統(tǒng)音頻設(shè)備并控制音量、靜音。很多“點(diǎn)了播放沒聲音”的問題不是文件壞了是這兩個(gè)對(duì)象沒綁定或者其中一個(gè)提前被銷毀。2.1 為什么 Qt 6 強(qiáng)制把音頻輸出拆出來Qt 5 的 QMediaPlayer 把播放控制和音頻輸出耦合在一起接口上簡單但想換一塊聲卡、想單獨(dú)調(diào)整輸出設(shè)備都要繞回播放器本身。Qt 6 把這個(gè)邊界拆開QMediaPlayer 只處理媒體狀態(tài)、播放位置和元數(shù)據(jù)QAudioOutput 負(fù)責(zé)輸出設(shè)備、音量比例和靜音標(biāo)志。好處是播放邏輯和渲染輸出解耦代價(jià)是剛上手的人容易漏掉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 改成整數(shù) 0~100。大量“同一段代碼為什么編譯不過”的問題根源都是版本差異。還有一個(gè)隱藏細(xì)節(jié)是生命周期。QAudioOutput 不能作為局部變量在 lambda 或構(gòu)造函數(shù)棧上創(chuàng)建后立刻銷毀否則 QMediaPlayer 內(nèi)部持有的音頻輸出引用失效程序不會(huì)立刻報(bào)錯(cuò)但聲音就斷了。這兩個(gè)對(duì)象建議都做成 PlayerWindow 的成員變量直到窗口關(guān)閉才釋放。2.2 最小可運(yùn)行代碼打開文件就能出聲新建 Qt Widgets Applicationmain.cpp 內(nèi)容如下#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(暫停/繼續(xù)); 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(); }這段代碼里四個(gè)關(guān)鍵點(diǎn)。setAudioOutput(audioOutput)必須執(zhí)行漏掉這行播放器用了默認(rèn)的空輸出整個(gè)程序沒有任何音頻設(shè)備播放狀態(tài)正常但聽不到聲音。audioOutput.setVolume(0.8)是浮點(diǎn)比例范圍 0.0~1.0不是 Qt 5 的整數(shù)邏輯。setSource(QUrl::fromLocalFile(path))要求傳 QUrl不能用普通 QString帶中文空格路徑也不會(huì)有問題。暫停按鈕通過playbackState()判斷當(dāng)前狀態(tài)再?zèng)Q定調(diào)用pause()還是play()比維護(hù)一個(gè)布爾標(biāo)志位可靠。2.3 編譯配置與兩個(gè)典型編譯錯(cuò)誤工程文件用 qmake 寫法最少新建 player.pro內(nèi)容如下QT multimedia widgets CONFIG c17 TARGET qt_player SOURCES main.cppCMake 版本對(duì)應(yīng)這樣配置適合用 CLion 或命令行構(gòu)建的工程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模塊少了這個(gè)組件QMediaPlayer頭文件都找不到。Qt Creator 里構(gòu)建套件要和安裝的編譯器一致MSVC 套件配 MinGW 的 Qt 或反過來都會(huì)在鏈接階段報(bào)一堆 undefined reference。我一般讓新手直接裝 Qt 6 的 mingw 版本配 Qt Creator少碰環(huán)境變量。| 報(bào)錯(cuò)信息 | 常見原因 | 處理方式 | |error: QAudioOutput file not found| 工程文件沒加 multimedia 模塊 | .pro 里補(bǔ)QT multimediaCMake COMPONENTS 里補(bǔ) Multimedia | |no member named setAudioOutput in QMediaPlayer| 用 Qt 5 的頭文件和庫編譯 Qt 6 代碼 | 確認(rèn) qmake/cmake 選擇的是 Qt 6或按 Qt 5 API 改寫 | |undefined reference to QMediaPlayer| 構(gòu)建套件與 Qt 庫架構(gòu)不一致 | 在 Qt Creator 里重新選擇匹配的構(gòu)建套件刪除 build 目錄重新編譯 |Qt 國內(nèi)鏡像下載安裝包時(shí)注意選對(duì)包名官方在線安裝器里 Qt 6.5 的Qt Multimedia大概在Qt Qt 6.5.3 Additional Libraries分組下只裝 Qt Base 是編譯不過的。3. 播放列表QListView QStringListModel 管理歌曲隊(duì)列很多播放器項(xiàng)目直接使用 QListWidget 存放歌曲名數(shù)據(jù)量小沒問題但你會(huì)遇到一個(gè)實(shí)際麻煩QListWidget 的每個(gè) item 只是字符串要綁定完整路徑只能往 Qt::UserRole 里塞數(shù)據(jù)代碼繞一圈。更直接的做法是讓數(shù)據(jù)模型和視圖分開。3.1 為什么不直接用 QListWidgetQListWidget 是把數(shù)據(jù)存進(jìn)視圖內(nèi)部適合“寫十行代碼就不改了”的場景。QListView QStringListModel 把歌曲列表和界面展示分開文件名列表只是字符串?dāng)?shù)組路徑列表單獨(dú)存一個(gè) QStringList兩個(gè)列表通過同樣的行號(hào)對(duì)應(yīng)。后面要加雙擊切換、自動(dòng)下一首、清空列表、刷新目錄都不用去碰視圖內(nèi)部數(shù)據(jù)。播放器窗口類的頭文件建議這樣組織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 的行號(hào)一一對(duì)應(yīng)QStringListModel 只負(fù)責(zé)喂給視圖顯示路徑是播放邏輯的原始數(shù)據(jù)。這樣構(gòu)造函數(shù)里初始化代碼的順序就不會(huì)錯(cuò)先 new QMediaPlayer 和 QAudioOutput再 new QListView 和 QStringListModel。3.2 遍歷目錄QDir 過濾與加載到列表在播放器界面里放一個(gè)“打開目錄”按鈕下面這段代碼負(fù)責(zé)掃描文件夾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; // 只掃描當(dāng)前目錄需要子目錄用 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(); } // 中文文件名按系統(tǒng)區(qū)域設(shè)置排序避免拼音亂序 dir.setSorting(QDir::Name | QDir::LocaleAware); m_playlistModel-setStringList(m_trackNames); if (!m_trackPaths.isEmpty()) playAt(0); }entryInfoList返回 QFileInfo 列表fileName()拿顯示名absoluteFilePath()拿絕對(duì)路徑播放和顯示各取所需。filters用的是 QDir::Files只留文件不包含子目錄。setSorting(QDir::Name | QDir::LocaleAware)對(duì)中文文件名有實(shí)際效果不加的話順序可能亂。如果你想支持子目錄遞歸把entryInfoList換成QDirIterator(folderPath, filters, QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot)但要注意重名文件和目錄層級(jí)導(dǎo)致的排序問題簡單播放器先用單目錄更穩(wěn)。3.3 雙擊播放與 EndOfMedia 自動(dòng)下一首雙擊列表某一行觸發(fā)播放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); }構(gòu)造函數(shù)里連接雙擊信號(hào)connect(m_playlistView, QListView::doubleClicked, this, PlayerWindow::onPlaylistDoubleClicked);onPlaylistDoubleClicked 槽里直接調(diào)用 playAt 即可void PlayerWindow::onPlaylistDoubleClicked(const QModelIndex index) { playAt(index.row()); }自動(dòng)切歌靠 QMediaPlayer::mediaStatusChanged 信號(hào)。播放到文件末尾媒體狀態(tài)變成 EndOfMedia這時(shí)觸發(fā)下一首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 枚舉里和播放器業(yè)務(wù)最相關(guān)的幾個(gè)狀態(tài)| 枚舉值 | 觸發(fā)時(shí)機(jī) | 需要處理的業(yè)務(wù) | | LoadedMedia | setSource 后媒體加載完成 | 此時(shí) durationChanged 才準(zhǔn)確可啟用進(jìn)度條 | | BufferingMedia | 網(wǎng)絡(luò)流或大文件緩沖 | 進(jìn)度條可以顯示緩沖但不需要彈錯(cuò)誤 | | StalledMedia | 數(shù)據(jù)讀取跟不上播放 | 不要把卡頓誤判成崩潰 | | EndOfMedia | 當(dāng)前文件播放完畢 | 自動(dòng)切歌或停止 | | InvalidMedia | 文件損壞或格式不支持 | 配合 errorOccurred 彈提示 |注意 EndOfMedia 對(duì)本地文件是穩(wěn)定觸發(fā)的但如果播放過程中手動(dòng)拖動(dòng)進(jìn)度條到末尾某些 Qt 版本下不會(huì)進(jìn)入 EndOfMedia這種邊界暫時(shí)不用管先保證歌曲自然放完能切歌。4. 進(jìn)度條、音量、時(shí)間格式三個(gè)聯(lián)動(dòng)細(xì)節(jié)播放器界面上最容易被忽略的是進(jìn)度條和播放狀態(tài)的相互影響。QMediaPlayer 每播一小段時(shí)間就發(fā)射 positionChanged你如果無條件用它刷新 QSlider用戶在拖動(dòng)進(jìn)度條手柄時(shí)會(huì)被信號(hào)不斷拉回去。4.1 positionChanged 推sliderMoved 拉用兩個(gè)方向的信號(hào)控制進(jìn)度條代碼模式如下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 把進(jìn)度條范圍設(shè)置為媒體總時(shí)長單位是毫秒int 足夠容納常規(guī)音頻。positionChanged 刷新滑塊位置但用isSliderDown()擋住拖動(dòng)過程防止用戶正在拖時(shí)滑塊被拉走。sliderMoved 是用戶拖動(dòng)期間持續(xù)觸發(fā)的信號(hào)這里調(diào)用 setPosition 做跳轉(zhuǎn)松手后 positionChanged 恢復(fù)同步。qint64 轉(zhuǎn) int 在這里有條件限制。一首歌時(shí)長 10 分鐘也就是 60 萬毫秒int 完全沒問題。但如果是幾十小時(shí)的長音頻qint64 給進(jìn)度條 setRangeint 可能不夠我一般在工程里按秒計(jì)算duration / 1000傳給進(jìn)度條誤差一秒以內(nèi)拖動(dòng)時(shí)再setPosition(value * 1000)。顯示當(dāng)前時(shí)間位置的 QLabel 同樣可以用 positionChanged 更新配合一個(gè)毫秒轉(zhuǎn)字符串的函數(shù)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))表示最少占兩位不足補(bǔ) 0所以 09:05 這種顯示格式不需要手動(dòng)補(bǔ)零。4.2 自定義進(jìn)度條外觀不改 QSlider 默認(rèn)樣式QSlider 默認(rèn)樣式在深色界面上很突兀比較快的方案是直接用樣式表換槽和手柄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指滑塊左側(cè)已經(jīng)播放過的部分groove是整條軌道handle的margin: -5px 0讓豎直方向向外擴(kuò)展把手柄中心對(duì)到軌道上。槽高 4px、手柄寬 14px 時(shí)margin 設(shè)置成-(14-4)/2即 -5px正好居中對(duì)齊。如果手柄看起來偏上或偏下調(diào) margin 的負(fù)值。4.3 音量滑塊和靜音還原音量滑塊取值范圍設(shè)成 0~100然后換成 QAudioOutput 的浮點(diǎn)音量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 是浮點(diǎn)字面量保證整數(shù)除法不會(huì)發(fā)生滑到 50 時(shí)得到 0.5 而不是 0。這里的 Qt 6 語義容易和 Qt 5 弄混Qt 5 的 setVolume 用 0~100 整數(shù)Qt 6 的 QAudioOutput::volume 用 0.0~1.0 浮點(diǎn)。靜音按鈕建議直接調(diào)用 setMuted不要用 setVolume(0) 模擬connect(m_muteButton, QPushButton::clicked, this, [this]() { m_audioOutput-setMuted(!m_audioOutput-isMuted()); });如果程序里同時(shí)有音量滑塊和靜音按鈕滑塊 valueChanged 會(huì)把音量值寫回 QAudioOutputsetMuted 單獨(dú)控制靜音標(biāo)志兩者不沖突。4.4 單曲循環(huán)和列表循環(huán)的控制前面的 playNext 實(shí)現(xiàn)了列表循環(huán)但要支持單曲循環(huán)就得在 onMediaStatusChanged 里判斷// 構(gòu)造函數(shù)里保存一個(gè)狀態(tài)或可配置項(xiàng) m_loopMode QMediaPlayer::Infinite; // 單曲循環(huán)用 QMediaPlayer 自帶的 setLoops 更直接但和播放列表切歌邏輯混在一起容易亂。我習(xí)慣在 onMediaStatusChanged 里手動(dòng)控制if (status QMediaPlayer::EndOfMedia) { if (m_loopCurrent) { m_player-setPosition(0); m_player-play(); } else { playNext(); } }單曲循環(huán)和自動(dòng)下一首只需要在切歌分支前加一個(gè)判斷。這樣不會(huì)影響到播放列表的索引位置。5. 發(fā)布給沒有 Qt 的機(jī)器以及三個(gè)高風(fēng)險(xiǎn)崩潰點(diǎn)播放器寫完在 Qt Creator 里按運(yùn)行沒問題不等于你拷貝 exe 到別的電腦能跑。Qt 程序發(fā)布要處理插件目錄、運(yùn)行庫和多媒體后端依賴。5.1 windeployqt 生成發(fā)布目錄打開 Qt 命令行工具進(jìn)入編譯出的 release 目錄執(zhí)行cd /d D:\build\qt_player\release D:\Qt\6.5.3\mingw_64\bin\windeployqt.exe qt_player.exe --releasewindeployqt 會(huì)掃描 exe 依賴的 Qt DLL并復(fù)制 platforms、styles、multimedia 等插件目錄到 exe 旁邊。如果你是 MSVC 套件編譯的建議加--compiler-runtime參數(shù)它會(huì)帶上 Visual C 運(yùn)行庫等價(jià)于給目標(biāo)機(jī)器安裝 vc_redist。給客戶交付時(shí)如果不想帶翻譯文件用--no-translations可以減小體積但后續(xù)要做 qt 國際化翻譯文件路徑就靠這一層目錄不能隨便刪。5.2 開發(fā)機(jī)上常見的 qt.qpa.plugin 報(bào)錯(cuò)開發(fā)環(huán)境直接運(yùn)行出錯(cuò)的場景報(bào)錯(cuò)形如qt.qpa.plugin: Could not find the Qt platform plugin windows這是因?yàn)槌绦驔]找到 plugins 目錄下的 qwindows.dll??梢耘R時(shí)告訴程序插件路徑set QT_QPA_PLATFORM_PLUGIN_PATHD:\Qt\6.5.3\mingw_64\plugins這只是定位問題的手段。發(fā)布機(jī)器上不能依賴這個(gè)環(huán)境變量正確產(chǎn)物是 exe 旁邊帶一個(gè) plugins 目錄。部署后如果雙擊 exe 沒反應(yīng)先檢查 exe 同級(jí)目錄下有沒有plugins\platforms\qwindows.dll。5.3 三個(gè)崩潰點(diǎn)生命周期、后端 DLL、槽函數(shù)返回類型看 Qt 播放器崩潰多數(shù)集中在三個(gè)位置。第一個(gè)是 QMediaPlayer 或 QAudioOutput 被提前銷毀。比如在按鈕的 lambda 里新建臨時(shí)對(duì)象播放第一次點(diǎn)擊播放正常第二次點(diǎn)擊時(shí)臨時(shí)對(duì)象析構(gòu)音頻輸出失效并觸發(fā)段錯(cuò)誤。解決辦法是把這兩個(gè)類設(shè)為成員變量指針初始化后在整個(gè)窗口生命周期內(nèi)不釋放。第二個(gè)是媒體后端加載失敗。Qt 6 在 Windows 上默認(rèn)使用 FFmpeg 解碼發(fā)布目錄中 multimedia 插件和 FFmpeg 相關(guān) DLL 缺一不可。程序運(yùn)行后播放列表正常但點(diǎn)擊播放沒反應(yīng)并報(bào) InvalidMedia多半是后端 DLL 缺失。在 main.cpp 開頭加一行qputenv(QT_DEBUG_PLUGINS, 1)控制臺(tái)會(huì)打印后端加載明細(xì)發(fā)布前記得刪掉。第三個(gè)是槽函數(shù)和 connect 的簽名不匹配。Qt 6 新語法下 lambda 返回類型如果帶值比如[this](int v) { return m_player-setPosition(v); }在某些重載場景下會(huì)因?yàn)榉祷刂挡灰恢聦?dǎo)致編譯失敗或運(yùn)行期行為異常。槽函數(shù)默認(rèn)返回值會(huì)被忽略保持 void 是更穩(wěn)妥的寫法。部署完成后用一臺(tái)沒有安裝 Qt 的干凈虛擬機(jī)驗(yàn)證最小集合雙擊 exe、打開目錄、播放、切歌、拖動(dòng)進(jìn)度、靜音、關(guān)閉窗口。全程打開任務(wù)管理器觀察進(jìn)程退出是否干凈。這一套走完再考慮換膚、歌詞、音頻可視化這些附加功能核心播放鏈路保持住后面的擴(kuò)展就不會(huì)推倒重來。本文還有配套的精品資源點(diǎn)擊獲取