Setup description
Table in PySide created with
QMainWindow - > QWidget -> QTableView -> TableModel (QAbstractTableModel) -> array[]
For second and third column is created ButtonDelegate(QStyledItemDelegate)
buttons toggle value in the first column - but butons must be separate for specific reason in my application (toggling with one button is not a solution)
button with value of the first column is "hidden"
only whole single row is selected (important in my application where I'm separately showing detailed data on the selected row)
Detailed description of functionality
Buttons in my application don't necesserily toggle value. Easiest to explaining fonctionality of my application is something like configuration list.
Initially in the list are generic items which can be selected and the two buttons are "+" (add/select) and "-" (remove/deselect).
Some items can be added only once, in that case the buttons are really only toggling the item selection. If not selected only the button "+" is show and if selected only button "-" is shown.
Some items can be added multiple times. In that case initially the item is unseleted. Presing "+" selects the item, shows "-" buton, but button "+" is still shown, as the item can be added multiple times. When pressed "+" once again, the next row with the same item is added, again with both "+" and "-" shown. Then pressing "-" works in reverse way, removing row where "-" is pressed until last item of the same type, where "-" results in unselected item. Therefore function of +/- is content dependent.
There few reasons I decided to have buttons in separate columns - keep possibility to sort based on selection state, header to show "Add" for "+" and "Remove" for "-"
Problem description
when button in last column is disabled (pushing False button and then True button), the selection moves to next row - should remain in the same
also, probably the showing and hiding of active button should be done in paint (instead of the openPersistentEditor). I was looking in the documentation and examples from google to find way how to, but still I haven't figured it out. If you could show me how, I would appreciate it. Also if you have link to some good tutorial on this topic (paint) I would be glad, because still I'm not getting how to use it.
Minimal functioning example:
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QAbstractItemView
from PySide6.QtWidgets import QTableView, QWidget, QStyledItemDelegate, QPushButton
from PySide6.QtCore import Qt, QModelIndex, QAbstractTableModel, QItemSelectionModel
class TrueButtonDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
self.parent().openPersistentEditor(index) # this should be somewhere else, not in paint
def createEditor(self, parent, option, index):
editor = QPushButton('True', parent)
editor.setEnabled(False)
editor.clicked.connect(self.buttonClicked)
return editor
def setEditorData(self, editor, index):
if not index.data():
editor.setText('True')
editor.setEnabled(True)
editor.setFlat(False)
else:
editor.setText('')
editor.setEnabled(False)
editor.setFlat(True)
def setModelData(self, editor, model, index):
model.setData(index, True, role=Qt.EditRole)
def buttonClicked(self):
self.commitData.emit(self.sender())
def eventFilter(self, obj, event):
if event.type() == event.Type.Wheel:
event.setAccepted(False)
return True
return super().eventFilter(obj, event)
class FalseButtonDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
self.parent().openPersistentEditor(index) # this should be somewhere else, not in paint
def createEditor(self, parent, option, index):
editor = QPushButton('False', parent)
editor.setEnabled(True)
editor.clicked.connect(self.buttonClicked)
return editor
def setEditorData(self, editor, index):
if index.data():
editor.setText('False')
editor.setEnabled(True)
editor.setFlat(False)
else:
editor.setText('')
editor.setEnabled(False)
editor.setFlat(True)
def setModelData(self, editor, model, index):
model.setData(index, False, role=Qt.EditRole)
def buttonClicked(self):
self.commitData.emit(self.sender())
def eventFilter(self, obj, event):
if event.type() == event.Type.Wheel:
event.setAccepted(False)
return True
return super().eventFilter(obj, event)
class TableModel(QAbstractTableModel):
def __init__(self, localData=[[]], parent=None):
super().__init__(parent)
self.modelData = localData
def headerData(self, section: int, orientation: Qt.Orientation, role: int):
if role == Qt.DisplayRole:
if orientation == Qt.Vertical:
return "Row " + str(section)
def columnCount(self, parent=None):
return 3
def rowCount(self, parent=None):
return len(self.modelData)
def data(self, index: QModelIndex, role: int):
if role == Qt.DisplayRole:
row = index.row()
return self.modelData[row]
def setData(self, index, value = None, role=Qt.DisplayRole):
row = index.row()
self.modelData[row] = value
index = self.index(row, 0)
self.dataChanged.emit(index, index)
index = self.index(row, 1)
self.dataChanged.emit(index, index)
index = self.index(row, 2)
self.dataChanged.emit(index, index)
return True
app = QApplication()
data = [True, True, True, True, True, True, True, True, True, True, True, True, True, True]
model = TableModel(data)
tableView = QTableView()
tableView.setModel(model)
selectionModel = QItemSelectionModel(model)
tableView.setSelectionModel(selectionModel)
tableView.setItemDelegateForColumn(1, FalseButtonDelegate(tableView))
tableView.setItemDelegateForColumn(2, TrueButtonDelegate(tableView))
tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
tableView.setSelectionMode(QAbstractItemView.SingleSelection)
widget = QWidget()
widget.horizontalHeader = tableView.horizontalHeader()
widget.horizontalHeader.setStretchLastSection(True)
widget.mainLayout = QVBoxLayout()
widget.mainLayout.setContentsMargins(1,1,1,1)
widget.mainLayout.addWidget(tableView)
widget.setLayout(widget.mainLayout)
mainWindow = QMainWindow()
mainWindow.setCentralWidget(widget)
mainWindow.setGeometry(0, 0, 380, 300)
mainWindow.show()
exit(app.exec())
The reason for this behavior is that disabling the widget automatically sets the focus to the next available widget in the focus chain.
The actual behavior is based on the QAbstractItemView's re-implementation of focusNextPrevChild, which creates a "virtual" QKeyPressEvent with a tab (or backtab) key that is sent to the keyPressEvent() handler.
By default, this results in calling the table view's reimplementation of moveCursor(), which focuses on the next selectable item (the first item in the next row in your case).
A possible workaround for this would be to use a subclass of QTableView and override focusNextPrevChild(); in this way you can first check if the current widget is a button and a child of the viewport (meaning it's one of your editors), and eventually just return True without doing anything else:
class TableView(QTableView):
def focusNextPrevChild(self, isNext):
if isNext:
current = QApplication.focusWidget()
if isinstance(current, QPushButton) and current.parent() == self.viewport():
return True
return super().focusNextPrevChild(isNext)
Unfortunately, this won't resolve a major issue with your implementation.
Implementing such complex systems like yours, requires some special care and knowledge about how Qt views work, and the main problem is related to the fact that setModelData() can be triggered by various reasons; one of them is whenever the current index of the view changes. This can happen with keyboard navigation (tab/backtab, arrows, etc), but also when the mouse changes the current selection: you can see this in your UI by clicking and keeping the mouse button pressed on an item on the first column, and then begin to drag the mouse on items that have buttons; since that operation changes the selection model, this also triggers the current index change, and consequentially the setModelData of the delegate, since the persistent editor is opened.
A better implementation (which also doesn't require separate delegates) implies knowing whether the current index corresponds to the "true" or "false" column. As long as you know the column used to show contents when the value is True, then setting the value and showing the buttons is just a matter of comparing those three values:
value = index.data()
trueColumn = index.column() == self.TrueColumn
if value == trueColumn:
# we are in the column that should show the widget
else:
# we are in the other column (whatever it is)
Setting the data when the button is pressed follows the same concept; if the button is in the "true" column (the one used to set the value to False), set it to False, and vice versa:
model.setData(index, index.column() != self.TrueColumn, Qt.EditRole)
Then, some further adjustments are also required:
to avoid focus problems, you can make the editor ignore mouse events by setting the attribute Qt.WA_TransparentForMouseEvents, and ignore keyboard focus by setting the focus policy to No.Focus; then restore the default behavior when the editor is "restored";
to make the button transparent, use a stylesheet that makes every component invisible: color: transparent; background: transparent; border: none;;
do not open the editor in the paint method, but properly call openPersistentIndex() both when the model is set and when new rows are added;
if you want to hide the text of the index, just override displayText() and return an empty string; in this way you can keep the default paint behavior which shows selected items;
class ButtonDelegate(QStyledItemDelegate):
TrueColumn = 1
isClicked = False
def buttonClicked(self):
self.isClicked = True
self.commitData.emit(self.sender())
self.isClicked = False
def createEditor(self, parent, option, index):
editor = QPushButton(str(index.column() != self.TrueColumn), parent)
editor.clicked.connect(self.buttonClicked)
return editor
def eventFilter(self, editor, event):
if event.type() == event.MouseMove:
editor.mouseMoveEvent(event)
event.setAccepted(True)
return True
return super().eventFilter(editor, event)
def displayText(self, *args):
return ''
def setEditorData(self, editor, index):
value = index.data()
trueColumn = index.column() == self.TrueColumn
if value == trueColumn:
editor.setAttribute(Qt.WA_TransparentForMouseEvents, False)
editor.setStyleSheet('')
editor.setFocusPolicy(Qt.StrongFocus)
if self.isClicked:
editor.setFocus()
self.parent().setCurrentIndex(index)
else:
editor.setAttribute(Qt.WA_TransparentForMouseEvents, True)
editor.setStyleSheet(
'color:transparent; background: transparent; border: none;')
editor.setFocusPolicy(Qt.NoFocus)
def setModelData(self, editor, model, index):
sender = self.sender()
if sender:
model.setData(index, index.column() != self.TrueColumn, Qt.EditRole)
app = QApplication([])
data = [True] * 16
tableView = QTableView()
tableView.setModel(model)
selectionModel = QItemSelectionModel(model)
tableView.setSelectionModel(selectionModel)
delegate = ButtonDelegate(tableView)
tableView.setItemDelegateForColumn(1, delegate)
tableView.setItemDelegateForColumn(2, delegate)
tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
tableView.setSelectionMode(QAbstractItemView.SingleSelection)
def updateEditors(parent, first, last):
for row in range(first, last + 1):
tableView.openPersistentEditor(model.index(row, 1))
tableView.openPersistentEditor(model.index(row, 2))
updateEditors(None, 0, model.rowCount() - 1)
model.rowsInserted.connect(updateEditors)
# ...
A further improvement would consider tab navigation, and for this you need to tweak the model and the view. With the following modifications, pressing tab only changes between indexes with valid data or active editor:
class TableModel(QAbstractTableModel):
tabPressed = False
def __init__(self, localData=[[]], parent=None):
super().__init__(parent)
self.modelData = localData
def flags(self, index):
flags = super().flags(index)
if 0 < index.column() < self.columnCount() and self.tabPressed:
if (index.column() != 1) == self.modelData[index.row()]:
flags &= ~(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
return flags
def headerData(self, section: int, orientation: Qt.Orientation, role: int):
if role == Qt.DisplayRole and orientation == Qt.Vertical:
return "Row " + str(section)
def columnCount(self, parent=None):
return 3
def rowCount(self, parent=None):
return len(self.modelData)
def data(self, index: QModelIndex, role: int):
if role == Qt.DisplayRole:
return self.modelData[index.row()]
def setData(self, index, value = None, role=Qt.DisplayRole):
row = index.row()
self.modelData[row] = value
# do not emit dataChanged for each index, emit it for the whole range
self.dataChanged.emit(self.index(row, 0), self.index(row, 2))
return True
class TableView(QTableView):
def moveCursor(self, action, modifiers):
self.model().tabPressed = True
new = super().moveCursor(action, modifiers)
self.model().tabPressed = False
return new
# ...
tableView = TableView()
Update: further options
It occured to me that there is another available alternative: while keeping the two-column requirement, it is possible to have a single delegate, as long as the table has properly set spans.
This requests some ingenuity, and a further class (with a proper user property set) is required, but it might provide a better result; the trick is to create a custom widget that contains both buttons. Some further adjustments are required too (especially to ensure that the size of the inner widgets is respected whenever the columns are resized).
class Switch(QWidget):
valueChanged = Signal(bool)
clicked = Signal()
_value = False
def __init__(self, table, column):
super().__init__(table.viewport())
self.setFocusPolicy(Qt.TabFocus)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self.spacing = self.style().pixelMetric(QStyle.PM_HeaderGripMargin)
layout.setSpacing(self.spacing)
self.buttons = []
for v in range(2):
button = QPushButton(str(bool(v)))
self.buttons.append(button)
layout.addWidget(button)
button.setMinimumWidth(10)
button.clicked.connect(self.buttonClicked)
self.header = table.horizontalHeader()
self.columns = column, column + 1
self.updateButtons(False)
self.header.sectionResized.connect(self.updateSizes)
self.resizeTimer = QTimer(self, interval=0, singleShot=True,
timeout=self.updateSizes)
#Property(bool, user=True, notify=valueChanged)
def value(self):
return self._value
#value.setter
def value(self, value):
if self._value != value:
self._value = value
self.valueChanged.emit(value)
self.updateButtons(self._value)
def updateButtons(self, value):
focused = False
self.setFocusProxy(None)
for i, button in enumerate(self.buttons):
if i != value:
button.setAttribute(Qt.WA_TransparentForMouseEvents, False)
self.setFocusProxy(button)
button.setStyleSheet('')
else:
if button.hasFocus():
focused = True
button.setAttribute(Qt.WA_TransparentForMouseEvents, True)
button.setStyleSheet(
'color: transparent; background: transparent; border: none;')
if focused:
self.setFocus(Qt.MouseFocusReason)
def buttonClicked(self):
button = self.sender()
self.value = bool(self.buttons.index(button))
self.clicked.emit()
def updateSizes(self):
for i, column in enumerate(self.columns):
size = self.header.sectionSize(column)
if i == 0:
size -= self.spacing
self.layout().setStretch(i, size)
self.layout().activate()
def focusNextPrevChild(self, isNext):
return False
def resizeEvent(self, event):
self.updateSizes()
class SwitchButtonDelegate(QStyledItemDelegate):
def displayText(self, *args):
return ''
def createEditor(self, parent, option, index):
editor = Switch(self.parent(), index.column())
def clicked():
if persistent.isValid():
index = persistent.model().index(
persistent.row(), persistent.column(), persistent.parent())
view.setCurrentIndex(index)
view = option.widget
persistent = QPersistentModelIndex(index)
editor.clicked.connect(clicked)
editor.valueChanged.connect(lambda: self.commitData.emit(editor))
return editor
# ...
tableView.setItemDelegateForColumn(1, SwitchButtonDelegate(tableView))
def updateEditors(parent, first, last):
for row in range(first, last + 1):
tableView.setSpan(row, 1, 1, 2)
tableView.openPersistentEditor(model.index(row, 1))
Of course, the simpler solution is to avoid any editor at all, and delegate the painting to the item delegate.
class PaintButtonDelegate(QStyledItemDelegate):
_pressIndex = _mousePos = None
def __init__(self, trueColumn=0, parent=None):
super().__init__(parent)
self.trueColumn = trueColumn
def paint(self, painter, option, index):
opt = QStyleOptionViewItem(option)
self.initStyleOption(opt, index)
style = opt.widget.style()
opt.text = ''
opt.state |= style.State_Enabled
style.drawControl(style.CE_ItemViewItem, opt, painter, opt.widget)
if index.data() == (index.column() == self.trueColumn):
btn = QStyleOptionButton()
btn.initFrom(opt.widget)
btn.rect = opt.rect
btn.state = opt.state
btn.text = str(index.column() != self.trueColumn)
if self._pressIndex == index and self._mousePos in btn.rect:
btn.state |= style.State_On
if index == option.widget.currentIndex():
btn.state |= style.State_HasFocus
style.drawControl(style.CE_PushButton, btn, painter, opt.widget)
def editorEvent(self, event, model, option, index):
if event.type() == event.MouseButtonPress:
if index.data() == (index.column() == self.trueColumn):
self._pressIndex = index
self._mousePos = event.pos()
option.widget.viewport().update()
elif event.type() == event.MouseMove and self._pressIndex is not None:
self._mousePos = event.pos()
option.widget.viewport().update()
elif event.type() == event.MouseButtonRelease:
if self._pressIndex == index and event.pos() in option.rect:
model.setData(index, not index.data(), Qt.EditRole)
self._pressIndex = self._mousePos = None
option.widget.viewport().update()
elif event.type() == event.KeyPress:
if event.key() == Qt.Key_Space:
value = not index.data()
model.setData(index, value, Qt.EditRole)
newIndex = model.index(index.row(), self.trueColumn + (not value))
option.widget.setCurrentIndex(newIndex)
option.widget.viewport().update()
return super().editorEvent(event, model, option, index)
# ...
delegate = PaintButtonDelegate(1, tableView)
tableView.setItemDelegateForColumn(1, delegate)
tableView.setItemDelegateForColumn(2, delegate)
Note that in this case, if you want to keep a valid keyboard (Tab) navigation, the model also requires adjustments:
class TableModel(QAbstractTableModel):
# ...
def flags(self, index):
flags = super().flags(index)
if 0 < index.column() < 3:
if index.data() == index.column() - 1:
flags &= ~Qt.ItemIsEnabled
return flags
This unfortunately results in unexpected behavior of the horizontal header, as only the enabled columns will be "highlighted" with some specific styles.
That said, the other important drawback of this approach is that you will completely lose any animation provided by the style: since the style uses actual widgets to create visual animations, and the painting is only based on the current QStylOption value, those animations will not be available.
I have implemented a delegate and model on a TreeView, which contains an spinbox on the column number one, but the number format is not the correct one. But, after I click the spinbox, it is setted correctly. I am using PyQt5.
Here I attached two images which explain it much better.
Value setted on a first time
The value is seen correctly after click the spinbox
Here is the code where spinbox are initialized on a first time:
def createEditor(self, parent, option, index):
item = index.internalPointer()
if index.column() == 0:
editor = super().createEditor(parent, option, index)
elif index.column() == 1:
name = item.get_data()[0]
data = item.get_data()[1]
editor = QtWidgets.QSpinBox(parent, minimum=-9999999999, maximum=9999999999)
# editor.setEditTriggers(QAbstractItemView.SelectedClicked)
editor.setObjectName(name)
editor.setValue(data)
editor.setStyleSheet("""
border-radius: 0px;
""")
editor.installEventFilter(self)
else:
editor = super().createEditor(parent, option, index)
return editor
I think that I missed a comitData somewhere, but I don't know where is the better place to emit it, or if it is the correct way to do that.
Thanks you,
Marcel
The problem was resolved.
It was not in the delegate, and it has not got any relationship about QSpinBox, but the model and str format.
On QtCore.Qt.DisplayRole role on function data, I did the correct conversion, as I show on the following code:
def data(self, in_index, role):
if not in_index.isValid():
return QVariant()
node = in_index.internalPointer()
if role == QtCore.Qt.DisplayRole:
if node.data(in_index.column()) is None:
return ''
if in_index.column() == 1:
return str(float(node.data(in_index.column())))
return str(node.data(in_index.column()))
Thanks you all.
Regards,
Marcel
In PyQt, I am looking for a way to set the height of rows in a QTreeView (similarly to QTableView.setRowHeight(row, row_height), but QTreeView does not have this function). QAbstractItemModel is used to set the tree model. I read some suggestions here using and sub-classing QAbstractItemDelegate.sizeHint(option, index) but I don't know exactly how to call them correctly within my tree model.
Any minimal code or suggestion would be greatly appreciated. Thanks.
The QTreeView works out a height for each row based on the data and size hints returned for each item. I think you probably just need to return size hints, either for all your items, or at least the first row (if you have setUniformRowHeights(True)). Incidentally this can significantly improve performance and so you should set it if you can.
So you just need to implement your AbstractItemModel.data() method to return a size hint in the SizeHintRole. Something like this:
def data(self, index, role = QtCore.Qt.DisplayRole):
# Check the index, possibly return None
if role == QtCore.Qt.DisplayRole:
# Return the data
elif role == QtCore.Qt.SizeHintRole:
return QtCore.QSize(item_width,item_height)
# Other roles - maybe return None if you don't use them.
EDIT: Big example
You say you are still having trouble so here is a complete working example, based on the standard QT itemviews example. Try varying the QSize returned in the data method to see how the view changes:
import sys
from PySide import QtCore,QtGui
class TreeItem(object):
def __init__(self, data, parent=None):
self.parentItem = parent
self.data = data
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self.rootItem = TreeItem(None)
for i,c in enumerate("abcdefg"):
child = TreeItem([i,c],self.rootItem)
self.rootItem.appendChild(child)
parent = self.rootItem.childItems[1]
child = TreeItem(["down","down"],parent)
parent.appendChild(child)
def columnCount(self, parent):
return 2
def data(self, index, role):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole:
item = index.internalPointer()
return item.data[index.column()]
elif role == QtCore.Qt.SizeHintRole:
print "giving size hint"
return QtCore.QSize(40,40)
return None
def flags(self, index):
if not index.isValid():
return QtCore.Qt.NoItemFlags
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return ["A","B"][section]
return None
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.childItems[row]
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
parentItem = index.internalPointer().parentItem
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return len(parentItem.childItems)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
model = TreeModel()
view = QtGui.QTreeView()
view.setModel(model)
view.setWindowTitle("Simple Tree Model")
view.show()
sys.exit(app.exec_())
It's pretty simple to do this with stylesheet :
self.setStyleSheet("QTreeView::item { padding: 10px }")
Don't know about Python, but C++ code would be like this:
model->setData(model->index(/*your index*/), QSize(20, 20), Qt::SizeHintRole);
And you need to set it for all items in your tree.
If you want use QItemDelegate - you no need to call this function, you just set your delegate to view, like this (C++ code again, but main idea is the same):
treeView->setItemDelegate(new MyDelegate(this));
Than view will use it when it need it.
For the PySide bindings, here's the Python code for the custom delegate on the AbstractItemModel.
from PySide import QtCore, QtGui
tree = QTreeView()
tree.model = QtGui.QAbstractItemModel()
tree.setModel(tree.model)
size = QtCore.QSize(20, 20)
index = tree.model.index(row, col) # row, col are your own
tree.model.setData(index, size, QtCore.Qt.SizeHintRole)
delegate = MyDelegate()
tree.setItemDelegate(delegate)
This is just translating the code from #RazrFalcon to Python, and was requested by another user.
With Qt 5.9.2, I had trouble with simultaneously getting QTreeView auto-sizing columns and custom row heights. Using SizeHintRole worked for height but seemingly wrecked auto-width. The solution for me was to set the rows to have uniform heights and then do something like this in Python 3.7:
self._tree = QTreeView()
self._tree.setAlternatingRowColors(True)
self._tree.setUniformRowHeights(True)
...
fi = QFontInfo(self.font())
self._tree.setStyleSheet(
f'''
QTreeView {{
alternate-background-color: #E6FFFF;
}}
QTreeView::Item{{
height:{fi.pixelSize() * 2}px;
}}
''')
I'll speculate that setting colors and heights this way provides a tiny bit of a performance improvement.
I have a QTreeView of a QStandardItemModel, and want one column to be checkboxes only (no text, just checkboxes). This has been discussed for QAbstractItemModel. I have tried to implement the solution there (see SSCCE below), but only a single checkbox shows up, in the first row of the view. I am not sure what extra magic needs to be added for the checkbox to show up in every row.
Note, as discussed at this related question, I want a column of checkboxes with no text fields, so simply applying setCheckable(True) to each item in a column will not be enough, as that leaves an (empty) text field next to the checkbox.
SSCCE
# -*- coding: utf-8 -*-
from PySide import QtGui, QtCore
import sys
class CheckBoxDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent = None):
QtGui.QStyledItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
return None
def paint(self, painter, option, index):
checked = bool(index.model().data(index, QtCore.Qt.DisplayRole))
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)
QtGui.QApplication.style().drawControl(QtGui.QStyle.CE_CheckBox, check_box_style_option, painter)
def editorEvent(self, event, model, option, index):
if not (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
# 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
def setModelData (self, editor, model, index):
newValue = not bool(index.model().data(index, QtCore.Qt.DisplayRole))
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())
#BUILD THE TREE
app = QtGui.QApplication(sys.argv)
model = QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(['Title', 'Summary', 'Checkbox'])
rootItem = model.invisibleRootItem()
#Adding branches and leaves to root
item0 = [QtGui.QStandardItem('Title0'), QtGui.QStandardItem('Summary0')]
item00 = [QtGui.QStandardItem('Title00'), QtGui.QStandardItem('Summary00')]
rootItem.appendRow(item0)
item0[0].appendRow(item00)
treeView= QtGui.QTreeView()
treeView.setModel(model)
treeView.expandAll()
treeView.show()
#PUT THE CHECKBOX IN COLUMN 2
myDelegate = CheckBoxDelegate()
treeView.setItemDelegateForColumn(2, myDelegate)
sys.exit(app.exec_())
There is a problem with the number of columns: there's only two columns, except for the top level items. The delegate is set on the third column, so it's only set for the top level items.
You define you lines with:
item1 = [QtGui.QStandardItem('Title1'), QtGui.QStandardItem('Summary1')]
Here, clearly, there's only two columns. But before that, you set the header labels with:
model.setHorizontalHeaderLabels(['Title', 'Summary', 'Checkbox'])
I guess this line set that there is three columns for the top level items, but it doesn't have any effect on the child items.
If you directly create lines with three items, your problem is solved:
item0 = [QtGui.QStandardItem('Title0'), QtGui.QStandardItem('Summary0'), QtGui.QStandardItem()]
item00 = [QtGui.QStandardItem('Title00'), QtGui.QStandardItem('Summary00'), QtGui.QStandardItem()]
I think you don't need an item delegate at all. Simply consider using QStandardItem::setFlags() function along with the Qt::ItemIsUserCheckable flag for each QStandardItem you add to your tree.
I have QTableWidget with CheckBoxes in some cells. I want to disable user to perform mouse click over the table cells (so he can't change checkBox state) for some time while I am using data from the table. I've tried table.setDisabled(1) but that disables whole table and I need scroll to be enabled.
Any help would be appreciated.
EDIT
To be more precise: there could be up to 15x3000 cells in table, filled with text(editable), checkbox(checkable), svg graphic(opens other window when double click on it) or some custom widgets(which also have clickable or editable parts). I need to disable user to click or double click over cells(so he can't change any of them) for 1sec - 10sec time interval (solution must be something fast, not iterating through all items), but I need scroll-bar to be enabled and normal table visibility.
One way to achieve this is to subclass QTableWidgetItem and re-implement the setData method. That way, you can control whether items accept values for certain roles.
To control the "checkability" for all items, you could add a class attribute to the subclass which could be tested whenever a value for the check-state role was passed to setData.
Here's what the subclass might look like:
class TableWidgetItem(QtGui.QTableWidgetItem):
_blocked = True
#classmethod
def blocked(cls):
return cls._checkable
#classmethod
def setBlocked(cls, checkable):
cls._checkable = bool(checkable)
def setData(self, role, value):
if role != QtCore.Qt.CheckStateRole or self.checkable():
QtGui.QTableWidgetItem.setData(self, role, value)
And the "checkability" of all items would be disabled like this:
TableWidgetItem.setCheckable(False)
UPDATE:
The above idea can be extended by adding a generic wrapper class for cell widgets.
The classes below will block changes to text and check-state for table-widget items, and also a range of keyboard and mouse events for cell widgets via an event-filter (other events can be blocked as required).
The cell-widgets would need to be created like this:
widget = CellWidget(self.table, QtGui.QLineEdit())
self.table.setCellWidget(row, column, widget)
and then accessed like this:
widget = self.table.cellWidget().widget()
Blocking for the whole table would be switched on like this:
TableWidgetItem.setBlocked(True)
CellWidget.setBlocked(True)
# or Blockable.setBlocked(True)
Here are the classes:
class Blockable(object):
_blocked = False
#classmethod
def blocked(cls):
return cls._blocked
#classmethod
def setBlocked(cls, blocked):
cls._blocked = bool(blocked)
class TableWidgetItem(Blockable, QtGui.QTableWidgetItem):
def setData(self, role, value):
if (not self.blocked() or (
role != QtCore.Qt.EditRole and
role != QtCore.Qt.CheckStateRole)):
QtGui.QTableWidgetItem.setData(self, role, value)
class CellWidget(Blockable, QtGui.QWidget):
def __init__(self, parent, widget):
QtGui.QWidget.__init__(self, parent)
self._widget = widget
layout = QtGui.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(widget)
widget.setParent(self)
widget.installEventFilter(self)
if hasattr(widget, 'viewport'):
widget.viewport().installEventFilter(self)
widget.show()
def widget(self):
return self._widget
def eventFilter(self, widget, event):
if self.blocked():
etype = event.type()
if (etype == QtCore.QEvent.KeyPress or
etype == QtCore.QEvent.KeyRelease or
etype == QtCore.QEvent.MouseButtonPress or
etype == QtCore.QEvent.MouseButtonRelease or
etype == QtCore.QEvent.MouseButtonDblClick or
etype == QtCore.QEvent.ContextMenu or
etype == QtCore.QEvent.Wheel):
return True
return QtGui.QWidget.eventFilter(self, widget, event)
Just iterate through all QStandardItems and change flags values for items which should not be changeable.
You can use flag: Qt::ItemIsEditable or/and Qt::ItemIsEnabled.
You would need to disable the items themselves as opposed to the whole table if you have other items than QCheckBoxes that you would not like to disable. See the python code below for details:
'''
Iterate through all the check boxes in the standard items
and make sure the enabled flag is cleared so that the items are disabled
'''
for standardItem in standardItems:
standardItem.setFlags(standardItem.flags() & ~Qt.ItemIsEnabled)
Here you can find the corresponding documentation:
void QTableWidgetItem::setFlags(Qt::ItemFlags flags)
Sets the flags for the item to the given flags. These determine whether the item can be selected or modified.