一、项目简介
项目名称:本地音乐播放器
环境和使用技术:QtCreator6.5.3、mingw_64、QtDesigner界面设计、基于QWidget项目布局、QSS样式设置、自定义控件、信号槽机制、媒体播放技术、简单动画效果、文件操作
项目说明:实现在本地对音乐进行管理和播放的音乐播放器,,该项目可分为四个部分:界面布局、音乐管理、播放控制、持久化操作
项目源代码:https://github.com/lx467/-lx-LXMusic-
二、界面的说明
界面上的空间比较多,归结为两部分可以分为head区,和body区。
head区域从左往右依次为:图标、搜索框、更换皮肤按钮、最小化 & 最大化 & 退出按钮。
body区域分为左侧种类选择区域和右侧Page展示区。
Body左侧区域有两部分组成:在线音乐 和 我的音乐 ,两部分内部的控件种类是相同的
- 说明区域,实际为QLabel
- ⾃定义控件(按钮的扩展):图片+文本+动画
- 同上,⾃定义控件(按钮的扩展):图片+文本+动画
- 同上,⾃定义控件(按钮的扩展):图片+文本+动画
说明区域,实际为QLabel
Body右侧区域由:Page区、播放进度、播放控制区三部分构成。
- Page区:歌曲信息页面,点击 < 或 > 具有轮播图效果
- ② 播放进度:当前歌曲播放进度说明,支持seek功能,与播放控制区时间、以及LRC歌词是同步的
- ③ 播放控制区域:显示歌曲图片&名称&歌手、 播放模式 & 下⼀曲 & 播放暂停 & 上⼀曲 & 音量调节和静音 & 添加本地⾳乐 当前播放时间 / 歌曲总时长 & 弹出歌词窗⼝按钮
【Page区说明】
当点击body左侧不同按钮时,Page区域会显示不同的页面
Body右侧目前支持的4个页面结构,整体的布局是相同的,唯独Page区域显示的内容稍有区别。 推荐页面具有类似轮播图的动态效果:
整个页面内容可以分为上下两组:今日为你推荐、你的歌曲补给站。两组的布局实际是相同的,元素说明:
-
上方显示1行,内部有4个推荐元素;下方显示2行,每行有4个推荐元素
-
左右两侧⼀个按钮,点击后推荐内容会更换下一批,不停点击会循环推荐
-
当鼠标悬停在推荐元素上时,推荐元素会向上移动,当鼠标离开时,又回到原位置
-
当鼠标悬停在推荐元素上时,同时会出现小手图标,说明该推荐元素具有点击功能
该页面中内容也为自定义元素,后序页面实现时具体分析。
三、界面的开发
3.1 创建工程
创建⼀个基于QWidget的工程,选中生成form选项,将来界⾯部分主要使用QDesigner来设计
3.2 主界面布局设置
基于Widget局部 ,QT系统提供4种布局管理器:
-
QVBoxLayout:垂直布局
-
QHBoxLayout:水平布局
-
QGridLayout:栅格布局
-
QFormLayout:表单布局
由于⼀个widget中只能包含上述布局管理器中的⼀种,所以直接使⽤布局管理器来布局不是很灵活;而⼀个widget中可以包含多个widget,在widget中的控件可以进行水平、垂直、栅格、表单等布局操作,非常灵活。
因此本项⽬基于Widget来进⾏布局
窗⼝主框架设计 【主窗⼝的布局】
选中LXMusic,在弹出的属性中找到geometry属性,将窗口宽度修改为:1040,高度修改为700
从控件区拖拽⼀个Widget到窗口区域,objectName修改为:background,选中LXMusic,然后点击垂直布局,background就填充满了整个窗口
整个窗口由head和body上下两部分组成。
直接拖两个Widget放到设计区,双击将名字修改为head和body;
修改背景颜色方便查看效果,head背景色修改为green,body背景色修改为pink;
background-color:green;
background-color:pink;
head在上,body在下,然后选中background对象,点击垂直布局
head和body平分了整个background,并且head和body的margin有间隔。再次选中background对象,右侧属性部分下滑找到Layout,将Margni和Space修改为0
修完完成后,head和body之间的间隔就没有了。
但是head占区域过大,选中head对象,将head的minimumSize和maxmumSize属性的高度都调整为80,这样head的大小就固定了
head内部设计
- head内部由两部分构成,headLeft区域显示图标,headRight区域为搜索框和功能按钮区域。 拖两个widget到head中,将objectName修改为headLeft和headRight
- 然后选中head对象,点击水平布局(垂直布局左侧就是水平布局),它们就会分布在两侧。
- 继续选中head对象,下滑找到Layout属性,将Margin和Spacing全部设置为0;
- 选中headLeft对象,将minimumSize和maximumSize的宽度修改为200,就能看到head的初步效果
【headLeft】
拖⼀个QLabel控件放置headLeft内,将QLabel的objectName修改为logo,text属性修改为空;然后选中headLeft,点击⽔平布局,此时QLabel就会填充满headLeft。同样需要选中headLeft,下滑找到Layout属性,将Margin和spacing全部设置为0.
【headRight】
headRight内部也是由两部分构成:搜索框和按钮区域 拖拽两个widget到headRight,修改objectName为SearchBox和SettingBox,将SearchBox的minimumSize和maximumSize的宽度修改为300
选中headRight,然后点击水平布局,并将headRight的Margin和Spacing修改为0
【searchBox】
拖⼀个QLineEdit进去,并将其objectName修改为searchEdit然后选中searchBox点击水平布局
【settingBox】
拖拽⼀个按钮PushButton到SettingBox,按钮的minimumSize和maximumSize的宽度和⾼度都修改为30,然后⿏标选中,按着ctrl键+⿏标拖拽,复制3个出来摆放好,依次将四个按钮的objectName从左往右修改为:skinButton、maxButton、minButton、quitButton,并将按钮的text属性也修改为空,将来设置图片。
在控件区域找到Spacers,找到Horizontal Spacer控件,拖拽到SettingBox区域
选中SettingBox,点击水平布局,并将SettingBox的Margin和Spacing修改为0
Body部分布局
- 整个body部分是由bodyLeft和bodyRight两部分组成。
- 拖两个Widget到Body中,将objectName修改为bodyLeft和bodyRight
- 选中body,点击水平布局,将bodyLeft的minimumSize和maxmumSize的宽度修改为200
- 选中Body,将body的Margin和Spacing修改为0
bodyLeft内部布局
- 拖拽⼀个Widget到bodyLeft,将objectName修改为leftBox,背景颜⾊修改为:backgroundcolor:pink;
- 拖拽Vertical Spacer到bodyLeft
- 选中leftBox,将minmumSize和maxmumSize的⾼度修改为400
- 选中bodyLeft,点击垂直布局,并将bodyLeft的Margin和Spacing修改为0
leftBox内部布局
- 拖拽两个Widget到leftBox中,将objectName依次修改为:onlineMusic和myMusic
- 选中leftBox,点击垂直布局,然后将Margin和Spacing设置为0
- onlineMusic 和 myMusic内部的元素都是相同的,由⼀个QLabel和三个Widget构成,后期Widget会替换为⾃定义按钮,此处先用Widget占位。因此分别向onlineMusic和myMusic内部拖拽⼀个QLabel和三个QWidget,并选中 onlineMusic 和 myMusic点击垂直布局,然后将Margin和Spacing设置为0
bodyRight布局
bodyRight由层叠窗口、进度滑竿、播放控制区三部分组成
- 拖拽层叠窗口控件Stacked Widget,就在Widget控件上方到bodyRight中
- 拖拽Widget到bodyRight,将objectName修改为processBar,将minimumSize和maximumSize的⾼度修改为30,背景颜色修改为绿色
- 拖拽Widget到bodyRight,将objectName修改为controlBox,将minmumSize高度修改为60
- 选中bodyRight,点击垂直布局,然后将bodyRight的Margin和Spacing修改为0
stackedWidget内部增加页面
- stackedWidget默认会提供两个页面,还需添加四个页面。
- 在对象区域选中stackedWidget控件,然后右键单击弹出菜单中选择添加页:
以类似的方式添加添加4个页面,并修改每个页面的objectName如下:
总共六个页面,每个页面都有自己的索引,所以是从0开始的,将来切换页面时就是通过索引来切换的。选中stackedWidget,然后右键单击,弹出菜单中选择:改变页顺序,在弹出的窗⼝中就能看到每个页面的索引
ControlBox内部布局
-
该区域内部由三部分组成:歌曲信息部分、播放控制部分、时间显示
-
拖拽三个Widget到ControlBox中,将ObjectName依次修改为play1、play2、play3
-
选中ControlBox,点击⽔平布局,将ControlBox的Margin和Spacing修改为0
play1内部:
- 拖拽3个QLabel,放置歌曲图片、歌手名和歌曲名字,调整好位置,将QLabel的objectName修改为: musicCoverLabel、musicNameLabel、musicSingerLabel
- 然后选中play1,点击栅格布局
play2内部:
- 从左到右依次摆放6个按钮,按钮的minimumSize和maxmumSize均修改为30*30,将objectName从左往右依次修改为:playModeButton、playUpButton、PlayButton、playDownButton、soundButton、addLocalButton;
- 然后选中play2,点击水平布局,并将play_2的Margin和Spacing修改为0
play3内部:
- 拖四个QLabel和⼀个按钮,调整大小位置,从左往右QLabel的objectName依次修改为:labelNull、currentTimeLabel、lineLabel、totalTimeLabel,按钮的objectName修改为lyricButton,按钮的maxmumSize的宽度和⾼度修改为30*30;
- 选中play3,点击水平布局,并将play2的Margin和Spacing修改为0
3.3 界面的美化
主窗口设定
仔细观察发现主窗⼝是没有标题栏,因此在窗⼝创建前,就需要设置下窗口的格式
QWidget::setWindowFlag(...): //设置窗⼝格式,⽐如创建⽆边框的窗⼝
由于窗口中控件比较多,这些控件将来都需要初始化,如果将所有代码放在LXMusic的构造函数中实现,将来会造成构造函数非常臃肿,因此在LXMusic类中添加initUI()方法来完成界面初始化工作
// LXMusic.h ⽂件中添加:
void initUI();
// 添加完成后,光标放在函数名字上按 alt + Enter 组合键完成⽅法定义
// LXMusic.cpp 头⽂件中完成定义
void QQMusic::initUI()
{
// 设置⽆边框窗⼝,即窗⼝将来⽆标题栏
setWindowFlag(Qt::WindowType::FramelessWindowHint);
}
添加完成后一定要在LXMusic的构造函数中调用initUI()函数,否则设置不会生效。
运行后,发现有以下两个问题:
- 窗口无标题栏,找不到关闭按钮,导致窗口无法关闭窗⼝
- 无法拖拽窗口
可以先将光标放在任务栏中当前应⽤程序图标上,弹出的框中选择关闭,后序会实现关闭功能
解决界面无法关闭问题
让quitButton按钮关联一个槽函数,当点击这个按钮的时候,调用程序的退出接口即可
//LXMusic.h文件中
private slots:
void on_quitButton_clicked();
//LXMusic.cpp文件中
void LXMusic::on_quitButton_clicked()
{
this->close();
}
//注意:如果直接使用代码实现,需要使用connect绑定槽函数,如果使用ui创建的函数,那么不需要我们自己绑定
解决主界面无法拖动问题
主界面无法拖动,此时只需要处理下鼠标单击(mousePressEvent)和鼠标移动(mouseMoveEvent)事件即可
鼠标左键按下时,记录下窗口左上角和鼠标的相对位置
鼠标移动时,会产生新的位置,保持鼠标和窗⼝左上角相对位置不变,通过move修改窗口的左上角坐标即可
// LXMusic.h中添加
protected:
// 重写QWidget类的⿏标单击和⿏标滚轮事件
void mousePressEvent(QMouseEvent *event)override;
void mouseMoveEvent(QMouseEvent* event)override;
// 记录光标相对于窗⼝标题栏的相对距离
QPoint dragPosition;
-------------------------------------------------------------
// LXMusic.cpp中添加
void LXMusic::mousePressEvent(QMouseEvent* event){
// 拦截⿏标左键单击事件
if(event->button() == Qt::LeftButton){
dragPosition = event->globalPosition().toPoint() - geometry().topLeft();
event->accept();
}
QWidget::mousePressEvent(event);
}
void LXMusic::mouseMoveEvent(QMouseEvent* event){
// 拦截⿏标左键单击事件
if(event->buttons() == Qt::LeftButton){
move(event->globalPosition().toPoint() - dragPosition);
event->accept();
}
QWidget::mouseMoveEvent(event);
}
// event->globalPositon().toPoint():⿏标按下事件发⽣时,光标相对于屏幕左上⻆位置
// frameGeometry().topLeft(): ⿏标按下事件发⽣时,窗⼝左上⻆位置
// geometry(): 不包括边框及顶部标题区的范围
// frameGeometry(): 包括边框及顶部标题区的范围
// event->globalPositon().toPoint() - frameGeometry().topLeft() 即为:
// ⿏标按下时,窗⼝左上⻆和光标之间的距离差
// 想要窗⼝⿏标按下时窗⼝移动,只需要在mouseMoveEvent中,让光标和窗⼝左上⻆保持相同的位置差
// 获取⿏标相对于屏幕左上⻆的全局坐标
给窗口周围设置阴影部分
再仔细观察,窗口周围是有阴影效果的,窗口四周黑色部分就是阴影
给窗口添加阴影需要用到 QGraphicsDropShadowEffect 类,具体步骤如下:
-
创建 QGraphicsDropShadowEffect 类对象
-
设置阴影的属性。⽐如:设置阴影的偏移、颜⾊、圆⻆等
-
将阴影设置到具体对象上
在 initUI() 函数中添加如下代码:
//设置窗口背景透明
this->setAttribute(Qt::WA_TranslucentBackground);
//设置窗口阴影部分
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0,0);//设置阴影偏移量
shadowEffect->setColor(Qt::black);//设置阴影颜色
shadowEffect->setBlurRadius(10);//设置阴影半径
this->setGraphicsEffect(shadowEffect);
注意:给窗口设置阴影效果时,需要将窗口标题栏无边框,背景设置为透明
添加图片资源
添加⼀个qrc文件,将图⽚资源拷贝到工程目录下,并添加到工程中
将之前布局时所有按钮的背景全色全部清除掉,按照下面的风格重新设定。
head部分处理
不满意下面的设定,可以通过颜色查看器自行更改设定
控件 | QSS美化 |
---|---|
headLeft | #headLeft{background-color:#F0F0F0;/设置背景颜⾊为浅灰⾊/} |
headRight | #headRight{background-color:#F5F5F5; /设置背景颜⾊为亮灰⾊/} |
logo | #logo{border-radius:0px;background-image: url(:/images/Logo.png);background-repeat:no-repeat;border:none; background-position:center center;} |
lineEdit | #lineEdit{ background-color: #E3E3E3; /设置背景颜⾊/border-radius: 17px; /设置四个⻆的圆⻆/padding-left: 17px; /内部⽂字到边的距离/} |
settingBox | /* 类型选择器 /QPushButton {border-radius:0px; /设置按钮的边框圆⻆为 0 像素,实现直⻆边缘/background-repeat:no-repeat; / 背景图⽚不重复平铺*/border: none; /⽆边框/background-position:center center; /背景图⽚放置在按钮的中⼼位置/}/悬停状态/QPushButton:hover {background-color: rgba(230,0,0,0.5); /设置背景颜⾊为半透明的红⾊/} |
skin | background-image: url(:/images/skin.png); |
max | background-image: url(:/images/max.png); |
min | background-image: url(:/images/min.png); |
quit | background-image: url(:/images/quit.png); |
bodyLeft | #bodyLeft{background-color:#F0F0F0;/设置背景颜⾊为浅灰⾊/} |
bodyRight | #bodyRight{background-color:#F5F5F5; /设置背景颜⾊为亮灰⾊/} |
播放控制区处理
祛除play1、play2、play3的页面布局时设置的临时背景色。 将按钮上的文字全部去除,然后重新添加样式和图片
控件 | QSS美化 |
---|---|
play2 | QPushButton{border: none; /* 去除边框 */ }/悬停状态/QPushButton:hover {/设置背景颜⾊为半透明的红⾊/background-color: rgba(220,220,220,0.5); } |
playMode | #playMode{ background-image: url(:/images/shuffle_2.png);} |
playPrev | #playPrev{ background-image: url(:/images/up.png);} |
play | #play{ background-image: url(:/images/play3.png);} |
playNext | #playNext{ background-image: url(:/images/down.png);} |
volume | #volume{ background-image: url(:/images/volumn.png);} |
addLocal | #addLocal{ background-image: url(:/images/add.png);} |
lrcWord | #lrcWord{border: none; /* 去除边框 */ background-repeat:no-repeat; background-position:center center;}QPushButton:hover {background-color: rgba(220,220,220,0.5); } |
说明:QPushButton:hover {background-color: rgba(220,220,220,0.5); 表示,当鼠标移到这个控件时,该控件的颜色改变为rgba(220,220,220,0.5);
3.4 自定义控件BtForm
BtForm界⾯设计
添加⼀个新设计界面,命名为BtForm
该控件实际由:图片、文字、动画三部分组成。图⽚和文字分别用QLabel展示,动画部分内部实际为4个QLabel
- 将BtForm的geometry的宽度和高度修改为200*35。
- 拖⼀个Widget到btForm中,objectName修改为BtStyle,将btForm的margin和Spacing设置为0.
- 拖2个QLable、1个Widget和一个horizontalSpacer到btStyle中,并将objectName依次修改为btIconLabel、btTextLabel、lineBox ,btIconLabel的minimumSize和maximumSize的宽度设置为30(为了看到效果可将颜色设置为red) btTextLabel的minimumSize和maximumSize的宽度设置为90(为了看到效果可将颜色设置为green) lineBox的minimumSize和maximumSize的宽度设置为30,然后选中btStyle进行水平对齐,并将其margin和Spacing设置为0
- 然后往lineBox内部拖4个QLabel,objectName依次修改为line1、line2、line3、line4,minimumSize和 maximumSize的宽度均设置为2,选中lineBox进行水平对齐,并将其margin和Spacing设置为0
控件 | QSS美化 |
---|---|
btStyle | #btStyle:hover { background:#D8D8D8;} |
lineBox | .QLabel { background-color:#FFFFFF;} |
将bodyLeft内部onlineMusic和MyMusic中的QWidget全部提升为BtForm。具体操作: 选中要提升的控件,比如:Recommend,在弹出的菜单中选择提升为,会出现⼀个新窗口(如下右侧图),在提升的类名称中输⼊要提升为的类型BtForm,然后点击添加,最后选中btform.h点击提升,便可以将Rec由QWidget提升为自定义的BtForm类型
此时运行的话会出现一个问题,BtForm控件都是一样的,这不是我们预期的结果。
解决BtForm控件显示都是一样内容的问题
// btform.h 新增
// 按钮id:该按钮对应的page⻚
int _pageId = 0;
// 设置图标 ⽂字 id
void setIconAndTextAndId(QString btIcon,QString content,int mid);
--------------------------------------------------------------------------------
// btform.cpp新增
void BtForm::setIconAndTextAndId(QString iconURL, QString text,int pageId){
//修改图标
ui->btIconLabel->setPixmap(iconURL);
//修改内容
ui->btTextLabel->setText(text);
//设置id
this->_pageId = pageId;
}
在LXMusic.cpp的initUi()函数中新增:
//设置btform内容
ui->recommend->setIconAndTextAndId(":/images/recommend.png","推荐",0);
ui->musicHall->setIconAndTextAndId(":/images/musicHall.png", "乐馆", 1);
ui->audio->setIconAndTextAndId(":/images/audio.png", "视频", 2);
ui->likeMusic->setIconAndTextAndId(":/images/likeMusic.png", "喜欢", 3);
ui->recentMusic->setIconAndTextAndId(":/images/recentMusic.png", "最近播放", 4);
ui->localMusic->setIconAndTextAndId(":/images/localMusic.png", "本地和下载", 5);
注意:设置id的目的是为了后面点击某个控件的时候,跳转到stackWidget不同的界面中。
给每个控件设置槽函数,使得点击该控件时,显示相应的效果
按钮响应
重写鼠标mousePressEvent,当按钮按下时:
- 按钮颜色发生变化
- 给LXMusic类发送click信号
// btform.h 新增
protected:
//鼠标点击事件
virtual void mousePressEvent(QMouseEvent*evnet)override;
----------------------------------------------------------------------
// btform.cpp新增
void BtForm::mousePressEvent(QMouseEvent *event)
{
//避免编译器触发警告
(void)event;
//点击修改背景颜色
ui->BtStyle->setStyleSheet("#BtStyle{background-color:rgb(30, 206, 154);}");
//当按钮按下的时候,通过发送信号给主界面对象,修改右边bodyRight的Page界面
//发送信号
emit btClick(_pageId);
}
LXMusic类处理该信号,内部:实现窗口切换,并清除上次按钮点击留下的样式,因此LXMuisc中需要新增:
// lxmusic.h 新增
// btForm点击槽函数
private slots:
void onBtformClicked(int pageId);
------------------------------------------------------
//lxmusic.cpp中
//处理BtForm发来的信号的槽函数
void LXMusic::onBtformClicked(int pageId)
{
//获取btform类型的孩子
QList<BtForm*> btFormList = this->findChildren<BtForm*>();
for(auto btForm : btFormList){
if(btForm->getPageId() != pageId){
btForm->clearBackground();
}
}
//修改stackPage页面
ui->stackedWidget->setCurrentIndex(pageId);
}
//绑定BtForm相关控件信号与槽函数
void LXMusic::connectSignalAndSlots()
{
connect(ui->recommend, &BtForm::btClick, this, &LXMusic::onBtformClicked);
connect(ui->musicHall, &BtForm::btClick, this, &LXMusic::onBtformClicked);
connect(ui->audio, &BtForm::btClick, this, &LXMusic::onBtformClicked);
connect(ui->likeMusic, &BtForm::btClick, this, &LXMusic::onBtformClicked);
connect(ui->recentMusic, &BtForm::btClick, this, &LXMusic::onBtformClicked);
connect(ui->localMusic, &BtForm::btClick, this, &LXMusic::onBtformClicked);
}
//在LXMusic::initUi()函数中调用
connectSignalAndSlots();
因为_pageId是私有的,且改变BtForm的颜色需在BtForm中完成,所以需要添加两个函数:
// btform.h 新增
public:
int getPageId();
void clearBackground();
---------------------------------------------------
//btform.cpp
//获取当前控件对应的stackWidget页面索引
int BtForm::getPageId()
{
return _pageId;
}
//清楚点击后留存的颜色
void BtForm::clearBackground()
{
ui->BtStyle->setStyleSheet("#BtStyle:hover{background:#D8D8D8;}");
}
为了能看到Page切换的效果,可以在stackedWidget的每个page上放⼀个QLabel说明 ,也可以通过QDebug通过控制台输出验证。
BtFrom上的动画效果
Qt中QPropertyAnimation类可以提供简单的动画效果,允许对QObject获取派⽣类的可读写属性进⾏动画处理,创建平滑、连续的动画效果,比如控件的位置、大小、颜⾊等属性变化,使用时需包含QPropertyAnimation头文件,关键函数说明:
/*
功能:实例化QPropertyAnimation类对象
参数:
target: 给target设置动画效果
propertyName:动画如何变化,⽐如:geometry,让target以矩形的⽅式滑动
parent:该动画实例的⽗对象,即将该对象加到对象树中
*/
QPropertyAnimation(QObject *target,
const QByteArray &propertyName,
QObject *parent = nullptr);
/*
功能: 设置动画持续的时⻓
参数: 单位为毫秒
*/
void setDuration(int msecs);
/*
功能:根据value创建关键帧
参数:
step:值再0~1之间,0表⽰开始,1表⽰停⽌
value:动画的⼀个关键帧,即动画现在的形态,假设是基于geometry,可以设置矩形的范围
*/
void setKeyValueAt(qreal step, const QVariant &value);
/*
功能:设置动画的循环次数
参数:
loopCount:默认值是1,表⽰动画执⾏1次,如果是-1,表⽰⽆限循环
*/
void setLoopCount(int loopCount);
/// 槽函数 ///
void pause(); // 暂停动画
void start(QAbstractAnimation::DeletionPolicy policy = KeepWhenStopped); // 开
启动画
void stop(); // 停⽌动画
// 设置动画的起始帧
void setStartValue(const QVariant &value)
// 设置动画的结束帧
void setEndValue(const QVariant &value);
/*
设置动画效果步骤:
1. 创建QPropertyAnimation 对象
2. 设置动画的持续时间
3. 设置动画的关键帧
4. 设置动画的循环次数【⾮必须】,如果未调⽤动画默认执⾏⼀次
5. 开启动画
6. 动画运⾏结束时,会发射finished信号,如果需要进⾏额外处理时,处理该信号即可
*/
lineBox中的lineLabel1、lineLabel2、lineLabel3、lineLabel4添加动画效果,BtForm类中增加如下代码:
//btform.h
public:
void setLabelAnimation(QPropertyAnimation* animation, QLabel* label,int time, int x, int y);
private:
//动画对象
QPropertyAnimation* animationLine1;
QPropertyAnimation* animationLine2;
QPropertyAnimation* animationLine3;
QPropertyAnimation* animationLine4;
------------------------------------------------------------------
//btform.cpp
//设置动画
void BtForm::setLabelAnimation(QPropertyAnimation* animation, QLabel* label,int time, int x, int y)
{
animation = new QPropertyAnimation(label, "geometry", this);
animation->setDuration(time);//持续时间
animation->setKeyValueAt(0,QRect(x,y,2,15));
animation->setKeyValueAt(0.5,QRect(x,y + 15,2,15));
animation->setKeyValueAt(1,QRect(x,y,2,15));
animation->setLoopCount(-1);
animation->start();
}
//btform.cpp中的构造函数中调用上述函数,初始化动画
//设置动画效果
setLabelAnimation(animationLine1,ui->lineLabel1,1500,4,0);
setLabelAnimation(animationLine2,ui->lineLabel2,1600,10,0);
setLabelAnimation(animationLine3,ui->lineLabel3,1700,16,0);
setLabelAnimation(animationLine4,ui->lineLabel4,1800,22,0);
关于动画显示
动画并不是所有页面都显示,只有当前选中的页面显示,所以默认情况下,动画隐藏。
注:后续会修改
//btform.h中
public:
//显示动画
void showAnimation();
//隐藏动画
void hideAnimation();
------------------------------------------
//btform.cpp中
//btform.cpp中的构造函数中调用上述函数,隐藏动画
//隐藏动画
hideAnimation();
//新增函数
void BtForm::showAnimation()
{
ui->lineBox->show();
}
void BtForm::hideAnimation()
{
ui->lineBox->hide();
}
//lxmusic中,修改void LXMusic::onBtformClicked(int pageId)函数
void LXMusic::onBtformClicked(int pageId)
{
//获取btform类型的孩子
QList<BtForm*> btFormList = this->findChildren<BtForm*>();
for(auto btForm : btFormList){
if(btForm->getPageId() != pageId){
//不是当前选中的,清除颜色,隐藏动画
btForm->clearBackground();
btForm->hideAnimation();
}
else{
//是当前选中,显示动画
btForm->showAnimation();
}
}
//修改stackPage页面
ui->stackedWidget->setCurrentIndex(pageId);
}
3.5 推荐页面
推荐页面分析
仔细观察推荐页面,对其进行拆解发现,推荐页面由五部分构成:
图片待写
- "推荐"文本提示,即QLabel
- "今⽇为你推荐"文本提示,即QLabel
- 具体推荐的歌曲内容,点击左右两侧翻页按钮,具有轮番图效果,将光标放到图上,有图⽚上移动画
- "你的歌曲补给站"文本提示,即QLabel
- 具体显示音乐,和③实际是⼀样的,不同的是③中⾳乐只有一行,⑤中的音乐有两行,因为页面中元素较多,直接摆到⼀个页面太拥挤,从右侧的滚动条可以看出,整个页面中的元素都放置在QScrollArea中。
仔细分析3发现,里面包含了:
左右各两个按钮,点击之后中间的图片会左右移动,Qt中未提供类似该种组合控件,因此3实际为自定义控件。
3中按钮之间的元素,由图片和底下的文字组成,当光标放在图⽚上会有上移的动画,因此该元素实际也为自定义控件
推荐页面的布局
在stackedWidget中选中推荐页面,objectName为recommendPage的页面,删掉之前添加的QLabel推荐提示(没添加不用)。
- 拖拽⼀个QScrollArea到recPage中,geometry的宽度和⾼度修改为 820 和 500,样式将边框去掉。
- 拖拽⼀个QLable,objectName修改为recText,显⽰内容修改为推荐,minimumSize和maximumSize的高度均修改为50,Font大小修改为24
- 再拖拽⼀个QLable和Widget,QLable的objectName修改为recMusictext,内容修改为"今日为你推荐",minimumSize和maximumSize的⾼度均修改为30,Font大小修改为18;Widget的objectName修改为recMusicBox
- 再拖拽⼀个QLabel和Widget,QLabel的objectName修改为supplyMusicText,内容修改为"你的⾳乐补给站",minimumSize和maximumSize的⾼度均修改为30,Font大小修改为18;Widget的objectName修改为supplyMusicBox。
- 最后选中QScrollArea,点击垂直布局。
这样整个recommendPage基本就布局完成
美化scollArea滚动条
#scrollArea{
border:none;
}
/* 设置垂直滚动条整体样式 */
QScrollBar:vertical {
border: none; /* 无边框 */
background: #F0F0F0; /* 滚动条背景色 */
width: 5px; /* 垂直滚动条宽度 */
margin: 0px 0px 0px 0px; /* 外边距 */
}
/* 垂直滚动条手柄 */
QScrollBar::handle:vertical {
background: #D8D8D8; /* 手柄颜色 */
border-radius: 3px; /* 圆角半径 */
}
/* 垂直滚动条手柄悬停效果 */
QScrollBar::handle:vertical:hover {
background: #D3D3D3; /* 悬停时颜色加深 */
}
/* 垂直滚动条向上/向下按钮 */
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
border: none;
background: none; /* 隐藏上下按钮 */
height: 0px; /* 设置高度为0隐藏按钮 */
}
/* 垂直滚动条页面区域(手柄上下空白区域) */
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
background: none; /* 页面区域透明 */
}
自定义RecBox
RecBox界⾯布局
- 新添加设计师界面,命名为RecBox。geometry的宽⾼修改为:685*440。
- 添加三个Widget,objectName依次修改为leftPage、musicContent、rightPage; leftPage 和 rightPage的minimumSize和maximumSize修改宽为30,然后选中RecBox点击⽔平布局。将RecBox的margin和Spacing修改为0
- 在upPage和downPage中各拖⼀个按钮,upPage中按钮objectName修改为btUp,minimumSize的⾼度修改为220;downPage中按钮objectName修改为btDown,minimumSize的⾼度修改为220;然后选中upPage和downPage点击⽔平布局。将upPagedownPage和的margin和Spacing修改为0。
- 在musicContent中拖两个Widget,objectName依次修改为recListUp和recListDown,然后选中musicContent点击垂直布局,将musicContent的margin和Spacing修改为0。(为了看清楚效果可临时将recListUp背景色设置为:background-color:green; 将recListDown背景色设置为:background-color:red;)
- 在recListUp和recListDown中分别拖两个⽔平布局器,依次命名为recListUpHLayout和recListDownHLayout,选中recListUp和recListDown点击⽔平布局,将margin和Spacing修改为0
按钮添加如下QSS美化:
名称 | qss样式 |
---|---|
btUp | QPushButton { background-repeat:no-repeat; border:none; background-image : url(:/images/up_page.png); background-position:center center; }QPushButton:hover {background-color: #1ECD97; } |
btDown | QPushButton { background-repeat:no-repeat; border:none; background-image : url(:/images/down_page.png); background-position:center center; } QPushButton:hover {background-color: #1ECD97; } |
将LXMusic主界⾯中recommendPage页面中的recMusicBox和supplyMusicBox提升为RecBox,就能看到如下效果:
自定义recBoxItem
RecBoxItem界面布局
添加⼀个Designer 界面,命名为RecBoxItem,geometry的宽和⾼设置为:150 * 200。
- 拖拽⼀个Widget到RecBoxItem中,objectName修改为musicImageBox,minimumSize和maximumSize的高度均修改为150;
- 拖拽⼀个QLabel到Widget中,objectName修改为recBoxItemText,⽂本设置为"推荐-001",QLabel的alignment属性设置为水平、垂直居中。
- 拖拽⼀个QLabel到musicImageBox中,objectName修改为recMusicImage, geometry设置为:[(0, 0), 150*150]
- 拖拽⼀个QPushButton到musicImageBox中,objectName修改为recMusicBtn,删除掉文本内容。在属性中找到cursor,点击选择⼩⼿图标
#recMusicBtn
{
border:none;
}
RecBoxItem类中添加动画效果
在RecBoxItem类中拦截鼠标进入和离开事件,在进⼊时让图片上移,在离开时让图片下移回到原位。
// RecBoxItem.h 新增
protected:
virtual bool eventFilter(QObject *watched, QEvent *event)override;
// RecBoxItem.cpp 新增
#include <QPropertyAnimation>
//拦截鼠标进入和离开时的事件处理
bool RecBoxItem::eventFilter(QObject *watch, QEvent *event)
{
if(watch == ui->musicImageBox){
int imageWidth = ui->musicImageBox->width();
int imageHeight = ui->musicImageBox->height();
if(event->type() == QEvent::Enter){
QPropertyAnimation* animation = new QPropertyAnimation(ui->musicImageBox,"geometry");
animation->setDuration(100);
animation->setStartValue(QRect(0,10,imageWidth,imageHeight));
animation->setEndValue(QRect(0,0,imageWidth,imageHeight));
animation->start();
//动画结束会发送finished信号,拦截该信号并销毁animation对象
connect(animation, &QPropertyAnimation::finished, this, [=](){
delete animation;
});
return true;
}else if(event->type() == QEvent::Leave){
QPropertyAnimation* animation = new QPropertyAnimation(ui->musicImageBox,"geometry");
animation->setDuration(150);
animation->setStartValue(QRect(0,0,imageWidth,imageHeight));
animation->setEndValue(QRect(0,10,imageWidth,imageHeight));
animation->start();
//动画结束会发送finished信号,拦截该信号并销毁animation对象
connect(animation, &QPropertyAnimation::finished, this, [=](){
delete animation;
});
return true;
}
}
//其他事件交给父类处理
return QObject::eventFilter(watch, event);
}
注意:不要忘记事件拦截器安装,否则时间拦截不到,因此需要在构造函数中添加:
ui->musicImageBox->installEventFilter(this);
该类中还需要添加设置推荐文本和图片的方法,将来需要在外部来设置每个RecBoxItem的文本和图片:
// RecBoxItem.h 新增
void setText(const QString& text);
void setImage(const QString& imagePath);
// RecBoxItem.cpp 新增
void RecBoxItem::setText(const QString& text)
{
ui->recBoxItemText->setText(text);
}
void RecBoxItem::setImage(const QString& imagePath)
{
QString imageStyle = "border-image:url("+imagePath+");";
ui->recMusicImage->setStyleSheet(imageStyle);
}
RecBox添加RecBoxItem
每个RecBoxItem都有对应的图⽚和推荐文本,在往RecBox中添加RecBoxItem前需要先将图片路径和对应文本准备好。由于图片和文本具有对应关系,可以以键值对方式来进行组织,以下实现的时采用QT内置的QJsonObject对象管理图片路径和文本内容:
QJsonObject类:
头⽂件: <QJsonObject>
// 功能: 插⼊<key, value>键值对,如果key已经存在,则⽤value更新与key对应的value
// 返回值:返回指向新插⼊项的键值对
QJsonObject::iterator insert(const QString &key, const QJsonValue &value);
// 功能:获取与key对应的value
// 返回值:返回的value⽤QJsonValue对象组织
QJsonValue QJsonObject::value(const QString &key) const
QJsonArray类
作⽤:管理的是QJsonValue对象
头⽂件:<QJsonArray>
该类重载了[]运算符,可以通过下标⽅式获取管理的QJsonValue对象
QJsonValue operator[](int i) const
QJsonValueRef operator[](int i)
// 往QJsonArray中插⼊⼀个QJsonValue对象
void append(const QJsonValue &value)
QJsonValue类
// 单参构造⽅法,将QJsonObject对象转换为QJsonValue对象
QJsonValue(const QJsonObject &o)
// 将内部管理的数据转化成QJsonObject返回
QJsonObject toObject() const
// 将内部管理的数据转化成QString返回
QString toString() const
图⽚路径和对应文本的准备⼯作,应该在LXMusic类中处理好,RecBoxItem只负责设置,因此该准备工作需要在LXMusic类中进行,故LXMusic中需要添加如下代码:
// LXMusic.h 新增
// 参数num:RecBox中图⽚个数
QJsonArray RandomPicture();
// LXMusic.cpp中新增
//将图片路径打乱
QJsonArray LXMusic::randomPticture()
{
//设置随机数种子
srand(time(0));
//添加推荐图片名称
QVector<QString> imageName;
for(int i = 1; i <= 24; ++i){
imageName << "00" + QString::number(i) + ".jpg";
}
//打乱顺序
std::random_shuffle(imageName.begin(), imageName.end());
QJsonArray objArray;
int n = imageName.size();
for(int i = 0; i < n; ++i){
QJsonObject obj;
obj.insert("imagePath", ":/image/" + imageName[i]);
// arg(i, 3, 10, QCchar('0'))
// i:要放⼊%1位置的数据
// 3: 三位数
// 10:表⽰⼗进制数
// QChar('0'):数字不够三位,前⾯⽤字符'0'填充
QString strText = QString("推荐-%1").arg(i + 1, 3, 10, QChar('0'));
obj.insert("text", strText);
objArray.append(obj);
}
return objArray;
}
recBox中添加元素
由于recPage页面中有两个RecBox控件,上面的RecBox为一行四列,下方的RecBox为2行四列,因此在RecBox类中增加以下成员变量:
// RecBox.h 新增
#include <QJsonArray>
public:
void initRecBoxUi(QJsonArray data, int row);
void createRecBoxItem();
private:
int row; // 记录当前RecBox实际总⾏数
int col; // 记录当前RecBox实际每⾏有⼏个元素
QJsonArray imageList; // 保存界⾯上的图⽚, ⾥⾯实际为key、value键值对
RecBox的构造函数中,将row和col默认设置为1和4,count需要具体来计算
RecBox::RecBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::RecBox),
row(1),
col(4)
{
ui->setupUi(this);
}
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
// 如果是两⾏,说明当前RecBox是主界⾯上的supplyMusicBox
if(2 == row)
{
this->row = row;
this->col = 8;
}
else
{
// 否则:只有⼀⾏,为主界⾯上recMusicBox
ui->recBoxBottom->hide();
}
// 图⽚保存起来
imageList = data;
//向recListUpHlyout添加元素
createRecBoxItem();
}
void RecBox::createRecBoxItem()
{
for(int i = 0; i < _col; ++i){
RecBoxItem* item = new RecBoxItem();
QJsonObject obj = _imageList[i].toObject();
item->setText(obj.value("text").toString());
item->setImage(obj.value("imagePath").toString());
if(i >= _col / 2 && _row == 2){
//添加八个元素
ui->recListDownHLayout->addWidget(item);
}
else{
//添加四个元素
ui->recListUpHLayout->addWidget(item);
}
}
}
效果:
RecBox中btUp和btDown按钮clicked处理
添加槽函数
选中recbox.ui⽂件,分别选中btUp和btDown,右键单击弹出菜单选择转到槽,选中clicked确定, btUp和btDown的槽函数就添加好了
imageList中图⽚分组
假设imageList中有24张图片路径和推荐文本信息,如果将信息分组:
-
如果是recMusicBox,将元素按照col分组,即每4个元素为⼀组,可分为6组;如果是supplyMuscBox,将元素按照col分组,即每8个元素为⼀组,可分为3组。
-
RecBox类中添加currentIndex和count整形成员变量,currentIndex记录当前显⽰组,count记录总的信息组数。当点击btUp时,currentIndex–,显⽰前⼀组,如果currentIndex⼩于0时,将其设置为count-1;当点击btDown按钮时,currentIndex++显⽰下⼀组,当currentIndex为count时,将count设置为0.
这样就实现了轮番显示效果
// recbox.h 中新增
int currentIndex; // 标记当先显⽰第⼏组图⽚和推荐信息
int count; // 标记imageList中元素按照col分组总数
//recbox.cpp
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
if(row == 2){
_row = row;
_col = 8;
}
else{
//隐藏RecBoxDownHlyout
ui->recListDown->hide();
}
_imageList = data;
//当前在第几组,默认0组
_currentIndex = 0;
_count = ceil(data.size() / _col); //算出有多少组
createRecBoxItem();
}
void RecBox::on_btUp_clicked()
{
_currentIndex--;
if(_currentIndex < 0)_currentIndex = 0;
createRecBoxItem();
}
void RecBox::on_btDown_clicked()
{
_currentIndex++;
if(_currentIndex == _count)_currentIndex = 0;
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;
}
int index = 0;
for(int i = _currentIndex * _col; i < _col + _currentIndex * _col; ++i){
RecBoxItem* item = new RecBoxItem();
QJsonObject obj = _imageList[i].toObject();
item->setText(obj.value("text").toString());
item->setImage(obj.value("imagePath").toString());
if(index >= _col / 2 && _row == 2){
ui->recListDownHLayout->addWidget(item);
}
else{
ui->recListUpHLayout->addWidget(item);
}
++index;
}
}
效果图:
这样实现每次启动程序,推荐页面推荐的元素都是一样的,所以可以在lxmusic.h文件中的LXMusic类的构造函数中加上随机数种子:
srand(time(0));
3.6 自定义CommonPage
CommonPage页面布局
-
新增加⼀个设计界面,objectName修改为CommonPage,geometry的宽高修改为800*500
-
拖拽⼀个QLabel、两个Widget和⼀个List View控件到CommonPage中,并将List View变形为QLIstWidget,objectName从上往下依次修改为pageTittle、musicPlayBox、listLabelBox、pageMusicList,然后选中CommonPage点击垂直布局,将CommonPage的margin和Spacing修改为0
-
pageTittle的minimumSize和maximumSize的⾼度修改为30。
-
musicPlayBox的minimumSize和maximumSize的⾼度修改为150。
-
listLabelBox的minimumSize和maximumSize的⾼度修改为40
-
musicPlayBox中拖拽⼀个QLabel,objectName修改为musicImageLabel,minimumSize和maximumSize的宽度修改为150 ,拖拽⼀个Widget,objectName修改为playAll,minimumSize和maximumSize的宽度修改为120,在其内部拖拽 ⼀个PushButton和Vertical Space(即垂直弹簧),将按钮的objectName修改为playAllBtn,minimumSize和 maximumSize的宽和⾼修改为100*30,文本内容修改为"播放全部",然后选中playAll点击垂直布局,拖拽⼀个Horizontal Spacer到CommonPage中,放在playAll之后。然后选中musicPlayBox,点击水平布局,将margin和spacing设置为0.
-
listLabelBox中拖拽三个QLabel,内容依次修改为:歌曲名称、歌手名称、专辑名称,objectName从左往右依次修改为:musicNameLabel、musicSingerLabel、musicAlbumLabel然后选中musicPlayBox,点击水平布局,将margin和spacing设置为0
界面美化
//给下面控件设置qss样式
//musicAlbumLabel,musicNameLabel,musicSingerLabel
QLabel {
text-align: left; /* 文字左对齐 */
padding-left: 40px; /* 文字向右偏移 10px */
padding-right: 0; /* 右侧无内边距 */
font-size: 10px; /* 设置字体大小为 16px */
font-weight: bold; /* 设置字体加粗 */
}
//pageTittle
QLabel {
text-align: left; /* 文字左对齐 */
padding-left: 25px; /* 文字向右偏移 10px */
padding-right: 0; /* 右侧无内边距 */
font-size: 30px; /* 设置字体大小为 16px */
font-weight: bold; /* 设置字体加粗 */
}
//playAllBtn
#playAllBtn
{
background-color:#E3E3E3;
border-radius:10px;
}
#playAllBtn:hover
{
background-color:#1ECD97;
}
//pageMusicList
#pageMusicList{border:none;}
CommonPage界⾯设置和显⽰
CommonPage页面是我喜欢、本地下载、最近播放三个界面的共同类型,因此该类需要提供设置: pageTittle 和 musicImageLabel的公共方法,将来在程序启动时完成三个界面信息的设置,因此CommonPage类需要添加⼀个public的setCommonPageUI函数
// commonpage.h 中新增
public:
void setCommonPageUi(const QString &title, const QString &image);
// commonpage.cpp 中新增
void CommonPage::setCommonPageUi(const QString &title, const QString &image)
{
//设置标题
ui->pageTittle->setText(title);
//设置图片
ui->musicImageLabel->setPixmap(QPixmap(image));
//是图片自动缩放
ui->musicImageLabel->setScaledContents(true);
}
界面设置的函数需要在程序启动时就完成好配置,即需要在LXMusic的initUi()函数中调⽤完成设置:
3.7 自定义ListItemBox
ListItemBox页面分析
CommonPage页面创建好之后,等音乐加载到程序之后,就可以将音乐信息往CommonPage的pageMusicList中显⽰了
图片代谢:
上图每行都是QListWidget中的⼀个元素,每个元素中包含多个控件:
- 收藏图标,即QLabel
- 歌曲名称,即QLabel
- VIP和SQ,VIP即收费会员专享,SQ为无损⾳乐,也是两个QLabel
- 歌手名称,即QLabel
- 音乐专辑名称,即QLabel
此处,需要将上述所有QLabel组合在⼀起,作为⼀个独立的控件,添加到QListWidget中,因此该控件也需要自定义
ListItemBox页面布局
添加⼀个设计师界面,objectName为ListItemBox,geometry的宽度和高度修改为800*45
- 拖三个Widget到ListItemBox中,objectName从左往右依次修改为musicNameBox、musicSingerBox、 musicAlbumBox,将musicNameBox的minimumSize和maximumSize的宽修改为380,将musicSingerBox的minimumSize和maximumSize的宽修改为200,然后选中ListItemBox,点击水平布局,将ListItemBox的margin和spacing修改为0
- musicNameBox: 拖拽⼀个QPushButton到musicNameBox中,objectName修改为likeBtn,minimumSize和maximumSize的宽高修改为25*25。 拖⼀个QLabel到musicNameBox中,objectName修改为musicNameLabel,minimumSize和的宽修改为130. 拖⼀个QLabel到musicNameBox中,objectName修改为VIPLabel,minimumSize和maximumSize的宽修改为30,maximumSize高度修改为15,文本内容修改为VIP。 拖⼀个QLabel到musicNameBox中,objectName修改为SQLabel,minimumSize和maximumSize的宽修改为25,maximumSize高度修改为15,文本内容修改为SQ。 拖拽⼀个水平弹簧控件到musicNameBox中,将上述控件撑到musicNameBox的左侧 选中musicNameBox,点击水平布局,将musicNameBox的margin和spacing修改为0
- 拖拽⼀个QLabel到musicSingerBox中,objectName修改为musicSingerLabel,然后选中musicSingerBox点击水平布局,将musicSingerBox的margin和spacing修改为0
- 拖拽⼀个QLabel到albumBox中,objectName修改为albumNameLabel,然后选中albumBox点击水平布局,将musicSingerBox的margin和spacing修改为0
界面美化
#likeBtn
{
border:none;
}
#VIPLabel
{
border:1px solid #1ECD96;
color:#1ECD96;
border-radius:2px;
}
#SQLabel
{
border:1px solid #FF6600;
color:#FF6600;
border-radius:2px;
}
ListItemBox显⽰测试
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);
}
⽀持hover效果
ListItemBox添加到CommonPage中的QListWidget之后,自带hover效果,但是背景颜色和界面不太搭配,此处重新实现hover效果,此处重写enterEvent和leaveEvent来实现hover效果
//listitembox.h
protected:
virtual void enterEvent(QEnterEvent* event)override;
virtual void leaveEvent(QEvent*event)override;
//listitembox.cpp
//鼠标离开,取消背景色
void ListItemBox::leaveEvent(QEvent *event)
{
(void)event;
setStyleSheet("");
}
//鼠标进入,修改背景色
void ListItemBox::enterEvent(QEnterEvent *event)
{
(void)event;
setStyleSheet("background-color:#EFEFEF");
}
3.8 ⾃定义MusicSlider
由于QT内置的Horizontal Slider(⽔平滑竿)不是很好看,该控件也采用自定义。 该控件比较简单,实际就是两个QFrame嵌套起来的
- 添加⼀个设计师界面,objectName修改为MusicSlider,geometry修改为800*20。 *
- 拖拽⼀个QFrame,objectName修改为inLine,geometry修改为[(0,8), 8004]。
- 拖拽⼀个QFrame,objectName修改为outLine,geometry修改为[(0,8), 400*4]。
- 选中MusicSlider,点击水平布局。
inLine和outLine的样式设置如下:
#inLine
{
background-color:#EBEEF5;
}
#outLine
{
background-color:#1ECC94;
}
打开LXMusic.ui,选中progressBar清除之前样式,将progressBar提升为MusicSlider,运行程序就能看到效果
3.9 自定义volumeTool
VolumeTool控件分析
音量调节控件本来也可以使⽤Qt内置的垂直滑杆来代替,只是垂直滑杆不好看,因此也自定义
- 内部为类似MusicSlider控件+小圆球,圆球实际为⼀个QPushButton
- 音量大小文本显示,实际为QLabel
- QPushButton,点击之后在静音和取消静音切换
- ⼀个倒三角,Qt未提供三⻆控件,该控件需要手动绘制,用来提示是播放控制区那个按钮按下的
VolumeTool界⾯布局
- 生成⼀个QT设计师界面,objectName命名为VolumeTool,geometry的宽⾼修改为100*350。
- 拖拽⼀个Widget到VolumeTool中,objectName修改为volumeWidget,geometry修改为:[(10,10), 80*300]
- *拖拽⼀个QPushButton到volumeWidget,objectName修改为silenceBtn,mimimumSize和maximumSize的宽高修改为80,45
- 拖拽⼀个QLabel到volumeWidget,objectName修改为volumeRatio,mimimumSize和maximumSize的高修改为30,QLabel的alignment属性修改为水平和垂直居中。
- 拖拽⼀个QWidget到volumeWidget,objectName修改为sliderBox。geometry修改为:[(0,0), 80*225]
- sliderBox内部: 拖拽⼀个QFrame,objectName修改为inSlider,geometry修改为[(38, 25), 4,180]。拖拽⼀个QFrame,objectName修改为outSlider,geometry修改为[(38, 25), 4,180]。 拖拽⼀个QPushButton,objectName修改为sliderBtn,geometry修改为[(33, 20), 14,14], mimimumSize和maximumSize的宽高14*14
美化界面
#volumeWidget{
background-color:#ffffff;
border-radius:5px;
}
#silenceBtn
{
border:none;
}
#silenceBtn:hover{
background-color:#F0F0F0;
}
#inSlider
{
background-color:#ECECEC;
}
#outSlider
{
background-color:#1ECC94;
}
#sliderBtn
{
background-color:#1ECC94;
border-radius:7px;
}
注意:静音底下的空缺用来绘制三角
界⾯设置
该控件属于弹出窗口,即点击了主界面的音量调节按钮后,才需要弹出该界面,点击其他位置该界面自动隐藏。因此在窗口创建时,需要设置窗口为无边框以及为弹出窗口
//volumetool.cpp中
//在构造函数中添加如下内容:
VolumeTool::VolumeTool(QWidget *parent)
: QWidget(parent)
, ui(new Ui::VolumeTool)
{
ui->setupUi(this);
// 在windows上,设置透明效果后,窗⼝需要加上Qt::FramelessWindowHint格式
// 否则没有控件位置的背景是⿊⾊的
// 由于默认窗⼝有阴影,因此还需要将窗⼝的原有的阴影去掉,窗⼝需要加上Qt::NoDropShadowWindowHint
this->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
//将窗口设为透明
this->setAttribute(Qt::WA_TranslucentBackground);
//给窗口设置自定义阴影
QGraphicsDropShadowEffect* se = new QGraphicsDropShadowEffect(this);
se->setBlurRadius(10);
se->setOffset(0,0);
se->setColor("#64646464");
this->setGraphicsEffect(se);
//设置图片
ui->silenceBtn->setIcon(QIcon(":/images/sound.png"));
// ⾳量的默认⼤⼩是20
ui->outSlider->setGeometry(ui->outSlider->x(), 180 - 36 - 25, ui->outSlider->width(), 20);
ui->sliderBtn->move(ui->sliderBtn->x(), ui->outSlider->y() - ui->sliderBtn->height()/2);
ui->volumeRatio->setText("20%");
}
音量调节属于主界面上元素,因此在LXMusic类中需要添加VolumeTool的对象,在initUi中new该类的对象。
主界面中音量调节按钮添加clicked槽函数
//lxmusic.h文件中
private:
VolumeTool* _volumetool;
//lxmusic.cpp中
//构造函数中创建VolumeTooll类对象
_volumetool = new VolumeTool();
//点击volumeButton后调用的槽函数
void LXMusic::on_volumeButton_clicked()
{
//获取volumeButton左上角的坐标
QPoint point = ui->volumeButton->mapToGlobal(QPoint(0,0));
//计算volume窗⼝的左上⻆位置
// 让该窗⼝显⽰在⿏标点击的正上⽅
// ⿏标位置:减去窗⼝宽度的⼀半,以及⾼度恰巧就是窗⼝的左上⻆
QPoint volumeLeftTop = point - QPoint(_volumetool->width()/2, _volumetool->height());
// 微调窗⼝位置
volumeLeftTop.setY(volumeLeftTop.y()+30);
volumeLeftTop.setX(volumeLeftTop.x()+15);
// 3. 移动窗⼝位置
_volumetool->move(volumeLeftTop);
_volumetool->show();
}
绘制三⻆
由于Qt中并未给出三角控件,因此三角需要手动绘制,故在VolumeTool类中重写paintEvent事件函数
// volumetool.h中新增
virtual void paintEvent(QPaintEvent* event)override;
// volumetool.cpp中新增
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(QBrush(Qt::white));
// 创建⼀个三⻆形
QPolygon polygon;
polygon.append(QPoint(30, 320));
polygon.append(QPoint(70, 320));
polygon.append(QPoint(50, 340));
// 绘制三⻆形
painter.drawPolygon(polygon);
}
3.10 更换主窗口图标
更换窗口图标,在主界面显示时,在标题栏显示设置的图标
//lxmusic.cpp
void LXMusic::initUi(){
........
//更换主窗口图标
this->setWindowIcon(QIcon(":/images/logo.png"));
............
}
3.11 处理最小化和最大化按钮
由于窗口中控件并非全部基于Widget布局,有些控件的位置是计算死得,窗口最大化时有些控件可能无法适配尺寸,因此禁止窗口最大化
//lxmusic.h
private slots:
void on_minButton_clicked();
void on_skinButton_clicked();
//lxmusic.cpp
//最小化
void LXMusic::on_minButton_clicked()
{
showMinimized();
}
//换肤图标
void LXMusic::on_skinButton_clicked()
{
QMessageBox::information(this, "温馨提⽰", "正在加班紧急⽀持中...");
}
void LXMusic::initUi(){
........
//禁用最大化图标
ui->maxButton->setEnabled(false);
............
}
四、音乐管理
界⾯处理好之后,现在就需要将⾳乐⽂件加载到程序然后显示在界面上,待后续播放操作
4.1 音乐加载
lxmusic类中给addLocal添加槽函数。
音乐文件在磁盘中,可以借助QFileDialog类完成音乐文件加载。QFileDialog类中函数介绍:
//构造函数:
QFileDialog(QWidget *parent = nullptr, // 指定该对象的⽗对象
const QString &caption = QString(), // 设置窗⼝标题
const QString &directory = QString(), // 设置默认打开⽬录
const QString &filter = QString()) // 设置过滤器,可以只打开指定后缀⽂件
//默认创建的是打开对话框
//⽂件过滤器:
//⽐如:打开指定⽂件夹下所有.cpp .h 以及.png的⽂件
QString filter = "代码⽂件(.cpp *.h)";
//过滤器可以在构造QFileDialog对象时传⼊,也可以通过setNameFilters函数设置
void setNameFilters(const QStringList &filters);
//有些时候⽂件的后缀不⼀定能给全,⽐如图⽚格式:.pnp .bmp .jpg等,有些格式甚⾄没有接触过,
//但也属于图⽚⽂件,该种情况下最好使⽤MIME类型过滤
//MIME类型(Multipurpose Internet Mail Extensions)是⼀种互联⽹标准,⽤于表⽰⽂档、⽂件或字节流的性质和格式。
//语法:type/subType
//⽐如:text/plain 表⽰⽂本⽂件 application/octet-stream表⽰通⽤的⼆进制数据流的MIME类
void setMimeTypeFilters(const QStringList &filters)
// 设置打开对话框的类型
QFileDialog::AcceptOpen:表⽰对话框为打开对话框
QFileDialog::AcceptSave:表⽰对话框为保存对话框
void setAcceptMode(QFileDialog::AcceptMode mode);
// 设置选择⽂件的数量和类型
void setFileMode(QFileDialog::FileMode mode);
QFileDialog::AnyFile //⽤⼾可以选择任何⽂件,甚⾄指定⼀个不存在的⽂件
QFileDialog::ExistingFile //⽤⼾只能选择单个存在的⽂件名称
QFileDialog::Directory //⽤⼾可以选择⼀个⽬录名称
QFileDialog::ExistingFiles //⽤⼾可以选择⼀个或者多个存在的⽂件名称
// 设置⽂件对话框的当前⽬录
void setDirectory(const QString &directory);
// 获取当前⽬录
QDir::currentPath();
当点击加号的时候,弹出选择文件的窗口:
void LXMusic::on_addLocalButton_clicked()
{
//创建QFileDialog类对象
QFileDialog fileDialog(this);
fileDialog.setWindowTitle("添加本地音乐");
//设置为打开窗口
fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
//设置对话框模式
//设置该窗口为打开可选择一个或多个文件的窗口
fileDialog.setFileMode(QFileDialog::ExistingFiles);
//设置窗口过滤器
QStringList mimeList;
mimeList << "application/octet-stream";
fileDialog.setMimeTypeFilters(mimeList);
//设置对话框默认的打开路径,设置⽬录为当前⼯程所在⽬录
QDir dir(QDir::currentPath());
dir.cdUp();
dir.cdUp();
QString path = dir.path() + "/music";
fileDialog.setDirectory(path);
if(QFileDialog::Accepted == fileDialog.exec()){
//获取btform类型的孩子
QList<BtForm*> btFormList = this->findChildren<BtForm*>();
for(auto btForm : btFormList){
if(btForm->getPageId() != 5){
btForm->clearBackground();
btForm->hideAnimation();
}
else{
btForm->showAnimation();
btForm->setBackground();
}
}
//修改stackPage页面
ui->stackedWidget->setCurrentIndex(5);
// 获取对话框的返回值
QList<QUrl> urls = fileDialog.selectedUrls();
//获取到的音乐后续处理
}
}
4.2 Music类
将来添加到播放器中的音乐比较多,可借助⼀个类对所有的音乐进行管理。添加新C++类与添加设计师界⾯类似
每首音乐文件,将来需要获取其内部的歌曲名称、歌手、音乐专辑、歌曲时长等信息,因此在MusicList类中,将所有的歌曲文件以Music对象方式管理起来
//music.h
public:
Music();
public:
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::Music():_isLike(false),_isHistory(false){}
Music::Music(const QUrl &url)
:_isLike(false)
,_isHistory(false)
,_musicUrl(url)
{
_musicId = QUuid::createUuid().toString();
}
void Music::setIsLike(bool isLike)
{
_isLike = isLike;
}
void Music::setIsHistory(bool isHistory)
{
_isHistory = isHistory;
}
void Music::setMusicName(const QString &musicName)
{
_musicName = musicName;
}
void Music::setSingerName(const QString &singerName)
{
_singerName = singerName;
}
void Music::setAlbumName(const QString &albumName)
{
_albumName = albumName;
}
void Music::setDuration(const qint64 duration)
{
_duration = duration;
}
void Music::setMusicUrl(const QUrl &url)
{
_musicUrl = url;
}
void Music::setMusicId(const QString &musicId)
{
_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;
}
4.3 musicList类的实现
LXMusic中,通过QFileDialog将⼀组音乐文件的url获取到之后,可以交给MusicList类来管理。 但是LXMusic加载的二进制文件不一定全部都是音乐文件,因此MusicList类中需要对文件的MIME类型再次检测,以筛选出真正的音乐文件
QMimeDatabase类是Qt中主要用于处理文件的MIME类型,经常用于:
-
文件类型识别
-
文件过滤
-
多媒体文件处理
-
文件导入导出
-
文件管理器
该类中的mimeTypeForFile函数可用于获取给定文件的MIME类型
// QMimeDatabase类的mimeTypeForFile⽅法
// 功能:获取fileName⽂件的MIME类型
// fileName:⽂件的名称
// mode: MatchMode为枚举类型,表明如何匹配⽂件的MIME类型
// MatchDefault: 通过⽂件名和⽂件内容来进⾏查询匹配,⽂件名优先于⽂件内容,如果⽂件扩展名未知,或者匹配多个MIME类型,则使⽤⽂件内容匹配
// MatchExtension: 通过⽂件名查询匹配
// MatchContent:通过⽂件内容来查询匹配
QMimeType mimeTypeForFile(const QString &fileName,MatchMode mode = MatchDefault) const
// QMimeType类中的name属性,保存了获取到的MIME类型,
// 可以通过该类的name()⽅法以字符串⽅式返回MIME类型
QString name();
对于歌曲⽂件:
- audio/mpeg: 适用于mp3格式的⾳乐文件
- audio/flac: 无损压缩的音频文件,不会破坏任何原有的音频信息
- audio/wav : 表示wav格式的歌曲文件
- audio/mp3:表示mp3格式的歌曲文件
上述歌曲文件格式,Qt的QMediaPlayer类都是支持的
//musiclist.h
typedef typename QVector<Music>::iterator iterator;
public:
MusicList();
public:
void addMusicByUrl(const QList<QUrl>&url);
iterator begin();
iterator end();
private:
QVector<Music> _musicList;
//musiclist.cpp
MusicList::MusicList() {}
//让musiclist类能够支持范围for的使用
iterator MusicList::begin()
{
return _musicList.begin();
}
iterator MusicList::end()
{
return _musicList.end();
}
//通过音乐路径构造music对象,然后添加到musiclist中管理
void MusicList::addMusicByUrl(const QList<QUrl> &url)
{
for(const auto& e : url){
QMimeDatabase mimedata;
QMimeType mime = mimedata.mimeTypeForFile(e.toLocalFile());
if(mime.name() != "audio/mpeg" && mime.name() != "audio/flac" && mime.name() != "audio/mp3")
{
continue;
}
// 如果是⾳乐⽂件,加⼊歌曲列表
_musicList.push_back(e);
}
}
4.4 解析音乐文件元数据
对于每首歌曲,将来在界面上需要显示出:歌曲名称、歌手、专辑名称,在播放时还需要拿到歌曲总时长,因此在构造音乐对象时,就需要将上述信息解析出来。歌曲元数据解析,需要⽤到 QMediaPlayer ,该类也是用来进行歌曲播放的类,后续在播放音乐位置详细介绍
QMediaPlayer类中的setSource()函数
// 功能:设置要播放的媒体源,媒体数据从中读取
// media: 要播放的媒体内容,⽐如⼀个视频或⾳频⽂件,该类提供了⼀个QUrl格式的单参构造
void setSource(const QUrl& url);
注意:该函数执行后立即返回,不会等待媒体加载完成,也不检查错误,如果在媒体加载时发生错误,会触发mediaStatusChanged和error信号 ,由于加载媒体文件需要时间,可以通过QMediaPlayer::metaDataChanged信号检测媒体数据是否就绪,媒体元数据加载成功之后,可以通过metaData函数获取指定的媒体数据
//获取音乐名称
_musicName = player.metaData().value(QMediaMetaData::Title).toString();
本⽂需要获取媒体的:标题、作者、专辑、持续时长,以下是需要遵守的传入名称
valye(传入的参数) | description(说明) | type(返回类型) |
---|---|---|
QMediaMetaData::Title | 媒体的标题 | QString |
QMediaMetaData::Author | 媒体的作者 | QStringList |
QMediaMetaData::AlbumTitle | 媒体所属专辑名称 | QString |
QMediaMetaData::Duration | 媒体的播放时长 | qint64 |
注意:有些媒体中媒体数据可能不全,即有些媒体数据获取不到,比如盗版歌曲
使用QMediaPlayer媒体播放类时,需要在LXMusic.pro项目工程文件中添加媒体模块multimedia ,该模块主要用来播放各种音频视频文件等.
QT += core gui multimedia
添加完成之后,重新将项⽬构建⼀下,否则Qt create可能识别不过来
对音乐文件进行解析:
// music.h 中新增
#include<QUrl>
#include<QMediaPlayer>
#include <QMediaMetaData>
private:
void parseMediaMetaData();
// music.cpp 中新增
//解析元媒体的数据信息,保存到music类中
void Music::parseMediaMetaData()
{
QMediaPlayer player;
//qDebug() << "nihao";
// 媒体元数据解析需要时间,只有等待解析完成之后,才能提取⾳乐信息
// 当元数据加载完成以后,会发送metadataChanged信号,此时就可以处理我们需要的内容了
// 连接元数据变化信号
QObject::connect(&player, &QMediaPlayer::metaDataChanged, [&]() {
// 提取元数据
_musicName = player.metaData().value(QMediaMetaData::Title).toString();
_singerName = player.metaData().value(QMediaMetaData::Author).toString();
_albumName = player.metaData().value(QMediaMetaData::AlbumTitle).toString();
_duration = player.duration();
QString fileName = _musicUrl.toString();
int i = fileName.size() - 1;
for(; i >= 0; --i){
if(fileName[i] == '/')break;
}
int index = fileName.indexOf("-");
if(_musicName.isEmpty())
{
if(index != -1)
{
// "动物世界 - 薛之谦.mp3"
_musicName = fileName.mid(i + 1, index - i - 1).trimmed();
}
else
{
// "动物世界.mp3"
_musicName = fileName.mid(i + 1, fileName.indexOf('.') - i - 1).trimmed();
}
}
// 作者为空
if(_singerName.isEmpty())
{
if(index != -1)
{
_singerName = fileName.mid(index+1, fileName.indexOf('.')-index-1).trimmed();
}
else
{
_singerName = "未知歌⼿";
}
}
if(_albumName.isEmpty()){_albumName = "未知专辑";}
// 打印结果
qDebug() << "歌曲名称:" << (_musicName.isEmpty() ? "未知" : _musicName);
qDebug() << "歌手:" << (_singerName.isEmpty() ? "未知" : _singerName);
qDebug() << "专辑:" << (_albumName.isEmpty() ? "未知" : _albumName);
qDebug() << "时长:" << _duration << "毫秒";
});
// 解析时候需要读取歌曲数据,读取歌曲⽂件需要⽤到QMediaPlayer类
//加载歌曲文件
player.setSource(_musicUrl);
}
4.5 音乐分类
QQMusic中,有三个显示歌曲信息的页面:
-
likePage:管理和显示点击小心心后收藏的歌曲
-
localPage:管理和显示本地加载的歌曲
-
recentPage:管理和显示历史播放过的歌曲
这三个页面的类型都是CommonPage,每个页面应该维护自己页面中的歌曲。因此CommonPage类中需要新增:
// commonpage.h中新增
// 区分不同page⻚⾯
enum PageType{
LIKEPAGE,
RECENTPAGE,
LOCALPAGE
};
// 新增成员函数
public:
void setMusicListType(PageType pageType);
private:
//歌单列表
QVector<QString> musicOfPage; //存放音乐的数组
PageType _pageType;//标记属于哪个页面
//commonpage.cpp
void CommonPage::setMusicListType(PageType pageType)
{
_pageType = pageType;
}
//lxmusic.cpp
void initUi()
{
.......
//初始化commandpage界面
ui->likeMusicPage->setCommonPageUi("我喜欢的音乐",":/images/likemusic.jpg");
ui->likeMusicPage->setMusicListType(PageType::LIKEPAGE);
ui->recentMusicPage->setCommonPageUi("最近播放",":/images/recentmusic.jpg");
ui->recentMusicPage->setMusicListType(PageType::RECENTPAGE);
ui->localMusicPage->setCommonPageUi("本地下载",":/images/localmusic.jpg");
ui->localMusicPage->setMusicListType(PageType::LOCALPAGE);
}
QQMusic中,点击addLocal(本地加载)按钮后,会通过其musicList成员变量,将music添加到musicList中管理,在添加过程中,每个歌曲会对应⼀个Music对象,Music对象在构造时,会完成歌曲文件的加载,顺便完成歌曲名称、作者、专辑名称等元数据的解析。⼀切准备就绪之后,每个CommonPage页面,通过LXMusic的musicList分离出自己页面的歌曲,保存在musicOfPage中
//commonPage.h
void addMusicToMusicPage(MusicList& musicList);
//commonPage.cpp
//将musicList中的音乐分别保存在对应的Page中
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
//删除存在的旧数据
_musicOfPage.clear();
ui->pageMusicList->clear();
for(auto& music : musicList){
switch(_pageType){
case PageType::LIKEPAGE:
{
if(music.getIsLike()){
_musicOfPage.push_back(music.getMusicId());
}
break;
}
case PageType::RECENTPAGE:
{
if(music.getIsHistory()){
_musicOfPage.push_back(music.getMusicId());
}
break;
}
case PageType::LOCALPAGE:
{
_musicOfPage.push_back(music.getMusicId());
break;
}
default:
{
qDebug() << "功能暂未支持";
}
}
}
}
4.6 Music数据保存并显示到界面中
通过QFileDialog将⾳乐从本地磁盘加载到程序中后,拿到的是所有音乐文件的QUrl,而在程序中需要的是经过元数据解析之后的Music对象,并且Music对象需要管理起来,此时就可以采用MusicList类对解析之后的Music对象进行管理,LXMusic类中只需要保存MusicList的对象,就可以让lxmusic.ui界⾯中CommonPage对象完成Music信息往界面更新
//lxmusic.h
void LXMusic::on_addLocalButton_clicked()
{
.....
//将音乐保存到musiclist中
_musicList.addMusicByUrl(urls);
//将添加的音乐显示在localPage中
ui->localMusicPage->refreshPage(_musicList);
}
refreshPage函数的实现步骤:
- 调用addMusicIdPageFromMusicList函数,从musicList中添加当前页面的歌曲
- 遍历musicListOfPage,拿到每首音乐后先检查其是否在,存在则添加
- 界面上需要更新每首歌曲的:歌曲名称、作者、专辑名称,而commonPage中只保存了歌曲的musicId,因此需要在MusicList中增加通过musicID查找Music对象的方法
// commonpage.h中新增
void refreshPage(MusicList& musicList);
//commonPage.cpp中新增
void CommonPage::refreshPage(MusicList &musicList)
{
addMusicToMusicPage(musicList);
for(auto& musicId : _musicOfPage){
auto it = musicList.findMusicById(musicId);
if(it == musicList.end())continue;
//初始化ListItemBox对象
ListItemBox * listItemBox = new ListItemBox(ui->pageMusicList);
listItemBox->setMusicName(it->getMusicName());
listItemBox->setSingerName(it->getSingerName());
listItemBox->setAblumName(it->getAlbumName());
//设置QListWigetItem对象的宽和高
QListWidgetItem* item = new QListWidgetItem(ui->pageMusicList);
item->setSizeHint(QSize(ui->pageMusicList->width(),45));
//添加到QListWiget中
ui->pageMusicList->setItemWidget(item,listItemBox);
}
// 更新完成后刷新下界⾯
repaint();
}
//listitembox.h
public:
void setMusicName(const QString& MusicName);
void setSingerName(const QString& SingerName);
void setAblumName(const QString& AblumName);
void ListItemBox::setMusicName(const QString &MusicName)
{
ui->musicNameLabel->setText(MusicName);
}
void ListItemBox::setSingerName(const QString &SingerName)
{
ui->musicSingerLabel->setText(SingerName);
}
void ListItemBox::setAblumName(const QString &AblumName)
{
ui->albumNameLabel->setText(AblumName);
}
//musiclist.h
iterator findMusicById(QString& musicId);
//musiclist.cpp
iterator MusicList::findMusicById(QString &musicId)
{
iterator it = _musicList.begin();
while(it != _musicList.end()){
if(it->getMusicId() == musicId){
break;
}
++it;
}
return it;
}
4.7 优化QListWidget
移除QListWidget的水平滚动条
//commonpage.cpp
CommonPage::CommonPage(QWidget *parent)
: QWidget(parent)
, ui(new Ui::CommonPage)
{
ui->setupUi(this);
// 不要⽔平滚动条
ui->pageMusicList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}
美化垂直滚动条
#pageMusicList{
border:none;
}
/* 设置垂直滚动条整体样式 */
QScrollBar:vertical {
border: none; /* 无边框 */
background: #F0F0F0; /* 滚动条背景色 */
width: 5px; /* 垂直滚动条宽度 */
margin: 0px 0px 0px 0px; /* 外边距 */
}
/* 垂直滚动条手柄 */
QScrollBar::handle:vertical {
background: #D8D8D8; /* 手柄颜色 */
border-radius: 3px; /* 圆角半径 */
}
/* 垂直滚动条手柄悬停效果 */
QScrollBar::handle:vertical:hover {
background: #D3D3D3; /* 悬停时颜色加深 */
}
/* 垂直滚动条向上/向下按钮 */
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
border: none;
background: none; /* 隐藏上下按钮 */
height: 0px; /* 设置高度为0隐藏按钮 */
}
/* 垂直滚动条页面区域(手柄上下空白区域) */
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
background: none; /* 页面区域透明 */
}
4.8 音乐收藏
-
更新小心心图标
-
更新Music的我喜欢属性,但ListItemBox并没有歌曲数据,所以只能发射信号,让其父元素CommonPage来处理
当CommonPage往界面更新Music信息时,也要根据Music的isLike属性更新对应的图标。因此ListItemBox需要根据 当点击我喜欢按钮之后,要切换ListItemBox中的小心心。因此ListItemBox中添加设置bool类型isLike成员变量,以及setIsLike函数,在CommonPage添加Music信息到界⾯时,要能够设置小心心图片
//listitembox.h
private slots:
void onLikeBtnCliked();
public:
void setIcon(bool isLike);
signals:
void setIsLike(bool isLike);
//listitembox.cpp
void ListItemBox::onLikeBtnCliked()
{
//设置为相反的状态
_isLike = !_isLike;
//发送信号
emit setIsLike(_isLike);
}
//设置图标
void ListItemBox::setIcon(bool isLike)
{
_isLike = isLike;
if(isLike){
ui->likeBtn->setIcon(QIcon(":/images/love2.png"));
}
else{
ui->likeBtn->setIcon(QIcon(":/images/love.png"));
}
}
//构造函数中添加
connect(ui->likeBtn, &QPushButton::clicked,this,&ListItemBox::onLikeBtnCliked);
CommonPage在往QListWidget中添加元素时,会创建⼀个个ListItemBox对象,每个对象将来都可能会发射setIsLike信号,因此在将ListItemBox添加完之后,CommonPage应该关联先该信号,将需要更新的的Music信息以及是否喜欢,同步给LXMusic
//commonpage.h
signals:
void updateLikeMusic(bool isLike, QString musicId);
//commonpage.cpp
void CommonPage::refreshPage(MusicList &musicList)
{
.....
//初始化ListItemBox对象
ListItemBox * listItemBox = new ListItemBox(ui->pageMusicList);
listItemBox->setMusicName(it->getMusicName());
listItemBox->setSingerName(it->getSingerName());
listItemBox->setAblumName(it->getAlbumName());
listItemBox->setIcon(it->getIsLike());
//绑定信号与槽
connect(listItemBox, &ListItemBox::setIsLike, this, [=](bool isLike){
emit updateLikeMusic(isLike, it->getMusicId());
});
.......
}
LXMusic收到CommonPage发射的updateLikePage信号后,通知其上的likePage、localPage、recentPage更新其界面的我喜欢歌曲信息
//lxmusic.h
private slots:
void onUpdateLikeMusic(bool isLike, QString musicId);
//lxmusic.cpp
void LXMusic::onUpdateLikeMusic(bool isLike, QString musicId)
{
qDebug() << "22";
// 1. 找到该⾸歌曲,并更新对应Music对象信息
auto it = _musicList.findMusicById(musicId);
if(it != _musicList.end())
{
it->setIsLike(isLike);
}
// 2. 通知三个⻚⾯更新⾃⼰的数据
ui->likeMusicPage->refreshPage(_musicList);
ui->localMusicPage->refreshPage(_musicList);
ui->recentMusicPage->refreshPage(_musicList);
}
void LXMusic::connectSignalAndSlots()
{
....
connect(ui->likeMusicPage,&CommonPage::updateLikeMusic, this,&LXMusic::onUpdateLikeMusic);
connect(ui->localMusicPage,&CommonPage::updateLikeMusic, this,&LXMusic::onUpdateLikeMusic);
connect(ui->recentMusicPage,&CommonPage::updateLikeMusic, this,&LXMusic::onUpdateLikeMusic);
....
}
五、音乐播放控制
歌曲已经添加到程序并完成解析,解析的信息也更新到界面了,所有前置⼯作基本完成,接下来重点处理音乐播放,歌曲播放需要用到Qt提供的QMediaPlayer类和QMediaPlaylist类
QMediaPlayer类
QMediaPlayer是Qt框架中用于支持各种音频和视频的播放,流媒体的播放,各种播放模式(单曲播放、列表播放、循环播放等),各种播放模式(播放、暂停、停止等),信号槽机制可以让用户在播放状态改变时进行所需控制,使用时需要包含 #include 头⽂件,并且需要在.pro项目文件中添加媒体库,即: QT += multimedia ,将 multimedia 模块导入到工程中,就可以使⽤该模块中提供的媒体播放控制的相关类,比如:QMediaPlayer、QMediaPlayList等
常用属性:
// 部分常⽤属性
const qint64 duration; // 保存媒体的总播放时间,单位为毫秒
const QMediaContent currentMedia; // 当前正在播放媒体的媒体内容
const QString error; // 最近⼀次错误信息
int volume; // 保存⾳量⼤⼩,范围在0~100之间
const bool audioAvailable; // ⾳频是否可⽤,audioAvailableChanged
信号⽤于监听其状态
QMediaPlaylist* playtlist; // 播放列表
常用函数:
qint64 duration() const; // 获取当前媒体的总时间
qint64 position() const; // 获取当前媒体的播放位置
int volume() const; // 获取播放⾳量⼤⼩
bool isMuted() const; // 检测是否静⾳
State state() const; // 获取当前媒体的播放状态
QMediaContent currentMedia() const; // 获取当前正在播放的媒体内容
QString errorString() const // 获取最近的⼀次错误
常用槽函数:
void pause(); // 播放媒体
void play(); // 暂停媒体
void stop(); // 停⽌播放媒体
void setMuted(bool muted); // 设置是否静⾳,true为静⾳,false为⾮静⾳
void setVolume(int volume); // 设置播放⾳量,volume取值范围在0~100之间
void setPosition(qint64 position); // 设置播放位置,position为要播放的时间,单位毫秒
// 设置播放列表,若播放多个媒体需要设置,默认为空
void setPlaylist(QMediaPlaylist *playlist);
// 设置媒体源
void setMedia(const QMediaContent &media, QIODevice *stream = nullptr);
常用信号:
void stateChanged(QMediaPlayer::State state); // 播放状态改变时发射该信号
void durationChanged(qint64 duration); // 播放时⻓改变时发射该状态
void positionChanged(qint64 position); // 播放位置改变时发射该状态
void volumeChanged(int volume); // ⾳量改变时发射该信号
void metaDataChanged(bool available); // 源数据改变发出
QMediaPlaylist类
在qt6中,QMediaPlaylist类被废弃了,无法在使用,此时需要自定义一个QMediaPlaylist类来对音乐进行相关的管理:
//QMediaPlaylist.h
#ifndef QMEDIAPLAYLIST_H
#define QMEDIAPLAYLIST_H
#include <QMediaPlayer>
#include <QUrl>
#include <QList>
#include <QRandomGenerator>
#include <QDebug>
// 播放模式枚举
enum class PlaybackMode {
Sequential, // 顺序播放
Loop, // 循环播放
Random, // 随机播放
CurrentItemLoop // 单曲循环
};
// 自定义播放列表类
class QMediaPlaylist : public QObject {
Q_OBJECT
public:
explicit QMediaPlaylist(QObject *parent = nullptr);
// 添加媒体文件
void addMedia(const QUrl &url);
// 设置播放模式
void setPlaybackMode(PlaybackMode mode);
// 获取下一个媒体文件
QUrl next();
// 获取上一个媒体文件
QUrl previous();
// 获取当前媒体文件
QUrl currentMedia() const;
void setCurrentIndex(int index);
int getCurrentIndex() const;
PlaybackMode playbackMode()const;
void clear();
void sendIndexAndMediaChanged(int index, QUrl url);
signals:
// 播放模式改变信号
void playbackModeChanged(PlaybackMode mode);
// 当前索引改变信号
void currentIndexChanged(int position);
// 当前媒体改变信号
void currentMediaChanged(const QUrl &content);
private:
QList<QUrl> mediaList; // 媒体文件列表
int currentIndex; // 当前媒体索引
PlaybackMode mode; // 当前播放模式
};
#endif // QMEDIAPLAYLIST_H
//QMediaPlaylist.cpp
#include "qmediaplaylist.h"
QMediaPlaylist::QMediaPlaylist(QObject *parent)
:QObject(parent)
, currentIndex(-1)
, mode(PlaybackMode::Loop)
{}
// 添加媒体文件
void QMediaPlaylist::addMedia(const QUrl &url) {
if(currentIndex < 0){
currentIndex = 0;
}
mediaList.append(url);
}
// 设置播放模式
void QMediaPlaylist::setPlaybackMode(PlaybackMode mode) {
if (this->mode != mode) {
this->mode = mode;
emit playbackModeChanged(mode); // 发射播放模式改变信号
}
}
// 获取下一个媒体文件
QUrl QMediaPlaylist::next() {
if (mediaList.isEmpty()) return QUrl();
int previousIndex = currentIndex;
int index = currentIndex;
switch (mode) {
case PlaybackMode::Sequential:
index = (currentIndex + 1) % mediaList.size();
if (currentIndex == 0 && previousIndex != -1) {
// 顺序播放模式下,播完后停止
return QUrl();
}
break;
case PlaybackMode::Loop:
index = (currentIndex + 1) % mediaList.size();
break;
case PlaybackMode::Random:
index = QRandomGenerator::global()->bounded(mediaList.size());
break;
case PlaybackMode::CurrentItemLoop:
// 保持 currentIndex 不变
index = currentIndex;
break;
}
if (index != previousIndex) {
setCurrentIndex(index);
}
return mediaList.at(currentIndex);
}
// 获取上一个媒体文件
QUrl QMediaPlaylist::previous() {
if (mediaList.isEmpty()) return QUrl();
int previousIndex = currentIndex;
int index = currentIndex;
switch (mode) {
case PlaybackMode::Sequential:
case PlaybackMode::Loop:
index = (currentIndex - 1 + mediaList.size()) % mediaList.size();
break;
case PlaybackMode::Random:
index = QRandomGenerator::global()->bounded(mediaList.size());
break;
case PlaybackMode::CurrentItemLoop:
// 保持 currentIndex 不变
index = currentIndex;
break;
}
if (index != previousIndex) {
setCurrentIndex(index);
}
return mediaList.at(currentIndex);
}
// 获取当前媒体文件
QUrl QMediaPlaylist::currentMedia() const {
if (currentIndex >= 0 && currentIndex < mediaList.size()) {
return mediaList.at(currentIndex);
}
return QUrl();
}
//设置当前播放索引
void QMediaPlaylist::setCurrentIndex(int index) {
if (index >= mediaList.size()) {
currentIndex = mediaList.size() - 1;
} else if (index < 0) {
currentIndex = 0;
} else {
currentIndex = index;
}
//qDebug() << currentIndex;
emit currentIndexChanged(currentIndex); // 发射当前索引改变信号
//qDebug() << "33333";
emit currentMediaChanged(mediaList.at(currentIndex)); // 发射当前媒体改变信号
}
//获取当前索引
int QMediaPlaylist::getCurrentIndex()const
{
//qDebug() << currentIndex;
return currentIndex;
}
PlaybackMode QMediaPlaylist::playbackMode() const
{
return mode;
}
void QMediaPlaylist::clear()
{
mediaList.clear();
currentIndex = -1;
}
void QMediaPlaylist::sendIndexAndMediaChanged(int index, QUrl url)
{
emit currentIndexChanged(index);
emit currentMediaChanged(url);
}
5.1 音乐播放
播放媒体和播放列表初始化
在播放之前,需要先将QMediaPlayer和QMediaPlaylist初始化好。QQMusic类中需要添加QMediaPlayer和QMediaPlaylist的对象指针,在界⾯初始化时将这两个类的对象创建好
//lxmusic.h
public:
void initPlayer();
private:
QMediaPlayer* _player;
QMediaPlaylist* _playList;
//lxmusic.cpp
void LXMusic::initPlayer()
{
//创建播放器
_player = new QMediaPlayer(this);
//创建播放列表
_playList = new QMediaPlaylist(this);
//设置播放模式:默认为循环播放
_playList->setPlaybackMode(PlaybackMode::Loop);
//默认音量设置为20
QAudioOutput *audioOutput = new QAudioOutput(this);
_player->setAudioOutput(audioOutput);
_player->setAudioOutput(audioOutput);
audioOutput->setVolume(0.5);// 设置音量(范围是 0.0 到 1.0)20%
}
播放列表设置
播放之前,先要将歌曲加入用于播放的媒体列表,由于每个CommonPage页面的歌曲不同,因此CommonPage中新增将其页面歌曲添加到播放列表的方法
//commonpage.h
void addMusicToPlayList(MusicList& musicList, QMediaPlaylist* playList);
//commonpage.cpp
void CommonPage::addMusicToPlayList(MusicList &musicList, QMediaPlaylist *playList)
{
// 根据⾳乐列表中⾳乐所属的⻚⾯,将⾳乐添加到playList中
for(auto music : musicList){
switch(_pageType){
case PageType::LOCALPAGE:{
playList->addMedia(music.getMusicUrl());
break;
}
case PageType::LIKEPAGE:{
if(music.getIsLike()){
playList->addMedia(music.getMusicUrl());
}
break;
}
case PageType::RECENTPAGE:{
if(music.getIsHistory()){
playList->addMedia(music.getMusicUrl());
}
break;
}
default:
qDebug() << "该页面暂未实现";
break;
}
}
}
播放和暂停
当点击播放和暂停按钮时,播放状态应该在播放和暂停之间切换。播放器的状态如下有:PlayingState()、PausedState()、StoppedState() ,刚开始为停止状态,下面使用isPlaying函数进行判断音乐是否正在播放:
//lxmusic.h
private slots:
void onPlayClicked();
void onPlayStausTochangeIcon();
void finishedAutoplay(QMediaPlayer::PlaybackState state);
private:
bool _isManualPlay = false; //标志位,避免手动操作时调用finishedAutoPlay函数
//lxmusic.cpp
//播放暂停音乐,槽函数
void LXMusic::onPlayClicked()
{
//正在播放,则暂停,否则播放
if(_player->isPlaying()){
_player->pause();
}
else{
_player->play();
}
}
//播放状态改变,随之改变播放按钮的图标
void LXMusic::onPlayStausTochangeIcon()
{
if(_player->playbackState() == QMediaPlayer::PlayingState){
// 播放状态
ui->playButton->setIcon(QIcon(":/images/play2.png"));
}
else{
// 暂停状态
ui->playButton->setIcon(QIcon(":/images/play1.png"));
}
}
//监听播放结束信号,自动播放下一个
void LXMusic::finishedAutoplay(QMediaPlayer::PlaybackState state)
{
if (_isManualPlay) {
return; // 如果是手动播放,忽略信号处理
}
if (state == QMediaPlayer::StoppedState ) {
QUrl nextMedia = _playList->next();
if (!nextMedia.isEmpty()) {
_player->setSource(nextMedia);
_player->play();
} else {
qDebug() << "Playback finished.";
}
}
}
//绑定槽函数
connect(ui->playButton,&QPushButton::clicked, this, &LXMusic::onPlayClicked);
connect(_player,&QMediaPlayer::playbackStateChanged,this,&LXMusic::onPlayStausTochangeIcon);
// 监听播放结束信号,自动播放下一个
connect(_player, &QMediaPlayer::playbackStateChanged, this, &LXMusic::finishedAutoplay);
//初始化playlist,并初始化player
void LXMusic::on_addLocalButton_clicked()
{
........
//将音乐添加到_playlist中,并添加索引为零的音乐到播放器中
ui->localMusicPage->addMusicToPlayList(_musicList,_playList);
_player->setSource(_playList->currentMedia());
}
说明:
- playbackState()获取当前播放状态,当播放状态发生改变的时候,player对象会发出QMediaPlayer::playbackStateChanged的信号,使用槽函数绑定该信号就能知道什么时候改变,然后更改播放按钮的图标
完成上面的步骤就可以实现,第一首音乐的播放和根据设置的播放模式,进行自动播放。
5.2 上一曲和下一曲的切换实现
在定义的播放列表中,提供了previous()和next()函数,通过设置前⼀个或者下⼀个歌曲为当前播放源,player就会播放对应的歌曲
//lxmusic.h
private slots:
void onPlayUpClicked();
void onPlayDownClicked();
//lxmusic.cpp
//切换到上一首歌
void LXMusic::onPlayUpClicked()
{
_isManualPlay = true;//避免调用finishedAutoplay函数
_player->setSource(_playList->previous());
_player->play();
_isManualPlay = false;
}
//切换到下一首歌
void LXMusic::onPlayDownClicked()
{
_isManualPlay = true;//避免调用finishedAutoplay函数
_player->setSource(_playList->next());
_player->play();
_isManualPlay = false;
}
//槽函数绑定
connect(ui->playUpButton,&QPushButton::clicked, this, &LXMusic::onPlayUpClicked);
connect(ui->playDownButton,&QPushButton::clicked, this, &LXMusic::onPlayDownClicked);
5.3 播放模式的设置
在在定义的播放列表中,实现了四个自定义模式,在构造函数中默认了播放模式是循环播放:
enum class PlaybackMode {
Sequential, // 顺序播放
Loop, // 循环播放
Random, // 随机播放
CurrentItemLoop // 单曲循环
};
播放模式切换时会触发 playbackModeChanged 信号,在该信号对应槽函数中,完成图片切换,并在鼠标放在该按钮上时给出提示信息
//lxmusic.h
private slots:
void onPlaybackModeClicked();
void onPlaybackModeChange(PlaybackMode playbackMode);
//lxmusic.cpp
//当点击播放模式按钮的时候,切换播放模式
void LXMusic::onPlaybackModeClicked()
{
if(_playList->playbackMode() == PlaybackMode::Loop){
_playList->setPlaybackMode(PlaybackMode::Random);
}
else if(_playList->playbackMode() == PlaybackMode::Random){
_playList->setPlaybackMode(PlaybackMode::Sequential);
}
else if(_playList->playbackMode() == PlaybackMode::Sequential)
{
_playList->setPlaybackMode(PlaybackMode::CurrentItemLoop);
}else if(_playList->playbackMode() == PlaybackMode::CurrentItemLoop){
_playList->setPlaybackMode(PlaybackMode::Loop);
}
else{
qDebug() << "功能暂未支持";
}
}
//播放模式切换,会自动触发信号,该槽函数与播放模式信号进行绑定
void LXMusic::onPlaybackModeChange(PlaybackMode playbackMode)
{
if(playbackMode == PlaybackMode::Loop){
ui->playModeButton->setIcon(QIcon(":/images/loopPlay.png"));
ui->playModeButton->setToolTip("列表循环");
}
else if(playbackMode == PlaybackMode::Random){
ui->playModeButton->setIcon(QIcon(":/images/randomPlay.png"));
ui->playModeButton->setToolTip("随机播放");
}
else if(playbackMode == PlaybackMode::CurrentItemLoop){
ui->playModeButton->setIcon(QIcon(":/images/currentItemLoop.png"));
ui->playModeButton->setToolTip("单曲循环");
}else if(playbackMode == PlaybackMode::Sequential){
ui->playModeButton->setIcon(QIcon(":/images/sequentialPlay.png"));
ui->playModeButton->setToolTip("顺序播放");
}
else{
qDebug() << "功能暂未支持";
}
}
//绑定信号与槽
//播放模式更换,更改图标
connect(ui->playModeButton,&QPushButton::clicked, this, &LXMusic::onPlaybackModeClicked);
connect(_playList,&QMediaPlaylist::playbackModeChanged, this, &LXMusic::onPlaybackModeChange);
5.4 播放所有按钮的逻辑实现
播放所有按钮属于CommonPage中的按钮,其对应的槽函数添加在CommonPage类中,但是CommonPage不具有音乐播放的功能,因此当点击播放所有按钮后之后,播放所有的槽函数应该发射出信号,让QQMusic类完成播放。由于likePage、localPage、recentPage三个CommonPage页面都有playAllBtn,因此该信号需要带上PageType参数,需要让QQMusic在处理该信号时,知道播放哪个页面的歌曲
//commonpage.h
signals:
void playAll(PageType type);
//commonpage.cpp
//点击播放所有按钮的时候,发送信号给lxmusic
//在构造函数中调用
connect(ui->playAllBtn, &QPushButton::clicked,this, [=](){
emit CommonPage::playAll(_pageType);});
//lxmusic.h
private slots:
void onPlayAll(PageType type);
public:
void playAllOfPage(CommonPage* page, int index);
//lxmusic.cpp
//实现播放对应页面中的歌曲
void LXMusic::onPlayAll(PageType type)
{
CommonPage* page = nullptr;
switch(type){
case PageType::LIKEPAGE:
{
page = ui->likeMusicPage;
break;
}
case PageType::LOCALPAGE:
{
page = ui->localMusicPage;
break;
}
case PageType::RECENTPAGE:
{
page = ui->recentMusicPage;
break;
}
default:
qDebug() << "功能暂未支持";
break;
}
playAllOfPage(page,0);
}
//更改播放列表中的歌曲
void LXMusic::playAllOfPage(CommonPage *page, int index)
{
if(_currentPlayPage != page){
_currentPlayPage = page;
// QUrl url = _playList->currentMedia();
//清空播放列表中的歌
_playList->clear();
//将对应页面中存在的歌,加入到播放列表中
page->addMusicToPlayList(_musicList,_playList);
}
//设置当前播放列表的索引
_playList->setCurrentIndex(index);
//播放音乐
_player->setSource(_playList->currentMedia());
_player->play();
}
//关联CommonPage发出的播放所有信号,实现播放对应页面的歌曲
connect(ui->likeMusicPage,&CommonPage::playAll, this, &LXMusic::onPlayAll);
connect(ui->localMusicPage,&CommonPage::playAll, this, &LXMusic::onPlayAll);
connect(ui->recentMusicPage,&CommonPage::playAll, this, &LXMusic::onPlayAll);
5.5 双击页面中的音乐进行播放
当QListWidget中的项被双击时,会触发 doubleClicked 信号
// QListWidget中的项被双击时触发
// QModelIndex类中的row()函数会返回被点击的QListWidgetItem在QListWidget中索引
void doubleClicked(const QModelIndex &index);
该信号在QListWidget的基类中定义,有⼀个index参数,表示被双击的QListWidgetItem在QListWidget中的索引,该索引刚好与QMediaPlaylist中歌曲的所以⼀致,被双击时直接播放该首歌曲即可
//commonpage.h
signals:
void playMusicbyIndex(CommonPage* page, int index);
//commonpage.cpp
//双击音乐后,发送信号给lxmusic,让其实现播放
connect(ui->pageMusicList,&QListWidget::doubleClicked, this, [=](const QModelIndex& index){ emit playMusicbyIndex(this, index.row());});
//lxmusic.h
private:
bool _isManualPlay = false; //标志位,避免双击时,调用finishedAutoPlay函数
private slots:
void playMusicByIndex(CommonPage* page, int index);
//lxmuic.cpp
//播放对应页面中,index索引下的歌曲
void LXMusic::playMusicByIndex(CommonPage *page, int index)
{
playAllOfPage(page, index);
}
//更改下面函数
void LXMusic::playAllOfPage(CommonPage *page, int index)
{
if(_currentPlayPage != page){
_currentPlayPage = page;
// QUrl url = _playList->currentMedia();
//清空播放列表中的歌
_playList->clear();
//将对应页面中存在的歌,加入到播放列表中
page->addMusicToPlayList(_musicList,_playList);
}
//设置当前播放列表的索引
_playList->setCurrentIndex(index);
_isManualPlay = true;
//播放音乐
_player->setSource(_playList->currentMedia());
_player->play();
_isManualPlay = false;
}
//关联CommonPage发出的双击音乐播放信号,实现播放对应页面的歌曲
connect(ui->likeMusicPage,&CommonPage::playMusicbyIndex, this, &LXMusic::playMusicByIndex);
connect(ui->localMusicPage,&CommonPage::playMusicbyIndex, this, &LXMusic::playMusicByIndex);
connect(ui->recentMusicPage,&CommonPage::playMusicbyIndex, this, &LXMusic::playMusicByIndex);
5.6 最近播放同步
当播放歌曲改变时,即播放的媒体源发生了变化,QMediaPlayer会触发metaDataAvailableChanged 信号,QMediaPlaylist也会触发 currentIndexChanged 信号,该信号会带index参数,表示现在是媒体播放列表中的index歌曲被播放,通过index可以获取到recentPage页面中具体播放的歌曲,将该歌曲对应Music对象的isHistoty属性修改为true,然后更新下rencentPage的歌曲列表,播放过的歌曲就添加到历史播放页面中了
问题:获取likePage、localPage、recentPage哪个CommonPage⻚⾯中的歌曲呢?
答案:LXMusic类中维护CommonPage*变量currentPage,记录当前正在播放的CommonPage页面,初始时设置为localPage,当播放的页面发生改变时,修改currentPage为当前正在播放页面,其中点击播放所有按钮以及双击QListWidget中项的时候都回引起currentPage的改变
//lxmusic.h
private:
CommonPage* _currentPage;
//lxmusic.cpp
LXMusic::LXMusic(QWidget *parent)
: QWidget(parent)
, ui(new Ui::LXMusic)
,_currentPage(nullptr)
{
ui->setupUi(this);
//初始化界面
initUi();
//初始化播放器
initPlayer();
//绑定信号和槽函数
connectSignalAndSlots();
}
void LXMusic::playAllOfPage(CommonPage *page, int index)
{
_currentPage = page;
//清空播放列表中的歌
_playList->clear();
.............
}
准备工作完成之后,同步最近播放歌曲的逻辑实现如下:
播放索引改变的时候会发送currentIndexChanged信号,在LXMusic中接受该信号,将当前索引对应的歌曲中的isHistory属性设置为true,然后再刷新recentPage界面将_musicList中已经播放的歌曲添加到recentPage页面中即可。
//lxmusic.h
private slots:
void onCurrentIndexChanged(int index);
//lxmusic.cpp
void LXMusic::onCurrentIndexChanged(int index)
{
QString musicId = _currentPage->getMusicIdByIndex(index);
if(musicId.isEmpty())return;
auto it = _musicList.findMusicById(musicId);
if(it != _musicList.end()){
// 将该⾳乐设置为历史播放记录
it->setIsHistory(true);
}
//刷新最近播放界面
ui->recentMusicPage->refreshPage(_musicList);
}
//绑定槽函数
// 播放列表中的索引发⽣改变,此时将播放⾳乐收藏到历史记录中
connect(_playList, &QMediaPlaylist::currentIndexChanged, this,&LXMusic::onCurrentIndexChanged);
//commonpage.h
QString getMusicIdByIndex(int index);
//commonpage.cpp
QString CommonPage::getMusicIdByIndex(int index)
{
if(index >= _musicOfPage.size()){
return "";
}
return _musicOfPage[index];
}
但这样发现刚开始点击播放音乐按钮的时候,音乐并没有添加到最近播放页面,需进行如下修改:
//lxmusic.h
void setMusicHistoryByUrl(QUrl url);
//lxmusic.cpp
//播放暂停音乐,槽函数
void LXMusic::onPlayClicked()
{
if(_player->source().isEmpty()){
QUrl url = _playList->currentMedia();
qDebug() << url;
if(!url.isEmpty()){
setMusicHistoryByUrl(url);
//刷新最近播放界面
ui->recentMusicPage->refreshPage(_musicList);
//设置播放源
_player->setSource(_playList->currentMedia());
}
}
//正在播放,则暂停,否则播放
if(_player->isPlaying()){
_player->pause();
}
else{
_player->play();
}
}
void LXMusic::on_addLocalButton_clicked()
{
//这个函数中,将_player->setSource(_playList->currentMedia());去掉
//不要这里初始化
}
5.7 音量设置
当点击静音按钮时,音量应该在静音和非静音之间进行切换,并且按钮上图标需要同步切换。鼠标在滑竿上点击或拖动滑竿时,应该跟进滑竿的高低比率,设置音量大小,同时修改界面音量比率
静⾳和⾮静⾳切换
给静音按钮参加槽函数onSilenceBtnClicked,并在构造函数中connect按钮的clicked信号,当按钮点击时候,调用setMuted(bool nuted)函数,完成静音和非静音的设置,由于VolumeTool不具备媒体播放控制,因此当静音状态发生改变时,发射设置静音信号,让LXMusic来处理
//volumetool.h
signals:
void setSilence(bool isMuted);
private:
bool _isMuted;
int _volumeRatio;
private slots:
void onSilenceBtnClicked();
//volumetool.cpp
//构造函数中初始化成员变量
VolumeTool::VolumeTool(QWidget *parent)
: QWidget(parent)
, ui(new Ui::VolumeTool)
,_isMuted(false)
,_volumeRatio(20){.............}
void VolumeTool::onSilenceBtnClicked()
{
_isMuted = !_isMuted;
if(_isMuted){
ui->silenceBtn->setIcon(QIcon(":/images/volume2.png"));
}else{
ui->silenceBtn->setIcon(QIcon(":/images/volume.png"));
}
//发送信号
emit setSilence(_isMuted);
}
//绑定槽函数
connect(ui->silenceBtn, &QPushButton::clicked, this, &VolumeTool::onSilenceBtnClicked);
//lxmusic.h
private slots:
void setMusicSilence(bool isMuted);
//lxmusic.cpp
//设置静音或不静音
void LXMusic::setMusicSilence(bool isMuted)
{
QAudioOutput* audioOutput = _player->audioOutput();
audioOutput->setMuted(isMuted);
}
//绑定槽函数
//接受volumetool发来的静音信号
connect(_volumetool, &VolumeTool::setSilence, this, &LXMusic::setMusicSilence);
⿏标按下、滚动以及释放事件处理
当鼠标在滑竿上按下时,需要设置sliderBtn和outLine的位置,当鼠标在滑竿上移动或者鼠标抬起时,需要设置SliderBtnoutLine结束的位置,即改变VolumeTool中滑竿的显示。具体修改播放媒体音量大小操作应该由于QQMusic负责处理,因此当鼠标移动或释放时,需要发射信号让QQMusic知道需要修改播放媒体的音量大小了
//volumetool.h
public:
void setVolume();
protected:
virtual bool eventFilter(QObject* object, QEvent* event)override;
signals:
void setMusicVolume(int volumeRatio);
//volumetool.cpp
//重写函数,过滤鼠标按下、移动、释放的事件
bool VolumeTool::eventFilter(QObject *object, QEvent *event)
{
if(object == ui->volumeBox){
if(event->type() == QEvent::MouseButtonPress){
setVolume();
}
else if(event->type() == QEvent::MouseMove){
emit setMusicVolume(_volumeRatio);
}
else if(event->type() == QEvent::MouseButtonRelease){
setVolume();
emit setMusicVolume(_volumeRatio);
}
return true;
}
return QWidget::eventFilter(object, event);
}
//计算音量大小
void VolumeTool::setVolume()
{
int height = ui->volumeBox->mapFromGlobal(QCursor().pos()).y();
height = height < 25 ? 25 : height;
height = height > 205 ? 205 : height;
//计算圆圈的位置
ui->sliderBtn->move(ui->sliderBtn->x(), height - ui->sliderBtn->height() / 2);
//计算outslider位置
ui->outSlider->setGeometry(ui->outSlider->x(), height, ui->outSlider->width(), 205 - height);
//计算音量比率
_volumeRatio = (int)(ui->outSlider->height() / (float)180 * 100);
//设置给volumeRatio显示
ui->volumeRatio->setText(QString::number(_volumeRatio) + "%");
}
//在构造函数中下载过滤器
ui->volumeBox->installEventFilter(this);
LXMusic类拦截VolumeTool发射的setMusicVolume信号,将音量大小设置为指定值
//lxmusic.h
private slots:
void setMusicVolume(int volumeRatio);
//lxmusic.cpp
//接受volumetool的信号,设置音量
void LXMusic::setMusicVolume(int volumeRatio)
{
QAudioOutput* audioOutput = _player->audioOutput();
audioOutput->setVolume(volumeRatio / 100.0);
}
//绑定信号槽
//接受volumetool发来的信号,设置音量大小
connect(_volumetool,&VolumeTool::setMusicVolume, this, &LXMusic::setMusicVolume);
5.8 当前播放时间和总时间更新
- 当播放源的持续时长发生改变时,QMediaPlayer会触发durationChanged信号,该信号中提供了将要播放媒体的总时长
- 媒体在持续播放过程中,QMediaPlayer会发射positionChanged,该信号带有⼀个qint64类型参数,表示媒体当前持续播放的时间
- 因此在LXMusic类中给这两个信号关联槽函数,在槽函数中将信号传递来的参数更新到对应的label中即可
//lxmusic.h
void onDurationChanged(qint64 duration);
void onPositionChanged(qint64 position);
//lxmusic.cpp
//更新总时间
void LXMusic::onDurationChanged(qint64 duration)
{
ui->totalTimeLabel->setText((QString("%1:%2").arg(duration/1000/60, 2, 10,QChar('0')).arg(duration/1000%60,2,10,QChar('0'))));
}
//更新当前播放时间
void LXMusic::onPositionChanged(qint64 position)
{
ui->currentTimeLabel->setText((QString("%1:%2").arg(position/1000/60, 2, 10,QChar('0')).arg(position/1000%60,2,10,QChar('0'))));
}
//绑定信号槽
//监听_player总时间的变化
connect(_player, &QMediaPlayer::durationChanged, this, &LXMusic::onDurationChanged);
//监听_player当前播放位置变化
connect(_player,&QMediaPlayer::positionChanged, this, &LXMusic::onPositionChanged);
5.9 进度条的更新
seek功能介绍
播放器的seek功能指,通过时间或位置快速定位到视频或音频流的特定位置,允许用户在播放过程中随时跳转到特定时间点,从而快速找到感兴趣的内容或重新开始播放
在界面上的体现是,当在MusicSlider上点击或者拖拽的时候,会跳转到歌曲的指定位置进行播放,并且歌曲的当前持续播放时间要同步修改
进度条界面显示
进度条功能进度界面展示与音量调节位置类似,拦截鼠标按下、鼠标移动、以及鼠标释放消息即可。在内部捕获到鼠标的位置的横坐标x,将x作为outLine的宽度。即在鼠标按下、移动、释放的时候,修改outLine的宽度即可
//musicSlider.h
protected:
virtual void mousePressEvent(QMouseEvent* event)override;
virtual void mouseMoveEvent(QMouseEvent* event)override;
virtual void mouseReleaseEvent(QMouseEvent* event)override;
//musicSlider.cpp
public:
void moveSlider();
private:
qint64 _currentPosition;//滑块当前位置
qint64 _maxWidth;//滑块的最大宽度
MusicSlider::MusicSlider(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MusicSlider)
{
ui->setupUi(this);
//初始化进度条
_currentPosition = 0;
_maxWidth = width();
moveSlide();
}
//移动进度条
void MusicSlider::moveSlide()
{
if(_currentPosition < 0){
_currentPosition = 0;
}
if(_currentPosition > _maxWidth){
_currentPosition = _maxWidth;
}
// 根据当前进度设置外部滑动条的位置
ui->outLine->setMaximumWidth(_currentPosition);
ui->outLine->setGeometry(0,8, _currentPosition, 4);
}
void MusicSlider::mousePressEvent(QMouseEvent *event)
{
_currentPosition = event->pos().x();
moveSlide();
}
void MusicSlider::mouseMoveEvent(QMouseEvent *event)
{
// QRect rect = QRect(0,0,width(), height());
// QPoint pos = event->pos();
// if(!rect.contains(pos)){
// return;
// }
if(event->buttons() == Qt::LeftButton){
_currentPosition = event->pos().x();
moveSlide();
}
}
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
_currentPosition = event->pos().x();
moveSlide();
//发送信号,给lxmusic让其更新当前播放时间
emit setMusicSliderPostion((float)_currentPosition / (float)_maxWidth);
}
进度条同步持续播放时间
当鼠标释放之后,计算出进度条当前位置currentPos和总宽度的maxWidth比率,然后发射信号告诉LxMusic,让player按照该比率更新持续播放时间
// musicslider.h 新增
signals:
void setMusicSliderPosition(float);
//musicslider.cpp
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
_currentPosition = event->pos().x();
moveSlide();
//发送信号,给lxmusic让其更新当前播放时间
emit setMusicSliderPostion((float)_currentPosition / (float)_maxWidth);
}
//lxmusic.h
void onMusicSliderChanged(float value);
//lxmusic.cpp
//根据进度条位置,更新当前播放时间
void LXMusic::onMusicSliderChanged(float value)
{
qint64 duration = (qint64)(_player->duration() * value);
ui->currentTimeLabel->setText((QString("%1:%2").arg(duration/1000/60, 2, 10,QChar('0')).arg(duration/1000%60,2,10,QChar('0'))));
//设置当前播放时间
_player->setPosition(duration);
}
//绑定槽函数
//监听进度条的变化,改变当前播放时间
connect(ui->processBar, &MusicSlider::setMusicSliderPostion, this, &LXMusic::onMusicSliderChanged);
持续时间同步进度条
//musicslider.h
public:
void syncSlide(float value);
//musicslider.cpp
//进度条同步当前播放时间
void MusicSlider::syncSlide(float value)
{
_currentPosition = (qint64)_maxWidth * value;
moveSlide();
}
//lxmusic.h
private slots:
void onMusicSliderChanged(float value);
//lxmusic.cpp
//更新当前播放时间并设置进度条的位置
void LXMusic::onPositionChanged(qint64 position)
{
ui->currentTimeLabel->setText((QString("%1:%2").arg(position/1000/60, 2, 10,QChar('0')).arg(position/1000%60,2,10,QChar('0'))));
//设置进度条的位置
ui->processBar->syncSlide((float)position / (float)_player->duration());
}
//绑定信号槽
//监听_player当前播放位置变化
connect(_player,&QMediaPlayer::positionChanged, this, &LXMusic::onPositionChanged);
5.10 歌曲名称、歌手和封面图片同步
在进行歌曲切换时候,歌曲名称、歌手以及歌曲的封面图,也需要更新到界面。歌曲名称、歌手可以再Music对象中进行获取,歌曲的封面图可以通过player到歌曲的元数据中获取,获取时需要使用"ThumbnailImage"作为参数,注意有些歌曲可能没有封面图,如果没有设置⼀张默认的封面图。 由于歌曲切换时,player需要将新播放歌曲作为播放源,并解析歌曲文件,如果歌曲文件是有效的才能播放;因此LXMusic类可以给QMediaPlayer发射的 metaDataChanged() 信号关联槽函数,当歌曲更换时,完成信息的更新
//lxmusic.h
private slots:
void onMetaDataChanged();
//lxmusic.cpp
//音乐改变时,更新图片歌名歌手
void LXMusic::onMetaDataChanged()
{
QString musicName = _player->metaData().value(QMediaMetaData::Title).toString();
QString singerName = _player->metaData().value(QMediaMetaData::Author).toString();
if(musicName.isEmpty() || singerName.isEmpty())
{
auto it = _musicList.findMusicById(_currentPlayPage->getMusicIdByIndex(_playList->getCurrentIndex()));
if(it != _musicList.end())
{
musicName = it->getMusicName();
singerName = it->getSingerName();
}
}
//设置歌手名称、歌名
ui->musicNameLabel->setText(musicName);
ui->musicSingerLabel->setText(singerName);
//获取图片
QVariant coverImage = _player->metaData().value(QMediaMetaData::ThumbnailImage);
if(coverImage.isValid()){
// 获取封⾯图⽚成功
QImage image = coverImage.value<QImage>();
// 设置封⾯图⽚
ui->musicCoverLabel->setPixmap(QPixmap::fromImage(image));
// 缩放填充到整个Label
ui->musicCoverLabel->setScaledContents(true);
_currentPlayPage->setImageLabel(QPixmap::fromImage(image));
}
else{
qDebug() << " 11111";
ui->musicCoverLabel->setPixmap(QPixmap(":/images/localmusic.jpg"));
// 缩放填充到整个Label
ui->musicCoverLabel->setScaledContents(true);
_currentPlayPage->setImageLabel(QPixmap(":/images/localmusic.jpg"));
}
}
//绑定信号槽
//监听歌曲的改变,显示歌名歌手和图片
connect(_player,&QMediaPlayer::metaDataChanged, this, &LXMusic::onMetaDataChanged);
//commonpage.h
public:
void setImageLabel(const QPixmap& pixmap);
//commonpage.cpp
void CommonPage::setImageLabel(const QPixmap &pixmap)
{
ui->musicImageLabel->setPixmap(pixmap);
ui->musicImageLabel->setScaledContents(true);
}
5.11 歌词同步
播放歌曲时,当点击"词"按钮后窗口会慢慢弹出,当点击隐藏按钮后,窗口会慢慢隐藏,且没有标题栏。内部显示当前播放歌曲的歌词,以及歌曲名称和作者。当点击下拉按钮时,窗口会隐藏起来
lrc歌词界⾯分析
该页面最上面由两个label和一个按钮组成,下面由七个label组成。
lrc歌词界⾯布局
在qt create中新创建⼀个qt 设计师界面,命名为LrcPage,geometry的宽高修改为:1028*688
- 拖⼀个Widget到LrcPage中,objectName修改为bgStyle,选中LrcPage,然后点击垂直布局,并将LrcPage的margin和spacing修改为0;
- 拖两个Widget到bgStyle中,objectName从上往下分别修改为lrcTop和lrcContent,lrcTop的minimumSize和maximumSize的高修改为50;然后选中bgStyle点击垂直布局,并将bgStyle的margin和spacing修改为0;
- 拖⼀个按钮到lrcTop中,objectName修改为hideBtn,minimumSize和maximumSize的宽和高修改为:30*50;
- 拖⼀个Widget到lrcTop中,objectName修改为titleBox 然后选中lrcTop,点击⽔平布局,并将lrcTop的margin和spacing修改为0;
- 拖两个QLabel到titleBox中,objectName从上往下修改为musicSinger和musicName,然后选中titleBox,点击垂直布局,并将titleBox的margin和spacing修改为0
- 拖六个QLabel到lrcContent中,从上往下将objectName依次修改为:line1、line2、line3、lineCenter、line4、line5、line6,将line1~line6的minimumSize的高度修改为50,font修改为15,将lineCenter的minimumSize高度修改为80,font的大小修改为25;
- 拖两个垂直弹簧,一个放在line1上,⼀个放在line6下,将所有的QLabel撑到中间 ,选中lrcContent,然后点击垂直布局,将lrcContent的margin和spacing修改为0
歌词界面优化
#bgStyle{
border-image: url(:/images/bg.png);
}
*{
color : #FFFFFF;
}
#lineCenter
{
color:#1ECE9A;
}
#hideBtn
{
border:none;
}
LrcPage显示并添加动画效果
在LrcPage的构造函数中,将窗口的标题栏去除掉;并给hideBtn关联clicked信号,当按钮点击时将窗口隐藏
//lxmusic.h
private slots:
void onLrcWordClicked();
private:
LrcPage* _lrcWord;
//lxmusic.cpp
void LXMusic::onLrcWordClicked()
{
_lrcWord->show();
_animal->start();
}
void LXMusic::initUi(){
......................
//初始化lrcPage页面
_lrcWord = new LrcPage(this);
_lrcWord->hide();
// lrcPage添加动画效果
_animal = new QPropertyAnimation(_lrcWord, "geometry", this);
_animal->setDuration(250);
_animal->setStartValue(QRect(10, 10+_lrcWord->height(),_lrcWord->width(), _lrcWord->height()));
_animal->setEndValue(QRect(10, 10, _lrcWord->width(), _lrcWord->height()));
}
//绑定信号槽
//点击歌词按钮时,弹出歌词窗口
connect(ui->lyricButton, &QPushButton::clicked, this, &LXMusic::onLrcWordClicked);
//lrcpage.h
private:
QPropertyAnimation* animation;
//lrcpage.cpp
LrcPage::LrcPage(QWidget *parent)
: QWidget(parent)
, ui(new Ui::LrcPage)
{
ui->setupUi(this);
//设置窗口无边框
this->setWindowFlag(Qt::FramelessWindowHint);
//设置按钮图标
ui->hideBtn->setIcon(QIcon(":/images/xiala.png"));
animation = new QPropertyAnimation(this, "geometry", this);
animation->setDuration(250);
animation->setStartValue(QRect(10, 10, width(), height()));
animation->setEndValue(QRect(10, 10 + height(), width(), height()));
//点击按钮显示动画
connect(ui->hideBtn, &QPushButton::clicked, [=](){
animation->start();
//this->hide();
});
//动画结束,隐藏窗口
connect(animation, &QPropertyAnimation::finished, [=](){
this->hide(); // 隐藏窗口
});
}
lrc歌词解析和同步
lrc是英文lyric(歌词)的缩写,被用作歌词文件的扩展名。该文件将歌词和歌词出现的时间编辑到⼀起,当歌曲播放的时候,按照歌词文件中的时间依次将歌词显示出来。
-
标准格式:[分钟:秒.毫秒] 歌词
-
其他格式:① [分钟:秒] 歌词 ② [分钟:秒:毫秒] 歌词
每首歌的lrc歌词有多行文本,因此lrc歌词中的每行可以采用结构体管理
//lrcpage.h
struct lyricLine{
qint64 _time;
QString _text;
lyricLine(qint64 time, QString text)
:_time(time)
,_text(text)
{}
};
private:
QVector<lyricLine> lrcLines; //保存歌词文件中每一行的内容,一行为单位
通过歌曲名找LRC⽂件
在做项目的时候,我将歌词文件和歌曲文件保存在一个文件夹中,因此这里我直接将音乐文件的后缀换成了lrc。
//music.h
public:
QString getLrcFilePath()const;
//music.cpp
QString Music::getLrcFilePath() const
{
//qDebug() << _musicUrl;
//QString path = _musicUrl.toString();
QString path = _musicUrl.toLocalFile();
//qDebug() << path;
path.replace(".mp3", ".lrc");
path.replace(".flac", ".lrc");
path.replace(".mpga", ".lrc");
return path;
}
LRC歌词解析
找到lrc歌词文件后,由lrcPage类完成对歌词的解析。解析的⼤概步骤:
- 打开歌词文件
- 以行为单位,读取歌词文件中的每一行
- 按照lrc歌词文件格式,从每行文本中解析出时间和歌词 [00:17.94]那些失眠的人啊 你们还好吗 [0:58.600.00]你像⼀只飞来飞去的蝴蝶
- 用<时间,行歌词>构建⼀个LrcLine对象存储到lrcLines中
//lrcpage.h
public:
bool parseLrc(const QString& lrcPath);
//lrcpage.cpp
bool LrcPage::parseLrc(const QString &lrcPath)
{
lrcLines.clear();
QFile file(lrcPath);
if(!file.open(QFile::ReadOnly)){
qDebug()<<"打开⽂件:"<<lrcPath;
return false;
}
while(!file.atEnd())
{
QString lrcWord = file.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(lyricLine(lineTime, word.trimmed()));
}
// for(auto word : lrcLines)
// {
// qDebug()<<word._time<<" "<<word._text;
// }
return true;
}
根据歌曲播放位置获取歌词并同步显⽰
当歌曲播放进度改变时候,QMediaPlayer的positionChanged信号会触发,该信号同步播放时间的时候已经在QQMusic类中处理过了,在其槽函数中就能拿到当前歌曲的播放时间,通过播放时间,就能在LrcPage中找到对应行的歌词
- 根据时间获取存在lrcLines中的行号,默认以第0行开始
- 根据行号开始读取lrcLines中对应的内容
- 根据播放时间的改变信号,将歌词内容显示在歌词页面上
//lxmusic.cpp
//更改函数
//更新当前播放时间并设置进度条的位置
void LXMusic::onPositionChanged(qint64 position)
{
ui->currentTimeLabel->setText((QString("%1:%2").arg(position/1000/60, 2, 10,QChar('0')).arg(position/1000%60,2,10,QChar('0'))));
//设置进度条的位置
ui->processBar->syncSlide((float)position / (float)_player->duration());
if(_playList->getCurrentIndex() >= 0){
//显示歌词
_lrcWord->showLrcWord(position);
}
}
//音乐改变时,更新图片歌名歌手
void LXMusic::onMetaDataChanged()
{
QString musicName = _player->metaData().value(QMediaMetaData::Title).toString();
QString singerName = _player->metaData().value(QMediaMetaData::Author).toString();
QString musicId = _currentPlayPage->getMusicIdByIndex(_playList->getCurrentIndex());
//qDebug() << "musicId::" << musicId;
// if(musicName.isEmpty() || singerName.isEmpty())
// {
auto it = _musicList.findMusicById(musicId);
if(it != _musicList.end())
{
musicName = it->getMusicName();
singerName = it->getSingerName();
//加载歌词
_lrcWord->parseLrc(it->getLrcFilePath());
}
//}
//设置歌手名称、歌名
ui->musicNameLabel->setText(musicName);
ui->musicSingerLabel->setText(singerName);
_lrcWord->setMusicName(musicName);
_lrcWord->setSingerName(singerName);
.........................
}
//lrcpage.h
public:
int getLineLrcWordIndex(qint64 time);
QString getLineWord(qint64 index);
void showLrcWord(int time);
void setMusicName(const QString& name);
void setSingerName(const QString& name);
//lrcpage.cpp
//根据时间获取行号
int LrcPage::getLineLrcWordIndex(qint64 time)
{
// 如果歌词是空的,返回-1
if(lrcLines.isEmpty())
{
return -1;
}
if(lrcLines[0]._time > time)
{
return 0;
}
// 通过时间⽐较,获取下标
for(int i = 1; i < lrcLines.size(); ++i)
{
if(time > lrcLines[i-1]._time && time <= lrcLines[i]._time)
{
return i-1;
}
}
// 如果没有找到,返回最后⼀⾏
return lrcLines.size()-1;
}
//根据行号获取内容
QString LrcPage::getLineWord(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(getLineWord(index-3));
ui->line2->setText(getLineWord(index-2));
ui->line3->setText(getLineWord(index-1));
ui->lineCenter->setText(getLineWord(index));
ui->line4->setText(getLineWord(index+1));
ui->line5->setText(getLineWord(index+2));
ui->line6->setText(getLineWord(index+3));
}
}
//修改页面上的歌名
void LrcPage::setMusicName(const QString &name)
{
ui->musicName->setText(name);
}
//修改页面上的歌手名
void LrcPage::setSingerName(const QString &name)
{
ui->singerName->setText(name);
}
六、数据库支持
数据库初始化
//LXMusic.pro文件中
QT += core gui multimedia sql
//lxmusic.h
#include<QSqlDatabase>
#include<QMessageBox>
#include<QSqlError>
#include<QSqlQuery>
private:
QSqlDatabase sqlite;
public:
void initSQlite();
//lxmusic.cpp
//初始化数据库
void LXMusic::initSQlite()
{
//加载驱动
sqlite = QSqlDatabase::addDatabase("QSQLITE");
//设置数据库名称
sqlite.setDatabaseName("LXMusic.db");
//打开数据库
if(!sqlite.open()){
QMessageBox::critical(this, "打开QQMusicDB失败",sqlite.lastError().text());
return;
}
qDebug() << "数据库连接成功";
// 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] 表成功!!!";
}
歌曲信息写⼊数据库
当程序退出的时候,通过musicList获取到所有music对象,然后将music对象写⼊数据库
//musiclist.h
void writeToDB();
//musiclist.cpp
//让数据库中写入数据
void MusicList::writeToDB()
{
for(const auto& music : _musicList){
QString musicId = music.getMusicId();
QSqlQuery query;
query.prepare("select exists(select 1 from musicInfo where musicId = ?)");
query.addBindValue(musicId);
if(!query.exec()){
qDebug()<<"查询失败: "<<query.lastError().text();
return;
}
if(query.next()){
bool isExists = query.value(0).toBool();
if(isExists)
{
// musicId的歌曲已经存在
// 2. 存在:不需要再插⼊musci对象,此时只需要将isLike和isHistory属性进⾏更新
query.prepare("UPDATE musicInfo SET isLike = ?, isHistory = ? WHERE musicId = ?");
query.addBindValue(music.getIsLike()? 1 : 0);
query.addBindValue(music.getIsHistory()? 1 : 0);
query.addBindValue(musicId);
if(!query.exec())
{
qDebug()<<"更新失败: "<<query.lastError().text();
}
qDebug()<<"更新music信息: "<<music.getMusicName()<<" "<<musicId;
}
else
{
// 3. 不存在:直接将music的属性信息插⼊数据库
query.prepare("INSERT INTO musicInfo(musicId, musicName,musicSinger, albumName, musicUrl,\
duration, isLike, isHistory)VALUES(?,?,?,?,?,?,?,?)");
query.addBindValue(musicId);
query.addBindValue(music.getMusicName());
query.addBindValue(music.getSingerName());
query.addBindValue(music.getAlbumName());
query.addBindValue(music.getMusicUrl().toLocalFile());
query.addBindValue(music.getDuration());
query.addBindValue(music.getIsLike() ? 1 : 0);
query.addBindValue(music.getIsHistory()? 1 : 0);
if(!query.exec())
{
qDebug()<<"插⼊失败: "<<query.lastError().text();
return;
}
qDebug()<<"插⼊music信息: "<<music.getMusicName()<<" "<<musicId;
}
}
}
}
//lxmusic.cpp
//退出
void LXMusic::on_quitButton_clicked()
{
//关闭前写入数据库中
_musicList.writeToDB();
//qDebug() << "写入完成";
//关闭数据库
sqlite.close();
//关闭程序
this->close();
}
程序启动时读取数据库恢复歌曲数据
在程序启动时,从数据库中读取到歌曲的信息,将歌曲信息设置到musicList中,然后让likePage、localPage、recentPage将musicList中个歌曲更新到各自页面中。 从数据库读取歌曲数据的操作,应该让MusicList类完成,因为该类管理所有的Music对象
//musiclist.h
void readFromDB();
//musiclist.cpp
void MusicList::readFromDB()
{
QString sql("SELECT musicId, musicName, musicSinger, albumName,\
duration, musicUrl, isLike, isHistory FROM musicInfo");
QSqlQuery query;
if(!query.exec(sql))
{
qDebug()<<"数据库查询失败";
return;
}
qDebug() << "查询完成";
while(query.next())
{
Music music;
music.setMusicId(query.value(0).toString());
music.setMusicName(query.value(1).toString());
music.setSingerName(query.value(2).toString());
music.setAlbumName(query.value(3).toString());
music.setDuration(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);
}
}
//lxmusic.h
void initMusiclist();
//lxmusic.cpp
void LXMusic::initMusiclist()
{
//初始化musiclist
_musicList.readFromDB();
//将数据更新到各自的页面中
ui->recentMusicPage->refreshPage(_musicList);
ui->likeMusicPage->refreshPage(_musicList);
ui->localMusicPage->refreshPage(_musicList);
//默认将localMusicPage页面中的歌曲设置进playList中
ui->localMusicPage->addMusicToPlayList(_musicList,_playList);
}
LXMusic::LXMusic(QWidget *parent)
: QWidget(parent)
, ui(new Ui::LXMusic)
,_currentPlayPage(nullptr)
{
ui->setupUi(this);
//初始化界面
initUi();
//初始化播放器
initPlayer();
//绑定信号和槽函数
connectSignalAndSlots();
//初始化数据库
initSQlite();
//初始化musiclist并同步数据到页面中
initMusiclist();
}
注意:上面的初始化需要在初始化数据库后面调用,数据库没有建立连接,将不能查询
七、功能查补
7.1 BtForm上动画问题
当播放不同页面歌曲时,BtForm按钮上的跳动动画应该跟随播放页面变化而变化,即那个page页面播放,就应该让该页面的对应BtForm上的动画显示,其余BtForm按钮上的动画隐藏,这样跳动的⾳符始终就可以标记当前正在播放的页面,LXmusic类中currentPage标记当前播放页面,QStackedWidget中提供了通过页面找索引的方法,即currentPage可以找到其在层叠窗口中的索引,该索引与BtForm中的pageId是对应的。因此在LXMusic中定义updateBtFormAnimal函数,该函数实现原理如下:
- 通过stackedwidget对象获取到当前播放页面的索引值,然后找到BtForm类型对象的所有孩子,进行判断,如果是当前播放页面就显示动画,如果不是那么就隐藏动画
- 绑定播放器的播放状态改变信号,当播放状态改变时,调用该函数进行判断
//lxmusic.h
private slots:
void updateBtFormAnimation()
//lxmusic.cpp
void LXMusic::updateBtFormAnimation()
{
int index = ui->stackedWidget->indexOf(_currentPlayPage);
if(-1 == index){
qDebug() << "该页面不存在";
return ;
}
QList<BtForm*> btlist = findChildren<BtForm*>();
for(auto& btForm : btlist){
if(btForm->getPageId() != index){
btForm->hideAnimation();
}
else{
btForm->showAnimation();
}
}
}
//修改函数
void LXMusic::onBtformClicked(int pageId)
{
//获取btform类型的孩子
QList<BtForm*> btFormList = this->findChildren<BtForm*>();
for(auto btForm : btFormList){
if(btForm->getPageId() != pageId){
btForm->clearBackground();
}
else{
btForm->setBackground();
}
}
ui->stackedWidget->setCurrentIndex(pageId);
// qDebug() << pageId;
}
//绑定信号槽
//当播放状态发生改变的时候,更新BtForm上的动画
connect(_player,&QMediaPlayer::playbackStateChanged, this, &LXMusic::updateBtFormAnimation);
7.2 点击BtForm偶尔窗⼝乱移问题
正常情况下,当按钮在非BtForm按钮上点击时,左键长按拖拽窗口才能移动。但是当点击BtForm按钮时,偶尔也能看到窗口移动,估计可能在BtForm上点击时,由于手抖或者其他原因,该次点击时间被qt识别成鼠标移动事件处理了,因此导致窗口移动了。
解决⽅案:在LXMusic类中设置⼀个成员变量isDrag,当鼠标点击时间发生时,如果点在BtForm按钮上则将isDrag设置为false,否则设置为true,此时如果鼠标长按滚动时,再移动窗口
//lxmusic.h
private:
bool _isDrag;//标志位,防止点击BtForm的时候窗口乱移
//lxmusic.cpp,修改函数
void LXMusic::onBtformClicked(int pageId)
{
//获取btform类型的孩子
QList<BtForm*> btFormList = this->findChildren<BtForm*>();
for(auto btForm : btFormList){
if(btForm->getPageId() != pageId){
btForm->clearBackground();
}
else{
btForm->setBackground();
}
}
ui->stackedWidget->setCurrentIndex(pageId);
//设为false防止乱移
_isDrag = false;
// qDebug() << pageId;
}
void LXMusic::mousePressEvent(QMouseEvent* event){
if(event->button() == Qt::LeftButton){
_isDrag = true;
_dragPosition = event->globalPosition().toPoint() - geometry().topLeft();
event->accept();
}
QWidget::mousePressEvent(event);
}
void LXMusic::mouseMoveEvent(QMouseEvent* event){
if(event->buttons() == Qt::LeftButton && _isDrag){
move(event->globalPosition().toPoint() - _dragPosition);
event->accept();
}
QWidget::mouseMoveEvent(event);
}
7.3 点击添加按钮歌曲重复加载问题
当点击加载按钮对同⼀个目录下歌曲重复加载时,仍能加载到程序中并更新到数据库,正常情况下相同目录中的歌曲只能在播放器中加载一份。出现该问题的原因,是加载时,未对已经存在的歌曲⽂件进行过滤导致,因此在addMusicsByUrl中检测下,如果歌曲已经存在则无需解析,也无需加载。
如何知道歌曲是否已经被加载过了呢?最简单的方式是直接到musicList中查找,但是musicList为线性的QVector,循环遍历效率太低,因此可以借助QSet容器(相当于C++标准库中的unordered_set),将歌曲的路径保存⼀份(同⼀个电脑上,文件路径不可能重复),在进行歌曲加载时,先检测歌曲⽂件是否存在,如果不存在则添加否则不添加
//musiclist.h
#include<QSet>
QSet<QString> _musicPaths;
//musiclist.cpp
//修改函数
//通过url加载文件到歌曲列表
void MusicList::addMusicByUrl(const QList<QUrl> &urls)
{
for(const auto& url : urls){
//检查音乐文件是否已经添加过
QString musicPath = url.toLocalFile();
if(_musicPaths.contains(musicPath))
continue;
//分析文件类型,如果是音乐类型那么就通过url构建music对象,保存到musicList中
QMimeDatabase mimedata;
QMimeType mime = mimedata.mimeTypeForFile(url.toLocalFile());
if(mime.name() != "audio/mpeg" && mime.name() != "audio/mflac" && mime.name() != "audio/mp3")
{
continue;
}
//如果歌曲还没有被加载过,同时是一个有效的歌曲文件,加载进歌曲列表中
_musicPaths.insert(musicPath);
// 如果是⾳乐⽂件,加⼊歌曲列表
_musicList.push_back(url);
}
}
//从数据库中读取数据
void MusicList::readFromDB()
{
QString sql("SELECT musicId, musicName, musicSinger, albumName,\
duration, musicUrl, isLike, isHistory FROM musicInfo");
QSqlQuery query;
if(!query.exec(sql))
{
qDebug()<<"数据库查询失败";
return;
}
qDebug() << "查询完成";
while(query.next())
{
//qDebug() << query.value(0).toString();
Music music;
music.setMusicId(query.value(0).toString());
music.setMusicName(query.value(1).toString());
music.setSingerName(query.value(2).toString());
music.setAlbumName(query.value(3).toString());
music.setDuration(query.value(4).toLongLong());
music.setMusicUrl("file:///" + query.value(5).toString());
music.setIsLike(query.value(6).toBool());
music.setIsHistory(query.value(7).toBool());
_musicList.push_back(music);
_musicPaths.insert(music.getMusicUrl().toLocalFile());
}
// for(auto& url : _musicPaths){
// qDebug() << url;
// }
}
//修改
//music.cpp
QString Music::getLrcFilePath() const
{
//qDebug() << _musicUrl;
//QString path = _musicUrl.toString();
QString path = _musicUrl.toLocalFile();
//qDebug() << path;
path.replace(".mp3", ".lrc");
path.replace(".flac", ".lrc");
path.replace(".mpga", ".lrc");
return path;
}
7.4 添加系统托盘
当点击关闭按钮时,不让播放器直接退出,而是将窗口隐藏掉,窗口的图标在系统托盘位置,当在播放器图标上右键单击时,弹出菜单让用户选择是显示窗口还是继续退出
//lxmusic.h
#include <QSystemTrayIcon>
#include <QMenu>
public:
void createTray();
private slots:
void quitLXMusic();
//lxmusic.cpp
//初始化系统托盘
void LXMusic::createTray()
{
//增加系统托盘
QSystemTrayIcon* tryIcon = new QSystemTrayIcon(this);
tryIcon->setIcon(QIcon(":/images/logo.png"));
//创建托盘菜单
QMenu* menu = new QMenu(this);
menu->addAction("还原",this, &QWidget::showNormal);
menu->addSeparator();
menu->addAction("退出", this, &LXMusic::quitLXMusic);
//将托盘菜单添加到托盘图标
tryIcon->setContextMenu(menu);
tryIcon->showMessage("提示", "应用程序已最小化到托盘", QSystemTrayIcon::Information, 3000);
//为托盘图标添加点击事件处理。当左键点击时还原窗口
connect(tryIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
if (reason == QSystemTrayIcon::Trigger) { // 左键点击
this->showNormal(); // 还原窗口
}
});
//显示系统托盘
tryIcon->show();
}
void LXMusic::quitLXMusic()
{
qDebug() << "退出按钮被点击";
//关闭前写入数据库中
_musicList.writeToDB();
qDebug() << "写入完成";
//关闭数据库
sqlite.close();
//关闭程序
QApplication::quit();
}
void LXMusic::on_quitButton_clicked()
{
// 点击关闭按钮时,程序不退出,隐藏掉
// ⽤⼾可以从系统托盘位置选择显⽰或者关闭
hide();
}
7.5 保证程序只运行一次
现在每点击⼀次LXMusic.exe,都会创建⼀个LXMusic的实例,即同⼀个机器上可以运行多份程序实例,多个实例同时运⾏有以下缺陷:
- 多个实例同时运行可能会导致资源浪费,如内存、CPU效率等
- 如果应用程序涉及对共享数据的修改,多个程序同时运行可能会导致数据不⼀致问题
- 若多个实例尝试访问同⼀资源时,如文件、数据库等,可能会导致冲突或错误
- 另外,用户体验不是很好,多个实例操作时容易混淆因此有时会禁止程序多开,即⼀个应用程序只能运行⼀个实例,也称为单实例应用程序或单例应⽤程序
在Qt中,禁止程序多开的方式有好几种,此处采用共享内存实现。
共享内存是操作系统中的概念,是进程间通信的⼀种机制。由于相同key值的共享内存只能存在⼀份,因此在程序启动时可以检测共享内存是否已经被创建,如果已经创建则说明程序已经在运行,否则程序还没有运行
//mian.cpp
// 创建共享内存
QSharedMemory sharedMem("LXMusic");
// 如果共享内存已经被占⽤,说明已经有实例在运⾏
if (sharedMem.attach()) {
QMessageBox::information(nullptr, "QQMusic", "QQMusic已经在运⾏...");
return 0;
}
sharedMem.create(1);
7.6 禁⽌qDebug()输出
要逐个删除程序中qDebug的打印太麻烦,可以再配置文件中通过添加以下语句,禁止qDebug输出,只能禁止release模式下的输出,debug模式下不禁止:
//LXMusic.pro
# ban qDebug output
DEFINES += QT_NO_DEBUG_OUTPUT
八、项目思维导图
项目到这就结束了