云计算百科
云计算领域专业知识百科平台

基于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()里面,在里面添加

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

2、实现鼠标拖动窗口

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

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

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()
函数中添加:

// 设置窗⼝背景透明
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页面

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()函数中新增:

// 设置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信号

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

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

// 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类中添加:

// 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界面的标准来确定

// 设置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显示。

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

QQMusic的initUI中设置默认选中

// 本地下载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新增

#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);
}

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

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

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

// 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中需要添加如下代码:

// 设置随机图⽚【歌曲的图⽚】
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类中新增加以下成员变量:

#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需要具体来计算:

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的槽函数就添加好了。

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。

// 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()成员函数中新加

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函数中新增:

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函数。

// 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(函数中调用完成设置:

//在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类的初始化方法中添加如下代码:

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效果。

// 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、界面设计

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

// 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槽函数。

// 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事件函 数。

// 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类完成音乐文件加载。

// 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类都是支持的。

// 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、介绍

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

// 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新增 

// 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。

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数据解析代码如下:

// 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信息往界面更新。

// 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类中需要新增:

// 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 中。

// 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类中新增:

// 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对象的方法。

// 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类中新增:

// 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槽函数最后调用。

// 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信息到界面时,要能够设置小心心图片。

// 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来处理

// 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。

// 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更新其界面的我喜欢歌曲信息。

// 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、歌曲重复显示问题

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

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

音乐播放控制

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

#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中新增将其页面歌曲添加到模仿列表的方法。

// 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()。

// 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信号。

// 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叠加,因此将按钮上个添加背景图片样式去除掉。

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就会播放对应的歌曲。

// 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、切换播放模式

// 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信号,在该信号对应槽函数中,完成图片切换。

// 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在处理该信号时,知道播放哪个页面的歌曲。

// 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首歌曲开始播放。注意不要忘记关联信号槽。

// 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中歌曲的所以一致,被双击时直接播放该首歌曲即可。

// 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的改变。

// 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();

// …
}

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

// 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来处理。 

// 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知道需要修改播放媒体的音量大小了。

// 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更新 

// 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信号,将音量大小设置为指定值。

// 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更新到界面总时间即可。

// 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中捕获该信号,便可获取到正在播放媒体的持续时间。

// 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的宽度即可。

// 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按照该比率更新持续播放时间。

// 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函数更新下。

// 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))信 号关联槽函数,当歌曲更换时,完成信息的更新。

// 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信号,当按钮点击时将窗口隐藏。

// 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按钮添加槽函数,在槽函数中将窗口显示出来。

// 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中给"歌词"按钮添加槽函数,当按钮点击时,显示窗口,开启动画

// 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类中,在构造窗口时设置下移动画,给"下拉"按钮添加槽函数,当"下拉按钮"点击时,开启动画;当动画结束时,将窗口隐藏。

// 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歌词中的每行可以采用结构体管理。

// 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歌词文件。

// 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中。

// 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中找到对应行的歌词。

// 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找到对应行歌词并显示出来。

// 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、数据库初始化

// qqmusic.h 中新增
#include <QSqlDatabase>
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对象写入数据库。

// musiclist.h 中新增
// 所有歌曲信息更新到数据库
void writeToDB();
// musiclist.cpp 中新增
void MusicList::writeToDB()
{
for (auto music : musicList)
{
// 让music对象将⾃⼰写⼊数据库
music.insertMusicToDB();
}
}
// music.h 中新增
// 将当前Music对象更新到数据库
void insertMusicToDB();
// music.cpp 中新增
#include <QSqlQuery>
#include <QSqlError>
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对象。

// musiclist.h 中新增
void readFromDB();
// musiclist.cpp 中新增
#include <QSqlQuery>
#include <QSqlError>
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中将其去掉

赞(0)
未经允许不得转载:网硕互联帮助中心 » 基于QT的仿QQ音乐播放器
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!