Kivy是一个很优秀的,基于Python的GUI库,可以利用Python快速编程的特点,快速的编写windows, linux, mac, android, ios等主流平台的应用程序。同wxPython、PyQt相比,最大的优点是可以快速地编写移动应用程序。
在 windows 命令行中,执行以下命令
python -m pip install docutils pygments pypiwin32 kivy.deps.sdl2 kivy.deps.glew
python -m pip install kivy.deps.gstreamer
python -m pip install kivy
以上如果安装过慢,可以使用清华镜像:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package
安装kivy官方示例
python -m pip install kivy_examples
验证kivy安装成功:
1 from kivy.app import App
2 from kivy.uix.button import Button
3
4 class TestApp(App):
5 def build(self):
6 return Button(text='iPaoMi')
7
8 TestApp().run()
你将看到执行上面的 Python 代码,会运行如下的窗口,这可以算是 kivy 版的 hello world 了。
4.1 Kivy基础
创建一个kivy应用程序步骤:
下面是一个最小化应用程序的例子:
import kivy
kivy.require('1.0.6') # 用你当前的kivy版本替换
from kivy.app import App
from kivy.uix.label import Label
class MyApp(App):
def build(self):
return Label(text='Hello world')
if __name__ == '__main__':
MyApp().run()
你可以保存上面的代码到一个诸如main.py的text文件中,并尝试运行它。
三、Kivy APP的生命周期
首先,让我们熟悉一下Kivy应用程序的生命周期:
正如你上面看到的,对于所有的应用程序,我们的入口就是run()方法,在我们的例子中就是MyApp().run()。我们会面会重新回顾这里。下面我们首先看第一行代码:
from kivy.app import App
你的应用程序的基类需要继承APP类,它在kivy_installation_dir/kivy/app.py中。
提示:如果你想进一步的研究APP类,你可以打开该文件进行深入的探讨。我们鼓励你重读它的代码。Kivy是基于Python实现的并且其文档都在实际的文件里。
第二行:
from kivy.uix.label import Label
请注意,包的路径被展示了。uix模块包含着例如布局、部件等用户接口元素。
再看第五行:
class MyApp(App):
这里定义了我们的应用程序的基类。你仅仅需要改变一下你的应用程序MyApp的名字。
第七行:
def build(self):
正如在生命周期图片显示的那样,这是你初始化并返回你的根部件的地方。我们的例子在第八行:
return Label(text='Hello Kivy')
这里我们初始化了一个标签,标签文本是:Hello Kivy,并返回了它的实例。这个标签将作为我们应用程序的根部件。
现在我们将在11行和12行运行我们的应用程序:
if __name__ == '__main__':
MyApp().run()
这里,MyAPP类被实例化并运行了它的run()方法。这样就初始化并启动了Kivy应用程序。
四、运行应用程序
为了运行应用程序,根据你的操作系统,按照下面的说明:
$ python main.py
$python main.py
或者
c:/appdir>kivy.bat main.py
$ kivy main.py
Android:
你的应用程序需要一些补充的文件以便在安卓上运行。请参阅为安卓程序打包
运行程序后,一个诸如下面的窗口将被呈现:
五、定制应用程序
下面让我们扩展一下,做一个简单的用户名/密码的输入页面。
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
class LoginScreen(GridLayout):
def __init__(self, **kwargs):
super(LoginScreen, self).__init__(**kwargs)
self.cols = 2
self.add_widget(Label(text='User Name'))
self.username = TextInput(multiline=False)
self.add_widget(self.username)
self.add_widget(Label(text='password'))
self.password = TextInput(password=True, multiline=False)
self.add_widget(self.password)
class MyApp(App):
def build(self):
return LoginScreen()
if __name__ == '__main__':
MyApp().run()
在第二行,我们导入了网格布局:
from kivy.uix.gridlayout import GridLayout
在第九行,这个类被用作我们根部件的作为一个基本布局:
class LoginScreen(GridLayout):
在第12行,我们重载了方法init()以便于我们添加部件和行为:
def __init__(self, **kwargs):
super(LoginScreen, self).__init__(**kwargs)
我们不能忘记调用父类的super方法,以实现基类的基本功能;同时也要注意在调用super时不要忽略**kwargs参数,因为它们有时在内部使用。
第15行:
self.cols = 2
self.add_widget(Label(text='User Name'))
self.username = TextInput(multiline=False)
self.add_widget(self.username)
self.add_widget(Label(text='password'))
self.password = TextInput(password=True, multiline=False)
self.add_widget(self.password)
我们使用网格布局以两列来管理它的孩子们,并且每行添加了一个标签和一个文本输入框。
运行程序如图:
试着重新改变窗口大小,你会发现部件会自动调整尺寸。
上面的代码没有处理用户的输入,没有使用各种数据验证,包括部件的尺寸、位置等等。我们将会继续深入的研究他们。
六、平台细节
打开一个终端应用程序,并且设置Kivy的环境变量
在Windows平台上,仅仅需要双击kivy.bat,一个终端窗口会自动打开,并设置各种变量
在nix* 系统下,如果kivy没有全局安装,打开终端输入:
export python=$PYTHONPATH:/path/to/kivy_installation
4.2 环境配置
许多环境变量可以用来控制Kivy的初始化和行为。
例如限制文本渲染使用PIL实现:
$ KIVY_TEXT = pil python main.py
环境变量需要在导入Kiry之前设置:
import os
os.environ['KIVY_TEXT'] = 'pil'
import kivy
一、路径控制
V1.0.7版本加入
你可以控制诸如配置文件、模块、扩展和Kivy数据的默认路径
KIVY_DATA_DIR:Kivy数据默认路径
KIVY_EXTS_DIR:Kivy扩展默认路径
KIVY_MODULES_DIR:Kivy模块默认路径
KIVY_HOME:Kivy主默认路径,该路径被用来作为本地配置,该路径必须是可写入的,默认为:
桌面系统:/.kivy
安卓系统:.kivy
IOS 系统:/Documents/.kivy
V1.9.0版本加入
KIVY_SDL2_PATH:如果设置,在编译Kivy时将从该路径寻找SDL2库和头文件,代替系统方面的路径。为使用相同的库,在编译Kivy应用程序时,路径必须在PATH环境变量启动前添加。
在编译Kivy时必须使用,在执行时没有必要。
二、配置
KIVY_USER_DEFAULTCONFIG:如果该配置被设置,则Kivy不会读取用户的配置文件
KIVY_NO_CONFIG:如果设置,则没有配置文件及用户配置路径进行读写
KIVY_NO_FILELOG:如果设置,则不会记录日志到一个文件
KIVY_NO_CONSOLELOG:如果设置,则日志不会输出到控制台
V1.9.0版本加入
KIVY_NO_ARGS:如果设置,则在命令行传递的参数将不被解析,例如,你可以安全的使用你自己的不需要-分隔符的参数写一个脚本或应用程序:
import os
os.environ['KIVY_NO_ARGS'] = "l"
import kivy
kivy.core在你的系统平台中尝试选择最佳实现。为了测试或定制安装,你也许想限制选择一个特定的实现。
KIVY_WINDOW:创建一个窗口的实现,可选值为sdl2,pygame,x11, egl_rpi
KIVY_TEXT:渲染文本的实现,可选值为sdl2, pil, pygame, sdlttf
KIVY_VIDEO:渲染视频的实现,可选值为pygst, gstplayer, pyglet, ffpyplayer, null
KIVY_AUDIO:播放音频的实现,可选值为sdl2, gstplayer, pygst, ffpyplayer, pygame
KIVY_IMAGE:读取图像的实现,可选值为sdl2, pil, pygame, imageio, tex, dds, gif
KIVY_CAMERA:读取摄像头的实现,可选值为videocapture, avfoundation, pygst, opencv
KIVY_SPELLING:拼写的实现,可选值为enchant, osxappkit
KIVY_CLIPBOARD:剪切板管理的实现,可选值为sdl2, pygame, dummy, android
四、度量
V1.4.0版本加入
KIVY_DPI:如果设置,则单位使用Metrics.dpi
V1.5.0版本加入
KIVY_METRICS_DENSITY:如果设置,则单位使用Metrics.density
V1.5.0加入
KIVY_METRICS_FONTSCALE:如果设置,则单位使用Metrics.fontscale
五、图像
KIVY_GLES_LIMITS:是否GLES2限制被执行。如果设置为false,Kivy将不会真正的GLES2兼容。下面是一个当被设置为True时的潜在的不兼容列表
V1.8.1版本加入
KIVY_BCM_DISPMANX_ID:改变默认的用来显示Raspberry Pi,默认值为0,可选值如下:
0: DISPMANX_ID_MAIN_LCD
1: DISPMANX_ID_AUX_LCD
2: DISPMANX_ID_HDMI
3: DISPMANX_ID_SDTV
4: DISPMANX_ID_FORCE_LCD
5: DISPMANX_ID_FORCE_TV
6: DISPMANX_ID_FORCE_OTHER
4.3 配置Kivy
Kivy的配置文件命名为config.ini,遵循standard INI格式。
一、定位配置文件
配置文件的位置由环境变量KIVY_HOME来控制:
/config.ini
在桌面系统中,默认路径是:
/.kivy/config.ini
假如你的系统用户名是'tito',那么配置文件的路径如下:
在安卓系统上,默认路径是:
/.kivy/config.ini
如果你的应用程序名字为'org.kivy.launcher',文件路径是:
/data/data/org.kivy.launcher/files/.kivy/config.ini
在IOS系统上,默认路径为:
/Documents/.kivy/config.ini
二、理解配置符号
所有的配置符号在kivy.config文件中均有详细的解释。
4.4架构预览
我们将要花费一些时间以软件管理的角度来解释如何设计Kivy。这将是理解每一部分如何一起工作的关键。你也许浏览过源代码,也许有理一个粗糙的概念;但是看源代码也许是令人生畏的,因此这节内容将会详细解释一些基本的概念。你可以略过本节以待日后再看,但我们建议你至少大致浏览一下。
Kivy由几个模块组成,下面是Kivy架构图:
Architectural Overview
一、核心提供者和输入提供者
理解Kivy内部思想的关键是模块化和抽象。我们试图抽象一些基本的任务,例如打开一个窗口、展示图片和文本、播放音频、从摄像头获取图像、拼写检查等等。我们称它们为核心任务。这使得API既容易使用又容易扩展。更重要的是,它允许我们用:what we call,在你的应用程序运行时的不同场景指定不同的提供者。例如,在OS X,Linux和Windows系统上,针对不同的核心任务有不同的本地APIS。一方面这些APIS同操作系统进行交流,另一方面我们调用的Kivy的核心提供者扮演着中间交流层的角色。使用特殊核心提供者的便利之处是我们可以充分的使用操作系统暴露出来的功能,尽可能的使程序运行的更高效。它给用户提供了一个机会。另外,通过使用那些针对一个平台的库,我们可以高效地较低应用程序打包的尺寸。这也使得移植到别的平台更容易些。安卓端口从这方面就获利巨大。
我们利用同样的思路处理输入提供者。输入提供者,支持一些特殊的输入设备,例如苹果的trackpads,TUIO或者鼠标模拟器。如果你需要添加一个新的输入设备,你只需要简单的添加一个新的类来读取你的输入数据并且将其转换到Kivy的基本事件。
二、图形
Kivy的图像接口是我们对OpenGL的抽象。在更低的层面,Kivy使用OpenGL来分发硬件加速指令。对于一个初学者来说,写OpenGL代码也许是令人迷惑的。这也是我们提供图形API接口的原因,你只需要使用简单的封装好的方法(例如Canvas,Rectangle等等)进行绘画即可。
我们的所有部件本身就是使用这些图形接口,为了更高效的表现,它们在C语言执行。
另外一个便利之处是这些图形接口可以自动优化绘画指令。如果你不是一个OpenGL的专家,这将是很有用的帮助。在很多的场景下,它将使你的绘画代码更高效。
当然你也可以使用OpenGL命令。我们在所有的设备上使用的是OpenGL2.0(GLSE2),所以如果你想保持更好的跨平台的兼容性,我们建议你仅仅使用GLSE2提供的函数。
三、核心
核心提供者提供了一些通用的功能,例如:
四、UIX(部件和布局)
UIX模块包含常用的部件和布局,你可以重复使用它们来创建一个用户接口。
五、模块
如果你曾经使用过现代的网页浏览器,并定制过一些插件,那么你已经了解了关于我们模块类的基本概念。模块用来被注入功能到Kivy程序中,即使原作者没有包括它。
一个例子展示了一个总是显示FPS的模块和一些描绘FPS的图表。
你也可以写自己的模块。
六、输入事件(触摸)
Kivy抽象了不同的输入类型和资源,例如触摸、鼠标、TUIO等等。所有这些输入类型的共同之处是你可以使用一个屏幕上的2D坐标以及一些特殊的输入事件关联起来。(一些其它的输入设备,例如加速器你就不能简单的使用2D坐标来描述。这类输入设备被分别对待)
所有的这些输入设备类型被Touch()类的实例来表示。(注意这不仅仅代表手指触摸,也同样表示别的输入类型。我们只是因为类似而采取了Touch的名字。)一个Touch的实例或对象,包含着三个状态,当一个触摸进入到了这些状态,你的程序被通知,该事件发生了。这三个状态如下:
gthank:前面的两句话不知道什么意思!
七、部件和事件发送
部件这个术语通常用在GUI编程中,来描述和用户交互的那部分程序。在Kivy中,一个部件是一个对象由来接收输入事件。在屏幕上有一个可视的代表是没有必要的。所有的部件在一个部件树中管理:一个部件能有零或者多个子部件。但是在树的顶端只能有一个根部件,根部件没有父部件;其它的部件直接或间接的是根部件的子部件。
当一个新的输入数据可用时,Kivy发送一个事件。根部件首先收到该事件。根据触摸的状态,on_touch_down, on_touch_move, on_touch_up事件被发送到根部件,这将导致在根部件中相应的事件处理函数被调用。
在树中的每一个部件能选择吸收或传递事件。如果一个事件处理函数返回True,这意味着这个事件被吸收并正确的处理,对于该事件,没有进一步的处理会发生。否则,这个事件处理函数通过调用它的父类的各自事件处理函数的实现,传递部件到它自己的子部件上。最终事件传递到部件的基类,在它的事件处理函数里,什么到不做,仅仅传递touchs到它的子部件。
#这是一个move/up的模拟
def on_touch_down(self, touch):
for child in self.children[:]:
if(child.dispatch('on_touch_down', touch)):
return True
它可能比看起来更容易。下面的章节会展示一个例子,漂亮的应用程序能够快速的被创建。
通常你想限制一个部件监听触摸事件的屏幕区域。你可以使用部件的collide_point()方法来做到这点。你传递给它触摸的坐标,并且它将返回监听的区域。默认情况下,屏幕上矩形区域的检测使用部件的坐标和尺寸来描述,但是你也能在自己的类中进行重载。
4.5 事件和属性
事件是Kivy编程里面一个重要的部分。对于有GUI开发经验的人来说也许不是那么让人惊奇,但对于初学者是一个重要的概念。一旦你理解了事件如何工作、如何绑定,你将会在Kivy到处发现它们。它们使你想利用Kivy实现任何的行为变得很容易。
下面的插图显示了在Kivy框架中事件如何被处理。
events and properties
一、介绍事件发送
Kivy框架的最重要的基类之一就是EventDispatcher类。这个类允许你注册事件类型并发送它们到感兴趣的地方(通常是其它事件发送者)。部件、动画、时钟类都是事件发送的例子。
EventDispatcher对象依赖主循环生成和处理事件。
二、主循环
在上面插图中,主循环作为轮廓。这个循环运行在应用程序的全部生命周期中,直到应用程序退出时才终止。
在循环里面,每一次迭代,当发生用户输入、传感器或者一些其他资源、画面被渲染显示时,总会有事件生成。
你的应用程序可以指定回调函数,它们在主循环中被调用。如果一个回调函数费时太长或者根本不会退出,则主循环会中断同时你的应用程序无法正常运行。
在Kivy应用程序中,你必须避免使用长循环、死循环或睡眠(sleeping),如下代码是需要避免的:
while True:
animate_something()
time.sleep(.10)
当你运行上面的代码,则你的程序永远无法退出该循环,要预防Kivy做类似的事情。结果将会看到一个无法交互的黑色的窗口。正确的方式的,你需要定制(schedule)你的animate_somthing()函数重复调用。
(一)重复事件
你可以使用schedule_interval()每隔X时间调用一个函数或方法,下面是一个例子,每隔1/30秒调用一次my_callback函数:
def my_callback(dt):
print 'my callback is called', dt
Clock.schedule_interval(my_callback, 1/30.)
你有两种方法来取消前面定制的事件,第一种是:
Clock.unschedule(my_callback)
或者你在你的回调函数中返回False,那么你的事件将会自动取消:
count = 0
def my_callback(dt):
global count
count += 1
if count == 10:
print 'Last call of my callback, bye bye!'
return False
print 'My callback is called'
Clock.schedule_interval(my_callback, 1/30.)
(二)单次事件
使用schedule_once(),你可以定制执行一次你的回调函数,比如在下一帧,或X秒后:
def my_callback(dt):
print 'My callback is called!'
Clock.schedule_once(my_callback, 1)
上面的代码中,my_callback()函数将会在1秒后执行。1秒参数是在执行该程序前等待的时间,以秒为单位。但是你可以使用特殊的值作为时间参数得到一切其它结果:
其中 x = -1最为常用。
重复执行一个函数的第二种方法是:一个回调函数使用schedule_once递归调用了自己,在外部schedule_once函数中又调用了该回调函数:
def my_callback(dt):
print 'My callback is called !'
Clock.schedule_once(my_callback, 1)
Clock.schedule_once(my_callback, 1)
当主循环尝试保持定制请求时,当恰好一个定制的回调函数被调用时,有一些不确定的情况会发生。有时另外一些回调函数或一些任务花费了超出预期的时间,则定时会被延迟。
在第二种解决方案中,在上一次迭代执行结束后,下一次迭代每秒至少会被调用一次。而使用schedule_interval(),回调函数则每秒都会被调用。
(三)事件跟踪
如果你想为下一帧定制一个仅执行一次的函数,类似一个出发器,你可能这样做:
Clock.unschedule(my_callback)
Clock.schedule_once(my_callback, 0)
这种方式的代价是昂贵的,因为你总是调用unschedule()方法,无论你是否曾经定制过它。另外,unschedule()方法需要迭代时钟的弱引用列表,目的是找到你的回调函数并移除它。替代的方法是使用出发器:
trigger = Clock.create_trigger(my_callback)
#随后
trigger()
每次你调用trigger,它会为你的回调函数定制一个信号调用,如果已经被定制,则不会重新定制。
三、部件事件
每个部件都有两个默认的事件类型:
四、自定义事件
为了创建一个自定义事件,你需要在一个类中注册事件名,并创建一个同名的方法:
class MyEventDispatcher(EventDispatcher):
def __init__(self, **kwargs):
self.register_event_type('on_test')
super(MyEventDispatcher, self).__init__(**kwargs)
def do_something(self, value):
#当do_something被调用时,on_test事件将会连同value被发送
self.dispatch('on_test', value)
def on_test(self, *args):
print "I am dispatched", args
五、附加回调
为了使用事件,你必须绑定回调函数。当事件被发送时,你的回调函数将会连同参数被调用。
一个回调函数可以是任何python函数,但是你必须确保它接受事件发出的参数。因此,使用*args的参数会更安全,这样将会在args列表中接收到所有的参数。例如:
def my_callback(value, *args):
print "Hello, I got an event!", args
e = MyEventDispatcher()
e.bind(on_test = my_callback)
e.do_something('test')
有关附加回调函数更多的示例可以参阅kivy.event.EventDispatcher.bind()文档
六、属性介绍
属性是一个很好的方法用来定义事件并绑定它们。本质上来说,当你的对象的特征值发生变化时,它们创造事件,所有的引用特征值的属性都会自动更新。
有不同类型的属性来描述你想要处理的数据类型。
七、声明属性
为了声明属性,你必须在类的级别进行。当你的对象被创建时,该类将会进行实际特征值的初始化。特写属性不是特征值,它们是基于你的特征值创建事件的机制。
class MyWidget(Widget):
text = StringProperty('')
当重载init时,总是接受**kwargs参数并使用super()调用父类的init方法:
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
八、发送属性事件
Kivy的属性,默认提供一个on_
注意,如果新的属性值等于当前值,该事件不会被调用。
例如:
class CustomBtn(Widget):
pressed = ListProperty([0, 0])
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.pressed = touch.pos
return True
return super(CustomBtn, self).on_touch_down(touch)
def on_pressed(self, instance, pos):
print('pressed at{pos}'.format(pos=pos))
在第3行:
pressed = ListProperty([0,0])
我们定义了pressed属性,类型为ListProperty,默认值为[0, 0],当属性值发生改变时,on_pressed事件被调用。
在第5行:
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.pressed = touch.pos
return True
return super(CustomBtn, self).on_touch_down(touch)
我们重载了on_touch_down()方法,我们为我们的部件做了碰撞检测。
如果触摸发生在我们的部件内部,我们改变touch.pos按下的值并返回True,表明我们处理了这次触摸并不想它继续传递。
最后,如果触摸发生在我们的部件外部,我们使用super()调用原始事件并返回结果。它允许触摸事件继续传递。
最后在11行:
def on_pressed(self, instance, pos):
print ('pressed at {pos}'.format(pos=pos))
我们定义了on_pressed函数,当属性值改变时,该函数被调用。
注意当属性值被定义时,on_
事件在类内部被调用。为了在类的外部监控或观察任何属性值的变动,你可以以下面的方式绑定属性值。
your_widget_instance.bind(property_name=function_name)
例如,考虑以下代码:
class RootWidget(BoxLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.add_widget(Button(text='btn 1'))
cb = CustomBtn()
cb.bind(pressed=self.btn_pressed)
self.add_widget(cb)
self.add_widget(Button(text='btn 2'))
def btn_pressed(self, instance, pos):
print ('pos: printed from root widget: {pos}'.format(pos=.pos))
如果你运行上面的代码,你会注意到在控制台有两个打印信息。一个来自on_pressed事件,该事件在CustomBtn类内部被调用,另一个来自我们绑定属性改变的btn_pressed函数
你也需要注意到传递给on_
def btn_pressed(self, instance, pos):
第一个参数是self,是该函数被定义的类的实例。你可以如下面的方式使用一个内联函数:
cb = CustomBtn()
def _local_func(instance, pos):
print ('pos: printed from root widget: {pos}'.format(pos=.pos))
cb.bind(pressed=_local_func)
self.add_widget(cb)
第一个参数是属性被定义的类的实例。
第二个参数是属性的新的值。
下面是一个完整的丽日,你能拷贝下来进行实验。
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ListProperty
class RootWidget(BoxLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.add_widget(Button(text='btn 1'))
cb = CustomBtn()
cb.bind(pressed=self.btn_pressed)
self.add_widget(cb)
self.add_widget(Button(text='btn 2'))
def btn_pressed(self, instance, pos):
print ('pos: printed from root widget: {pos}'.format(pos=pos))
class CustomBtn(Widget):
pressed = ListProperty([0, 0])
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.pressed = touch.pos
# we consumed the touch. return False here to propagate
# the touch further to the children.
return True
return super(CustomBtn, self).on_touch_down(touch)
def on_pressed(self, instance, pos):
print ('pressed at {pos}'.format(pos=pos))
class TestApp(App):
def build(self):
return RootWidget()
if __name__ == '__main__':
TestApp().run()
运行结果如下:
property_events_binding
我们的定制按钮没有可视的表述,因此显示一个黑块。你能触摸或点击它以在控制台查看输出。
九、混合属性
当定义一个AliasProperty时,通常的做法是定义getter()和setter函数。当getter()和setter()函数使用bind被调用时,它落在了你的肩上。考虑以下代码:
cursor_pos = AliasProperty(_get_cursor_pos, None, bind=(
'cursor', 'padding', 'pos', 'size', 'focus',
'scroll_x', 'scroll_y'))
'''Current position of the cursor, in (x, y).
:attr:`cursor_pos` is a :class:`~kivy.properties.AliasProperty`, read-only.
'''
这里cursor_pos是一个AliasProperty,它使用_get_cursor_pos作为getter(),并且setter()为None,表明这是一个只读属性。
在最后,当任何使用bind=argument的属性改变时,on_cursor_pos事件被发送。
4.6 管理输入
一、输入架构
Kivy能处理很多类型的输入:鼠标、触摸屏、加速器、陀螺仪等等。它在下列的平台中能处理多点触摸协议:Tuio,WM_Touch, MacMultitouchSupport, MT Protocol A/B and Android.
输入全局架构可以被描述为:
输入提供者(Input Providers) -> 运动事件(Motion event) -> 投递处理(Post processing) -> 发送到窗口(Dispatch to Window)
所有管理输入事件的类是运动事件(MotionEvent)。它派生成两种类型的事件类:
触摸事件:一个运动事件能至少包含X和Y坐标。所有的触摸事件通过部件树进行发送。
非触摸事件:剩下的,例如加速器是一个连续事件,没有坐标。它没有开始和停止状态。这些事件不通过部件树发送。
一个运动事件被一个输入提供者(Input Provider)生成。一个输入提供者负责从操作系统、网络或其他应用程序读取输入事件。下是几个输入提供者:
TuioMotionEventProvider:创建一个UDP服务并监听TUIO/OSC消息。
WM_MotionEventProvider:使用窗口API读取多点触摸信息并发送到Kivy。
ProbeSysfsHardwareProbe:在Linux系统下,迭代所有连接到计算机上的硬件,并附着一个多点输入提供者为每一个被发现的多点触摸设备。
更多:...
当你写一个应用程序时,你不必创建一个输入提供者,Kivy会尝试自动检测可用的硬件。但是,如果你想支持定制的硬件,你就需要配置Kivy并使它工作。
在新创建的运动事件被传递到用户之前,Kivy应用投递处理(post-processing)到输入。每一个运动事件被分析并纠正错误输入,做出有意义的解释:
当处理事件后,运动事件被发送到窗口。正如前面解释的那样,不是所有的事件都发送到事件树:窗口过滤它们。对于一个事件:
如果它仅仅是一个运动事件,它会被发送到on_motion()
如果它是一个触摸事件,则触摸点的(x, y)坐标(在0-1范围内)会被重新转换为屏幕尺寸(宽/高),并发送到:
二、运动事件配置
依赖你的硬件和使用的输入提供者,也许有更多的信息可以被使用。例如,触摸输入有(x, y)坐标,但是也许还有压力信息,触摸尺寸,加速度等等。
一个运动事件配置是一个标识事件里面有什么特征可用字符串,让我们想象一下你在on_touch_move方法中:
def on_touch_move(self, touch):
print(touch.profile)
return super(..., self).on_touch_move(touch)
打印信息为:
['pos', 'angle']
注意:很多人将配置的名字和属性对应的名字混淆在了一块。在可用的配置文件里的'angle'不意味着触摸事件对象有一个angle对象。
对于'pos'配置,属性pox, x, y是可用的。对于'angle'配置,属性 a 是可用的。正如我们所说,对于触摸事件,'pos'是一个托管的配置,但'angle'不是。你能通过检测'angle'配置是否存在来扩展你的交互:
def on_touch_move(self, touch):
print('the touch is at position', touch.pos)
if 'angle' in touch.profile:
print('the touch angle is', touch.a)
你能在motionevent文档中找到一个关于可用的配置的列表。
三、触摸事件
一个触摸事件是一个特殊的运动事件,它的属性is_touch被设置为True。对于所有的触摸事件,你会自动拥有X和Y坐标,对应着窗口的宽和高。换句话说,所有的触摸事件有'pox'配置。
(一)触摸事件基础
默认情况下,触摸事件被发送到当前所有的可显示的部件。这意味着无论事件发生在部件的内部与否,它都能收到事件。
如果你有关于其他GUI的开发经验,这可能是反直觉的。一般典型的做法是划分屏幕为几何区域,并且只有坐标在部件区域内部时才发送触摸或鼠标事件到部件。
当使用触摸事件工作时,这个要求变得非常的有限制性。强击,缩放和长按可能来自想了解部件及其如何反应的外部。
为了提供最大的灵活性,Kivy发送事件到所有的部件,并让它们决定是否响应它们。如果你仅想在部件内部响应触摸事件,你可以简单的进行检测:
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
#触摸发生在部件区域的内部,做自己的相应
pass
(二)坐标
一旦你使用了一个带有矩阵转换的部件,你必须注意矩阵转换。一些部件,例如分散(Scatter)有它们自己的矩阵转换,这意味着触摸必须能正确的传递触摸坐标到Scatter的子部件。
你必须根据上下文使用上面的一个来转换相应的坐标。看分散(scatter)的实现:
def on_touch_down(self, touch):
#将当前的坐标压入,为了后面能存储它
touch.push()
#转换触摸坐标到局部坐标
touch.apply_transform_2d(self.to_local)
#像平时一样,发送触摸到子部件
#在touch里面的坐标是局部坐标
ret = super(..., self).on_touch_down(touch)
#无论结果如何,不要忘记调用之后弹出你的转换,
#这样,坐标会变成父坐标
touch.pop()
#返回结果(依赖于你的所需)
return ret
(三)触摸形状
如果触摸有一个形状,它可以在'shape'属性中体现。现在,仅仅ShapeRect被暴露:
from kivy.input.shape import ShapeRect
def on_touch_move(self,touch):
if isinstance(touch.shape, ShapeRect):
print('My touch have a rectangle shape of size',
(touch.shape.width, touch.shape.height))
(四)双击
双击是指在一段时间和范围内轻点两次。它由doubletap post-processing模块来计算。你能测试当前的触摸是双击或不是:
def on_touch_down(self, touch):
if touch.is_double_tap:
print('touch is a double tap!')
print('- interval is', touch.double_tab_time)
print('- distance between previous is', touch.double_tap_distance)
(五)三击
三击是只在一段时间和一定范围内轻击三次。它由tripletap post-processing模块来计算。你能测试当前的触摸是否为三击:
def on_touch_down(self, touch):
if touch.is_triple_tap:
print('Touch is a triple tap !')
print(' - interval is', touch.triple_tap_time)
print(' - distance between previous is', touch.triple_tap_distance)
(六)捕获触摸事件
对于父部件使用on_touch_down发送一个触摸事件到子部件,它是可能的,但通过on_touch_move或on_touch_up就不可以。这可能发生在特定的场景下,例如当一个触摸事件发生在父部件边框外部时,父部件决定不通知它的子部件。
当你捕获了一个事件,你会总是收到移动(move)和弹起(up)事件,但是对于捕获有一些限制:
下面是一个例子来演示如何使用捕获:
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
#如果触摸检测来自我们自己的部件,让我们捕获它。
touch.grab(self)
# 并响应这次触摸.
return True
def on_touch_up(self, touch):
#这里,你不用检测触摸碰撞或类似操作,
#你仅需要检测是否它是一个捕获的触摸事件
if touch.grab_current is self:
#OK,当前触摸事件被派发给我们
#做一些感兴趣的操作
print('Hello world!')
#不要忘记释放掉,否则可能会有副作用
touch.ungrab(self)
#最后响应这次触摸
return True
(七)触摸事件管理
为了了解触摸事件如何在部件间被控制和传播,请参阅部件触摸事件冒泡机制(Widget touch event bubbling)
4.7 部件
一、部件介绍
在Kivy中,部件是创建GUI接口的基本。它提供了一个画板,能用来在屏幕上绘画。它能接收事件并响应它们。有关Widget类的深度的解释,请参看模块文档。
二、操纵部件树
在Kivy中部件使用树来管理。你的应用程序有一个根部件,它通常有拥有自己子部件的子部件。子部件被children(一个Kivy的列表属性(ListProperty)))特征值代表.
部件树能使用以下方法进行操作:
例如如果你想在一个盒子布局(BoxLayout)中添加一个按钮,你可以:
layout = BoxLayout(padding = 10)
button = Button(text = 'My First Button')
layout.add_widget(button)
按钮被添加到布局:按钮的父属性被设置为layout,layout将会把button添加到它的子部件列表中。
如果要从layout中移除button,可以:
layout.remove_widget(button)
移除后,按钮的父属性被设置为None,layout将从子部件列表中移除button.如果你想将layout中的所有子部件全部移除,可以:
layout.clear_widgets()
注意:永远不要手动配置子部件列表,除非你真的明白你在做什么。部件树和一个图形树相关联。例如,如果你添加一个部件到子部件列表,但没有添加它的画布到图形树,那么部件将称为一个子部件,但是屏幕上没有任何东西显示。更重要的是,你可能在以后调用add_widget, remove_widget, clear_widgets中会出现问题。
三、遍历部件树
部件类实例的children中包含所有的子部件,你能容易的遍历部件树:
root = BoxLayout()
#...添加子部件到root...
for child in root.children
print(child)
但是,这必须要小心使用。如果你试图使用前面章节提供的方法来修改children,你必须用children列表的拷贝:
for child in root.children[:]:
#配置部件树,例如移除所有width<100的部件
if child.width < 100:
root.remove_widget(child)
默认情况下,部件不影响子部件的尺寸和位置。pos特征值是屏幕坐标的绝对位置。(除非你使用了相对布局(relativelayout)和尺寸)。
四、部件Z索引
渲染部件的顺序是基于在部件树的位置。最后的部件的画布最后被渲染。add_widget有一个index参数,用来设置Z索引:
root.add_widget(widget, index)
五、使用布局管理
布局是一种特殊的部件,它控制它的子部件的尺寸和位置。有不同类型的布局,按照不同的规则自动组织它们的子部件。布局使用size_hint和pos_hint属性来确定它们子部件的尺寸和位置。
(一)盒子布局(BoxLayout)
盒子布局以相邻的方式(或平行或垂直)来安排他们的子部件,填满所有的空间。子部件的size_hint属性能被用来改变被允许的比例或设置固定的尺寸。
box layout
(二)网格布局(GridLayout)
网格布局排列部件在网格内。你必须至少制定网格的一个维度,这样Kivy才能计算元素的尺寸及如何排列它们。
grid layout
(三)堆叠布局(StackLayout)
堆叠布局一个接一个的排列部件,但是在一个维度上使用了一组尺寸,不要试图使它们填满整个空间。它常用来显示有同样尺寸的子部件。
stack layout
(四)锚点布局(AnchorLayout)
一种简单的布局,它仅关注子部件的位置。它允许在一个相对于布局的边的位置放置子部件。size_hint被忽略。
anchor layout
(五)浮动布局(FloatLayout)
浮动布局允许放置任意位置和尺寸的子部件。默认size_hint(1, 1)将会使每一个子部件有同样的尺寸作为整个布局,所以,如果你有多个子部件,你可能想改变这个值。你能设置set_hint到(None, None)来使用绝对的尺寸。同样也可以使用pos_hint设置位置。
float layout
(六)相对布局(RelativeLayout)
有点像FloatLayout,除了子部件的位置是相对于布局位置,而不是屏幕位置。
查看每个布局的文档,可以用更深入的了解。
[size_hint]和[pos_hint]:
size_hint是一个关于size_hint_x和size_hint_y的ReferenceListProperty.它接受从0到1,或None的值,默认为(1,1)。这表示如果部件在一个布局中,布局会相对于布局尺寸,在两个方向上,尽可能为它分配足够的空间。
设置size_hint到(0.5, 0.8),表示在布局中将会使部件有50%的宽和80%的高可用。
考虑以下代码:
BoxLayout:
Button:
text:'Button 1'
#默认size_hint是1, 1,我们不需要明确地指定它
#但是它在这里被设置会更清晰。
size_hint:1, 1
加载kivy目录:
cd $KIVYDIR/examples/demo/kivycatalog
python main.py
使用你的Kivy的安装路径代替$KIVYDIR。点击盒子布局左侧的按钮。粘贴上面的代码到右边,你将会看到,按钮占有布局100%的尺寸。
改变size_hint_x/size_hint_y到0.5将会时部件有布局50%的宽/高。
你能看到,虽然我们指定了size_hint_x和size_hint_y到0.5,但是仅仅size_hint_x生效了。这是因为盒子布局在orientation是垂直时控制着size_hint_y,在orientation是水平是控制着size_hint_x。被控制维度的尺寸的计算依赖子部件的总的数目。在这个例子中,仅有一个子部件,因此,它将持有100%的父部件的高度。
让我们添加另外一个按钮到布局,看看会发生什么。
盒子布局会为它的子部件平分可用的空间。让我们使用size_hint设置一个按钮的尺寸。第一个按钮指定0.5的size_hint_x,第二个按钮的size_hint_x,默认为1,则总的宽度将变成0.5+1=1.5,第一个按钮的宽度就会变为0.5/1.5=0.333...,约为1/3的宽度。剩下的盒子布局的宽度分配给另一个按钮,约为2/3。如果有多个剩余的子部件,他们会进行平分。
如果你想控制部件的绝对大小,你可以设置size_hint_x/size_hint_y,或者二者均为None,这样部件的width和height特征值就会使用。
pos_hint是一个字典,默认为空。正如size_hint, 布局对使用pos_hint分别对待,通常你可以添加任何pos特征值(x, y, left, top, center_x, center_y),让我们实验下面的代码,以了解pos_hint:
FloatLayout:
Button:
text:'We Will'
pos:100, 100
size_hint:.2, .4
Button:
text:'Wee Wiill'
pos:200, 200
size_hint:.4, .2
Button:
text:'Rock You'
pos_hint:{'x':.3, 'y':.6}
size_hint:.5, .2
效果如图:和size_hint一样,你应当实验pos_hint来理解它对部件位置的影响。
六、为布局添加一个背景
经常被询问的关于布局的一个问题是:
如何为一个布局添加一个背景图片/颜色/视频/...
布局本质上没有可视的元素:默认情况下,他们没有画布指令。但是你可以添加画布指令到一个布局实例,正如添加一个背景颜色:
from kivy.graphics import Clolr, Rectangle
with layout_instance.canvas.before:
Clolr(0, 1, 0, 1)#绿色;颜色范围从0~1代替0~255
self.rect = Rectangle(size = layout_instance.size, pos = layout_instance.pos)
不幸的是,这仅仅会在布局的初始位置和尺寸画一个矩形。当布局的尺寸和位置改变时,为确保矩形被画在布局内部,我们需要监听矩形尺寸和位置的任何变动和更新:
with layout_instance.canvas.before:
Color(0, 1, 0, 1)
self.rect = Rectangle(size = layout_instance.size, pos = layout_instance.pos)
def update_rect(instance, value):
instance.rect.pos = instance.pos
instance.rect.size = instance.size
#监听尺寸和位置的更新
layout_instance.bind(pos=update_rect, size=update_rect)
在kv中:
FloatLayout:
canvas.before:
Color:
rgba:0, 1, 0, 1
Rectangle:
#这里的self代表部件,例如BoxLayout
pos:self.pos
size: self.size
kv声明设置了一个隐性的绑定:最后两个kv语句确保pos和size值随着FloatLayout的pos的改变而自动更新。
现在,我们添加一些功能:
from kivy.graphics import Color,Rectangle
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
class RootWidget(FloatLayout):
"""docstring for RootWidget"""
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
#add a button to layout
self.add_widget(
Button(
text = "modifiy preCondition",
size_hint = (.5, .5),
pos_hint = {"center_x": .1,"center_y": .1}
)
)
class MainApp(App):
def build(self):
self.root = root = RootWidget()
root.bind(size=self._update_rect, pos=self._update_rect)
with root.canvas.before:
Color(0,0.5,0,0.5)
self.rect = Rectangle(size = root.size,pos=root.pos)
return root
def _update_rect(self, instance, value):
self.rect.pos = instance.pos
self.rect.size = instance.size
if __name__ == '__main__':
MainApp().run()
from kivy.app import App
from kivy.lang import Builder
root = Builder.load_string(
'''
FloatLayout:
canvas.before:
Color:
rgba:0, 1, 0, 1
Rectangle:
pos: self.pos
size: self.size
Button:
text: 'Hello World'
size_hint: .5, .5
pos_hint:{'center_x':.5, 'center_y':.5}
'''
)
class MainApp(App):
def build(self):
return root
if __name__ == '__main__':
MainApp().run()
运行效果如下:
添加颜色到背景用一个custom layouts rule/class
如果我们需要使用多重布局的话,这种添加背景到布局实例的方法会变得笨重。为了解决这个问题,我们可以创建布局类的子类,并创建你自己的添加了背景的布局:
from kivy.app import App
from kivy.graphics import Color, Rectangle
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import AsyncImage
class RootWidget(BoxLayout):
pass
class CustomLayout(FloatLayout):
def __init__(self, **kwargs):
# make sure we aren't overriding any important functionality
super(CustomLayout, self).__init__(**kwargs)
with self.canvas.before:
Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255
self.rect = Rectangle(size=self.size, pos=self.pos)
self.bind(size=self._update_rect, pos=self._update_rect)
def _update_rect(self, instance, value):
self.rect.pos = instance.pos
self.rect.size = instance.size
class MainApp(App):
def build(self):
root = RootWidget()
c = CustomLayout()
root.add_widget(c)
c.add_widget(
AsyncImage(
source="http://www.everythingzoomer.com/wp-content/uploads/2013/01/Monday-joke-289x277.jpg",
size_hint= (1, .5),
pos_hint={'center_x':.5, 'center_y':.5}))
root.add_widget(AsyncImage(source='http://www.stuffistumbledupon.com/wp-content/uploads/2012/05/Have-you-seen-this-dog-because-its-awesome-meme-puppy-doggy.jpg'))
c = CustomLayout()
c.add_widget(
AsyncImage(
source="http://www.stuffistumbledupon.com/wp-content/uploads/2012/04/Get-a-Girlfriend-Meme-empty-wallet.jpg",
size_hint= (1, .5),
pos_hint={'center_x':.5, 'center_y':.5}))
root.add_widget(c)
return root
if __name__ == '__main__':
MainApp().run()
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
Builder.load_string('''
canvas.before:
Color:
rgba: 0, 1, 0, 1
Rectangle:
pos: self.pos
size: self.size
CustomLayout:
AsyncImage:
source: 'http://www.everythingzoomer.com/wp-content/uploads/2013/01/Monday-joke-289x277.jpg'
size_hint: 1, .5
pos_hint: {'center_x':.5, 'center_y': .5}
AsyncImage:
source: 'http://www.stuffistumbledupon.com/wp-content/uploads/2012/05/Have-you-seen-this-dog-because-its-awesome-meme-puppy-doggy.jpg'
CustomLayout
AsyncImage:
source: 'http://www.stuffistumbledupon.com/wp-content/uploads/2012/04/Get-a-Girlfriend-Meme-empty-wallet.jpg'
size_hint: 1, .5
pos_hint: {'center_x':.5, 'center_y': .5}
''')
class RootWidget(BoxLayout):
pass
class CustomLayout(FloatLayout):
pass
class MainApp(App):
def build(self):
return RootWidget()
if __name__ == '__main__':
MainApp().run()
结果如下:
在子类中定义背景,确保它被用在每一个定制布局的实例中。
现在,为了添加一个图片或颜色到内置的Kivy布局背景中,总体来说,我们需要为布局问题重载kv规则。考虑网格布局:
canvas.before:
Color:
rgba: 0, 1, 0, 1
BorderImage:
source: '../examples/widgets/sequenced_images/data/images/button_white.png'
pos: self.pos
size: self.size
下面,我们把这段代码放入Kivy应用程序:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.lang import Builder
Builder.load_string('''
canvas.before:
BorderImage:
# BorderImage behaves like the CSS BorderImage
border: 10, 10, 10, 10
source: '../examples/widgets/sequenced_images/data/images/button_white.png'
pos: self.pos
size: self.size
GridLayout:
size_hint: .9, .9
pos_hint: {'center_x': .5, 'center_y': .5}
rows:1
Label:
text: "I don't suffer from insanity, I enjoy every minute of it"
text_size: self.width-20, self.height-20
valign: 'top'
Label:
text: "When I was born I was so surprised; I didn't speak for a year and a half."
text_size: self.width-20, self.height-20
valign: 'middle'
halign: 'center'
Label:
text: "A consultant is someone who takes a subject you understand and makes it sound confusing"
text_size: self.width-20, self.height-20
valign: 'bottom'
halign: 'justify'
''')
class RootWidget(FloatLayout):
pass
class MainApp(App):
def build(self):
return RootWidget()
if __name__ == '__main__':
MainApp().run()
结果如下:
由于我们重载了网格布局的规则,任何应用该类的地方都会显示图片。
一个动画背景如何显示呢?
你可以设置绘画指令,像Rectangle/BorderImage/Ellipse/...一样来使用一个特别的材质:
Rectangle:
texture: reference to a texture
我们来显示一个动画背景:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import Image
from kivy.properties import ObjectProperty
from kivy.lang import Builder
Builder.load_string('''
canvas.before:
BorderImage:
# BorderImage behaves like the CSS BorderImage
border: 10, 10, 10, 10
texture: self.background_image.texture
pos: self.pos
size: self.size
CustomLayout:
size_hint: .9, .9
pos_hint: {'center_x': .5, 'center_y': .5}
rows:1
Label:
text: "I don't suffer from insanity, I enjoy every minute of it"
text_size: self.width-20, self.height-20
valign: 'top'
Label:
text: "When I was born I was so surprised; I didn't speak for a year and a half."
text_size: self.width-20, self.height-20
valign: 'middle'
halign: 'center'
Label:
text: "A consultant is someone who takes a subject you understand and makes it sound confusing"
text_size: self.width-20, self.height-20
valign: 'bottom'
halign: 'justify'
''')
class CustomLayout(GridLayout):
background_image = ObjectProperty(
Image(
source='../examples/widgets/sequenced_images/data/images/button_white_animated.zip',
anim_delay=.1))
class RootWidget(FloatLayout):
pass
class MainApp(App):
def build(self):
return RootWidget()
if __name__ == '__main__':
MainApp().run()
为了理解到底发生了什么,先看13行:
texture:self.background_image.texture
这表明BorderImage的材质属性在background_image更新时都将被更新。我们定义了background_image属性在40行:
background_image = ObjectProperty(...)
这段代码设置background_miage是一个ObjectProperty,在那儿我们添加了一个Image部件。一个Image部件有一个textuer属性,self.background_image.texture设置了一个对于texture的引用。Image部件支持动画:图片的材质在动画改变时会被更新,并且BorderImage指令的材质跟着更新了。
您还可以自定义数据的纹理贴图。更多信息请参阅Texture文档。
七、嵌套布局
当然,关于如何扩展这部分内容应该是很有趣的!
gthank-没有实际内容
八、尺寸和坐标度量
Kivy的默认长度单位是像素(pixel),所有尺寸和位置都使用它。你也可以使用别的单位以获得更好的跨平台的效果。
可用的单位有pt, mm, cm, inch, dp, sp.你可以在metrics文档中了解它们的用法。
你可以用screen应用模拟不同的设备来测试你的应用程序。
九、用屏幕管理进行屏幕分离
如果你的应用程序由不同的屏幕组成,你可能想有一个容易的方式来从一个屏幕导航到另一个屏幕。幸运的是,有一个ScreenManager类,允许你分别定义屏幕,并从一个屏幕到另外一个屏幕设置基本转换(TransitionBase)。
4.8 图形
一、画布介绍
部件绘画的表现使用画布来完成。你可以将它看作一个无限制的画板,或者一个绘画指令的集合。
在你的画布中,你可以应用的指令有很多,但是最主要的两个是:
上下文指令不画任何东西,但是它们改变顶点指令的结果。
画布包含指令的两种子集合:canvas.before, canvas.after。这些指令在canvas前或后执行。这些指令直到用户访问时才创建。
为了添加一个画布指令到部件,你需要使用画布上下文:
class MyWidget(Widget):
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
with self.canvas:
#为主画布添加你的指令
with self.canvas.before:
#渲染前执行
with self.canvas.after:
#渲染后执行
二、上下文指令
上下文指令操纵Opengl上下文。你可以旋转、转换、拉伸你的画布。你也可以附着一个材质或改变绘画的颜色。这是最常用的,但是其它也是很有用的,比如:
with self.canvas.before:
Color(1, 0, .4, mode='rgb')
三、绘画指令
绘画指令简单的包括画一条直线或一个多边形,复杂的包括贝塞尔曲线:
with self.canvas:
#画一条直线,使用默认的颜色
Line(points=(x1, y1, x2, y2, x3, y3))
#画一个半透明的红色的正方形
Color(1, 0, 0, .5, mode='rgba')
Rectangle(pos=self.pos, size=self.size)
四、配置指令
有时,你想更新或移除你的指令,根据你的需要可以使用不同的方式:
你可以保持一个你的指令的引用并更新它们:
class MyWidget(Widget):
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
with self.canvas:
self.rect = Rectangle(pos=self.pos, size=self.size)
self.bind(pos=self.update_rect)
self.bind(size=self.update_rect)
def update_rect(self, *args):
self.rect.pos = self.pos
self.rect.size = self.size
或者你可以清理你的画布并启动刷新:
class MyWidget(Widget):
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
self.draw_my_stuff()
self.bind(pos=self.draw_my_stuff)
self.bind(size=self.draw_my_stuff)
def draw_my_stuff(self):
self.canvas.clear()
with self.canvas:
self.rect = Rectangle(pos=self.pos, size=self.size)
注意更新的指令被认为是最佳实践,因为它需要更少的开销和避免创建新的指令。
4.9 Kivy语言
一、语言背后的思想
当你的应用程序变得更复杂时,构建部件树和明确的声明绑定将变得冗长和难以维护。KV语言试图克服这些缺点。
KV语言(有时被叫kvlang,或kivy语言),允许你以声明的方式来创建你的部件树,并以一种自然的方式绑定部件属性或回调函数。针对UI,它支持快速原型和敏捷改动。它也使得逻辑和用户接口能更好的分离。
二、如何加载KV
有两种方式来加载KV代码:
通过名字约定
Kivy查找你的应用程序类的小写的同名KV文件,如果它以'App'结尾则去掉它,例如:
MyApp -> my.kv
如果这个文件定义了一个根部件,它将会附着到应用程序的根特征值,并用它作为应用程序部件树的根。
Builder
你可以告诉Kivy直接加载一个字符串或一个文件。如果这个字符串或文件定义了根部件,它将被返回。
Builder.load_file('path/to/file.kv')
或者
Builder.load_string('kv_string')
三、管理上下文
一个KV源构成的规则,用来描述部件的内容。你可以有一个根规则和任何数量的类或模板规则。
根规则通过声明你的根部件类来声明,不需要任何缩进,后面跟着冒号(:),并且被设置为应用程序实例的根特征值。
Widget:
一个类规则,有一对尖括号(<>)包括部件类名组成,后面跟冒号(:),定义类的实例如何被生动地表达:
:
和Python一样,规则使用缩进进行界定,和良好的Python习惯一样,缩进的每一级别最好是4个空格。
有三个关键字来指定KV语言:
四、特殊的语法
有两个特殊语法来为整个KV上下文定义值:
#:import name x.y.z
#:import isdir os.path.isdir
#:import np numpy
上面的代码等价于:
from x.y import z as name
from os.path import isdir
import numpy as np
#:set name value
等价于:
name = value
五、实例化子部件
为了声明部件的子部件,仅在规则里面声明这些子部件即可:
MyRootWidget:
BoxLayout:
Button:
Button:
上面的例子定义了一个MyRootWidget的实例作为我们的根部件,它有一个子部件是BoxLayout的实例。BoxLayout进一步有两个Button类的子部件。在Python代码中应该是这样:
root = MyRootWidget()
box = BoxLayout()
box.add_widget(Button())
box.add_widget(Button())
root.add_widget(box)
你会发现在KV中,仅用很少的代码,易写并易读。
当然,在Python中,你可以传递关键字参数到你的部件中。例如,设置一个GridLayout的列的数目,我们可以这样写:
grid = GridLayout(cols = 3)
在KV中,你可以直接在规则中设置子部件的属性:
GridLayout:
cols:3
这个值被评估为一个Python表达式,并且表达式中所有的属性值都将被监听。例如在Python中:
grid = GridLayout(cols = len(self.data))
self.bind(data = grid.setter('cols'))
当你的数据变化时,显示跟着更新,在KV中只需这样:
GridLayout:
cols:len(root.data)
注意,当属性名以小写字母开头时,部件名首字母应当大写。遵循PEP8 Naming Conventions是被鼓励的。
六、事件绑定
在KV语言中,你可以使用":"语法来绑定事件:
Widget:
on_size: my_callback()
你也可以使用args关键字传递参数:
TextInput:
on_text:app.search(args[1])
更复杂的表达式可能类似这样:
pos:self.center_x - self.texture_size[0] / 2, self.center_y - self.texture_size[1] / 2
这个表达式监听center_x, center_y, texture_size的变动。如果其中一个发生了改变,表达式将会更新pos字段。
你也可以在KV语言中处理on_事件。例如输入框有一个聚焦(focus)属性,它将自动生成on_focus事件:
TextInput:
on_focus:print(args)
七、扩展画布
KV语言可以这样来定义你的画布指令:
MyWidget:
canvas:
Color:
rgba: 1, .3, .8, .5
Line:
points: zip(self.data.x, self.data.y)
当属性值改变时它们将更新,当然,你也可以使用canvas.before和canvas.after.
八、引用部件
在一个部件树中,经常需要访问/引用其他的部件。KV语言提供了一个使用id's的方法来做这些工作。将它们认为是只能用于Kv语言类级别变量。看下面代码:
:
Button:
id: f_but
TextInput:
text: f_but.state
:
Button:
id: s_but
TextInput:
text: s_but.state
一个id被限制到它被声明的作用域内,所以在
id是一个部件的弱引用(weakref)并且不是部件本身。因此,存储id不能防止部件被垃圾回收。为了证明:
:
label_widget: label_widget
Button:
text: 'Add Button'
on_press: root.add_widget(label_widget)
Button:
text: 'Remove Button'
on_press: root.remove_widget(label_widget)
Label:
id: label_widget
text: 'widget'
上面的代码中,虽然一个到label_widget的引用被存储到MyWidget中,但是因为它仅仅是一个弱引用,一旦别的引用被移除,它不足以保持对象存活。因此,当移除按钮被点击后(将移除其他的引用)窗口将重新计算尺寸(调用垃圾回收导致重新检测label_widget),当点击添加按钮来添加部件,一个引用错误将发生(ReferenceError:weakly-referenced object no longer exists)
为了保持部件存活,一个对label_widget的引用必须被保持。可以使用id.self或label_widget.self做到。正确的方式如下:
:
label_widget: label_widget.__self__
九、在Python代码中访问Kv语言定义的部件
考虑以下在my.kv中的代码:
:
# both these variables can be the same name and this doesn't lead to
# an issue with uniqueness as the id is only accessible in kv.
txt_inpt: txt_inpt
Button:
id: f_but
TextInput:
id: txt_inpt
text: f_but.state
on_text: root.check_status(f_but)
在myapp.py:
...
class MyFirstWidget(BoxLayout):
txt_inpt = ObjectProperty(None)
def check_status(self, btn):
print('button state is: {state}'.format(state=btn.state))
print('text input text is: {txt}'.format(txt=self.txt_inpt))
...
txt_inpt被作为ObjectProperty初始化:
txt_inpt = ObjectProperty(None)
这是效果导致self.txt_inpt是None。在KV语言中,这个属性更新被id:txt_inpt引用的持有TextInput的实例。
txt_inpt:txt_inpt
从这点向上,self.txt_inpt持有一个被id txt_input标识的部件的引用并且能被用在类的任何地方,正如在check_status函数中一样。对照这个函数,你仅仅需要传递id到你想用的地方。
你可以使用ids来访问带id标识的对象,这是一种更简单的方法:
Label:
id: loki
text: 'loki: I AM YOUR GOD!'
Button:
id: hulk
text: "press to smash loki"
on_release: root.hulk_smash()
在你的Python代码中:
class Marvel(BoxLayout):
def hulk_smash(self):
self.ids.hulk.text = "hulk: puny god!"
self.ids["loki"].text = "loki: >_
当你的kv文件被解析时,kivy收集所有的带id标签的部件,并放置它们到self.ids字典中。这意味着你能以字典的风格来迭代这些部件并访问它们。
for key, val in self.ids.items():
print("key={0}, val={1}".format(key, val))
注意,虽然self.ids很简洁,它被认为是使用ObjectProperty的最佳实践。但是创建一个字典的引用,将会提供更快的访问速度并更加清晰。
十、动态类
考虑下面代码:
:
Button:
text: "Hello world, watch this text wrap inside the button"
text_size: self.size
font_size: '25sp'
markup: True
Button:
text: "Even absolute is relative to itself"
text_size: self.size
font_size: '25sp'
markup: True
Button:
text: "Repeating the same thing over and over in a comp = fail"
text_size: self.size
font_size: '25sp'
markup: True
Button:
为了替代重复的代码,我们可以使用模板来代替:
:
text_size: self.size
font_size: '25sp'
markup: True
:
MyBigButt:
text: "Hello world, watch this text wrap inside the button"
MyBigButt:
text: "Even absolute is relative to itself"
MyBigButt:
text: "repeating the same thing over and over in a comp = fail"
MyBigButt:
这个被规则声明的类继承自按钮类。它允许我们改变默认值,并为每一个实例创建绑定而不用在Python那边添加任何新的代码。
十一、在多个部件中重用样式
看下面的在my.kv中的代码:
:
Button:
on_press: self.text(txt_inpt.text)
TextInput:
id: txt_inpt
:
Button:
on_press: self.text(txt_inpt.text)
TextInput:
id: txt_inpt
在myapp.py中
class MyFirstWidget(BoxLayout):
def text(self, val):
print('text input text is: {txt}'.format(txt=val))
class MySecondWidget(BoxLayout):
writing = StringProperty('')
def text(self, val):
self.writing = val
因为两个类共同使用相同的.kv风格。如果我们为两个部件重用风格,这将使得设计简化。你可以在my.kv中这样写代码:
:
Button:
on_press: self.text(txt_inpt.text)
TextInput:
id: txt_inpt
用一个逗号(,)来分离类名,所有的类将都有同样的kv属性。
十二、使用KV语言设计
使用Kivy语言的一个目标就是分离逻辑和表现。表现层使用kv文件来表示,逻辑使用py文件来表示。
(一)py文件中写代码
让我们开始一个小例子,首先,在main.py文件中:
import kivy
kivy.require('1.0.5')
from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
from kivy.properties import ObjectProperty, StringProperty
class Controller(FloatLayout):
'''Create a controller that receives a custom widget from the kv lang file.
Add an action to be called from the kv lang file.
'''
label_wid = ObjectProperty()
info = StringProperty()
def do_action(self):
self.label_wid.text = 'My label after button press'
self.info = 'New info text'
class ControllerApp(App):
def build(self):
return Controller(info='Hello world')
if __name__ == '__main__':
ControllerApp().run()
在这个例子中,我们创建了一个带有两个属性的控制类:
另外,我们创建了一个do_action()方法来使用这些属性。它将会改变info文本和label_wid部件的文本。
(二)在controller.kv中布局
执行一个没有相应的.kv文件的应用程序可以运行,但是没有任何东西被显示到屏幕上。这是被期望的,因为控制类没有部件在里面,它仅仅是一个FloatLayout。我们能围绕Controller类在一个controller.kv文件中创建UI,当我们运行ControllerApp时它会被加载。这将如何实现及什么文件被加载都在kivy.app.App.load_kv()方法中被描述。
#:kivy 1.0
:
label_wid: my_custom_label
BoxLayout:
orientation: 'vertical'
padding: 20
Button:
text: 'My controller info is: ' + root.info
on_press: root.do_action()
Label:
id: my_custom_label
text: 'My label before button press'
在垂直布局的BoxLayout中,有一个标签和一个按钮。看起来很简单,有3个事情将被做:
从Controller使用数据。一旦在controller中info属性被改变,表达式text:'My Controller info is:' + root.info将会自动更新。
传递数据到Controller。表达式id:my_custom_label被赋值给id为my_custom_label的标签。于是,在表达式label_wid:my_custom_label中使用my_custom_label传递部件Label的实例到你的Controller。
使用Controller的on_press方法创建一个定制的回调函数。
root和self被保留为关键字,可用在任何地方。root代表规则内的根部件,self代表当前部件。
在规则内你可以使用任何id声明,同root和self一样。例如,你可以在on_press()中这样:
Button:
on_press:root.do_action();my_custom_label.font_size = 18
现在,我们运行main.py, controller.kv将会被自动加载,按钮和标签也将显示并响应你的触摸事件。
4.10集成其他框架
在Kivy内部使用Twisted框架
gthank:Twisted是用Python实现的基于事件驱动的网络引擎框架。Twisted诞生于2000年初,在当时的网络游戏开发者看来,无论他们使用哪种语言,手中都鲜有可兼顾扩展性及跨平台的网络库。Twisted的作者试图在当时现有的环境下开发游戏,这一步走的非常艰难,他们迫切地需要一个可扩展性高、基于事件驱动、跨平台的网络开发框架,为此他们决定自己实现一个,并从那些之前的游戏和网络应用程序的开发者中学习,汲取他们的经验教训。Twisted支持许多常见的传输及应用层协议,包括TCP、UDP、SSL/TLS、HTTP、IMAP、SSH、IRC以及FTP。Twisted对于其支持的所有协议都带有客户端和服务器实现,同时附带有基于命令行的工具,使得配置和部署产品级的Twisted应用变得非常方便。
你可以使用kivy.support.install_twisted_reactor函数来安装一个运行在kivy事件循环里面的twisted反应器。任何传递到该函数的参数和关键字都会被传递给threadedselect反应器交错函数。通常有一个参数传递twisted的reactor.startRunning.
警告:不像默认的Twisted反应器,安装的反应器不会处理任何的信号,除非你设置了installSignalHandlers关键字参数为1.这将使kivy来和平时一样来处理信号,除非你明确地想twisted反应器来处理信号。
kivy的样例中含一个关于twisted服务端和客户端的小例子。服务端程序有一个简单的twisted服务运行并在日志上记录所有的信息。客户端程序能发送消息到服务端,并打印它发送及响应它的信息。例子基于twisted文档的的简单的Echo 例子,你可以在下面的链接找到它们:
http://twistedmatrix.com/documents/current/_downloads/simpleserv.py
http://twistedmatrix.com/documents/current/_downloads/simpleclient.py
为了测试这个例子,首先运行echo_server_app.py,接着运行echo_client_app.py。服务端将会使用简单的echo消息来回应任何来自客户端发送的消息。
服务端:
#在导入和使用反应器之前,install_twisted_rector必须首先被调用
from kivy.support import install_twisted_reactor
install_twisted_reactor()
from twisted.internet import reactor
from twisted.internet import protocol
class EchoProtocol(protocol.Protocol):
def dataReceived(self, data):
response = self.factory.app.handle_message(data)
if response:
self.transport.write(response)
class EchoFactory(protocol.Factory):
protocol = EchoProtocol
def __init__(self, app):
self.app = app
from kivy.app import App
from kivy.uix.label import Label
class TwistedServerApp(App):
def build(self):
self.label = Label(text="server started\n")
reactor.listenTCP(8000, EchoFactory(self))
return self.label
def handle_message(self, msg):
self.label.text = "received: %s\n" % msg
if msg == "ping":
msg = "pong"
if msg == "plop":
msg = "kivy rocks"
self.label.text += "responded: %s\n" % msg
return msg
if __name__ == '__main__':
TwistedServerApp().run()
客户端:
#在导入和使用反应器之前,install_twisted_rector必须首先被调用
from kivy.support import install_twisted_reactor
install_twisted_reactor()
#一个简单的客户端,它能发送消息到echo服务端
from twisted.internet import reactor, protocol
class EchoClient(protocol.Protocol):
def connectionMade(self):
self.factory.app.on_connection(self.transport)
def dataReceived(self, data):
self.factory.app.print_message(data)
class EchoFactory(protocol.ClientFactory):
protocol = EchoClient
def __init__(self, app):
self.app = app
def clientConnectionLost(self, conn, reason):
self.app.print_message("connection lost")
def clientConnectionFailed(self, conn, reason):
self.app.print_message("connection failed")
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout
#一个简单的kivy应用程序,带有一个文本框来输入消息,
#并有一个文本标签来显示所有的从服务端返回的消息
class TwistedClientApp(App):
connection = None
def build(self):
root = self.setup_gui()
self.connect_to_server()
return root
def setup_gui(self):
self.textbox = TextInput(size_hint_y=.1, multiline=False)
self.textbox.bind(on_text_validate=self.send_message)
self.label = Label(text='connecting...\n')
self.layout = BoxLayout(orientation='vertical')
self.layout.add_widget(self.label)
self.layout.add_widget(self.textbox)
return self.layout
def connect_to_server(self):
reactor.connectTCP('localhost', 8000, EchoFactory(self))
def on_connection(self, connection):
self.print_message("connected succesfully!")
self.connection = connection
def send_message(self, *args):
msg = self.textbox.text
if msg and self.connection:
self.connection.write(str(self.textbox.text))
self.textbox.text = ""
def print_message(self, msg):
self.label.text += msg + "\n"
if __name__ == '__main__':
TwistedClientApp().run()
4.11最佳实践&4.12高级图形技术
一、最佳实践
(一)设计你的应用程序代码
(二)处理窗口尺寸大小的改变
(三)管理资源
材质(Atlas)
缓存(Cache)
(四)跨可台
(五)小技巧
皮肤
使用模块
Kivy-Remote-Shell
二、高级图形技术(Advanced Graphics)
作者:gthank
链接:https://www.jianshu.com/p/ac1d6180391b
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。