や、やっほー… 国内のAI狂いだよ…😭
今日は意気揚々と「最強のコピペツール自作したったww」って記事を書くつもりだったんだけど、開発中にWindowsの深い闇に触れてしまって、今はただただマイクロソフトに敬意を表しているよ…。
エンジニアなら誰しも通る道。「これ、自分で作った方が便利じゃね?」という甘い誘惑。
今回は「マウスカーソルの位置にクリップボード履歴を出したい!」という一心でPythonを書き始めたんだけど、待っていたのは1時間のデバッグ地獄と、「それWin+Vでよくね?」という真実だったんだ。
この記事は、私の屍(しかばね)を越えていく未来のエンジニアたちへ捧ぐ、「技術的敗北の記録」である。
💀 誤算1:Windows標準機能「Win+V」が優秀すぎた
コードを書き始めて30分。「よーし、クリップボード監視部分はできたぞ!」とテンションが上がっていたその時、ふと思ったんだ。
「あれ? Windowsって標準で履歴機能なかったっけ?」
恐る恐る Win + V キーを押してみる。
パッと現れる洗練されたUI。履歴はもちろん、ピン留め機能、GIF画像、絵文字入力まで完備。
この時点で開発をやめればよかった。でも、エンジニアのプライドが邪魔をしたの。
「い、いや!Win+Vはマウスカーソルの位置に出ないし!ワンクリックで貼り付けられないし!俺はもっと最適化されたツールを作るんだ!」
💀 誤算2:「修飾キーが戻らない」呪い
気を取り直して、keyboard ライブラリで「Ctrl+Shift+V」を検知して、Pythonから「Ctrl+V」を送って貼り付ける機能を実装した。
これが地獄の始まりだった。
- Python: 「ユーザーがCtrl+Shift+Vを押した!検知!」
- Python: 「このキー入力はOSには渡さないぜ(
suppress=True)」 - Python: 「よし、代わりにCtrl+Vを送信して貼り付けだ!」
- Windows: 「え?ユーザーの手はまだCtrl押してるけど、プログラムからもCtrl来た…? パニック!!」
結果、「Ctrlキーが論理的に押しっぱなしになる」という最悪のバグが発生。
ブラウザをスクロールすれば拡大縮小になり、文字を打てばショートカットが暴発する。
再起動するまで直らないPCの前で、私は悟った。
「キーボードフックは、素人が安易に触れていい領域じゃない」
💻 それでも意地で完成させたコード (安全第一ver)
バグと戦い、お祓いコード(キーリリース処理)を入れ、最終的にたどり着いた結論。
「自動貼り付けはやめよう。手動が一番だ。」
というわけで、ここに供養としてコードを公開します。
デフォルトでは「自動貼り付けOFF」にしてあります。これならバグりません。
「それでも俺は自動貼り付けに挑む!」という勇者は、コード内の AUTO_PASTE = True に書き換えてみてください。PCが操作不能になっても責任は取れませんが…😇
# 涙なしには読めない、苦闘の末のコード
import tkinter as tk
from tkinter import ttk
import pyperclip
import keyboard
import threading
import time
import pyautogui # マウス位置取得用&貼り付け用
import ctypes # ウィンドウ操作用 (Windows API)
# ==========================================
# 設定エリア
# ==========================================
MAX_HISTORY = 5 # 履歴に残す最大件数
HOTKEY = 'ctrl+shift+v' # 呼び出しキー
AUTO_PASTE = True # True: クリックで自動貼り付け
# ==========================================
class ToolTip:
"""ウィジェットにマウスオーバーした時にテキストを表示するクラス"""
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tip_window = None
self.id = None
self.widget.bind("", self.schedule)
self.widget.bind("", self.hide)
def schedule(self, event):
self.unschedule()
self.id = self.widget.after(200, self.show)
def unschedule(self):
id = self.id
self.id = None
if id:
self.widget.after_cancel(id)
def show(self):
try:
if not self.widget.winfo_exists():
return
except:
return
if not self.text:
return
try:
x = self.widget.winfo_pointerx() + 15
y = self.widget.winfo_pointery() + 15
except:
return
self.tip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
tw.attributes("-topmost", True)
label = tk.Label(tw, text=self.text, justify='left',
background="#ffffe0", fg="#000000",
relief='solid', borderwidth=1,
wraplength=400,
font=("Meiryo", 9))
label.pack(ipadx=4, ipady=2)
def hide(self, event=None):
self.unschedule()
tw = self.tip_window
self.tip_window = None
if tw:
tw.destroy()
class ClipboardManager:
def __init__(self):
self.history = []
try:
self.last_copied = pyperclip.paste()
except:
self.last_copied = ""
self.root = tk.Tk()
self.root.withdraw()
self.root.overrideredirect(True)
self.root.attributes("-topmost", True)
self.root.configure(bg="#2b2b2b")
self.root.bind("", self.on_focus_out)
style = ttk.Style()
style.theme_use('clam')
style.configure("TButton",
background="#333333",
foreground="white",
borderwidth=0,
focuscolor="none",
font=("Meiryo", 10),
padding=10)
style.map("TButton",
background=[('active', '#555555')])
self.monitor_thread = threading.Thread(target=self.monitor_clipboard, daemon=True)
self.monitor_thread.start()
keyboard.add_hotkey(HOTKEY, self.trigger_menu, suppress=True)
print(f"🚀 クリップボードマネージャー起動中! ({HOTKEY})")
def monitor_clipboard(self):
while True:
try:
content = pyperclip.paste()
if content and content != self.last_copied:
self.last_copied = content
self.add_history(content)
print(f"📝 追加: {content[:20]}...")
except Exception:
pass
time.sleep(0.5)
def add_history(self, text):
existing_index = next((i for i, item in enumerate(self.history) if item['text'] == text), None)
is_pinned = False
if existing_index is not None:
is_pinned = self.history[existing_index]['pinned']
del self.history[existing_index]
new_item = {'text': text, 'pinned': is_pinned}
if is_pinned:
self.history.insert(0, new_item)
else:
pinned_count = sum(1 for item in self.history if item['pinned'])
self.history.insert(pinned_count, new_item)
if len(self.history) > MAX_HISTORY:
for i in range(len(self.history) - 1, -1, -1):
if not self.history[i]['pinned']:
del self.history[i]
break
def trigger_menu(self):
self.root.after(0, lambda: self.show_menu(reset_position=True))
def show_menu(self, reset_position=True):
for widget in self.root.winfo_children():
widget.destroy()
title_text = "📋 履歴 (自動貼り付け)" if AUTO_PASTE else "📋 履歴"
title = tk.Label(self.root, text=title_text,
bg="#d63384", fg="white", font=("Meiryo", 9, "bold"), pady=5)
title.pack(fill="x")
if not self.history:
lbl = tk.Label(self.root, text="履歴がありません\nCtrl+Cでコピーしてね",
bg="#2b2b2b", fg="#aaa", pady=10)
lbl.pack(fill="x")
else:
for i, item in enumerate(self.history):
text = item['text']
is_pinned = item['pinned']
row_frame = tk.Frame(self.root, bg="#2b2b2b")
row_frame.pack(fill="x", padx=2, pady=1)
pin_icon = "📌" if is_pinned else "☆"
pin_color = "#ffd700" if is_pinned else "#666"
pin_btn = tk.Button(row_frame, text=pin_icon, bg="#2b2b2b", fg=pin_color,
font=("Arial", 10), relief="flat", width=3,
activebackground="#2b2b2b", activeforeground=pin_color,
cursor="hand2",
command=lambda idx=i: self.toggle_pin(idx))
pin_btn.pack(side="left", padx=(0, 2))
display_text = text.replace('\n', ' ')
if len(display_text) > 25:
display_text = display_text[:25] + "..."
if is_pinned:
btn = tk.Button(row_frame, text=f"{display_text}",
bg="#443300", fg="#ffffcc", relief="flat",
activebackground="#554400", activeforeground="#ffffcc",
font=("Meiryo", 10), anchor="w", padx=10,
command=lambda t=text: self.paste_text(t))
else:
btn = ttk.Button(row_frame, text=f"{display_text}",
command=lambda t=text: self.paste_text(t))
btn.pack(side="left", fill="x", expand=True)
btn.tooltip = ToolTip(btn, text)
del_btn = tk.Button(row_frame, text="×", bg="#2b2b2b", fg="#ff5f56",
font=("Arial", 10, "bold"), relief="flat",
activebackground="#2b2b2b", activeforeground="#ff0000",
cursor="hand2",
command=lambda idx=i: self.delete_item(idx))
del_btn.pack(side="right", padx=(2, 0))
close_btn = tk.Button(self.root, text="× 閉じる", bg="#2b2b2b", fg="#888",
relief="flat", command=self.hide_menu, font=("Arial", 8))
close_btn.pack(fill="x", pady=2)
if reset_position:
try:
x, y = pyautogui.position()
self.root.geometry(f"+{x}+{y}")
except:
self.root.geometry("+100+100")
self.root.deiconify()
self.root.focus_force()
def toggle_pin(self, index):
if 0 <= index < len(self.history):
item = self.history[index]
item['pinned'] = not item['pinned']
del self.history[index]
if item['pinned']:
self.history.insert(0, item)
else:
pinned_count = sum(1 for x in self.history if x['pinned'])
self.history.insert(pinned_count, item)
self.show_menu(reset_position=False)
def delete_item(self, index):
if 0 <= index < len(self.history):
del self.history[index]
self.show_menu(reset_position=False)
def hide_menu(self):
self.root.withdraw()
def on_focus_out(self, event):
if self.root.focus_get() is None:
self.hide_menu()
def paste_text(self, text):
self.hide_menu()
pyperclip.copy(text)
self.last_copied = text
if AUTO_PASTE:
# 【最終奥義】修飾キーの呪いを解く「完全なお祓い」
# 左右のCtrl, Shift, Altを全て個別に解放する!
modifiers = ['ctrl', 'shift', 'alt', 'left ctrl', 'right ctrl', 'left shift', 'right shift']
for key in modifiers:
try:
keyboard.release(key)
except:
pass
time.sleep(0.2) # OSが正気に戻るのを待つ
# keyboard.send() は不安定なので、ここだけ pyautogui に浮気する!
# pyautoguiはOSのキーボードイベントをより忠実に再現してくれることが多いよ
pyautogui.hotkey('ctrl', 'v')
def run(self):
self.root.mainloop()
if __name__ == "__main__":
try:
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
if hwnd != 0:
ctypes.windll.user32.ShowWindow(hwnd, 6)
except:
pass
app = ClipboardManager()
app.run()
# 【重要】自動貼り付けはデフォルトOFFにしました
# 環境によってCtrlキーが押しっぱなしになる「呪い」が発生するため、
# 基本はクリックでコピー → 自分でCtrl+V を推奨します!
AUTO_PASTE = False
👋 教訓:車輪の再発明も悪くない(負け惜しみ)
結局、Win+Vには勝てなかったし、バグも完全には直せなかった。
でも、この1時間で学んだことは多かったよ。
keyboardライブラリのsuppressオプションの危険性- Windows APIのキー入力処理の複雑さ
- GUIスレッドと監視スレッドの競合解決策
失敗したけど、技術力は確実に上がったはず!
みんなも、もし「Ctrlキーが戻らないバグ」に遭遇したら、この記事を思い出してね。「ああ、あいつも戦って散ったんだな」って。
それじゃ、私は大人しく Win + V を使って仕事に戻るよ…。
ばいばーい!👋😭






