中級者向け No.45

DBクエリビルダー

SQLite向けにSQLを編集・実行できるクエリツール。構文ハイライト・スキーマ一覧・サンプルSQL挿入で手早く書けます。

🎯 難易度: ★★★ 📦 ライブラリ: tkinter(標準ライブラリ) ⏱️ 制作時間: 30〜90分

1. アプリ概要

SQLite向けにSQLを編集・実行できるクエリツール。構文ハイライト・スキーマ一覧・サンプルSQL挿入で手早く書けます。

このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★★ です。

このアプリは「データベース連携」カテゴリです。データベース連携 GUI は永続化と画面更新を結びつける典型例で、トランザクション・スキーマ設計・UI 反映の三点を一度に学べます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。

コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。

応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。

DBクエリビルダー 実行画面(Windows)
実行画面(Windows)
DBクエリビルダー 実行画面(Linux Mint)
実行画面(Linux Mint)

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. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

追加インストール不要(標準ライブラリのみ使用)
app45.py
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._connself._db_path)を初期化し、最後に _build_ui() で画面を組み立てます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

ウィジェット構成

画面は tk.Frame×10・tk.Label×4・ttk.Entryttk.Button×7・ttk.PanedWindow×2・ttk.Treeview×2・ttk.Scrollbar×5・ttk.Comboboxtk.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. 1
    ファイルを作成する

    新しいファイルを作成して app45.py と保存します。使うのは datetimeosresqlite3tkinter だけなので、追加インストールは不要です。

  2. 2
    クラスの骨格を作る

    App45 クラスを定義し、__init__ と、末尾の root = tk.Tk()mainloop() の最小構成を書いて、まず空のウィンドウが出ることを確認します。

  3. 3
    ウィンドウを設定する

    title("DBクエリビルダー")geometry("1060x1006")configure(bg="#1e1e1e")minsize(1060, 1006) をコンストラクタで設定します。

  4. 4
    画面部品を並べる

    _build_ui() の中で、tk.Frame×10・tk.Label×4・ttk.Entryttk.Button×7・ttk.PanedWindow×2・ttk.Treeview×2・ttk.Scrollbar×5・ttk.Comboboxtk.Text×4・ttk.Notebook を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。

  5. 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. 6
    中心になるメソッドを実装する

    アプリの本体である _execute()(41行)・_refresh_schema()(19行)・_on_schema_click()(15行)など を実装します。SQL の実行結果は execute() の戻り値から取り出します。

  7. 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行)。DELETEUPDATE は確認なしで実行され、commit() まで進みます(344-345行)。練習はこのメモリ DB だけで行い、「📂 開く」で大事な .db ファイルを開かないでください。課題1・課題2に追加のライブラリは要りません。

  1. 課題1:DELETE も確認なしで実行され、commit まで進む

    _execute(); で文を区切り、SELECTPRAGMA 以外をそのまま実行します(338-345行)。DELETE でも DROP でも、聞き返しはありません。commit() まで走るため、ファイルの DB を開いていればその場で書き戻されます。

    期待結果:起動した直後のまま、「サンプル:」から DELETE を選ぶ。エディタに DELETE FROM products WHERE id = 1; が入る。「▶ 実行 (F5)」を押すと、確認は出ずに下のステータスが ✅ 実行完了 に変わる。左のスキーマで products をダブルクリックすると ✅ 4 件取得 になり、リンゴ の行が消えている。直したあとは、アプリを開き直して同じ手順を試すと実行の前に確認が出て、「いいえ」のあとは ✅ 5 件取得 のまま変わらない。

    合格条件

    • SELECTPRAGMA 以外の文を実行する前に、確認のメッセージが出る
    • 「いいえ」を選んだときは1件も変わらない
    • SELECTPRAGMA は今までどおり確認なしで動く

    ヒント①(どこを触るか): 触るのは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. 課題2:「✨ フォーマット」を押すと、検索する文字まで大文字になって結果が消える

    _format_sql() は改行を入れる前に formatted = sql.upper() で全体を大文字にします(401行)。405行には「元の大小文字を保持(簡易版)」と書いてありますが、保持されていません。SQLite は引用符の中身を大文字と小文字で区別するので、結果まで変わります。

    期待結果:起動した直後のエディタには SELECT * FROM sqlite_master WHERE type='table'; が入っている(140行)。「▶ 実行 (F5)」を押すと productssqlite_sequencesales の3行が出て、下のステータスが ✅ 3 件取得 になる。次に「✨ フォーマット」を押すと sqlite_masterSQLITE_MASTERtype='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. 課題3:新しいボタンを追加する

    既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と command= の接続がセットで身に付きます。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のアプリにも挑戦してみましょう。

🐛
エラーが出て動かないときは

写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
次のレベルへ進む参考書

このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。