Central themes: Event binding and canvas instructions in kv language
This tutorial directly follows on from the previous, so start by retrieving the previous code, as below:
main.py:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.slider import Slider
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.slider import Slider
from kivy.uix.widget import Widget
from kivy.graphics import Rectangle, Color, Line
from random import random
class DrawingWidget(Widget):
def __init__(self):
super(DrawingWidget, self).__init__()
with self.canvas:
Color(1, 1, 1, 1)
self.rect = Rectangle(size=self.size,
pos=self.pos)
self.bind(pos=self.update_rectangle,
size=self.update_rectangle)
def update_rectangle(self, instance, value):
self.rect.pos = self.pos
self.rect.size = self.size
def on_touch_down(self, touch):
super(DrawingWidget, self).on_touch_down(touch)
if not self.collide_point(*touch.pos):
return
with self.canvas:
Color(random(), random(), random())
self.line = Line(points=[touch.pos[0], touch.pos[1]], width=2)
def on_touch_move(self, touch):
if not self.collide_point(*touch.pos):
return
self.line.points = self.line.points + [touch.pos[0], touch.pos[1]]
class Interface(BoxLayout):
pass
class DrawingApp(App):
def build(self):
root_widget = Interface()
return root_widget
DrawingApp().run()
drawing.kv:
<Interface>:
orientation: 'vertical'
DrawingWidget:
Slider:
min: 0
max: 1
value: 0.5
size_hint_y: None
height: 80
Slider:
min: 0
max: 1
value: 0.5
size_hint_y: None
height: 80
Slider:
min: 0
max: 1
value: 0.5
size_hint_y: None
height: 80
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: 80
Label:
text: 'output colour:'
Widget:
The first thing to do is draw the coloured Rectangle that the final Widget uses to display an output colour, and for this we need to know how to draw canvas instructions in kv language. The syntax is as below:
Widget:
canvas:
Color:
rgb: 0, 1, 0 # using a fixed colour for now
Rectangle:
size: self.size
pos: self.pos
Run the code, and you’ll see another of kv language’s most important features; automatic event binding. In the original Python code of tutorial 7 we needed an extra .bind(...) call to make the be updated to always be placed within its Widget. In kv language this is not necessary, the dependency on self.size and self.pos is automatically detected, and a binding automatically created!
This is also the generic syntax for canvas instructions; first add canvas: (or canvas.before or canvas.after), then, indent by 4 spaces, and add canvas instructions much like you would Widgets. However, note that canvas instructions are not widgets.
The only thing now missing from the original Python interface implementation in tutorial 7 is having the Sliders automatically update the output colour rectangle. Change the <Interface>: rule to the following:
<Interface>:
orientation: 'vertical'
DrawingWidget:
Slider:
id: red_slider
min: 0
max: 1
value: 0.5
size_hint_y: None
height: 80
Slider:
id: green_slider
min: 0
max: 1
value: 0.5
size_hint_y: None
height: 80
Slider:
id: blue_slider
min: 0
max: 1
value: 0.5
size_hint_y: None
height: 80
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: 80
Label:
text: 'output colour:'
Widget:
canvas:
Color:
rgb: red_slider.value, green_slider.value, blue_slider.value
Rectangle:
size: self.size
pos: self.pos
There are actually only two changes here; we gave each Slider an id declaration, and in the canvas Color referred to the sliders with this name. Giving a widget an id is just like naming it in Python so that you can refer to it elsewhere.
Thanks to kv’s automatic binding, this is all we need to do to have the Color update automatically whenever a slider value changes. Run the code, and you should see that things work exactly as they did in the original Python interface.
We can finish this tutorial with a couple of extra kv conveniences. First, just as we added an automatically updating Rectangle in the Widget kv, we can do the same for the background of the DrawingWidget. Delete the __init__ and update_rectangle methods in the Python DrawingWidget code, and add a new rule in the kv file:
<DrawingWidget>:
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Second, you might have noticed that there’s a lot of code duplication in each of the Slider rules - we set the same min, max, initial value`, ``size_hint_y` and height for every one. As is normal in Python, it would be natural to abstract this in a new class, so as to set each value only once. You can probably already see how to do this with what we’ve learned so far (make a new class YourSlider(Slider): in the Python and add a new <YourSlider>: rule in the kv), but I’ll note that you can even do this entirely in kv:
<ColourSlider@Slider>:
min: 0
max: 1
value: 0.5
size_hint_y: None
height: 80
<Interface>:
orientation: 'vertical'
DrawingWidget:
ColourSlider:
id: red_slider
ColourSlider:
id: green_slider
ColourSlider:
id: blue_slider
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: 80
Label:
text: 'output colour:'
Widget:
canvas:
Color:
rgb: red_slider.value, green_slider.value, blue_slider.value
Rectangle:
size: self.size
pos: self.pos
The new <ColourSlider@Slider>: rule defines a dynamic class, a Python class kv rule without a corresponding Python code definition. This is convenient if you want to do something repeatedly only in kv, and never access it from Python.
At this point, we’ve reached feature parity with the original Python code, and seen all the basics of kv language. In the next tutorial we’ll finish off the original purpose of all these sliders; letting the user set the colour of line that is drawn by the DrawingWidget.
Full code
main.py:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.slider import Slider
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.slider import Slider
from kivy.uix.widget import Widget
from kivy.graphics import Rectangle, Color, Line
from random import random
class DrawingWidget(Widget):
def on_touch_down(self, touch):
super(DrawingWidget, self).on_touch_down(touch)
if not self.collide_point(*touch.pos):
return
with self.canvas:
Color(random(), random(), random())
self.line = Line(points=[touch.pos[0], touch.pos[1]], width=2)
def on_touch_move(self, touch):
if not self.collide_point(*touch.pos):
return
self.line.points = self.line.points + [touch.pos[0], touch.pos[1]]
class Interface(BoxLayout):
pass
class DrawingApp(App):
def build(self):
root_widget = Interface()
return root_widget
DrawingApp().run()
drawing.kv:
<DrawingWidget>:
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
<ColourSlider@Slider>:
min: 0
max: 1
value: 0.5
size_hint_y: None
height: 80
<Interface>:
orientation: 'vertical'
DrawingWidget:
ColourSlider:
id: red_slider
ColourSlider:
id: green_slider
ColourSlider:
id: blue_slider
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: 80
Label:
text: 'output colour:'
Widget:
canvas:
Color:
rgb: red_slider.value, green_slider.value, blue_slider.value
Rectangle:
size: self.size
pos: self.pos