Make QTableView editable when model is pandas dataframe - python

In my gui file I create a QTableView as follows (this is the part that is automatically generated by Qt Designer):
self.pnl_results = QtGui.QTableView(self.tab_3)
font = QtGui.QFont()
font.setPointSize(7)
self.pnl_results.setFont(font)
self.pnl_results.setFrameShape(QtGui.QFrame.StyledPanel)
self.pnl_results.setFrameShadow(QtGui.QFrame.Sunken)
self.pnl_results.setEditTriggers(QtGui.QAbstractItemView.AllEditTriggers)
self.pnl_results.setShowGrid(True)
self.pnl_results.setSortingEnabled(True)
self.pnl_results.setCornerButtonEnabled(True)
self.pnl_results.setObjectName("pnl_results")
I then define a model that enables me to link the pandas dataframe to the QTableView:
class PandasModel(QtCore.QAbstractTableModel):
"""
Class to populate a table view with a pandas dataframe
"""
def __init__(self, data, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
def rowCount(self, parent=None):
return len(self._data.values)
def columnCount(self, parent=None):
return self._data.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
return str(self._data.values[index.row()][index.column()])
return None
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._data.columns[col]
return None
def setData(self, index, value, role):
if not index.isValid():
return False
if role != QtCore.Qt.EditRole:
return False
row = index.row()
if row < 0 or row >= len(self._data.values):
return False
column = index.column()
if column < 0 or column >= self._data.columns.size:
return False
self._data.values[row][column] = value
self.dataChanged.emit(index, index)
return True
def flags(self, index):
flags = super(self.__class__,self).flags(index)
flags |= QtCore.Qt.ItemIsEditable
flags |= QtCore.Qt.ItemIsSelectable
flags |= QtCore.Qt.ItemIsEnabled
flags |= QtCore.Qt.ItemIsDragEnabled
flags |= QtCore.Qt.ItemIsDropEnabled
return flags
and finally a add my pandas dataframe (df) to the model:
model = PandasModel(df)
self.ui.pnl_results.setModel(model)
This correctly displays my pandas dataframe in the QTableView. However, for some reason when I edit the fileds the return to their original values (and also once I edit the field it is starting as empty). How can I make it editable and then write the reults back to the pandas dataframe?

Your model lacks setData method. The default implementation from QtCore.QAbstractTableModel does nothing and returns False. You need to implement this method in your model to make its items editable. If df is the actual container storing the data, you should simply change the value of the item stored in the container in setData method. It could look like this:
def setData(self, index, value, role):
if not index.isValid():
return False
if role != QtCore.Qt.EditRole:
return False
row = index.row()
if row < 0 or row >= len(self._data.values):
return False
column = index.column()
if column < 0 or column >= self._data.columns.size:
return False
self._data.values[row][column] = value
self.dataChanged.emit(index, index)
return True
You also need to implement flags method to return a value containing QtCore.Qt.ItemIsEditable.

Related

How to only update the data that has changed in QTableView (Python)

So basically this is my PandasModel:
class pandasModel(QAbstractTableModel):
def __init__(self, data, editable=False):
QAbstractTableModel.__init__(self)
self._data = data
self.editable = editable
def rowCount(self, parent=None):
return self._data.shape[0]
def columnCount(self, parnet=None):
return self._data.shape[1]
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
if role == Qt.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
elif role == Qt.TextAlignmentRole:
return Qt.AlignCenter
def setData(self, index, value, role):
if role == Qt.EditRole:
self._data.iloc[index.row(), index.column()] = value
return True
return False
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._data.columns[col]
def flags(self, index):
if self.editable:
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
else:
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
def sort(self, col, order):
"""sort table by given column number col"""
self.layoutAboutToBeChanged.emit()
self._data = self._data.sort_values(
self._data.columns[col], ascending=order == Qt.AscendingOrder)
self.layoutChanged.emit()
And this is the way I'm updating my Table when the data is changed and received from the thread using API.
Basically my other Thread runs in a while loop constantly and if the data is changed it emits the new array of data to main thread and then the data is received and that is when the data is changed.
self.Table.setModel(pandasModel(self.myDataFrame))
I only wanted to update the new data but this updates whole table, due to that the selection is also lost!
Is there other way we can do it so that the values are updated but the table doesn't reset completely and also the selections of rows to be not lost???
P.S. the data is huge so cannot just pick one and update manually I want something dynamic

How to SetData in a QtableView Cell using pyside

I'm trying to set a value in a cell in my QtableView. I'm using Pyside2.
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, mlist=None):
super(TableModel, self).__init__()
self._items = [] if mlist == None else mlist
self._header = []
def data(self, index, role = QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self._items[index.row()][index.column()]
return None
def setData(self, index, value, role = QtCore.Qt.EditRole):
if value is not None and role == QtCore.Qt.EditRole:
self._items[index.row()][index.column()] = value
return True
return False
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# skip the irrelevant code #
Table = QTableView()
model = TableModel()
Table.setModel(model)
row, column = a_function_that_outputs_the_row_and_column_of_the_cell_to_edit()
self.Table.model().setData(index ?, value)
What I'm struggling with is getting to understand how to pass the index as an argument, I have the row and column, any help on how to do that properly?
You need to use model.index(row, column, parent=None) (see the documentation) to get the appropriate QModelIndex.
If the model is two dimensional (not a tree view), the parent is not required:
model.setData(model.index(row, column), value)

Optimize PyQt5 QAbstractView for Pandas Model

I am currently writing a SQL query interface through Python.
When writing the data into a QTableView, I am using a QAbstractTableModel to write the query result.
This works fine for smaller queries, but becomes extremely slow when trying to present many rows and columns. Is there anyway to increase the speed that the dataframe is loaded into the QTableView?
Here is the code for my QAbtractTableModel class:
class SQLConnection_PandasModel(QtCore.QAbstractTableModel):
def __init__(self, reason, df, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._df = df.copy()
self.original_df = df
self.reason = reason
# PyQt5 Slots and Signals
if self.reason == 'Read':
self.conSig = sqlWindow()
self.conSig.dataChanged.connect(self.conSig.sql_table_updated)
# set the shortcut ctrl+F for find in menu
self.find_list_row = []
# setup menu options
def toDataFrame(self):
return self._df.copy()
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if orientation == QtCore.Qt.Horizontal:
try:
return self._df.columns.tolist()[section]
except (IndexError,):
return QtCore.QVariant()
elif orientation == QtCore.Qt.Vertical:
try:
# return self.df.index.tolist()
return self._df.index.tolist()[section]
except (IndexError,):
return QtCore.QVariant()
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return QtCore.QVariant()
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
#print(type(self._df.iloc[index.row(), index.column()]))
if isinstance(self._df.iloc[index.row(), index.column()], bytes):
return QtCore.QVariant('(BLOB)')
else:
return QtCore.QVariant(str(self._df.iloc[index.row(), index.column()]))
def setData(self, index, value, role):
row = self._df.index[index.row()]
col = self._df.columns[index.column()]
if hasattr(value, 'toPyObject'):
# PyQt4 gets a QVariant
value = value.toPyObject()
else:
# PySide gets an unicode
dtype = self._df[col].dtype
if dtype != object:
if ((np.issubdtype(dtype, np.integer)) or isinstance(dtype, int)) and value.isnumeric():
value = None if value == '' else dtype.type(value)
self._df.loc[row, col] = value
# This is the signal to my RDB class that a cell has changed and to update the rdb_DF.
if role == QtCore.Qt.EditRole and self.reason == 'Read':
#print('Emitting 1',row , col)
self.conSig.dataChanged.emit(row, col, value)
return True
elif ((np.issubdtype(dtype, np.integer)) or isinstance(dtype, int)) and not(value.isnumeric()):
return False
else:
value = None if value == '' else dtype.type(value)
self._df.loc[row, col] = value
# This is the signal to my RDB class that a cell has changed and to update the rdb_DF.
if role == QtCore.Qt.EditRole and self.reason == 'Read':
#print('Emitting 2', row, col)
self.conSig.dataChanged.emit(row, col, value)
return True
else:
self._df.loc[row, col] = value
# This is the signal to my RDB class that a cell has changed and to update the rdb_DF.
if role == QtCore.Qt.EditRole and self.reason == 'Read':
#print('Emitting 3', row, col)
self.original_df.at[row, col] = value
#print(self.original_df)
self.conSig.dataChanged.emit(row, col, self.original_df)
return True
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self._df.index)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self._df.columns)
def sort(self, column, order):
colname = self._df.columns.tolist()[column]
self.layoutAboutToBeChanged.emit()
self._df.sort_values(colname, ascending=order == QtCore.Qt.AscendingOrder, inplace=True)
self._df.reset_index(inplace=True, drop=True)
self.layoutChanged.emit()
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
I would appreciate any assistance I could get in increasing the speed that the dataframe is loaded.
Thank you.
Well I answered my own question. For those in the future that are running into an issue where you are trying to populate QTableViews using a QAbstractTableModel with large datasets...here you go:
class SQLConnection_PandasModel(QtCore.QAbstractTableModel):
def __init__(self, reason, df, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._df = np.array(df.values)
self.original_df = df.copy()
self._cols = df.columns
self.r, self.c = np.shape(self._df)
self.reason = reason
# PyQt5 Slots and Signals
if self.reason == 'Read':
self.conSig = sqlWindow()
self.conSig.dataChanged.connect(self.conSig.sql_table_updated)
# set the shortcut ctrl+F for find in menu
self.find_list_row = []
def rowCount(self, parent=None):
return self.r
def columnCount(self, parent=None):
return self.c
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
if isinstance(self._df[index.row(), index.column()], bytes):
row = self._df[index.row()][0] - 1
col = self._df[index.column()][0] -1
self._df[row, col] = '(BLOB)'
return str('(BLOB)')
return str(self._df[index.row(),index.column()])
return None
def setData(self, index, value, role):
row = self._df[index.row()]
col = self._df[index.column()]
if hasattr(value, 'toPyObject'):
# PyQt4 gets a QVariant
value = value.toPyObject()
else:
# PySide gets an unicode
dtype = self._df.dtype
if dtype != object:
value = None if value == '' else dtype.type(value)
table_row = row[0]-1
table_col = col[0]-1
self._df[table_row, table_col] = value
# This is the signal to my RDB class that a cell has changed and to update the rdb_DF.
if role == QtCore.Qt.EditRole and self.reason == 'Read':
column_name = self.original_df.columns[table_col]
self.original_df.loc[table_row ,column_name] = value
my_df = pd.DataFrame(self._df)
my_df.columns = self.original_df.columns
self.conSig.dataChanged.emit(table_row, table_col, my_df)
return True
def headerData(self, p_int, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self._cols[p_int]
elif orientation == QtCore.Qt.Vertical:
return p_int
return None
def sort(self, column, order):
colname = self._df.columns.tolist()[column]
self.layoutAboutToBeChanged.emit()
self._df.sort_values(colname, ascending=order == QtCore.Qt.AscendingOrder, inplace=True)
self._df.reset_index(inplace=True, drop=True)
self.layoutChanged.emit()
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
I converted the pandas df into a numpy array and that really sped it up. Finally, I convert the numpy back to pandas when I need to print out. This increased the speed drastically.
Pandas loading time for 120k rows and 10 columns: ~280 seconds.
Numpy loading time for 120k rows and 10 columns: 6.634 seconds.

Pyqt5 QAbstractTableModel dataChanged not updating data

I am trying to update my QTableView after I receive a notice via pydispatcher of a change in the system. I did create the following functions
def rowCount(self, parent=None):
return len(self.m_list)
def columnCount(self, parent=None):
return len(self.table_def)
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.table_def[col]['Header']
return QVariant()
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
if index.row() >= len(self.m_list) or index.row() < 0:
return None
row = index.row()
col = index.column()
print("Called for (%s, %s, %s)" % (row, col, role))
if role == Qt.DisplayRole:
return self.table_def[index.column()]['Function'](index.row())
elif role == Qt.BackgroundRole:
batch = (index.row() // 100) % 2
if batch == 0:
return QApplication.palette().base()
return QApplication.palette().alternateBase()
else:
return None
def flags(self, index):
if not index.isValid():
return None
return Qt.ItemIsEnabled
def update_model(self, data):
print('update_model')
index_1 = self.index(0, 0)
index_2 = self.index(0, 1)
self.dataChanged.emit(index_1, index_2, [Qt.DisplayRole])
The line self.dataChanged.emit(index_1, index_2, [Qt.DisplayRole]) does not seems to do anything; i.e. data(self, index, role=Qt.DisplayRole) is not called.
If I click on the table, then data(self, index, role=Qt.DisplayRole) is called and the table update.
The fix that I have right now is to call beginResetModel() and endResetModel(). That works, but it is not how it should work.
Any idea what could be happening?
I had the same problem and I fixed it just by calling self.headerDataChanged.emit instead. So, to do that, once you change something in the table, call the following:
self.headerDataChanged.emit(Qt.Horizontal, idx1, idx2)
where self._data is your data within the class. idx1 and idx2 are the first and last indexes of changed data, respectively. Qt.Horizontal is an example and it could be vertical depending on your table content.

Pandas df in editable QTableView: remove check boxes

I have a pandas dataframe that I would like to present in a QtableView and make it editable. I have create the below model, but for some reason the output has checkboxes in every field. How can I get rid of them?
The outout looks like this:
And this this is the model that is used to make the pandas dataframe shown in a qtavleview and make it editable (I'm using PySide)
class PandasModelEditable(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
def rowCount(self, parent=None):
return len(self._data.values)
def columnCount(self, parent=None):
return self._data.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return unicode(self._data.iloc[index.row(), index.column()])
return unicode()
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return None
if orientation == QtCore.Qt.Horizontal:
try:
return '%s' % unicode(self._data.columns.tolist()[section])
except (IndexError,):
return unicode()
elif orientation == QtCore.Qt.Vertical:
try:
return '%s' % unicode(self._data.index.tolist()[section])
except (IndexError,):
return unicode()
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | \
QtCore.Qt.ItemIsEditable
def setData(self, index, value, role=QtCore.Qt.EditRole):
if index.isValid():
self._data.iloc[index.row(), index.column()] = value
if self.data(index, QtCore.Qt.DisplayRole) == value:
self.dataChanged.emit(index, index)
return True
return unicode()
Removing QtCore.Qt.ItemIsSelectable does not solve the problem as it doesn't seem to have any effet.
You are returning the wrong default values from data and setaData. The former should return None (so you could just remove the last line), whilst the latter should return False.

Categories

Resources