中級者向け No.03

SQLiteデータベースブラウザ

SQLiteデータベースを開いてテーブル一覧・カラム情報・レコードを表示し、任意のSQL文を実行できるGUIブラウザ。tkinter と sqlite3 だけで実現する本格的なDBツールです。

🎯 難易度: ★★☆ 普通 📦 ライブラリ: tkinter, sqlite3, os(すべて標準ライブラリ) ⏱️ 制作時間: 60〜120分

1. アプリ概要

このアプリは Python 標準ライブラリの tkintersqlite3 だけを使って実装した SQLiteデータベースブラウザです。既存の .db / .sqlite ファイルを開くか、新規ファイルを作成して接続し、テーブル一覧・カラム情報の確認や任意の SQL 文の実行を行えます。

画面は 3 つのエリアに分かれています。上段の左ペインにはデータベース内のテーブル一覧が表示され、テーブルを選択すると右ペインにカラム情報(カラム名・型・NOT NULL制約・主キー)が自動表示されます。中段はSQL 入力エリアで、テキストボックスに SQL 文を書いて「実行」ボタンまたは F5 キーで実行します。下段の結果グリッド(ttk.Treeview)には SELECT 結果が表示され、INSERT / UPDATE / DELETE などの更新系クエリは実行完了後に影響行数を表示します。

使用ライブラリはすべて標準ライブラリです(tkinter, sqlite3, os)。外部パッケージのインストールは不要で、Python をインストールすればすぐに動かせます。難易度は ★★☆ 普通 で、PanedWindow・Treeview・filedialog など実務的なウィジェットの使い方を学べます。

ソースコードは完全な動作状態で提供しています。コピーしてそのまま実行し、動作を確認したらコードを読んで仕組みを理解しましょう。カスタマイズセクションにはテーブル編集機能やエクスポート機能など発展的なアイデアを掲載しています。

2. 機能一覧

  • 既存の SQLite ファイル(.db / .sqlite / .sqlite3)を開いて接続
  • 新規 SQLite データベースファイルの作成・接続
  • テーブル一覧の表示(Listbox で選択可能)
  • 選択テーブルのカラム情報を自動表示(カラム名・型・NOT NULL・主キー)
  • テーブル選択時に SELECT 文を自動セットして即実行
  • テキストエリアで任意の SQL 文を入力・実行(SELECT / INSERT / UPDATE / DELETE / CREATE TABLE など)
  • F5 キーによるキーボードショートカット実行
  • クエリ結果を Treeview で行・列表示(水平・垂直スクロール対応)
  • 更新系クエリ実行後のテーブル一覧自動更新
  • ステータスバーに取得行数・影響行数を表示
  • SQL エラーをダイアログで分かりやすく通知

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows・Mac・Linux すべて対応

以下の環境で動作確認しています。

  • Python 3.10 以上
  • OS: Windows 10/11・macOS 12+・Ubuntu 20.04+

使用するライブラリはすべて Python 標準ライブラリです。pip install は不要です。

  • tkinter — GUI ウィジェット(標準ライブラリ)
  • sqlite3 — SQLite データベース操作(標準ライブラリ)
  • os — ファイルパス操作(標準ライブラリ)

4. 完全なソースコード

💡
コードのコピー方法

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

追加インストール不要(標準ライブラリのみ使用)
app003.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os


class App03:
    """SQLiteデータベースブラウザ"""

    def __init__(self, root):
        self.root = root
        self.root.title("SQLiteデータベースブラウザ")
        self.root.geometry("900x600")
        self.root.configure(bg="#f8f9fc")
        self.conn = None
        self.db_path = None
        self._build_ui()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#3776ab", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🗄️ SQLiteデータベースブラウザ",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#3776ab", fg="white").pack(side=tk.LEFT, padx=12)
        ttk.Button(title_frame, text="📂 DBを開く",
                   command=self._open_db).pack(side=tk.RIGHT, padx=12)
        ttk.Button(title_frame, text="✨ 新規DB",
                   command=self._new_db).pack(side=tk.RIGHT, padx=4)

        # 上段: テーブル一覧 + カラム情報
        top = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        top.pack(fill=tk.BOTH, expand=False, padx=4, pady=4)

        # テーブル一覧
        tbl_frame = ttk.LabelFrame(top, text="テーブル一覧", padding=4)
        self.tbl_list = tk.Listbox(tbl_frame, font=("Courier New", 11),
                                    width=20, exportselection=False)
        self.tbl_list.pack(fill=tk.BOTH, expand=True)
        self.tbl_list.bind("<<ListboxSelect>>", self._on_table_select)
        top.add(tbl_frame, weight=1)

        # カラム情報
        col_frame = ttk.LabelFrame(top, text="カラム情報", padding=4)
        cols = ("cid", "name", "type", "notnull", "pk")
        self.col_tree = ttk.Treeview(col_frame, columns=cols,
                                      show="headings", height=6)
        for c, h, w in [("cid", "#", 30), ("name", "名前", 120),
                         ("type", "型", 80), ("notnull", "NOT NULL", 70),
                         ("pk", "PK", 40)]:
            self.col_tree.heading(c, text=h)
            self.col_tree.column(c, width=w, minwidth=30)
        self.col_tree.pack(fill=tk.BOTH, expand=True)
        top.add(col_frame, weight=2)

        # SQL入力エリア
        sql_frame = ttk.LabelFrame(self.root, text="SQL実行", padding=6)
        sql_frame.pack(fill=tk.X, padx=4, pady=(0, 4))
        self.sql_text = tk.Text(sql_frame, height=3, font=("Courier New", 11),
                                bg="white")
        self.sql_text.pack(fill=tk.X, expand=True, side=tk.LEFT)
        self.sql_text.insert("1.0", "SELECT * FROM sqlite_master WHERE type='table';")
        btn_frame = tk.Frame(sql_frame, bg=sql_frame.cget("background"))
        btn_frame.pack(side=tk.RIGHT, padx=6)
        ttk.Button(btn_frame, text="▶ 実行\n(F5)",
                   command=self._run_sql).pack(pady=2)
        ttk.Button(btn_frame, text="クリア",
                   command=lambda: self.sql_text.delete("1.0", tk.END)).pack()
        self.root.bind("<F5>", lambda e: self._run_sql())

        # 結果グリッド
        result_frame = ttk.LabelFrame(self.root, text="結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=4, pady=(0, 4))
        self.result_tree = ttk.Treeview(result_frame, show="headings")
        h_sb = ttk.Scrollbar(result_frame, orient=tk.HORIZONTAL,
                             command=self.result_tree.xview)
        v_sb = ttk.Scrollbar(result_frame, 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)

        self.status_var = tk.StringVar(value="DBファイルを開いてください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _open_db(self):
        path = filedialog.askopenfilename(
            title="SQLiteファイルを選択",
            filetypes=[("SQLiteファイル", "*.db *.sqlite *.sqlite3"),
                       ("すべて", "*.*")])
        if path:
            self._connect(path)

    def _new_db(self):
        path = filedialog.asksaveasfilename(
            title="新規DBファイル",
            defaultextension=".db",
            filetypes=[("SQLiteファイル", "*.db"), ("すべて", "*.*")])
        if path:
            self._connect(path)

    def _connect(self, path):
        if self.conn:
            self.conn.close()
        try:
            self.conn = sqlite3.connect(path)
            self.db_path = path
            self.root.title(f"SQLiteブラウザ — {os.path.basename(path)}")
            self.status_var.set(f"接続済み: {path}")
            self._load_tables()
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _load_tables(self):
        if not self.conn:
            return
        self.tbl_list.delete(0, tk.END)
        cur = self.conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
        for row in cur.fetchall():
            self.tbl_list.insert(tk.END, row[0])

    def _on_table_select(self, event):
        sel = self.tbl_list.curselection()
        if not sel:
            return
        table = self.tbl_list.get(sel[0])
        # カラム情報
        self.col_tree.delete(*self.col_tree.get_children())
        for row in self.conn.execute(f"PRAGMA table_info({table})"):
            self.col_tree.insert("", "end", values=row[:5])
        # SQLにSELECTをセット
        self.sql_text.delete("1.0", tk.END)
        self.sql_text.insert("1.0", f"SELECT * FROM {table} LIMIT 100;")
        self._run_sql()

    def _run_sql(self):
        if not self.conn:
            messagebox.showwarning("警告", "DBを開いてください")
            return
        sql = self.sql_text.get("1.0", tk.END).strip()
        if not sql:
            return
        try:
            cur = self.conn.execute(sql)
            self.conn.commit()
            # 結果をTreeviewに表示
            self.result_tree.delete(*self.result_tree.get_children())
            if cur.description:
                cols = [d[0] for d in cur.description]
                self.result_tree.configure(columns=cols)
                for c in cols:
                    self.result_tree.heading(c, text=c)
                    self.result_tree.column(c, width=max(80, len(c)*10), minwidth=40)
                rows = cur.fetchall()
                for row in rows:
                    self.result_tree.insert("", "end",
                                            values=[str(v) if v is not None else "NULL"
                                                    for v in row])
                self.status_var.set(f"{len(rows)} 行取得")
            else:
                self.result_tree.configure(columns=("result",))
                self.result_tree.heading("result", text="結果")
                self.result_tree.insert("", "end", values=("OK",))
                self.status_var.set(f"実行完了 ({cur.rowcount} 行影響)")
                self._load_tables()
        except Exception as e:
            messagebox.showerror("SQLエラー", str(e))


if __name__ == "__main__":
    root = tk.Tk()
    app = App03(root)
    root.mainloop()

5. コード解説

SQLiteデータベースブラウザのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。

クラス設計とコンストラクタ(__init__)

App03 クラスにアプリの全機能をまとめています。__init__ でウィンドウサイズ・背景色の設定と、接続状態を保持するインスタンス変数(self.connself.db_path)の初期化を行い、最後に _build_ui() を呼んで UI を構築します。self.conn は SQLite 接続オブジェクトで、未接続時は None になります。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os


class App03:
    """SQLiteデータベースブラウザ"""

    def __init__(self, root):
        self.root = root
        self.root.title("SQLiteデータベースブラウザ")
        self.root.geometry("900x600")
        self.root.configure(bg="#f8f9fc")
        self.conn = None
        self.db_path = None
        self._build_ui()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#3776ab", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🗄️ SQLiteデータベースブラウザ",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#3776ab", fg="white").pack(side=tk.LEFT, padx=12)
        ttk.Button(title_frame, text="📂 DBを開く",
                   command=self._open_db).pack(side=tk.RIGHT, padx=12)
        ttk.Button(title_frame, text="✨ 新規DB",
                   command=self._new_db).pack(side=tk.RIGHT, padx=4)

        # 上段: テーブル一覧 + カラム情報
        top = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        top.pack(fill=tk.BOTH, expand=False, padx=4, pady=4)

        # テーブル一覧
        tbl_frame = ttk.LabelFrame(top, text="テーブル一覧", padding=4)
        self.tbl_list = tk.Listbox(tbl_frame, font=("Courier New", 11),
                                    width=20, exportselection=False)
        self.tbl_list.pack(fill=tk.BOTH, expand=True)
        self.tbl_list.bind("<<ListboxSelect>>", self._on_table_select)
        top.add(tbl_frame, weight=1)

        # カラム情報
        col_frame = ttk.LabelFrame(top, text="カラム情報", padding=4)
        cols = ("cid", "name", "type", "notnull", "pk")
        self.col_tree = ttk.Treeview(col_frame, columns=cols,
                                      show="headings", height=6)
        for c, h, w in [("cid", "#", 30), ("name", "名前", 120),
                         ("type", "型", 80), ("notnull", "NOT NULL", 70),
                         ("pk", "PK", 40)]:
            self.col_tree.heading(c, text=h)
            self.col_tree.column(c, width=w, minwidth=30)
        self.col_tree.pack(fill=tk.BOTH, expand=True)
        top.add(col_frame, weight=2)

        # SQL入力エリア
        sql_frame = ttk.LabelFrame(self.root, text="SQL実行", padding=6)
        sql_frame.pack(fill=tk.X, padx=4, pady=(0, 4))
        self.sql_text = tk.Text(sql_frame, height=3, font=("Courier New", 11),
                                bg="white")
        self.sql_text.pack(fill=tk.X, expand=True, side=tk.LEFT)
        self.sql_text.insert("1.0", "SELECT * FROM sqlite_master WHERE type='table';")
        btn_frame = tk.Frame(sql_frame, bg=sql_frame.cget("background"))
        btn_frame.pack(side=tk.RIGHT, padx=6)
        ttk.Button(btn_frame, text="▶ 実行\n(F5)",
                   command=self._run_sql).pack(pady=2)
        ttk.Button(btn_frame, text="クリア",
                   command=lambda: self.sql_text.delete("1.0", tk.END)).pack()
        self.root.bind("<F5>", lambda e: self._run_sql())

        # 結果グリッド
        result_frame = ttk.LabelFrame(self.root, text="結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=4, pady=(0, 4))
        self.result_tree = ttk.Treeview(result_frame, show="headings")
        h_sb = ttk.Scrollbar(result_frame, orient=tk.HORIZONTAL,
                             command=self.result_tree.xview)
        v_sb = ttk.Scrollbar(result_frame, 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)

        self.status_var = tk.StringVar(value="DBファイルを開いてください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _open_db(self):
        path = filedialog.askopenfilename(
            title="SQLiteファイルを選択",
            filetypes=[("SQLiteファイル", "*.db *.sqlite *.sqlite3"),
                       ("すべて", "*.*")])
        if path:
            self._connect(path)

    def _new_db(self):
        path = filedialog.asksaveasfilename(
            title="新規DBファイル",
            defaultextension=".db",
            filetypes=[("SQLiteファイル", "*.db"), ("すべて", "*.*")])
        if path:
            self._connect(path)

    def _connect(self, path):
        if self.conn:
            self.conn.close()
        try:
            self.conn = sqlite3.connect(path)
            self.db_path = path
            self.root.title(f"SQLiteブラウザ — {os.path.basename(path)}")
            self.status_var.set(f"接続済み: {path}")
            self._load_tables()
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _load_tables(self):
        if not self.conn:
            return
        self.tbl_list.delete(0, tk.END)
        cur = self.conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
        for row in cur.fetchall():
            self.tbl_list.insert(tk.END, row[0])

    def _on_table_select(self, event):
        sel = self.tbl_list.curselection()
        if not sel:
            return
        table = self.tbl_list.get(sel[0])
        # カラム情報
        self.col_tree.delete(*self.col_tree.get_children())
        for row in self.conn.execute(f"PRAGMA table_info({table})"):
            self.col_tree.insert("", "end", values=row[:5])
        # SQLにSELECTをセット
        self.sql_text.delete("1.0", tk.END)
        self.sql_text.insert("1.0", f"SELECT * FROM {table} LIMIT 100;")
        self._run_sql()

    def _run_sql(self):
        if not self.conn:
            messagebox.showwarning("警告", "DBを開いてください")
            return
        sql = self.sql_text.get("1.0", tk.END).strip()
        if not sql:
            return
        try:
            cur = self.conn.execute(sql)
            self.conn.commit()
            # 結果をTreeviewに表示
            self.result_tree.delete(*self.result_tree.get_children())
            if cur.description:
                cols = [d[0] for d in cur.description]
                self.result_tree.configure(columns=cols)
                for c in cols:
                    self.result_tree.heading(c, text=c)
                    self.result_tree.column(c, width=max(80, len(c)*10), minwidth=40)
                rows = cur.fetchall()
                for row in rows:
                    self.result_tree.insert("", "end",
                                            values=[str(v) if v is not None else "NULL"
                                                    for v in row])
                self.status_var.set(f"{len(rows)} 行取得")
            else:
                self.result_tree.configure(columns=("result",))
                self.result_tree.heading("result", text="結果")
                self.result_tree.insert("", "end", values=("OK",))
                self.status_var.set(f"実行完了 ({cur.rowcount} 行影響)")
                self._load_tables()
        except Exception as e:
            messagebox.showerror("SQLエラー", str(e))


if __name__ == "__main__":
    root = tk.Tk()
    app = App03(root)
    root.mainloop()

PanedWindow と LabelFrame によるレイアウト

ttk.PanedWindow を使うと、ユーザーが境界線をドラッグしてペインの幅を自由に変更できます。テーブル一覧ペインには weight=1、カラム情報ペインには weight=2 を指定して初期比率を設定しています。各ペインの内側は ttk.LabelFrame でラベル付きの枠を作り、視覚的に区別しています。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os


class App03:
    """SQLiteデータベースブラウザ"""

    def __init__(self, root):
        self.root = root
        self.root.title("SQLiteデータベースブラウザ")
        self.root.geometry("900x600")
        self.root.configure(bg="#f8f9fc")
        self.conn = None
        self.db_path = None
        self._build_ui()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#3776ab", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🗄️ SQLiteデータベースブラウザ",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#3776ab", fg="white").pack(side=tk.LEFT, padx=12)
        ttk.Button(title_frame, text="📂 DBを開く",
                   command=self._open_db).pack(side=tk.RIGHT, padx=12)
        ttk.Button(title_frame, text="✨ 新規DB",
                   command=self._new_db).pack(side=tk.RIGHT, padx=4)

        # 上段: テーブル一覧 + カラム情報
        top = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        top.pack(fill=tk.BOTH, expand=False, padx=4, pady=4)

        # テーブル一覧
        tbl_frame = ttk.LabelFrame(top, text="テーブル一覧", padding=4)
        self.tbl_list = tk.Listbox(tbl_frame, font=("Courier New", 11),
                                    width=20, exportselection=False)
        self.tbl_list.pack(fill=tk.BOTH, expand=True)
        self.tbl_list.bind("<<ListboxSelect>>", self._on_table_select)
        top.add(tbl_frame, weight=1)

        # カラム情報
        col_frame = ttk.LabelFrame(top, text="カラム情報", padding=4)
        cols = ("cid", "name", "type", "notnull", "pk")
        self.col_tree = ttk.Treeview(col_frame, columns=cols,
                                      show="headings", height=6)
        for c, h, w in [("cid", "#", 30), ("name", "名前", 120),
                         ("type", "型", 80), ("notnull", "NOT NULL", 70),
                         ("pk", "PK", 40)]:
            self.col_tree.heading(c, text=h)
            self.col_tree.column(c, width=w, minwidth=30)
        self.col_tree.pack(fill=tk.BOTH, expand=True)
        top.add(col_frame, weight=2)

        # SQL入力エリア
        sql_frame = ttk.LabelFrame(self.root, text="SQL実行", padding=6)
        sql_frame.pack(fill=tk.X, padx=4, pady=(0, 4))
        self.sql_text = tk.Text(sql_frame, height=3, font=("Courier New", 11),
                                bg="white")
        self.sql_text.pack(fill=tk.X, expand=True, side=tk.LEFT)
        self.sql_text.insert("1.0", "SELECT * FROM sqlite_master WHERE type='table';")
        btn_frame = tk.Frame(sql_frame, bg=sql_frame.cget("background"))
        btn_frame.pack(side=tk.RIGHT, padx=6)
        ttk.Button(btn_frame, text="▶ 実行\n(F5)",
                   command=self._run_sql).pack(pady=2)
        ttk.Button(btn_frame, text="クリア",
                   command=lambda: self.sql_text.delete("1.0", tk.END)).pack()
        self.root.bind("<F5>", lambda e: self._run_sql())

        # 結果グリッド
        result_frame = ttk.LabelFrame(self.root, text="結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=4, pady=(0, 4))
        self.result_tree = ttk.Treeview(result_frame, show="headings")
        h_sb = ttk.Scrollbar(result_frame, orient=tk.HORIZONTAL,
                             command=self.result_tree.xview)
        v_sb = ttk.Scrollbar(result_frame, 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)

        self.status_var = tk.StringVar(value="DBファイルを開いてください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _open_db(self):
        path = filedialog.askopenfilename(
            title="SQLiteファイルを選択",
            filetypes=[("SQLiteファイル", "*.db *.sqlite *.sqlite3"),
                       ("すべて", "*.*")])
        if path:
            self._connect(path)

    def _new_db(self):
        path = filedialog.asksaveasfilename(
            title="新規DBファイル",
            defaultextension=".db",
            filetypes=[("SQLiteファイル", "*.db"), ("すべて", "*.*")])
        if path:
            self._connect(path)

    def _connect(self, path):
        if self.conn:
            self.conn.close()
        try:
            self.conn = sqlite3.connect(path)
            self.db_path = path
            self.root.title(f"SQLiteブラウザ — {os.path.basename(path)}")
            self.status_var.set(f"接続済み: {path}")
            self._load_tables()
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _load_tables(self):
        if not self.conn:
            return
        self.tbl_list.delete(0, tk.END)
        cur = self.conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
        for row in cur.fetchall():
            self.tbl_list.insert(tk.END, row[0])

    def _on_table_select(self, event):
        sel = self.tbl_list.curselection()
        if not sel:
            return
        table = self.tbl_list.get(sel[0])
        # カラム情報
        self.col_tree.delete(*self.col_tree.get_children())
        for row in self.conn.execute(f"PRAGMA table_info({table})"):
            self.col_tree.insert("", "end", values=row[:5])
        # SQLにSELECTをセット
        self.sql_text.delete("1.0", tk.END)
        self.sql_text.insert("1.0", f"SELECT * FROM {table} LIMIT 100;")
        self._run_sql()

    def _run_sql(self):
        if not self.conn:
            messagebox.showwarning("警告", "DBを開いてください")
            return
        sql = self.sql_text.get("1.0", tk.END).strip()
        if not sql:
            return
        try:
            cur = self.conn.execute(sql)
            self.conn.commit()
            # 結果をTreeviewに表示
            self.result_tree.delete(*self.result_tree.get_children())
            if cur.description:
                cols = [d[0] for d in cur.description]
                self.result_tree.configure(columns=cols)
                for c in cols:
                    self.result_tree.heading(c, text=c)
                    self.result_tree.column(c, width=max(80, len(c)*10), minwidth=40)
                rows = cur.fetchall()
                for row in rows:
                    self.result_tree.insert("", "end",
                                            values=[str(v) if v is not None else "NULL"
                                                    for v in row])
                self.status_var.set(f"{len(rows)} 行取得")
            else:
                self.result_tree.configure(columns=("result",))
                self.result_tree.heading("result", text="結果")
                self.result_tree.insert("", "end", values=("OK",))
                self.status_var.set(f"実行完了 ({cur.rowcount} 行影響)")
                self._load_tables()
        except Exception as e:
            messagebox.showerror("SQLエラー", str(e))


if __name__ == "__main__":
    root = tk.Tk()
    app = App03(root)
    root.mainloop()

テーブル選択と PRAGMA table_info(_on_table_select)

Listbox の選択変更イベント <<ListboxSelect>>_on_table_select が呼ばれます。ここで PRAGMA table_info(テーブル名) を実行するとカラム情報が取得でき、結果を col_tree(Treeview)に挿入します。さらに SQL テキストに SELECT * FROM テーブル名 LIMIT 100; を自動セットして _run_sql() を呼び出すことで、テーブルを選ぶだけでデータが一覧表示される快適な UX を実現しています。

    def _on_table_select(self, event):
        sel = self.tbl_list.curselection()
        if not sel:
            return
        table = self.tbl_list.get(sel[0])
        # カラム情報
        self.col_tree.delete(*self.col_tree.get_children())
        for row in self.conn.execute(f"PRAGMA table_info({table})"):
            self.col_tree.insert("", "end", values=row[:5])
        # SQLにSELECTをセット
        self.sql_text.delete("1.0", tk.END)
        self.sql_text.insert("1.0", f"SELECT * FROM {table} LIMIT 100;")
        self._run_sql()

SQL 実行と Treeview への結果表示(_run_sql)

_run_sqlconn.execute(sql) でクエリを実行します。cursor.description が存在すれば SELECT 系クエリとして、カラム名リストを result_tree.configure(columns=cols) で動的に設定してから行を挿入します。descriptionNone の場合は INSERT / UPDATE / DELETE などの更新系クエリで、cur.rowcount で影響行数を表示します。更新後は _load_tables() を呼んでテーブル一覧を再読み込みします。

    def _run_sql(self):
        if not self.conn:
            messagebox.showwarning("警告", "DBを開いてください")
            return
        sql = self.sql_text.get("1.0", tk.END).strip()
        if not sql:
            return
        try:
            cur = self.conn.execute(sql)
            self.conn.commit()
            self.result_tree.delete(*self.result_tree.get_children())
            if cur.description:
                cols = [d[0] for d in cur.description]
                self.result_tree.configure(columns=cols)
                for c in cols:
                    self.result_tree.heading(c, text=c)
                    self.result_tree.column(c, width=max(80, len(c)*10), minwidth=40)
                rows = cur.fetchall()
                for row in rows:
                    self.result_tree.insert("", "end",
                                            values=[str(v) if v is not None else "NULL"
                                                    for v in row])
                self.status_var.set(f"{len(rows)} 行取得")
            else:
                self.result_tree.configure(columns=("result",))
                self.result_tree.heading("result", text="結果")
                self.result_tree.insert("", "end", values=("OK",))
                self.status_var.set(f"実行完了 ({cur.rowcount} 行影響)")
                self._load_tables()
        except Exception as e:
            messagebox.showerror("SQLエラー", str(e))

DB 接続と _connect / _load_tables

_connect(path) は既存の接続があれば先に close() してから sqlite3.connect(path) で接続します。接続後はウィンドウタイトルにファイル名を反映し、_load_tables() を呼んでテーブル一覧を更新します。_load_tablessqlite_master テーブルから type='table' の行を取得して Listbox に表示します。

    def _connect(self, path):
        if self.conn:
            self.conn.close()
        try:
            self.conn = sqlite3.connect(path)
            self.db_path = path
            self.root.title(f"SQLiteブラウザ — {os.path.basename(path)}")
            self.status_var.set(f"接続済み: {path}")
            self._load_tables()
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _load_tables(self):
        if not self.conn:
            return
        self.tbl_list.delete(0, tk.END)
        cur = self.conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
        for row in cur.fetchall():
            self.tbl_list.insert(tk.END, row[0])

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    ファイルを作成してライブラリをインポートする

    新しいファイルを作成して app003.py と保存し、tkintersqlite3os をインポートします。

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

    App03 クラスを定義し、__init__ でウィンドウサイズ・タイトル・背景色を設定します。self.conn = Noneself.db_path = None を初期化してから _build_ui() を呼びます。

  3. 3
    タイトルバーとボタンを作る

    tk.Frame に青色背景を設定し、タイトルラベルと「DBを開く」「新規DB」ボタンを配置します。

  4. 4
    PanedWindow でテーブル一覧とカラム情報ペインを作る

    ttk.PanedWindow(orient=HORIZONTAL) を作り、左に Listbox(テーブル一覧)、右に Treeview(カラム情報)を LabelFrame に収めて追加します。Listbox に <<ListboxSelect>> イベントをバインドします。

  5. 5
    SQL 入力エリアを作る

    tk.Text ウィジェットと「実行」「クリア」ボタンを横並びに配置します。root.bind("<F5>", ...) でキーボードショートカットも設定します。

  6. 6
    結果グリッド(Treeview)を作る

    水平・垂直スクロールバー付きの ttk.Treeview を配置します。列は実行時に動的に設定するため、初期は空のままにします。

  7. 7
    DB 接続・テーブル読み込み・SQL 実行メソッドを実装する

    _connect_load_tables_on_table_select_run_sql を順番に実装します。各メソッドに try-except を追加してエラーを messagebox で表示します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。

💡 クエリ履歴機能を追加する

実行した SQL 文をリストに保存し、Combobox のドロップダウンで再選択できる履歴機能を追加しましょう。collections.deque で最大件数を制限すると便利です。

💡 SELECT 結果を CSV にエクスポートする

結果グリッドの内容を CSV ファイルに書き出す「エクスポート」ボタンを追加しましょう。filedialog.asksaveasfilename() でファイル保存ダイアログを開き、csv モジュールで書き出します。

💡 テーブル編集(INSERT / DELETE)を GUI で行う

結果 Treeview でセルを直接ダブルクリックして編集し、UPDATE 文を自動生成して実行するインライン編集機能に挑戦してみましょう。

💡 SQL 構文ハイライトを追加する

入力テキストボックスに Text.tag_configure と正規表現を使って SELECT・FROM・WHERE などのキーワードを色付けするシンタックスハイライトを実装しましょう。

8. よくある問題と解決法

❌ OperationalError: no such table が出る

原因:テーブル名のタイプミス、または接続先 DB にそのテーブルが存在しない場合です。

解決法:テーブル一覧 Listbox でテーブルを選択すると SQL が自動セットされるので、まずそちらで確認してください。

❌ PanedWindow の境界をドラッグできない

原因:macOS では ttk.PanedWindow のサッシュ(境界)が見えにくい場合があります。

解決法:マウスを左右ペインの境界に合わせると矢印カーソルに変わるので、そのままドラッグしてください。

❌ DB ファイルを開いてもテーブルが表示されない

原因:ファイルが空の新規 DB か、テーブルがまだ作成されていない可能性があります。

解決法:SQL 入力エリアに CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT); を入力して実行すると、テーブル一覧に追加されます。

❌ 日本語データが文字化けして表示される

原因:SQLite 自体は UTF-8 対応ですが、他のツールで SJIS などで作成されたファイルを開いた場合に発生します。

解決法:Python の sqlite3 モジュールは UTF-8 で読み書きするため、元のファイルのエンコーディングを確認してください。

9. 練習問題

アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。

  1. 課題1:新しいテーブルを作成してデータを追加する

    SQL 入力エリアで CREATE TABLE 文を実行してテーブルを作り、INSERT INTO で複数行のデータを追加してみましょう。テーブル一覧に表示されることを確認してください。

  2. 課題2:WHERE 句・ORDER BY・LIMIT を使ったクエリを実行する

    既存のテーブルに対して条件付き検索・並び替え・件数制限の SQL を試してみましょう。結果グリッドの列幅が自動調整されることを確認してください。

  3. 課題3:SELECT 結果を CSV ファイルにエクスポートする機能を追加する

    「CSV 保存」ボタンを追加し、現在の result_tree の内容を csv モジュールでファイルに書き出す機能を実装してみましょう。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のNo.04に挑戦しましょう。