クリップボードマネージャー
コピーしたテキストの履歴を保存・管理・検索できるクリップボードマネージャー。pyperclipの活用を学びます。
1. アプリ概要
コピーしたテキストの履歴を保存・管理・検索できるクリップボードマネージャー。pyperclipの活用を学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ)・pyperclip で、難易度は ★★☆ です。
Pythonでは tkinter を使うことで、クロスプラットフォームなGUIアプリを簡単に作成できます。このアプリを通じて、ウィジェットの配置・イベント処理・データ管理など、GUI開発の実践的なスキルを習得できます。
ソースコードは完全な動作状態で提供しており、コピーしてそのまま実行できます。まずは実行して動作を確認し、その後コードを読んで仕組みを理解していきましょう。カスタマイズセクションでは機能拡張のアイデアも紹介しています。
GUIアプリ開発は、プログラミングの楽しさを実感できる最も効果的な学習方法のひとつです。アプリを作ることで、変数・関数・クラス・イベント処理など、プログラミングの重要な概念が自然と身についていきます。このアプリをきっかけに、オリジナルアプリの開発にも挑戦してみてください。
2. 機能一覧
- クリップボードマネージャーのメイン機能
- 直感的なGUIインターフェース
- 入力値のバリデーション
- エラーハンドリング
- 結果の見やすい表示
- キーボードショートカット対応
3. 事前準備・環境
Python 3.10 以上 / Windows・Mac・Linux すべて対応
以下の環境で動作確認しています。
- Python 3.10 以上
- OS: Windows 10/11・macOS 12+・Ubuntu 20.04+
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import threading
import time
from datetime import datetime
try:
import pyperclip
PYPERCLIP_AVAILABLE = True
except ImportError:
PYPERCLIP_AVAILABLE = False
class App30:
"""クリップボードマネージャー"""
MAX_HISTORY = 200
HISTORY_FILE = os.path.join(os.path.dirname(__file__), "clipboard_history.json")
POLL_INTERVAL_MS = 500
def __init__(self, root):
self.root = root
self.root.title("クリップボードマネージャー")
self.root.geometry("860x600")
self.root.configure(bg="#f8f9fc")
self._history = []
self._pinned = []
self._last_clip = ""
self._monitoring = False
self._load_history()
self._build_ui()
self._start_monitoring()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _load_history(self):
if os.path.exists(self.HISTORY_FILE):
try:
with open(self.HISTORY_FILE, encoding="utf-8") as f:
data = json.load(f)
self._history = data.get("history", [])
self._pinned = data.get("pinned", [])
except Exception:
pass
def _save_history(self):
try:
with open(self.HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump({
"history": self._history[-self.MAX_HISTORY:],
"pinned": self._pinned,
}, f, ensure_ascii=False, indent=2)
except Exception:
pass
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#37474f", pady=8)
header.pack(fill=tk.X)
tk.Label(header, text="📋 クリップボードマネージャー",
font=("Noto Sans JP", 14, "bold"),
bg="#37474f", fg="white").pack(side=tk.LEFT, padx=12)
self.monitor_btn = tk.Button(
header, text="⏸ 監視停止", bg="#546e7a", fg="white",
relief=tk.FLAT, font=("Arial", 10), padx=10,
command=self._toggle_monitoring)
self.monitor_btn.pack(side=tk.RIGHT, padx=8)
tk.Label(header, text="監視中:", bg="#37474f", fg="#b0bec5",
font=("Arial", 9)).pack(side=tk.RIGHT)
if not PYPERCLIP_AVAILABLE:
tk.Label(self.root,
text="⚠ pyperclip が未インストールです (pip install pyperclip)。"
"クリップボード監視は無効です。",
bg="#fff3cd", fg="#856404",
font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X)
# メインエリア
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
# 左: 履歴 + ピン留め
left = tk.Frame(paned, bg="#f8f9fc")
paned.add(left, weight=5)
notebook = ttk.Notebook(left)
notebook.pack(fill=tk.BOTH, expand=True)
# 履歴タブ
hist_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(hist_tab, text="履歴")
self._build_history_tab(hist_tab)
# ピン留めタブ
pin_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(pin_tab, text="⭐ ピン留め")
self._build_pinned_tab(pin_tab)
# 右: プレビュー
right = ttk.LabelFrame(paned, text="プレビュー", padding=4)
paned.add(right, weight=3)
self._build_preview(right)
# ステータス
self.status_var = tk.StringVar(value="監視中...")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._refresh_history_list()
self._refresh_pinned_list()
def _build_history_tab(self, parent):
# ツールバー
bar = tk.Frame(parent, bg="#f8f9fc")
bar.pack(fill=tk.X, padx=4, pady=4)
tk.Label(bar, text="🔍", bg="#f8f9fc").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *a: self._refresh_history_list())
ttk.Entry(bar, textvariable=self.search_var,
width=20).pack(side=tk.LEFT, padx=4, fill=tk.X, expand=True)
ttk.Button(bar, text="全削除",
command=self._clear_history).pack(side=tk.RIGHT, padx=4)
# カテゴリフィルター
cat_f = tk.Frame(parent, bg="#f8f9fc")
cat_f.pack(fill=tk.X, padx=4)
tk.Label(cat_f, text="種別:", bg="#f8f9fc").pack(side=tk.LEFT)
self.cat_var = tk.StringVar(value="すべて")
for val in ["すべて", "テキスト", "URL", "コード", "数字"]:
ttk.Radiobutton(cat_f, text=val, variable=self.cat_var,
value=val,
command=self._refresh_history_list
).pack(side=tk.LEFT, padx=3)
# リスト
cols = ("time", "type", "preview")
self.hist_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=16,
selectmode="browse")
for c, h, w in [("time", "時刻", 72), ("type", "種別", 56),
("preview", "内容プレビュー", 300)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True, padx=4)
self.hist_tree.bind("<<TreeviewSelect>>", self._on_select_hist)
self.hist_tree.bind("<Double-1>", self._copy_to_clipboard)
# ボタン行
btn_f = tk.Frame(parent, bg="#f8f9fc")
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📌 ピン留め",
command=self._pin_selected).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="📋 クリップボードにコピー",
command=self._copy_to_clipboard).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_selected).pack(side=tk.LEFT, padx=4)
def _build_pinned_tab(self, parent):
cols = ("label", "type", "preview")
self.pin_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=18,
selectmode="browse")
for c, h, w in [("label", "ラベル", 120), ("type", "種別", 56),
("preview", "内容プレビュー", 280)]:
self.pin_tree.heading(c, text=h)
self.pin_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.pin_tree.yview)
self.pin_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.pin_tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
self.pin_tree.bind("<<TreeviewSelect>>", self._on_select_pin)
self.pin_tree.bind("<Double-1>", self._copy_pinned)
btn_f = tk.Frame(parent, bg=parent.cget("bg"))
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📋 コピー",
command=self._copy_pinned).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="✏ ラベル変更",
command=self._rename_pin).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_pin).pack(side=tk.LEFT, padx=4)
def _build_preview(self, parent):
self.preview_text = tk.Text(parent, bg="#0d1117", fg="#c9d1d9",
font=("Courier New", 11), relief=tk.FLAT,
wrap=tk.WORD, state=tk.DISABLED)
sb = ttk.Scrollbar(parent, command=self.preview_text.yview)
self.preview_text.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.preview_text.pack(fill=tk.BOTH, expand=True)
info_f = tk.Frame(parent, bg=parent.cget("background"))
info_f.pack(fill=tk.X, pady=4)
self.char_count_var = tk.StringVar(value="")
tk.Label(info_f, textvariable=self.char_count_var,
bg=info_f.cget("bg"), font=("Arial", 9),
fg="#666").pack(anchor="w")
# 手動追加
manual_f = ttk.LabelFrame(parent, text="手動追加", padding=6)
manual_f.pack(fill=tk.X, pady=4)
self.manual_text = tk.Text(manual_f, height=4, bg="#fafafa",
font=("Arial", 10), relief=tk.FLAT)
self.manual_text.pack(fill=tk.X, pady=2)
ttk.Button(manual_f, text="➕ 履歴に追加",
command=self._add_manual).pack(anchor="e")
# ── データ処理 ──────────────────────────────────────────────
def _classify(self, text):
import re
text_s = text.strip()
if re.match(r"https?://", text_s):
return "URL"
if re.search(r"^\s*(def |class |import |#|//|<!)", text_s, re.MULTILINE):
return "コード"
if re.match(r"^[\d\s,.\-+*/()%$¥€£]+$", text_s):
return "数字"
return "テキスト"
def _add_entry(self, text):
if not text or not text.strip():
return
# 重複チェック(直近)
if self._history and self._history[0].get("text") == text:
return
entry = {
"time": datetime.now().strftime("%H:%M:%S"),
"date": datetime.now().strftime("%Y-%m-%d"),
"text": text,
"type": self._classify(text),
}
self._history.insert(0, entry)
if len(self._history) > self.MAX_HISTORY:
self._history.pop()
self._save_history()
self.root.after(0, self._refresh_history_list)
def _refresh_history_list(self):
query = self.search_var.get().strip().lower() if hasattr(self, "search_var") else ""
cat = self.cat_var.get() if hasattr(self, "cat_var") else "すべて"
self.hist_tree.delete(*self.hist_tree.get_children())
for entry in self._history:
if cat != "すべて" and entry.get("type") != cat:
continue
text = entry.get("text", "")
if query and query not in text.lower():
continue
preview = text[:80].replace("\n", "↵")
self.hist_tree.insert("", "end",
values=(entry.get("time", ""),
entry.get("type", ""),
preview))
self.status_var.set(
f"履歴: {len(self._history)} 件 / ピン: {len(self._pinned)} 件")
def _refresh_pinned_list(self):
self.pin_tree.delete(*self.pin_tree.get_children())
for p in self._pinned:
preview = p.get("text", "")[:80].replace("\n", "↵")
self.pin_tree.insert("", "end",
values=(p.get("label", ""),
p.get("type", ""),
preview))
def _set_preview(self, text):
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.insert("1.0", text)
self.preview_text.config(state=tk.DISABLED)
lines = text.count("\n") + 1
self.char_count_var.set(
f"{len(text)} 文字 / {lines} 行")
def _get_selected_text(self):
sel = self.hist_tree.selection()
if not sel:
return None
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
return filtered[idx]
return None
# ── イベントハンドラ ─────────────────────────────────────
def _on_select_hist(self, event):
entry = self._get_selected_text()
if entry:
self._set_preview(entry.get("text", ""))
def _on_select_pin(self, event):
sel = self.pin_tree.selection()
if sel:
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._set_preview(self._pinned[idx].get("text", ""))
def _copy_to_clipboard(self, event=None):
entry = self._get_selected_text()
if not entry:
return
text = entry.get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
self.status_var.set(f"コピーしました: {text[:40]}")
except Exception as e:
messagebox.showerror("エラー", str(e))
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _copy_pinned(self, event=None):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
text = self._pinned[idx].get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
except Exception:
pass
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _pin_selected(self):
entry = self._get_selected_text()
if not entry:
return
label = entry.get("text", "")[:20].replace("\n", " ")
pin_entry = {**entry, "label": label}
if any(p.get("text") == entry.get("text") for p in self._pinned):
messagebox.showinfo("情報", "すでにピン留めされています")
return
self._pinned.append(pin_entry)
self._save_history()
self._refresh_pinned_list()
self.status_var.set(f"ピン留めしました: {label}")
def _rename_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx >= len(self._pinned):
return
win = tk.Toplevel(self.root)
win.title("ラベル変更")
win.geometry("320x120")
tk.Label(win, text="新しいラベル:").pack(pady=8)
var = tk.StringVar(value=self._pinned[idx].get("label", ""))
ttk.Entry(win, textvariable=var, width=30).pack()
def save():
self._pinned[idx]["label"] = var.get()
self._save_history()
self._refresh_pinned_list()
win.destroy()
ttk.Button(win, text="保存", command=save).pack(pady=8)
def _delete_selected(self):
sel = self.hist_tree.selection()
if not sel:
return
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
self._history.remove(filtered[idx])
self._save_history()
self._refresh_history_list()
def _delete_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._pinned.pop(idx)
self._save_history()
self._refresh_pinned_list()
def _clear_history(self):
if messagebox.askyesno("確認", "履歴をすべて削除しますか?"):
self._history.clear()
self._save_history()
self._refresh_history_list()
def _add_manual(self):
text = self.manual_text.get("1.0", tk.END).strip()
if not text:
return
self._add_entry(text)
self.manual_text.delete("1.0", tk.END)
# ── クリップボード監視 ──────────────────────────────────
def _start_monitoring(self):
self._monitoring = True
self._poll()
def _poll(self):
if not self._monitoring:
return
if PYPERCLIP_AVAILABLE:
try:
current = pyperclip.paste()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
else:
# tkinter クリップボードで代替
try:
current = self.root.clipboard_get()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
self._poll_id = self.root.after(self.POLL_INTERVAL_MS, self._poll)
def _toggle_monitoring(self):
self._monitoring = not self._monitoring
if self._monitoring:
self.monitor_btn.config(text="⏸ 監視停止")
self._poll()
else:
self.monitor_btn.config(text="▶ 監視開始")
if hasattr(self, "_poll_id"):
self.root.after_cancel(self._poll_id)
def _on_close(self):
self._monitoring = False
self._save_history()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App30(root)
root.mainloop()
5. コード解説
クリップボードマネージャーのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。
クラス設計とコンストラクタ
App30クラスにアプリの全機能をまとめています。__init__メソッドでウィンドウの基本設定を行い、_build_ui()でUI構築、process()でメイン処理を担当します。この分離により、各メソッドの責任が明確になりコードが読みやすくなります。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import threading
import time
from datetime import datetime
try:
import pyperclip
PYPERCLIP_AVAILABLE = True
except ImportError:
PYPERCLIP_AVAILABLE = False
class App30:
"""クリップボードマネージャー"""
MAX_HISTORY = 200
HISTORY_FILE = os.path.join(os.path.dirname(__file__), "clipboard_history.json")
POLL_INTERVAL_MS = 500
def __init__(self, root):
self.root = root
self.root.title("クリップボードマネージャー")
self.root.geometry("860x600")
self.root.configure(bg="#f8f9fc")
self._history = []
self._pinned = []
self._last_clip = ""
self._monitoring = False
self._load_history()
self._build_ui()
self._start_monitoring()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _load_history(self):
if os.path.exists(self.HISTORY_FILE):
try:
with open(self.HISTORY_FILE, encoding="utf-8") as f:
data = json.load(f)
self._history = data.get("history", [])
self._pinned = data.get("pinned", [])
except Exception:
pass
def _save_history(self):
try:
with open(self.HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump({
"history": self._history[-self.MAX_HISTORY:],
"pinned": self._pinned,
}, f, ensure_ascii=False, indent=2)
except Exception:
pass
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#37474f", pady=8)
header.pack(fill=tk.X)
tk.Label(header, text="📋 クリップボードマネージャー",
font=("Noto Sans JP", 14, "bold"),
bg="#37474f", fg="white").pack(side=tk.LEFT, padx=12)
self.monitor_btn = tk.Button(
header, text="⏸ 監視停止", bg="#546e7a", fg="white",
relief=tk.FLAT, font=("Arial", 10), padx=10,
command=self._toggle_monitoring)
self.monitor_btn.pack(side=tk.RIGHT, padx=8)
tk.Label(header, text="監視中:", bg="#37474f", fg="#b0bec5",
font=("Arial", 9)).pack(side=tk.RIGHT)
if not PYPERCLIP_AVAILABLE:
tk.Label(self.root,
text="⚠ pyperclip が未インストールです (pip install pyperclip)。"
"クリップボード監視は無効です。",
bg="#fff3cd", fg="#856404",
font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X)
# メインエリア
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
# 左: 履歴 + ピン留め
left = tk.Frame(paned, bg="#f8f9fc")
paned.add(left, weight=5)
notebook = ttk.Notebook(left)
notebook.pack(fill=tk.BOTH, expand=True)
# 履歴タブ
hist_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(hist_tab, text="履歴")
self._build_history_tab(hist_tab)
# ピン留めタブ
pin_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(pin_tab, text="⭐ ピン留め")
self._build_pinned_tab(pin_tab)
# 右: プレビュー
right = ttk.LabelFrame(paned, text="プレビュー", padding=4)
paned.add(right, weight=3)
self._build_preview(right)
# ステータス
self.status_var = tk.StringVar(value="監視中...")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._refresh_history_list()
self._refresh_pinned_list()
def _build_history_tab(self, parent):
# ツールバー
bar = tk.Frame(parent, bg="#f8f9fc")
bar.pack(fill=tk.X, padx=4, pady=4)
tk.Label(bar, text="🔍", bg="#f8f9fc").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *a: self._refresh_history_list())
ttk.Entry(bar, textvariable=self.search_var,
width=20).pack(side=tk.LEFT, padx=4, fill=tk.X, expand=True)
ttk.Button(bar, text="全削除",
command=self._clear_history).pack(side=tk.RIGHT, padx=4)
# カテゴリフィルター
cat_f = tk.Frame(parent, bg="#f8f9fc")
cat_f.pack(fill=tk.X, padx=4)
tk.Label(cat_f, text="種別:", bg="#f8f9fc").pack(side=tk.LEFT)
self.cat_var = tk.StringVar(value="すべて")
for val in ["すべて", "テキスト", "URL", "コード", "数字"]:
ttk.Radiobutton(cat_f, text=val, variable=self.cat_var,
value=val,
command=self._refresh_history_list
).pack(side=tk.LEFT, padx=3)
# リスト
cols = ("time", "type", "preview")
self.hist_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=16,
selectmode="browse")
for c, h, w in [("time", "時刻", 72), ("type", "種別", 56),
("preview", "内容プレビュー", 300)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True, padx=4)
self.hist_tree.bind("<<TreeviewSelect>>", self._on_select_hist)
self.hist_tree.bind("<Double-1>", self._copy_to_clipboard)
# ボタン行
btn_f = tk.Frame(parent, bg="#f8f9fc")
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📌 ピン留め",
command=self._pin_selected).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="📋 クリップボードにコピー",
command=self._copy_to_clipboard).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_selected).pack(side=tk.LEFT, padx=4)
def _build_pinned_tab(self, parent):
cols = ("label", "type", "preview")
self.pin_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=18,
selectmode="browse")
for c, h, w in [("label", "ラベル", 120), ("type", "種別", 56),
("preview", "内容プレビュー", 280)]:
self.pin_tree.heading(c, text=h)
self.pin_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.pin_tree.yview)
self.pin_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.pin_tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
self.pin_tree.bind("<<TreeviewSelect>>", self._on_select_pin)
self.pin_tree.bind("<Double-1>", self._copy_pinned)
btn_f = tk.Frame(parent, bg=parent.cget("bg"))
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📋 コピー",
command=self._copy_pinned).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="✏ ラベル変更",
command=self._rename_pin).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_pin).pack(side=tk.LEFT, padx=4)
def _build_preview(self, parent):
self.preview_text = tk.Text(parent, bg="#0d1117", fg="#c9d1d9",
font=("Courier New", 11), relief=tk.FLAT,
wrap=tk.WORD, state=tk.DISABLED)
sb = ttk.Scrollbar(parent, command=self.preview_text.yview)
self.preview_text.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.preview_text.pack(fill=tk.BOTH, expand=True)
info_f = tk.Frame(parent, bg=parent.cget("background"))
info_f.pack(fill=tk.X, pady=4)
self.char_count_var = tk.StringVar(value="")
tk.Label(info_f, textvariable=self.char_count_var,
bg=info_f.cget("bg"), font=("Arial", 9),
fg="#666").pack(anchor="w")
# 手動追加
manual_f = ttk.LabelFrame(parent, text="手動追加", padding=6)
manual_f.pack(fill=tk.X, pady=4)
self.manual_text = tk.Text(manual_f, height=4, bg="#fafafa",
font=("Arial", 10), relief=tk.FLAT)
self.manual_text.pack(fill=tk.X, pady=2)
ttk.Button(manual_f, text="➕ 履歴に追加",
command=self._add_manual).pack(anchor="e")
# ── データ処理 ──────────────────────────────────────────────
def _classify(self, text):
import re
text_s = text.strip()
if re.match(r"https?://", text_s):
return "URL"
if re.search(r"^\s*(def |class |import |#|//|<!)", text_s, re.MULTILINE):
return "コード"
if re.match(r"^[\d\s,.\-+*/()%$¥€£]+$", text_s):
return "数字"
return "テキスト"
def _add_entry(self, text):
if not text or not text.strip():
return
# 重複チェック(直近)
if self._history and self._history[0].get("text") == text:
return
entry = {
"time": datetime.now().strftime("%H:%M:%S"),
"date": datetime.now().strftime("%Y-%m-%d"),
"text": text,
"type": self._classify(text),
}
self._history.insert(0, entry)
if len(self._history) > self.MAX_HISTORY:
self._history.pop()
self._save_history()
self.root.after(0, self._refresh_history_list)
def _refresh_history_list(self):
query = self.search_var.get().strip().lower() if hasattr(self, "search_var") else ""
cat = self.cat_var.get() if hasattr(self, "cat_var") else "すべて"
self.hist_tree.delete(*self.hist_tree.get_children())
for entry in self._history:
if cat != "すべて" and entry.get("type") != cat:
continue
text = entry.get("text", "")
if query and query not in text.lower():
continue
preview = text[:80].replace("\n", "↵")
self.hist_tree.insert("", "end",
values=(entry.get("time", ""),
entry.get("type", ""),
preview))
self.status_var.set(
f"履歴: {len(self._history)} 件 / ピン: {len(self._pinned)} 件")
def _refresh_pinned_list(self):
self.pin_tree.delete(*self.pin_tree.get_children())
for p in self._pinned:
preview = p.get("text", "")[:80].replace("\n", "↵")
self.pin_tree.insert("", "end",
values=(p.get("label", ""),
p.get("type", ""),
preview))
def _set_preview(self, text):
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.insert("1.0", text)
self.preview_text.config(state=tk.DISABLED)
lines = text.count("\n") + 1
self.char_count_var.set(
f"{len(text)} 文字 / {lines} 行")
def _get_selected_text(self):
sel = self.hist_tree.selection()
if not sel:
return None
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
return filtered[idx]
return None
# ── イベントハンドラ ─────────────────────────────────────
def _on_select_hist(self, event):
entry = self._get_selected_text()
if entry:
self._set_preview(entry.get("text", ""))
def _on_select_pin(self, event):
sel = self.pin_tree.selection()
if sel:
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._set_preview(self._pinned[idx].get("text", ""))
def _copy_to_clipboard(self, event=None):
entry = self._get_selected_text()
if not entry:
return
text = entry.get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
self.status_var.set(f"コピーしました: {text[:40]}")
except Exception as e:
messagebox.showerror("エラー", str(e))
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _copy_pinned(self, event=None):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
text = self._pinned[idx].get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
except Exception:
pass
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _pin_selected(self):
entry = self._get_selected_text()
if not entry:
return
label = entry.get("text", "")[:20].replace("\n", " ")
pin_entry = {**entry, "label": label}
if any(p.get("text") == entry.get("text") for p in self._pinned):
messagebox.showinfo("情報", "すでにピン留めされています")
return
self._pinned.append(pin_entry)
self._save_history()
self._refresh_pinned_list()
self.status_var.set(f"ピン留めしました: {label}")
def _rename_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx >= len(self._pinned):
return
win = tk.Toplevel(self.root)
win.title("ラベル変更")
win.geometry("320x120")
tk.Label(win, text="新しいラベル:").pack(pady=8)
var = tk.StringVar(value=self._pinned[idx].get("label", ""))
ttk.Entry(win, textvariable=var, width=30).pack()
def save():
self._pinned[idx]["label"] = var.get()
self._save_history()
self._refresh_pinned_list()
win.destroy()
ttk.Button(win, text="保存", command=save).pack(pady=8)
def _delete_selected(self):
sel = self.hist_tree.selection()
if not sel:
return
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
self._history.remove(filtered[idx])
self._save_history()
self._refresh_history_list()
def _delete_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._pinned.pop(idx)
self._save_history()
self._refresh_pinned_list()
def _clear_history(self):
if messagebox.askyesno("確認", "履歴をすべて削除しますか?"):
self._history.clear()
self._save_history()
self._refresh_history_list()
def _add_manual(self):
text = self.manual_text.get("1.0", tk.END).strip()
if not text:
return
self._add_entry(text)
self.manual_text.delete("1.0", tk.END)
# ── クリップボード監視 ──────────────────────────────────
def _start_monitoring(self):
self._monitoring = True
self._poll()
def _poll(self):
if not self._monitoring:
return
if PYPERCLIP_AVAILABLE:
try:
current = pyperclip.paste()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
else:
# tkinter クリップボードで代替
try:
current = self.root.clipboard_get()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
self._poll_id = self.root.after(self.POLL_INTERVAL_MS, self._poll)
def _toggle_monitoring(self):
self._monitoring = not self._monitoring
if self._monitoring:
self.monitor_btn.config(text="⏸ 監視停止")
self._poll()
else:
self.monitor_btn.config(text="▶ 監視開始")
if hasattr(self, "_poll_id"):
self.root.after_cancel(self._poll_id)
def _on_close(self):
self._monitoring = False
self._save_history()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App30(root)
root.mainloop()
LabelFrameによるセクション分け
ttk.LabelFrame を使うことで、入力エリアと結果エリアを視覚的に分けられます。padding引数でフレーム内の余白を設定し、見やすいレイアウトを実現しています。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import threading
import time
from datetime import datetime
try:
import pyperclip
PYPERCLIP_AVAILABLE = True
except ImportError:
PYPERCLIP_AVAILABLE = False
class App30:
"""クリップボードマネージャー"""
MAX_HISTORY = 200
HISTORY_FILE = os.path.join(os.path.dirname(__file__), "clipboard_history.json")
POLL_INTERVAL_MS = 500
def __init__(self, root):
self.root = root
self.root.title("クリップボードマネージャー")
self.root.geometry("860x600")
self.root.configure(bg="#f8f9fc")
self._history = []
self._pinned = []
self._last_clip = ""
self._monitoring = False
self._load_history()
self._build_ui()
self._start_monitoring()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _load_history(self):
if os.path.exists(self.HISTORY_FILE):
try:
with open(self.HISTORY_FILE, encoding="utf-8") as f:
data = json.load(f)
self._history = data.get("history", [])
self._pinned = data.get("pinned", [])
except Exception:
pass
def _save_history(self):
try:
with open(self.HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump({
"history": self._history[-self.MAX_HISTORY:],
"pinned": self._pinned,
}, f, ensure_ascii=False, indent=2)
except Exception:
pass
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#37474f", pady=8)
header.pack(fill=tk.X)
tk.Label(header, text="📋 クリップボードマネージャー",
font=("Noto Sans JP", 14, "bold"),
bg="#37474f", fg="white").pack(side=tk.LEFT, padx=12)
self.monitor_btn = tk.Button(
header, text="⏸ 監視停止", bg="#546e7a", fg="white",
relief=tk.FLAT, font=("Arial", 10), padx=10,
command=self._toggle_monitoring)
self.monitor_btn.pack(side=tk.RIGHT, padx=8)
tk.Label(header, text="監視中:", bg="#37474f", fg="#b0bec5",
font=("Arial", 9)).pack(side=tk.RIGHT)
if not PYPERCLIP_AVAILABLE:
tk.Label(self.root,
text="⚠ pyperclip が未インストールです (pip install pyperclip)。"
"クリップボード監視は無効です。",
bg="#fff3cd", fg="#856404",
font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X)
# メインエリア
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
# 左: 履歴 + ピン留め
left = tk.Frame(paned, bg="#f8f9fc")
paned.add(left, weight=5)
notebook = ttk.Notebook(left)
notebook.pack(fill=tk.BOTH, expand=True)
# 履歴タブ
hist_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(hist_tab, text="履歴")
self._build_history_tab(hist_tab)
# ピン留めタブ
pin_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(pin_tab, text="⭐ ピン留め")
self._build_pinned_tab(pin_tab)
# 右: プレビュー
right = ttk.LabelFrame(paned, text="プレビュー", padding=4)
paned.add(right, weight=3)
self._build_preview(right)
# ステータス
self.status_var = tk.StringVar(value="監視中...")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._refresh_history_list()
self._refresh_pinned_list()
def _build_history_tab(self, parent):
# ツールバー
bar = tk.Frame(parent, bg="#f8f9fc")
bar.pack(fill=tk.X, padx=4, pady=4)
tk.Label(bar, text="🔍", bg="#f8f9fc").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *a: self._refresh_history_list())
ttk.Entry(bar, textvariable=self.search_var,
width=20).pack(side=tk.LEFT, padx=4, fill=tk.X, expand=True)
ttk.Button(bar, text="全削除",
command=self._clear_history).pack(side=tk.RIGHT, padx=4)
# カテゴリフィルター
cat_f = tk.Frame(parent, bg="#f8f9fc")
cat_f.pack(fill=tk.X, padx=4)
tk.Label(cat_f, text="種別:", bg="#f8f9fc").pack(side=tk.LEFT)
self.cat_var = tk.StringVar(value="すべて")
for val in ["すべて", "テキスト", "URL", "コード", "数字"]:
ttk.Radiobutton(cat_f, text=val, variable=self.cat_var,
value=val,
command=self._refresh_history_list
).pack(side=tk.LEFT, padx=3)
# リスト
cols = ("time", "type", "preview")
self.hist_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=16,
selectmode="browse")
for c, h, w in [("time", "時刻", 72), ("type", "種別", 56),
("preview", "内容プレビュー", 300)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True, padx=4)
self.hist_tree.bind("<<TreeviewSelect>>", self._on_select_hist)
self.hist_tree.bind("<Double-1>", self._copy_to_clipboard)
# ボタン行
btn_f = tk.Frame(parent, bg="#f8f9fc")
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📌 ピン留め",
command=self._pin_selected).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="📋 クリップボードにコピー",
command=self._copy_to_clipboard).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_selected).pack(side=tk.LEFT, padx=4)
def _build_pinned_tab(self, parent):
cols = ("label", "type", "preview")
self.pin_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=18,
selectmode="browse")
for c, h, w in [("label", "ラベル", 120), ("type", "種別", 56),
("preview", "内容プレビュー", 280)]:
self.pin_tree.heading(c, text=h)
self.pin_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.pin_tree.yview)
self.pin_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.pin_tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
self.pin_tree.bind("<<TreeviewSelect>>", self._on_select_pin)
self.pin_tree.bind("<Double-1>", self._copy_pinned)
btn_f = tk.Frame(parent, bg=parent.cget("bg"))
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📋 コピー",
command=self._copy_pinned).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="✏ ラベル変更",
command=self._rename_pin).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_pin).pack(side=tk.LEFT, padx=4)
def _build_preview(self, parent):
self.preview_text = tk.Text(parent, bg="#0d1117", fg="#c9d1d9",
font=("Courier New", 11), relief=tk.FLAT,
wrap=tk.WORD, state=tk.DISABLED)
sb = ttk.Scrollbar(parent, command=self.preview_text.yview)
self.preview_text.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.preview_text.pack(fill=tk.BOTH, expand=True)
info_f = tk.Frame(parent, bg=parent.cget("background"))
info_f.pack(fill=tk.X, pady=4)
self.char_count_var = tk.StringVar(value="")
tk.Label(info_f, textvariable=self.char_count_var,
bg=info_f.cget("bg"), font=("Arial", 9),
fg="#666").pack(anchor="w")
# 手動追加
manual_f = ttk.LabelFrame(parent, text="手動追加", padding=6)
manual_f.pack(fill=tk.X, pady=4)
self.manual_text = tk.Text(manual_f, height=4, bg="#fafafa",
font=("Arial", 10), relief=tk.FLAT)
self.manual_text.pack(fill=tk.X, pady=2)
ttk.Button(manual_f, text="➕ 履歴に追加",
command=self._add_manual).pack(anchor="e")
# ── データ処理 ──────────────────────────────────────────────
def _classify(self, text):
import re
text_s = text.strip()
if re.match(r"https?://", text_s):
return "URL"
if re.search(r"^\s*(def |class |import |#|//|<!)", text_s, re.MULTILINE):
return "コード"
if re.match(r"^[\d\s,.\-+*/()%$¥€£]+$", text_s):
return "数字"
return "テキスト"
def _add_entry(self, text):
if not text or not text.strip():
return
# 重複チェック(直近)
if self._history and self._history[0].get("text") == text:
return
entry = {
"time": datetime.now().strftime("%H:%M:%S"),
"date": datetime.now().strftime("%Y-%m-%d"),
"text": text,
"type": self._classify(text),
}
self._history.insert(0, entry)
if len(self._history) > self.MAX_HISTORY:
self._history.pop()
self._save_history()
self.root.after(0, self._refresh_history_list)
def _refresh_history_list(self):
query = self.search_var.get().strip().lower() if hasattr(self, "search_var") else ""
cat = self.cat_var.get() if hasattr(self, "cat_var") else "すべて"
self.hist_tree.delete(*self.hist_tree.get_children())
for entry in self._history:
if cat != "すべて" and entry.get("type") != cat:
continue
text = entry.get("text", "")
if query and query not in text.lower():
continue
preview = text[:80].replace("\n", "↵")
self.hist_tree.insert("", "end",
values=(entry.get("time", ""),
entry.get("type", ""),
preview))
self.status_var.set(
f"履歴: {len(self._history)} 件 / ピン: {len(self._pinned)} 件")
def _refresh_pinned_list(self):
self.pin_tree.delete(*self.pin_tree.get_children())
for p in self._pinned:
preview = p.get("text", "")[:80].replace("\n", "↵")
self.pin_tree.insert("", "end",
values=(p.get("label", ""),
p.get("type", ""),
preview))
def _set_preview(self, text):
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.insert("1.0", text)
self.preview_text.config(state=tk.DISABLED)
lines = text.count("\n") + 1
self.char_count_var.set(
f"{len(text)} 文字 / {lines} 行")
def _get_selected_text(self):
sel = self.hist_tree.selection()
if not sel:
return None
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
return filtered[idx]
return None
# ── イベントハンドラ ─────────────────────────────────────
def _on_select_hist(self, event):
entry = self._get_selected_text()
if entry:
self._set_preview(entry.get("text", ""))
def _on_select_pin(self, event):
sel = self.pin_tree.selection()
if sel:
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._set_preview(self._pinned[idx].get("text", ""))
def _copy_to_clipboard(self, event=None):
entry = self._get_selected_text()
if not entry:
return
text = entry.get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
self.status_var.set(f"コピーしました: {text[:40]}")
except Exception as e:
messagebox.showerror("エラー", str(e))
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _copy_pinned(self, event=None):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
text = self._pinned[idx].get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
except Exception:
pass
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _pin_selected(self):
entry = self._get_selected_text()
if not entry:
return
label = entry.get("text", "")[:20].replace("\n", " ")
pin_entry = {**entry, "label": label}
if any(p.get("text") == entry.get("text") for p in self._pinned):
messagebox.showinfo("情報", "すでにピン留めされています")
return
self._pinned.append(pin_entry)
self._save_history()
self._refresh_pinned_list()
self.status_var.set(f"ピン留めしました: {label}")
def _rename_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx >= len(self._pinned):
return
win = tk.Toplevel(self.root)
win.title("ラベル変更")
win.geometry("320x120")
tk.Label(win, text="新しいラベル:").pack(pady=8)
var = tk.StringVar(value=self._pinned[idx].get("label", ""))
ttk.Entry(win, textvariable=var, width=30).pack()
def save():
self._pinned[idx]["label"] = var.get()
self._save_history()
self._refresh_pinned_list()
win.destroy()
ttk.Button(win, text="保存", command=save).pack(pady=8)
def _delete_selected(self):
sel = self.hist_tree.selection()
if not sel:
return
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
self._history.remove(filtered[idx])
self._save_history()
self._refresh_history_list()
def _delete_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._pinned.pop(idx)
self._save_history()
self._refresh_pinned_list()
def _clear_history(self):
if messagebox.askyesno("確認", "履歴をすべて削除しますか?"):
self._history.clear()
self._save_history()
self._refresh_history_list()
def _add_manual(self):
text = self.manual_text.get("1.0", tk.END).strip()
if not text:
return
self._add_entry(text)
self.manual_text.delete("1.0", tk.END)
# ── クリップボード監視 ──────────────────────────────────
def _start_monitoring(self):
self._monitoring = True
self._poll()
def _poll(self):
if not self._monitoring:
return
if PYPERCLIP_AVAILABLE:
try:
current = pyperclip.paste()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
else:
# tkinter クリップボードで代替
try:
current = self.root.clipboard_get()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
self._poll_id = self.root.after(self.POLL_INTERVAL_MS, self._poll)
def _toggle_monitoring(self):
self._monitoring = not self._monitoring
if self._monitoring:
self.monitor_btn.config(text="⏸ 監視停止")
self._poll()
else:
self.monitor_btn.config(text="▶ 監視開始")
if hasattr(self, "_poll_id"):
self.root.after_cancel(self._poll_id)
def _on_close(self):
self._monitoring = False
self._save_history()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App30(root)
root.mainloop()
Entryウィジェットとイベントバインド
ttk.Entryで入力フィールドを作成します。bind('
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import threading
import time
from datetime import datetime
try:
import pyperclip
PYPERCLIP_AVAILABLE = True
except ImportError:
PYPERCLIP_AVAILABLE = False
class App30:
"""クリップボードマネージャー"""
MAX_HISTORY = 200
HISTORY_FILE = os.path.join(os.path.dirname(__file__), "clipboard_history.json")
POLL_INTERVAL_MS = 500
def __init__(self, root):
self.root = root
self.root.title("クリップボードマネージャー")
self.root.geometry("860x600")
self.root.configure(bg="#f8f9fc")
self._history = []
self._pinned = []
self._last_clip = ""
self._monitoring = False
self._load_history()
self._build_ui()
self._start_monitoring()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _load_history(self):
if os.path.exists(self.HISTORY_FILE):
try:
with open(self.HISTORY_FILE, encoding="utf-8") as f:
data = json.load(f)
self._history = data.get("history", [])
self._pinned = data.get("pinned", [])
except Exception:
pass
def _save_history(self):
try:
with open(self.HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump({
"history": self._history[-self.MAX_HISTORY:],
"pinned": self._pinned,
}, f, ensure_ascii=False, indent=2)
except Exception:
pass
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#37474f", pady=8)
header.pack(fill=tk.X)
tk.Label(header, text="📋 クリップボードマネージャー",
font=("Noto Sans JP", 14, "bold"),
bg="#37474f", fg="white").pack(side=tk.LEFT, padx=12)
self.monitor_btn = tk.Button(
header, text="⏸ 監視停止", bg="#546e7a", fg="white",
relief=tk.FLAT, font=("Arial", 10), padx=10,
command=self._toggle_monitoring)
self.monitor_btn.pack(side=tk.RIGHT, padx=8)
tk.Label(header, text="監視中:", bg="#37474f", fg="#b0bec5",
font=("Arial", 9)).pack(side=tk.RIGHT)
if not PYPERCLIP_AVAILABLE:
tk.Label(self.root,
text="⚠ pyperclip が未インストールです (pip install pyperclip)。"
"クリップボード監視は無効です。",
bg="#fff3cd", fg="#856404",
font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X)
# メインエリア
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
# 左: 履歴 + ピン留め
left = tk.Frame(paned, bg="#f8f9fc")
paned.add(left, weight=5)
notebook = ttk.Notebook(left)
notebook.pack(fill=tk.BOTH, expand=True)
# 履歴タブ
hist_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(hist_tab, text="履歴")
self._build_history_tab(hist_tab)
# ピン留めタブ
pin_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(pin_tab, text="⭐ ピン留め")
self._build_pinned_tab(pin_tab)
# 右: プレビュー
right = ttk.LabelFrame(paned, text="プレビュー", padding=4)
paned.add(right, weight=3)
self._build_preview(right)
# ステータス
self.status_var = tk.StringVar(value="監視中...")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._refresh_history_list()
self._refresh_pinned_list()
def _build_history_tab(self, parent):
# ツールバー
bar = tk.Frame(parent, bg="#f8f9fc")
bar.pack(fill=tk.X, padx=4, pady=4)
tk.Label(bar, text="🔍", bg="#f8f9fc").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *a: self._refresh_history_list())
ttk.Entry(bar, textvariable=self.search_var,
width=20).pack(side=tk.LEFT, padx=4, fill=tk.X, expand=True)
ttk.Button(bar, text="全削除",
command=self._clear_history).pack(side=tk.RIGHT, padx=4)
# カテゴリフィルター
cat_f = tk.Frame(parent, bg="#f8f9fc")
cat_f.pack(fill=tk.X, padx=4)
tk.Label(cat_f, text="種別:", bg="#f8f9fc").pack(side=tk.LEFT)
self.cat_var = tk.StringVar(value="すべて")
for val in ["すべて", "テキスト", "URL", "コード", "数字"]:
ttk.Radiobutton(cat_f, text=val, variable=self.cat_var,
value=val,
command=self._refresh_history_list
).pack(side=tk.LEFT, padx=3)
# リスト
cols = ("time", "type", "preview")
self.hist_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=16,
selectmode="browse")
for c, h, w in [("time", "時刻", 72), ("type", "種別", 56),
("preview", "内容プレビュー", 300)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True, padx=4)
self.hist_tree.bind("<<TreeviewSelect>>", self._on_select_hist)
self.hist_tree.bind("<Double-1>", self._copy_to_clipboard)
# ボタン行
btn_f = tk.Frame(parent, bg="#f8f9fc")
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📌 ピン留め",
command=self._pin_selected).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="📋 クリップボードにコピー",
command=self._copy_to_clipboard).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_selected).pack(side=tk.LEFT, padx=4)
def _build_pinned_tab(self, parent):
cols = ("label", "type", "preview")
self.pin_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=18,
selectmode="browse")
for c, h, w in [("label", "ラベル", 120), ("type", "種別", 56),
("preview", "内容プレビュー", 280)]:
self.pin_tree.heading(c, text=h)
self.pin_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.pin_tree.yview)
self.pin_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.pin_tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
self.pin_tree.bind("<<TreeviewSelect>>", self._on_select_pin)
self.pin_tree.bind("<Double-1>", self._copy_pinned)
btn_f = tk.Frame(parent, bg=parent.cget("bg"))
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📋 コピー",
command=self._copy_pinned).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="✏ ラベル変更",
command=self._rename_pin).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_pin).pack(side=tk.LEFT, padx=4)
def _build_preview(self, parent):
self.preview_text = tk.Text(parent, bg="#0d1117", fg="#c9d1d9",
font=("Courier New", 11), relief=tk.FLAT,
wrap=tk.WORD, state=tk.DISABLED)
sb = ttk.Scrollbar(parent, command=self.preview_text.yview)
self.preview_text.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.preview_text.pack(fill=tk.BOTH, expand=True)
info_f = tk.Frame(parent, bg=parent.cget("background"))
info_f.pack(fill=tk.X, pady=4)
self.char_count_var = tk.StringVar(value="")
tk.Label(info_f, textvariable=self.char_count_var,
bg=info_f.cget("bg"), font=("Arial", 9),
fg="#666").pack(anchor="w")
# 手動追加
manual_f = ttk.LabelFrame(parent, text="手動追加", padding=6)
manual_f.pack(fill=tk.X, pady=4)
self.manual_text = tk.Text(manual_f, height=4, bg="#fafafa",
font=("Arial", 10), relief=tk.FLAT)
self.manual_text.pack(fill=tk.X, pady=2)
ttk.Button(manual_f, text="➕ 履歴に追加",
command=self._add_manual).pack(anchor="e")
# ── データ処理 ──────────────────────────────────────────────
def _classify(self, text):
import re
text_s = text.strip()
if re.match(r"https?://", text_s):
return "URL"
if re.search(r"^\s*(def |class |import |#|//|<!)", text_s, re.MULTILINE):
return "コード"
if re.match(r"^[\d\s,.\-+*/()%$¥€£]+$", text_s):
return "数字"
return "テキスト"
def _add_entry(self, text):
if not text or not text.strip():
return
# 重複チェック(直近)
if self._history and self._history[0].get("text") == text:
return
entry = {
"time": datetime.now().strftime("%H:%M:%S"),
"date": datetime.now().strftime("%Y-%m-%d"),
"text": text,
"type": self._classify(text),
}
self._history.insert(0, entry)
if len(self._history) > self.MAX_HISTORY:
self._history.pop()
self._save_history()
self.root.after(0, self._refresh_history_list)
def _refresh_history_list(self):
query = self.search_var.get().strip().lower() if hasattr(self, "search_var") else ""
cat = self.cat_var.get() if hasattr(self, "cat_var") else "すべて"
self.hist_tree.delete(*self.hist_tree.get_children())
for entry in self._history:
if cat != "すべて" and entry.get("type") != cat:
continue
text = entry.get("text", "")
if query and query not in text.lower():
continue
preview = text[:80].replace("\n", "↵")
self.hist_tree.insert("", "end",
values=(entry.get("time", ""),
entry.get("type", ""),
preview))
self.status_var.set(
f"履歴: {len(self._history)} 件 / ピン: {len(self._pinned)} 件")
def _refresh_pinned_list(self):
self.pin_tree.delete(*self.pin_tree.get_children())
for p in self._pinned:
preview = p.get("text", "")[:80].replace("\n", "↵")
self.pin_tree.insert("", "end",
values=(p.get("label", ""),
p.get("type", ""),
preview))
def _set_preview(self, text):
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.insert("1.0", text)
self.preview_text.config(state=tk.DISABLED)
lines = text.count("\n") + 1
self.char_count_var.set(
f"{len(text)} 文字 / {lines} 行")
def _get_selected_text(self):
sel = self.hist_tree.selection()
if not sel:
return None
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
return filtered[idx]
return None
# ── イベントハンドラ ─────────────────────────────────────
def _on_select_hist(self, event):
entry = self._get_selected_text()
if entry:
self._set_preview(entry.get("text", ""))
def _on_select_pin(self, event):
sel = self.pin_tree.selection()
if sel:
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._set_preview(self._pinned[idx].get("text", ""))
def _copy_to_clipboard(self, event=None):
entry = self._get_selected_text()
if not entry:
return
text = entry.get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
self.status_var.set(f"コピーしました: {text[:40]}")
except Exception as e:
messagebox.showerror("エラー", str(e))
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _copy_pinned(self, event=None):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
text = self._pinned[idx].get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
except Exception:
pass
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _pin_selected(self):
entry = self._get_selected_text()
if not entry:
return
label = entry.get("text", "")[:20].replace("\n", " ")
pin_entry = {**entry, "label": label}
if any(p.get("text") == entry.get("text") for p in self._pinned):
messagebox.showinfo("情報", "すでにピン留めされています")
return
self._pinned.append(pin_entry)
self._save_history()
self._refresh_pinned_list()
self.status_var.set(f"ピン留めしました: {label}")
def _rename_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx >= len(self._pinned):
return
win = tk.Toplevel(self.root)
win.title("ラベル変更")
win.geometry("320x120")
tk.Label(win, text="新しいラベル:").pack(pady=8)
var = tk.StringVar(value=self._pinned[idx].get("label", ""))
ttk.Entry(win, textvariable=var, width=30).pack()
def save():
self._pinned[idx]["label"] = var.get()
self._save_history()
self._refresh_pinned_list()
win.destroy()
ttk.Button(win, text="保存", command=save).pack(pady=8)
def _delete_selected(self):
sel = self.hist_tree.selection()
if not sel:
return
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
self._history.remove(filtered[idx])
self._save_history()
self._refresh_history_list()
def _delete_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._pinned.pop(idx)
self._save_history()
self._refresh_pinned_list()
def _clear_history(self):
if messagebox.askyesno("確認", "履歴をすべて削除しますか?"):
self._history.clear()
self._save_history()
self._refresh_history_list()
def _add_manual(self):
text = self.manual_text.get("1.0", tk.END).strip()
if not text:
return
self._add_entry(text)
self.manual_text.delete("1.0", tk.END)
# ── クリップボード監視 ──────────────────────────────────
def _start_monitoring(self):
self._monitoring = True
self._poll()
def _poll(self):
if not self._monitoring:
return
if PYPERCLIP_AVAILABLE:
try:
current = pyperclip.paste()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
else:
# tkinter クリップボードで代替
try:
current = self.root.clipboard_get()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
self._poll_id = self.root.after(self.POLL_INTERVAL_MS, self._poll)
def _toggle_monitoring(self):
self._monitoring = not self._monitoring
if self._monitoring:
self.monitor_btn.config(text="⏸ 監視停止")
self._poll()
else:
self.monitor_btn.config(text="▶ 監視開始")
if hasattr(self, "_poll_id"):
self.root.after_cancel(self._poll_id)
def _on_close(self):
self._monitoring = False
self._save_history()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App30(root)
root.mainloop()
Textウィジェットでの結果表示
結果表示にはtk.Textウィジェットを使います。state=tk.DISABLEDでユーザーが直接編集できないようにし、表示前にNORMALに切り替えてからinsert()で内容を更新します。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import threading
import time
from datetime import datetime
try:
import pyperclip
PYPERCLIP_AVAILABLE = True
except ImportError:
PYPERCLIP_AVAILABLE = False
class App30:
"""クリップボードマネージャー"""
MAX_HISTORY = 200
HISTORY_FILE = os.path.join(os.path.dirname(__file__), "clipboard_history.json")
POLL_INTERVAL_MS = 500
def __init__(self, root):
self.root = root
self.root.title("クリップボードマネージャー")
self.root.geometry("860x600")
self.root.configure(bg="#f8f9fc")
self._history = []
self._pinned = []
self._last_clip = ""
self._monitoring = False
self._load_history()
self._build_ui()
self._start_monitoring()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _load_history(self):
if os.path.exists(self.HISTORY_FILE):
try:
with open(self.HISTORY_FILE, encoding="utf-8") as f:
data = json.load(f)
self._history = data.get("history", [])
self._pinned = data.get("pinned", [])
except Exception:
pass
def _save_history(self):
try:
with open(self.HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump({
"history": self._history[-self.MAX_HISTORY:],
"pinned": self._pinned,
}, f, ensure_ascii=False, indent=2)
except Exception:
pass
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#37474f", pady=8)
header.pack(fill=tk.X)
tk.Label(header, text="📋 クリップボードマネージャー",
font=("Noto Sans JP", 14, "bold"),
bg="#37474f", fg="white").pack(side=tk.LEFT, padx=12)
self.monitor_btn = tk.Button(
header, text="⏸ 監視停止", bg="#546e7a", fg="white",
relief=tk.FLAT, font=("Arial", 10), padx=10,
command=self._toggle_monitoring)
self.monitor_btn.pack(side=tk.RIGHT, padx=8)
tk.Label(header, text="監視中:", bg="#37474f", fg="#b0bec5",
font=("Arial", 9)).pack(side=tk.RIGHT)
if not PYPERCLIP_AVAILABLE:
tk.Label(self.root,
text="⚠ pyperclip が未インストールです (pip install pyperclip)。"
"クリップボード監視は無効です。",
bg="#fff3cd", fg="#856404",
font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X)
# メインエリア
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
# 左: 履歴 + ピン留め
left = tk.Frame(paned, bg="#f8f9fc")
paned.add(left, weight=5)
notebook = ttk.Notebook(left)
notebook.pack(fill=tk.BOTH, expand=True)
# 履歴タブ
hist_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(hist_tab, text="履歴")
self._build_history_tab(hist_tab)
# ピン留めタブ
pin_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(pin_tab, text="⭐ ピン留め")
self._build_pinned_tab(pin_tab)
# 右: プレビュー
right = ttk.LabelFrame(paned, text="プレビュー", padding=4)
paned.add(right, weight=3)
self._build_preview(right)
# ステータス
self.status_var = tk.StringVar(value="監視中...")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._refresh_history_list()
self._refresh_pinned_list()
def _build_history_tab(self, parent):
# ツールバー
bar = tk.Frame(parent, bg="#f8f9fc")
bar.pack(fill=tk.X, padx=4, pady=4)
tk.Label(bar, text="🔍", bg="#f8f9fc").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *a: self._refresh_history_list())
ttk.Entry(bar, textvariable=self.search_var,
width=20).pack(side=tk.LEFT, padx=4, fill=tk.X, expand=True)
ttk.Button(bar, text="全削除",
command=self._clear_history).pack(side=tk.RIGHT, padx=4)
# カテゴリフィルター
cat_f = tk.Frame(parent, bg="#f8f9fc")
cat_f.pack(fill=tk.X, padx=4)
tk.Label(cat_f, text="種別:", bg="#f8f9fc").pack(side=tk.LEFT)
self.cat_var = tk.StringVar(value="すべて")
for val in ["すべて", "テキスト", "URL", "コード", "数字"]:
ttk.Radiobutton(cat_f, text=val, variable=self.cat_var,
value=val,
command=self._refresh_history_list
).pack(side=tk.LEFT, padx=3)
# リスト
cols = ("time", "type", "preview")
self.hist_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=16,
selectmode="browse")
for c, h, w in [("time", "時刻", 72), ("type", "種別", 56),
("preview", "内容プレビュー", 300)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True, padx=4)
self.hist_tree.bind("<<TreeviewSelect>>", self._on_select_hist)
self.hist_tree.bind("<Double-1>", self._copy_to_clipboard)
# ボタン行
btn_f = tk.Frame(parent, bg="#f8f9fc")
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📌 ピン留め",
command=self._pin_selected).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="📋 クリップボードにコピー",
command=self._copy_to_clipboard).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_selected).pack(side=tk.LEFT, padx=4)
def _build_pinned_tab(self, parent):
cols = ("label", "type", "preview")
self.pin_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=18,
selectmode="browse")
for c, h, w in [("label", "ラベル", 120), ("type", "種別", 56),
("preview", "内容プレビュー", 280)]:
self.pin_tree.heading(c, text=h)
self.pin_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.pin_tree.yview)
self.pin_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.pin_tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
self.pin_tree.bind("<<TreeviewSelect>>", self._on_select_pin)
self.pin_tree.bind("<Double-1>", self._copy_pinned)
btn_f = tk.Frame(parent, bg=parent.cget("bg"))
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📋 コピー",
command=self._copy_pinned).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="✏ ラベル変更",
command=self._rename_pin).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_pin).pack(side=tk.LEFT, padx=4)
def _build_preview(self, parent):
self.preview_text = tk.Text(parent, bg="#0d1117", fg="#c9d1d9",
font=("Courier New", 11), relief=tk.FLAT,
wrap=tk.WORD, state=tk.DISABLED)
sb = ttk.Scrollbar(parent, command=self.preview_text.yview)
self.preview_text.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.preview_text.pack(fill=tk.BOTH, expand=True)
info_f = tk.Frame(parent, bg=parent.cget("background"))
info_f.pack(fill=tk.X, pady=4)
self.char_count_var = tk.StringVar(value="")
tk.Label(info_f, textvariable=self.char_count_var,
bg=info_f.cget("bg"), font=("Arial", 9),
fg="#666").pack(anchor="w")
# 手動追加
manual_f = ttk.LabelFrame(parent, text="手動追加", padding=6)
manual_f.pack(fill=tk.X, pady=4)
self.manual_text = tk.Text(manual_f, height=4, bg="#fafafa",
font=("Arial", 10), relief=tk.FLAT)
self.manual_text.pack(fill=tk.X, pady=2)
ttk.Button(manual_f, text="➕ 履歴に追加",
command=self._add_manual).pack(anchor="e")
# ── データ処理 ──────────────────────────────────────────────
def _classify(self, text):
import re
text_s = text.strip()
if re.match(r"https?://", text_s):
return "URL"
if re.search(r"^\s*(def |class |import |#|//|<!)", text_s, re.MULTILINE):
return "コード"
if re.match(r"^[\d\s,.\-+*/()%$¥€£]+$", text_s):
return "数字"
return "テキスト"
def _add_entry(self, text):
if not text or not text.strip():
return
# 重複チェック(直近)
if self._history and self._history[0].get("text") == text:
return
entry = {
"time": datetime.now().strftime("%H:%M:%S"),
"date": datetime.now().strftime("%Y-%m-%d"),
"text": text,
"type": self._classify(text),
}
self._history.insert(0, entry)
if len(self._history) > self.MAX_HISTORY:
self._history.pop()
self._save_history()
self.root.after(0, self._refresh_history_list)
def _refresh_history_list(self):
query = self.search_var.get().strip().lower() if hasattr(self, "search_var") else ""
cat = self.cat_var.get() if hasattr(self, "cat_var") else "すべて"
self.hist_tree.delete(*self.hist_tree.get_children())
for entry in self._history:
if cat != "すべて" and entry.get("type") != cat:
continue
text = entry.get("text", "")
if query and query not in text.lower():
continue
preview = text[:80].replace("\n", "↵")
self.hist_tree.insert("", "end",
values=(entry.get("time", ""),
entry.get("type", ""),
preview))
self.status_var.set(
f"履歴: {len(self._history)} 件 / ピン: {len(self._pinned)} 件")
def _refresh_pinned_list(self):
self.pin_tree.delete(*self.pin_tree.get_children())
for p in self._pinned:
preview = p.get("text", "")[:80].replace("\n", "↵")
self.pin_tree.insert("", "end",
values=(p.get("label", ""),
p.get("type", ""),
preview))
def _set_preview(self, text):
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.insert("1.0", text)
self.preview_text.config(state=tk.DISABLED)
lines = text.count("\n") + 1
self.char_count_var.set(
f"{len(text)} 文字 / {lines} 行")
def _get_selected_text(self):
sel = self.hist_tree.selection()
if not sel:
return None
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
return filtered[idx]
return None
# ── イベントハンドラ ─────────────────────────────────────
def _on_select_hist(self, event):
entry = self._get_selected_text()
if entry:
self._set_preview(entry.get("text", ""))
def _on_select_pin(self, event):
sel = self.pin_tree.selection()
if sel:
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._set_preview(self._pinned[idx].get("text", ""))
def _copy_to_clipboard(self, event=None):
entry = self._get_selected_text()
if not entry:
return
text = entry.get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
self.status_var.set(f"コピーしました: {text[:40]}")
except Exception as e:
messagebox.showerror("エラー", str(e))
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _copy_pinned(self, event=None):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
text = self._pinned[idx].get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
except Exception:
pass
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _pin_selected(self):
entry = self._get_selected_text()
if not entry:
return
label = entry.get("text", "")[:20].replace("\n", " ")
pin_entry = {**entry, "label": label}
if any(p.get("text") == entry.get("text") for p in self._pinned):
messagebox.showinfo("情報", "すでにピン留めされています")
return
self._pinned.append(pin_entry)
self._save_history()
self._refresh_pinned_list()
self.status_var.set(f"ピン留めしました: {label}")
def _rename_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx >= len(self._pinned):
return
win = tk.Toplevel(self.root)
win.title("ラベル変更")
win.geometry("320x120")
tk.Label(win, text="新しいラベル:").pack(pady=8)
var = tk.StringVar(value=self._pinned[idx].get("label", ""))
ttk.Entry(win, textvariable=var, width=30).pack()
def save():
self._pinned[idx]["label"] = var.get()
self._save_history()
self._refresh_pinned_list()
win.destroy()
ttk.Button(win, text="保存", command=save).pack(pady=8)
def _delete_selected(self):
sel = self.hist_tree.selection()
if not sel:
return
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
self._history.remove(filtered[idx])
self._save_history()
self._refresh_history_list()
def _delete_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._pinned.pop(idx)
self._save_history()
self._refresh_pinned_list()
def _clear_history(self):
if messagebox.askyesno("確認", "履歴をすべて削除しますか?"):
self._history.clear()
self._save_history()
self._refresh_history_list()
def _add_manual(self):
text = self.manual_text.get("1.0", tk.END).strip()
if not text:
return
self._add_entry(text)
self.manual_text.delete("1.0", tk.END)
# ── クリップボード監視 ──────────────────────────────────
def _start_monitoring(self):
self._monitoring = True
self._poll()
def _poll(self):
if not self._monitoring:
return
if PYPERCLIP_AVAILABLE:
try:
current = pyperclip.paste()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
else:
# tkinter クリップボードで代替
try:
current = self.root.clipboard_get()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
self._poll_id = self.root.after(self.POLL_INTERVAL_MS, self._poll)
def _toggle_monitoring(self):
self._monitoring = not self._monitoring
if self._monitoring:
self.monitor_btn.config(text="⏸ 監視停止")
self._poll()
else:
self.monitor_btn.config(text="▶ 監視開始")
if hasattr(self, "_poll_id"):
self.root.after_cancel(self._poll_id)
def _on_close(self):
self._monitoring = False
self._save_history()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App30(root)
root.mainloop()
例外処理とmessagebox
try-except で ValueError と Exception を捕捉し、messagebox.showerror() でユーザーにわかりやすいエラーメッセージを表示します。入力バリデーションは必ず実装しましょう。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import threading
import time
from datetime import datetime
try:
import pyperclip
PYPERCLIP_AVAILABLE = True
except ImportError:
PYPERCLIP_AVAILABLE = False
class App30:
"""クリップボードマネージャー"""
MAX_HISTORY = 200
HISTORY_FILE = os.path.join(os.path.dirname(__file__), "clipboard_history.json")
POLL_INTERVAL_MS = 500
def __init__(self, root):
self.root = root
self.root.title("クリップボードマネージャー")
self.root.geometry("860x600")
self.root.configure(bg="#f8f9fc")
self._history = []
self._pinned = []
self._last_clip = ""
self._monitoring = False
self._load_history()
self._build_ui()
self._start_monitoring()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _load_history(self):
if os.path.exists(self.HISTORY_FILE):
try:
with open(self.HISTORY_FILE, encoding="utf-8") as f:
data = json.load(f)
self._history = data.get("history", [])
self._pinned = data.get("pinned", [])
except Exception:
pass
def _save_history(self):
try:
with open(self.HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump({
"history": self._history[-self.MAX_HISTORY:],
"pinned": self._pinned,
}, f, ensure_ascii=False, indent=2)
except Exception:
pass
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#37474f", pady=8)
header.pack(fill=tk.X)
tk.Label(header, text="📋 クリップボードマネージャー",
font=("Noto Sans JP", 14, "bold"),
bg="#37474f", fg="white").pack(side=tk.LEFT, padx=12)
self.monitor_btn = tk.Button(
header, text="⏸ 監視停止", bg="#546e7a", fg="white",
relief=tk.FLAT, font=("Arial", 10), padx=10,
command=self._toggle_monitoring)
self.monitor_btn.pack(side=tk.RIGHT, padx=8)
tk.Label(header, text="監視中:", bg="#37474f", fg="#b0bec5",
font=("Arial", 9)).pack(side=tk.RIGHT)
if not PYPERCLIP_AVAILABLE:
tk.Label(self.root,
text="⚠ pyperclip が未インストールです (pip install pyperclip)。"
"クリップボード監視は無効です。",
bg="#fff3cd", fg="#856404",
font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X)
# メインエリア
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
# 左: 履歴 + ピン留め
left = tk.Frame(paned, bg="#f8f9fc")
paned.add(left, weight=5)
notebook = ttk.Notebook(left)
notebook.pack(fill=tk.BOTH, expand=True)
# 履歴タブ
hist_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(hist_tab, text="履歴")
self._build_history_tab(hist_tab)
# ピン留めタブ
pin_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(pin_tab, text="⭐ ピン留め")
self._build_pinned_tab(pin_tab)
# 右: プレビュー
right = ttk.LabelFrame(paned, text="プレビュー", padding=4)
paned.add(right, weight=3)
self._build_preview(right)
# ステータス
self.status_var = tk.StringVar(value="監視中...")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._refresh_history_list()
self._refresh_pinned_list()
def _build_history_tab(self, parent):
# ツールバー
bar = tk.Frame(parent, bg="#f8f9fc")
bar.pack(fill=tk.X, padx=4, pady=4)
tk.Label(bar, text="🔍", bg="#f8f9fc").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *a: self._refresh_history_list())
ttk.Entry(bar, textvariable=self.search_var,
width=20).pack(side=tk.LEFT, padx=4, fill=tk.X, expand=True)
ttk.Button(bar, text="全削除",
command=self._clear_history).pack(side=tk.RIGHT, padx=4)
# カテゴリフィルター
cat_f = tk.Frame(parent, bg="#f8f9fc")
cat_f.pack(fill=tk.X, padx=4)
tk.Label(cat_f, text="種別:", bg="#f8f9fc").pack(side=tk.LEFT)
self.cat_var = tk.StringVar(value="すべて")
for val in ["すべて", "テキスト", "URL", "コード", "数字"]:
ttk.Radiobutton(cat_f, text=val, variable=self.cat_var,
value=val,
command=self._refresh_history_list
).pack(side=tk.LEFT, padx=3)
# リスト
cols = ("time", "type", "preview")
self.hist_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=16,
selectmode="browse")
for c, h, w in [("time", "時刻", 72), ("type", "種別", 56),
("preview", "内容プレビュー", 300)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True, padx=4)
self.hist_tree.bind("<<TreeviewSelect>>", self._on_select_hist)
self.hist_tree.bind("<Double-1>", self._copy_to_clipboard)
# ボタン行
btn_f = tk.Frame(parent, bg="#f8f9fc")
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📌 ピン留め",
command=self._pin_selected).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="📋 クリップボードにコピー",
command=self._copy_to_clipboard).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_selected).pack(side=tk.LEFT, padx=4)
def _build_pinned_tab(self, parent):
cols = ("label", "type", "preview")
self.pin_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=18,
selectmode="browse")
for c, h, w in [("label", "ラベル", 120), ("type", "種別", 56),
("preview", "内容プレビュー", 280)]:
self.pin_tree.heading(c, text=h)
self.pin_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.pin_tree.yview)
self.pin_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.pin_tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
self.pin_tree.bind("<<TreeviewSelect>>", self._on_select_pin)
self.pin_tree.bind("<Double-1>", self._copy_pinned)
btn_f = tk.Frame(parent, bg=parent.cget("bg"))
btn_f.pack(fill=tk.X, padx=4, pady=4)
ttk.Button(btn_f, text="📋 コピー",
command=self._copy_pinned).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="✏ ラベル変更",
command=self._rename_pin).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_f, text="🗑 削除",
command=self._delete_pin).pack(side=tk.LEFT, padx=4)
def _build_preview(self, parent):
self.preview_text = tk.Text(parent, bg="#0d1117", fg="#c9d1d9",
font=("Courier New", 11), relief=tk.FLAT,
wrap=tk.WORD, state=tk.DISABLED)
sb = ttk.Scrollbar(parent, command=self.preview_text.yview)
self.preview_text.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.preview_text.pack(fill=tk.BOTH, expand=True)
info_f = tk.Frame(parent, bg=parent.cget("background"))
info_f.pack(fill=tk.X, pady=4)
self.char_count_var = tk.StringVar(value="")
tk.Label(info_f, textvariable=self.char_count_var,
bg=info_f.cget("bg"), font=("Arial", 9),
fg="#666").pack(anchor="w")
# 手動追加
manual_f = ttk.LabelFrame(parent, text="手動追加", padding=6)
manual_f.pack(fill=tk.X, pady=4)
self.manual_text = tk.Text(manual_f, height=4, bg="#fafafa",
font=("Arial", 10), relief=tk.FLAT)
self.manual_text.pack(fill=tk.X, pady=2)
ttk.Button(manual_f, text="➕ 履歴に追加",
command=self._add_manual).pack(anchor="e")
# ── データ処理 ──────────────────────────────────────────────
def _classify(self, text):
import re
text_s = text.strip()
if re.match(r"https?://", text_s):
return "URL"
if re.search(r"^\s*(def |class |import |#|//|<!)", text_s, re.MULTILINE):
return "コード"
if re.match(r"^[\d\s,.\-+*/()%$¥€£]+$", text_s):
return "数字"
return "テキスト"
def _add_entry(self, text):
if not text or not text.strip():
return
# 重複チェック(直近)
if self._history and self._history[0].get("text") == text:
return
entry = {
"time": datetime.now().strftime("%H:%M:%S"),
"date": datetime.now().strftime("%Y-%m-%d"),
"text": text,
"type": self._classify(text),
}
self._history.insert(0, entry)
if len(self._history) > self.MAX_HISTORY:
self._history.pop()
self._save_history()
self.root.after(0, self._refresh_history_list)
def _refresh_history_list(self):
query = self.search_var.get().strip().lower() if hasattr(self, "search_var") else ""
cat = self.cat_var.get() if hasattr(self, "cat_var") else "すべて"
self.hist_tree.delete(*self.hist_tree.get_children())
for entry in self._history:
if cat != "すべて" and entry.get("type") != cat:
continue
text = entry.get("text", "")
if query and query not in text.lower():
continue
preview = text[:80].replace("\n", "↵")
self.hist_tree.insert("", "end",
values=(entry.get("time", ""),
entry.get("type", ""),
preview))
self.status_var.set(
f"履歴: {len(self._history)} 件 / ピン: {len(self._pinned)} 件")
def _refresh_pinned_list(self):
self.pin_tree.delete(*self.pin_tree.get_children())
for p in self._pinned:
preview = p.get("text", "")[:80].replace("\n", "↵")
self.pin_tree.insert("", "end",
values=(p.get("label", ""),
p.get("type", ""),
preview))
def _set_preview(self, text):
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete("1.0", tk.END)
self.preview_text.insert("1.0", text)
self.preview_text.config(state=tk.DISABLED)
lines = text.count("\n") + 1
self.char_count_var.set(
f"{len(text)} 文字 / {lines} 行")
def _get_selected_text(self):
sel = self.hist_tree.selection()
if not sel:
return None
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
return filtered[idx]
return None
# ── イベントハンドラ ─────────────────────────────────────
def _on_select_hist(self, event):
entry = self._get_selected_text()
if entry:
self._set_preview(entry.get("text", ""))
def _on_select_pin(self, event):
sel = self.pin_tree.selection()
if sel:
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._set_preview(self._pinned[idx].get("text", ""))
def _copy_to_clipboard(self, event=None):
entry = self._get_selected_text()
if not entry:
return
text = entry.get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
self.status_var.set(f"コピーしました: {text[:40]}")
except Exception as e:
messagebox.showerror("エラー", str(e))
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _copy_pinned(self, event=None):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
text = self._pinned[idx].get("text", "")
if PYPERCLIP_AVAILABLE:
try:
pyperclip.copy(text)
except Exception:
pass
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.status_var.set(f"コピーしました: {text[:40]}")
def _pin_selected(self):
entry = self._get_selected_text()
if not entry:
return
label = entry.get("text", "")[:20].replace("\n", " ")
pin_entry = {**entry, "label": label}
if any(p.get("text") == entry.get("text") for p in self._pinned):
messagebox.showinfo("情報", "すでにピン留めされています")
return
self._pinned.append(pin_entry)
self._save_history()
self._refresh_pinned_list()
self.status_var.set(f"ピン留めしました: {label}")
def _rename_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx >= len(self._pinned):
return
win = tk.Toplevel(self.root)
win.title("ラベル変更")
win.geometry("320x120")
tk.Label(win, text="新しいラベル:").pack(pady=8)
var = tk.StringVar(value=self._pinned[idx].get("label", ""))
ttk.Entry(win, textvariable=var, width=30).pack()
def save():
self._pinned[idx]["label"] = var.get()
self._save_history()
self._refresh_pinned_list()
win.destroy()
ttk.Button(win, text="保存", command=save).pack(pady=8)
def _delete_selected(self):
sel = self.hist_tree.selection()
if not sel:
return
idx = self.hist_tree.index(sel[0])
query = self.search_var.get().strip().lower()
cat = self.cat_var.get()
filtered = [e for e in self._history
if (cat == "すべて" or e.get("type") == cat) and
(not query or query in e.get("text", "").lower())]
if idx < len(filtered):
self._history.remove(filtered[idx])
self._save_history()
self._refresh_history_list()
def _delete_pin(self):
sel = self.pin_tree.selection()
if not sel:
return
idx = self.pin_tree.index(sel[0])
if idx < len(self._pinned):
self._pinned.pop(idx)
self._save_history()
self._refresh_pinned_list()
def _clear_history(self):
if messagebox.askyesno("確認", "履歴をすべて削除しますか?"):
self._history.clear()
self._save_history()
self._refresh_history_list()
def _add_manual(self):
text = self.manual_text.get("1.0", tk.END).strip()
if not text:
return
self._add_entry(text)
self.manual_text.delete("1.0", tk.END)
# ── クリップボード監視 ──────────────────────────────────
def _start_monitoring(self):
self._monitoring = True
self._poll()
def _poll(self):
if not self._monitoring:
return
if PYPERCLIP_AVAILABLE:
try:
current = pyperclip.paste()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
else:
# tkinter クリップボードで代替
try:
current = self.root.clipboard_get()
if current and current != self._last_clip:
self._last_clip = current
self._add_entry(current)
except Exception:
pass
self._poll_id = self.root.after(self.POLL_INTERVAL_MS, self._poll)
def _toggle_monitoring(self):
self._monitoring = not self._monitoring
if self._monitoring:
self.monitor_btn.config(text="⏸ 監視停止")
self._poll()
else:
self.monitor_btn.config(text="▶ 監視開始")
if hasattr(self, "_poll_id"):
self.root.after_cancel(self._poll_id)
def _on_close(self):
self._monitoring = False
self._save_history()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App30(root)
root.mainloop()
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app30.py と保存します。
-
2クラスの骨格を作る
App30クラスを定義し、__init__とmainloop()の最小構成を作ります。
-
3タイトルバーを作る
Frameを使ってカラーバー付きのタイトルエリアを作ります。
-
4入力フォームを実装する
LabelFrameとEntryウィジェットで入力エリアを作ります。
-
5処理ロジックを実装する
_calculate()メソッドに計算・処理ロジックを実装します。
-
6結果表示を実装する
TextウィジェットかLabelに結果を表示する_show_result()を実装します。
-
7エラー処理を追加する
try-exceptとmessageboxでエラーハンドリングを追加します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 データのエクスポート機能
計算結果をCSV・TXTファイルに保存するエクスポート機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。
💡 入力履歴機能
以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。
8. よくある問題と解決法
❌ 日本語フォントが表示されない
原因:システムに日本語フォントが見つからない場合があります。
解決法:font引数を省略するかシステムに合ったフォントを指定してください。
❌ ウィンドウのサイズが変更できない
原因:resizable(False, False)が設定されています。
解決法:resizable(True, True)に変更してください。
9. 練習問題
アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。
-
課題1:機能拡張
クリップボードマネージャーに新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。
-
課題2:UIの改善
色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしてみましょう。
-
課題3:保存機能の追加
入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。