Wednesday, June 25, 2014

36. Drawing App 2


This Drawing Application extends Tutorial 34. It shows how we can find child widgets of a class. A class Toolbar is on the left of the screen and the Drawing class represents the rest of the screen. The Drawing class will be the parent of all widgets we draw.




The root class is based on RelativeLayout, but there are Widgets for Toolbar and Drawing, as well as the 4 widgets which will be drawn. These four classes are shown in the next slide. We will use NumericProperty and StringProperty for our variables. Line is also imported, as we need to call some Line canvas instructions from inside this Python file.




These are the 4 widgets which are drawn and dynamically added to the Drawing class. There shapes are defined in the Kivy language.




The Toolbox class uses most of the Kivy language kv file for instructions for Buttons, ToggleButtons and Labels. In the python file, it does only a few things. It defines 3 Properties. Two are for the width and height. Shape is a string which indicates the selected shape. The function on_w, and on_h, are called each time by Kivy whenever the width or height changes, according to user selection in the Toolbar. Whenever the width or height changes, we call a function to erase the box, should it exist. The box is created in the Drawing class.




The Drawing class has to have a lot of code. It is responsible for adding instances of one of the 4 widgets. Besides that we have some functions, like Clear, Undo, and Box. The NumericProperty, sel, indicates if a box has been drawn around the current widget. We show, here, the first half of the function common. It sees if the user has selected one of the 4 shapes. The Drawing class then adds the corresponding widget with the add_widget function. The common function receives the touch argument, so it knows where to place the widget.




This is the second half of the common function. It adds one of the last two shapes depending on the value of shape property of the Toolbox class.




The function common is called from either on_touch_down or on touch move, which Kivy calls if it detects a click or drag. We also pass the touch parameter to the function common.




These functions are called from the Kivy kv file if the corresponding Button is pressed. The function clear will clear all the widgets in the Drawing class. Undo removes the last widget from the Drawing class. Notice the children function will get all the children widgets of the Drawing class. We only remove the top-most widget. There is a try clause, so we will not get an error should there be no widget. In that case, we ask it to do nothing.




The box function is called when the last button in the Toolbar is pressed. The list of widget childs will be in the ch list. Should there be none, the function just exits. But if there are widgets, it sees if the sel property is 0 or 1. If sel is 0, a box is drawn around topmost widget, and then sel is set. If sel is set, and Box button is pressed, it will remove the rectangle, and clear sel, so a new box may be drawn.




The box_resize function is called by on_w and on_h functions. It removes the box should it exist.




The root class is based on RelativeLayout. The application's title is 'Drawing Application 2'. This will be displayed on the window.


# ex36.py

from kivy.uix.widget import Widget
from kivy.uix.relativelayout import RelativeLayout
from kivy.app import App
from kivy.properties import (NumericProperty,
                             StringProperty)
from kivy.graphics import Line

class ColorEllipse(Widget):
    pass

class ColorRectangle(Widget):
    pass

class ColorUpTriangle(Widget):
    pass

class ColorDownTriangle(Widget):
    pass

class ToolBox(Widget):
    w = NumericProperty(20)
    h = NumericProperty(20)
    shape = StringProperty('down:0')

    def on_w(self,*args):
        self.drawing.box_resize(*args)

    def on_h(self,*args):
        self.drawing.box_resize(*args)
    
class Drawing(Widget):
    sel = NumericProperty(0)
    def common(self,touch):
        if touch.x>150:
            if self.toolbox.shape=='down:0':
                ce = ColorEllipse()
                ce.center = touch.pos
                self.add_widget(ce)
            elif self.toolbox.shape=='down:1':
                cr = ColorRectangle()
                cr.center = touch.pos
                self.add_widget(cr)
            elif self.toolbox.shape=='down:2':
                cut = ColorUpTriangle()
                cut.center = touch.pos
                self.add_widget(cut)
            elif self.toolbox.shape=='down:3':
                cdt = ColorDownTriangle()
                cdt.center = touch.pos
                self.add_widget(cdt)
        
    def on_touch_down(self,touch):
        self.common(touch)

    def on_touch_move(self,touch):
        self.common(touch)

    def clear(self,*args):
        self.clear_widgets()

    def undo(self,*args):
        try: self.remove_widget(self.children[0])
        except: pass

    def box(self,*args):
        ch = self.children
        if len(ch) == 0: return
        if self.sel==0:
            with self.canvas:
                self.select=Line(rectangle=(ch[0].x-5,ch[0].y-5,
                                ch[0].width+10,ch[0].height+10),width=2)
                self.sel=1
        else:
            self.canvas.remove(self.select)
            self.sel = 0

    def box_resize(self,*args):
        if self.sel==1:
            self.canvas.remove(self.select)
            self.sel = 0
       
class Ex36(RelativeLayout):
    pass
        
class Ex36App(App):
    def build(self):
        self.title = 'Drawing Application 2'
        return Ex36()

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



In the kv file, random() function is imported from the random module with alias of rnd. The random() function returns a number from 0 to 1. Next two variables are set, sz and off. The kv for the first Widget is added. It gets its size from the w and h properties.




This is the kv for the second widget. It is similar to the first except it is based on the Rectangle canvas instruction.




This widget draws a triangle which points upwards.




This widget draws a triangle which points downwards.




In the root class, attributes are created to point to ToolBox and Drawing class. The Toolbox id is set, and it creates it's own drawing attribute. We could have renamed it, if we had wanted, however that adds only confusion. The Toolbox will only use the first 100 pixels of the screen.




In the canvas instructions, we set the color as reddish, which is applied to the entire toolbox. Then three blue lines are drawn. These lines will delimit the width and height sections as we will see.




The first ToggleButton is drawn. It is the ellipse shape. Here, we can see how the shape string is created. The on_state is called whenever the state changes. The state is a string, it can be 'normal' or 'down'. We add a string ':0' to it. Thus, the two possible values are 'normal:0' or 'down:0'. We also set the initial state as 'down'. There are canvas.after instruction to draw over the ToggleButton.




ToggleButton 2 is for the rectangle shape. Now the state variable is set as 'normal:1' or 'down:1', when the ToggleButton is pressed.




ToggleButton 3 is for the triangle up shape.




ToggleButton 4 is for the triangle down shape.




We have a label indicating this section deals with 'width'. There is an up button, which increments the w property.




The down button decrements w, and finally the label indicates the current value of w .




The Label indicates we are in the 'height' section. The up button increments the value of the h property.




There is a button that when pressed, it will decrement the h property. Finally, the Label indicates the current value of the h property. Both w and h are bounded, between 1 and 49.




The buttons for 'Clear', 'Undo', and 'Box' are created and call the appropriate function in the Drawing class.




The Drawing class itself has very little kv code. It has the attribute toolbox. It is sized to fit the rest of screen, and is colored white.


# ex36.kv
#:import rnd random.random
#:set sz 42
#:set off 4

<ColorEllipse>:
    size_hint: None, None
    size: app.root.toolbox.w,app.root.toolbox.h
    canvas:
        Color:
            rgb: rnd(),rnd(),rnd()
        Ellipse:
            pos: self.pos
            size: self.size

<ColorRectangle>:
    size_hint: None, None
    size: app.root.toolbox.w,app.root.toolbox.h
    canvas:
        Color:
            rgb: rnd(),rnd(),rnd()
        Rectangle:
            pos: self.pos
            size: self.size

<ColorUpTriangle>:
    size_hint: None, None
    size: app.root.toolbox.w,app.root.toolbox.h
    canvas:
        Color:
            rgb: rnd(),rnd(),rnd()
        Triangle:
            points:
                (self.x,self.y, self.x+self.width,self.y,
                self.x+self.width/2,self.y+self.height)

<ColorDownTriangle>:
    size_hint: None, None
    size: app.root.toolbox.w,app.root.toolbox.h
    canvas:
        Color:
            rgb: rnd(),rnd(),rnd()
        Triangle:
            points:
                (self.x,self.y+self.height,
                self.x+self.width,self.y+self.height,
                self.x+self.width/2,self.y)

            
<Ex36>:
    toolbox: toolbox_id
    drawing: drawing_id
    ToolBox:
        id: toolbox_id
        drawing: drawing_id
        size_hint_x: None
        width: 100
        pos: 0,0
        canvas:
            Color:
                rgb: .5,0,0
            Rectangle:
                size: self.size
                pos: self.pos
            Color:
                rgb: 0,0,.5
            Line:
                points: 0,root.height-105, 100,root.height-105
                width: 2
            Line:
                points: 0,root.height-245, 100,root.height-245
                width: 2
            Line:
                points: 0,root.height-395, 100,root.height-395
                width: 2
        ToggleButton:
            pos: off,root.height-45
            size: sz,sz
            group: 'shape'
            on_state: toolbox_id.shape = self.state + ':0'
            state: 'down'
            canvas.after:
                Line:
                    ellipse: self.x+10,self.y+10,20,20
        ToggleButton:
            pos: off+50, root.height-45
            size: sz,sz
            group: 'shape'
            on_state: toolbox_id.shape = self.state + ':1'
            canvas.after:
                Line:
                    rectangle: self.x+10,self.y+10,20,20
        ToggleButton:
            pos: off,root.height-95
            size: sz,sz
            group: 'shape'
            on_state: toolbox_id.shape = self.state + ':2'
            canvas.after:
                Line:
                    points:
                        (self.x+10,self.y+10, self.x+30,self.y+10,
                        self.x+20,self.y+30)
                    close: True
        ToggleButton:
            pos: off+50,root.height-95
            size: sz,sz
            group: 'shape'
            on_state: toolbox_id.shape = self.state + ':3'
            canvas.after:
                Line:
                    points:
                        (self.x+10,self.y+30, self.x+30,self.y+30,
                        self.x+20,self.y+10)
                    close: True
        Label:
            pos: off,root.height-145
            size: 2*sz,sz
            text: 'width'
        Button:
            pos: off, root.height-195
            size: sz,sz
            on_press: if toolbox_id.w < 49 :toolbox_id.w += 1
            canvas.after:
                Line:
                    points:
                        (self.x+10,self.y+10, self.x+30,self.y+10,
                        self.x+20,self.y+30)
                    close: True
        Button:
            pos: off+50,root.height-195
            size: sz,sz
            on_press: if toolbox_id.w > 1: toolbox_id.w -= 1
            canvas.after:
                Line:
                    points:
                        (self.x+10,self.y+30, self.x+30,self.y+30,
                        self.x+20,self.y+10)
                    close: True
        Label:
            pos: off, root.height-245
            size: 2*sz,sz
            text: 'w: ' + str(toolbox_id.w)

        Label:
            pos: off,root.height-295
            size: 2*sz,sz
            text: 'height'
        Button:
            pos: off, root.height-345
            size: sz,sz
            on_press: if toolbox_id.h < 49 :toolbox_id.h += 1
            canvas.after:
                Line:
                    points:
                        (self.x+10,self.y+10, self.x+30,self.y+10,
                        self.x+20,self.y+30)
                    close: True
        Button:
            pos: off+50,root.height-345
            size: sz,sz
            on_press: if toolbox_id.h > 1: toolbox_id.h -= 1
            canvas.after:
                Line:
                    points:
                        (self.x+10,self.y+30, self.x+30,self.y+30,
                        self.x+20,self.y+10)
                    close: True
        Label:
            pos: off, root.height-395
            size: 2*sz,sz
            text: 'h: ' + str(toolbox_id.h)
        Button:
            pos: off, root.height-495
            size: 2*sz,sz
            text: 'Clear'
            on_press: root.drawing.clear(*args)
        Button:
            pos: off, root.height-545
            size: 2*sz,sz
            text: 'Undo'
            on_press: root.drawing.undo(*args)
        Button:
            pos: off, root.height-595
            size: 2*sz,sz
            text: 'Box'
            on_press: root.drawing.box(*args)
            
            
    Drawing:
        id: drawing_id
        toolbox: toolbox_id
        size_hint_x: None
        width: root.width-100
        pos: 100,0
        canvas:
            Color:
                rgb: 1,1,1
            Rectangle:
                size: self.size
                pos: self.pos





This is one result of running the program. A separate video goes over the usage in detail. Because w and h are properties, any change in their values will update all the child widgets. Should we not want this behavior, we will have to make them regular variables.



Demo of App on Youtube.

3 comments:

  1. Python:

    Good Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.

    https://www.emexotechnologies.com/online-courses/python-training-in-electronic-city/

    ReplyDelete
  2. Good Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.

    Python Training in electronic city

    ReplyDelete