Hello I'm relatively new to kivy. So far doing basic stuff has been relatively straightforward but this has stumped me. I'm making an app that needs to dynamically add rectangular canvas items to a grid in a scrollview. Since I'm doing this I need to create the scrollview in python and not in the .kv file. How can I do this so that the size of the rectangles will be the same as the window size upon resizing the windows?
.py file:
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.graphics import Line,Rectangle
from kivy.uix.carousel import Carousel
from kivy.uix.scrollview import ScrollView
from kivy.core.window import Window
class Scroll(ScrollView):
def __init__(self, **kwargs):
super(Scroll, self).__init__(**kwargs)
layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
layout.bind(minimum_height=layout.setter('height'))
# Make sure the height is such that there is something to scroll.
for i in range(100):
SkillStat = RelativeLayout(pos=(0,0), height=100, size_hint_y=None, size_hint_x=self.width)
with SkillStat.canvas:
Rectangle(pos=self.pos,size=(self.width, 90))
layout.add_widget(SkillStat)
self.add_widget(layout)
pass
pass
class Sheet(Carousel):
pass
class SheetApp(App):
def build(self):
return Sheet()
if __name__ == '__main__':
SheetApp().run()
.kv file:
# file name: Sheet.kv
<Sheet>:
RelativeLayout:
Scroll:
size_hint:(1,1)
The two main problem in your code are:
You are doing all your size and position setting of your SkillStat and its canvas in an __init__() method. In an __init__() method of a widget, the position of the widget is always (0,0), and the size is (100, 100). Those properties are not set to real values until the widget is actually drawn.
You are doing all this in python instead of in kv. In kv, bindings are created for many properties that you set, and get automatically updated. If you do your widget setup in python, you must provide those bindings yourself.
Here is a modified version of your Scroll class and a new MyRelativeLayout class that handle those bindings:
class MyRelativeLayout(RelativeLayout):
def adjust_size(self, *args):
self.rect.size = self.size # set the size of the Rectangle
class Scroll(ScrollView):
def __init__(self, **kwargs):
super(Scroll, self).__init__(**kwargs)
layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
layout.bind(minimum_height=layout.setter('height'))
# Make sure the height is such that there is something to scroll.
for i in range(100):
SkillStat = MyRelativeLayout(pos=(0,0), height=100, size_hint=(1.0, None))
with SkillStat.canvas.before:
SkillStat.rect = Rectangle()
SkillStat.bind(size=SkillStat.adjust_size)
layout.add_widget(SkillStat)
self.add_widget(layout)
Note the SkillStat.bind() call to create the needed bindings, and the Rectangle is saved as SkillStat.rect in each MyRelativeLayout instance. Those bindings will get triggered as soon as the SkillStat gets displayed, so the initial pos and size of the Rectangle are not needed.
EDIT: Setting the pos of the Rectangle in a binding was probably causing problems. The default pos of the Rectangle is (0,0), which is what it should always be. So, we only need to adjust the size of the Rectangle. I have removed the binding for pos.
Solution
Create a class with inheritance from RelativeLayout.
Update or remove the instructions you have added to a canvas by using bind function.
Snippets
class CustomLayout(RelativeLayout):
def __init__(self, **kwargs):
super(CustomLayout, self).__init__(**kwargs)
with self.canvas:
self.rect = Rectangle(pos=self.pos, size=(self.width, 90))
self.bind(pos=self.update_rect, size=self.update_rect)
def update_rect(self, *args):
self.rect.pos = self.pos
self.rect.size = self.size
Example
main.py
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.graphics import Line, Rectangle
from kivy.uix.carousel import Carousel
from kivy.uix.scrollview import ScrollView
from kivy.core.window import Window
from kivy.lang import Builder
class CustomLayout(RelativeLayout):
def __init__(self, **kwargs):
super(CustomLayout, self).__init__(**kwargs)
with self.canvas:
self.rect = Rectangle(pos=self.pos, size=(self.width, 90))
self.bind(pos=self.update_rect, size=self.update_rect)
def update_rect(self, *args):
self.rect.pos = self.pos
self.rect.size = self.size
class Scroll(ScrollView):
def __init__(self, **kwargs):
super(Scroll, self).__init__(**kwargs)
layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
layout.bind(minimum_height=layout.setter('height'))
# Make sure the height is such that there is something to scroll.
for i in range(100):
SkillStat = CustomLayout(pos=(0, 0), height=100, size_hint_y=None, size_hint_x=self.width)
layout.add_widget(SkillStat)
self.add_widget(layout)
class Sheet(Carousel):
pass
Builder.load_file('main.kv')
class SheetApp(App):
def build(self):
return Sheet()
if __name__ == '__main__':
SheetApp().run()
main.kv
#:kivy 1.11.0
<Sheet>:
RelativeLayout:
Scroll:
size_hint:(1,1)
bar_width: 10
effect_cls: "ScrollEffect"
scroll_type: ['bars']
bar_color: [1, 0, 0, 1] # red color
bar_inactive_color: [0, 0, 1, 1] # blue color
Output
Related
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.
I have two button widgets. The Button1 was rotated and the Button2 wasn't. I'm trying to test if the two button widgets will collide.
Here is my code:
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.graphics import *
from kivy.logger import Logger
class RotateMe(Button):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
with self.canvas.before:
PushMatrix()
self.rot = Rotate(angle= 45, origin= self.center)
with self.canvas.after:
PopMatrix()
class Main(Widget):
def __init__(self):
super(Main, self).__init__()
self.size = Window.size
self.rotatethis = RotateMe(text= "Button1", center= self.center)
self.add_widget(self.rotatethis)
self.somebutton = Button()
self.somebutton.text = "Button2"
self.somebutton.right = self.center_x - 60
self.somebutton.y = self.center_y
self.add_widget(self.somebutton)
if self.somebutton.collide_widget(self.rotatethis):
Logger.info("The buttons collided! Much happiness")
else:
Logger.info("The buttons didn't collide. Such sadness.")
class TestApp(App):
def build(self):
return Main()
TestApp().run()
This is how it looked when I ran my code.
I can clearly see from the output (the image) that the two buttons collided but when I checked the log , it says that the Button Widgets didn't collide. It seems that the widget position of Button1 didn't change/rotate.
I tried to change the position of the Button2 to see at which point it collides with Button1. And I found out that the Widget position of Button1 didn't change.
Like this:
The white square at the back of Button1 is the actual position of the Button1 widget.
Is there a way to rotate the position of Button1 widget?
UPDATE: I tried to use the ScatterLayout as suggested in here.
Here is my updated code:
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.graphics import *
from kivy.logger import Logger
from kivy.uix.scatterlayout import ScatterLayout
from kivy.uix.floatlayout import FloatLayout
class RotateMe(ScatterLayout):
def __init__(self):
ScatterLayout.__init__(self)
self.size_hint = (None, None)
self.size = (100, 100)
self.rotation = 45
self.do_rotation = False
self.add_widget(Button(text= "Button1"))
class Main(FloatLayout):
def __init__(self):
super(Main, self).__init__()
self.size = Window.size
self.rotatethis = RotateMe()
self.rotatethis.center = self.center
self.add_widget(self.rotatethis)
self.button2 = Button(text= "Button2")
self.button2.size_hint = (None, None)
self.button2.size = (100, 100)
self.button2.right = self.center_x - 60
self.button2.y = self.center_y
self.add_widget(self.button2)
if self.button2.collide_widget(self.rotatethis):
Logger.info("The buttons collided! Much happiness")
else:
Logger.info("The buttons didn't collide. Such sadness.")
class TestApp(App):
def build(self):
return Main()
TestApp().run()
Now the buttons were collided but when I try to change the position of the Button2, like this:
self.button2.y = self.center_y + 45
The output is like this:
Looking at the output (image), the two buttons' widgets doesn't seem to collide with each other but when I look at the log, it says that they collided with each other.
It turns out that the widget size of Button1 is this (the colored teal background):
I want to draw circle in center of FloatLayout. With my knowledges I obtained only default values for this. Why circle in showed code isn't red? Can You explain me process for obtaining necessary coordinates, please?
import kivy
from kivy.config import Config
kivy.config.Config.set('graphics','resizable', False)
from kivy.app import App
from kivy.graphics import Color, Ellipse
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
class Scene(FloatLayout):
def __init__(self, **kwargs):
super(Scene, self).__init__(**kwargs)
def draw_circle(self):
with self.canvas:
Color=(1,0,0)
circ = Ellipse(pos = (self.center_x, self.center_y), size=(20,20))
def on_touch_down(self, touch):
pass
class Game(BoxLayout):
def __init__ (self,**kwargs):
super(Game, self).__init__(**kwargs)
self.orientation = 'vertical'
but1 = Button(text = 'button 1')
self.add_widget(but1)
self.scene = Scene()
self.add_widget(self.scene)
class TestApp(App):
def build(self):
game = Game()
game.scene.draw_circle()
return game
if __name__ == '__main__':
TestApp().run()
You should define the size of your float layout when you create it.
self.scene = Scene(size=(300, 300))
Then your circle should be at the center of the FloatLayout dimensions.
I also think FloatLayout is better used with size_hint and pos_hint instead of fixed coordinates.
You can call draw_circle with Clock to make sure the layout is completely initiated first.
Then make sure to create your color like this Color(1, 0, 0). Not Color = ()
from kivy.config import Config
Config.set('graphics','resizable', False)
from kivy.app import App
from kivy.graphics import Color, Ellipse
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.clock import Clock
class Scene(FloatLayout):
def draw_circle(self, dt):
with self.canvas:
Color(1,0,0)
circ = Ellipse(pos = (self.center_x, self.center_y), size=(20,20))
class Game(BoxLayout):
def __init__ (self,**kwargs):
super(Game, self).__init__(**kwargs)
self.orientation = 'vertical'
but1 = Button(text = 'button 1')
self.add_widget(but1)
self.scene = Scene()
self.add_widget(self.scene)
class TestApp(App):
def build(self):
game = Game()
Clock.schedule_once(game.scene.draw_circle) # call draw_circle on next frame
return game
if __name__ == '__main__':
TestApp().run()
Using Kivy, how can you change the background colour of a label in Python and not by using the Kv language?
I have tried this:
with self.canvas:
Color(1., 0, 0)
Rectangle(pos=(10, 10), size=(500, 500))
However this just creates a red square on the bottom left of the screen. It would be really useful if there was a alternative to the idea above and I could change the background colour of a label using python and not kv language.
Well, Rectangle position and size should rather match Label position and size:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.graphics import Color, Rectangle
class MyApp(App):
def build(self):
layout = FloatLayout()
label = Label(
text='test',
pos=(20, 20),
size=(180, 100),
size_hint=(None, None))
with label.canvas:
Color(0, 1, 0, 0.25)
Rectangle(pos=label.pos, size=label.size)
layout.add_widget(label)
return layout
if __name__ == '__main__':
MyApp().run()
Version with auto-adjusting:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.graphics import Color, Rectangle
class MyLabel(Label):
def on_size(self, *args):
self.canvas.before.clear()
with self.canvas.before:
Color(0, 1, 0, 0.25)
Rectangle(pos=self.pos, size=self.size)
class MyApp(App):
def build(self):
layout = FloatLayout()
label = MyLabel(
text='test',
pos=(20, 20),
size_hint=(0.5, 0.5))
layout.add_widget(label)
return layout
if __name__ == '__main__':
MyApp().run()
Just to share my trick after nearly 5 years of your question is that instead of using canvas, you can just simply use Button: with disabled: True. Because button is a clickable label, then why don't we reuse it's background (This to make you code as simplest as possible)
If you are wonder how to remove it's darken color, you can see here.
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.stacklayout import StackLayout
class mylayout(StackLayout):
def __init__(self, **kwargs):
super(mylayout, self).__init__(**kwargs)
for i in range(10):
btn = Button(text=str(i), width=40, size_hint=(None, 0.15), orientation= 'lr-bt')
self.add_widget(btn)
class NameApp(App):
def build(self):
mL = mylayout()
return mL
if __name__ == "__main__":
NameApp().run()
I have attempted to change the orientation here, but the orientation displayed on the app is still as if it were default
Orientation is a property of the layout, not of the widgets it contains. You can use
self.orientation = "lr-bt"
in the __init__ function to assign the property to the layout. You could also assign it in the appropriate .kv file if you use that.