PyQt - Implement a QAbstractTableModel for display in QTableView
Asked Answered
P

4

18

I would like to display a pandas data frame in a PyQt table. I have made some progress with this, but have not been able to correctly derive the Table Model class. Any help with this would be much appreciated.

** Note full example code here **

I am struggling to generate a valid QtCore.QAbstractTableModel derived class. Following on from a previous question about QItemDelegates I am trying to generate a table model from a Pandas DataFrame to insert real data. I have working example code here, but if I replace my TableModel with TableModel2 in the Widget class (ln 152) I cannot get the table to display.

class TableModel2(QtCore.QAbstractTableModel): 
    def __init__(self, parent=None, *args): 
        super(TableModel2, self).__init__()
        #QtCore.QAbstractTableModel.__init__(self, parent, *args)
        self.datatable = None
        self.headerdata = None
        self.dataFrame = None
        self.model = QtGui.QStandardItemModel(self)

    def update(self, dataIn):
        print 'Updating Model'
        self.datatable = dataIn
        print 'Datatable : {0}'.format(self.datatable)
        headers = dataIn.columns.values
        header_items = [
                    str(field)
                    for field in headers
        ]
        self.headerdata = header_items
        print 'Headers'
        print self.headerdata

        for i in range(len(dataIn.index.values)):
            for j in range(len(dataIn.columns.values)):
                #self.datatable.setItem(i,j,QtGui.QTableWidgetItem(str(df.iget_value(i, j))))
                self.model.setItem(i,j,QtGui.QStandardItem(str(dataIn.iget_value(i, j))))

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.datatable.index) 

    def columnCount(self, parent=QtCore.QModelIndex()):
        return len(self.datatable.columns.values) 

    def data(self, index, role=QtCore.Qt.DisplayRole): 
        if not index.isValid(): 
            return QtCore.QVariant()
        elif role != QtCore.Qt.DisplayRole: 
            return QtCore.QVariant() 
        #return QtCore.QVariant(self.model.data(index)) 
            return QtCore.QVariant(self.model.data(index)) 

    def headerData(self, col, orientation, role):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return QtCore.QVariant()
        return QtCore.QVariant(self.headerdata[col])

    def setData(self, index, value, role=QtCore.Qt.DisplayRole):
        print "setData", index.row(), index.column(), value

    def flags(self, index):
        if (index.column() == 0):
            return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled
        else:
            return QtCore.Qt.ItemIsEnabled 

I am attempting to create the model and then add it to the view, like this:

class Widget(QtGui.QWidget):
    """
    A simple test widget to contain and own the model and table.
    """
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        l=QtGui.QVBoxLayout(self)
        cdf = self.get_data_frame()
        self._tm=TableModel(self)
        self._tm.update(cdf)
        self._tv=TableView(self)
        self._tv.setModel(self._tm)
        for row in range(0, self._tm.rowCount()):
            self._tv.openPersistentEditor(self._tm.index(row, 0))
        l.addWidget(self._tv)

    def get_data_frame(self):
        df = pd.DataFrame({'Name':['a','b','c','d'], 
        'First':[2.3,5.4,3.1,7.7], 'Last':[23.4,11.2,65.3,88.8], 'Class':[1,1,2,1], 'Valid':[True, True, True, False]})
        return df

Thanks for your attention!

Note : Edit 2 I have incorporated the QStandardItemModel into TableModel2. Also deleted the dataFrameToQtTable function after @mata's comment. This is getting a bit closer but still not working.

Premature answered 17/7, 2013 at 10:28 Comment(1)
Pandas has a Qt table example in its repo. github.com/pydata/pandas/commits/master/pandas/sandbox/…Baptista
P
18

Ok I have figured this one out with the above suggestion and some help from the Rapid GUI book by Summerfield. There is no underlying model that exists in the QAbstractTableModel. Only three functions need be overridden, and the data may be stored in any user defined format, as long as it is returned in the data call.

A very simple implementation could be:

class TableModel(QtCore.QAbstractTableModel): 
    def __init__(self, parent=None, *args): 
        super(TableModel, self).__init__()
        self.datatable = None

    def update(self, dataIn):
        print 'Updating Model'
        self.datatable = dataIn
        print 'Datatable : {0}'.format(self.datatable)

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.datatable.index) 

    def columnCount(self, parent=QtCore.QModelIndex()):
        return len(self.datatable.columns.values) 

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            i = index.row()
            j = index.column()
            return '{0}'.format(self.datatable.iget_value(i, j))
        else:
            return QtCore.QVariant()

    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled

This enables you to view any compatable data frame in a Qt view.

I have updated the Gist over here

This should get you going quickly if you also need to do this.

Premature answered 17/7, 2013 at 14:37 Comment(1)
Unfortunately column and index names doesn't display ? It will be nice if you could fix that.Cirrose
S
2

This is probably your problem:

def rowCount(self, parent=QtCore.QModelIndex()):
    if type(self.datatable) == pd.DataFrame:
    ...


def columnCount(self, parent=QtCore.QModelIndex()):
    if (self.datatable) == pd.DataFrame:
    ...

You set your datatable to a QTableWidget in dataFrameToQtTable, so it can't be a pd.DataFrame, your methods will always return 0.

Without the type check, you would have caught the problem immediately. Do you really want to silently ignore all cases where your type doesn't match (better let it raise an error if it doesn't follow the same interface you're expecting)? Typechecks are in most cases unnecessary.

Sinapism answered 17/7, 2013 at 11:15 Comment(2)
Yes, I know that this is wrong, but this is my problem. I don't think that it should be set to a QTableWidgetItem. I would like to update the internal model in QAbstractTableModel, but I am not sure of how to do this - or find the row or column count. Thanks.Premature
I have simplified the example code to remove this dataFrameToQtTable hopefully the problem is more clear.Premature
C
1

Pandas 0.13 provides as an experimental feature:

PySide support for the qtpandas DataFrameModel and DataFrameWidget

see https://github.com/pydata/pandas/blob/master/doc/source/faq.rst

you can add this feature using

from pandas.sandbox.qtpandas import DataFrameModel, DataFrameWidget
Cirrose answered 4/3, 2014 at 8:6 Comment(0)
J
0

Here is a minimal QAbstractItemModel implementation to show how it worked.

# Created by [email protected] at 2022/3/2
from __future__ import annotations

import typing

from PySide2 import QtWidgets, QtCore


class TreeNode(object):
    columns: typing.Sequence[str]
    children: typing.Sequence[TreeNode]
    parent: typing.Union[None, TreeNode]

    def __init__(self, columns, children) -> None:
        self.columns = columns
        self.children = children
        self.parent = None
        for child in self.children:
            child.parent = self

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


class TreeModel(QtCore.QAbstractItemModel):
    def __init__(self, parent: typing.Optional[QtCore.QObject] = None) -> None:
        super().__init__(parent)
        self._rootNode = TreeNode(columns=["Name", "Description"], children=[
            TreeNode(columns=["Animal", "The Animal"], children=[
                TreeNode(columns=["Ant", "The Ant"], children=[]),
                TreeNode(columns=["Bee", "The Bee"], children=[]),
                TreeNode(columns=["Cat", "The Cat"], children=[]),
            ]),
            TreeNode(columns=["Plant", "The Plant"], children=[
                TreeNode(columns=["Apple", "The Apple"], children=[]),
                TreeNode(columns=["Banana", "The Banana"], children=[]),
            ]),
        ])

    def _indexToNode(self, index: QtCore.QModelIndex):
        return index.internalPointer() if index.isValid() else self._rootNode

    def index(self, row: int, column: int, parent: QtCore.QModelIndex = ...) -> QtCore.QModelIndex:
        return self.createIndex(row, column, self._indexToNode(parent).children[row])

    def parent(self, child: QtCore.QModelIndex) -> QtCore.QModelIndex:
        parent = self._indexToNode(child).parent
        return QtCore.QModelIndex() if parent is None else self.createIndex(parent.row(), 0, parent)

    def rowCount(self, parent: QtCore.QModelIndex = ...) -> int:
        return len(self._indexToNode(parent).children)

    def columnCount(self, parent: QtCore.QModelIndex = ...) -> int:
        return len(self._indexToNode(parent).columns)

    def data(self, index: QtCore.QModelIndex, role: int = ...) -> typing.Any:
        if role == QtCore.Qt.ItemDataRole.DisplayRole:
            return self._indexToNode(index).columns[index.column()]

    def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = ...) -> typing.Any:
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return self._rootNode.columns[section]


app = QtWidgets.QApplication()
treeView = QtWidgets.QTreeView()
treeView.setModel(TreeModel())
treeView.show()
app.exec_()
Jameljamerson answered 2/3, 2022 at 6:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.