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

2019-02-25 18:10发布

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 have custom data "views" that sort and filter the model via arbitrary, and potentially - runtime generated from code strings JS functors, something like that:

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

The QSortFilterProxyModel interface doesn't seem to be particularly well suited to the task, instead being fixated on static roles and pre-compiled rules.

I tried using QJSValue properties on the C++ side, but it seems like it is not possible, the C++ code just doesn't compile with that property type. And if I set the property type to QVariant I get error messages from QML that functions can only be bound to var properties. Evidently, var to QVariant conversion doesn't kick in here as it does for return values.

2条回答
不美不萌又怎样
2楼-- · 2019-02-25 18:54

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.

查看更多
放我归山
3楼-- · 2019-02-25 18:55

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.

查看更多
登录 后发表回答