Qt中的模型/视图/委托框架是一种数据与可视化相互分离的技术,起源于Smalltalk的设计模式——Mode/View/Controller(MVC,模型/视图/控制器),通常在构建用户界面时使用。
MVC是由3部分组成。Model是应用程序对象,View是它的界面展示,Controller定义了界面对用户输入的反应方式。
Qt提供的技术方法和MVC稍有不同,称为Model/View/Delegate(模型/视图/委托),可以提供与MVC相同的全部功能,如下图,MVC中的控制器的部分功能既可以通过委托实现,也可以通过模型实现。
一般来说,模型从数据源中读/写数据,视图从模型的索引中获取需要呈现的数据,并通过委托绘制。对于用户的编辑操作,视图会要求委托提供一个编辑器,并把编辑后的结果传递给模型。模型、视图和委托使用信号/槽机制相互通信。
一、关于模型(Model):
模型中数据存储的基本单元是item,每个item都对应唯一的索引值(QModelIndex),每个索引值都有3个属性:行、列、父对象。
对于一维模型,如列表(List),只会用到行
对于二维模型,如表格(Table),会用到行和列
对于三维模型,如树(Tree),会用到行、列、父对象。
所有模型都基于QAbstractItemModel类,它定义了一个接口,视图和委托使用该接口访问数据。通过该接口数据不一定要存储在模型中,可以保持在由单独的类、文件、数据库或者默写其它应用程序组件提供的数据结构或者存储库中。
QAbstactItemModel是处理列表、表格、树的基类,在此基础上,QAbstactListModel和QAbstractTableMode提供了处理列表和表格的更好选择,因为它们提供了一些常用的方法。需要注意的是,这3个Model都是抽象模型,必须子类化并且要重新实现部分方法才能使用。如果不想这么麻烦,Qt也提供了一些便准的现有模型,可以直接实例化处理数据。
模型角色:
模型中的项目可以为其它组建执行各种角色,允许为不同情况提供不同类型的数据,比如使用setText(),setIcon(),setForeground()等函数可以设置不同的角色。例如,在视图中正常显示字符串需要设置Qt.DisplayRole角色,这一般是项目的默认角色;为项目添加ToolTip提示功能,需要设置Qt.ToolTipRole角色;设置项目item的文本颜色,需要设置Qt.ForegroundRole角色。
一个项目item可以包含多个不同角色的数据,也就是说拥有这些角色,标准角色由Qt.ItemDataRole定义。
模型通过索引(QModelIndex)定位角色,使用setData()函数可以设置角色,使用data()函数可以获取角色。
有些模型(如QStringListMode)只支持字符串,不支持颜色、图片等角色;有些模型(如QStandardItemMode)支持字符串、颜色、图片等多种角色。
二、关于视图(View):
视图从模型中获取数据并在界面上呈现。QListWidget/QTableWidget/QTreeWidget是分别继承QListView/QTableView/QTreeView类,包含了默认的模型,使用简单一些,但是QListView/QTableView/QTreeView类更加灵活,配合不同模型(Model)可以处理更复杂的数据处理,比如数据的筛选、过滤、复杂计算、及时更新等。
三、关于委托(Delegate):
Delegate(委托或代理)的作用包括以下两个方面:
绘制视图中来自模型的数据,委托会参考项目的角色和数据进行绘制,不同的角色和数据有不同的绘制效果(比如字符串颜色,字体,背景色,图标等)。
在视图与模型之间交互操作时提供临时编辑组件的功能,该编辑器位于视图的顶层。
QAbstractItemDelegate是委托的抽象基类,它的子类QStyledItemDelegate是所有Qt项目视图的默认委托,并在创建视图时自动安装。QStyledItemDelegate是QListView、QTableView、QTreeView的默认委托,如果要编辑QTableView,那么其默认委托(QStyledItemDelegate)会提供QLineEdit作为编辑器;对于子类QListWidget/QTableWidget/QTreeWidget也一样。如果要使用其他编辑器,比如vQTableView使用QSpinBox作为委托编辑器,就需要通过自定义委托实现。
四、QListView应用案例:
QListView是Qt中用来存储列表的纯视图类。QListView是基于Qt模型/视图/委托架构提供更灵活的方法,一般与QStringListModel绑定管理数据。
1、绑定模型和初始化数据
QListView需要绑定模型,一般绑定QStringListModel模型。QStringListModel是一个可编辑模型,,提供了可编辑模型的所有标准功能,可用于在视图小部件中显示多个字符串的简单情况。
模型既可以在实例化时传递字符串列表来初始化数据,也可以使用setStringList()函数设置字符串,视图绑定模型使用setModel()函数。
实例化时传递字符串列表初始化数据示例:
# 使用for循环表达式,创建一个字符串列表
model = QStringListModel(['行'+str(i) for i in range(6)])
使用setStringList()函数初始化数据示例:
model = QStringListMode()
model.setStringList(['行'+str(i) for i in range(6)])
视图绑定模型示例:
# 实例化一个QListView列表视图对象
listView = QListView()
# 实例化QStringListModel模型
model = QStringListModel(['row'+str(i) for i in range(6)])
# 列表视图绑定模型
listView.setModel(model)
2、增、删、改、查、移、选
关于数据的操作要通过模型来完成,下面介绍的是QStringListModel的相关函数。
使用index()函数可以获取item对应的模型索引;使用flag()函数可以获取item的flag信息,该信息决定了item是否可以被选中、编辑及交互等。如果item的flag为Qt.NoItemFlags,那么该item将无法被选择、编辑、拖动等。
data()将用于获取项目数据,setData()函数,insertRow()和insertRows()用于插入行,removeRow()和removeRows()用于删除行,rowCount()用于获取列表的长度,stringList()函数用于获取字符串列表的内容,moveRow()函数用于移动行。
关于添加行(没有提供addRow()函数),通过使用insertRow()函数,在模型的末尾(通过获取模型的行数,得到索引值)来添加一行。
# 获取模型的行数
num = model.rowCount()
# 因为索引从0开始,使用模型的行数num(整数),实现在模型数据项的末尾添加1行
model.insertRow(num)
# 给模型添加内容,setData()的一个参数类型是模型索引,不是整数类型
index = model.index(row)
text ='给模型添加的字符串数据'
# 给添加的行,设置数据,需要该行的索引
model.setData(index,text)
3、一个综合案例代码
import os
import sys
from PySide6.QtWidgets import *
from PySide6.QtGui import *
from PySide6.QtCore import *
class QListViewDemo(QWidget):
addCount = 0
insertCount = 0
def __init__(self, parent=None) :
super(QListViewDemo,self).__init__(parent)
self.setWindowTitle("QListView案例")
self.text = QPlainTextEdit("用来显示QListView相关信息:")
self.listView = QListView()
# 定义一个QStringListMoel模型,可以先创建模型,然后定义数据;也可以直接创建模式时创建数据
# self.model = QStringListModel()
# self.model.setStringList(['行'+str(i) for i in range(6)])
self.model = QStringListModel(['行' + str(i) for i in range(6)])
# 把视图(listView对象)和模式进行关联
self.listView.setModel(self.model)
# 定义第二个视图对象(listView),依然关联上面的模式。
# 在视图1中更改模式,会发现在视图2中会发生同样的变化。
self.listView2 = QListView()
self.listView2.setModel(self.model)
# 设置列表视图2的最大高度100像素
self.listView2.setMaximumHeight(180)
# 定义一组按钮
self.buttonAdd = QPushButton('增加')
self.buttonDelete = QPushButton('删除')
self.buttonUp = QPushButton('上移')
self.buttonDown = QPushButton('下移')
self.buttonInsert = QPushButton('插入')
# 定义上面5个按钮的单击信号槽函数
self.buttonAdd.clicked.connect(self.onAdd)
self.buttonInsert.clicked.connect(self.onInsert)
self.buttonUp.clicked.connect(self.onUp)
self.buttonDown.clicked.connect(self.onDown)
self.buttonDelete.clicked.connect(self.onDelete)
# 定义一个水平布局管理器,把上面5个按钮对象加入,进行水平布局
layoutH = QHBoxLayout()
layoutH.addWidget(self.buttonAdd)
layoutH.addWidget(self.buttonInsert)
layoutH.addWidget(self.buttonUp)
layoutH.addWidget(self.buttonDown)
layoutH.addWidget(self.buttonDelete)
# 定义一组按钮
self.buttonSelectAll = QPushButton('全选')
self.buttonClear = QPushButton('清除选择')
self.buttonSelectOutput = QPushButton('输出选择')
# 定义上面3个按钮的单击信号槽函数
#
# 定义一个水平布局管理器,加入上面3个按钮,进行水平布局
layoutH2 = QHBoxLayout()
layoutH2.addWidget(self.buttonSelectAll)
layoutH2.addWidget(self.buttonClear)
layoutH2.addWidget(self.buttonSelectOutput)
# 创建一个垂直布局管理器,作为主窗口上的主布局管理器
layout = QVBoxLayout()
layout.addWidget(self.listView)
layout.addLayout(layoutH)
layout.addLayout(layoutH2)
layout.addWidget(self.text)
layout.addWidget(self.listView2)
# 把垂直布局管理器添加到主窗口
self.setLayout(layout)
def onAdd(self):
self.addCount += 1
# 新增项目的文本字符串
text = f'新增--{self.addCount}'
# 获取模型的行数
num = self.model.rowCount()
# 模型中插入一行,在模型的末尾(相当于追加)
self.model.insertRow(num)
# 获取模型的最后一行的索引
index = self.model.index(num,0)
# 因为两个视图都连接了上面一个模型,所以,视图中的数据都会更新
# 注意:模型的行数与索引值,是不同的类型(对象),一个是 ,一个是
self.model.setData(index,text)
# 在文本框中显示一行内容:在模型中添加了一行数据
self.text.appendPlainText(f'新增item:{text}')
def onInsert(self):
self.insertCount += 1
# 获取当前行的索引对象
index =self.listView.currentIndex()
row = index.row()
text = f'插入--{self.insertCount}'
# 插入一个空白行,后面
self.model.insertRow(row)
# index对象是执行insertRow()前获取的索引,下面setData()函数,
self.model.setData(index,text)
#
self.text.appendPlainText(f'行:{row},新增了item:{text}')
def onUp(self):
index = self.listView.currentIndex()
row = index.row()
if row > 0: # 判断不是第一行(索引值为0)
self.model.moveRow(QModelIndex(),row,QModelIndex(),row -1)
def onDown(self):
index = self.listView.currentIndex()
row = index.row()
if row <= self.model.rowCount() -1:
self.model.moveRow(QModelIndex(),row+1,QModelIndex(),row)
def onDelete(self):
'''
QListView对象总有一个currentIndex(),如果实例化的QListView对象的项目为空,
row = index.row() # 返回为-1
text = self.model.data(index) # 返回为None
'''
index = self.listView.currentIndex()
text = self.model.data(index)
row = index.row()
if row >= 0 :
self.model.removeRow(row)
self.text.appendPlainText(f'行:{row},删除item{text}')
if __name__ == "__main__":
app = QApplication(sys.argv)
demo = QListViewDemo()
demo.show()
sys.exit(app.exec())
代码在deepin20系统下运行界面如下:
上面代码案例创建了2个QListView,绑定了同一个QStringListModel模型实例,通过几个按钮的单击信号与槽函数,实现了增加行、插入行、删除行、上移/下移行,两个视图数据是同步变化的,而且保持一致,依次来说明模型/视图框架的优越性,只须维护一套数据,绑定的所有视图呈现的数据将保持一致。