最近的数据库课设准备做一个音乐播放器,以QML/QT为构架,边学边做在CSDN上做些笔记。
目前尝试实现本地音乐播放功能中读取本地文件夹下MP3文件并显示的功能,先直接上效果图。
选择了某一文件并通过模型/视图构架进行了显示
选择了一个没有合适文件的目录会显示出提示用户进行选择的按钮
整个界面使用了QML进行编写,笔者在这个功能上花了比较多的时间,所以先写这个内容,之后有时间把之前的一些内容补上来。
所涉及到的内容
1.模型/视图构架
2.自定义模型类
3.QML与C++交互
4.QML中信号与槽
5.Qt当中读写文件
6.Qt当中遍历文件夹
实现功能的思路大致是自定义一个模型类,其中存储了自定义的音乐类来表示文件夹内的音乐文件信息。将得到的模型类绑定到ListView上并且自定义一个委托去显示。由于笔者这里是通过Loader来进行中间页面的切换的,所以每次载入页面时,根据本地的一个.dat文件中的本地音乐目录信息,来更新显示列表。并且下次再打开播放器的时候可以根据上次设置的音乐文件目录来显示列表。大致是这样的思路,接下来给出一些细节。
简单来说就是数据通过模型来存储,通过委托去设置如何显示数据,在视图上绑定模型与委托去显示数据,从而得到数据与显示的分离。QT中的模型/视图构架也是MVC设计模式的一种延伸。
QML当中提供了ListView,TreeView,TableView这三个视图,这里是直接使用了ListView这个视图,使用的方式大致如下:
ListView{
id:l_view
model:m_model//自定义的模型对象
focus:true
delegate:Button{
//需要注意这里的index与model的index是不一样的使用,要使用listview自带的index
property string btn_color
anchors.left:parent.left
anchors.right: parent.right
height: 30
background: Rectangle{
id:btn_bck
color:btn_color=(index%2==0?"white":"#f2f5ff")
//根据序号的奇偶显示不同的颜色
}
Row{
anchors.left:parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 10
Text{//可以直接设置Text的大小来保障文本长度不越界
anchors.verticalCenter: parent.verticalCenter
text:lview.model.name(index)//在切换目录时如果切换目录不含音乐则虽然模型是空的,
//但是view的index是不会改变的,会造成越界问题,通过槽函数将visible设置为false解决这个问题
font.pointSize: 10
}
}
MouseArea{//鼠标移上列表中的项颜色会发生变化
anchors.fill: parent
hoverEnabled: true
onEntered: {
btn_bck.color="#e6e6f0"
}
onExited: {
btn_bck.color=btn_color
}
onClicked: {
console.log(index)
console.log(lview.currentIndex)
}
}
//每创建一个列表项就更新model中的当前项
Component.onCompleted: {
console.log(index)
lview.model.setcuritem(index,this)
}
}
}
QML中同样提供了ListView默认用的ListModel模型可以在其中设置ListElement(这是一个字典里面存储键值对)来设置所使用的数据,但是这里我用不到就不多说了。
之前的模型/视图构架说了要将模型绑定到视图上,但是默认的模型很多时候不能满足我们的需求,只能自定义模型。这里因为是绑定到ListView上,所以我们自定义的模型是继承自QAbstractListModel,去自定义一个ListModel。
这里先给出我们自定义的一个音乐类,在Model当中存的是这个类。
#ifndef MUSIC_H
#define MUSIC_H
#include
#include
class Music:public QObject//我这里偷懒了就通过一个.h去写这个类了,最好不要这样。。。
{
Q_OBJECT
Q_PROPERTY(QString Name READ Name)
//如果想要在QML中直接使用一个属性必须定义成Q_PROPERTY,常用设置READ,WRITE,NOTIFY属性
private:
QString name;
QString writer;
QString time;
QString album;
public:
Music(const QString Name="",const QString Writer="",const QString Time="",const QString Album="",QObject* parent=nullptr):QObject(parent),name(Name),writer(Writer),time(Time),album(Album)
{}//因为要存在vector中,所以默认constructor是不能少的
//这里需要我显示地定义复制与赋值构造函数,这两个函数一般用来处理深度/浅度赋值的问题
Music(const Music& music)
{
this->name=music.Name();
this->writer=music.Writer();
this->time=music.Time();
this->album=music.Album();
}
Music operator=(const Music& music)
{
return music;
}
QString Name() const
{
return name;
}
QString Writer() const//表示不能对该成员内部的变量做修改
{
return writer;
}
QString Time()const
{
return time;
}
QString Album()const
{
return album;
}
QVariant cur;//表示当前的model组件,因为是会根据外面的值进行改变的,所以是public
~Music()
{}
};
#endif // MUSIC_H
实现QAbstractListModel必须要实现以下三个虚函数,并且在自定义的模型类中给出用于存储数据的数据结构。
int rowCount(const QModelIndex& parent=QModelIndex())//用来给出模型中项的个数
QVarient data(cosnt QModelIndex& index,int role=Qt::QDisplayRole) const//返回相应的数据
QHash roleNames() const//为了在QML中通过别名来使用数据
除了这三个函数之外,还需要给出一些常用的函数比如添加数据,删除数据,等等。接下来给出全部代码。使用模型类的时候有一些问题,虽然能够实现,但没有按照很标准的做法去做,如果有大佬知道的话,也希望能帮我指出,这里先谢谢啦
#ifndef MUSIC_MODEL_H
#define MUSIC_MODEL_H
#include
#include
#include
#include
#include"music.h"
class Music_Model : public QAbstractListModel
//自定义数据模型,需要自己给出所存储模型的数据类型
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countchange)//因为要在QML中使用这个属性
private:
QVector music_vct;//使用vector去存储数据
int internel_index=-1;//内部index用来处理排序以及显示的问题
signals:
void countchange();//当前模型更新时发射这个信号
void cur_change(int index);//当前模型选项改变时发射这个信号
public:
//这个枚举相当于定义数据的别名
enum datatype{
type1=1,
type2,
type3,
type4,
type5
};
Music_Model(QObject *parent=nullptr);
Q_INVOKABLE void add_data(const QString name,const QString writer,const QString album,const QString time);
Q_INVOKABLE void remove_data(int index);
Q_INVOKABLE void clear_model();
Q_INVOKABLE int count()const;
void Add(Music &music);
int rowCount(const QModelIndex &parent = QModelIndex() ) const
{//一定要实现的虚函数1
Q_UNUSED(parent);
return music_vct.size();
}
QVariant data(const QModelIndex &index,int role=Qt::DisplayRole)const;
//一定要实现的虚函数2
QHash roleNames()const;
//一定要实现的虚函数3,如果要在QML中使用模型那么一定要重写这个函数
Q_INVOKABLE void setcuritem(int index,QVariant j)
{
internel_index=index;
emit cur_change(index);
music_vct[index].cur=j;
}
//因为QML中没办法正常使用Music类,所以试着直接返回QString试试,讲道理最好不要这么做
Q_INVOKABLE QString name(int index);
~Music_Model(){}
};
#endif // MUSIC_MODEL_H
以下是.cpp文件
#include "music_model.h"
#include
Music_Model::Music_Model(QObject* parent):QAbstractListModel (parent)
{
//注意凡是在QT中使用的组件/模型类都要设置其父亲
}
void Music_Model::Add(Music &music)
{
emit countchange();
beginInsertRows(QModelIndex(),count(),count());
//注意在对自定义模型插入前必须要先给出这个函数
//第二与三个参数表示插入后的数据是第几行都是最后一行则表示都是插在最后,换句话说这里是给出插入位置,
//因为一直插入在最后一行,所以下标给vector现在的范围
music_vct.push_back(music);
endInsertRows();
//对行/列进行插入必须在上下文中进行,上下文范围限定函数是BeginInsertRows(
}
void Music_Model::clear_model()
{
internel_index=-1;
emit countchange();
beginRemoveRows(QModelIndex(),0,music_vct.size()-1);//注意这里删除范围是指下标范围的闭区间,否则会越界
music_vct.clear();
endRemoveRows();
//删除通插入要在上下文中进行
}
void Music_Model::add_data(const QString name,const QString writer,const QString album,const QString time)
{
emit countchange();
Music music(name,writer,album,time);
//这里的添加数据一定是末尾添加
Add(music);
}
void Music_Model::remove_data(int index)
{
emit countchange();
beginRemoveRows(QModelIndex(),index,index);//后两个参数的含义与插入时的相同
music_vct.erase(music_vct.begin()+index);
endRemoveRows();
}
int Music_Model::count()const
{
return music_vct.size();
}
/*得到相应索引下的数据 这里大致说一下QVariant,这个东西相当于集合类型,
可以存多种QT基本类型,并且提供了相互转化的函数*/
QVariant Music_Model::data(const QModelIndex &index,int role)const
{
if (index.row() < 0 || index.row() >= music_vct.size())
{
return QVariant();
}
qDebug()< Music_Model::roleNames() const
{
QHash d;
d[datatype::type1] = QByteArray("Name");
d[datatype::type2] = QByteArray("writer");
d[datatype::type3] = QByteArray("time");
d[datatype::type4] = QByteArray("album");
d[datatype::type5]= QByteArray("cur");
return d;
}
QString Music_Model::name(int index)
{
if(index<0||index>=music_vct.size())
return "";
qDebug()<< music_vct.at(index).Name()<
在C++中定义完了模型与类,得放入QML中调用。这里涉及到了如何将自定义的C++类载入QML,这里使用的是注册的方法。比较简单就直接给出代码了。
#include
#include
#include
#include
#include"dir_to_model.h"
#include"music_model.h"
#include"music.h"
#include"local_dir.h"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
//这个函数要结合泛型去使用,函数原型如下
//qmlRegisterType<头文件名>("头文件名",1,0,"类型名") 类型名务必大写
//使用的时候如下import dir_to_model 1.0
qmlRegisterType("local_dir",1,0,"Local_Dir");
//引入一个文件读写类
qmlRegisterType("dir_to_model",1,0,"Dir_Model");
//引入文件地址与模型的转换类
qmlRegisterType("music_model",1,0,"Music_Model");
//引入自定义的模型
qmlRegisterType("music",1,0,"Music");
//引入Music类
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
使用的时候需要注意最好是像使用一般的QML对象那样创建一个带id的组件,比如我使用自定义的model。
Music_Model
{
id:m_model
}
这样在该界面载入时就会创建这个对象(这也就是为什么一定要默认构造函数),因为定义了id所以要使用这个自定义类对象的函数与属性还是很方便的。
之所以要用信号与槽是因为每次通过按钮更新音乐文件目录时,我们会得到新目录下的文件数量,根据文件数量是否为0(也就是有无音乐文件)来决定是显示列表还是显示提示按钮。通过在更新时发射一个模型当中文件信息数量的信号,并用一个槽函数去处理。
QML中信号与槽的使用和QT当中的没差太多,首先先说一下如何自定义信号,QT中信号是一个函数QML中也是。在某一个组件中以siganl关键字,定义一个带类型参数且无返回值的函数作为信号。如下:
Component{
id:tp
signal:example_signal(int bz)
}
通过Connections组件将发射信号的对象与对应的处理连接起来,大致如下:
Connections
{
target:tp
//target表示发射信号的对象,我这里是将信号直接在发射信号的对象当中声明的
onExample_signal:{
//这里处理的属性名最好按照这样的格式去写
}
}
这里给出功能中所用的信号与槽的代码。
FolderDialog{
//文件夹选择对话框,需要通过一个文件对话框选择我所需要的文件目录
//这个组件需要import Qt.labs.platform 1.0
signal change_dir(int m_count)
//该对象中设置信号
id:file_dir
onFolderChanged: {//当前目录变化
music_dir=folder;//music_dir表示音乐文件目录的全局变量
dmodel.dir_to_model=music_dir.substring(8)
//这里的dmodel对象是一个用于将获得的文件夹路径转换成模型的c++自定义对象
//注意要从第八个字符开始才能获得绝对路径
dmodel.update_model(m_model)
//更新音乐模型
file_dir.change_dir(m_model.count)
//音乐模型被更新了,发射一个信号,根据新模型的长度决定要显示什么
//因为是子组件的信号,所以一定要子组件.signal()去使用
local_d.set_localdir(music_dir.substring(8));
//把新的目录写入一个.dat文件
}
}
Connections{
target:file_dir
//发射信号的对象是文件目录选择框
onChange_dir:{
console.log("folderchange信号触发后槽函数启动:change the dir")
num_song=m_count.toString()
//nums_song是一个用于显示目录中有多少首歌的全局变量
if(m_count==0)
{
list_view.visible=false
import_note.visible=true
}
else
{
list_view.visible=true
import_note.visible=false
}
//没文件显示提示,有文件显示列表
}
}
我这里用Qt下读写文件基本上就涉及到QDir,QFile这两个类。
QDir用来设置生成文件的所在目录,默认的目录是编译完成后以Build开头的构建目录(如果没有改过构架目录名的话)。可以使用CurrentPath()与setCurrentPath()这两个函数去显示当前项目目录与设置当前项目目录。
读写文件要用QFile去处理,处理方式与C++当中文件的处理差不多,基本上分为如下几步:
之所以要用读写文件的功能是将我所选择的本地音乐文件目录读写到一个文件中,使下次打开的时候仍然可以显示上次所选择的内容。这里我设置了一个Local_dir类去处理该文件的读写,关键的代码是如何写这个文件。具体的代码如图所示:
void Local_dir::set_localdir(QString dir)
{
emit change_dir();
//每次目录改变都会发射一个信号
QFile* file=new QFile;
file->setFileName(file_dir);
file->open(QIODevice::ReadWrite|QIODevice::Text|QIODevice::Truncate);
//因为我在该类的构造函数中做了处理,保证文件是一定存在的,这里直接打开就可以了
file->write(dir.toUtf8());//将一个UTF-8格式的字符串按二进制数组写入
//也就是说写入的数据必须是二进制数组的
file->close();
this->local_dir=dir;
}
因为要得到一个文件夹下所有的音乐文件必须要遍历文件夹,QT提供了QDirIterator来遍历文件夹,所涉及到的类还有QDir与QStringList,通过QDir判断当前目录是否存在,并用QStringList来起到文件后缀名过滤的作用。声明QDirIterator对象后,使用hasNext()函数判断文件有无读完,并用next()得到目录下的下一个文件,通过fileInfo()返回文件信息。
在这个功能中通过遍历文件夹读取文件后更新模型,具体代码如下:
void dir_to_model::update_model(Music_Model* music_model)
//因为QML对象实际上是使用的指针来动态声明的,所以这里的参数一定要是指针
{
qDebug()<count()!=0)
music_model->clear_model();
if(this->dir_str.length()==0)
{
qDebug()<<"null dir"<
大体上关键代码就这些,如果有大佬有什么意见的话,欢迎猛戳。
你可能感兴趣的:(杂项)