tkinterでカレンダー選択から日付情報を得るダイアログを作成する。

# Abstract
pythonで簡単なGUIを作成する機会があり、その際に日付の入力欄を作る必要があった。要件としては、入力欄は基本的には手打ちだが、カレンダー選択による自動入力機能を付けてほしいとうことだった。Webのノリで簡単にできるだろうと高をくくっていたら意外とはまってしまったので、誰の役に立つかわからないが残しておくことにする。

# 環境
下記環境で実装してみる。
- python 3.12.5
- Windows10, 11

下記の外部ライブラリを使用しました
- tkcalendar==1.6.1
# 要件
再利用性を考え下記の簡易要件でクラスを設計する。
- tkinterで呼び出すダイアログ
- ダイアログにカレンダーを表示する
- カレンダーが選択されたか確認するメンバーを有す
- カレンダー選択結果をstringsで取り出すメンバーを有す
- カレンダー選択結果をdatetimeで取り出すメンバーを有す

# 実装

~~~python: calendar_dialog.py
import tkinter
from tkinter.simpledialog import Dialog
from typing import override
from datetime import datetime
from tkcalendar import Calendar


class CalendarDialog(Dialog):
    __date: str
    __calendar : Calendar
    def __init__(self, master : tkinter.Tk, title=None) -> None:
        """カレンダーを選択し日付を得るダイアログを生成する

        Args:
            master (tkinter.Tk): 配置するコンポーネント
            title (_type_, optional): _description_. Defaults to None.
        """
        self.__date = ""
        self.__calendar = None
        super().__init__(parent=master, title=title)
        
    @override
    def body(self, master : tkinter.Tk) -> None:
        """ダイアログ内に配置するコンポーネント.今回はカレンダーを表示する。

        Args:
            master (tkinter.Tk): 配置するコンポーネント
        """
        self.__calendar = Calendar(master, showweeknumbers=False,date_pattern="yyyy/mm/dd")
        self.__calendar.grid(sticky="w", row=0, column=0)
        
    @override
    def apply(self) -> None:
        """OKが押された時の処理
        """
        self.__date = self.__calendar.get_date()
        
    def __str__(self) -> str:
        return self.to_str()
    
    def __repr__(self) -> str:
        return str(self)

    def to_str(self) -> str:
        """日付をで得る

        Returns:
            str: 日付
        """
        return self.__date

    def to_datetime(self) -> datetime:
        """日付を得る

        Returns:
            datetime: 日付
        """
        return datetime.strptime(r"%Y/%m/%d")

    def is_selected(self) -> bool:
        """カレンダーが選択されたか"""
        return self.__date != ''
~~~
いろいろ方法はあると思うが、既存のダイアログをカスタマイズして、カレンダーから情報を返す方法が簡単そうだったので今回はこの方法をとる。
具体的には`tkinter.simpledialog.Dialog`を継承し、下記メンバを2つオーバライドすることで簡単に実現できる。

- `body()` ダイアログの本体を表示するメソッド
- `apply()` OKボタンが押されたときに実行するメソッド

`body()`でカレンダーを表示させ、OKボタンをトリガーに日付の値を`self.__date`メンバーへ格納する。
公式いわくOKやCancelの表示が気に食わないひとはbuttonbox()をオーバーライドしてカスタマイズしてとのこと。

# テスト
トップレベルにモジュールとtestコードを置くと仮定します。

```
    calendar_dialog.py
    test.py
```
~~~python:test.py
import tkinter
from tkinter import Frame
from tkinter import Label
from tkinter import Entry
from tkinter import Button

from calendar_dialog import CalendarDialog


class CalendarDialogTest(tkinter.Tk):
    __entry : Entry
    def __init__(self, screenName: str | None = None, baseName: str | None = None, className: str = "Tk", useTk: bool = True, sync: bool = False, use: str | None = None) -> None:
        super().__init__(screenName, baseName, className, useTk, sync, use)
        self.geometry("200x200")
        fream = Frame(self, bd=4, relief="groove")
        label = Label(fream, text="Date")
        label.grid(padx=2, sticky="w", row=0, column=0)
        self.__entry = Entry(fream, width=20)
        self.__entry.grid(sticky="w", row=1, column=0)
        button = Button(fream,text="get",width=4,command=self.__get_date)
        button.grid(sticky="w", row=1, column=1)
        fream.pack()

    def __get_date(self) -> None:
        """コールバック"""
        dialog = CalendarDialog(self, title="calendar")
        if dialog.is_selected():
            self.__entry.delete(0, tkinter.END)
            self.__entry.insert(0,dialog.to_str())
            

if __name__ == '__main__':
    CalendarDialogTest().mainloop()
    
~~~
実行可能なテストコードを示す。
手打ち入力とカレンダー入力に対応した入力欄を作ることができた。

![CalenderDialog.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3136817/4a01e98d-5fbb-6a47-0ac1-56320162404c.gif)



# 参考
[Tkinter ダイアログ](https://docs.python.org/ja/3/library/dialog.html#module-tkinter.simpledialog)