Python+PySide6之模型/视图/委托框架QListView案例实践

Qt中的模型/视图/委托框架是一种数据与可视化相互分离的技术,起源于Smalltalk的设计模式——Mode/View/Controller(MVC,模型/视图/控制器),通常在构建用户界面时使用。

MVC是由3部分组成。Model是应用程序对象,View是它的界面展示,Controller定义了界面对用户输入的反应方式。

Qt提供的技术方法和MVC稍有不同,称为Model/View/Delegate(模型/视图/委托),可以提供与MVC相同的全部功能,如下图,MVC中的控制器的部分功能既可以通过委托实现,也可以通过模型实现。

Python+PySide6之模型/视图/委托框架QListView案例实践_第1张图片

一般来说,模型从数据源中读/写数据,视图从模型的索引中获取需要呈现的数据,并通过委托绘制。对于用户的编辑操作,视图会要求委托提供一个编辑器,并把编辑后的结果传递给模型。模型、视图和委托使用信号/槽机制相互通信。

  • 来自模型的信号将数据源的变更信息通知视图。
  • 来自视图的信号提供用户与当前项目的交互信息。
  • 来自委托的信号在编辑的时候告诉模型和视图编辑器的状态。

一、关于模型(Model):

模型中数据存储的基本单元是item,每个item都对应唯一的索引值(QModelIndex),每个索引值都有3个属性:行、列、父对象。

对于一维模型,如列表(List),只会用到行

对于二维模型,如表格(Table),会用到行和列

对于三维模型,如树(Tree),会用到行、列、父对象。

所有模型都基于QAbstractItemModel类,它定义了一个接口,视图和委托使用该接口访问数据。通过该接口数据不一定要存储在模型中,可以保持在由单独的类、文件、数据库或者默写其它应用程序组件提供的数据结构或者存储库中。

QAbstactItemModel是处理列表、表格、树的基类,在此基础上,QAbstactListModel和QAbstractTableMode提供了处理列表和表格的更好选择,因为它们提供了一些常用的方法。需要注意的是,这3个Model都是抽象模型,必须子类化并且要重新实现部分方法才能使用。如果不想这么麻烦,Qt也提供了一些便准的现有模型,可以直接实例化处理数据。

  • QStringListModel,用于存储QString项的简单列表,一般和QListView或者QComboBox一起使用。
  • QStandardItemModel,可以管理更复杂的项目树结构,可以用于表示列表、表格、树视图所需要的各种不同的数据结构,该模型还包含了数据项,每个项可以包含任意数据。QStandardItemModel可以与QListView,QTableView和QTreeView一起使用。
  • QFileSystemModel是一个用于维护有关目录内容的信息的模型,它本身不保存任何数据,只是表示本地文件系统上的文件和目录。QFileSystemModel可以与QListView、QTableView、QTreeView一起使用。
  • QSqlQueryModel、QSqlTableModel、QSqlRelationalTableModel用于使用模型/视图约定访问数据库,一般和QTableView一起使用。

模型角色:

模型中的项目可以为其它组建执行各种角色,允许为不同情况提供不同类型的数据,比如使用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)可以处理更复杂的数据处理,比如数据的筛选、过滤、复杂计算、及时更新等。

  • QListView:在列表中显示模型的数据,一般和QStringListModel一起使用,也可以使用QStandardItemModel。使用QFileSystemModel可以显示本地文件目录。
  • QTableView:在表格中显示模型的数据,一般和QStandardItemModel一起使用。如果要显示数据库数据,则可以使用QSqlQueryModel、QSqlTableModel、QSqlRelationalModel。使用QFileSystemModel可以显示文件目录。
  • QTreeView:在树中显示模型的数据,一般和QStandardItemModel一起使用,使用QFileSystemModel可以显示文件目录。

三、关于委托(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系统下运行界面如下:

Python+PySide6之模型/视图/委托框架QListView案例实践_第2张图片

上面代码案例创建了2个QListView,绑定了同一个QStringListModel模型实例,通过几个按钮的单击信号与槽函数,实现了增加行、插入行、删除行、上移/下移行,两个视图数据是同步变化的,而且保持一致,依次来说明模型/视图框架的优越性,只须维护一套数据,绑定的所有视图呈现的数据将保持一致。

你可能感兴趣的:(python,pyqt,学习,笔记)