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.
Related
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
After I change one or more cell contents of my qtableview, each changed cell loses its formatting.
Code:
...
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
value =self._data[index.row()][index.column()]
return str(value)
if role == Qt.TextAlignmentRole:
value = self._data[index.row()][index.column()]
if isinstance(value, int):
return Qt.AlignVCenter + Qt.AlignRight
if role == Qt.ForegroundRole:
value = self._data[index.row()][index.column()]
if (isinstance(value, int)
and value < 0):
return QtGui.QColor('red')
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
self._data[index.row()][index.column()] = value
self.dataChanged.emit(index, index, (Qt.DisplayRole, Qt.TextAlignmentRole, Qt.ForegroundRole,))
return True
return False
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
How can i change the code in order to keep formatting after changing cell content?
I have the following working code, which opens a QFileDialog with an extra column that shows the file name again (pointless, I know, but it’s a result of simplifying my issue):
from PySide2 import QtCore, QtWidgets
class MyProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(MyProxyModel, self).__init__(parent)
self._parents = {}
def mapToSource(self, index):
if index.column() == 4:
return QtCore.QModelIndex()
return super(MyProxyModel, self).mapToSource(index)
def columnCount(self, index):
return 5
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole and index.column() == 4:
return self.index(index.row(), 0, self._parents[index]).data(role)
return super(MyProxyModel, self).data(index, role)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if section == 4 and orientation == QtCore.Qt.Horizontal \
and role == QtCore.Qt.DisplayRole:
return 'My Column'
return super(MyProxyModel, self).headerData(section, orientation, role)
def index(self, row, column, parent=QtCore.QModelIndex()):
if column == 4:
index = self.createIndex(row, column)
self._parents[index] = parent
return index
return super(MyProxyModel, self).index(row, column, parent)
def parent(self, index):
if index.column() == 4:
return QtCore.QModelIndex()
return super(MyProxyModel, self).parent(index)
QtWidgets.QApplication([])
dialog = QtWidgets.QFileDialog()
dialog.setOption(dialog.DontUseNativeDialog, True)
dialog.setProxyModel(MyProxyModel(dialog))
dialog.exec_()
As you can see, parent() is returning an invalid index for items of column 4, and instead I’m retrieving the actual parent inside data(), which isn’t ideal. But if I try the following, it exits with an access violation:
(...)
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole and index.column() == 4:
# Either return causes access violation.
return self.index(index.row(), 0, self.parent(index)).data(role)
return self.index(index.row(), 0, index.parent()).data(role)
return index.sibling(index.row(), 0).data(role)
return super(MyProxyModel, self).data(index, role)
(...)
def parent(self, index):
if index.column() == 4:
return self._parents[index]
return super(MyProxyModel, self).parent(index)
(...)
I also tried leveraging QModelIndex’s internal pointer, with the same result (access violation):
# No __init__() defined; data() exactly like above.
(...)
def index(self, row, column, parent=QtCore.QModelIndex()):
if column == 4:
return self.createIndex(row, column, parent)
return super(MyProxyModel, self).index(row, column, parent)
def parent(self, index):
if index.column() == 4:
return index.internalPointer()
return super(MyProxyModel, self).parent(index)
(...)
Pretty sure I’m missing something, but I can’t figure out what it is…
The main problem is that the parent should not be invalid, even for a "virtual" index.
Also, in order to properly interact with the fake column, the following three aspects must be considered:
the internalId() of the parent is required for createIndex(), otherwise you'll have the same index for the same pair of row/column even if they have different parents;
flags() must return valid flags, in this case you can return the flag of the sibling in the first row;
sibling() must be return the result of self.index() for the virtual column, or use a valid starting index to compute the sibling;
mapToSource should return a valid source index so that the view can properly access its data; an invalid index is usually considered the root of the model, and returning it represents an issue: if you double click an index, the file dialog tries to open it, and since an invalid index is considered the root of the file system model (which is a "folder"), it will then navigate to it;
class MyProxyModel(QtCore.QSortFilterProxyModel):
# ...
def mapToSource(self, index):
if index.column() == 4:
index = index.sibling(index.row(), 0)
return super(MyProxyModel, self).mapToSource(index)
def index(self, row, column, parent=QtCore.QModelIndex()):
if column == 4:
index = self.createIndex(row, column, parent.internalId())
self._parents[index] = parent
return index
return super(MyProxyModel, self).index(row, column, parent)
def parent(self, index):
if index.column() == 4:
return self._parents[index]
return super(MyProxyModel, self).parent(index)
def flags(self, index):
if index.column() == 4:
return self.flags(index.sibling(index.row(), 0))
return super().flags(index)
def sibling(self, row, column, idx):
if column == 4:
return self.index(row, column, idx.parent())
elif idx.column() == 4:
idx = self.index(idx.row(), 0, idx.parent())
return super().sibling(row, column, idx)
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.
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.