CSVデータビューア・エディタ
CSVファイルを表形式で表示・編集・フィルタリング・ソートできるアプリ。pandasとTtk Treeviewの組み合わせを学びます。
1. アプリ概要
CSV の中身を確かめたり数か所だけ直したりするたびに Excel を開く手間を、自作のビューアで置き換えます。表形式での表示・編集・フィルタリング・並べ替えを1つのウィンドウで扱い、データの保持は pandas、表の描画は Ttk Treeview が受け持ちます。
Excel や CSV の集計・加工を自動化したいだけなら、PythonでExcel作業を自動化するのほうが近道です。
pandas が入っていない環境でも簡易版の SimpleDf に切り替わって表示・検索・編集・保存はできますが、列見出しクリックでの並べ替えと「📊 統計情報」は pandas がないと動きません。
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>を押すとconfirm()で確定(_set_cell()が先にself.dfを書き換え、その行の表示をself.dfの値で描き直す) - キー
<Escape>からlambda e: entry.destroy()を実行(編集を取り消す) - 編集欄からフォーカスが外れたとき(
<FocusOut>)もconfirm()で確定 ttk.Treeviewによる表形式の一覧表示(見出しクリックで並べ替え。空欄以外がすべて数値の列は数値の大小順)- CSV ファイルの読み込みと書き出し(値は書いてあるとおりの文字列で持つので、
007が7になったり空欄がnanになったりしない) - 読み込み時に文字コードを自動で試し(BOM ありなら utf-8-sig、なしなら utf-8 → cp932 → latin-1)、ステータス欄に表示。保存も同じ文字コードで書き出す
- 保存は検索で絞り込み中でも全行が対象(表示中の行数が全行より少ないときは、ステータス欄に「保存済み(絞り込みに関係なく全行)」と表示)
filedialog.askopenfilename()によるファイル選択ダイアログfiledialog.asksaveasfilename()によるファイル保存ダイアログmessagebox.askyesno()・messagebox.showerror()・messagebox.showinfo()によるダイアログ通知- ウィンドウはタイトル「CSVデータビューア・エディタ」・サイズ 900x600 で起動
3. 事前準備・環境
Python 3.11 以上(Ubuntu で 3.14・3.15 の起動を確認) / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。
- Python 3.11 以上(Ubuntu で 3.14・3.15 の起動を確認)
- 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 SimpleDf:
"""pandas が無いときの簡易版 DataFrame(値はすべて文字列のまま持つ)"""
def __init__(self, headers, rows):
self.columns = headers
self._rows = dict(enumerate(rows)) # 行番号 → 行。削除しても番号はずれない
def iterrows(self):
return iter(list(self._rows.items()))
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.values())
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.encoding = "utf-8-sig" # 読み込んだときの文字コード(保存にも使う)
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):
# 先頭に BOM があれば utf-8-sig、無ければ utf-8 → cp932 の順に試す(最後は latin-1)
with open(path, "rb") as f:
has_bom = f.read(3) == b"\xef\xbb\xbf"
encodings = (["utf-8-sig"] if has_bom else ["utf-8", "cp932"]) + ["latin-1"]
if PANDAS_AVAILABLE:
for enc in encodings:
try:
# dtype=str: 007 や 100 を数値にせず、書いてあるとおりの文字列で持つ
# keep_default_na=False: 空欄を NaN にせず "" のまま持つ
df = pd.read_csv(path, encoding=enc, dtype=str,
keep_default_na=False)
break
except UnicodeDecodeError:
continue
else:
# pandas なし: csv モジュールで読込
for enc in encodings:
try:
with open(path, newline="", encoding=enc) as f:
reader = csv.reader(f)
headers = next(reader)
rows = list(reader)
break
except UnicodeDecodeError:
continue
df = SimpleDf(headers, rows)
self.df = df
self.encoding = enc
self.filepath = path
self._setup_columns()
self._fill_rows(self.df)
self.root.title(f"CSVビューア — {os.path.basename(path)}")
self.status_var.set(f"{os.path.basename(path)} | "
f"{len(self.df)} 行 × {len(self.df.columns)} 列"
f" | 文字コード: {enc}")
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 _insert_row(self, idx, values):
# iid に self.df の行番号を入れておくと、表の行から self.df の行をたどれる
self.tree.insert("", "end", iid=str(idx), values=[str(v) for v in values])
def _fill_rows(self, df):
self.tree.delete(*self.tree.get_children())
for idx, row in df.iterrows():
self._insert_row(idx, list(row))
def _filter(self):
if self.df is None:
return
q = self.search_var.get().strip().lower()
col = self.filter_col_var.get()
if PANDAS_AVAILABLE:
if not q:
filtered = self.df
elif col == "すべて":
mask = self.df.apply(
lambda s: s.str.lower().str.contains(q, regex=False)).any(axis=1)
filtered = self.df[mask]
else:
filtered = self.df[
self.df[col].str.lower().str.contains(q, regex=False)]
self._fill_rows(filtered)
else:
self.tree.delete(*self.tree.get_children())
ci = self.df.columns.index(col) if col in self.df.columns else None
for idx, row in self.df.iterrows():
target = row if ci is None else row[ci:ci + 1]
if not q or any(q in v.lower() for v in target):
self._insert_row(idx, row)
shown = len(self.tree.get_children())
self.status_var.set(f"{shown} 件表示 / 全 {len(self.df)} 件")
def _to_number(self, s):
"""空欄以外がすべて数値の列なら数値の Series を返す(空欄は NaN)。違えば None"""
filled = s[s.str.strip() != ""]
if len(filled) == 0 or pd.to_numeric(filled, errors="coerce").isna().any():
return None
return pd.to_numeric(s.where(s.str.strip() != ""), errors="coerce")
def _sort_by(self, col):
if self.df is None or not PANDAS_AVAILABLE:
return
num = self._to_number(self.df[col])
if num is not None: # 数値の列は数値の大小で並べる(文字列だと "100" < "20" になる)
order = num.sort_values(kind="stable", na_position="last").index
self.df = self.df.loc[order].copy()
else:
self.df = self.df.sort_values(col, kind="stable")
self._filter() # 検索中なら絞り込みを保ったまま並べ直す
def _set_cell(self, row_id, col_idx, new_val):
"""self.df を書き換えてから、その行の表示を self.df の値で描き直す"""
if PANDAS_AVAILABLE:
pos = self.df.index.get_loc(int(row_id))
self.df.iat[pos, col_idx] = new_val
values = list(self.df.iloc[pos])
else:
values = self.df._rows[int(row_id)]
values[col_idx] = new_val
# 表から値を読み戻すと "007" が 7 に変わるので、self.df の文字列を使う
self.tree.item(row_id, values=[str(v) for v in values])
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
# 表示用の値は "007" が 7 に変わることがあるので、元の値は self.df から取る
if PANDAS_AVAILABLE:
val = self.df.iat[self.df.index.get_loc(int(row_id)), col_idx]
else:
val = self.df._rows[int(row_id)][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):
if not entry.winfo_exists():
return
self._set_cell(row_id, col_idx, entry.get())
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
n = len(self.df.columns)
if PANDAS_AVAILABLE:
idx = int(self.df.index.max()) + 1 if len(self.df) else 0
self.df.loc[idx] = [""] * n
else:
idx = max(self.df._rows, default=-1) + 1
self.df._rows[idx] = [""] * n
self._insert_row(idx, [""] * n)
self.status_var.set(f"行を追加しました(全 {len(self.df)} 件)")
def _delete_row(self, event=None):
sel = self.tree.selection()
if sel:
if messagebox.askyesno("確認", "選択行を削除しますか?"):
for item in sel:
if PANDAS_AVAILABLE:
self.df = self.df.drop(index=int(item))
else:
del self.df._rows[int(item)]
self.tree.delete(item)
self.status_var.set(f"削除しました(全 {len(self.df)} 件)")
def _show_stats(self):
if self.df is None or not PANDAS_AVAILABLE:
messagebox.showinfo("情報", "pandasが必要です")
return
view = self.df.copy()
for c in view.columns: # 文字列で読んだので、数値の列だけ数値に直して集計する
num = self._to_number(view[c])
if num is not None:
view[c] = num
desc = view.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 self.df is None:
return
if not self.filepath:
self._save_as()
return
self._write_csv(self.filepath)
def _save_as(self):
if self.df is None:
return
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSVファイル", "*.csv"), ("すべて", "*.*")])
if path:
self.filepath = path
self._write_csv(path)
def _write_csv(self, path):
# 表(Treeview)ではなく self.df を書き出す。検索で絞り込み中でも全行を保存する
try:
self.df.to_csv(path, index=False, encoding=self.encoding)
shown = len(self.tree.get_children())
note = "" if shown == len(self.df) else "(絞り込みに関係なく全行)"
self.status_var.set(f"保存済み{note}: {len(self.df)} 行 / "
f"{self.encoding} / {path}")
except Exception as e:
messagebox.showerror("エラー", str(e))
if __name__ == "__main__":
root = tk.Tk()
app = App08(root)
root.mainloop()
5. コード解説
CSVデータビューア・エディタのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App08 クラスにアプリの全機能をまとめています(メソッド18個。ファイル全体は339行)。__init__ ではタイトル「CSVデータビューア・エディタ」とウィンドウサイズ 900x600 を設定します。あわせて状態を保持する変数(self.df・self.filepath・読み込んだ文字コードを覚える self.encoding)を初期化し、最後に _build_ui() で画面を組み立てます。
pandas が入っていない環境のために、モジュール直下に簡易版の SimpleDf クラス(メソッド4個)も用意しています。行を「行番号→行」の辞書で持つので、途中の行を削除しても残りの行番号がずれません。この場合も表示・検索(列指定を含む)・編集・行の追加と削除・保存は使えますが、並べ替えと統計情報は pandas が必要です。
※ 該当部分のコード本体は 「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() を呼べるようにイベントも登録しています。セル編集用の tk.Entry には、bind("<Return>", ...) と bind("<FocusOut>", ...) で編集の確定 confirm()、bind("<Escape>", ...) で lambda e: entry.destroy() を登録しています。Enter で確定すると編集欄が消え、そのあと FocusOut でもう一度 confirm() が呼ばれることがあるため、confirm() の先頭で entry.winfo_exists() を確かめて二重に確定しないようにしています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
読み込み: 文字コードの判定と dtype=str
_load_file() は、ファイルの先頭3バイトが UTF-8 の BOM なら utf-8-sig、そうでなければ utf-8 → cp932 の順に試し、どれでも読めなければ最後に latin-1 で読みます。使った文字コードは self.encoding に覚えて、ステータス欄にも表示します。
pandas では pd.read_csv(path, encoding=enc, dtype=str, keep_default_na=False) で読みます。dtype=str を付けないと、pandas が列の型を推測して 007 を 7、100 を 100.0(同じ列に空欄があるとき)に変えてしまい、そのまま保存すると元の表記が失われます。keep_default_na=False は空欄を NaN にせず空文字のまま持つための指定です。こうして、書いてあるとおりの文字列で持つようにしています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
表の行と self.df を iid で対応づける
画面の表(Treeview)と、データ本体の self.df は別物です。そこで _insert_row() で行を入れるとき、Treeview の各行の iid(行の識別子)に self.df の行番号を入れておきます。検索や並べ替えで表示順が変わっても、iid から self.df の行をたどれます。
セルをダブルクリックすると、_on_double_click() が self.df から元の値を取り出して編集欄に入れます(表に表示されている値は "007" が 7 になって返ってくることがあるためです)。確定すると _set_cell()(11行)が先に self.df を書き換え、その行の表示を self.df の値(文字列)で描き直します。表から値を読み戻さないので、同じ行にある "007" が 7 と表示し直されることもありません。行追加 _add_row() と行削除 _delete_row() も同じ行番号で self.df 側を更新するので、編集のあとに検索や並べ替えをしても内容が残ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
並べ替えと統計情報
値をすべて文字列で持っているので、そのまま並べると "100" が "20" より前に来ます。_sort_by()(10行)は、_to_number()(6行)で「空欄以外がすべて数値の列か」を調べ、数値の列なら数値の大小で、そうでなければ文字列の順で self.df を並べ替えます(数値の列では空欄は末尾に回ります)。並べ替えたあとは _filter() を呼ぶので、検索で絞り込んでいる最中でも絞り込みは保たれます。
_show_stats()(17行)も同じ _to_number() を使い、数値の列だけ数値に直したコピーに対して describe(include="all") を実行します。元の self.df は文字列のまま変わりません。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
検索と保存
_filter()(25行)は、入力した語を含む行だけを表に入れ直します。pandas では str.contains(q, regex=False) を使うので、( や * などの記号を入れても正規表現としては解釈されず、文字どおりに探します。
保存(_save_csv()・_save_as())は、どちらも _write_csv() から self.df.to_csv(path, index=False, encoding=self.encoding) で書き出します。表(Treeview)ではなく self.df を書き出すため、検索で絞り込んでいる最中でも全行が保存されます。表示中の行数が全行より少ないときは、ステータス欄に「保存済み(絞り込みに関係なく全行)」と表示されます。文字コードは読み込んだときと同じものを使います。見出しクリックで並べ替えたあとに保存すると、並べ替えた順で書き出されます。self.df が無い(まだ CSV を開いていない)ときは、保存ボタンを押しても何もしません。
※ 該当部分のコード本体は 「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>", ...)の登録も忘れずに(最後の3つはセル編集用のtk.Entryに付けます)。 -
6中心になるメソッドを実装する
まず読み込みの
_load_file()(36行)を書き、dtype=strで読めていること(たとえばprint(self.df.dtypes)で各列が文字列の型(pandas 3 以降はstr、pandas 2 ではobject)になっていること)を確かめます。次に、表に行を入れる_insert_row()(3行)で iid にself.dfの行番号を入れ、セル編集の_on_double_click()(32行)と、先にself.dfを書き換えてからその行の表示を描き直す_set_cell()(11行)を実装します。続けて検索の_filter()(25行)、並べ替えの_to_number()(6行)・_sort_by()(10行)、統計の_show_stats()(17行)、保存の_write_csv()(10行)を足していきます。 -
7動作確認する
python app08.pyで起動し、各ボタンが反応すること・キー操作(<KeyRelease>・<Delete>・<Return>・<Escape>)が効くこと・マウス操作(クリック・ホイールなど)が効くことを確認します。あわせて「セルを編集 → 検索で絞り込む → 検索欄を空にする」の順に操作して編集が残ること、絞り込み中に保存しても全行が書き出されることを、元のファイルとは別名で保存して確かめておくと安心です。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 配色をまとめて管理する
背景色 #f8f9fc をはじめ、色コードが各所に直書きされています。色を1つの辞書にまとめると、テーマの切り替えが1か所の変更で済むようになります。
💡 画面の文言を自分好みに変える
ラベルやボタンの文言を自分の好みに書き換えてみましょう。文言を変えるだけでも、どのコードがどの表示に対応しているかが立体的に見えてきます。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:フォント名を指定しているのは、タイトル(Noto Sans JP)、ステータスバーとセル編集欄(Arial)、統計情報ウィンドウ(Courier New)です。Courier New が等幅でない書体に置き換わると統計表の列がずれて見えますが、読み込んだ値と保存される CSV の中身には影響しません。列を揃えたい場合は、_show_stats() の font=("Courier New", 10) を自分の OS にある等幅フォントの名前へ書き換えてください。ほかの font 引数は省略しても動きます。
❌ <KeyRelease> キーを押しても反応しない
原因:bind("<KeyRelease>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。
解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("900x600") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
❌ 検索で絞り込んで保存したのに、全行が保存された
原因:仕様です。保存は表(Treeview)ではなく self.df を書き出すため、絞り込みは保存内容に影響しません。絞り込みで表示中の行数が全行より少ないときに保存すると、ステータス欄に「保存済み(絞り込みに関係なく全行)」と出ます。
解決法:絞り込んだ行だけを別ファイルにしたい場合は、不要な行を「🗑️ 行削除」で消してから「💾 名前を付けて保存」で別名保存してください。元のファイルを上書きしないよう注意してください。
❌ 日本語が文字化けして表示される
原因:このコードは utf-8 → cp932 → latin-1 の順に試し、エラーなく読めた最初の文字コードを採用します。EUC-JP など、このコードが試さない文字コードのファイルは、cp932 でたまたまエラーなく読めてしまうか、最後の latin-1(どんなバイト列でもエラーなく読める)で読まれます。どちらの場合も日本語は文字化けして表示されます。
解決法:ステータス欄の「文字コード:」と元のファイルの文字コードを見比べてください。食い違っている場合は、テキストエディタで UTF-8 か Shift_JIS(cp932)に変換してから開き直すか、_load_file() の試す文字コードの並びに、そのファイルの文字コードを足してください。latin-1 で読んだまま日本語を入力して保存すると、latin-1 で表せない文字はエラーになります。
❌ 見出しをクリックしても並べ替わらない・数値順にならない
原因:並べ替え(_sort_by())は pandas が入っていないと何もしません。また、空欄以外に1つでも数値でない値(例: 「1,200」「-」)がある列は、文字列の順で並べます。
解決法:pip install pandas を実行してください。数値順にしたい列は、カンマや記号を含まない数値だけにそろえてください。
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"。数値列かどうかは、並べ替えでも使っているself._to_number(self.df[c]) is not Noneで判定できます。
つまずきやすい点: このコードはdtype=strで読むので、すべての列の型が文字列(pandas 3 以降はstr、pandas 2 ではobject)になります。pd.api.types.is_numeric_dtype()で型を調べても数値列は見つかりません。また pandas が入っていない環境ではself.dfが簡易クラスSimpleDfに差し替わり、self.df[列名]の書き方も_to_number()も使えません。判定の条件にPANDAS_AVAILABLE and …を付けておいてください。 -
課題2:未保存の変更があるときに確認する
いまのコードは、セルを編集したあと保存せずにウィンドウを閉じたり、別の CSV を開いたりすると、編集内容が確認なしで失われます。未保存の変更があるときは、確認のダイアログを出すようにします。
期待結果:セルを編集してから「📂 CSVを開く」を押すか、ウィンドウの × を押すと「保存していない変更があります。破棄しますか?」と聞かれる。「いいえ」を選ぶと元の画面に戻る。
合格条件
- 編集・行追加・行削除のどれをしても「未保存」の扱いになる
- 保存したあとや、CSV を開き直した直後は確認が出ない
- ステータス欄かタイトルに、未保存かどうかが分かる印(例: 先頭に「*」)が付く
ヒント①(どこを触るか):
self.dfを書き換えているのは_set_cell()・_add_row()・_delete_row()の3か所です。ここでself.dirty = Trueにし、_load_file()と_write_csv()の成功時にFalseに戻します(__init__で初期値Falseを忘れずに)。
ヒント②(使うもの): × ボタンを押したときの処理はself.root.protocol("WM_DELETE_WINDOW", 関数)で差し替えられます。確認には既に使っているmessagebox.askyesno()が使えます。
つまずきやすい点: ウィンドウを閉じる関数の中で、確認が通ったら最後にself.root.destroy()を呼ばないと、× を押してもウィンドウが閉じなくなります。また並べ替え(_sort_by())もself.dfの行の順番を変えるので、保存すればファイルの行順も変わります。並べ替えを「変更」に数えるかどうかも決めておきましょう。 -
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このアプリで中心になるのは文字コードを順に試す with open と、csv / pandas による読み込み。下の表は『退屈なこと』と『Effective』について、出版社や著者が公開している目次から引いた該当章です(各章の解説内容は当サイトでは未確認。「—」は目次の見出しにその語が無いもので、扱っていないとは限りません)。
| この記事で使う書き方 | 退屈なことはPythonにやらせよう 第3版 | Effective Python 第3版 |
|---|---|---|
| csv(CSV の読み書き) | 18章(18.1 CSV形式) | — |
| ファイルの読み書き(open・with) | 10章(ファイルの読み書き) | 10章(項目82 contextlibとwith文) |
| try-except(例外処理) | 4章(4.8 例外処理) | 10章(項目80 try/except/else/finally) |
| class(クラス) | — | 7章(クラスとインタフェース) |
| 内包表記 | — | 6章(内包表記とジェネレータ) |
本が無くても進めます。tkinter の部品はtkinter ウィジェット一覧、文法は基本文法一覧(どちらも当サイト)。2冊の違いと向く人は書籍紹介で比較しています。