控件

Widget 简介

Widget 是 Kivy 中 GUI 界面的基础构建块。它提供了一个可用于在屏幕上绘制的 Canvas。它接收事件并对其作出反应。关于 Widget 类的深入解释,请参阅模块文档。

操作 Widget 树

Kivy 中的控件以树状结构组织。您的应用程序有一个`根控件`,它通常拥有可以再拥有自己子控件的|children|。控件的子控件通过|children|属性表示,这是一个 Kivy 的|ListProperty|。

可以通过以下方法操作控件树:

例如,如果你想在BoxLayout中添加一个按钮,可以这样做:

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

按钮被添加到布局中:按钮的父属性将被设置为该布局;该布局会将按钮添加到其子元素列表中。要从布局中移除按钮,请执行以下操作:

layout.remove_widget(button)

移除后,按钮的父属性将被设置为None,并且布局会从其子组件列表中移除该按钮。

如果你想清除一个部件内的所有子部件,请使用 clear_widgets() 方法:

layout.clear_widgets()

警告

除非你确实清楚自己在做什么,否则切勿自行操作子部件列表。部件树与图形树相关联。例如,如果你将部件添加到子部件列表中,却没有将其画布添加到图形树中,那么该部件虽然会成为子部件,但屏幕上不会绘制任何内容。此外,在后续调用 add_widget、remove_widget 和 clear_widgets 时,你可能会遇到问题。

遍历树结构

Widget 类实例的 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 < 100
    if child.width < 100:
        root.remove_widget(child)

默认情况下,Widget 不会影响其子 Widget 的尺寸和位置。pos 属性是屏幕坐标中的绝对位置(除非你使用 relativelayout,这一点稍后会详细说明),而 size 则是绝对尺寸。

控件 Z 索引

控件绘制的顺序基于控件在控件树中的位置。add_widget 方法接受一个 index 参数,可用于指定其在控件树中的位置:

root.add_widget(widget, index)

索引值较小的控件将绘制在索引值较大的控件之上。请注意,index 的默认值为 0,因此除非另有指定,后添加的控件会绘制在其他控件之上。

使用布局来组织界面。

|Layout|是一种特殊的控件,用于控制其子控件的大小和位置。存在多种不同类型的布局,以实现对子控件的不同自动组织方式。布局利用|size_hint|和|pos_hint|属性来确定其|children|的|size|和|pos|

../_images/boxlayout.gif ../_images/gridlayout.gif ../_images/stacklayout.gif ../_images/anchorlayout.gif ../_images/floatlayout.gif

BoxLayout:以相邻方式(垂直或水平)排列子控件,以填满所有空间。子控件的`size_hint`属性可用于调整分配给每个子控件的比例,或为其中一些子控件设置固定尺寸。

GridLayout:将控件按网格排列。您必须至少指定网格的一个维度,以便 Kivy 能够计算元素的大小及其排列方式。

StackLayout:将子控件彼此相邻排列,但在其中一个维度上设置固定大小,而不试图让它们填满整个空间。这对于显示具有相同预定义大小的子控件非常有用。

AnchorLayout:一个仅关注子控件位置的简单布局。它允许将子控件放置在相对于布局边框的位置。size_hint 不被支持。

FloatLayout:允许以任意位置和尺寸放置子组件,尺寸可以是绝对的,也可以是相对于布局大小的。默认的 size_hint 为 (1, 1),这会使每个子组件与整个布局大小相同,因此如果你有多个子组件,可能需要更改此值。你可以将 size_hint 设置为 (None, None) 以使用绝对尺寸配合 size 属性。此组件同样支持 pos_hint,它是一个字典,用于设置相对于布局位置的位置。

RelativeLayout:行为与 FloatLayout 类似,但子控件的位置是相对于布局位置,而非屏幕。

请参阅各个布局的文档,以更深入地理解。

size_hintpos_hint

size_hintsize_hint_xsize_hint_yReferenceListProperty。它接受从 01None 的值,默认值为 (1, 1)。这意味着如果该控件位于布局中,布局将在两个方向上(相对于布局的大小)尽可能多地为其分配空间。

例如,将 size_hint 设置为 (0.5, 0.8),将使 Widgetlayout 内占可用宽度的 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目录,但请将$KIVYDIR替换为你的安装目录(可通过:py:mod:`os.path.dirname(kivy.__file__)`发现)::

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

将出现一个新窗口。点击左侧“欢迎”|Spinner|下方的区域,并将该处的文本替换为上面你的kv代码。

../_images/size_hint%5BB%5D.jpg

从上面的图片可以看出,Button 占据了布局 size 的 100%。

size_hint_x/size_hint_y 设置为 .5 将使 Widget 占据 layout width/height 的 50%。

../_images/size_hint%5Bb_%5D.jpg

你可以看到,尽管我们将`size_hint_x`和`size_hint_y`都设为.5,但似乎只有`size_hint_y`被遵循了。这是因为当`orientation`为`vertical`时,BoxLayout`控制`size_hint_y;当`orientation`为`horizontal`时,控制`size_hint_x`。被控制的维度大小是根据`BoxLayout`中`children`的总数来计算的。在这个例子中,一个子项的`size_hint_y`被控制(.5/.5 = 1)。因此,该控件占据了父布局高度的100%。

让我们在 layout 中添加另一个 Button,看看会发生什么。

../_images/size_hint%5Bbb%5D.jpg

boxlayout 本质上会在其 children 之间平均分配可用空间。在我们的示例中,比例是50-50,因为我们有两个 children。让我们在一个子元素上使用 size_hint,看看结果如何。

../_images/size_hint%5BoB%5D.jpg

如果子组件指定了 size_hint,这表示该 Widget 将从 boxlayout 分配给它的 size 中占用多少空间。在我们的示例中,第一个 Buttonsize_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 width 的 66.66%。

尝试使用 size_hint 来熟悉它。

如果你想要控制一个 Widget 的绝对 size,可以将 size_hint_x/size_hint_y 或两者都设置为 None,这样该 widget 的 width 和/或 height 属性就会被尊重。

pos_hint 是一个字典,默认为空。与 size_hint 类似,布局对 pos_hint 的处理方式有所不同,但通常你可以向 pos 的任何属性(xyrighttopcenter_xcenter_y)添加值,以使 Widget 相对于其 parent 进行定位。

让我们在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

这给我们带来了:

../_images/pos_hint.jpg

size_hint 类似,您应通过实验使用 pos_hint 来理解它对控件位置的影响。

为布局添加背景

关于布局,一个经常被问到的问题是:

"How to add a background image/color/video/... to a Layout"

布局本质上没有视觉表现:默认情况下,它们没有画布指令。然而,您可以轻松地向布局实例添加画布指令,例如添加彩色背景:

在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 FloatLayout
            pos: self.pos
            size: self.size

kv 声明设置了一个隐式绑定:最后两行 kv 代码确保当 floatlayoutpos 发生变化时,矩形的 possize 值会随之更新。

现在我们将上述代码片段放入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()

两个应用的外观应该大致如下:

../_images/layout_background.png

为**自定义布局规则/类**的背景添加颜色。

如果我们需要在多个布局中使用背景,直接向布局实例添加背景的方式很快就会变得繁琐。为解决这一问题,您可以继承布局类并创建自己的布局,以便在布局中添加背景。

使用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()

两个应用的外观应该大致如下:

../_images/custom_layout_background.png

在自定义布局类中定义背景,可确保其在每个CustomLayout实例中都会被使用。

现在,要在内置的 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()

结果应该看起来像这样:

../_images/global_background.png

由于我们正在覆盖GridLayout类的规则,因此在我们应用中任何使用这个类的地方都会显示该图像。

来个**动画背景**怎么样?

你可以设置绘制指令(如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('''
<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

这指定了每当 background_imagetexture 属性更新时,BorderImagetexture 属性也会随之更新。我们在第40行定义了 background_image 属性:

background_image = ObjectProperty(...

这将`background_image`设置为一个|ObjectProperty|,在其中我们添加了一个|Image|控件。图像控件具有`texture`属性;在您看到`self.background_image.texture`的地方,这设置了一个引用`texture`指向该属性。|Image|控件支持动画:每当动画变化时,图像的纹理会被更新,同时BorderImage指令的纹理也会在这个过程中被更新。

你也可以直接将自定义数据绘制到纹理上。具体细节,请参阅 Texture 的文档。

嵌套布局

是的!看到这个过程的可扩展性有多强,真是相当有趣。

尺寸与位置度量

Kivy 默认的长度单位是像素,所有尺寸和位置默认都以像素表示。您也可以使用其他单位来表达它们,这有助于在不同设备间实现更好的一致性(这些单位会自动转换为像素大小)。

可用的单位有 ptmmcminchdpsp。您可以在 metrics 文档中了解它们的用法。

您还可以尝试使用 screen 来模拟不同设备屏幕,以便为您的应用程序进行测试。

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

如果你的应用程序由多个屏幕组成,你可能希望有一种简便的方法在 Screen 之间导航。幸运的是,有 ScreenManager 类,它允许你分别定义屏幕,并设置从一个屏幕到另一个屏幕的 TransitionBase