What's difference setData, setItemData and setIem method of QStandardItemModel? - pyqt

I am confused how to use setData, setItemData and setItem method of QStandardItemModel, these method seem have the same effect, i wonder to know which method should i choose to use will be best?
class DemoD(QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
table = QTableView()
model = QStandardItemModel(4, 2)
table.setModel(model)
# delegate = SpinBoxDelegate()
# table.setItemDelegate(delegate)
for row in range(4):
for col in range(2):
item = QStandardItem('china')
model.setItem(row, col, item)
index = model.index(row, col)
value = QVariant((row + 1)*(col + 1))
model.setData(index, value)
model.setItemData(index, {1: 'a', 2: 'b'})
self.setCentralWidget(table)
self.resize(400, 300)
app = QApplication([])
demo = DemoD()
demo.show()
app.exec()

If you want to understand the concepts of a Qt model you should read the following guides:
Model/View Programming
Model/View Tutorial
Why Qt is misusing model/view terminology?
Documentation of each method: setData(), setItemData() and setItem().
Previous concepts:
QStandarItemModel: It is a class that inherits from QAbstractItemModel that allows to store any type of information unlike QAbstractItemModel that only defines the behavior.
Considering that you have read the previous links carefully, we will try to explain the difference between the different methods that you indicate:
setData(): Every Qt model inherits from QAbstractItemModel so this class defines the general behavior, in this case it is defined that the setData() model is responsible for modifying the information of a role associated with a QModelIndex. In other words, it is the generic method that you have to implement if you want to implement an editable model, for example QStringListModel is not an editable model so it does not implement it but in the case of QStandardItemModel it is editable so you can modify the information of the model through of that method.
setItem(): QStandardItem is a concept of QStandardItemModel that is conceptually similar to QModelIndex. This element allows you to easily interact with the QModelIndex. If a QStandardItem is not associated with a model it will only store the information, at the time a model is assigned all information is passed to the model, and the model informs you of any changes that can be made by other methods such as setData. An equivalent to setData of the model is the setData method of QStandardItem but the latter does not need to provide the QModelIndex since that information is internally available or can be obtained when a model is established.
For example:
it.setText("foo")
it.setTextAlignment(QtCore.Qt.AlignCenter)
is equivalent to
it.model().setData(it.index(), "foo", QtCore.Qt.DisplayRole)
it.model().setData(it.index(), QtCore.Qt.AlignCenter, QtCore.Qt.TextAlignmentRole)
As you can see, QStandardItem allows you to modify the information of the item in a simple way, and in a simple way you can say that it is an item of the model.
setItemData(): It is a method that allows you to modify the information of several roles associated to a QModelIndex by checking if the roles are valid, in general if you use an invalid model the method will not update the information but in the case of QStandardItemModel that handles the generic information it is established that everything Role is valid for what will always work.
In the case of QStandardItemModel the following codes are equivalent:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
model = QtGui.QStandardItemModel(1, 1)
it = QtGui.QStandardItem()
model.setItem(0, 0, it)
# The following lines modify the text shown
# to be related to the Qt::DisplayRole role and
# the QModelIndex associated with the QStandardItem:
it.setText("foo")
it.setData("foo", QtCore.Qt.DisplayRole)
model.setData(it.index(), "foo", QtCore.Qt.DisplayRole)
model.setItemData(it.index(), {QtCore.Qt.DisplayRole: "foo"})
# The same as the previous lines but in the case of
# the background and the text colors of the item.
it.setForeground(QtGui.QColor("red"))
it.setBackground(QtGui.QColor("blue"))
it.setData(QtGui.QColor("red"), QtCore.Qt.ForegroundRole)
it.setData(QtGui.QColor("blue"), QtCore.Qt.BackgroundRole)
model.setData(it.index(), QtGui.QColor("red"), QtCore.Qt.ForegroundRole)
model.setData(it.index(), QtGui.QColor("blue"), QtCore.Qt.BackgroundRole)
model.setItemData(
it.index(),
{
QtCore.Qt.ForegroundRole: QtGui.QColor("red"),
QtCore.Qt.BackgroundRole: QtGui.QColor("blue"),
},
)

Both setData and setItemData are very similar.
What you have to understand is that Qt models use roles to assign certain data to each "index". This means that each index (a reference to a model row and column, possibly including a parent if the model supports trees) can have different data attached to it. The most commonly used data role is the "DisplayRole", which is what an item view usually shows as text; but other data is usually implemented (see ItemDataRole, which helps an item view to correctly show the model data to the user.
The most important difference between setData and setItemData is the mapping. What you're doing does not work as the keywords you're using are not recognized as usable roles.
In your example ({1: 'a', 2: 'b'}), 1 maps to DecorationRole (which is used from item views to show a decoration - an icon) and 2 maps to EditRole, which is used whenever the user wants to edit the contents of that item, something that can differ from what's displayed (think about entering a date in a short form such as "10/11", that can be an actual date that is finally shown as "november 10 2019").
Finally, setItem is a special function of QStandardItemModel that creates a new item (or overwrites an existing one) with the new provided QStandardItem object.
I'm providing a test example that will better show what happens in all three situations.
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.table = QtWidgets.QTableView()
layout.addWidget(self.table)
# hide headers, we're not interested
self.table.horizontalHeader().setVisible(False)
self.table.verticalHeader().setVisible(False)
self.table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.model = QtGui.QStandardItemModel()
self.table.setModel(self.model)
for item in range(1, 6):
item = QtGui.QStandardItem('item {}'.format(item))
self.model.appendRow(item)
toolLayout = QtWidgets.QHBoxLayout()
layout.addLayout(toolLayout)
self.itemTextEdit = QtWidgets.QLineEdit('text')
toolLayout.addWidget(self.itemTextEdit)
self.itemSetTextButton = QtWidgets.QPushButton('Set text')
toolLayout.addWidget(self.itemSetTextButton)
self.itemSetTextButton.clicked.connect(self.setText)
toolLayout.addSpacing(5)
self.itemAlignCombo = QtWidgets.QComboBox()
toolLayout.addWidget(self.itemAlignCombo)
for alignText in ('Left', 'Center', 'Right'):
alignment = QtCore.Qt.AlignVCenter | getattr(QtCore.Qt, 'Align{}'.format(alignText))
self.itemAlignCombo.addItem(alignText, alignment)
self.itemSetAlignButton = QtWidgets.QPushButton('Set alignment')
toolLayout.addWidget(self.itemSetAlignButton)
self.itemSetAlignButton.clicked.connect(self.setAlignment)
self.table.setCurrentIndex(self.model.index(0, 0))
toolLayout.addSpacing(5)
self.setDataButton = QtWidgets.QPushButton('SetItemData()')
toolLayout.addWidget(self.setDataButton)
self.setDataButton.clicked.connect(self.setItemData)
setItemLayout = QtWidgets.QHBoxLayout()
layout.addLayout(setItemLayout)
self.itemRowSpin = QtWidgets.QSpinBox()
setItemLayout.addWidget(self.itemRowSpin)
self.itemRowSpin.setRange(1, self.model.rowCount() + 1)
self.itemRowSpin.setValue(self.itemRowSpin.maximum())
self.setItemButton = QtWidgets.QPushButton('SetItem()')
setItemLayout.addWidget(self.setItemButton)
self.setItemButton.clicked.connect(self.setItem)
def setText(self):
# set the text of the current item
index = self.table.currentIndex()
self.model.setData(index, self.itemTextEdit.text())
def setAlignment(self):
# set the alignment of the current item
index = self.table.currentIndex()
self.model.setData(index, self.itemAlignCombo.currentData(), QtCore.Qt.TextAlignmentRole)
def setItemData(self):
# set *both* text and alignment of the current item
index = self.table.currentIndex()
self.model.setItemData(index, {
QtCore.Qt.DisplayRole: self.itemTextEdit.text(),
QtCore.Qt.TextAlignmentRole: self.itemAlignCombo.currentData()
})
def setItem(self):
# set a new item for the selected row with the selected text and alignment
item = QtGui.QStandardItem()
item.setText(self.itemTextEdit.text())
item.setTextAlignment(QtCore.Qt.Alignment(self.itemAlignCombo.currentData()))
self.model.setItem(self.itemRowSpin.value() - 1, 0, item)
self.itemRowSpin.setMaximum(self.model.rowCount() + 1)
self.itemRowSpin.setValue(self.itemRowSpin.maximum())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

Related

Widget inside QListWidgetItem disappears after internal move

I have a QListWidget which is populated by QLabel via .setItemWidget() and a drag and drop mode InternalMove, when I move an item inside the list its label disappears.
How can I solve this issue?
A minimal example to reproduce
from PyQt5.QtWidgets import (
QApplication, QLabel, QStyle,
QListWidget, QListWidgetItem
)
from PyQt5.QtCore import QSize
import sys
if __name__ == '__main__':
app = QApplication(sys.argv)
list = QListWidget()
list.setFixedHeight(400)
list.setDragDropMode(QListWidget.DragDropMode.InternalMove)
for _ in range(8):
item = QListWidgetItem()
item.setSizeHint(QSize(40, 40))
list.addItem(item)
label = QLabel()
label.setPixmap(list.style().standardIcon(
QStyle.StandardPixmap.SP_ArrowUp).pixmap(QSize(40,40)))
list.setItemWidget(item, label)
list.show()
sys.exit(app.exec())
edit
After reading the documentation for the .setItemWidget() which states:
This function should only be used to display static content in the place of a list widget item. If you want to display custom dynamic content or implement a custom editor widget, use QListView and subclass QStyledItemDelegate instead.
I wonder if this is related to the issue and what does "static content" mean in this context, is QLabel considered "dynamic content"?
edit #2
The problem is inside a dropEvent() a dropMimeData() is called which in turn creates a complete new item? (rowsInserted is called), which isn't supposed to happen for self items I guess, because a widget set in the dragged item isn't serialized and stored inside mimedata so the widget is decoupled, The dropMimeData() is usually called when you drag and drop items from a different list.
So I guess an ugly way to solve this is to store a manually serialized widget inside a QListWidget.mimeData() as a custom mimetype via QMimeData.setData() and recreate the widget after a drop inside QListWidget.dropMimeData().
for example:
from PyQt5.QtWidgets import (
QApplication, QLabel, QStyle,
QListWidget, QListWidgetItem
)
from PyQt5.QtCore import QSize, QMimeData, QBuffer, QIODevice
from PyQt5.QtGui import QPixmap
import pickle
import sys
class ListWidget(QListWidget):
def mimeData(self, items:list[QListWidgetItem]) -> QMimeData:
mimedata = QListWidget.mimeData(self, items)
# e.g. serialize pixmap
custommime = []
for item in items:
label:QLabel = self.itemWidget(item)
buff = QBuffer()
buff.open(QIODevice.OpenModeFlag.WriteOnly)
label.pixmap().save(buff, 'PNG')
buff.close()
custommime.append(buff.data())
mimedata.setData('application/custommime', pickle.dumps(custommime))
#
return mimedata
def dropMimeData(self, index:int, mimedata:QMimeData, action) -> bool:
result = QListWidget.dropMimeData(self, index, mimedata, action)
# e.g. recreate pixmap
if mimedata.hasFormat('application/custommime'):
for i, data in enumerate(
pickle.loads(mimedata.data('application/custommime')),
start=index):
pixmap = QPixmap()
pixmap.loadFromData(data, 'PNG')
label = QLabel()
label.setPixmap(pixmap)
self.setItemWidget(self.item(i), label)
#
return result
if __name__ == '__main__':
app = QApplication(sys.argv)
list = ListWidget()
list.setFixedHeight(400)
list.setDragDropMode(QListWidget.DragDropMode.InternalMove)
list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
for i in range(8):
item = QListWidgetItem()
item.setSizeHint(QSize(40, 40))
list.addItem(item)
label = QLabel()
label.setPixmap(list.style().standardIcon(
QStyle.StandardPixmap.SP_DialogOkButton + i).pixmap(QSize(40,40)))
list.setItemWidget(item, label)
list.show()
sys.exit(app.exec())
This is caused by a Qt bug which only affects fairly recent versions. I can consistently reproduce it when using Qt-5.15.6 and Qt-6.4.0 - but not e.g. Qt-5.12.1. The issue seems to be closely related to QTBUG-100128.
UPDATE:
Unfortunately, after some further experimentation today, it seems the suggested work-around given below isn't an effective solution. I have found it's also possible to make item-widgets disappear by drag and drop onto non-empty areas.
After testing some other versions of Qt5, I can confirm that the bug is completely absent in 5.12.x, 5.13.x, 5.14.x, 5.15.0 and 5.15.1. This agrees with the existing Qt bug report above which identified Qt-5.15.2 as the version where the bug was introduced.
Contrary to what is suggested in the question, there's no reason whatsoever why a label should not be used as an item-widget. The term "static content", just means "not updated by user-defined custom drawing".
UPDATE 2:
This bug seems to be a regression from QTBUG-87057, which made quite a large number of internal changes to how list-view rows are moved during drag and drop. The complexity of those change probably means a simple work-around that undoes its negative side-effects probably isn't possible. The changes affect all Qt5 versions greater than 5.15.1 and Qt6 versions greater than 6.0.
AFAICS, this only affects dragging and dropping the current last item in the view onto a blank area. Other items and multiple selections aren't affected. This suggests the following work-around:
class ListWidget(QListWidget):
def dropEvent(self, event):
if (self.currentRow() < self.count() - 1 or
self.itemAt(event.pos()) is not None):
super().dropEvent(event)
list = ListWidget()
...
or using an event-filter:
class Monitor(QObject):
def eventFilter(self, source, event):
if event.type() == QEvent.Drop:
view = source.parent()
if (view.currentRow() == view.count() - 1 and
view.itemAt(event.pos()) is None):
return True
return super().eventFilter(source, event)
monitor = Monitor()
list = QListWidget()
list.viewport().installEventFilter(monitor)
...

Accessing variables from a method in class A and using it in Class B in python3.5

I have a BaseClass and two classes (Volume and testing) which inherits from the BaseClass. The class "Volume" use a method "driving_style" from another python module. I am trying to write another method "test_Score" which wants to access variables computed in the method "driving_style" which I want to use to compute further. These results will be accessed to the class "testing" as shown.
from training import Accuracy
import ComputeData
import model
class BaseClass(object):
def __init__(self, connections):
self.Type = 'Stock'
self.A = connections.A
self.log = self.B.log
def getIDs(self, assets):
ids = pandas.Series(assets.ids, index=assets.B)
return ids
class Volume(BaseClass):
def __init__(self, connections):
BaseClass.__init__(self, connections)
self.daystrade = 30
self.high_low = True
def learning(self, data, rootClass):
params.daystrade = self.daystrade
params.high_low = self.high_low
style = Accuracy.driving_style()
return self.Object(data.universe, style)
class testing(BaseClass):
def __init__(self, connections):
BaseClass.__init__(self, connections)
def learning(self, data, rootClass):
test_score = Accuracy.test_score()
return self.Object(data.universe, test_score)
def driving_style(date, modelDays, params):
daystrade = params.daystrade
high_low = params.high_low
DriveDays = model.DateRange(date, params.daystrade)
StopBy = ComputeData.instability(DriveDays)
if high_low:
style = ma.average(StopBy)
else:
style = ma.mean(StopBy)
return style
def test_score(date, modelDays, params):
"want to access the following from the method driving_style:"
DriveDays =
StopBy =
return test_score ("which i compute using values DriveDays and StopBy and use test_score in the method learning inside
the 'class - testing' which inherits some params from the BaseClass")
You can't use locals from a call to a function that was made elsewhere and has already returned.
A bad solution is to store them as globals that you can read from later (but that get replaced on every new call). A better solution might to return the relevant info to the caller along with the existing return values (return style, DriveDays, StopBy) and somehow get it to where it needs to go. If necessary, you could wrap the function into a class and store the computed values as attributes on an instance of the class, while keeping the return type the same.
But the best solution is probably to refactor, so the stuff you want is computed by dedicated methods that you can call directly from test_score and driving_style independently, without duplicating code or creating complicated state dependencies.
In short, basically any time you think you need to access locals from another function, you're almost certainly experiencing an XY problem.

PyMEL embedding layouts

what's the best way to embed multiple PyMEL layouts in a single form?
For example, for each row of a form, I want to specify that:
row one uses a 2-column layout for label+textField pair
row two uses a 1-column layout for a menu item that has its own annotation label
row three adds a full-width execution button to the 1-column layout
all controls scale appropriately if the window is resized by a user
TIA!
import pymel.core as pm
def test(*args):
print('model name: {}'.format(modelTField.getText()))
print('model geom: {}'.format(modelMenu.getValue()))
if pm.window("testUI", ex=1): pm.deleteUI("testUI")
window = pm.window("testUI", t="Test v0.1", w=500, h=200)
mainLayout = pm.verticalLayout()
# two column layout
col2Layout = pm.horizontalLayout(ratios=[1, 2], spacing=10)
pm.text(label='Model name')
global modelTField
modelTField = pm.textField()
col2Layout.redistribute()
# single column layout
col1Layout = pm.horizontalLayout()
global modelMenu
modelMenuName = "modelMenu"
modelMenuLabel = "Model mesh"
modelMenuAnnotation = "Select which geo corresponds to the model shape"
modelMenu = pm.optionMenu(modelMenuName, l=modelMenuLabel, h=20, ann=modelMenuAnnotation)
pm.menuItem(l="FooShape")
pm.menuItem(l="BarShape")
# execute
buttonLabel = "[DOIT]"
button = pm.button(l=buttonLabel, c=test)
col2Layout.redistribute()
# display window
pm.showWindow(window)
Since the pm.horizontalLayout() and pm.verticalLayout automatically redistribute the content, you do not need to call the redistribute yourself. But for these functions to work you need a with statement like this:
import pymel.core as pm
def test(*args):
print('model name: {}'.format(modelTField.getText()))
print('model geom: {}'.format(modelMenu.getValue()))
if pm.window("testUI", ex=1): pm.deleteUI("testUI")
window = pm.window("testUI", t="Test v0.1", w=500, h=200)
with pm.verticalLayout() as mainLayout:
with pm.horizontalLayout(ratios=[1, 2], spacing=10) as col2Layout:
pm.text(label='Model name'
global modelTField
modelTField = pm.textField()
with pm.horizontalLayout() as col1Layout:
global modelMenu
modelMenuName = "modelMenu"
modelMenuLabel = "Model mesh"
modelMenuAnnotation = "Select which geo corresponds to the model shape"
modelMenu = pm.optionMenu(modelMenuName, l=modelMenuLabel, h=20, ann=modelMenuAnnotation)
pm.menuItem(l="FooShape")
pm.menuItem(l="BarShape")
# execute
buttonLabel = "[DOIT]"
button = pm.button(l=buttonLabel, c=test)
# display window
pm.showWindow(window)
To create UI with python it helps a lot if you enclose everything into a class, especially if you use PyMel. This way you could do:
class MyWindow(pm.ui.Window):
def __init(self):
self.title = "TestUI"
def buttonCallback(self, *args):
do something....
This is very helpful if you have callbacks since everything you need can be accessed from within the class and you need no global variables at all.

Return old value to combobox with dynamic search and autocompletion

I have a reimplemented comboBox that performs dynamic search and autocompletion (code isn't mine). The problem is when I type something, that doesn't match any value in combobox list and press enter - I receive an empty string. But I wish to receive instead an old value, that was in combobox before I started to type other value. Could anybody help me with that?
Also I want to ask the meaning of 2 strings in ExtendedComboBox class (as long as code isn't mine):
inside function on_completer_activated there is expression if text: ; I can't understand what does it mean, because I always write the whole expression (like if text == True: or something like that)
I don't understand the meaning of [str] in line self.activated[str].emit(self.itemText(index)). I have never seen this kind of construction in pyqt when something in square brackets comes directly after a signal.
code:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class ExtendedComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
super(ExtendedComboBox, self).__init__(parent)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setEditable(True)
# add a filter model to filter matching items
self.pFilterModel = QtCore.QSortFilterProxyModel(self)
self.pFilterModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.pFilterModel.setSourceModel(self.model())
# add a completer, which uses the filter model
self.completer = QtWidgets.QCompleter(self.pFilterModel, self)
# always show all (filtered) completions
self.completer.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion)
self.setCompleter(self.completer)
# connect signals
self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
self.completer.activated.connect(self.on_completer_activated)
# on selection of an item from the completer, select the corresponding item from combobox
def on_completer_activated(self, text):
if text:
index = self.findText(text)
self.setCurrentIndex(index)
self.activated[str].emit(self.itemText(index))
# on model change, update the models of the filter and completer as well
def setModel(self, model):
super(ExtendedComboBox, self).setModel(model)
self.pFilterModel.setSourceModel(model)
self.completer.setModel(self.pFilterModel)
# on model column change, update the model column of the filter and completer as well
def setModelColumn(self, column):
self.completer.setCompletionColumn(column)
self.pFilterModel.setFilterKeyColumn(column)
super(ExtendedComboBox, self).setModelColumn(column)
class ComboBox_Model(QtCore.QAbstractListModel):
def __init__(self, data_list = [], parent = None):
super(ComboBox_Model, self).__init__()
self.data_list = data_list
def rowCount(self, parent):
return len(self.data_list)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
value = self.data_list[row]
return value
if role == QtCore.Qt.EditRole:
row = index.row()
value = self.data_list[row]
return value
class Mainwindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.combobox = ExtendedComboBox()
self.layout_1 = QtWidgets.QHBoxLayout()
self.layout_1.addWidget(self.combobox)
self.setLayout(self.layout_1)
data = ['some text to display', 'other text to display', 'different text']
self.model = ComboBox_Model(data)
self.combobox.setModel(self.model)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
application = Mainwindow()
application.show()
sys.exit(app.exec())
When a combobox is set as editable, by default allows insertion of non existing items at the bottom of the current model when pressing return. Since the model used in that code is not editable, when pressing return with unrecognized text the combobox is unable to add the new item (and select it), which results in setting the index to -1.
You can connect to the embedded QLineEdit returnPressed signal and check whether the current index is valid or not; this is possible because the signal is also previously connected to the combobox insertion, so when you receive the signal the combo has already tried to add the new item and eventually set the (possibly) invalid index.
In order to store the previous index, just connect to the currentIndexChanged() and save it as long as it's greater or equal to 0.
class ExtendedComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
# ...
self.lineEdit().returnPressed.connect(self.returnPressed)
self.currentIndexChanged.connect(self.storePreviousIndex)
self.previousIndex = self.currentIndex()
def storePreviousIndex(self, index):
if index >= 0:
self.previousIndex = index
def returnPressed(self):
if self.currentIndex() < 0 or self.currentText() != self.itemText(self.currentIndex()):
self.setCurrentIndex(self.previousIndex)
Note that the second comparison in returnPressed is to add compatibility to the default internal model, in case setModel() is not called and the insertion policy is NoInsert.
About the two final questions:
the if statement checks if the condition is true or not, or, if you want, the condition is not false, as in "not nothing" (aka, False, 0, None); you can do some experiments with simple statements to better understand: if True:, if 1:, if 'something': will all result as valid conditions, while if False:, if 0: or if '': not.
some signals have multiple signatures for their arguments, meaning that the same signal can be emitted more than once, each time with different types of arguments; for example the activated signal of QComboBox is emitted twice, the first time as int with the new current index, then with the new current text; whenever you want to connect to (or emit) an overload that is not the default one, you need to specify the signature in brackets. In the case above, the signal is explicitly emitted for the str signature only (I don't know why the int was not, though). Note that overloaded signals are being gradually removed in Qt (in fact, the [str] signature of activated() is considered obsolete since Qt 5.14).

How to access widget after being created by add_widget?

When I use add_widget, and try to access it by using ids, I get a key error.
def create_rss(self, *args):
for rss in "food", "wood", "stone", "iron", "gold":
self.ids["res"].add_widget(ToggleButton(id=rss, text=rss))
self.ids["res"].ids[rss].state = "down"
I expected to be able to change the state of the toggle button since it was being done before creating it with add_widget.
When trying to print the parent of the toggle button, I get nothing. So maybe add_widget its not making the togglebuttons the children of "res"?
Question
how can I access the button out side of the for loop?
Solution
There are two methods for the solution.
Method 1 - Create own ids dictionary
This method involves creating our own ids dictionary type property. The advantages of this method are random and sequential access.
Snippets
from kivy.properties import DictProperty
...
class class-name(...):
my_ids = DictProperty({})
def create_rss(self, *args):
for rss in "food", "wood", "stone", "iron", "gold":
toggle_button = ToggleButton(id=rss, text=rss)
self.my_ids[rss] = toggle_button
self.ids["res"].add_widget(toggle_button)
toggle_button.state = "down"
def access_using_my_ids(self):
print(f"\nsequential access:")
for key, value in self.my_ids.items():
print(f"key={key}, value={value}, text={value.text}")
print(f"\nrandom access:")
print(f"object={self.my_ids['stone']}, text={self.my_ids['stone'].text}")
Method 2 - Kivy Widget Tree
This method uses Kivy Widget Tree and a for loop to access the children. The disadvantages of this method is sequential access only.
Snippets
for child in reversed(self.ids.res.children):
if isinstance(child, ToggleButton):
print(f"ToggleButton.text={child.text}")
Kivy ids - created in Python script
Kivy ids created in Python script are not stored in self.ids dictionary type property. Therefore, one will get an error when trying to access it.
Solution
Assign it to a variable.
Snippets
def create_rss(self, *args):
for rss in "food", "wood", "stone", "iron", "gold":
toggle_button = ToggleButton(id=rss, text=rss)
self.ids["res"].add_widget(toggle_button)
toggle_button.state = "down"

Resources