Qt项目——音乐播放器

一.项目介绍 

本篇文章,我们将来介绍一个Qt的项目,仿QQ音乐的简易音乐播放器。

先来看一下QQ音乐的一个界面,包含了诸多的功能:

那么我们自己实现的简易音乐播放器,主要包含以下若干模块:

  1. 界面设计:包括界面设计、界面美化。控件的设计等。
  2. 歌曲管理模块:包括歌曲的载入、歌曲信息解析,歌曲分类管理等。 
  3. 歌曲播放模块:包括歌曲的播放、暂停、切换等基本功能,以及歌词的同步功能。
  4. 数据持久化:实现歌曲的收藏、将歌曲载入最近播放,将用户操作的数据永久保存等。

本项目的开发环境包括: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数据库的使用教程: 

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音乐设计的音乐播放器的基本功能就已经全部设计完毕啦。


三.遇到的问题

  1. 每一个部分的控件的大小和位置的设置。
  2. 构建音量调节界面时,如何将该界面显示在音量图标的正上方。
  3. 如果歌曲是盗版的,如何获取其歌曲名称和歌手名称。
  4. 歌曲收藏逻辑应该如何实现。
  5. 音量及歌曲进度条的处理。
  6. SQLite数据库语句的使用不够熟练。

四.总结

以上就是简易音乐播放器的全部必要基础内容啦,后续还可能会该项目进行进一步的完善和扩展,敬请期待。

项目代码放在Gitee:https://gitee.com/npy-learn-programming/my-projects/tree/master/musicPlayer

1 关于 Easy Player: Easy Player 是由于个人兴趣而制作的一款基于Qt的在线音乐播放器 目前是第一个版本 并未进行足量优化 因此 在使用过程中可能存在某些Bug 请谅解 2 功能介绍: 目前功能支持歌曲在线搜索 单曲循环(其他循环方式后期添加) 添加搜索结果到试听列表 下载音乐到本地 歌词同步显示 还不能同步滚动 3 使用方法: (1)首先 从按钮说起: 左边第一排:播放(暂停) 下一首 单曲循环 下载当前歌曲 歌词显示; 左边第二排:音量键 右边第一排:歌曲时间轴 (2)其次 搜索: “歌曲特征”输入关键词搜索 会呈现搜索结果在搜索列表 搜索列表右边的按钮表示添加歌曲到播放列表 (3)最后 播放列表: 在歌曲列表中双击歌曲播放 右边的按钮表示下载歌曲 目前是下载完成之后才会提示 之后会做一个下载列表界面 4 其他 本来打算在下载的时候加入多线程 另外加一个数据库保存播放信息 但由于时间关系 并没有在这个版本加入 之后的版本会不断完善 欢迎大家下载测试和提意见 声明:代码仅供参考 请尊重原创 作者:Reyn 博客地址:http: blog youkuaiyun.com jan5 reyn">1 关于 Easy Player: Easy Player 是由于个人兴趣而制作的一款基于Qt的在线音乐播放器 目前是第一个版本 并未进行足量优化 因此 在使用过程中可能存在某些Bug 请谅解 2 功能介绍: 目前功能支持歌曲在线搜索 单曲 [更多]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

很楠不爱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值