【初心者向け】Fletの基本動作を理解するために簡単なアプリの中身を見る

Featured image of the post

はじめに

Pythonのフレームワーク「Flet」について、公式チュートリアルにあるチャットアプリの仕組みを調べてみた😊

この記事で解決したいこと

Fletのアプリの仕組みをイメージできるようにする!

  • チャットアプリのディレクトリ構成
  • チャットアプリの処理を1つずつ理解

今回のゴール

✅チャットアプリの仕組みを理解する!

Image in a image block

デモページ:https://flet-chat.fly.dev/

公式のGitHub:https://github.com/flet-dev/examples/blob/main/python/tutorials/chat/chat.py

公式のチュートリアル:https://flet.dev/docs/tutorials/python-realtime-chat/

チャットアプリのコードを見てみる

ディレクトリ構成
Image in a image block

💡
なんと1ファイルだけで完結している😳

今から見ていくコード

公式のGitHubで紹介されているチャットアプリのコードを解説する。

コード全文
💡
このあと細かく見ていくので読み飛ばしてもOK!

chat.py(GitHubのコードにコメントを付けたもの)

import flet as ft

# メッセージクラス
class Message():
    def __init__(self, user_name: str, text: str, message_type: str):
        self.user_name = user_name              # ユーザー名
        self.text = text                        # メッセージ
        self.message_type = message_type        # メッセージタイプ

# Rowを継承したコントロール
# チャットに必要なコントロールをまとめる
class ChatMessage(ft.Row):
    # コンストラクタ
    def __init__(self, message: Message):
        # 親クラスのコンストラクタ
        super().__init__()

        self.vertical_alignment="start"
        # デフォルトのコントロールを設定
        self.controls=[
                # アバター
                ft.CircleAvatar(
                    content=ft.Text(self.get_initials(message.user_name)),
                    color=ft.colors.WHITE,
                    bgcolor=self.get_avatar_color(message.user_name),
                ),
                # ユーザー名、メッセージ
                ft.Column(
                    [
                        ft.Text(message.user_name, weight="bold"),
                        ft.Text(message.text, selectable=True),
                    ],
                    tight=True,
                    spacing=5,
                ),
            ]

    # ユーザー名のイニシャルを取得
    def get_initials(self, user_name: str):
        return user_name[:1].capitalize()

    # アバターの背景色を取得
    def get_avatar_color(self, user_name: str):
        colors_lookup = [
            ft.colors.AMBER,
            ft.colors.BLUE,
            ft.colors.BROWN,
            ft.colors.CYAN,
            ft.colors.GREEN,
            ft.colors.INDIGO,
            ft.colors.LIME,
            ft.colors.ORANGE,
            ft.colors.PINK,
            ft.colors.PURPLE,
            ft.colors.RED,
            ft.colors.TEAL,
            ft.colors.YELLOW,
        ]
        # ユーザー名のハッシュ値から使用する背景色を決定
        return colors_lookup[hash(user_name) % len(colors_lookup)]

def main(page: ft.Page):
    page.horizontal_alignment = "stretch"
    page.title = "Flet Chat"

    # ユーザー名を登録(チャットに参加するボタンがクリックされた時のイベントハンドラ)
    def join_chat_click(e):
        # 参加ユーザー名が空の場合
        if not join_user_name.value:
            # エラーメッセージを表示
            join_user_name.error_text = "Name cannot be blank!"
            join_user_name.update()
        else:
            # ユーザー名をセッションに保存し、ダイアログを閉じる
            page.session.set("user_name", join_user_name.value)
            page.dialog.open = False
            # 新しいメッセージのプレフィックスを設定(ユーザー名の後に「:」を付ける」)
            new_message.prefix = ft.Text(f"{join_user_name.value}: ")
            # ログインメッセージを送信
            page.pubsub.send_all(Message(user_name=join_user_name.value, text=f"{join_user_name.value} has joined the chat.", message_type="login_message"))
            page.update()

    # メッセージ送信(メッセージを送信するボタンがクリックされた時のイベントハンドラ)
    def send_message_click(e):
        # 新しいメッセージが空でないことをチェック
        if new_message.value != "":
            # メッセージを送信
            page.pubsub.send_all(Message(page.session.get("user_name"), new_message.value, message_type="chat_message"))
            # メッセージをクリアし、フォーカスを新しいメッセージ入力欄に移す
            new_message.value = ""
            new_message.focus()
            page.update()

    # メッセージを追加する(メッセージが届いた時のイベントハンドラ)
    def on_message(message: Message):
        # チャットメッセージを追加
        if message.message_type == "chat_message":
            m = ChatMessage(message)
        # ログインメッセージを追加
        elif message.message_type == "login_message":
            m = ft.Text(message.text, italic=True, color=ft.colors.BLACK45, size=12)
        # チャットコントロールにメッセージを追加
        chat.controls.append(m)
        page.update()

    # メッセージの受信を購読する(pubsub.send_allで発信されたメッセージを自動的に受け取ることができる)。
    page.pubsub.subscribe(on_message)   # 引数;受信時のイベントハンドラー

    # ユーザー名を入力するダイアログを作成
    join_user_name = ft.TextField(
        label="Enter your name to join the chat",
        autofocus=True,
        on_submit=join_chat_click,
    )
    page.dialog = ft.AlertDialog(
        open=True,
        modal=True,
        title=ft.Text("Welcome!"),
        content=ft.Column([join_user_name], width=300, height=70, tight=True),
        actions=[ft.ElevatedButton(text="Join chat", on_click=join_chat_click)],
        actions_alignment="end",
    )

    # チャットを表示するリストビューを作成
    chat = ft.ListView(
        expand=True,
        spacing=10,
        auto_scroll=True,
    )

    # チャットを入力するテキストフィールドを作成
    new_message = ft.TextField(
        hint_text="Write a message...",
        autofocus=True,
        shift_enter=True,
        min_lines=1,
        max_lines=5,
        filled=True,
        expand=True,
        on_submit=send_message_click,
    )

		# 全てのコントロールをページに追加
    page.add(
        # チャットエリア
        ft.Container(
            content=chat,
            border=ft.border.all(1, ft.colors.OUTLINE),
            border_radius=5,
            padding=10,
            expand=True,
        ),
        # チャット入力エリア
        ft.Row(
            [
                new_message,
                ft.IconButton(
                    icon=ft.icons.SEND_ROUNDED,
                    tooltip="Send message",
                    on_click=send_message_click,
                ),
            ]
        ),
    )

ft.app(port=8550, target=main, view=ft.WEB_BROWSER)

💡
さっそく上から順に見ていく!
メッセージクラスを定義

✅1つのメッセージに必要なデータをまとめたクラス。

# メッセージクラス
class Message():
    def __init__(self, user_name: str, text: str, message_type: str):
        self.user_name = user_name              # ユーザー名
        self.text = text                        # メッセージ
        self.message_type = message_type        # メッセージタイプ

例:3つのメッセージの中身

Image in a image block

チャット用コントロールクラスを定義

✅1回のチャットに必要なコントロール(画面のパーツ)をまとめたクラス。

# Rowを継承したコントロール
# チャットに必要なコントロールをまとめる
class ChatMessage(ft.Row):
    # コンストラクタ
    def __init__(self, message: Message):
        # 親クラスのコンストラクタ
        super().__init__()

        self.vertical_alignment="start"
        # デフォルトのコントロールを設定
        self.controls=[
                # アバター
                ft.CircleAvatar(
                    content=ft.Text(self.get_initials(message.user_name)),
                    color=ft.colors.WHITE,
                    bgcolor=self.get_avatar_color(message.user_name),
                ),
                # ユーザー名、メッセージ
                ft.Column(
                    [
                        ft.Text(message.user_name, weight="bold"),
                        ft.Text(message.text, selectable=True),
                    ],
                    tight=True,
                    spacing=5,
                ),
            ]

    # ユーザー名のイニシャルを取得
    def get_initials(self, user_name: str):
        return user_name[:1].capitalize()

    # アバターの背景色を取得
    def get_avatar_color(self, user_name: str):
        colors_lookup = [
            ft.colors.AMBER,
            ft.colors.BLUE,
            ft.colors.BROWN,
            ft.colors.CYAN,
            ft.colors.GREEN,
            ft.colors.INDIGO,
            ft.colors.LIME,
            ft.colors.ORANGE,
            ft.colors.PINK,
            ft.colors.PURPLE,
            ft.colors.RED,
            ft.colors.TEAL,
            ft.colors.YELLOW,
        ]
        # ユーザー名のハッシュ値から使用する背景色を決定
        return colors_lookup[hash(user_name) % len(colors_lookup)]

具体的にはチャットの1行にあたる部分(メッセージ、ユーザー名、アバター)をまとめている。

Image in a image block


💡
以降はmain関数の中!

ユーザー名を登録するjoin_chat_clickを定義

最初にチャットを開いたときにユーザー名を入力する仕様になっている。

Image in a image block

✅ここで「Join Chat」をクリックしたときこの関数が呼ばれる。(クリックイベントの登録は後述)

		# ユーザー名を登録(チャットに参加するボタンがクリックされた時のイベントハンドラ)
    def join_chat_click(e):
        # 参加ユーザー名が空の場合
        if not join_user_name.value:
            # エラーメッセージを表示
            join_user_name.error_text = "Name cannot be blank!"
            join_user_name.update()
        else:
            # ユーザー名をセッションに保存し、ダイアログを閉じる
            page.session.set("user_name", join_user_name.value)
            page.dialog.open = False
            # 新しいメッセージのプレフィックスを設定(ユーザー名の後に「:」を付ける」)
            new_message.prefix = ft.Text(f"{join_user_name.value}: ")
            # ログインメッセージを送信
            page.pubsub.send_all(Message(user_name=join_user_name.value, text=f"{join_user_name.value} has joined the chat.", message_type="login_message"))
            page.update()

💡
メッセージの送信にはPubSubが使われている。
https://flet.dev/docs/guides/python/pub-sub/

ここではmessage_type="login_message"でログインしたことを通知している。

メッセージを送信するsend_message_clickを定義

チャット画面の右下に送信ボタンがある。

Image in a image block

✅この送信ボタンをクリックしたときにこの関数が呼ばれる。(クリックイベントの登録は後述)

		# メッセージ送信(メッセージを送信するボタンがクリックされた時のイベントハンドラ)
    def send_message_click(e):
        # 新しいメッセージが空でないことをチェック
        if new_message.value != "":
            # メッセージを送信
            page.pubsub.send_all(Message(page.session.get("user_name"), new_message.value, message_type="chat_message"))
            # メッセージをクリアし、フォーカスを新しいメッセージ入力欄に移す
            new_message.value = ""
            new_message.focus()
            page.update()

💡
メッセージの送信にはPubSubが使われている。
https://flet.dev/docs/guides/python/pub-sub/

ここではmessage_type="chat_message"でチャットを送ったことを通知している。

メッセージを追加するon_messageを定義

送信されたメッセージを画面に表示する処理が必要。

Image in a image block

PubSubメッセージを受信したとき(ログインしたとき、チャットが送信されたとき)にこの関数が呼ばれる。

		# メッセージを追加する(メッセージが届いた時のイベントハンドラ)
    def on_message(message: Message):
        # チャットメッセージを追加
        if message.message_type == "chat_message":
            m = ChatMessage(message)
        # ログインメッセージを追加
        elif message.message_type == "login_message":
						# 【注意】このままだと文字が黒くて見ずらい。色を「ft.colors.WHITE」に変えるのがおすすめ。
            m = ft.Text(message.text, italic=True, color=ft.colors.BLACK45, size=12)
        # チャットコントロールにメッセージを追加
        chat.controls.append(m)
        page.update()

		# メッセージの受信を購読する(pubsub.send_allで発信されたメッセージを自動的に受け取ることができる)。
    page.pubsub.subscribe(on_message)   # 引数;受信時のイベントハンドラー

💡
「ログインしたとき」「チャットを送信したとき」pubsub.send_all関数でPubSubメッセージが送信される。
💡
message_typeは「ログイン用」と「チャット用」の2種類どちらかを受信する仕様。受信した種類に合わせて表示する内容を変えている。

ログインしたとき(message_type="chat_message”を受信したとき)

Image in a image block

チャットを送信したとき(message_type="login_message”を受信したとき)

Image in a image block

(↑見やすくするため文字色を変えている)

ユーザー登録ダイアログを作成

アプリ起動時にユーザー名の入力ダイアログが表示される。

Image in a image block

✅このダイアログを作成する

		# ユーザー名を入力するダイアログを作成
    join_user_name = ft.TextField(
        label="Enter your name to join the chat",
        autofocus=True,
        on_submit=join_chat_click,
    )
    page.dialog = ft.AlertDialog(
        open=True,
        modal=True,
        title=ft.Text("Welcome!"),
        content=ft.Column([join_user_name], width=300, height=70, tight=True),
        actions=[ft.ElevatedButton(text="Join chat", on_click=join_chat_click)],
        actions_alignment="end",
    )

チャットを表示する部分を作成

ユーザー名登録後、チャット画面が表示される。

Image in a image block

✅この画面上部のチャットを表示する部分。

		# チャットを表示するリストビューを作成
    chat = ft.ListView(
        expand=True,
        spacing=10,
        auto_scroll=True,
    )

メッセージ入力欄を作成
Image in a image block

✅同様に、画面下部のチャット入力欄。

		# チャットを入力するテキストフィールドを作成
    new_message = ft.TextField(
        hint_text="Write a message...",
        autofocus=True,
        shift_enter=True,
        min_lines=1,
        max_lines=5,
        filled=True,
        expand=True,
        on_submit=send_message_click,
    )

全てのコントロールをページに追加
Image in a image block

✅先ほど作成した「チャットエリア」と「チャット入力エリア」と「送信ボタン」をページに追加する。

		# 全てのコントロールをページに追加
    page.add(
        # チャットエリア
        ft.Container(
            content=chat,
            border=ft.border.all(1, ft.colors.OUTLINE),
            border_radius=5,
            padding=10,
            expand=True,
        ),
        # チャット入力エリア
        ft.Row(
            [
                new_message,
                ft.IconButton(
                    icon=ft.icons.SEND_ROUNDED,
                    tooltip="Send message",
                    on_click=send_message_click,
                ),
            ]
        ),
    )

ポイントまとめ