I would like to modify any cell (except header) within given QTableView. Here below is my original code that does not allow for any changes:
import sys
import csv
from datetime import datetime, timedelta
import calendar
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sqlite3
from pandas import DataFrame
class TBWindow(QMainWindow):
def __init__(self, parent=None):
super(TBWindow, self).__init__(parent)
sql = "select * from stock"
args = []
conn = sqlite3.connect('DataBase.db')
c = conn.cursor()
c.execute(sql,args)
data = c.fetchall()
names = list(map(lambda x: x[0], c.description))
c.close()
conn.close()
data = DataFrame(data, columns = names)
MenuBar = self.menuBar()
saveFile = QAction("&Save File", self)
saveFile.setShortcut("Ctrl+S")
saveFile.setStatusTip('Save File')
saveFile.triggered.connect(self.file_save)
MenuBar.addAction(saveFile)
self.setWindowTitle('Aplikace Princezna Pampeliska')
self.centralwidget = QWidget(self)
self.lineEdit = QLineEdit(self.centralwidget)
self.view = QTableView(self.centralwidget)
self.comboBox = QComboBox(self.centralwidget)
self.label = QLabel(self.centralwidget)
self.gridLayout = QGridLayout(self.centralwidget)
self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1)
self.gridLayout.addWidget(self.view, 1, 0, 1, 3)
self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1)
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.setCentralWidget(self.centralwidget)
self.label.setText("Filter")
self.model = PandasModel(data)
self.proxy = QSortFilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
for column in range(self.view.horizontalHeader().count()):
self.view.resizeColumnToContents(column)
for i in (0,1,3):
self.view.horizontalHeader().setSectionResizeMode(i, QHeaderView.Stretch)
self.comboBox.addItems(list(data.columns.values))
self.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)
self.comboBox.currentIndexChanged.connect(self.on_comboBox_currentIndexChanged)
self.horizontalHeader = self.view.horizontalHeader()
def file_save(self):
newModel = self.view.model()
data = []
for row in range(newModel.rowCount()):
rowRes = []
for column in range(newModel.columnCount()):
index = newModel.index(row, column)
item = newModel.data(index)
if item != '':
rowRes.append(item)
data.append(rowRes)
dataFrame = DataFrame(data)
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
fileName, fEnd = QFileDialog.getSaveFileName(self, "Save File", "", ".csv")
if fileName:
address = fileName+fEnd
dataFrame.to_csv(address, index=False, header=False)
#pyqtSlot(str)
def on_lineEdit_textChanged(self, text):
search = QRegExp(text, Qt.CaseInsensitive, QRegExp.RegExp)
self.proxy.setFilterRegExp(search)
#pyqtSlot(int)
def on_comboBox_currentIndexChanged(self, index):
self.proxy.setFilterKeyColumn(index)
class PandasModel(QAbstractTableModel):
def __init__(self, data, parent=None):
QAbstractTableModel.__init__(self, parent)
self._data = data
def rowCount(self, parent=None):
return self._data.shape[0]
def columnCount(self, parent=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()])
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._data.columns[col]
return None
if __name__ == '__main__':
app = QApplication(sys.argv)
main = TBWindow()
main.showMaximized()
sys.exit(app.exec_())
I have read some articles about modifying cells within QTableView and found out that one needs to modify the model not the view. Hence the addition of this bit of code makes the cells editable:
def flags(self, index):
flags = super(self.__class__,self).flags(index)
flags |= Qt.ItemIsEditable
flags |= Qt.ItemIsSelectable
flags |= Qt.ItemIsEnabled
flags |= Qt.ItemIsDragEnabled
flags |= Qt.ItemIsDropEnabled
return flags
However, the changes do not persist, they disappear right after I click elsewhere. I tried to add function setData() in this manner with no success:
def setData(self, index, value, role=Qt.EditRole):
row = index.row()
col = index.column()
self._data[row][name] = value
self.emit(SIGNAL('dataChanged()'))
return True
The program exits with some key error. Any suggestions what should be changed? Thanks!
Your code has 2 errors:
the first is caused by a bad programming practice: you should not call the variables with names that you used with other variables, functions or classes. In your case flags is the name of the method and therefore you should not use it in the name of the variable:
def flags(self, index):
fl = super(self.__class__,self).flags(index)
fl |= Qt.ItemIsEditable
fl |= Qt.ItemIsSelectable
fl |= Qt.ItemIsEnabled
fl |= Qt.ItemIsDragEnabled
fl |= Qt.ItemIsDropEnabled
return fl
The second error is caused because you are not using the proper method to access and assign the data in pandas, this must be done through the iloc method. Another problem is that you are using a valid method for PyQt4 to notify the changes, you must use the new syntax plus the dataChanged signal to add new fields in Qt5 as you can see in the docs:
def setData(self, index, value, role=Qt.EditRole):
if index.isValid():
row = index.row()
col = index.column()
self._data.iloc[row][col] = float(value)
self.dataChanged.emit(index, index, (Qt.DisplayRole, ))
return True
return False
Plus:
I have also previously implemented a class that creates a model using pandas as you can see in the question How to display a Pandas data frame with PyQt5
Related
I wish to add a checkbox to a cell with text in my QTableView:
when a cell is not activated we should see a text and a greyed out checkbox, like this
when we open a delegate editor, we should be able to edit text and to change state of a checkbox
Just for info - further I was planning to do next: when a checkbox is activated - a cell will reject signal for updating text in it, and if checkbox is unchecked - text will be updated when special signal is emitted.
I understand that I should subclass QStyledItemDelegate and re-implement paint method, but as far as I am a beginner I found it difficult to figure out this task.
You can use this example to write and test a code:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
import pickle
class Mainwindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
self.setCentralWidget(self.table)
self.list = ['item_1', 'item_2','item_3']
self.data = [
[1, 'Blocks γ=500 GOST 31359-2007', self.list[0], 0.18, 0.22],
[2, 'Blocks γ=600 GOST 31359-2008', self.list[0], 0.25, 0.27],
[3, 'Insulation', self.list[0], 0.041, 0.042],
[3, 'Insulation', self.list[0], 0.041, 0.042]
]
self.model = Materials(self.data)
self.table.setModel(self.model)
self.table.setSelectionBehavior(self.table.SelectRows)
self.table.setSelectionMode(self.table.SingleSelection)
for row in range(len(self.model.materials)):
index = self.table.model().index(row,2)
self.table.setIndexWidget(index, self.setting_combobox(index))
def setting_combobox(self, index):
widget = QtWidgets.QComboBox()
list = self.list
widget.addItems(list)
widget.setCurrentIndex(0)
widget.currentTextChanged.connect(lambda value: self.model.setData(index, value))
return widget
class Materials(QtCore.QAbstractTableModel):
def __init__(self, materials = [[]], parent = None):
super(Materials, self).__init__()
self.materials = materials
def rowCount(self, parent):
return len(self.materials)
def columnCount(self, parent):
return len(self.materials[0])
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = self.materials[row][column]
return value
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
value = self.materials[row][column]
return value
if role == QtCore.Qt.FontRole:
if index.column() == 0:
boldfont = QtGui.QFont()
boldfont.setBold(True)
return boldfont
def setData(self, index, value, role = QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.materials[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
if __name__ == '__main__':
app = QtWidgets.QApplication([])
application = Mainwindow()
application.show()
sys.exit(app.exec())
A possible solution is to enable the role Qt.CheckStateRole and modify the position of the checkbox using the delegate. For this, the model must also be modified to store the state of the checkbox.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class CustomDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
value = index.data(QtCore.Qt.CheckStateRole)
if value is None:
model = index.model()
model.setData(index, QtCore.Qt.Unchecked, QtCore.Qt.CheckStateRole)
super().initStyleOption(option, index)
option.direction = QtCore.Qt.RightToLeft
option.displayAlignment = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
class Mainwindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
self.setCentralWidget(self.table)
self.list = ["item_1", "item_2", "item_3"]
self.data = [
[1, "Blocks γ=500 GOST 31359-2007", self.list[0], 0.18, 0.22],
[2, "Blocks γ=600 GOST 31359-2008", self.list[0], 0.25, 0.27],
[3, "Insulation", self.list[0], 0.041, 0.042],
[3, "Insulation", self.list[0], 0.041, 0.042],
]
self.model = Materials(self.data)
self.table.setModel(self.model)
self.table.setSelectionBehavior(self.table.SelectRows)
self.table.setSelectionMode(self.table.SingleSelection)
for row in range(len(self.model.materials)):
index = self.table.model().index(row, 2)
self.table.setIndexWidget(index, self.setting_combobox(index))
delegate = CustomDelegate(self.table)
self.table.setItemDelegateForColumn(4, delegate)
self.resize(640, 480)
def setting_combobox(self, index):
widget = QtWidgets.QComboBox()
list = self.list
widget.addItems(list)
widget.setCurrentIndex(0)
widget.currentTextChanged.connect(
lambda value: self.model.setData(index, value)
)
return widget
class Materials(QtCore.QAbstractTableModel):
def __init__(self, materials=[[]], parent=None):
super(Materials, self).__init__()
self.materials = materials
self.check_states = dict()
def rowCount(self, parent):
return len(self.materials)
def columnCount(self, parent):
return len(self.materials[0])
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = self.materials[row][column]
return value
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
value = self.materials[row][column]
return value
if role == QtCore.Qt.FontRole:
if index.column() == 0:
boldfont = QtGui.QFont()
boldfont.setBold(True)
return boldfont
if role == QtCore.Qt.CheckStateRole:
value = self.check_states.get(QtCore.QPersistentModelIndex(index))
if value is not None:
return value
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.materials[row][column] = value
self.dataChanged.emit(index, index, (role,))
return True
if role == QtCore.Qt.CheckStateRole:
self.check_states[QtCore.QPersistentModelIndex(index)] = value
self.dataChanged.emit(index, index, (role,))
return True
return False
def flags(self, index):
return (
QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsUserCheckable
)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
application = Mainwindow()
application.show()
sys.exit(app.exec())
I would like to update the cell content of a QTableView with a ComboBox whenever the toggle of a Checkbox above changes. I am using QTableView and a custom delegate to draw the ComboBoxes. The Checkboxes are controlled within the QTableView itself. Currently, when I toggle the checkboxes, the Comboboxes below will appear, but I could not manage to remove the ComboBoxes when toggling off the CheckBoxes.
My code sample is below.
import sys
import pandas as pd
from pandas.api.types import is_numeric_dtype
import numpy as np
from PyQt5.QtCore import (QAbstractTableModel, Qt, pyqtProperty, pyqtSlot,
QVariant, QModelIndex, pyqtSignal)
from PyQt5.QtWidgets import (QComboBox, QApplication, QAbstractItemView,
QItemDelegate, QCheckBox, QMainWindow, QTableView)
class DataFrameModel(QAbstractTableModel):
DtypeRole = Qt.UserRole + 1000
ValueRole = Qt.UserRole + 1001
def __init__(self, df=pd.DataFrame(), parent=None):
super(DataFrameModel, self).__init__(parent)
self._dataframe = df
self.df2 = pd.DataFrame(self._dataframe.iloc[2:3, :].to_dict())
def setDataFrame(self, dataframe):
self.beginResetModel()
self._dataframe = dataframe.copy()
self.endResetModel()
def dataFrame(self):
return self._dataframe
dataFrame = pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)
#pyqtSlot(int, Qt.Orientation, result=str)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return QVariant()
if orientation == Qt.Horizontal:
try:
return self._dataframe.columns.tolist()[section]
except (IndexError,):
return QVariant()
elif orientation == Qt.Vertical:
try:
if section in [0, 1]:
pass
else:
return self._dataframe.index.tolist()[section - 2]
except (IndexError,):
return QVariant()
def rowCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
return len(self._dataframe.index)
def columnCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
return self._dataframe.columns.size
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
col = index.column()
row = index.row()
dt = self.df2[self.df2.columns[col]].dtype
is_numeric = is_numeric_dtype(self.df2[self.df2.columns[col]])
if row == 0 and is_numeric:
value = self._dataframe.iloc[row, col].text()
else:
value = self._dataframe.iloc[row, col]
if role == Qt.DisplayRole:
return value
elif role == Qt.CheckStateRole:
if row == 0 and is_numeric:
if self._dataframe.iloc[row, col].isChecked():
return Qt.Checked
else:
return Qt.Unchecked
elif role == DataFrameModel.ValueRole:
return value
elif role == DataFrameModel.ValueRole:
return value
if role == DataFrameModel.DtypeRole:
return dt
return QVariant()
def roleNames(self):
roles = {
Qt.DisplayRole: b'display',
DataFrameModel.DtypeRole: b'dtype',
DataFrameModel.ValueRole: b'value'
}
return roles
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
col = index.column()
row = index.row()
is_numeric = is_numeric_dtype(self.df2[self.df2.columns[col]])
if role == Qt.CheckStateRole and index.row() == 0:
if is_numeric:
if value == Qt.Checked:
self._dataframe.iloc[row, col].setChecked(True)
self._dataframe.iloc[row, col].setText("Grade Item")
else:
self._dataframe.iloc[row, col].setChecked(False)
self._dataframe.iloc[row, col].setText("Not a Grade")
elif row == 1 and role == Qt.EditRole:
if isinstance(value, QVariant):
value = value.value()
if hasattr(value, 'toPyObject'):
value = value.toPyObject()
self._dataframe.iloc[row, col] = value
elif row >= 2 and role == Qt.EditRole:
try:
value = eval(value)
if not isinstance(
value,
self._dataframe.applymap(type).iloc[row, col]):
value = self._dataframe.iloc[row, col]
except:
value = self._dataframe.iloc[row, col]
self._dataframe.iloc[row, col] = value
self.dataChanged.emit(index, index, (Qt.DisplayRole,))
return True
def flags(self, index):
if not index.isValid():
return None
if index.row() == 0:
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
Qt.ItemIsUserCheckable)
else:
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
def sort(self, column, order):
self.layoutAboutToBeChanged.emit()
col_name = self._dataframe.columns.tolist()[column]
sheet1 = self._dataframe.iloc[:2, :]
sheet2 = self._dataframe.iloc[2:, :].sort_values(
col_name, ascending=order == Qt.AscendingOrder, inplace=False)
sheet2.reset_index(drop=True, inplace=True)
sheet3 = pd.concat([sheet1, sheet2], ignore_index=True)
self.setDataFrame(sheet3)
self.layoutChanged.emit()
class ComboBoxDelegate(QItemDelegate):
def __init__(self, parent, choices=None):
super().__init__(parent)
self.items = choices
def createEditor(self, parent, option, index):
self.parent().model().dataChanged.emit(index, index, (Qt.DisplayRole,))
if is_numeric_dtype(
self.parent().model().df2[
self.parent().model().df2.columns[index.column()]]):
checked = self.parent().model().dataFrame.iloc[
0, index.column()].isChecked()
if checked:
editor = QComboBox(parent)
editor.addItems(self.items)
editor.currentIndexChanged.connect(self.currentIndexChanged)
return editor
def paint(self, painter, option, index):
if isinstance(self.parent(), QAbstractItemView):
self.parent().openPersistentEditor(index)
def setModelData(self, editor, model, index):
value = editor.currentText()
model.setData(index, value, Qt.DisplayRole)
def setEditorData(self, editor, index):
text = index.data(Qt.DisplayRole) or ""
editor.setCurrentText(text)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
#pyqtSlot()
def currentIndexChanged(self):
editor = self.sender()
self.commitData.emit(editor)
class MainWindow(QMainWindow):
def __init__(self, pandas_sheet):
super().__init__()
self.pandas_sheet = pandas_sheet
self.table = QTableView()
self.setCentralWidget(self.table)
check_bx_lst = []
is_number = [is_numeric_dtype(self.pandas_sheet[col]) for col
in self.pandas_sheet.columns]
for is_numb in is_number:
if is_numb:
checkbox = QCheckBox('Not a Grade')
checkbox.setChecked(False)
check_bx_lst.append(checkbox)
else:
check_bx_lst.append(None)
for i in range(2):
self.pandas_sheet.loc[-1] = [' '] * \
self.pandas_sheet.columns.size
self.pandas_sheet.index = self.pandas_sheet.index + 1
self.pandas_sheet = self.pandas_sheet.sort_index()
self.pandas_sheet.loc[0] = check_bx_lst
model = DataFrameModel(self.pandas_sheet)
self.table.setModel(model)
self.table.setSortingEnabled(True)
delegate = ComboBoxDelegate(self.table,
[None, 'Test', 'Quiz'])
self.table.setItemDelegateForRow(1, delegate)
self.table.resizeColumnsToContents()
self.table.resizeRowsToContents()
if __name__ == '__main__':
df = pd.DataFrame({'a': ['student ' + str(i) for i in range(5)],
'b': np.arange(5),
'c': np.random.rand(5)})
app = QApplication(sys.argv)
window = MainWindow(df)
window.table.model().sort(df.columns.get_loc("a"), Qt.AscendingOrder)
window.setFixedSize(280, 200)
window.show()
sys.exit(app.exec_())
I would like to remove the Combobox when the checkbox above is toggled off.
Any help is really appreciated.
You are using openPersistentEditor within the paint function, which is simply wrong: painting happens very often for every index the delegate is used, and you're practically calling createEditor each time each cell in that row is painted, something that happens for all the cells when the view is scrolled, or for any cell hovered by the mouse.
Following your logic, the creator is finally created probably due to painting requested by data change, because at that point the if conditions in createEditor are True. From that point on, you create a persistent editor, and if you don't remove it it will just stay there.
Obviously, all this is not a good approach, mostly because you virtually check if it's ok to create the editor from a paint function, which doesn't make much sense.
You should connect to the dataChanged signal for the model to verify the checked status and then open or close the editor accordingly.
class MainWindow(QMainWindow):
def __init__(self, pandas_sheet):
# ...
model.dataChanged.connect(self.dataChanged)
def dataChanged(self, topLeft, bottomRight, roles):
if topLeft.row() == 0:
if topLeft.data(QtCore.Qt.CheckStateRole):
self.table.openPersistentEditor(topLeft.sibling(1, topLeft.column()))
else:
self.table.closePersistentEditor(topLeft.sibling(1, topLeft.column()))
I've over-simplified the if condition, but I assume that the concept is clear enough.
I have QTableView which views a pandas table. The first row has QComboBox delegates. When I sort the table by a column the delegates disappear.
A working example of my code is below.
import sys
import pandas as pd
import numpy as np
from PyQt5.QtCore import (QAbstractTableModel, Qt, pyqtProperty, pyqtSlot,
QVariant, QModelIndex)
from PyQt5.QtWidgets import (QItemDelegate, QComboBox, QMainWindow, QTableView,
QApplication)
class DataFrameModel(QAbstractTableModel):
DtypeRole = Qt.UserRole + 1000
ValueRole = Qt.UserRole + 1001
ActiveRole = Qt.UserRole + 1
def __init__(self, df=pd.DataFrame(), parent=None):
super(DataFrameModel, self).__init__(parent)
self._dataframe = df
def setDataFrame(self, dataframe):
self.beginResetModel()
self._dataframe = dataframe.copy()
self.endResetModel()
def dataFrame(self):
return self._dataframe
dataFrame = pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)
#pyqtSlot(int, Qt.Orientation, result=str)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return QVariant()
if orientation == Qt.Horizontal:
try:
return self._dataframe.columns.tolist()[section]
except (IndexError, ):
return QVariant()
elif orientation == Qt.Vertical:
try:
if section in [0]:
pass
else:
return self._dataframe.index.tolist()[section - 1]
except (IndexError, ):
return QVariant()
def rowCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
return len(self._dataframe.index)
def columnCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
return self._dataframe.columns.size
def data(self, index, role=Qt.DisplayRole):
if not index.isValid() or not (0 <= index.row() < self.rowCount()
and 0 <= index.column() <
self.columnCount()):
return QVariant()
row = self._dataframe.index[index.row()]
col = self._dataframe.columns[index.column()]
dt = self._dataframe[col].dtype
val = self._dataframe.iloc[row][col]
if role == Qt.DisplayRole:
return str(val)
elif role == DataFrameModel.ValueRole:
return val
if role == DataFrameModel.DtypeRole:
return dt
return QVariant()
def roleNames(self):
roles = {
Qt.DisplayRole: b'display',
DataFrameModel.DtypeRole: b'dtype',
DataFrameModel.ValueRole: b'value'
}
return roles
def setData(self, index, value, role):
col = index.column()
row = index.row()
if index.row() == 0:
if isinstance(value, QVariant):
value = value.value()
if hasattr(value, 'toPyObject'):
value = value.toPyObject()
self._dataframe.iloc[row, col] = value
self.dataChanged.emit(index, index, (Qt.DisplayRole,))
else:
try:
value = eval(value)
if not isinstance(
value,
self._dataframe.applymap(type).iloc[row, col]):
value = self._dataframe.iloc[row, col]
except Exception as e:
value = self._dataframe.iloc[row, col]
self._dataframe.iloc[row, col] = value
self.dataChanged.emit(index, index, (Qt.DisplayRole,))
return True
def flags(self, index):
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
def sort(self, column, order):
self.layoutAboutToBeChanged.emit()
col_name = self._dataframe.columns.tolist()[column]
sheet1 = self._dataframe.iloc[:1, :]
sheet2 = self._dataframe.iloc[1:, :].sort_values(
col_name, ascending=order == Qt.AscendingOrder, inplace=False)
sheet2.reset_index(drop=True, inplace=True)
sheet3 = pd.concat([sheet1, sheet2], ignore_index=True)
self.setDataFrame(sheet3)
self.layoutChanged.emit()
class ComboBoxDelegate(QItemDelegate):
def __init__(self, owner, choices):
super().__init__(owner)
self.items = choices
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.addItems(self.items)
editor.currentIndexChanged.connect(self.currentIndexChanged)
return editor
def paint(self, painter, option, index):
if isinstance(self.parent(), QItemDelegate):
self.parent().openPersistentEditor(0, index)
QItemDelegate.paint(self, painter, option, index)
def setModelData(self, editor, model, index):
value = editor.currentText()
model.setData(index, value, Qt.EditRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
#pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
class MainWindow(QMainWindow):
def __init__(self, pandas_sheet):
super().__init__()
self.pandas_sheet = pandas_sheet
for i in range(1):
self.pandas_sheet.loc[-1] = [''] * len(self.pandas_sheet.columns.values)
self.pandas_sheet.index = self.pandas_sheet.index + 1
self.pandas_sheet = self.pandas_sheet.sort_index()
self.table = QTableView()
self.setCentralWidget(self.table)
delegate = ComboBoxDelegate(self.table,
['m1', 'm2', 'm3'])
model = DataFrameModel(self.pandas_sheet, self)
self.table.setModel(model)
self.table.setSortingEnabled(True)
self.table.setItemDelegateForRow(0, delegate)
for i in range(model.columnCount()):
ix = model.index(0, i)
self.table.openPersistentEditor(ix)
self.table.resizeColumnsToContents()
self.table.resizeRowsToContents()
if __name__ == '__main__':
df = pd.DataFrame({'a': ['col0'] * 5,
'b': np.arange(5),
'c': np.random.rand(5)})
app = QApplication(sys.argv)
window = MainWindow(df)
window.show()
sys.exit(app.exec_())
The image below shows the table before sorting.
The image below after sorting the table by one of the columns.
I would like to have the style for the first row the same before and after sorting by any column. Is this possible?
By default the editors are not displayed unless the user interacts with the items using the events indicated in the flags assigned to editTriggers or if you force them to open them using openPersistentEditor().
Considering the last option you can automate the task shown but for this the view must be accessible from the delegate's paint method since it is always called by what a solution is to pass it as a parent (it seems that you are trying to implement) and use the openPersistentEditor() if it is the view, in your case there is an error since the parent is not a QItemDelegate but inherits from QAbstractItemView, in addition you must pass the QModelIndex.
Considering the above, the solution is:
def paint(self, painter, option, index):
if isinstance(self.parent(), QAbstractItemView):
self.parent().openPersistentEditor(index)
So with the above every time the delegates are repainted (for example after a sorting) openPersistentEditor() will be called making the editors visible.
Update:
The editor must save the information in the QModelIndex roles through setModelData and retrieve them using setEditorData, in your case you do not implement the second one so the editor will not obtain the information when the editor is created again. In addition setModelData saves the information in Qt::EditRole but in your model it does not handle that role so you must use Qt::DisplayRole.
Considering the above, the solution is:
class ComboBoxDelegate(QItemDelegate):
def __init__(self, parent, choices):
super().__init__(parent)
self.items = choices
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.addItems(self.items)
editor.currentIndexChanged.connect(self.currentIndexChanged)
return editor
def paint(self, painter, option, index):
if isinstance(self.parent(), QAbstractItemView):
self.parent().openPersistentEditor(index)
def setModelData(self, editor, model, index):
value = editor.currentText()
model.setData(index, value, Qt.DisplayRole)
def setEditorData(self, editor, index):
text = index.data(Qt.DisplayRole) or ""
editor.setCurrentText(text)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
#pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
Note: I don't believe this is a duplicate of How to programmatically change/update data in Python PyQt4 TableView? as I don't want to insert new rows. I want to edit the table data.
I have changed my code below. The table holds integers in columns titled "2" and "3". When changing the value of the QSpinBox, I want the numbers in the table to get multiplied by the value from the QSpinBox programatically.
Question: How can I programmatically multiply the values in these columns by the spinbox multiplier value?
Note: if the UI doesn't exist, the model still needs to work (without this multiplier feature). Meaning; I believe that the spinbox must initiate model data edit.
import sys
try:
from PySide2 import QtWidgets
except ImportError:
from PyQt5 import QtWidgets
try:
from PySide2 import QtCore
except ImportError:
from PyQt5 import QtCore
class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self, table_data, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.table_data = table_data
def rowCount(self, parent):
return len(self.table_data)
def columnCount(self, parent):
return len(self.table_data[0])
def flags(self, index):
original_flags = super(MyTableModel, self).flags(index)
return original_flags | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
item = index.internalPointer()
if item is not None:
print(item)
value = self.table_data[row][column]
return value
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.table_data[row][column] = value
self.dataChanged.emit(index, index)
return True
return QtCore.QAbstractTableModel.setData(self, index, value, role)
class Widget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
QtWidgets.QWidget.__init__(self, *args, **kwargs)
self.view = QtWidgets.QTableView()
self.setLayout(QtWidgets.QVBoxLayout())
self.layout().addWidget(self.view)
table_data = [['HD', '1920', '1080', 'other', 'stuff', 'here'], ['lowres', '640', '480', 'other', 'stuff', 'here']]
self.proxy_model = QtCore.QSortFilterProxyModel()
self.model = MyTableModel(table_data=table_data)
self.proxy_model.setSourceModel(self.model)
self.proxy_model.setDynamicSortFilter(True)
self.view.setModel(self.proxy_model)
self.proxy_model.dataChanged.connect(self.on_data_changed)
self.view.setSortingEnabled(True) # requires proxy model
self.view.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.view.horizontalHeader().setStretchLastSection(True)
self.view.horizontalHeader().setSectionsMovable(True)
def on_data_changed(self, _from, _to):
model = _from.model() # proxy model
model.blockSignals(True)
for index in self.view.selectionModel().selectedIndexes():
model.setData(index, _from.data())
model.blockSignals(False)
print('data was changed in table')
def change_value(self, col, multiplier):
print('Multiply the value (for all rows) in column %s with: %s' % (col, multiplier))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
m = QtWidgets.QWidget()
w = Widget()
e = QtWidgets.QSpinBox()
e.setValue(1)
e.valueChanged.connect(lambda: w.change_value(col=1, multiplier=e.value()))
e.valueChanged.connect(lambda: w.change_value(col=2, multiplier=e.value()))
l = QtWidgets.QHBoxLayout()
l.addWidget(w)
l.addWidget(e)
m.setLayout(l)
m.show()
sys.exit(app.exec_())
Am I supposed to execute self.proxy_mode.setData() and if so, how?
This is how I solved it. Not completely sure about getting the index via mapFromSource, but it works.
import sys
import copy
try:
from PySide2 import QtWidgets
except ImportError:
from PyQt5 import QtWidgets
try:
from PySide2 import QtCore
except ImportError:
from PyQt5 import QtCore
class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self, table_data, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.table_data = table_data
def rowCount(self, parent):
return len(self.table_data)
def columnCount(self, parent):
return len(self.table_data[0])
def flags(self, index):
original_flags = super(MyTableModel, self).flags(index)
return original_flags | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
item = index.internalPointer()
if item is not None:
print(item)
value = self.table_data[row][column]
return value
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.table_data[row][column] = value
self.dataChanged.emit(index, index)
return True
return QtCore.QAbstractTableModel.setData(self, index, value, role)
class Widget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
QtWidgets.QWidget.__init__(self, *args, **kwargs)
self.view = QtWidgets.QTableView()
self.setLayout(QtWidgets.QVBoxLayout())
self.layout().addWidget(self.view)
table_data = [['HD', '1920', '1080', 'other', 'stuff', 'here'], ['lowres', '640', '480', 'other', 'stuff', 'here']]
self._original_table_data = copy.deepcopy(table_data)
self.proxy_model = QtCore.QSortFilterProxyModel()
self.model = MyTableModel(table_data=table_data)
self.proxy_model.setSourceModel(self.model)
self.proxy_model.setDynamicSortFilter(True)
self.view.setModel(self.proxy_model)
self.proxy_model.dataChanged.connect(self.on_data_changed)
self.view.setSortingEnabled(True) # requires proxy model
self.view.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.view.horizontalHeader().setStretchLastSection(True)
self.view.horizontalHeader().setSectionsMovable(True)
def on_data_changed(self, _from, _to):
model = _from.model() # proxy model
model.blockSignals(True)
for index in self.view.selectionModel().selectedIndexes():
model.setData(index, _from.data())
model.blockSignals(False)
print('data was changed in table')
def change_value(self, col, multiplier):
print('Multiply the value (for all rows) in column %s with: %s' % (col, multiplier))
index = self.proxy_model.mapFromSource(QtCore.QModelIndex())
row_count = self.proxy_model.rowCount(index)
for row in range(0, row_count):
original_value = self._original_table_data[row][col]
index = self.proxy_model.index(row, col)
self.proxy_model.setData(index, int(original_value)*int(multiplier))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
m = QtWidgets.QWidget()
w = Widget()
q = QtWidgets.QLabel('Multiplier:')
e = QtWidgets.QSpinBox()
e.setValue(1)
e.valueChanged.connect(lambda: w.change_value(col=1, multiplier=e.value()))
e.valueChanged.connect(lambda: w.change_value(col=2, multiplier=e.value()))
l = QtWidgets.QHBoxLayout()
l.addWidget(w)
l.addWidget(q)
l.addWidget(e)
m.setLayout(l)
m.show()
sys.exit(app.exec_())
I want to sort a QTableView when I click on the headers of my QHeaderView. I've found a several code sample on the internet like this one: Sort QTableView in pyqt5
but it doesn't work for me. I also look in the Rapid Gui programming Book from Summerfield but I could find something that was working either.
In this quick example how could I sort the table by name or age?
Here is my code :
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import pandas as pd
import operator
# This class was generated from the Qt Creator
class Ui_tableView_ex(object):
def setupUi(self, tableView_ex):
tableView_ex.setObjectName("tableView_ex")
tableView_ex.resize(800, 600)
self.centralwidget = QWidget(tableView_ex)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QGridLayout(self.centralwidget)
self.gridLayout.setObjectName("gridLayout")
self.myTable = QTableView(self.centralwidget)
self.myTable.setObjectName("monTablo")
self.gridLayout.addWidget(self.myTable, 0, 0, 1, 1)
tableView_ex.setCentralWidget(self.centralwidget)
self.retranslateUi(tableView_ex)
QMetaObject.connectSlotsByName(tableView_ex)
def retranslateUi(self, tableView_ex):
_translate = QCoreApplication.translate
tableView_ex.setWindowTitle(_translate("tableView_ex", "MainWindow"))
class TableTest(QMainWindow, Ui_tableView_ex):
def __init__(self, parent=None):
super(TableTest, self).__init__(parent)
self.setupUi(self)
self.model = TableModel()
self.myTable.setModel(self.model)
self.myTable.setShowGrid(False)
self.hView = HeaderView(self.myTable)
self.myTable.setHorizontalHeader(self.hView)
self.myTable.verticalHeader().hide()
# adding alternate colours
self.myTable.setAlternatingRowColors(True)
self.myTable.setStyleSheet("alternate-background-color: rgb(209, 209, 209)"
"; background-color: rgb(244, 244, 244);")
# self.myTable.setSortingEnabled(True)
# self.myTable.sortByColumn(1, Qt.AscendingOrder)
class HeaderView(QHeaderView):
def __init__(self, parent):
QHeaderView.__init__(self, Qt.Horizontal, parent)
self.model = TableModel()
self.setModel(self.model)
# Setting font for headers only
self.font = QFont("Helvetica", 12)
self.setFont(self.font)
# Changing section backgroud color. font color and font weight
self.setStyleSheet("::section{background-color: pink; color: green; font-weight: bold}")
self.setSectionResizeMode(1)
self.setSectionsClickable(True)
class TableModel(QAbstractTableModel):
def __init__(self):
QAbstractTableModel.__init__(self)
super(TableModel, self).__init__()
self.headers = ["Name", "Age", "Grades"]
self.stocks = [["George", "26", "80%"],
["Bob", "16", "95%"],
["Martha", "22", "98%"]]
self.data = pd.DataFrame(self.stocks, columns=self.headers)
def update(self, in_data):
self.data = in_data
def rowCount(self, parent=None):
return len(self.data.index)
def columnCount(self, parent=None):
return len(self.data.columns.values)
def setData(self, index, value, role=None):
if role == Qt.EditRole:
row = index.row()
col = index.column()
column = self.data.columns.values[col]
self.data.set_value(row, column, value)
self.update(self.data)
return True
def data(self, index, role=None):
if role == Qt.DisplayRole:
row = index.row()
col = index.column()
value = self.data.iloc[row, col]
return value
def headerData(self, section, orientation, role=None):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return self.data.columns.values[section]
# -----------------NOT WORKING!!!---------------
# =================================================
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
self.layoutAboutToBeChanged.emit()
self.data = sorted(self.data, key=operator.itemgetter(Ncol))
if order == Qt.DescendingOrder:
self.data.reverse()
self.layoutChanged.emit()
# =================================================
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
app.setStyle(QStyleFactory.create("Fusion"))
main_window = TableTest()
main_window.show()
app.exec_()
sys.exit()
I've try to modify a few things, but nothing worked out and when I uncomment the setSortingEnabled(True) line the window doesn't even open. So there is no way I can know if I'm closer to the solution or not! I would also like to change the text color depending on the grade(red under 50 an green over 50 for example). For that, I haven't search that much, so i will try it by myself before asking aquestion but if you have any hint It would be much appreciated!
Thank you for your help!
Your sort function is the problem, you are not using the pandas DataFrame sorting functions, and self.data become a python list, then other functions fail and the program crash.
To correctly sort the DataFrame use the sort_values function like this:
def sort(self, Ncol, order):
"""Sort table by given column number."""
self.layoutAboutToBeChanged.emit()
self.data = self.data.sort_values(self.headers[Ncol],
ascending=order == Qt.AscendingOrder)
self.layoutChanged.emit()