I am new to python and kivy and am learning how to pass information between kivy objects and python. I have the basic concepts down but this problem has me stumped.
I am writing an app to manage GPS waypoints that are classified into groups. The intent is for the user to select a waypoint group from a Spinner, which populates a list of waypoints in a RecycleView. The user then selects a waypoint from the RecycleView list. The selected waypoint is passed on for further processing. It's this last step (the passing) that is failing.
I developed this waypoint management function in its own development program and it works as intended. The problem came when I added the development code into a ScreenManager. This is a small part of a much larger project so I've stripped out all of the distractions in the code below and reorganized to make this easier to debug.
The app has several screens managed with ScreenManager. The waypoint selection screen presents a Spinner for choosing a waypoint group and a RecycleView (called RV()) for choosing a waypoint. The waypoint choice is processed in class RVItem(). The Spinner, RecycleView, and RVItem() work fine. The problem (in the ScreenManager version) occurs when I try to pass the chosen waypoint back to a label in the kivy code. The RVItem.on_release() event handler successfully captures the selected waypoint but I can't figure out how to send the selection back to the Label on the screen. My problem is in the RVItem.on_release() code. The Label's id in the .kv file is route_id. I left a list of some of my attempts to send the waypoint to route_id.text in the RVItem.on_release() code but I can't find anything that works. What am I missing?
I lastly tried accessing the Label using route_id = ObjectProperty(None)
in class Route(). I couldn't get that to work either but it doesn't effect how the program runs or crashes so I left the property in the code in case it is helpful.
To duplicate the problem: Copy the code into files main.py and ScreenManager.kv. Start the program and when the Main Menu opens click on the Route button. Click the Choose a group spinner, select a group off of the dropdown list, and then choose a waypoint from the RecycleView list. The program will crash at the end of the RVItem.on_release() code. The error will be KeyError: 'route_id'
and
AttributeError: 'super' object has no attribute '__getattr__'
I've spent hours trying to figure this out myself. If you can suggest a solution, please also let me know how I should have gone about debugging this myself.
I am running Python 3.8 and Kivy 2.0.
# main.py
# BoatInstruments.222
# Stripped down version to demonstrate the problem passing the
# RecycleView's response back to the kivy Label
from kivy.app import App
from kivy.uix.recycleview import RecycleView
from kivy.factory import Factory
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.lang import Builder
from kivy.properties import ObjectProperty
Builder.load_file('ScreenManager.kv')
class ScreenManagement(ScreenManager):
pass
class MainMenu(Screen):
pass
class RVItem(Factory.Button):
# Returns the waypoint selected by RecycleView()
def get_data_index(self):
return self.parent.get_view_index_at(self.center)
@property
def rv(self):
return self.parent.recycleview
def on_release(self):
app = App.get_running_app()
data_index = self.get_data_index()
current_waypoint = app.waypoints[data_index]
print("\r\ncurrent_waypoint = ", current_waypoint, "\r\n") # Successful to this point
# Write the route (current_waypoint for debugging) to kivy label Route.route_id # !!! FAIL !!!
# These are some of the things that I tried.
print("app.root is ", app.root)
app.root.ids.route_id.text = current_waypoint # This works in dev code (without ScreenManager) because there class WMApp(App) returns the root widget Route()
# root.ids.route_id.text = current_waypoint
# root.route_id.text = current_waypoint
# self.ids.route_id.text = current_waypoint
# self.parent.ids.route_id.text = current_waypoint
# scrRoute = app.root.ids.route_id.text
# root.ids.screen_manager.get_screen('route')
# scrRoute.ids.route_id.text = current_waypoint
# self.route_id.text = current_waypoint
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__()
self.data = [] # Initialize the list of Groups
class Route(Screen):
# Presents a list of waypoint groups in a spinner. After choosing, populates rv_id.data with that group's waypoints.
route_id = ObjectProperty(None)
def spinner_clicked(self, value): # value is the selected waypoint group
# Get the Group's list of waypoints and send them to RV
app = App.get_running_app()
self.ids.rv_id.data = [{'text': item} for item in app.waypoints]
def new_route(self):
print("Attempting Route.new_route()")
app = App.get_running_app()
app.wptroute = []
app.root.ids.route_id.text = "" # !!! FAIL !!!
def done_route(self):
print("Attempting Route.done_route()")
class BoatInstrumentsApp(App):
groups = ['CYC', 'MHYC', 'GRYC', 'CLYC', 'Cleveland'] # accessed in kivy via app.list_of_groups
waypoints = ['GRSC A', 'GRSC B', 'GRSC C', 'GRSC D', 'GRSC E', 'GRSC F']
wptroute = []
def __init__(self, **kwargs):
super().__init__(**kwargs)
def build(self):
return ScreenManagement()
if __name__ == '__main__':
BoatInstrumentsApp().run()
# ScreenManager.kv
<ScreenManagement>:
id: screen_manager
MainMenu:
id: mainmenu
name: 'mainmenu'
manager: 'screen_manager'
Route:
id: route
name: 'route'
manager: 'screen_manager'
# ##################################################################
<MainMenu>:
BoxLayout:
orientation: 'vertical'
padding: 120
spacing: 30
Label:
text: "Main Menu"
font_size: 60
Button:
text: "Route"
font_size: 40
on_release: app.root.current = 'route'
# ##################################################################
<Route>:
route_id: route_id # I added this property late. It may or may not be useful
BoxLayout:
orientation: 'horizontal'
padding: 5
spacing: 5
BoxLayout: # Left column: Groups and Waypoints
orientation: 'vertical'
Spinner: # Spinner: Waypoint Group
id: spinner_id
size_hint: (1, 0.15)
text: "Choose a group"
font_size: '40dp'
values: app.groups
on_text: root.spinner_clicked(spinner_id.text)
Label:
size_hint: (1, 0.04)
RV: # RecycleView: Waypoints
id: rv_id
viewclass: 'RVItem'
RecycleBoxLayout:
default_size: None, 30 # Set the RV child box height
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: 5
BoxLayout: # Right column: Label, list of waypoints, two buttons
id: box_id
orientation: 'vertical'
Label:
text: "Route"
size_hint: (1, 0.15)
font_size: '40dp'
# ########### HERE ###########################################
#Display the route (or current_waypoint for debugging)
Label: # This label will contain the waypoints of the route, line by line
id: route_id
text: "Route goes here"
RelativeLayout:
size_hint: 1, 0.24
Button: # Button: New Route
id: new_route_id
text: "New Route"
font_size: '40dp'
size_hint: 0.8, 0.48
pos_hint: {"x":0.1, "top":1}
on_release: root.new_route()
Button: # Button: Done
id: done_route_id
text: "Done"
font_size: '40dp'
size_hint: 0.8, 0.48
pos_hint: {"x":0.1, "bottom":1}
# on_release: root.done_route()
on_release: app.root.current = 'mainmenu'
Since you are using a ScreenManager
, you can use the get_screen()
method of ScreenManager
to access the Screen
that contains the route_id
Label
. Try replacing:
app.root.ids.route_id.text = current_waypoint
with:
app.root.get_screen('route').ids.route_id.text = current_waypoint