最近Pytonでツール類を作る機会が多く、その中でもFletを使う機会が多くなったので、自分自身への備忘録と、これから Fletを始める方々へのアドバイスを兼ねて、Fletによるツール開発で最初に知っておくべきことを纏めてみました。
Fletを使ってクロスプラットフォームの開発をしてみたい方は、是非この記事を参考にしてください。
Fletとは
Flet は、Python でクロスプラットフォームな GUI アプリを開発するためのフレームワークです。Flutter という Google のフレームワークをベースにしており、Python で記述されたコードを Flutter に変換して実行します。そのため、Flutter で用意されている豊富な UI コンポーネントや機能を利用することができます。
詳細については下記の公式ページに詳しく記載されています。
仕組み
FletのUIを記述したPythonソースコードは、FletによりFlutterの言語(Dart)に変換され、必要なライブラリと合わせてFlutterエンジン上で実行されます。
Flutterは各プラットフォームに応じたエンジンが用意されているため、Fletを使って開発したプログラムは必然的にクロスプラットフォームに対応することになります。
PythonのソースコードをFlutterへ変換する処理は初回実行時のみであるため、初回(=ソースコード変更後)は少々起動に時間が掛かりますが、2回目以降は早く起動するようになります。
Fletの特徴
Flet のメリット、デメリットを紹介しておきます。
メリット | 説明 |
---|---|
クロスプラットフォーム | Windows、Mac、Linux、iOS、Android の各プラットフォームで 動作するアプリを開発できる |
高速 | Flutter のパフォーマンスをそのまま利用できるため、高速なアプリを 開発できる |
簡単 | Python で記述できるため、フロントエンド開発の経験がなくても 簡単にアプリを開発できる |
デメリット | 説明 |
---|---|
カスタマイズ性 | Flutter の UI コンポーネントや機能をそのまま利用するため、 カスタマイズ性は Flutter に依存する |
パフォーマンス | Python で記述されたコードを Flutter に変換するため、 パフォーマンスが劣化する可能性がある |
ドキュメント | Flet はまだ開発初期段階であるため、ドキュメントが不十分な部分がある |
Fletを使うと見栄えの良いUIが比較的簡単に開発できますが、その反面UIコンポーネントの見た目や動作のカスタマイズがとてもに苦手です。
例えば、デスクトップアプリやWebアプリで多用されるファイルのドラッグ&ドロップが使えない、テキストボックスなどのUIコンポーネントのデザインがほとんど変更できないなど、多くの制限があります。
従って、C#やJavaのような凝ったUIの動作や、複雑なレイアウトをFletと実現しようとすると、途端に超えられない壁にぶつかります。
Pythonで比較的簡単なアプリをマルチプラットフォームで実現したい、あるいはPythonのスキルを活かして見栄えの良いUIを実現したい場合にのみ、Fletを採用する方が無難な気がします。
インストール方法
まず最初に、FletはPython 3.7以上での利用が可能です。その前提で下記のpipコマンドを実行して下さい。
1 |
pip install flet |
また、プログラムで flet を使う場合は、下記の import を記述します。
1 |
import flet as ft |
Fletを使ったプログラムの作り方
公式ページではUIコンポーネントのことを「コントロール」と表記していましたので、本記事でもコントロールという呼び方をします。あらかじめご了承下さい。
Fletは4つのステップでプログラムを記述します。
最後の ft.app() で Flutterのエンジンが起動し、画面が表示されることになるのですが、その実態は組み込みのWebサーバであり、それがあたかもデスクトップアプリの様に振舞ってくれています。
最も簡単な画面
下記は公式ページのチュートリアルに掲載されているサンプルプログラムです。
1 2 3 4 5 6 |
import flet as ft def main(page: ft.Page): page.add(ft.Text(value="Hello, world!")) ft.app(target=main) |
実行すると次の画面が表示されます。
サンプルプログラムでは、main関数の中で ft.Text(value="Hello, world!") を1つだけ add() で登録していますが、ここに様々な コントロールを登録すことで画面を作成します。
Windows ネイティブアプリとWebアプリ
サンプルプログラムの最後にFletのエンジンを呼び出していますが、下記の記述に少し手を加えることで、Webアプリとして実行することが可能です。
1 |
ft.app(target=main) |
具体的には、view引数に ft.AppView.WEB_BROWSERを指定します。
1 |
ft.app(target=main, view=ft.AppView.WEB_BROWSER) |
画面作成の基本
先ほども説明しましたが、コントロールをPageクラスのインスタンスに登録することで画面を作成します。
コントロールには大きく分けて以下のものが用意されています。
- レイアウトを担当するもの(Container,Row,Column,ListView,GridView,・・・)
- ナビゲーションを担当するもの(AppBar,NavigationRail,Navigation Bar)
- 画面の表示を担当するもの(Text,Image,Canvas,Markdown,・・・)
- 画面の入力を担当するもの(Button,TextField,Checkbox,DropDown,Slider,・・・)
- グラフ表示を担当するもの(LineChart,BarChart,PieChat,MatpotlibChat,・・・)
- その他の補助機能(Audio,FilePicker,Tooltip,HapticFreedback,・・・)
全てを紹介しきれないので、詳細は公式ページに譲るとして、本記事では最初に理解しておくべきコントロールに絞って解説していきたいと思います。
Page
PageはWindowとコントロールを管理するクラスです。
1 2 3 4 5 6 7 8 9 |
page.title = "タイトル" page.window_width = 1000 # Winodwの横幅 page.window_height = 1000 # Windowの高さ page.window_resizable=True # サイズの変更の許可 page.window_frameless = False # Windowの外枠を消す page.window_always_on_top = False # 常に画面の最上位に表示する page.bgcwindow_bgcolor = ft.colors.WHITE # 背景色 page.window_prevent_close = False # Windowの×アイコンでイベントを発火させる page.on_window_event = None # Windowの×アイコンクリック時に実行させたい関数 |
Windowの×ボタンで何か処理を行いたい場合は、下記の様に記述します。
1 2 3 4 5 6 7 8 |
# Windowの×アイコンクリック時の処理 def window_event(e): if e.data == "close": #~ ここに行いたい処理を記述 ~ page.window_destroy() # Windowを閉じる page.window_prevent_close = True # Windowの×アイコンでイベントを発火させる page.on_window_event = window_event # Windowの×アイコンクリック時にwindow_eventを実行 |
Updateメソッドによる再描画
Flet では、コントロールに何らかの値を代入しても、画面には反映されません。例えば、page.title プロパティにプログラム名をセットしても、そのままでは表示されません。
page及びコントロールに対して表示項目を設定した場合、かならず update() メソッドを呼ぶ必要があります。
update() メソッドは page 及び 各コントロールに用意されています。
画面全体を再描画したい場合は page.update() を、特定のコントロールに対して値を変更した場合、そのコントロールに用意されている update() メソッドを呼ぶようにします。
レイアウト用コントロール
レイアウト用UIコンポーネントは、指定されたコントロール(レイアウト用コントロールを含む)を内包し、レイアウトを調整する役割を持ちます。
レイアウト系コントロールには、縦横のサイズ(width,height)指定や、画面のサイズに合わせてレイアウトを自動調整(expand)する機能が備わっており、引数で指定することが出来るようになっています。
引数名 | 意味 | 詳細 |
---|---|---|
width | 横幅の指定 | ドット単位で指定 |
heigh | 高さの指定 | ドット単位で指定 |
expand | 横幅、高さの自動調整 | True又は1で有効、False 又は 0で無効。 expand=Trueの場合、width,heigh の値は無視される。 |
Column
Columnは第一引数(controls)で指定されたコントロールを縦に並べます。spacing により項目間の空白が指定できます。
1 2 3 4 5 6 7 8 9 |
def main(page: ft.Page): # 表示項目(Text)を20個作成 item_list = [ft.Text(f"項目{x}",size=20, width=100) for x in range(20)] # 表示項目をPageに追加 page.add(ft.Column(item_list,scroll=ft.ScrollMode.ALWAYS,spacing=10, expand=True)) ft.app(target=main) |
Row
Rowは第一引数(controls)で指定されたコントロールを横に並べます。spacing により項目間の空白が指定できます。
1 2 3 4 5 6 7 8 9 |
def main(page: ft.Page): # 表示項目(Text)を20個作成 item_list = [ft.Text(f"項目{x}",size=20, width=100) for x in range(20)] # 表示項目をPageに追加 page.add(ft.Row(item_list,scroll=ft.ScrollMode.ALWAYS,spacing=10, expand=True)) ft.app(target=main) |
GridView
GridViewはコントロールをマトリクス上に表示するものです。 runs_count で横に並べる個数が指定できます。
マウスのドラッグ&ドロップ操作で上下スクロールが可能なため、スクロールバーは非表示の状態になっています。マウスを右端に持っていくとスクロールバーが表示されますが、表示用のスペースが無い場合は表示されません。
1 2 3 4 5 6 7 8 9 |
def main(page: ft.Page): # 表示項目(Text)を20個作成 item_list = [ft.Text(f"項目{x}",size=20, width=100) for x in range(20)] # 表示項目をPageに追加 page.add(ft.GridView(item_list,runs_count=5,expand=True)) ft.app(target=main) |
GridViewは画像をサムネイル表示する場合に重宝します。下記は指定したフォルダ内にあるpngファイルを表示する例です。
1 2 3 4 5 6 7 8 9 10 11 |
import glob def main(page: ft.Page): # 指定されたフォルダの画像ファイルを取得 item_list = [ft.Image(src=x) for x in glob.glob("O:\images\*.png")] # 表示項目をPageに追加 page.add(ft.GridView(item_list,runs_count=5,expand=True)) ft.app(target=main) |
Tabs
Tabsはタブを表示するコントロールですが、使うには少しルールを覚える必要があります。
まず最初にTabクラスのcontent にタブに表示したいコントロールを指定するのですが、content は1つしかコントロールを受け付けてくれません。
そこで、ColumnやRowなどのレイアウト系コントロールに、あらかじめ使いたいコントロールをリストで登録しておき、それを Tab の content に指定します。
このようにして複数のTabを作成し、最後にTabsの tabs引数にリスト形式で渡すことでタブを実現します。
下図は2つのタブに画像を表示するサンプルプログラムの実行結果です。便宜上、タブ1とタブ2には同じ画像ファイルを表示しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import glob def main(page: ft.Page): # 指定されたフォルダの画像ファイルを取得 item_list = [ft.Image(src=x) for x in glob.glob("O:\images\*.png")] # 画像ファイルをRow に指定 column = ft.Row(item_list,scroll=ft.ScrollMode.ALWAYS) # タブ1とタブ2に画像ファイルのRowを指定 tab1 = ft.Tab(text="タブ1",content=column) tab2 = ft.Tab(text="タブ2",content=column) # Tab に tab1,とtab2をリストで指定し、TabをPageに追加 page.add(ft.Tabs(tabs=[tab1,tab2],animation_duration=0,selected_index=0,expand=True)) ft.app(target=main) |
Container
Container は単一のコントロールに対して、背景色や周りのコントロールとの間隔を調整する役割を持ちます。
margin や padding は隣り合うコントロールとの隙間を開ける数値ですが、上下左右個別を個別に指定できるわけではありません。
また、表示系コントロールにおいては前景色(文字色)は指定できるものの、背景色の指定が出来ないものも存在するため、その場合はContainer を使って背景色を変えなければなりません。
下図は、それぞれのTextコントロールに対して、Containerを使用し、GridViewでマトリクス表示した例です。
このサンプルプログラムでは、引数で指定したコントロールを Containerに登録して返す専用関数 add_container を用意し、表示項目の生成時に呼び出すようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def add_container(control): return ft.Container(content=control, bgcolor=ft.colors.LIGHT_BLUE, border_radius=30, alignment=ft.alignment.center, margin=20, padding=10 ) def main(page: ft.Page): # 表示項目(Text)を20個作成 item_list = [add_container(ft.Text(f"項目{x}",size=30)) for x in range(20)] # 表示項目をPageに追加 page.add(ft.GridView(item_list,runs_count=5,expand=True)) ft.app(target=main) |
入力/表示用コントロール
入力/表示系のコントロールの多くは、表示サイズ(width,height,expand)や表示色(color,bgcolor)が指定可能で、valueによって値の表示や取得が行なえるようになっています。
これら引数で使われている名前は同時にプロパティとしても実装されており、インスタンス生成後はプロパティとしてアクセスし、値の設定や取得を行います。
引数名 | 意味 | 詳細 |
---|---|---|
color | 表示色 | ft.Colors.〇○○のほか、"#ffeeaa"や "white"などの記述方法 でも指定が可能。 |
bgcolor | 背景色 | 背景色。 |
width | 横幅の指定 | ドット単位で指定。 |
heigh | 高さの指定 | ドット単位で指定。 |
expand | 横幅、高さの自動調整 | True又は1で有効、False 又は 0で無効。 expand=Trueの場合、width,heigh の値は無視される。 |
label | 項目名 | 入力項目のタイトル名。 TextFieldやDropdownの場合は入力エリアの左上に、Checkbox の場合はチェックの横に、Sliderの場合はスライドのつまみの上に 表示される。 |
value | コントロールの値 | TextField、Dropdown、Checkbox、Sliderなどから値を 取得したり、設定するためのもの。 Imageコントロールだけは src に画像ファイルを設定する。 |
tooltip | ツールチップ表示 | マウスを当てると表示される簡単な説明文。 |
但し、入力/表示系コントロールの全てが上記の引数をサポートしている訳ではありません。例えばImageコントロールはvalueの代わりに src というプロパティにファイル名や画像のURLを代入する必要があります。
実際に使う場合は、どのプロパティで値のやり取りが出来るかを、公式サイトのコントロールリファレンスでご確認下さい。
Text
Textコントロールは、画面に任意の文字列を表示するためのものです。第一引数、もしくは value 引数に表示したい文字列を、size 引数にフォントサイズを指定します。
Textコントロールは背景色を指定することが可能ですが、日本語の文字列を表示する場合、文字と文字の間に薄い縦線が入る場合があるので、使い方には注意が必要です。
下図は3つのTextコントロールを使って文字列を表示するサンプルプログラムの実行例です。文字列を改行したい場合、その位置に \n を記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
def main(page: ft.Page): # Textコントロール page.add(ft.Text ( value="Programingの\n世界にようこそ!", size=50 ) ) page.add(ft.Text ( value="Flet User Interface", size=40, color=ft.colors.WHITE, bgcolor=ft.colors.RED ) ) page.add(ft.Text ( value="Flet は楽しい", size=40, color=ft.colors.BLACK, bgcolor=ft.colors.LIGHT_GREEN, weight=ft.FontWeight.BOLD, ) ) ft.app(target=main) |
Button
Buttonコントロールには豊富な種類があり、様々なアイコンも用意されています。使い方はいずれも同じで、アイコン又はボタンの表記を設定し、ボタンがクリックされた時に実行したい関数を on_click 引数に指定するだけです。
下図はボタンをクリックすると、テキストコントロールに「ボタンが押されました」というメッセージを表示するサンプルの実行結果です。
ボタンが押された時、Textコントロールに値を表示するために、Textコントロールのインスタンスを txt 変数に格納し、それをボタンクリックの関数(イベントハンドラ)内で参照し、 value プロパティに値を代入しています。
そして、最後に TextコントロールのUpdate() を呼び出して画面に表示しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def main(page: ft.Page): # Iconボタンクリック時のイベント処理 def access_time(e): txt.value ="ボタンが押されました" txt.update() # Icon ボタンの作成 page.add(ft.IconButton ( icon=ft.icons.ACCESS_TIME, icon_color="blue400", icon_size=40, tooltip="Access Time", on_click=access_time ) ) # ボタンが押された時のメッセージ表示欄 txt = ft.Text() page.add(txt) ft.app(target=main) |
TextField
TextFieldコントロールは任意の文字列を入力するコントロールです。value引数を通じて入力文字列の設定/取得を行います。
外枠の色や太さ、背景色をカスタマイズすることが可能です。また、multiline引数をTrueにすることで複数行の入力が行なえるようになり、max_lines や min_lines で入力エリアの最大/最小行数を制限することが出来ます。
以下はTextFieldコントロールの引数を使ってデザインをカスタマイズした例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
def main(page: ft.Page): page.add(ft.TextField ( label="名前", value="フレッツ太郎", hint_text="ここに氏名を入力して下さい", ) ) page.add(ft.TextField ( label="住所", value="大阪府大阪市", hint_text="ここに住所を入力して下さい", border_color=ft.colors.PINK, border_width=3, border_radius=10 ) ) page.add(ft.TextField ( label="年齢", value="38", color=ft.colors.BLACK, border=ft.InputBorder.NONE, bgcolor="#ffddee", filled=True, ) ) page.add(ft.TextField ( label="職業", value="会社員", bgcolor="#eeffee", border=ft.InputBorder.UNDERLINE, ) ) ft.app(target=main) |
Checkbox、Dropdown、Slider、Image
ここからは4つのコントロールをまとめて説明致します。それぞれ単純なのでソースコードを見て頂ければ使い方は分かるかと思います。
これらのコントロールはTextFieldと同様、value 引数(又はプロパティ)で値の設定や取得を行います。但し、Imageだけは 例外で srcで画像を指定することにご注意ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
def main(page: ft.Page): # Checkbox コントロール page.add(ft.Checkbox(label="チェックボックス",value=True)) # Dropdownコントロール page.add(ft.Dropdown ( label="ドロップダウン", width=300, options=[ ft.dropdown.Option("Red"), ft.dropdown.Option("Green"), ft.dropdown.Option("Blue"), ], value="Green" # 画面起動時はGreenが選択された状態になる ) ) # Sliderコントロール page.add(ft.Slider(min=0, max=100, divisions=10, label="{value}%")) # Imageコントロール page.add(ft.Image(src="https://flet.dev/img/pages/home/flet-home.png",expand=True)) ft.app(target=main) |
ユーザーコントロール
ユーザーコントロールは複数のコントロールを1つのコントロールとして束ねたものです。複雑なUIの動作をユーザーコントロールとして作成しておけば、簡単に再利用が可能となります。
Flet でユーザーコントロールを作成する場合、次のルールを守らなければなりません。
下記は2つのコントロール(TextコントロールとTextFieldコントロール)をRowコントロールで束ねたユーザーコントロールの実行例です。
label 引数で渡された値を左側に表示し、その右側に入力フィールドを表示するという簡単なものです。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Input(ft.UserControl): def __init__(self,label,value = None): super().__init__() self.label = label self.value = value # ユーザーコントロールのインスタンスを返す def build(self): return ft.Row([ ft.Text(self.label), ft.TextField(value=self.value,border_width=3) ]) |
単純に1つの値をやり取りするだけなら上記でも十分ですが、複雑なコントロールになると、複数の入力に対して何らかの処理を行い、その結果を value で取得するようなケースもあるでしょう。
そういう場合は value という名前のメソッドを作成し、それをプロパティとして公開する方法が最も簡単です。
下記は、上記のサンプルにおいて、メソッドを value プロパティとして使えるようにするためのサンプルになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#======== ユーザーコントロールの定義 ======== class Input(ft.UserControl): # 継承元のコンストラクタを呼んだ後で、ユーザーコントロールの初期処理を行なう def __init__(self,label,value = None): super().__init__() self.label = label self.text_field = ft.TextField(value=value,border_width=3) # valueプロパティによる値の取得 @property def value(self): return self.text_field.value # valueプロパティによる値の設定 @value.setter def value(self,val): self.text_field.value = val # ユーザーコントロールのインスタンスを返す def build(self): return ft.Row([ ft.Text(self.label), self.text_field ]) #======== ここまで ======== def main(page: ft.Page): # User コントロール page.add(Input("メールアドレス")) ft.app(target=main) |
値の保存と読み出し
page クラスには、任意の値を json ファイルとして保存したり、読み出したりするためのメソッドが用意されています。
機能 | メソッド |
---|---|
キーによる値の保存 | page.client_storage.set("key",値) |
キーによる値の取得 | page.client_storage.get("key") |
キーの存在チェック | client_storage.contains_key("key") |
キーの削除 | page.client_storage.remove("key") |
但し、保存先が不明確なのと、プログラム修正とビルドを繰り返すうちに値が消えることがあるので、例えば一度入力した内容を次回利用時にも覚えさせておきたい場合などの軽い用途で使うのが良さそうです。
まとめ
今回は python を使ってクロスプラットフォームなアプリが簡単に作れる Flet について、概要と仕組み、得意不得意、インストール方法、画面の作り方について解説しました。
公式サイトのチュートリアルは充実しているものの、いざFletで開発するとなると、細かい挙動で不明点が多く、ツボにハマってしまうこともしばしば・・・
最近は多くのサイトで「やってみた」的な記事が散開されますが、いずれも軽く触れてみた程度で、実用的な情報はまだまだ少ない印象です。
しかし、確実に情報は増えてきており、つい半年前では出来なかったことが出来るようになったりと、今後期待が持てるフレームワークです。
PythonのUIライブラリは数多くの種類が存在しますが、Fletのシェアが今後益々伸びていくことを願いながら、今回の記事を終わりたいと思います。
この記事がFlet開発において皆さんの一助になれば幸いです。
コメント