
今回はTkinterを使ってGUIのキッチンタイマーを作成してみましょう。処理はストップウォッチとよく似ていて、予めユーザーが時間をセットできるようにしたら、あとはストップウォッチ的に計測した時間を差し引いて残り時間を求めればOKです。
目次
必要なウィジェットを表示
まずは必要になるウィジェットを画面に表示してみましょう。
import tkinter as tk
 # tk.Frameを継承したApplicationクラスを作成
class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        # ウィンドウの設定
        master.title("キッチンタイマー")
        master.geometry("430x280") # タイマーの幅は430x280
        # 実行内容
        self.pack()
        self.create_widget()
    # create_widgetメソッドを定義
    def create_widget(self):
        # 全体の親キャンバス
        self.canvas_bg = tk.Canvas(self.master, width=430, height=280)
        self.canvas_bg.pack()
        # タイマー用のキャンバス
        self.canvas_time = tk.Canvas(self.canvas_bg, width=410, height=80, bg="lightgreen")
        self.canvas_time.place(x=10, y=10)
        # 分ボタン
        self.min_button = tk.Button(self.canvas_bg, width=8, height=2, text="分", font=("MSゴシック体", "18","bold"))
        self.min_button.place(x=10, y=100)
        # 秒ボタン
        self.sec_button = tk.Button(self.canvas_bg, width=8, height=2, text="秒", font=("MSゴシック体", "18","bold"))
        self.sec_button.place(x=150, y=100)
        # リセットボタン
        self.reset_button = tk.Button(self.canvas_bg, width=8, height=2, text="リセット", font=("MSゴシック体", "18","bold"))
        self.reset_button.place(x=290, y=100)
        # スタート/ストップボタン
        start_button = tk.Button(self.canvas_bg, width=27, height=2, text="スタート/ストップ", font=("MSゴシック体", "18","bold"))
        start_button.place(x=10, y=190)
if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()
 
キッチンタイマーで必要となるのは
- 時間を表示するためのキャンバス
- 時間をセットするためのボタン(今回は「分」と「秒」)
- リセットボタン
- スタートボタン
- ストップ(一時停止)ボタン
スタートボタンとストップボタンは同時に存在する必要がないので、今回は「スタート/ストップボタン」として1つにまとめています。
また、全体の親となるキャンバスを用意して子となるウィジェットを相対配置にしておくことで、このタイマーをなにかのアプリケーションにそのままはめ込むことができるようにしています。
タイマーの数字テキストを表示
タイマーに数字を表示してみます。
        # 変数定義
        self.left_min = 0 # 残り時間(分)
        self.left_sec = 0 # 残り時間(秒)
    # create_widgetメソッドを定義
    def create_widget(self):
        # タイマー用のキャンバス
        self.canvas_time = tk.Canvas(self.canvas_bg, width=410, height=80, bg="lightgreen")
        self.canvas_time.place(x=10, y=10)
        # タイマーに数字を表示
        self.canvas_time.create_text(250,40,text=str(self.left_min).zfill(2) + ":", font=("MSゴシック体", "36", "bold"), tag="min_text", anchor="e") # 分を表示
        self.canvas_time.create_text(250,40,text=str(self.left_sec).zfill(2), font=("MSゴシック体", "36", "bold"), tag="sec_text", anchor="w") # 秒を表示
 
タイマーの数字は変化することがわかっているので変数として14~16行目に定義。
33~35行目でcanvasウィジェットのcreate_textメソッドを使って表示させます。ラベルウィジェットで表示すると背景が灰色のウィンドウになってしまい、見た目が悪いためです。
また、タイマーの数字は2桁で表示するのが一般的です。そこで数字をそのまま表示するのではなく、文字列型に変換したうえでzfillメソッドを使って2桁に満たない場合は残りを0で埋める(ゼロパディング)処理をしています。
現状まだ表示しただけなので動作はしません。
これに機能(処理)を書き加えていきましょう。
分ボタンの処理
まずは簡単かつユーザーが最初に操作する分ボタンと秒ボタンから作っていきましょう。
押すとボタンに応じた時間が変数にセットされ、画面にも表示するよう処理を記述します。
        # 変数定義
        self.set_time = 0 # セット時間
    # create_widgetメソッドを定義
    def create_widget(self):
        # 分ボタン
        self.min_button = tk.Button(self.canvas_bg, width=8, height=2, text="分", font=("MSゴシック体", "18","bold"), command=self.min_button_clicked)
        self.min_button.place(x=10, y=100)
    # minボタンを押した時
    def min_button_clicked(self):
        if self.left_min < 59: # 最大59分まで
            self.set_time += 60 # セット時間をプラス
            self.left_min += 1 # 残り時間(分)をプラス
            self.canvas_time.delete("min_text") # 表示時間(分)を消去
            self.canvas_time.create_text(250, 40, text=str(self.left_min).zfill(2) + ":", font=("MSゴシック体", "36", "bold"), tag="min_text", anchor="e") # 分を表示
comanndオプションで分ボタンが押された時にmin_button_clicked関数が呼び出されるように記述。
min_button_clicked関数には、残り時間(分)が59未満の場合、残り時間(分)をプラスして、表示時間を更新する(消去して作り直す)処理を記述します。
また、13行目でset_time変数を定義し、min_button_clicked関数実行時に操作するようにもしています。
これは後でタイマーを作った時、残り時間が分と秒に分かれている形式では扱いづらいため、合計残り時間(秒)を保存しておく変数が欲しかったためです。
秒ボタンの処理
分ボタンと同じように作っていきます。
    # create_widgetメソッドを定義
    def create_widget(self):
        # 秒ボタン
        self.sec_button = tk.Button(self.canvas_bg, width=8, height=2, text="秒", font=("MSゴシック体", "18","bold"), command=self.sec_button_clicked)
        self.sec_button.place(x=150, y=100)
    # secボタンを押した時
    def sec_button_clicked(self):
        if self.left_sec < 59: # 最大59秒まで
            self.set_time += 1 # セット時間をプラス
            self.left_sec += 1 # 残り時間(秒)をプラス
            self.canvas_time.delete("sec_text") # 表示時間(秒)を消去
            self.canvas_time.create_text(250,40,text=str(self.left_sec).zfill(2), font=("MSゴシック体", "36", "bold"), tag="sec_text", anchor="w") # 秒を表示
何度も使う処理を関数に分ける
ここで、「分を表示」「秒を表示」といった処理が最初にタイマーに数字を表示する時と、分ボタンや秒ボタンを押した時とで2回、使い回しているのがわかります。
また、表示上の数字を更新する際には「表示時間(分)を消去」「分を表示」という2つの処理を行う必要があり、この処理はこのあと時間更新の処理を作った際も使い回すことがわかります。
毎回探してコピペするのも面倒ですし可読性も悪いので関数に分けてしまいましょう。
    # create_widgetメソッドを定義
    def create_widget(self):
        # タイマー用のキャンバス
        self.canvas_time = tk.Canvas(self.canvas_bg, width=410, height=80, bg="lightgreen")
        self.canvas_time.place(x=10, y=10)
        # タイマーに数字を表示
        self.update_min_text() # 分の表示更新
        self.update_sec_text() # 秒の表示更新
# 各ボタンを押した時の処理
    # minボタンを押した時
    def min_button_clicked(self):
        if self.left_min < 59: # 最大59分まで
            self.set_time += 60 # セット時間をプラス
            self.left_min += 1 # 残り時間(分)をプラス
            self.update_min_text() # 分の表示更新
    # secボタンを押した時
    def sec_button_clicked(self):
        if self.left_sec < 59: # 最大59秒まで
            self.set_time += 1 # セット時間をプラス
            self.left_sec += 1 # 残り時間(秒)をプラス
            self.update_sec_text() # 秒の表示更新
# 個別の処理
    # 分の表示更新
    def update_min_text(self):
        self.canvas_time.delete("min_text") # 表示時間(分)を消去
        self.canvas_time.create_text(250, 40, text=str(self.left_min).zfill(2) + ":", font=("MSゴシック体", "36", "bold"), tag="min_text", anchor="e") # 分を表示
    # 秒の表示更新
    def update_sec_text(self):
        self.canvas_time.delete("sec_text") # 表示時間(秒)を消去
        self.canvas_time.create_text(250,40,text=str(self.left_sec).zfill(2), font=("MSゴシック体", "36", "bold"), tag="sec_text", anchor="w") # 秒を表示
メインのコードがスッキリしました。
リセットボタンの処理
次にリセットボタンの処理を作っていきます。
押すとセットした時間を00:00にリセットします。
    # create_widgetメソッドを定義
    def create_widget(self):
        # リセットボタン
        self.reset_button = tk.Button(self.canvas_bg, width=8, height=2, text="リセット", font=("MSゴシック体", "18","bold"), command=self.reset_button_clicked)
        self.reset_button.place(x=290, y=100)
# 各ボタンを押した時の処理
    # resetボタンを押した時
    def reset_button_clicked(self):
        self.set_time = 0 # セット時間をリセット
        self.left_min = 0 # 残り時間(分)をリセット
        self.left_sec = 0 # 残り時間(秒)をリセット
        self.update_min_text() # 分の表示更新
        self.update_sec_text() # 秒の表示更新
先ほど関数に分けた処理が早速役に立ちましたね。楽です。
スタート/ストップボタンの処理
さて、いよいよメインのスタート/ストップボタンです。
といっても本当のメインはある意味時間更新処理の方なので、スタートボタンでは
- 押すと他のボタンを押せない状態(DISABLED)にする
- スタートボタンを押した時間を保存する
- 時間更新処理を開始する
ストップボタンでは
- 他のボタンを押せる状態(NORMAL)にする
- ストップボタンを押した時点の残り時間を保存する
- 時間更新処理を停止させる
という処理のみ行います。
        # 変数定義
        self.timer_on = False # タイマーの状態
        self.start_time = 0 # 開始時間
        self.left_time = 0 # 残り時間
        self.left_min = 0 # 残り時間(分)
        self.left_sec = 0 # 残り時間(秒)
        self.after_id = 0 # after_id変数を定義
    # create_widgetメソッドを定義
    def create_widget(self):
        # スタート/ストップボタン
        start_button = tk.Button(self.canvas_bg, width=27, height=2, text="スタート/ストップ", font=("MSゴシック体", "18","bold"), command=self.start_button_clicked)
        start_button.place(x=10, y=190)
# 各ボタンを押した時の処理
    #startボタンを押した時
    def start_button_clicked(self):
        if self.set_time >= 1:
            if self.timer_on == False:
                self.timer_on = True
                # 各種ボタンを押せなくする
                self.min_button["state"] = tk.DISABLED
                self.sec_button["state"] = tk.DISABLED
                self.reset_button["state"] = tk.DISABLED
                self.start_time =time.time() # 開始時間を代入
                self.update_time() # updateTime関数を実行
            elif self.timer_on == True:
                self.timer_on = False
                # 各種ボタンを押せるようにする
                self.min_button["state"] = tk.NORMAL
                self.sec_button["state"] = tk.NORMAL
                self.reset_button["state"] = tk.NORMAL
                self.set_time = self.left_time
                app.after_cancel(self.after_id)
このボタンはタイマーが動いていない時はスタートボタン、動いている最中はストップボタンとして異なる処理を行うため、timer_on変数を用意してタイマーの状態によって条件分岐を行えるようにします。
このtimer_on変数をタイマースタート時にTrueに、ストップ時にFalseになるよう記述します。
このボタンはセットされている時間が0の時には作動してほしくないのでset_time変数が1以上の場合のみ処理を行うように記述。
スタートボタンを押すと時間更新処理(update_time関数)が実行されるようにし、ストップボタンを押すとupdate_time関数の中で実行するafterメソッドをキャンセルするよう記述します。
時間更新の処理
最後に時間更新の処理、いわばタイマー本体です。
スタートボタンを押すとこの関数が実行され、残り時間(left_time)があるうちは再帰的に呼び出されて時間を更新し続けます。
ストップボタンを押すか、残り時間が0になると再帰処理をやめてストップします。
import time
import math
        # 変数定義
        self.elapsed_time = 0 # 経過時間
# 個別の処理
    # 時間更新処理
    def update_time(self):
        self.elapsed_time = time.time() - self.start_time  # 経過時間を計算
        self.left_time = self.set_time - self.elapsed_time # 残り時間を計算
        self.left_min = math.floor(self.left_time // 60) # 残り時間(分)を計算
        self.left_sec = math.floor(self.left_time % 60) # 残り時間(秒)を計算
        self.update_min_text() # 分の表示更新
        self.update_sec_text() # 秒の表示更新
        if self.left_time > 0.1:
            self.after_id = self.after(10, self.update_time)
        else:
            self.timer_on = False
            # 各種ボタンを押せるようにする
            self.min_button["state"] = tk.NORMAL
            self.sec_button["state"] = tk.NORMAL
            self.reset_button["state"] = tk.NORMAL
            self.set_time = self.left_time
            app.after_cancel(self.after_id)
タイマーの残り時間は
セットした時間 - スタートボタンを押してから今までに経過した時間
の式で求められます。
この処理をするためにまず経過時間を入れるelapsed_time変数を定義。
update_time関数の中で現在時間とスタート時間から経過時間を割り出し、セット時間と経過時間から残り時間(left_time)を割り出します。
こうして計算した残り時間から残り時間(分)と残り時間(秒)を計算し、タイマーの表示を更新しています。
update_time関数の再帰処理は残り時間が残っている場合のみになるよう条件を付けています。
※ただしこのタイマーは残り時間が0.1秒を切って少しすると停止するため厳密に正確ではありません。19行目の記述を0にすると最後にタイマーに表示されるのが00:00ではなく-01:59になってしまうためです。
残り時間がなくなると、ストップボタンを押したときと同様、各種ボタンが押せるようになり、afterメソッドをキャンセルするよう記述しています。
完成
今回作成したキッチンタイマーアプリのソースコードです。
import tkinter as tk
import time
import math
 # tk.Frameを継承したApplicationクラスを作成
class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        # ウィンドウの設定
        master.title("キッチンタイマー")
        master.geometry("430x280") # タイマーの幅は430x280
        # 変数定義
        self.timer_on = False # タイマーの状態
        self.start_time = 0 # 開始時間
        self.set_time = 0 # セット時間
        self.elapsed_time = 0 # 経過時間
        self.left_time = 0 # 残り時間
        self.left_min = 0 # 残り時間(分)
        self.left_sec = 0 # 残り時間(秒)
        self.after_id = 0 # after_id変数を定義
        # 実行内容
        self.pack()
        self.create_widget()
    # create_widgetメソッドを定義
    def create_widget(self):
        # 全体の親キャンバス
        self.canvas_bg = tk.Canvas(self.master, width=430, height=280)
        self.canvas_bg.pack()
        # タイマー用のキャンバス
        self.canvas_time = tk.Canvas(self.canvas_bg, width=410, height=80, bg="lightgreen")
        self.canvas_time.place(x=10, y=10)
        # タイマーに数字を表示
        self.update_min_text() # 分の表示更新
        self.update_sec_text() # 秒の表示更新
        # 分ボタン
        self.min_button = tk.Button(self.canvas_bg, width=8, height=2, text="分", font=("MSゴシック体", "18","bold"), command=self.min_button_clicked)
        self.min_button.place(x=10, y=100)
        # 秒ボタン
        self.sec_button = tk.Button(self.canvas_bg, width=8, height=2, text="秒", font=("MSゴシック体", "18","bold"), command=self.sec_button_clicked)
        self.sec_button.place(x=150, y=100)
        # リセットボタン
        self.reset_button = tk.Button(self.canvas_bg, width=8, height=2, text="リセット", font=("MSゴシック体", "18","bold"), command=self.reset_button_clicked)
        self.reset_button.place(x=290, y=100)
        # スタート/ストップボタン
        start_button = tk.Button(self.canvas_bg, width=27, height=2, text="スタート/ストップ", font=("MSゴシック体", "18","bold"), command=self.start_button_clicked)
        start_button.place(x=10, y=190)
# 各ボタンを押した時の処理
    # minボタンを押した時
    def min_button_clicked(self):
        if self.left_min < 59: # 最大59分まで
            self.set_time += 60 # セット時間をプラス
            self.left_min += 1 # 残り時間(分)をプラス
            self.update_min_text() # 分の表示更新
    # secボタンを押した時
    def sec_button_clicked(self):
        if self.left_sec < 59: # 最大59秒まで
            self.set_time += 1 # セット時間をプラス
            self.left_sec += 1 # 残り時間(秒)をプラス
            self.update_sec_text() # 秒の表示更新
    # resetボタンを押した時
    def reset_button_clicked(self):
        self.set_time = 0 # セット時間をリセット
        self.left_min = 0 # 残り時間(分)をリセット
        self.left_sec = 0 # 残り時間(秒)をリセット
        self.update_min_text() # 分の表示更新
        self.update_sec_text() # 秒の表示更新
    #startボタンを押した時
    def start_button_clicked(self):
        if self.set_time >= 1:
            if self.timer_on == False:
                self.timer_on = True
                # 各種ボタンを押せなくする
                self.min_button["state"] = tk.DISABLED
                self.sec_button["state"] = tk.DISABLED
                self.reset_button["state"] = tk.DISABLED
                self.start_time =time.time() # 開始時間を代入
                self.update_time() # updateTime関数を実行
            elif self.timer_on == True:
                self.timer_on = False
                # 各種ボタンを押せるようにする
                self.min_button["state"] = tk.NORMAL
                self.sec_button["state"] = tk.NORMAL
                self.reset_button["state"] = tk.NORMAL
                self.set_time = self.left_time
                app.after_cancel(self.after_id)
# 個別の処理
    # 時間更新処理
    def update_time(self):
        self.elapsed_time = time.time() - self.start_time  # 経過時間を計算
        self.left_time = self.set_time - self.elapsed_time # 残り時間を計算
        self.left_min = math.floor(self.left_time // 60) # 残り時間(分)を計算
        self.left_sec = math.floor(self.left_time % 60) # 残り時間(秒)を計算
        self.update_min_text() # 分の表示更新
        self.update_sec_text() # 秒の表示更新
        if self.left_time > 0.1:
            self.after_id = self.after(10, self.update_time)
        else:
            self.timer_on = False
            # 各種ボタンを押せるようにする
            self.min_button["state"] = tk.NORMAL
            self.sec_button["state"] = tk.NORMAL
            self.reset_button["state"] = tk.NORMAL
            self.set_time = self.left_time
            app.after_cancel(self.after_id)
    # 分の表示更新
    def update_min_text(self):
        self.canvas_time.delete("min_text") # 表示時間(分)を消去
        self.canvas_time.create_text(250, 40, text=str(self.left_min).zfill(2) + ":", font=("MSゴシック体", "36", "bold"), tag="min_text", anchor="e") # 分を表示
    # 秒の表示更新
    def update_sec_text(self):
        self.canvas_time.delete("sec_text") # 表示時間(秒)を消去
        self.canvas_time.create_text(250,40,text=str(self.left_sec).zfill(2), font=("MSゴシック体", "36", "bold"), tag="sec_text", anchor="w") # 秒を表示
if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()
 
実際に動かしてみたものがこちらです。








