0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

背景

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

いきなり作成したソース

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

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アプリを起動

後記

使ったコントロール

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

DataTable単体ではスクロールできないのか・・

意外と苦労したのは、DatTableをスクロールできるようにするところでした。

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

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

表示画面と使い方

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?