本音樂播放器完整項目源碼(包含各個按鈕的圖片文件):
ly/Project-Code - Gitee.com
一.本地持久化
請注意,學習此部分之前需要讀者具有一定的Mysql基礎。如果讀者能夠接受無法本地持久化,那么可以跳過這部分內容,直接去看邊角問題處理。我們這里使用SQLite數據庫進行本地持久化保存,因為它在使用時不需要配置任何環境。而且我們也只會用一些簡單的增刪改查,下面是SQLite的教程:
SQLite 教程 | 菜鳥教程
Qt中已經內置了SQLite,在安裝qt開發環境時,SQLite環境已經配置好了,??在.pro?件中導?數據庫模塊就可以使?。
// *.pro文件中添加模塊
QT += sql
1.1QSqlDatabases類的介紹
QSqlDatabase類主要處理與數據庫的連接,它提供了創建、配置、打開和關閉數據庫連接的?法。
數據庫的連接和關閉:
// 功能:根據type來添加數據庫驅動
// type:數據庫類型 [QDB2:IBM DB2, QMYSQL:MySQL, QOCI:Oracle, QODBC:ODBC,
QSQLITE:SQLite...]
// connectionName: 數據庫連接的名稱[可選]。如果提供,可以為數據庫連接指定?個唯?的名稱
// 返回值:表?新創建的數據庫連接
static QSqlDatabase addDatabase(const QString &type,const QString &connectionName =QLatin1String(defaultConnection))// 添加SQLite數據庫驅動,返回?個連接
QSqlDatabase QQMusicDB = QSqlDatabase::addDatabase("QSQLITE");// 功能:設置數據庫?件的名稱
// name: 要連接的數據庫的名稱。對于SQLite,通常是數據庫?件的路徑;對于其他數據庫系統,?
如MySQL通常是數據庫管理系統中數據庫的名稱
void setDatabaseName(const QString &name);// 功能:打開數據庫連接,即和數據庫真正建?連接
// 返回值:連接成功連接返回true,否則返回false,注意:可以使?isopen()?法檢測是否打開
// user:數據庫??名
// password: 數據庫密碼
bool open();
bool open(const QString &user, const QString &password);// 功能:關閉數據庫連接,釋放所有資源,并使與數據庫?起使?的所有QSqlQuery對象?效
void close();
1.2本文會用到的SqLite的數據類型
數據類型 | 描述 |
NULL | 值是?個NULL值 |
INTEGER | 值是?個帶符號的整數,根據值的??存儲在1 2 3 4 6 或 8 字節中 |
REAL | 值是?個浮點數,存儲為8字節的IEEE浮點數 |
TEXT | 值是?個?本字符串,使?數據庫編碼(UTF-8、UTF-16BE 或 UTF-6LE)存儲 |
1.3QSqlQuery類的介紹
// 功能:準備SQL語句,該語句中包含?個或者多個參數占位符。這些參數占位符在SQL中默認為?表
?
// 也可以?定義占位符。其允許提前先設置好SQL語句結構,但是不執?
bool prepare(const QString &query);// 功能:使?參數的名稱(即通過prepare構造SQL語句時設置的占位符)來綁定值
// placeholder: 參數占位符的名稱
// val:要綁定的值
void bindValue(const QString &placeholder,const QVariant &val,QSql::ParamType paramType = QSql::In);// 功能:通過位置來幫實際值
// pos: 是參數的位置,從0開始計數
// val: 要綁定的值
void QSqlQuery::bindValue(int pos,const QVariant &val,QSql::ParamType paramType = QSql::In);// 功能:按照建表時成員的順序綁定
void addBindValue(const QVariant &val,
QSql::ParamType paramType = QSql::In);
基本的用法是:通過prepare先將SQL語句準備好,在準備時實際值可以先?其他符號占?,然后通過bindValue來綁定實際值,通過命名綁定和位置綁定都可以,綁定好之后,調?exec執?。
而構造好SQL語句,使?QSQLQuery的對象query執?SQL語句,查詢結果可以通過query獲取:
// 功能:將查詢結果的當前?指針向后移動??,如果移動后有記錄則返回true,否則返回false
// 利?該?法,搭配while循環,可獲取到所有查詢記錄
bool next();// 功能:獲取查詢記錄中索引為index的域的值
// 查詢結果按照select語句后所查詢字段順序,從左往右基于0開始編號,依次遞增
// select name, age, gpa from student;
// 每條查詢結果中,name的索引為0 age的索引為1, gpa的索引為2
QVariant value(int index) const;// 功能:根據查詢記錄中,name字段對應的值,如果名字不匹配,將返回?個?法的QVariant
QVariant value(const QString &name) const;
1.4數據庫創建思路概述
? ? ? ? 我們這里是對導入的音樂以及是否喜歡和最近播放進行本地持久化。那么還記得我們之前有一張musicList表存儲在主界面類中嗎?無論歌曲的我喜歡狀態還是最近播放狀態被改變,都會反映到這張表中。所以我們只需要在程序結束時,將該musicList中的所有歌曲的各項屬性加載到數據庫中。然后程序再次啟動時讀取數據庫,填充musicList列表然后刷新三張CommonPage頁面即可。
為了避免歌曲重復加載,我們這里給MusicList加一個set表維護歌曲的所有路徑:
//MusicList::addMusicsByUrls新增
if(fileType == "audio/mpeg" || fileType == "audio/flac" || fileType == "audio/wav")
{if(!filePaths.contains(url.toLocalFile()))//新增部分{musicList.push_back(Music(url));//添加到哈希集合中filePaths.insert(url.toLocalFile());}
}//添加成員變量
QSet<QString> filePaths;
1.5本地持久化實現
首先我們給主界面函數新增一個initDb的方法用來初始化數據庫以及程序與數據庫的連接:
void SekaiMusic::initDb()
{//設置我喜歡,本地下載,最近播放的文本和圖片ui->likePage->setCommonPageImage(":/images/ilikebg.png","我喜歡");ui->localPage->setCommonPageImage(":/images/localbg.png","本地音樂");ui->recentPage->setCommonPageImage(":/images/recentbg.png","最近播放");//設置頁面類型ui->likePage->setPageType(PageType::LIKE_PAGE);ui->localPage->setPageType(PageType::LOCAL_PAGE);ui->recentPage->setPageType(PageType::RECENT_PAGE);//連接數據庫sekaiMusicDb = QSqlDatabase::addDatabase("QSQLITE");//添加數據庫驅動sekaiMusicDb.setDatabaseName("SekaiMusic.db");if(!sekaiMusicDb.open()){qDebug() << "數據庫打開出錯:" << sekaiMusicDb.lastError().text();return;}QSqlQuery query;query.prepare("create table if not exists MusicInfo( \id integer primary key autoincrement,\musicId varchar(50) unique,\musicName varchar(50),\singerName varchar(50),\albumName varchar(50),\duration bigint,\isLike integer,\isHistory integer,\musicPath varchar(256));");if(!query.exec()){qDebug() << "數據庫初始化錯誤" << query.lastError().text();return;}qDebug() << "數據庫表創建/連接成功!!";
}
順帶把三個CommonPage的初始化工作也放到這個函數中。接下來我們添加initMusicList函數,讓musicList通過讀取數據庫來初始化播放列表:
void SekaiMusic::initMusicList()
{musicList.loadMusicOfDb();ui->likePage->reFresh(musicList);ui->localPage->reFresh(musicList);ui->recentPage->reFresh(musicList);
}void MusicList::loadMusicOfDb()
{QSqlQuery query;query.prepare("select musicId,musicName,singerName,albumName,duration,isLike,isHistory,musicPath from MusicInfo");if(!query.exec()){qDebug() << "數據庫表查詢失敗" << query.lastError().text();return;}qDebug() << "表查詢成功";while(query.next()){//刪除失效數據if(!QFileInfo::exists(query.value("musicPath").toString())){QSqlQuery query_delete;query_delete.prepare("delete from MusicInfo where musicId = ?");query_delete.addBindValue(query.value("musicId").toString());if(!query_delete.exec()){qDebug() << "失效數據刪除失敗" << query_delete.lastError().text();}elseqDebug() << "失效數據刪除成功";continue;}//說明數據存在Music music;music.setMusicId(query.value(0).toString());music.setMusicName(query.value(1).toString());music.setSingerName(query.value(2).toString());music.setAlbumName(query.value(3).toString());music.setDuration(query.value(4).toLongLong());music.setIsLike(query.value(5).toInt() == 1);music.setIsHistory(query.value(6).toInt() == 1);music.setMusicUrl(QUrl::fromLocalFile(query.value(7).toString()));musicList.push_back(music);filePaths.insert(music.getMusicUrl().toLocalFile());//插入到哈希集合中}
}
接下來當程序關閉時我們讓musicList自己把所有的music數據寫入/更新到數據庫中,當然這個函數放到關閉窗口按鈕的槽函數中執行:
//關閉按鈕的槽函數中//更新數據庫musicList.updateMusicOfDb();//關閉數據庫sekaiMusicDb.close();this->close();void MusicList::updateMusicOfDb()
{for(auto& music : musicList){music.insertSelfOfDb();}
}void Music::insertSelfOfDb()
{QSqlQuery query;query.prepare("SELECT EXISTS (SELECT 1 FROM MusicInfo WHERE musicId = ?)");query.addBindValue(musicId);if(!query.exec()){qDebug()<<"查詢失敗: "<<query.lastError().text();return;}if(query.next()){bool isExists = query.value(0).toBool();if(isExists){//說明數據之前已經插入到數據庫中了//不需要再插入music對象,此時只需要將isLike和isHistory屬性進行更新query.prepare("UPDATE MusicInfo SET isLike = ?, isHistory = ? WHERE musicId = ?");query.addBindValue(isLike? 1 : 0);query.addBindValue(isHistory? 1 : 0);query.addBindValue(musicId);if(!query.exec()){qDebug()<<"更新失敗: "<<query.lastError().text();}qDebug()<<"更新music信息: "<<musicName<<" "<<musicId;}else{//說明該歌曲之前沒有被插入到數據庫中query.prepare("insert into MusicInfo (musicId,musicName,singerName,albumName,duration,isLike,isHistory,musicPath) values(?,?,?,?,?,?,?,?);");query.addBindValue(musicId);query.addBindValue(musicName);query.addBindValue(singerName);query.addBindValue(albumName);query.addBindValue(duration);query.addBindValue(isLike ? 1 : 0);query.addBindValue(isHistory? 1 : 0);query.addBindValue(musicUrl.toLocalFile());if(!query.exec()){qDebug()<<"插入失敗: "<<query.lastError().text();return;}qDebug()<<"插入music信息: "<<musicName<<" "<<musicId;}}
}
這樣當我們第一次把歌曲信息加載到程序中,第二次再打開程序時就不需要再去重復導入了,同時如果第二次打開程序時,本地文件被刪除了,那么數據庫會自動把失效數據刪除,不再讓其導入到播放列表中。
二.邊角問題處理
2.1最大化,最小化和換膚問題處理
最大化因為我們之前再設計Ui界面時有些空間的尺寸是寫死的,比如按鈕圖標30*30,所以如果要最大化,需要我們自己去做適配。這里不再介紹。當然換膚問題也需要自己再去做適配。所以我們只處理最小化的情況,只需要調用一個函數即可:
//邊角問題處理
void SekaiMusic::on_min_clicked()
{showMinimized();
}void SekaiMusic::on_max_clicked()
{QMessageBox::information(this,"溫馨提示","「最大化」功能加載中... ███████? 90%,\n抱歉,不是卡了,是我們的CPU正在為您的體驗全力燃燒。");
}void SekaiMusic::on_skin_clicked()
{QMessageBox::information(this,"溫馨提示","皮膚功能正在騎馬趕來的路上~");
}
2.2添加系統托盤
我們一般見到的音樂軟件,都是點擊關閉按鈕后不會立即關閉窗口而是縮小到系統托盤中,所以我們這里也為我們的程序添加一個這樣的效果:
//SekaiMusic中新增成員變量:QSystemTrayIcon* trayIcon;//系統托盤//initUi中新增//初始化系統托盤trayIcon = new QSystemTrayIcon(this);trayIcon->setIcon(QIcon(":/images/tubiao.png"));trayIcon->setToolTip("SekaiMusic");//創建托盤菜單QMenu* trayMenu = new QMenu(this);trayMenu->addAction("還原窗口",this,&SekaiMusic::showWindows);trayMenu->addSeparator();trayMenu->addAction("關閉窗口",this,&SekaiMusic::closeWindows);trayIcon->setContextMenu(trayMenu);//在關閉窗口時顯示系統托盤,點擊還原窗口時隱藏
void SekaiMusic::on_quit_clicked()
{hide();//隱藏主窗口trayIcon->show();//最小化到系統托盤
}void SekaiMusic::showWindows()
{show();//同時隱藏系統托盤trayIcon->hide();
}void SekaiMusic::closeWindows()
{//更新數據庫musicList.updateMusicOfDb();//關閉數據庫sekaiMusicDb.close();this->close();
}
2.3保證程序運行時只有一個實例
我們這里禁止程序啟動多次,一般也不需要,多個實例同時運?有以下缺陷:
? 多個實例同時運?可能會導致資源浪費,如內存、CPU效率等
? 如果應?程序涉及對共享數據的修改,多個程序同時運?可能會導致數據不?致問題
? 若多個實例嘗試訪問同?資源時,如?件、數據庫等,可能會導致沖突或錯誤
? 另外,??體驗不是很好,多個實例操作時容易混淆
因此有時會禁?程序多開,即?個應?程序只能運??個實例,也稱為單實例應?程序或單例應?程序。在Qt中,禁?程序多開的?式有好?種,此處采?共享內存實現。
共享內存是操作系統中的概念,是進程間通信的?種機制。由于相同key值的共享內存只能存在?份,因此在程序啟動時可以檢測共享內存是否已經被創建,如果已經創建則說明程序已經在運?,否則程序還沒有運?。
//修改main.cpp為如下內容
#include "sekaimusic.h"#include <QApplication>
#include <QSharedMemory>int main(int argc, char *argv[])
{QApplication a(argc, argv);// 創建共享內存-確保程序只有一個實例運行QSharedMemory sharedMem("SekaiMusic");// 如果共享內存已經被占?,說明已經有實例在運?if (sharedMem.attach()) {QMessageBox::information(nullptr, "SekaiMusic", "SekaiMusic已經在運?...");sharedMem.detach();return 0;}sharedMem.create(1);//當然這1字節的內存空間也需要我們手動去釋放,否則除非電腦重啟,這個共享內存會一直存在//連接 aboutToQuit 信號確保資源釋放QObject::connect(qApp, &QCoreApplication::aboutToQuit, [&sharedMem]() {if (sharedMem.isAttached()) {sharedMem.detach();qDebug() << "共享內存已正確釋放";}});SekaiMusic w;w.show();return a.exec();
}
2.4解決界面偶爾亂移動的問題
我們之前解決窗口無法拖動的問題是這樣子去解決的:
void SekaiMusic::mouseMoveEvent(QMouseEvent *event)
{if(event->buttons() == Qt::LeftButton){//注button無法處理移動事件,buttons更為合適,可以參考官方文檔this->move(event->globalPos() - dragPosition);return;}//其他事件默認處理QWidget::mouseMoveEvent(event);
}void SekaiMusic::mousePressEvent(QMouseEvent *event)
{//判斷左鍵同時判斷鼠標是否在窗口內if(event->button() == Qt::LeftButton){//記錄相對位置dragPosition = event->globalPos() - frameGeometry().topLeft();return;}//其他事件默認處理QWidget::mousePressEvent(event);
}
這樣會有一個問題,如果我們鼠標按下卻沒有拖動怎么辦,那么下一次我們不小心拖一下他就會亂移動。因為我們拖動時會有三個動作:按下-拖動-釋放,所以我們可以添加一個標記isDragging,然后將原來的代碼改為如下代碼即可解決問題:
void SekaiMusic::mouseMoveEvent(QMouseEvent *event)
{if(event->buttons() == Qt::LeftButton){// 如果是第一次移動(還未記錄初始相對位置),則記錄初始相對位置if (!isDragging) {dragPosition = event->globalPos() - frameGeometry().topLeft();isDragging = true;}// 移動窗口this->move(event->globalPos() - dragPosition);return;}// 其他事件默認處理QWidget::mouseMoveEvent(event);
}void SekaiMusic::mousePressEvent(QMouseEvent *event)
{// 判斷左鍵if(event->button() == Qt::LeftButton){// 僅標記左鍵按下,不記錄位置isDragging = false;return;}// 其他事件默認處理QWidget::mousePressEvent(event);
}void SekaiMusic::mouseReleaseEvent(QMouseEvent *event)
{// 鼠標釋放時重置標志位if (event->button() == Qt::LeftButton) {isDragging = false;}QWidget::mouseReleaseEvent(event);
}
2.5禁止qDebug()輸出
要逐個刪除程序中qDebug的打印太?煩,可以在配置?件中通過添加以下語句,禁?qDebug輸出:
# ban qDebug output
DEFINES += QT_NO_DEBUG_OUTPUT
2.6對程序進行打包
Qt可執?程序在運?的時候,需要依賴Qt框架中的?些庫?件,如果對?及其上之前未安裝Qt環境,點擊可執?程序運?時,會提?缺少xxx.dll動態庫信息等。為了讓開發好的Qt可執?程序在未安裝Qt環境的機器上也可以運?,就需要對項?進?打包,打包的過程會將exe可執?程序運?時所需的依賴?件全部整合到?起,將打包好的包?起發給對端,雙擊exe可執?程序時就可以執?。
注意:打包時exe需要?release版本,debug是調試版本,release版本編譯器會去除調試信息,并會對?程進?優化等操作,使程序體積更?,運?效率更?。
2.6.1windeployqt打包?具
windeployqt 是 Qt 提供的?個?具,?于?動收集并復制運? Qt 應?程序所需的動態鏈接庫(.dll ?件)及其他資源(如插件、QML 模塊等)到可執??件所在的?錄。這樣你就可以將應?程序和這些依賴項?起打包,確保在沒有 Qt 環境的其他機器上也能運?。
【主要功能】
- ?動收集依賴項: windeployqt 會分析你的 Qt 應?程序,確定它所依賴的 Qt 庫?件(如Qt6Core.dll, Qt6Widgets.dll),并將這些?件復制到應?程序的?錄。
- 處理插件和QML模塊: 如果你的應?程序使?了 Qt 的插件(如平臺插件 qwindows.dll 或圖形驅動插件等),windeployqt 也會將這些插件?并打包。對于使? QML 的應?程序,它也會?動收集必要的 QML 模塊。
- 處理資源?件: 如果你的應?程序包含了 Qt 的資源?件(如圖標、翻譯?件等),它也會確保這些資源正確包含在最終的應?程序中。
2.6.2打包流程
- 配置好Qt環境變量
- 選擇以release?式編譯程序。編譯好之后,在?程?錄上?層會?成包含release字段的?件夾,?件夾內部就有release模式的可執?程序。
- 將新建?個?件夾,命名為SekaiMusic,將release模式可執?程序拷?到SekaiMusic。
- 進?SekaiMusic,在該?件夾內部,按shift,然后?標右鍵單擊,彈出菜單中選擇"在此處打開Powershell 窗?(S)",在彈出窗?中輸? windeployqt .\SekaiMusic.exe,windeployqt?具就會?動完成打包。
如果不想要僅僅只是壓縮包的形式,可以參考這位博主的文章將我們自己寫的程序打包為安裝包:
Qt入門(三):項目打包_qt打包-CSDN博客
這里我們不再介紹,上面的四個步驟結束時將該?錄壓縮之后,發給對?,對?收到之后直接解壓,點擊exe之后就可以運?。
其他的邊角問題,讀者可以自行進行解決,上面邊角問題解決之后基本上已經沒有大問題了(當然博主感覺應該是沒有什么大問題了,你要說程序運行過程中你把歌曲文件刪了碰到的問題,也是個問題,但是博主這里便不再介紹如何解決了,畢竟我們這是個練手項目,不是長時間運營的項目)
到此,我們的音樂播放器項目已經完成。