tkinterでPythonファイルを選んで表示するGUIを作る
前回は、Pythonコードが増えていくと、だんだん自分でも管理しづらくなるという話をしました。
最初は1本のスクリプトで十分です。
CSVを読む。
Excelを出力する。
Accessに接続する。
画像を加工する。
フォルダを走査する。
ファイルをコピーする。
ひとつひとつは小さな処理でも、増えていくうちに、
「これはどこに置けばいいんだろう」
「この処理は分けたほうがいいのかな」
「この名前で後からわかるかな」
「似たようなコードが前にもあった気がする」
という状態になっていきます。
そこでこのシリーズでは、自分のPythonコードを読み取り、
- 何系の処理なのか
- どこに置くべきか
- どう分けるべきか
- 何をYAML化できるか
- クラス化や継承の候補があるか
- 整理後に壊れていないか
を確認できる Python設計ナビ を作っていきます。
ただし、いきなり賢いAdvisorは作りません。
第2話の今回は、まず入口を作ります。
今回作るもの
今回作るのは、Pythonファイルを選んで、その中身を画面に表示するだけの小さなGUIです。
完成イメージはこんな感じです。
[ Pythonファイルを選択 ] [ 解析する ]
選択ファイル:
sample/sample_script.py
--------------------------------
ここにPythonコードを表示
--------------------------------
解析結果:
行数: 25
文字数: 820
まだ本格的な解析はしません。
importを抜き出すことも、関数一覧を作ることも、クラスを検出することも、Advisorコメントを出すことも、今回はまだやりません。
今回はあくまで、
Pythonファイルを選ぶ
↓
中身を表示する
↓
簡単な情報を表示する
ここまでです。
でも、この入口ができると、次回から ScriptAnalyzer を接続できます。
つまり今回は、Python設計ナビの「目」と「画面」を作る回です。
プロジェクトフォルダを用意する
まずは、1つのプロジェクトフォルダを作ります。
このシリーズでは、基本的に 1つのフォルダを基準にして、相対パスで育てていく 方針にします。
たとえば、最初はこのくらいで十分です。
python_design_navi/
├─ main.py
├─ sample/
│ └─ sample_script.py
├─ src/
│ └─ gui/
│ └─ app.py
├─ rules/
├─ output/
└─ logs/
今回は、まず main.py と src/gui/app.py を使います。
最初から完璧なフォルダ構成にする必要はありません。
むしろ、この連載では、
最初は小さく作る
↓
機能が増える
↓
置き場所に迷う
↓
Advisorが必要になる
↓
フォルダ構成を育てる
という流れを大事にします。
設計は、最初から完成させるものではなく、動くものを観測しながら育てるものです。
なぜGUIから作るのか
先にAnalyzerを作ってもよいのですが、今回はGUIから作ります。
理由は単純です。
自分のコードを選んで、画面に表示できるだけで、ツールを作っている実感が出るからです。
コマンドラインで動く解析ツールも便利ですが、今回作りたいのは「設計ナビ」です。
ファイルを選び、結果を見て、Advisorの助言を読む。
そのためには、最初に画面があったほうがわかりやすいです。
また、GUIは状態を持ちます。
たとえば、
- 今選んでいるファイル
- 表示しているコード
- 解析結果
- Advisorコメント
こういう情報を画面側で覚えておく必要があります。
そのため、GUI部分はクラスにすると扱いやすくなります。
今回は GuiApp というクラスを作ります。
main.pyを作る
まず、プロジェクトの入口になる main.py を作ります。
from src.gui.app import GuiApp
def main():
app = GuiApp()
app.run()
if __name__ == "__main__":
main()main.py はできるだけ薄くします。
ここに画面処理を全部書いてしまうと、後で大きくなります。
今回の main.py の役割は、
GuiAppを作る
↓
runする
これだけです。
このように入口を薄くしておくと、後で機能が増えても整理しやすくなります。
GuiAppクラスを作る
次に src/gui/app.py を作ります。
まずは、最小構成のGUIを作ります。
import tkinter as tk
from tkinter import filedialog, messagebox
from pathlib import Path
class GuiApp:
def __init__(self):
self.root = tk.Tk()
self.root.title("Python設計ナビ")
self.root.geometry("900x600")
self.selected_file_path: Path | None = None
self._build_widgets()
def _build_widgets(self):
top_frame = tk.Frame(self.root)
top_frame.pack(fill=tk.X, padx=10, pady=10)
self.select_button = tk.Button(
top_frame,
text="Pythonファイルを選択",
command=self.select_file,
)
self.select_button.pack(side=tk.LEFT)
self.analyze_button = tk.Button(
top_frame,
text="解析する",
command=self.analyze_file,
)
self.analyze_button.pack(side=tk.LEFT, padx=10)
self.file_label = tk.Label(
self.root,
text="選択ファイル:未選択",
anchor="w",
)
self.file_label.pack(fill=tk.X, padx=10)
self.code_text = tk.Text(self.root, wrap=tk.NONE)
self.code_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.result_label = tk.Label(
self.root,
text="解析結果:未実行",
anchor="w",
justify=tk.LEFT,
)
self.result_label.pack(fill=tk.X, padx=10, pady=10)
def select_file(self):
file_path = filedialog.askopenfilename(
title="Pythonファイルを選択",
filetypes=[("Python files", "*.py"), ("All files", "*.*")]
)
if not file_path:
return
self.selected_file_path = Path(file_path)
self.file_label.config(text=f"選択ファイル:{self.selected_file_path}")
self.load_code(self.selected_file_path)
def load_code(self, file_path: Path):
try:
code = file_path.read_text(encoding="utf-8")
except UnicodeDecodeError:
try:
code = file_path.read_text(encoding="cp932")
except Exception as e:
messagebox.showerror("読み込みエラー", str(e))
return
except Exception as e:
messagebox.showerror("読み込みエラー", str(e))
return
self.code_text.delete("1.0", tk.END)
self.code_text.insert(tk.END, code)
self.result_label.config(text="解析結果:未実行")
def analyze_file(self):
if self.selected_file_path is None:
messagebox.showwarning("未選択", "先にPythonファイルを選択してください。")
return
code = self.code_text.get("1.0", "end-1c")
line_count = len(code.splitlines())
char_count = len(code)
result_text = (
"解析結果:\n"
f"行数: {line_count}\n"
f"文字数: {char_count}"
)
self.result_label.config(text=result_text)
def run(self):
self.root.mainloop()
これで、最小限のGUIが動きます。
今回のコードの役割
今回の GuiApp には、いくつかの役割があります。
__init__
画面の初期化
_build_widgets
ボタンやテキスト欄を作る
select_file
ファイル選択ダイアログを開く
load_code
選んだPythonファイルを読み込む
analyze_file
簡単な解析結果を表示する
run
GUIを開始する
まだ小さなクラスですが、すでに「状態を持つ」形になっています。
特に重要なのはここです。
self.selected_file_path: Path | None = None
これは、現在選択されているPythonファイルのパスを保持するための変数です。
GUIでは、ボタンを押したタイミングと、解析するタイミングが別になります。
そのため、
ファイルを選んだ
↓
そのファイルパスを覚えておく
↓
あとで解析ボタンを押したら、そのファイルを対象にする
という流れが必要になります。
こういう「状態」を扱うとき、クラスはかなり便利です。
関数だけでも作れますが、選択ファイル、表示テキスト、解析結果、画面部品が増えてくると、どこに何を置いているのかわかりにくくなります。
だから今回は、最初から GuiApp クラスとして作っています。
文字コードについて
今回の load_code では、まず utf-8 で読み込みます。
code = file_path.read_text(encoding="utf-8")
ただし、実務で使っているPythonファイルやCSV関連スクリプトでは、Windows環境の都合で cp932 が出てくることがあります。
そのため、utf-8 で読めなかった場合は cp932 でも試しています。
except UnicodeDecodeError:
try:
code = file_path.read_text(encoding="cp932")
本格的には、文字コード自動判定や設定ファイル化も考えられます。
たとえば将来的には、
file_read:
encodings:
- utf-8
- cp932
- shift_jis
のように、YAMLへ逃がすこともできます。
ただし、今回はそこまでやりません。
まずは動くことを優先します。
解析ボタンはまだ仮のAnalyzer
今回の analyze_file は、まだ本格的なAnalyzerではありません。
やっているのは、行数と文字数を数えるだけです。
line_count = len(code.splitlines())
char_count = len(code)
でも、これで十分です。
なぜなら、GUI側から見ると、
ファイルを選ぶ
↓
コードを読む
↓
解析ボタンを押す
↓
結果が表示される
という流れが完成しているからです。
次回は、この analyze_file の中身を直接大きくするのではなく、別のクラスへ分けていきます。
それが ScriptAnalyzer です。
たとえば次回以降は、こういう形になります。
from src.analyzers.script_analyzer import ScriptAnalyzer
analyzer = ScriptAnalyzer(code)
result = analyzer.analyze()
GUIは画面を担当する。
Analyzerはコード解析を担当する。
このように、役割を分けていきます。
今回はまだAdvisorを作らない
Python設計ナビという名前なので、早くAdvisorを出したくなります。
でも今回はまだ作りません。
今回の目的は、Advisorにコードを渡すための入口を作ることです。
GuiApp
↓
ScriptAnalyzer
↓
RuleMatcher
↓
CodeAdvisor
この流れのうち、今回は一番上の GuiApp を作りました。
ここで無理にAdvisorまで入れると、1記事で扱う内容が多くなりすぎます。
このシリーズでは、少しずつ育てます。
一度に完成形へ飛ばない。
動くものを作り、観測し、必要になったら分ける。
これも設計ナビの考え方です。
今回できたこと
今回できたことを整理すると、こうです。
できたこと:
- tkinterでGUIを表示できた
- Pythonファイルを選択できた
- 選んだファイルのパスを保持できた
- Pythonファイルの中身を画面に表示できた
- 解析ボタンを押して、行数と文字数を表示できた
逆に、まだやっていないことはこれです。
まだやらないこと:
- import一覧の抽出
- 関数一覧の抽出
- クラス一覧の抽出
- read_csvなどの命令カウント
- CSV系・DB系・Excel系などの分類
- Advisorコメントの表示
- YAMLとの連携
この「まだやらないこと」を明確にしておくのは大事です。
機能を増やしすぎると、またコードが迷子になります。
Python設計ナビを作っているコード自体が迷子になっては意味がありません。


