TKinterで複数の日時が選択可能なカレンダーを作る

## 0. 初めに
みなさん、TKinterしてますか？
僕はしてます
TKinterとはPythonの標準ライブラリで簡単にGUIアプリを作成することができます
細かいチュートリアルなどはこちらが分かりやすかったので参考にさせていただきました
https://qiita.com/nnahito/items/ad1428a30738b3d93762

## 1. なぜ作ったか
これはある日僕が仕事での業務アプリを作っている時のこと、カレンダーのポップアップが出てきて日付を複数選択するUIを作る必要が出てきました
色々調べた結果、TKinterにはカレンダーを表示するライブラリがありました。
しかし、そちらを使おうとするとどうしても複数選択にバインドすることができない。
ないなら自分で作れ。エンジニアとしての本能が僕を駆り立てて今回作成にあたりました。

## 2. 実装

``` Python
import tkinter as tk
import calendar
from datetime import datetime, timedelta



def get_dates_and_weekdays(year, month):
    # 月の最初の日を取得
    start_date = datetime(year, month, 1)
    # 翌月の最初の日を計算し、その1日前（月の最後の日）を取得
    if month == 12:
        end_date = datetime(year + 1, 1, 1) - timedelta(days=1)
    else:
        end_date = datetime(year, month + 1, 1) - timedelta(days=1)

    # 月の全ての日付と曜日を含むリストを作成
    dates_and_weekdays = []
    current_date = start_date
    while current_date <= end_date:
        weekday_name = calendar.day_name[current_date.weekday()]
        dates_and_weekdays.append((current_date.strftime("%Y-%m-%d"), weekday_name))
        current_date += timedelta(days=1)

    return dates_and_weekdays

    

class MultiSelectCalendar(tk.Frame):

    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        self.pack()
        self.create_grid()
        self.create_widgets()
        self.date_list = []
        
    def create_grid(self):
        self.grid_rowconfigure(0, weight=1)
        self.grid_rowconfigure(1, weight=4)
        self.grid_rowconfigure(2, weight=1)

    def create_widgets(self):
        self.top = tk.Frame(self)
        self.current_year = datetime.now().year
        self.current_month = datetime.now().month
        self.current_day = datetime.now().day
        self.year_choose = tk.IntVar()
        self.month_choose = tk.IntVar()
        self.year_choose.set(self.current_year)
        self.month_choose.set(self.current_month)
        self.year_spinbox = tk.Spinbox(self.top, from_=self.current_year-5, to=self.current_year+10, textvariable=self.year_choose, width=5,command =self.fill_day)

        self.year_spinbox.pack(side="left")
        self.year_label = tk.Label(self.top, text="年")
        self.year_label.pack(side="left")
        self.month_spinbox = tk.Spinbox(self.top, from_=1, to=12, textvariable=self.month_choose, width=5,command = self.fill_day)
        self.month_spinbox.pack(side="left")
        self.month_label = tk.Label(self.top, text="月")
        self.month_label.pack(side="left")
        self.top.grid(row=0, column=0, sticky="nsew")
        self.calender = tk.Canvas(self, width=500, height=500)
        self.calender.bind("<Button-1>", self.select_day)
        self.create_calender()
        self.fill_day()
        self.calender.grid(row=1, column=0, sticky="nsew")
        self.exit_button = tk.Button(self, text="閉じる", command=self.close)
        self.exit_button.grid(row=2, column=0, sticky="nsew")
        

    def close(self):
        self.master.destroy()
    

    def create_calender(self):
        self.calender.create_rectangle(0, 0, 490, 490, fill="white")
        self.calender.create_line(0, 70, 490, 70)
        self.calender.create_line(0, 140, 490, 140)
        self.calender.create_line(0, 210, 490, 210)
        self.calender.create_line(0, 280, 490, 280)
        self.calender.create_line(0, 350, 490, 350)
        self.calender.create_line(0, 420, 490, 420)
        self.calender.create_line(70, 0, 70, 490)
        self.calender.create_line(140, 0, 140, 490)
        self.calender.create_line(210, 0, 210, 490)
        self.calender.create_line(280, 0, 280, 490)
        self.calender.create_line(350, 0, 350, 490)
        self.calender.create_line(420, 0, 420, 490)
        self.calender.create_text(35, 35, text="日", font=("Helvetica", 20), fill="red")
        self.calender.create_text(105, 35, text="月", font=("Helvetica", 20))
        self.calender.create_text(175, 35, text="火", font=("Helvetica", 20))
        self.calender.create_text(245, 35, text="水", font=("Helvetica", 20))
        self.calender.create_text(315, 35, text="木", font=("Helvetica", 20))
        self.calender.create_text(385, 35, text="金", font=("Helvetica", 20))
        self.calender.create_text(455, 35, text="土", font=("Helvetica", 20), fill="blue")
    



    def fill_day(self):
        self.calender.delete("all")
        self.create_calender()
        self.year = int(self.year_spinbox.get())
        self.month =int(self.month_spinbox.get())

        day_list = get_dates_and_weekdays(self.year,self.month)

        week_count = 0
        for day in day_list:
            if day[1] == "Sunday":
                self.calender.create_text(35, 105+70*week_count, text=day[0][8:], font=("Helvetica", 20), fill="red")
            if day[1] == "Monday":
                self.calender.create_text(105, 105+70*week_count, text=day[0][8:], font=("Helvetica", 20))
            if day[1] == "Tuesday":
                self.calender.create_text(175, 105+70*week_count, text=day[0][8:], font=("Helvetica", 20))
            if day[1] == "Wednesday":
                self.calender.create_text(245, 105+70*week_count, text=day[0][8:], font=("Helvetica", 20))
            if day[1] == "Thursday":
                self.calender.create_text(315, 105+70*week_count, text=day[0][8:], font=("Helvetica", 20))
            if day[1] == "Friday":
                self.calender.create_text(385, 105+70*week_count, text=day[0][8:], font=("Helvetica", 20))
            if day[1] == "Saturday":
                self.calender.create_text(455, 105+70*week_count, text=day[0][8:], font=("Helvetica", 20), fill="blue")
                week_count += 1
            



    def choose_day(self,x,y):
        x = x//70
        y = y//70-1
        day = self.calender.itemcget(self.calender.find_closest(x*70+35, y*70+105)[0], "text")
        if not day.isdigit():
            return
        date = f"{self.year}-{self.month}-{day:02}"
        if date in self.date_list:
            self.date_list.remove(date)
            self.calender.itemconfig(self.calender.find_closest(x*70+35,y*70+105)[0], fill = "black")
        else:
            self.date_list.append(date)
            self.calender.itemconfig(self.calender.find_closest(x*70+35,y*70+105)[0], fill = "green")
        print(self.date_list)
        return self.date_list
    



    def select_day(self, event):
        self.choose_day(event.x, event.y)
        print(sorted(self.date_list))
```
クラスにて定義してます。必要なさいにインスタンス生成するとポップアップが表示されます

```py
root = tk.Tk()
app = MultiSelectCalendar(root)
root.mainloop()
```

## 3. 結果
使用イメージです。
![スクリーンショット 2024-01-17 11.10.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526661/f8a29ea7-7129-2ee8-8e38-a78b60533bfa.png)
選択した日付の文字は緑に表示されています。
上のスピンボックスにて年、月を指定します。

## 4. 感想
今回初めてGUIアプリを設計、実装しています。作る側になって初めて優れたUIとは何か、という視点を獲得できました。説明書なしでも簡単に扱えるアプリ、そんなアプリを作れるように日々邁進していきます