以下は『記録と模倣を司る時計仕掛けの自動人形』のソースコード全文です:
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import json
import time
import threading
from pynput import mouse, keyboard
from pynput.mouse import Button as MouseButton
from pynput.keyboard import Key, Controller as KeyboardController
recording = False
recorded_actions = []
listener_threads = []
stop_flag = threading.Event()
mouse_controller = mouse.Controller()
keyboard_controller = KeyboardController()
def start_recording():
global recording, recorded_actions
if recording:
return
recorded_actions = []
recording = True
clear_tree()
start_button.config(state="disabled")
stop_button.config(state="normal")
status_label.config(text="記録中...(F1で停止)")
def on_click(x, y, button, pressed):
if recording and pressed:
action = {
"type": "click",
"x": x,
"y": y,
"button": "left" if button == MouseButton.left else "right",
"wait": 0.5
}
recorded_actions.append(action)
insert_action_to_tree(action)
def on_press(key):
global recording
if key == Key.f1:
stop_recording()
return False
else:
try:
key_str = key.char if hasattr(key, 'char') and key.char else str(key)
except AttributeError:
key_str = str(key)
action = {"type": "key", "key": key_str, "wait": 0.5}
recorded_actions.append(action)
insert_action_to_tree(action)
mouse_listener = mouse.Listener(on_click=on_click)
keyboard_listener = keyboard.Listener(on_press=on_press)
listener_threads.clear()
listener_threads.extend([mouse_listener, keyboard_listener])
mouse_listener.start()
keyboard_listener.start()
def stop_recording():
global recording
recording = False
for listener in listener_threads:
listener.stop()
start_button.config(state="normal")
stop_button.config(state="disabled")
status_label.config(text="記録停止")
def insert_action_to_tree(action):
if action["type"] == "click":
desc = f"Click {action['button']} at ({action['x']}, {action['y']})"
elif action["type"] == "key":
desc = f"Key {action['key']}"
wait = action["wait"]
tree.insert("", "end", values=(desc, wait))
def clear_tree():
for item in tree.get_children():
tree.delete(item)
def update_wait_times():
for i, item_id in enumerate(tree.get_children()):
values = tree.item(item_id)["values"]
try:
wait = float(values[1])
except ValueError:
wait = 0.5
recorded_actions[i]["wait"] = wait
def play_actions():
if not recorded_actions:
messagebox.showwarning("警告", "記録された操作がありません。")
return
try:
repeat = int(repeat_entry.get())
if repeat < 1:
raise ValueError
except ValueError:
messagebox.showerror("入力エラー", "繰り返し回数は1以上の整数で指定してください。")
return
stop_flag.clear()
update_wait_times()
play_button.config(state="disabled")
interrupt_button.config(state="normal")
def runner():
try:
for i in range(repeat):
if stop_flag.is_set():
status_label.config(text="中断されました")
break
status_label.config(text=f"{i+1}回目/{repeat}回中 登録操作実行中…")
for action in recorded_actions:
if stop_flag.is_set():
break
wait = action.get("wait", 0.5)
if action["type"] == "click":
mouse_controller.position = (action["x"], action["y"])
btn = MouseButton.left if action["button"] == "left" else MouseButton.right
mouse_controller.click(btn)
elif action["type"] == "key":
key = action["key"]
try:
if key.startswith("Key."):
k = getattr(Key, key.replace("Key.", ""))
keyboard_controller.press(k)
keyboard_controller.release(k)
else:
keyboard_controller.press(key)
keyboard_controller.release(key)
except Exception as e:
print(f"キーエラー: {key} → {e}")
time.sleep(wait)
if not stop_flag.is_set():
status_label.config(text="操作完了")
except Exception as e:
messagebox.showerror("実行エラー", str(e))
status_label.config(text="エラー")
finally:
play_button.config(state="normal")
interrupt_button.config(state="disabled")
threading.Thread(target=runner, daemon=True).start()
def stop_execution():
stop_flag.set()
def save_actions():
if not recorded_actions:
return
update_wait_times()
path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON", "*.json")])
if path:
with open(path, "w", encoding="utf-8") as f:
json.dump(recorded_actions, f, indent=2)
messagebox.showinfo("保存完了", "操作を保存しました。")
def load_actions():
global recorded_actions
path = filedialog.askopenfilename(filetypes=[("JSON", "*.json")])
if path:
with open(path, "r", encoding="utf-8") as f:
recorded_actions = json.load(f)
clear_tree()
for act in recorded_actions:
insert_action_to_tree(act)
def delete_selected():
selected = tree.selection()
if not selected:
return
for item_id in selected:
idx = tree.index(item_id)
tree.delete(item_id)
recorded_actions.pop(idx)
def edit_wait_cell(event):
region = tree.identify("region", event.x, event.y)
if region != "cell":
return
column = tree.identify_column(event.x)
if column != "#2":
return
row_id = tree.identify_row(event.y)
if not row_id:
return
x, y, width, height = tree.bbox(row_id, column)
value = tree.item(row_id, "values")[1]
edit_box = tk.Entry(tree)
edit_box.place(x=x, y=y, width=width, height=height)
edit_box.insert(0, value)
edit_box.focus_set()
def save_edit(event=None):
try:
new_value = float(edit_box.get())
current_values = tree.item(row_id, "values")
tree.item(row_id, values=(current_values[0], new_value))
index = tree.index(row_id)
recorded_actions[index]["wait"] = new_value
except ValueError:
messagebox.showerror("入力エラー", "数字を入力してください。")
finally:
edit_box.destroy()
edit_box.bind("<Return>", save_edit)
edit_box.bind("<FocusOut>", lambda e: edit_box.destroy())
def apply_bulk_wait():
try:
val = float(bulk_wait_entry.get())
for i, item_id in enumerate(tree.get_children()):
desc = tree.item(item_id)["values"][0]
tree.item(item_id, values=(desc, val))
recorded_actions[i]["wait"] = val
except ValueError:
messagebox.showerror("エラー", "数字を入力してください。")
# --- GUI構築 ---
root = tk.Tk()
##タイトルバーアイコン表示用
data = '''R0lGODdhEAAQAIUAAAAAAAICAigoJgICAjU2MwgICBoaGVdXVklJR2lpaAMDAxQUFBYWFZWVlBAQEMnJx9vb24ODgwEBAXR1chcXFwICAQYGBjg4NhwcGiAgHSIiIiYnJjk7OaSkouLi4DAwKz8/P/Ly8ENDQy4uLSEhIUtLS0ZGRiEhHyAgH52dnRkZF5ycnP///3V1c83NzFVVVQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAEAAQAEAI1wABAAgQAIACBxYsBFgwsIJAAAMAWCgwIMCBBCcUKHgxUOAABSoIFGiQgEAHBAYiFoQIQIOAAgUOHDBQQACJjh0vCCCAACaCAwIc4BywoGaEBhAeJEhwQQGFlRE1EAD608ABBAxwFkwxoQIBAwYIVEiwQqsEAAwy0CyQocBZCSsLMhBAl+aECUGHYjAgIAGCBw+mEhjAAqoCmAFchPDQ4gMADhQishywwQQBugQIlAAAIi4ADHQRNOgwk8CGoQAWRBghAKwAFBE4DF0AIcHO1pcPQBAxgEJAADs='''
root.tk.call('wm', 'iconphoto', root._w, tk.PhotoImage(data=data))
####
root.title("マウス・キーボード自動操作ツール - 記録と模倣を司る時計仕掛けの自動人形")
top_frame = tk.Frame(root)
top_frame.pack(padx=10, pady=5)
start_button = tk.Button(top_frame, text="登録開始", command=start_recording)
start_button.grid(row=0, column=0, padx=5)
stop_button = tk.Button(top_frame, text="登録終了(F1)", state="disabled", command=stop_recording)
stop_button.grid(row=0, column=1, padx=5)
play_button = tk.Button(top_frame, text="操作開始", command=play_actions)
play_button.grid(row=0, column=2, padx=5)
interrupt_button = tk.Button(top_frame, text="中断", state="disabled", command=stop_execution)
interrupt_button.grid(row=0, column=3, padx=5)
save_button = tk.Button(top_frame, text="保存", command=save_actions)
save_button.grid(row=0, column=4, padx=5)
load_button = tk.Button(top_frame, text="読み込み", command=load_actions)
load_button.grid(row=0, column=5, padx=5)
delete_button = tk.Button(top_frame, text="選択削除", command=delete_selected)
delete_button.grid(row=0, column=6, padx=5)
repeat_label = tk.Label(top_frame, text="繰り返し回数:")
repeat_label.grid(row=0, column=7, padx=(15, 0))
repeat_entry = tk.Entry(top_frame, width=5)
repeat_entry.insert(0, "1")
repeat_entry.grid(row=0, column=8, padx=2)
bulk_wait_label = tk.Label(top_frame, text="待機秒数一括:")
bulk_wait_label.grid(row=0, column=9, padx=(15, 0))
bulk_wait_entry = tk.Entry(top_frame, width=5)
bulk_wait_entry.insert(0, "0.5")
bulk_wait_entry.grid(row=0, column=10, padx=2)
bulk_wait_button = tk.Button(top_frame, text="一括適用", command=apply_bulk_wait)
bulk_wait_button.grid(row=0, column=11, padx=2)
status_label = tk.Label(root, text="準備完了")
status_label.pack()
tree = ttk.Treeview(root, columns=("Action", "Wait"), show="headings", height=15)
tree.heading("Action", text="操作内容")
tree.heading("Wait", text="待機秒数(次操作まで)")
tree.column("Action", width=300)
tree.column("Wait", width=150)
tree.pack(padx=10, pady=10)
tree.bind("<Double-1>", edit_wait_cell)
root.mainloop()