该文章翻译的官方文档 原文: https://doc.qt.io/qt-5/modelview.html
每个UI开发人员都应该了解Mode/View编程,本教程将对Model/View进行全面的介绍。
表(Table)、列表(List)和树(Tree)控件是gui中经常使用的控件。这些控件访问数据的方式有两种。比较传统的方法是控件包含一个用于存储数据的内部容器。这种方法非常直观,但是,在许多重要的应用程序中,它会导致数据同步问题。第二种方法是Model/View编程,其中控件不维护内部数据容器。它们通过标准化接口访问外部数据,因此避免了数据重复。乍一看,这似乎很复杂,但是一旦您仔细观察,就会发现它不仅容易掌握,而且Mode/View编程的许多好处也变得更加清晰。
您还将了解到新的应用程序是否可以通过Mode/View编程更容易地编写,或者经典的标准控件是否也可以工作。本教程包括用于编辑和集成到项目中的示例代码。教程的源代码位于qt的examples/widgets/tutorial s/modelview目录中。
更多详细信息,参照:
Mode/View是一种用于在处理数据集的控件中分离数据和视图的技术。标准控件不是为将数据与视图分离而设计的,这就是为什么Qt有两种不同类型的控件。两种类型的控件看起来都一样,但它们与数据的交互方式不同。
标准控件使用的数据是作为控件一部分的
视图(View)类操作外部数据(模型)
让我们更仔细地看一看标准的表格控件。表格控件是用户可以更改的数据元素的二维数组。通过读写表格控件提供的数据元素,表格控件可以集成到程序流中。这种方法在许多应用程序中非常直观和有用,但是使用标准表格控件显示和编辑数据库表可能会有问题。必须协调两个数据副本:一个在控件外部;一个在控件内部。开发人员负责同步两个版本。除此之外,表示和数据的紧密耦合使得编写单元测试更加困难。
Mode/View提供了一个使用更通用的体系结构的解决方案。Mode/View消除了标准控件可能出现的数据一致性问题。Mode/View还可以更容易地使用同一数据的多个视图,因为一个模型可以传递给多个视图。最重要的区别是Mode/View控件不在表单元格后面存储数据。实际上,它们直接从您的数据操作。因为视图类不知道数据的结构,所以需要提供一个包装器,使数据符合QabstracteModel接口。视图使用此接口读取和写入数据。实现QabstratemModel的类的任何实例都称为模型。一旦视图接收到指向模型的指针,它将读取和显示其内容,并成为其编辑器。
下面是Model/View 控件及其相应标准控件。
控件 | 标准控件 | Model/View 视图类 |
---|---|---|
QListWidget | QListView | |
QTableWidget | QTableView | |
QTreeWidget | QTreeView | |
QColumnView shows a tree as a hierarchy of lists | ||
|
QComboBox can work as both a view class and also as a traditional widget |
在窗体和模型之间使用适配器非常方便。我们可以直接从表内部编辑存储在表中的数据,但是在文本字段中编辑数据更为方便。没有直接的Model/View能够在对控件的一个数据而不是数据集操作时将数据和视图分离开。比如QLineEdit,QCheckBox…因此我们需要一个适配器来将表单连接到数据源。
QdataWidgetMapper是一个很好的解决方案,因为它将表单控件映射到表行,并使为数据库表构建表单变得非常容易。
如果要开发模Model/View 应用程序,应该从哪里开始? 我们建议从一个简单的示例开始并逐步扩展它。 这使得理解架构变得更加容易。 在调用IDE之前,尝试详细了解Model/View 体系结构已被证明对许多开发人员来说不太方便。 从具有演示数据的简单Model/View 应用程序开始,实质上更容易。 试试看! 只需将您自己的数据替换为以下示例中的数据即可。
下面是7个非常简单和独立的应用程序,它们显示了Model/View 编程的不同方面。 源代码可以在examples / widgets / tutorials / modelview目录中找到。
我们从使用QTableView显示数据的应用程序开始。 我们稍后会添加编辑功能。
(文件来源:examples / widgets / tutorials / modelview / 1_readonly / main.cpp)
// main.cpp
#include
#include
#include "mymodel.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QTableView tableView;
MyModel myModel(0);
tableView.setModel( &myModel );
tableView.show();
return a.exec();
}
main()函数:
这是有趣的部分:我们创建一个MyModel实例并使用tableView.setModel(&myModel); 将它的指针传递给tableView。 tableView将调用它收到的指针的方法来找出两件事:
该模型需要一些代码来响应这一点。
我们有一个表数据集,所以让我们从QAbstractTableModel开始,因为它比更通用的QAbstractItemModel更容易使用。
(文件来源:examples / widgets / tutorials / modelview / 1_readonly / mymodel.h)
// mymodel.h
#include
class MyModel : public QAbstractTableModel
{
Q_OBJECT
public:
MyModel(QObject *parent);
int rowCount(const QModelIndex &parent = QModelIndex()) const override ;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
};
QAbstractTableModel需要实现三个抽象方法。
(文件来源:examples / widgets / tutorials / modelview / 1_readonly / mymodel.cpp)
// mymodel.cpp
#include "mymodel.h"
MyModel::MyModel(QObject *parent)
:QAbstractTableModel(parent)
{
}
int MyModel::rowCount(const QModelIndex & /*parent*/) const
{
return 2;
}
int MyModel::columnCount(const QModelIndex & /*parent*/) const
{
return 3;
}
QVariant MyModel::data(const QModelIndex &index, int role) const
{
if (role == Qt::DisplayRole)
{
return QString("Row%1, Column%2")
.arg(index.row() + 1)
.arg(index.column() +1);
}
return QVariant();
}
行和列的数量由MyModel :: rowCount()和MyModel :: columnCount()提供。 当视图必须知道单元格的文本是什么时,它会调用方法MyModel :: data()。 使用参数索引指定行和列信息,并将角色设置为Qt :: DisplayRole。 其他角色将在下一节中介绍。 在我们的示例中,生成了应显示的数据。 在实际的应用程序中,MyModel将有一个名为MyData的成员,它作为所有读写操作的目标。
这个小例子展示了模型的被动性。 该模型不知道何时使用或需要哪些数据。 它只是在每次视图请求时提供数据。
当模型的数据需要更改时会发生什么? 视图如何实现数据已更改并需要再次读取? 该模型必须发出一个信号,指示哪些单元格范围已经改变。 这将在2.3节中说明。
除了控制视图显示的文本外,模型还控制文本的外观。 当我们稍微改变模型时,我们得到以下结果:
事实上,除了data()方法外,不需要更改任何内容来设置字体、背景颜色、对齐方式和复选框。下面是产生上面所示结果的data()方法。不同的是,这次我们使用参数int role根据其值返回不同的信息片段。 (文件来源:examples / widgets / tutorials / modelview / 2_formatting / mymodel.cpp)
// mymodel.cpp
QVariant MyModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
int col = index.column();
// generate a log message when this method gets called
qDebug() << QString("row %1, col%2, role %3")
.arg(row).arg(col).arg(role);
switch(role){
case Qt::DisplayRole:
if (row == 0 && col == 1) return QString("<--left");
if (row == 1 && col == 1) return QString("right-->");
return QString("Row%1, Column%2")
.arg(row + 1)
.arg(col +1);
break;
case Qt::FontRole:
if (row == 0 && col == 0) //change font only for cell(0,0)
{
QFont boldFont;
boldFont.setBold(true);
return boldFont;
}
break;
case Qt::BackgroundRole:
if (row == 1 && col == 2) //change background only for cell(1,2)
{
QBrush redBackground(Qt::red);
return redBackground;
}
break;
case Qt::TextAlignmentRole:
if (row == 1 && col == 1) //change text alignment only for cell(1,1)
{
return Qt::AlignRight + Qt::AlignVCenter;
}
break;
case Qt::CheckStateRole:
if (row == 1 && col == 0) //add a checkbox to cell(1,0)
{
return Qt::Checked;
}
}
return QVariant();
}
每个格式化属性都将通过对data()方法的单独调用从模型中请求。角色参数用于让模型知道正在请求哪个属性:
enum Qt::ItemDataRole | Meaning | Type |
---|---|---|
Qt::DisplayRole | 文本 | Qt::DisplayRole |
Qt::FontRole | 字体 | QFont |
BackgroundRole | 用于单元格背景的画笔 | 用于单元格背景的画笔 |
Qt::TextAlignmentRole | 文本对齐方式 | enum Qt::AlignmentFlag |
Qt::CheckStateRole | 使用qvariant()取消复选框,或者使用qt::checked和Qt::Unchecked | enum Qt::ItemDataRole |
请参阅qt命名空间文档以了解有关qt::itemDataRole枚举功能的更多信息。
现在,我们需要确定使用分离的模型如何影响应用程序的性能,所以让我们跟踪视图调用data()方法的频率。为了跟踪视图调用模型的频率,我们在data()方法中放置了一个debug语句,该语句将记录到错误输出流中。在我们的小示例中,data()将被调用42次。每次将光标悬停在字段上时,都将再次调用data(),每个单元格调用7次。这就是为什么在调用data()并缓存昂贵的查找操作时,确保数据可用非常重要。
QVariant MyModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
int col = index.column();
if (role == Qt::DisplayRole)
{
if (row == 0 && col == 0)
{
return QTime::currentTime().toString();
}
}
return QVariant();
}
没有什么东西可以让时钟滴答作响。我们需要每秒钟告诉视图时间已经改变,需要重新阅读。我们用计时器来做这个。在构造函数中,我们将其间隔设置为1秒,并连接其超时信号。
(文件来源:examples/widgets/tutorials/modelview/3 ngmodel/mymodel.cpp)
MyModel::MyModel(QObject *parent)
:QAbstractTableModel(parent)
{
// selectedCell = 0;
timer = new QTimer(this);
timer->setInterval(1000);
connect(timer, SIGNAL(timeout()) , this, SLOT(timerHit()));
timer->start();
}
响应槽函数:
(文件来源: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)
void MyModel::timerHit()
{
//we identify the top left cell
QModelIndex topLeft = createIndex(0,0);
//emit a signal to make the view reread identified data
emit dataChanged(topLeft, topLeft);
}
我们要求视图通过发出dataChanged()信号再次读取左上角单元格中的数据。注意,我们没有显式地将dataChanged()信号连接到视图。这在我们调用setModel()时自动发生。
可以通过该方法隐藏标题:tableView->verticalHeader()->hide();
但是,头部内容是通过模型设置的,因此我们重新实现headerdata()方法:
(文件来源:examples/widgets/tutorials/modelview/4_headers/mymodel.cpp)
QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role == Qt::DisplayRole)
{
if (orientation == Qt::Horizontal) {
switch (section)
{
case 0:
return QString("first");
case 1:
return QString("second");
case 2:
return QString("third");
}
}
}
return QVariant();
}
请注意,方法headerdata()还有一个参数角色,其含义与myModel::data()中的相同。
在本例中,我们将构建一个应用程序,通过重复输入表单元格中的值,自动用内容填充窗口标题。为了方便地访问窗口标题,我们将qTableView放在一个qmainwindow中。
模型决定了编辑功能是否可用。我们只需要修改模型就可以启用可用的编辑功能。这是通过重新实现以下虚函数来完成的:setdata()和flags()。
(文件来源:examples/widgets/tutorials/modelview/5_edit/mymodel.h)
// mymodel.h
#include
#include
const int COLS= 3;
const int ROWS= 2;
class MyModel : public QAbstractTableModel
{
Q_OBJECT
public:
MyModel(QObject *parent);
int rowCount(const QModelIndex &parent = QModelIndex()) const override ;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex & index) const override ;
private:
QString m_gridData[ROWS][COLS]; //holds text entered into QTableView
signals:
void editCompleted(const QString &);
};
我们使用二维数组qstring m_griddata来存储数据。这使得m_griddata成为mymodel的核心。MyModel的其余部分就像一个包装器,将m_GridData适配到QabstratemModel接口。我们还引入了editCompleted()信号,这使得将修改后的文本传输到窗口标题成为可能。
(文件来源:examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)
bool MyModel::setData(const QModelIndex & index, const QVariant & value, int role)
{
if (role == Qt::EditRole)
{
//save value from editor to member m_gridData
m_gridData[index.row()][index.column()] = value.toString();
//for presentation purposes only: build and emit a joined string
QString result;
for (int row= 0; row < ROWS; row++)
{
for(int col= 0; col < COLS; col++)
{
result += m_gridData[row][col] + ' ';
}
}
emit editCompleted( result );
}
return true;
}
每次用户编辑单元格时都会调用setdata()。index参数告诉我们哪个字段已被编辑,value提供编辑过程的结果。角色将始终设置为qt::editRole,因为我们的单元格只包含文本。如果存在复选框,并且用户权限设置为允许选中该复选框,则还将使用设置为qt::checkStateRole的角色进行调用。
(文件来源:示例/widgets/tutorials/modelview/5_edit/mymodel.cpp)
Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}
可以使用flags()调整单元格的各种属性。
返回Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled足以显示可以选择单元格的编辑器。
如果编辑一个单元格修改的数据多于该特定单元格中的数据,则模型必须发出dataChanged()信号,以便读取已更改的数据。
您可以将上面的示例转换为具有树视图的应用程序。只需将QTableView替换为QTreeView,这将导致读/写树。不必更改模型。树不会有任何层次结构,因为模型本身没有任何层次结构。
QListView, QTableView和QTreeView都使用le 一个包含列表、表和树的抽象模型。这使得可以使用同一模型中的几种不同类型的视图类。
到目前为止,我们的示例模型是这样的:
我们要展示一棵真正的树。为了建立一个模型,我们将数据包装在上面的示例中。这一次我们使用QStandardItemModel,它是一个层次数据容器,也实现了QAbstractItemModel。若要显示树,QStandardItemModel必须填充 QStandardItems,它能够保存文本、字体、复选框或画笔等项的所有标准属性。
(文件来源:examples/widgets/tutorials/modelview/6_treeview/mainwindow.cpp)
// modelview.cpp
#include
#include
#include
#include "mainwindow.h"
const int ROWS = 2;
const int COLUMNS = 3;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
treeView = new QTreeView(this);
setCentralWidget(treeView);
standardModel = new QStandardItemModel ;
QList<QStandardItem *> preparedRow =prepareRow("first", "second", "third");
QStandardItem *item = standardModel->invisibleRootItem();
// adding a row to the invisible root item produces a root element
item->appendRow(preparedRow);
QList<QStandardItem *> secondRow =prepareRow("111", "222", "333");
// adding a row to an item starts a subtree
preparedRow.first()->appendRow(secondRow);
treeView->setModel(standardModel);
treeView->expandAll();
}
QList<QStandardItem *> MainWindow::prepareRow(const QString &first,
const QString &second,
const QString &third)
{
QList<QStandardItem *> rowItems;
rowItems << new QStandardItem(first);
rowItems << new QStandardItem(second);
rowItems << new QStandardItem(third);
return rowItems;
}
我们只是实例化一个QStandardItemModel并将几个QStandardItems添加到构造函数中。 然后我们可以创建分层数据结构,因为QStandardItem可以容纳其他QStandardItem。 节点在视图中折叠和展开。
我们想要访问所选项目的内容,以便将其与层次结构级别一起输出到窗口标题中。
所以让我们创建几个项目:
(文件来源:examples / widgets / tutorials / modelview / 7_selections / mainwindow.cpp)
#include
#include
#include
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
treeView = new QTreeView(this);
setCentralWidget(treeView);
standardModel = new QStandardItemModel ;
QStandardItem *rootNode = standardModel->invisibleRootItem();
//defining a couple of items
QStandardItem *americaItem = new QStandardItem("America");
QStandardItem *mexicoItem = new QStandardItem("Canada");
QStandardItem *usaItem = new QStandardItem("USA");
QStandardItem *bostonItem = new QStandardItem("Boston");
QStandardItem *europeItem = new QStandardItem("Europe");
QStandardItem *italyItem = new QStandardItem("Italy");
QStandardItem *romeItem = new QStandardItem("Rome");
QStandardItem *veronaItem = new QStandardItem("Verona");
//building up the hierarchy
rootNode-> appendRow(americaItem);
rootNode-> appendRow(europeItem);
americaItem-> appendRow(mexicoItem);
americaItem-> appendRow(usaItem);
usaItem-> appendRow(bostonItem);
europeItem-> appendRow(italyItem);
italyItem-> appendRow(romeItem);
italyItem-> appendRow(veronaItem);
//register the model
treeView->setModel(standardModel);
treeView->expandAll();
//selection changes shall trigger a slot
QItemSelectionModel *selectionModel= treeView->selectionModel();
connect(selectionModel, SIGNAL(selectionChanged (const QItemSelection &, const QItemSelection &)),
this, SLOT(selectionChangedSlot(const QItemSelection &, const QItemSelection &)));
}
视图管理单独选择模型中的选项,可以使用selectionModel()方法检索。 我们检索选择模型,以便将槽函数连接到其selectionChanged()信号。
(文件来源:examples / widgets / tutorials / modelview / 7_selections / mainwindow.cpp)
void MainWindow::selectionChangedSlot(const QItemSelection & /*newSelection*/, const QItemSelection & /*oldSelection*/)
{
//get the text of the selected item
const QModelIndex index = treeView->selectionModel()->currentIndex();
QString selectedText = index.data(Qt::DisplayRole).toString();
//find out the hierarchy level of the selected item
int hierarchyLevel=1;
QModelIndex seekRoot = index;
while(seekRoot.parent() != QModelIndex())
{
seekRoot = seekRoot.parent();
hierarchyLevel++;
}
QString showString = QString("%1, Level %2").arg(selectedText)
.arg(hierarchyLevel);
setWindowTitle(showString);
}
我们通过调用treeView->selectionModel()->currentIndex()获得与选择相对应的模型索引,并通过使用模型索引获得字段的字符串。然后我们只计算项目的层次级别。顶级项没有父级,parent()方法将返回默认构造的QModelIndex()。这就是为什么我们使用parent()方法迭代到顶层,同时计算迭代期间执行的步骤。
可以检索选择模型(如上所示),但也可以使用QAbstractItemView::setSelectionModel进行设置。因为只使用了一个选择模型的一个实例,所以有3个视图类可以进行同步选择。要在3个视图之间共享选择模型,请使用SelectionModel(),并使用SetSelectionModel()将结果分配给第二个和第三个视图类。
使用Model/View的典型方法是包装特定数据,使其可用于视图类。然而,Qt还为常见的底层数据结构提供了预定义的模型。如果可用的数据结构之一适合您的应用程序,那么预定义的模型是一个不错的选择。
QStringListModel | 存储字符串列表 |
QStandardItemModel | 存储任意层次项目 |
QFileSystemModel、QDirModel | 封装本地文件系统 |
QSqlQueryModel | 封装SQL结果集 |
QSqlTableModel | 封装SQL表 |
QSqlRelationalTableModel | 封装带外键的SQL表 |
QSortFilterProxyModel | 排序和/或筛选其他模型 |
在目前为止的所有示例中,数据在单元格中显示为文本或复选框,并作为文本或复选框进行编辑。 提供这些演示和编辑服务的组件称为委托。 我们才刚刚开始使用委托,因为视图使用默认委托。 但是想象一下,我们想要一个不同的编辑器(例如,滑块或下拉列表)或想象我们想要将数据呈现为图形。 让我们来看一个名为Star Delegate的例子,其中星星用于显示评级:
视图有一个setItemDelegate()方法,该方法替换默认委托并安装自定义委托。可以通过创建继承自QStyledItemDelegate的类来编写新委托。为了编写一个显示星型并且没有输入功能的委托,我们只需要重写2个方法。
class StarDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
StarDelegate(QWidget *parent = 0);
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const;
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const;
};
paint() 根据基础数据的内容绘制星号。可以通过调用index.data()来查找数据。委托的sizeHint()方法用于获取每个星的尺寸,因此单元将提供足够的高度和宽度来容纳星。
如果要在视图类的网格中使用自定义图形表示来显示数据,则编写自定义委托是正确的选择。如果您想离开网格,您将不会使用自定义委托,而是使用自定义视图类。
其他参考:
模型的被动性为程序员提供了新的挑战。模型中的不一致可能导致应用程序崩溃。由于模型受到视图中无数调用的影响,很难找出哪个调用使应用程序崩溃,以及哪个操作引入了问题。
QtLabs提供了一个名为ModelTest的软件,它在您的编程运行时检查模型。每次模型更改时,ModelTest都会扫描模型并使用断言报告错误。这对于树模型尤其重要,因为它们的层次结构特性会留下许多细微不一致的可能性。
与视图类不同,ModelTest使用超出范围的索引来测试模型。这意味着您的应用程序可能会与ModelTest一起崩溃,即使没有它也能完美运行。因此,在使用ModelTest时,还需要处理超出范围的所有索引。