How to show only part of model in QTreeView? - qt

I've been playing around with this code and I don't understand, how to adjust model columns visibility.
for i in range(3):
parent1 = QStandardItem('Family {}. Some long status text for sp'.format(i))
for j in range(3):
child1 = QStandardItem('Child {}'.format(i*3+j))
child2 = QStandardItem('row: {}, col: {}'.format(i, j+1))
child3 = QStandardItem('row: {}, col: {}'.format(i, j+2))
parent1.appendRow([child1, child2, child3])
model.appendRow(parent1)
# span container columns
view.setFirstColumnSpanned(i, view.rootIndex(), True)
I want to have one model with many fields and several views, each displaying only specific columns.
I don't want to hide columns with setColumnHidden, because when model gets extended, I'll have to hide new columns.
I'm familiar with Gtk and it's quite simple there: you create a TreeView, manually add columns, fill them with renderers and tell each renderer, from which column in model it should take data. Is it possible with Qt?

You can use a QSortFilterProxyModel:
class FilterColumnModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super(FilterColumnModel, self).__init__(parent)
self._columns = None
#property
def columns(self):
return self._columns
#columns.setter
def columns(self, columns):
self._columns = columns[:]
self.invalidateFilter()
def filterAcceptsColumn(self, source_column, source_parent):
if self.columns is None:
return True
return source_column in self.columns
filter_model = FilterColumnModel()
filter_model.setSourceModel(model)
filter_model.columns = [0, 2]
view.setModel(filter_model)
view.setUniformRowHeights(True)

Related

How to hide the first column using a QSortFilterProxyModel with a Qabstractitemmodel

In widget my implementation, model the source model (subclassed from a QAbstractItemModel), proxy_model (subclassed from QSortFilterProxyModel) the proxy, and tree (QTreeView) the tree representing the model.
I want to hide the first column.
I tried to use tree.hideColumn(0), the the tree is shown flat.
If I subclass filterAcceptsColumn in the proxy to return True only for the second column,, then no rows are shown.
I believe this is because the parent/child relationships are anchored on the first column in the indexes, and when the proxy ask for the number of rows for a given index of column 1, the model returns 0 (which is the expected behavior in the model implementation if I understood well).
If I set rowCount to return non 0 values in the model for columns index > 0, I can see the tree and the rows, but then the model is not passing the QAbstractItemModelTester test with the folloing error:
qt.modeltest: FAIL! childIndex != childIndex1 () returned FALSE
I understand well that in the tree model, child index must be attached to a single parent index (the first column).
But how am I supposed to be hiding the first column in a proxy model if the parent child relationship of the source model are not retained by the proxy if the first column is filtered? I feel it is a "bug" from the proxy, or I missed something !
Do anyone know the proper way of filtering/hiding the first column in the tree view without losing the parent/child information, and still validating a qmodel implementation?
Thanks !
A proper and correct implementation would at least require the proxy to create indexes for the parent of the second column, requiring correct implementation of index(), parent(), mapToSource() and mapFromSource(). For tree models that can be really tricky.
If the source model is not too complex and all its functions are correctly implemented, a possible workaround could be to just override the data() (and headerData) of the proxy and always return the sibling of the next column.
The following test is done with a simple QStandardItemModel, but I don't think using a QAbstractItemModel should be any different, as long as it's correctly implemented.
from PyQt5 import QtCore, QtGui, QtWidgets
class ColumnSwapProxy(QtCore.QSortFilterProxyModel):
def data(self, index, role=QtCore.Qt.DisplayRole):
return super().data(index.sibling(index.row(), index.column() + 1), role)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal:
section += 1
return super().headerData(section, orientation, role)
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.combo = QtWidgets.QComboBox()
layout.addWidget(self.combo)
self.tree = QtWidgets.QTreeView()
layout.addWidget(self.tree)
self.model = QtGui.QStandardItemModel()
self.createTree(self.model.invisibleRootItem())
self.tree.setModel(self.model)
self.model.setHorizontalHeaderLabels(
['Root', 'Fake root'] + ['Col {}'.format(c) for c in range(2, 6)])
self.tree.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.proxy = ColumnSwapProxy()
self.proxy.setSourceModel(self.model)
self.combo.addItem('Base model', self.model)
self.combo.addItem('First column hidden', self.proxy)
self.combo.currentIndexChanged.connect(self.setModel)
def setModel(self):
model = self.combo.currentData()
self.tree.setModel(model)
lastColumn = self.model.columnCount() - 1
self.tree.header().setSectionHidden(lastColumn, model == self.proxy)
def createTree(self, parent, level=0):
for r in range(10):
first = QtGui.QStandardItem('Root {} (level {})'.format(level + 1, r + 1))
if level < 2 and not r & 3:
self.createTree(first, level + 1)
row = [first]
for c in range(5):
row.append(QtGui.QStandardItem(
'Column {} (level {})'.format(c + 2, level + 1)))
parent.appendRow(row)
import sys
app = QtWidgets.QApplication(sys.argv)
w = Test()
w.show()
sys.exit(app.exec_())

Implementing a delegate for wordwrap in a QTreeView (Qt/PySide/PyQt)?

I have a tree view with a custom delegate to which I am trying to add word wrap functionality. The word wrapping is working fine, but the sizeHint() seems to not work, so when the text wraps, the relevant row does not expand to include it.
I thought I was taking care of it in sizeHint() by returning document.size().height().
def sizeHint(self, option, index):
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setHtml(text)
document.setTextWidth(option.rect.width())
return QtCore.QSize(document.idealWidth(), document.size().height())
However, when I print out document.size().height() it is the same for every item.
Also, even if I manually set the height (say, to 75) just to check that things will look reasonable, the tree looks like a goldfish got shot by a bazooka (that is, it's a mess):
As you can see, the text in each row is not aligned properly in the tree.
Similar posts
Similar issues have come up before, but no solutions to my problem (people usually say to reimplement sizeHint(), and that's what I am trying):
QTreeWidget set height of each row depending on content
QTreeView custom row height of individual rows
http://www.qtcentre.org/threads/1289-QT4-QTreeView-and-rows-with-multiple-lines
SSCCE
import sys
from PySide import QtGui, QtCore
class SimpleTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self, parent)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setGeometry(500,200, 400, 300)
self.setUniformRowHeights(False) #optimize: but for word wrap, we don't want this!
print "uniform heights in tree?", self.uniformRowHeights()
self.model = QtGui.QStandardItemModel()
self.model.setHorizontalHeaderLabels(['Task', 'Description'])
self.setModel(self.model)
self.rootItem = self.model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('You have been blocked up')]
item00 = [QtGui.QStandardItem('Tickle nose, this is a very long entry. Row should resize.'), QtGui.QStandardItem('Key first step')]
item1 = [QtGui.QStandardItem('<b>Get a job</b>'), QtGui.QStandardItem('Do not blow it')]
self.rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.rootItem.appendRow(item1)
self.setColumnWidth(0,150)
self.expandAll()
self.setWordWrap(True)
self.setItemDelegate(ItemWordWrap(self))
class ItemWordWrap(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
def paint(self, painter, option, index):
text = index.model().data(index)
document = QtGui.QTextDocument() # #print "dir(document)", dir(document)
document.setHtml(text)
document.setTextWidth(option.rect.width()) #keeps text from spilling over into adjacent rect
painter.save()
painter.translate(option.rect.x(), option.rect.y())
document.drawContents(painter) #draw the document with the painter
painter.restore()
def sizeHint(self, option, index):
#Size should depend on number of lines wrapped
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setHtml(text)
document.setTextWidth(option.rect.width())
return QtCore.QSize(document.idealWidth() + 10, document.size().height())
def main():
app = QtGui.QApplication(sys.argv)
myTree = SimpleTree()
myTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
The issue seems to stem from the fact that the value for option.rect.width() passed into QStyledItemDelegate.sizeHint() is -1. This is obviously bogus!
I've solved this by storing the width in the model from within the paint() method and accessing this from sizeHint().
So in your paint() method add the line:
index.model().setData(index, option.rect.width(), QtCore.Qt.UserRole+1)
and in your sizeHint() method, replace document.setTextWidth(option.rect.width()) with:
width = index.model().data(index, QtCore.Qt.UserRole+1)
if not width:
width = 20
document.setTextWidth(width)

Having A Working QSqlRelationalDelegate With QSortFilterProxyModel

I am using QSortFilterProxyModels all the time. However, if a QSqlRelation is setup on the source model, along with a QSqlRelationalDelegate on the view, whenever the view is switched to the proxy model, the QSqlRelationalDelegate disappears, leaving the basic QLineEdit or QSpinBox.
How can I make columns in a view work with both a QSortFilterProxyModel and QSqlRelationalDelegate, giving the expected QCombobox drop down?
This is a better methodology: QSqlRelationalTableModel with QSqlRelationalDelegate not working behind QAbstractProxyModel.
One needs to use mapToSource because the views index can be different then the models index.
class Delegate(QtSql.QSqlRelationalDelegate):
"""
Delegate handles custom editing. This allows the user to have good
editing experience.
Because the join table uses a proxy model a subclass QSqlRelationalDelegate
is required. This is to support the foreign key combobox.
"""
def __init__(self, parent = None):
"""
Class constructor.
"""
# Python super lets you avoid referring to the base class explicitly.
super(Delegate, self).__init__(parent)
def createEditor(self, parent, option, index):
"""
This creates the editors in the delegate.
Reimplemented from QAbstractItemDelegate::createEditor().
Returns the widget used to edit the item specified by
index for editing.
The parent widget and style option are used to control how the
editor widget appears.
1. Get the model associated with the view. In this case it is the
QSortFilterProxyModel.
2. Because with a proxy model the views index does not have to be the
same as the models index. If one sorts,
then the index are not the same.
3. mapToSource.
This is why mapToSource is being used.
mapToSouce Reimplement this function to return the
model index in the proxy model that corresponds to the
sourceIndex from the source model.
4. Return the createEditor with the base index being set to the source
model and not the proxy model.
"""
if index.column() == 2:
proxy = index.model()
base_index = proxy.mapToSource(index)
return super(Delegate, self).createEditor(parent, option, base_index)
else:
return super(Delegate, self).createEditor(parent, option, index)
def setEditorData(self, editor, index):
"""
Once the editor has been created and given to the view
the view calls setEditorData().
This gives the delegate the opportunity to populate the editor
with the current data, ready for the user to edit.
Sets the contents of the given editor to the data for the item
at the given index.
Note that the index contains information about the model being used.
The base implementation does nothing.
If you want custom editing you will need to reimplement this function.
1. Get the model which is a QSortFilterProxyModel.
2. Call mapToSource().
Because with a proxy model the views index does not have to be the
same as the models index. If one sorts,
then the index are not the same.
This is why mapToSource is being used. MapToSouce Reimplement this
function to return the model index in the proxy model
that corresponds to the sourceIndex from the source model.
3. Return setEditorData with the editor and the mapToSource index.
4. Else for all other columns return the base method.
"""
if index.column() == 2:
proxy = index.model()
base_index = proxy.mapToSource(index)
return super(JoinDelegate, self).setEditorData(editor, base_index)
else:
return super(Delegate, self).setEditorData(editor, index)
def setModelData(self, editor, model, index):
if index.column() == 2:
base_model = model.sourceModel()
base_index = model.mapToSource(index)
return super(JoinDelegate, self).setModelData(editor, base_model, base_index)
else:
super(Delegate, self).setModelData(editor, model, index)
def sizeHint(self, option, index):
"""
This pure abstract function must be reimplemented if you want to
provide custom rendering. The options are specified by option and
the model item by index.
"""
if index.isValid():
column = index.column()
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
# change cell Width, height (One can add or subtract to change the relative dimension)
return QtCore.QSize(QtSql.QSqlRelationalDelegate.sizeHint(self, option, index).width() - 200,
QtSql.QSqlRelationalDelegate.sizeHint(self, option, index).height() + 40)
else:
return super(Delegate, self).sizeHint(option, index)
By default, QSqlRelationalDelegate can't handle proxy models, so you have to subclass it. The below is probably far from perfect, so comments/tweaks are welcome, but has been working well on views that have a mixture of QSqlRelations/straight data, without glitches.
class ProxyDelegate(QSqlRelationalDelegate):
def __init__(self):
QSqlRelationalDelegate.__init__(self)
def createEditor(self, p, o, i): # parent, option, index
if i.model().sourceModel().relation(i.column()).isValid(): # if the column has a QSqlRelation, then make the expected QComboBox
e = QComboBox(p)
return e
else:
return QStyledItemDelegate(p).createEditor(p, o, i)
def setEditorData(self, e, i):
m = i.model()
sM = m.sourceModel()
relation = sM.relation(i.column())
if relation.isValid():
m = i.model()
sM = m.sourceModel()
relation = sM.relation(i.column())
pModel = QSqlTableModel() # pModel means populate model. Because I've aimed for generic use, it makes a new QSqlTableModel, even if one already exists elsewhere for that SQL table
pModel.setTable(relation.tableName())
pModel.select()
e.setModel(pModel)
pModel.sort(pModel.fieldIndex(relation.displayColumn()), Qt.AscendingOrder) # default sorting. A custom attribute would need adding to each source model class, in order for this line to know the desired sorting order for this QComboBox delegate
e.setModelColumn(pModel.fieldIndex(relation.displayColumn()))
e.setCurrentIndex(e.findText(m.data(i).toString()))
else:
return QStyledItemDelegate().setEditorData(e, i)
def setModelData(self, e, m, i):
m = i.model() # this could probably be written more elegantly so you don't need to create another SqlModel
sM = m.sourceModel()
relation = sM.relation(i.column())
table = relation.tableName()
indexColumn = relation.indexColumn()
indexColumnId = sM.fieldIndex(indexColumn)
displayColumn = relation.displayColumn()
if relation.isValid():
pModel = QSqlTableModel()
pModel.setTable(relation.tableName())
pModel.select()
displayColumnId = pModel.fieldIndex(displayColumn)
chosenRowInPModel = pModel.match(pModel.index(0, displayColumnId), Qt.DisplayRole, e.currentText())[0].row()
chosenIdInPModel = pModel.data(pModel.index(chosenRowInPModel, indexColumnId)).toString()
m.setData(i, chosenIdInPModel)
self.closeEditor.emit(e, QAbstractItemDelegate.NoHint)
else:
QStyledItemDelegate().setModelData(e, m, i)

Laying Widgets in QGridLayout

I need to create the QWidget(QtoolButton) in QgridLayout without specifying the indices for row and column. It should automatically get created to next empty cell in the layout according to row and column mentioned.
I was not able to find any method in QgridLayout help.
I tried .addWidget (self, QWidget w), but it add all the QWidget to the index of (0,0) and all the buttons lie over each other.
Thanks in advance.
Let's suppose that you have a QGridLayout with 4 rows and 3 columns and you want to add buttons to it automatically from top to bottom and from left to right. That can easily be achieved if you are able to predict the position of the next button to be added. In our case:
row = number of added buttons / number of columns
column = number of added buttons % number of columns
(other type of filling work similarly). Let's put it in code:
from PyQt4.QtGui import *
class MyMainWindow(QMainWindow):
def __init__(self, parent=None):
super(MyMainWindow, self).__init__(parent)
self.central = QWidget(self)
self.grid = QGridLayout(self.central)
self.rows = 4
self.cols = 3
self.items = self.grid.count()
while(self.items < (self.rows * self.cols)):
self.addButton()
self.setCentralWidget(self.central)
def addButton(self):
# the next free position depends on the number of added items
row = self.items/self.cols
col = self.items % self.cols
# add the button to the next free position
button = QPushButton("%s, %s" % (row, col))
self.grid.addWidget(button, row, col)
# update the number of items
self.items = self.grid.count()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
ui = MyMainWindow()
ui.show()
sys.exit(app.exec_())
You can handle the "next empty cell" by calculating rows and columns yourself. For example, you can subclass QGridLayout to implement any "next empty cell" algorithm according to your needs:
class AutoGridLayout(QGridLayout):
def __init__(self):
QGridLayout.__init__(self)
self.column = 0
self.row = 0
def addNextWidget(self, widget):
self.addWidget(widget, self.row, self.column)
self.column = self.column + 1 # Automatically advance to next column
# Setup main widget
app = QApplication(sys.argv)
mainWindow = QMainWindow()
centralWidget = QWidget()
mainWindow.setCentralWidget(centralWidget)
# Add widgets using the AutoGridLayout
layout = AutoGridLayout()
centralWidget.setLayout(layout)
layout.addNextWidget(QPushButton("1", centralWidget))
layout.addNextWidget(QPushButton("2", centralWidget))
layout.addNextWidget(QPushButton("3", centralWidget))
# Show and run the application
mainWindow.show()
app.exec_()
This source shall only show the general idea - you can manage the row and column indices according to your needs. Just implement the necessary logic in the addNextWidget() method by calculating the next desired row/column (in this example, the next column in row 0 is used).
Addition to other answers: If you need just rows with variable number of items, and not an actual grid, then you should use multiple QHBoxLayouts (one for each row) nested in one QVBoxLayout. That will also get you the behaviour you want, new items created on demand, without nasty gaps.

Clickable elements or child widgets inside custom-painted delegate

I have a QListView, where I display items using a custom delegate with custom painting. Within each item (i.e. each list row) I want to be able to show a couple of "hyperlinks" which the user could click on and which would then call on some functions.
I have already tried to check the official documentation (e.g. Model/View Programming) as well as quite a lot of googling, but haven't been able to figure out how to accomplish this.
I have two ideas, each with their own problems:
I could draw them using child widgets, like a flat QPushButton. How do I then position and display these widgets?
I could also draw them as text strings. How do I then make them clickable? Or can I capture click events on the parent QListView and somehow determine coordinates from those? I could then match coordinates to these clickable elements and act accordingly.
My initial approach was to use QListWidget with .setItemWidget(), where I had a proper widget with a layout and child widgets. Unfortunately this was too slow when my list grew to hundreds or thousands of items. That's why I changed to QListView with a delegate.
I seem to be closing in on a solution.
I can receive clicks on the elements by overriding the delegate's .editorEvent(event, model, option, index). I can then find out the event.type(), the clicked row from index.row() and the actual coordinates from event.x() and event.y() (since, if the event type is MouseButtonRelease, the event is a QMouseEvent).
From these, I think I can correlate the coordinates to my elements on screen and act accordingly.
I will update this answer once I have working code.
EDIT
A simple working example, using PySide:
class MyModel(QtGui.QStandardItemModel):
def __init__(self):
super(MyModel, self).__init__()
for i in range(10): self.appendRow(QtGui.QStandardItem("Row %d" % i))
class MyDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
super(MyDelegate, self).__init__(parent)
self.links = {}
def makeLinkFunc(self, row, text):
def linkFunc(): print("Clicked on %s in row %d" % (text, row))
return linkFunc
def paint(self, painter, option, index):
painter.save()
textHeight = QtGui.QFontMetrics(painter.font()).height()
painter.drawText(option.rect.x()+2, option.rect.y()+2+textHeight, index.data())
rowLinks = {}
for i in range(3):
text = "Link %d" % (3-i)
linkWidth = QtGui.QFontMetrics(font).width(text)
x = option.rect.right() - (i+1) * (linkWidth + 10)
painter.drawText(x, y, text)
rect = QtCore.QRect(x, y - textHeight, linkWidth, textHeight)
rowLinks[rect] = self.makeLinkFunc(index.row(), text)
self.links[index.row()] = rowLinks
painter.restore()
def sizeHint(self, option, index):
hint = super().sizeHint(option, index)
hint.setHeight(30)
return hint
def editorEvent(self, event, model, option, index):
if event.type() == QtCore.QEvent.MouseButtonRelease:
for rect, link in self.links[index.row()].items():
if rect.contains(event.pos()):
link()
return True
return False
listmodel = MyModel()
listview = QtGui.QListView()
listview.setModel(listmodel)
listview.setItemDelegate(MyDelegate(parent=listview))
listview.setSelectionMode(QtGui.QAbstractItemView.NoSelection)

Resources