Update QTableView When Data Source Changes in Qt Using `QAbstractTableModel` and `QSortFilterProxyModel`.

Understanding the Problem and Solution

The problem at hand revolves around updating a QTableView when its data source changes. A QAbstractTableModel serves as the “base” table model, while a QSortFilterProxyModel is used to filter and sort the data. However, the current implementation does not update the QTableView after the data source changes.

Background Information

To tackle this issue, it’s essential to understand how the QAbstractTableModel and QSortFilterProxyModel interact with each other. The QAbstractTableModel is responsible for providing data to the view, while the QSortFilterProxyModel acts as an intermediary between the model and the view, filtering and sorting the data before it reaches the view.

Update Strategy

To update the QTableView when its data source changes, we need to follow these steps:

  1. Update the DataStore object with new data.
  2. Notify the ProxyModel of the change by calling self.proxy_model.setSourceModel(self.model) and then self.proxy_model.update().
  3. Finally, update the QTableView by setting its model to the updated ProxyModel.

Code Explanation

Here’s a detailed explanation of the changes made:

DataStore Class Updates

The DataStore class now holds a reference to the new data source.

class DataStore(object):
    def __init__(self):

        # ...

        self.model = TableModel(self.df)
        self.proxy_model = ProxyModel()
        self.proxy_model.setSourceModel(self.model)
        self.proxy_model.setDynamicSortFilter(True)

    @property
    def df(self):
        return self.model.df

    @df.setter
    def df(self, df):
        # Update the data source here
        self.model.beginResetModel()
        self.model.__data = df.copy()
        self.model.endResetModel()

The df property now updates the model’s internal state when its value changes.

Proxy Model Class Updates

No changes are required in the ProxyModel class, as it already acts as a filter and does not affect the data source directly.

class ProxyModel(QtCore.QSortFilterProxyModel):
    # ...

def filterAcceptsRow(self, sourceRow, sourceParent):
    # ... (unchanged)

Table Model Class Updates

The TableModel class now updates its internal state when the data source changes.

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self.__data = data

    @property
    def df(self):
        return self.__data

    @df.setter
    def df(self, df):
        # Update the data source here
        self.beginResetModel()
        self.__data = df.copy()
        self.endResetModel()

    # ... (unchanged)

The TableModel class now updates its internal state when the data source changes.

MainWindow Class Updates

Finally, the MainWindow class has been updated to reflect these changes.

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.ds = DataStore()
        # ...

    def on_event(self):
        # ...

        data = {
            "Name": ["new_name1", "new_name2", "new_name3"],
            "Value": ["new_value1", "new_value2", "new_value3"],
            "Description": ["new_descr1", "new_descr2", "new_descr3"],
            "Setting": ["TRUE", "FALSE", "TRUE"],
        }

        new_df = pd.DataFrame(data, columns=["Name", "Value", "Setting"])
        self.ds.df = new_df

The MainWindow class now updates the data source after a change occurs.

Conclusion

In this article, we explored how to update a QTableView when its data source changes using a QAbstractTableModel and a QSortFilterProxyModel. We modified the classes to include an update strategy that notifies both models of any changes to their data sources. This ensures seamless updates between the view, model, and proxy model, providing a robust solution for dynamic data rendering in Qt-based applications.

Code

Here’s the complete code with all the modifications:

import sys

import pandas as pd
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt


class DataStore(object):
    def __init__(self):

        # ...

        self.model = TableModel(self.df)
        self.proxy_model = ProxyModel()
        self.proxy_model.setSourceModel(self.model)
        self.proxy_model.setDynamicSortFilter(True)

    @property
    def df(self):
        return self.model.df

    @df.setter
    def df(self, df):
        # Update the data source here
        self.model.beginResetModel()
        self.model.__data = df.copy()
        self.model.endResetModel()


class ProxyModel(QtCore.QSortFilterProxyModel):
    def filterAcceptsRow(self, sourceRow, sourceParent):
        print("Entered filterAcceptsRow function with source row: {}".format(sourceRow))
        idx = self.sourceModel().index(sourceRow, 2, sourceParent)
        value = idx.data()
        if value == "TRUE":
            return True
        if value == "FALSE":
            return False


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self.__data = data

    @property
    def df(self):
        return self.__data

    @df.setter
    def df(self, df):
        # Update the data source here
        self.beginResetModel()
        self.__data = df.copy()
        self.endResetModel()

    def data(self, index, role):
        if role == Qt.DisplayRole:
            value = self.__data.iloc[index.row(), index.column()]
            return str(value)

    def rowCount(self, index):
        return self.__data.shape[0]

    def columnCount(self, index):
        return self.__data.shape[1]

    def headerData(self, section, orientation, role):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self.__data.columns[section])

            if orientation == Qt.Vertical:
                return str(self.__data.index[section])


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.ds = DataStore()
        # ...

    def on_event(self):
        data = {
            "Name": ["new_name1", "new_name2", "new_name3"],
            "Value": ["new_value1", "new_value2", "new_value3"],
            "Description": ["new_descr1", "new_descr2", "new_descr3"],
            "Setting": ["TRUE", "FALSE", "TRUE"],
        }

        new_df = pd.DataFrame(data, columns=["Name", "Value", "Setting"])
        self.ds.df = new_df

Last modified on 2025-01-19