PyQt5中的抽象模型基类QAbstractItemModel与自定义模型
一、关于QAbstractItemModel类
-
QAbstractItemModel类继承自QObject, 该类是Qt所有模型类的基类,用于管理模型/视图结构中的数据。Qt的所有模型都需要子类化该类。注意,该类是抽象类,我们不应该创建该类的对象。
-
该类的构造函数,原型为__init__(parent: QObject = None)
二、QAbstractItemModel 类中的纯虚函数及有效模型索引
2.1 有效模型索引的创建
模型索引是由 QModelIndex 类进行描述的,但该类只有一个默认构造函数,而使用默认构造函数创建的模型索引是无效模型索引,因此要创建一个有效的模型索引,需要使用工厂函数QAbstractItemModel::createIndex()来创建,在重新实现纯虚函数 index()和 parent()时,都有可能会调用该工厂函数来创建模型索引。
2.2 相关函数及其原型
下面为相关的函数及其原型(本文使用的是C++代码的书写格式):
-
virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex ()) const = 0; //纯虚函数
返回由行 row、列 column、父索引 parent 指定的数据项的模型索引,当子类重新实现此函数时,模型索引需调用 createIndex()函数来创建。 -
virtual QModelIndex parent(const QModelIndex &index) const = 0; //纯虚函数
返回索引 index 的父模型索引,若没有父模型索引,则返回无效的模型索引。重新实现该函数时需要小心调用 QModelIndex 中的成员函数(比如 QModelIndex::parent());因为自定义的模型索引只会调用自定义的实现,因此 QModelIndex::parent()会调用此处重新实现的该函数,从而导致无限递归。重新实现该函数时,通常也使用 createIndex()函数创建模型索引。 -
virtual int rowCount(const QModelIndex &parent = QModelIndex ()) const = 0; //纯虚函数
virtual int columnCount(const QModelIndex &parent =QModelIndex ()) const = 0; //纯虚函数
以上函数表示,返回父模型索引 parent 下的行/列数,在实现基于表格的模型时,若父模型索引有效,则以上函数都应返回 0。 -
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0; //纯虚函数
返回索引 index 所引用的项在给定角色 role 下存储的数据。注意:若没有需要返回的值,应返回无效的 QVariant,而不是返回 0。该函数用于向视图和委托提供项目数据,也就是说视图和委托是显示的该函数返回的值。 -
virtual bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole); //虚拟的
设置索引 index 所引用数据项的值为 value,其角色为 role。若设置成功则返回 true,并且应发送 dataChanged()信号,否则返回 false。默认实现为返回 false,虽然此函数不是纯虚函数,但若模型是可编辑模型,则必须重新实现此函数, -
QModelIndex createIndex(int row, int column, void *ptr = Q_NULLPTR); //受保护的
QModelIndex createIndex(int row, int column, quintptr id); //受保护的- 以上函数表示使用内部指针 ptr 或内部 ID 为给定的行 row 和列 column 创建一个模型索引。
- 当重新实现纯虚函数 index()时,需要调用该函数创建模型索引。
- 内部指针可由 QModelIndex::internalPointer()函数获取,内部 ID 可由QModelIndex::internalId()函数获取。 内部指针其实质就是指指向模型实际所管理的数据,因此不一定需要使用 internalPointer()函数来获取。
- quintptr 类型在 32 位指针的系统上是 quint32,在 64 位指针的系统上是 quint64,其最终结果都是使用 typedef 重命的 unsigned int 类型(只是系统不同长度不同)
-
bool hasIndex(int row, int column, const QModelIndex &parent =QModelIndex ()) const;
若根据 row、 column、 parent 返回的模型索引是有效索引,则返回 true,否则返回 false。 -
virtual bool hasChildren(const QModelIndex &parent = QModelIndex ())const; //虚拟的
若 parent 拥有任何子女,则返回 true,否则返回 false,注意,若设置标志为Qt::ItemNeverHasChildren,则使用此方法的行为是未定义的。在分层模型中,查找数据项的子项目数量是一项昂贵的操作,因此 rowCount()函数应在确有必要时进行调用,通过首先调用此函数判断数据项是否有子项,然后再决定是否调用 rowCount()函数是一种有效的方法。
三、插入和删除行/列
3.1 插入和删除行/列后的数据更新
要使模型能插入行/列和删除行/列,子类需要重新实现以下虚函数:
- insertRows();
- insertColumns();
- removeRows();
- removeColumns();
下面以 insertRows()虚函数为例,讲解其规则(其余函数,原理类同):
在将新行插入到任何基础数据结构之前,必须调用 beginInsertRows()函数(称其为 begin 函数),该函数会通知其他组件(比如视图或委托)行数将要发生变化,完成插入操作之后,还需要调用 endInsertRows()函数(称其为 end 函数)以通知其他组件,该模型的行数已经更改,若 insertRows()插入成功,则返回 true,否则返回 false。
更改模型结构的另一种方法:
通常使用 begin 和 end 函数就能够达到通知其他组件模型结构变化的目的,但对于结构比较复杂的模型,则这种方法可能会比较低效,比如若有一个有 300 百万行的模型,需要删除所有偶数行,这将有可能使用beginRemoveRows和endRemoveRows达到150万次之多,这显然是低效的。此时可使用以下步骤来更新模型结构
- 发送 layoutAboutToBeChanged()信号
- 更新模型结构的内部数据。
- 使用 chnagePersistentIndexList()更新持久索引。
- 发送 layoutChanged()信号。
以上步骤可用于更新任何结构的模型。
3.2 插入操作相关的函数
-
virtual bool insertColumns(int column,int count,const QModelIndex &parent=QModelIndex ()); //虚拟的
virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex ()); //虚拟的-
QAbstractItemModel 类对以上虚函数的默认实现什么也没有做,并返回 false,因此要想模型支持插入操作,需要重新实现以上虚函数。重新实现时需要调用相应的 begin 函数和 end 函数。
-
以上函数表示在指定的列 column/行 row 之前插入 count 行/列,插入的新行/列将是 parent 模型索引所指数据项的子项,若插入成功则返回 true,否则返回 false。
-
-
bool insertRow(int row, const QModelIndex &parent = QModelIndex ());
bool insertColumn(int column, const QModelIndex &parent = QModelIndex ());
以上函数分别调用虚函数 insertRows()和 insertColumns() -
void beginInsertColumns(const QModelIndex &parent, int first, int last); //受保护的
void beginInsertRows(const QModelIndex &parent, int first, int last); //受保护的
void endInsertColumns(); //受保护的
void endInsertRows(); //受保护的- 以上函数分别是插入列和行时的 begin 和 end 函数,其中在数据被插入之前beginInsertColunms()函数会发送 columnsAboutToBeInserted()信号,beginInsertRows()函数会发送 rowsAboutToBeInserted()信号,视图或代理通过连接到以上信号,以更新其视图显示,若视图或代理未对以上信号做出处理,则不会正确显示插入的行(需验证信号问题)。
- 参数 parent 表示被插入到新列/行的父索引, first 和 last 分别表示新列插入后的开始和结束列/行号,下面以 beginInsertCloumns()函数为例进行讲解,beginInsertRows()函数原理是相同的(原理见下图)。
3.3 删除操作相关的函数
-
virtual bool removeColumns(int column,int count,const QModelIndex &parent=QModelIndex ()); //虚拟的
virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex ()); //虚拟的- QAbstractItemModel 类对以上虚函数的默认实现什么也没有做,并返回 false,因此要想模型支持删除操作,需要重新实现以上虚函数。重新实现时需要调用相应的 begin 函数和 end 函数。
- 以上函数表示删除模型索引parent所指数据项之下从列column/行row开始的cout行/列。若删除成功则返回 true,否则返回 false。
-
bool removeRow(int row, const QModelIndex &parent = QModelIndex ());
bool removeColumn(int column, const QModelIndex &parent = QModelIndex ());
以上函数分别调用虚函数 removeRows()和 removeColumns() -
void beginRemoveColumns(const QModelIndex &parent, int first, int last); //受保护的
void beginRemoveRows(const QModelIndex &parent, int first, int last); //受保护的
void endRemoveColumns(); //受保护的
void endRemoveRows(); //受保护的- 以上函数分别是删除列和行时的 begin 和 end 函数,其中在数据被删除之前beginRemoveColunms()函数会发送 columnsAboutToBeRemoved()信号,beginRemoveRows()函数会发送 rowsAboutToBeRemoved()信号,视图或代理通过连接到以上信号,以更新其视图显示,若视图或代理未对以上信号做出处理,则不会正确显示删除的行。
- 参数 parent 表示被删除的列/行的父索引, first 和 last 分别表示被删除的数据项的开始和结束列/行号,下面以 beginRemovedCloumns()函数为例进行讲解,beginRemovedRows()函数原理是相同的(原理见下图)。
3.4 移动操作相关的函数
-
virtual bool moveColumns(const QModelIndex &sourceParent, int sourceColumn, int count,const QModelIndex &destinationParent, int destinationChild); //虚拟的
virtual bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count,const QModelIndex &destinationParent, int destinationChild); //虚拟的- QAbstractItemModel 类对以上虚函数的默认实现什么也没有做,并返回 false,因此要想模型支持移动操作,需要重新实现以上虚函数。重新实现时需要调用相应的 begin 函数和 end 函数。
- 以上函数表示从源模型索引 sourceParent 所指数据项之下,从列 column/行 row 开始的 cout 行/列,移至目标模型索引 destinationParent 所指数据项之下的destinationChild 之前。若移动成功则返回 true,否则返回 false。
-
bool moveRow(const QModelIndex &sourceParent, int sourceRow,const QModelIndex &destinationParent, int destinationChild);
bool moveColumn(const QModelIndex &sourceParent, int sourceColumn,const QModelIndex &destinationParent, int destinationChild);
以上函数分别调用虚函数 moveRows()和 moveColumns() -
bool beginMoveColumns(const QModelIndex &sourceParent, int sourceFirst, int sourceLast,const QModelIndex &destinationParent, int destinationChild); //受保护的
bool beginMoveRows(const QModelIndex &sourceParent, int sourceFirst, int sourceLast,const QModelIndex &destinationParent, int destinationChild); //受保护的
void endMoveColumns(); //受保护的
void endMoveRows(); //受保护的- 以上函数分别是移动列和行时的 begin 和 end 函数, begin 函数简化了模型中实体的移动,负责移动模型中的持久索引,否则需要自已完成持久索引的移动。使用以上的 begin 和 end 函数是 changePersistentIndex()函数及发送 layoutChanged()和layoutAboutToBeChanged()信号的替代方法(验证)。
- 参数 sourceParent 表示被移动的列/行的父索引, destinationPart 表示移动后所在新列/行的父索引,以上函数表示把从 sourceFirst 开始到 sourceLast 结束的数据项移至 destinationChild 表示的列/行之前。
- 当在同一父索引中移动时,必须确保destinationChild位于(sourceFirst, sourceLast+1)的范围之外(原理见下图)。另外,不要将列/行移至自已的子项或其祖先,若发生以上两种情形之一,则以上函数应返回 false。
- 下面以 beginMoveCloumns()函数为例进行讲解, beginMoveRows()函数原理是相同的(原理见下图)。
四、模型标头
相关函数如下:
-
virtual QVariant headerData(int section, Qt::Orientation orientation,int role =Qt::DisplayRole) const; //虚拟的
-
virtual bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::EditRole); //虚拟的
以上函数用于获取和设置位于方向 orientation 和位置 section 的标头数据,其数据角色为 role(见下图)。重新实现 setHeaderData 函数时,必须明确地发送 headerDataChanged()信号。
五、另一种设置数据项的函数
相关函数如下:
- virtual bool setItemData(const QModelIndex &index, const QMap<int, QVariant> &roles); //虚拟的
- virtual QMap<int, QVariant> itemData(const QModelIndex &index) const; //虚拟的
- 以上函数主要用于获取和设置组成数据项的各数据元素的信息,
- 注意, QMap<int,QVariant>中的 int 指的是 Qt::ItemDataRole 枚举成员对应的 int值,比如 QMap<int, QVariant> mp; mp.insert(6,f); setItemData(index, mp);表示把数据项的字体角色(Qt::FontRole 对应的 int 值为 6)设置为字体 f。
- itemData 函数是模型/视图结构中唯一能获取项目角色的函数。
使用 setItemDate()函数设置数据项(效果见图中 EEE)简明示例:
QFont f;
f.setPixelSize(22);
QMap<int, QVariant> mp; //创建 QMap
mp.insert(0,"EEE"); //设置 Qt::DisplayRole 角色的数据
mp.insert(1,QIcon("F:/1i.png")); //设置 Qt::DecorationRole 角色的数据
mp.insert(3,"222"); //设置角色 Qt::ToolTipRole 的数据
mp.insert(6,f); //设置角色 Qt::Font 的数据
//以下数据项由以上 4 个数据元素组成。
pmodel->setItemData(pmodel->index(0,1,QModelIndex()),mp);
六、自定义模型
6.1 自定义模型相关的虚函数
自定义模型至少需要实现以下虚函数:
- columnCout()
- rowCount()
- index()
- parent()
- data()
为了能添加自已的数据到模型中,通常还需要重新实现 setData()函数,而不重新实现setData()则无法向模型中添加数据。
6.2 自定义模型的基本原理和步骤
-
数据:实际数据可使用 QList、数组、整型、或单独的一个类来保存,数据可存放在模型中,也可存放在文件等其他地方。
-
columnCout()、 rowCount()、 index()、 parent()这 4 个函数用于共同设计模型的结构,因为使用索引表示模型中的某个数据项的位置,因此设计模型索引的结构就是设计模型的结构
-
行数和列数的设计:比如对于 3 行 4 列的表格结构 columnCout()应返回 4,rowCount()应返回 3;对于列表结构,则因为列表只需要 1 列,所以 columncout()应总是返回 1, rowCount()返回该列表的行数;对于树形结构模型,则更复杂,需要根据当前父节点的情况进行判断,以返回该父节点拥有的列数和行数。
-
parent()函数(父模型索引)的设计:因为表格结构中的所有单元格都属于同一个父索引之下,所以可把所有单元格都视为顶级节点,因此他们的父索引可以以无效模型索引作为父索引,因此 parent()可以返回一个无效模型索引;对于列表结构的模型,同样只需返回一个无效模型索引即可;对树形结构模型,此步骤比较复杂,可以通过获取当前节点的父节点及其行号和列号,然后使用 createIndex()创建该父节点的索引。
-
index()函数的设计:该函数用于为模型中的每个数据项创建索引,创建索引需要使用 createIndex()函数,对于表格结构,只需向 createIndex()函数传递当前数据项所在的行号、列号及使用的数据的指针即可;对于列表结构,则列号始终为 0,其余同表格结构;对于树形结构,需要向该函数传递当前数据项位于父索引中的行号、列号及使用的数据的指针。
-
-
data()函数的返回值决定了视图上应显示的数据,也就是说在界面上用户看到的数据是由该函数返回的, 若返回不当的值,则数据无法正常显示在视图上,下面以使用内置的标准视图类为例来讲解怎样设计此函数。 data()函数会被视图类调用多次, 视图每次都会向 data 传递一个不同的 role(角色)参数值,然后视图根据 data 返回的值,设置该 role 的数据, 因此在设计 data 函数的返回值时,需要根据 role 的不同值返回不同的数据,以使视图正确的显示。
七、示例代码
自定义表格模型示例代码
运行效果如下:
小手一抖,点个赞再走哦~~~