目录
项目介绍
该项目是仿QQ音乐客户端界面使用 Qt Creator 4.11.1 (Community) 实现的音乐播放器软件,在自学Qt课程时,敲的都是⼀些测试验证的小demo,知识点难以系统联系起来,通过做项目把自己学习过的Qt基础知识全部串联起来,融会贯通。
项目概述
综合运用UI界面设计其主要功能如下:
窗口head部分:
- 点击窗口最小化按钮,窗口最小化
- 点击窗口最大化按钮,窗口无反应(即禁止窗口最大化)
- 点击关闭按钮,程序隐藏到任务栏中
- 点击换肤按钮,更好皮肤(该功能暂未支持,后期可继续拓展)
- 点击搜索框搜索功能(该功能暂未支持,后期可继续拓展)
窗口body左侧:
- 点击推荐按钮,窗口右侧显示:推荐Page(暂只有⻚⾯)
- 点击电台按钮,窗口右侧显示:电台Page(未⽀持)
- 点击⾳乐馆按钮,窗口右侧显示:⾳乐馆Page(未⽀持)
- 点击我喜欢按钮,窗口右侧显示:收藏的⾳乐Page
- 点击本地下载按钮,窗口右侧显示:本地⾳乐Page
- 点击最近播放按钮,窗口右侧显示:最近播放Page
**注意:**左侧按钮,当光标悬停在上面时有不同颜色突出显示,当点击时颜色会加深。
窗口右侧:
当窗口左侧不同按钮点击,在窗口右侧会展示不同的页面,本项目暂只⽀持了本地⾳乐、喜欢
⾳乐、最近播放⾳乐的展⽰。具体功能如下:
- 点击全部播放按钮,播放当前页面列表中所有音乐
- 双击列表中某⾳乐,播放当前选中音乐
- 点击爱心⽀持收藏
- 支持最近播放过的音乐
播放控制区:
- 支持seek功能,即拖拽到歌曲指定位置播放
- 支持随机、单曲循环、循环播放
- 支持播放上⼀曲
- 支持播放下一首
- 支持播放和暂停
- 支持音量调节和静音
- 支持歌词总时长显示+当前播放时间显示
- 支持LRC歌词同步显示
- 支持歌名、歌手、歌曲专辑图片显示
界面开发
界面分析
界面上控件比较多,归类之后建议分为三部分:head区、body区、play区
head区域:从左向右依次为图标Logo、搜索框、更换皮肤按钮、最小化&最大化&退出按钮
body区域:分为左侧类型选择区域和右侧Page展示区
body左侧区域有两部分组成:在线音乐和我的音乐,两部分内部的控件种类是相同的
- ①⑤区域划分,实际为QLabel
- ②自定义控件:图片+文本+动画
- ③④⑤⑥⑦⑧同②一样,自定义控件:图片+文本+动画
body右侧区域:Page区
Page区:歌曲信息界面,鼠标点击翻页按钮具有轮播图效果。
play区域:播放控制区、播放进度两部分构成
播放进度:当前歌曲播放进度说明,支持seek功能,与播放控制区时间、以及LRC歌词是同步的
播放控制区域:显示歌曲图片&歌名&歌手、播放模式选择&上一首&播放|暂停&下一首&音量条件和静音&添加本地歌曲、当前播放时间/歌曲总时长&弹出歌词窗口按钮
Page说明:
当点击body左侧不同的按钮时,Page页面会显示不同的页面
|-------|----------------------------------------------------------------------------|
| 推荐按钮 | |
| 电台 | 暂未支持 |
| 音乐馆 | 暂未支持 |
| 喜欢 | |
| 本地和下载 | |
| 最近播放 | |
body右侧目前支持的4个页面结构,整体的布局是相同的,唯独Page区域显示的内容稍有区别
推荐页面具有类似轮播图的动态效果:
整个页面内容可以分为上下两组:今日为你推荐、你的推荐歌单。两组的布局实际是相同的,元素
说明:
- 上⽅显示1行,内部有4个推荐元素;下方显示2行,每行有4个推荐元素
- 左右两侧⼀个按钮,点击后推荐内容会更换下⼀批,不停点击会循环推荐
- 当鼠标悬停在推荐元素上时,推荐元素会向上移动,当⿏标离开时,⼜回到原位置
- 当鼠标悬停在推荐元素上时,同时会出现小手图标,说明该推荐元素具有点击功能
该页面中内容也为自定义元素,后序⻚⾯实现时具体分析。
喜欢、本地和下载、最近播放类似下图:
这三个Page中布局、控件都是相同的,只是填充的数据不⼀样。每个Page中包含了多个控件,大致如下:
- ①QLabel:类型说明
- ②QLabel:图片显示
- ③QButton:播放全部按钮
- ④一组QLabel说明:歌名、歌手、歌曲专辑
- ⑤QListWidget:播放列表
可以通过自定义控件的方式,将①~⑤的控件集成到一起形成一个新的控件,方便复用,因此这三个Page属于同一个自定义类型的Page
这两个页面,将来由QStackedWidget控件组织管理起来,就可以实现点击不同按钮,显示不同页面的效果。
歌词界面:
解析当前正在播放音乐的歌词,同步显示在界面上
显示内容分为:歌词部分信息、左上角收起隐藏按钮
- 歌词部分展示当前正在播放词和在播放部分前三行和后三行歌词展示,当前播放歌词突出显示
- 点击收起按钮后,该页面会以动画滑动的方式收起
当歌曲有LRC歌词时,播放时歌词会随播放时间自动调整,歌词没有LRC歌词时,歌词部分显示空字符
创建工程
创建⼀个基于QWidget的⼯程,选中⽣成form选项,将来界⾯部分主要使⽤QDesigner来设计。
主界面布局设计
基于Widget局部
Qt系统提供四种布局管理器:
- QHBoxLayout:水平布局
- QVBoxLayout:垂直布局
- QGridLayout:栅格布局
- QFormLayout:表单布局
由于⼀个widget中只能包含上述布局管理器中的⼀种,所以直接使⽤布局管理器来布局不是很灵活;
而⼀个widget中可以包含多个widget,在widget中的控件可以进行水平、垂直、栅格、表单等布局操作,非常灵活。
因此本项目基于Widget来进行布局
窗口主框架设计
主窗口的布局
选中该项目,在弹出的属性中找个geometry属性,将窗口高度宽度改为:1040*700
从控件区拖拽⼀个Widget到窗⼝区域,objectName修改为:background,选中QQMusic,然后
点击垂直布局,background就填充满了整个窗口
为了看到效果,选中backroound控件,然后右键单击,弹出菜单中选择改变样式表,内部添加:
background-color:gray;
此处的颜⾊效果仅为⽅便看到界⾯效果,等界⾯框架设计完成后,将所有的颜⾊清除掉,界⾯添加特定颜⾊。
整个窗口由head、body、play上中下三部分组成。
直接拖三个Widget放到设计区,双击将名字修改为head、body、play;
head、body、play平分了整个background,并且它们三个之间的margin有间隔。再次选中background对象,右侧属性部分下滑找到Layout,将Margni和Space修改为0
但是head占区域过⼤,选中head对象,将head的minimumSize和maxmumSize属性的⾼度都调整为80,这样head的大小就固定了
head内部设计
head内部由两部分构成,headLeft区域显⽰图标Logo,headRight区域为搜索框和功能按钮区域。
拖两个widget到head中,选中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进去,然后选中searchBox点击⽔平布局
settingBox
拖拽⼀个按钮到SettingBox,按钮的minimumSize和maximumSize的宽度和⾼度都修改为30,然后⿏标选中,按着ctrl键+⿏标拖拽,复制3个出来摆放好,依次将四个按钮的objectName从左往右修改为:skin、max、min、quit,并将按钮的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的⾼度修改为420
- 选中bodyLeft,点击垂直布局,并将bodyLeft的Margin和Spacing修改为0
leftBox内部布局
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一个层叠窗口
- 拖拽层叠窗口控件StackedWidget,就在Widget控件上方到bodyRight中
- 选中bodyRight,点击垂直布局,然后将bodyRight的Margin和Spacing修改为0
stackedWidget内部增加页面
stackedWidget默认会提供两个⻚⾯,还需添加四个⻚⾯
在对象区域选中stackedWidget控件,然后右键单击弹出菜单中选择添加⻚
以类似的⽅式添加添加4个⻚⾯,并修改每个⻚⾯的objectName如下:
总共六个⻚⾯,每个⻚⾯都有⾃⼰的索引,所以是从0开始的,将来切换⻚⾯时就是通过索引来切换的。
选中stackedWidget,然后右键单击,弹出菜单中选择:改变页顺序,在弹出的窗⼝中就能看到每个页面的索引
六个⻚⾯中,recPage页面需要实现,musicPage、radioPage暂未⽀持
Play布局
进度滑竿、播放控制区两部分组成
- 拖拽Widget到Play,将objectName修改为processBar,将minimumSize和maximumSize的⾼度修改为75
- 选中Play,点击垂直布局,然后将bodyRight的Margin和Spacing修改为0
ControlBox内部布局
该区域内部由三部分组成:歌曲信息部分、播放控制部分、时间显示
- 拖拽三个Widget到ControlBox中,将ObjectName依次修改为play_1、play_2、play_3
- 选中ControlBox,点击⽔平布局,将ControlBox的Margin和Spacing修改为0
play1内部:
- 拖拽3个QLabel,放置歌曲图⽚、歌⼿名和歌曲名字,调整好位置,将QLabel的objectName修改为: musicCover、musicName、musicSinger
- 然后选中play1,点击栅格布局
拖入一个Widget:
- 左侧Widget放入Vertical Spacer和一个QLabel,QLabel的objectName修改为currentTime
- Widget的宽度修改为60,利用弹簧调整QLabel位置与progressBar进度条平行
- 选中Widget,点击⽔平布局,并将Margin和Spacing修改为0
play2内部:
- 添加两个Widget最上面从左到右依次摆放6个按钮,按钮的minimumSize和maxmumSize均修改为30*30,将objectName从左往右依次修改为:playMode、playUp、Play、playDown、volume、addLocal,最下面放一个进度条将objectName修改为:progressBar,进度条高度设置为20
- 然后选中play2,点击⽔平布局,并将play_2的Margin和Spacing修改为0
拖入一个Widget:
- 又侧Widget放入Vertical Spacer和一个QLabel,QLabel的objectName修改为totalTime
- Widget的宽度修改为60,利用弹簧调整QLabel位置与progressBar进度条平行
- 选中Widget,点击⽔平布局,并将Margin和Spacing修改为0
play3内部:
- 拖入一个QPushButton按钮,按钮的objectName修改为lrcWord
- 选中play3,点击⽔平布局,并将play3的Margin和Spacing修改为0
界面美化
主窗口设定
仔细观察发现主窗⼝是没有标题栏,因此在窗⼝创建前,就需要设置下窗⼝的格式
cpp
QWidget::setWindowFlag(...): 设置窗⼝格式,⽐如创建⽆边框的窗⼝
由于窗口中控件比较多,这些控件将来都需要初始化,如果将所有代码放在QQMusic的构造函数中实现,将来会造成构造函数非常臃肿,因此在QQMusic类中添加initUI()方法来完成界面初始化⼯作
cpp
// QQMusic.h ⽂件中添加:
void initUI();
// 添加完成后,光标放在函数名字上按 alt + Enter 组合键完成⽅法定义
// QQMusic.cpp 头⽂件中完成定义
void QQMusic::initUI()
{
// 设置⽆边框窗⼝,即窗⼝将来⽆标题栏
setWindowFlag(Qt::WindowType::FramelessWindowHint);
}
添加完成后⼀定要在QQMusic的构造函数中调用initUI()函数,否则设置不会生效。运行后,发现有以下两个问题:
- 窗口无标题栏,找不到关闭按钮,导致窗口无法关闭
- 窗口无法拖拽
关闭窗口,可以先将光标放在任务栏中当前应用程序图标上,弹出的框中选择关闭,后序会实现关闭功能
主界面无法拖动,此时只需要处理下鼠标单击(mousePressEvent)和鼠标移动(mouseMoveEvent)事件即可
鼠标左键按下时,记录下窗口左上⻆和鼠标的相对位置
鼠标移动时,会产⽣新的位置,保持鼠标和窗口左上⻆相对位置不变,通过move修改窗口的左上⻆坐标即可
cpp
// QQMusic.h中添加
protected:
// 重写QWidget类的⿏标单击和⿏标滚轮事件
void mousePressEvent(QMouseEvent *event)override;
void mouseMoveEvent(QMouseEvent* event)override;
// 记录光标相对于窗⼝标题栏的相对距离
QPoint dragPosition;
// QQMusic.cpp中添加
void QQMusic::mousePressEvent(QMouseEvent *event)
{
// 拦截⿏标左键单击事件
if(event->button() == Qt::LeftButton)
{
// event->globalPos():⿏标按下事件发⽣时,光标相对于屏幕左上⻆位置
// frameGeometry().topLeft(): ⿏标按下事件发⽣时,窗⼝左上⻆位置
// geometry(): 不包括边框及顶部标题区的范围
// frameGeometry(): 包括边框及顶部标题区的范围
// event->globalPos() - frameGeometry().topLeft() 即为:
// ⿏标按下时,窗⼝左上⻆和光标之间的距离差
// 想要窗⼝⿏标按下时窗⼝移动,只需要在mouseMoveEvent中,让光标和窗⼝左上⻆保持相同的位置差
// 获取⿏标相对于屏幕左上⻆的全局坐标
dragPosition = event->globalPos() - frameGeometry().topLeft();
return;
}
QWidget::mousePressEvent(event);
}
void QQMusic::mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() == Qt::LeftButton)
{
// 根据⿏标移动更新窗⼝位置
move(event->globalPos() - dragPosition);
return;
}
QWidget::mouseMoveEvent(event);
}
再仔细观察,窗口周围是有阴影效果的,窗口四周黑色部分就是阴影
给窗⼝添加阴影需要用到QGraphicsDropShadowEffect类
- 创建QGraphicsDropShadowEffect类对象
- 设置阴影的属性。比如:设置阴影的偏移、颜色、圆角等
- 将阴影设置到具体对象上
在initUI()函数中添加如下代码:
cpp
// 设置窗⼝背景透明
this->setAttribute(Qt::WA_TranslucentBackground);
// 给窗⼝设置阴影效果
QGraphicsDropShadowEffect* shadowEffect = new
QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0,0); // 设置阴影偏移
shadowEffect->setColor("#000000"); // 设置阴影颜⾊:⿊⾊
shadowEffect->setBlurRadius(10); // 设置阴影的模糊半径
this->setGraphicsEffect(shadowEffect);
给窗口设置阴影效果时,需要将窗口标题栏⽆边框,背景设置为透明
添加图片资源
添加⼀个qrc⽂件,将图片资源拷贝到工程目录下,并添加到工程中
将之前布局时所有按钮的背景颜色全部清除掉,按照下面的风格重新设定
head处理
|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 控件 | QSS美化 |
| headLeft | cpp #headRight { background-color:rgb(246,246,246); }
|
| headRight | cpp #headRight { background-color:rgb(250,250,250); }
|
| Logo | cpp #logo { background-image:url(":/images/Logo.png"); background-repeat:no-repeat; background-position:center center; }
|
| lineEdit | cpp #lineEdit { background-color:rgb(227, 227, 227); border:none; border-radius:18px; padding-left:10px; }
|
| settingBox | cpp QPushButton { border:none; background-repeat:no-repeat; background-position:center center; } QPushButton:hover { background-color:rgba(24,233,114,0.5); }
|
| skin | cpp #skin { border:none; background-image:url(":/images/skin.png"); background-repeat:no-repeat; background-position:center center; }
|
| max | cpp #max { background-image:url(":/images/max.png"); }
|
| min | cpp #min { background-image:url(":/images/min.png"); }
|
| quit | cpp #quit { background-image:url(":/images/quit.png"); }
|
| bodyLeft | cpp background-color:rgb(246,246,246);
|
| bodyRight | cpp #bodyRight { background-color:rgb(250,250,250); }
|
| Play | cpp #Play { background-color:rgb(250,250,250); }
|
播放控制区处理
祛除play1、play2、play3的页面布局时设置的临时背景色。将按钮上的文字全部去除,然后重新添加样式和图片
|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 控件 | QSS美化 |
| play2 | cpp QPushButton { border:none; background-repeat:no-repeat; background-position: center center; }
|
| playMode | cpp QPushButton:hover { background-color:rgba(24,233,114,0.5); }
|
| playUp | cpp #playUp { background-image:url(":/images/playUp.png"); } QPushButton:hover { background-color:rgba(24,233,114,0.5); }
|
| play | cpp #play { border-radius:17px; } QPushButton { background-color:rgba(24,233,114,0.8); }
|
| playDown | cpp #playDown { background-image:url(":/images/playDown.png"); } QPushButton:hover { background-color:rgba(24,233,114,0.5); }
|
| volume | cpp #volume { background-image:url(":/images/volume.png"); } QPushButton:hover { background-color:rgba(24,233,114,0.5); }
|
| addLocal | cpp #addLocal { background-image:url(":/images/addLocal.png"); } QPushButton:hover { background-color:rgba(24,233,114,0.5); }
|
| lrcWord | cpp #song:hover { background-color:rgba(24,233,114,0.5); } #song { border:none; background-image:url(":/images/ci.png"); background-repeat:no-repeat; background-position: center center; }
|
自定义控件
BtForm
BtForm界面设计
添加⼀个新设计界面,命名为BtForm
该控件实际由:图片、⽂字、动画三部分组成。图片和⽂字分别⽤QLabel展示,动画部分内部实际为4个QLabel
- 将BtForm的geometry的宽度和⾼度修改为200*35。
- 拖⼀个Widget到btForm中,objectName修改为btStyle,将btForm的margin和Spacing设置为0
- 拖2个QLable和1个Widget到btStyle中,并将objectName依次修改为btIcon、btText、lineBoxbtIcon的minimumSize和maximumSize的宽度设置为30(为了看到效果可将颜色设置为red)btText的minimumSize和maximumSize的宽度设置为90(为了看到效果可将颜色设置为green)lineBox的minimumSize和maximumSize的宽度设置为30然后选中btStyle,并将其margin和Spacing设置为0
- 然后往lineBox内部拖4个QLabel,objectName依次修改为line1、line2、line3、line4,minimumSize和maximumSize的宽度均设置为2
|---------|------------------------------------------------------------------------------------------------------------------------|
| 控件 | QSS美化 |
| btStyle | cpp #btStyle:hover { border-radius: 7px; background-color:#e4e4e4; } QLabel { background-color : transparent; }
|
| lineBox | cpp QLabel { background-color:#FFFFFF } #lineBox{ background-color:transparent; }
|
将bodyLeft内部onlineMusic和MyMusic中的QWidget全部提升为BtForm
选中要提升的控件,比如:Rec,在弹出的菜单中选择提升为,会出现⼀个新窗口(如下右侧图),在提升的类名称中输⼊要提升为的类型BtForm,然后点击添加,最后选中btform.h点击提升,便可以将Rec由QWidget提升为⾃定义的BtForm类型
BtForm类中实现
设置按钮上的图⽚和文字信息,以及该按钮关联的page页面
cpp
// btform.h 新增
// 按钮id:该按钮对应的page⻚
int id = 0;
// 设置图标 ⽂字 id
void seticon(QString btIcon,QString content,int mid);
// btform.cpp新增
void btFrom::seticon(QString btIcon,QString btText,int mid)
{
// 设置⾃定义按钮的图⽚、⽂字、以及id
ui->btIcon->setPixmap(QPixmap(btIcon));
ui->btText->setText(btText);
this->id = mid;
}
在QQMusic.cpp的initUI()函数中新增:
cpp
void Widget::initUi()
{
// ...
// 设置BodyLeft中6个btForm的信息
ui->rec->seticon(":/images/rec.png", "推荐", 1);
ui->music->seticon(":/images/music.png", "⾳乐馆", 2);
ui->audio->seticon(":/images/radio.png", "电台", 3);
ui->like->seticon(":/images/like.png", "我喜欢", 4);
ui->local->seticon(":/images/local.png", "本地下载", 5);
ui->recent->seticon(":/images/recent.png", "最近播放", 6);
}
按钮响应
重写鼠标mousePressEvent,当按钮按下时:
- 按钮颜色发生变化
- 给QQMusic类发送click信号
cpp
/ btform.h 新增
protected:
// ⿏标点击事件
virtual void mousePressEvent(QMouseEvent *event);
// btform.cpp新增
void btFrom::mousePressEvent(QMouseEvent *event)
{
// 告诉编译器不要触发警告
(void)event;
// ⿏标点击之后,背景变为绿⾊,⽂字变为⽩⾊
ui->btStyle->setStyleSheet("#btStyle{ background:rgb(30,206,154);}*{color:#F6F6F6;}");
emit click(this->id); // 发送⿏标点击信号
}
- QQMusic类处理该信号,内部:实现窗口切换,并清除上次按钮点击留下的样式,因此QQMuisc中需要新增:
cpp
// qqmusic.h 新增
// btForm点击槽函数
void onBtFormClick(int id);
// qqmusic.cpp 新增
void QQMusic::connectSignalAndSlot()
{
// ...
// ⾃定义的btFrom按钮点击信号,当btForm点击后,设置对应的堆叠窗⼝
connect(ui->rec, &btFrom::click, this, &QQMusic::onBtFormClick);
connect(ui->musics, &btFrom::click, this, &QQMusic::onBtFormClick);
connect(ui->audio, &btFrom::click, this, &QQMusic::onBtFormClick);
connect(ui->like, &btFrom::click, this, &QQMusic::onBtFormClick);
connect(ui->local, &btFrom::click, this, &QQMusic::onBtFormClick);
connect(ui->recent, &btFrom::click, this, &QQMusic::onBtFormClick);
}
void Widget::onBtFormClick(int id)
{
// 1.获取当前⻚⾯所有btFrom按钮类型的对象
QList<BtForm*> buttonList = this->findChildren<BtForm*>();
// 2.遍历所有对象, 如果不是当前id的按钮,则把之前设置的背景颜⾊清除掉
foreach(BtForm* btitem, buttonList)
{
if(id != btitem->getId())
{
btitem->clearBg();
}
}
// 3.设置当前栈空间显⽰⻚⾯
ui->stackedWidget->setCurrentIndex(id - 1);
}
- BtForm类中新增:
cpp
// btform.h 新增
public:
// 清除上⼀次按钮点击留下的样式
void clearBg();
// 获取id
int getId();
// btform.cpp 新增:
void BtForm::clearBg()
{
// 清除上⼀个按钮点击的背景效果,恢复之前的样式
ui->btStyle->setStyleSheet("#btStyle:hover{ background:#D8D8D8;} ");
}
int BtForm::getId()
{
return id;
}
为了能看到Page切换的效果,可以在stackedWidget的每个page上放⼀个QLabel说明
BtFrom上的动画效果
Qt中QPropertyAnimation类可以提供简单的动画效果,允许对QObject获取派生类的可读写属性进行动画处理,创建平滑、连续的动画效果,比如控件的位置、大小、颜色等属性变化,使用时需包含<QPropertyAnimation>
关键函数说明:
cpp
/*
功能:实例化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中的line1、line2、line3、line4添加动画效果,BtForm类中增加如下代码:
cpp
// btform.h 新增:
// linebox动画起伏效果
QPropertyAnimation *animationLine1;
QPropertyAnimation *animationLine2;
QPropertyAnimation *animationLine3;
QPropertyAnimation *animationLine4;
// btform.cpp的构造函数中新增:
BtForm::BtForm(QWidget *parent) :
QWidget(parent),
ui(new Ui::BtForm)
{
ui->setupUi(this);
// 设置line1的动画效果
line1Animal = new QPropertyAnimation(ui->line1, "geometry", this);
line1Animal->setDuration(1800);
line1Animal->setKeyValueAt(0, QRect(0, 15, 2, 0));
line1Animal->setKeyValueAt(0.5, QRect(0, 0, 2, 15));
line1Animal->setKeyValueAt(1, QRect(0, 15, 2, 0));
line1Animal->setLoopCount(-1);
line1Animal->start();
// 设置line2的动画效果
line2Animal = new QPropertyAnimation(ui->line2, "geometry", this);
line2Animal->setDuration(1700);
line2Animal->setKeyValueAt(0, QRect(6, 15, 2, 0));
line2Animal->setKeyValueAt(0.5, QRect(6, 0, 2, 15));
line2Animal->setKeyValueAt(1, QRect(6, 15, 2, 0));
line2Animal->setLoopCount(-1);
line2Animal->start();
// 设置line3的动画效果
line3Animal = new QPropertyAnimation(ui->line3, "geometry", this);
line3Animal->setDuration(1900);
line3Animal->setKeyValueAt(0, QRect(12, 15, 2, 0));
line3Animal->setKeyValueAt(0.5, QRect(12, 0, 2, 15));
line3Animal->setKeyValueAt(1, QRect(12, 15, 2, 0));
line3Animal->setLoopCount(-1);
line3Animal->start();
// 设置line4的动画效果
line4Animal = new QPropertyAnimation(ui->line4, "geometry", this);
line4Animal->setDuration(2000);
line4Animal->setKeyValueAt(0, QRect(18, 15, 2, 0));
line4Animal->setKeyValueAt(0.5, QRect(18, 0, 2, 15));
line4Animal->setKeyValueAt(1, QRect(18, 15, 2, 0));
line4Animal->setLoopCount(-1);
line4Animal->start();
}
关于动画显示
动画并不是所有页面都显示,只有当前选中的页面显示,所以默认情况下,动画隐藏。默认情况下设置addlocal显示
cpp
// btform.h 新增:
// 显⽰动画效果
void showAnimal();
// btform.cpp的中新增:
void btFrom::showAnimal()
{
// 显⽰linebox, 设置颜⾊为绿⾊
ui->linebox->show();
}
// QQMusic的initUI中设置默认选中
void QQMusic::initUi()
{
// ...
// 本地下载BtForm动画默认显⽰
ui->local->showAnimal();
ui->stackedWidget->setCurrentIndex(4);
}
推荐页面
推荐页面分析
仔细观察推荐页面,对其进行拆解发现,推荐页面由五部分构成:
- ①"推荐"文本提⽰,即QLabel
- ②"今⽇为你推荐"⽂本提⽰,即QLabel
- ③具体推荐的歌曲内容,点击左右两侧翻页按钮,具有轮番图效果,将光标放到图上,有图片上移动
- ④"你的推荐歌单"文本提示,即QLabel
- ⑤具体显示音乐,和③实际是⼀样的,不同的是③中⾳乐只有⼀⾏,⑤中的音乐有两行
- 因为页面中元素较多,直接摆到⼀个⻚⾯太拥挤,从右侧的滚动条可以看出,整个页面中的元素都放置在QScrollArea中
仔细分析③发现,里面包含了:
- 左右各两个按钮,点击之后中间的图片会左右移动,Qt中未提供类似该种组合控件,因此③实际为⾃定义控件
- ③中按钮之间的元素,由图⽚和底下的文字组成,当光标放在图片上会有上移的动画,因此该元素实际也为自定义控件
推荐页布局
在stackedWidget中选中推荐页面,objectName为recPage的页⾯,删掉之前添加的QLabel推荐提示
- ①拖拽⼀个QScrollArea到recPage中,geometry的宽度和⾼度修改为822和527
- ②拖拽⼀个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,点击垂直布局。
自定义recBox
RecBox界面布局
- 新添加设计师界⾯,命名为RecBox。geometry的宽⾼修改为:685*400。
- 添加三个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美化:
|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| btUp | cpp #btUp { border:none; background-image:url(:/images/up_page.png); background-repeat:no-repeat; background-position:center center; } QPushButton:hover { background-color:#1ECD97; }
|
| btDown | cpp #btDown { border:none; background-image:url(:/images/Down_page.png); background-repeat:no-repeat; background-position:center center; } QPushButton:hover { background-color:#1ECD97; }
|
将QQMusic主界⾯中recPage页面中的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,点击选择小手图标
cpp
#recMusicBt
{
border:none;
}
RecBoxItem类中添加动画效果
在RecBoxItem类中拦截鼠标进⼊和离开事件,在进⼊时让图片上移,在离开时让图片下移回到原位
cpp
// RecBoxItem.h 新增
bool eventFilter(QObject *watched, QEvent *event);
// RecBoxItem.cpp 新增
#include <QPropertyAnimation>
#include <QDebug>
bool RecBoxItem::eventFilter(QObject *watched, QEvent *event)
{
// 注意:recItem上有⼀个按钮,当⿏标放在按钮上时在开启动画
if(watched == ui->musicImageBox)
{
int ImgWidget = ui->musicImageBox->width();
int ImgHeight = ui->musicImageBox->height();
// 拦截⿏标进⼊事件
if(event->type() == QEvent::Enter)
{
QPropertyAnimation* animation = new QPropertyAnimation(ui->musicImageBox, "geometry");
animation->setDuration(100);
animation->setStartValue(QRect(9,10,ImgWidget,ImgHeight));
animation->setEndValue(QRect(9, 0, ImgWidget, ImgHeight));
animation->start();
// 注意:动画结束的时候会触发finished信号,拦截到该信号,销毁animation
connect(animation, &QPropertyAnimation::finished, this, [=](){
delete animation;
qDebug()<<"图⽚上移动画结束";
});
return true;
}
else if(event->type() == QEvent::Leave)
{
// 拦截⿏标离开事件
QPropertyAnimation* animation = new QPropertyAnimation(ui->musicImageBox, "geometry");
animation->setDuration(150);
animation->setStartValue(QRect(9,0,ImgWidget,ImgHeight));
animation->setEndValue(QRect(9, 10, ImgWidget, ImgHeight));
animation->start();
// 注意:动画结束的时候会触发finished信号,拦截到该信号,销毁animation
connect(animation, &QPropertyAnimation::finished, this, [=](){
delete animation;
qDebug()<<"图⽚上移动画结束";
});
return true;
}
}
return QObject::eventFilter(watched, event);
}
// 注意:不要忘记事件拦截器安装,否则时间拦截不到,因此需要在构造函数中添加:
// 拦截事件处理器时,⼀定要安装事件拦截器
ui->musicImageBox->installEventFilter(this);
该类中还需要添加设置推荐文本和图片的⽅法,将来需要在外部来设置每个RecBoxItem的文本和图片
cpp
// 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 imgStyle = "border-image:url("+Imagepath+");";
ui->recMusicImg->setStyleSheet(imgStyle);
}
RecBox添加RecBoxItem
图片路径和推荐文本准备
每个RecBoxItem都有对应的图片和推荐文本,在往RecBox中添加RecBoxItem前需要先将图片路径和对应⽂本准备好。由于图片和文本具有对应关系,可以以键值对⽅式来进⾏组织,以下实现的时采用Qt内置的QJsonObject对象管理图片路径和文本内容
cpp
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() cons
图⽚路径和对应⽂本的准备⼯作,应该在QQMusic类中处理好,RecBoxItem只负责设置,因此该准备工作需要在QQMusic类中进行,故QQMusic中需要添加如下代码:
cpp
// QQMusic.h 新增
// 参数num:RecBox中图⽚个数
QJsonArray RandomPicture();
// QQMusic.cpp中新增
// 设置随机图⽚【歌曲的图⽚】
QJsonArray QQMusic::randomPiction()
{
// 推荐⽂本 + 推荐图⽚路径
QVector<QString> vecImageName;
vecImageName<<"001.png"<<"003.png"<<"004.png"<<"005.png"<<"006.png"<<"007.png"
<<"008.png"<<"009.png"<<"010.png"<<"011.png"<<"012.png"<<"013.png"
<<"014.png"<<"015.png"<<"016.png"<<"017.png"<<"018.png"<<"019.png"
<<"020.png"<<"021.png"<<"022.png"<<"023.png"<<"024.png"<<"025.png"
<<"026.png"<<"027.png"<<"028.png"<<"029.png"<<"030.png"<<"031.png"
<<"032.png"<<"033.png"<<"034.png"<<"035.png"<<"036.png"<<"037.png"
<<"038.png"<<"039.png";
std::random_shuffle(vecImageName.begin(), vecImageName.end());
// 001.png
// path: ":/images/rec/"+vecImageName[i];
// text: "推荐-001"
QJsonArray objArray;
for(int i = 0; i < vecImageName.size(); i++)
{
QJsonObject obj;
obj.insert("path", ":/images/rec/"+vecImageName[i]);
// arg(i, 3, 10, QCchar('0'))
// i:要放⼊%1位置的数据
// 3: 三位数
// 10:表⽰⼗进制数
// QChar('0'):数字不够三位,前⾯⽤字符'0'填充
QString strText = QString("推荐-%1").arg(i + 1, 3, 10, QChar('0'));
obj.insert("text", strText);
objArray.append(obj);
}
return objArray;
}
recBox中添加元素
由于recPage页面中有两个RecBox控件,上⾯的RecBox为⼀行四列,下⽅的RecBox为2行四列,因此在RecBox类中增加以下成员变量
cpp
// RecBox.h 新增
#include <QJsonArray>
public:
void initRecBoxUi(QJsonArray data, int row);
private:
int row; // 记录当前RecBox实际总⾏数
int col; // 记录当前RecBox实际每⾏有⼏个元素
QJsonArray imageList; // 保存界⾯上的图⽚, ⾥⾯实际为key、value键值对
RecBox的构造函数中,将row和col默认设置为1和4,count需要具体来计算
cpp
RecBox::RecBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::RecBox),
row(1),
col(4)
{
ui->setupUi(this);
}
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
// 如果是两⾏,说明当前RecBox是主界⾯上的supplyMusicBox
if(2 == row)
{
this->row = row;
this->col = 8;
}
else
{
// 否则:只有⼀⾏,为主界⾯上recMusicBox
ui->recBoxBottom->hide();
}
// 图⽚保存起来
imageList = data;
// 往RecBox中添加图⽚
createRecItem();
}
void RecBox::createRecBoxItem()
{
// 创建RecBoxItem对象,往RecBox中添加
// col
for(int i = 0; i < col; ++i)
{
RecBoxItem* item = new RecBoxItem();
// 设置⾳乐图⽚与对应⽂本
QJsonObject obj = imageList[i].toObject();
item->setRecText(obj.value("text").toString());
item->setRecImage(obj.value("path").toString());
// 将RecBoxItem对象添加到RecBox中
ui->recListUpHLayout->addWidget(item);
}
}
运行程序可以看到:
上⾯RecBox正确,recListUpHLayout中添加了4个RecBoxItem元素并显示出来,recListDownHLayout被隐藏了,而下⾯的RecBox中内容不对,对于下⽅RecBox,期望recListUpHLayout中显示添加4个RecBoxItem,recListDownHLayout中显示添加4个RecBoxItem,而上述代码往RecBox中添加RecBoxItem时没有添加任何限制
createRecBoxItem()函数修改如下:
cpp
void RecBox::createRecBoxItem()
{
// 添加之前先删除RecBox中的元素
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->setRecText(obj.value("text").toString());
item->setRecImage(obj.value("path").toString());
// recMusicBox:col为4,元素添加到ui->recListUpHLayout中
// supplyMuscBox: col为8, ui->recListUpHLayout添加4个,ui->recListDownHLayout添加4个
// 即supplyMuscBox上下两⾏都要添加
// 如果是recMusicBox:row为1,只能执⾏else,所有4个RecBoxItem都添加到ui->recListUpHLayout中
// 如果是supplyMuscBox:row为2,col为8,col/2结果为4,i为0 1 2 3时,元素添加到ui->recListDownHLayout中
// i为4 5 6 7时,元素添加到ui->recListUpHLayout中
if(row == 2 && index++ >= col / 2)
{
ui->recListDownHLayout->addWidget(item);
}
else
{
ui->recListUpHLayout->addWidget(item);
}
}
}
RecBox中btUp和btDown按钮clicked处理
添加槽函数
选中recbox.ui⽂件,分别选中btUp和btDown,右键单击弹出菜单选择转到槽,选中clicked确定, btUp和btDown的槽函数就添加好了
cpp
void RecBox::on_btUp_clicked()
{
// 点击btUp按钮,显⽰前4张图⽚,如果已经是第⼀张图⽚,循环从后往前显示
}
void RecBox::on_btDown_clicked()
{
// 点击btUp按钮,显⽰前8张图⽚,如果已经是第⼀张图⽚,循环从后往前显示
}
imageList中图片分组
- 假设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
cpp
// recbox.h 中新增
int currentIndex; // 标记当先显⽰第⼏组图⽚和推荐信息
int count; // 标记imageList中元素按照col分组总数
// recbox.cpp 中新增
void RecBox::initRecBoxUi(QJsonArray data, int row)
{
if(row == 2)
{
this->row = row;
col = 8;
}
else
{
ui->recListDown->hide(); // 隐藏第二行
}
imageList = data;
currentIndex = 0;
count = imageList.size() / col;
// 在RecBox控件中添加RecBoxItem
createRecBoxItem();
}
void RecBox::on_btUp_clicked()
{
// 点击btUp按钮,显⽰前⼀组图⽚,如果已经是第⼀组图⽚,显⽰最后⼀组
currentIndex--;
if(currentIndex < 0)
{
currentIndex = count - 1;
}
createRecBoxItem();
}
void RecBox::on_btDown_clicked()
{
// 点击btDown按钮,显⽰下⼀组图⽚,如果已经是最后⼀组图⽚,显⽰第0组
currentIndex++;
if(currentIndex >= count)
{
currentIndex = 0;
}
createRecBoxItem();
}
程序启动时图片随机显示
仔细观察发现,每次程序启动时,显⽰的图⽚都是相同的,这是因为random_shuffle在随机打乱元素时,需要设置随机数种⼦,否则默认使⽤的种⼦是相同的,就导致每次打乱的结果都是相同的,所以每次程序启动时RecBox中显⽰的内容都是相同的,因此在randomPiction()调⽤之前需要设置随机数种⼦
cpp
// QQMusic类的initUi函数中新增
void QQMusic::initUi()
{
// ...
// 本地下载BtForm动画默认显⽰
ui->local->showAnimal();
ui->stackedWidget->setCurrentIndex(4);
// 设置RecBox图⽚、⾏数
srand(time(NULL));
ui->recMusicBox->initRecBoxUi(randomPiction(), 1);
ui->supplyMuscBox->initRecBoxUi(randomPiction(), 2);
}
自定义CommonPage
CommonPage页面分析
我的⾳乐下的:喜欢、本地和下载、最近播放三个按钮表⾯上看对应三个Page⻚⾯,分析之后发现,这三个Page⻚⾯实际是雷同的,因此只需要定义⼀个⻚⾯CommonPage,将stackedWidget中这三个⻚⾯的类型提升为CommonPage即可
上图为本地⾳乐的Page⻚⾯,对⻚⾯拆解后,发现该⻚⾯可以分四部分:
- 页面说明,比如:本地⾳乐,该部分实际就是QLabel的提示说明;
- 正在播放音乐图⽚和播放全部按钮;
- ⾳乐列表中每个部分的文本提示,实际就是三个QLabel
- 本⻚⾯对应的⾳乐列表,即QListWidget
CommonPage页面布局
- 新增加⼀个设计界⾯,objectName修改为CommonPage,geometry的宽⾼修改为800*515
- 拖拽⼀个QLabel、两个Widget和⼀个ListView控件到CommonPage中,objectName从上往下依次修改为pageTittle、musicPlayBox、listLabelBox、pageMusicList,然后选中CommonPage点击垂直布局,将CommonPage的margin和Spacing修改为0。
- pageTittle的minimumSize和maximumSize的⾼度修改为30。musicPlayBox的minimumSize和maximumSize的⾼度修改为150。listLabelBox的minimumSize和maximumSize的⾼度修改为40。
- 将pageTittle的⽂本内容修改为"本地⾳乐"
- musicPlayBox中拖拽⼀个QLabel,objectName修改为musicImageLabel,minimumSize和maximumSize的宽度修改为150
- 拖拽⼀个Widget,objectName修改为playAll,minimumSize和maximumSize的宽度修改为120,在其内部拖拽⼀个PushButton和Vertical Space(即垂直弹簧),将按钮的objectName修改为playAllBtn,minimumSize和maximumSize的宽和⾼修改为100*30,⽂本内容修改为"播放全部",然后选中playAll点击垂直布局
- 拖拽⼀个HorizontalSpacer到CommonPage中,放在playAll之后,然后选中musicPlayBox,点击⽔平布局,将margin和spacing设置为0.
- listLabelBox中拖拽三个QLabel,内容依次修改为:歌曲名称、歌⼿名称、专辑名称,objectName从左往右依次修改为:musicNameLabel、musicSingerLabel、musicAlbumLabel,然后选中musicPlayBox,点击⽔平布局,将margin和spacing设置为0
- 选中List View,右键单击弹出菜单中选择"变形为",选择QListWidget
选中QQMusic⻚⾯,将stackedWidget中我喜欢、本地下载、最近播放对应的页面提升为CommonPage,页面就处理完成了。
cpp
#playAllBtn
{
background-color:rgb(227,227,227);
border-radius:14px;
}
#playAllBtn:hover
{
background-color:rgb(219,219,219);
}
CommonPage界⾯设置和显示
CommonPage⻚⾯是喜欢、本地和下载、最近播放三个界⾯的共同类型,因此该类需要提供设置: pageTittle和musicImageLabel的公共⽅法,将来在程序启动时完成三个界⾯信息的设置,因此CommonPage类需要添加⼀个public的setCommonPageUI函数
cpp
// 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);
}
界⾯设置的函数需要在程序启动时就完成好配置,即需要在QQMusic的initUi()函数中调⽤完成设置:
cpp
void Widget::initUi()
{
....
// 设置我喜欢、本地⾳乐、最近播放⻚⾯
ui->likePage->setCommonPageUI("我喜欢", ":/images/ilikebg.png");
ui->localPage->setCommonPageUI("本地⾳乐", ":/images/localbg.png");
ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");
}
自定义ListItemBox
ListItemBox⻚⾯分析
CommonPage⻚⾯创建好之后,等⾳乐加载到程序之后,就可以将⾳乐信息往CommonPage的pageMusicList中显⽰了
上图每⾏都是QListWidget中的⼀个元素,每个元素中包含多个控件:
- 收藏图标,即QLabel
- 歌曲名称,即QLabel
- VIP和SQ,VIP即收费会员专享,SQ为⽆损⾳乐,也是两个QLabel
- 歌⼿名称,即QLabel
- ⾳乐专辑名称,即QLabel
此处,需要将上述所有QLabel组合在⼀起,作为⼀个独⽴的控件,添加到QListWidget中,因此该控件也需要⾃定义。
ListItemBox⻚⾯布局
- 添加⼀个设计师界⾯,objectName为ListItemBox,geometry的宽度和⾼度修改为800*46
- 拖三个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,然后选中musicNameBox点击⽔平布局,将musicSingerBox的margin和spacing修改为0
- 拖拽⼀个QLabel到albumBox中,objectName修改为albumNameLabel,然后选中albumBox点击⽔平布局,将musicSingerBox的margin和spacing修改为0
|----------|---------------------------------------------------------------------------------------|
| likeBtn | cpp #likeBtn { border:none; }
|
| VIPLabel | cpp #VIPLabel { border: 1px solid #1ECD96; color:#1ECD96; border-radius:3px; }
|
| SQLabel | cpp #SQLabel { border: 1px solid #FF6600; color:#FF6600; border-radius:3px; }
|
ListItemBox显示测试
ListItemBox将来要添加到CommonPage⻚⾯中的QListWidget中,因此在CommonPage类的初始化⽅法中添加如下代码:
cpp
#include "listitembox.h"
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效果
cpp
// listitembox.h 新增
protected:
void enterEvent(QEvent *event);
void leaveEvent(QEvent *event);
// listitembox.cpp 新增
void ListItemBox::enterEvent(QEvent *event)
{
(void)event;
setStyleSheet("background-color:#EFEFEF");
}
void ListItemBox::leaveEvent(QEvent *event)
{
(void)event;
setStyleSheet("");
}
自定义MusicSlider
由于QT内置的HorizontalSlider(⽔平滑竿)不是很好看,该控件也采⽤⾃定义。该控件⽐较简单,实际就是两个QFrame嵌套起来的
- 添加⼀个设计师界⾯,objectName修改为MusicSlider,geometry修改为800*20。
- 拖拽⼀个QFrame,objectName修改为inLine,geometry修改为[(0,8),800*4]。
- 拖拽⼀个QFrame,objectName修改为outLine,geometry修改为[(0,8),400*4]。
- 选中MusicSlider,点击⽔平布局。
inLine和outLine的样式设置如下:
|----------|-------------------------------------------------------------|
| #inLine | cpp #inLine { background-color:rgb(221, 221, 221); }
|
| #outLine | cpp #outLine { background-color:rgb(0, 0, 0); }
|
打开QQMusic.ui,选中progressBar清除之前样式,将progressBar提升为MusicSlider,运⾏程序就能看到效果
自定义VolumeTool
VolumeTool控件分析
⾳量调节控件本来也可以使⽤Qt内置的垂直滑杆来代替,只是垂直滑杆不好看,因此也⾃定义
- 内部为类似MusicSlider控件+⼩圆球,圆球实际为⼀个QPushButton
- ⾳量⼤⼩⽂本显⽰,实际为QLabel
- QPushButton,点击之后在静⾳和取消静⾳切换
- ⼀个倒三⻆,Qt未提供三⻆控件,该控件需要⼿动绘制,⽤来提⽰是播放控制区那个按钮按下的
VolumeTool界面布局
- ⽣成⼀个QT设计师界⾯,objectName命名为VolumeTool,geometry的宽⾼修改为80*350
- 拖拽⼀个Widget到VolumeTool中,objectName修改为volumeWidget,geometry修改为:[(10,10),60*300]
- 拖拽⼀个QPushButton到volumeWidget,objectName修改为silenceBtn,mimimumSize和maximumSize的宽⾼修改为60*45
- 拖拽⼀个QLabel到volumeWidget,objectName修改为volumeRatio,mimimumSize和maximumSize的⾼修改为30,QLabel的alignment属性修改为⽔平和垂直居中
- 拖拽⼀个QWidget到volumeWidget,objectName修改为sliderBox。geometry修改为:[(0,0), 60*225]
sliderBox内部:
- 拖拽⼀个QFrame,objectName修改为inSlider,geometry修改为[(28,25),4*180]。
- 拖拽⼀个QFrame,objectName修改为outSlider,geometry修改为[(28,25),4*180]。
- 拖拽⼀个QPushButton,objectName修改为sliderBtn,geometry修改为[(23,18),14*14], mimimumSize和maximumSize的宽⾼14*14
|---------------|--------------------------------------------------------------------------------------------------|
| #volumeWidget | cpp #volumeWidget { background-color:rgb(255,255,255); border-radius:7px; }
|
| #slienceBtn | cpp #silenceBtn { border:none; } #silenceBtn:hover { background-color:rgb(234,234,234); }
|
| #inSlider | cpp #inLine { background-color:rgb(236,236,236); }
|
| #outSlider | cpp #outLine { background-color:rgb(0,204,101); }
|
| sliderBtn | cpp #silderBtn { background-color:rgb(0,204,101); border-radius:7px; }
|
静⾳底下的空缺⽤来绘制三⻆
界面设置
该控件属于弹出窗⼝,即点击了主界⾯的⾳量调节按钮后,才需要弹出该界⾯,点击其他位置该界⾯⾃动隐藏。因此在窗⼝创建时,需要设置窗⼝为⽆边框以及为弹出窗口
cpp
// VolumeTool.cpp 的构造函数中添加如下代码
#include <QGraphicsDropShadowEffect>
VolumeTool::VolumeTool(QWidget *parent) :
QWidget(parent),
ui(new Ui::VolumeTool)
{
ui->setupUi(this);
// 弹出音量条件界面
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
// 将Widget多余边界隐藏
setAttribute(Qt::WA_TranslucentBackground);
// 窗口自定义阴影效果
QGraphicsDropShadowEffect * shadowsEffect = new QGraphicsDropShadowEffect(this);
shadowsEffect->setColor("#646464");
shadowsEffect->setBlurRadius(14);
shadowsEffect->setOffset(0, 0);
this->setGraphicsEffect(shadowsEffect);
// 设置音量静音按钮图标
ui->silenceBtn->setIcon(QIcon(":/images/volume.png"));
// 设置默认音量百分比
ui->volumeRatio->setText("20%");
// 中心坐标
ui->outLine->setGeometry(rect.x(), 180 - 36 + 25, rect.width(), 36);
// 定位按钮位置
ui->silderBtn->move(ui->silderBtn->x(), ui->outLine->y() - ui->silderBtn->y() / 2);
}
界⾯创建及弹出
⾳量调节属于主界⾯上元素,因此在QQMusic类中需要添加VolumeTool的对象,在initUi中new该类的对象
主界⾯中⾳量调节按钮添加clicked槽函数
cpp
// qqmusic.h中新增
#include "volumetool.h"
VolumeTool* volumeTool;
// qqmusic.cpp中新增
void QQMusic::initUi()
{
// ...
// 创建⾳量调节窗⼝对象并挂到对象树
volumeTool = new VolumeTool(this);
}
void QQMusic::on_volume_clicked()
{
// 先要调整窗⼝的显⽰位置,否则该窗⼝在主窗⼝的左上⻆
// 1. 获取该按钮左上⻆的图标
QPoint point = ui->volume->mapToGlobal(QPoint(0,0));
// 2. 计算volume窗⼝的左上⻆位置
// 让该窗⼝显⽰在⿏标点击的正上⽅
// ⿏标位置:减去窗⼝宽度的⼀半,以及⾼度恰巧就是窗⼝的左上⻆
QPoint volumeLeftTop = point - QPoint(volumeTool->width()/2, volumeTool->height());
// 微调窗⼝位置
volumeLeftTop.setY(volumeLeftTop.y()+30);
volumeLeftTop.setX(volumeLeftTop.x()+15);
// 3. 移动窗⼝位置
volumeTool->move(volumeLeftTop);
// 4. 将窗⼝显⽰出来
volumeTool->show();
}
绘制三⻆
由于Qt中并未给出三⻆控件,因此三⻆需要⼿动绘制,故在VolumeTool类中重写paintEvent事件函数
cpp
// volumetool.h中新增
void paintEvent(QPaintEvent *event);
// volumetool.cpp中新增
#include <QPainter>
void VolumeTool::paintEvent(QPaintEvent *event)
{
(void)event;
// 绘制volumerTool界面下的倒三角
QPainter painter(this);
// 设置画笔
painter.setPen(Qt::NoPen);
// 设置画刷
painter.setBrush(::QBrush(Qt::white));
// 绘制三角形 定位坐标系
QPolygon polygon;
QPoint a(10, 300 + 5);
QPoint b(10 + 60, 300 + 5);
QPoint c(10 + 30, 300 + 20);
polygon.append(a);
polygon.append(b);
polygon.append(c);
painter.drawPolygon(polygon);
}
音乐管理
界面处理好之后,现在就需要将⾳乐⽂件加载到程序然后显示在界⾯上,待后续播放操作
音乐加载
QQMusic类中给addLocal添加槽函数
⾳乐⽂件在磁盘中,可以借助QFileDialog类完成⾳乐⽂件加载。QFileDialog类中函数介绍:
cpp
构造函数:
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)是⼀种互联⽹标准,⽤于表⽰⽂档、⽂件或
字节流的性质和格式*/
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();
打开函数实现如下:
cpp
// qqmusic.cpp 中新增
#include <QDir>
#include <QFileDialog>
void QQMusic::on_addLocal_clicked()
{
// 1. 创建⼀个⽂件对话框
QFileDialog fileDialog(this);
fileDialog.setWindowTitle("添加本地⾳乐");
// 2. 创建⼀个打开格式的⽂件对话框
fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
// 3. 设置对话框模式
// 只能选择⽂件,并且⼀次性可以选择多个存在的⽂件
fileDialog.setFileMode(QFileDialog::ExistingFiles);
// 4. 设置对话框的MIME过滤器
QStringList mimeList;
mimeList<<"application/octet-stream";
fileDialog.setMimeTypeFilters(mimeList);
// 5. 设置对话框默认的打开路径,设置⽬录为当前⼯程所在⽬录
QDir dir(QDir::currentPath());
dir.cdUp();
QString musicPath = dir.path()+"/QQMusic/musics/";
fileDialog.setDirectory(musicPath);
// 6. 显⽰对话框,并接收返回值
// 模态对话框, exec内部是死循环处理
if(fileDialog.exec() == QFileDialog::Accepted)
{
// 切换到本地⾳乐界⾯,因为加载完的⾳乐需要在本地⾳乐界⾯显⽰
ui->stackedWidget->setCurrentIndex(4);
// 获取对话框的返回值
QList<QUrl> urls = fileDialog.selectedUrls();
// 拿到歌曲⽂件后,将歌曲⽂件交由musicList进⾏管理
}
}
MusicList类
添加C++类MusicList
将来添加到播放器中的⾳乐⽐较多,可借助⼀个类对所有的⾳乐进⾏管理。添加新C++类与添加设计师界⾯类似:
歌曲对象存储
每⾸⾳乐⽂件,将来需要获取其内部的歌曲名称、歌⼿、⾳乐专辑、歌曲时⻓等信息,因此在MusicList类中,将所有的歌曲⽂件以Music对象方式管理起来。
QQMusic中,通过QFileDialog将⼀组⾳乐⽂件的url获取到之后,可以交给MusicList类来管理
但是QQMusic加载的⼆进制⽂件不⼀定全部都是⾳乐⽂件,因此MusicList类中需要对⽂件的MIME类型再次检测,以筛选出真正的⾳乐⽂件。
QMimeDatabase类是Qt中主要⽤于处理⽂件的MIME类型,经常⽤于:
- ⽂件类型识别
- ⽂件过滤
- 多媒体⽂件处理
- ⽂件导⼊导出
- ⽂件管理器
该类中的mimeTypeForFile函数可⽤于获取给定⽂件的MIME类型
cpp
// 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格式的歌曲⽂件
// 上述三种⾳乐格式⽂件,Qt的QMediaPlayer类都是⽀持的
对于歌曲文件:
- audio/mpeg:适⽤于mp3格式的⾳乐⽂件
- audio/flac:⽆损压缩的⾳频⽂件,不会破坏任何原有的⾳频信息
- audio/wav:表⽰wav格式的歌曲⽂件
上述歌曲⽂件格式,Qt的QMediaPlayer类都是⽀持的
cpp
// musiclist.h 中新增
#include <QVector>
QVector<Music> musicList; // Music类是⾃定义的C++类,描述歌曲相关信息
// 将QQMusic⻚⾯中读取到的⾳乐⽂件,检测是⾳乐⽂件后添加到musicList中
void addMusicByUrl(const QList<QUrl>& urls);
// musiclist.cpp中新增
void MusicList::addMusicByUrl(const QList<QUrl> &urls)
{
for(auto musicUrl : urls)
{
// 由于添加进来的⽂件不⼀定是歌曲⽂件,因此需要再次筛选出⾳乐⽂件
QMimeDatabase db;
QMimeType mime = db.mimeTypeForFile(musicUrl.toLocalFile());
if(mime.name() != "audio/mpeg" && mime.name() != "audio/flac")
{
continue;
}
// 如果是⾳乐⽂件,加⼊歌曲列表
musicList.push_back(musicUrl);
}
}
Music类
Music类介绍
该⽤来描述⼀个⾳乐⽂件,⽐如:⾳乐名称、歌⼿名称、专辑名称、⾳乐持续时⻓,当在界⾯上点击收藏之后,⾳乐会被标记为喜欢,播放之后需要标记为历史记录。因此该类中⾄少需要以下成员:
cpp
// music.h中新增
#include <QUrl>
#include <QString>
class Music
{
public:
Music();
Music(const QUrl& url);
void setIsLike(bool isLike);
void setIsHistory(bool isHistory);
void setMusicName(const QString& musicName);
void setSingerName(const QString& singerName);
void setAlbumName(const QString& albumName);
void setDuration(const qint64 duration);
void setMusicUrl(const QUrl& url);
void setMusicId(const QString& musicId);
bool getIsLike();
bool getIsHistory();
QString getMusicName();
QString getSingerName();
QString getAlbumName();
qint64 getDuration();
QUrl getMusicUrl();
QString getMusicId();
private:
bool isLike; // 标记⾳乐是否为我喜欢
bool isHistory; // 标记⾳乐是否播放过
// ⾳乐的基本信息有:歌曲名称、歌⼿名称、专辑名称、总时⻓
QString musicName;
QString singerName;
QString albumName;
qint64 duration; // ⾳乐的持续时⻓,即播放总的时⻓
// 为了标记歌曲的唯⼀性,给歌曲设置id
// 磁盘上的歌曲⽂件经常删除或者修改位置,导致播放时找不到⽂件,或者重复添加
// 此处⽤musicId来维护播放列表中⾳乐的唯⼀性
QString musicId;
QUrl musicUrl; // ⾳乐在磁盘中的位置
};
Music::Music()
: isLike(false)
, isHistory(false)
{}
void Music::setIsLike(bool isLike)
{
this->isLike = isLike;
}
void Music::setIsHistory(bool isHistory)
{
this->isHistory = isHistory;
}
void Music::setMusicName(const QString &musicName)
{
this->musicName = musicName;
}
void Music::setSingerName(const QString &singerName)
{
this->singerName = singerName;
}
void Music::setAlbumName(const QString &albumName)
{
this->albumName = albumName;
}
void Music::setDuration(const qint64 duration)
{
this->duration = duration;
}
void Music::setMusicUrl(const QUrl &url)
{
this->musicUrl = url;
}
void Music::setMusicId(const QString &musicId)
{
this->musicId = musicId;
}
bool Music::getIsLike()
{
return isLike;
}
bool Music::getIsHistory()
{
return isHistory;
}
QString Music::getMusicName()
{
return musicName;
}
QString Music::getSingerName()
{
return singerName;
}
QString Music::getAlbumName()
{
return albumName;
}
qint64 Music::getDuration()
{
return duration;
}
QUrl Music::getMusicUrl()
{
return musicUrl;
}
QString Music::getMusicId()
{
return musicId;
}
另外,该类还需要添加⼀个带有歌曲⽂件路径的构造函数,当给定有效⾳乐⽂件后,Music类需要负责将该⾳乐⽂件的元数据解析出来。
为了保证Music对象的唯⼀性,给每个Music对象设置⼀个UUID。
UUID,即通⽤唯⼀识别码(Universally Unique Identifier),确保在分布式系统中每个元素都有唯⼀的标识
UUID由⼀组32位数的16进制数字组成,形式为8-4-4-4-12的32个字符,⽐如:"550e8400-e29b- 41d4-a716-446655440000"
在Music对象查找和更新时,可以已通过对⽐UUID,来保证Music对象的唯⼀性
Qt中QUuid类可⽣成UUID。
cpp
Music::Music(const QUrl &url)
: isLike(false)
, isHistory(false)
, musicUrl(url)
{
musicId = QUuid::createUuid().toString();
}
解析⾳乐⽂件元数据
对于每⾸歌曲,将来在界⾯上需要显⽰出:歌曲名称、歌⼿、专辑名称,在播放时还需要拿到歌曲总时⻓,因此在构造⾳乐对象时,就需要将上述信息解析出来
歌曲元数据解析,需要⽤到 QMediaPlayer ,该类也是⽤来进⾏歌曲播放的类,后续在播放⾳乐位置详细介绍
QMediaPlayer类中的setMedia()函数
cpp
// 功能:设置要播放的媒体源,媒体数据从中读取
// media: 要播放的媒体内容,⽐如⼀个视频或⾳频⽂件,该类提供了⼀个QUrl格式的单参构造
void setMedia(const QMediaContent &media, QIODevice *stream = nullptr)
该函数执⾏后⽴即返回,不会等待媒体加载完成,也不检查错误,如果在媒体加载时发⽣错误,会触发mediaStatusChanged和error信号
由于加载媒体⽂件需要时间,可以通过QMediaObject类中的isMetaDataAvailable()⽅法检测媒体数据是否可用
cpp
// 检测媒体源是否有效,如果是有效的返回true,否则返回false
bool isMetaDataAvailable() const;
媒体元数据加载成功之后,可以通过QMediaObject类的metaData函数获取指定的媒体数据:
cpp
// 返回要获取的媒体数据key的值
QVariant QMediaObject::metaData(const QString &key) const
本⽂需要获取媒体的:标题、作者、专辑、持续时⻓
|------------|-------------|-------------|
| valye | description | type |
| Title | 媒体的标题 | QString |
| Auther | 媒体的作者 | QStringLIst |
| AlbumTitle | 媒体所属专辑名称 | QString |
| Duration | 媒体的播放时长 | qint64 |
有些媒体中媒体数据可能不全,即有些媒体数据获取不到,比如盗版歌曲
使⽤QMediaPlayer媒体播放类时,需要在QQMusic.pro项目工程文件中添加媒体模块multimedia ,该模块主要⽤来播放各种⾳频视频⽂件等,该模块中提供了很多类
|-------------------------------------------------|------------------|
| 媒体模块中的主要类 | 功能 |
| QMediaplayer,QMediaPlayList | 播放压缩音频(MP3、AAC等) |
| QSoundEffer,QSound | 播放音效文件(WAV文件) |
| QAudioOutput | 播放低延迟的音频 |
| QAudioInput | 访问原始音频输入数据 |
| QAudioRecorder | 录制编码的音频数据 |
| QAudioDeviceInfo | 发现音频设备 |
| QMediaPlayer,QvideoWidget,QGraphics VideoItem | 视频播放 |
| QMediaPlayer,QvideoFrame,QAbstract VideoSurface | 视频处理 |
| QCamera,QVideoWidget,QGraphicsVideoItem | 摄像头取景框 |
| QCamera,QAbstractVideoSurface,QVideoFrame | 取景框预览处理 |
| QCamera,QCameralmage Capture | 摄像头拍照 |
| QCamera,QMediaRecorder | 摄像头录像 |
| ORadioTuner,ORadioData | 收听数字广播 |
cpp
QT += core gui multimedia
添加完成之后,重新将项目构建⼀下,否则Qt create可能识别不过来
⾳乐⽂件的meta数据解析如下:
cpp
// music.h 中新增
private:
void parseMediaMetaData();
// music.cpp 中新增
#include <QMediaPlayer>
#include <QCoreApplication>
#include <QUuid>
void Music::parseMediaMetaData()
{
// 解析时候需要读取歌曲数据,读取歌曲⽂件需要⽤到QMediaPlayer类
QMediaPlayer player;
player.setMedia(musicUrl);
// 媒体元数据解析需要时间,只有等待解析完成之后,才能提取⾳乐信息,此处循环等待
// 循环等待时:主界⾯消息循环就⽆法处理了,因此需要在等待解析期间,让消息循环继续处理
while(!player.isMetaDataAvailable())
{
QCoreApplication::processEvents();
}
// 解析媒体元数据结束,提取元数据信息
if(player.isMetaDataAvailable())
{
musicName = player.metaData("Title").toString();
singerName = player.metaData("Author").toString();
albumName = player.metaData("AlbumTitle").toString();
duratioplayer. Durationtion();
if(musicName.isEmpty())
{
musicName = "歌曲未知";
}
if(singerName.isEmpty())
{
singerName = "歌⼿未知";
}
if(albumName.isEmpty())
{
albumName = "专辑名未知";
}
qDebug()<<musicName<<" "<<singerName<<" "<<albumName<<" "<<duration;
}
}
// 该函数需要在Music的构造函数中调⽤,当创建⾳乐对象时,顺便完成歌曲⽂件的加载
Music::Music(const QUrl &url)
: isLike(false)
, isHistory(false)
, musicUrl(url)
{
musicId = QUuid::createUuid().toString();
parseMediaMetaData();
}
Music数据保存
通过QFileDialog将音乐从本地磁盘加载到程序中后,拿到的是所有⾳乐⽂件的QUrl,而在程序中需要的是经过元数据解析之后的Music对象,并且Music对象需要管理起来,此时就可以采用MusicList类对解析之后的Music对象进⾏管理,QQMusic类中只需要保存MusicList的对象,就可以让qqMusic.ui界⾯中CommonPage对象完成Music信息往界⾯更新。
cpp
// qqmusic.h 新增
#include "musiclist.h"
MusicList musicList;
// qqmusic.cpp
void QQMusic::on_addLocal_clicked()
{
// ....
// 6. 显⽰对话框,并接收返回值
// 模态对话框, exec内部是死循环处理
if(fileDialog.exec() == QFileDialog::Accepted)
{
// 切换到本地⾳乐界⾯,因为加载完的⾳乐需要在本地⾳乐界⾯显⽰
ui->stackedWidget->setCurrentIndex(4);
// 获取对话框的返回值
QList<QUrl> urls = fileDialog.selectedUrls();
// 拿到歌曲⽂件后,将歌曲⽂件交由musicList进⾏管理
musicList.addMusicByUrl(urls);
// 更新到本地⾳乐列表
ui->localPage->refresh(musicList);
}
}
由于添加的是本地音乐,因此⾳乐信息需要由ui->localPage更新到其内部的QListWidget中
音乐分类
QQMusic中,有三个显示歌曲信息的页面:
- likePage:管理和显示点击爱心后收藏的歌曲
- localPage:管理和显示本地加载的歌曲
- recentPage:管理和显示历史播放过的歌曲
这三个⻚⾯的类型都是CommonPage,每个页面应该维护自己页面中的歌曲。因此CommonPage类中需要新增:
cpp
// commonpage.h中新增
// 区分不同page⻚⾯
enum PageType
{
LIKE_PAGE, // 我喜欢⻚⾯
LOCAL_PAGE, // 本地下载⻚⾯
HISTORY_PAGE // 最近播放⻚⾯
};
class CommonForm : public QWidget
{
// 新增成员函数
public:
void setMusicListType(PageType pageType);
// 新增成员变量
private:
// 歌单列表
QVector<QString> musicListOfPage; // 具体某个⻚⾯的⾳乐,将来只需要存储⾳乐的id即可
PageType pageType; // 标记属于likePage、localPage、recentPage哪个⻚⾯
};
// commonpage.cpp中新增:
void CommonPage::setMusicListType(PageType pageType)
{
this->pageType = pageType;
}
// qqmusic.cpp中新增:
void initUi()
{
// ...
// 设置CommonPage的信息
ui->likePage->setMusicListType(PageType::LIKE_PAGE);
ui->likePage->setCommonPageUI("我喜欢", ":/images/ilikebg.png");
ui->localPage->setMusicListType(PageType::LOCAL_PAGE);
ui->localPage->setCommonPageUI("本地⾳乐", ":/images/localbg.png");
ui->recentPage->setMusicListType(PageType::HISTORY_PAGE);
ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");
}
QQMusic中,点击addLocal(本地加载)按钮后,会通过其musicList成员变量,将music添加到musicList中管理,在添加过程中,每个歌曲会对应⼀个Music对象,Music对象在构造时,会完成歌曲文件的加载,顺便完成歌曲名称、作者、专辑名称等元数据的解析。⼀切准备就绪之后,每个CommonPage页面,通过QQMusic的musicList分离出自己页面的歌曲,保存在musicListOfPage中。
cpp
// commonpage.h中新增:
#include "musiclist.h"
private:
void addMusicToMusicPage(MusicList &musicList);
// commonpage.cpp 中新增:
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
// 将旧内容清空
musicListOfPage.clear();
for(auto& music : musicList)
{
switch(musicListType)
{
case LOCAL_LIST:
musicListOfPage.push_back(music.getMusicId());
break;
case LIKE_LIST:
{
if(music.getIsLike())
{
musicListOfPage.push_back(music.getMusicId());
}
break;
}
case HOSTORY_LIST:
{
if(music.getIsHistory())
{
musicListOfPage.push_back(music.getMusicId());
break;
}
}
default:
break;
}
}
}
由于musicList所属类,并不能直接⽀持范围for,因此需要在MusicList类中新增:
cpp
// musiclist.h中新增:
typedef typename QVector<Music>::iterator iterator;
iterator begin();
iterator end();
// musiclist.cpp中新增:
iterator MusicList::begin()
{
return musicList.begin();
}
iterator MusicList::end()
{
return musicList.end();
}
更新Music信息到ComonPage界面
歌曲分类完成之后,歌曲信息就可以更新到CommonPage页面了
更新步骤:
- 调⽤addMusicIdPageFromMusicList函数,从musicList中添加当前页面的歌曲
- 遍历musicListOfPage,拿到每⾸⾳乐后先检查其是否在,存在则添加
- 界⾯上需要更新每首歌曲的:歌曲名称、作者、专辑名称,而commonPage中只保存了歌曲的musicId,因此需要在MusicList中增加通过musicID查找Music对象的⽅法
cpp
// commonpage.h中新增
void reFresh(MusicList& musicList);
// commonpage.cpp 中新增:
void CommonPage::reFresh(MusicList &musicList)
{
// 添加前需要清空列表内已存在的元素
ui->PageMusicList->clear();
// 添加歌曲
addMusicToMusicPage(musicList);
for(auto musicId : musicOfPage)
{
auto it = musicList.findMusicByMusicId(musicId);
// 没有找到
if(it == musicList.end())
continue;
// 将ListBoxItem对象放到pageMusicList中
ListItemBox* listItemBox = new ListItemBox(this);
// 歌曲名称,作者,专辑更新到界面
listItemBox->setMusicName(it->getMusicName());
listItemBox->setMusicSinger(it->getMusicSinger());
listItemBox->setMusicAlbum(it->getMusicAlbumn());
listItemBox->setLikeMusic(it->getIsLike());
QListWidgetItem* item = new QListWidgetItem(ui->PageMusicList);
item->setSizeHint(QSize(listItemBox->width(), listItemBox->height()));
ui->PageMusicList->setItemWidget(item, listItemBox);
// 拦截收藏时触发的信号
connect(listItemBox, &ListItemBox::setIsLike, this, [=](bool isLike){
// 更新歌曲库状态,通知qqmusic,让其中likePage,localPage,recentPage更新歌曲库中信息
// 给qqmusic发送信号
emit updataLikeMusic(isLike, it->getMusicId());
});
}
// 窗口重绘paintEvent
repaint(); // 立马响应
}
// musiclist.h中新增
iterator findMusicById(const QString& musicId);
// musiclist.cpp中新增
iterator MusicList::findMusicByMusicId(const QString &musicId)
{
for(auto it = begin(); it != end(); ++it)
{
if(it->getMusicId() == musicId)
{
return it;
}
}
return end();
}
将歌曲名称、作者、专辑名称、喜欢图片等往ListBoxItem界⾯中更新时,需要ListBoxItem提供对应的set方法,因此需要在ListItemBox类中新增:
cpp
// listitembox.h中新增:
public:
void setMusicName(const QString& name);
void setSinger(const QString& singer);
void setAlbumName(const QString& albumName);
void setLikeIcon(bool like);
private:
bool isLike;
// listitembox.cpp中新增:
ListItemBox::ListItemBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::ListItemBox),
isLike(false)
{
ui->setupUi(this);
}
void ListItemBox::setMusicName(const QString &musicName)
{
ui->musicNameLabel->setText(musicName);
}
void ListItemBox::setMusicSinger(const QString &musicSinger)
{
ui->musicSingerLabel->setText(musicSinger);
}
void ListItemBox::setAlbumName(const QString& albumName)
{
ui->musicAlbumLabel->setText(albumName);
}
void ListItemBox::setLikeMusic(bool isLike)
{
this->isLike = isLike;
if(isLike)
{
ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
}
else
{
ui->likeBtn->setIcon(QIcon(":images/like_3.png"));
}
}
更新音乐信息到界面的函数处理完成之后,需要在QQMusic的addLocal槽函数最后调用
cpp
// qqmusic.cpp中新增:
void QQMusic::onAddLocalClick()
{
// ...
// 6. 显⽰对话框,并接收返回值
// 模态对话框, exec内部是死循环处理
if(fileDialog.exec() == QFileDialog::Accepted)
{
// 切换到本地⾳乐界⾯,因为加载完的⾳乐需要在本地⾳乐界⾯显⽰
ui->stackedWidget->setCurrentIndex(4);
// 获取对话框的返回值
QList<QUrl> urls = fileDialog.selectedUrls();
// 注意:后序需要将⾳乐信息添加到数据库,否则每次打开是都需要添加⾳乐太⿇烦了
musicList.addMusicByUrl(urls);
// 更新到本地⾳乐列表
ui->localPage->refresh(musicList);
}
}
CommonPage显示不足处理
歌曲作者对齐处理
解析歌曲元数据时,有些歌曲文件中可能不存在歌曲名称、作者、歌曲专辑等,为了界面上显示出歌曲名称,从歌曲⽂件名中解析出歌曲名称和作者,这样解析出来的歌曲名称后面多⼀个空格,作者之前多⼀个空格,导致界面显示的时候歌手名称对不齐。因此在往界⾯设置之前,可以将名称前后的空格去除掉
QString类提供了⼀个 trimmed() 方法,专⻔⽤来去除字符串前后空白字符的
cpp
// music.cpp ⽂件修改
void Music::parseMediaMetaMusic()
{
if(musicName.isEmpty())
{
if(index != -1)
{
musicName = fileName.mid(0, index).trimmed();
}
else
{
musicName = fileName.mid(0, fileName.indexOf('.')).trimmed();
}
}
// Singer为空
if(musicSinger.isEmpty())
{
if(index != -1)
{
musicSinger = fileName.mid(index + 1, fileName.indexOf('.') - index - 1).trimmed();
}
else
{
musicSinger = "未知歌手";
}
}
if(musicAlbumn.isEmpty())
{
musicAlbumn = "未知专辑";
}
// ...
}
显示延迟问题
在CommonPage的 reFresh() 函数中,将ListItemBox设置好之后,更新到界面,有时候不会立马显示出来,等鼠标放置ListWidget上或者界面刷新的时候,才会显示出来。这是因为往界面更新元素的操作,没有引起窗体的重绘,导致不能实时显示出来,因此添加完元素之后,需要触发重绘事件,将元素及时绘制出来
cpp
// 该⽅法负责将歌曲信息更新到界⾯
void CommonPage::reFresh(MusicList &musicList)
{
// ...
// 该函数最后添加上repaint()函数调⽤
// repaint()会⽴即执⾏paintEvent(),不会等待事件队列的处理
// update()将⼀个paintEvent事件添加到事件队列中,等待稍后执⾏,即不会⽴即执⾏paintEvent。
repaint();
}
移除掉QListWidget的⽔平滚动条
⼀般歌曲名称、作者、专辑名称不会将ListItemBox沾满,为了界⾯好看,可以让CommonPage中的QListWidget控件去除掉⽔平滚动条
cpp
CommonPage::CommonPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::CommonPage)
{
ui->setupUi(this);
// 不要⽔平滚动条
ui->pageMusicList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}
QListWidget选中后背景色设置
QListWidget中ListItemBox选中之后,背景颜⾊和界⾯不是很搭,⽤如下QSS代码设置ListItemBox选中后的背景颜色
cpp
#pageMusicList::item:selected /*::item表⽰⼦控件,即ListItemBox :selected:
表⽰选中*/
{
background-color:#EFEFEF;
}
QListWidget的垂直滚动条美化
cpp
#PageMusicList::item:selected
{
background-color:rgb(230,230,230);
}
QScrollBar:vertical
{
border:none;
width:10px;
height:10px;
background-color:rgb(250,250,250);
margin: 0px 0px 0px 0px;
}
QScrollBar::handle:vertical
{
width:10px;
background-color:rgb(225,225,225);
border-radius:5px;
min-heigth:20px;
}
音乐收藏
喜欢图标处理
当CommonPage往界⾯更新Music信息时,也要根据Music的isLike属性更新对应的图标。因此ListItemBox需要根据
当点击喜欢按钮之后,要切换ListItemBox中的爱心图标。因此ListItemBox中添加设置bool类型isLike成员变量,以及setIsLike函数,在CommonPage添加Music信息到界⾯时,要能够设置爱心图标
cpp
// listItemBox.h 中新增
bool isLike;
void setLikeMusic(bool isLike);
// listItemBox.cp 中新增
ListItemBox::ListItemBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::ListItemBox),
isLike(false)
{
ui->setupUi(this);
}
void ListItemBox::setLikeMusic(bool isLike)
{
this->isLike = isLike;
if(isLike)
{
ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
}
else
{
ui->likeBtn->setIcon(QIcon(":images/like_3.png"));
}
}
点击喜欢按钮处理
当喜欢某首歌曲时,可以点击界⾯上爱心收藏该该歌曲。我喜欢按钮中应该有以下操作:
- 更新爱心图标
- 更新Music的喜欢属性,但ListItemBox并没有歌曲数据,所以只能发射信号,上其父元素CommonPage来处理
cpp
// listItemBox.h 中新增
public:
void onLikeBtnClicked(); // 按钮点击槽函数
signals:
void setIsLike(bool); // 通知更新歌曲数据信号
// ListItemBox.cpp 中新增
ListItemBox::ListItemBox(QWidget *parent) :
QWidget(parent),
ui(new Ui::ListItemBox),
isLike(false)
{
ui->setupUi(this);
// 关联槽函数 执行切换图片事件
connect(ui->likeBtn, &QPushButton::clicked, this, &ListItemBox::onLikeClicked);
}
void ListItemBox::onLikeClicked()
{
isLike = !isLike;
setLikeMusic(isLike);
// 通过发射信号来通知修改Music对象中isLike熟悉
emit setIsLike(isLike);
}
- CommonPage在往QListWidget中添加元素时,会创建⼀个个ListItemBox对象,每个对象将来都可能会发射setLikeMusic信号,因此在将ListItemBox添加完之后,CommonPage应该关联先该信号,将需要更新的的Music信息以及是否喜欢,同步给QQMusic
cpp
// commonpage.h中新增
signals:
void updateLikeMusic(bool isLike, QString musicId);
// commonpage.cpp中新增
// 该⽅法负责将歌曲信息更新到界面
void CommonPage::reFresh(MusicList &musicList)
{
// ...
for(auto musicId : musicOfPage)
{
// ...
QListWidgetItem* item = new QListWidgetItem(ui->pageMusicList);
item->setSizeHint(QSize(listItemBox->width(), listItemBox->height()));
ui->pageMusicList->setItemWidget(item, listItemBox);
// 接收ListItemBox发射的setLikeMusic信号
connect(listItemBox, &ListItemBox::setIsLike, this, [=](bool isLike){
emit updateLikeMusic(isLike, it->getMusicId());
});
}
ui->pageMusicList->repaint();
}
QQMusic收到CommonPage发射的updateLikePage信号后,通知其上的likePage、localPage、recentPage更新其界面的喜欢歌曲信息
cpp
// qqmusic.h 新增
void onUpdateLikeMusic(bool isLike, QString musicId); // 响应CommonPage发射updateLikeMusic信号
// qqmusic.cpp新增
void QQMusic::connectSignalAndSlots()
{
// ...
// 关联CommonPage发射的updateLikeMusic信号
connect(ui->likePage, &CommonPage::updateLikeMusic, this,&QQMusic::onUpdateLikeMusic);
connect(ui->localPage, &CommonPage::updateLikeMusic, this,&QQMusic::onUpdateLikeMusic);
connect(ui->recentPage, &CommonPage::updateLikeMusic, this,&QQMusic::onUpdateLikeMusic);
}
void QQMusic::onUpdateLikeMusic(bool isLike, QString musicId)
{
// 通过ID找到该音乐,修改状态
auto it = musiclist.findMusicByMusicId(musicId);
if(it != musiclist.end())
{
it->setIsLike(isLike);
}
// 更新页面歌曲列表
ui->page_like->reFresh(musiclist);
ui->page_local->reFresh(musiclist);
ui->page_recent->refresh(musiclist);
}
歌曲重复显示问题
当界⾯上歌曲数据更新之后,CommonPage往页面上更新其musicOfPage内容时,musicOfPage和界⾯中的QListWidget中已经有数据了,需要先将之前的内容清楚掉,否则就会重复。
cpp
// commonpage.cpp修改
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
musicOfPage.clear();
// ...
}
void CommonPage::reFresh(MusicList &musicList)
{
ui->pageMusicList->clear();
// ...
}
音乐播放控制
歌曲已经添加到程序并完成解析,解析的信息也更新到界面了,所有前置⼯作基本完成,接下来重点处理音乐播放,歌曲播放需要⽤到Qt提供的QMediaPlayer类和QMediaPlaylist类
QMediaPlayer类
QMediaPlayer类说明
MediaPlayer是Qt框架中⽤于⽀持各种⾳频和视频的播放,流媒体的播放,各种播放模式(单曲播放、列表播放、循环播放等),各种播放模式(播放、暂停、停⽌等),信号槽机制可以让用户在播放状态改变时进⾏所需控制。
使⽤时需要包含 #include <QMediaPlayer> 头⽂件,并且需要在.pro项目文件中添加媒体库,即: QT += multimedia ,将 multimedia 模块导⼊到⼯程中,就可以使用该模块中提供的媒体播放控制的相关类,比如:QMediaPlayer、QMediaPlayList等
属性和方法
枚举类型
QMediaPlayer::State枚举类型:
|-------------------------------|---------|------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMdiaPlayer::StoppedState | 0 | 播放停止状态 |
| QMdiaPlayer::PlayingState | 1 | 播放状态 |
| QMdiaPlayer::PausedState | 2 | 播放暂停状态 |
QMediaPlayer::Flag
|---------------------------------|---------|----------------------------------------------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMdiaPlayer::LowLatency | 0 | 播放未压缩的音频⽂件,播放表现为低时延,主要播放蜂鸣、手机铃声 |
| QMdiaPlayer::StreamPlayback | 1 | 播放给予QIODevice构建的媒体文件,QMediaPlayer或自动选择支持的流进行播放 |
| QMdiaPlayer::VideoSurface | 2 | 渲染视频到QAbstractVideoSurface输出 |
QMediaPlayer::Error
|--------------------------------------|---------|--------------------------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMdiaPlayer::NoError | 0 | 没有错误 |
| QMdiaPlayer::ResourceError | 1 | 媒体源无法解析 |
| QMdiaPlayer::FormatError | 2 | 媒体源格式不支持,可能会播放,但是没有音频和视频组件 |
| QMdiaPlayer::NetworkError | 3 | 网络错误 |
| QMdiaPlayer::AccessDeniedError | 4 | 没有媒体源访问权限 |
| QMdiaPlayer::ServiceMissingError | 5 | 没有有效的播放服务,无法继续播放 |
QMediaPlayer::MediaStatus
|-------------------------------------|---------|----------------------------------------------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMdiaPlayer::UnknownMediaStatus | 0 | 媒体的状态未被定义 |
| QMdiaPlayer::NoMedia | 1 | 没有媒体文件,player处于StoppedState |
| QMdiaPlayer::LoadingMedia | 2 | 媒体文件加载中,player可以处于任何状态 |
| QMdiaPlayer::LoadedMedia | 3 | 媒体文件已经加载,player处于StoppedState |
| QMdiaPlayer::StalledMedia | 4 | 媒体处于延迟或者暂时中断状态,player处于PlayingState或PauseState |
| QMdiaPlayer::BufferingMedia | 5 | 媒体正在缓冲数据,player处于PlayingState或PauseState |
| QMdiaPlayer::BufferedMedia | 6 | 媒体数据缓冲完成,player处于PlayingState或PauseState |
| QMdiaPlayer::EndOfMedia | 7 | 媒体结束,player处于StoppedState |
| QMdiaPlayer::InvalidMedia | 8 | 非法的媒体⽂件,player处于StoppedState |
常用属性
cpp
// 部分常⽤属性
const qint64 duration; // 保存媒体的总播放时间,单位为毫秒
const QMediaContent currentMedia; // 当前正在播放媒体的媒体内容
const QString error; // 最近⼀次错误信息
int volume; // 保存⾳量⼤⼩,范围在0~100之间
const bool audioAvailable; // ⾳频是否可⽤,audioAvailableChanged信号⽤于监听其状态
QMediaPlaylist* playtlist; // 播放列表
常用函数
cpp
qint64 duration() const; // 获取当前媒体的总时间
qint64 position() const; // 获取当前媒体的播放位置
int volume() const; // 获取播放⾳量⼤⼩
bool isMuted() const; // 检测是否静⾳
State state() const; // 获取当前媒体的播放状态
QMediaContent currentMedia() const; // 获取当前正在播放的媒体内容
QString errorString() const // 获取最近的⼀次错误
常用槽函数
cpp
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);
常用信号
cpp
void stateChanged(QMediaPlayer::State state); // 播放状态改变时发射该信号
void durationChanged(qint64 duration); // 播放时⻓改变时发射该状态
void positionChanged(qint64 position); // 播放位置改变时发射该状态
void volumeChanged(int volume); // ⾳量改变时发射该信号
void metaDataAvailableChanged(bool available); // 源数据改变发出
以上只列出了本次需要⽤到的属性、方法和槽函数,后续需要使用时请参考Qt帮助手册
QMediaPlaylist类
QMediaPlaylist类介绍
QMediaPlaylist 类提供了⼀种灵活而强⼤的方式管理媒体⽂件的播放列表。通过结合QMediaplayer,可以实现顺序播放、循环播放随机播放等多种播放模式,提升用户的媒体播放体验。该类提供了以下功能:
- 添加和删除媒体文件
- 播放模式设置(列表播放、随机播放、单曲循环)
- 控制播放列表(开始,停止,上⼀曲,下⼀曲)
- 获取和设置当前媒体文件 信号槽支持
若播放多个媒体文件,必须使用该类来管理媒体文件,将该列表设置到player上,就可实现更加灵活的播放支持
属性和方法
枚举类型
QMediaPlaylist::PlaybackMode
|---------------------------------------|---------|--------------------|
| 枚举状态名称 | 枚举值 | 说明 |
| QMediaPlaylist::CurrentItemOnce | 0 | 单词播放 |
| QMediaPlaylist::CurrentItemInLoop | 1 | 单曲循环 |
| QMediaPlaylist::Sequential | 2 | 从当前选中位置开始,顺序播放 |
| QMediaPlaylist::Loop | 3 | 列表中⽂件循环播放 |
| QMediaPlaylist::Random | 4 | 列表中⽂件随机播放 |
常见属性
cpp
int currentIndex; // 当前播放的媒体⽂件在媒体列表中的索引
const QMediaContent currentMedia; // 当前选中的媒体⽂件
QMediaPlaylist::PlaybackMode playbackMode; // 媒体列表中⽂件的播放模式
常见方法
cpp
bool addMedia(const QMediaContent &content); // 向媒体列表中添加单个媒体⽂件
int mediaCount() const; // 获取播放列表中⽂件的个数
int currentIndex() const; // 获取当前播放的媒体的索引
bool clear(); // 清空媒体列表
QMediaPlaylist::PlaybackMode playbackMode() const; // 获取媒体列表的播放模式
QMediaContent currentMedia() const; // 获取当前播放的媒体⽂件
QString errorString() const; // 获取最近⼀次发⽣过的错误
常见槽函数
cpp
void next(); // 下⼀曲
void previous(); // 上⼀曲
void setCurrentIndex(int playlistPosition); // 设置当前播放媒体的索引
void shuffle(); // 媒体顺序打乱,重建媒体索引
常用信号
cpp
// 列表播放模式⽅法改变时发射
void playbackModeChanged(QMediaPlaylist::PlaybackMode mode);
// 当前索引发⽣改变时发射
void currentIndexChanged(int position);
// 当前媒体⽂件改变时发⽣
void currentMediaChanged(const QMediaContent &content);
该类提供的方法非常丰富,此处暂介绍了需要⽤到的内容,后续开发时需要用到其他内容请参考Qt帮助手册
歌曲播放
播放媒体和播放列表初始化
在播放之前,需要先将QMediaPlayer和QMediaPlaylist初始化好。QQMusic类中需要添加QMediaPlayer和QMediaPlaylist的对象指针,在界⾯初始化时将这两个类的对象创建好
cpp
#include <QMediaPlayer>
#include <QMediaPlaylist>
// qqmusic.h 新增
public:
void initPlayer(); // 初始化媒体对象
private:
//播放器相关
QMediaPlayer* player;
// 要多⾸歌曲播放,以及更复杂的播放设置,需要给播放器设置媒体列表
QMediaPlaylist* playlist;
// qqmusic.cpp 中添加
QQMusic::QQMusic(QWidget *parent)
: QWidget(parent)
, ui(new Ui::QQMusic)
{
ui->setupUi(this);
// 窗⼝控件的初始化⼯作
initUI();
// 初始化播放器
initPlayer();
// 关联所有信号和槽
connectSignalAndSlot();
}
void QQMusic::initPlayer()
{
// 创建播放器
player = new QMediaPlayer(this);
// 创建播放列表
playList = new QMediaPlaylist(this);
// 设置播放模式:默认为循环播放
playList->setPlaybackMode(QMediaPlaylist::Loop);
// 将播放列表设置给播放器
player->setPlaylist(playList);
// 默认⾳量⼤⼩设置为20
player->setVolume(20);
}
播放列表设置
播放之前,先要将歌曲加⼊用于播放的媒体列表,由于每个CommonPage页面的歌曲不同,因此CommonPage中新增将其页面歌曲添加到模仿列表的方法。
cpp
// commonpage.h 中新增
#include <QMediaPlaylist>
void addMusicToPlayer(MusicList &musicList, QMediaPlaylist *playList);
// commonpage.cpp 中新增
void CommonPage::addMusicToPlayer(MusicList &musicList, QMediaPlaylist*playList)
{
for(auto music : musicList)
{
switch (pageType)
{
case LIKE_PAGE:
if(music.getIsLike())
{
musicOfPage.push_back(music.getMusicId());
}
break;
case LOCAL_PAGE:
musicOfPage.push_back(music.getMusicId());
break;
case HISTORY_PAGE:
if(music.getIsHistory())
{
musicOfPage.push_back(music.getMusicId());
}
break;
default:
qDebug() << "暂未支持";
}
}
}
播放和暂停
- 当点击播放和暂停按钮时,播放状态应该在播放和暂停之间切换。播放器的状态如下,刚开始为停止状态
- QMediaPlayer的播放状态有:PlayingState()、PausedState()、StoppedState()。
|------------------|--------------------------|----------|
| 播放状态 | 对应槽函数 | 说明 |
| PlayingState | cpp void play();
| 正在播放 |
| PausedState | cpp void pause();
| 暂停 |
| StoppedState | cpp void stop();
| 停止状态 |
cpp
// qqmusic.h 中新增
// 播放控制区域
void onPlayCliked(); // 播放按钮
// qqmusic.cpp 中新增
void QQMusic::onPlayCliked()
{
if(QMediaPlayer::PlayingState == player->state())
{
// 正在播放点击触发暂停事件
player->pause();
}
else if(QMediaPlayer::PausedState == player->state())
{
// 暂停点击触发继续播放事件
player->play();
}
else if(QMediaPlayer::StoppedState == player->state())
{
// 启动时点击触发开始播放事件
player->play();
}
else
{
qDebug() << player->error();
}
}
void QQMusic::connectSignalAndSlots()
{
// ...
// 播放控制区的信号和槽函数关联
connect(ui->play, &QPushButton::clicked, this, &QQMusic::onPlayMusic);
}
播放时默认是从播放列表索引为0的歌曲开始播放的
另外播放状态改变的时候,需要修改播放按钮上图标,图⽚的修改可以在onPlayCliked函数中设置,也可以拦截
QMediaPlayer中的stateChanged信号,当播放状态改变的时候,QMediaPlayer会触发该信号,在stateChanged信号中修改播放按钮也可以,此处拦截stateChanged信号
cpp
// qqmusic.h 新增
// QMediaPlayer信号处理
// 播放状态发⽣改变
void onPlayStateChanged();
// qqmusic.cpp 新增
// QMediaPlayer信号关联槽函数
void QQMusic::onPlayStateChanged()
{
if(player->state() == QMediaPlayer::PlayingState)
{
// 处于播放状态时
ui->play->setIcon(QIcon(":/images/play_on.png"));
}
else
{
ui->play->setIcon(QIcon(":/images/play.png"));
}
}
void QQMusic::initPlayer()
{
// ...
// QMediaPlayer信号和槽函数关联
// 播放状态改变时:暂停和播放之间切换
connect(player, &QMediaPlayer::stateChanged, this,&QQMusic::onPlayStateChanged);
}
播放和暂停切换的时候,按钮上的图标有重叠,是因为之前在界⾯设置的时候,为了能看到效果,给 按钮添加了背景图片,背景图片和图标是两种属性,都设置时就ui叠加,因此将按钮上个添加背景图片样式去除掉。
cpp
void QQMusic::initUi()
{
// 按钮的背景图⽚样式去除掉之后,需要设置默认图标
// 播放控制区按钮图标设定
ui->play->setIcon(QIcon(":/images/play_2.png")); // 默认为暂停图标
ui->playMode->setIcon(QIcon(":/images/shuffle_2.png")); // 默认为随机播放
volumeTool = new VolumeTool(this);
}
上⼀曲和下⼀曲
播放列表中,提供了previous()和next()函数,通过设置前⼀个或者下⼀个歌曲为当前播放源,player就会播放对应的歌曲
cpp
// qqmusic.h 新增
void onPlayUpCliked(); // 上⼀曲
void onPlayDownCliked(); // 下⼀曲
// qqmusic.cpp 新增
void QQMusic::onPlayUpCliked()
{
playList->previous();
}
void QQMusic::onPlayDownCliked()
{
playList->next();
}
void QQMusic::connectSignalAndSlots()
{
// ...
// 播放控制区的信号和槽函数关联
connect(ui->play, &QPushButton::clicked, this, &QQMusic::onPlayMusic);
connect(ui->playUp, &QPushButton::clicked, this,&QQMusic::onPlayUpClicked);
connect(ui->playDown, &QPushButton::clicked, this,&QQMusic::onPlayDownClicked);
}
播放模式设置
媒体列表提供了以下播放模式:
|---------------------------------------|---------|--------------------|
| 枚举状态名称 | 枚举值 | 说明 |
| QMediaPlaylist::CurrentItemOnce | 0 | 单曲播放 |
| QMediaPlaylist::CurrentItemInLoop | 1 | 单曲循环 |
| QMediaPlaylist::Sequential | 2 | 从当前选中位置开始,顺序播放 |
| QMediaPlaylist::Loop | 3 | 列表中文件循环播放 |
| QMediaPlaylist::Random | 4 | 列表中文件随机播放 |
QMediaPlaylist提供了获取和设置播放模式的方法:
cpp
QMediaPlaylist::PlaybackMode playbackMode() const; // 获取播放模式
void setPlaybackMode(QMediaPlaylist::PlaybackMode mode); // 设置播放模式
目前暂支持:循环播放、随机播放、单曲循环
cpp
// qqmusic.h 中新增
void onPlaybackModeCliked(); // 播放模式设置
// qqmusic.cpp 中新增
void QQMusic::initPlayer()
{
// ...
// 设置播放模式
connect(ui->playMode, &QPushButton::clicked, this,&QQMusic::onPlaybackModeCliked);
}
void QQMusic::onPlaybackModeClicked()
{
// 列表循环->随机播放->单曲循环
if(playList->playbackMode() == QMediaPlaylist::Loop)
{
// 列表循环->随机播放
playList->setPlaybackMode(QMediaPlaylist::Random);
ui->playMode->setToolTip("随机播放");
}
else if(playList->playbackMode() == QMediaPlaylist::Random)
{
playList->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
ui->playMode->setToolTip("单曲循环");
}
else if(playList->playbackMode() == QMediaPlaylist::CurrentItemInLoop)
{
playList->setPlaybackMode(QMediaPlaylist::Loop);
ui->playMode->setToolTip("列表循环");
}
else
{
qDebug() << "暂未支持";
}
}
播放模式切换时会触发 playbackModeChanged 信号,在该信号对应槽函数中,完成图片切换
cpp
// qqmusic.h 中新增
// 播放模式切换槽函数
void onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode);
/ qqmusic.cpp 中新增
void QQMusic::onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode)
{
if(playbackMode == QMediaPlaylist::Loop)
{
ui->playMode->setIcon(QIcon(":/images/playMode.png"));
}
else if(playbackMode == QMediaPlaylist::Random)
{
ui->playMode->setIcon(QIcon(":/images/shuffle.png"));
}
else if(playbackMode == QMediaPlaylist::CurrentItemInLoop)
{
ui->playMode->setIcon(QIcon(":/images/single_play.png"));
}
else
{
qDebug() << "暂未支持该模式";
}
}
void QQMusic::initPlayer()
{
// ...
// 播放列表的模式放⽣改变时的信号槽关联
connect(playList, &QMediaPlaylist::playbackModeChanged,this,&QQMusic::onPlaybackModeChanged)
}
播放所有
- 播放所有按钮属于CommonPage中的按钮,其对应的槽函数添加在CommonPage类中,但是CommonPage不具有音乐播放的功能,因此当点击播放所有按钮后之后,播放所有的槽函数应该发射出信号,让QQMusic类完成播放。
- 由于likePage、localPage、recentPage三个CommonPage页面都有playAllBtn,因此该信号需要带上PageType参数,需要让QQMusic在处理该信号时,知道播放哪个页面的歌曲。
cpp
// commonpage.h 中新增加
signals:
// 该信号由QQMusic处理--在构函数中捕获
void playAll(PageType pageType);
// commonpage.cpp 中修改
CommonPage::CommonPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::CommonPage)
{
ui->setupUi(this);
// playAllBtn按钮的信号槽处理
// 当播放按钮点击时,发射playAll信号,播放当前⻚⾯的所有歌曲
// playAll信号交由QQMusic中处理
connect(ui->playAllBtn, &QPushButton::clicked, this, [=](){
emit playAll(pageType);
});
// ...
}
在QQMusic中,给playAll信号关联槽函数,并播放当前Page页面的所有音乐。playAll槽函数中,根据pageType将当前page页⾯记录下来,默认从该页⾯的第0首歌曲开始播放。注意不要忘记关联信号槽。
cpp
// qqmusic.h 中新增
// 播放所有信号的槽函数
#include "commonpage.h"
void onPlayAll(PageType pageType);
void playAllOfCommonPage(CommonPage* commonPage, int index);
// qqmusic.cpp 中新增
void QQMusic::onPlayAll(PageType pageType)
{
// 播放当前对应页面中的所有歌曲
CommonPage* page = ui->page_local;
switch(pageType)
{
case PageType::LIKE_PAGE:
page = ui->page_like;
break;
case PageType::LOCAL_PAGE:
page = ui->page_local;
break;
case PageType::HISTORY_PAGE:
page = ui->page_recent;
break;
default:
qDebug() << "无该页面";
}
// 播放当前page页面
playAllMusicOfCommonPage(page, 0);
}
void QQMusic::playAllOfCommonPage(CommonPage *commonPage, int index)
{
// 清空之前加载过的playlist
playList->clear();
// 添加当前播放页面歌曲
page->addMusicToPlaylist(musiclist, playList);
// 从第一首开始播放
playList->setCurrentIndex(index);
// 开始播放
player->play();
}
void QQMusic::connectSignalAndSlots()
{
// ...
// 关联播放所有的信号和槽函数
connect(ui->likePage, &CommonPage::playAll, this, &QQMusic::onPlayAll);
connect(ui->localPage, &CommonPage::playAll, this, &QQMusic::onPlayAll);
connect(ui->recentPage, &CommonPage::playAll, this, &QQMusic::onPlayAll);
}
双击CommPage页⾯QListWidget项播放
cpp
// QListWidget中的项被双击时触发
// QModelIndex类中的row()函数会返回被点击的QListWidgetItem在QListWidget中索引
void doubleClicked(const QModelIndex &index);
该信号在QListWidget的基类中定义,有⼀个index参数,表示被双击的QListWidgetItem在QListWidget中的索引,该索引刚好与QMediaPlaylist中歌曲的所以⼀致,被双击时直接播放该首歌曲即可。
cpp
// CommonPage.h 中新增
signals:
void playMusicByIndex(CommonPage*, int);
// commonpage.cpp 中新增
CommonPage::CommonPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::CommonPage)
{
// ...
connect(ui->pageMusicList, &QListWidget::doubleClicked, this, [=](constQModelIndex &index){
// ⿏标双击后,发射信号告诉QQMusic,博能放this⻚⾯中共被双击的歌曲
emit playMusicByIndex(this, index.row());
});
}
// qqmusic.h 中新增
// CommonPage中playMusicByIndex信号对应槽函数
void playMusicByIndex(CommonPage* page, int index);
// qqmusic.cpp 中新增
void QQMusic::playMusicByIndex(CommonPage *page, int index)
{
playAllMusicOfCommonPage(page, index);
}
void QQMusic::connectSignalAndSlots()
{
// ...
// 处理likePage、localPage、recentPage中ListItemBox双击
connect(ui->likePage, &CommonPage::playMusicByIndex, this,&QQMusic::playMusicByIndex);
connect(ui->localPage, &CommonPage::playMusicByIndex, this,&QQMusic::playMusicByIndex);
connect(ui->recentPage, &CommonPage::playMusicByIndex, this,&QQMusic::playMusicByIndex);
}
最近播放同步
- 当播放歌曲改变时,即播放的媒体源发⽣了变化,QMediaPlayer会触发metaDataAvailableChanged 信号,QMediaPlaylist也会触发currentIndexChanged信号,该信号会带index参数,表⽰现在是媒体播放列表中的index歌曲被播放,通过index可以获取到recentPage页⾯中具体播放的歌曲,将该歌曲对应Music对象的isHistoty属性修改为true,然后更新下rencentPage的歌曲列表,播放过的歌曲就添加到历史播放页面中了。
- 获取likePage、localPage、recentPage哪个CommonPage页⾯中的歌曲呢?
- QQMusic类中维护CommonPage*变量currentPage,记录当前正在播放的CommonPage页面,初始时设置为localPage,当播放的页面发生改变时,修改currentPage为当前正在播放页面,其中点击播放所有按钮以及双击QListWidget中项的时候都回引起currentPage的改变。
cpp
// qqmusic.h 中新增
CommonPage* curPage;
// qqmusic.cpp 中修改
void QQMusic::initUi()
{
// ...
// 将localPage设置为当前⻚⾯
ui->stackedWidget->setCurrentIndex(4);
currentPage = ui->localPage;
// ...
}
void QQMusic::playAllOfCommonPage(CommonPage* commonPage, int index)
{
currentPage = commonPage;
// 播放page所在⻚⾯的⾳乐
// 将播放列表先清空,否则⽆法播放当前CommonPage⻚⾯的歌曲
// 另外:该⻚⾯⾳乐不⼀定就在播放列表中,因此需要先将该⻚⾯⾳乐添加到播放列表
playList->clear();
// ...
}
准备工作完成之后,同步最近播放歌曲的逻辑实现如下:
cpp
// qqmusic.h 中新增
// ⽀持播放历史记录
void onCurrentIndexChanged(int index);
// qqmusic.cpp 中新增
void QQMusic::initPlayer(int index)
{
// ...
// 播放列表项发⽣改变,此时将播放⾳乐收藏到历史记录中
connect(playList, &QMediaPlaylist::currentIndexChanged, this,&QQMusic::onCurrentIndexChanged);
}
void QQMusic::onCurrentIndexChanged(int index)
{
// ⾳乐的id都在commonPage中的musicListOfPage中存储着
const QString& musicId = currentPage->getMusicIdByIndex(index);
// 有了MusicId就可以再musicList中找到该⾳乐
auto it = musicList.findMusicByMusicId(musicId);
if(it != musicList.end())
{
// 将该⾳乐设置为历史播放记录
it->setIsHistory(true);
}
ui->recentPage->reFresh(musicList);
}
// commmonpage.h 中新增
const QString& getMusicIdByIndex(int index) const;
// commonpage.cpp 中新增
QString CommonPage::getMusicIdByIndex(int index)
{
if(index >= musicOfPage.size())
{
qDebug()<<"⽆此歌曲";
return "";
}
return musicOfPage[index];
}
音量设置
功能分析
- 当点击静音按钮时,⾳量应该在静音和非静音之间进行切换,并且按钮上图标需要同步切换
- 鼠标在滑竿上点击或拖动滑竿时,应该跟进滑竿的高低比率,设置音量大小,同时修改界面音量比率
QMediaPlayer提供支持
QMediaPlayer中音量相关操作如下:
cpp
int volume; // 标记⾳量⼤⼩,值在0~100之间
int volume()const; // 获取⾳量⼤⼩
void setVolume(int); // 槽函数:设置⾳量⼤⼩
bool muted; // 是否静⾳,true为静⾳,false为⾮静⾳
bool isMuted()const; // 获取静⾳状态
bool setMuted(bool muted); // 槽函数:设置静⾳或⾮静⾳
静音和非静音切换
- VolumeTool类中需要添加两个成员变量,并在构造函数中完成默认值的设置
- 给静音按钮参加槽函数onSilenceBtnClicked,并在构造函数中connect按钮的clicked信号,当按钮点击时候,调用setMuted(bool nuted)函数,完成静音和非静音的设置
- 由于VolumeTool不具备媒体播放控制,因此当静音状态发生改变时,发射设置静音信号,让QQMusic来处理
cpp
// volumetool.h 中新增
signals:
void setSilence(bool); // 设置静⾳信号
void onSilenceBtnClicked(); // 静⾳按钮槽函数
bool isMuted; // 记录静⾳或⾮静⾳,当点击静⾳按钮时,在true和false之间切换
int volumeRatio; // 标记⾳量⼤⼩
// volumetool.cpp 中新增
VolumeTool::VolumeTool(QWidget *parent) :
QWidget(parent),
ui(new Ui::VolumeTool),
isMuted(false), // 默认静⾳
volumeRatio(20) // 默认⾳量为20%
{
//...
// 关联静⾳按钮的信号槽
connect(ui->silenceBtn, &QPushButton::clicked, this,&VolumeTool::onSilenceBtnClicked);
}
void VolumeTool::onSilenceBtnClicked()
{
isMuted = !isMuted;
if(isMuted)
{
ui->silenceBtn->setIcon(QIcon(":/images/silent.png"));
}
else
{
ui->silenceBtn->setIcon(QIcon(":/images/volumn.png"));
}
emit setSilence(isMuted);
}
// qqMusic.h 中新增
void setMusicSilence(bool isMuted);
// qqmusic.cpp 中新增
void QQMusic::setMusicSilence(bool isMuted)
{
player->setMuted(isMuted);
}
void QQMusic::connectSignalAndSlots()
{
// ...
// 设置静⾳
connect(volumeTool, &VolumeTool::setSilence, this,&QQMusic::setMusicSilence);
}
鼠标按下、滚动以及释放事件处理
当鼠标在滑竿上按下时,需要设置sliderBtn和outLine的位置,当鼠标在滑竿上移动或者鼠标抬起时,需要设置SliderBtnoutLine结束的位置,即改变VolumeTool中滑竿的显示。具体修改播放媒体音量大小操作应该由于QQMusic负责处理,因此当鼠标移动或释放时,需要发射信号让QQMusic知道需要修改播放媒体的音量大小了。
cpp
// volumetool.h 中新增
// 发射修改⾳量⼤⼩槽函数
void setMusicVolume(int);
// 事件过滤器
bool eventFilter(QObject* object, QEvent* event);
// volumetool.cpp 中新增
bool VolumeTool::eventFilter(QObject *object, QEvent *event)
{
// 过滤volumeBox上的事件
if(object == ui->volumeBox)
{
if(event->type() == QEvent::MouseButtonPress)
{
// 如果是⿏标按下事件,修改sliderBtn和outLine的位置,并计算volumeRation
setVolume();
}
else if(event->type() == QEvent::MouseMove)
{
// 如果是⿏标滚动事件,修改sliderBtn和outLine的位置,并计算volumeRation,
setVolume();
// 并发射setMusicVolume信号
emit setMusicVolume(volumeRatio);
}
else if(event->type() == QEvent::MouseButtonRelease)
{
// 如果是⿏标释放事件,直接发射setMusicVolume信号
emit setMusicVolume(volumeRatio);
}
return true;
}
return QObject::eventFilter(object, event);
}
VolumeTool::VolumeTool(QWidget *parent) :
QWidget(parent),
ui(new Ui::VolumeTool),
isMuted(false),
volumeRatio(20)
{
// ...
// 安装事件过滤器
ui->volumeBox->installEventFilter(this);
}
outLine和SliderBtn以及volumeRation更新
- outLine坐标:[(28,25),4*180]。
- 当outLine最低时,即高度为0时候,outLine左上⻆坐标为:(28,205)
- 当outLine最高时,即高度为180时候,outLine左上⻆坐标为:(28,25)
- 当刷吧在滑竿上滚动时,鼠标坐标转化为volumeBox上相对坐标时,鼠标y坐标必须在[25~205]范围内。
- 据鼠标在滑竿上的相对高度更新:SliderBtn、outLine以及volumeRation的值
cpp
// volumetool.h 中新增
// 根据⿏标在滑竿上滑动更新滑动界⾯,并按照⽐例计算⾳量⼤⼩
void setVolume();
// volumetool.cpp 中新增
void VolumeTool::setVolume()
{
// 获取鼠标点击时Y的坐标
int height = ui->volumeBox->mapFromGlobal(QCursor().pos()).y();
// height坐标合法性
height = height < 25 ? 25 : height;
height = height > 205 ? 205 : height;
// 更新outLine动画
ui->outLine->setGeometry(ui->outLine->x(), height, ui->outLine->width(), 205 - height);
// 更新sliderBtn按钮位置
ui->silderBtn->move(ui->silderBtn->x(), ui->outLine->y() - ui->silderBtn->height() / 2);
// 计算音量大小
volumeRatio = (int)(ui->outLine->height() / (float)180 * 100);
// 更新百分比
ui->volumeRatio->setText(QString::number(volumeRatio) + "%");
}
QQMusic类拦截VolumeTool发射的setMusicVolume信号,将音量大小设置为指定值。
cpp
// qqmusic.h 中新增
void setPlayerVolume(int vomume); // 设置音量大小
// qqmusic.cpp 中新增
void QQMusic::setPlayerVolume(int volume)
{
player->setVolume(volume);
}
void QQMusic::connectSignalAndSlots()
{
// ...
// 设置⾳量⼤⼩
connect(volumeTool, &VolumeTool::setMusicVolume, this,&QQMusic::setPlayerVolume);
}
当前播放时间和总时间更新
- 界面歌曲总时间更新
- 歌曲总时间在Music对象中可以获取,也可以让player调⽤⾃⼰的duration()⽅法获取。但是这两种 获取歌曲总时间的调用时机不太好确定。我们期望的是当歌曲发生切换时,获取到正在播放歌曲的总时长。
- 当播放源的持续时长发生改变时,QMediaPlayer会触发durationChanged信号,该信号中提供了将要播放媒体的总时长。
cpp
// duration为将要播放媒体的总时⻓
void QMediaPlayer::durationChanged(qint64 duration);
因此在QQMusic类中给该信号关联槽函数,在槽函数中将duration更新到界⾯总时间即可。
cpp
// qqmusic.h 中新增
// 歌曲持续时⻓改变时[歌曲切换]
void onDurationChanged(qint64 duration);
// qqmusic.cpp 中新增
void QQMusic::onDurationChanged(qint64 duration)
{
ui->totalTime->setText(QString("%1:%2").arg(duration/1000/60, 2, 10,QChar('0')).arg(duration/1000%60,2,10,QChar('0')));
}
void QQMusic::initPlayer()
{
// ...
// 媒体持续时⻓更新,即:⾳乐切换,时⻓更新,界⾯上时间也要更新
connect(player, &QMediaPlayer::durationChanged, this,&QQMusic::onDurationChanged);
}
界面歌曲当前播放时间更新
媒体在持续播放过程中,QMediaPlayer会发射positionChanged,该信号带有⼀个qint64类型参数,表示媒体当前持续播放的时间。
cpp
// position: 媒体持续播放时间
void positionChanged(qint64 position);
因此,在QQMusic中捕获该信号,便可获取到正在播放媒体的持续时间。
cpp
/ qqmusic.h 中新增
// 播放位置改变,即持续播放时间改变
void onPositionChanged(qint64 duration);
// qqmusic.cpp 中新增
void QQMusic::onPositionChanged(qint64 duration)
{
ui->currentTime->setText(QString("%1:%2").arg(duration/1000/60, 2, 10,QChar('0')).arg(duration/1000%60,2,10,QChar('0')));
// 界⾯上的进度条也需要同步修改
}
void QQMusic::initPlayer()
{
// ...
// 播放位置发⽣改变,即已经播放时间更新
connect(player, &QMediaPlayer::positionChanged, this,&QQMusic::onPositionChanged);
}
在持续播放时间改变的同时,界面上的进度条应该也要前进
进度条处理[seek功能]
seek功能介绍
播放器的seek功能指,通过时间或位置快速定位到视频或音频流的特定位置,允许用户在播放过程中随时跳转到特定时间点,从而快速找到感兴趣的内容或重新开始播放。
在界⾯上的体现是,当在MusicSlider上点击或者拖拽的时候,会跳转到歌曲的指定位置进行播放,并且歌曲的当前持续播放时间要同步修改。
进度条界面显示
cpp
// musicslider.h 中新增
void mousePressEvent(QMouseEvent *event); // 重写⿏标按下事件
void mouseMoveEvent(QMouseEvent *event); // 重写⿏标滚动事件
void mouseReleaseEvent(QMouseEvent *event); // 重写⿏标释放事件
void moveSilder(); // 修改outLine的宽度为currentPos
private:
int currentPos; // 滑动条当前位置
// musicslider.cpp 中新增
MusicSlider::MusicSlider(QWidget *parent) :
QWidget(parent),
ui(new Ui::MusicSlider)
{
ui->setupUi(this);
// 初始情况下,还没有开始播放,将当前播放进度设置为0
currentPos = 0;
maxWidth = width();
moveSilder();
}
void MusicSlider::mousePressEvent(QMouseEvent *event)
{
// 注意:QMouseEvent中的pos()为⿏标相对于widget的坐标,不是相当于screen
// 因此⿏标位置的 x 坐标可直接作为outLine的宽度
currentPos = event->pos().x();
moveSilder();
}
void MusicSlider::mouseMoveEvent(QMouseEvent *event)
{
// 检测移动时鼠标是否在矩形内的限制
QRect musicSliderRect = QRect(0, 0, geometry().width(), geometry().height());
if(!musicSliderRect.contains(event->pos()))
{
return;
}
if(event->buttons() == Qt::LeftButton)
{
currentPos = event->pos().x();
if(currentPos < 0)
{
currentPos = 0;
}
if(currentPos > maxWidch)
{
currentPos = maxWidch;
}
moveSlider();
}
}
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
currentPos = event->pos().x();
moveSlider();
emit setMusicSliderPositon(ui->outLine->width() / (float)maxWidch);
}
void MusicSlider::moveSlider()
{
ui->outLine->setGeometry(ui->outLine->x(), ui->outLine->y(), currentPos, ui->outLine->height());
}
进度条同步持续播放时间
当鼠标释放之后,计算出进度条当前位置currentPos和总宽度的maxWidth比率,然后发射信号告诉QQMusic,让player按照该比率更新持续播放时间。
cpp
// musicslider.h 新增
signals:
void setMusicSliderPosition(float);
// musicslider.cpp 中新增
void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
currentPos = event->pos().x();
moveSlider();
emit setMusicSliderPositon(ui->outLine->width() / (float)maxWidch);
}
// qqmusic.h 中新增
void onMusicSliderChanged(float value); // 进度条改变
// qqmusic.cpp 中新增
void QQMusic::onMusicSliderChanged(float value)
{
qint64 duration = totalTime * ration;
ui->currentTime->setText(QString("%1:%2").arg(duration / 1000 / 60, 2, 10, QChar('0'))
.arg(duration / 1000 % 60, 2, 10, QChar('0')));
// 媒体播放时间修改
player->setPosition(duration);
}
void QQMusic::connectSignalAndSlots()
{
// ...
// 进度条拖拽
connect(ui->progressBar, &MusicSlider::setMusicSliderPosition, this,&QQMusic::onMusicSliderChanged);
}
持续时间同步进度条
当播放位置更新时,界⾯上持续播放时间⼀直在更新,因此进度条也需要持续向前进,MusicSlider应该提供setStep函数,播放进度持续更新时,也将进度条通过setStep函数更新下。
cpp
// musicslider.h 中新增
void setStep(float bf);
// musicslider.cpp 中新增
void MusicSlider::setStep(float bf)
{
currentPos = maxWidth*bf;
moveSilder();
}
// qqmusic.cpp 中修改
void QQMusic::onPositionChanged(qint64 duration)
{
// 1. 更新已经播放时间
ui->currentTime->setText(QString("%1:%2").arg(duration/1000/60, 2, 10,QChar('0')).arg(duration/1000%60,2,10,QChar('0')));
// 2. 进度条处理
ui->progressBar->setStep((float)duration / (float)totalDuration);
}
歌曲名称、歌手和封面图⽚同步
- 在进⾏歌曲切换时候,歌曲名称、歌手以及歌曲的封⾯图,也需要更新到界⾯。歌曲名称、歌手可以再Music对象中进行获取,歌曲的封面图可以通过player到歌曲的元数据中获取,获取时需要使用"ThumbnailImage"作为参数,注意有些歌曲可能没有封面图,如果没有设置⼀张默认的封面图。
- 由于歌曲切换时,player需要将新播放歌曲作为播放源,并解析歌曲文件,如果歌曲文件是有效的才能播放;因此QQMusic类可以给QMediaPlayer发射的metaDataAvailableChanged(bool) 信号关联槽函数,当歌曲更换时,完成信息的更新。
cpp
// qqmusic.h中新增
void onMetaDataAvailableChangedChanged(bool available)
// qqmusic.cpp 中新增
void QQMusic::onMetaDataAvailableChangedChanged(bool available)
{
// 播放源改变
qDebug()<<"歌曲切换";
// 1. 从player播放歌曲的元数据中获取歌曲信息
QString singer = player->metaData("Author").toStringList().join(",");
QString musicName = player->metaData("Title").toString();
if(musicName.isEmpty())
{
auto it = musicList.findMusicByMusicId(currentPage->getMusicIdByIndex(curPlayMusicIndex));
if(it != musicList.end())
{
musicName = it->getMusicName();
singer = it->getMusicSinger();
}
}
// 2. 设置歌⼿、歌曲名称、专辑名称
ui->musicName->setText(musicName);
ui->musicSinger->setText(singer);
// 3. 获取封⾯图⽚
QVariant coverImage = player->metaData("ThumbnailImage");
if(coverImage.isValid())
{
// 获取封⾯图⽚成功
QImage image = coverImage.value<QImage>();
// 设置封⾯图⽚
ui->musicCover->setPixmap(QPixmap::fromImage(image));
// 缩放填充到整个Label
ui->musicCover->setScaledContents(true);
currentPage->setImageLabel(QPixmap::fromImage(image));
}
else
{
// 设置默认图⽚-修改
qDebug()<<"歌曲没有封⾯图⽚";
}
}
void CommonForm::setImageLabel(QPixmap pixMap)
{
ui->musicImgLabel->setPixmap(pixMap);
ui->musicImgLabel->setScaledContents(true);
}
lrc歌词同步
播放歌曲时,当点击"词"按钮后窗口会慢慢弹出,当点击隐藏按钮后,窗口会慢慢隐藏,且没有标题栏。内部显示当前播放歌曲的歌词。当点击下拉按钮时,窗口会隐藏起来。
lrc歌词界面分析
lrcPage中元素种类比较少,具体分析如下:
①~⑦均为QLabel,用来显示歌词,④为当前正在播放歌词,①②③为当前播放歌词的前三句,⑤⑥⑦为当前播放歌词的后三句。歌词会随着播放时间持续,从下往上移动,⑧为按钮,点击之后窗口隐藏。
lrc歌词界面布局
- 在qt create中新创建⼀个qt设计师界⾯,命名为LrcPage,geometry的宽⾼修改为:1020*680。
- 拖⼀个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到lrcPage中,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 | cpp #bgStyle { border-image:url(":/images/bg.png"); } * { color:#FFFFFF; }
|
| lineCenter | cpp #lineCenter { color:#1ECE9A; }
|
| hideBtn | cpp #hideBtn { border:none; }
|
LrcPage显示
在LrcPage的构造函数中,将窗口的标题栏去除掉;并给hideBtn关联clicked信号,当按钮点击时将窗口隐藏
cpp
// lrcPage.cpp 中添加
LyricsPage::LyricsPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::LyricsPage)
{
ui->setupUi(this);
setWindowFlag(Qt::FramelessWindowHint);
connect(ui->hideBtn, &QPushButton::clicked, this, [=]{
hide();
});
ui->hideBtn->setIcon(QIcon(":/images/xiala.png"));
}
在QQMusic中,创建LrcPage的指针,并在initUi()方法中创建窗口的对象,创建好之后将窗口隐藏起来
在QQMusic中,给lrcWord按钮添加槽函数,在槽函数中将窗口显示出来
cpp
// qqmusic.h 中添加
#include "lrcpage.h"
LrcPage* lrcPage;
void onLrcWordClicked();
// qqmusic.cpp 中添加
void QQMusic::initUI()
{
// ...
// 创建lrc歌词窗⼝
lrcPage = new LrcPage(this);
lrcPage->hide();
}
void QQMusic::onLrcWordClicked()
{
lrcPage->show();
}
void QQMusic::connectSignalAndSlot()
{
// ...
// 显⽰歌词窗⼝
connect(ui->lrcWord, &QPushButton::clicked, this,&QQMusic::onLrcWordClicked);
}
LrcPage添加动画效果
当点击QQMusic中"歌词"按钮时,lrcPage窗⼝是以动画效果显示出来的,当点击lrcPage上"下拉"按钮时,窗口先以动画的方式下移,动画结束后窗口隐藏
窗口显示和上移动画:
- QQMusic的initUi函数中,创建lrcPage对象并将窗口隐藏;给lrcPage窗口添加上移动画,动画暂不开启
- QQMusic中给"歌词"按钮添加槽函数,当按钮点击时,显示窗口,开启动画
cpp
// qqmusic.h中新增
#include <QPropertyAnimation>
// 歌词按钮槽函数
void onLrcWordClicked();
private:
QPropertyAnimation* lrcAnimation;
// qqmusic.cpp 中新增
void QQMusic::initUi()
{
// ...
// 窗⼝添加阴影效果
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setOffset(0, 0);
shadowEffect->setColor("#000000"); // ⿊⾊
// 此处需要将圆⻆半径不能太⼤,否则动画效果有问题,可以设置为10
shadowEffect->setBlurRadius(20);
this->setGraphicsEffect(shadowEffect);
// ...
// 实例化LrcWord对象
lrcPage = new LrcPage(this);
lrcPage->hide();
// lrcPage添加动画效果
lrcAnimation = new QPropertyAnimation(lrcPage, "geometry", this);
lrcAnimation->setDuration(250);
lrcAnimation->setStartValue(QRect(10, 10+lrcPage->height(),
lrcPage->width(), lrcPage->height()));
lrcAnimation->setEndValue(QRect(10, 10, lrcPage->width(), lrcPage->height()));
}
// 显示窗口并开启动画
void QQMusic::onLrcWordClicked()
{
lrcPage->show();
lrcAnimation->start();
}
void QQMusic::connectSignalAndSlots()
{
// ...
// 歌词按钮点击信号和槽函数
connect(ui->lrcWord, &QPushButton::clicked, this,&QQMusic::onLrcWordClicked);
// ...
}
窗口隐藏和下移动画
LrcPage类中,在构造窗口时设置下移动画,给"下拉"按钮添加槽函数,当"下拉按钮"点击时,开启动画;当动画结束时,将窗口隐藏。
cpp
// lrcpage.h 中新增
#include <QPropertyAnimation>
private:
QPropertyAnimation* lrcAnimation;
// lrcpage.cpp 中新增
LrcPage::LrcPage(QWidget *parent) :
QWidget(parent),
ui(new Ui::LrcPage)
{
ui->setupUi(this);
// ...
lrcAnimation = new QPropertyAnimation(this, "geometry", this);
lrcAnimation->setDuration(250);
lrcAnimation->setStartValue(QRect(10, 10, width(), height()));
lrcAnimation->setEndValue(QRect(10, 10+height(), width(), height()));
// 点击设置下拉按钮时开启动画
connect(ui->hideBtn, &QPushButton::clicked, this, [=]{
lrcAnimation->start();
});
// 动画结束时,将窗⼝隐藏
connect(lrcAnimation, &QPropertyAnimation::finished, this, [=]{
hide();
});
}
lrc歌词解析和同步
什么是LRC歌词
- lrc是英⽂lyric(歌词)的缩写,被用作歌词文件的扩展名。该文件将歌词和歌词出现的时间编辑到⼀起,当歌曲播放的时候,按照歌词文件中的时间依次将歌词显示出来
- 标准格式:[分钟:秒.毫秒]歌词
- 其他格式:[分钟:秒]歌词 [分钟:秒:毫秒]歌词
- 每首歌的lrc歌词有多行文本,因此lrc歌词中的每行可以采用结构体管理
cpp
// lrcpage.h 中新增
struct LyricLine
{
qint64 time; // 时间
QString text; // 歌词内容
LyricLine(qint64 qtime, QString qtext)
: time(qtime)
, text(qtext)
{}
};
// LrcPage类中添加成员变量
QVector<LrcLine> lrcLines; // 按照时间的先后次序保存每⾏歌词
通过歌曲名找LRC文件
- ⼀般情况下,播放器在设计之初就会设计好歌曲文件和歌词文件的存放位置,以及对应关系,通常歌曲⽂件和lrc歌词文件名字相同,后缀不同。在磁盘存放的时候,可以将歌曲文件和lrc文件分两个文件夹存储,也可以存储到⼀个文件夹下
- 本文为了方便处理,存储在⼀个文件夹下,因此可以通过Music对象快速找到lrc歌词文件
cpp
// music.h 中新增
QString getLrcFilePath() const;
// music.cpp 中新增
QString Music::getLrcFilePath() const
{
// ⾳频⽂件和LRC⽂件在⼀个⽂件夹下
// 直接将⾳频⽂件的后缀替换为.lrc
QString path = musicUrl.toLocalFile();
path.replace(".mp3", ".lrc");
path.replace(".flac", ".lrc");
path.replace(".mpga", ".lrc");
return path;
}
LRC歌词解析
找到lrc歌词文件后,由lrcPage类完成对歌词的解析。解析的大概步骤:
- 打开歌词文件
- 以行为单位,读取歌词文件中的每⼀行
- 按照lrc歌词文件格式,从每行文本中解析出时间和歌词
- ⽤<时间,行歌词>构建⼀个LrcLine对象存储到lrcLines中
cpp
// lrcpage.h 中新增
bool parseLrc(const QString& lrcPath);
// lrcpage.cpp 中新增
bool LrcPage::parseLrc(const QString& lrcPath)
{
lrcLines.clear();
// 打开歌词⽂件
QFile lrcFile(lrcPath);
if(!lrcFile.open(QFile::ReadOnly))
{
qDebug()<<"打开⽂件:"<<lrcPath;
return false;
}
while(!lrcFile.atEnd())
{
QString lrcWord = lrcFile.readLine(1024);
int left = lrcWord.indexOf('[');
int right = lrcWord.indexOf(']');
// 解析时间
qint64 lineTime = 0;
int start = 0;
int end = 0;
QString time = lrcWord.mid(left, right-left+1);
// 解析分钟
start = 1;
end = time.indexOf(':');
lineTime += lrcWord.mid(start, end-start).toInt()*60*1000;
// 解析秒
start = end + 1;
end = time.indexOf('.', start);
lineTime += lrcWord.mid(start, end-start).toInt()*1000;
// 解析毫秒
start = end+1;
end = time.indexOf('.', start);
lineTime += lrcWord.mid(start, end-start).toInt();
// 解析歌词
QString word = lrcWord.mid(right+1).trimmed();
lrcLines.push_back(LrcLine(lineTime, word.trimmed()));
}
// 测试验证
for(auto word : lrcLines)
{
qDebug()<<word.time<<" "<<word.text;
}
return true;
}
根据歌曲播放位置获取歌词并显示
当歌曲播放进度改变时候,QMediaPlayer的positionChanged信号会触发,该信号同步播放时间的时候已经在QQMusic类中处理过了,在其槽函数中就能拿到当前歌曲的播放时间,通过播放时间,就能在LrcPage中找到对应行的歌词。
cpp
// lrcpage.h 中新增
int getLineWordLineIndex(qint64 pos);
// lrcpage.cpp 中新增
int LrcPage::getLineLrcWordIndex(qint64 pos)
{
// 如果歌词是空的,返回-1
if(lrcLines.isEmpty())
{
return -1;
}
if(lrcLines[0].time > pos)
{
return 0;
}
// 通过时间⽐较,获取下标
for(int i = 1; i < lrcLines.size(); ++i)
{
if(pos > lrcLines[i-1].time && pos <= lrcLines[i].time)
{
return i-1;
}
}
// 如果没有找到,返回最后⼀⾏
return lrcLines.size()-1;
}
QString LrcPage::getLineLrcWord(qint64 index)
{
if(index < 0 || index >= lrcLines.size())
{
return "";
}
return lrcLines[index].text;
}
void LrcPage::showLrcWord(int time)
{
// 先要获取歌词--根据歌词的时间进⾏获取
int index = getLineLrcWordIndex(time);
if(-1 == index)
{
ui->line1->setText("");
ui->line2->setText("");
ui->line3->setText("");
ui->lineCenter->setText("当前歌曲⽆歌词");
ui->line4->setText("");
ui->line5->setText("");
ui->line6->setText("");
}
else
{
ui->line1->setText(getLineLrcWord(index-3));
ui->line2->setText(getLineLrcWord(index-2));
ui->line3->setText(getLineLrcWord(index-1));
ui->lineCenter->setText(getLineLrcWord(index));
ui->line4->setText(getLineLrcWord(index+1));
ui->line5->setText(getLineLrcWord(index+2));
ui->line6->setText(getLineLrcWord(index+3));
}
}
lrc歌词同步播放进度
- 当歌曲发生切换时,需要完成lrc歌词文件的解析
- 当歌曲播放进度发生改变时,根据歌曲的当前播放时间,通过lrcPage找到对应行歌词并显示出来
cpp
// qqmusic.cpp 添加
void QQMusic::onMetaDataAvailableChanged(bool available)
{
// 歌曲名称、歌曲作者直接到Musci对象中获取
// 此时需要知道媒体源在播放列表中的索引
QString musicId = currentPage->getMusicIdByIndex(currentIndex);
auto it = musicList.findMusicByMusicId(musicId);
// ...
// 加载lrc歌词并解析
if(it != musicList.end())
{
lrcPage->parseLrc(it->getLrcFilePath());
}
}
void QQMusic::onPositionChanged(qint64 position)
{
// 1. 更新当前播放时间
ui->currentTime->setText(QString("%1:%2").arg(position/1000/60, 2, 10,QChar('0'))
.arg(position/1000%60, 2, 10,QChar('0')));
// 2. 更新进度条的位置
ui->progressBar->setStep(position/(float)totalTime);
// 3. 同步lrc歌词
if(playList->currentIndex() >= 0)
{
lrcPage->showLrcWord(position);
}
}
持久化支持
支持播放相关功能之后,每次在验证功能时都需要从磁盘中加载歌曲文件,非常麻烦。而且之前收藏的喜欢歌曲以及播放记录在程序关闭之后就没有了,这⼀般是无法接受的
因此需要将每次在播放器上进行的操作保留下来,比如:所加载的歌曲、以及歌曲信息;收藏歌曲信息;历史播放等信息保存起来,当下次程序启动时,将保存的信息加载到播放器即可,这样就能将在播放器上的操作记录保留下来了
要永久性保存,最简单的方式就是直接保存到文件,但是保存文件不安全,而且需要自己操作文件比较⿇烦,本次采用数据库完成信息的持久保存
SQLite数据库介绍
常⻅的数据库管理系统有:Oracle、SqlServer、MySQL等,这些数据库管理系统有⼀个特点,必须先要在本地安装数据库系统的软件,然后才能使用数据库管理系统提供的服务。而数据库管理系统的安装和卸载,有时候简直是噩梦,怎么装就是失败,最后导致重装操作系统
本文操作⼀款轻量级、无需安装的桌面型数据库SQLite,SQLite是非常流行的开源嵌入式数据库,将源文件添加到工程就可以直接使用。它很好的支持关系型数据库所具备的⼀些基本特征,比如:标准SQL语法、事务、数据表和索引等
SQLite主要特征:
- 管理简单,甚⾄可以认为无需管理
- 操作方便,SQLite生成的数据库文件可以在各个平台无缝移植
- 可以⾮常方便的以多种形式嵌入到其他应用程序中,如静态库、动态库等
- 易于维护
Qt中已经内置了SQLite,在安装qt开发环境时,SQLite环境已经配置好了,用户在.pro文件中导入数据库模块就可以使用
cpp
// qqmusic.pro
QT += sql
QSqlDatabase类介绍
QSqlDatabase类主要处理与数据库的连接,它提供了创建、配置、打开和关闭数据库连接的方法
数据库连接和关闭
cpp
// 功能:根据type来添加数据库驱动
// type:数据库类型 [QDB2:IBM DB2, QMYSQL:MySQL, QOCI:Oracle, QODBC:ODBC,QSQLITE:SQLite...]
// connectionName: 数据库连接的名称[可选]。如果提供,可以为数据库连接指定⼀个唯⼀的名称
// 返回值:表⽰新创建的数据库连接
static QSqlDatabase addDatabase(const QString &type,const QString &connectionName = QLatin1String(defaultConnection))
// 添加SQLite数据库驱动,返回⼀个连接
QSqlDatabase QQMusicDB = QSqlDatabase::addDatabase("QSQLITE");
// 功能:设置数据库⽂件的名称
// name: 要连接的数据库的名称。对于SQLite,通常是数据库⽂件的路径;对于其他数据库系统,⽐
如MySQL,通常是数据库管理系统中数据库的名称
void setDatabaseName(const QString &name);
// 功能:打开数据库连接,即和数据库真正建⽴连接
// 返回值:连接成功连接返回true,否则返回false,注意:可以使⽤isopen()⽅法检测是否打开
// user:数据库⽤⼾名
// password: 数据库密码
bool open();
bool open(const QString &user, const QString &password);
// 功能:关闭数据库连接,释放所有资源,并使与数据库⼀起使⽤的所有QSqlQuery对象⽆效
void close();
下⾯给出⼀个数据库连接的示例,测试方式:
radioPage和musicPage页面没有使用留待扩展,可以在radioPage页面拖⼀个按钮,objectName修改为connectDB,然后添加槽函数,槽函数中实现连接SQLite数据库代码。
cpp
// qqmusic.h 中新增
void on_connectDB_clicked();
// qqmusic.cpp 中新增
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
void QQMusic::on_connectDB_clicked()
{
// 加载数据库驱动
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
// 设置数据库名字
db.setDatabaseName("QQMusic.db");
// 打开数据库
if(!db.open())
{
qDebug()<<"QQMusic:"<<db.lastError().text();
}
qDebug()<<"数据库QQMusic.db创建成功";
// 关闭数据库连接,释放资源
db.close();
}
数据库连接成功后,会在构建目录中⽣成QQMusic.db⽂件,QQMusic.db就是通过setDatabaseName设置的名字
创建表
创建表是标准的SQL语句,语法如下:
cpp
CREATE TABLE tableName(
column1 dataType,
column2 dataType,
column3 dataType
...
);
CREATE TABLE tableName(
column1 dataType PRIMARY KEY AUTOINCREMENT, -- column1为主键,⾃动增⻓
column2 dataType NOT NULL, -- 在往表中插⼊数据时,第⼆个字段不能为空
column3 dataType,
...
);
-- 创建表,如果表已经存在则不创建
CREATE TABLE IF NOT EXISTS tableName(
column1 dataType PRIMARY KEY AUTOINCREMENT,
column2 dataType NOT NULL,
column3 dataType,
...
);
SQLite支持的数据类型:
|----------|---------------------------------------------|
| 数据类型 | 描述 |
| NULL | 值是⼀个NULL值 |
| INTEGER | 值是⼀个带符号的整数,根据值的大小存储在1 2 3 4 6或8字节中 |
| REAL | 值是⼀个浮点数,存储为8字节的IEEE浮点数 |
| TEXT | 值是⼀个文本字符串,使用数据库编码(UTF-8、UTF-16BE或UTF-6LE)存储 |
| BLOB | 值是⼀个blob数据,完全根据它的输入存储 |
创建⼀个学生
cpp
-- 创建⼀个学⽣表,内部包含学⽣姓名、年龄、性别、成绩
CREATE TABLE IF NOT EXISTS student(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age REAL,
gpa REAL
);
数据库表创建好之后,通过QSqlQuery类来执⾏SQL语句和操作数据库。QSqlQuery类中的exec()函数用来执⾏SQL语句:
cpp
// 功能:执⾏SQL语句
// query: 要实⾏的sql语句,以字符串⽅式提供
// 返回值:执⾏成功返回true,否则返回false
bool exec(const QString &query);
// 功能:执⾏SQL语句
// 该⽅法需要提前将SQL语句通过prepare⽅法准备好,然后使⽤bindValue⽅法绑定参数值,
// 最后调⽤exec ⽅法执⾏
bool exec();
建立数据库连接之后,数据库连接不要关闭,则连接⼀直存在,可以直接执行SQL语句。
在audioPage页面新增加⼀个按钮,objectName修改为:createTable,并添加槽函数,执行如下代码
cpp
// qqmusic.h 中新增
void on_createTable_clicked();
// qqmusic.cpp 中新增
#include <QSqlQuery>
void QQMusic::on_createTable_clicked()
{
// 创建⼀个学⽣表,包含:学⽣id(主键,⾃动增⻓), 学⽣姓名(⾮空), 年龄,成绩绩点
QString sql("CREATE TABLE IF NOT EXISTS student(\
id INTEGER PRIMARY KEY AUTOINCREMENT,\
name TEXT NOT NULL,\
age INTEGER,\
gpa REAL\
)");
QSqlQuery query;
if(!query.exec(sql))
{
qDebug()<<"学⽣表创建失败:"<<query.lastError().text();
return;
}
qDebug()<<"创建学⽣表成功!!!";
}
插入数据:
往表中插入数据的SQL语句:
cpp
-- 在这⾥,column1, column2,...columnN 是要插⼊数据的表中的列的名称。
INSERT INTO TABLE_NAME [(column1, column2, column3,...columnN)]
VALUES (value1, value2, value3,...valueN);
-- 如果要为表中的所有列添加值,您也可以不需要在 SQLite 查询中指定列名称。
-- 但要确保值的顺序与列在表中的顺序⼀致
INSERT INTO TABLE_NAME VALUES (value1,value2,value3,...valueN);
INSERT INTO student(id, name, age, gpa) VALUES(1, '张三', 19, 3.5);
INSERT INTI student VALUES(2, '李四', 20, 4.0);
使用QSqlQuery类操作:
cpp
// 功能:准备SQL语句,该语句中包含⼀个或者多个参数占位符。这些参数占位符在SQL中默认为?表示
// 也可以⾃定义占位符。其允许提前先设置好SQL语句结构,但是不执⾏
bool prepare(const QString &query);
// 功能:使⽤参数的名称(即通过prepare构造SQL语句时设置的占位符)来绑定值
// placeholder: 参数占位符的名称
// val:要绑定的值
void bindValue(const QString &placeholder,const QVariant &val,QSql::ParamType paramType = QSql::In);
// 功能:通过位置来帮实际值
// pos: 是参数的位置,从1开始计数
// val: 要绑定的值
void QSqlQuery::bindValue(int pos,const QVariant &val,QSql::ParamType paramType = QSql::In);
// 功能:按照建表时成员的顺序绑定
void addBindValue(const QVariant &val,QSql::ParamType paramType = QSql::In);
通过prepare先将SQL语句准备好,在准备时实际值可以先用其他符号占用,然后通过bindValue来绑定实际值,通过命名绑定和位置绑定都可以,绑定好之后,调用exec执行
在audioPage页面新增加⼀个按钮,objectName修改为:insert,并添加槽函数,执行如下代码
cpp
// qqmusic.h 中新增
void on_insert_clicked();
// qqmusic.cpp 中新增
void QQMusic::on_insert_clicked()
{
QSqlQuery query;
// 通过命名绑定
query.prepare("INSERT INTO student(name, age, gpa) VALUES(:name, :age,:gpa)");
query.bindValue(":name", "张三");
query.bindValue(":age", 19);
query.bindValue(":gpa", 3.5);
if(!query.exec())
{
qDebug()<<"学⽣表插⼊数据:"<<query.lastError().text();
return;
}
qDebug()<<"插⼊数据成功!!!";
// 位置绑定
query.prepare("INSERT INTO student(name, age, gpa) VALUES(?, ?, ?)");
query.bindValue(0, "李四");
query.bindValue(1, 20);
query.bindValue(2, 4.0);
if(!query.exec())
{
qDebug()<<"学⽣表插⼊数据:"<<query.lastError().text();
return;
}
qDebug()<<"插⼊数据成功!!!";
query.prepare("INSERT INTO student(name, age, gpa) VALUES(?, ?, ?)");
query.addBindValue("王五");
query.addBindValue(20);
query.addBindValue(3.8);
if(!query.exec())
{
qDebug()<<"学⽣表插⼊数据:"<<query.lastError().text();
return;
}
qDebug()<<"插⼊数据成功!!!";
}
查询数据
SQLite的查询语句语法如下:
cpp
// 查询每条记录指定列的值
SELECT column1, column2, columnN FROM table_name;
// 查询结果为表中所有记录
SELECT * FROM table_name;
构造好SQL语句,使⽤QSQLQuery的对象query执行SQL语句,查询结果可以通过query获取
cpp
// 功能:将查询结果的当前⾏指针向后移动⼀⾏,如果移动后有记录则返回true,否则返回false
// 利⽤该⽅法,搭配while循环,可获取到所有查询记录
bool next();
// 功能:获取查询记录中索引为index的域的值
// 查询结果按照select语句后所查询字段顺序,从左往右基于0开始编号,依次递增
// select name, age, gpa from student;
// 每条查询结果中,name的索引为0 age的索引为1, gpa的索引为2
QVariant value(int index) const;
// 功能:根据查询记录中,name字段对应的值,如果名字不匹配,将返回⼀个⾮法的QVariant
QVariant value(const QString &name) const;
在audioPage页面新增加⼀个按钮,objectName修改为:select,并添加槽函数,执行如下代码:
cpp
// qqmusic.h 中新增
void on_select_clicked();
// qqmusic.cpp 中新增
void QQMusic::on_select_clicked()
{
// 按照字段查询
QString sql("SELECT name, age, gpa FROM student");
QSqlQuery query;
if(!query.exec(sql))
{
qDebug()<<"查询学⽣表student失败:"<<query.lastError().text();
return;
}
// 按照字段名称获取查询到的结果
while(query.next())
{
qDebug()<<query.value("name").toString()<<" "<<query.value("age").toInt()<<" "<<query.value("gpa").toDouble();
}
qDebug()<<"===================================";
// 查询所有列,按照索引获取
sql = "SELECT * FROM student";
if(!query.exec(sql))
{
qDebug()<<"查询学⽣表student失败:"<<query.lastError().text();
return;
}
// 按照字段名称获取查询到的结果
while(query.next())
{
qDebug()<<query.value(0).toInt()<<" "<<query.value(1).toString()<<" "<<query.value(2).toInt()<<" "<<query.value(3).toDouble();
}
}
更新数据
cpp
-- 将指定条件所对应记录的相应列修改为指定值
UPDATE table_name
SET column1 = value1, column2 = value2...., columnN = valueN
WHERE [condition];
UPDATE student SET age = 21 WHERE name='王五';
在audioPage页面新增加⼀个按钮,objectName修改为:update,并添加槽函数,执行如下代码
cpp
// qqmusic.h 中新增
void on_update_clicked();
// qqmusic.cpp 中新增
void QQMusic::on_update_clicked()
{
QSqlQuery query;
query.prepare("UPDATE student SET age = :age WHERE name = :name");
query.bindValue(":age", 18);
query.bindValue(":name", "王五");
if(!query.exec())
{
qDebug()<<"更新学⽣信息失败:"<<query.lastError().text();
return;
}
qDebug()<<"查看更新结果:";
QString sql("SELECT * FROM student WHERE name = '王五'");
if(!query.exec(sql))
{
qDebug()<<"查询学⽣信息失败:"<<query.lastError().text();
return;
}
while(query.next())
{
qDebug()<<query.value(0).toInt()<<" "
<<query.value(1).toString()<<" "
<<query.value(2).toInt()<<" "
<<query.value(3).toDouble();
}
}
删除数据
cpp
DELETE FROM table_name WHERE [condition];
// ⽐如:删除名字为王五的同学
DELETE FROM student WHERE name = '王五';
在audioPage页面新增加⼀个按钮,objectName修改为:delete,并添加槽函数,执行如下代码:
cpp
// qqmusic.h 中添加
void on_deleteRecord_clicked();
// qqmusic.cpp 中添加
void QQMusic::on_deleteRecord_clicked()
{
QSqlQuery query;
query.prepare("DELETE FROM student WHERE name = ?");
query.addBindValue("王五");
if(!query.exec())
{
qDebug()<<"删除学⽣信息失败:"<<query.lastError().text();
return;
}
qDebug()<<"查看删除结果:";
QString sql("SELECT * FROM student");
if(!query.exec(sql))
{
qDebug()<<"查询学⽣信息失败:"<<query.lastError().text();
return;
}
while(query.next())
{
qDebug()<<query.value(0).toInt()<<" "
<<query.value(1).toString()<<" "
<<query.value(2).toInt()<<" "
<<query.value(3).toDouble();
}
}
操作完成之后,最后别忘记关闭数据库连接,否则会造成资源泄漏
QQMusic中数据库支持
数据库初始化
cpp
// qqmusic.h 中新增
#include <QSqlDatabase>
QSqlDatabase sqlite;
// qqmusic.cpp 中新增
void QQMusic::initSQLite()
{
// 1. 创建数据库连接
sqlite = QSqlDatabase::addDatabase("QSQLITE");
// 2. 设置数据库名称
sqlite.setDatabaseName("QQMusic.db");
// 3. 打开数据库
if(!sqlite.open())
{
QMessageBox::critical(this, "打开QQMusicDB失败",
sqlite.lastError().text());
return;
}
qDebug()<<"SQLite连接成功,并创建 [QQMusic.db] 数据库!!!";
// 4. 创建数据库表
QString sql = ("CREATE TABLE IF NOT EXISTS musicInfo(\
id INTEGER PRIMARY KEY AUTOINCREMENT,\
musicId varchar(200) UNIQUE,\
musicName varchar(50),\
musicSinger varchar(50),\
albumName varchar(50),\
duration BIGINT,\
musicUrl varchar(256),\
isLike INTEGER,\
isHistory INTEGER)"
);
QSqlQuery query;
if(!query.exec(sql))
{
QMessageBox::critical(this, "创建数据库表失败",query.lastError().text());
return;
}
qDebug()<<"创建 [musicInfo] 表成功!!!";
}
QQMusic::QQMusic(QWidget *parent)
: QWidget(parent)
, ui(new Ui::QQMusic)
, currentIndex(-1)
{
ui->setupUi(this);
initUi();
// 初始化数据库
initSQLite();
initPlayer();
connectSignalAndSlots();
}
歌曲信息写入数据库
当程序退出的时候,通过musicList获取到所有music对象,然后将music对象写入数据库
cpp
// musiclist.h 中新增
// 所有歌曲信息更新到数据库
void writeToDB();
// musiclist.cpp 中新增
void MusicList::writeToDB()
{
for(auto music : musicList)
{
// 让music对象将⾃⼰写⼊数据库
music.insertMusicToDB();
}
}
// music.h 中新增
// 将当前Music对象更新到数据库
void insertMusicToDB();
// music.cpp 中新增
#include <QSqlQuery>
#include <QSqlError>
void Music::insertMusicToDB()
{
// 1. 检测music是否在数据库中存在
QSqlQuery query;
// 当SELECT 1...查询到结果后,我们需要知道是否存在
// SELECT EXISTS(⼦查询) : ⼦查询中如果有记录,SELECT EXISTS返回TRUE
// 如果⼦查询中没有满⾜条件的记录, SELECT EXISTS返回FALSE
query.prepare("SELECT EXISTS (SELECT 1 FROM MusicInfo WHERE musicId = ?)");
query.addBindValue(musicId);
if(!query.exec())
{
qDebug()<<"查询失败: "<<query.lastError().text();
return;
}
if(query.next())
{
bool isExists = query.value(0).toBool();
if(isExists)
{
// musicId的歌曲已经存在
// 2. 存在:不需要再插⼊musci对象,此时只需要将isLike和isHistory属性进行更新
query.prepare("UPDATE MusicInfo SET isLike = ?, isHistory = ? WHERE musicId = ?");
query.addBindValue(isLike? 1 : 0);
query.addBindValue(isHistory? 1 : 0);
query.addBindValue(musicId);
if(!query.exec())
{
qDebug()<<"更新失败: "<<query.lastError().text();
}
qDebug()<<"更新music信息: "<<musicName<<" "<<musicId;
}
else
{
// 3. 不存在:直接将music的属性信息插⼊数据库
query.prepare("INSERT INTO MusicInfo(musicId, musicName, musicSinger, albumName, musicUrl, duration, isLike, isHistory) VALUES(?,?,?,?,?,?,?,?)");
query.addBindValue(musicId);
query.addBindValue(musicName);
query.addBindValue(musicSinger);
query.addBindValue(musicAlbumn);
query.addBindValue(musicUrl.toLocalFile());
query.addBindValue(duration);
query.addBindValue(isLike ? 1 : 0);
query.addBindValue(isHistory? 1 : 0);
if(!query.exec())
{
qDebug()<<"插⼊失败: "<<query.lastError().text();
return;
}
qDebug()<<"插⼊music信息: "<<musicName<<" "<<musicId;
}
}
}
// qqmusic.cpp 中新增
void QQMusic::on_quit_clicked()
{
// 更新数据库
musicList.writeToDB();
// 关闭数据库连接
sqlite.close();
// 关闭窗⼝
close();
}
程序启动时读取数据库恢复歌曲数据
在程序启动时,从数据库中读取到歌曲的信息,将歌曲信息设置到musicList中,然后让likePage、localPage、recentPage将musicList中个歌曲更新到各自页面中
从数据库读取歌曲数据的操作,应该让MusicList类完成,因为该类管理所有的Music对象
cpp
// musiclist.h 中新增
void readFromDB();
// musiclist.cpp 中新增
#include <QSqlQuery>
#include <QSqlError>
void MusicList::readFromDB()
{
QString sql("SELECT musicId, musicName, musicSinger, albumName,\
duration, musicUrl, isLike, isHistory \
FROM musicInfo");
QSqlQuery query;
if(!query.exec(sql))
{
qDebug()<<"数据库查询失败";
return;
}
while(query.next())
{
Music music;
music.setMusicId(query.value(0).toString());
music.setMusicName(query.value(1).toString());
music.setMusicSinger(query.value(2).toString());
music.setMusicAlbum(query.value(3).toString());
music.setMusicDuration(query.value(4).toLongLong());
music.setMusicUrl(query.value(5).toString());
music.setIsLike(query.value(6).toBool());
music.setIsHistory(query.value(7).toBool());
musicList.push_back(music);
}
}
// qqmusic.h 中新增
void initMusicList();
// qqmusic.cpp 中新增
void QQMusic::initMusicList()
{
// 1. 从数据库读取歌曲信息
musicList.readFromDB();
// 2. 更新CommonPage⻚⾯
// 设置CommonPage的信息
ui->likePage->setMusicListType(PageType::LIKE_PAGE);
ui->likePage->reFresh(musicList);
ui->localPage->setMusicListType(PageType::LOCAL_PAGE);
ui->localPage->reFresh(musicList);
ui->recentPage->setMusicListType(PageType::HISTORY_PAGE);
ui->recentPage->reFresh(musicList);
}
QQMusic::QQMusic(QWidget *parent)
: QWidget(parent)
, ui(new Ui::QQMusic)
, currentIndex(-1)
{
// ...
// 初始化数据库
initSQLite();
// 加载数据库歌曲⽂件
initMusicList();
// ...
}
void QQMusic::initUi()
{
//...
// 设置CommonPage的信息
ui->likePage->setCommonPageUI("喜欢", ":/images/ilikebg.png");
ui->localPage->setCommonPageUI("本地和下载", ":/images/localbg.png");
ui->recentPage->setCommonPageUI("最近播放", ":/images/recentbg.png");
// ...
}
边角问题处理
更换主窗口图标
更换窗口图标,在主界面显示时,在标题栏显示设置的图标
cpp
void QQMusic::initUi()
{
this->setWindowFlag(Qt::FramelessWindowHint);
setAttribute(Qt::WA_TranslucentBackground);
setWindowIcon(QIcon(":/images/tubiao.png")); // 设置主窗⼝图标
// ...
}
处理最大化、最小化按钮
由于窗口中控件并非全部基于Widget布局,有些控件的位置是计算死得,窗口最大化时有些控件可能无法适配尺寸,因此禁止窗口最大化
cpp
// qqmusic.h 中新增
void on_skin_clicked();
void on_max_clicked();
void on_min_clicked();
// qqmusic.cpp 中新增
void QQMusic::on_skin_clicked()
{
QMessageBox::information(this, "温馨提⽰", "⼩哥哥正在加班紧急⽀持中...");
}
void QQMusic::on_min_clicked()
{
showMinimized();
}
void QQMusic::initUi()
{
this->setWindowFlag(Qt::FramelessWindowHint);
setAttribute(Qt::WA_TranslucentBackground);
setWindowIcon(QIcon(":/images/tubiao.png")); // 设置主窗⼝图标
ui->max->setEnabled(false);
// ...
}
歌词按钮的样式
cpp
#lrcWord:hover
{
background-color:rgba(24,233,114,0.5);
}
#lrcWord
{
border:none;
background-image:url(":/images/ci.png");
background-repeat:no-repeat;
background-position: center center;
}
另外,LrcPage页面中的按钮,当鼠标放上去时,可以显示向上收拾样式,更容易识别出此处是按钮。具体操作,选中LrcPage.ui界面中hideBtn按钮,然后在属性页面找到cursor属性,然后选择指向手势
CommonPage中滚动条格式
CommonPage中QScorllArea垂直滚动条的样式不太好看,可以借助CommonPage中QListWidget滚动条样式设置:
cpp
QScrollBar:vertical
{
border:none;
width:10px;
height:10px;
background-color:rgb(250,250,250);
margin: 0px 0px 0px 0px;
}
QScrollBar::handle:vertical
{
width:10px;
background-color:rgb(225,225,225);
border-radius:5px;
min-heigth:20px;
}
BtForm上动画问题
当播放不同页面歌曲时,BtForm按钮上的跳动动画应该跟随播放页面变化而变化,即那个page页面播放,就应该让该页面的对应BtForm上的动画显示,其余BtForm按钮上的动画隐藏,这样跳动的音符始终就可以标记当前正在播放的页面。
QQmusic类中currentPage标记当前播放页面,QStackedWidget中提供了通过页面找索引的方法,即currentPage可以找到其在层叠窗口中的索引,该索引与BtForm中的pageId是对应的。因此在qqMusic中定义updateBtFormAnimal函数,该函数实现原理如下:
- 获取currentPage在stackedWidget中的索引
- 获取QQMusic中所有BtFrom*的元素,保存到btFroms
- 遍历btFroms,如果那个按钮的pageId等于currentPage的索引,则显示该按钮的动画,否则隐藏
**注意:**所有修改currentPage位置之后都需要调用updateBtFormAnimal()函数
cpp
// btform.h 修改
// 添加isShow参数
void BtForm::showAnimal(bool isShow);
// btform.cpp 修改
void BtForm::showAnimal(bool isShow)
{
// 当按钮点击时,根据isShow状态显⽰或隐藏动画
if(isShow)
{
ui->lineBox->show();
}
else
{
ui->lineBox->hide();
}
}
// qqmusc.h 新增
void updateBtformAnimal();
// qqmusic.cpp 新增
void QQMusic::updateBtformAnimal()
{
// 获取currentPage在stackedWidget中的索引
int index = ui->stackedWidget->indexOf(currentPage);
if(-1 == index)
{
qDebug()<<"该⻚⾯不存在";
return;
}
// 获取QQMusci界⾯上所有的btForm
QList<BtForm*> btForms = this->findChildren<BtForm*>();
for(auto btForm : btForms)
{
if(btForm->getPageId() == index)
{
// 将currentPage对⻥竿的btForm找到了
btForm->showAnimal(true);
}
else
{
btForm->showAnimal(false);
}
}
}
void QQMusic::initUi()
{
// ...
// 将localPage设置为当前⻚⾯
ui->stackedWidget->setCurrentIndex(4);
currentPage = ui->localPage;
// 本地下载BtForm动画默认显⽰
ui->local->showAnimal(true);
// ...
}
点击BtForm偶尔窗口乱移问题
正常情况下,当按钮在非BtForm按钮上点击时,左键长按拖拽窗口才能移动。但是当点击BtForm按钮时,偶尔也能看到窗口移动,估计可能在BtForm上点击时,由于手抖或者其他原因,该次点击时间被qt识别成鼠标移动事件处理了,因此导致窗口移动了
**解决方案:**在QQMusic类中设置⼀个成员变量isDrag,当鼠标点击时间发生时,如果点在BtForm按钮上则将isDrag设置为false,否则设置为true,此时如果鼠标长按滚动时,再移动窗口
cpp
// qqmusic.h 中新增
bool isDrag; // 为true时候窗⼝才能拖拽
// qqmusic.cpp 中新增
void QQMusic::mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() == Qt::LeftButton && isDrag)
{
move(event->globalPos() - dragPos);
qDebug()<<"mouse move";
return;
}
QWidget::mouseMoveEvent(event);
}
void QQMusic::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
isDrag = true;
// 获取⿏标相对于电脑屏幕左上⻆的全局坐标
dragPos = event->globalPos() - geometry().topLeft();
return;
}
QWidget::mousePressEvent(event);
}
void QQMusic::onBtFormClick(int pageId)
{
// 清除之前btForm按钮的颜⾊背景
// 获取所有的BtForm按钮
QList<BtForm*> btFormList = this->findChildren<BtForm*>();
for(auto btForm : btFormList)
{
if(btForm->getPageId() != pageId)
{
btForm->clearBackground();
btForm->showAnimal(false);
}
else
{
btForm->showAnimal(true);
}
}
ui->stackedWidget->setCurrentIndex(pageId);
qDebug()<<"切换⻚⾯"<<pageId;
// 点击btForm时,窗⼝不能拖拽
isDrag = false;
}
点击添加按钮歌曲重复加载问题
当点击加载按钮对同⼀个目录下歌曲重复加载时,仍能加载到程序中并更新到数据库,正常情况下相同目录中的歌曲只能在播放器中加载⼀份。出现该问题的原因,是加载时,未对已经存在的歌曲文件进行过滤导致,因此在addMusicsByUrl中检测下,如果歌曲已经存在则无需解析,也无需加载
如何知道歌曲是否已经被加载过了呢?最简单的方式是直接到musicList中查找,但是musicList为线性的QVector,循环遍历效率太低,因此可以借助QSet容器(相当于C++标准库中的unordered_set),将歌曲的路径保存⼀份(同⼀个电脑上,文件路径不可能重复),在进⾏歌曲加载时,先检测歌曲⽂件是否存在,如果不存在则添加否则不添加
cpp
// musiclist.h 新增
#include <QSet>
QSet<QString> musicPaths;
// musiclist.cpp 新增
void MusicList::addMusicsByUrl(const QList<QUrl> &musicUrls)
{
// 将所有的⾳乐放置到musicList
for(auto e : musicUrls)
{
QString musicPath = e.toLocalFile();
// 检测歌曲是否存在,如果再才能添加
if(musicPaths.contains(musicPath))
continue;
// 歌曲还没有加载过,将其解析并添加到歌曲列表
musicPaths.insert(musicPath);
// 如果musicUrl是⼀个有效的歌曲⽂件,再将其添加到歌曲列表中
// 检测歌曲⽂件的MIME类型
QMimeDatabase mimeDB;
QMimeType mimeType = mimeDB.mimeTypeForFile(musicPath);
QString mime = mimeType.name();
// ...
}
}
点击添加按钮加载歌曲后,在点击加载时,已经在歌曲列表中的歌曲不会重复加载了
但是当播放器重启后,点击加载还是能将相同目录下的歌曲加载进来,再点击加载时就加载不进来了。这是因为在程序启动时,从数据库中读取歌曲信息添加到musicList中,但是没有将歌曲的路径保存在musicPaths中,所以歌曲是添加到程序中了,但是musicPaths中还是空的,所以再点击加载按钮后,还是会将相同目录下的歌曲加载到程序中,此时musicPaths不为空了,再点击加载时相同目录下的歌曲肯定就无法添加了
因此,当程序启动时,读取数据库歌曲信息恢复musicList时,需要将歌曲的路径也要往musicPaths中保存⼀份
cpp
void MusicList::readFromDB()
{
QSqlQuery query;
query.prepare("select musicId, musicName, musicSinger, albumName, musicUrl, duration, isLike, isHistory from MusicInfo");
if(!query.exec())
{
qDebug() << "select error !!!";
return;
}
while(query.next())
{
Music music;
music.setMusicId(query.value(0).toString());
music.setMusicName(query.value(1).toString());
music.setMusicSinger(query.value(2).toString());
music.setMusicAlbumn(query.value(3).toString());
music.setMusicUrl("file:///" + query.value(4).toString());
music.setMusicDuration(query.value(5).toLongLong());
music.setIsLike(query.value(6).toBool());
music.setIsHistory(query.value(7).toBool());
musicList.push_back(music);
// 将数据库中保存过的歌曲文件恢复至musicPaths中
musicPaths.insert(music.getMusicUrl().toLocalFile());
}
}
添加系统托盘
当点击关闭按钮时,不让播放器直接退出,而是将窗口隐藏掉,窗口的图标在系统托盘位置,当在播放器图标上右键单击时,弹出菜单让用户选择是显示窗口还是继续退出
cpp
// qqmusic.cpp 中新增
void quitQQMusic();
// qqmusic.cpp 中新增
#include <QSystemTrayIcon>
#include <QMenu>
void QQMusic::initUi()
{
// ...
this->setGraphicsEffect(shadowEffect);
// 添加托盘
// 创建托盘图标
QSystemTrayIcon *trayIcon = new QSystemTrayIcon(this);
trayIcon->setIcon(QIcon(":/images/tubiao.png"));
// 创建托盘菜单
QMenu *trayMenu = new QMenu(this);
trayMenu->addAction("还原", this, &QWidget::showNormal);
trayMenu->addSeparator();
trayMenu->addAction("退出", this, &QQMusic::quitQQMusic);
// 将托盘菜单添加到托盘图标
trayIcon->setContextMenu(trayMenu);
// 显⽰托盘
trayIcon->show();
// 设置BtForm图标 & ⽂本信息
// ...
}
void QQMusic::on_quit_clicked()
{
// 点击关闭按钮时,程序不退出,隐藏掉
// ⽤⼾可以从系统托盘位置选择显⽰或者关闭
hide();
}
void QQMusic::quitQQMusic()
{
// 歌曲信息写⼊数据库
musicList.writeToDB();
// 断开与SQLite的链接
sqlite.close();
// 关闭窗⼝
close();
}
保证程序只运行⼀次
现在每点击⼀次QQMusic.exe,都会创建⼀个QQMusic的实例,即同⼀个机器上可以运行多份程序实例,多个实例同时运行有以下缺陷:
- 多个实例同时运行可能会导致资源浪费,如内存、CPU效率等
- 如果应用程序涉及对共享数据的修改,多个程序同时运行可能会导致数据不⼀致问题
- 若多个实例尝试访问同⼀资源时,如文件、数据库等,可能会导致冲突或错误
- 另外,用户体验不是很好,多个实例操作时容易混淆
因此有时会禁止程序多开,即⼀个应用程序只能运行⼀个实例,也称为单实例应用程序或单例应用程序
在Qt中,禁止程序多开的方式有好几种,此处采用共享内存实现
共享内存是操作系统中的概念,是进程间通信的⼀种机制。由于相同key值的共享内存只能存在⼀份,因此在程序启动时可以检测共享内存是否已经被创建,如果已经创建则说明程序已经在运行,否则程序还没有运行
cpp
#include <QMessageBox>
#include <QSharedMemory>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 创建共享内存
QSharedMemory sharedMem("QQMusic");
// 如果共享内存已经被占⽤,说明已经有实例在运⾏
if (sharedMem.attach())
{
QMessageBox::information(nullptr, "QQMusic", "QQMusic已经在运⾏...");
return 0;
}
sharedMem.create(1);
QQMusic w;
w.show();
return a.exec();
}
禁止qDebug()输出
要逐个删除程序中qDebug的打印太⿇烦,可以再配置文件中通过添加以下语句,禁止qDebug输出:
cpp
# ban qDebug output
DEFINES += QT_NO_DEBUG_OUTPUT
项目打包
程序编译好之后,将exe可执⾏程序直接拷贝给同学或朋友运行,为什么运行不了?
Qt可执⾏程序在运行的时候,需要依赖Qt框架中的⼀些库文件,如果对方及其上之前未安装Qt环境,点击可执行程序运行时,会提示缺少xxx.dll动态库信息等。为了让开发好的Qt可执⾏程序在未安装Qt环境的机器上也可以运行,就需要对项目进行打包,打包的过程会将exe可执行程序运行时所需的依赖文件全部整合到⼀起,将打包好的包⼀起发给对端,双击exe可执行程序时就可以执行。
**注意:**打包时exe需要用release版本,debug是调试版本,release版本编译器会去除调试信息,并会对工程进行优化等操作,使程序体积更小,运行效率更高。
windeployqt打包工具
windeployqt是Qt提供的⼀个工具,用于自动收集并复制运行Qt应用程序所需的动态链接库(.dll⽂件)及其他资源(如插件、QML模块等)到可执行文件所在的目录。这样你就可以将应用程序和这些依赖项⼀起打包,确保在没有Qt环境的其他机器上也能运行
- 自动收集依赖项:windeployqt 会分析你的Qt应用程序,确定它所依赖的Qt库文件(如Qt6Core.dll,Qt6Widgets.dll),并将这些文件复制到应用程序的目录
- 处理插件和QML模块:如果你的应用程序使用了Qt的插件(如平台插件qwindows.dll或图形驱动插件等),windeployqt也会将这些插件⼀并打包。对于使用QML的应用程序,它也会自动收集必要的QML模块
- 处理资源文件:如果你的应用程序包含了Qt的资源文件(如图标、翻译文件等),它也会确保这些资源正确包含在最终的应用程序中。
打包步骤:
- 配置好Qt环境变量
- 选择以release方式编译程序。编译好之后,在工程目录上⼀层会生成包含release字段的文件夹,文件夹内部就有release模式的可执行程序
- 将新建⼀个文件夹,命名为QQMusic,将release模式可执行程序拷贝到QQMusic
- 进入QQMusic,在该文件夹内部,按shift,然后鼠标右键单击,弹出菜单中选择"在此处打开Powershell窗口(S)",在弹出窗口中输⼊windeployqt.\QQMusic.exe,windeployqt工具就会自动完成打包
如果环境变量没有配置好,可能会出现以下问题:
qt安装后对应构建器的环境变量没有配置
本项目使用mingw64构建,因此将qt安装目录下
D:\ApplyTool\ProgramTool\QT\5.14.2\mingw73_64\bin 目录配置到环境变量
配置过程中如果报警告:Cannot find GCC installation directory...
是g++工具目录没有配置到环境变量中,qt环境安装后,g++可能在其他目录,我电脑上是在:D:\ApplyTool\ProgramTool\QT\Tools\mingw730_64\bin 目录下,将该目录配置到环境变量中
配置好之后,将该目录压缩之后,发给对方,对方收到之后直接解压,点击exe之后就可以运行
项目总结
项目中用到了以下技术:
- Qt designer界面设计,基于QWidget的项目布局,QSS样式设置、自定义控件
- 信号槽机制
- 媒体播放技术
- 简单动画效果
- SQLite数据库使用
- QPainter绘图
- 文件操作
后续可扩展性:
- 支持换肤功能
- 增加登录/注册/账户管理
- 支持简单的本地搜索
- 增加网络模块,完善在线歌曲和推荐系统模块
个人收获:
- 能将之前学过Qt、C++知识点能串联起来,对知识的理解更加清晰,比如:如何让自定义类支持范围for、lambda表达式等更进一步的掌握。
- 查看帮助文档能力加强。之前自己不习惯看英文档,但是做项⽬时许多接⼝需要自己通过Qt参考数据去挖掘,比如:QMediaPlayer、QMediaPlaylist、QSqlDatabase等,结合在线翻译耐心研究,慢慢查阅文档能力就培养起来了。
- 利用Qt Creator中的设计师(Designer),可以方便地进行界面的拖拽设计。深入理解各种控件的属性和用法,学会使用布局管理器来控制界面的排版
- 解决和排查问题能能力加强。Qt的官方文档非常全面,是学习和解决问题的宝库,之前遇到bug都比较慌,不知道如何下手,现在遇到bug之后,我会看bug现象,是崩溃还是逻辑不对,然后仔细分析,有些简单的bug通过分 析之后便能发现代码错误位置;有些隐藏比较深的bug,通过添加打印信息快速定位 bug,然后解决,实在不行下断点单步调试。
- 在设计应用时,应该尽可能地将功能模块化,提高代码的重用性。利用Qt的信号与槽机制,可以实现模块间的低耦合通信。