2

I use QTableView to display and edit a Pandas DataFrame. I use this method in the TableModel class to remove rows:

  def removeRows(self, position, rows, QModelIndex):
        start, end = position, rows 
        self.beginRemoveRows(QModelIndex, start, end) #
        self._data.drop(position,inplace=True)
        self._data.reset_index(drop=True,inplace=True)
        self.endRemoveRows() #
        self.layoutChanged.emit()
        return True

It works fine until I add comboBox to some cells on the TableView. I use the following codes to add combobox (in the Main class), but when I delete a row it shows the error message (Python 3.10, Pandas 1.4.1): IndexError: index 2 is out of bounds for axis 0 with size 2 or (Python 3.9, Pandas 1.3.5) : 'IndexError: single positional indexer is out-of-bounds'

        count=len(combo_type)
        for type in combo_type:
            for row_num in range(self.model._data.shape[0]):
                # print(i)
                combo = CheckableComboBox(dept_list,self.model._data,row_num,type,count)
                self.tableView.setIndexWidget(self.model.index(row_num, self.model._data.shape[1] - 2*count), combo)
            count=count-1

But if I comment out the two lines: self.beginRemoveRows(QModelIndex, start, end) and self.endRemoveRows() from removeRows method, it works and there are no more error messages. But according to the Qt documents, these two methods must be called.

A removeRows() implementation must call beginRemoveRows() before the rows are removed from the data structure, and it must call endRemoveRows() immediately afterwards.

 def removeRows(self, position, rows, QModelIndex):
                start, end = position, rows 
                #self.beginRemoveRows(QModelIndex, start, end) # remove
                self._data.drop(position,inplace=True)
                self._data.reset_index(drop=True,inplace=True)
                #self.endRemoveRows() # remove
                self.layoutChanged.emit()
                return True

I have tried for hours, but I cannot figure this out. Can anyone help me and explain what is wrong with my code, please?

This is my class for Table Model:

from PyQt5 import QtCore
from PyQt5.QtCore import Qt
from datetime import datetime
import pandas as pd


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.DisplayRole or role == Qt.EditRole:
            # See below for the nested-list data structure.
            # .row() indexes into the outer list,
            # .column() indexes into the sub-list
            print(index.row(), index.column())
            value = self._data.iloc[index.row(), index.column()]
            
            # Perform per-type checks and render accordingly.
            if isinstance(value, datetime):
            # Render time to YYY-MM-DD.
                if pd.isnull(value):
                    value=datetime.min
                return value.strftime("%Y-%m-%d")

            if isinstance(value, float):
            # Render float to 2 dp
                return "%.2f" % value

            if isinstance(value, str):
            # Render strings with quotes
                # return '"%s"' % value
                return value

            # Default (anything not captured above: e.g. int)
            return value

    # implement rowCount
    def rowCount(self, index):
        # The length of the outer list.
        return self._data.shape[0]
    
    # implement columnCount
    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return self._data.shape[1]
    
    # implement flags
    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

    # implement setData
    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data.iloc[index.row(), index.column()] = value
            # self._data.iat[index.row(), self._data.shape[1]-1] = value
            self.dataChanged.emit(index, index)
            return True

    def headerData(self, section, orientation, role):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self._data.columns[section])
            if orientation == Qt.Vertical:
                return str(self._data.index[section])
            
    
    def insertRows(self, position, rows, QModelIndex, parent):
        self.beginInsertRows(QModelIndex, position, position+rows-1)
        default_row=[[None] for _ in range(self._data.shape[1])]
        new_df=pd.DataFrame(dict(zip(list(self._data.columns),default_row)))
        self._data=pd.concat([self._data,new_df])
        self._data=self._data.reset_index(drop=True)
        self.endInsertRows()
        self.layoutChanged.emit()
        return True

    def removeRows(self, position, rows, QModelIndex):
        start, end = position, rows 
        self.beginRemoveRows(QModelIndex, start, end) # if remove these 02 lines, it works
        self._data.drop(position,inplace=True)
        self._data.reset_index(drop=True,inplace=True)
        self.endRemoveRows() # if remove these 02 lines, it works
        self.layoutChanged.emit()
        return True

Class for checkable combobox:

from PyQt5.QtWidgets import  QComboBox
from PyQt5.QtCore import Qt
import CONSTANT

class CheckableComboBox(QComboBox):
    def __init__(self,item_list, df,number,type,col_offset_value):
        super().__init__()
        self._changed = False
        self.view().pressed.connect(self.handleItemPressed)
        self.view().pressed.connect(self.set_df_value)
        # Store checked item
        self.checked_item=[]
        self.checked_item_index=[]
        self.type=type
        self.col_offset_value=col_offset_value
        
        # DataFrame to be modified
        self.df=df
        # Order number of the combobox
        self.number=number
        
        for i in range(len(item_list)):
            self.addItem(item_list[i])
            self.setItemChecked(i, False)
        # self.activated.connect(self.set_df_value)
            
    def set_df_value(self):
        print(self.number)
        self.df.iat[self.number,self.df.shape[1]-self.col_offset_value*2+1]=','.join(self.checked_item)
        print(self.df)

    def setItemChecked(self, index, checked=False):
        item = self.model().item(index, self.modelColumn())  # QStandardItem object

        if checked:
            item.setCheckState(Qt.Checked)

        else:
            item.setCheckState(Qt.Unchecked)
            
    def set_item_checked_from_list(self,checked_item_index_list):
        for i in range(self.count()):
            item = self.model().item(i, 0)
            if i in checked_item_index_list:
                item.setCheckState(Qt.Checked)
            else:
                item.setCheckState(Qt.Unchecked)
                
    
    def get_item_checked_from_list(self,checked_item_index_list):
        self.checked_item.clear()
        self.checked_item.extend(checked_item_index_list)
    
            
    def handleItemPressed(self, index):
        item = self.model().itemFromIndex(index)

        if item.checkState() == Qt.Checked:
            item.setCheckState(Qt.Unchecked)
            if item.text() in self.checked_item:
                self.checked_item.remove(item.text())
                self.checked_item_index.remove(index.row())
            print(self.checked_item)
            print(self.checked_item_index)

        else:
            
            if item.text()!=CONSTANT.ALL \
                and CONSTANT.ALL not in self.checked_item \
                and item.text()!=CONSTANT.GWP \
                and CONSTANT.GWP not in self.checked_item \
                and item.text()!=CONSTANT.NO_ALLOCATION \
                and CONSTANT.NO_ALLOCATION not in self.checked_item :
                item.setCheckState(Qt.Checked)
                self.checked_item.append(item.text())
                self.checked_item_index.append(index.row())
                print(self.checked_item)
                print(self.checked_item_index)

            else:
                self.checked_item.clear()
                self.checked_item_index.clear()
                self.checked_item.append(item.text())
                self.checked_item_index.append(index.row())
                self.set_item_checked_from_list(self.checked_item_index)


        self._changed = True
        
        self.check_items()

    def hidePopup(self):
        if not self._changed:
            super().hidePopup()
        self._changed = False


    def item_checked(self, index):
        # getting item at index
        item = self.model().item(index, 0)
        # return true if checked else false
        return item.checkState() == Qt.Checked
    
    def check_items(self):
        # traversing the items
        checkedItems=[]
        for i in range(self.count()):
            # if item is checked add it to the list
            if self.item_checked(i):
                checkedItems.append(self.model().item(i, 0).text())

Main class:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt,QDate,QThread
from net_comm_ui import Ui_MainWindow
from PyQt5.QtWidgets import QApplication, QMainWindow
from pathlib import Path
import multiprocessing
from  TableModel import TableModel
from CheckableComboBox import CheckableComboBox
import copy
import datetime
import re
import json
from pathlib import Path
import pandas as pd
import os
from net_comm_worker import Worker
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from PyQt5.QtCore import pyqtSlot

dept_list = ['A','B','C','D','E','F','G','H']
combo_type=['METHOD','LOB','DEPT','CHANNEL']


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.tableView = QtWidgets.QTableView()
        import pandas as pd
    
        
        mydict = [{'a': 1, 'b': 2, 'c': 3, 'd': 4},
          {'a': 100, 'b': 200, 'c': 300, 'd': 400},
          {'a': 1000, 'b': 2000, 'c': 3000, 'd': 4000 }]
        
        self.data=pd.DataFrame(mydict)
        
        print('initial self.data')
        print(self.data)
        
        self.data['Allocation Method'] = ''
        self.data['Allocation Method Selected']=''
        self.data['Allocation LOB'] = ''
        self.data['Allocation LOB Selected']=''
        self.data['Allocation DEPT'] = ''
        self.data['Allocation DEPT Selected']=''
        self.data['Allocation CHANNEL'] = ''
        self.data['Allocation CHANNEL Selected']=''
        

        self.model = TableModel(self.data)
        self.tableView.setModel(self.model)
        self.setCentralWidget(self.tableView)
        self.setGeometry(600, 200, 500, 300)
        
        count=len(combo_type)
        # Set ComboBox to cells
        for type in combo_type:
            for row_num in range(self.model._data.shape[0]):
                # print(i)
                combo = CheckableComboBox(dept_list,self.model._data,row_num,type,count)
                self.tableView.setIndexWidget(self.model.index(row_num, self.model._data.shape[1] - 2*count), combo)
            count=count-1
        
        button = QPushButton('Delete row', self)
        button.move(100,200)
        button.clicked.connect(self.delete_row)
        
 
    def delete_row(self):
        index = self.tableView.currentIndex()
        if index.row()<self.model._data.shape[0]:
            self.model.removeRows(index.row(), 1, index)
            print('self.model._data')
            print(self.model._data)
            print('self.data')
            print(self.data)



app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

I add one method to add row. Is the self.layoutChanged.emit() is mandatory to update TableView or there is a more efficient way?:

def insertRows(self, position, rows, QModelIndex, parent):
    self.beginInsertRows(QModelIndex, position, position+rows-1)
    default_row=[[None] for _ in range(self._data.shape[1])]
    new_df=pd.DataFrame(dict(zip(list(self._data.columns),default_row)))
    self._data=pd.concat([self._data,new_df])
    self._data.reset_index(drop=True, inplace=True)
    self.endInsertRows()
    self.layoutChanged.emit() # ==> is this mandatory?
    return True
Dinh Quang Tuan
  • 468
  • 3
  • 10

1 Answers1

2

Your example passes the wrong index to removeRows, which also does not calculate the start and end values correctly. It can be fixed like this:

class MainWindow(QtWidgets.QMainWindow):
    ...
    def delete_row(self):
        index = self.tableView.currentIndex()
        self.model.removeRows(index.row(), 1)

    def insert_row(self):
        self.model.insertRows(self.model.rowCount(), 1)
class TableModel(QtCore.QAbstractTableModel):
    ...
    def rowCount(self, parent=QModelIndex()):
        ...

    def columnCount(self, parent=QModelIndex()):
        ...

    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._data.shape[1])]
                new_df = pd.DataFrame(dict(zip(list(self._data.columns), default_row)))
                self._data = pd.concat([self._data, new_df])
            self._data = self._data.reset_index(drop=True)
            self.endInsertRows()
            return True
        return False

    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._data.drop(index, inplace=True)
            self._data.reset_index(drop=True, inplace=True)
            self.endRemoveRows()
            return True
        return False
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • 1
    Shouldn't `layoutChanged` be preceded by `layoutAboutToBeChanged`? And is it actually necessary for this? Isn't `begin/endRemoveRows` sufficient? – musicamante Mar 06 '22 at 00:55
  • 1
    @musicamante I just copied it from the OPs code. It's not really relevant to the question, so I will just remove it. – ekhumoro Mar 06 '22 at 01:13
  • Thank you. I will try and report on results. But what will happen if I don't call self.beginRemoveRows(QModelIndex, start, end) and self.endRemoveRows() ??? – Dinh Quang Tuan Mar 06 '22 at 01:31
  • 1
    @DinhQuangTuan the model (and its views) need to know that it's been updated and rows have been removed, and it's important that both begin and end functions are called so that indexes are properly updated and views can correctly adjust their selections accordingly. So, it's **very** important that you call those functions. Read [the related documentation](https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows) and also the [model/view reference](https://doc.qt.io/qt-5/model-view-programming.html) – musicamante Mar 06 '22 at 01:37
  • 1
    @musicamante Ironically, that `layoutChanged` signal in the original example was updating the view as expected when `beginRemoveRows` and `endRemoveRows` were commented out (which the OP already discovered). Of course, it is much more inefficient to do it that way, since it operates on the entire view. – ekhumoro Mar 06 '22 at 02:31
  • @ekhumoro Thank for your help, it works fine. So if I call beginRemoveRows and endRemoveRows, the layoutChanged is not necessary. – Dinh Quang Tuan Mar 06 '22 at 09:09
  • @ekhumoro I have just add insertRows method at the end of my post to add a new row at the end of the tableView but I have to use self.layoutChanged.emit() . As per your above answer, it is very inefficient so I am wondering if there is a more efficient way? – Dinh Quang Tuan Mar 06 '22 at 09:40
  • 1
    @DinhQuangTuan You don't need to use `layoutChanged`. I have expanded my example code to include `insertRows` as well. Also note that I have changed how the `parent` argument is implemented. For a table, the parent will always be an invalid index, so you can simply use a hard-coded default for more convenience. – ekhumoro Mar 06 '22 at 17:01
  • @ekhumoro : def insert_row(self): index = self.tableView.currentIndex() self.model.insertRows(self.model.rowCount(index ), 1) ==> insertRows needs index. Thank you so much for your kind support. – Dinh Quang Tuan Mar 07 '22 at 03:30
  • @DinhQuangTuan The index isn't needed if a default value is given for the `parent` argument in all the methods (see the `TableModel` class in my answer). – ekhumoro Mar 07 '22 at 16:37