仿照Windows自带的画图程序做一个画图app,除了支持Andorid和iOS,也支持Windows
在第一章做时钟app时,我们用了Kivy的标准部件:布局,文本框和按钮。通过这些高层次的抽象,我们能够灵活的修改部件的外观—,可以使用一整套成熟的组件,而不仅仅是单个原始图形。这种方式并非放之四海而皆准,马上你就会看到,Kivy还提供了低层的抽象工具:画点和线。
我认为做画图app是自由绘画最好的方式。我们的应用会看着有点像Windows自带的画图程序。
不同的是,我们的画图app支持多平台,包括Andorid和iOS。我们也忽略了图像处理的功能,像矩形选框,图层,保存文件等。这些功能可以自己练习。
关于移动设备:Kivy完全支持iOS开发,即使你没有类似开发经验也不难。因此,建议你先在熟悉的平台上快速实现app,这样就可以省略编译的时间和一堆细节。Android开发更简单,由于Kivy Launcher可以让Kivy代码直接在Android上运行。 Kivy可以不用编译直接在Andorid上运行测试,相当给力,绝对RAD(rapid application development)。 窗口改变大小的问题,并没有广泛用于移动设备,Kivy应用在不同的移动设备和桌面系统平台使用类似的处理方式。因此,开始编写和调试都非常容易,直到版本确定的最后阶段才需要集中精力弥补这些问题。
我们还会学习Kivy中两个相反的功能:触摸屏的多点触控和桌面系统的鼠标点击。
作为移动设备的第一大法,Kivy为多点触控输入提供了一个模拟层,可以使用鼠标。可以通过右键激活功能。但是,这个多点触控模拟器并不适合真实的场景,仅适合调试用。
画图app最会这这样:
设置画板
我们的app通过root部件自动覆盖全局,整个屏幕都可以画画。到后面增加工具按钮的时候再调整。
root部件是处于最外层,每个Kivy的app都有一个,可以根据app的需求制定任何部件作为root部件。比如上一章的时钟app,BoxLayout
就是root部件;如果没其他要求,布局部件就是用来包裹其他控件的。
现在这个画图app,我们需要root部件具有更多的功能;用户应该可以画线条,支持多点触控。不过Kivy没有自带这些功能,所以我们自己建。
建立新部件很简单,只要继承Kivy的Widget
类就行。如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 |
<span class="kn">from</span> <span class="nn">kivy.app</span> <span class="kn">import</span> <span class="n">App</span> <span class="kn">from</span> <span class="nn">kivy.uix.widget</span> <span class="kn">import</span> <span class="n">Widget</span> <span class="k">class</span> <span class="nc">CanvasWidget</span><span class="p">(</span><span class="n">Widget</span><span class="p">):</span> <span class="k">pass</span> <span class="k">class</span> <span class="nc">PaintApp</span><span class="p">(</span><span class="n">App</span><span class="p">):</span> <span class="k">def</span> <span class="nf">build</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="n">CanvasWidget</span><span class="p">()</span> <span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span> <span class="n">PaintApp</span><span class="p">()</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> |
这就是画图app的main.py
,PaintApp
类就是应用的起点。以后我们不会重复这些代码,只把重要的部分显示出来。
Widget
类通常作为基类,就行Python的object
和Java的Object
。当它按照as is
方式使用时,Widget
功能极少。它没有可以直接拿来用的可视化的外观和属性。Widget
的子类都是很简单易用的。
制作好看的外观
首先,让我们做个好看的外观,虽然不是核心功能,但长相影响第一印象。下面我们改改外观,包括窗口大小,鼠标形状。
可视化外观
我认为任何画图软件的背景色都应该是白的。和第一章类似,我们在__name = '__main__'
后面加上就行:
1 2 3 4 |
<span class="kn">from</span> <span class="nn">kivy.core.window</span> <span class="kn">import</span> <span class="n">Window</span> <span class="kn">from</span> <span class="nn">kivy.utils</span> <span class="kn">import</span> <span class="n">get_color_from_hex</span> <span class="n">Window</span><span class="o">.</span><span class="n">clearcolor</span> <span class="o">=</span> <span class="n">get_color_from_hex</span><span class="p">(</span><span class="s1">'#FFFFFF'</span><span class="p">)</span> |
你可能想把import
语句放到前面,其实Kivy的一些模块导入有顺序要求,且会产生副作用,尤其是Window
对象。这在好的Python程序中很少见,导入模块产生的副作用有点小问题。
窗口大小
另一个要改的就是窗口大小,下面的改变不影响移动设备。在桌面系统上,Kivy的窗口时可以调整的,后面我们会设置禁止调整。
如果目标设备明确,设置窗口大小是很有用的,这样就可以决定屏幕分辨率的参数,实现最好的适配效果。
要改变窗口大小,就把下面的代码放到from kivy.core.window import Window
上面。
1 2 3 4 |
<span class="kn">from</span> <span class="nn">kivy.config</span> <span class="kn">import</span> <span class="n">Config</span> <span class="n">Config</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'graphics'</span><span class="p">,</span> <span class="s1">'width'</span><span class="p">,</span> <span class="s1">'960'</span><span class="p">)</span> <span class="n">Config</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'graphics'</span><span class="p">,</span> <span class="s1">'height'</span><span class="p">,</span> <span class="s1">'540'</span><span class="p">)</span> <span class="c1"># 16:9</span> |
如果要禁止窗口调整:
1 |
<span class="n">Config</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'graphics'</span><span class="p">,</span> <span class="s1">'resizable'</span><span class="p">,</span> <span class="s1">'0'</span><span class="p">)</span> |
如果没有充分理由,千万别这么做,因为把窗口调整这点小自由从用户手中拿走实在太伤感情了。如果把应用像素精确到1px,移动设备用户可能就不爽了,而Kivy布局可以建立自适应的界面。
鼠标样式
之后就是改变鼠标光标的样式。Kivy没有支持,不过可以过Pygame实现,基于SDL窗口和OpenGL内容管理模块,在Kivy的桌面平台应用开发中用途广泛。如果你这么用,移动应用大都不支持Pygame。
之后就是改变鼠标光标的样式。Kivy没有支持,不过可以过Pygame实现,基于SDL窗口和OpenGL内容管理模块,在Kivy的桌面平台应用开发中用途广泛。如果你这么用,移动应用大都不支持Pygame。
图中@
是黑的,-
是白的,其他字符是透明的。所以的线都是等宽的,且是8的倍数(SDL的限制)。鼠标的光标运行后是这样:
当前的Pygame版本有个bug,
pygame.cursors.compile()
黑白显示颠倒。以后应该会修复。不过pygame_compile_cursor()
是正确的方法,Pygame的Simple DirectMedia Layer (SDL)兼容库。 现在,我们把光标应用到app中,替换PaintApp.build
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<span class="kn">from</span> <span class="nn">kivy.base</span> <span class="kn">import</span> <span class="n">EventLoop</span> <span class="k">class</span> <span class="nc">PaintApp</span><span class="p">(</span><span class="n">App</span><span class="p">):</span> <span class="k">def</span> <span class="nf">build</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">EventLoop</span><span class="o">.</span><span class="n">ensure_window</span><span class="p">()</span> <span class="k">if</span> <span class="n">EventLoop</span><span class="o">.</span><span class="n">window</span><span class="o">.</span><span class="vm">__class__</span><span class="o">.</span><span class="vm">__name__</span><span class="o">.</span><span class="n">endswith</span><span class="p">(</span><span class="s1">'Pygame'</span><span class="p">):</span> <span class="k">try</span><span class="p">:</span> <span class="kn">from</span> <span class="nn">pygame</span> <span class="kn">import</span> <span class="n">mouse</span> <span class="c1"># pygame_compile_cursor is a fixed version of</span> <span class="c1"># pygame.cursors.compile</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">pygame_compile_cursor</span><span class="p">()</span> <span class="n">mouse</span><span class="o">.</span><span class="n">set_cursor</span><span class="p">((</span><span class="mi">24</span><span class="p">,</span> <span class="mi">24</span><span class="p">),</span> <span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mi">9</span><span class="p">),</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="k">except</span><span class="p">:</span> <span class="k">pass</span> <span class="k">return</span> <span class="n">CanvasWidget</span><span class="p">()</span> |
代码很简单,注意下面四点:
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文档提供了全部的api说明。 现在做的这些比Kivy的模块更底层,并不常用,不过也不用害怕触及更多的细节。有很多功能只能通过底层的模块实现,因为Kivy还没达到面面俱到的程度。尤其是那些不能跨平台的功能,会涉及很多系统层的实现。
Kivy/Pygame/SDL/OS的关系如下图所示:
SDL已经把系统底层的API都封装好了,兼容多个系统,Pygame再将SDL转换成Python,Kivy可以导入Pygame模块调用这些功能。
为什么不直接用SDL呢?可以看SDL文档。
多点触控模拟器
让运行桌面应用时,Kivy提供了一个模拟器实现多点触控操作。实际上是一个右击行为,获取半透明的点;按住右键时可以拖拽。
如果你没有真实的多点触控设备,这个功能可能适合调试。但是,也会占用右键的功能。不调试的时候还是建议你禁用这个功能,避免对用户造成困扰。设置方法如下:
1 |
<span class="n">Config</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'input'</span><span class="p">,</span> <span class="s1">'mouse'</span><span class="p">,</span> <span class="s1">'mouse,disable_multitouch'</span><span class="p">)</span> |
触摸绘画
要实现用户通过触摸绘画的效果,可以在用户输入后屏幕会出现一个圆圈。
部件如果带on_touch_down
事件,就可以实现上述功能。正在需要的是点击位置的坐标,为CanvasWidget
添加一个方法获取即可:
1 2 3 |
<span class="k">class</span> <span class="nc">CanvasWidget</span><span class="p">(</span><span class="n">Widget</span><span class="p">):</span> <span class="k">def</span> <span class="nf">on_touch_down</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="nb">print</span><span class="p">(</span><span class="n">touch</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="n">y</span><span class="p">)</span> |
要在屏幕上画画,我们就要实现Widget.canvas
属性。Kivy的canvas
属性是一个底层为OpenGL的可绘制层,不过没有底层图形API那么复杂,canvas
可以持续保留我们画过的图。
基本图形如圆(Color),线(Line), 矩形(Rectangle),贝塞尔曲线(Bezier),可以通过kivy.graphics
导入。
canvas简介
Canvas
的API可以直接调用,也可以通过上下文关联with
关键字调用。如下所示:
1 |
<span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Line</span><span class="p">(</span><span class="n">circle</span><span class="o">=</span><span class="p">(</span><span class="n">touch</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="mi">25</span><span class="p">)))</span> |
这里的Line
元素的参数是图形命令队列。
如果你想立刻试验代码,请先看下一节屏幕显示触摸轨迹中更完整的例子。
通过上下文关联with关键字调用可以让代码更简练,尤其是在同时操作多个指令时。下面的代码与之前一致:
1 2 |
<span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="p">:</span> <span class="n">Line</span><span class="p">(</span><span class="n">circle</span><span class="o">=</span><span class="p">(</span><span class="n">touch</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="mi">25</span><span class="p">))</span> |
需要注意的是,如前面所说,canvas上后面调用的指令不会覆盖前面调用的指令;因此,canvas是一个不断增长的数组,里面都是不断显示元素的指令,更新频率60fps,但是也不能让canvas无限增长下去。
例如,所见即所得的程序(如HTML5的<canvas>
)里有一条设计规则就是通过背景色填充擦除之前的图像。在浏览器里面可以很直观的写出:
1 2 3 4 |
<span class="c1">// JavaScript code for clearing the canvas</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">rect</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">)</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">fillStyle</span> <span class="o">=</span> <span class="s1">'#FFFFFF'</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">fill</span><span class="p">()</span> |
在Kivy设计中,这种模型也是增加指令;首先获取前面所有的图形元素,然后把它们画成矩形。这个看着挺好其实不对:
1 2 3 4 |
<span class="c1"># 看着和avaScript代码一样,但是错了。</span> <span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="p">:</span> <span class="n">Color</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="n">Rectangle</span><span class="p">(</span><span class="n">pos</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">pos</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">)</span> |
和内存泄露差不多,这个bug很久没被发现,使代码冗余,性能降低。由于显卡加速的功能,包括智能手机运行速度都很快。所以很难意识到这是一个bug。为了清除Kivy的canvas,应该用
canvas.clear()
来清除所有指令,后面会介绍。
屏幕显示触摸轨迹
我们马上做一个按钮来清屏;现在让我们把触摸的轨迹显示出来。让我们把print()
删掉,然后增加一个方法在CanvasWidget
下面:
1 2 3 4 5 |
<span class="k">class</span> <span class="nc">CanvasWidget</span><span class="p">(</span><span class="n">Widget</span><span class="p">):</span> <span class="k">def</span> <span class="nf">on_touch_down</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="p">:</span> <span class="n">Color</span><span class="p">(</span><span class="o">*</span><span class="n">get_color_from_hex</span><span class="p">(</span><span class="s1">'#0080FF80'</span><span class="p">))</span> <span class="n">Line</span><span class="p">(</span><span class="n">circle</span><span class="o">=</span><span class="p">(</span><span class="n">touch</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="mi">25</span><span class="p">),</span> <span class="n">width</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span> |
这样就每次都会画一个空心圆在画布上。Color
指令为Line
取色。
注意
hex('#0080FF80')
并不是CSS颜色格式,因为它有四个组成部分,表示alpha值,即透明度。类似于rgb()
与rgba()
的区别。
可能你会觉得奇怪,我们用Line
画的是圈,而不是直线。Kivy的图形元素具体很强的自定义功能,比如我们可以用Rectangle
和Triangle
画自定义的图片,用source
参数设置即可。
前面的程序效果如下图所示: 画图app完整的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<span class="c1"># In main.py</span> <span class="kn">from</span> <span class="nn">kivy.app</span> <span class="kn">import</span> <span class="n">App</span> <span class="kn">from</span> <span class="nn">kivy.config</span> <span class="kn">import</span> <span class="n">Config</span> <span class="kn">from</span> <span class="nn">kivy.graphics</span> <span class="kn">import</span> <span class="n">Color</span><span class="p">,</span> <span class="n">Line</span> <span class="kn">from</span> <span class="nn">kivy.uix.widget</span> <span class="kn">import</span> <span class="n">Widget</span> <span class="kn">from</span> <span class="nn">kivy.utils</span> <span class="kn">import</span> <span class="n">get_color_from_hex</span> <span class="k">class</span> <span class="nc">CanvasWidget</span><span class="p">(</span><span class="n">Widget</span><span class="p">):</span> <span class="k">def</span> <span class="nf">on_touch_down</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="p">:</span> <span class="n">Color</span><span class="p">(</span><span class="o">*</span><span class="n">get_color_from_hex</span><span class="p">(</span><span class="s1">'#0080FF80'</span><span class="p">))</span> <span class="n">Line</span><span class="p">(</span><span class="n">circle</span><span class="o">=</span><span class="p">(</span><span class="n">touch</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="mi">25</span><span class="p">),</span> <span class="n">width</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span> <span class="k">class</span> <span class="nc">PaintApp</span><span class="p">(</span><span class="n">App</span><span class="p">):</span> <span class="k">def</span> <span class="nf">build</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="n">CanvasWidget</span><span class="p">()</span> <span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span> <span class="n">Config</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'graphics'</span><span class="p">,</span> <span class="s1">'width'</span><span class="p">,</span> <span class="s1">'400'</span><span class="p">)</span> <span class="n">Config</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'graphics'</span><span class="p">,</span> <span class="s1">'height'</span><span class="p">,</span> <span class="s1">'400'</span><span class="p">)</span> <span class="n">Config</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">'input'</span><span class="p">,</span> <span class="s1">'mouse'</span><span class="p">,</span> <span class="s1">'mouse,disable_multitouch'</span><span class="p">)</span> <span class="kn">from</span> <span class="nn">kivy.core.window</span> <span class="kn">import</span> <span class="n">Window</span> <span class="n">Window</span><span class="o">.</span><span class="n">clearcolor</span> <span class="o">=</span> <span class="n">get_color_from_hex</span><span class="p">(</span><span class="s1">'#FFFFFF'</span><span class="p">)</span> <span class="n">PaintApp</span><span class="p">()</span><span class="o">.</span><span class="n">run</span><span class="p">()</span> |
这里没有加入鼠标光标显示的部分。paint.kv
文件也没有了,用build()
方法返回根部件。
注意from kivy.core.window import Window
行,是由于有些模块有副作用,所有放在后面导入。Config.set()
应该放在任何有副作用模块的前面。
下面,我们增加一些特性,让画图app实现我们想要的功能。
清屏
到目前为止,我们清屏的做法就是重启程序。下面我们增加一个按钮来清屏。我们用上一章时钟app的按钮即可,没什么新鲜,有意思的是位置。
上一章时钟app里面,我们没有讨论过位置,所有部件都放在BoxLayouts
里面。现在我们的app没有任何布局,因为根部件就是CanvasWidget
,我们没有实现任何子部件的位置。
在Kivy里面,布局部件缺失表示每一个部件都可以随意设置位置和大小(类似的UI设计工具,如Delphi,Visual Basic等等都如此)。
要让清屏按钮放在右上角,我们这么做:
1 2 3 4 5 6 7 8 |
<span class="c1"># In paint.kv</span> <span class="nt"><CanvasWidget></span><span class="p">:</span> <span class="nt">Button</span><span class="p">:</span> <span class="nt">text</span><span class="p">:</span> <span class="s">'Delete'</span> <span class="nt">right</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">root.right</span> <span class="nt">top</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">root.top</span> <span class="nt">width</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">80</span> <span class="nt">height</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">40</span> |
按钮的right
和top
属性与根部件的属性一致。我们还可以进行数学运行,如root.top – 20
。结果很直接,right
和top
属性都是绝对值。
注意我们定义了一个<CanvasWidget>
类却没有指定父类。这么做可以是因为我们在Python代码理论已经定义了一个同样的类。Kivy允许我们扩展所有的类,包括内部类,如<Button>
和<Label>
,以及自定义类。
这里体现了Kivy语言描述对象的可视化属性的一个好思路,类似于MVC设计方法,让内容与逻辑分离。同时,也更好的保持了所有Python程序的结构不变。这种Python代码与Kivy语言分离的思想让程序更容易维护。
传递事件
如果你跟着教程看到现在,准备去按清屏键。你会发现没反应,因为还没有增加事件,所有没有反馈。所有单击按钮不会有动作,相反会在画布上留下空心圈。
因为所有的触摸都是发生在CanvasWidget.on_touch_down
上,并没有传递给其他子部件,所以清屏按钮没反应。不像HTML的DOMDOM,Kivy事件不会从嵌套的元素升级为父元素显示出来。它们走另一条路,如果事件传递到父元素没有反应,才从父元素下降到子元素。
最直接的方式就是这样:
1 2 3 4 |
<span class="c1"># 注意:不是最优代码</span> <span class="k">def</span> <span class="nf">on_touch_down</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="k">for</span> <span class="n">widget</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">children</span><span class="p">:</span> <span class="n">widget</span><span class="o">.</span><span class="n">on_touch_down</span><span class="p">(</span><span class="n">touch</span><span class="p">)</span> |
实际上,Widget.on_touch_down
的默认行为有很多,所有我们可以直接调用,让代码更简练。
1 2 3 |
<span class="k">def</span> <span class="nf">on_touch_down</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="k">if</span> <span class="n">Widget</span><span class="o">.</span><span class="n">on_touch_down</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="k">return</span> |
如果事件被正常处理了,on_touch_down
这个handler返回True
。触摸按钮会返回True
是因为按钮响应了,然后很快的改变其外观。这就是为了取消我们的事件处理需要做的事情,当我们画圈的时候,方法的第二个行就return
。
清屏
现在我们回到清屏按钮上。其实很简单,就是下面两行:
1 2 |
<span class="k">def</span> <span class="nf">clear_canvas</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span> |
别忘了把事件绑定到paint.kv
文件:
1 2 |
<span class="nt">Button</span><span class="p">:</span> <span class="nt">on_release</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">root.clear_canvas()</span> |
这样就可以清屏了,同时还把按钮也清除了。因为CanvasWidget
是根部件,按钮是子部件。按钮部件本身没有被删除,它的画布Button.canvas
从CanvasWidget.canvas.children
层级中移除了,因此不存在了。
要保留按钮,可以这样:
1 2 3 4 |
<span class="k">def</span> <span class="nf">clear_canvas</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="o">.</span><span class="n">children</span> <span class="o">=</span> <span class="p">[</span><span class="n">widget</span><span class="o">.</span><span class="n">canvas</span> <span class="k">for</span> <span class="n">widget</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">children</span><span class="p">]</span> |
但是这么做不够好,因为不同的部件初始化和运行方式不同。更好的做法是:
- 从
CanvasWidget
部件中删除所有子部件; - 然后清除画布;
- 最后再重新增加子部件,这样它们就可以正确的初始化了。
这个版本有点长,但是更合理:
1 2 3 4 5 6 7 |
<span class="k">class</span> <span class="nc">CanvasWidget</span><span class="p">(</span><span class="n">Widget</span><span class="p">):</span> <span class="k">def</span> <span class="nf">clear_canvas</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">saved</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">children</span><span class="p">[:]</span> <span class="bp">self</span><span class="o">.</span><span class="n">clear_widgets</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span> <span class="k">for</span> <span class="n">widget</span> <span class="ow">in</span> <span class="n">saved</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">add_widget</span><span class="p">(</span><span class="n">widget</span><span class="p">)</span> |
解释一下saved = self.children[:]
语句。[:]
操作符是复制数组(就是“创建一个元素相同的数组”)。如果我们写saved = self.children
,那就会从self.children
和saved
同时删除所有子部件。因为Python赋值是引用,与Kivy无关。
如果想进一步了解Python的特性,可以看看StackOverflow
现在,我们已经可以用蓝色的圈钱画图了,如下所示。这当然并非最终版,请看下面的内容。
连点成线
我们的app已经可以清屏了,不过只能画圈。下面在改进一下。
要保持连续触控画线(按住然后拖拽),我们需增加一个监听器,on_touch_move
。每次使用都会收到最新点的位置。
如果我们一次只有一条线,我们可以把这条线保存为self.current_line
。但是,由于这是多点触控,我们就要用其他方法来保存touch
变量了。
之所以能实现这些,是因为每个触控自始至终都访问相同的touch
对象。还有一个touch.ud
属性,是一个字典类型,ud
就是用户数据(user data),可以灵活的跟踪所有的触控。初始值为空字典{}
。
下面我们要做的是:
- 在
on_touch_down
的handler创建一个新线,然后储存到touch.ud
。现在我们要用直线来代替空心圈。 - 在
on_touch_move
里面增加一个新点到线的末尾。我们增加的是直线元素,但是事件处理过程是每秒调用很多次实现这条线,每次都很短,最终看起来就很平滑。
更先进的图形程序可以用复杂的算法让线条呈现的更真实。包括贝塞尔曲线实现线条的高分辨率的无缝连接,并且从点的速度和压力推断线的厚度。这些具体的技术我们不打算实现了,不过读者可以作为一个练习。
上述过程的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span class="kn">from</span> <span class="nn">kivy.graphics</span> <span class="kn">import</span> <span class="n">Color</span><span class="p">,</span> <span class="n">Line</span> <span class="k">class</span> <span class="nc">CanvasWidget</span><span class="p">(</span><span class="n">Widget</span><span class="p">):</span> <span class="k">def</span> <span class="nf">on_touch_down</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="k">if</span> <span class="n">Widget</span><span class="o">.</span><span class="n">on_touch_down</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="k">return</span> <span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="p">:</span> <span class="n">Color</span><span class="p">(</span><span class="o">*</span><span class="n">get_color_from_hex</span><span class="p">(</span><span class="s1">'#0080FF80'</span><span class="p">))</span> <span class="n">touch</span><span class="o">.</span><span class="n">ud</span><span class="p">[</span><span class="s1">'current_line'</span><span class="p">]</span> <span class="o">=</span> <span class="n">Line</span><span class="p">(</span> <span class="n">points</span><span class="o">=</span><span class="p">(</span><span class="n">touch</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="n">y</span><span class="p">),</span> <span class="n">width</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span> <span class="k">def</span> <span class="nf">on_touch_move</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="k">if</span> <span class="s1">'current_line'</span> <span class="ow">in</span> <span class="n">touch</span><span class="o">.</span><span class="n">ud</span><span class="p">:</span> <span class="n">touch</span><span class="o">.</span><span class="n">ud</span><span class="p">[</span><span class="s1">'current_line'</span><span class="p">]</span><span class="o">.</span><span class="n">points</span> <span class="o">+=</span> <span class="p">(</span> <span class="n">touch</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="n">y</span><span class="p">)</span> |
这样就可以画线了。之后让我们来实现颜色选择功能,不断的完善我们的画图app。
调色板
画图app当然不能没有调色板。调色板其实就是可选颜色列表,可以让颜色选取很简单。通过图像编辑器都有调色板,带有全真彩24位色16,777,216种。如下图所示: 但是,就是你不打算完成一个主流的图像编辑器,我们也打算限制颜色的种类。因为对那些没有色彩常识的人来说,放一堆颜色只会让人头大。而且,互联网上的UI设计用色也会逐渐统一。
在我们的app中,我们打算使用扁平化的UI设计风格,基于一列精心挑选的颜色。当然,你可以选自己喜欢的颜色,因人而异。
颜色是一门学问,尤其是具体任务的兼容性与稳定性。低对比度的组合可能用来装饰元素或者标题,但是它们不符合正文的风格;另外,高对比度的颜色,如白与黑,不容易吸引注意力。
因此,颜色使用的首要原则是除非你很专业,否则用别人调好的颜色。最好的起点就是操作系统的用色。一些精彩案例如下:
- Tango调色板,在Linux开源环境中使用广泛。- Google在2014年GoogleIO大会上发布的Material design。
- 非官方的iOS 7颜色风格,超赞。
还有很多调色板可以学习,自行Google之。
按钮的子类
因为我使用的颜色很少,所以用单选按钮就可以了。Kivy的ToggleButton
可以实现功能,不过有个限制:在一个单选组内,所有的按钮可以同时不选。也就是说,画图的时候可能没颜色。当然我们也可以设定默认颜色,但是用户可能会觉得很奇怪,所有我们不打算这么用。
Python的OOP模式可以很好的解决这个问题,我们可以继承ToggleButton
类,然后改造它的功能。之后,每次都会有一个颜色被选中了。
子类还会实现另外一个功能:在调色板上,我们想让每个颜色按钮有唯一颜色。我们可以用之前的技术为每个按钮分配背景色,那就要一堆代码来分配。但是,我们如果写一个背景色属性,就可以在paint.kv
文件里面分配了。
这样就可以在paint.kv
文件中使用按钮时保持调色板定义的可读性,同时在子类中实现的具体的细节——会展示OOP程序应该怎样实现。
去掉全不选功能
首先,让我们把全不选的功能去掉。
首先,让我们实现一个标准的ToggleButton
部件。我们之间在paint.kv
文件里面增加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<span class="nt">BoxLayout</span><span class="p">:</span> <span class="nt">orientation</span><span class="p">:</span> <span class="s">'horizontal'</span> <span class="nt">padding</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">3</span> <span class="nt">spacing</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">3</span> <span class="nt">x</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">0</span> <span class="nt">y</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">0</span> <span class="nt">width</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">root.width</span> <span class="nt">height</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">40</span> <span class="nt">ToggleButton</span><span class="p">:</span> <span class="nt">group</span><span class="p">:</span> <span class="s">'color'</span> <span class="nt">text</span><span class="p">:</span> <span class="s">'Red'</span> <span class="nt">ToggleButton</span><span class="p">:</span> <span class="nt">group</span><span class="p">:</span> <span class="s">'color'</span> <span class="nt">text</span><span class="p">:</span> <span class="s">'Blue'</span> <span class="nt">state</span><span class="p">:</span> <span class="s">'down'</span> |
我们用了与BoxLayout
类似的方式,每个颜色按钮单独分配一个工具栏。布局部件本文的位置是绝对的,其x
和y
的值都是0,也就是左下角,宽度与CanvasWidget
一致。
每个ToggleButton
都属于同一color
组。因此同一时间只有一个颜色可以被选中。
改写标准行为
要实现改写,让我们定义ToggleButton
子类:
1 2 3 4 5 6 7 |
<span class="kn">from</span> <span class="nn">kivy.uix.behaviors</span> <span class="kn">import</span> <span class="n">ToggleButtonBehavior</span> <span class="kn">from</span> <span class="nn">kivy.uix.togglebutton</span> <span class="kn">import</span> <span class="n">ToggleButton</span> <span class="k">class</span> <span class="nc">RadioButton</span><span class="p">(</span><span class="n">ToggleButton</span><span class="p">):</span> <span class="k">def</span> <span class="nf">_do_press</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">state</span> <span class="o">==</span> <span class="s1">'normal'</span><span class="p">:</span> <span class="n">ToggleButtonBehavior</span><span class="o">.</span><span class="n">_do_press</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> |
这样当按下按钮,状态'normal'
就会变成'down'
。
现在我们把paint.kv
文件里面ToggleButton
改成RadioButton
,立刻就会看到不同。
这也是Kivy框架最吸引人的地方:小代码实现大功能。
要在Kivy语言中使用
RadioButton
,其定义需要在导入main.py
文件。由于现在只有一个Python文件,这并不重要,但是一定记住:自定义的Kivy部件,和其他的Python类和函数一样,需要在使用之前被导入。
彩色按钮
现在按钮的功能正常了,我们把彩色按钮都做出来。如下图所示: 要实现这些,我们得用background_color
属性。Kivy的背景色不仅可以使用单一颜色,可以用彩色;我们首先需要一个纯白色背景,然后画上想要的颜色。这样我们就只要为任意数量的彩色按钮准备两种模式(正常和按下的)即可。
这和第一章时钟app是一样的。除了按钮的中心区域允许着色,选中的状态有个黑边。
新按钮
加油!我们就快完工了,在paint.kv
里面加入新类ColorButton
:
1 2 3 4 5 6 |
<span class="nt"><ColorButton@RadioButton></span><span class="p">:</span> <span class="nt">group</span><span class="p">:</span> <span class="s">'color'</span> <span class="nt">on_release</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">app.canvas_widget.set_color(self.background_color)</span> <span class="nt">background_normal</span><span class="p">:</span> <span class="s">'color_button_normal.png'</span> <span class="nt">background_down</span><span class="p">:</span> <span class="s">'color_button_down.png'</span> <span class="nt">border</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">(3, 3, 3, 3)</span> |
你会发现,我们把group: 'color'
移到这里避免重复代码。
我们还要配置on_release
事件handler,作用于已经被选中的按钮。现在,每个按钮已经把自己的background_color
属性传递给事件handler,剩下的事情就是把颜色分配给画布。这个事件将由CanvasWidget
处理,需要通过PaintApp
类显示出来。
1 2 3 4 5 6 7 |
<span class="k">class</span> <span class="nc">PaintApp</span><span class="p">(</span><span class="n">App</span><span class="p">):</span> <span class="k">def</span> <span class="nf">build</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># set_color()方法后面实现</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas_widget</span> <span class="o">=</span> <span class="n">CanvasWidget</span><span class="p">()</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas_widget</span><span class="o">.</span><span class="n">set_color</span><span class="p">(</span> <span class="n">get_color_from_hex</span><span class="p">(</span><span class="s1">'#2980B9'</span><span class="p">))</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas_widget</span> |
这么配置的原因是我们不能在paint.kv
文件的类定义中使用root
;因为那样会指向ColorButton
自身(类规则里面的根定义在paint.kv
文件的顶层)。我们还可以设置默认颜色,就像代码里显示的。
在main.py
文件里面,让我们来实现CanvasWidget
的set_color()
方法,可以当作是ColorButton
的事件handler。代码很简单,就是把颜色作为参数:
1 2 |
<span class="k">def</span> <span class="nf">set_color</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">new_color</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Color</span><span class="p">(</span><span class="o">*</span><span class="n">new_color</span><span class="p">))</span> |
定义调色板
下面我们来定义调色板。首先让我们把RadioButton
从paint.kv
文件中删掉。
为了使用CSS颜色定义方式,我们需要将适当的函数导入paint.kv
文件。把下面这行代码放在paint.kv
文件开头。
1 |
<span class="c1">#:import C kivy.utils.get_color_from_hex</span> |
这行代码实际上和Python的代码一样:
1 |
<span class="kn">from</span> <span class="nn">kivy.utils</span> <span class="kn">import</span> <span class="n">get_color_from_hex</span> <span class="k">as</span> <span class="n">C</span> |
我们使用扁平化设计的配色方式,代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
<span class="nt">BoxLayout</span><span class="p">:</span> <span class="c1"># ...</span> <span class="nt">ColorButton</span><span class="p">:</span> <span class="nt">background_color</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">C('#2980b9')</span> <span class="nt">state</span><span class="p">:</span> <span class="s">'down'</span> <span class="nt">ColorButton</span><span class="p">:</span> <span class="nt">background_color</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">C('#16A085')</span> <span class="nt">ColorButton</span><span class="p">:</span> <span class="nt">background_color</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">C('#27AE60')</span> |
很简单吧,这样就为每个ColorButton
按钮定义了background_color
属性。其他的属性都是继承于Python中ColorButton
类的定义。
这样,增加任意数量的按钮都可以很好的排列了。
设置线的宽度
最后一个,也是最简单的功能就是设置线条的宽度。如下图所示,我们可以重用前面调色板的资源和样式。
这个UI也是一种RadioButton
子类,命名为LineWidthButton
。在paint.kv
文件中就是这样:
1 2 3 4 5 |
<span class="nt"><LineWidthButton@ColorButton></span><span class="p">:</span> <span class="nt">group</span><span class="p">:</span> <span class="s">'line_width'</span> <span class="nt">on_release</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">app.canvas_widget.set_line_width(self.text)</span> <span class="nt">color</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">C('#2C3E50')</span> <span class="nt">background_color</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">C('#ECF0F1')</span> |
与ColorButton
不同之处在于第2、3行代码。这些按钮属于另外一组,由其他的事件handler触发。当然,这两组按钮依然很相似。
布局很简单,和调色板的样式一致,只是垂直摆放:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<span class="nt">BoxLayout</span><span class="p">:</span> <span class="nt">orientation</span><span class="p">:</span> <span class="s">'vertical'</span> <span class="nt">padding</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">2</span> <span class="nt">spacing</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">2</span> <span class="nt">x</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">0</span> <span class="nt">top</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">root.top</span> <span class="nt">width</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">80</span> <span class="nt">height</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">110</span> <span class="nt">LineWidthButton</span><span class="p">:</span> <span class="nt">text</span><span class="p">:</span> <span class="s">'Thin'</span> <span class="nt">LineWidthButton</span><span class="p">:</span> <span class="nt">text</span><span class="p">:</span> <span class="s">'Normal'</span> <span class="nt">state</span><span class="p">:</span> <span class="s">'down'</span> <span class="nt">LineWidthButton</span><span class="p">:</span> <span class="nt">text</span><span class="p">:</span> <span class="s">'Thick'</span> |
注意
CanvasWidget.set_line_width
事件监听器会接受宽度调节按钮的text
属性。这样实现是为了简化,允许我们为每一个按钮定义一个唯一的宽度值。 实际开发中,这种方法固然无可厚非。但是,当我们要把文字翻译成日语或法语的时候,这种对应关系就丢失了。
改变线条宽度
让我们把前面做好的模块都组合起来,这样就可以控制线条的粗细了。我们把线条宽度存储在CanvasWidget.line_width
变量中,与按钮的文字一一对应,然后用on_touch_down
触发事件改变线条宽度。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span class="k">class</span> <span class="nc">CanvasWidget</span><span class="p">(</span><span class="n">Widget</span><span class="p">):</span> <span class="n">line_width</span> <span class="o">=</span> <span class="mi">2</span> <span class="k">def</span> <span class="nf">on_touch_down</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">touch</span><span class="p">):</span> <span class="c1"># ...</span> <span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">canvas</span><span class="p">:</span> <span class="n">touch</span><span class="o">.</span><span class="n">ud</span><span class="p">[</span><span class="s1">'current_line'</span><span class="p">]</span> <span class="o">=</span> <span class="n">Line</span><span class="p">(</span> <span class="n">points</span><span class="o">=</span><span class="p">(</span><span class="n">touch</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">touch</span><span class="o">.</span><span class="n">y</span><span class="p">),</span> <span class="n">width</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">line_width</span><span class="p">)</span> <span class="k">def</span> <span class="nf">set_line_width</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">line_width</span><span class="o">=</span><span class="s1">'Normal'</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">line_width</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'Thin'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s1">'Normal'</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s1">'Thick'</span><span class="p">:</span> <span class="mi">4</span> <span class="p">}[</span><span class="n">line_width</span><span class="p">]</span> |
这样就完成Kivy的画图app了,开始画图吧。
总结
这一章,我们重点学习了Kivy应用开发中的一些方法,包括自定义窗口,改变鼠标光标,窗口大小,背景色,通过画布指令绘制自定义的图形,正确的处理支持多平台的触摸事件,并且考虑多点触控的情况。
在完成画图app之后,关于Kivy的一件显而易见的事情就是这个框架具有高度的开放性和通用性。不需要一大堆死板的组件,Kivy让开发者可以通过图形基本元素和行为的运用,让自定义模块变得简单灵活。也就是说,Kivy没有自带很多开箱即用的部件,但是通过几行Python代码就可以做出需要的东西。
模块化的API设计方法缺乏美感,因为它限制了设计的柔性。最终的结果完全的满足你对项目的需求。客户总想要一些爆点,比如三角形按钮——当然,你还可以为它增加质地,这些都可以两三行代码搞定。(假如你想用WinAPI做一个三角形按钮。那就真掉坑里了。)
Kivy的自定义部件还可以重用。实际上,你可以把main.py
的CanvasWidget
模块导入其他应用。
自然用户界面
我们的第二个应用比第一个应用更具交互性。不仅是在按钮上,还有多点触控手势。
所有的窗口都支持触摸屏,对用户来说这是普遍共识,尤其在触摸屏设备上。只要用手指就可以绘画,好像在真实的画布上,即使手指很脏也可以上面画画。
这种界面被称为NUI(自然界面,natural user interface)。有一些有趣的特性:NUI应用可以被小朋友或者宠物使用——可以在屏幕上看到和触摸图形元素。这是一种自然、直观的界面,一种“不需要思考”的事情,与Norton Commander的反直觉截然不同。直觉不应该接受蓝屏、ASCII码的表现形式。
下一章,我们将建立另外一个Kivy程序,只能Android用。将Python与Android API的Java类很好的结合在一起。