以下是模型/查看待办事项列表教程的稍微修改版本。
我有一个类Heard
,它由Animal
的列表组成。Heard
作为HeardModel
的底层数据,在我的接口中显示在ListView
中。
在我的MainWindow
中,我创建了一个名为add_animal_to_heard
的函数,它:
- 使用用户输入 创建一个新的
- 使用
Heard
类的add_animal
方法将新的Animal
添加到Heard
- 告诉
HeardModel
使用layoutChanged.emit()
更新视图
Animal
我关心的是最后一点。为了管理应用程序中日益增加的复杂性,HeardModel
不应该知道在底层Heard
数据更改时触发布局更改吗?这是可能的吗?如果是,有什么理由是不可取的吗?
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import Qt
from typing import Dict, List
qt_creator_file = "animals.ui"
Ui_MainWindow, QtBaseClass = uic.loadUiType(qt_creator_file)
class Animal:
def __init__(self, genus: str, species: str):
self.genus = genus
self.species = species
def name(self):
return f"{self.genus} {self.species}"
class Heard:
animals: List[Animal]
def __init__(self, animals: List[Animal]):
self.animals = animals
def add_animal(self, animal: Animal):
self.animals.append(animal)
def remove_animal(self, animal: Animal):
self.animals.remove(animal)
class HeardModel(QtCore.QAbstractListModel):
heard: Heard
def __init__(self, *args, heard: Heard, **kwargs):
super(HeardModel, self).__init__(*args, **kwargs)
self.heard = heard
def data(self, index, role):
if role == Qt.DisplayRole:
animal = self.heard.animals[index.row()]
return animal.name()
def rowCount(self, index):
return len(self.heard.animals)
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
Ui_MainWindow.__init__(self)
self.setupUi(self)
self.model = HeardModel(heard=Heard([Animal('Canis', 'Familiaris'), Animal('Ursus', 'Horribilis')]))
self.heardView.setModel(self.model)
self.addButton.pressed.connect(self.add_animal_to_heard)
def add_animal_to_heard(self):
genus = self.genusEdit.text()
species = self.speciesEdit.text()
if genus and species: # Don't add empty strings.
# Create new animal
new_animal = Animal(genus, species)
# Add animal to heard
self.model.heard.add_animal(new_animal)
# Trigger refresh.
self.model.layoutChanged.emit()
# Empty the input
self.genusEdit.setText("")
self.speciesEdit.setText("")
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
您的Heard
对象(也许您指的是"Herd"?)与模型没有直接关系,除非您这样做。
你必须创建一个"链接"在它们之间,可以根据您的需要以各种方式完成。
注意,处理数据(和模型)大小变化的正确的方法不是使用layoutChanged
,而是使用QAbstractItemModel:beginInsertRows()
(必须以endInsertRows()
结束)和beginRemoveRows()
(然后是endRemoveRows()
)的插入/删除函数。这一点非常重要,因为使用这些函数可以确保视图在更改期间保持持久索引列表,从而允许适当的功能:正确处理选择,优化视图更新,并且可能的项编辑器仍将与正确的索引相关联。
最好让模型处理它的行为,而不是在外部这样做:这对于应该在模型类中发出的信号也是有效的。虽然它在技术上不会改变结果,但从对象结构和代码维护的角度来看,它更正确(参见"关注点分离")。
无论如何,最常用的方法是让模型处理底层数据结构中项的插入和删除:class HeardModel(QtCore.QAbstractListModel):
# ...
def add_animal(self, animal):
row = self.rowCount()
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.heard.add_animal(animal)
self.endInsertRows()
def remove_animal(self, animal):
try:
row = self.heard.animals.index(animal)
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self.heard.remove_animal(animal)
self.endRemoveRows()
except ValueError:
print(f'animal {animal.name} not in model')
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# ...
def add_animal_to_heard(self):
genus = self.genusEdit.text()
species = self.speciesEdit.text()
if genus and species: # Don't add empty strings.
self.model.add_animal(Animal(genus, species))
self.genusEdit.clear()
self.speciesEdit.clear()
请注意,这不是获得想要的结果的唯一方法。
例如,您可以在Heard
对象中创建模型的引用,然后在各自的函数中实现上述功能。
在这种情况下,您可以实现insertRows()
和removeRows()
,这样您就可以在Heard
对象的add_animal()
中调用insertRow()
,只要模型引用存在(并且它是一个QAbstractItemModel实例)。
但是,正如文档(包括QAbstractListModel的文档)中所解释的那样,无论如何都必须实现相关的开始/结束函数。
在任何情况下,使用唯一的接口处理模型和数据结构都是非常重要的,否则你将冒着意想不到的结果或致命崩溃的风险:例如,如果你试图从animals
列表中删除一个动物,而没有正确通知模型,它的data()
函数将引发一个IndexError
.
这方面不能低估,特别是考虑到可能"增加应用程序的复杂性";正如你自己注意到的;想象一下,在你最后一次打开项目的几个月后,给程序添加一个新特性或修复一个新发现的bug:不仅在它们建立一段时间后很容易忘记实现的某些方面,而且可能很难再次理解代码做了什么(以及为什么或如何),甚至很难找到新修改可能引入的进一步bug的原因。
也就是说,由于Heard
对象似乎没有实现很多函数,您可以通过将其与模型合并并使用单个类来简化一切:
class HeardModel(QAbstractListModel):
def __init__(self, animals):
super().__init__()
self.animals = animals
你甚至可以更进一步,实际上使用多重继承合并它们:
class Heard:
animals: List[Animal]
def __init__(self, animals):
super().__init__() # important!
self.animals = animals
# etc...
# note: the inheritance order is important
class HeardModel(Heard, QtCore.QAbstractListModel):
# no __init__ override required unless it needs other operations
def add_animal(self, animal):
row = self.rowCount()
self.beginInsertRows(QtCore.QModelIndex(), row, row)
super().add_animal(animal)
self.endInsertRows()
# etc...
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.model = HeardModel([
Animal('Canis', 'Familiaris'),
Animal('Ursus', 'Horribilis')
])
# etc...