警告
以下の記事はFletのバージョン0.8.x
で作成したものです。本注意を記述した現在の最新バージョンは0.22.1
(2024/05/21)であり、Class記法においてUserControl
は非推奨、加えてClassの定義で必ずbuild()
を実装しなければならなかった点が変更となっています。以上の点から本記事とは別に最新のバージョンでの記法に修正した内容を投稿予定です。
今回は次の記事:
をクラス型で定義しなおすといった内容です。
Pythonのクラス型に関しては前回の記事:
を参考までに。
記事は前回の流れを踏襲して、
- Main (main.py)
- Nav Bar (header.py)
- Side Bar (sidebar.py)
の順に紹介します。
1. Main (main.py)
アプリケーションを起動するメイン(Main)部分はmain.py
とします。このモジュールでは
-
header.py
のインポート -
sidebar.py
のインポート - ヘッダー・サイドバー・メインの3構成でレイアウトを作成
- アプリケーションの起動
といった処理を行います。以前紹介した記事の内容と変わりません:
import flet
from header import AppHeader
from sidebar import Sidebar
from flet import (
Page,
Row,
Text,
)
def main(page: Page):
page.title = "Example Layout"
page.padding = 10
my_text = Text("Main Body")
# インスタンス作成時にpage.appbarにナビゲーションバーが設定される
AppHeader(page)
sidebar = Sidebar()
layout = Row(
controls=[sidebar, my_text],
tight=True, # 水平方向の隙間をどうするか。デフォルトはFalseですべての要素に余白を与える。
expand=True, # 利用可能なスペースを埋めるようにするか。
vertical_alignment="start", # 画面上部から表示。ほかに"center"や"end"などの値がある。
)
page.add(layout)
page.update()
flet.app(target=main)
メイン部分は簡単なテキストを表示させるだけになっています。
2. Nav Bar (header.py)
FletコンポーネントのClassを定義する際に、UserControl
をClass定義の引数に渡します。(こちらが基底クラスとなります。)
Classの内部ではText
やElevatedButton
などの要素を定義します。ここで、UserControl
についていくつか注意すべき点について次の通り:
-
UserControl
は再利用可能な独立したコンポーネントのビルドを行うためのクラスであり、ビルドを行うために必ずbuild()
メソッドを実装する必要がある。 -
UserControl
は独立したコンポーネントであるため、Pageに対する変更に対しても変更を反映するために、self.update()
を呼び出す。(独立したコンポーネントは自身を更新できるように実装すべし) -
UserControl
を基底クラスとした派生クラスのコンストラクターは必ず、super().__init__()
を呼び出す。
つまり、関数形式からクラス形式に転じる際の変更点は
-
def
→class
- コンストラクター
__init__()
の実装 - ビルド
build()
の実装 - クラスオブジェクトのメンバに各コンポーネントを割り当てる
といったことになります。4つ目が個人的には一番面倒なところにはなりますね。(self
を何度も使用するので)
以上の点を考慮して実装したコードが次の通り:
from flet import (
AppBar,
colors,
Container,
ElevatedButton,
Icon,
IconButton,
icons,
margin,
Page,
PopupMenuButton,
PopupMenuItem,
Row,
Text,
UserControl,
)
class AppHeader(UserControl):
def __init__(self, page: Page):
super().__init__()
self.page = page
self.toggle_dark_light_icon = IconButton(
icon="light_mode",
selected_icon = "dark_mode",
tooltip=f"switch light and dark mode",
on_click=self.toggle_icon,
)
something_btn = ElevatedButton(text="Button")
self.appbar_items = [
PopupMenuItem(text="Login"),
PopupMenuItem(), # divider
PopupMenuItem(text="Settings"),
]
# Appのpage.appbarフィールドの設定
self.page.appbar = AppBar(
leading=Icon(icons.TRIP_ORIGIN_ROUNDED),
leading_width=100,
title=Text(value="Example", size=32, text_align="center"),
center_title=False,
toolbar_height=75,
bgcolor=colors.SURFACE_VARIANT,
actions=[
Container(
content=Row(
[
self.toggle_dark_light_icon,
something_btn,
PopupMenuButton(
items=self.appbar_items
),
],
alignment="spaceBetween",
),
margin=margin.only(left=50, right=25)
)
],
)
def build(self):
return self.page.appbar
def toggle_icon(self, e):
self.page.theme_mode = "light" if self.page.theme_mode == "dark" else "dark"
self.page.update()
self.toggle_dark_light_icon.selected = not self.toggle_dark_light_icon.selected
self.page.update()
※コンストラクター__init__(self, page)
について、クラスを呼び出す際にはAppHeader(page)
とします。第1引数のself
はインスタンスオブジェクトが渡されているのですが、こちらの事情について曖昧の場合はPythonのクラスについて復習されることをお勧めします。参考までにこちらを参照ください。
ヘッダー部分の内容については前回の関数形式の場合との変更点は特にないです。
3. Side Bar (sidebar.py)
サイドバーの実装についてもヘッダーと同様に、変更していきます。変更後は次の通り:
from flet import (
alignment,
border_radius,
colors,
Container,
CrossAxisAlignment,
FloatingActionButton,
Icon,
IconButton,
icons,
NavigationRail,
NavigationRailDestination,
NavigationRailLabelType,
Row,
Text,
UserControl,
)
class Sidebar(UserControl):
def __init__(self):
super().__init__()
self.nav_rail_visible = True
self.nav_rail_items = [
NavigationRailDestination(
icon_content=Icon(icons.BOOKMARK_BORDER),
selected_icon_content=Icon(icons.BOOKMARK),
label="Second"
),
NavigationRailDestination(
icon=icons.FAVORITE_BORDER,
selected_icon=icons.FAVORITE,
label="First"
),
NavigationRailDestination(
icon=icons.SETTINGS_OUTLINED,
selected_icon_content=Icon(icons.SETTINGS),
label_content=Text("Settings"),
),
]
self.nav_rail = NavigationRail(
height= 300,
selected_index=None,
label_type=NavigationRailLabelType.ALL,
min_width=100,
min_extended_width=400,
leading=FloatingActionButton(icon=icons.CREATE, text="ADD"),
group_alignment=-0.9,
destinations=self.nav_rail_items,
on_change=lambda e: print("Selected destination: ", e.control.selected_index),
)
self.toggle_nav_rail_button = IconButton(
icon=icons.ARROW_CIRCLE_LEFT,
icon_color=colors.BLUE_GREY_400,
selected=False,
selected_icon=icons.ARROW_CIRCLE_RIGHT,
on_click=self.toggle_nav_rail,
tooltip="Collapse Nav Bar",
)
def build(self):
self.view = Container(
content=Row(
[
self.nav_rail,
Container( # vertical divider
bgcolor=colors.BLACK26,
border_radius=border_radius.all(30),
height=220,
alignment=alignment.center_right,
width=2
),
self.toggle_nav_rail_button,
],
expand=True,
vertical_alignment=CrossAxisAlignment.START,
visible=self.nav_rail_visible,
)
)
return self.view
def toggle_nav_rail(self, e):
self.nav_rail.visible = not self.nav_rail.visible
self.toggle_nav_rail_button.selected = not self.toggle_nav_rail_button.selected
self.toggle_nav_rail_button.tooltip = "Open Side Bar" if self.toggle_nav_rail_button.selected else "Collapse Side Bar"
self.view.update()
self.page.update()
関数形式からクラス形式にするにあたって、特に処理の変更なし。
4. Main(main.py)の実行
1. Main (main.py)
から3. Side Bar (sidebar.py)
までの工程でコンポーネントをそれぞれ作成することができたので、main.py
を実行すれば前回と同じように次の表示がされるはずです:
上記画像は、前回の記事の変更点として
- Mainの箇所が
Main Body
に変更 - 右上の「太陽のアイコン」をクリック後
です。
後は、
- 各種ボタンに対して適切な名前の追加・アイコンの変更、イベントの追加を行う
-
Main Body
部分にいい感じの実装をする
といったことをすればよさそうです。今回はレイアウトが作れればOKなのでここいらで完了。
5. 少しお遊び
Main Body
に追加する要素について、簡単なサンプルをこちらで記述。イベントの取得などを試してみるといった観点で作成しました。まずは、適当なモジュールでお試し:
def main(page: Page):
hist = []
def add_element(e):
value = e.control.data["key"]
hist.append(Text(value=f"ELEMENNT ADDED {value}"))
add_elements.controls.clear()
add_elements.controls.append(hist[-1])
page.update()
def del_element(e):
if len(hist) != 0:
hist.pop()
add_elements.controls.clear()
page.update()
if len(hist) >= 1:
add_elements.controls.append(hist[-1])
page.update()
btn_1 = ElevatedButton(text="Button1", on_click=add_element, data={"key": "value1"})
btn_2 = ElevatedButton(text="Button2", on_click=add_element, data={"key": "value2"})
btn_3 = ElevatedButton(text="Button3", on_click=add_element, data={"key": "value3"})
btn_del = ElevatedButton(text="Delete", on_click=del_element)
add_elements = Column()
layout = Column(
controls=[
Row(
controls=[btn_1, btn_2, btn_3],
alignment="center",
),
btn_del,
add_elements
]
)
page.add(layout)
page.update()
flet.app(target=main)
こちらのコードは
- 追加ボタンを3つ(サイドバーのボタンで何かできるか?)
- 追加ボタンには
text
、on_click
そしてdata
属性を設定 - 削除ボタンを1つ
- 削除ボタンには
text
、on_click
を設定 - 空の
Column
要素(ボタン押下で要素を追加する用) - ボタンクリックイベント用の関数
add_element
- 画面要素を格納するリスト
hist
(履歴みたいに使用できるか?。スコープはmain
内部)
からなっています。
試したこと
-
on_click
属性を使用して要素を追加 -
イベントが発火されたときに渡される引数
e
の中身を調べる:-
e
: オブジェクト情報(人が読めるのは同じかそうでないか程度) -
e.control
: クラス名とその引数に関する情報
例:elevatedbutton {'text': 'Button1'}
-
e.control.text
:text
属性の値
例:Button1
-
e.control.data
:data
属性の値
例:{'key': 'value1'}
-
-
hist
に格納されたText
の要素の末尾を画面表示、それ以外は履歴として残す。残された履歴はDelete
ボタンで末尾を削除されると末尾の前の要素が表示される。(戻るボタンに近い。)
例:
【手順】以下の順番でボタンをクリックする。Button1
→Button2
→Button3
→Delete
→Delete
→Delete
【確認】Delete
ボタンの下の表示は次のようになるはず。
ELEMENNT ADDED value1
→ELEMENNT ADDED value2
→ELEMENNT ADDED value3
→ELEMENNT ADDED value2
→ELEMENNT ADDED value1
→非表示(hist
が空)
上記のようなものを使用して、サイドバーとかのボタンで画面遷移を行うようにしていきたい。
6. まとめ
今回は簡単にだが、前回の記事を関数形式からクラス形式の記述に変更した。この変更により、特定のコンポーネントを作成し、それを流用して使用するといったことが可能となる。(今回はレイアウトでしたが、カードといった要素があるのでその1要素をクラスで定義しておくと便利)
レイアウトの作成ができたので、次回以降はほかのコンポーネントの定義または画面遷移の実装などを実施していきたい。