วันพุธที่ 23 กันยายน พ.ศ. 2558

Kivy Project : Kivy: Interactive Applications in Python Chapter 3

Attributes, id and root

ใน chapter นี้ จะพูดถึง 4 ส่วนประกอบ นี้ เป็นหลัก คือ toolbox, drawing space, general options, and status bar ทั้ง 4 ส่วนประกอบนี้จะมีการโต้ตอบกัน ซึ่งเรามีความจำเป็นต้องเพิ่ม attributes ไปที่ class ของ project สามารถแสดงความสัมพันธ์ในไฟล์ comiccreator ได้ดังนี้

ภาพจากหนังสือ Kivy : Interactive Applications in Python 

การสร้าง ids ด้วยตัวเอง ไม่ควรนำไปใช้ภายนอกภาษา Kivy ควรสร้าง attributes ไว้เรียกใช้ใน Python Code

จากโค้ด comiccreator.kv เรานำมาดัดแปลงดังนี้

# File name: comiccreator.kv
#:kivy 1.7.0
<ComicCreator>:
    AnchorLayout:
        anchor_x: 'left'
        anchor_y: 'top'
        ToolBox:
            id: _tool_box
            drawing_space: _drawing_space
            comic_creator: root
            size_hint: None,None
            width: 100
    AnchorLayout:
        anchor_x: 'right'
        anchor_y: 'top'
        DrawingSpace:
            id: _drawing_space
            status_bar: _status_bar
            general_options: _general_options
            tool_box: _tool_box
            size_hint: None,None
            width: root.width - _tool_box.width
            height: root.height - _general_options.height - _status_bar.height
    AnchorLayout:
        anchor_x: 'center'
        anchor_y: 'bottom'
        BoxLayout:
            orientation: 'vertical'
            GeneralOptions:
                id: _general_options
                drawing_space: _drawing_space
                comic_creator: root
                size_hint: 1,None
                height: 48
            StatusBar:
                id: _status_bar
                size_hint: 1,None
                height: 24

IDs ที่ highlight ด้วยสีเหลือง คือการประกาศตัวแปร ส่วนที่ hightlight ด้วยสีฟ้า คือการใช้ ids สร้าง attributes มีความสัมพันธ์ดัง comiccreator diagram ก่อนหน้านี้  

ชื่อของ attributes ไม่จำเป็นต้องเปลี่ยนแปลงมาก แค่เพิ่ม _ข้างหน้า ก็พอ เช่น _status_bar 

Basic widget events – dragging the stickman

ตัวอย่าง basic widget events 
    
mouse event , finger event , pen event

ตัวอย่าง event 3 แบบ

on_touch_down : เมื่อกดคลิกบนหน้าจอ หรือ ทัชบนหน้าจอโปรแกรม
on_touch_move : เมื่อกดคลิกแล้วย้าย เช่น ลากเมาส์
on_touch_up      : เมื่อปล่อยเมาส์ หรือ เอานิ้วออกจากหน้าจอ

on_touch_down กับ on_touch_up จะไม่ค่อยมีปัญหาในการแสดงผล แต่ on_touch_move จะไม่แสดงผลที่ไม่มี dragging action วิธีการ add ความสามารถในการ drag สามารถทำได้โดย ดัดแปลงโค้ดดังนี้

โค้ด comicwidgets.kv (add drag capability) 

# File name: comicwidgets.kv
#:kivy 1.7.0
#:import comicwidgets comicwidgets
<DraggableWidget>:
    size_hint: None, None

<StickMan>:
    size: 48,48
    ...

ส่วน comicwidgets.py ให้ดัดแปลงโค้ดดังนี้

# File name: comicwidgets.py
import kivy
kivy.require('1.7.0')
from kivy.uix.relativelayout import RelativeLayout
from kivy.graphics import Line

class DraggableWidget(RelativeLayout):
    def __init__(self,  **kwargs):
        self.selected = None
        self.touched = False
        super(DraggableWidget, self).__init__(**kwargs)

ตรงส่วน __init__ เป็น constructor คลาสนี้จะมี 3 overload method on_touch_down , on_touch_move , on_touch_up

เริ่มที่ on_touch_down

    def on_touch_down(self, touch):
        if self.collide_point(touch.x, touch.y):
            self.touched = True
            self.select()
            return True
        return super(DraggableWidget, self).on_touch_down(touch)

ใช้ method colide_point เพื่อเช็คพิกัดของการ touch หรือ คลิก

event ต่อมา

    def select(self):
        if not self.selected:
            self.ix = self.center_x
            self.iy = self.center_y
            with self.canvas:
                self.selected = Line(rectangle=(0,0,self.width,self.height), dash_offset=2)

event นี้คือจะเช็คว่าถ้าคลิิกตรงกลางของ object ต่างๆ จะให้สร้าง กรอบสี่เหลี่มที่เป็นเส้นประล้อมรอบขึ้นมา

ภาพจากหนังสือ Kivy : Interactive Applications in Python 

ต่อด้วย on_touch_move

    def on_touch_move(self, touch):
        (x,y) = self.parent.to_parent(touch.x, touch.y)
        if self.selected and self.touched and self.parent.collide_point(x - self.width/2, y -self.height/2):
            go = self.parent.general_options
            go.translation=(touch.x-self.ix,touch.y-self.iy)
            return True
        return super(DraggableWidget, self).on_touch_move(touch)

ตรงส่วนการ drag หรือ การลาก จะมีการเช็ค collide_point ไม่ให้ลาก object ออกไปนอก drawing space ถ้าเงื่อนไขเป็น True ก็จะสั่งให้เรียกใช้ translate method

    def translate(self, x, y):
        self.center_x = self.ix = self.ix + x
        self.center_y = self.iy = self.iy + y

สุดท้าย on_touch_up

   def on_touch_up(self, touch):
        self.touched = False
        if self.selected:
            if not self.parent.general_options.group_mode:
                self.unselect()
        return super(DraggableWidget, self).on_touch_up(touch)

on_touch_up event เป็นการคืนค่าสถานะของ on_touch_down ถ้าเช็คแล้วมีการ selected อยู่ ก็จะเรียกใช้ method unselected()

    def unselect(self):
        if self.selected:
            self.canvas.remove(self.selected)
            self.selected = None

มีโค้ดอีกนิดหน่อยใน comicwidgets.py

class StickMan(DraggableWidget):
    pass


Localizing coordinates – adding stickmen

หัวข้อนี้จะพูดถึงการทำปุ่มตรงส่วน toolbox ให้มี event 

# File name: toolbox.py
import kivy
kivy.require('1.7.0')
import math
from kivy.uix.togglebutton import ToggleButton
from kivy.graphics import Line
from comicwidgets import StickMan, DraggableWidget

class ToolButton(ToggleButton):
    def on_touch_down(self, touch):
        ds = self.parent.drawing_space
        if self.state == 'down' and ds.collide_point(touch.x, touch.y):
            (x,y) = ds.to_widget(touch.x, touch.y)
            self.draw(ds, x, y)
            return True
        return super(ToolButton, self).on_touch_down(touch)

    def draw(self, ds, x, y):
        pass

บรรทัดที่ highlight คือ ส่วนที่ใช้ในการเรียก method ในการวาด  ToolStickMan , ToolCircle , และ ToolLine

ตัวอย่างเช่น ToolStickMan  เป็น method ในการวาด stickman ขนาด 48x48 pixel บนหน้า drawingspace

class ToolStickman(ToolButton):
    def draw(self, ds, x, y):
        sm = StickMan(width=48, height=48)
        sm.center = (x,y)
        ds.add_widget(sm)


Binding and unbinding events – sizing limbs and heads

หัวข้อนี้จะพูดถึงการ ย่อขยายต่างๆ เช่น วงกลม กับ เส้น เพื่อสร้าง stickman ในขนาดที่แตกต่างออกไป

class toolfigure จะมีทั้งหมด 6 medthod

class ToolFigure(ToolButton):

    def draw(self, ds, x, y):
        (self.ix, self.iy) = (x,y)
        with ds.canvas:
            self.figure=self.create_figure(x,y,x+1,y+1)
        ds.bind(on_touch_move=self.update_figure)
        ds.bind(on_touch_up=self.end_figure)

    def update_figure(self, ds, touch):
        if ds.collide_point(touch.x, touch.y):
            (x,y) = ds.to_widget(touch.x, touch.y)
            ds.canvas.remove(self.figure)
            with ds.canvas:
                self.figure = self.create_figure(self.ix, self.iy,x,y)

    def end_figure(self, ds, touch):
        ds.unbind(on_touch_move=self.update_figure)
        ds.unbind(on_touch_up=self.end_figure)
        ds.canvas.remove(self.figure)
        (fx,fy) = ds.to_widget(touch.x, touch.y)
        self.widgetize(ds,self.ix,self.iy,fx,fy)

    def widgetize(self,ds,ix,iy,fx,fy):
        widget = self.create_widget(ix,iy,fx,fy)
        (ix,iy) = widget.to_local(ix,iy,relative=True)
        (fx,fy) = widget.to_local(fx,fy,relative=True)
        widget.canvas.add(self.create_figure(ix,iy,fx,fy))
        ds.add_widget(widget)

    def create_figure(self,ix,iy,fx,fy):
        pass

    def create_widget(self,ix,iy,fx,fy):
        pass

class toolfigure จะมีทั้งหมด 6 medthod
  1. draw: ให้เริ่มวาดจากจุดที่เราคลิกครั้งแรก เช่น เริ่มจากตรงกลางของวงกลม หรือ จุดปลายของเส้น
  2. update_figure : update จุดเริ่มต้นถึงจุดสุดท้ายของรูป เช่น รัศมีจากจุดกึ่งกลาง หรือ จากจุดหนึ่งไปยังอีกจุดหนึ่งของเส้น
  3. end_figure : จะบอกจุดสุดท้าย การทำงานคล้ายกับ update_figure
  4. widgetize : สร้างการ drag figure 
  5. create_figure : เป็น method ที่เรียกใช้ ToolLine ,ToolCircle ในการวาดรูป
  6. create_widget : ทำงานเหมือน create_figure 
ไม่ต้องการให้ on_touch_move กับ on_touch_up events ทำงานตลอดเวลา จึงใช้ Binding and unbinding events ในการแก้ปัญหา (ให้แค่ลากแล้วปล่อย) 

โค้ดในส่วนของ class ToolLine ,ToolCircle

class ToolLine(ToolFigure):
    def create_figure(self,ix,iy,fx,fy):
        return Line(points=[ix, iy, fx, fy])

    def create_widget(self,ix,iy,fx,fy):
        pos = (min(ix, fx), min(iy, fy)) 
        size = (abs(fx-ix), abs(fy-iy))
        return DraggableWidget(pos = pos, size = size)

class ToolCircle(ToolFigure):
    def create_figure(self,ix,iy,fx,fy):
        return Line(circle=[ix,iy,math.hypot(ix-fx,iy-fy)])

    def create_widget(self,ix,iy,fx,fy):
        r = math.hypot(ix-fx, iy-fy)
        pos = (ix-r, iy-r)
        size = (2*r, 2*r)
        return DraggableWidget(pos = pos, size = size)

ทั้ง 2 class จะใช้การคำนวณแบบธรรมดาในการกำหนดขนาด

โค้ดในส่วนของ toolbox.kv  (มีการแก้ไข)

# File name: toolbox.kv
#:kivy 1.7.0
#:import toolbox toolbox
<ToolButton>:
    size_hint: None,None
    size: 48,48
    group: 'tool'
    canvas:
        PushMatrix:
        Translate:
            xy: self.x,self.y
    canvas.after:
        PopMatrix:

<ToolBox@GridLayout>:
    cols: 2
    padding: 2
    tool_circle: _tool_circle
    tool_line: _tool_line
    tool_stickman: _tool_stickman
    ToolCircle:
        id:  _tool_circle
        canvas:
            Line:
                circle: 24,24,14
    ToolLine:
        id: _tool_line
        canvas:
            Line:
                points: 10,10,38,38
    ToolStickman:
        id: _tool_stickman
        StickMan:
            pos_hint: {'center_x':.5,'center_y':.5}

โค้ดนี้จะมีการเพิ่ม class ที่เราทำใหม่เข้ามา ToolCircle , ToolLine , ToolStickman และมีการเพิ่ม attributes ที่เอาไว้ใช้ในการสร้าง gestures ใน chapter ที่ 4

Binding events in the Kivy language

ในหัวข้อนี้จะพูดถึงการทำ event ในส่วนของ general option

โค้ด generaloption.kv

# File name: generaloptions.kv
#:kivy 1.7.0
#:import generaloptions generaloptions
<GeneralOptions>:
    orientation: 'horizontal'
    padding: 2
    Button:
        text: 'Clear'
        on_press: root.clear(*args)
    Button:
        text: 'Remove'
        on_release: root.remove(*args)
    ToggleButton:
        text: 'Group'
        on_state: root.group(*args)
    Button:
        text: 'Color'
        on_press: root.color(*args)
    ToggleButton:
        text: 'Gestures'
        on_state: root.gestures(*args)

Button หรือ ปุ่มกด จะมี event หลักๆอยู่ 2 อย่างคือ on_press กับ on_release 
on_press จะใช้กับ Clear Button กับ Color Button ก็คือกดครั้งเดียวพอ ทำงานเสร็จก็จบ
on_release  จะใช้กับ Remove Button กดแล้วสามารถกดต่อได้อีก
on_state จะใช้ ToggleButton ก็คือ กดครั้งนึงไว้แล้วจะทำงานตลอดจนกว่าจะกดอีกครั้งเพื่อหยุด ใช้กับ GroupButton กับ GestureButton

โค้ดในส่วน generaloptions.py

# File name: generaloptions.py
import kivy
kivy.require('1.7.0')
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty, ListProperty

class GeneralOptions(BoxLayout):
    group_mode = False
    translation = ListProperty(None)

    def clear(self, instance):
        self.drawing_space.clear_widgets()

    def remove(self, instance):
        ds = self.drawing_space
        if len(ds.children) > 0:
            ds.remove_widget(ds.children[0])

    def group(self, instance, value):
        if value == 'down':
            self.group_mode = True
        else:
            self.group_mode = False
            self.unselect_all()

    def color(self, instance):
        pass

    def gestures(self, instance, value):
        pass

    def unselect_all(self):
        for child in self.drawing_space.children:
            child.unselect()

    def on_translation(self,instance,value):
        for child in self.drawing_space.children:
            if child.selected:
                child.translate(*self.translation)

class GeneralOptions มีทั้งหมด 7 methods
  1. clear : ล้าง widget ที่เราวาดใน drawing space ทั้งหมด
  2. remove : ลบการวาดครั้งล่าสุด ถ้ากดอีกก็จะลบการวาดครั้งก่อนหน้านั้นไปเรื่อยๆ
  3. group : ถ้าคลิกเลือกจะมีการเปลี่ยนโหมด จาก True เป็น False
  4. color : ในที่นี้ยังไม่มี event
  5. gesture : ในที่นี้ยังไม่มี event
  6. unselect_all : ทำงานเมื่อไมได้คลิกรูปใดๆ
  7. on_translation : 

Creating your own events – the magical properties

การสร้าง properties

ตัวอย่าง types of properties
NumericProperty , StringProperty , ListProperty , DictProperty , or ObjectProperty

ถ้าเป็น Kivy property จะให้ตั้งชื่อโดย ใส่ on_ หน้าชื่อของ properties เช่น state property ของ
ToogleButton ก็จะเป็น on_state

นอกจากนี้ยังกล่าวถึงการสร้าง event ของ group ซึ่งใช้ state propertty ของ ToogleButton ซึ่งเราต้องการให้ เมื่อเราอยู่ใน group mode เราสามารถเลือก object หลายๆตัว แล้วลากไปพร้อมๆกันได้

ภาพจากหนังสือ Kivy : Interactive Applications in Python 

แก้โค้ดในส่วนของ comicwidgets.py ดังนี้

# File name: comicwidgets.py
import kivy
kivy.require('1.7.0')
from kivy.uix.relativelayout import RelativeLayout
from kivy.graphics import Line

class DraggableWidget(RelativeLayout):
    def __init__(self,  **kwargs):
        self.selected = None
        self.touched = False
        super(DraggableWidget, self).__init__(**kwargs)

    def on_touch_down(self, touch):
        if self.collide_point(touch.x, touch.y):
            self.touched = True
            self.select()
            return True
        return super(DraggableWidget, self).on_touch_down(touch)

    def select(self):
        if not self.selected:
            self.ix = self.center_x
            self.iy = self.center_y
            with self.canvas:
                self.selected = Line(rectangle=(0,0,self.width,self.height), dash_offset=2)

    def on_touch_move(self, touch):
        (x,y) = self.parent.to_parent(touch.x, touch.y)
        if self.selected and self.touched and self.parent.collide_point(x - self.width/2, y -self.height/2):
            go = self.parent.general_options
            go.translation=(touch.x-self.ix,touch.y-self.iy)
            return True
        return super(DraggableWidget, self).on_touch_move(touch)

    def translate(self, x, y):
        self.center_x = self.ix = self.ix + x
        self.center_y = self.iy = self.iy + y

    def on_touch_up(self, touch):
        self.touched = False
        if self.selected:
            if not self.parent.general_options.group_mode:
                self.unselect()
        return super(DraggableWidget, self).on_touch_up(touch)

    def unselect(self):
        if self.selected:
            self.canvas.remove(self.selected)
            self.selected = None

class StickMan(DraggableWidget):
    pass

Kivy and properties

หัวข้อนี้ก็จะเป็นตัวอย่างของ property ใน Kivy 

ตัวอย่างจากโค้ด statusbar.py

# File name: statusbar.py
import kivy
kivy.require('1.7.0')
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty, ObjectProperty

class StatusBar(BoxLayout):
    counter = NumericProperty(0)
    previous_counter = 0

    def on_counter(self, instance, value):
        if value == 0:
            self.msg_label.text = "Drawing space cleared"
        elif value - 1 == self.__class__.previous_counter:
            self.msg_label.text = "Widget added"
        elif value + 1 == StatusBar.previous_counter:
            self.msg_label.text = "Widget removed"
        self.__class__.previous_counter = value

มีการใช้ NumericProperty (on_counter) ในการนับ Figure ที่เราสร้าง 

โค้ดในส่วน statusbar.kv

# File name: statusbar.kv
#:kivy 1.7.0
#:import statusbar statusbar
<StatusBar>:
    msg_label: _msg_label
    orientation: 'horizontal'
    Label:
        text: 'Total Figures: ' + str(root.counter)
    Label:
        id: _msg_label
        text: "Kivy started"

ส่วนต่อมาเราต้องการที่ count หรือนับ figure ที่อยู่บน drawings space ให้ทำการแก้ไขโค้ดใน drawingspace.py ดังนี้

# File name: drawingspace.py
import kivy
kivy.require('1.7.0')
from kivy.uix.relativelayout import RelativeLayout

class DrawingSpace(RelativeLayout):
    def on_children(self, instance, value):
        self.status_bar.counter = len(self.children)

โค้ดในส่วนของ  drawingspace.kv

# File name: drawingspace.kv
#:kivy 1.7.0
#:import drawingspace drawingspace
<DrawingSpace@RelativeLayout>:

หน้าต่างเริ่มต้นโปรแกรม


ทดลองสร้าง Figure ต่างๆ

เมื่อคลิกเมาส์ที่ figure จะมีการสร้าง กรอบสี่เหลี่ยมที่เป็นเส้นประล้อมรอบ

ลองกด clear button จะเห็นได้ว่า figure นั้นถูกลบไปหมดแล้ว และมีข้อความแสดง Drawing space cleared

ลองสร้าง Stickman แล้วใช้คำสั่ง Group 

สามารถเคลื่อนย้าย Stickman ได้



ทดลองสร้าง Stickman 2 ตัว 

ทดลองใช้คำสั่ง Remove 

จะเห็นได้ว่าตัวที่เพิ่มเข้ามาล่าสุดถูก Remove ออกไป

อ้างอิงข้อมูลและรูปภาพจากหนังสือ Kivy : Interactive Applications in Python

ไม่มีความคิดเห็น:

แสดงความคิดเห็น