PyQt: How Can I set row heights of QTreeView - qt

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.

Related

Iterate through a pyqt pyside QtreeView with a filesystem model and format value based on a condition [duplicate]

I'm having a QListView with a QFileSystemModel. Based on a selection in a QTreeView, the QListView shows the content of the folder.
Now I need to change the color of the filenames depending on some condition.
The initial idea would be to iterate over the items in the QListView and set the color for each item depending on whether the condition is fulfilled. However this seems to be impossible, since the setData() method of QFileSystemModel only accepts changes to the EditRole, ignoring something like [see this]
self.FileModel.setData(index, QtGui.QBrush(QtCore.Qt.red), role=QtCore.Qt.ForegroundRole)
This has also been pointed out here
and the suggestion in the latter was to subclass QItemDelegate for the purpose of colorizing items in the QListView.
I therefore subclassed QStyledItemDelegate and reimplemented its paint() method to show the filename in green, if the condition is fulfilled - which works fine. However it now looks kind of ugly: File icons are lost and the "mouse_over" effect is not working anymore.
While this subclassing is anyway a messy work-around, my top-level question would be
Is there a way to colorize items in a QListView connected to a QFileSystemModel based on a condition?
Now provided that this might not be the case and sticking to the subclassing of QItemDelegate,
Is there a way to get the original behaviour with nice selections and icons back?
Does anyone know which ItemDelegate is originally used for QFileSystemModel in a QListView and how to use it?
Is it possible to get its source code and copy the paint method from there ?
Here is a minimal code that uses subclassing and shows the descibed behaviour. It uses a QLineEdit where one can type in a string, such that all files containing that string are highlighted in green.
import sys
from PyQt4 import QtGui, QtCore
class MyFileViewDelegate(QtGui.QStyledItemDelegate ):
def __init__(self, parent=None, *args, **kwargs):
QtGui.QItemDelegate.__init__(self, parent, *args)
self.condition = None
self.isMatch = False
self.brush_active = QtGui.QBrush(QtGui.QColor("#79b9ed"))
self.brush_active_matched = QtGui.QBrush(QtGui.QColor("#58cd1c"))
self.pen = QtGui.QPen(QtGui.QColor("#414141") )
self.pen_matched = QtGui.QPen(QtGui.QColor("#39c819") )
self.pen_active = QtGui.QPen(QtGui.QColor("#eef2fd") )
self.pen_active_matched = QtGui.QPen(QtGui.QColor("#e7fade") )
def paint(self, painter, option, index):
text = index.data(QtCore.Qt.DisplayRole)
self.matchText(text)
painter.save()
######## set background
painter.setPen(QtGui.QPen(QtCore.Qt.NoPen))
if option.state & QtGui.QStyle.State_Selected:
if self.isMatch:
painter.setBrush(self.brush_active_matched)
else:
painter.setBrush(self.brush_active)
painter.drawRect(option.rect)
######## set font color
if option.state & QtGui.QStyle.State_Selected:
if self.isMatch:
painter.setPen(self.pen_active_matched)
else:
painter.setPen(self.pen_active)
else:
if self.isMatch:
painter.setPen(self.pen_matched)
else:
painter.setPen(self.pen)
painter.drawText(option.rect, QtCore.Qt.AlignLeft, text)
painter.restore()
def matchText(self, filename):
# testing condition. In the real case this is much more complicated
if (self.condition != None) and (self.condition != "") and (self.condition in filename):
self.isMatch = True
else:
self.isMatch = False
def setCondition(self, condition):
self.condition = condition
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None, useDelegate = False):
super(MainWidget, self).__init__(parent)
self.setLayout(QtGui.QVBoxLayout())
self.FolderModel = QtGui.QFileSystemModel()
self.FolderModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.AllDirs)
self.FolderModel.setRootPath("")
self.FolderView = QtGui.QTreeView(parent=self)
self.FolderView.setModel(self.FolderModel)
self.FolderView.setHeaderHidden(True)
self.FolderView.hideColumn(1)
self.FolderView.hideColumn(2)
self.FolderView.hideColumn(3)
self.FolderView.expanded.connect(self.FolderView.scrollTo)
self.FolderView.clicked[QtCore.QModelIndex].connect(self.browserClicked)
self.FileModel = QtGui.QFileSystemModel()
self.FileModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Files)
self.FileView = QtGui.QListView(parent=self)
self.FileView.setModel(self.FileModel)
self.FileViewDelegate = None
if useDelegate:
self.FileViewDelegate = MyFileViewDelegate()
self.FileView.setItemDelegate(self.FileViewDelegate)
self.FileView.setSelectionMode( QtGui.QAbstractItemView.ExtendedSelection )
self.LineEdit = QtGui.QLineEdit()
self.LineEdit.textChanged.connect(self.changeCondition)
# Add Widgets to layout
self.layout().addWidget(self.FolderView)
self.layout().addWidget(self.FileView)
self.layout().addWidget(self.LineEdit)
def changeCondition(self, text):
if self.FileViewDelegate:
self.FileViewDelegate.setCondition(text)
def browserClicked(self, index):
# the signal passes the index of the clicked item
# set the FileView's root_index to the clicked index
dir_path = self.FileModel.filePath(index)
root_index = self.FileModel.setRootPath(dir_path)
self.FileView.setRootIndex(root_index)
class App(QtGui.QMainWindow):
def __init__(self, parent=None, useDelegate=False):
super(App, self).__init__(parent)
self.central = MainWidget(parent =self, useDelegate=useDelegate)
self.setCentralWidget(self.central)
if __name__=='__main__':
app = QtGui.QApplication(sys.argv)
thisapp = App(None, True) # set False to view App without custom FileViewDelegate
thisapp.show()
sys.exit(app.exec_())
This is the comparison of how it looks with and without subclassing QItemDelegate:
just to mention, another problem with this code is, that once the condition is changed, one needs to move the mouse into the QFileView to initiate the repainting. I wonder which slot I could use to connect to the LineEdit.textChange signal to do that directly.
There's no need for an item-delegate. It can be achieved much more simply by reimplementing the data method of the QFileSystemModel:
class FileSystemModel(QtGui.QFileSystemModel):
def __init__(self, *args, **kwargs):
super(FileSystemModel, self).__init__(*args, **kwargs)
self.condition = None
def setCondition(self, condition):
self.condition = condition
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
def data(self, index, role=QtCore.Qt.DisplayRole):
if self.condition and role == QtCore.Qt.TextColorRole:
text = index.data(QtCore.Qt.DisplayRole)
if self.condition in text:
return QtGui.QColor("#58cd1c")
return super(FileSystemModel, self).data(index, role)
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None, useDelegate = False):
super(MainWidget, self).__init__(parent)
...
self.FileModel = FileSystemModel(self)
...
def changeCondition(self, text):
self.FileModel.setCondition(text)

QStyledItemDelegate disabling button in last column moves selection to next row

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.

QTableView header word wrap

I'm trying to set the horizontal and vertical headers in QTableView to word wrap but without any success.
I want to set all columns to be the same width (including the vertical header), and those columns that have multiline text to word wrap. If word is wider than the column it should elide right.
I've managed to set the elide using QTableView -> horizontalHeader() -> setTextElideMode(Qt::ElideRight), but I can't do the same for word wrap since QHeaderView doesn't have setWordWrap method. So event if text is multiline it will just elide. Setting the word wrap on the table view doesn't do anything. The table cells contain only small numbers so the issue is only with the headers, and I want to avoid using '/n' since the headers are set dynamically. Is there maybe some other setting I've changed that's not allowing word wrap to function?
I was able to consolidate the two approaches above (c++, Qt 5.12) with a pretty nice result. (no hideheaders on the model)
Override QHeaderView::sectionSizeFromContents() such that size accounts for text wrapping
QSize MyHeaderView::sectionSizeFromContents(int logicalIndex) const
{
const QString text = this->model()->headerData(logicalIndex, this->orientation(), Qt::DisplayRole).toString();
const int maxWidth = this->sectionSize(logicalIndex);
const int maxHeight = 5000; // arbitrarily large
const auto alignment = defaultAlignment();
const QFontMetrics metrics(this->fontMetrics());
const QRect rect = metrics.boundingRect(QRect(0, 0, maxWidth, maxHeight), alignment, text);
const QSize textMarginBuffer(2, 2); // buffer space around text preventing clipping
return rect.size() + textMarginBuffer;
}
Set the default alignment to have word wrap (optionally, center)
tableview->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter | (Qt::Alignment)Qt::TextWordWrap);
I've managed to find the solution using subclassing of QHeaderView and reimplementing sectionSizeFromContents and paintSection methods in it. Here's the demo in PyQt5 (tested with Python 3.5.2 and Qt 5.6):
import sys
import string
import random
from PyQt5 import QtCore, QtWidgets, QtGui
class HeaderViewWithWordWrap(QtWidgets.QHeaderView):
def __init__(self):
QtWidgets.QHeaderView.__init__(self, QtCore.Qt.Horizontal)
def sectionSizeFromContents(self, logicalIndex):
if self.model():
headerText = self.model().headerData(logicalIndex,
self.orientation(),
QtCore.Qt.DisplayRole)
options = self.viewOptions()
metrics = QtGui.QFontMetrics(options.font)
maxWidth = self.sectionSize(logicalIndex)
rect = metrics.boundingRect(QtCore.QRect(0, 0, maxWidth, 5000),
self.defaultAlignment() |
QtCore.Qt.TextWordWrap |
QtCore.Qt.TextExpandTabs,
headerText, 4)
return rect.size()
else:
return QtWidgets.QHeaderView.sectionSizeFromContents(self, logicalIndex)
def paintSection(self, painter, rect, logicalIndex):
if self.model():
painter.save()
self.model().hideHeaders()
QtWidgets.QHeaderView.paintSection(self, painter, rect, logicalIndex)
self.model().unhideHeaders()
painter.restore()
headerText = self.model().headerData(logicalIndex,
self.orientation(),
QtCore.Qt.DisplayRole)
painter.drawText(QtCore.QRectF(rect), QtCore.Qt.TextWordWrap, headerText)
else:
QtWidgets.QHeaderView.paintSection(self, painter, rect, logicalIndex)
class Model(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.model_cols_names = [ "Very-very long name of my first column",
"Very-very long name of my second column",
"Very-very long name of my third column",
"Very-very long name of my fourth column" ]
self.hide_headers_mode = False
self.data = []
for i in range(0, 10):
row_data = []
for j in range(0, len(self.model_cols_names)):
row_data.append(''.join(random.choice(string.ascii_uppercase +
string.digits) for _ in range(6)))
self.data.append(row_data)
def hideHeaders(self):
self.hide_headers_mode = True
def unhideHeaders(self):
self.hide_headers_mode = False
def rowCount(self, parent):
if parent.isValid():
return 0
else:
return len(self.data)
def columnCount(self, parent):
return len(self.model_cols_names)
def data(self, index, role):
if not index.isValid():
return None
if role != QtCore.Qt.DisplayRole:
return None
row = index.row()
if row < 0 or row >= len(self.data):
return None
column = index.column()
if column < 0 or column >= len(self.model_cols_names):
return None
return self.data[row][column]
def headerData(self, section, orientation, role):
if role != QtCore.Qt.DisplayRole:
return None
if orientation != QtCore.Qt.Horizontal:
return None
if section < 0 or section >= len(self.model_cols_names):
return None
if self.hide_headers_mode == True:
return None
else:
return self.model_cols_names[section]
class MainForm(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.model = Model()
self.view = QtWidgets.QTableView()
self.view.setModel(self.model)
self.view.setHorizontalHeader(HeaderViewWithWordWrap())
self.setCentralWidget(self.view)
def main():
app = QtWidgets.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
An open Qt issue from 2010 on this suggests that this may not be easily possible.
However, according to the only comment from 2015, there is a simple workaround for this very issue which goes like this:
myTable->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter | (Qt::Alignment)Qt::TextWordWrap);
I just tested with Qt 5.12 and fortunately found it still working.
In python:
myTableView.horizontalHeader().setDefaultAlignment(Qt.AlignCenter | Qt.Alignment(Qt.TextWordWrap))

setStyleSheet or QStyledItemDelegate for a node in QTreeView

I am trying to change a icon for specific nodes in a QTreeView. Nodes will use QCheckbox, and based on app logic, some nodes will instead use other icons for node selected/unselected state.
An example below shows how I can set a stylesheet for the entire tree, but I don't think I can set a stylesheet for a single node in the tree.
#Based on http://stackoverflow.com/questions/5374168/unable-to-select-checkbox-inside-treeview
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
changeStyleTriggered = QtCore.pyqtSignal()
def __init__(self):
QtGui.QWidget.__init__(self)
self.createTree()
self.connectSlots()
def createTree(self):
data = ['A', 'B', 'C', 'D', 'E', 'F']
model = MyTreeView(data)
self.treeView = QTreeView()
self.treeView.setModel(model)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.treeView)
self.changeIconPushButton = QtGui.QPushButton("Change a single icon")
layout.addWidget(self.changeIconPushButton)
self.setLayout(layout)
def fireChangeStyleSignal(self):
self.changeStyleTriggered.emit()
#pyqtSlot()
def changeStyleSignal(self):
print(">>changeStyleSignal()")
self.setStyleSheet("QTreeView::indicator:unchecked {image: url(:/icons/image.png);}")
def connectSlots(self):
self.changeStyleTriggered.connect(self.changeStyleSignal)
self.changeIconPushButton.clicked.connect(self.fireChangeStyleSignal)
class TestItem():
def __init__(self, name, checked):
self.checked = checked
self.name = name
class MyTreeView(QAbstractListModel):
def __init__(self, args, parent=None):
super(StbTreeView, self).__init__(parent)
self.args = []
for item_name in args:
self.args.append(TestItem(item_name, False))
for item in self.args:
print (item.name)
def rowCount(self, parent):
return len(self.args)
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return "Nodes"
def flags(self, index):
return Qt.ItemIsUserCheckable | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsEnabled
def data(self, index, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
row = index.row()
print (self.args[row].name)
return self.args[row].name
if role == Qt.CheckStateRole:
row = index.row()
print (self.args[row].checked)
if self.args[row].checked == False:
return Qt.Unchecked
else:
return Qt.Checked
def setData(self, index, value, role):
if role == Qt.CheckStateRole:
row = index.row()
self.args[row].checked = not self.args[row].checked
return True
def main():
myapp = QApplication(sys.argv)
window = Window()
window.show()
myapp.exec_()
if __name__ == '__main__':
main()
The other option is to use QStyledItemDelegate. Below is a small class to change the font, but I haven't worked out how to change a single icon with delegate.
class BoldDelegate(QtGui.QStyledItemDelegate):
def paint(self, painter, option, index):
option.font.setWeight(QtGui.QFont.Bold)
QtGui.QStyledItemDelegate.paint(self, painter, option, index)
and to use it run this (eg in a #pyqtSlot() for the QTreeView):
self.setItemDelegate(BoldDelegate(self))
Any ideas?
A QCheckBox doesn't use an icon to display its state. You should first create a project where, through whatever means, you get a widget that acts like you wish, with correct checked/unchecked appearance.
Once you have that, you'll have your delegate create such items based on some property of the data item of the model. For example, you might have a custom role that indicates to your delegate that a non-default widget class should be used to edit the item.

View QStandardItemModel with column of checkboxes in PySide/PyQt?

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.

Resources