Why are widgets not created at __init__ function? - python

I'm new to kivy so I'm trying to do an App, useful to me, to learn this framework.
My need is to manage my shift work schedule so I started inserting calendar days into a GridLayout. After several troubles now I'm blocked because I've got this error:
File "turno.py", line 53, in load_content
self.gridMonthView.add_widget(DaysInfo(day=wid))
AttributeError: 'NoneType' object has no attribute 'add_widget'
The load_content is called from inside the __init__ function, the error happens when I try to add DaysInfo widget (one for each day of the current month).
The strange (for me) behaviour is that if I comment line n.47 (that calls the load_content method) the program goes running and then I'm able to use the load_content from inside functions prevMonth and nextMonth without having any error.
So the question is:
Why does it seem that inside the __init__ method I can't use add_widget for gridMonthView reference/object but it's possible from the other methods of the same class?
Maybe that is not possible to add widget before the __init__ function ends something that I don't know/understand?
So, here's the code:
A little module to handle dates: calendario.py
from calendar import Calendar
from datetime import date
IT_months = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile',\
'Maggio', 'Giugno', 'Luglio', 'Agosto',\
'Settembre', 'Ottobre', 'Novembre', 'Dicembre']
IT_days = ['Lunedi',
'Martedi',
'Mercoledi',
'Giovedi',
'Venerdi',
'Sabato',
'Domenica']
class Calendario():
def __init__ (self):
self.currDay = date.today()
self.calen = Calendar(0)
# def __init__ (self):
def getMonth(self):
return IT_months[self.currDay.month-1]
# def getMonth(self):
def getYear(self):
return str(self.currDay.year)
# def getYear(self):
def getDaysOfMonth(self, listOfDays):
listOfDays = []
for td in self.calen.itermonthdates(self.currDay.year, self.currDay.month):
listOfDays.append(td.day)
# for td in ...
return listOfDays
# def getDaysOfMonth(self, listOfDays):
def setNextMonth(self):
if self.currDay.month == 12:
self.currDay = self.currDay.replace(month=1, year=(self.currDay.year+1))
else:
self.currDay = self.currDay.replace(month=(self.currDay.month+1))
# def setNextMonth(self):
def setPrevMonth(self):
if self.currDay.month == 1:
self.currDay = self.currDay.replace(month=12, year=(self.currDay.year-1))
else:
self.currDay = self.currDay.replace(month=(self.currDay.month-1))
# def setNextMonth(self):
the .kv file: turno.kv
#:kivy 1.9.0
#
# menu bar
#
<MenuBar>:
orientation: 'horizontal'
padding: 1
spacing: 1
size_hint_y: 0.15
Button:
text: "Icon"
size_hint_x: 0.3
on_press: root.menu()
Button:
text: "Title"
#size_hint: 1, 0.5
#
# day's info
#
<DaysInfo>:
orientation: 'vertical'
padding: 1
spacing: 1
Label:
color: 1,0,0,1
background_color: 1,1,1,1
id: f1
text: " "
Label:
id: f2
text: " "
Label:
id: f3
text: " "
#
# month view
#
<MonthView>:
gridMonthView: gridMonthView
#
orientation: "vertical"
#
# month selection
#
BoxLayout:
id: box1
orientation: 'horizontal'
padding: 1
spacing: 1
size_hint_y: 0.15
Button:
backgroud_color: 0,1,0,1
text: " << "
size_hint_x: 0.1
on_press: root.prevMonth()
Button:
id: idSelMonth
text: root.curMonth
size_hint_x: 0.5
Button:
id: isSelYear
text: root.curYear
size_hint_x: 0.3
Button:
text: " >> "
size_hint_x: 0.1
on_press: root.nextMonth()
#
# week's days
#
BoxLayout:
id: box2
orientation: 'horizontal'
padding: 1
spacing: 1
color: 0., 0.5, 0.5, 1
size_hint_y: 0.1
Label:
text: "Lu"
Label:
text: "Ma"
Label:
text: "Me"
Label:
text: "Gi"
Label:
text: "Ve"
Label:
text: "Sa"
Label:
text: "Do"
#
# Month's days
#
GridLayout:
id: gridMonthView
cols: 7
rows: 6
padding: 1
spacing: 1
size_hint_y: 0.6
#
# Turno Main Form
#
<TurnoMainForm>:
orientation: 'vertical'
padding: 20
spacing: 10
#width: 400
#height: 800
# menu bar
MenuBar:
# month view
MonthView:
id: id1
the app's source code: turno.py
# -*- coding: utf-8 -*-
"""
Created on Mon Oct 06 12:25:04 2015
#author: a809077
"""
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.properties import ObjectProperty, StringProperty
from kivy.config import Config
from calendario import Calendario
# impostazione della grandezza della finestra
Config.set('graphics', 'width', '480')
Config.set('graphics', 'height', '800')
class MenuBar(BoxLayout):
def __init__(self, **kwargs):
super(MenuBar, self).__init__(**kwargs)
print ("------- MenuBar -------")
for child in self.children:
print(child)
def menu (self):
print (" ====== click sul menu ======")
# end of class: MenuBar(BoxLayout)
##
## visualizza i dati mensili
##
class MonthView(BoxLayout):
gridMonthView = ObjectProperty(None)
curMonth = StringProperty()
curYear = StringProperty()
def __init__(self, **kwargs):
print ("------- Month View -------")
self.curMonth = TurnoApp.currCal.getMonth()
self.curYear = TurnoApp.currCal.getYear()
super(MonthView, self).__init__(**kwargs)
self.load_content(True)
self.printInfo()
def load_content(self, clearWidget = False):
if (clearWidget):
print "---- Clear Widgets ----"
self.gridMonthView.clear_widgets()
lod = list ()
lod = TurnoApp.currCal.getDaysOfMonth(lod)
for wid in lod:
self.gridMonthView.add_widget(DaysInfo(day=wid))
def prevMonth (self):
print (" ====== click sul mese precedente 1 ======")
TurnoApp.currCal.setPrevMonth()
self.curMonth = TurnoApp.currCal.getMonth()
self.curYear = TurnoApp.currCal.getYear()
#self.printInfo()
self.load_content(True)
def nextMonth (self):
print (" ====== click sul mese successivo ======")
TurnoApp.currCal.setNextMonth()
self.curMonth = TurnoApp.currCal.getMonth()
self.curYear = TurnoApp.currCal.getYear()
#self.printInfo()
self.load_content(True)
def printInfo (self):
print " ____ items ____"
for key, val in self.ids.items():
print("key={0}, val={1}".format(key, val))
print " ____ childs ____"
for child in self.children:
print("{} -> {}".format(child, child.id))
print " ____ walk ____"
for obj in self.walk():
print obj
# end of class: MonthView(GridLayout):
class DaysInfo(BoxLayout):
def __init__(self, **kwargs):
super(DaysInfo, self).__init__()
#print ("-- Days Info - {:d} ------".format(kwargs["day"]))
self.ids["f1"].text =str(kwargs["day"])
# end of class: DaysInfo(BoxLayout):
class TurnoMainForm(BoxLayout):
def __init__(self, **kwargs):
super(TurnoMainForm, self).__init__(**kwargs)
print ("-------TurnoMainForm-------")
for child in self.children:
print(child)
# end of class: TurnoMainForm(BoxLayout):
class TurnoApp (App):
# icon = 'mia_icona.png'
title = 'Turno Terna'
currCal = Calendario()
def build (self):
return TurnoMainForm()
#return MonthView()
# end of class: TurnoApp (App):
TurnoApp().run()
I don't try to reduce the code to an example, instead I post everything because it may be better to understand where the problem is and to give me some tip to improve the code.

Why does it seem that inside the init method I can't use add_widget for gridMonthView reference/object but it's possible from the other methods of the same class?
Because self.gridMonthView has not yet been set yet (i.e. modified from the default value of None) within the __init__. This is a technical limitation - imagine two widgets with kv rules that reference one another, in this case at least one of them can't reference the other in its __init__ because it will be the first one to actually have been instantiated. This is the kind of effect you see here, the widgets must all be instantiated before references between them can be set.
It works in other methods because you call them only later, after all the instantiation is complete.
You can use something like Clock.schedule_once(self.post_init, 0) and put your widget addition in the post_init method; scheduling with an interval of 0 makes sure it will happen before the next frame, but after everything currently in progress has completed.

Related

KivyMD MDTimePicker is not returning time, MDTimePicker returns None

I'm trying to make a program where the program makes the user pick a due date and time for their tasks. User presses a button to open a dialog that takes task name, description and due date-time from them. Everything is fine, except I couldn't make the get_time() function do anything other than return time. And it returns None instead of the picked time. I made a seperate test app just to test if the picker works and it does. It can change the text of a label too, not just return time.
main.py
from kivymd.app import MDApp
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivymd.uix.dialog import MDDialog
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.picker import MDDatePicker, MDTimePicker
from datetime import datetime
from kivymd.uix.list import TwoLineListItem
class MainApp(MDApp):
task_list_dialog = None
def build(self):
return Builder.load_file('main.kv')
def show_task_dialog(self):
if not self.task_list_dialog:
self.task_list_dialog = MDDialog(
title="Create Task",
type="custom",
content_cls=DialogContent(),
)
self.task_list_dialog.open()
def close_dialog(self, *args):
self.task_list_dialog.dismiss()
def add_task(self, task, description, task_date):
self.root.get_screen("testwin").ids['container'].add_widget(ListItem(text="[size=18]"+task.text+"[/size]", secondary_text=task_date))
print(task.text+"\n", description.text+"\n", task_date)
task.text = '' # set the dialog entry to an empty string(clear the text entry)
description.text = ''
class DialogContent(MDBoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.ids.date_text.text = str(datetime.now().strftime('%A %d %B %Y, %H:%M'))
def show_date_picker(self):
date_dialog = MDDatePicker()
date_dialog.bind(on_save=self.on_save)
date_dialog.open()
def on_save(self, instance, value, date_range):
date = value.strftime('%A %d %B %Y')
time = self.show_time_picker()
self.ids.date_text.text = date+str(time)
def show_time_picker(self):
time_dialog = MDTimePicker()
time_dialog.bind(time=self.get_time)
time_dialog.open()
def get_time(self, instance, time):
return time
class ListItem(TwoLineListItem):
def __init__(self, pk=None, **kwargs):
super().__init__(**kwargs)
self.pk = pk
class WindowManager(ScreenManager):
pass
class TestWindow(Screen):
pass
if __name__ == "__main__":
MainApp().run()
main.kv
#: include testwindow.kv
WindowManager:
TestWindow:
testwindow.kv
<TestWindow>:
name: "testwin"
Screen:
BoxLayout:
orientation: "vertical"
BoxLayout:
orientation: 'vertical'
ScrollView:
do_scroll_x: False
do_scroll_y: True
pos_hint: {"center_x": 0.5, "center_y": 0.5}
size_hint: 1, 0.7
MDList:
id: container
MDFloatingActionButton:
icon: 'plus-thick'
on_release: app.show_task_dialog()
elevation_normal: 10
pos_hint: {'x': 0.82, 'y': 0.07}
<DialogContent>:
orientation: "vertical"
spacing: "10dp"
size_hint: 1, None
height: "420dp"
BoxLayout:
orientation: 'vertical'
MDTextField:
id: task_text
hint_text: "Task"
on_text_validate: (app.add_task(task_text, task_desc, date_text.text), app.close_dialog())
MDTextField:
id: task_desc
mode: "fill"
multiline: True
hint_text: "Task Description"
size_hint_y: 0.1
BoxLayout:
orientation: 'horizontal'
size_hint_y: 0.1
MDIconButton:
icon: 'calendar'
on_release: root.show_date_picker()
padding: '10dp'
MDLabel:
spacing: '10dp'
font_size: 15
id: date_text
BoxLayout:
orientation: 'horizontal'
size_hint_y: 0.1
MDRaisedButton:
id: savetask
text: "SAVE"
on_release: (app.add_task(task_text, task_desc, date_text.text), app.close_dialog())
MDFlatButton:
id: canceltask
text: 'CANCEL'
on_release: app.close_dialog()
<ListItem>:
id: the_list_item
markup: True
multiline: True
The problem is that in method on_save you assigned the returned value of the method show_time_picker (which is clearly None) to a var. time. That's why you never get the time value (instead of that None).
One of many ways to achieve that is as follows:
First create class attributes to store the desired values. This way you will be able to separate attrs. from methods and access those attrs. anywhere in your code.
class DialogContent(MDBoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Create new attributes to save desired data later.
self._time = None
self._data = None
self.ids.date_text.text = str(datetime.now().strftime('%A %d %B %Y, %H:%M'))
def on_save(self, instance, value, date_range):
# Assign the date value hare.
self._date = value.strftime('%A %d %B %Y')
# Open time picker.
self.show_time_picker()
def show_time_picker(self):
time_dialog = MDTimePicker()
# time_dialog.bind(time=self.get_time)
time_dialog.bind(on_save=self.get_time)
time_dialog.open()
# Here's a conventional trick for checking what 'args' or 'kwargs' are used by some method.
# For that, define that function with variable no. of args and kwargs and print those.
# def get_time(self, *args, **kwargs):
# print(args, kwargs)
# As you see that should be two args, 'MDTimePicker' object and the 'time' value but with no kwargs.
# So you can write it as,
def get_time(self, instance, time):
# Assign the time value here.
self._time = time
# Now set date and time.
self.ids.date_text.text = self._date+str(self._time)
Note:
Since in method show_time_picker you bind a kivy property time (of MDTimePicker) to a method (get_time), so this method (get_time) will only be called whenever the value (time) changes. In simple words, say, the user just opens and then closes the time-picker without modifying the time, then as the value has not changed, the default value (None) will be used.
That's why it is safe to use (by forcing the user to use ok button) another default method on_save.

Enabling/disabling buttons onclick through kv file

I wrote a simple code to create a timer, and for this I added two buttons: Start and Stop. The idea is that when one of the buttons is enabled the other isn't, and viceversa.
The code below works, but I want to get the same behaviour using the kv file.
def update_time(self, instance):
timer = Clock.schedule_interval(self.timer, 1.0 / 1.0)
instance.disabled = True
self.stop_button.disabled = False
def stop_updating(self, instance):
Clock.unschedule(self.timer)
instance.disabled = True
self.timer_button.disabled = False
If I try to use the code snippet below, the script throws an error because it is lacking one argument (instance).
Button:
text: 'Start'
on_press: root.update_time()
I know the "disabled" property in kv file exists, but I don't know how (or whether) to use it to modify the two buttons I want to be modified, onclick.
Any idea how to go about it?
All script code
class LandingScreen(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def go_to_timer(self):
clock_app.screen_manager.current = 'Timer'
class TimerScreen(GridLayout):
current_time = StringProperty()
timer_start = ObjectProperty(Button)
timer_stop = ObjectProperty(Button)
tot_sec = 0
def __init__(self, **kwargs): # Widgets without .kv file
super().__init__(**kwargs)
self.rows = 4
self.current_time = '00:00'
def update_time(self, instance):
timer = Clock.schedule_interval(self.timer, 1.0 / 1.0)
instance.disabled = True
# self.stop_button.disabled = False
self.timer_stop.disabled = False
def stop_updating(self, instance):
Clock.unschedule(self.timer)
instance.disabled = True
# self.timer_button.disabled = False
self.timer_start.disabled = False
def timer(self, instance):
# Code
class ClockApp(App):
def build(self):
self.screen_manager = ScreenManager()
# Landing screen
self.landing_screen = LandingScreen()
screen = Screen(name='Landing')
screen.add_widget(self.landing_screen)
self.screen_manager.add_widget(screen)
# Timer screen
self.timer_screen = TimerScreen()
screen = Screen(name='Timer')
screen.add_widget(self.timer_screen)
self.screen_manager.add_widget(screen)
return self.screen_manager
def go_to_landing(self):
clock_app.screen_manager.current = 'Landing'
if __name__ == '__main__':
clock_app = ClockApp()
clock_app.run()
kv file
<TimerScreen>:
rows: 5
Label:
font_size: 60
text: root.current_time
GridLayout:
cols: 3
Label:
text: ''
Button:
id: 'timer_start'
text: 'Start'
on_press: root.update_time(self)
Label:
text: ''
Label:
text: ''
Button:
id: 'timer_stop'
text: 'Stop'
on_press: root.stop_updating(self)
disabled: True
Label:
text: ''
Label:
text: ''
Label:
text: ''
Label:
text: ''
AnchorLayout:
anchor_x: 'left'
Button:
text: 'Back'
width: root.width / 7
size_hint_x: None
on_press: app.go_to_landing()
<LandingScreen>:
rows: 7
Label:
font_size: 50
text: 'Clock types'
canvas.before:
Color:
rgba: 1, 0, 0, 1
GridLayout:
cols:3
Label:
text: ''
Button:
text: 'Timer'
on_press: root.go_to_timer()
Label:
text: ''
Label:
text: ''
Button:
text: 'Countdown'
on_press: root.go_to_countdown()
Label:
text: ''
Label:
text: ''
Label:
text: ''
I think your code is mostly working. Just a few notes on the use of the ids:
You should not use a string literal as an id. Instead of id: 'timer_start', use id: timer_start. If you use a string literal, you will encounter problems if you try to reference it elsewhere in the kv (it will be interpreted as a string, not an id).
When declaring an ObjectProperty in a class that will be defined by an id in the kv, use ObjectProperty(None) in the declaration in the class.
Use declarations in the kv to match up with the declarations in the class.
Applying the above to your python code:
class TimerScreen(GridLayout):
current_time = StringProperty()
timer_start = ObjectProperty(None)
timer_stop = ObjectProperty(None)
tot_sec = 0
and in the corresponding kv:
<TimerScreen>:
timer_start: timer_start
timer_stop: timer_stop
rows: 5
and the ids should be changed to:
Button:
id: timer_start
text: 'Start'
on_press: root.update_time(self)
and:
Button:
id: timer_stop
text: 'Stop'
on_press: root.stop_updating(self)
disabled: True
The typical declaration of an ObjectProperty in kv like:
timer_start: timer_start
can be confusing, but that is how it is usually done. I prefer to use a different name for the ObjectProperty, to make it clearer. The first name in the above is the name of the ObjectProperty, and the second is the id. They just happen to have the same name.

Passing Kivy Widget to another class

I am building a dynamic user interface using Python and Kivy. Because I need to add and remove widgets dynamically I want to use a separate class to handle adding and removing widgets from a GridLayout. I called this class LayoutManager.
The GridLayout is defined in my kv-File (id: "config_box_layout"). Inside my root widget class in my python code I am referencing the GridLayout via id. This is working properly. This reference is passed to the LayoutManager in the constructor. I tried passing it via ObjectProperty or GridLayout.
The problem is that I always end up with this kind of error if I try to remove widgets from the Layout:
'kivy.properties.ObjectProperty' object has no attribute 'remove_widget'
If I try to remove a widget in the save-method inside my Tab class using config_box_layout.remove_widget(some newly created Label) it's working properly.
I think the problem is that Kivy and all the kv-widgets are weakly-referenced and handling those references over to other classes seem to be not the intended use case.
I try to seperate classes to avoid doing all the coding stuff in one big fat main Layout class.
Looking forward to any help! :)
main.py
import kivy
from util.layoutManager import LayoutManager
from kivy.app import App
kivy.require('1.11.1')
from kivy.uix.label import Label
from kivy.uix.tabbedpanel import TabbedPanel
from kivy.properties import ObjectProperty
from kivy.uix.popup import Popup
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from functools import partial
class ChooseDirectoryDialog(FloatLayout):
add = ObjectProperty(None)
cancel = ObjectProperty(None)
save = ObjectProperty(None)
class Tab(TabbedPanel):
config_add_src = ObjectProperty()
ingest_layout = ObjectProperty()
configManager = ConfigManager()
config_box_layout = ObjectProperty()
layoutManager = LayoutManager(config_box_layout)
def add_src(self):
content = ChooseDirectoryDialog(cancel=self.cancel, save=self.save)
self._popup = Popup(title="Add Source", content=content,
size_hint=(0.9, 0.9))
self._popup.open()
def cancel(self):
self._popup.dismiss()
def delete_source(self, description, widget):
self.configManager.delete_source(description)
self.remove_source_from_ui(description)
def remove_source_from_ui(self, description):
self.layoutManager.remove_configuration(description)
def save(self, srcpath, destpath, description):
desc_label = Label(text=description)
desc_label.size_hint_y = None
desc_label.height = 30
self.config_box_layout.add_widget(desc_label)
srcpath_label = Label(text=srcpath)
srcpath_label.size_hint_y = None
srcpath_label.height = 30
self.config_box_layout.add_widget(srcpath_label)
destpath_label = Label(text=destpath)
destpath_label.size_hint_y = None
destpath_label.height = 30
self.config_box_layout.add_widget(destpath_label)
deleteButton = Button(text="Quelle löschen")
deleteButton.size_hint_y = None
deleteButton.height = 30
deleteButton.bind(on_press=partial(self.delete_source, description))
self.config_box_layout.add_widget(deleteButton)
self.layoutManager.add_configuration(description,
desc_label, srcpath_label, destpath_label, deleteButton)
self.configManager.add_source(description, srcpath, destpath)
self._popup.dismiss()
def copyToDestination(self, srcpath, destpath):
pass
class AutoIngest(App):
def build(self):
return Tab()
if __name__ == '__main__':
#Builder.load_file('autoingest.kv')
AutoIngest().run()
autoingest.kv
#:kivy 1.11.1
<Tab>:
do_default_tab: False
config_add_button: add_button
config_box_layout: config_box
TabbedPanelItem:
text: 'Konfiguration'
TabbedPanel:
tab_width: 200
do_default_tab: False
TabbedPanelItem:
text: 'Quellen verwalten'
StackLayout:
orientation: "lr-tb"
padding: 10
Button:
size_hint: .2, .1
id: add_button
text: 'Quelle hinzufügen'
on_press: root.add_src()
GridLayout:
id: config_box
cols: 4
Label:
size_hint_y: None
height: 30
text: "Bezeichnung"
Label:
size_hint_y: None
height: 30
text: "Quell-Pfad"
Label:
size_hint_y: None
height: 30
text: "Ziel-Pfad"
Label:
size_hint_y: None
height: 30
text: "Aktionen"
<ChooseDirectoryDialog>:
text_input: text_input
BoxLayout:
size: root.size
pos: root.pos
orientation: "vertical"
Label:
size_hint_y: None
height: 30
text: "Bezeichnung"
TextInput:
id: text_input
size_hint_y: None
height: 30
multiline: False
Label:
size_hint_y: None
height: 30
text: "Quellverzeichnis auswählen"
FileChooserListView:
id: source_chooser
Label:
size_hint_y: None
height: 30
text: "Zielverzeichnis auswählen"
FileChooserListView:
id: destination_chooser
BoxLayout:
size_hint_y: None
height: 30
Button:
text: "Cancel"
on_release: root.cancel()
Button:
text: "Add"
on_release: root.save(source_chooser.path, destination_chooser.path, text_input.text)
layoutManager.py
from kivy.uix.gridlayout import GridLayout
from kivy.properties import ObjectProperty
class LayoutManager:
#I alrey tried to pass the GridLayout itself. This didn't work either.
def __init__(self, configlayout: ObjectProperty):
self.configurations = {}
self.configlayout = configlayout
def remove_configuration(self, description):
widgets = self.configurations.get(description)
for x in widgets:
self.configlayout.remove_widget(x)
def add_configuration(self, description, *widgets):
self.configurations[description] = {'widgets': widgets}
I believe the problem is that the Tab instance ids have not been setup when your lines:
config_box_layout = ObjectProperty()
layoutManager = LayoutManager(config_box_layout)
are executed, so config_box_layout is not yet set.
You can force the creation of the LayoutManager to delay until the ids are created using kivy.clock in the build() method of the App:
class AutoIngest(App):
def build(self):
Clock.schedule_once(self.setup_layoutManager)
return Tab()
def setup_layoutManager(self, dt):
self.root.layoutManager = LayoutManager(self.root.config_box_layout)
Also, the line:
layoutManager = LayoutManager(config_box_layout)
can be removed from the Tab class.
Once that is working you will discover issues in your remove_configuration() method of LayoutManager.

Kivy: updating displayed button text from Python code

Short version:
How can I alter button text displayed onscreen from the main.py file of a Kivy application?
Longer version:
I'm putting togther a multiple-choice game using Kivy.
I have the gameplay working as I want it to at the python level: when the user clicks on the correct answer button they are awarded points and the answers attached to the buttons are changed. Under the bonnet everything seems to be fine.
My problem is that I can't work out how to update the text on the buttons displayed onscreen from the main.py file. The game plays fine, but the text displayed on the buttons never alters. How do I fix my code to do this?
Below is a simplified version of what I'm tring to do:
My main.py file:
# -*- coding: utf-8 -*-
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen, NoTransition
from kivy.properties import ObjectProperty
from functools import partial
import random
vocabDict = {'latte': 'milk', 'mela': 'apple', 'melograno': 'pomegranate', 'fragola': 'strawberry', 'acqua': 'water'}
vocabList = ['acqua', 'latte', 'mela', 'melograno', 'fragola']
currentWord = 'acqua'
score = 0
class ScreenOne(Screen):
pass
class ScreenTwo(Screen):
def __init__(self,**kwargs):
super(ScreenTwo, self).__init__(**kwargs)
Screen.currentWord = self.getCurrentWord()
Screen.questionText = Screen.currentWord
Screen.vocabDict = self.getVocabDict()
Screen.currentScore = self.getCurrentScore()
Screen.possibleAnswerList = [Screen.currentWord]
self.playHand(Screen.currentWord,Screen.vocabDict,Screen.currentScore)
def getCurrentWord(self):
return currentWord
def getVocabDict(self):
return vocabDict
def getCurrentScore(self):
return score
def playHand(self,currentWord,vocabDict,score):
possibleAnswerList = [currentWord]
currentWord = currentWord
vocabDict = vocabDict
currentScore = score
while len(possibleAnswerList) < 3:
potentialChoice = random.choice(vocabDict.keys())
if potentialChoice not in possibleAnswerList:
possibleAnswerList.append(potentialChoice)
random.shuffle(possibleAnswerList)
# How do I visualize these changes on screen?
Screen.button1Text = vocabDict[possibleAnswerList[0]]
Screen.button2Text = vocabDict[possibleAnswerList[1]]
Screen.button3Text = vocabDict[possibleAnswerList[2]]
Screen.possibleAnswerList = possibleAnswerList
print "Screen.button1Text = " + Screen.button1Text
print "Screen.button2Text = " + Screen.button2Text
print "Screen.button3Text = " + Screen.button3Text
def button1Pressed(instance):
print "Screen.possibleAnswerList[0] = " + Screen.possibleAnswerList[0]
print "Screen.currentWord = " + Screen.currentWord
if Screen.possibleAnswerList[0] == Screen.currentWord:
print "Correct!"
Screen.currentScore += 1
print Screen.currentScore
instance.playHand(Screen.currentWord,Screen.vocabDict,Screen.currentScore)
else:
print "Incorrect!"
def button2Pressed(instance):
if Screen.possibleAnswerList[1] == Screen.currentWord:
print "Correct!"
Screen.currentScore += 1
print instance.currentScore
instance.playHand(Screen.currentWord,Screen.vocabDict,Screen.currentScore)
else:
print "Incorrect!"
def button3Pressed(instance):
if instance.possibleAnswerList[2] == currentWord:
print "Correct!"
instance.currentScore += 1
print instance.currentScore
instance.playHand(Screen.currentWord,Screen.vocabDict,Screen.currentScore)
else:
print "Incorrect!"
class Manager(ScreenManager):
screen_one = ObjectProperty(None)
screen_two = ObjectProperty(None)
class ScreensApp(App):
def build(self):
m = Manager(transition=NoTransition())
return m
if __name__ == "__main__":
ScreensApp().run()
My screens.kv file:
#:kivy 1.8.0
<ScreenOne>:
BoxLayout:
orientation: "vertical"
size: root.size
spacing: 20
padding: 20
Label:
text: "Main Menu"
Button:
text: "Button 1"
on_release: root.manager.current = "screen2"
<ScreenTwo>:
BoxLayout:
orientation: "vertical"
size: root.size
spacing: 20
padding: 20
Label:
id: label
text: root.questionText
Button:
id: button1
text: root.button1Text
on_release: root.button1Pressed()
Button:
id: button2
text: root.button2Text
on_release: root.button2Pressed()
Button:
id: button3
text: root.button3Text
on_release: root.button3Pressed()
<Manager>:
id: screen_manager
screen_one: screen_one
screen_two: screen_two
ScreenOne:
id: screen_one
name: "screen1"
manager: screen_manager
ScreenTwo:
id: screen_two
name: "screen2"
manager: screen_manager
As should be pretty evident, I'm a total beginner at Kivy, so I'd really appreciate it if you could show me exactly what I need to change, including the specific syntax that should be used.
Thanks in advance for your time and wisdom.
You need to make button1/2/3Text a StringProperty such as
from kivy.properties import StringProperty # <---- (:
class ScreenTwo(Screen):
button1Text = StringProperty ('some text')
button2Text = StringProperty ('some text2')
button3Text = StringProperty ('some text3')
def __init__(...):
...
#notice that Screen was replaced by "self"
# How do I visualize these changes on screen?
self.button1Text = vocabDict[possibleAnswerList[0]]
self.button2Text = vocabDict[possibleAnswerList[1]]
self.button3Text = vocabDict[possibleAnswerList[2]]
Please also replace Screen with self in all other places as well since you are putting variables on a kivy class and its weird ;)
I hope this will help you

kv Language id linked with another class Object as ObjectProperty kivy

I am trying to update a label text from another class using its update method in Clock but I couldn't understand why its not updating the label properly .I have a sample code below :
gui_v9 = '''
#:import Clock kivy.clock.Clock
<Level_1>:
on_enter: self.EnterLevel_1()
<ScoreBar>:
time_Label: timelabel
GridLayout:
rows: 4
cols: 1
size: root.size
#Space away from border
padding: 2
spacing: 10
canvas:
Color:
rgba: 204/255.0, 204/255.0, 0/255.0, 1
Rectangle:
# self here refers to the widget i.e FloatLayout
pos: self.pos
size: self.size
Button:
text: 'Score'
size_hint: .5, .5
Label:
text: "Level 1"
Label:
text: "Time :"
id: timelabel
Button:
text: 'Mute'
'''
class ScoreBar(Widget):
time_Label = ObjectProperty(None)
def __init__(self):
super(ScoreBar, self).__init__()
class Level_1(Screen,Widget):
def __init__(self, **kwargs):
super(Level_1, self).__init__(**kwargs)
self.layout = GridLayout(cols=2,spacing=(10),padding=10)
def EnterLevel_1(self):
print "Hi This is EnterLevel_1 . Level One Gui work area "
scoreBar = ScoreBar()
Field = tama(speed=3)
self.layout.add_widget(Field)
self.layout.add_widget(scoreBar)
self.add_widget(self.layout)
Clock.schedule_interval(Field.update, 10.0/100)
#Field
class tama(Widget):
def __init__(self, speed=1 ):
super(tama, self).__init__()
self.speed = speed
self.id = "Field"
self.size = (800,600)
self.Extra = 200
print ScoreBar().time_Label.text
def update(self,dt):
print ScoreBar().time_Label.text
ScoreBar().time_Label.text ="cdfdfd"
# Create the screen manager
Builder.load_string(gui_v9)
sm = ScreenManager()
sm.add_widget(Level_1(name='level_1'))
class MyJB(App):
def build(self):
return sm
if __name__ == '__main__':
MyJB().run()
The issue is that you have lines like
print ScoreBar().time_Label.text
This doesn't tell you anything about the existing ScoreBar, it makes a new one and returns information about that.
From the tama, you could refer to self.parent.children[1] to access the one you actually originally added, or devise another way to access the reference.

Categories

Resources