QT练手项目:音乐播放器
先叠几层甲:
该项目中很多功能和函数的实现方法肯定有更简单更优化的,本文章里的方法属于比较笨的那种,仅供新手初学者以及博主自己参考,文章中提供的代码及源文件难免会有疏忽、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());
}
到这里,这个音乐播放器就算完成了。
欢迎在评论区提问交流~
- 感谢你赐予我前进的力量