做GUI应用程序开发的同学想必都知道MVC设计模式,MVC即Model-View-Controller,模型-视图-控制器。现我试图将Qt中的“MVC”讲清楚,先从简单的模型-视图说起。
模型-视图中,模型用于数据的存放/管理,视图用于数据的显示。模型-视图的核心思想是将模型和视图解耦,即将二者分离:模型对外提供标准接口,通过接口外界可以存取数据,模型不需要关心数据的显示;视图负责定义数据的显示方式,它不需要知道数据的组织存储。模型-视图从概念图上可以直观的理解为:
Qt中提供了几个现成的模型用于处理数据项,当然了,所谓模型其实就是c++中的类。
(1) QStringListModel:用于存储简单的QString类型数据的列表项
(2) QStandardItemModel:用于管理组成树形结构存储的数据项,其中每一个数据项可以是任意类型的数据
(3) QFileSystemModel:用于管理有关本地文件系统的文件和目录信息
这些模型类都继承自QAbstractTableModel:
同理,Qt也提供了几个现成的视图用于数据显示:
(1) QListView:列表形式显示模型中的数据项
(2) QTableView:表格形式显示模型中的数据项
(3) QTreeView:树形结构显示模型中的数据项
这些类都继承自QAbstractItemView:
模型-视图的工作,主要依赖于信号与槽机制,
(1) 当数据发生改变时,模型发出信号以通知视图
(2) 当用户和视图进行交互时,视图发出信号通知模型以提供交互信息
问:视图如何找到模型中的数据?
前面讲到模型“对外提供标准接口,通过接口外界可以存取”,这个标准的接口就是索引,视图通过模型提供的索引访问模型中的具体数据,也就是说Qt模型中必须为模型中的每一个数据项提供一个独一无二的索引,这是模型-视图中的关键技术。这个索引接口定义在QAbstractItemModel类中,它是一个虚函数:
virtual QModelIndex index(int row,int column, const QModelIndex &parent = QModelIndex()) const = 0;
该函数的返回的是一个QModelIndex的对象,QModelIndex是Qt模型中的索引类,它其中包含了具体数据访问的途径和一个指向模型的指针:
QVariant data(int role = Qt::DisplayRole) const; //获取该索引指向的数据,返回值类型QVariant这个后面说
const QAbstractItemModel* model() const; //获取该索引所在的模型
该函数的返回值不难理解,难以理解的是3个形参。这个得从Qt模型的索引机制讲起。
Qt中要获取模型中的某个数据项,需要知道3个量:行号、列号、父节点,这3量合称为三元组。索引在需要时由模型实时创建。
视图的分类主要有:行(List)、列表(Table)和树形(Tree)。对应到模型中的数据项的摆放也主要是这3种结构。下面分别对这3种数据组织模型的存取举例。需要说明,对数据的存取不单单是下面所举例的方法,详细内容可参阅Qt Create提供的帮助文档。
(1) 行结构数据项模型
行结构的数据项存放于QStringListModel模型中,
//Widget.h
class Widget : public QWidget
{
Q_OBJECT
public:
QStringListModel m_model; //列表模型
QListView m_listview; //列表视图
void initListMode(); //列表模型初始化函数
void initListView(); //列表视图初始化函数
QPushButton testBtn; //按钮,用于测试索引数据
public:
Widget(QWidget *parent = 0);
~Widget();
public slots:
void onTestBtnClick(); //按钮槽函数
};
//Widget.cpp
Widget::Widget(QWidget *parent) : QWidget(parent), testBtn(this)
{
//初始化按钮并连接信号与槽
testBtn.move(200, 140);
testBtn.setText("inedx");
connect(&testBtn, SIGNAL(clicked()), this, SLOT(onTestBtnClick()));
//初始化模型和视图
initListMode();
initListView();
//将视图和模型挂接
m_listview.setModel(&m_model);
}
void Widget::initListMode()
{
m_model.setParent(this);
QStringList data;
data << "Hello" << "World" << "Qt";
m_model.setStringList(data);
}
void Widget::initListView()
{
m_listview.setParent(this);
m_listview.move(10, 10);
m_listview.resize(240, 120);
}
void Widget::onTestBtnClick()
{
//通过三元组索引数据
for (int i = 0; i < m_model.rowCount(); i++)
{
//虚拟根节点,由QModelIndex()获得
QModelIndex index = m_model.index(i, 0, QModelIndex());
QVariant v = index.data();
qDebug() << v;
}
}
标准模型QStandardItemModel模型支持列表结构的数据项:
//Widget.h
class Widget : public QWidget
{
Q_OBJECT
QStandardItemModel m_model; //标准模型
QTableView m_tableview; //表格视图
void initStandardItemModel();
void initTableView();
QPushButton testBtn;
public:
Widget(QWidget *parent = 0);
~Widget();
public slots:
void onTestBtnClick();
};
//Widget.cpp
Widget::Widget(QWidget *parent) : QWidget(parent), testBtn(this)
{
testBtn.move(200, 140);
testBtn.setText("inedx");
connect(&testBtn, SIGNAL(clicked()), this, SLOT(onTestBtnClick()));
initStandardItemModel();
initTableView();
m_tableview.setModel(&m_model);
}
void Widget::initStandardItemModel()
{
//QStandardItemModel的每一个数据项为QStandardItem类型
QStandardItem* root = m_model.invisibleRootItem(); //定义虚拟节点的数据项
QStandardItem* item0 = new QStandardItem(); //定义数据项0
QStandardItem* item1 = new QStandardItem(); //数据项1
QStandardItem* item2 = new QStandardItem();
if((root != NULL) && (item0 != NULL) && (item1 != NULL) && (item2 != NULL) )
{
//为行的每一列增加数据
item0->setData(1, Qt::DisplayRole); //Qt::DisplayRole为该数据的角色。模型中需要为数据设置具体角色才能在视图中显示
item1->setData("Qt", Qt::DisplayRole);
item2->setData(139, Qt::DisplayRole);
item0->setEditable(false);
item1->setEditable(false);
item2->setEditable(false);
//将数据项增加到虚拟根节点中
root->setChild(0, 1, item1);
root->setChild(0, 2, item2);
root->setChild(0, 0, item0);
}
}
void Widget::onTestBtnClick()
{
//通过三元组获取数据项
for (int i = 0; i < m_model.rowCount(); i++)
{
for (int j = 0; j < m_model.columnCount(); j++)
{
//QModelIndex()为虚拟根节点
QModelIndex index = m_model.index(i, j, QModelIndex());
QVariant v = index.data();
qDebug() << v;
}
}
}
标准模型QStandardItemModel模型还支持树形结构的数据项,对上面代码稍作更改:
class Widget : public QWidget
{
Q_OBJECT
QStandardItemModel m_model; //标准模型
QTreeView m_treeview; //树形视图
void initStandardItemModel();
void initTreeView();
QPushButton testBtn;
public:
Widget(QWidget *parent = 0);
~Widget();
public slots:
void onTestBtnClick();
};
Widget::Widget(QWidget *parent) : QWidget(parent), testBtn(this)
{
testBtn.move(200, 140);
testBtn.setText("inedx");
connect(&testBtn, SIGNAL(clicked()), this, SLOT(onTestBtnClick()));
initStandardItemModel();
initTreeView();
m_treeview.setModel(&m_model);
}
//为QStandardItem增加数据项的操作并不修改
void Widget::initStandardItemModel()
{
QStandardItem* root = m_model.invisibleRootItem();
QStandardItem* item0 = new QStandardItem();
QStandardItem* item1 = new QStandardItem();
QStandardItem* item2 = new QStandardItem();
if((item0 != NULL) && (item1 != NULL) && (item2 != NULL) )
{
item0->setData(6, Qt::DisplayRole);
item1->setData("Qt", Qt::DisplayRole);
item2->setData(139, Qt::DisplayRole);
item0->setEditable(false);
item1->setEditable(false);
item2->setEditable(false);
root->setChild(0, 1, item1);
root->setChild(0, 2, item2);
root->setChild(0, 0, item0);
}
}
void Widget::initTreeView()
{
m_treeview.setParent(this);
m_treeview.move(10, 10);
m_treeview.resize(321, 120);
}
void Widget::onTestBtnClick()
{
for (int i = 0; i < m_model.rowCount(); i++)
{
for (int j = 0; j < m_model.columnCount(); j++)
{
QModelIndex index = m_model.index(i, j, QModelIndex());
QVariant v = index.data();
qDebug() << v;
}
}
}
运行结果似乎与列表没多大不同。那是因为我们没有增加子节点。下面增加子节点尝试:
void Widget::initStandardItemModel()
{
QStandardItem* root = m_model.invisibleRootItem();
QStandardItem* item0 = new QStandardItem();
QStandardItem* item1 = new QStandardItem();
QStandardItem* item2 = new QStandardItem();
QStandardItem* ch = new QStandardItem();
if((ch != NULL) && (item0 != NULL) && (item1 != NULL) && (item2 != NULL) )
{
item0->setData(6, Qt::DisplayRole);
item1->setData("Qt", Qt::DisplayRole);
item2->setData(139, Qt::DisplayRole);
item0->setEditable(false);
item1->setEditable(false);
item2->setEditable(false);
ch->setData("ch", Qt::DisplayRole);
item0->setChild(0, 0, ch);
root->setChild(0, 1, item1);
root->setChild(0, 2, item2);
root->setChild(0, 0, item0);
}
}
用于存放树形结构的数据项还有QFileSystemModel,它用于组织文件系统的目录文件。目录文件的组织方式本来就是树形的:
class Widget : public QWidget
{
Q_OBJECT
QFileSystemModel m_model; //文件系统模型
QTreeView m_treeview;
void initFileSystemModel();
void initTreeView();
QPushButton testBtn;
public:
Widget(QWidget *parent = 0);
~Widget();
public slots:
void onTestBtnClick();
};
Widget::Widget(QWidget *parent) : QWidget(parent), testBtn(this)
{
testBtn.move(200, 140);
testBtn.setText("inedx");
connect(&testBtn, SIGNAL(clicked()), this, SLOT(onTestBtnClick()));
//初始化文件系统模型
initFileSystemModel();
//为视图连接模型
m_treeview.setModel(&m_model);
//初始化树形视图
initTreeView(); //注意这3个步骤的顺序
}
void Widget::initFileSystemModel()
{
//设置模型的根目录
m_model.setRootPath(QDir::currentPath());
}
void Widget::initTreeView()
{
m_treeview.setParent(this);
m_treeview.move(10, 10);
m_treeview.resize(600, 200);
//设置树形结构的根目录
m_treeview.setRootIndex(m_model.index(QDir::currentPath()));
}
void Widget::onTestBtnClick()
{
//文件系统模型可通过路径获取根节点
QString path = QDir::currentPath();
QModelIndex root = m_model.index(path);
qDebug() << m_model.rowCount(root);
//通过三元组获取数据项
for (int i = 0; i < m_model.rowCount(root); i++)
{
for (int j = 0; j < m_model.columnCount(); j++)
{
QModelIndex ci = m_model.index(i, j, root);
qDebug() << ci.data().toString();
}
}
}
模型中的数据在试图中的显示方式(用途)可能不同,通过索引得到的存放数据的位置可以有多个,假设为2个,一个是用于常规显示,另一个用于鼠标在视图中对应位置停留时提示用的显示,要如何实现?
这就需要模型为数据设置数据角色(数据属性)。数据角色在不同的视图中以统一的风格显示对应数据。常见的的数据数据角色定义为(带带有Role单词):
Qt::DisplayRole 直接可见的提示信息(QString)
Qt::DecorationRole 以图表的方式显示(QIcon QPixmap)
Qt::EditRole 可编辑的数据信息(QString)
Qt::ToolTipRole 悬浮框中的补充提示信息(QString)
Qt::StatusTipRole 在状态指片内个显示的提示信息(QString)
Qt::WhatsThisRole 悬浮框中的详细帮助信息(QString)
Qt::SizeHintRole 数据大小信息(QSize)
以前面的标准模型QStandardItemModel模型的列表结构的数据项为例,初始化模型操作的代码为:
void Widget::initStandardItemModel()
{
QStandardItem* root = m_model.invisibleRootItem();
QStandardItem* item0 = new QStandardItem();
QStandardItem* item1 = new QStandardItem();
QStandardItem* item2 = new QStandardItem();
QStandardItem* ch = new QStandardItem();
if((ch != NULL) && (item0 != NULL) && (item1 != NULL) && (item2 != NULL) )
{
//为数据项增加两个数据,分别用于直接可见的显示信息和悬浮的提示信息
item0->setData(6, Qt::DisplayRole);
item0->setData("ID", Qt::ToolTipRole);
item1->setData("Qt", Qt::DisplayRole);
item1->setData("Object", Qt::ToolTipRole);
item2->setData(139, Qt::DisplayRole);
item2->setData("score", Qt::ToolTipRole);
item0->setEditable(false);
item1->setEditable(false);
item2->setEditable(false);
ch->setData("ch", Qt::DisplayRole);
root->setChild(0, 0, item0);
root->setChild(0, 1, item1);
root->setChild(0, 2, item2);
}
}
数据角色是模型为数据项中的数据附加的一个属性,这个属性代表Qt平台推荐的数据显示方式。不同的的视图完全可以自由解析或者忽略数据的角色信息。
Qt中的视图-模型基本操作就是这些,MVC中的控制器部分将在下来的写在文章。