PyQt5模型视图委托
Model-View-Delegate
模型视图委托(MVD)是PyQt中特有的设计模式,类似MVC设计模式,将MVC设计模式中的Controller当做MVD中的Delegate,两者的概念基本相同。不同的是委托不是独立存在,而是包含在视图里面。
模型视图委托设计模式中,模型负责存储和管理数据;视图负责显示数据,其中界面的框架和基础信息是视图负责,具体数据的显示是委托负责;委托不仅仅负责数据的显示,还有一个重要的功能是负责数据的编辑,如在视图中双击就可以编辑数据。
视图是怎么获取模型数据?首先初始化视图时需要给视图设置模型,然后通过索引获取模型中对应位置的数据。
模型
数据的存储一般是列表,表格和树,不同的存储方式有不同的操作和管理方法,为了适应这种差异性,PyQt中提供了一种统一的操作方法,如下图所示:
对于列表数据:
-
根节点永远是NULL
-
row递增,column是0
对于表结构:
-
根节点永远是NULL
-
row和column递增
对于树结构:
-
根节点是NULL,父节点可变
-
row递增,column是0
模型类:
-
QStandardItemModel 通用存储,可以存储任意结构,最常用
-
QStringListModel 存储一组字符串
-
QDirModel 存储文件系统
-
QSqlQueryModel 对SQL查询的结果进行封装
-
QSqlTableModel 对SQL中的表格进行封装
例子:
import sys from PyQt5.QtCore import Qt from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtWidgets import QApplication, QWidget, QTableView, QHBoxLayout class MyWidget(QWidget): def __init__(self): super(MyWidget, self).__init__() self.mode = QStandardItemModel() root = self.mode.invisibleRootItem() item1 = QStandardItem() item1.setData('1', Qt.DisplayRole) item2 = QStandardItem() item2.setData('2', Qt.DisplayRole) item3 = QStandardItem() item3.setData('3', Qt.DisplayRole) item4 = QStandardItem() item4.setData('4', Qt.DisplayRole) root.setChild(0, 0, item1) root.setChild(0, 1, item2) root.setChild(1, 0, item3) root.setChild(1, 1, item4) # 表结构存储 tableView = QTableView(self) tableView.setModel(self.mode) layout = QHBoxLayout() layout.addWidget(tableView) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) w = MyWidget() w.resize(500, 300) w.move(300, 300) w.setWindowTitle('Simple') w.show() sys.exit(app.exec_())
import sys from PyQt5.QtCore import Qt from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QTreeView class MyWidget(QWidget): def __init__(self): super(MyWidget, self).__init__() self.mode = QStandardItemModel() root = self.mode.invisibleRootItem() item1 = QStandardItem() item1.setData('1', Qt.DisplayRole) item2 = QStandardItem() item2.setData('2', Qt.DisplayRole) item3 = QStandardItem() item3.setData('3', Qt.DisplayRole) item4 = QStandardItem() item4.setData('4', Qt.DisplayRole) # 树结构存储 root.setChild(0, 0, item1) item1.setChild(0, 0, item2) item1.setChild(1, 0, item3) item3.setChild(0, 0, item4) treeView = QTreeView(self) treeView.setModel(self.mode) layout = QHBoxLayout() layout.addWidget(treeView) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) w = MyWidget() w.resize(500, 300) w.move(300, 300) w.setWindowTitle('Simple') w.show() sys.exit(app.exec_())
视图
视图主要是用来显示数据,不同的视图对象用于显示不同存储结构的数据,主要的视图对象如下:
-
QListView 列表形式显示
-
QTableView 表格形式显示
-
QTreeView 树结构显示
单独的视图需要配合模型使用,因此PyQt对视图进行了再次封装,直接内部封装模型,主要对象如下:
-
QListWidget 列表形式显示的界面
-
QTableWidget 表格形式显示的界面
-
QTreeWidget 树结构形式显示的界面
委托
委托被封装在视图里面,主要是负责数据的显示和编辑功能。
数据的编辑主要涉及的方法:
-
createEditor 在双击进入编辑时,创建编辑器,如创建QLineEdit,QTextEdit
-
updateEditorGeometry 设置编辑器显示的位置和大小
-
setEditorData 更新数据到视图
-
setModeData 通过索引更新数据到模型
如果需要修改编辑时操作数据的方式,就需要重写上述方法。
视图主要是负责显示,其中涉及的方法:
-
paint 负责绘制
-
editorEvent 负责处理事件
如果要实现自定义视图显示,需要重写paint方法,在paint方法中绘制需要显示的控件,然后在editorEvent方法中处理事件,更新数据。
例子
在TableView中默认的整型数据编辑使用的是计数器控件,本例中是将计数器变成单行文本控件,实现数据的编辑功能。
import sys from PyQt5.QtCore import Qt, QVariant from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtWidgets import QApplication, QWidget, QTableView, QHBoxLayout, QStyledItemDelegate, QLineEdit class MyTableView(QTableView): def __init__(self): super(MyTableView, self).__init__() class MyDelgate(QStyledItemDelegate): def __init__(self): super(MyDelgate, self).__init__() # 创建编辑器 def createEditor(self, parent, option, index): print('createEditor') if index.column() == 1: return QLineEdit(parent) else: return QStyledItemDelegate.createEditor(self, parent, option, index) # 设置编辑器的位置 def updateEditorGeometry(self, edit, option, index): print('updateEditorGeometry') if index.column() == 1: edit.setGeometry(option.rect) else: return QStyledItemDelegate.updateEditorGeometry(self, edit, option, index) # 设置数据到模型 def setModelData(self, edit, model, index): print('setModelData') if index.column() == 1: model.setData(index, int(edit.text()), Qt.DisplayRole) else: return QStyledItemDelegate.setModelData(self, edit, model, index) # 设置数据到视图 def setEditorData(self, edit, index): print('setEditorData') if index.column() == 1: edit.setText(str(index.data(Qt.DisplayRole))) else: return QStyledItemDelegate.setEditorData(self, edit, index) class MyWidget(QWidget): def __init__(self): super(MyWidget, self).__init__() self.mode = QStandardItemModel() root = self.mode.invisibleRootItem() item1 = QStandardItem() item1.setData('a', Qt.DisplayRole) item2 = QStandardItem() item2.setData(1, Qt.DisplayRole) item3 = QStandardItem() item3.setData(False, Qt.DisplayRole) item4 = QStandardItem() item4.setData('b', Qt.DisplayRole) item5 = QStandardItem() item5.setData(2, Qt.DisplayRole) item6 = QStandardItem() item6.setData(True, Qt.DisplayRole) root.setChild(0, 0, item1) root.setChild(0, 1, item2) root.setChild(0, 2, item3) root.setChild(1, 0, item4) root.setChild(1, 1, item5) root.setChild(1, 2, item6) tableView = MyTableView() tableView.setModel(self.mode) tableView.setItemDelegate(MyDelgate()) layout = QHBoxLayout() layout.addWidget(tableView) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) w = MyWidget() w.resize(500, 300) w.move(300, 300) w.setWindowTitle('Simple') w.show() sys.exit(app.exec_())
在TableView中默认的整型显示是数字字符串,本例中将数字字符串变成进度条显示。
import sys from PyQt5.QtCore import Qt, QEvent from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtWidgets import QApplication, QWidget, QTableView, QHBoxLayout, QStyledItemDelegate, QStyle, QStyleOptionProgressBar, QStyleOptionButton class MyTableView(QTableView): def __init__(self): super(MyTableView, self).__init__() # 一定要注意复写方法的返回值 class MyDelgate(QStyledItemDelegate): def __init__(self): super(MyDelgate, self).__init__() # 委托负责具体数据的显示,因此重新paint方法 def paint(self, painter, option, index): if index.column() == 1: style = QStyleOptionProgressBar() style.minimum = 0 style.maximum = 10 style.progress= index.data(Qt.DisplayRole) style.rect = option.rect QApplication.style().drawControl(QStyle.CE_ProgressBar, style, painter) elif index.column() == 2: style = QStyleOptionButton() if index.data(Qt.DisplayRole) == True: style.state = QStyle.State_On else: style.state = QStyle.State_Off style.state |= QStyle.State_Enabled style.rect = option.rect style.rect.setX(option.rect.x() + option.rect.width() / 2 - 7) QApplication.style().drawControl(QStyle.CE_CheckBox, style, painter) else: return QStyledItemDelegate.paint(self, painter, option, index) def editorEvent(self, event, model, option, index): if index.column() == 2: if event.type() == QEvent.MouseButtonPress and option.rect.contains(event.pos()): data = not index.data(Qt.DisplayRole) model.setData(index, data, Qt.DisplayRole) return True else: return QStyledItemDelegate.editorEvent(self, event, model, option, index) class MyWidget(QWidget): def __init__(self): super(MyWidget, self).__init__() self.mode = QStandardItemModel() root = self.mode.invisibleRootItem() item1 = QStandardItem() item1.setData('a', Qt.DisplayRole) item2 = QStandardItem() item2.setData(1, Qt.DisplayRole) item3 = QStandardItem() item3.setData(False, Qt.DisplayRole) item4 = QStandardItem() item4.setData('b', Qt.DisplayRole) item5 = QStandardItem() item5.setData(2, Qt.DisplayRole) item6 = QStandardItem() item6.setData(True, Qt.DisplayRole) root.setChild(0, 0, item1) root.setChild(0, 1, item2) root.setChild(0, 2, item3) root.setChild(1, 0, item4) root.setChild(1, 1, item5) root.setChild(1, 2, item6) tableView = MyTableView() tableView.setModel(self.mode) tableView.setItemDelegate(MyDelgate()) layout = QHBoxLayout() layout.addWidget(tableView) self.setLayout(layout) if __name__ == '__main__': app = QApplication(sys.argv) w = MyWidget() w.resize(500, 300) w.move(300, 300) w.setWindowTitle('Simple') w.show() sys.exit(app.exec_())
总结:如果修改视图中数据的显示方式,需要重写委托的paint方法,editorEvent方法是处理视图中的点击事件,根据需要绝对是否重写;如果要修改视图中数据的编辑方式,需要重写createEditor方法、updateEditorGeometry方法、setEditorData方法以及setModeData方法。