Zade's Weblog

程序人生

Monthly Archives: 9月 2010

Qt的界面模式-Model/View

MVC是界面设计的经典范例.虽然这个理念已经深入人心,但是在各个具体的实现里面是有差别的.最近我使用了Qt的界面开发,开始的时候使用了Item Based的界面类,例如QTreeWidget,但是后来我很快意识到这个方法的问题,从而转向了Model/View的模式.在这里把经验总结一下.

所谓的Item Based的界面控件,具体到Qt而言,就是QTreeWidget QListWidget等,这类控件的特点是,都是通过Item的方式向这些要素添加界面元素,例如QTreeWidgetItem, QListWidgetItem等.这样设计的好处是使用简单,界面元素和底层的数据方式隔离.但是问题是数据冗余,存储代价增大,并且进而带来了数据的一致性问题.

我们使用一个实例说明这类模式的主要问题,例如我们要删除QTreeWidget的一个条目,我们该如何做呢?一般的做法是删除底层的数据,然后删除相应的界面元素QTreeWidgetItem,二者的次序一般没有关系,特殊的情况需要特殊的处理.这样做是否比较麻烦呢?或者说,我们认为更好的方式是这样的: 删除底层的数据结构,界面刷新,以反映数据结构的变化.更直接的说,更好的模式是把数据结构存储在唯一的一个地方,界面显示只是一个获得这个数据并且显示的过程.

这个原理似乎很明显,但是Item Based的控件类明显不是这种做法,界面元素和底层的数据方式是隔离的,并不是一致的存储在某个地方.

原理虽然简单,但是具体并不如此简单,我们以QTreeView和QAbstractItemModel为例,简单的说明一下:

  1. QAbstractItemModel有几个函数必须重载,例如
    • data() 返回特定行的数据,其中值得注意的是参数role,一般的DisplayRole返回的是显示的文本,DecorationRole返回的是显示的图片,CheckStateRole返回的是显示选中状态的数值,所有这些数据通过QVariant的包装转发给系统,系统通过这些来显示当前的元素
    • index()返回指定行列的索引,函数原型是QModelIndex index(int row, int col,const QModelIndex &parent) const,刚开始的时候我有些疑惑,提供一个默认的实现,parent.child(row,col)不就可以了,为什么还让上层的用户提供一个不同的实现呢?后来逐渐的明白,这是给你一个构造更加复杂的QModelIndex 的机会,因为你可以在QModelIndex 上附加一个指针数据.
    • rowCount和columnCount提供行数和列数,一般我们是根据附加的指针数据获得的
    • parent返回指定元素的父亲,这个一般也是根据附加的指针数据获得的
  2. QModelIndex 是在View和Model之间传递数据的一个媒介,但是QModelIndex 是一个瞬时变量,在数据结构调整以后,会导致其无效.这告诉我们不能长期的保留一个QModelIndex 数值,在这种情况下应该使用QPersistentModelIndex.
  3. 在QModelIndex 附加指针,一旦更改这个指针,我们如何通知view使用新的指针而不是旧的指针呢(旧指针可能已经无效)?我在这个问题上费了好长的时间.最早的时候我没有做任何特定的实现,因为通过model的parent或者index函数得到的指针就是我修改以后的指针.但是实际上view会崩溃,原始就是view使用了被废弃的指针.这说明系统内部做了一定的缓存,我需要更新这个缓存.根据QModelIndex 和QPersistentModelIndex的关系,如果系统做了缓存,那么肯定是基于QPersistentModelIndex的缓存,QAbstractItemModel提供了changePersistentIndex函数,从字面的意思来看,应该是通知系统内部修改缓存的数据,调用了这个函数以后,程序不在崩溃,但是界面的对应的行数据不在显示,如图所示:
    imageimage
    这个诡异的结果应该是界面对应的元素没有找到,但是如何更新相应的解密那元素,我没有找到合适的函数,我的最终实现如下:
  4. void LayerItemModel::updateData(const QModelIndex& child,const QModelIndex& parent)
    {
    int row = child.row();
    this->beginRemoveRows(parent,row,row);
    this->endRemoveRows();
    this->beginInsertRows(parent,row,row);
    this->endInsertRows();
    this->changePersistentIndex(child,parent.child(row,0));
    }
    我知道这应该不是最好的方案,但是目前只能这样.