当前位置: 首页 > news >正文

基于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)
{// 设置⾃定义按钮的图⽚、⽂字、以及idui->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信号,拦截到该信号,销毁animationconnect(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信号,拦截到该信号,销毁animationconnect(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是主界⾯上的supplyMusicBoxif (2 == row){this->row = row;this->col = 8;}else{// 否则:只有⼀⾏,为主界⾯上recMusicBoxui->recBoxBottom->hide();}// 图⽚保存起来imageList = data;// 往RecBox中添加图⽚createRecItem();
}
void RecBox::createRecBoxItem()
{// 创建RecBoxItem对象,往RecBox中添加// colfor (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控件添加RecBoxItemcreateRecBoxItem();
}
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::NoDropShadowWindowHintsetAttribute(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"));// ⾳量的默认⼤⼩是20ui->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);// 默认⾳量⼤⼩设置为20player->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, [=](constQModelIndex& 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的位置,并计算volumeRationsetVolume();}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的宽度为currentPosprivate:int currentPos; // 滑动条当前位置// musicslider.cpp 中新增MusicSlider::MusicSlider(QWidget * parent) :QWidget(parent),ui(new Ui::MusicSlider){ui->setupUi(this);// 初始情况下,还没有开始播放,将当前播放进度设置为0currentPos = 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));// 缩放填充到整个Labelui->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 = newQGraphicsDropShadowEffect(this);shadowEffect->setOffset(0, 0);shadowEffect->setColor("#000000"); // ⿊⾊// 此处需要将圆⻆半径不能太⼤,否则动画效果有问题,可以设置为10shadowEffect->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⽂件在⼀个⽂件夹下// 直接将⾳频⽂件的后缀替换为.lrcQString 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)
{// 如果歌词是空的,返回-1if (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返回FALSEquery.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中将其去掉

相关文章:

  • 极简桌面app官网版下载 极简桌面最新版 安装包下载
  • 栈相关算法题解题思路与代码实现分享
  • 深入解析NuttX:为何它是嵌入式RTOS领域的标杆?​​
  • 思科路由器重分发(RIP动态路由+静态路由)
  • RAG技术与应用---0426
  • 8.学习笔记-Maven进阶(P82-P89)
  • 23种设计模式-行为型模式之观察者模式(Java版本)
  • 零基础上手Python数据分析 (24):Scikit-learn 机器学习初步 - 让数据预测未来!
  • stm32L4R5ZI Nucleo-144 GPIO点灯及按键中断
  • Log4j Properties 配置项详细说明
  • linux socket编程之tcp(实现客户端和服务端消息的发送和接收)
  • C盘爆红如何解决
  • 如何使用WebRTC
  • [FPGA Video IP] Video Processing Subsystem
  • 【分布式系统中的“瑞士军刀”_ Zookeeper】二、Zookeeper 核心功能深度剖析与技术实现细节
  • Deep Reinforcement learning for real autonomous mobile robot navigation
  • FreeBSD可以不经过windows服务器访问windows机器上的共享文件吗?
  • deepseek-cli开源的强大命令行界面,用于与 DeepSeek 的 AI 模型进行交互
  • JAVA后端开发常用的LINUX命令总结
  • 模板引擎语法-过滤器
  • 仲裁法修订草案二审稿拟增加规定规制虚假仲裁
  • 苏迪曼杯即将在厦门打响,国羽向创纪录的14冠进军
  • 韩国京畿道骊州市市长率团访问菏泽:想和菏泽一起办牡丹节
  • 生于1984年,马玥已任辽宁锦州北镇市代市长
  • 台媒称美派遣前军官出任“汉光演习”资深观察员,国防部回应
  • 【社论】上海经济开门红:不偏科、挑大梁