Qt Model/View模式

Model/View  结构

Qt 4推出了一组新的item view类,它们使用model/view结构来管理数据与表示层的关系。

Model-View-Controller(MVC), 是从Smalltalk发展而来的一种设计模式,常被用于构建用户界面。经典设计模式的著作中有这样的描述:

MVC 由三种对象组成。Model是应用程序对象,View是它的屏幕表示,Controller定义了用户界面如何对用户输入进行响应。在MVC之前,用户界面设计倾向于三者揉合在一起,MVC对它们进行了解耦,提高了灵活性与重用性。

 

model与数据源通讯,并提供接口给结构中的别的组件使用。通讯的性质依赖于数据源的种类与model实现的方式。

view从model获取model indexes,后者是数据项的引用。

delegate会对数据项进行渲染,当某个数据项被选中时,delegate通过model indexes与model直接进行交流。

models,views,delegates之间通过信号,槽机制来进行通讯:

从model发出的信号通知view数据源中的数据发生了改变。

从view发出的信号提供了有关被显示的数据项与用户交互的信息。

从delegate发生的信号被用于在编辑时通知model和view关于当前编辑器的状态信息。

Models

model为view和delegates使用数据提供了标准接口。

所有的item models都基于QAbstractItemModel类,这个类定义了用于views和delegates访问数据的接口。

数据本身不必存储在model,数据可被置于一个数据结构或另外的类,文件,数据库,或别的程序组件中。

不管数据在底层以何种数据结构存储,QAabstractItemModel的子类会以层次结构的形式来表示数据,结构中包含了数据项表。我们按这种约定来访问model中的数据项,但这个约定不会对如何显示这些数据有任何限制。数据发生改变时,model通过信号槽机制来通知关联的views。

 

Qt提供的一些现成的Models用于处理数据项的:

QStringListModel 用于存储简单的QString列表。

QStandardItemModel 管理复杂的树型结构数据项,每项都可以包含任意数据。

QDirModel  提供本地文件系统中的文件与目录信息。

QSqlQueryModel, QSqlTableModel,QSqlRelationTableModel用来访问数据库。

假如这些标准Model不满足你的需要,你应该子类化QAbstractItemModel,QAbstractListModel或是QAbstractTableModel来定制。

 

 

Model Indexes

为了使数据存储与数据访问分开,引入了model index的概念。通过model index,可以引用model中的数据项,Views和delegates都使用indexes来访问数据项,然后再显示出来。

model indexes提供了对一项数据信息的临时引用,通过它可以访问或是修改model中的数据。model有时会重新组织内部的数据结构,这时model indexes便会失效,因此不应该保存临时的model indexes

数据信息的长期引用,可使用persistent model index,这个会自动保持更新。

行与列

通过指定model中的行列数来获取任一项数据,可以得到与数据项一一对应的那个index。

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

通过Index.data即可获取对应的数据。也可通过model->data(index, Qt::DisplayRole).toString();获取。

一个model的顶级项(根项),由QModelIndex()取得。

父项

在model外部,引用一个数据项的唯一方法就是通过model index,因此需要在求取model index时指定父项的信息。

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

 

上图中,A项和C项作为model中顶层的兄弟项:

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

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

A有许多孩子,它的一个孩子B用以下代码获取:

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

项角色

 

model中的项可以作为各种角色来使用,这允许为不同的环境提供不同的数据。

每个数据项都可以为许多不同的角色提供数据,标准的角色在Qt::ItemDataRole中定义。我们可以通过指定model index与角色来获取我们需要的数据:

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

概念总结:

1、Model indexes为views与delegages提供model中数据项定位的信息,它与底层的数据结构无关。

2、通过指定行,列数,父项的model index来引用数据项。

3、依照别的组件的要求,model indexes被model构建。

4、使用index()时,如果指定了有效的父项的model index,那么返回得到的model index对应于父项的某个孩子。

5、使用index()时,如果指定了无效的父项的model index,那么返回得到的model index对应于顶层项的某个孩子。

6、角色对一个数据项包含的不同类型的数据给出了区分。

使用Model Indexes

QDirModel *model = new QDirModel;

     QModelIndex parentIndex = model->index(QDir::currentPath());

     int numRows = model->rowCount(parentIndex);

 for (int row = 0; row < numRows; ++row)

 {

         QModelIndex index = model->index(row, 0, parentIndex);

         tring text = model->data(index, Qt::DisplayRole).toString();

         // Display the text in a widget.

     }

以上的例子说明了从model中获取数据的基本原则:

1、model的行列数可以从rowCount()与columnCount()中得出。这些函数通常都需要一个表示父项的model index。

2、model indexes用来从model中访问数据项,数据项用行,列,父项model index定位。

3、 为了访问model顶层项,可以使用QModelIndex()指定。

4、数据项为不同的角色提供不同的数据。为了获取数据,除了model index之外,还要指定角色。

 

Views

view从model中获得数据项然后显示给用户。view负责管理从model中读取的数据的外观布局。

不同的view都完整实现了各自的功能:QListView把数据显示为一个列表,QTableView把Model 中的数据以table的形式表现,QTreeView 用具有层次结构的列表来显示model中的数据。这些类都基于QAbstractItemView抽象基类,尽管这些类都是现成的,完整的进行了实现,但它们都可以用于子类化以便满足定制需求。

它们自己可以去渲染每个数据项,也可以利用delegate来既处理渲染又进行编辑。

一个view创建时必不需要model,但在它能显示一些真正有用的信息之前,必须提供一个model。

选择项

view通过使用selections来跟踪用户选择的数据项。每个view可以维护单独使用的selections,也可以在多个views之间共享。

标题

从 model中获取数据的函数是QabstractItemModel::headerDate()。可派生QHeaderView来定制自己得标题。

 

实操

void MainWindow::stepUI()
{
    QSplitter *oSplitter = new QSplitter(this);
    QDirModel *oModel = new QDirModel();
    QTreeView *oTreeView = new QTreeView(oSplitter);
    //配置一个view去显示model中的数据,只需要简单地调用setModel(),并把目录model作为参数传递
    oTreeView->setModel(oModel);
    //setRootIndex()告诉views显示哪个目录的信息,这需要提供一个model index,然后用这个
    //model index去model中去获取数据
    //index()这个函数是QDirModel特有的,通过把一个目录做为参数,得到了需要的model index
    //其他的代码只是窗口show出来,进入程序的事件循环就好了
    oTreeView->setRootIndex(oModel->index(QDir::currentPath()));
    oSplitter->setWindowTitle("show DIr");
 
    QTableView *oTableView = new QTableView(oSplitter);
    oTableView->setModel(oModel);
    oTableView->setRootIndex(oModel->index(QDir::currentPath()));
 
    this->setCentralWidget(oSplitter);
}

 

几个要点:

1、         同一个model可以绑定多个view

2、         多个view之间共享选择:oTableView ->setSelectionModel(oTreeView ->selectionModel());

Delegates

既提供输入功能又负责渲染view中的每个数据项。

QAbstractItemDelegate 是model/view架构中的用于delegate的抽象基类。缺省的delegate实现在QItemDelegate类中提供。它可以用于Qt标准views的缺省 delegate.

 控制delegates的标准接口在QAbstractItemDelegate类中定义。Delegates通过实现paint()和sizeHint()以达到渲染内容的目的。

QT提供的delegate

简单的基于widget的delegates,可以从QItemDelegate子类化,而不是QAbstractItemDelegate,这样可以使用它提供的上述函数的缺省实现。delegate可以使用widget来处理编辑过程,也可以直接对事件进行处理。

Qt提供的标准views都使用QItemDelegate的实例来提供编辑功能。

通过view获取delegate:可以用itemDelegate()函数取得,而setItemDelegate() 函数可以设置一个定制delegate。

实操:使用QSpinBox提供编辑功能

class SpinBoxDelegate : public QItemDelegate
{
    Q_OBJECT
public:
    SpinBoxDelegate(QObject *parent = 0);
 
    // 获取编辑方式的窗体
    QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const;
    // 向编辑窗体里面写入数据
    void setEditorData(QWidget *editor, const QModelIndex &index) const;
    // 向模型写入数据
    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const Q_DECL_OVERRIDE;
 
    // 更新编辑器几何布局
    void updateEditorGeometry(QWidget *editor,
                              const QStyleOptionViewItem &option,
                              const QModelIndex &index) const;
};
①  delegate创建时不需要创建widget,只需要在真正使用的时候才需要创建,在createEditor中真正的调用。
QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QSpinBox *editor = new QSpinBox(parent);
    editor->setMinimum(0);
    editor->setMaximum(100);
    return editor;
}

此处我们不需要释放editor,因为view用完以后会释放。

②   将model中的数据同步到新创建的编辑器里面

void SpinBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
int value = index.model()->data(index, Qt::DisplayRole).toInt();
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->setValue(value);

}

③   当编辑完成,向model中提交数据

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);

}

标准的QItemDelegate类当它完成编辑时会发射closeEditor()信号来通知view。view保证编辑器widget关闭与销毁。

④   更新编辑器的位置,在编辑器创建或view尺寸或者位置变化的时候进行调用。

void SpinBoxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
editor->setGeometry(option.rect);
}
⑤  编辑完成提示
编辑完成以后会发送信号closeEditor来通知其他组件完成编辑和数据提交。我们也可以通过绑定信号closeEditor来进行其他操作。

 

排序

 

 

在model/view架构中,有两种方法进行排序:

1、 对model进行排序

①   编程实现:重新实现QAbstractItemModel::sort()函数

交互式实现:用户点击表头排序,把QHeaderView::sectionClicked()信号与QTableView::sortByColum()槽或QTreeView::sortByColumn()槽进行联结就好了。
QTableView中的sortByColumn方法,他有两个方法可调用,一个是槽函数:
void QTableView::sortByColumn(int column)

{

    Q_D(QTableView);

    if (column == -1)

        return;

    d->model->sort(column, d->horizontalHeader->sortIndicatorOrder());

}

另一个是公共函数:最终还是调用的槽函数。
void QTableView::sortByColumn(int column, Qt::SortOrder order)

{

    Q_D(QTableView);

    d->horizontalHeader->setSortIndicator(column, order);

    sortByColumn(column);

}

其实最终还是调用的model中的排序。

2、  通过代理model在view表示数据之前对model数据结构进行转换。

QT提供的常用类

QListWidget,QTreeWidget,QTableWidget这些类qt建议不要去进行派生子类。