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方法。