Qt 实操记录:打造自己的“ QQ 音乐播放器”

在这里插入图片描述

目录

  • 一.界面设计
    • 1.成品界面分析
    • 2.head界面实现
    • 3.body界面实现
    • 4.主界面设置
      • (1).设置无标题栏与阴影效果
      • (2).重写鼠标事件实现拖拽
  • 二.自定义控件
    • 1.BtFrom界面设计
    • 2.推荐页面设计
    • 3.recBox页面设计
    • 4.recBoxItem页面设计
      • (1).eventFilter介绍和使用
      • (2).QJsonObject介绍和使用
      • (3).向recBox中添加元素
      • (4).处理recBox的轮番按钮
    • 5.commonPage页面设计
    • 6.ListItemBox页面设计
    • 7.MusicSlider设计
    • 8.VolumeTool设计
  • 三.媒体类进行歌曲管理
    • 1.音乐的加载和分析
      • (1).MIME过滤器
      • (2).MusicList类
      • (3).Music类
      • (4).QMediaPlayer类解析元数据
    • 2.音乐分类
      • (1).更新music到CommonPage页面上
      • (2).收藏音乐
  • 四.播放控制区域实现
    • 1.播放控制类介绍
      • (1).QMediaPlayer类
      • (2).QMediaPlayList类
    • 2.歌曲播放
    • 3.播放与暂停
    • 4.上一曲和下一曲
    • 5.播放模式切换
    • 6.播放全部
    • 7.双击ListitemBox播放
    • 8.最近播放同步
    • 9.音量功能
    • 10.歌曲播放时间处理
    • 11.进度条设置
      • (1).进度条界面显示
      • (2).进度条同步播放时间
    • 12.修改歌曲信息
    • 13.lrc歌词设计
      • (1).lrc歌词页面动画设计
      • (2).歌词解析
  • 五.数据库实现持久化
    • 1.SQLite介绍
    • 2.QSqlDatabase类介绍
    • 3.数据库初始化
      • (1).initSQLite
      • (2).歌曲信息写入数据库
      • (3).程序启动读取数据库歌曲信息
  • 六.优化与debug
    • 1.添加系统托盘
    • 2.换肤最大最小化处理
    • 3.重复从本地添加音乐bug
    • 4.保证程序只运行⼀次
  • 七.总结

一.界面设计

1.成品界面分析

当我们将完成的音乐播放器程序运行起来后:
在这里插入图片描述

观察分析可得,主界面大致可分为几个部分:head(headleft和headright)和body(bodyleft和bodyright(层叠窗口和播放控制区域))。
在这里插入图片描述

2.head界面实现

据以上可得head从左至右分别为一个logo、搜索框、换肤按钮、最小化、最大化、关闭按钮。可以用QLabel QLineEdit 和四个QPushButton实现:
在这里插入图片描述
在这里插入图片描述

在headright中有一根垂直弹簧用来保证head在整个widget上保证一定的高度 不会被body过度挤压。
logo的QSS优化如下:

#logo{background-image:url(":/images/Logo.png");background-repeat:no-repeat;//不重复显示background-position:center center;//设置水平垂直居中border:none;//无边框
}

换肤最小最大化和关闭设置一个背景图片并且添加hover效果即可:

QPushButton:hover{background-color:rgba(225,0,0,0.5);
}

3.body界面实现

首先来实现bodyleft,观察成品可知需要两个Widget分别实现在线音乐我的音乐两个块功能,和headright一样这里也需要一条垂直弹簧将两块功能区顶起来:
在这里插入图片描述
bodyright则稍复杂些包括了三个部分:层叠窗口用来对应在线音乐和我的音乐的六个页面,进度条用来标识歌曲播放进度,播放控制区域用来进行对播放控制,以及歌曲封面和信息的显示。
在这里插入图片描述

4.主界面设置

(1).设置无标题栏与阴影效果

当我们把主界面的ui大致设置好以后,我们将程序跑起来发现widget还有标题栏,这个是多余的需要去掉。
在这里插入图片描述

setWindowFlag(Qt::FramelessWindowHint);//设置为无标题栏

但是当我们再次运行发现 在界面的四周应该添加上一定的阴影效果 看起来更好些。
这时候就需要用到QGraphicsDropShadowEffect 类,这是qt中专门用来添加阴影效果的:

QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(this);shadow->setColor("#000000");shadow->setBlurRadius(5);shadow->setOffset(0,0);this->setGraphicsEffect(shadow);

setColor用来设置阴影的颜色,setBlurRadius用来设置阴影的模糊半径,setOffset用来设置阴影的偏移,设置好shadow后将其设置到ui界面上即可。

(2).重写鼠标事件实现拖拽

为了实现对窗口的拖拽,要对鼠标按下和移动事件进行重写:

QPoint Relativedistance;//相对距离
void Widget::mouseMoveEvent(QMouseEvent *event)
{if(event->buttons() == Qt::LeftButton){move(event->globalPos()-Relativedistance);return ;}QWidget::mouseMoveEvent(event);
}//鼠标按下事件
void Widget::mousePressEvent(QMouseEvent *event)
{if(event->button() == Qt::LeftButton){//鼠标相对于窗口左上角的距离Relativedistance = event->globalPos() - geometry().topLeft();return ;}QWidget::mousePressEvent(event);
}

如此以后即可对窗口进行拖拽了。

二.自定义控件

1.BtFrom界面设计

BtFrom是用来填充bodyLeft内部onlineMusic和MyMusic中的QWidget全部提升为BtForm的。
设计如下:
在这里插入图片描述
从左到右分别用来放置图标,文字描述,播放的动画效果
在btfrom类中添加方法setIconAndText:用来设置图标和文字以及标识当前页面的索引。

int pageid;
void BtFrom::setIconAndText(const QString &icon, const QString &text,int id)
{ui->bticon->setPixmap(QPixmap(icon));ui->btText->setText(text);pageid = id;
}

以及我们点击对应的btfrom时应该添加一个绿色的hover效果
如图所示:
在这里插入图片描述

void BtFrom::mousePressEvent(QMouseEvent *event)
{(void)event;//防止未使用警告ui->btStyle->setStyleSheet("background-color:rgb(30,206,154)");emit btclick(this->pageid);
}

向Widget发送按钮点击信号,并且传递是哪个页面,Widget中接受并绑定槽函数处理此信号:

connect(ui->Rec,&BtFrom::btclick,this,&Widget::on_btFrom);connect(ui->radio,&BtFrom::btclick,this,&Widget::on_btFrom);connect(ui->music,&BtFrom::btclick,this,&Widget::on_btFrom);connect(ui->like,&BtFrom::btclick,this,&Widget::on_btFrom);connect(ui->local,&BtFrom::btclick,this,&Widget::on_btFrom);connect(ui->recent,&BtFrom::btclick,this,&Widget::on_btFrom);
void Widget::on_btFrom(int id)
{QList<BtFrom*> btfromlist = this->findChildren<BtFrom*>();for(auto btfrom : btfromlist){if(btfrom->getPageid() != id){btfrom->clearBground();}}ui->stackedWidget->setCurrentIndex(id);}

点击了哪个按钮,就将之前按钮的绿色背景即hover效果去掉:

void BtFrom::clearBground()
{ui->btStyle->setStyleSheet("#btStyle:hover{background-color:#D8D8D8;}");
}

接下来实现btfrom上的动画效果,这里会使用到QPropertyAnimation 类通过在一定时间内逐渐改变对象的属性值来创建动画效果。可以指定动画的起始值、结束值、持续时间和缓动曲线等参数,从而实现各种动画效果。
对象的构造函数:

QPropertyAnimation(QObject *target, const QByteArray &propertyName, QObject *parent = nullptr);

target:要进行动画处理的对象。
propertyName:要进行动画处理的属性名称,以 QByteArray 类型表示。
parent:动画对象的父对象,默认为 nullptr。

	QPropertyAnimation* line1Animation;QPropertyAnimation* line2Animation;QPropertyAnimation* line3Animation;QPropertyAnimation* line4Animation;line1Animation = new QPropertyAnimation(ui->line1, "geometry", this);line1Animation->setDuration(1500);line1Animation->setKeyValueAt(0, QRect(0, 25, 2, 0));line1Animation->setKeyValueAt(0.5, QRect(0, 0, 2, 25));line1Animation->setKeyValueAt(1, QRect(0, 25, 2, 0));line1Animation->setLoopCount(-1);line1Animation->start();

setDuration用来设置持续时间 setKeyValueAt用来设置关键帧 setLoopCount设置循环次数,接着将四个空间全设置上即可完成动画效果。

// 设置line2的动画效果line2Animation = new QPropertyAnimation(ui->line2, "geometry", this);line2Animation->setDuration(1600);line2Animation->setKeyValueAt(0, QRect(7, 25, 2, 0));line2Animation->setKeyValueAt(0.5, QRect(7, 0, 2, 25));line2Animation->setKeyValueAt(1, QRect(7, 25, 2, 0));line2Animation->setLoopCount(-1);line2Animation->start();// 设置line3的动画效果line3Animation = new QPropertyAnimation(ui->line3, "geometry", this);line3Animation->setDuration(1700);line3Animation->setKeyValueAt(0, QRect(14, 25, 2, 0));line3Animation->setKeyValueAt(0.5, QRect(14, 0, 2, 25));line3Animation->setKeyValueAt(1, QRect(14, 25, 2, 0));line3Animation->setLoopCount(-1);line3Animation->start();// 设置line4的动画效果line4Animation = new QPropertyAnimation(ui->line4, "geometry", this);line4Animation->setDuration(1800);line4Animation->setKeyValueAt(0, QRect(21, 25, 2, 0));line4Animation->setKeyValueAt(0.5, QRect(21, 0, 2, 25));line4Animation->setKeyValueAt(1, QRect(21, 25, 2, 0));line4Animation->setLoopCount(-1);line4Animation->start();

2.推荐页面设计

推荐页面如下图所示:
在这里插入图片描述
推荐页面主要由五部分组成比较简单:
在这里插入图片描述

3.recBox页面设计

推荐页面上我们需要设计一个recBox页面进行填充
在这里插入图片描述
主要有左右两个按钮就行切换轮放推荐上的封面图,封面图则使用上下两个布局管理器管理。

在这里插入图片描述
并且对按钮设置样式表QSS,包括背景图片、hover效果等。

#btUp{border:none;background-image:url(:/images/up_page.png);background-repeat:no-repeat;background-position:center center;
}QPushButton:hover{background-color:#1ECD97;
}

4.recBoxItem页面设计

接下来我们就要对recBox的布局中添加封面项,所以要设计一个recBoxitem:
在这里插入图片描述
接着我们要处理对item的鼠标进入离开的事件拦截:
在这里插入图片描述
就是如上图所示的动画效果,这里就要使用到eventFilter

(1).eventFilter介绍和使用

eventFilter 是 QObject 类中的一个虚函数,用于实现事件过滤机制。eventFilter 函数的原型如下:

bool QObject::eventFilter(QObject *watched, QEvent *event);

需要注意的在使用eventFilter拦截事件前 应先为控件安装事件过滤器

recBoxitem::recBoxitem(QWidget *parent) :QWidget(parent),ui(new Ui::recBoxitem)
{ui->setupUi(this);//安装事件过滤器ui->musicImageBox->installEventFilter(this);
}

接下来在eventFilter中拦截鼠标进入离开事件,编写动画效果:

bool recBoxitem::eventFilter(QObject *watched, QEvent *event)
{if(watched == ui->musicImageBox){if(event->type() == QEvent::Enter){QPropertyAnimation* animation = new QPropertyAnimation(watched, "geometry");animation->setDuration(300);animation->setStartValue(QRect(9, 9, ui->musicImageBox->width(), ui->musicImageBox->height()));animation->setEndValue(QRect(9, 0, ui->musicImageBox->width(), ui->musicImageBox->height()));animation->start();connect(animation,&QPropertyAnimation::finished,this,[=](){delete animation;});return true;}else if(event->type()==QEvent::Leave){QPropertyAnimation* animation = new QPropertyAnimation(watched, "geometry");animation->setDuration(300);animation->setStartValue(QRect(9, 0, ui->musicImageBox->width(), ui->musicImageBox->height()));animation->setEndValue(QRect(9, 9, ui->musicImageBox->width(), ui->musicImageBox->height()));animation->start();connect(animation,&QPropertyAnimation::finished,this,[=](){delete animation;});return true;}}return QObject::eventFilter(watched,event);
}

connect中使用lambda表达式销毁动画对象。还有个需要注意的细节,在当前函数中如果正确处理的鼠标进入或者离开事件则返回true,否则要继续调用基类的eventFilter。
此外还需要添加文本和图片的方法:

void recBoxitem::setText(const QString &text)
{ui->recBoxItemText->setText(text);
}void recBoxitem::setimagepath(const QString& path)
{QString style = "background-image:url("+path+");";ui->recMusicImage->setStyleSheet(style);
}

(2).QJsonObject介绍和使用

QJsonObject 是 Qt 库中用于处理 JSON对象的类。JSON 对象是由键值对组成的无序集合,键是字符串,值可以是字符串、数字、布尔值、数组、另一个 JSON 对象或 null。

QJsonObject::iterator insert(const QString &key, const QJsonValue &value);

以及一下常用方法:

QJsonValue QJsonObject::value(const QString &key) const

用key值返回value
QJsonArray则是用来存放QJsonValue的类型:
我们先来看下图片所在的目录:
在这里插入图片描述
进而编写随机存放图片的代码:

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget), currindex(-1)
{ui->setupUi(this);//设置随机数种子srand(time(NULL));initUi();}
QJsonArray Widget::RanddomPicture()
{QVector<QString> imagesArry;imagesArry<<"001.png"<<"003.png"<<"004.png"<<"005.png"<<"006.png"<<"007.png"<<"008.png"<<"009.png"<<"010.png"<<"011.png"<<"012.png"<<"013.png"<<"014.png"<<"015.png"<<"016.png"<<"017.png"<<"018.png"<<"019.png"<<"020.png"<<"021.png"<<"022.png"<<"023.png"<<"024.png"<<"025.png"<<"026.png"<<"027.png"<<"028.png"<<"029.png"<<"030.png"<<"031.png"<<"032.png"<<"033.png"<<"034.png"<<"035.png"<<"036.png"<<"037.png"<<"038.png"<<"039.png"<<"040.png";std::random_shuffle(imagesArry.begin(),imagesArry.end());QJsonArray JsonArry;for(int i=0;i<imagesArry.size();i++){QJsonObject obj;obj.insert("path",":/images/rec/"+imagesArry[i]);QString temp = QString("推荐-%1").arg(i,3,10,QChar('0'));obj.insert("text",temp);JsonArry.append(obj);}return JsonArry;
}

其中的random_shuffle 是 C++ 标准库中的一个算法,用于对指定范围内的元素进行随机重排。不过在 C++20 标准中被移除,替代它的是 std::shuffle,大家可以自行去了解。

(3).向recBox中添加元素

因为在recpage中有上下两个控件,分别为一行四列和两行四列,为了标识,我们需要在recBox类中添加行数和列数的成员变量:

	int row;//行数int col;//列数QJsonArray imageList;//保存图片

其中在构造函数中我们将变量初始化为一行四列。
实现代码:

void RecBox::initrecBox(QJsonArray jsonarray, int row)
{if(row == 2){this->row = row;this->col = 8;}else{ui->recListDown->hide();}imageList = jsonarray;creatRecBoxitem();
}void RecBox::creatRecBoxitem()
{//清除两个布局中的itemQList<recBoxitem*> retUplist = ui->recListUp->findChildren<recBoxitem*>();for(auto e : retUplist){ui->recListUpHLayout->removeWidget(e);delete e;}QList<recBoxitem*> retDownlist = ui->recListDown->findChildren<recBoxitem*>();for(auto e : retDownlist){ui->recListDownHLayout->removeWidget(e);delete e;}for(int i = 0; i < col; ++i) { RecBoxItem* item = new RecBoxItem(); QJsonObject obj = imageList[i].toObject(); item->setRecText(obj.value("text").toString()); item->setRecImage(obj.value("path").toString()); if(i >= col/2 && row == 2){ ui->recListDownHLayout->addWidget(item); } else { ui->recListUpHLayout->addWidget(item); } }  
}

(4).处理recBox的轮番按钮

接着我们要向recBox中的两个按钮添加槽函数 用来切换上一页和下一页。为了实现这一功能 我们要对图片进行分组,

    int currentindex;int count;
void RecBox::initrecBox(QJsonArray jsonarray, int row)
{if(row == 2){this->row = row;this->col = 8;}else{ui->recListDown->hide();}imageList = jsonarray;currentindex = 0;count = ceil(imageList.size()/col);//是否向上取整?creatRecBoxitem();
}

currentindex用来标识当前是第几组,count标识一共有多少组。count向上取整来计算是为了将所有的图片都能显示出来。
这样两个槽函数也很容易就能写出来了:


void RecBox::on_btDown_clicked()
{currentindex++;if(currentindex>=count){currentindex = 0;}creatRecBoxitem();
}void RecBox::on_btUp_clicked()
{currentindex--;if(currentindex<0){currentindex = count-1;}creatRecBoxitem();
}
//Widget.cpp中调用ui->recMusicBox->initrecBox(RanddomPicture(),1);
ui->supplyMusicBox->initrecBox(RanddomPicture(),2);

5.commonPage页面设计

commonPage页面是用来放在我喜欢、本地音乐和最近播放这三个页面的。
在这里插入图片描述
ui设计如下
在这里插入图片描述
因为这一个页面要放在三个不同的地方,所以会有不同的标题和封面,应给出一个方法用来设置:

void CommonPage::setCommonPageUI(const QString &text, const QString &path)
{ui->pageTittle->setText(text);ui->musicImageLabel->setPixmap(QPixmap(path));ui->musicImageLabel->setScaledContents(true);
}

然后再Widget.cpp中调用给出相应的参数即可。

6.ListItemBox页面设计

ui设计如下图所示:

在这里插入图片描述
接着对控件添加一个鼠标事件:

void ListitemBox::enterEvent(QEvent *event)
{(void)event;setStyleSheet("background-color:#EFEFEF");
}void ListitemBox::leaveEvent(QEvent *event)
{(void)event;setStyleSheet("");
}

7.MusicSlider设计

接下来我们再为进度条设计一个ui:
在这里插入图片描述
具体的进度条逻辑我们后续再做。

8.VolumeTool设计

接下来我们设计一个音量调节的控件:
在这里插入图片描述
因为音量控件属于弹出窗口,所以要设置窗口无框和弹出窗口:

VolumeTool::VolumeTool(QWidget *parent) :QWidget(parent),ui(new Ui::VolumeTool),isMuted(false),//默认非静音volume(20)
{ui->setupUi(this);//设置弹出窗口setWindowFlags(Qt::Popup | Qt::FramelessWindowHint|Qt::NoDropShadowWindowHint);setAttribute(Qt::WA_TranslucentBackground);// ⾃定义阴影效果QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);shadowEffect->setOffset(0, 0);shadowEffect->setColor("#646464");shadowEffect->setBlurRadius(10);setGraphicsEffect(shadowEffect);// 给按钮设置图标ui->silenceBtn->setIcon(QIcon(":/images/volumn.png"));ui->outSlider->setGeometry(ui->outSlider->x(),180+25-36,ui->outSlider->width(),36);ui->sliderBtn->setGeometry(ui->sliderBtn->x(),ui->outSlider->y()-ui->sliderBtn->height()/2,ui->sliderBtn->width(),ui->sliderBtn->height());}

在Widget中编写音量按钮的槽函数:

void Widget::on_volume_clicked()
{QPoint point = ui->volume->mapToGlobal(QPoint(0,0));QPoint volumeLeftTop = point - QPoint(volumetool->width()/2,volumetool->height());volumeLeftTop.setX(volumeLeftTop.x()+16);volumeLeftTop.setY(volumeLeftTop.y()+28);volumetool->move(volumeLeftTop);volumetool->show();
}

接着再把三角形画出来:

void VolumeTool::paintEvent(QPaintEvent *event)
{(void)event;//绘制三角形//创建绘画对象QPainter painter(this);//设置画笔painter.setPen(Qt::NoPen);//设置画刷颜色painter.setBrush(Qt::white);QPolygon polygon;QPoint a(10,300);QPoint b(10+80,300);QPoint c(10+40,300+20);polygon.append(a);polygon.append(b);polygon.append(c);painter.drawPolygon(polygon);}

最后这个音量控件就能正确的显示出来了:
在这里插入图片描述

三.媒体类进行歌曲管理

1.音乐的加载和分析

(1).MIME过滤器

我们对添加本地音乐的按钮编写槽函数:

void Widget::on_addLocal_clicked()//添加到本地音乐
{//创建文件对话框QFileDialog filedialog(this);//设置窗口标题filedialog.setWindowTitle("添加到本地音乐");filedialog.setAcceptMode(QFileDialog::AcceptOpen);filedialog.setFileMode(QFileDialog::ExistingFiles);//设置对话框的MIME过滤器QStringList mimelist;mimelist<<"application/octet-stream";filedialog.setMimeTypeFilters(mimelist);QDir dir(QDir::currentPath());dir.cdUp();QString music = dir.path() + "/Musicplayer/musics";qDebug()<<dir.path();filedialog.setDirectory(music);if(filedialog.exec() == QFileDialog::Accepted){//界面设置为本地下载ui->stackedWidget->setCurrentIndex(4);QList<QUrl> UrlList = filedialog.selectedUrls();//读取保存打开的音乐的url//保存到本地音乐中musiclist.addMusic(UrlList);ui->localpage->reFresh(musiclist);//将音乐信息添加到本地下载页面//将本地下载的音乐添加到媒体播放列表中ui->localpage->addMusicTOPlayList(musiclist,playlist);}}

MIME过滤器主要用于在文件对话框中限制用户能够选择的文件类型。例如上面的代码:

	QStringList mimelist;mimelist << "application/octet-stream";filedialog.setMimeTypeFilters(mimelist);

QStringList mimelist;:创建一个字符串列表,用于存储 MIME 类型。pplication/octet-stream 通常表示二进制数据
filedialog.setMimeTypeFilters(mimelist);将 MIME 类型列表设置到文件对话框中,限制用户只能选择符合该 MIME 类型的文件。

(2).MusicList类

我们创建一个MusicList类用来管理音乐。
首先我们先来了解一下歌曲文件的类型:

  • audio/mpeg: 适⽤于mp3格式的⾳乐⽂件
  • audio/flac: ⽆损压缩的⾳频⽂件,不会破坏任何原有的⾳频信息
  • audio/wav : 表⽰wav格式的歌曲⽂件

在MusicList类中添加变量和方法将添加到本地的音乐url保存起来:

 QVector<Music> musiclist;//管理一个个的Music对象
void MusicList::addMusic(const QList<QUrl> &urls)
{for(auto e: urls){//检测歌曲的MIME类型 过滤有效歌曲文件QMimeDatabase file;QMimeType type = file.mimeTypeForFile(e.toLocalFile());QString ret = type.name();if(ret == "audio/mpeg" || ret == "audio/flac"||ret == "audio/wav"){//构建music对象 并且添加进musiclist管理const Music music(e);//处理读取数据库后再次从本地添加音乐在页面上重复问题(数据库不重复UNIQUEmusicName)musiclist.push_back(music);}}
}

QMimeDatabase::mimeTypeForFile用于根据文件来确定其 MIME 类型。

(3).Music类

music类用来管理一个一个的音乐对象,其包含多种属性和方法:

class Music
{
public:Music();Music(QUrl url);void setMusicName(const QString& musicName);void setMusicSinger(const QString& musicSinger);void setMusicAlbum(const QString& musicAlbum);void setMusicUuid(QString uuid);void setIsLike(bool isLike);void setIsHistory(bool IsHistory);void setMusicDuration(const qint64 duration);void setMusicUrl(const QUrl musicUrl);QString getMusicName()const;QString getMusicSinger()const;QString getMusicAlbum()const;bool getIsLike()const;bool getIsHistory()const;qint64 getMusicDuration()const;QUrl getMusicUrl()const;QString getMusicUuid()const;QString getLrcFilePath();//将music信息插入musicInfovoid insertMusicTODB();//运算符重载bool operator==(const Music& t)const;
private:void parseMediaMetaData();private:QString musicName;QString musicSinger;QString musicAlbum;QString musicUuid;//musicid标识音乐qint64 duration;bool isLike;bool isHistory;QUrl musicUrl;
};

属性包含:歌曲名、歌手名、专辑名、歌曲的UUid、持续时长、是否为‘我喜欢’、是否为历史播放、音乐Url。

(4).QMediaPlayer类解析元数据

QMediaPlayer类用于在 Qt 应用程序中播放各种多媒体文件,如音频、视频等。使用这个类解析元数据:

void Music::parseMediaMetaData()
{// 创建一个 QMediaPlayer 对象用于播放媒体文件并获取元数据QMediaPlayer player;// 设置媒体播放器的媒体源为 musicUrl 所指向的音乐文件player.setMedia(musicUrl);// 等待媒体文件的元数据加载完成while(!player.isMetaDataAvailable()){// 让事件循环继续,避免界面冻结,确保程序可以响应其他事件QCoreApplication::processEvents();}// 检查元数据是否可用if(player.isMetaDataAvailable()){// 从元数据中获取音乐名称musicName = player.metaData("Title").toString();// 从元数据中获取歌手信息musicSinger = player.metaData("Author").toString();// 从元数据中获取专辑信息musicAlbum = player.metaData("AlbumTitle").toString();// 从媒体播放器中获取音乐的时长(以毫秒为单位)duration = player.duration();// 处理音乐文件元数据为空的情况,尝试从文件名中提取信息// 获取音乐文件的文件名QString musicUrlName = musicUrl.fileName();// 查找文件名中 '-' 的位置int index = musicUrlName.indexOf('-');// 若音乐名称为空if(musicName.isEmpty()){// 若文件名中包含 '-'if(index != -1){// 从文件名中截取 '-' 之前的部分作为音乐名称,并去除前后空格musicName = musicUrlName.mid(0,index).trimmed();}else{// 若文件名中不包含 '-',截取文件名中 '.' 之前的部分作为音乐名称,并去除前后空格musicName = musicUrlName.mid(0,musicUrlName.indexOf('.')).trimmed();}}// 若歌手信息为空if(musicSinger.isEmpty()){// 若文件名中不包含 '-'if(index == -1){// 将歌手信息设置为“未知歌手”musicSinger = "未知歌手";}else{// 从文件名中截取 '-' 之后、'.' 之前的部分作为歌手信息,并去除前后空格musicSinger = musicUrlName.mid(index+1,musicUrlName.indexOf('.')-1-index).trimmed();}}if(musicAlbum.isEmpty()){musicAlbum = "未知专辑";}qDebug()<<musicUrlName<<":"<<musicName<<":"<<musicSinger<<":"<<musicAlbum<<":"<<duration;}
}

2.音乐分类

根据commonPage知,音乐需要分为我喜欢、本地音乐和历史播放三种,为了区分我们添加枚举类型。

enum PageType
{LIKE_PAGE,//我喜欢页面LOCAL_PAGE,//本地下载页面HISTORY_PAGE//历史播放页面
};

并在Widget中初始化ui中:

ui->likePage->setMusicListType(PageType::LIKE_PAGE);ui->likePage->setCommonPageUI("我喜欢", ":/images/ilikebg.png");ui->localPage->setMusicListType(PageType::LOCAL_PAGE);ui->localPage->setCommonPageUI("本地⾳乐", ":/images/localbg.png");ui->recentPage->setMusicListType(PageType::HISTORY_PAGE);ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");

我们添加到本地的音乐还需要放到MusicList中管理:

void CommonPage::addMusicTOPlayList(MusicList &musiclist, QMediaPlaylist* playlist)
{for(auto music:musiclist){switch (pageType){case LIKE_PAGE:if(music.getIsLike()){playlist->addMedia(music.getMusicUrl());}break;case LOCAL_PAGE:playlist->addMedia(music.getMusicUrl());break;case HISTORY_PAGE:if(music.getIsHistory()){playlist->addMedia(music.getMusicUrl());}break;default:break;}}
}

为了让MusicList支持范围for操作 我们重写一下函数:

iterator MusicList::begin()
{return musiclist.begin();
}iterator MusicList::end()
{return musiclist.end();
}

(1).更新music到CommonPage页面上

void CommonPage::reFresh(MusicList &musiclist)
{//解决重复问题 清除listwidgetui->pagemusicBox->clear();//将歌曲uuid添加到musiclistOFPageaddMusicTOMusicPage(musiclist);for(auto musicUuid : musiclistOFPage){auto ret = musiclist.findMusicByUuid(musicUuid);if(ret == musiclist.end())continue;//添加ListitemBox到listWidget中ListitemBox* Listitembox = new ListitemBox(this);//设置音乐名称 歌手 专辑名称Listitembox->setMusicName(ret->getMusicName());Listitembox->setMusicSinner(ret->getMusicSinger());Listitembox->setMusicAlbum(ret->getMusicAlbum());//初始化 设置歌曲的是否为我喜欢Listitembox->setMusicLike(ret->getIsLike());QListWidgetItem* ListWidgetItem = new QListWidgetItem(ui->pagemusicBox);ListWidgetItem->setSizeHint(QSize(ui->pagemusicBox->width(),45));ui->pagemusicBox->setItemWidget(ListWidgetItem,Listitembox);//接受listitembox发送的信号setIsLikeconnect(Listitembox,&ListitemBox::setIsLike,this,[=](bool isLike){emit updataLikeMusic(isLike,ret->getMusicUuid());});}// repaint()会⽴即执⾏paintEvent(),不会等待事件队列的处理// update()将⼀个paintEvent事件添加到事件队列中,等待稍后执⾏,即不会⽴即执⾏paintEventrepaint();
}
void CommonPage::addMusicTOMusicPage(MusicList& musiclist)
{//清除之前的歌曲musiclistOFPage.clear();for(auto music : musiclist){switch(pageType){case LIKE_PAGE:if(music.getIsLike()){musiclistOFPage.push_back(music.getMusicUuid());}break;case LOCAL_PAGE:musiclistOFPage.push_back(music.getMusicUuid());break;case HISTORY_PAGE:if(music.getIsHistory()){musiclistOFPage.push_back(music.getMusicUuid());}break;default:qDebug()<<"未知歌曲";break;}}
}

(2).收藏音乐

接下来我们处理音乐是否为‘我喜欢’的相关操作:
首先我们需要切换图标变成爱心:

void ListitemBox::setMusicLike(bool islike)
{this->isLike = islike;if(isLike){ui->likeBtu->setIcon(QIcon(":/images/like_2.png"));}else{ui->likeBtu->setIcon(QIcon(":/images/like_3.png"));}
}//按钮槽函数
void ListitemBox::onLikeBtuCliked()
{isLike = !isLike;setMusicLike(isLike);emit setIsLike(isLike);
}

每个listitemBox都有可能发出setIsLike的信号,onnect(Listitembox,&ListitemBox::setIsLike,this,[=](bool isLike){ emit updataLikeMusic(isLike,ret->getMusicUuid());将需要更新music的信号传递给Widget,QQMusic收到CommonPage发射的updateLikePage信号后,通知其上的likePage、localPage、
recentPage更新其界⾯的我喜欢歌曲信息。


void Widget::OnupdataLikeMusic(bool isLike, QString musicUuid)
{//找到音乐 更新状态信息auto ret = musiclist.findMusicByUuid(musicUuid);if(ret != musiclist.end()){ret->setIsLike(isLike);}//更新页面信息ui->likepage->reFresh(musiclist);ui->localpage->reFresh(musiclist);ui->recentpage->reFresh(musiclist);
}

四.播放控制区域实现

1.播放控制类介绍

(1).QMediaPlayer类

QMediaPlayer 是 Qt 框架中用于处理和播放多媒体内容:
常用属性

  • media:用于设置或获取要播放的媒体资源,可以是本地文件路径、网络 URL 或 QMediaContent 对象。
  • position:表示当前播放位置,单位是毫秒。可通过设置该属性来实现播放进度的跳转。
  • duration:表示媒体文件的总时长,单位是毫秒。
  • volume:表示播放音量,取值范围是 0 到 100,0 表示静音,100表示最大音量。
  • playbackRate:表示播放速率,默认值为 1.0,即正常播放速度。大于 1.0 表示加快播放,小于 1.0表示减慢播放。

常用方法

  • setMedia(const QMediaContent &media):设置要播放的媒体资源。
  • play():开始播放媒体。
  • pause():暂停播放。
  • stop():停止播放。
  • setPosition(qint64 position):设置播放位置。
  • setVolume(int volume):设置播放音量。
  • setPlaybackRate(qreal rate):设置播放速率。

信号与槽

  • stateChanged(QMediaPlayer::State state):当播放状态发生改变时发出该信号,state表示新的播放状态,如QMediaPlayer::PlayingState(播放中)、QMediaPlayer::PausedState(暂停)、QMediaPlayer::StoppedState(停止)。
  • positionChanged(qint64 position):当播放位置发生改变时发出该信号,position 表示当前的播放位置。
  • durationChanged(qint64 duration):当媒体文件的总时长发生改变时发出该信号,duration表示新的总时长。
  • mediaStatusChanged(QMediaPlayer::MediaStatus
    status):当媒体状态发生改变时发出该信号,如
    QMediaPlayer::LoadedMedia(媒体已加载)、QMediaPlayer::BufferingMedia(媒体正在缓冲)等。

(2).QMediaPlayList类

QMediaPlaylist 是 Qt 框架里用于管理多媒体播放列表的类,常与 QMediaPlayer 搭配使用,可实现多媒体文件的顺序播放、随机播放、循环播放等功能。
常用属性

playbackMode:该属性用于设置播放列表的播放模式,常见的播放模式有:
QMediaPlaylist::CurrentItemOnce:当前项播放一次。

QMediaPlaylist::CurrentItemInLoop:当前项循环播放。
QMediaPlaylist::Sequential:顺序播放,播放完列表中的所有项后停止。
QMediaPlaylist::Loop:循环播放,播放完列表中的所有项后从头开始重新播放。
QMediaPlaylist::Random:随机播放,随机选择列表中的项进行播放。

常用方法

  • 添加和移除媒体项 addMedia(const QMediaContent &media):向播放列表中添加一个媒体项。
  • addMedia(const QList &mediaList):向播放列表中添加多个媒体项。
  • insertMedia(int index, const QMediaContent &media):在指定索引位置插入一个媒体项。
  • removeMedia(int position):移除指定索引位置的媒体项。 clear():清空播放列表中的所有媒体项。
  • 获取和设置播放列表信息 mediaCount():返回播放列表中的媒体项数量。
  • currentIndex():返回当前正在播放的媒体项的索引。
  • setCurrentIndex(int index):设置当前要播放的媒体项的索引。
  • media(int index):返回指定索引位置的媒体项。

信号与槽

  • currentIndexChanged(int position):当当前播放的媒体项索引发生改变时发出该信号,position 表示新的索引位置。

  • mediaInserted(int start, int end):当有媒体项插入到播放列表中时发出该信号,start 和 end 表示插入的媒体项的索引范围。

  • mediaRemoved(int start, int end):当有媒体项从播放列表中移除时发出该信号,start 和 end表示移除的媒体项的索引范围。

    mediaChanged(int start, int end):当播放列表中的媒体项发生改变时发出该信号,start 和 end 表示改变的媒体项的索引范围。

2.歌曲播放

首先我们先对播放媒体和媒体播放列表进行初始化:

QMediaPlayer* player;
QMediaPlaylist* playlist;void Widget::initPlayer()
{//创建播放器player = new QMediaPlayer(this);//创建播放列表playlist = new QMediaPlaylist(this);//设置初始播放模式playlist->setPlaybackMode(QMediaPlaylist::Loop);//播放器添加媒体播放列表player->setPlaylist(playlist);//设置初始音量player->setVolume(20);
}

在播放歌曲之前我们需要将歌曲添加到媒体播放列表中,因为不同CommonPage页面的歌曲不同,所以新增将页面歌曲添加到播放媒体列表的方法:

void CommonPage::addMusicTOPlayList(MusicList &musiclist, QMediaPlaylist* playlist)
{for(auto music:musiclist){switch (pageType){case LIKE_PAGE:if(music.getIsLike()){playlist->addMedia(music.getMusicUrl());}break;case LOCAL_PAGE:playlist->addMedia(music.getMusicUrl());break;case HISTORY_PAGE:if(music.getIsHistory()){playlist->addMedia(music.getMusicUrl());}break;default:break;}}
}

3.播放与暂停

接下来我们对播放控制区域的暂停播放按钮添加槽函数:

void Widget::onPlayCliked()
{if(QMediaPlayer::PlayingState == player->state())//正在播放媒体{player->pause();}else if(QMediaPlayer::PausedState == player->state())//暂停状态{player->play();}else if(QMediaPlayer::StoppedState == player->state())//停止状态{player->play();}else//打印错误信息{qDebug()<<player->errorString();}
}connect(ui->Play,&QPushButton::clicked,this,&Widget::onPlayCliked);

另外再播放暂停开始时,按钮上的图标也会发生变化,这时我们可以拦截stateChanged(QMediaPlayer::State state)信号进行更换图标:

void Widget::onPlayStateChanged()
{if(player->state() == QMediaPlayer::PlayingState){//播放状态ui->Play->setIcon(QIcon(":/images/play_on.png"));}else{//暂停状态ui->Play->setIcon(QIcon(":/images/play3.png"));}
}connect(player,&QMediaPlayer::stateChanged,this,&Widget::onPlayStateChanged);

4.上一曲和下一曲

接着我们对切换上下一曲歌添加槽函数:

void Widget::onPlayUpCliked()
{playlist->previous();
}void Widget::onPlayDownCliked()
{playlist->next();
}
//关联切换上一曲下一曲槽函数connect(ui->playUp,&QPushButton::clicked,this,&Widget::onPlayUpCliked);connect(ui->playDown,&QPushButton::clicked,this,&Widget::onPlayDownCliked);

5.播放模式切换

我们实现的播放器支持了三种播放模式:随机播放、单曲循环和、列表顺序循环。

void Widget::onPlaybackModeCliked()
{//列表循环 随机播放 单曲循环if(playlist->playbackMode() == QMediaPlaylist::Loop){playlist->setPlaybackMode(QMediaPlaylist::Random);//随机播放ui->playMode->setToolTip("随机播放");}else if(playlist->playbackMode() == QMediaPlaylist::Random){playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);//单曲循环ui->playMode->setToolTip("单曲循环");}else if(playlist->playbackMode() == QMediaPlaylist::CurrentItemInLoop){playlist->setPlaybackMode(QMediaPlaylist::Loop);//列表循环ui->playMode->setToolTip("列表循环");}else{qDebug()<<"暂不支持";}}connect(ui->playMode,&QPushButton::clicked,this,&Widget::onPlaybackModeCliked)

默认为顺序播放。
根据不同播放模式的变换相应的图标也应该更改:

void Widget::onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode)
{if(playbackMode == QMediaPlaylist::Loop){ui->playMode->setIcon(QIcon(":/images/list_play.png"));}else if(playbackMode == QMediaPlaylist::Random){ui->playMode->setIcon(QIcon(":/images/shuffle_2.png"));}else if(playbackMode == QMediaPlaylist::CurrentItemInLoop){ui->playMode->setIcon(QIcon(":/images/single_play.png"));}else{qDebug()<<"暂不支持";}
}connect(playlist,&QMediaPlaylist::playbackModeChanged,this,&Widget::onPlaybackModeChanged);

6.播放全部

在每个CommonPage页面上还有一个播放全部的按钮,但是CommonPage并不能控制音乐的播放,所以点击这个按钮的时候需要向Widget发送信号:

在这里插入图片描述

CommonPage::CommonPage(QWidget *parent) :QWidget(parent),ui(new Ui::CommonPage)
{ui->setupUi(this);//取消水平滚动条ui->pagemusicBox-connect(ui->playAllBtu,&QPushButton::clicked,this,[=](){emit playAll(pageType);});}
void Widget::onPlayAll(PageType pagetype)
{CommonPage* page = nullptr;switch (pagetype) {case PageType::LIKE_PAGE:page = ui->likepage;break;case PageType::LOCAL_PAGE:page = ui->localpage;break;case PageType::HISTORY_PAGE:page = ui->recentpage;break;default:break;}playAllOFCommonpage(page,0);//从当前页面的第0号音乐开始播放
}void Widget::playAllOFCommonpage(CommonPage *page, int index)
{playlist->clear();page->addMusicTOPlayList(musiclist,playlist);playlist->setCurrentIndex(index);player->play();
}//关联播放全部connect(ui->likepage,&CommonPage::playAll,this,&Widget::onPlayAll);connect(ui->localpage,&CommonPage::playAll,this,&Widget::onPlayAll);connect(ui->recentpage,&CommonPage::playAll,this,&Widget::onPlayAll);

7.双击ListitemBox播放

还需实现一个用户双击ListitemBox也可以播放音乐的功能:

//双击时先widget发送信号connect(ui->pagemusicBox,&QListWidget::doubleClicked,this,[=](QModelIndex index){emit playMusicByIndex(this,index.row());});
void Widget::playMusicByIndex(CommonPage *page, int index)
{playAllOFCommonpage(page,index);
}//关联双击connect(ui->likepage,&CommonPage::playMusicByIndex,this,&Widget::playMusicByIndex);connect(ui->localpage,&CommonPage::playMusicByIndex,this,&Widget::playMusicByIndex);connect(ui->recentpage,&CommonPage::playMusicByIndex,this,&Widget::playMusicByIndex);

8.最近播放同步

当我们播放了歌曲,此歌曲就应该添加到历史播放页面中:


void Widget::onCurrentIndexChanged(int index)
{currindex = index;//根据currentIndexChanged信号提供的index找到对应的musicQString musicid = currpage->getMusicIdIndex(index);auto it = musiclist.findMusicByUuid(musicid);if(it != musiclist.end()){it->setIsHistory(true);}ui->recentpage->reFresh(musiclist);//refresh检测是否History为true进而刷新到页面中
}QString CommonPage::getMusicIdIndex(int index)
{if(index >= musiclistOFPage.size()){qDebug()<<"暂无此音乐";return "";}return musiclistOFPage[index];
}
//关联当前播放音乐变化connect(playlist,&QMediaPlaylist::currentIndexChanged,this,&Widget::onCurrentIndexChanged);

9.音量功能

VolumeTool类中添加两个成员变量,在构造函数中关联静音按钮的槽函数,接着向Widget发送静音信号:

signals:void setSilence(bool);
bool isMuted;//是否为静音
int volume;//音量
//关联静音按钮槽函数connect(ui->silenceBtn,&QPushButton::clicked,this,&VolumeTool::onSilenceBtnCliked);
void VolumeTool::onSilenceBtnCliked()
{isMuted = !isMuted;if(isMuted)//true为静音{ui->silenceBtn->setIcon(QIcon(":/images/silent.png"));}else{ui->silenceBtn->setIcon(QIcon(":/images/volumn.png"));}emit setSilence(isMuted);
}
//关联setSilence信号connect(volumetool,&VolumeTool::setSilence,this,&Widget::setMusicSilence);
void Widget::setMusicSilence(bool Muted)
{player->setMuted(Muted);
}

还有在音量滑竿上拖动实现音量改变的功能:

bool VolumeTool::eventFilter(QObject *watched, QEvent *event)
{if(watched == ui->sliderBox){if(event->type() == QEvent::MouseButtonPress)//鼠标按下{calVolume();}else if(event->type() == QEvent::MouseButtonRelease)//鼠标松开{emit setMusicVolume(volume);}else if(event->type() == QEvent::MouseMove)//鼠标移动{calVolume();emit setMusicVolume(volume);}return true;}//不是sliderBox 是别的控件 让系统return QObject::eventFilter(watched,event);
}void VolumeTool::calVolume()
{//1 .将⿏标的位置转换为sloderBox上的相对坐标,此处只要获取y坐标int height = ui->sliderBox->mapFromGlobal(QCursor().pos()).y();// 2. ⿏标在volumeBox中可移动的y范围在[25, 205之间]height = height<25? 25 : height;height = height>205? 205 : height;// 3. 调整sliderBt的位置ui->sliderBtn->move(ui->sliderBtn->x(),height-ui->sliderBtn->height()/2);// 4. 更新outline的位置和⼤⼩ui->outSlider->setGeometry(ui->outSlider->x(),height,ui->outSlider->width(),205-height);// 5. 计算⾳量⽐率volume = (int)ui->outSlider->height()/(float)180*100;// 6. 设置给label显⽰出来ui->volumeRatio->setText(QString::number(volume)+"%");}
//关联setMusicVolume信号connect(volumetool,&VolumeTool::setMusicVolume,this,&Widget::setPlayVolume);
void Widget::setPlayVolume(int volume)
{player->setVolume(volume);
}

10.歌曲播放时间处理

获取到歌曲的总时间并且更新到界面上,在播放歌曲改变时进行,我们在Widget中接受durationChanged信号即可:

//关联DuratinonChanged信号
connect(player,&QMediaPlayer::durationChanged,this,&Widget::onDurationChanged);void Widget::onDurationChanged(qint64 duration)
{totalTime = duration;ui->totalTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0')).arg(duration/1000%60,2,10,QChar('0')));
}

以及歌曲当前已经播放时间的处理:QMediaPlayer会发射positionChanged信号

//关联PositionChanged信号connect(player,&QMediaPlayer::positionChanged,this,&Widget::onPositionChanged);
void Widget::onPositionChanged(qint64 position)
{//更新已经播放的视频ui->currentTime->setText(QString("%1:%2").arg(position/1000/60,2,10,QChar('0')).arg(position/1000%60,2,10,QChar('0')));//处理进度条ui->processBar->setStep(position/(float)totalTime);}

11.进度条设置

(1).进度条界面显示

在 Qt 中,“seek” 功能用于在多媒体播放过程中实现定位播放,也就是可以将播放位置跳转到指定的时间点。

class MusicSlider : public QWidget
{Q_OBJECTpublic:explicit MusicSlider(QWidget *parent = nullptr);~MusicSlider();void moveSilder();void setStep(float pace);
protected:void mouseMoveEvent(QMouseEvent *event);void mouseReleaseEvent(QMouseEvent *event);void mousePressEvent(QMouseEvent *event);
signals:void setMusicSilderPosition(float);private:Ui::MusicSlider *ui;int currPos;//当前进度int maxWidth;//最大进度
};void MusicSlider::moveSilder()
{ui->outLine->setGeometry(ui->outLine->x(),ui->outLine->y(),currPos,ui->outLine->height());
}void MusicSlider::mouseMoveEvent(QMouseEvent *event)
{currPos = event->pos().x();if(currPos<0){currPos = 0;}else if(currPos>maxWidth){currPos = maxWidth;}moveSilder();}void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{currPos = event->pos().x();moveSilder();emit setMusicSilderPosition(currPos/(float)maxWidth);
}void MusicSlider::mousePressEvent(QMouseEvent *event)
{currPos = event->pos().x();moveSilder();
}

(2).进度条同步播放时间

当进度条发生改变后相应的播放时间也该发生变化:

void Widget::onMusicSliderChanged(float value)
{//根据进度条比率调整播放时间qint64 duration = (qint64)(value*totalTime);ui->currentTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0')).arg(duration/1000%60,2,10,QChar('0')));player->setPosition(duration);
}
//关联setMusicSliderPosition信号connect(ui->processBar,&MusicSlider::setMusicSilderPosition,this,&Widget::onMusicSliderChanged);

当播放时间改变时同样的进度条也应该同步:

void MusicSlider::setStep(float pace)
{currPos = pace*maxWidth;moveSilder();
}void Widget::onPositionChanged(qint64 position)
{//更新已经播放的视频ui->currentTime->setText(QString("%1:%2").arg(position/1000/60,2,10,QChar('0')).arg(position/1000%60,2,10,QChar('0')));//处理进度条ui->processBar->setStep(position/(float)totalTime);if(currindex >= 0){lrcpage->showLrcWord(position);}}

12.修改歌曲信息

当歌曲切换时,应该同步修改界面上的歌曲封面歌手专辑等信息:

void Widget::onMetaDataAvailableChanged(bool available)
{(int)available;//获取当前歌曲idQString musicid = currpage->getMusicIdIndex(currindex);auto it = musiclist.findMusicByUuid(musicid);QString musicname("未知歌曲");QString musicsinger("未知歌手");if(it != musiclist.end()){musicname = it->getMusicName();musicsinger = it->getMusicSinger();}ui->musicName->setText(musicname);ui->musicSinger->setText(musicsinger);//获取封面图QVariant coverimage = player->metaData("ThumbnailImage");if(coverimage.isValid()){QImage image = coverimage.value<QImage>();ui->musicCover->setPixmap(QPixmap::fromImage(image));currpage->setMusicImage(QPixmap::fromImage(image));}else{qDebug()<<"暂无封面";ui->musicCover->setPixmap(QPixmap(":/images/cyx.png"));currpage->setMusicImage(QPixmap(":/images/cyx.png"));}ui->musicCover->setScaledContents(true); }

13.lrc歌词设计

(1).lrc歌词页面动画设计

除此之外我们还需要设计一个lrc歌词页面:如下图所示
在这里插入图片描述
ui设计如下
在这里插入图片描述
重要的是给lrc页面添加一个动画效果:
在这里插入图片描述

//创建lrcpage实例lrcpage = new LrcPage(this);lrcpage->setGeometry(5,5,width(),height());lrcpage->hide();//设置歌词展示的上移动画效果lrcAnimation = new QPropertyAnimation(lrcpage,"geometry",this);lrcAnimation->setDuration(400);lrcAnimation->setStartValue(QRect(5,5+lrcpage->height(),lrcpage->width(),lrcpage->height()));lrcAnimation->setEndValue(QRect(5,5,lrcpage->width(),lrcpage->height()));//关联打开歌词窗口信号connect(ui->lrcWord,&QPushButton::clicked,this,&Widget::onLrcWordClicked);void Widget::onLrcWordClicked()
{lrcpage->show();lrcAnimation->start();
}

接着设计歌词关闭的动画效果:

在这里插入代码片 //设置关闭动画效果lrcAnimation = new QPropertyAnimation(this,"geometry",this);lrcAnimation->setDuration(400);lrcAnimation->setStartValue(QRect(5,5,width(),height()));lrcAnimation->setEndValue(QRect(5,5+height(),width(),height()));connect(ui->hideBtn,&QPushButton::clicked,this,[=](){lrcAnimation->start();});connect(lrcAnimation,&QPropertyAnimation::finished,this,[=](){hide();});showLrcWord(-1);

(2).歌词解析

为了解析歌词我们定义一个结构体用来描述:

struct LrcLine{qint64 _time;//歌词所在时间QString _text;//歌词内容LrcLine(qint64 time,QString text):_time(time),_text(text){}
};

首先我们把歌曲文件后缀替换为.lrc:

QString Music::getLrcFilePath()//知道歌词文件
{QString temp = musicUrl.toLocalFile();//替换后缀为.lrctemp.replace(".mp3",".lrc");temp.replace(".flac",".lrc");temp.replace(".mpga",".lrc");return temp;
}

接着分析.lrc文件填充结构体:

bool LrcPage::parseLrc(QString lrcpath)
{QFile file(lrcpath);if(!file.open(QIODevice::ReadOnly)){qDebug()<<"文件打开失败";return false;}//将上一首歌词清除lrcLines.clear();//解析歌词while(!file.atEnd()){QString lrclineword = file.readLine(1024);// [00:17.94]那些失眠的⼈啊 你们还好吗// [0:58.600.00]你像⼀只⻜来⻜去的蝴蝶int start = 0,end = 0;end = lrclineword.indexOf(']');QString lrctime = lrclineword.mid(start,end-start+1);QString lrcword = lrclineword.mid(end+1,lrclineword.size()-end-1-1);//解析分钟qint64 linetime = 0;start = 1;end = lrctime.indexOf(':');linetime += lrclineword.mid(start,end-start).toInt()*60*1000;//解析秒start = end+1;end = lrctime.indexOf('.',start);linetime += lrclineword.mid(start,end-start).toInt()*1000;//解析毫秒start = end+1;end = lrctime.indexOf('.',start);linetime += lrclineword.mid(start,end-start).toInt();lrcLines.push_back(LrcLine(linetime,lrcword));}for(auto e:lrcLines){qDebug()<<e._time<<" "<<e._text;}return true;
}

接下来我们根据播放时间同步拿到歌词并显示:

int LrcPage::getLineLrcWordIndex(qint64 pos)
{//没有歌词返回负一if(lrcLines.empty()){return -1;}if(pos <= lrcLines[0]._time){return 0;}for(int i =1;i<lrcLines.size();i++){if(pos>=lrcLines[i-1]._time && pos<lrcLines[i]._time){return i-1;}}//没有找到 返回歌词最后一行return lrcLines.size()-1;
}
void LrcPage::showLrcWord(int time)
{int index = getLineLrcWordIndex(time);if(index == -1){ui->line1->setText("");ui->line2->setText("");ui->line3->setText("");ui->lineCenter->setText("当前歌曲无歌词");ui->line5->setText("");ui->line6->setText("");ui->line7->setText("");}else{ui->line1->setText(getLrcWordByIndex(index-3));ui->line2->setText(getLrcWordByIndex(index-2));ui->line3->setText(getLrcWordByIndex(index-1));ui->lineCenter->setText(getLrcWordByIndex(index));ui->line5->setText(getLrcWordByIndex(index+1));ui->line6->setText(getLrcWordByIndex(index+2));ui->line7->setText(getLrcWordByIndex(index+3));}
}QString LrcPage::getLrcWordByIndex(qint64 index)
{if(index<0 || index>=lrcLines.size()){return "";}return lrcLines[index]._text;
}

接着在歌曲发生切换和播放时间改变时调用函数:

void Widget::onMetaDataAvailableChanged(bool available)
{//.........//解析歌词if(it != musiclist.end()){QString lrcpath = it->getLrcFilePath();qDebug()<<"调用parseLrc";lrcpage->parseLrc(lrcpath);lrcpage->setMusicNameAndSinger(musicname,musicsinger);}}void Widget::onPositionChanged(qint64 position)
{//更新已经播放的视频ui->currentTime->setText(QString("%1:%2").arg(position/1000/60,2,10,QChar('0')).arg(position/1000%60,2,10,QChar('0')));//处理进度条ui->processBar->setStep(position/(float)totalTime);if(currindex >= 0){lrcpage->showLrcWord(position);}}

五.数据库实现持久化

1.SQLite介绍

SQLite 是⼀款轻量级、⽆需安装的桌⾯型数据库,

  • 开源免费:SQLite 遵循公共领域许可,这意味着你可以自由地使用、修改和分发它,无需支付任何费用,降低了开发成本。
  • 嵌入式特性:SQLite是嵌入式数据库,它没有独立的服务器进程,数据库以文件形式存储在磁盘上,应用程序可以直接访问该文件进行数据的读写操作,适合嵌入式设备和移动应用。
  • 轻量级:SQLite 的代码库体积小巧,占用资源少,无需复杂的配置和管理,对于资源有限的系统(如物联网设备)非常友好。

具体的语法语句操作 我们后续在使用中学习。

2.QSqlDatabase类介绍

QSqlDatabase 是 Qt 框架中用于管理数据库连接的核心类,它属于 Qt SQL 模块,QSqlDatabase 类代表一个数据库连接,通过它可以创建、打开、关闭和管理数据库连接。在使用 QSqlDatabase 之前,需要确保已经正确配置了 Qt SQL 模块:在.pro文件下:
在这里插入图片描述

3.数据库初始化

(1).initSQLite

void Widget::initSQLite()
{sqlite = QSqlDatabase::addDatabase("QSQLITE");sqlite.setDatabaseName("music.db");if(!sqlite.open()){QMessageBox::critical(this,"打开music.db失败",sqlite.lastError().text());return ;}qDebug()<<"数据库music.db创建成功";QString sql("CREATE TABLE IF NOT EXISTS musicInfo(\id INTEGER PRIMARY KEY AUTOINCREMENT,\musicUuid VARCHAR(200) UNIQUE,\musicName VARCHAR(50) UNIQUE,\musicSinger VARCHAR(50),\albumName VARCHAR(50),\duration BIGINT,\musicUrl VARCHAR(256),\isLike INTEGER,\isHistory INTEGER)");QSqlQuery sqlquery;if(!sqlquery.exec(sql)){QMessageBox::critical(this,"创建表musicInfo失败",sqlquery.lastError().text());return;}qDebug()<<"创建表musicInfo成功";}

(2).歌曲信息写入数据库

当程序退出时我们应该将歌曲信息写进数据库进而持久化:

void MusicList::writeMusicTODB()
{for(auto music : musiclist){music.insertMusicTODB();}
}void Music::insertMusicTODB()
{//检测当前music是否在表musicInfo中//存在 更新isLike和isHisotry属性//不存在将music信息插入表中QSqlQuery sqlquery;sqlquery.prepare("SELECT EXISTS (SELECT 1 FROM musicInfo where musicUuid = ?)");sqlquery.addBindValue(musicUuid);if(!sqlquery.exec()){qDebug()<<"查询当前music是否存在失败"<<sqlquery.lastError().text();return ;}qDebug()<<"查询成功";if(sqlquery.next()){bool isExist;//是否存在isExist = sqlquery.value(0).toBool();if(isExist){sqlquery.prepare("UPDATE musicInfo SET isLike = ?, isHistory = ? WHERE musicUuid =?");sqlquery.addBindValue(isLike ? 1:0);sqlquery.addBindValue(isHistory ? 1:0);sqlquery.addBindValue(musicUuid);if(!sqlquery.exec()){qDebug()<<"更新歌曲isLike.isHistory失败"<<sqlquery.lastError().text();return ;}qDebug()<<"更新成功";}else{sqlquery.prepare("INSERT INTO musicInfo(musicUuid,musicName,musicSinger,\albumName,duration,musicUrl,isLike,isHistory) \VALUES(?,?,?,?,?,?,?,?)");sqlquery.addBindValue(musicUuid);sqlquery.addBindValue(musicName);sqlquery.addBindValue(musicSinger);sqlquery.addBindValue(musicAlbum);sqlquery.addBindValue(duration);sqlquery.addBindValue(musicUrl.toLocalFile());sqlquery.addBindValue(isLike ? 1:0);sqlquery.addBindValue(isHistory ? 1:0);if(!sqlquery.exec()){qDebug()<<"插入歌曲信息失败"<<sqlquery.lastError().text();return ;}qDebug()<<"插入歌曲信息成功";}}}

(3).程序启动读取数据库歌曲信息

当程序启动后就从数据库中读取已经保存的歌曲信息:

void MusicList::readMusicBYDB()
{QSqlQuery sqlquery;sqlquery.prepare("SELECT musicUuid,musicName,musicSinger,albumName,duration,\musicUrl,isLike,isHistory FROM musicInfo");if(!sqlquery.exec()){qDebug()<<"查询失败"<<sqlquery.lastError().text();return ;}qDebug()<<"查询成功";while(sqlquery.next()){Music music;music.setMusicUuid(sqlquery.value(0).toString());music.setMusicName(sqlquery.value(1).toString());music.setMusicSinger(sqlquery.value(2).toString());music.setMusicAlbum(sqlquery.value(3).toString());music.setMusicDuration(sqlquery.value(4).toLongLong());music.setMusicUrl("file:///"+sqlquery.value(5).toString());music.setIsLike(sqlquery.value(6).toBool());music.setIsHistory(sqlquery.value(7).toBool());musiclist.push_back(music);}
}

六.优化与debug

1.添加系统托盘

当我们点击程序的关闭按钮时,发现程序直接退出了,可有时候我们只是想让程序不显示在任务栏而退居与系统托盘中,所以我们要添加:

//添加系统托盘QSystemTrayIcon* trayIcon = new QSystemTrayIcon(this);trayIcon->setIcon(QIcon(":/images/tubiao.png"));//创建托盘菜单QMenu* trayMenu = new QMenu(this);trayMenu->addAction("显示",this,&QWidget::showNormal);trayMenu->addAction("退出",this,&Widget::quitMusic);trayIcon->setContextMenu(trayMenu);trayIcon->show();
void Widget::quitMusic()
{//保存歌曲信息musiclist.writeMusicTODB();//关闭数据库sqlite.close();//关闭窗口close();
}void Widget::on_exit_clicked()
{hide();
}

这样当我们再点击程序的关闭按钮后,它会隐藏到系统托盘中:
在这里插入图片描述

2.换肤最大最小化处理

换肤无非是自定义的改变下背景颜色和图片之类的效果,可以作为项目的拓展来完成,这里暂且这样处理:

void Widget::on_min_clicked()
{showMinimized();
}void Widget::on_skin_clicked()
{QMessageBox::information(this, "提示", "换肤功能正在紧急支持中...");
}

3.重复从本地添加音乐bug

当我们打开程序读取数据库中的歌曲后,我们再次从本地添加同样的音乐就会出现重复的情况,这里我们在MusicList添加音乐时做去重处理即可解决:


void MusicList::addMusic(const QList<QUrl> &urls)
{for(auto e: urls){//检测歌曲的MIME类型 过滤有效歌曲文件QMimeDatabase file;QMimeType type = file.mimeTypeForFile(e.toLocalFile());QString ret = type.name();if(ret == "audio/mpeg" || ret == "audio/flac"||ret == "audio/wav"){//构建music对象 并且添加进musiclist管理const Music music(e);//处理读取数据库后再次从本地添加音乐在页面上重复问题(数据库不重复UNIQUEmusicName)if(-1 != musiclist.indexOf(music)){qDebug()<<"已有音乐 请勿重复添加"<<music.getMusicName();}musiclist.push_back(music);}}
}

4.保证程序只运行⼀次

  • 资源管理与优化
  • 数据一致性与完整性
  • 避免功能异常和混乱

因为以上的原因我们通过共享内存来保证只有一个程序实例:


#include <QApplication>
#include <QMessageBox>
#include <QSharedMemory>//共享内存int main(int argc, char *argv[])
{QApplication a(argc, argv);//Music PlayerQSharedMemory sharedMemory("musicPlayer");//如果已经被占用说明有实例在运行if(sharedMemory.attach()){QMessageBox::information(nullptr,"musicPlayer","musicPlayer已经在运行");return 0;}sharedMemory.create(1);Widget w;w.show();return a.exec();
}

七.总结

项目源码与项目打包压缩包都在:码云匿名者

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/35976.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何打造安全稳定的亚马逊采购测评自养号下单系统?

在当今的电商领域&#xff0c;亚马逊作为全球领先的在线购物平台&#xff0c;其商品种类繁多&#xff0c;用户基数庞大&#xff0c;成为了众多商家和消费者的首选。而对于一些需要进行商品测评或市场调研的用户来说&#xff0c;拥有一个稳定、安全的亚马逊账号体系显得尤为重要…

Python文字识别OCR

一.引言 文字识别&#xff0c;也称为光学字符识别&#xff08;Optical Character Recognition, OCR&#xff09;&#xff0c;是一种将不同形式的文档&#xff08;如扫描的纸质文档、PDF文件或数字相机拍摄的图片&#xff09;中的文字转换成可编辑和可搜索的数据的技术。随着技…

如何在 Github 上获得 1000 star?

作为程序员&#xff0c;Github 是第一个绕不开的网站。我们每天都在上面享受着开源带来的便利&#xff0c;我相信很多同学也想自己做一个开源项目&#xff0c;从而获得大家的关注。然而&#xff0c;理想很丰满&#xff0c;现实却是开发了很久的项目仍然无人问津。 最近&#x…

汽车机械钥匙升级一键启动的优点

汽车机械钥匙升级一键启动的优点主要包括&#xff1a; 便捷性&#xff1a;一键启动功能的引入极大地提升了用车便捷性。车主无需翻找钥匙&#xff0c;只需在车辆感应范围内轻触启动键&#xff0c;即可轻松发动汽车。 安全性&#xff1a;移动管家专车专用一键启动系统配备了防…

[QT]深入理解Qt中的信号与槽机制

文章目录 信号与槽1. 信号和槽概述信号的本质槽的本质说明 2. 信号和槽的使用2.1 连接信号和槽2.2 查看内置信号和槽2.3 通过 Qt Creator 生成信号槽代码 3. 自定义信号和槽3.1 基本语法3.2 带参数的信号和槽**示例1&#xff1a;重载信号槽****示例2&#xff1a;信号槽参数列表…

Axure设计之下拉多选框制作教程C(中继器)

利用Axure制作下拉多选器组件可以极大地提升原型制作的效率和效果。以下是基于你提供的详细步骤的详细指导&#xff0c;帮助你在Axure中实现一个功能完善、高保真且可复用的下拉多选器组件。 一、案例预览 预览地址&#xff1a;https://pghy0i.axshare.com 实现效果包括&#…

STC89C52单片机学习——第25节: [11-1]蜂鸣器

写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难&#xff0c;但我还是想去做&#xff01; 本文写于&#xff1a;2025.03.18 51单片机学习——第25节: [11-1]蜂鸣器 前言开发板说明引用解答和科普一、蜂鸣器…

Linux上的`i2c-tools`工具集的详细介绍;并利用它操作IMX6ULL的I2C控制器进而控制芯片AP3216C读取光照值和距离值

IC-Tools 工具集介绍 i2c-tools 是 Linux 下用于 IC 设备调试 的用户空间工具集(你也可以把它看成是一个库&#xff0c;类似于之前自己用过的触摸屏库tslib库、FreeType矢量字符库)&#xff0c;它提供了一系列命令行工具&#xff0c;可以扫描、读取、写入 IC 设备&#xff0c;…

《CircleCI:CircleCI:解锁软件开发持续集成(CI)和持续部署(CD)高效密码》:此文为AI自动生成

《CircleCI&#xff1a;CircleCI&#xff1a;解锁软件开发持续集成&#xff08;CI&#xff09;和持续部署&#xff08;CD&#xff09;高效密码》&#xff1a;此文为AI自动生成 一、CircleCI 初印象 在当今软件开发的快节奏赛道上&#xff0c;持续集成&#xff08;CI&#xff0…

LinuX---Shell脚本创建和执行

概述&#xff1a; 它是一个命令行解释器&#xff0c;接收应用程序/用户命令&#xff0c;然后调用操作系统内核。 Shell还是一个功能强大的编程语言&#xff0c;易编写、易调试、灵活性强。 Linux提供的Shell解析器有 atguiguubuntu:~$ cat /etc/shells # /etc/shells: valid …

再学:Solidity数据类型

目录 1.uint&#xff1a;无符号整型 2.引用类型 3.数组 4.注意gas的消耗 ​编辑 5.映射 1.uint&#xff1a;无符号整型 注意能容纳的最大值和最小值 2.引用类型 值类型赋值 相当于 拷贝 若拷贝开销过大&#xff0c;可以考虑引用类型。 memory&#xff1a;只存在于函数内部…

Docker Desktop配置国内镜像源教程

在使用 Docker 时&#xff0c;由于默认镜像源在国外&#xff0c;经常会遇到下载速度慢、连接超时等问题。本文将详细介绍如何在 Windows 系统中为 Docker 配置国内镜像源&#xff0c;以提升镜像拉取速度。 常用国内镜像源 https://docker.1ms.run清华镜像源 https://docker.m…

C#中SerialPort 的使用

最近在学习C#的SerialPort &#xff0c;关于SerialPort 的使用&#xff0c;做如下总结&#xff1a; 1.可以通过函数System.IO.Ports.SerialPort.GetPortNames() 将获得系统所有的串口名称。C#代码如下&#xff1a; string[] sPorts SerialPort.GetPortNames(); foreach(stri…

深度学习 Deep Learning 第2章 线性代数

深度学习 第2章 线性代数 线性代数是深度学习的语言。 张量操作是神经网络计算的基石&#xff0c;矩阵乘法是前向传播的核心&#xff0c;范数约束模型复杂度&#xff0c;而生成空间理论揭示模型表达能力的本质。 本章介绍线性代数的基本内容&#xff0c;为进一步学习深度学习做…

EDID读取学习

简介 Video BIOS可以被认为是一个具有独立硬件抽象层的操作系统。它不会阻止或监视操作系统、应用程序或设备驱动程序对硬件的直接访问。虽然不推荐,但一些DOS应用程序确实可以改变基本的硬件设置,而根本不需要通过视频BIOS。大多数现代应用程序和操作系统都避免直接使用硬件…

基于SpringBoot的在线拍卖系统

摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统&#xff0c;主要的模块包括管理员&#xff1b;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…

Android audio(8)-native音频服务的启动与协作(audiopolicyservice和audioflinger)

音频策略的构建 1、概述 2、AudiopolicyService 2.1 任务 2.2 启动流程 2.2.1 加载audio_policy.conf&#xff08;xml&#xff09;配置文件 2.2.2 初始化各种音频流对应的音量调节点 2.2.3 加载audio policy硬件抽象库 2.2.4设置输出设备 ps:audiopatch流程简介 2.2.5打开输出设…

Springdoc 全部注解一文解释清楚

文章目录 **1. 核心注解****Tag-Class类上** **2. 方法级别注解****Operation-方法描述****ApiResponse 和 ApiResponses-方法的返回结果** **3. 参数相关注解****Parameter-方法参数****Parameters方法参数&#xff08;单个&#xff09;** **4. 实体模型相关注解****Schema-描…

Git的基本指令

一、回滚 1.git init 在项目文件夹中打开bash生成一个.git的子目录&#xff0c;产生一个仓库 2.git status 查看当前目录下的所有文件的状态 3.git add . 将该目录下的所有文件提交到暂存区 4.git add 文件名 将该目录下的指定文件提交到暂存区 5.git commit -m 备注信…

通过qemu仿真树莓派系统调试IoT固件和程序

通过qemu仿真树莓派系统调试IoT固件和程序 本文将介绍如何使用 QEMU 模拟器在 x86 架构的主机上运行 Raspberry Pi OS&#xff08;树莓派操作系统&#xff09;。我们将从下载镜像、提取内核和设备树文件&#xff0c;到启动模拟环境&#xff0c;并进行一些常见的操作&#xff0…