# LTSKFMusicPlayer **Repository Path**: LTSKF/ltskfmusic-player ## Basic Information - **Project Name**: LTSKFMusicPlayer - **Description**: No description available - **Primary Language**: C++ - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-03-28 - **Last Updated**: 2025-04-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: Qt, Cpp, Qml, ini, Sqlite ## README # LTSKFMusicPlayer #### 介绍 **LTSYF音乐播放器项目** 该项目是一款音乐播放器项目,主要功能是实现本地音乐的列表播放,软件具有用户登录、注册、音乐播放等页面,在登录后,用户可以选择音乐存放的文件夹路径将音乐加载到播放列表,播放模式可以列表播放、随机播放、单曲循环,同时支持记录用户播放的历史信息。 #### 软件架构 软件基于Qt-Desktop 5.14.2 MinGW - 32bit开发 使用C++编程语言 #### 安装教程 1. fork仓库 2. 准备Qt开发环境 3. 运行程序 #### 使用说明 1. 本项目是一个本地音乐播放器,需要手动选择具有mp3格式文件的文件夹进行读取 2. 软件音量调节bar是控制音乐的相对音量,不会直接修改设备的音量 #### 参与贡献 1. [灵荼山克府 (LTSKF) - Gitee.com](https://gitee.com/LTSKF) ltskf@qq.com #### 特技 1. 软件登录页面,背景具有一定的透明度,背景图片是随机播放的,用户可以在相应的目录下放入图片来加入随机队列 ![](./md/PixPin_2025-03-30_14-02-05.png) 2. 用户可以在登录页面进行账户登录或者注册账号,登录不存在的账号是无法进入播放器页面的。 3. 下图是播放器的播放页面 ![](./md/PixPin_2025-03-30_14-53-36.png "播放器页面") - 其中,播放器的背景也是随机的,和前面的登录页面公用一个随机队列。 - 左上角的MusucName能够实时的显示当前音乐的名称。 - 接着,下面的进度条可以跟随音乐的进度变动,拖动进度条也可以实现改变音乐的进度。 - 下面一行从左到右依次是上一首、播放/暂停、下一首、播放模式(列表播放、随机播放、单首循环)、音量(相对音量)、历史记录、选择文件夹。 ![随机](./icon/Random.png "随机")![单首循环](./icon/One.png "单首循环") - 音乐列表记录当前文件夹下的可用的音乐资源。列表的出现都需要点击相应的按钮才能实现,收回也是。 ![音乐列表](./md/PixPin_2025-03-30_15-06-35.png "音乐列表") - 列表的弹出具有动画属性。 md文件夹中可以查看 4. 清晰的目录结构,项目中每一个模块的功能都集成一个类,由需要该功能的地方实例化对象调用对象使用。(图片中的类并不是最终项目结构) ![目录结构](./md/PixPin_2025-03-30_15-14-00.png "目录结构") 5. 借助github desktop进行工程进度管理。 ![git](./md/PixPin_2025-03-30_15-17-16.png "git") #### 关键代码解析 1. 列表的淡入淡出动画关键代码 ```cpp // 渐入动画 void PlayerWindow::showAnimation(QWidget *window) { // 改变窗口位置 QPropertyAnimation animation(window, "pos"); // 动画持续时间 animation.setDuration(500); // 移动起始坐标 animation.setStartValue(QPoint(this->width(), 0)); // 移动终止坐标 animation.setEndValue(QPoint(this->width() - ui->listWidgetMusicList->width(), 0)); animation.start(); // 因为动画需要持续 // 所以需要加上事件循环 QEventLoop loop; // 等待动画结束 connect(&animation, &QPropertyAnimation::finished, &loop, &QEventLoop::quit); loop.exec(); } ``` 在点击列表图标时会根据bool选择项的调用动画。 函数参数时需要使用动画的窗口,项目中分别是音乐播放列表和历史列表。 根据窗口创建动画对象实现窗口的pos属性的改变,设置持续时间,开始坐标,结束坐标,中间的位移交给动画对象处理,调用start启动动画。 创建循环事件是因为动画start后是处于异步执行的状态,不会在start出阻塞,所以监听动画的结束信号并执行quit函数,最后启动循环事件。 2. 播放器背景设置 ```cpp // 设置窗口背景及透明度 void PlayerWindow::setBackGround(const QString &filename) { // 创建照片 QPixmap pixmap(filename); // 获取窗口大小 QSize windowSize = this->size(); // 将图片缩放 QPixmap scalePixmap = pixmap.scaled(windowSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); // 创建对象并设置背景 QPalette palette = this->palette(); // 传递缩放后的图片给 QBrush palette.setBrush(QPalette::Background, QBrush(scalePixmap)); // 将调色板应用到窗口 this->setPalette(palette); this->setAutoFillBackground(true); // 设置窗口透明度 this->setWindowOpacity(0.9); } ``` 主要是使用些内置的方法对背景进行调整,借助QPixmap对图片进行修改。 3. 读取目录中我音乐文件 ```cpp // 将目录中的文件添加到音乐 void PlayerWindow::setListWidgetMusicListItems(QString musicDir) { QDir dir(musicDir);// 根据目录构造对象 if(!dir.exists()){ // 不存在就退出 return ; } // 清楚history的内容 ui->listWidgetMusicHistoryList->clear(); // 提取目录下的文件并显示在list容器中 QStringList dirList = dir.entryList(); // 清空list内容后显示 ui->listWidgetMusicList->clear(); for (auto it = dirList.begin(); it != dirList.end(); ++it) { // 设置过滤 if((*it).endsWith(".mp3")){ ui->listWidgetMusicList->addItem(*it); } } // 检查 QListWidget 是否为空 if (ui->listWidgetMusicList->count() == 0) { // 没有找到 MP3 文件,不执行后续操作 qDebug() << "未找到 MP3 文件"; // 将 player 的媒体内容设为空 player->stop(); ui->pushButtonPlayer->setIcon(QIcon(":/icon/Player.png")); player->setMedia(QMediaContent()); return; } // 默认第一行 ui->listWidgetMusicList->setCurrentRow(0); // 尝试加载初始音乐文件 initMusicPlayer(); player->stop(); ui->pushButtonPlayer->setIcon(QIcon(":/icon/Player.png")); } ``` 函数会默认传递过来一个绝对路径的字符串,利用目录操作函数读取目录下的文件,先清楚播放列表原来的音乐目录,然后过滤保存以.mp3结尾的音乐文件将其添加到播放列表中,如果用户选择的目录没有音乐就需要特殊处理,最后将音乐选中状态改为第一个音乐文件,尝试加载音乐文件,设置音乐的状态。 4. 音乐文件的加载 ```cpp // 加载音乐文件 void PlayerWindow::initMusicPlayer() { QString musicName = ui->listWidgetMusicList->currentItem()->text(); QString musicAbsPath = musicDir + "/" + musicName; player->setMedia(QUrl::fromLocalFile(musicAbsPath)); qDebug() << musicAbsPath; // 检查是否有错误发生 if (player->error() != QMediaPlayer::NoError) { qDebug() << "音乐加载错误: " << player->errorString(); // 播放下一首音乐 handleNextSlot(); } } ``` 读取音乐列表音乐的名称,将其和音乐所在路径组合,通过这个路径配置播放媒体。 5. 双击播放历史来播放当前音乐 ```cpp // 处理点击历史音乐列表的事件 void PlayerWindow::on_listWidgetMusicHistoryList_itemDoubleClicked(QListWidgetItem *item) { // 获取当前点击的行 int index = ui->listWidgetMusicHistoryList->row(item); ui->listWidgetMusicHistoryList->setCurrentRow(index); // 找到音乐列表中对应位置 QString musicName = ui->listWidgetMusicHistoryList->currentItem()->text(); for (int i = 0; i < ui->listWidgetMusicList->count(); ++i) { QListWidgetItem *item = ui->listWidgetMusicList->item(i); if (item->text() == musicName) { ui->listWidgetMusicList->setCurrentRow(i); break; } } startPlayMusic(); } ``` 获取点击的项目的下表,通过下标获取音乐的名称,然后到播放列表中找到对应的名称,遍历播放列表,找到对应的歌曲下标设为选中状态,剩下的交给startPlayMusic函数处理. 6. Qt内置序列化和反序列化的再封装,借助其实现读取文件中用户注册信息。 ```cpp // 读取文件查找用户信息 bool MyDataStream::findUserInfo(const QString& userName, const QString& userPassWord) { QFile file(userFilePath); if (file.open(QIODevice::ReadOnly)) { QDataStream stream(&file); QString allData; // 读取信息 stream >> allData; // 分割数据 QStringList userInfos = allData.split("/", Qt::SkipEmptyParts); for (int i = 0; i < userInfos.size(); i += 2) { if (i + 1 < userInfos.size() && userInfos[i] == userName && userInfos[i + 1] == userPassWord) { file.close(); return true; } } file.close(); } else { qDebug() << "文件打开失败"; } return false; } ``` 7. 将读取的时间转成mm:ss格式。 ```cpp ```cpp // 把毫秒转换为 mm:ss 格式的字符串 QString PlayerWindow::millisecondsToMMSS(int milliseconds) { int totalSeconds = milliseconds / 1000; int minutes = totalSeconds / 60; int seconds = totalSeconds % 60; return QString("%1:%2").arg(minutes, 2, 10, QChar('0')).arg(seconds, 2, 10, QChar('0')); } ``` ``` 将时间/1000获得秒数,然后提取分和秒,后面字符串的拼接用的占位符,arg里面第一个是替换的信息,2是保留2位数组,10是10进制,后面是不足的补0。 ``` 8. QDataStream已被弃用,使用数据库记录用户信息。 将数据库操作函数进行封住进一个类中,通过创建类对象调用函数实现具体的功能。 这是查询用户信息是否在数据库中的封装函数。 ```cpp // 查找目标是否在数据库中,value的格式为"value1/value2" bool SQLOp::selectValue(QString value) { // 将传入的 value 按 / 分割成字符串列表 QStringList values = value.split("/"); if (values.size() != 2) { // 如果分割后的值数量不是 2,说明格式不正确 qDebug() << "Invalid value format. Expected 'username/password'."; return false; } QString username = values[0]; QString password = values[1]; // 构建 SQL 查询语句,假设表名为 users,字段名为 username 和 password QSqlQuery query; query.prepare("SELECT COUNT(*) FROM user WHERE username = :username AND password = :password"); query.bindValue(":username", username); query.bindValue(":password", password); // 执行 SQL 查询 if (query.exec()) { // 获取查询结果 if (query.next()) { // 获取查询结果中的计数 int count = query.value(0).toInt(); // 如果计数为 1,说明用户名和密码匹配 return count == 1; } } else { // 打印查询错误信息 qDebug() << "Query error:" << query.lastError().text(); } return false; } ``` 调用函数前将用户的信息进行封装,/是作为分割标识符存在的。调用sqlit函数将用户信息恢复。然后使用占位符和占位符替换组成合法的sql语句,执行sql语句,使用next将查询的数据游标移动到第一条数据(默认是最后一个,所以next是第一个),将相应的结果提取并转为int判断是否为1返回判断结果。(name是主键所以不会出现两条)。 ```cpp QSqlQuery query; query.prepare("SELECT COUNT(*) FROM user WHERE username = :username AND password = :password"); query.bindValue(":username", username); query.bindValue(":password", password); ``` 这样使用sql语句可以防止sql注入问题,有效的保证程序的安全。 9. SQL封装类使用单例化设计模式保证数据库对象只会初始化一次。 ```cpp class SQLOp { public: static SQLOp *getInstance(); ~SQLOp();// 记得在析构函数中写关闭数据库的语句 void createTable(); void insertValue(QString value); bool selectValue(QString value); private: SQLOp(); SQLOp(const SQLOp &other); QSqlDatabase database; static SQLOp *mySql; }; ``` 10. 使用ini文件记录软件的基础配置,在项目启动时能读取配置文件进行基础设置加载。 ```cpp // 游客信息不记录 if(name != ""){ // 将用户信息保存到ini配置文件中 setting = new QSettings("user.ini", QSettings::IniFormat); // 设置键值对 user是节(user节)后面的是键,最后面的是值 setting->setValue("user/userName", name); setting->setValue("user/passWord", pass); } ``` #### 使用到的信号 | 信号 | 功能 | |:------------------------------:|:----------- | | &QPushButton::clicked | 监听按钮的点击事件 | | &QMediaPlayer::durationChanged | 监听音乐媒体的变化 | | &QMediaPlayer::positionChanged | 监听音乐播放位置的变化 | | &QSlider::valueChanged | 监听进度条值的改变 | | &QSlider::sliderMoved | 监听进度条的移动 | #### QML模块 > 因为QML制作的画面更加精美,并且QML和C++有更好的兼容性,所以采用QML重构登录注册页面 > > 为什么不采用QML制作播放器页面呐,QML并不适合处理强逻辑,所以播放器依旧是C++加ui文件实现。 - 窗口拖动功能 ```qml property int dragx: 0 //属性命令要小写字母开头 property int dragy: 0 property bool isDrag: false //窗口拖动 MouseArea{ width: parent.width height: 50 //鼠标按下 onPressed: { //记录下鼠标按下的坐标,更改标志位 root.dragx = mouseX; root.dragy = mouseY; root.isDrag = true } //鼠标抬起 onReleased: root.isDrag = false;//鼠标释放 //位置改变时候 onPositionChanged: { if(root.isDrag) { //拖动的位置 = 鼠标移动后位置-鼠标按下的位置 root.x += mouseX - root.dragx root.y += mouseY - root.dragy } } } ``` 定义属性记录鼠标按下时的位置,并设置属性为拖动状态,在位置改变时计算鼠标移动的相对位置,然后将窗口的xy坐标相加得到绝对位置从而改变窗口的位置。 - 动画旋转 ```qml Image { id: icon x: 100 y: 100 source: "qrc:/icon/Music.png" states: [State{ name:"rotated" PropertyChanges { target: icon rotation:360 } } ] transitions: Transition { RotationAnimation{ duration: 1000 //RotationAnimation.Clockwise 顺时针旋转,默认为顺时针 //RotationAnimation.Counterclockwise:逆时针旋转 //direction: RotationAnimation.Counterclockwise //RotationAnimation.Shortest:沿最短路径的方向旋转 } } MouseArea{ anchors.fill: parent onClicked: { if(icon.state === "rotated") { icon.state ="" } else { icon.state = "rotated" } } } } ``` 定义了一个图片组件,定义一个数组states,数组里面定义rotated属性,配置rotated属性旋转360度持续1s,鼠标点击有效区域时更改状态以实现不同的旋转方向。 - 界面的定时消失 ```qml Timer { id: timer interval: 3000 // 3秒 running: true onTriggered: { fadeOutAnimation.start(); // 开始动画 } } SequentialAnimation { id: fadeOutAnimation NumberAnimation { target: failed; property: "opacity"; to: 0; duration: 1000 } // 渐变到透明需要1秒时间 PropertyAction { target: failed; property: "visible"; value: false } // 设置visible为false,彻底隐藏窗口 } ``` 设置定时器,3秒后触发动画时间的开始,动画时间记录了一个窗口从消失到消失1秒的动画效果。 - QML和C++的混合 ```qml //定义信号 signal myButtonClicked(string name, string pass, int flag) ``` ```qml onClicked: { console.log(userName.text + passWord.text) //发送信号(注册按钮:0 登录按钮:1) myButtonClicked(userName.text, passWord.text, 1)//触发自定义信号 } ``` ```cpp engine.load(QUrl(QStringLiteral("qrc:/Login.qml"))); rootObject = engine.rootObjects().first(); QObject::connect(rootObject, SIGNAL(myButtonClicked(QString, QString, int)), this, SLOT(recv_info(QString, QString, int))); ``` 在qml里面声明信号,在点击按钮后发送信号,在对应类文件中加载页面(只有load的qml才会显示),第二行可以找到qml的根组件,最后面则是老版的槽函数的配置,使用的是宏,因为缺少检查机制,所以一定要保证自己的函数都是正确的。 ![](./md/PixPin_2025-04-02_11-53-45.png) ![](./md/PixPin_2025-04-02_11-54-13.png)