pythonpyside2qtableviewqsortfilterproxymodelqitemdelegate

Qt TableView+Delegate+ProxyIndex+PersistenIndex - deleting rows with widgets


I'm trying to figure out how to code table with widgets with sorting/filtering possibility. I use QItemDelegate, QAbstractTableModel, QTableView.

Already checked many threads on this topic which led me to code part of my app.

What I want to understand is how to properly delete rows to keep all these indexes and data coherent. At first I though I got it right, but playing with sorting and deleting more then few times shows very strange behavior for the column with button delete widget.

For example if I keep clicking button in the same table row (eg 5) these delete buttons simply shifts upword while first 3 column are updated correctly. Also some delete button are being duplicated.

Whole code:

import sys
from PySide2.QtWidgets import QApplication

app = QApplication( )


from PySide2.QtWidgets import QApplication, QLabel, QMainWindow, QWidget, QHBoxLayout ,QVBoxLayout,QGridLayout,QStackedLayout, QTableView 
from PySide2.QtCore import Qt

# QHBoxLayout   Linear horizontal layout
# QVBoxLayout   Linear vertical layout
# QGridLayout   In indexable grid XxY
# QStackedLayout
def get_layout(ll):

    if ll not in ['grid','hbox','vbox','stack']:
        # print(ll)
        # print('wrong layout name')
        exit()
        
    ret_layout = QGridLayout()
    
    if ll=='hbox':
        ret_layout = QHBoxLayout()
    elif ll=='vbox':
        ret_layout = QVBoxLayout()
    elif ll=='stack':
        ret_layout = QStackedLayout()

    return ret_layout
    
    
    
    

class MainWindow(QMainWindow):
 
    def __init__(self, win_title='Default title', win_layout='grid',main_widget = QWidget()):
    
        super(MainWindow, self).__init__()

        self.setWindowTitle(win_title)
        self.layout_name=win_layout
        self.main_layout=get_layout(win_layout)
        self.setGeometry(100,100,1000,800)
         
        main_widget.setLayout(self.main_layout)
         
        self.setCentralWidget(main_widget)
 
    def addFrame(self,frame):
    
        if self.layout_name!='grid':
        
            if type(frame)==type([]):
                self.main_layout.addWidget(frame[-1])
            else:
                self.main_layout.addWidget(frame)
                
        else:
            self.main_layout.addWidget(frame[2],frame[0],frame[1])

            


import sys
import json
import PySide2
from PySide2 import QtCore, QtGui, QtWidgets 
from PySide2.QtCore import Qt #, QFile, QIODevice


class ExampleWidget(QtWidgets.QWidget):
    def __init__(self, x, index, parent=None):
        super(ExampleWidget, self).__init__(parent)
        self.orig_index=index
        self.p_index = QtCore.QPersistentModelIndex(index)
        
        self.content_button = QtWidgets.QWidget(self)
        lay = QtWidgets.QHBoxLayout(self.content_button)
        lay.setContentsMargins(0, 0, 0,0)
        
        self.delete_btn = QtWidgets.QPushButton("delete "+str(index.row()))
        
        self.delete_btn.clicked.connect(self.delete_clicked)
        lay.addWidget(self.delete_btn)
        self.content_button.move(x, 0)
        
    @QtCore.Slot()
    def delete_clicked(self):

        model = self.p_index.model() # QSortFilterProxyModel
        src_model=model.sourceModel()
        src_index=model.mapToSource(self.orig_index)
        rows_to_del=1
        
        zxc=src_model.removeRows(src_index.row(),rows_to_del,src_index, self.p_index )
        # self.deleteLater()
        # self.QtWidgets.~QWidget()
        
        

        
class CustomDelegate(QtWidgets.QItemDelegate): #QStyledItemDelegate

    def __init__(self, parent=None):
        super(CustomDelegate, self).__init__(parent)
        
    def paint(self, painter, option, index):
        self.parent().openPersistentEditor(index)
        super(CustomDelegate, self).paint(painter, option, index)
        
    def createEditor(self, parent, option, index):
        
        if index.column()==3:
            return ExampleWidget(300, index, parent)
        
            

            
class TableModel(QtCore.QAbstractTableModel ): 

    def __init__(self, table=[], col_headers=[], row_headers=[], bg_color=None ):
        super(TableModel, self).__init__()
        
        self.table =  table 
        self.col_headers=col_headers  
        self.row_headers=row_headers  
        self.bg_color=bg_color or QtGui.QColor('lightgrey')
        
    
    def itemFromIndex(self,index):
        if not index.isValid():
            return None
    
        rr=index.row()
        cc=index.column()
        return str ( self.table[rr][cc] ) 
        
        
    def removeRows(self,row, count, index, p_index):
        self.layoutAboutToBeChanged.emit()
        self.beginRemoveRows(index,row,row)
        del self.table[row]
        self.endRemoveRows()
        self.layoutChanged.emit()
        
        return True
        
        
    def data(self, index, role ):
    
        if not index.isValid():
            return None
    
        rr=index.row()
        cc=index.column()
        
        if role == Qt.DisplayRole:
            vvalue = str ( self.table[rr][cc] ) 
            return vvalue #QtWidgets.QPushButton
        
        if role == Qt.BackgroundRole:
            return self.bg_color
                
    def rowCount(self, index):
        return len(self.table)
        # return 5
        
    def columnCount(self, index):
        if len(self.table)>0:
            return len(self.table[0])
        return 0
        
        
    def headerData(self, section, orientation, role):
    
        if section<0:
            return
    
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                if len(self.col_headers)>section:
                    return str(self.col_headers[section])

            if orientation == Qt.Vertical:
                if len(self.row_headers)>section:
                    return str(self.row_headers[section])
        
        
        
        
class TableView( QtWidgets.QTableView):

    def __init__(self,params={}):
        super(TableView, self).__init__( )
        
        self.model = TableModel()
        
        self.sorting_proxy = QtCore.QSortFilterProxyModel()
        self.sorting_proxy.setSourceModel( self.model)
        self.setModel(self.sorting_proxy)       
        
        self.delegate=CustomDelegate(self)
        self.setItemDelegate(self.delegate)
        
        self.setSortingEnabled(True)
        self.setCornerButtonEnabled(False)
        self.horizontalHeader().setStretchLastSection(True)
        self.clicked.connect(self.onClick)
        self.verticalHeader().hide()
        
        tmp_header_bg_color='lightgrey'
        if 'header_bg_color' in params:
            tmp_header_bg_color=params['header_bg_color']
            
        tmp_style="""QWidget  { background-color:%s; border:none; }
                    QHeaderView::section { background-color:%s; border:none; }
                    QTableCornerButton::section { background-color:%s; border:none; }
                    """ % (tmp_header_bg_color,tmp_header_bg_color,tmp_header_bg_color)
            
        self.setStyleSheet(tmp_style)
        
        if len(params)>0:
            if 'show_grid' in params:
                self.setShowGrid(params['show_grid'])
            if 'auto_resize':
                self.auto_resize=True
            

            
    @QtCore.Slot(QtCore.QModelIndex)
    def onClick(self, ix):
        # it = self.model.itemFromIndex(ix)
        it=self.sorting_proxy.data(ix)
        if hasattr(it,'data'):
            print(it.data())            
        else:
            print(it)
                
            
    def sortByCol(self,cc,dir='asc'):
        if dir=='asc':
            self.sortByColumn(cc, Qt.AscendingOrder)
        else:
            self.sortByColumn(cc, Qt.DescendingOrder)
            
            
            
            
            
    def setdata(self,data_list,col_headers=[],row_headers=[]):

        self.model.table=data_list
        self.model.col_headers=col_headers
        self.model.row_headers=row_headers
        
        self.model.layoutChanged.emit()
        
        if hasattr(self,'auto_resize'):
            self.resizeColumnsToContents()
            self.resizeRowsToContents()

            

            

data_list = [
        ('ACETIC ACID', 117.9, 16.7, 1.049),
        ('ACETIC ANHYDRIDE', 140.1, -73.1, 1.087),
        ('ACETONE', 56.3, -94.7, 0.791),
        ('ACETONITRILE', 81.6, -43.8, 0.786),
        ('ANISOLE', 154.2, -37.0, 0.995),
        ('BENZYL ALCOHOL', 205.4, -15.3, 1.045),
        ('BENZYL BENZOATE', 323.5, 19.4, 1.112),
        ('BUTYL ALCOHOL NORMAL', 117.7, -88.6, 0.81),
        ('BUTYL ALCOHOL SEC', 99.6, -114.7, 0.805),
        ('BUTYL ALCOHOL TERTIARY', 82.2, 25.5, 0.786),
        ('CHLOROBENZENE', 131.7, -45.6, 1.111),
        ('CYCLOHEXANE', 80.7, 6.6, 0.779),
        ('CYCLOHEXANOL', 161.1, 25.1, 0.971),
        ('CYCLOHEXANONE', 155.2, -47.0, 0.947),
        ('DICHLOROETHANE 1 2', 83.5, -35.7, 1.246),
        ('DICHLOROMETHANE', 39.8, -95.1, 1.325),
        ('DIETHYL ETHER', 34.5, -116.2, 0.715),
        ('DIMETHYLACETAMIDE', 166.1, -20.0, 0.937),
        ('DIMETHYLFORMAMIDE', 153.3, -60.4, 0.944),
        ('DIMETHYLSULFOXIDE', 189.4, 18.5, 1.102),
        ('DIOXANE 1 4', 101.3, 11.8, 1.034),
        ('DIPHENYL ETHER', 258.3, 26.9, 1.066),
        ('ETHYL ACETATE', 77.1, -83.9, 0.902),
        ('ETHYL ALCOHOL', 78.3, -114.1, 0.789),
        ('ETHYL DIGLYME', 188.2, -45.0, 0.906),
        ('ETHYLENE CARBONATE', 248.3, 36.4, 1.321),
        ('ETHYLENE GLYCOL', 197.3, -13.2, 1.114),
        ('FORMIC ACID', 100.6, 8.3, 1.22),
        ('HEPTANE', 98.4, -90.6, 0.684),
        ('HEXAMETHYL PHOSPHORAMIDE', 233.2, 7.2, 1.027),
        ('HEXANE', 68.7, -95.3, 0.659),
        ('ISO OCTANE', 99.2, -107.4, 0.692),
        ('ISOPROPYL ACETATE', 88.6, -73.4, 0.872),
        ('ISOPROPYL ALCOHOL', 82.3, -88.0, 0.785),
        ('METHYL ALCOHOL', 64.7, -97.7, 0.791),
        ('METHYL ETHYLKETONE', 79.6, -86.7, 0.805),
        ('METHYL ISOBUTYL KETONE', 116.5, -84.0, 0.798),
        ('METHYL T-BUTYL ETHER', 55.5, -10.0, 0.74),
        ('METHYLPYRROLIDINONE N', 203.2, -23.5, 1.027),
        ('MORPHOLINE', 128.9, -3.1, 1.0),
        ('NITROBENZENE', 210.8, 5.7, 1.208),
        ('NITROMETHANE', 101.2, -28.5, 1.131),
        ('PENTANE', 36.1, ' -129.7', 0.626),
        ('PHENOL', 181.8, 40.9, 1.066),
        ('PROPANENITRILE', 97.1, -92.8, 0.782),
        ('PROPIONIC ACID', 141.1, -20.7, 0.993),
        ('PROPIONITRILE', 97.4, -92.8, 0.782),
        ('PROPYLENE GLYCOL', 187.6, -60.1, 1.04),
        ('PYRIDINE', 115.4, -41.6, 0.978),
        ('SULFOLANE', 287.3, 28.5, 1.262),
        ('TETRAHYDROFURAN', 66.2, -108.5, 0.887),
        ('TOLUENE', 110.6, -94.9, 0.867),
        ('TRIETHYL PHOSPHATE', 215.4, -56.4, 1.072),
        ('TRIETHYLAMINE', 89.5, -114.7, 0.726),
        ('TRIFLUOROACETIC ACID', 71.8, -15.3, 1.489),
        ('WATER', 100.0, 0.0, 1.0),
        ('XYLENES', 139.1, -47.8, 0.86)
        ]

tmpheaders=['a','b','c']

mw = MainWindow(win_layout='hbox' )

ff=TableView(params={'show_grid':False,'auto_resize':1 })
mw.addFrame(ff)
ff.setdata(data_list,tmpheaders)

mw.show()

app.exec_()

Solution

  • You must create a method that removes a row:

    class TableModel(QAbstractTableModel):
        # ...
        def removeRow(self, row):
            self.beginRemoveRows(QModelIndex(), row, row)
            del self.table[row]
            self.endRemoveRows()

    And then map the row in the sourceModel:

    class ExampleWidget(QWidget):
        # ...
    
        @Slot()
        def delete_clicked(self):
            index = QModelIndex(self.p_index)
            model = index.model()
            source_index = model.mapToSource(index)
            source_model = source_index.model()
            source_model.removeRow(source_index.row())