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.
Related
I have a DataFrame in Pandas which collects some data from an Excel document. I created a GUI with PyQt5 in order to make it look more interesting but here is the thing.
Is it possbile to make a dynamic search bar in order to search through that DataFrame? For example, my DataFrame has over 3k+ rows and I wanna search for John Doe, then the results will come up on the GUI. As far as I know, QLineEdit is used for this but I can't seem to implement it on my code.
Is it me that is doing wrong or it is not possible to do it on a DataFrame? And if anyone wanna help me, just let me know, I would be so grateful and thankful, I guess it'll only take 10-15 minutes. I can also post the code here, but talking on Discord and explaining you in detail and also sharing screens would be a lot easier.
This can be done by subclassing QAbstractTableModel to create a custom table model that uses the underlying dataframe for supplying data to a QTableView. This custom model can then be combined with a QProxyFilterSortModel to filter the data in the table. To create a custom non-editable model from QAbstractTableModel you need to implement rowCount, columnCount, data, and headerData at the very least. In this case, minimal implemetation could be something like this:
class DataFrameModel(QtCore.QAbstractTableModel):
def __init__(self, data_frame, parent = None):
super().__init__(parent)
self.data_frame = data_frame
def rowCount(self, index):
if index.isValid():
return 0
return self.data_frame.shape[0]
def columnCount(self, index):
if index.isValid():
return 0
return self.data_frame.shape[1]
def data(self, index, role):
if not index.isValid() or role != QtCore.Qt.DisplayRole:
return None
return str(self.data_frame.iloc[index.row(), index.column()])
def headerData(self, section, orientation, role=None):
if role != QtCore.Qt.DisplayRole:
return None
if orientation == QtCore.Qt.Vertical:
return self.data_frame.index[section]
else:
return self.data_frame.columns[section]
To show and filter the data in a table you could do something like this:
class MyWidget(QtWidgets.QWidget):
def __init__(self, parent = None):
super().__init__(parent)
self.table_view = QtWidgets.QTableView()
self.proxy_model = QtCore.QSortFilterProxyModel()
# by default, the QSortFilterProxyModel will search for keys in the first column only
# setting QSortFilterProxyModel.filterKeyColumn to -1 will match values in all columns
self.proxy_model.setFilterKeyColumn(-1)
self.table_view.setModel(self.proxy_model)
# line edit for entering (part of) the key that should be searched for
self.line_edit = QtWidgets.QLineEdit()
self.line_edit.textChanged.connect(self.filter_text)
vlayout = QtWidgets.QVBoxLayout(self)
vlayout.addWidget(self.line_edit)
vlayout.addWidget(self.table_view)
def filter_text(self, text):
self.proxy_model.setFilterFixedString(text)
def set_data(self, data):
self.model = DataFrameModel(pd.DataFrame(data))
self.proxy_model.setSourceModel(self.model)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
win = MyWidget()
win.set_data({'a':['apple', 'banana', 'cherry'], 'b':[4,5,6], 'c':['green', 'yellow', 'red']})
win.show()
app.exec()
Of course this is a very basic implementation but I might help you get started.
I think the library pandas_ui is what you are looking for here.
It allows you to play with your dataframe interactively, i.e. in a microsoft excel fashion. Particularly, the column filtering option should do what you want.
This question already has an answer here:
How to save selected Items to Qsettings from QListWidget, QTableWidget
(1 answer)
Closed 3 years ago.
Need to save current values of QTableWidget. The QTableWidget will be iniside QTabWidget and there will be several adjacent tabs containing multiple tables in it. And inside the table there will be QCheckBox, QComoBox as cellwidgetitem which values also need to be saved. Beside this I also need to save value of QSpinBox and QDoubleSpinBox which are inside gridlayout. I am taking help of a code from https://gist.github.com/eyllanesc/be8a476bb7038c7579c58609d7d0f031 which saves values of QLineEdit inside a QFormLayout where the QFormLayout is inside a QTabWidget. If multiple tabs are instantiated then QLineEdit values inside the tabs cannot be saved. Moreover, if a QTableWidget is added below a QTabWidget then the values of the QTableWidget cannot be saved as well.
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtCore import QFileInfo, QSettings
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import qApp, QApplication, QMainWindow, QFormLayout, QLineEdit, QTabWidget, QWidget, QAction, QVBoxLayout, QTableWidget, QTableWidgetItem
def restore(settings):
finfo = QFileInfo(settings.fileName())
print(settings.fileName())
if finfo.exists() and finfo.isFile():
for w in qApp.allWidgets():
mo = w.metaObject()
if w.objectName() != "":
for i in range(mo.propertyCount()):
name = mo.property(i).name()
val = settings.value("{}/{}".format(w.objectName(), name), w.property(name))
w.setProperty(name, val)
def save(settings):
for w in qApp.allWidgets():
mo = w.metaObject()
if w.objectName() != "":
for i in range(mo.propertyCount()):
name = mo.property(i).name()
settings.setValue("{}/{}".format(w.objectName(), name), w.property(name))
class mainwindow(QMainWindow):
settings = QSettings("gui.ng", QSettings.IniFormat)
def __init__(self, parent=None):
super(mainwindow, self).__init__(parent)
self.setObjectName("MainWindow")
self.initUI()
restore(self.settings)
def initUI(self):
exitAction = QAction(QIcon('icon\\exit.png'), 'Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.triggered.connect(self.close)
self.toolbar = self.addToolBar('Exit')
self.toolbar.setMovable(False)
self.toolbar.addAction(exitAction)
self.tab_widget = QTabWidget(self) # add tab
self.tab_widget.setObjectName("tabWidget")
self.tab2 = QWidget()
self.tab2.setObjectName("tab2")
self.tab3 = QWidget()
self.tab3.setObjectName("tab3")
self.tab_widget.addTab(self.tab2, "Tab_2")
self.tab_widget.addTab(self.tab3, "Tab_3")
self.tab2UI()
self.tab3UI()
self.vlay = QVBoxLayout()
self.vlay.addWidget(self.tab_widget)
self.qtable = QTableWidget()
self.qtable.setRowCount(3)
self.qtable.setColumnCount(3)
self.qtable.setItem(0, 0, QTableWidgetItem("text1"))
self.qtable.setItem(0, 1, QTableWidgetItem("text1"))
self.qtable.setItem(0, 2, QTableWidgetItem("text1"))
self.qtable.setItem(1, 0, QTableWidgetItem("text2"))
self.qtable.setItem(1, 1, QTableWidgetItem("text2"))
self.qtable.setItem(1, 2, QTableWidgetItem("text2"))
self.qtable.setItem(2, 0, QTableWidgetItem("text3"))
self.qtable.setItem(2, 1, QTableWidgetItem("text3"))
self.qtable.setItem(2, 2, QTableWidgetItem("text3"))
self.vlay.addWidget(self.qtable)
self.qVlayWidget = QWidget()
self.qVlayWidget.setLayout(self.vlay)
self.setCentralWidget(self.qVlayWidget)
def tab2UI(self):
self.layout_2 = QFormLayout()
nameLe = QLineEdit(self)
nameLe.setObjectName("nameLe_2")
self.layout_2.addRow("Name_2", nameLe)
addressLe = QLineEdit()
addressLe.setObjectName("addressLe_2")
self.layout_2.addRow("Address_2", addressLe)
self.tab2.setLayout(self.layout_2)
def tab3UI(self):
self.layout_3 = QFormLayout()
nameLe = QLineEdit(self)
nameLe.setObjectName("nameLe_3")
self.layout_3.addRow("Name_3", nameLe)
addressLe = QLineEdit()
addressLe.setObjectName("addressLe_3")
self.layout_3.addRow("Address_3", addressLe)
self.tab3.setLayout(self.layout_3)
def closeEvent(self, event):
save(self.settings)
QMainWindow.closeEvent(self, event)
def main():
app = QApplication(sys.argv)
ex = mainwindow()
ex.setGeometry(100, 100, 1000, 600)
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The QLineEdit data is stored, the problem is that its not shown. The visible property of a widget depends on its ancestor[s]; since a QTabWidget has a QWidget for each of its tab, only the current one is visible, while the other (including all their children) are not visible. A QLineEdit is a complex widget, and if you manually unset the visible property to all its internal components, they won't be correctly restored again.
This becomes clear if you close the window with the second tab set as current: once you'll open it again, the second tab will be shown and its contents correctly displayed, while the first one QLineEdit won't.
A workaround for this is to check if the widget is "stored" as not visible and has a parent. If that's the case, just do not apply the visible property.
def restore(settings):
# ...
for w in qApp.allWidgets():
mo = w.metaObject()
parent = w.parent()
if w.objectName() != "":
for i in range(mo.propertyCount()):
name = mo.property(i).name()
val = settings.value("{}/{}".format(w.objectName(), name), w.property(name))
if name == 'visible' and val == 'false' and parent:
continue
w.setProperty(name, val)
That said, this is clearly not the good way to save the data of any widget, since it saves every property it has: the title of the example is "Functions to save and restore Widgets", not its editable data which is what you need.
Also, QTableWidget contents cannot be stored using this method, as its data is not part of the widget properties (and nothing else was saved anyway, because you didn't set its object name).
You have to find your own implementation to save those contents, which might be something similar to the following code.
Note that I'm using a QByteArray to store the table data. You create a QByteArray, then a QDataStream with it as argument, which is used for reading or writing. Note that both reading and writing are consequential, as data is always appended on writing or "popped-out" each time it has been read.
class mainwindow(QMainWindow):
# ...
def save(self):
# using findChildren is for simplicity, it's probably better to create
# your own list of widgets to cycle through
for w in self.findChildren((QLineEdit, QTableWidget)):
name = w.objectName()
if isinstance(w, QLineEdit):
self.settings.setValue("{}/text".format(name), w.text())
elif isinstance(w, QTableWidget):
# while we could add sub-setting keys for each combination of
# row/column items, it's better to store the data in a single
# "container"
data = QByteArray()
stream = QDataStream(data, QIODevice.WriteOnly)
rowCount = w.rowCount()
columnCount = w.columnCount()
# write the row and column count first
stream.writeInt(rowCount)
stream.writeInt(columnCount)
# then write the data
for row in range(rowCount):
for col in range(columnCount):
stream.writeQString(w.item(row, col).text())
self.settings.setValue("{}/data".format(name), data)
def restore(self):
for w in self.findChildren((QLineEdit, QTableWidget)):
name = w.objectName()
if isinstance(w, QLineEdit):
w.setText(self.settings.value("{}/text".format(name), w.text()))
elif isinstance(w, QTableWidget):
data = self.settings.value("{}/data".format(name))
if not data:
continue
stream = QDataStream(data, QIODevice.ReadOnly)
# read the row and column count first
rowCount = stream.readInt()
columnCount = stream.readInt()
w.setRowCount(rowCount)
w.setColumnCount(columnCount)
# then read the data
for row in range(rowCount):
for col in range(columnCount):
cellText = stream.readQString()
if cellText:
w.item(row, col).setText(cellText)
There is a table with some values in it. what I am trying to do is to capture changes of the table content and save them inside a SQLite database. so I connected table to cellChanged and itemSelectionChanged signals. when I try to change some cells content, it will check it to verify it does not contain letters, and it's a number. so if after changing the cell, it has letters inside, it will try to rechange the content to its previous value.
I also have a search toolbar to search in table for some keyword. what the search toolbar does, is to colorize the cells that matching that specific keyword. but in doing so, it emits cellChanged signal and not cellSelectionChanged signal. that causes an error (myapp object has no attribute previous_value), because I haven't selected any cell yet. But if I select a cell and then start seeking, writing anything inside search box will change the content of every cell, because it is triggering cellChanged signal. my question is how to differentiate between color and text changes in cellChanged function. thank you in advance.
here is the sample code:(python 3.7.4, pyqt5 5.13.0)
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
class myapp(QWidget):
def __init__(self):
QWidget.__init__(self)
self.tableWidget = QTableWidget()
self.tableWidget.setSortingEnabled(True)
self.tableWidget.setRowCount(5)
self.tableWidget.setColumnCount(5)
for i in range(5):
for j in range(5):
item=QTableWidgetItem("cell["+str(i) + ","+ str(j) + "]")
self.tableWidget.setItem(i,j,item)
self.tableWidget.cellChanged.connect(self.on_tablewidget_cell_changed)
self.tableWidget.itemSelectionChanged.connect(self.on_tablewidget_item_selection_changed)
self.search_entry = QLineEdit()
self.search_entry.textChanged.connect(self.seek)
layout = QVBoxLayout()
layout.addWidget(self.tableWidget)
layout.addWidget(self.search_entry)
self.setLayout(layout)
def on_tablewidget_cell_changed(self,row,col):
value = self.tableWidget.item(row,col).text()
if not self.check_numeric(value):
self.tableWidget.item(row,col).setText(self.previous_value)
def on_tablewidget_item_selection_changed(self):
row = self.tableWidget.currentRow()
col = self.tableWidget.currentColumn()
self.previous_value = self.tableWidget.item(row,col).text()
def seek(self):
keyword = self.search_entry.text()
items = self.tableWidget.findItems(keyword, Qt.MatchContains)
for item in items:
item.setBackground(Qt.red)
def check_numeric(self,value):
value = value.strip()
tmp = "".join([i for i in value if i in "0123456789"])
return(len(value) == len(tmp))
app = QApplication(sys.argv)
win = myapp()
win.show()
sys.exit(app.exec_())
Instead of capturing cellChanged signals to check if the user input is valid, you could set an item delegate for the table widget, and check the user input in there before setting the model data. For example:
class IntItemDelegate(QItemDelegate):
def setModelData(self, editor, item_model, model_index):
# only update the model data if the user input consists of digits only.
text = editor.text()
if self.check_numeric(text):
item_model.setData(model_index, text)
def check_numeric(self, value):
return value.isdecimal()
class myapp(QWidget):
def __init__(self):
QWidget.__init__(self)
self.tableWidget = QTableWidget()
self.tableWidget.setSortingEnabled(True)
self.tableWidget.setRowCount(5)
self.tableWidget.setColumnCount(5)
for i in range(5):
for j in range(5):
item=QTableWidgetItem("cell["+str(i) + ","+ str(j) + "]")
self.tableWidget.setItem(i,j,item)
# set a custom item delegate which will take care of checking the user input
self.tableWidget.setItemDelegate( IntItemDelegate())
self.search_entry = QLineEdit()
self.search_entry.textChanged.connect(self.seek)
layout = QVBoxLayout()
layout.addWidget(self.tableWidget)
layout.addWidget(self.search_entry)
self.setLayout(layout)
def seek(self):
keyword = self.search_entry.text()
items = self.tableWidget.findItems(keyword, Qt.MatchContains)
for item in items:
item.setBackground(Qt.red)
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.
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)