【Python】tkinterで作業時間に応じて給料を計算してくれるアプリを作ってみた

#経緯
python初心者です。「pythonではタイマーを簡単に作ることができるから、初心者にはタイマーがオススメ！」と言ってる記事を見かけたので、鵜呑みにしてタイマーを作ることにしました。
ただ時間を測るだけじゃ面白くないので、「時給を設定しておくと作業時間に見合った給料を計算してくれる」という機能もつけます。


#つけたい機能
- タイマー関連
 - 「開始」「一時停止」「完了」ボタンで動かせるタイマー
 - 時給とタイマーの時間から給料を計算
 - 起動中のタイマーのリセット（間違えて開始ボタンを押してしまった時に、余分な給料が発生するのを防ぐのため）

- お金関連
 - 時給の変更
 - 給料のリセット

- 履歴関連
 - タイマーの起動履歴（開始日時と終了日時、タイマーの時間）
 - ↑この履歴の削除（1件 / 全件）
 - 合計作業時間の表示


#環境
- PC : windows10
- 言語 : python 3.7.6


#実際に作ったもの
##メイン画面タブ

![2020-06-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/bf3087f3-ed17-88f4-349a-770643b6354c.png)
起動時の画面はこんな感じ。

![2020-06-02 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/bbd57e33-c34c-7864-f7bf-684feabcb0d3.png)
「開始」ボタンを押すとタイマーが起動し、「開始」ボタンが「一時停止」ボタンに変わります。

![2020-06-02 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/cc1b86e5-ea9e-3b1b-8426-0040b59bc549.png)
「一時停止」ボタンを押すと、タイマーが一時停止して「再開」ボタンに変わります。

「完了」ボタンを押すとタイマーの時間を記録し、設定してある時給を使って給与を計算します。
「リセット」ボタンを押すと、給与の計算は行わずにタイマー開始前の状態に戻します。
ちなみに、タイマーが動いていない状態でこれらのボタンを押しても何も起こりません。

![2020-06-02 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/255b30eb-0679-d787-30ba-257dce0f5935.png)
なお、誤クリックを防ぐため、「完了」「リセット」ボタンを押したときにはこのような確認ウィンドウを出すようにしました。

![2020-06-02 (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/ef4c5d3c-503a-fec9-c09e-638e4dee3f49.png)
「給与引き出し」ボタンを押すと、これまた確認ウィドウを出して、給与の引き出し（「給与:○円」のところを「給与:0円」に変更）をします。


　


##記録タブ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/acf2273b-b620-9bf1-31a5-acebbeddbb92.png)
「記録」タブでは、タイマーの履歴を見ることができます。
例えば一番上の「06/02　00:12～00:14　00:08」は、「開始ボタンを6/2の00:12に押し、完了ボタンを00:14に押した。完了時のタイマーの表記は00:08」という意味です。

自分はどの時間に一番集中できるのか分かればいいな、と思いこの履歴機能をつけました。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/1f3f8451-d167-2a77-7658-36270c4d99d3.png)
各項目はこのように選択でき、「削除」ボタンで削除できます。
「全て削除」ボタンでは全ての項目を削除できます。
　
　

##引き出し記録タブ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/c2074613-f866-2340-f958-e03f28ea538f.png)
給与の引き出し記録を見ることが出来ます。
　
　
##設定タブ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/32452087-9699-29fa-9250-ecd1faa0df72.png)
合計作業時間の確認・リセットと、時給の変更ができます。

![1591025774093.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500402/93dc7b17-bdc3-ad2d-596a-e1d841647e67.jpeg)
時給欄に整数値以外を入れるとエラーウィンドウを出し、時給を変更しないようにしてあります。




#ソースコード
```python:app.py
#-*-coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk,messagebox
from time import time
import datetime
import pickle

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

        master.title("勤怠管理")
        master.geometry("350x170")
        
        #初期値の設定
        self.wage = 1000 #時給
        self.have_money = 0 #給与
        self.total = 0 #現在動いているタイマーの合計時間（秒）
        self.record_list = [] #タイマーの時間の記録
        self.money_record_list = [] #給与の引き出し記録

        self.data_list = [self.wage,self.have_money,self.record_list,self.money_record_list,self.total]
        
        #データのロード
        self.load_data()

        #タブと内容を作成
        self.create_tabs()
        self.create_main(self.main)
        self.create_record(self.record)
        self.create_history(self.history)
        self.create_setting(self.setting)


    #タブを作成/設置
    def create_tabs(self):
        self.notebook = tk.ttk.Notebook()
        self.main = tk.Frame(self.notebook)
        self.setting = tk.Frame(self.notebook)
        self.history = tk.Frame(self.notebook)
        self.record = tk.Frame(self.notebook)
        self.notebook.add(self.main, text="メイン画面")
        self.notebook.add(self.record, text="記録")
        self.notebook.add(self.history, text="引き出し記録")
        self.notebook.add(self.setting, text="設定")
        self.notebook.pack(expand=True, fill="both")

    #「メイン画面」タブ
    def create_main(self,master):
        lb_1 = tk.Label(master, text="作業時間")
        lb_1.place(x=10, y=0)
        self.canvas = tk.Canvas(master,width=290,height=80)
        self.canvas.place(x=10,y=20)

        self.spbt = tk.Button(master,text="開始",command=self.start_pause_button,width=10)
        self.spbt.place(x=250, y=20)
        tk.Button(master,text="完了",command=self.stop_button_click,width=10).place(x=250, y=50)
        tk.Button(master,text="リセット",command=self.reset_button_click,width=10).place(x=250, y=80)
        tk.Button(master,text="給与引き出し",command=self.withdraw,width=10).place(x=10, y=115)
        
        self.wage_var = tk.StringVar()
        lb_2 = tk.Label(master,textvariable=self.wage_var)
        self.wage_var.set("時給 : {0}円".format(self.wage))
        lb_2.place(x=10, y=90)
        self.money_var = tk.StringVar()
        lb_3 = tk.Label(master, textvariable=self.money_var)
        self.money_var.set("給与 : {0}円".format(int(self.have_money)))
        lb_3.place(x=100, y=90)

        self.stop_time = 0.0
        self.elapsed_time = 0.0 #現在測っている時間（タイマーに表示されている時間）（秒）
        self.timer_running = False #タイマーが動いていればTrue、止まっていればFalse

        master.after(50,self.update)

    #「記録」タブ
    def create_record(self,master):
        self.record_lb = tk.Listbox(master,width=42,height=8)
        self.record_lb.place(x=0,y=0)
        self.record_load() #datファイルに保存してある記録を出力

        tk.Button(master,text="削除",command=self.del_record,width=10).place(x=260, y=0) #データを1件削除
        tk.Button(master,text="全て削除",command=self.del_all_records,width=10).place(x=260, y=30) #データを全件削除

    #「引き出し記録」タブ
    def create_history(self,master):
        self.money_record_lb = tk.Listbox(master,width=42,height=8)
        self.money_record_lb.place(x=0,y=0)
        self.money_record_load() #datファイルに保存してある引き出し記録を出力

        tk.Button(master,text="削除",command=self.del_money_record,width=10).place(x=260, y=0) #データ1件削除
        tk.Button(master,text="全て削除",command=self.del_all_money_records,width=10).place(x=260, y=30) #データを全件削除

    #「設定」タブ
    def create_setting(self,master):
        #時給の変更
        lb_1 = tk.Label(master, text="・時給")
        lb_1.place(x=5,y=3)
        value = tk.StringVar()
        value.set(self.wage)
        self.wage_sp = tk.Spinbox(master,textvariable=value,from_=0,to=100000000,width=8,increment=50)
        self.wage_sp.place(x=50,y=6)
        tk.Button(master,text="変更",command=self.chg_wage,width=5).place(x=120, y=2)

        #合計作業時間のリセット
        self.total_var = tk.StringVar()
        lb_2 = tk.Label(master, textvariable=self.total_var)
        self.str_total = self.strtime(self.total)
        self.total_var.set("・合計作業時間  {0}".format(self.str_total))
        lb_2.place(x=5,y=40)
        tk.Button(master,text="リセット",command=self.reset_total_click,width=5).place(x=135, y=35)



    #開始 / 一時停止ボタンが押された時
    def start_pause_button(self):
        if self.timer_running:
            self.pause_timer()
            self.spbt["text"] = "再開"
        else:
            #「再開」ではなく「開始」を押す時、その時刻を取得
            if not self.elapsed_time:
                self.dt_start = datetime.datetime.now() #タイマー開始時刻を取得（記録として保存するため）
            self.start_timer()
            self.spbt["text"] = "一時停止"

    #タイマー開始
    def start_timer(self):
        if not self.timer_running:
            self.start_time = time() - self.elapsed_time
            self.timer_running = True

    #タイマー一時停止
    def pause_timer(self):
        if self.timer_running:
            self.stop_time = time() - self.start_time #一時停止ボタンを押したときのタイマーの表示時間
            self.timer_running = False

    #完了ボタンが押された時
    def stop_button_click(self):
        if self.elapsed_time:
            msgbox = messagebox.askyesno("確認","記録を完了してもよろしいですか？")
            if msgbox:
                self.start_time = time()
                self.add_wage()
                self.add_total()
                self.dt_stop = datetime.datetime.now()
                self.new_record()
                self.spbt["text"] = "開始"
                self.stop_time = 0.0
                self.elapsed_time = 0.0
                self.timer_running = False
                self.save_data()

    #リセットボタンが押された時
    def reset_button_click(self):
        if self.elapsed_time:
            msgbox = messagebox.askyesno("確認","ストップウォッチをリセットしますか？")
            if msgbox:
                self.spbt["text"] = "開始"
                self.resettimer()

    #タイマーリセット
    def resettimer(self):
        self.start_time = time()
        self.stop_time = 0.0
        self.elapsed_time = 0.0
        self.timer_running = False

    #タイマーの時間を更新
    def update(self):
        self.canvas.delete("time")
        if self.timer_running:
            self.elapsed_time = time() - self.start_time
            elapsed_time_str = self.strtime(self.elapsed_time)
            self.canvas.create_text(200,40,text=elapsed_time_str,font=("Helvetica",40,"bold"),fill="black",tag="time",anchor="e")
        else:
            stop_time_str = self.strtime(self.stop_time)
            self.canvas.create_text(200,40,text=stop_time_str,font=("Helvetica",40,"bold"),fill="black",tag="time",anchor="e")
        
        self.master.after(50,self.update)

    #タイマー停止時に給与を追加
    def add_wage(self):
        self.have_money += int(self.wage) * self.elapsed_time / 3600
        self.money_var.set("給与 : {0}円".format(int(self.have_money)))



    #前回の記録を記録
    def new_record(self):
        start_date = self.dt_start.strftime("%m/%d")
        start_time = self.dt_start.strftime("%H:%M")
        stop_time = self.dt_stop.strftime("%H:%M")
        elapsed_time = self.strtime(self.elapsed_time)
        str_record = "{0}    {1}～{2}    {3}".format(start_date,start_time,stop_time,elapsed_time)
        self.record_lb.insert(0,str_record)
        self.record_list.insert(0,str_record)

    #記録の削除（1件）
    def del_record(self):
        index = self.record_lb.curselection()
        if index:
            msgbox = messagebox.askyesno("確認","選択中の記録を削除してもよろしいですか？")
            if msgbox:
                self.record_lb.delete(int(index[0]))
                self.record_list.pop(int(index[0]))
                self.record_lb.select_set(int(index[0])-1)
                self.save_data()
                
    #全ての記録を削除
    def del_all_records(self):
        print(self.record_lb.size())
        if self.record_lb.size() != 0:
            msgbox = messagebox.askyesno("確認","全ての記録を削除してもよろしいですか？")
            if msgbox:
                self.record_lb.delete(0,tk.END)
                self.record_list.clear()
                self.save_data()

    #給与引き出し
    def withdraw(self):
        if self.have_money:
            msgbox = messagebox.askyesno("確認","給与を引き出しますか？")
            if msgbox:
                self.dt_wd = datetime.datetime.now()
                self.add_money_record()
                self.have_money = 0
                self.money_var.set("給与 : {0}円".format(int(self.have_money)))
                self.save_data()

    #前回の給与引き出し記録を追加
    def add_money_record(self):
        wd_time = self.dt_wd.strftime("%m/%d  %H:%M")
        record_money = int(self.have_money)
        str_money_record = "{0}    -{1}円".format(wd_time,record_money)
        self.money_record_lb.insert(0,str_money_record)
        self.money_record_list.insert(0,str_money_record)
    
    #引き出し記録の削除（1件）
    def del_money_record(self):
        index = self.money_record_lb.curselection()
        if index:
            msgbox = messagebox.askyesno("確認","選択中の記録を削除してもよろしいですか？")
            if msgbox:
                self.money_record_lb.delete(int(index[0]))
                self.money_record_list.pop(int(index[0]))
                self.money_record_lb.select_set(int(index[0])-1)
                self.save_data()

    #全ての引き出し記録の削除
    def del_all_money_records(self):
         if self.money_record_lb.size() != 0:
            msgbox = messagebox.askyesno("確認","全ての記録を削除してもよろしいですか？")
            if msgbox:
                self.money_record_lb.delete(0,tk.END)
                self.money_record_list.clear()
                self.save_data()



    #時給の変更
    def chg_wage(self):
        if self.wage_sp.get().isnumeric():
            self.wage = self.wage_sp.get()
            self.wage_var.set("時給 : {0}円".format(self.wage))
            self.save_data()
        else:
            messagebox.showinfo("エラー","時給は整数値で入力して下さい")

    #合計作業時間の更新
    def add_total(self):
        self.total += self.elapsed_time
        self.str_total = self.strtime(self.total)
        self.total_var.set("・合計作業時間  {0}".format(self.str_total))

    #合計作業時間のリセットボタンが押された時
    def reset_total_click(self):
        if self.total:
            msgbox = messagebox.askyesno("確認","合計作業時間をリセットしますか？")
            if msgbox:
                self.total = 0
                self.str_total = self.strtime(self.total)
                self.total_var.set("・合計作業時間  {0}".format(self.str_total))
                self.save_data()



    #self.record_lbのロード
    def record_load(self):
        if self.record_lb.size() == 0:
            for i in self.record_list:
                self.record_lb.insert(tk.END,i) #リストrecord_listの要素をリストボックスrecord_lbにinsertする

    #self.money_record_lbのロード
    def money_record_load(self):
        if self.money_record_lb.size() == 0:
            for i in self.money_record_list:
                self.money_record_lb.insert(tk.END,i) #リストmoney_record_listの要素をリストボックスmoney_record_lbにinsertする

    #データの取得
    def load_data(self):
        with open("log.dat","rb") as fp:
            load_list = pickle.load(fp)
            count = 0
            for i in load_list:
                self.data_list[count] = i
                count += 1
        
        self.wage = self.data_list[0]
        self.have_money = self.data_list[1]
        self.record_list = self.data_list[2]
        self.money_record_list = self.data_list[3]
        self.total = self.data_list[4]

    #データの保存
    def save_data(self):
        data_list = [self.wage,self.have_money,self.record_list,self.money_record_list,self.total]
        with open("log.dat","wb") as fp:
            pickle.dump(data_list,fp)




    #時間をh:mm:ssの形のstr型に変換
    def strtime(self,time):
        hour = int(time / 3600)
        min = int((time / 60) % 60)
        sec =int(time % 60)

        if hour == 0:
            if min < 10:
                if sec < 10:
                    strtime = "0{min}:0{sec}".format(min=min,sec=sec)
                else:
                    strtime = "0{min}:{sec}".format(min=min,sec=sec)
            else:
                if sec < 10:
                    strtime = "{min}:0{sec}".format(min=min,sec=sec)
                else:
                    strtime = "{min}:{sec}".format(min=min,sec=sec)
        else:
            if min < 10:
                if sec < 10:
                    strtime = "{hour}:0{min}:0{sec}".format(hour=hour,min=min,sec=sec)
                else:
                    strtime = "{hour}:0{min}:{sec}".format(hour=hour,min=min,sec=sec)
            else:
                if sec < 10:
                    strtime = "{hour}:{min}:0{sec}".format(hour=hour,min=min,sec=sec)
                else:
                    strtime = "{hour}:{min}:{sec}".format(hour=hour,min=min,sec=sec)

        return strtime


def main():
    root = tk.Tk()
    root.resizable(width=False, height=False) 
    app = Application(master=root)
    app.mainloop()

if __name__ == "__main__":
    main()

```


#このアプリを使ってみて
なかなかいい感じです。

自分は時給を300円に設定して、作業時間×300円を趣味に使っていいというルールで使っています。
5時間作業すれば8時間漫画喫茶に行ける！ やるぞ！！！ 新作ゲーム買うのに20時間勉強しないと！！ やるぞ！！！ と、モチベーションの維持に役立っています。　
#最後に
pythonを触り始めてまだ日が浅く、コードの中に「ここはこうした方がいい」というところがあると思います。今後の勉強に役立てたいので、そういったところが目についた方はコメントで教えていただけるとありがたいです。