コードスニペットマネージャー
よく使うコードスニペットを言語別・タグ別に保存・検索・コピーできる管理ツール。SQLiteとクリップボード操作を学びます。
1. アプリ概要
よく使うコードスニペットを言語別・タグ別に保存・検索・コピーできる管理ツール。SQLiteとクリップボード操作を学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★☆ です。
このアプリは「ユーティリティ」カテゴリです。小回りの効くユーティリティは Python の真骨頂で、汎用的なユーザー操作を組み合わせる発想が他のアプリにもそのまま流用できます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。
応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。
2. 機能一覧
- 「+ 新規」ボタン(
_new_snippet())・「💾 保存」ボタン(_save_snippet())・「🗑 削除」ボタン(_delete_snippet())・「📋 コードコピー」ボタン(_copy_code())・「☆ お気に入り」ボタン(_toggle_fav_filter())で操作 - イベント
<<TreeviewSelect>>から_on_select()を実行 - マウス操作
<Double-1>から_copy_code()を実行 - キー
<KeyRelease>から_on_code_change()を実行 ttk.Treeviewによる表形式の一覧表示sqlite3.connect()による SQLite データベース接続と SQL 実行messagebox.askyesno()・messagebox.showerror()によるダイアログ通知- ウィンドウはタイトル「コードスニペットマネージャー」・サイズ 1100x700 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
import os
import re
from datetime import datetime
class App49:
"""コードスニペットマネージャー"""
LANGUAGES = ["Python", "JavaScript", "TypeScript", "HTML", "CSS",
"SQL", "Bash", "Go", "Rust", "Java", "C", "C++",
"Ruby", "PHP", "その他"]
SAMPLE_SNIPPETS = [
("リスト内包表記", "Python", "comprehension,list",
"squares = [x**2 for x in range(10)]\neven = [x for x in range(20) if x % 2 == 0]"),
("fetch API", "JavaScript", "fetch,async,api",
"const res = await fetch('https://api.example.com/data');\n"
"const data = await res.json();\nconsole.log(data);"),
("flexbox中央揃え", "CSS", "flexbox,center,layout",
".container {\n display: flex;\n justify-content: center;\n align-items: center;\n}"),
("SELECT with JOIN", "SQL", "join,select,query",
"SELECT u.name, o.amount\nFROM users u\n"
"INNER JOIN orders o ON u.id = o.user_id\nWHERE o.amount > 1000;"),
("デコレーター", "Python", "decorator,functools",
"import functools\n\ndef timer(func):\n @functools.wraps(func)\n"
" def wrapper(*args, **kwargs):\n import time\n"
" start = time.time()\n result = func(*args, **kwargs)\n"
" print(f'{func.__name__}: {time.time()-start:.3f}s')\n"
" return result\n return wrapper"),
("Goのgoroutine", "Go", "goroutine,channel",
"ch := make(chan int)\ngo func() {\n ch <- 42\n}()\n"
"value := <-ch\nfmt.Println(value)"),
]
def __init__(self, root):
self.root = root
self.root.title("コードスニペットマネージャー")
self.root.geometry("1100x700")
self.root.configure(bg="#1e1e1e")
db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"snippets.db")
self._conn = sqlite3.connect(db_path)
self._init_db()
self._current_id = None
self._fav_only = False
self._build_ui()
self._load_list()
def _init_db(self):
self._conn.execute("""
CREATE TABLE IF NOT EXISTS snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
language TEXT DEFAULT '',
tags TEXT DEFAULT '',
code TEXT DEFAULT '',
note TEXT DEFAULT '',
created_at TEXT,
updated_at TEXT,
favorite INTEGER DEFAULT 0
)
""")
self._conn.commit()
count = self._conn.execute(
"SELECT COUNT(*) FROM snippets").fetchone()[0]
if count == 0:
now = datetime.now().isoformat(timespec="seconds")
for title, lang, tags, code in self.SAMPLE_SNIPPETS:
self._conn.execute(
"INSERT INTO snippets "
"(title,language,tags,code,created_at,updated_at)"
" VALUES (?,?,?,?,?,?)",
(title, lang, tags, code, now, now))
self._conn.commit()
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#252526", pady=6)
header.pack(fill=tk.X)
tk.Label(header, text="📋 コードスニペットマネージャー",
font=("Noto Sans JP", 12, "bold"),
bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
ttk.Button(header, text="+ 新規",
command=self._new_snippet).pack(side=tk.LEFT, padx=4)
ttk.Button(header, text="💾 保存",
command=self._save_snippet).pack(side=tk.LEFT, padx=4)
ttk.Button(header, text="🗑 削除",
command=self._delete_snippet).pack(side=tk.LEFT, padx=4)
ttk.Button(header, text="📋 コードコピー",
command=self._copy_code).pack(side=tk.LEFT, padx=4)
# 検索バー
search_f = tk.Frame(self.root, bg="#1e1e1e", pady=4)
search_f.pack(fill=tk.X, padx=8)
tk.Label(search_f, text="🔍", bg="#1e1e1e",
fg="#ccc").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *a: self._load_list())
ttk.Entry(search_f, textvariable=self.search_var,
width=28).pack(side=tk.LEFT, padx=4)
tk.Label(search_f, text="言語:", bg="#1e1e1e", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
self.lang_filter_var = tk.StringVar(value="すべて")
ttk.Combobox(search_f, textvariable=self.lang_filter_var,
values=["すべて"] + self.LANGUAGES,
state="readonly", width=12).pack(side=tk.LEFT)
self.lang_filter_var.trace_add("write", lambda *a: self._load_list())
self.fav_btn = ttk.Button(search_f, text="☆ お気に入り",
command=self._toggle_fav_filter)
self.fav_btn.pack(side=tk.LEFT, padx=8)
# メインエリア (PanedWindow)
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
left = tk.Frame(paned, bg="#1e1e1e")
paned.add(left, weight=1)
self._build_list_panel(left)
right = tk.Frame(paned, bg="#1e1e1e")
paned.add(right, weight=2)
self._build_edit_panel(right)
# ステータスバー
self.status_var = tk.StringVar(value="スニペットを選択してください")
tk.Label(self.root, textvariable=self.status_var,
bg="#252526", fg="#858585", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)
def _build_list_panel(self, parent):
tk.Label(parent, text="スニペット一覧", bg="#1e1e1e",
fg="#888", font=("Arial", 9)).pack(anchor="w", padx=4)
cols = ("title", "lang", "tags", "fav")
self.tree = ttk.Treeview(parent, columns=cols, show="headings",
selectmode="browse")
self.tree.heading("title", text="タイトル")
self.tree.heading("lang", text="言語")
self.tree.heading("tags", text="タグ")
self.tree.heading("fav", text="★")
self.tree.column("title", width=130, anchor="w")
self.tree.column("lang", width=80, anchor="w")
self.tree.column("tags", width=100, anchor="w")
self.tree.column("fav", width=28, anchor="center")
sb = ttk.Scrollbar(parent, command=self.tree.yview)
self.tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.pack(fill=tk.BOTH, expand=True)
self.tree.bind("<<TreeviewSelect>>", self._on_select)
self.tree.bind("<Double-1>", lambda e: self._copy_code())
self.count_lbl = tk.Label(parent, text="0 件", bg="#1e1e1e",
fg="#555", font=("Arial", 8))
self.count_lbl.pack(anchor="e", padx=4)
def _build_edit_panel(self, parent):
# フォーム
form = tk.Frame(parent, bg="#252526", pady=6)
form.pack(fill=tk.X, padx=4)
r0 = tk.Frame(form, bg="#252526")
r0.pack(fill=tk.X, padx=6, pady=2)
tk.Label(r0, text="タイトル:", bg="#252526", fg="#ccc",
font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
self.title_entry = ttk.Entry(r0)
self.title_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
self.fav_var = tk.IntVar()
tk.Checkbutton(r0, variable=self.fav_var, bg="#252526",
text="★ お気に入り", fg="#ffd700",
selectcolor="#252526",
activebackground="#252526",
font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
r1 = tk.Frame(form, bg="#252526")
r1.pack(fill=tk.X, padx=6, pady=2)
tk.Label(r1, text="言語:", bg="#252526", fg="#ccc",
font=("Arial", 9), width=8, anchor="e").pack(side=tk.LEFT)
self.lang_var = tk.StringVar(value="Python")
ttk.Combobox(r1, textvariable=self.lang_var,
values=self.LANGUAGES, width=14).pack(side=tk.LEFT, padx=4)
self.lang_var.trace_add("write", lambda *a: self._highlight_code())
tk.Label(r1, text="タグ(カンマ区切り):", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 2))
self.tags_entry = ttk.Entry(r1)
self.tags_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
# コードエディタ
editor_f = tk.Frame(parent, bg="#1e1e1e")
editor_f.pack(fill=tk.BOTH, expand=True, padx=4, pady=(4, 0))
tk.Label(editor_f, text="コード:", bg="#1e1e1e", fg="#888",
font=("Arial", 9)).pack(anchor="w")
code_area = tk.Frame(editor_f, bg="#1e1e1e")
code_area.pack(fill=tk.BOTH, expand=True)
self.line_canvas = tk.Canvas(code_area, width=36, bg="#0d1117",
highlightthickness=0)
self.line_canvas.pack(side=tk.LEFT, fill=tk.Y)
self.code_text = tk.Text(
code_area, bg="#0d1117", fg="#d4d4d4",
font=("Courier New", 10), relief=tk.FLAT,
insertbackground="#fff", selectbackground="#264f78",
undo=True, wrap=tk.NONE, tabs=("4m",))
ysb = ttk.Scrollbar(code_area, orient=tk.VERTICAL,
command=self.code_text.yview)
xsb = ttk.Scrollbar(editor_f, orient=tk.HORIZONTAL,
command=self.code_text.xview)
self.code_text.configure(xscrollcommand=xsb.set,
yscrollcommand=ysb.set)
ysb.pack(side=tk.RIGHT, fill=tk.Y)
self.code_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
xsb.pack(fill=tk.X)
self.code_text.bind("<KeyRelease>", self._on_code_change)
# メモ
note_f = tk.Frame(parent, bg="#1e1e1e")
note_f.pack(fill=tk.X, padx=4, pady=4)
tk.Label(note_f, text="メモ:", bg="#1e1e1e", fg="#888",
font=("Arial", 9)).pack(anchor="w")
self.note_text = tk.Text(note_f, height=3, bg="#1a1a2e", fg="#8b949e",
font=("Arial", 9), relief=tk.FLAT,
insertbackground="#fff")
self.note_text.pack(fill=tk.X)
# ── DB 操作 ───────────────────────────────────────────────────
def _load_list(self):
query = self.search_var.get().strip().lower()
lang_f = self.lang_filter_var.get()
sql = "SELECT id, title, language, tags, favorite FROM snippets WHERE 1=1"
params = []
if query:
sql += (" AND (LOWER(title) LIKE ? OR LOWER(tags) LIKE ?"
" OR LOWER(code) LIKE ?)")
params += [f"%{query}%"] * 3
if lang_f != "すべて":
sql += " AND language = ?"
params.append(lang_f)
if self._fav_only:
sql += " AND favorite = 1"
sql += " ORDER BY updated_at DESC"
rows = self._conn.execute(sql, params).fetchall()
self.tree.delete(*self.tree.get_children())
for sid, title, lang, tags, fav in rows:
self.tree.insert("", tk.END, iid=str(sid),
values=(title, lang, tags,
"★" if fav else ""))
self.count_lbl.config(text=f"{len(rows)} 件")
def _on_select(self, event=None):
sel = self.tree.selection()
if not sel:
return
sid = int(sel[0])
row = self._conn.execute(
"SELECT id,title,language,tags,code,note,favorite"
" FROM snippets WHERE id=?", (sid,)).fetchone()
if not row:
return
self._current_id = row[0]
self.title_entry.delete(0, tk.END)
self.title_entry.insert(0, row[1])
self.lang_var.set(row[2])
self.tags_entry.delete(0, tk.END)
self.tags_entry.insert(0, row[3])
self.code_text.delete("1.0", tk.END)
self.code_text.insert(tk.END, row[4])
self.note_text.delete("1.0", tk.END)
self.note_text.insert(tk.END, row[5] or "")
self.fav_var.set(row[6])
self._update_line_numbers()
self._highlight_code()
self.status_var.set(f"ID={sid} {row[1]} [{row[2]}]")
def _new_snippet(self):
self._current_id = None
self.title_entry.delete(0, tk.END)
self.lang_var.set("Python")
self.tags_entry.delete(0, tk.END)
self.code_text.delete("1.0", tk.END)
self.note_text.delete("1.0", tk.END)
self.fav_var.set(0)
self._update_line_numbers()
self.status_var.set("新規スニペット")
self.title_entry.focus_set()
def _save_snippet(self):
title = self.title_entry.get().strip()
if not title:
messagebox.showerror("エラー", "タイトルを入力してください")
return
lang = self.lang_var.get()
tags = self.tags_entry.get().strip()
code = self.code_text.get("1.0", tk.END).rstrip("\n")
note = self.note_text.get("1.0", tk.END).rstrip("\n")
fav = self.fav_var.get()
now = datetime.now().isoformat(timespec="seconds")
if self._current_id is None:
cur = self._conn.execute(
"INSERT INTO snippets "
"(title,language,tags,code,note,favorite,created_at,updated_at)"
" VALUES (?,?,?,?,?,?,?,?)",
(title, lang, tags, code, note, fav, now, now))
self._current_id = cur.lastrowid
self._conn.commit()
self.status_var.set(f"新規保存: {title}")
else:
self._conn.execute(
"UPDATE snippets SET title=?,language=?,tags=?,code=?,"
"note=?,favorite=?,updated_at=? WHERE id=?",
(title, lang, tags, code, note, fav, now, self._current_id))
self._conn.commit()
self.status_var.set(f"更新: {title}")
self._load_list()
try:
self.tree.selection_set(str(self._current_id))
except Exception:
pass
def _delete_snippet(self):
if self._current_id is None:
return
title = self.title_entry.get().strip() or f"ID={self._current_id}"
if not messagebox.askyesno("削除確認", f"「{title}」を削除しますか?"):
return
self._conn.execute("DELETE FROM snippets WHERE id=?",
(self._current_id,))
self._conn.commit()
self._current_id = None
self._new_snippet()
self._load_list()
self.status_var.set("削除しました")
def _copy_code(self):
code = self.code_text.get("1.0", tk.END).strip()
if not code:
return
self.root.clipboard_clear()
self.root.clipboard_append(code)
self.status_var.set("コードをクリップボードにコピーしました")
def _toggle_fav_filter(self):
self._fav_only = not self._fav_only
self.fav_btn.config(text="★ お気に入り" if self._fav_only else "☆ お気に入り")
self._load_list()
# ── エディタ支援 ──────────────────────────────────────────────
def _on_code_change(self, event=None):
self._update_line_numbers()
self._highlight_code()
def _update_line_numbers(self):
self.line_canvas.delete("all")
try:
line_num = int(self.code_text.index("@0,0").split(".")[0])
while True:
dline = self.code_text.dlineinfo(f"{line_num}.0")
if dline is None:
break
self.line_canvas.create_text(
32, dline[1] + dline[3] // 2,
text=str(line_num), anchor="e",
fill="#858585", font=("Courier New", 10))
line_num += 1
except Exception:
pass
def _highlight_code(self):
lang = self.lang_var.get()
for tag in ("kw", "str_", "cmt", "num", "bi"):
self.code_text.tag_remove(tag, "1.0", tk.END)
self.code_text.tag_configure("kw", foreground="#569cd6")
self.code_text.tag_configure("str_", foreground="#ce9178")
self.code_text.tag_configure("cmt", foreground="#6a9955")
self.code_text.tag_configure("num", foreground="#b5cea8")
self.code_text.tag_configure("bi", foreground="#4ec9b0")
content = self.code_text.get("1.0", tk.END)
if lang == "Python":
kw_pat = (r"\b(def|class|import|from|return|if|elif|else|for|"
r"while|try|except|finally|with|as|pass|break|continue|"
r"in|not|and|or|is|None|True|False|lambda|yield|async|"
r"await|raise|del|global|nonlocal)\b")
bi_pat = (r"\b(print|len|range|type|str|int|float|list|dict|set|"
r"tuple|bool|open|sum|max|min|sorted|enumerate|zip|map|"
r"filter|super|self)\b")
str_pat = r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"[^"\n]*"|\'[^\'\n]*\')'
cmt_pat = r"#[^\n]*"
elif lang in ("JavaScript", "TypeScript"):
kw_pat = (r"\b(const|let|var|function|return|if|else|for|while|"
r"class|new|import|export|from|await|async|try|catch|"
r"finally|typeof|instanceof|of|in|true|false|null|"
r"undefined|this|super)\b")
bi_pat = (r"\b(console|document|window|Array|Object|Promise|fetch|"
r"JSON|Math|Date|Error|parseInt|parseFloat|String|"
r"Boolean|Number|Map|Set)\b")
str_pat = r'(`[^`]*`|"[^"\n]*"|\'[^\'\n]*\')'
cmt_pat = r"//[^\n]*"
elif lang == "SQL":
kw_pat = (r"\b(SELECT|FROM|WHERE|JOIN|INNER|LEFT|RIGHT|OUTER|ON|"
r"GROUP|BY|ORDER|HAVING|INSERT|UPDATE|DELETE|CREATE|"
r"TABLE|DROP|ALTER|AS|AND|OR|NOT|IN|LIKE|BETWEEN|"
r"EXISTS|DISTINCT|LIMIT|OFFSET|SET|VALUES|INTO)\b")
bi_pat = (r"\b(COUNT|SUM|AVG|MAX|MIN|COALESCE|NULLIF|CASE|WHEN|"
r"THEN|ELSE|END|NOW|DATE|CAST|CONVERT)\b")
str_pat = r"'[^']*'"
cmt_pat = r"--[^\n]*"
else:
kw_pat = None
bi_pat = None
str_pat = r'"[^"\n]*"|\'[^\'\n]*\''
cmt_pat = r"//[^\n]*|#[^\n]*"
flags = re.IGNORECASE if lang == "SQL" else 0
def apply(pattern, tag):
if not pattern:
return
for m in re.finditer(pattern, content, flags):
s = f"1.0 + {m.start()} chars"
e = f"1.0 + {m.end()} chars"
self.code_text.tag_add(tag, s, e)
apply(r"\b\d+(\.\d+)?\b", "num")
apply(str_pat, "str_")
apply(cmt_pat, "cmt")
apply(bi_pat, "bi")
apply(kw_pat, "kw")
if __name__ == "__main__":
root = tk.Tk()
app = App49(root)
root.mainloop()
5. コード解説
コードスニペットマネージャーのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App49 クラスにアプリの全機能をまとめています(メソッド15個・全449行)。__init__ ではタイトル「コードスニペットマネージャー」とウィンドウサイズ 1100x700 を設定します。あわせて状態を保持する変数(self._current_id・self._fav_only)を初期化し、そのあと _init_db() → _build_ui() → _load_list() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×10・tk.Label×11・ttk.Button×5・ttk.Entry×3・ttk.Combobox×2・ttk.PanedWindow・ttk.Treeview・ttk.Scrollbar×3・tk.Checkbutton・tk.Canvas・tk.Text×2 で構成しています。PanedWindow を使っているので、ペインの境界をマウスでドラッグして幅を変えられます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「+ 新規」ボタン(_new_snippet())・「💾 保存」ボタン(_save_snippet())・「🗑 削除」ボタン(_delete_snippet())・「📋 コードコピー」ボタン(_copy_code())・「☆ お気に入り」ボタン(_toggle_fav_filter())を command= で接続しています。また bind("<<TreeviewSelect>>", ...) から _on_select()、bind("<Double-1>", ...) から _copy_code()、bind("<KeyRelease>", ...) から _on_code_change() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
SQLite データベースの接続と実行
sqlite3.connect() でデータベースファイルに接続し、SQL の実行は execute()、確定は commit() で行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception を捕捉しています。あわせて messagebox.askyesno()・messagebox.showerror() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app49.py と保存します。使うのは
datetime・os・re・sqlite3・tkinterだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App49クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("コードスニペットマネージャー")・geometry("1100x700")・configure(bg="#1e1e1e")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×10・tk.Label×11・ttk.Button×5・ttk.Entry×3・ttk.Combobox×2・ttk.PanedWindow・ttk.Treeview・ttk.Scrollbar×3・tk.Checkbutton・tk.Canvas・tk.Text×2 を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_new_snippet()・_save_snippet()・_delete_snippet()・_copy_code()・_toggle_fav_filter())につなぎます。bind("<<TreeviewSelect>>", ...)・bind("<Double-1>", ...)・bind("<KeyRelease>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_save_snippet()(34行)・_on_select()(24行)・_delete_snippet()(13行)など を実装します。SQL の実行結果はexecute()の戻り値から取り出します。 -
7動作確認する
python app49.pyで起動し、各ボタンが反応すること・データベースファイルを開いて内容が表示されること・キー操作(<KeyRelease>)が効くこと・マウス操作(クリック・ホイールなど)が効くことを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 クラス定数を書き換えて動きを変える
データはクラス定数(LANGUAGES・SAMPLE_SNIPPETS)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。
💡 配色をまとめて管理する
背景色 #1e1e1e をはじめ、色コードが各所に直書きされています。色を1つの辞書にまとめると、テーマの切り替えが1か所の変更で済むようになります。
💡 マウスホイールでも行番号を追従させる
行番号は編集操作のタイミングでしか再描画されません。code_text.bind("<MouseWheel>", ...) を追加し、root.after(10, self._update_line_numbers) を呼ぶと、スクロール操作でも行番号が追従します。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ 「database is locked」というエラーが出る
原因:同じデータベースファイルを別のツール(DB Browser など)で開いたまま書き込むと、SQLite がファイルをロックしてこのエラーになります。
解決法:他のツールでの接続を閉じてから実行してください。書き込みは commit() で確定するまで反映されない点にも注意しましょう。
❌ <KeyRelease> キーを押しても反応しない
原因:bind("<KeyRelease>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。
解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("1100x700") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
❌ スクロールすると行番号が本文とズレる
原因:行番号を描画する _update_line_numbers() は <KeyRelease> など編集操作のタイミングでしか呼ばれません。マウスホイールでコード欄をスクロールしても行番号側は再描画されないため、本文とずれて見えることがあります。
解決法:code_text.bind("<MouseWheel>", lambda e: self.root.after(10, self._update_line_numbers)) のように、ホイール操作にも行番号の再描画をひもづけましょう。少し遅らせて呼ぶことで、スクロール後の表示位置に行番号を合わせられます。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。どちらの課題も、初回起動で入るサンプル6件だけで確かめられます。スニペットはコードと同じフォルダの snippets.db に保存され、「🗑 削除」で消した行は元に戻せません。追加のライブラリは要りません。
-
課題1:検索欄に入れた記号を文字として扱う
検索は SQL の
LIKEで組み立てています。%は「何文字でも」、_は「任意の1文字」という意味を持つため、そのまま入れると全件に一致します。記号もふつうの文字として検索してください。期待結果:初回起動でサンプル6件が入った状態にする。検索欄に
%を1文字入れても件数は6 件のままで、まったく絞り込めない。直したあとは1 件になり、コードにx % 2 == 0を含む「リスト内包表記」だけが残る。_を入れたときは「SELECT with JOIN」と「デコレーター」の2 件になる。合格条件
listのような普通の語での検索結果は、これまでと変わらない- 言語フィルタと「☆ お気に入り」の絞り込みは、これまでどおり効く
- 検索欄を空に戻すと全件表示に戻る
ヒント①(どこを触るか): 触るのは
_load_list()のif query:の中1か所です。SQL文とparamsを組み立てているところです。
ヒント②(使うもの): SQLite のLIKEはESCAPE句で逃がし文字を決められます。3つのLIKE ?すべてにESCAPEを付けたうえで、query側は%と_の前に逃がし文字を差し込んでから渡します。
つまずきやすい点: 逃がし文字そのものも先に二重にしないと、その記号を検索したときに壊れます。ESCAPEに渡せるのは1文字だけで、SQL に2文字が届くとsqlite3.OperationalError: ESCAPE expression must be a single characterが出ます。パラメータはparams += [f"%{query}%"] * 3と同じ値を3つ使い回しているので、逃がす処理は1回で足ります。 -
課題2:絞り込み中に保存すると一覧から消えたように見える
「☆ お気に入り」で絞り込んだまま新規保存すると、保存はできているのに一覧には出ません。選択も外れたままです。保存したものがどうなったか分かるようにしてください。
期待結果:「☆ お気に入り」ボタンを1回押す(表示が
★ お気に入りに変わり、件数は0 件になる)。「+ 新規」を押してタイトルにテストと入れ、「💾 保存」を押す。ステータスは新規保存: テストになるのに、一覧は0 件のままで行が増えない。直したあとは、いまの絞り込みでは表示されないことが画面から分かる。合格条件
- 絞り込んでいないときは、これまでどおり保存した行が選択された状態になる
- 「★ お気に入り」にチェックを入れて保存したときは、絞り込み中でも一覧に残る
- 保存したデータ自体は消えない(絞り込みを外すと出てくる)
ヒント①(どこを触るか): 触るのは
_save_snippet()の末尾1か所です。self._load_list()の下にあるtry:とexcept Exception:のところです。
ヒント②(使うもの):self.tree.exists(str(self._current_id))で、その行が一覧にあるかを確かめられます。無いときはself.status_var.set(...)で知らせます。いまの絞り込み状態はself._fav_onlyと検索欄のself.search_varで分かります。
つまずきやすい点: いまはselection_set()が失敗してもexcept Exception:のpassが握りつぶすので、画面には何も出ません。検索欄に文字が入っているときも同じことが起きるため、条件をお気に入りだけに絞ると取りこぼします。判定はself._load_list()のあとに置いてください(前に置くと、作り直す前の一覧を見ることになります)。 -
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。