[Python][tkinter] 1次元配列でカレンダー表示

作ったプログラムの備忘録

# はじめに
1次元配列でgrid表示させる方法で、カレンダー表示させてみました。

日付ウィジェットの配置は単純化できますが、日祝土曜日の色変化やヘッダー部分など含めると、結果的にウィジェット配置のコードもそこそこ長くなってしまったなと言う感じです。

https://qiita.com/takafi/items/dc2fcf8d5372d631e6b2

![978FBBF0-9043-4DB1-AE36-F02546C489B7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079481/b088e7ed-7042-556d-f826-cefad1421ddd.png)
![43DD3293-F66C-482A-852B-A1D74C490979.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079481/9b7e8dc2-145b-d31d-b4ae-ba1cb6dc0fd4.png)

## 動作テスト環境
OS: Windows 10 Pro 64bit
言語： Python 3.9.13

## 曜日表示
テキストが1文字ずつなので、`f'{"日月火水木金土"[i]}'`とすればループで表示可能です。
土日のみ後から文字色を変更しています。

```Python:
        # 曜日表示
        self.youbi = [tk.Label(self.frame2, text=f'{"日月火水木金土"[i]}') for i in range(7)]
        [self.youbi[i].grid(row=0, column=i, padx=3, pady=5, sticky=tk.NSEW) for i in range(7)]
        self.youbi[0].configure(fg='crimson')
        self.youbi[6].configure(fg='navy')
```

## カレンダー表示
`self.bom`は`def days_setting(self):`で設定したその月の1日の曜日（`isoweekday()`：月曜が1、日曜が7を返す関数）で、その値を足して行番号と列番号を計算することで、表示させています。
日祝判定は`not jpbizday.is_bizday(year, month, day)`で実施し、土曜は列番号から判定させています。

```Python:
        self.calender = []
        for i in range(self.eom):
            tmp = tk.Button(self.frame2, text=f'{i+1}', relief=tk.GROOVE,
                            command=partial(self.click_date, i+1))
            tmp.grid(row=(self.bom + i)//7+1, column=(self.bom + i)%7, padx=3, pady=5, sticky=tk.NSEW)
            if (self.bom + i)%7 == 6:
                tmp.configure(fg='navy')
            elif not jpbizday.is_bizday(datetime.date(self.now_year, self.now_month, i+1)):
                tmp.configure(fg='crimson')
            self.calender.append(tmp)
```



# ソースコード
```Python:
import datetime
import calendar
import jpbizday
import tkinter as tk
from tkinter import messagebox as mb
from functools import partial

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title('calendar')
        self.master.geometry('420x360')

        self.master.grid_rowconfigure(1, weight=1)
        self.master.grid_columnconfigure(0, weight=1)

        # 変数初期値設定
        self.now_year = datetime.datetime.now().year
        self.now_month = datetime.datetime.now().month
        self.days_setting()
        self.create_widgets()

    def create_widgets(self):

        # 年月表示フレーム
        self.frame1 = tk.Frame(self.master)
        self.frame1.grid(row=0, column=0, sticky=tk.NSEW)
        self.frame1.grid_rowconfigure(0, weight=1)
        self.frame1.grid_columnconfigure((0, 4), weight=1)

        # 年月選択ボタン
        self.navi_back = tk.Button(self.frame1, text=f'<', relief=tk.FLAT,
                                    command=lambda: self.month_change('left'))
        self.navi_back.grid(row=0, column=0, padx=(140, 10), pady=10, sticky=tk.E)
        self.navi = [tk.Label(self.frame1, text=f'{self.yml[i]}') for i in range(len(self.yml))]
        [self.navi[i].grid(row=0, column=i+1, pady=10) for i in range(len(self.yml))]
        self.navi_next = tk.Button(self.frame1, text=f'>', relief=tk.FLAT,
                                    command=lambda: self.month_change('right'))
        self.navi_next.grid(row=0, column=4, padx=(10, 140), pady=10, sticky=tk.W)

        # カレンダー表示フレーム
        self.frame2 = tk.Frame(self.master)
        self.frame2.grid(row=1, column=0, padx=10, pady=(0, 10), sticky=tk.NSEW)
        self.frame2.grid_rowconfigure(tuple(range(7)), weight=1)
        self.frame2.grid_columnconfigure(tuple(range(7)), weight=1)

        # 曜日表示
        self.youbi = [tk.Label(self.frame2, text=f'{"日月火水木金土"[i]}') for i in range(7)]
        [self.youbi[i].grid(row=0, column=i, padx=3, pady=5, sticky=tk.NSEW) for i in range(7)]
        self.youbi[0].configure(fg='crimson')
        self.youbi[6].configure(fg='navy')

        # カレンダー表示
        self.calender = []
        for i in range(self.eom):
            tmp = tk.Button(self.frame2, text=f'{i+1}', relief=tk.GROOVE,
                                command=partial(self.click_date, i+1))
            tmp.grid(row=(self.bom + i)//7+1, column=(self.bom + i)%7, padx=3, pady=5, sticky=tk.NSEW)
            if (self.bom + i)%7 == 6:
                tmp.configure(fg='navy')
            elif not jpbizday.is_bizday(datetime.date(self.now_year, self.now_month, i+1)):
                tmp.configure(fg='crimson')
            self.calender.append(tmp)
        # 行数固定用NULLボタン
        tmp = tk.Button(self.frame2, text=' ', relief=tk.FLAT)
        tmp.grid(row=6, column=6, padx=3, pady=5, sticky=tk.NSEW)

    # 年月変更処理
    def month_change(self, direction):
        if direction == 'left':
            self.now_month -= 1
        elif direction == 'right':
            self.now_month += 1
        if self.now_month == 0:
            self.now_year -= 1
            self.now_month = 12
        elif self.now_month == 13:
            self.now_year += 1
            self.now_month = 1
        # 年月変更
        self.days_setting()
        # 画面更新
        self.create_widgets()

    # 年月設定処理
    def days_setting(self):
        # 1日の曜日（beginning of the month）
        self.bom = datetime.datetime(self.now_year, self.now_month, 1).isoweekday()
        # 月末の日付（end of month）
        self.eom = calendar.monthrange(self.now_year, self.now_month)[1]
        # 年月表示用の配列（year, month, list）
        self.yml = [self.now_year, ' / ', self.now_month]

    # 日付クリック時の処理
    def click_date(self, date):
        mb.showinfo('click', f'{self.now_year}/{self.now_month}/{date}')

if __name__ == '__main__':
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

```

# 最後に
マインスイーパーに比べるとあまりすっきり感のないコードになってしまいました。
やはりフレーム、年月選択、曜日表示などループ以外のウィジェットが増えていくと煩雑化します。