I am trying to create a dashboard of buttons on the Raspberry Pi touchscreen as a home hobby and I want to create some buttons that you press and release, some that latch on and off and some that have multiple states.
I want to change the state of a button when it is pressed. So I can detect that the button is pressed via the CreateButton class, but I want to pass the button state information in at init. When I do so, I get
AttributeError: 'CreateButton' object has no attribute '_disabled_count'
If I remove the init in CreateButton the code runs, which I don't understand. Can you suggest where I'm going wrong?
Many thanks
Main.py
import pickle
from kivy.app import App
from kivy.uix.image import AsyncImage
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from typing import List
# Define the data structure of a button state
class StateDefinition(object):
def __init__(self, Label, Image, Colour, Action):
self.Label = Label
self.Image = Image
self.Colour = Colour
self.Action = Action
# Define the data structure of a button
class ButtonDefinition(object):
def __init__(self, buttonname: str, currentstate: int, buttonstates: List[StateDefinition]):
self.currentstate = currentstate
self.buttonname = buttonname
self.buttonstates = buttonstates
# Define the data structure of a dashboard - a collection of related buttons
class DashboardDefinition(object):
def __init__(self, dashboardname: str, dashboardbuttons: List[ButtonDefinition]):
self.dashboardname = dashboardname
self.dashboardbuttons = dashboardbuttons
# This class creates the kivy button and binds it to the action
class CreateButton(Button):
def __init__(self, **kwargs):
print("Got Here")
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
if touch.button == "right":
print(self.id, "right mouse clicked")
elif touch.button == "left":
print(self.id, "left mouse clicked")
else:
print(self.id)
return True
return super(CreateButton, self).on_touch_down(touch)
## This is the class that builds the Carousel of buttons
class BuildLayout(GridLayout):
def __init__(self, **kwargs):
super(BuildLayout, self).__init__(**kwargs)
self.build_dashboard()
def build_dashboard(self):
# Define all the test data
state1 = StateDefinition(Label = 'State 1', Image = 'Image1.png', Colour = '#FF0000', Action = 'A')
state2 = StateDefinition(Label = 'State 2', Image = 'Image2.png', Colour = '#00FF00', Action = 'B')
state3 = StateDefinition(Label = 'State 3', Image = 'Image3.png', Colour = '#0000FF', Action = 'C')
button1 = ButtonDefinition(buttonname = "Button 1", currentstate = 0, buttonstates = [state1])
button2 = ButtonDefinition(buttonname = 'Button 2', currentstate = 0, buttonstates = [state2, state1])
button3 = ButtonDefinition(buttonname = 'Button 3', currentstate = 0, buttonstates = [state3, state2, state1])
dashboard1 = DashboardDefinition(dashboardname = "Test Dashboard", dashboardbuttons = [button1, button2, button3])
for buttonID in range(0, len(dashboard1.dashboardbuttons)):
buttonwidget = CreateButton(id = str(buttonID))
buttonwidget.text = dashboard1.dashboardbuttons[buttonID].buttonname + "\nButton State: " + \
str(dashboard1.dashboardbuttons[buttonID].currentstate)
self.add_widget(buttonwidget)
# This is tha main class that sets up the app
class LayoutApp(App):
def build(self):
return BuildLayout()
if __name__ == '__main__':
LayoutApp().run()
Layout.kv
#:kivy 1.11.0
<CreateButton>:
font_size: 18
on_touch_down: self.on_touch_down
<BuildLayout>:
rows: 3
cols: 3
row_force_default: True
row_default_height: 150
col_force_default: True
col_default_width: 150
padding: [10]
spacing: [10]
When you override the __init__ method for Kivy elements, it won't do the inherited __init__ method unless you manually tell it to. If you don't do that, it won't get set up properly with all the properties Kivy expects (like _disabled_count).
I expect this will work if you just add super().__init__(**kwargs) (or super(<classname>, self).__init__(**kwargs) for Python 2 compatibility) as the first line of your new __init__ methods, like you did in BuildLayout. It's easy to forget!
Related
I've been trying to change the label2 text in layout 2, with button1 in layout1, but it doesn't seem to work, when I press the button nothing happens
Here's the code:
class layout1(GridLayout):
def __init__(self,**kwargs):
super().__init__(**kwargs)
self.cols = 2
self.button1 = Button(text = "Button 1 changes screen 2", on_press = self.change_label)
self.add_widget(self.button1)
self.change_button = Button(text = "move to screen 2", on_press = self.change_screen)
self.add_widget(self.change_button)
def change_screen(self, instance):
practice_app.sm.current = "screen2"
def change_label(self,instance):
func_layout = layout2()
func_layout.label2.text = "changed"
class layout2(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 2
self.label2 = Label(text = "this should change")
self.add_widget(self.label2)
class TestApp(App):
def build(self):
self.sm = ScreenManager()
screen1 = Screen(name = "screen1")
screen1.add_widget(layout1())
self.sm.add_widget(screen1)
screen2 = Screen(name = "screen2")
screen2.add_widget(layout2())
self.sm.add_widget(screen2)
return self.sm
if __name__ == "__main__":
practice_app = TestApp()
practice_app.run()
There are many ways to do what you want. Since you are not using kv, perhaps the easiest way is to save a reference to layout2. Here is a modified version of your build() method that does that:
class TestApp(App):
def build(self):
self.sm = ScreenManager()
screen1 = Screen(name = "screen1")
screen1.add_widget(layout1())
self.sm.add_widget(screen1)
screen2 = Screen(name = "screen2")
self.layout2 = layout2() # save reference to layout2
screen2.add_widget(self.layout2)
self.sm.add_widget(screen2)
return self.sm
And then, use that reference in the change_label() method:
def change_label(self,instance):
# func_layout = layout2() # creates a new instance of layout2 (not the one in the GUI)
func_layout = App.get_running_app().layout2
func_layout.label2.text = "changed"
I created some instances of a class (Player class) in my first Screen, and would like to access these instances (and their data) in another Screen. The idea is this:
Player class: instance of this class contains players name and their points. (done)
WelcomeWindow(Screen) class: extract name of players via TextInput + create instances of Player class and assign the names. (done)
ThirdRound(Screen) class: Access names and points of the player instances, display the names on a label and change the players points when a +/- Button is pressed. (problem)
I marked in the code below where I tried to access the instances, but got the error:
AttributeError: 'NoneType' object has no attribute 'get_screen'
My question is: How can I access the player points in ThirdRound(Screen) class in order to display their points on a label and to change the points when +/- Button is pressed?
(I searched for similar cases but was not able to apply them to my case.)
.py file:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.textinput import TextInput
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.properties import ObjectProperty, NumericProperty
class Player:
def __init__(self, name):
self.name = name
self.points = 0
def reset_points(self):
self.points = 0
def add_point(self, *args):
self.points += 1
def subtract_point(self, *args):
self.points -= 1
class WelcomeWindow(Screen):
# Introduce names of the 4 players
def __init__(self, **kwargs):
super(WelcomeWindow, self).__init__(**kwargs)
self.name = "welcomewindow"
# Create global layout of HOME screen
global_layout = GridLayout(rows=3)
self.add_widget(global_layout)
# Some code with labels and buttons
# Create button to go to next screen
go_further_button = Button(text="Go to first round")
go_further_button.bind(on_release=self.go_further)
global_layout.add_widget(go_further_button)
def go_further(self, *args):
#Give names to players
self.player1 = Player("name1") # <--- 4 player instances are created here
self.player2 = Player("name2")
self.player3 = Player("name3")
self.player4 = Player("name4")
self.manager.current = "thirdround"
self.manager.transition.direction = "left"
class ThirdRound(Screen):
def __init__(self, **kwargs):
super(ThirdRound, self).__init__(**kwargs)
self.name = "thirdround"
welcome_window = self.manager.get_screen('welcomewindow') # <--- Trying to access player instances here but failed
self.player1_points = welcome_window.player1.points
def change_label(self, add_sub, *args): # <--- method which will change the points of the player instance
label = self.ids['testlabel']
if add_sub == 'add':
label.text = 'should add one point'
elif add_sub == 'sub':
label.text = 'should subtract one point'
kv = Builder.load_file("Kingen.kv")
WindowManager = ScreenManager()
WindowManager.add_widget(WelcomeWindow())
WindowManager.add_widget(ThirdRound())
class KingenApp(App):
def build(self):
return WindowManager
if __name__ == "__main__":
KingenApp().run()
.kv file:
<ThirdRound>:
GridLayout:
rows: 3
Label:
id: testlabel
text: 'shows number of points here'
Button:
text: "+"
on_release: root.change_label('add')
Button:
text: "-"
on_release: root.change_label('sub')
The __init__() method of ThirdRound is called at the line:
WindowManager.add_widget(ThirdRound())
Specifically, the ThirdRound() is executed before the WindowManager.add_widget, so the ThirdRound instance does not have its manager yet set when its __init__() is executed, so it is still None. Try moving the code that accesses the manager till it is needed. Something like this:
class ThirdRound(Screen):
def __init__(self, **kwargs):
super(ThirdRound, self).__init__(**kwargs)
self.name = "thirdround"
def change_label(self, add_sub, *args): # <--- method which will change the points of the player instance
welcome_window = self.manager.get_screen('welcomewindow')
label = self.ids['testlabel']
if add_sub == 'add':
welcome_window.player1.points += 1
label.text = str(welcome_window.player1.points)
elif add_sub == 'sub':
welcome_window.player1.points -= 1
label.text = str(welcome_window.player1.points)
I am trying to put a given pictures in a gridlayout that can scroll, and when i select the picture the color of the image change, here is my code:
CONTAINER_PNG = os.path.join(AllImage_ROOT, 'images')
IMAGES_NAMES = [c[:-4] for c in os.listdir(CONTAINER_PNG)]
LIST_IM = os.listdir(CONTAINER_PNG)
class ImageButton(ButtonBehavior, Image):
pass
class AllImage(BoxLayout):
# screen_manager = ObjectProperty()
def __init__(self, **kwargs):
BoxLayout.__init__(self, **kwargs)
self.orientation='vertical'
splitter = Splitter(sizable_from = 'bottom')
root = ScrollView()
layout = GridLayout(cols=4, spacing=10)
layout2 = GridLayout(cols=4, spacing=10)
button = ImageButton(source = 'mix.png')
layout2.add_widget(button)
self.add_widget(layout2)
for im in IMAGES_NAMES:
if IMAGES_NAMES != None :
btn = ImageButton(source = im+'.png')
btn.bind(on_press= lambda a:layout.add_widget( ToggleButton(text = 'work') ))
btn.bind(on_press= lambda b:self.background_color(1,1,1))
layout.add_widget(btn)
layout2.add_widget(splitter)
root.add_widget(layout)
self.add_widget(root)
class TryApp(App):
def build(self):
return AllImage()
def on_pause(self):
return True
if __name__ == "__main__":
TryApp().run()
I know i am doing things wrong, so i have several questions :
1- Why when i add a Splitter between my 2 Grids it don't work(the splitter is not visible)
2- How can i change the color of my ImageButton ?
3- The scrollview isn't working on my GridLayout, how can i custom my Grid that can be bigger than my window.
Thank you for your time :)
kivy tries to make things simple by separating the UI from the logic..from the kivy docs,it says You must deactivate at least one of the size_hint instructions (x or y) of the child to enable scrolling..
<AllImage>:
orientation:'vertical'
ScrollView:
do_scroll_x:False
GridLayout:
cols:4
spacing:10
size_hint_y:None
height: 8*dp(80)
for clarity sake,try to implement the UI stuffs in a kv file to make things easier to read.
btn.bind(on_release= lambda a:layout.add_widget( ToggleButton(text = 'work') ))
btn.bind(on_press= lambda b:self.background_color(1,1,1))
i dont think the on_press can handle two methods at the sametime,so try this
I can't find the syntax for setting on_press in python code to change the screen anywhere. I keep getting errors for anything like Button(text = 'hi', on_press = self.current = 'start_menu. Here's the code and it works as is.
class LoadMenu(Screen):
def __init__(self, **kwargs):
super(LoadMenu, self).__init__(**kwargs)
Clock.schedule_once(self.update)
def update(self, dt):
L = [x for x in range(len(os.listdir('saves')))]
for x in L:
x = self.add_widget(Button(text = os.listdir('saves')[x]))
I haven't positioned the buttons so they just are on top of each other but I can fix that later. What I need to happen is for each button to change to the play screen on press so that will be the same for each button but I also need each one to load the Shelve file they a referencing.(I know I'll need another function for that) Can I have an on_press trigger two events at once, and again how do I set it in the python code?
Consider the following programme:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen
from kivy.uix.button import Button
from kivy.clock import Clock
from kivy.properties import StringProperty
dirlist = ['aaa', 'bbb', 'ccc', 'ddd']
class MyButton(Button):
prop = StringProperty('')
def on_press(self):
print "Class-defined on_press handler (I'm {})".format(self.text)
def other_on_press_handler(sender):
print "other_on_press_handler, from {}".format(sender.text)
def some_func(text):
print "yeah: " + text
class LoadMenu(Screen):
def __init__(self, **kwargs):
super(LoadMenu, self).__init__(**kwargs)
Clock.schedule_once(self.update)
def on_press_handler(self, sender):
print "on_press_handler, from {}".format(sender.text)
self.parent.current = 'sc2'
def yet_another_on_press_handler(self, sender):
print "yet_another_on_press_handler, from {}".format(sender.text)
self.parent.current = 'sc2'
def update(self, dt):
for x in range(len(dirlist)):
my_b = Button(text = dirlist[x], on_press=self.on_press_handler)
self.parent.ids.button_container.add_widget(my_b)
if x > 1:
my_b.bind(on_press=other_on_press_handler)
if x == 3:
my_b.bind(on_press=lambda sender: some_func("Whoa, lambda was here ({})".format(sender.text)))
for x in range(len(dirlist)):
my_b = MyButton(text = 'my '+ dirlist[x], prop="{} {}".format(dirlist[x], x))
self.parent.ids.button_container.add_widget(my_b)
my_b.bind(on_press=self.yet_another_on_press_handler)
root = Builder.load_string("""
ScreenManager:
LoadMenu:
name: 'sc1'
GridLayout:
cols: 4
id: button_container
Screen:
name: 'sc2'
BoxLayout:
Button:
text: "Go back"
on_press: root.current = 'sc1'
""")
class MyApp(App):
def build(self):
return root
if __name__ == '__main__':
a = MyApp()
a.run()
Let's start by looking at the update method in LoadMenu: In the first loop, a bunch of buttons is generated, and each receives an on_press callback at creation. The last two buttons in the loop get bound to another callback, and the last example shows how to use a lambda expression to generate a callback.
In the second for loop, we instantiate object of class MyButton, a child of Button. Note that we also define an on_press handler in the class definition; this gets called in addition to other functions we may have bound.
But really, this is actually all pretty nicely explained in the kivy Events and Properties docs.
Well, I'm having trouble in a specific part of my code. It uses kivy, however I'm pretty sure there is a python solution. Here is the thing: I'll have a button, when pressed will take me to another screen, when it's pressed, calls method vai, that changes, or should change the string variable value that was created in init method. Afterwards, when another screen shows up, its button receives that CHANGED variable at text parameter. But the real issue is that by the time second screen appears, the button text does not change, remaining the value I set up on init once, not the changed value on vai method.
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.button import Button
class Principal(App):
def build(self):
return SM()
class SM(ScreenManager):
def __init__(self):
super(SM, self).__init__()
self.add_widget(Um())
self.current = 'TelaUm'
class Um(Screen):
def __init__(self):
super(Um, self).__init__()
self.name = 'TelaUm'
self.add_widget(UmC())
class UmC(FloatLayout):
def __init__(self):
super(UmC, self).__init__()
self.btnSelecionado = 'qualquer_merda'
self.btn = Button(id = 'Bosta!', text = 'Bosta!', pos_hint = { 'center_x' : .5, 'center_y' : .5 }, size_hint = (None, None))
self.btn.bind(on_press = self.vai)
self.add_widget(self.btn)
def vai(self, instance):
self.parent.parent.add_widget(Dois())
self.parent.parent.current = 'TelaDois'
self.btnSelecionado = instance.id
class Dois(Screen):
def __init__(self):
super(Dois, self).__init__()
self.name = 'TelaDois'
self.add_widget(DoisC())
class DoisC(UmC, FloatLayout):
def __init__(self):
super(DoisC, self).__init__()
self.btn2 = Button(text = self.btnSelecionado, pos_hint = { 'center_x' : .5, 'center_y' : .5 }, size_hint = (None, None) )
self.add_widget(self.btn2)
Principal().run()
I don't completely understand what you are trying to do. But it looks to me like the second screen is a new instance of UmC and therefore has its own value of btnSelecionado. So inevitably this new instance has the value from init since it's only the old instance that has been changed.
def vai(self, instance):
self.parent.parent.add_widget(Dois())
self.parent.parent.current = 'TelaDois'
self.btnSelecionado = instance.id
Line 2 creates a new instance and line 4 sets the value in the old instance.