PyQt Model/View编程:通过实例代码来认识 Delegate 组件的强大功能

文章目录

  • 1. Model/View编程方法与Delegate组件功能介绍
    • 1.1 Model/View原理介绍
    • 1.2 Delegate 基本功能
  • 2. Delegate类及方法属性
  • 3. 自定义ComboBox风格delegate类
  • 4. 以进度条方式显示字段的自定义Delegate类
  • 5. 用星号来显示表格中评级字段示例
  • 6. 总结

1. Model/View编程方法与Delegate组件功能介绍

1.1 Model/View原理介绍

PyQt把MVC中的 View 与 Control 放在一起,将数据管理与呈现功能分开,不同的视图可以使用相同的model数据,称为 Model/View 方法。
用Widget控件操作数据时,数据与视图是放在一起。
PyQt Model/View编程:通过实例代码来认识 Delegate 组件的强大功能_第1张图片
Model/View方式,数据与视图是分开管理
PyQt Model/View编程:通过实例代码来认识 Delegate 组件的强大功能_第2张图片

这个编程方法中,除了model, view组件, 还引入了 delegate 的概念,负责item数据项级别的呈现(display)与编辑(edit)功能。因此Model/View模式提供了3种组件,并提供了相应的类,有的功能是虚方法,需要手工实现。

1.2 Delegate 基本功能

Delegate 负责数据项(item) 的显示以及提供编辑控件。当修改数据时,由Delegate负责编辑器器与model之间通信,如读取、更新数据。
PyQt Model/View编程:通过实例代码来认识 Delegate 组件的强大功能_第3张图片

Delegate,字面上是item的代理,其作用类似于item数据项项这个粒度的view+controller。笔者认为此处用英文更容易理解。

默认QListView, QTableView, QTreeView都提供了默认的Delegate类实现。有时,我们希望将数据以图形化方式呈现,如下图,将进度按进度条的方式来呈现。
PyQt Model/View编程:通过实例代码来认识 Delegate 组件的强大功能_第4张图片

默认View提供的编辑器是1个QLineEdit控件, 可视化数据库时,通常会遇到多选值字段,编辑这类字段希望提供下拉列表comboBox的功能。

上述两种场景,需要自定义delegate来实现,本文将以实例方式讲解如何实现。

2. Delegate类及方法属性

Delegate基类为 QAbstractItemDelegate, 有3个子类,关系如下:

QAbstractItemDelegate
QItemDelegate
QStyledItemDelegate
QSqlRelationalDelegate

Delegate类的选择

  • 自定义delegate建议继承QStyledItemDelegate。
    QTableView, QListView内部已实现了 QStyledItemDelegate类,可根据item的类型与role, 提供不同的渲染特性。
  • QItemDelegate 与QStyledItemDelegate区别,不继承原视图的style,需要自定义样式。

继承QStyledItemDelegate类,修改item的外观,必须要重载下列方法:

  • paint()
  • sizeHint()

自定义editor 编辑器,需要重载以下方法:

  • createEditor() 创建editor , closeEditor() 关闭编辑器
    - setEditorData(), 从model -> editor populate 数据
    -setModelData() 从 editor -> 更新model

或者使用editorEvent方法来更新
editorEvent(event, model, option, index)
其它可选方法:
updateEditorGeometry()

3. 自定义ComboBox风格delegate类

下拉列表在数据库Choices字段时必然会用到,因此是可视化数据时必然会用到的1个自定义delegate。
本例,编辑model中state字段时,用combo下拉列表控件将可选择的数据项列出,用户可从中选择以方便修改。
PyQt Model/View编程:通过实例代码来认识 Delegate 组件的强大功能_第5张图片
注意: combox下拉列表内容可以预先准备好,或者从数据库的数据字典表中读取。

实现代码如下:

from PyQt5.QtSql import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys

class WidgetDelegate (QStyledItemDelegate):
    def __init__(self, parent,value_data):
           super(WidgetDelegate, self).__init__(parent)
           self.value_data = value_data

    def createEditor(self, parent, option, index): 
        combo = QComboBox(parent)
        for idx,label in enumerate(self.value_data):
            combo.addItem(label)
        return combo
    
    def setEditorData(self, editor, index):  
        # 从model中读数据,更新Editor的显示值 
        # 读取当前节点的值
        value = index.model().data(index, Qt.EditRole)   
        if isinstance(value,int):
            value = str(value)
        print(f"cell ({index.row()},{index.column()}) data: {value}")     
        if value:   # 如果不在combo中,添加进来。
            if not editor.findText(value):
                editor.addItem(value)
            editor.setCurrentText(value)   # 将选择值设为current
        else:
             editor.setCurrentIndex(0)

    def setModelData(self, editor, model, index):   
        # 从editor值更新model数据
        model.setData(index, editor.currentText(),  Qt.EditRole)
        
    def commitAndCloseEditor(self):
        """Commits the data and closes the editor. :) """
        editor = self.sender()
        # The commitData signal must be emitted when we've finished editing
        self.commitData.emit(editor)
        #delegate完成编辑后,应发送closeEditor ()信号通知其它组件。
        self.closeEditor.emit(editor)

class MyWin(QMainWindow):
    def __init__(self) -> None:
        super(MyWin, self).__init__()
        self.setGeometry(400, 200, 1200, 700)
        self.conn = QSqlDatabase.addDatabase("QSQLITE")
        self.conn.setDatabaseName("./stores.sqlite")
        ok = self.conn.open()
        if not ok: 
            print("Unable to open data source file.")
            print("Connection failed: ", self.conn.lastError().text())
            sys.exit(1) # Error code 1 - signifies error in opening fil

        self.initUI()
    
    def initUI(self):
        self.model = QSqlRelationalTableModel(None,self.conn)
        self.model.setTable("stores")
        self.model.setEditStrategy(QSqlTableModel.OnFieldChange)
        self.model.setJoinMode(QSqlRelationalTableModel.LeftJoin)
        self.model.select()
        headers = ['store_id','store_name','phone','state']
        for columnIndex, header in enumerate(headers):
            self.model.setHeaderData(columnIndex, Qt.Horizontal, header)
        
        # 创建 delegate对象
        slist = ['江苏', '山东', '广西', "辽宁"]
        delegate = WidgetDelegate(self,slist)      
            
        self.table_view = QTableView()
        self.table_view.setModel(self.model)      # 绑定model 
        self.table_view.setItemDelegateForColumn(3, delegate)  # 设定第4列使用自定义delegate        
        self.table_view.resizeColumnsToContents()
        self.setCentralWidget(self.table_view)
        
    
    def closeEvent(self, event):
        # 关闭数据库
        self.conn.close()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")
    win = MyWin()
    win.show()
    sys.exit(app.exec_())


4. 以进度条方式显示字段的自定义Delegate类

前面章节讲过,如果只是改变item数据项的显示外观,只需要重载QStyledItemDelegate类的 paint() 方法与 sizeHint()方法,即可。
sizeHint() 方法决定了item显示时的单元格大小。 而显示的方式,是在paint()方法内,定义了1个ProgressBar, 并将该列数据转为进度条的百分比。
本例只考虑显示外观,编辑器使用默认即可,无须重载createEdit()方法。 完整代码如下

import sys
import random
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class ProgressDelegate(QStyledItemDelegate):
    
    def sizeHint(self, option, index):
        ans = QStyledItemDelegate.sizeHint(self, option, index)
        ans.setWidth(ans.width()+10)
        return ans
    
    def paint(self,painter, option, index):
        if index.column() == 3:
            progress = int(index.data())
            progressBarOption = QStyleOptionProgressBar()
            progressBarOption.rect = option.rect
            progressBarOption.minimum = 0
            progressBarOption.maximum = 100
            progressBarOption.progress = progress
            progressBarOption.text = str(progress)+"%"
            progressBarOption.textVisible = True            
            QApplication.style().drawControl(QStyle.CE_ProgressBar, progressBarOption, painter)
        else:
            QStyledItemDelegate.paint(self, painter, option, index)

    def editorEvent(self, event: QEvent,
                    model: QAbstractItemModel,
                    option: 'QStyleOptionViewItem',
                    index: QModelIndex):
        if event.type() == QEvent.MouseButtonPress:
            item = index.model().data(index, Qt.UserRole)
            self.lastPos = event.pos()  # - option.rect.topLeft()
            print( f" Pos: ({self.lastPos.x()}, {self.lastPos.y()})")

        return QStyledItemDelegate.editorEvent(self, event, model, option,
                                               index)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(300,100,650,300)
        self.table = QTableView()
        data = [
            [1, 9, 2],
            [1, 0, -1],
            [3, 5, 2],
            [3, 3, 2],
            [5, 8, 9],
        ]
        model_1 = QStandardItemModel(4, 4)
        model_1.setHorizontalHeaderLabels(["Value_1", "Value_2", "Value_3", "Data"])
        model_1.itemChanged.connect(self.onChange)
        for row in range(4):
            for column in range(4):
                item = QStandardItem(str(random.randint(1,100)))
                model_1.setItem(row, column, item)
        self.table.setModel(model_1)
        
        dg = ProgressDelegate(self.table)
        self.table.setItemDelegate(dg)

        self.setCentralWidget(self.table)
    
    def onChange(self,item):
        print("item changed : ", item.text())

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()

5. 用星号来显示表格中评级字段示例

在一些表格化数据显示时,可能希望用星号来显示评级字段,更加直观,如酒店星级字段。 下面的示例自定义了1个delegate类来完成这个任务, 同样本例只重载 paint() 与 sizeHint()两个方法

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
 
class RatingDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        super(RatingDelegate, self).__init__(parent)
    def paint(self, painter, option, index):
        rating = index.data(Qt.DisplayRole)
        star = '\u2605'
        text = star * rating
        painter.drawText(option.rect, Qt.AlignLeft, text)
    # override the sizeHint() methods, add 10 pixels to the width of the cell
    def sizeHint(self, option, index):
        size = super(RatingDelegate, self).sizeHint(option, index)
        size.setWidth(size.width() + 10)
        return size
    

class MyWin(QMainWindow):
    def __init__(self):
        super().__init__()
        self.title = "PyQt5 TableWidget"
        self.setGeometry(100, 100, 600, 380)        
        self.initUI()
    
    def initUI(self):        
        # create an instance of QStandardItemModel, 5 rows and 3 columns. 
        self.model = QStandardItemModel(5, 3)
        header = ['酒店', '单价', '星级']
        self.model.setHorizontalHeaderLabels(header)
        data = [
            ("亚朵", 450, 4),
            ("希尔顿", 700, 5),
            ("假日", 600, 5),
            ("白天鹅", 800, 5),
            ("锦江之星", 290, 3),            
        ]
        for row, item in enumerate(data):
            for column, value in enumerate(item):
                index = self.model.index(row, column, QModelIndex())
                self.model.setData(index, value)
        
        self.tableview = QTableView()
        self.tableview.setModel(self.model)
        
        dg = RatingDelegate()
        self.tableview.setItemDelegateForColumn(2, dg)
        
        self.setCentralWidget(self.tableview)
        self.show()
        

if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    ex = MyWin()
    sys.exit(app.exec_())   

输出如下:
PyQt Model/View编程:通过实例代码来认识 Delegate 组件的强大功能_第6张图片

6. 总结

  • Delegate类在Model/View框架中,负责item单元格数据的显示外观的渲染,当修改数据时负责提供1个编辑器。
  • Delegate组件的基类是 QAbstractItemDelegate,共有3个子类 QItemDelegate , QStyledItemDelegate, QSqlRelationalDelegate. 内置View类默认实现了 QStyledItemDelegate。
  • 在表格可视化方式处理数据库时,可能需要自定义Delegate, 可以选择继承 QStyledItemDelegate子类,item显示外观由 paint() , sizeHint()负责,编辑器由 createEditor() , setEditorData(),setModelData() 3个方法负责。

你可能感兴趣的:(PyQt,pyqt,前端,python,qt)