Skip to content

Instantly share code, notes, and snippets.

@mottosso
Last active August 29, 2015 14:19
Show Gist options
  • Save mottosso/62afdbf65521fdb9d9ee to your computer and use it in GitHub Desktop.
Save mottosso/62afdbf65521fdb9d9ee to your computer and use it in GitHub Desktop.
QObject list-model with declarative syntax

QObject list-model with declarative syntax

Declaratively create items for use in models exported to QML.

See also




The Problem

Traditionally, data is exposed to QML through the use of roles. Each role represents a number and associated name, the name is used from QML and it's equivalent role is used for lookup in the QAbstract*Model.

app.qml

Text {
  text: name  // String-equivalent of role
}

app.py

class Model(QtCore.QAbstractListModel):
  def __init__(self, parent=None):
    super(Model, self).__init__(self, parent)
    self.items = [{"name": "Marcus"}, {"name": "Lukas"}]

  def data(self, index, role):
    key = self.roleNames()[role]
    return self.items[index.row()][key]

  def roleNames(self):
    return {QtCore.Qt.UserRole: "name"}

The problem:

  • Roles are implementation detail
  • Keys are duplicated both in roleNames and each item declaration

The model could potentially look like this, without interfering with it's interface towards QML.

class Model(QtCore.QAbstractListModel):
  def __init__(self, parent=None):
    super(Model, self).__init__(self, parent)
    self.items = [{"name": "Marcus"}, {"name": "Lukas"}]

  def data(self, index, role):
    return self.items[index.row()][role]

Another disadvantage (debatable) of exposing data via roles is the lack of namespaces in their usage.

app.qml

Text {
  text: name  // No namespace
}

No namespace adds cognintive load in that it is easily mistaken for scope variables.




A Solution

In this implementation, only a single role is exposed to QML - item. Each data member is then accessible via item, making item effectively into a namespace whilst allowing a single implementation of a model to be reused with an arbitrary amount of implementations for items.

app.py

class CustomItem(AbstractItem):
  name = str
  age = int
  
item = CustomItem(name="Marcus", age=10)

app.qml

Text {
  text: item.name
}

Cons

  • QML Bindings are made to each item as opposed to the model, which may decrease performance in large models.
  • Model items may be destroyed, causing errors such as RuntimeError: wrapped C/C++ object of type Item has been deleted

    Make sure to explicitly assign the model as parent to your QObjects such that they get properly garbage collected.

  • dataChanged not emitted when data changes. Other objects such as QSortFilterProxyModel relies on this signal to invalidate itself.

    A workaround is to emit this signal explicitly in your items.

See also

"""Demonstrate the use of QObjects as model items
Use QObjects and a single role - "item" - with dot-notation access via QML.
In order to aid in the creation of items, a metaclass is used to
automatically convert class attributes into pyqtProperty attributes.
"""
import sys
from PyQt5 import QtCore, QtGui, QtQuick
class PropertyType(QtCore.pyqtWrapperType):
"""Metaclass for converting class attributes into pyqtProperties
Usage:
>>> class AbstractClass(QtCore.QObject):
... __metaclass__ = PropertyType
"""
def __new__(cls, name, bases, attrs):
for key, type in attrs.copy().items():
if key.startswith("__"):
continue
attrs[key] = QtCore.pyqtProperty(
type, fget=lambda self, n=key: getattr(self, "_" + n, None),
constant=True)
return super(PropertyType, cls).__new__(cls, name, bases, attrs)
class AbstractItem(QtCore.QObject):
"""Baseclass for QAbstractListModel items
Any class attributes are converted into pyqtProperties
and must be declared with its type as value.
Usage:
>>> class Item(AbstractItem):
... name = str
... age = int
... alive = bool
...
>>> item = Item(name="Marcus", age=5, alive=True)
>>> assert item.name == "Marcus"
>>> assert item.age == 5
>>> assert item.alive == True
"""
__metaclass__ = PropertyType
def __init__(self, **kwargs):
super(AbstractItem, self).__init__()
for key, value in kwargs.items():
setattr(self, "_" + key, value)
class MyItem(AbstractItem):
"""Demonstrates the use of AbstractItem"""
name = str
color = str
family = str
class Model(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super(Model, self).__init__(parent)
self.items = []
def add_item(self, item):
self.beginInsertRows(QtCore.QModelIndex(),
self.rowCount(),
self.rowCount())
self.items.append(item)
self.endInsertRows()
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def data(self, index, role=QtCore.Qt.DisplayRole):
try:
return self.items[index.row()]
except IndexError:
return QtCore.QVariant()
def roleNames(self):
return {
QtCore.Qt.UserRole: "item"
}
if __name__ == '__main__':
# Example
class Application(QtGui.QGuiApplication):
def __init__(self):
super(Application, self).__init__(sys.argv)
window = QtQuick.QQuickView()
window.setResizeMode(window.SizeRootObjectToView)
window.setWidth(400)
window.setHeight(600)
engine = window.engine()
engine.addImportPath(".")
model = Model()
# Add a number of items
model.add_item(MyItem(name="Linus", color="yellow"))
model.add_item(MyItem(name="Marcus", color="brown"))
model.add_item(MyItem(name="Richard", color="red"))
context = engine.rootContext()
context.setContextProperty("objModel", model)
window.setSource(QtCore.QUrl.fromLocalFile("test.qml"))
window.show()
self.window = window
self.model = model
app = Application()
app.exec_()
import QtQuick 2.3
Rectangle {
color: "steelblue"
anchors.fill: parent
ListView {
anchors.fill: parent
model: objModel
delegate: Text {
text: item.name
color: item.color
font.pixelSize: 50
Component.onCompleted: {
print(item.color)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment