LoginSignup
12
20

【Flet】Fletで簡単なWEBアプリケーションのレイアウトを作成

Posted at

今回の記事は、Fletのチュートリアル後にWEBレイアウト的なものを試しに作成してみたので、その備忘録です。時間を置くと何を作っていたかも忘れるのでなるはやで作成していきます。

レイアウトとしては、次のような感じ:

outline.png

ナビゲーションバー(Nav Bar)があり、サイドバー(Side Bar)+サイドバーを閉じる・開くボタン(btn)そして本文(Main)といったものです。
Mainについてはどのようなものを実装するか検討中のため、簡単なテキストの出力にとどめました。

今回の作成物のスクショがこちら(ボタン名などはかなり適当なのでご注意):

example.png

今回の記事で参照した記事は

で主に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:listtextを追加
  • 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を次のように記載しました。

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の中に、オブジェクトを設定していきます。このリストの中に入れた順番に表示されるはずです。
細かい、設定については上記コードにあるようにtightexpandといったものを参照します。

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ウィジェットの一部であり、通常のコンテンツとして配置することができません。

と返答が来ました。Scaffoldfletのドキュメントで検索しても出てこないので正確な回答ではありませんが、FlutterAppBarと同様にfletAppBarTextなどと同じコントロールに配置することができません。そのため、Page#appbarAppBarを配置することでこの問題を回避します。(公式ドキュメントではこちらで書かれていました。)なので、上記コードを以下のように書き直します:

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#bannerPage#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オブジェクトのbannerBannerオブジェクトをセット
  • バナーはBanner#openTrueの時に表示され、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に記載していくことで実装ができます。

header.py
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 or DARK
  • ボタンを押下したときのテーマに合わせて、以下のようにテーマを入れ替える:
    • ダークモード → ライトモード
    • ライトモード → ダークモード
  • IconButton#tooltipに値を入れることで、ボタンをマウスホバーした際に、その値を表示する(説明を表示する場合に使用。ほかのボタンも同じく設定可能)

といったものです。

3. Side Bar (sidebar.py)

サイドバーを作成するために、NavigationRail(NavigationRailDestination)を使用します。サイドバーは今回は左側に設置しますが、Rowを上手く設定すれば右側に設置することも可能です。
さて、サイドバーを表示するコードは以下の通りです:

sidebar.py
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)

上記コードを少しずつ説明しますが、ざっくりというとviewPageに渡すことでサイドバーが表示されます。なので、viewの中身について深堀していきます。
まず、複数の要素をまとめておりますが細かい内容になるのでこちらは後回し、そしてレイアウトを設定するためにflet#Containerを使用していることに注意してください。今回、大まかな使用用途は

  • レイアウトの設定
  • 区切り線

となります。
viewの中身を確認してみると、nav_railContainer(区切り線)そしてtoggle_nav_rail_buttonRowcontrols:listに順番に挿入されているのことがわかります。(サイドバー→区切り線→ボタン)

次に、nav_railを確認してみます。NavigationRailの中を確認してみるとleadingにボタン、destinationsnav_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#visibleTrueのとき表示、Falseのとき非表示
  • 上記、性質を用いてイベントが走ったときに、TrueならFalseに、FalseならTrueに変更
  • 同時に、tooltipを「サイドバーを閉じる」から「サイドバーを開く」という表現に変更
  • Pageの更新

サイドバーが非表示になるので(区切り線→ボタン)という状態に変化します。
このようにしてサイドバーを閉じたり、開いたりすることができます。
(なお、NavigationRail#visibleが公式ドキュメントに記載がない。何故?)

4. まとめ

今回はナビゲーションバーとサイドバーおよびその周辺で使用したものについて紹介しました。現在のままでは、少しコードが長くなりすぎている、加えてmain.pyとかみ合っていない状態です。(紹介のためにclassバージョンだとself.が加わって見ずらいのですべて取っ払いました。あとは自分のための復習ですね。)
次回の記事では、レイアウト要素をclassバージョンに書き直し、最終的な実装物を紹介できたらと考えています。

12
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
20