Pillole per sfuggire alla ripetitività di un filtro Qt

Lavorando su vari progetti Qt, mi sono spesso cimentato nella scrittura del codice necessario per poter filtrare gli elementi dei modelli con cui avevo a che fare. Per quanto questa operazione sia diventata con il tempo più immediata, l’ho sempre trovata tediosa e ripetitiva. Nella prima scrittura del modello, infatti, difficilmente si è a conoscenza di quali siano i dati da visualizzare e quali no e, purtroppo, questa informazione non è neanche nelle mani del cliente. La conseguenza è che mi sono trovato spesso a modificare il codice del filtro scritto in C++, a ricompilare e poi a provare quello che avevo prodotto nell’applicazione QML su cui lavoravo.

Per cercare di velocizzare questo processo di modifica mi sono detto: “Perché non scrivere un modello che sia personalizzabile direttamente da QML? Sarei più veloce nelle modifiche e magari anche il cliente potrebbe essere in grado di modificarlo”. Il mio obiettivo era quello di poter scrivere codice come questo:

PredicateFilter {
    sourceModel: DataModel {}

    predicate: AndPredicate {
        OrPredicate {
            EqualPredicate {
                property: "active"
                value: true
            }

            NotEqualPredicate {
                property: "type"
                value: "music"
            }
        }

        GreaterPredicate {
            property: "level"
            value: 5
        }
    }
}

che genera un modello in cui tutti gli elementi hanno le proprietà che rispettano: `(active == true or type != “music) and level > 5”.

In questo articolo voglio presentare una introduzione alla soluzione che ho implementato, nella speranza che possa essere d’aiuto a chiunque debba affrontare il mio stesso problema.

Analisi del modello

Prima di partire a scrivere codice bisogna sempre analizzare quello con cui si ha a che fare; la cosa può sembrare ovvia ma è sempre importante avere un’idea chiara di cosa dobbiamo affrontare, il rischio è di prendere una strada sbagliata e non arrivare a nulla.

Il modello con cui stavo lavorando rappresentava una lista di QObject, ognuno appartenente a una categoria di oggetti. Ogni oggetto aveva un identificativo della categoria e delle proprietà che erano specifiche della stessa. Il modello permetteva di accedere a un elemento della lista utilizzando la posizione che l’oggetto occupava nella lista stessa attraverso il metodo getObject(int i). Ultima informazione importante: il modello si occupava di notificare il cambio del numero di elementi ma non di indicare quali elementi erano stati aggiunti/eliminati.

Il modello, in estrema sintesi era definito in questo modo:


private:
    QList<QObject *> objects;

public:
    QObject *getObject(int i) const {
        return objects[i];
    }
}

Dopo aver capito come fosse fatto il modello sono passato alla scrittura del filtro vero e proprio.

PredicateFilter

Il filtro che volevo scrivere doveva rispettare queste regole:

Per rispettare le regole che mi ero dato (che figura ci avrei fatto sennò?) il filtro avrebbe dovuto delegare a un altro oggetto il compito di decidere se un dato del modello dovesse essere filtrato o meno. Il filtro sarebbe stato agnostico rispetto alla struttura dei dati e non avrebbe avuto codice specifico per il controllo delle proprietà degli oggetti del modello.

La classe base per questo tipo di filtri è il QSortFilterProxyModel (https://doc.qt.io/qt-5/qsortfilterproxymodel.html). Questa classe ordina e filtra gli elementi di un altro modello oltre a essere essa stessa un modello e quindi può essere usata dovunque serva un QAbstractItemModel.

Questo il risultato delle considerazioni:

class PredicateFilter: public QSortFilterProxyModel {

...
private: 
    Predicate *predicate;

public:
    bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const {

        QObject *value = qobject_cast<DataModel *>(sourceModel()).getObject(row);

        return (*predicate)(value);
    }
}

Con lo scheletro del modello pronto, il passo successivo era quello di andare a definire la classe che si fosse occupata di validare un oggetto di DataModel.

Predicate

La classe che si sarebbe dovuta occupare di validare un oggetto del modello doveva essere, nella sua forma base, quanto più semplice possibile: avrebbe dovuto permettere di validare un oggetto del modello (attraverso l’implementazione dell’operator()) e notificare, in caso, il cambio di una proprietà di un oggetto (attraverso il segnale predicateChanged()).

class Predicate : public QObject
{
	Q_OBJECT

public:
	explicit Predicate(QObject *parent = 0);
	virtual bool operator()(QObject *obj);

signals:
	void predicateChanged();
};

Definita l’interfaccia per tutti i Predicate era arrivato il momento di passare alla scrittura del vero codice.

ComparisonPredicate

A questo punto, era doverosa l’implementazione del primo tipo di Predicate, che avrebbe permesso di confrontare il valore di una proprietà di un oggetto di DataModel con un valore predefinito e di notificare il cambiamento del valore della proprietà.

Questa l’implementazione della classe base:

class ComparisonPredicate : public Predicate
{

	Q_OBJECT

	Q_PROPERTY(QVariant value READ getValue WRITE setValue NOTIFY valueChanged)
	Q_PROPERTY(QString property READ getProperty WRITE setProperty NOTIFY propChanged)

signals:
	void valueChanged(QVariant value);
	void propChanged(QString prop);

protected:
	virtual void createConnections(QObject *obj);
	virtual bool predicate(QVariant v) = 0;
};

bool ComparisonPredicate::operator()(QObject *obj) { 
    createConnections(o, prop);

    QVariant propertyValue = o->property(prop);
    return predicate(propertyValue);
}

void ComparisonPredicate::createConnections(QObject *obj, QString prop) {
    const QMetaObject* metaObject = obj->metaObject();
	for(int i = 0; i < metaObject->propertyCount(); ++i) {
		QMetaProperty metaObjectProp = metaObject->property(i);

		// Connect to relative `prop` changed signal
		if (QString::fromLatin1(metaObjectProp.name()) == prop &&          
            !metaObjectProp.notifySignal().methodSignature().isEmpty()) {

			// Prepend `2` to signal signature to create the same string created by SIGNAL
			connect(obj, "2" + metaObjectProp.notifySignal().methodSignature(), this, SIGNAL(predicateChanged()));
		}
	}
}

Il metodo createConnections() si occupa di creare le giuste connessioni “oggetto filtrato – istanza di ComparisonPredicate” per poter notificare il cambiamento del valore di una proprietà dell’oggetto filtrato in questo modo:

  1. Cercare tra le proprietà quella voluta
  2. Cercare il segnale collegato al cambio della proprietà
  3. Connettere il segnale trovato al passo 2. con il segnale predicateChanged()

Il metodo predicate(), invece, è il punto di personalizzazione delle classi derivate da ComparisonPredicate; è il punto dove avverrà il confronto tra il valore atteso della proprietà e il valore reale della stessa.

LogicalPredicate

Completata la scrittura del ComparisonPredicate, risultava subito chiara una cosa: l’impossibilità di collegare due o più istanze diComparisonPredicate in assenza di operatori logici.

Questo nuovo tipo di Predicate non avrebbe dovuto lavorare direttamente su QObject ma su altri Predicate, portando a un nuovo problema: qual è il modo per permettere di poter scrivere, lato QML, una lista di Predicate, allo stesso modo in cui è possibile posizionare un numero arbitrario di Rectangle in una Row? Il modo corretto è quello di utilizzare QQmlListProperty (https://doc.qt.io/qt-5/qqmllistproperty.html) che permette di esporre a QML una proprietà, data da una lista di oggetti derivati da QObject, andando a definire una serie di metodi per l’aggiunta e la rimozione di elementi dalla proprietà stessa.

La parte di codice specializzata per questo tipo di Predicate è solo:

bool LogicalPredicate::operator()(QObject *obj)
{
	// I have to iterate all over the predicates because predicates connections are created on
	// predicate execution.
	auto res = false;
	for (auto p: predicates)
	{
		// Check predicate
        ...
	}
	return res;
}

Conclusioni

Durante la scrittura di applicazioni Qt ci si trova molto spesso a dover implementare filtri sugli elementi di un modello, per poter visualizzare un set diverso a seconda della situazione di utilizzo, specialmente quando andiamo a lavorare in QML. Per quanto sia sempre possibile scrivere tutti i filtri che servono in C++, questa operazione risulta molto spesso noiosa e monotona. L’idea riportata in questo articolo vuole essere uno spunto per cominciare a scrivere il codice necessario per personalizzare facilmente, mediante l’uso di QML, i filtri che possono servire durante la scrittura di un’applicazione. Esiste un’implementazione del filtro molto simile a quella riportata in questo articolo, che può essere trovata qui. Questa implementazione richiede che il modello originale esponga i dati degli elementi attraverso il metodo data(), rendendone impossibile l’utilizzo nel progetto su cui lavoravo.