Qt视图模型

Catalogue   

  • 数据(Data):是实际的数据,如数据库的一个数据表或SQL查询结果,内存中的一个 StringList,或磁盘文件结构等。
  • 视图或视图组件(View):是屏幕上的界面组件,视图从数据模型获得每个数据项的模型索引(model index),通过模型索引获取数据,然后为界面组件提供显示数据。Qt 提供一些现成的数据视图组件,如 QListView、QTreeView 和 QTableView 等。
  • 模型或数据模型(Model):与实际数据通信,并为视图组件提供数据接口。它从原始数据提取需要的内容,用于视图组件进行显示和编辑。Qt 中有一些预定义的数据模型,如 QStringListModel 可作为 StringList 的数据模型,QSqlTableModel 可以作为数据库中一个数据表的数据模型。
  • 委托(Delegate):根据数据绘制UI

模型、视图和代理之间使用信号和槽通信。当源数据发生变化时,数据模型发射信号通知视图组件;当用户在界面上操作数据时,视图组件发射信号表示这些操作信息;当编辑数据时,代理发射信号告知数据模型和视图组件编辑器的状态。

视图

视图组件(View)就是显示数据模型的数据的界面组件,Qt 提供的视图组件如下:

  • QListView:用于显示单列的列表数据,适用于一维数据的操作。
  • QTreeView:用于显示树状结构数据,适用于树状结构数据的操作。
  • QTableView:用于显示表格状数据,适用于二维表格型数据的操作。
  • QColumnView:用多个QListView显示树状层次结构,树状结构的一层用一个QListView显示。
  • QHeaderView:提供行表头或列表头的视图组件,如QTableView的行表头和列表头。

模型

在 model/view架构中,model提供一种标准接口,供视图和委托访问数据。在Qt中,这个接口由QAbstractItemModel类进行定义。不管底层数据是如何存储的,只要是QAbstractItemModel的子类,都提供一种表格形式的层次结构。视图利用统一的转换来访问模型中的数据。

下面是各种model的组织示意图。我们利用此图来理解什么叫“一种表格形式的层次结构”。

List Model虽然是线性的列表,也有一个 Root Item(根节点),之下才是呈线性的一个个数据,而这些数据实际可以看作是一个只有一列的表格,但是它是有层次的,因为有一个根节点。Table Model 就比较容易理解,只是也存在一个根节点。Tree Model 主要面向层次数据,而每一层次都可以都很多列,因此也是一个带有层次的表格。

为了能够使得数据的显示同存储分离,我们引入模型索引(model index)的概念。通过索引,我们可以访问模型的特定元素的特定部分。视图和委托使用索引来请求所需要的数据。由此可以看出,只有模型自己需要知道如何获得数据,模型所管理的数据类型可以使用通用的方式进行定义。索引保存有创建的它的那个模型的指针,这使得同时操作多个模型成为可能。

1
QAbstractItemModel *model = index.model();

模型索引提供了所需要的信息的临时索引,可以用于通过模型取回或者修改数据。由于模型随时可能重新组织其内部的结构,因此模型索引很可能变成不可用的,此时,就不应该保存这些数据。如果你需要长期有效的数据片段,必须创建持久索引。持久索引保证其引用的数据及时更新。临时索引(也就是通常使用的索引)由QModelIndex类提供,持久索引则是QPersistentModelIndex类。

为了定位模型中的数据,我们需要三个属性:行号、列号以及父索引。

我们前面介绍过模型的基本形式:数据以二维表的形式进行存储。此时,一个数据可以由行号和列号进行定位。注意,我们仅仅是使用“二维表”这个名词,并不意味着模型内部真的是以二维数组的形式进行存储;所谓“行号”“列号”,也仅仅是为方便描述这种对应关系,并不真的是有行列之分。通过指定行号和列号,我们可以定位一个元素项,取出其信息。此时,我们获得的是一个索引对象:

1
2
3
4
5
6
QModelIndex index = model->index(row, column, ...);

QModelIndex QAbstractItemModel::index(int row,
int column,
const QModelIndex &parent=QModelIndex()) const

在一个简单的表格中,每一个项都可以由行号和列号确定。因此,我们只需提供两个参数即可获取到表格中的某一个数据项:

1
2
3
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(1, 1, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());

函数的最后一个参数始终是 QModelIndex()

在类似表格的视图中,比如列表和表格,行号和列号足以定位一个数据项。但是,对于树型结构,仅有两个参数就不足够了。这是因为树型结构是一个层次结构,而层次结构中每一个节点都有可能是另外一个表格。所以,每一个项需要指明其父节点。前面说过,在模型外部只能用过索引访问内部数据,因此,index()函数还需要一个 parent 参数:

1
QModelIndex index = model->index(row, column, parent);

图中,A 和 C 都是模型中的顶级项:

1
2
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());

A 还有自己的子项。那么,我们就应该使用下面的代码获取 B 的索引:

1
QModelIndex indexB = model->index(1, 0, indexA);

由此我们看到,如果只有行号和列号两个参数,B 的行号是 1,列号是 0,这同与 A 同级的行号是 1,列号是 0 的项相同,所以我们通过 parent 属性区别开来。

以上我们讨论了有关索引的定位。现在我们来看看模型的另外一个部分:数据角色。模型可以针对不同的组件(或者组件的不同部分,比如按钮的提示以及显示的文本等)提供不同的数据。例如,Qt::DisplayRole用于视图的文本显示。通常来说,数据项包含一系列不同的数据角色,这些角色定义在Qt::ItemDataRole枚举中。

我们可以通过指定索引以及角色来获得模型所提供的数据:

1
QVariant value = model->data(index, role);

通过为每一个角色提供恰当的数据,模型可以告诉视图和委托如何向用户显示内容。不同类型的视图可以选择忽略自己不需要的数据。当然,我们也可以添加我们所需要的额外数据。

总结一下:

  • 模型使用索引来提供给视图和委托有关数据项的位置的信息,这样做的好处是,模型之外的对象无需知道底层的数据存储方式;
  • 数据项通过行号、列号以及父项三个坐标进行定位;
  • 模型索引由模型在其它组件(视图和委托)请求时才会被创建;
  • 如果使用index()函数请求获得一个父项的可用索引,该索引会指向模型中这个父项下面的数据项。这个索引指向该项的一个子项;如果使用index()函数请求获得一个父项的不可用索引,该索引指向模型的最顶级项;
  • 角色用于区分数据项的不同类型的数据。

下面回到前面我们曾经见过的模型QFileSystemModel,看看如何从模型获取数据。

1
2
3
QFileSystemModel *model = new QFileSystemModel;
QModelIndex parentIndex = model->index(QDir::currentPath());
int numRows = model->rowCount(parentIndex);

在这个例子中,我们创建了QFileSystemModel的实例,使用QFileSystemModel重载的index()获取索引,然后使用rowCount()函数计算当前目录下有多少数据项(也就是行数)。前面一章中迷迷糊糊的代码,现在已经相当清楚了。

为简单起见,下面我们只关心模型第一列。我们遍历所有数据,取得第一列索引:

1
2
for (int row = 0; row < numRows; ++row) {
QModelIndex index = model->index(row, 0, parentIndex);

我们使用index()函数,第一个参数是每一行行号,第二个参数是 0,也就是第一列,第三个参数是parentIndex,也就是当前目录作为父项。我们可以使用模型的data()函数获取每一项的数据。注意,该函数返回值是QVariant,实际是一个字符串,因此我们直接转换成QString:

1
QString text = model->data(index, Qt::DisplayRole).toString();// 使用 text 数据

上面的代码片段显示了从模型获取数据的一些有用的函数:

  • 模型的数目信息可以通过rowCount()和columnCount()获得。这些函数需要制定父项;
  • 索引用于访问模型中的数据。我们需要利用行号、列号以及父项三个参数来获得该索引;
  • 当我们使用QModelIndex()创建一个空索引使用时,我们获得的就是模型中最顶级项;
  • 数据项包含了不同角色的数据。为获取特定角色的数据,必须指定这个角色。

  1. QStringListModel、QDirModel、QFileSystemModel等预定义model
  2. QStandardItemModel 可当作列表模型、表格模型、树模型来使用的通用模型
  3. QAbstractItemModel自定义模型

QStandardItemModel常用API

  • flags(const QModelIndex &index):返回被给模型索引index的标志

  • data(const QModelIndex &index, int role = Qt::DisplayRole):返回模型索引index的底层数据(一个模型索引包括某一元素的信息,包括行,列,以及数据),用于视图和委托访问数据

  • headerData(int selection, Qt::Orientation orientation, int role =Qt::DisplayRole):返回某部分对应方向上的表头 ,为Views提供显示在Views顶部(即最上方和最左边)的标识

  • rowCount(const QModelIndex &parent=QModelIndex()):返回被给的模型索引下有多少行,返回的是parent的孩子数。而不是整个行数。如果没有子元素,则返回0。

  • columnCount(const QModelIndex &parent=QModelIndex()):这个函数通常与给定的parent无关,所涉及的类有几列就返回几

  • setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole):用来设置模型索引index中存储的数据

  • setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::EditRole):

  • insertRows(int position, int rows, const QModelIndex &index = QModelIndex()):进行插入操作时,前后分别需要调用beginInsertRows()和endInsertRows()函数

委托(Delegate)

代理就是在视图组件上为编辑数据提供编辑器,如在表格组件中编辑一个单元格的数据时,缺省是使用一个 QLineEdit 编辑框。代理负责从数据模型获取相应的数据,然后显示在编辑器里,修改数据后,又将其保存到数据模型中。

QAbstractltemDelegate 是所有代理类的基类,作为抽象类,它不能直接使用。它的一个子类 QStyledltemDelegate,是 Qt 的视图组件缺省使用的代理类。

QStyledItemDelegate常用API

  • updateEditorGeometry:设置编辑器editor的尺寸和位置
  • painter: 绘制给定模型索引index所对应的项
  • setEditorData: 使用给定模型索引index所对应的模型项的数据来填充编辑器editor
  • setModelData:从编辑器editor中获取数据并设置为给定模型索引index所对应的模型项的数据
  • setHint: 返回委托需要的、显示或编辑给定模型索引index所代表的项的尺寸
  • updateEditorGeometry: 设置编辑器editor的尺寸和位置

实例

参考