Home > PyQt5, python > PyQt5: model-view

PyQt5: model-view

8 Maggio 2019

Torna all’indice degli appunti

Programmazione Model/View

Qt contiene un set di classi che usano un’architettura model/view per la gestione della relazione che intercorre tra i dati dell’applicazione (model), e il modo di rappresentarli all’utente (view).
L’utilizzo di questa architettura introduce la separazione tra le varie funzionalità che permettono una maggiore flessibilità nella presentazione degli elementi.
Il pattern model/view è simile al più noto MVC (Model-View-Controller), la differenza è che in questa architettura gli oggetti view e controller sono combinati insieme, introducendo il concetto di Delegate.


Nel pattern model/view, il model comunica con la sorgente di dati (Data) e fornisce un’interfaccia per gli altri componenti di questa architettura.
La view, ottiene dal model degli indici (model-indexes), che sono i riferimenti agli elementi della sorgente dei dati.
Fornendo questi indici al model, la view può ottenere gli elementi dei dati dalla sorgente (Data) e rappresentarli.
Il Delegate fa da collegamento tra model e view.
Quando un elemento viene editato da view, il delegate comunica direttamente con il model attraverso il model-index di tale elemento.
In generale quindi, le classi model/view possono essere suddivise in tre gruppi: models, views e delegates.
Ognuno di questi componenti è definito da una abstract class, che fornisce un’interfaccia comune che può essere subclassata, in modo da poter reimplementare i metodi necessari al nostro scopo.

Ovviamente Models, Views e Delegates comunicano tra loro usando segnali e slots.

I segnali emessi dal model informano la view che i dati in possesso di Data, sono cambiati;
I segnali emessi dalla view informano gli altri componenti se sono avvenute modifiche da parte dell’utente;
I segnali emessi dal delegate durante l’editing di un elemento, informano model e view, dello stato dell’editor;

MODELS

Il componente model si basa sulla classe QAbstractItemModel, che fornisce un’interfaccia comune utilizzata da view e delegate per avere accesso ai dati.
Tale interfaccia è flessibile a tal punto da poter gestire views che rappresentano ad esempio:


Come anticipato, il model ha anche il compito di notificare, a tutte le views ad esso collegato, quando un dato cambia e questo viene effettuato tramite il meccanismo segnali/slot.

Qt mette a disposizione alcuni models preconfezionati, come ad esempio:

QStringListModel: usato per immagazzinare una semplice lista di stringhe;
QStandardItemModel: gestisce strutture di elementi ad albero, dove ogni elemento può contenere diversi tipi di dato;
QFileSystemModel: usato per gestire informazioni su files e directories;

model index

Per far sì che la rappresentazione di un dato (view) rimanga separata dal modo di accedere al dato stesso, viene introdotto il concetto di model-index.
Ogni singolo pezzo di informazione di un dato che può essere ottenuto dal model, viene rappresentato da un model-index. Tale indice viene utilizzato dalla view e dal delegate per rappresentare il dato richiesto.
I model-indexes forniscono riferimenti temporanei a pezzi di informazione e possono quindi essere usati per ottenere, o modificare i dati,attraverso il modello.
Dal momento che i models possono riorganizzare la propria struttura interna di volta in volta, i model-indexes possono diventare non validi e non dovrebbero essere immagazzinati.
Se invece si dovesse avere la necessità di un long term model-index, sarebbe bene utilizzare esplicitamente un persisten model-index:

QModelIndex: utilizzato come riferimento temporaneo all’informazione;
QPersistentModelIndex: utilizzato come riferimento persistente all’informazione.

Per ottenere un model-index che corrisponda ad un dato, devono essere specificate 3 proprietà al model: row, column e parent model-index.

Row e Column

Nella sua forma più semplice, un model può essere visto come una tabella, dove, ogni suo elemento, può essere localizzato con i numeri di riga (row) e di colonna (column).

>>> model = QStandardItemModel()
>>> model.setItem(0, 0, QStandardItem("A"))
>>> model.setItem(1, 1, QStandardItem("B"))
>>> model.setItem(2, 1, QStandardItem("C"))
>>> from PyQt5.QtCore import QModelIndex
>>> index_a = model.index(0, 0, QModelIndex())
>>> index_b = model.index(1, 1, QModelIndex())
>>> model.itemFromIndex(index_a).text()
'A'
>>> model.itemFromIndex(index_b).text()
'B'

In tutti i casi dove gli elementi sono di tipo top-level, il parent index è sempre QModelIndex().
Diverso è il caso del tree model dove ci sono elementi (child) che avranno bisogno di un parent index.

>>> model = QStandardItemModel()
>>> model.setItem(0, 0, QStandardItem("A"))
>>> item_a = model.item(0, 0)
>>> item_a.setChild(1, 0, QStandardItem("B"))
>>> item_a.hasChildren()
True
>>> index_a = model.index(0, 0, QModelIndex())
>>> index_b = model.index(1, 0, index_a)
>>> model.itemFromIndex(index_a).text()
'A'
>>> model.itemFromIndex(index_b).text()
'B'
>>> model.itemFromIndex(index_b).parent().text() # il parent di B
'A'

Item Roles

Gli elementi in un model sono costituiti da diversi roles (ruoli), nel senso che un certo tipo di dato ha una funzione ben precisa.
Ad esempio Qt.DisplayRole è usato per accedere alla stringa che viene visualizzata come testo, in una view,
mentre Qt.ToolTipRole è il dato che apparirà nel tooltip relativo ad un elemento e così via.
Quindi Un elemento (item), contiene diversi dati, uno per ogni Qt.ItemDataRole.
L’accesso a questi dati del model è garantito dal metodo data(index, role), dove index è l’indice dell’item e role il Qt.Role assegnato al dato:

...
>>> item_a.setData("Tool tip of A item", Qt.ToolTipRole)
>>> model.data(index_a, Qt.ToolTipRole)
'Tool tip of A item'

VIEWS

Come detto, nell’architettura model/view, una view ottiene i dati dal model e li mostra all’utente.
La separazione tra contenuti e loro rappresentazione, è ottenuta dall’utilizzo di una standard model interface (QAbstractItemModel), di una standard view interface (QAbstractItemView) e di model indexes.
Una view può essere costruita senza model, ma bisognerà fornirle un model non appena si vorranno visualizzare le informazioni richieste.
Il model viene fornito alla view con il metodo setModel(model) dove model è un oggetto QStandardItemModel.

All’occorrenza, sono già disponibili implementazioni complete di views, come ad esempio:

QListView: per visualizzare i dati come lista;
QTableView: per visualizzare i dati in forma tabellare;
QTreeView: per visualizzare i dati in forma di lista gerarchica;

Vediamo un’esempio con QListView:

import sys
from PyQt5.QtWidgets import (QApplication, QListView, QMainWindow, QVBoxLayout,
                             QWidget)
from PyQt5.QtGui import QStandardItemModel, QStandardItem


class MainWindow(QMainWindow):
    def __init__(self, parent=None, model=None):
        super(MainWindow, self).__init__(parent)
        self.setWindowTitle("QListView Example")
        self.central_widget = FormWidget(self, model)
        self.setCentralWidget(self.central_widget)
        self.resize(250, 100)


class FormWidget(QWidget):
    def __init__(self, parent, model=None):
        super(FormWidget, self).__init__(parent)
        layout = QVBoxLayout()
        self.setLayout(layout)
        self.view = QListView()
        self.view.setModel(model)
        layout.addWidget(self.view)


if __name__ == "__main__":
    app = QApplication([sys.argv])
    iterable = ["italy", "france", "spain"]
    model = QStandardItemModel()
    for row, name in enumerate(iterable):
        item = QStandardItem(name)
        item.setCheckable(True)
        model.setItem(row, 0, item)
    main_window = MainWindow(parent=None, model=model)
    main_window.show()
    sys.exit(app.exec_())

DELEGATES

Il componente delegate si basa sulla classe QAbstractItemDelegate.
L’implementazione standard di tale classe è QStyledItemDelegate e viene usata come delegate di default nelle views standard.
A differenza del pattern MVC, delegate non è un componente separato come lo sarebbe il Controller.
Le standard views di Qt usano un’istanza di QItemDelegate. Questa implementazione di delegate, permette di rappresentare tutti gli elementi, in uno stile riconoscibile da tutte le standard views (QListView, QTable, QTreeView, ecc). Il delegate di default gestisce anche tutti i roles utilizzati dalle suddette views.
E’ possibile ottenere l’oggetto delegate utilizzato da una view, con il metodo itemDelegate:

>>> import sys
... from PyQt5.QtWidgets import (QApplication, QListView, QMainWindow, QVBoxLayout,
...                              QWidget)
>>> app = QApplication([])
>>> iterable = ["italy", "france", "spain"]
>>> from PyQt5.QtGui import QStandardItemModel, QStandardItem
>>> model = QStandardItemModel()
>>> for row, name in enumerate(iterable):
...     model.setItem(row, 0, QStandardItem(name))
>>> view = QListView()
>>> view.setModel(model)
>>> view.itemDelegate()
<PyQt5.QtWidgets.QStyledItemDelegate object at 0x0361E300>

Nel caso si utilizzi una view personalizzata, è bene utilizzare un delegate personalizzato e per settare tale delegate si utilizzerà il metodo setItemDelegate(custom_delegate).
Stesso discorso vale per il widget editor che permette l’editing dell’elemento, sarà necessario l’editor con il metodo createEditor.
Quando l’utente, tramite l’editor, avrà modificato un elemento, il delegate avrà il compito di comunicare tale “nuovo” dato al model. Questa operazione viene effettuata con il metodo setModelData(editor, model, modelindex).

Viediamo un esempio completo.

VIEW MODEL E DELEGATE personalizzati

Vediamo come gestire, con un custom-model ed un custom-delegate, un custom widget come ad esempio una list-view.
Quando si crea una list-view personalizzata, è necessario subclassare QAbstractListModel.
Come si nota dal sito ufficiale, quando si subclassa QAbstractListModel, bisogna implementare i metodi:

rowCount
data

Facoltativamente anche headerData.

rowCount(parent_index): ritorna il numero di righe (row) sotto il parent_index passato come argomento;
data(index, role): ritorna il dato immagazzinato per l’elemento di indice index, per il role specificato;

Iniziamo dal custom list-model:

class CustomListModel(QAbstractListModel):
    def __init__(self, parent=None, iterable=[]):
        super(CustomListModel, self).__init__(parent)
        self._data = iterable

    def rowCount(self, parent=QModelIndex()):
        return len(self._data)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid() or not 0 <= index.row() < self.rowCount():
            return QVariant()
        row = index.row()
        if role == Qt.DisplayRole:
            return str(self._data[row])
        return QVariant()

Come da manuale, abbiamo implementato i metodi rowCount e data.

>>> from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, QVariant
>>> custom_model = CustomListModel(parent=None, iterable=[10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
>>> custom_model.rowCount()
10
>>> id_row4 = custom_model.index(3)
>>> custom_model.data(id_row4)
'40'
>>> custom_model.data(id_row3, Qt.BackgroundRole)
<PyQt5.QtCore.QVariant object at 0x0355D670>

Come si nota, nel caso non vengano trovati dati per il role specificato, verrà restituito un generico QVariant.
QVariant, in parole povere, è una classe che può rappresentare tutti i tipi di dati Qt.

Implementiamo il model personalizzato nel codice di esempio precedente:

import sys
from PyQt5.QtWidgets import (QApplication, QListView, QMainWindow, QVBoxLayout,
                             QWidget)
from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, QVariant


class MainWindow(QMainWindow):
    def __init__(self, parent=None, model=None):
        super(MainWindow, self).__init__(parent)
        self.setWindowTitle("Custom ListView Example")
        self.central_widget = FormWidget(self, model)
        self.setCentralWidget(self.central_widget)
        self.resize(300, 200)


class FormWidget(QWidget):
    def __init__(self, parent, model=None):
        super(FormWidget, self).__init__(parent)
        layout = QVBoxLayout()
        self.setLayout(layout)
        self.view = QListView()
        self.view.setModel(model)
        layout.addWidget(self.view)


class CustomListModel(QAbstractListModel):
    def __init__(self, parent=None, iterable=[]):
        super(CustomListModel, self).__init__(parent)
        self._data = iterable

    def rowCount(self, parent=QModelIndex()):
        return len(self._data)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid() or not 0 <= index.row() < self.rowCount():
            return QVariant()
        row = index.row()
        if role == Qt.DisplayRole:
            return str(self._data[row])
        return QVariant()


if __name__ == "__main__":
    app = QApplication([sys.argv])
    iterable = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
    model = CustomListModel(parent=None, iterable=iterable)
    main_window = MainWindow(parent=None, model=model)
    main_window.show()
    sys.exit(app.exec_())

Vediamo ora il custom-Delegate.

Quando in una view visualizziamo un dato dal model, il delegate è il componente che si occupa di disegnare l’elemento rappresentante tale dato.
Si occupa anche di fornire un editor nel momento in cui tale elemento viene editato.
Il dato verrà disegnato dal delegate in modo diverso a seconda del role corrispondente.
Il delegate di default è QStyledItemDelegate.
Il metodo che ci interessa implementare è paint(painter, option, index, dove painter è un oggetto QPainter, necessario per il disegno di linee e forme e option, un oggetto QStyleOptionViewItem, fondamentale per descrivere i parametri necessari al disegno di un elemento in una view.
Sarà molto importante utilizzare le variabili rect e state di option, per sapere rispettivamente, dove disegnare e se l’elemento è attivo o selezionato.

Per il nostro esempio vogliamo disegnare una progressbar in ogni elemento della list-view, quindi:

class CustomDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        item_var = index.data(Qt.DisplayRole)  # QVariant corrispondente
        item_str = item_var.toPyObject()
        opts = QStyleOptionProgressBar()
        opts.rect = option.rect
        opts.minimum = 0
        opts.maximum = 100
        opts.text = item_str
        opts.textAlignment = Qt.AlignCenter
        opts.textVisible = True
        opts.progress = int(item_str)
        QApplication.style().drawControl(QStyle.CE_ProgressBar, opts, painter)

per implementare il nostro custom-delegate, basterà utilizzare il metodo della view: setDelegate(delegate):

import sys
from PyQt5.QtWidgets import (QApplication, QListView, QMainWindow, QVBoxLayout,
                             QWidget, QStyledItemDelegate, QStyle,
                             QStyleOptionProgressBar)
from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, QVariant


class MainWindow(QMainWindow):
    ...


class FormWidget(QWidget):
    ...
        self.view = QListView()
        self.view.setModel(model)
        delegate = CustomDelegate()
        self.view.setItemDelegate(delegate)
        layout.addWidget(self.view)


class CustomListModel(QAbstractListModel):
    ...


class CustomDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        item_data = index.data(Qt.DisplayRole)  # QVariant corrispondente
        opts = QStyleOptionProgressBar()
        opts.rect = option.rect
        opts.minimum = 0    # limite minimo progress bar
        opts.maximum = 100  # limite massimo progress bar
        opts.text = "{}/{} [{}%]".format(item_data, 100, int(item_data))
        opts.textAlignment = Qt.AlignCenter
        opts.textVisible = True
        opts.progress = int(item_data)
        QApplication.style().drawControl(QStyle.CE_ProgressBar, opts, painter)


if __name__ == "__main__":
    app = QApplication([sys.argv])
    iterable = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
    model = CustomListModel(parent=None, iterable=iterable)
    main_window = MainWindow(parent=None, model=model)
    main_window.show()
    sys.exit(app.exec_())

Manca ora l’attivazione dell’editor per la modifica del dato dell’elemento.

Prima di tutto bisogna ricordarsi che l’editor è attivabile se il custom model prevede tra i suoi flag, Qt.ItemIsEditable, quindi reimplementiamo il metodo
flags ereditato da QAbstractItemModel:

class CustomListModel(QAbstractListModel):
    ...
    def flags(self, index):
        flags_ = super(CustomListModel, self).flags(index)
        return flags_ | Qt.ItemIsEditable
    ...

In questo modo, tra i flag di ogni elemento di indice index, ci sarà sempre il flag ItemIsEditable.

Ora reimplementiamo i 3 metodi fondamentali del delegate:

createEditor: ritorna il widget utilizzato per modificare il dato proveniente dal model;
setEditorData: fornisce il widget con il dato da modificare;
setModelData: ritorna il dato modificato al model;

Già che ci siamo utilizziamo un widget QSpinBox per modificare il dato:

class CustomDelegate(QStyledItemDelegate):
    ...

    def createEditor(self, parent, option, index):
        editor = QSpinBox(parent)
        editor.setRange(0, 100)
        return editor

    def setEditorData(self, editor, index):
        value = index.data(Qt.DisplayRole)
        editor.setValue(int(value))

    def setModelData(self, editor, model, index):
        value = editor.value()
        variant = QVariant(value)
        model.setData(index, variant)

Come si nota, in createEditor creiamo il widget SpinBox che utilizzeremo per la modifica del dato, con setEditorData, facciamo in modo che al doppio click, quando si attiva il widget editor per l’elemento editato, il dato di tale elemento sia visibile, infine, setModelData, metodo con il quale avvisiamo il model della modifica effettuata.
Dei 3 metodi di QItemDelegate, setModelData è il puù importante poichè prende il dato dall’editor widget e lo immagazzina nel model, ad un determinato indice index.

Non ci resta che tornare nuovamente al custom model e reimplementare il metodo setData:

class CustomListModel(QAbstractListModel):
    ...
    def setData(self, index, variant, role=Qt.EditRole):
        if role == Qt.EditRole:
            self._data[index.row()] = variant.value()
            self.dataChanged.emit(index, index)  # emetto il segnale
            return True
        return False
    ...

Come da specifica, il metodo setData di QAbstractItemModel da cui deriva il nostro QAbstractListModel, prende il dato di un determinato role e lo setta nell’elemento di indice index.
Se il dato viene settato, il metodo deve emettere il segnale dataChanged e poi ritornare True, altrimenti ritornare False.
Il segnale dataChanged viene emesso ogni volta che il dato di un elemento (item) viene modificato e serve ad aggiornare la view, negli elementi coinvolti dalla modifica, quelli compresi tra topleft_index e bottomright_index.
Nota: Essendo in una lista, topleft_index e bottomright_index coincidono.
con index.row recuperiamo l’indice di riga del list-model e modifichiamo in tale indice, il dato prelevato dall’oggetto variant in arrivo dal delegate tramite il metodo setModelData.

il codice definitivo si traduce quindi in:

import sys
from PyQt5.QtWidgets import (QApplication, QListView, QMainWindow, QVBoxLayout,
                             QWidget, QStyledItemDelegate, QStyle, QSpinBox,
                             QStyleOptionProgressBar)
from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, QVariant


class MainWindow(QMainWindow):
    def __init__(self, parent=None, model=None):
        super(MainWindow, self).__init__(parent)
        self.setWindowTitle("Custom ListView Example")
        self.central_widget = FormWidget(self, model)
        self.setCentralWidget(self.central_widget)
        self.resize(300, 200)


class FormWidget(QWidget):
    def __init__(self, parent, model=None):
        super(FormWidget, self).__init__(parent)
        layout = QVBoxLayout()
        self.setLayout(layout)
        delegate = CustomDelegate()
        self.view = QListView()
        self.view.setModel(model)
        self.view.setItemDelegate(delegate)
        layout.addWidget(self.view)


class CustomListModel(QAbstractListModel):
    def __init__(self, parent=None, iterable=[]):
        super(CustomListModel, self).__init__(parent)
        self._data = iterable

    def rowCount(self, parent=QModelIndex()):
        return len(self._data)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid() or not 0 <= index.row() < self.rowCount():
            return QVariant()
        row = index.row()
        if role == Qt.DisplayRole:
            return str(self._data[row])
        return QVariant()

    def flags(self, index):
        flags_ = super(CustomListModel, self).flags(index)
        return flags_ | Qt.ItemIsEditable

    def setData(self, index, variant, role=Qt.EditRole):
        print("[MODEL] Setting data...")
        if role == Qt.EditRole:
            self._data[index.row()] = variant.value()
            print("[MODEL] Emitting signal for indexes %s, %s..."
                  % (index.row(), index.row()))
            self.dataChanged.emit(index, index)  # emetto il segnale
            return True
        return False


class CustomDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        item_data = index.data(Qt.DisplayRole)  # QVariant corrispondente
        opts = QStyleOptionProgressBar()
        opts.rect = option.rect
        opts.minimum = 0    # limite minimo progress bar
        opts.maximum = 100  # limite massimo progress bar
        opts.text = "{}/{} [{}%]".format(item_data, 100, int(item_data))
        opts.textAlignment = Qt.AlignCenter
        opts.textVisible = True
        opts.progress = int(item_data)
        QApplication.style().drawControl(QStyle.CE_ProgressBar, opts, painter)

    def createEditor(self, parent, option, index):
        print("[DELEGATE] Creating editor for index %s..." % index.row())
        editor = QSpinBox(parent)
        editor.setRange(0, 100)
        return editor

    def setEditorData(self, editor, index):
        print("[DELEGATE] Updating view for index %s..." % index.row())
        value = index.data(Qt.DisplayRole)
        editor.setValue(int(value))

    def setModelData(self, editor, model, index):
        print("[DELEGATE] Setting data to model at index %s..." % index.row())
        value = editor.value()
        variant = QVariant(value)
        model.setData(index, variant)


if __name__ == "__main__":
    app = QApplication([sys.argv])
    iterable = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
    model = CustomListModel(parent=None, iterable=iterable)
    main_window = MainWindow(parent=None, model=model)
    main_window.show()
    sys.exit(app.exec_())


[DELEGATE] Creating editor for index 4...
[DELEGATE] Updating view for index 4...
[DELEGATE] Setting data to model at index 4...
[MODEL] Setting data...
[MODEL] Emitting signal for indexes 4, 4...
[DELEGATE] Updating view for index 4...

Torna all’indice degli appunti

Categorie:PyQt5, python Tag: ,
I commenti sono chiusi.