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)
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.
How can I add children to my current qtreeview items in the code i provided below? I want it to look something like this:
The code below creates a treeview that is sorted but im not entirely clear on how to add children items. Hope you can help, thanks.
import sys
from PySide import QtGui, QtCore
class SortModel(QtGui.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(SortModel, self).__init__(*args, **kwargs)
def lessThan(self, left, right):
leftData = self.sourceModel().data(left)
rightData = self.sourceModel().data(right)
if leftData:
leftData = leftData.lower()
if rightData:
rightData = rightData.lower()
print('L:', leftData, 'R:', rightData)
return leftData < rightData
class Browser(QtGui.QDialog):
def __init__(self, parent=None):
super(Browser, self).__init__(parent)
self.initUI()
def initUI(self):
self.resize(200, 300)
self.setWindowTitle('Assets')
self.setModal(True)
self.results = ""
self.uiItems = QtGui.QTreeView()
self.uiItems.setAlternatingRowColors(True)
self.uiItems.setSortingEnabled(True)
self.uiItems.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiItems.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.uiItems.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
self._model = self.create_model(self)
self._spmodel = SortModel(self)
self._spmodel.setSourceModel(self._model)
self._spmodel.setDynamicSortFilter(True)
self.uiItems.setModel(self._spmodel)
grid = QtGui.QGridLayout()
grid.setContentsMargins(0, 0, 0, 0)
grid.addWidget(self.uiItems, 0, 0)
self.setLayout(grid)
self.setLayout(grid)
self.uiItems.doubleClicked.connect(self.doubleClickedItem)
self.show()
def doubleClickedItem(self, item):
name = item.data(role=QtCore.Qt.DisplayRole)
print name
def create_model(self, parent):
items = [
'Cookie dough',
'Hummus',
'Spaghetti',
'Dal makhani',
'Chocolate whipped cream'
]
model = QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(['Name'])
for item in items:
model.appendRow(QtGui.QStandardItem(item))
return model
def showEvent(self, event):
geom = self.frameGeometry()
geom.moveCenter(QtGui.QCursor.pos())
self.setGeometry(geom)
super(Browser, self).showEvent(event)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Escape:
# self.hide()
self.close()
event.accept()
else:
super(Browser, self).keyPressEvent(event)
def main():
app = QtGui.QApplication(sys.argv)
ex = Browser()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You would do this in your create_model method. There are several different ways of appending child rows and columns. Instead of passing a QStandardItem directly to model.appendRow, create it and save it in a variable. Then you can use QStandardItem.appendRow to add children to your top level rows.
I don't know Python and may get the syntax wrong, but the basic pattern is something like this:
std_item = QtGui.QStandardItem ("Dinner")
child_std_item = QtGui.QStandardItem ("Drinks")
std_item.appendRow (child_std_item)
Alternately, you can do this at the model level using model.insertRow and specify the QModelIndex of the parent item. You can get the QModelIndex of an item using model.indexFromItem. Hopefully that's enough to get you going. Each QStandardItem knows its parent, if any, and its children, so it's usually a matter of having the parent available to add/change/remove children.
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.
With PyQt4, I am using a QtableView with more than 10 columns. The user must have the choice of showing/hiding a column.
This is generally done by adding a small button in the top-right of the table's header. The button shows a menu with checked/unchecked Checkboxes allowing to hide/show columns.
This is an example from Sqlite-Manager Table.
So, I wonder how can I do the same with PyQt's QtableView?
Thanks,
Thank you Kitsune Meyoko, it was a great Idea.. ;)
I found another solution pretty much like yours by using QMenu with Checkable QActions instead of a QPushButton: Let's Go:
import sys
import string
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Header(QHeaderView):
def __init__(self, parent=None):
super(Header, self).__init__(Qt.Horizontal, parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.ctxMenu)
self.setup()
#pyqtSlot(bool)
def printID(self, i):
print("id")
if i == False:
self.hideSection(0)
else:
self.showSection(0)
#pyqtSlot(bool)
def printNAME(self, i):
print("name")
if i == False:
self.hideSection(1)
else:
self.showSection(1)
#pyqtSlot(bool)
def printUSERNAME(self, i):
print("username")
if i == False:
self.hideSection(2)
else:
self.showSection(2)
def setup(self):
self.id = QAction("id",self)
self.id.setCheckable(True)
self.id.setChecked(True)
self.connect(self.id, SIGNAL("triggered(bool)"), self, SLOT("printID(bool)"))
self.name = QAction("name",self)
self.name.setCheckable(True)
self.name.setChecked(True)
self.connect(self.name, SIGNAL("triggered(bool)"), self, SLOT("printNAME(bool)"))
self.username = QAction("username",self)
self.username.setCheckable(True)
self.username.setChecked(True)
self.connect(self.username, SIGNAL("triggered(bool)"), self, SLOT("printUSERNAME(bool)"))
def ctxMenu(self, point):
menu = QMenu(self)
self.currentSection = self.logicalIndexAt(point)
menu.addAction(self.id)
menu.addAction(self.name)
menu.addAction(self.username)
menu.exec_(self.mapToGlobal(point))
class Table(QTableWidget):
def __init__(self, parent=None):
super(Table, self).__init__(parent)
self.setHorizontalHeader(Header(self))
self.setColumnCount(3)
self.setHorizontalHeaderLabels(['id', 'name', 'username'])
self.populate()
def populate(self):
self.setRowCount(10)
for i in range(10):
for j,l in enumerate(string.ascii_letters[:3]):
self.setItem(i, j, QTableWidgetItem(l))
if __name__ == '__main__':
app = QApplication(sys.argv)
t = Table()
t.show()
app.exec_()
sys.exit()
In QTableView not have kind of button just like "Sqlite-Manager Table". But your can custom widget by using QtGui.QPushButton and work with QtGui.QMenu together to get column from user. And use QTableView.hideColumn (self, int column) & QTableView.showColumn (self, int column) to hide show your column;
Full example;
import sys
import random
from functools import partial
from PyQt4 import QtGui
class QCustomTableViewWidget (QtGui.QWidget):
def __init__ (self, myQStandardItemModel, *args, **kwargs):
super(QCustomTableViewWidget, self).__init__(*args, **kwargs)
# Layout setup
self.localQTableView = QtGui.QTableView()
self.rightCornerQPushButton = QtGui.QPushButton()
menuQHBoxLayout = QtGui.QHBoxLayout()
menuQHBoxLayout.addStretch(1)
menuQHBoxLayout.addWidget(self.rightCornerQPushButton)
allQVBoxLayout = QtGui.QVBoxLayout()
allQVBoxLayout.addLayout(menuQHBoxLayout)
allQVBoxLayout.addWidget(self.localQTableView)
self.setLayout(allQVBoxLayout)
# Object setup
self.localQTableView.setModel(myQStandardItemModel)
self.rightCornerQPushButton.setText('Show column')
currentQMenu = QtGui.QMenu()
for column in range(myQStandardItemModel.columnCount()):
currentQAction = QtGui.QAction('Column %d' % (column + 1), currentQMenu)
currentQAction.setCheckable(True)
currentQAction.setChecked(True)
currentQAction.toggled.connect(partial(self.setColumnVisible, column))
currentQMenu.addAction(currentQAction)
self.rightCornerQPushButton.setMenu(currentQMenu)
def setColumnVisible (self, column, isChecked):
if isChecked:
self.localQTableView.showColumn(column)
else:
self.localQTableView.hideColumn(column)
def tableView (self):
return self.localQTableView
# Simulate data
myQStandardItemModel = QtGui.QStandardItemModel()
for _ in range(10):
myQStandardItemModel.appendRow([QtGui.QStandardItem('%d' % random.randint(100, 999)), QtGui.QStandardItem('%d' % random.randint(100, 999)), QtGui.QStandardItem('%d' % random.randint(100, 999))])
# Main application
myQApplication = QtGui.QApplication(sys.argv)
myQCustomTableViewWidget = QCustomTableViewWidget(myQStandardItemModel)
myQCustomTableViewWidget.show()
sys.exit(myQApplication.exec_())