Remove QComboBox when QCheckBox is toggled off or on using QItemDelegate - python

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.

Related

PyQt5 update values in editable QTableView

I have an editable QTableView with a range of x values, their squares and their cubes.
Is there a way that if any value is changed a signal is launched to update the rest of the cells in the same row?
For instance, if I change the value x = 2 by x = 5, then somehow to know that the change has happened and the code has to update the rest of the values in the row.
I paste in a sample of my original code in case it helps.
from PyQt5.QtCore import QAbstractTableModel, Qt
class PandasModelEditable(QAbstractTableModel):
def __init__(self, data):
QAbstractTableModel.__init__(self)
self._data = data
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()])
column_count = self.columnCount()
for column in range(0, column_count):
if (index.column() == column and role == Qt.TextAlignmentRole):
return Qt.AlignHCenter | Qt.AlignVCenter
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._data.columns[col]
return None
def setData(self, index, value, role):
if not index.isValid():
return False
if role != 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):
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
if __name__ == '__main__':
import sys
import pandas as pd
from PyQt5.QtWidgets import QApplication, QTableView
df = pd.DataFrame({'x': range(5),
'x²': [i**2 for i in range(5)],
'x³': [i**3 for i in range(5)]
})
app = QApplication(sys.argv)
model = PandasModelEditable(df)
view = QTableView()
view.setModel(model)
view.resize(350, 200)
view.show()
sys.exit(app.exec_())
EDITED
Since the kind answers are not 100 % helpful and I populate the QtableView with a pandas DataFrame, I have opened a new post with a new question.
I noted for you the lines in which I made changes.
from PyQt5.QtCore import QAbstractTableModel, Qt
class PandasModelEditable(QAbstractTableModel):
def __init__(self, data):
QAbstractTableModel.__init__(self)
self._data = data
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()])
column_count = self.columnCount()
for column in range(0, column_count):
if (index.column() == column and role == Qt.TextAlignmentRole):
return Qt.AlignHCenter | Qt.AlignVCenter
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._data.columns[col]
return None
def setData(self, index, value, role):
if not value.isdigit(): # +++
return False # +++
if not index.isValid():
return False
if role != 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._data.values[row][1] = int(value)**2 # +++
self._data.values[row][2] = int(value)**3 # +++
self.dataChanged.emit(index, index)
return True
def flags(self, index):
# return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
fl = QAbstractTableModel.flags(self, index) # +++
if index.column() == 0: # +++
fl |= Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable # +++
return fl # +++
if __name__ == '__main__':
import sys
import pandas as pd
from PyQt5.QtWidgets import QApplication, QTableView
df = pd.DataFrame({'x': range(5),
'x²': [i**2 for i in range(5)],
'x³': [i**3 for i in range(5)]
})
app = QApplication(sys.argv)
model = PandasModelEditable(df)
view = QTableView()
view.setModel(model)
view.resize(350, 200)
view.show()
sys.exit(app.exec_())

Change state of checkbox programmatically in qtableview

I found this model to visualize qtableview with checkboxes. It works, but now I want also to change the state of checkboxes programmatically (for example a button that check/uncheck all checkboxes). I have no idea how I can do it...
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class TableModel(QAbstractTableModel):
def __init__(self, parent=None):
super(TableModel, self).__init__(parent)
self.tableData = [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
self.checks = {}
def columnCount(self, *args):
return 3
def rowCount(self, *args):
return 3
def checkState(self, index):
if index in self.checks.keys():
return self.checks[index]
else:
return Qt.Unchecked
def data(self, index, role=Qt.DisplayRole):
row = index.row()
col = index.column()
if role == Qt.DisplayRole:
return '{0}'.format(self.tableData[row][col])
elif role == Qt.CheckStateRole and col == 0:
return self.checkState(QPersistentModelIndex(index))
return None
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
if role == Qt.CheckStateRole:
self.checks[QPersistentModelIndex(index)] = value
return True
return False
def flags(self, index):
fl = QAbstractTableModel.flags(self, index)
if index.column() == 0:
fl |= Qt.ItemIsEditable | Qt.ItemIsUserCheckable
return fl
You have to use the setData() method, in this method the dataChanged signal must be emitted when the value associated with a role changes:
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
if role == Qt.CheckStateRole:
self.checks[QPersistentModelIndex(index)] = value
self.dataChanged.emit(index, index)
return True
return False
If you want to check and uncheck the items you must iterate over the items:
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.button = QPushButton("Checked", checkable=True)
self.button.clicked.connect(self.on_clicked)
self.view = QTableView()
self.model = TableModel(self)
self.view.setModel(self.model)
lay = QVBoxLayout(self)
lay.addWidget(self.button)
lay.addWidget(self.view)
#pyqtSlot(bool)
def on_clicked(self, state):
c = 0
for r in range(self.model.rowCount()):
ix = self.model.index(r, c)
self.model.setData(
ix, Qt.Checked if state else Qt.Unchecked, Qt.CheckStateRole
)
self.button.setText("Unchecked" if state else "Checked")

Sorting QTableView with combo delegates removes delgetes except after double clicking

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())

QTableView model using QComboBox

In this example below I have a simple QTableView which is populated using an AbstractModel. Each row in the table displays information related to a class object called Asset. It has a property called Items that contains a list of strings. I want to know how can i populate the QTableView with a combobox displaying this list of strings for each row.
Secondly when a user changes the item selected in the dropdown, i would like to trigger an event so i can later use it to properly change the color of the colored dot to green or red depending on the object's property called 'Status'
The status would indicate if the Current Version (meaning the latest item in the dropdown list) is the chosen item. If its the last item in the list, meaning the latest item, it would be green, otherwise it's red.
The property 'Active' indicates which item in the dropdownlist is currently selected.
If the status is 0 then it's out dated and if the status is 1 that means the latest version in the dropdownlist is being used.
import sys
from PySide import QtGui, QtCore
class Asset(object):
def __init__(self, name, items=None, status=0, active=0):
self._status = 0
self._name = ''
self._items = []
self._active = active
self.name = name
self.items = items if items != None else []
self.status = status
class AssetModel(QtCore.QAbstractTableModel):
attr = ["Name", "Options"]
def __init__(self, *args, **kwargs):
QtCore.QAbstractTableModel.__init__(self, *args, **kwargs)
self._items = []
def clear(self):
self._items = []
self.reset()
def rowCount(self, index=QtCore.QModelIndex()):
return len(self._items)
def columnCount(self, index=QtCore.QModelIndex()):
return len(self.attr)
def addItem(self, sbsFileObject):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self._items.append(sbsFileObject)
self.endInsertRows()
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return AssetModel.attr[section]
return QtCore.QAbstractTableModel.headerData(self, section, orientation, role)
def getItem(self, index):
row = index.row()
if index.isValid() and 0 <= row < self.rowCount():
return index.data(role=QtCore.Qt.UserRole)
return None
def getSelectedItems(self, selection):
objs = []
for i, index in enumerate(selection):
item = self.getItem(index)
objs.append(item)
return objs
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if 0 <= index.row() < self.rowCount():
item = self._items[index.row()]
col = index.column()
if 0 <= col < self.columnCount():
if role == QtCore.Qt.DisplayRole:
if col == 0:
return getattr(item, 'name', '')
if col == 1:
return (getattr(item, 'items', []))
elif role == QtCore.Qt.UserRole:
if col == 0:
return item
elif role == QtCore.Qt.DecorationRole:
if col == 0:
status = getattr(item, 'status', 0)
col = QtGui.QColor(255,0,0,255)
if status == 1:
col = QtGui.QColor(255,128,0,255)
elif status == 2:
col = QtGui.QColor(255,255,0,255)
px = QtGui.QPixmap(120,120)
px.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(px)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
px_size = px.rect().adjusted(12,12,-12,-12)
painter.setBrush(col)
painter.setPen(QtGui.QPen(QtCore.Qt.black, 4,
QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
painter.drawEllipse(px_size)
painter.end()
return QtGui.QIcon(px)
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.resize(400,300)
# controls
asset_model = QtGui.QSortFilterProxyModel()
asset_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
asset_model.setSourceModel(AssetModel())
self.ui_assets = QtGui.QTableView()
self.ui_assets.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.ui_assets.setModel(asset_model)
self.ui_assets.verticalHeader().hide()
main_layout = QtGui.QVBoxLayout()
main_layout.addWidget(self.ui_assets)
self.setLayout(main_layout)
self.unit_test()
def unit_test(self):
assets = [
Asset('Doug', ['v01', 'v02', 'v03'], 0),
Asset('Amy', ['v10', 'v11', 'v13'], 1),
Asset('Kevin', ['v11', 'v22', 'v53'], 2),
Asset('Leslie', ['v13', 'v21', 'v23'], 0)
]
self.ui_assets.model().sourceModel().clear()
for i, obj in enumerate(assets):
self.ui_assets.model().sourceModel().addItem(obj)
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You have 2 tasks:
Make your model editable because when using the combobox you must edit the values, in addition you must implement new roles to access all the properties of Asset, for it modify the class Asset:
class Asset(object):
def __init__(self, name, items=[], active=0):
self.active = active
self.name = name
self.items = items
#property
def status(self):
return self.active == len(self.items) - 1
To make an editable model, you must implement the setData() method and enable the Qt.ItemIsEditable flag:
class AssetModel(QtCore.QAbstractTableModel):
attr = ["Name", "Options"]
ItemsRole = QtCore.Qt.UserRole + 1
ActiveRole = QtCore.Qt.UserRole + 2
def __init__(self, *args, **kwargs):
QtCore.QAbstractTableModel.__init__(self, *args, **kwargs)
self._items = []
def flags(self, index):
fl = QtCore.QAbstractTableModel.flags(self, index)
if index.column() == 1:
fl |= QtCore.Qt.ItemIsEditable
return fl
def clear(self):
self.beginResetModel()
self._items = []
self.endResetModel()
def rowCount(self, index=QtCore.QModelIndex()):
return len(self._items)
def columnCount(self, index=QtCore.QModelIndex()):
return len(self.attr)
def addItem(self, sbsFileObject):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self._items.append(sbsFileObject)
self.endInsertRows()
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return AssetModel.attr[section]
return QtCore.QAbstractTableModel.headerData(self, section, orientation, role)
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if 0 <= index.row() < self.rowCount():
item = self._items[index.row()]
col = index.column()
if role == AssetModel.ItemsRole:
return getattr(item, 'items')
if role == AssetModel.ActiveRole:
return getattr(item, 'active')
if 0 <= col < self.columnCount():
if role == QtCore.Qt.DisplayRole:
if col == 0:
return getattr(item, 'name', '')
if col == 1:
return getattr(item, 'items')[getattr(item, 'active')]
elif role == QtCore.Qt.DecorationRole:
if col == 0:
status = getattr(item, 'status')
col = QtGui.QColor(QtCore.Qt.red) if status else QtGui.QColor(QtCore.Qt.green)
px = QtGui.QPixmap(120, 120)
px.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(px)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
px_size = px.rect().adjusted(12, 12, -12, -12)
painter.setBrush(col)
painter.setPen(QtGui.QPen(QtCore.Qt.black, 4,
QtCore.Qt.SolidLine,
QtCore.Qt.RoundCap,
QtCore.Qt.RoundJoin))
painter.drawEllipse(px_size)
painter.end()
return QtGui.QIcon(px)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if 0 <= index.row() < self.rowCount():
item = self._items[index.row()]
if role == AssetModel.ActiveRole:
setattr(item, 'active', value)
return True
return QtCore.QAbstractTableModel.setData(self, index, value, role)
Use a delegate, for it you must overwrite the methods createEditor(), setEditorData() and setModelData() where we created the QComboBox, updated the selection of the QComboBox with the information of the model, and updated the model with the selection of the QComboBox. We also use paint() to make the QComboBox persistent.
class AssetDelegate(QtGui.QStyledItemDelegate):
def paint(self, painter, option, index):
if isinstance(self.parent(), QtGui.QAbstractItemView):
self.parent().openPersistentEditor(index)
QtGui.QStyledItemDelegate.paint(self, painter, option, index)
def createEditor(self, parent, option, index):
combobox = QtGui.QComboBox(parent)
combobox.addItems(index.data(AssetModel.ItemsRole))
combobox.currentIndexChanged.connect(self.onCurrentIndexChanged)
return combobox
def onCurrentIndexChanged(self, ix):
editor = self.sender()
self.commitData.emit(editor)
self.closeEditor.emit(editor, QtGui.QAbstractItemDelegate.NoHint)
def setEditorData(self, editor, index):
ix = index.data(AssetModel.ActiveRole)
editor.setCurrentIndex(ix)
def setModelData(self, editor, model, index):
ix = editor.currentIndex()
model.setData(index, ix, AssetModel.ActiveRole)
Then we establish the delegate and pass it as a parent to the QTableView so that it can be persisted automatically:
self.ui_assets.setItemDelegateForColumn(1, AssetDelegate(self.ui_assets))
The complete code can be found at the following link.

QColorDialog and QItemDelegate

I have a QTableView and I'm Using a QItemDelegate in it as button.
I'm trying to change the color of the button, so when i click on it i call a QColorDialog. But then I'm having trouble sending the color it back to the button.
Here's how it is going so far:
the Button and QColorDialog:
class ButtonDelegate(QItemDelegate):
def __init__(self, parent):
QItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
widget = QWidget()
layout = QHBoxLayout()
widget.setLayout(layout)
btn = QPushButton('')
btn.setStyleSheet("background-color:rgb(86,12,65)")
ix = QPersistentModelIndex(index)
btn.clicked.connect(lambda ix = ix : self.onClicked(ix))
layout.addWidget(btn)
layout.setContentsMargins(2,2,2,2)
if not self.parent().indexWidget(index):
self.parent().setIndexWidget(index, widget)
def onClicked(self, ix):
colorPicker = QColorDialog.getColor()
#colorPicker.show()
r = str(colorPicker.red())
g = str(colorPicker.red())
b = str(colorPicker.red())
bgcolor = 'background-color:rgb(' + r + ',' + g + ',' + b +')'
What's the next step? I tried declaring the button in the delegate init as self.btn = QPushButton() and then reuse it at the onCLick Method, but the button don't even get drawn that way.
Some enlightenment?
Thank you!
edit 1
Model:
class Model(QAbstractTableModel):
def __init__(self, vtxTable, parent = None):
QAbstractTableModel.__init__(self, parent)
#data
self.groups = []
#header
self.header_labels = ['Color', 'Group', 'Done']
self.vtxTable = vtxTable
def rowCount(self, parent):
return len(self.groups)
def columnCount(self, parent):
return 3
def flags(self, index):
if index.column() == 2:
fl = fl = Qt.ItemIsEnabled | Qt.ItemIsSelectable
else:
fl = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
return fl
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.header_labels[section]
def removeRow(self, row, parent = QModelIndex()):
self.beginRemoveRows(parent, row, row)
self.groups.remove(self.groups[row])
self.endRemoveRows()
self.vtxTable.setModel(QStringListModel())
def insertRows(self, position, row, values = [] , parent = QModelIndex()):
lastposition = self.rowCount(0)
self.beginInsertRows(parent, lastposition, lastposition)
self.groups.insert(lastposition, values)
self.endInsertRows()
def setData(self, index, value, role = Qt.EditRole):
setIt = False
value = value
row = index.row()
column = index.column()
if role == Qt.EditRole:
setIt = True
if not len(value) == 0:
if value in self.getGrpNames():
warning("Group must have a unique name.")
setIt = False
else:
setIt = True
else:
warning("New group must have a name.")
setIt = False
if role == Qt.BackgroundRole:
setIt = True
if setIt:
self.groups[row][column] = value
self.dataChanged.emit(row, column)
return False
def data(self, index, role):
if not index.isValid():
return
row = index.row()
column = index.column()
if role == Qt.DisplayRole:
if column == 0:
#value = [self.groups[row][column].redF(), self.groups[row][column].greenF(), self.groups[row][column].blueF()]
value = self.groups[row][column]
else:
value = self.groups[row][column]
return value
elif role == Qt.TextAlignmentRole:
return Qt.AlignCenter;
elif role == Qt.EditRole:
index = index
return index.data()
elif role == Qt.BackgroundRole and column == 0:
value = self.groups[row][column]
def getGrpNames(self):
rows = self.rowCount(1)
grps = []
for row in range(rows):
grp = self.index(row, 1).data()
grps.append(grp)
return grps
def getAllVtx(self):
rows = self.rowCount(1)
allVtxs = []
for row in range(rows):
index = self.createIndex(row, 3)
vtxs = index.data()
for vtx in vtxs:
allVtxs.append(vtx)
return allVtxs
def getData(self):
rows = self.rowCount(1)
data = {}
for row in range(rows):
color = self.index(row, 0).data()
grp = self.index(row, 1).data()
done = self.index(row, 2).data()
vtxs = self.groups[row][3]
#index = self.createIndex(row,0)
data[grp] = [grp, color, done, vtxs]
return data
def queryVtx(self, vtx):
data = self.getData()
for key in data:
vtxs = data[key][3]
if vtx in vtxs:
return data[key][0]
else:
return False
Table View:
class Table(QTableView):
def __init__(self, *args, **kwargs):
QTableView.__init__(self, *args, **kwargs)
self.setItemDelegateForColumn(0, colorDelegate(self))
hHeader = self.horizontalHeader()
#hHeader.setSectionResizeMode(QHeaderView.Fixed);
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
vHeader = self.verticalHeader()
vHeader.hide()
When clicking on the first cell of the row I'd like to be able top pick a color for it, and save it in the model.
Thank you.
The goal of the delegates is to customize each item that is displayed in a QAbstractItemView, the other way to do it is by inserting a widget using the indexWidget method. The advantage of the delegates is that the memory consumption is minimal. It is advisable not to use them at the same time.
The delegates have the following methods:
paint(): is the method responsible for drawing the item that is normally displayed
createEditor(): is the method is responsible for creating the editor.
setEditorData(): get the value of the model and establish it in the editor.
setModelData(): save the data obtained from the editor to the model.
An example of that type of delegate is the following:
from PySide2.QtWidgets import *
from PySide2.QtGui import *
from PySide2.QtCore import *
class Delegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
dialog = QColorDialog(parent)
return dialog
def setEditorData(self, editor, index):
color = index.data(Qt.BackgroundRole)
editor.setCurrentColor(color)
def setModelData(self, editor, model, index):
color = editor.currentColor()
model.setData(index, color, Qt.BackgroundRole)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
model = QStandardItemModel(4, 4)
for i in range(model.rowCount()):
for j in range(model.columnCount()):
color = QColor(qrand() % 256, qrand() % 256, qrand() % 256)
it = QStandardItem("{}{}".format(i, j))
it.setData(color, Qt.BackgroundRole)
model.setItem(i, j, it)
w = QTableView()
w.setModel(model)
w.setItemDelegate(Delegate())
w.show()
sys.exit(app.exec_())
Update:
I have improved your code since it had unnecessary elements, the main error is that these Qt::BackgroundRole must return a QColor
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
import warnings
class Model(QAbstractTableModel):
def __init__(self, parent = None):
QAbstractTableModel.__init__(self, parent)
#data
self.groups = []
#header
self.header_labels = ['Color', 'Group', 'Done']
def rowCount(self, parent=QModelIndex()):
return len(self.groups)
def columnCount(self, parent=QModelIndex()):
return 3
def flags(self, index):
fl = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if not index.column() in (2, ):
fl |= Qt.ItemIsEditable
return fl
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.header_labels[section]
def removeRow(self, row, parent = QModelIndex()):
self.beginRemoveRows(parent, row, row)
self.groups.remove(self.groups[row])
self.endRemoveRows()
def insertRows(self, values = [], position=-1):
if position == -1:
position = self.rowCount()
self.beginInsertRows(QModelIndex(), position, position)
self.groups.insert(position, values)
self.endInsertRows()
def setData(self, index, value, role = Qt.EditRole):
setIt = False
row = index.row()
column = index.column()
if role == Qt.EditRole:
setIt = True
if len(value) != 0:
if value in self.getGrpNames():
warning("Group must have a unique name.")
setIt = False
else:
setIt = True
else:
warning("New group must have a name.")
setIt = False
if role == Qt.BackgroundRole:
setIt = True
if setIt:
self.groups[row][column] = value
self.dataChanged.emit(index, index)
return False
def data(self, index, role):
if not index.isValid():
return
row = index.row()
column = index.column()
value = None
if role == Qt.DisplayRole:
if column == 0:
value = self.groups[row][column]
else:
value = self.groups[row][column]
elif role == Qt.TextAlignmentRole:
value = Qt.AlignCenter;
elif role == Qt.BackgroundRole and column == 0:
value = QColor(self.groups[row][column])
return value
def getGrpNames(self):
rows = self.rowCount(1)
grps = []
for row in range(rows):
grp = self.index(row, 1).data()
grps.append(grp)
return grps
def getAllVtx(self):
rows = self.rowCount(1)
allVtxs = []
for row in range(rows):
index = self.createIndex(row, 3)
vtxs = index.data()
for vtx in vtxs:
allVtxs.append(vtx)
return allVtxs
def getData(self):
rows = self.rowCount(1)
data = {}
for row in range(rows):
color = self.index(row, 0).data()
grp = self.index(row, 1).data()
done = self.index(row, 2).data()
vtxs = self.groups[row][3]
#index = self.createIndex(row,0)
data[grp] = [grp, color, done, vtxs]
return data
def queryVtx(self, vtx):
data = self.getData()
for key in data:
vtxs = data[key][3]
if vtx in vtxs:
return data[key][0]
else:
return False
class ColorDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
dialog = QColorDialog(parent)
return dialog
def setEditorData(self, editor, index):
color = index.data(Qt.BackgroundRole)
editor.setCurrentColor(color)
def setModelData(self, editor, model, index):
color = editor.currentColor()
model.setData(index, color, Qt.BackgroundRole)
class Table(QTableView):
def __init__(self, *args, **kwargs):
QTableView.__init__(self, *args, **kwargs)
self.setItemDelegateForColumn(0, ColorDelegate(self))
hHeader = self.horizontalHeader()
#hHeader.setSectionResizeMode(QHeaderView.Fixed);
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
vHeader = self.verticalHeader()
vHeader.hide()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = Table()
model = Model()
w.setModel(model)
model.insertRows(["red", "group1", "no"])
model.insertRows(["blue", "group1", "no"], 0)
w.show()
sys.exit(app.exec_())

Categories

Resources