基于QT的仿QQ音乐播放器

一、项目介绍

该项目是基于QT开发的⾳乐播放软件,界面友好,功能丰富,主要功能如下:

窗口hand部分:
点击最小化按钮,窗口最小化
点击最大化按钮,窗口最大化
点击关闭按钮,程序退出
窗口body左侧部分:
点击推荐按钮,窗口右侧显示:推荐Page(暂时只有页面)
点击电台按钮,窗口右侧显示:电台Page(未⽀持)
点击音乐馆按钮,窗口右侧显示:音乐馆Page(未⽀持)
点击我喜欢按钮,窗口右侧显示:收藏的音乐Page
点击本地下载按钮,窗口右侧显示:本地音乐Page
点击最近播放按钮,窗口右侧显示:最近播放Page
窗口body右侧部分
当窗口左侧不同按钮点击,在窗口右侧会展示不同的页面,本项目暂只支持了本地音乐、喜欢音乐、最近播放音乐的展示。具体功能如下:
点击全部播放按钮,播放当前页面列表中所有音乐
双击列表中某⾳乐,播放当前选中音乐
点击心支持收藏
支持最近播放过音乐记忆

点击推荐按钮,窗口右侧显示:推荐Page(暂只有页面)
播放控制区域
支持seek功能,即拖拽到歌曲指定位置播放
支持:随机、单曲循环、循环播放
支持播放上⼀曲
支持播放下⼀曲
支持播放和暂停
支音量调节和静音
支持歌曲总时长显示、当前播放时间显示
支持LRC歌词同步显示

二、界面开发

界面大体上分为head和body两部分

1、head部分分析

从左到右依此为图标、搜索框、更换皮肤按钮、最小化按钮、最大化按钮、关闭按钮

2、body部分分析

body区域分为左侧种类选择区域和右侧Page展示区。

Body左侧区域由两部分组成:在线音乐 和 我的音乐,两部分内部的控件种类是相同的。

Body右侧部分由 page区、播放进度、播放控制区三个部分组成

①page区域:歌曲信息页面,点击 "<" 或 ">"具有轮番播图效果

②播放进度:当前歌曲播放进度说明,支持seek功能,与播放控制区时间、以及LRC歌词是同步的

③播放控制区域:显示歌曲图片&名称&歌手播放模式&下一曲&播放暂停&上一曲&音量调节和静音&添加本地音乐当前播放时间/歌曲总时长&弹出歌词窗口按钮

当点击时的page页面:

所以我喜欢、本地音乐、最近播放共用一个commonPage页面,

推荐页面需要支持点击按钮时的轮番展示校效果,所以单独用一个页面
歌词展示

显示内容分为:歌曲信息、歌词部分、左上方收起隐藏按钮。

歌曲信息由歌曲名称(QLabel)和歌手名称(QLabel)构成

歌词部分展示当前在唱歌词(QLabel)和在唱部分前三行和后三行歌词(QLabel)展示,当前播放歌词突出显示

点击收起按钮后,该页面会以动画的方式收起

当歌曲有LRC歌词时,播放时歌词会随播放时间自动调整;歌曲没有LRC歌词时,歌词部分显示空字符。
歌曲控制区

从左至右依次为

1、歌曲封面 2、歌曲信息 3、切换播放模式

4、上一曲 5、播放/暂停 6、下一曲

7、调节声音 8、添加本地音乐 9、总时间与当前播放时间

10、显示歌词

QQMusic类

创建一个APPlication,将ui界面的布局完成后对其进行界面美化.

1、widget窗口无标题

将初始化界面的工作放在void initui()里面,在里面添加

cpp 复制代码
// 设置⽆边框窗⼝,即窗⼝将来⽆标题栏
 setWindowFlag(Qt::WindowType::FramelessWindowHint);

2、实现鼠标拖动窗口

重写QQmusic父类中的mousepressEvent和mousemoveEventvent事件;

鼠标左键按下时,记录下窗口左上角和鼠标的相对位置;

鼠标移动时,会产生新的位置,保持鼠标和窗口左上角相对位置不变,通过move修改窗口的左上角坐标即可。

cpp 复制代码
void QQMusic::mousePressEvent(QMouseEvent* event)
{
    // 拦截⿏标左键单击事件
    if (event->button() == Qt::LeftButton)
    {
        // event->globalPos():⿏标按下事件发⽣时,光标相对于屏幕左上⻆位置
        // frameGeometry().topLeft(): ⿏标按下事件发⽣时,窗⼝左上⻆位置
        // geometry(): 不包括边框及顶部标题区的范围
        // frameGeometry(): 包括边框及顶部标题区的范围
        // event->globalPos() - frameGeometry().topLeft() 即为:
        // ⿏标按下时,窗⼝左上⻆和光标之间的距离差
        // 想要窗⼝⿏标按下时窗⼝移动,只需要在mouseMoveEvent中,让光标和窗⼝左上⻆保持相同的位置差
        // 获取⿏标相对于屏幕左上⻆的全局坐标
        dragPosition = event->globalPos() - frameGeometry().topLeft();
        return;
    }
    QWidget::mousePressEvent(event);
}
void QQMusic::mouseMoveEvent(QMouseEvent* event)
{
    if (event->buttons() == Qt::LeftButton)
    {
        // 根据⿏标移动更新窗⼝位置
        move(event->globalPos() - dragPosition);
        return;
    }
    QWidget::mouseMoveEvent(event);
}

3、给窗口添加阴影

在 initUI() 函数中添加:

cpp 复制代码
// 设置窗⼝背景透明
this->setAttribute(Qt::WA_TranslucentBackground);

// 给窗⼝设置阴影效果
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0, 0); // 设置阴影偏移
shadowEffect->setColor("#000000"); // 设置阴影颜⾊:⿊⾊
shadowEffect->setBlurRadius(10); // 设置阴影的模糊半径
this->setGraphicsEffect(shadowEffect);

BtForm类

添加一个新设计师界面,命名为BtForm

将bfForm的ui界面设计好后,将QQmusic的ui界面的相关的QWidget全部提升为BtForm。

效果图:

1、设置按钮上的图片和文字信息,以及该按钮关联的page页面

cpp 复制代码
void btFrom::seticon(QString btIcon, QString btText, int mid)
{
    // 设置⾃定义按钮的图⽚、⽂字、以及id
    ui->btIcon->setPixmap(QPixmap(btIcon));
    ui->btText->setText(btText);
    this->id = mid;
}

在QQMusic.cpp的initUI()函数中新增:

cpp 复制代码
// 设置BodyLeft中6个btForm的信息
 ui->rec->seticon(":/images/rec.png", "推荐", 1);
 ui->music->seticon(":/images/music.png", "⾳乐馆", 2);
 ui->audio->seticon(":/images/radio.png", "电台", 3);
 ui->like->seticon(":/images/like.png", "我喜欢", 4);
 ui->local->seticon(":/images/local.png", "本地下载", 5);
 ui->recent->seticon(":/images/recent.png", "最近播放", 6);

2、按下btForm键后的响应(重写其父类的mousePressEvent)

当按钮按下时:①按钮颜色发生变化 ②给QQMusic类发送click信号

cpp 复制代码
void btFrom::mousePressEvent(QMouseEvent* event)
{
    // 告诉编译器不要触发警告
    (void)event;
    // ⿏标点击之后,背景变为绿⾊,⽂字变为⽩⾊
    ui->btStyle->setStyleSheet("#btStyle{ background:rgb(30,206,154);} *{color:#F6F6F6; }");
    emit click(this->id); // 发送⿏标点击信号
}

③QQMusic类处理该信号,内部:实现窗口切换,并清除上次按钮点击留下的样式,因此QQMuisc中需要新增:

cpp 复制代码
// qqmusic.cpp 新增
void QQMusic::connectSignalAndSlot()
{
 // ...
 // ⾃定义的btFrom按钮点击信号,当btForm点击后,设置对应的堆叠窗⼝
 connect(ui->rec, &btFrom::click, this, &QQMusic::onBtFormClick);
 connect(ui->musics, &btFrom::click, this, &QQMusic::onBtFormClick);
 connect(ui->audio, &btFrom::click, this, &QQMusic::onBtFormClick);
 connect(ui->like, &btFrom::click, this, &QQMusic::onBtFormClick);
 connect(ui->local, &btFrom::click, this, &QQMusic::onBtFormClick);
 connect(ui->recent, &btFrom::click, this, &QQMusic::onBtFormClick);
}

void Widget::onBtFormClick(int id)
{
    // 1.获取当前⻚⾯所有btFrom按钮类型的对象
    QList<BtForm*> buttonList = this->findChildren<BtForm*>();
    // 2.遍历所有对象, 如果不是当前id的按钮,则把之前设置的背景颜⾊清除掉
    foreach(BtForm * btitem, buttonList)
    {
        if (id != btitem->getId())
        {
            btitem->clearBg();
        }
    }
    // 3.设置当前栈空间显⽰⻚⾯
    ui->stackedWidget->setCurrentIndex(id - 1);
}

bfFom类中添加:

cpp 复制代码
// btform.cpp 新增:
void BtForm::clearBg()
{
    // 清除上⼀个按钮点击的背景效果,恢复之前的样式
    ui->btStyle->setStyleSheet("#btStyle:hover{ background:#D8D8D8;} ");
}
int BtForm::getId()
{
    return id;
}

3、btForm类中的动画效果

即给bfForm类中的4个QLabel设置动画效果,在btFrom类中的构造函数中新增,里面的QRect类的参数根据自己设置的ui界面的标准来确定

cpp 复制代码
// 设置line1的动画效果
line1Animal = new QPropertyAnimation(ui->line1, "geometry", this);
line1Animal->setDuration(1500);                      //持续时间
line1Animal->setKeyValueAt(0, QRect(0, 15, 2, 0));   //关键帧
line1Animal->setKeyValueAt(0.5, QRect(0, 0, 2, 15));
line1Animal->setKeyValueAt(1, QRect(0, 15, 2, 0));
line1Animal->setLoopCount(-1);                       //循环次数
line1Animal->start(); 
// 设置line2的动画效果
line2Animal = new QPropertyAnimation(ui->line2, "geometry", this);
line2Animal->setDuration(1600);
line2Animal->setKeyValueAt(0, QRect(7, 15, 2, 0));
line2Animal->setKeyValueAt(0.5, QRect(7, 0, 2, 15));
line2Animal->setKeyValueAt(1, QRect(7, 15, 2, 0));
line2Animal->setLoopCount(-1);
line2Animal->start();
// 设置line3的动画效果
line3Animal = new QPropertyAnimation(ui->line3, "geometry", this);
line3Animal->setDuration(1700);
line3Animal->setKeyValueAt(0, QRect(14, 15, 2, 0));
line3Animal->setKeyValueAt(0.5, QRect(14, 0, 2, 15));
line3Animal->setKeyValueAt(1, QRect(14, 15, 2, 0));
line3Animal->setLoopCount(-1);
line3Animal->start();
// 设置line4的动画效果
line4Animal = new QPropertyAnimation(ui->line4, "geometry", this);
line4Animal->setDuration(1800);
line4Animal->setKeyValueAt(0, QRect(21, 15, 2, 0));
line4Animal->setKeyValueAt(0.5, QRect(21, 0, 2, 15));
line4Animal->setKeyValueAt(1, QRect(21, 15, 2, 0));
line4Animal->setLoopCount(-1);
line4Animal->start();
}

动画并不是所有页面都显示,只有当前选中的页面显示,所以默认情况下,动画隐藏。默认情况下设置addlocal显示。

cpp 复制代码
// btform.cpp的中新增:
void btFrom::showAnimal()
{
 // 显⽰linebox, 设置颜⾊为绿⾊
 ui->linebox->show();
}

QQMusic的initUI中设置默认选中

cpp 复制代码
// 本地下载BtForm动画默认显⽰
 ui->local->showAnimal();
 ui->stackedWidget->setCurrentIndex(4);

推荐页面(recPage类)

1、recPage自定义

分析:

①"推荐"文本提示,即QLabel

②"今日为你推荐"文本提示,即QLabel

③具体推荐的歌曲内容,点击左右两侧翻页按钮,具有轮番图效果,将光标放到图上,有图片上移动画

④"你的歌曲补给站"文本提示,即QLabel具体显示音乐,和③实际是一样的,不同的是③中音乐只有一行,⑤中的音乐有两行因为页面中元素较多,直接摆到一个页面太拥挤,从右侧的滚动条可以看出,整个页面中的元素都放置在QScrollArea中。

仔细分析③发现,里面包含了:

左右各两个按钮,点击之后中间的图片会左右移动,Qt中未提供类似该种组合控件,因此③实际为自定义控件。

③中按钮之间的元素,由图片和底下的文字组成,当光标放在图片上会有上移的动画,因此该元素实际也为自定义控件。


完成布局后的效果:

2、自定义recBox

完成布局后的效果

将QQMusic主界面中recPage页面中的recMusicBox和supplyMusicBox提升为RecBox,就能看到如下效果。

3、自定义recBoxItem

完成布局后的效果

4、RecBoxItem类中添加动画效果

在RecBoxItem类中拦截鼠标进入和离开事件,在进入时让图片上移,在离开时让图片下移回到原位。
在RecBoxItem.cpp新增

cpp 复制代码
#include <QPropertyAnimation>
#include <QDebug>
bool RecBoxItem::eventFilter(QObject* watched, QEvent* event)
{
    // 注意:recItem上有⼀个按钮,当⿏标放在按钮上时在开启动画
    if (watched == ui->musicImageBox)
    {
        int ImgWidget = ui->musicImageBox->width();
        int ImgHeight = ui->musicImageBox->height();
        // 拦截⿏标进⼊事件
        if (event->type() == QEvent::Enter)
        {
            QPropertyAnimation* animation = new QPropertyAnimation(ui -> musicImageBox, "geometry");
            animation->setDuration(100);
            animation->setStartValue(QRect(9, 10, ImgWidget, ImgHeight));
            animation->setEndValue(QRect(9, 0, ImgWidget, ImgHeight));
            animation->start();
            // 注意:动画结束的时候会触发finished信号,拦截到该信号,销毁animation
            connect(animation, &QPropertyAnimation::finished, this, [=]() {
                delete animation;
                qDebug() << "图⽚上移动画结束";
                });
            return true;
        }
        else if (event->type() == QEvent::Leave)
        {
            // 拦截⿏标离开事件
            QPropertyAnimation* animation = new QPropertyAnimation(ui -> musicImageBox, "geometry");
            animation->setDuration(150);
            animation->setStartValue(QRect(9, 0, ImgWidget, ImgHeight));
            animation->setEndValue(QRect(9, 10, ImgWidget, ImgHeight));
            animation->start();
            // 注意:动画结束的时候会触发finished信号,拦截到该信号,销毁animation
            connect(animation, &QPropertyAnimation::finished, this, [=]() {
                delete animation;
                qDebug() << "图⽚上移动画结束";
                });
            return true;
        }
    }
    return QObject::eventFilter(watched, event);
}

注意:拦截事件处理器时一定要先安装事件处理器

cpp 复制代码
// 注意:不要忘记事件拦截器安装,否则时间拦截不到,因此需要在构造函数中添加:
// 拦截事件处理器时,⼀定要安装事件拦截器
ui->musicImageBox->installEventFilter(this);

该类中还需要添加设置推荐文本和图片的方法,将来需要在外部来设置每个重新框项目的文本和图

片:

cpp 复制代码
// RecBoxItem.cpp 新增
void RecBoxItem::setText(const QString& text)
{
    ui->recBoxItemText->setText(text);
}
void RecBoxItem::setImage(const QString& Imagepath)
{
    QString imgStyle = "border-image:url("+Imagepath+");";
    ui->recMusicImg->setStyleSheet(imgStyle);
}

5、RecBox添加RecBoxItem

图片路径和推荐文本准备

每个RecBoxltem都有对应的图片和推荐文本,在往RecBox中添加RecBoxltem前需要先将图片路径和对应文本准备好。由于图片和文本具有对应关系,可以以键值对方式来进行组织,以下实现的时采用QT内置的QJsonObject对象管理图片路径和文本内容。

使用QT内置的QJsonObject对象管理图片路径和文本内容,图片路径和对应文本的准备工作,应该在QQMusic类中处理好,RecBoxItem只负责设置RecBox,因此该准备工作需要在QQMusic类中进行,在QQMusic中需要添加如下代码:

cpp 复制代码
// 设置随机图⽚【歌曲的图⽚】
QJsonArray QQMusic::randomPiction()
{
    // 推荐⽂本 + 推荐图⽚路径
    QVector<QString> vecImageName;
    vecImageName << "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(vecImageName.begin(), vecImageName.end());
    // 001.png
    // path: ":/images/rec/"+vecImageName[i];
    // text: "推荐-001"
    QJsonArray objArray;
    for (int i = 0; i < vecImageName.size(); ++i)
    {
        QJsonObject obj;
        obj.insert("path", ":/images/rec/" + vecImageName[i]);
        // arg(i, 3, 10, QCchar('0'))
        // i:要放⼊%1位置的数据
        // 3: 三位数
        // 10:表⽰⼗进制数
        // QChar('0'):数字不够三位,前⾯⽤字符'0'填充
        QString strText = QString("推荐-%1").arg(i, 3, 10, QChar('0'));
        obj.insert("text", strText);
        objArray.append(obj);
    }
    return objArray;
}

recBox中添加元素

由于recPage页面中有两个RecBox控件,上面的RecBox为一行四列,下方的RecBox为2行四列,因此在RecBox类中新增加以下成员变量:

cpp 复制代码
#include <QJsonArray>
public:
    void initRecBoxUi(QJsonArray data, int row);

private:
    int row; // 记录当前RecBox实际总⾏数
    int col; // 记录当前RecBox实际每⾏有⼏个元素
    QJsonArray imageList; // 保存界⾯上的图⽚, ⾥⾯实际为key、value键值对

RecBox的构造函数中,将row和col默认设置为1和4,count需要具体来计算:

cpp 复制代码
RecBox::RecBox(QWidget* parent) :
    QWidget(parent),
    ui(new Ui::RecBox),
    row(1),
    col(4)
{
    ui->setupUi(this);
}
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
    // 如果是两⾏,说明当前RecBox是主界⾯上的supplyMusicBox
    if (2 == row)
    {
        this->row = row;
        this->col = 8;
    }
    else
    {
        // 否则:只有⼀⾏,为主界⾯上recMusicBox
        ui->recBoxBottom->hide();
    }
    // 图⽚保存起来
    imageList = data;
    // 往RecBox中添加图⽚
    createRecItem();
}
void RecBox::createRecBoxItem()
{
    // 创建RecBoxItem对象,往RecBox中添加
    // col
    for (int i = 0; i < col; ++i)
    {
        RecBoxItem* item = new RecBoxItem();
        // 设置⾳乐图⽚与对应⽂本
        QJsonObject obj = imageList[i].toObject();
        item->setRecText(obj.value("text").toString());
        item->setRecImage(obj.value("path").toString());
        // recMusicBox:col为4,元素添加到ui->recListUpHLayout中
        // supplyMuscBox: col为8, ui->recListUpHLayout添加4个,ui->recListDownHLayout添加4个
        // 即supplyMuscBox上下两⾏都要添加
        // 如果是recMusicBox:row为1,只能执⾏else,所有4个RecBoxItem都添加到ui->recListUpHLayout中
        // 如果是supplyMuscBox:row为2,col为8,col/2结果为4,i为0 1 2 3时,元素添加到ui->recListDownHLayout中
        // i为4 5 6 7时,元素添加到ui->recListUpHLayout中
        if (i >= col / 2 && row == 2)
        {
            ui->recListDownHLayout->addWidget(item);
        }
        else
        {
            ui->recListUpHLayout->addWidget(item);
         }
    }
}

6、RecBox中btUp和btDown按钮clicked处理

添加槽函数

选中recbox.ui文件,分别选中btUp和btDown,右键单击弹出菜单选择转到槽,选中clicked确定,

btUp和btDown的槽函数就添加好了。

cpp 复制代码
void RecBox::on_btUp_clicked()
{
 // 点击btUp按钮,显⽰前4张图⽚,如果已经是第⼀张图⽚,循环从后往前显⽰
}
void RecBox::on_btDown_clicked()
{
 // 点击btUp按钮,显⽰前8张图⽚,如果已经是第⼀张图⽚,循环从后往前显⽰
}

假设imageList中有24组图片路径和推荐文本信息,如果将信息分组:

如果是recMusicBox,将元素按照col分组,即每4个元素为一组,可分为6组

如果是supplyMuscBox,将元素按照col分组,即每8个元素为一组,可分为3组。

RecBox类中添加currentIndex和count整形成员变量,currentIndex记录当前显示组,count记录总的信息组数。当点击btUp时,currentIndex-,显示前一组,如果currentIndex小于O时,将其设置为count-1;

点击btDown按钮时,currentIndex++显示下一组,当currentIndex为count时,将count

设置为0。

cpp 复制代码
// recbox.cpp 中新增
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
    // ...
    imageList = data;

    // 默认显⽰第0组
    currentIndex = 0;

    // 计算总共有⼏组图⽚,ceil表⽰向上取整
    count = ceil(imageList.size() / col);
    // 在RecBox控件添加RecBoxItem
    createRecBoxItem();
}
void RecBox::on_btUp_clicked()
{
    // 点击btUp按钮,显⽰前⼀组图⽚,如果已经是第⼀组图⽚,显⽰最后⼀组
    currentIndex--;
    if (currentIndex < 0)
    {
        currentIndex = 0;
    }

    createRecBoxItem();
}
void RecBox::on_btDown_clicked()
{
    // 点击btDown按钮,显⽰下⼀组图⽚,如果已经是最后⼀组图⽚,显⽰第0组
    currentIndex++;
    if (currentIndex >= count)
    {
        currentIndex = 0;
    }
    createRecBoxItem();
}

元素重复分析

每次btUp和btDown点击后,应该显示前一组和后一组图片,由于之前recListUpHLayout和

recListDownHLayout中已经有元素了,因此需要先将之前的元素删除掉。

在RecBox::createRecBoxItem()成员函数中新加

cpp 复制代码
void RecBox::createRecBoxItem()
{
    // 溢出掉之前旧元素
    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;
    }
}

程序启动时图片随机显示

每次程序启动时,显示的图片都是相同的,这是因为random_shuffle在随机打乱元素时,需要设置随机数种子,否则默认使用的种子是相同的,就导致每次打乱的结果都是相同的,所以每次程序启动时RecBox中显示的内容都是相同的,因此在randomPiction()调用之前需要设置随机数种子。

QQMusic类的initUi函数中新增:

cpp 复制代码
srand(time(NULL));
ui->recMusicBox->initRecBoxUi(randomPiction(), 1);
ui->supplyMuscBox->initRecBoxUi(randomPiction(), 2);

commonPage页面

1、commonPage页面分析

我的音乐下的:我喜欢、本地下载、最近播放三个按钮表面上看对应三个Page页面,分析之后发现,这三个Page页面实际是雷同的,因此只需要定义一个页面CommonPage,将stackedWidget中这三个页面的类型提升为CommonPage即可。

①页面说明,比如:本地音乐,该部分实际就是QLabel的提示说明;

②正在播放音乐图片和播放全部按钮;

③音乐列表中每个部分的文本提示,实际就是三个QLabel

④本页面对应的音乐列表,即QListWidget。

2、commonPage界面设计和显示

把connomPage的界面布局和QSS样式设置好后分析。

CommonPage页面是我喜欢、本地下载、最近播放三个界面的共同类型,因此该类需要提供设置:pageTittle和 musicImageLabel的公共方法,将来在程序启动时完成三个界面信息的设置,因此CommonPage类需要添加一个public的setCommonPageUI函数。

cpp 复制代码
// commonpage.cpp 中新增
void CommonPage::setCommonPageUI(const QString& title, const QString& image)
{
    // 设置标题
    ui->pageTittle->setText(title);
    // 设置封⾯栏
    ui->musicImageLabel->setPixmap(QPixmap(image));
    ui->musicImageLabel->setScaledContents(true);
}

界面设置的函数需要在程序启动时就完成好配置,即需要在QQMusic的initUi(函数中调用完成设置:

cpp 复制代码
//在Widget::initUi()中新增
// 设置我喜欢、本地⾳乐、最近播放⻚⾯
ui->likePage->setCommonPageUI("我喜欢", ":/images/ilikebg.png");
ui->localPage->setCommonPageUI("本地⾳乐", ":/images/localbg.png");
ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");

自定义ListItemBox

1、ListItemBox界面分析

CommonPage页面创建好之后,等音乐加载到程序之后,就可以将音乐信息往CommonPage的

pageMusicList中显示了。

上图每行都是QListWidget中的一个元素,每个元素中包含多个控件:

①收藏图标,即QLabel

②歌曲名称,即QLabel

③VIP和SQ,VIP即收费会员专享,SQ为无损音乐,也是两个QLabel

④歌手名称,即QLabel

⑤音乐专辑名称,即QLabel

此处,需要将上述所有QLabel组合在一起,作为一个独立的控件,添加到QListWidget中,因此该控件也需要自定义。

2、ListItemBox显示测试

设置完ListItemBox的界面布局和QSS样式表后,ListItemBox将来要添加到CommonPage页面中的QListWidget中,因此在CommonPage类的初始化方法中添加如下代码:

cpp 复制代码
void CommonPage::setCommonPageUI(const QString& title, const QString& image)
{
     // 设置标题
     ui->pageTittle->setText(title);
    
    // 设置封⾯栏
     ui->musicImageLabel->setPixmap(QPixmap(image));

     ui->musicImageLabel->setScaledContents(true);
    // 测试
    ListItemBox* listItemBox = new ListItemBox(this);
    QListWidgetItem* listWidgetItem = new QListWidgetItem(ui->pageMusicList);
    listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(), 45));
    ui->pageMusicList->setItemWidget(listWidgetItem, listItemBox);
}

3、支持hover效果

ListltemBox添加到CommonPage中的QListWidget之后,自带hover效果,但是背景颜色和界面不太搭配,此处重新实现hover效果,此处重写enterEvent和leaveEvent来实现hover效果。

cpp 复制代码
// listitembox.cpp 新增
void ListItemBox::enterEvent(QEvent* event)
{
    (void)event;
    setStyleSheet("background-color:#EFEFEF");
}
void ListItemBox::leaveEvent(QEvent* event)
{
    (void)event;
    setStyleSheet("");
}

自定义MusicSlider

由于QT内置的HorizontalSlider(水平滑竿)不是很好看,该控件也采用自定义。该控件比较简单,实际就是两个QFrame嵌套起来的,达到如下效果:

自定义VolumeTool

1、VolumeTool控件分析

①内部为类似MusicSlider控件+小圆球,圆球实际为一个QPushButton

音量大小文本显示,实际为QLabel

③QPushButton,点击之后在静音和取消静音切换

④一个倒三角,Qt未提供三角控件,该控件需要手动绘制,用来提示是播放控制区那个按钮按下

2、界面设计

该控件属于弹出窗口,即点击了主界面的音量调节按钮后,才需要弹出该界面,点击其他位置该界面自动隐藏。因此在窗口创建时,需要设置窗口为无边框以及为弹出窗口。

cpp 复制代码
// VolumeTool.cpp 的构造函数中添加如下代码
#include <QGraphicsDropShadowEffect>
VolumeTool::VolumeTool(QWidget* parent) :
    QWidget(parent),
    ui(new Ui::VolumeTool)
{
    ui->setupUi(this);
    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);
    // 给按钮设置图标
    ui->silenceBtn->setIcon(QIcon(":/images/volumn.png"));
    // ⾳量的默认⼤⼩是20
    ui->outLine->setGeometry(ui->outLine->x(), 180 - 36 - 25, ui->outLine -> width(), 20);//根据自定义的控件大小来
    ui->silderBtn->move(ui->silderBtn->x(), ui->outLine->y() - ui -> silderBtn->height() / 2);
    ui->volumeRatio->setText("20%");
}

3、界面创建及弹出

音量调节属于主界面上元素,因此在QQMusic类中需要添加VolumeTool的对象,在initUi中new该类的对象。主界面中音量调节按钮添加clicked槽函数。

cpp 复制代码
// qqmusic.cpp中新增
void QQMusic::initUi()
{
    // ...

    // 创建⾳量调节窗⼝对象并挂到对象树
    volumeTool = new VolumeTool(this);
}
void QQMusic::on_volume_clicked()
{
    // 先要调整窗⼝的显⽰位置,否则该窗⼝在主窗⼝的左上⻆
    // 1. 获取该按钮左上⻆的左标
    QPoint point = ui->volume->mapToGlobal(QPoint(0, 0));
    // 2. 计算volume窗⼝的左上⻆位置
    // 让该窗⼝显⽰在⿏标点击的正上⽅
    // ⿏标位置:减去窗⼝宽度的⼀半,以及⾼度恰巧就是窗⼝的左上⻆
    QPoint volumeLeftTop = point - QPoint(volumeTool->width() / 2, volumeTool -> height());
    // 微调窗⼝位置
    volumeLeftTop.setY(volumeLeftTop.y() + 30);
    volumeLeftTop.setX(volumeLeftTop.x() + 15);

    // 3. 移动窗⼝位置
    volumeTool->move(volumeLeftTop);
    // 4. 将窗⼝显⽰出来
    volumeTool->show();
}

4、绘制三角

由于Qt中并未给出三角控件,因此三角需要手动绘制,故在VolumeTool类中重写paintEvent事件函

数。

cpp 复制代码
// volumetool.cpp中新增
#include <QPainter>
void VolumeTool::paintEvent(QPaintEvent* event)
{
    (void)event;
    // 1. 创建绘图对象
    QPainter painter(this);
    // 2. 设置抗锯⻮
    painter.setRenderHint(QPainter::Antialiasing, true);
    // 3. 设置画笔
    // 没有画笔时:画出来的图形就没有边框和轮廓线
    painter.setPen(Qt::NoPen);
    // 4. 设置画刷颜⾊
    painter.setBrush(Qt::white);
    // 创建⼀个三⻆形
    QPolygon polygon;
    polygon.append(QPoint(30, 300));//坐标根据vooltool控件来确定
    polygon.append(QPoint(70, 300));
    polygon.append(QPoint(50, 320));
    // 绘制三⻆形
    painter.drawPolygon(polygon);
}

音乐管理

1、音乐加载

QQMusic类中给addLocal添加槽函数。音乐文件在磁盘中,可以借助QFileDialog类完成音乐文件加载。

cpp 复制代码
// qqmusic.cpp 中新增
#include <QDir>
#include <QFileDialog>
void QQMusic::on_addLocal_clicked()
{
    // 1. 创建⼀个⽂件对话框
    QFileDialog fileDialog(this);
    fileDialog.setWindowTitle("添加本地⾳乐");
    // 2. 创建⼀个打开格式的⽂件对话框
    fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
    // 3. 设置对话框模式
    // 只能选择⽂件,并且⼀次性可以选择多个存在的⽂件
    fileDialog.setFileMode(QFileDialog::ExistingFiles);
    // 4. 设置对话框的MIME过滤器
    QStringList mimeList;
    mimeList << "application/octet-stream";
    fileDialog.setMimeTypeFilters(mimeList);
    // 5. 设置对话框默认的打开路径,设置⽬录为当前⼯程所在⽬录
    QDir dir(QDir::currentPath());
    dir.cdUp();
    QString musicPath = dir.path() + "/QQMusic/musics/";
    fileDialog.setDirectory(musicPath);
    // 6. 显⽰对话框,并接收返回值
    // 模态对话框, exec内部是死循环处理
    if (fileDialog.exec() == QFileDialog::Accepted)
    {
        // 切换到本地⾳乐界⾯,因为加载完的⾳乐需要在本地⾳乐界⾯显⽰
        ui->stackedWidget->setCurrentIndex(4);
        // 获取对话框的返回值
        QList<QUrl> urls = fileDialog.selectedUrls();
        // 拿到歌曲⽂件后,将歌曲⽂件交由musicList进⾏管理
        // ...
    }
}

MusicList类

将来添加到播放器中的音乐比较多,可借助一个类对所有的音乐进行管理。

1、歌曲对象存储

每首音乐文件,将来需要获取其内部的歌曲名称、歌手、音乐专辑、歌曲时长等信息,因此在

MusicList类中,将所有的歌曲文件以Music对象方式管理起来。在QQMusic中,通过QFileDialog将一组音乐文件的url获取到之后,可以交给MusicList类来管理。但是QQMusic加载的二进制文件不一定全部都是音乐文件,因此MusicList类中需要对文件的MIME类型再次检测,以筛选出真正的音乐文件。

QMimeDatabase类是Qt中主要用于处理文件的MIME类型,经常用于:文件类型识别、文件过滤

、多媒体文件处理、文件导入导出、文件管理器,该类中的mimeTypeForFile函数可用于获取给定文件的MIME类型。

对于歌曲文件:
audio/mpeg:适用于mp3格式的音乐文件
audio/flac:无损压缩的音频文件,不会破坏任何原有的音频信息
audio/wav:表示wav格式的歌曲文件
上述歌曲文件格式,Qt的QMediaPlayer类都是支持的。

cpp 复制代码
// musiclist.h 中新增
#include <QVector>
QVector<Music> musicList; // Music类是⾃定义的C++类,描述歌曲相关信息
// 将QQMusic⻚⾯中读取到的⾳乐⽂件,检测是⾳乐⽂件后添加到musicList中
void addMusicByUrl(const QList<QUrl>& urls);
// musiclist.cpp中新增
void MusicList::addMusicByUrl(const QList<QUrl>& urls)
{
    for (auto musicUrl : urls)
    {
        // 由于添加进来的⽂件不⼀定是歌曲⽂件,因此需要再次筛选出⾳乐⽂件
        QMimeDatabase db;
        QMimeType mime = db.mimeTypeForFile(musicUrl.toLocalFile());
        if (mime.name() != "audio/mpeg" && mime.name() != "audio/flac")
        {
            continue;
        }
        // 如果是⾳乐⽂件,加⼊歌曲列表
        musicList.push_back(musicUrl);
    }
}

Music类

1、介绍

该用来描述一个音乐文件,比如:音乐名称、歌手名称、专辑名称、音乐持续时长,当在界面上点击收藏之后,音乐会被标记为喜欢,播放之后需要标记为历史记录。因此该类中至少需要以下成员:

cpp 复制代码
// music.h中新增
#include <QUrl>
#include <QString>
class Music
{
public:
    Music();
    Music(const QUrl& url);
    void setIsLike(bool isLike);
    void setIsHistory(bool isHistory);
    void setMusicName(const QString& musicName);
    void setSingerName(const QString& singerName);
    void setAlbumName(const QString& albumName);
    void setDuration(const qint64 duration);
    void setMusicUrl(const QUrl& url);
    void setMusicId(const QString& musicId);
    bool getIsLike();
    bool getIsHistory();
    QString getMusicName();
    QString getSingerName();
    QString getAlbumName();
    qint64 getDuration();
    QUrl getMusicUrl();
    QString getMusicId();
private:
    bool isLike; // 标记⾳乐是否为我喜欢
    bool isHistory; // 标记⾳乐是否播放过
    // ⾳乐的基本信息有:歌曲名称、歌⼿名称、专辑名称、总时⻓
    QString musicName;
    QString singerName;
    QString albumName;
    qint64 duration; // ⾳乐的持续时⻓,即播放总的时⻓
    // 为了标记歌曲的唯⼀性,给歌曲设置id
    // 磁盘上的歌曲⽂件经常删除或者修改位置,导致播放时找不到⽂件,或者重复添加
    // 此处⽤musicId来维护播放列表中⾳乐的唯⼀性
    QString musicId;
    QUrl musicUrl; // ⾳乐在磁盘中的位置
};

Music.cpp新增

cpp 复制代码
// music.cpp中新增
Music::Music()
    : isLike(false)
    , isHistory(false)
{}
void Music::setIsLike(bool isLike)
{
    this->isLike = isLike;
}
void Music::setIsHistory(bool isHistory)
{
    this->isHistory = isHistory;
}
void Music::setMusicName(const QString& musicName)
{
    this->musicName = musicName;
}
void Music::setSingerName(const QString& singerName)
{
    this->singerName = singerName;
}
void Music::setAlbumName(const QString& albumName)
{
    this->albumName = albumName;
}
void Music::setDuration(const qint64 duration)
{
    this->duration = duration;
}
void Music::setMusicUrl(const QUrl& url)
{
    this->musicUrl = url;
}
void Music::setMusicId(const QString& musicId)
{
    this->musicId = musicId;
}
bool Music::getIsLike()
{
    return isLike;
}
bool Music::getIsHistory()
{
    return isHistory;
}
QString Music::getMusicName()
{
    return musicName;
}
QString Music::getSingerName()
{
    return singerName;
}
QString Music::getAlbumName()
{
    return albumName;
}
qint64 Music::getDuration()
{
    return duration;
}
QUrl Music::getMusicUrl()
{
    return musicUrl;
}
QString Music::getMusicId()
{
    return musicId;
}

另外,该类还需要添加一个带有歌曲文件路径的构造函数,当给定有效音乐文件后,Music类需要负责将该音乐文件的元数据解析出来。为了保证Music对象的唯一性,给每个Music对象设置一个UUID。UUID,即通用唯一识别码(Universally UniqueIdentifier),确保在分布式系统中每个元素都有唯一的标识。UUID由一组32位数的16进制数字组成,形式为8-4-4-4-12的32个字符,比如:"550e8400-e29b-41d4-a716-446655440000"在Music对象查找和更新时,可以已通过对比UUID,来保证Music对象的唯一性。Qt中QUuid类可生成UUID。

cpp 复制代码
Music::Music(const QUrl& url)
    : isLike(false)
    , isHistory(false)
    , musicUrl(url)
{
    musicId = QUuid::createUuid().toString();
}

2、解析音乐文件源数据

对于每首歌曲,将来在界面上需要显示出:歌曲名称、歌手、专辑名称,在播放时还需要拿到歌曲总时长,因此在构造音乐对象时,就需要将上述信息解析出来。歌曲元数据解析,需要用到QMediaPlayer,该类也是用来进行歌曲播放的类。

//QMediaPlayer类中的setMedia()函数

// 功能:设置要播放的媒体源,媒体数据从中读取

// media: 要播放的媒体内容,⽐如⼀个视频或⾳频⽂件,该类提供了⼀个QUrl格式的单参构造

void setMedia(const QMediaContent& media, QIODevice* stream = nullptr)

//注意:该函数执⾏后⽴即返回,不会等待媒体加载完成,也不检查错误,如果在媒体加载时发⽣错

//误,会触发mediaStatusChanged和error信号

// 检测媒体源是否有效,如果是有效的返回true,否则返回false

bool isMetaDataAvailable() const;

//媒体元数据加载成功之后,可以通过QMediaObject类的metaData函数获取指定的媒体数据:

// 返回要获取的媒体数据key的值

QVariant QMediaObject::metaData(const QString& key) const;

该项目中需要获取媒体的标题、作者、专辑、持续时长。

音乐文件的mate数据解析代码如下:

cpp 复制代码
// music.h 中新增
private:
 void parseMediaMetaData();
// music.cpp 中新增
#include <QMediaPlayer>
#include <QCoreApplication>
#include <QUuid>
void Music::parseMediaMetaData()
{
    // 解析时候需要读取歌曲数据,读取歌曲⽂件需要⽤到QMediaPlayer类
    QMediaPlayer player;
    player.setMedia(musicUrl);
    // 媒体元数据解析需要时间,只有等待解析完成之后,才能提取⾳乐信息,此处循环等待
    // 循环等待时:主界⾯消息循环就⽆法处理了,因此需要在等待解析期间,让消息循环继续处理
    while (!player.isMetaDataAvailable())
    {
        QCoreApplication::processEvents();
    }
    // 解析媒体元数据结束,提取元数据信息
    if (player.isMetaDataAvailable())
    {
        musicName = player.metaData("Title").toString();
        singerName = player.metaData("Author").toString();
        albumName = player.metaData("AlbumTitle").toString();
        duration = player.duration();
        if (musicName.isEmpty())
        {
            musicName = "歌曲未知";
        }
        if (singerName.isEmpty())
        {
            singerName = "歌⼿未知";
        }
        if (albumName.isEmpty())
        {
            albumName = "专辑名未知";
        }
        qDebug() << musicName << " " << singerName << " " << albumName << " " << duration;
    }
}
// 该函数需要在Music的构造函数中调⽤,当创建⾳乐对象时,顺便完成歌曲⽂件的加载
Music::Music(const QUrl& url)
    : isLike(false)
    , isHistory(false)
    , musicUrl(url)
{
    musicId = QUuid::createUuid().toString();
    parseMediaMetaData();
}

3、Music数据保存

通过QFileDialog将音乐从本地磁盘加载到程序中后,拿到的是所有音乐文件的QUrl,而在程序中需要的是经过元数据解析之后的Music对象,并且Music对象需要管理起来,此时就可以采用MusicList类对解析之后的Music对象进行管理,QQMusic类中只需要保存MusicList的对象,就可以让qqMusic.ui界面中CommonPage对象完成Music信息往界面更新。

cpp 复制代码
// qqmusic.h 新增
#include "musiclist.h"
MusicList musicList;
// qqmusic.cpp 
void QQMusic::on_addLocal_clicked()
{
    // ....
    // 6. 显⽰对话框,并接收返回值
    // 模态对话框, exec内部是死循环处理
    if (fileDialog.exec() == QFileDialog::Accepted)
    {
        // 切换到本地⾳乐界⾯,因为加载完的⾳乐需要在本地⾳乐界⾯显⽰
        ui->stackedWidget->setCurrentIndex(4);
        // 获取对话框的返回值
        QList<QUrl> urls = fileDialog.selectedUrls();

        // 拿到歌曲⽂件后,将歌曲⽂件交由musicList进⾏管理
        musicList.addMusicByUrl(urls);
        // 更新到本地⾳乐列表
        ui->localPage->reFresh(musicList);
    }
}

4、音乐分类

QQMusic中,有三个显示歌曲信息的页面:

likePage:管理和显示点击小v心心后收藏的歌曲

localPage:管理和显示本地加载的歌曲

recentPage:管理和显示历史播放过的歌曲

这三个页面的类型都是CommonPage,每个页面应该维护自己页面中的歌曲。因此CommonPage类中需要新增:

cpp 复制代码
// commonpage.h中新增
// 区分不同page⻚⾯
enum PageType
{
    LIKE_PAGE, // 我喜欢⻚⾯
    LOCAL_PAGE, // 本地下载⻚⾯
    HISTORY_PAGE // 最近播放⻚⾯
};
class CommonForm : public QWidget
{
    // 新增成员函数
public:
    void setMusicListType(PageType pageType);
    // 新增成员变量
private:
    // 歌单列表
    QVector<QString> musicListOfPage; // 具体某个⻚⾯的⾳乐,将来只需要存储⾳乐的id即可
    PageType pageType; // 标记属于likePage、localPage、recentPage哪个⻚⾯
};
// commonpage.cpp中新增:
void CommonPage::setMusicListType(PageType pageType)
{
    this->pageType = pageType;
}
// qqmusic.cpp中新增:
void initUi()
{
    // ...
    // 设置CommonPage的信息
    ui->likePage->setMusicListType(PageType::LIKE_PAGE);
    ui->likePage->setCommonPageUI("我喜欢", ":/images/ilikebg.png");
    ui->localPage->setMusicListType(PageType::LOCAL_PAGE);
    ui->localPage->setCommonPageUI("本地⾳乐", ":/images/localbg.png");
    ui->recentPage->setMusicListType(PageType::HISTORY_PAGE);
    ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");
}

QQMusic中,点击addLocal(本地加载)按钮后,会通过其musicList成员变量,将music添加到

musicList中管理,在添加过程中,每个歌曲会对应一个Music对象,Music对象在构造时,会完成歌曲文件的加载,顺便完成歌曲名称、作者、专辑名称等元数据的解析。一切准备就绪之后,每个

CommonPage页面,通过QQMusic的musicList分离出自己页面的歌曲,保存在musicListOfPage

中。

cpp 复制代码
// commonpage.h中新增:
#include "musiclist.h"
private:
    void addMusicToMusicPage(MusicList& musicList);


    // commonpage.cpp 中新增:
    void CommonPage::addMusicToMusicPage(MusicList& musicList)
    {
        // 将旧内容清空
        musicListOfPage.clear();
        for (auto& music : musicList)
        {
            switch (musicListType)
            {
            case LOCAL_LIST:
                musicListOfPage.push_back(music.getMusicId());
                break;
            case LIKE_LIST:
            {
                if (music.getIsLike())
                {
                    musicListOfPage.push_back(music.getMusicId());
                }
                break;
            }
            case HOSTORY_LIST:
            {
                if (music.getIsHistory())
                {
                    musicListOfPage.push_back(music.getMusicId());
                    break;
                }
            }
            default:
                break;
            }
        }
    }

由于musicList所属类,并不能直接支持范围for,因此需要在MusicList类中新增:

cpp 复制代码
// musiclist.h中新增:
typedef typename QVector<Music>::iterator iterator;
iterator begin();
iterator end();
// musiclist.cpp中新增:
iterator MusicList::begin()
{
    return musicList.begin();
}
iterator MusicList::end()
{
    return musicList.end();
}

5、更新Muic对象到CommonPage页面

步骤:

  1. 调用addMusicldPageFromMusicList函数,从musicList中添加当前页面的歌曲

2.遍历musicListOfPage,拿到每首音乐后先检查其是否在,存在则添加。

3.界面上需要更新每首歌曲的:歌曲名称、作者、专辑名称,而commonPage中只保存了歌曲的musicld,因此需要在MusicList中增加通过musicID查找Music对象的方法。

cpp 复制代码
// commonpage.h中新增
void reFresh(MusicList& musicList);
// commonpage.cpp 中新增:
void CommonPage::reFresh(MusicList& musicList)
{
    // 从musicList中分离出当前⻚⾯的所有⾳乐
    addMusicIdPageFromMusicList(musicList);
    // 遍历歌单,将歌单中的歌曲显⽰到界⾯
    for (auto musicId : musicListOfPage)
    {
        auto it = musicList.findMusicById(musicId);
        if (it == musicList.end())
            continue;
        ListItemBox* listItemBox = new ListItemBox(ui->pageMusicList);
        listItemBox->setMusicName(it->getMusicName());
        listItemBox->setSinger(it->getSingerName());
        listItemBox->setAlbumName(it->getAlbumName());
        listItemBox->setLikeIcon(it->getIsLike());
        QListWidgetItem* listWidgetItem = new QListWidgetItem(ui -> pageMusicList);
        listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(), 45));
        ui->pageMusicList->setItemWidget(listWidgetItem, listItemBox);
    }
    // 更新完成后刷新下界⾯
    repaint();
}
// musiclist.h中新增
iterator findMusicById(const QString& musicId);
// musiclist.cpp中新增
iterator MusicList::findMusicById(const QString& musicId)
{
    for (iterator it = begin(); it != end(); ++it)
    {
        if (it->getMusicId() == musicId)
        {
            return it;
        }
    }
    return end();
}

将歌曲名称、作者、专辑名称、喜欢图片等往ListBoxItem界面中更新时,需要ListBoxItem提供对应的set方法,因此需要在ListltemBox类中新增:

cpp 复制代码
// listitembox.h中新增:
public:
    void setMusicName(const QString& name);
    void setSinger(const QString& singer);
    void setAlbumName(const QString& albumName);
    void setLikeIcon(bool like);

private:
    bool isLike;

    // listitembox.cpp中新增:
    ListItemBox::ListItemBox(QWidget* parent) :
        QWidget(parent),
        ui(new Ui::ListItemBox),
        isLike(false) // 默认设置为false,⾳乐加载上来之后,点击了⼩⼼ 才为true
    {
        ui->setupUi(this);
    }
    void ListItemBox::setMusicName(const QString& name)
    {
        ui->musicNameLabel->setText(name);
    }
    void ListItemBox::setSinger(const QString& singer)
    {
        ui->musicSingerLabel->setText(singer);
    }
    void ListItemBox::setAlbumName(const QString& albumName)
    {
        ui->musicAlbumLabel->setText(albumName);
    }
    void ListItemBox::setLikeIcon(bool like)
    {
        isLike = like;
        if (isLike)
        {
            ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
        }
        else
        {
            ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));
        }
    }

更新音乐信息到界面的函数处理完成之后,需要在QQMusic的addLocal槽函数最后调用。

cpp 复制代码
// qqmusic.cpp中新增:
void QQMusic::onAddLocalClick()
{
    // ...
    // 6. 显⽰对话框,并接收返回值
    // 模态对话框, exec内部是死循环处理
    if (fileDialog.exec() == QFileDialog::Accepted)
    {
        // 切换到本地⾳乐界⾯,因为加载完的⾳乐需要在本地⾳乐界⾯显⽰
        ui->stackedWidget->setCurrentIndex(4);
        // 获取对话框的返回值
        QList<QUrl> urls = fileDialog.selectedUrls();
        // 注意:后序需要将⾳乐信息添加到数据库,否则每次打开是都需要添加⾳乐太⿇烦了
        musicList.addMusicByUrl(urls);
        // 更新到本地⾳乐列表
        ui->localPage->reFresh(musicList);
    }
}

音乐收藏(点击小心心)

1、收藏图标处理

当CommonPage往界面更新Music信息时,也要根据Music的isLike属性更新对应的图标。因此

ListItemBox需要根据当点击我喜欢按钮之后,要切换ListItemBox中的小心心。因此ListItemBox中添加设置bool类型isLike成员变量,以及setIsLike函数,在CommonPage添加Music信息到界面时,要能够设置小心心图片。

cpp 复制代码
// listItemBox.h 中新增
bool isLike;
void setLikeMusic(bool isLike);
// listItemBox.cp 中新增
ListItemBox::ListItemBox(QWidget* parent) :
    QWidget(parent),
    ui(new Ui::ListItemBox),
    isLike(false)
{
    ui->setupUi(this);
}
void ListItemBox::setLikeMusic(bool isLike)
{
    this->isLike = isLike;
    if (isLike)
    {
        ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
    }
    else
    {
        ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));
    }
}

2、点击收藏按钮处理

当喜欢某首歌曲时,可以点击界面上红色小心心收藏该首歌曲。我喜欢按钮中应该有以下操作:

1.更新小心心图标

2.更新Music的我喜欢属性,但ListItemBox并没有歌曲数据,所以只能发射信号,让其父元素

CommonPage来处理

cpp 复制代码
// listItemBox.h 中新增
public:
    void onLikeBtnClicked(); // 按钮点击槽函数

signals:
    void setIsLike(bool); // 通知更新歌曲数据信号

    // ListItemBox.cpp 中新增
    ListItemBox::ListItemBox(QWidget* parent) :
        QWidget(parent),
        ui(new Ui::ListItemBox),
        isLike(false)
    {
        ui->setupUi(this);

        // likeBtn按钮连接其点击槽函数
        connect(ui->likeBtn, &QPushButton::clicked, this,&ListItemBox::onLikeBtnClicked);
    }
    void ListItemBox::onLikeBtnClicked()
    {
        isLike = !isLike;
        setIsLike(isLike);
        emit setIsLike(isLike);
    }

3.CommonPage在往QListWidget中添加元素时,会创建一个个ListItemBox对象,每个对象将来都

可能会发射setLikeMusic信号,因此在将ListItemBox添加完之后,CommonPage应该关联先该信

号,将需要更新的的Music信息以及是否喜欢,同步给QQMusiC。

cpp 复制代码
// commonpage.h中新增
signals:
    void updateLikeMusic(bool isLike, QString musicId);

    // commonpage.cpp中新增
    // 该⽅法负责将歌曲信息更新到界⾯
    void CommonPage::reFresh(MusicList& musicList)
    {
        // ...
        for (auto musicId : musicOfPage)
        {
            // ...
            QListWidgetItem* item = new QListWidgetItem(ui->pageMusicList);
            item->setSizeHint(QSize(listItemBox->width(), listItemBox->height()));
            ui->pageMusicList->setItemWidget(item, listItemBox);

            // 接收ListItemBox发射的setLikeMusic信号
            connect(listItemBox, &ListItemBox::setIsLike, this, [=](bool isLike) {
                emit updateLikeMusic(isLike, it->getMusicId());
                });
        }
        ui->pageMusicList->repaint();
    }

QQMusic收到CommonPage发射的updateLikePage信号后,通知其上的likePage、localPage、

recentPage更新其界面的我喜欢歌曲信息。

cpp 复制代码
// qqmusic.h 新增
void onUpdateLikeMusic(bool isLike, QString musicId); // 响应CommonPage发射updateLikeMusic信号
// qqmusic.cpp新增
void QQMusic::connectSignalAndSlots()
{
    // ...

    // 关联CommonPage发射的updateLikeMusic信号
    connect(ui->likePage, &CommonPage::updateLikeMusic, this,
        &QQMusic::onUpdateLikeMusic);
    connect(ui->localPage, &CommonPage::updateLikeMusic, this,
        &QQMusic::onUpdateLikeMusic);
    connect(ui->recentPage, &CommonPage::updateLikeMusic, this,
        &QQMusic::onUpdateLikeMusic);
}
void QQMusic::onUpdateLikeMusic(bool isLike, QString musicId)
{
    // 1. 找到该⾸歌曲,并更新对应Music对象信息
    auto it = musicList.findMusicByMusicId(musicId);
    if (it != musicList.end())
    {
        it->setIsLike(isLike);
    }
    // 2. 通知三个⻚⾯更新⾃⼰的数据
    ui->likePage->reFresh(musicList);
    ui->localPage->reFresh(musicList);
    ui->recentPage->reFresh(musicList);
}

3、歌曲重复显示问题

cpp 复制代码
// commonpage.cpp修改
void CommonPage::addMusicToMusicPage(MusicList& musicList)
{
    musicOfPage.clear();

    // ...
}
void CommonPage::reFresh(MusicList& musicList)
{
    ui->pageMusicList->clear();
    //...
}

音乐播放控制

1、播放媒体和播放列表初始化

cpp 复制代码
#include <QMediaPlayer>
#include <QMediaPlaylist>
// qqmusic.h 新增
public:
    void initPlayer(); // 初始化媒体对象
private:
    //播放器相关
    QMediaPlayer* player;
    // 要多⾸歌曲播放,以及更复杂的播放设置,需要给播放器设置媒体列表
    QMediaPlaylist* playList;
    // qqmusic.cpp 中添加
    QQMusic::QQMusic(QWidget* parent)
        : QWidget(parent)
        , ui(new Ui::QQMusic)
    {
        ui->setupUi(this);
        // 窗⼝控件的初始化⼯作
        initUI();
        // 初始化播放器
        initPlayer();
        // 关联所有信号和槽
        connectSignalAndSlot();
    }
    void QQMusic::initPlayer()
    {
        // 创建播放器
        player = new QMediaPlayer(this);
        // 创建播放列表
        playList = new QMediaPlaylist(this);
        // 设置播放模式:默认为循环播放
        playList->setPlaybackMode(QMediaPlaylist::Loop);
        // 将播放列表设置给播放器
        player->setPlaylist(playList);
        // 默认⾳量⼤⼩设置为20
        player->setVolume(20);
    }

2、播放列表设置

播放之前,先要将歌曲加入用于播放的媒体列表,由于每个CommonPage页面的歌曲不同,因此

CommonPage中新增将其页面歌曲添加到模仿列表的方法。

cpp 复制代码
// commonpage.h 中新增
#include <QMediaPlaylist>
void addMusicToPlayer(MusicList& musicList, QMediaPlaylist* playList);
// commonpage.cpp 中新增
void CommonPage::addMusicToPlayer(MusicList& musicList, QMediaPlaylist* playList)
{
    // 根据⾳乐列表中⾳乐所属的⻚⾯,将⾳乐添加到playList中
    for (auto music : musicList)
    {
        switch (pageType)
        {
        case LOCAL_PAGE:
        {
            playList->addMedia(music.getMusicUrl());
            break;
        }
        case LIKE_PAGE:
        {
            if (music.getIsLike())
            {
                playList->addMedia(music.getMusicUrl());
            }
            break;
        }
        case HISTORY_PAGE:
        {
            if (music.getIsHistory())
            {
                playList->addMedia(music.getMusicUrl());
            }
            break;
        }
        default:
            break;
        }
    }
}

3、播放和暂停

当点击播放和暂停按钮时,播放状态应该在播放和暂停之间切换。播放器的状态如下,刚开始为停止状态QMediaPlayer的播放状态有:PlayingState()、PausedState()、StoppedState()。

cpp 复制代码
// qqmusic.h 中新增
// 播放控制区域
void onPlayCliked(); // 播放按钮
// qqmusic.cpp 中新增
void QQMusic::onPlayCliked()
{
    qDebug() << "播放按钮点击";
    if (player->state() == QMediaPlayer::PlayingState) {
        // 如果是歌曲正在播放中,按下播放键,此时应该暂停播放
        player->pause();
    }
    else if (player->state() == QMediaPlayer::PausedState)
    {
        // 如果是暂停状态,按下播放键,继续开始播放
        player->play();
    }
    else if (player->state() == QMediaPlayer::StoppedState)
    {
        player->play();
    }
}
void QQMusic::connectSignalAndSlots()
{
    // ...
    // 播放控制区的信号和槽函数关联
    connect(ui->play, &QPushButton::clicked, this, &QQMusic::onPlayMusic);
}

注意:播放时默认是从播放列表索引为0的歌曲开始播放的。

另外播放状态改变的时候,需要修改播放按钮上图标,图片的修改可以在onPlayCliked函数中设置,也可以拦截QMediaPlayer中的stateChanged信号,当播放状态改变的时候,QMediaPlayer会触发该信号,在stateChanged信号中修改播放按钮也可以,此处拦截stateChanged信号。

cpp 复制代码
// qqmusic.h 新增
// QMediaPlayer信号处理
// 播放状态发⽣改变
void onPlayStateChanged();

// qqmusic.cpp 新增
// QMediaPlayer信号关联槽函数
void QQMusic::onPlayStateChanged()
{
    qDebug() << "播放状态改变";
    if (player->state() == QMediaPlayer::PlayingState)
    {
        // 播放状态
        ui->play->setIcon(QIcon(":/images/play_on.png"));
    }
    else
    {
        // 暂停状态
        ui->play->setIcon(QIcon(":/images/play3.png"));
    }
}
void QQMusic::initPlayer()
{
    // ...

    // QMediaPlayer信号和槽函数关联
    // 播放状态改变时:暂停和播放之间切换
    connect(player, &QMediaPlayer::stateChanged, this, &QQMusic::onPlayStateChanged);
}

播放和暂停切换的时候,按钮上的图标有重叠,是因为之前在界面设置的时候,为了能看到效果,给按钮添加了背景图片,背景图片和图标是两种属性,都设置时就ui叠加,因此将按钮上个添加背景图片样式去除掉。

cpp 复制代码
void QQMusic::initUi()
{
    // 按钮的背景图⽚样式去除掉之后,需要设置默认图标
    // 播放控制区按钮图标设定
    ui->play->setIcon(QIcon(":/images/play_2.png")); // 默认为暂停图标
    ui->playMode->setIcon(QIcon(":/images/shuffle_2.png")); // 默认为随机播放
    volumeTool = new VolumeTool(this);
}

4、上一曲和下一曲

播放列表中,提供了previous()和next()函数,通过设置前一个或者下一个歌曲为当前播放源,player就会播放对应的歌曲。

cpp 复制代码
// qqmusic.h 新增
void onPlayUpCliked(); // 上⼀曲
void onPlayDownCliked(); // 下⼀曲
// qqmusic.cpp 新增
void QQMusic::onPlayUpCliked()
{
    playList->previous();
}
void QQMusic::onPlayDownCliked()
{
    playList->next();
}
void QQMusic::connectSignalAndSlots()
{
    // ...
    // 播放控制区的信号和槽函数关联
    connect(ui->play, &QPushButton::clicked, this, &QQMusic::onPlayMusic);
    connect(ui->playUp, &QPushButton::clicked, this, &QQMusic::onPlayUpClicked);
    connect(ui->playDown, &QPushButton::clicked, this, &QQMusic::onPlayDownClicked);
}

4、切换播放模式

cpp 复制代码
// qqmusic.h 中新增
void onPlaybackModeCliked(); // 播放模式设置
// qqmusic.cpp 中新增
void QQMusic::initPlayer()
{
    // ...
    // 设置播放模式
    connect(ui->playMode, &QPushButton::clicked, this,&QQMusic::onPlaybackModeCliked);
}
void QQMusic::onPlaybackModeCliked()
{
    // 播放模式是针对播放列表的
    // 播放模式⽀持:循环播放、随机播放、单曲循环三种模式
    if (playList->playbackMode() == QMediaPlaylist::Loop)
    {
        // 列表循环
        ui->playMode->setToolTip("随机播放");
        playList->setPlaybackMode(QMediaPlaylist::Random);
    }
    else if (playList->playbackMode() == QMediaPlaylist::Random)
    {
        // 随机播放
        ui->playMode->setToolTip("单曲循环");
        playList->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
    }
    else if (playList->playbackMode() == QMediaPlaylist::CurrentItemInLoop)
    {
        ui->playMode->setToolTip("列表循环");
        playList->setPlaybackMode(QMediaPlaylist::Loop);
    }
    else
    {
        qDebug() << "播放模式错误";
    }
}

播放模式切换时会触发playbackModeChanged信号,在该信号对应槽函数中,完成图片切换。

cpp 复制代码
// qqmusic.h 中新增
// 播放模式切换槽函数
void onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode);
// qqmusic.cpp 中新增
void QQMusic::onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode)
{
    if (playbackMode == QMediaPlaylist::Loop)
    {
        ui->playMode->setIcon(QIcon(":/images/list_play.png"));
    }
    else if (playbackMode == QMediaPlaylist::Random)
    {
        ui->playMode->setIcon(QIcon(":/images/shuffle_2.png"));
    }
    else if (playbackMode == QMediaPlaylist::CurrentItemInLoop)
    {
        ui->playMode->setIcon(QIcon(":/images/single_play.png"));
    }
    else
    {
        qDebug() << "暂不⽀持该模式";
    }
}
void QQMusic::initPlayer()
{
    // ...

    // 播放列表的模式放⽣改变时的信号槽关联
    connect(playList, &QMediaPlaylist::playbackModeChanged, this,
        &QQMusic::onPlaybackModeChanged);
}

5、播放所有

播放所有按钮属于CommonPage中的按钮,其对应的槽函数添加在CommonPage类中,但是

CommonPage不具有音乐播放的功能,因此当点击播放所有按钮后之后,播放所有的槽函数应该发射出信号,让QQMusic类完成播放。由于likePage、localPage、recentPage三个CommonPage页面都有playAllBtn,因此该信号需要带上PageType参数,需要让QQMusic在处理该信号时,知道播放哪个页面的歌曲。

cpp 复制代码
// commonpage.h 中新增加
signals:
    // 该信号由QQMusic处理--在构函数中捕获
    void playAll(PageType pageType);
    // commonpage.cpp 中修改
    CommonPage::CommonPage(QWidget* parent) :
        QWidget(parent),
        ui(new Ui::CommonPage)
    {
        ui->setupUi(this);
        // playAllBtn按钮的信号槽处理
        // 当播放按钮点击时,发射playAll信号,播放当前⻚⾯的所有歌曲
        // playAll信号交由QQMusic中处理
        connect(ui->playAllBtn, &QPushButton::clicked, this, [=]() {
            emit playAll(pageType);
            });

        // ...
    }

在QQMusic中,给playAll信号关联槽函数,并播放当前Page页面的所有音乐。playAll槽函数中,根据pageType将当前page页面记录下来,默认从该页面的第0首歌曲开始播放。注意不要忘记关联信号槽。

cpp 复制代码
// qqmusic.h 中新增
// 播放所有信号的槽函数
#include "commonpage.h"
void onPlayAll(PageType pageType);
void playAllOfCommonPage(CommonPage* commonPage, int index);
// qqmusic.cpp 中新增
void QQMusic::onPlayAll(PageType pageType)
{
    CommonPage* page = nullptr;
    switch (pageType)
    {
    case PageType::LIKE_PAGE:
        page = ui->likePage;
        break;
    case PageType::LOCAL_PAGE:
        page = ui->localPage;
        break;
    case PageType::HOSTORY_PAGE:
        page = ui->recentPage;
        break;
    default:
        qDebug() << "扩展";
    }
    // 从当前⻚⾯的零号位置开始播放
    playAllOfCommonPage(page, 0);
}
void QQMusic::playAllOfCommonPage(CommonPage* commonPage, int index)
{
    // 播放page所在⻚⾯的⾳乐
    // 将播放列表先清空,否则⽆法播放当前CommonPage⻚⾯的歌曲
    // 另外:该⻚⾯⾳乐不⼀定就在播放列表中,因此需要先将该⻚⾯⾳乐添加到播放列表
    playList->clear();
    // 将当前⻚⾯歌曲添加到播放列表
    page->addMusicToPlayer(musicList, playList);
    // 设置当前播放列表的索引
    playList->setCurrentIndex(index);
    // 播放
    player->play();
}
void QQMusic::connectSignalAndSlots()
{
    // ...
    // 关联播放所有的信号和槽函数
    connect(ui->likePage, &CommonPage::playAll, this, &QQMusic::onPlayAll);
    connect(ui->localPage, &CommonPage::playAll, this, &QQMusic::onPlayAll);
    connect(ui->recentPage, &CommonPage::playAll, this, &QQMusic::onPlayAll);
}

6、鼠标双击播放

当QListWidget中的项被双击时,会触发doubleClicked信号,该信号在QListWidget的基类中定义,有一个index参数,表示被双击的QListWidgetItem在QListWidget中的索引I,该索引刚好与QMediaPlaylist中歌曲的所以一致,被双击时直接播放该首歌曲即可。

cpp 复制代码
// CommonPage.h 中新增
signals:
    void playMusicByIndex(CommonPage*, int);
    // commonpage.cpp 中新增
    CommonPage::CommonPage(QWidget* parent) :
        QWidget(parent),
        ui(new Ui::CommonPage)
    {
        // ...

        connect(ui->pageMusicList, &QListWidget::doubleClicked, this, [=](const
            QModelIndex& index) {
                // ⿏标双击后,发射信号告诉QQMusic,博能放this⻚⾯中共被双击的歌曲
                emit playMusicByIndex(this, index.row());
            });
    }


    // qqmusic.h 中新增
    // CommonPage中playMusicByIndex信号对应槽函数
    void playMusicByIndex(CommonPage* page, int index);
    // qqmusic.cpp 中新增
    void QQMusic::playMusicByIndex(CommonPage* page, int index)
    {
        playAllMusicOfCommonPage(page, index);
    }
    void QQMusic::connectSignalAndSlots()
    {
        // ...
        // 处理likePage、localPage、recentPage中ListItemBox双击
        connect(ui->likePage, &CommonPage::playMusicByIndex, this,
            &QQMusic::playMusicByIndex);
        connect(ui->localPage, &CommonPage::playMusicByIndex, this,
            &QQMusic::playMusicByIndex);
        connect(ui->recentPage, &CommonPage::playMusicByIndex, this,
            &QQMusic::playMusicByIndex);
    }

7、同步最近播放的歌曲

当播放歌曲改变时,即播放的媒体源发生了变化,QMediaPlayer会触metaDataAvailableChanged信号,QMediaPlaylist也会触发currentIndexChanged信号,该信号会带index参数,表示现在是媒体播放列表中的index歌曲被播放,通过index可以获取到recentPage页面中具体播放的歌曲,将该歌曲对应Music对象的isHistoty属性修改为true,然后更新下rencentPage的歌曲列表,播放过的歌曲就添加到历史播放页面中了。

问题:获取likePage、localPage、recentPage哪个CommonPage页面中的歌曲呢?

答案:QQMusic类中维护CommonPage*变量currentPage,记录当前正在播放的CommonPage页

面,初始时设置为localPage,当播放的页面发生改变时,修改currentPage为当前正在播放页面,其中点击播放所有按钮以及双击QListWidget中项的时候都回引起currentPage的改变。

cpp 复制代码
// qqmusic.h 中新增
CommonPage* curPage;
// qqmusic.cpp 中修改
void QQMusic::initUi()
{
    // ...

    // 将localPage设置为当前⻚⾯
    ui->stackedWidget->setCurrentIndex(4);
    currentPage = ui->localPage;

    // ...
}
void QQMusic::playAllOfCommonPage(CommonPage* commonPage, int index)
{
    currentPage = commonPage;
    // 播放page所在⻚⾯的⾳乐
    // 将播放列表先清空,否则⽆法播放当前CommonPage⻚⾯的歌曲
    // 另外:该⻚⾯⾳乐不⼀定就在播放列表中,因此需要先将该⻚⾯⾳乐添加到播放列表
    playList->clear();

    // ...
}

准备工作完成之后,同步最近播放歌曲的逻辑实现如下:

cpp 复制代码
// qqmusic.h 中新增
// ⽀持播放历史记录
void onCurrentIndexChanged(int index);

// qqmusic.cpp 中新增
void QQMusic::initPlayer(int index)
{
    // ...

    // 播放列表项发⽣改变,此时将播放⾳乐收藏到历史记录中
    connect(playList, &QMediaPlaylist::currentIndexChanged, this,
        &QQMusic::onCurrentIndexChanged);
}
void QQMusic::onCurrentIndexChanged(int index)
{
    // ⾳乐的id都在commonPage中的musicListOfPage中存储着
    const QString& musicId = currentPage->getMusicIdByIndex(index);
    // 有了MusicId就可以再musicList中找到该⾳乐
    auto it = musicList.findMusicByMusicId(musicId);
    if (it != musicList.end())
    {
        // 将该⾳乐设置为历史播放记录
        it->setIsHistory(true);
    }
    ui->recentPage->reFresh(musicList);
}
// commmonpage.h 中新增
const QString& getMusicIdByIndex(int index) const;
// commonpage.cpp 中新增
QString CommonPage::getMusicIdByIndex(int index)
{
    if (index >= musicOfPage.size())
    {
        qDebug() << "⽆此歌曲";
        return "";
    }
    return musicOfPage[index];
}

8、音量设置

a、功能分析

当点击静音按钮时,音量应该在静音和非静音之间进行切换,并且按钮上图标需要同步切换。

鼠标在滑竿上点击或拖动滑竿时,应该跟进滑竿的高低比率,设置音量大小,同时修改界面音量比

率。

b. QMediaPlayer提供支持

QMediaPlayer中音量相关操作如下:

int volume; // 标记⾳量⼤⼩,值在0~100之间

int volume()const; // 获取⾳量⼤⼩

void setVolume(int); // 槽函数:设置⾳量⼤⼩

bool muted; // 是否静⾳,true为静⾳,false为⾮静⾳

bool isMuted()const; // 获取静⾳状态

bool setMuted(bool muted); // 槽函数:设置静⾳或⾮静⾳

c、静音和非静音

VolumeTool类中需要添加两个成员变量,并在构造函数中完成默认值的设置。

给静音按钮参加槽函数onSilenceBtnClicked,并在构造函数中connect按钮的clicked信号,当按

钮点击时候,调用setMuted(boolnuted)函数,完成静音和非静音的设置。

由于VolumeTool不具备媒体播放控制,因此当静音状态发生改变时,发射设置静音信号,让

QQMusic来处理。

cpp 复制代码
// volumetool.h 中新增
signals:
    void setSilence(bool); // 设置静⾳信号
    void onSilenceBtnClicked(); // 静⾳按钮槽函数
    bool isMuted; // 记录静⾳或⾮静⾳,当点击静⾳按钮时,在true和false之间切换
    int volumeRatio; // 标记⾳量⼤⼩
    // volumetool.cpp 中新增
    VolumeTool::VolumeTool(QWidget* parent) :
        QWidget(parent),
        ui(new Ui::VolumeTool),
        isMuted(false), // 默认静⾳
        volumeRatio(20) // 默认⾳量为20%
    {
        //...

        // 关联静⾳按钮的信号槽
        connect(ui->silenceBtn, &QPushButton::clicked, this,
            &VolumeTool::onSilenceBtnClicked);
    }
    void VolumeTool::onSilenceBtnClicked()
    {
        isMuted = !isMuted;
        if (isMuted)
        {
            ui->silenceBtn->setIcon(QIcon(":/images/silent.png"));
        }
        else
        {
            ui->silenceBtn->setIcon(QIcon(":/images/volumn.png"));
        }
        emit setSilence(isMuted);
    }
    // qqMusic.h 中新增
    void setMusicSilence(bool isMuted);
    // qqmusic.cpp 中新增
    void QQMusic::setMusicSilence(bool isMuted)
    {
        player->setMuted(isMuted);
    }
    void QQMusic::connectSignalAndSlots()
    {
        // ...
        // 设置静⾳
        connect(volumeTool, &VolumeTool::setSilence, this,
            &QQMusic::setMusicSilence);
    }

d.鼠标按下、滚动以及释放事件处理

当鼠标在滑竿上按下时,需要设置sliderBtn和outLine的位置,当鼠标在滑竿上移动或者鼠标抬起时,需要设置SliderBtnoutLine结束的位置,即改变VolumeTool中滑竿的显示。具体修改播放媒体音量大小操作应该由于QQMusic负责处理,因此当鼠标移动或释放时,需要发射信号让QQMusic知道需要修改播放媒体的音量大小了。

cpp 复制代码
// volumetool.h 中新增
// 发射修改⾳量⼤⼩槽函数
void setMusicVolume(int);
// 事件过滤器
bool eventFilter(QObject* object, QEvent* event);
// volumetool.cpp 中新增

bool VolumeTool::eventFilter(QObject * object, QEvent * event)
{
    // 过滤volumeBox上的事件
    if (object == ui->volumeBox)
    {
        if (event->type() == QEvent::MouseButtonPress)
        {
            // 如果是⿏标按下事件,修改sliderBtn和outLine的位置,并计算
            volumeRation
                setVolume();
        }
        else if (event->type() == QEvent::MouseMove)
        {
            // 如果是⿏标滚动事件,修改sliderBtn和outLine的位置,并计算
            volumeRation,
                setVolume();
            // 并发射setMusicVolume信号
            emit setMusicVolume(volumeRatio);
        }
        else if (event->type() == QEvent::MouseButtonRelease)
        {
            // 如果是⿏标释放事件,直接发射setMusicVolume信号
            emit setMusicVolume(volumeRatio);
        }

        return true;
    }

    return QObject::eventFilter(object, event);
}
VolumeTool::VolumeTool(QWidget* parent) :
    QWidget(parent),
    ui(new Ui::VolumeTool),
    isMuted(false),
    volumeRatio(20)
{
    // ...

    // 安装事件过滤器
    ui->volumeBox->installEventFilter(this);
}

e.outLine和SliderBtn以及volumeRation更新

cpp 复制代码
// volumetool.h 中新增
// 根据⿏标在滑竿上滑动更新滑动界⾯,并按照⽐例计算⾳量⼤⼩
void setVolume();
// volumetool.cpp 中新增
void VolumeTool::setVolume()
{
    // 1. 将⿏标的位置转换为sloderBox上的相对坐标,此处只要获取y坐标
    int height = ui->volumeBox->mapFromGlobal(QCursor().pos()).y();
    // 2. ⿏标在volumeBox中可移动的y范围在[25, 205之间]
    height = height < 25 ? 25 : height;
    height = height > 205 ? 205 : height;
    // 3. 调整sloderBt的位置
    ui->silderBtn->move(ui->silderBtn->x(), height - ui->silderBtn -> height() / 2);
    // 4. 更新outline的位置和⼤⼩
    ui->outLine->setGeometry(ui->outLine->x(), height, ui->outLine->width(),205 - height);
    // 5. 计算⾳量⽐率
    volumeRatio = (int)((int)ui->outLine->height() / (float)180 * 100);
    // 6. 设置给label显⽰出来
    ui->volumeRatio->setText(QString::number(volumeRatio) + "%");
}

f.QQMusic类拦截VolumeTool发射的setMusicVolume信号,将音量大小设置为指定值。

cpp 复制代码
// qqmusic.h 中新增
void setPlayerVolume(int vomume); // 设置⾳量⼤⼩
// qqmusic.cpp 中新增
void QQMusic::setPlayerVolume(int volume)
{
    player->setVolume(volume);
}
void QQMusic::connectSignalAndSlots()
{
    // ...

    // 设置⾳量⼤⼩
    connect(volumeTool, &VolumeTool::setMusicVolume, this,
        &QQMusic::setPlayerVolume);
}

9、当前播放时间和总时间更新

a、界面歌曲总时间更新

歌曲总时间在Music对象中可以获取,也可以让player调用自己的duration()方法获取。但是这两种

获取歌曲总时间的调用时机不太好确定。我们期望的是当歌曲发生切换时,获取到正在播放歌曲的

总时长。当播放源的持续时长发生改变时,QMediaPlayer会触发durationChanged信号,该信号中提供了将要播放媒体的总时长。因此在QQMusic类中给该信号关联槽函数,在槽函数中将duration更新到界面总时间即可。

cpp 复制代码
// qqmusic.h 中新增
// 歌曲持续时⻓改变时[歌曲切换]
void onDurationChanged(qint64 duration);
// qqmusic.cpp 中新增
void QQMusic::onDurationChanged(qint64 duration)
{
    ui->totalTime->setText(QString("%1:%2").arg(duration / 1000 / 60, 2, 10,
        QChar('0'))
        .arg(duration / 1000 % 60, 2, 10,
            QChar('0')));
}
void QQMusic::initPlayer()
{
    // ....
    // 媒体持续时⻓更新,即:⾳乐切换,时⻓更新,界⾯上时间也要更新
    connect(player, &QMediaPlayer::durationChanged, this,
        &QQMusic::onDurationChanged);
}

b、界面歌曲当前播放时间更新

媒体在持续播放过程中,QMediaPlayer会发射positionChanged,该信号带有一个qint64类型参

数,表示媒体当前持续播放的时间。因此,在QQMusic中捕获该信号,便可获取到正在播放媒体的持续时间。

cpp 复制代码
// qqmusic.h 中新增
// 播放位置改变,即持续播放时间改变
void onPositionChanged(qint64 duration);
// qqmusic.cpp 中新增
void QQMusic::onPositionChanged(qint64 duration)
{
    ui->currentTime->setText(QString("%1:%2").arg(duration / 1000 / 60, 2, 10,
        QChar('0'))
        .arg(duration / 1000 % 60, 2, 10,
            QChar('0')));

    // 界⾯上的进度条也需要同步修改
}
void QQMusic::initPlayer()
{
    // ....
    // 播放位置发⽣改变,即已经播放时间更新
    connect(player, &QMediaPlayer::positionChanged, this,
        &QQMusic::onPositionChanged);
}

10、进度条处理

a、seek功能介绍

播放器的seek功能指,通过时间或位置快速定位到视频或音频流的特定位置,允许用户在播放过程中随时跳转到特定时间点,从而快速找到感兴趣的内容或重新开始播放。

b、进度条界面显示

进度条功能进度界面展示与音量调节位置类似,拦截鼠标按下、鼠标移动、以及鼠标释放消息即可。在内部捕获到鼠标的位置的横坐标x,将x作为outLine的宽度即可。即在鼠标按下、移动、释放的时候,修改outLine的宽度即可。

cpp 复制代码
// musicslider.h 中新增
void mousePressEvent(QMouseEvent * event); // 重写⿏标按下事件
void mouseMoveEvent(QMouseEvent * event); // 重写⿏标滚动事件
void mouseReleaseEvent(QMouseEvent * event); // 重写⿏标释放事件
void moveSilder(); // 修改outLine的宽度为currentPos

 private:
     int currentPos; // 滑动条当前位置
    
        
        // musicslider.cpp 中新增
        MusicSlider::MusicSlider(QWidget * parent) :
        QWidget(parent),
        ui(new Ui::MusicSlider)
    {
        ui->setupUi(this);
        // 初始情况下,还没有开始播放,将当前播放进度设置为0
        currentPos = 0;
        maxWidth = width();
        moveSilder();
    }
    void MusicSlider::mousePressEvent(QMouseEvent* event)
    {
        // 注意:QMouseEvent中的pos()为⿏标相对于widget的坐标,不是相当于screen
        // 因此⿏标位置的 x 坐标可直接作为outLine的宽度
        currentPos = event->pos().x();
        moveSilder();
    }
    void MusicSlider::mouseMoveEvent(QMouseEvent* event)
    {
        // 如果⿏标不在MusicSlider的矩形内,不进⾏拖拽
        QRect rect = QRect(0, 0, width(), height());
        QPoint pos = event->pos();
        if (!rect.contains(pos))
        {
            return;
        }
        // 根据⿏标滑动的位置更新outLine的宽度
        if (event->buttons() == Qt::LeftButton)
        {
            // 验证:⿏标点击的x坐标是否越界,如果越界将其调整到边界
            currentPos = event->pos().x();
            if (currentPos < 0)
            {
                currentPos = 0;
            }
            if (currentPos > maxWidth)
            {
                currentPos = maxWidth;
            }

                moveSilder();
        }
    }
    void MusicSlider::mouseReleaseEvent(QMouseEvent* event)
    {
        currentPos = event->pos().x();
        moveSilder();
    }
    void MusicSlider::moveSilder()
    {
        // 根据当前进度设置外部滑动条的位置
        ui->outLine->setMaximumWidth(currentPos);
        ui->outLine->setGeometry(0, 8, currentPos, 4);
    }

c、进度条同步持续播放时间

当鼠标释放之后,计算出进度条当前位置currentPos和总宽度的maxWidth比率,然后发射信号告诉QQMusic,让player按照该比率更新持续播放时间。

cpp 复制代码
// musicslider.h 新增
signals:
    void setMusicSliderPosition(float);
    // musicslider.cpp 中新增
    void MusicSlider::mouseReleaseEvent(QMouseEvent* event)
    {
        currentPos = event->pos().x();
        moveSilder();
        emit setMusicSliderPosition((float)currentPos / (float)maxWidth);
    }
    // qqmusic.h 中新增
    void onMusicSliderChanged(float value); // 进度条改变
    // qqmusic.cpp 中新增
    void QQMusic::onMusicSliderChanged(float value)
    {
        // 1. 计算当前seek位置的时⻓
        qint64 duration = (qint64)(totalDuration * value);

            // 2. 转换为百分制,设置当前时间
            ui->currentTime->setText(QString("%1:%2").arg(duration / 1000 / 60, 2, 10,
                QChar('0'))
                .arg(duration / 1000 % 60, 2, 10,
                    QChar('0')));
        // 3. 设置当前播放位置
        player->setPosition(duration);
    }
    void QQMusic::connectSignalAndSlots()
    {
        // ...
        // 进度条拖拽
        connect(ui->progressBar, &MusicSlider::setMusicSliderPosition, this,
            &QQMusic::onMusicSliderChanged);
    }

d、持续时间同步进度条

当播放位置更新时,界面上持续播放时间一直在更新,因此进度条也需要持续向前进。MusicSlider应该提供setStep函数,播放进度持续更新时,也将进度条通过setStep函数更新下。

cpp 复制代码
// musicslider.h 中新增
void setStep(float bf);
// musicslider.cpp 中新增
void MusicSlider::setStep(float bf)
{
    currentPos = maxWidth * bf;
    moveSilder();
}
// qqmusic.cpp 中修改
void QQMusic::onPositionChanged(qint64 duration)
{
    // 1. 更新已经播放时间
    ui->currentTime->setText(QString("%1:%2").arg(duration / 1000 / 60, 2, 10,
        QChar('0'))
        .arg(duration / 1000 % 60, 2, 10,
            QChar('0')));
    // 2. 进度条处理
   
        ui->progressBar->setStep((float)duration / (float)totalDuration);
}

e、歌曲名、歌手和封面时间同步

在进行歌曲切换时候,歌曲名称、歌手以及歌曲的封面图,也需要更新到界面。歌曲名称、歌手可以再Music对象中进行获取,歌曲的封面图可以通过player到歌曲的元数据中获取,获取时需要使

用"Thumbnaillmage"作为参数,注意有些歌曲可能没有封面图,如果没有设置一张默认的封面图。

由于歌曲切换时,player需要将新播放歌曲作为播放源,并解析歌曲文件,如果歌曲文件是有效的才能播放;因此QQMusic类可以给QMediaPlayer发射的metaDataAvailableChanged(bool))信

号关联槽函数,当歌曲更换时,完成信息的更新。

cpp 复制代码
// qqmusic.h中新增
void onMetaDataAvailableChangedChanged(bool available)
// qqmusic.cpp 中新增
void QQMusic::onMetaDataAvailableChangedChanged(bool available)
{
    // 播放源改变
    qDebug() << "歌曲切换";
    // 1. 从player播放歌曲的元数据中获取歌曲信息
    QString singer = player->metaData("Author").toStringList().join(",");
    QString musicName = player->metaData("Title").toString();
    if (musicName.isEmpty())
    {
        auto it = musicList.findMusicByMusicId(currentPage -
> getMusicIdByIndex(curPlayMusicIndex));
        if (it != musicList.end())
        {
            musicName = it->getMusicName();
            singer = it->getMusicSinger();
        }
    }
    // 2. 设置歌⼿、歌曲名称、专辑名称
    ui->musicName->setText(musicName);
    ui->musicSinger->setText(singer);

        // 3. 获取封⾯图⽚
        QVariant coverImage = player->metaData("ThumbnailImage");
    if (coverImage.isValid())
    {
        // 获取封⾯图⽚成功
        QImage image = coverImage.value<QImage>();
        // 设置封⾯图⽚
        ui->musicCover->setPixmap(QPixmap::fromImage(image));
        // 缩放填充到整个Label
        ui->musicCover->setScaledContents(true);
        currentPage->setImageLabel(QPixmap::fromImage(image));
    }
    else
    {
        // 设置默认图⽚-修改
        qDebug() << "歌曲没有封⾯图⽚";
    }
}
void CommonForm::setImageLabel(QPixmap pixMap)
{
    ui->musicImgLabel->setPixmap(pixMap);
    ui->musicImgLabel->setScaledContents(true);
}

Lrl歌词同步

1、界面分析

①和②为QLabel,分别显示作者和歌曲名称;

③~⑨均为QLabel,用来显示歌词,⑥为当前正在播放歌词,③④⑤为当前播放歌词的前三句,⑦⑧⑨为当前播放歌词的后三句。歌词会随着播放时间持续,从下往上移动。

①为按钮,点击之后窗口隐藏。

2、Lrc歌词显示

在LrcPage的构造函数中,将窗口的标题栏去除掉;并给hideBtn关联clicked信号,当按钮点击时将窗口隐藏。

cpp 复制代码
// lrcPage.cpp 中添加
LyricsPage::LyricsPage(QWidget* parent) :
    QWidget(parent),
    ui(new Ui::LyricsPage)
{
    ui->setupUi(this);
    setWindowFlag(Qt::FramelessWindowHint);
    connect(ui->hideBtn, &QPushButton::clicked, this, [=] {
        hide();
        });
    ui->hideBtn->setIcon(QIcon(":/images/xiala.png"));
}

在QQMusic中,创建LrcPage的指针,并在initUi)方法中创建窗口的对象,创建好之后将窗口隐藏起来;在QQMusic中,给IrcWord按钮添加槽函数,在槽函数中将窗口显示出来。

cpp 复制代码
// qqmusic.h 中添加
#include "lrcpage.h"
LrcPage* lrcPage;
void onLrcWordClicked();
// qqmusic.cpp 中添加
void QQMusic::initUI()
{
    // ...
    // 创建lrc歌词窗⼝
    lrcPage = new LrcPage(this);
    lrcPage->hide();
}
void QQMusic::onLrcWordClicked()
{

        lrcPage->show();
}
void QQMusic::connectSignalAndSlot()
{
    // ...

    // 显⽰歌词窗⼝
    connect(ui->lrcWord, &QPushButton::clicked, this,
        &QQMusic::onLrcWordClicked);
}

3、LcrPage添加动画效果

a、上移动画效果

①QQMusic的initUi函数中,创建IrcPage对象并将窗口隐藏;给IrcPage窗口添加上移动画,动画暂

不开启

②QQMusic中给"歌词"按钮添加槽函数,当按钮点击时,显示窗口,开启动画

cpp 复制代码
// qqmusic.h 中新增
#include <QPropertyAnimation>
// 歌词按钮槽函数
void onLrcWordClicked();
private:
    QPropertyAnimation* lrcAnimation;
    // qqmusic.cpp 中新增
    void QQMusic::initUi()
    {
        // ...
        // 窗⼝添加阴影效果
        QGraphicsDropShadowEffect* shadowEffect = new
            QGraphicsDropShadowEffect(this);
        shadowEffect->setOffset(0, 0);
        shadowEffect->setColor("#000000"); // ⿊⾊
        // 此处需要将圆⻆半径不能太⼤,否则动画效果有问题,可以设置为10
        shadowEffect->setBlurRadius(20);
        this->setGraphicsEffect(shadowEffect);

        // ...

            // 实例化LrcWord对象
            lrcPage = new LrcPage(this);
        lrcPage->hide();

        // lrcPage添加动画效果
        lrcAnimation = new QPropertyAnimation(lrcPage, "geometry", this);
        lrcAnimation->setDuration(250);
        lrcAnimation->setStartValue(QRect(10, 10 + lrcPage->height(),
            lrcPage->width(), lrcPage->height()));
        lrcAnimation->setEndValue(QRect(10, 10, lrcPage->width(), lrcPage -> height()));
    }
    // 显⽰窗⼝ 并 开启动画
    void QQMusic::onLrcWordClicked()
    {
        lrcPage->show();
        lrcAnimation->start();
    }
    void QQMusic::connectSignalAndSlots()
    {
        // ...
        // 歌词按钮点击信号和槽函数
        connect(ui->lrcWord, &QPushButton::clicked, this,
            &QQMusic::onLrcWordClicked);
        // ...

b、隐藏窗口和下移动画

LrcPage类中,在构造窗口时设置下移动画,给"下拉"按钮添加槽函数,当"下拉按钮"点击时,开启动画;当动画结束时,将窗口隐藏。

cpp 复制代码
// lrcpage.h 中新增
#include <QPropertyAnimation>
private:
    QPropertyAnimation* lrcAnimation;
    // lrcpage.cpp 中新增
    LrcPage::LrcPage(QWidget* parent) :
        QWidget(parent),
        ui(new Ui::LrcPage)

    {
        ui->setupUi(this);

        // ... 
        lrcAnimation = new QPropertyAnimation(this, "geometry", this);
        lrcAnimation->setDuration(250);
        lrcAnimation->setStartValue(QRect(10, 10, width(), height()));
        lrcAnimation->setEndValue(QRect(10, 10 + height(), width(), height()));

        // 点击设置下拉按钮时开启动画
        connect(ui->hideBtn, &QPushButton::clicked, this, [=] {
            lrcAnimation->start();
            });

        // 动画结束时,将窗⼝隐藏
        connect(lrcAnimation, &QPropertyAnimation::finished, this, [=] {
            hide();
            });
    }

4、Lrc歌词解析和同步

每首歌的Irc歌词有多行文本,因此Irc歌词中的每行可以采用结构体管理。

cpp 复制代码
// lrcpage.h 中新增
struct LyricLine
{
    qint64 time; // 时间
    QString text; // 歌词内容
    LyricLine(qint64 qtime, QString qtext)
        : time(qtime)
        , text(qtext)
    {}
};
// LrcPage类中添加成员变量
QVector<LrcLine> lrcLines; // 按照时间的先后次序保存每⾏歌词

a、通过歌名找lrc文件

一般情况下,播放器在设计之初就会设计好歌曲文件和歌词文件的存放位置,以及对应关系,通常歌曲文件和Irc歌词文件名字相同,后缀不同。在磁盘存放的时候,可以将歌曲文件和Irc文件分两个文件夹存储,也可以存储到一个文件夹下。本文为了方便处理,存储在一个文件夹下,因此可以通过Music对象快速找到Irc歌词文件。

cpp 复制代码
// music.h 中新增
QString getLrcFilePath() const;
// music.cpp 中新增
QString Music::getLrcFilePath() const
{
    // ⾳频⽂件和LRC⽂件在⼀个⽂件夹下
    // 直接将⾳频⽂件的后缀替换为.lrc
    QString path = musicUrl.toLocalFile();
    path.replace(".mp3", ".lrc");
    path.replace(".flac", ".lrc");
    path.replace(".mpga", ".lrc");
    return path;
}

b、歌词解析

找到Irc歌词文件后,由IrcPage类完成对歌词的解析。解析的大概步骤:

①打开歌词文件

②以行为单位,读取歌词文件中的每一行

③按照Irc歌词文件格式,从每行文本中解析出时间和歌词

00:17.94\]那些失眠的人啊你们还好吗 \[0:58.600.00\]你像一只飞来飞去的蝴蝶 ④用\<时间,行歌词\>构建一个LrcLine对象存储到IrcLines中。 ```cpp // lrcpage.h 中新增 bool parseLrc(const QString& lrcPath); // lrcpage.cpp 中新增 bool LrcPage::parseLrc(const QString& lrcPath) { lrcLines.clear(); // 打开歌词⽂件 QFile lrcFile(lrcPath); if (!lrcFile.open(QFile::ReadOnly)) { qDebug() << "打开⽂件:" << lrcPath; return false; } while (!lrcFile.atEnd()) { QString lrcWord = lrcFile.readLine(1024); // [00:17.94]那些失眠的⼈啊 你们还好吗 // [0:58.600.00]你像⼀只⻜来⻜去的蝴蝶 int left = lrcWord.indexOf('['); int right = lrcWord.indexOf(']'); // 解析时间 qint64 lineTime = 0; int start = 0; int end = 0; QString time = lrcWord.mid(left, right - left + 1); // 解析分钟 start = 1; end = time.indexOf(':'); lineTime += lrcWord.mid(start, end - start).toInt() * 60 * 1000; // 解析秒 start = end + 1; end = time.indexOf('.', start); lineTime += lrcWord.mid(start, end - start).toInt() * 1000; // 解析毫秒 start = end + 1; end = time.indexOf('.', start); lineTime += lrcWord.mid(start, end - start).toInt(); // 解析歌词 QString word = lrcWord.mid(right + 1).trimmed(); lrcLines.push_back(LrcLine(lineTime, word.trimmed())); } // 测试验证 for (auto word : lrcLines) { qDebug() << word.time << " " << word.text; } return true; } ``` #### c、根据歌曲播放位置获取歌词并显示 当歌曲播放进度改变时候,QMediaPlayer的positionChanged信号会触发,该信号同步播放时间的时候已经在QQMusic类中处理过了,在其槽函数中就能拿到当前歌曲的播放时间,通过播放时间,就能在LrcPage中找到对应行的歌词。 ```cpp // lrcpage.h 中新增 // lrcpage.cpp 中新增 int LrcPage::getLineLrcWordIndex(qint64 pos) { // 如果歌词是空的,返回-1 if (lrcLines.isEmpty()) { return -1; } if (lrcLines[0].time > pos) { return 0; } // 通过时间⽐较,获取下标 for (int i = 1; i < lrcLines.size(); ++i) { if (pos > lrcLines[i - 1].time && pos <= lrcLines[i].time) { return i - 1; } } // 如果没有找到,返回最后⼀⾏ return lrcLines.size() - 1; } QString LrcPage::getLineLrcWord(qint64 index) { if (index < 0 || index >= lrcLines.size()) { return ""; } return lrcLines[index].text; } void LrcPage::showLrcWord(int time) { // 先要获取歌词--根据歌词的时间进⾏获取 int index = getLineLrcWordIndex(time); if (-1 == index) { 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(getLineLrcWord(index - 3)); ui->line2->setText(getLineLrcWord(index - 2)); ui->line3->setText(getLineLrcWord(index - 1)); ui->lineCenter->setText(getLineLrcWord(index)); ui->line4->setText(getLineLrcWord(index + 1)); ui->line5->setText(getLineLrcWord(index + 2)); ui->line6->setText(getLineLrcWord(index + 3)); } } ``` #### d、Irc歌词同步播放进度 当歌曲发生切换时,需要完成Irc歌词文件的解析; 当歌曲播放进度发生改变时,根据歌曲的当前播放时间,通过IrcPage找到对应行歌词并显示出来。 ```cpp // qqmusic.cpp 添加 void QQMusic::onMetaDataAvailableChanged(bool available) { // 歌曲名称、歌曲作者直接到Musci对象中获取 // 此时需要知道媒体源在播放列表中的索引 QString musicId = currentPage->getMusicIdByIndex(currentIndex); auto it = musicList.findMusicByMusicId(musicId); // ... // 加载lrc歌词并解析 if (it != musicList.end()) { lrcPage->parseLrc(it->getLrcFilePath()); } } void QQMusic::onPositionChanged(qint64 position) { // 1. 更新当前播放时间 ui->currentTime->setText(QString("%1:%2").arg(position / 1000 / 60, 2, 10, QChar('0')) .arg(position / 1000 % 60, 2, 10, QChar('0'))); // 2. 更新进度条的位置 ui->progressBar->setStep(position / (float)totalTime); // 3. 同步lrc歌词 if (playList->currentIndex() >= 0) { lrcPage->showLrcWord(position); } } ``` ## 歌曲数据支持持久化 支持播放相关功能之后,每次在验证功能时都需要从磁盘中加载歌曲文件,非常麻烦。而且之前收藏的喜欢歌曲以及播放记录在程序关闭之后就没有了,这一般是无法接受的。因此需要将每次在播放器上进行的操作保留下来,比如:所加载的歌曲、以及歌曲信息;收藏歌曲信息;历史播放等信息保存起来,当下次程序启动时,将保存的信息加载到播放器即可,这样就能将在播放器上的操作记录保留下来了。要永久性保存,最简单的方式就是直接保存到文件,但是保存文件不安全,而且需要自己操作文件比较麻烦,本文采用数据库完成信息的持久保存。 ### 1、SQLite数据库 SQLite主要特征: 。管理简单,甚至可以认为无需管理。 。操作方便,SQLite生成的数据库文件可以在各个平台无缝移植。 。可以非常方便的以多种形式嵌入到其他应用程序中,如静态库、动态库等。 。易于维护。 ### 2、QQMuic中数据库支持 #### a、数据库初始化 ```cpp // qqmusic.h 中新增 #include QSqlDatabase sqlite; // qqmusic.cpp 中新增 void QQMusic::initSQLite() { // 1. 创建数据库连接 sqlite = QSqlDatabase::addDatabase("QSQLITE"); // 2. 设置数据库名称 sqlite.setDatabaseName("QQMusic.db"); // 3. 打开数据库 if (!sqlite.open()) { QMessageBox::critical(this, "打开QQMusicDB失败", sqlite.lastError().text()); return; } qDebug() << "SQLite连接成功,并创建 [QQMusic.db] 数据库!!!"; // 4. 创建数据库表 QString sql = ("CREATE TABLE IF NOT EXISTS musicInfo(\ id INTEGER PRIMARY KEY AUTOINCREMENT,\ musicId varchar(200) UNIQUE,\ musicName varchar(50),\ musicSinger varchar(50),\ albumName varchar(50),\ duration BIGINT,\ musicUrl varchar(256),\ isLike INTEGER,\ isHistory INTEGER)" ); QSqlQuery query; if (!query.exec(sql)) { QMessageBox::critical(this, "创建数据库表失败", query.lastError().text()); return; } qDebug() << "创建 [musicInfo] 表成功!!!"; } QQMusic::QQMusic(QWidget* parent) : QWidget(parent) , ui(new Ui::QQMusic) , currentIndex(-1) { ui->setupUi(this); initUi(); // 初始化数据库 initSQLite(); initPlayer(); connectSignalAndSlots(); } ``` #### b、歌曲信息写入数据库 当程序退出的时候,通过musicList获取到所有music对象,然后将music对象写入数据库。 ```cpp // musiclist.h 中新增 // 所有歌曲信息更新到数据库 void writeToDB(); // musiclist.cpp 中新增 void MusicList::writeToDB() { for (auto music : musicList) { // 让music对象将⾃⼰写⼊数据库 music.insertMusicToDB(); } } // music.h 中新增 // 将当前Music对象更新到数据库 void insertMusicToDB(); // music.cpp 中新增 #include #include void Music::insertMusicToDB() { // 1. 检测music是否在数据库中存在 QSqlQuery query; // 当SELECT 1...查询到结果后,我们需要知道是否存在 // SELECT EXISTS(⼦查询) : ⼦查询中如果有记录,SELECT EXISTS返回TRUE // 如果⼦查询中没有满⾜条件的记录, SELECT EXISTS返回FALSE 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) { // musicId的歌曲已经存在 // 2. 存在:不需要再插⼊musci对象,此时只需要将isLike和isHistory属性进⾏更新 query.prepare("UPDATE MusicInfo SET isLike = ?, isHistory = ? WHERE musicId = ? "); query.addBindValue(isLike ? 1 : 0); query.addBindValue(isHistory ? 1 : 0); query.addBindValue(musicId); if (!query.exec()) { qDebug() << "更新失败: " << query.lastError().text(); } qDebug() << "更新music信息: " << musicName << " " << musicId; } else { // 3. 不存在:直接将music的属性信息插⼊数据库 query.prepare("INSERT INTO MusicInfo(musicId, musicName, musicSinger, albumName, musicUrl, \ duration, isLike, isHistory)\ VALUES(? , ? , ? , ? , ? , ? , ? , ? )"); query.addBindValue(musicId); query.addBindValue(musicName); query.addBindValue(musicSinger); query.addBindValue(musicAlbumn); query.addBindValue(musicUrl.toLocalFile()); query.addBindValue(duration); query.addBindValue(isLike ? 1 : 0); query.addBindValue(isHistory ? 1 : 0); if (!query.exec()) { qDebug() << "插⼊失败: " << query.lastError().text(); return; } qDebug() << "插⼊music信息: " << musicName << " " << musicId; } } } // qqmusic.cpp 中新增 void QQMusic::on_quit_clicked() { // 更新数据库 musicList.writeToDB(); // 关闭数据库连接 sqlite.close(); // 关闭窗⼝ close(); } ``` #### c、程序启动时读取数据库恢复歌曲数据 在程序启动时,从数据库中读取到歌曲的信息,将歌曲信息设置到musicList中,然后让likePage、 localPage、recentPage将musicList中个歌曲更新到各自页面中。从数据库读取歌曲数据的操作,应该让MusicList类完成,因为该类管理所有的Music对象。 ```cpp // musiclist.h 中新增 void readFromDB(); // musiclist.cpp 中新增 #include #include void MusicList::readFromDB() { QString sql("SELECT musicId, musicName, musicSinger, albumName,\ duration, musicUrl, isLike, isHistory \ FROM musicInfo"); QSqlQuery query; if (!query.exec(sql)) { qDebug() << "数据库查询失败"; 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.setMusicAlbum(query.value(3).toString()); music.setMusicDuration(query.value(4).toLongLong()); music.setMusicUrl(query.value(5).toString()); music.setIsLike(query.value(6).toBool()); music.setIsHistory(query.value(7).toBool()); musicList.push_back(music); } } // qqmusic.h 中新增 void initMusicList(); // qqmusic.cpp 中新增 void QQMusic::initMusicList() { // 1. 从数据库读取歌曲信息 musicList.readFromDB(); // 2. 更新CommonPage⻚⾯ // 设置CommonPage的信息 ui->likePage->setMusicListType(PageType::LIKE_PAGE); ui->likePage->reFresh(musicList); ui->localPage->setMusicListType(PageType::LOCAL_PAGE); ui->localPage->reFresh(musicList); ui->recentPage->setMusicListType(PageType::HISTORY_PAGE); ui->recentPage->reFresh(musicList); } QQMusic::QQMusic(QWidget* parent) : QWidget(parent) , ui(new Ui::QQMusic) , currentIndex(-1) { // ... // 初始化数据库 initSQLite(); // 加载数据库歌曲⽂件 initMusicList(); // ... } ``` QQMusic中的initUi中将其去掉 ![](https://i-blog.csdnimg.cn/direct/e351e4b011ce477c9278da493ec45b53.png)

相关推荐
How_doyou_do2 分钟前
项目实战-贪吃蛇大作战【补档】
c语言·c++·visual studio
Ethon_王4 分钟前
走进Qt--信号与槽机制详解与实战
c++·qt
武昌库里写JAVA5 分钟前
vuex源码分析(一)——初始化vuex
java·开发语言·spring boot·学习·课程设计
不思念一个荒废的名字7 分钟前
【刷题Day29】Python/JAVA - 03(浅)
java·开发语言·jvm·python
画个大饼8 分钟前
深度对比:Objective-C与Swift的RunTime机制与底层原理
开发语言·objective-c·swift
TNTLWT13 分钟前
3、CMake语法:制作和使用动态库和静态库
开发语言·cmake
小卡皮巴拉27 分钟前
【力扣刷题实战】丢失的数字
c++·算法·leetcode·位运算
凤年徐28 分钟前
【C/C++】深入理解指针(六)
c语言·开发语言·c++·经验分享·笔记·指针
学了就忘36 分钟前
JavaScript 异步编程与请求取消全指南
开发语言·javascript·ecmascript
通南北40 分钟前
使用python实现自动化拉取压缩包并处理流程
开发语言·python·自动化