今回の記事は以前の記事を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
)を用いて宣言する。以下は、同サイトで紹介されているコードを少し書き換えたものです。
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
を呼び出すだけにしました:
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
を簡潔に記述する目的で作成しました。
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.controls
にSidebar()
とContentBody()
の順にそれぞれ渡しています(Row
はその名の通り、横にコンポーネントを並べます。)。
レイアウト自体は非常にシンプルですね。
最後に、コンポーネントを順に焼き直していきます。
まずは、ボディから。
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()
を並べてくれます)。
次に、ヘッダー。
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=page
とself.page_title=page_title
を設定するのだけお忘れなく。
最後に、サイドバーです。
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. まとめ
今回の記事は前回作成した記事の復習および整理になっていますが、まとめたおかげで画面遷移などの実装のイメージが付きやすくなった印象です。たまには整理が重要だと感じた次第です。