memopy

pythonで作ってみました的なブログ

python tkinterでカレンダーを作成する(カレンダー編②)

python tkinterでカレンダーを作成する(カレンダー編②)

f:id:memopy:20170615180348p:plain
今回はメインとなるカレンダーのGUIから作成する。tkinterにはカレンダーウィジェットというものは存在しないため、ウィジェットを組み合わせてカレンダーを作っていく必要がある。
今回はメインとなる各日付を表示するウィジェットにはボタンタイプを採用した。これにより、日付をクリックしてスケジュールの登録を可能にする。(ラベルタイプでもできるが、「押した感」を出せるのはボタンタイプとなる。)
その他、ラベルと、それらを格納するフレームを上図のとおり配置する。

スクリプト全文

※python3で作成
(python2では、インポートするモジュールをimport Tkinter as tkとする。以下、同じ。)

# -*- coding:utf-8 -*-

import tkinter as tk

# カレンダーを作成するフレームクラス
class mycalendar(tk.Frame):
    def __init__(self,master=None,cnf={},**kw):
        "初期化メソッド"
        import datetime
        tk.Frame.__init__(self,master,cnf,**kw)
        
        # 現在の日付を取得
        now = datetime.datetime.now()
        # 現在の年と月を属性に追加
        self.year = now.year
        self.month = now.month

        # frame_top部分の作成
        frame_top = tk.Frame(self)
        frame_top.pack(pady=5)
        self.previous_month = tk.Label(frame_top, text = "<", font = ("",14))
        self.previous_month.pack(side = "left", padx = 10)
        self.current_year = tk.Label(frame_top, text = self.year, font = ("",18))
        self.current_year.pack(side = "left")
        self.current_month = tk.Label(frame_top, text = self.month, font = ("",18))
        self.current_month.pack(side = "left")
        self.next_month = tk.Label(frame_top, text = ">", font = ("",14))
        self.next_month.pack(side = "left", padx = 10)

        # frame_week部分の作成
        frame_week = tk.Frame(self)
        frame_week.pack()
        button_mon = d_button(frame_week, text = "Mon")
        button_mon.grid(column=0,row=0)
        button_tue = d_button(frame_week, text = "Tue")
        button_tue.grid(column=1,row=0)
        button_wed = d_button(frame_week, text = "Wed")
        button_wed.grid(column=2,row=0)
        button_thu = d_button(frame_week, text = "Thu")
        button_thu.grid(column=3,row=0)
        button_fri = d_button(frame_week, text = "Fri")
        button_fri.grid(column=4,row=0)
        button_sta = d_button(frame_week, text = "Sat", fg = "blue")
        button_sta.grid(column=5,row=0)
        button_san = d_button(frame_week, text = "San", fg = "red")
        button_san.grid(column=6,row=0)

        # frame_calendar部分の作成
        self.frame_calendar = tk.Frame(self)
        self.frame_calendar.pack()

        # 日付部分を作成するメソッドの呼び出し
        self.create_calendar(self.year,self.month)

    def create_calendar(self,year,month):
        "指定した年(year),月(month)のカレンダーウィジェットを作成する"
        
        # calendarモジュールのインスタンスを作成
        import calendar
        cal = calendar.Calendar()
        # 指定した年月のカレンダーをリストで返す
        days = cal.monthdayscalendar(year,month)

        # 日付ボタンを格納する変数をdict型で作成
        self.day = {}
        # for文を用いて、日付ボタンを生成
        for i in range(0,42):
            c = i - (7 * int(i/7))
            r = int(i/7)
            try:
                # 日付が0でなかったら、ボタン作成
                if days[r][c] != 0:
                    self.day[i] = d_button(self.frame_calendar,text = days[r][c])
                    self.day[i].grid(column=c,row=r)
            except:
                """
                月によっては、i=41まで日付がないため、日付がないiのエラー回避が必要
                """
                break

# デフォルトのボタンクラス
class d_button(tk.Button):
    def __init__(self,master=None,cnf={},**kw):
        tk.Button.__init__(self,master,cnf,**kw)
        self.configure(font=("",14),height=2, width=4, relief="flat")
            
# ルートフレームの定義      
root = tk.Tk()
root.title("Calendar App")
mycal = mycalendar(root)
mycal.pack()
root.mainloop()

これを実行すると、上図のGUIが完成する。(ただし、まだクリックしても何も起こらない)
以降で、主要な部分について細部を補足説明したい。

カレンダー部分のクラス化

今回は、クラス化による開発を行っている。
クラス化を行わない、いわゆる手続き型の記述で書くこともできるが、クラス化をすることによって、カレンダーを作成するクラスを作成することによって、他のGUIでもこのクラスをそのまま活用することができる。

クラス化手法によるGUIの作成については次の記事も参考にして頂きたい。
python tkinterのクラス化手法によるGUI作成 - memopy

このmycalendarクラスは、大きく分けて2つのメソッドを構成している。

class mycalendar(tk.Frame):
    def __init__(self,master=None,cnf={},**kw):
        """
        frame_top, frame_week の定義
        """
    def create_calendar(self,year,month):
        """
        frame_calendar の定義
        """

__init__初期化メソッドでは、frame_topとframe_weekを定義し、create_calendarメソッドで、frame_calendarを定義している。
frame_calendarを初期化メソッドから分けた理由は、frame_calendar部分の日付ボタンは、月が変わると変更しないといけないため、月が変わった時に再度このメソッドを呼び出せばいいように切り分けた。

デフォルトのボタンをクラス化

カレンダーの部品となるボタンウィジェットは、全て同じボタンに統一しなければならない。そのため、ボタンウィジェットのみクラス化し、全てにおいて同一形状のボタンを使えるようにした。

class d_button(tk.Button):
    def __init__(self,master=None,cnf={},**kw):
        tk.Button.__init__(self,master,cnf,**kw)
        self.configure(font=("",14), height=2, width=4, relief="flat")

カレンダー日付部分のボタン作成

カレンダーの日付部分を配置するにはどうすればよいだろうか。
ウィジェットを順番に配置するには、pack()メソッドを用いた。今回は、碁盤の目のように配置するため、grid()メソッドを用いる。

grid メソッドの基本

gridメソッドにもいろいろなオプションがあるが、基本的な指定は次のとおりである。

widget.grid(column = 0 , row = 0)

これは、フレームを碁盤の目に区切り、列(column)と行(row)を指定する。左上がcolumn = 0, row = 0 となる。
f:id:memopy:20170617191155p:plain

forループを使ったボタンの生成

使用するボタンは全部で42個必要になる。これを1つずつ定義するのは大変なので、forループ文で42個定義する。
ここは、表題のカレンダーとは別にスクリプトを作成して、動作を確認していきたい。

for i in range(0,42):
    print(i)

上記のスクリプトで、iが0から41までの合計42個出力される。
ここでボタンを生成すればよい。生成したボタンは、あらかじめdict型の変数を用意しておき、iをkeyとしてボタンオブジェクトを追加していく。
まずは、pack()メソッドを用いて配置してみる。

import tkinter as tk
root = tk.Tk()
day = {}
for i in range(0,42):
    day[i] = tk.Button(root,text=i)
    day[i].pack()
root.mainloop()

f:id:memopy:20170617191923p:plain
画面は切れているが、0~41まで42個のボタンが配置された。
今度は、grid()メソッドを用いて配置してみる。しかし、ここでは少し工夫が必要だ。
i の値は 0 ~ 41 まで変化していくが、この i の値を使って、column と row の値を計算する必要がある。
f:id:memopy:20170617193038p:plain

import tkinter as tk
root = tk.Tk()
day = {}
for i in range(0,42):
    day[i] = tk.Button(root,text=i)
    c = i - (7 * int(i/7))
    r = int(i/7)
    day[i].grid(column = c, row = r)
root.mainloop()

f:id:memopy:20170617194614p:plain
グリッド上にボタンを配置できた!
今度は、このボタンにインデックス番号(i)の値ではなく、指定した年月の日付を入れればよい。

calendarモジュール

指定した年月に対して、開始日の曜日を求める公式も存在する。ただし、うるう年も考慮する必要があり、結構めんどくさい計算になる。
しかしさすがはpython。このようなニーズにも標準モジュールが用意されている。それがcalendarモジュールだ。
calendarモジュールにも様々なメソッドが用意されているが、今回は、指定した年月を指定すると、週単位の日付リストを返すメソッドを使用する。
下記は、2017年6月を指定した例

import calendar
cal = calendar.Calendar()
for days in cal.monthdayscalendar(2017,6):
    print(days)

f:id:memopy:20170617195921p:plain
このように、週単位でリストの値として日付が返ってくる。
これを先の、day[i]のときの日付はどのように指定したらよいだろうか。
f:id:memopy:20170618074226p:plain
リストの中のリストは、ボタンをgrid()メソッドで配置したときに使用した、変数 r, c を用いて、days[r][c]とすれば参照することができる。

これを、先ほどfor文で作成したボタンのtextに指定してみる。

import tkinter as tk
import calendar
cal = calendar.Calendar()
days = cal.monthdayscalendar(2017,6)

root = tk.Tk()
day = {}
for i in range(0,42):
    c = i - (7 * int(i/7))
    r = int(i/7)
    day[i] = tk.Button(root,text=days[r][c])
    day[i].grid(column = c, row = r)

root.mainloop()

f:id:memopy:20170617201913p:plain
日付がtextの値として指定された。日付がない部分は、0となっている。
この状態でロジックが完成したように見えるが、インタプリタコンソールを見ると「インデックスの範囲外ですよ」というエラーメッセージが表示されている。
f:id:memopy:20170618074742p:plain

これは、ボタンは42個作成されたが、月によっては、日付が42個ない。6月のように第5週までしかない月では、0の部分を入れても、35個しか値がないからだ。そのため、i = 35 以降でエラーが発生する。この発生は絶対起こりえるので、try-exceptステートメントで回避する。

import tkinter as tk
import calendar
cal = calendar.Calendar()
days = cal.monthdayscalendar(2017,6)

root = tk.Tk()
day = {}
for i in range(0,42):
    c = i - (7 * int(i/7))
    r = int(i/7)
    try:
        day[i] = tk.Button(root,text=days[r][c])
        day[i].grid(column = c, row = r)
    except:
        break

root.mainloop()

最後に、日付が0場合には、ボタンを作成しない条件分岐を定義すれば、カレンダー日付部分のロジックとなる。

import tkinter as tk
import calendar
cal = calendar.Calendar()
days = cal.monthdayscalendar(2017,6)

root = tk.Tk()
day = {}
for i in range(0,42):
    c = i - (7 * int(i/7))
    r = int(i/7)
    try:
        if days[r][c] != 0 :
            day[i] = tk.Button(root,text=days[r][c])
            day[i].grid(column = c, row = r)
    except:
        break

root.mainloop()

f:id:memopy:20170617203459p:plain

まとめ

かなり長い記事になってしまったが、これらのロジックを組み合わせると冒頭のカレンダークラスが完成する。
次回は、月を変更したときに、カレンダーの日付部分を変更する処理を実装したい。

前の記事
python tkinterでGUIアプリを作る(第2弾~カレンダー編~) - memopy
次の記事
python tkinter カレンダーの月めくり処理を実装する(カレンダー編③) - memopy

質問や記事の誤りがありましたら、コメントお願いします。