Python Flet で、テキストファイルエンコードする画面を作ってみる。

# 背景

visualstudioで開発を進めていて、気づいたらShift-Jisでファイルを作成してしまうことがありました。
いちいちエディタでUTF-8に保存しなおすのが面倒だったので、勉強ついでに、エンコーダーを作ってみました。

# いきなり作成したソース

お試しで作成したものなので、本来考慮すべきエラー処理や、クラス設計は一切してません！苦笑
あくまでも参考なので、そのまま使うことはお勧めしません。

```python
import flet as ft
import os
import chardet

def detect_encoding(file_path):
    """ファイルのエンコーディングを判定する。"""
    with open(file_path, 'rb') as f:
        raw_data = f.read(1024)  # 先頭 1024 バイトを読み込む
    result = chardet.detect(raw_data)
    return result['encoding']

def is_text_file(file_path):
    """ファイルがバイナリかどうかを判定する。
    - NULL バイト (\x00) を含む場合はバイナリと判定する。
    """
    try:
        with open(file_path, 'rb') as f:
            chunk = f.read(1024)  # 先頭 1024 バイトをチェック
        return b'\x00' not in chunk  # NULL バイトがなければテキスト
    except Exception:
        return False  # 例外が発生した場合はバイナリ扱い

def convert_encoding(file_path, target_encoding):
    """指定されたエンコーディングにファイルを変換する。"""
    original_encoding = detect_encoding(file_path)
    
    if not original_encoding:
        return "判定不可", "N/A", "失敗"
    
    try:
        with open(file_path, 'r', encoding=original_encoding) as f:
            content = f.read()
        with open(file_path, 'w', encoding=target_encoding) as f:
            f.write(content)
        return original_encoding, target_encoding, "成功"
    except Exception as e:
        return original_encoding, str(e), "失敗"

def main(page: ft.Page):
    """Fletアプリのメイン処理。"""
    page.title = "エンコード変換アプリ"
    page.window_width = 1200
    page.window_height = 800
    page.window_resizable = True
    page.window_maximizable = True
    page.update()

    tb_folder_path = ft.TextField(label="フォルダパス", width=500, read_only=True)
    btn_select = ft.ElevatedButton("フォルダ選択", on_click=lambda e: file_picker.get_directory_path())

    tb_ext_input = ft.TextField(
        label="対象の拡張子 (カンマ区切り)",
        value="txt,csv,log",
        on_change=lambda e: list_files(tb_folder_path.value),
    )

    cb_select_all = ft.Checkbox(
        label="すべて選択",
        on_change=lambda e: toggle_all_selection(e.control.value)
    )

    dd_encoding = ft.Dropdown(
        label="変換先エンコード",
        options=[
            ft.dropdown.Option("utf-8"),
            ft.dropdown.Option("utf-16"),
            ft.dropdown.Option("shift_jis"),
            ft.dropdown.Option("iso-8859-1"),
            ft.dropdown.Option("euc-jp"),
        ],
        value="utf-8"
    )

    btn_convert = ft.ElevatedButton("変換実行", on_click=lambda e: process_files(), disabled=True)

    grd_file_table = ft.DataTable(
        columns=[
            ft.DataColumn(ft.Text("ファイル名")),
            ft.DataColumn(ft.Text("予測エンコーディング")),
        ],
        rows=[]
    )

    grd_result_table = ft.DataTable(
        columns=[
            ft.DataColumn(ft.Text("ファイル名")),
            ft.DataColumn(ft.Text("元エンコード")),
            ft.DataColumn(ft.Text("新エンコード")),
            ft.DataColumn(ft.Text("結果")),
        ],
        rows=[]
    )

    file_picker = ft.FilePicker(on_result=lambda e: pick_folder_result(e))

    selected_files = {}

    def pick_folder_result(e):
        """フォルダ選択時の処理。"""
        if e.path:
            tb_folder_path.value = e.path
            list_files(e.path)
            btn_convert.disabled = False
            page.update()

    def list_files(folder):
        """指定フォルダ内のテキストファイルを一覧表示（バイナリ除外）。"""
        if not folder or not os.path.exists(folder):
            return

        grd_file_table.rows.clear()
        selected_files.clear()

        if cb_select_all.value:  # すべて選択がONなら全ファイルを対象
            extensions = None
        else:
            extensions = [ext.strip() for ext in tb_ext_input.value.split(",")]

        for file_name in os.listdir(folder):
            file_path = os.path.join(folder, file_name)
            if os.path.isfile(file_path) and is_text_file(file_path):  # 🔹 バイナリを除外
                if extensions is None or any(file_name.endswith(f".{ext}") for ext in extensions):
                    encoding = detect_encoding(file_path) or "不明"
                    grd_file_table.rows.append(
                        ft.DataRow(
                            cells=[
                                ft.DataCell(ft.Text(file_name)),
                                ft.DataCell(ft.Text(encoding)),
                            ]
                        )
                    )

        page.update()

    def toggle_all_selection(select_all):
        """すべて選択・解除 & 拡張子入力欄の有効/無効を切り替え。"""
        tb_ext_input.disabled = select_all  # すべて選択なら拡張子入力を無効化
        list_files(tb_folder_path.value)  # ファイル一覧を更新
        page.update()

    def process_files():
        """選択されたファイルのみエンコード変換。"""
        folder = tb_folder_path.value
        target_encoding = dd_encoding.value

        if not folder or not os.path.exists(folder):
            page.snack_bar = ft.SnackBar(ft.Text("フォルダパスが正しくありません"))
            page.snack_bar.open = True
            page.update()
            return
        
        grd_result_table.rows.clear()
        for file_name in os.listdir(folder):
            file_path = os.path.join(folder, file_name)
            if os.path.isfile(file_path) and is_text_file(file_path):  # バイナリファイル除外
                original_enc, new_enc, result = convert_encoding(file_path, target_encoding)
                grd_result_table.rows.append(
                    ft.DataRow(
                        cells=[
                            ft.DataCell(ft.Text(file_name)),
                            ft.DataCell(ft.Text(original_enc)),
                            ft.DataCell(ft.Text(new_enc)),
                            ft.DataCell(ft.Text(result)),
                        ]
                    )
                )
        
        page.update()
    
    page.overlay.append(file_picker)

    page.add(
        ft.Row([tb_folder_path, btn_select]),
        tb_ext_input,
        cb_select_all,
        ft.Column(controls=[grd_file_table],scroll=ft.ScrollMode.AUTO,expand=True),
        dd_encoding,
        btn_convert,
        ft.Column(controls=[grd_result_table],scroll=ft.ScrollMode.AUTO,expand=True)
    )

ft.app(target=main, view=ft.FLET_APP)  # Fletアプリを起動
```

# 後記

## 使ったコントロール

今回使用したコントロールは、`TextField`、`Checkbox`、`Column`、`Dropdown`、`ElevatedButton`、`DataTable`です。経験上、大体このあたりのコントロールが使えるようになれば、困らない気がしましたので、勉強してみました。

## DataTable単体ではスクロールできないのか・・
意外と苦労したのは、DatTableをスクロールできるようにするところでした。

FletのDataTableをそのまま、使用すると、いい感じに表示してくれていたので、
ビューとしても優秀なのかと思っていたら・・
項目が増えたときに、画面から見切れてしまいました。
どうやら、DataTable単体では、スクロールバーを設定する。という機能はないみたい。

対策として、Columnに、DataTableを入れ子にすることで、解決できています。よかった。


# 表示画面と使い方

1. フォルダ選択ダイアログでフォルダを選択
1. 配下のテキストファイルのエンコードを表示
1. 実行ボタン押下で、選択したエンコードに変換。
1. 以上！