音乐播放器

目录

[1. 项目演示](#1. 项目演示)

2.界面开发

2.1界面简要分析

[2.2 界面开发](#2.2 界面开发)

2.2.1创建工程

2.2.2主界面布局设计

窗口主框架设计

head内部设计

【headLeft】

【headRight】

【searchBox】

【settingBox】

bodyLeft内部布局

leftBox内部布局

bodyRight布局

stackedWidget内部增加页面

ControlBox内部布局

play1内部:

play2内部:

play3内部:

[2.3 界面美化](#2.3 界面美化)

2.3.1主窗口设定

[2.3.2 添加图片资源](#2.3.2 添加图片资源)

[2.3.3 head处理](#2.3.3 head处理)

[2.3.4 播放控制区处理](#2.3.4 播放控制区处理)

[3. 自定义控件](#3. 自定义控件)

[3.1 BtForm](#3.1 BtForm)

[3.1.1 BtForm界面设计](#3.1.1 BtForm界面设计)

[3.1.2 BtForm类中实现](#3.1.2 BtForm类中实现)

[3.2 推荐页面](#3.2 推荐页面)

[3.2.1 推荐页面分析](#3.2.1 推荐页面分析)

[3.2.2 推荐页布局](#3.2.2 推荐页布局)

[3.2.3 自定义recBox](#3.2.3 自定义recBox)

[3.2.4 自定义recBoxItem](#3.2.4 自定义recBoxItem)

[3.2.5 RecBox添加RecBoxItem](#3.2.5 RecBox添加RecBoxItem)

[3.2.6 RecBox中btUp和btDown按钮clicked处理](#3.2.6 RecBox中btUp和btDown按钮clicked处理)

[3.3 自定义CommonPage](#3.3 自定义CommonPage)

[3.3.1 CommonPage页面分析](#3.3.1 CommonPage页面分析)

[3.3.2 CommonPage页面布局](#3.3.2 CommonPage页面布局)

[3.3.3 CommonPage界面设置和显示](#3.3.3 CommonPage界面设置和显示)

[3.4 自定义ListItemBox](#3.4 自定义ListItemBox)

[3.4.1 ListItemBox页面分析](#3.4.1 ListItemBox页面分析)

[3.4.2 ListItemBox页面布局](#3.4.2 ListItemBox页面布局)

[3.4.3 ListltemBox显示测试](#3.4.3 ListltemBox显示测试)

[3.4.4 支持hover效果](#3.4.4 支持hover效果)

[3.5 自定义MusicSlider](#3.5 自定义MusicSlider)

[3.6 自定义VolumeTool](#3.6 自定义VolumeTool)

[3.6.1 VolumeTool控件分析](#3.6.1 VolumeTool控件分析)

[3.6.2 VolumeTool界面布局](#3.6.2 VolumeTool界面布局)

[3.6.3 界面设置](#3.6.3 界面设置)

[3.6.4 界面创建及弹出](#3.6.4 界面创建及弹出)

[3.6.5 绘制三角](#3.6.5 绘制三角)

[4. 音乐管理](#4. 音乐管理)

[4.1 音乐加载](#4.1 音乐加载)

[4.2 MusicList类](#4.2 MusicList类)

[4.2.1 添加C++类MusicList](#4.2.1 添加C++类MusicList)

[4.2.2 歌曲对象存储](#4.2.2 歌曲对象存储)

[4.3 Music类](#4.3 Music类)

[4.3.1 Music类介绍](#4.3.1 Music类介绍)

[4.3.2 解析音乐文件元数据](#4.3.2 解析音乐文件元数据)

[4.4 音乐分类](#4.4 音乐分类)

[4.5 更新Music信息到ComonPage界面](#4.5 更新Music信息到ComonPage界面)

[4.6 CommonPage显示不足处理](#4.6 CommonPage显示不足处理)

[4.7 音乐收藏](#4.7 音乐收藏)

[4.7.1 我喜欢图标处理](#4.7.1 我喜欢图标处理)

[4.7.2 点击我喜欢按钮处理](#4.7.2 点击我喜欢按钮处理)

5.音乐播放控制

[5.1 QMediaPlayer类](#5.1 QMediaPlayer类)

[5.1.1 QMediaPlayer类说明](#5.1.1 QMediaPlayer类说明)

[5.1.2 属性和方法](#5.1.2 属性和方法)

[5.2 QMediaPlaylist类](#5.2 QMediaPlaylist类)

[5.2.1 QMediaPlaylist类介绍](#5.2.1 QMediaPlaylist类介绍)

[5.2.2 属性和方法](#5.2.2 属性和方法)

[5.3 歌曲播放](#5.3 歌曲播放)

[5.3.1 播放媒体和播放列表初始化](#5.3.1 播放媒体和播放列表初始化)

[5.3.2 播放列表设置](#5.3.2 播放列表设置)

[5.3.3 播放和暂停](#5.3.3 播放和暂停)

[5.3.4 上一曲和下一曲](#5.3.4 上一曲和下一曲)

[5.3.5 播放模式设置](#5.3.5 播放模式设置)

[5.3.6 播放所有](#5.3.6 播放所有)

[5.3.7 双击CommPage页面QListWidget项播放](#5.3.7 双击CommPage页面QListWidget项播放)

[5.3.8 最近播放同步](#5.3.8 最近播放同步)

[5.3.9 音量设置](#5.3.9 音量设置)

[5.3.10 当前播放时间和总时间更新](#5.3.10 当前播放时间和总时间更新)

[5.3.11 进度条处理[seek功能]](#5.3.11 进度条处理[seek功能])

[5.3.12 歌曲名称、歌手和封面图片同步](#5.3.12 歌曲名称、歌手和封面图片同步)

[5.4 lrc歌词同步](#5.4 lrc歌词同步)

[5.4.1 Irc歌词界面分析](#5.4.1 Irc歌词界面分析)

[5.4.2 Irc歌词界面布局](#5.4.2 Irc歌词界面布局)

[5.4.3 LrcPage显示](#5.4.3 LrcPage显示)

[5.4.4 LrcPage添加动画效果](#5.4.4 LrcPage添加动画效果)

[5.4.5 Irc歌词解析和同步](#5.4.5 Irc歌词解析和同步)

[6. 持久化支持](#6. 持久化支持)

[6.1 SQLite数据库介绍](#6.1 SQLite数据库介绍)

[6.2 QSqlDatabase类介绍](#6.2 QSqlDatabase类介绍)

[6.2.1 数据库连接和关闭](#6.2.1 数据库连接和关闭)

[6.2.2 创建表](#6.2.2 创建表)

[6.2.3 插入数据](#6.2.3 插入数据)

[6.2.4 查询数据](#6.2.4 查询数据)

[6.2.5 更新数据](#6.2.5 更新数据)

[6.2.6 删除数据](#6.2.6 删除数据)

[6.3 MiniMusic中数据库支持](#6.3 MiniMusic中数据库支持)

[6.3.1 数据库初始化](#6.3.1 数据库初始化)

[6.3.2 歌曲信息写入数据库](#6.3.2 歌曲信息写入数据库)

[6.3.3 程序启动时读取数据库恢复歌曲数据](#6.3.3 程序启动时读取数据库恢复歌曲数据)

[7. 边角问题处理](#7. 边角问题处理)

[7.1 更换主窗口图标](#7.1 更换主窗口图标)

[7.2 处理最大化、最小化按钮](#7.2 处理最大化、最小化按钮)

[7.3 歌词按钮的样式](#7.3 歌词按钮的样式)

[7.4 CommonPage中滚动条格式](#7.4 CommonPage中滚动条格式)

[7.5 BtForm上动画问题](#7.5 BtForm上动画问题)

[7.6 点击BtForm偶尔窗口乱移问题](#7.6 点击BtForm偶尔窗口乱移问题)

[7.7 点击添加按钮歌曲重复加载问题](#7.7 点击添加按钮歌曲重复加载问题)

[7.8 添加系统托盘](#7.8 添加系统托盘)

[7.9 保证程序只运行一次](#7.9 保证程序只运行一次)

[7.10 禁止qDebug()输出](#7.10 禁止qDebug()输出)

[7.11 项目打包](#7.11 项目打包)

[7.11.1 为什么要打包](#7.11.1 为什么要打包)

[7.11.2 windeployqt打包工具](#7.11.2 windeployqt打包工具)

[7.11.3 打包步骤](#7.11.3 打包步骤)

[8. 项目总结和面试](#8. 项目总结和面试)

[8.11 项目总结](#8.11 项目总结)

[8.3 项目扩展](#8.3 项目扩展)

附录


1. 项目演示

该项目是基于QT开发的音乐播放软件,界面友好,功能丰富。主要功能如下:

【窗口head部分】

  • 点击最小化按钮,窗口最小化
  • 点击最大化按钮,窗口无反应(即禁止窗口最大化)
  • 点击关闭按钮,程序退出
  • 点击皮肤按钮,更换皮肤(该功能暂未支持)
  • 搜索框搜索功能(该功能暂未支持)

【窗口body左侧】

  • 点击推荐按钮,窗口右侧显示:推荐Page
  • 点击电台按钮,窗口右侧显示:电台Page
  • 点击音乐馆按钮,窗口右侧显示:音乐馆Page
  • 点击我喜欢按钮,窗口右侧显示:收藏的音乐Page
  • 点击本地下载按钮,窗口右侧显示:本地音乐Page
  • 点击最近播放按钮,窗口右侧显示:最近播放Page

注意:左侧按钮,当光标悬停时会有不同颜色突出显示,当点击时会有绿色显示,并且按钮的

右侧有跳动的竖条。

【窗口右侧】

当窗口左侧不同按钮点击,在窗口右侧会展示不同的页面,本项目暂只支持了本地音乐、喜欢音乐、最近播放音乐的展示。具体功能如下:

  • 点击全部播放按钮,播放当前页面列表中所有音乐
  • 双击列表中某音乐,播放当前选中音乐
  • 点击心心支持收藏
  • 支持最近播放过音乐记忆

【播放控制区】

  • 支持seek功能,即拖拽到歌曲指定位置播放
  • 支持:随机、单曲循环、循环播放
  • 支持播放上一曲
  • 支持播放下一曲
  • 支持播放和暂停
  • 支持音量调节和静音
  • 支持歌曲总时长显示+当前播放时间显示
  • 支持LRC歌词同步显示

2.界面开发

2.1界面简要分析

界面上控件比较多,归类之后主要分为两部分:head区和body区。

head区域从左往右依次为:图标、搜索框、更换皮肤按钮、最小化&最大化&退出按钮。

body区域分为左侧种类选择区域和右侧Page展示区。

Body左侧区域有两部分组成:在线音乐和我的音乐,两部分内部的控件种类是相同的。

①说明区域,实际为QLabel

②自定义控件(按钮的扩展):图片+文本+动画

③同②,自定义控件(按钮的扩展):图片+文本+动画

④同②,自定义控件(按钮的扩展):图片+文本+动画

Body右侧区域由:Page区、播放进度、播放控制区三部分构成。

①Page区:歌曲信息页面,点击<或>具有轮播图效果

②播放进度:当前歌曲播放进度说明,支持seek功能,与播放控制区时间、以及LRC歌词是同步的

③播放控制区域:显示歌曲图片&名称&歌手、

播放模式&下一曲&播放暂停&上一曲&音量调节和静音&添加本地音乐

当前播放时间/歌曲总时长&弹出歌词窗口按钮

【Page区说明】

当点击body左侧不同按钮时,Page区域会显示不同的页面。

推荐按钮:

电台

音乐馆

我喜欢

本地下载

最近播放

Body右侧目前支持的4个页面结构,整体的布局是相同的,唯独Page区域显示的内容稍有区别。

推荐页面具有类似轮播图的动态效果:

整个页面内容可以分为上下两组:今日为你推荐、你的歌曲补给站。两组的布局实际是相同的,元素说明:

  • 上方显示1行,内部有4个推荐元素;下方显示2行,每行有4个推荐元素
  • 左右两侧一个按钮,点击后推荐内容会更换下一批,不停点击会循环推荐
  • 当鼠标悬停在推荐元素上时,推荐元素会向上移动,当鼠标离开时,又回到原位置
  • 当鼠标悬停在推荐元素上时,同时会出现小手图标,说明该推荐元素具有点击功能

该页面中内容也为自定义元素,后序页面实现时具体分析。

我喜欢、本地下载、最近播放类似下图:

这三个Page中布局、控件都是相同的,只是填充的数据不一样。每个Page中包含了多个控件,大致如下:

①QLabel:类型说明

②QLabel:图片显示

③QButton:播放全部按钮

④一组QLabel说明:音乐、歌手、专辑

⑤QListWidget:播放列表

可以通过自定义控件的方式,将①~5的控件集成到一起形成一个新的控件,方便复用,因此这三个Page属于同一个自定义类型的Page。

这六个页面,将来由QStackedWidget控件组织起来,就可以实现点击不同按钮,显示不同页面效果。

【歌词页面】

解析当前正在播放音乐的歌词,同步显示在界面上。

显示内容分为:歌曲信息、歌词部分、左上方收起隐藏按钮。

  • 歌曲信息由歌曲名称(QLabel)和歌手名称(QLabel)构成
  • 歌词部分展示当前在唱歌词(QLabel)和在唱部分前三行和后三行歌词(QLabel)展示,当前播放歌词突出显示
  • 点击收起按钮后,该页面会以动画的方式收起

当歌曲有LRC歌词时,播放时歌词会随播放时间自动调整;歌曲没有LRC歌词时,歌词部分显示空字符。

以上对本项目的界面进行了简单的说明,大家先有个初步了解,接下来利用QTDesigner完成界面的布局。

2.2 界面开发

2.2.1创建工程

创建一个基于QWidget的工程,选中生成form选项,将来界面部分主要使用QDesigner来设计。

2.2.2主界面布局设计

基于Widget局部

QT系统提供4种布局管理器:

  • QHBoxLayout:水平布局
  • QVBoxLayout:垂直布局
  • QGridLayout:栅格布局
  • QFormLayout:表单布局

由于一个widget中只能包含上述布局管理器中的一种,所以直接使用布局管理器来布局不是很灵活;

而一个widget中可以包含多个widget,在widget中的控件可以进行水平、垂直、栅格、表单等布局操作,非常灵活。

因此本项目基于Widget来进行布局。

窗口主框架设计

【主窗口的布局】

①选中MiniMusic,在弹出的属性中找到geometry属性,将窗口宽度修改为:1040,高度修改为700

②从控件区拖拽一个Widget到窗口区域,objectName修改为:background,选中miniMusic,然后

点击垂直布局,background就填充满了整个窗口。

为了看到效果,选中backroound控件,然后右键单击,弹出菜单中选择改变样式表,内部添加:

css 复制代码
background-color:gray;

点击OK就能看到灰色的背景效果了。

注意:此处的颜色效果仅为方便看到界面效果,等界面框架设计完成后,将所有的颜色清除掉,界面添加特定颜色。

整个窗口由head和body上下两部分组成。

直接拖两个Widget放到设计区,双击将名字修改为head和body;

修改背景颜色方便查看效果,head背景色修改为green,body背景色修改为pink;

css 复制代码
background-color:green;

background-color:pink;

head在上,body在下,然后选中background对象,点击垂直布局。

head和body平分了整个background,并且head和body的margin有间隔。再次选中background对

象,右侧属性部分下滑找到Layout,将Margni和Space修改为0

修完完成后,head和body之间的间隔就没有了。

但是head占区域过大,选中head对象,将head的minimumSize和maxmumSize属性的高度都调整为80,这样head的大小就固定了。

head内部设计

head内部由两部分构成,headLeft区域显示图标,headRight区域为搜索框和功能按钮区域。

拖两个widget到head中,将objectName修改为headLeft和headRight,背景颜色修改为:

css 复制代码
headLeft background-color:yellow;
headRight background-color:blue;

然后选中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,背景颜色分别修改为:

css 复制代码
SearchBox background-color:red;
SettingBox background-color:orange;

选中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

②将bodyLeft颜色修改为:

css 复制代码
bodyLeft background-color:#f0f0f0
bodyRight background-color:#f5f5f5

③选中body,点击水平布局,将bodyLeft的minimumSize和maxmumSize的宽度修改为200

④选中Body,将body的Margin和Spacing修改为0

bodyLeft内部布局

①拖拽一个Widget到bodyLeft,将objectName修改为leftBox,背景颜色修改为:background-color:pink;

②拖拽Vertical Spacer到bodyLeft

③选中leftBox,将minmumSize和maxmumSize的高度修改为400

④选中bodyLeft,点击垂直布局,并将bodyLeft的Margin和Spacing修改为0

leftBox内部布局

leftBox内部包含:在线音乐和我的音乐两部分。

①拖拽两个Widget到leftBox中,将objectName依次修改为:onlineMusic和myMusic

②颜色分别修改为:

css 复制代码
onlineMusic background-color:#f0f0f0
myMusic background-color:#f5f5f5

③选中leftBox,点击垂直布局,然后将Margin和Spacing设置为0

④onlineMusic 和myMusic内部的元素都是相同的,由一个QLabel和三个Widget构成,后期Widget

会替换为自定义按钮,此处先用Widget占位。因此分别向onlineMusic和myMusic内部拖拽一个QLabel和三个QWidget,并选中onlineMusic和myMusic点击垂直布局,然后将Margin和Spacing设置为0

bodyRight布局

bodyRight由层叠窗口、进度滑竿、播放控制区三部分组成。

①拖拽层叠窗口控件Stacked Widget,就在Widget控件上方到bodyRight中

②拖拽Widget到bodyRight,将objectName修改为processBar,将minimumSize和maximumSize的高度修改为30,背景颜色修改为绿色

③拖拽Widget到bodyRight,将objectName修改为controlBox,将minmumSize高度修改为60

④选中bodyRight,点击垂直布局,然后将bodyRight的Margin和Spacing修改为0

⑤为了能看到效果,将processBar颜色修改为:background-color:pink;

stackedWidget内部增加页面

stackedWidget默认会提供两个页面,还需添加四个页面。

在对象区域选中stackedWidget控件,然后右键单击弹出菜单中选择添加页:

以类似的方式添加添加4个页面,并修改每个页面的objectName如下:

总共六个页面,每个页面都有自己的索引,所以是从0开始的,将来切换页面时就是通过索引来切换的。

选中stackedWidget,然后右键单击,弹出菜单中选择:改变页顺序,在弹出的窗口中就能看到每个页面的索引

ControlBox内部布局

该区域内部由三部分组成:歌曲信息部分、播放控制部分、时间显示

①拖拽三个Widget到ControlBox中,将ObjectName依次修改为play1、play2、play3颜色依次修改为:

css 复制代码
play1 background-color:#FFFAFA;
play2 background-color:#F8F8FF;
play3 background-color:#FFFAF0;

②选中ControlBox,点击水平布局,将ControlBox的Margin和Spacing修改为0

play1内部:

拖拽3个QLabel,放置歌曲图片、歌手名和歌曲名字,调整好位置,将QLabel的objectName修改为:musicCover、musicName、musicSinger

然后选中playl,点击栅格布局

play2内部:

从左到右依次摆放6个按钮,按钮的minimumSize和maxmumSize均修改为30*30,将objectName从左往右依次修改为:playMode、playUp、Play、playDown、volume、addLocal;

然后选中play2,点击水平布局,并将play_2的Margin和Spacing修改为0

play3内部:

拖四个QLabel和一个按钮,调整大小位置,从左往右QLabel的objectName依次修改为:labelNull、currentTime、line、totalTime,按钮的objectName修改为lrcWord,按钮的maxmumSize的宽度和高度修改为30*30;

选中play3,点击水平布局,并将play2的Margin和Spacing修改为0

2.3 界面美化

2.3.1主窗口设定

仔细观察发现主窗口是没有标题栏,因此在窗口创建前,就需要设置下窗口的格式。

cpp 复制代码
QWidget::setWindowFlag(...); //设置窗⼝格式,⽐如创建⽆边框的窗⼝

由于窗口中控件比较多,这些控件将来都需要初始化,如果将所有代码放在miniMusic的构造函数中实现,将来会造成构造函数非常臃肿,因此在miniMusic类中添加initUl(方法来完成界面初始化工作。

cpp 复制代码
// minimusic.h文件中添加
void initUI();
// 添加完成后,光标放在函数名字上按 alt + Enter 组合键完成方法定义



// minimusic.cpp头文件中完成定义
void MiniMusic::initUI()
{
    // 设置无边框窗口,即窗口将来无标题栏
    setWindowFlag(Qt::WindowType::FramelessWindowHint);
}

添加完成后一定要在mimusic的构造函数中调用initUI()函数,否则设置不会生效。

运行后,发现有以下两个问题:

  • 窗口无标题栏,找不到关闭按钮,导致窗口无法关闭
  • 窗口无法拖拽

关闭窗口,可以先将光标放在任务栏中当前应用程序图标上,弹出的框中选择关闭,后序会实现关闭功能。

主界面无法拖动,此时只需要处理下鼠标单击(mousePressEvent和鼠标移动(mouseMoveEvent)事

件即可。

鼠标左键按下时,记录下窗口左上角和鼠标的相对位置

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

cpp 复制代码
// minimusic.h中添加
protected:
    // 重写QWidget类的鼠标点击和鼠标滚轮时间
    void mousePressEvent(QMouseEvent *event);
    void mouseMoveEvent(QMouseEvent *event);

    // 记录光标相对于窗口标题栏的相对距离
    QPoint dragPosition;


// minimusic.cpp中添加
void MiniMusic::mousePressEvent(QMouseEvent *event)
{
    // 拦截鼠标左键单击事件
    if(event->button() == Qt::LeftButton)
    {
        /*
        QPoint point = event->globalPos();// 鼠标按下事件发生时,光标相对于屏幕左上角的位置
        QPoint pos = this->frameGeometry().topLeft();   // 鼠标按下事件发生时,窗口左上角位置
        QRect poz = geometry(); //不包含边框及顶部标题区的范围
        QRect poa = frameGeometry();    // 包含边框及顶部标题区的范围
        QPoint cha = event->globalPos() - frameGeometry().topLeft(); // 即为鼠标按下时,窗口左上角和光标之间的距离差
        */
        // 想要窗口鼠标按下时窗口移动,只需要在mouseMoveEvent中,让光标和窗口左上角保持相同的位置差
        // 获取鼠标相对于屏幕左上角的全局坐标
        dragPosition = event->globalPos() - frameGeometry().topLeft();
        return;
    }
    // 剩下的交给系统正常处理
    QWidget::mousePressEvent(event);
}

void MiniMusic::mouseMoveEvent(QMouseEvent *event)
{
    if(event->buttons() == Qt::LeftButton)
    {
        // 更具鼠标移动更新窗口位置
        move(event->globalPos() - dragPosition);
        return;
    }
    // 剩下的交给系统正常处理
    QWidget::mouseMoveEvent(event);
}

给窗口添加阴影需要用到QGraphicsDropShadowEffect类,具体步骤如下:

  1. 创建QGraphicsDropShadowEffect类对象
  2. 设置阴影的属性。比如:设置阴影的偏移、颜色、圆角等
  3. 将阴影设置到具体对象上

在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);

注意:给窗口设置阴影效果时,需要将窗口标题无边框,背景设置为透明。

2.3.2 添加图片资源

添加一个qrc文件,将图片资源拷贝到工程目录下,并添加到工程中。

将之前布局时所有按钮的背景颜色全部清除掉,按照下面的风格重新设定

2.3.3 head处理

headLeft

css 复制代码
#headLeft
{
	background-color:#F0F0F0;/*背景颜色设置为浅灰色*/
}

headRight

css 复制代码
#headRight
{
	background-color:#F5F5F5;/*设置背景颜色为亮灰色*/
}

logo

css 复制代码
#logo
{
	border-radius:0px;
	background-image:url(:/images/Logo.png);
	background-repeat:no-repeat;
	border:none;
	background-position:center center;
}

lineEdit

css 复制代码
#lineEdit
{
	background-color:#E3E3E3;	/*设置背景颜色*/
	border-radius:17px;			/*设置四个角的圆角*/
	padding-left:17px;			/*内部文字到边的距离*/
}

settingBox

css 复制代码
/*类型选择器*/
QPushButton
{
	border-radius:0px;	/*设置按钮的边框圆角为0像素,实现直角边缘*/
	background-repeat:no-repeat;	/*背景图片不重复平铺*/
	border:none;	/*无边框*/
	background-position: center center;	/*背景图片放置在按钮的中心位置*/
}

/*悬停状态*/
QPushButton:hover
{
	background-color:rgba(230,0,0,0,5);	/*设置背景颜色为半透明的红色*/
}

skin

css 复制代码
background-image:url(:/images/skin.png);

max

css 复制代码
background-image:url(:/images/max.png);

min

css 复制代码
background-image:url(:/images/min.png);

quit

css 复制代码
background-image:url(:/images/quit.png);

bodyLeft

css 复制代码
#bodyLeft
{
	background-color:#F0F0F0;/*设置背景颜色为浅灰色*/
}

bodyRight

css 复制代码
#bodyRight
{
	background-color:#F5F5F5;	/*设置背景颜色为亮灰色*/
}

2.3.4 播放控制区处理

祛除playl、play2、play3的页面布局时设置的临时背景色。

将按钮上的文字全部去除,然后重新添加样式和图片。

play2

css 复制代码
QPushButton
{
	border:none;	/*去除边框*/
}

/*悬停状态*/
QPushButton:hover
{
	/*设置背景颜色为半透明的红色*/
	background-color:rgba(220,220,220,0.5);
}

playMode

css 复制代码
#playMode
{
	/* 背景图路径 */
	background-image:url(:/images/shuffle_2.png);
	/* 关键:禁止平铺,只显示一张原图 */
    background-repeat: no-repeat;
    /* 关键:让图片在控件内居中显示 */
    background-position: center center;
}

playUp

css 复制代码
#playUp
{
	background-image:url(:/images/up.png);
	background-repeat:no-repeat;
	background-position:center center;
}

play

css 复制代码
#play
{
	background-image:url(:/images/play3.png);
	background-repeat:no-repeat;
	background-position:center center;
}

playDown

css 复制代码
#playDown
{
	background-image:url(:/images/down.png);
	background-repeat:no-repeat;
	background-position:center center;
}

volume

css 复制代码
#volume
{
	background-image:url(:/images/volumn.png);
	background-repeat:no-repeat;
	background-position:center center;
}

addLocal

css 复制代码
#addLocal
{
	background-image:url(:/images/add.png);
	background-repeat:no-repeat;
	background-position:center center;
}

lrcWord

css 复制代码
#lrcWord
{
	background-image:url(:/images/ci.png);
	border:none;	/*去除边框*/
	background-repeat:no-repeat;
	background-position:center center;
}

QPushButton:hover
{
	/*设置背景颜色为半透明的红色*/
	background-color:rgba(220,220,220,0.5);
}

3. 自定义控件

3.1 BtForm

3.1.1 BtForm界面设计

添加一个新设计界面,命名为BtForm。

该控件实际由:图片、文字、动画三部分组成。图片和文字分别用QLabel展示,动画部分内部实际为4个QLabel。

①将BtForm的geometry的宽度和高度修改为200*35。

②拖一个Widget到btForm中,objectName修改为btStyle,将btForm的margin和Spacing设置为0.

③拖2个QLable和1个Widget到btStyle中,并将objectName依次修改为btlcon、btText、lineBoxbtlcon的minimumSize和maximumSize的宽度设置为30(为了看到效果可将颜色设置为red)

btText的minimumSize和maximumSize的宽度设置为9o(为了看到效果可将颜色设置为green)

lineBox的minimumSize和maximumSize的宽度设置为30

然后选中btStyle,并将其margin和Spacing设置为0

④然后往lineBox内部拖4个QLabel,objectName依次修改为linel、line2、line3、line4,minimumSize和maximumSize的宽度均设置为2

btStyle

css 复制代码
#btStyle:hover
{
	background:#D8D8D8;
}

lineBox

css 复制代码
.QLabel
{
	background-color:#FFFFFF;
}

将bodyLeft内部onlineMusic和MyMusic中的QWidget全部提升为BtForm。具体操作:

选中要提升的控件,比如:Rec,在弹出的菜单中选择提升为,会出现一个新窗口(如下右侧图),在提升的类名称中输入要提升为的类型BtForm,然后点击添加,最后选中btform.h点击提升,便可以将Rec由QWidget提升为自定义的BtForm类型。

3.1.2 BtForm类中实现

1.设置按钮上的图片和文字信息,以及该按钮关联的page页面

cpp 复制代码
// btform.h中新增
// 按钮id:该按钮对应的page页
int id = 0;

// 设置图标 文字 id
void seticon(QString btIcon,QString btText,int mid);

// 在brform.cpp中新增
void btForm::seticon(QString btIcon, QString btText, int mid)
{
    // 设置自定义按钮的图片、文字、以及id
    ui->btIcon->setPixmap(QPixmap(btIcon));
    ui->btText->setText(btText);
    id = mid;
}

在miniMusic.cpp的initUI()函数中新增:

cpp 复制代码
void MiniMusic::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);
}
  1. 按钮响应

重写鼠标mousePressEvent,当按钮按下时:

①按钮颜色发生变化

②给miniMusic类发送click信号

cpp 复制代码
// btform.h中新增

protected:
    // 鼠标点击事件
    void mousePressEvent(QMouseEvent *event);
signals:
    // btForm按键点击信号
    void click(int id);


// btform.cpp新增

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

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

cpp 复制代码
// minimusic.h 新增
// btForm点击槽函数
void onBtFormClick(int id);
//btForm按钮点击信号处理
void connectSignalAndSlot();


// minimusic.cpp 新增
void MiniMusic::onBtFormClick(int id)
{
    // 1.获取当前页面所有btFrom按钮类型的对象
    QList<btForm*> buttonList = this->findChildren<btForm*>();

    // 2.遍历所有对象,如果不是当前id的按钮,则把之前设置的背景颜色清除掉
    foreach(btForm* btitem,buttonList)// 类似for循环
    {
        if(id != btitem->getId())
        {
            btitem->clearBg();
        }
    }

    // 3.设置当前栈空间显示页面
    ui->stackedWidget->setCurrentIndex(id -1);
}

//btForm按钮点击信号处理 -- 该函数需要在构造函数中调用
void MiniMusic::connectSignalAndSlot()
{
    // 自定义的btForm按钮点击信号,当btForm点击后,设置对应的堆叠窗口
    connect(ui->rec,&btForm::click,this,&MiniMusic::onBtFormClick);
    connect(ui->music,&btForm::click,this,&MiniMusic::onBtFormClick);
    connect(ui->audio,&btForm::click,this,&MiniMusic::onBtFormClick);
    connect(ui->like,&btForm::click,this,&MiniMusic::onBtFormClick);
    connect(ui->local,&btForm::click,this,&MiniMusic::onBtFormClick);
    connect(ui->recent,&btForm::click,this,&MiniMusic::onBtFormClick);
}

④ 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切换的效果,可以在stackedWinget的每个Page上面放一个QLabel说明。

3.BtFrom上的动画效果

Qt中QPropertyAnimation类可以提供简单的动画效果,允许对QObject获取派生类的可读写属性进行动画处理,创建平滑、连续的动画效果,比如控件的位置、大小、颜色等属性变化,使用时需包含<QPropertyAnimation>。

关键函数说明:

cpp 复制代码
#include <QPropertyAnimation>
/*
功能:实例化QPropertyAnimation类对象
参数:
    target:给target设置动画效果
    propertyName:动画如何变化,比如:geometry,让target以矩形的方式滑动
    parent:该动画实例的父对象,即将该对象加到对象树中
*/
QPropertyAnimation(QObject *target, const QByteArray &propertyName, QObject *parent = nullptr);

/*
功能:设置动画持续时长
参数:单位为毫秒
*/
void setDuration(int msecs);

/*
功能:根据value创建关键帧
参数:
    step:值在0~1之间,0表示开始,1表示停止
    value:动画的一个关键帧,即动画现在的状态,假设是基于geometry,可以设置矩形的范围
*/
void setKeyValueAt(qreal step,const QVariant &value);

/*
功能:设置动画的循环次数
参数:
    loopCount:默认值是1,表示动画执行1次,如果是-1,表示无限循环
*/

void setLoopCount(int loopCount);

/*********************槽函数*********************/
void pause();// 暂停动画
void start(QAbstractAnimation::DeletionPolicy policy = KeepWhenStopped);// 开启动画
void stop(); // 停止动画

// 设置动画的起始帧
void setStartValue(const QVariant &&value);
// 设置动画的结束帧
void setEndValue(const QVariant &value);

/*
设置动画效果步骤
1.创建QPropertyAnimation对象
2.设置动画的持续事件
3.设置动画的关键帧
4.设置动画的循环次数[非必须],如果未调用动画默认执行一次
5.开启动画
6.动画运行结束时,会发射finished信号,如果需要进行额外处理时,处理该信号即可
*/

上述方法是本项目中需要用到的函数,大家可以通过Qt帮助手册了解更多。

lineBox中line1、line2、line3、line4添加动画效果,btForm类中增加如下代码:

cpp 复制代码
// btform.h新增
// linebox动画起伏效果
QPropertyAnimation *animationLine1;
QPropertyAnimation *animationLine2;
QPropertyAnimation *animationLine3;
QPropertyAnimation *animationLine4;

// 动画函数
void animation(QPropertyAnimation *animationLine,int index);

// btform.cpp的构造函数中新增
btForm::btForm(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::btForm)
{
    ui->setupUi(this);

    // 设置line1的动画效果
    animationLine1 = new QPropertyAnimation(ui->line1,"geometry",this);
    animation(animationLine1,0);

    animationLine2 = new QPropertyAnimation(ui->line2,"geometry",this);
    animation(animationLine2,7);

    animationLine3 = new QPropertyAnimation(ui->line3,"geometry",this);
    animation(animationLine3,14);


    animationLine4 = new QPropertyAnimation(ui->line4,"geometry",this);
    animation(animationLine4,21);


}

void btForm::animation(QPropertyAnimation *animationLine,int index)
{
    animationLine->setDuration(1500);
    animationLine->setKeyValueAt(0,QRect(index,15,2,0));
    animationLine->setKeyValueAt(0.5,QRect(index,0,2,15));
    animationLine->setKeyValueAt(1,QRect(index,15,2,0));
    animationLine->setLoopCount(-1);
    animationLine->start();
}

4.关于动画显示

动画并不是所有页面都显示,只有当前选中的页面显示,所以默认情况下,动画隐藏。默认情况下设置addlocal显示。

cpp 复制代码
// btform.h 新增
// 显示动画效果
// 显示动画效果
void showAnimal(bool isShow);

// btform.cpp中新增
void btForm::showAnimal(bool isShow)
{
    if(isShow)
    {
        // 显示linebox,设置颜色为绿色
        ui->lineBox->show();
    }
    else
    {
        // 隐藏
        ui->lineBox->hide();
    }
}

// miniMusic的initUI中设置默认选中
void MiniMusic::initUI()
{
    //...
    
    // 本地下载页面btForm动画默认显示
    ui->local->showAnimal(false);
    ui->stackedWidget->setCurrentIndex(4);
}

3.2 推荐页面

3.2.1 推荐页面分析

仔细观察推荐页面,对其进行拆解发现,推荐页面由五部分构成:

①"推荐"文本提示,即QLabel

②"今日为你推荐"文本提示,即QLabel

③具体推荐的歌曲内容,点击左右两侧翻页按钮,具有轮番图效果,将光标放到图上,有图片上移动画。

④"你的歌曲补给站"文本提示,即QLabel

⑤具体显示音乐,和③实际是一样的,不同的是③中音乐只有一行,⑤中的音乐有两行因为页面中元素较多,直接摆到一个页面太拥挤,从右侧的滚动条可以看出,整个页面中的元素都放置在QScrollArea中。

仔细分析③发现,里面包含了:

左右各两个按钮,点击之后中间的图片会左右移动,Qt中未提供类似该种组合控件,因此③实际为自定义控件。

③中按钮之间的元素,由图片和底下的文字组成,当光标放在图片上会有上移的动画,因此该元素实际也为自定义控件。

3.2.2 推荐页布局

在stackedWidget中选中推荐页面,objectName为recPage的页面,删掉之前添加的QLabel推荐提示。

①拖拽一个QScrollArea到recPage中,geometry的宽度和高度修改为820和 500,

②拖拽一个QLable,objectName修改为recText,显示内容修改为推荐,minimumSize和maximumSize的高度均修改为50,Font大小修改为24

③再拖拽一个QLable和Widget,QLable的objectName修改为recMusictext,内容修改为"今日为你推荐",minimumSize和maximumSize的高度均修改为30,Font大小修改为18;Widget的objectName修改为recMusicBox

④再拖拽一个QLabel和Widget,QLabel的objectName修改为supplyMusicText,内容修改为"你的音乐补给站",minimumSize和maximumSize的高度均修改为30,Font大小修改为18;Widget的objectName修改为supplyMusicBox。

⑤最后选中QScrollArea,点击垂直布局。

整个recPage基本就布局完成。

3.2.3 自定义recBox

  1. RecBox界面布局

①新添加设计师界面,命名为RecBox。geometry的宽高修改为:685*440。

②添加三个Widget,objectName依次修改为leftPage、musicContent、rightPage;leftPage和rightPage的minimumSize和maximumSize修改宽为30,然后选中RecBox点击水平布局。将RecBox的margin和Spacing修改为0

③在upPage和downPage中各拖一个按钮,upPage中按钮objectName修改为btUp,minimumSize的高度修改为220;downPage中按钮objectName修改为btDown,minimumSize的高度修改为220;然后选中upPage和downPage点击水平布局。将upPagedownPage和的margin和Spacing修改为0。

③在musicContent中拖两个Widget,objectName依次修改为recListUp和recListDown,然后选中musicContent点击垂直布局,将musicContent的margin和Spacing修改为o。(为了看清楚效果可临时将recListUp背景色设置为:background-color:green;将recListDown背景色设置为:background-color:red;)

在recListUp和recListDown中分别拖两个水平布局器,依次命名为recListUpHLayout和recListDownHLayout,选中recListUp和recListDown点击水平布局,将margin和Spacing修改为0。

按钮添加如下QSS美化:

btUp:

css 复制代码
QPushButton
{
	background-repeat:no-repeat;
	border:none;
	background-image:url(:/images/up_page.png);
	background-position:center center;
}

QPushButton:hover
{
	background-color:#1ECD97;
}

btDown:

css 复制代码
QPushButton
{
	background-repeat:no-repeat;
	border:none;
	background-image:url(:/images/down_page.png);
	background-position:center center;
}

QPushButton:hover
{
	background-color:#1ECD97;
}

将miniMusic主界面中recPage页面中的recMusicBox和supplyMusicBox提升为RecBox,就能看到如下效果。(去掉recbox中的背景颜色)

3.2.4 自定义recBoxItem

1. RecBoxItem界面布局

添加一个Designer界面,命名为RecBoxltem,geometry的宽和高设置为:150*200。

①拖拽一个Widget到RecBoxltem中,objectName修改为musiclmageBox,minimumSize和maximumSize的高度均修改为150;

②拖拽一个QLabel到Widget中,objectName修改为recBoxltemText,文本设置为"推荐-001",QLabel的alignment属性设置为水平、垂直居中。

③拖拽一个QLabel到musiclmageBox中,objectName修改为recMusiclmage,geometry设置为:[(0, 0),150*150]

④拖拽一个QPushButton到musiclmageBox中,objectName修改为recMusicBtn,删除掉文本内容。在属性中找到cursor,点击选择小手图标

css 复制代码
#recMusicBtn
{
	border:none;
	background-color:rgb(0, 255, 0);
}

2. RecBoxItem测试

cpp 复制代码
// recBox.cpp构造函数中添加如下代码
RecBoxItem* item = new RecBoxItem();
ui->recListUpHLayout->addWidget(item);

上述代码是在RecBox的构造函数中添加一个RecBoxItem的控件。

3. 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);

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

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->recMusicImage->setStyleSheet(imgStyle);
}

3.2.5 RecBox添加RecBoxItem

1. 图片路径和推荐文本准备

每个RecBoxltem都有对应的图片和推荐文本,在往RecBox中添加RecBoxltem前需要先将图片路径和对应文本准备好。由于图片和文本具有对应关系,可以以键值对方式来进行组织,以下实现的时采用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);

/*
QJsonArray类
作用:管理的是QJsonValue对象
头文件:<QJsonArray>
该类重载了[]运算符,可以通过下标方式获取管理的QJsonValue对象
*/
QJsonValue operator[] (int i) const;
QJsonValue operator[] (int i);

// 往QJsonArray中插入一个QJsonValue对象
void append(const QJsonValue &value);

// QJsonValue类
// 单参构造方法,将QjsonObject对象转换为QJsonValue对象
QJsonValue(const QJsonObject &o);

// 将内部管理的数据转化成QJsonObject返回
QJsonObject toObject() const;

// 将内部管理的数据转化成QString返回
QString toString() const;

图片路径和对应文本的准备工作,应该在miniMusic类中处理好,RecBoxltem只负责设置,因此该准备工作需要在miniMusic类中进行,故miniMusic中需要添加如下代码:

cpp 复制代码
// miniMusic.h 新增
// 参数num:RecBox中图片个数
// QJsonArray randomPiction();



// miniMusic.cpp中新增
// 设置随机图片[歌曲图片]
QJsonArray MiniMusic::randomPiction()
{
    // 推荐文本 + 推荐图片路径
    QVector<QString> vecImageName;
    vecImageName <<  "001.png" << "003.png" <<"004.png" << "005.png" << "006.png"
                     "007.png" << "008.png" <<"009.png" << "010.png" << "011.png"
                     "012.png" << "013.png" <<"014.png" << "015.png" << "016.png"
                     "017.png" << "018.png" <<"019.png" << "020.png" << "021.png"
                     "022.png" << "023.png" <<"024.png" << "025.png" << "026.png"
                     "027.png" << "028.png" <<"029.png" << "030.png" << "031.png"
                     "032.png" << "033.png" <<"034.png" << "035.png" << "036.png"
                     "037.png" << "038.png" <<"039.png" << "040.png";
    std::random_shuffle(vecImageName.begin(),vecImageName.end());

    // 001.png
    // path:":/images/rec/" + vectorImageName[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,QChar('0'))
        // i:要放入%1位置的数据
        // 3:三位数
        // 10:表示十进制数
        // QChar('0'):数字不够三位,前面用字符'0'填充
        QString strText = QString("推荐-%1").arg(i,3,10,QChar('0'));
        obj.insert("text",strText);

        objArray.append(obj);
    }
    return objArray;
}

2. recBox中添加元素

由于recPage页面中有两个RecBox控件,上面的RecBox为一行四列,下方的RecBox为2行四列,因此在RecBox类中增加以下成员变量:

cpp 复制代码
// RecBox.h 新增
#include <QJsonArray>

public:
    // ui界面初始化
    void initRecBoxUi(QJsonArray data,int row);
    // 往RecBox中添加图片
    void createRecItem();

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;
        col = 8;
    }
    else
    {
        // 否则:只有一行,为主界面上recListDown
        ui->recListDown->hide();
    }
    // 图片保存起来
    imageList = data;
    // 往recBox中添加图片
    createRecItem();
}

void RecBox::createRecItem()
{
    // 创建RecBoxIteam对象,往RecBox中添加
    // col
    for(int i = 0;i < col;++i)
    {
        RecBoxItem *item = new RecBoxItem();
        // 设置音乐图片与对应的文本
        QJsonObject obj = imageList[i].toObject();
        item->setText(obj.value("text").toString());
        item->setImage(obj.value("path").toString());

        // 将RecBoxItem对象添加到RecBox中
        ui->recListUpHLayout->addWidget(item);
    }
}

运行程序可以看到:

上面RecBox正确,recListUpHLayout中添加了4个RecBoxltem元素并显示出来,recListDownHLayout被隐藏了,而下面的RecBox中内容不对,对于下方RecBox,期望recListUpHLayout中显示添加4个RecBoxItem,recListDownHLayout中显示添加4RecBoxItem,而上述代码往RecBox中添加RecBoxltem时没有添加任何限制。

createRecBoxltem(函数修改如下:

cpp 复制代码
void RecBox::createRecItem()
{
    // 创建RecBoxIteam对象,往RecBox中添加
    // col
    for(int i = 0;i < col;++i)
    {
        RecBoxItem *item = new RecBoxItem();
        // 设置音乐图片与对应的文本
        QJsonObject obj = imageList[i].toObject();
        item->setText(obj.value("text").toString());
        item->setImage(obj.value("path").toString());

        // recMusicBox: col为4,元素添加到ui->recListUpHLayout中
        // supplyMusicBox: col为8,ui->recListUpHLayout添加4个,ui->recListDownHLayout添加4个
        // 即supplyMusicBox上下两行都要添加
        // 如果是recMusicBox: row为1,只能执行else,所有4个RecBoxItem都添加到ui->recListUpHLayout中
        // 如果是supplyMusicBox: row为2,col为8,col/2结果为4,i为0 1 2 3时,元素添加到ui->recListDownHLayout中
        // i为4 5 6 7时,元素添加到ui->recListUpHLayout中
        if(i >= col / 2 && row == 2)
        {
            // 将RecBoxItem对象添加到RecBox中
            ui->recListDownHLayout->addWidget(item);
        }
        else
        {
            // 将RecBoxItem对象添加到RecBox中
            ui->recListUpHLayout->addWidget(item);
        }
    }
}

3.2.6 RecBox中btUp和btDown按钮clicked处理

1.添加槽函数

选中recbox.ui文件,分别选中btUp和btDown,右键单击弹出菜单选择转到槽,选中clicked确定,btUp和btDown的槽函数就添加好了。

cpp 复制代码
void RecBox::on_btUp_clicked()
{
    // 点击btUp按钮,显示前4张图片,如果已经是第一张图片,循环从后往前显示
}

void RecBox::on_btDown_clicked()
{
    // 点击btUp按钮,显示前8张图片,如果已经是第一张图片,循环从后往前显示
}

2. imageList中图片分组

假设imageList中有24组图片路径和推荐文本信息,如果将信息分组:

如果是recMusicBox,将元素按照col分组,即每4个元素为一组,可分为6组;如果是supplyMuscBox,将元素按照col分组,即每8个元素为一组,可分为3组。

RecBox类中添加currentlndex和count整形成员变量,currentlndex记录当前显示组,count记录总的信息组数。当点击btUp时,currentlndex-,显示前一组,如果currentlndex小于o时,将其设置为count-1;当点击btDown按钮时,currentIndex++显示下一组,当currentlndex为count时,将count设置为0。

这样就实现了轮番显示效果。

cpp 复制代码
// recbox.h 中新增

private:
    int currentIndex;   // 标记当前显示第几组图片和推荐信息
    int count;          // 标记imageList中元素按照col分组总数

// recbox.cpp 中新增

void RecBox::initRecBoxUi(QJsonArray data, int row)
{
    // ...

    // 图片保存起来
    imageList = data;

    // 默认显示第0组
    currentIndex = 0;

    // 计算总共有几组图片,ceil表示向上取整
    count = ceil(imageList.size()/col);

    // 往recBox中添加图片
    createRecItem();
}

void RecBox::on_btUp_clicked()
{
    // 点击btUp按钮,显示前4张图片,如果已经是第一张图片,循环从后往前显示
    currentIndex--;
    if(currentIndex < 0)
    {
        currentIndex = 0;
    }
    createRecItem();
}

void RecBox::on_btDown_clicked()
{
    // 点击btUp按钮,显示前8张图片,如果已经是第一张图片,循环从后往前显示
    currentIndex++;
    if(currentIndex >= count)
    {
        currentIndex = 0;
    }
    createRecItem();
}

3. 元素重复分析

每次btUp和btDown点击后,应该显示前一组和后一组图片,由于之前recListUpHLayout和recListDownHLayout中已经有元素了,因此需要先将之前的元素删除掉。

cpp 复制代码
void RecBox::createRecItem()
{
    // 移除掉之前旧的元素
    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;
    }


    // 创建RecBoxIteam对象,往RecBox中添加
    // ...
}

4. 按照分组计算imageList中元素偏移

|---------|---------|---------|---------|---------|---------|---------|---------|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 001.png | 001.png | 001.png | 001.png | 001.png | 001.png | 001.png | 001.png |
| 第0组 |||| 第1组 ||||
| 第0组起始元素在imageList中元素偏移量为:i=0 |||| 第0组起始元素在imageList中元素偏移量为:i=4 ||||

即i的初始值应该为:i = i + currentIndex*col;

cpp 复制代码
void RecBox::createRecItem()
{
    // 移除掉之前旧的元素
    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;
    }


    // 创建RecBoxIteam对象,往RecBox中添加
    // col
    int index = 0;
    for(int i = currentIndex*col;i < col + currentIndex*col;++i)
    {
        RecBoxItem *item = new RecBoxItem();
        // 设置音乐图片与对应的文本
        QJsonObject obj = imageList[i].toObject();
        item->setText(obj.value("text").toString());
        item->setImage(obj.value("path").toString());

        // recMusicBox: col为4,元素添加到ui->recListUpHLayout中
        // supplyMusicBox: col为8,ui->recListUpHLayout添加4个,ui->recListDownHLayout添加4个
        // 即supplyMusicBox上下两行都要添加
        // 如果是recMusicBox: row为1,只能执行else,所有4个RecBoxItem都添加到ui->recListUpHLayout中
        // 如果是supplyMusicBox: row为2,col为8,col/2结果为4,i为0 1 2 3时,元素添加到ui->recListDownHLayout中
        // i为4 5 6 7时,元素添加到ui->recListUpHLayout中
        if(index >= col / 2 && row == 2)
        {
            // 将RecBoxItem对象添加到RecBox中
            ui->recListDownHLayout->addWidget(item);
        }
        else
        {
            // 将RecBoxItem对象添加到RecBox中
            ui->recListUpHLayout->addWidget(item);
        }
        ++index;
    }
}

5. 程序启动时图片随机显示

仔细观察发现,每次程序启动时,显示的图片都是相同的,这是因为random_shuffle在随机打乱元素时,需要设置随机数种子,否则默认使用的种子是相同的,就导致每次打乱的结果都是相同的,所以每次程序启动时RecBox中显示的内容都是相同的,因此在randomPiction(调用之前需要设置随机数种子。

cpp 复制代码
// miniMusic类的initUI函数中新增
void MiniMusic::initUI()
{
    //...
    // 本地下载页面btForm动画默认显示
    ui->local->showAnimal(false);
    ui->stackedWidget->setCurrentIndex(4);

    // 初始化推荐页面
    srand(time(nullptr));
    ui->recMusicBox->initRecBoxUi(randomPiction(),1);
    ui->supplyMusicBox->initRecBoxUi(randomPiction(),2);
}

3.3 自定义CommonPage

3.3.1 CommonPage页面分析

我的音乐下的:我喜欢、本地下载、最近播放三个按钮表面上看对应三个Page页面,分析之后发现,这三个Page页面实际是雷同的,因此只需要定义一个页面CommonPage,将stackedWidget中这三个页面的类型提升为CommonPage即可。

上图为本地音乐的Page页面,对页面拆解后,发现该页面可以分四部分:

①页面说明,比如:本地音乐,该部分实际就是QLabel的提示说明;

②正在播放音乐图片和播放全部按钮;

③音乐列表中每个部分的文本提示,实际就是三个QLabel

④本页面对应的音乐列表,即QListWidget。

3.3.2 CommonPage页面布局

新增加一个设计界面,objectName修改为CommonPage,geometry的宽高修改为80o*500。

① 拖拽一个QLabel、两个Widget和一个List View控件到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修改为musiclmageLabel,minimumSize和maximumSize的宽度

修改为150。

拖拽一个Widget,objectName修改为playAll,minimumSize和maximumSize的宽度修改为120,在其内部拖拽一个PushButton和VerticalSpace(即垂直弹簧),将按钮的objectName修改为playAllBtn,minimumSize和maximumSize的宽和高修改为100*30,文本内容修改为"播放全部",然后选中playAll点击垂直布局。

拖拽一个Horizontal Spacer到CommonPage中,放在playAll之后。

然后选中musicPlayBox,点击水平布局,将margin和spacing设置为0.

④ listLabelBox中

拖拽三个QLabel,内容依次修改为:歌曲名称、歌手名称、专辑名称,objectName从左往右依次

修改为:musicNameLabel、musicSingerLabel、musicAlbumLabel,然后选中listLabelBox,点击水平布局,将margin和spacing设置为0.

⑤ 选中List View,右键单击弹出菜单中选择"变形为",选择QListWidget。

选中miniMusic页面,将stackedWidget中我喜欢、本地下载、最近播放对应的页面提升为CommonPage,页面就处理完成。

cpp 复制代码
#playAllBtn
{
	background-color:#E3E3E3;
	border-radius:10px;
}

#playAllBtn:hover
{
	background-color:#1ECD97;
}

3.3.3 CommonPage界面设置和显示

CommonPage页面是我喜欢、本地下载、最近播放三个界面的共同类型,因此该类需要提供设置:pageTittle和musiclmageLabel的公共方法,将来在程序启动时完成三个界面信息的设置,因此CommonPage类需要添加一个public的setCommonPageUl函数。

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));
    // 让QLabel里的图片自动缩放,填满整个QLabel大小
    ui->musicImageLabel->setScaledContents(true);
}

界面设置的函数需要在程序启动时就完成好配置,即需要在miniMusic的initUi()函数中调用完成设置:

cpp 复制代码
void MiniMusic::initUI()
{
    //...
    // 设置我喜欢、本地音乐、最近播放页面
    ui->likePage->setCommonPageUI("我喜欢",":/images/ilikebg.png");
    ui->localPage->setCommonPageUI("本地音乐",":/images/localbg.png");
    ui->recentPage->setCommonPageUI("最近播放",":/images/recentbg.png");
}

3.4 自定义ListItemBox

3.4.1 ListItemBox页面分析

CommonPage页面创建好之后,等音乐加载到程序之后,就可以将音乐信息往CommonPage的pageMusicList中显示了。

上图每行都是QListWidget中的一个元素,每个元素中包含多个控件:

① 收藏图标,即QLabel

② 歌曲名称,即QLabel

③ VIP和SQ,VIP即收费会员专享,SQ为无损音乐,也是两个QLabel

④ 歌手名称,即QLabel

⑤ 音乐专辑名称,即QLabel

此处,需要将上述所有QLabel组合在一起,作为一个独立的控件,添加到QListWidget中,因此该控件也需要自定义。

3.4.2 ListItemBox页面布局

添加一个设计师界面,objectName为ListltemBox,geometry的宽度和高度修改为80o*45。

① 拖三个Widget到ListItemBox中,objectName从左往右依次修改为musicNameBox、musicSingerBox、musicAlbumBox,将musicNameBox的minimumSize和maximumSize的宽修改为380,将musicSingerBox的minimumSize和maximumSize的宽修改为200,然后选中ListltemBox,点击水平布局,将ListltemBox的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:

css 复制代码
#likeBtn
{
	border:none;
}

VIPLabel:

css 复制代码
#VIPLabel
{
	border: 1px solid #1ECD96;
	color:#1ECD96;
	border-radius:2px;
}

SQLabel:

css 复制代码
#SQLabel
{
	border:1px solid #FF6600;
	color:#FF6600;
	border-radius:2px;
}

3.4.3 ListltemBox显示测试

ListItemBox将来要添加到CommonPage页面中的QListWidget中,因此在CommonPage类的初始化方法中添加如下代码:

cpp 复制代码
// commonpage.h中新增
#include "listitembox.h"

// commpage.cpp中添加
// 设置图片和标题
void CommonPage::setCommonPageUI(const QString &title, const QString &image)
{
    // 设置标题
    ui->pageTittle->setText(title);

    // 设置封面栏
    ui->musicImageLabel->setPixmap(QPixmap(image));
    // 让QLabel里的图片自动缩放,填满整个QLabel大小
    ui->musicImageLabel->setScaledContents(true);

    // 测试
    // 1. 创建自定义列表项控件
    ListItemBox* listItemBox = new ListItemBox(this);
    // 2. 创建QListWidget的列表项容器
    QListWidgetItem* listWidgetItem = new QListWidgetItem(ui->pageMusicList);
    // 3. 设置列表项的尺寸:宽度=列表宽度,高度=45px(固定行高)
    listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(),45));
    // 4. 把自定义控件绑定到列表项上,显示在列表里
    ui->pageMusicList->setItemWidget(listWidgetItem,listItemBox);
}

3.4.4 支持hover效果

ListltemBox添加到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("");
}

3.5 自定义MusicSlider

由于QT内置的HorizontalSlider(水平滑竿)不是很好看,该控件也采用自定义。

该控件比较简单,实际就是两个QFrame嵌套起来的。

① 添加一个设计师界面,objectName修改为MusicSlider,geometry修改为80o*20。

② 拖拽一个QFrame,objectName修改为inLine,geometry修改为[(o,8),800*4]。

③ 拖拽一个QFrame,objectName修改为outLine,geometry修改为[(o,8),400*4]。

④ 选中MusicSlider,点击水平布局。

⑤ inLine和outLine的样式设置如下:

inLine:

css 复制代码
#inLine
{
	background-color:#EBEEF5;
}

outLine:

css 复制代码
#outLine
{
	background-color:#1ECC94;
}

打开MiniMusic.ui,选中progressBar清除之前样式,将progressBar提升为MusicSlider,运行程序

就能看到效果。

3.6 自定义VolumeTool

3.6.1 VolumeTool控件分析

音量调节控件本来也可以使用Qt内置的垂直滑杆来代替,只是垂直滑杆不好看,因此也自定义。

① 内部为类似MusicSlider控件+小圆球,圆球实际为一个QPushButton

② 音量大小文本显示,实际为QLabel

③ QPushButton,点击之后在静音和取消静音切换

④ 一个倒三角,Qt未提供三角控件,该控件需要手动绘制,用来提示是播放控制区那个按钮按下的。

3.6.2 VolumeTool界面布局

① 生成一个QT设计师界面,objectName命名为VolumeTool,geometry的宽高修改为100*350。

② 拖拽一个Widget到VolumeTool中,objectName修改为volumeWidgetgeometry修改为:[(10,10),80*300]

拖拽一个QPushButton到volumeWidget,objectName修改为silenceBtnmimimumSize和maximumSize的宽高修改为80*45

拖拽一个QLabel到volumeWidget,objectName修改为volumeRatio,mimimumSize和maximumSize的高修改为30,QLabel的alignment属性修改为水平和垂直居中。

拖拽一个QWidget到volumeWidget,objectName修改为sliderBox。geometry修改为:[(0,0),80*225]

③ sliderBox内部:

拖拽一个QFrame,objectName修改为inSlider,geometry修改为[(38,25),4*180]。

拖拽一个QFrame,objectName修改为outSlider,geometry修改为[(38,25),4*180]。

拖拽一个QPushButton,objectName修改为sliderBtn,geometry修改为[(33,20),14*14],mimimumSize和maximumSize的宽高14*14。

volumeWidget

css 复制代码
#volumeWidget
{
	background-color:#ffffff;
	border-radius:5px;
}

silenceBtn

css 复制代码
#silenceBtn
{
	border:none;
}

#silenceBtn:hover
{
	background-color:#F0F0F0;
}

inSlider

css 复制代码
#inSlider
{
	background-color:#ECECEC;
}

outSlider

css 复制代码
#outSlider
{
	background-color:#1ECC94;
}

sliderBtn

css 复制代码
#sliderBtn
{
	background-color:#1ECC94;
	border-radius:7px;
}

注意:静音底下的空缺用来绘制三角。

3.6.3 界面设置

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

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

    // 在windows上,设置透明效果后,窗口需要加上Qt::FramelessWindowHint格式
    // 否则没有控件位置的背景是黑色的
    // 由于默认窗口有阴影,因此还需要将窗口的原有阴影去掉,窗口需要加上Qt::NoDropShadowWindowHint
    setAttribute(Qt::WA_TranslucentBackground);

    // 自定义阴影效果
    QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect(this);
    shadowEffect->setOffset(0,0);
    shadowEffect->setColor("#646464");
    shadowEffect->setBlurRadius(10);
    setGraphicsEffect(shadowEffect);

    // 给按钮设置图标
    ui->silenceBtn->setIcon(QIcon(":/images/volume.png"));
    // 音量的默认大小是20
    ui->volumeRatio->setText("20%");

    // 设置outSlider尺寸
    QRect rect = ui->outSlider->geometry();
    ui->outSlider->setGeometry(rect.x(),180-36+25,rect.width(),36);
    ui->sliderBtn->move(ui->sliderBtn->x(),ui->outSlider->y() - ui->sliderBtn->height()/2);
}

3.6.4 界面创建及弹出

音量调节属于主界面上元素,因此在QQMusic类中需要添加VolumeTool的对象,在initUi中new该类的对象。

主界面中音量调节按钮添加clicked槽函数。

cpp 复制代码
// minimusic.h中新增
#include "volumetool.h"

VolumeTool *volumeTool;

// minimusic.cpp中新增
void MiniMusic::initUI()
{
    //...

    // 创建音量调节窗口对象并挂到对象树
    volumeTool = new VolumeTool(this);
}

// 添加按钮点击槽函数
// 音量按键点击槽函数
void MiniMusic::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();
}

3.6.5 绘制三角

由于Qt中并未给出三角控件,因此三角需要手动绘制,故在VolumeTool类中重写paintEvent事件函

数。

cpp 复制代码
// volumetool.h中新增
// 画画函数
void paintEvent(QPaintEvent *event);


void VolumeTool::paintEvent(QPaintEvent *event)
{
    (void)event;
    // 1.创建绘图对象
    QPainter painter(this);

    // 2.设置抗锯齿
    painter.setRenderHint(QPainter::Antialiasing,true);

    // 3.设置画笔
    // 没有画笔时: 画出来的图形没有边框和轮廓线
    painter.setPen(Qt::NoPen);

    // 4.设置画刷颜色
    painter.setBrush(QBrush(Qt::white));

    // 创建一个三角形
    QPolygon polygon;
    polygon.append(QPoint(30,300));
    polygon.append(QPoint(70,300));
    polygon.append(QPoint(50,320));

    // 绘制三角形
    painter.drawPolygon(polygon);
}

4. 音乐管理

界面处理好之后,现在就需要将音乐文件加载到程序然后显示在界面上,待后续播放操作。

4.1 音乐加载

QQMusic类中给addLocal添加槽函数。

音乐文件在磁盘中,可以借助QFileDialog类完成音乐文件加载。QFileDialog类中函数介绍:

cpp 复制代码
// 构造函数
QFileDialog(QWidget *parent = nullptr,  // 指定该对象的父对象
                         const QString &caption = QString(),    // 设置标题窗口
                         const QString &directory = QString(),  // 设置默认打开目录
                         const QString &filter = QString());    // 设置过滤器,可以只打开指定后缀文件
// 默认创建的是打开对话框


#include <QFileDialog>

/*
文件过滤器
筛选所需要格式的文件,格式:每组文件之间用两个分号隔开,同一组内不同后缀之间用空格隔开
比如:打开指定文件夹下所有.cpp.h以及.png的文件
QString filter = "代码文件(.cpp *.h)";
过滤器可以在构造QFileDialog对象时传入,也可以通过setNameFilters函数设置
void setNameFilters(const QStringList &filters);

有些时候文件的后缀不一定能给全,比如图片格式:.pnp .bmp ·jpg等,有些格式甚至没有接触过,
但也属于图片文件,该种情况下最好使用MIME类型过滤
MIME类型(Multipurpose Internet Mail Extensions)是一种互联网标准,用于表示文档、文件或字节流的性质和格式。
语法:type/subType
比如:text/plain表示文本文件application/octet-stream表示通用的二进制数据流的MIME类型

void setMimeTypeFilters(const QStringList &filters)
示例:
*/
QStringListmimeTypeFilters;
    mimeTypeFilters<<"image/jpeg" // will show "JPEG image (*.jpeg *.jpg *.jpe)
                   <<"image/png"// will show "PNG image(*.png)"
                   <<"application/octet-stream";//will show"All files (*)"
QFileDialog dialog(this);
dialog.setMimeTypeFilters(mimeTypeFilters);

// 设置打开对话框的类型
QFileDialog::Acceptopen:表示对话框为打开对话框
QFileDialog::AcceptSave:表示对话框为保存对话框
voidsetAcceptMode(QFileDialog::AcceptMode mode);

// 设置选择文件的数量和类型
voidsetFileMode(QFileDialog::FileMode mode);
QFileDialog::AnyFile // 用户可以选择任何文件,甚至指定一个不存在的文件
QFileDialog::ExistingFile // 用户只能选择单个存在的文件名称
QFileDialog::Directory // 用户可以选择一个目录名称
QFileDialog::ExistingFiles //用户可以选择一个或者多个存在的文件名称

//设置文件对话框的当前目录
void setDirectory(const QString &directory);

// 获取当前目录
QDir::currentPath();

打开函数实现如下:

cpp 复制代码
// minimusic.cpp 中新增
#include <QDir>
#include <QFileDialog>

// 添加歌曲按钮点击槽函数
// 添加歌曲按钮点击槽函数
void MiniMusic::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() + "/MiniMusic/musics/";
    fileDialog.setDirectory(musicPath);

    // 6. 显示对话框,并接收返回值
    // 模态对话框,exec内部是死循环处理
    if(fileDialog.exec() == QFileDialog::Accepted)
    {
        // 切换到本地音乐界面,因为加载完的音乐需要在本地音乐界面显示
        ui->stackedWidget->setCurrentIndex(4);

        // 获取对话框的返回值
        QList<QUrl> urls = fileDialog.selectedUrls();

        // 拿到歌曲文件后,将歌曲文件交由musicList进行管理
        // ...
    }
}

4.2 MusicList类

4.2.1 添加C++类MusicList

将来添加到播放器中的音乐比较多,可借助一个类对所有的音乐进行管理。添加新C++类与添加设计师界面类似:

一个新C++类就添加完成。

4.2.2 歌曲对象存储

每首音乐文件,将来需要获取其内部的歌曲名称、歌手、音乐专辑、歌曲时长等信息,因此在MusicList类中,将所有的歌曲文件以Music对象方式管理起来。

QQMusic中,通过QFileDialog将一组音乐文件的url获取到之后,可以交给MusicList类来管理。

但是QQMusic加载的二进制文件不一定全部都是音乐文件,因此MusicList类中需要对文件的MIME类型再次检测,以筛选出真正的音乐文件。

QMimeDatabase类是Qt中主要用于处理文件的MIME类型,经常用于:

  • 文件类型识别
  • 文件过滤
  • 多媒体文件处理
  • 文件导入导出
  • 文件管理器

该类中的mimeTypeForFile函数可用于获取给定文件的MIME类型

cpp 复制代码
// QMimeDatabase类的mimeTypeForfile方法
// 功能: 获取fileName文件的MIME类型
// fileName: 文件的名称
// mode:MatchMode为枚举类型,表明如何匹配文件的MIME类型
//      MatechDefault:通过文件名和文件内容来进行查询匹配,文件名优先于文件内容,如果文件扩展名未知
// 或者匹配多个MIME类型,则使用文件内容匹配
// MatchExtension:通过文件
// MatchContent:通过文件内容来查询匹配
#include <QMimeType>
#include <QMimeData>
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++类,描述歌曲相关信息

// 将miniMusic页面中读到的音乐文件,检测是音乐文件后添加到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);
    }
}

4.3 Music类

4.3.1 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 setDyration(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:
    // 解析媒体元数据
    void parseMediametaData();

private:
    bool isLike;    // 标记音乐是否为我喜欢
    bool isHistory; // 标记音乐是否播放过

    // 音乐的基本信息有:歌曲名称、歌手名称、专辑名称、总时长
    QString musicName;
    QString singerName;
    QString albumName;
    qint64 duration;    // 音乐的持续时长,即播放总的时长

    // 为了标记歌曲的唯一性,给歌曲设置id
    // 磁盘上的歌曲文件经常删除或者修改位置,导致播放时找不到文件,或者重复添加
    // 此处用musicId来维护播放列表中音乐的唯一性
    QString musicId;
    QUrl musicUrl;  // 音乐在磁盘中的位置
};


// music.cpp中新增

Music::Music()
    :isLike(false)
    ,isHistory(false)
{

}

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::setDyration(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,即通用唯一识别码(UniversallyUniqueIdentifier),确保在分布式系统中每个元素都有唯一的标识。

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();
    parseMediametaData();
}

4.3.2 解析音乐文件元数据

对于每首歌曲,将来在界面上需要显示出:歌曲名称、歌手、专辑名称,在播放时还需要拿到歌曲总时长,因此在构造音乐对象时,就需要将上述信息解析出来。

歌曲元数据解析,需要用到QMediaPlayer,该类也是用来进行歌曲播放的类,后续在播放音乐位置详细介绍。

QMediaPlayer类中的setMedia()函数

cpp 复制代码
// 功能:设置要播放的媒体源,媒体数据从中读取
// media: 要播放的媒体内容,⽐如⼀个视频或⾳频⽂件,该类提供了⼀个QUrl格式的单参构造
void setMedia(const QMediaContent &media, QIODevice *stream = nullptr);

注意:该函数执行后立即返回,不会等待媒体加载完成,也不检查错误,如果在媒体加载时发生错误,会触发mediaStatusChanged和error信号。

由于加载媒体文件需要时间,可以通过QMediaObject类中的isMetaDataAvailable(方法检测媒体数据是否可用

QMediaObject类是QMediaPlayer类的基类。

cpp 复制代码
// 检测媒体源是否有效,如果是有效的返回true.否则返回false 
bool isMetaDataAvailable() const;

媒体元数据加载成功之后,可以通过QMediaObject类的metaData函数获取指定的媒体数据:

cpp 复制代码
// 返回要获取的媒体数据key的值
QVariant QMediaObject::metaData(const QString &key) const;

文本需要获取媒体的:标题、作者、专辑、持续时长

|------------|-----------------|-------------|
| valye | description | type |
| Title | 媒体的标题 | QString |
| Author | 媒体的作者 | QStringList |
| AlbumTitle | 媒体所属专辑名称 | QString |
| Duration | 媒体的播放时长 | qint64 |

注意:有些媒体中媒体数据可能不全,即有些媒体数据获取不到,比如盗版歌曲。

使用QMediaPlayer媒体播放类时,需要在miniMusic.pro项目工程文件中添加媒体模块multimedia,该模块主要用来播放各种音频视频文件等,该模块中提供了很多类:

cpp 复制代码
QT       += core gui multimedia

添加完成之后,重新将项目构建一下,否则Qtcreate可能识别不过来。

音乐文件的meta数据解析如下:

cpp 复制代码
// music.h中新增
private:
    // 解析媒体元数据
    void parseMediametaData();

// music.cpp中新增
#include <QUuid>
#include <QMediaPlayer>
#include <QCoreApplication>


// 解析媒体元数据
void Music::parseMediametaData()
{
    // 解析时候需要注意读取歌曲数据,读取歌曲文件需要用到QMediaPlayer类
    QMediaPlayer player;
    player.setMedia(musicUrl);

    // 媒体元数据解析需要时间,只有等待解析完成之后,才能提取音乐信息,此处循环等待
    // 循环等待时,主界面消息就无法处理了,因此需要在等待解析期间,让消息循环继续处理
    while(!player.isMetaDataAvailable())
    {
        QCoreApplication::processEvents();
    }

    // 解析媒体元数据结束,提取元数据信息
    if(player.isMetaDataAvailable())
    {
        musicName = player.metaData("Title").toString();
        singerName = player.metaData("Author").toString();
        albumName = player.metaData("AlbumTitle").toString();
        duration = player.duration();
        if(musicName.isEmpty())
        {
            musicName = "歌曲未知";
        }
        if(singerName.isEmpty())
        {
            singerName = "歌手未知";
        }
        if(albumName.isEmpty())
        {
            albumName = "专辑名未知";
        }
        qDebug() << musicName << ":" << singerName << ":" << albumName << ":" << duration;
    }
}

/*************************************/

Music::Music(const QUrl &url)
    :isLike(false)
    ,isHistory(false)
    ,musicUrl(url)
{
    musicId = QUuid::createUuid().toString();
    
    // 该函数需要在Music的构造函数中调用,当创建音乐对象时,顺便完成歌曲文件的加载
    parseMediametaData();
}

4.3.3Music数据保存

通过QFileDialog将音乐从本地磁盘加载到程序中后,拿到的是所有音乐文件的QUrl,而在程序中需要的是经过元数据解析之后的Music对象,并且Music对象需要管理起来,此时就可以采用MusicList类对解析之后的Music对象进行管理,QQMusic类中只需要保存MusicList的对象,就可以让qqMusic.ui界面中CommonPage对象完成Music信息往界面更新。

cpp 复制代码
// minimusic.h 新增
#include "musiclist.h"
// 音乐管理
MusicList musicList;


// minimusic.cpp
// 音量按键点击槽函数
void MiniMusic::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();
}

// 添加歌曲按钮点击槽函数
void MiniMusic::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中。

4.4 音乐分类

QQMusic中,有三个显示歌曲信息的页面:

  • likePage:管理和显示点击小心心后收藏的歌曲
  • localPage:管理和显示本地加载的歌曲
  • recentPage:管理和显示历史播放过的歌曲

这三个页面的类型都是CommonPage,每个页面应该维护自己页面中的歌曲。因此CommonPage类中需要新增:

cpp 复制代码
// commonpage.h中新增

// 区分不同page页面

// 区分不同的page页面
enum PageType
{
    LIKE_PAGE,  // 我喜欢页面
    LOCAL_PAGE, // 本地下载页面
    HISTORY_PAGE  // 最近播放页面
};

class CommonPage : 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;
}

// minimusic.cpp中新增

void MiniMusic::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 复制代码
// connonpage.h 中新增
#include "musiclist.h"

private:
    // 音乐页面添加音乐
    void addMusicToMusicPage(MusicList &musicList);

// commonpage.cpp 中新增
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
    // 将旧内容清空
    musicListOfPage.clear();

    for(auto& music : musicList)
    {
        switch (pageType)
        {
        case LOCAL_PAGE:
            musicListOfPage.push_back(music.getMusicId());
            break;
        case LIKE_PAGE:
            if(music.getIsLike())
            {
                musicListOfPage.push_back(music.getMusicId());
            }
            break;
        case HISTORY_PAGE:
            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中新增
MusicList::iterator MusicList::begin()
{
    return musicList.begin();
}

MusicList::iterator MusicList::end()
{
    return musicList.end();
}

这样就完成了歌曲的分类。

4.5 更新Music信息到ComonPage界面

歌曲分类完成之后,歌曲信息就可以更新到CommonPage页面了。

更新步骤:

  1. 调用addMusicldPageFromMusicList函数,从musicList中添加当前页面的歌曲
  2. 遍历musicListOfPage,拿到每首音乐后先检查其是否在,存在则添加。
  3. 界面上需要更新每首歌曲的:歌曲名称、作者、专辑名称,而commonPage中只保存了歌曲的musicld,因此需要在MusicList中增加通过musicID查找Music对象的方法。
cpp 复制代码
// commonpage.h 中新增
// 负责将歌曲显示到界面上
void reFresh(MusicList musicList);

// commonpage.cpp 中新增
void CommonPage::reFresh(MusicList musicList)
{

    // 从musicList中分离出当前页面的所有音乐
    addMusicToMusicPage(musicList);

    // 遍历歌单.将歌单中的歌曲显示到界面
    for(auto musicId:musicListOfPage)
    {
        auto it = musicList.findMusicById(musicId);
        if(it == musicList.end())
            continue;
        ListItemBox *listItemBox = new ListItemBox(ui->pageMusicList);
        listItemBox->setMusicName(it->getMusicName());
        listItemBox->setSinger(it->getSingerName());
        listItemBox->setAlbumName(it->getAlbumName());
        listItemBox->setLikeIcon(it->getIsLike());

        QListWidgetItem *listWidgetItem = new QListWidgetItem(ui->pageMusicList);
        listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(),45));
        ui->pageMusicList->setItemWidget(listWidgetItem,listItemBox);
    }
    // 更新完之后刷新一下页面
    // 触发窗口重绘paineEvent
    // update();    //update()将paintEvent事件放在事件循环队列中,没有立马处理
    repaint();      // 立马响应paintEvent事件
}


// musiclist.h 中新增
// 按照Id查找音乐
iterator findMusicById(const QString &musicId);

// musiclist.cpp 中新增
MusicList::iterator MusicList::findMusicById(const QString &musicId)
{
    for(iterator it = begin();it != end();++it)
    {
        if(it->getMusicId() == musicId)
        {
            return it;
        }
    }
    return end();
}

将歌曲名称、作者、专辑名称、喜欢图片等往ListBoxltem界面中更新时,需要ListBoxltem提供对应的set方法,因此需要在ListltemBox类中新增:

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)    // 默认设置为false,音乐加载上来之后,点击小心心才为true
{
    ui->setupUi(this);
}

void ListItemBox::setMusicName(const QString &name)
{
    ui->musicNameLabel->setText(name);
}

void ListItemBox::setSinger(const QString &singer)
{
    ui->musicSingerLabel->setText(singer);
}

void ListItemBox::setAlbumName(const QString &albumName)
{
    ui->albumNameLabel->setText(albumName);
}

void ListItemBox::setLikeIcon(bool like)
{
    isLike = like;
    if(isLike)
    {
        ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
    }
    else
    {
        ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));
    }
}

更新音乐信息到界面的函数处理完成之后,需要在QQMusic的addLocal槽函数最后调用。

cpp 复制代码
// minimusic.cpp 中新增

// 添加歌曲按钮点击槽函数
void MiniMusic::on_addLocal_clicked()
{
    //...

    // 6. 显示对话框,并接收返回值
    // 模态对话框,exec内部是死循环处理
    if(fileDialog.exec() == QFileDialog::Accepted)
    {
        // 切换到本地音乐界面,因为加载完的音乐需要在本地音乐界面显示
        ui->stackedWidget->setCurrentIndex(4);

        // 获取对话框的返回值
        QList<QUrl> urls = fileDialog.selectedUrls();

        // 拿到歌曲文件后,将歌曲文件交由musicList进行管理
        musicList.addMusicByUrl(urls);

//        for(auto &e :  urls)
//        {
//            qDebug() << e << "\n";
//        }

        // 更新到本地音乐列表
        ui->localPage->reFresh(musicList);
    }
}

4.6 CommonPage显示不足处理

a. 歌曲作者对齐处理

解析歌曲元数据时,有些歌曲文件中可能不存在歌曲名称、作者、歌曲专辑等,为了界面上显示出歌曲名称,从歌曲文件名中解析出歌曲名称和作者,比如:""2002年的第一场雪-刀郎.mp3"。这样解析出来的歌曲名称后面多一个空格,作者之前多一个空格,导致界面显示的时候歌手名称对不

齐。因此在往界面设置之前,可以将名称前后的空格去除掉。

QString类提供了一个trimmed()方法,专门用来去除字符串前后空白字符的。

cpp 复制代码
// music.cpp 文件修改
// 解析媒体元数据
void Music::parseMediametaData()
{
    //...
    
        QString fileName = musicUrl.fileName();
        // 找"-"的位置
        int index = fileName.indexOf('-');

        if(musicName.isEmpty())
        {
            if(index != -1)
            {
                // "2002年的第一场雪 - 刀郎.mp3"
                musicName = fileName.mid(0,index).trimmed();
            }
            else
            {
                // "2002年的第一场雪.mp3"
                musicName = fileName.mid(0,fileName.indexOf('-')).trimmed();
            }
        }

        if(singerName.isEmpty())
        {
            if(index != -1)
            {
                singerName = fileName.mid(index+1,fileName.indexOf('.')-index-1).trimmed();
            }
            else
            {
                singerName = "未知歌手";
            }

        }

    //...
}

b. 显示延迟问题

在CommonPage的reFresh()函数中,将ListltemBox设置好之后,更新到界面,有时候不会立马显示出来,等鼠标放置ListWidget上或者界面刷新的时候,才会显示出来。

这是因为往界面更新元素的操作,没有引起窗体的重绘,导致不能实时显示出来,因此添加完元素之后,需要触发重绘事件,将元素及时绘制出来。

cpp 复制代码
// 该⽅法负责将歌曲信息更新到界⾯
void CommonPage::reFresh(MusicList &musicList)
{
    // ...
    // 该函数最后添加上repaint()函数调⽤
    // repaint()会⽴即执⾏paintEvent(),不会等待事件队列的处理
    // update()将⼀个paintEvent事件添加到事件队列中,等待稍后执⾏,即不会⽴即执⾏paintEvent。
    repaint();
}

c. 移除掉QListWidget的水平滚动条

一般歌曲名称、作者、专辑名称不会将ListltemBox沾满,为了界面好看,可以让CommonPage中的QListWidget控件去除掉水平滚动条。

cpp 复制代码
CommonPage::CommonPage(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::CommonPage)
{
    ui->setupUi(this);

    // 不要水平滚动条
    ui->pageMusicList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

}

d. QListWidget选中后背景色设置

QListWidget中ListItemBox选中之后,背景颜色和界面不是很搭,用如下QSS代码设置ListltemBox选中后的背景颜色。

css 复制代码
#pageMusicList::item:selected    /*::item表示子控件,即ListItemBox : selected:表示选中*/
{
    background-color:#EFEFEF;
}

e. QListWidget的垂直滚动条美化

css 复制代码
#pageMusicList::item:selected    /*::item表示子控件,即ListItemBox : selected:表示选中*/
{
    background-color:#EFEFEF;
}

QScrollBar:vertical
{
    border:none;
    width: 10px;
    background-color:#FFFFFF;
    margin: 0px 0px 0px 0px;
}

QScrollBar::handle:vertical
{
    width:10px;
    background-color:#E3E3E3;
    border-radius:5px;
    min-height: 20px;
}

4.7 音乐收藏

4.7.1 我喜欢图标处理

当CommonPage往界面更新Music信息时,也要根据Music的isLike属性更新对应的图标。因此ListltemBox需要根据当点击我喜欢按钮之后,要切换ListltemBox中的小心心。因此ListltemBox中添加设置bool类型isLike成员变量,以及setlsLike函数,在CommonPage添加Music信息到界面时,要能够设置小心心图片。

cpp 复制代码
// listitemBox.h 中新增
bool isLike;    // 是否喜欢
// 设置是否喜欢
void setLikeIcon(bool like);

// listitemBox.cpp 中新增
ListItemBox::ListItemBox(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::ListItemBox),
    isLike(false)   // 默认设置为false,音乐加载上来之后,点击小心心才为true
{
    ui->setupUi(this);
}

void ListItemBox::setLikeIcon(bool like)
{
    isLike = like;
    if(isLike)
    {
        ui->likeBtn->setIcon(QIcon(":/images/like_2.png"));
    }
    else
    {
        ui->likeBtn->setIcon(QIcon(":/images/like_3.png"));
    }
}

4.7.2 点击我喜欢按钮处理

当喜欢某首歌曲时,可以点击界面上红色小心心收藏该首歌曲。我喜欢按钮中应该有以下操作:

  1. 更新小心心图标

  2. 更新Music的我喜欢属性,但ListltemBox并没有歌曲数据,所以只能发射信号,让其父元素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)   // 默认设置为false,音乐加载上来之后,点击小心心才为true
{
    ui->setupUi(this);

    // likeBtn按钮连接其点击槽函数
    connect(ui->likeBtn,&QPushButton::clicked,this,&ListItemBox::onLikeBtnClicked);
}

void ListItemBox::onLikeBtnClicked()
{
    isLike = !isLike;
    setIsLike(isLike);
    // 发送信号
    emit setIsLike(isLike);
}
  1. CommonPage在往QListWidget中添加元素时,会创建一个个ListltemBox对象,每个对象将来都可能会发射setLikeMusic信号,因此在将ListltemBox添加完之后,CommonPage应该关联先该信号,将需要更新的的Music信息以及是否喜欢,同步给miniMusic。
cpp 复制代码
// commonpage.h 中新增
signals:
    // 歌曲是否喜欢的信号
    void updateLikeMusic(bool isLike,QString musicId);

// commonpage.cpp 中新增
// 该方法负责将歌曲信息更新到界面
void CommonPage::reFresh(MusicList musicList)
{
    //...
    
    for(auto musicId:musicListOfPage)
    {
        //...

        QListWidgetItem *listWidgetItem = new QListWidgetItem(ui->pageMusicList);
        listWidgetItem->setSizeHint(QSize(ui->pageMusicList->width(),45));
        ui->pageMusicList->setItemWidget(listWidgetItem,listItemBox);

        // 接收ListItemBox发射的setIsLike信号
        connect(listItemBox,&ListItemBox::setIsLike,this,[=](bool isLike){
            emit updateLikeMusic(isLike,it->getMusicId());
        });
    }

    //...
}
  1. QQMusic收到CommonPage发射的updateLikePage信号后,通知其上的likePage、localPage、recentPage更新其界面的我喜欢歌曲信息。
cpp 复制代码
// minimusc.h 新增
void onUpdateLikeMusic(bool isLike,QString musicId);    // 响应CommonPage发射updateLikeMusic信号

// minimusic.cpp 新增

void MiniMusic::connectSignalAndSlot()
{
    // ...
    // 关联CommonPage发射的updateLikeMusic信号
    connect(ui->likePage,&CommonPage::updateLikeMusic,this,&MiniMusic::onUpdateLikeMusic);
    connect(ui->localPage,&CommonPage::updateLikeMusic,this,&MiniMusic::onUpdateLikeMusic);
    connect(ui->recentPage,&CommonPage::updateLikeMusic,this,&MiniMusic::onUpdateLikeMusic);
}

void MiniMusic::onUpdateLikeMusic(bool isLike, QString musicId)
{
    // 1.找到该首歌曲,并更新对应Music对象信息
    auto it = musicList.findMusicById(musicId);
    if(it != musicList.end())
    {
        it->setIsLike(isLike);
    }

    // 2.通知三个页面更新自己的数据
    ui->likePage->reFresh(musicList);
    ui->localPage->reFresh(musicList);
    ui->recentPage->reFresh(musicList);
}

5.歌曲重复显示问题

当界面上歌曲数据更新之后,CommonPage往页面上更新其musicofPage内容时,musicOfPage和界面中的QListWidget中已经有数据了,需要先将之前的内容清楚掉,否则就会重复。

cpp 复制代码
// commonpage.cpp 修改
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
    // 将旧内容清空
    musicListOfPage.clear();

    //...
}


void CommonPage::reFresh(MusicList musicList)
{

    // 清空旧内容
    ui->pageMusicList->clear();

    //...
}

5.音乐播放控制

歌曲已经添加到程序并完成解析,解析的信息也更新到界面了,所有前置工作基本完成,接下来重点处理音乐播放,歌曲播放需要用到Qt提供的QMediaPlayer类和QMediaPlaylist类。

5.1 QMediaPlayer类

5.1.1 QMediaPlayer类说明

QMediaPlayer是Qt框架中用于支持各种音频和视频的播放,流媒体的播放,各种播放模式(单曲播放、列表播放、循环播放等),各种播放模式(播放、暂停、停止等),信号槽机制可以让用户在播放状态改变时进行所需控制。

使用时需要包含#include<QMediaPlayer>头文件,并且需要在.pro项目文件中添加媒体库,即:QT+=multimedia,将multimedia模块导入到工程中,就可以使用该模块中提供的媒体播放控制的相关类,比如:QMediaPlayer、QMediaPlayList等。

5.1.2 属性和方法

a.枚举类型

【QMediaPlayer::State枚举类型】

|----------------------------|-----|--------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMediaPlayer::StoppedState | 0 | 播放停止状态 |
| QMediaPlayer::PlayingState | 1 | 播放状态 |
| QMediaPlayer::PausedState | 2 | 播放暂停状态 |

[QMediaPlayer::Flag】

|-----------------------------|-----|--------------------------------------------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMediaPlayer::LowLatency | 0 | 播放未压缩的音频文件,播放表现为低时 延,主要播放蜂鸣、手机铃声 |
| QMediaPlayer:StreamPlayback | 1 | 播放给予QIODevice构建的媒体文件, QMediaPlayer或自动选择支持的流进行播 放 |
| QMediaPlayer::VideoSurface | 2 | 渲染视频到QAbstractVideoSurface输出 |

【QMediaPlayer::Error】

|-----------------------------------|-----|----------------------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMediaPlayer::NoError | 0 | 没有错误 |
| QMediaPlayer::ResourceError | 1 | 媒体源无法解析 |
| QMediaPlayer::FormatError | 2 | 媒体源格式不支持,可能会播放,但是没有音频和视频组件 |
| QMediaPlayer::NetworkError | 3 | 网络错误 |
| QMediaPlayer::AccessDeniedError | 4 | 没有媒体源的访问权限 |
| QMediaPlayer::ServiceMissingError | 5 | 没有有效的播放服务,无法继续播放 |

【QMediaPlayer::MediaStatus】

|---------------------------------|-----|-------------------------------------------------|
| 状态枚举名称 | 枚举值 | 说明 |
| QMediaPlayer:UnknownMediaStatus | 0 | 媒体的状态未被定义 |
| QMediaPlayer::NoMedia | 1 | 没有媒体文件,player处于StoppedState |
| QMediaPlayer::LoadingMedia | 2 | 媒体文件加载中,player可以处于任何状 态 |
| QMediaPlayer::LoadedMedia | 3 | 媒体文件已经加载,player处于StoppedState |
| QMediaPlayer::StalledMedia | 4 | 媒体处于延迟或者暂时中断状态,player 处于PlayingState或PauseState |
| QMediaPlayer::BufferingMedia | 5 | 媒体正在缓冲数据,player处于PlayingState或PauseState |
| QMediaPlayer::BufferedMedia | 6 | 媒体数据缓冲完成,player处于PlayingState或PauseState |
| QMediaPlayer::EndOfMedia | 7 | 媒体结束,player处于StoppedState |
| QMediaPlayer::InvalidMedia | 8 | 非法的媒体文件,player处于StoppedState |

b.常用属性

cpp 复制代码
// 部分常用属性
const qint64 duration;  // 保存媒体的总播放时间,单位为毫秒
const QMediaContent currentMedia;   // 当前正在播放媒体的媒体内容
const QString error;                // 最近一次信息错误
int volume;                         // 保存音量大小,范围在0~100之间
const bool audioAvailable;          // 音频是可用,audioAvailableChanged信号用于监听其状态
QMediaPlaylist* playtlist;          // 播放列表

c.常用函数

cpp 复制代码
qint64 duration() const;    // 获取当前媒体的总时间
qint64 position() const;    // 获取当前媒体的播放位置
int volume() const;         // 获取播放音量大小
bool isMuted() const;       // 检测是否静音
State state() const;        // 获取当前媒体的播放状态
QMediaContent currentMedia() const; // 获取当前正在播放的媒体内容
QString errorString() const;        // 获取最近的一次错误

d.常用槽函数

cpp 复制代码
void paise();   // 播放媒体
void play();    // 暂停媒体
void stop();    // 停止媒体播放
void setMuted(bool meted);  // 设置是否静音,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);

e.常用信号

cpp 复制代码
void stateChanged(QMediaPlayer::State state);   // 播放状态改变时发射该信号
void durationChanged(qint64 duration);          // 播放时长改变时发射该信号
void positionChanged(qint64 position);          // 播放位置改变时发射该状态
void volumeChanged(int volume);                 // 音量改变时发射该信号
void metaDateAvailableChanged(bool available);  // 源数据改变发出

以上只列出了本次需要用到的属性、方法和槽函数,后续需要使用时请参考Qt帮助手册。

5.2 QMediaPlaylist类

5.2.1 QMediaPlaylist类介绍

QMediaPlaylist类提供了一种灵活而强大的方式管理媒体文件的播放列表。。通过结合QMediaplayer,可以实现顺序播放、循环播放随机播放等多种播放模式,提升用户的媒体播放体验。该类提供了以下功能:

  • 添加和删除媒体文件
  • 播放模式设置(列表播放、随机播放、单曲循环)
  • 控制播放列表(开始,停止,上一曲,下一曲)
  • 获取和设置当前媒体文件
  • 信号槽支持

若播放多个媒体文件,必须使用该类来管理媒体文件,将该列表设置到player上,就可实现更加灵活的播放支持。

5.2.2 属性和方法

a.枚举类型

[QMediaPlaylist::PlaybackMode]

|-----------------------------------|-----|----------------|
| 枚举状态名称 | 枚举值 | 说明 |
| QMediaPlaylist::CurrentItemOnce | 0 | 单词播放 |
| QMediaPlaylist::CurrentItemInLoop | 1 | 单曲循环 |
| QMediaPlaylist::Sequential | 2 | 从当前选中位置开始,顺序播放 |
| QMediaPlaylist:Loop | 3 | 列表中文件循环播放 |
| QMediaPlaylist::Random | 4 | 列表中文件随机播放 |

b.常见属性

cpp 复制代码
int currentIndex;   // 当前播放的媒体文件在媒体列表中的索引
const QMediaContent currentMedia;   // 当前选中的媒体文件
QMediaPlaylist::PlaybackMode playbackMode;  // 媒体列表中文件的播放模式

a. 常见方法

cpp 复制代码
bool addMedia(const QMediaContent &content);        // 向媒体列表中添加单个媒体文件
int mediaCount() const;                             // 获取播放列表中文件的个数
int currentIndex() const;                           // 获取当前播放的媒体的索引
bool clear();                                       // 清空媒体列表
QMediaPlaylist::PlaybackMode playbackMode() const;  // 获取媒体列表的播放模式
QMediaContent currentMedia() const;                 // 获取当前播放的媒体文件
QString errorString() const;                        // 获取最近一次发生过的错误

b. 常用槽函数

cpp 复制代码
void next();                                // 下一曲
void previous();                            // 上一曲
void setCurrentIndex(int playlistPosition); // 设置当前播放媒体的索引
void shuffle();                             // 媒体顺序打乱,重建媒体索引

c.常用信号

cpp 复制代码
// 列表播放模式方法改变时发射
void playbackModeChanged(QMediaPlaylist::PlaybackMode mode);
// 当前索引发生改变时发射
void currentIndexChanged(int position);
// 当前媒体文件改变时发生
void currentMediaChanged(const QMediaContent &content);

该类提供的方法非常丰富,此处暂介绍了需要用到的内容,后续开发时需要用到其他内容请参考Qt帮助手册。

5.3 歌曲播放

5.3.1 播放媒体和播放列表初始化

在播放之前,需要先将QMediaPlayer和QMediaPlaylist初始化好。QQMusic类中需要添加QMediaPlayer和QMediaPlaylist的对象指针,在界面初始化时将这两个类的对象创建好。

cpp 复制代码
#include <QMediaPlayer>
#include <QMediaPlaylist>

// minimusic.h 新增
public:
    void initPlayer();  // 初始化媒体对象

private:
    // 播放器相关
    QMediaPlayer* player;
    // 要多首歌曲播放,以及更复杂的播放设置,需要给播放器设置媒体列表
    QMediaPlaylist* playList;

// minimusic.cpp 添加
MiniMusic::MiniMusic(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::MiniMusic)
{
    ui->setupUi(this);
    // 界面ui初始化
    initUI();

    //btForm按钮点击信号处理
    connectSignalAndSlot();

    // 初始化播放器哦
    initPlayer();
}

void MiniMusic::initPlayer()
{
    // 创建播放器哦
    player = new QMediaPlayer(this);

    // 创建播放列表
    playList = new QMediaPlaylist(this);

    // 设置播放模式:默认为循环播放
    playList->setPlaybackMode(QMediaPlaylist::Loop);

    // 将播放列表设置给播放器
    player->setPlaylist(playList);

    // 默认音量大小设置为20
    player->setVolume(20);
}

5.3.2 播放列表设置

播放之前,先要将歌曲加入用于播放的媒体列表,由于每个CommonPage页面的歌曲不同,因此CommonPage中新增将其页面歌曲添加到模仿列表的方法。

cpp 复制代码
// commonpage.h 中新增
#include <QMediaPlaylist>

public:
    // 向播放器添加音乐
    void addMusicToPlayer(MusicList &musicList,QMediaPlaylist *playList);

// commonpage.cpp 中新增
void CommonPage::addMusicToMusicPage(MusicList &musicList)
{
    // 将旧内容清空
    musicListOfPage.clear();

    for(auto& music : musicList)
    {
        switch (pageType)
        {
        case LOCAL_PAGE:
            musicListOfPage.push_back(music.getMusicId());
            break;
        case LIKE_PAGE:
            if(music.getIsLike())
            {
                musicListOfPage.push_back(music.getMusicId());
            }
            break;
        case HISTORY_PAGE:
            if(music.getIsHistory())
            {
                musicListOfPage.push_back(music.getMusicId());
            }
            break;
        default:

            break;
        }
    }
}

5.3.3 播放和暂停

当点击播放和暂停按钮时,播放状态应该在播放和暂停之间切换。播放器的状态如下,刚开始为停止状态。

QMediaPlayer的播放状态有:PlayingState()、PausedState()、StoppedState()。

|--------------|---------------|------|
| 播放状态 | 对应槽函数 | 说明 |
| PlayingState | void play(); | 正在播放 |
| PausedState | void pause(); | 暂停 |
| StoppedState | void stop(); | 停止状态 |

cpp 复制代码
// minimusic.h 中新增
// 播放区域控制
void onPlayCliked();    // 播放按钮

// minimusic.cpp 中新增
void MiniMusic::onPlayCliked()
{
    qDebug() << "播放按钮点击";
    if(player->state() == QMediaPlayer::PlayingState)
    {
        // 如果是歌曲正在播放中,按下播放键,此时应该暂停播放
        player->pause();
    }
    else if(player->state() == QMediaPlayer::PausedState)
    {
        // 如果是暂停状态,按下播放键,继续开始播放
        player->play();
    }
    else if(player->state() == QMediaPlayer::StoppedState)
    {
        player->play();
    }
}

void MiniMusic::connectSignalAndSlot()
{
    // ...

    // 播放控制区的信号和槽函数关联
    connect(ui->play,&QPushButton::clicked,this,&MiniMusic::onPlayCliked);
}

注意:播放时默认是从播放列表索引为0的歌曲开始播放的。

另外播放状态改变的时候,需要修改播放按钮上图标,图片的修改可以在onPlayCliked函数中设置,也可以拦截.

QMediaPlayer中的stateChanged信号,当播放状态改变的时候,QMediaPlayer会触发该信号,在

stateChanged信号中修改播放按钮也可以,此处拦截stateChanged信号。

cpp 复制代码
// minimusic.h新增
// QMediaPlayer信号处理
// 播放状态发生改变
void onPlayStateChanged();

// minimusic.cpp 新增
// QMediaPlayer信号关联槽函数
void MiniMusic::onPlayStateChanged()
{
    qDebug() << "播放状态改变";
    if(player->state() == QMediaPlayer::PlayingState)
    {
        // 播放状态
        ui->play->setIcon(QIcon(":/images/play_on.png"));
    }
    else
    {
        // 暂停状态
        ui->play->setIcon(QIcon(":/images/play3.png"));
    }
}

void MiniMusic::initPlayer()
{
    // ...

    // QMediaPlayer信号和槽函数关联
    // 播放状态改变时: 暂停和播放之间切换
    connect(player,&QMediaPlayer::stateChanged,this,&MiniMusic::onPlayStateChanged);
}

播放和暂停切换的时候,按钮上的图标有重叠,是因为之前在界面设置的时候,为了能看到效果,给按钮添加了背景图片,背景图片和图标是两种属性,都设置时就ui叠加,因此将按钮上个添加背景图片样式去除掉。

cpp 复制代码
void MiniMusic::initUI()
{
    // ...
    // 按钮的背景图片样式去除掉之后,需要设置默认图标
    // 播放控制区按钮图标设定
    ui->play->setIcon(QIcon(":/images/play_2.png"));    // 默认为暂停图标
    ui->playMode->setIcon(QIcon(":/images/shuffle_2.png")); // 默认为随机播放
}

5.3.4 上一曲和下一曲

播放列表中,提供了previous(和next()函数,通过设置前一个或者下一个歌曲为当前播放源,player就会播放对应的歌曲。

cpp 复制代码
// minimusic.h 新增
void onPlayUpCliked();  // 上一曲
void onPlayDownCliked();    // 下一曲

// minimusic.cpp 新增
void MiniMusic::onPlayUpCliked()
{
    playList->previous();
}

void MiniMusic::onPlayDownCliked()
{
    playList->next();
}

void MiniMusic::connectSignalAndSlot()
{
    // ...
    // 播放控制区的信号和槽函数关联
    connect(ui->play,&QPushButton::clicked,this,&MiniMusic::onPlayUpCliked);
    connect(ui->playUp,&QPushButton::clicked,this,&MiniMusic::onPlayCliked);
    connect(ui->playDown,&QPushButton::clicked,this,&MiniMusic::onPlayDownCliked);
}

5.3.5 播放模式设置

媒体列表提供了以下播放模式:

|----------------------------------|-----|----------------|
| 枚举状态名称 | 枚举值 | 说明 |
| 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 复制代码
// minimusic.h 中新增
void onPlaybackModeCliked();    // 播放模式设置

// minimusic.cpp 中新增
void MiniMusic::initPlayer()
{
    // ...
    // 设置播放模式
    connect(ui->playMode,&QPushButton::clicked,this,&MiniMusic::onPlaybackModeCliked);
}


void MiniMusic::onPlaybackModeCliked()
{
    // 播放模式是针对播放列表的
    // 播放模式支持:循环播放、随机播放、单曲循环三种模式
    if(playList->playbackMode() == QMediaPlaylist::Loop)
    {
        // 列表循环
        ui->playMode->setToolTip("随机播放");
        playList->setPlaybackMode(QMediaPlaylist::Random);
    }
    else if(playList->playbackMode() == QMediaPlaylist::Random)
    {
        // 随机播放
        ui->playMode->setToolTip("单曲循环");
        playList->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
    }
    else if(playList->playbackMode() == QMediaPlaylist::CurrentItemInLoop)
    {
        // 随机播放
        ui->playMode->setToolTip("列表循环");
        playList->setPlaybackMode(QMediaPlaylist::Loop);
    }
    else
    {
        qDebug() << "播放格式错误";
    }
}

播放模式切换时会触发playbackModechanged信号,在该信号对应槽函数中,完成图片切换。

cpp 复制代码
// minimusic.h 中新增
// 播放模式切换槽函数
void onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode);

// minimusic.cpp 中新增
void MiniMusic::onPlaybackModeChanged(QMediaPlaylist::PlaybackMode playbackMode)
{
    if(playbackMode == QMediaPlaylist::Loop)
    {
        ui->playMode->setIcon(QIcon(":/images/list_play.png"));
    }
    else if(playbackMode == QMediaPlaylist::Random)
    {
        ui->playMode->setIcon(QIcon(":/images/shuffle_2.png"));
    }
    else if(playbackMode == QMediaPlaylist::CurrentItemInLoop)
    {
        ui->playMode->setIcon(QIcon(":/images/single_play.png"));
    }
    else
    {
        qDebug() << "暂不支持该模式";
    }
}

void MiniMusic::initPlayer()
{
    // ...
    // 播放列表的模式发生改变时的信号槽关联        
    connect(playList,&QMediaPlaylist::playbackModeChanged,this,&MiniMusic::onPlaybackModeChanged);
}

5.3.6 播放所有

播放所有按钮属于CommonPage中的按钮,其对应的槽函数添加在CommonPage类中,但是

CommonPage不具有音乐播放的功能,因此当点击播放所有按钮后之后,播放所有的槽函数应该发射出信号,让MiniMusic类完成播放。

由于likePage、localPage、recentPage三个CommonPage页面都有playAllBtn,因此该信号需要带上PageType参数,需要让MiniMusic在处理该信号时,知道播放哪个页面的歌曲。

cpp 复制代码
// commonpage.h 中新增
signals:
    // 该信号由MiniMusic处理--在构造函数中捕获
    void PlayAll(PageType pageType);

// commonpage.cpp 中修改
CommonPage::CommonPage(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::CommonPage)
{
    ui->setupUi(this);

    // 不要水平滚动条
    ui->pageMusicList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    // playAllBtn按钮的信号槽处理
    // 当播放按钮点击时,发射playAll信号,播放当前页面的所有歌曲
    // playAll信号交由MiniMusic中处理
    connect(ui->playAllBtn,&QPushButton::clicked,this,[=](){
        emit PlayAll(pageType);
    });

}

在MiniMusic中,给playAll信号关联槽函数,并播放当前Page页面的所有音乐。playAll槽函数中,根据pageType将当前page页面记录下来,默认从该页面的第o首歌曲开始播放。注意不要忘记关联信号槽。

cpp 复制代码
// minimusic.h 中新增
// 播放所有信号的槽函数
#include "commonpage.h"
// 判断哪个页面的playAll
void onPlayAll(PageType pageType);
// 传入页面和歌曲序列播放歌曲
void playAllOfCommonPage(CommonPage* commonPage,int index);


// minimusic.cpp 中新增
void MiniMusic::onPlayAll(PageType pageType)
{
    CommonPage* page = nullptr;
    switch (pageType)
    {
    case PageType::LIKE_PAGE:
        page = ui->likePage;
        break;
    case PageType::LOCAL_PAGE:
        page = ui->localPage;
        break;
    case PageType::HISTORY_PAGE:
        page = ui->recentPage;
    default:
        qDebug() << "扩展";
        break;
    }
    // 从当前页面的零号位置开始播放
    playAllOfCommonPage(page,0);
}

void MiniMusic::playAllOfCommonPage(CommonPage *commonPage, int index)
{
    // 播放page所在页面的歌曲
    // 将播放列表先清空,否则无法播放当前CommonPage页面的歌曲
    // 另外:该页面音乐不一定就在播放列表中,因此需要先将该页面音乐添加到播放列表
    playList->clear();

    // 将当前页面歌曲添加到播放列表
    commonPage->addMusicToPlayer(musicList,playList);

    // 设置当前播放列表的索引
    playList->setCurrentIndex(index);

    // 播放
    player->play();
}

void MiniMusic::connectSignalAndSlot()
{
    // ...
    // 关联播放所有的信号和槽函数
    connect(ui->likePage,&CommonPage::PlayAll,this,&MiniMusic::onPlayAll);
    connect(ui->localPage,&CommonPage::PlayAll,this,&MiniMusic::onPlayAll);
    connect(ui->recentPage,&CommonPage::PlayAll,this,&MiniMusic::onPlayAll);
}

5.3.7 双击CommPage页面QListWidget项播放

当QListWidget中的项被双击时,会触发doubleClicked信号,

cpp 复制代码
// QListWidget中的项被双击时触发
// QModelIndex类中的row()函数会返回被点击的QListWidgetItem在QListWidget中索引
void doubleClicked(const QModelIndex &index);

该信号在QListWidget的基类中定义,有一个index参数,表示被双击的QListWidgetltem在QListWidget中的索引,该索引刚好与QMediaPlaylist中歌曲的所以一致,被双击时直接播放该首歌曲即可。

cpp 复制代码
// CommonPage.h 中新增
signals:
    //  发射在哪个页面第几行歌曲的信号
    void playMusicByIndex(CommonPage*,int);

// commonpage.cpp 中新增
CommonPage::CommonPage(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::CommonPage)
{
    ui->setupUi(this);

    // ...

    connect(ui->pageMusicList,&QListWidget::doubleClicked,this,[=](const QModelIndex &index){
        // 鼠标双击后,发射信号告诉MiniMusic,就能放this页面中被双击的歌曲
        emit playMusicByIndex(this,index.row());
    });
}

// minimusic.h 中新增
// CommonPage中playMusicByIndex信号对应槽函数
void playMusicByIndex(CommonPage* page,int index);

// minimusic.cpp 中新增
void MiniMusic::playMusicByIndex(CommonPage *page, int index)
{
    playAllOfCommonPage(page,index);
}


void MiniMusic::connectSignalAndSlot()
{
    // ...
    // 处理likePage、localPage、recentPage中ListItemBox双击
    connect(ui->likePage,&CommonPage::playMusicByIndex,this,&MiniMusic::playMusicByIndex);
    connect(ui->localPage,&CommonPage::playMusicByIndex,this,&MiniMusic::playMusicByIndex);
    connect(ui->recentPage,&CommonPage::playMusicByIndex,this,&MiniMusic::playMusicByIndex);
}

5.3.8 最近播放同步

当播放歌曲改变时,即播放的媒体源发生了变化,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 复制代码
// minimusic.h 中新增
// 记录当前正在播放的页面
CommonPage* currentPage;

// minimusic.cpp 中修改
void MiniMusic::initUI()
{
    // ...

    // 将localPage设置为当前页面
    currentPage = ui->localPage;
}

void MiniMusic::playAllOfCommonPage(CommonPage *commonPage, int index)
{
    currentPage = commonPage;

    // 播放page所在页面的歌曲
    // ...
}

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

cpp 复制代码
// minimusic.h 中新增
// 支持播放历史记录
void onCurrentIndexChanged(int index);

// minimusic.cpp 中新增
void MiniMusic::initPlayer()
{
    // ...
    
    // 播放列表项发生改变时,此时将播放音乐收藏到历史记录中
    connect(playList,&QMediaPlaylist::currentIndexChanged,this,&MiniMusic::onCurrentIndexChanged);
}

void MiniMusic::onCurrentIndexChanged(int index)
{
    // 音乐的id都在commonPage中的musicListOfPage中存储着
    const QString& musicId = currentPage->getMusicIdByIndex(index);

    // 有了MusicId就可以在musicList中找到该音乐
    auto it = musicList.findMusicById(musicId);
    if(it != musicList.end())
    {
        // 将该音乐设置为历史播放记录
        it->setIsHistory(true);
    }
    ui->recentPage->reFresh(musicList);
}


// commonpage.h 中新增
// 通过index获取音乐的id
const QString getMusicIdByIndex(int index) const;

// commonpage.cpp 中新增
const QString CommonPage::getMusicIdByIndex(int index) const
{
    if(index >= musicListOfPage.size())
    {
        qDebug() << "无此歌曲";
        return "";
    }
    return musicListOfPage[index];
}

5.3.9 音量设置

a.功能分析

当点击静音按钮时,音量应该在静音和非静音之间进行切换,并且按钮上图标需要同步切换。鼠标在滑竿上点击或拖动滑竿时,应该跟进滑竿的高低比率,设置音量大小,同时修改界面音量比率。

b. QMediaPlayer提供支持

QMediaPlayer中音量相关操作如下:

cpp 复制代码
int volume;                 // 标记音量大小,值在0~100之间
int volume() const;         // 获取音量大小
void setVolume(int);        // 槽函数:设置音量大小

bool muted;                 // 是否静音,true为静音,false为非静音
bool isMuted() const;       // 获取静音状态
bool setMuted(bool muted);  // 槽函数:设置静音或非静音

c.静音和非静音切换

VolumeTool类中需要添加两个成员变量,并在构造函数中完成默认值的设置。

给静音按钮参加槽函数onSilenceBtnClicked,并在构造函数中connect按钮的clicked信号,当按钮点击时候,调用setMuted(bool nuted)函数,完成静音和非静音的设置。

由于VolumeTool不具备媒体播放控制,因此当静音状态发生改变时,发射设置静音信号,让MinuMusic来处理。

cpp 复制代码
// volumetool.h 中新增
signals:
    void setSilence(bool);  // 设置静音信号

public:
    void onSilenceBtnClicked(); // 静音按钮槽函数

private:
    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);
}


// minimusic.h 中新增
// 设置是否静音
void setMusicSilence(bool isMuted);

// minimusic.cpp 中新增
void MiniMusic::setMusicSilence(bool isMuted)
{
    player->setMuted(isMuted);
}

void MiniMusic::connectSignalAndSlot()
{
    // ...
    // 设置静音
    connect(volumeTool,&VolumeTool::setSilence,this,&MiniMusic::setMusicSilence);
}

d. 鼠标按下、滚动以及释放事件处理

当鼠标在滑竿上按下时,需要设置sliderBtn和outLine的位置,当鼠标在滑竿上移动或者鼠标抬起时,需要设置SliderBtnoutLine结束的位置,即改变VolumeTool中滑竿的显示。具体修改播放媒体音量大小操作应该由于MiniMusic负责处理,因此当鼠标移动或释放时,需要发射信号让MiniMusic知道需要修改播放媒体的音量大小了。

cpp 复制代码
// volumetool.h 中新增
signals:
    void setMusicVolume(int);   // 发射修改音量大小槽函数

// 事件过滤器
bool eventFilter(QObject* object,QEvent* event);

// volumetool.cpp 中新增
bool VolumeTool::eventFilter(QObject *object, QEvent *event)
{
    // 过滤volumeBox上的事件
    if(object == ui->sliderBox)
    {
        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) // 默认音量我20%
{
    // ...
    
    // 安装事件过滤器
    ui->sliderBox->installEventFilter(this);
}

e. outLine和SliderBtn以及volumeRation更新

outLine坐标:[(38,25),4*180]。

当outLine最低时,即高度为0时候,outLine左上角坐标为:(38,205)

当outLine最高时,即高度为180时,outLine左上角坐标为:(38,25)

当鼠标在滑竿上滚动时,鼠标坐标转化为volumeBox上相对坐标时,鼠标y坐标必须在[25~205]范围内。

根据鼠标在滑竿上的相对高度更新:SliderBtn、outLine以及volumeRation的值。

cpp 复制代码
// volumetoo.h 中新增
// 根据鼠标在滑竿上滑动更新滑动界面,并按照比例计算音量大小
void setVolume();

// volumetool.cpp 中新增
void VolumeTool::setVolume()
{
    // 1.将鼠标的位置转换为sloderBox上的相对坐标,此处只要获取y坐标
    int height = ui->sliderBox->mapFromGlobal(QCursor().pos()).y();

    // 2.鼠标在sliderBox中课移动的y范围在[25,205]之间
    height = height < 25 ? 25 : height;
    height = height > 205 ? 205 : height;

    // 3.调整sliderBtn的位置和大小
    ui->sliderBtn->move(ui->sliderBtn->x(),height - ui->sliderBtn->height()/2);

    // 4.更新outSlider的位置和大小
    ui->outSlider->setGeometry(ui->outSlider->x(),height,ui->outSlider->width(),205-height);

    // 5.计算音量比率
    volumeRatio = (int)((int)ui->outSlider->height()/(float)180*100);

    // 6.设置给label显示出来
    ui->volumeRatio->setText(QString::number(volumeRatio)+"%");
}

f. MiniMusic类拦截VolumeTool发射的setMusicVolume信号,将音量大小设置为指定值。

cpp 复制代码
// minimusic.h 中新增
// 设置音量大小
void setPlayerVolume(int volume);

// minimusic.cpp 中新增
void MiniMusic::setPlayerVolume(int volume)
{
    player->setVolume(volume);
}

void MiniMusic::connectSignalAndSlot()
{
    // ...

    // 设置音量大小
    connect(volumeTool,&VolumeTool::setMusicVolume,this,&MiniMusic::setPlayerVolume);
}

5.3.10 当前播放时间和总时间更新

a.界面歌曲总时间更新

歌曲总时间在Music对象中可以获取,也可以让player调用自己的duration(方法获取。但是这两种获取歌曲总时间的调用时机不太好确定。我们期望的是当歌曲发生切换时,获取到正在播放歌曲的总时长。

当播放源的持续时长发生改变时,QMediaPlayer会触发durationChanged信号,该信号中提供了将要播放媒体的总时长。

cpp 复制代码
// duration为将要播放媒体的总时长
void QMediaPlayer::durationChanged(qint64 duration);

因此在MiniMusic类中给该信号关联槽函数,在槽函数中将duration更新到界面总时间即可。

cpp 复制代码
// minimusic.h 中新增
// 歌曲持续时长改变时(歌曲切换)
void onDurationChanged(qint64 duration);

// minimusic.cpp 中新增
void MiniMusic::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 MiniMusic::initPlayer()
{
    // ...

    // 媒体持续时长更新,即:音乐切换,时长更新,界面上时间也要更新
    connect(player,&QMediaPlayer::durationChanged,this,&MiniMusic::onDurationChanged);
}

b. 界面歌曲当前播放时间更新

媒体在持续播放过程中,QMediaPlayer会发射positionChanged,该信号带有一个qint64类型参数,表示媒体当前持续播放的时间。

cpp 复制代码
// position:媒体持续播放时间
void positionChanged(qint64 position);

因此,在MiniMusic中捕获该信号,便可获取到正在播放媒体的持续时间。

cpp 复制代码
// minimusic.h 中新增
// 播放位置改变,即持续播放时间改变
void onPositionChanged(qint64 duration);

// minimusic.cpp 中新增

void MiniMusic::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 MiniMusic::initPlayer()
{
    // ...

    // 播放位置发生改变,即以及播放时间更新
    connect(player,&QMediaPlayer::positionChanged,this,&MiniMusic::onPositionChanged);
}

在持续播放时间改变的同时,界面上的进度条应该也要前进。

5.3.11 进度条处理[seek功能]

1.seek功能介绍

播放器的seek功能指,通过时间或位置快速定位到视频或音频流的特定位置,允许用户在播放过程中随时跳转到特定时间点,从而快速找到感兴趣的内容或重新开始播放。

在界面上的体现是,当在MusicSlider上点击或者拖拽的时候,会跳转到歌曲的指定位置进行播放,并且歌曲的当前持续播放时间要同步修改。

2.进度条界面显示

进度条功能进度界面展示与音量调节位置类似,拦截鼠标按下、鼠标移动、以及鼠标释放消息即可。在内部捕获到鼠标的位置的横坐标x,将x作为outLine的宽度即可。即在鼠标按下、移动、释放的时候,修改outLine的宽度即可。

cpp 复制代码
// musicslider.h 中新增
void mousePressEvent(QMouseEvent *event);   // 重写鼠标按下事件
void mouseMoveEvent(QMouseEvent *event);    // 重写鼠标滚动事件
void mouseReleaseEvent(QMouseEvent *event); // 重写鼠标释放事件

void moveSilder();  // 重写鼠标释放事件

private:
    int currentPos; // 滑动条当前位置

// musicslider.cpp 中新增
MusicSlider::MusicSlider(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MusicSlider)
{
    ui->setupUi(this);

    // 初始情况下,还没有开始播放,将当前播放进度设置为0
    currentPos = 0;
    maxWidth = width();
    moveSilder();
}


void MusicSlider::mousePressEvent(QMouseEvent *event)
{
    // 注意:QMouseEvent中的pos()为鼠标相对于widget的坐标,不是相当于screen
    // 因此鼠标位置的x坐标可直接作为outLine的宽度
    currentPos = event->pos().x();
    moveSilder();
}

void MusicSlider::mouseMoveEvent(QMouseEvent *event)
{
    // 如果鼠标不在MusicSlider的矩形内,不进行拖拽
    QRect rect = QRect(0,0,width(),height());
    QPoint pos = event->pos();
    if(!rect.contains(pos))
    {
        return;
    }

    // 根据鼠标滑动的位置更新outLine的宽度
    if(event->buttons() == Qt::LeftButton)
    {
        // 验证:鼠标点击的x坐标是否越界,如果越界将其调整到边界
        currentPos = event->pos().x();
        if(currentPos < 0)
        {
            currentPos = 0;
        }
        if(currentPos > maxWidth)
        {
            currentPos = maxWidth;
        }
        moveSilder();
    }
}

void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
    currentPos = event->pos().x();
    moveSilder();
}

void MusicSlider::moveSilder()
{
    // 根据当前进度设置外部滑动条的位置
    ui->outLine->setMaximumWidth(currentPos);
    ui->outLine->setGeometry(0,8,currentPos,4);
}

3. 进度条同步持续播放时间

当鼠标释放之后,计算出进度条当前位置currentPos和总宽度的maxWidth比率,然后发射信号告诉MiniMusic,让player按照该比率更新持续播放时间。

cpp 复制代码
// musicslider.h新增
signals:
    void setMusicSliderPosition(float);

// musicslider.cpp新增

void MusicSlider::mouseReleaseEvent(QMouseEvent *event)
{
    currentPos = event->pos().x();
    moveSilder();
    
    emit setMusicSliderPosition((float)currentPos/(float)maxWidth);
}

// minimusic.h中新增
// 进度条改变
void onMusicSliderChanged(float value);

// minimusic.cpp 中新增
void MiniMusic::onMusicSliderChanged(float value)
{
    // 1.计算当前seek位置的时长
    qint64 duration = (qint64)(totalTime* value);

    // 2.转换为百分制,设置当前时间
    ui->currentTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0'))
                             .arg(duration/1000%60,2,10,QChar('0')));

    // 3.设置当前播放位置
    player->setPosition(duration);
}


void MiniMusic::connectSignalAndSlot()
{
    // ...
    // 进度条拖拽
    connect(ui->processBar,&MusicSlider::setMusicSliderPosition,this,&MiniMusic::onMusicSliderChanged);
}

4. 持续时间同步进度条

当播放位置更新时,界面上持续播放时间一直在更新,因此进度条也需要持续向前进,MusicSlider应该提供setStep函数,播放进度持续更新时,也将进度条通过setStep函数更新下。

cpp 复制代码
// musicslider.h 中新增
void setStep(float bf);

// musicslider.cpp 中新增
void MusicSlider::setStep(float bf)
{
    currentPos = maxWidth*bf;
    moveSilder();
}

// minimusic.cpp 中修改
void MiniMusic::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')));
    // 界面上的进度条也需要同时修改
    ui->processBar->setStep((float)duration/(float)totalTime);
}

5.3.12 歌曲名称、歌手和封面图片同步

在进行歌曲切换时候,歌曲名称、歌手以及歌曲的封面图,也需要更新到界面。歌曲名称、歌手可以再Music对象中进行获取,歌曲的封面图可以通过player到歌曲的元数据中获取,获取时需要使用"Thumbnaillmage"作为参数,注意有些歌曲可能没有封面图,如果没有设置一张默认的封面图。由于歌曲切换时,player需要将新播放歌曲作为播放源,并解析歌曲文件,如果歌曲文件是有效的才能播放;因此QQMusic类可以给QMediaPlayer发射的metaDataAvailableChanged(bool)信号关联槽函数,当歌曲更换时,完成信息的更新。

cpp 复制代码
// minimusic.h中新增
void onMetaDataAvailableChanged(bool available);

// minimusic.cpp 中新增
void MiniMusic::onMetaDataAvailableChanged(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);
}

5.4 lrc歌词同步

播放歌曲时,当点击"词"按钮后窗口会慢慢弹出,当点击隐藏按钮后,窗口会慢慢隐藏,且没有标题栏。内部显示当前播放歌曲的歌词,以及歌曲名称和作者。当点击下拉按钮时,窗口会隐藏起来。

5.4.1 Irc歌词界面分析

IrcPage中元素种类比较少,具体分析如下:

①和②为QLabel,分别显示作者和歌曲名称;

③~⑨均为QLabel,用来显示歌词,为当前正在播放歌词,③④⑤为当前播放歌词的前三句,⑦⑧⑨为当前播放歌词的后三句。歌词会随着播放时间持续,从下往上移动。

⑩为按钮,点击之后窗口隐藏。

5.4.2 Irc歌词界面布局

在qt create中新创建一个qt 设计师界面,命名为LrcPage,geometry的宽高修改为:1020*680。

①拖一个Widget到LrcPage中,objectName修改为bgStyle,选中LrcPage,然后点击垂直布局,并将LrcPage的margin和spacing修改为0;

②拖两个Widget到bgStyle中,objectName从上往下分别修改为lrcTop和lrcContent,IrcTop的

minimumSize和maximumSize的高修改为50;然后选中bgStyle点击垂直布局,并将bgStyle的

margin和spacing修改为0;

③拖一个按钮到IrcTop中,objectName修改为hideBtn,minimumSize和maximumSize的宽和高修

改为:30*50;拖一个Widget到IrcTop中,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

css 复制代码
#bgStyle
{
	border-image:url(:/images/bg.png);
}

*
{
	color:#FFFFFF;
}

lineCenter

css 复制代码
#lineCenter
{
	color:#1ECE9A;
}

hideBtn

css 复制代码
#hideBtn
{
	border:none;
}

5.4.3 LrcPage显示

在LrcPage的构造函数中,将窗口的标题栏去除掉;并给hideBtn关联clicked信号,当按钮点击时将窗口隐藏。

cpp 复制代码
// lrcPage.cpp 中添加

LrcPage::LrcPage(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::LrcPage)
{
    ui->setupUi(this);

    setWindowFlag(Qt::FramelessWindowHint);
    connect(ui->hideBtn,&QPushButton::clicked,this,[=]{
        hide();
    });

    ui->hideBtn->setIcon(QIcon(":/images/xiala.png"));

}

在mINIMusic中,创建LrcPage的指针,并在initUi()方法中创建窗口的对象,创建好之后将窗口隐藏起来;

在MiniMusic中,给lrcWord按钮添加槽函数,在槽函数中将窗口显示出来。

cpp 复制代码
// minimusic.h中添加
#include "lrcpage.h"

LrcPage *lrcPage;

// 歌词按钮槽函数
void onLrcWordClicked();

// minimusic.cpp 中添加
void MiniMusic::initUI()
{
    // ...
    // 实例化lrcWord对象
    lrcPage = new LrcPage(this);
    lrcPage->hide();
}


void MiniMusic::onLrcWordClicked()
{
    lrcPage->show();
}

void MiniMusic::connectSignalAndSlot()
{
    //...
    // 显示歌词窗口
    connect(ui->lrcWord,&QPushButton::clicked,this,&MiniMusic::onLrcWordClicked);
}

5.4.4 LrcPage添加动画效果

当点击QQMusic中"歌词"按钮时,IrcPage窗口是以动画效果显示出来的,当点击lrcPage上"下拉"按钮时,窗口先以动画的方式下移,动画结束后窗口隐藏。

1.窗口显示和上移动画

①MiniMusic的initUi函数中,创建lrcPage对象并将窗口隐藏;给lrcPage窗口添加上移动画,动画暂不开启

②MiniMusic中给"歌词"按钮添加槽函数,当按钮点击时,显示窗口,开启动画

cpp 复制代码
// minimusic.h 中新增
#include <QPropertyAnimation>

// 歌词按钮槽函数
void onLrcWordClicked();

private:
    QPropertyAnimation* lrcAnimation;

// minimusic.cpp 中新增
void MiniMusic::initUI()
{
    // ...
    
    // 窗口添加阴影效果
    QGraphicsDropShadowEffect* shadowEffect2 = new QGraphicsDropShadowEffect(this);
    shadowEffect2->setOffset(0,0);
    shadowEffect2->setColor("#000000"); // 黑色
    // 此处需要将圆角半径不能太大,否则动画效果有问题,可以设置为10
    shadowEffect2->setBlurRadius(10);
    this->setGraphicsEffect(shadowEffect2);

    // 实例化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 MiniMusic::onLrcWordClicked()
{
    // 显示窗口并开启动画
    lrcPage->show();

    lrcAnimation->start();
}

void MiniMusic::connectSignalAndSlot()
{
    // ...
    // 歌词按钮点击信号和槽函数
    connect(ui->lrcWord,&QPushButton::clicked,this,&MiniMusic::onLrcWordClicked);
}

2. 窗口隐藏和下移动画

LrcPage类中,在构造窗口时设置下移动画,给"下拉"按钮添加槽函数,当"下拉按钮"点击时,开启动画;当动画结束时,将窗口隐藏。

cpp 复制代码
// lrcpage.h 中新增
#include <QPropertyAnimation>

private:
    QPropertyAnimation* lrcAnimation;

// lrcpage.cpp 中新增
LrcPage::LrcPage(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::LrcPage)
{
    ui->setupUi(this);

    //...
    // 点击设置下拉按钮时开启动画
    connect(ui->hideBtn,&QPushButton::click,this,[=]{
        lrcAnimation->start();
    });

    // 动画结束时,将窗口隐藏
    connect(lrcAnimation,&QPropertyAnimation::finished,this,[=]{
       hide();
    });
    
}

5.4.5 Irc歌词解析和同步

1.什么是LRC歌词

Irc是英文lyric(歌词)的缩写,被用作歌词文件的扩展名。该文件将歌词和歌词出现的时间编辑到一起,当歌曲播放的时候,按照歌词文件中的时间依次将歌词显示出来。

标准格式:[分钟:秒.毫秒]歌词

其他格式:①[分钟:秒]歌词②[分钟:秒:毫秒]歌词

每首歌的lrc歌词有多行文本,因此lrc歌词中的每行可以采用结构体管理。

cpp 复制代码
// lrcpage.h 中新增
struct LyricLine
{
    qint64 time;    // 时间
    QString text;   // 歌词内容
    LyricLine(qint64 qtime,QString qtext)
        :time(qtime)
        ,text(qtext)
    {}
};

// lrcPage类中添加成员变量

QVector<LyricLine> lrcLines;    // 按照时间的先后次序保存每行歌词

2. 通过歌曲名找LRC文件

一般情况下,播放器在设计之初就会设计好歌曲文件和歌词文件的存放位置,以及对应关系,通常歌曲文件和lrc歌词文件名字相同,后缀不同。在磁盘存放的时候,可以将歌曲文件和lrc文件分两个文件夹存储,也可以存储到一个文件夹下。

本文为了方便处理,存储在一个文件夹下,因此可以通过Music对象快速找到lrc歌词文件。

cpp 复制代码
// music.h 中新增
QString getLrcFilePath() const;

// music.cpp 中新增
QString Music::getLrcFilePath() const
{
    // D:/musics/2002年的第一场学.mp3  歌曲文件路径
    // D:/musics/2022年的第一场学.lrc  歌词文件
    QString lrcPath = musicUrl.toLocalFile();
    lrcPath.replace(".mp3", ".lrc");
    lrcPath.replace(".flac", ".lrc");
    lrcPath.replace(".mpga", ".lrc");

    return lrcPath;
}

也可以将歌曲文件和lrc歌词文件分别存储到两个文件夹。

3. LRC歌词解析

找到Irc歌词文件后,由lrcPage类完成对歌词的解析。解析的大概步骤:

①打开歌词文件

②以行为单位,读取歌词文件中的每一行

③按照Irc歌词文件格式,从每行文本中解析出时间和歌词

00:17.94\]那些失眠的人啊你们还好吗 \[0:58.600.00\]你像一只飞来飞去的蝴蝶 ④用\<时间,行歌词\>构建一个LrcLine对象存储到IrcLines中。 ```cpp // lrcpage.h 中新增 bool parseLrcFile(const QString& lrcFilePath); // lrcpage.cpp 中新增 bool LrcPage::parseLrcFile(const QString &lrcFilePath) { // 1. 打开文件 QFile file(lrcFilePath); if(!file.open(QIODevice::ReadOnly)) { qDebug()<<"打开lrc文件:"<按照]分割 int start = 0, end = 0; end = lrclineWord.indexOf(']', start); QString lrcTime = lrclineWord.mid(start, end-start+1); QString lrcWord = lrclineWord.mid(end+1, lrclineWord.size()-end-1-1); // 2. 在时间中解析出分:秒.毫秒 // [0:17.94] [0:33.600.00] // 解析分 qint64 lineTime = 0; start = 1; end = lrcTime.indexOf(':', start); lineTime += lrcTime.mid(start, end - start).toInt()*60*1000; // 解析分并将其转化为毫秒 // 解析秒 start = end+1; end = lrcTime.indexOf('.', start); lineTime += lrcTime.mid(start, end - start).toInt()*1000; // 解析秒并将其转换为毫秒 // 解析毫秒 start = end+1; end = lrcTime.indexOf('.', start); lineTime += lrcTime.mid(start, end - start).toInt(); // 解析毫秒 // 3. 将该行给次保存 lrcWordLines.push_back(LrcWordLine(lineTime, lrcWord)); } for(auto e : lrcWordLines) { qDebug()<= lrcWordLines[i-1].lrcTime && time < lrcWordLines[i].lrcTime) { // 第i-1行还没有播放完 return i-1; } } // 最后一行唱完之后,歌曲结束了,但是还有收尾音乐 // 让歌词界面显示最后一行歌词 return lrcWordLines.size()-1; } QString LrcPage::getLrcWordByIndex(int index) { if(index < 0 || index >= lrcWordLines.size()) { return ""; } return lrcWordLines[index].lrcText; } void LrcPage::showLrcWordLine(qint64 time) { // 1. 根据当前所唱歌曲的时间来获取当前所唱歌曲在QVector中的索引 int index = getLrcWordLineIndex(time); // 2. 更新前三行 当前播放行 后三行到界面 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(getLrcWordByIndex(index-3)); ui->line2->setText(getLrcWordByIndex(index-2)); ui->line3->setText(getLrcWordByIndex(index-1)); ui->lineCenter->setText(getLrcWordByIndex(index)); ui->line4->setText(getLrcWordByIndex(index+1)); ui->line5->setText(getLrcWordByIndex(index+2)); ui->line6->setText(getLrcWordByIndex(index+3)); } } ``` **5. lrc歌词同步播放进度** 当歌曲发生切换时,需要完成lrc歌词文件的解析; 当歌曲播放进度发生改变时,根据歌曲的当前播放时间,通过lrcPage找到对应行歌词并显示出来。 ```cpp // minimusic.cpp 添加 void MiniMusic::onMetaDataAvailableChanged(bool available) { (int)(available); // 歌曲名称、歌曲作者直接到Musci对象中获取 // 此时需要知道媒体源在播放列表中的索引 QString musicId = currentPage->getMusicIdByIndex(currentIndex); auto it = musicList.findMusicByMusicId(musicId); //... // 解析歌曲的LRC歌词 if(it != musicList.end()) { // 获取lrc文件的路径 QString lrcPath = it->getLrcFilePath(); // 解析lrc文件 lrcPage->parseLrcFile(lrcPath); } } void MiniMusic::onPositionChanged(qint64 position) { // 更新当前播放时间 ui->currentTime->setText(QString("%1:%2").arg(position/1000/60, 2, 10, QChar('0')) .arg(position/1000%60, 2, 10, QChar('0'))); // 更新进度条的位置 ui->progressBar->setStep(position/(float)totalTime); // 在歌词界面同步显示歌词 if(currentIndex >= 0) { lrcPage->showLrcWordLine(position); } } ``` ## 6. 持久化支持 支持播放相关功能之后,每次在验证功能时都需要从磁盘中加载歌曲文件,非常麻烦。而且之前收藏的喜欢歌曲以及播放记录在程序关闭之后就没有了,这一般是无法接受的。 因此需要将每次在播放器上进行的操作保留下来,比如:所加载的歌曲、以及歌曲信息;收藏歌曲信息;历史播放等信息保存起来,当下次程序启动时,将保存的信息加载到播放器即可,这样就能将在播放器上的操作记录保留下来了。 要永久性保存,最简单的方式就是直接保存到文件,但是保存文件不安全,而且需要自己操作文件比较麻烦,本文采用数据库完成信息的持久保存。 ### 6.1 SQLite数据库介绍 常见的数据库管理系统有:Oracle、SqlServer、MySQL等,这些数据库管理系统有一个特点,必须先要在本地安装数据库系统的软件,然后才能使用数据库管理系统提供的服务。而数据库管理系统的安装和卸载,有时候兼职是噩梦,怎么装就是失败,最后导致重装操作系统。 本文操作一款轻量级、无需安装的桌面型数据库SQLite,SQLite是非常流行的开源嵌入式数据库,将源文件添加到工程就可以直接使用。它很好的支持关系型数据库所具备的一些基本特征,比如:标准SQL语法、事务、数据表和索引等。 SQLite主要特征: * 管理简单,甚至可以认为无需管理。 * 操作方便,SQLite生成的数据库文件可以在各个平台无缝移植。 * 可以非常方便的以多种形式嵌入到其他应用程序中,如静态库、动态库等。 * 易于维护。 Qt中已经内置了SQLite,在安装qt开发环境时,SQLite环境已经配置好了,用户在.pro文件中导入数据库模块就可以使用。 ```cpp // qqmusic.pro QT += sql ``` [SQLite 教程 \| 菜鸟教程![](https://csdnimg.cn/release/blog_editor_html/release2.4.6/ckeditor/plugins/CsdnLink/icons/icon-default.png)https://www.runoob.com/sqlite/sqlite-tutorial.html](https://www.runoob.com/sqlite/sqlite-tutorial.html "SQLite 教程 | 菜鸟教程") ### 6.2 QSqlDatabase类介绍 QSqlDatabase类主要处理与数据库的连接,它提供了创建、配置、打开和关闭数据库连接的方法。 #### 6.2.1 数据库连接和关闭 ````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 // minimusic.h 中新增 void on_connectDB_clicked(); // qqmusic.cpp 中新增 #include #include #include void MiniMusic::on_connectDB_clicked() { // 加载数据库驱动 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); // 设置数据库名字 db.setDatabaseName("MiniMusic.db"); // 打开数据库 if(!db.open()) { qDebug()<<"QQMusic:"< void MiniMusic::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()<<"学生表创建失败:"< QSqlDatabase sqlite; // minimusic.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(); } ``` #### 6.3.2 歌曲信息写入数据库 当程序退出的时候,通过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 #include 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()<<"查询失败: "< #include 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); } } // minimusic.h 中新增 void initMusicList(); // minimusic.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); } MiniMusic::MiniMusic(QWidget *parent) : QWidget(parent) , ui(new Ui::QQMusic) , currentIndex(-1) { // ... // 初始化数据库 initSQLite(); // 加载数据库歌曲文件 initMusicList(); // ... } void MiniMusic::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"); // ... } ``` ## 7. 边角问题处理 ### 7.1 更换主窗口图标 更换窗口图标,在主界面显示时,在标题栏显示设置的图标。 ```cpp void QQMusic::initUi() { // 设置无边框窗口 this->setWindowFlag(Qt::FramelessWindowHint); // 设置窗口背景透明 setAttribute(Qt::WA_TranslucentBackground); // 设置主窗口图标 setWindowIcon(QIcon(":/images/tubiao.png")); // ... } ``` ### 7.2 处理最大化、最小化按钮 由于窗口中控件并非全部基于Widget布局,有些控件的位置是计算死得,窗口最大化时有些控件可能无法适配尺寸,因此禁止窗口最大化。 ```cpp // minimusic.h 中新增 void on_skin_clicked(); void on_max_clicked(); void on_min_clicked(); // minimusic.cpp 中新增 void MiniMusic::on_skin_clicked() { QMessageBox::information(this, "温馨提示", "小哥哥正在加班紧急支持中..."); } void QQMusic::on_min_clicked() { showMinimized(); } void MiniMusic::initUi() { this->setWindowFlag(Qt::FramelessWindowHint); setAttribute(Qt::WA_TranslucentBackground); setWindowIcon(QIcon(":/images/tubiao.png")); // 设置主窗口图标 ui->max->setEnabled(false); // ... } ``` ### 7.3 歌词按钮的样式 ```css #lrcWord { border: none; background-image: url(:/images/ci.png); background-repeat: no-repeat; background-position: center center; } #lrcWord:hover { background-color: rgba(220, 220, 220, 0.5); } ``` 另外,LrcPage页面中的按钮,当鼠标放上去时,可以显示向上收拾样式,更容易识别出此处是按钮。具体操作,选中LrcPage.ui界面中hideBtn按钮,然后在属性页面找到cursor属性,然后选择指向收拾。 ### 7.4 CommonPage中滚动条格式 CommonPage中QScorllArea垂直滚动条的样式不太好看,可以借助CommonPage中QListWidget滚动条样式设置: ```css QScrollBar:vertical { border: none; width: 10px; background-color: #F0F0F0; margin: 0px 0px 0px 0px; } QScrollBar::handle:vertical { width: 10px; background-color: #E3E3E3; border-radius: 5px; min-height: 20px; } ``` 另外,推荐页面上:推荐、今日为你推荐、你的音乐补给,文本字体稍微有点大,可以往小调点。 比如:推荐字体大小为18;今日为你推荐和你的音乐补给字体大小为15; ### 7.5 BtForm上动画问题 当播放不同页面歌曲时,BtForm按钮上的跳动动画应该跟随播放页面变化而变化,即那个page页面播放,就应该让该页面的对应BtForm上的动画显示,其余BtForm按钮上的动画隐藏,这样跳动的音符始终就可以标记当前正在播放的页面。 Minimusic类中currentPage标记当前播放页面,QStackedWidget中提供了通过页面找索引的方法,即currentPage可以找到其在层叠窗口中的索引,该索引与BtForm中的pageld是对应的。因此在MiniMusic中定义updateBtFormAnimal函数,该函数实现原理如下: * 获取currentPage在stackedWidget中的索引 * 获取QQMusic中所有BtFrom\*的元素,保存到btFroms * 遍历btFroms,如果那个按钮的pageld等于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(); } } // minimusc.h 新增 void updateBtformAnimal(); // minimusic.cpp 新增 void MiniMusic::updateBtformAnimal() { // 获取currentPage在stackedWidget中的索引 int index = ui->stackedWidget->indexOf(currentPage); if(index == -1) { qDebug()<<"该页面不存在"; return; } // 获取QQMusic界面上所有的btForm QList btForms = this->findChildren(); for(auto btForm : btForms) { if(btForm->getPageId() == index) { // 将currentPage对鱼竿的btForm找到了 btForm->showAnimal(true); } else { btForm->showAnimal(false); } } } void MiniMusic::initUi() { // ... // 将localPage设置为当前页面 ui->stackedWidget->setCurrentIndex(4); currentPage = ui->localPage; // 本地下载BtForm动画默认显示 ui->localPage->showAnimal(true); // ... } ``` ### 7.6 点击BtForm偶尔窗口乱移问题 正常情况下,当按钮在非BtForm按钮上点击时,左键长按拖拽窗口才能移动。但是当点击BtForm按钮时,偶尔也能看到窗口移动,估计可能在BtForm上点击时,由于手抖或者其他原因,该次点击时间被qt识别成鼠标移动事件处理了,因此导致窗口移动了。 解决方案:在QQMusic类中设置一个成员变量isDrag,当鼠标点击时间发生时,如果点在BtForm按钮上则将isDrag设置为false,否则设置为true,此时如果鼠标长按滚动时,再移动窗口。 ```cpp // minimusic.h 中新增 bool isDrag; // 为true时候窗口才能拖拽 // minimusic.cpp 中新增 void MiniMusic::mouseMoveEvent(QMouseEvent *event) { if(event->buttons() == Qt::LeftButton && isDrag) { move(event->globalPos() - dragPos); qDebug()<<"mouse move"; return; } QWidget::mouseMoveEvent(event); } void MiniMusic::mousePressEvent(QMouseEvent *event) { if(event->button() == Qt::LeftButton) { isDrag = true; // 获取鼠标相对于电脑屏幕左上角的全局坐标 dragPos = event->globalPos() - geometry().topLeft(); return; } QWidget::mousePressEvent(event); } void MiniMusic::onBtFormClick(int pageId) { // 清除之前btForm按钮的颜色背景 // 获取所有的BtForm按钮 QList btFormList = this->findChildren(); for(auto btForm : btFormList) { if(btForm->getPageId() != pageId) { btForm->clearBackground(); btForm->showAnimal(false); } else { btForm->showAnimal(true); } } ui->stackedWidget->setCurrentIndex(pageId); qDebug()<<"切换页面"< QSet musicPaths; // musiclist.cpp 新增 void MusicList::addMusicsByUrl(const QList &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()<<"数据库查询失败:"< #include void MiniMusic::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 MiniMusic::on_quit_clicked() { // 点击关闭按钮时,程序不退出,隐藏掉 // 用户可以从系统托盘位置选择显示或者关闭 hide(); } void MiniMusic::quitQQMusic() { // 歌曲信息写入数据库 musicList.writeToDB(); // 断开与SQLite的链接 sqlite.close(); // 关闭窗口 close(); } ``` ### 7.9 保证程序只运行一次 现在每点击一次QQMusic.exe,都会创建一个QQMusic的实例,即同一个机器上可以运行多份程序实例,多个实例同时运行有以下缺陷: * 多个实例同时运行可能会导致资源浪费,如内存、CPU效率等 * 如果应用程序涉及对共享数据的修改,多个程序同时运行可能会导致数据不一致问题 * 若多个实例尝试访问同一资源时,如文件、数据库等,可能会导致冲突或错误 * 另外,用户体验不是很好,多个实例操作时容易混淆 因此有时会禁止程序多开,即一个应用程序只能运行一个实例,也称为单实例应用程序或单例应用程序。 在Qt中,禁止程序多开的方式有好几种,此处采用共享内存实现。 共享内存是操作系统中的概念,是进程间通信的一种机制。由于相同key值的共享内存只能存在一份,因此在程序启动时可以检测共享内存是否已经被创建,如果已经创建则说明程序已经在运行,否则程序还没有运行。 ```cpp #include #include 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(); } ``` ### 7.10 禁止qDebug()输出 要逐个删除程序中qDebug的打印太麻烦,可以再配置文件中通过添加以下语句,禁止qDebug输出: ```cpp # ban qDebug output DEFINES += QT_NO_DEBUG_OUTPUT ``` ### 7.11 项目打包 #### 7.11.1 为什么要打包 程序编译好之后,将exe可执行程序直接拷贝给同学或朋友运行,为什么运行不了? 答:Qt可执行程序在运行的时候,需要依赖Qt框架中的一些库文件,如果对方及其上之前未安装Qt环境,点击可执行程序运行时,会提示缺少xXX.dll动态库信息等。为了让开发好的Qt可执行程序在未安装Qt环境的机器上也可以运行,就需要对项目进行打包,打包的过程会将exe可执行程序运行时所需的依赖文件全部整合到一起,将打包好的包一起发给对端,双击exe可执行程序时就可以执行。 注意:打包时exe需要用release版本,debug是调试版本,release版本编译器会去除调试信息,并会对工程进行优化等操作,使程序体积更小,运行效率更高。 #### 7.11.2 windeployqt打包工具 windeployqt是Qt提供的一个工具,用于自动收集并复制运行Qt应用程序所需的动态链接库(.dll 文件)及其他资源(如插件、QML模块等)到可执行文件所在的目录。这样你就可以将应用程序和这些依赖项一起打包,确保在没有Qt环境的其他机器上也能运行。 **【主要功能】** * 自动收集依赖项:windeployqt会分析你的Qt应用程序,确定它所依赖的Qt库文件(如Qt6Core.dll,Qt6Widgets.dll),并将这些文件复制到应用程序的目录。 * 处理插件和QML模块:如果你的应用程序使用了Qt的插件(如平台插件qwindows.dll或图形驱动插件等),windeployqt也会将这些插件一并打包。对于使用QML的应用程序,它也会自动收集必要的QML模块。 * 处理资源文件:如果你的应用程序包含了Qt的资源文件(如图标、翻译文件等),它也会确保这些资源正确包含在最终的应用程序中。 #### 7.11.3 打包步骤 1. 配置好Qt环境变量 2. 选择以release方式编译程序。编译好之后,在工程目录上一层会生成包含release字段的文件夹,文件夹内部就有release模式的可执行程序。 3. 将新建一个文件夹,命名为QQMusic,将release模式可执行程序拷贝到QQMusic。 4. 进入QQMusic,在该文件夹内部,按shift,然后鼠标右键单击,弹出菜单中选择"在此处打开Powershell 窗口(S)",在弹出窗口中输入windeployqt.IQQMusic.ext,windeployqt工具就会自动完成打包。 ![](https://i-blog.csdnimg.cn/direct/9ad730825f894815be02b10efcee997f.png) ![](https://i-blog.csdnimg.cn/direct/69298fabfaef4920afa8ca7f3ea566cb.png) 如果环境变量没有配置好,可能会出现以下问题: **a.qt安装后对应构建器的环境变量没有配置** 本项目使用mingw64构建,因此将qt安装目录下, D:\\ApplyTool\\ProgramTool\\QT\\5.14.2\\mingw73_64\\bin目录配置到环境变量。 **b.配置过程中如果报警告:Cannot find GCC installation directory..** 是g++工具目录没有配置到环境变量中,qt环境安装后,g++可能在其他目录,我电脑上是在: D:\\ApplyTool\\ProgramTool\\QT\\Tools\\mingw730_64\\bin目录下,将该目录配置到环境变量中。 配置好之后,将该目录压缩之后,发给对方,对方收到之后直接解压,点击exe之后就可以运行。 ## 8. 项目总结和面试 ### 8.11 项目总结 ![](https://i-blog.csdnimg.cn/direct/2a5de66ae0ea48eaa938453e297ac8d9.png) ### 8.3 项目扩展 * 支持换肤功能 * 增加登录/注册/账户管理 * 支持简单的本地搜索 * 增加网络模块,完善在线歌曲和推荐系统模块 * 程序最小化后,以腾讯QQMusic中歌词页面方式显示歌词 ## 附录 [https://gitee.com/Axurea/music![](https://csdnimg.cn/release/blog_editor_html/release2.4.6/ckeditor/plugins/CsdnLink/icons/icon-default.png)https://gitee.com/Axurea/music](https://gitee.com/Axurea/music "https://gitee.com/Axurea/music")

相关推荐
森G8 小时前
46、环境配置---------QChart
c++·qt
冉佳驹12 小时前
Qt【第六篇】 ——— 事件处理、多线程、网络与文件等操作详解
qt·http·udp·tcp·事件·多线程与互斥锁
用户8055336980313 小时前
嵌入式Linux驱动开发——模块参数与内核调试:让模块"活"起来的魔法
qt
冉佳驹14 小时前
Qt【第七篇】 ——— QSS 样式表与绘图 API 核心用法及 UI 定制功能总结
qt·qbrush·qpainter·qss·paintevent·qpen
森G14 小时前
45、QGraphicsScene 与 QGraphicsView 框架---------绘图
c++·qt
sycmancia15 小时前
QT——计算器核心算法
开发语言·qt·算法
Pyeako1 天前
PyQt5 + PaddleOCR实战:打造桌面级实时文字识别工具
开发语言·人工智能·python·qt·paddleocr·pyqt5
FL16238631291 天前
基于yolov26+pyqt5的混凝土墙面缺陷检测系统python源码+pytorch模型+评估指标曲线+精美GUI界面
python·qt·yolo
森G1 天前
39、拓展知识---------事件系统
c++·qt