#!/usr/bin/env python # coding: utf-8 # # Kivy指南-2-画图app # # > 仿照Windows自带的画图程序做一个画图app,除了支持Andorid和iOS,也支持Windows # # - toc: true # - badges: true # - comments: true # - categories: [jupyter,Kivy,Android,iOS] # - image: kbpic/2.1paintapp.png # 在第一章做时钟app时,我们用了Kivy的标准部件:布局,文本框和按钮。通过这些高层次的抽象,我们能够灵活的修改部件的外观—,可以使用一整套成熟的组件,而不仅仅是单个原始图形。这种方式并非放之四海而皆准,马上你就会看到,Kivy还提供了低层的抽象工具:画点和线。 # # # 我认为做画图app是自由绘画最好的方式。我们的应用会看着有点像Windows自带的画图程序。 # # 不同的是,我们的画图app支持多平台,包括Andorid和iOS。我们也忽略了图像处理的功能,像矩形选框,图层,保存文件等。这些功能可以自己练习。 # # >关于移动设备:Kivy完全支持iOS开发,即使你没有类似开发经验也不难。因此,建议你先在熟悉的平台上快速实现app,这样就可以省略编译的时间和一堆细节。Android开发更简单,由于[Kivy Launcher](https:// # play.google.com/store/apps/details?id=org. # kivy.pygame)可以让Kivy代码直接在Android上运行。 # >Kivy可以不用编译直接在Andorid上运行测试,相当给力,绝对RAD(rapid application development)。 # >窗口改变大小的问题,并没有广泛用于移动设备,Kivy应用在不同的移动设备和桌面系统平台使用类似的处理方式。因此,开始编写和调试都非常容易,直到版本确定的最后阶段才需要集中精力弥补这些问题。 # 我们还会学习Kivy中两个相反的功能:触摸屏的多点触控和桌面系统的鼠标点击。 # # 作为移动设备的第一大法,Kivy为多点触控输入提供了一个模拟层,可以使用鼠标。可以通过右键激活功能。但是,这个多点触控模拟器并不适合真实的场景,仅适合调试用。 # # 画图app最会这这样: # # ![paintapp](kbpic/2.1paintapp.png) # ## 设置画板 # 我们的app通过root部件自动覆盖全局,整个屏幕都可以画画。到后面增加工具按钮的时候再调整。 # # root部件是处于最外层,每个Kivy的app都有一个,可以根据app的需求制定任何部件作为root部件。比如上一章的时钟app,`BoxLayout`就是root部件;如果没其他要求,布局部件就是用来包裹其他控件的。 # # 现在这个画图app,我们需要root部件具有更多的功能;用户应该可以画线条,支持多点触控。不过Kivy没有自带这些功能,所以我们自己建。 # # 建立新部件很简单,只要继承Kivy的`Widget`类就行。如下所示: # In[ ]: from kivy.app import App from kivy.uix.widget import Widget class CanvasWidget(Widget): pass class PaintApp(App): def build(self): return CanvasWidget() if __name__ == '__main__': PaintApp().run() # 这就是画图app的`main.py`,`PaintApp`类就是应用的起点。以后我们不会重复这些代码,只把重要的部分显示出来。 # >`Widget`类通常作为基类,就行Python的`object`和Java的`Object`。当它按照`as is`方式使用时,`Widget`功能极少。它没有可以直接拿来用的可视化的外观和属性。`Widget`的子类都是很简单易用的。 # ## 制作好看的外观 # 首先,让我们做个好看的外观,虽然不是核心功能,但长相影响第一印象。下面我们改改外观,包括窗口大小,鼠标形状。 # ### 可视化外观 # 我认为任何画图软件的背景色都应该是白的。和第一章类似,我们在`__name = '__main__'`后面加上就行: # In[ ]: from kivy.core.window import Window from kivy.utils import get_color_from_hex Window.clearcolor = get_color_from_hex('#FFFFFF') # 你可能想把`import`语句放到前面,其实Kivy的一些模块导入有顺序要求,且会产生副作用,尤其是`Window`对象。这在好的Python程序中很少见,导入模块产生的副作用有点小问题。 # ### 窗口大小 # 另一个要改的就是窗口大小,下面的改变不影响移动设备。在桌面系统上,Kivy的窗口时可以调整的,后面我们会设置禁止调整。 # # >如果目标设备明确,设置窗口大小是很有用的,这样就可以决定屏幕分辨率的参数,实现最好的适配效果。 # # 要改变窗口大小,就把下面的代码放到`from kivy.core.window import Window`上面。 # In[ ]: from kivy.config import Config Config.set('graphics', 'width', '960') Config.set('graphics', 'height', '540') # 16:9 # 如果要禁止窗口调整: # In[ ]: Config.set('graphics', 'resizable', '0') # 如果没有充分理由,千万别这么做,因为把窗口调整这点小自由从用户手中拿走实在太伤感情了。如果把应用像素精确到1px,移动设备用户可能就不爽了,而Kivy布局可以建立自适应的界面。 # ### 鼠标样式 # 之后就是改变鼠标光标的样式。Kivy没有支持,不过可以过Pygame实现,基于SDL窗口和OpenGL内容管理模块,在Kivy的桌面平台应用开发中用途广泛。如果你这么用,移动应用大都不支持Pygame。 # 之后就是改变鼠标光标的样式。Kivy没有支持,不过可以过Pygame实现,基于SDL窗口和OpenGL内容管理模块,在Kivy的桌面平台应用开发中用途广泛。如果你这么用,移动应用大都不支持Pygame。 # ![mousepointer](kbpic/2.2mousepointer.png) # # 图中`@`是黑的,`-`是白的,其他字符是透明的。所以的线都是等宽的,且是8的倍数(SDL的限制)。鼠标的光标运行后是这样: # ![crosshair](kbpic/2.3crosshair.png) # # >当前的Pygame版本有个bug,`pygame.cursors.compile()`黑白显示颠倒。以后应该会修复。不过`pygame_compile_cursor()`是正确的方法,[Pygame的Simple DirectMedia Layer (SDL)兼容库](http://goo.gl/2KaepD)。 # # 现在,我们把光标应用到app中,替换`PaintApp.build`方法: # In[ ]: from kivy.base import EventLoop class PaintApp(App): def build(self): EventLoop.ensure_window() if EventLoop.window.__class__.__name__.endswith('Pygame'): try: from pygame import mouse # pygame_compile_cursor is a fixed version of # pygame.cursors.compile a, b = pygame_compile_cursor() mouse.set_cursor((24, 24), (9, 9), a, b) except: pass return CanvasWidget() # 代码很简单,注意下面四点: # - `EventLoop.ensure_window()`: 这个函数到app窗口 ( `EventLoop.window` ) 准备好才执行。 # - `EventLoop.window.__class__.__name__.endswith('Pygame')`: # 这个条件检查窗口名称Pygame,只是Pygame条件下才执行自定义光标。 # - `try ... except`模块里面是Pygame的`mouse.set_cursor`。 # - 变量`a`和`b`通过SDL构建了光标,表示异或(XOR)和与(AND),都是SDL独有的实现方式。 # >[Pygame文档](http://www.pygame.org)提供了全部的api说明。 # # 现在做的这些比Kivy的模块更底层,并不常用,不过也不用害怕触及更多的细节。有很多功能只能通过底层的模块实现,因为Kivy还没达到面面俱到的程度。尤其是那些不能跨平台的功能,会涉及很多系统层的实现。 # # Kivy/Pygame/SDL/OS的关系如下图所示: # ![multiapi](kbpic/2.4multiapi.png) # # SDL已经把系统底层的API都封装好了,兼容多个系统,Pygame再将SDL转换成Python,Kivy可以导入Pygame模块调用这些功能。 # # >为什么不直接用SDL呢?可以看[SDL文档](https://www.libsdl.org/)。 # ### 多点触控模拟器 # 让运行桌面应用时,Kivy提供了一个模拟器实现多点触控操作。实际上是一个右击行为,获取半透明的点;按住右键时可以拖拽。 # # 如果你没有真实的多点触控设备,这个功能可能适合调试。但是,也会占用右键的功能。不调试的时候还是建议你禁用这个功能,避免对用户造成困扰。设置方法如下: # In[ ]: Config.set('input', 'mouse', 'mouse,disable_multitouch') # ## 触摸绘画 # 要实现用户通过触摸绘画的效果,可以在用户输入后屏幕会出现一个圆圈。 # # 部件如果带`on_touch_down`事件,就可以实现上述功能。正在需要的是点击位置的坐标,为`CanvasWidget`添加一个方法获取即可: # In[ ]: class CanvasWidget(Widget): def on_touch_down(self, touch): print(touch.x, touch.y) # 要在屏幕上画画,我们就要实现`Widget.canvas`属性。Kivy的`canvas`属性是一个底层为OpenGL的可绘制层,不过没有底层图形API那么复杂,`canvas`可以持续保留我们画过的图。 # # 基本图形如圆(Color),线(Line), 矩形(Rectangle),贝塞尔曲线(Bezier),可以通过`kivy.graphics`导入。 # ### canvas简介 # `Canvas`的API可以直接调用,也可以通过上下文关联`with`关键字调用。如下所示: # In[ ]: self.canvas.add(Line(circle=(touch.x, touch.y, 25))) # 这里的`Line`元素的参数是图形命令队列。 # # >如果你想立刻试验代码,请先看下一节**屏幕显示触摸轨迹**中更完整的例子。 # # 通过上下文关联with关键字调用可以让代码更简练,尤其是在同时操作多个指令时。下面的代码与之前一致: # In[ ]: with self.canvas: Line(circle=(touch.x, touch.y, 25)) # 需要注意的是,如前面所说,canvas上后面调用的指令不会覆盖前面调用的指令;因此,canvas是一个不断增长的数组,里面都是不断显示元素的指令,更新频率60fps,但是也不能让canvas无限增长下去。 # # 例如,所见即所得的程序(如HTML5的``)里有一条设计规则就是通过背景色填充擦除之前的图像。在浏览器里面可以很直观的写出: # # ```JavaScript # // JavaScript code for clearing the canvas # canvas.rect(0, 0, width, height) # canvas.fillStyle = '#FFFFFF' # canvas.fill() # ``` # # 在Kivy设计中,这种模型也是增加指令;首先获取前面所有的图形元素,然后把它们画成矩形。这个看着挺好其实不对: # In[ ]: # 看着和avaScript代码一样,但是错了。 with self.canvas: Color(1, 1, 1) Rectangle(pos=self.pos, size=self.size) # >和内存泄露差不多,这个bug很久没被发现,使代码冗余,性能降低。由于显卡加速的功能,包括智能手机运行速度都很快。所以很难意识到这是一个bug。为了清除Kivy的canvas,应该用`canvas.clear()`来清除所有指令,后面会介绍。 # ### 屏幕显示触摸轨迹 # 我们马上做一个按钮来清屏;现在让我们把触摸的轨迹显示出来。让我们把`print()`删掉,然后增加一个方法在`CanvasWidget`下面: # In[ ]: class CanvasWidget(Widget): def on_touch_down(self, touch): with self.canvas: Color(*get_color_from_hex('#0080FF80')) Line(circle=(touch.x, touch.y, 25), width=4) # 这样就每次都会画一个空心圆在画布上。`Color`指令为`Line`取色。 # > 注意`hex('#0080FF80')`并不是CSS颜色格式,因为它有四个组成部分,表示alpha值,即透明度。类似于`rgb()`与`rgba()`的区别。 # # 可能你会觉得奇怪,我们用`Line`画的是圈,而不是直线。Kivy的图形元素具体很强的自定义功能,比如我们可以用`Rectangle`和`Triangle`画自定义的图片,用`source`参数设置即可。 # # 前面的程序效果如下图所示: # ![Displayingtouches](kbpic/2.5Displayingtouches.png) # 画图app完整的代码如下: # In[ ]: # In main.py from kivy.app import App from kivy.config import Config from kivy.graphics import Color, Line from kivy.uix.widget import Widget from kivy.utils import get_color_from_hex class CanvasWidget(Widget): def on_touch_down(self, touch): with self.canvas: Color(*get_color_from_hex('#0080FF80')) Line(circle=(touch.x, touch.y, 25), width=4) class PaintApp(App): def build(self): return CanvasWidget() if __name__ == '__main__': Config.set('graphics', 'width', '400') Config.set('graphics', 'height', '400') Config.set('input', 'mouse', 'mouse,disable_multitouch') from kivy.core.window import Window Window.clearcolor = get_color_from_hex('#FFFFFF') PaintApp().run() # 这里没有加入鼠标光标显示的部分。`paint.kv`文件也没有了,用`build()`方法返回根部件。 # # 注意`from kivy.core.window import Window`行,是由于有些模块有副作用,所有放在后面导入。`Config.set()`应该放在任何有副作用模块的前面。 # # 下面,我们增加一些特性,让画图app实现我们想要的功能。 # ## 清屏 # 到目前为止,我们清屏的做法就是重启程序。下面我们增加一个按钮来清屏。我们用上一章时钟app的按钮即可,没什么新鲜,有意思的是位置。 # # 上一章时钟app里面,我们没有讨论过位置,所有部件都放在`BoxLayouts`里面。现在我们的app没有任何布局,因为根部件就是`CanvasWidget`,我们没有实现任何子部件的位置。 # # 在Kivy里面,布局部件缺失表示每一个部件都可以随意设置位置和大小(类似的UI设计工具,如Delphi,Visual Basic等等都如此)。 # # 要让清屏按钮放在右上角,我们这么做: # # ```yaml # # In paint.kv # : # Button: # text: 'Delete' # right: root.right # top: root.top # width: 80 # height: 40 # ``` # # 按钮的`right`和`top`属性与根部件的属性一致。我们还可以进行数学运行,如`root.top – 20`。结果很直接,`right`和`top`属性都是绝对值。 # # 注意我们定义了一个``类却没有指定父类。这么做可以是因为我们在Python代码理论已经定义了一个同样的类。Kivy允许我们扩展所有的类,包括内部类,如`