【ミリしらプログラム】シンプルなWebカウンターをつくる【Python】

ブログ
この記事は約20分で読めます。

どうもヨスケです。

近頃忙しくて全く更新できていませんでした。

少しずつ更新していこうと思います。

近況報告はまた別途行います。

 

今回はプログラムミリしらの筆者が、
仕事に使うためにアプリを自作したので、そのご紹介です。

 

数取り器(カウンター)を作ってみた

下のようなカウンターを作ってみました。
一日の業務量をまとめるために、行った業務のカウントアップをしたいと思い作ってみました。

 

実装した機能

・カウンタの追加、クリックでカウントアップします。
・テキスト入力欄の追加、単位もつけることができます。
・カウンタの入れ替え、入力欄の入れ替え(ドラッグアンドドロップ)
・個別の削除ボタン(✕)
・カウンタの減数ボタン、カウントリセットボタン

助けてくれたAI

今回作成を補助してくれたのは、OpenAI o1-preview 君です。

要求を伝えて、やり取りをしながら、約1時間ほどで完成しました。

書いたコード(Python)

VS codeなどで実行すれば動きますので、遊んでみてください。

開発環境などがない方は、実行ファイルを下からダウンロードできます。

ちなみにREADME.txtもChatGPT 4oさんに作成してもらいました。

便利な世の中です。

一人で仕事をするのには本当に助かります。

※修正依頼は受付しておりません。悪しからずご承知おきください。

■Pythonコード

import tkinter as tk
from tkinter import simpledialog, colorchooser
import json
import os
import uuid

# データファイルのパスを設定
import sys
if getattr(sys, 'frozen', False):
# 実行ファイルの場合
application_path = os.path.dirname(sys.executable)
else:
# スクリプトを直接実行している場合
application_path = os.path.dirname(os.path.abspath(__file__))

data_file_path = os.path.join(application_path, 'data.json')

# データをロード
def load_data():
if os.path.exists(data_file_path):
with open(data_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 'id'がない場合はUUIDを生成
for counter in data.get('counters', []):
if 'id' not in counter:
counter['id'] = str(uuid.uuid4())
for text_input in data.get('text_inputs', []):
if 'id' not in text_input:
text_input['id'] = str(uuid.uuid4())
return data
return {'counters': [], 'text_inputs': []}

# データをセーブ
def save_data():
with open(data_file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False)

# 色を選択するダイアログ
def choose_color():
color_code = colorchooser.askcolor(title="色の選択")
if color_code[1]:
return color_code[1]
else:
return None # キャンセルされた場合

# ウィジェットの参照を保持する辞書
counter_widgets = {}
text_input_widgets = {}

# ドラッグ&ドロップのための変数
dragging_widget = None
dragging_type = None # 'counter' または 'text_input'
dragging = {'is_dragging': False, 'start_x': 0, 'start_y': 0}

# カウンターを追加
def add_counter():
name = simpledialog.askstring("カウンター名", "新しいカウンターの名前を入力してください:")
if name:
color = choose_color()
if color:
counter = {
'id': str(uuid.uuid4()), # 一意のIDを生成
'name': name,
'count': 0,
'color': color
}
data['counters'].append(counter)
create_counter_widget(counter)
rearrange_widgets()

# カウンターのウィジェットを作成
def create_counter_widget(counter):
frame = tk.Frame(counters_frame, bg=counter['color'], width=200, height=180, relief='raised', bd=2)
frame.pack_propagate(False)
counter_widgets[counter['id']] = frame # ウィジェットへの参照を辞書に保持

# ドラッグ&ドロップ用の変数
local_dragging = {'is_dragging': False, 'start_x': 0, 'start_y': 0}

# マウスイベントのコールバック関数
def on_counter_press(event):
global dragging_widget, dragging_type
dragging_widget = counter
dragging_type = 'counter'
local_dragging['is_dragging'] = False
local_dragging['start_x'] = event.x_root
local_dragging['start_y'] = event.y_root

def on_counter_motion(event):
dx = abs(event.x_root - local_dragging['start_x'])
dy = abs(event.y_root - local_dragging['start_y'])
if dx > 5 or dy > 5:
local_dragging['is_dragging'] = True

def on_counter_release(event):
global dragging_widget, dragging_type
if local_dragging['is_dragging']:
# ドラッグ&ドロップで位置を入れ替える
if dragging_type == 'counter' and dragging_widget is not None:
x_root = event.x_root
y_root = event.y_root
target_widget = root.winfo_containing(x_root, y_root)
# ターゲットのカウンターを見つける
target_counter = None
for c in data['counters']:
widget = counter_widgets[c['id']]
if widget == target_widget or widget == target_widget.master or widget == target_widget.master.master:
target_counter = c
break
if target_counter and target_counter != dragging_widget:
# データ内の位置を入れ替える
index1 = data['counters'].index(dragging_widget)
index2 = data['counters'].index(target_counter)
data['counters'][index1], data['counters'][index2] = data['counters'][index2], data['counters'][index1]
rearrange_widgets()
else:
# クリックとして処理(カウントアップ)
increment_counter(counter, count_label)
dragging_widget = None
dragging_type = None
local_dragging['is_dragging'] = False

# イベントバインディング
frame.bind('<ButtonPress-1>', on_counter_press)
frame.bind('<B1-Motion>', on_counter_motion)
frame.bind('<ButtonRelease-1>', on_counter_release)

# 削除ボタン(✕マーク)を右上に配置
delete_button = tk.Button(frame, text="✕", command=lambda: delete_counter(counter), relief='flat', bg=counter['color'])
delete_button.place(relx=1.0, rely=0.0, anchor='ne')

# タイトルを太字に
name_label = tk.Label(frame, text=counter['name'], bg=counter['color'], fg='black', font=("Arial", 16, "bold"))
name_label.pack(side='top')

# カウント数を大きく表示
count_label = tk.Label(frame, text=f"{counter['count']}", font=("Arial", 48), bg=counter['color'])
count_label.pack(expand=True)

# ボタンを含むフレーム
button_frame = tk.Frame(frame, bg=counter['color'])
button_frame.pack(side='bottom', fill='x')

# 減数ボタン
decrement_button = tk.Button(button_frame, text="-", command=lambda: decrement_counter(counter, count_label))
decrement_button.pack(side='left', fill='x', expand=True)

# リセットボタン
reset_button = tk.Button(button_frame, text="リセット", command=lambda: reset_counter(counter, count_label))
reset_button.pack(side='left', fill='x', expand=True)

def delete_counter(counter):
data['counters'] = [c for c in data['counters'] if c['id'] != counter['id']]
frame = counter_widgets.pop(counter['id'])
frame.destroy()
rearrange_widgets()

# カウンターをインクリメント
def increment_counter(counter, count_label):
counter['count'] += 1
count_label.config(text=f"{counter['count']}")

# カウンターをデクリメント
def decrement_counter(counter, count_label):
counter['count'] -= 1
count_label.config(text=f"{counter['count']}")

# カウンターをリセット
def reset_counter(counter, count_label):
counter['count'] = 0
count_label.config(text=f"{counter['count']}")

# テキスト入力欄を追加
def add_text_input():
title = simpledialog.askstring("題名設定", "テキスト入力欄の題名を入力してください:")
unit = simpledialog.askstring("単位設定", "単位を入力してください(任意):")
if title:
color = choose_color()
if color:
text_input = {
'id': str(uuid.uuid4()), # 一意のIDを生成
'title': title,
'unit': unit,
'value': '',
'color': color
}
data['text_inputs'].append(text_input)
create_text_input_widget(text_input)
rearrange_widgets()

# テキスト入力欄のウィジェットを作成
def create_text_input_widget(text_input):
frame = tk.Frame(counters_frame, bg=text_input['color'], width=600, height=75, relief='raised', bd=2)
frame.pack_propagate(False)
text_input_widgets[text_input['id']] = frame # ウィジェットへの参照を辞書に保持

# ドラッグ&ドロップ用の変数
local_dragging = {'is_dragging': False, 'start_x': 0, 'start_y': 0}

# マウスイベントのコールバック関数
def on_text_input_press(event):
global dragging_widget, dragging_type
dragging_widget = text_input
dragging_type = 'text_input'
local_dragging['is_dragging'] = False
local_dragging['start_x'] = event.x_root
local_dragging['start_y'] = event.y_root

def on_text_input_motion(event):
dx = abs(event.x_root - local_dragging['start_x'])
dy = abs(event.y_root - local_dragging['start_y'])
if dx > 5 or dy > 5:
local_dragging['is_dragging'] = True

def on_text_input_release(event):
global dragging_widget, dragging_type
if local_dragging['is_dragging']:
# ドラッグ&ドロップで位置を入れ替える
if dragging_type == 'text_input' and dragging_widget is not None:
x_root = event.x_root
y_root = event.y_root
target_widget = root.winfo_containing(x_root, y_root)
# ターゲットのテキスト入力欄を見つける
target_text_input = None
for t in data['text_inputs']:
widget = text_input_widgets[t['id']]
if widget == target_widget or widget == target_widget.master or widget == target_widget.master.master:
target_text_input = t
break
if target_text_input and target_text_input != dragging_widget:
# データ内の位置を入れ替える
index1 = data['text_inputs'].index(dragging_widget)
index2 = data['text_inputs'].index(target_text_input)
data['text_inputs'][index1], data['text_inputs'][index2] = data['text_inputs'][index2], data['text_inputs'][index1]
rearrange_widgets()
dragging_widget = None
dragging_type = None
local_dragging['is_dragging'] = False

# イベントバインディング
frame.bind('<ButtonPress-1>', on_text_input_press)
frame.bind('<B1-Motion>', on_text_input_motion)
frame.bind('<ButtonRelease-1>', on_text_input_release)

# 削除ボタン(✕マーク)を右上に配置
delete_button = tk.Button(frame, text="✕", command=lambda: delete_text_input(text_input), relief='flat', bg=text_input['color'])
delete_button.place(relx=1.0, rely=0.0, anchor='ne')

# タイトルを太字に
title_label = tk.Label(frame, text=text_input['title'], bg=text_input['color'], font=("Arial", 16, "bold"))
title_label.pack(side='top')

# 入力フィールドと単位ラベルを含むフレーム
input_frame = tk.Frame(frame, bg=text_input['color'])
input_frame.pack(expand=True, fill='x', padx=5)

# テキスト入力フィールド
entry_var = tk.StringVar(value=text_input['value'])
entry = tk.Entry(input_frame, font=("Arial", 24), textvariable=entry_var)
entry.pack(side='left', fill='x', expand=True)

# 単位ラベル
if text_input['unit']:
unit_label = tk.Label(input_frame, text=text_input['unit'], bg=text_input['color'], font=("Arial", 16))
unit_label.pack(side='left')

# テキストが変更されたときにデータを更新
def on_text_change(*args):
text_input['value'] = entry_var.get()

entry_var.trace_add('write', on_text_change)

def delete_text_input(text_input):
data['text_inputs'] = [t for t in data['text_inputs'] if t['id'] != text_input['id']]
frame = text_input_widgets.pop(text_input['id'])
frame.destroy()
rearrange_widgets()

# ウィジェットの再配置
def rearrange_widgets():
# 既存のウィジェットをクリア
for widget in counters_frame.winfo_children():
widget.grid_forget()

# カウンターを配置
counter_widgets_list = [counter_widgets[c['id']] for c in data['counters']]
for index, widget in enumerate(counter_widgets_list):
row = index // 3
col = index % 3
widget.grid(row=row, column=col, padx=5, pady=5)

# テキスト入力欄を配置
text_input_widgets_list = [text_input_widgets[t['id']] for t in data['text_inputs']]
# カウンターの行数を取得
counter_rows = (len(counter_widgets_list) + 2) // 3 # カウンターの行数
for index, widget in enumerate(text_input_widgets_list):
row = counter_rows + index # カウンターの次の行から配置
widget.grid(row=row, column=0, columnspan=3, padx=5, pady=5, sticky='we')

# アプリケーションの終了処理
def on_closing():
try:
save_data()
except Exception as e:
print(f"データの保存中にエラーが発生しました: {e}")
root.destroy()

# メインウィンドウの設定
root = tk.Tk()
root.title("カウンターアプリ")

# カウンターとテキスト入力欄を配置するフレーム
counters_frame = tk.Frame(root)
counters_frame.pack(fill='both', expand=True)

# データをロード
data = load_data()

# ウィジェットの参照を保持する辞書
counter_widgets = {}
text_input_widgets = {}

# カウンターを作成
for counter in data['counters']:
create_counter_widget(counter)

# テキスト入力欄を作成
for text_input in data['text_inputs']:
create_text_input_widget(text_input)

rearrange_widgets()

# ボタンを配置するフレーム
buttons_frame = tk.Frame(root)
buttons_frame.pack(side='bottom')

# カウンター追加ボタン
add_counter_button = tk.Button(buttons_frame, text="カウンター追加", command=add_counter)
add_counter_button.pack(side='left', padx=5, pady=5)

# テキスト入力欄追加ボタン
add_text_input_button = tk.Button(buttons_frame, text="テキスト入力欄追加", command=add_text_input)
add_text_input_button.pack(side='left', padx=5, pady=5)

# ウィンドウを閉じる際の処理
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()

今後は、毎日の業務日誌のようなものを書いていく予定です。

今後ともよろしくお願いします。

 

 

タイトルとURLをコピーしました