8. 控件

优质
小牛编辑
132浏览
2023-12-01

英文原文

控件简介

控件Widget是 Kivy 图形界面中的基本元素。控件提供了一个画布Canvas,这是用来在屏幕上进行绘制的。控件接收事件,并且对事件作出反应。想要对 控件Widget进行更深入的了解,可以去看看这个模块的文档。

操作控件树

Kivy 以树的形式来组织控件。你的应用程序会有一个根控件,通常会含有若干的子控件 children,这些子控件还可以有自己的子控件。一个控件的子控件会以 children属性的形式表述,这个属性是 Kivy 中的一个列表属性 ListProperty

可以用一下方法来操作控件树:

例如下面的代码,就是在一个盒式布局 BoxLayout 中添加一个按钮:

  1. layout = BoxLayout(padding=10)
  2. button = Button(text='My first button')
  3. layout.add_widget(button)

这个按钮就添加到布局当中去了:按钮的 parent 属性会被设置为这个布局;这个按钮也会被添加到布局中的子控件列表。要把这个按钮从这个布局中删掉也很简单:

  1. layout.remove_widget(button)

移除了之后,这个按钮的 parent 属性就会被设置为 None,也会被从布局的子控件列表中移除。

要是想清空一个控件中的所有自科技,那就用 clear_widgets()方法就可以了:

  1. layout.clear_widgets()

特别注意

千万别自己手动去操作子控件列表,除非你确定自己掌控得非常深入透彻。因为控件树是和绘图树联系在一起的。例如,如果你添加了一个控件到子控件列表,但没有添加这个新子控件的画布到绘图树上,那么就会出现这种情况:这个控件确实成了一个子控件,但是屏幕上不会显示出来。此外,如果你后续使用添加、移除、清空控件这些操作,可能还会遇到问题。

遍历控件树

控件类实例的子控件children列表属性中包含了所有的子控件。所以可以用如下的方式来进行遍历:

  1. root = BoxLayout()
  2. # ... add widgets to root ...
  3. for child in root.children:
  4. print(child)

然而,这样的操作可得谨慎使用。如果你要用之前一节中提到的方法来修改这个子控件列表电话,请一定用下面这种方法来做一下备份:

  1. for child in root.children[:]:
  2. # manipulate the tree. For example here, remove all widgets that have a
  3. # width
  4. if child.width 100:
  5. root.remove_widget(child)

默认情况下,控件是不会对子控件的尺寸/位置进行改变的。位置属性 pos是屏幕坐标系上的绝对值(除非你使用相对布局relativelayout,这个以后再说),而尺寸属性 size就是一个绝对的尺寸大小。

控件索引Z

控件绘制的顺序,是基于各个控件在控件树中的位置。添加控件方法 add_widget 可以接收一个索引参数,这样就能指定该新增控件在控件树中的位置。

  1. root.add_widget(widget, index)

索引值小的控件会被绘制在索引值大的控件之上。一定要记住,默认值是 0 ,所以后添加的控件总会在所有控件的最顶层,除非指定了索引值。

整理布局

布局 layout是一种特别的控件,它会控制自己子控件的尺寸和位置。有各种不同的布局,这些布局分别为子控件提供拜托你个的自动组织整理。这些布局使用尺寸推测 size_hint位置推测 pos_hint这两个属性来决定子控件children尺寸 size位置pos

盒式布局 BoxLayout: 所有控件充满整个空间,以互相挨着的方块的方式来分布,横着或者竖着排列都可以。子控件的 size_hint 属性可以用来改变每个子控件的比例,也可以设置为固定尺寸。

../_images/boxlayout1.gif ../_images/gridlayout1.gif ../_images/stacklayout1.gif ../_images/anchorlayout1.gif../_images/floatlayout1.gif

网格布局 GridLayout: 以一张网格的方式来安排控件。你必须指定好网格的维度,确定好分成多少格,这样 Kivy 才能计算出每个元素的尺寸并且确定如何安排这些元素的位置。

栈状布局 StackLayout: 挨着放一个个控件,彼此邻近,在某一个维度上有固定大小,而使它们填充整个空间。 这适合用来显示相同预定义大小的子控件。

锚式布局 AnchorLayout: 一种非常简单的布局,只关注子控件的位置。 将子控件放在布局的边界位置。 不支持size_hint。

浮动布局 FloatLayout: 允许放置具任意位置和尺寸的子控件,可以是绝对尺寸,也可以是相对布局的相对尺寸。 默认的 size_hint(1,1)会让每个子控件都与整个布局一样大,所以如果你多个子控件就要修改这个值。可以把 size_hint 设置成 (None, None),这样就可以使用 size 这个绝对尺寸属性。控件也支持 pos_hint,这个属性是一个 dict 词典,用来设置相对布局的位置。

相对布局 RelativeLayout: 和浮动布局 FloatLayout 差不多,不同之处在于子控件的位置是相对于布局空间的,而不是相对于屏幕。

想要深入理解各种布局的话,可以仔细阅读各种文档。

size_hintpos_hint:

size_hint 是一个 引用列表属性 ReferenceListProperty ,包括 size_hint_xsize_hint_y 两个变量。接收的变量值是从0到1的各种数值,或者 None, 默认值为 (1, 1)。这表示如果控件处在布局之内,布局将会在两个方向分配全部尺寸(相对于布局大小)给该控件。

举个例子,设置size_hint 为 (0.5, 0.8),就会给该控件Widget 分配布局 layout 内50% 宽,80% 高的尺寸。

例如下面这个例子:

  1. BoxLayout:
  2. Button:
  3. text: 'Button 1'
  4. # default size_hint is 1, 1, we don't need to specify it explicitly
  5. # however it's provided here to make things clear
  6. size_hint: 1, 1

加载 Kivy 目录:

  1. cd $KIVYDIR/examples/demo/kivycatalog
  2. python main.py

把上面代码中的 $KIVYDIR 替换成你的 Kivy 安装位置。在左边点击标注有 Box Layout 的按钮。 然后将上面的代码粘贴到窗口右侧的编辑器内。

../_images/size_hint[B].jpg

然后你就可以看到上图这样的界面了,这个按钮 Button 会占据整个布局尺寸 size的 100%。

修改size_hint_x/size_hint_y 为 .5 这就会把控件 Widget 调整为布局 layout 的50% 宽度 width/高度 height

../_images/size_hint[b_].jpg

这时候效果如上图所示,虽然我们已经同时指定了 size_hint_xsize_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看看有什么效果。

../_images/size_hint[bb].jpg

盒式布局 boxlayout 默认对其所有的子控件 children分配了等大的空间。在咱们这个例子里面,比例是50-50,因为有两个子控件 children。那么接下来咱们就对其中的一个子控件设置一下 size_hint,然后看看效果怎么样。

../_images/size_hint[oB].jpg

从上图可以看出,如果一个子控件有了一个指定的 size_hint,这就会决定该控件 Widget使用盒式布局 boxlayout提供的空间中的多大比例,来作为自己的尺寸 size 。在我们这个例子中,第一个按钮 Buttonsize_hint_x设置为了 .5。那么这个控件分配到的空间计算方法如下:

  1. first child's size_hint divided by
  2. first child's size_hint + second child's size_hint + ...n(no of children)
  3. .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

  1. FloatLayout:
  2. Button:
  3. text: "We Will"
  4. pos: 100, 100
  5. size_hint: .2, .4
  6. Button:
  7. text: "Wee Wiill"
  8. pos: 200, 200
  9. size_hint: .4, .2
  10. Button:
  11. text: "ROCK YOU!!"
  12. pos_hint: {'x': .3, 'y': .6}
  13. size_hint: .5, .2

这份代码的输出效果如下图所示:

../_images/pos_hint.jpg

说了半天size_hint,你不妨自己试试探索一下 pos_hint,来理解一下这个属性对控件位置的效果。

给布局添加背景

关于布局,有一个问题经常被问道:

“怎么给一个布局添加背景图片/颜色/视频/等等……”

本来默认的各种布局都是没有视觉呈现的:因为布局不像控件,布局是默认不含有绘图指令的。不过呢,还是你可以给一个布局实例添上绘图指令,也就可以添加一个彩色背景了:

在 Python 中的实现方法:

  1. from kivy.graphics import Color, Rectangle
  2. with layout_instance.canvas.before:
  3. Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255
  4. self.rect = Rectangle(size=layout_instance.size,
  5. pos=layout_instance.pos)

然而很不幸,这样只能在布局的初始化位置以布局的初始尺寸绘制一个矩形。所以还要对布局的尺寸和位置变化进行监听,然后对矩形的尺寸位置进行更新,这样才能保证这个矩形一直绘制在布局的内部。可以用如下方式实现:

  1. with layout_instance.canvas.before:
  2. Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255
  3. self.rect = Rectangle(size=layout_instance.size,
  4. pos=layout_instance.pos)
  5. def update_rect(instance, value):
  6. instance.rect.pos = instance.pos
  7. instance.rect.size = instance.size
  8. # listen to size and position changes
  9. layout_instance.bind(pos=update_rect, size=update_rect)

在 kv 文件中:

  1. FloatLayout:
  2. canvas.before:
  3. Color:
  4. rgba: 0, 1, 0, 1
  5. Rectangle:
  6. # self here refers to the widget i.e BoxLayout
  7. pos: self.pos
  8. size: self.size

上面的 Kv 文件中的生命,就建立了一个隐含的绑定:上面 Kv 代码中的最后两行保证了矩形的位置 pos尺寸 size会在浮动布局 floatlayout位置 pos发生变化的时候进行更新。

接下来咱们把上面的代码片段放进 Kivy 应用里面。

纯 Python 方法:

  1. from kivy.app import App
  2. from kivy.graphics import Color, Rectangle
  3. from kivy.uix.floatlayout import FloatLayout
  4. from kivy.uix.button import Button
  5. class RootWidget(FloatLayout):
  6. def __init__(self, **kwargs):
  7. # make sure we aren't overriding any important functionality
  8. super(RootWidget, self).__init__(**kwargs)
  9. # let's add a Widget to this layout
  10. self.add_widget(
  11. Button(
  12. text="Hello World",
  13. size_hint=(.5, .5),
  14. pos_hint={'center_x': .5, 'center_y': .5}))
  15. class MainApp(App):
  16. def build(self):
  17. self.root = root = RootWidget()
  18. root.bind(size=self._update_rect, pos=self._update_rect)
  19. with root.canvas.before:
  20. Color(0, 1, 0, 1) # green; colors range from 0-1 not 0-255
  21. self.rect = Rectangle(size=root.size, pos=root.pos)
  22. return root
  23. def _update_rect(self, instance, value):
  24. self.rect.pos = instance.pos
  25. self.rect.size = instance.size
  26. if __name__ == '__main__':
  27. MainApp().run()

使用 Kv 语言:

  1. from kivy.app import App
  2. from kivy.lang import Builder
  3. root = Builder.load_string('''
  4. FloatLayout:
  5. canvas.before:
  6. Color:
  7. rgba: 0, 1, 0, 1
  8. Rectangle:
  9. # self here refers to the widget i.e FloatLayout
  10. pos: self.pos
  11. size: self.size
  12. Button:
  13. text: 'Hello World!!'
  14. size_hint: .5, .5
  15. pos_hint: {'center_x':.5, 'center_y': .5}
  16. ''')
  17. class MainApp(App):
  18. def build(self):
  19. return root
  20. if __name__ == '__main__':
  21. MainApp().run()

上面这两个应用的效果都如下图所示:
../_images/layout_background.png

自定义布局规则/类增加背景色

上面那一段中咱们对布局实例增加背景的方法,如果用到很多歌布局里面,那就很快变得特别麻烦了。要解决这种需求,就可以基于布局类 Layout 创建一个自定义的布局子类,给自定义的这个类增加一个背景。

使用 Python:

  1. from kivy.app import App
  2. from kivy.graphics import Color, Rectangle
  3. from kivy.uix.boxlayout import BoxLayout
  4. from kivy.uix.floatlayout import FloatLayout
  5. from kivy.uix.image import AsyncImage
  6. class RootWidget(BoxLayout):
  7. pass
  8. class CustomLayout(FloatLayout):
  9. def __init__(self, **kwargs):
  10. # make sure we aren't overriding any important functionality
  11. super(CustomLayout, self).__init__(**kwargs)
  12. with self.canvas.before:
  13. Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255
  14. self.rect = Rectangle(size=self.size, pos=self.pos)
  15. self.bind(size=self._update_rect, pos=self._update_rect)
  16. def _update_rect(self, instance, value):
  17. self.rect.pos = instance.pos
  18. self.rect.size = instance.size
  19. class MainApp(App):
  20. def build(self):
  21. root = RootWidget()
  22. c = CustomLayout()
  23. root.add_widget(c)
  24. c.add_widget(
  25. AsyncImage(
  26. source="http://www.everythingzoomer.com/wp-content/uploads/2013/01/Monday-joke-289x277.jpg",
  27. size_hint= (1, .5),
  28. pos_hint={'center_x':.5, 'center_y':.5}))
  29. 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'))
  30. c = CustomLayout()
  31. c.add_widget(
  32. AsyncImage(
  33. source="http://www.stuffistumbledupon.com/wp-content/uploads/2012/04/Get-a-Girlfriend-Meme-empty-wallet.jpg",
  34. size_hint= (1, .5),
  35. pos_hint={'center_x':.5, 'center_y':.5}))
  36. root.add_widget(c)
  37. return root
  38. if __name__ == '__main__':
  39. MainApp().run()

使用 Kv 语言:

  1. from kivy.app import App
  2. from kivy.uix.floatlayout import FloatLayout
  3. from kivy.uix.boxlayout import BoxLayout
  4. from kivy.lang import Builder
  5. Builder.load_string('''
  6. <CustomLayout>
  7. canvas.before:
  8. Color:
  9. rgba: 0, 1, 0, 1
  10. Rectangle:
  11. pos: self.pos
  12. size: self.size
  13. <RootWidget>
  14. CustomLayout:
  15. AsyncImage:
  16. source: 'http://www.everythingzoomer.com/wp-content/uploads/2013/01/Monday-joke-289x277.jpg'
  17. size_hint: 1, .5
  18. pos_hint: {'center_x':.5, 'center_y': .5}
  19. AsyncImage:
  20. source: 'http://www.stuffistumbledupon.com/wp-content/uploads/2012/05/Have-you-seen-this-dog-because-its-awesome-meme-puppy-doggy.jpg'
  21. CustomLayout
  22. AsyncImage:
  23. source: 'http://www.stuffistumbledupon.com/wp-content/uploads/2012/04/Get-a-Girlfriend-Meme-empty-wallet.jpg'
  24. size_hint: 1, .5
  25. pos_hint: {'center_x':.5, 'center_y': .5}
  26. ''')
  27. class RootWidget(BoxLayout):
  28. pass
  29. class CustomLayout(FloatLayout):
  30. pass
  31. class MainApp(App):
  32. def build(self):
  33. return RootWidget()
  34. if __name__ == '__main__':
  35. MainApp().run()

上面这两个应用的效果都如下图所示:

../_images/custom_layout_background.png

在自定义布局类中定义了背景之后,就是要确保在自定义布局的各个实例中使用到这个新特性。

首先,要在全局上增加一个图形或者颜色给内置的 Kivy 布局的背景,这就需要将所用布局的默认 Kv 规则进行覆盖。

就拿网格布局 GridLayout 举例吧:

  1. <GridLayout>
  2. canvas.before:
  3. Color:
  4. rgba: 0, 1, 0, 1
  5. BorderImage:
  6. source: '../examples/widgets/sequenced_images/data/images/button_white.png'
  7. pos: self.pos
  8. size: self.size

接下来把这段代码放到一个 Kivy 应用里面:

  1. from kivy.app import App
  2. from kivy.uix.floatlayout import FloatLayout
  3. from kivy.lang import Builder
  4. Builder.load_string('''
  5. <GridLayout>
  6. canvas.before:
  7. BorderImage:
  8. # BorderImage behaves like the CSS BorderImage
  9. border: 10, 10, 10, 10
  10. source: '../examples/widgets/sequenced_images/data/images/button_white.png'
  11. pos: self.pos
  12. size: self.size
  13. <RootWidget>
  14. GridLayout:
  15. size_hint: .9, .9
  16. pos_hint: {'center_x': .5, 'center_y': .5}
  17. rows:1
  18. Label:
  19. text: "I don't suffer from insanity, I enjoy every minute of it"
  20. text_size: self.width-20, self.height-20
  21. valign: 'top'
  22. Label:
  23. text: "When I was born I was so surprised; I didn't speak for a year and a half."
  24. text_size: self.width-20, self.height-20
  25. valign: 'middle'
  26. halign: 'center'
  27. Label:
  28. text: "A consultant is someone who takes a subject you understand and makes it sound confusing"
  29. text_size: self.width-20, self.height-20
  30. valign: 'bottom'
  31. halign: 'justify'
  32. ''')
  33. class RootWidget(FloatLayout):
  34. pass
  35. class MainApp(App):
  36. def build(self):
  37. return RootWidget()
  38. if __name__ == '__main__':
  39. MainApp().run()

效果大概如下图所示:

../_images/global_background.png

我们已经对网格布局 GridLayout 类的规则进行了覆盖,所以接下来在应用中使用这个类就都会显示那幅图片了。

动画背景怎么弄呢?

就像在矩形Rectangle/ 边界图像BorderImage /椭圆Ellipse/等里面添加设置绘图指令一样,可以用一个特定的纹理属性 texture :

  1. Rectangle:
  2. texture: reference to a texture

可以用下面的代码实现一个动画背景:

  1. from kivy.app import App
  2. from kivy.uix.floatlayout import FloatLayout
  3. from kivy.uix.gridlayout import GridLayout
  4. from kivy.uix.image import Image
  5. from kivy.properties import ObjectProperty
  6. from kivy.lang import Builder
  7. Builder.load_string('''
  8. <CustomLayout>
  9. canvas.before:
  10. BorderImage:
  11. # BorderImage behaves like the CSS BorderImage
  12. border: 10, 10, 10, 10
  13. texture: self.background_image.texture
  14. pos: self.pos
  15. size: self.size
  16. <RootWidget>
  17. CustomLayout:
  18. size_hint: .9, .9
  19. pos_hint: {'center_x': .5, 'center_y': .5}
  20. rows:1
  21. Label:
  22. text: "I don't suffer from insanity, I enjoy every minute of it"
  23. text_size: self.width-20, self.height-20
  24. valign: 'top'
  25. Label:
  26. text: "When I was born I was so surprised; I didn't speak for a year and a half."
  27. text_size: self.width-20, self.height-20
  28. valign: 'middle'
  29. halign: 'center'
  30. Label:
  31. text: "A consultant is someone who takes a subject you understand and makes it sound confusing"
  32. text_size: self.width-20, self.height-20
  33. valign: 'bottom'
  34. halign: 'justify'
  35. ''')
  36. class CustomLayout(GridLayout):
  37. background_image = ObjectProperty(
  38. Image(
  39. source='../examples/widgets/sequenced_images/data/images/button_white_animated.zip',
  40. anim_delay=.1))
  41. class RootWidget(FloatLayout):
  42. pass
  43. class MainApp(App):
  44. def build(self):
  45. return RootWidget()
  46. if __name__ == '__main__':
  47. MainApp().run()

要理解这里到底发生了什么,得从第 13 行开始看:

  1. texture: self.background_image.texture

这里是指定让边界图像 BorderImage 的纹理属性在背景图像 background_image 的纹理属性发生更新的时候进行同步更新。背景图像 background_image 属性的定义是在第 40 行:

  1. 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,所有的尺寸和位置都用这个单位来表达。你也可以用其他单位来衡量,在跨平台多种设备的时候,这有助于实现更好的连续性体验(这些设备会把尺寸自动转换到像素)。

可用单位包括 ptmmcm, inchdp and sp。可以在度量文档 metrics 中了解更多相关内容。

你还可以探索一下屏幕模块 screen的用法,这个可以模拟出不同设备的屏幕,供测试应用。

使用屏幕管理器进行屏幕分割

如果你的应用程序要包含多个程序,那可能就需要从一个屏幕 Screen到另一个屏幕 Screen提供一个导航的通道。幸运的是,正好有一个屏幕管理器类ScreenManager,这个类允许你来定义分开的各个屏幕,设置屏幕管理器的TransitionBase就可以实现从一个屏幕到另一个屏幕的跳转导航。