Making a slightly more efficient function structure - python

I have a bunch of lights I'm trying to control. Rather than have each button state change call a unique function I want to try and have a multipurpose function as thats what functions are for (as far as I understand).
Button calling function:
ToggleButton:
id: KitchenSpot1Toggle
text: "Kitchen Spot 1"
on_press: root.changeKS1(1)
The function:
def changeKS1(self,change):
if change==1 and b.get_light(1, 'on'):
self.KitchenSpot1(False)
else:
self.KitchenSpot1(True)
That function then calls this function to physically change the state of the light using a 3rd part library.
def KitchenSpot1(self,state):
lights[0].name
lights[0].on = state
The reason I passed "1" inside of the function is because it didn't like having nothing passed in it (I don't know why it didn't like it). If you hadn't already guessed it, I am new at this. I have a bit of a cpp micro controller background but I'm trying to get my head around python and PC based programming. I'm looking for a bit of advice on how best I can condense this and make it as efficient as possible. I may not know much about python, but, I know I shouldn't be typing practically the same thing out 30 times.
Thanks in advance to anyone that can share some of their wisdom.
Its with noting I am using kivy with python to generate the button.
Full main.py code:
from kivy.properties import StringProperty
import kivy
from kivy.uix.togglebutton import ToggleButton
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.app import App
kivy.require('1.10.0')
from phue import Bridge
import nest
b = Bridge('xx.xxx.xxx.xxx')
b.connect()
b.get_api()
lights = b.lights
class Controller(GridLayout):
state = StringProperty('down')
def __init__(self, **kwargs):
super(Controller, self).__init__(**kwargs)
Clock.schedule_interval(self.update, 1.0 / 60.0)
def KitchenSpot1(self,state):
lights[0].name
lights[0].on = state
def changeKS1(self,change):
if change==1 and b.get_light(1, 'on'):
self.KitchenSpot1(False)
else:
self.KitchenSpot1(True)
def KitchenSpot2(self,state):
lights[1].name
lights[1].on = state
def KitchenSpot3(self,state):
lights[2].name
lights[2].on = state
def OfficeSpot1(self,state):
lights[3].name
lights[3].on = state
def OfficeSpot2(self,state):
lights[4].name
lights[4].on = state
def OfficeSpot3(self,state):
lights[5].name
lights[5].on = state
def OfficeSpot4(self,state):
lights[6].name
lights[6].on = state
def JuliaBedside(self,state):
lights[7].name
lights[7].on = state
def JohnBedside(self,state):
lights[8].name
lights[8].on = state
def update(self, dt):
if b.get_light(1, 'on'):
self.state = 'down'
else:
self.state = 'normal'
class ActionApp(App):
def build(self):
return Controller()
if __name__ == "__main__":
myApp = ActionApp()
myApp.run()
Full action.kv code
<Controller>:
cols: 4
rows: 3
spacing: 10
state: "normal"
ToggleButton:
id: KitchenSpot1Toggle
text: "Kitchen Spot 1"
on_press: root.changeKS1(1)
#on_release: root.KitchenSpot1(False)
#state1 = app.update.h
state: root.state
ToggleButton:
text: "Kitchen Spot 2"
Button:
text: "Kitchen Spot 3"
Button:
text: "Kitchen Spot 4"
Button:
text: "Office Spot 1"
Button:
text: "Office Spot 2"
Button:
text: "Office Spot 3"
Button:
text: "Office Spot 4"
Update:
Python program:
def lightcontrol(self,lightnumber):
if b.get_light(1, 'on'):
lights[lightnumber].name
lights[lightnumber].on (False)
#self.KitchenSpot1(False)
else:
lights[lightnumber].name
lights[lightnumber].on (True)
#self.KitchenSpot1(True)
Kivy button:
ToggleButton:
id: KitchenSpot1Toggle
text: "Kitchen Spot 1"
on_press: root.lightcontrol(0)

Have each button call the same function but with a different parameter.
# Add the a number parameter here based on what you've
def KitchenSpot(self,state, light_index):
lights[light_index].name
lights[light_index].on = state
Then in the KV file,
Button:
text: "Kitchen Spot 3"
on_press: root.KitchenSpot(state, light_index = 3)
Button:
text: "Kitchen Spot 4"
on_press: root.KitchenSpot(state, light_index = 4)
You only have to create the function one, with each button passing in the relevant light_index number.

Neither knowing kivy nor phue I've tried to reduce the problem to your code redundancy by abstracting the definition of your methods (and also the creation of your action.kv file).
So I hope this what you are looking for: First, I would define all the relevant data of the buttons in a global variable, like:
BUTTONS = [
{'id': 0, 'methodname': 'KitchenSpot1', 'text': 'Kitchen Spot 1'},
{'id': 1, 'methodname': 'KitchenSpot2', 'text': 'Kitchen Spot 2'},
...
]
Then define your Controller-class with all unique methods just like you did (__init__ and update in your case; however I don't see what update should do, I just left it be):
class Controller(GridLayout):
state = StringProperty('down')
def __init__(self, **kwargs):
super(Controller, self).__init__(**kwargs)
Clock.schedule_interval(self.update, 1.0 / 60.0)
def update(self, dt):
if b.get_light(1, 'on'):
self.state = 'down'
else:
self.state = 'normal'
# this iteratively creates your button-individual methods
for button_dict in BUTTONS:
def func(self, state):
lights[button_dict['id']].name # whatever this might do just adressing the name attribute. Is it a python-property on lights that does some action?
lights[button_dict['id']].on = state
def func_change(self, change):
if change == True and b.get_light(button_dict['id'], 'on'):
getattr(self, button_dict['methodname'])(False)
else:
getattr(self, button_dict['methodname'])(True)
# create .KitchenSpot1, .KitchenSpot2, ...
setattr(Controller, button_dict['methodname'], func)
# create .changeKitchenSpot1, .changeKitchenSpot2, ...
setattr(Controller, "change{}".format(button_dict['methodname']), func_change)
The instantiated Controller will have bound methods named accordingly to all methodnames and change-methodnames.
Finally you can create your action.kv file dynamically
actionkv_toggle_button = """
ToggleButton:
id: {methodname}Toggle
text: "{text}"
on_press: root.change{methodname}(1)
#on_release: root.{methodname}(False)
#state1 = app.update.h
state: root.state
"""
actionkv_str = """
<Controller>:
cols: 4
rows: 3
spacing: 10
state: "normal"
{buttons}
""".format(
buttons="".join([
actionkv_toggle_button.format(
methodname=button_dict['methodname'],
text=button_dict['text']
) for button_dict in BUTTONS
])
)
this gives the output
<Controller>:
cols: 4
rows: 3
spacing: 10
state: "normal"
ToggleButton:
id: KitchenSpot1Toggle
text: "Kitchen Spot 1"
on_press: root.changeKitchenSpot1(1)
#on_release: root.KitchenSpot1(False)
#state1 = app.update.h
state: root.state
ToggleButton:
id: KitchenSpot2Toggle
text: "Kitchen Spot 2"
on_press: root.changeKitchenSpot2(1)
#on_release: root.KitchenSpot2(False)
#state1 = app.update.h
state: root.state
Save it to a file
with open('action.kv', 'w') as f:
f.write(actionkv_str)
Helpful links:
Dynamically attaching methods to a class
String formatting in python
List comprehension

Related

Python, Kivy: Problem with calling functions from different classes/screens

I'm having trouble with correctly calling functions from different classes.
I am making a simple game which calculates the score using the amount of time it takes to clear a level. There's a stopwatch running in the background and I want to add a pause button that popup menu, and a resume button inside this popup menu.
The problem is that when calling the pause function from within the popup menu, it will also be returned inside the popup, instead of inside the main widget.
Here is a simplified version of the code:
import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import NumericProperty
from kivy.uix.widget import Widget
from kivy.uix.popup import Popup
from kivy.clock import Clock
root_widget = Builder.load_file('app.kv')
class ExampleWidget(Widget):
time = NumericProperty(0)
paused = False
stop = False
# Keeping time
def increment_time(self, interval):
self.time += .1
print(self.time) # To check if stopwatch is running or not
# Stop should mean that the stopwatch must reset when it starts again.
# When paused it should resume when it starts again
def stop_start_or_pause(self):
# stop stopwatch
if self.stop:
Clock.unschedule(self.increment_time)
print('Stopped')
# Make sure time is 0 when restarting
elif not self.stop and not self.paused:
# Keeping time
self.time = 0
Clock.schedule_interval(self.increment_time, .1)
# Pause stopwatch
elif self.paused:
Clock.unschedule(self.increment_time)
print("!!", self.time) # To make it easier to see if stopwatch actually resumes where it left off
print('unscheduled') # Just to confirm and to make it a bit easier to see
# resume stopwatch
elif not self.paused:
Clock.schedule_interval(self.increment_time, .1)
class PopupMenu(Popup):
example = ExampleWidget()
class MyApp(App):
ExampleWidget = ExampleWidget()
def build(self):
return ExampleWidget()
MyApp().run()
.kv file:
#:import Factory kivy.factory.Factory
<PopupMenu#Popup>
auto_dismiss: False
size_hint_y: .8
size_hint_x: .9
title: 'Pause'
example: app.ExampleWidget
BoxLayout:
Button:
text: 'resume'
on_press: root.example.paused = False
on_release: root.dismiss(); root.example.stop_start_or_pause()
size: self.size
<ExampleWidget>:
GridLayout:
col: 2
rows: 3
size: root.size
Button:
text: 'start'
size: self.size
on_press: root.stop = False; root.stop_start_or_pause()
Button:
text: 'stop'
size: self.size
on_press: root.stop = True; root.stop_start_or_pause()
Button:
text: 'Pause menu'
size: self.size
on_press: root.paused = True
on_release: Factory.PopupMenu().open(); root.stop_start_or_pause()
Label:
text: str(round(root.time))
size: self.size
I tried making a function and using Clock.schedule.interval() to keep checking if paused == True, but it keeps returning:
AttributeError: 'float' object has no attribute 'stopped'
This didn't seem like efficient solution anyways, so I didn't want to spend too much time on this function. I also tried to find 'stupid' mistakes (I.e. ',' instead of '.') but that was before I realised that the resume button returned a 'second' stopwatch instead of updating the one I actually wanted to use.
I hope that someone can help, and that my question is clear. English is not my first language so I sometimes have a hard time finding the best way to explain/ask questions.
Thank you in advance!
If I understand your question, the problem is with your MyApp class:
class MyApp(App):
ExampleWidget = ExampleWidget()
def build(self):
return ExampleWidget()
This code is creating two instances of ExampleWidget. One is returned in the build() method, and one is saved as the ExampleWidget attribute of MyApp. Now, when you use the ExampleWidget attribute of MyApp, you are not referencing the ExampleWidget that is the root of your GUI, so it has no effect on what appears on the screen. The fix is to just creat a single instance of ExampleWidget, like this:
class MyApp(App):
ExampleWidget = ExampleWidget()
def build(self):
return self.ExampleWidget

How to use OOP in Kivy Python?

Please help me, my competition is around the corner
I tried to use OOP in Kivy. This is my simple Python code for testing:
class location:
def __init__(self, total_house, total_land):
self.total_house = total_house
self.total_land = total_land
class test(BoxLayout):
def addNum(self):
App.get_running_app().x.total_house += 1
class testApp(App):
x = location(NumericProperty(10),NumericProperty(5))
testApp().run()
this is my kv file:
<test>:
orientation: 'vertical'
Label:
text: str(app.x.total_house)
Button:
text: 'add'
on_press: root.addNum()
This is the output
I want the output to be 10 and when the button is pressed the number is added by one.
Please help me, I am new to KIVY
One way of getting pure value from Kivy Property is to use the built-in .get(EventDispatcher obj) method from the kivy.properties.Property class:
class test(BoxLayout):
def addNum(self):
App.get_running_app().x.get(EventDispatcher()) += 1
But before that, you need to import the EventDispatcher class first:
from kivy._event import EventDispatcher
Also please note that while this works in theory and it will indeed change the value of the x variable, I would recommend directly changing the label's own text, something like this:
.py
def numberify(*args):
# This functions is for universally changing str to either int or float
# so that it doesn't happen to return something like 8.0 which isn't that great
a = []
for w in range(0, len(args)):
try:
a.append(int(args[w]))
except ValueError:
a.append(float(args[w]))
return a if len(a) > 1 else a[0]
class test(BoxLayout):
def addNum(self):
self.ids.label1.text = str(numberify(self.ids.label1.text) + 1)
.kv
<test>:
orientation: 'vertical'
Label:
id: label1
text: str(app.x.total_house)
Button:
id: button1
text: 'add'
on_press: root.addNum()
Learn more about Kivy Property here and understand how it's not always necessary to use them :)

adding two or more Values in kivy and getting the result dynamic array

I'm new in Kivy and i need your help ..
I have a little question:
I need a dynamic array, which the user can enter first value, in the erst TextInput Box , then he can press the Button "new line", he gets the possibility to enter second value in a new TextInput Box,than he can press again the Button "new line".. he has the possibility to enter a third value in the new TextInput Box ..
at any time he can press "Result" .. to get the sum of this Values in a label
How can I make this dynamic array ?
Thanks
H Lothric ..
this is the code
main.py
from kivy.uix.textinput import TextInput
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
class MainWindow(Screen):
def __init__(self, **kwargs):
super(MainWindow, self).__init__(**kwargs)
self.counter = 1
self.textlist = [TextInput()]
self.ids.grid.add_widget(Label(text='Input value ' + self.counter))
self.counter += 1
self.ids.grid.add_widget(self.textlist[0])
# function to create new inputs, that button 'new line' calls:
def addnewtextinput(self):
self.ids.grid.add_widget(Label(text='Input value ' + self.counter))
self.counter += 1
self.textlist.append(TextInput())
self.ids.grid.add_widget(self.textlist[-1])
# function to get a result:
def getresult(self):
result = 0
for i in self.textlist:
# you may convert it to float if you need, like float(i.text)
result += int(i.text)
self.ids.label_id.text = str(result)
class WindowManager(ScreenManager):
pass
class MyMainApp(App):
def build(self):
b1=WindowManager()
MainWindow()
return b1
if __name__ == "__main__":
MyMainApp().run()
and this is the main.kv
<CustButton#Button>:
font_size: 40
WindowManager:
MainWindow:
<MainWindow>:
name: "main"
GridLayout:
cols:1
# you will control that GridLayout from .py so here it's empty
GridLayout:
# set the id to be able to control it from .py file
id: grid
cols: 2
CustButton:
text: "new line"
on_press: root.addnewtextinput()
CustButton:
text: "result"
font_size: "30sp"
on_press: root.getresult()
TextInput:
id:label_id
font_size: 40
multiline: True
this is the code
What's the problem? Make a list of TextInputs and then take the values from it. Something like this:
# create a list with first text input:
self.textlist = [TextInput()]
# I don't know which layout you are using, for example BoxLayout with box variable. Add TextInput to box:
self.box.add_widget(self.textlist[0])
# function to create new inputs, that button 'new line' calls:
def addnewtextinput(self):
self.textlist.append(TextInput())
self.box.add_widget(self.textlist[-1])
# function to get a result:
def getresult(self):
result = 0
for i in self.textlist:
# you may convert it to float if you need, like float(i.text)
result += int(i.text)
return result
Ok, in your case it would be like this:
.py file:
# import this
from kivy.properties import ObjectProperty
from kivy.clock import Clock
...
class MainWindow(Screen):
def __init__(self, **kwargs):
super(MainWindow, self).__init__(**kwargs)
# we have to delay running that function, it will run when kv file will be ready to provide widgets by id
Clock.schedule_once(self.getids)
self.counter = 1
self.textlist = [TextInput()]
self.grid.add_widget(Label(text='Input value ' + counter))
self.counter += 1
self.grid.add_widget(self.textlist[0])
# don't forget this
grid = ObjectProperty(None)
# function to connect layout with the variable
def getids(self):
self.grid = self.ids.grid
# function to create new inputs, that button 'new line' calls:
def addnewtextinput(self):
self.grid.add_widget(Label(text='Input value ' + self.counter))
self.counter += 1
self.textlist.append(TextInput())
self.grid.add_widget(self.textlist[-1])
# function to get a result:
def getresult(self):
result = 0
for i in self.textlist:
# you may convert it to float if you need, like float(i.text)
result += int(i.text)
self.ids.label_id.text = str(result)
class MyMainApp(App):
def build(self):
self.b1 = WindowManager()
self.b1.add_widget(MainWindow())
return self.b1
.kv file:
<MainWindow>:
name: "main"
# don't forget to add this
grid: grid.__self__
GridLayout:
cols:1
# you will control that GridLayout from .py so here it's empty
GridLayout:
# set the id to be able to control it from .py file
id: grid
cols: 2
CustButton:
text: "new line"
on_press: root.addnewtextinput()
CustButton:
text: "result"
font_size: "30sp"
on_press: root.getresult()
TextInput:
id:label_id
font_size: 40
multiline: True

Kivy: How to change ScreenManager's "current" property on __init__

My App is a ScreenManager. Depending on the circumstances, I want the active screen to either be "Screen 1" or "Screen 2" when the app opens. How would I do this the most elegant way? I thought that this is as trivial as changing the current property in the initialization of the app. Sadly, this does not work. Here's what should work imo:
main.py:
MyApp(App):
def build(self):
return Builder.load_file("MyApp.kv")
def __init__(self, **kwargs):
super(MyApp, self).__init__(**kwargs)
if foo: # Here's the problem:
self.root.current = "Screen 1"
else:
self.root.current = "Screen 2"
MyApp.kv:
ScreenManager:
Screen1:
name: "Screen 1"
Screen2:
name: "Screen 2"
<Screen1#Screen>
etc...
But it doesn't. Throws the following error:
self.root.current = "Screen 1"
AttributeError: 'NoneType' object has no attribute 'current'
My guess is that I set the current attribute to early, before root is set. An idea of mine is to 1) create a property-var for MyApp, 2) set current to be that property, 3) change that property in the init method. That's a lot of effort and code-cluttering just to change a screen on initialization.
How would I do it? Thanks a lot in advance!
That's simply because you don't have self.root object specified. Why would you need to change Screens during __init__ ? You should use build function for that.
My example:
import random
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager
Builder.load_string('''
<Root>:
Screen:
name: "Screen 1"
Label:
text: "Screen 1!"
Screen:
name:"Screen 2"
Label:
text: "Screen 2!"
''')
class Root(ScreenManager):
pass
class MyApp(App):
def build(self):
self.root = Root()
foo = random.randint(0,1)
if foo:
self.root.current = "Screen 1"
else:
self.root.current = "Screen 2"
return self.root
MyApp().run()
self.root.cureent_screen property will be changed before self.root object will be visible
Only this is working.
I said above statement because I tryed many codes(but not works)
class NextScreen(ScreenManager):
def __init__(self,**kwargs):
super(NextScreen,self).__init__(**kwargs)
#code goes here and add:
Window.bind(on_keyboard=self.Android_back_click)
def Android_back_click(self,window,key,*largs):
# print(key)
if key == 27:
if self.current_screen.name == "Screen1" or self.current_screen.name == "Screen_main":
return False
elif self.current_screen.name == "Screen2":
try:
self.current = "Screen1"
except:
self.current = "Screen_main"
return True
Use 'esc' button to get key(27) which means back in android

Kivy multiple selection with checkboxes

I'm trying to create a view using Kivy that has a list of options that are all selected by default, and the user can choose to deselect some entries (by clicking on the checkbox or anywhere on the row).
Clicking on the label part of the row item works, but I noticed that clicking on the checkbox doesn't change the selection which I can't work out how to solve (I tried a few different state bindings, I left them commented out in the example code)
Here is a quick example showing what I've tried.
from kivy.app import App
from kivy.properties import StringProperty, ListProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.selectableview import SelectableView
from kivy.uix.togglebutton import ToggleButtonBehavior
from kivy.adapters.models import SelectableDataItem
from kivy.lang import Builder
Builder.load_string("""
#: import ListAdapter kivy.adapters.listadapter.ListAdapter
#: import Factory kivy.factory.Factory
<MyListItem>:
height: 50
on_state: root.is_selected = args[1] == "down"
state: "down" if root.is_selected else "normal"
BoxLayout:
spacing: 10
CheckBox:
on_state: root.is_selected = args[1] == "down"
state: "down" if root.is_selected else "normal"
# on_state: root.state = args[1]
# state: root.state
Label:
text: root.name
<Page>:
orientation: "vertical"
ListView:
id: LV
adapter: ListAdapter(data=root.data, cls=Factory.MyListItem, args_converter=root.args_converter, selection_mode="multiple", propagate_selection_to_data=True)
Button:
size_hint_y: None
text: "print selection"
on_press: print(LV.adapter.selection)
""")
class MyListItem(ToggleButtonBehavior, SelectableView, BoxLayout):
name = StringProperty()
def __repr__(self):
return "%s(name=%r)" % (type(self).__name__, self.name)
def on_state(self, me, state):
print me, state
if state == "down":
self.select()
else:
self.deselect()
# self.is_selected = state == "down"
class DataItem(SelectableDataItem):
def __init__(self, name, **kwargs):
super(DataItem, self).__init__(**kwargs)
self.name = name
def __repr__(self):
return "%s(name=%r, is_selected=%r)" % (type(self).__name__, self.name, self.is_selected)
class Page(BoxLayout):
data = ListProperty()
def __init__(self, **kwargs):
super(Page, self).__init__(**kwargs)
self.data = [DataItem("Item {}".format(i), is_selected=True) for i in range(10)]
def args_converter(self, index, data_item):
return {
"index": index,
"name": data_item.name,
}
class ExampleApp(App):
def build(self):
return Page()
if __name__ == "__main__":
ExampleApp().run()
I'm using Kivy v1.9.1-dev
Edit: I worked out how to get all the entries pre-selected, I've updated the code and took that part of the question out.
Just in case someone else has the the question I point to the right url:
You should consider the new RecycleView, which has all the functionality you request. Look here for a sample: Kivy: alternative to deprecated features

Categories