8. 控件
控件简介
控件Widget
是 Kivy 图形界面中的基本元素。控件提供了一个画布Canvas
,这是用来在屏幕上进行绘制的。控件接收事件,并且对事件作出反应。想要对 控件Widget
进行更深入的了解,可以去看看这个模块的文档。
操作控件树
Kivy 以树的形式来组织控件。你的应用程序会有一个根控件,通常会含有若干的子控件 children
,这些子控件还可以有自己的子控件。一个控件的子控件会以 children
属性的形式表述,这个属性是 Kivy 中的一个列表属性 ListProperty
可以用一下方法来操作控件树:
add_widget()
: 添加一个控件作为子控件;remove_widget()
: 从子控件列表中去掉一个控件;clear_widgets()
: 清空一个控件的所有子控件。
例如下面的代码,就是在一个盒式布局 BoxLayout 中添加一个按钮:
layout = BoxLayout(padding=10)
button = Button(text='My first button')
layout.add_widget(button)
这个按钮就添加到布局当中去了:按钮的 parent 属性会被设置为这个布局;这个按钮也会被添加到布局中的子控件列表。要把这个按钮从这个布局中删掉也很简单:
layout.remove_widget(button)
移除了之后,这个按钮的 parent 属性就会被设置为 None,也会被从布局的子控件列表中移除。
要是想清空一个控件中的所有自科技,那就用 clear_widgets()
方法就可以了:
layout.clear_widgets()
特别注意
千万别自己手动去操作子控件列表,除非你确定自己掌控得非常深入透彻。因为控件树是和绘图树联系在一起的。例如,如果你添加了一个控件到子控件列表,但没有添加这个新子控件的画布到绘图树上,那么就会出现这种情况:这个控件确实成了一个子控件,但是屏幕上不会显示出来。此外,如果你后续使用添加、移除、清空控件这些操作,可能还会遇到问题。
遍历控件树
控件类实例的子控件children
列表属性中包含了所有的子控件。所以可以用如下的方式来进行遍历:
root = BoxLayout()
# ... add widgets to root ...
for child in root.children:
print(child)
然而,这样的操作可得谨慎使用。如果你要用之前一节中提到的方法来修改这个子控件列表电话,请一定用下面这种方法来做一下备份:
for child in root.children[:]:
# manipulate the tree. For example here, remove all widgets that have a
# width
if child.width 100:
root.remove_widget(child)
默认情况下,控件是不会对子控件的尺寸/位置进行改变的。位置属性 pos
是屏幕坐标系上的绝对值(除非你使用相对布局relativelayout
,这个以后再说),而尺寸属性 size
就是一个绝对的尺寸大小。
控件索引Z
控件绘制的顺序,是基于各个控件在控件树中的位置。添加控件方法 add_widget
可以接收一个索引参数,这样就能指定该新增控件在控件树中的位置。
root.add_widget(widget, index)
索引值小的控件会被绘制在索引值大的控件之上。一定要记住,默认值是 0 ,所以后添加的控件总会在所有控件的最顶层,除非指定了索引值。
整理布局
布局 layout
是一种特别的控件,它会控制自己子控件的尺寸和位置。有各种不同的布局,这些布局分别为子控件提供拜托你个的自动组织整理。这些布局使用尺寸推测 size_hint
和位置推测 pos_hint
这两个属性来决定子控件children
的尺寸 size
和 位置pos
。
盒式布局 BoxLayout: 所有控件充满整个空间,以互相挨着的方块的方式来分布,横着或者竖着排列都可以。子控件的 size_hint 属性可以用来改变每个子控件的比例,也可以设置为固定尺寸。
网格布局 GridLayout: 以一张网格的方式来安排控件。你必须指定好网格的维度,确定好分成多少格,这样 Kivy 才能计算出每个元素的尺寸并且确定如何安排这些元素的位置。
栈状布局 StackLayout: 挨着放一个个控件,彼此邻近,在某一个维度上有固定大小,而使它们填充整个空间。 这适合用来显示相同预定义大小的子控件。
锚式布局 AnchorLayout: 一种非常简单的布局,只关注子控件的位置。 将子控件放在布局的边界位置。 不支持size_hint。
浮动布局 FloatLayout: 允许放置具任意位置和尺寸的子控件,可以是绝对尺寸,也可以是相对布局的相对尺寸。 默认的 size_hint(1,1)会让每个子控件都与整个布局一样大,所以如果你多个子控件就要修改这个值。可以把 size_hint 设置成 (None, None),这样就可以使用 size 这个绝对尺寸属性。控件也支持 pos_hint,这个属性是一个 dict 词典,用来设置相对布局的位置。
相对布局 RelativeLayout: 和浮动布局 FloatLayout 差不多,不同之处在于子控件的位置是相对于布局空间的,而不是相对于屏幕。
想要深入理解各种布局的话,可以仔细阅读各种文档。
size_hint
是一个 引用列表属性 ReferenceListProperty
,包括 size_hint_x
和size_hint_y
两个变量。接收的变量值是从0到1的各种数值,或者 None, 默认值为 (1, 1)。这表示如果控件处在布局之内,布局将会在两个方向分配全部尺寸(相对于布局大小)给该控件。
举个例子,设置size_hint
为 (0.5, 0.8),就会给该控件Widget
分配布局 layout
内50% 宽,80% 高的尺寸。
例如下面这个例子:
BoxLayout:
Button:
text: 'Button 1'
# default size_hint is 1, 1, we don't need to specify it explicitly
# however it's provided here to make things clear
size_hint: 1, 1
加载 Kivy 目录:
cd $KIVYDIR/examples/demo/kivycatalog
python main.py
把上面代码中的 $KIVYDIR 替换成你的 Kivy 安装位置。在左边点击标注有 Box Layout 的按钮。 然后将上面的代码粘贴到窗口右侧的编辑器内。
然后你就可以看到上图这样的界面了,这个按钮 Button 会占据整个布局尺寸 size
的 100%。
修改size_hint_x
/size_hint_y
为 .5 这就会把控件 Widget
调整为布局 layout
的50% 宽度 width
/高度 height
。
这时候效果如上图所示,虽然我们已经同时指定了 size_hint_x
和 size_hint_y
为 .5,但似乎只有对 size_hint_x
的修改起作用了。这是因为在盒式布局 boxlayout
中,当orientation
被设置为竖直方向(vertical) 的时候,size_hint_y
由布局来控制,而如果orientation
被设置为水平方向(horizontal)的时候, size_hint_x
由布局来控制,所以这些情况手动设定就无效了。 这些受控维度的尺寸,是根据子控件 children
在 盒式布局 boxlayout
中的总编号来计算的。在上面的例子中,这个子控件的size_hint_y
是受控的(.5/.5 = 1)。所以,这里控件就占据了上层布局的整个高度。
接下来咱们再添加一个按钮 Button
到这个 布局 layout
看看有什么效果。
盒式布局 boxlayout
默认对其所有的子控件 children
分配了等大的空间。在咱们这个例子里面,比例是50-50,因为有两个子控件 children
。那么接下来咱们就对其中的一个子控件设置一下 size_hint,然后看看效果怎么样。
从上图可以看出,如果一个子控件有了一个指定的 size_hint
,这就会决定该控件 Widget
使用盒式布局 boxlayout
提供的空间中的多大比例,来作为自己的尺寸 size
。在我们这个例子中,第一个按钮 Button
的size_hint_x
设置为了 .5。那么这个控件分配到的空间计算方法如下:
first child's size_hint divided by
first child's size_hint + second child's size_hint + ...n(no of children)
.5/(.5+1) = .333...
盒式布局 BoxLayout 的剩余宽度 width
会分配给另外的一个子控件 children
。在我们这个例子中,这就意味着第二个按钮 Button
会占据整个布局 layout
的 66.66% 宽度 width
。
修改 size_hint
探索一下来多适应一下吧。
如果你想要控制一个控件 Widget
的绝对尺寸 size
,可以把size_hint_x
/size_hint_y
当中的一个或者两个都设置成 None,这样的话该控件的宽度 width
和高度 height
的属性值就会生效了。
pos_hint
是一个词典 dict,默认值是空。相比于size_hint
,布局对pos_hint
的处理方式有些不同,不过大体上你还是可以对pos
的各种属性设定某个值来设定控件 Widget
在父控件 parent
中的相对位置(可以设定的属性包括:x
, y
, right
, top
, center_x
, center_y
)。
咱们用下面 kivycatalog 中的代码来可视化地理解一下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
,来理解一下这个属性对控件位置的效果。
给布局添加背景
关于布局,有一个问题经常被问道:
“怎么给一个布局添加背景图片/颜色/视频/等等……”
本来默认的各种布局都是没有视觉呈现的:因为布局不像控件,布局是默认不含有绘图指令的。不过呢,还是你可以给一个布局实例添上绘图指令,也就可以添加一个彩色背景了:
在 Python 中的实现方法:
from kivy.graphics import Color, Rectangle
with layout_instance.canvas.before:
Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255
self.rect = Rectangle(size=layout_instance.size,
pos=layout_instance.pos)
然而很不幸,这样只能在布局的初始化位置以布局的初始尺寸绘制一个矩形。所以还要对布局的尺寸和位置变化进行监听,然后对矩形的尺寸位置进行更新,这样才能保证这个矩形一直绘制在布局的内部。可以用如下方式实现:
with layout_instance.canvas.before:
Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255
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
# listen to size and position changes
layout_instance.bind(pos=update_rect, size=update_rect)
在 kv 文件中:
FloatLayout:
canvas.before:
Color:
rgba: 0, 1, 0, 1
Rectangle:
# self here refers to the widget i.e BoxLayout
pos: self.pos
size: self.size
上面的 Kv 文件中的生命,就建立了一个隐含的绑定:上面 Kv 代码中的最后两行保证了矩形的位置 pos
和尺寸 size
会在浮动布局 floatlayout
的位置 pos
发生变化的时候进行更新。
接下来咱们把上面的代码片段放进 Kivy 应用里面。
纯 Python 方法:
from kivy.app import App
from kivy.graphics import Color, Rectangle
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
class RootWidget(FloatLayout):
def __init__(self, **kwargs):
# make sure we aren't overriding any important functionality
super(RootWidget, self).__init__(**kwargs)
# let's add a Widget to this layout
self.add_widget(
Button(
text="Hello World",
size_hint=(.5, .5),
pos_hint={'center_x': .5, 'center_y': .5}))
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, 1, 0, 1) # green; colors range from 0-1 not 0-255
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()
使用 Kv 语言:
from kivy.app import App
from kivy.lang import Builder
root = Builder.load_string('''
FloatLayout:
canvas.before:
Color:
rgba: 0, 1, 0, 1
Rectangle:
# self here refers to the widget i.e FloatLayout
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()
上面这两个应用的效果都如下图所示:
给自定义布局规则/类增加背景色
上面那一段中咱们对布局实例增加背景的方法,如果用到很多歌布局里面,那就很快变得特别麻烦了。要解决这种需求,就可以基于布局类 Layout 创建一个自定义的布局子类,给自定义的这个类增加一个背景。
使用 Python:
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()
使用 Kv 语言:
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('''
<CustomLayout>
canvas.before:
Color:
rgba: 0, 1, 0, 1
Rectangle:
pos: self.pos
size: self.size
<RootWidget>
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 规则进行覆盖。
就拿网格布局 GridLayout 举例吧:
<GridLayout>
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('''
<GridLayout>
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
<RootWidget>
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()
效果大概如下图所示:
我们已经对网格布局 GridLayout 类的规则进行了覆盖,所以接下来在应用中使用这个类就都会显示那幅图片了。
动画背景怎么弄呢?
就像在矩形Rectangle/ 边界图像BorderImage /椭圆Ellipse/等里面添加设置绘图指令一样,可以用一个特定的纹理属性 texture :
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('''
<CustomLayout>
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
<RootWidget>
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_image 设置成一个对象属性 ObjectProperty
,这样就可以在其中添加一个图形控件 Image
。图像控件有一个纹理属性(texture property);在前面的 self.background_image.texture 这句代码中,就是建立了一个名为 texture 的到这个属性的引用。图形控件 Image
支持动画(animation):随着动画的改变,图像的纹理会同步更新,在这个过程中,边界图像 BorderImage 指令的 texture 纹理属性也会同步更新。
(译者注:texture of BorderImage instruction,这里我对 instruction 的翻译应该是不太对的,不过我还没理清楚该怎么表述。)
也可以直接传递自定义数据到纹理属性 texture。更多细节可以参考纹理 Texture
的文档。
网状布局
嗯,看看这个过程如何扩展是很有趣的。
尺寸和位置度量
Kivy 的默认长度单位是像素 pixel,所有的尺寸和位置都用这个单位来表达。你也可以用其他单位来衡量,在跨平台多种设备的时候,这有助于实现更好的连续性体验(这些设备会把尺寸自动转换到像素)。
可用单位包括 pt
, mm
,cm
, inch
, dp
and sp
。可以在度量文档 metrics
中了解更多相关内容。
你还可以探索一下屏幕模块 screen
的用法,这个可以模拟出不同设备的屏幕,供测试应用。
使用屏幕管理器进行屏幕分割
如果你的应用程序要包含多个程序,那可能就需要从一个屏幕 Screen
到另一个屏幕 Screen
提供一个导航的通道。幸运的是,正好有一个屏幕管理器类ScreenManager
,这个类允许你来定义分开的各个屏幕,设置屏幕管理器的TransitionBase
就可以实现从一个屏幕到另一个屏幕的跳转导航。