(三十六)Model/View Programming (二)

来源:互联网 发布:微博数据集下载 编辑:程序博客网 时间:2024/05/02 04:19

四、Delegate类

1、Delegate类简介

    不同于MVC模式,模型/视图设计并不包含用于处理与用户交互的完全独立的组件。 通常,视图负责把模型数据显示给用户,以及处理用户的输入。为了让这种输入有灵活性,这种交互由Delegates来完成。这些组件在视图中提供输入功能,同时负责传递视图中的单个项。控制Delegates的标准接口在 QAbstractItemDelegate类中定义。

    QQAbstractItemDelegate则是所有Delegate的抽象基类。自Qt 4.4之后,默认的Delegate实现是QStyledItemDelegate。但QStyledItemDelegate和QItemDelegate都可以作为视图的编辑器,二者的区别在于,QStyledItemDelegate使用当前样式进行绘制。在实现自定义委托时,推荐使用 QStyledItemDelegate作为基类。

    Delegates通过实现 paint() 和 sizeHint()函数来传递他们本身的内容。但是,简单的基于组件的Delegates可以子类化QItemDelegate 类而不是 QAbstractItemDelegate类,这样就可以利用这些函数的默认实现。

    Delegates的编辑器可以通过使用组件来管理编辑的过程来实现,也可以通过直接处理事件来实现。第一种方法在这一节的后面会讲到,在Spin Box Delegate这个例子中也是使用的这种方法。
    Pixelator这个例子演示了如何建立一个专门用于表格视图的自定义委托。

2、Delegate使用

    QT提供的标准视图使用QItemDelegate的实例提供编辑功能。Delegates接口的默认实现以通常的样式将项传递给每一个标准视图:QListView, QTableView, 和 QTreeView。所有的标准角色都由标准视图的默认Delegates处理。

    视图所使用的Delegates可以用itemDelegate()函数返回。setItemDelegate()函数允许你为一个标准视图安装一个自定义的Delegates,当为自定义的视图设定一个Delegates时必须要用到这个函数。

    下面要实现的Delegates用一个QSpinBox来提供编辑功能,主要是想用于显示整数的Model。虽然我们基于这个目的建立了一个基于整数的自定义模型,但是我们可以很容易地以QStandardItemModel代替,因为自定义委托控制数据的输入。我们构造一个表格视图以显示模型里的内容,同时会使用自定义的Delegates来进行编辑。


    我们用QItemDelegate类来子类化这个Delegate,因为我们不想去写那些自定义的显示函数。然而,我们必须提供下面的函数以管理编辑器组件:

classSpinBoxDelegate : public QItemDelegate 
{     
      Q_OBJECT  
      public:    SpinBoxDelegate(QObject *parent = 0); 
      QWidget *createEditor(QWidget *parent,const QStyleOptionViewItem &option, const QModelIndex &index)const; 
      void setEditorData(QWidget *editor, constQModelIndex &index) const; 
      void setModelData(QWidget *editor, QAbstractItemModel *model, 
                      const QModelIndex &index) const; 
      void updateEditorGeometry(QWidget *editor,const QStyleOptionViewItem &option, const QModelIndex &index)const; 
};

    注意,构建Delegate时编辑器组件是没有建立的。只有需要时我们才构建一个编辑器组件。

3、提供编辑器

    当表格视图需要提供编辑器时,就向Delegate请求提供一个适合当前被修改项的编辑器组件。createEditor()函数提供了Delegate用于建立一个合适组件需要的所有东西:

QWidget *SpinBoxDelegate::createEditor(QWidget *parent,

    const QStyleOptionViewItem &/*option */,

    const QModelIndex &/*index */) const

{

    QSpinBox *editor= new QSpinBox(parent);

    editor->setMinimum(0);

    editor->setMaximum(100);

 

    returneditor;

}

    注意,我们不需要保留一个指向编辑器组件的指针,因为当不再需要编辑器的时候,视图会负责销毁它。
    我们把Delegate默认的事件过滤器安装在编辑器上,以确保它提供用户所期望的标准编辑快捷键。额外的快捷键也可以增加到编辑器上以允许更复杂的行为。

    通过调用我们稍后定义的函数,视图确保能正确地设定编辑器的数据和几何尺寸大小。根据视图提供的模型索引,我们可以建立不同的编辑器。比如,我们有一列数据是整数,一列数据是字符,那根据当前被编辑的列,我们可以返回一个SpinBox 或 QLineEdit。
    Delegate必须提供一个函数以便将Model数据复制到编辑器里。在这个例子中,我们读取储存在displayrole里的数据,并相应的把这个值设定在编辑器spin box中。

voidSpinBoxDelegate::setEditorData(QWidget *editor,

                                    const QModelIndex &index)const

{

    intvalue = index.model()->data(index, Qt::EditRole).toInt();

 

    QSpinBox *spinBox= static_cast<QSpinBox*>(editor);

    spinBox->setValue(value);

}

    在这个例子中,我们知道编辑器组件是一个spin box,但是在Model中,我们可能会因应不同的数据类型提供不同的编辑器,在存取它的成员函数前,我们需要将这个组件转换成适合的类型。

4、提交数据给模型

    当用户在spin box中完成编辑数值时,通过调用setModelData()函数,视图要求Delegate将被编辑后的值储存到Model中。

voidSpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,

                                   const QModelIndex &index)const

{

    QSpinBox *spinBox= static_cast<QSpinBox*>(editor);

    spinBox->interpretText();

    intvalue = spinBox->value();

 

    model->setData(index,value, Qt::EditRole);

}

    由于视图为Delegate管理编辑器组件,所以我们只要以编辑器提供的内容更新模型。在这个例子中,我们确保spinbox的内容是最新的,同时通过指定的index把spinbox包含的值更新到模型中。

    当Delegate完成编辑时,标准的QItemDelegate类会发出closeEditor()信号通知视图。视图会确保关闭和销毁编辑器组件。在这个例子中,我们只提供简单的编辑功能,所以我们不需要发出这个信号。

    所有的数据操作都是通过QAbstractItemModel提供的接口进行的。这使得Delegate最大程度地独立于它所操控的数据类型。但是为了使用某些类型的编辑器组件,一些假设是必须要做的。在这个例子中,我们假设Model包含的数据全部是整数值,但是我们仍然可以将这个Delegate用于不同种类的Model,因为QVariant可以为没有考虑到的数据提供合理的默认值。

5、更新编辑器几何外形

    编辑器的几何外形由Delegate负责管理。当创建编辑器时,以及视图中项的大小及位置改变时,都必须设定编辑器的几何外形。幸好,在视图中,View 选项对象中提供了所有必需的几何尺寸信息。

voidSpinBoxDelegate::updateEditorGeometry(QWidget *editor,

    const QStyleOptionViewItem &option,const QModelIndex &/* index */) const

{

    editor->setGeometry(option.rect);

}

    在这个例子中,我们只使用了View选项提供的项矩形几何尺寸信息。传递多个要素项的Delegate不会直接使用项矩形。它会定位与项中的其它要素编辑器的位置。

6、编辑提示

    编辑后,Delegate应该为其他组件提供关于编辑过程结果的提示,同时提供协助后续编辑操作的提示。这可以通过发射closeEditor()信号以及一个合适的提示来实现。这个过程由默认的QItemDelegate事件过滤器负责,在构造spinbox时,事件过滤器就已安装了。

    Spinbox的行为可以稍作调整以使它更易用。在QItemDelegate提供的默认事件过滤器中,如果用户按回车来确认他们在spinbox中的选择,Delegate就把值提交给Model并关闭spinbox。通过在spinbox中安装自定义的的事件过滤器,可以改变这种行为,并提供适合我们需要的编辑提示。例如,我们可以用EditNextItem提示来发射closeEditor()信号,来自动地开始视图中下一个项的编辑。

    另一种不需要使用事件过滤器的方法就是提供我们自己的编辑器组件,或者是为了方便可以子类化QSpinbox。这种可选方法让我们对编辑器的行为有更多的控制,而这需要以写额外的代码作为代价。如果要自定义一个标准Qt编辑器组件的行为,在Delegate中安装事件过滤器通常是较容易的。
    Delegate不必一定要发射这些提示,但相对于那些发射提示以支持通用编辑动作的Delegate,他们跟程序的整合性就会降低,所起的作用也很小。

五、项视图的选择处理

1、项视图的选择处理简介

    项视图类中使用的选择模型较QT3中有了很多的改进,为基于Model/View架构的选择提供了更全面的描述。虽然项视图提供的处理选择的标准类已经是很充足,但可以创建专门的选择模型以适应自定义的模型和视图的需要。

    视图中关于选中项的信息存储在QItemSelectionModel类的一个实例中,用于保存数据项的模型索引信息,并且独立于任何视图。由于一个模型可以用于很多视图,所以项视图共用选择也是可行的,应用程序就可以以一致的方式显示多个视图。

    选择由多个选择范围组成。仅仅通过记录选中项的开始模型索引与结束模型索引范围,维护大量项的选择信息。为了描述选择,构建多个非连续选择数据项。

    选择是选择模型保留模型索引的集合。最近选择的数据项被称为current selection。即使在使用之后,应用程序可以通过使用某种类型的选择命令来修改选择的效果。

    当前项与选中项

在视图中,有一个当前项和一个选中项两种独立状态。一个项有可能同时是当前项与选中项。例如,当使用键盘导航时,视图负责确保当中总是有一个当前项。下面的表格列出了当前项与被选择项的不同之处:

Current Item

Selected Items

只能有一个当前项

可以有多个选中项

当前项会随着键盘或鼠标按钮点击而改变

当用户交互作用于项时,项选择状态的设定或者撤销要看几个预定义的模式而定—如单选,多选等

如果编辑键F2按下或选项被双击,当前项将被编辑

当前项被用作一个支撑点来定义一个选择或反向选择(或两者的混合)的范围

当前项由焦点矩形标注

选中的项由选择矩形标注

 

    在处理选择时,考虑使用QItemSelectionModel来记录一个项模型中所有项的选择状态是很有帮助的。只要建立一个选择模型,项的集合就可以选择,撤销选择,或者反向选择,而不需要知道哪些项已经被选择。任何时候都可以提取所有被选择项的索引,同时通过信号和槽机制,其他的组件也可以知道选择模型的变动。

2、选择模型的使用

    标准视图类提供了大多数应用程序都可以使用的默认选择模型。一个视图所使用的选择模型可以通过视图的selectionModel()函数获取,用setSelectionModel()函数可以在多个视图中共用同一选择模型,所以一般不需要构造新的选择模型。

    指定一个模型,同时为QItemSelection指定一对模型索引,就可以创建一个选择。使用索引指向指定模型中的项,并把解释成一个选中项方块中左上角和右下角的项。要把选择应用于Model里的项,必须把选择提交给选择模型。可以通过多种方法实现,每一种方法在显示选择模型的选择都有不同的效果。

    选择项

    为了演示选择的一些基本特征,我们构建了一个共有32个项的自定义表格模型的实例,并且用一个表格视图显示它的数据。

  TableModel *model = new TableModel(8, 4, &app);
  QTableView *table = new QTableView(0);
  table->setModel(model);
  QItemSelectionModel* selectionModel = table->selectionModel();

    提取视图的默认选择模型以备后用。我们不修改模型里的任何项,而是选择一些显示在视图左上角的项。要达到这个效果,我们要提取选择区域中左上角及右下角相应的模型索引:

  QModelIndex topLeft;
  QModelIndex bottomRight;
  topLeft = model->index(0, 0, QModelIndex());
  bottomRight = model->index(5, 2, QModelIndex());

    要选择模型中的这些项,并看到视图上相应的变化,我们需要构建一个选择对象,并把它应用于选择模型:

 QItemSelection selection(topLeft,bottomRight);
  selectionModel->select(selection,QItemSelectionModel::Select);

    通过使用一选择标识组合定义的命令就可以将选择应用于选择模型。在本例中,不管项的原来的状态是怎样,使用的标识会使选择对象记录的项都包含在选择模型中。选择的结果由视图显示。


    项的选择可以通过由选择标识定义的不同操作进行修改。由此而产生的选择结果可能有一个复杂的结构,但能通过选择模型被有效地呈现。

    读取选择状态

    储存在选择模型里的模型索引可以通过selectedIndexes()函数读取。selectedIndexes()函数返回一个未排序的模型索引列表,只要我们知道这些模型索引属于哪一个Model,就可以历遍这些选择项。

QModelIndexList indexes= selectionModel->selectedIndexes();

QModelIndex index;

foreach(index,indexes)

{

    QString text= QString("(%1,%2)").arg(index.row()).arg(index.column());

    model->setData(index,text);

}

    以上代码使用QT的foreach关键字来历遍及修改选择模型返回的索引所对应的项。

选择模型发射信号以表明选择的变动。这些信号通知其它组件关于项模型中整体选择及当前焦点项的改变。我们可以把selectionChanged()信号连接到一个槽,当选择改变时,就可以检查模型中被选择或撤销选择的项。这个槽的呼叫要用到两个QItemSelection对象:一个包含新选择项对应的索引列表,另一个包含新撤销选择项的对应索引列表。以下代码我们提供一个接受selectionChanged() 信号的槽,用一个字符串填满选择的项,并清除撤销选择项的内容。

voidMainWindow::updateSelection(constQItemSelection &selected,
     const QItemSelection&deselected)
{
     QModelIndex index;
     QModelIndexList items =selected.indexes();
     foreach (index, items) {
     QString text=QString("(%1,%2)").arg(index.row()).arg(index.column());
     model->setData(index, text);
     }
     items = deselected.indexes();
     foreach (index, items)
        model->setData(index,"");
}

    将currentChanged()信号连接到一个使用两个模型索引为参数的槽函数,我们就能够保持对当前焦点项的跟踪。这两个索引分别对应前一个焦点项和当前焦点项。下面的代码我们提供一个接受currentChanged()的槽,并使用提供的信息更新QMainWindow的状态栏:

voidMainWindow::changeCurrent(const QModelIndex ¤t,

    const QModelIndex &previous)

{

    statusBar()->showMessage(

        tr("Movedfrom (%1,%2) to (%3,%4)")

            .arg(previous.row()).arg(previous.column())

            .arg(current.row()).arg(current.column()));

}

    用户通过这些信号对选择进行监控,但是我们也可以直接更新选择模型。

    更新选择

    选择命令是通过QItemSelectionModel::SelectionFlag定义的一个选择标识组合来执行的。当任何一个select()函数被调用时,每一个选择标识就会通知选择模型应怎样更新选择项的内部记录。最常用的标识就是Select,它指示选择模型把指定的项记录为选中的状态。 Toggle标识使选择模型转换指定项的状态,选择原来没有选中的项,撤销对当前选中项的选择。Deselect标识撤销对指定项的选择。

    选择模型里的单个项可以通过建立一个选择项,然后把这些选择项应用到选择模型来更新。在下面的代码中,我们把第二个选择项应用于前面的表格模型,然后用Toggle指令来转换指定项的选择状态。

QItemSelection toggleSelection;
  topLeft =model->index(2, 1, QModelIndex());
  bottomRight =model->index(7, 3, QModelIndex());
  toggleSelection.select(topLeft, bottomRight);
  selectionModel->select(toggleSelection,QItemSelectionModel::Toggle);

这个操作的结果显示在下面的表格视图中,直观地显示了我们达到的效果:


    默认情况下,选择指令只对模型索引指定的单个项进行操作。然而,用于描述选择指令的标识可以跟额外的标识搭配使用,以改变整行和整列的选择。例如,如果用一个索引调用select(),但是跟一个有Select和Rows组合的指令使用,该项所在的整行都会被选择。下面的代码演示了Rows和 Columns的使用:

 QItemSelection columnSelection;

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

    bottomRight= model->index(0, 2, QModelIndex());

    columnSelection.select(topLeft,bottomRight);

    selectionModel->select(columnSelection,

    QItemSelectionModel::Select| QItemSelectionModel::Columns);

    QItemSelection rowSelection;

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

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

    rowSelection.select(topLeft,bottomRight);

    selectionModel->select(rowSelection,

    QItemSelectionModel::Select| QItemSelectionModel::Rows);

    虽然只指定了四个索引给选择模型,但Columns 和 Rows 选择标识的使用意味着选择了两列和两行。下图显示了这两个选择的结果:


    应用于本例模型的命令全部都是涉及累积模型中项的选择的。其实它还可以清除选择,或者是以新的选择替换当前的选择。

    要用一个新选择替换当前的选择,就要用Current标识跟其它选择标识搭配使用。使用这个标识的指令通知选择模型用select()函数里指定的模型索引集合来替换当前的模型索引集合。在开始增加新的选择前,如果要清除所有的选择,就要用Clear标识跟其它选择标识搭配使用。它有重设选择模型的索引集合的效果。

    选择模型的所有项

    为了选择模型里的所有项,必须为模型的每一层建立一个选择,以涵盖该层的所有项。我们通过一个给定的父索引并提取左上角和右下角相应的索引来实现这个操作。

QModelIndex topLeft= model->index(0, 0, parent);

QModelIndex bottomRight= model->index(model->rowCount(parent)-1,

        model->columnCount(parent)-1,parent);

    以这些索引和模型构建一个选择,到时相应的项就会在选择模型中被选中。

QItemSelection selection(topLeft,bottomRight);

selectionModel->select(selection, QItemSelectionModel::Select);

    对模型所有的层都要执行这样的操作,对于顶层项,我们以通常的方法定义父项索引:

QModelIndex parent= QModelIndex();

    对于等级层次类型的模型,使用hasChildren() 函数可以确定一个指定的项是否是另一层次项的父项。

六、自定义Model

    模型与视图的组件在功能上的分离,使得可以创建模型并能利用现有的视图。这种方法让我们可以使用标准的图形用户界面组件,如QListView, QTableView, 和QTreeView,来呈现多样化来源的数据。

    QAbstractItemModel类提供了一个足够灵活的接口,它支持以层次结构安排信息的数据源,为数据的插入,移动,修改或排序提供了可能性。它同时对拖放操作也提供支持。

    QAbstractListModel 和 QAbstractTableModel 类为较简单的无层次数据结构的接口提供支持,同时用作简单列表和表格模型的起点(模型基类)会比较容易使用。

    本节中,我们建立一个简单的只读模型来了解模型/视图结构的基本原则。然后我们改写这个简单模型使得用户可以修改项。
    对于更为复杂的模型,请看例子Simple Tree Model 关于子类化QAbstractItemModel的要求在 Model Subclassing Reference子类化模型参考的文档中会有更加详细的描述。

1、模型设计

    当为一个现有的数据类型创建一个新的模型时,考虑哪一种类型的模型适合于为数据提供接口是很重要的。如果数据结构可以用一个列表或表格的项来表示,可以子类化QAbstractListModel 或QAbstractTableModel,因为这两个类为很多函数提供了合适的默认实现。

    然而,如果底层的数据结构只能以层次树结构来表示,那就必须子类化 QAbstractItemModel类,在SimpleTree Model 例子中使用的就是这种方法。
    在本节中,我们实现一个基于字符串列表的简单模型,所以QAbstractListModel是一个理想的基类。

    无论底层的数据结构如何组织,在自定义的模型中提供QAbstractItemModel的接口函数是很好的思路,这可以很好的访问底层的数据结构。这样使得操作带数据的模型更容易,而且可以使用标准API与其他通用的Model/View组件进行交互。

2、只读模型

    我们这里实现的模型是一个简单的、无层次的、基于标准的QStringListModel类的只读数据模型。它包含一个QStringList字符串列表作为内部数据源,并且只实现所需要的东西作为功能性模型。为了使实现更加容易,我们子类化 QAbstractListModel,因为它为列表模型定义了合理的默认行为,并且有比QAbstractItemModel更简单的接口。

    当实现一个模型时,很重要的一点是QAbstractItemModel本身并不存储任何数据,只提供一个视图用于存取数据的接口。对于一个微型只读模型,只需要实现几个函数就可以,因为大多数接口都有默认的实现。类的声明如下:

classStringListModel :public QAbstractListModel
{
     Q_OBJECT

public:
     StringListModel(constQStringList &strings, QObject*parent = 0)
         :QAbstractListModel(parent),stringList(strings) {}

     int rowCount(constQModelIndex &parent =QModelIndex()) const;
     QVariant data(constQModelIndex &index, int role)const;
     QVariant headerData(intsection, Qt::Orientationorientation,
                        int role = Qt::DisplayRole)const;

private:
     QStringList stringList;
};

    除了模型的构造函数,我们只需要实现两个函数:rowCount()返回模型里的行数,data()返回指定模型索引对应项的数据。设计良好的模型同时还要实现headerData(),以便让树型或表格视图显示它们的表头信息。

    需要注意的是这是一个非层次结构的模型,所以我们不必担心父-子项关系。如果我们的模型是层次结构的,那我们还必须实现index() and parent()函数。字符串列表储存在内部的私有成员变量stringList中。

3、模型的维度

    我们想要模型的行数跟字符串列表中字符串的个数一样,在实现rowCount()函数时要记住这一点。

intStringListModel::rowCount(const QModelIndex &parent) const
{
     return stringList.count();
}

    因为模型是非层次结构的,因此我们可以忽略父项对应的模型索引。从QAbstractListModel派生出来的模型默认只保留一栏,所以我们不必重新实现 columnCount()函数。

4、模型的表头和数据

    对于视图中的项,我们要返回字符串列表中的字符串。data()函数就是负责返回对应索引参数项的数据的:

QVariant StringListModel::data(constQModelIndex &index, int role) const
{
     if (!index.isValid())
         return QVariant();
     if (index.row() >=stringList.size())
         return QVariant();
     if (role ==Qt::DisplayRole)
         returnstringList.at(index.row());
     else
         return QVariant();
}

    如果提供的模型索引有效,行数在字符串列表的项范围内,且要求的角色是支持的,那我们只返回一个有效的QVariant。
    一些视图,如QTreeView和 QTableView,表头可以跟项数据一起显示。如果我们的模型显示在带表头的在视图中,我们想让表头显示行号和列号。我们可以通过子类化headerData() 函数来提供表头的相关信息。

QVariantStringListModel::headerData(intsection, Qt::Orientation orientation,
                    introle) const
{
     if (role !=Qt::DisplayRole)
         return QVariant();
     if (orientation ==Qt::Horizontal)
         returnQString("Column%1").arg(section);
     else
         returnQString("Row%1").arg(section);
}

    再次强调,只有是我们支持的角色才返回一个有效的QVariant。当要确定返回的确切数据时,表头的方向也是要考虑的。并不是所有的视图在显示项数据时都显示表头,那些不显示表头的视图可能已设定了隐藏表头。但是,我们还是建议实现headerData()函数以便提供模型数据的相关信息。

    一个项可以有几个角色,依据指定的角色可以给出不同的数据。在我们现在创建的模型中,项只有一个角色DisplayRole,所以不管指定什么角色,我们都返回角色指定相应项的数据。然而,我们可以重新利用DisplayRole角色提供的数据用在其它角色上,如ToolTipRole角色,视图可以以工具提示的形式显示项的信息。

5、可编辑模型

    只读模型显示了如何向用户呈现简单的选项,但是对于大多数的应用程序,一个可编辑的列表模型会更有用。我们可以通过修改为只读模型而实现的 data()函数,以及实现另外两个函数flags()和 setData(),来把只读模型改为可以编辑项的模型。下面的函数声明要加到类定义中:

Qt::ItemFlagsflags(constQModelIndex &index) const;
  bool setData(constQModelIndex &index, const QVariant&value,
                  introle =Qt::EditRole);

6、可编辑模型的标识

    在创建编辑器之前,委托会检查项是否可以编辑。模型必须让委托知道它的项是可以编辑的。我们可以通过为模型里的每一个项返回正确的标识来实现这个要求。在这个例子中,我们把所有的项激活并把它们设定为可选择及可编辑的:

Qt::ItemFlagsStringListModel::flags(constQModelIndex &index) const
{
     if (!index.isValid())
         returnQt::ItemIsEnabled;
     returnQAbstractItemModel::flags(index) | Qt::ItemIsEditable;
}

    我们不需要知道委托怎样执行实际的编辑过程。我们只需要为委托提供一个在模型中设定数据的方法。这个方法可以通过setData()函数来实现:

boolStringListModel::setData(const QModelIndex &index,
               constQVariant &value, int role)
{
     if (index.isValid()&& role == Qt::EditRole) {
        stringList.replace(index.row(),value.toString());
         emitdataChanged(index, index);
         return true;
     }
   return false;
}

    在这个模型中,对应模型索引的字符串项被提供的值所代替。然而,在我们修改字符串列表之前,我们必须保证索引是有效的,项是正确的类型,并且角色是支持的角色。通常,角色我们要用EditRole,因为它是标准项委托所使用的角色。然而,对于逻辑值,可以使用角色 Qt::CheckStateRole,并设定标识为Qt::ItemIsUserCheckable;这样就可以用一个复选框来编辑这个值。对于所有的角色,模型中的底层数据都是一样的,这只是为了让模型与标准组件结合使用时更加容易。

    当设定数据以后,模型必须要让视图知道某些数据已经被改动。这个可以通过发射 dataChanged()信号来完成。因为只有一个项的数据有更改,所以信号中所指定的项范围仅限于一个模型索引。
    同时,data()函数也要更改以增加对Qt::EditRole角色的检测

QVariantStringListModel::data(constQModelIndex &index, int role) const
{
     if (!index.isValid())
         return QVariant();
     if (index.row() >=stringList.size())
         return QVariant();
     if (role ==Qt::DisplayRole || role == Qt::EditRole)
        returnstringList.at(index.row());
     else
         return QVariant();
}

7、插入、删除行

    在一个模型中,行数和列数是可以改变的。在字符串列表模型中,只有行数改变有意义,所以我们只需要重新实现插入和删除行的函数。这些函数在类定义中的声明如下:

boolinsertRows(intposition, int rows, const QModelIndex &index = QModelIndex());
  bool removeRows(intposition, int rows, const QModelIndex &index= QModelIndex());

    由于字符串列表模型中行数对应的字符串列表中的字符串数量,所以insertRows()函数在字符串列表中指定的位置之前插入几个空的字符串。插入的字符串的个数跟所指定的行数是相等的。

    父索引通常是用来决定行应加在模型的什么位置。在本例中,我们只有一个顶层的字符串列表,所以我们插入空字符串到这个列表就可以。

boolStringListModel::insertRows(int position, int rows, constQModelIndex&parent)
{
     beginInsertRows(QModelIndex(),position,position+rows-1);
     for (int row = 0; row< rows; ++row) {
        stringList.insert(position,"");
     }
     endInsertRows();
     return true;
}

    模型首先调用beginInsertRows()函数以通知其它组件行数将要改变。这个函数指定了所插入行的首行和末尾行的行号,以及父项的模型索引。当改变了字符串列表后,再调用endInsertRows()函数以完成操作并通知其它组件模型的维度已经改变,同时返回true值表明插入行的操作成功。

    从模型中删除行的函数写起来也很简单。通过给出位置和行数就可以从模型中删除指定的行。为了简化我们的实现,我们忽略了父索引,只从字符串列表中删除对应的项。

boolStringListModel::removeRows(int position, int rows, constQModelIndex&parent)
{
    beginRemoveRows(QModelIndex(), position,position+rows-1);
     for (int row = 0; row< rows; ++row) {
        stringList.removeAt(position);
     }
     endRemoveRows();
     return true;
}

beginRemoveRows()函数必须要在删除任何底层数据之前调用,并且要指定删除行的首行和末尾行。这样就可以让其它组件在数据失效前存取数据。行被删除后,模型发射endRemoveRows()信号以完成操作,并通知通知其它组件模型的维度已经改变。

下一步

    通过使用QListView类将模型的项展现在一个垂直列表表格中,我们就可以显示这个模型或其它模型提供的数据。对于字符串列表模型,视图同时也提供了一个默认的编辑器,以便对项进行操作。

    在模型子类化参考这节文档中详细地讨论了子类化QAbstractItemModel的要求,同时为那些必须实现的虚函数提供指引,以便在不同类型的模型中赋予各种特性。

七、项视图便利类

    Qt4 同时也引入了一些标准组件以提供传统的基于项的容器组件。这些组件的作用跟Qt3里的项视图类相似,但为了使用底层的模型/视图架构以改善性能以及方便维护,已经对这些类进行了重写。这些旧的项视图类在兼容库中仍然是可用的。

    这些基于项的组件已经按他们的用途进行命名:QListWidget提供项的一个列表,QTreeWidget显示多层次树形结构, QTableWidget提供单元格项的一个表格。每一个类都继承基类 QAbstractItemView的行为,这个基类实现了项选择和表头管理等一些常用的行为。

1、列表组件

    单层次列表的项用一个 QListWidget和一些QListWidgetItems来显示。列表组件的构造跟任何其它组件一样:

QListWidget*listWidget = new QListWidget(this);

    列表项可以在构造的时候直接的添加到列表组件中:

new QListWidgetItem(tr("Sycamore"),listWidget);
  new QListWidgetItem(tr("Chestnut"),listWidget);
  new QListWidgetItem(tr("Mahogany"),listWidget);

    列表项在构造时也可以不指定父列表组件,而是在之后添加到列表中:

QListWidgetItem*newItem = new QListWidgetItem;
  newItem->setText(itemText);
  listWidget->insertItem(row, newItem);

    列表里的每个项都可以显示文字标识和一个图标。表现文字的颜色和字体也可以可以改变,以便为项提供一个自定义的外观。工具提示, 状态栏提示, 以及“这是什么”等帮助功能都可以很容易地设定,以确保这个列表可以完全地跟应用程序融合在一起。

newItem->setToolTip(toolTipText);
  newItem->setStatusTip(toolTipText);
  newItem->setWhatsThis(whatsThisText);

    默认情况下,列表里的项按照他们创建时的顺序来显示。列表项可以按照Qt::SortOrder指定的规则进行排序,以产生一个按升序或降序排列的列表:

listWidget->sortItems(Qt::AscendingOrder);
  listWidget->sortItems(Qt::DescendingOrder);

2、树形组件

    树形或层次结构型的列表项由QTreeWidget和QTreeWidgetItem类提供。树形组件里的每个项都可以有它们自己的子项,同时也可以显示多列信息。树形组件的创建跟其它组件一样:

QTreeWidget*treeWidget = new QTreeWidget(this);

    在把项加到树形组件之前,必须先设定列数。例如,我们可以定义两列,然后创建一个表头,以便在每一列的顶部提供一个标识:

treeWidget->setColumnCount(2);
  QStringList headers;
  headers<< tr("Subject") <<tr("Default");
  treeWidget->setHeaderLabels(headers);

    为每一列建立标识最容易的方法就是提供一个字符串列表。对于更复杂的表头,可以构造一个树形项,按照你的要求进行布置,并把它作为树形组件的表头来使用。

    树形组件的顶级项以树形组件作为它们的父组件来构造。它们可以以任意的顺序插入,或者在构造每个项时,你可以通过指定前一个项来确保以特定的顺序列出这些项:

 QTreeWidgetItem*cities = new QTreeWidgetItem(treeWidget);
 cities->setText(0,tr("Cities"));
 QTreeWidgetItem *osloItem = new QTreeWidgetItem(cities);
 osloItem->setText(0, tr("Oslo"));
 osloItem->setText(1, tr("Yes"));
 QTreeWidgetItem *planets = new QTreeWidgetItem(treeWidget, cities);

    树形组件处理顶层项的方法跟处理其它层次的项的方法稍有不同。树形组件的顶层项可以通过调用树形组件的takeTopLevelItem()函数来进行删除,而其它低层次的项要通过调用它们父项的 takeChild()函数来进行删除。顶层项的插入用insertTopLevelItem()函数,而其它层次的项要用父项的insertChild()函数来插入。

    在树型组件的顶层项及其它层级的项之间移动是很容易的。我们只需检测这些项是否是顶层项,这个信息由每个项的parent()函数提供。比如,我们可以删除树形组件里的当前项,而不用去管它的具体位置是什么:

QTreeWidgetItem*parent = currentItem->parent();
  intindex;
  if(parent) {
     index =parent->indexOfChild(treeWidget->currentItem());
     delete parent->takeChild(index);
  } else{
     index=treeWidget->indexOfTopLevelItem(treeWidget->currentItem());
     delete treeWidget->takeTopLevelItem(index);
     }

    把项插入到树形组件的某个位置也是使用一样的模式:

QTreeWidgetItem*parent = currentItem->parent();
  QTreeWidgetItem *newItem;
  if(parent)
     newItem = newQTreeWidgetItem(parent,treeWidget->currentItem());
  else
     newItem = new QTreeWidgetItem(treeWidget,treeWidget->currentItem());

3、表格组件

    表格组件跟电子表单程序里用到的相似,都是用QTableWidget和 QTableWidgetItem构造。它们提供一个带表头的可滚动表格,而把项置于其中来使用。
    在构建表格组件时可以指定行数和列数,未定义大小的表格组件也可以在需要的时候再加上行数和列数。

QTableWidget*tableWidget;
  tableWidget = new QTableWidget(12, 3, this);

项可以在添加到表格的指定位置之前,在表格之外单独构造:

QTableWidgetItem*newItem = new QTableWidgetItem(tr("%1").arg(pow(row, column+1)));
  tableWidget->setItem(row,column, newItem);

通过在表格外构造项,并把它们作为表头使用,就可以添加表格的水平和垂直表头:

QTableWidgetItem*valuesHeaderItem = newQTableWidgetItem(tr("Values"));
  tableWidget->setHorizontalHeaderItem(0, valuesHeaderItem);

注意,表格的行号和列号是从0开始的。

4、共有特性

    有几个基于项的特性是每个项视图简便类共有的,它们都可以通过每个类的相同接口来进行使用。接下来的章节中我们将通过使用不同组件的几个例子来说明这些特性。对于所使用到得函数,可以通过查看 Model/View Classes列表中的每个组件来获得更详细的内容。

隐藏项

    有时需要在一个项视图组件中隐藏项是有用的,而不是删除项。以上所有组件的项都可以隐藏和重现。可以通过调用isItemHidden()函数知道一个项是否被隐藏,同时用setItemHidden()函数可以将项隐藏。
因为这个操作是基于项的,所以相同的函数适用于所有便利类。

选择

    项的选择方式由组件的选择模式(QAbstractItemView::SelectionMode)控制。这个属性控制用户是否可以选择一个或多个项,如果是多项选择,选择是否必须是一个连续的项范围。以上所有组件的选择模式的工作方式都是一样的。

    单项选择:当用户需要在一个组件中选择一个单一项时,默认的SingleSelection模式是最适合的。这种模式的当前项和选中项是一样的。


    多项选择:使用这种模式,用户可以在不改变当前选择的情况下切换这个组件中任意项的选择状态,跟非排他性的复选框单独切换的方式很相似的。


    扩展选择:需要选择许多相邻项的组件,如电子制表,就要使用到扩展选择模式ExtendedSelection。使用这种模式,组件里连续范围的项可以同时用鼠标和键盘进行选择。如果使用修改键,也可以创建复杂的选择,如涉及到组件里跟其它选中的的项不相邻的多个项的选择。如果用户在选择一个项时没有使用修改键,那原来的选择就会被清除。


    组件中被选中的项可以用 selectedItems()函数来读取,它提供一个可以遍历的相关项的列表。例如,我们可以用下面的代码找出一个选择项列表中所有数值的总和:

QList<QTableWidgetItem*>selected = tableWidget->selectedItems();
  QTableWidgetItem *item;
  intnumber = 0;
  doubletotal = 0;
  foreach(item, selected)

{
    bool ok;
    double value = item->text().toDouble(&ok);
    if(ok && !item->text().isEmpty())

   {
      total += value;
      number++;
   }
 }

    对于单选模式,当前项是被选中的。而对于多选模式和扩展模式,当前项不一定在选择范围内,这取决于用户选择的方式。

 

搜索

    无论是作为一个开发人员还是作为一项面向用户的服务,能够在一个项视图中找出项是很有帮助的。所有3种项视图便利类提供了一个通用的findItems()函数,使得这个功能可以尽可能的一致和简单。
    项是以Qt::MatchFlags中的一个值所指定的规则,按照项的文字进行搜索的。我们可以用findItems()函数得到一个符合要求的项的列表。

QTreeWidgetItem *item;  QList<QTreeWidgetItem *> found = treeWidget->findItems(        itemText, Qt::MatchWildcard);  foreach(item, found) {     treeWidget->setItemSelected(item, true);     // Show the item->text(0) for each item.


  }

    上面的代码得到的结果是,树形组件中的项如果包含搜索字符串中的文字就会被选中。这个模式同样可以应用于列表组件和表格组件。

八、项视图的拖放

    模型/视图架构完全支持Qt的拖放基础。列表、表格、树形组件中的项可以在视图间拖动,数据可以以MIME类型的格式进行导入和导出。

    标准视图自动支持内部的拖放,他们的项可以被移动以改变他们的显示顺序。默认情况下,这些视图是不能进行拖放操作的,因为他们被设定成最简单最常用的用法。如果要拖动项,则要开启视图的一些属性,并且项本身也必须是允许拖动的。

    相比那些完全支持拖放的模型,只允许项从一个视图中导出而不允许数据放入到其中的模型是很少的。
    更多关于在新模型中启用拖放支持的内容,请看子类化模型参考这一章节。

1、便利类视图的使用

    QListWidget、QTableWidget和 QTreeWidget的每一种类型的项都默认配置有一套不同的标识。比如,QListWidgetItem或 QTreeWidgetItem的初始标识是启用的enabled, 可复选的checkable,可选择的selectable,同时可以作为拖放操作的数据源;QTableWidgetItem可以进行编辑操作,并且可以作为拖放操作的目标。

    虽然所有的标准项都有一或两个标识用于拖放操作,但是你还是要在视图本身设置不同的属性,利用内置的拖放支持。

l  要启用项的拖动,就要把视图的dragEnabled 属性设定为true

l  要允许用户将内部或外部项拖放到视图中,则要把视图的viewport()的 acceptDrops属性设定为true。

l  要显示当前拖动的项将被放在什么地方,则要设定视图的showDropIndicator属性。它会提供关于项在视图中的放置位置的持续更新信息。

    例如,我们可以用以下代码在列表组件中启用拖放功能。

QListWidget*listWidget= new QListWidget(this);
listWidget->setSelectionMode(QAbstractItemView::SingleSelection);
listWidget->setDragEnabled(true);
listWidget->viewport()->setAcceptDrops(true);
listWidget->setDropIndicatorShown(true);

    结果就得到一个可以让项在视图里进行复制的列表组件,甚至可以让用户在包含相同类型数据的视图间拖动项。这两种情况,项是被复制而不是移动。

    如果用户要在视图间移动项,那我们就要设定列表组件的拖放模式dragDropMode。

listWidget->setDragDropMode(QAbstractItemView::InternalMove);

2、Model/View类使用

    建立一个可以拖放的基于模型的视图跟简便类视图是一样的模式。例如,可以用建立QListWidget一样的方法建立一个QListView:

QListView*listView = newQListView(this);
listView->setSelectionMode(QAbstractItemView::ExtendedSelection);
listView->setDragEnabled(true);
listView->setAcceptDrops(true);
listView->setDropIndicatorShown(true);

    因为视图所显示的数据的存取是由模型控制的,所以模型也要提供对拖放操作的支持。模型支持的动作可以通过重新实现QAbstractItemModel::supportedDropActions()函数来指定。下面的代码实现复制和移动的操作:

Qt::DropActionsDragDropListModel::supportedDropActions()const
{
     return Qt::CopyAction |Qt::MoveAction;
}

    虽然可以指定一个Qt::DropActions里的值的任意组合,但是还是要写模型来支持他们。例如,为了让一个列表模型能正确地支持 Qt::MoveAction动作,模型必须重新实现QAbstractItemModel::removeRows()函数,不管是直接还是间接从他的 基类继承实现。

3、开启项的拖放

    通过重新实现QAbstractItemModel::flags()函数来提供合适的标识,模型指示视图哪些项可以拖动,哪些项接受放置。

    例如:通过确保返回的标识包含Qt::ItemIsDragEnabled 和 Qt::ItemIsDropEnabled,一个基于QAbstractListModel的简单列表模型就可以使每个项都可以拖放:

Qt::ItemFlagsDragDropListModel::flags(constQModelIndex &index) const
{
     Qt::ItemFlags defaultFlags=QStringListModel::flags(index);
     if (index.isValid())
         returnQt::ItemIsDragEnabled |Qt::ItemIsDropEnabled | defaultFlags;
     else
         returnQt::ItemIsDropEnabled |defaultFlags;
}

    注意,项可以被放置在模型的顶层,而拖动操作只对合法项有效。

    在以上的代码中,因为这个模型是从QStringListModel中衍生出来的,所以我们要调用它的flags()函数实现以包含一套默认的标识。

4、导出数据的编码

    在拖放操作中,当项的数据从一个模型中导出时,它们会被编码成对应一个或多个MIME类型的适当的格式。模型通过重新实现返回一个标准MIME类型的列表的QAbstractItemModel::mimeTypes()函数,来声明它们可以供项使用的MIME类型。
例如,一个只提供纯文本的模型要提供以下的实现:

QStringListDragDropListModel::mimeTypes()const
{
     QStringList types;
     types <<"application/vnd.text.list";
     return types;
}

    模型同时还要提供对公开格式的数据进行编码的代码。这个可以通过重新实现QAbstractItemModel::mimeData()函数提供一个QMimeData对象来实现,就像在其它的拖放操作里一样。

    以下代码的功能是索引参数相关的项数据将被编码为纯文本并被存储在QMimeData对象。

QMimeData*DragDropListModel::mimeData(constQModelIndexList &indexes) const
{
     QMimeData *mimeData = new QMimeData();
     QByteArray encodedData;
     QDataStream stream(&encodedData,QIODevice::WriteOnly);
     foreach (QModelIndex index, indexes) {
         if (index.isValid()){
            QString text =data(index, Qt::DisplayRole).toString();
             stream<<text;
         }
     }
    mimeData->setData("application/vnd.text.list",encodedData);
     return mimeData;
}

    由于向函数提供了一个模型索引的链表,所以在层次结构和非层次结构中使用这种方法一般来说是足够了的。
    注意,自定义的数据类型必须声明为meta objects,并且要为它们实现流操作。详细内容请看QMetaObject类里的描述。

5、向模型中插入释放的数据

    任何指定模型处理释放数据的方法要看它的类型(列表,表格或树形)以及它向用户显示其内容的方法而定。通常,累积释放数据的方法是最适合模型底层数据存储的方法。

    不同类型的模型会用不同的方法处理释放的数据。列表和表格模型只提供一个存储项的平面结构。因此,当数据被释放到视图中的一个现有的项上面时,它们可以插入新的行(和列),或者使用提供的数据覆盖掉模型里项的内容。树形模型一般是向他们的底层数据增加包含新数据的子项,因此它的行为会比用户所能想到的更有可预见性。

    释放数据的处理通过重新实现模型的QAbstractItemModel::dropMimeData()函数来实现。例如,一个处理简单字符串列表的模型可以提供一个实现来分别处理放置于现有项之上的数据以及放置于模型顶层的数据(例如,放置到一个无效的项上面)。

    模型首先要确保操作应该作用于的数据是以可用的格式提供的,并且它在模型里的目标是有效的:

boolDragDropListModel::dropMimeData(const QMimeData *data,
     Qt::DropAction action,int row, int column, constQModelIndex &parent)
{
     if (action ==Qt::IgnoreAction)
         return true;
     if(!data->hasFormat("application/vnd.text.list"))[/font]
         return false; 
     if (column > 0)
         return false;

    如果提供的数据不是纯文本,或给出的用于放下的列号是无效的,则这个简单的单列字符串列表模型可以将此操作标志为失败。

    根据数据是否被放置在一个现有的项上面作为判断,插入模型的数据将作不同的处理。在这个简单例子中,我们允许把数据放在现有项之间,列表第一个项之前,和最后一个项之后。

    当一个放下操作发生时,如果父项相对应的模型索引是有效的,意味着放下操作发生在一个项上面,如果是无效的,则意味着放下操作发生在视图中对应于模型顶层的某个位置。

intbeginRow;
     if (row != -1)
         beginRow = row;

    我们先检查指定的行号看它是否可以用来将项插入到模型中,不管父项的索引是否有效:

elseif (parent.isValid())
         beginRow =parent.row();

    如果父项索引是有效德尔,则放下操作发生在一个项上。在这个简单的列表模型中,我们找出项的行号,并用这个值把放下的项插入到模型的顶层。

else
    beginRow =rowCount(QModelIndex());

    当放下动作发生在视图的某个位置,同时行号又是不可使用的,那我们就把项添加在模型的顶层项。
    在层次结构模型中,当放下动作发生在一个项上时,把要插入的项作为该项的子项插入到模型中会更好。在这里讲的简单例子中,模型只要一层,因此这个方法是不适合的。

6、解码导入数据

    每个dropMimeData()的实现同时也必须对数据进行解码, 并把它插入到模型的底层数据结构中。
    对应一个简单的字符串列表模型,编码后的项可以被解码并汇入到 QStringList::

  QByteArrayencodedData =data->data("application/vnd.text.list");
     QDataStreamstream(&encodedData,QIODevice::ReadOnly);
     QStringList newItems;
     int rows = 0;
     while (!stream.atEnd()) {
         QString text;
         stream >> text;
         newItems <<text;
         ++rows;
     }

    字符串就可以插入到底层数据。为了保持一致性,可以通过模型自己的接口实现:

insertRows(beginRow,rows,QModelIndex());
     foreach (QString text,newItems) {
         QModelIndex idx=index(beginRow, 0, QModelIndex());
         setData(idx, text);
         beginRow++;
     }
     return true;
}

    注意,模型通常要提供 QAbstractItemModel::insertRows()函数和 QAbstractItemModel::setData()函数的实现。

九、代理模型

    在模型/视图架构中,单个模型提供的数据项可以共享于任意多个视图中,并且每个视图都可能以完全不同的方式显示相同的信息。自定义视图和委托是为相同数据提供根本不同的呈现方式的有效方法。然而,应用程序经常要提供常规的视图用到相同数据的不同处理版本上,如一个列表项的不同排序视图。

    虽然看上去好像适合作为视图的内部参数来执行排序和筛选操作,但是这种方法不允许多个视图共用这种潜在的代价高昂的操作所得的结果。另外一种方法,涉及在模型本身内部的排序,同样会导致相似的问题:每一个视图都必须显示根据最新的处理操作所组织的数据项。

    为了解决这个问题,模型/视图架构使用代理模型来管理单独模型和视图之间的信息。从视图角度看,代理模型是跟普通模型表现一样的组件,可以存取代表该视图的源模型的数据。不管有多少个代理模型放在视图和源模型之间,模型/视图架构使用的信号和槽会确保每一个视图都能及时的更新。

1、代理模型使用

    代理模型可以安插在一个现有的模型和任意数量的视图之间。Qt提供了一个标准的代理模型,QSortFilterProxyModel,它通常被实例化就可直接使用,但它也可以子类化以提供自定义的筛选和排序行为。QSortFilterProxyModel可以以下面的方式使用:

  QSortFilterProxyModel*filterModel = new QSortFilterProxyModel(parent);
     filterModel->setSourceModel(stringListModel);
     QListView *filteredView = new QListView;
     filteredView->setModel(filterModel);

    由于代理模型是从QAbstractItemModel继承而来的,所以它可以连接到各种视图,并共用于视图间。他们也可以通过一个管道途径的安排,用来处理从其它代理模型得到的信息。
    QSortFilterProxyModel类被设计为可实例化并直接在程序中使用。子类化这个类并实现所要求的比较操作,可以创建更多自定义的代理模型。

2、自定义代理模型

    通常情况下,代理模型所使用的处理形式涉及将每个项的数据从他的源模型原始位置映射到代理模型中的任意一个不同的位置。在有些模型中,一些项可能在代理模型中没有对应的位置;这些模型就是筛选代理模型。视图使用代理模型提供的模型索引存取项,以及那些在这个模型中没有包含源模型信息或源项位置的项。

    QSortFilterProxyModel 使得源模型的数据在提供给视图之前可以被筛选,同时允许源模型的数据以排好序的数据提供给视图。

3、自定义筛选模型

    QSortFilterProxyModel类提供了一个相当通用多变的筛选模型,可以用于各种常见得情况。对于高级使用者,可以子类化QSortFilterProxyModel来提供一个能够执行自定义筛选的机制。

    子类化QSortFilterProxyModel可以重新实现两个虚函数,当请求或使用代理模型的模型索引时要调用这两个函数:

l  filterAcceptsColumn()函数用于筛选源模型中指定的列

l  filterAcceptsRow()函数用于筛选源模型中指定的行

    以上两个QSortFilterProxyModel函数的默认实现返回true,以确保所有的项都可以传递给视图;重新实现这些函数应该返回false以筛选出单独的行和列。

4、自定义排序模型

    QSortFilterProxyModel 实例使用Qt内建的qStableSort()函数来建立源模型项和代理模型项之间的映射,在不改变源模型结构的情况下将一个排序后的项显示到视图上。为了提供自定义的排序行为,就要重新实现 lessThan()函数以执行自定义的比较。

5、自定义数据模型

    QIdentityProxyModel实例不会排序和过滤源模型的结构,但提供了一个数据代理的基类。在QFileSystemModel外,这对于不同文件的背景角色提供不同的颜色可能是有用的。

十、模型的子类化引用

模型子类化需要提供QAbstractItemModel基类定义的多个虚函数的实现。需要实现的函数数量取决于模型的类型,比如是否提供简单列表、表格、复杂层次项的视图。继承自QAbstractListModel和QAbstractTableModel的模型能够利用这两个基类默认的函数实现。以类树形结构显示数据项的模型必须提供QAbstractItemModel的多个函数实现。

在子类模型中需要实现的函数分为三组:

l  项数据处理:需要实现函数的所有模型需要使视图和委托能够查询模型的维度、检查项和获取数据

l  导航和索引创建:多层次模型需要提供视图能够导航显示树形结构的函数,并且获取项的模型索引

l  拖放支持和MIME类型处理:模型需要继承内外拖放操作控制方式的函数。这些函数允许其他组件和应用程序能够理解的MIME方式进行描述数据。

更多的信息,查看C++ GUI Programming with Qt 4的项视图类的章节

1、项数据处理

模型能够提供多种层次的数据访问方式,比如简单的只读组件、重绘操作支持、可编辑。

只读访问

    为了提供对模型数据的只读访问,必须对模型子类的以下函数重新实现。

    flags()用于其他组件获取模型的每个项信息。在大多数模型中,需要使用Qt::ItemIsEnabled和Qt::ItemIsSelectable的组合。

    data()用于提供视图和委托项数据。通常,模型只需要提供Qt::DisplayRole和任何应用程序用户指定的角色的数据,但提供Qt::ToolTipRole、Qt::AccessibleTextRole和Qt::AccessibleDescriptionRole角色的数据也是很好的练习。关于每个角色关联的类型信息在文档中查阅Qt::ItemDataRole枚举类型。

    headerData()为了显示表头,提供带信息的视图。这些信息只能由可以显示表头的视图获取。

    rowCount()提供模型中要显示的数据的行数。

    这四个函数必须在所有模型类型中实现,包括QAbstractListModel的子类列表模型和QAbstractTableModel的子类表格模型。

    此外,以下函数必须在QAbstractTableModel和QAbstractItemModel的直接子类中实现。

    columnCount()提供模型要显示的数据的列数。列表模型由于已经在QAbstractListModel中实现了,所以不需要提供这个函数。

可编辑项

可编辑模型允许数据项可以修改,提供插入和删除行和列的函数。为了开启可编辑,下面的函数需要正确实现。

    flags()必须返回每个项相近的标识组合。函数的返回值必须包含Qt::ItemIsEditable标识。

    setData()用于修改由模型索引指定的数据项。为了接受用户输入,这个函数需要处理和Qt::EditRole有关的数据。函数实现需要接受由Qt::ItemDataRole指定的多种角色关联的数据。在数据项改变后,模型必须发送dataChanged()信号通知变化的其他组件。

    setHeaderData()用于修改水平和垂直的表头信息。数据项改变后,模型必须发送headerDataChanged()通知变化的其他组件。

重绘模型

所有模型支持插入和删除行,表格模型和层次模型支持插入和删除列。在模型的维度变化前后通知其他组件是重要的。以下函数的实现是为了允许模型重绘。但函数实现必须确保合适的函数被调用通知相关的视图和委托。

    insertRows()用于在所有的模型中插入行和数据项。函数实现在插入新行到任何底层数据结构前必须调用beginInsertRows()函数,在插入行后必须立即调用endInsertRows()函数。

    removeRows()用于删除所有模型中包含的行和数据项。函数实现中在插入新的列到底层数据结构前需要调用beginRemoveRows()函数,插入新列后必须立即调用endRemoveRows()函数。

    insertColumns()用于在表格模型和层次模型中添加新的行和数据项。函数实现中在从任何底层数据结构中删除行之前需要先调用beginInsertColumns()函数,删除后必须立即调用endInsertColumns()函数。

    removeColumns()用于删除表格模型和层次模型中的列和数据项。函数实现中在从任何底层数据结构中删除列前必须调用beginRemoveColumns()函数,删除后必须立即调用endRemoveColumns()函数。

    通常,如果操作成功,这些函数应该返回true。但是,有一些操作部分成功的情况,比如插入的行数少于指定的插入行数。在这样的情况下,模型应该返回alse指示失败,使其他相关组件处理这种情况。

    在调用重绘API函数中发送的信号会给相关组件在数据不可用前采取措施的机会。开始和结束函数中对插入和删除的封装使模型能够正确管理永久模型索引。

    通常,开始和结束函数能通知其他组件有关模型底层结构的变化。对于模型结构的复杂变化,可能会涉及到内部数据的重组或排序,发送layoutChanged()信号引起所有相关的视图更新是必须的。

模型数据的惰性填充

    模型数据的惰性填充允许模型的信息请求延缓到实际视图需要时。

    有些模型需要从远程数据源获取数据,或是必须执行耗时的操作为了获取数据组织方式的信息。由于视图通常要求尽可能多的信息为了精确显示模型数据,为了减少不必要的重复数据请求,限制返回信息的数量是有用的。

    在层次模型找到所给项的孩子的数量是代价昂贵的操作,确保模型的rowCount()实现只在需要时调用是有用的。本例中,hasChildren()函数需要一种不昂贵的方式重新实现,QTreeView,为父项绘制适合的外观。

    hasChildren()函数重新实现是返回true还是false,为了弄清有多少个孩子显示而调用rowCount()函数对于视图来说并不需要。例如,如果父项没有扩展显示子项,QTreeView不必清楚有多少子项。

    如果知道有多少项有子项,重新实现hasChildren()函数中无条件返回true有时是可用的方法。当尽可能快地做模型数据的填充时,这可以确保每个项随后可以检查子项。缺点是在用户试图显示不存在的子项时,没有子项的项在有些视图中不能正确显示。

2、导航和模型索引创建

    为了导航显示的树形结构和获取项的模型索引,层次模型需要提供视图能够调用的函数。

    显示到视图的结构由底层数据结构决定,每个模型子类由通过提供以下函数的实现创建自身的模型索引决定。

    index() 通过父项的模型索引,函数允许视图和委托访问项的子项。如果没有合法的子项能被找到,函数必须返回QModelIndex()无效的模型索引。

    parent()提供给定子项的父项的模型索引,如果模型索引指定的是模型中的顶层项,或是模型中没有合法的父项,函数必须返回一个无效的模型索引。

3、拖放支持和MIME类型处理

    model/view类支持拖放操作,对于很多程序来说是提供有效的默认行为。然而,也可能是定义在拖放操作间项被编码的方式,他们默认是被拷贝还是移动,还是如何插入一个存在的模型中。

MIME data

默认情况下,内置模型和视图使用内部MIME类型(application / x-qabstractitemmodeldatalist)传递关于模型索引的信息。 它指定项列表的数据,包含每个项的行和列号,以及有关每个项支持的角色的信息。

 

使用此MIME类型编码的数据可以通过调用QAbstractItemModel :: mimeData()与包含要序列化的项的QModelIndexList获得。

当在自定义模型中实现拖放支持时,可以通过重新实现以下函数来导出特殊格式的数据项:

mimeData()此函数可以重新实现以返回非默认应用程序/ x-qabstractitemmodeldatalist内部MIME类型的格式的数据。子类可以从基类中获取默认的QMimeData对象,并以其他格式向其添加数据。

对于许多模型,提供由MIME类型(例如text / plain和image / png)表示的通用格式的项的内容是有用的。 请注意,可以使用QMimeData :: setImageData(),QMimeData :: setColorData()和QMimeData:: setHtml()函数轻松将图像,颜色和HTML文档添加到QMimeData对象

 

接受丢弃的数据

 

当在视图上执行拖放操作时,查询底层模型以确定它支持哪些类型的操作以及它可以接受的MIME类型。 此信息由QAbstractItemModel :: supportedDropActions()和QAbstractItemModel:: mimeTypes()函数提供。 不覆盖QAbstractItemModel提供的实现的模型支持项的复制操作和默认的内部MIME类型。

 

当将序列化项数据放到视图上时,使用QAbstractItemModel :: dropMimeData()的实现将数据插入到当前模型中。 此函数的默认实现将永远不会覆盖模型中的任何数据; 相反,它会尝试将数据项作为项的兄弟元素或该项的子元素插入。

 

要利用QAbstractItemModel的内置MIME类型的默认实现,新模型必须提供以下函数的重新实现:

  insertRows()这些函数使模型能够使用QAbstractItemModel :: dropMimeData()提供的现有实现自动插入新数据。

insertColumns()

setData()允许使用项填充新的行和列。

setItemData()此功能为填充新项提供了更有效的支持。

 

要接受其他形式的数据,必须重新实现这些功能:

supportedDropActions()用于返回drop操作的组合,指示模型接受的拖放操作的类型。

mimeTypes()用于返回可由模型解码和处理的MIME类型的列表。通常,支持输入到模型中的MIME类型与编码供外部组件使用的数据时使用的MIME类型相同。

dropMimeData()执行通过拖放操作传输的数据的实际解码,确定将在模型中设置的位置,并在必要时插入新的行和列。在子类中实现此函数的方式取决于每个模型公开的数据的要求。

 

如果dropMimeData()函数的实现通过插入或删除行或列来更改模型的大小,或者如果数据项被修改,则必须注意确保发出所有相关信号。 可以有用的是简单地调用子类中其他函数的重新实现,例如setData(),insertRows()和insertColumns(),以确保模型一致地运行。

 

为了确保拖拽操作正常工作,重要的是重新实现以下从模型中删除数据的函数:

removeRows()

removeRow()

removeColumns()

removeColumn()

有关使用项视图进行拖放的更多信息,请参阅使用项视图进行拖放。

 

方便视图

 

方便视图(QListWidget,QTableWidget和QTreeWidget)覆盖默认的拖放功能,以提供不太灵活,但更自然的行为,适用于许多应用程序。 例如,由于将数据放入QTableWidget中的单元格中是更常见的,用正在传输的数据替换现有内容,底层模型将设置目标项的数据,而不是将新的行和列插入模型中。 有关方便视图中拖放的详细信息,请参阅使用项视图中的拖放。

 

大量数据的性能优化

canFetchMore()函数检查父级是否有更多可用数据,并相应地返回true或false。 fetchMore()函数根据指定的父代获取数据。 这两个函数可以组合,例如,在涉及增量数据以填充QAbstractItemModel的数据库查询中。 我们重新实现canFetchMore()以指示是否有更多的数据要获取,fetchMore()根据需要填充模型。

 

另一个例子是动态填充的树模型,当树模型中的分支扩展时,我们重新实现fetchMore()。

 

如果重新实现fetchMore()向模型中添加行,则需要调用beginInsertRows()和endInsertRows()。 此外,canFetchMore()和fetchMore()必须重新实现,因为它们的默认实现返回false,不做任何操作。



0 0
原创粉丝点击