一.项目介绍
本篇文章,我们将来介绍一个Qt的项目,仿QQ音乐的简易音乐播放器。
先来看一下QQ音乐的一个界面,包含了诸多的功能:
那么我们自己实现的简易音乐播放器,主要包含以下若干模块:
- 界面设计:包括界面设计、界面美化。控件的设计等。
- 歌曲管理模块:包括歌曲的载入、歌曲信息解析,歌曲分类管理等。
- 歌曲播放模块:包括歌曲的播放、暂停、切换等基本功能,以及歌词的同步功能。
- 数据持久化:实现歌曲的收藏、将歌曲载入最近播放,将用户操作的数据永久保存等。
本项目的开发环境包括:C++、Qt、
下面我们就来逐模块的实现该音乐播放器项目。
二.项目设计
1.窗口界面设计
首先我们需要对整个窗口进行一个区域划分:
整个窗口我们分为两部分:head和body。
在body内部,又将其分为bodyleft和bodyright两部分。
bodyright中则又包含了歌曲的信息显示和控制区域。
因此整个界面我们大致将其分为四个区域。
下面我们开始正式设计界面。
(1)整体界面设计
首先呢,整个页面我们应该设计一个适合的尺寸,这个可以根据实际观感来自行进行修改。
每个区域,我们都应该使用一个QWidget控件来存储其内部的各种子控件。 这样做的好处有两个,一是当窗口的大小需要调整时,此时只需调整整个根节点的QWidget的大小,所有的子QWidget都会跟着等比例的变化;二是方便对各个区域的子控件进行排版。
一开始做时,我们将各个界面的背景颜色进行区分,方便进行辨认。
来看这个界面设计,整个界面使用一个QWidget控件,将其背景设置为暗红色方便我们辨认,随后又使用两个QWidget控件,蓝色代表head,绿色代表body, 它们两个通过垂直布局平分整个界面,创建好控件之后,我们应该及时在右侧修改其对象名称,这样方便我们进行控件的辨认。
但是现在的界面给出一个问题:
1.由于控件具有盒子模型的属性,所以我们创建之后其默认携带了外边距,但是我们并不想要这些外边距,该怎么消除?
这里我们应该选择外层的musicPlayer控件,在其属性页中找到Layout,其中就包含诸多布局边距,我们将所有的边距清0即可,结果如下:
能够看出,所有的边距清0之后,外层的musicPlayer就被完全覆盖。
随后我们调整两个控件的大小:
通过修改控件的最大最小限制尺寸,来修改并固定其大小。
(2)head设计
通过观察,我们发现head实际上也能分为两部分,左侧我们设计成整个音乐播放器的图标,右侧则包含搜索框,以及窗口的控制。
因此我们需要继续在head中添加两个QWidget。
左侧为音乐播放器的logo,右侧则为搜索框和按钮区,这里我们推荐将搜索框和控制区也分别放在单独的QWidget中,所以我们继续创建新的QWidget。
其中按钮区域我们设计了换肤、最小化、最大化、关闭四个按钮,值得注意的是,由于按钮区的区域过大,如果直接将四个按钮按照水平布局排布,那么四个按钮就会位于区域的中心,但是我们希望他们排布在区域的最右侧,因此加入一个垫片控件来占领左侧区间,这样在使用水平排布,四个按钮就会被压缩在最右侧区域。
(3)body设计
body区分为左右两个部分,其中左侧部分的主要功能是包含一些按钮,点击不同的按钮,右侧区域会跟随进行变换。
由于body左侧区域我们也用不完全,所以同样创建一个子QWidget,并添加一个垫片。
那么在左侧控制区,我们期望拥有两部分功能:
- 在线音乐:即播放器推荐的音乐,包括每日推荐、电台、音乐馆。
- 我的音乐:包括我喜欢、本地音乐、最近播放。
因此我们需要再添加两个子QWidget。
在QQ音乐中,当我们点击这些按钮时都会产生高亮表示,因此功能按钮我们需要自定义设计,所以这里我们先用QWidget来占位表示。
对于body的右侧部分,我们能够看出,当我们点击body左侧的功能按钮时,右侧区域下放的播放显示和控制区域不变,变的只有上半部分,因此这部分需要使用层叠窗口。
其中进度条我们也希望自己实现一个,所以这里也使用一个QWidget来存放进度条。
下面来看音乐控制区域的设计。
根据例子,我们期望将该区域分为三个部分,分别是歌曲的信息部分,播放控制部分、时间显示及歌词显示部分。
到此为止,我们已经完成了整体界面的设计,来看成果:
由于我当前的这个窗口比例较小,所以有些内容不能完全显示。
此时我们发现了一个问题,我们运行程序得到的窗口,是自带标题栏的,这样就导致我们这个窗口,它拥有两个标题栏。这是不合常理的,所以我们期望将其自带的标题栏给去除,就有用到以下方法:
void musicPlayer::initUi()
{
this->setWindowFlags(Qt::FramelessWindowHint);//去除标题栏
}
结果如下:
能够发现标题栏没了,但是由于我们还没有实现窗口的关闭功能,所以无法直接在窗口上点击关闭,只能通过电脑下方的任务栏关闭;同时窗口也不能被拖拽移动了。
实现了基本的界面布局之后,我们开始一步步的实现其功能。
(4)添加窗口阴影
我们希望界面窗口边界具有下述图片的这种阴影效果,该怎么实现呢?
需要用到类 #include <QGraphicsDropShadowEffect>
//设置窗口背景透明
this->setAttribute(Qt::WA_TranslucentBackground);
//给窗口设置阴影效果
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0,0);//设置阴影区域相较于窗口界面的距离
shadowEffect->setColor("#000000");//设置阴影颜色为黑色
shadowEffect->setBlurRadius(10);//设置阴影的宽度
this->setGraphicsEffect(shadowEffect);
想要窗口界面具有阴影效果,就必须将窗口的背景透明化,结果如下:
(5)head美化设计
下面我们对head部分进行美化设计。
首先是logo设计,载入logo图片,需要我们创建qrc文件并导入图片:
通过代码或样式表将图片载入logo控件中,我们更推荐使用样式表的方式,这样能够使我们的代码更加整洁:
#logo
{
background-image:url(:/images/musiclogo.png);
border-radius:0px; /*设置按钮的边框圆⻆为 0 像素,
实现直⻆边缘*/
background-repeat:no-repeat; /* 背景图⽚不重复平铺
*/
border: none; /*⽆边框*/
background-position:center center; /*背景图⽚放置在
按钮的中⼼位置*/
}
颜色的设计,就需要我们自己去寻找合适的颜色,这里是我选择的颜色:
#headLeft
{
background-color:#F0F0F0;/*设置背景颜⾊为浅灰⾊*/
}
#headRight
{
background-color:#F5F5F5; /*设置背景颜⾊为亮灰⾊*/
}
搜索框的设计,我们希望改变一下搜索框的背景颜色,以及将搜索框的四个角改成圆弧状,此时我们可以去设置搜索框的样式表:
#search
{
background-color: #E3E3E3; /*设置背景颜⾊*/
border-radius:17px; /*设置四个⻆的圆⻆*/
padding-left: 17px; /*内部⽂字到边的距离*/
}
最后是最右边四个按钮的图片替换:
采用样式表的方式:
QPushButton
{
border-radius:0px; /*设置按钮的边框圆⻆为 0 像素,实现直⻆边缘*/
background-repeat:no-repeat; /* 背景图⽚不重复平铺*/
border: none; /*⽆边框*/
background-position:center center; /*背景图⽚放置在按钮的中⼼位置*/
}
/*悬停状态*/
QPushButton:hover
{
background-color:rgb(255, 109, 80);/*当鼠标悬停在按钮上时改变颜色*/
}
当同一个QWidget中的若干个相同的控件具有相同的样式时,我们可以将其集中在父类QWidget的样式表中处理,随后在各自单独的样式表中设置不同的样式。
background-image: url(:/images/skin.png);
background-image: url(:/images/max.png);
background-image: url(:/images/min.png);
background-image: url(:/images/quit.png);
如此一来我们就对head完成了界面美化,结果如下:
body界面的样式设置方式与上述完全类似,这里就不过多分享了。
(5)功能按钮自定义
像body左侧这样的按钮,它即是一个按钮,同时其中又包含图片和文字等其他控件,Qt自带的按钮肯定没办法实现这样的效果,所以我们就需要额外创建新的Qt界面来实现:
将其命名为BtForm,即按键界面,随后我们开始对新的界面进行设计:
其内部结构如右图所示,其中lineBox是为了设计出音符跳动的效果。
设计完BtFrom的界面之后,我们回到原主界面当中, 将预存的功能按钮的QWidget类提升为BTForm类:
将所有的六个功能按钮都进行类提升之后,运行项目结果如下:
随后我们可以再对这些按钮添加悬停变色样式:
那么将这些样式定义出来之后,他们的样式都是一样的,我们该如何修改它们的图标和文本信息呢???
这个就需要通过代码来实现了,因为它们都是一个一个的BtForm对象,所以我们可以构建一个函数,让不同的对象去调用,从而设置自己的图标和文本信息:
//设置功能按钮的图标和文本信息
void BtForm::setIconAndText(const QString &icon, const QString &text)
{
//设置功能按钮图标
ui->btIcon->setPixmap(QPixmap(icon));
//设置功能按钮文本
ui->btText->setText(text);
}
在BtFrom类中定义函数,随后在主界面类的 initUi() 函数中调用:
//设置功能按钮的图标和文本信息
ui->rec->setIconAndText(":/images/rec.png","推荐");
ui->radio->setIconAndText(":/images/radio.png","电台");
ui->music->setIconAndText(":/images/music.png","音乐馆");
ui->like->setIconAndText(":/images/like.png","我喜欢");
ui->local->setIconAndText(":/images/local.png","本地下载");
ui->recent->setIconAndText(":/images/recent.png","最近播放");
结果如下:
随后,我们希望当鼠标点击这些功能按钮时,按钮的背景颜色能够发生变化,这就需要用到鼠标点击事件了:
void BtForm::mousePressEvent(QMouseEvent *event)
{
(void)event;
//鼠标点击时,改变背景颜色
ui->btStyle->setStyleSheet("#btStyle{background-color:rgb(30,206,154);}");
}
结果如下:
我们发现,这个结果也并非所愿,因为理想中的结果是一个按钮点击之后,另一个按钮的背景应该回退,这一部分的处理,我们放在按钮和 bodyRight 堆叠页面的切换关联中。
那么我们该如何处理功能按钮和堆叠页面的切换关联呢???
要知道,。只有当按钮被点击,页面才会切换,所以还是要放在按钮点击事件中处理。
在堆叠页面中,每个页面都有自己的索引数字,那么我们就可以通过这个数字来建立关联:当按钮被点击时,向musicPlayer发送一个信号,信号的内容是页面的索引数字,随后让musicPlayer来进行页面切换:
我们在BtForm中新建一个变量用来记录索引数字,随后在我们设置功能按钮的图标和文本信息时,就获取到对应的pageId:
建立btClick信号,当按钮被点击时,就发送出去:
void BtForm::mousePressEvent(QMouseEvent *event)
{
(void)event;
//鼠标点击时,改变背景颜色
ui->btStyle->setStyleSheet("#btStyle{background-color:rgb(30,206,154);}");
//发送按钮点击信号,将pageId传给musicPlayer
emit btClick(pageId);
}
在musicPlayer中创建一个函数来统一处理所以得信号槽连接:
void musicPlayer::connectSignalAndSlots()
{
//关联功能按钮和页面切换
connect(ui->rec,&BtForm::btClick,this,&musicPlayer::onBtFromClick);
connect(ui->radio,&BtForm::btClick,this,&musicPlayer::onBtFromClick);
connect(ui->music,&BtForm::btClick,this,&musicPlayer::onBtFromClick);
connect(ui->like,&BtForm::btClick,this,&musicPlayer::onBtFromClick);
connect(ui->local,&BtForm::btClick,this,&musicPlayer::onBtFromClick);
connect(ui->recent,&BtForm::btClick,this,&musicPlayer::onBtFromClick);
}
创建信号处理函数来切换页面:
void musicPlayer::onBtFromClick(int pageId)
{
ui->stackedWidget->setCurrentIndex(pageId);//切换page页面
}
下面来处理如何取消未选中的按钮的高亮背景,要知道这个取消背景的事件,是和切换页面共同进行的,所以我们将这个操作同样定义在上述槽函数中:
void musicPlayer::onBtFromClick(int pageId)
{
ui->stackedWidget->setCurrentIndex(pageId);//切换page页面
//切换页面的同时去除其他功能按钮的高亮背景
//1.获取所有的功能按钮,放入列表当中
QList<BtForm*> btFromList = this->findChildren<BtForm*>();
//2.将所有pageId与点击按钮不同的按钮的高亮背景去除
for(auto btFrom : btFromList)
{
if(btFrom->getPageId() != pageId)
{
btFrom->clearBackground();
}
}
}
取消高亮背景,我们必须获取到所有的BtFrom对象,然后将他们存放在链表中,然后遍历链表去判断它们的索引pageId是否与当前点击的pageId相同,如果不相同,就要取消它们的高亮背景:
void BtForm::clearBackground()
{
ui->btStyle->setStyleSheet("#btStyle:hover{ background:#d8d8d8;}");
}
结果如下,此时切换功能按钮,就会消除原本的按钮的高亮背景啦。
下面我们来看,如何给功能按钮的最右侧设置动画效果。
Qt中QPropertyAnimation类可以提供简单的动画效果,允许对QObject获取派生类的可读写属性进行动画处理,创建平滑、连续的动画效果,比如控件的位置、大小、颜色等属性变化,使用时需包含 <QPropertyAnimation>。
下面是该类中提供的若干个可以设置动画效果的关键函数:
功能:实例化QPropertyAnimation类对象
参数:
target: 给target设置动画效果
propertyName:动画如何变化,⽐如:geometry,让target以矩形的⽅式滑动
parent:该动画实例的父对象,即将该对象加到对象树中
QPropertyAnimation(QObject *target, const QByteArray &propertyName, QObject *parent = nullptr);
功能: 设置动画持续的时⻓
参数: 单位为毫秒
void setDuration(int msecs);
功能:根据value创建关键帧
参数:
step:值再0~1之间,0表⽰开始,1表⽰停⽌
value:动画的⼀个关键帧,即动画现在的形态,假设是基于geometry,可以设置矩形的范
void setKeyValueAt(qreal step, const QVariant &value);
功能:设置动画的循环次数
参数:
loopCount:默认值是1,表⽰动画执⾏1次,如果是-1,表⽰⽆限循环
void setLoopCount(int loopCount);
槽函数:
void pause(); // 暂停动画
void start(QAbstractAnimation::DeletionPolicy policy = KeepWhenStopped); // 开
启动画
void stop(); // 停⽌动画
一般情况下,音符的跳动是要根据歌曲的播放来进行的,当对应功能界面中的歌曲播放时,对应的音符进行跳动,但是现阶段我们还没有实现歌曲模块的内容,所以先将音符跳动处理在界面的构造函数中。
//控制音符跳动
QPropertyAnimation* line1Animation;
QPropertyAnimation* line2Animation;
QPropertyAnimation* line3Animation;
QPropertyAnimation* line4Animation;
由于我们总共有四个QLable来代表音符,所以需要在BtFrom中实例出四个动画对象。
//实现音符跳动
line1Animation = new QPropertyAnimation(ui->line1,"geometry",this);//实例动画对象为矩形方式滑动
line1Animation->setDuration(1500);//设置动画持续时间
//设置关键帧,即矩形在整个过程中的某一帧所处的位置
line1Animation->setKeyValueAt(0,QRect(0,20,3,0));//开始
line1Animation->setKeyValueAt(0.5,QRect(0,0,3,20));//中间
line1Animation->setKeyValueAt(1,QRect(0,20,3,0));//结束
line1Animation->setLoopCount(-1);//设置动画无限循环
line1Animation->start();//开始动画
注意,设置关键帧时,我们要传入矩形的左上角坐标以及矩形的宽度和高度,可以参考控件的属性:
这里我们只给出了第一个小矩形的动画效果,后续矩形可以完全沿用上述代码,值得注意的是,后续矩形的动画持续时间要做区别,以保证它们的动画参差不齐。
结果如下:能够看出四个小矩形在参差不齐的跳动。
此外还有一个小问题,那就是所有的动画都在跳动,但是我们希望的是只有正在使用的功能按钮的音符会跳动,其他的都要被隐藏起来,这个其实很好实现:
- 在构造函数中先将所有的动画隐藏。
- 定义两个函数,一个用来显示动画,一个用来隐藏动画。
- 跟高亮背景的设置相似,当鼠标点击时,调用函数显示动画,同时取消其他按钮的动画。
void BtForm::showAnimation()
{
ui->lineBox->show();
}
void BtForm::hideAnimation()
{
ui->lineBox->hide();
}
效果如下:
(6)在线音乐页面设计
在QQ音乐中,推荐、电台、音乐馆三个界面是类似的,所以这里我们以推荐为例来实现。
在QQ音乐中,推荐页面包含诸多内容,有显示文本的QLabel控件、当鼠标悬停在歌曲图片上时的跳动效果、左右两侧的箭头点击能够实现歌曲更换,以及页面显示不全能够使用鼠标滑轮下滑。
很显然,除了显示文本信息的QLabel外,其他内容都是无法通过Qt自带的控件来实现的,所以我们需要自定义控件来完成,思路类似于BtFrom。
在推荐页面中添加可以滑动的页面Scroll Area,再在滑动页面中加入文本信息和准备被提升的两个QWidget。
下面来设计推荐页面歌曲的自定义控件,首先创建新的类名为RecBox的设计师页面,将其ui界面设计成如下效果:
对左右两侧的按钮进行美化处理,最后将musicPlayer中的两个musicBox进行类提升:
结果如下:
能够看出,当我们提升的界面大小超过可滑动界面的
//添加recItem
RecBoxItem* item = new RecBoxItem();
ui->recListUpHLayout->addWidget(item);
原始大小时,就会自动生成下拉条。
下面我们需要设计推荐中显示歌曲图片,歌曲名字以及当鼠标放在图片上时,会进行跳动的动画效果。很显然,要实现这些内容,依然需要创建自定义控件。
这里我们将按钮的颜色设置为绿色,实际上按钮的颜色需要隐藏,因为图片需要能够覆盖按钮。
由于我们没有在RecBox的推荐列表中添加QWidget,所以这里就不能通过类提升来将图片载入进去了,而是需要在其构造函数中创建对象来载入:
//添加recItem
RecBoxItem* item = new RecBoxItem();
ui->recListUpHLayout->addWidget(item);
结果如下:
下面我们来为图片设置调动的动画,想要设置动画,我们前边已经分享过了,可以继续沿用音符的例子,那么我们要做的,就是设置当鼠标移动到按钮上时,图片会上移,离开按钮,会复原,那么就需要我们对鼠标移入移出两个事件做处理:
首先我们需要在构造函数中安装一个事件拦截器,通过该拦截器来拦截事件:
//安装拦截器
ui->musicImageBox->installEventFilter(this);
随后使用父类自带的事件拦截函数来拦截,其中watched参数表示要将事件关注在那个控件上。
bool RecBoxItem::eventFilter(QObject *watched, QEvent *event)
{
//当事件发生在按钮上时进行拦截
if(watched == ui->musicImageBox)
{
if(event->type() == QEvent::Enter)//鼠标进入
{
//实现图片跳动
QPropertyAnimation* animation = new QPropertyAnimation(ui->musicImageBox,"geometry");//实例动画对象
animation->setDuration(150);//设置动画持续时间
//设置关键帧,即动画在整个过程中的某一帧所处的位置
animation->setStartValue(QRect(9,9,132,150));//开始
animation->setEndValue(QRect(9,0,132,150));//结束
animation->start();//开始动画
//销毁动画对象
connect(animation,&QPropertyAnimation::finished,this,[=]()
{
delete animation;
});
return true;
}
else if(event->type() == QEvent::Leave)//鼠标离开
{
//实现图片跳动
QPropertyAnimation* animation = new QPropertyAnimation(ui->musicImageBox,"geometry");//实例动画对象
animation->setDuration(150);//设置动画持续时间
//设置关键帧,即动画在整个过程中的某一帧所处的位置
animation->setStartValue(QRect(9,0,132,150));//开始
animation->setEndValue(QRect(9,9,132,150));//结束
animation->start();//开始动画
//销毁动画对象
connect(animation,&QPropertyAnimation::finished,this,[=]()
{
delete animation;
});
return true;
}
}
//不关心的事件交给父类处理
return QObject::eventFilter(watched,event);
}
由于图片的动画不是持续存在于程序的整个生命周期,所以我们不能将它挂在对象树上,且必须在其结束时清除该对象。
下面我们来为空白的RecMusicBox设置歌曲的图片和歌曲名。
图片和名称的设置通常是由外部类来确定和设计的,所以我们在RecMusicBox中提供设置图片和名称的函数:
void RecBoxItem::setRecText(const QString &text)
{
ui->recBoxItemText->setText(text);
}
void RecBoxItem::setRecImage(const QString &imagepath)
{
QString style = "background-image:url(" + imagepath + ");";
ui->recMusicImage->setStyleSheet(style);
}
我们希望,在程序打开时,就载入图片,所以我们将这个工作,交给主类MusicPlayer。
创建一个处理所有歌曲图片的函数:
QJsonArray musicPlayer::randomPiction()
{
//将所有的文件路径存入vector中
QVector<QString> imagePathVec;
imagePathVec << "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(imagePathVec.begin(),imagePathVec.end());
//使用键值对将歌曲的路径和名字关联
QJsonArray objArray;//存储键值对
for(int i = 0;i < imagePathVec.size();i++)
{
QJsonObject obj;
obj.insert("path","/images/rec/" + imagePathVec[i]);
QString strText = QString("推荐-%1").arg(i,3,10,QChar('0'));//创建名字格式
obj.insert("text",strText);
objArray.append(obj);
}
return objArray;
}
先将所有的歌曲路径保存在vector中并打乱,通过Qt中的构建键值对的类QJsonObject,将歌曲的路径及文本名称构建成键值对,将每一个键值对都加入到键值对数组QJsonArray中,最后返回该数组。 Qt的这个键值对类QJsonObject,可以同时保存多个键值对,再将整个对象加入数组中。
随后,我们需要在MusicPlayer的初始化函数中区调用两个推荐界面的初始化函数:
我们期望与上例中的情况保持一个,一个只有一行推荐,一个有两行推荐。
//添加RecMusicBox图片
ui->recMusicBox->initRecBoxUi(randomPiction(),1);
ui->supplyMusicBox->initRecBoxUi(randomPiction(),2);
将所有的歌曲信息以及要推荐的行数传入。
在RecBox类中添加变量来保存行数,列数以及图片信息:
在RecBox类中创建初始化UI界面的函数:
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
if(row == 2)//UI界面显示两行图片
{
this->row = row;
col = 8;
}
else//UI界面显示一行图片
{
ui->recListDown->hide();//将另一个列表隐藏
}
imageList = data;
//添加图片
createRecItem();
}
判断出要设置的行数之后,最终调用createRecItem函数来完成歌曲图片和文本的添加:
void RecBox::createRecItem()
{
for(int i = 0;i < col;i++)
{
//创建音乐item对象
RecBoxItem* item = new RecBoxItem();
//获取键值对信息
QJsonObject obj = imageList[i].toObject();
item->setRecText(obj.value("text").toString());
item->setRecImage(obj.value("path").toString());
//将item对象添加
ui->recListUpHLayout->addWidget(item);
}
}
结果如下:
但是我们发现,下边音乐补给站的图片并没有分成两行,这个该如何处理呢?
其实很简单,只需要增加一个判断:
if(i >= col / 2 && row == 2)
{
ui->recListDownHLayout->addWidget(item);
}
else
{
ui->recListUpHLayout->addWidget(item);
}
如果要添加的是两行,且上半部分已经添加了四个音乐图片,那么就可以转而向下半部分添加了,结果如下:
接下来,我们要处理的就是左右两边的按钮,使它们能够实现图片轮番显示的效果。
如果要轮番显示图片,我们就必须将所有的图片进行分组,所以我们需要在RecBox类中添加新的变量:
int currentIndex; //记录图片分组的下标
int count;//记录图片的组数
并在initRecBoxUi函数中对齐进行初始化:
currentIndex = 0;
count = imageList.size() / col;
那么在两个按钮的槽函数中,我们需要做的就是改变显示图片的组数,然后重新调用函数来载入图片:
void RecBox::on_btUp_clicked()
{
if(--currentIndex < 0)
currentIndex = count - 1;
createRecItem();
}
void RecBox::on_btDown_clicked()
{
if(++currentIndex >= count)
currentIndex = 0;
createRecItem();
}
void RecBox::createRecItem()
{
//去除原本的内容
QList<RecBoxItem*> recUpList = ui->recListUp->findChildren<RecBoxItem*>();
for(auto e : recUpList)
{
ui->recListUpHLayout->removeWidget(e);
delete e;
}
QList<RecBoxItem*> recDownList = ui->recListDown->findChildren<RecBoxItem*>();
for(auto e : recDownList)
{
ui->recListDownHLayout->removeWidget(e);
delete e;
}
int index = 0;
for(int i = col * currentIndex ;i < col + col * currentIndex;i++)
{
//创建音乐item对象
RecBoxItem* item = new RecBoxItem();
//获取键值对信息
QJsonObject obj = imageList[i].toObject();
item->setRecText(obj.value("text").toString());
item->setRecImage(obj.value("path").toString());
//将item对象添加
if(index++ >= col / 2 && row == 2)
ui->recListDownHLayout->addWidget(item);
else
ui->recListUpHLayout->addWidget(item);
}
}
我们必须先把原本拥有的图片对象获取到并去除,随后通过图片在列表中的下标与自己所在组数的映射关系, 循环载入新的组的图片,结果如下:
此外我们会发现,我们的程序每次运行时,推荐的图片都是一样的,这样就没有了随机性,我们希望每次新打开程序时,都会推荐不同的内容,这是为什么呢???当初我们在载入图片的时候,明明有打乱顺序:
//打乱图片的路径序列
std::random_shuffle(imagePathVec.begin(),imagePathVec.end());
这是因为,我们没有为其设置随机数种子,这样每次程序运行,函数都只会使用其默认的随机数种子,导致其每次打乱的顺序都是一样的,所以我们需要在初始化函数中设置一个随机数种子:
//设置随机数种子
srand(time(NULL));
到此,推荐页面我们基本设计完毕,随后我们还可以设计,当点击图片后边隐藏的按钮后,可以播放对应的音乐,在这里,我们就先设计到此。
(7)我的音乐页面设计
能够看出,在QQ音乐中,我喜欢、最近播放是类似的,界面基本都是相同的,所以我们计划将我的音乐中的三个功能界面同样设计成类似的,以我喜欢为例。
新建一个名为commonPage的设计师界面,即公共界面,将其设置为如下布局:
设计完成之后,在主界面中将我的音乐中的三个堆叠页面提升为该类型:
结果如下:
我们将“播放全部”按钮的样式修改一下,使得当鼠标悬停时其具有高亮显示:
下面我们来设置三个界面的文本标题和图片,这个步骤要由主界面初始化来完成,因此我们需在CommonPage类中提供函数:
void CommonPage::setTextAndImageUi(const QString &text, const QString &imagepath)
{
ui->pageTittle->setText(text);
ui->musicImageLable->setPixmap(QPixmap(imagepath));
ui->musicImageLable->setScaledContents(true);//图片能够按钮控件尺寸完全填充
}
随后在主界面初始化时进行调用:
//初始化我的音乐功能界面的文本和图片信息
ui->likePage->setTextAndImageUi("我喜欢",":/images/ilikebg.png");
ui->localPage->setTextAndImageUi("本地下载",":/images/cyx.png");
ui->recentPage->setTextAndImageUi("最近播放",":/images/recentbg.png");
结果如下:
这里临时借用了三张图片来设置,后续该图片应该根据最后一次播放过的歌曲的图片来设置。
下面我们来设计列表框中音乐信息的显示:
新建一个名为ListBoxItem的设计师界面,将其设计为如下样式:
下面我们将该歌曲界面载入列表框中:
//将歌曲UI载入列表中
ListBoxItem* listItemBox = new ListBoxItem(this);//创建歌曲ui对象
QListWidgetItem* listWidgetItem = new QListWidgetItem(ui->pageMusicList);//创建列表框单个窗口对象
listWidgetItem->setSizeHint(QSize(listItemBox->width(),listItemBox->height()));//设置窗口的大小
ui->pageMusicList->setItemWidget(listWidgetItem, listItemBox);//将歌曲ui载入窗口对象最终载入列表框中
注意,这里不能直接将ListBoxItem对象直接载入列表框中,而是必须通过QListWidgetItem对象作为一个载体。结果如下:
能够看出当我们鼠标进入歌曲item时, 其背景颜色就会发生变化,但是我们希望换一种更合适的颜色,我们希望当鼠标悬停时,整个歌曲item都会变色,所以我们通过鼠标进入离开两个事件,来修改其背景颜色:
void ListBoxItem::enterEvent(QEvent *event)
{
(void)event;
setStyleSheet("background-color:#EFEFEF");
}
void ListBoxItem::leaveEvent(QEvent *event)
{
(void)event;
setStyleSheet("");
}
结果如下:
(8)音乐进度条设计
虽然Qt自带的也有进度条控件,但是其样式并不是很好看,所以我们希望自定义一个进度条控件,将其进行美化处理,这需要我们新创建一个名为MusicSlider的设计师界面。
那么我们的进度条该如何实现呢???
其实这个非常简单,我们知道,进度条都是有一浅一深两种颜色的,浅色表示进度条的总长度,深色表示当前的进度,所以我们可以构建两个重叠的QFrame矩形框,底层的矩形框颜色较浅,宽度固定,顶层的矩形框颜色较深,其宽度初始为0,随后会随时间不短变宽,这样我们就模拟出了进度条的效果:
初始情况下设置了一下outLine的宽度,方便我们观察,随后将MusicPlayer中的processBar提升为该类 ,结果如下:
(9)音量调节设计
我们希望,点击界面上的音量图标,会给我们弹出音量控制的界面,其中包含音量调节的进度条,音量百分比显示以及静音的控制按钮,很显然,这样一个界面也是需要我们自己来设计的,所以新建一个名为VolumeTool的设计师界面,将界面设计为如下样式:
其中最下边的空间,我们用来设置用来表示该界面归属的一个小三角符号。
下面我们将该界面设置为弹出界面,即只有当我们点击音量按钮时,才显示该界面,当我们点击其他地方时,该界面即消失。
//设置为弹出界面
setWindowFlags(Qt::Popup);
首先需要在VolumeTool类的构造函数中,设置界面为弹出界面的属性。
//音量调节界面成员变量
VolumeTool* volumeTool;
在MusicPlayer类中添加音量调节界面的成员变量,在初始化函数中完成对象创建:
//创建音量界面对象
volumeTool = new VolumeTool(this);
随后构建槽函数,点击时显示音量调节界面:
void musicPlayer::on_volume_clicked()
{
volumeTool->show();
}
结果如下:
能够发现,这个界面竟然显示在了我的电脑屏幕的最左上角,但是我们希望的是该界面能显示在音量按钮的正上方,该如何调整呢???
要这样做,我们就要移动其位置,要移动位置,就必须得到要移动到的位置的坐标,因此,我们需要得到音量按钮的坐标,然后通过界面的大小计算出其应该显示的位置坐标。
此时,我们还会面临一个问题,音量调节界面的坐标是相对于屏幕来计算的,但是音量按钮的坐标则是相对于程序界面的,所以我们需要把音量按钮的坐标变为相对于屏幕的坐标,再来计算出界面应该所处的坐标。
void musicPlayer::on_volume_clicked()
{
//获取音量按钮相对于屏幕左上角的坐标
QPoint point = ui->volume->mapToGlobal(QPoint(0,0));
//计算得出界面应在的左上角坐标
QPoint volumeLeftTop = point - QPoint(volumeTool->width()/2, volumeTool->height());
//移动界面的位置
volumeTool->move(volumeLeftTop);
//显示界面
volumeTool->show();
}
mapToGlobal函数能够将界面坐标计算为相对于屏幕某一位置的坐标,这里设置为(0,0)即屏幕的左上角,而界面应在的左上角的坐标,即按钮的坐标的高 - 界面的高; 按钮的坐标的宽 - 界面的宽的一半,随后移动界面位置,结果如下
但是此时我们发现,界面貌似相较于按钮有些偏左了,这是因为我们是按照按钮的左上角坐标去计算的界面的坐标,实际应该是使用按钮的中心的最上方的坐标,但是这个也好处理,只需让最终的坐标的横坐标再 + 按钮宽度的一半即可:
volumeLeftTop.rx() += ui->volume->width() / 2;
此时我们又会发现,整个界面除了我们摆放的控件之外,还有很多留白部分,我们希望将这些留白设置成透明效果,同时给这个界面设置一个和整个界面相同的边界阴影效果,这样方便我们判断整个界面的窗口:
//设置为弹出界面
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint| Qt::NoDropShadowWindowHint);
// 在windows上,设置透明效果后,窗⼝需要加上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);
结果如下:
下面我们来设置一下静音的图标,以及默认音量大小为20%。
//设置静音按钮图标
ui->silenceBtn->setIcon(QIcon(":/images/volume.png"));
//设置默认音量为20%
ui->volumeRatio->setText("20%");
//修改outLine矩形的高度
ui->outLine->setGeometry(ui->outLine->x(),ui->outLine->height() - ui->outLine->height() * 0.2 + ui->outLine->y(),
ui->outLine->width(),ui->outLine->height() * 0.2);
//修改按钮位置
ui->sliderBtn->move(ui->sliderBtn->x(),ui->outLine->y() - ui->sliderBtn->height() / 2);
在VolumeTool的构造函数中添加如上函数来设置。,结果如下:
下面我们来添加一下音量显示界面下方的小三角角标:
在Qt自带的控件中,并没有形似三角形的,所以我们只能自己来设计,此处通过绘图API来实现。
在VolumeTool类中创建绘图事件函数,这样当音量调节界面显示时,就会触发该事件,绘制出三角形。
void VolumeTool::paintEvent(QPaintEvent *event)
{
(void)event;
//创建画图对象
QPainter painter(this);
//设置实线画笔
painter.setPen(Qt::NoPen);
//设置白色画刷
painter.setBrush(Qt::white);
//创建三角形
QPolygon polygon;
//设置三角形的位置坐标
polygon.append(QPoint(30,300));
polygon.append(QPoint(70,300));
polygon.append(QPoint(50,320));
//绘制三角形
painter.drawPolygon(polygon);
}
结果如下:
(10) 歌词界面设计
在QQ音乐的歌词界面,包含很多内容,但是我们仅希望,将红框圈中的内容给展现出来。
结果如下,其中歌词部分是使用七个QLabel来显示:
在构造函数中隐藏该界面的标题栏,同时设置左上角隐藏按钮的图标:
//隐藏窗口标题栏
setWindowFlag(Qt::FramelessWindowHint);
ui->hideBtn->setIcon(QIcon(":/images/downPage.png"));
同时为隐藏按钮设置槽函数:
void LrcPage::on_hideBtn_clicked()
{
this->hide();
}
在MusicPlayer类中定义歌词界面对象,并在initUi函数中初始化:
//创建歌词显示界面,默认隐藏
lrcPage = new LrcPage(this);
lrcPage->hide();
为右下角的歌词显示按钮构造槽函数,显示歌词界面:
void musicPlayer::on_lrcWord_clicked()
{
lrcPage->show();
}
结果如下:
能够看出,这个界面虽然能显示,但是没有完全对照我们主界面的大小,同时它的显示也不是那种动画的效果,下面我们来解决这些问题。
首先是界面没有对齐的情况,我们可以通过轻幅度的设置歌词界面的位置来解决:
lrcPage->setGeometry(10,10,lrcPage->width(),lrcPage->height());
结果如下:
然后就是设置简单的动画效果,我们希望歌词界面显示时,能够从下到上逐渐上移显现,隐藏时则是相反的从上到下逐渐隐藏。由于两个歌词界面的操作按钮分别处于不同类中,所以必须分别在不同的类中创建动画对象来实现:
在MusicPlayer类中:
//歌词界面动画对象
QPropertyAnimation* pageAnimation;
在initUi函数中:
//初始化歌词动画对象并设置动画效果
pageAnimation = new QPropertyAnimation(lrcPage,"geometry",this);//实例动画对象
pageAnimation->setDuration(250);//设置动画持续时间
//设置关键帧,即动画在整个过程中的某一帧所处的位置
pageAnimation->setStartValue(QRect(10,10 + lrcPage->height(),lrcPage->width(),lrcPage->height()));//开始
pageAnimation->setEndValue(QRect(10,10,lrcPage->width(),lrcPage->height()));//结束
最后在槽函数中启动动画:
void musicPlayer::on_lrcWord_clicked()
{
lrcPage->show();
pageAnimation->start();
}
在lrcPage类中:
//创建歌词界面显示动画对象
QPropertyAnimation* animation;
在构造函数中:
animation = new QPropertyAnimation(this,"geometry",this);
animation->setDuration(250);//设置动画持续时间
//设置关键帧,即动画在整个过程中的某一帧所处的位置
animation->setStartValue(QRect(10,10,width(),height()));//开始
animation->setEndValue(QRect(10,10 + height(),width(),height()));//结束
ui->hideBtn->setIcon(QIcon(":/images/downPage.png"));
最后在槽函数中启动动画并隐藏界面:
void LrcPage::on_hideBtn_clicked()
{
animation->start();
//动画结束时,隐藏界面
connect(animation,&QPropertyAnimation::finished,this,[=](){
this->hide();
});
}
值得注意的是,在隐藏界面时,必须要在动画结束之后,否则动画效果无法展现。
结果如下:
2.功能设计
(1)窗口关闭
窗口关闭非常简单,只需要设计一个槽函数,调用close()方法即可:
void musicPlayer::on_quit_clicked()
{
close();
}
(2)窗口移动
我们希望实现窗口能够随着鼠标的拖拽而能够移动,那么该如何实现呢?
来看这个图片,外边框表示电脑的屏幕,内边框表示我们音乐播放器界面,红点表示鼠标点击的位置, 我们要得到界面左上角相对于鼠标点击位置的相差距离。该距离计算方法如下:
相差距离 = 鼠标相对于屏幕左上角的坐标 - 界面左上角的坐标
计算出这个距离之后,我们交换一下运算数,那么就能得到:
界面左上角的坐标 = 鼠标相对于屏幕左上角的坐标 - 相差距离
接下来我们就通过鼠标点击事件和鼠标移动事件来实现窗口移动:
protected:
//重写鼠标单击和鼠标移动事件
void mouseMoveEvent(QMouseEvent* event);
void mousePressEvent(QMouseEvent* event);
//记录光标相对于界面左上角的位置
QPoint dragPosition;
虚函数在父类中是保护成员,所以重写时也要进行保护。
void musicPlayer::mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() == Qt::LeftButton)
{
move(event->globalPos() - dragPosition);//移动界面位置
return;
}
QWidget::mouseMoveEvent(event);
}
void musicPlayer::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
dragPosition = event->globalPos() - geometry().topLeft();//得到相对位置
return;
}
QWidget::mousePressEvent(event);
}
重写这两个虚函数其中 event->globalPos() 表示鼠标相对于屏幕左上角的坐标,geometry().topLeft() 表示界面左上角的坐标。
在上述鼠标点击和鼠标移动事件中,我们只处理窗口的移动,其他使用到鼠标点击和鼠标移动事件的情况,我们都交给父类的事件去处理。
(3)添加本地音乐
当我们点击如下的 + 按钮时,我们希望打开文件对话框来添加音乐,所以需要为该按钮设置点击槽函数,随后打开文件对话框,并设置对话框的若干属性:
void musicPlayer::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());//默认路径是项目生成的debug文件
//将路径改为当前工程所在的目录下的musics目录
dir.cdUp();
QString musicPath = dir.path() + "/musicPlayer/musics/";
filedialog.setDirectory(musicPath);
//显示文件对话框为模态,并获取其返回值,如果为打开文件,就获取所有文件到列表中
if(filedialog.exec() == QFileDialog::Accepted)
{
// 获取对话框打开的所有文件的url路径
QList<QUrl> urls = filedialog.selectedUrls();
//将所有文件的url路径交给歌曲列表对象
musicList.addMusicByUrl(urls);
}
}
结果如下:
我们获取到所有要添加的歌曲的url路径之后,肯定是要通过该路径解析出歌曲的信息,包括歌曲名称,歌手名称,专辑名称,歌曲时长等,然后将所有的歌曲对象维护在一个链表中。
因此,我们需要创建两个类,Music类负责解析url路径并添加歌曲各种信息,MusicList类负责维护所有的歌曲对象。
在MusicList类中创建 addMusicByUrl 函数来添加音乐:
void MusicList::addMusicByUrl(const QList<QUrl> &urls)
{
for(auto e : urls)
{
//需要重新筛选出音乐文件添加入歌曲列表
//构建QMimeDatabase对象,从QUrl对象中获取文件的扩展名保存在QMimeType对象中
QMimeDatabase mimeDB;
QMimeType mimeType = mimeDB.mimeTypeForName(e.toLocalFile());
//通过name函数得到扩展名的字符串形式
QString mime = mimeType.name();
// audio/mpeg : 适⽤于mp3格式的⾳频⽂件
// audio/flac : 表⽰⽆损⾳频压缩格式
// audio/wav : 表⽰wav格式的歌曲⽂件
//判断是否是音乐类型
if(mime == "audio/mpeg" || mime == "audio/flac" || mime == "audio/wav")
{
//通过url路径构建Music对象
Music music(e);
musicList.push_back(music);
}
}
}
由于我们前边设置了文件对话框的过滤器为显示所有文件,所以在这里我们需要进一步过滤出音乐文件才能添加进歌曲列表。
在Music类中加入以下这些成员变量,并为其均设置一个set和get方法:
QString musicName;//歌曲名称
QString musicSinger;//歌手
QString musicAlbumn;//歌曲专辑
qint64 musicTime;//歌曲时间
bool isLike;//是否收藏
bool isHistory;//是否为历史播放
QUrl musicUrl;//歌曲url路径
QString musicId;//标识歌曲唯一性
为了标识每首歌的唯一性,我们为每首歌设置一个UUID,即通用唯⼀识别码(Universally Unique Identifier),确保在分布式系统中每个元素都有唯⼀的标识。在构造函数中为其设置:
Music::Music(QUrl url)
:isLike(false)
,isHistory(false)
,musicUrl(url)
{
//为每首歌设置独有的musicId
musicId = QUuid::createUuid().toString();
}
接下来我们就要通过音乐文件的url路径来解析出音乐的各种信息了。
解析音乐文件的元数据,需要用到QMediaPlayer ,该类也是用来进行歌曲播放的类,使用媒体播放类时,必须在项目的工程文件中添加媒体模块:
该模块主要用来播放各种音频视频文件等。
在Music类中添加如下函数来完成歌曲元数据的解析工作:
void Music::parseMediaMetaData()
{
//创建歌曲解析对象
QMediaPlayer player;
//导入音乐的url路径进行解析
player.setMedia(musicUrl);
// 音乐元数据解析需要时间,只有等待解析完成之后,才能提取⾳乐信息,此处循环等待
while(!player.isMetaDataAvailable())
{
// 循环等待会导致主界⾯消息循环⽆法处理,因此需要在等待解析期间,触发事件让消息循环继续处理
QCoreApplication::processEvents();
}
// 解析媒体元数据结束,提取元数据信息
if(player.isMetaDataAvailable())
{
musicName = player.metaData("Title").toString();//得到歌曲名称
musicSinger = player.metaData("Author").toString();//得到歌手名称
musicAlbumn = player.metaData("AlbumTitle").toString();//得到歌曲专辑
musicTime = player.duration();//得到歌曲时长
//如果歌曲为盗版,可能从元数据中无法解析出信息
//此时需要从歌曲的文件名称上去寻找歌曲信息
QString fileName = musicUrl.fileName();
int index = fileName.indexOf('-');//歌曲名称与歌手之间的连接符
if(musicName.isEmpty())
{
if(index != -1)
{
musicName = fileName.mid(0,index);
}
else
{
musicName = fileName.mid(0,fileName.indexOf('.'));//以.mp3结尾
}
}
if(musicSinger.isEmpty())
{
if(index != -1)
{
musicSinger = fileName.mid(index + 2,fileName.indexOf('.') - index - 2);
}
else
{
musicSinger = "歌⼿未知";
}
}
if(musicAlbumn.isEmpty())
{
musicAlbumn = "专辑名未知";
}
qDebug()<<musicName<<" "<<musicSinger<<" "<<musicAlbumn<<" "<<musicTime;
}
}
解析元数据需要时间,因此我们必须循环判断其解析是否完成,但是如果解析较慢,将会一直执行死循环,这会导致整个主界面无法进行消息循环,因此必须触发事件让主界面能够正常的进行消息循环。
此外,我们解析到的歌曲可能会是盗版歌曲,导致其元数据内容不全,此时我们就需要从歌曲的文件命上来提取信息:
最后我们通过debug来打印一下看是否能正确的提取数据:
其中歌曲时间的单位是毫秒(ms)。
我的音乐部分总共有三个界面,分别是我喜欢、本地下载和最近播放,那么我们就需要将歌曲分为这三个类别。
为了标识每个页面的类别,我们采用枚举的方法:
//创建枚举类型标识歌曲所属页面
enum PageType
{
LIKE_PAGE,
LOCAL_PAGE,
RECENT_PAGE
};
在CommonPage中定义歌曲列表类别变量,来标识该页面的类别,同时定义出一个vector,用来管理当前页面歌曲列表中的歌曲ID:
//标识音乐列表类型
PageType pageType;
//存储页面歌曲列表中的歌曲ID
QVector<QString> musicOfPage;
在主界面MusicPlayer的初始化函数中,就为页面设置好对应的类型:
//初始化我的音乐功能界面的文本和图片信息,并设置歌曲列表类型
ui->likePage->setPageType(LIKE_PAGE);
ui->likePage->setCommonPageUi("我喜欢",":/images/ilikebg.png");
ui->localPage->setPageType(LOCAL_PAGE);
ui->localPage->setCommonPageUi("本地下载",":/images/cyx.png");
ui->recentPage->setPageType(RECENT_PAGE);
ui->recentPage->setCommonPageUi("最近播放",":/images/recentbg.png");
划分好歌曲分类之后,就可以将拿到的所有歌曲加入到对应页面的歌曲列表中了:
void CommonPage::addMusicToMusicPage(MusicList musicList)
{
//添加前清除原有的元素
musicOfPage.clear();
for(auto music : musicList)
{
switch(pageType)
{
case LIKE_PAGE:
if(music.getMusicLike())
musicOfPage.push_back(music.getMusicId());
break;
case LOCAL_PAGE:
musicOfPage.push_back(music.getMusicId());
break;
case RECENT_PAGE:
if(music.getMusicHistory())
musicOfPage.push_back(music.getMusicId());
break;
default:
qDebug() << "暂未支持";
}
}
}
此处要注意的是,MusicList类没有实现迭代器功能,所以无法直接使用范围for,所以我们为其实现begin和end方法,来满足范围for的使用:
MusicList::iterator MusicList::begin()
{
return musicList.begin();
}
MusicList::iterator MusicList::end()
{
return musicList.end();
}
最后的最后,我们就需要将歌曲载入界面中了:
void CommonPage::reFresh(MusicList &musicList)
{
//因为我们的添加是遍历所有的歌曲重新添加一遍,所以添加之前应该将原有的歌曲清除
ui->pageMusicList->clear();
//添加歌曲到本页面的歌曲列表
addMusicToMusicPage(musicList);
//查看该歌曲ID是否在列表中存在
for(auto musicId : musicOfPage)
{
auto it = musicList.findMusicByMusicId(musicId);//通过该方法获取到music对象
if(it == musicList.end())
continue;
//将歌曲UI载入列表中
ListBoxItem* listItemBox = new ListBoxItem(this);//创建歌曲ui对象
//设置歌曲属性
listItemBox->setMusicName(it->getMusicName());
listItemBox->setMusicSinger(it->getMusicSinger());
listItemBox->setMusicAlbum(it->getMusicAlbumn());
QListWidgetItem* listWidgetItem = new QListWidgetItem(ui->pageMusicList);//创建列表框单个窗口对象
listWidgetItem->setSizeHint(QSize(listItemBox->width(),listItemBox->height()));//设置窗口的大小
ui->pageMusicList->setItemWidget(listWidgetItem, listItemBox);//将歌曲ui载入窗口对象最终载入列表框中
}
}
在MusicList类中添加通过歌曲ID来获取歌曲对象的方法:
MusicList::iterator MusicList::findMusicByMusicId(const QString &musicId)
{
for(iterator it = begin(); it != end(); ++it)
{
if(it->getMusicId() == musicId)
{
return it;
}
}
return end();
}
还需要在ListBoxItem类中添加如下方法来设置UI界面上的歌曲属性:
void ListBoxItem::setMusicName(const QString &musicName)
{
ui->musicNameLabel->setText(musicName);
}
void ListBoxItem::setMusicSinger(const QString &musicSinger)
{
ui->musicSingerLable->setText(musicSinger);
}
void ListBoxItem::setMusicAlbum(const QString &musicAlbum)
{
ui->musicAlbumLable->setText(musicAlbum);
}
结果如下:
(4)歌曲收藏功能
能够看到,我们还没有为歌曲的收藏按钮设置图标,想要给歌曲设置收藏图标,就要的到该歌曲是否处于收藏的状态,因此需要在ListBoxItem中加入isLike变量,默认为false即未收藏,来获取歌曲的收藏状态,同时构建函数来改变歌曲收藏图标:
void ListBoxItem::setMusicLikeIcon(bool isLike)
{
this->isLike = isLike;
if(isLike)
ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
else
ui->likeBtn->setIcon(QIcon(":/images/like_1.png"));
}
在reFresh函数中,加载歌曲时,因为是新增的歌曲,所以默认为未收藏,将其设置未收藏的图标:
listItemBox->setMusicLikeIcon(it->getMusicLike());
结果如下:
同时我们要为该按钮设置点击槽函数,可以设置其收藏和取消收藏两种方式:
void ListBoxItem::on_likeBtn_clicked()
{
isLike = !isLike;
setMusicLikeIcon(isLike);
}
结果如下:
我们将歌曲收藏之后,应该将该被收藏的歌曲添加到我喜欢列表中,取消收藏,再将该歌曲从我喜欢列表中移除。
此时我们就需要发送一个信号,告诉歌曲对象,你的收藏状态改变了,应该改变你的isLike变量,然后根据该变量判断是否要将该歌曲载入或移除我喜欢列表。
void ListBoxItem::on_likeBtn_clicked()
{
isLike = !isLike;
setMusicLikeIcon(isLike);
//发送信号
emit setIsLike(isLike);
}
当按钮点击时,我们发送信号,该信号由谁接收呢???我们要知道,最终我们是改变某个歌曲对象的收藏状态,所以我们必须能够找到该歌曲对象, 而我们所写的所有代码中,只有CommonPage的refresh函数中能够找到单个对象,所以在该函数中接收信号。
接收信号之后,我们并不能在该函数中直接处理,因为我们实际是在处理页面的信息,所以应该交给其外层MusicPlayer去处理,所以我们接收信号的同时,再向MusicPlayer发送信号,等于是作为一个信号的中转站:
//接收ListBoxItem发送的收藏信号,并将信号转发给MusicPlayer
connect(listItemBox,&ListBoxItem::setIsLike,this,[=](bool isLike){
emit upDataLikeMusic(isLike,it->getMusicId());
});
这里发送的信息包括歌曲的收藏状态以及歌曲ID,这样我们就能在MusicPlayer中定义最终槽函数来处理了。
在MusicPlayer的槽函数处理列表中添加信号槽连接:
//处理歌曲收藏信号
connect(ui->likePage,&CommonPage::upDataLikeMusic,this,&musicPlayer::onUpdateLikeMusic);
connect(ui->localPage,&CommonPage::upDataLikeMusic,this,&musicPlayer::onUpdateLikeMusic);
connect(ui->recentPage,&CommonPage::upDataLikeMusic,this,&musicPlayer::onUpdateLikeMusic);
因为在我们的三个界面中,都可以进行歌曲的收藏或取消收藏处理,所以三个界面都应该能够接收该信号,槽函数如下:
void musicPlayer::onUpdateLikeMusic(bool isLike, QString musicId)
{
auto it = musicList.findMusicByMusicId(musicId);
if(it != musicList.end())
{
it->setMusicLike(isLike);
}
ui->likePage->reFresh(musicList);
ui->localPage->reFresh(musicList);
ui->recentPage->reFresh(musicList);
}
歌曲被收藏或取消收藏后,三个界面都应该更新该歌曲的收藏状态。
(5)歌曲播放
想要在Qt中播放音乐,就需要用到QMediaPlayer类,QMediaPlayer是Qt框架中用于支持各种音频和视频的播放,流媒体的播放,各种播放模式(单曲播放、列表播放、循环播放等),各种播放模式(播放、暂停、停止等),信号槽机制可以让用户在播放状态改变时进行所需控制。
同时,我们还需要用到QMediaPlaylist类,它提供了⼀种灵活而强大的方式管理媒体文件的播放列表。通过结合QMediaplayer,可以实现多首歌曲顺序播放、循环播放随机播放等多种播放模式,提升用户的媒体播放体验。
想要播放歌曲,首先就要在MusicPlayer类中进行上述两个类的对象初始化,其中QMediaPlayer对象是播放源,QMediaPlaylist对象是播放列表:
//音乐播放对象
QMediaPlayer* player;
//音乐播放源列表
QMediaPlaylist playList;
在MusicPlayer类中新增一个初始化音乐播放器的函数,在其中完成基本的初始化工作:
void musicPlayer::initPlayer()
{
//初始化歌曲播放器
player = new QMediaPlayer(this);
//初始化歌曲播放列表
playList = new QMediaPlaylist(this);
//设置默认播放方式为循环播放
playList->setPlaybackMode(QMediaPlaylist::Loop);
//将播放列表交给播放器
player->setPlaylist(playList);
//设置默认音量为20%
player->setVolume(20);
}
接下来,我们就需要将对应的歌曲添加到媒体播放列表中,那么这一步骤该在哪里实现???
因为不同页面保存着自己的歌曲,所以在将歌曲载入媒体播放列表时,也应是每个界面将自己内部的歌曲载入,所以需要在CommonPage类中实现:
void CommonPage::addMusicToPlayList(MusicList &musicList, QMediaPlaylist *playList)
{
for(auto music : musicList)
{
switch(pageType)
{
case LIKE_PAGE:
if(music.getMusicLike())
playList->addMedia(music.getMusicUrl());
break;
case LOCAL_PAGE:
playList->addMedia(music.getMusicUrl());
break;
case RECENT_PAGE:
if(music.getMusicHistory())
playList->addMedia(music.getMusicUrl());
break;
default:
qDebug() << "暂未支持";
}
}
}
根据页面属性,再按照歌曲的属性信息,将对应歌曲添加到媒体播放列表中。
那么该函数,又该在哪里被调用呢???自然是当我们在将歌曲添加到界面之后进行调用:
void musicPlayer::on_addLocal_clicked()
{
//...
//将歌曲载入媒体播放列表
ui->localPage->addMusicToPlayList(musicList,playList);
}
接下来,我们来实现歌曲的播放和暂停功能,同时实现其对应的图标样式变化,那么在设置之前,我们应该将一开始为播放按钮设置的背景图片去除,因为通过样式表设置的背景图片会和通过setIcon函数设置的图标相冲突,因此我们应该去除样式表中的设置,转而在初始化函数中为按钮设置默认图标:
void musicPlayer::on_Play_clicked()
{
//判断播放源当前状态
if(player->state() == QMediaPlayer::PlayingState)
{
//处于播放状态,点击后应暂停
player->pause();
ui->Play->setIcon(QIcon(":/images/play2.png"));
}
else if(player->state() == QMediaPlayer::PausedState)
{
//处于暂停状态,点击后应播放
player->play();
ui->Play->setIcon(QIcon(":/images/play1.png"));
}
else if(player->state() == QMediaPlayer::StoppedState)
{
//一开始载入歌曲时,歌曲处于停止状态,点击后应播放
player->play();
ui->Play->setIcon(QIcon(":/images/play1.png"));
}
else
{
//播放错误,打印错误信息
qDebug() << player->errorString();
}
}
结果如下:
(6)歌曲切换
有了播放功能之后,我们再来直接通过槽函数实现歌曲的上下切换:
void musicPlayer::on_playUp_clicked()
{
//切换为上一曲
playList->previous();
}
void musicPlayer::on_playDown_clicked()
{
//切换为下一曲
playList->next();
}
(7)歌曲播放模式
我们播放歌曲可以有列表循环、随机播放、单曲循环三个模式,且都对应有自己的图标和文字提示,默认情况下,我们设置播放模式为列表循环,当单击按钮时,就能进行歌曲播放模式的切换,具体代码实现如下:
void musicPlayer::on_playMode_clicked()
{
//列表循环->随机播放->单曲循环
if(playList->playbackMode() == QMediaPlaylist::Loop)
{
playList->setPlaybackMode(QMediaPlaylist::Random);
ui->playMode->setToolTip("随机播放");
ui->playMode->setIcon(QIcon(":/images/random.png"));
}
else if(playList->playbackMode() == QMediaPlaylist::Random)
{
playList->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
ui->playMode->setToolTip("单曲循环");
ui->playMode->setIcon(QIcon(":/images/single.png"));
}
else if(playList->playbackMode() == QMediaPlaylist::CurrentItemInLoop)
{
playList->setPlaybackMode(QMediaPlaylist::Loop);
ui->playMode->setToolTip("列表循环");
ui->playMode->setIcon(QIcon(":/images/sequential.png"));
}
}
结果如下:
值得注意的是,在Qt设计模式下,列表设置为单曲循环时,上一首下一首按钮的功能被禁用了。
(8)播放全部功能
在我们的三个界面上,都有“播放全部”这个按钮,点击之后默认播放当前界面歌曲列表中的第一首歌,因为该按钮处于CommonPage中,无法直接控制歌曲的播放源,因此我们需要让其发送信号给MusicPlayer,才能进行歌曲的播放:
void CommonPage::on_playAllBtn_clicked()
{
emit playAll(pageType);
}
设置“播放全部”按钮的点击槽函数,向MusicPlayer发送播放全部的信号,并捎带界面的类型。
在MusicPlayer中绑定信号槽:
//处理播放全部按钮信号
connect(ui->likePage,&CommonPage::playAll,this,&musicPlayer::onPlayAll);
connect(ui->localPage,&CommonPage::playAll,this,&musicPlayer::onPlayAll);
connect(ui->recentPage,&CommonPage::playAll,this,&musicPlayer::onPlayAll);
最后处理播放功能:
void musicPlayer::onPlayAll(PageType pageType)
{
//确定当前界面类型
CommonPage* page = nullptr;
switch(pageType)
{
case LIKE_PAGE:
page = ui->likePage;
break;
case LOCAL_PAGE:
page = ui->localPage;
break;
case RECENT_PAGE:
page = ui->recentPage;
break;
default:
qDebug() << "暂未支持";
}
//完成歌曲播放
playAllMusicOfCommonPage(page,0);
}
void musicPlayer::playAllMusicOfCommonPage(CommonPage *page, int index)
{
if(player->state() == QMediaPlayer::PausedState)
{
//处于暂停状态,点击后应播放
player->play();
ui->Play->setIcon(QIcon(":/images/play1.png"));
}
else if(player->state() == QMediaPlayer::StoppedState)
{
//一开始载入歌曲时,歌曲处于停止状态,点击后应播放
player->play();
ui->Play->setIcon(QIcon(":/images/play1.png"));
}
else
{
//播放错误,打印错误信息
qDebug() << player->errorString();
}
//清空播放列表内容
playList->clear();
//向播放列表中载入当前界面的歌曲
page->addMusicToPlayList(musicList,playList);
//默认播放从第0首开始
playList->setCurrentIndex(index);
//播放歌曲
player->play();
}
当我们的界面切换时,播放列表playList内部的歌曲也必须跟着进行清空并重新载入。此外,点击播放全部按钮,也应使下方播放按钮的图标进行改变。
(9)双击歌曲播放
我们还需要实现,当双击歌曲条目时,播放该歌曲。其实这个功能的实现,和上述播放全部功能是类似的,当我们双击歌曲条目时,发送信号给MusicPlayer,将页面类型以及歌曲在列表中的下标index告诉MusicPlayer,就可以复用上述函数来完成实现:
//绑定双击歌曲条目信号
connect(ui->pageMusicList,&QListWidget::doubleClicked,this,[=](const QModelIndex& index){
emit playMusicByIndex(this,index.row());
});
在CommonPage的构造函数中绑定双击信号槽,并发送信号给MusicPlayer,捎带当前页面和歌曲的下标index。
在MusicPlayer中关联信号槽,并进行处理:
//处理双击歌曲播放信号
connect(ui->likePage,&CommonPage::playMusicByIndex,this,&musicPlayer::onPlayMusicByIndex);
connect(ui->localPage,&CommonPage::playMusicByIndex,this,&musicPlayer::onPlayMusicByIndex);
connect(ui->recentPage,&CommonPage::playMusicByIndex,this,&musicPlayer::onPlayMusicByIndex);
void musicPlayer::onPlayMusicByIndex(CommonPage *page, int index)
{
playAllMusicOfCommonPage(page,index);
}
(10)历史播放界面处理
当我们正在播放某首歌时,它应该被添加到历史播放这个页面中去,想要添加,我们就必须拿到这首歌,并且将它的是否历史播放属性设置为真。
在musicPlayList类中,存在一个当前播放歌曲的下标改变的信号,那么我们就可以通过接收这个信号,获取到当前播放歌曲在播放列表的下标,进而得到该歌曲的ID,以及歌曲本身,最后设置其历史播放属性。
此外,只有当我们播放我喜欢和本地下载中的歌曲时,才应该将该歌曲添加到历史播放中去,所以我们需要在MusicPlayer中新增一个currentPage成员变量,来记录当前所处的页面。
//设置默认界面为本地下载界面
ui->stackedWidget->setCurrentIndex(4);
currentPage = ui->localPage;
默认设置为本地下载界面,同时,当我们通过“播放全部”或者“双击条目”播放音乐时,应及时更改当前界面:
void musicPlayer::playAllMusicOfCommonPage(CommonPage *page, int index)
{
currentPage = page;
//...
}
MusicPlayer中定义槽函数来管理歌曲下标改变信号:
//处理当前歌曲下标切换信号
connect(playList,&QMediaPlaylist::currentIndexChanged,this,&musicPlayer::onCurrentIndexChanged);
通过槽函数处理信号:
void musicPlayer::onCurrentIndexChanged(int index)
{
//值得注意的是,当前播放列表中的歌曲和界面中的musicOfPage成员中的歌曲是完全一致的
QString musicId = currentPage->findMusicIdByIndex(index);
auto music = musicList.findMusicByMusicId(musicId);
if(music != musicList.end())
{
music->setMusicHistory(true);
}
//更新最近播放页面
ui->recentPage->reFresh(musicList);
}
在CommonPage中新增通过歌曲列表下标获取歌曲ID的函数:
QString CommonPage::findMusicIdByIndex(int index)
{
if(index >= musicOfPage.size())
{
qDebug() << "无此歌曲";
return "";
}
return musicOfPage[index];
}
结果如下:
(11)音量调节功能
音量调节,主要包括按钮静音和拖动进度条使音量增大或减小功能,下面我们来实现这些功能。
首先我们应该在volumeTool定义一个 isMuted 变量,原来表示是否静音,为按钮构成槽函数:
void VolumeTool::on_silenceBtn_clicked()
{
isMuted = !isMuted;
if(isMuted)
ui->silenceBtn->setIcon(QIcon(":/images/silent.png"));
else
ui->silenceBtn->setIcon(QIcon(":/images/volume.png"));
//发送静音信号
emit setMusicMuted(isMuted);
}
点击按钮,改变音量图标,同时向MusicPlayer发送信号,通过信号槽来实现静音功能:
//关联歌曲静音槽函数
connect(volumeTool,&VolumeTool::setMusicMuted,this,&musicPlayer::onSetMusicMuted);
void musicPlayer::onSetMusicMuted(bool isMuted)
{
player->setMuted(isMuted);
if(isMuted)
ui->volume->setIcon(QIcon(":/images/silent.png"));
else
ui->volume->setIcon(QIcon(":/images/volume.png"));
}
注意最上边更改图标是更改的音量调节界面的图标,而主界面上的图标也应该改变,结果如下 :
处理好静音功能,我们再来看如何实现拖拽进度条来调节音量大小。
想要实现音量的调节功能,我们就必须获取到鼠标点击的坐标,这样才能通过坐标来处理进度条的大小以及小按钮的位置,当我们点击或者拖拽鼠标时,进度条的位置和小按钮的位置要随着鼠标位置一起改变,松开鼠标时,通过此时的坐标,计算出音量的大小比例,进而将该比例作为信号通知给MusicPlayer来进行音量调节。
因此,我们必须拦截鼠标的点击、松开和移动三个事件,并进行处理:
在VolumeTool类中添加volumeRatio变量,用于记录当前的音量大小,默认为20,构建事件过滤函数,拦截三个事件:
// 安装事件过滤器
ui->sliderBox->installEventFilter(this);
使用事件过滤器之前必须先安装。
bool VolumeTool::eventFilter(QObject *watched, QEvent *event)
{
//事件发生在音量调节Box中
if(watched == ui->sliderBox)
{
if(event->type() == QEvent::MouseButtonPress)
{
//鼠标按下事件,通过函数设置坐标并计算音量大小
setVolume();
}
else if(event->type() == QEvent::MouseMove)
{
//拖拽时,也应发送音量改变信号,这样可以在拖拽时,实时感受音量大小的改变
setVolume();
emit setMusicVolume(volumeRatio);
}
else if(event->type() == QEvent::MouseButtonRelease)
{
//鼠标松开事件,发送信号给MusicPlayer传递音量大小
emit setMusicVolume(volumeRatio);
}
return true;
}
//不关心的事件交给父类处理
return QObject::eventFilter(watched,event);
}
当鼠标按下或者时移动时,都通过函数来调节坐标和计算音量比例,鼠标松开时才发送信号。
在setVolume函数中实现控件坐标移动并计算音量比例:
void VolumeTool::setVolume()
{
//得到鼠标点击纵坐标
int height = ui->sliderBox->mapFromGlobal(QCursor().pos()).y();
//限制鼠标的可移动范围
height = height < ui->inLine->y() ? ui->inLine->y() : height;
height = height > (ui->inLine->y() + ui->inLine->height()) ? ui->inLine->y() + ui->inLine->height() : height;
//调整outLine的高度
ui->outLine->setGeometry(ui->outLine->x(),height,ui->outLine->width(),ui->inLine->y() + ui->inLine->height() - height);
//调整小按钮的位置
ui->sliderBtn->move(ui->sliderBtn->x(),height - ui->sliderBtn->height() / 2);
//计算音量比例
volumeRatio = (int)(ui->outLine->height() * 100 / ui->inLine->height());
//更新音量比例显示
ui->volumeRatio->setText(QString::number(volumeRatio) + "%");
}
通过QCutsor类的pos函数得到鼠标的点击坐标,值得注意的是,这个坐标是相对于整个屏幕的坐标,因此必须将其转化为相对于音量调节界面的坐标。
在MusicPlayer中关联信号,构建槽函数完成音量调节:
//关联音量调节信号
connect(volumeTool,&VolumeTool::setMusicVolume,this,&musicPlayer::onSetMusicVolume);
void musicPlayer::onSetMusicVolume(int volumeRatio)
{
player->setVolume(volumeRatio);
}
结果如下:
(12)歌曲时间同步
我们希望在界面的右下角可以同步歌曲的总时长和当前的进度时间,该如何实现呢?
首先来看总时长,当播放源的持续时长发生切换时,QMediaPlayer会发送durationChanged信号,该信号会提供将要播放的媒体的总时长。因此,我们只需要拦截该信号,就可以得到正在播放的歌曲的总时长。
//关联音量调节信号
connect(volumeTool,&VolumeTool::setMusicVolume,this,&musicPlayer::onSetMusicVolume);
直接关联该信号,然后进行处理:
void musicPlayer::onMusicDurationChanged(qint64 duration)
{
// duration/1000/60 为分
// duration/1000%60 为秒
ui->totalTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0'))
.arg(duration/1000%60,2,10,QChar('0')));
}
其中arg为字符串替换函数,具体为,将%1替换为 duration/1000/60,占据2个字符,以10进制形式,字符不足时补‘0’,%2同理。
结果如下:
下面来看歌曲的时间如何跟随歌曲进度变化。
实际上,当媒体自己的时间发生变化时,QMediaPlayer会发送positionChanged信号,该信号会提供正在播放的媒体当前的运行时长,因此歌曲当前时长的处理方式与上述总时长完全一致。
//关联音量调节信号
connect(volumeTool,&VolumeTool::setMusicVolume,this,&musicPlayer::onSetMusicVolume);
void musicPlayer::onMusicPositionChanged(qint64 position)
{
// position/1000/60 为分
// position/1000%60 为秒
ui->currentTime->setText(QString("%1:%2").arg(position/1000/60,2,10,QChar('0'))
.arg(position/1000%60,2,10,QChar('0')));
}
结果如下:
(13)歌曲进度调节功能
歌曲进度条的调节,和音量的调节是非常相似的,完全可以复用其事件过滤器的方式,然后修改进度条的宽度即可。但是除了事件过滤器之外,我们还可以通过将三个事件重写的方式来处理:
void MusicSlider::moveSilder()
{
ui->outLine->setGeometry(ui->outLine->x(),ui->outLine->y(),currentPos,ui->outLine->height());
}
void MusicSlider::mouseMoveEvent(QMouseEvent *event)
{
//鼠标移动时,必须左键按下
if(event->buttons() == Qt::LeftButton)
{
currentPos = event->pos().x();
if(currentPos < 0)
currentPos = 0;
else if(currentPos > ui->inLine->width())
currentPos = ui->inLine->width();
moveSilder();
}
}
void MusicSlider::mousePressEvent(QMouseEvent *event)
{
currentPos = event->pos().x();
moveSilder();
}
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
currentPos = event->pos().x();
moveSilder();
}
其中currentPos是定义的用来存储进度条当前位置的变量,初始为0。
下面我们来看,如何将进度条和歌曲的播放时间相同步。
这个其实也蛮简单的,要知道,进度条的当前进度相对于整个进度的比例,是和歌曲的当前时间相对于总时间的比率是完全相同的,它们两个都表示一首歌的进度信息。所以当进度条的位置改变时,就可以向MusicPlayer发送一个信号,传入这个比率,然后在通过这个比率计算歌曲的当前时间,再通过这个时间去设置歌曲的进度即可。
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
currentPos = event->pos().x();
moveSilder();
//发送信号,传入比率
emit setMusicSliderPosition((float)currentPos / ui->inLine->width());
}
发送信号应在鼠标松开事件中进行,在MusicPlayer中连接信号槽并进行处理:
//获取歌曲进度条比率
connect(ui->processBar,&MusicSlider::setMusicSliderPosition,this,&musicPlayer::onSetMusicSliderPosition);
这里在MusicPlayer中定义新变量totalTime,在同步歌曲总时长时记录歌曲的总时长。
void musicPlayer::onSetMusicSliderPosition(float ratio)
{
qint64 position = totalTime * ratio;
ui->currentTime->setText(QString("%1:%2").arg(position/1000/60,2,10,QChar('0'))
.arg(position/1000%60,2,10,QChar('0')));
//通过当前时间设置歌曲进度
player->setPosition(position);
}
结果如下:
但是此时我们只实现了歌曲进度跟随进度条变化,却没有实现进度条根据歌曲进度来变化,这又该如和实现呢???
相同的,因为进度条的比率和时间相同,所以我只需要先得到时间的比率,再传给进度条,再让进度条根据该比率去重新设置宽度即可。
void MusicSlider::setStep(float ratio)
{
currentPos = this->width() * ratio;
moveSilder();
}
在设置歌曲当前进度时长的函数中计算比率并调用上述函数:
void musicPlayer::onMusicPositionChanged(qint64 position)
{
// position/1000/60 为分
// position/1000%60 为秒
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((float)position / totalTime);
}
(14)歌曲信息及图片同步
播放源发生改变时,我们应该更新出播放源的歌曲名称,歌手以及歌曲的封面图。当播放源发生变化时,QMediaPlayer会发送metaDataAvailableChanged信号,为该信号关联信号槽进行处理:
//获取歌曲切换信号
connect(player,&QMediaPlayer::metaDataAvailableChanged,this,&musicPlayer::onMetaDataAvailableChanged);
在MusicPlayer中新建currentMusicIndex变量,记录当前播放歌曲的下标。
void musicPlayer::onMetaDataAvailableChanged(bool available)
{
(void)available;
QString musicId = currentPage->findMusicIdByIndex(currentMusicIndex);
auto it = musicList.findMusicByMusicId(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));
}
else
{
//歌曲元数据中没有图片,设置默认图片
ui->musicCover->setPixmap(QString(":/images/record.png"));
}
//图片完全填充
ui->musicCover->setScaledContents(true);
}
结果如下:
除了底部播放控制区显示歌曲图片之外,在每个界面中还有一个显示当前歌曲图片的区域:
该位置的图片也应跟随当前界面播放的歌曲进行改变。这个很好实现,在CommonPage类中添加一个设置图片的函数,然后在上述代码中设置底部图片的同时,也设置界面图片即可。
void CommonPage::setPageImage(QPixmap pixmap)
{
ui->musicImageLable->setPixmap(pixmap);
ui->musicImageLable->setScaledContents(true);
}
if(coverImage.isValid())
{
QImage image = coverImage.value<QImage>();
ui->musicCover->setPixmap(QPixmap::fromImage(image));
//界面图片设置
currentPage->setPageImage(QPixmap::fromImage(image));
}
else
{
//歌曲元数据中没有图片,设置默认图片
ui->musicCover->setPixmap(QString(":/images/record.png"));
//界面图片设置
currentPage->setPageImage(QString(":/images/record.png"));
}
结果如下:
(15)歌词同步
想要同步歌词,就必须拥有对应歌曲的.lrc文件,该文件中标注了各个时间段对应的歌词信息,其标准格式为 [分钟:秒.毫秒] 歌词:
因此想要同步歌词,就必须获取到该lrc文件的路径,解析该lrc文件。为此,需要在music类中构建函数,用来得到对应歌曲的lrc文件路径。
QString Music::getMusicLrcPath() const
{
//歌曲的lrc文件和MP3文件在同一路径下
//因此我们可以先获取歌曲的文件路径,再将后缀替换为.lrc
QString lrcPath = musicUrl.toLocalFile();
//进行路径替换
lrcPath.replace(".mp3",".lrc");
lrcPath.replace(".flac", ".lrc");
lrcPath.replace(".mpga", ".lrc");
return lrcPath;
}
同步显示当前播放歌曲的歌词,自然是在当播放源发生变化时进行,因此需要在 onMetaDataAvailableChanged槽函数中调用:
void musicPlayer::onMetaDataAvailableChanged(bool available)
{
(void)available;
//...
//获取歌词路径
if(it != musicList.end())
{
QString lrcPath = it->getMusicLrcPath();
lrcPage->parseLrcPath(lrcPath);
//设置歌词界面歌曲名称和歌手
lrcPage->setMusicNameAndSinger(musicName,musicSinger);
}
}
同时,我们也应将歌曲名称和歌手在界面中进行设置,同样设计方法来实现。
void LrcPage::setMusicNameAndSinger(const QString musicName, const QString musicSinger)
{
ui->musicName->setText(musicName);
ui->musicSinger->setText(musicSinger);
}
在LrcPage类中创建函数来解析歌曲歌词路径。
lrc文件中包含歌词的时间以及歌词文本两个重要信息,因此我们需要在LrcPage类中构建类来维护这两个信息:
struct LrcLine
{
qint64 time; // 时间
QString text; // 歌词内容
LrcLine(qint64 qtime, QString qtext)
: time(qtime)
, text(qtext)
{}
};
同时创建一个Qvector来存放每一段歌词对象:
//用于存放每一段歌词对象
QVector<LrcLine> lrcLines;
随后在parseLrcPath函数中完成歌词路径的解析,得到每一段歌词的时间和文本:
bool LrcPage::parseLrcPath(const QString lrcPath)
{
//打开歌词文件
QFile file(lrcPath);
if(!file.open(QFile::ReadOnly))
{
qDebug() << "打开文件:" << lrcPath;
return false;
}
//更换歌曲时要清空QVector中存放的上一首歌曲的歌词
lrcLines.clear();
while(!file.atEnd())//没有到文件结尾就一直获取
{
//获取每行字符串
QString lrcWord = file.readLine(1024);
//以'['和']'作为歌词时间和歌词文本的分界线
int left = lrcWord.indexOf('[');
int right = lrcWord.indexOf(']');
//获取歌词时间字符串
QString lrcTime = lrcWord.mid(left + 1,right - left + 1);
//将时间字符串转化为qint64类型的整型
//获取分
qint64 time = 0;
int start = 0,end = 0;
end = lrcTime.indexOf(':');
time += lrcTime.mid(start,end - start).toInt() * 60 * 1000;
//获取秒
start = end + 1;
end = lrcTime.indexOf('.',start);
time += lrcTime.mid(start,end - start).toInt() * 1000;
//获取毫秒
start = end + 1;
end = lrcTime.indexOf('.',start);
time += lrcTime.mid(start,end - start).toInt();
//获取歌词文本字符串
QString lrcText = lrcWord.mid(right + 1,lrcWord.size() - right - 2);
//载入QVector中
lrcLines.push_back(LrcLine(time,lrcText));
}
for(auto e : lrcLines)
{
qDebug() << e.time << ':' << e.text;
}
return true;
}
值得注意的是,当歌曲发生切换时,应及时将QVector中存放的歌词信息清空。
获取所有的歌词信息之后,我们通过测试来查看是否均获取正确,结果如下:
拿到歌曲的歌词信息之后,接下来就要在界面上将歌词进行轮番更替显示了,我们将该工作构建方法来完成,因为歌词的显示是与时间挂钩的,所以我们将这一方法放在 MusicPlayer 的 onMusicPositionChanged 函数中调用更合适,传入歌曲当前的进行时长,这样就可以根据该时间来选择要显示的歌词:
void musicPlayer::onMusicPositionChanged(qint64 position)
{
// position/1000/60 为分
// position/1000%60 为秒
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((float)position / totalTime);
//当前有歌曲在播放,才显示歌词
if(currentMusicIndex >= 0)
{
lrcPage->showLrcWord(position);
}
}
这里要注意的一点是,只有当前界面的列表中存在正在播放的歌曲时,才进行歌词显示。
void LrcPage::showLrcWord(qint64 position)
{
//根据当前时间获取一个高亮显示的歌词在QVector中的索引
int index = getLineIndexByPosition(position);
if(index == -1)
{
ui->line1->setText("");
ui->line2->setText("");
ui->line3->setText("");
ui->lineCenter->setText("当前歌曲无歌词");
ui->line4->setText("");
ui->line5->setText("");
ui->line6->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->line4->setText(getLrcWordByIndex(index + 1));
ui->line5->setText(getLrcWordByIndex(index + 2));
ui->line6->setText(getLrcWordByIndex(index + 3));
}
}
int LrcPage::getLineIndexByPosition(qint64 position)
{
if(lrcLines.isEmpty())
//歌词为空
return -1;
if(position < lrcLines[0].time)
//当前时间小于第一行歌词的时间,默认显示第一行
return 0;
for(int i = 1;i < lrcLines.size();i++)
{
if(position >= lrcLines[i - 1].time && position < lrcLines[i].time)
return i - 1;
}
//当前时间大于歌词最后一行的时间,默认显示最后一行
return lrcLines.size() - 1;
}
在设置歌词文本到界面上时,我们必须考虑到如果当前索引的越界问题,因此将其构造成一个方法来单独解决:
QString LrcPage::getLrcWordByIndex(int index)
{
//越界索引,返回空字符
if(index < 0 || index >= lrcLines.size())
{
return "";
}
return lrcLines[index].text;
}
结果如下:
至此,关于Qt音乐播放器中的常见的界面设计和方法实现已经全部完成了。
3.数据库设计
虽然音乐播放器的整体结构我们已经全部实现,但是我们仍然一个问题,那就是当我们每次重新启动程序时,程序都是全新的,也就是说我们历史上对音乐播放器的各种操作都没有被保留下来,包括歌曲的添加,歌曲的收藏,以及历史播放中的歌曲列表等。
这就会很影响用户的使用体验,因此我们需要借助数据库,将我们历史上对音乐播放器的操作记录下来,当再次启动程序时,将历史记录重新导入程序,这样就可以实现保留历史操作的效果。
(1)数据库选择
常见的数据库管理系统有:Oracle、SqlServer、MySQL等,这些数据库管理系统有一个特点,必须先要在本地安装数据库系统的软件,然后才能使用数据库管理系统提供的服务。而数据库管理系统的安装和卸载,有时候简直是噩梦,怎么装就是失败,最后导致重装操作系统。
那么在Qt中,已经内置了⼀个轻量级、无需安装的桌面型数据库SQLite,在安装Qt环境时,SQLIte的环境就也已经被配置好了,只需在使用时在.pro文件中导入数据库模块即可。SQLite是非常流行的开源嵌入式数据库,将源文件添加到工程就可以直接使用。它很好的支持关系型数据库所具备的⼀些基本特征,比如:标准SQL语法、事务、数据表和索引等。
下面给出SQLite数据库的使用教程:
(2)音乐播放器使用SQLite
Qt中内置很多类,用于处理与数据库之间的关联操作。
QSqlDatabase类主要处理与数据库的连接,提供了创建、配置、打开和关闭数据库连接的方法。
QSqlQuery类用来执行SQL语句和操作数据库。QSqlQuery类的exec()函数用来执行SQL语句。
要让音乐播放器在关闭时能够记录我们已经做过的各种操作,就需要在关闭时将音乐播放器中的相关信息保存到数据库中,例如不同界面的歌曲列表信息等,在下一次启动音乐播放器时,再从数据库中读取这些信息,重新加入到音乐播放器中。
下面我们就来看,如何完成数据库的各种操作。
(3)初始化数据库
初始化数据库,我们应执行如下操作:
首先在主窗口类中定义数据库属性:
//数据库对象
QSqlDatabase* qSqlite;
void musicPlayer::initSqlite()
{
//数据库驱动加载
QSqlDatabase qSqlite = QSqlDatabase::addDatabase("QSQLITE");
//设置数据库名称
qSqlite.setDatabaseName("MusicPlayer.db");
//打开数据库
if(!qSqlite.open())
{
QMessageBox::critical(this,"MusicPlayer","数据库打开失败!");
}
qDebug() << "数据库打开成功";
//创建表
QString sql = "CREATE TABLE IF NOT EXISTS MusicInfo(\
id INTEGER PRIMARY KEY AUTOINCREMENT,\
musicId varcher(50) UNIQUE,\
musicName varcher(50),\
musicSinger varcher(50),\
albumName varcher(50),\
musicUrl varcher(256),\
musicTime BIGINT,\
isLike INTEGER,\
isHistory INTEGER)";
QSqlQuery query;
if(!query.exec(sql))
{
QMessageBox::critical(this,"MusicPlayer","初始化错误!");
return;
}
qDebug() << "创建表格成功";
}
创建的表格中,我们需要将歌曲的各种信息都写进去。但是值得注意的是,在表格中需要设置主键约束,但是我们定义的标识歌曲唯一性的ID并不适合作为主键,因为其顺序是错乱的,而主键要求必须有序,因此需要额外创建一个属性来作为主键ID。
(4)向表格中写入数据
创建完表格,先尝试向表格中写入数据,此时我们需要从MusicList列表中拿到每一首music,进而将每首music的属性信息写入表格,为此,我们要分别在MusicList类和MusicList类中分别创建一个新方法:
void MusicList::writeToDB()
{
for(auto music : musicList)
{
music.addMusicInfoToDB();
}
}
void Music::addMusicInfoToDB()
{
//判断当前music信息是否存在于表格中,如果存在,只需更新isLike和isHistory属性,不存在则写入全部信息
QSqlQuery query;
query.prepare("SELECT EXISTS(SELECT 1 FROM MusicInfo WHERE MusicId = ?)"); //快速查询语句
query.addBindValue(musicId);
if(!query.exec())
{
qDebug() << "查询失败!" << query.lastError().text();
return;
}
if(query.next())
{
bool isExists = query.value(0).toBool();
if(isExists)
{
//歌曲存在于表格中
query.prepare("UPDATE MusicInfo SET isLike = ?, isHistory = ? WHEHE musicId = ?");
query.addBindValue(isLike ? 1 : 0);
query.addBindValue(isHistory ? 1 : 0);
query.addBindValue(musicId);
if(!query.exec())
{
qDebug() << "更新失败!" << query.lastError().text();
return;
}
qDebug() << "更新music信息:" << musicName << " " << musicId;
}
else
{
//歌曲不存在于表格中
query.prepare("INSERT INTO MusicInfo(musicId, musicName, musicSinger, albumName, musicUrl,\
musicTime, isLike, isHistory)\
VALUES(?,?,?,?,?,?,?,?)");
query.addBindValue(musicId);
query.addBindValue(musicName);
query.addBindValue(musicSinger);
query.addBindValue(musicAlbumn);
query.addBindValue(musicUrl.toLocalFile());
query.addBindValue(musicTime);
query.addBindValue(isLike ? 1 : 0);
query.addBindValue(isHistory ? 1 : 0);
if(!query.exec())
{
qDebug() << "插入失败!" << query.lastError().text();
return;
}
qDebug() << "插入music信息:" << musicName << " " << musicId;
}
}
}
插入信息时,一定要保证每一列的内容与创建表格时的顺序保持一致。
最后,我们希望在窗口关闭时将信息写入数据库,因此在窗口的关闭槽函数中进行调用:
void musicPlayer::on_quit_clicked()
{
//将music信息写入数据库
musicList.writeToDB();
//与数据库断开连接
qSqlite->close();
//关闭窗口
close();
}
(5)将数据库信息载入播放器
将数据库信息载入播放器,首先我们需要将数据库表格中记录的歌曲信息读取,然后生成一个个的music对象,在将这些对象添加到MusicList中:
void MusicList::readToDB()
{
QSqlQuery query;
query.prepare("SELECT musicId, musicName, musicSinger, albumName, musicUrl, musicTime,\
isLike, isHistory FROM MusicInfo");
if(!query.exec())
{
qDebug() << "数据库查询失败!" << query.lastError().text();
return;
}
while(query.next())
{
Music music;
music.setMusicId(query.value(0).toString());
music.setMusicName(query.value(1).toString());
music.setMusicSinger(query.value(2).toString());
music.setMusicAlbumn(query.value(3).toString());
music.setMusicUrl(query.value(4).toString());
music.setMusicTime(query.value(5).toLongLong());
music.setMusicLike(query.value(6).toBool());
music.setMusicHistory(query.value(7).toBool());
musicList.push_back(music);
}
}
随后在主界面类中,我们需要添加初始化MusicList方法,在程序运行时更新MusicList:
void musicPlayer::initMusicList()
{
//读取歌曲信息
musicList.readToDB();
//设置页面类型并更新每个页面的歌曲列表
ui->likePage->setPageType(LIKE_PAGE);
ui->likePage->reFresh(musicList);
ui->localPage->setPageType(LOCAL_PAGE);
ui->localPage->reFresh(musicList);
ui->recentPage->setPageType(RECENT_PAGE);
ui->recentPage->reFresh(musicList);
}
值得注意的是,初始化MusicList方法的调用必须在初始化数据库方法之后,因为后者要从前者获取数据。
此外,在将MusicList中的music载入每个页面时,必须先为每个页面设置好其类型,这样才能保证对应的歌曲能够准确无误的载入到页面中去。
至此,仿QQ音乐设计的音乐播放器的基本功能就已经全部设计完毕啦。
三.遇到的问题
- 每一个部分的控件的大小和位置的设置。
- 构建音量调节界面时,如何将该界面显示在音量图标的正上方。
- 如果歌曲是盗版的,如何获取其歌曲名称和歌手名称。
- 歌曲收藏逻辑应该如何实现。
- 音量及歌曲进度条的处理。
- SQLite数据库语句的使用不够熟练。
四.总结
以上就是简易音乐播放器的全部必要基础内容啦,后续还可能会该项目进行进一步的完善和扩展,敬请期待。
项目代码放在Gitee:https://gitee.com/npy-learn-programming/my-projects/tree/master/musicPlayer