Kivy callbacks for instanced buttons/widgets? - python

I am trying to make some instanced buttons that will trigger the 'on_toggle_button_state' function like the KV snippet below. I'm sure it's something simple I've overlooked, but this has stumped me for longer than I'd like to admit.
AttributeError: 'ToggleButtons' object has no attribute 'state'
class ToggleButtons(StackLayout):
def __init__(self, *args, **kwargs):
super().__init__(**kwargs)
conn = sqlite3.connect('players_db.db')
c = conn.cursor()
c.execute("DELETE FROM players WHERE (name IS NULL OR name = '')") ##Remove empty entries
c.execute("SELECT * FROM players")
records = c.fetchall()
records = list(filter(None, records))
for i in range(0, len(records)):
name = str(records[i])[2:-3]
b = ToggleButton(size_hint = (0.2, 0.2), text = name, on_state = self.on_toggle_button_state())
self.add_widget(b)
def on_toggle_button_state(self, widget):
print("toggle state: " + widget.state)
if widget.state == "normal":
widget.text = "OFF"
self.count_enabled = False
else:
widget.text = "ON"
self.count_enabled = True
KV that works for not-instanced buttons:
StackLayout:
ToggleButton:
text: "Charlie"
on_state: root.on_toggle_button_state(self)
size_hint: 0.2, 0.2

In you kv file use <ToggleButton>: to assign the on_state method with self.parent.on_toggle_button_state(self).
self is the dynamically instantiated button and parent is the ToggleButtons StackLayout where you have defined the on_toggle_button_state method.
I've modified your code a bit so I could run it without the SQL stuff. So ignore these changes.
The difference between ToggleButton and <ToggleButton> is that the first places an instance into the StackLayout - not what you want here.
And the bracket notation, defines methods & styles for each of your instances.
Please find the snippet below with the mentioned change:
from kivy.app import App
from kivy.uix.stacklayout import StackLayout
from kivy.uix.button import Button
from kivy.lang import Builder
kv = '''
StackLayout:
<ToggleButton>:
text: "Charlie"
on_state:
self.parent.on_toggle_button_state(self)
size_hint: 0.2, 0.2
'''
Builder.load_string(kv)
class ToggleButton(Button):
pass
class ToggleButtons(StackLayout):
def __init__(self, *args, **kwargs):
super().__init__(**kwargs)
# conn = sqlite3.connect('players_db.db')
# c = conn.cursor()
# c.execute("DELETE FROM players WHERE (name IS NULL OR name = '')") ##Remove empty entries
# c.execute("SELECT * FROM players")
# records = c.fetchall()
# records = list(filter(None, records))
records = ["test1", "test2"]
for i in range(0, len(records)):
# name = str(records[i])[2:-3]
name = records[i] # just for testing
print(name)
b = ToggleButton(size_hint = (0.2, 0.2), text = name, on_state = self.on_toggle_button_state)
self.add_widget(b)
def on_toggle_button_state(self, widget):
print("toggle state: " + widget.state)
if widget.state == "normal":
widget.text = "OFF"
widget.count_enabled = False
else:
widget.text = "ON"
widget.count_enabled = True
class MyApp(App):
def build(self):
return ToggleButtons()
if __name__ == '__main__':
MyApp().run()

Related

How to make changes to global object attribute with Kivy TextInput and have it save for later use?

My app currently has 10 "players" with a name, offensive rating, and defensive rating. I want to give the user the opportunity to change the names of the players before running my game simulation code, but it appears I can only temporarily change the name inside of the current screen. Here's my current python code for when the user inputs the name from the gui:
class SecondWindow(Screen):
def pg1_name_val(self, widget):
pg1.name = widget.text
If I code in a print(pg1.name) right below that, it will print the correct name, but when the user hits "play" to shift to a results screen, the names go back to the default names.
How do I make pg1.name from my SecondWindow(Screen) become the global pg1.name? Is there a simple line of code I can use to return pg1.name as the global variable?
EDIT ----
So I've came a long way with my app this week, but now that I want to have a separate "results" screen, I've realized that I lost all of my data from the previous screen. It looks like all of the changes I make to my "players" are going back to defaults once the screen changes. I'll paste some code below for reference. Basically, my question now is this: Is there a way to keep the objects from one screen and return them back as the global instance of the object. Not just the local instance of the object.
.py -
class Player():
def __init__(self,name,off,deff):
self.name = name
self.off = off
self.deff = deff
self.stats = 0
class Team():
def init(self,pg, sg, sf, pf, c):
self.pg = pg
self.sg = sg
self.sf = sf
self.pf = pf
self.c = c
self.score = 0
self.results = None
self.to = 0
On the screen where I'm creating my team, I have a button to run the simulation. This button runs the sim correctly, records all of the game results in a string, prints all of the results to the console, attributes, and score correctly. The button also changes the screen to where I want to display a scroll view of the results string, but the results are empty.
I'd really appreciate any help on this, as I'm at a roadblock. Thanks!
It would kinda look the same. Depending on how exactly you wanna keep the 10 different players.
class Player():
def __init__(self, name, off, deff):
self.name = name
self.off = off
self.deff = deff
self.stats = 0
player = [Player('', 0, 0)] * 10
class SecondWindow(Screen):
def __init__(self, **kwargs):
super(SecondWindow, self).__init__(**kwargs)
self.player = player
def name_change(self):
self.player[0].name = self.ids.name.text
player[0].name = self.player[0].name
def next_screen(self):
self.manager.current = 'result'
class ResultWindow(Screen):
def __init__(self, **kwargs):
super(ResultWindow, self).__init__(**kwargs)
def on_enter(self, *args):
print(player[0].name)
class Manager(ScreenManager):
pass
class Player(App):
def build(self):
sm = ScreenManager()
sm.add_widget(SecondWindow(name='second'))
sm.add_widget(ResultWindow(name='result'))
return sm
if __name__ == '__main__':
Player().run()
You would have to work with OOP, here I created a list of you player object for all ten players it is an easy way to keep the code short.
You might like to keep them as 10 different objects the good part of a list it is easy to iterate through in a for loop and then get all the data for a stats board on a screen and also easy to expand later if you need 20 players or more.
I didn't change anything in the .kv file.
.kv
<SecondWindow>:
TextInput:
id: name
hint_text: 'Player name'
size: 260, 32
pos: (root.width / 2) - (self.width / 2), 248
multiline: False
on_text_validate: root.name_change()
Button:
id: create_btn
text: 'Next Screen'
size: 260, 32
pos: (root.width / 2) - (self.width / 2), 206
color: utils.get_color_from_hex('#ffffff')
background_color: utils.get_color_from_hex('#016bf8')
on_press: root.next_screen()
<ResultWindow>:
Label:
id: name
The on_text_validate only works with multiline false and when enter is pressed if you don't want to press enter user on_text instead.
--Edit--
I did some research this morning and thought maybe to update this a bit with my findings.
You could instead of doing it the import to all screens do it in the screenmanager and just call it from the screen like this:
pyhon.py
import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
class Player:
def __init__(self, name, off, deff):
self.name = name
self.off = off
self.deff = deff
self.stats = 0
class SecondWindow(Screen):
def __init__(self, **kwargs):
super(SecondWindow, self).__init__(**kwargs)
def name_change(self):
self.manager.player.name = self.ids.name.text
def next_screen(self):
self.manager.current = 'result'
class ResultWindow(Screen):
def __init__(self, **kwargs):
super(ResultWindow, self).__init__(**kwargs)
def on_enter(self):
self.ids.name.text = self.manager.player.name
class Manager(ScreenManager):
player = Player('', 0, 0)
def __init__(self, **kwargs):
super(Manager, self).__init__(**kwargs)
class PlayerApp(App):
def build(self):
return Builder.load_file('test.kv')
if __name__ == '__main__':
PlayerApp().run()
the kv file would look something like this:
.kv
#:import utils kivy.utils
Manager:
SecondWindow:
name: 'second'
ResultWindow:
name: 'result'
<SecondWindow>:
TextInput:
id: name
hint_text: 'Player name'
size: 260, 32
size_hint: None, None
pos: (root.width / 2) - (self.width / 2), 248
multiline: False
on_text_validate: root.name_change()
Button:
id: create_btn
text: 'Next Screen'
size_hint: None, None
size: 260, 32
pos: 0, 0
color: utils.get_color_from_hex('#ffffff')
background_color: utils.get_color_from_hex('#016bf8')
on_press: root.next_screen()
<ResultWindow>:
Label:
id: name
hope it helps

The binding on button is confusing in Kivy

As a test, I prepared the following code,
Firstly, I set the button text from two function names add_front and add_back
Then, I get the function handle from the name and make a partial function to bind it to Buttons.
Though the binding seems ok, the results are random.
Anybody can help me out?
"""
#author:
#file:test-bind.py
#time:2022/01/3111:42
#file_desc
"""
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
from kivy.clock import Clock
import random
from functools import partial
Builder.load_string("""
<MButton#Button>:
_cb:[]
<TestWin>:
inp1:inp1_
lbl1:lbl1_
btn1:btn1_
btn2:btn2_
orientation: 'vertical'
BoxLayout:
orientation:'horizontal'
TextInput:
id:inp1_
readonly: True
Label:
id:lbl1_
MButton:
id:btn1_
text: 'add_front'
MButton:
id:btn2_
text: 'add_back'
Button:
id:btn_cls_
text:'clear'
on_press:root.clear_elements()
Button:
id:btn_shuffle_
text:'Shuffle'
on_press:root.shuffle_btn()
TextInput:
multiline: True
text:'Usage: press <Shuffle> to randomize button function and set a random number in [0,9], press <add_front> or <add_back> buttons to insert to list'
""")
class Box:
def __init__(self):
self.elements =[]
def add(self,e,front=False):
if front:
self.elements.insert(0,e)
else:
self.elements.append(e)
def add_front(self,e):
print("add_front",e)
self.add(e,front=True)
def add_back(self,e):
print("add_back",e)
self.add(e,front=False)
class TestWin(BoxLayout):
inp1 = ObjectProperty()
lbl1 = ObjectProperty()
btn1 = ObjectProperty()
btn2 = ObjectProperty()
btn_bind = ObjectProperty()
def __init__(self, **kwargs):
super(TestWin, self).__init__(**kwargs)
self.box = Box()
Clock.schedule_interval(self.update_elements_display, 0.5)
def update_elements_display(self,*args):
self.lbl1.text = "%s"%str(self.box.elements)
pass
def clear_elements(self):
self.box.elements=[]
def shuffle_btn(self):
btn_txt_ = ["add_front", "add_back"]
random.shuffle(btn_txt_)
self.btn1.text = btn_txt_[0]
self.btn2.text = btn_txt_[1]
v = random.randint(0,9)
self.inp1.text= "%d"%v
# bind func
for btn in [self.btn1,self.btn2]:
# clear old bind firstly
for cb in btn._cb:
btn.funbind("on_press",cb)
btn._cb = []
# The following codes give wrong result
#foo_ = getattr(self.box, btn.text)
#foo = lambda elem, instance: foo_(elem)
#call_back_ = partial(foo, self.inp1.text)
# The following codes give correct result
if btn.text=="add_back":
call_back_ = partial(self.box.add_back, self.inp1.text)
elif btn.text =="add_front":
call_back_ = partial(self.box.add_front, self.inp1.text)
btn._cb.append(call_back_)
btn.fbind('on_press',call_back_)
print("bind to",call_back_)
class TestApp(App):
def build(self):
return TestWin()
if __name__ == '__main__':
TestApp().run()
Edit:
Modifying the following codes may give me correct result,
but I wonder why
# The following codes give wrong result
#foo_ = getattr(self.box, btn.text)
#foo = lambda elem, instance: foo_(elem)
#call_back_ = partial(foo, self.inp1.text)
# The following codes give correct result
if btn.text=="add_back":
call_back_ = partial(self.box.add_back, self.inp1.text)
elif btn.text =="add_front":
call_back_ = partial(self.box.add_front, self.inp1.text)
# The following doesn't work
# foo_ = getattr(self.box, btn.text)
# foo = lambda elem, instance: foo_(elem)
# call_back_ = partial(foo, self.inp1.text)
#this is ok
call_back_ = partial(getattr(self.box, btn.text), self.inp1.text)

How do I increase the width of the dropdown list within a spinner?

How do I increase the width of the dropdown list within a spinner? My button as you can see in the photo is small, and the values ​​in my drop-down list do not appear completely. I did a little research on the internet and I could see that some people say that I need to create a class spinner and add these features. But I don't know how to do this. Could someone show me a code example of how I do this?
main.kv (simplified code)
...
Spinner:
id: spinnerrpi
size_hint: None, None
width: '30sp'
height: '30sp'
border: 0,0,0,0
background_normal: 'seta1.png'
background_down: 'seta2.png'
values: "Branco Neve","Banco Gelo","Amarelo","Rosa Claro","Bege"
on_text: app.spinner_rpiso(spinnerrpi.text)
...
main.py (simplified code)
...
class PrimeiraJanela(Screen):
pass
class GerenciadorDeJanelas(ScreenManager):
pass
class MainApp(App):
texture = ObjectProperty()
def build(self):
self.title = 'MyApp'
self.texture = Image(source = 'wave.png').texture
sm = ScreenManager()
sm.add_widget(PrimeiraJanela(name = 'primeira'))
sm.current = 'primeira'
return sm
def spinner_rpiso(self, value):
if (value=='Branco Neve'):
self.root.get_screen('primeira').ids.rpi.text = str('0.90')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
if (value=='Banco Gelo'):
self.root.get_screen('primeira').ids.rpi.text = str('0.70')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
if (value=='Amarelo'):
self.root.get_screen('primeira').ids.rpi.text = str('0.70')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
if (value=='Rosa Claro'):
self.root.get_screen('primeira').ids.rpi.text = str('0.60')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
if (value=='Bege'):
self.root.get_screen('primeira').ids.rpi.text = str('0.60')
self.root.get_screen('primeira').ids.spinnerrpi.text = ''
def exit(self):
App.get_running_app().stop()
aplicativo = MainApp()
aplicativo.run()
Here is an extension of Spinner that honors an option_width property:
class SpinnerWithOptionWidth(Spinner):
option_width = NumericProperty(0) # the new property
def __init__(self, **kwargs):
self.invisible_attacher = None
super(SpinnerWithOptionWidth, self).__init__(**kwargs)
def on_is_open(self, instance, value):
# This method is modified from Spinner
attacher = self
if value:
if self.option_width > 0:
if self.invisible_attacher is None:
# The DropDown is the same width as the widget it attaches to
# so make an invisible widget with the desired width
self.invisible_attacher = Widget(opacity=0, size_hint=(None, None))
self.add_widget(self.invisible_attacher)
self.invisible_attacher.pos = (self.center_x - self.option_width/2, self.y)
self.invisible_attacher.size = (self.option_width, self.height)
attacher = self.invisible_attacher
# open th DropDown
self._dropdown.open(attacher)
else:
if self._dropdown.attach_to:
if self.invisible_attacher:
self.remove_widget(self.invisible_attacher)
self.invisible_attacher = None
self._dropdown.dismiss()

How to add new widget in GridLayout without resizing older widgets?

I have added GridLayout to ScrollView, and I'm adding widgets in GridLayout dynamically from python program.Instead of using more space of window it's resizing height of older widgets.What am i doing wrong here?
I tried putting BoxLayout inside GridLayout but it's not working.
I also tried to add widgets directly to ScrollView but i found out ScrollView only supports one widget.
My kv code:
<Downloading>:
my_grid: mygrid
GridLayout:
cols: 1
size_hint_y : None
hight: self.minimum_height
id: mygrid
My python code:
class Downloading(ScrollView):
set_text = ObjectProperty()
my_grid = ObjectProperty()
def __init__(self, select, link, path, username, password):
self.select = select
self.link = link
self.path = path
self.username = username
self.password = password
self.p_bar = []
self.stat = []
self.parent_conn, self.child_conn = Pipe()
p = Process(target=main, args=(self.child_conn, self.select,
self.link, self.path,
self.username, self.password))
p.start()
super().__init__()
self.event = Clock.schedule_interval(self.download_GUI, 0.1)
def newFile(self, title):
# self.newId = "stat" + str(len(self.p_bar) + 1)
self.stat.append(Label(text=''))
self.p_bar.append(ProgressBar())
self.my_grid.add_widget(Label(text=title))
self.my_grid.add_widget(self.stat[-1])
self.my_grid.add_widget(self.p_bar[-1])
def download_GUI(self, a):
temp = self.parent_conn.recv()
print(temp)
if temp == "new":
self.downloading = True
return
if self.downloading:
self.newFile(temp)
self.downloading = False
return
if type(temp) == type({}):
self.complete = temp['complete']
if not self.complete:
status = "{0}//{1} # {2} ETA: {3}".format(temp['dl_size'],
temp['total_size'],temp['speed'],temp['eta'])
self.stat[-1].text = status
self.p_bar[-1].value = temp['progress']
return
if temp == "end":
self.event.cancel()
Clock.schedule_once(exit, 3)
I believe you just need to set the height for each widget that you add to your GridLayout. For example:
self.my_grid.add_widget(Label(text=title, size_hint=(1, None), height=50))
You might need to do the same for the other widgets you are adding. The GridLayout may give the initially added widgets more space than that, but will not squeeze them any any tighter than your specified height.
After applying following changes to kv file
<Downloading>:
size: self.size
my_grid: mygrid
GridLayout:
cols: 1
size_hint_y : None
row_default_height: '25dp'
row_force_default: True
id: mygrid
And adding this line in the end of constructor (__init__), it is working as expected.
self.my_grid.bind(minimum_height=self.my_grid.setter('height'))

Kivy Custom Widget Sizing within a Layout

I'm trying to use a custom widget within a layout in another application, but instead of scaling correctly within the layout, it's taking up the entire screen. Here's the .kv files, broken down to the relevant parts (I took out all the callbacks, which function as expected):
<NodeWidget>
BoxLayout:
orientation: 'horizontal'
size:root.size
ToggleButton:
size_hint: 0.10345, 1
group: 'Ends'
background_normal: 'Left_Up.png'
background_down: 'Left_Down.png'
Button:
size_hint: 0.801724, 1
background_normal: 'Center_Button_Up.png'
background_down: 'Center_Button_Down.png'
ToggleButton:
size_hint: 0.094827, 1
group: 'Ends'
background_normal: 'Right_Up.png'
background_down: 'Right_Down.png'
Below is the .kv file for the separate app that's importing the class from above and using it:
<NodeEditorWidget>:
GridLayout:
size: root.size
cols: 3
Label:
text: 'Test 1'
NodeWidget:
...
Label:
text: 'Test 3'
Here's what I see when I run the test app now:
Thanks in advance for your help!
So I solved this by moving it back to straight python, no .kv files. Here's the code:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ListProperty, StringProperty, BooleanProperty, ObjectProperty, NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton
#This widget contains two buttons
#The buttons each expose two events
#Names still aren't functioning
class NodeWidget(BoxLayout):
#We expose events:
#on_press when the central button is pressed
#on_release when the central button is released
#on_press_right when the right button is pressed
#on_press_left when the left button is pressed
press = BooleanProperty(False)
release = BooleanProperty(False)
press_right = BooleanProperty(False)
press_left = BooleanProperty(False)
release_right = BooleanProperty(False)
release_left = BooleanProperty(False)
#Properties exposed to set internal properties
title = StringProperty('')
left_background_normal = StringProperty('Left_Up.png')
left_background_down = StringProperty('Left_Down.png')
center_background_normal = StringProperty('Center_Button_Up.png')
center_background_down = StringProperty('Center_Button_Down.png')
right_background_normal = StringProperty('Right_Up.png')
right_background_down = StringProperty('Right_Down.png')
font_name = StringProperty('Roboto')
markup = BooleanProperty(True)
padding_x = NumericProperty(0)
padding_y = NumericProperty(0)
#Which node is active
active_left = BooleanProperty(False)
active_right = BooleanProperty(False)
#Object Properties for internal elements
left_button = ObjectProperty(None)
center_button = ObjectProperty(None)
right_button = ObjectProperty(None)
def __init__(self, **kwargs):
super(NodeWidget, self).__init__(**kwargs)
center = Button()
left = ToggleButton()
right = ToggleButton()
left.size_hint = (0.10345, 1)
center.size_hint = (0.801724, 1)
right.size_hint = (0.094827, 1)
left.background_normal = 'Left_Up.png'
left.background_down = 'Left_Down.png'
center.background_normal = 'Center_Button_Up.png'
center.background_down = 'Center_Button_Down.png'
right.background_normal = 'Right_Up.png'
right.background_down = 'Right_Down.png'
left.group = 'ends'
right.group = 'ends'
left.bind(on_press=self.ActivateNode_Left)
left.bind(on_release=self.ReleaseNode_Left)
right.bind(on_press=self.ActivateNode_Right)
right.bind(on_release=self.ReleaseNode_Right)
center.bind(on_press=self.PressNode)
center.bind(on_release=self.ReleaseNode)
self.left_button = left
self.center_button = center
self.right_button = right
self.bind(title=self.SetTitle)
self.bind(left_background_normal=self.SetLeftBackgroundNormal)
self.bind(left_background_down=self.SetLeftBackgroundDown)
self.bind(center_background_normal=self.SetCenterBackgroundNormal)
self.bind(center_background_down=self.SetCenterBackgroundDown)
self.bind(right_background_normal=self.SetRightBackgroundNormal)
self.bind(right_background_down=self.SetRightBackgroundDown)
self.bind(font_name=self.SetFontName)
self.bind(markup=self.SetMarkup)
self.bind(padding_x=self.SetPaddingX)
self.bind(padding_y=self.SetPaddingY)
self.add_widget(left)
self.add_widget(center)
self.add_widget(right)
def ActivateNode_Left(self, *args):
if self.active_left == False:
self.active_left = True
self.active_right = False
else:
self.active_left = False
self.active_right = True
if self.press_left == True:
self.press_left = False
else:
self.press_left = True
def ActivateNode_Right(self, *args):
if self.active_right == False:
self.active_right = True
self.active_left = False
else:
self.active_right = False
self.active_left = True
if self.press_right == True:
self.press_right = False
else:
self.press_right = True
def ReleaseNode_Left(self, *args):
if self.release_left == True:
self.release_left == False
else:
self.release_left == True
def ReleaseNode_Right(self, *args):
if self.release_right == True:
self.release_right == False
else:
self.release_right == True
def PressNode(self, *args):
if self.press == True:
self.press = False
else:
self.press = True
def ReleaseNode(self, *args):
if self.release == True:
self.release = False
else:
self.release = True
def SetTitle(self, *args):
self.center_button.text = self.title
def SetLeftBackgroundDown(self, *args):
self.left_button.background_down = self.left_background_down
def SetLeftBackgroundNormal(self, *args):
self.left_button.background_normal = self.left_background_normal
def SetCenterBackgroundDown(self, *args):
self.center_button.background_down = self.center_background_down
def SetCenterBackgroundNormal(self, *args):
self.center_button.background_normal = self.center_background_normal
def SetRightBackgroundDown(self, *args):
self.right_button.background_down = self.right_background_down
def SetRightBackgroundNormal(self, *args):
self.right_button.background_normal = self.right_background_normal
def SetFontName(self, *args):
self.center_button.font_name = self.font_name
def SetMarkup(self, *args):
self.center_button.markup = self.markup
def SetPaddingX(self, *args):
self.center_button.padding_x = self.padding_x
def SetPaddingY(self, *args):
self.center_button.padding_y = self.padding_y

Categories

Resources