渡邉一郎日記

趣味かつ独学の成人男性がPython の様々な内容を記事にして発信しております

【tkinter & nfcpy & pandas】簡易的なオフライン勤怠管理ツールを作ってみた

以前、nfcpy と PaSoRi を利用してICカードのIDmを読み取る方法を記事にしました。

その記事の最後に、「勤怠管理などに利用できるでしょう」的なことを記述したなと思い出しまして。

どうにか作成できないかなということで、結構な力技で作ってみました。

この記事では、「tkinter・nfcpy・pandas」をメインとした簡易的なオフライン勤怠管理ツールを作ってみたので、ざっくり説明します。

IDmの読み取りは容易であるが故に、悪用されやすいというデメリットを持っています。今回作成したツールはIDmだけで登録者を識別しているため、セキュリティ的に心許ないことをご了承ください。

基本的な内容を説明している記事

watanabe-ichiro-nikki.hatenablog.com

watanabe-ichiro-nikki.hatenablog.com

こんな感じです

実行例

id-name.csvの中身

登録者名.csvの中身

ソースコード

ちょっと長いのでこちらに載せてます↓

github.com

フォルダ構成

Documents/
    └ kintai/
         ├ 登録者名.csv(随時作成、記録用:日付・時間・出/退)
         |
         ├ id_name.csv(紐づけ用:ID・名前)
         |
         └ kintai_tool.py(GUI生成、紐づけ処理、記録処理)
PATH
>> C:/User/Owner/Documents/kintai/登録者名.csv
>> C:/User/Owner/Documents/kintai/id_name.csv
>> C:/User/Owner/Documents/kintai/kintai_tool.py

使用ライブラリ

import tkinter as tk
import pandas as pd
import nfc
import binascii
import datetime as dt
import subprocess

基本設定

#-----------------------------------基本設定-----------------------------------

# メインウィンドウ作成
root = tk.Tk()

# ウィンドウ内のエリア変化率を等倍に指定
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)

# ウィンドウの大きさを決定
root.minsize(width=800, height=700)
root.maxsize(width=800, height=700)

# 使用したいフォント
yu_font = "游ゴシック"

# ボタンにカーソルが乗ったときの色
def enter_bg(event):
  event.widget["bg"] = "#D3D3D3"

# ボタンからカーソルが離れたときの色
def leave_bg(event):
  event.widget["bg"] = "SystemButtonFace"

# マウスカーソル時のポイント変更
mouse = "hand2"


# フォルダ・ファイルのパス
folder_path = r"C:\Users\Owner\Documents\kintai"
id_path = fr"{folder_path}\id_name.csv"

# id_name.csvが無ければ作成
if os.path.exists(id_path) == False:
  # id_name.csvファイルのテンプレート作成
  df_temp = pd.DataFrame([[None]*5],
                         columns=["time", "last_name", "first_name", "full_name", "id"])
  df_temp = df_temp.drop([0])
  #
  # id_name.csvファイルを作成
  df_temp.to_csv(id_path, index=None, encoding="shift jis")

#-----------------------------------------------------------------------------

home_display()

root.mainloop()

ここでは、フレームやウィジェット、その他機能を作っていく前の基本的な設定を行なっていきます。

  • tk.Tk() でウィンドウを作成
  • grid_rowconfigure と grid_columnconfigure で、ウィンドウ内の [0, 0] エリアの変化率を等倍に指定(ウィンドウを拡大・縮小した際にエリアが等倍で変化する)
  • minsize と maxsize の両方を同値で指定することで、ウィンドウの大きさを固定
  • ウィジェットへ表示させる文字に対して、font を指定するためにここで決めておく(後のウィジェット作成時にこれを指定する)
  • Buttonウィジェットにカーソルを 乗せたときの色 と 離したときの色 を変化させる関数を作成(色を16進数で指定していますが、文字列でも可能)
  • Buttonウィジェットにカーソルを乗せるとカーソルの形が変化するようにここで決めておく(font 同様にウィジェット作成時にこれを指定する|カーソルの種類はこちら
  • PATHを指定するたびに全てを記述するのは大変なので、ここで指定しておく
  • IDmと氏名を紐づけるCSVファイルがなければ作成する if 文を設置
  • home_display() で最初のフレームが呼び出される
  • root.mainloop() でウィンドウが削除されるまでイベントをループさせる

ホーム画面

ホーム画面

#-----------------------------------1.ホーム画面-----------------------------------

def home_display():
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール")
  #
  # メインページフレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # ボタン作成
  button1_1 = tk.Button(frame, text="出社確認", font=(yu_font, 35, "bold"),
                        cursor=mouse, command=in_office1)
  button1_2 = tk.Button(frame, text="退社確認", font=(yu_font, 35, "bold"),
                        cursor=mouse, command=out_office1)
  button1_3 = tk.Button(frame, text="メンバー登録", font=(yu_font, 35, "bold"),
                        cursor=mouse, command=subscribe_member1)
  button1_4 = tk.Button(frame, text="管理ファイルを開く", font=(yu_font, 15),
                        fg="blue", cursor=mouse, command=folder_open)
  #
  # ボタンのカーソル時色変更
  button1_1.bind("<Enter>", enter_bg)
  button1_1.bind("<Leave>", leave_bg)
  button1_2.bind("<Enter>", enter_bg)
  button1_2.bind("<Leave>", leave_bg)
  button1_3.bind("<Enter>", enter_bg)
  button1_3.bind("<Leave>", leave_bg)
  button1_4.bind("<Enter>", enter_bg)
  button1_4.bind("<Leave>", leave_bg)
  #
  # ボタン配置
  button1_1.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  button1_2.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  button1_3.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  button1_4.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  #
  # frameを最上面へ
  frame.tkraise()


def folder_open():
  # 管理ファイルがあるフォルダを開く
  subprocess.Popen(["explorer", folder_path], shell=True)

#------------------------------------------------------------------------------

最初に表示するフレームにウィジェットを配置して機能を持たせていきます。

  • ウィンドウのタイトルを「勤怠管理ツール」に指定
  • フレームを作成して、ウィンドウの [0, 0] エリアに設置(tk.NSEW で上下左右に変化できるように設定)
  • 「出社確認」「退社確認」「メンバー登録」「管理ファイルを開く」の4つのボタンを作成(ここで、font や cursor を指定する)
  • 各ボタンに対して、カーソルが 乗ったとき・離れたとき に色が変化するように設定
  • 「管理ファイルを開く」ボタンに持たせる関数では、エクスプローラーでフォルダを開くように設定

出社確認

ホーム画面のフレーム から 各フレーム への遷移は、該当フレームを最上面に表示させることで動かしています。

どのフレームも構成としてはほとんど変わらないので、共通している部分は少しずつ省略していきます(笑)

画面1|出社確認

画面1(出社確認)

#-----------------------------------2_1.出社確認-----------------------------------

def in_office1():
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール|出社確認")
  #
  # フレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # ラベル・ボタン作成
  label2_1 = tk.Label(frame, text="出社確認", font=(yu_font, 35))
  label2_2 = tk.Label(frame, text="ICカードを置いて\r\nOKボタンを押してください",
                      font=(yu_font, 35, "bold"))
  button2_1 = tk.Button(frame, text="OK", font=(yu_font, 25, "bold"),
                        cursor=mouse, command=ic_read2)
  button2_2 = tk.Button(frame, text="ホームへ", font=(yu_font, 15),
                        cursor=mouse, command=home_display)
  #
  # ボタンのカーソル時色変更
  button2_1.bind("<Enter>", enter_bg)
  button2_1.bind("<Leave>", leave_bg)
  button2_2.bind("<Enter>", enter_bg)
  button2_2.bind("<Leave>", leave_bg)
  #
  # ラベル・ボタン配置
  label2_1.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label2_2.pack(side=tk.TOP, pady=30, ipadx=20, ipady=10)
  button2_1.pack(pady=20, ipadx=20, ipady=10)
  button2_2.pack(side=tk.BOTTOM, pady=20)
  #
  # frameを最上面へ
  frame.tkraise()


def ic_read2():
  # 読み取り待機
  clf = nfc.ContactlessFrontend("usb")
  try:
    tag = clf.connect(rdwr={"targets": ["212F", "424F"],
                            "on-connect": lambda tag: False})
  finally:
    clf.close()
  #
  # タグ情報からIDmを抽出
  if tag.TYPE == "Type3Tag":
    idm = binascii.hexlify(tag.idm).decode()
  #
  # 次のフレームへ
  in_office2(idm)

#-------------------------------------------------------------------------------

このフレームでは、OKボタンを押した時にPaSoRiでIDmを読み取る機能を持たせています。

  • nfc.ContactlessFrontend("usb") で読み取り開始
  • rdwr でタグの読み書きを指定
  • "targets": ["212F", "424F"] で読み込むデータレートを指定(対象が Type4Tag を持っている場合、データレートを指定して Type3Tag の情報のみを読み込む)
  • "on-connect" は False となった時にタグ情報を返すため、tag を引数に持った関数を False で指定
  • clf.close() でリーダーデバイスを閉じる
  • 読み取ったタグ情報から idm の値を取り出す
  • idm の情報を引数として次のフレームの関数へ渡す

画面2|出社確認

画面2(出社確認)

#-----------------------------------2_2.出社確認-----------------------------------

def in_office2(id_info):
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール|出社確認")
  #
  # フレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # 現時刻を取得
  date2 = dt.datetime.now().strftime(r"%Y/%m/%d")
  time2 = dt.datetime.now().strftime(r"%X")
  #
  # 読み取ったカードの持ち主の名前を取得
  df_read = pd.read_csv(id_path, header=0, index_col=None, encoding="shift jis")
  human_info = df_read[df_read["id"]==str(id_info)]
  last_name = human_info.iat[0, 1]
  full_name = human_info.iat[0, 3]
  #
  # 該当者のcsvファイルに記録
  df_rec2 = pd.read_csv(fr"{folder_path}\{full_name}.csv",
                        header=0, index_col=None, encoding="shift jis")
  df_app2 = pd.DataFrame([[date2, time2, "出"]], columns=df_rec2.columns.values)
  df_rec2 = pd.concat([df_rec2, df_app2])
  df_rec2.to_csv(fr"{folder_path}\{full_name}.csv", index=None, encoding="shift jis")
  #
  # ラベル・ボタン作成
  label2_3 = tk.Label(frame, text="読み取り完了!", font=(yu_font, 35, "bold"))
  label2_4 = tk.Label(frame, text=fr"{last_name}さん、おはようございます!",
                      font=(yu_font, 35, "bold"))
  label2_5 = tk.Label(frame, text=fr"タイムスタンプ|{time2}", font=(yu_font, 35, "bold"))
  #
  # ラベル・ボタン配置
  label2_3.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label2_4.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label2_5.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  #
  # 3秒後、ホーム画面へ戻る
  label2_4.after(3000, home_display)
  #
  # frameを最上面へ
  frame.tkraise()

#-------------------------------------------------------------------------------

このフレームでは、ID情報と紐付けられた登録者の名字と読み取ったときの時間を表示させています。また、裏ではCSVファイルに[日付・時間・出社]を記録しています。

  • 1行目をヘッダーにして id_name.csv を開く
  • id_name.csv 内の「id」列から、読み取った IDm と同じ文字列を取得してその1行を抽出
  • 抽出した1行には、登録者の情報:[登録日時・姓・名・氏名・ID]が入っている
  • iat[0, 1]:姓、iat[0, 3]:氏名
  • 1行目をヘッダーにして 登録者名.csv を開く
  • 読み取った日時と”出”を記録
  • 記録したファイルは「index=None」を指定して保存
  • このフレームを表示した3秒後にホーム画面に戻るよう設定

退社確認

退社確認のフレームは、出社確認とほとんど同じなので説明は省略します(笑)

画面1|退社確認

画面1(退社確認)

#-----------------------------------3_1.退社確認-----------------------------------

def out_office1():
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール|退社確認")
  #
  # フレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # ラベル・ボタン作成
  label3_1 = tk.Label(frame, text="退社確認", font=(yu_font, 35))
  label3_2 = tk.Label(frame, text="ICカードを置いて\r\nOKボタンを押してください",
                      font=(yu_font, 35, "bold"))
  button3_1 = tk.Button(frame, text="OK", font=(yu_font, 25, "bold"),
                        cursor=mouse, command=ic_read3)
  button3_2 = tk.Button(frame, text="ホームへ", font=(yu_font, 15),
                        cursor=mouse, command=home_display)
  #
  # ボタンのカーソル時色変更
  button3_1.bind("<Enter>", enter_bg)
  button3_1.bind("<Leave>", leave_bg)
  button3_2.bind("<Enter>", enter_bg)
  button3_2.bind("<Leave>", leave_bg)
  #
  # ラベル・ボタン配置
  label3_1.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label3_2.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  button3_1.pack(pady=20, ipadx=20, ipady=10)
  button3_2.pack(side=tk.BOTTOM, pady=20)
  #
  # frameを最上面へ
  frame.tkraise()


def ic_read3():
  # 読み取り待機
  clf = nfc.ContactlessFrontend("usb")
  try:
    tag = clf.connect(rdwr={"targets": ["212F", "424F"],
                            "on-connect": lambda tag: False})
  finally:
    clf.close()
  #
  # タグ情報からIDmを抽出
  if tag.TYPE == "Type3Tag":
    idm = binascii.hexlify(tag.idm).decode()
  #
  # 次のフレームへ
  out_office2(idm)

#-------------------------------------------------------------------------------

画面2|退社確認

画面2(退社確認)

#-----------------------------------3_2.退社確認-----------------------------------

def out_office2(id_info):
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール|出社確認")
  #
  # フレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # 現時刻を取得
  date3 = dt.datetime.now().strftime(r"%Y/%m/%d")
  time3 = dt.datetime.now().strftime(r"%X")
  #
  # 読み取ったカードの持ち主の名前を取得
  df_read = pd.read_csv(id_path, header=0, index_col=None, encoding="shift jis")
  human_info = df_read[df_read["id"]==str(id_info)]
  last_name = human_info.iat[0, 1]
  full_name = human_info.iat[0, 3]
  #
  # 該当者のcsvファイルに記録
  df_rec3 = pd.read_csv(fr"{folder_path}\{full_name}.csv",
                        header=0, index_col=None, encoding="shift jis")
  df_app3 = pd.DataFrame([[date3, time3, "退"]], columns=df_rec3.columns.values)
  df_rec3 = pd.concat([df_rec3, df_app3])
  df_rec3.to_csv(fr"{folder_path}\{full_name}.csv", index=None, encoding="shift jis")
  #
  # ラベル・ボタン作成
  label3_3 = tk.Label(frame, text="読み取り完了!", font=(yu_font, 35, "bold"))
  label3_4 = tk.Label(frame, text=fr"{last_name}さん、お疲れ様でした!",
                      font=(yu_font, 35, "bold"))
  label3_5 = tk.Label(frame, text=fr"タイムスタンプ|{time3}", font=(yu_font, 35, "bold"))
  #
  # ラベル・ボタン配置
  label3_3.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label3_4.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label3_5.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  #
  # 3秒後、ホーム画面へ戻る
  label3_4.after(3000, home_display)
  #
  # frameを最上面へ
  frame.tkraise()

#-------------------------------------------------------------------------------

メンバー登録

メンバー登録では、

「id_name.csv に登録者情報を記録」
「登録者のCSVファイル作成(登録者名.csv)」
「IDの重複を弾く処理」

を行ないます。

画面1|メンバー登録

画面1(メンバー登録)

#-----------------------------------4_1.メンバー登録-----------------------------------

def subscribe_member1():
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール|メンバー登録")
  #
  # フレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # ラベル・ボタン作成
  label4_1 = tk.Label(frame, text="氏名を入力してください", font=(yu_font, 35, "bold"))
  label4_2 = tk.Label(frame, text="姓:", font=(yu_font, 25))
  label4_3 = tk.Label(frame, text="名:", font=(yu_font, 25))
  text4_1 = tk.Entry(frame, font=(yu_font, 20))
  text4_2 = tk.Entry(frame, font=(yu_font, 20))
  button4_1 = tk.Button(frame, text="OK", font=(yu_font, 20), cursor=mouse,
                        command=lambda: subscribe_member2(text4_1.get(),text4_2.get()))
  button4_2 = tk.Button(frame, text="ホームへ", font=(yu_font, 15), cursor=mouse,
                        command=home_display)
  #
  # ボタンのカーソル時色変更
  button4_1.bind("<Enter>", enter_bg)
  button4_1.bind("<Leave>", leave_bg)
  button4_2.bind("<Enter>", enter_bg)
  button4_2.bind("<Leave>", leave_bg)
  #
  # ラベル・ボタン配置
  label4_1.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label4_2.place(x=200, y=200)
  text4_1.place(x=300, y=200, width=300, height=35)
  label4_3.place(x=200, y=300)
  text4_2.place(x=300, y=300, width=300, height=35)
  button4_1.place(x=350, y=400, width=100, height=50)
  button4_2.pack(side=tk.BOTTOM, pady=20)
  #
  # frameを最上面へ
  frame.tkraise()

#-------------------------------------------------------------------------------------

Buttonウィジェットの command には関数を渡しますが、引数を与えた関数を渡したい場合は lambda を使用します。

「function」と「function()」では少し違います。

「function」は、関数本体を指します。
「function()」は、関数が実行されます。

よって、引数を与えた状態の関数は勝手に実行されてしまうため、lambda を使って引数を与えた関数を関数本体として認識させています。

  • tk.Entry(frame) で姓・名を入力する欄を作成
  • 入力された文字列を「.get()」で取得して次のフレームの関数へ渡す

画面2|メンバー登録

画面2(メンバー登録)

#-----------------------------------4_2.メンバー登録-----------------------------------

def subscribe_member2(last_name2, first_name2):
  # 名前の情報は上の関数から変数として渡す
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール|メンバー登録")
  #
  # フレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # ラベル・ボタン作成
  label4_4 = tk.Label(frame, text=f"{last_name2} {first_name2}さん\r\nでよろしいでしょうか?",
                      font=(yu_font, 35, "bold"))
  button4_3 = tk.Button(frame, text="OK", font=(yu_font, 20), cursor=mouse,
                        command=lambda: subscribe_member3(last_name2, first_name2))
  button4_4 = tk.Button(frame, text="戻る", font=(yu_font, 20), cursor=mouse,
                        command=subscribe_member1)
  button4_5 = tk.Button(frame, text="ホームへ", font=(yu_font, 15), cursor=mouse,
                        command=home_display)
  #
  # ボタンのカーソル時色変更
  button4_3.bind("<Enter>", enter_bg)
  button4_3.bind("<Leave>", leave_bg)
  button4_4.bind("<Enter>", enter_bg)
  button4_4.bind("<Leave>", leave_bg)
  button4_5.bind("<Enter>", enter_bg)
  button4_5.bind("<Leave>", leave_bg)
  #
  # ラベル・ボタン配置
  label4_4.pack(side=tk.TOP, pady=60, ipadx=20, ipady=20)
  button4_3.place(x=250, y=400, width=100, height=50)
  button4_4.place(x=450, y=400, width=100, height=50)
  button4_5.pack(side=tk.BOTTOM, pady=20)
  #
  # frameを最上面へ
  frame.tkraise()

#-------------------------------------------------------------------------------------

このフレームでは、OKボタンを押すと次のフレームへ、戻るボタンを押すと前のフレームへ遷移するように機能を持たせています。

次のフレームへ遷移する場合は、最初に入力された「姓・名」の文字列をそのまま渡します。

画面3|メンバー登録

画面3(メンバー登録)

#-----------------------------------4_3.メンバー登録-----------------------------------

def subscribe_member3(last_name3, first_name3):
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール|メンバー登録")
  #
  # フレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # ラベル・ボタン作成
  label4_5 = tk.Label(frame, text="氏名を登録しました!", font=(yu_font, 35, "bold"))
  label4_6 = tk.Label(frame, text="ICカードを置いて\r\nOKボタンを押してください",
                      font=(yu_font, 35, "bold"))
  button4_6 = tk.Button(frame, text="OK", font=(yu_font, 25, "bold"), cursor=mouse,
                        command=lambda: ic_read4(last_name3, first_name3))
  button4_7 = tk.Button(frame, text="ホームへ", font=(yu_font, 15), cursor=mouse,
                        command=home_display)
  #
  # ボタンのカーソル時色変更
  button4_6.bind("<Enter>", enter_bg)
  button4_6.bind("<Leave>", leave_bg)
  button4_7.bind("<Enter>", enter_bg)
  button4_7.bind("<Leave>", leave_bg)
  #
  # ラベル・ボタン配置
  label4_5.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label4_6.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  button4_6.pack(pady=20, ipadx=20, ipady=10)
  button4_7.pack(side=tk.BOTTOM, pady=20)
  #
  # frameを最上面へ
  frame.tkraise()


def ic_read4(last, first):
  # 読み取り待機
  clf = nfc.ContactlessFrontend("usb")
  try:
    tag = clf.connect(rdwr={"targets": ["212F", "424F"],
                            "on-connect": lambda tag: False})
  finally:
    clf.close()
  #
  # タグ情報からIDmを抽出
  if tag.TYPE == "Type3Tag":
    idm = binascii.hexlify(tag.idm).decode()
  #
  # idmの重複確認 > 重複していたらエラー画面|重複していなければ記録
  df_rec4 = pd.read_csv(id_path, header=0, index_col=None, encoding="shift jis")
  id_conf = df_rec4[df_rec4["id"]==idm]
  if id_conf.size > 0:
    # 重複している > エラー画面
    subscribe_error(last, first)
  else:
    # 重複していない > id_name.csvに登録者の情報を記録
    date4 = dt.datetime.now().strftime(r"%Y/%m/%d")
    time4 = dt.datetime.now().strftime(r"%X")
    df_app4 = pd.DataFrame([[f"{date4} {time4}", last, first, last + first, idm]],
                           columns=df_rec4.columns.values)
    df_rec4 = pd.concat([df_rec4, df_app4])
    df_rec4.to_csv(id_path, index=None, encoding="shift jis")
    #
    # 登録者のcsvファイルを作成
    df_make = pd.DataFrame([[None]*3], columns=["日付", "時間", "出/退"])
    df_make = df_make.drop([0])
    df_make.to_csv(fr"{folder_path}\{last}{first}.csv",
                   index=None, encoding="shift jis")
    #
    # 次のフレームへ
    subscribe_member4(last)

#-------------------------------------------------------------------------------------

このフレームでは、

「登録者のIDと他の登録者のIDの重複がないかの確認」
「重複していなければ、[登録日時・姓・名・氏名・ID]を id_name.csv へ記録、登録者名.csv の作成、次のフレームへの遷移、を実行」
「重複していれば、エラー画面への遷移を実行」

を行なっています。

  • 変数:id_conf は、IDの重複がなければ0行5列のデータとなり id_conf.size は「0」となる
  • IDが重複していれば、id_conf には該当行(1行5列)のデータが代入され id_conf.size は「>0(この場合 5 )」となる
  • 次のフレームへの遷移では、「姓」の文字列を渡す

画面4|メンバー登録

IDの重複分岐(左:重複なし、右:重複あり)

#-----------------------------------4_4.メンバー登録-----------------------------------

def subscribe_member4(name):
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール|メンバー登録")
  #
  # フレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # ラベル・ボタン作成
  label4_7 = tk.Label(frame, text="ICカードを登録しました!", font=(yu_font, 35, "bold"))
  label4_8 = tk.Label(frame, text=f"{name}さんよろしくお願いします!",
                      font=(yu_font, 35, "bold"))
  #
  # ラベル・ボタン配置
  label4_7.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label4_8.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  #
  # 3秒後、ホーム画面へ戻る
  label4_8.after(3000, home_display)
  #
  # frameを最上面へ
  frame.tkraise()


# id情報重複エラー画面
def subscribe_error(last_name4, first_name4):
  # ウィンドウのタイトル
  root.title(r"勤怠管理ツール|メンバー登録")
  #
  # フレーム作成
  frame = tk.Frame(root)
  frame.grid(row=0, column=0, sticky=tk.NSEW)
  #
  # ラベル・ボタン作成
  label4_9 = tk.Label(frame, text="ID情報が重複しています", font=(yu_font, 35, "bold"))
  label4_10 = tk.Label(frame, text="もう一度やり直してください",
                       font=(yu_font, 35, "bold"))
  #
  # ラベル・ボタン配置
  label4_9.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  label4_10.pack(side=tk.TOP, pady=20, ipadx=20, ipady=20)
  #
  # 2秒後、前の画面へ戻る
  label4_10.after(2000, lambda: subscribe_member3(last_name4, first_name4))
  #
  # frameを最上面へ
  frame.tkraise()

#-------------------------------------------------------------------------------------

IDが重複していない場合は、登録者を「姓」+さん で表示させて登録が完了したことを伝えます。

IDが重複していた場合は、その旨の文章を表示させて前のフレームへ戻ります。このとき、「姓・名」はまだ登録されていないため、前のフレームの関数に引数として渡しています。

  • 登録が完了した場合は、このフレームを3秒間表示させた後にホーム画面へ戻る
  • エラーの関数には前もって「姓・名」のデータを引数として渡しておき、前のフレームへ戻る際にそのまま引数として渡す

改良したい部分

tkinter のフリーズ

tkinterのフリーズ画面

カードがない状態でOKボタンを押すと、 tkinter がフリーズします。

作り始めたときは、フレームを表示している間はずっと読み取りができる状態にしようとしていました。

ですが、どうやら tkinter 自体が「待機する」ことに向いていないようで、フリーズ祭りでした。

もし、改善策があればご教授いただきたいです。よろしくお願いします。

追記:フルネームが同じだと上書きされる

登録者名.csv を作成する際に、
「登録者名が同じだとCSVファイルが上書きされてしまう」
ということに気付きました。

もし、同名の方がいらっしゃる場合は何かしら名前に文字を加えてください(笑)

よろしくお願いいたします。

データレートの指定

今回のプログラムでは、IDを読み取る時にデータレートを ["212F", "424F"] と指定しました。

これだと、スマホや非接触ICカードは読み取れても、クレジットカードやマイナンバーカードは読み取れません。

もし、後者も使いたい場合は、OKボタンではなく
「非接触型IC・スマホ」「クレカ・マイナンバーカード」
の2つのボタンを設置して、データレート指定を合わせてあげれば可能かなと思います。

多分(笑)

まとめ

この記事では、「tkinter・nfcpy・pandas」をメインとした簡易的なオフライン勤怠管理ツールという力技で作ったツールについて、ざっくり説明しました。

「簡易的な」「オフライン」というワードを入れた理由は、「複雑な」「オンライン」ツールを作るのにはまだまだ勉強が足りないからです(笑)

Flask や Django などはまだ触れていない領域で、これから勉強したい内容の1つです。

もし、何かしら作れるようになったら、今度は「簡易的な」「オンライン」ツールを作りたいなと思っています。