My question is what would be the most efficient way to display an object (in my case Project
class) attributes in different widgets? I read that there are proxy models, but I couldn't figure out if it would be more efficient in my case to use proxy models rather then three separate QStandardItemModels (one model per widget)? I also don't understand how to implement proxy models in my case. The real case scenario and a simplified example are described below.
I need to display different attributes of an object (Project class
) in different widgets. In real case scenario Project
object has four attributes: two data frames (df1
and df2
) and two strings (project
and folder
). project
attribute is unique for each project, while folder
attribute can be the same for different projects. df1
has 4 columns and looks likes this (Item Name
column can have duplicate values, Number
column will not have duplicate values):
Item Name Checked Number Checked Number
0 Banana True UHOSD7 False
1 Apple False T71HA0 False
2 Banana True WOGUAJ False
3 Plum False C0CBN5 False
Another data frame looks like this (no duplicate values in "Parameters"
column):
Parameters Values
0 par1 6
1 par2 5
2 par3 6
I will have following widgets in the software: one QTreeView
(i.e.tree
) and three QListViews
(list1
, list2
and list3
). In tree
I need to display project
attributes for different Project
objects grouped by folder
attribute (as shown on the screenshot below). Items in tree
can be added or removed.
In list1
, list2
and list3
I need to display different columns of df1
and df2
attrbutes of Project
object that is currently selected in the tree
. In list1
I need to display unique values of Item Name
column of df1
. Selecting an item in a list1
should display in list2
items from Number
column for rows where value in Item Name
column is equal to the value of the selected item in list1
(for example if "Banana" selected in list1
, list2
should contain UHOSD7
and WOGUAJ
). Items in list1
and list2
can not be added/removed, but items are checkable. If check status of item is changed then information about current check status need to be updated in df1
(columns Checked
or Checked Number
).
In list3
I need to display Parameters
column of df2
. In this list items are unchackable and items can not be removed/added.
By selecting an item in any of the lists (list1
, list2
and list3
) I need to have access to any attribute of Project
object to which selected item belongs (as mentioned above no removing/adding of items in df1
, df2
, project
, folder
needed).
As an example, I have a QTreeView
and two QListViews
. Each project has three attributes: project name, folder and one dataframe (``df1```).
To display items in the widgets I have created three models:
CustomTreeModel
subclassing QStandardItemModel
displays data in tree
;
NameListModel
subclassing QStandardItemModel
displays data in list1
;
NumberListModel
subclassing QStandardItemModel
displays data in list2
;
To update information in the df1
about check status of items in list1
and list2
of selectedProject
item and to handle change of item selection in tree
and list1
I use four functions:
handle_tree_item_selection_changed
handle_name_item_checked
handle_name_selection_changed
handle_number_item_checked
Therefore, I have implemented three models: one separate model per widget. I guess it is not the most efficient way to do it.
Example of df1
attribute for Project 1:
Item Name Checked Number Checked Number
0 Banana True UHOSD7 False
1 Apple False T71HA0 False
2 Banana True WOGUAJ False
3 Plum False C0CBN5 False
Code:
import sys
import pandas as pd
from PySide6.QtWidgets import QApplication, QMainWindow, QListView, QTreeView, QWidget, QVBoxLayout
from PySide6.QtGui import QStandardItemModel, QStandardItem
from PySide6.QtCore import Qt
import random
import string
class ProjectItem(object):
def __init__(self, project, folder):
self.project = project
self.folder = folder
self.df = pd.DataFrame({'Item Name': ["Banana", "Apple", "Banana", "Plum"],
'Checked': [bool(random.getrandbits(1)) for i in range(4)], 'Number': [self.id_generator() for i in
range(4)], 'Checked Number': [bool(random.getrandbits(1)) for i in range(4)]})
def id_generator(self, size=6, chars=string.ascii_uppercase + string.digits):
...
return ''.join(random.choice(chars) for _ in range(size))
class NameListModel(QStandardItemModel):
def __init__(self, data_frame=None):
super().__init__()
self._data_frame = data_frame
# Populate the list view with unique item names
if self._data_frame:
unique_items = data_frame['Item Name'].unique()
for item_name in unique_items:
item = QStandardItem(item_name)
item.setCheckable(True)
self.appendRow(item)
@property
def data_frame(self):
return self._data_frame
@data_frame.setter
def data_frame(self, new_data_frame):
self._data_frame = new_data_frame
# Update the model (repopulate or modify existing items)
self.update_model()
def update_dataframe(self, item):
item_name = item.text()
checked = item.checkState() == Qt.Checked
self.data_frame.loc[self.data_frame['Item Name'] == item_name, 'Checked'] = checked
def update_model(self):
self.clear()
unique_items = self.data_frame['Item Name'].unique()
for item_name in unique_items:
item = QStandardItem(item_name)
checked = self.data_frame.loc[self.data_frame['Item Name'] == item_name, 'Checked'].iloc[0]
item.setCheckState(Qt.Checked if checked else Qt.Unchecked)
item.setCheckable(True)
self.appendRow(item)
def selected_item(self, index):
item = self.itemFromIndex(index)
if item:
# Retrieve custom data (ProjectItem) from UserRole
return item.data(Qt.DisplayRole)
class NumberListModel(QStandardItemModel):
def __init__(self, data_frame=None):
super().__init__()
self._data_frame = data_frame
# Populate the list view with unique item names
if self._data_frame:
items = data_frame['Number']
for item_name in items:
item = QStandardItem(item_name)
item.setCheckable(True)
self.appendRow(item)
@property
def data_frame(self):
return self._data_frame
@data_frame.setter
def data_frame(self, new_data_frame):
self._data_frame = new_data_frame
# Update the model (repopulate or modify existing items)
self.update_model()
def update_dataframe(self, item):
item_name = item.text()
checked = item.checkState() == Qt.Checked
self.data_frame.loc[self.data_frame['Number'] == item_name, 'Checked Number'] = checked
def update_model(self):
# Implement logic to update the model when data_frame changes
# For example, clear existing items and repopulate with new data_frame
self.clear()
items = self.data_frame['Number']
for item_name in items:
item = QStandardItem(item_name)
checked = self.data_frame.loc[self.data_frame['Number'] == item_name, 'Checked Number'].iloc[0]
item.setCheckState(Qt.Checked if checked else Qt.Unchecked)
item.setCheckable(True)
self.appendRow(item)
def selected_item(self, index):
item = self.itemFromIndex(index)
if item:
# Retrieve custom data (ProjectItem) from UserRole
return item.data(Qt.DisplayRole)
class CustomTreeModel(QStandardItemModel):
def __init__(self, parent=None):
super().__init__(parent)
self.setHorizontalHeaderLabels(["Project"])
self.folder_items = {} # Store folder items for grouping
def add_project(self, project, folder):
new_project = ProjectItem(project, folder)
if folder not in self.folder_items:
folder_item = QStandardItem(folder)
self.appendRow([folder_item])
self.folder_items[folder] = folder_item
else:
folder_item = self.folder_items[folder]
project_item = QStandardItem(project)
# Set custom data (ProjectItem) as UserRole
project_item.setData(new_project, Qt.UserRole)
# Insert the project item using beginInsertRows and endInsertRows
row = folder_item.rowCount()
self.beginInsertRows(folder_item.index(), row, row)
folder_item.appendRow([project_item])
self.endInsertRows()
# Disable item editing
folder_item.setEditable(False)
project_item.setEditable(False)
def selected_item(self, index):
item = self.itemFromIndex(index)
if item:
# Retrieve custom data (ProjectItem) from UserRole
return item.data(Qt.UserRole)
def handle_tree_item_selection_changed(new_selection, old_selection):
index = new_selection.indexes()[0]
project_item = treemodel.selected_item(index)
if project_item:
print(f"Selected Project: {project_item.project}, Folder: {project_item.folder}")
name_list_model.data_frame = project_item.df
def handle_name_item_checked(item):
item_name = item.text()
checked = item.checkState() == Qt.Checked
index = tree_view.selectionModel().currentIndex()
project_item = treemodel.selected_item(index)
project_item.df.loc[project_item.df['Item Name'] == item_name, 'Checked'] = checked
def handle_name_selection_changed(new_selection, old_selection):
index = tree_view.selectionModel().currentIndex()
project_item = treemodel.selected_item(index)
name_index = new_selection.indexes()[0]
name_item = name_list_model.selected_item(name_index)
if name_item:
number_list_model.data_frame = project_item.df[project_item.df['Item Name'] == name_item]
def handle_number_item_checked(item):
item_name = item.text()
checked = item.checkState() == Qt.Checked
index = tree_view.selectionModel().currentIndex()
project_item = treemodel.selected_item(index)
project_item.df.loc[project_item.df['Number'] == item_name, 'Checked Number'] = checked
if __name__ == "__main__":
app = QApplication(sys.argv)
window = QMainWindow()
window.setWindowTitle("Custom Tree Model Example")
window.setGeometry(100, 100, 600, 400)
tree_view = QTreeView()
central_widget = QWidget()
layout = QVBoxLayout(central_widget)
layout.addWidget(tree_view)
name_list_view = QListView()
layout.addWidget(name_list_view)
number_list_view = QListView()
layout.addWidget(number_list_view)
window.setCentralWidget(central_widget)
treemodel = CustomTreeModel()
tree_view.setModel(treemodel)
tree_view.selectionModel().selectionChanged.connect(handle_tree_item_selection_changed)
name_list_model = NameListModel()
name_list_view.setModel(name_list_model)
name_list_model.itemChanged.connect(handle_name_item_checked)
name_list_view.selectionModel().selectionChanged.connect(handle_name_selection_changed)
number_list_model = NumberListModel()
number_list_view.setModel(number_list_model)
number_list_model.itemChanged.connect(handle_number_item_checked)
treemodel.add_project("Project 1", "Folder 1")
treemodel.add_project("Project 2", "Folder 1")
treemodel.add_project("Project 2", "Folder 3")
window.show()
sys.exit(app.exec())
Your approach is not completely off.
The main problem comes from the fact that you have some fields that are not unique, but you want to make them such by "grouping" their item values.
This makes it necessary to use separated models that expose different index counts and eventually map their values accordingly.
Note that using QStandardItemModel for a source data frame that probably needs to be updated with user interaction is normally not a good choice. A QAbstractItemModel subclass is normally much better, and in this specific case QAbstractListModel is the most logical choice.
The most important benefit is that this makes reading and updating the data frame more immediate, without the need of using signals.
I'd suggest this structure, then:
NameListModel
(a QAbstractListModel subclass) that shows unique names;NumberListModel
(another QAbstractListModel subclass) that lists all numbers in the project; this model also returns the name associated with the number using a special role;NumberFilteredModel
(a filter proxy) that eventually shows only numbers corresponding to the selected "filter" (the name selected in NameListModel) and by default shows nothing;Both QAbstractListModels actually keep a list of their contents (unique names, or numbers) as main data source for the view, which is then used as reference with the actual original data frame in order to get and set the real values; most importantly, the check state, but also the name associated with the number in the NumberListModel (used for filtering).
ProjectRole = Qt.UserRole + 1
NameRole = ProjectRole + 10
class ProjectModel(QStandardItemModel):
def __init__(self, parent=None):
super().__init__(parent)
self.setHorizontalHeaderLabels(["Project"])
self.folder_items = {} # Store folder items for grouping
def add_project(self, project, folder):
new_project = ProjectItem(project, folder)
if folder not in self.folder_items:
folder_item = QStandardItem(folder)
self.appendRow([folder_item])
self.folder_items[folder] = folder_item
else:
folder_item = self.folder_items[folder]
project_item = QStandardItem(project)
project_item.setData(new_project, ProjectRole)
folder_item.appendRow([project_item])
class NameListModel(QAbstractListModel):
def __init__(self, data_frame):
super().__init__()
self.df = data_frame
self._names = data_frame['Item Name'].unique()
def flags(self, index):
return super().flags(index) | Qt.ItemIsUserCheckable
def rowCount(self, parent=None):
return len(self._names)
def data(self, index, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
return self._names[index.row()]
elif role == Qt.CheckStateRole:
name = self._names[index.row()]
checked = self.df.loc[
self.df['Item Name'] == name, 'Checked'
].iloc[0]
return Qt.Checked if checked else Qt.Unchecked
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.CheckStateRole:
name = self._names[index.row()]
self.df.loc[
self.df['Item Name'] == name, 'Checked'
] = value == Qt.Checked
self.dataChanged.emit(index, index)
return True
return False
class NumberListModel(QAbstractListModel):
def __init__(self, data_frame=None):
super().__init__()
self.df = data_frame
self._numbers = data_frame['Number']
def flags(self, index):
return super().flags(index) | Qt.ItemIsUserCheckable
def rowCount(self, parent=None):
return len(self._numbers)
def data(self, index, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
return self._numbers[index.row()]
elif role == Qt.CheckStateRole:
number = self._numbers[index.row()]
checked = self.df.loc[
self.df['Number'] == number, 'Checked'
].iloc[0]
return Qt.Checked if checked else Qt.Unchecked
elif role == NameRole:
number = self._numbers[index.row()]
return self.df.loc[
self.df['Number'] == number, 'Item Name'
].iloc[0]
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.CheckStateRole:
name = self._numbers[index.row()]
self.df.loc[
self.df['Number'] == name, 'Checked'
] = value == Qt.Checked
self.dataChanged.emit(index, index)
return True
return False
class NumberFilteredModel(QSortFilterProxyModel):
_filter = None
def __init__(self, source):
super().__init__()
self.source = source
self.setSourceModel(source)
def invalidate(self):
self.setFilter()
def setFilter(self, filter=None):
if self._filter == filter:
return
self._filter = filter
self.invalidateFilter()
def filterAcceptsRow(self, row, parent):
if self._filter is None:
return False
return self.source.index(row, 0).data(NameRole) == self._filter
The Project object also provides a simple helper function that resets the filter of the number model.
def generate_id(size=6, chars=string.ascii_uppercase+string.digits):
return ''.join(random.choice(chars) for _ in range(size))
class ProjectItem(object):
def __init__(self, project, folder):
self.project = project
self.folder = folder
self.df = pd.DataFrame({
'Item Name': ["Banana", "Apple", "Banana", "Plum"],
'Checked': [bool(random.getrandbits(1)) for i in range(4)],
'Number': [generate_id() for i in range(4)],
'Checked Number': [bool(random.getrandbits(1)) for i in range(4)]
})
self.name_model = NameListModel(self.df)
self.number_model = NumberFilteredModel(NumberListModel(self.df))
def invalidate_number_filter(self):
self.number_model.invalidate()
Finally, the UI implementation. As a related note, while using simple functions and global objects is obviously not strictly forbidden, using a proper class with its own methods is not only preferable, but also better in terms of code/object structure, readability and maintenance.
class TestWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Custom Tree Model Example")
self.tree_view = QTreeView()
self.tree_view.setEditTriggers(self.tree_view.NoEditTriggers)
self.projectModel = ProjectModel()
self.tree_view.setModel(self.projectModel)
self.projectModel.add_project("Project 1", "Folder 1")
self.projectModel.add_project("Project 2", "Folder 1")
self.projectModel.add_project("Project 2", "Folder 3")
self.name_list_view = QListView()
self.number_list_view = QListView()
central_widget = QWidget()
layout = QVBoxLayout(central_widget)
layout.addWidget(self.tree_view)
layout.addWidget(self.name_list_view)
layout.addWidget(self.number_list_view)
self.setCentralWidget(central_widget)
self.tree_view.selectionModel().selectionChanged.connect(
self.project_selected)
def project_selected(self, selection):
indexList = selection.indexes()
if not indexList:
return
project_item = indexList[-1].data(ProjectRole)
if project_item:
self.name_list_view.setModel(project_item.name_model)
self.number_list_view.setModel(project_item.number_model)
project_item.invalidate_number_filter()
# reconnect, since setModel() changes the selection model
self.name_list_view.selectionModel().selectionChanged.connect(
self.name_selected)
def name_selected(self, selection):
indexList = selection.indexes()
if not indexList:
return
name = indexList[-1].data()
self.number_list_view.model().setFilter(name)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = TestWindow()
window.show()
sys.exit(app.exec())