PR

【無料配布】Flet 1.0テンプレート集|基本構成+Excel取込対応の社内ツールひな形

Python

Fletのアイコン、レイアウト、画面遷移と、3回にわたってFlet 1.0で見つけた「AIが古い書き方を出してエラーなく壊れる」パターンを紹介してきました。

🥕 第1回 【保存版】Fletアイコン一覧&使い方|バージョンで名前が変わってAIが誤爆する理由 Flet 1.0の破壊的変更とアイコンAPIの変遷、AIが古い書き方を出す理由をまとめました。 → 🥕 第2回 Fletのレイアウトが崩れてもエラーが出ない理由|Row/Column・expand・ResponsiveRowで直す方法 AIがよくやるexpand忘れ・ネスト崩壊のパターンと、ResponsiveRow・scrollを使った直し方をまとめました。 → 🥕 第3回 Fletで画面遷移が動かない理由|page.go廃止とft.View引数順の罠、TemplateRouteの使い方 page.go廃止・ft.View引数順の罠・TemplateRouteの使い方を実例で整理しました。 →

毎回「正しい書き方」を確認するところから始めるのは手間なので、これまでの知見をあらかじめ組み込んだテンプレートを2種類作り、GitHubで無料配布することにしました。

大事な前置きですが、これは「これさえあれば業務アプリが完成する」という完成品ではありません。画面遷移・レイアウト・Excel連携という骨組みだけを最短で動かして触ってみるための出発点で、データの永続化や認証、入力検証といった各社固有の要件は含まれていません。そこから先は、このテンプレートをそのままAIに読み込ませて「ここに機能を足して」と一緒に育てていく、いわば土台としての位置づけです。

この記事で分かること

  • 基本構成テンプレート(画面遷移+レイアウトの最小構成)の中身
  • 社内ツール開発テンプレート(サイドバー+ダッシュボード+Excel取込)の中身
  • FilePickerで新たに見つかった2つの罠(action=ft.PickFiles()・allowed_extensions)
  • メッセージ表示はpage.open()ではなくpage.show_dialog()を使う件
  • expand=Trueだけではスクロールしない問題と直し方
  • テンプレートをGitHubからダウンロードする方法

スポンサーリンク

基本構成テンプレート(basic):画面遷移とレイアウトの最小構成

これからFletで何か作る人が、最初にコピーするためのひな形です。ホーム画面ともう1画面だけの構成で、これまでの記事で確認した「正しい書き方」だけを押さえています。

"""Flet 基本構成テンプレート"""
import flet as ft


def layout_sample() -> ft.Column:
    """Row + expand と ResponsiveRow の最小サンプル。"""

    def card(label: str, color: str) -> ft.Container:
        return ft.Container(
            col={"xs": 12, "sm": 6, "md": 4},
            bgcolor=color,
            border_radius=10,
            padding=14,
            height=70,
            content=ft.Text(label, size=13),
        )

    return ft.Column(
        [
            ft.Text("Row + expand の例", size=15, weight=ft.FontWeight.BOLD),
            ft.Row(
                [
                    ft.Container(width=110, height=70, bgcolor=ft.Colors.INDIGO_100, border_radius=10, padding=10,
                                 content=ft.Text("固定幅\nwidth=110", size=12)),
                    ft.Container(expand=True, height=70, bgcolor=ft.Colors.INDIGO_50, border_radius=10, padding=10,
                                 content=ft.Text("expand=True(残り幅いっぱいに広がる)", size=12)),
                ],
                spacing=12,
            ),
            ft.Container(height=20),
            ft.Text("ResponsiveRow の例(画面幅を狭めると1列→2列→3列に変わります)", size=15, weight=ft.FontWeight.BOLD),
            ft.ResponsiveRow(
                [
                    card("xs:12 / sm:6 / md:4", ft.Colors.GREEN_100),
                    card("xs:12 / sm:6 / md:4", ft.Colors.BLUE_100),
                    card("xs:12 / sm:6 / md:4", ft.Colors.ORANGE_100),
                ],
                spacing=12,
                run_spacing=12,
            ),
        ],
        spacing=10,
    )


def main(page: ft.Page):
    page.title = "Flet 基本構成テンプレート"
    page.theme = ft.Theme(color_scheme_seed=ft.Colors.INDIGO)

    def route_change(e=None):
        page.views.clear()
        page.views.append(
            ft.View(
                route="/",
                controls=[
                    ft.AppBar(title=ft.Text("ホーム"), bgcolor=ft.Colors.SURFACE),
                    ft.Container(height=8),
                    ft.Text("Flet 基本構成テンプレートへようこそ", size=18, weight=ft.FontWeight.BOLD),
                    ft.Text("このテンプレートは Flet 1.0 系で動作確認済みです。", size=13, color=ft.Colors.GREY_600),
                    ft.Container(height=20),
                    layout_sample(),
                    ft.Container(height=20),
                    ft.Button("このテンプレートについて", icon=ft.Icons.INFO_OUTLINE, on_click=lambda e: page.navigate("/about")),
                ],
                padding=24,
                scroll=ft.ScrollMode.AUTO,
            )
        )
        if page.route == "/about":
            page.views.append(
                ft.View(
                    route="/about",
                    controls=[
                        ft.AppBar(title=ft.Text("About"), bgcolor=ft.Colors.SURFACE),
                        ft.Container(height=8),
                        ft.Text("このテンプレートについて", size=18, weight=ft.FontWeight.BOLD),
                        ft.Text(
                            "route= / controls= を明示した ft.View と、page.navigate() による\n"
                            "画面遷移のサンプルです。新しい画面を増やす場合は、この route_change()\n"
                            "の中に if page.route == \"...\" のブロックを追加してください。\n\n"
                            "ホーム画面のRow(固定幅+expand=True)とResponsiveRowは、そのまま\n"
                            "コピーして他の画面のレイアウトの土台としても使えます。",
                            size=13,
                        ),
                        ft.Container(height=16),
                        ft.Button("ホームへ戻る", icon=ft.Icons.ARROW_BACK, on_click=lambda e: page.navigate("/")),
                    ],
                    padding=24,
                )
            )
        page.update()

    page.on_route_change = route_change
    route_change()


ft.run(main)
基本構成テンプレートのホーム画面
▲ ホーム画面。Row+expandの例とResponsiveRowの例を両方載せている
基本構成テンプレートのAbout画面
▲ 「このテンプレートについて」から遷移するAbout画面

ft.View(route=, controls=)のキーワード引数、page.navigate()、route_change()の直接呼び出しといった、これまでの記事で確認した書き方をすべて反映済みです。ブラウザの幅を変えると、ResponsiveRowの部分が実際に列数を変えて折り返します。

幅の広い画面でのResponsiveRow(3列)
▲ 幅の広い画面では3列
幅の狭い画面でのResponsiveRow(1列)
▲ 幅の狭い画面では1列に変わる

スポンサーリンク

社内ツール開発テンプレート(business-dashboard):業務アプリの骨組みになる出発点

サイドバー付きの複数ページ構成、ResponsiveRowによるダッシュボード、ft.TemplateRouteを使った顧客一覧→詳細ページ、Excelファイルの取り込みまでを組み合わせたテンプレートです。

繰り返しになりますが、これは業務アプリの完成品ではありません。画面遷移・レイアウト・Excel連携という骨組みを最短で動かして触ってみるための出発点で、データベースへの永続化、ログイン認証、入力値のバリデーションといった各社固有の要件はあえて含んでいません。まずこの状態を触って構造をつかんでから、AIに「この骨組みに認証機能を足して」「このデータをSQLiteに保存するようにして」のように、必要な機能を一緒に積み増していく使い方を想定しています。

画面構成はダッシュボード・顧客一覧・顧客詳細の3つです。

社内ツールテンプレートのダッシュボード
▲ ダッシュボード画面。サイドバーは幅固定、メインエリアはexpand=True
社内ツールテンプレートの顧客一覧
▲ 顧客一覧画面
社内ツールテンプレートの顧客詳細
▲ 顧客詳細画面(ID:2)。ft.TemplateRouteで動的に解決している

顧客詳細のルーティング部分は、画面遷移編で紹介したft.TemplateRouteをそのまま使っています。

troute = ft.TemplateRoute(page.route)
if troute.match("/customers/:id"):
    page.views.append(
        build_customer_detail_view(page, page.route, troute.id, customers)
    )

レイアウトはRow([サイドバー(幅固定), メインエリア(expand=True)])という形を基本にしています。サイドバーの幅は業務ツールらしく固定にし、メインエリアだけを画面幅に応じて伸縮させる考え方は、レイアウト崩壊編で紹介した内容そのものです。


スポンサーリンク

Excel取り込み機能:実務のExcelはきれいな1行ヘッダーではない

顧客一覧画面の「Excelから読み込む」ボタンから、月別の予算・実績表(sample_sales.xlsx)を選択すると、得意先ごとに年間の予算・実績を集計して画面のデータを差し替えます。

Excel取り込み後のダッシュボード
▲ Excel取り込みロジックを実行した後のダッシュボード(10社、合計予算・実績・達成率が更新されている)
Excel取り込み後の顧客一覧
▲ 同じく取り込み後の顧客一覧(10社分の実績・予算)

この2枚は、ボタンをクリックしてファイル選択ダイアログを操作する一連の流れそのものではなく、Excelを読み込むロジック本体(load_customers_from_excel())を実際の業務Excelファイルに対して実行した結果を画面に反映させたものです。読み込みロジック自体は本物のExcel(月別の予算実績表)に対して実行し、合計予算・合計実績がファイル自身の「合計」行の数値と一致することまで確認しています。

このテンプレートの土台になった実際の業務Excelは、AIに「Excelを読み込む処理を書いて」と頼んだときに想定されがちな、きれいな1行ヘッダーの表ではありませんでした。

  • タイトル行の下、見出しが2段に分かれている(月のグループ見出し行+予算/実績などの列見出し行)
  • 「4月」(全角)と「2月」「3月」(半角)のように表記が揺れている
  • 「上期」「下期」「25年度」のような集計・比較列が、実データの月別列に混ざって並んでいる

列名の文字列を完全一致で探すパーサーだと、この表記ゆれだけで簡単に壊れます。そこでdata.pyでは、集計列らしきラベル(「上期」「下期」、”年度”を含む文字列)を先に除外し、残った列を「月のグループ見出し→予算列→実績列」という並び順(位置)で機械的に読み取る方式に変えています。

_NON_MONTH_LABELS = ("上期", "下期")


def _is_month_group(label) -> bool:
    if not isinstance(label, str):
        return False
    if label in _NON_MONTH_LABELS:
        return False
    if "年度" in label:
        return False
    return True

自社のExcelフォーマットが異なる場合は、この判定部分と、月グループの次の列を予算・その次を実績として読み取っている位置を、実際のファイルに合わせて調整してください。


スポンサーリンク

FilePickerで新たに見つかった2つの罠

Excel取り込みボタンを作る過程で、アイコン編・レイアウト崩壊編・画面遷移編では出てこなかった、FilePicker固有の罠が2つ見つかりました。

1つ目は、file_picker.pick_files()をon_clickから直接呼ぶと、ダイアログが開かないことがある点です。pick_files()は非同期メソッドで、同期のon_clickからawaitなしで直接呼ぶと「ユーザー操作の直後」というブラウザ側のタイミング要件を外れてしまいます。公式ドキュメントにも明記されている挙動で、宣言的なaction=指定に変えると解決します。

ft.Button(
    "Excelから読み込む",
    icon=ft.Icons.UPLOAD_FILE,
    action=ft.PickFiles(
        file_picker,
        file_type=ft.FilePickerFileType.CUSTOM,
        allowed_extensions=["xlsx"],
        allow_multiple=False,
    ),
)

2つ目は、allowed_extensionsだけを指定しても、拡張子フィルタが効かないことがある点です。file_type=ft.FilePickerFileType.CUSTOMを併せて指定しないと、allowed_extensionsの指定自体が無視されます。上のコード例のように、この2つはセットで書く必要があります。

なお、テンプレート本体のft.PickFiles(...)にはinitial_directory=も指定しています。ファイル選択ダイアログの初期表示フォルダをアプリのディレクトリに揃えておくと、sample_sales.xlsxのようなサンプルファイルを毎回探し回らずに済むので、地味に効く小さな工夫です。


スポンサーリンク

メッセージ表示にはpage.open()ではなくpage.show_dialog()を使う

ファイルが選択されなかったときや、読み込みに失敗したときのメッセージ表示にも、Flet 1.0での変更点が1つあります。

def on_import_result(e: ft.FilePickerResultEvent):
    if not e.files:
        page.show_dialog(ft.SnackBar(ft.Text("ファイルが選択されませんでした")))
        return
    try:
        imported = load_customers_from_excel(e.files[0].path)
    except Exception as ex:
        page.show_dialog(ft.SnackBar(ft.Text(f"読み込みに失敗しました: {ex}")))
        return
    customers.clear()
    customers.update(imported)
    route_change()

page.open(snack_bar)という書き方を見かけることがありますが、page.open()はFlet 1.0には存在しません。SnackBarのようなダイアログ系の部品を表示する場合はpage.show_dialog()を使います。


スポンサーリンク

expand=Trueだけではスクロールしない

Excel取り込み後、得意先が10社に増えた状態でウィンドウを縦に狭くすると、Container(expand=True)だけの実装では中身があふれてもスクロールバーが出ず、下の方の項目が見えなくなる不具合がありました。

expand=Trueは「親の余った領域いっぱいに広がる」という指定であって、あふれた分を自動でスクロール可能にする指定ではありません。views.pyの共通レイアウト部分では、メインエリアの中身をさらにft.Column(expand=True, scroll=ft.ScrollMode.AUTO)で包む形に直しています。

ft.Container(
    expand=True,
    padding=24,
    content=ft.Column(
        [content],
        expand=True,
        scroll=ft.ScrollMode.AUTO,
    ),
),

こうしておくと、380px程度まで縦を狭めても、サイドバーは固定のままメインエリアだけがマウスホイールやトラックパッドでスクロールできるようになります。

縦380pxまで狭めた状態でメインエリアだけがスクロールする様子
▲ ウィンドウを縦380pxまで狭めた状態。右端に薄いスクロールバーが出て、メインエリアだけがスクロールしている

これもレイアウト崩壊編で扱ったexpandの挙動の続きの話で、「広がる」ことと「あふれた分を見られるようにする」ことは別の指定だと覚えておくと役立ちます。


スポンサーリンク

動かないときに確認すること(Linux+Zenity)

Fletの公式情報によると、Linuxでデスクトップアプリとして実行する場合、FilePickerの動作にZenityが必要です。ブラウザで実行する場合は不要ですが、Linux環境でデスクトップウィンドウとして動かしていて「ボタンを押してもファイル選択ダイアログが開かない」場合は、sudo apt-get install zenityのようなコマンドでZenityがインストールされているか確認してください。


スポンサーリンク

「重い」問題について

Flet製のアプリを実行ファイル化(flet build)する場合、いくつか気をつけておきたい点があります。手元の検証環境でflet build linuxを試したところ、5分弱経っても完了しないほど時間がかかりました。初回ビルドはFlutter本体を巻き込む処理のため、相応に時間がかかるようです。Android SDKが手元の環境になかったため、flet build apkのビルドまでは検証できていません。

exe化・パッケージ化したアプリの起動速度や配布サイズについては、ビルドオプションによって結果が変わるという情報もネット上で見かけます。実務で配布する場合は、自社の環境で実際にビルドして起動時間やファイルサイズを確認し、公式ドキュメントのflet buildのオプション一覧もあわせて確認することをおすすめします。


スポンサーリンク

AIへの追加開発のコツ

このテンプレートを土台にAIへ「画面を追加して」「認証機能を足して」と頼むと、AIは既存コードの書き方を参考にする傾向があるため、page.go()や位置引数のft.View(...)のような古い書き方を持ち出してくる可能性が下がります。それでも念のため、プロンプトに「Flet 1.0の書き方で、ft.Viewはroute=とcontrols=を明示して」のように具体的に指定しておくと安全です。

サイドバーのメニューを増やしたい場合は、views.pyのMENU_ITEMSにタプルを追加し、main.pyのroute_change()に対応する分岐を追加するだけで済むように作ってあります。まずはこの単純な機能追加をAIと一緒に試してみると、テンプレート全体の構造がつかみやすいはずです。


スポンサーリンク

テンプレートのダウンロード

基本構成テンプレートと社内ツール開発テンプレートの両方を、GitHubリポジトリとして無料公開しています。MITライセンスなので、自由に改変・再配布していただいて構いません。

🔗 GitHubでテンプレートを見る/ダウンロード
cd templates/basic  # または templates/business-dashboard\npip install -r requirements.txt\npython main.py

スポンサーリンク

よくある質問(FAQ)

Qこのテンプレートだけで業務アプリとして使えますか?

Aそのままでは使えません。データの永続化(ファイルやデータベースへの保存)、ログイン認証、入力値のチェックといった実務に必須の機能は含まれていないためです。画面遷移・レイアウト・Excel連携という骨組みを最短で確認できる出発点として、そこから先を自社の要件に合わせて積み増していく前提のテンプレートです。

QFilePickerのボタンを押しても何も起きません。原因は何が考えられますか?

Apick_files()をon_clickから直接呼んでいないか確認してください。action=ft.PickFiles(...)という宣言的な指定に変える必要があります。Linux環境の場合はZenityが未インストールの可能性もあります。

Q拡張子を絞ったつもりなのに、他の形式のファイルも選べてしまいます。

Aallowed_extensionsだけでなく、file_type=ft.FilePickerFileType.CUSTOMを併せて指定してください。この指定がないとallowed_extensionsが無視されます。

Q自社のExcelフォーマットに合わせて調整するには、どこを見ればいいですか?

Atemplates/business-dashboard/data.pyのload_customers_from_excel()が読み込みロジック本体です。見出し行の位置や、月データと集計列を区別する_is_month_group()の判定条件を、自社のファイル構成に合わせて書き換えてください。

Qウィンドウを小さくすると画面の下の方が見えなくなります。

Aスクロールさせたい範囲にscroll=ft.ScrollMode.AUTOを指定してください。expand=Trueだけでは、あふれた分を自動でスクロール可能にはしてくれません。


スポンサーリンク

まとめ+シリーズ振り返り

この記事では、Fletのアイコン・レイアウト・画面遷移で見つけた知見をまとめた2種類のテンプレートと、その過程で新たに見つかった罠を紹介しました。

  • 基本構成テンプレート・社内ツール開発テンプレートは、完成品ではなく骨組みを確認するための出発点。データの永続化・認証・入力検証は含まれていない
  • FilePickerはaction=ft.PickFiles(...)で宣言的に指定し、allowed_extensionsはfile_type=CUSTOMとセットで使う
  • メッセージ表示はpage.open()ではなくpage.show_dialog()を使う
  • expand=Trueは「広がる」指定であって「あふれた分をスクロール可能にする」指定ではない
  • 実務のExcelはきれいな1行ヘッダーとは限らない。位置ベースの判定にしておくと表記ゆれに強くなる

アイコン・レイアウト・画面遷移・テンプレート配布と4回にわたって、Fletのバージョンアップでエラーなく壊れるパターンを追いかけてきました。テンプレートを土台に、ぜひAIと一緒に自社向けの機能を育ててみてください。



ニンジン🥕アイコン

Follow me on X

本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕

@ninjin_py_vba をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket
まとめハブ
Python
スポンサーリンク

コメント