kivy 之 布局(下)

70 阅读7分钟

kivy最新版2.3.1 (截止2025年上半年), 其提供的布局类有如下这些:

  1. GridLayout
  2. PageLayout
  3. BoxLayout
  4. StackLayout
  5. AnchorLayout
  6. FloatLayout
  7. RelativeLayout
  8. ScatterLayout

布局(下)我们来讨论后4个布局类.

1.AnchorLayout

AnchorLayout是锚点布局. 该布局可以通过2个属性anchor_xanchor_y将子Widget设置在其内部的9个位置.

默认情况下, anchor_x被设置为middle, anchor_y被设置为center.

需要注意的是,AnchorLayout要展现出效果,需要结合子组件的size_hintsize属性共同使用,否则虽然设置了子组件的位置,但默认组件会填充满整个AnchorLayout可容纳的空间.

Sample 1: 简单理解

image.png

如:

  • GridLayout容器的第3个空间是可以允许放置绿框尺寸大小的组件,
  • 在其中放置一个AnchorLayout,设置它的属性anchor_x='right', anchor_y='top',
  • 在这个AnchorLayout中放置一个Button(text='3', size_hint=[.5, .5]),

最终Button在A容器中的表现是仅占这个绿框空间的右上角的1/4的空间.

代码如下:

from kivy.app import App
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label


class MyGridLayout(GridLayout):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.cols = 2
        self.rows = 2

        self.add_widget(Label(text='placeHolder 1'))
        self.add_widget(Label(text='placeHolder 2'))

        anchor3 = AnchorLayout(anchor_x='right', anchor_y='top')
        anchor3.add_widget(Button(text='3', background_color='yellow', size_hint=[.5, .5]))
        self.add_widget(anchor3)


class AnchorApp(App):

    def build(self):
        return MyGridLayout()


if __name__ == '__main__':
    AnchorApp().run()

sample 2: 嵌套长方形

基于anchor_x='center', anchor_y='center'的思路, 可以实现一个嵌套的长方形

image.png

代码如下:

from kivy.app import App
from kivy.uix.anchorlayout import AnchorLayout
from kivy.graphics import Color, Rectangle


class OuterAnchorLayout(AnchorLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        with self.canvas.before:
            Color(1, 0, 0, 0.5)  # 外层AnchorLayout背景色为半透明红色
            self.rect_outer = Rectangle(size=self.size, pos=self.pos)
        self.bind(size=self.update_rect_outer, pos=self.update_rect_outer)

    def update_rect_outer(self, *args):
        self.rect_outer.size = self.size
        self.rect_outer.pos = self.pos


class MiddleAnchorLayout(AnchorLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        with self.canvas.before:
            Color(0, 1, 0, 0.5)  # 中层AnchorLayout背景色为半透明绿色
            self.rect_middle = Rectangle(size=self.size, pos=self.pos)
        self.bind(size=self.update_rect_middle, pos=self.update_rect_middle)

    def update_rect_middle(self, *args):
        self.rect_middle.size = self.size
        self.rect_middle.pos = self.pos


class InnerAnchorLayout(AnchorLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        with self.canvas.before:
            Color(0, 0, 1, 0.5)  # 内层AnchorLayout背景色为半透明蓝色
            self.rect_inner = Rectangle(size=self.size, pos=self.pos)
        self.bind(size=self.update_rect_inner, pos=self.update_rect_inner)

    def update_rect_inner(self, *args):
        self.rect_inner.size = self.size
        self.rect_inner.pos = self.pos


class NestedAnchorLayoutApp(App):
    def build(self):
        outer = OuterAnchorLayout()
        middle = MiddleAnchorLayout(size_hint=[.5, .5])
        inner = InnerAnchorLayout(size_hint=[.5, .5])
        outer.add_widget(middle)
        middle.add_widget(inner)
        return outer


if __name__ == '__main__':
    NestedAnchorLayoutApp().run()

重点还是要理解AnchorLayout的属性anchor_xanchor_y以及同子组件的size_hint的共同作用, 还有一点不可忽视, 就是AnchorLayout的父组件的空间的布局, 也会起到一定作用.

Sample 3: 占了谁的百分比?

这个例子更好的诠释了size_hint的意义.

  • 最底层的AnchorLayout(我们称之为底层Anchor)中包含了9个子AnchorLayout
  • 每个子AnchorLayout分别被放置在了AnchorLayoutanchor_xanchor_y构成的9个方位,可以参见其中的英文描述
  • 并且这9个子AnchorLayout都各自包含一个Button,按钮被设置了不同的尺寸占比,它们的横向占比都是.33333(1/3),纵向占比有.2.33333的区别
  • 可以看到每个按钮的尺寸占比都是基于底层Anchor的,理解这点很重要.

image.png

代码如下:

from kivy.app import App
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.button import Button


class MyAnchorLayout(AnchorLayout):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        topLeft = AnchorLayout(anchor_x='left', anchor_y='top')
        topCenter = AnchorLayout(anchor_x='center', anchor_y='top')
        topRight = AnchorLayout(anchor_x='right', anchor_y='top')

        self.add_widget(topLeft)
        self.add_widget(topCenter)
        self.add_widget(topRight)

        topLeft.add_widget(Button(text='topLeft', size_hint=[.33333, .2]))
        topCenter.add_widget(Button(text='topCenter', size_hint=[.33333, .33333]))
        topRight.add_widget(Button(text='topRight', size_hint=[.33333, .2]))

        centerLeft = AnchorLayout(anchor_x='left', anchor_y='center')
        centerCenter = AnchorLayout(anchor_x='center', anchor_y='center')
        centerRight = AnchorLayout(anchor_x='right', anchor_y='center')

        self.add_widget(centerLeft)
        self.add_widget(centerCenter)
        self.add_widget(centerRight)

        centerLeft.add_widget(Button(text='centerLeft', size_hint=[.33333, .33333]))
        centerCenter.add_widget(Button(text='centerCenter', size_hint=[.33333, .2]))
        centerRight.add_widget(Button(text='centerRight', size_hint=[.33333, .33333]))

        bottomLeft = AnchorLayout(anchor_x='left', anchor_y='bottom')
        bottomCenter = AnchorLayout(anchor_x='center', anchor_y='bottom')
        bottomRight = AnchorLayout(anchor_x='right', anchor_y='bottom')

        self.add_widget(bottomLeft)
        self.add_widget(bottomCenter)
        self.add_widget(bottomRight)

        bottomLeft.add_widget(Button(text='bottomLeft', size_hint=[.33333, .2]))
        bottomCenter.add_widget(Button(text='bottomCenter', size_hint=[.33333, .33333]))
        bottomRight.add_widget(Button(text='bottomRight', size_hint=[.33333, .2]))


class MyAnchorApp(App):

    def build(self):
        return MyAnchorLayout()


if __name__ == '__main__':
    MyAnchorApp().run()

AnchorLayout的官方文档

API说明

kivy.uix.anchorlayout.AnchorLayout

属性和方法

anchor_x

水平锚点, 可选值有left,center, right

类型: OptionProperty

默认值:center

anchor_y

垂直锚点, 可选值有top,center,bottom

类型: OptionProperty

默认值:center

do_layout(*largs)

当布局由触发器调用时,会调用此函数. 如果自定义PageLayout的子类, 不应该覆盖此方法, 而是应该重写_trigger_layout()

类型:function

padding

布局框与每个内部Widget的填充间距.

有3种使用方法,分别是:

  • [padding_left, padding_top, padding_right, padding_bottom]
  • [padding_horizontal, padding_vertical]
  • [padding]

类型:VariableListProperty

默认值:[0, 0, 0, 0]

2.FloatLayout

FloatLayout是一个浮动布局, 允许子Widget以相对位置、绝对位置, 相对大小、绝对大小任意放置.

FloatLayout的坐标定位, 就像我们初中学习的坐标一样, 是以左下角作为坐标的原点(0,0). x轴向右, y轴向上.

子Widget的pos_hint属性用于定位子Widget相对FloatLayout整体大小的相对位置. 依旧是以原点为起点, pos_hint={'x': .3, 'y': .4}表示子组件与原点的横向距离是FloatLayout宽度的30%, 距离原点的纵向距离是FloatLayout高度的40%.

子Widget的pos属性用于定位子组件的绝对位置, 如pos=[100, 200]表示距离原点的横向距离是100, 纵向距离是200.

子Widget的size_hint属性用于控制子Widget相对于FloatLayout的相对尺寸.

子Widget的size属性用于控制子Widget的绝对尺寸, 但是必须先使size_hint无效, 因为size_hint的优先级更高. 方法:size_hint=[None, None].

下面是一个典型的案例:

from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout


class MyFloadLayout(FloatLayout):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.add_widget(Button(text='this is a button 1.',
                               size_hint=[.2, .1],
                               pos_hint={'x': .3, 'y': .3}))
        
        self.add_widget(Button(text='this is a button 2.',
                               size_hint=[None, None],
                               size=[300, 100],
                               pos=[0, 0]))


class MyFloatLayoutApp(App):

    def build(self):
        return MyFloadLayout()


if __name__ == '__main__':
    MyFloatLayoutApp().run()

其效果如下:

image.png

FloatLayout的官方文档

API说明

kivy.uix.floatlayout.FloatLayout

do_layout(*largs)

当布局由触发器调用时,会调用此函数. 如果自定义PageLayout的子类, 不应该覆盖此方法, 而是应该重写_trigger_layout()

类型:function

3.RelativeLayout

RelativeLayout是相对布局, 它只能为子元素提供一种相对位置的解决方案.

它和FloatLayout比较相似, 不过它的子Widget总是能保持同它的相对位置.

当一个位置为(0, 0)的组件被添加到一个相对布局中时, 当相对布局的位置改变时, 子组件也会移动, 子组件的坐标保持为(0, 0), 因为它们始终是相对于父布局的.

然而在测试时, 笔者并没有找到二者的具体差异, 这里贴出2个例子, 大家可以尝试一下, 看看效果.

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.relativelayout import RelativeLayout


class MyRelativeLayout(RelativeLayout):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.add_widget(Button(text='1', size_hint=[.3, .3], pos_hint={'x': .1, 'y': .1}))
        self.add_widget(Button(text='2', size_hint=[.3, .3], pos_hint={'x': .15, 'y': .15}))
        # self.add_widget(Button(text='1', size_hint = (10, 10)))


class RelativeLayoutApp(App):

    def build(self):
        box = BoxLayout()
        box.add_widget(Button(text='empty button'))

        reLayout = MyRelativeLayout()

        box.add_widget(reLayout)


        box.add_widget(Button(text='empty button'))

        return box


if __name__ == '__main__':
    RelativeLayoutApp().run()

Relative的官方文档

API说明

kivy.uix.relativelayout.RelativeLayout

属性和方法

4.ScatterLayout

其布局效果同RelativeLayout完全相同.

ScatterLayoutRelativeLayout多的功能是, 其内部是基于Scatter实现的, 那么就可以通过此Layout来对子组件进行平移、旋转、缩放. 具体可以参考:Scatter的官方文档

如果想要调用其添加的子Widget, 需要使用self.content.children.

演示例子:

from kivy.app import App
from kivy.graphics import Color, Rectangle
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import AsyncImage
from kivy.uix.scatterlayout import ScatterLayout


class MyBoxLayout(GridLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.cols = 3
        self.rows = 3

        with self.canvas:
            Color(1, 1, 1, 1)
            self.rect = Rectangle(pos=self.pos, size=self.size)
            self.bind(size=self.update_rect, pos=self.update_rect)

        for i in range(9):
            scatter = MyScatterLayout()
            self.add_widget(scatter)

    def update_rect(self, *args):
        self.rect.size = self.size
        self.rect.pos = self.pos


class MyScatterLayout(ScatterLayout):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.add_widget(AsyncImage(
            source='http://gips3.baidu.com/it/u=3886271102,3123389489&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960'))


class ScatterLayoutApp(App):
    def build(self):
        return MyBoxLayout()


if __name__ == '__main__':
    ScatterLayoutApp().run()

ScatterLayout的官方文档

API说明

kivy.uix.scatterlayout.ScatterLayout

属性和方法