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}
このコードを実行すると下図のように表示されます。

ウィジェットの位置を設定するには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}
コード解説
このコードでは、ボタンを押すと薄紫色の領域でラベルがランダムに位置を変え、その位置を取得して表示します。また、親や他のウィジェットの位置やサイズを取得しコンソールに表示されます。
コードの構成
このコードは下記のツリー構成になっています。
<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()ではランダムにラベルの位置を変更する処理を記述しています。位置を変更するラベルのidはlabel1、label2、label3です。ここでは下記の3つの処理をしています。
- Labelのidの数値を取得。
- 乱数の開始数値と終了数値を取得。
- 親ウィジェットの位置を設定。
- 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]のリストが作成されます。
この例では、移動用ラベルのidがlabel1、label2、label3のなっているので、その数値を記述するために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(start, stop)
startには乱数を生成する開始の整数を指定し、stopには終了の整数を指定します。
この例のself.ids.colored_area.widthはカラー領域の幅を取得しています。label.widthはself.ids.label{i}の幅を取得しています。これの取得結果は、[1080,50]になり、1080-50=1030がrandint()のパラメーターstopに指定されます。つまり、0~1030の間で乱数を生成し、その結果が変数new_xに代入されます。new_yも同様に高さを取得しています。
カラー領域の幅からラベルの幅を引いている理由は、カラー領域の幅を超えてラベルが表示されるのを防ぐためにしています。この計算をしない場合、幅の終端の1080位置にラベルが置かれた場合そのラベルはカラー領域の外側に表示されます。

親ウィジェットの位置を設定
label.pos = (new_x + self.ids.colored_area.x, new_y + self.ids.colored_area.y)
new_xとnew_yはカラー領域の範囲が計算されていますが、posを使用して実際に配置するのは親の位置になります。つまり、posではカラー領域の範囲で位置を指定できません。そのため、親とカラー領域の位置の差分を足す必要があります。
self.ids.colored_area.xとself.ids.colored_area.yには親から見たカラー領域のx軸とy軸の原点(左下角)を取得しています。ここで取得された値は[60,90]になり、言い換えれば親とカラー領域の間の余白になります。これをnew_xとnew_yに足すと親の位置が計算できます。

Labelのtextにカラー領域内の位置を表示する
Labelのtextにカラー領域内の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