Kivy Custom Widget Sizing within a Layout - python

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

Related

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)

Kivy callbacks for instanced buttons/widgets?

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()

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()

MDSpinner from kivymd doesn't work for me as expected

In my project , I use the MDSpinner class from KivyMD to show loading status of a widget. The code is as follows:
KV file( relevant part)
MDSpinner:
id: spinner
size_hint: None, None
size: dp(46), dp(46)
pos_hint: { 'center_x': .5, 'center_y': .9 }
active: True if app.processing else False
I have a Boolean property processing and the corresponding on_processing function as follows
class Mark2MarketApp(MDApp):
processing = BooleanProperty(defaultValue=False)
def __init__(self, **kwargs):
super().__init__(**kwargs)
start = time.time()
Builder.load_file("RootWidget.kv")
self.screen_manager = ScreenManager()
addMainScreen(self.screen_manager, self)
self.screen_manager.current = "Main"
self.current = "Main"
self.processing = False
self.manager_open = False
self.filePath = ""
self.symbol = []
self.qty = []
self.cost = []
self.side = []
Window.bind(on_keyboard=self.events)
self.file_manager = MDFileManager(
exit_manager=self.exit_manager,
select_path=self.select_path,
)
self.file_manager.ext = ['.csv', '.CSV', '.xlsx', '.XLSX']
self.popup = self.get_popup()
self.no_data_popup = self.no_data_popup()
end = time.time()
print('elapsed time for startup is %d seconds ' % (end - start))
def on_processing(self, instance, value):
print('instance, value', instance, value)
I set the processing variable before and after code that does some heavy lifting as follows
def gain_loss(self):
self.processing = True
try:
gl = self.screen_manager.get_screen('GainLoss')
gl.add_widgets()
except ScreenManagerException:
gl = GainLossScreen(self.screen_manager, name='GainLoss')
self.screen_manager.add_widget(gl)
gl.add_widgets()
if len(tryout.product_dict) == 0:
self.no_data_popup.open()
else:
self.screen_manager.current = 'GainLoss'
self.processing = False
However, the spinner doesnt get activated. I debugged the MDSpinner source code and it does call the activation.start method when processing is set to True and stops the animation when processing is set to false. But the spinner just doesnt spin!.
Interestingly, if I comment out the last line self.processing = False, then the spinner starts spinning .
Thanks in advance

Updating Progress Bar displayed in Popup in Kivy

How do you update a progress bar that is displayed in Kivy. In the following example I get AttributeError: 'super' object has no attribute '__getattr__'. The issue is in the following line
self.ids.progress.value = value
I can see why since the progressBar widget is in <LoadingPopup> and not <MainScreen> but after trying several different things I can't reference the progressBar widget in <LoadingPopup> from the do_update method.
Thanks in advance
import threading
from functools import partial
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import StringProperty
from kivy.uix.spinner import Spinner
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.popup import Popup
Builder.load_string('''
<LoadingPopup>:
title: "Popup"
size_hint: None, None
size: 400, 400
auto_dismiss: False
BoxLayout:
orientation: "vertical"
ProgressBar:
id: progress
size_hint: (1.0, 0.06)
<MainScreen>:
BoxLayout:
orientation: 'vertical'
Spinner:
id: first
text: ' First Number'
values: ['1','2','3','4','5','6','7','8','9']
Spinner:
id: second
text: ' Second Number'
values: ['1','2','3','4','5','6','7','8','9']
Label:
id: result
text: ' Result'
color: 0,0,0,1
Button:
id: res
on_press: root.doit_in_thread(first.text,second.text)
text: 'Multiply'
''')
class LoadingPopup(Popup):
def __init__(self, obj, **kwargs):
super(LoadingPopup, self).__init__(**kwargs)
class MainScreen(FloatLayout):
changet = StringProperty()
def __init__(self, **kwargs):
super(MainScreen, self).__init__(**kwargs)
def doit_in_thread(self, fir, sec):
popup = LoadingPopup(self)
popup.open()
threading.Thread(target=partial(self.onMul, fir, sec, popup)).start()
def do_update(self, value, text, *args):
self.ids.progress.value = value
self.ids.result.text = text
def onMul(self,fir,sec, popup):
a = (int(fir)*int(sec))
print(a)
b = 0
old_value = 0
endRange = 1000000
for i in range(endRange):
progress = int(((i+1)*100)/endRange)
if progress != old_value and progress % 5 == 0:
text = str(b*(int(fir)*int(sec)))
Clock.schedule_once(partial(self.do_update, progress, text))
old_value = progress
b+=1
popup.dismiss()
class TestApp(App):
def build(self):
return MainScreen()
if __name__ == "__main__":
TestApp().run()
One way to do it, is to keep a reference of the popup so you can address it later:
def __init__(self, **kwargs):
super(MainScreen, self).__init__(**kwargs)
self.popup = LoadingPopup(self)
def doit_in_thread(self, fir, sec):
self.popup.open()
threading.Thread(target=partial(self.onMul, fir, sec, self.popup)).start()
def do_update(self, value, text, *args):
self.popup.ids.progress.value = value
self.ids.result.text = text
Python Code
Replace all references of popup with self.popup so that you can access it in the methods inside class MainScreen.
Replace self.ids.progress.value with self.popup.ids.progress.value
Since a = (int(fir)*int(sec), replace str(b*(int(fir)*int(sec))) with str(b * a)
Don't need to pass popup to onMul method, access it using self.popup
Snippet
class MainScreen(FloatLayout):
changet = StringProperty()
def doit_in_thread(self, fir, sec):
self.popup = LoadingPopup(self)
self.popup.open()
threading.Thread(target=partial(self.onMul, fir, sec)).start()
def do_update(self, value, text, *args):
self.popup.ids.progress.value = value
self.ids.result.text = text
def onMul(self, fir, sec):
a = (int(fir)*int(sec))
print(a)
b = 0
old_value = 0
endRange = 1000000
for i in range(endRange):
progress = int(((i+1)*100)/endRange)
if progress != old_value and progress % 5 == 0:
text = str(b*(int(fir)*int(sec)))
# text = str(b * a)
print("\ttext=", text)
Clock.schedule_once(partial(self.do_update, progress, text))
old_value = progress
b+=1
self.popup.dismiss()
kv file
The result is not visible because the default background colour of Label widget is black, and the text colour for result is also set to black, color: 0,0,0,1. Therefore, remove color: 0,0,0,1.
Snippet
Label:
id: result
text: ' Result'
Output

Categories

Resources