学会使用视图委托
尽管PyQt5的列表视图、树形视图和表格视图都已经提供了许多的接口让我们实现想要的功能,但是在数据编辑和显示的个性化控制方面显得有点薄弱。比方说,我们想让表格视图某一列上的单元格都默认将数值以进度条(QProgress)的方式来显示,而当用户双击单元格进行编辑时,进度条消失,出现一个数值调节框(QSpinBox)。用户编辑结束后,进度条再次显示,且进度值等于新数值。
显然,列表、树形和表格视图本身提供的接口很难让我们实现以上功能,而我们用视图委托的话却能够轻松实现。
1. 基本介绍
PyQt5给我们提供了QItemDelegate和 QStyledItemDelegate类,用于实现委托功能。这两者没多大区别,只不过后者能够基于当前界样式在界面上绘制出新的内容,并且能够使用QSS。所以笔者建议通过继承QStyledItemDelegate类来实现委托功能。
from PyQt5.QtWidgets import QStyledItemDelegate
class DelegateDemo(QStyledItemDelegate):
def __init__(self):
super(DelegateDemo, self).__init__()
继承后我们可以通过重新paint()、createEditor()、setEditorData()、updateEditorGeometry()和setModelData()函数来绘制控件以及控制数据的编辑和显示方式:
from PyQt5.QtWidgets import QStyledItemDelegate
class DelegateDemo(QStyledItemDelegate):
def __init__(self):
super(DelegateDemo, self).__init__()
def paint(self, painter, option, index):
pass
def editorEvent(self, event, model, option, index):
pass
def createEditor(self, parent, option, index):
pass
def updateEditorGeometry(self, editor, option, index):
pass
def setEditorData(self, editor, index):
pass
def setModelData(self, editor, model, index):
pass
委托功能编写完毕后,我们可以调用相应视图的setItemDelegate方法将委托中绘制的东西映射到视图上:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTableView
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.table = QTableView(self)
delegate_demo = DelegateDemo()
self.table.setItemDelegate(delegate_demo)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
下面我们用实例来讲解下委托中各个函数的作用。
笔者在这一章将只用表格QTableView来进行示范,当然知识点同样适用列表视图和树形视图,读者可以在学习完本章内容后再拿后面两种视图进行练习。
同样适用QListWidget、QTreeWidget和QTableWidget。
2. paint()和editorEvent()
通过paint函数我们可以对视图上特定的行、列或项进行渲染。
- painter是我们的绘图工具,有了它就可以在界面上绘制出很多东西。关于绘图的一些知识点,请大家去看下《快速掌握PyQt5》 绘图与打印这一章节。
- option参数包含了目标项的一些具体信息,比如我们要知道目标绘制区域(项)大小的话,就可以通过option.rect获取到。
- index参数可以帮我们获取到指定的行、列或项,当然也可以通过该参数拿到相应项上的数据。
在表格视图中,一个单元格就是一个项(item)。
了解了该函数的作用后,我们来看下如何在一个单元格中个性化显示图片和其它样式:
图片下载地址:https://www.easyicon.net/1229836-heart_icon.html
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QStandardItem, QStandardItemModel, QPixmap, QBrush, QPen
from PyQt5.QtWidgets import QApplication, QWidget, QTableView, QStyledItemDelegate, QHBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(650, 240)
self.model = QStandardItemModel(6, 6) # 1
for row in range(6):
for column in range(6):
item = QStandardItem(f'({row}, {column})')
self.model.setItem(row, column, item)
self.table = QTableView()
self.table.setModel(self.model)
self.table.horizontalHeader().setStretchLastSection(True)
delegate_demo = DelegateDemo() # 2
self.table.setItemDelegate(delegate_demo)
h_layout = QHBoxLayout()
h_layout.addWidget(self.table)
self.setLayout(h_layout)
class DelegateDemo(QStyledItemDelegate):
def __init__(self):
super(DelegateDemo, self).__init__()
def paint(self, painter, option, index):
if index.row() == 0 and index.column() == 0: # 3
rect = option.rect
painter.drawPixmap(rect.x(), rect.y(), 20, 20, QPixmap('heart.png'))
painter.drawPixmap(rect.x()+20, rect.y(), 20, 20, QPixmap('heart.png'))
painter.drawPixmap(rect.x()+40, rect.y(), 20, 20, QPixmap('heart.png'))
elif index.row() == 0 and index.column() == 1: # 4
rect = option.rect
x = rect.x()
y = rect.y()
width = rect.width()
height = rect.height()
painter.drawPixmap(x+width/2-10, y+height/2-10, 20, 20, QPixmap('heart.png'))
elif index.row() == 0 and index.column() == 2: # 5
rect = option.rect
data = index.data()
painter.drawText(rect.x(), rect.y()+10, data)
elif index.row() == 0 and index.column() == 3: # 6
brush = QBrush(Qt.SolidPattern)
brush.setColor(Qt.red)
rect = option.rect
painter.fillRect(rect.x(), rect.y(), rect.width()/2, rect.height(), brush)
elif index.row() == 0 and index.column() == 4: # 7
pen = QPen(Qt.SolidLine)
painter.setPen(pen)
painter.drawRect(option.rect)
elif index.column() == 5: # 8
rect = option.rect
data = index.data()
painter.drawPixmap(rect.x(), rect.y(), 20, 20, QPixmap('heart.png'))
painter.drawText(rect.x()+50, rect.y()+15, data)
else: # 9
option.displayAlignment = Qt.AlignCenter
return super(DelegateDemo, self).paint(painter, option, index)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 定义一个QStandardItemModel模型,并在每个单元格上显示行列坐标。在没设置委托前,程序界面显示如下:
2. 实例化我们写好的委托,并将其设置到视图上。
3. 通过index参数获取行列值,从而有针对性地对不同单元格(项)进行渲染。如果单元格所在的行列值都为0,那么我们先通过option.rect获取到该单元格的区域范围(QRect类型),接着调用painter的drawPixmap方法在指定位置绘制图片。显示如下:
4. 绘制一张图片,并居中显示:
5. 通过index参数获取到目标单元格的数据,并调用drawText方法绘制在指定位置:
6. 实例化一个笔刷,并调用fillRect方法填充半个单元格:
7. 实例化一个画笔,并给单元格画上边框(绘制不完整):
8. 注意这里的判断稍有不同,笔者是想给这一列上的所有单元格都进行绘制。绘制操作也很简单,就是调用drawPixmap和drawText方法绘制图片和文本:
在绘制文本时,x坐标给的是rect.x()+50。这个50是一个固定值,这样就没法做到自适应,也就是说单元格变大时,文本位置还是不变的:
所以我们可以将其改成rect.x()+rect.width()/2,让文本位置根据单元格的宽度自动调整:
9. 除了以上特定的单元格,剩余单元格全部按照原先样式居中显示(返回父类的paint函数就表示将原先的样式也绘制出来):
通过上面的例子我们知道了可以用paint函数来个性化绘制一些图片、文本和边框之类的等等,那如果我们想要绘制一个控件呢?这时候就不得不提到QStyle这个类了。QStyle非常强大,封装了一个GUI界面的视觉样式和效果,通过它我们可以让界面呈现出不同的风格。不过笔者这里将只会讲解如何使用这个类的drawControl方法,其他的知识点还请大家自行浏览下文档。drawControl方法介绍如下:
- element参数指待绘制控件的表现形式,比如我们要绘制一个进度条的话,就传入QStyle.CE_ProgressBar;如果要绘制复选框,那就传入QStyle.CE_CheckBox。element参数可填写的值汇总列表请见该链接。
- option参数指待绘制控件的属性,我们通常会传入QStyleOption的子类。比如要绘制的是进度条,那么我们就实例化一个QStyleOptionProgressBar,再设置相关属性后传给option。如果是复选框,那我们这里就用一个QStyleOptionButton。子类汇总如下:
- painter的话就传入paint函数的painter参数。
- widget参数是可选的,在绘制时起辅助作用。没怎么用到,笔者这里就不赘述了。
现在我们通过一个示例来演示下如何使用这个方法。在下面这个例子中,笔者在单元格(项)中绘制了进度条和复选框按钮:
class DelegateDemo(QStyledItemDelegate):
def __init__(self):
super(DelegateDemo, self).__init__()
def paint(self, painter, option, index):
if index.row() == 0 and index.column() == 0: # 1
progress_style = QStyleOptionProgressBar()
progress_style.rect = option.rect
progress_style.minimum = 0
progress_style.maximum = 100
progress_style.progress = 50
QApplication.style().drawControl(QStyle.CE_ProgressBar, progress_style, painter)
elif index.row() == 0 and index.column() == 1: # 2
check_style = QStyleOptionButton()
check_style.rect = option.rect
if index.data():
check_style.state = QStyle.State_Enabled | QStyle.State_On
else:
check_style.state = QStyle.State_Enabled | QStyle.State_Off
QApplication.style().drawControl(QStyle.CE_CheckBox, check_style, painter)
else:
return super(DelegateDemo, self).paint(painter, option, index)
将代码模块导入部分修改如下:
import sys
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QApplication, QWidget, QTableView, QStyledItemDelegate, QHBoxLayout, QStyle, \
QStyleOptionProgressBar, QStyleOptionButton
1. 实例化一个QStyleOptionProgressBar对象,设置进度条的相关属性。调用QApplication.style()方法获取到程序界面的QStyle对象,然后再调用drawControl绘制出一个进度条。
2. 实例化一个QStyleOptionButton对象,并设置复选框按钮的属性。如果单元格中有数值的话,那么复选框就处于选中状态,没有值的话就处于非选中状态(状态属性值请到该页面进行查阅)。最后再通过drawControl方法进行绘制。
运行截图如下:
大家运行程序后可能会发现复选框按钮不能通过鼠标点击来改变状态,因为paint只负责绘制,具体的事件响应逻辑我们还需要通过其他函数来实现。这里我们只需要重新实现下editorEvent方法就好:
- model指当前视图的模型,通过该参数我们可以更新模型值。
现在给上面的程序添加如下代码:
def editorEvent(self, e, model, option, index):
if index.row() == 0 and index.column() == 1:
if e.type() == e.MouseButtonPress:
if index.data():
model.setData(index, '')
else:
model.setData(index, '(0, 1)')
return True
return super(DelegateDemo, self).editorEvent(e, model, option, index)
如果当前操作的是目标单元格的话,判断下是不是鼠标按下事件,是的话再判断当前项有没有数据,接着调用setData方法修改目标单元格内容。修改之后paint函数会根据单元格的内容重新绘制。
3. createEditor()
通常我们双击单元格后,单元格内会出现一个输入框让我们修改数据。那假如我想在双击后出现一个数字调节框QSpinBox呢?那此时就需要用createEditor方法创建一个出来。
- parent指定新建控件的父类,我们只需在控件实例化时传入即可。
- 该函数最后要返回一个控件。
以下是示例代码:
import sys
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QApplication, QWidget, QTableView, QStyledItemDelegate, QHBoxLayout, QStyle, \
QStyleOptionProgressBar, QSpinBox
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(650, 240)
self.model = QStandardItemModel(6, 6) # 1
self.model.setItem(0, 0, QStandardItem('10'))
self.table = QTableView()
self.table.setModel(self.model)
self.table.horizontalHeader().setStretchLastSection(True)
delegate_demo = DelegateDemo()
self.table.setItemDelegate(delegate_demo)
h_layout = QHBoxLayout()
h_layout.addWidget(self.table)
self.setLayout(h_layout)
class DelegateDemo(QStyledItemDelegate):
def __init__(self):
super(DelegateDemo, self).__init__()
def paint(self, painter, option, index):
if index.row() == 0 and index.column() == 0:
progress_style = QStyleOptionProgressBar()
progress_style.rect = option.rect
progress_style.minimum = 0
progress_style.maximum = 100
progress_style.progress = int(index.data()) # 2
QApplication.style().drawControl(QStyle.CE_ProgressBar, progress_style, painter)
def createEditor(self, parent, option, index): # 3
print('createEditor')
if index.row() == 0 and index.column() == 0:
spin_box = QSpinBox(parent)
spin_box.setRange(0, 100)
return spin_box
else:
return super(DelegateDemo, self).createEditor(parent, option, index)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
1. 创建好模型后,给第一个单元格设置一个初始值10(字符串)。
2. 在paint函数中让进度条的值等于单元格的数值,要将字符串转为整型值。
3. 在createEditor函数中,创建一个QSpinBox对象,设置调节范围为1-100(正好是进度条的最大最小值),最后返回该对象。
运行截图如下:
初始界面
双击单元格进行编辑
编辑结束后进度条值也会更新
4. updateEditorGeometry()
我们已经在createEditor函数中返回一个QSpinBox,所以当用户双击单元格时会出现一个数字调节框。那如果想改变这个调节框的位置和大小,就需要实现updateEditorGeometry函数了。
- editor就是createEditor函数返回的控件。
- 我们可以让这个控件调用setGeometry方法,从而改变它自己的位置和大小。
- option参数能够给我们提供单元格(项)的大小。
新增代码如下:
def updateEditorGeometry(self, editor, option, index):
print('updateEditorGeometry')
if index.row() == 0 and index.column() == 0:
rect = option.rect
editor.setGeometry(rect.x(), rect.y(), rect.width(), rect.height() / 2)
else:
return super(DelegateDemo, self).updateEditorGeometry(editor, option, index)
笔者这里调整了QSpinBox的大小,让它的高度变为单元格高度的一半。
运行截图如下:
当然不要像笔者这样做,因为进度条都还可以看到。
5. setEditorData()
该函数的作用就是把模型中相应单元格(项)的数据显示到editor(QSpinBox)上,但大家会发现其实不用这个方法也完全没关系,因为在上面的代码中,当我们双击进行编辑时,QSpinBox就已经会显示最新编辑过的值了。
- 通过index.data()获取到数据,然后调用editor控件相应的方法将该数值显示出来。
新增代码如下:
def setEditorData(self, editor, index):
print('setEditorData')
if index.row() == 0 and index.column() == 0:
editor.setValue(int(index.data()))
else:
super(DelegateDemo, self).setEditorData(editor, index)
当然如果我们想在每次编辑时,QSpinBox都能够显示同一个值,那重新实现这个函数是比较有意义的:
editor.setValue(50)
6. setModelData()
该函数的作用是将编辑好的值更新到模型中,但其实在我们这个例子中就算不实现,也是会自动更新的,所以是否要重新实现setEditorData()和setModelData()函数(或者怎样实现)还是要根据项目需求来定。
- 首先获取到editor控件上的值,然后通过model.setData()将其更新到模型中。
新增代码如下:
def setModelData(self, editor, model, index):
print('setModelData')
if index.row() == 0 and index.column() == 0:
model.setData(index, editor.value())
else:
return super(DelegateDemo, self).setModelData(editor, model, index)
相信阅读过这一章后,大家对视图委托的用法也有了一定了解,希望大家能够好好运用起来,给自己程序中的视图添加更多个性化的东西。