I want to change a cell based on the value in another cell. I'm using a QTableView populated by QAbstractTableModel with a pandas dataframe. Here is the code of Pandas Model:
class PandasModelEditable(QAbstractTableModel):
def __init__(self, data, parent=None):
QAbstractItemModel.__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=Qt.DisplayRole):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
return unicode(self._data.iloc[index.row(), index.column()])
return None
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return None
if orientation == Qt.Horizontal:
try:
return '%s' % unicode(self._data.columns.tolist()[section])
except (IndexError,):
return unicode()
elif orientation == Qt.Vertical:
try:
return '%s' % unicode(self._data.index.tolist()[section])
except (IndexError,):
return unicode()
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
def setData(self, index, value, role=Qt.EditRole):
if index.isValid():
self._data.iloc[index.row(), index.column()] = value
if self.data(index, Qt.DisplayRole) == value:
self.dataChanged.emit(index, index)
return True
return False
The principal dataframe (df
in code) has this structure:
| Player | O1_Score | O2_Score | O3_Score | O1_Rank | O2_Rank | O3_Rank |
| ------- | -------- | -------- | -------- | ------- | ------- | ------- |
| First | 9.66 | 10.24 | 8.52 | 2 | 1 | 3 |
| Second | 7.45 | 11.31 | 12.56 | 3 | 2 | 1 |
Obviously my dataframe has more scores (83) and its respective rank, just for example
The dataframe for table (dataframe_tp
in code) and table has exactly the same columns shown below, in GUI has this visual:
| Player | Selected O | Score | FO | NO1 | SO | NO2 | TO | NO3 |
| ------- | ---------- | ----- | -- | --- | -- | --- | -- | --- |
| First | ---------- | ----- | 1 | --- | 2 | --- | 3 | --- |
| Second | ---------- | ----- | 1 | --- | 2 | --- | 3 | --- |
Where: Selected O
means Selected Option, FO
means First Option, SO
means Second Option, TO
means Third Option, NO
means Name Option with a number to identify the above options
Based on the table above, Score
depends on Selected O
, NO1
depends on FO
, NO2
depends on SO
, NO3
depends on TO
.
Code:
self.squad_table = QTableView()
self.dataframe_tp = df.loc[:, ['Player']]
self.dataframe_tp["Selected O"] = '-'
self.dataframe_tp["Score"] = 0
self.dataframe_tp['FO'] = 1
self.dataframe_tp['NO1'] = '-'
self.dataframe_tp['SO'] = 2
self.dataframe_tp['NO2'] = '-'
self.dataframe_tp['TO'] = 3
self.dataframe_tp['NO3'] = '-'
model = PandasModelEditable(self.dataframe_tp)
self.squad_table.setModel(model)
self.squad_table.show()
And the expected output would be:
| Player | Selected O | Score | FO | NO1 | SO | NO2 | TO | NO3 |
| ------- | ---------- | ----- | -- | ------- | -- | -------- | -- | -------- |
| First | O3_Score | 8.52 | 1 | O2_Rank | 2 | O1_Rank | 3 | O3_Rank |
| Second | O2_Score | 11.31 | 1 | O3_Rank | 2 | O2_Rank | 3 | O1_Rank |
FO
, SO
, TO
should traverse over ranks of every player and bring the column header.
And a modification can be change value of Selected O
, or FO
, or SO
like below:
| Player | Selected O | Score | FO | NO1 | SO | NO2 | TO | NO3 |
| ------- | ---------- | ----- | -- | ------- | -- | -------- | -- | -------- |
| First | O1_Score | 9.66 | 2 | O1_Rank | 2 | O1_Rank | 3 | O3_Rank |
| Second | O2_Score | 11.31 | 1 | O3_Rank | 1 | O3_Rank | 3 | O1_Rank |
For the first use only edit the classes as @musicamante told me implementing this code:
class CustomizedPandasModel(QAbstractTableModel):
# Previous code hidden to clarify changes, above code is exactly the same
def setData(self, index, value, role=Qt.EditRole):
based_columns = [6, 8, 10, 12]
if role == Qt.EditRole:
row = index.row()
column = index.column()
tmp = str(value)
if column in based_columns:
# If selected column in list of accepted columns to edit
if column == 6 and tmp in self._data.columns.values.tolist():
# If value is in the list of headers
index_no = self._data.columns.get_loc(tmp) # Get index of column
self._data.iloc[row, column + 1] = self._data.iloc[row, index_no] # Set adjacent column as the value of that attribute
self._data.iloc[row, column] = tmp # Set actual cell to new value
self.dataChanged.emit(index, index)
Basically the only change was in class Model def setdata
where I added a list of the index values columns(based_columns
) for my utility, look up if the value to change was within the list of columns, get its index based on the value, and then define the next column as the value of that particular attribute based on the row of the edited cell and the column being looked up.