PyQt5:为pandas表模型实现removeRows
我使用 QTableView 来显示和编辑 Pandas DataFrame。 我在 TableModel 类中使用此方法来删除行:
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
它工作正常,直到我将组合框添加到 TableView 上的某些单元格。我使用以下代码添加组合框(在 Main 类中),但是当我删除一行时,它会显示错误消息(Python 3.10,Pandas 1.4.1): IndexError:索引 2 超出尺寸为 2 的轴 0 的范围
或(Python 3.9、Pandas 1.3.5):'IndexError:单个位置索引器超出范围'
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
但如果我注释掉这两行: self.beginRemoveRows(QModelIndex, start, end)
和来自removeRows方法的self.endRemoveRows()
,它有效并且没有更多错误消息。但根据Qt文档,这两个方法必须被调用。
removeRows() 实现必须在调用之前调用 beginRemoveRows() 行从数据结构中删除,并且必须调用 之后立即 endRemoveRows()。
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
我已经尝试了几个小时,但我无法弄清楚这一点。谁能帮我解释一下我的代码有什么问题吗?
这是我的表模型类:
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
可检查组合框的类:
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())
主类:
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_()
我添加一种方法来添加行。 self.layoutChanged.emit() 是否必须更新 TableView 还是有更有效的方法?:
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
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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您的示例将错误的索引传递给
removeRows
,这也无法正确计算开始值和结束值。可以这样修复: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: