0

I'm using a custom Pandas Model(QAbstractTableModel) which is editable. I'm trying to add rows and then the user should be able to edit them and pass the values to the dataframe. Unfortunately, the example I found from PyQt5: Implement removeRows for pandas table model doesn't work when I add rows. This is my model class:

class pandasModel(QAbstractTableModel):
     def __init__(self, dataframe: pd.DataFrame, parent=None):
         QAbstractTableModel.__init__(self, parent)
         self._dataframe = dataframe
        
     def rowCount(self, parent=QModelIndex()) -> int:
         """ Override method from QAbstractTableModel

         Return row count of the pandas DataFrame
         """
         if parent == QModelIndex():
             return len(self._dataframe)

         return 0

     def columnCount(self, parent=QModelIndex()) -> int:
         """Override method from QAbstractTableModel

         Return column count of the pandas DataFrame
         """
         if parent == QModelIndex():
             return len(self._dataframe.columns)
         return 0

     def data(self, index: QModelIndex, role=Qt.ItemDataRole):
         """Override method from QAbstractTableModel

         Return data cell from the pandas DataFrame
         """
         if not index.isValid():
             return None
         
         if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                try:
                    value = self._dataframe.iloc[index.row(), index.column()]
                    return str(value)
                    value=float(value)
                except ValueError:                
                    msg = QMessageBox()
                    msg.setIcon(QMessageBox.Critical)
                    msg.setText("Please, use integers or decimals.")
                    msg.setWindowTitle("Error")
                    msg.setStandardButtons(QMessageBox.Ok)
                    msg.exec_()
                    return 0 

     def headerData(
         self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole):
         """Override method from QAbstractTableModel

         Return dataframe index as vertical header data and columns as horizontal header data.
         """
         if role == Qt.DisplayRole:
             if orientation == Qt.Horizontal:
                 return str(self._dataframe.columns[section])
             if orientation == Qt.Vertical:
                 return str(self._dataframe.index[section])
         return None
     
     def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._dataframe.iloc[index.row(), index.column()] = value
            return True
        return False
    
     def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
    
     def removeRows(self, position, rows, parent=QModelIndex()):
        start, end = position, position + rows - 1
        if 0 <= start <= end and end < self.rowCount(parent):
            self.beginRemoveRows(parent, start, end)
            for index in range(start, end + 1):
                self._dataframe.drop(index, inplace=True)
            self._dataframe.reset_index(drop=True, inplace=True)
            self.endRemoveRows()
            return True
        return False
    
     def insertRows(self, position, rows, parent=QModelIndex()):
         start, end = position, position + rows - 1
         if 0 <= start <= end:
             self.beginInsertRows(parent, start, end)
             for index in range(start, end + 1):
                 default_row = [[None] for _ in range(self._dataframe.shape[1])]
                 new_df = pd.DataFrame(dict(zip(list(self._dataframe.columns), default_row)))
                 self._dataframe = pd.concat([self._dataframe, new_df])
             self._dataframe = self._dataframe.reset_index(drop=True)
             self.endInsertRows()
             return True
         return False

My global dataframe to be used:

regionalPop = {'Region':[1,1],'Starting_Index': [0,0], 'Ending_Index': [0,0]}
dfRegion=pd.DataFrame(data=regionalPop)

And the main class:

class regionalPlot(QtWidgets.QMainWindow,Ui_regionalWindow):
def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.setupUi(self)
    
    #populating REGIONS table model
    global dfRegion
    regionalPop = {'Region':[1,1],'Starting_Index': [0,0], 'Ending_Index': [0,0]}
    dfRegion=pd.DataFrame(data=regionalPop)
    self.model1 = pandasModel(dfRegion)
    #sorting enable
    self.proxyModel = QSortFilterProxyModel()
    self.proxyModel.setSourceModel(self.model1)
    #binding model to table
    self.tableView_2.setModel(self.proxyModel)
    #-------------------------------------

    #Button Actions
    #add_button pressed
    self.actionAdd_Region.triggered.connect(lambda: self.insert_row())
    #minus button pressed
    self.actionDelete_Region.triggered.connect(lambda: self.delete_row())
    #update pressed
    self.actionUpdate_Tables.triggered.connect(lambda: self.updateTable())
#ACTIONS
def delete_row(self):
    if self.tableView_2.selectionModel().hasSelection():
        indexes =[QPersistentModelIndex(index) for index in self.tableView_2.selectionModel().selectedRows()]
        for index in indexes:
            print('Deleting row %d...' % index.row())
            self.model1.removeRow(index.row())

def insert_row(self):
    global dfRegion
    self.model1.insertRows(self.model1.rowCount(), 1)
    print (dfRegion)

def updateTable(self):
     #populating table model
     self.model1 = pandasModel(dfRegion)
     #sorting enable
     self.proxyModel = QSortFilterProxyModel()
     self.proxyModel.setSourceModel(self.model1)
     #binding model to table
     self.tableView_2.setModel(self.proxyModel)

And the Ui_regionalWindow:

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_regionalWindow(object):
    def setupUi(self, regionalWindow):
        regionalWindow.setObjectName("regionalWindow")
        regionalWindow.resize(1184, 996)
        self.centralwidget = QtWidgets.QWidget(regionalWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.tableView_2 = QtWidgets.QTableView(self.centralwidget)
        self.tableView_2.setGeometry(QtCore.QRect(10, 90, 256, 831))
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.tableView_2.sizePolicy().hasHeightForWidth())
        self.tableView_2.setSizePolicy(sizePolicy)
        self.tableView_2.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
        self.tableView_2.setAlternatingRowColors(True)
        self.tableView_2.setObjectName("tableView_2")
        self.label_2 = QtWidgets.QLabel(self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(10, 0, 241, 112))
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth())
        self.label_2.setSizePolicy(sizePolicy)
        font = QtGui.QFont()
        font.setPointSize(14)
        self.label_2.setFont(font)
        self.label_2.setObjectName("label_2")
        regionalWindow.setCentralWidget(self.centralwidget)
        self.toolBar = QtWidgets.QToolBar(regionalWindow)
        self.toolBar.setIconSize(QtCore.QSize(40, 40))
        self.toolBar.setObjectName("toolBar")
        regionalWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar)
        self.actionAdd_Region = QtWidgets.QAction(regionalWindow)
        self.actionAdd_Region.setObjectName("actionAdd_Region")
        self.actionDelete_Region = QtWidgets.QAction(regionalWindow)
        self.actionDelete_Region.setObjectName("actionDelete_Region")
        self.actionUpdate_Tables = QtWidgets.QAction(regionalWindow)
        self.actionUpdate_Tables.setObjectName("actionUpdate_Tables")
        self.toolBar.addAction(self.actionAdd_Region)
        self.toolBar.addAction(self.actionDelete_Region)
        self.toolBar.addSeparator()
        self.toolBar.addAction(self.actionUpdate_Tables)
        self.toolBar.addSeparator()
        self.toolBar.addSeparator()

        self.retranslateUi(regionalWindow)
        QtCore.QMetaObject.connectSlotsByName(regionalWindow)

    def retranslateUi(self, regionalWindow):
        _translate = QtCore.QCoreApplication.translate
        regionalWindow.setWindowTitle(_translate("regionalWindow", "Regional Analysis"))
        self.label_2.setText(_translate("regionalWindow", "Regions"))
        self.toolBar.setWindowTitle(_translate("regionalWindow", "toolBar"))
        self.actionAdd_Region.setText(_translate("regionalWindow", "Add Region"))
        self.actionAdd_Region.setToolTip(_translate("regionalWindow", "Add region of the signal. Left field is the start and right field is the end of the region of the signal."))
        self.actionDelete_Region.setText(_translate("regionalWindow", "Delete Region"))
        self.actionDelete_Region.setToolTip(_translate("regionalWindow", "Delete an existing region of the signal."))
        self.actionUpdate_Tables.setText(_translate("regionalWindow", "Update Tables"))
        self.actionUpdate_Tables.setToolTip(_translate("regionalWindow", "Click to update changes to the tabular data"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    regionalWindow = QtWidgets.QMainWindow()
    ui = Ui_regionalWindow()
    ui.setupUi(regionalWindow)
    regionalWindow.show()
    sys.exit(app.exec_())

I'm using three buttons one to add rows, one to remove rows, and one to update the table. Unfortunately, when I add rows to the dataframe it doesn't change anything and it keeps the same rows as defined before the editing, but when I trigger editing the values are changed.

To summarise: I need to add rows to the dataframe "dfRegion" with this model. Even though I see a visual change in the GUI when I click self.actionAdd_Region, I print the dfRegion and there are no rows added in the dataframe.

fotiskt
  • 1
  • 2
  • What do you mean by "when I add rows to the dataframe it doesn't change anything"? What is "it"? Are you referring to the global `dfRegion`? Then it's because `concat()` does **not** change the dataframe, but creates a new one. While you could overwrite the global dataframe, you should *not* use globals (they can create confusion about their scope, which is exactly your problem); instead, you should just overwrite the `self._dataframe` *after* adding all new data. – musicamante Oct 13 '22 at 09:05
  • Note that you can only ask *one* question per post. In any case: for point 2, just return the `value` (no `str()`) when the role is `EditRole`, so that the user can only type numbers; 3. get the `index()` of each item in the new row, then use `setData()`. Finally, always try to provide a [mre], as your code has things that are completely irrelevant for the question (colors) while it's missing relevant parts that make us difficult to reproduce it (the UI). – musicamante Oct 13 '22 at 09:08
  • @musicamante sorry if I wasn't clear. I meant when I call the insert_row function by clicking actionAdd_Region I see a visual change in the GUI but when I print the dataframe no rows have been added to it. I tried global cause it was my last resort to overwrite the dataframe. – fotiskt Oct 13 '22 at 09:12
  • Then what said above is exactly the cause of your problem: you're just overwriting the model's dataframe, not the global one. Instead of trying to access the global object, just get the model's one: `self.model1._dataframe`. – musicamante Oct 13 '22 at 09:15
  • @musicamante noted and thanks for the support gonna keep your suggestions about 2 and 3 and I'm going to delete them from the original so the question can be focused on the insert_row problem. I've edited out the colors (I missed these in the first place) and I did not provide the UI cause it has many more elements in the main window than those affecting the question I'm going to reproduce it and edit it later. Can you please be more specific about the no1. problem. Thanks again for the support. – fotiskt Oct 13 '22 at 09:45
  • That's exactly why you need to provide a [mre]: create a *dedicated* content based on your question. Create a separate script that only includes the parts necessary for the question, if you need an UI, create a *different* ui file and use that in the example (but in your case it's unnecessary, as you're using just a few widgets, so create the UI by code). I already answered about point 1: you're trying to access a global object `dfRegion`, but you're using *another* df in the model every time you update it; instead of accessing the global, access the `self.model1._dataframe`. – musicamante Oct 13 '22 at 10:50
  • @musicamante Fixed. Thank you so much, I've been battling with it for hours. – fotiskt Oct 13 '22 at 10:59

0 Answers0