Usage

Model

The easymodel module provides an simple but powerful general purpose treemodel. The model uses easymodel.TreeItem instances that act like rows.

The tree item itself uses a easymodel.ItemData instance to provide the data. That way, the structure and logic of the whole model is encapsulated in the model and tree item classes. They take care of indexing, insertion, removal etc.

As a user you only need to subclass easymodel.ItemData to wrap arbitrary objects. It is pretty easy.

There is also a rudimentary easymodel.ListItemData that might be enough for simple data.

Root

Each model needs a root item. The root item is the parent of all top level tree items that hold data. It is also responsible for the headers. So in most cases it is enough to simply use a easymodel.ListItemData:

import easymodel

headers = ['Name', 'Speed', 'Altitude']
rootdata = easymodel.ListItemData(headers)
# roots do not have a parent
rootitem = easymodel.TreeItem(rootdata, parent=None)

Creating a simple model

There are three steps involved. Create a root item, a model and wrap the data. The creation of a model is simple:

# use the root item from above
model = easymodel.TreeModel(rootitem)

Thats it. The root item might already have children. That way, you can initialize a model with data.

Add Items

Let’s assume our data consists of items with 3 values: Name, Velocity, Altitude. The data might describe a vehicle, like an airplane or something like that. Before we create our own easymodel.ItemData subclasses, we use simple lists, so we can use easymodel.ListItemData. First create the data:

data1 = easymodel.ListItemData(['Cessna', 250, 2000])
data2 = easymodel.ListItemData(['747', 750, 6000])
data3 = easymodel.ListItemData(['Fuel Plane', '730', 5000])

Wrap the data in items:

# specify the parent to add it directly to the model
item1 = easymodel.TreeItem(data1, parent=rootitem)
# or add it later
item2 = easymodel.TreeItem(data2)
item2.set_parent(rootitem)
# use the builtin to_item method
item3 = data3.to_item(item2)

The tree items will automatically update the model. No need to emit any signals or call further methods.

Remove Items

Let’s say the fuel plane finished its job and landed. You can remove it from the model simply by setting the parent to None:

item3.set_parent(None)

You could have also used the model’s methods to remove it but this way is much easier.

Wrap arbitrary objects

To wrap arbitrary objects in an item data instance, you need to subclass it. Let’s assume we have a very simple airplane class:

class Airplane(object):
    """This is the data we want to display in a view. An airplane.

    It has a name, a velocity and altitude.
    """
    def __init__(self, name, speed, altitude):
        self.name = name
        self.speed = speed
        self.altitude = altitude

Let’s create a item data subclass that has three columns: Name, Speed, Altitude. Speed and Altitude should be editable.

First subclass easymodel.ItemData. It can store an airplane instance.:

class AirplaneItemData(easymodel.ItemData):
    """An item data object that can extract information from an airplane instance.
    """
    def __init__(self, airplane):
        self.airplane = airplane

The column count is 3 and we can also give access to the airplane that is stored:

def column_count(self,):
    """Return 3. For name, velocity and altitude."""
    return 3

def internal_data(self):
    """Return the airplane instance"""
    return self.airplane

By default an item is enabled and selectable. But speed and altitude should be editable. So lets override easymodel.ItemData.flags():

def flags(self, column):
    """Return flags for enabled and selectable. Speed and altitude are also editable."""
    default = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
    if column == 0:
        return default
    else:
        return default | QtCore.Qt.ItemIsEditable

Now we need pass the data to the model. This is pretty simple. Just pass the right attribute for each column:

def data(self, column, role):
    """Return the data of the airplane"""
    if role == QtCore.Qt.DisplayRole:
        return (self.airplane.name, self.airplane.speed, self.airplane.altitude)[column]

Setting the data is not that complicated. Just set the right attribute for each column:

def set_data(self, column, value, role):
    """Set the data of the airplane"""
    if role == QtCore.Qt.EditRole or role == QtCore.Qt.DisplayRole:
        attr = ('speed', 'altitude')[column-1]
        setattr(self.airplane, attr, value)
        return True
    return False

Now we can use this class to wrap our own airplanes and add them to a treeitem/model:

# create a plane
plane = Airplane('Nimbus 4', 0, 0)
# wrap it in a data object
planedata = AirplaneItemData(plane)
# add it to the model
planeitem = easymodel.TreeItem(planedata, rootitem)

Delegate

Sometimes you want to have arbitrary widgets in your views. ItemDelegates of Qt are cool, but it is very hard to get your arbitrary widget into the view.

If the widget changes a lot or you want to use the UI Designer, the regular workflow of styled item delegates is a bit flawed. The easymodel.Widgetdelegate is there to help.

Let’s assume you want have a spin box and a randomize button for the altitude of your planes in a view. The widget might look like this:

class RandomSpinBox(QtGui.QWidget):
    """SpinBox plus randomize button
    """

    def __init__(self, parent=None, flags=0):
        super(RandomSpinBox, self).__init__(parent, flags)
        self.main_hbox = QtGui.QHBoxLayout(self)
        self.value_sb = QtGui.QSpinBox(self)
        self.random_pb = QtGui.QPushButton("Randomize")
        self.main_hbox.addWidget(self.value_sb)
        self.main_hbox.addWidget(self.random_pb)

        self.random_pb.clicked.connect(self.randomize)

    def randomize(self, *args, **kwargs):
        v = random.randint(0, 99)
        self.value_sb.setValue(v)

To create a delegate for this widget subclass easymodel.Widgetdelegate:

class RandomSpinBoxDelegate(easymodel.WidgetDelegate):
    """RandomSpinBox delegate"""

    def __init__(self, parent=None):
        super(RandomSpinBoxDelegate, self).__init__(parent)

Implement the abstract methods. First reimplement easymodel.Widgetdelegate.create_widget(). It is used to create the widget that will be rendered in the view:

def create_widget(self, parent=None):
    return RandomSpinBox(parent)

If your editor should look exactly the same you can reuse this function:

def create_editor_widget(self, parent, option, index):
    return self.create_widget(parent)

Now you need to implement easymodel.Widgetdelegate.setEditorData(). It will set the editor in the right state to represent a index in the model. So we take the data of the index and put it in the spinbox:

def setEditorData(self, widget, index):
    d = index.data(QtCore.Qt.DisplayRole)
    if d:
        widget.value_sb.setValue(int(d))
    else:
        widget.value_sb.setValue(int(0))

easymodel.Widgetdelegate.set_widget_index() does the same for the widget that is rendered. Every time an index is painted, the widget has to be set in the right state to represent the index. Because we already did that for the editor we can reuse the function:

def set_widget_index(self, index):
    self.setEditorData(self.widget, index)

Now all that is left is easymodel.Widgetdelegate.setModelData(). Here you take the value from the editor and set the data in the model:

def setModelData(self, editor, model, index):
    v = editor.value_sb.value()
    model.setData(index, v, QtCore.Qt.EditRole)

Done! Now you can use the delegate in any view. But I recommend using one of the views in easymodel.widgetdelegate.

You can either use the easymodel.WidgetDelegateViewMixin for your own views or use one of the premade views: easymodel.WD_AbstractItemView, easymodel.WD_ListView, easymodel.WD_TableView, easymodel.WD_TreeView.

They will make the user experience better. When the user clicks an widget delegate, it will be set into edit mode and the click will be propagated to the editor. That way it behaves almost like the widget delegate were a regular widget.

Little example app

Let’s create a simple widget with a view and controls to add new items into the view. We reuse the code from above.

The window has a view, an add button and 3 edits for name, speed and altitude. When the add button is clicked, a new airplane should be inserted into the model. The parent should be the currently selected index.

First create the widget:

class AirplaneAppWidget(QtGui.QWidget):
    def __init__(self, parent=None, flags=0):
        super(AirplaneAppWidget, self).__init__(parent, flags)
        self.main_vbox = QtGui.QVBoxLayout(self)
        self.add_hbox = QtGui.QHBoxLayout()

        self.instruction_lb = QtGui.QLabel("Select Item and click add!", self)
        self.view = easymodel.WD_TreeView(self)

        self.add_pb = QtGui.QPushButton('Add')
        self.add_pb.clicked.connect(self.add_airplane)

        self.name_lb = QtGui.QLabel('Name')
        self.name_le = QtGui.QLineEdit()
        self.speed_lb = QtGui.QLabel('Speed')
        self.speed_sb = QtGui.QSpinBox()
        self.altitude_lb = QtGui.QLabel('Altitude')
        self.altitude_sb = QtGui.QSpinBox()

        self.main_vbox.addWidget(self.instruction_lb)
        self.main_vbox.addWidget(self.view)
        self.main_vbox.addLayout(self.add_hbox)
        self.add_hbox.addWidget(self.add_pb)
        self.add_hbox.addWidget(self.name_lb)
        self.add_hbox.addWidget(self.name_le)
        self.add_hbox.addWidget(self.speed_lb)
        self.add_hbox.addWidget(self.speed_sb)
        self.add_hbox.addWidget(self.altitude_lb)
        self.add_hbox.addWidget(self.altitude_sb)

        self.delegate1 = RandomSpinBoxDelegate()
        self.view.setItemDelegateForColumn(2, self.delegate1)

        # Now we can build ourselves models
        # First we need a root
        rootdata = easymodel.ListItemData(['Name', 'Velocity', 'Altitude'])
        root = easymodel.TreeItem(rootdata)
        # Create a new model with the root
        model = easymodel.TreeModel(root)

        self.view.setModel(model)

Now for the button callback. All we need to do is create an airplane, wrap it in a data/item and parent it under the current index:

def add_airplane(self, *args, **kwargs):
    # get parent item
    currentindex = self.view.currentIndex()
    if currentindex.isValid():
        # items are stored in the internal pointer
        # but if you use a proxy model this might not work
        # user the TREEITEM_ROLE instead
        pitem = currentindex.data(easymodel.TREEITEM_ROLE)
    else:
        # nothing selected. Take root as parent
        pitem = self.view.model().root

    # create a new airplane
    name = self.name_le.text()
    speed = self.speed_sb.value()
    altitude = self.altitude_sb.value()
    airplane = Airplane(name, speed, altitude)
    # wrap it in an item data instance
    adata = AirplaneItemData(airplane)
    # create a tree item.
    # because parent is given, the item will
    # automatically be inserted in the model
    easymodel.TreeItem(adata, parent=pitem)

The rest of the app code can look like this:

app = QtGui.QApplication([], QtGui.QApplication.GuiClient)
app.setStyle(QtGui.QStyleFactory.create("plastique"))
apw = AirplaneAppWidget()
apw.show()
app.exec_()

Complete Code

Everything put together:

import random

from PySide import QtCore, QtGui

import easymodel


class Airplane(object):
    """This is the data we want to display in a view. An airplane.

    It has a name, a velocity and altitude.
    """
    def __init__(self, name, speed, altitude):
        self.name = name
        self.speed = speed
        self.altitude = altitude


class AirplaneItemData(easymodel.ItemData):
    """An item data object that can extract information from an airplane instance.
    """
    def __init__(self, airplane):
        self.airplane = airplane

    def data(self, column, role):
        """Return the data of the airplane"""
        if role == QtCore.Qt.DisplayRole:
            return (self.airplane.name, self.airplane.speed, self.airplane.altitude)[column]

    def set_data(self, column, value, role):
        """Set the data of the airplane"""
        if role == QtCore.Qt.EditRole or role == QtCore.Qt.DisplayRole:
            attr = ('name', 'speed', 'altitude')[column]
            setattr(self.airplane, attr, value)
            return True
        return False

    def column_count(self,):
        """Return 3. For name, velocity and altitude."""
        return 3

    def internal_data(self):
        """Return the airplane instance"""
        return self.airplane

    def flags(self, column):
        """Return flags for enabled and selectable. Speed and altitude are also editable."""
        default = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
        if column == 0:
            return default
        else:
            return default | QtCore.Qt.ItemIsEditable


class RandomSpinBox(QtGui.QWidget):
    """SpinBox plus randomize button
    """

    def __init__(self, parent=None, flags=0):
        super(RandomSpinBox, self).__init__(parent, flags)
        self.main_hbox = QtGui.QHBoxLayout(self)
        self.value_sb = QtGui.QSpinBox(self)
        self.random_pb = QtGui.QPushButton("Randomize")
        self.main_hbox.addWidget(self.value_sb)
        self.main_hbox.addWidget(self.random_pb)

        self.random_pb.clicked.connect(self.randomize)

    def randomize(self, *args, **kwargs):
        v = random.randint(0, 99)
        self.value_sb.setValue(v)


class RandomSpinBoxDelegate(easymodel.WidgetDelegate):
    """RandomSpinBox delegate
    """

    def __init__(self, parent=None):
        super(RandomSpinBoxDelegate, self).__init__(parent)

    def create_widget(self, parent=None):
        return RandomSpinBox(parent)

    def create_editor_widget(self, parent, option, index):
        return self.create_widget(parent)

    def setEditorData(self, widget, index):
        d = index.data(QtCore.Qt.DisplayRole)
        if d:
            widget.value_sb.setValue(int(d))
        else:
            widget.value_sb.setValue(int(0))

    def set_widget_index(self, index):
        self.setEditorData(self.widget, index)

    def setModelData(self, editor, model, index):
        v = editor.value_sb.value()
        model.setData(index, v, QtCore.Qt.EditRole)


class AirplaneAppWidget(QtGui.QWidget):
    def __init__(self, parent=None, flags=0):
        super(AirplaneAppWidget, self).__init__(parent, flags)
        self.main_vbox = QtGui.QVBoxLayout(self)
        self.add_hbox = QtGui.QHBoxLayout()

        self.instruction_lb = QtGui.QLabel("Select Item and click add!", self)
        self.view = easymodel.WD_TreeView(self)

        self.add_pb = QtGui.QPushButton('Add')
        self.add_pb.clicked.connect(self.add_airplane)

        self.name_lb = QtGui.QLabel('Name')
        self.name_le = QtGui.QLineEdit()
        self.speed_lb = QtGui.QLabel('Speed')
        self.speed_sb = QtGui.QSpinBox()
        self.altitude_lb = QtGui.QLabel('Altitude')
        self.altitude_sb = QtGui.QSpinBox()

        self.main_vbox.addWidget(self.instruction_lb)
        self.main_vbox.addWidget(self.view)
        self.main_vbox.addLayout(self.add_hbox)
        self.add_hbox.addWidget(self.add_pb)
        self.add_hbox.addWidget(self.name_lb)
        self.add_hbox.addWidget(self.name_le)
        self.add_hbox.addWidget(self.speed_lb)
        self.add_hbox.addWidget(self.speed_sb)
        self.add_hbox.addWidget(self.altitude_lb)
        self.add_hbox.addWidget(self.altitude_sb)

        self.delegate1 = RandomSpinBoxDelegate()
        self.view.setItemDelegateForColumn(2, self.delegate1)

        # Now we can build ourselves models
        # First we need a root
        rootdata = easymodel.ListItemData(['Name', 'Velocity', 'Altitude'])
        root = easymodel.TreeItem(rootdata)

        # Create a new model with the root
        self.model = easymodel.TreeModel(root)
        self.view.setModel(self.model)

    def add_airplane(self, *args, **kwargs):
        # get parent item
        currentindex = self.view.currentIndex()
        if currentindex.isValid():
            # items are stored in the internal pointer
            # but if you use a proxy model this might not work
            # user the TREEITEM_ROLE instead
            pitem = currentindex.data(easymodel.TREEITEM_ROLE)
        else:
            # nothing selected. Take root as parent
            pitem = self.view.model().root

        # create a new airplane
        name = self.name_le.text()
        speed = self.speed_sb.value()
        altitude = self.altitude_sb.value()
        airplane = Airplane(name, speed, altitude)
        # wrap it in an item data instance
        adata = AirplaneItemData(airplane)
        # create a tree item.
        # because parent is given, the item will
        # automatically be inserted in the model
        easymodel.TreeItem(adata, parent=pitem)

if __name__ == "__main__":
    # Create a view to show what is happening
    app = QtGui.QApplication([], QtGui.QApplication.GuiClient)
    app.setStyle(QtGui.QStyleFactory.create("plastique"))
    apw = AirplaneAppWidget()
    apw.show()
    app.exec_()