はじめに
暇なので作りました
コードは全部Geminiに投げました
技術スタック
- Python 3.12
- CustomTkinter
- JSON / CSV (utf-8-sig)
- PyInstaller (--onefile --noconsole)
動作画面
管理者画面
パネルボタン説明
- リアルタイム更新はon/offが可能
- 間隔はmsで管理可能(初期設定は3000ms)
- ログ出力フォルダは管理者パネルで設定可能
- 表示名を管理者側で管理可能
- 初期設定はPC名を割り当てるため
- タスクリストはcsvで追加/削除
- 共有設定を保存
- json形式で出力する※後述
- 全ログデータを集計出力
- 接続先変更ボタンで参照
config.jsonを変更
ユーザー利用画面
パネルボタン説明
- 画面真ん中コンボボックスでタスクを設定
- 管理画面で設定したタスクリストを参照する
- 休憩入り, 業務終了はボタンを押すだけ
- 休憩は稼働時間がカウントされる
- 工数計算用
- 休憩は稼働時間がカウントされる
- 設定ファイルを再選択ボタンで参照
config.jsonを変更
設計について
技術に詳しくない人が、劣悪な環境(共有フォルダ運用・低スペックPC)でも確実に動かせること
- 設定ファイルの配布フェーズの簡略化
- SSOT (Single Source of Truth) の徹底
- 「休憩」「終了」 工数の導入
- Excelファーストの設計
- 低スペックPCでも動くように調整
設定ファイルの配布フェーズの簡略化
通常、クライアント・サーバー型の構成にしない場合、各PCに設定ファイルを配る手間が発生
これが導入・運用の最大の壁になる
↓
初期設定ウィザード方式で対処
アプリ側に「設定ファイルのパスを記憶する(admin_settings.json / user_settings.json)」機能を持たせた
→管理者は共有フォルダにconfig.jsonを1つ置くだけでOK
ユーザーは初回起動時にそのファイルを選ぶだけで、自分専用の接続設定がローカルに保存される
- 接続先パスの動的解決
起動時に「自分の足元にパスの記憶があるか」をチェックし、なければファイルダイアログでユーザーに指定させる
import os
import json
from tkinter import filedialog, messagebox
# ローカルに保存される「接続先記憶ファイル」
LOCAL_MEMO_FILE = "user_settings.json"
def get_shared_config():
# 1. ローカルの記憶を確認
if os.path.exists(LOCAL_MEMO_FILE):
with open(LOCAL_MEMO_FILE, "r", encoding="utf-8") as f:
config_path = json.load(f).get("config_path")
# 記憶されたパスが有効ならそれを返す
if config_path and os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as cf:
return json.load(cf), config_path
# 2. 記憶がない、またはパスが無効ならユーザーに場所を聞く
messagebox.showinfo("初期設定", "共有フォルダ内の config.json を選択してください。")
path = filedialog.askopenfilename(title="config.jsonを選択", filetypes=[("JSON", "*.json")])
if path:
# 選んでもらったパスを次回の起動のためにローカルへ保存
with open(LOCAL_MEMO_FILE, "w", encoding="utf-8") as f:
json.dump({"config_path": path}, f)
with open(path, "r", encoding="utf-8") as cf:
return json.load(cf), path
return None, None
- 管理者側で最初の
config.jsonが存在しない場合のデッドロック回避
管理者は「最初のconfig.json」を作る役割も兼ねるため、既存ファイルを選ぶ(askopenfilename)だけでなく、新しく作る(asksaveasfilename)処理を分岐させ、デッドロックを回避
# 管理者パネル起動時の分岐ロジック(抜粋)
if not config_exists:
# 既存接続か新規作成かをユーザーに問う
is_connect = messagebox.askyesno("初期設定", "既存のconfig.jsonに接続しますか?\n(いいえを選ぶと新規作成します)")
if is_connect:
path = filedialog.askopenfilename(...) # 上書き警告が出ない
else:
path = filedialog.asksaveasfilename(...) # 新規ファイルを作成
SSOT (Single Source of Truth) の徹底
前項で説明した通り、管理者・利用者ともに、すべて1つのconfig.jsonを見に行く。
- マッピング管理: PC名と表示名(個人名)の紐付けを管理者が一括で設定可能。これにより、ユーザーに自分の名前を入力してと頼む必要がなくなる
-
デッドロックの回避: 管理者パネルは
config.jsonがなければその場で生成。これが最初の1個となり、システム全体が動き出す起点になる
- ID管理の外部化(PC名 → 表示名への解決)
ユーザーアプリは「自分が誰か」を知る必要はありません。ただPC名(Hostname)をログに刻むだけ
表示名への変換は、管理者が一元管理するconfig.json内のマッピングリストによって「後付け」で解決
# 管理者パネル:共有設定からPC名を表示名に変換するコア処理
def get_user_display_name(self, pc_name):
"""
config.jsonに記述されたマッピングを参照し、
未登録のPCはそのままの名前を、登録済みなら表示名を返す。
"""
mapping = self.config.get("user_mapping", {})
return mapping.get(pc_name, pc_name)
- 管理者による「動的な状態監視」
全ユーザーの進捗ログが1つのフォルダに集約され、管理者はそのファイルを読み取り専用で叩き続けることで、DBなしでリアルタイム監視を実現
# 管理者パネル:共有フォルダの全ファイルを走査し、最新の状態をマッピング
def refresh_monitor(self):
log_path = self.config["save_path"]
for f in glob.glob(os.path.join(log_path, "*.csv")):
pc_name = os.path.basename(f).replace(".csv", "")
# 表示名の解決(SSOT参照)
display_name = self.get_user_display_name(pc_name)
with open(f, "r", encoding="utf-8-sig") as csv_f:
rows = list(csv.reader(csv_f))
if len(rows) > 1:
last_row = rows[-1]
# 終了時間が空 = 「現在進行中」と定義(ビジネスロジックの共有)
if not last_row[2]:
# ここでTreeview(画面表示)を更新
self.tree.insert("", "end", values=(display_name, last_row[0], ...))
「休憩」「終了」 工数の導入
主に休憩時間という工数を管理したい現場向け
- 休憩入り: 現在のタスクを締め、自動で「休憩」タスクを開始。工数として時間をカウントし続ける
- 業務終了: その瞬間にすべての記録をストップ。翌朝まで「休憩中」や「作業中」が残ってしまう事故を防ぐ
- 共通処理:現在タスクの封印
どんな遷移をするにしても、「今やっていること」を確定させる処理を共通化
def stop_current_task(self):
"""
現在稼働中のタスクがあれば、その瞬間の時刻を『終了時間』として
CSVの3列目に打刻して1行を完結させる。
"""
if self.current_task and self.start_time:
end_time = datetime.now()
# 既存の開始行に対して、終了時間を追記して保存
self.save_log(self.current_task, self.start_time, end_time)
- 「休憩入り」:状態の自動遷移
「休憩」も一つの工数として計上するため、現在の作業を止めた直後に、自動で「休憩」という名前のタスクを開始
def start_break(self):
# 1. 今の作業を終了打刻
self.stop_current_task()
# 2. 即座に『休憩』タスクとして新しい行を開始(終了時間は空のまま)
self.current_task = "休憩"
self.start_time = datetime.now()
self.save_log(self.current_task, self.start_time, None) # 稼働中として記録
# UI側の表示もオレンジ(休憩中)に切り替え
self.status_label.configure(text="休憩中", text_color="orange")
- 「業務終了」:記録の完全停止
業務終了は「次」を開始しません。これにより、翌朝まで「休憩中」が延々と記録される状態を防止
def finish_work(self):
if not self.current_task: return
# 確認ダイアログで誤操作を防止
if messagebox.askyesno("業務終了", "本日の全業務記録を完了しますか?"):
# 最後の作業を閉じるだけで、新しいタスクは生成しない
self.stop_current_task()
# 全ての状態をクリア
self.current_task = None
self.start_time = None
self.status_label.configure(text="業務終了", text_color="red")
Excelファースト
現場でよく使うのはExcelだと思われるため
- UTF-8-SIG (BOM付き): すべてのCSV出力をBOM付きに固定。これにより、Excelでダブルクリックした瞬間に文字化けする現象を根絶
- インポート/エクスポート: タスクリストをUI上でポチポチ編集させるのではなく、CSVで一括操作させることで、大量のタスク管理もExcelのオートフィルで対応可能に
-
UTF-8-SIG (BOM付き)による文字化け根絶
Pythonのデフォルト(UTF-8)でCSVを吐くと、Excelで開いた瞬間に日本語が文字化けする。これを防ぐために、すべてのファイル入出力にutf-8-sigを指定し、ExcelにUTF-8であることを明示する
import csv
def save_csv_excel_friendly(file_path, data_rows):
"""
Excelでダブルクリックした瞬間に正しく開けるCSVを生成する
"""
with open(file_path, "w", encoding="utf-8-sig", newline="") as f:
writer = csv.writer(f)
# ヘッダーとデータを一気に書き込む
writer.writerows(data_rows)
- タスク管理の「Excelへの丸投げ」 (Import/Export)
タスクリストの追加・削除画面をGUIの廃止
代わりに、「テンプレートを出力 → Excelで一括編集 → 読み込み」というフローにすることで、100個のタスク追加もExcelのオートフィルで対応
# 管理者パネル:タスクリストのインポート処理(抜粋)
def import_tasks(self):
path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
if path:
with open(path, "r", encoding="utf-8-sig") as f:
reader = csv.reader(f)
next(reader) # 1行目のヘッダーをスキップ
# Excelで編集された「タスク名」の列をリスト化
new_tasks = [row[0] for row in reader if row]
# UI上のテキストエリアに流し込み(人間が最後に確認して保存)
self.task_text.delete("1.0", "end")
self.task_text.insert("1.0", "\n".join(new_tasks))
低スペックPCでも動くように調整
- リアルタイム監視のon/off機能:負荷が高い仕事中はオフにするなど、柔軟な対応をできるように
- ms単位のポーリング制御: リアルタイム監視の間隔を管理者が調整。PCが重ければ間隔を広げ、余裕があれば1秒更新など調整可能
- ms(ミリ秒)単位の可変ポーリング
監視間隔をハードコーディングせず、config.jsonから動的に取得するように
現場の負荷状況に合わせて、管理者それぞれが「1秒」から「10秒」まで、GUI上でいつでも調整可能
def refresh_monitor(self):
# リアルタイム更新のチェックが入っている時だけ実行
if self.auto_refresh.get():
self.update_tree_view() # 全ログファイルの走査処理
# 設定値(ms)を取得。入力エラーがあれば3000ms(3秒)で安全にフォールバック
try:
interval_ms = int(self.refresh_entry.get())
except ValueError:
interval_ms = 3000
# 指定されたミリ秒後に自分自身を再実行
self.root.after(interval_ms, self.refresh_monitor)
- 差分なしの読み込み
軽量化を意識
データベースを使わず、かつネットワーク負荷を抑えるため、利用者側は「追記のみ」、管理者側は「最新の1行だけ見る」という役割分担を徹底
# 管理者側:全ファイルを走査するが、中身は「最後の一行」しか読み込まない
with open(log_file_path, "r", encoding="utf-8-sig") as f:
lines = list(csv.reader(f))
if len(lines) > 1:
last_status = lines[-1]
# 終了時間が空(未入力)= 現在稼働中と判定
if not last_status[2]:
self.display_active_user(pc_name, last_status)
PyInstaller オプションについて
-
--onefile:
.exeファイルを一つ、共有フォルダに置くだけ
「これをデスクトップにコピーして使ってください」という一言で導入が完結するように設計 -
--noconsole: コンソール画面を非表示に -
--name "進捗入力くん":
「ファイル名」もユーザーインターフェース
デフォルト名だとuser_app.exeになるけど、これだと何のソフトか直感的にわからない -
--clean:
PyInstallerはキャッシュが残ると、たまに謎のビルドエラーや実行時エラーを起こすことがあるため
pyinstaller --onefile --noconsole --name "進捗管理者パネル" admin_app.py
追記(2026/3/10)
リアルタイム進捗反映とログの整合性向上
- ユーザー側:タスク開始時に仮登録
def switch_task(self):
# (中略:既存タスクの停止処理など)
# 1. 新しいタスクの情報をセット
self.current_task = new_task
self.start_time = datetime.now()
# 2. 開始した瞬間にCSVに「終了空欄」で書き込む
# これにより、管理者がファイルを読み込んだ時に「今のタスク」が見えるようになる
self.save_log(self.current_task, self.start_time, None)
- 管理者側:終了時間が空のデータを探す
# 管理者側の判定ロジック
with open(f, "r", encoding="utf-8-sig") as csv_f:
r = list(csv.reader(csv_f))
# 最終行の終了時間(インデックス2)が空かどうかをチェック
if len(r) > 1 and not r[-1][2]:
# 進行中としてTreeviewに表示
start = datetime.strptime(r[-1][1], "%Y-%m-%d %H:%M:%S")
elap = str(datetime.now() - start).split(".")[0]
self.tree.insert("", "end", values=(name, r[-1][0], elap, adm))
※タスクは例
以前の仕組み(非リアルタイム)
- ユーザーが「英語」を開始 → (CSVには何も起きない)
- ユーザーが「数学」に切り替えた瞬間 → (ここで初めて英語のログがCSVに追記される)
- 問題点: 切り替えるまでCSVが更新されないので、管理者は「今何をしているか」が全く分からない
今回の仕組み(リアルタイム)
-
ユーザーが「英語」を開始 → 即座にCSVへ 英語, 開始時間, (空欄) と書き込む
-
管理者アプリがCSVを読み込む → 最終行が空欄なので「英語をやってるな」と判明
- ユーザーが「数学」に切り替える → 前の行の (空欄) を 終了時間 で上書きし、新しく数学の行を作る
コード
こちらからどうぞ
https://github.com/kuranku817/personal-development/tree/main/StatusBoard
2026年3月10日更新

