Sort and filter a C++ model via QML functors?

后端 未结 2 1506
失恋的感觉
失恋的感觉 2021-01-20 23:47

I have a polymorphic (as in arbitrary roles) QObject model that is mostly instantiated declaratively from QML, as in this answer, and I would like to be able to hav

相关标签:
2条回答
  • 2021-01-21 00:34

    Update:

    Revisiting the issue, I finally came with a finalized solution, so I decided to drop in some updates. First, the relevant code:

    void set_filter(QJSValue f) {
      if (f != m_filter) {
        m_filter = f;
        filterChanged();
        invalidate();
      }
    }
    
    void set_sorter(QJSValue f) {
      if (f != m_sort) {
        m_sort = f;
        sorterChanged();
        sort(0, Qt::DescendingOrder);
      }
    }
    
    bool filterAcceptsRow(int sourceRow, const QModelIndex & sourceParent) const {
      if (!m_filter.isCallable()) return true;
      QJSValueList l;
      l.append(_engine->newQObject(sourceModel()->index(sourceRow, 0, sourceParent).data().value<QObject*>()));
      return m_filter.call(l).toBool();
    }
    
    bool lessThan(const QModelIndex & left, const QModelIndex & right) const {
      if (!m_sort.isCallable()) return false;
      QJSValueList l;
      l.append(_engine->newQObject(sourceModel()->data(left).value<QObject*>()));
      l.append(_engine->newQObject(sourceModel()->data(right).value<QObject*>()));
      return m_sort.call(l).toBool();
    }
    

    I found this solution to be simpler, safer and better performing than the QQmlScriptString & QQmlExpression duo, which does offer automatic updates on notifications, but as already elaborated in the comments below GrecKo's answer, was kinda flaky and not really worth it.

    The hack to get auto-updates for external context property changes is to simply reference them before returning the actual functor:

    filter: { expanded; SS.showHidden; o => expanded && (SS.showHidden ? true : !o.hidden) }
    

    Here is a simple expression using the new shorthand function syntax, it references expanded; SS.showHidden; in order to trigger reevaluations if those change, then implicitly returns the functor

    o => expanded && (SS.showHidden ? true : !o.hidden)

    which is analogous to:

    return function(o) { return expanded && (SS.showHidden ? true : !o.hidden) }

    which filters out objects based on whether the parent node is expanded, whether the child node is hidden and whether hidden objects are still displayed.

    This solution has no way to automatically respond to changes to o.hidden, as o is inserted into the functor upon evaluation and can't be referenced in the binding expression, but this can easily be implemented in the delegates of views that need to dynamically respond to such changes:

    Connections {
          target: obj
          onHiddenChanged: triggerExplicitEvaluation()
    }
    

    Remember that the use case involves a schema-less / single QObject* role model that facilitates a metamorphic data model where model item data is implemented via QML properties, so none of the role or regex stock filtering mechanisms are applicable here, but at the same time, this gives the genericity to use a single mechanism to implement sorting and filtering based on any criteria and arbitrary item data, and performance is very good, despite my initial concerns. It doesn't implement a sorting order, that is easily achievable by simply flipping the comparison expression result.

    0 讨论(0)
  • 2021-01-21 00:42

    As you mentionned, you could use QJSValue. But that's pretty static. What if you want to use a filter like filter: function(o) { return o.size > slider.value; } with a dynamic slider ? You'll have to manually call invalidateFilter().

    As a more practical alternative, you could instead use QQmlScriptString as a property & QQmlExpression to execute it. Using QQmlExpression allows you to be notified of context changes with setNotifyOnValueChanged.

    Your syntax would change to be like so : filter: o.size > slider.value.

    If you are looking for an out of the box solution, I've implemented this in a library of mine : SortFilterProxyModel on GitHub

    You can take a look at ExpressionFilter & ExpressionSorter, those do the same as what you initially wanted. You can check the complete source code in the repo.

    How to use it :

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

    But as @dtech mentionned, the overhead of going back and forth between qml and c++ for each row of the model is quite noticeable. That's why I created more specific filters and sorters. In your case, we would use RangeFilter and RoleSorter :

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

    Doing like this, we have a nice declarative API and the parameters are only passed once from qml to c++. All the filtering and sorting is then entirely done on the c++ side.

    0 讨论(0)
提交回复
热议问题