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

Qt 实操记录:打造自己的“ QQ 音乐播放器”

在这里插入图片描述

目录

  • 一.界面设计
    • 1.成品界面分析
    • 2.head界面实现
    • 3.body界面实现
    • 4.主界面设置
      • (1).设置无标题栏与阴影效果
      • (2).重写鼠标事件实现拖拽
  • 二.自定义控件
    • 1.BtFrom界面设计
    • 2.推荐页面设计
    • 3.recBox页面设计
    • 4.recBoxItem页面设计
      • (1).eventFilter介绍和使用
      • (2).QJsonObject介绍和使用
      • (3).向recBox中添加元素
      • (4).处理recBox的轮番按钮
    • 5.commonPage页面设计
    • 6.ListItemBox页面设计
    • 7.MusicSlider设计
    • 8.VolumeTool设计
  • 三.媒体类进行歌曲管理
    • 1.音乐的加载和分析
      • (1).MIME过滤器
      • (2).MusicList类
      • (3).Music类
      • (4).QMediaPlayer类解析元数据
    • 2.音乐分类
      • (1).更新music到CommonPage页面上
      • (2).收藏音乐
  • 四.播放控制区域实现
    • 1.播放控制类介绍
      • (1).QMediaPlayer类
      • (2).QMediaPlayList类
    • 2.歌曲播放
    • 3.播放与暂停
    • 4.上一曲和下一曲
    • 5.播放模式切换
    • 6.播放全部
    • 7.双击ListitemBox播放
    • 8.最近播放同步
    • 9.音量功能
    • 10.歌曲播放时间处理
    • 11.进度条设置
      • (1).进度条界面显示
      • (2).进度条同步播放时间
    • 12.修改歌曲信息
    • 13.lrc歌词设计
      • (1).lrc歌词页面动画设计
      • (2).歌词解析
  • 五.数据库实现持久化
    • 1.SQLite介绍
    • 2.QSqlDatabase类介绍
    • 3.数据库初始化
      • (1).initSQLite
      • (2).歌曲信息写入数据库
      • (3).程序启动读取数据库歌曲信息
  • 六.优化与debug
    • 1.添加系统托盘
    • 2.换肤最大最小化处理
    • 3.重复从本地添加音乐bug
    • 4.保证程序只运行⼀次
  • 七.总结

一.界面设计

1.成品界面分析

当我们将完成的音乐播放器程序运行起来后:
在这里插入图片描述

观察分析可得,主界面大致可分为几个部分:head(headleft和headright)和body(bodyleft和bodyright(层叠窗口和播放控制区域))。
在这里插入图片描述

2.head界面实现

据以上可得head从左至右分别为一个logo、搜索框、换肤按钮、最小化、最大化、关闭按钮。可以用QLabel QLineEdit 和四个QPushButton实现:
在这里插入图片描述
在这里插入图片描述

在headright中有一根垂直弹簧用来保证head在整个widget上保证一定的高度 不会被body过度挤压。
logo的QSS优化如下:

#logo{
	background-image:url(":/images/Logo.png");
	background-repeat:no-repeat;//不重复显示
	background-position:center center;//设置水平垂直居中
	border:none;//无边框
}

换肤最小最大化和关闭设置一个背景图片并且添加hover效果即可:

QPushButton:hover{
	background-color:rgba(225,0,0,0.5);
}

3.body界面实现

首先来实现bodyleft,观察成品可知需要两个Widget分别实现在线音乐我的音乐两个块功能,和headright一样这里也需要一条垂直弹簧将两块功能区顶起来:
在这里插入图片描述
bodyright则稍复杂些包括了三个部分:层叠窗口用来对应在线音乐和我的音乐的六个页面,进度条用来标识歌曲播放进度,播放控制区域用来进行对播放控制,以及歌曲封面和信息的显示。
在这里插入图片描述

4.主界面设置

(1).设置无标题栏与阴影效果

当我们把主界面的ui大致设置好以后,我们将程序跑起来发现widget还有标题栏,这个是多余的需要去掉。
在这里插入图片描述

setWindowFlag(Qt::FramelessWindowHint);//设置为无标题栏

但是当我们再次运行发现 在界面的四周应该添加上一定的阴影效果 看起来更好些。
这时候就需要用到QGraphicsDropShadowEffect 类,这是qt中专门用来添加阴影效果的:

QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(this);

    shadow->setColor("#000000");
    shadow->setBlurRadius(5);
    shadow->setOffset(0,0);

    this->setGraphicsEffect(shadow);

setColor用来设置阴影的颜色,setBlurRadius用来设置阴影的模糊半径,setOffset用来设置阴影的偏移,设置好shadow后将其设置到ui界面上即可。

(2).重写鼠标事件实现拖拽

为了实现对窗口的拖拽,要对鼠标按下和移动事件进行重写:

QPoint Relativedistance;//相对距离
void Widget::mouseMoveEvent(QMouseEvent *event)
{
    if(event->buttons() == Qt::LeftButton)
    {
        move(event->globalPos()-Relativedistance);
        return ;
    }
    QWidget::mouseMoveEvent(event);
}

//鼠标按下事件
void Widget::mousePressEvent(QMouseEvent *event)
{
    if(event->button() == Qt::LeftButton)
    {
        //鼠标相对于窗口左上角的距离
        Relativedistance = event->globalPos() - geometry().topLeft();
        return ;
    }
    QWidget::mousePressEvent(event);
}

如此以后即可对窗口进行拖拽了。

二.自定义控件

1.BtFrom界面设计

BtFrom是用来填充bodyLeft内部onlineMusic和MyMusic中的QWidget全部提升为BtForm的。
设计如下:
在这里插入图片描述
从左到右分别用来放置图标,文字描述,播放的动画效果
在btfrom类中添加方法setIconAndText:用来设置图标和文字以及标识当前页面的索引。

int pageid;
void BtFrom::setIconAndText(const QString &icon, const QString &text,int id)
{
    ui->bticon->setPixmap(QPixmap(icon));
    ui->btText->setText(text);

    pageid = id;
}

以及我们点击对应的btfrom时应该添加一个绿色的hover效果
如图所示:
在这里插入图片描述

void BtFrom::mousePressEvent(QMouseEvent *event)
{
    (void)event;//防止未使用警告

    ui->btStyle->setStyleSheet("background-color:rgb(30,206,154)");

    emit btclick(this->pageid);
}

向Widget发送按钮点击信号,并且传递是哪个页面,Widget中接受并绑定槽函数处理此信号:

connect(ui->Rec,&BtFrom::btclick,this,&Widget::on_btFrom);
    connect(ui->radio,&BtFrom::btclick,this,&Widget::on_btFrom);
    connect(ui->music,&BtFrom::btclick,this,&Widget::on_btFrom);
    connect(ui->like,&BtFrom::btclick,this,&Widget::on_btFrom);
    connect(ui->local,&BtFrom::btclick,this,&Widget::on_btFrom);
    connect(ui->recent,&BtFrom::btclick,this,&Widget::on_btFrom);
void Widget::on_btFrom(int id)
{
    QList<BtFrom*> btfromlist = this->findChildren<BtFrom*>();

    for(auto btfrom : btfromlist)
    {
        if(btfrom->getPageid() != id)
        {
            btfrom->clearBground();
        }

    }

    ui->stackedWidget->setCurrentIndex(id);

}

点击了哪个按钮,就将之前按钮的绿色背景即hover效果去掉:

void BtFrom::clearBground()
{
    ui->btStyle->setStyleSheet("#btStyle:hover{background-color:#D8D8D8;}");
}

接下来实现btfrom上的动画效果,这里会使用到QPropertyAnimation 类通过在一定时间内逐渐改变对象的属性值来创建动画效果。可以指定动画的起始值、结束值、持续时间和缓动曲线等参数,从而实现各种动画效果。
对象的构造函数:

QPropertyAnimation(QObject *target, const QByteArray &propertyName, QObject *parent = nullptr);

target:要进行动画处理的对象。
propertyName:要进行动画处理的属性名称,以 QByteArray 类型表示。
parent:动画对象的父对象,默认为 nullptr。

	QPropertyAnimation* line1Animation;
    QPropertyAnimation* line2Animation;
    QPropertyAnimation* line3Animation;
    QPropertyAnimation* line4Animation;
	line1Animation = new QPropertyAnimation(ui->line1, "geometry", this);
    line1Animation->setDuration(1500);
    line1Animation->setKeyValueAt(0, QRect(0, 25, 2, 0));
    line1Animation->setKeyValueAt(0.5, QRect(0, 0, 2, 25));
    line1Animation->setKeyValueAt(1, QRect(0, 25, 2, 0));
    line1Animation->setLoopCount(-1);
    line1Animation->start();

setDuration用来设置持续时间 setKeyValueAt用来设置关键帧 setLoopCount设置循环次数,接着将四个空间全设置上即可完成动画效果。

// 设置line2的动画效果
    line2Animation = new QPropertyAnimation(ui->line2, "geometry", this);
    line2Animation->setDuration(1600);
    line2Animation->setKeyValueAt(0, QRect(7, 25, 2, 0));
    line2Animation->setKeyValueAt(0.5, QRect(7, 0, 2, 25));
    line2Animation->setKeyValueAt(1, QRect(7, 25, 2, 0));
    line2Animation->setLoopCount(-1);
    line2Animation->start();
     // 设置line3的动画效果
     line3Animation = new QPropertyAnimation(ui->line3, "geometry", this);
     line3Animation->setDuration(1700);
     line3Animation->setKeyValueAt(0, QRect(14, 25, 2, 0));
     line3Animation->setKeyValueAt(0.5, QRect(14, 0, 2, 25));
     line3Animation->setKeyValueAt(1, QRect(14, 25, 2, 0));
     line3Animation->setLoopCount(-1);
     line3Animation->start();
     // 设置line4的动画效果
     line4Animation = new QPropertyAnimation(ui->line4, "geometry", this);
     line4Animation->setDuration(1800);
     line4Animation->setKeyValueAt(0, QRect(21, 25, 2, 0));
     line4Animation->setKeyValueAt(0.5, QRect(21, 0, 2, 25));
     line4Animation->setKeyValueAt(1, QRect(21, 25, 2, 0));
     line4Animation->setLoopCount(-1);
     line4Animation->start();

2.推荐页面设计

推荐页面如下图所示:
在这里插入图片描述
推荐页面主要由五部分组成比较简单:
在这里插入图片描述

3.recBox页面设计

推荐页面上我们需要设计一个recBox页面进行填充
在这里插入图片描述
主要有左右两个按钮就行切换轮放推荐上的封面图,封面图则使用上下两个布局管理器管理。

在这里插入图片描述
并且对按钮设置样式表QSS,包括背景图片、hover效果等。

#btUp{
	border:none;
	background-image:url(:/images/up_page.png);
	background-repeat:no-repeat;
	background-position:center center;
}

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

4.recBoxItem页面设计

接下来我们就要对recBox的布局中添加封面项,所以要设计一个recBoxitem:
在这里插入图片描述
接着我们要处理对item的鼠标进入离开的事件拦截:
在这里插入图片描述
就是如上图所示的动画效果,这里就要使用到eventFilter

(1).eventFilter介绍和使用

eventFilter 是 QObject 类中的一个虚函数,用于实现事件过滤机制。eventFilter 函数的原型如下:

bool QObject::eventFilter(QObject *watched, QEvent *event);

需要注意的在使用eventFilter拦截事件前 应先为控件安装事件过滤器

recBoxitem::recBoxitem(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::recBoxitem)
{
    ui->setupUi(this);
    //安装事件过滤器
    ui->musicImageBox->installEventFilter(this);
}

接下来在eventFilter中拦截鼠标进入离开事件,编写动画效果:

bool recBoxitem::eventFilter(QObject *watched, QEvent *event)
{
    if(watched == ui->musicImageBox)
    {
        if(event->type() == QEvent::Enter)
        {

            QPropertyAnimation* animation = new QPropertyAnimation(watched, "geometry");
            animation->setDuration(300);
            animation->setStartValue(QRect(9, 9, ui->musicImageBox->width(), ui->musicImageBox->height()));
            animation->setEndValue(QRect(9, 0, ui->musicImageBox->width(), ui->musicImageBox->height()));
            animation->start();
            connect(animation,&QPropertyAnimation::finished,this,[=](){
                    delete animation;
            });
            return true;
        }
        else if(event->type()==QEvent::Leave)
        {
            QPropertyAnimation* animation = new QPropertyAnimation(watched, "geometry");
            animation->setDuration(300);
            animation->setStartValue(QRect(9, 0, ui->musicImageBox->width(), ui->musicImageBox->height()));
            animation->setEndValue(QRect(9, 9, ui->musicImageBox->width(), ui->musicImageBox->height()));
            animation->start();
            connect(animation,&QPropertyAnimation::finished,this,[=](){
                    delete animation;
            });
            return true;
        }
    }
    return QObject::eventFilter(watched,event);
}
  

connect中使用lambda表达式销毁动画对象。还有个需要注意的细节,在当前函数中如果正确处理的鼠标进入或者离开事件则返回true,否则要继续调用基类的eventFilter。
此外还需要添加文本和图片的方法:

void recBoxitem::setText(const QString &text)
{
    ui->recBoxItemText->setText(text);
}

void recBoxitem::setimagepath(const QString& path)
{
    QString style = "background-image:url("+path+");";
    ui->recMusicImage->setStyleSheet(style);
}

(2).QJsonObject介绍和使用

QJsonObject 是 Qt 库中用于处理 JSON对象的类。JSON 对象是由键值对组成的无序集合,键是字符串,值可以是字符串、数字、布尔值、数组、另一个 JSON 对象或 null。

QJsonObject::iterator insert(const QString &key, const QJsonValue &value);

以及一下常用方法:

QJsonValue QJsonObject::value(const QString &key) const

用key值返回value
QJsonArray则是用来存放QJsonValue的类型:
我们先来看下图片所在的目录:
在这里插入图片描述
进而编写随机存放图片的代码:

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
    , currindex(-1)
{
    ui->setupUi(this);
    //设置随机数种子
    srand(time(NULL));

    initUi();

}
QJsonArray Widget::RanddomPicture()
{
    QVector<QString> imagesArry;
    imagesArry<<"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(imagesArry.begin(),imagesArry.end());

    QJsonArray JsonArry;
    for(int i=0;i<imagesArry.size();i++)
    {
        QJsonObject obj;
        obj.insert("path",":/images/rec/"+imagesArry[i]);

        QString temp = QString("推荐-%1").arg(i,3,10,QChar('0'));

        obj.insert("text",temp);
        JsonArry.append(obj);

    }
    return JsonArry;
}

其中的random_shuffle 是 C++ 标准库中的一个算法,用于对指定范围内的元素进行随机重排。不过在 C++20 标准中被移除,替代它的是 std::shuffle,大家可以自行去了解。

(3).向recBox中添加元素

因为在recpage中有上下两个控件,分别为一行四列和两行四列,为了标识,我们需要在recBox类中添加行数和列数的成员变量:

	int row;//行数
    int col;//列数

    QJsonArray imageList;//保存图片

其中在构造函数中我们将变量初始化为一行四列。
实现代码:

void RecBox::initrecBox(QJsonArray jsonarray, int row)
{
    if(row == 2)
    {
        this->row = row;
        this->col = 8;
    }
    else
    {
        ui->recListDown->hide();
    }
    imageList = jsonarray;
    creatRecBoxitem();
}

void RecBox::creatRecBoxitem()
{
    //清除两个布局中的item
    QList<recBoxitem*> retUplist = ui->recListUp->findChildren<recBoxitem*>();
    for(auto e : retUplist)
    {
        ui->recListUpHLayout->removeWidget(e);
        delete e;
    }

    QList<recBoxitem*> retDownlist = ui->recListDown->findChildren<recBoxitem*>();
    for(auto e : retDownlist)
    {
        ui->recListDownHLayout->removeWidget(e);
        delete e;
    }


	for(int i = 0; i < col; ++i) 
	{ 
		RecBoxItem* item = new RecBoxItem(); 

		QJsonObject obj = imageList[i].toObject(); 
		item->setRecText(obj.value("text").toString()); 
		item->setRecImage(obj.value("path").toString()); 
		if(i >= col/2 && row == 2)
		{ 
			 ui->recListDownHLayout->addWidget(item); 
 		} 
 		else 
 		{ 
			ui->recListUpHLayout->addWidget(item); 
		} 
 	}  
}

(4).处理recBox的轮番按钮

接着我们要向recBox中的两个按钮添加槽函数 用来切换上一页和下一页。为了实现这一功能 我们要对图片进行分组,

    int currentindex;
    int count;
void RecBox::initrecBox(QJsonArray jsonarray, int row)
{
    if(row == 2)
    {
        this->row = row;
        this->col = 8;
    }
    else
    {
        ui->recListDown->hide();
    }
    imageList = jsonarray;

    currentindex = 0;
    count = ceil(imageList.size()/col);//是否向上取整?


    creatRecBoxitem();
}

currentindex用来标识当前是第几组,count标识一共有多少组。count向上取整来计算是为了将所有的图片都能显示出来。
这样两个槽函数也很容易就能写出来了:


void RecBox::on_btDown_clicked()
{
    currentindex++;
    if(currentindex>=count)
    {
        currentindex = 0;
    }

    creatRecBoxitem();
}

void RecBox::on_btUp_clicked()
{
    currentindex--;
    if(currentindex<0)
    {
        currentindex = count-1;
    }
    creatRecBoxitem();
}
//Widget.cpp中调用

ui->recMusicBox->initrecBox(RanddomPicture(),1);
ui->supplyMusicBox->initrecBox(RanddomPicture(),2);

5.commonPage页面设计

commonPage页面是用来放在我喜欢、本地音乐和最近播放这三个页面的。
在这里插入图片描述
ui设计如下
在这里插入图片描述
因为这一个页面要放在三个不同的地方,所以会有不同的标题和封面,应给出一个方法用来设置:

void CommonPage::setCommonPageUI(const QString &text, const QString &path)
{
    ui->pageTittle->setText(text);
    ui->musicImageLabel->setPixmap(QPixmap(path));

    ui->musicImageLabel->setScaledContents(true);
}

然后再Widget.cpp中调用给出相应的参数即可。

6.ListItemBox页面设计

ui设计如下图所示:

在这里插入图片描述
接着对控件添加一个鼠标事件:

void ListitemBox::enterEvent(QEvent *event)
{
    (void)event;
    setStyleSheet("background-color:#EFEFEF");
}

void ListitemBox::leaveEvent(QEvent *event)
{
    (void)event;
    setStyleSheet("");
}

7.MusicSlider设计

接下来我们再为进度条设计一个ui:
在这里插入图片描述
具体的进度条逻辑我们后续再做。

8.VolumeTool设计

接下来我们设计一个音量调节的控件:
在这里插入图片描述
因为音量控件属于弹出窗口,所以要设置窗口无框和弹出窗口:

VolumeTool::VolumeTool(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::VolumeTool),
    isMuted(false),//默认非静音
    volume(20)
{
    ui->setupUi(this);

    //设置弹出窗口
    setWindowFlags(Qt::Popup | 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/volumn.png"));

     ui->outSlider->setGeometry(ui->outSlider->x(),180+25-36,ui->outSlider->width(),36);

     ui->sliderBtn->setGeometry(ui->sliderBtn->x(),ui->outSlider->y()-ui->sliderBtn->height()/2,ui->sliderBtn->width(),ui->sliderBtn->height());


}

在Widget中编写音量按钮的槽函数:

void Widget::on_volume_clicked()
{
    QPoint point = ui->volume->mapToGlobal(QPoint(0,0));

    QPoint volumeLeftTop = point - QPoint(volumetool->width()/2,volumetool->height());
    volumeLeftTop.setX(volumeLeftTop.x()+16);
    volumeLeftTop.setY(volumeLeftTop.y()+28);
    volumetool->move(volumeLeftTop);
    volumetool->show();
}

接着再把三角形画出来:

void VolumeTool::paintEvent(QPaintEvent *event)
{
    (void)event;
    //绘制三角形
    //创建绘画对象
    QPainter painter(this);

    //设置画笔
    painter.setPen(Qt::NoPen);

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

    QPolygon polygon;
    QPoint a(10,300);
    QPoint b(10+80,300);
    QPoint c(10+40,300+20);

    polygon.append(a);
    polygon.append(b);
    polygon.append(c);

    painter.drawPolygon(polygon);

}

最后这个音量控件就能正确的显示出来了:
在这里插入图片描述

三.媒体类进行歌曲管理

1.音乐的加载和分析

(1).MIME过滤器

我们对添加本地音乐的按钮编写槽函数:

void Widget::on_addLocal_clicked()//添加到本地音乐
{
    //创建文件对话框
    QFileDialog filedialog(this);
    //设置窗口标题
    filedialog.setWindowTitle("添加到本地音乐");

    filedialog.setAcceptMode(QFileDialog::AcceptOpen);
    filedialog.setFileMode(QFileDialog::ExistingFiles);

    //设置对话框的MIME过滤器
    QStringList mimelist;
    mimelist<<"application/octet-stream";

    filedialog.setMimeTypeFilters(mimelist);

    QDir dir(QDir::currentPath());
    dir.cdUp();
    QString music = dir.path() + "/Musicplayer/musics";
    qDebug()<<dir.path();
    filedialog.setDirectory(music);

    if(filedialog.exec() == QFileDialog::Accepted)
    {
        //界面设置为本地下载
        ui->stackedWidget->setCurrentIndex(4);

        QList<QUrl> UrlList = filedialog.selectedUrls();
        //读取保存打开的音乐的url
        //保存到本地音乐中

        musiclist.addMusic(UrlList);
        ui->localpage->reFresh(musiclist);//将音乐信息添加到本地下载页面

        //将本地下载的音乐添加到媒体播放列表中

        ui->localpage->addMusicTOPlayList(musiclist,playlist);
    }


}

MIME过滤器主要用于在文件对话框中限制用户能够选择的文件类型。例如上面的代码:

	QStringList mimelist;
    mimelist << "application/octet-stream";
    filedialog.setMimeTypeFilters(mimelist);

QStringList mimelist;:创建一个字符串列表,用于存储 MIME 类型。pplication/octet-stream 通常表示二进制数据
filedialog.setMimeTypeFilters(mimelist);将 MIME 类型列表设置到文件对话框中,限制用户只能选择符合该 MIME 类型的文件。

(2).MusicList类

我们创建一个MusicList类用来管理音乐。
首先我们先来了解一下歌曲文件的类型:

  • audio/mpeg: 适⽤于mp3格式的⾳乐⽂件
  • audio/flac: ⽆损压缩的⾳频⽂件,不会破坏任何原有的⾳频信息
  • audio/wav : 表⽰wav格式的歌曲⽂件

在MusicList类中添加变量和方法将添加到本地的音乐url保存起来:

 QVector<Music> musiclist;//管理一个个的Music对象
void MusicList::addMusic(const QList<QUrl> &urls)
{
    for(auto e: urls)
    {
        //检测歌曲的MIME类型 过滤有效歌曲文件
        QMimeDatabase file;
        QMimeType type = file.mimeTypeForFile(e.toLocalFile());
        QString ret = type.name();

        if(ret == "audio/mpeg" || ret == "audio/flac"||ret == "audio/wav")
        {
            //构建music对象 并且添加进musiclist管理
            const Music music(e);
            //处理读取数据库后再次从本地添加音乐在页面上重复问题(数据库不重复UNIQUEmusicName)
            musiclist.push_back(music);
        }

    }
}

QMimeDatabase::mimeTypeForFile用于根据文件来确定其 MIME 类型。

(3).Music类

music类用来管理一个一个的音乐对象,其包含多种属性和方法:

class Music
{
public:
    Music();

    Music(QUrl url);

    void setMusicName(const QString& musicName);
    void setMusicSinger(const QString& musicSinger);
    void setMusicAlbum(const QString& musicAlbum);
    void setMusicUuid(QString uuid);
    void setIsLike(bool isLike);
    void setIsHistory(bool IsHistory);

    void setMusicDuration(const qint64 duration);
    void setMusicUrl(const QUrl musicUrl);


    QString getMusicName()const;
    QString getMusicSinger()const;
    QString getMusicAlbum()const;

    bool getIsLike()const;
    bool getIsHistory()const;

    qint64 getMusicDuration()const;
    QUrl getMusicUrl()const;

    QString getMusicUuid()const;

    QString getLrcFilePath();
    //将music信息插入musicInfo
    void insertMusicTODB();

    //运算符重载
    bool operator==(const Music& t)const;
private:
    void parseMediaMetaData();

private:
    QString musicName;
    QString musicSinger;
    QString musicAlbum;

    QString musicUuid;//musicid标识音乐

    qint64 duration;
    bool isLike;
    bool isHistory;

    QUrl musicUrl;
};

属性包含:歌曲名、歌手名、专辑名、歌曲的UUid、持续时长、是否为‘我喜欢’、是否为历史播放、音乐Url。

(4).QMediaPlayer类解析元数据

QMediaPlayer类用于在 Qt 应用程序中播放各种多媒体文件,如音频、视频等。使用这个类解析元数据:

void Music::parseMediaMetaData()
{
    // 创建一个 QMediaPlayer 对象用于播放媒体文件并获取元数据
    QMediaPlayer player;
    // 设置媒体播放器的媒体源为 musicUrl 所指向的音乐文件
    player.setMedia(musicUrl);

    // 等待媒体文件的元数据加载完成
    while(!player.isMetaDataAvailable())
    {
        // 让事件循环继续,避免界面冻结,确保程序可以响应其他事件
        QCoreApplication::processEvents();
    }

    // 检查元数据是否可用
    if(player.isMetaDataAvailable())
    {
        // 从元数据中获取音乐名称
        musicName = player.metaData("Title").toString();
        // 从元数据中获取歌手信息
        musicSinger = player.metaData("Author").toString();
        // 从元数据中获取专辑信息
        musicAlbum = player.metaData("AlbumTitle").toString();
        // 从媒体播放器中获取音乐的时长(以毫秒为单位)
        duration = player.duration();

        // 处理音乐文件元数据为空的情况,尝试从文件名中提取信息
        // 获取音乐文件的文件名
        QString musicUrlName = musicUrl.fileName();
        // 查找文件名中 '-' 的位置
        int index = musicUrlName.indexOf('-');

        // 若音乐名称为空
        if(musicName.isEmpty())
        {
            // 若文件名中包含 '-'
            if(index != -1)
            {
                // 从文件名中截取 '-' 之前的部分作为音乐名称,并去除前后空格
                musicName = musicUrlName.mid(0,index).trimmed();
            }
            else
            {
                // 若文件名中不包含 '-',截取文件名中 '.' 之前的部分作为音乐名称,并去除前后空格
                musicName = musicUrlName.mid(0,musicUrlName.indexOf('.')).trimmed();
            }
        }

        // 若歌手信息为空
        if(musicSinger.isEmpty())
        {
            // 若文件名中不包含 '-'
            if(index == -1)
            {
                // 将歌手信息设置为“未知歌手”
                musicSinger = "未知歌手";
            }
            else
            {
                // 从文件名中截取 '-' 之后、'.' 之前的部分作为歌手信息,并去除前后空格
                musicSinger = musicUrlName.mid(index+1,musicUrlName.indexOf('.')-1-index).trimmed();
            }
        }


        if(musicAlbum.isEmpty())
        {
            musicAlbum = "未知专辑";
        }

        qDebug()<<musicUrlName<<":"<<musicName<<":"<<musicSinger<<":"<<musicAlbum
               <<":"<<duration;
    }
}

2.音乐分类

根据commonPage知,音乐需要分为我喜欢、本地音乐和历史播放三种,为了区分我们添加枚举类型。

enum PageType
{
    LIKE_PAGE,//我喜欢页面
    LOCAL_PAGE,//本地下载页面
    HISTORY_PAGE//历史播放页面
};

并在Widget中初始化ui中:

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

我们添加到本地的音乐还需要放到MusicList中管理:

void CommonPage::addMusicTOPlayList(MusicList &musiclist, QMediaPlaylist* playlist)
{
    for(auto music:musiclist)
    {
        switch (pageType)
        {
            case LIKE_PAGE:
                if(music.getIsLike())
                {
                    playlist->addMedia(music.getMusicUrl());
                }
                break;
            case LOCAL_PAGE:
                    playlist->addMedia(music.getMusicUrl());
                break;
            case HISTORY_PAGE:
                if(music.getIsHistory())
                {
                    playlist->addMedia(music.getMusicUrl());
                }
                break;
            default:
                break;
        }
    }
}

为了让MusicList支持范围for操作 我们重写一下函数:

iterator MusicList::begin()
{
    return musiclist.begin();
}

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

(1).更新music到CommonPage页面上

void CommonPage::reFresh(MusicList &musiclist)
{
    //解决重复问题 清除listwidget
    ui->pagemusicBox->clear();

    //将歌曲uuid添加到musiclistOFPage
    addMusicTOMusicPage(musiclist);

    for(auto musicUuid : musiclistOFPage)
    {
        auto ret = musiclist.findMusicByUuid(musicUuid);
        if(ret == musiclist.end())
            continue;

        //添加ListitemBox到listWidget中
        ListitemBox* Listitembox = new ListitemBox(this);
        //设置音乐名称 歌手 专辑名称
        Listitembox->setMusicName(ret->getMusicName());
        Listitembox->setMusicSinner(ret->getMusicSinger());
        Listitembox->setMusicAlbum(ret->getMusicAlbum());
        //初始化 设置歌曲的是否为我喜欢
        Listitembox->setMusicLike(ret->getIsLike());

        QListWidgetItem* ListWidgetItem = new QListWidgetItem(ui->pagemusicBox);
        ListWidgetItem->setSizeHint(QSize(ui->pagemusicBox->width(),45));
        ui->pagemusicBox->setItemWidget(ListWidgetItem,Listitembox);

        //接受listitembox发送的信号setIsLike
        connect(Listitembox,&ListitemBox::setIsLike,this,[=](bool isLike){
            emit updataLikeMusic(isLike,ret->getMusicUuid());
        });
    }

    // repaint()会⽴即执⾏paintEvent(),不会等待事件队列的处理
    // update()将⼀个paintEvent事件添加到事件队列中,等待稍后执⾏,即不会⽴即执⾏paintEvent
    repaint();
}
void CommonPage::addMusicTOMusicPage(MusicList& musiclist)
{
    //清除之前的歌曲
    musiclistOFPage.clear();

    for(auto music : musiclist)
    {
        switch(pageType)
        {
            case LIKE_PAGE:
            if(music.getIsLike())
            {
                musiclistOFPage.push_back(music.getMusicUuid());
            }
                break;
            case LOCAL_PAGE:
                musiclistOFPage.push_back(music.getMusicUuid());
                break;
            case HISTORY_PAGE:
            if(music.getIsHistory())
            {
                musiclistOFPage.push_back(music.getMusicUuid());
            }
                break;
        default:
            qDebug()<<"未知歌曲";
            break;
        }
    }
}

(2).收藏音乐

接下来我们处理音乐是否为‘我喜欢’的相关操作:
首先我们需要切换图标变成爱心:

void ListitemBox::setMusicLike(bool islike)
{
    this->isLike = islike;
    if(isLike)
    {
        ui->likeBtu->setIcon(QIcon(":/images/like_2.png"));
    }
    else
    {
        ui->likeBtu->setIcon(QIcon(":/images/like_3.png"));
    }
}

//按钮槽函数
void ListitemBox::onLikeBtuCliked()
{
    isLike = !isLike;
    setMusicLike(isLike);

    emit setIsLike(isLike);
}

每个listitemBox都有可能发出setIsLike的信号,onnect(Listitembox,&ListitemBox::setIsLike,this,[=](bool isLike){ emit updataLikeMusic(isLike,ret->getMusicUuid());将需要更新music的信号传递给Widget,QQMusic收到CommonPage发射的updateLikePage信号后,通知其上的likePage、localPage、
recentPage更新其界⾯的我喜欢歌曲信息。


void Widget::OnupdataLikeMusic(bool isLike, QString musicUuid)
{
    //找到音乐 更新状态信息
    auto ret = musiclist.findMusicByUuid(musicUuid);
    if(ret != musiclist.end())
    {
        ret->setIsLike(isLike);
    }

    //更新页面信息
    ui->likepage->reFresh(musiclist);
    ui->localpage->reFresh(musiclist);
    ui->recentpage->reFresh(musiclist);
}

四.播放控制区域实现

1.播放控制类介绍

(1).QMediaPlayer类

QMediaPlayer 是 Qt 框架中用于处理和播放多媒体内容:
常用属性

  • media:用于设置或获取要播放的媒体资源,可以是本地文件路径、网络 URL 或 QMediaContent 对象。
  • position:表示当前播放位置,单位是毫秒。可通过设置该属性来实现播放进度的跳转。
  • duration:表示媒体文件的总时长,单位是毫秒。
  • volume:表示播放音量,取值范围是 0 到 100,0 表示静音,100表示最大音量。
  • playbackRate:表示播放速率,默认值为 1.0,即正常播放速度。大于 1.0 表示加快播放,小于 1.0表示减慢播放。

常用方法

  • setMedia(const QMediaContent &media):设置要播放的媒体资源。
  • play():开始播放媒体。
  • pause():暂停播放。
  • stop():停止播放。
  • setPosition(qint64 position):设置播放位置。
  • setVolume(int volume):设置播放音量。
  • setPlaybackRate(qreal rate):设置播放速率。

信号与槽

  • stateChanged(QMediaPlayer::State state):当播放状态发生改变时发出该信号,state表示新的播放状态,如QMediaPlayer::PlayingState(播放中)、QMediaPlayer::PausedState(暂停)、QMediaPlayer::StoppedState(停止)。
  • positionChanged(qint64 position):当播放位置发生改变时发出该信号,position 表示当前的播放位置。
  • durationChanged(qint64 duration):当媒体文件的总时长发生改变时发出该信号,duration表示新的总时长。
  • mediaStatusChanged(QMediaPlayer::MediaStatus
    status):当媒体状态发生改变时发出该信号,如
    QMediaPlayer::LoadedMedia(媒体已加载)、QMediaPlayer::BufferingMedia(媒体正在缓冲)等。

(2).QMediaPlayList类

QMediaPlaylist 是 Qt 框架里用于管理多媒体播放列表的类,常与 QMediaPlayer 搭配使用,可实现多媒体文件的顺序播放、随机播放、循环播放等功能。
常用属性

playbackMode:该属性用于设置播放列表的播放模式,常见的播放模式有:
QMediaPlaylist::CurrentItemOnce:当前项播放一次。

QMediaPlaylist::CurrentItemInLoop:当前项循环播放。
QMediaPlaylist::Sequential:顺序播放,播放完列表中的所有项后停止。
QMediaPlaylist::Loop:循环播放,播放完列表中的所有项后从头开始重新播放。
QMediaPlaylist::Random:随机播放,随机选择列表中的项进行播放。

常用方法

  • 添加和移除媒体项 addMedia(const QMediaContent &media):向播放列表中添加一个媒体项。
  • addMedia(const QList &mediaList):向播放列表中添加多个媒体项。
  • insertMedia(int index, const QMediaContent &media):在指定索引位置插入一个媒体项。
  • removeMedia(int position):移除指定索引位置的媒体项。 clear():清空播放列表中的所有媒体项。
  • 获取和设置播放列表信息 mediaCount():返回播放列表中的媒体项数量。
  • currentIndex():返回当前正在播放的媒体项的索引。
  • setCurrentIndex(int index):设置当前要播放的媒体项的索引。
  • media(int index):返回指定索引位置的媒体项。

信号与槽

  • currentIndexChanged(int position):当当前播放的媒体项索引发生改变时发出该信号,position 表示新的索引位置。

  • mediaInserted(int start, int end):当有媒体项插入到播放列表中时发出该信号,start 和 end 表示插入的媒体项的索引范围。

  • mediaRemoved(int start, int end):当有媒体项从播放列表中移除时发出该信号,start 和 end表示移除的媒体项的索引范围。

    mediaChanged(int start, int end):当播放列表中的媒体项发生改变时发出该信号,start 和 end 表示改变的媒体项的索引范围。

2.歌曲播放

首先我们先对播放媒体和媒体播放列表进行初始化:

QMediaPlayer* player;
QMediaPlaylist* playlist;

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

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

    //设置初始播放模式
    playlist->setPlaybackMode(QMediaPlaylist::Loop);


    //播放器添加媒体播放列表
    player->setPlaylist(playlist);

    //设置初始音量
    player->setVolume(20);
}

在播放歌曲之前我们需要将歌曲添加到媒体播放列表中,因为不同CommonPage页面的歌曲不同,所以新增将页面歌曲添加到播放媒体列表的方法:

void CommonPage::addMusicTOPlayList(MusicList &musiclist, QMediaPlaylist* playlist)
{
    for(auto music:musiclist)
    {
        switch (pageType)
        {
            case LIKE_PAGE:
                if(music.getIsLike())
                {
                    playlist->addMedia(music.getMusicUrl());
                }
                break;
            case LOCAL_PAGE:
                    playlist->addMedia(music.getMusicUrl());
                break;
            case HISTORY_PAGE:
                if(music.getIsHistory())
                {
                    playlist->addMedia(music.getMusicUrl());
                }
                break;
            default:
                break;
        }
    }
}

3.播放与暂停

接下来我们对播放控制区域的暂停播放按钮添加槽函数:

void Widget::onPlayCliked()
{
    if(QMediaPlayer::PlayingState == player->state())//正在播放媒体
    {
        player->pause();

    }
    else if(QMediaPlayer::PausedState == player->state())//暂停状态
    {
        player->play();
    }
    else if(QMediaPlayer::StoppedState == player->state())//停止状态
    {
        player->play();
    }
    else//打印错误信息
    {
        qDebug()<<player->errorString();
    }
}

connect(ui->Play,&QPushButton::clicked,this,&Widget::onPlayCliked);

另外再播放暂停开始时,按钮上的图标也会发生变化,这时我们可以拦截stateChanged(QMediaPlayer::State state)信号进行更换图标:

void Widget::onPlayStateChanged()
{
    if(player->state() == QMediaPlayer::PlayingState)
    {
        //播放状态
        ui->Play->setIcon(QIcon(":/images/play_on.png"));
    }
    else
    {
        //暂停状态
        ui->Play->setIcon(QIcon(":/images/play3.png"));
    }
}

connect(player,&QMediaPlayer::stateChanged,this,&Widget::onPlayStateChanged);

4.上一曲和下一曲

接着我们对切换上下一曲歌添加槽函数:

void Widget::onPlayUpCliked()
{
    playlist->previous();
}

void Widget::onPlayDownCliked()
{
    playlist->next();
}
//关联切换上一曲下一曲槽函数
    connect(ui->playUp,&QPushButton::clicked,this,&Widget::onPlayUpCliked);
    connect(ui->playDown,&QPushButton::clicked,this,&Widget::onPlayDownCliked);

5.播放模式切换

我们实现的播放器支持了三种播放模式:随机播放、单曲循环和、列表顺序循环。

void Widget::onPlaybackModeCliked()
{
    //列表循环 随机播放 单曲循环
    if(playlist->playbackMode() == QMediaPlaylist::Loop)
    {
        playlist->setPlaybackMode(QMediaPlaylist::Random);//随机播放
        ui->playMode->setToolTip("随机播放");
    }
    else if(playlist->playbackMode() == QMediaPlaylist::Random)
    {
        playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);//单曲循环
        ui->playMode->setToolTip("单曲循环");
    }
    else if(playlist->playbackMode() == QMediaPlaylist::CurrentItemInLoop)
    {
        playlist->setPlaybackMode(QMediaPlaylist::Loop);//列表循环
        ui->playMode->setToolTip("列表循环");
    }
    else
    {
        qDebug()<<"暂不支持";
    }

}

connect(ui->playMode,&QPushButton::clicked,this,&Widget::onPlaybackModeCliked)

默认为顺序播放。
根据不同播放模式的变换相应的图标也应该更改:

void Widget::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()<<"暂不支持";
    }
}

 connect(playlist,&QMediaPlaylist::playbackModeChanged,this,&Widget::onPlaybackModeChanged);

6.播放全部

在每个CommonPage页面上还有一个播放全部的按钮,但是CommonPage并不能控制音乐的播放,所以点击这个按钮的时候需要向Widget发送信号:

在这里插入图片描述

CommonPage::CommonPage(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::CommonPage)
{
    ui->setupUi(this);
    //取消水平滚动条
    ui->pagemusicBox-

    connect(ui->playAllBtu,&QPushButton::clicked,this,[=](){
        emit playAll(pageType);
    });

}
void Widget::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;
        break;
    default:
        break;
    }
    playAllOFCommonpage(page,0);//从当前页面的第0号音乐开始播放
}

void Widget::playAllOFCommonpage(CommonPage *page, int index)
{
   

    playlist->clear();

    page->addMusicTOPlayList(musiclist,playlist);

    playlist->setCurrentIndex(index);

    player->play();
}

//关联播放全部
    connect(ui->likepage,&CommonPage::playAll,this,&Widget::onPlayAll);
    connect(ui->localpage,&CommonPage::playAll,this,&Widget::onPlayAll);
    connect(ui->recentpage,&CommonPage::playAll,this,&Widget::onPlayAll);

7.双击ListitemBox播放

还需实现一个用户双击ListitemBox也可以播放音乐的功能:

//双击时先widget发送信号
    connect(ui->pagemusicBox,&QListWidget::doubleClicked,this,[=](QModelIndex index){
        emit playMusicByIndex(this,index.row());
    });
void Widget::playMusicByIndex(CommonPage *page, int index)
{
    playAllOFCommonpage(page,index);
}
 //关联双击
    connect(ui->likepage,&CommonPage::playMusicByIndex,this,&Widget::playMusicByIndex);
    connect(ui->localpage,&CommonPage::playMusicByIndex,this,&Widget::playMusicByIndex);
    connect(ui->recentpage,&CommonPage::playMusicByIndex,this,&Widget::playMusicByIndex);

8.最近播放同步

当我们播放了歌曲,此歌曲就应该添加到历史播放页面中:


void Widget::onCurrentIndexChanged(int index)
{
    currindex = index;
    //根据currentIndexChanged信号提供的index找到对应的music
    QString musicid = currpage->getMusicIdIndex(index);

    auto it = musiclist.findMusicByUuid(musicid);

    if(it != musiclist.end())
    {
        it->setIsHistory(true);
    }
    ui->recentpage->reFresh(musiclist);//refresh检测是否History为true进而刷新到页面中
}

QString CommonPage::getMusicIdIndex(int index)
{
    if(index >= musiclistOFPage.size())
    {
        qDebug()<<"暂无此音乐";
        return "";
    }
    return musiclistOFPage[index];
}
//关联当前播放音乐变化
    connect(playlist,&QMediaPlaylist::currentIndexChanged,this,&Widget::onCurrentIndexChanged);

9.音量功能

VolumeTool类中添加两个成员变量,在构造函数中关联静音按钮的槽函数,接着向Widget发送静音信号:

signals:
    void setSilence(bool);
bool isMuted;//是否为静音
int volume;//音量
//关联静音按钮槽函数
     connect(ui->silenceBtn,&QPushButton::clicked,this,&VolumeTool::onSilenceBtnCliked);
void VolumeTool::onSilenceBtnCliked()
{
    isMuted = !isMuted;
    if(isMuted)//true为静音
    {
        ui->silenceBtn->setIcon(QIcon(":/images/silent.png"));
    }
    else
    {
        ui->silenceBtn->setIcon(QIcon(":/images/volumn.png"));
    }

    emit setSilence(isMuted);
}
//关联setSilence信号
    connect(volumetool,&VolumeTool::setSilence,this,&Widget::setMusicSilence);
void Widget::setMusicSilence(bool Muted)
{
    player->setMuted(Muted);
}

还有在音量滑竿上拖动实现音量改变的功能:

bool VolumeTool::eventFilter(QObject *watched, QEvent *event)
{
    if(watched == ui->sliderBox)
    {
        if(event->type() == QEvent::MouseButtonPress)//鼠标按下
        {
            calVolume();
        }
        else if(event->type() == QEvent::MouseButtonRelease)//鼠标松开
        {
            emit setMusicVolume(volume);
        }
        else if(event->type() == QEvent::MouseMove)//鼠标移动
        {
            calVolume();
            emit setMusicVolume(volume);
        }
        return true;
    }
    //不是sliderBox 是别的控件 让系统
    return QObject::eventFilter(watched,event);
}

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

    // 2. ⿏标在volumeBox中可移动的y范围在[25, 205之间]
    height = height<25? 25 : height;
    height = height>205? 205 : height;
    // 3. 调整sliderBt的位置
    ui->sliderBtn->move(ui->sliderBtn->x(),height-ui->sliderBtn->height()/2);
    // 4. 更新outline的位置和⼤⼩
    ui->outSlider->setGeometry(ui->outSlider->x(),height,ui->outSlider->width(),205-height);

    // 5. 计算⾳量⽐率
    volume = (int)ui->outSlider->height()/(float)180*100;
    // 6. 设置给label显⽰出来
    ui->volumeRatio->setText(QString::number(volume)+"%");

}
//关联setMusicVolume信号
    connect(volumetool,&VolumeTool::setMusicVolume,this,&Widget::setPlayVolume);
void Widget::setPlayVolume(int volume)
{
    player->setVolume(volume);
}

10.歌曲播放时间处理

获取到歌曲的总时间并且更新到界面上,在播放歌曲改变时进行,我们在Widget中接受durationChanged信号即可:

//关联DuratinonChanged信号
connect(player,&QMediaPlayer::durationChanged,this,&Widget::onDurationChanged);
    
void Widget::onDurationChanged(qint64 duration)
{
    totalTime = duration;
    ui->totalTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0'))
                           .arg(duration/1000%60,2,10,QChar('0')));
}

以及歌曲当前已经播放时间的处理:QMediaPlayer会发射positionChanged信号

//关联PositionChanged信号
    connect(player,&QMediaPlayer::positionChanged,this,&Widget::onPositionChanged);
void Widget::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->processBar->setStep(position/(float)totalTime);


}

11.进度条设置

(1).进度条界面显示

在 Qt 中,“seek” 功能用于在多媒体播放过程中实现定位播放,也就是可以将播放位置跳转到指定的时间点。



class MusicSlider : public QWidget
{
    Q_OBJECT

public:
    explicit MusicSlider(QWidget *parent = nullptr);
    ~MusicSlider();

    void moveSilder();

    void setStep(float pace);
protected:
    void mouseMoveEvent(QMouseEvent *event);
    void mouseReleaseEvent(QMouseEvent *event);
    void mousePressEvent(QMouseEvent *event);
signals:
    void setMusicSilderPosition(float);

private:
    Ui::MusicSlider *ui;
    int currPos;//当前进度
    int maxWidth;//最大进度
};

void MusicSlider::moveSilder()
{
    ui->outLine->setGeometry(ui->outLine->x(),ui->outLine->y(),currPos,ui->outLine->height());
}



void MusicSlider::mouseMoveEvent(QMouseEvent *event)
{

    currPos = event->pos().x();
    if(currPos<0)
    {
        currPos = 0;
    }
    else if(currPos>maxWidth)
    {
        currPos = maxWidth;
    }
    moveSilder();

}

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

    emit setMusicSilderPosition(currPos/(float)maxWidth);
}

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

(2).进度条同步播放时间

当进度条发生改变后相应的播放时间也该发生变化:

void Widget::onMusicSliderChanged(float value)
{
    //根据进度条比率调整播放时间
    qint64 duration = (qint64)(value*totalTime);
    ui->currentTime->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0'))
                                            .arg(duration/1000%60,2,10,QChar('0')));

    player->setPosition(duration);
}
//关联setMusicSliderPosition信号
    connect(ui->processBar,&MusicSlider::setMusicSilderPosition,this,&Widget::onMusicSliderChanged);

当播放时间改变时同样的进度条也应该同步:

void MusicSlider::setStep(float pace)
{
    currPos = pace*maxWidth;
    moveSilder();
}

void Widget::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->processBar->setStep(position/(float)totalTime);
    if(currindex >= 0)
    {
        lrcpage->showLrcWord(position);
    }

}

12.修改歌曲信息

当歌曲切换时,应该同步修改界面上的歌曲封面歌手专辑等信息:

void Widget::onMetaDataAvailableChanged(bool available)
{
    (int)available;
    //获取当前歌曲id
    QString musicid = currpage->getMusicIdIndex(currindex);
    auto it = musiclist.findMusicByUuid(musicid);
    QString musicname("未知歌曲");
    QString musicsinger("未知歌手");
    if(it != musiclist.end())
    {
        musicname = it->getMusicName();
        musicsinger = it->getMusicSinger();
    }
    ui->musicName->setText(musicname);
    ui->musicSinger->setText(musicsinger);

    //获取封面图
    QVariant coverimage = player->metaData("ThumbnailImage");
    if(coverimage.isValid())
    {
        QImage image = coverimage.value<QImage>();
        ui->musicCover->setPixmap(QPixmap::fromImage(image));
        currpage->setMusicImage(QPixmap::fromImage(image));
    }
    else
    {
        qDebug()<<"暂无封面";
        ui->musicCover->setPixmap(QPixmap(":/images/cyx.png"));
        currpage->setMusicImage(QPixmap(":/images/cyx.png"));

    }
    ui->musicCover->setScaledContents(true); 
    

}

13.lrc歌词设计

(1).lrc歌词页面动画设计

除此之外我们还需要设计一个lrc歌词页面:如下图所示
在这里插入图片描述
ui设计如下
在这里插入图片描述
重要的是给lrc页面添加一个动画效果:
在这里插入图片描述

//创建lrcpage实例
    lrcpage = new LrcPage(this);
    lrcpage->setGeometry(5,5,width(),height());
    lrcpage->hide();
    //设置歌词展示的上移动画效果
    lrcAnimation = new QPropertyAnimation(lrcpage,"geometry",this);
    lrcAnimation->setDuration(400);
    lrcAnimation->setStartValue(QRect(5,5+lrcpage->height(),
                                      lrcpage->width(),lrcpage->height()));
    lrcAnimation->setEndValue(QRect(5,5,
                                    lrcpage->width(),lrcpage->height()));

//关联打开歌词窗口信号
    connect(ui->lrcWord,&QPushButton::clicked,this,&Widget::onLrcWordClicked);
    
    void Widget::onLrcWordClicked()
{
    lrcpage->show();
    lrcAnimation->start();
}

接着设计歌词关闭的动画效果:

在这里插入代码片 //设置关闭动画效果
    lrcAnimation = new QPropertyAnimation(this,"geometry",this);
    lrcAnimation->setDuration(400);
    lrcAnimation->setStartValue(QRect(5,5,
                                      width(),height()));
    lrcAnimation->setEndValue(QRect(5,5+height(),
                                    width(),height()));

    connect(ui->hideBtn,&QPushButton::clicked,this,[=](){lrcAnimation->start();});
    connect(lrcAnimation,&QPropertyAnimation::finished,this,[=](){
        hide();
    });
    showLrcWord(-1);

(2).歌词解析

为了解析歌词我们定义一个结构体用来描述:

struct LrcLine{
    qint64 _time;//歌词所在时间
    QString _text;//歌词内容

    LrcLine(qint64 time,QString text)
        :_time(time)
        ,_text(text)
    {}
};

首先我们把歌曲文件后缀替换为.lrc:

QString Music::getLrcFilePath()//知道歌词文件
{
    QString temp = musicUrl.toLocalFile();
    //替换后缀为.lrc
    temp.replace(".mp3",".lrc");
    temp.replace(".flac",".lrc");
    temp.replace(".mpga",".lrc");
    return temp;
}

接着分析.lrc文件填充结构体:

bool LrcPage::parseLrc(QString lrcpath)
{
    QFile file(lrcpath);
    if(!file.open(QIODevice::ReadOnly))
    {
        qDebug()<<"文件打开失败";
        return false;
    }
    //将上一首歌词清除
    lrcLines.clear();
    //解析歌词
    while(!file.atEnd())
    {
        QString lrclineword = file.readLine(1024);
        // [00:17.94]那些失眠的⼈啊 你们还好吗
        // [0:58.600.00]你像⼀只⻜来⻜去的蝴蝶
        int start = 0,end = 0;
        end = lrclineword.indexOf(']');
        QString lrctime = lrclineword.mid(start,end-start+1);
        QString lrcword = lrclineword.mid(end+1,lrclineword.size()-end-1-1);
        //解析分钟
        qint64 linetime = 0;
        start = 1;
        end = lrctime.indexOf(':');
        linetime += lrclineword.mid(start,end-start).toInt()*60*1000;
        //解析秒
        start = end+1;
        end = lrctime.indexOf('.',start);
        linetime += lrclineword.mid(start,end-start).toInt()*1000;
        //解析毫秒
        start = end+1;
        end = lrctime.indexOf('.',start);
        linetime += lrclineword.mid(start,end-start).toInt();

        lrcLines.push_back(LrcLine(linetime,lrcword));
    }
    for(auto e:lrcLines)
    {
        qDebug()<<e._time<<" "<<e._text;
    }
    return true;
}

接下来我们根据播放时间同步拿到歌词并显示:

int LrcPage::getLineLrcWordIndex(qint64 pos)
{
    //没有歌词返回负一
    if(lrcLines.empty())
    {
        return -1;
    }
    if(pos <= lrcLines[0]._time)
    {
        return 0;
    }
    for(int i =1;i<lrcLines.size();i++)
    {
        if(pos>=lrcLines[i-1]._time && pos<lrcLines[i]._time)
        {
            return i-1;
        }
    }
    //没有找到 返回歌词最后一行
    return lrcLines.size()-1;
}
void LrcPage::showLrcWord(int time)
{
    int index = getLineLrcWordIndex(time);
    if(index == -1)
    {
        ui->line1->setText("");
        ui->line2->setText("");
        ui->line3->setText("");
        ui->lineCenter->setText("当前歌曲无歌词");
        ui->line5->setText("");
        ui->line6->setText("");
        ui->line7->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->line5->setText(getLrcWordByIndex(index+1));
        ui->line6->setText(getLrcWordByIndex(index+2));
        ui->line7->setText(getLrcWordByIndex(index+3));
    }
}

QString LrcPage::getLrcWordByIndex(qint64 index)
{
    if(index<0 || index>=lrcLines.size())
    {
        return "";
    }
    return lrcLines[index]._text;
}

接着在歌曲发生切换和播放时间改变时调用函数:

void Widget::onMetaDataAvailableChanged(bool available)
{
    //.........
    //解析歌词
    if(it != musiclist.end())
    {
        QString lrcpath = it->getLrcFilePath();
        qDebug()<<"调用parseLrc";
        lrcpage->parseLrc(lrcpath);
        lrcpage->setMusicNameAndSinger(musicname,musicsinger);
    }

}



void Widget::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->processBar->setStep(position/(float)totalTime);
    if(currindex >= 0)
    {
        lrcpage->showLrcWord(position);
    }

}

五.数据库实现持久化

1.SQLite介绍

SQLite 是⼀款轻量级、⽆需安装的桌⾯型数据库,

  • 开源免费:SQLite 遵循公共领域许可,这意味着你可以自由地使用、修改和分发它,无需支付任何费用,降低了开发成本。
  • 嵌入式特性:SQLite是嵌入式数据库,它没有独立的服务器进程,数据库以文件形式存储在磁盘上,应用程序可以直接访问该文件进行数据的读写操作,适合嵌入式设备和移动应用。
  • 轻量级:SQLite 的代码库体积小巧,占用资源少,无需复杂的配置和管理,对于资源有限的系统(如物联网设备)非常友好。

具体的语法语句操作 我们后续在使用中学习。

2.QSqlDatabase类介绍

QSqlDatabase 是 Qt 框架中用于管理数据库连接的核心类,它属于 Qt SQL 模块,QSqlDatabase 类代表一个数据库连接,通过它可以创建、打开、关闭和管理数据库连接。在使用 QSqlDatabase 之前,需要确保已经正确配置了 Qt SQL 模块:在.pro文件下:
在这里插入图片描述

3.数据库初始化

(1).initSQLite

void Widget::initSQLite()
{
    sqlite = QSqlDatabase::addDatabase("QSQLITE");

    sqlite.setDatabaseName("music.db");

    if(!sqlite.open())
    {
        QMessageBox::critical(this,"打开music.db失败",sqlite.lastError().text());
        return ;
    }

    qDebug()<<"数据库music.db创建成功";

    QString sql("CREATE TABLE IF NOT EXISTS musicInfo(\
                id INTEGER PRIMARY KEY AUTOINCREMENT,\
                musicUuid VARCHAR(200) UNIQUE,\
                musicName VARCHAR(50) UNIQUE,\
                musicSinger VARCHAR(50),\
                albumName VARCHAR(50),\
                duration BIGINT,\
                musicUrl VARCHAR(256),\
                isLike INTEGER,\
                isHistory INTEGER)");

    QSqlQuery sqlquery;
    if(!sqlquery.exec(sql))
    {
        QMessageBox::critical(this,"创建表musicInfo失败",sqlquery.lastError().text());
        return;
    }
    qDebug()<<"创建表musicInfo成功";

}

(2).歌曲信息写入数据库

当程序退出时我们应该将歌曲信息写进数据库进而持久化:

void MusicList::writeMusicTODB()
{
    for(auto music : musiclist)
    {
        music.insertMusicTODB();
    }
}


void Music::insertMusicTODB()
{
    //检测当前music是否在表musicInfo中
    //存在 更新isLike和isHisotry属性
    //不存在将music信息插入表中
    QSqlQuery sqlquery;
    sqlquery.prepare("SELECT EXISTS (SELECT 1 FROM musicInfo where musicUuid = ?)");
    sqlquery.addBindValue(musicUuid);
    if(!sqlquery.exec())
    {
        qDebug()<<"查询当前music是否存在失败"<<sqlquery.lastError().text();
        return ;
    }
    qDebug()<<"查询成功";


    if(sqlquery.next())
    {
        bool isExist;//是否存在
        isExist = sqlquery.value(0).toBool();

        if(isExist)
        {
            sqlquery.prepare("UPDATE musicInfo SET isLike = ?, isHistory = ? WHERE musicUuid =?");
            sqlquery.addBindValue(isLike ? 1:0);
            sqlquery.addBindValue(isHistory ? 1:0);
            sqlquery.addBindValue(musicUuid);

            if(!sqlquery.exec())
            {
                qDebug()<<"更新歌曲isLike.isHistory失败"<<sqlquery.lastError().text();
                return ;
            }
            qDebug()<<"更新成功";
        }
        else
        {
            sqlquery.prepare("INSERT INTO musicInfo(musicUuid,musicName,musicSinger,\
                                albumName,duration,musicUrl,isLike,isHistory) \
                                VALUES(?,?,?,?,?,?,?,?)");

            sqlquery.addBindValue(musicUuid);
            sqlquery.addBindValue(musicName);
            sqlquery.addBindValue(musicSinger);
            sqlquery.addBindValue(musicAlbum);
            sqlquery.addBindValue(duration);
            sqlquery.addBindValue(musicUrl.toLocalFile());
            sqlquery.addBindValue(isLike ? 1:0);
            sqlquery.addBindValue(isHistory ? 1:0);

            if(!sqlquery.exec())
            {
                qDebug()<<"插入歌曲信息失败"<<sqlquery.lastError().text();
                return ;
            }
            qDebug()<<"插入歌曲信息成功";
        }
    }

}

(3).程序启动读取数据库歌曲信息

当程序启动后就从数据库中读取已经保存的歌曲信息:

void MusicList::readMusicBYDB()
{
    QSqlQuery sqlquery;
    sqlquery.prepare("SELECT musicUuid,musicName,musicSinger,albumName,duration,\
                    musicUrl,isLike,isHistory FROM musicInfo");
    if(!sqlquery.exec())
    {
        qDebug()<<"查询失败"<<sqlquery.lastError().text();
        return ;
    }
    qDebug()<<"查询成功";

    while(sqlquery.next())
    {
        Music music;
        music.setMusicUuid(sqlquery.value(0).toString());
        music.setMusicName(sqlquery.value(1).toString());
        music.setMusicSinger(sqlquery.value(2).toString());
        music.setMusicAlbum(sqlquery.value(3).toString());
        music.setMusicDuration(sqlquery.value(4).toLongLong());
        music.setMusicUrl("file:///"+sqlquery.value(5).toString());
        music.setIsLike(sqlquery.value(6).toBool());
        music.setIsHistory(sqlquery.value(7).toBool());

        musiclist.push_back(music);

    }
}

六.优化与debug

1.添加系统托盘

当我们点击程序的关闭按钮时,发现程序直接退出了,可有时候我们只是想让程序不显示在任务栏而退居与系统托盘中,所以我们要添加:

//添加系统托盘
    QSystemTrayIcon* trayIcon = new QSystemTrayIcon(this);
    trayIcon->setIcon(QIcon(":/images/tubiao.png"));

    //创建托盘菜单
    QMenu* trayMenu = new QMenu(this);
    trayMenu->addAction("显示",this,&QWidget::showNormal);
    trayMenu->addAction("退出",this,&Widget::quitMusic);

    trayIcon->setContextMenu(trayMenu);
    trayIcon->show();
void Widget::quitMusic()
{
    //保存歌曲信息
    musiclist.writeMusicTODB();
    //关闭数据库
    sqlite.close();
    //关闭窗口
    close();
}

void Widget::on_exit_clicked()
{
    hide();
}

这样当我们再点击程序的关闭按钮后,它会隐藏到系统托盘中:
在这里插入图片描述

2.换肤最大最小化处理

换肤无非是自定义的改变下背景颜色和图片之类的效果,可以作为项目的拓展来完成,这里暂且这样处理:

void Widget::on_min_clicked()
{
    showMinimized();
}

void Widget::on_skin_clicked()
{
    QMessageBox::information(this, "提示", "换肤功能正在紧急支持中...");
}

3.重复从本地添加音乐bug

当我们打开程序读取数据库中的歌曲后,我们再次从本地添加同样的音乐就会出现重复的情况,这里我们在MusicList添加音乐时做去重处理即可解决:


void MusicList::addMusic(const QList<QUrl> &urls)
{
    for(auto e: urls)
    {
        //检测歌曲的MIME类型 过滤有效歌曲文件
        QMimeDatabase file;
        QMimeType type = file.mimeTypeForFile(e.toLocalFile());
        QString ret = type.name();

        if(ret == "audio/mpeg" || ret == "audio/flac"||ret == "audio/wav")
        {
            //构建music对象 并且添加进musiclist管理
            const Music music(e);
            //处理读取数据库后再次从本地添加音乐在页面上重复问题(数据库不重复UNIQUEmusicName)
            if(-1 != musiclist.indexOf(music))
            {
                qDebug()<<"已有音乐 请勿重复添加"<<music.getMusicName();
            }
            musiclist.push_back(music);
        }

    }
}

4.保证程序只运行⼀次

  • 资源管理与优化
  • 数据一致性与完整性
  • 避免功能异常和混乱

因为以上的原因我们通过共享内存来保证只有一个程序实例:


#include <QApplication>
#include <QMessageBox>
#include <QSharedMemory>//共享内存

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    //Music Player
    QSharedMemory sharedMemory("musicPlayer");
    //如果已经被占用说明有实例在运行
    if(sharedMemory.attach())
    {
        QMessageBox::information(nullptr,"musicPlayer","musicPlayer已经在运行");
        return 0;
    }
    sharedMemory.create(1);
    Widget w;
    w.show();
    return a.exec();
}

七.总结

项目源码与项目打包压缩包都在:码云匿名者


http://www.kler.cn/a/591562.html

相关文章:

  • 马蜂窝携手腾讯云接入DeepSeek,率先应用于旅游AI智能应用“AI游贵州”
  • Ubuntu “文件系统根目录”上的磁盘空间不足
  • 【操作系统安全】任务4:Windows 系统网络安全实践里常用 DOS 命令
  • 河南大学移动应用开发实验报告1
  • Spring Boot Starter 启动器:简化依赖管理,快速构建应用
  • 自发自用省电费,余电上网稳收益!安科瑞分布式光伏监测系统智领绿色能源未来
  • 十七、实战开发 uni-app x 项目(仿京东)- 后端指南
  • 游戏服务器分区的分布式部署
  • Go基础语法阶段核心内容(5天)
  • 路由器安全研究:D-Link DIR-823G v1.02 B05 复现与利用思路
  • 使用 AJAX 前后端传递数据
  • 《Python实战进阶》No25: 自动化测试:unittest 与 pytest 的对比
  • Vue3项目中可以尝试封装那些组件
  • 删除 Git 历史提交记录中的大文件
  • 【css酷炫效果】实现鱼群游动动态效果
  • Docker和 Docker Compose安装MySQL:快速搭建数据库环境
  • 【STM32】从新建一个工程开始:STM32 新建工程的详细步骤
  • vue:组件的使用
  • Asp.net Core API 本地化
  • 淘宝/天猫获得淘宝商品评论 API 返回值说明