Home > database >  PySide2 QListView.setRootIndex with customModel not working as expected
PySide2 QListView.setRootIndex with customModel not working as expected

Time:12-09

I am pretty new to QT and I am using PySide2 (latest version) with Python 3.9.6.

I want to use a CustomModel via QAbstractItemModel on a QtreeView and at the same time with a QListView. I have a CustomModel with a two-level hierarchy data. I want to see the full data in the treeview (working). At the beginning I show the same model in the QListView. It shows only the top level items. So far so good. Now I connected the setRootIndex fn from the QListView to the clicked signal of the QTreeView.

I want to be able to click on a root level item and see only the children in the QListView. I thought the .setRootIndex should do the trick, but its weirdly offsetting the shown children. And it's showing only ONE of the children and offsetted by the index count of the first level item.

Please see the gif: Example Code in action

  1. First both views show the same model.
  2. Then I click the first root element in the left treeView.
  3. It updates the right ListView, but only the first children is shown.
  4. And the second item shows its child but the second and with one gap in the listView

Here is a (almost) working example. I really hope someone can spot the mistake or my misconception of things..

The .setRootIndex on the QListView is confusing me. I tried approaching it differntly in the .index and .parent and .rowCount functions of the CustomModel. But like this it somehow works at least. I have the feeling I am doing something wrong somewhere or the QListView wants things differntly like the QTreeView.

Is it even possible and a good idea to use the same model in two views? I really thought so and this is the hole point of a model/viewcontroller approach, isn't it?

# -*- coding: utf-8 -*-

from typing import *


from PySide2 import QtWidgets
from PySide2.QtCore import QAbstractItemModel, QModelIndex
from PySide2.QtGui import Qt
from PySide2.QtWidgets import QListView, QTreeView


class FirstLevelItem:
    def __init__(self, name) -> None:
        self.name = name
        self.children = []


class SecondLevelItem:
    def __init__(self, name, parent) -> None:
        self.name = name
        self.parent = parent


class CustomModel(QAbstractItemModel):
    def __init__(self, root_items, parent=None):
        super().__init__(parent)
        self.root_items = root_items

    def rowCount(self, itemIndex):
        """Has to return the number of children of the itemIndex.
        If its not a valid index, its a root item, and we return the count of all root_items.
        If its a valid one and can have children, return the number of children.
        This makes the Model to ask for more indexes for each item.
        Only works if parent is set properly"""
        if itemIndex.isValid():
            item = itemIndex.internalPointer()
            if isinstance(item, FirstLevelItem):
                return len(item.children)
            else:
                return 0
        else:
            return len(self.root_items)

    def columnCount(self, parent=None):
        return 1

    def parent(self, child_index):
        """Has to return an index pointing to the parent of the current index."""
        if child_index.isValid():
            # get the item of this index
            item = child_index.internalPointer()
            # check if its one with a parent
            if isinstance(item, SecondLevelItem):
                # get the parent obj from the item
                parent_item = item.parent
                # now we have to find the parents row index to be able to create the index pointing to it
                parent_row = parent_item.children.index(item)
                # create an index with the parent row and column and the parent item itself
                return self.createIndex(parent_row, 0, parent_item)
            else:
                return QModelIndex()
        else:
            return QModelIndex()

    def data(self, index, role):
        if not index.isValid():
            return None

        item = index.internalPointer()
        if role == Qt.DisplayRole:
            return item.name

        return None

    def index(self, row, column, parentIndex):
        if parentIndex.isValid():
            parent_item = parentIndex.internalPointer()
            return self.createIndex(row, column, parent_item.children[row])
        else:
            return self.createIndex(row, column, self.root_items[row])


class ModelTestDialog(QtWidgets.QDialog):

    window_instance = None

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowFlags(self.windowFlags() ^ Qt.WindowContextHelpButtonHint)
        # self.setMinimumSize(1024, 1024)
        self.setWindowTitle("ModelTestDialog")

        rootItems = []
        for i in range(0, 3):
            name = ["FirstLevel_A", "FirstLevel_B", "FirstLevel_C"][i]
            rootItem = FirstLevelItem(name)
            rootItems.append(rootItem)
            for j in range(0, 3):
                name = ["SecondLevel_A", "SecondLevel_B", "SecondLevel_C"][j]
                childItem = SecondLevelItem(name, rootItem)
                rootItem.children.append(childItem)

        self.model = CustomModel(rootItems)

        self.treeView = QTreeView()
        self.treeView.setModel(self.model)

        self.listView = QListView()
        self.listView.setModel(self.model)

        self.main_layout = QtWidgets.QVBoxLayout(self)
        self.listViews_layout = QtWidgets.QHBoxLayout()

        self.main_layout.addLayout(self.listViews_layout)
        self.listViews_layout.addWidget(self.treeView)
        self.listViews_layout.addWidget(self.listView)

        self.treeView.clicked[QModelIndex].connect(self.listView.setRootIndex)


if __name__ == "__main__":
    app = QtWidgets.QApplication()
    form = ModelTestDialog()
    form.show()
    app.exec_()

CodePudding user response:

There is absolutely nothing wrong about using the same model in multiple views.

That is the whole concept behind the model/view paradigm (which relies on the principle of separation of concerns): the same model can be shared amongs multiple views, even if they show the content of that model in different ways.

That is completely respected by Qt (as long as the model is properly implemented, obviously); this also happens for similar concepts in Qt, like the QTextDocument interface used in QTextEdit (the same document can be shown on different QTextEdit instances), or the QGraphicsScene shown in a QGraphicsView (each view can show a different portion of the same scene).

The actual issue

You're using the wrong row for the parent:

    parent_row = parent_item.children.index(item)

The above returns the index (row) of the child item, but you need to use createIndex() as a reference for the parent, because parent() has to return the row/column of the parent, not that of the child.

In this simple case, just return the index within the root_items:

    parent_row = self.root_items.index(parent_item)

A better approach

I would suggest a more flexible structure, where a single base class is used for all items, and it always has a parent attribute. To do this, you need to also create a "root item" which contains all top level items.

You can still create subclasses for items if you need more flexibility or specialization, but the default behavior remains unchanged, making the implementation simpler especially in the case you need further levels within the structure.

The major benefit of this approach is that you never need to care about the item type to know its level: you know that you need to access the root item when the given index is invalid, and for any other case (like index creation, parent access, etc), the implementation is much more easy and readable. This will automatically make easier to add support for other features, like moving items and drag&drop.

class TreeItem:
    parent = None
    def __init__(self, name='', parent=None):
        self.name = name
        self.children = []
        if parent:
            parent.appendChild(self)

    def appendChild(self, item):
        self.insertChild(len(self.children), item)

    def insertChild(self, index, item):
        self.children.insert(index, item)
        item.parent = self

    def row(self):
        if self.parent:
            return self.parent.children.index(self)
        return -1


class CustomModel(QAbstractItemModel):
    def __init__(self, root_items=None, parent=None):
        super().__init__(parent)
        self.root_item = TreeItem()
        if root_items:
            for item in root_items:
                self.root_item.appendChild(item)

    def rowCount(self, itemIndex):
        if itemIndex.isValid():
            return len(itemIndex.internalPointer().children)
        else:
            return len(self.root_item.children)

    def columnCount(self, parent=None):
        return 1

    def parent(self, child_index):
        if child_index.isValid():
            item = child_index.internalPointer()
            if item.parent:
                return self.createIndex(item.parent.row(), 0, item.parent)
        return QModelIndex()

    def data(self, index, role):
        if not index.isValid():
            return None

        item = index.internalPointer()
        if role == Qt.DisplayRole:
            return item.name

    def index(self, row, column, parentIndex=QModelIndex()):
        if parentIndex.isValid():
            parent_item = parentIndex.internalPointer()
            return self.createIndex(row, column, parent_item.children[row])
        else:
            return self.createIndex(row, column, self.root_item.children[row])


class ModelTestDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowFlags(self.windowFlags() ^ Qt.WindowContextHelpButtonHint)
        self.setWindowTitle('ModelTestDialog')

        rootItems = []
        for i in range(0, 3):
            name = 'FirstLevel {}'.format('ABC'[i])
            rootItem = TreeItem(name)
            rootItems.append(rootItem)
            for j in range(0, 3):
                name = 'SecondLevel {} (child of {})'.format('ABC'[j], 'ABC'[i])
                TreeItem(name, rootItem)
                # or, alternatively:
                # rootItem.appendChild(TreeItem(name))

        self.model = CustomModel(rootItems)

        self.treeView = QTreeView()
        self.treeView.setModel(self.model)

        self.listView = QListView()
        self.listView.setModel(self.model)

        self.main_layout = QVBoxLayout(self)
        self.listViews_layout = QHBoxLayout()

        self.main_layout.addLayout(self.listViews_layout)
        self.listViews_layout.addWidget(self.treeView)
        self.listViews_layout.addWidget(self.listView)

        self.treeView.clicked.connect(self.listView.setRootIndex)

As you can see, the whole model code is much simpler and cleaner: there is no need to check for item level/type, as the concept of the structure makes that automatically immediate.

Further notes:

  • the Qt API suggests that the parent argument of index() should be optional; while it's common to use None for that, a default (and invalid) QModelIndex() is preferable, as I did above;
  • python implicitly returns None if no other return value is given;
  • in the last few years, Qt has been in the process of removing all overloaded signals, replacing them with more verbose and unique ones; in general, it's unnecessary to specify them, especially where no overload actually exists (self.treeView.clicked);
  • Related