Creating a column of editable Checkbox in QTableview using QItemDelegate - python

I'm using Pyside2, Python 3.8. I have a QTableView, the value of the first column is a bool, I want to implement a delegate to paint Editable CheckBoxs in the first column. I've tried many SO answers, the one that've been the most helpful is this one.
It partially works, ie: You have to double click the cell, then you get a 'kind of' combobox instead of a checkbox in my first column (see picture below)
I've managed to do this without going through a Delegate, using the CkeckStateRole, it worked, the checkboxs aren't editable, but I have many editable other columns and It seems that there's no escape from implementing a delegate as it is the more proper way to do it.
here's my Table Model class:
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, mlist=None):
super(TableModel, self).__init__()
self._items = [] if mlist == None else mlist
self._header = []
def rowCount(self, parent = QtCore.QModelIndex):
return len(self._items)
def columnCount(self, parent = QtCore.QModelIndex):
return len(self._header)
def flags(self, index):
if index.column() == 0:
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled
return QtCore.Qt.ItemIsEnabled
def data(self, index, role = QtCore.Qt.DisplayRole):
if not index.isValid():
return None
elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self._items[index.row()][index.column()]
elif role == QtCore.Qt.CheckStateRole:
return None
else:
return None
def setData(self, index, value, role = QtCore.Qt.EditRole):
if value is not None and role == QtCore.Qt.EditRole:
self._items[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
return False
Here's my CheckBox Delegate class:
class CheckBoxDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent = None):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
if not (QtCore.Qt.ItemIsEditable & index.flags()):
return None
check = QtWidgets.QCheckBox(parent)
check.clicked.connect(self.stateChanged)
return check
def setEditorData(self, editor, index):
editor.blockSignals(True)
editor.setChecked(index.data())
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.isChecked(), QtCore.Qt.EditRole)
def paint(self, painter, option, index):
value = index.data()
if value:
value = QtCore.Qt.Checked
else:
value = QtCore.Qt.Unchecked
self.drawCheck(painter, option, option.rect, value)
self.drawFocus(painter, option, option.rect)
#QtCore.Slot()
def stateChanged(self):
print("sender", self.sender())
self.commitData.emit(self.sender())
Edit #1
After some research, I've found out that I've forgot to put self.MyTableView.setItemDelegateForColumn(0, CheckBoxDelegate(self)) in my MainWindow __init__
Well, What happens now is I get a centered CheckBox in my first column, I can't edit it, but, when I double click it, It creates a second Checkbox in the same cell at the left, which is Editable and also sets the new CheckState to my Model.
Let's say my centered CheckBox is set to True, I double click, a second Checked CheckBox is created to its left, I Uncheck it, I click on another cell, the second checkBox disappears, the centered checkBox is set to Unchecked, and my ModelData is updated.
(see picture below for what happens after double clicking the centered CheckBox)
Fix attempts
1. Not overriding paint():
The column contains either True or false, double clicking the cell creates an editable checkbox which sets the data in the model to the new value and then disappears.
2. the createEditor() method returns None
def createEditor(self, parent, option, index):
return None
It creates a single centered checkbox, no labels, but the checkbox isn't editable
3. Not overriding createEditor() method
Creates a single centered checkbox with no labels, double clicking the cell replace the checkbox with a combobox (True or False), from which I can change the CheckBox state.
Neither of those fixes gave me what I'm looking for: A signe centered checkbox created that is editable with a single click

There is absolutely no need for a custom delegate, as it's already supported by the default one. You just need to correctly return the ItemIsUserCheckable flag, and implement the CheckStateRole in both data() and setData().
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, mlist=None, checkableColumns=None):
super(TableModel, self).__init__()
self._items = [] if mlist == None else mlist
self._header = ['aaaa', 'bbb', 'ccc']
if checkableColumns is None:
checkableColumns = []
elif isinstance(checkableColumns, int):
checkableColumns = [checkableColumns]
self.checkableColumns = set(checkableColumns)
def setColumnCheckable(self, column, checkable=True):
if checkable:
self.checkableColumns.add(column)
else:
self.checkableColumns.discard(column)
self.dataChanged.emit(
self.index(0, column), self.index(self.rowCount() - 1, column))
def rowCount(self, parent = QtCore.QModelIndex):
return len(self._items)
def columnCount(self, parent = QtCore.QModelIndex):
return len(self._header)
def flags(self, index):
flags = QtCore.Qt.ItemIsEnabled
if index.column() in self.checkableColumns:
# only this flag is required
flags |= QtCore.Qt.ItemIsUserCheckable
return flags
def data(self, index, role = QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if (role == QtCore.Qt.CheckStateRole and
index.column() in self.checkableColumns):
value = self._items[index.row()][index.column()]
return QtCore.Qt.Checked if value else QtCore.Qt.Unchecked
elif (index.column() not in self.checkableColumns and
role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole)):
return self._items[index.row()][index.column()]
else:
return None
def setData(self, index, value, role = QtCore.Qt.EditRole):
if (role == QtCore.Qt.CheckStateRole and
index.column() in self.checkableColumns):
self._items[index.row()][index.column()] = bool(value)
self.dataChanged.emit(index, index)
return True
if value is not None and role == QtCore.Qt.EditRole:
self._items[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
return False
Then, if you want to center the checkbox whenever the index has no text, you can simply use a proxy style:
class ProxyStyle(QtWidgets.QProxyStyle):
def subElementRect(self, element, opt, widget=None):
if element == self.SE_ItemViewItemCheckIndicator and not opt.text:
rect = super().subElementRect(element, opt, widget)
rect.moveCenter(opt.rect.center())
return rect
return super().subElementRect(element, opt, widget)
# ...
app.setStyle(ProxyStyle())

Related

PyQt: Dragging item from list view and dropping to table view, the drop index is always -1

In my code I have successfully implemented Drag n Drop from one QListView to another QListView and internal move is also working fine.
Now, due to a necessity, I have modified view that accepts drops i.e. I am trying to Drag from QListView to QTableView instead.
The issue is, when I dropped on QTableView, every time it prints invalid index i.e. -1 (printing that in dropEvent().
In my previous implementation between list views, even when the item was dropped in between items, the index was appropriately updated. This is not the case here. Thanks for answering.
class SerialTestStepListView(QtGui.QTableView):
itemSelectionChanged = pyqtSignal()
casualSignal3 = pyqtSignal()
casualSignal4 = pyqtSignal()
def __init__(self,parent = None):
QListView.__init__(self, parent)
self.setAcceptDrops(True)
# Hide column here......
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(self.InternalMove)
self.setSelectionBehavior(self.SelectRows)
def dragEnterEvent(self, event):
if event.mimeData().hasFormat("application/xml-chirag"):
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
if event.mimeData().hasFormat("application/xml-chirag"):
event.setDropAction(QtCore.Qt.MoveAction)
event.accept()
else:
event.ignore()
def dropEvent(self, event):
data = event.mimeData()
bstream = data.retrieveData("application/xml-chirag", QtCore.QVariant.ByteArray)
selected = pickle.loads(bstream)
index = self.indexAt(event.pos()).row()
print("into the drop event")
print(index) # This is printing -1
print(self.indexAt(event.pos()))
self.emit(SIGNAL("casualSignal3"),selected, index)
event.accept()
def startDrag(self, event):
indx = self.indexAt(event.pos())
index = indx.row()
print("into the drag event")
self.emit(SIGNAL("casualSignal4"),indx, index)
if not indx.isValid():
pass
else:
return True
def mouseMoveEvent(self, event):
self.startDrag(event)
Model is:
class SerialTestListModel(QtCore.QAbstractListModel):
def __init__(self, testStep = [], parent = None):
QtCore.QAbstractListModel.__init__(self, parent)
self.__TestSteps = testStep
def rowCount(self, parent):
return len(self.__TestSteps)
def flags(self, index):
if index.isValid():
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | QtCore.Qt.ItemIsEnabled | QtCore.Qt.MoveAction
else:
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | QtCore.Qt.ItemIsEnabled
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
return self.__TestSteps[row]
def setData(self, index, value, role = QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
self.dataChanged.emit(index,index)
return True
return False
def insertRows(self, position, rows, selected, parent = QtCore.QModelIndex()):
if selected is None:
pass
else:
if ((position == -1) and (selected is not None)):
position = self.rowCount(parent)
self.beginInsertRows(parent, position, position + rows - 1)
for i in range(rows):
self.__TestSteps.insert(position, selected)
self.endInsertRows()
return True
def removeRows(self, position, rows, parent = QtCore.QModelIndex()):
if position == -1:
pass
else:
self.beginRemoveRows(parent, position, position + rows - 1)
for i in range(rows):
value = self.__TestSteps[position]
self.__TestSteps.remove(value)
self.endRemoveRows()
return True
Controller part:
SerialTestStepListViewHdlr = CTC.SerialTestStepListView()
SerialTestStepListViewHdlr.show()
SerialTestStepListViewHdlr.connect(SerialTestStepListViewHdlr, SIGNAL("casualSignal3"), acceptDrag)
def acceptDrag(selected, index):
SerialTestStepListModel = mod.SerialTestListModel(testStep)
#selected = str(selected)
SerialTestStepListModel.insertRows(index, 1, selected)
SerialTestStepListViewHdlr.setModel(SerialTestStepListModel)
This was resolved after replacing:
index = self.indexAt(event.pos()).row()
with
index = self.rowAt(event.pos().y())
in dropEvent() method.

How to query the main app's widget from inside of model's method

The code below creates QTableView linked to QAbstractTableModel.
When "Show All" QCheckBox is checked QTableView should show all seven Items: 'One','Two','Three','Four','Five','Six' and 'Seven'.
But when unchecked QTableView should only show odd indexed Items.
To accomplish it first the state of QCheckBox needs to be queried from the inside of model's data() method. But since self.checkBox is declared inside of Window class it is not currently possible.
Question: How to query the self.checkBox state from inside of model's methods?
import sys, os
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items=['One','Two','Three','Four','Five','Six','Seven']
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def data(self, index, role):
if not index.isValid() or not (0<=index.row()<len(self.items)):
return QtCore.QVariant()
item=str(self.items[index.row()])
if role==QtCore.Qt.UserRole:
return item
if role==QtCore.Qt.DisplayRole:
return item
if role==QtCore.Qt.TextColorRole:
return QtCore.QVariant(QtGui.QColor(QtCore.Qt.white))
if role == QtCore.Qt.BackgroundRole:
if index.row()%2:
return QtCore.QVariant(QtGui.QColor(QtCore.Qt.gray))
else:
return QtCore.QVariant(QtGui.QColor(QtCore.Qt.darkGray))
if role == QtCore.Qt.TextAlignmentRole:
return (QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
def headerData(self, column, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.TextAlignmentRole:
return (QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
if role == QtCore.Qt.BackgroundRole:
return QtCore.QVariant(QtGui.QColor(QtCore.Qt.blue))
if role == QtCore.Qt.ForegroundRole:
if orientation == QtCore.Qt.Horizontal:
return QtCore.QVariant(QtGui.QColor(QtCore.Qt.red))
elif orientation == QtCore.Qt.Vertical:
return QtCore.QVariant(QtGui.QColor(QtCore.Qt.green))
if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal:
return QtCore.QString('Horizont Column')
if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Vertical:
return QtCore.QString('Vertical Column')
if role == QtCore.Qt.FontRole:
return QtGui.QFont('Times', pointSize=12, weight=-1, italic=True)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
mainLayout=QtGui.QHBoxLayout()
self.setLayout(mainLayout)
self.viewA=QtGui.QTableView()
self.viewA.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch)
myModel=TableModel()
self.viewA.setModel(myModel)
mainLayout.addWidget(self.viewA)
self.checkBox=QtGui.QCheckBox("Show All")
self.checkBox.setChecked(True)
mainLayout.addWidget(self.checkBox)
self.show()
view=Window()
sys.exit(app.exec_())
This is not a Qt related question. Is a code design problem.
You don't want your model knowing anything about your views. Imagine that in the future you replace your check box with other kind of view (combo box, radio group, etc), your will have to change your model...
You have to listen to changes in your check box, and then, based on the check box status, your controller should call yourmodel.loadAll() or yourmodel.loadOdd(). Using the right methods on your model, your 'TableView' will show the results automatically.
You should read something about MVC.

how to set default value of Qdatetimeedit used as qdelegate

I'm using QDateTimeEdit as a delegate on my QTableview to show start date and end date.
when I try to populate data I receive from database, QDateTimeEdit delegate does not display it.
Here is my code:
Class DateDelegate:
class DateDelegate(QtGui.QItemDelegate):
def __init__(self, parent):
QtGui.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
self.dateEdit = QtGui.QDateTimeEdit(parent)
self.dateEdit.setCalendarPopup(True)
self.dateEdit.setMinimumDate(QtCore.QDate(2014, 03, 01))
self.dateEdit.setDisplayFormat(_translate("Form", "dd/mm/yyyy", None))
return self.dateEdit
def setModelData(self, editor, model, index):
value = self.dateEdit.dateTime().toPyDateTime()
strDate = value.strftime('%d/%m/%Y')
model.setData(index, strDate, QtCore.Qt.EditRole)
Class AssetTableModel:
class AssetTableModel(QtCore.QAbstractTableModel):
def __init__(self, assets = [], headers = [], parent = None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__assets = assets
self.__headers = headers
def rowCount(self, parent):
return len(self.__assets)
def columnCount(self, parent):
return len(self.__assets[0])
def flags(self, index):
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
def data(self, index, role):
row = index.row()
column = index.column()
if role == QtCore.Qt.EditRole:
return self.__assets[row][column]
if role == QtCore.Qt.DisplayRole:
print self.__assets[row][column]
return self.__assets[row][column]
def setData(self, index, value, role = QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.__assets[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if section < len(self.__headers):
return self.__headers[section]
else:
return "not implimented"
else:
return "verticle not implimented"
def insertRows(self, position, rows, parent = QtCore.QModelIndex()):
self.beginInsertRows( parent, position, position + rows - 1 )
for i in range(rows):
defaultValues = [ "" for c in range( self.columnCount( None ) ) ]
self.__assets.insert( position, defaultValues )
self.endInsertRows()
return True
Class AssetWidget:
class AssetWidget(QtGui.QDialog):
def __init__(self, parent = None):
super(AssetWidget, self).__init__(parent)
uic.loadUi(uipath+'/AssetTable.ui', self)
# DB call here
self.loadAssetData()
# db call ends here
self.model = None
self.fillCombo(self.assetType)
self.cellDelegate = CellDelegate(self)
for i in range(10):
self.assetTV.setItemDelegateForColumn(i, self.cellDelegate)
self.sDateDelegate = DateDelegate(self)
self.assetTV.setItemDelegateForColumn(10, self.sDateDelegate )
self.assetTV.setItemDelegateForColumn(11, self.sDateDelegate)
self.connect(self.assettypeCB, QtCore.SIGNAL("currentIndexChanged(int)"), self.loadAssets )
self.connect(self.closeBTN , QtCore.SIGNAL("clicked()"), self.close )
self.connect(self.addRowBTN, QtCore.SIGNAL("clicked()"), self.addRow )
self.connect(self.assetTV, QtCore.SIGNAL("doubleClicked(QModelIndex)"), self.tableEdited )
self.show()
I think you are missing the setEditorData() method in your ItemDelegate.
From your attached sourcecode, I assume you are storing the date as a string? In my opinion, it is better to use a QDateTime object to store your date/time. If you do this, you do not need a ItemDelegate to provide an appropriate editor, because Qt knows which editor it needs to provide for this kind of datatype. (see Qt Documentation - Standard Editing Widgets.
However, if you still want to store your date as a string, see this sample program below on how to use delegtes.
from PyQt4 import QtCore
from PyQt4 import QtGui
import sys
class myModel(QtCore.QAbstractTableModel):
def __init__(self, parent):
QtCore.QAbstractTableModel.__init__(self, parent)
self.lst = []
#populate with a few dummy dates
#store dates as str values
dateTime = QtCore.QDateTime.currentDateTime()
for i in range(10):
strDate = dateTime.toString("dd/mm/yyyy")
self.lst.append([strDate])
dateTime = dateTime.addDays(1)
def rowCount(self, parent = QtCore.QModelIndex()):
return len(self.lst)
def columnCount(self, parent = QtCore.QModelIndex()):
return 1
def data(self, index, role = QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
if role == QtCore.Qt.DisplayRole:
return self.lst[row][col]
if role == QtCore.Qt.EditRole:
return self.lst[row][col]
def setData(self, index, value, role = QtCore.Qt.EditRole):
row = index.row()
col = index.column()
self.lst[row][col] = value
def flags(self, index):
return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
class DateDelegate(QtGui.QItemDelegate):
def __init__(self, parent):
QtGui.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
dateTimeEdit = QtGui.QDateTimeEdit(parent) #create new editor
#set properties of editor
dateTimeEdit.setDisplayFormat("dd/mm/yyyy")
dateTimeEdit.setCalendarPopup(True)
return dateTimeEdit
def setModelData(self, editor, model, index):
value = editor.dateTime().toString("dd/mm/yyyy")
model.setData(index, value)
def setEditorData(self, editor, index):
value = index.model().data(index, QtCore.Qt.EditRole)
qdate = QtCore.QDateTime().fromString(value, "dd/mm/yyyy")
editor.setDateTime(qdate)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
table = QtGui.QTableView()
data = myModel(table)
table.setModel(data)
d = DateDelegate(table)
table.setItemDelegateForColumn(0, d)
table.resize(800, 600)
table.show()
sys.exit(app.exec_())

Creating a model/view interface with sliders using PyQt

I have a GUI that consists of a number of sliders, and instead of updating the sliders manually when the underlying data changes, I'd like to store the data in a subclass of QAbstractListModel and have the slider positions update automatically. My subclass looks like this:
from PyQt4 import QtCore
class myDataModel(QtCore.QAbstractListModel):
def __init__(self, initData, parent=None):
super(myDataModel, self).__init__(parent)
self.__data = initData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if index.row() > len(self.__data):
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.__data[index.row()]
return None
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__data)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid() or role != QtCore.Qt.EditRole:
return False
self.__data[index.row()] = value
self.dataChanged.emit(index, index)
return True
How can I connect this model to the sliders in my GUI so that when the data in the model is changed, the sliders change, and vice versa?
Edit: Here is a mockup of the basic interface I have been working on:
Edit: I still haven't been able to get this to work. Here is my model class:
class dataModel(QtCore.QAbstractListModel):
def __init__(self, initData, parent=None):
super(dataModel, self).__init__(parent)
self.__data = initData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if index.row() > len(self.__data):
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.__data[index.row()]
return None
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__data)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid() or role != QtCore.Qt.EditRole:
return False
self.__data[index.row()] = value
self.dataChanged.emit(index, index)
return True
Here is the Delegate class:
class sliderDelegate(QtGui.QItemDelegate):
'''
classdocs
'''
def __init__(self, parent=None):
'''
Constructor
'''
super(sliderDelegate, self).__init__(parent)
def setEditorData(self, editor, index):
editor.setValue(index.model().data(index, QtCore.Qt.EditRole))
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), QtCore.Qt.EditRole)
And here is the setup code:
self._model = dataModel([0 for i in xrange(20)])
self._parameterMapper = QtGui.QDataWidgetMapper(mainWindowInstance)
self._parameterMapper.setModel(self._model)
self._parameterMapper.setItemDelegate(sliderDelegate(mainWindowInstance))
self._parameterMapper.addMapping(self._mainWindowInstance.ui.mySlider, 0)
self._parameterMapper.toFirst()
Unfortunately I get the following error when toFirst() is called:
editor.setValue(index.model().data(index, QtCore.Qt.EditRole))
AttributeError: 'NoneType' object has no attribute 'data'
Any help would be appreciated.
So I haven't used QDataWidgetMapper. It does look interesting, but looks more useful for when you want to have multiple widgets updated to a particular row in a model (and be able to switch between rows easily), rather than each row of a model corresponding to the value of a widget (which I think is what you are after).
So this is my rather rough implementation. Hopefully you'll be able to extend it to your application (might need a bit more error checking added, and maybe the ability to link multiple sliders to a single model row, and possibly then extending to other types of widgets)
When you drag the slider, the model is updated to the sliders new value. I've also added a text box where you can type in a number, and click the button, which will set the model to a specific value. You will notice the slider will update to this value!
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
main_layout = QtGui.QVBoxLayout()
# Create the model
self.model = MyModel()
# Create a slider and link it to the model
self.slider1 = QtGui.QSlider()
self.model.add_slider(self.slider1)
main_layout.addWidget(self.slider1)
# Add a lineEdit and button to force update the model
# Note that the LineEdit is not linked to the model, so won't update with the slider
self.edit = QtGui.QLineEdit()
button = QtGui.QPushButton('update model')
button.clicked.connect(self.on_clicked)
main_layout.addWidget(self.edit)
main_layout.addWidget(button)
self.setLayout(main_layout)
def on_clicked(self):
self.model.update_model(int(self.edit.text()),self.slider1)
class MyModel(QtGui.QStandardItemModel):
def __init__(self,*args,**kwargs):
super(MyModel,self).__init__(*args,**kwargs)
self._slider_list = {}
self.itemChanged.connect(self.on_item_changed)
def add_slider(self,slider):
if slider in self._slider_list:
raise Exception('You cannot link a slider to the model twice')
item = QtGui.QStandardItem(str(slider.value()))
self._slider_list[slider] = item
self.appendRow(item)
slider.valueChanged.connect(lambda value: self.update_model(value,slider))
def update_model(self,value,slider):
if str(value) != self._slider_list[slider].text():
self._slider_list[slider].setText(str(value))
print 'update_model: %d'%value
def on_item_changed(self,item):
slider = self._slider_list.keys()[self._slider_list.values().index(item)]
if slider.value() != int(item.text()):
slider.setValue(int(item.text()))
print 'on_item_changed: %s'%item.text()
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Hope that helps!

PyQt - Column of Checkboxes in a QTableView

I am dynamically creating a QTableView from a Pandas dataframe. I have example code here.
I can create the table, with the checkboxes but I cannot get the checkboxes to reflect the model data, or even to change at all to being unchecked.
I am following example code from this previous question and taking #raorao answer as a guide. This will display the boxes in the table, but non of the functionality is working.
Can anyone suggest any changes, or what is wrong with this code. Why is it not reflecting the model, and why can it not change?
Do check out my full example code here.
Edit one : Update after comment from Frodon :
corrected string cast to bool with a comparison xxx == 'True'
class CheckBoxDelegate(QtGui.QStyledItemDelegate):
"""
A delegate that places a fully functioning QCheckBox in every
cell of the column to which it's applied
"""
def __init__(self, parent):
QtGui.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
'''
Important, otherwise an editor is created if the user clicks in this cell.
** Need to hook up a signal to the model
'''
return None
def paint(self, painter, option, index):
'''
Paint a checkbox without the label.
'''
checked = index.model().data(index, QtCore.Qt.DisplayRole) == 'True'
check_box_style_option = QtGui.QStyleOptionButton()
if (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
check_box_style_option.state |= QtGui.QStyle.State_Enabled
else:
check_box_style_option.state |= QtGui.QStyle.State_ReadOnly
if checked:
check_box_style_option.state |= QtGui.QStyle.State_On
else:
check_box_style_option.state |= QtGui.QStyle.State_Off
check_box_style_option.rect = self.getCheckBoxRect(option)
# this will not run - hasFlag does not exist
#if not index.model().hasFlag(index, QtCore.Qt.ItemIsEditable):
#check_box_style_option.state |= QtGui.QStyle.State_ReadOnly
check_box_style_option.state |= QtGui.QStyle.State_Enabled
QtGui.QApplication.style().drawControl(QtGui.QStyle.CE_CheckBox, check_box_style_option, painter)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton or presses
Key_Space or Key_Select and this cell is editable. Otherwise do nothing.
'''
print 'Check Box editor Event detected : '
if not (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
print 'Check Box edior Event detected : passed first check'
# Do not change the checkbox-state
if event.type() == QtCore.QEvent.MouseButtonRelease or event.type() == QtCore.QEvent.MouseButtonDblClick:
if event.button() != QtCore.Qt.LeftButton or not self.getCheckBoxRect(option).contains(event.pos()):
return False
if event.type() == QtCore.QEvent.MouseButtonDblClick:
return True
elif event.type() == QtCore.QEvent.KeyPress:
if event.key() != QtCore.Qt.Key_Space and event.key() != QtCore.Qt.Key_Select:
return False
else:
return False
# Change the checkbox-state
self.setModelData(None, model, index)
return True
Here's a port of the same code above for PyQt5.
Posting here as there doesn't appear to be another example of a working CheckBox Delegate in QT5, and i was tearing my hair out trying to get it working. Hopefully it's useful to someone.
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QModelIndex
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QApplication, QTableView
class CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
"""
Important, otherwise an editor is created if the user clicks in this cell.
"""
return None
def paint(self, painter, option, index):
"""
Paint a checkbox without the label.
"""
self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
'''
if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
# Change the checkbox-state
self.setModelData(None, model, index)
return True
return False
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
model.setData(index, 1 if int(index.data()) == 0 else 0, QtCore.Qt.EditRole)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
model = QStandardItemModel(4, 3)
tableView = QTableView()
tableView.setModel(model)
delegate = CheckBoxDelegate(None)
tableView.setItemDelegateForColumn(1, delegate)
for row in range(4):
for column in range(3):
index = model.index(row, column, QModelIndex())
model.setData(index, 1)
tableView.setWindowTitle("Check Box Delegate")
tableView.show()
sys.exit(app.exec_())
I've found a solution for you. The trick was:
to write the setData method of the model
to always return a QVariant in the data method
Here it is. (I had to create a class called Dataframe, to make the example work without pandas. Please replace all the if has_pandas statements by yours):
from PyQt4 import QtCore, QtGui
has_pandas = False
try:
import pandas as pd
has_pandas = True
except:
pass
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None, *args):
super(TableModel, self).__init__()
self.datatable = None
self.headerdata = None
def update(self, dataIn):
print 'Updating Model'
self.datatable = dataIn
print 'Datatable : {0}'.format(self.datatable)
if has_pandas:
headers = dataIn.columns.values
else:
headers = dataIn.columns
header_items = [
str(field)
for field in headers
]
self.headerdata = header_items
print 'Headers'
print self.headerdata
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.datatable.index)
def columnCount(self, parent=QtCore.QModelIndex()):
if has_pandas:
return len(self.datatable.columns.values)
else:
return len(self.datatable.columns)
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
i = index.row()
j = index.column()
return QtCore.QVariant('{0}'.format(self.datatable.iget_value(i, j)))
else:
return QtCore.QVariant()
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
if index.column() == 4:
self.datatable.iset_value(index.row(), 4, value)
return value
return value
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return '{0}'.format(self.headerdata[col])
def flags(self, index):
if index.column() == 4:
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled
else:
return QtCore.Qt.ItemIsEnabled
class TableView(QtGui.QTableView):
"""
A simple table to demonstrate the QComboBox delegate.
"""
def __init__(self, *args, **kwargs):
QtGui.QTableView.__init__(self, *args, **kwargs)
self.setItemDelegateForColumn(4, CheckBoxDelegate(self))
class CheckBoxDelegate(QtGui.QStyledItemDelegate):
"""
A delegate that places a fully functioning QCheckBox in every
cell of the column to which it's applied
"""
def __init__(self, parent):
QtGui.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
'''
Important, otherwise an editor is created if the user clicks in this cell.
** Need to hook up a signal to the model
'''
return None
def paint(self, painter, option, index):
'''
Paint a checkbox without the label.
'''
checked = index.data().toBool()
check_box_style_option = QtGui.QStyleOptionButton()
if (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
check_box_style_option.state |= QtGui.QStyle.State_Enabled
else:
check_box_style_option.state |= QtGui.QStyle.State_ReadOnly
if checked:
check_box_style_option.state |= QtGui.QStyle.State_On
else:
check_box_style_option.state |= QtGui.QStyle.State_Off
check_box_style_option.rect = self.getCheckBoxRect(option)
# this will not run - hasFlag does not exist
#if not index.model().hasFlag(index, QtCore.Qt.ItemIsEditable):
#check_box_style_option.state |= QtGui.QStyle.State_ReadOnly
check_box_style_option.state |= QtGui.QStyle.State_Enabled
QtGui.QApplication.style().drawControl(QtGui.QStyle.CE_CheckBox, check_box_style_option, painter)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton or presses
Key_Space or Key_Select and this cell is editable. Otherwise do nothing.
'''
print 'Check Box editor Event detected : '
print event.type()
if not (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
print 'Check Box editor Event detected : passed first check'
# Do not change the checkbox-state
if event.type() == QtCore.QEvent.MouseButtonPress:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease or event.type() == QtCore.QEvent.MouseButtonDblClick:
if event.button() != QtCore.Qt.LeftButton or not self.getCheckBoxRect(option).contains(event.pos()):
return False
if event.type() == QtCore.QEvent.MouseButtonDblClick:
return True
elif event.type() == QtCore.QEvent.KeyPress:
if event.key() != QtCore.Qt.Key_Space and event.key() != QtCore.Qt.Key_Select:
return False
else:
return False
# Change the checkbox-state
self.setModelData(None, model, index)
return True
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
print 'SetModelData'
newValue = not index.data().toBool()
print 'New Value : {0}'.format(newValue)
model.setData(index, newValue, QtCore.Qt.EditRole)
def getCheckBoxRect(self, option):
check_box_style_option = QtGui.QStyleOptionButton()
check_box_rect = QtGui.QApplication.style().subElementRect(QtGui.QStyle.SE_CheckBoxIndicator, check_box_style_option, None)
check_box_point = QtCore.QPoint (option.rect.x() +
option.rect.width() / 2 -
check_box_rect.width() / 2,
option.rect.y() +
option.rect.height() / 2 -
check_box_rect.height() / 2)
return QtCore.QRect(check_box_point, check_box_rect.size())
###############################################################################################################################
class Dataframe(dict):
def __init__(self, columns, values):
if len(values) != len(columns):
raise Exception("Bad values")
self.columns = columns
self.values = values
self.index = values[0]
super(Dataframe, self).__init__(dict(zip(columns, values)))
pass
def iget_value(self, i, j):
return(self.values[j][i])
def iset_value(self, i, j, value):
self.values[j][i] = value
if __name__=="__main__":
from sys import argv, exit
class Widget(QtGui.QWidget):
"""
A simple test widget to contain and own the model and table.
"""
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
l=QtGui.QVBoxLayout(self)
cdf = self.get_data_frame()
self._tm=TableModel(self)
self._tm.update(cdf)
self._tv=TableView(self)
self._tv.setModel(self._tm)
for row in range(0, self._tm.rowCount()):
self._tv.openPersistentEditor(self._tm.index(row, 4))
self.setGeometry(300, 300, 550, 200)
l.addWidget(self._tv)
def get_data_frame(self):
if has_pandas:
df = pd.DataFrame({'Name':['a','b','c','d'],
'First':[2.3,5.4,3.1,7.7], 'Last':[23.4,11.2,65.3,88.8], 'Class':[1,1,2,1], 'Valid':[True, False, True, False]})
else:
columns = ['Name', 'First', 'Last', 'Class', 'Valid']
values = [['a','b','c','d'], [2.3,5.4,3.1,7.7], [23.4,11.2,65.3,88.8], [1,1,2,1], [True, False, True, False]]
df = Dataframe(columns, values)
return df
a=QtGui.QApplication(argv)
w=Widget()
w.show()
w.raise_()
exit(a.exec_())
I added
if event.type() == QEvent.MouseButtonPress or event.type() == QEvent.MouseMove:
return False
to prevent checkbox from flickering when moving the mouse

Categories