#!/usr/bin/env python # coding: utf-8 # # Kivy指南-1-时钟app # # > 一个仿iOS和Android内置时钟应用的app # # - toc: true # - badges: true # - comments: true # - categories: [jupyter,Kivy,Android,iOS] # - image: kbpic/1.1clockapp.png # 一个仿iOS和Android内置时钟应用的app。分两部分: # 1. 个没有交互的数字时钟,简述Kivy的事件驱动(event-driven)方法,引入计时器的功能,持续更新。 # 2. 交互的秒表功能,设计流畅的自适应布局。 # # # 学习大纲: # * Kivy语言基础,DSL(domain-specific language)处理部件(widgets) # * Kivy布局方式 # * 自定义字体和文字样式 # * 事件管理 # # app最终效果如下,只要60行代码,Python代码和kv代码各一半。 # # ![clockapp](kbpic/1.1clockapp.png "clockapp") # ## 起点 # 将kivy的`helloworld`稍作修改。增加一个布局容器(layout container),`BoxLayout`,后面可增加更多部件。 # In[4]: # %load ../0_Hello/main.py from kivy.app import App class ClockApp(App): pass if __name__ == "__main__": ClockApp().run() # ```yaml # # clock.kv # BoxLayout: # orientation: 'vertical' # # Label: # text: '00:00:00' # ``` # `BoxLayout`容器可以包含多个子部件,水平或垂直堆放。由于`kv`只有一个子部件,`BoxLayout`就会让它充满所有空间。 # > 当运行`main.py`文件时,Kivy自动调用`clock.kv`。类名是`ClockApp`,`.kv`文件名就是`clock`,类名小写并去掉`App`。 # ## 新UI # 扁平化设计模式(flat design paradigm)如日中天,覆盖Web,移动,桌面应用领域,兴起于iOS7和Win8。互联网公司也追随,于Google I/O 2014出Material design,其他HTML5框架,如Bootstrap亦如是。 # # 扁平化设计强调内容胜于外观,忽略逼真图片的阴影和细致的质地,支持纯色和简单几何图形。强调比学院派的仿真设计(skeuomorphic design)更简单的程序化创造,前者倾向于丰富视觉效果和艺术感。 # # >仿真主义是用户界面设计的主流方法。认为应用程序属于真实世界的一部分,比如一个带按钮的计算器app应该被做成廉价的、物质的计算器的感觉,有助于提升用户体验(得看是谁用)。 # 如今,放弃视觉细节而转向简单、流线型界面仿佛是共识。另一方面,仅靠一堆彩色框框就想做成惊世骇俗的作品很有难度。扁平化设计成了文字排版好的代名词原因就是文字成了UI设计中重要的部分,所有我们要让文字好看。 # ### 设计灵感 # 模仿Android 4.1 Jelly Bean的时钟设计。字体是Google的[Roboto](http://www.fontsquirrel.com/fonts/roboto)字体,取代了Android 4.0 Ice Cream Sandwich的Droid字体。 # # ![clockui](kbpic/1.2android4.1clockUI.png "clockui") # ### 加载自定义字体 # Kivy默认是Droid Sans字体,通过`font_name`属性可设置自定义字体。这里只有一种字体,可以直接将`.ttf`文件名放上。 # ```yaml # # clock.kv # Label: # font_name: 'Loster.ttf' # ``` # 但是我们要好几种字体,一个属性就不够了。因为不同字体都是单个文件,而属性只能跟一个文件名。涉及多种字体可以用`LabelBase.register`方法可以接受多种字体,如下所示: # In[7]: LabelBase.register( name="Roboto", fn_regular="Roboto-Regular.ttf", fn_bold="Roboto-Bold.ttf", fn_italic="Roboto-Italic.ttf", fn_bolditalic="Roboto-BoldItalic.ttf", ) # 改进之后,一个部件的`font_name`属性可设置多种自定义字体了。但这种方法有两个限制: # 1. kivy只接受TrueType的`.ttf`字体。如果是OpenType的`.otf`或者网页字体如`.woff`,得先[转换](http://fontforge.org/)。 # 2. 字体normal,italic,bold,bold italic四种样式有最大值。旧字体没问题,如Droid Sans。但是新字体都有4到20多种样式,其高度和其他特征也不同。Roboto至少有12种样式。 # # 第二点迫使我们选择app字体时要把12种样式全放进去,这么做会增大app的体积,Roboto字体有1.7M。 # # 本例中我们只要两种样式:浅色(`Roboto-Thin.ttf`)和加粗(`Roboto-Medium.ttf`) # In[ ]: from kivy.core.text import LabelBase LabelBase.register( name="Roboto", fn_regular="Roboto-Thin.ttf", fn_bold="Roboto-Medium.ttf" ) # 下面我们来使用字体,放到`Label`后面即可。 # ```yaml # # clock.kv # Label: # text: '00:00:00' # font_name: 'Roboto' # font_size: 60 # ``` # ### 字体格式 # markup语言毋庸置疑HTML。Kivy实现了另外一种BBCode的markup语言,用[]作标签。 # # | BBCode tag |Effect on text | # | :-------------: |:-------------:| # | [b]...[/b] | **Effect on text**| # | [i]...[/i] | *Italic*| # | [font=Lobster]...[/font] | Change font| # | [color=#FF0000]...[/color] | Set color with CSS-like syntax| # | [sub]...[/sub] | Subscript (text below the line)| # | [sup]...[/sup] | Superscript (text above the line)| # | [ref=name]...[/ref] | Clickable zone, `` in HTML| # | [anchor=name] | Named location, `` in HTML| # # >由于Kivy发展很快,以上内容绝非最终版本,详情查阅[kivy文档](http://kivy.org)。 # 再看看图2,我们要实现小时数字加粗的效果就easy了。 # # ```yaml # # clock.kv # Label: # text: '[b]00[/b]:00:00' # markup: True # ``` # # Kivy的BBCode需要将markup属性设置为True。 # >如果要整行加粗,可以直接设bold属性为True。其他斜体、颜色、字体、大小同理。 # ### 改变背景色 # 下面我们来调整窗口背景色,是`Window`对象的一个属性。可以在`__name__ == '__main__'`后面增加代码: # In[ ]: from kivy.core.window import Window from kivy.utils import get_color_from_hex Window.clearcolor = get_color_from_hex("#101216") # 函数`get_color_from_hex`允许使用[CSS的RGB颜色值(`#RRGGBB`)](https:// # developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_started/Color),也可以用其他函数。 # ## 显示时间 # 大多数UI框架都是事件驱动,Kivy也不例外。这种方式相比通常的程序更简单——事件驱动的代码需要不断返回到主循环(`main loop`);但是,这么做不能处理用户行为(点击鼠标,改变窗口),而且界面会冻结(`freeze`),Windows经常这样`程序停止响应`。 # # 总之,不能在程序里面加无限循环实现。 # In[ ]: # Don't do this while True: update_time() # some function that displays time sleep(1) # 理论上可行,但UI实际会失去相应,直到系统或用户关闭进程才结束。记住Kivy内部一直运行主循环,我们可以通过事件与计算器来利用它。 # # 事件驱动还意味着我们需要对不同事件作出响应,可能是用户输入,网络行为,或超时等等。 # # 很多程序监听共同事件之一就是`App.on_start`,定义在类里面,在app初始化的时候调用。另一个常见的是`on_press`,当用户点击,tap,或其他按钮操作时启用。 # # 通过时间和计时器,我们就可以用Kivy自带的Clock类实现想要的功能。两个方法: # * `Clock.schedule_once`:在一段时间后运行一次 # * `Clock.schedule_interval`:周期性的运行 # # > 和JavaScript中的`window.setTimeout`和`window.setInterval`类似。其实Kivy和JS很像,即使API完全不同。 # # `Clock`所有的计时事件都是Kivy主循环的一部分。这种方法与线程不同,这样调用一个阻塞函数可能会阻止其他事件被及时唤醒。 # ### 更新屏幕上的时间 # 要接入显示时间的`Label`部件,需要给它一个`id`,通过`id`属性来获取部件,这和Web开发类似。 # # ```yaml # # clock.kv # Label: # id: time # ``` # 之后就可以通过`root.ids.time`来接入`Label`部件了。这里`root`就是`BoxLayout`。 # # 给`ClockApp`类增加一个`update_time`方法来更新时间: # In[ ]: def update_time(self, nap): self.root.ids.time.text = strftime("[b]%H[/b]:%M:%S") # 再增加一个调度功能,让程序更新后每秒更新一次: # In[ ]: def on_start(self): Clock.schedule_interval(self.update_time, 1) # 运行程序看看是不是开始更新了。代码如下: # In[ ]: # %load main.py from kivy.app import App from kivy.clock import Clock from kivy.core.window import Window from kivy.utils import get_color_from_hex from time import strftime class ClockApp(App): def update_time(self, nap): self.root.ids.time.text = strftime("[b]%H[/b]:%M:%S") def on_start(self): Clock.schedule_interval(self.update_time, 1) if __name__ == "__main__": Window.clearcolor = get_color_from_hex("#301216") ClockApp().run() # 看看python的`time`标准库`strftime`函数是如何与Kivy的BBCode组合成C语言字符串的。 # # ```yaml # # %load clock.kv # BoxLayout: # orientation: 'vertical' # # Label: # text: '[b]00[/b]:00:00' # markup: True # id: time # ``` # ### 用属性绑定部件 # 除了ID绑定部件,还可以新建一个属性,在kv文件中进行绑定。这么做更符合DRY原则,只是多几行代码。如下所示: # In[ ]: # In main.py from kivy.properties import ObjectProperty from kivy.uix.boxlayout import BoxLayout class ClockLayout(BoxLayout): time_prop = ObjectProperty(None) # 我们在这段代码用`BoxLayout`写了个新root部件类,它有一个自定义属性`time_prop`,将连接`Label`部件。 # # 在`clock.kv`文件里,我们把属性绑定`id`,自定义属性和默认属性语法一致: # # ```yaml # # %load clock.kv # ClockLayout: # time_prop: time # # Label: # id: time # ``` # # 这样,Python代码不需要知道`id`就可以连接`Label`部件,用新属性`root.time_prop.text = "demo"`。 # # 这样做使代码的可移植性更好,消除了反射(refactor)时Python代码同步的问题。靠属性还是`root.ids`去连接Python代码这事儿,只是代码风格问题,不重要。后面还会介绍其他Kivy属性的用法,让数据绑定更容易。 # ## 布局基础 # Kivy提供了一堆`Layout`类来布局。`Layout`又是`Widget`类的子类,是个容器类。每个布局都是影响其子类位置和尺寸。 # # 在这个app中,我们的UI很直接,不需要什么神奇,如下所示: # # ![layout](kbpic/1.3layout.png "layout") # 做这种界面就要`BoxLayout`,一种一维网格。在`clock.kv`里面已经有`BoxLayout`了,只有一个子部件。Kivy的布局默认充满屏幕,所以自动适应屏幕。 # # 如果增加一个`Layout`,就会分一半屏幕,`vertical`和`horizontal`决定分割的方向。 # # 我们这里就用`vertical`分三块,然后中间那块用`horizontal`分两块,Esay吧。 # ### 完成布局 # 由于中间这块是按钮,不应该比时间还大,可以增加一个`height`属性,然后设置`size_hint`属性为`None`。`size_hint`属性是一个元组`(宽, 高)`,影响部件的宽和高。如果你想用绝对高度和宽度,就要设置`size_hint`属性为`None`,否则高度和宽度设置无效,部件会自动计算尺寸。代码如下: # # ```yaml # # %load clock.kv # BoxLayout: # orientation: 'vertical' # Label: # id: time # text: '[b]00[/b]:00:00' # font_name: 'Roboto' # font_size: 60 # markup: True # BoxLayout: # height: 90 # orientation: 'horizontal' # padding: 20 # spacing: 20 # size_hint: (1, None) # Button: # text: 'Start' # font_name: 'Roboto' # font_size: 25 # bold: True # Button: # text: 'Reset' # font_name: 'Roboto' # font_size: 25 # bold: True # Label: # id: stopwatch # text: '00:00.[size=40]00[/size]' # font_name: 'Roboto' # font_size: 60 # markup: True # ``` # 运行代码,会发现按钮没有完全填充`BoxLayout`,因为用了`padding`和`spacing`属性,与CSS类似。`main.py`代码如下: # In[ ]: # %load main.py from kivy.app import App from kivy.clock import Clock from kivy.core.window import Window from kivy.utils import get_color_from_hex from kivy.core.text import LabelBase from time import strftime LabelBase.register( name="Roboto", fn_regular="Roboto-Thin.ttf", fn_bold="Roboto-Medium.ttf" ) class ClockApp(App): def update_time(self, nap): self.root.ids.time.text = strftime("[b]%H[/b]:%M:%S") def on_start(self): Clock.schedule_interval(self.update_time, 1) if __name__ == "__main__": Window.clearcolor = get_color_from_hex("#123456") ClockApp().run() # ### 减少重复 # 之前的kv代码一堆重复,其实可以借助CSS的方法是代码更精炼,更DRY。在`BoxLayout`外面增加一个新定义: # # ```yaml # # %load clock.kv #