今回の記事は、Flet
のチュートリアル後にWEBレイアウト的なものを試しに作成してみたので、その備忘録です。時間を置くと何を作っていたかも忘れるのでなるはやで作成していきます。
レイアウトとしては、次のような感じ:
ナビゲーションバー(Nav Bar)があり、サイドバー(Side Bar)+サイドバーを閉じる・開くボタン(btn)そして本文(Main)といったものです。
Mainについてはどのようなものを実装するか検討中のため、簡単なテキストの出力にとどめました。
今回の作成物のスクショがこちら(ボタン名などはかなり適当なのでご注意):
今回の記事で参照した記事は
で主にFletドキュメントのチュートリアルから引っ張ってきています。
1. Main (main.py)
まず、Fletを使用する際のスタートラインは
import flet
from flet import Page
def main(page: Page):
page.title = "Example"
text = Text("Hello Flet")
page.controls.append(text)
page.update()
flet.app(target=main)
となります。上記処理は
-
Text
オブジェクトのインスタンス生成(text
) -
Page
オブジェクトのcontrols:list
にtext
を追加 -
Page
オブジェクトのupdate
メソッドの呼び出し
を行います。この処理と等価な処理が
import flet
from flet import Page, Text
def main(page: Page):
page.title = "Example"
text = Text("Hello Flet")
page.add(text)
flet.app(target=main)
です。基本的にはPage#add()
を使用していく方が便利です。(チュートリアルでもadd
メソッドを使用しています。)
このように、Page
オブジェクトにテキストや画像などを追加していき、画面を作成していきます。
以下でコードを記載しますが、
- main.py
- header.py
- sidebar.py
が同階層にあることを想定して、main.py
を次のように記載しました。
import flet
from flet import(
Page,
Text,
Row,
)
from header import AppHeader
from sidebar import Sidebar
def main(page: Page):
page.title = "Example"
page.padding = 10
# インスタンス作成時にpage.appbarにナビゲーションバーが設定される
AppHeader(page)
sidebar = Sidebar(page)
# サイドバーとメインコンテンツを横並びに
layout = Row(
controls=[sidebar, Text("Main Content")],
tight=True, # 水平方向の隙間をどうするか。デフォルトはFalseですべての要素に余白を与える。
expand=True, # 利用可能なスペースを埋めるようにするか。
vertical_alignment="start", # 画面上部から表示。ほかに"center"や"end"などの値がある。
)
# pageオブジェクトに追加
page.add(layout)
page.update()
flet.app(target=main)
AppHeader
およびSidebar
は後述するとして、新しくflet#Row()
オブジェクトを導入しました。行にあたるRow
があるのであれば列にあたるColumn
もあります。(flet#Column()
)これらは、今回のようにレイアウトを作成したいときに非常に便利なオブジェクトで、ざっくり
-
Row
: 横方向にオブジェクトを並べる -
Column
: 縦方向にオブジェクトを並べる
といったことが可能です。flet#Row#controls
の中に、オブジェクトを設定していきます。このリストの中に入れた順番に表示されるはずです。
細かい、設定については上記コードにあるようにtight
やexpand
といったものを参照します。
2. Nav Bar (header.py)
導入部分で紹介した図では、Nav Barにいくつかオブジェクトを並べていましたが、初めは表示させることのみに着目してコードを記載していきます。そのため、header.py
とは若干脱線します。基本について習熟済みの場合はこちらは読み飛ばしてください。
まず、Nav Barのメインはflet#AppBar()
となります。こちらをインポートしインスタンス生成、Page
オブジェクトに追加することでナビゲーションバーを実装することができます:
import flet
from flet import (
AppBar,
colors,
ElevatedButton,
Icon,
icons,
Page,
Text,
)
def main(page: Page):
app_bar = 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, # ナビゲーションバーの背景色
)
page.add(app_bar)
flet.app(target="main")
このようにすると、画面上部にナビゲーションバーが表示されます。
しかし、この方法でText
を追加するとエラーが発生します。
理由について公式ドキュメントでは見つけられなかったので、ChatGPTでFlutter
について同様な内容がないかどうか聞いてみたところ、
Flutterにおいて、AppBarとTextを同じコントロール内に直接配置することはできません。AppBarはScaffoldウィジェットの一部であり、通常のコンテンツとして配置することができません。
と返答が来ました。Scaffold
はflet
のドキュメントで検索しても出てこないので正確な回答ではありませんが、Flutter
のAppBar
と同様にflet
のAppBar
はText
などと同じコントロールに配置することができません。そのため、Page#appbar
にAppBar
を配置することでこの問題を回避します。(公式ドキュメントではこちらで書かれていました。)なので、上記コードを以下のように書き直します:
import flet
from flet import (
AppBar,
colors,
ElevatedButton,
Icon,
icons,
Page,
Text,
)
def main(page: Page):
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,
)
page.add(
Text("Main content")
)
flet.app(target="main")
このように設定することでナビゲーションバーとメインが表示されるはずです。
他にもないかと調べてみたところ、Page#banner
やPage#dialog
が見つかりましたが、AppBar
のような事象は特になかったです。
参考までに、ナビゲーションバーにバナーを表示させるコードが以下になります。(サンプルからとってきたものでバナーを表示させるトリガーはボタンのクリックになります):
import flet
from flet import (
AppBar,
Banner,
colors,
ElevatedButton,
Icon,
icons,
Page,
Text,
TextButton,
)
def main(page: Page):
def close_banner(e):
page.banner.open = False
page.update()
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,
)
page.banner = Banner(
bgcolor=colors.AMBER_100,
leading=Icon(icons.WARNING_AMBER_ROUNDED, color=colors.AMBER, size=40),
content=Text(
"Oops, there were some errors while trying to delete the file. What would you like me to do?"
),
actions=[
TextButton("Retry", on_click=close_banner),
TextButton("Ignore", on_click=close_banner),
TextButton("Cancel", on_click=close_banner),
],
)
def show_banner_click(e):
page.banner.open = True
page.update()
page.add(ElevatedButton("Show Banner", on_click=show_banner_click))
flet.app(target=main)
上記のコードではざっくり:
-
Page
オブジェクトのbanner
にBanner
オブジェクトをセット - バナーは
Banner#open
がTrue
の時に表示され、False
では表示されない -
on_click
イベント、つまりボタンのクリックによってバナーの表示を制御している- "Show Banner"ボタンを押下 → バナー表示
- "Retry/Ignore/Cancel"ボタンを押下 → バナー非表示
余談として、Page#banner
ではなく、Page#add()
によってバナーを追加した場合(上記コードでpage.
を除く)でもアプリケーションは正常に動きました。ただし、controls=[ElevatedButton, Banner]
の順番で追加したとしても表示されるのはナビゲーションバーの下部でした。
Banners are displayed at the top of the screen, below a top app bar.
(ドキュメント通りです。はい。)
(ここから、header.py
の内容になります。)
さて、ナビゲーションバーにボタンやらいろいろ追加していきます。こちらはAppBar#actions
に記載していくことで実装ができます。
import flet
from flet import (
AppBar,
colors,
Container,
ElevatedButton,
Icon,
IconButton,
icons,
margin,
Page,
PopupMenuButton,
PopupMenuItem,
Row,
Text,
)
def main(page: Page):
def toggle_icon(e):
page.theme_mode = "light" if page.theme_mode == "dark" else "dark"
toggle_dark_light_icon.selected = not toggle_dark_light_icon.selected
page.update()
toggle_dark_light_icon = IconButton(
icon="light_mode",
selected_icon = "dark_mode",
tooltip=f"switch light / dark mode",
on_click=toggle_icon,
)
appbar_items = [
PopupMenuItem(text="Login"),
PopupMenuItem(), # divider
PopupMenuItem(text="Settings"),
]
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(
[
toggle_dark_light_icon,
ElevatedButton(text="Button"),
PopupMenuButton(
items=appbar_items
),
],
alignment="spaceBetween",
),
margin=margin.only(left=50, right=25)
)
]
)
page.update()
flet.app(target=main)
上記コードでは、
ElevatedButton
PopupMenuButton(PopupMenuItem)
IconButton
といったボタンを追加しました。それぞれ、クリックイベントを追加することができますが、実装しているのはライトまたはダークテーマに入れ替えるためのボタンのみです。イベントの実装はほとんどこちらをまねたものです。toggle_icon
メソッドは
-
Page#theme_mode
:SYSTEM (default)
,LIGHT
orDARK
- ボタンを押下したときのテーマに合わせて、以下のようにテーマを入れ替える:
- ダークモード → ライトモード
- ライトモード → ダークモード
-
IconButton#tooltip
に値を入れることで、ボタンをマウスホバーした際に、その値を表示する(説明を表示する場合に使用。ほかのボタンも同じく設定可能)
といったものです。
3. Side Bar (sidebar.py)
サイドバーを作成するために、NavigationRail(NavigationRailDestination)
を使用します。サイドバーは今回は左側に設置しますが、Row
を上手く設定すれば右側に設置することも可能です。
さて、サイドバーを表示するコードは以下の通りです:
import flet
from flet import (
alignment,
border_radius,
colors,
Container,
FloatingActionButton,
Icon,
IconButton,
icons,
NavigationRail,
NavigationRailDestination,
NavigationRailLabelType,
Page,
Row,
Text,
)
def main(page: Page):
def toggle_nav_rail(e):
nav_rail.visible = not nav_rail.visible
toggle_nav_rail_button.selected = not toggle_nav_rail_button.selected
toggle_nav_rail_button.tooltip = "Open Side Bar" if toggle_nav_rail_button.selected else "Collapse Side Bar"
page.update()
nav_rail_items = [
NavigationRailDestination(
icon=icons.FAVORITE_BORDER,
selected_icon=icons.FAVORITE,
label="First"
),
NavigationRailDestination(
icon_content=Icon(icons.BOOKMARK_BORDER),
selected_icon_content=Icon(icons.BOOKMARK),
label="Second"
),
NavigationRailDestination(
icon=icons.SETTINGS_OUTLINED,
selected_icon_content=Icon(icons.SETTINGS),
label_content=Text("Settings"),
),
]
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, # -1.0から1.0の間の値により整列方法を指定(-1.0(default):ページ上部, 0.0:ページ中央, 1.0:ページ下部)
destinations=nav_rail_items,
on_change=lambda e: print("Selected destination: ", e.control.selected_index),
)
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=toggle_nav_rail,
tooltip="Collapse Nav Bar",
)
view = Container(
content=Row(
controls=[
nav_rail, # サイドバー本体
Container( # サイドバーとボタンの区切り線
bgcolor=colors.BLACK26,
border_radius=border_radius.all(30),
height=220,
alignment=alignment.center_right,
width=2
),
toggle_nav_rail_button, # サイドバーの表示・非表示を制御するためのボタン
],
expand=True, # 画面の空白部分を補うかどうか
vertical_alignment="start", # 画面上部より表示
)
)
page.add(view)
flet.app(target=main)
上記コードを少しずつ説明しますが、ざっくりというとview
をPage
に渡すことでサイドバーが表示されます。なので、view
の中身について深堀していきます。
まず、複数の要素をまとめておりますが細かい内容になるのでこちらは後回し、そしてレイアウトを設定するためにflet#Container
を使用していることに注意してください。今回、大まかな使用用途は
- レイアウトの設定
- 区切り線
となります。
view
の中身を確認してみると、nav_rail
、Container(区切り線)
そしてtoggle_nav_rail_button
がRow
のcontrols:list
に順番に挿入されているのことがわかります。(サイドバー→区切り線→ボタン)
次に、nav_rail
を確認してみます。NavigationRail
の中を確認してみるとleading
にボタン、destinations
にnav_rail_items(NavigationRailDestination)
とあります。これがサイドバーのボタンです。leading
のボタンにはイベントは追加されていませんが、destinations
のボタンにはイベントが追加されています。
ここで、on_change
イベントはボタンが未選択・選択状態になることでイベントが発火し、ここでは選択されたボタンがnav_rail_items
の何番目のボタンであるかをターミナルに表示しています。余談ですが、コード内のprint(e.control)
としてターミナルに出力させると、nav_rail
の設定値が確認できます。アクセス方法は、NavigationRail
のプロパティを指定すればよいです。以下は出力される内容です:
Selected destination: navigationrail {'height': 300, 'labeltype': 'all', 'minwidth': 100, 'minextendedwidth': 400, 'groupalignment': -0.9, 'selectedindex': '0'}
最後に、toggle_nav_rail_button
ですが、こちらにはクリックイベントが追加されています。メソッドtoggle_nav_rail
は以下のように処理をします:
-
NavigationRail#visible
はTrue
のとき表示、False
のとき非表示 - 上記、性質を用いてイベントが走ったときに、
True
ならFalse
に、False
ならTrue
に変更 - 同時に、
tooltip
を「サイドバーを閉じる」から「サイドバーを開く」という表現に変更 -
Page
の更新
サイドバーが非表示になるので(区切り線→ボタン)という状態に変化します。
このようにしてサイドバーを閉じたり、開いたりすることができます。
(なお、NavigationRail#visible
が公式ドキュメントに記載がない。何故?)
4. まとめ
今回はナビゲーションバーとサイドバーおよびその周辺で使用したものについて紹介しました。現在のままでは、少しコードが長くなりすぎている、加えてmain.py
とかみ合っていない状態です。(紹介のためにclass
バージョンだとself.
が加わって見ずらいのですべて取っ払いました。あとは自分のための復習ですね。)
次回の記事では、レイアウト要素をclass
バージョンに書き直し、最終的な実装物を紹介できたらと考えています。