图形绘制

页面背景

看过布局的朋友们,一定发现了,在创建一个widget控件后,为布局设置背景色或者经常会有一个with self.canvas并且加了几个属性和绑定了事件,这些其实是我们在生成一个控件后,Kivy自动生成的一个类似画布的。我们通过对画布的更改可以设置颜色、尺寸、背景图等。
这里说明一下,canvas 学过HTML5的同学肯定觉得眼熟,但是实际两个是不相同的。
HTML的Canvas的定义是:

HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。
画布是一个矩形区域,您可以控制其每一像素。
canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。

而Kivy的Canvas本质是一组在坐标空间的指令容器,可以理解成坐标空间中一个无限的绘画板,通过添加指令来绘制图形。
我们先来使用纯色进行设置背景。

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from kivy.uix.boxlayout import BoxLayout
from kivy.app import App
from kivy.graphics import Rectangle, Color


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

        with self.canvas:
            Color(1, 1, 1, 1)
            Rectangle(pos=self.pos, size=self.size)


class BoxLayoutApp(App):
    def build(self):
        return BoxLayoutWidget()


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

运行上面代码可以发现,在布局中,左下角有一块100*100的白色小方块,这时候我们就完成了基础的颜色设置,但是我们需要把白色填满整个方块。其实以上代码的操作是初始化一个BoxLayou然后,初始化后其实一个黑色的背景,然后在with里面新加一个画布,对画布初始化为白色背景。如果我们要填满整个窗口只需要在初始化好画布之后,对画布进行大小重定义即可实现覆盖。参考如下:

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from kivy.uix.boxlayout import BoxLayout
from kivy.app import App
from kivy.graphics import Rectangle, Color


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

        with self.canvas:
            # 设置颜色值,通常百分比形式比较难把握,我们可以选择将rgb的色值除255,
            # 例如:海蓝宝石(#7FFFD4)色的rgb的色值是(127,255,212)写成Kivy的Color就写成
            # Color(127/255,255/255,212/255,0),最后一个值是设置透明度的一般选择0~1之间的值。
            # 0不透明,1为透明
            Color(127 / 255, 255 / 255, 212 / 255, 1)
            self.rect = Rectangle(pos=self.pos, size=self.size)
            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 BoxLayoutApp(App):
    def build(self):
        return BoxLayoutWidget()


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

老样子,我们看看KV文件配合的情况下如何实现以上样子

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from kivy.uix.boxlayout import BoxLayout
from kivy.app import App


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


class ColorApp(App):
    def build(self):
        return BoxLayoutWidget()


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

kv文件

<BoxLayoutWidget>
    canvas:
        Color:
            rgba: [127 / 255, 255 / 255, 212 / 255, 1]

        Rectangle:
            size: self.size
            pos: self.pos

还可以在有底色的情况下,使用背景图片。
只需把前面Python代码中19行新增一个参数即可如下:

self.rect = Rectangle(pos=self.pos, size=self.size, source='canvas_image.jpg')

或者kv文件中Rectangle下新增source: 'canvas_image.jpg',其中canvas_image.jpg是图片名称和地址。

基本图形绘制

前面,我们使用纯色背景以及使用图片来将Canvas这个画布进行上色和调整等,接下来我们试试在画好的画布中进行简单图形的绘制,比如。矩形、椭圆等

矩形

其实我们前面也一直在画这个矩形,只是有的部分把他填充了。

# !/usr/bin/env python3
# -*- coding: utf-8 -*-


from kivy.uix.relativelayout import RelativeLayout
from kivy.app import App


class RelativeWidget(RelativeLayout):
    def __init__(self, **kwargs):
        super(RelativeWidget, self).__init__(**kwargs)


class DrawRectangleApp(App):
    def build(self):
        return RelativeWidget()


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

drawrectangle.kv

<RelativeWidget>
    canvas:
        Color:
            rgba: [1,1,1,1]
        Rectangle:
            size: self.width*0.2, self.height*.15
            pos: self.x+10, self.y+10
椭圆
# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from kivy.uix.relativelayout import RelativeLayout
from kivy.app import App


class RelativeWidget(RelativeLayout):
    def __init__(self, **kwargs):
        super(RelativeWidget, self).__init__(**kwargs)


class DrawEllipseApp(App):
    def build(self):
        return RelativeWidget()


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

drawellipse.kv

<RelativeWidget>
    canvas:
        Color:
            rgba: [1,1,1,1]
        Ellipse:
            size: self.width*.35, self.height*.35
            pos: self.x+250, self.top-400
半圆及多边形

其实半圆的画法和椭圆基本一致。只是增加了三个新属性。

  • angle_start: 开始线角度,开始方向与y轴的角度。
  • angle_end: 结束线角度,结束方向与y轴的角度。一般angle_end的大小要大于angle_start,若不大于则需要加 360° ,此时Kivy会顺时针画图形。否则则是逆时针画图形。
  • egments: 多边形的边数,可以用来画三角形,六边形等形状。

例如,我们改下前面的椭圆,在它旁边加多一个六边形,一个三角形以及一个扇形还有正圆。Python代码我们继续使用椭圆的代码。改下Kv代码就好:

<RelativeWidget>
    canvas:
        Color:
            rgba: [1,1,1,1]
        Ellipse:  # 正圆
            size: self.width*0.20, self.width*0.20
            pos: self.x+20, self.top-200

        Ellipse:  # 正六边形
            size: self.height*0.25, self.height*0.25
            pos: self.x+200, self.top-200
            segments: 6

        Ellipse:  # 超过180°的顺时针扇形
            size: self.width*0.25, self.height*.25
            pos: self.x+400, self.top-200
            angle_start: 120
            angle_end: 420

        Ellipse:  # 正三角
            size: self.height*.25, self.height*.25
            pos: self.x+400, self.top-400
            segments: 3

        Ellipse:  # 正半圆
            size: self.height*0.25, self.height*.25
            pos: self.x+200, self.top-400
            angle_start: 0
            angle_end: 180

        Ellipse:  # 椭圆
            size: self.height*0.30, self.width*.15
            pos: self.x+20, self.top-400
多边形

前面使用了画圆的方式使用参数segments来画多边形,这些多变形画出来它的边长是随着圆的尺寸决定的,长度是不能自己决定的。
Kivy还提供了一种方式,用于指定各边的顶点坐标,可以绘制特殊边的多边形,但是值支持四个坐标点,可用于菱形、平行四边形、梯形等。

一样,我们值更改Kv文件如下:

<RelativeWidget>
    canvas:
        Color:
            rgba: [1,1,1,1]

        Quad:
            points: 400,250, 640,280, 480,500, 380,520  # 设置顶点坐标。 按X轴,Y轴的顺序读取顶点坐标。

Quad中的points会按照x轴,y轴的的顺序,读取点的位置,并且在画布canvas上绘制一个多边形。

点和线

kv文件

<RelativeWidget>
    canvas:
        Color:
            rgba: [1,1,1,1]

        Line:
            points: 310,350, 310,280,  360,350, 510,350

        Point:
            points:300,200, 300,400
            pointsize: 5

其实就像数学里面理解的,所有的线是由无数个点挨着排列形成的,同理面是由线挨着排列形成的。我们也可以用点和线画前面的图形。
例如:

<RelativeWidget>
    canvas:
        Color:
            rgba: [1,1,1,1]

        Line:  # 线
            points: 110,150, 310,80

        Point: # 点
            points:100,200, 180,260
            pointsize: 5  # 点大小

        Line: # 椭圆
            ellipse: 210,320, 80,60, 120,420,180
            width: 2  # 线宽

        Line: # 圆
            circle: 350,350, 40,0, 360,180
            width: 1.5 

        Line: # 矩形
            rectangle: 410,310, 80, 70

        Line:
            points: 510,310, 540,390, 590,320
            close: True  # 是否闭合

KV中Line对应参数说明:

  • ellipse:210, 320 表示椭圆的位置;80, 60 代表椭圆的宽和长;120, 420,180分别对应: angel_start, angel_end, segments.
  • circle: 350,350 表示圆心的位置;40表示圆的半径;0,360,180分别对应: angel_start, angel_end, segments.
  • rectangle: 420, 310 表示矩形位置左下角的顶点; 80,70,代表宽高
  • points: 510, 310 代表第一个点的位置,以此类推。

Cancas 属性

前面我们已经初步了解了Canvas画布基本绘制功能。我们来深入了解下他有哪些属性。
在Kivy里,每个小部件和布局基本都有他的CanvasCanvas.beforecanvas.after,其实我们可以将canvas看作在坐标空间种,一个无限的绘图板,通过添加绘图指令来绘制想要的图形。Kivy的所有部件都是共享一个坐标空间的,且不限于窗口或者屏幕的大小。
我们前面学习的时候也已经试过给画布和小部件设置背景以及显示的颜色。而且我们还能添加不同Instructions指令来达到不同的页面效果。

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout
from kivy.graphics import Rectangle, Color
from kivy.graphics.instructions import InstructionGroup


class RelativeWidget(RelativeLayout):
    def __init__(self, **kwargs):
        super(RelativeWidget, self).__init__(**kwargs)
        blue = InstructionGroup()
        blue.add(Color(1, 0, 0, .6))
        blue.add(Rectangle(pos=self.pos, size=(300,300)))
        self.canvas.add(blue)

        green = InstructionGroup()
        green.add(Color(0, 1, 0, 0.4))
        green.add(Rectangle(pos=(300, 300), size=(300, 300)))
        self.canvas.add(green)


class AttributeApp(App):
    def build(self):
        return RelativeWidget()


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

通常情况,我们在使用完canvas画布后,还需使用clear()方法清除所有加载在画布种的Instructions指示类型。
我们可以把上面的代码改成with语法,效果是一致的。

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout
from kivy.graphics import Rectangle, Color
from kivy.graphics.instructions import InstructionGroup


class RelativeWidgetWith(RelativeLayout):
    def __init__(self, **kwargs):
        super(RelativeWidgetWith, self).__init__(**kwargs)
        with self.canvas:
            Color(1, 0, 0, .6)
            Rectangle(pos=self.pos, size=(300, 300))

            Color(0, 1, 0, 0.4)
            Rectangle(pos=(300, 300), size=(300, 300))


class AttributeApp(App):
    def build(self):
        return RelativeWidgetWith()


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

还有一些常用的属性:

Canvas属性说明
add(Instructions c)将Instructions类型的c添加到Canvas中
clear()删除所有Instructions
get_group(str groupname)返回特定组下所有Instructions
insert(int index, Instructions c)指定位置插入c
indexof(Instructions c)返回c下标
length()返回canvas的长度
remove()删除指定的c
remove_group(str groupname)删除该组下所有的c

在Kivy每个小部件都有属性canvas,除了这个包括有canvas.beforecanvas.after属性,用法的话基本和canvas一致。只是在运行顺序上的优先级不同。
大概顺序是:

  • canvas.before > canvas > widget(canvas.befor,canvas,canvas.after) > canvas.after

旋转、平移和缩放空间坐标

在canvas 中是可以使用Rotate指令来控制旋转操作,他与ScatterLayout布局不同,它是对整个坐标空间,因此所有的子部件都是会受到影响。在使用整个参数时,需要指定三个参数;

  • axis: 设置用于旋转的轴,通常为z轴(0,0,1)
  • angle: 设置旋转度数
  • origin: 设置旋转参考点

来试试效果:

# !/usr/bin/env python3
# -*- coding: utf-8 -*-
# 选择平移缩放


from kivy.uix.gridlayout import GridLayout
from kivy.app import App


class RotateGridLayoutWidget(GridLayout):
    def __init__(self, **kwargs):
        super(RotateGridLayoutWidget, self).__init__(**kwargs)


class RotateTranslateZoomApp(App):
    def build(self):
        return RotateGridLayoutWidget()


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

rotatetranslatezoom.kv

<MyImage@Image>
    source:'rotatetranslatezoom.jpg'
    pos: self.parent.pos
    size_hit: .5, .4
    canvas:
        Color:
            rgba: 1,0,0,.5
        Line:
            rectangle: self.x, self.y, self.width,self.height

<RotateGridLayoutWidget>
    cols: 2
    canvas:

        Color:
            rgba:(1,1,1,1)
        Rectangle:
            pos: self.pos
            size: self.size

    Button:
        text: 'col:1 row:1'

    FloatLayout:
        canvas:
            Rotate:
                axis:(0,0,1)
                angle: 60
                origin: self.center
            Color:
                rgba:(1,1,1,1)
            Rectangle:
                pos: self.pos
                size: self.size
        MyImage:

    Button:
        text: 'col:1 row:2'

以上布局,会出现两个按钮和一张图片。其中按钮按列排列。但是由于我们在FloatLayout中使用了Rotate这样导致了画布进行了旋转。我们看到的效果是图像是沿着中点,逆时针旋转了60度的情况。并且由于旋转的是在Button前面加入的,所以导致后方的空间坐标系也收到了影响,也连带着旋转了。而且点击第二个按钮黑色部分发现无效。而是需要点击按钮本身的位置,也就是第一个按钮的垂直排列的列下方才有效。为了避免这个情况,我们需要引入两个新的命令。

  • PushMatrix : 保存上下文环境。
  • PopMatrix : 恢复上下文环境。
    就像上面的栗子:

我们在对某个图像的画布或者其他进行操作的时候。就会导致代码排序在它之后的内容受到影响。这个时候我们就可以使用前面两个命令对环境进行保存和回复。
我们来改下KV文件:

<MyImage@Image>
    source:'rotatetranslatezoom.jpg'
    pos: self.parent.pos
    size_hit: .5, .4
    canvas:
        PushMatrix
        Rotate:
            axis:(0,0,1)
            angle: 60
            origin: self.center
        Color:
            rgba: 1,0,0,.5
        Line:
            rectangle: self.x, self.y, self.width,self.height
        PopMatrix

<RotateGridLayoutWidget>
    cols: 2
    canvas:

        Color:
            rgba:(1,1,1,1)
        Rectangle:
            pos: self.pos
            size: self.size

    Button:
        text: 'col:1 row:1'

    FloatLayout:
        MyImage:


    Button:
        text: 'col:1 row:2'

以上文件运行后,会发现,红色矩形框旋转了60度。后面的按钮以及图像均不会受到影响,这是因为Rotate指令我们是放在canvas中的,是在屏幕上以及确定位置之后才开始执行的,并且在显示的时候,就使用前面的恢复环境命令恢复了。但是我们旋转的时候,就是为了让图像旋转。现在的情况却是图像并未旋转,这个时候,可以来试试前面我们学过的属性canvas.beforcanvas.after我们只需要在位置还没确定的时候,就旋转,这样图像就会在画布旋转之后进行图像的显示。这样就能达到指定效果了。我们在改动下KV文件:

<MyImage@Image>
    source:'rotatetranslatezoom.jpg'
    pos: self.parent.pos
    size_hit: .5, .4
    canvas.before:  # 使用canvas.before 命令
        PushMatrix  # 保存当前上下文环境
        Rotate:
            axis:(0,0,1)  # 旋转
            angle: 60     # 角度
            origin: self.center  # 起点
    canvas:
        Color:
            rgba: 1,0,0,.5
        Line:
            rectangle: self.x, self.y, self.width,self.height

    canvas.after:
        PopMatrix  # 恢复上下文环境

<RotateGridLayoutWidget>
    cols: 2
    canvas:

        Color:
            rgba:(1,1,1,1)
        Rectangle:
            pos: self.pos
            size: self.size

    Button:
        text: 'col:1 row:1'

    FloatLayout:
        MyImage:


    Button:
        text: 'col:1 row:2'

使用上面的kv文件,就会发现图像和矩形框都一起旋转了。这样就达到了,旋转的同时连小部件也旋转。当然会发现我们为什么不把PopMatrix写在canvas的末行,而是大费周章的写个canvas.after,这里我就不解释了结合前面的知识点,大家可以尝试下,将PopMatrix写在canvas末行,看看是什么效果。

试过了旋转,我们来看看平移:
canvas平移指令是Translate除了属性以外,该指令和我们前面用的Rotate指令基本是一致。
在使用Translate时,是需要指定X轴,Y轴,Z轴上的移动距离,如果不指定,则默认不移动。我们再拿前面的旋转代码给他加上平移属性看看。

Translate:
    x: -50
    y: 100
    z:0

就不贴完整代码了,大概就是在Rotate属性结束下方另起一行,加上以上四行代码。我们会发现,图片的位置会向左平移了50个像素,将按钮盖住了,并且也向上移动了100个像素。如果这里我们将z轴的值进行改变,会发现图像不见了,这是因为图像的位置已经不在画布上了。
同理缩放(Scale)也是和前面一致,我们只需要指定xyz轴上的缩放倍数即可:

Scale:
    xyz:(1.25,1.30,0.5)

如上,则是x轴和y轴分别缩放1.25和1.30.倍,z轴缩放0.5倍。

实战--画板

我们先来做一个初始画板,先不添加其他复杂元素,来实现能写字的功能:

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Line, Color


class DrawCanvasWidget(Widget):
    def __init__(self, **kwargs):
        super(DrawCanvasWidget, self).__init__(**kwargs)
        """设置画笔的默认颜色为黑色"""
        self.canvas.add(Color(rgb=[0, 0, 0]))
        self.line_width = 2

    def on_touch_down(self, touch):
        """触摸显示轨迹"""
        if Widget.on_touch_down(self, touch):
            return
        with self.canvas:
            touch.ud['current_line'] = Line(points=(touch.x, touch.y), width=self.line_width)

    def on_touch_move(self, touch):
        """连线"""
        if 'current_line' in touch.ud:
            touch.ud['current_line'].points += (touch.x, touch.y)


class PaintApp(App):
    def build(self):
        self.draw_canvas_widget = DrawCanvasWidget()
        return self.draw_canvas_widget


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

Kivy提供了on_touch_downon_touch_move方法来实现监听屏幕点击和屏幕移动触发事件

<DrawCanvasWidget>:
    canvas.before:
        Color:
            rgba:[1,1,1,1]
        Rectangle:
            pos: self.pos
            size: self.size

以上能够实现使用一种黑色画笔在白色画板上进行写字。接下来,我们试试能否通过选择颜色去替换画笔颜色,然后进行绘画。
Kivy种设置颜色的方法一般是使用rgba(r红色,g绿色,b蓝色,alpha名度)一般我们就是使用颜色和255进行比值,得到百分比。我们也可以直接使用十六进制进行表示。当需要大量颜色的时候,Kivy在utils包内也提供了 get_color_from_hex()方法进行16进制和百分比转换,我们使用当时时候传入十六进制字符串即可。
我们可以选择在python内使用,还是在kv文件内使用,使用方法如下:
python文件内:

from kivy.utils import get_color_from_hex
get_color_from_hex('#98feab')

kv文件内使用:

#:import C kivy.utils.get_color_from_hex
Button:
    background_color: C('#98feab')

我们可以在画板类里面写上一个方法来改变canvas画布颜色的方法。不是canvas.before背景:

def chang_color(self, new_color):
    """调色"""
    self.canvas.add(Color(*new_color))

通过chang_color方法在init中初始化一个颜色。在kv文件设置按钮,通过按钮单击调用chang_color激活颜色的改变。
具体代码如下:

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Line, Color
from kivy.utils import get_color_from_hex


class DrawCanvasWidget(Widget):
    def __init__(self, **kwargs):
        super(DrawCanvasWidget, self).__init__(**kwargs)
        """设置画笔的默认颜色为黑色"""
        self.change_color(get_color_from_hex('#12aced'))
        self.line_width = 2

    def on_touch_down(self, touch):
        """触摸显示轨迹"""
        if Widget.on_touch_down(self, touch):
            return
        with self.canvas:
            touch.ud['current_line'] = Line(points=(touch.x, touch.y), width=self.line_width)

    def on_touch_move(self, touch):
        """连线"""
        if 'current_line' in touch.ud:
            touch.ud['current_line'].points += (touch.x, touch.y)

    def change_color(self, new_color):
        """调色"""
        self.canvas.add(Color(*new_color))


class PaintApp(App):
    def build(self):
        self.draw_canvas_widget = DrawCanvasWidget()
        return self.draw_canvas_widget


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

kv文件

#:import C kivy.utils.get_color_from_hex   # 引入颜色转换方法

<BottomColorButton@ToggleButton>:
    group: 'color'
    background_normal: ''
    background_down: ''
    border: (3, 3, 3, 3)
    on_release: app.draw_canvas_widget.change_color(self.background_color)


<DrawCanvasWidget>:
    canvas.before:
        Color:
            rgba: [1, 1, 1, 1]
        Rectangle:
            pos: self.pos
            size: self.size

    BoxLayout:
        id: bottom_box
        orientation: 'horizontal'
        padding: 2
        spacing: 2
        size: root.width, 40

        BottomColorButton:
            background_color: C('#19caad')
            state: 'down'                     # 按钮状态

        BottomColorButton:
            background_color: C('#8cc7b5')

        BottomColorButton:
            background_color: C('#a0eee1')

        BottomColorButton:
            background_color: C('#bee7e9')

        BottomColorButton:
            background_color: C('#beedc7')

        BottomColorButton:
            background_color: C('#d6d5b7')

        BottomColorButton:
            background_color: C('#d1ba74')

        BottomColorButton:
            background_color: C('#e6ceac')

        BottomColorButton:
            background_color: C('#ecad9e')

        BottomColorButton:
            background_color: C('#f4606c')

        BottomColorButton:
            background_color: C('#3498db')

        BottomColorButton:
            background_color: C('#1abc9c')

        BottomColorButton:
            background_color: C('#2ecc71')

        BottomColorButton:
            background_color: C('#f1c40f')

        BottomColorButton:
            background_color: C('#e67e22')

        BottomColorButton:
            background_color: C('#e74c3c')

        BottomColorButton:
            background_color: C('#9b59bc')

        BottomColorButton:
            background_color: C('#ecf0f1')

        BottomColorButton:
            background_color: C('#95a5a6')

        BottomColorButton:
            background_color: C('#000000')

有了画笔颜色的改变,我们接下来看看改变画笔的粗细。
同理,使用与改变颜色相同方法做一个画笔粗细:

    def change_line_width(self, line_width='Normal'):
        self.line_width = {'Thin': 1, 'Normal': 2, 'Thick': 4}[line_width]
<LineWidthButton@ToggleButton>:
    group: 'line_width'
    color: C('#2c3e50')
    background_color: C('#ecf0f1')
    background_normal: ''
    background_down: ''
    border: (3, 3, 3, 3)
    on_release: app.draw_canvas_widget.change_line_width(self.text)

画布内新增改变按钮


    BoxLayout:
        orientation: 'horizontal'
        padding: 2
        spacing: 2
        x: 0
        top: root.top
        size_hint: None,None
        size: 280, 44

        LineWidthButton:
            text: 'Thin'

        LineWidthButton:
            text: 'Normal'
            state: 'down'

        LineWidthButton:
            text: 'Thick'

新增清空画板功能;

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Line, Color
from kivy.utils import get_color_from_hex
# 添加边框样式
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.uix.togglebutton import ToggleButton


# 因为已经在这边添加了ToggleButton的公共类,提取出公共属性的两个就不需要使用ToggleButton而是可以直接使用
# FrameToggleButton即可
class FrameToggleButton(ToggleButton):
    """当前按钮添加边框"""

    def do_press(self):
        """点击改变状态"""
        if self.state == 'normal':
            ToggleButtonBehavior.do_press(self)


class DrawCanvasWidget(Widget):
    def __init__(self, **kwargs):
        super(DrawCanvasWidget, self).__init__(**kwargs)
        """设置画笔的默认颜色为黑色"""
        self.change_color(get_color_from_hex('#12aced'))
        self.change_line_width()

    def on_touch_down(self, touch):
        """触摸显示轨迹"""
        if Widget.on_touch_down(self, touch):
            return
        with self.canvas:
            touch.ud['current_line'] = Line(points=(touch.x, touch.y), width=self.line_width)

    def on_touch_move(self, touch):
        """连线"""
        if 'current_line' in touch.ud:
            touch.ud['current_line'].points += (touch.x, touch.y)

    def change_color(self, new_color):
        """调色"""
        self.last_color = new_color
        self.canvas.add(Color(*new_color))

    def change_line_width(self, line_width='Normal'):
        self.line_width = {'Thin': 1, 'Normal': 2, 'Thick': 4}[line_width]

    def clear_canvas(self):
        saved = self.children[:]
        self.clear_widgets()
        self.canvas.clear()
        for widget in saved:
            self.add_widget(widget)
        self.change_color(self.last_color)


class PaintApp(App):
    def build(self):
        self.draw_canvas_widget = DrawCanvasWidget()
        return self.draw_canvas_widget


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

#:import C kivy.utils.get_color_from_hex

<BottomColorButton@FrameToggleButton>:
    group: 'color'
    background_normal: ''
    background_down: ''
    border: (1, 1, 1, 1)
    on_release: app.draw_canvas_widget.change_color(self.background_color)

<LineWidthButton@FrameToggleButton>:
    group: 'line_width'
    color: C('#2c3e50')
    background_color: C('#ecf0f1')
    background_normal: ''
    background_down: ''
    border: (3, 3, 3, 3)
    on_release: app.draw_canvas_widget.change_line_width(self.text)


<DrawCanvasWidget>:
    canvas.before:
        Color:
            rgba: [1, 1, 1, 1]
        Rectangle:
            pos: self.pos
            size: self.size

    BoxLayout:
        orientation: 'horizontal'
        padding: 2
        spacing: 2
        x: 0
        top: root.top
        size_hint: None,None
        size: 280, 44

        LineWidthButton:
            text: 'Thin'

        LineWidthButton:
            text: 'Normal'
            state: 'down'

        LineWidthButton:
            text: 'Thick'

        Button:
            text: 'Clear'
            on_release: root.clear_canvas()


    BoxLayout:
        id: bottom_box
        orientation: 'horizontal'
        padding: 2
        spacing: 2
        size: root.width, 40

        BottomColorButton:
            background_color: C('#19caad')
            state: 'down'

        BottomColorButton:
            background_color: C('#8cc7b5')

        BottomColorButton:
            background_color: C('#a0eee1')

        BottomColorButton:
            background_color: C('#bee7e9')

        BottomColorButton:
            background_color: C('#beedc7')

        BottomColorButton:
            background_color: C('#d6d5b7')

        BottomColorButton:
            background_color: C('#d1ba74')

        BottomColorButton:
            background_color: C('#e6ceac')

        BottomColorButton:
            background_color: C('#ecad9e')

        BottomColorButton:
            background_color: C('#f4606c')

        BottomColorButton:
            background_color: C('#3498db')

        BottomColorButton:
            background_color: C('#1abc9c')

        BottomColorButton:
            background_color: C('#2ecc71')

        BottomColorButton:
            background_color: C('#f1c40f')

        BottomColorButton:
            background_color: C('#e67e22')

        BottomColorButton:
            background_color: C('#e74c3c')

        BottomColorButton:
            background_color: C('#9b59bc')

        BottomColorButton:
            background_color: C('#ecf0f1')

        BottomColorButton:
            background_color: C('#95a5a6')

        BottomColorButton:
            background_color: C('#000000')