本音乐播放器完整项目源码(包含各个按钮的图片文件):
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之后就可以运⾏。
其他的边角问题,读者可以自行进行解决,上面边角问题解决之后基本上已经没有大问题了(当然博主感觉应该是没有什么大问题了,你要说程序运行过程中你把歌曲文件删了碰到的问题,也是个问题,但是博主这里便不再介绍如何解决了,毕竟我们这是个练手项目,不是长时间运营的项目)
到此,我们的音乐播放器项目已经完成。