LoginSignup
3
5

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

Last updated at Posted at 2023-08-06

警告
以下の記事はFletのバージョン0.8.xで作成したものです。本注意を記述した現在の最新バージョンは0.22.1(2024/05/21)であり、Class記法においてUserControlは非推奨、加えてClassの定義で必ずbuild()を実装しなければならなかった点が変更となっています。以上の点から本記事とは別に最新のバージョンでの記法に修正した内容を投稿予定です。

今回は次の記事:

をクラス型で定義しなおすといった内容です。
Pythonのクラス型に関しては前回の記事:

を参考までに。

記事は前回の流れを踏襲して、

  1. Main (main.py)
  2. Nav Bar (header.py)
  3. Side Bar (sidebar.py)

の順に紹介します。

1. Main (main.py)

アプリケーションを起動するメイン(Main)部分はmain.pyとします。このモジュールでは

  • header.pyのインポート
  • sidebar.pyのインポート
  • ヘッダー・サイドバー・メインの3構成でレイアウトを作成
  • アプリケーションの起動

といった処理を行います。以前紹介した記事の内容と変わりません:

main.py
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の内部ではTextElevatedButtonなどの要素を定義します。ここで、UserControlについていくつか注意すべき点について次の通り:

  • UserControlは再利用可能な独立したコンポーネントのビルドを行うためのクラスであり、ビルドを行うために必ずbuild()メソッドを実装する必要がある。
  • UserControlは独立したコンポーネントであるため、Pageに対する変更に対しても変更を反映するために、self.update()を呼び出す。(独立したコンポーネントは自身を更新できるように実装すべし)
  • UserControlを基底クラスとした派生クラスのコンストラクターは必ず、super().__init__()を呼び出す。

つまり、関数形式からクラス形式に転じる際の変更点は

  • defclass
  • コンストラクター__init__()の実装
  • ビルドbuild()の実装
  • クラスオブジェクトのメンバに各コンポーネントを割り当てる

といったことになります。4つ目が個人的には一番面倒なところにはなりますね。(selfを何度も使用するので)
以上の点を考慮して実装したコードが次の通り:

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

サイドバーの実装についてもヘッダーと同様に、変更していきます。変更後は次の通り:

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を実行すれば前回と同じように次の表示がされるはずです:

example2.png
]

上記画像は、前回の記事の変更点として

  • Mainの箇所がMain Bodyに変更
  • 右上の「太陽のアイコン」をクリック後

です。

後は、

  • 各種ボタンに対して適切な名前の追加・アイコンの変更、イベントの追加を行う
  • Main Body部分にいい感じの実装をする

といったことをすればよさそうです。今回はレイアウトが作れればOKなのでここいらで完了。

5. 少しお遊び

Main Bodyに追加する要素について、簡単なサンプルをこちらで記述。イベントの取得などを試してみるといった観点で作成しました。まずは、適当なモジュールでお試し:

sample.py
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つ(サイドバーのボタンで何かできるか?)
  • 追加ボタンにはtexton_clickそしてdata属性を設定
  • 削除ボタンを1つ
  • 削除ボタンにはtexton_clickを設定
  • 空のColumn要素(ボタン押下で要素を追加する用)
  • ボタンクリックイベント用の関数add_element
  • 画面要素を格納するリストhist(履歴みたいに使用できるか?。スコープはmain内部)

からなっています。

試したこと

  1. on_click属性を使用して要素を追加

  2. イベントが発火されたときに渡される引数eの中身を調べる:

    • e : オブジェクト情報(人が読めるのは同じかそうでないか程度)
    • e.control : クラス名とその引数に関する情報
      例:elevatedbutton {'text': 'Button1'}
    • e.control.text : text属性の値
      例:Button1
    • e.control.data : data属性の値
      例: {'key': 'value1'}
  3. histに格納されたTextの要素の末尾を画面表示、それ以外は履歴として残す。残された履歴はDeleteボタンで末尾を削除されると末尾の前の要素が表示される。(戻るボタンに近い。)
    例:
    【手順】以下の順番でボタンをクリックする。Button1Button2Button3DeleteDeleteDelete
    【確認】Deleteボタンの下の表示は次のようになるはず。
    ELEMENNT ADDED value1ELEMENNT ADDED value2ELEMENNT ADDED value3ELEMENNT ADDED value2ELEMENNT ADDED value1→非表示(histが空)

上記のようなものを使用して、サイドバーとかのボタンで画面遷移を行うようにしていきたい。

6. まとめ

今回は簡単にだが、前回の記事を関数形式からクラス形式の記述に変更した。この変更により、特定のコンポーネントを作成し、それを流用して使用するといったことが可能となる。(今回はレイアウトでしたが、カードといった要素があるのでその1要素をクラスで定義しておくと便利)
レイアウトの作成ができたので、次回以降はほかのコンポーネントの定義または画面遷移の実装などを実施していきたい。

3
5
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
3
5