#!/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
#