SWELL公式サイトへ 詳しくはこちら

Python設計ナビ 第2話

  • URLをコピーしました!
目次

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.pysrc/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設計ナビを作っているコード自体が迷子になっては意味がありません。

よかったらシェアしてね!
  • URLをコピーしました!
目次