[QML] Utilizzare delegati multipli su una ListView con DelegateChooser

La possibilità di utilizzare delegati differenti per presentare dati provenienti da un model in Qt è stato un argomento che mi si è posto davanti numerose volte negli ultimi anni. In QWidget i delegati hanno una struttura complessa e spesso lo sforzo nell’implementarli non vale la pena rispetto a un qualsiasi workaround.

In QML la scrittura di delegati custom è estremamente semplice, ma fino a Qt 5.12 non esisteva un componente apposito per gestire la scelta e doveva essere implementato ogni volta. Una soluzione tipica era quella di utilizzare un Loader con uno switch indicando il delegato più appropriato:

ListView {
    model: contactModel
    delegate: Loader {
        sourceComponent: {
            switch(type) {
            case "name":
                return nameDelegate;
            case "age":
                return ageDelegate;
            }
        }
    }
    NameDelegate { 
      id: nameDelegate
    }
    AgeDelegate { 
        id: ageDelegate
    }
}

Da Qt 5.12 è stato introdotto all’interno del package Qt.labs.qmlmodels il componente DelegateChooser.

DelegateChooser ha esattamente la funzione di uno switch che agisce su un role specifico del model. Il componente DelegateChoice ha la funzione di switch-case.

Vediamo un caso d’uso. Cominciamo definendo una ListView e un model.

    ListView {
        anchors.fill: parent
        delegate: delegateChooser
        model: ListModel {
            ListElement { name: "Status"; type: "string"; label: "OK" }
            ListElement { name: "Synchronize"; type: "bool"; check: false }
            ListElement { name: "Tolerance"; type: "int"; value: 7; min: 0; max: 10 }
            ListElement { name: "Quality"; type: "quality"; quality: 3 }
            ListElement { name: "Safe Mode"; type: "bool"; check: true }
        }
    }

Come delegate andremo a dichiarare il DelegateChooser.

    DelegateChooser {
        id: delegateChooser
        role: "type"

        DelegateChoice {
            roleValue: "bool"
            BoolDelegate {
                // explicitely calling model, not actually necessary
                entryName: model.name
                value: model.check
            }
        }
    }

Le uniche proprietà del componente sono role e choices. La proprietà role sta ad indicare il roleName su cui verrà discriminata la scelta del delegato. La proprietà choices verrà popolata automaticamente con ogni componente DelegateChoice definito. Ce ne sarà tra per ogni caso che vogliamo gestire, specificando tramite roleValue il caso specifico.

È anche possibile possibile innestare un DelegateChooser all’interno di un DelegateChoice specificandola nella proprietà delegate.

Implementiamo l’intera lista di DelegateChoice per il nostro model

DelegateChooser {
        id: delegateChooser
        role: "type"

        DelegateChoice {
            roleValue: "bool"
            BoolDelegate {
                entryName: model.name
                value: model.check
            }
        }
        
        DelegateChoice {
            roleValue: "string"
            StringDelegate {
                entryName: name
                value: label
            }
        }

        DelegateChoice {
            roleValue: "quality"
            QualityDelegate {
                entryName: name
                value: quality
            }
        }
    }

Semplice, no? questo è il risultato che ottieniamo

Da notare come il BoolDelegate faccia riferimento esplicito al model tramite model.name e model.check. Questo non è necessario in quanto model è una proprietà implicita ereditata dall’oggetto padre. Personalmente preferisco specificarlo ed essere più verboso in favore di maggiore chiarezza del codice. BoolDelegate, StringDelegate, IntDelegate ed EntryDelegate sono dei delegati custom che permettono di ottenere l’effetto mostrato sopra.

Ma, un momento. Cosa succede se il nostro model ha un elemento che non è elencato nel DelegateChooser? Vediamolo subito, modifichiamo il ListModel di conseguenza:

    ListView {
        anchors.fill: parent
        delegate: delegateChooser
        model: ListModel {
            ListElement { name: "Status"; type: "string"; label: "OK" }
            ListElement { name: "Synchronize"; type: "bool"; check: false }
            ListElement { name: "Tolerance"; type: "int"; value: 7; min: 0; max: 10 }
            ListElement { name: "Quality"; type: "quality"; quality: 3 }
            ListElement { name: "Destroy the World"; type: "double"; trigger: 1.5 }
            ListElement { name: "Safe Mode"; type: "bool"; check: true }
        }
    }

Il risultato

Oops! Si è perso qualche pezzo di troppo.

L’elemento non presente non viene disegnato come prevedibile. Quello che accade però è che tutti gli elementi successivi non vengono renderizzati a loro volta. È necessario quindi prevedere un caso di default come per un normale switch a fine sequenza.

        DelegateChoice {
            StringDelegate {
                entryName: name
                value: "no delegate provided"
                rectColor: Material.color(Material.Red)
            }
        }

Riproviamo adesso

Molto meglio adesso.

Aggiungiamo un altro caso non gestito per avere un’altra conferma del risultato.

    ListView {
        anchors.fill: parent
        delegate: delegateChooser
        model: ListModel {
            ListElement { name: "Status"; type: "string"; label: "OK" }
            ListElement { name: "Synchronize"; type: "bool"; check: false }
            ListElement { name: "Tolerance"; type: "int"; value: 7; min: 0; max: 10 }
            ListElement { name: "Quality"; type: "quality"; quality: 3 }
            ListElement { name: "Safe Mode"; type: "bool"; check: true }
            ListElement { name: "Destroy the World"; type: "double"; trigger: 1.5 }
            ListElement { name: "Answer to Live, Universe and everything"; type: "towel"; answer: 42 }
        }
    }
Risultato raggiunto.

Obiettivo raggiunto. Ricordarsi sempre che il componente di default va’ sempre specificato per ultimo per non incorrere nello stesso errore di sopra.

Trovare a questo link la demo completa con il codice utilizzato per l’esempio.

Riferimenti:

  • Questo post di StackOverflow racchiude un po’ la storia delle varie soluzioni utilizzate nel tempo.
  • Questo interessante blog post di Orthogonal che mostra l’utilizzo di DelegateChooser, da cui ho preso spunto per i delegati.

Lascia un commento