PyQt把MVC中的 View 与 Control 放在一起,将数据管理与呈现功能分开,不同的视图可以使用相同的model数据,称为 Model/View 方法。
用Widget控件操作数据时,数据与视图是放在一起。
Model/View方式,数据与视图是分开管理
这个编程方法中,除了model, view组件, 还引入了 delegate 的概念,负责item数据项级别的呈现(display)与编辑(edit)功能。因此Model/View模式提供了3种组件,并提供了相应的类,有的功能是虚方法,需要手工实现。
Delegate 负责数据项(item) 的显示以及提供编辑控件。当修改数据时,由Delegate负责编辑器器与model之间通信,如读取、更新数据。
Delegate,字面上是item的代理,其作用类似于item数据项项这个粒度的view+controller。笔者认为此处用英文更容易理解。
默认QListView, QTableView, QTreeView都提供了默认的Delegate类实现。有时,我们希望将数据以图形化方式呈现,如下图,将进度按进度条的方式来呈现。
默认View提供的编辑器是1个QLineEdit控件, 可视化数据库时,通常会遇到多选值字段,编辑这类字段希望提供下拉列表comboBox的功能。
上述两种场景,需要自定义delegate来实现,本文将以实例方式讲解如何实现。
Delegate基类为 QAbstractItemDelegate, 有3个子类,关系如下:
Delegate类的选择
继承QStyledItemDelegate类,修改item的外观,必须要重载下列方法:
paint()
sizeHint()
自定义editor 编辑器,需要重载以下方法:
createEditor()
创建editor , closeEditor() 关闭编辑器 setEditorData()
, 从model -> editor populate 数据setModelData()
从 editor -> 更新model或者使用editorEvent方法来更新
editorEvent(event, model, option, index)
其它可选方法:
updateEditorGeometry()
下拉列表在数据库Choices字段时必然会用到,因此是可视化数据时必然会用到的1个自定义delegate。
本例,编辑model中state字段时,用combo下拉列表控件将可选择的数据项列出,用户可从中选择以方便修改。
注意: 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_())
前面章节讲过,如果只是改变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_()
在一些表格化数据显示时,可能希望用星号来显示评级字段,更加直观,如酒店星级字段。 下面的示例自定义了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_())
paint() , sizeHint()
负责,编辑器由 createEditor()
, setEditorData()
,setModelData()
3个方法负责。