Skip to content
Advertisement

PyQT: Storing multidimensional data and displaying as QTableView using model/view framework

EDIT: I’ve changed my approach since asking this question – see answer below.

I’m building an application which displays data in a series of tables. I’m currently using PyQT’s item-based QTableWidget and manually updating the tables whenever data changes. I’d like to migrate to a model/view architecture using QAbstractItemModel and QTableView.

My data has 3 dimensions:

record_number (record1, record2, record3 etc.)
attribute (name, address, phone etc.)
data_source (db, edited, csv)

I’d like to store this data in a single model, and display different dimensions in different tables.

Data example:

data = {
    "record1": {
        "name": {"csv": "a", "edited": "b", "db": "c"},
        "address": {"csv": "d", "edited": "e", "db": "f"},
        "phone": {"csv": "g", "edited": "h", "db": "i"},
    },
    "record2": {
        "name": {"csv": "j", "edited": "k", "db": "l"},
        "address": {"csv": "m", "edited": "n", "db": "o"},
        "phone": {"csv": "p", "edited": "q", "db": "r"},
    },
    "record3": {
        "name": {"csv": "s", "edited": "t", "db": "u"},
        "address": {"csv": "v", "edited": "w", "db": "x"},
        "phone": {"csv": "y", "edited": "z", "db": "aa"},
    }
}

In table1 I want to display records on the y-axis, and attributes on the x-axis, using the “edited” data_source:

    name    | address |   phone
1:  b           e           h
2:  k           n           q
3:  t           w           z

In table2 I want to display a single record, with attribute on the y-axis and data_source on the x-axis:

Record1

            csv |   edited  |   db
name        a         b          c
address     d         e          f
phone       g         h          i

How would I implement this using PyQT’s model/view framework, so that data is stored in a single model but represented differently for table1 and table2?

Advertisement

Answer

I’ve changed my approach since asking this question. The question came from a misunderstanding of Qt’s model/view architecture – I assumed that the model == datastore, whereas it looks like in most cases the model contains data access and update methods.

I found these two tutorials helpful in demonstrating this:

As per @musicamante’s comment, I’m now using two models to access data. Data is stored in a Pandas DataFrame with a MultiIndex (to account for the nested dictionary data), and the selection in table1 is used to set the which data is accessed in model2.

class Model1(QtCore.QAbstractTableModel):
    index: QtCore.QModelIndex
    data: pd.DataFrame
    _data: pd.DataFrame
    h_header: list

    def __init__(self, data, h_header):
        super(Model1, self).__init__()
        self._data = data
        self.h_header = h_header

    def data(self, index: QtCore.QModelIndex, role=None):
        row = index.row()
        col = index.column()
        col_name = self.h_header[col]

        if role == Qt.DisplayRole:
            source = "edited"
            value = self._data.loc[row, (col_name, source)]
            return str(value)

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

    def columnCount(self, index):
        return len(self.h_header)

    def headerData(self, section, orientation, role=None):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self.h_header[section])
            if orientation == Qt.Vertical:
                return str(self._data.index[section])


class Model2(QtCore.QAbstractTableModel):
    index: QtCore.QModelIndex
    data: pd.DataFrame
    _data: pd.DataFrame
    h_header: list
    v_header: list

    def __init__(self, data, h_header, v_header):
        super(Model2, self).__init__()
        self._data = data
        self.h_header = h_header
        self.v_header = v_header
        self.current_row = -1

    def set_current_row(self, row: int):
        self.current_row = row
        self.layoutChanged.emit()

    def data(self, index: QtCore.QModelIndex, role=None):
        row = index.row()
        col = index.column()
        row_name = self.v_header[row]
        col_name = self.h_header[col]

        if role == Qt.DisplayRole:
            if self.current_row == -1:
                return ""
            else:
                value = self._data.loc[self.current_row, (row_name, col_name)]
                return str(value)

    def rowCount(self, index):
        return len(self.v_header)

    def columnCount(self, index):
        return len(self.h_header)

    def headerData(self, section, orientation, role=None):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self.h_header[section])
            if orientation == Qt.Vertical:
                return str(self.v_header[section])


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.table1 = QtWidgets.QTableView()
        self.table2 = QtWidgets.QTableView()
    
    ...

    def connect_models_tables(self):

        self.model1 = Model1(self.data, self.header_list)
        self.model2 = Model2(self.header_list)

        self.table1.setModel(self.model1)
        self.table2.setModel(self.model2)

        sel1 = QtCore.QItemSelectionModel(self.model1)
        self.table1.setSelectionModel(sel1)
        self.table1.selectionModel().selectionChanged.connect(self.on_sel_change)
    
    def on_sel_change(self):
        if len(self.table1.selectedIndexes()):
            self.model2.set_current_row(self.table1.selectedIndexes()[0].row())
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement