先叠几层甲:

该项目中很多功能和函数的实现方法肯定有更简单更优化的,本文章里的方法属于比较笨的那种,仅供新手初学者以及博主自己参考,文章中提供的代码及源文件难免会有疏忽、bug,仅供参考、学习、交流;除了正常的讨论问题、建议外,还请各位大佬键盘之下给我留点面子……

音乐播放器算是很经典的练手项目了,能够帮助初学者小白(像我这样的)快速的入门上手QT的一些操作和特性,本文将从头到尾讲述如何写出这样一个音乐播放器:

主界面:专辑封面、播放列表、控制区域
歌词显示并随时间滚动

界面设计:

首先打开界面文件,将需要用到的组件拖进去并排布整齐:

歌曲封面和界面上的一些文字(播放列表、正在播放、时间什么的)使用label组件,播放列表和使用listwidget,按钮使用pushbutton,进度条音量条就用slider,同样的,菜单栏里添加一个“文件”按钮用于导入歌曲。

编辑样式表,设置按钮的属性“border”为none,即不显示按钮的轮廓,方便待会更换图标。
这里的background-color直接点击添加颜色就可以了,hover的属性是当鼠标悬浮在按钮上,综合下来的效果就是鼠标经过按钮时会有个粉底。

接下来导入图标,需要新建资源文件:

然后将图片图标什么的全导入到这个文件里就好了,记得提前把图标什么的整理成一个文件夹放到项目目录里:

给按钮设置图标的方式很简单,鼠标右键按钮点编辑样式表,输入图标的url即可,这里的url可以直接从导入好的资源文件那里复制。


不过要注意的是,通过这种方式更换的图标后续在程序里是没办法再更改的(至少我试了是不行)。所以这种方法只适合更换只有单一状态的按钮图标,比如播放列表、上一首下一首的图标等。

所以如果要更换有多种状态的按钮的图标,这里建议直接在mainwindow的构造函数里更换:

ui->volButton->setIcon(QIcon(":/icon/volume-high-solid.png"));//设置音量键、播放键、播放模式的图标
ui->playButton->setIcon(QIcon(":/icon/play-solid.png"));
ui->modeButton->setIcon(QIcon(":/icon/repeat-solid.png"));

当然,在构造函数里我们也得让音量条和歌词界面默认是隐藏的,还要让播放列表提示用户导入歌曲:

ui->volSlider->hide();//默认隐藏音量条
ui->listWidget_lrc->hide();//默认不显示歌词界面
ui->listWidget->addItem("请先导入音乐……");//提示用户先导入音乐

运行一下看看效果:

那么播放器的界面设计到这里就算完成了,接下来就是用代码实现这些控件具体的功能了。

程序设计:

导入歌曲文件:

右击添加好的动作转到槽,信号选中triggered(),编写槽函数。

那么问题来了,我们要如何获取音乐所存在的路径呢?幸运的是,QT提供了相应的接口,我们可以使用QFileDialog::getExistingDirectory唤出打开文件的窗口并获取打开的路径,记得包含头文件QFileDialog和QDir。
接下来就好办了,直接放出代码:
像nomusic、isplaying、volnone、playmode、nolrc等都是事先定义好用于标识状态的全局变量,下文中还会再用到,之后就不再赘述。

void MainWindow::on_actionopen_triggered()
{
    playlist.clear();//导入歌曲前先清空播放列表
    QString path=QFileDialog::getExistingDirectory(this,"选择音乐所在路径","C:\\Users\\YUAN1\\Desktop\\qt_player");
    QDir musicdir(path);
    auto filelist=musicdir.entryList(QStringList()<<"*.mp3"<<"*.wav");//获取可以播放的音频文件名称
    for(auto file : filelist)
    {
        playlist.append(QUrl::fromLocalFile(path+"/"+file));//将文件名处理成路径添加到播放列表里
    }
    ui->listWidget->clear();//先清空播放列表
    ui->listWidget->addItems(filelist);//将音乐名称添加到界面上的播放列表里
    ui->listWidget->setCurrentRow(0);//默认选择第一首(第0行)
    if(ui->listWidget->count()==0)//如果没导入任何文件(之前打开的目录里没有一首歌)
    {
        ui->listWidget->addItem("没有找到音乐……");
        nomusic=1;//当前是没有音乐可播放的状态
    }
    else
    {
        nomusic=0;//当前是有音乐可以播放的状态
        int index=ui->listWidget->currentRow();//获取当前选中的行号
        media_player->setSource(playlist[index]);//设置播放器的播放源,来自播放列表里的第index文件
        QString imgurl=playlist[index].toString().remove("file:///");
        imgurl.replace(".mp3",".jpg");//获取与音乐文件同名的专辑封面图片文件并显示在lable中
        ui->img_label->setPixmap(QPixmap(imgurl));
        for(int i=0;ilistWidget->count();i++)//设置播放列表文字居中显示
        {
            ui->listWidget->item(i)->setTextAlignment(Qt::AlignCenter);
        }
    }
}

这样我们就实现了导入歌曲到播放列表中,并显示第一首的专辑封面。
这里可能会有人问,代码中的“playlist”和“media_player”是什么?
playlist是用于存放获取到的歌曲文件的名称的列表,要事先在mainwindow.h中定义以供全局使用。
media_player是QT自带的multimedia组件里的东西,我们要实现打开pro文件导入这个组件才能够使用它:

还需要包含头文件QMediaPlayer和QAudioOutput,他们一个负责媒体播放,一个负责音频设备。
然后在mainwindow.h中定义他们的指针,并在构造函数中创建对象,这样就可以全局使用了。

//mainwindow.h
class MainWindow : public QMainWindow
{
……
……
public:
……
private slots:
……
private:
    ……
    QList<QUrl> playlist;
    QAudioOutput *audio_output;
    QMediaPlayer *media_player;
    ……
};

//mainwindow.cpp
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ……
    ……
    audio_output=new QAudioOutput(this);
    media_player=new QMediaPlayer(this);
    media_player->setAudioOutput(audio_output);//设置media_player的输出由audio_output接管
}

那么接下来就要实现播放器的核心功能,播放音乐了!

播放音乐:

右击播放按钮,转到槽,信号选中clicked(),编写槽函数。

void MainWindow::on_playButton_clicked()
{
    if(nomusic==1)return;//如果当前没有音乐可以播放就直接返回,防止因为接下来的操作越界
    if(isplaying==0)//如果当前没在播放
    {
        isplaying=1;//在播放了哦
        ui->title_lable->setText(ui->listWidget->item(ui->listWidget->currentRow())->text().remove(".mp3"));//显示当前播放的音乐
        ui->playButton->setIcon(QIcon(":/icon/pause-solid.png"));//将播放图标变更为暂停图标
        media_player->play();//播放音乐
        int index=ui->listWidget->currentRow();//获取当前listwidget选中的行号(与播放列表里的编号是对应的)

        ui->listWidget_lrc->clear();//清空已有的歌词
        QString lrcurl=playlist[index].toString().remove("file:///");//获取和音乐文件同名的lrc歌词文件的路径
        lrcurl.replace(".mp3",".lrc");
        QFile lrcfile(lrcurl);
        bool isopen=lrcfile.open(QIODevice::ReadOnly);//如果打开失败(说明没有找到歌词文件)
        if(!isopen)
        {
            ui->listWidget_lrc->addItem("无歌词");//直接在歌词界面显示无歌词
            nolrc=1;//设置当前是没有歌词的状态
            ui->listWidget_lrc->item(0)->setTextAlignment(Qt::AlignCenter);//设置居中显示
            lrcfile.close();//有始有终
        }
        else
        {
            int line=0;//设置行号用于在下面的循环中向QMap中插入value
            nolrc=0;//设置当前是有歌词的状态
            lrcrow.clear();//先清空QMap中的时间数据
            while(1)
            {
                char buf[1024];//定义一个缓冲区用于存放readline的数据
                int flag=lrcfile.readLine(buf,1024);
                if(flag==0||flag==-1)break;//如果读不到数据了就直接跳出循环
                QString buf_str=buf;//将buf转为QString
                QString buf_time=buf_str;//将buf复制一份,接下来一份处理成纯歌词,一份处理成时间数据
                buf_str.remove(0,11);
                buf_time.truncate(11);
                buf_time.remove("[");buf_time.remove("]");
                buf_time.replace(":",".");
                QStringList time_list=buf_time.split(".");
                qint64 min=time_list[0].toInt();
                qint64 sec=time_list[1].toInt();
                qint64 lsec=time_list[2].toInt();
                qint64 posc=(min*60+sec)*1000+lsec*10;//将时间转换为毫秒
                lrcrow.insert(posc,line);//将当前歌词的时间数据和对应的行号插入到QMap中
                line++;//下一行
                ui->listWidget_lrc->addItem(buf_str);//把歌词显示在widget上
            }
            for(int i=0;ilistWidget_lrc->count();i++)//歌词居中显示
            {
                ui->listWidget_lrc->item(i)->setTextAlignment(Qt::AlignCenter);
            }
            lrcfile.close();//有始有终
        }

    }
    else
    {
        isplaying=0;//暂停了哦
        ui->playButton->setIcon(QIcon(":/icon/play-solid.png"));//将暂停图标变更为播放图标
        media_player->pause();//暂停播放
    }
}

关于歌词的处理,我的思路是使用readline一行一行的读取歌词,将歌词开头的时间码[00:00.000]和歌词分开处理保存,使用QMap(lrcrow)保存歌词的行号和对应的时间数据,歌词就直接显示在listwidget上,当然,要事先在mainwindow.h里定义好:

//mainwindow.h
class MainWindow : public QMainWindow
{
……
……
public:
……
private slots:
……
private:
    ……
    QList<QUrl> playlist;
    QAudioOutput *audio_output;
    QMediaPlayer *media_player;
    ……
    QMap<qint64,int> lrcrow;
};

播放模式:

播放模式懒得写了- -,就只写个切换图标啥的吧,如果要实现也是在这个函数里扩写每种状态下的操作。右击播放模式按钮,转到槽,信号选中clicked(),编写槽函数。

void MainWindow::on_modeButton_clicked()
{
    if(playmode>3)//根据播放模式变更图标
    {
        playmode=1;
    }
    if(playmode==1)
    {
        ui->modeButton->setIcon(QIcon(":/icon/repeat-solid.png"));//列表循环
    }
    else if(playmode==2)
    {
        ui->modeButton->setIcon(QIcon(":/icon/shuffle-solid.png"));//随机播放
        ui->listWidget->setCurrentRow(0);
    }
    else if(playmode==3)
    {
        ui->modeButton->setIcon(QIcon(":/icon/repeat-solid _1.png"));//单曲循环
    }
    playmode++;

}

上一首、下一首:

右击按钮转到槽什么的不想再提了,再提就不礼貌了- -

切歌的过程就是:暂停播放→获取listwidget选中的行号→行号±1→根据新的行号设置播放来源→播放音乐。

void MainWindow::on_prevButton_clicked()
{
    if(nomusic==1)return;//如果没有音乐可以播放就直接返回不进行任何操作
    if(ui->listWidget->count()==1)return;//只有一首歌你切个p的歌
    media_player->pause();//先暂停
    int index=ui->listWidget->currentRow();//获取当前的行号
    if(index==0)index=ui->listWidget->count()-1;//如果已经是第一首歌了再前一首就是最后一首歌
    else index--;//不然就直接index-1就好了
    media_player->setSource(playlist[index]);//重新设置播放源
    ui->listWidget->setCurrentRow(index);//让listwidget选中上一首
    QString imgurl=playlist[index].toString().remove("file:///");//处理专辑封面
    imgurl.replace(".mp3",".jpg");
    ui->img_label->setPixmap(QPixmap(imgurl));
    ui->title_lable->setText(ui->listWidget->item(ui->listWidget->currentRow())->text().remove(".mp3"));
    ui->listWidget_lrc->clear();
    QString lrcurl=playlist[index].toString().remove("file:///");
    lrcurl.replace(".mp3",".lrc");
    QFile lrcfile(lrcurl);
    bool isopen=lrcfile.open(QIODevice::ReadOnly);//处理歌词
    if(!isopen)
    {
        ui->listWidget_lrc->addItem("无歌词");
        nolrc=1;
        ui->listWidget_lrc->item(0)->setTextAlignment(Qt::AlignCenter);
        lrcfile.close();
    }
    else
    {
        int line=0;
        nolrc=0;
        lrcrow.clear();
        while(1)
        {
            char buf[1024];
            int flag=lrcfile.readLine(buf,1024);
            if(flag==0||flag==-1)break;
            QString buf_str=buf;
            QString buf_time=buf_str;
            buf_str.remove(0,11);
            buf_time.truncate(11);
            buf_time.remove("[");buf_time.remove("]");
            buf_time.replace(":",".");
            QStringList time_list=buf_time.split(".");
            qint64 min=time_list[0].toInt();
            qint64 sec=time_list[1].toInt();
            qint64 lsec=time_list[2].toInt();
            qint64 posc=(min*60+sec)*1000+lsec*10;
            lrcrow.insert(posc,line);
            line++;
            ui->listWidget_lrc->addItem(buf_str);
        }
        for(int i=0;ilistWidget_lrc->count();i++)
        {
            ui->listWidget_lrc->item(i)->setTextAlignment(Qt::AlignCenter);
        }
        lrcfile.close();
    }
    if(isplaying==1)media_player->play();//如果是在播放音乐的时候切的歌就让他切完之后自动播放
}
void MainWindow::on_nextButton_clicked()
{
    if(nomusic==1)return;
    if(ui->listWidget->count()==1)return;
    media_player->pause();
    int index=ui->listWidget->currentRow();
    if(index==(ui->listWidget->count()-1))index=0;//如果已经是最后一首了再下一首就是第一首
    else index++;//不然就直接index+1
    media_player->setSource(playlist[index]);
    ui->listWidget->setCurrentRow(index);
    QString imgurl=playlist[index].toString().remove("file:///");//处理专辑封面
    imgurl.replace(".mp3",".jpg");
    qInfo()<img_label->setPixmap(QPixmap(imgurl));
    ui->title_lable->setText(ui->listWidget->item(ui->listWidget->currentRow())->text().remove(".mp3"));
    ui->listWidget_lrc->clear();
    QString lrcurl=playlist[index].toString().remove("file:///");
    lrcurl.replace(".mp3",".lrc");
    QFile lrcfile(lrcurl);
    bool isopen=lrcfile.open(QIODevice::ReadOnly);//处理歌词
    if(!isopen)
    {
        ui->listWidget_lrc->addItem("无歌词");
        nolrc=1;
        ui->listWidget_lrc->item(0)->setTextAlignment(Qt::AlignCenter);
        lrcfile.close();
    }
    else
    {
        int line=0;
        nolrc=0;
        lrcrow.clear();
        while(1)
        {
            char buf[1024];
            int flag=lrcfile.readLine(buf,1024);
            if(flag==0||flag==-1)break;
            QString buf_str=buf;
            QString buf_time=buf_str;
            buf_str.remove(0,11);
            buf_time.truncate(11);
            buf_time.remove("[");buf_time.remove("]");
            buf_time.replace(":",".");
            QStringList time_list=buf_time.split(".");
            qint64 min=time_list[0].toInt();
            qint64 sec=time_list[1].toInt();
            qint64 lsec=time_list[2].toInt();
            qint64 posc=(min*60+sec)*1000+lsec*10;
            lrcrow.insert(posc,line);
            line++;
            ui->listWidget_lrc->addItem(buf_str);
        }
        for(int i=0;ilistWidget_lrc->count();i++)
        {
            ui->listWidget_lrc->item(i)->setTextAlignment(Qt::AlignCenter);
        }
        lrcfile.close();
    }
    if(isplaying==1)media_player->play();
}

播放进度显示、使用进度条控制播放进度:

这里我们使用qt的connect()来捕获QMediaPlayer的信号。要实现播放进度的显示,我们需要获取到歌曲的总时长、歌曲当前的播放进度。因为捕获到的时间数据是毫秒的格式,所以要先处理成00:00格式再显示到lable上。

connect(media_player,&QMediaPlayer::durationChanged,this,[=](qint64 duration)
    {
                //捕获歌曲总时长(毫秒)并将时长转化为00:00的格式显示在label上
                ui->totallable->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0')).arg(duration/1000%60));
                ui->timeSlider->setRange(0,duration);//设置进度条的范围为0-歌曲总时长
            });
    //获取歌曲的当前播放进度
connect(media_player,&QMediaPlayer::positionChanged,this,[=](qint64 pos)
    {
                //捕获到当前播放时长(毫秒)变化时将当前时间显示到lable
                ui->timelable->setText(QString("%1:%2").arg(pos/1000/60,2,10,QChar('0')).arg(pos/1000%60,2,10,QChar('0')));
                ui->timeSlider->setValue(pos);//根据当前播放时长设置进度条的进度
            });

既然根据播放时长设置进度条的进度的功能写完了,不妨顺便把歌词滚动做了:

connect(media_player,&QMediaPlayer::durationChanged,this,[=](qint64 duration)
    {
                ui->totallable->setText(QString("%1:%2").arg(duration/1000/60,2,10,QChar('0')).arg(duration/1000%60));
                ui->timeSlider->setRange(0,duration);
            });
connect(media_player,&QMediaPlayer::positionChanged,this,[=](qint64 pos)
    {
                ui->timelable->setText(QString("%1:%2").arg(pos/1000/60,2,10,QChar('0')).arg(pos/1000%60,2,10,QChar('0')));
                ui->timeSlider->setValue(pos);
                if(nolrc!=1)//如果有歌词可以显示则进行歌词进度的处理
                {
                    QMap<qint64,int>::iterator iter=lrcrow.begin();
                    while(iter!=lrcrow.end())
                    {
                        if(iter.key()>=pos-100 && iter.key()<=pos+100)
                        {
                            ui->listWidget_lrc->setCurrentRow(iter.value());//如果和时间对上了就让listwidget_lrc选中对应时间的歌词
                            break;
                        }
                        iter++;
                    }
                }
            });

拖动进度条变更播放进度直接用槽函数就好了:

void MainWindow::on_timeSlider_sliderMoved(int position)
{
    if(nomusic==1)return;
    if(isplaying==0)
    {
        isplaying=1;//在播放了哦
        ui->title_lable->setText(ui->listWidget->item(ui->listWidget->currentRow())->text().remove(".mp3"));//显示当前播放的音乐
        ui->playButton->setIcon(QIcon(":/icon/pause-solid.png"));//将播放图标变更为暂停图标
        media_player->play();//播放音乐
        int index=ui->listWidget->currentRow();//获取当前listwidget选中的行号(与播放列表里的编号是对应的)

        ui->listWidget_lrc->clear();//清空已有的歌词
        QString lrcurl=playlist[index].toString().remove("file:///");//获取和音乐文件同名的lrc歌词文件的路径
        lrcurl.replace(".mp3",".lrc");
        QFile lrcfile(lrcurl);
        bool isopen=lrcfile.open(QIODevice::ReadOnly);//如果打开失败(说明没有找到歌词文件)
        if(!isopen)
        {
            ui->listWidget_lrc->addItem("无歌词");//直接在歌词界面显示无歌词
            nolrc=1;//设置当前是没有歌词的状态
            ui->listWidget_lrc->item(0)->setTextAlignment(Qt::AlignCenter);//设置居中显示
            lrcfile.close();//有始有终
        }
        else
        {
            int line=0;//设置行号用于在下面的循环中向QMap中插入value
            nolrc=0;//设置当前是有歌词的状态
            lrcrow.clear();//先清空QMap中的时间数据
            while(1)
            {
                char buf[1024];//定义一个缓冲区用于存放readline的数据
                int flag=lrcfile.readLine(buf,1024);
                if(flag==0||flag==-1)break;//如果读不到数据了就直接跳出循环
                QString buf_str=buf;//将buf转为QString
                QString buf_time=buf_str;//将buf复制一份,接下来一份处理成纯歌词,一份处理成时间数据
                buf_str.remove(0,11);
                buf_time.truncate(11);
                buf_time.remove("[");buf_time.remove("]");
                buf_time.replace(":",".");
                QStringList time_list=buf_time.split(".");
                qint64 min=time_list[0].toInt();
                qint64 sec=time_list[1].toInt();
                qint64 lsec=time_list[2].toInt();
                qint64 posc=(min*60+sec)*1000+lsec*10;//将时间转换为毫秒
                lrcrow.insert(posc,line);//将当前歌词的时间数据和对应的行号插入到QMap中
                line++;//下一行
                ui->listWidget_lrc->addItem(buf_str);//把歌词显示在widget上
            }
            for(int i=0;i<ui->listWidget_lrc->count();i++)//歌词居中显示
            {
                ui->listWidget_lrc->item(i)->setTextAlignment(Qt::AlignCenter);
            }
            lrcfile.close();//有始有终
        }
    }
    media_player->setPosition(position);//根据拖动的进度条设置当前播放进度
}

音量控制:

直接用槽函数,因为setVolume的参数是float类型的0~1之间的数值,slider的数值范围是0~100,因此需要转换一下。

void MainWindow::on_volSlider_valueChanged(int value)
{
    float vol=(float)value/(float)100;//处理音量条的数据与setVolume的参数对应
    audio_output->setVolume(vol);
    if(value==0)//当音量为0时将音量图标更改为静音图标
    {
        volnone=1;
        ui->volButton->setIcon(QIcon(":/icon/volume-xmark-solid.png"));
    }
    else
    {
        if(volnone==1)
        {
            ui->volButton->setIcon(QIcon(":/icon/volume-high-solid.png"));
            volnone=0;
        }
        volnone=0;
    }
}

歌词面板、音量条的隐藏、显示:

void MainWindow::on_volButton_clicked()
{
    if(ui->volSlider->isHidden())//显示、隐藏音量条
    ui->volSlider->show();
    else
    ui->volSlider->hide();
}
void MainWindow::on_listButton_clicked()
{
    if(ui->listWidget_lrc->isHidden())//按下按钮显示或隐藏歌词面板
    {
        ui->listWidget_lrc->show();
        ui->label->setText("歌词:");
    }
    else
    {
        ui->listWidget_lrc->hide();
        ui->label->setText("播放列表:");
    }

}

双击歌词跳转到对应时间播放进度:

void MainWindow::on_listWidget_lrc_itemDoubleClicked(QListWidgetItem *item)
{
    int index=ui->listWidget_lrc->currentRow();
    QMap<qint64,int>::iterator iter=lrcrow.begin();//双击歌词跳转到歌词对应的播放进度
    for(int i=0;i<index;i++)
    {
        iter++;
    }
    media_player->setPosition(iter.key());
}

到这里,这个音乐播放器就算完成了。

源码:点击下载(onedrive链接)

欢迎在评论区提问交流~


逸一时,误一世