pythonpyqtqabstracttablemodel

how insertRow to QAbstractTableModel with indexes?


im try to create small app in PyQT that show table from database and user will be able to add new rows and fill them. Table's data taken from pd.dataframe with 2 cols and dates-indexes. New inserted rows must be with new index == last date-index + 1 day and 2 empty editable items App looks like this

img - app

And i want it to be like this

img - what i want

So, I spent many days on how to do this, but did not understand. This question may seem Newbee, but if someone can help I will be grateful

here is a full reproducible code

import sys

import pandas as pd
from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableView, QWidget, QGridLayout, QPushButton


class TableModel(QAbstractTableModel):

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

    def rowCount(self, parent=None) -> int:
        return self.table_data.shape[0]

    def columnCount(self, parent=None) -> int:
        return self.table_data.shape[1]

    def data(self, index: QModelIndex, role: int = ...):
        if role == Qt.DisplayRole:
            row = self.table_data.index[index.row()]
            col = self.table_data.columns[index.column()]
            value = self.table_data.loc[row][col]
            return str(value)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self.table_data.columns[section])
            if orientation == Qt.Vertical:
                return str(self.table_data.index[section])

    def insertRows(self, row: int, count: int, parent: QModelIndex = ...) -> bool:
        self.beginInsertRows()
        # whats here?
        self.endInsertRows()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = QMainWindow()

    widget = QWidget()

    layout = QGridLayout()
    widget.setLayout(layout)
    indexes = ['2023-01-03', '2023-01-02', '2023-01-01']
    data = {'col1': ['aa', 'bb', 'cc'], 'col2': [44, 55, 66]}
    table_data = pd.DataFrame(data=data, index=indexes)
    model = TableModel(table_data=table_data)
    table = QTableView()
    table.setModel(model)
    layout.addWidget(table)

    button = QPushButton("add row")
    button.clicked.connect(model.insertRows)

    button_2 = QPushButton("save changes")

    layout.addWidget(button)
    layout.addWidget(button_2)
    win.setCentralWidget(widget)

    win.show()
    app.exec()

Solution

  • Oh boy. Okay, so Qt models and indexes are quite complicated as you might already know, but let me try to explain. The model mediates the view and data rather than holding the data in itself or in the view. When inserting rows, you are letting the model know that the structure is going to change, you make the change, then tell it that you are done.

    The parent index corresponds to the parent into which the new rows are inserted; first and last are the row numbers that the new rows will have after they have been inserted.

    self.beginInsertRows(parent, 2, 4) tells the model that new rows will be inserted and have row numbers 2-4 (2, 3, 4).

    So appending rows would look like self.beginInsertRows(parent, self.rowCount(parent), self.rowCount(parent)). Of course if the parent is the root index, then parent just is QModelIndex()

    After correctly calling self.beginInsertRows, we then make the change to the underlying data. In your case, you are going to insert probably blank rows into your dataframe. Then you call self.endInsertRows() with no arguments.

        def insertRows(self, row: int, count: int, parent: QModelIndex = QModelIndex()) -> bool:
            if self.beginInsertRows(parent, row, row+count-1):
                for i in range(count):
                    new_index = f'New Index{i}'
                    self.table_index.loc[new_index] = ['<empty>']*self.columnCount(parent)
                return self.endInsertRows()
            else:
                return False
    

    You will need other code that accomplishes what you actually want when inserting a row or just input the data in the GUI.