クリップボードマネージャー
コピーしたテキストの履歴を保存・管理・検索できるクリップボードマネージャー。pyperclipの活用を学びます。
1. アプリ概要
コピーしたテキストの履歴を保存・管理・検索できるクリップボードマネージャー。pyperclipの活用を学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ)・pyperclip で、難易度は ★★☆ です。
このアプリはこんなときに使います。コピーした内容が次のコピーで消えてしまう「上書き地獄」を解決したいとき。コーディング中に何度も使う定型文・URL・コードスニペットを履歴からすぐ呼び出したいとき——そんな日常の小さなストレスを自前ツールで消せます。pyperclip によるクリップボード監視は、翻訳ツール・スニペットランチャー・定型文入力支援など「コピー内容をフックして加工する」アプリ全般の基礎になります。
pyperclip でつまずきやすい点が3つあります。① Linux で Pyperclip could not find a copy/paste mechanism と出る: バックエンドが必要です(apt install xclip または xsel)。Windows・Mac は標準で動きます。② 履歴監視の CPU 使用率が高い: paste() を tight loop で回すと重くなります。root.after(500, check) で 0.5 秒間隔のポーリングにしましょう。③ 同じ内容が何度も履歴に積まれる: 直前の値と比較し、変化したときだけ追加するようにします。
慣れたら、こう応用できます。履歴を SQLite に永続化すれば再起動後も残せます(保存の型は SQLiteタスク管理アプリ の実装が参考になります)。よく使う項目のピン留め、履歴の絞り込み検索、グローバルホットキーでの呼び出しなども定番の拡張です。標準ライブラリ以外のツールの探し方は Pythonライブラリ一覧 も参考にしてください。
2. 機能一覧
- 「⏸ 監視停止」ボタン(
_toggle_monitoring())・「全削除」ボタン(_clear_history())・「📌 ピン留め」ボタン(_pin_selected())・「📋 クリップボードにコピー」ボタン(_copy_to_clipboard())・「🗑 削除」ボタン(_delete_selected())・「📋 コピー」ボタン(_copy_pinned())・「✏ ラベル変更」ボタン(_rename_pin())・「🗑 削除」ボタン(_delete_pin())・「➕ 履歴に追加」ボタン(_add_manual())・「保存」ボタンで操作 - イベント
<<TreeviewSelect>>から_on_select_hist()を実行 - マウス操作
<Double-1>から_copy_to_clipboard()を実行 - イベント
<<TreeviewSelect>>から_on_select_pin()を実行 - マウス操作
<Double-1>から_copy_pinned()を実行 ttk.Treeviewによる表形式の一覧表示- JSON ファイルの保存・読み込み(起動時に自動実行)
messagebox.askyesno()・messagebox.showerror()・messagebox.showinfo()によるダイアログ通知- ウィンドウはタイトル「クリップボードマネージャー」・サイズ 1002x712 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
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("1002x712")
self.root.minsize(1002, 712)
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=ttk.Style().lookup("TLabelframe", "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 クラスにアプリの全機能をまとめています(メソッド27個・全464行)。__init__ ではタイトル「クリップボードマネージャー」とウィンドウサイズ 1002x712 を設定します。あわせて状態を保持する変数(self._last_clip・self._monitoring)を初期化し、そのあと _load_history() → _build_ui() → _start_monitoring() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×9・tk.Label×8・tk.Button・ttk.PanedWindow・ttk.Notebook・ttk.LabelFrame×2・ttk.Entry×2・ttk.Button×9・ttk.Radiobutton(ループで生成)・ttk.Treeview×2・ttk.Scrollbar×3・tk.Text×2・ttk.Style・tk.Toplevel で構成しています。見出し付きの枠(LabelFrame)は「プレビュー」「手動追加」のラベルでエリアを区切っています。PanedWindow を使っているので、ペインの境界をマウスでドラッグして幅を変えられます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「⏸ 監視停止」ボタン(_toggle_monitoring())・「全削除」ボタン(_clear_history())・「📌 ピン留め」ボタン(_pin_selected())・「📋 クリップボードにコピー」ボタン(_copy_to_clipboard())・「🗑 削除」ボタン(_delete_selected())・「📋 コピー」ボタン(_copy_pinned())・「✏ ラベル変更」ボタン(_rename_pin())・「🗑 削除」ボタン(_delete_pin())・「➕ 履歴に追加」ボタン(_add_manual())・「保存」ボタンを command= で接続しています。また bind("<<TreeviewSelect>>", ...) から _on_select_hist()、bind("<Double-1>", ...) から _copy_to_clipboard()、bind("<<TreeviewSelect>>", ...) から _on_select_pin()、bind("<Double-1>", ...) から _copy_pinned() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
JSON への保存と読み込み
データは json.dump() でファイル(clipboard_history.json)へ保存します。起動時は json.load() で読み戻します。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
クラス定数によるデータ定義
クラスの先頭でデータを定数として定義しています:MAX_HISTORY = 200、HISTORY_FILE = os.path.join(os.path.dirname(__file__), 'clipboard_history.json')。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・ImportError を捕捉しています。あわせて messagebox.askyesno()・messagebox.showerror()・messagebox.showinfo() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app30.py と保存します。外部ライブラリ
pyperclipを使います。先にpip install pyperclipを実行してください。 -
2クラスの骨格を作る
App30クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("クリップボードマネージャー")・geometry("1002x712")・configure(bg="#f8f9fc")・minsize(1002, 712)をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×9・tk.Label×8・tk.Button・ttk.PanedWindow・ttk.Notebook・ttk.LabelFrame×2・ttk.Entry×2・ttk.Button×9・ttk.Radiobutton(ループで生成)・ttk.Treeview×2・ttk.Scrollbar×3・tk.Text×2・ttk.Style・tk.Toplevelを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_toggle_monitoring()・_clear_history()・_pin_selected()・_copy_to_clipboard()・_delete_selected()・_copy_pinned()・_rename_pin()・_delete_pin()・_add_manual()・save)につなぎます。bind("<<TreeviewSelect>>", ...)・bind("<Double-1>", ...)・bind("<<TreeviewSelect>>", ...)・bind("<Double-1>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_refresh_history_list()(17行)・_poll()(21行)・_rename_pin()(21行)など を実装します。 -
7動作確認する
python app30.pyで起動し、各ボタンが反応すること・マウス操作(クリック・ホイールなど)が効くことを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 クラス定数を書き換えて動きを変える
データはクラス定数(MAX_HISTORY・HISTORY_FILE)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。
💡 入力履歴機能
以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("1002x712") の固定サイズで起動するためです。
解決法:geometry() と minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(1002, 712) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は全アプリ共通の定型課題です)。
-
課題1:メールアドレスを種別として見分ける
_classify()はコピーした文字列を「URL・コード・数字・テキスト」の4種類に分けています。メールアドレスは「テキスト」に入ってしまい、種別で絞り込めません。「メール」を足します。期待結果:
abc@example.comをコピーすると履歴の「種別」列が「メール」になり、種別で「メール」を選ぶとその行だけが残る。合格条件
https://example.comは今までどおり「URL」のまま- 「➕ 履歴に追加」で手動追加したメールアドレスにも同じ種別が付く
- 種別「すべて」を選ぶと、これまでどおり全件が表示される
ヒント①(どこを触るか): 触るのは
_classify()と_build_history_tab()の2か所です。_classify()は3つのifを上から順に試し、どれにも当たらなければ「テキスト」を返します。絞り込みのラジオボタンは_build_history_tab()の["すべて", "テキスト", "URL", "コード", "数字"]の並びです。
ヒント②(使うもの): 追加するifは最後のreturn "テキスト"の直前で構いません。判定はre.match(r"[^@\s]+@[^@\s]+\.[a-zA-Z]+$", text_s)のような簡単な形で十分です(import reは_classify()の先頭にあります)。
つまずきやすい点: 種別は履歴に追加した時点で決まり、clipboard_history.jsonに保存されます。すでにある履歴の種別は後から変わらないので、判定を足したあとにコピーし直して確かめてください。なお履歴は暗号化されずにclipboard_history.jsonにそのまま保存されます。テストにはこの例のようなダミーのアドレスを使い、実在のアドレスやパスワードはコピーしないでください。 -
課題2:ピン留めタブにも検索欄を付ける
検索と種別フィルターがあるのは履歴タブだけで、ピン留めタブは全件が並ぶだけです。ピン留めにも検索を足します。ただし絞り込むと、画面の行番号と
self._pinnedの番号がずれる点に注意してください。期待結果:ピン留めタブの検索欄に文字を入れると一致するピンだけが並び、その行を選んだときのプレビューが選んだピンの中身になる。
合格条件
- 絞り込んだ状態で「🗑 削除」を押すと、画面に出ている行が消える(別のピンは消えない)
- 「✏ ラベル変更」で付けた名前が、絞り込み中でも選んだ行に付く
- 検索欄を空にすると全件が戻る
ヒント①(どこを触るか): 検索欄を足すのは
_build_pinned_tab()、絞り込むのは_refresh_pinned_list()です。さらにself.pin_tree.index(sel[0])をそのままself._pinnedの番号として使っているのが_on_select_pin()・_copy_pinned()・_rename_pin()・_delete_pin()の4か所あり、ここを直さないと絞り込み中に別のピンを操作します。
ヒント②(使うもの): 履歴タブのself.search_varとtrace_add("write", ...)が手本です。_refresh_pinned_list()のinsert()にiid=str(元の番号)を渡し、各メソッドでint(sel[0])として受け取ると対応づけが崩れません。
つまずきやすい点: 履歴タブと同じself.search_varを使い回すと、履歴側の一覧まで一緒に絞り込まれます。ピン用は別の名前(例:self.pin_search_var)にしてください。削除の動作を試す前にclipboard_history.jsonをコピーして控えておくと、取り違えて消したときに戻せます。 -
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。