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_()
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())