当底层数据改变时,QAbstractItemModel可以触发布局改变吗?



以下是模型/查看待办事项列表教程的稍微修改版本。

我有一个类Heard,它由Animal的列表组成。Heard作为HeardModel的底层数据,在我的接口中显示在ListView中。

在我的MainWindow中,我创建了一个名为add_animal_to_heard的函数,它:

  1. 使用用户输入
  2. 创建一个新的Animal
  3. 使用Heard类的add_animal方法将新的Animal添加到Heard
  4. 告诉HeardModel使用layoutChanged.emit()更新视图

我关心的是最后一点。为了管理应用程序中日益增加的复杂性,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...

最新更新