第10章 数据处理

  1. 应用程序中往往要存储大量的数据,并对它们进行处理,然后可以通过各种形式显示给用户,用户需要时还可以对数据进行编辑。Qt中的模型/视图架构用来实现大量数据的存储、处理及显示。这种架构引入的功能分离思想为开发者定制项目的显示提供了高度的灵活性,而且还提供了一个标准的模型接口来允许大范围的数据源使用已经存在的项目视图。使用该架构可以将数据和界面进行分离,使得相同的数据在多个不同的视图中进行显示成为可能,而且还可以创建新的视图,而不需要改变底层的数据框架。
  2. 利用模型/视图架构可以轻松完成以数据为中心的程序开发,对于复杂的数据显示和处理,建议使用Qt Widgets编程;如果主要是进行数据的显示,则可以使用Qt Quick编程,配合动画、状态和过渡等相关类型,可以设计出流畅的数据展示界面。

10.1 Qt Widgets中的模型/视图架构

  1. 模型/视图架构包含3部分:
    1)模型(Model):是应用对象,用来表示数据,模型与数据源进行通信,为架构中的其他组件提供了接口。
    2)视图(View):是模型的用户界面,用来显示数据,视图从模型中获得模型索引(Model Index),模型索引用来表示数据项。
    3)委托(Delegate,也被称为代理):可以定制数据的渲染和编辑方式,在标准的视图中,委托渲染数据项,当编辑项目时,委托使用模型索引直接与模型进行通信。

10.1.1 模型类

10.1.1.1 QAbstractItemModel类

  1. 在模型/视图架构中,模型提供了一个标准的接口供视图和委托来访问数据。在Qt Widgets中,这个标准的接口使用QAbstractItemModel类来定义。无论数据项是怎样存储在何种底层数据结构中,QAbstractItemModel的子类都会以层次结构来表示数据,这个结构中包含了数据项表。视图按照这种约定来访问模型中的数据项,但是这不会影响数据的显示,视图可以使用任何形式将数据显示出来。当模型中的数据发生变化时,模型会通过信号和槽机制告知与其相关联的视图。
  2. QAbstractItemModel为数据提供了一个十分灵活的接口来处理各种视图,这些视图可以将数据表现为表格、列表和树等形式。然而,当要实现一个新的模型时,如果它基于列表或者表格的数据结构,那么可以使用QAbstractListModel和QAbstractTableModel类,因为它们为一些常见的功能提供了默认的实现。这些类都可以被子类化来提供模型,从而支持特殊类型的列表和表格。

10.1.1.2 Qt Widgets中现成的模型

  1. Qt Widgets中也提供了一些现成的模型来处理数据项:
    1)QStringListModel用来存储一个简单的QString项目列表;
    2)QStandardItemModel管理复杂的树型结构数据项,每一个数据项可以包含任意的数据;
    3)QFileSystemModel提供了一个保持文件系统信息的模型,它并不包含任何的数据项目,而是代表了本地文件系统中的文件和目录。可以和QListView或者QTreeView一起使用来显示一个目录中内容;
    4)QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel用来访问数据库。
cpp 复制代码
QFileSystemModel model;
    // 指定要监视的目录
model.setRootPath(QDir::currentPath());
    // 创建树型视图
QTreeView tree;
    // 为视图指定模型
tree.setModel(&model);
    // 指定根索引
tree.setRootIndex(model.index(QDir::currentPath()));
    // 创建列表视图
 QListView list;   
 list.setModel(&model);
 list.setRootIndex(model.index(QDir::currentPath()));
 tree.show();    
 list.show();
  1. 如果Qt提供的这些标准模型无法满足需要,还可以子类化QAbstractItemModel、QAbstractListModel或者QAbstractTableModel来创建自定义的模型。

10.1.1.3 常见的3种模型

常见的3种模型分别是列表模型(List Model)、表格模型(Table Model)和树模型(Tree Model),它们的示意图如下:

10.1.1.4 模型索引

  1. 为了确保数据的表示与数据的获取相分离,Qt引入了模型索引的概念。每一块可以通过模型获取的数据都使用一个模型索引来表示,视图和委托使用这些索引来请求数据项并显示。这样,只有模型需要知道怎样获取数据,被模型管理的数据类型可以广泛的被定义。模型索引包含一个指针,指向创建它们的模型,当使用多个模型时可以避免混淆。
  2. 模型索引由QModelIndex类提供,它是对一块数据的临时引用,可以用来检索或者修改模型中的数据。因为模型随时可能对内部的结构进行重新组织,这样模型索引可能失效,所以不需要也不应该存储模型索引。如果需要对一块数据进行长时间的引用,必须使用QPersistentModelIndex创建模型索引。如果要获得一个数据项的模型索引,必须指定模型的3个属性:行号、列号和父项的模型索引,其中,row、column和parent分别代表了这3个属性。
cpp 复制代码
QModelIndex index = model->index(row, column, parent);

10.1.1.5 行和列

  1. 在最基本的形式中,一个模型可以通过把它看做一个简单的表格来访问,这时每个数据项可以使用行号和列号来定位。但这并不意味着在底层的数据块是存储在数组结构中的,使用行号和列号只是一种约定,以确保各组件间可以相互通信。
  2. 行号和列号都是从0开始的,列表模型和表格模型的所有数据项都是以根项(Root item)为父项的,这些数据项都可以被称为顶层数据项(Top level item),在获取这些数据项的索引时,父项的模型索引可以用QModelIndex()表示。例如Table Model中的A、B、C这3项的模型索引可以用如下代码获取:
cpp 复制代码
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(1, 1, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());

10.1.1.6 父项

类似于表格的接口对于在使用表格或者列表时是非常理想的,但是像树视图一样的结构需要模型提供一个更加灵活的接口,因为每一个数据项都可能成为其他数据项的父项,一个树视图中的顶层数据项也可能包含其他的数据项列表。当为模型项请求一个索引时,必须提供该数据项父项的一些信息。顶层数据项可以使用QModelIndex()作为父项索引,但是在树模型中,如果一个数据项不是顶层数据项,那么就要指定它的父项索引。例如Tree Model中的A、B、C这3项的模型索引可以使用如下代码获得:

cpp 复制代码
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexC = model->index(2, 0, QModelIndex());
QModelIndex indexB = model->index(1, 0, indexA);

10.1.1.7 项角色

  1. 模型中的数据项可以作为各种角色在其他组件中使用,允许为不同的情况提供不同类型的数据。例如,Qt::DisplayRole用于访问一个字符串,所以可以作为文本显示在视图中。通常情况下,数据项包含了一些不同角色的数据,这些标准的角色由枚举类型Qt::ItemDataRole来定义,常用的角色包括Qt::DisplayRole、Qt::EditRole和Qt::DecorationRole等。

  2. 通过为每个角色提供适当的项目数据,模型可以为视图和委托提供提示,告知数据应该怎样展示给用户。角色指出了从模型中引用哪种类型的数据,视图可以使用不同的方式来显示不同的角色。不同类型的视图也可以自由的解析或者忽略这些角色信息。

  3. 可以通过向模型指定相关数据项对应的模型索引以及特定的角色来获取需要的类型的数据,例如:

cpp 复制代码
QVariant value = model->data(index, role);

10.1.1.8 示例程序

cpp 复制代码
// 创建标准项模型
QStandardItemModel model;
// 获取模型的根项(Root Item),根项是不可见的
QStandardItem *parentItem = model.invisibleRootItem();
// 创建标准项item0,并设置显示文本,图标和工具提示
QStandardItem *item0 = new QStandardItem;
item0->setText("A");
QPixmap pixmap0(50, 50);
pixmap0.fill("red");
item0->setIcon(QIcon(pixmap0));
item0->setToolTip("indexA");
// 将创建的标准项作为根项的子项
parentItem->appendRow(item0);          
// 将创建的标准项作为新的父项
parentItem = item0;
// 创建新的标准项,它将作为item0的子项
QStandardItem *item1 = new QStandardItem;
item1->setText("B");
QPixmap pixmap1(50,50);
pixmap1.fill("blue");
item1->setIcon(QIcon(pixmap1));
item1->setToolTip("indexB");
parentItem->appendRow(item1);
// 创建新的标准项,这里使用了另一种方法来设置文本、图标和工具提示
QStandardItem *item2 = new QStandardItem;
QPixmap pixmap2(50,50);
pixmap2.fill("green");
item2->setData("C", Qt::EditRole);
item2->setData("indexC", Qt::ToolTipRole);
item2->setData(QIcon(pixmap2), Qt::DecorationRole);
parentItem->appendRow(item2);
... ...
 // 在树视图中显示模型
QTreeView view;
view.setModel(&model);
view.show();    
 // 获取item0的索引并输出item0的子项数目,然后输出了item1的显示文本和工具提示
QModelIndex indexA = model.index(0, 0, QModelIndex());
qDebug() << "indexA row count: " << model.rowCount(indexA);
QModelIndex indexB = model.index(0, 0, indexA);
qDebug() << "indexB text: " << model.data(indexB, Qt::EditRole).toString();
qDebug() << "indexB toolTip: " << model.data(indexB, Qt::ToolTipRole).toString();

10.1.2 视图类

  1. 在模型/视图架构中,视图包含了模型中的数据项,并将它们呈现给用户,而数据的表示方法可能与底层用于存储数据项的数据结构完全不同。这种内容与表现的分离之所以能够实现,是因为使用了QAbstractItemModel提供的一个标准模型接口,还有QAbstractItemView提供的一个标准视图接口,以及使用了模型索引提供了一种通用的方法来表示数据。视图通常管理从模型获取的数据的整体布局,它们可以自己渲染独立的数据项,也可以使用委托来处理渲染和编辑。
  2. Qt提供了几种不同类型的视图:QListView将数据项显示为一个列表;QTableView将模型中的数据显示在一个表格中;QTreeView将模型的数据项显示在具有层次的列表中。这些类都是基于QAbstractItemView抽象基类的。这些类可以直接使用,也可以被子类化来提供定制的视图。
  3. 对于一些视图,例如QTableView和QTreeView,在显示项目的同时还可以显示头部。这是通过QHeaderView类实现的,它们使用QAbstractItemModel::headerData()函数从模型中获取数据,然后一般使用一个标签来显示头部信息。可以通过子类化QHeaderView类来设置标签的显示。
  4. 除了呈现数据,视图还处理项目间的导航,以及项目选择的一些功能。视图也实现了一些基本的用户接口特性,比如上下文菜单和拖放等。视图可以为项目提供默认的编辑实现,当然也可以和委托一起来提供一个自定义的编辑器。
  5. 视图中的项目选择的行为和模式通过如下枚举类型进行设置:
    1)选择行为QAbstractItemView::SelectionBehavior

2)选择模式QAbstractItemView::SelectionMode

  1. 在模型/视图架构中对项目的选择提供了非常方便的处理方法。在视图中被选择的项目的信息存储在一个QItemSelectionModel实例中,这样被选择的项目的模型索引便保持在一个独立的模型中,与所有的视图都是独立的。当在一个模型上设置多个视图时,就可以实现在多个视图之间共享选择。
  2. 选择由选择范围指定,只需要记录每一个选择范围开始和结束的模型索引即可,非连续的选择可以使用多个选择范围来描述。选择可以看作是在选择模型中保存的一个模型索引集合,最近的项目选择被称为当前选择。
  3. 在视图中,总是有一个当前项目和一个被选择的项目,它们两者是两个独立的状态。在同一时间,一个项目可以既是当前项目,同时也是被选择的项目。视图负责确保总是有一个项目作为当前项目来实现键盘导航。
  4. 当操作选择时,可以将QItemSelectionModel看做是一个项目模型中所有项目的选择状态的一个记录。一旦设置了一个选择模型,所有的项目集合都可以被选择,取消选择,或者切换选择状态,而不需要知道哪一个项目已经被选择了。
  5. 标准的视图类中提供了默认的选择模型,可以在大多数的应用中直接使用。属于一个视图的选择模型可以使用这个视图的selectionModel()函数获得,而且还可以在多个视图之间使用setSelectionModel()函数来共享该选择模型,所以一般不需要重新构建一个选择模型,例如:
cpp 复制代码
tableView = new QTableView;
tableView->setModel(model);
// 获取视图的项目选择模型
QItemSelectionModel *selectionModel = tableView->selectionModel();
// 定义左上角和右下角的索引,然后使用这两个索引创建选择
QModelIndex topLeft;
QModelIndex bottomRight;
topLeft = model->index(1, 1, QModelIndex());
bottomRight = model->index(5, 2, QModelIndex());
QItemSelection selection(topLeft, bottomRight);
// 使用指定的选择模式来选择项目
selectionModel->select(selection, QItemSelectionModel::Select);  
     
QTableView *tableView2;
tableView2 = new QTableView;
tableView2->setWindowTitle("tableView2");
tableView2->resize(400, 300);
tableView2->setModel(model);
tableView2->setSelectionModel(selectionModel);
tableView2->show();

10.1.3 委托类

10.1.3.1 QAbstractItemDelegate

  1. 一般的,视图用来将模型中的数据展示给用户,也用来处理用户的输入。为了获得更高的灵活性,交互可以由委托来执行。这些委托组件提供了输入功能,而且也负责渲染一些视图中的个别项目。控制委托的标准接口在QAbstractItemDelegate类中定义。
  2. QAbstractItemDelegate是委托的抽象基类,包含QItemDelegate和QStyledItemDelegate两个子类。从Qt 4.4开始,默认的委托实现由QStyledItemDelegate类提供。委托通过实现paint()和sizeHint()函数来使它们可以渲染自身的内容。然而,简单的基于部件的委托可以通过子类化QStyledItemDelegate来实现,而不需要使用QAbstractItemDelegate,这样可以使用这些函数的默认实现。委托的编辑器可以通过两种方式来实现,一种是使用部件来管理编辑过程,另一种是直接处理事件。
  3. Qt中的标准视图都使用QStyledItemDelegate的实例来提供编辑功能,这种委托接口的默认实现为QListView、QTableView和QTreeView等标准视图的每一个项目提供了普通风格的渲染。可以使用itemDelegate()函数获取一个视图中使用的委托,使用setItemDelegate()函数可以为一个视图安装一个自定义委托。

10.1.3.2 自定义委托

  1. 这里的委托使用了QSpinBox来提供编辑功能,主要用于显示整数的模型。向项目中添加新的C++类,类名为SpinBoxDelegate,基类设置为QStyledItemDelegate。完成后将spinboxdelegate.h文件内容更改如下:
cpp 复制代码
class SpinBoxDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    SpinBoxDelegate(QObject *parent = nullptr);
    QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
                          const QModelIndex &index) const override;
    void setEditorData(QWidget *editor, const QModelIndex &index) const override;
    void setModelData(QWidget *editor, QAbstractItemModel *model,
                      const QModelIndex &index) const override;
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option,
                      const QModelIndex &index) const override;
};  
  1. 下面到spinboxdelegate.cpp文件中,添加这几个函数的定义。当视图需要一个编辑器时,它会告知委托来为被修改的项目提供一个编辑器部件。这里的createEditor()函数为委托设置一个合适的部件提供了所需要的一切。
cpp 复制代码
// 创建编辑器
QWidget *SpinBoxDelegate::createEditor(QWidget *parent,
                                       const QStyleOptionViewItem &/* option */,
                                       const QModelIndex &/* index */) const
{
    QSpinBox *editor = new QSpinBox(parent);
    editor->setFrame(false);
    editor->setMinimum(0);
    editor->setMaximum(100);
    return editor;
}  
  1. 委托必须将模型中的数据复制到编辑器中,需要为模型中不同类型的数据提供不同的编辑器,要在访问部件的成员函数以前将它转换为合适的类型。
cpp 复制代码
// 为编辑器设置数据
void SpinBoxDelegate::setEditorData(QWidget *editor,
                                    const QModelIndex &index) const
{
    int value = index.model()->data(index, Qt::EditRole).toInt();
    QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
    spinBox->setValue(value);
}
  1. 当用户完成了对QSpinBox部件中数据的编辑,视图会通过调用setModelData()函数来告知委托将编辑好的数据存储到模型中。这里调用了interpretText()函数来确保获得的是QSpinBox中最近更新的数值。
cpp 复制代码
// 将数据写入到模型
void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
                                   const QModelIndex &index) const
{
    QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
    spinBox->interpretText();
    int value = spinBox->value();
    model->setData(index, value, Qt::EditRole);
}
  1. 委托有责任来管理编辑器的几何布局,必须在创建编辑器以及视图中项目的大小或位置改变时设置它的几何布局,视图使用QStyleOptionViewItem对象提供了所有需要的几何布局信息。
cpp 复制代码
// 更新编辑器几何布局
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
                                           const QStyleOptionViewItem &option, 
                                           const QModelIndex &/* index */) const
{
    editor->setGeometry(option.rect);
}
  1. 到mainwindow.cpp文件中使用:
cpp 复制代码
SpinBoxDelegate *delegate = new SpinBoxDelegate(this);
tableView->setItemDelegate(delegate);
  1. 编辑完成后,委托应该为其他组件提供提示,告知它们编辑操作的结果,提供提示也有利于后续的编辑操作。这个可以通过在发射colseEditor()信号时使用合适的提示来实现,它们会被在构造编辑器时安装的默认的QStyledItemDelegate事件过滤器捕获。可以通过调整编辑器的行为来使得它更加友好。
  2. 对于QStyledItemDelegate提供的默认事件过滤器,如果用户在SpinBox编辑器中按下回车键,那么委托就会向模型提交数值然后关闭编辑器。可以通过在SpinBox上安装自己的事件过滤器来改变这个行为,并提供编辑提示来迎合自己的需要。

10.1.4 项目视图的便捷类

10.1.4.1 3个便捷类

  1. 从Qt 4开始引进了一些标准部件来提供经典的基于项的容器部件,它们底层是通过模型/视图框架实现的。这些部件分别是:
    1)QListWidget提供了一个项目列表;
    2)QTreeWidget显示了一个多层次的树结构;
    3)QTableWidget提供了一个以项目作为单元的表格。
  2. 它们都继承了QAbstractItemView类的行为。这些类之所以被称为便捷类,是因为它们使用起来比较简单,适合于少量的数据的存储和显示。因为它们没有将视图和模型进行分离,所以没有视图类灵活,不能和任意的模型一起使用,一般建议使用模型/视图的方式来处理数据。
  3. 对于这3个便捷类,它们都使用了相同的接口提供了一些基于项的特色功能。例如,有时候在项目视图部件中需要隐藏一些项目而不是删除它们,可以使用QListWidgetItem类和QTreeWidgetItem类提供的setHidden()函数;判断一个项目是否隐藏,可以使用相应的isHidden()函数;可以使用三个便捷类的selectedItems()函数来获取选择的项目,它会返回一个相关项目的列表;还可以使用findItems()函数来进行项目的查找,它也会返回一个相关项目的列表。

10.1.4.2 QListWidget

cpp 复制代码
QListWidget listWidget;
// 一种添加项目的简便方法
new QListWidgetItem("a", &listWidget);
// 添加项目的另一种方法,这样还可以进行各种设置
QListWidgetItem *listWidgetItem = new QListWidgetItem;
listWidgetItem->setText("b");
listWidgetItem->setIcon(QIcon("../listwidget/yafeilinux.png"));
listWidgetItem->setToolTip("this is b!");
listWidget.insertItem(1, listWidgetItem);
// 设置排序为倒序
listWidget.sortItems(Qt::DescendingOrder);
// 显示列表部件
listWidget.show();
  1. 单层的项目列表一般使用一个QListWidget和一些QListWidgetItem来显示,一个列表部件可以像一般的窗口部件那样进行创建。可以在创建QListWidgetItem时将它直接添加到已经创建的列表部件中,也可以稍后使用QListWidget类的insertItem()函数来添加。
  2. 列表中的每一个项目都可以显示一个文本标签和一个图标,还可以为其设置工具提示、状态提示和"What's This?"提示。
  3. 默认的,列表中的项目会根据它们添加的顺序进行排序,也可以使用sortItems()函数对项目进行排序,比如程序中使用的Qt::DescendingOrder是按字母降序排序,还有一个Qt::AscendingOrder是按字母升序进行排序。

10.1.4.3 QTreeWidget

cpp 复制代码
QTreeWidget treeWidget;
// 必须设置列数
treeWidget.setColumnCount(2);
// 设置标头
QStringList headers;
headers << "name" << "year";
treeWidget.setHeaderLabels(headers);
// 添加项目
QTreeWidgetItem *grade1 = new QTreeWidgetItem(&treeWidget);
grade1->setText(0, "Grade1");
QTreeWidgetItem *student = new QTreeWidgetItem(grade1);
student->setText(0, "Tom");
student->setText(1, "1986");
QTreeWidgetItem *grade2 = new QTreeWidgetItem(&treeWidget, grade1);
grade2->setText(0, "Grade2");
treeWidget.show();
  1. 树或者项目的层次列表由QTreeWidget和QTreeWidgetItem类提供,在树部件中的每一个项目都可以有它自己的子项目,而且可以显示多列的信息。在向树部件中添加项目以前,必须先使用setColumnCount()函数设置列的个数,比如程序中设置了两列,然后还为这两列提供了标头。树部件中的顶层项目使用树部件作为父部件来进行创建,它们可以使用任意的顺序被插入,也可以构建项目时指定它的前一个项目,比如程序中创建grade2时就指定了grade1为它的前一个项目。
  2. 树部件对于顶层项目和更深层次的项目的处理略有不同。例如可以使用树部件的takeTopLevelItem()函数来删除顶层项目,但是其他层次的项目就要调用它们父项目的takeChild()函数来删除;在树部件中插入顶层项目可以使用insertTopLevelItem()函数,但插入其他层次的项目就要使用其父项目的insertChild()函数。在顶层和其他层之间移动项目是很容易的,只需要检查该项目是否为顶层项目,这个可以使用parent()函数获得。可以使用下面的代码来删除当前的项目:
cpp 复制代码
// 先获取当前项目的父项目
QTreeWidgetItem *parent = currentItem->parent();
int index;
// 当前项目有父项目,则使用其父项目删除当前项目,否则使用树部件删除当前项目
if (parent) { 
    index = parent->indexOfChild(treeWidget->currentItem());
    delete parent->takeChild(index);
} else {
    index = treeWidget->indexOfTopLevelItem(treeWidget->currentItem());
    delete treeWidget->takeTopLevelItem(index);
}

可以使用相同的方法在当前项目之后添加新的项目:

cpp 复制代码
QTreeWidgetItem *parent = currentItem->parent();
QTreeWidgetItem *newItem;
if (parent)
    newItem = new QTreeWidgetItem(parent, treeWidget->currentItem());
else
    newItem = new QTreeWidgetItem(treeWidget, treeWidget->currentItem());

10.1.4.4 QTableWidget

cpp 复制代码
// 创建表格部件,同时指定行数和列数
QTableWidget tableWidget(3, 2);
// 创建表格项目,并插入到指定单元
QTableWidgetItem *tableWidgetItem = new QTableWidgetItem("qt");
tableWidget.setItem(1, 1, tableWidgetItem);
// 创建表格项目,并将它们作为标头
QTableWidgetItem *headerV = new QTableWidgetItem("first");
tableWidget.setVerticalHeaderItem(0,headerV);
QTableWidgetItem *headerH = new QTableWidgetItem("ID");
tableWidget.setHorizontalHeaderItem(0,headerH);
tableWidget.show();

项目表格使用QTableWidget和QTableWidgetItem来构建,它提供了一个包含标头和项目的可滚动表格部件。表格一般在构造时就指定它的行数和列数,项目可以在表格外先构建,然后再添加到表格中指定的位置,而且表格项目还可以作为水平或者垂直标头。

10.1.5 在项目视图中启用拖放

  1. 模型/视图框架完全支持Qt的拖放应用,在列表、表格和树中的项目可以在视图中被拖拽,数据可以作为MIME编码的数据被导入和导出。
  2. 标准视图可以自动支持内部的拖放,这样可以用来改变项目的排列顺序。默认的,视图的拖放功能并没用被启用,如果要进行项目的拖动,需要进行一些属性的设置。
  3. 如果要在一个新的模型中启用拖放功能,那么还要重新实现一些函数。

10.1.5.1 在便捷类中启用拖放

  1. 在QListWidget、QTableWidget和QTreeWidget中的每一种类型的项目都默认配置了一组不同的标志。例如,每一个QListWidgetItem和QTreeWidgetItem被初始化为可用的、可检查的、可选择的,也可以用做拖放操作的源;而每一个QTableWidgetItem可以被编辑和用做拖放操作的目标。尽管所有的标准项目都有一个或者两个标志来设置拖放,但是,一般还是需要在视图中设置一些属性来使它启用对拖放操作的内建支持:
    1)启用项目拖拽,要将视图的dragEnable属性设置为true;
    2)要允许用户将内部或者外部的项目放入视图中,需要设置视图的viewport()的acceptDrops属性为true;
    3)要显示现在用户拖拽的项目将要被放置的位置,需要设置showDropIndicator属性。
cpp 复制代码
// 设置选择模式为单选
listWidget.setSelectionMode(QAbstractItemView::SingleSelection);
// 启用拖动
listWidget.setDragEnabled(true);
// 设置接受拖放
listWidget.viewport()->setAcceptDrops(true);
// 设置显示将要被放置的位置
listWidget.setDropIndicatorShown(true);
// 设置拖放模式为移动项目,如果不设置,默认为复制项目
listWidget.setDragDropMode(QAbstractItemView::InternalMove);

10.1.5.2 在模型/视图类中启用拖放

  1. 在视图中启用拖放功能与在便捷类中的设置是相似的:
cpp 复制代码
listView.setSelectionMode(QAbstractItemView::ExtendedSelection);
listView.setDragEnabled(true);
listView.setAcceptDrops(true);
listView.setDropIndicatorShown(true);
  1. 因为视图中显示的数据是由模型控制的,所以也要为使用的模型提供拖放操作的支持。这需要重新实现一些必要的函数。
cpp 复制代码
Qt::DropActions supportedDropActions() const override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action,
                           int row, int column, const QModelIndex &parent) override;
cpp 复制代码
// 设置支持放入动作
Qt::DropActions StringListModel::supportedDropActions() const
{
    return Qt::CopyAction | Qt::MoveAction;
}

// 设置在拖放操作中导出条目的数据编码类型
QStringList StringListModel::mimeTypes() const
{
    QStringList types;
    // "application/vnd.text.list"是自定义的类型,在后面的函数中要保持一致
    types << "application/vnd.text.list";
    return types;
}

// 将拖放的数据放入QMimeData中
QMimeData * StringListModel::mimeData(const QModelIndexList &indexes) const
{
    QMimeData *mimeData = new QMimeData();
    QByteArray encodedData;
    QDataStream stream(&encodedData, QIODevice::WriteOnly);
    foreach (const QModelIndex &index, indexes) {
        if (index.isValid()) {
            QString text = data(index, Qt::DisplayRole).toString();
            stream << text;
        }
    }
    // 将数据放入QMimeData中
    mimeData->setData("application/vnd.text.list", encodedData);
    return mimeData;
}

// 将拖放的数据放入模型中
bool StringListModel::dropMimeData(const QMimeData *data, int row, int column, const QModelIndex &parent)
{
    // 如果放入动作是Qt::IgnoreAction,那么返回true
    if (action == Qt::IgnoreAction)
        return true;
    // 如果数据的格式不是指定的格式,那么返回false
    if (!data->hasFormat("application/vnd.text.list"))
        return false;
    // 因为这里是列表,只用一列,所以列大于0时返回false
    if (column > 0)
        return false;
    // 设置开始插入的行
    int beginRow;
    if (row != -1)
        beginRow = row;
    else if (parent.isValid())
        beginRow = parent.row();
    else
        beginRow = rowCount(QModelIndex());

// 将数据从QMimeData中读取出来,然后插入到模型中
    QByteArray encodedData = data->data("application/vnd.text.list");
    QDataStream stream(&encodedData, QIODevice::ReadOnly);
    QStringList newItems;
    int rows = 0;
    while (!stream.atEnd()) {
        QString text;
        stream >> text;
        newItems << text;
        ++rows;
    }
    insertRows(beginRow, rows, QModelIndex());
    foreach (const QString &text, newItems) {
        QModelIndex idx = index(beginRow, 0, QModelIndex());
        setData(idx, text);
        beginRow++;
    }
    return true;
}
  1. 模型通过flags()函数提供合适的标志来向视图表明哪些项目可以被拖拽,哪些项目可以接受放入。
cpp 复制代码
Qt::ItemFlags StringListModel::flags(const QModelIndex &index) const
{
    // 如果该索引无效,那么只支持放入操作
    if (!index.isValid())
        return Qt::ItemIsEnabled | Qt::ItemIsDropEnabled;

    // 如果该索引有效,那么既支持拖拽操作,也支持放入操作
    return QAbstractItemModel::flags(index) | Qt::ItemIsEditable 
            | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
}

10.1.6 代理模型

代理模型可以将一个模型中的数据进行排序或者过滤,然后提供给视图进行显示。Qt中提供了QSortFilterProxyModel作为标准的代理模型来完成模型中数据的排序和过滤,如果要使用一个代理模型,只需要为其设置源模型,然后在视图中使用该代理模型即可。例如:

cpp 复制代码
QSortFilterProxyModel *filterModel;
// 初始化:
QStringList list;
list << "yafei" << "yafeilinux" << "Qt" << "Qt Creator";
QStringListModel *listModel = new QStringListModel(list, this);
filterModel = new QSortFilterProxyModel(this);
// 为代理模型添加源模型
filterModel->setSourceModel(listModel);
// 在视图中使用代理模型
ui->listView->setModel(filterModel);
cpp 复制代码
void MainWindow::on_pushButton_clicked()
{
    QRegularExpression rx(ui->lineEdit->text());
    filterModel->setFilterRegularExpression(rx);
}

这里将行编辑器中的文本作为正则表达式的内容,然后使用该正则表达式作为代理模型的过滤器。这样每当条件改变时,都会自动更新视图的显示。

10.1.7 数据-窗口映射器

数据-窗口映射器QDataWidgetMapper类在数据模型的一个区域和一个窗口部件间提供了一个映射,这样就可以实现在一个窗口部件上显示和编辑一个模型中的一行数据。例如:设计如下界面,然后通过按钮来浏览数据模型中的内容。

cpp 复制代码
QDataWidgetMapper *mapper;
// 初始化:
QStandardItemModel *model = new QStandardItemModel(3, 2, this);
model->setItem(0, 0, new QStandardItem("xiaoming"));
model->setItem(0, 1, new QStandardItem("90"));
model->setItem(1, 0, new QStandardItem("xiaogang"));
model->setItem(1, 1, new QStandardItem("75"));
model->setItem(2, 0, new QStandardItem("xiaohong"));
model->setItem(2, 1, new QStandardItem("80"));
mapper = new QDataWidgetMapper(this);
// 设置模型
mapper->setModel(model);
// 设置窗口部件和模型中列的映射
mapper->addMapping(ui->lineEdit, 0);
mapper->addMapping(ui->lineEdit_2, 1);
// 显示模型中的第一行
mapper->toFirst();
cpp 复制代码
// 上一条按钮
void MainWindow::on_pushButton_clicked()
{
    mapper->toPrevious();
}
// 下一条按钮
void MainWindow::on_pushButton_2_clicked()
{
    mapper->toNext();
}

这里分别使用了toPrevious()函数和toNext()函数来显示模型中上一行和下一行的数据。还有一个toLast()函数可以显示模型中最后一行的数据。

10.2 Qt Widgets中的数据库应用

10.2.1 数据库简介

  1. Qt SQL模块提供了对数据库的支持,该模块中的众多类基本上可以分为3层:

    1)驱动层:为具体的数据库和SQL接口层之间提供了底层的桥梁;
    2)SQL接口层:提供了对数据库的访问,其中的QSqlDatabase类用来创建连接,QSqlQuery类可以使用SQL语句来实现与数据库交互,其他几个类对该层提供了支持;
    3)用户接口层:实现了将数据库中的数据链接到窗口部件上,这些类是使用模型/视图框架实现的,它们是更高层次的抽象,即便不熟悉SQL也可以操作数据库。
  2. 要使用Qt SQL模块中的这些类,需要在项目文件(.pro文件)中添加一行代码:QT += sql

10.2.2 SQL数据库驱动

  1. Qt SQL模块使用数据库驱动插件来和不同的数据库接口进行通信。由于Qt SQL模块的接口是独立于数据库的,所以所有数据库特定的代码都包含在了这些驱动中。Qt默认支持一些驱动,也可以添加其他驱动,Qt中包含的驱动如下表所列:
  2. SQLite数据库是一款轻型的文件型数据库,无需数据库服务器,主要应用于嵌入式领域,支持跨平台,而且Qt对它提供了很好的默认支持。
  3. 遍历输出驱动列表:
cpp 复制代码
#include <QApplication>
#include <QSqlDatabase>
#include <QStringList>
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug() << "Available drivers:";
    QStringList drivers = QSqlDatabase::drivers();
    foreach(QString driver, drivers)
        qDebug() << driver;
    return a.exec();
}

这里使用了QSqlDatabase类的静态函数drivers()获取了可用的驱动列表,然后将它们遍历输出。运行程序,在应用程序输出栏可以看到输出的结果为:QSQLITE、QODBC和QPSQL,表明现在仅支持这3个驱动。其实,也可以在Qt安装目录下的plugins/sqldrivers文件夹中看到所有的驱动插件文件。

10.2.3 创建数据库连接

  1. 要想使用QSqlQuery或者QSqlQueryModel访问数据库,那么先要创建并打开一个或者多个数据库连接。数据库连接使用连接名来定义,而不是使用数据库名,可以向相同的数据库创建多个连接。QSqlDatabase也支持默认连接的概念,就是一个没有命名的连接。在使用QSqlQuery或者QSqlQueryModel的成员函数时需要指定一个连接名作为参数,如果没有指定,那么就会使用默认连接。如果在应用程序中只需要有一个数据库连接,那么使用默认连接是很方便的。
cpp 复制代码
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
db.setHostName("bigblue");
db.setDatabaseName("flightdb");
db.setUserName("acarlson");
db.setPassword("1uTbSbAs");
bool ok = db.open();
  1. 同时创建多个连接:
    下面的示例代码中创建了两个名为first和second的连接:
cpp 复制代码
QSqlDatabase firstDB = QSqlDatabase::addDatabase("QMYSQL", "first");
QSqlDatabase secondDB = QSqlDatabase::addDatabase("QMYSQL", "second");
  1. 创建完连接后,可以在任何地方使用QSqlDatabase::database()静态函数通过连接名称获取指向数据库连接的指针,如果调用该函数时没有指明连接名称,那么会返回默认连接,例如:
cpp 复制代码
QSqlDatabase defaultDB = QSqlDatabase::database();
QSqlDatabase firstDB = QSqlDatabase::database("first");
QSqlDatabase secondDB = QSqlDatabase::database("second");
  1. 要移除一个数据库连接,需要先使用QSqlDatabase::close()关闭数据库,然后使用静态函数QSqlDatabase::removeDatabase()移除该连接。
  2. 添加新的C++头文件connection.h,这个头文件中添加了一个建立连接的函数,使用这个头文件的目的主要是为了简化主函数中的内容。
cpp 复制代码
#ifndef CONNECTION_H
#define CONNECTION_H

#include <QMessageBox>
#include <QSqlDatabase>
#include <QSqlQuery>

static bool createConnection()
{
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName(":memory:");
    if (!db.open()) {
        QMessageBox::critical(0, "Cannot open database",
            "Unable to establish a database connection.", QMessageBox::Cancel);
        return false;
    }
    QSqlQuery query;
    query.exec("create table student (id int primary key, "
               "name varchar(20))");
    query.exec("insert into student values(0, 'LiMing')");
    query.exec("insert into student values(1, 'LiuTao')");
    query.exec("insert into student values(2, 'WangHong')");
    return true;
}

#endif // CONNECTION_H

这里先创建了一个SQLite数据库的默认连接,设置数据库名称时使用了":memory:",表明这个是建立在内存中的数据库(SQLite数据库支持内存中的临时数据库),也就是说该数据库只在程序运行期间有效,等程序运行结束时就会将其销毁。当然,也可以将其改为一个具体的数据库名称,比如my.db,这样就会在项目生成目录中创建该数据库文件。

  1. 下面调用createConnection()函数来创建数据库连接,使用QSqlQuery查询整张表并将其所有内容进行输出。
cpp 复制代码
int main(int argc, char *argv[])
{
		QApplication a(argc, argv);
		if (!createConnection()) return 1;
		// 使用QSqlQuery查询整张表
		QSqlQuery query;
		query.exec("select * from student");
		while(query.next()) {
   		qDebug() << query.value(0).toInt() << query.value(1).toString();
		}
		return a.exec();
}

10.2.4 执行SQL语句

  1. 执行一个查询。QSqlQuery类提供了一个接口,用于执行SQL语句和浏览查询的结果集。要执行一个SQL语句,只需要简单的创建一个QSqlQuery对象,然后调用QSqlQuery::exec()函数即可,例如:
cpp 复制代码
QSqlQuery query;
query.exec("select * from student");
  1. 浏览结果集。QSqlQuery提供了对结果集的访问,可以一次访问一条记录。当执行完exec()函数后,QSqlQuery的内部指针会位于第一条记录前面的位置。必须调用一次QSqlQuery::next()函数来使其前进到第一条记录,然后可以重复使用next()函数来访问其他的记录,直到该函数的返回值为false,例如可以使用以下代码来遍历一个结果集:
cpp 复制代码
while(query.next()) {
		qDebug() << query.value(0).toInt() 
      	    		<< query.value(1).toString();
}

QSqlQuery::value()函数可以返回当前记录的一个字段值。比如value(0)就是第一个字段的值,各个字段从0开始编号。该函数返回一个QVariant,不同的数据库类型会自动映射为Qt中最接近的相应类型,这里的toInt()和toString()就是将QVariant转换为int和QString类型。另外,QSqlQuery类中提供了多个函数来实现在结果集中进行定位,next()函数定位到下一条记录,previous()定位到前一条记录,first()定位到第一条记录,last()定位到最后一条记录,seek(n)定位到第n条记录。当前行的索引可以使用at()返回;record()函数可以返回当前指向的记录;如果数据库支持,那么可以使用size()来返回结果集中的总行数。

  1. 插入一条记录:
cpp 复制代码
query.exec("insert into student (id, name) values (100, 'ChenYun')");

如果想在同一时间插入多条记录,那么一个有效的方法就是将查询语句和真实的值分离,可以使用占位符来完成。Qt支持两种占位符:名称绑定和位置绑定。

1)名称绑定:

cpp 复制代码
query.prepare("insert into student (id, name) values (:id, :name)");
int idValue = 100;
QString nameValue = "ChenYun";
query.bindValue(":id", idValue);
query.bindValue(":name", nameValue);
query.exec();

2)位置绑定:

cpp 复制代码
query.prepare("insert into student (id, name) values (?, ?)");
int idValue = 100;
QString nameValue = "ChenYun";
query.addBindValue(idValue);
query.addBindValue(nameValue);
query.exec();

当要插入多条记录时,只需要调用QSqlQuery::prepare()一次,然后使用多次bindValue()或者addBindValue()函数来绑定需要的数据,最后调用一次exec()函数就可以了。其实,进行多条数据插入时,还可以使用批处理进行:

cpp 复制代码
query.prepare("insert into student (id, name) values (?, ?)");
QVariantList ids;
ids << 20 << 21 << 22;
query.addBindValue(ids);
QVariantList names;
names << "xiaoming" << "xiaoliang" << "xiaogang";
query.addBindValue(names);
if(!query.execBatch()) qDebug() << query.lastError();
  1. 记录的更新和删除。它们和插入操作是相似的,并且也可以使用占位符。
cpp 复制代码
query.exec("update student set name = 'xiaohong' where id = 20"); // 更新
query.exec("delete from student where id = 21");                  // 删除
  1. 事务。事务可以保证一个复杂操作的原子性,就是对于一个数据库操作序列,这些操作要么全部做完,要么一条也不做,它是一个不可分割的工作单位。在Qt中,如果底层的数据库引擎支持事务,那么QSqlDriver::hasFeature(QSqlDriver::Transactions)会返回true。可以使用QSqlDatabase::transaction()来启动一个事务,然后编写一些希望在事务中执行的SQL语句,最后调用QSqlDatabase::commit()提交或者QSqlDatabase::rollback()回滚。当使用事务时必须在创建查询以前就开始事务。
cpp 复制代码
QSqlDatabase::database().transaction();
QSqlQuery query;
query.exec("SELECT id FROM employee WHERE name = 'Torild Halvorsen'");
if (query.next()) {
    int employeeId = query.value(0).toInt();
    query.exec("INSERT INTO project (id, name, ownerid) "
               "VALUES (201, 'Manhattan Project', "
               + QString::number(employeeId) + ')');
}
QSqlDatabase::database().commit();

10.2.5 SQL查询模型

  1. 除了QSqlQuery,Qt还提供了3个更高层的类来访问数据库,分别是QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel。这3个类都是从QAbstractTableModel派生来的,可以很容易地实现将数据库中的数据在QListView和QTableView等视图中进行显示。使用这些类的另一个好处是,这样可以使编写的代码很容易适应其他的数据源。例如,如果开始使用了QSqlTableModel,而后来要改为使用XML文件来存储数据,这样需要做的仅是更换一个数据模型。
  2. QSqlQueryModel提供了一个基于SQL查询的只读模型。
cpp 复制代码
QSqlQueryModel *model = new QSqlQueryModel(this);
model->setQuery("select * from student");
model->setHeaderData(0, Qt::Horizontal, tr("学号"));
model->setHeaderData(1, Qt::Horizontal, tr("姓名"));
model->setHeaderData(2, Qt::Horizontal, tr("课程"));
QTableView *view = new QTableView(this);
view->setModel(model);

这里先创建了QSqlQueryModel对象,然后使用setQuery()来执行SQL语句查询整张student表,并使用setHeaderData()来设置显示标头。后面创建了视图,并将QSqlQueryModel对象作为要显示的模型。这里要注意,其实QSqlQueryModel中存储的是执行完setQuery()函数后的结果集,所以视图中显示的是结果集的内容。QSqlQueryModel中还提供了columnCount()返回一条记录中字段的个数;rowCount()返回结果集中记录的条数;record()返回第n条记录;index()返回指定记录的指定字段的索引;clear()可以清空模型中的结果集。

10.2.6 SQL表格模型

  1. QSqlTableModel提供了一个一次只能操作一个SQL表的读写模型,它是QSqlQuery的更高层次的替代品,可以浏览和修改独立的SQL表,并且只需编写很少的代码,而且不需要了解SQL语法。该模型默认是可读可写的,如果想让其成为只读的,那么可以从视图进行设置。

    1)创建数据表
cpp 复制代码
   	QSqlQuery query;
    // 创建student表
    query.exec("create table student (id int primary key, "
                       "name varchar, course int)");
    query.exec("insert into student values(1, '李强', 11)");
    query.exec("insert into student values(2, '马亮', 11)");
    query.exec("insert into student values(3, '孙红', 12)");
    // 创建course表
    query.exec("create table course (id int primary key, "
                       "name varchar, teacher varchar)");
    query.exec("insert into course values(10, '数学', '王老师')");
    query.exec("insert into course values(11, '英语', '张老师')");
    query.exec("insert into course values(12, '计算机', '白老师')");

2)显示表

cpp 复制代码
model = new QSqlTableModel(this);
model->setTable("student");
model->select();
// 设置编辑策略
model->setEditStrategy(QSqlTableModel::OnManualSubmit);
ui->tableView->setModel(model);

这里创建一个QSqlTableModel后,只需使用setTable()来为其指定数据库表,然后使用select()函数进行查询,调用这两个函数就等价于执行了"select * from student"这个SQL语句。这里还可以使用setFilter()来指定查询时的条件。在使用该模型以前,一般还要设置其编辑策略,它由QSqlTableModel::EditStrategy枚举类型定义。

3)修改操作

cpp 复制代码
// 提交修改按钮
void MainWindow::on_pushButton_clicked() 
{
    // 开始事务操作
    model->database().transaction();
    if (model->submitAll()) {
        if(model->database().commit())   // 提交
            QMessageBox::information(this, tr("tableModel"),
                                     tr("数据修改成功!"));
    } else {
        model->database().rollback();    // 回滚
        QMessageBox::warning(this, tr("tableModel"),
                           tr("数据库错误: %1").arg(model->lastError().text()),
                           QMessageBox::Ok);
    }
}
// 撤销修改按钮
void MainWindow::on_pushButton_2_clicked()
{
    model->revertAll();
}

如果使用submitAll()将模型中的修改向数据库提交成功,那么执行commit();否则进行回滚rollback(),并提示错误信息。

如果要撤销修改,可以简单调用revertAll()函数将模型中的修改进行恢复。

4)筛选操作:可以使用setFilter()函数来进行数据筛选:注意,筛选的字符串中"%1"必须用单引号括起来

cpp 复制代码
// 查询按钮,进行筛选
void MainWindow::on_pushButton_5_clicked()
{
   QString name = ui->lineEdit->text();
    // 根据姓名进行筛选,一定要使用单引号
    model->setFilter(QString("name = '%1'").arg(name));
    model->select();
}
// 显示全表按钮
void MainWindow::on_pushButton_6_clicked()
{
    model->setTable("student");
    model->select();
}

5)排序操作:可以使用setSort()函数来对指定的字段进行排序。

cpp 复制代码
// 按id升序排列按钮
void MainWindow::on_pushButton_5_clicked() 
{
		//id字段,即第0列,升序排列
    model->setSort(0, Qt::AscendingOrder); 
    model->select();
}
// 按id降序排列按钮
void MainWindow::on_pushButton_6_clicked() 
{
    model->setSort(0, Qt::DescendingOrder);
    model->select();
}

6)删除记录

cpp 复制代码
 // 删除选中行按钮
void MainWindow::on_pushButton_4_clicked()
{   
		// 获取选中的行
    int curRow = ui->tableView->currentIndex().row(); 
    // 删除该行
    model->removeRow(curRow);   
    int ok = QMessageBox::warning(this,tr("删除当前行!"),
          tr("你确定删除当前行吗?"), QMessageBox::Yes, QMessageBox::No);
    if(ok == QMessageBox::No)
    {         // 如果不删除,则撤销
        model->revertAll();
    } else { // 否则提交,在数据库中删除该行
        model->submitAll();
    }
}

可以调用removeRow()来删除行,这时该行的最前面会显示"!"号。在删除行时会弹出一个对话框,提示是否确定要删除该行,如果确定删除,那么就执行submitAll()函数进行提交修改,否则执行revertAll()函数进行恢复。

7)添加记录

cpp 复制代码
// 添加记录按钮
void MainWindow::on_pushButton_3_clicked()    
{   
		 // 获得表的行数
    int rowNum = model->rowCount();          
    int id = 10;
    // 添加一行
    model->insertRow(rowNum);                  
    model->setData(model->index(rowNum,0), id);
     // 可以直接提交
    model->submitAll();                     
}

可以使用insertRow()插入一行,使用setData()可以为一个字段设置值。注意,因为id为主键,所以必须为其提供一个id值。

10.2.7 SQL关系表格模型

  1. QSqlRelationalTableModel继承自QSqlTableModel,并且对其进行了扩展,提供了对外键的支持。一个外键就是一个表中的一个字段和其他表中的主键字段之间的一对一的映射。例如,student表中的course字段对应的是course表中的id字段,那么就称字段course是一个外键。因为这里的course字段的值是一些数字,这样的显示很不友好,使用关系表格模型,就可以将它显示为course表中的name字段的值。
cpp 复制代码
QSqlRelationalTableModel *model = new QSqlRelationalTableModel(this);
model->setTable("student");
model->setRelation(2, QSqlRelation("course", "id", "name"));
model->select();
QTableView *view = new QTableView(this);
view->setModel(model);
setCentralWidget(view);
  1. Qt中还提供了一个QSqlRelationalDelegate委托类,它可以为QSqlRelationalTableModel显示和编辑数据。这个委托为一个外键提供了一个QComboBox部件来显示所有可选的数据,这样就显得更加人性化了。
cpp 复制代码
view->setItemDelegate(new QSqlRelationalDelegate(view));

10.3 Qt Widgets中的XML应用

  1. XML(Extensible Markup Language,可扩展标记语言),是一种类似于HTML的标记语言,设计目的是用来传输数据,而不是显示数据。XML的标签没有被预定义,用户需要在使用时自行进行定义。XML是W3C(万维网联盟)的推荐标准。相对于数据库表格的二维表示,XML使用的树形结构更能表现出数据的包含关系,作为一种文本文件格式,XML简单明了的特性使得它在信息存储和描述领域非常流行。
  2. Qt中提供了Qt XML模块来进行XML文档的处理,这里主要提供了两种解析方法: DOM方法,可以进行读写;SAX方法(SAX相关类已经从模块移除),可以进行读取。从Qt 5开始,Qt XML模块不再提供维护,而是推荐使用Qt Core模块中的QXmlStreamReader和QXmlStreamWriter进行XML读取和写入,这是一种基于流的方法。
  3. XML文档示例:
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<library>
    <book id="01">
        <title>Qt</title>
        <author>shiming</author>
    </book>
    <book id="02">
        <title>Linux</title>
        <author>yafei</author>
    </book>
</library>

1)每个XML文档都由XML声明语句(或者称为XML序言)开始,它是对XML文档处理的环境和要求的说明,比如这里的<?xml version="1.0" encoding="UTF-8"?>,其中xml version="1.0",表明使用的XML的版本号,这里字母是区分大小写的;encoding="UTF-8"是使用的编码。

2)XML文档内容由多个元素组成,一个元素由起始标签<标签名>、终止标签</标签名>以及两个标签之间的内容组成。文档中第一个元素被称为根元素,比如这里的,XML文档必须有且只有一个根元素。

3)元素的名称是区分大小写的,元素还可以嵌套,比如这里的library、book、title和author等都是元素。

4)元素可以包含属性,用来描述元素的相关信息,属性名和属性值在元素的起始标签中给出,格式为<元素名 属性名="属性值">,如,属性值必须在单引号或者双引号中。

5)在元素中可以包含子元素,也可以只包含文本内容,比如这里的Qt中的Qt就是文本内容。

10.3.1 QXmlStreamReader

  1. 从Qt 4.3开始引入了两个新的类来读取和写入XML文档:QXmlStreamReader和QXmlStreamWriter。QXmlStreamReader可以从QIODevice或者QByteArray中读取数据。它提供了一个快速的解析器通过一个简单的流API来读取格式良好的XML文档,它是作为Qt的SAX解析器的替代品的身份出现的,因为它比SAX解析器更快更方便。流读取器的基本原理就是将XML文档报告为一个记号(tokens)流,应用程序代码自身来驱动循环,在需要的时候可以从读取器中一个接一个的拉出记号。这个是通过调用readNext()函数实现的,它可以读取下一个记号,然后返回一个记号类型,它由枚举类型QXmlStreamReader::TokenType定义,其所有取值如表所列。
  2. 可以使用isStartElement()和text()等函数来判断这个记号是否包含需要的信息。使用这种主动拉取记号的方式最大的好处就是可以构建递归解析器,也就是可以在不同的函数或者类中来处理XML文档中的不同记号。
cpp 复制代码
QFile file("../myxmlstream/my.xml");
if (!file.open(QFile::ReadOnly | QFile::Text))
{
    qDebug()<<"Error: cannot open file";
    return 1;
}
QXmlStreamReader reader;
// 设置文件,这时会将流设置为初始状态
reader.setDevice(&file);
// 如果没有读到文档结尾,而且没有出现错误
while (!reader.atEnd()) {
    // 读取下一个记号,它返回记号的类型
    QXmlStreamReader::TokenType type = reader.readNext();
    // 下面便根据记号的类型来进行不同的输出
    if (type == QXmlStreamReader::StartDocument)
        qDebug() << reader.documentEncoding() << reader.documentVersion();
    if (type == QXmlStreamReader::StartElement) {
        qDebug() << "<" << reader.name() << ">";
        if (reader.attributes().hasAttribute("id"))
            qDebug() << reader.attributes().value("id");
    }
    if (type == QXmlStreamReader::EndElement)
        qDebug() << "</" << reader.name() << ">";
    if (type == QXmlStreamReader::Characters && !reader.isWhitespace())
        qDebug() << reader.text();
}
// 如果读取过程中出现错误,那么输出错误信息
if (reader.hasError()) {
    qDebug() << "error: " << reader.errorString();
}
file.close();

10.3.2 QXmlStreamWriter

  1. 与QXmlStreamReader对应的是QXmlStreamWriter,它通过一个简单的流API提供了一个XML写入器。QXmlStreamWriter的使用也是十分简单的,只需要调用相应的记号的写入函数来写入相关数据即可。
cpp 复制代码
QFile file("../myxmlstream/my2.xml");
if (!file.open(QFile::WriteOnly | QFile::Text))
{
    qDebug() << "Error: cannot open file";
    return 1;
}
QXmlStreamWriter stream(&file);
stream.setAutoFormatting(true);
stream.writeStartDocument();
stream.writeStartElement("bookmark");
stream.writeAttribute("href", "https://www.qt.io/");
stream.writeTextElement("title", "Qt Home");
stream.writeEndElement();
stream.writeEndDocument();
file.close();
  1. 这里使用了setAutoFormatting(true)函数来自动设置格式,这样会自动换行和添加缩进。然后使用了writeStartDocument(),该函数会自动添加首行的XML声明,添加元素可以使用writeStartElement(),要注意,一定要在元素的属性、文本等添加完成后,使用writeEndElement()来关闭前一个打开的元素。在最后使用writeEndDocument()来完成文档的写入。

10.4 Qt Quick中的模型/视图架构简介

在Qt Quick中也使用模型、视图和委托的概念来存储显示数据。这种开发架构将可视的数据模块化,从而让开发人员和设计人员能够分别控制数据的不同层面。例如,开发人员可以很方便地在列表视图和表格视图之间进行切换。而将数据实例封装进一个委托,可以使开发人员决定如何显示或处理这些数据。

css 复制代码
Item {
    width: 200; height: 250

    ListModel {
        id: myModel
        ListElement { type: "Dog"; age: 8 }
        ListElement { type: "Cat"; age: 5 }
    }

    Component {
        id: myDelegate
        Text { text: type + ", " + age; font.pointSize: 12 }
    }

    ListView {
        anchors.fill: parent
        model: myModel; delegate: myDelegate
    }
}

1)这里首先创建了一个ListModel作为数据模型,然后使用一个Component组件作为委托,最后使用ListView作为视图,在视图中需要指定模型和委托。ListView的数据模型model用来提供数据,委托delegate用来设置数据的显示方式。

2)在ListModel中,使用了ListElement添加数据项。每一个数据项都可以有多种类型的角色,比如这里有两个:type和age,并且分别指定了它们的值。如果模型中没有包含任何命名的角色,那么可以通过modelData角色来提供数据。

3)委托可以使用一个组件来实现,在其中可以直接绑定数据模型中的角色,比如这里将type和age的值显示在了一个Text文本中。委托还可以使用一个特殊的index角色,它包含了模型中数据项的索引值。

10.5 Qt Quick中的数据模型

Qt Quick提供的模型类型主要包含在QtQml.Models模块中,另外还有一个基于XML的QtQml.XmlListModel模型,以及现在版本中还处于实验阶段的TableModel模型。如果这些模型都不能满足需要,还可以使用Qt C++定义模型,或者使用QtQuick.LocalStorage类型来读取和写入SQLite数据库。

10.5.1 整数作为模型

最简单的,可以使用整数作为模型。在这种情况下,模型中不包含任何数据角色。例如,在下面的代码片段中创建了一个包含5个数据项的ListView:

css 复制代码
Item {
    width: 200; height: 250
    Component {
        id: itemDelegate
        Text { text: "I am item number: " + index }
    }
    ListView {
        anchors.fill: parent
        model: 5
        delegate: itemDelegate
    }
}

10.5.2 ListModel

  1. ListModel是一个简单的容器,可以包含ListElement类型来存储数据。ListModel的数据项的数量可以使用count属性获得。为了维护模型中的数据,该类型还提供了一系列方法,包括追加append()、插入insert()、移动move()、移除remove()、获取get()、替换set()和清空clear()等。其中一些方法需要接受字典类型(如"cost": 5.95)作为其参数,这种字典类型会被模型自动转换成ListElement对象。如果需要通过模型修改ListElement中的内容,可以使用setProperty()方法,这个方法可以修改给定索引位置的ListElement的属性值。
  2. ListElement需要在ListModel中定义,使用方法同其它QML类型基本没有区别,不同之处在于,ListElement没有固定的属性,而是包含一系列自定义的键值。可以把ListElement看作是一个键值对组成的集合,其中键被称为role(角色),它使用与属性相同的语法进行定义,角色既定义了如何访问数据,也定义了数据本身。例如:cost: 5.95
    1)角色的名字以小写字母开始,并且应当是给定模型中所有ListElement通用的名字。
    2)角色的值必须是简单的常量:字符串(带有引号,可以包含在QT_TR_NOOP调用中)、布尔类型(true和false)、数字或枚举类型(例如AlignText.AlignHCenter)。
  3. 角色的名字供委托获取数据使用,每一个角色的名字都可以在委托的作用域内访问,并且指向当前ListElement中对应的值。另外,角色还可以包含列表数据,例如包含多个ListElement。
css 复制代码
ListModel {
    id: fruitModel

    ListElement {
        name: "Apple"; cost: 2.45
        attributes: [
            ListElement { description: "Core" },
            ListElement { description: "Deciduous" }
        ]
    }

    ListElement {
        name: "Orange"; cost: 3.25
        attributes: [
            ListElement { description: "Citrus" }
        ]
    }

    ListElement { ... ...}
}

Component {
    id: fruitDelegate
    
    Item {
        width: 200; height: 50
        
        Text { id: nameField; text: name }
        Text { text: '$' + cost; anchors.left: nameField.right }
        Row {
            anchors.top: nameField.bottom; spacing: 5
            Text { text: "Attributes:" }
            Repeater {
                model: attributes
                Text { text: description }
            }
        }
        MouseArea {
            anchors.fill: parent
            onClicked: fruitModel.setProperty(
                                index, "cost", cost * 2)
        }
    }
}
ListView {
    anchors.fill: parent
    model: fruitModel; delegate: fruitDelegate
}

10.5.3 XmlListModel

  1. XmlListModel可以从XML数据创建只读的模型,即可以作为视图的数据源,也可以为Repeater等能够和模型数据进行交互的类型提供数据。由于XmlListModel的数据是异步加载的,因此当程序启动、数据尚未加载的时候,界面会显示一段时间的空白。可以使用XmlListModel::status属性判断模型加载的状态。该属性可取的值为:
    1)XmlListModel.Null:模型中没有XML数据;
    2)XmlListModel.Ready:XML数据已经加载到模型;
    3)XmlListModel.Loading:模型正在读取和加载XML数据;
    4)XmlListModel.Error:加载数据出错,详细出错信息可以使用errorString()获得。
  2. XmlListModel是只读模型,当原始XML数据发生改变时,可以通过调用reload()刷新模型数据。
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
    <channel>
        ...
	    <item>
	        <title>名校投放新专业</title>
	        <pubDate>2016</pubDate>
	        ...
	    </item>
	    <item>
	        <title>北京普通初中</title>
	        <pubDate>2016</pubDate>
	        ...
        </item>
	    ...
    </channel>
</rss>
css 复制代码
XmlListModel {
    id: xmlModel
    source: "http://www.people.com.cn/rss/edu.xml"
    query: "/rss/channel/item"
    
    XmlListModelRole { name: "title"; elementName: "title" }
    XmlListModelRole { name: "pubDate"; elementName: "pubDate" }
}

ListView {
    id: view
    anchors.fill: parent
    model: xmlModel
    focus: true
    spacing: 8
    delegate: Label {
        id: label
        width: view.width; height: 50
        verticalAlignment: Text.AlignVCenter
        text: title + ": " + pubDate
        font.pixelSize: 15; elide: Text.ElideRight
        color: label.ListView.isCurrentItem ? 
                                 "white" : "black"
        background: Image {
            visible: label.ListView.isCurrentItem
            source: "bg.png"
        }
    }
}

10.5.4 TableModel

  1. TableModel从Qt 5.14引入,在现在的版本中依然需要通过实验模块Qt.labs.qmlmodels来提供。
  2. 在该类型出现以前,要想创建具有多个列的模型,需要通过C++中自定义QAbstractTableModel子类来实现。而TableModel的目的就是实现一个简单的模型,可以将JavaScript/JSON对象存储为能与TableView一起使用的表格模型的数据,而不再需要子类化QAbstractTableModel。
css 复制代码
TableModel {
    id: tableModel
    TableModelColumn { display: "checked" }
    TableModelColumn { display: "amount" }
    TableModelColumn { display: "fruitType" }
    TableModelColumn { display: "fruitName" }
    TableModelColumn { display: "fruitPrice" }

    rows: [
        {
            checked: false, amount: 1, fruitType: "Apple",
            fruitName: "Granny Smith", fruitPrice: 1.50
        },
        {
            checked: true, amount: 4, fruitType: "Orange",
            fruitName: "Navel", fruitPrice: 2.50
        },
        {
            checked: false, amount: 1, fruitType: "Banana",
            fruitName: "Cavendish", fruitPrice: 3.50
        }
    ]
}
  1. 模型中的每个列都是通过声明TableModelColumn实例来指定的,其中每个实例的顺序决定了其列索引。使用rows属性或通过调用appendRow()来设置模型的初始行数据。
  2. TableModel设计用于JavaScript/JSON数据,其中每一行都是一些简单的键值对。要访问特定行,可以使用getRow(),也可以通过rows属性直接访问模型的JavaScript数据,但不能以这种方式修改模型数据。要添加新行,可以使用appendRow()和insertRow(),要修改现有行,可以使用setRow()、moveRow()、removeRow()和clear()等方法。
css 复制代码
TableView {
    anchors.fill: parent
    columnSpacing: 1; rowSpacing: 1
    boundsBehavior: Flickable.StopAtBounds

    model: tableModel

    delegate:  TextInput {
        text: model.display; padding: 12; selectByMouse: true
        onAccepted: model.display = text

        Rectangle {
            anchors.fill: parent; color: "#efefef"; z: -1
        }
    }
}

10.5.5 其他模型类型

  1. ObjectModel包含了用于在视图中进行显示的可视项目,也就是说,该类型可以将Qt Quick中的可视化项目作为数据项显示到视图上。与ListModel不同,使用ObjectModel的视图不需要指定委托,因为ObjectModel的数据项本身就是可视化项目。可以使用model的附加属性index获取数据项的索引位置。该类型也提供了追加append()、插入insert()、移动move()、移除remove()、获取get()和清空clear()等方法。
  2. DelegateModel类型封装了一个模型和用于显示这个模型的委托,可以使用model属性指定模型,delegate属性指定委托。一般情况下并不需要使用DelegateModel。不过,如果需要将QAbstractItemModel的子类作为模型使用的时候,使用DelegateModel可以很方便地操作和访问modelIndex()。另外,DelegateModel可以与Package一起,为多种视图提供委托,也可以与DelegateModelGroup一起用于排序和过滤委托项。DelegateModelGroup类型提供了一种定位DelegateModel委托项的模型数据的方法,并且能够对委托项进行排序和过滤。
  3. Package类型可以结合DelegateModel,实现让委托为多个视图提供共享的上下文。在Package中的任何项目都会通过Package.name附加属性分配一个名称。
  4. 这里使用Package作为DelegateModel的委托,里面包含了两个命名Package.name的项目:list和grid。在DelegateModel类型中包含一个parts属性,它可以选取一个DelegateModel模型,这个模型中会使用指定名称的项目作为委托。例如这里在ListView中使用了parts.list作为模型,该模型就会使用Package中的list项目作为委托。
css 复制代码
Rectangle {
    width: 200; height: 300

    DelegateModel {
        id: delegateModel
        delegate: Package {
            Text { id: listDelegate; width: parent.width; height: 25;
                text: 'in list'; Package.name: 'list'}
            Text { id: gridDelegate; width: parent.width / 2; height: 50;
                text: 'in grid'; Package.name: 'grid' }
        }
        model: 5
    }
    Rectangle{
        height: parent.height/2; width: parent.width; color: "lightgrey"
        ListView {
            anchors.fill: parent; model: delegateModel.parts.list
        }
    }
    GridView {
        y: parent.height/2;
        height: parent.height/2; width: parent.width;
        cellWidth: width / 2; cellHeight: 50
        model: delegateModel.parts.grid
    }
}

10.5.6 在委托中使用必需属性来匹配模型角色

  1. required关键字声明的必需属性在模型视图程序中扮演特殊角色。为了更好地控制可访问的角色,并使委托在视图之外更为独立和适用,可以借助必需属性。如果委托包含必需属性,则不用指定角色,QML引擎将检查必需属性的名称是否与模型角色的名称匹配,如果是,则该属性将绑定到模型中的相应值。
  2. 注意,如果在委托中使用了必需属性,那么用到的模型角色都要进行声明,比如这里只声明了type和age角色,所以现在noise无法直接使用,不仅如此,model、index和modelData等常用的角色也将无法直接使用,除非明确把它们设置为必需属性。
  3. 还有一种情况,就是委托中的属性与模型角色名称相同,这时,只需要为相关属性添加required关键字即可。
css 复制代码
ListModel {
    id: myModel
    ListElement { type: "Dog"; age: 8; noise: "meow" }
    ListElement { type: "Cat"; age: 5; noise: "woof" }
}
component MyDelegate : Text {
    required property string type
    required property int age
    text: type + ", " + age
}
ListView {
    anchors.fill: parent
    model: myModel
    delegate: MyDelegate {}
}

10.5.7 LocalStorage

  1. LocalStorage是一个用于读取和写入SQLite数据库的单例类型,可以使用openDatabaseSync()打开一个本地存储的SQL数据库。这些数据库是特定于用户的,也是特定于QML的,但是可以被所有QML应用程序访问。数据库保存在QQmlEngine::offlineStoragePath()返回的子文件夹Databases中。数据库的链接无须手动释放,它们会被JavaScript的垃圾收集器自动关闭。
  2. LocalStorage模块的API与HTML 5 Web Database API兼容。模块中所有API都是异步的,每一个函数的最后一个参数都是该操作的回调函数。如果不关心这个回调函数,可以简单地忽略该参数。
css 复制代码
Text {
    text: "?"
    anchors.horizontalCenter: parent.horizontalCenter
    
    function findGreetings() {
        var db = LocalStorage.openDatabaseSync("QQmlExampleDB",
                                "1.0", "The Example QML SQL!", 1000000);
        db.transaction(
                    function(tx) {
                        // 如果数据库表不存在则进行创建
                        tx.executeSql('CREATE TABLE IF NOT EXISTS Greeting
                                   (salutation TEXT, salutee TEXT)');
                        // 添加一条记录
                        tx.executeSql('INSERT INTO Greeting VALUES(?, ?)',
                                      [ 'hello', 'world' ]);
                        // 显示内容
                        var rs = tx.executeSql('SELECT * FROM Greeting');
                        
                        var r = ""
                        for(var i = 0; i < rs.rows.length; i++) {
                            r += rs.rows.item(i).salutation + ", "
                                    + rs.rows.item(i).salutee + "\n"
                        }
                        text = r
                    }
                    )
    }
    Component.onCompleted: findGreetings()
}
  1. 可以使用如下方式打开或创建数据库:
css 复制代码
import QtQuick.LocalStorage as Sql
var db = Sql.openDatabaseSync(identifier, version, description,
                            estimated_size, callback(db))

openDatabaseSync()函数返回数据库标识符为identifier的数据库。如果数据库不存在,将会自动创建。回调函数callback(db)以该数据库作为参数,当数据库创建失败时,callback()函数才会被回调。参数description和estimatedSize将被写入INI文件,不过这两个参数现在都没有使用。函数可能会抛出异常,异常代码为SQLException.DATABASE_ERR或SQLException.VERSION_ERR。

数据库创建完成之后,系统会创建一个INI文件,用于指定数据库的特性。这些数据能够被应用程序工具使用。

10.6 视图类型

视图作为数据项集合的容器,不仅提供了强大的功能,还可以进行定制来满足样式或行为上的特殊需求。视图类型主要是Flickable的几个子类型,包括列表视图ListView、网格视图GridView、表格视图TableView及其子类型树视图TreeView。作为Flickable的子类型,这几个视图在数据量超出窗口范围时,可以进行拖动以显示更多的数据。

10.6.1 ListView

10.6.1.1 键盘导航和高亮

  1. 使用键盘控制视图,需要设置focus属性为true,以便ListView能够接收键盘事件。keyNavigationEnabled属性可以设置是否启用键盘导航。还可以设置keyNavigationWraps属性为true,这样当使用键盘导航时如果到达列表的最后一个数据项,会自动跳转到列表的第一个数据项。
  2. highlight属性可以设置一个组件作为高亮,实际的组件实例的几何形状是被列表管理的,以便该高亮留在当前项目,除非将highlightFollowsCurrentItem属性设置为false。
  3. 高亮项目的默认z值为0。默认情况下,ListView负责移动高亮项的位置。可以自行设置高亮项的移动速度和改变大小的速度,可用的属性有highlightMoveVelocity 、highlightMoveDuration 、highlightResizeVelocity 和highlightResizeDuration 。前两个分别以速度值和持续时间设置高亮项移动速度;后两个分别以速度值和持续时间设置高亮项大小改变的速度。默认情况下,速度值为每秒400像素,持续时间值为-1。如果同时设置速度值和持续时间,则取二者之中较快的一个。要使用这4个属性,必须保证highlightFollowsCurrentItem为true才会有效果。
  4. 移动速度和持续时间属性用于index变化而产生的移动,例如调用incrementCurrentIndex(),而当用户轻击ListView时,轻击的速度将用于控制移动速度。ListView还会在委托的根项目中附加多个属性,例如ListView.isCurrentItem,可以对当前项进行特殊处理。
  5. 示例:
css 复制代码
ListView {
    id: listview; anchors.fill: parent; anchors.margins: 30
    model: 5; spacing: 5
    delegate: numberDelegate; snapMode: ListView.SnapToItem
    header: Rectangle {
        width: 50; height: 20; color: "#b4d34e"
        Text {anchors.centerIn: parent; text: "header"}
    }
    footer: Rectangle {
        width: 50; height: 20; color: "#797e65"
        Text {anchors.centerIn: parent; text: "footer"}
    }
    highlight: Rectangle {
        color: "black"; radius: 5
        opacity: 0.3; z:5
    }
    focus: true; keyNavigationWraps :true
    highlightMoveVelocity: -1
    highlightMoveDuration: 1000
}

1)这里分别使用了两个Rectangle项目来作为header和footer。在highlight中使用了一个黑色半透明的矩形,并设置了其z值为5,目的是让高亮可以显示在所有数据项的上面,也可以设置为大于0的其它值。这里必须在ListView中设置focus为true,才可以使用键盘进行导航。

2)当使用高亮时,可以使用一系列属性控制高亮的行为。preferredHighlightBegin属性和preferredHighlightEnd属性用来设置高亮(当前项目)的最佳范围,前者必须小于后者。它们还受到highlightRangeMode属性的影响。

10.6.1.2 数据分组

  1. ListView支持数据的分组显示:相关数据可以出现在一个分组中。每个分组还可以使用委托定义其显示的样式。ListView定义了一个section附加属性,用于将相关数据显示在一个分组中,section是一个属性组,其属性包含:
    1)section.property:定义分组的依据,也就是根据数据模型的哪一个角色进行分组;
    2)section.criteria:定义如何创建分组名字,可选值是:
    ViewSection.FullString:默认,依照section.property定义的值创建分组;
    ViewSection.FirstCharacter:依照section.property值的首字母创建分组;
    3)section.delegate:与ListView的委托类似,用于提供每一个分组的委托组件,其z属性值为2。
    4)section.labelPositioning:定义当前或下一个分组标签的位置,可选值是:
    ViewSection.InlineLabels:默认,分组标签出现在数据项之间;
    ViewSection.CurrentLabelAtStart:在列表滚动时,当前分组的标签始终出现在列表视图开始的位置;
    ViewSection.NextLabelAtEnd:在列表滚动时,下一分组的标签始终出现在列表视图末尾。该选项要求系统预先找到下一个分组的位置,因此可能会有一定的性能问题。
  2. ListView中的每一个数据项都有ListView.section、ListView.previousSection和ListView.nextSection等附加属性。
css 复制代码
Rectangle {
    id: container; width: 150; height: 300

    ListModel {
        id: nameModel
        ListElement { name: "LiLi"; group: "friend" }
        ListElement { name: "LiuMing"; group: "friend" }
        ... ...
    }

    ListView {
        anchors.fill: parent; model: nameModel
        delegate: Text { text: name; font.pixelSize: 18 }
        section.property: "group"
        section.criteria: ViewSection.FullString
        section.delegate: sectionHeading
    }

    Component {
        id: sectionHeading
        Rectangle {
            width: container.width; height: childrenRect.height
            color: "lightsteelblue"
            Text {
                text: section; font.bold: true; font.pixelSize: 20
            }
        }
    }
}

这里使用了模型中的group角色进行分组,并且是FullString匹配,这样就会按照模型中的group角色的值进行分组,将group值相同的分在一组进行显示。

10.6.2 GridView

  1. 网格视图GridView在一块可用的空间中以方格形式显示数据列表。GridView和ListView非常类似,实质的区别在于,GridView需要在一个二维表格视图中使用委托,而不是线性列表中。相对于ListView,GridView并不建立在委托的大小及其之间的间距之上,GridView使用cellWidth和cellHeight属性控制单元格的大小,每一个委托所渲染的数据项都会出现在这样一个单元格的左上角。
  2. GridView也可以包含头部和脚部以及使用高亮委托,这与ListView是类似的。还可以使用flow属性设置GridView的方向,可选值为:
    1)GridView.FlowLeftToRight:默认值,表格从左向右开始填充,按照从上向下的顺序添加行。此时,表格是纵向滚动的。
    2)GridView.FlowTopToBottom:表格从上向下开始填充,按照从左向右的顺序添加列。此时,表格是横向滚动的。
css 复制代码
ListModel {
    id: model
    ListElement { name: "Jim"; portrait: "icon.png" }
    ... ...
}

GridView {
    id: grid; width: 200; height: 200
    cellWidth: 100; cellHeight: 100
    model: model; delegate: contactDelegate
    highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
    focus: true
}

Component {
    id: contactDelegate
    Item {
        width: grid.cellWidth; height: grid.cellHeight
        Column {
            anchors.centerIn: parent
            Image { source: portrait; anchors.horizontalCenter:
                    parent.horizontalCenter }
            Text { text: name; anchors.horizontalCenter:
                    parent.horizontalCenter }
        }
    }
}

这里创建了一个网格视图,视图中每一个单元格的宽度和高度均为100像素,而委托中为每一个数据项设置了一个图片和一个文本。

10.6.3 视图过渡

  1. 在ListView和GridView中,因为修改了模型中的数据而需要更改视图上的数据项时,可以指定一个过渡使视图的变化出现动画效果。可以使用过渡的属性有:populate、add、remove、move、displaced、addDisplaced、removeDisplaced和moveDisplaced等。
css 复制代码
ListView {
    width: 160; height: 320
    model: ListModel {}

    delegate: Rectangle {
        width: 100; height: 30; border.width: 1
        color: "lightsteelblue"
        Text { anchors.centerIn: parent; text: name }
    }
    add: Transition {
        NumberAnimation { property: "opacity";
               from: 0; to: 1.0; duration: 400 }
        NumberAnimation { property: "scale";
               from: 0; to: 1.0; duration: 400 }
    }
    displaced: Transition {
        NumberAnimation { properties: "x,y"; duration: 400;
               easing.type: Easing.OutBounce }
    }
    focus: true
    Keys.onSpacePressed: model.insert(0, { "name": "Item "
                                                 + model.count })
}
  1. 这里每当按下空格键的时候,都会向模型中添加一个数据项。在视图中为添加add和移位displaced操作设置了过渡效果,所以每当添加数据项时都会有动画效果。注意,这里的NumberAnimation对象并不需要指定target和to属性,因为视图已经隐式的将target设置为了对应的项目,将to设置为了该项目最终的位置。
  2. 运行代码,快速按下空格键的时候会有一些数据项无法正常添加,下面来看一下如何解决这个问题。
  3. 一个视图过渡有可能在任意时刻被其他过渡打断。如果只进行简单的过渡,无需考虑动画中断的情况。但是,如果过渡中更改了一些属性,那么中断可能会引起不可预料的后果。例如,在前面示例中快速按下空格键出现的问题,项目0通过add过渡插入到了index 0的位置。这时项目1非常快速地插入到index 0的位置,而此时项目0的过渡还没有结束。项目1插入到项目0的前面,所以项目0要移位,视图就会中断项目0的add过渡,并开始项目0的displaced过渡。因为中断的发生,opacity和scale动画没有结束,会导致项目的opacity和scale值小于1.0。要解决这个问题,在displaced过渡中要确保项目的属性已经到达了在add过渡中设置的值。例如:
css 复制代码
displaced: Transition {
    NumberAnimation { properties: "x,y"; duration: 400; 
                      easing.type: Easing.OutBounce }
    // 确保opacity和scale值变为1.0
    NumberAnimation { property: "opacity"; to: 1.0 }
    NumberAnimation { property: "scale"; to: 1.0 }
}
相关推荐
小陈工2 小时前
2026年4月1日技术资讯洞察:AI芯片革命、数据库智能化与云原生演进
前端·数据库·人工智能·git·python·云原生·开源
酉鬼女又兒2 小时前
零基础快速入门前端深入掌握箭头函数、Promise 与 Fetch API —— 蓝桥杯 Web 考点全解析(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·css·职场和发展·蓝桥杯·es6·js
迷藏4942 小时前
**发散创新:Go语言中基于上下文的优雅错误处理机制设计与实战**在现代后端开发中,**错误处理**早已不是简单
java·开发语言·后端·python·golang
2301_764441332 小时前
基于python实现的便利店投资分析财务建模评估
开发语言·python·数学建模
猿小喵2 小时前
MySQL数据库参数解读-第二篇
数据库·mysql
逆境不可逃2 小时前
【用AI学Agent】Agent入门进阶:Prompt工程
大数据·数据库·人工智能
杰克尼2 小时前
知识点总结--day10(Spring-Cloud框架)
java·开发语言
wengqidaifeng2 小时前
备战蓝桥杯----C/C++组 (三)算法讲解前言
c语言·c++·蓝桥杯
PD我是你的真爱粉2 小时前
MySQL 索引深度解析:从底层结构到实战优化
数据库·mysql