Trier et filtrer un modèle C ++ via les foncteurs QML?

J’ai un modèle QObject polymorphe (comme dans des rôles arbitraires) qui est principalement instancié de manière déclarative à partir de QML, comme dans cette réponse , et j’aimerais pouvoir disposer de “vues” de données personnalisées qui sortinger et filtrer le modèle de manière arbitraire et potentiellement – runtime généré à partir de chaînes de code JS foncteurs, quelque chose comme ça:

  DataView { sourceModel: model filter: function(o) { return o.size > 3 } sort: function(a, b) { return a.size > b.size } } 

L’interface QSortFilterProxyModel ne semble pas particulièrement adaptée à la tâche, mais plutôt fixée sur des rôles statiques et des règles précompilées.

J’ai essayé d’utiliser les propriétés QJSValue du côté C ++, mais il semble que ce ne soit pas possible, le code C ++ ne se comstack pas avec ce type de propriété. Et si je règle le type de propriété sur QVariant QML m’indique que les fonctions ne peuvent être liées qu’à des propriétés var . De toute évidence, la conversion var en QVariant n’intervient pas ici comme pour les valeurs de retour.

Comme vous l’avez mentionné, vous pouvez utiliser QJSValue. Mais c’est assez statique. Que faire si vous souhaitez utiliser un filtre tel que filter: function(o) { return o.size > slider.value; } filter: function(o) { return o.size > slider.value; } avec un curseur dynamic? Vous devrez appeler manuellement invalidateFilter() .

Comme alternative plus pratique, vous pouvez utiliser QQmlScriptSsortingng tant que propriété & QQmlExpression pour l’exécuter. L’utilisation de QQmlExpression vous permet d’être averti des modifications de contexte avec setNotifyOnValueChanged .

Votre syntaxe changerait pour ressembler à ceci: filter: o.size > slider.value .

Si vous cherchez une solution prête à l’emploi , je l’ai implémentée dans une bibliothèque de moi: SortFilterProxyModel sur GitHub.

Vous pouvez regarder dans ExpressionFilter & ExpressionSorter , ceux-ci font la même chose que ce que vous vouliez au départ. Vous pouvez vérifier le code source complet dans le référentiel.

Comment l’utiliser :

 import SortFilterProxyModel 0.2 // ... SortFilterProxyModel { sourceModel: model filters: ExpressionFilter { expression: model.size > 3 } sorters: ExpressionSorter { expression: modelLeft.size < modelRight.size } } 

Mais, comme @dtech l'a mentionné, les frais généraux liés aux allers-retours entre qml et c ++ pour chaque ligne du modèle sont assez évidents. C'est pourquoi j'ai créé des filtres et des sortingeurs plus spécifiques. Dans votre cas, nous utiliserions RangeFilter et RoleSorter :

 import SortFilterProxyModel 0.2 // ... SortFilterProxyModel { sourceModel: model filters: RangeFilter { roleName: "size" minimumValue > 3 minimumInclusive: true } sorters: RoleSorter { roleName: "size" } } 

En procédant ainsi, nous avons une belle API déclarative et les parameters ne sont passés qu'une fois de qml à c ++. L'ensemble du filtrage et du sorting est alors entièrement effectué côté c ++.

J’ai pu contourner l’incapacité d’avoir des propriétés QJSValue en utilisant les installations de dynamisme de Qt.

Dans cette approche, les propriétés du foncteur sont implémentées dans QML en tant que proxy / “sous-classe” du type C ++. Ensuite, au lieu d’utiliser directement le type C ++ exposé, celui qui est étendu dans QML est utilisé:

 // DataView.qml import QtQuick 2.9 import Core 1.0 DataView { // so use _this_ DataView instead the one from Core property var filter: null property var sort: null } 

Du côté C ++, les fonctions QSortFilterProxyModel appropriées doivent être implémentées comme suit:

 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override { QJSValue func = property("filter").value(); if (!func.isCallable()) return true; QJSValueList l; l.append(_engine->newQObject(sourceModel()->index(sourceRow, 0, sourceParent).data(Qt::UserRole + 1).value())); return func.call(l).toBool(); } bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { QJSValue func = property("sort").value(); if (!func.isCallable()) return false; QJSValueList l; l.append(_engine->newQObject(sourceModel()->data(left).value())); l.append(_engine->newQObject(sourceModel()->data(right).value())); return func.call(l).toBool(); } 

De cette façon, les conversions appropriées ont lieu. Il suffit donc de configurer les parameters et d’intégrer l’invocation de foncteurs du côté C ++.

Et cela fonctionne comme prévu, mis à part les performances non stellaires (toujours acceptables), probablement en raison des frais généraux liés aux allers-retours entre les couches C ++ et QML. Il y a beaucoup de va-et-vient ici – d’abord la recherche dynamic de la propriété, la conversion implicite par le moteur, puis l’acquisition des objects de données via l’interface de modèle, qui acquiert elle-même celle de la couche QML, juste pour la reconvertir dans les valeurs JS, à passer en tant que parameters pour que les foncteurs puissent être invoqués, la valeur de retour convertie et remise à C ++ pour la conversion finale …

Notez l’utilisation de engine->newQObject() qui est le seul moyen que j’ai trouvé pour obtenir une QJSValue d’un QObject , ce qui est un peu suspect car, dans mon cas, les objects sont déjà enregistrés avec le moteur, car c’est là qu’ils sont définis. et instancié. Le temps nous le dira. En attendant, de meilleures solutions sont les bienvenues.

Mettre à jour:

Il est donc possible d’utiliser QJSValue comme un type de propriété après tout. Le problème était qu’il n’avait pas implémenté l’opérateur != Requis par les internes de Qt. Cela ressemble à une omission, car la classe fournit déjà une fonction equal() , implémenter l’opérateur aurait donc été sortingvial. Et c’est toujours, bien que comme fonction libre:

 bool operator !=(QJSValue const & a, QJSValue const & b) { return !a.equals(b); } 

Et maintenant, il comstack, ce qui permet d’alléger au moins une partie des frais généraux. Notez que le membre de la propriété doit être mutable , car les fonctions de l’API sont const .

Mise à jour 2:

La partie fishy engine->newQObject() peut également être omise, en convertissant directement le QVariant renvoyé par l’interface du modèle en une valeur de script, comme QVariant :

 l.append(_engine->toScriptValue(sourceModel()->index(sourceRow, 0, sourceParent).data(Qt::UserRole + 1))); 

Mise à jour 3:

Bien que les résultats de la mise à jour 2 permettent de transmettre les objects à QML, cette approche n’autorise pas l’access à leurs propriétés. Elles sont toutes undefined . Donc, engine->newQObject() à engine->newQObject() pour le moment.