pyjamas
pyjamas

Reputation: 5428

PyQt model for connecting to multiple different QTableViews?

I have the classes defined in the code below to display a Pandas DataFrame (data structure for representing 2D tables) with a MultiHeader (a header with multiple levels) like this:

enter image description here

What the data might look like in Excel: enter image description here

I do this with two QTableViews, one for the data in the DataFrame itself and one for the MultiHeader labels. However, I would like to be able to store the DataFrame in a single model, and connect these multiple QTableViews to it. Ideally I could pass an additional argument to the data() method from the view indicating whether the view is for the header or body, but I don't think this is possible?

Some reasons I want to combine them into a single model...

How can I refactor this code so the views are connected to only a single model for the single DataFrame?


from PyQt5 import QtGui, QtCore, QtWidgets
import pandas as pd
import numpy as np
import sys

# DataTableModel and DataTableView show the data in the rows of the DataFrame

class DataTableModel(QtCore.QAbstractTableModel):
    """
    Model for DataTableView to connect for DataFrame data
    """

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

    # Headers for DataTableView are hidden. Header data is shown in HeaderView
    def headerData(self, section, orientation, role=None):
        pass

    def columnCount(self, parent=None):
        return len(self.df.columns)

    def rowCount(self, parent=None):
        return len(self.df)

    # Returns the data from the DataFrame
    def data(self, index, role=None):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            col = index.column()
            cell = self.df.iloc[row, col]
            return str(cell)

class DataTableView(QtWidgets.QTableView):
    def __init__(self, df):
        super().__init__()

        # Create and set model
        model = DataTableModel(df)
        self.setModel(model)

        # Hide the headers. The DataFrame headers (index & columns) will be displayed in the DataFrameHeaderViews
        self.horizontalHeader().hide()
        self.verticalHeader().hide()

# HeaderModel and HeaderView show the header of the DataFrame, in this case a 3 level header

class HeaderModel(QtCore.QAbstractTableModel):
    def __init__(self, df):
        super().__init__()
        self.df = df

    def columnCount(self, parent=None):
        return len(self.df.columns.values)

    def rowCount(self, parent=None):
        if type(self.df.columns) == pd.MultiIndex:
            if type(self.df.columns.values[0]) == tuple:
                return len(self.df.columns.values[0])
            else:
                return 1

    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.ToolTipRole:
            if type(self.df.columns) == pd.MultiIndex:
                row = index.row()
                col = index.column()
                return str(self.df.columns.values[col][row])
            else:  # Not MultiIndex
                col = index.column()
                return str(self.df.columns.values[col])

    # A simple example of some way this model might modify its data
    def delete_first_column(self):
        self.beginResetModel()
        self.df = self.df.drop(self.df.columns[0], axis=1)
        self.endResetModel()

class HeaderView(QtWidgets.QTableView):
    def __init__(self, df):
        super().__init__()
        self.setModel(HeaderModel(df))
        self.clicked.connect(self.model().delete_first_column)

        self.horizontalHeader().hide()
        self.verticalHeader().hide()

        self.setFixedHeight(115)

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setStyle('Windows XP')

    tuples = [('A', 'one', 'x'), ('A', 'one', 'y'), ('A', 'two', 'x'), ('A', 'two', 'y'),
              ('B', 'one', 'x'), ('B', 'one', 'y'), ('B', 'two', 'x'), ('B', 'two', 'y')]
    columns = pd.MultiIndex.from_tuples(tuples, names=['first', 'second', 'third'])
    multidf = pd.DataFrame(np.arange(40).reshape(5,8), columns=columns[:8])

    container = QtWidgets.QWidget()
    layout = QtWidgets.QVBoxLayout()
    container.setLayout(layout)

    header = HeaderView(multidf)
    data = DataTableView(multidf)

    layout.addWidget(header)
    layout.addWidget(data)

    print(header.model().df is data.model().df)
    container.show()
    sys.exit(app.exec_())

Upvotes: 1

Views: 2033

Answers (1)

Simon Hibbs
Simon Hibbs

Reputation: 6211

I'm not sure I can definitively answer this, but the way I'd try is to keep the two models and two views you already have, then create a new model and view that act at unified interfaces. In fact they would just be wrappers over your existing models and views. So if someone calls delete_first_column() on your wrapper class, it handles the details of passing that on to both the underlying body and header, keeping them in sync.

If you're adventurous, you can use QTableView.setSpan() to create the appearance of merged columns in your view.

Upvotes: 1

Related Questions