I'm using a table to control the visibility and color of a plot. I would like a checkbox to toggle visibility and a drop-down to select the color. To this end, I have something like the following. It feels as through having a persistent editor prevents use of the checkbox.
The example is a bit contrived (in how the model/view are set up), but illustrates how the checkbox doesn't function while the editor is open.
How can I have a checkbox that can be used alongside a visible combobox? Is it better to use two columns?
import sys
from PyQt5 import QtWidgets, QtCore
class ComboDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent):
super().__init__(parent=parent)
def createEditor(self, parent, option, index):
combo = QtWidgets.QComboBox(parent)
li = []
li.append("Red")
li.append("Green")
li.append("Blue")
li.append("Yellow")
li.append("Purple")
li.append("Orange")
combo.addItems(li)
combo.currentIndexChanged.connect(self.currentIndexChanged)
return combo
def setEditorData(self, editor, index):
editor.blockSignals(True)
data = index.model().data(index)
if data:
idx = int(data)
else:
idx = 0
editor.setCurrentIndex(0)
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentIndex())
#QtCore.pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
class PersistentEditorTableView(QtWidgets.QTableView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
QtCore.pyqtSlot('QVariant', 'QVariant')
def data_changed(self, top_left, bottom_right):
for row in range(len(self.model().tableData)):
self.openPersistentEditor(self.model().index(row, 0))
class TableModel(QtCore.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 QtCore.Qt.Unchecked
def data(self, index, role=QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
if role == QtCore.Qt.DisplayRole:
return '{0}'.format(self.tableData[row][col])
elif role == QtCore.Qt.CheckStateRole and col == 0:
return self.checkState(QtCore.QPersistentModelIndex(index))
return None
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid():
return False
if role == QtCore.Qt.CheckStateRole:
self.checks[QtCore.QPersistentModelIndex(index)] = value
self.dataChanged.emit(index, index)
return True
return False
def flags(self, index):
fl = QtCore.QAbstractTableModel.flags(self, index)
if index.column() == 0:
fl |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
return fl
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
view = PersistentEditorTableView()
view.setItemDelegateForColumn(0, ComboDelegate(view))
model = TableModel()
view.setModel(model)
model.dataChanged.connect(view.data_changed)
model.layoutChanged.connect(view.data_changed)
index = model.createIndex(0, 0)
persistet_index = QtCore.QPersistentModelIndex(index)
model.checks[persistet_index] = QtCore.Qt.Checked
view.data_changed(index, index)
view.show()
sys.exit(app.exec_())
NOTE: After some rethinking and analysis of the Qt source code, I realized that my original answer, while valid, is a bit imprecise.
The problem relies on the fact that all events received on an index with an active editor are automatically sent to the editor, but since the editor could have a geometry that is smaller than the visual rect of the index, if a mouse event is sent outside the geometry that event is ignored and not processed by the view as it normally would without an editor.
UPDATE: In fact, the view does receive the event; the problem is that if an editor exists the event is automatically ignored furtherward (since it's assumed that the editor handled it) and nothing is sent to the editorEvent method of the delegate.
You can intercept "mouse edit" events as long as you implement the virtual edit(index, trigger, event) function (which is not the same as the edit(index) slot). Note that this means that if you override that function, you cannot call the default edit(index) anymore, unless you create a separate function that calls the default implementation (via super().edit(index)).
Consider that the following code works better if the delegate is actually a QStyledItemDelegate, instead of the simpler QItemDelegate class: the Qt dev team itself suggests to use the styled class instead of the basic one (which is intended for very basic or specific usage), as it's generally considered more consistent.
class PersistentEditorTableView(QtWidgets.QTableView):
# ...
def edit(self, index, trigger, event):
# if the edit involves an index change, there's no event
if (event and index.column() == 0 and
index.flags() & QtCore.Qt.ItemIsUserCheckable and
event.type() in (event.MouseButtonPress, event.MouseButtonDblClick) and
event.button() == QtCore.Qt.LeftButton):
opt = self.viewOptions()
opt.rect = self.visualRect(index)
opt.features |= opt.HasCheckIndicator
checkRect = self.style().subElementRect(
QtWidgets.QStyle.SE_ItemViewItemCheckIndicator,
opt, self)
if event.pos() in checkRect:
if index.data(QtCore.Qt.CheckStateRole):
state = QtCore.Qt.Unchecked
else:
state = QtCore.Qt.Checked
return self.model().setData(
index, state, QtCore.Qt.CheckStateRole)
return super().edit(index, trigger, event)
Obviously, you could do something similar by implementing the mousePressEvent on the view, but that could complicate things if you need some different implementation of the mouse press event also, and you should also consider the double click events. Implementing edit() is conceptually better, since it's more consistent with the purpose: clicking -> toggling.
There's only one last catch: keyboard events.
A persistent editor automatically grabs the keyboard focus, so you can't use any key (usually, the space bar) to toggle the state if the editor handles that event; since a combobox handles some "toggle" events to show its popup, those events won't be received by the view (the focus is on the combo, not on the view!) unless you ignore the event by properly implementing the eventFilter of the delegate for both keyboard events and focus changes.
Original answer
There are various possibilities to work around this:
as you proposed, use a separate column; this is not always possible or suggested, as the model structure could not allow it;
create an editor that also includes a QCheckBox; as long as there will always be an open editor, this is not an issue, but could create some level of inconsistency if editor could actually be open and destroyed;
add the combo to a container that has a fixed margin, so that mouse event not handled by the combo can be captured by the delegate event filter;
The third possibility is a bit more complex, but it ensures that both displaying and interaction are consistent with the normal behavior.
To achieve this, using a QStyledItemDelegate is suggested as it provides access to the style options.
class ComboDelegate(QtWidgets.QStyledItemDelegate):
# ...
def createEditor(self, parent, option, index):
option = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(option, index)
style = option.widget.style()
textRect = style.subElementRect(
style.SE_ItemViewItemText, option, option.widget)
editor = QtWidgets.QWidget(parent)
editor.index = QtCore.QPersistentModelIndex(index)
layout = QtWidgets.QHBoxLayout(editor)
layout.setContentsMargins(textRect.left(), 0, 0, 0)
editor.combo = QtWidgets.QComboBox()
layout.addWidget(editor.combo)
editor.combo.addItems(
("Red", "Green", "Blue", "Yellow", "Purple", "Orange"))
editor.combo.currentIndexChanged.connect(self.currentIndexChanged)
return editor
def setEditorData(self, editor, index):
editor.combo.blockSignals(True)
data = index.model().data(index)
if data:
idx = int(data)
else:
idx = 0
editor.combo.setCurrentIndex(0)
editor.combo.blockSignals(False)
def eventFilter(self, editor, event):
if (event.type() in (event.MouseButtonPress, event.MouseButtonDblClick)
and event.button() == QtCore.Qt.LeftButton):
style = editor.style()
size = style.pixelMetric(style.PM_IndicatorWidth)
left = editor.layout().contentsMargins().left()
r = QtCore.QRect(
(left - size) / 2,
(editor.height() - size) / 2,
size, size)
if event.pos() in r:
model = editor.index.model()
index = QtCore.QModelIndex(editor.index)
if model.data(index, QtCore.Qt.CheckStateRole):
value = QtCore.Qt.Unchecked
else:
value = QtCore.Qt.Checked
model.setData(
index, value, QtCore.Qt.CheckStateRole)
return True
return super().eventFilter(editor, event)
def updateEditorGeometry(self, editor, opt, index):
# ensure that the editor fills the whole index rect
editor.setGeometry(opt.rect)
Unrelated, but still important:
Consider that the #pyqtSlot decorator is usually not required in normal situations like these. Also note that you missed the # for the data_changed decorator, and the signature is also invalid, since it's incompatible with the signals you've connected it to. A more correct slot decoration would have been the following:
#QtCore.pyqtSlot(QtCore.QModelIndex, QtCore.QModelIndex, 'QVector<int>')
#QtCore.pyqtSlot('QList<QPersistentModelIndex>', QtCore.QAbstractItemModel.LayoutChangeHint)
def data_changed(self, top_left, bottom_right):
# ...
With the first decoration for the dataChanged signal, and the second for layoutChanged. But, as said before, it's generally unnecessary to use slots as they usually are only required for better threading handling and sometimes provide slightly improved performance (which is not that important to this purpose).
Also note that if you want to ensure that there's always an open editor whenever the model changes its "layout", you should also connect to rowsInserted and columnsInserted signals, since those operations do not send the layout change signal.
Related
I am using PyQt5 to make an application. One of my widgets will be a QListView that displays a list of required items, e.g. required to cook a particular dish, say.
For most of these, the listed item is the only possibility. But for a few items, there is more than one option that will fulfill the requirements. For those with multiple possibilities, I want to display those possibilities in a functional QComboBox. So if the user has no whole milk, they can click that item, and see that 2% milk also works.
How can I include working combo boxes among the elements of my QListView?
Below is an example that shows what I have so far. It can work in Spyder or using python -i, you just have to comment or uncomment as noted. By "work", I mean it shows the required items in QListView, but the combo boxes show only the first option, and their displays can't be changed with the mouse. However, I can say e.g. qb1.setCurrentIndex(1) at the python prompt, and then when I move the mouse pointer onto the widget, the display updates to "2% milk". I have found it helpful to be able to interact with and inspect the widget in Spyder or a python interpreter, but I still have this question. I know there are C++ examples of things like this around, but I have been unable to understand them well enough to do what I want. If we can post a working Python example of this, it will help me and others too I'm sure.
from PyQt5.QtWidgets import QApplication, QComboBox, QListView, QStyledItemDelegate
from PyQt5.QtCore import QAbstractListModel, Qt
# A delegate for the combo boxes.
class QBDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
painter.drawText(option.rect, Qt.AlignLeft, self.parent().currentText())
# my own wrapper for the abstract list class
class PlainList(QAbstractListModel):
def __init__(self, elements):
super().__init__()
self.elements = elements
def data(self, index, role):
if role == Qt.DisplayRole:
text = self.elements[index.row()]
return text
def rowCount(self, index):
try:
return len(self.elements)
except TypeError:
return self.elements.rowCount(index)
app = QApplication([]) # in Spyder, this seems unnecessary, but harmless.
qb0 = 'powdered sugar' # no other choice
qb1 = QComboBox()
qb1.setModel(PlainList(['whole milk','2% milk','half-and-half']))
d1 = QBDelegate(qb1)
qb1.setItemDelegate(d1)
qb2 = QComboBox()
qb2.setModel(PlainList(['butter', 'lard']))
d2 = QBDelegate(qb2)
qb2.setItemDelegate(d2)
qb3 = 'cayenne pepper' # there is no substitute
QV = QListView()
qlist = PlainList([qb0, qb1, qb2, qb3])
QV.setModel(qlist)
QV.setItemDelegateForRow(1, d1)
QV.setItemDelegateForRow(2, d2)
QV.show()
app.exec_() # Comment this line out, to run in Spyder. Then you can inspect QV etc in the iPython console. Handy!
There are some misconceptions in your attempt.
First of all, setting the delegate parent as a combo box and then setting the delegate for the list view won't make the delegate show the combo box.
Besides, as the documentation clearly says:
Warning: You should not share the same instance of a delegate between views. Doing so can cause incorrect or unintuitive editing behavior since each view connected to a given delegate may receive the closeEditor() signal, and attempt to access, modify or close an editor that has already been closed.
In any case, adding the combo box to the item list is certainly not an option: the view won't have anything to do with it, and overriding the data() to show the current combo item is not a valid solution; while theoretically item data can contain any kind of object, for your purpose the model should contain data, not widgets.
In order to show a different widget for a view, you must override createEditor() and return the appropriate widget.
Then, since you probably need to keep the data available when accessing the model and for the view, the model should contain the available options and eventually return the current option or the "sub-list" depending on the situation.
Finally, rowCount() must always return the row count of the model, not that of the content of the index.
A possibility is to create a "nested model" that supports a "current index" for the selected option for inner models.
Then you could either use openPersistentEditor() or implement flags() and add the Qt.ItemIsEditable for items that contain a list model.
class QBDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
value = index.data(Qt.EditRole)
if isinstance(value, PlainList):
editor = QComboBox(parent)
editor.setModel(value)
editor.setCurrentIndex(value.currentIndex)
# submit the data whenever the index changes
editor.currentIndexChanged.connect(
lambda: self.commitData.emit(editor))
else:
editor = super().createEditor(parent, option, index)
return editor
def setModelData(self, editor, model, index):
if isinstance(editor, QComboBox):
# the default implementation tries to set the text if the
# editor is a combobox, but we need to set the index
model.setData(index, editor.currentIndex())
else:
super().setModelData(editor, model, index)
class PlainList(QAbstractListModel):
currentIndex = 0
def __init__(self, elements):
super().__init__()
self.elements = []
for element in elements:
if isinstance(element, (tuple, list)) and element:
element = PlainList(element)
self.elements.append(element)
def data(self, index, role=Qt.DisplayRole):
if role == Qt.EditRole:
return self.elements[index.row()]
elif role == Qt.DisplayRole:
value = self.elements[index.row()]
if isinstance(value, PlainList):
return value.elements[value.currentIndex]
else:
return value
def flags(self, index):
flags = super().flags(index)
if isinstance(index.data(Qt.EditRole), PlainList):
flags |= Qt.ItemIsEditable
return flags
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
item = self.elements[index.row()]
if isinstance(item, PlainList):
item.currentIndex = value
else:
self.elements[index.row()] = value
return True
def rowCount(self, parent=None):
return len(self.elements)
app = QApplication([])
qb0 = 'powdered sugar' # no other choice
qb1 = ['whole milk','2% milk','half-and-half']
qb2 = ['butter', 'lard']
qb3 = 'cayenne pepper' # there is no substitute
QV = QListView()
qlist = PlainList([qb0, qb1, qb2, qb3])
QV.setModel(qlist)
QV.setItemDelegate(QBDelegate(QV))
## to always display the combo:
#for i in range(qlist.rowCount()):
# index = qlist.index(i)
# if index.flags() & Qt.ItemIsEditable:
# QV.openPersistentEditor(index)
QV.show()
app.exec_()
I'm coding a Python CRUD app that shows radiosondes on a map and a QTableView. I'm using QStyledItemDelegate to set an editor and regex validator for each column and it is working great. But for the geometry column I would like to parse the binary data and show it on a custom form (lat, lng, elevation), be able to edit them and if clicked OK encode them back to WKB format and update the data.
When I click OK the filed is not updated, instead it becomes empty. If I try to edit after that any other cell nothing happens and if I try to edit that exact cell the app crashes. The same happens if I click Cancel.
The setData method returns True and the data in the DB gets updated.
I tried with dataChanged.emit() on the QSqlTableModel and also with update() method on the QTableView.
main2.py:
from PyQt5.Qt import QStyledItemDelegate
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import Qt
from shapely import wkb, wkt
import folium
import io
class Ui_RadioSondes(object):
def setupUi(self, RadioSondes):
self.centerCoord = (44.071800, 17.578125)
RadioSondes.setObjectName("RadioSondes")
.
.
.
self.tableView_2.setItemDelegate(ValidatedItemDelegate())
.
.
.
class ValidatedItemDelegate(QStyledItemDelegate):
def createEditor(self, widget, option, index):
if not index.isValid():
return 0
if index.column() == 0: #only on the cells in the first column
editor = QtWidgets.QLineEdit(widget)
validator = QtGui.QRegExpValidator(QtCore.QRegExp('[\w]{1,10}'), editor)
editor.setValidator(validator)
return editor
if index.column() == 2:
editor = QtWidgets.QSpinBox(widget)
editor.setMaximum(360)
editor.setMinimum(1)
return editor
.
.
.
if index.column() == 9:
self.form = QtWidgets.QWidget()
self.formLayout = QtWidgets.QFormLayout(self.form)
self.formLayout.setVerticalSpacing(12)
self.formLayout.setObjectName("formLayout")
###__________ Latitude__________###
self.latLabel = QtWidgets.QLabel(self.form)
self.latLabel.setObjectName("latLabel")
self.latLabel.setText("Latitude")
self.latLabel.adjustSize()
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.latLabel)
self.latEdit = QtWidgets.QLineEdit(self.form)
# lineEdit.textChanged.connect(validateFields)
self.latEdit.setObjectName("latEdit")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.latEdit)
###__________ Longitude__________###
self.lngLabel = QtWidgets.QLabel(self.form)
self.lngLabel.setObjectName("lngLabel")
self.lngLabel.setText("Longitude")
self.lngLabel.adjustSize()
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lngLabel)
self.lngEdit = QtWidgets.QLineEdit(self.form)
# lineEdit.textChanged.connect(validateFields)
self.lngEdit.setObjectName("lngEdit")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.lngEdit)
###__________ Elevation__________###
self.elevationLabel = QtWidgets.QLabel(self.form)
self.elevationLabel.setObjectName("elevationLabel")
self.elevationLabel.setText("Elevation")
self.elevationLabel.adjustSize()
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.elevationLabel)
self.elevationEdit = QtWidgets.QLineEdit(self.form)
# lineEdit.textChanged.connect(validateFields)
self.elevationEdit.setObjectName("elevationEdit")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.elevationEdit)
self.buttonBox = QtWidgets.QDialogButtonBox(self.form)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.formLayout.addWidget(self.buttonBox)
self.form.resize(200, 300)
self.prevData = index.data()
self.index = index
self.widget = widget
self.model = self.widget.parent().parent().parent().parent().parent().parent().parent().objModel
self.t_view = self.widget.parent().parent().parent().parent().parent().parent().parent().tableView_2
data = self.model.data(self.index)
geomWkb = wkb.loads(bytes.fromhex(data))
self.latEdit.setText(str(geomWkb.x))
self.lngEdit.setText(str(geomWkb.y))
self.elevationEdit.setText(str(geomWkb.z))
self.buttonBox.accepted.connect(self.generateGeom)
self.buttonBox.rejected.connect(self.cancelGeomEdit)
return self.form
return super(ValidatedItemDelegate, self).createEditor(widget, option, index)
def generateGeom(self):
print(self.latEdit.text())
print(self.lngEdit.text())
print(self.elevationEdit.text())
geomStr = "POINT Z (" + self.latEdit.text() + " " + self.lngEdit.text() + " " + self.elevationEdit.text() + ")"
geom = wkt.loads(geomStr)
geomWkb = wkb.dumps(geom, hex=True, srid=4326)
try:
self.model.setData(self.index, geomWkb, Qt.EditRole)
self.form.close()
#self.t_view.update()
except AssertionError as error:
print(error)
def cancelGeomEdit(self):
self.form.destroy(destroyWindow=True)
Here is the whole code on GitHub: https://github.com/draugnim/pyCrud
EDIT
I managed to get it working by calling self.model.selet() at the end of generateGeom() and cancelGeomEdit(). But still if I click on the X button and close the form the edited cell becomes blank, also this and all other cells become uneditable.
The item delegate uses the editor's user property, which is considered the main default property of a Qt object. For QLineEdit it's text(), for a QSpinBox it's value(), etc.
If you want to provide a custom, advanced editor, the solution is to create a subclass with a custom user property.
Note that the Qt internal way of dealing with item data is a bit strict on types, and since PyQt doesn't expose the QVariant class, the only option is to use a suitable type. For you case, QVector3D is a perfect choice.
Then, using an external window is a bit tricky, as delegates are normally supposed to be simple field editors that exist inside the view. To work around that, the following must be considered:
the editor must notify the delegate that the inserted data has been accepted, and it must destroy itself whenever it's closed;
key press events must be ignored in the filter (return False), so that the editor can properly handle them;
focus and hide events must ignored too, as the delegate by default tries to update the model when the editor has not been "rejected" but it loses focus or is hidden;
the geometry must be set using the parent's top level window(), and the updateEditorGeometry() must be ignored as the delegate would try to update the geometry whenever the view is shown again after being hidden or it's resized;
Since the given code is not a minimal, reproducible example, I'll provide a generic example of the concept.
from PyQt5 import QtCore, QtGui, QtWidgets
from random import randrange
class CoordinateEditor(QtWidgets.QDialog):
submit = QtCore.pyqtSignal(QtWidgets.QWidget)
def __init__(self, parent):
super().__init__(parent)
self.setWindowModality(QtCore.Qt.WindowModal)
layout = QtWidgets.QFormLayout(self)
self.latitudeSpin = QtWidgets.QDoubleSpinBox(minimum=-90, maximum=90)
layout.addRow('Latitude', self.latitudeSpin)
self.longitudeSpin = QtWidgets.QDoubleSpinBox(minimum=-180, maximum=180)
layout.addRow('Longitude', self.longitudeSpin)
self.elevationSpin = QtWidgets.QDoubleSpinBox(minimum=-100, maximum=100)
layout.addRow('Elevation', self.elevationSpin)
buttonBox = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel)
layout.addRow(buttonBox)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
self.finished.connect(self.deleteLater)
def accept(self):
super().accept()
self.submit.emit(self)
#QtCore.pyqtProperty(QtGui.QVector3D, user=True)
def coordinateData(self):
return QtGui.QVector3D(
self.longitudeSpin.value(),
self.latitudeSpin.value(),
self.elevationSpin.value()
)
#coordinateData.setter
def coordinateData(self, data):
self.longitudeSpin.setValue(data.x())
self.latitudeSpin.setValue(data.y())
self.elevationSpin.setValue(data.z())
def showEvent(self, event):
if not event.spontaneous():
geo = self.geometry()
geo.moveCenter(self.parent().window().geometry().center())
self.setGeometry(geo)
QtCore.QTimer.singleShot(0, self.latitudeSpin.setFocus)
class DialogDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
if index.column() == 1:
editor = CoordinateEditor(parent)
editor.submit.connect(self.commitData)
return editor
else:
return super().createEditor(parent, option, index)
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
if index.column() == 1 and index.data() is not None:
coords = index.data()
option.text = '{:.02f}, {:.02f}, {:.02f}'.format(
coords.y(), coords.x(), coords.z())
def eventFilter(self, source, event):
if isinstance(source, CoordinateEditor):
if event.type() in (event.KeyPress, event.FocusOut, event.Hide):
return False
return super().eventFilter(source, event)
def updateEditorGeometry(self, editor, option, index):
if not isinstance(editor, CoordinateEditor):
super().updateEditorGeometry(editor, option, index)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = QtWidgets.QTableView()
test.setItemDelegate(DialogDelegate(test))
model = QtGui.QStandardItemModel(0, 2)
for row in range(10):
coordItem = QtGui.QStandardItem()
coords = QtGui.QVector3D(
randrange(-180, 181),
randrange(-90, 91),
randrange(-100, 101))
coordItem.setData(coords, QtCore.Qt.DisplayRole)
model.appendRow((
QtGui.QStandardItem('Data {}'.format(row + 1)),
coordItem,
))
test.setModel(model)
test.resizeColumnsToContents()
test.show()
sys.exit(app.exec_())
I have a QTreeWidget in my gui in which the contents will be cleared whenever it loads in a different set of data and I am trying to tracked what has been checked as User loads in different data set.
Initially, I thought of tracking it using derive_tree_items method that I created in which it contains the QTreeWidgetItem object, however as soon as I tried to load in a new set of data, the objects that I stored will be lost as they are deleted (expected)..
Currently at a lost what is a better way to 'track' these checkable items? (I may also need to populate them into QMenu + QAction, hence the trackable checking but that will be for next time)
In my code, you can replicate by:
Click on button 'Data-01'
Check any objects, eg. I checked 'c102' and 'a102'
Click on button 'Data-02'
Click on button 'Data-01' again
Expecting to see 'c102', 'a102' is checked..
IsNewItemRole = QtCore.Qt.UserRole + 1000
class CustomTreeWidgetItem(QtGui.QTreeWidgetItem):
"""Initialization class for QTreeWidgetItem creation.
Args:
widget (QtGui.QTreeWidget): To append items into.
text (str): Input name for QTreeWidgetItem.
is_tristate (bool): Should it be a tri-state checkbox. False by default.
"""
def __init__(self, parent=None, text=None, is_tristate=False, is_new_item=False):
super(CustomTreeWidgetItem, self).__init__(parent)
self.setText(0, text)
# flags = QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
if is_tristate:
# flags |= QtCore.Qt.ItemIsTristate
# Solely for the Parent item
self.setFlags(
self.flags()
| QtCore.Qt.ItemIsTristate
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsUserCheckable
)
else:
self.setFlags(
self.flags()
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsUserCheckable
)
self.setCheckState(0, QtCore.Qt.Unchecked)
self.setData(0, IsNewItemRole, is_new_item)
def setData(self, column, role, value):
"""Override QTreeWidgetItem setData function.
QTreeWidget does not have a signal that defines when an item has been
checked/ unchecked. And so, this method will emits the signal as a
means to handle this.
Args:
column (int): Column value of item.
role (int): Value of Qt.ItemDataRole. It will be Qt.DisplayRole or
Qt.CheckStateRole
value (int or unicode):
"""
state = self.checkState(column)
QtGui.QTreeWidgetItem.setData(self, column, role, value)
if (role == QtCore.Qt.CheckStateRole and
state != self.checkState(column)):
tree_widget = self.treeWidget()
if isinstance(tree_widget, CustomTreeWidget):
tree_widget.itemToggled.emit(self, column)
class CustomTreeWidget(QtGui.QTreeWidget):
"""Initialization class for QTreeWidget creation.
Args:
widget ():
"""
# itemToggled = QtCore.pyqtSignal(QtGui.QTreeWidgetItem, bool)
itemToggled = QtCore.Signal(QtGui.QTreeWidgetItem, bool)
contentUpdates = QtCore.Signal()
def __init__(self, widget=None):
super(CustomTreeWidget, self).__init__(widget)
self.rename_counter = False
# self.itemToggled.connect(self.handleItemToggled)
self.currentItemChanged.connect(self.selection_item_changed)
self.itemChanged.connect(self.tree_item_changed)
self.itemDoubleClicked.connect(self.tree_item_double_clicked)
def selection_item_changed(self, current, previous):
"""Overrides widget's default signal.
Emiited when current item selection is changed. This will also toggles
the state of `self.add_child_btn`.
If a child item is selected, the "Add Child" button will be disabled.
Args:
current (CustomTreeWidgetItem): Currently selected item.
previous (CustomTreeWidgetItem or None): Previous selected item.
"""
state = True
if not current or current.parent():
state = False
def tree_item_changed(self, item, column):
"""Overrides widget's default signal.
Emitted when the contents of the selected item in the column changes.
Args:
item (CustomTreeWidgetItem): Selected item.
column (int): Column value of the selected item.
"""
if self.rename_counter and self.prev_name != item.text(column):
self.rename_counter = False
item.setData(0, IsNewItemRole, True)
self.contentUpdates.emit()
elif item.checkState(column) == QtCore.Qt.Checked:
print('Item Checked')
elif item.checkState(column) == QtCore.Qt.Unchecked:
print('Item Unchecked')
def tree_item_double_clicked(self, item, column):
"""Overrides widget's default signal.
Emitted when User performs double clicks inside the widget.
Args:
item (CustomTreeWidgetItem): Selected item.
column (int): Column value of the selected item.
"""
self.prev_name = item.text(column)
self.rename_counter = True
def derive_tree_items(self, mode="all"):
all_items = OrderedDict()
root_item = self.invisibleRootItem()
top_level_count = root_item.childCount()
for i in range(top_level_count):
top_level_item = root_item.child(i)
top_level_item_name = str(top_level_item.text(0))
child_num = top_level_item.childCount()
all_items[top_level_item_name] = []
for n in range(child_num):
child_item = top_level_item.child(n)
child_item_name = str(child_item.text(0)) or ""
all_items[top_level_item_name].append(child_item)
return all_items
class MainApp(QtGui.QWidget):
def __init__(self, parent=None):
super(MainApp, self).__init__(parent)
self._diff_highlight = False
self._tree = CustomTreeWidget()
self._tree.header().hide()
# QTreeWidget default signals override
self._tree.contentUpdates.connect(self.update_dictionary)
tree_layout = QtGui.QVBoxLayout()
self.btn1 = QtGui.QPushButton("Data-01")
self.btn2 = QtGui.QPushButton("Data-02")
tree_layout.addWidget(self._tree)
tree_layout.addWidget(self.btn1)
tree_layout.addWidget(self.btn2)
main_layout = QtGui.QHBoxLayout()
main_layout.addLayout(tree_layout)
self.setLayout(main_layout)
self.setup_connections()
def setup_connections(self):
self.btn1.clicked.connect(self.show_data_01)
self.btn2.clicked.connect(self.show_data_02)
def update_dictionary(self):
print '>>> update: ', self._tree.derive_tree_items()
def show_data_01(self):
print '>>> Button1 test'
self._tree.clear()
test_dict1 = {
"itemA" :{
"menuA": ["a101", "a102"],
},
"itemBC": {
"menuC": ["c101", "c102", "c103"],
"menuB": ["b101"]
},
}
for page_name, page_contents in test_dict1.items():
# page_item = PageHeaderItem(self._tree, page_name)
for pk, pv in page_contents.items():
parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
for c in pv:
child = CustomTreeWidgetItem(parent, c)
self._tree.expandAll()
def show_data_02(self):
print '>>> Button2 test'
self._tree.clear()
test_dict2 = {
"itemD" :{
"menuD": ["d100"],
},
}
for page_name, page_contents in test_dict2.items():
# page_item = PageHeaderItem(self._tree, page_name)
for pk, pv in page_contents.items():
parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
for c in pv:
child = CustomTreeWidgetItem(parent, c)
self._tree.expandAll()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
w = MainApp()
w.show()
sys.exit(app.exec_())
A QTreeWidget (like QListWidget and QTableWidget) has its internal model; it's some sort of a high-level access to a data model, and its actual model is not directly (as in easily) accessible, nor it should. They are "simplified" model-view interfaces, intended for general use that don't require advanced editing, but - most importantly - they only support their own, single and unique model. There's no easy way to change it except from their Q[viewType]WidgetItem interfaces, unless you completely reset the model, meaning that you'll need to "store" the data somewhere else if you want to use multiple models in the same view, making the whole thing much more complex than it needs to be and much prone to errors and issues, which is exactly what happens in your case.
On the opposite side, those QWidgetItemViews offer some features missing in standard models and views, and one of them is the "auto-check" of items in QTreeWidgets.
While that feature is very useful, it could be a serius PITA when you need to show different data models on the same view; this means that, to avoid the rhetorical reinvention of the wheel, it's better to stick with the QTreeView/QStandardItemModel pair and just implement the tristate mechanism instead of using convoluted methods that might clash with the internal implementation of a QTreeWidget.
Separate QStandardItemModel subclass instancies, with parent/child Tristate support
The most important aspect here is that you will be using a single data model class instance for each data set (instead of multiple dict + view's model pairs), making it much easier to switch between them with a simple flick of setModel().
The drawback is the aforementioned lack of parent/children state support, which has to be implemented; once that logic is solved, you'll get multiple persistent, unique and consistent models, no matter how many of them you actually need.
Besides the actual model contents initialization, you are only required to subclass two methods of QStandardItemModel:
setData(index, value, role) is overridden to apply the check state to the children indexes: if the role is Qt.CheckState and the index has any children, the [un]checked state is applied to them; if the index has a parent, the index emits the dataChanged signal to the model, ensuring that its view[s] requires updates (otherwise the checkbox visible state won't be updated correctly until the view is repainted)[1];
data(index, role) overriding is required to "show" the checkstate for the parent(s); it doesn't matter what the model's index data is: if it has any children, its state totally depends on them (all/any/none checked), otherwise it's based on the default model index's checkState;
Once that's solved, you only have to care about setting the newly selected model to the view, and all states will be there as they were before switching to another model, if any.
To keep some consistence with your example, I used your dict-based model data creation logic, but I'd suggest you to use a recursive method to add sub-children.
Since I was already there, I also added a mechanism to store the expanded state of every index, for better view/model consistency; it's not required, but it really helps user experience :-) Keep in mind that that's just there for demonstration purposes: obviously, if you add/remove items without taking care of the internal expandState dict, this won't work properly (or won't work at all!).
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
dataSets = [
{
"itemA" :{
"menuA": ["a101", "a102"],
},
"itemBC": {
"menuC": ["c101", "c102", "c103"],
"menuB": ["b101"]
},
},
{
"itemD" :{
"menuD": ["d100"],
},
}
]
class TreeModel(QtGui.QStandardItemModel):
checkStateChange = QtCore.pyqtSignal(QtCore.QModelIndex, bool)
def __init__(self, dataSet):
super(TreeModel, self).__init__()
# unserialize data, as per your original code; you might want to use a
# recursive function instead, to allow multiple levels of items
for page_name, page_contents in dataSet.items():
for pk, pv in page_contents.items():
parent = QtGui.QStandardItem(pk)
parent.setCheckable(True)
self.appendRow(parent)
if pv:
parent.setTristate(True)
for c in pv:
child = QtGui.QStandardItem(c)
child.setCheckable(True)
parent.appendRow(child)
self.dataChanged.connect(self.checkStateChange)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.CheckStateRole:
childState = QtCore.Qt.Checked if value else QtCore.Qt.Unchecked
# set all children states according to this parent item
for row in range(self.rowCount(index)):
for col in range(self.columnCount(index)):
childIndex = self.index(row, col, index)
self.setData(childIndex, childState, QtCore.Qt.CheckStateRole)
# if the item has a parent, emit the dataChanged signal to ensure
# that the parent state is painted correctly according to what data()
# will return; note that this will emit the dataChanged signal whatever
# the "new" parent state is, meaning that it might still be the same
parent = self.parent(index)
if parent.isValid():
self.dataChanged.emit(parent, parent)
return super(TreeModel, self).setData(index, value, role)
def data(self, index, role=QtCore.Qt.DisplayRole):
# QStandardItemModel doesn't support auto tristate based on its children
# as it does for QTreeWidget's internal model; we have to implement that
if role == QtCore.Qt.CheckStateRole and self.flags(index) & QtCore.Qt.ItemIsTristate:
childStates = []
# collect all child check states
for row in range(self.rowCount(index)):
for col in range(self.columnCount(index)):
childIndex = self.index(row, col, index)
childState = self.data(childIndex, QtCore.Qt.CheckStateRole)
# if the state of a children is partially checked we can
# stop here and return a partially checked state
if childState == QtCore.Qt.PartiallyChecked:
return QtCore.Qt.PartiallyChecked
childStates.append(childState)
if all(childStates):
# all children are checked, yay!
return QtCore.Qt.Checked
elif any(childStates):
# only some children are checked...
return QtCore.Qt.PartiallyChecked
# no item is checked, so bad :-(
return QtCore.Qt.Unchecked
return super(TreeModel, self).data(index, role)
def checkStateChange(self, topLeft, bottomRight):
# if you need some control back to your data outside the model, here is
# the right place to do it; note that *usually* the topLeft and
# bottomRight indexes are the same, expecially with QStandardItemModels
# but that would not be the same in some special cases
pass
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.treeView = QtWidgets.QTreeView()
layout.addWidget(self.treeView)
self.models = []
self.expandStates = {}
for i, dataSet in enumerate(dataSets):
model = TreeModel(dataSet)
button = QtWidgets.QPushButton('Data-{:02}'.format(i + 1))
layout.addWidget(button)
button.clicked.connect(lambda _, model=model: self.setModel(model))
def getExpandState(self, expDict, model, index=QtCore.QModelIndex()):
# set the index expanded state, if it's not the root index:
# the root index is not a valid index!
if index.isValid():
expDict[index] = self.treeView.isExpanded(index)
# if the index (or root index) has children, set their states
for row in range(model.rowCount(index)):
for col in range(model.columnCount(index)):
childIndex = model.index(row, col, index)
# if the current index has children, set their expand state
# using this function, which is recursive
for childRow in range(model.rowCount(childIndex)):
self.getExpandState(expDict, model, childIndex)
def setModel(self, model):
if self.treeView.model():
if self.treeView.model() == model:
# the model is the same, no need to update anything
return
# save the expand states of the current model before changing it
prevModel = self.treeView.model()
self.expandStates[prevModel] = expDict = {}
self.getExpandState(expDict, prevModel)
self.treeView.setModel(model)
if model in self.expandStates:
# if the new model has expand states saved, restore them
for index, expanded in self.expandStates.get(model, {}).items():
self.treeView.setExpanded(index, expanded)
else:
self.treeView.expandAll()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
[1]: In this example the dataChanged signal is emitted whenever any child item check state changes. This isn't a big issue, but if you really need to avoid unnecessary dataChanged notifications you might need to add a QtCore.QTimer.singleshot delayed dataChanged signal emission only if the parent state has changed. It's not that hard to achieve, but I didn't think it was really necessary for this example.
I would like to render each item in a QTreeView differently based on a number of attributes stored in a database and based on whether the item is a folder or a file. However, I don't understand how the QTreeView or QFileSystemModel communicate with the delegate. Whenever an item must be drawn, including during initialization, I'd expect to provide the delegate with all the parameters it requires and then use a series of if statements within the delegate to set how the particular item is drawn. I've only found the .setItemDelegate method and don't know when or how the delegate is actually called or how it loops through all the items in the model. Below is an example based on material online. There are two problems:
I placed code in comments that I was unable to get working. Once I understand how the delegate can receive information from the QTreeView (or calling class), I believe I can do the rest.
I was unable to get this subclass of the QTreeView to display the folder and file icons.
Code:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class fileSystemDelegate(QItemDelegate):
def __init__(self, parent=None):
QItemDelegate.__init__(self, parent) #shouldn't this insure the icons are drawn?
def paint(self, painter, option, index):
painter.save()
# set background
painter.setPen(QPen(Qt.NoPen))
if option.state & QStyle.State_Selected: #DURING DRAW LOOP: idx = self.currentIndex(); if self.fileSystemModel.isDir(idx): PAINT RED
painter.setBrush(QBrush(Qt.red))
else:
painter.setBrush(QBrush(Qt.white)) #ELSE PAINT WHITE
painter.drawRect(option.rect)
# draw item
painter.setPen(QPen(Qt.black))
text = index.data(Qt.DisplayRole)
painter.drawText(option.rect, Qt.AlignLeft, text) #there is no painter.drawIcon?
painter.restore()
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
delegate = fileSystemDelegate()
self.setItemDelegate(delegate) # how to provide delegate with additional info about the item to be drawn ?
self.fileSystemModel = QFileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT 1:
I've added an example "database" in the form of a dictionary and changed the approach to rely on the data method rather than the delegate. I would expect this code to perform the dictionary lookup whenever information is displayed in the tree and therefore print to the terminal when the user enters C:\Program Files\Internet Explorer\ on a Microsoft Windows computer. However, it just displays the directory without printing anything to the terminal. I'd like to know:
How do I get if statements in the data method to trigger for every item in the display as they are being drawn?
How can I display an icon after the default icon is displayed, on the same row?
Code:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = QFileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
path = self.fileSystemModel.filePath(index)
if self.fileSystemModel.isDir(index):
if database.get(path) != None:
if database[path][0] > 0:
print("Acting on custom data 0.") # add another icon after the regular folder icon
if database[path][1] > 0:
print("Acting on custom data 1.") # add another (different) icon after the regular folder or previous icon
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT 2:
Subclassing the model definitely did make a difference. Now the script appears to be calling my new data method on every item. Unfortunately, the data method doesn't work yet so the result is a treeview without icons or text. Sometimes I receive the error: "QFileSystemWatcher: failed to add paths: C:/PerfLogs". Based on examples online, I've commented where I think my errors may be, but I cannot yet get this to work. What am I doing wrong?
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}
class newFileModel(QFileSystemModel):
def __init__(self, parent=None):
QFileSystemModel.__init__(self, parent)
#self.elements = [[Do I need this? What should go here?]]
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
path = self.filePath(index)
if self.isDir(index):
if database.get(path) != None:
if database[path][0] > 0:
print("Acting on custom data 0.") # I can add code here for different color text, etc.
if database[path][1] > 0:
print("Acting on custom data 1.") # I'll add code later
#return self.data(index, role) # Do I need this about here?
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = newFileModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
Here is a basic demo that shows how to add an extra column with icons and other formatting. Note that an attempt is made to normalise the file-paths so that comparisons and dictionary look-ups should be more reliable:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {
QFileInfo('C:\Program Files\Internet Explorer\ExtExport.exe').absoluteFilePath(): (1, 3),
QFileInfo('C:\Program Files\Internet Explorer\iexplore.exe').absoluteFilePath(): (0, 0),
}
class FileSystemModel(QFileSystemModel):
def __init__(self, parent=None):
super().__init__(parent)
style = qApp.style()
self.icons = [
style.standardIcon(QStyle.SP_MessageBoxInformation),
style.standardIcon(QStyle.SP_MessageBoxWarning),
]
def columnCount(self, parent=QModelIndex()):
return super().columnCount(parent) + 1
def data(self, index, role=Qt.DisplayRole):
extra = False
if index.isValid():
extra = index.column() == self.columnCount(index.parent()) - 1
info = self.fileInfo(index)
path = info.absoluteFilePath()
if path in database:
major, minor = database[path]
print('found:', (major, minor), path)
if extra:
if role == Qt.DecorationRole:
if major > 0:
return self.icons[0]
else:
return self.icons[1]
elif role == Qt.DisplayRole:
return '%s/%s' % (major, minor)
elif role == Qt.ForegroundRole:
if minor > 2:
return QColor('red')
if not extra:
return super().data(index, role)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if (orientation == Qt.Horizontal and
role == Qt.DisplayRole and
section == self.columnCount() - 1):
return 'Extra'
return super().headerData(section, orientation, role)
class FileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = FileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
self.header().moveSection(self.fileSystemModel.columnCount() - 1, 1)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = FileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT:
The roles used in the data method are all documented under the ItemDataRole enum, and are introduced as follows:
Each item in the model has a set of data elements associated with it,
each with its own role. The roles are used by the view to indicate to
the model which type of data it needs. Custom models should return
data in these types.
For the extra column that has been added, it is necessary to supply everything, because it is a virtual column that is not part of the underlying model. But for the other columns, we can just call the base-class implementation to get the default values (of course, if desired, we could also return custom values for these columns to modify the existing behaviour).
first off, im new to python and pyqt so please bear with me.
Im using a QTableView with a QSqlTableModel everything works as intended.
The last column of the view contains only 0 and 1 as value which i want to display as checkbox and this column should be editable.
Ive read that you should subclass QItemDelegate which i did. Unluckily my table wont show the last column as a checkbox.
I tried to set the delegate only for the last column (the way i would prefer) using setItemDelegateForColumn(), it didnt work. So i modified it and set it for the entire QTableView using setItemDelegate() reacting only to requests to the last column. It still wont work. Wont work means there are no error messages it just wont do what i say ;) It seems that none of the methods i reimplemented gets ever called except init(). So i guess im missing something fundamental.
Ive extracted the relevant lines of code, KSCheckBoxDelegate is my subclass. This is the version where the delegate is set up for the entire QTableView.
-- code from applications main class --
self.taglist = QTableView()
self.tagmodel = QSqlTableModel()
self.tagmodel.setTable("data")
self.tagmodel.select()
self.taglist.setModel(self.tagmodel)
print self.taglist.itemDelegate()
myDel = KSCheckBoxDelegate(self)
myDel.colnumber = 3
self.taglist.setItemDelegate(myDel)
-- KSCheckBoxDelegate.py --
from PyQt4.QtGui import *
class KSCheckBoxDelegate(QStyledItemDelegate):
colnumber = None
def __init__ (self, parent = None):
print "KSCheckBoxDelegate::init"
QStyledItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
print "KSCheckBoxDelegate::createEditor"
if index.column()==self.colnumber:
return QCheckBox(self)
else:
return QStyledItemDelegate.createEditor(self, parent, option, index)
def setEditorData (self, editor, index):
print "KSCheckBoxDelegate::setEditorData"
if index.column() == self.colnumber:
cont = index.model().data(index, Qt.DisplayRole).toString()
if cont == "1":
editor.setCheckState(Qt.Checked)
else:
editor.setCheckState(Qt.UnChecked)
else:
QStyledItemDelegate.setEditorData(self,editor, index)
def setModelData (self, editor, model, index):
print "KSCheckBoxDelegate::setModelData"
if index.column() == self.colnumber:
if editor.checkBox.checkState() == Qt.Checked:
model.setData(index, 1)
else:
model.setData(index, 0)
else:
QStyledItemDelegate.setModelData(self, editor, model, index)
Any hints for me on that issue?
Furthermore i have difficulties with the currentChanged() signal of the QTableViews selectionModel. Im printing the top right coordinates of the selection. I keep getting wrong indexes (not invalid) when clicking with the left mouse button. Using the cursor keys gets me the right indexes. Using selectionChanged() has the same behaviour. Im actually getting the coordinates of the second last selected cell of the QTableView. For instance im clicking on the coordinates <1,1> <2,1> the second click would show me the coordinates <1,1>.
selInd = self.taglist.selectionModel().selectedIndexes()
if(len(selInd) > 0):
self.xPosData=selInd[0].column()
self.yPosData=selInd[0].row()
Fixed that by myself, with using QTableView.currentIndex() instead of selectionModel.selectedIndexes() :)
And last off using OnManualSubmit as editStrategy doesnt throw an error (return false) when calling submitAll() but doesnt save the data either. It works with choosing OnFieldChange as editStrategy. Which i can live with but is not was i have intended to do.
Thanks for your time.
Horst
I think it would be simpler to create your own model basing QSqlTableModel, and for your 0/1 column return QVariant() for QDisplayRole and return Qt::Checked/Qt::Unchecked for Qt::CheckStateRole depending on value. For all other cases return QSqlTableModel::data
class MySqlTableModel: public QSqlTableModel
{
public:
// contructors
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole)
{
if(index.column() == 3 /* your 0/1 column */)
{
if(role == Qt::DisplayRole)
{
return QVariant();
}
else if(role == Qt::CheckStateRole)
{
QString value = QSqlTableModel::data(index, Qt::DisplayRole).toString();
return value == "1" ? Qt::Checked : Qt::Unchecked;
}
}
return QSqlTableModel::data(index, role);
}
};
I know it's C++ code, but logic is still same, so just readjust it for Python
at least i managed to have my delegate show checkboxes with the correct state in display and edit mode. Because i couldnt find a complete description on how to do it ill share it. Maybe other ppl trying to do something similar without having worked with qt ever before.
Only one thing is missing, thats the items being editable, i guess its related to the flags question i asked in my opening post.
-- KSCheckBoxDelegate.py --
class KSCheckBoxDelegate(QStyledItemDelegate):
def __init__ (self, parent = None):
print "KSCheckBoxDelegate::init"
QStyledItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
print "KSCheckBoxDelegate::createEditor"
return QCheckBox(parent)
def setEditorData (self, editor, index):
print "KSCheckBoxDelegate::setEditorData"
cont = index.model().data(index, Qt.DisplayRole).toString()
if cont == "1":
editor.setCheckState(Qt.Checked)
else:
editor.setCheckState(Qt.Unchecked)
def setModelData (self, editor, model, index):
print "KSCheckBoxDelegate::setModelData"
if editor.checkState() == Qt.Checked:
model.setData(index, 1)
else:
model.setData(index, 0)
def paint (self, painter, option, index):
myopt = QStyleOptionViewItemV4(option)
newopt = QStyleOptionButton()
newopt.rect = myopt.rect
newopt.palette = myopt.palette
if index.data().toBool():
newopt.state |= QStyle.State_On
QApplication.style().drawControl(QStyle.CE_CheckBox, newopt, painter)
def sizeHint (self, option, index):
return 18