基于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中将其去掉

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/77581.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQL知识点合集---第二弹

数据一 <select id"listPositionAuditCheckSample" resultType"net.nxe.cloud.content.server.entity.PositionAuditCheckSample"><trim prefixOverrides"union all"><if test"userSampleCount ! null and userSampleCount…

【QT】QT控制硬件

QT控制硬件 1.上位机程序开发2.具体例子控制led灯3. linux中的函数跟QT类里面的函数同名&#xff0c;发生冲突4.示例代码 1.上位机程序开发 QT做一个上位机程序&#xff0c;控制底层的硬件设备(下位机) 总结&#xff1a; 在构造函数里面去初始化&#xff0c;打开硬件驱动在析…

Flutter介绍、Flutter Windows Android 环境搭建 真机调试

目录 Flutter介绍 Windows 环境搭建 1.安装配置JDK 2.下载安装Android Studio 3.下载配置Flutter SDK ​4.运行Flutter doctor命令检测环境是否配置成功 ​5.打开Android Studio安装Flutter/Dart 插件 ​6.插件运行Flutter项目 ​编辑 Flutter Android真机调试 Flut…

Android Studio 中使用 SQLite 数据库开发完整指南(Kotlin版本)

文章目录 1. 项目准备1.1 创建新项目1.2 添加必要依赖 2. 数据库设计3. 实现数据库3.1 创建实体类 (Entity)3.2 创建数据访问对象 (DAO)3.3 创建数据库类 4. 创建 Repository5. 创建 ViewModel6. 实现 UI 层6.1 创建笔记列表 Activityactivity_notes_list.xmlNotesListActivity…

Vue基础(7)_计算属性

计算属性(computed) 一、使用方式&#xff1a; 1.定义计算属性&#xff1a; 在Vue组件中&#xff0c;通过在 computed 对象中定义计算属性名称及对应的计算函数来创建计算属性。计算函数会返回计算属性的值。 2.在模板中使用计算属性&#xff1a; 在Vue的模板中&#xff0c;您…

辛格迪客户案例 | 华道生物细胞治疗生产及追溯项目(CGTS)

01 华道&#xff08;上海&#xff09;生物医药有限公司&#xff1a;细胞治疗领域的创新先锋 华道&#xff08;上海&#xff09;生物医药有限公司&#xff08;以下简称“华道生物”&#xff09;是一家专注于细胞治疗技术研发与应用的创新型企业&#xff0c;尤其在CAR-T细胞免疫…

[26] cuda 应用之 nppi 实现图像格式转换

[26] cuda 应用之 nppi 实现图像格式转换 讲述 nppi 接口定义通过nppi实现 bayer 格式转rgb格式官网参考信息:http://gwmodel.whu.edu.cn/docs/CUDA/npp/group__image__color__debayer.html#details1. 接口定义 官网关于转换的原理是这么写的: Grayscale Color Filter Array …

2025“钉耙编程”中国大学生算法设计春季联赛(8)10031007

题目的意思很好理解找从最左边到最右边最短路&#xff08;BFS&#xff09; #include <bits/stdc.h> using namespace std; int a[510][510]; // 存储网格中每个位置是否有障碍&#xff08;1表示有障碍&#xff0c;0表示无障碍&#xff09; int v[510][510]; // 记录每…

【Linux】第十一章 管理网络

目录 1.TCP/IP网络模型 物理层&#xff08;Physical&#xff09; 数据链路层&#xff08;Date Link&#xff09; 网络层&#xff08;Internet&#xff09; 传输层&#xff08;Transport&#xff09; 应用层&#xff08;Application&#xff09; 2. 对于 IPv4 地址&#…

python_股票月数据趋势判断

目录 前置 代码 视频&月数据 前置 1 A股月数据趋势大致判断&#xff0c;做一个粗略的筛选 2 逻辑&#xff1a; 1&#xff09;取最近一次历史最高点 2&#xff09;以1&#xff09;中最高点为分界点&#xff0c;只看右侧数据&#xff0c;取最近一次最低点 3&#xf…

Python PyAutoGUI库【GUI 自动化库】深度解析与实战指南

一、核心工作原理 底层驱动机制&#xff1a; 通过操作系统原生API模拟输入使用ctypes库调用Windows API/Mac Cocoa/Xlib屏幕操作依赖Pillow库进行图像处理 事件模拟流程&#xff1a; #mermaid-svg-1CGDRNzFNEffhvSa {font-family:"trebuchet ms",verdana,arial,sans…

Spring框架allow-bean-definition-overriding详细解释

Spring框架中&#xff0c;allow-bean-definition-overriding 是一个控制是否允许覆盖同名Bean定义的配置属性。以下是详细说明&#xff1a; ​1. 作用​ ​允许/禁止Bean定义覆盖​&#xff1a;当Spring容器中检测到多个同名的Bean定义时&#xff0c;此配置决定是否允许后续的…

机器人抓取位姿检测——GRCN训练及测试教程(Pytorch)

机器人抓取位姿检测——GRCN训练及测试教程(Pytorch) 这篇文章主要介绍了2020年IROS提出的一种名为GRCN的检测模型,给出了代码各部分的说明,并给出windows系统下可以直接复现的完整代码,包含Cornell数据集。 模型结构图 github源码地址:https://github.com/skumra/robo…

在web应用后端接入内容审核——以腾讯云音频审核为例(Go语言示例)

腾讯云对象存储数据万象&#xff08;Cloud Infinite&#xff0c;CI&#xff09;为用户提供图片、视频、语音、文本等文件的内容安全智能审核服务&#xff0c;帮助用户有效识别涉黄、违法违规和广告审核&#xff0c;规避运营风险。本文以音频审核为例给出go语言示例代码与相应结…

GraphRAG知识库概要设计展望

最近研究了一下GraphRAG&#xff0c;写了一个文档转换工具还有图可视化工具&#xff0c;结合langchain构建RAG经验&#xff0c;还有以前的数据平台&#xff0c;做了一个知识库概要设计&#xff0c;具体应用欢迎留言探讨。 一、GraphRAG整体概述 GraphRAG图基检索增强生成&…

Android Studio 日志系统详解

文章目录 一、Android 日志系统基础1. Log 类2. 日志级别 二、Android Studio 中的 Logcat1. 打开 Logcat2. Logcat 界面组成3. 常用 Logcat 命令 三、高级日志技巧1. 自定义日志工具类2. 打印方法调用栈3. 打印长日志4. JSON 和 XML 格式化输出 四、Logcat 高级功能1. 自定义日…

深度对比:Objective-C与Swift的RunTime机制与底层原理

1. RunTime简介 RunTime&#xff08;运行时&#xff09;是指程序在运行过程中动态管理类型、对象、方法等的机制。Objective-C 和 Swift 都拥有自己的运行时系统&#xff0c;但设计理念和实现方式有很大不同。理解 RunTime 的底层原理&#xff0c;是掌握 iOS 高级开发的关键。…

使用手机录制rosbag包

文章目录 简介录制工具录制步骤录制设置设置IMU录制频率设置相机分辨率拍照模式录制模式数据制作获取数据数据转为rosbag查看rosbag简介 ROS数据包(rosbag)是ROS系统中用于记录和回放传感器数据的重要工具,通常用于算法调试、系统测试和数据采集。传统上,rosbag依赖于ROS环…

浅谈PCB传输线(一)

前言&#xff1a;浅谈传输线的类型&#xff0c;以及传输线的一些行为特性。 1.传输线的种类 2.互连线被视为传输线的场景 3.传输线的行为特性*** 1.传输线的种类 PCB 中的信号传输线通常有两种基本类型: 微带线和带状线。此外&#xff0c;还有第三种类型–共面线(没有参考平面…

【angular19】入门基础教程(一):项目的搭建与启动

angular现在发展的越来越能完善了&#xff0c;在vue和react的强势竞争下&#xff0c;它迎来了自己的巨大变革。项目工程化越来越好&#xff0c;也开始拥抱了vite这种高效的构建方式。所以&#xff0c;我们有必要来学习这么一个框架了。 项目实现效果 nodejs环境 Node.js - v^…