Kivy: Work with events

Viewed 1562

In class Palete is is button, which is binded with method for removing contents of canvas in class Paint. I think, that solving is using of custom events. I have'nt got experincies with events and Ido'nt underestand description in kivy manual. I need to preserve structure of class. Can You help to me?

from kivy.app import App
from kivy.uix.widget import Widget

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button

from kivy.graphics import Color, Rectangle
from functools import partial


class Paint(Widget):

    def __init__(self, palete,**kwargs):
        # make sure we aren't overriding any important functionality
        super(Paint, self).__init__(**kwargs)
        self.palete = palete
        self.contents = []

    def on_touch_down(self, touch):
        color = self.palete.act_col
        with self.canvas:
            Color(color[0], color[1], color[2])
            sqr = Rectangle(pos = (touch.x, touch.y), size=(20, 40))
            self.contents.append(sqr)

class Palete(BoxLayout):
    def __init__(self, **kwargs):
        super(Palete, self).__init__(**kwargs)

        self.act_col= (1, 0, 0)

        self.licol =[]
        self.licol.append((1, 0, 0))
        self.licol.append((0, 1, 0))

        self.layout = BoxLayout(size_hint=(1, None), height=50)

        for i in range(0, len(self.licol)):
            but = Button( id = str(i))
            col = self.licol[i]
            but.background_normal = ''
            but.background_color = (col[0], col[1], col[2], 1)


            act = partial(self.SetColor, but.id)
            but.bind(on_press=act)
            self. layout.add_widget(but)

        but = Button(text="<--",on_press = self.ClearContents)
        self.layout.add_widget(but)

    def SetColor(self,_col, h):
        ind = int(_col)
        self.act_col = self.licol[ind]
        pass

    def ClearContents(self, obj):
        if len(self.contents)!= 0:
            self.canvas.remove(self.contents[-1])
            self.contents = self.contents[:-1]


class MyPaintApp(App):

    def build(self):
        root = BoxLayout(orientation='vertical')
        self.palete = Palete()

        self.paint =Paint(self.palete)
        root.add_widget(self.paint)

        root.add_widget(self.palete.layout)

        return root


if __name__ == '__main__':
    MyPaintApp().run()
2 Answers

canvas andcontents are attributes of the Paint class and you are trying to access it from the Palate class.

What you can do, if you do not want to alter the structure, is pass the reference of your Paint class to the Palete class:

from kivy.app import App
from kivy.uix.widget import Widget

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button

from kivy.graphics import Color, Rectangle
from functools import partial


class Paint(Widget):

    def __init__(self, palete,**kwargs):
        # make sure we aren't overriding any important functionality
        super(Paint, self).__init__(**kwargs)
        self.palete = palete
        self.palete.paint = self
        self.contents = []


    def on_touch_down(self, touch):
        color = self.palete.act_col
        with self.canvas:
            Color(color[0], color[1], color[2])
            sqr = Rectangle(pos = (touch.x, touch.y), size=(20, 40))
            self.contents.append(sqr)

class Palete(BoxLayout):
    def __init__(self, **kwargs):
        super(Palete, self).__init__(**kwargs)
        self.act_col= (1, 0, 0)
        self.licol =[]
        self.licol.append((1, 0, 0))
        self.licol.append((0, 1, 0))
        self.paint = None

        self.layout = BoxLayout(size_hint=(1, None), height=50)

        for i in range(0, len(self.licol)):
            but = Button( id = str(i))
            col = self.licol[i]
            but.background_normal = ''
            but.background_color = (col[0], col[1], col[2], 1)


            act = partial(self.SetColor, but.id)
            but.bind(on_press=act)
            self. layout.add_widget(but)

        but = Button(text="<--",on_press = self.ClearContents)
        self.layout.add_widget(but)

    def SetColor(self,_col, h):
        ind = int(_col)
        self.act_col = self.licol[ind]
        pass

    def ClearContents(self, obj):
        if not self.paint:
            return
        if self.paint.contents:
            self.paint.canvas.remove(self.paint.contents.pop())


class MyPaintApp(App):

    def build(self):
        root = BoxLayout(orientation='vertical')
        self.palete = Palete()

        self.paint = Paint(self.palete)
        root.add_widget(self.paint)

        root.add_widget(self.palete.layout)

        return root


if __name__ == '__main__':
    MyPaintApp().run()

I think you should remove the objects from the class that implements the canvas to preserve the encapsulation. Another option is to move the Callback to the Paint class and bind the on_press event to it (from the Paint class):

from kivy.app import App
from kivy.uix.widget import Widget

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button

from kivy.graphics import Color, Rectangle
from functools import partial


class Paint(Widget):

    def __init__(self, palete,**kwargs):
        # make sure we aren't overriding any important functionality
        super(Paint, self).__init__(**kwargs)
        self.palete = palete
        self.contents = []
        self.palete.clear_btn.bind(on_press = self.clear_contents)

    def on_touch_down(self, touch):
        color = self.palete.act_col
        with self.canvas:
            Color(color[0], color[1], color[2])
            sqr = Rectangle(pos = (touch.x, touch.y), size=(20, 40))
            self.contents.append(sqr)

    def clear_contents(self, obj):
        if self.contents:
            self.canvas.remove(self.contents.pop())

class Palete(BoxLayout):
    def __init__(self, **kwargs):
        super(Palete, self).__init__(**kwargs)
        self.act_col= (1, 0, 0)
        self.licol =[]
        self.licol.append((1, 0, 0))
        self.licol.append((0, 1, 0))
        self.paint = None

        self.layout = BoxLayout(size_hint=(1, None), height=50)

        for i in range(0, len(self.licol)):
            but = Button( id = str(i))
            col = self.licol[i]
            but.background_normal = ''
            but.background_color = (col[0], col[1], col[2], 1)


            act = partial(self.set_color, but.id)
            but.bind(on_press=act)
            self. layout.add_widget(but)

        self.clear_btn = Button(text="<--")
        self.layout.add_widget(self.clear_btn)

    def set_color(self,_col, h):
        ind = int(_col)
        self.act_col = self.licol[ind]


class MyPaintApp(App):

    def build(self):
        root = BoxLayout(orientation='vertical')
        self.palete = Palete()

        self.paint = Paint(self.palete)
        root.add_widget(self.paint)

        root.add_widget(self.palete.layout)

        return root


if __name__ == '__main__':
    MyPaintApp().run()

Use list.pop instead of slicing to remove the last item from the list. It's more readable and don`t make a copy of the list every time.

enter image description here

Please refer to the detail explanations and example.

Overload method __init__

def __init__(self, **kwargs):
    super(Paint, self).__init__(**kwargs)

We overload the method __init__() so as to add widgets and to define their behavior. One should not forget to call super in order to implement the functionality of the original class being overloaded. Also note that it is good practice not to omit the **kwargs while calling super, as they are sometimes used internally.

Paint Class

1. Added Kivy Properties

Since you will be accessing palette and contents, we declared the following:

from kivy.properties import ObjectProperty, ListProperty


class Paint(Widget):
    palette = ObjectProperty(None)
    contents = ListProperty([])

2. on_touch_down Event

By default, touch events are dispatched to all currently displayed widgets. This means that widgets receive the touch event whether it occurs within their physical area or not. Kivy dispatches the touch event to all widgets and lets them decide how to react to them. If you only want to respond to touch events inside the widget/button, you have to check it using self.collide_point method. When it collides, you should get only one widget/button.

In the following snippet, we override the on_touch_down() method of the Widget class. Here, we check for collision of the touch with our widget.

If the touch falls inside our widget, we create a square on the canvas and return True, indicating that we have consumed the touch and don't want it to propagate any further.

Finally, if the touch falls outside our widget, we call the original event using super(Paint, self).on_touch_down(touch) and return the result. This allows the touch event propagation to continue as it would normally have occurred.

def on_touch_down(self, touch):
    if self.collide_point(*touch.pos):
        color = self.palette.act_col
        with self.canvas:
            Color(color[0], color[1], color[2])
            sqr = Rectangle(pos=(touch.x, touch.y), size=(20, 40))
            self.contents.append(sqr)
        return True
    return super(Paint, self).on_touch_down(touch)

Palette Class

3. act_col - ListProperty

Since act_col will be accessed in class Paint, we declared it as a Kivy ListProperty.

act_col = ListProperty([1, 0, 0])

4. Nested BoxLayout - Removed

Since Palette class is a BoxLayout, we don't need a nested BoxLayout. Therefore it is removed.

5. on_press Event

The only argument passed to the callback is the object i.e. button, which we have bound to.

def SetColor(self, btn):
    self.act_col = self.licol[int(btn.id)]

def ClearContents(self, btn):

MyPaintApp Class

6. build method

Instead of putting everything in the build method of class MyPaintApp, we have moved them into class Painting. We have also declared ObjectProperty to hook up to class palette and class paint.

class Painting(BoxLayout): palette = ObjectProperty(None) paint = ObjectProperty(None)

def __init__(self, **kwargs):
    super(Painting, self).__init__(**kwargs)
    self.orientation = "vertical"

    self.palette = Palette()
    self.paint = Paint(palette=self.palette)

    self.add_widget(self.paint)
    self.add_widget(self.palette)

Example

main.py

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.graphics import Color, Rectangle
from kivy.properties import ObjectProperty, ListProperty


class Paint(Widget):
    palette = ObjectProperty(None)
    contents = ListProperty([])

    def __init__(self, palette, **kwargs):
        # make sure we aren't overriding any important functionality
        super(Paint, self).__init__(**kwargs)
        self.palette = palette

    def on_touch_down(self, touch):
        """
        We override the on_touch_down() method of the Widget class. Here, we check for
        collision of the touch with our widget.

        If the touch falls inside our widget, we create a square on the canvas and return True,
        indicating that we have consumed the touch and don't want it to propagate any further.

        Finally, if the touch falls outside our widget, we call the original event using super(...)
        and return the result. This allows the touch event propagation to continue as it would
        normally have occurred.

        :param touch:
        :return:
        """
        if self.collide_point(*touch.pos):
            color = self.palette.act_col
            with self.canvas:
                Color(color[0], color[1], color[2])
                sqr = Rectangle(pos=(touch.x, touch.y), size=(20, 40))
                self.contents.append(sqr)
            return True
        return super(Paint, self).on_touch_down(touch)


class Palette(BoxLayout):
    act_col = ListProperty([1, 0, 0])

    def __init__(self, **kwargs):
        super(Palette, self).__init__(**kwargs)
        self.licol = []
        self.licol.append((1, 0, 0))
        self.licol.append((0, 1, 0))

        self.size_hint = (1, None)
        self.height = 50

        for i in range(0, len(self.licol)):
            but = Button(id=str(i))
            col = self.licol[i]
            but.background_normal = ''
            but.background_color = (col[0], col[1], col[2], 1)
            but.bind(on_press=self.SetColor)
            self.add_widget(but)

        but = Button(text="<--", on_press=self.ClearContents)
        self.add_widget(but)

    # The only argument passed to the callback is the
    # object (i.e. button) which we have bound to
    def SetColor(self, btn):
        self.act_col = self.licol[int(btn.id)]

    # The only argument passed to the callback is the
    # object (i.e. button) which we have bound to
    def ClearContents(self, btn):
        if len(self.parent.paint.contents) != 0:
        # list.pop() - removes and returns the last item in the list
            self.parent.paint.canvas.remove(self.parent.paint.contents.pop())


class Painting(BoxLayout):
    palette = ObjectProperty(None)
    paint = ObjectProperty(None)

    def __init__(self, **kwargs):
        super(Painting, self).__init__(**kwargs)
        self.orientation = "vertical"

        self.palette = Palette()
        self.paint = Paint(palette=self.palette)

        self.add_widget(self.paint)
        self.add_widget(self.palette)


class MyPaintApp(App):

    def build(self):
        return Painting()


if __name__ == '__main__':
    MyPaintApp().run()

Output

Figure 1 - App Startup enter image description here enter image description here enter image description here

Related