19

I'm new to Kivy and I have this little demo snippet that demonstrates my problem:

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder


class KivyGuiApp(App):
    def build(self):
        return root_widget


class MyBox(BoxLayout):
    def print_ids(self, *args):
        print("\nids:")
        for widget in self.walk():
            print("{} -> {}".format(widget, widget.id))
    def print_names(self, *args):
        print("\nnames:")
        for widget in self.walk():
            print("{} -> {}".format(widget, widget.name))



root_widget = Builder.load_string("""
MyBox:
    id: screen_manager
    name: 'screen_manager'

    SimpleLayout:
        id: simple_layout
        name: 'simple_layout'


<SimpleLayout@BoxLayout>:
    id: simple_layout_rule
    name: 'simple_layout_rule'
    Button:
        id: button_ids
        name: 'button_ids'
        text: 'print ids to console'
        on_release: app.root.print_ids()
    Button:
        id: button_names
        name: 'button_names'
        text: 'print names to console'
        on_release: app.root.print_names()
""")


if __name__ == '__main__':
    KivyGuiApp().run()

So When you run the code there will be two buttons:

  • first to walk over all widgets and print their names (which works as expected - returns 'name' for each widget),
  • second button to walk over all widgets as well, but instead of names print their ids (which doesn't work - returns None for each id).

My questions are:

  1. Isn't 'id' a property just like 'name'?
  2. How can I access id for each widget from python side?

Bonus question:

  1. Can I access a widget "globally" by it's id (assuming all id's are unique)? By "globally" I mean for example accessing (in code above) ids from 'MyBox:' widget without referencing parent-child, but just by ids (or maybe any other property that would be unique to every widget). I was thinking of creating a dictionary { id : widget object } of all widgets for easy access, unless there is another way I'm not aware of? To emphasize - I'm trying to avoid referencing by children-parent way (which is rather messy when you want to change your widget tree later) and I'd like to generate widgets in .kv language. So what would be the best way to do that?

EDIT:

So here's the easiest way I could think of to reference widgets by unique "global" id.

First I created a class which will be inherited by my App class:

class KivyWidgetInterface():
    ''' interface for  global widget access '''
    global_widgets = {}
    def register_widget(self, widget_object):
        ''' registers widget only if it has unique gid '''
        if widget_object.gid not in self.global_widgets:
            self.global_widgets[widget_object.gid] = widget_object

    def get_widget(self, widget_gid):
        ''' returns widget if it is registered '''
        if widget_gid in self.global_widgets:
            return self.global_widgets[widget_gid]
        else:
            return None

So the widget will be registered only if it has gid - a widget class variable - and it is unique. This way I can store only vital widgets in this dict. Also, it is easily accessible from both .kv and python side.

Now i create the gid variables and register them to the dict in .kv:

<PickDirectory>:
    gid: 'pick_directory'
    on_pos: app.register_widget(self)
    on_selection: app.get_widget('filelist').some_func()
<FileListView>:
    gid: 'filelist'
    on_pos: app.register_widget(self)   
    Button:
        name: 'not important'
    Button:
        gid: 'tab_browse_button1'
        on_pos: app.register_widget(self)

Thing that bothers me actually is that I register my widgets in this "global" dictionary with the 'on_pos' event... which I really don't like, but I was unable to find any reliable way of calling a register method after the widget was initialized (on_pos is called right after the init phase, when widget is positioned and later very rarely, so... seemed like the least bothering way of doing that with my knowledge of kivy api, the order widgets are initialized with .kv language etc; so if there is a better way I'd be very grafeul for any pointers).

Anyhow, this way I can easy bind any event to any method in any class right from the .kv

One thing to remember is that the gid (global id's) need to be unique globally, but I don't find that any more disturbing than keeping ids unique locally(which could be equally or even more confusing for me). And as I said - I'd like to register the widgets differently, but I couldn't find any other reliable way of doing this (and I don't find Clock to be reliable for such things).

kilbee
  • 403
  • 1
  • 4
  • 13

3 Answers3

8

Actually, no. name in your widgets is a variable and id is just a widget reference, weakref according to the docs. Maybe python docs will help you understand how it works. What you did was printing id, not a variable "id" inside a widget.

In the kivy docs it's explained that after kv is parsed, ids are collected into a ObservableDict. The id works like a python dict key id:Widget but only if accessed through the dictionary(ids). I think kv parser just takes all ids into dict and works only with the dict it creates.

Button:
    id: test
    text: 'self.id'
#or
Button:
    id: 'test'
    text: 'self.id'

Even if it's written like a string, nothing changes. So I expect parser to behave like this: grabs whatever whole word is after id:, turns to a string, appends to a ids dictionary <id_string>:Widget_weakref, forgets about id in your .kv or just ignores it if it works with .kv again. Therefore, when id is called directly(not dictionary-like d[key]), it behaves like an empty/None variable. I hope I'm right.


To answer the second and the third one:

If you mean accessing widget by id in MyBox directly for example SimpleLayout, then yes.

python class:

self.ids.simple_layout

kv MyBox rule:

MyBox:
    id: screen_manager
    name: 'screen_manager'
    BoxLayout:
        Label:
            id: my_label
            text: 'test'
        Button:
            text: 'button'
            on_release: self.text = root.ids.my_label.text

However, to access all widgets by their ids in way like python globals work, it's not possible. You'd need to access class/widget first and then its ids dictionary

Peter Badida
  • 11,310
  • 10
  • 44
  • 90
  • Thanks, this finally clears up everything for me and answers my questions. So I have created my own class with dict of custom variables (used as ids in dict keys) and objects (widgets) as values. I can now easily access any widget from any place I want with simple reference, without worrying about gui structure, assuming keys (custom ids) are unique of course. It works and serves the purpose, but the question is - is it ok to keep such dict and access data this way? Or the design is bad? – kilbee Mar 04 '16 at 19:10
  • Well, it's for you to say if it's good or bad, better said.. if you really need such a thing or it just takes more space. Also, it depends on how good/bad it's coded for performance stuff. However, you won't make `.apk` significantly bigger with it that's for sure. Maybe it'd be nice to provide simple and _short_ example under your question as this kind of storing could help someone in future. And maybe it can become a bad habit(storing all widgets) or some security issues can appear. – Peter Badida Mar 04 '16 at 19:23
  • I was affraid it would take up too much resources for example, or there was a better way to do that with single call, also people do not seem to be bothered by the lack of such feature so it got me thinking that my way of doing things may not be practical (even if it seems to me the easiest/fastest way of designing gui in kivy). So I guess I'll indeed edit my post with my solution just so people point out any issues with it. – kilbee Mar 04 '16 at 19:45
  • I think people may have same id for different widgets, that would could be a problem together with storing it at one place. Python-people generally don't like `globals`, though sometimes do. This seems like a `global` to me. Also, now it's not restricting users from having same names, manipulating it for each widget differently and so on. The pros are that ids dict inside a widget contains only its children ids and if you'd have only single dict, there would be no such thing. However, now you can use ids separately, or join them together like you did and this probably makes it the best way. – Peter Badida Mar 04 '16 at 20:07
  • My thinking is exactly that - duplicate id in two different widget trees can be even more confusing than just unique ids. Anyway I edited the post with my solution wich actually joins the best of two worlds I think. There are some issues with this as well, as pointed out in the post. – kilbee Mar 04 '16 at 20:38
4

Isn't 'id' a property just like 'name'?

No, ids are a special syntax that exist only in kv language, or accessible via the root widget of the rule in python (self.ids.idname). There is an id property, but I'm not sure it's actually used anywhere, it seems to exist mostly for legacy reasons. I think we were considering removing it.

How can I access id for each widget from python side?

Via the ids property of the root widget. For instance, in a MyBox method you can write self.ids.simple_layout to get the SimpleLayout.

I was thinking of creating a dictionary { id : widget object } of all widgets for easy access

This is not generically possible because multiple widgets can have the same id. That's why they are rule-local and accessed via the root widget of the rule.

You can construct such a list yourself if you like, either accounting for duplicates or with the knowledge that you won't create them.

inclement
  • 29,124
  • 4
  • 48
  • 60
  • Thanks, I ended up with dict I can easily access so this solves my problem. What's your opinion on keeping references in dict? Is it bad design, would recommend something better? – kilbee Mar 04 '16 at 19:14
3

Recently I have been asking myself a similar question: how to access a property in another widget. I have found the answer myself and I'm posting here as I find it not very intuitive.

The idea of this code is simple: when on click a button, it changes a property which is in a child widget of another class.

Usually when calling inside the same widget tree it's rather easy and can be called by a simple self.ids.simple_layout (or any variation with app, self or root).

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button

kv = """
<Test>:
    Button:
        text: "Access label"
        on_press: root.access_label()
    MyWidget:
        id: my_widget


<MyWidget>
    Label:
        id: my_label
        text: "not yet accessed"
"""

Builder.load_string(kv)
class MyWidget(BoxLayout):
    pass

class Test(BoxLayout):
   def access_label(self):
        self.ids.my_widget.ids.my_label.text = 'Accessed!'

class TestApp(App):
    def build(self):
        return Test()

if __name__ == '__main__':
    TestApp().run()

The tricky part is self.ids.my_widget.ids.my_label.text and the fact that the id my_widget must be in the root widget and not in the <My_widget> definition.

I'm not sure I fully understand myself, but from what I understand it seems that when defining another widget with another class, it creates another tree, which means:

  • the id has to be assigned in the main tree
  • the two trees are not linked and one has to use twice ids when accessing a property or a function.

Please correct me if I'm wrong

L. C.
  • 93
  • 2
  • 8
  • The `build()` method returns `Test()` which is the root widget which is overridden by the `` rule, which adds a `Button` and a `MyWidget` widget. The `MyWidget`, is overridden by the `` rule, which adds a `Label`. The collective ID's are now nested, hence the additional `.ids` is needed to access the modified `` ID's tree. You now need to traverse from the root, through `MyWidget:` and it's ID's to reach `my_label.text` You have one root, `Test:` but it has been modified to include a simple `Button:` and a complex `MyWidget:` widget, hence the convoluted pathway. – Android Control Aug 26 '23 at 17:13
  • This may also help clarify: [https://kivy.org/doc/stable/api-kivy.uix.widget.html#kivy.uix.widget.Widget.ids](https://kivy.org/doc/stable/api-kivy.uix.widget.html#kivy.uix.widget.Widget.ids) – Android Control Aug 26 '23 at 17:14
  • Also, you could just use a Kivy property and link it to an `id:` as explained here: [https://stackoverflow.com/a/47445226/1506858](https://stackoverflow.com/a/47445226/1506858) – Android Control Aug 26 '23 at 17:34