PyQt ToolTip for QTreeView - pyqt

Please axplain how to enable and show a tooltip for each item in QTreeView. I found a sample of code class TreeModel(QAbstractItemModel) but due to my beginner's level I can't understand how to apply it to my needs.
Data for tooltip should be taken from value of key "note" in dictionary data_for_tree.
#!/usr/bin/env python -tt
# -*- coding: utf-8 -*-
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
reload(sys)
sys.setdefaultencoding('utf8')
data_for_tree = {"tomato":{"color":"red","ammount":"10", "note":"a note for tomato"},"banana":{"color":"yellow","ammount":"1", "note":"b note for banana"}, "some fruit":{"color":"unknown","ammount":"100", "note":"some text"}}
class TreeModel(QAbstractItemModel):
def data(self, index, role=Qt.DisplayRole):
#...
if role == Qt.ToolTipRole:
return 'ToolTip'
def flags(self, index):
if not index.isValid():
return Qt.NoItemFlags # 0
return Qt.ItemIsSelectable # or Qt.ItemIsEnabled
class ProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super(ProxyModel, self).__init__(parent)
def lessThan(self, left, right):
leftData = self.sourceModel().data(left)
rightData = self.sourceModel().data(right)
try:
return float(leftData) < float(rightData)
except ValueError:
return leftData < rightData
class MainFrame(QWidget):
def __init__(self):
QWidget.__init__(self)
self.MyTreeView = QTreeView()
self.MyTreeViewModel = QStandardItemModel()
self.MyTreeView.setModel(self.MyTreeViewModel)
self.most_used_cat_header = ['Name', "ammount", "color"]
self.MyTreeViewModel.setHorizontalHeaderLabels(self.most_used_cat_header)
self.MyTreeView.setSortingEnabled(True)
self.MyTreeView_Fill()
MainWindow = QHBoxLayout(self)
MainWindow.addWidget(self.MyTreeView)
self.setLayout(MainWindow)
def MyTreeView_Fill(self):
for k in data_for_tree:
name = QStandardItem(k)
ammount = QStandardItem(data_for_tree[k]["ammount"])
note = QStandardItem(data_for_tree[k]["color"])
tooltip = data_for_tree[k]["note"]
item = (name, ammount, note)
self.MyTreeViewModel.appendRow(item)
self.MyTreeView.sortByColumn(1, Qt.DescendingOrder)
proxyModel = ProxyModel(self)
proxyModel.setSourceModel(self.MyTreeViewModel)
self.MyTreeView.setModel(proxyModel)
c = 0
while c < len(self.most_used_cat_header):
self.MyTreeView.resizeColumnToContents(c)
c=c+1
if __name__ == "__main__":
app = QApplication(sys.argv)
main = MainFrame()
main.show()
main.move(app.desktop().screen().rect().center() - main.rect().center())
sys.exit(app.exec_())

As you are using the QStandardItem and QStandardItemModel classes (which is what I would recommend!) you don't need to bother with the TreeModel class you have found. Creating your own model is rarely necessary, but for some reason tutorials often encourage you to do so. If you find something encouraging you to subclass QAbstractItemModel, I suggest you check on stack overflow first to see if there is a simpler way to do it! In this case, there is a very simple way to add your tooltips.
If you look at the C++ documentation (which I often find more useful than the PyQt documentation for finding out what methods are available), you will see that QStandardItem has a method called setToolTip().
So all you need to do is call this method on each of the items you add to the model. For example, inside the loop in the MyTreeView_Fill method:
name = QStandardItem(k)
ammount = QStandardItem(data_for_tree[k]["ammount"])
note = QStandardItem(data_for_tree[k]["color"])
tooltip = data_for_tree[k]["note"]
name.setToolTip(tooltip)
ammount.setToolTip(tooltip)
note.setToolTip(tooltip)
Here I've set the tooltip to be the same for every cell in the row (name, amount and note) but you could easily change this to have a different tooltip for one of the cells (hopefully it is obvious how to do that)

Related

PySide2 QListView.setRootIndex with customModel not working as expected

I am pretty new to QT and I am using PySide2 (latest version) with Python 3.9.6.
I want to use a CustomModel via QAbstractItemModel on a QtreeView and at the same time with a QListView.
I have a CustomModel with a two-level hierarchy data.
I want to see the full data in the treeview (working).
At the beginning I show the same model in the QListView. It shows only the top level items.
So far so good.
Now I connected the setRootIndex fn from the QListView to the clicked signal of the QTreeView.
I want to be able to click on a root level item and see only the children in the QListView.
I thought the .setRootIndex should do the trick, but its weirdly offsetting the shown children.
And it's showing only ONE of the children and offsetted by the index count of the first level item.
Please see the gif:
First both views show the same model.
Then I click the first root element in the left treeView.
It updates the right ListView, but only the first children is shown.
And the second item shows its child but the second and with one gap in the listView
Here is a (almost) working example.
I really hope someone can spot the mistake or my misconception of things..
The .setRootIndex on the QListView is confusing me.
I tried approaching it differntly in the .index and .parent and .rowCount functions of the CustomModel. But like this it somehow works at least. I have the feeling I am doing something wrong somewhere or the QListView wants things differntly like the QTreeView.
Is it even possible and a good idea to use the same model in two views?
I really thought so and this is the hole point of a model/viewcontroller approach, isn't it?
# -*- coding: utf-8 -*-
from typing import *
from PySide2 import QtWidgets
from PySide2.QtCore import QAbstractItemModel, QModelIndex
from PySide2.QtGui import Qt
from PySide2.QtWidgets import QListView, QTreeView
class FirstLevelItem:
def __init__(self, name) -> None:
self.name = name
self.children = []
class SecondLevelItem:
def __init__(self, name, parent) -> None:
self.name = name
self.parent = parent
class CustomModel(QAbstractItemModel):
def __init__(self, root_items, parent=None):
super().__init__(parent)
self.root_items = root_items
def rowCount(self, itemIndex):
"""Has to return the number of children of the itemIndex.
If its not a valid index, its a root item, and we return the count of all root_items.
If its a valid one and can have children, return the number of children.
This makes the Model to ask for more indexes for each item.
Only works if parent is set properly"""
if itemIndex.isValid():
item = itemIndex.internalPointer()
if isinstance(item, FirstLevelItem):
return len(item.children)
else:
return 0
else:
return len(self.root_items)
def columnCount(self, parent=None):
return 1
def parent(self, child_index):
"""Has to return an index pointing to the parent of the current index."""
if child_index.isValid():
# get the item of this index
item = child_index.internalPointer()
# check if its one with a parent
if isinstance(item, SecondLevelItem):
# get the parent obj from the item
parent_item = item.parent
# now we have to find the parents row index to be able to create the index pointing to it
parent_row = parent_item.children.index(item)
# create an index with the parent row and column and the parent item itself
return self.createIndex(parent_row, 0, parent_item)
else:
return QModelIndex()
else:
return QModelIndex()
def data(self, index, role):
if not index.isValid():
return None
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.name
return None
def index(self, row, column, parentIndex):
if parentIndex.isValid():
parent_item = parentIndex.internalPointer()
return self.createIndex(row, column, parent_item.children[row])
else:
return self.createIndex(row, column, self.root_items[row])
class ModelTestDialog(QtWidgets.QDialog):
window_instance = None
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlags(self.windowFlags() ^ Qt.WindowContextHelpButtonHint)
# self.setMinimumSize(1024, 1024)
self.setWindowTitle("ModelTestDialog")
rootItems = []
for i in range(0, 3):
name = ["FirstLevel_A", "FirstLevel_B", "FirstLevel_C"][i]
rootItem = FirstLevelItem(name)
rootItems.append(rootItem)
for j in range(0, 3):
name = ["SecondLevel_A", "SecondLevel_B", "SecondLevel_C"][j]
childItem = SecondLevelItem(name, rootItem)
rootItem.children.append(childItem)
self.model = CustomModel(rootItems)
self.treeView = QTreeView()
self.treeView.setModel(self.model)
self.listView = QListView()
self.listView.setModel(self.model)
self.main_layout = QtWidgets.QVBoxLayout(self)
self.listViews_layout = QtWidgets.QHBoxLayout()
self.main_layout.addLayout(self.listViews_layout)
self.listViews_layout.addWidget(self.treeView)
self.listViews_layout.addWidget(self.listView)
self.treeView.clicked[QModelIndex].connect(self.listView.setRootIndex)
if __name__ == "__main__":
app = QtWidgets.QApplication()
form = ModelTestDialog()
form.show()
app.exec_()
There is absolutely nothing wrong about using the same model in multiple views.
That is the whole concept behind the model/view paradigm (which relies on the principle of separation of concerns): the same model can be shared amongs multiple views, even if they show the content of that model in different ways.
That is completely respected by Qt (as long as the model is properly implemented, obviously); this also happens for similar concepts in Qt, like the QTextDocument interface used in QTextEdit (the same document can be shown on different QTextEdit instances), or the QGraphicsScene shown in a QGraphicsView (each view can show a different portion of the same scene).
The actual issue
You're using the wrong row for the parent:
parent_row = parent_item.children.index(item)
The above returns the index (row) of the child item, but you need to use createIndex() as a reference for the parent, because parent() has to return the row/column of the parent, not that of the child.
In this simple case, just return the index within the root_items:
parent_row = self.root_items.index(parent_item)
A better approach
I would suggest a more flexible structure, where a single base class is used for all items, and it always has a parent attribute. To do this, you need to also create a "root item" which contains all top level items.
You can still create subclasses for items if you need more flexibility or specialization, but the default behavior remains unchanged, making the implementation simpler especially in the case you need further levels within the structure.
The major benefit of this approach is that you never need to care about the item type to know its level: you know that you need to access the root item when the given index is invalid, and for any other case (like index creation, parent access, etc), the implementation is much more easy and readable. This will automatically make easier to add support for other features, like moving items and drag&drop.
class TreeItem:
parent = None
def __init__(self, name='', parent=None):
self.name = name
self.children = []
if parent:
parent.appendChild(self)
def appendChild(self, item):
self.insertChild(len(self.children), item)
def insertChild(self, index, item):
self.children.insert(index, item)
item.parent = self
def row(self):
if self.parent:
return self.parent.children.index(self)
return -1
class CustomModel(QAbstractItemModel):
def __init__(self, root_items=None, parent=None):
super().__init__(parent)
self.root_item = TreeItem()
if root_items:
for item in root_items:
self.root_item.appendChild(item)
def rowCount(self, itemIndex):
if itemIndex.isValid():
return len(itemIndex.internalPointer().children)
else:
return len(self.root_item.children)
def columnCount(self, parent=None):
return 1
def parent(self, child_index):
if child_index.isValid():
item = child_index.internalPointer()
if item.parent:
return self.createIndex(item.parent.row(), 0, item.parent)
return QModelIndex()
def data(self, index, role):
if not index.isValid():
return None
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.name
def index(self, row, column, parentIndex=QModelIndex()):
if parentIndex.isValid():
parent_item = parentIndex.internalPointer()
return self.createIndex(row, column, parent_item.children[row])
else:
return self.createIndex(row, column, self.root_item.children[row])
class ModelTestDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlags(self.windowFlags() ^ Qt.WindowContextHelpButtonHint)
self.setWindowTitle('ModelTestDialog')
rootItems = []
for i in range(0, 3):
name = 'FirstLevel {}'.format('ABC'[i])
rootItem = TreeItem(name)
rootItems.append(rootItem)
for j in range(0, 3):
name = 'SecondLevel {} (child of {})'.format('ABC'[j], 'ABC'[i])
TreeItem(name, rootItem)
# or, alternatively:
# rootItem.appendChild(TreeItem(name))
self.model = CustomModel(rootItems)
self.treeView = QTreeView()
self.treeView.setModel(self.model)
self.listView = QListView()
self.listView.setModel(self.model)
self.main_layout = QVBoxLayout(self)
self.listViews_layout = QHBoxLayout()
self.main_layout.addLayout(self.listViews_layout)
self.listViews_layout.addWidget(self.treeView)
self.listViews_layout.addWidget(self.listView)
self.treeView.clicked.connect(self.listView.setRootIndex)
As you can see, the whole model code is much simpler and cleaner: there is no need to check for item level/type, as the concept of the structure makes that automatically immediate.
Further notes:
the Qt API suggests that the parent argument of index() should be optional; while it's common to use None for that, a default (and invalid) QModelIndex() is preferable, as I did above;
python implicitly returns None if no other return value is given;
in the last few years, Qt has been in the process of removing all overloaded signals, replacing them with more verbose and unique ones; in general, it's unnecessary to specify them, especially where no overload actually exists (self.treeView.clicked);

How can I programmatically expand/collapse and select an item in a tree?

Let's say I added a TreeView widget called treeView in Qt Designer and I'm using this code to add a few items to it:
class StandardItem(Qt.QStandardItem):
def __init__(self, txt='', font_size=11, set_bold=False, color=Qt.QColor(0, 0, 0)):
super().__init__()
fnt = Qt.QFont('Open Sans', font_size)
fnt.setBold(set_bold)
self.setEditable(False)
self.setForeground(color)
self.setFont(fnt)
self.setText(txt)
model = Qt.QStandardItemModel()
rootNode = model.invisibleRootItem()
A = StandardItem("A")
A.appendRows([StandardItem("1"),StandardItem("2"),StandardItem("3")])
B = StandardItem("B")
B.appendRows([StandardItem("1"),StandardItem("2")])
rootNode.appendRows([A,B])
self.treeView.setModel(model)
How can I write a function that collapses/expands item A? And how can I write a function that selects item A, as if it has been clicked? I might be missing something in the docs.
Expansion and selection are tasks that affect the visual part so they must be handled by sight. The first task is done through the setExpanded() method and the second using the select() method of the selectionModel() associated with the view, in both cases the QModelIndex associated with the item is used:
import sys
from PyQt5 import Qt
class StandardItem(Qt.QStandardItem):
def __init__(self, txt="", font_size=11, set_bold=False, color=Qt.QColor(0, 0, 0)):
super().__init__()
fnt = Qt.QFont("Open Sans", font_size)
fnt.setBold(set_bold)
self.setEditable(False)
self.setForeground(color)
self.setFont(fnt)
self.setText(txt)
class MainWindow(Qt.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.treeView = Qt.QTreeView()
self.setCentralWidget(self.treeView)
self.model = Qt.QStandardItemModel()
rootNode = self.model.invisibleRootItem()
A = StandardItem("A")
A.appendRows([StandardItem("1"), StandardItem("2"), StandardItem("3")])
B = StandardItem("B")
B.appendRows([StandardItem("1"), StandardItem("2")])
rootNode.appendRows([A, B])
self.treeView.setModel(self.model)
index_A = A.index()
self.treeView.setExpanded(index_A, True)
self.treeView.selectionModel().select(index_A, Qt.QItemSelectionModel.Select)
def main():
app = Qt.QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()
if __name__ == "__main__":
main()
Note:
It seems that the index() function of the QStandardItem has a bug since it sometimes returns invalid QModelIndex for valid QStandardItem.
print(A.child(0).index().isValid())
Output:
False
This is caused because when the child was added, the "A" was not part of the model then the model is null in those children, as can be seen with.
print(A.child(0).model())
Output:
None
If instead "A" are added to the model first and then the children are newly added, then the model is passed.
self.model = Qt.QStandardItemModel()
rootNode = self.model.invisibleRootItem()
A = StandardItem("A")
B = StandardItem("B")
rootNode.appendRows([A, B])
A.appendRows([StandardItem("1"), StandardItem("2"), StandardItem("3")])
B.appendRows([StandardItem("1"), StandardItem("2")])
print(A.child(0).model())
print(A.child(0).index().isValid())
Output:
<PyQt5.QtGui.QStandardItemModel object at 0x7fb40aa868b0>
True
In that case it is better to use the indexFromItem() method of QStandardItemModel:
self.treeView.selectionModel().select(
self.model.indexFromItem(A.child(0)), Qt.QItemSelectionModel.Select
)

PySide2: How to re-implement QFormLayout.takeRow()?

I've noticed that QFormLayout in Pyside2 does not have the takeRow method like its PyQt5 counterpart. I've attempted to subclass QFormLayout to incorporate a similar method, but I've run into Runtime Errors, as the removal behavor of the LabelRole item is different than the FieldRole item. Another issue being that the LabelRole item does not actually get taken off the row even when the row itself is removed.
The following is the test sample I've been working with using Python 3.8.6:
from PySide2.QtWidgets import *
import sys
class MyFormLayout(QFormLayout):
def __init__(self, *args, **kwargs):
super(MyFormLayout, self).__init__(*args, **kwargs)
self.cache = []
print(f"Formlayout's identity: {self=}\nwith parent {self.parent()=}")
def takeRow(self, row: int):
print(f"Called {self.takeRow.__name__}")
print(f"{self.rowCount()=}")
label_item = self.itemAt(row, QFormLayout.LabelRole)
field_item = self.itemAt(row, QFormLayout.FieldRole)
print(f"{label_item=}\n{field_item=}")
self.removeItem(label_item)
self.removeItem(field_item)
self.removeRow(row) ## <-- This seems necessary to make the rowCount() decrement. Alternative?
label_item.widget().setParent(None) ## <-- Runtime Error Here?
field_item.layout().setParent(None)
self.cache.append(label_item.widget(), field_item)
print(f"{self.rowCount()=}")
print(f"{self.cache=}")
print(self.cache[0])
print("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")
return label_item, field_item
def restoreRow(self, insert_idx: int):
print(f"Called {self.restoreRow.__name__}")
print(f"{self.rowCount()=}")
print(f"{self.cache=}")
to_insert = self.cache.pop()
self.insertRow(insert_idx, to_insert[0], to_insert[1])
print("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")
class MyWindow(QWidget):
def __init__(self):
super(MyWindow, self).__init__()
self.mainlay = MyFormLayout(self)
self.cmb = QComboBox()
self.cmb.addItems(["Placeholder", "Remove 1 and 2"])
self.cmb.currentTextChanged.connect(self.remove_rows_via_combo)
self.current_text = self.cmb.currentText()
self.hlay1, self.le1, self.btn1 = self.le_and_btn(placeholderText="1")
self.hlay2, self.le2, self.btn2 = self.le_and_btn(placeholderText="2")
self.hlay3, self.le3, self.btn3 = self.le_and_btn(placeholderText="3")
self.hlay4, self.le4, self.btn4 = self.le_and_btn(placeholderText="4")
self.remove_btn = QPushButton("Remove", clicked=self.remove_row_via_click)
self.restore_btn = QPushButton("Restore", clicked=self.restore_a_row_via_click)
self.mainlay.addRow("Combobox", self.cmb)
for ii, hlayout in zip(range(1, 5), [self.hlay1, self.hlay2, self.hlay3, self.hlay4]):
self.mainlay.addRow(f"Row {ii}", hlayout)
self.mainlay.addRow(self.remove_btn)
self.mainlay.addRow(self.restore_btn)
#staticmethod
def le_and_btn(**kwargs):
hlay, le, btn = QHBoxLayout(), QLineEdit(**kwargs), QPushButton()
hlay.addWidget(le)
hlay.addWidget(btn)
return hlay, le, btn
def remove_row_via_click(self):
self.mainlay.takeRow(1)
def restore_a_row_via_click(self):
self.mainlay.restoreRow(1)
def remove_rows_via_combo(self, text):
print(f"{self.remove_rows_via_combo.__name__} received the text: {text}")
if text == "Remove 1 and 2":
self.mainlay.takeRow(1)
self.mainlay.takeRow(1)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MyWindow()
win.show()
sys.exit(app.exec_())
I would like to understand why the behavior of the role items is different and how the method may be properly re-implemented.
The problem is that the label was created internally by Qt from a string, rather than by explicitly creating a QLabel in Python. This means that when the row is removed, the last remaining reference is also removed, which deletes the label on the C++ side. After that, all that's left on the Python side is an empty PyQt wrapper - so when you try to call setParent on it, a RuntimeError will be raised, because the underlying C++ part no longer exists.
Your example can therefore be fixed by getting python references to the label/field objects before the layout-item is removed:
class MyFormLayout(QFormLayout):
...
def takeRow(self, row: int):
print(f"Called {self.takeRow.__name__}")
print(f"{self.rowCount()=}")
label_item = self.itemAt(row, QFormLayout.LabelRole)
field_item = self.itemAt(row, QFormLayout.FieldRole)
print(f"{label_item=}\n{field_item=}")
# get refs before removal
label = label_item.widget()
field = field_item.layout() or field_item.widget()
self.removeItem(label_item)
self.removeItem(field_item)
self.removeRow(row)
label.setParent(None)
field.setParent(None)
self.cache.append((label, field))
print(f"{self.rowCount()=}")
print(f"{self.cache=}")
print(self.cache[0])
print("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")
return label, field

Python - AutoComplete in an Editable Gtk.TreeView Cell

I was recently coding PyQt with a QComboBox in a QTable. The QComboBox has autocomplete on by default.
I was wanting to try and reproduce this in Python3 with Gtk3. I came across this example:
Gtk.Entry in Gtk.TreeView (CellRenderer)
that seems to have successfully added an autocompletion to a ComboBox in a Treeview. The example is not complete and I was hoping someone could give me a complete working example. I don't know how I 'connect' the CellRendererAutoComplete class to one of the columns in a treeview.
What I really want is to have AutoCompletion in an editable TreeView cell (with or without a ComboBox), but I have never come across this in the past in Gtk.
Thank you.
Here is a code example:
# https://stackoverflow.com/questions/13756787/
# https://python-gtk-3-tutorial.readthedocs.io/en/latest/cellrenderers.html
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
#######################################################################
class CellRendererAutoComplete(Gtk.CellRendererText):
""" Text entry cell which accepts a Gtk.EntryCompletion object """
__gtype_name__ = 'CellRendererAutoComplete'
def __init__(self, completion):
self.completion = completion
Gtk.CellRendererText.__init__(self)
def do_start_editing(
self, event, treeview, path, background_area, cell_area, flags):
if not self.get_property('editable'):
return
entry = Gtk.Entry()
entry.set_completion(self.completion)
entry.connect('editing-done', self.editing_done, path)
entry.show()
entry.grab_focus()
return entry
def editing_done(self, entry, path):
self.emit('edited', path, entry.get_text())
#######################################################################
class CellRendererTextWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="CellRendererText Example")
self.set_default_size(200, 200)
self.liststore = Gtk.ListStore(str, str)
self.liststore.append(["Fedora", "http://fedoraproject.org/"])
self.liststore.append(["Slackware", "http://www.slackware.com/"])
self.liststore.append(["Sidux", "http://sidux.com/"])
treeview = Gtk.TreeView(model=self.liststore)
renderer_text = Gtk.CellRendererText()
column_text = Gtk.TreeViewColumn("Text", renderer_text, text=0)
treeview.append_column(column_text)
renderer_editabletext = Gtk.CellRendererText()
renderer_editabletext.set_property("editable", True)
########
# This is the problem area, I suppose, but I'm not sure
x = CellRendererAutoComplete()
renderer_editabletext.connect('on_edit',x(renderer_editabletext))
########
column_editabletext = Gtk.TreeViewColumn("Editable Text",renderer_editabletext, text=1)
treeview.append_column(column_editabletext)
renderer_editabletext.connect("edited", self.text_edited)
self.add(treeview)
def text_edited(self, widget, path, text):
self.liststore[path][1] = text
win = CellRendererTextWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()
So here is an example showing a CellRendererText and a CellRendererCombo using an EntryCompletion.
Because of the complexity of the Treeview, where one ListStore can be the model behind the Completion, Combo, and Entry, and another model can be behind the Treeview, you should have a very good grasp of Gtk.Treeview to understand this example. Note that this example only uses one Liststore, and only one editable column used by both the CellRendererText and the CellRendererColumn. This makes the example confusing, but it the simplest I can come up with since I do not know the intended use for this Treeview.
#!/usr/bin/python
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class CellRendererTextWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="CellRendererText Example")
self.set_default_size(200, 200)
self.liststore = Gtk.ListStore(str, str)
self.liststore.append(["Fedora", "http://fedoraproject.org/"])
self.liststore.append(["Slackware", "http://www.slackware.com/"])
self.liststore.append(["Sidux", "http://sidux.com/"])
treeview = Gtk.TreeView(model=self.liststore)
self.completion = Gtk.EntryCompletion(model = self.liststore)
self.completion.set_text_column(1)
self.completion.connect('match-selected', self.renderer_match_selected)
renderer_text = Gtk.CellRendererText()
column_text = Gtk.TreeViewColumn("Text", renderer_text, text=0)
treeview.append_column(column_text)
######## CellRendererText with EntryCompletion example
renderer_text = Gtk.CellRendererText()
renderer_text.connect('editing-started', self.renderer_text_editing_started)
renderer_text.connect('edited', self.text_edited)
renderer_text.set_property("editable", True)
column_text_autocomplete = Gtk.TreeViewColumn("Editable Text", renderer_text, text=1)
treeview.append_column(column_text_autocomplete)
######## CellRendererCombo with EntryCompletion example
renderer_combo = Gtk.CellRendererCombo(model = self.liststore)
renderer_combo.set_property("text-column", 1)
renderer_combo.connect('editing-started', self.renderer_combo_editing_started)
renderer_combo.connect('changed', self.combo_changed)
renderer_combo.set_property("editable", True)
column_combo_autocomplete = Gtk.TreeViewColumn("Editable Combo", renderer_combo, text=1)
treeview.append_column(column_combo_autocomplete)
self.add(treeview)
def renderer_match_selected (self, completion, model, tree_iter):
''' beware ! the model and tree_iter passed in here are the model from the
EntryCompletion, which may or may not be the same as the model of the Treeview '''
text_match = model[tree_iter][1]
self.liststore[self.path][1] = text_match
def renderer_text_editing_started (self, renderer, editable, path):
''' since the editable widget gets created for every edit, we need to
connect the completion to every editable upon creation '''
editable.set_completion(self.completion)
self.path = path # save the path for later usage
def text_edited(self, widget, path, text):
self.liststore[path][1] = text
def renderer_combo_editing_started (self, renderer, combo, path):
''' since the editable widget gets created for every edit, we need to
connect the completion to every editable upon creation '''
editable = combo.get_child() # get the entry of the combobox
editable.set_completion(self.completion)
self.path = path # save the path for later usage
def combo_changed (self, combo, path, tree_iter):
''' the path is from the treeview and the tree_iter is from the model
of the combobox which may or may not be the same model as the treeview'''
combo_model = combo.get_property('model')
text = combo_model[tree_iter][1]
self.liststore[path][1] = text
win = CellRendererTextWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()
In the official docs, it is explicitly stated that the purpose of editing-started is to add a EntryCompletion and etc. I also subclassed Gtk.CellRendererText before I found that little hint in the docs.

PyQt - QCombobox in QTableview

I am displaying data from an SQLite database in a QTableView using a QSqlTableModel. Letting the user edit this data works fine. However, for some columns I want to use QComboboxes instead of free text cells, to restrict the list of possible answers.
I have found this SO answer and am trying to implement it on my model/view setting, but I'm running into problems (so this is a follow-up).
Here's a full mini-example:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from PyQt5 import QtSql
from PyQt5.QtWidgets import (QWidget, QTableView, QApplication, QHBoxLayout,
QItemDelegate, QComboBox)
from PyQt5.QtCore import pyqtSlot
import sys
class ComboDelegate(QItemDelegate):
"""
A delegate that places a fully functioning QComboBox in every
cell of the column to which it's applied
source: https://gist.github.com/Riateche/5984815
"""
def __init__(self, parent, items):
self.items = items
QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
combo = QComboBox(parent)
li = []
for item in self.items:
li.append(item)
combo.addItems(li)
combo.currentIndexChanged.connect(self.currentIndexChanged)
return combo
def setEditorData(self, editor, index):
editor.blockSignals(True)
# editor.setCurrentIndex(int(index.model().data(index))) #from original code
editor.setCurrentIndex(index.row()) # replacement
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentIndex())
#pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
class Example(QWidget):
def __init__(self):
super().__init__()
self.resize(400, 150)
self.createConnection()
self.fillTable() # comment out to skip re-creating the SQL table
self.createModel()
self.initUI()
def createConnection(self):
self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("test.db")
if not self.db.open():
print("Cannot establish a database connection")
return False
def fillTable(self):
self.db.transaction()
q = QtSql.QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Cars;")
q.exec_("CREATE TABLE Cars (Company TEXT, Model TEXT, Year NUMBER);")
q.exec_("INSERT INTO Cars VALUES ('Honda', 'Civic', 2009);")
q.exec_("INSERT INTO Cars VALUES ('VW', 'Golf', 2013);")
q.exec_("INSERT INTO Cars VALUES ('VW', 'Polo', 1999);")
self.db.commit()
def createModel(self):
self.model = QtSql.QSqlTableModel()
self.model.setTable("Cars")
self.model.select()
def initUI(self):
layout = QHBoxLayout()
self.setLayout(layout)
view = QTableView()
layout.addWidget(view)
view.setModel(self.model)
view.setItemDelegateForColumn(0, ComboDelegate(self, ["VW", "Honda"]))
for row in range(0, self.model.rowCount()):
view.openPersistentEditor(self.model.index(row, 0))
def closeEvent(self, e):
for row in range(self.model.rowCount()):
print("row {}: company = {}".format(row, self.model.data(self.model.index(row, 0))))
if (self.db.open()):
self.db.close()
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
In this case, I want to use a QCombobox on the "Company" column. It should be displayed all the time, so I'm calling openPersistentEditor.
Problem 1: default values I would expect that this shows the non-edited field's content when not edited (i.e. the company as it is listed in the model), but instead it apparently shows the ith element of the combobox's choices.
How can I make each combobox show the model's actual content for this field by default?
Problem 2: editing When you comment out "self.fill_table()" you can check whether the edits arrive in the SQL database. I would expect that choosing any field in the dropdown list would replace the original value. But (a) I have to make every choice twice (the first time, the value displayed in the cell remains the same), and (b) the data appears in the model weirdly (changing the first column to 'VW', 'Honda', 'Honda' results in ('1', 'VW', '1' in the model). I think this is because the code uses editor.currentIndex() in the delegate's setModelData, but I have not found a way to use the editor's content instead. How can I make the code report the user's choices correctly back to the model? (And how do I make this work on first click, instead of needing 2 clicks?)
Any help greatly appreciated. (I have read the documentation on QAbstractItemDelegate, but I don't find it particularly helpful.)
Found the solution with the help of the book Rapid GUI Programming with Python and Qt:
createEditor and setEditorData do not work as I expected (I was misguided because the example code looked like it was using the text content but instead was dealing with index numbers). Instead, they should look like this:
def setEditorData(self, editor, index):
editor.blockSignals(True)
text = index.model().data(index, Qt.DisplayRole)
try:
i = self.items.index(text)
except ValueError:
i = 0
editor.setCurrentIndex(i)
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentText())
I hope this helps someone down the line.

Resources