FletでGUIアプリを作成してみた

# はじめに
お久しぶりです。
記事を書くのに、かなり時間が空きました。
ずっとフロントエンドの勉強をしていて、JavascriptやReactをやっていました。
勉強したことをまとめるか～って感じで考えていたんですが、まだまだ理解できていない部分が多いので、もう少し勉強してまとめたいと思います。

先日、幕張メッセで**AWS Summit 2024**が開催され、参加してきましたが、最先端の技術に触れかなりモチベが上がったため、何か作るか～からの今回の記事となります。
今回は、**Flet**というPythonのGUIライブラリを用いたアプリを作成したのでまとめていきたいと思います。

![IMG_8042.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/1a5de04f-434a-3d3d-9614-b7fb49d613f6.jpeg)

# Fletとは
Fletは、2022年に登場した`Flutter`をベースとして開発されたGUIフレームワークです。
GUIアプリだけではなく、アプリの起動の仕方によってはWebアプリケーションとしても使えます。
さらにFlutterをもとに開発されているので、モバイルアプリにもできるとかなんとか…

PythonでGUIアプリを開発する際には、`Tkinter`や`Kivy`などのライブラリを用いた方も多くいるかと思います。
ただ、個人的な意見としてTkinterやKivyは「**見た目がダサい**」の一言に限ります。
もっとモダンな見た目がいいなーとずっと思っていたので、気になってました。
他にも、レスポンシブなレイアウトを作成したり、ウィジェットが豊富なため複雑なGUI設計に向いていたりと優れた点が多いです。

https://flet.dev/

# Fletの基礎
Fletは`Page`という大きな枠組みの中に、`Control`という機能を組み合わせていくことで構築していきます。
HTMLでいうdivタグの中に`button`タグや`input`タグを入れていく感じですね。
フロントエンド開発をやっている人からするとコンポーネントといった単語の方が聞き慣れているかと思いますが、Fletではコントロールと言います。
また、それらを`Row`を使用して横に並べることもできます。
このようにしてアプリを構築していくようですね。

![flet.excalidraw.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/d7f41999-0b2c-8ca1-5044-fceefdcc699f.png)


いたってシンプルな構造かと思います。

# Fletの始め方
Fletは以下のコマンドでインストールします。
Pythonのバージョンは3.7以上でないと対応していないみたいです。

```
pip install flet
```

インストールが完了したら、適当なファイルを作成して以下のコードを入力してみてください。

```python
import flet as ft

def main(page: ft.Page):
    page.add(ft.Text("Hello, World!"))

ft.app(target=main)
```

このコードを実行すると、**Hello World!** と表示されたかと思います。
main関数を定義しますが、これが`Page`の部分にあたります。
この中に`Control`を書いていく感じですね。
上記のサンプルでは、`page.add(ft.Text("Hello, World!"))`の部分でテキストコントロールをPageに追加しています。
この部分は以下のようにも書けます。
```python
import flet as ft

def main(page: ft.Page):
    page.controls.append(ft.Text("Hello, World!"))
    page.update()

ft.app(target=main)
```
コードが長くなっただけですね。
基本的にaddメソッドを使用していいと思います。

最後に、appメソッドにmainを渡せばOKです。
また、Streamlitなどのフレームワークとは違い、コマンドを入力せずにPythonファイルを実行するだけでGUIアプリが起動するのも便利ですね。

# データ可視化アプリの作成
では、実際に適当なアプリケーションを作成してみましょう。
実際に業務で使用しているTkinterのGUIアプリが起動するまで遅く、使いづらいなと感じていたので似たような機能を実装してみました。
今回はcsvファイルを読み込んで、指定したデータの散布図を作成して、ディレクトリに保存するだけの簡単な機能です。

まずは必要なライブラリをインポートしましょう。

```python
import flet as ft
from flet.matplotlib_chart import MatplotlibChart
import pandas as pd
import matplotlib.pyplot as plt
import os
from pathlib import Path
import re
```
fletでmatplotlibのチャートを表示するには`from flet.matplotlib_chart import MatplotlibChart`というコードを追加する必要があるみたいです。
Plotlyのチャートを表示したい場合も、同じようなライブラリがあるようです。目的に応じて必要なライブラリをインポートしてください。

では、main関数を定義していきます。

```python
def main(page: ft.Page):
    page.title = "CSV Scatter Plot Generator"
    page.padding = 20

    status_text = ft.Text()
    plot_container = ft.Container(visible=False)
```

TitleやPaddingはPage部分の設定みたいなもので、実際に表示されるものではないです。
2つのコントロールを定義しました。
これらは後にaddメソッドでPageに追加していきます。

それでは、ファイルを読み込んだ際の処理を書いていきます。
```python
def on_file_selected(e: ft.FilePickerResultEvent):
    if e.files:
        file_path = e.files[0].path
        try:
            status_text.value = "Generating graph..."
            page.update()

            fig, plot_path = create_scatter_plot(file_path)
            
            plot_container.content = MatplotlibChart(fig)
            plot_container.visible = True
            status_text.value = f"Scatter Plot has been saved: {plot_path}"
        except Exception as ex:
            status_text.value = f"Error: {str(ex)}"
        
        page.update()
```

今回は実際に業務で使用するので、例外処理も含めて書いています。
ファイルがアップロードされたときに`ft.FilePickerResultEvent`というイベントが発生します。
そのイベントをもとに処理を書いていきます。
ここでは、別で用意する`create_scatter_plot`関数から出力される結果を`MatplotlibChart`メソッドを用いてグラフを表示させています。
厳密には`plot_container.visible = True`で表示させています。
その後、グラフの保存に成功した際に表示させる文字列を書いています。

では、グラフを作成する関数を書いていきます。

```python
def create_scatter_plot(csv_path):
    try:
        df = pd.read_csv(csv_path)
        fig, ax = plt.subplots(figsize=(10, 8))
        ax.scatter(df.iloc[:, 0], df.iloc[:, 5])
        
        csv_path = Path(csv_path).resolve()
        safe_filename = re.sub(r'[^\w\-_\.]', '_', csv_path.stem)
        graph_filename = safe_filename + ".png"

        plot_path = os.path.join(os.path.dirname(csv_path), graph_filename)
        fig.savefig(plot_path)
        
        return fig, str(plot_path)
    except Exception as e:
        raise ValueError(f"Error: {str(e)}")
```

基本的にはいつも通りにPandasとmatoplotlibを用いた処理を書いていきます。
また、今回はグラフを作成した時点で、読み込んだcsvファイルと同じディレクトリにグラフを保存したいので、その処理を書いています。
今回は、指定するデータを固定していますが、ユーザビリティを高めるために、ドロップボックスを用意して、表示させたいデータを選択できるようなGUIを実装するのもいいかもしれませんね。

最後に、ファイル読み込むためのボタンと、そのボタンにファイルを読み込ませる機能を持たせるコードを書いて、Pageに追加しましょう。

```python
file_picker = ft.FilePicker(on_result=on_file_selected)
page.overlay.append(file_picker)

page.add(
    ft.ElevatedButton("File Upload", on_click=lambda _: file_picker.pick_files(allowed_extensions=["csv"])),
    status_text,
    plot_container
)

ft.app(target=main)
```
ファイルピッカーを`overlay`メソッドを用いて追加していますが、addメソッドとは少し異なります。
似たような挙動ではありますが、ログやファイル選択、ポップアップメニューを表示するときなど、一時的なUI要素を構築する際に、Page上に表示するために使用されるようです。
詳細については調べてみてください。

それでは、Pythonファイルを実行してみてください。

![flet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/09282915-22ed-c698-b90d-2c333bfbdbfd.png)

ファイルをアップロードするためのボタンがあるので押下してcsvファイルを読み込んでください。
今回はタイタニックのデータを読み込ませました。
すると以下のように散布図が表示されたかと思います。

![flet2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/7664a939-83d9-4724-813a-5920df9452e1.png)

また、読み込ませたcsvファイルのディレクトリ内にグラフが画像ファイルとして保存されているかと思います。

以下に全体のコードを記載します。
<details><summary>全体コード</summary>

```python
import flet as ft
from flet.matplotlib_chart import MatplotlibChart
import pandas as pd
import matplotlib.pyplot as plt
import os
from pathlib import Path
import re

def main(page: ft.Page):
    page.title = "CSV Scatter Plot Generator"
    page.padding = 20

    status_text = ft.Text()
    plot_container = ft.Container(visible=False)


    def on_file_selected(e: ft.FilePickerResultEvent):
        if e.files:
            file_path = e.files[0].path
            try:
                status_text.value = "Generating graph..."
                page.update()

                fig, plot_path = create_scatter_plot(file_path)
                
                # MatplotlibChartウィジェットを使用してグラフを表示
                plot_container.content = MatplotlibChart(fig)
                plot_container.visible = True
                status_text.value = f"Scatter Plot has been saved: {plot_path}"
            except Exception as ex:
                status_text.value = f"Error: {str(ex)}"
            
            page.update()
            
    def create_scatter_plot(csv_path):
        try:
            # CSVファイルを読み込む
            df = pd.read_csv(csv_path)
            # 散布図を作成
            fig, ax = plt.subplots(figsize=(10, 8))
            ax.scatter(df.iloc[:, 0], df.iloc[:, 5])

            csv_path = Path(csv_path).resolve()
            safe_filename = re.sub(r'[^\w\-_\.]', '_', csv_path.stem)
            graph_filename = safe_filename + ".png"

            plot_path = os.path.join(os.path.dirname(csv_path), graph_filename)
            fig.savefig(plot_path)
            
            return fig, str(plot_path)
        except Exception as e:
            raise ValueError(f"Error: {str(e)}")

    file_picker = ft.FilePicker(on_result=on_file_selected)
    page.overlay.append(file_picker)

    page.add(
        ft.ElevatedButton("File Upload", on_click=lambda _: file_picker.pick_files(allowed_extensions=["csv"])),
        status_text,
        plot_container
    )

ft.app(target=main)
```

</details>

# まとめ
お疲れ様でした。
いかがだったでしょうか。
TkinterやKivyよりもモダンな見た目になり、読み込みなどもスムーズで使いやすい印象です。
個人的にはGUIアプリはFletを使っていこうかなと思います。
この記事がこれからFletを使っていこう、気になっているという方の参考になれば幸いです。

最近は記事を書くのをサボっていましたが、ちょくちょく勉強した内容をまとめたり、有益な情報などを紹介したりしようと思います！
それでは！！！