【Flet】2つのドロップダウンを連動させる

Fletで2つのドロップダウンを連携させる方法について書きます。
1つ目のドロップダウンで選択したものに応じて2つ目のドロップダウンの選択肢が変化します。

動作イメージはこんな感じです。
![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960906/056abf6c-fb0e-d0ab-6298-5948b6d6b0d9.gif)

Fleについてはこちらの記事がわかりやすいです。

https://qiita.com/NasuPanda/items/48849d7f925784d6b6a0

ドロップダウンの公式リファレンスはこちら

https://flet.dev/docs/controls/dropdown

この記事は自分の備忘録に近いのでご容赦ください。

# コード
やや長いので分割して説明を書いていきます。
最後にコード全体を示します。

ざっくり処理の流れを書くと、
1.親ドロップダウンが選択される
↓
2.表示内容をすべて削除
↓
3.子ドロップダウンの選択肢を生成
↓
4.表示内容を生成
↓
5.ページの表示を更新する
↓
1に戻る
この繰り返しになります。

どうやらドロップダウンの選択肢はそのドロップダウンが生成されたときの内容で固定されるので、選択肢を更新するために表示内容ごと一度削除しています。
理想的なのは子ドロップダウンのみ削除、再生成することですが特定のコントロールのみを指定して削除することができなかったのでこの形になりました。


## ドロップダウンの作成

最初に表示するドロップダウンの作成を行います
。
この時、一緒にドロップダウンの選択肢である辞書を作成します。
今回は辞書を使いましたが`list`や`pandas.DataFrame`でも同様のことができると思います。

ここからは、便宜上最初に選択するドロップダウンを親、2つ目に選択するドロップダウンを子として表記します。

```py
import flet as ft #fletをインポート

class Display(ft.UserControl):　#コンポーネントの作成
    def __init__(self, page:ft.Page):
        super().__init__() #コンポーネント化に必要
        self.page = page #page情報の読み込み

    
    def build(self):
        # 選択肢の辞書を作成
        self.options_dict = { 
            "大文字":["A","B","C"],
            '小文字': ["a","b","c"],
            '数字': [1,2,3]
        }

        # 親のドロップダウンの作成
        self.select_key = ft.Dropdown(
            value =None, #初期値
            on_change=self.on_change_key, #選択肢が選択されたときの動作
            options=self.select_key_options() #親の選択肢
        )
        
        #子のドロップダウンの作成
        self.select_item = ft.Dropdown(
            value = None, #初期値
            options= self.select_item_options(self.select_key.value) #子の選択肢
        )

        #表示内容
        self.view=ft.Column(
            controls=[
                self.select_key,
                self.select_item
            ]
        )

        return self.view
```
ドロップダウンについて
・初期値は選択肢にあるものならNone以外でもOK
・親の`on_change`は必ず必要。子の選択肢の生成と表示の更新を行う`on_change_key`を指定している
・親の`options`は公式リファレンスのようにべた書きでもOKだが、今回は辞書から選択肢を生成するため関数を指定している
・詳しくは後述するが、ページの削除と生成が繰り返されるので`options`には必ず選択肢を生成する関数の返り値を指定する

表示内容について
・ページの削除と生成を繰り返すために一度変数`view`にいれてから、その変数を返すという書き方にしています

## ドロップダウンが変更されたときの処理
先ほどのドロップダウンの`on_change`や`options`に指定した関数が後に続きます。
これらの関数は`class`の中に入っています。
```py
    def on_change_key(self,e): #親の選択肢が変更されたときの関数
        self.page.controls.pop() #ページの内容をすべて削除
        self.select_item.options =self.select_item_options(self.select_key.value) #子の選択肢を生成されたものに変更
        self.page.controls.append(self.view) #ページの内容を再生成
        self.page.update() #ページの表示を更新

    def select_key_options(self): #親の選択肢を生成する関数
        key_list = list(self.options_dict.keys()) #辞書からキーのみを取り出す
        key_option_list = [] #選択肢を入れるリストを用意
        for i in key_list:
            key_option_list.append(ft.dropdown.Option(text=i)) #選択肢を生成
        return key_option_list #選択肢のリストを返す

    def select_item_options(self,key_value): #子の選択肢を生成する関数、引数は親で選択された値
        item_option_list = [] #選択肢を入れるリストを生成
        if key_value != None: #親が選択されている時
            item_list = self.options_dict[key_value] #辞書から選択肢の名前を取得
            for i in item_list:
                item_option_list.append(ft.dropdown.Option(text=i)) #選択肢を生成
        return item_option_list
```
前述したようにドロップリストの選択肢は生成時に固定されるようなので、子の選択肢を更新した後は必ずページの削除と再生成が必要です。上に提示したコードでは削除→更新→再生成の順番ですが、更新→削除→再生成でも問題なくできました。

## コードの全体
このコードをコピペすれば最初に示したGIFのように動作するはずです。
```py
import flet as ft

class Display(ft.UserControl):
    def __init__(self, page:ft.Page):
        super().__init__()
        self.page = page

    
    def build(self):
        self.options_dict = {
            "大文字":["A","B","C"],
            '小文字': ["a","b","c"],
            '数字': [1,2,3]
        }

        self.select_key = ft.Dropdown(
            value ="大文字",
            on_change=self.on_change_key,
            options=self.select_key_options()
        )

        self.select_item = ft.Dropdown(
            value = None,
            options= self.select_item_options(self.select_key.value)
        )


        self.view=ft.Column(
            controls=[
                self.select_key,
                self.select_item
            ]
        )

        return self.view

    def on_change_key(self,e):
        self.select_item.options =self.select_item_options(self.select_key.value)
        self.page.controls.pop()
        self.page.controls.append(self.view)
        self.page.update()

    def select_key_options(self):
        key_list = list(self.options_dict.keys())
        key_option_list = []
        for i in key_list:
            key_option_list.append(ft.dropdown.Option(text=i))
        return key_option_list

    def select_item_options(self,key_value):
        key_value = self.select_key.value
        item_option_list = []
        if key_value != None:
            item_list = self.options_dict[key_value]
            for i in item_list:
                item_option_list.append(ft.dropdown.Option(text=i))
        return item_option_list
        

def main(page:ft.Page):
    page.add(Display(page))

ft.app(target=main)
```

また何かわかったことがあれば追記したいと思います。（特に表示内容の削除周り）