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()
{
//清除两个布局中的item
QList<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信息插入musicInfo
void 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)
{
//解决重复问题 清除listwidget
ui->pagemusicBox->clear();
//将歌曲uuid添加到musiclistOFPage
addMusicTOMusicPage(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发送的信号setIsLike
connect(Listitembox,&ListitemBox::setIsLike,this,[=](bool isLike){
emit updataLikeMusic(isLike,ret->getMusicUuid());
});
}
// repaint()会⽴即执⾏paintEvent(),不会等待事件队列的处理
// update()将⼀个paintEvent事件添加到事件队列中,等待稍后执⾏,即不会⽴即执⾏paintEvent
repaint();
}
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找到对应的music
QString 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_OBJECT
public:
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;
//获取当前歌曲id
QString 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();
//替换后缀为.lrc
temp.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 Player
QSharedMemory sharedMemory("musicPlayer");
//如果已经被占用说明有实例在运行
if(sharedMemory.attach())
{
QMessageBox::information(nullptr,"musicPlayer","musicPlayer已经在运行");
return 0;
}
sharedMemory.create(1);
Widget w;
w.show();
return a.exec();
}
七.总结
项目源码与项目打包压缩包都在:码云匿名者