Reputation: 5428
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:
What the data might look like in Excel:
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...
header.model().df is data.model().df
is True to start, but False once delete_first_column
is called and self.df
is overwrittenHow 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
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