The Goal
I want to create a small script that adds buttons dynamically, but still lets me perform functions on specific ones via root.
My Methods
I made this script.
It is capable of dynamically adding large buttons along the top. Each of these buttons slightly changes its own color when pressed.
It has two smalls buttons at the bottom. The first button dynamically adds new large buttons along the top.
The second button resets the color of the first large button on the top.
My Code
#!/usr/bin/env python3
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
Builder.load_string('''
<RootWidget>:
Button:
text: 'Add'
size_hint: (None, None)
size: (40, 40)
pos: (40, 40)
group: 'action'
on_press: root.createNextTarget()
Button:
text: 'res'
size_hint: (None, None)
size: (40, 40)
pos: (100, 40)
group: 'action'
on_press: root.resetTarget()
''')
class RootWidget(FloatLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
#note: self.ids isn't populated yet. I guess we can't use it yet.
self.createNextTarget()
def resetTarget(self):
f_target = self.ids['targetbutton0']
f_target.background_color = (1.0, 1.0, 1.0, 1.0)
return True
def countTargets(self):
return [str(x.__class__.__name__) for x in self.children if x != None].count('TargetButton')
def createNextTarget(self):
f_nextButton = TargetButton(id="targetbutton"+str(self.countTargets()),
size_hint=(None, None),
pos=(80 + (10 + 60) * self.countTargets(), 100),
size=(60, 60),
background_normal = '',
background_color = (1, 1, 1, 1),
group = 'target')
self.add_widget(f_nextButton)
f_nextButton.bind(on_press=TargetButton.lowerAllRGB)
class TargetButton(Button):
def __init__(self, **kwargs):
super(TargetButton, self).__init__(**kwargs)
def lowerAllRGB(self):
f_r, f_g, f_b, f_a = self.background_color
if f_r >= 0.1: f_r = f_r - 0.1
if f_g >= 0.1: f_g = f_g - 0.1
if f_b >= 0.1: f_b = f_b - 0.1
self.background_color = (f_r, f_g, f_b, f_a)
return True
class TestApp(App):
def build(self):
return RootWidget()
def on_stop(self):
print("TestApp.on_stop: finishing", self.root.ids)
if __name__ == '__main__':
TestApp().run()
The Problem
If I try to hit the reset button (that accesses the widget via root.ids), I get the error: KeyError: 'targetbutton0'
After finding a post about a similar problem, I thought root.ids just wouldn't work during RootWidget.__init__.
But when I use the button to add buttons after RootWidget.__init__ is finished, TestApp.on_stop() still prints:
TestApp.on_stop: finishing {}
So root.ids is still empty, and doesn't seem to include any dynamically added widgets despite me assigning an id attribute to each of them.
My questions to you
Given the way I am dynamically adding widgets, is using root.ids just worthless for my purposes?
Is there a decent way for me to access my widgets via id? I saw another question here asking something similar. But it didn't answer my question about dynamically added widgets.
Question 1 - root.ids / self.ids
Given the way I am dynamically adding widgets, is using root.ids just
worthless for my purposes?
Answer
id assigned to dynamically added widgets are not store in self.ids or root.ids. Therefore, you cannot access dynamically added widgets using self.ids['targetbutton0'] or self.ids.targetbutton0. If you do that, you will get a KeyError because it is not found in self.ids which is a dictionary type property.
When your kv file is parsed, Kivy collects all the widgets tagged with id’s and places them in this self.ids dictionary type property.
Note:
These type of id (i.e. id assigned to dynamically created widget) is deprecated and will be removed in a future Kivy version.
[WARNING] Deprecated property "<StringProperty name=id>" of object "<kivy.uix.button.Button object at 0x7feeec0968d0>" has been set, it will be removed in a future version
Question 2
Is there a decent way for me to access my widgets via id?
Solution
You could create your own list of ids of dictionary type property.
Snippets
from kivy.properties import DictProperty
class RootWidget(FloatLayout):
dynamic_ids = DictProperty({}) # declare class attribute, dynamic_ids
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.createNextTarget()
def resetTarget(self):
f_target = self.dynamic_ids['targetbutton0']
f_target.background_color = (0.0, 1.0, 1.0, 1.0) # cyan colour
return True
...
def createNextTarget(self):
id = "targetbutton" + str(self.countTargets())
f_nextButton = TargetButton(id=id,
size_hint=(None, None),
pos=(80 + (10 + 60) * self.countTargets(), 100),
size=(60, 60),
background_normal = '',
background_color = (1, 1, 1, 1), # white colour
group = 'target')
self.add_widget(f_nextButton)
self.dynamic_ids[id] = f_nextButton
f_nextButton.bind(on_press=TargetButton.lowerAllRGB)
Output
Kivy 2.0 throws an error when you use id='value' when dynamically creating kivy widgets from python main file. But by using weak references you achieve the following success.
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
import weakref
class MyLayout(Widget):
def use_weakref_id_to_replace_text(self, *args):
self.ids.AddUserTextBox.text = "shotta"
print("released and renamed the text to shotta")
def add_textinpt_with_weak_ref_dyn_id(self, *args):
print('Pressed and added text input box')
textinput = TextInput(pos=(380,380),text='123')
self.add_widget(textinput)
# We'll use a weak ref to add our dynamic id
self.ids['AddUserTextBox'] = weakref.ref(textinput)
class MdApp(App):
def build(self):
root = MyLayout()
Btn = Button(size=(250,250), pos=(100,100),text="Dynamic id assign n use id to rename")
Btn.bind(on_release=root.use_weakref_id_to_replace_text,on_press=root.add_textinpt_with_weak_ref_dyn_id)
root.add_widget(Btn)
return root
if __name__ == '__main__':
MdApp().run()
Related
I was making a program similiar to exel.
And then I ran into a issue.
StackLayout stacks good, but because the size of inputs is static it leave a blank space in some cases.
I try to do like size=(self.width/5, self.height/5).
If someone saw how exel look now that there are more inputs of the screen this is why I use ScrollLayout and I want only 5 inputs in one row(user can change it in display settings(I will do it then creating rest of UI))
Here is what I had tried for now.
main.py:
from kivy.metrics import dp
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.stacklayout import StackLayout
from kivy.uix.textinput import TextInput
class EditorGrid(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
size = dp(self.width / 5)
print(size)
for i in range(0, 100):
b = TextInput(text=str(i + 1), size_hint=(None, None), size=(dp(size), dp(size)))
self.add_widget(b)
def on_size(self, *args):
self.size = dp(self.width/5)
print(self.size)
pass
class pxApp(App):
pass
if __name__ == "__main__":
pxApp().run()
kivy file:
Scrolling:
<Scrolling#ScrollView>
do_scroll_x: True
do_scroll_y: True
EditorGrid:
size_hint: 1, None
height: self.minimum_height
<EditorGrid>:
There is next problem that in init window size is 100x100.
I recommend using setters in binding under kv script. If you want to apply it in python, you could use a binding function for each button.
b = TextInput(.......) #omit size_hint and size attributes
#the following function is called every time EditorGrid(self) size_hint changes when the function is bind
#The function could be defined in a outer scope.
#Look at kivy callback functions to better understand the syntax
binding_function = lambda ins, *a: setattr(ins, 'size_hint', tuple((dim/5 for dim in self.size_hint))
b.bind(size_hint=binding_function)
I have a screen with a StackLayout. The first row of the stack includes a textinput and a "+" button which is meant to add another identical row below the actual one in a loop (i.e. another textinput with another "add" button). Then there is a "Save" button, which is supposed to be always at the end of the stack.
The dictionary is supposed to later grab the input from the text field, when pressing the save button, but this should not be relevant to my problem.
There are two problems with my code:
First, when I press the "+" button in a row, the button that gets highlighted is not this button itself, but the one in the row below (e.g. if there are three rows and I press the button in the second row, the button in the third row is highlighted)
Second and most importantly, starting from the second row onwards, each press of the "+" button adds a new row above the actual one, rather than below it.
I am aware that kivy assigns reverse order to the widgets (i.e. the one added last will be drawn first) and that is why I am manually assigning indexes to the new rows. However, I cannot achieve the desired behavior.
Here is a minimal working version:
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.stacklayout import StackLayout
from kivy.uix.screenmanager import ScreenManager, Screen
class AddWindow(Screen):
def __init__(self, **kwargs):
super(AddWindow, self).__init__(**kwargs)
self.grid = StackLayout()
self.grid.pos_hint = {"x":0.05,"top":0.8}
self.grid.size_hint = (0.9,None)
self.add_widget(self.grid)
self.i = 1
self.n = 1
self.inputs = {}
self.ing1 = TextInput(size_hint=(0.9,'0.3sp'))
self.grid.add_widget(self.ing1)
self.inputs['0'] = 'ing1'
self.addIng = Button(text="+", size_hint=(0.1,'0.3sp'))
self.addIng.bind(on_press=self.addIngredient)
self.grid.add_widget(self.addIng)
self.doneButton = Button(text="Save")
self.grid.add_widget(self.doneButton, index=0)
def addIngredient (self, instance):
self.ing = TextInput(size_hint=(0.9,'0.3sp'))
self.inputs[self.i] = 'ing{}'.format(self.i+1)
self.grid.add_widget(self.ing, index=self.n+1)
self.addNext = Button(text="+", size_hint=(0.1,'0.3sp'))
self.addNext.bind(on_press=self.addIngredient)
self.grid.add_widget(self.addNext, index=self.n+2)
self.i += 1
self.n += 2
print(self.inputs)
WMan = ScreenManager()
WMan.add_widget(AddWindow(name='add'))
class RecipApp(App):
def build(self):
return WMan
if __name__ == "__main__":
RecipApp().run()
What am I missing? Here is a screenshot for better clarity:
Screenshot
Here is a brute force method to do what you want by rebuilding the StackLayout each time a + `Button is pressed:
def addIngredient(self, instance):
tmp_children_list = self.grid.children[:]
self.grid.clear_widgets()
for index in range(len(tmp_children_list)-1, -1, -1):
child = tmp_children_list[index]
self.grid.add_widget(child)
if child == instance:
# this is the pressed Button, so add new row after it
self.n += 1
self.ing = TextInput(size_hint=(0.9,'0.3sp'))
self.ing.text = str(self.n) # add text just for identification
self.grid.add_widget(self.ing)
self.addNext = Button(text="+", size_hint=(0.1,'0.3sp'))
self.addNext.bind(on_press=self.addIngredient)
self.grid.add_widget(self.addNext)
Here is a quick and dirty approach , where your problems should be fixed due to a remove and add of the save button. Between those to actions you add a new row before you finally add the save button again. Hope it helps you to adjust your code. The key part is the custom on_press method of the AddButton.
from kivy.app import App
from kivy.uix.stacklayout import StackLayout
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.core.window import Window
class AddButton(Button):
def __init__(self, **kwargs):
super(AddButton, self).__init__(**kwargs)
def on_press(self):
self.parent.remove_widget(self.parent.save_btn)
self.parent.add_widget(TextInput(size_hint=(None, None), size=(Window.width * 0.8 - self.parent.padding[1], Window.height * 0.1)))
self.parent.add_widget(AddButton(text="+", size_hint=(None, None), size=(Window.width * 0.2, Window.height * 0.1)))
self.parent.add_widget(self.parent.save_btn)
class MyApp(App):
def build(self):
Window.size = (400, 600)
layout = StackLayout(orientation="lr-tb", padding=(20, 40))
layout.save_btn = Button(text="Save", size_hint=(None, None),
size=(Window.width - layout.padding[1], Window.height * 0.1))
layout.add_widget(TextInput(size_hint=(None, None), size=(Window.width * 0.8 - layout.padding[1], Window.height * 0.1)))
layout.add_widget(AddButton(text="+", size_hint=(None, None), size=(Window.width * 0.2, Window.height * 0.1)))
layout.add_widget(layout.save_btn)
return layout
MyApp().run()
I am trying to create some themes in kivy. My program currently has 4 classes/screens. I have manged to be able to change the background colour of all the screens if a condition is met. I have tried to change the colour of all the text input's so intead of white, they are black. This is my code so far
Python:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.clock import *
from kivy.core.window import Window
from kivy.uix.textinput import TextInput
class WeatherRoot(ScreenManager, BoxLayout):
pass
class RegisterPage(Screen, BoxLayout):
pass
class WeatherApp(App):
pass
def iii(time):
print("h")
x = 1
if x == 1:
Window.clearcolor = (1, 1, 1, 0)
TextInput.background_color = (1,1,1,0)
pass
if __name__ == "__main__":
Clock.schedule_once(iii)
WeatherApp().run()
Kivy:
WeatherRoot:
<WeatherRoot>:
RegisterPage:
<RegisterPage>:
BoxLayout:
padding: [100, 50, 100, 50]
TextInput:
font_size: 30
TextInput:
font_size: 30
The code to change the TextInput colour isn't working.
The background is white but the text-input isn't black as you can see
How would I be able to change the colour of the TextInput and properties widgets in general (e.g. the text colour of all labels), if a condition is met, with python code?
Thanks in advance
Note - It doesn't work with foreground_color or any of the colour settings like that.
It seems that the concept of object/instance and classes does not differ. By using TextInput.background_color = (1,1,1,0) you add or modify the "background_color" property of the TextInput class, not of the objects/instances created based on the TextInput class.
If you want to modify the property of the TextInputs (a.k.a instances/objects created based on the TextInput class) you must access those objects using the kivy methods through the parents:
def iii(time):
x = 1
if x == 1:
Window.clearcolor = (1, 1, 1, 0)
root = App.get_running_app().root # WeatherRoot instance
screen = root.screens[0] # RegisterPage instance
box_layout = screen.children[0] # BoxLayout instance
for child in box_layout.children: # childs of box_layout
if isinstance(child, TextInput): # verify that the child is a TextInput
child.background_color = (1,1,1,0)
I think you have extrapolated that in the case of Window it behaves similar to TextInput, but they are not the same since the first one is an instance of the WindowBase class, it is not a class unlike the second.
I want to generate a dropdown-list in my second screen managed by Kivys ScreenManager. If I do so, I get this traceback:
...
File "C:/Users/ORANG/PycharmProjects/waldi/playground/cw.py", line 76, in on_text
instance.drop_down.open(instance)
File "C:\Kivy-1.9.0-py2.7-win32-x64\kivy27\kivy\uix\dropdown.py", line 215, in open
'Cannot open a dropdown list on a hidden widget')
kivy.uix.dropdown.DropDownException: Cannot open a dropdown list on a hidden widget
This is the code, which is basically the same as in this
example, just embedded in a screenmanager scenario simple as can be:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.textinput import TextInput
from kivy.properties import ListProperty, StringProperty
import re
from kivy.lang import Builder
Builder.load_string('''
<Lieferant>:
ComboLayout:
Label:
text: 'Label'
ComboEdit:
size_hint: .5, .3
pos_hint: {'center':(.5, .5)}
# `args` is the keyword for arguments passed to `on_text` in kv language
on_text: self.parent.on_text(self, args[1])
''')
class ComboEdit(TextInput):
"""
This class defines a Editable Combo-Box in the traditional sense
that shows it's options
"""
options = ListProperty(('',))
'''
:data:`options` defines the list of options that will be displayed when
touch is released from this widget.
'''
def __init__(self, **kw):
ddn = self.drop_down = DropDown()
ddn.bind(on_select=self.on_select)
super(ComboEdit, self).__init__(**kw)
def on_options(self, instance, value):
ddn = self.drop_down
# clear old options
ddn.clear_widgets()
for option in value:
# create a button for each option
but = Button(text=option,
size_hint_y=None,
height='36sp',
# and make sure the press of the button calls select
# will results in calling `self.on_select`
on_release=lambda btn: ddn.select(btn.text))
ddn.add_widget(but)
def on_select(self, instance, value):
# on selection of Drop down Item... do what you want here
# update text of selection to the edit box
self.text = value
class ComboLayout(BoxLayout):
rtsstr = StringProperty("".join(("Substrate1,,,Substrate1,,,Substrate1,,,",
"Substrate1,,,Substrate1,,,Substrate_coating",
",,,silicon,,,silicon_Substrate,,,substrate_",
"silicon,,,")))
def on_text(self, instance, value):
if value == '':
instance.options = []
else:
match = re.findall("(?<=,{3})(?:(?!,{3}).)*?%s.*?(?=,{3})" % value, \
self.rtsstr, re.IGNORECASE)
# using a set to remove duplicates, if any.
instance.options = list(set(match))
instance.drop_down.open(instance)
class Intro(Screen):
pass
class Lieferant(Screen):
pass
class CWApp(App):
def build(self):
sm = ScreenManager()
sm.add_widget(Intro(name='Intro'))
sm.add_widget(Lieferant(name='Lieferant'))
return sm
if __name__ == '__main__':
CWApp().run()
Is it possible to combine them? How would you do this?
This code is running if I just comment this line out, which adds a screen before the screen with the dropdown:
sm.add_widget(Intro(name='Intro'))
Ok - for this question I got the "Tumbleweed Badget" - LOL
I admit it's a very special one and makes obvious, how much I am a beginner here. So I asked another question based on the same problem and spend some effort to make it easier to understand the code and to reproduce it. This paid off! Look here for the solution if you tumble once into that kind of problem: Is it a bug in Kivy? DropDown + ScreenManager not working as expected
I want to create something that changes its general layout when the orientation, or size, of the window is changed. In the below example, I was trying to do this with 3 buttons. But it fails, and complains that the buttons already have a parent when I try to add them to a layout. Why do they already have a parent? I never added them to the AdaptWidget...
More importantly, how should I achieve what I want to achieve?
Code below:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
class AdaptWidget(BoxLayoutWrite failed: Broken pipe
def __init__(self, **kw):
super(AdaptWidget, self).__init__(**kw)
self.but1 = Button(text='but1')
self.but2 = Button(text='but2')
self.but3 = Button(text='but3')
self.layout = None
def on_size(self, *args):
self.clear_widgets()
if self.size[0] > self.size[1]:
self.layout = BoxLayout(orientation='horizontal')
self.but1.size_hint = 0.7, 1
self.layout.add_widget(self.but1)
vert = BoxLayout(orientation='vertical')
self.layout.add_widget(vert)
self.but2.size_hint = (1,0.5)
self.but3.size_hint = (1,0.5)
vert.add_widget(self.but2)
vert.add_widget(self.but3)
else:
self.layout = BoxLayout(orientation='vertical')
self.but1.size_hint = 1, 0.7
self.layout.add_widget(self.but1)
horiz = BoxLayout(oreintation='horizontal')
self.layout.add_widget(horiz)
self.but2.size_hint = 0.5, 1
self.but3.size_hint = 0.5, 1
horiz.add_widget(self.but2)
horiz.add_widget(self.but3)
self.add_widget(self.layout)
class TestLayoutApp(App):
def build(self):
return AdaptWidget()
if __name__ == '__main__':
TestLayoutApp().run()
Every time the size of your AdaptWidget changes, your on_size callback runs - but the callback doesn't remove any widgets from their parents, so on_size will always crash the second time it is called. You can always ensure a widget is removed from its parent:
if widget.parent is not None:
widget.parent.remove_widget(widget)
EDIT:
3 days ago I wrote this "answer", although I could not make the above suggestions work.
But now someone here: https://groups.google.com/forum/#!topic/kivy-users/Z5fjt5tNys8
Showed me how to do it.
So here comes the solution:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
class AdaptWidget(BoxLayout):
def __init__(self, **kw):
super(AdaptWidget, self).__init__(**kw)
# but1 = Button(id='but1', text='button1')
# but1 = ObjectProperty(None)
# self.but1 = ObjectProperty(None)
self.add_widget(Button(id='but1', text='button1'))
self.add_widget(Button(id='but2', text='button2'))
self.add_widget(Button(id='but3', text='button3'))
def on_size(self, *args):
if self.width > self.height:
self.orientation = 'horizontal'
# self.child.but1.size_hint = (0.7, 1)
# but1.size_hint = (0.7, 1)
# self.ids.but1.size_hint = (0.7, 1)
self.children[2].size_hint = (0.7, 1)
self.children[1].size_hint = (0.5, 1)
self.children[0].size_hint = (0.5, 1)
else:
self.orientation = 'vertical'
self.children[2].size_hint = (1, 0.7)
self.children[1].size_hint = (1, 0.5)
self.children[0].size_hint = (1, 0.5)
class TestLayoutApp(App):
def build(self):
return AdaptWidget()
if __name__ == '__main__':
TestLayoutApp().run()
Note that the solution above is much cleaner than the original code. No need to remove or clear any widgets. You just change their attributes.
EDIT:
Part of the problem was that the 'buildozer.spec'-file contained this:
# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = portrait
Instead of this:
# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = all