“我正在参加「掘金·启航计划」”
为什么需要 模型-视图 框架
模型(就是指的数据组成形式,链表,多叉树,矩阵结构等)和视图(就是实际显示出来的控件)分离,相等于就是前端和后端分离的架构了,这个能够减少代码耦合度,提高复用性。(大家想想网站开发,不就是这么个道理嘛)
原因:模型是一种数据之间的组成架构,我们只需要把数据项itme之间的兄弟关系,父子关系,这些关系对应映射到我们的模型中去(实际上就是根据我们的数据项关系自己指定出各自的行列信息,这个是一次性的),然后视图读取这个模型,update()一下,就会自动更新视图了。
所以,我们就只需要专注于我们的item的数据以及item关系的处理了(比如我们的多叉树数据结构,增加一个孩子啥的操作,由于之前一次性映射好了模型的,所以视图那边会根据这个模型自动更新的),也就是后端这个业务代码了。
模型结构
为了能够让我们更好的设计模型,也就是数据之间的关系,qt提供了QStandardItem、 QStandardItemModel 类来描述这个模型:
- QStandardItem:表示一个最基本的数据项item,可以包含显示的文本,图标,复选框。而且这个数据项可以可以指定背景色,字体。具有 使能,可编辑的,可选择的,可勾选的等状态。而且还能用于拖动和放下等操作的目标。还能存数据在里面。可以在Model中指定本item所处的行,列(所以这些数据项就能去组成丰富的组织结构,比如列表,树,表格)。比如我们想展示一个人员组织关系树,那么我们自定义一个结构体,叫做struct people,然后挂接上来作为数据索引实体。这个类好像一般不用去子类化,我没研究过。
- QStandardItemModel:表示这些数据项组成起来的总体,对其关系进行管理。它是继承自QAbstractItemModel类,里面提供了很多必须实现的虚函数接口。为了对上面我们自定义的数据关系进行管理,我们也需要子类化QAbstractItemModel,实现对应虚函数。此时每个项item就是对应这里面的 QModelIndex (这里每个QModelIndex和 item 是直接指针类型强制转换的),这个就是用来对应这个item的。
此时必须实现下面的虚函数,从而建立出所有的 QStandardItem 在 这个自定义的Model里面的组织结构关系。
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
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;
这里拿个函数 QModelIndex parent(const QModelIndex &child) 函数举例,就是要我们实现给出一个child项,返回出它的父节点项。(如果我们不实现这个,当然不行,因为Model也不知道这些项item的行列信息啊,所以视图就没法显示)。那我们当然就根据我们的自定义的数据结构关系(比如自定义的多叉树数据结构),找出它的父节点的地址,然后用这个地址调用QModelIndex createIndex(row, column, node)函数去创建出一个QModelIndex 返回即可(这就是数据项在模型中的映射过程)。(注意这里可以看出,模型内部还是以行row,列column作为item定位的)
其实也不一定就需要继承这个类,大部分情况应用都比较简单,直接用实例化这个类,给它添加item,然后item再添加自己的item,就形成了多叉树,挺简单的。比如下面就是例子。
model->appendRow(fileItem);
fileItem->appendRow(posItem);
有了模型之后,我们只需要将这个模型设置到一个view上去即可,这时候,这个view就会自动根据这个model里面的行列信息进行对应的显示了。
qt提供的视图有哪些
由于数据之间的结构关系,其实就三种,列表,树,矩阵,所以qt提供的视图类也就3种,分别是listview,treeview(描述能力包含listview),tableview(很明显,这个的描述能力可以包含前两者)。当然我们还能自己定义一个视图出来。这些实际上都是继承自QWidget,所以它们都是可视化控件了,只是需要设置一个模型XXModel后(原理:这个视图显示就是读取这个model里面的行列信息进行对应显示了),就能显示出具体东西来了。
函数分析
这里再分析一个函数 int rowCount(const QModelIndex &parent = QModelIndex()) const override;
这个是返回指定父节点的孩子数(每个孩子占一行,其中列为0),前提是给的参数parent是有效的。那这里为什么形参是
parent = QModelIndex()呢,说明如果说我们没有指定形参,那么parent就是QModelIndex(),意思是创建出一个QModelIndex栈对象。从QModelIndex()类的构造函数说明来看,显然这个parent现在是invalid的。既然parent不可用,那么rowCount返回的是什么???
我们的rowCount的说明没有指出来。但是在Model/View Programming部分的Model Classes中的Rows and columns里面有一句话是这样说的:Top level items in a model are always referenced by specifying QModelIndex() as their parent item. 意思是一个model中的顶级item也就是Top level item 的parent,一直都是通过QModelIndex()来指定的。
所以,知道一个model的所有行,你就必须使用QModelIndex()来作为其parent。如果你想知道一个model中的某个item下有多少行,你就需要将该item的modelindex作为parent。
所以直接调用 rowCount()函数,啥参数也不给,返回的就是这个模型的总行数。
参考博客:
C++ qt QModelIndex::QModelIndex()怎么解释?_百度知道
为什么需要QStyledItemDelegate
item数据之间的组成结构,是由我们的模型model来实现的,而显示是由view来实现的(只提供了基本的编辑交互),那么视图view中与item的交互(编辑,点击,以及其它更灵活的操作)是由谁来实现呢???
QStyledItemDelegate 这个代理类来完成,而且我们希望自己控制item的绘制效果(而QStyledItemDelegate没有对我们数据类型的绘制提供支持 ),那么就子类化 QStyledItemDelegate,并且重写 paint()函数即可。paint() 函数会被每一个item独立调用,而sizeHint()函数则可以定义每一个item 的大小。在重写 paint() 函数的时候,通常需要用 if 语句找到你需要进行渲染的数据类型并进行绘制,其他的数据类型需要调用父类的实现进行绘制。
该函数原型如下:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index)
踩坑记录
这个paint函数里,有个点需要特别注意:
比如我们想控制文本字符串的绘制,我们需要绘制一段字符串,然后用QFontMetrics去测量一下这段字符串宽度(所以这里依赖与字体),然后设置x坐标,继续绘制接下来的字符串。这里一定要注意了,painter的字体和QFontMetrics的字体是否相同,这样绘制出来的文本才不会有莫名其妙的空格。需要我们去显式的设置字体,比如:
QFontMetrics metrics = option.fontMetrics;
painter->setFont(option.font);
参考博客:
QStyledItemDelegate类的使用_兔子先生_的博客-CSDN博客_qstyleditemdelegate的用法