DBクエリビルダー
SQLite向けにSQLを編集・実行できるクエリツール。構文ハイライト・スキーマ一覧・サンプルSQL挿入で手早く書けます。
1. アプリ概要
SQLite向けにSQLを編集・実行できるクエリツール。構文ハイライト・スキーマ一覧・サンプルSQL挿入で手早く書けます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★★ です。
このアプリは「データベース連携」カテゴリです。データベース連携 GUI は永続化と画面更新を結びつける典型例で、トランザクション・スキーマ設計・UI 反映の三点を一度に学べます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。
応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。
2. 機能一覧
- 「📂 開く」ボタン(
_open_db())・「💾 新規作成」ボタン(_new_db())・「🔌 接続」ボタン(_connect_db())・「🔄 スキーマ更新」ボタン(_refresh_schema())・「▶ 実行 (F5)」ボタン(_execute())・「✨ フォーマット」ボタン(_format_sql())・「🗑 クリア」ボタン(lambda: (self.editor.delete('1.0', tk.END), self._update_line_nums()))で操作 - マウス操作
<Double-1>から_on_schema_click()を実行 - イベント
<<ComboboxSelected>>から_load_sample()を実行 - キー
<KeyRelease>からlambda e: (self._update_line_nums(), self._highlight_sql())を実行 - キー
<F5>から_execute()を実行 ttk.Treeviewによる表形式の一覧表示sqlite3.connect()による SQLite データベース接続と SQL 実行filedialog.askopenfilename()によるファイル選択ダイアログfiledialog.asksaveasfilename()によるファイル選択ダイアログmessagebox.showerror()・messagebox.showwarning()によるダイアログ通知- ウィンドウはタイトル「DBクエリビルダー」・サイズ 1060x1006 で起動
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, filedialog
import sqlite3
import os
class App45:
"""DBクエリビルダー"""
SAMPLE_SQL = [
("全レコード取得", "SELECT * FROM {table} LIMIT 100;"),
("件数カウント", "SELECT COUNT(*) FROM {table};"),
("列を指定して取得", "SELECT {cols} FROM {table} WHERE 条件 LIMIT 50;"),
("グループ集計", "SELECT category, COUNT(*), SUM(amount) FROM {table} GROUP BY category ORDER BY 2 DESC;"),
("テーブル一覧", "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"),
("テーブル定義", "PRAGMA table_info({table});"),
("INSERT", "INSERT INTO {table} ({cols}) VALUES ({vals});"),
("UPDATE", "UPDATE {table} SET column = 'value' WHERE id = 1;"),
("DELETE", "DELETE FROM {table} WHERE id = 1;"),
]
def __init__(self, root):
self.root = root
self.root.title("DBクエリビルダー")
self.root.geometry("1060x1006")
self.root.minsize(1060, 1006)
self.root.configure(bg="#1e1e1e")
self._conn = None
self._db_path = None
self._history = []
self._build_ui()
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#252526", pady=6)
header.pack(fill=tk.X)
tk.Label(header, text="🔍 DBクエリビルダー",
font=("Noto Sans JP", 12, "bold"),
bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
# DB接続バー
conn_f = tk.Frame(header, bg="#252526")
conn_f.pack(side=tk.LEFT, padx=12)
self.db_path_var = tk.StringVar(value="新規メモリDB")
ttk.Entry(conn_f, textvariable=self.db_path_var,
width=36, font=("Arial", 10)).pack(side=tk.LEFT, padx=4)
ttk.Button(conn_f, text="📂 開く",
command=self._open_db).pack(side=tk.LEFT, padx=2)
ttk.Button(conn_f, text="💾 新規作成",
command=self._new_db).pack(side=tk.LEFT, padx=2)
ttk.Button(conn_f, text="🔌 接続",
command=self._connect_db).pack(side=tk.LEFT, padx=2)
# メインエリア
main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
main_paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左パネル: スキーマ
left = tk.Frame(main_paned, bg="#252526")
main_paned.add(left, weight=1)
self._build_schema_panel(left)
# 中央: エディタ + 結果
center = ttk.PanedWindow(main_paned, orient=tk.VERTICAL)
main_paned.add(center, weight=4)
self._build_editor_panel(center)
self._build_result_panel(center)
self.status_var = tk.StringVar(value="DBを開いてください")
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)
# 自動接続(メモリDB)
self._connect_memory()
def _build_schema_panel(self, parent):
tk.Label(parent, text="スキーマ", bg="#252526", fg="#858585",
font=("Arial", 9)).pack(anchor="w", padx=6, pady=2)
# テーブルツリー
self.schema_tree = ttk.Treeview(parent, show="tree", height=20)
sb = ttk.Scrollbar(parent, command=self.schema_tree.yview)
self.schema_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.schema_tree.pack(fill=tk.BOTH, expand=True, padx=4)
self.schema_tree.bind("<Double-1>", self._on_schema_click)
ttk.Button(parent, text="🔄 スキーマ更新",
command=self._refresh_schema).pack(pady=4)
def _build_editor_panel(self, parent):
editor_f = tk.Frame(parent, bg="#1e1e1e")
parent.add(editor_f, weight=2)
# ツールバー
bar = tk.Frame(editor_f, bg="#252526")
bar.pack(fill=tk.X)
ttk.Button(bar, text="▶ 実行 (F5)",
command=self._execute).pack(side=tk.LEFT, padx=4, pady=4)
ttk.Button(bar, text="✨ フォーマット",
command=self._format_sql).pack(side=tk.LEFT, padx=4)
ttk.Button(bar, text="🗑 クリア",
command=lambda: (
self.editor.delete("1.0", tk.END),
self._update_line_nums())).pack(side=tk.LEFT, padx=4)
tk.Label(bar, text="サンプル:", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 4))
self.sample_var = tk.StringVar()
sample_cb = ttk.Combobox(bar, textvariable=self.sample_var,
values=[s[0] for s in self.SAMPLE_SQL],
state="readonly", width=18)
sample_cb.pack(side=tk.LEFT)
sample_cb.bind("<<ComboboxSelected>>", self._load_sample)
# 行番号 + エディタ
num_f = tk.Frame(editor_f, bg="#1e1e1e")
num_f.pack(fill=tk.BOTH, expand=True)
self.line_nums = tk.Text(num_f, width=4, bg="#252526", fg="#858585",
font=("Courier New", 12), state=tk.DISABLED,
relief=tk.FLAT, padx=4, pady=4, takefocus=0)
self.line_nums.pack(side=tk.LEFT, fill=tk.Y)
self.editor = tk.Text(num_f, bg="#1e1e1e", fg="#d4d4d4",
font=("Courier New", 12), insertbackground="#aeafad",
relief=tk.FLAT, undo=True, padx=8, pady=4,
wrap=tk.NONE)
ed_h_sb = ttk.Scrollbar(num_f, orient=tk.HORIZONTAL,
command=self.editor.xview)
ed_v_sb = ttk.Scrollbar(num_f, orient=tk.VERTICAL,
command=self.editor.yview)
self.editor.configure(xscrollcommand=ed_h_sb.set,
yscrollcommand=ed_v_sb.set)
ed_v_sb.pack(side=tk.RIGHT, fill=tk.Y)
ed_h_sb.pack(side=tk.BOTTOM, fill=tk.X)
self.editor.pack(fill=tk.BOTH, expand=True)
self.editor.bind("<KeyRelease>", lambda e: (self._update_line_nums(),
self._highlight_sql()))
self.root.bind("<F5>", lambda e: self._execute())
self._setup_sql_tags()
self.editor.insert("1.0", "SELECT * FROM sqlite_master WHERE type='table';")
self._update_line_nums()
self._highlight_sql()
def _build_result_panel(self, parent):
result_f = tk.Frame(parent, bg="#1e1e1e")
parent.add(result_f, weight=3)
result_nb = ttk.Notebook(result_f)
result_nb.pack(fill=tk.BOTH, expand=True)
# 結果グリッドタブ
grid_tab = tk.Frame(result_nb, bg="#1e1e1e")
result_nb.add(grid_tab, text="結果")
self.result_tree = ttk.Treeview(grid_tab, show="headings")
h_sb = ttk.Scrollbar(grid_tab, orient=tk.HORIZONTAL,
command=self.result_tree.xview)
v_sb = ttk.Scrollbar(grid_tab, orient=tk.VERTICAL,
command=self.result_tree.yview)
self.result_tree.configure(xscrollcommand=h_sb.set,
yscrollcommand=v_sb.set)
v_sb.pack(side=tk.RIGHT, fill=tk.Y)
h_sb.pack(side=tk.BOTTOM, fill=tk.X)
self.result_tree.pack(fill=tk.BOTH, expand=True)
# 履歴タブ
hist_tab = tk.Frame(result_nb, bg="#1e1e1e")
result_nb.add(hist_tab, text="実行履歴")
self.history_text = tk.Text(hist_tab, bg="#0d1117", fg="#c9d1d9",
font=("Courier New", 10), state=tk.DISABLED)
self.history_text.pack(fill=tk.BOTH, expand=True)
self.history_text.tag_configure("success", foreground="#3fb950")
self.history_text.tag_configure("error", foreground="#f97583")
self.history_text.tag_configure("sql", foreground="#569cd6")
# エラータブ
err_tab = tk.Frame(result_nb, bg="#1e1e1e")
result_nb.add(err_tab, text="エラー")
self.error_text = tk.Text(err_tab, bg="#2b0f0f", fg="#f97583",
font=("Courier New", 11), state=tk.DISABLED)
self.error_text.pack(fill=tk.BOTH, expand=True)
def _setup_sql_tags(self):
keywords = {
"keyword": "#569cd6",
"string": "#ce9178",
"number": "#b5cea8",
"comment": "#6a9955",
"function": "#dcdcaa",
}
for tag, color in keywords.items():
self.editor.tag_configure(tag, foreground=color)
def _highlight_sql(self):
import re
content = self.editor.get("1.0", tk.END)
for tag in ["keyword", "string", "number", "comment", "function"]:
self.editor.tag_remove(tag, "1.0", tk.END)
SQL_KEYWORDS = (
r"\b(SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AND|OR|NOT|"
r"INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|INDEX|DROP|ALTER|"
r"ADD|COLUMN|PRIMARY|KEY|FOREIGN|REFERENCES|UNIQUE|NULL|NOT NULL|"
r"GROUP BY|ORDER BY|HAVING|LIMIT|OFFSET|AS|DISTINCT|IN|LIKE|"
r"BETWEEN|EXISTS|UNION|ALL|PRAGMA|IF|EXISTS|DEFAULT|INTEGER|TEXT|"
r"REAL|BLOB|AUTOINCREMENT|COALESCE|CASE|WHEN|THEN|ELSE|END)\b"
)
patterns = [
("keyword", SQL_KEYWORDS),
("string", r"'[^']*'"),
("number", r"\b\d+(?:\.\d+)?\b"),
("comment", r"--[^\n]*"),
("function", r"\b(COUNT|SUM|AVG|MAX|MIN|LENGTH|SUBSTR|TRIM|UPPER|LOWER|"
r"DATE|DATETIME|STRFTIME|REPLACE|ROUND|ABS|COALESCE)\b"),
]
for tag, pattern in patterns:
for m in re.finditer(pattern, content, re.IGNORECASE):
self.editor.tag_add(tag,
f"1.0+{m.start()}c",
f"1.0+{m.end()}c")
def _update_line_nums(self):
content = self.editor.get("1.0", tk.END)
lines = content.count("\n")
self.line_nums.config(state=tk.NORMAL)
self.line_nums.delete("1.0", tk.END)
self.line_nums.insert("1.0", "\n".join(str(i) for i in range(1, lines + 1)))
self.line_nums.config(state=tk.DISABLED)
# ── DB操作 ──────────────────────────────────────────────────
def _open_db(self):
path = filedialog.askopenfilename(
filetypes=[("SQLite DB", "*.db *.sqlite *.sqlite3"),
("すべて", "*.*")])
if path:
self.db_path_var.set(path)
self._connect_db()
def _new_db(self):
path = filedialog.asksaveasfilename(
defaultextension=".db",
filetypes=[("SQLite DB", "*.db"), ("すべて", "*.*")])
if path:
self.db_path_var.set(path)
self._connect_db()
def _connect_memory(self):
try:
self._conn = sqlite3.connect(":memory:")
# サンプルテーブル作成
self._conn.executescript("""
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, category TEXT, price REAL, stock INTEGER
);
INSERT INTO products (name, category, price, stock) VALUES
('リンゴ', '果物', 150, 50),
('バナナ', '果物', 80, 100),
('キャベツ', '野菜', 120, 30),
('トマト', '野菜', 200, 20),
('オレンジ', '果物', 180, 45);
CREATE TABLE sales (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER, quantity INTEGER,
sale_date TEXT, amount REAL
);
INSERT INTO sales (product_id, quantity, sale_date, amount) VALUES
(1, 5, '2025-04-01', 750),
(2, 10, '2025-04-01', 800),
(3, 3, '2025-04-02', 360),
(1, 2, '2025-04-03', 300);
""")
self.status_var.set("インメモリDBに接続 (サンプルデータあり)")
self._refresh_schema()
except Exception as e:
self.status_var.set(f"エラー: {e}")
def _connect_db(self):
path = self.db_path_var.get().strip()
if not path or path == "新規メモリDB":
self._connect_memory()
return
try:
if self._conn:
self._conn.close()
self._conn = sqlite3.connect(path)
self._db_path = path
self.status_var.set(f"接続: {path}")
self._refresh_schema()
except Exception as e:
messagebox.showerror("接続エラー", str(e))
def _refresh_schema(self):
if not self._conn:
return
self.schema_tree.delete(*self.schema_tree.get_children())
try:
tables = self._conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
).fetchall()
for (tbl,) in tables:
node = self.schema_tree.insert("", "end", text=f"📋 {tbl}", iid=tbl)
cols = self._conn.execute(f"PRAGMA table_info({tbl})").fetchall()
for col in cols:
cid, cname, ctype, notnull, dflt, pk = col
pk_str = " 🔑" if pk else ""
nn_str = " NOT NULL" if notnull else ""
self.schema_tree.insert(node, "end",
text=f" {cname} ({ctype}{nn_str}{pk_str})")
except Exception as e:
self.status_var.set(f"スキーマ取得エラー: {e}")
def _on_schema_click(self, event):
sel = self.schema_tree.selection()
if sel:
iid = sel[0]
# テーブルノードの場合
tables = [self.schema_tree.item(n)["text"].strip().lstrip("📋 ")
for n in self.schema_tree.get_children()]
tbl = self.schema_tree.item(iid)["text"].strip().lstrip("📋 ")
if tbl in tables:
sql = f"SELECT * FROM {tbl} LIMIT 50;"
self.editor.delete("1.0", tk.END)
self.editor.insert("1.0", sql)
self._update_line_nums()
self._highlight_sql()
self._execute()
def _execute(self):
if not self._conn:
messagebox.showwarning("警告", "DBに接続してください")
return
sql = self.editor.get("1.0", tk.END).strip()
if not sql:
return
try:
# 複数のステートメントがある場合は最後のSELECTを実行
statements = [s.strip() for s in sql.split(";") if s.strip()]
last_select = None
for stmt in statements:
if stmt.upper().startswith("SELECT") or stmt.upper().startswith("PRAGMA"):
last_select = stmt
else:
self._conn.execute(stmt)
self._conn.commit()
if last_select:
cursor = self._conn.execute(last_select)
rows = cursor.fetchall()
cols = [d[0] for d in cursor.description] if cursor.description else []
self._show_results(cols, rows)
self.status_var.set(f"✅ {len(rows)} 件取得")
self._add_history(last_select, True, f"{len(rows)} 件")
else:
self.status_var.set("✅ 実行完了")
self._add_history(sql, True, "実行完了")
self._refresh_schema()
# エラーテキストをクリア
self.error_text.config(state=tk.NORMAL)
self.error_text.delete("1.0", tk.END)
self.error_text.config(state=tk.DISABLED)
except Exception as e:
self.status_var.set(f"❌ エラー: {e}")
self._add_history(sql, False, str(e))
self.error_text.config(state=tk.NORMAL)
self.error_text.delete("1.0", tk.END)
self.error_text.insert("1.0", str(e))
self.error_text.config(state=tk.DISABLED)
def _show_results(self, cols, rows):
self.result_tree.delete(*self.result_tree.get_children())
if not cols:
return
self.result_tree["columns"] = cols
for col in cols:
self.result_tree.heading(col, text=col)
self.result_tree.column(col, width=max(80, len(col) * 10),
minwidth=40)
for row in rows:
self.result_tree.insert("", "end",
values=[str(v) if v is not None else "NULL"
for v in row])
def _add_history(self, sql, success, msg):
self.history_text.config(state=tk.NORMAL)
import datetime
ts = datetime.datetime.now().strftime("%H:%M:%S")
tag = "success" if success else "error"
self.history_text.insert("1.0", f"[{ts}] {'✅' if success else '❌'} {msg}\n", tag)
self.history_text.insert("1.0", "", "")
self.history_text.insert("2.0", f"{sql[:200]}\n\n", "sql")
self.history_text.config(state=tk.DISABLED)
def _format_sql(self):
sql = self.editor.get("1.0", tk.END).strip()
keywords = ["SELECT", "FROM", "WHERE", "AND", "OR", "JOIN", "LEFT",
"INNER", "GROUP BY", "ORDER BY", "HAVING", "LIMIT",
"INSERT INTO", "VALUES", "UPDATE", "SET", "DELETE FROM"]
import re
formatted = sql.upper()
for kw in sorted(keywords, key=len, reverse=True):
formatted = re.sub(r'\b' + kw + r'\b', "\n" + kw, formatted,
flags=re.IGNORECASE)
# 元の大小文字を保持(簡易版)
self.editor.delete("1.0", tk.END)
self.editor.insert("1.0", formatted.strip())
self._update_line_nums()
self._highlight_sql()
def _load_sample(self, event):
name = self.sample_var.get()
sample = next((s for s in self.SAMPLE_SQL if s[0] == name), None)
if sample:
sql = sample[1].replace("{table}", "products").replace(
"{cols}", "name, price").replace("{vals}", "'商品名', 1000")
self.editor.delete("1.0", tk.END)
self.editor.insert("1.0", sql)
self._update_line_nums()
self._highlight_sql()
if __name__ == "__main__":
root = tk.Tk()
app = App45(root)
root.mainloop()
5. コード解説
DBクエリビルダーのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App45 クラスにアプリの全機能をまとめています(メソッド19個・全426行)。__init__ ではタイトル「DBクエリビルダー」とウィンドウサイズ 1060x1006 を設定します。あわせて状態を保持する変数(self._conn・self._db_path)を初期化し、最後に _build_ui() で画面を組み立てます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×10・tk.Label×4・ttk.Entry・ttk.Button×7・ttk.PanedWindow×2・ttk.Treeview×2・ttk.Scrollbar×5・ttk.Combobox・tk.Text×4・ttk.Notebook で構成しています。PanedWindow を使っているので、ペインの境界をマウスでドラッグして幅を変えられます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「📂 開く」ボタン(_open_db())・「💾 新規作成」ボタン(_new_db())・「🔌 接続」ボタン(_connect_db())・「🔄 スキーマ更新」ボタン(_refresh_schema())・「▶ 実行 (F5)」ボタン(_execute())・「✨ フォーマット」ボタン(_format_sql())・「🗑 クリア」ボタン(lambda: (self.editor.delete('1.0', tk.END), self._update_line_nums()))を command= で接続しています。また bind("<Double-1>", ...) から _on_schema_click()、bind("<<ComboboxSelected>>", ...) から _load_sample()、bind("<KeyRelease>", ...) から lambda e: (self._update_line_nums(), self._highlight_sql())、bind("<F5>", ...) から _execute() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
SQLite データベースの接続と実行
sqlite3.connect() でデータベースファイルに接続し、SQL の実行は execute()、確定は commit() で行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception を捕捉しています。あわせて messagebox.showerror()・messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app45.py と保存します。使うのは
datetime・os・re・sqlite3・tkinterだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App45クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("DBクエリビルダー")・geometry("1060x1006")・configure(bg="#1e1e1e")・minsize(1060, 1006)をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×10・tk.Label×4・ttk.Entry・ttk.Button×7・ttk.PanedWindow×2・ttk.Treeview×2・ttk.Scrollbar×5・ttk.Combobox・tk.Text×4・ttk.Notebookを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_open_db()・_new_db()・_connect_db()・_refresh_schema()・_execute()・_format_sql()・lambda: (self.editor.delete('1.0', tk.END), self._update_line_nums()))につなぎます。bind("<Double-1>", ...)・bind("<<ComboboxSelected>>", ...)・bind("<KeyRelease>", ...)・bind("<F5>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_execute()(41行)・_refresh_schema()(19行)・_on_schema_click()(15行)など を実装します。SQL の実行結果はexecute()の戻り値から取り出します。 -
7動作確認する
python app45.pyで起動し、各ボタンが反応すること・データベースファイルを開いて内容が表示されること・キー操作(<KeyRelease>・<F5>)が効くこと・マウス操作(クリック・ホイールなど)が効くことを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 クラス定数を書き換えて動きを変える
データはクラス定数(SAMPLE_SQL)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。
💡 配色をまとめて管理する
背景色 #1e1e1e をはじめ、色コードが各所に直書きされています。色を1つの辞書にまとめると、テーマの切り替えが1か所の変更で済むようになります。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ 「database is locked」というエラーが出る
原因:同じデータベースファイルを別のツール(DB Browser など)で開いたまま書き込むと、SQLite がファイルをロックしてこのエラーになります。
解決法:他のツールでの接続を閉じてから実行してください。書き込みは commit() で確定するまで反映されない点にも注意しましょう。
❌ <KeyRelease> キーを押しても反応しない
原因:bind("<KeyRelease>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。
解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("1060x1006") の固定サイズで起動するためです。
解決法:geometry() と minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(1060, 1006) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。このアプリは SQL を自分で書くエディタです。左のスキーマでテーブル名をダブルクリックすると SELECT * FROM テーブル LIMIT 50; が入って実行されます(313-327行)。「サンプル:」からは9種類のひな形を差し込めます(10-20行・411-420行)。ドラッグ&ドロップで組み立てる作りではありません。起動直後はメモリ上のサンプル DB につながっていて、閉じると中身は消えます(74-75行・247-272行)。DELETE や UPDATE は確認なしで実行され、commit() まで進みます(344-345行)。練習はこのメモリ DB だけで行い、「📂 開く」で大事な .db ファイルを開かないでください。課題1・課題2に追加のライブラリは要りません。
-
課題1:DELETE も確認なしで実行され、commit まで進む
_execute()は;で文を区切り、SELECTとPRAGMA以外をそのまま実行します(338-345行)。DELETEでもDROPでも、聞き返しはありません。commit()まで走るため、ファイルの DB を開いていればその場で書き戻されます。期待結果:起動した直後のまま、「サンプル:」から
DELETEを選ぶ。エディタにDELETE FROM products WHERE id = 1;が入る。「▶ 実行 (F5)」を押すと、確認は出ずに下のステータスが✅ 実行完了に変わる。左のスキーマでproductsをダブルクリックすると✅ 4 件取得になり、リンゴの行が消えている。直したあとは、アプリを開き直して同じ手順を試すと実行の前に確認が出て、「いいえ」のあとは✅ 5 件取得のまま変わらない。合格条件
SELECTとPRAGMA以外の文を実行する前に、確認のメッセージが出る- 「いいえ」を選んだときは1件も変わらない
SELECTとPRAGMAは今までどおり確認なしで動く
ヒント①(どこを触るか): 触るのは1か所です。
_execute()のelse:の中、self._conn.execute(stmt)(344行)の手前。
ヒント②(使うもの): 判定の手本は 341行のstmt.upper().startswith(...)です。messagebox.askyesno("確認", "次の文を実行しますか?")を使い、Falseのときはreturnします(messageboxは2行で import 済み)。文の中身も見せたいならstmt[:200]をメッセージへ足します。
つまずきやすい点:_execute()は SELECT 以外を先に全部実行してから、最後の SELECT を表示します(340-351行)。途中でreturnしても、それより前の文はもうcommit()(345行)まで済んでいます。「📂 開く」でファイルの DB を開いているとき、commit()した変更をアプリから戻す方法はありません。 -
課題2:「✨ フォーマット」を押すと、検索する文字まで大文字になって結果が消える
_format_sql()は改行を入れる前にformatted = sql.upper()で全体を大文字にします(401行)。405行には「元の大小文字を保持(簡易版)」と書いてありますが、保持されていません。SQLite は引用符の中身を大文字と小文字で区別するので、結果まで変わります。期待結果:起動した直後のエディタには
SELECT * FROM sqlite_master WHERE type='table';が入っている(140行)。「▶ 実行 (F5)」を押すとproducts・sqlite_sequence・salesの3行が出て、下のステータスが✅ 3 件取得になる。次に「✨ フォーマット」を押すとsqlite_masterはSQLITE_MASTER、type='table'はTYPE='TABLE'へ変わる。そのまま「▶ 実行 (F5)」を押すと✅ 0 件取得になり、表から3行が消える。直したあとは、フォーマットしてもtype='table'が小文字のまま残り、✅ 3 件取得のままになる。合格条件
- 「✨ フォーマット」のあとも、引用符の中の
'table'が小文字のまま残る - キーワードの前で改行が入る動きは今までどおり
- フォーマットの前後で
✅ 3 件取得の件数が変わらない
ヒント①(どこを触るか): 触るのは1か所です。
_format_sql()のformatted = sql.upper()(401行)。
ヒント②(使うもの): 403-404行のre.sub()は、置き換える側に大文字のkwをそのまま使っています。つまりformatted = sqlに変えるだけで、キーワードだけが大文字になり、ほかは元のまま残ります。
つまずきやすい点: この置き換えは引用符の中を見分けません。'from tokyo'のように文字列の中へキーワードと同じ語を書くと、直したあとでもFROMへ置き換わって改行が入ります。ここで直るのは「キーワードでない語まで大文字になる」ほうだけです。なおsqlite_sequenceは、CREATE TABLE(252-255行・262-266行)のAUTOINCREMENTに合わせて SQLite が自動で作るテーブルです。 - 「✨ フォーマット」のあとも、引用符の中の
-
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。