PR
Kivy Tutorials

How to Use RelativeLayout in Kivy KV Language

This article can be read in about 27 minutes.

This section presents some code examples using Kivy’s RelativeLayout. RelativeLayout is a layout that allows you to place widgets at arbitrary positions. It also explains how to calculate it based on the position and size of its parent and other widgets.

What is RelativeLayout?

RelativeLayout is a Kivy layout in which widgets are positioned relative to their parents. Widgets can be placed in any position using the pos_hint and pos properties, which set the widget’s position.

Code Example

relativelayout1.py

from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout

class RootWidget(RelativeLayout):
    pass

class RelativeLayout1(App):
    def build(self):
        return RootWidget()

if __name__ == "__main__":
    RelativeLayout1().run()

relativelayout1.kv

<RelativeLayout>:
    Button:
        text: "Button 1"
        size_hint: 1, None
        height: 100
        pos_hint: {'x': 0.1, 'y': 0.7}

    Button:
        text: "Button 2"
        size_hint: None, None
        size: 100, 40
        pos_hint: {'center_x': 0.5, 'center_y': 0.5}

    Button:
        text: "Button 3"
        size_hint: 0.2, 0.2
        pos_hint: {'right': 0.8, 'top': 0.3}

After executing this code, the widget will appear as shown in the figure below.

Example of Kivy's RelativeLayout application.

To set the widget’s position, use the pos_hint or pos property, where pos_hint is a percentage and pos is an absolute coordinate. These are explained in more detail in the following article.

The size of the widget is determined by the size_hint property. These details are explained in the following article.

Code Example to Obtain Widget Position

Here is an example of code that uses RelativeLayout to get the position of a widget. It also explains how to calculate the position and size based on the position and size of the parent and other widgets.

relativelayout2.py

from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.widget import Widget
from random import randint

class ColoredArea(Widget):
    pass

class RootWidget(RelativeLayout):
    # Place the initial position of Labels 1 through 3 off the screen.
    def on_kv_post(self, base_widget):
        self.ids.label1.pos = (2000,2000)
        self.ids.label2.pos = (2000,2000)
        self.ids.label3.pos = (2000,2000)
        return super().on_kv_post(base_widget)  

    def move_labels(self):
        for i in range(1, 4):
            label = self.ids[f'label{i}']
            new_x = randint(0, self.ids.colored_area.width - label.width)
            new_y = randint(0, self.ids.colored_area.height - label.height)
            label.pos = (new_x + self.ids.colored_area.x, new_y + self.ids.colored_area.y)
            self.ids[f'label{i}_pos'].text = f'Label {i} pos: ({new_x}, {new_y})'

        for child in self.children:
            if not hasattr(child, 'name'): # Exclude colored_area because it has no text.
                print(f"Root1-Child {child.text} size: {child.size}, pos: {child.pos}")

        print(f"Root2-Parent size: {self.size}, pos: {self.pos}")
        print(f"Root2-Child size: {self.ids.button.size}, pos: {self.ids.button.pos}")

class RelativeLayout2(App):
    def build(self):
        return RootWidget()

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

relativelayout2.kv

<RootWidget>:
    canvas.before:
        Color:
            rgba: 0.5, 0.5, 0.8, 0.5
        Rectangle:
            size: self.size
            pos: self.pos

    ColoredArea:
        id: colored_area
        name: 'name'

    Button:
        id: button
        text: "Move Labels"
        size_hint: None, None
        size: 150, 50
        pos_hint: {'center_x': 0.5, 'y': 0.9}
        on_press: 
            root.move_labels()
            # Obtains the size and position of its own widget.
            print(f"Self(button) pos-x: {self.x} pos-y: {self.y}")
            print(f"Self(button) pos-center_x: {self.center_x} pos-center_y: {self.center_y}")
            print(f"Self(button) width: {self.width} height: {self.height}")
            print(f"Self(button) size: {self.size}")
            print(f"Self(button) pos: {self.pos}")

            # Get size and position of other widgets by id.
            print(f"label1_pos pos-x: {label1_pos.x} pos-y: {label1_pos.y}")
            print(f"label1_pos pos-center_x: {label1_pos.center_x} pos-center_y: {label1_pos.center_y}")
            print(f"label1_pos width: {label1_pos.width} height: {label1_pos.height}")
            print(f"label1_pos size: {label1_pos.size}")
            print(f"label1_pos pos: {label1_pos.pos}")

            # Get the size and position of the parent.
            print(f"Parent pos-x: {self.parent.x} pos-y: {self.parent.y}")
            print(f"Parent pos-center_x: {self.parent.center_x} pos-center_y: {self.parent.center_y}")
            print(f"Parent.width: {self.parent.width} Parent.height: {self.parent.height}")
            print(f"Parent.size: {self.parent.size}")
            print(f"Parent.pos: {self.parent.pos}")

    Label:
        id: label1
        text: "1"
        size_hint: None, None
        size: 50, 50
        font_size: 50
    Label:
        id: label2
        text: "2"
        size_hint: None, None
        size: 50, 50
        font_size: 50
    Label:
        id: label3
        text: "3"
        size_hint: None, None
        size: 50, 50
        font_size: 50

    Label:
        id: label1_pos
        text: "Label 1 pos: "
        size_hint: None, None
        size: 200, 50
        pos_hint: {'center_x': 0.5}
        y: int(button.y - 100)
    Label:
        id: label2_pos
        text: "Label 2 pos: "
        size_hint: None, None
        size: label1_pos.size
        pos_hint: {'center_x': 0.5}
        y: int(button.y - 140)
    Label:
        id: label3_pos
        text: "Label 3 pos: "
        size_hint: None, None
        size: label1_pos.size
        pos_hint: {'center_x': 0.5}
        y: int(button.y - 180)

<ColoredArea>:
    canvas.before:
        Color:
            rgba: 0.5, 0.5, 0.8, 0.5
        Rectangle:
            size: self.size
            pos: self.pos
        
    size_hint: None, None
    size: (int(self.parent.width * 0.9), int(self.parent.height * 0.6))
    pos_hint: {'center_x': 0.5, 'center_y': 0.4}

Code Explanation

In this code, the label randomly changes position in the light purple area when the button is pressed, and the position is retrieved and displayed. It also retrieves the position and size of the parent and other widgets and displays them in the console.

Kivy RelativeLayout

Code Structure

This code has the following tree structure.

<RootWidget>
+--- RelativeLayout
+---+--- ColoredArea # colored_area is the area where the label can be moved.
+---+--- Button      # button
+---+--- Label       # label1    Label to be moved.
+---+--- Label       # label2    Label to be moved.
+---+--- Label       # label3    Label to be moved.
+---+--- Label       # label1_pos   Label to indicate location.
+---+--- Label       # label2_pos   Label to indicate location.
+---+--- Label       # label3_pos   Label to indicate location.

Moving Area of Labels

The ColoredArea subclass defines the label movement area. Labels move within this colored area.

class ColoredArea(Widget):
    pass
<ColoredArea>:
    canvas.before:
        Color:
            rgba: 0.5, 0.5, 0.8, 0.5
        Rectangle:
            size: self.size
            pos: self.pos
        
    size_hint: None, None
    size: (int(self.parent.width * 0.9), int(self.parent.height * 0.6))
    pos_hint: {'center_x': 0.5, 'center_y': 0.4}
<ColoredArea>:
    canvas.before:
        Color:
            rgba: 0.5, 0.5, 0.8, 0.5
        Rectangle:
            size: self.size
            pos: self.pos

A rectangle is drawn in canvas. The drawing method is the same as the background color of the layout. See the following article for details.

The background color of the layout and this colored area have the same RGBA value, but different colors are displayed. This happens because the alpha value (transparency) does not work for the layout background color.

size_hint: None, None
size: (int(self.parent.width * 0.9), int(self.parent.height * 0.6))

The above sets the size of the colored area. The size of the parent RelativeLayout is obtained and used as the basis. The size is specified as 90% width and 60% height. it is returned as a float type, so it is converted to an int type. the kv code will generate an error if this is not done.

pos_hint: {'center_x': 0.5, 'center_y': 0.4}

The colored area is placed at 50% on the x-axis and 40% on the y-axis.

Places Labels for Movement Outside the Screen Area

No position is set for the label for movement. If not set, the labels are overlapped at the default position of [0 , 0] (lower left). This does not look good, so the initial position of the label is placed off-screen.

class RootWidget(RelativeLayout):
    # Place the initial position of Labels 1 through 3 off the screen.
    def on_kv_post(self, base_widget):
        self.ids.label1.pos = (2000,2000)
        self.ids.label2.pos = (2000,2000)
        self.ids.label3.pos = (2000,2000)
        return super().on_kv_post(base_widget)  
   Label:
        id: label1
        text: "1"
        size_hint: None, None
        size: 50, 50
        font_size: 50

The reason why the label position is not set here is that if the position is set, the label will only move within the set range. The expected behavior is to move within the colored area.

The on_kv_post() is a callback function that is called when the kv file has finished loading. It can be placed in the constructor, but this is a more reliable way to obtain the ids.

Move Labels to Random Positions

In move_labels() of the RootWidget subclass, the process of randomly changing the position of labels is described. The id of the labels to be repositioned are label1, label2, and label3. The following three processes are performed here.

  1. Obtains the numeric value of the id of the labels.
  2. Obtains the start and end values of the random number.
  3. Set the position of the parent widget.
  4. Display the position in the colored area in the text of Label.
    def move_labels(self):
        for i in range(1, 4):
            label = self.ids[f'label{i}']
            new_x = randint(0, self.ids.colored_area.width - label.width)
            new_y = randint(0, self.ids.colored_area.height - label.height)
            label.pos = (new_x + self.ids.colored_area.x, new_y + self.ids.colored_area.y)
            self.ids[f'label{i}_pos'].text = f'Label {i} pos: ({new_x}, {new_y})'
Obtain the Numeric Value Used for the id of the Label
for i in range(1, 4):
    label = self.ids[f'label{i}']

The range() is Python built-in functions that creates a list of consecutive integers from the start number to the end number.

range(start, stop [, step ])

For start, specify an integer to start with; for stop, specify an integer to end with; and for step, advance by the number specified in step. For example, range(0,10,2) would create a list of [0,2,4,6,8]; a negative value for step would reverse the order to [8,6,4,2,0].

The number specified for stop is not counted. For example, in the case of range(1,4), a list of [1,2,3] is created.

In this example, since the id of the labels to be moved are label1, label2, and label3, range() is used to describe the numerical values of these labels.

Obtaining the Start and End Values of Random Numbers

The new_x calculates the width of the colored area and specifies the range of the x-axis to randint(). Similarly, new_y calculates the height of the colored area and specifies the range of the y-axis to randint().

new_x = randint(0, self.ids.colored_area.width - label.width)
new_y = randint(0, self.ids.colored_area.height - label.height)

The randint() is Python built-in functions that returns a randomly generated integer of type int.

randint(start, stop )

The start is the starting integer for which the random number is generated, and stop is the ending integer.

In this example self.ids.colored_area.width gets the width of the colored area. label.width gets the width of self.ids.label{i}. The result of this acquisition is [1080,50], and 1080 – 50 = 1030 is specified as the parameter stop of randint(). In other words, a random number is generated between 0 and 1030, and the result is assigned to the variable new_x. new_y obtains the height in the same way.

The reason for subtracting the width of the label from the width of the colored area is to prevent the label from exceeding the width of the colored area. Without this calculation, if a label is placed at position 1080 at the end of the width, the label will be displayed outside the colored area.

Kivy RelativeLayout. Example of labels sticking out of the screen.
Setting the Position of the Parent Widget
label.pos = (new_x self.ids.colored_area.x, new_y self.ids.colored_area.y)

Although new_x and new_y are calculated for the range of the colored area, it is the position of the parent that is actually positioned using pos. In other words, pos cannot specify a position within the color range. Therefore, it is necessary to add the difference between the position of the parent and the color region.

In self.ids.colored_area.x and self.ids.colored_area.y, the origin (lower left corner) of the x-axis and y-axis of the colored area from the parent is acquired. The value obtained here is [60,90], in other words, the margin between the parent and the colored area. Adding this to new_x and new_y, the position of the parent can be calculated.

RelativeLayout in Kivy. difference between parent and child regions.
Label Text With Position in Colored Area

The x-axis and y-axis positions within the color region are displayed in the text of Label.

self.ids[f'label{i}_pos'].text = f'Label {i} pos: ({new_x}, {new_y})'

Example of Getting the Position and Size of a Widget

An example of how to write to get the position and size of a widget is shown in the console.

No id Name Can Be Obtained

This time, I had a little trouble because I could not get the id name.

for child in self.children:
    if not hasattr(child, 'name'): # Exclude colored_area because it has no text.
        print(f"Root1-Child {child.text} size: {child.size}, pos: {child.pos}")

The above code uses children to get a list of children. The variable child is assigned the list of children objects in the RootWidget tree.

The hasattr() is Python built-in functions that checks for the existence of object attributes. It returns True if the specified attribute exists, or False if it does not.

hasattr(object, name )

object is an instance and name is an attribute name.

This is an error because there is no text defined in the ColoredArea. To avoid the error, the colored_area must be excluded. Since the id name cannot be obtained, the name property is defined instead. name property is often used to determine the screen name for screen transitions, but it is useful when you want to make some kind of determination.

   ColoredArea:
        id: colored_area
        name: 'name'

Place Widgets Based on Other Widget

Aligning widgets in a RelativeLayout can be a bit tedious. In such cases, it is sometimes better to position widgets based on the positions of other widget.

    Label:
        id: label1_pos
        text: "Label 1 pos: "
        pos_hint: {'center_x': 0.5}
        y: int(button.y - 100)

In the above, the Label position is specified based on the Button position.

Make It the Same Size as Other widgets

If you want to make the widget the same size as other widgets, you can specify it by specifying the widget’s id_name.size as shown below.

    Label:
        id: label2_pos
        text: "Label 2 pos: "
        size_hint: None, None
        size: label1_pos.size

Comment

Copied title and URL