PR
Python+Kivy入門

RelativeLayoutの使い方とウィジェット配置の計算方法: Kivyのレイアウト3【kv言語】

この記事は約20分で読めます。

KivyのRelativeLayoutを使ったいくつかのサンプルコード紹介します。RelativeLayoutは任意の位置にウィジェットを配置するレイアウトになります。また、親や他のウィジェットの位置やサイズを基準に計算する方法を解説します。

RelativeLayoutとは

RelativeLayoutは親に対して相対的にウィジェットを配置するKivyのレイアウトです。ウィジェットの位置を設定するpos_hintプロパティやposプロパティを使用して任意の位置にウィジェットを配置することができます。

サンプルコード

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}

このコードを実行すると下図のように表示されます。

KivyのRelativeLayoutのサンプルアプリ

ウィジェットの位置を設定するにはpos_hintプロパティまたはposプロパティを使います。pos_hintは割合で指定し、posは絶対座標で指定します。これらの詳細は下記の記事で説明しています。

ウィジェットのサイズはsize_hintプロパティを使います。詳細は下記の記事で説明しています。

ウィジェットの位置を取得するサンプルコード

RelativeLayoutを使用してウィジェットの位置を取得するサンプルコードを紹介します。また、親や他のウィジェットの位置やサイズを基準に計算する方法を解説します。

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):
    # Label1から3の初期位置を画面外に置いてる。
    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'): # colored_areaは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()
            # 自身のウィジェットのサイズと位置を取得する。
            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}")

            # 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}")

            # 親のサイズと位置を取得する。
            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}

コード解説

このコードでは、ボタンを押すと薄紫色の領域でラベルがランダムに位置を変え、その位置を取得して表示します。また、親や他のウィジェットの位置やサイズを取得しコンソールに表示されます。

Kivy RelativeLayoutサンプルアプリ

コードの構成

このコードは下記のツリー構成になっています。

<RootWidget>
+--- RelativeLayout
+---+--- ColoredArea # colored_area。ラベルの移動領域。
+---+--- Button      # button
+---+--- Label       # label1 移動用ラベル
+---+--- Label       # label2 移動用ラベル
+---+--- Label       # label3 移動用ラベル
+---+--- Label       # label1_pos 位置表示用ラベル
+---+--- Label       # label2_pos 位置表示用ラベル
+---+--- Label       # label3_pos 位置表示用ラベル

ラベルの移動領域

ColoredAreaサブクラスはラベルの移動領域を定義しています。ラベルはこのカラー領域内で移動します。

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

canvasで四角形を描画しています。描画方法はレイアウトの背景色と同じです。詳細は下記の記事をご覧ください。

レイアウトの背景色とこのカラー領域は同じRGBAを指定していますが、違う色が表示されてます。レイアウトの背景色ではアルファ値(透明度)が効かないためこのような現象になります。

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

上記では、カラー領域のサイズを設定しています。親であるRelativeLayoutのサイズを取得してそれを基準にしています。幅が90%、高さが60%のサイズに指定しています。float型で返ってくるので、int型に変換しています。kvコードではこれをしないとエラーが発生します。

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

x軸に50%、y軸に40%の位置にカラー領域を配置しています。

画面領域外へ移動用ラベルを配置する

移動用ラベルには位置を設定していません。設定しない場合、デフォルトの[0 , 0](左下)の位置にラベルが重なって表示されます。これでは見た目が良くないので、ラベルの初期位置を画面外に配置しています。

class RootWidget(RelativeLayout):
    # Label1から3の初期位置を画面外に置いてる。
    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

ここでラベルの位置を設定しない理由は、位置を設定するとその設定範囲内でしかラベルが移動されないためです。期待する動作としては、カラー領域内での移動になります。

on_kv_post()はkvファイルの読み込みが終わると呼ばれるコールバック関数です。コンストラクタ内で書いても良いですが、こちらの方が確実にidsを取得できます。

ランダムな位置にラベルを移動する

RootWidgetサブクラスのmove_labels()ではランダムにラベルの位置を変更する処理を記述しています。位置を変更するラベルのidlabel1label2label3です。ここでは下記の3つの処理をしています。

  1. Labelidの数値を取得。
  2. 乱数の開始数値と終了数値を取得。
  3. 親ウィジェットの位置を設定。
  4. 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})'
Labelのidに使われる数値を取得
for i in range(1, 4):
    label = self.ids[f'label{i}']

range()はpythonのビルトイン関数で、開始の数値から終了の数値までの連続する整数のリストを作成します。

range(start, stop[, step])

startには、開始する整数を指定し、stopには終了の整数を指定し、stepにはstepで指定した数値で進みます。例えばrange(0,10,2)にした場合、[0,2,4,6,8]のリストが作成されます。stepの数値をマイナスに指定した場合は逆順になり、[8,6,4,2,0]になります。

stopに指定した数値はカウントされません。例えばrange(1,4)にした場合、[1,2,3]のリストが作成されます。

この例では、移動用ラベルのidlabel1label2label3のなっているので、その数値を記述するためにrange()を使用しています。

乱数の開始数値と終了数値を取得

new_xはカラー領域の幅を計算し、x軸の範囲をrandint()に指定しています。同様にnew_yはカラー領域の高さを計算し、y軸の範囲をrandint()に指定しています。

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

randint()はPythonのビルトイン関数でランダムに生成されたint型の整数を返します。

randint(startstop)

startには乱数を生成する開始の整数を指定し、stopには終了の整数を指定します。

この例のself.ids.colored_area.widthはカラー領域の幅を取得しています。label.widthself.ids.label{i}の幅を取得しています。これの取得結果は、[1080,50]になり、1080-50=1030がrandint()のパラメーターstopに指定されます。つまり、0~1030の間で乱数を生成し、その結果が変数new_xに代入されます。new_yも同様に高さを取得しています。

カラー領域の幅からラベルの幅を引いている理由は、カラー領域の幅を超えてラベルが表示されるのを防ぐためにしています。この計算をしない場合、幅の終端の1080位置にラベルが置かれた場合そのラベルはカラー領域の外側に表示されます。

KivyのRelativeLayoutサンプル。ラベルが画面からはみ出す例
親ウィジェットの位置を設定
label.pos = (new_x + self.ids.colored_area.x, new_y + self.ids.colored_area.y)

new_xnew_yはカラー領域の範囲が計算されていますが、posを使用して実際に配置するのは親の位置になります。つまり、posではカラー領域の範囲で位置を指定できません。そのため、親とカラー領域の位置の差分を足す必要があります。

self.ids.colored_area.xself.ids.colored_area.yには親から見たカラー領域のx軸とy軸の原点(左下角)を取得しています。ここで取得された値は[60,90]になり、言い換えれば親とカラー領域の間の余白になります。これをnew_xnew_yに足すと親の位置が計算できます。

KivyのRelativeLayoutサンプル。親領域と子領域の差分
Labelのtextにカラー領域内の位置を表示する

Labeltextにカラー領域内のx軸とy軸の位置を表示しています。

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

ウィジェットの位置とサイズを取得する例

ウィジェットの位置とサイズを取得する書き方の例をコンソールに表示しています。

id名は取得できない

今回、id名が取得できないのでちょっと困りました。

for child in self.children:
    if not hasattr(child, 'name'): # colored_areaはtextが無いので除外
        print(f"Root1-Child {child.text} size: {child.size}, pos: {child.pos}")

上記のコードではchildrenを使用して子ウィジェットのリストを取得しています。変数childにはRootWidgetツリーの子ウィジェットのオブジェクトリストが代入されます。

hasattr()はPythoのビルトイン関数でオブジェクトの属性が存在するかを調べます。指定した属性が存在する場合はTrueを返し、存在しない場合はFalseを返します。

hasattr(object, name)

objectにはインスタンス、nameには属性名を指定します。

ColoredAreaにはtextが定義されていないので、エラーになります。エラーを発生させないためには、colored_areaを除外する必要があります。id名は取得できないので、代わりにnameプロパティを定義します。nameプロパティは画面遷移するときのスクリーン名を判定する場合に使用されることが多いですが、何らかの判定をしたい時には便利です。

    ColoredArea:
        id: colored_area
        name: 'name'

他のウィジェットを基準にしてウィジェットの位置を決める

RelativeLayoutでウィジェットを整列したい時は少し面倒です。そんな時は他のウィジェットの位置を基準に位置を決めると良い場合もあります。

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

上記ではButtonの位置を基準にLabelの位置を指定しています。

他のウィジェットと同じサイズにする

他のウィジェットと同じサイズにしたい場合は、下記のように他のウィジェットのid名.sizeで指定できます。

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

Comment

タイトルとURLをコピーしました