LoginSignup
0
1

【Flet】WEBレイアウトをClassで作成

Posted at

今回の記事は以前の記事をFletのバージョン0.21.X以降でUserControlが非推奨となったことを契機に焼き直しを図ることを目的としています。
まずはFletのバージョン更新に伴うClass形式の記述方法の変更点について紹介し、その後以前作成したコードを更新していきます。

本記事を作成するにあたって以下を参照しました。

1. FletにおけるClass形式の記述方法

Flet makes writing dynamic, real-time web apps a real fun!

Flet 0.21.0 further improves web apps development experience as well as using asyncio APIs in your Flet apps.

Here's what's new in Flet 0.21.0:
-- 中略 --
In this Flet release we also re-visited API for writing custom controls in Python.

As a result UserControl class has been deprecated. You just inherit from a specific control with layout that works for your needs.

平たく言うと、Fletのバージョン0.21.0以降ではUserControlを用いたClass宣言が非推奨となり、特定のコンポーネント(例えば、Text)を用いて宣言する。以下は、同サイトで紹介されているコードを少し書き換えたものです。

countdown.py
import asyncio
from flet import (
    app,
    Text,
    Page,
)

class Countdown(Text):
    def __init__(self, seconds):
        super().__init__()
        self.seconds = seconds

    def did_mount(self):
        self.running = True
        self.page.run_task(self.update_timer)

    def will_unmount(self):
        self.running = False

    async def update_timer(self):
        while self.seconds and self.running:
            mins, secs = divmod(self.seconds, 60)
            self.value = "{:02d}:{:02d}".format(mins, secs)
            self.update()
            await asyncio.sleep(1)
            self.seconds -= 1

def main(page: Page):
    page.add(Countdown(120), Countdown(60))


if __name__ == '__main__':
    app(target=main)

バージョン0.21.0以前では

class Countdown(UserControl):
    中略
    def build(self):
        return Text(...)

のようにbuild()メソッドを必ず実装する必要がありましたが、アップデートにより不要となり記述がすっきりしました。
以降の最新化では基本的にUserControl,build()を消去、コンストラクター__init__()でコンポーネントの定義を行っていきます。

2. コードの最新化

以前記述したコードよりもわかりやすいく分割するため以下のようなフォルダ構成にしました。

フォルダ構造
root.
|   .gitignore
|   layout.py
|   main.py
|   README.md
|   requirements.txt
|   
+---components
|   |   body.py
|   |   header.py
|   |   sidebar.py   

まず、rootフォルダを作成するために

flet create <folder_name>

を実行しました。assetsフォルダなど作成されますが今回は不要なので削除しています。
ファイルの相関としてはbody.py, header.py, sidebar.py -> layout.py -> main.pyといった感じでmain.pyは基本的にlayout.pyを呼び出すだけにしました:

main.py
from layout import MyLayout
from flet import (
    app,
    Page,
)

def main(page: Page):
    page.title = "Example Layout"
    page.padding = 10

    page.add(MyLayout(page))

if __name__ == '__main__':
    app(target=main)

次にlayout.pyですが、こちらはmain.pyを簡潔に記述する目的で作成しました。

layout.py
from components.body import ContentBody
from components.header import AppHeader
from components.sidebar import Sidebar
from flet import (
    CrossAxisAlignment,
    MainAxisAlignment,
    Row,
    Page,
)

class MyLayout(Row):
    def __init__(self, page: Page):
        super().__init__()
        self.page = page
        self.alignment=MainAxisAlignment.START
        self.vertical_alignment=CrossAxisAlignment.START

        AppHeader(page, 'WEB LAYOUT') # インスタンス生成時にpage.appbarにナビゲーションバーが設定される
        self.controls = [ Sidebar(), ContentBody() ]

MyLayoutはヘッダー、サイドバー、メインボディの3部構造で画面を作成するように設定しています。componentsフォルダからbody, header, sidebarモジュールをインポートし、ヘッダーを作成(AppHeader()をインスタンス生成)し、Row.controlsSidebar()ContentBody()の順にそれぞれ渡しています(Rowはその名の通り、横にコンポーネントを並べます。)。
レイアウト自体は非常にシンプルですね。

最後に、コンポーネントを順に焼き直していきます。
まずは、ボディから。

body.py
from flet import (
    Column,
    Text,
)

class ContentBody(Column):
    def __init__(self):
        super().__init__()
        self.spacing = 10
        self.controls = [
            Text('**********************'),
            Text('This is Main Body.'),
            Text('**********************'),
        ]

Text()Column()controlsに並べているだけです(Columnは縦にText()を並べてくれます)。

次に、ヘッダー。

header.py
from flet import (
    AppBar,
    colors,
    Container,
    ElevatedButton,
    Icon,
    IconButton,
    icons,
    MainAxisAlignment,
    margin,
    Page,
    PopupMenuButton,
    PopupMenuItem,
    Row,
    Text,
)

class AppHeader(AppBar):
    def __init__(self, page: Page, page_title: str="Example"):
        super().__init__()
        self.page = page
        self.page_title = page_title
        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,
        )
        self.appbar_items = [
            PopupMenuItem(text="Login"),
            PopupMenuItem(),
            PopupMenuItem(text="SignUp"),
            PopupMenuItem(),
            PopupMenuItem(text="Settings"),
        ]
        # Appのpage.appbarフィールドの設定
        self.page.appbar = AppBar(
            leading=Icon(icons.TRIP_ORIGIN_ROUNDED),
            leading_width=100,
            title=Text(value=self.page_title, size=32, text_align="center"),
            center_title=False,
            toolbar_height=75,
            bgcolor=colors.SURFACE_VARIANT,
            actions=[
                Container(
                    content=Row(
                        [
                            self.toggle_dark_light_icon,
                            ElevatedButton(text="SOMETHING"),
                            PopupMenuButton(
                                items=self.appbar_items
                            ),
                        ],
                        alignment=MainAxisAlignment.SPACE_BETWEEN,
                    ),
                    margin=margin.only(left=50, right=25)
                )
            ],
        )

    def toggle_icon(self, e):
        self.page.theme_mode = "light" if self.page.theme_mode == "dark" else "dark"
        self.toggle_dark_light_icon.selected = not self.toggle_dark_light_icon.selected
        self.page.update()

ボディと比較すると幾分複雑ですが、Page.appbarに対してAppBar()を設定しており、ほかはその詳細なレイアウトの設定となっています。非常に簡単なヘッダーで問題なければ

self.page.appbar = AppBar(
            title=Text(value=self.page_title, size=32, text_align="center"),
            center_title=False,
            toolbar_height=75,
            bgcolor=colors.SURFACE_VARIANT,
        )

とすればページタイトルと背景色、ヘッダーの高さのみを設定したシンプルなヘッダーが出来上がります。(それ以外は、プルダウンやロゴなどヘッダーをリッチにしている感じです。)後はコンストラクターでsuper().__init__(), self.page=pageself.page_title=page_titleを設定するのだけお忘れなく。

最後に、サイドバーです。

sidebar.py
from flet import (
    alignment,
    border_radius,
    colors,
    Container,
    CrossAxisAlignment,
    FloatingActionButton,
    Icon,
    IconButton,
    icons,
    NavigationRail,
    NavigationRailDestination,
    NavigationRailLabelType,
    Row,
    Text,
)

class Sidebar(Container):
    def __init__(self):
        super().__init__()
        self.nav_rail_visible = True
        self.nav_rail_items = [
            NavigationRailDestination(
                icon=icons.FAVORITE_BORDER,
                selected_icon=icons.FAVORITE,
                label="Favorite"
            ),
            NavigationRailDestination(
                icon_content=Icon(icons.BOOKMARK_BORDER),
                selected_icon_content=Icon(icons.BOOKMARK),
                label="Bookmark"
            ),
            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",
        )
        self.visible = self.nav_rail_visible
        self.content = Row(
            controls=[
                self.nav_rail,
                Container(
                    bgcolor=colors.BLACK26,
                    border_radius=border_radius.all(30),
                    height=480,
                    alignment=alignment.center_right,
                    width=2
                ),
                self.toggle_nav_rail_button,
            ],
            vertical_alignment=CrossAxisAlignment.START,
        )


    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.update()

Container.contentにナビゲーションバー、ディバイダー(分割線)とナビゲーションバーのトグルボタンを横並びに設定しています(ディバイダーがなくても問題なく、デザイン次第ですね。)。トグルボタンを押下するとナビゲーションバーが閉じたり、開いたりするイベントがtoggle_nav_rail()メソッドにより設定されています。

3. まとめ

今回の記事は前回作成した記事の復習および整理になっていますが、まとめたおかげで画面遷移などの実装のイメージが付きやすくなった印象です。たまには整理が重要だと感じた次第です。

0
1
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
0
1