コードスニペットマネージャー
よく使うコードスニペットを言語別・タグ別に保存・検索・コピーできる管理ツール。SQLiteとクリップボード操作を学びます。
1. アプリ概要
よく使うコードスニペットを言語別・タグ別に保存・検索・コピーできる管理ツール。SQLiteとクリップボード操作を学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★☆ です。
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 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クラスにアプリの全機能をまとめています。__init__メソッドでウィンドウの基本設定を行い、_build_ui()でUI構築、process()でメイン処理を担当します。この分離により、各メソッドの責任が明確になりコードが読みやすくなります。
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()
LabelFrameによるセクション分け
ttk.LabelFrame を使うことで、入力エリアと結果エリアを視覚的に分けられます。padding引数でフレーム内の余白を設定し、見やすいレイアウトを実現しています。
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()
Entryウィジェットとイベントバインド
ttk.Entryで入力フィールドを作成します。bind('
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()
Textウィジェットでの結果表示
結果表示にはtk.Textウィジェットを使います。state=tk.DISABLEDでユーザーが直接編集できないようにし、表示前にNORMALに切り替えてからinsert()で内容を更新します。
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()
例外処理とmessagebox
try-except で ValueError と Exception を捕捉し、messagebox.showerror() でユーザーにわかりやすいエラーメッセージを表示します。入力バリデーションは必ず実装しましょう。
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()
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app49.py と保存します。
-
2クラスの骨格を作る
App49クラスを定義し、__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モジュールを使います。