Fletを試す(2)-2 - 日付入力コンポーネントを作成してみる

# はじめに

https://qiita.com/donraq/items/043bde2e480d5359261b

↑のコンポーネント化の話に関連して、日付入力コンポーネントを作成してみました。
pythonのclassの書き方がきちんと分かっていないので、その辺は参考程度に見てください。

動作イメージは↓。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/071a9347-96e6-060e-a0aa-468d722c330f.gif)

こちらで実際の動きを見れます。
https://flet-sample-datepicker.fly.dev/

個人的な感想。
- コンポーネント化は、結構、簡単にできるような気がする
- 慣れれば、画面のレイアウト調整も楽なのではないかと思う
- 恐らくもう少し経てば公式のControlがもっと増えていくでしょう

# 前提事項

- 試した環境
    - Windows10
    - Python 3.10.8
    - Flet 0.3.2

# コンポーネントの仕様（ざっくり）

- カレンダー表示をするコンポーネント
- 初期表示は当日を選択した状態とする
- 日付クリックでon_selectedイベント実行する（処理内容は外部から指定してもらう）
- ヘッダに年月と左右の矢印、「今日」ボタンを配置
- 矢印クリックでカレンダーを前月と次月に書き換える
- 「今日」ボタンクリックで当日を選択した状態に戻す

# サンプルコード

コンポーネントとコンポーネント利用のサンプルコードは以下です。

カレンダーアイコンとテキスト入力欄を配置。
その下に作ったコンポーネントを配置。
アイコンクリックでカレンダーを表示／非表示するようにしてあります。
日付を選択したときに、日付をコンポーネントからもらって入力欄に設定し、カレンダーを非表示にしています。

少し変えればポップアップ形式にもできると思います。
実際に利用するには、入力欄＋カレンダーでコンポーネントにしてしまう方が良い気がします。

```python
import flet as ft
import datetime
from dateutil.relativedelta import relativedelta
import calendar
import itertools


class DatePiker(ft.UserControl):
    """日付選択"""

    def __init__(self, on_selected=None):
        super().__init__()
        # 初期値の日付
        self.default_date = datetime.date.today()
        # カレンダー表示年月
        self.yearmonth: datetime = datetime.date(
            self.default_date.year, self.default_date.month, 1)
        # 選択している日付
        self.selected_date: datetime = self.default_date
        # 日付選択時の処理
        self.on_selected = on_selected

    def build(self):
        DAY_WIDTH = 36
        DAY_SPACING = 4

        def updateCalender():
            """カレンダーの中身を更新する"""

            # ヘッダの年月設定
            self.txt_yearmonth.value = self.yearmonth.strftime("%Y/%m")

            # 日付のボタンを全部クリア
            for idx in range(42):  # 7日×6週
                self.btn_days[idx].text = "-"
                self.btn_days[idx].disabled = True
                self.btn_days[idx].style = ft.ButtonStyle(
                    padding=0, bgcolor=ft.colors.BACKGROUND)

            # カレンダー情報取得
            list_cal = list(
                itertools.chain.from_iterable(
                    calendar.monthcalendar(self.yearmonth.year, self.yearmonth.month)))

            # カレンダー情報を日付のボタンに設定
            idx = 0
            for day in list_cal:
                if day > 0:
                    self.btn_days[idx].text = day
                    self.btn_days[idx].disabled = False
                    if datetime.date(self.yearmonth.year, self.yearmonth.month, day) == self.selected_date:
                        self.btn_days[idx].style = ft.ButtonStyle(
                            padding=0, bgcolor=ft.colors.TRANSPARENT)
                idx = idx + 1

        def prev_clicked(e):
            self.yearmonth = self.yearmonth - relativedelta(months=1)
            updateCalender()
            self.update()

        def next_clicked(e):
            self.yearmonth = self.yearmonth + relativedelta(months=1)
            updateCalender()
            self.update()

        def today_clicked(e):
            self.yearmonth = datetime.date.today()
            self.selected_date = datetime.date.today()
            updateCalender()
            self.update()

        def day_clicked(e):
            day = e.control.text
            self.selected_date = datetime.date(
                self.yearmonth.year, self.yearmonth.month, day)
            updateCalender()
            self.update()
            if self.on_selected:
                self.on_selected()

        # I/O Controls
        self.btn_prev = ft.IconButton(
            icon=ft.icons.ARROW_BACK, on_click=prev_clicked
        )
        self.btn_next = ft.IconButton(
            icon=ft.icons.ARROW_FORWARD, on_click=next_clicked
        )
        self.txt_yearmonth = ft.Text(
            self.yearmonth.strftime("%Y/%m"), size=24, weight=ft.FontWeight.BOLD
        )
        self.btn_today = ft.ElevatedButton(
            "今日", on_click=today_clicked
        )
        self.btn_days = []
        week_cols = []
        for week in range(6):
            # 6週分の行を追加
            calenderRows = []
            for day in range(7):
                # 7日分のボタンを追加
                btn_day = ft.ElevatedButton(
                    width=DAY_WIDTH, style=ft.ButtonStyle(padding=0), on_click=day_clicked)
                self.btn_days.append(btn_day)
                calenderRows.append(btn_day)
            week_cols.append(
                ft.Row(
                    controls=calenderRows,
                    alignment=ft.MainAxisAlignment.CENTER,
                    spacing=DAY_SPACING,
                )
            )
        updateCalender()

        return ft.Column(
            [
                # ヘッダ
                ft.Row(
                    [
                        self.btn_prev, self.txt_yearmonth, self.btn_next, self.btn_today,
                    ],
                    alignment=ft.MainAxisAlignment.CENTER,
                ),
                # カレンダーヘッダ
                ft.Row(
                    [
                        ft.Text("月", width=DAY_WIDTH,
                                text_align=ft.TextAlign.CENTER),
                        ft.Text("火", width=DAY_WIDTH,
                                text_align=ft.TextAlign.CENTER),
                        ft.Text("水", width=DAY_WIDTH,
                                text_align=ft.TextAlign.CENTER),
                        ft.Text("木", width=DAY_WIDTH,
                                text_align=ft.TextAlign.CENTER),
                        ft.Text("金", width=DAY_WIDTH,
                                text_align=ft.TextAlign.CENTER),
                        ft.Text("土", width=DAY_WIDTH,
                                text_align=ft.TextAlign.CENTER, color=ft.colors.BLUE),
                        ft.Text("日", width=DAY_WIDTH,
                                text_align=ft.TextAlign.CENTER, color=ft.colors.RED),
                    ],
                    alignment=ft.MainAxisAlignment.CENTER,
                    spacing=DAY_SPACING,
                ),
                # カレンダー内容
                ft.Row(
                    [
                        ft.Column(
                            controls=week_cols,
                        )
                    ],
                    alignment=ft.MainAxisAlignment.CENTER,
                )
            ]
        )

    def update(self):
        super().update()


def main(page: ft.Page):

    def btn_calender_clicked(e):
        card.visible = not card.visible
        page.update()

    def date_selected():
        tf_date.value = datepiker.selected_date.strftime("%Y/%m/%d")
        card.visible = False
        page.update()

    # I/O Controls
    tf_date = ft.TextField()
    btn_calender = ft.IconButton(
        icon=ft.icons.CALENDAR_TODAY, on_click=btn_calender_clicked)
    datepiker = DatePiker(on_selected=date_selected)
    card = ft.Card(
        ft.Container(
            datepiker,
            margin=10,
            width=300,
        ),
        visible=True,
    )

    # Page レイアウト
    page.theme_mode = ft.ThemeMode.LIGHT
    page.add(
        ft.Column(
            [
                ft.Row(
                    [
                        btn_calender,
                        tf_date,
                    ]
                ),
                card,
                ft.Text("↑日付をカレンダーで入力できる"),
            ]
        )
    )


if __name__ == "__main__":
    ft.app(target=main)
```