memopy

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

python tkinter カレンダーの月めくり処理を実装する(カレンダー編③)

カレンダーの月めくり処理を実装する

カレンダーアプリを作成する第3回目。今回は、カレンダーの月めくりの処理を実装したい。

前回までに作成したGUI
f:id:memopy:20170615180348p:plain

月めくりのロジック

新規に作成するメソッドは次のロジックとなる。

  1. 月を変更するラベル("<", ">")がクリックされたときのイベント関数を定義
  2. イベント関数内で(以下、同じ。)押されたラベルが、"<" か ">" かを判定する
  3. "<" が押されたら、現在の月を -1 する
  4. ">" が押されたら、現在の月を +1 する
  5. 現在の月が 0 になったら、現在の年を -1 して、現在の月を 12 にする
  6. 現在の月が 13 になったら、現在の年を +1 して、現在の月を 1 にする
  7. frame_topにある年と月のラベルを変更する
  8. 変換した現在の年と月を引数にして、カレンダーの日付部分を作成するメソッド(create_calendar)を再度呼び出す

また、カレンダーの日付部分を作成するメソッド(create_calendar)には、「呼び出されたら、はじめに、現在定義されているボタンを全て削除する(初期化)処理」を追加する。

スクリプトの構成

スクリプトの全体構成を概観すると、次のようになる。なお、スクリプトの全文は、末尾に記載している。

class mycalendar(tk.Frame):
    def __init__(self,master=None,cnf={},**kw):
        """
        frame_top, frame_week の定義
        """
    def create_calendar(self,year,month):
        """
        frame_calendar の定義
        """
    def change_month(self,event)
        """
        月めくりのロジックを定義
        """

bindメソッドを使用したイベント処理

Buttonウィジェット以外でクリックしたときの処理を定義するためには、bindメソッドを用いなければならない。

bindメソッドに関する詳しい記事はこちらを参照
python tkinter クリックされたウィジェットのテキストや属性を取得する - memopy

ラベルがクリックされたときのイベント関数の定義と押されたラベルの判定

まずprivious_monthとnext_monthのラベルにbindメソッドを追加する。

def __init__(self,master=None,cnf={},**kw):
    # ~略~
    self.previous_month.bind("<1>",self.change_month)
    self.next_month.bind("<1>",self.change_month)
    # ~略~

これは、previous_month と next_month が左クリック("<1>")されたとき、self.change_monthメソッドを呼び出す。というものだ。
続いて、イベント関数(self.change_month)を定義する。

def change_month(self,event):
    print(event.widget["text"])

はじめにテストとして、押されたラベルを出力してみる。
f:id:memopy:20170618085140p:plain
これで、押されたラベルを判定することができる。

押されたラベルに応じて月の変更

押されたラベルが "<" だったら、現在の月(self.month)を -1 して、それ以外(">")だったら、現在の月を +1 する。

def change_month(self,event):
    # 押されたラベルを判定し、月の計算
    if event.widget["text"] == "<":
        self.month -= 1
    else:
        self.month += 1

    print(self.month)

f:id:memopy:20170618090619p:plain
"<" を押すと、6月から、1ずつ -1 されていくのがわかる。

現在の月が 0 または 13 になったときの処理

現在の月(self.month)の値が、0 になったら、現在の年(self.year)を -1 し、現在の月(self.month)の値が、13になったら、現在の年(self.year)の値を +1 する。

def change_month(self,event):
    # 押されたラベルを判定し、月の計算
    if event.widget["text"] == "<":
        self.month -= 1
    else:
        self.month += 1
    # 月が0、13になったときの処理
    if self.month == 0:
        self.year -= 1
        self.month = 12
    elif self.month == 13:
        self.year +=1
        self.month =1
        
    print(self.year,self.month)

f:id:memopy:20170618091231p:plain
正しく年の繰り上がり(下がり)処理ができていることがわかる。

frame_topにある年と月のラベルを変更する

frame_topにある年の表示は、current_year 、月の表示は、current_month でそれぞれ定義されているので、このtextオプションを上書きすればよい。
f:id:memopy:20170618095408p:plain

    # 押されたラベルを判定し、月の計算
    if event.widget["text"] == "<":
        self.month -= 1
    else:
        self.month += 1
    # 月が0、13になったときの処理
    if self.month == 0:
        self.year -= 1
        self.month = 12
    elif self.month == 13:
        self.year +=1
        self.month =1
    # frame_topにある年と月のラベルを変更する
    self.current_year["text"] = self.year
    self.current_month["text"] = self.month

    print(self.year,self.month)

f:id:memopy:20170618092430p:plain
ラベルのtextを変更することができた。

変換した現在の年と月を引数にして、カレンダーの日付部分を作成するメソッドを再度呼び出す

最後に、現在の年(self.year)と現在の月(self.month)の値を引数に、カレンダーの日付部分を作成するメソッドを再度呼び出す。

    # 押されたラベルを判定し、月の計算
    if event.widget["text"] == "<":
        self.month -= 1
    else:
        self.month += 1
    # 月が0、13になったときの処理
    if self.month == 0:
        self.year -= 1
        self.month = 12
    elif self.month == 13:
        self.year +=1
        self.month =1
    # frame_topにある年と月のラベルを変更する
    self.current_year["text"] = self.year
    self.current_month["text"] = self.month
    # 日付部分を作成するメソッドの呼び出し
    self.create_calendar(self.year,self.month)

f:id:memopy:20170618092814p:plain
メソッドが呼び出されたことは確認できるが、前のボタンを初期化(削除)していないので、前のボタンが残ったまま、新たな月の日付ボタンが生成された。

ボタンの初期化処理の追加

既に定義しているcreate_calendarメソッドに、「呼び出された時、既にボタンがある場合には、これらを削除する処理」を定義し、初期化する必要がある。

def create_calendar(self,year,month):
    "指定した年(year),月(month)のカレンダーウィジェットを作成する"

    # ボタンがある場合には削除する(初期化)
    try:
        for key,item in self.day.items():
            item.destroy()
    except:
        pass

    # ~略~

最初にカレンダーを作成したときは、ボタンがないためエラーが発生する。そのため、try-exceptステートメントでエラー回避する。
また、ボタンを格納しているself.dayの全てのアイテムを取得をするためには、items()メソッドを使用する。このとき、itemsで返ってくるdayはdict型なので、key と item の2つの変数を受けなければならない。そして、itemに対して、destroy()メソッドを使用して、ボタンを削除できる。
f:id:memopy:20170618094439p:plain
先ほどは不具合が出ていた、4月のカレンダーも正しく表示された。

スクリプト全文

以上のスクリプトの全体は次のとおりとなる。
※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.bind("<1>",self.change_month)
        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.bind("<1>",self.change_month)
        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)のカレンダーウィジェットを作成する"

        # ボタンがある場合には削除する(初期化)
        try:
            for key,item in self.day.items():
                item.destroy()
        except:
            pass
            
        # 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

    def change_month(self,event):
        # 押されたラベルを判定し、月の計算
        if event.widget["text"] == "<":
            self.month -= 1
        else:
            self.month += 1
        # 月が0、13になったときの処理
        if self.month == 0:
            self.year -= 1
            self.month = 12
        elif self.month == 13:
            self.year +=1
            self.month =1
        # frame_topにある年と月のラベルを変更する
        self.current_year["text"] = self.year
        self.current_month["text"] = self.month
        # 日付部分を作成するメソッドの呼び出し
        self.create_calendar(self.year,self.month)

# デフォルトのボタンクラス
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()

まとめ

これで、カレンダーとしての基本的かつ最低限の機能は実装した。
次回以降は、スケジュール管理機能を作成したい。

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

前の記事
python tkinterでカレンダーを作成する(カレンダー編②) - memopy