Qt TableView+Delegate+ProxyIndex+PersistenIndex - 使用小部件删除行
Qt TableView+Delegate+ProxyIndex+PersistenIndex - deleting rows with widgets
我正在尝试弄清楚如何使用具有 sorting/filtering 可能性的小部件编写 table 代码。
我使用 QItemDelegate、QAbstractTableModel、QTableView。
已经检查了很多关于这个主题的话题,这让我编写了我的应用程序的一部分。
我想了解的是如何正确删除行以保持所有这些索引和数据的一致性。起初我虽然我做对了,但是多次尝试排序和删除显示带有按钮删除小部件的列的行为非常奇怪。
例如,如果我一直点击同一 table 行(例如第 5 行)中的按钮,这些删除按钮只会向上移动单词,而前 3 列会正确更新。还有一些删除按钮被复制了。
完整代码:
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_()
您必须创建一个删除行的方法:
class TableModel(QAbstractTableModel):
# ...
<b>def removeRow(self, row):
self.beginRemoveRows(QModelIndex(), row, row)
del self.table[row]
self.endRemoveRows()</b>
然后映射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())
我正在尝试弄清楚如何使用具有 sorting/filtering 可能性的小部件编写 table 代码。 我使用 QItemDelegate、QAbstractTableModel、QTableView。
已经检查了很多关于这个主题的话题,这让我编写了我的应用程序的一部分。
我想了解的是如何正确删除行以保持所有这些索引和数据的一致性。起初我虽然我做对了,但是多次尝试排序和删除显示带有按钮删除小部件的列的行为非常奇怪。
例如,如果我一直点击同一 table 行(例如第 5 行)中的按钮,这些删除按钮只会向上移动单词,而前 3 列会正确更新。还有一些删除按钮被复制了。
完整代码:
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_()
您必须创建一个删除行的方法:
class TableModel(QAbstractTableModel):
# ...
<b>def removeRow(self, row):
self.beginRemoveRows(QModelIndex(), row, row)
del self.table[row]
self.endRemoveRows()</b>
然后映射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())