CSVデータビューア・エディタ
CSVファイルを表形式で表示・編集・フィルタリング・ソートできるアプリ。pandasとTtk Treeviewの組み合わせを学びます。
1. アプリ概要
CSVファイルを表形式で表示・編集・フィルタリング・ソートできるアプリ。pandasとTtk Treeviewの組み合わせを学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ)・pandas で、難易度は ★★☆ です。
このアプリは「データ処理」カテゴリです。データ分析・処理アプリは Python の真骨頂と言える領域で、生データから意味ある可視化・要約までを一通り体験できます。tkinter(標準ライブラリ)・pandas を活かして実装するこの構造は、他のアプリにも応用が効きます。
動かしながら読むことが理解の最短経路です。まずはコードをコピーして実行し、想定どおりに動くことを確認したうえで解説と照らし合わせてください。
カスタマイズでは「機能追加」「UI 改善」「エラー耐性」の三方向で考えると視野が広がります。練習問題にもそれぞれの方向の具体例を用意しています。
2. 機能一覧
- ループで生成するボタン群(それぞれ
_open_csv()・_save_csv()・_save_as()を実行) - 「➕ 行追加」ボタン(
_add_row())・「🗑️ 行削除 (Del)」ボタン(_delete_row())・「📊 統計情報」ボタン(_show_stats())で操作 - キー
<KeyRelease>から_filter()を実行 - イベント
<<ComboboxSelected>>から_filter()を実行 - マウス操作
<Double-1>から_on_double_click()を実行 - キー
<Delete>から_delete_row()を実行 - キー
<Return>から 処理 を実行 - キー
<Escape>からlambda e: entry.destroy()を実行 <FocusOut>イベント(ウィンドウ側からの通知)で 処理 を自動実行ttk.Treeviewによる表形式の一覧表示- CSV ファイルへの書き出し
filedialog.askopenfilename()によるファイル選択ダイアログfiledialog.asksaveasfilename()によるファイル選択ダイアログmessagebox.askyesno()・messagebox.showerror()・messagebox.showinfo()によるダイアログ通知- ウィンドウはタイトル「CSVデータビューア・エディタ」・サイズ 900x600 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import csv
import os
try:
import pandas as pd
PANDAS_AVAILABLE = True
except ImportError:
pd = None # pandas なしでも動くよう、後述の SimpleDf に切り替える
PANDAS_AVAILABLE = False
class App08:
"""CSVデータビューア・エディタ"""
def __init__(self, root):
self.root = root
self.root.title("CSVデータビューア・エディタ")
self.root.geometry("900x600")
self.root.configure(bg="#f8f9fc")
self.df = None
self.filepath = 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="📊 CSVデータビューア・エディタ",
font=("Noto Sans JP", 15, "bold"),
bg="#3776ab", fg="white").pack(side=tk.LEFT, padx=12)
# ツールバー
toolbar = tk.Frame(self.root, bg="#e8eef5", pady=5)
toolbar.pack(fill=tk.X)
for text, cmd in [("📂 CSVを開く", self._open_csv),
("💾 保存", self._save_csv),
("💾 名前を付けて保存", self._save_as)]:
ttk.Button(toolbar, text=text, command=cmd).pack(side=tk.LEFT, padx=4, pady=2)
tk.Label(toolbar, text=" 🔍 検索:",
bg="#e8eef5").pack(side=tk.LEFT, padx=(16, 4))
self.search_var = tk.StringVar()
search_entry = ttk.Entry(toolbar, textvariable=self.search_var, width=16)
search_entry.pack(side=tk.LEFT)
search_entry.bind("<KeyRelease>", lambda e: self._filter())
tk.Label(toolbar, text=" 列:", bg="#e8eef5").pack(side=tk.LEFT, padx=(12, 4))
self.filter_col_var = tk.StringVar(value="すべて")
self.filter_col_cb = ttk.Combobox(toolbar, textvariable=self.filter_col_var,
values=["すべて"], state="readonly", width=12)
self.filter_col_cb.pack(side=tk.LEFT)
self.filter_col_cb.bind("<<ComboboxSelected>>", lambda e: self._filter())
# グリッド
grid_frame = tk.Frame(self.root, bg="#f8f9fc")
grid_frame.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
self.tree = ttk.Treeview(grid_frame, show="headings", selectmode="browse")
h_sb = ttk.Scrollbar(grid_frame, orient=tk.HORIZONTAL,
command=self.tree.xview)
v_sb = ttk.Scrollbar(grid_frame, orient=tk.VERTICAL,
command=self.tree.yview)
self.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.tree.pack(fill=tk.BOTH, expand=True)
self.tree.bind("<Double-1>", self._on_double_click)
self.tree.bind("<Delete>", self._delete_row)
# 行操作パネル
row_frame = tk.Frame(self.root, bg="#e8eef5", pady=4)
row_frame.pack(fill=tk.X)
ttk.Button(row_frame, text="➕ 行追加",
command=self._add_row).pack(side=tk.LEFT, padx=8)
ttk.Button(row_frame, text="🗑️ 行削除 (Del)",
command=self._delete_row).pack(side=tk.LEFT, padx=4)
ttk.Button(row_frame, text="📊 統計情報",
command=self._show_stats).pack(side=tk.LEFT, padx=4)
self.status_var = tk.StringVar(value="CSVファイルを開いてください")
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_csv(self):
path = filedialog.askopenfilename(
filetypes=[("CSVファイル", "*.csv"), ("テキストファイル", "*.txt"),
("すべて", "*.*")])
if not path:
return
try:
self._load_file(path)
except Exception as e:
messagebox.showerror("エラー", str(e))
def _load_file(self, path):
self.filepath = path
if PANDAS_AVAILABLE:
encodings = ["utf-8", "cp932", "latin-1"]
for enc in encodings:
try:
self.df = pd.read_csv(path, encoding=enc)
break
except UnicodeDecodeError:
continue
else:
# pandas なし: csv モジュールで読込
rows = []
headers = []
for enc in ["utf-8", "cp932", "latin-1"]:
try:
with open(path, newline="", encoding=enc) as f:
reader = csv.reader(f)
headers = next(reader)
rows = list(reader)
break
except UnicodeDecodeError:
continue
# DataFrameを手動で構築
class SimpleDf:
def __init__(self, headers, rows):
self.columns = headers
self._rows = rows
def iterrows(self):
for i, r in enumerate(self._rows):
yield i, r
def __len__(self):
return len(self._rows)
def to_csv(self, path, index=False, encoding="utf-8"):
with open(path, "w", newline="", encoding=encoding) as f:
w = csv.writer(f)
w.writerow(self.columns)
w.writerows(self._rows)
self.df = SimpleDf(headers, rows)
self._setup_columns()
self._fill_rows(self.df)
self.root.title(f"CSVビューア — {os.path.basename(path)}")
n = len(self.df) if PANDAS_AVAILABLE else len(self.df._rows)
self.status_var.set(f"{os.path.basename(path)} | "
f"{n} 行 × {len(self.df.columns)} 列")
def _setup_columns(self):
cols = list(self.df.columns)
self.tree.configure(columns=cols)
for c in cols:
self.tree.heading(c, text=c,
command=lambda col=c: self._sort_by(col))
self.tree.column(c, width=max(80, len(str(c))*10), minwidth=40)
self.filter_col_cb.configure(values=["すべて"] + cols)
self.filter_col_var.set("すべて")
def _fill_rows(self, df):
self.tree.delete(*self.tree.get_children())
if PANDAS_AVAILABLE:
for _, row in df.iterrows():
self.tree.insert("", "end", values=list(row.astype(str)))
else:
for _, row in df.iterrows():
self.tree.insert("", "end",
values=[str(v) for v in row])
def _filter(self):
if self.df is None:
return
q = self.search_var.get().strip().lower()
col = self.filter_col_var.get()
self.tree.delete(*self.tree.get_children())
if PANDAS_AVAILABLE:
if not q:
filtered = self.df
elif col == "すべて":
mask = self.df.astype(str).apply(
lambda s: s.str.lower().str.contains(q, na=False)).any(axis=1)
filtered = self.df[mask]
else:
filtered = self.df[
self.df[col].astype(str).str.lower().str.contains(q, na=False)]
for _, row in filtered.iterrows():
self.tree.insert("", "end", values=list(row.astype(str)))
self.status_var.set(f"{len(filtered)} 件表示 / 全 {len(self.df)} 件")
else:
for _, row in self.df.iterrows():
vals = [str(v) for v in row]
if not q or any(q in v.lower() for v in vals):
self.tree.insert("", "end", values=vals)
def _sort_by(self, col):
if self.df is None or not PANDAS_AVAILABLE:
return
try:
self.df = self.df.sort_values(col)
except Exception:
self.df = self.df.sort_values(col, key=lambda s: s.astype(str))
self._fill_rows(self.df)
def _on_double_click(self, event):
"""セルをダブルクリックで編集"""
region = self.tree.identify_region(event.x, event.y)
if region != "cell":
return
row_id = self.tree.identify_row(event.y)
col_id = self.tree.identify_column(event.x)
col_idx = int(col_id.replace("#", "")) - 1
bbox = self.tree.bbox(row_id, col_id)
if not bbox:
return
x, y, w, h = bbox
val = self.tree.item(row_id)["values"][col_idx]
entry = tk.Entry(self.tree, font=("Arial", 11))
entry.insert(0, str(val))
entry.select_range(0, tk.END)
entry.place(x=x, y=y, width=w, height=h)
entry.focus_set()
def confirm(e=None):
new_val = entry.get()
values = list(self.tree.item(row_id)["values"])
values[col_idx] = new_val
self.tree.item(row_id, values=values)
entry.destroy()
entry.bind("<Return>", confirm)
entry.bind("<Escape>", lambda e: entry.destroy())
entry.bind("<FocusOut>", confirm)
def _add_row(self):
if self.df is None:
return
cols = self.tree.cget("columns")
n = len(cols)
self.tree.insert("", "end", values=[""] * n)
def _delete_row(self, event=None):
sel = self.tree.selection()
if sel:
if messagebox.askyesno("確認", "選択行を削除しますか?"):
for item in sel:
self.tree.delete(item)
def _show_stats(self):
if self.df is None or not PANDAS_AVAILABLE:
messagebox.showinfo("情報", "pandasが必要です")
return
desc = self.df.describe(include="all").to_string()
win = tk.Toplevel(self.root)
win.title("統計情報")
win.geometry("600x400")
txt = tk.Text(win, font=("Courier New", 10), wrap=tk.NONE)
txt.pack(fill=tk.BOTH, expand=True)
txt.insert("1.0", desc)
txt.config(state=tk.DISABLED)
def _save_csv(self):
if not self.filepath:
self._save_as()
return
self._write_csv(self.filepath)
def _save_as(self):
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSVファイル", "*.csv"), ("すべて", "*.*")])
if path:
self.filepath = path
self._write_csv(path)
def _write_csv(self, path):
try:
cols = self.tree.cget("columns")
with open(path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(cols)
for item in self.tree.get_children():
writer.writerow(self.tree.item(item)["values"])
self.status_var.set(f"保存済み: {path}")
except Exception as e:
messagebox.showerror("エラー", str(e))
if __name__ == "__main__":
root = tk.Tk()
app = App08(root)
root.mainloop()
5. コード解説
CSVデータビューア・エディタのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App08 クラスにアプリの全機能をまとめています(メソッド15個・全284行)。__init__ ではタイトル「CSVデータビューア・エディタ」とウィンドウサイズ 900x600 を設定します。あわせて状態を保持する変数(self.df・self.filepath)を初期化し、最後に _build_ui() で画面を組み立てます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×4・tk.Label×4・ttk.Button×3(ほかにループでも生成)・ttk.Entry・ttk.Combobox・ttk.Treeview・ttk.Scrollbar×2・tk.Entry・tk.Toplevel・tk.Text で構成しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「➕ 行追加」ボタン(_add_row())・「🗑️ 行削除 (Del)」ボタン(_delete_row())・「📊 統計情報」ボタン(_show_stats())を command= で接続しています。ボタンはラベルと処理をペアで並べたデータをループで回して生成し、それぞれ _open_csv()・_save_csv()・_save_as() を command= に渡しています。ボタンを増やしたいときはこのペアを1行足すだけで済みます。また bind("<KeyRelease>", ...) から _filter()、bind("<<ComboboxSelected>>", ...) から _filter()、bind("<Double-1>", ...) から _on_double_click()、bind("<Delete>", ...) から _delete_row()、bind("<Return>", ...) から 処理、bind("<Escape>", ...) から lambda e: entry.destroy()、bind("<FocusOut>", ...) から 処理 を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・ImportError・UnicodeDecodeError を捕捉しています。あわせて messagebox.askyesno()・messagebox.showerror()・messagebox.showinfo() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app08.py と保存します。外部ライブラリ
pandasを使います。先にpip install pandasを実行してください。 -
2クラスの骨格を作る
App08クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("CSVデータビューア・エディタ")・geometry("900x600")・configure(bg="#f8f9fc")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×4・tk.Label×4・ttk.Button×3(ほかにループでも生成)・ttk.Entry・ttk.Combobox・ttk.Treeview・ttk.Scrollbar×2・tk.Entry・tk.Toplevel・tk.Textを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_add_row()・_delete_row()・_show_stats())につなぎます。ループ生成のボタンは、ラベルと処理(_open_csv()・_save_csv()・_save_as())をペアで並べたデータをループで回し、command=に渡して接続します。bind("<KeyRelease>", ...)・bind("<<ComboboxSelected>>", ...)・bind("<Double-1>", ...)・bind("<Delete>", ...)・bind("<Return>", ...)・bind("<Escape>", ...)・bind("<FocusOut>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_on_double_click()(29行)・_filter()(24行)・_show_stats()(12行)など を実装します。 -
7動作確認する
python app08.pyで起動し、各ボタンが反応すること・キー操作(<KeyRelease>・<Delete>・<Return>・<Escape>)が効くこと・マウス操作(クリック・ホイールなど)が効くことを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 配色をまとめて管理する
背景色 #f8f9fc をはじめ、色コードが各所に直書きされています。色を1つの辞書にまとめると、テーマの切り替えが1か所の変更で済むようになります。
💡 画面の文言を自分好みに変える
ラベルやボタンの文言を自分の好みに書き換えてみましょう。文言を変えるだけでも、どのコードがどの表示に対応しているかが立体的に見えてきます。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ <KeyRelease> キーを押しても反応しない
原因:bind("<KeyRelease>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。
解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("900x600") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。
-
課題1:数値の列を右寄せにする
読み込んだ CSV のうち、数値だけの列を右寄せ表示にします。文字が混じる列は左寄せのままにしてください。
期待結果:「商品,単価,数量」の CSV を開くと、単価と数量の値が右端に揃い、商品名は左寄せのまま表示される。
合格条件
- 別の CSV を開き直すと、その CSV の数値列だけが右寄せになる
- 数字と文字が混ざる列(例: 「A-1」「B-2」)は左寄せのまま
- 見出し(ヘッダー)の表示は今までどおり
ヒント①(どこを触るか): 列の設定は
_setup_columns()のself.tree.column(c, width=…, minwidth=40)の1行です。ここにanchorを渡します。
ヒント②(使うもの): 右寄せはanchor="e"、左寄せは"w"。数値列かどうかはpd.api.types.is_numeric_dtype(self.df[c])で判定できます。
つまずきやすい点: pandas が入っていない環境ではself.dfがコード内で定義している簡易クラスSimpleDfに差し替わり、self.df[列名]の書き方ができません。このときpdはNoneなのでpd.api.types.is_numeric_dtype()も呼べません。判定の条件にPANDAS_AVAILABLE and …を付けておいてください。 -
課題2:セルの編集を DataFrame に書き戻す
いまのコードはセルを編集しても画面(Treeview)だけが変わり、
self.dfは古い値のままです。編集した内容がself.dfにも反映されるようにします。期待結果:セルをダブルクリックして「東京」を「大阪」に直して Enter を押したあと、検索欄に「大阪」と入力するとその行がヒットする(いまのコードでは編集前の値に戻ってしまう)。
合格条件
- 編集 → 検索 → 検索欄を空にする、の順に操作しても編集した値が残る
- 編集後に「📊 統計情報」を押すと新しい値を含んだ結果になる
- 見出しをクリックして並び替えたあとに編集しても、正しい行が書き換わる
ヒント①(どこを触るか): 編集を確定しているのは
_on_double_click()の中のconfirm()です。ここがself.tree.item(row_id, values=values)しか呼んでいません。
ヒント②(使うもの): 画面の行と DataFrame の行を対応づけるには、_fill_rows()のself.tree.insert("", "end", values=…)にiid=str(インデックス)を足します(iterrows()の第1返り値が DataFrame のインデックスです)。書き戻しはself.df.at[インデックス, 列名]。
つまずきやすい点:_fill_rows()は毎回 Treeview を全消ししてから入れ直すので、検索や並び替えのあとは画面の行番号と DataFrame の並びが一致しません。行番号ではなく iid で対応づけてください。 -
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。