一、模型视图简介
有时,我们的系统需要显示大量数据,比如从数据库中读取数据,以自己的方式显示在自己的应用程序的界面中。早期的 Qt 要实现这个功能,需要定义一个组件,在这个组件中保存一个数据对象,比如一个列表。我们对这个列表进行查找、插入等的操作,或者把修改的地方写回,然后刷新组件进行显示。这个思路很简单,也很清晰,但是对于大型程序,这种设计就显得苍白无力。比如,在一个大型系统中,你的数据可能很大,全部存入一个组件的数据对象中,效率会很低,并且这样的设计也很难在不同组件之间共享数据。如果你要几个组件共享一个数据对象,要么你就要用存取函数公开这个数据对象,要么你就必须把这个数据对象放进不同的组件分别进行维护。
Smalltalk 语言发明了一种崭新的实现,用来解决这个问题,这就是著名的 MVC 模型。对这个模型无需多言。MVC 是 Model-View-Controller 的简写,即模型-视图-控制器。在 MVC 中,模型负责获取需要显示的数据,并且存储这些数据的修改。每种数据类型都有它自己对应的模型,但是这些模型提供一个相同的 API,用于隐藏内部实现。视图用于将模型数据显示给用户。对于数量很大的数据,或许只显示一小部分,这样就能很好的提高性能。控制器是模型和视图之间的媒介,将用户的动作解析成对数据的操作,比如查找数据或者修改数据,然后转发给模型执行,最后再将模型中需要被显示的数据直接转发给视图进行显示。MVC 的核心思想是分层,不同的层应用不同的功能。
Qt 4 开始,引入了类似的 model/view 架构来处理数据和面向最终用户的显示之间的关系。当 MVC 的 V 和 C 结合在一起,我们就得到了 model/view 架构。这种架构依然将数据和界面分离,但是框架更为简单。同样,这种架构也允许使用不同界面显示同一数据,也能够在不改变数据的情况下添加新的显示界面。为了处理用户输入,我们还引入了委托(delegate)。引入委托的好处是,我们能够自定义数据项的渲染和编辑。
视图从模型获取模型索引,这种索引就是数据项的引用。通过将这个模型索引反向传给模型,视图又可以从数据源获取数据。在标准视图中,委托渲染数据项;在需要编辑数据时,委托使用直接模型索引直接与模型进行交互。
总的来说,model/view 架构将传统的 MV 模型分为三部分:模型、视图和委托。每一个组件都由一个抽象类定义,这个抽象类提供了基本的公共接口以及一些默认实现。模型、视图和委托则使用信号槽进行交互:
(1)来自模型的信号通知视图,其底层维护的数据发生了改变;
(2)来自视图的信号提供了有关用户与界面进行交互的信息;
(3)来自委托的信号在用户编辑数据项时使用,用于告知模型和视图编辑器的状态。
所有的模型都是QAbstractItemModel的子类。这个类定义了供视图和委托访问数据的接口。模型并不存储数据本身。这意味着,你可以将数据存储在一个数据结构中、另外的类中、文件中、数据库中,或者其他你所能想到的东西中。
QAbstractItemModel提供的接口足够灵活,足以应付以表格、列表和树的形式显示的数据。但是,如果你需要为列表或者表格设计另外的模型,直接继承QAbstractListModel和QAbstractTableModel类可能更好一些,因为这两个类已经实现了很多通用函数。
Qt 内置了许多标准模型:
(1)QStringListModel:存储简单的字符串列表。
(2)QStandardItemModel:可以用于树结构的存储,提供了层次数据。
(3)QFileSystemModel:本地系统的文件和目录信息。
(4)QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel:存取数据库数据。
正如上面所说,如果这些标准模型不能满足你的需要,就必须继承QAbstractItemModel、QAbstractListModel或者QAbstractTableModel,创建自己的模型类。
Qt 还提供了一系列预定义好的视图:QListView用于显示列表,QTableView用于显示表格,QTreeView用于显示层次数据。这些类都是QAbstractItemView的子类。这意味着,如果你要创建新的视图类,要继承QAbstractItemView。
QAbstractItemDelegate则是所有委托的抽象基类。自 Qt 4.4 依赖,默认的委托实现是QStyledItemDelegate。但是,QStyledItemDelegate和QItemDelegate都可以作为视图的编辑器,二者的区别在于,QStyledItemDelegate使用当前样式进行绘制。在实现自定义委托时,推荐使用QStyledItemDelegate作为基类,或者结合 Qt style sheets。
如果你觉得 model/view 模型过于复杂,或者有很多功能是用不到的,Qt 还有一系列方便使用的类。这些类都是继承自标准的视图类,并且继承了标准模型。这些类并不是为其他类继承而准备的,只是为了使用方便。它们包括QListWidget、QTreeWidget和QTableWidget。这些类远不如视图类灵活,不能使用另外的模型,因此只适用于简单的情形。
二、QListWidget
#include "widget.h"
#include <QHBoxLayout>
#include <QIcon>
#include <QString>
#include <QObject>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
/*
* QListWidget 是简单的列表组件。当我们不需要复杂的列表时,可以选择 QListWidget。
* QListWidget 中可以添加 QListWidgetItem 类型作为列表项, QListWidgetItem 即可以
* 有文本,也可以有图标。
*/
lb=new QLabel(this);
lb->setFixedWidth(70);
lw=new QListWidget(this);
//更改图标的显示方式
lw->setViewMode(QListView::IconMode);
/*
* 注意这两种添加方式的区别:第一种需要在构造时设置所要添加到的 QListWidget 对象;
* 第二种方法不需要这样设置,而是要调用 addItem()或者 insertItem()自行添加。
*/
new QListWidgetItem(QIcon(":/listview/chrome"),tr("Chrome"),lw);
lw->addItem(new QListWidgetItem(QIcon(":/listview/firefox"),tr("Firefox")));
lw->addItem(new QListWidgetItem(QIcon(":/listview/opera"),tr("Opera")));
QListWidgetItem *newitem=new QListWidgetItem;
newitem->setIcon(QIcon(":/listview/ie"));
newitem->setText(tr("IE"));
lw->insertItem(3,newitem);
//使用布局管理器
QHBoxLayout *layout=new QHBoxLayout;
layout->addWidget(lb);
layout->addWidget(lw);
setLayout(layout);
//使用 QListWidget 发出的各种信号来判断是哪个列表项被选择。
connect(lw,&QListWidget::currentTextChanged,lb,&QLabel::setText);
}
Widget::~Widget()
{
}
三、QTreeWidget
顾名思义,这是用来展示树型结构(也就是层次结构)的。同前面说的QListWidget类似,这个类需要同另外一个辅助类QTreeWidgetItem一起使用。不过,既然是提供方面的封装类,即便是看上去很复杂的树,在使用这个类的时候也是显得比较简单的。当不需要使用复杂的QTreeView特性的时候,我们可以直接使用QTreeWidget代替。
#include "widget.h"
#include <QApplication>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QStringList>
#include <QString>
#include <QList>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
/*
* 创建一个 QTreeWidget 实例,使用 QStringList 设置了 headers,也就是树的表头。
* 接下来我们使用的还是 QStringList 设置数据。这样,我们实现的是带有层次结构的
* 树状表格。利用这一属性,我们可以比较简单地实现类似 Windows 资源管理器的界面。
*/
QTreeWidget tw;
QStringList headers;
headers<<"Name"<<"Number";
tw.setHeaderLabels(headers);
QStringList roottextlist1;
roottextlist1<<"Root"<<"0";
QStringList roottextlist2;
roottextlist2<<"Root2"<<"0";
/*
* 向 QTreeWidget 添加QTreeWidgetItem。 QTreeWidgetItem 有很多重载的构造函数。
* 这里有2个参数,第一个参数用于指定这个项属于哪一个树,类似QListWidgetItem,
* 如果指定了这个值,则意味着该项被直接添加到树中;第二个参数指定显示的文字。
* 第二个参数是 QStringList 类型。我们创建了作为根的 QTreeWidgetItem root。
* 然后添加了第一个叶节点,之后又添加一个,而这个则设置了可选标记。最后,我们将
* 这个 root 添加到一个QTreeWidgetItem 的列表,作为 QTreeWidget 的数据项。
*/
QTreeWidgetItem *root=new QTreeWidgetItem(&tw,roottextlist1);
new QTreeWidgetItem(root,QStringList()<<QString("leaf 1")<<"1");
QTreeWidgetItem *leaf2=new QTreeWidgetItem(root,QStringList()<<QString("leaf 2")<<"2");
leaf2->setCheckState(0,Qt::Checked);
QTreeWidgetItem *root2=new QTreeWidgetItem(&tw,roottextlist2);
new QTreeWidgetItem(root2,QStringList()<<QString("leaf 1")<<"1");
tw.show();
return a.exec();
}
四、QTableWidget
#include "widget.h"
#include <QApplication>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QString>
#include <QStringList>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
/*
* 首先我们创建了 QTableWidget 对象,然后设置列数和行数。接下来使用一个 QStringList,
* 设置每一列的标题。我们可以通过调用 setItem()函数来设置表格的单元格的数据。这个函数
* 前两个参数分别是行索引和列索引,这两个值都是从 0 开始的,第三个参数则是一个
* QTableWidgetItem 对象。 Qt 会将这个对象放在第 row 行第 col 列的单元格中。
*/
QTableWidget tw;
tw.setColumnCount(3);
tw.setRowCount(5);
QStringList headers;
headers<<"ID"<<"Name"<<"Age";
tw.setHorizontalHeaderLabels(headers);
tw.setItem(0,0,new QTableWidgetItem(QString("01")));
tw.setItem(1,0,new QTableWidgetItem(QString("02")));
tw.setItem(2,0,new QTableWidgetItem(QString("03")));
tw.setItem(3,0,new QTableWidgetItem(QString("04")));
tw.setItem(4,0,new QTableWidgetItem(QString("05")));
tw.setItem(0,1,new QTableWidgetItem(QString("Eric")));
tw.show();
return a.exec();
}
前面我们已经了解到有关 list、table 和 tree 三个最常用的视图类的便捷类的使用。前面也提到过,由于这些类仅仅是提供方便,功能、实现自然不如真正的 model/view 强大。
QStringListModel是最简单的模型类,
具备向视图提供字符串数据的能力。QStringListModel是一个可编辑的模型,可以为组件提供一系列字符串作为数据。我们可以将其看作是封装了
QStringList的模型。QStringList是一种很常用的数据类型,实际上是一个
字符串列表(也就是QList<QString>)。既然是列表,它也就是线性的数据结构,因此,
QStringListModel很多时候都会作为QListView或者QComboBox这种只有一列的视图组件的数据模型。
五、QStringListModel
#include "widget.h"
#include <QStringList>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QInputDialog>
#include <QLineEdit>
#include <QMessageBox>
MyListView::MyListView(QWidget *parent)
: QWidget(parent)
{
this->resize(300,300);
/*
* 首先,我们创建了一个 QStringList 对象,向其中插入了几个数据;然后将其作为
* QStringListModel 的底层数据。这样,我们可以理解为, QStringListModel 将
* QStringList 包装了起来。视图获取模型的信号。
*/
QStringList data;
data<<"letter A"<<"letter B"<<"letter C";
model=new QStringListModel(this);
model->setStringList(data);
listView=new QListView(this);
listView->setModel(model);
//界面代码
QHBoxLayout *btnLayout = new QHBoxLayout;//水平分布
QPushButton *insertBtn = new QPushButton(tr("insert"),this);
connect(insertBtn,&QPushButton::clicked,this,&MyListView::insertData);
QPushButton *delBtn = new QPushButton(tr("Delete"),this);
connect(delBtn,&QPushButton::clicked,this,&MyListView::deleteData);
QPushButton *showBtn = new QPushButton(tr("Show"),this);
connect(showBtn,&QPushButton::clicked,this,&MyListView::showData);
btnLayout->addWidget(insertBtn);
btnLayout->addWidget(delBtn);
btnLayout->addWidget(showBtn);
QVBoxLayout *mainLayout = new QVBoxLayout(this);//垂直分布
mainLayout->addWidget(listView);
mainLayout->addLayout(btnLayout);
setLayout(mainLayout);
}
MyListView::~MyListView()
{
}
void MyListView::insertData(){
bool isok;
/*
* 我们使用 QInputDialog::getText()函数要求用户输入数据。这是 Qt 的标准对话框,
* 用于获取用户输入的字符串。
*/
QString text=QInputDialog::getText(this,"Insert",
"请输入新数据:",QLineEdit::Normal,"你正在输入新数据",&isok);
if(isok){
/*
* 当用户点击了 OK 按钮,我们使用listView->currentIndex()函数,获取 QListView当前数据。
* 这个函数的返回值是一个 QModelIndex 类型。现在只要这个类保存了三个重要的数据:
* 行索引、列索引以及该数据属于哪一个模型。该返回值是一个int,也就是当前是第几行。
*/
QModelIndex currindex=listView->currentIndex();
/*
* bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());
*该函数会将 count 行插入到模型给定的 row 的位置,新行的数据将会作为 parent 的子元素。
* 如果 row 为 0,新行将被插入到 parent 的所有数据之前,否则将在指定位置的数据之前。
* 如果 parent 没有子元素,则会新插入一个单列数据。函数插入成功返回 true,否则返回
* false。我们在这段代码中调用的是 insertRows(row, 1)。这是 QStringListModel 的一个重载。
* 参数 1 说明要插入 1 条数据。记得之前我们已经把 row 设置为当前行,因此,这行语句实际上
* 是在当前的 row 位置插入 count 行,这里的 count 为 1。由于我们没有添加任何数据,实际
* 效果是,我们在 row 位置插入了 1 个空行。
*/
model->insertRows(currindex.row(),1);
/*
* 利用 setData()函数把我们用 QInputDialog接受的数据设置为当前行数据,
* 并调用 edit()函数,使这一行可以被编辑。
*/
model->setData(currindex,text);
listView->edit(currindex);
}
}
void MyListView::deleteData(){
/*
* 用 rowCount()函数判断了一下,要求最终始终保留 1 行。这是因为我们写的简单地插入操作所限制,
* 如果把数据全部删除,就不能再插入数据了。
*/
if(model->rowCount()>1){
/*
* 使用模型的 removeRows()函数可以轻松完成这个操作。
*/
model->removeRows(listView->currentIndex().row(),1);
}
}
void MyListView::showData(){
QStringList data = model->stringList();//返回模型的字符串列表便于存储数据
QString str;
foreach(QString s, data) {
str += s + "\n";
}
QMessageBox::information(this, "Data", str);//标准模态对话框
}
六、QFileSystemModel
QStandardItemModel和QFileSystemModel。QStandardItemModel是一种多用途的模型,能够让列表、表格、树等视图显示不同的数据结构。这种模型会将数据保存起来。试想一下,列表和表格所要求的数据结构肯定是不一样的:前者是一维的,后者是二维的。因此,模型需要保存有实际数据,当视图是列表时,以一维的形式提供数据;当视图是表格时,以二维的形式提供数据。QFileSystemModel则是另外一种方式。它的作用是维护一个目录的信息。因此,它不需要保存数据本身,而是保存这些在本地文件系统中的实际数据的一个索引。我们可以利用QFileSystemModel显示文件系统的信息、甚至通过模型来修改文件系统。QTreeView是最适合应用QFileSystemModel的视图
#include "widget.h"
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QInputDialog>
#include <QMessageBox>
FileSystemWidget::FileSystemWidget(QWidget *parent)
: QWidget(parent)
{
/*
* 构造函数很简单,我们首先创建了 QFileSystemModel 实例,然后将其作为一个QTreeView 的模型。
* 注意我们将 QFileSystemModel 的根目录路径设置为当前目录。剩下来的都很简单,我们添加了按钮
* 之类,这些都不再赘述。对于 treeView 视图,我们使用了 setRootIndex()对模型进行过滤。我们
* 可以尝试一下,去掉这一句的话,我们的程序会显示整个文件系统的目录;而这一句的作用是,从模型
* 中找到 QDir::currentPath()所对应的索引,然后显示这一位置。也就是说,这一语句的作用实际是
* 设置显示哪个目录。
*/
model = new QFileSystemModel;
model->setRootPath(QDir::currentPath());
treeView = new QTreeView(this);
treeView->setModel(model);
treeView->setRootIndex(model->index(QDir::currentPath()));
treeView->setSortingEnabled(true);//对列头排序
//界面代码及信号与槽
QPushButton *mkdirButton = new QPushButton(tr("新建文件夹"), this);
QPushButton *rmButton = new QPushButton(tr("删除文件夹"), this);
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(mkdirButton);
buttonLayout->addWidget(rmButton);
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(treeView);
layout->addLayout(buttonLayout);
setLayout(layout);
setWindowTitle("File System Model");
resize(600,400);
connect(mkdirButton,&QPushButton::clicked,this,&FileSystemWidget::mkdir);
connect(rmButton,&QPushButton::clicked,this,&FileSystemWidget::rm);
}
void FileSystemWidget::mkdir(){
/*
* 正如代码所示,首先我们获取选择的目录。后面这个 isValid()判断很重要,因为默认情况
* 下是没有目录被选择的,此时路径是非法的,为了避免程序出现异常,必须要有这一步判断。
* 然后弹出对话框询问新的文件夹名字,如果创建失败会有提示,否则就是创建成功。这时候
* 你会发现,硬盘的实际位置的确创建了新的文件夹。
*/
QModelIndex index=treeView->currentIndex();
if(!index.isValid()){
return ;
}
// QInputDialog允许用户输入一个值,并将其值返回
QString dirName=QInputDialog::getText(this,tr("创建新文件夹"),tr("文件名"));
if(!dirName.isEmpty()){
if(!model->mkdir(index,dirName).isValid()){
QMessageBox::information(this,tr("创建新文件夹"),tr("创建失败"));
}
}
}
void FileSystemWidget::rm(){
/*
* 这里同样需要先检测路径是否合法。另外需要注意的是,目录和文件的删除不是一个函数,
* 需要调用 isDir()函数检测。
*/
QModelIndex index=treeView->currentIndex();
if(!index.isValid()){
return ;
}
bool ok;
if(model->fileInfo(index).isDir()){
ok=model->rmdir(index);//删除目录,成功则返回true
}
else{
ok=model->remove(index);//删除文件
}
if(!ok){
QMessageBox::information(this,tr("移除文件"),tr("操作失败"));
}
}
FileSystemWidget::~FileSystemWidget()
{
}