How to access widgets within and outside of a screen class

7,644 views
Skip to first unread message

Tyro

unread,
Jul 4, 2017, 9:21:26 PM7/4/17
to Kivy users support

I am at sea again in trying to refer to widgets both within and outside of the class in which they’re created.  I was able to do so before I incorporated screens into my program, having been taught to use the App.get_running_app() method, but that doesn’t seem to work now.  In this scaled-down code example, I use two screens, one created within the Python code and one in a kv file.  I’ve highlighted three examples of the problem I’m having.

Hoping for guidance.  Thanks.

 

kv file:


 <CustButton@Button>:

    font_size: 40
    color: 0, 0, 0, 1
    background_normal: ''
    background_color: .3, .3, 1,1
<FirstScreen>:
    BoxLayout:
        orientation: 'vertical'
        BoxLayout:
            padding: 200
            CustButton:
                id: switch_button
                text: "screen switch button"
                on_press: root.manager.current = 'second'

 

Python:


from kivy.app import App

from kivy.uix.screenmanager import ScreenManager, Screen

from kivy.uix.boxlayout import BoxLayout

from kivy.uix.dropdown import DropDown

from kivy.uix.button import Button

from kivy.uix.label import Label

from kivy.uix.textinput import TextInput

 

 

user_list = ['alpha','beta','gamma']

class SecondScreen(Screen):

    def __init__(self, **kwargs):

        super(SecondScreen, self).__init__(**kwargs)

        b = BoxLayout(orientation='vertical', padding=500)

        t = TextInput(id='my_text', height='60dp', cursor_width="2sp")

        dropdown = DropDown()

        for index in range(len(user_list)):

            btn = Button(text=user_list[index], size_hint_y=None, height=44)

            btn.bind(on_release=lambda btn: dropdown.select(btn.text))

            dropdown.add_widget(btn)

        mainbutton = Button(text='Names')

        mainbutton.bind(on_release=dropdown.open)

        dropdown.bind(on_select=lambda instance, x: setattr(t, 'text', x))

        b.add_widget(t)

        b.add_widget(mainbutton)

        l=Label(height='60dp')  #spacer

        b.add_widget(l)

        j= Button(text ="switch back")

        #j.bind(on_release=outside_function())

        j.bind(on_release=self.switcher)

        b.add_widget(j)

        self.add_widget(b)

 

    def switcher(self,obj):

        global user

        self.manager.current = 'first'

        # 1) If I try this:

        user = self.t.text

        #  I get: AttributeError: 'SecondScreen' object has no attribute 't'

 

        # 2) If I try this:

        app_ref = App.get_running_app()

        user = app_ref.root.ids.t.text

        #  I get: AttributeError: 'super' object has no attribute '__getattr__'

 

 

# 3) If I call this function from within SecondScreen class and try to refer to a widget:

def outside_function():

    app_ref = App.get_running_app()

    app_ref.root.ids.switch_button.disabled = True

    #  I get: AttributeError: 'NoneType' object has no attribute 'ids'

 

 

class FirstScreen(Screen):

    pass

 

class TestApp(App):

    def build(self):

        sm = ScreenManager()

        sm.add_widget(FirstScreen(name='first'))

        sm.add_widget(SecondScreen(name='second'))

        return sm

 

if __name__ == '__main__':

    TestApp().run()

Monty Mole

unread,
Jul 5, 2017, 3:14:17 AM7/5/17
to Kivy users support
you could keep a global reference to the root layout at build time. or you could traverse the widget tree by using parent and child references.
example below, all 3 references in   def callback_rvbtn(self)   work.



from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button

Builder.load_string('''
<MainInterface>:
    orientation: 'vertical'

    Label:
        text: 'Recycleviewtest'
        font_size: '20'
        size_hint: 1,.1
        text_size: self.size
   
    Label:
        id: stat
        text: 'status here...'
        font_size: '20'
        size_hint: 1,.1
        text_size: self.size
   
    RecycleView:
        id: rv
        viewclass: 'myButton'
        RecycleBoxLayout:
            orientation: 'vertical'
            default_size: None, dp(40)
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height

    Button:
        text: 'Press me'
        size_hint_y: 0.1
        on_release: root.callback_btn(self)

<myButton>:
    on_release:self.callback_rvbtn()

''')

root = None
stat = None

class myButton(Button):
    def callback_rvbtn(self):
        #self.parent.parent.parent.ids.stat.text = 'btn recycle pressed: %s' %self.text 
        #root.ids.stat.text = 'btn recycle pressed: %s' %self.text
        stat.text = 'btn recycle pressed: %s' %self.text
   
class MainInterface(BoxLayout):
    def __init__(self, **kwargs):
        super(MainInterface, self).__init__(**kwargs)
        self.ids.rv.data = [{'text': str(x)} for x in range(200)]
   
    def callback_btn(self,btn):
        self.ids.rv.data =  [{'text': 'yo !'} for x in range(200)]
       

class TestApp(App):
    def build(self):
        global root            
        global stat
        root = MainInterface()   
        stat = root.ids.stat
        return root

Tyro

unread,
Jul 5, 2017, 12:43:29 PM7/5/17
to Kivy users support
Thanks very much for taking the time to write up this code.  I will try to incorporate this into my program, though I confess that this beginner is having trouble seeing exactly how.  I have to say that I'm surprised there's not a simpler way to access widgets--both within and outside of the class in which they're defined.  Is my program structure that unusual?  Does the problem have anything to do with the fact that I define one screen in the kv file and the other within the Python code?  (Why could I no longer refer to all widgets using the App.get_running_app() method once I incorporated screens?)

Monty Mole

unread,
Jul 5, 2017, 2:21:03 PM7/5/17
to Kivy users support

without looking too much into your code I wonder why you dont declare everything in the kv file ? You could start with the screenmanager and then put the screens under it with ids to reference each...
something like this:

<MaintInterface>:
    ScreenManager:
        id: screenmanager

        Screen:
            name:   'screen1'
            id: screen1
   
            BoxLayout:
                   ......

        Screen:
            name:   'screen2'
            id: screen2
   
            BoxLayout:
                   ......

in order to access the instances of those classes you can keep the reference to the main instance and then access all other instances by their ids:

class Loader(App):
    def build(self):
        global root
        Builder.load_file(kvfile)
        root = MainInterface()
        return root

and then later:
         root.ids['screenmanager'].current='screen1'

learn kv... it' easier ... do your gui stuff in kv and the logic in the python part...

and btw: the good thing about classes is that they are encapsulated ... the bad thing is that they are encapsulated   :)

Tyro

unread,
Jul 5, 2017, 3:43:17 PM7/5/17
to Kivy users support
The reason I declare one class (SecondScreen) in the Python file is that it involves loading user names from a data file into the dropdown list, and I assumed this couldn't be done in kv.  (In the code I uploaded, I simplified things by including the data (user_list) in the code so that you could run my code without the data file.)  Am I wrong about not being able to read from a file in kv?  The relevant part of my actual (not simplified) code is 

user_list = []
try:
with open('user.dat', 'rb') as f:
user_data = pickle.load(f)
for i in user_data:
user_list.append(i[0])
except:
user_list = []
class SecondScreen(Screen):
    def __init__(self, **kwargs):
        super(SecondScreen, self).__init__(**kwargs)
        b = BoxLayout(orientation = 'vertical', padding = '100dp')
        t = TextInput(id = 'my_text', size_hint_y = None, halign = 'middle', valign = 'middle', height = '60dp')

dropdown = DropDown()
for index in range(len(user_list)):
btn = Button(text=user_list[index], size_hint_y=None, height=44)
btn.bind(on_release=lambda btn: dropdown.select(btn.text))
dropdown.add_widget(btn)
        ... (more stuff here)      

ZenCODE

unread,
Jul 7, 2017, 8:31:12 AM7/7/17
to Kivy users support
If you want to access the buttons directly, you need to store a reference to them. So, in the contructor of ScreenTwo:

        for index in range(len(user_list)):

        self.btns = [Button(text=user_list[index], size_hint_y=None, height=44) for index in range(len(user_list)]

        [drop_down.add_widget(btn) for btn in self.btns]


Now you should be able to access those buttons via self.manager.current_screen.btns[0] etc. There are other ways of course, but that should work?

Tyro

unread,
Jul 7, 2017, 12:22:37 PM7/7/17
to kivy-...@googlegroups.com
Thanks for your help.  What I want is to be able to refer to *any* widget from *any* part of my program, for example, get the text in t, the text widget, regardless of which method is referring to it, whether the method is in the current screen's class or that of another screen, or outside of both, i.e., somewhere else in my program.  This seems fundamental.
Before I adopted screens, I was able to do all this with the App.get_running_app() method.  (So, to get the text in t:  user = app_ref.root.ids.t.text).  I don't know why this doesn't work now that my classes are within screens (if that's the right way to phrase it).  But I'd happily accept anything equivalent!
Thanks again.

Matthew Einhorn

unread,
Jul 7, 2017, 12:42:06 PM7/7/17
to Kivy users support
I didn't read the full thread, but you may want to take a look at the kivy namespace behavior. That allows you to specify a name for a widget that you can access using the namespace.

On Jul 7, 2017 12:22 PM, "Tyro" <jawa...@gmail.com> wrote:
Thanks for your help.  What I want is to be able to refer to *any* widget from *any* part of my program, for example, get the text in t, the text widget, regardless of which method is referring to it, whether the method is in the current screen's class or that of another screen, or outside of both.  I'd like to be able to do this from any method or function anywhere in the program.  This seems fundamental.
Before I introduced screens, I was able to do all this with the App.get_running_app() method.  (So, to get the text in t:  user = app_ref.root.ids.t.text).  I don't know why this doesn't work now that my classes are within screens (if that's the right way to phrase it).  But I'd happily accept anything equivalent!

--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Tyro

unread,
Jul 7, 2017, 5:01:58 PM7/7/17
to Kivy users support
Thank you.  I will look at that.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.

Monty Mole

unread,
Jul 8, 2017, 6:51:47 AM7/8/17
to kivy-...@googlegroups.com
looks like your app_ref is not valid ... did you save it globally ?  heres a little example of different ways to access across class boundaries ..


from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager

Builder.load_string('''
<MainInterface>:
    Screen:
        BoxLayout:
            orientation: 'vertical'

            Label:
                id: statlabel
                text: ''
           
            Button:
                text: 'btn1'
                on_release: root.callback_btn_insideroot(self)
           
            ButtonOutsideRoot:
                text: 'btn2'
                on_release: self.callback_btn_outsideroot()   
           
            ButtonOutsideRoot:
                text: 'btn3'
                on_release: self.callback_btn_outsideroot()
               
            ButtonOutsideRoot:
                text: 'btn4'
                on_release: self.callback_btn_outsideroot()
               
''')

class ButtonOutsideRoot(Button):

    def callback_btn_outsideroot(self):                                                     # no args given so the reference to the root interface(ScreenManager) has has to be found otherwise
        if self.text == 'btn2':                                                                      # self refers to the calling class automatically
            main.ids.statlabel.text = 'referenced by global main %s' %self.text            
           
        elif self.text == 'btn3':            
            App.get_running_app().root.ids.statlabel.text = 'referenced by App.get_running_app() %s' %self.text

        if self.text == 'btn4':
            self.parent.parent.parent.ids.statlabel.text = 'refereced by kivy tree %s' %self.text

  
class MainInterface(ScreenManager):
   
    def callback_btn_insideroot(self,btn):
        if btn.text == 'btn1':              
            self.ids.statlabel.text = 'self referecenced by callback %s' %btn.text
       
      

class ExampleApp(App):
    def build(self):
        global main           
        main = MainInterface()   # global main reference
        return main

if __name__ == '__main__':
    ExampleApp().run()
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.

Tyro

unread,
Jul 8, 2017, 10:47:05 PM7/8/17
to Kivy users support
I will study your tutorial carefully.  In the meantime, can I ask you what you meant to write here:  "print untersuche(self)"
main = None

class ButtonOutsideRoot(Button):
    global main


    def callback_btn_outsideroot(self):                                                     # no args given so the reference to the root interface(ScreenManager) has has to be found otherwise
        if self.text == 'btn2':                                                                      # self refers to the calling class automatically
            main.ids.statlabel.text = 'referenced by global main %s' %self.text            
           
        elif self.text == 'btn3':            
            App.get_running_app().root.ids.statlabel.text = 'referenced by App.get_running_app() %s' %self.text

        if self.text == 'btn4':
            self.parent.parent.parent.ids.statlabel.text = 'refereced by kivy tree %s' %self.text

  
class MainInterface(ScreenManager):
   
    def callback_btn_insideroot(self,btn):
        if btn.text == 'btn1':              
            self.ids.statlabel.text = 'self referecenced by callback %s' %btn.text
            print untersuche(self)

Monty Mole

unread,
Jul 9, 2017, 4:20:15 AM7/9/17
to Kivy users support
My mistake. just delete that, that was a call to examine the class. You dont need it. Take a look how 'global main' is used. Thats where you keep a global reference across class boundaries.

Tyro

unread,
Jul 9, 2017, 11:50:34 PM7/9/17
to Kivy users support
I will, and I'll report back on my success or...non-success.  Thanks much.

ZenCODE

unread,
Jul 10, 2017, 2:01:13 AM7/10/17
to Kivy users support
Try this?


from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager

Builder.load_string('''
<MainInterface>:
    Screen:
        BoxLayout:
            orientation: 'vertical'

            Label:
                id: statlabel
                text: ''
           
            Button:
                text: 'btn1'
                on_release: root.callback_btn_insideroot(self)
           
            ButtonOutsideRoot:
                text: 'btn2'
                on_release: root.callback_btn_insideroot(self)   
           
            ButtonOutsideRoot:
                text: 'btn3'
                on_release: root.callback_btn_insideroot(self)
               
            ButtonOutsideRoot:
                text: 'btn4'
                on_release: root.callback_btn_insideroot(self)
               
''')


class ButtonOutsideRoot(Button):
    pass


class MainInterface(ScreenManager):
   
    def callback_btn_insideroot(self, btn):
        self.ids.statlabel.text = 'Screen = {0}, button = {1}'.format(self, btn)

      

class ExampleApp(App):
    def build(self):

Monty Mole

unread,
Jul 11, 2017, 12:16:56 PM7/11/17
to Kivy users support
also 'global main' has only to be in the (App) class oncle. I corrected the example (again .. )

Tyro

unread,
Jul 11, 2017, 11:55:11 PM7/11/17
to kivy-...@googlegroups.com

I’ve made good progress, thanks to numerous helpful explanations--I (believe I) can now access any widget from any class.

I have a new problem, though.  The code below is a minimal version of my program.  I can get either of its two screens to display by setting current screen in the app build.  And if I start with SecondScreen, I can switch to FirstScreen.  However, I can’t go the other direction—If I start with FirstScreen and its button is clicked, only a black screen appears.  What do I have wrong here?  (FirstScreen has to be constructed within the Python code rather than in the kv file, because in the actual program I load the button texts from a file.) 


Another, less serious problem:  If I start with FirstScreen, the other one (SecondScreen) appears for a fraction of a second before FIrstScreen is displayed.  Maybe related to the first problem?  Thanks for any edification.


import kivy

kivy.require('1.9.0')
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button

from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.lang import Builder

Builder.load_string('''
<CustButton@Button>:
font_size: 40
color: 0, 0, 0, 1
background_normal: ''
background_color: .3, .3, 1,1
    height: "130dp"
size_hint_y: None

<MainInterface>:
padding: 50
switch_button: switch_button
exit_button: exit_button
SecondScreen:
BoxLayout:
spacing: 20
CustButton:
id: switch_button
text: "Switch to FirstScreen"
on_release: root.current = "first"
CustButton:
id: exit_button
background_color: [.71, .086, .075,1]
text: "Exit"
on_release: app.stop()
''')


class MainInterface(ScreenManager):
pass

class SecondScreen(Screen):
pass

class FirstScreen(Screen):
def __init__(self, **kwargs):
super(FirstScreen, self).__init__(**kwargs)
players_list = ['alpha', 'beta', 'gamma']

b = BoxLayout(orientation='vertical', padding=500)
        self.txt = TextInput(id='my_text', height='60dp', cursor_width="2sp")
dropdown = DropDown()
for index in range(len(players_list)):
btn = Button(text=players_list[index], size_hint_y=None, height=44)
btn.bind(on_release=lambda btn: dropdown.select(btn.text))
dropdown.add_widget(btn)

mainbutton = Button(text='Names')
mainbutton.bind(on_release=dropdown.open)
        dropdown.bind(on_select=lambda instance, x: setattr(self.txt, 'text', x))
b.add_widget(self.txt)
b.add_widget(mainbutton)
l=Label(text = '')
b.add_widget(l)
j= Button(text ="switch to SecondScreen")
j.bind(on_release=self.fetch_name)
b.add_widget(j)
self.add_widget(b)

def fetch_name(self,obj):
app_ref = App.get_running_app()
app_ref.root.ids.switch_button.text = self.txt.text
main.current = 'second'

class MinimalApp(App):

def build(self):
global main
main = MainInterface()
        main.add_widget(FirstScreen(name='first'))
main.add_widget(SecondScreen(name='second'))
main.current = 'first'
        return main

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



Monty Mole

unread,
Jul 12, 2017, 9:20:20 AM7/12/17
to Kivy users support
Hi Tyro,
because I couldnt figure out what exactly was wrong with your code and to see how it would look like, I reworked the code and it mostly works. This is what I did:

- tried to put most stuff in kv
- tried to move all calls to the main layout class so everything is in one place in main (global)
- put the dynamic dropdown in __init__ of the main layout. (so everything is in one place...)
- put ??? behind lines with possible errors

When I started with kivy I did it exactly like you (doing most gui stuff in code) until I realised that it just gets very complicated when your code grows and even worse when you have to rework the complete layout.
I have an app that has a different layouts with different screens when started on tablet or phone and found it unpossible to do everything dynamicall in python code. Now I just load two different kv files.
And as you can see, you can always add additional widget with add_widget later.

hope this helps

# ---------------------------------------------------------------------------------------------------------------------------------------


import kivy
kivy.require('1.9.0')

from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget

Builder.load_string('''
<MainInterface>:
    padding: 50
    #switch_button: switch_button              # ???
    #exit_button: exit_button

    Screen:
        id: firstscreen
        name:'first'
       
        BoxLayout:
            orientation:'vertical'
            padding: 50                       # ??? instead of 500 thats why you got a black screenn
           
            TextInput:
                id: txt
                height:'60dp'
                cursor_width:"2sp"
           
            Button:
                text: 'Names'
                on_release: root.dropdown.open(root.ids.txt)

            Label:
                text: 'Label here'

            Button:
                text: "switch to SecondScreen"
                on_release: root.fetch_name()


    Screen:
        name: 'second'
        id: secondscreen

       
        BoxLayout:
            spacing: 20
           
            CustButton:
                id: switch_button
                text: 'Switch to FirstScreen'
                on_release: root.current = 'first'
                size_hint_y: .1


            CustButton:
                id: exit_button
                background_color: .71, .086, .075,1
                text: "Exit"
                on_release: app.stop()
                size_hint_y: .1

               
               
<CustButton@Button>:
    font_size: 40
    color: 0, 0, 0, 1
    background_normal: ''
    background_color: .3, .3, 1,1
    height: "130dp"
    size_hint_y: None               
''')

class MainInterface(ScreenManager):

    def __init__(self, **kwargs):
        super(MainInterface, self).__init__(**kwargs)
       
        self.current='first'

       
        players_list = ['alpha', 'beta', 'gamma']
       
        self.dropdown = DropDown()

       
        for index in range(len(players_list)):
            btn = Button(text=players_list[index], size_hint_y=None, height=44)
            btn.bind(on_release = lambda btn: self.dropdown.select(btn.text))
            self.dropdown.add_widget(btn)   
        self.dropdown.bind(on_select=lambda instance, x: setattr(main.ids.txt, 'text', x))   # somethin was wrong here and broke the screenmanager
       

    def fetch_name(self):
        pass
        #app_ref = App.get_running_app()                        #you dont need this becaus you already have it in the global main var
        #app_ref.root.ids.switch_button.text = self.txt.text
        main.ids.switch_button.text = main.ids.txt.text

        main.current = 'second'

class MinimalApp(App):
    def build(self):
        global main
        main = MainInterface()

Tyro

unread,
Jul 12, 2017, 11:01:14 PM7/12/17
to Kivy users support
This works beautifully.  Next I'll see if I can incorporate this design into my real (i.e., full) program.
It wasn't that I didn't want to put all of the GUI stuff in the kv file; it's just that I didn't know how to put part of the first screen's design in the kv file and then do the loading of the buttons from a data file in my Python code.  I'm obliged to you for showing me that could be done and for fixing the rest of my logic.  I hope that I can report back soon that the full program works.  Thanks again.

Tyro

unread,
Jul 13, 2017, 7:32:58 PM7/13/17
to Kivy users support
And it does!  Awesome.  Thank you so much for your help.
Reply all
Reply to author
Forward
0 new messages