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