Kivy label texture_size is 0,0 after changing text - python

I have a label in my .kv file:
Label:
id: question
font_size: 40
center_x: root.center_x
center_y: root.center_y
I have the following in my root widget class:
class MainScreen(Widget):
question = ObjectProperty(None)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_question()
def set_question(self):
self.question.text = "placeholder"
print(self.question.texture_size)
def on_question(self,instance, value):
print(value.texture_size)
This returns [0,0] twice. I was under the impression that on_question would fire when the self.question.text changed, and that the value parameter would be the updated label, and thus with the correct texture_size. However, this is not the case and it appears that either texture_size is not updated, or that the print statement in on_question is called before texture_size is set.
How do I access texture_size after it is set?

This is an interesting problem as the docs recommend to bind on texture_size explicitly, which not worked for me. Furthermore a manuelly forced refresh with texture_update() did not work as well. So the only way I was able to get the texture size was (as already mentioned in the comments) with a Clock event. Here is my approach, maybe it helps you with your problem.
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.lang.builder import Builder
from kivy.uix.label import Label
from kivy.clock import Clock
kv_string = """
<MainWidget>:
question: question_id
Label:
id: question_id
font_size: 40
center_x: root.center_x
center_y: root.center_y
"""
Builder.load_string(kv_string)
class MainWidget(Widget):
question = ObjectProperty(None)
def __init__(self, **kwargs):
super(MainWidget, self).__init__(**kwargs)
self.question.bind(texture_size=self.on_question)
self.set_question()
def set_question(self):
self.question.text = "placeholder"
#print(self.question.texture_size)
def on_question(self, instance, value):
if isinstance(value, Label):
Clock.schedule_once(self.get_texture_size, 0)
def get_texture_size(self, dt):
print(self.question.texture_size)
class MyApp(App):
def build(self):
main = MainWidget()
return main
MyApp().run()

Related

Referencing Layout from a different class kivy

EDIT: so this is a very simple version of what it is like in my app but i think you get the point. Basically i want to destroy the Buttons created in the for loop with the Button on the destroywidgets screen.
.kv:
MainWindow:
<MainWindow>
FloatLayout:
size_hint: 1, .1
Button:
text:"next screen"
size_hint:.1,1
pos_hint:{"x": 0, "y": 0}
on_release: app.root.current = "destroywidgets"
Button:
text:"laodwidgets"
on_release: root.create_widgets()
size_hint:.1, 1
pos_hint:{"x": .5, "y": 0}
.py:
from kivy.app import App
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.scrollview import ScrollView
class MainWindow(Screen):
name = "mainwindow"
def __init__(self, **kwargs):
super(MainWindow, self).__init__()
self.scrl_view_1 = ScrollView(
size_hint_y=.85,
pos_hint={"x": 0, "y": .15},
do_scroll_x=False,
do_scroll_y=True,
size_hint_x=1
)
self.scrl_child_1 = GridLayout(
size_hint_x=1,
size_hint_y=None,
cols=2,
height=1000,
row_default_height=150,
row_force_default=True
)
self.add_widget(self.scrl_view_1)
self.scrl_view_1.add_widget(self.scrl_child_1)
def create_widgets(self):
print("creating widgets")
for i in range(0, 6):
btn = Button(
text=str(i)
)
self.scrl_child_1.add_widget(btn)
print("added")
class DestroyWidgets(Screen):
name = "destroywidgets"
def __init__(self, **kwargs):
super(DestroyWidgets, self).__init__()
btn_destroy_widgets = Button(
text="Destroy children of Mainwindow",
#some on release function to clear all children from scrl_child_1 in Mainwindow
)
self.add_widget(btn_destroy_widgets)
class ShoppingList(App):
def build(self):
self.sm = ScreenManager()
self.sm.add_widget(MainWindow(name="mainwindow"))
self.sm.add_widget(DestroyWidgets(name="destroywidgets"))
return self.sm
if __name__ == "__main__":
main_app = ShoppingList()
main_app.run()
So I have to reference a GridLayout which is inside of a ScrollView created in class A in class B. Since you cannot give Layouts an ID in python code and reference it with self.ids i can't figure out how to do it. I tried suggestions from another post with the weakref.ref method as example but i couldn't get it to work. The whole point is that i have to destroy all children of Layouts from other classes in a function somehow.
Heres just a little snippet of my code which i think will be enough. If you need more just write me. Thanks for all the help in advance!
class SelfMadePlans(Screen):
name = "selfmadeplans"
def __init__(self, **kwargs):
super(SelfMadePlans, self).__init__()
self.scrl_view_2 = ScrollView(
size_hint_y=.85,
pos_hint={"x": 0, "y": 0},
do_scroll_x=False,
do_scroll_y=True,
size_hint_x=1
)
self.scrl_child_2 = GridLayout(
size_hint_x=1,
size_hint_y=None,
cols=3,
height=20000,
row_default_height=150,
row_force_default=True,
)
self.add_widget(self.scrl_view_2)
self.scrl_view_2.add_widget(self.scrl_child_2)
and then something in another class like:
class B:
def destroy_children(self):
MDApp.get_running_app().sm.get_screen("selfmadeplans").ids.scrl_child_2.children.clear()
First of all you must pass var. no. of kwargs in __init__ in order to use and get all the default functionalities.
...
def __init__(self, **kwargs):
super(MainWindow, self).__init__(**kwargs)
...
etc.
Next to access certain screen from ScreenManager you can use method get_screen as follows,
...
btn_destroy_widgets = Button(
text="Destroy children of Mainwindow",
#some on release function to clear all children from scrl_child_1 in Mainwindow
)
btn_destroy_widgets.bind(on_release = self.destroy_widgets_on_main)
self.add_widget(btn_destroy_widgets)
def destroy_widgets_on_main(self, *args):
main_window = self.manager.get_screen("mainwindow")
main_window.scrl_child_1.clear_widgets()

Displaying a background image in Kivy

I just started to learn Kivy so I am still familiar with its functionalities.
I am trying to put an image as a background to my app main page. This is what I did:
class Prin(BoxLayout):
def __init__(self,**kwargs):
super(Prin,self).__init__(**kwargs)
layout = BoxLayout(orientation='vertical')
with self.canvas:
self.rect = Rectangle(source='test.png', pos=layout.center, size=(self.width, self.height))
self.text = Label(text='Press start')
fb = Button(text='Start!', size_hint =(0.5, 0.1), pos_hint ={'center_x':.5, 'y':.5}, padding=(10, 0), on_press=self.start)
layout.add_widget(self.text)
layout.add_widget(fb)
self.add_widget(layout)
def start(self,event):
self.text.text = self.text.text+ "\nNew line"
class MainApp(App):
def build(self):
return Prin()
if __name__ == "__main__":
app = MainApp()
app.run()
The desired behavior is an image covering the whole screen, that's why I've put pos=self.center, size=(self.width, self.height)
This is the output:
So I have two questions:
1/ Why is the image appearing in the left bottom side ? What widget is actually there ? I am supposed to have only a BoxLayout with 2 widgets in a vertical orientation. I don't understand what is there.
2/ What should why put in size and pos to have the desired output ?
I would recommend putting all graphic elements in a .kv file, so there are fewer imports and it looks better.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
KV = ("""
<Prin>
BoxLayout:
orientation: 'vertical'
canvas.before:
Rectangle:
pos: self.pos
size: self.size
source: 'test.png'
Label:
id: label
text: 'TEXT'
Button:
text: 'Start!'
size_hint: None, None
size_hint: 0.5, 0.1
pos_hint: {'center_x': .5, 'center_y': .5}
padding: 10, 0
on_press: root.start()
""")
class Prin(BoxLayout):
Builder.load_string(KV)
def __init__(self, **kwargs):
super(Prin, self).__init__(**kwargs)
def start(self):
self.ids.label.text += "\nNew line"
class MainApp(App):
def build(self):
return Prin()
if __name__ == "__main__":
app = MainApp()
app.run()
If you still want to do this not in the kv file, then the problem is in self.size, by default, this value is [100, 100], only after calling the class and adding it to the main window it changes.
from kivy.core.window import Window
class Prin(BoxLayout):
def __init__(self, **kwargs):
super(Prin, self).__init__(**kwargs)
with self.canvas.before:
Rectangle(source='test.png', pos=self.pos, size=Window.size)
print(self.size) # [100, 100]
...
class MainApp(App):
def build(self):
self.screen = Prin()
return self.screen
def on_start(self):
print(self.screen.size) # [800, 600]
And don't forget about imports when you ask a question, the code should run without any manipulation
In response to your questions:
The image is appearing in that position because pos=layout.center is not a valid position and so instead sets it to a default value ([100, 100] I believe). To fix this, change pos=layout.center to pos=layout.pos
Your size is the default value also! This is getting a little technical but when you initialise your Prin class you are specifying the size of the Rectangle to be the current size of the BoxLayout. However, since it has not been initialised yet, the BoxLayout doesn't yet have a size! Again, Kivy handles this by giving it a default size.
Why are my Buttons and Labels correct?
Kivy automatically binds the children of a BoxLayout to the size and position of the BoxLayout. This binding ensures that when the position and size of the BoxLayout are changed, so too are the widgets within it (https://kivy.org/doc/stable/api-kivy.event.html).
Why doesn't Kivy bind the rectangle?
This has something to do with the canvas. The canvas is a drawing instruction shared by a widget, and not a property of any individual widget. Hence you'll programmatically bind your rectangle to the BoxLayout. (https://kivy.org/doc/stable/api-kivy.graphics.instructions.html)
How do I achieve this binding you speak of?
Two ways. Firstly (preferred), you can define your widgets in the KV language as this will automatically handle any binding you wish. Second, you can create an 'on_size' callback. Something like:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.graphics import Rectangle
class Prin(BoxLayout):
def __init__(self, **kwargs):
super(Prin, self).__init__(**kwargs)
layout = BoxLayout(orientation='vertical')
with self.canvas:
self.rect = Rectangle(source='test.png', pos=layout.pos, size=self.size)
self.text = Label(text='Press start')
fb = Button(text='Start!', size_hint=(0.5, 0.1), pos_hint={'center_x': .5, 'y': .5}, padding=(10, 0),
on_press=self.start)
layout.add_widget(self.text)
layout.add_widget(fb)
self.add_widget(layout)
def start(self, *_):
self.text.text = self.text.text + "\nNew line"
def resize(self, *_):
widgets = self.children[:]
self.canvas.clear()
self.clear_widgets()
with self.canvas:
self.rect = Rectangle(source='test.png', pos=self.pos, size=self.size)
for widget in widgets:
self.add_widget(widget)
on_size = resize
class TestApp(App):
def build(self):
return Prin()
if __name__ == "__main__":
app = TestApp()
app.run()
I just would like to add as a BIG P.S. although the above code solves your problem, it does so in probably the least efficient way imaginable. It is far better to define your widget in the kv file.

How can I change screen using bottom sheet in kivymd?

I have made a simple app in kivymd. But I can not change screen on click on button inside kivymd. Everything works great. But when I click on button then it popup toast also but screen is not changing. What will be changes or better implementation for this?
app.py
from kivymd.app import MDApp
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.lang import Builder
from main_screen_str import helper_string
from kivy.core.window import Window
from kivymd.toast import toast
from kivymd.uix.bottomsheet import MDGridBottomSheet
Window.size = (300, 500)
class MainScreen(Screen):
pass
class SettingsScreen(Screen):
pass
class AboutScreen(Screen):
pass
class MainApp(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sm = ScreenManager()
self.sm.add_widget(MainScreen(name="main_screen"))
self.sm.add_widget(SettingsScreen(name="settings_screen"))
self.sm.add_widget(AboutScreen(name="about_screen"))
self.main_str = Builder.load_string(helper_string)
def build(self):
screen = Screen()
screen.add_widget(self.main_str)
return screen
def callback_for_menu_items(self, *args):
if args[0] == 'Home':
toast(args[0])
self.sm.current = "main_screen"
if args[0] == 'Settings':
toast(args[0])
self.sm.current = "settings_screen"
if args[0] == 'About':
toast(args[0])
self.sm.current = "about_screen"
def show_example_grid_bottom_sheet(self):
self.bottom_sheet_menu = MDGridBottomSheet()
data = {
"Home": "home",
"Settings": "settings",
"About": "information-outline",
}
for item in data.items():
self.bottom_sheet_menu.add_item(
item[0],
lambda x, y=item[0]: self.callback_for_menu_items(y),
icon_src=item[1],
)
self.bottom_sheet_menu.open()
if __name__ == '__main__':
MainApp().run()
This builder string to create screen.
Are there any better solution for this?
builder string
helper_string = """
ScreenManager:
MainScreen:
SettingsScreen:
AboutScreen:
<MainScreen>:
name: 'main_screen'
MDIconButton:
icon: "menu"
theme_text_color: "Custom"
text_color: 1,0,0,1
on_press: app.show_example_grid_bottom_sheet()
<SettingsScreen>:
name: 'settings_screen'
<AboutScreen>:
name: 'about_screen'
"""
In your __init__() method of the App, you are building self.sm with the lines:
self.sm = ScreenManager()
self.sm.add_widget(MainScreen(name="main_screen"))
self.sm.add_widget(SettingsScreen(name="settings_screen"))
self.sm.add_widget(AboutScreen(name="about_screen"))
But self.sm is not used as part of your GUI. So your changes to self.sm has no effect on your GUI. The line following that:
self.main_str = Builder.load_string(helper_string)
basically does exactly the same thing as the previous lines.
Then in your build() method, you are creating a new Screen and adding the self.main_str as a child of that Screen.
While you can have a ScreenManager as a child of a Screen, in your posted example that does not seem to serve any purpose.
Here is a modified version of part of the MainApp that I think will do what you want:
class MainApp(MDApp):
# def __init__(self, **kwargs):
# super().__init__(**kwargs)
# self.sm = ScreenManager()
# self.sm.add_widget(MainScreen(name="main_screen"))
# self.sm.add_widget(SettingsScreen(name="settings_screen"))
# self.sm.add_widget(AboutScreen(name="about_screen"))
#
# self.main_str = Builder.load_string(helper_string)
def build(self):
self.sm = Builder.load_string(helper_string)
return self.sm
# screen = Screen()
# screen.add_widget(self.main_str)
# return screen
The above code greatly simplifies the build() method, eliminates the __init__() method, and now self.sm is actually part of the GUI.
Note that when you load a kv string that has a root node with Builder.load_string(), that root node is created and returned. The lines in your kv string:
ScreenManager:
MainScreen:
SettingsScreen:
AboutScreen:
result in a ScreenManager instance being created along with the three children listed for it, so the code in your __init__() method was duplicating that.

How to set a height to some buttons of GridLayout in Kivy Python?

I'm working in an app with kivy and I have an issue that involve the GridLayout. I have a screen with different rows and I want the buttons of the last row to have always the same height (11,1% of the height of the Screen). I have tried to modify the attribute height in the buttons but doesn't work properly. With size_hint_y works fine , but the fact is that i want to do with height because the screen won't have always the same number of rows (is responsive and it depends of the selections of previous screens). I attach here the code that I've done with the attribute height calculated through the command Window.height/9:
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.core.window import Window
class LoginScreen(GridLayout):
def __init__(self,**kwargs):
super(LoginScreen, self).__init__(**kwargs)
self.cols=2
self.add_widget(Label(text='Subject'))
self.add_widget(Label(text=''))
self.add_widget(Label(text='1'))
self.add_widget(TextInput(multiline=False))
self.add_widget(Label(text='2'))
self.add_widget(TextInput(multiline=False))
self.add_widget(Label(text='3'))
self.add_widget(TextInput(multiline=False))
self.add_widget(Label(text='4'))
self.add_widget(TextInput(multiline=False))
b1=Button(text='Exit',background_color=[0,1,0,1],height=int(Window.height)/9.0) #doesn't work properly
self.add_widget(b1)
b2=Button(text='Run',background_color=[0,1,0,1],height=int(Window.height)/9.0) #doesn't work properly
self.add_widget(b2)
b1.bind(on_press=exit)
class SimpleKivy(App):
def build(self):
return LoginScreen()
if __name__=='__main__':
SimpleKivy().run()
I know it could be done with kivy language in a easier way but for my app is better to do in this way. If anyone knows how to fix this problem I would be very grateful.
If you want a widget in a grid/box layout to have a fixed size, you should set its size_hint to None first. And always use kivy lang at such tasks - no exceptions.
from kivy.app import App
from kivy.uix.screenmanager import Screen
from kivy.lang import Builder
gui = '''
LoginScreen:
GridLayout:
cols: 2
Label:
text: 'Subject'
Label:
Label:
text: '1'
SingleLineTextInput:
Label:
text: '2'
SingleLineTextInput:
Label:
text: '3'
SingleLineTextInput:
Label:
text: '4'
SingleLineTextInput:
GreenButton:
text: 'Exit'
on_press: app.stop()
GreenButton:
text: 'Run'
<SingleLineTextInput#TextInput>:
multiline: False
<GreenButton#Button>:
background_color: 0, 1, 0, 1
size_hint_y: None
height: self.parent.height * 0.111
'''
class LoginScreen(Screen):
pass
class SimpleKivy(App):
def build(self):
return Builder.load_string(gui)
if __name__ == '__main__':
SimpleKivy().run()
Try this
class LoginScreen(GridLayout):
def __init__(self,**kwargs):
super(LoginScreen, self).__init__(**kwargs)
self.cols=2
self.add_widget(Label(text='Subject'))
self.add_widget(Label(text=''))
self.add_widget(Label(text='1'))
self.add_widget(TextInput(multiline=False))
self.add_widget(Label(text='2'))
self.add_widget(TextInput(multiline=False))
self.add_widget(Label(text='3'))
self.add_widget(TextInput(multiline=False))
self.add_widget(Label(text='4'))
self.add_widget(TextInput(multiline=False))
b1=Button(text='Exit',background_color=[0,1,0,1],size_hint_y=None, height=int(Window.height)/8.9)
self.add_widget(b1)
b2=Button(text='Run',background_color=[0,1,0,1],size_hint_y=None, height=int(Window.height)/8.9)
self.add_widget(b2)
b1.bind(on_press=exit)
Edited to change it to be at 11%.
And here one that keeps the button at 11% in response to the window size, where you redraw the Grid Layer whenever the window is resized (as by the bind to 'on_resize').
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
class LoginScreen(GridLayout):
def __init__(self,**kwargs):
super(LoginScreen, self).__init__(**kwargs)
#init and add grid layer
self.cols=2
self.layout = GridLayout(cols=self.cols)
self.add_widget(self.layout)
#function to set the buttons based on the current window size
self.set_content(Window.width, Window.height)
#bind above function to get called whenever the window resizes
Window.bind(on_resize=self.set_content)
def set_content(self, width, height, *args):
#first remove the old sized grid layer
self.remove_widget(self.layout)
#now build a new grid layer with the current size
self.layout =GridLayout(cols=self.cols)
self.layout.add_widget(Label(text='Subject'))
self.layout.add_widget(Label(text=''))
self.layout.add_widget(Label(text='1'))
self.layout.add_widget(TextInput(multiline=False))
self.layout.add_widget(Label(text='2'))
self.layout.add_widget(TextInput(multiline=False))
self.layout.add_widget(Label(text='3'))
self.layout.add_widget(TextInput(multiline=False))
self.layout.add_widget(Label(text='4'))
self.layout.add_widget(TextInput(multiline=False))
b1=Button(text='Exit',background_color=[0,1,0,1],size_hint_y=None, height=int(Window.height)/8.9)
self.layout.add_widget(b1)
b2=Button(text='Run',background_color=[0,1,0,1],size_hint_y=None, height=int(Window.height)/8.9)
self.layout.add_widget(b2)
b1.bind(on_press=exit)
#add the newly sized layer
self.add_widget(self.layout)
class SimpleKivy(App):
def build(self):
return LoginScreen()
if __name__=='__main__':
SimpleKivy().run()

Kivy widget is bigger than it should be

I tried to make by own coockie-clicker, so I creaded an kivy widget and declared an image of an coockie as part of it.
Everytime you click on the wiget, a counter goes up and the number is displayed on an label.
Everything went fine, after I got help here on stack overflow, but now I am faced with the problem, that the widet is to big, so even if I click on the right upper corner, to counter goes up, aldoug I do not clicked on the coockie.
Here is the sourcecode:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.core.window import Window
from kivy.clock import Clock
from kivy.animation import Animation
from kivy.core.text.markup import *
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import NumericProperty
from kivy.properties import StringProperty
Builder.load_string('''
<Root>:
Kecks:
pos: 300, 300
size: 100, 100
<Kecks>:
Image:
pos: root.pos
id: my_image
source: root.weg
Label:
id: my_Label
font_size: 50
text: root.txt
center_x: 345
center_y: 200
''')
class Root(FloatLayout):
def __init__(self, *args, **kwargs):
super(Root, self).__init__(*args, **kwargs)
class Kecks(Widget):
count = NumericProperty(0)
amount = NumericProperty(1)
txt = StringProperty()
level = NumericProperty(1)
weg = StringProperty('piernik.png')
def __init__(self, *args, **kwargs):
super(Kecks, self).__init__(*args, **kwargs)
#self.txt = str(self.count)
Clock.schedule_interval(self.Update, 1/60.)
def Update(self, *args):
self.txt = str(self.count)
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.count += self.amount
class app(App):
def build(self):
Window.clearcolor = (10, 0, 0, 1)
return Root()
if __name__ == "__main__":
app().run()
The problem is you haven't defined your collide_points on which area you want that event to be triggered.
Consider if you want your collide_points on your my_image to trigger on_touch_down event, you need to adjust like this:
def on_touch_down(self, touch):
# to check if touch.pos collides on my_image
if self.ids.my_image.collide_point(*touch.pos):
self.count += self.amount
Also perhaps consider using pos_hint and size_hint as these will help you with consistency with your app running in different devices (size of screen, for instance), rather than using absolute size and/or position.
Hope this helps.

Categories

Resources