📘

Flatを少しでも理解するためにチュートリアルをやる - チャット

2024/05/09に公開

Flatとは

Fletは、Pythonを使ってWebアプリ、デスクトップアプリ、モバイルアプリを簡単に開発できるフレームワークです。

https://flet.dev/

作るもの

https://youtu.be/t8GH3Mvl5p0

チャットの作成

チュートリアルを参考にチャットアプリをつくって行きます。

実行環境

  • flet Ver.0.21.2
  • Python Ver.3.10.11

ライブラリのインストール

pipを使いfletをインストールします。

pip install flet


メッセージの表示

ユーザのメッセージ送信とメッセージ履歴表示の基礎を作ります。

このプログラムには以下のコントロールを使っています

  • Text チャットを表示するテキスト
  • TextField チャットメッセージの記入
  • ElevatedButton メッセージの送信ボタン
  • Column メッセージ(テキスト)を垂直に表示をする
  • Row TextFieldとElevatedButtonを水平に表示する

全体のコード:01.py

01.py
import flet as ft

def main(page: ft.Page):
    # チャットメッセージを表示するコントロール
    chat = ft.Column()

    # 新規メッセージ入力欄
    new_message = ft.TextField()

    # 送信ボタンクリック時の処理
    def send_click(e):
        # 新しいメッセージを`chat`コントロールに追加
        chat.controls.append(ft.Text(new_message.value))

        # TextField(メッセージ)を空にする
        new_message.value = ""

        # 画面を更新
        page.update()

    page.add(
        chat, ft.Row(controls=[new_message, ft.ElevatedButton("Send", on_click=send_click)])
    )

ft.app(main, view=ft.AppView.WEB_BROWSER)

ユーザがメッセージを送信すると、send_clickを呼び出すイベントが発生します。
入力されたメッセージがchatに追加されます。
そしてメッセージ入力欄は空になります。

view=ft.AppView.WEB_BROWSER:Webから使えるようになります。

実行画面


チャットのやり取り

前のチャットアプリでは、複数のブラウザを開くと各タブで独立したチャットの履歴が保持されていました。

FeltのPubSubを使うことで、リアルタイムに別々のブラウザ間でメッセージのやり取りをすることが可能になります。

  1. メッセージ受信処理の登録
    • ユーザがメッセージの受信するための処理を登録します。
    • on_message関数はメッセージが送られてきたときに呼ばれます。
page.pubsub.subscribe(on_message)
  1. メッセージ受信処理
    • 送られてきたメッセージを、チェット履歴に登録します。
def on_message(message: Message):
    # 送られてきたメッセージをchatに追加
    chat.controls.append(ft.Text(f"{message.user}: {message.text}"))
    page.update()
  1. メッセージ送信処理
    • ユーザが送信ボタンを押したときに呼ばれます。
    • 送信されたメッセージをMessageオブジェクトに変換し、page.pubsub.send_all()を使いすべてのユーザに配信します

全体のコード:02.py

02.py
import flet as ft

class Message():
    def __init__(self, user: str, text: str):
        self.user = user
        self.text = text

def main(page: ft.Page):
    page.title = 'チャット'
    
    chat = ft.Column()
    new_message = ft.TextField()
    
    def on_message(message: Message):
        # 送られてきたメッセージをchatに追加
        chat.controls.append(ft.Text(f"{message.user}: {message.text}"))
        page.update()
        
    # 全クライアントに対してメッセージを配信するためのPub/Subチャンネルを登録
    page.pubsub.subscribe(on_message)
    
    def send_click(e):
        # メッセージを送信 (Pub/Subを利用)
        page.pubsub.send_all(Message(user=page.session_id, text=new_message.value))
        new_message.value = ''
        page.update()
        
    page.add(
        chat, ft.Row(controls=[new_message, ft.ElevatedButton("Send", on_click=send_click)])
    )


ft.app(target=main, view=ft.AppView.WEB_BROWSER)

実行画面

  • チャットのやり方
    1. ブラウザを2つ用意します。
    2. チャット画面があるブラウザからURLをコピー
    3. 何もないブラウザにURLをペースト

https://youtu.be/wWkZ3mLNHPU

注意

タブを複製すると複製元のブラウザのセッションIDが同じになります。
セッションIDが同じなので片方のブラウザが同期させません。

https://youtu.be/ZBAXz9Bc1cM


ユーザ名設定

前に作成したチャットアプリには基本機能ができています。
ですが、ユーザ名がセッションIDで誰とメッセージのやり取りをしているのかがあまり分からないため、使いやすいとは言えません。

セッションIDの代わりにユーザ名を表示するように改良していきます。

ユーザ名を取得するために、AlertDialogを使います。

AlertDialog
user_name = ft.TextField(label="Enter your name")

page.dialog = ft.AlertDialog(
    open=True, # Trueだとプログラム開始時に開く
    modal=True,
    title=ft.Text("Welcome!"),
    content=ft.Column([user_name], tight=True),
    actions=[ft.ElevatedButton(text="Join chat", on_click=join_click)],
    actions_alignment="end"
)

チャットに参加するとすべてのユーザにチャットに参加したことが通知されるメッセージが送信されます。

参加メッセージがチャットメッセージを区別するためにMessageクラスにmessage_typeプロパティを追加

class Message():
    def __init__(self, user: str, text: str, message_type: str):
        self.user = user
        self.text = text
        self.message_type = message_type

message_typeの区別するためにon_messageを以下のコード変更にします。

def on_message(message: Message):
    if message.message_type == "chat_message":
        chat.controls.append(ft.Text(f"{message.user}: {message.text}"))
    elif message.message_type =="login_message":
        chat.controls.append(
            ft.Text(message.text, italic=True, color=ft.colors.BLACK45, size=12)
        )
    page.update()

チャットに参加したときとメッセージを送信したとの2つのイベントができました。

AlertDialogに入力されたユーザ名前をsessionを使いを保存できるようにします。

def join_click(e):
    if not user_name.value:
        user_name.error_text = "Name cannot be blank!"
        user_name.update()
    else:
        page.session.set("user_name", user_name.value)
        page.dialog.open = False
        page.pubsub.send_all(Message(user=user_name.value, text=f"{user_name.value} has joined the chat.", message_type="login_message"))
        page.update()

また、ユーザ名が入力されていなければエラーが出るようになっています。

全体のコード:03.py

03.py
import flet as ft

class Message():
    def __init__(self, user: str, text: str, message_type: str):
        self.user = user
        self.text = text
        self.message_type = message_type

def main(page: ft.Page):
    page.title = 'チャット'
    
    chat = ft.Column()
    new_message = ft.TextField()
    
    def on_message(message: Message):
        if message.message_type == "chat_message":
            chat.controls.append(ft.Text(f"{message.user}: {message.text}"))
        elif message.message_type =="login_message":
            chat.controls.append(
                ft.Text(message.text, italic=True, color=ft.colors.BLACK45, size=12)
            )
        page.update()
        
    page.pubsub.subscribe(on_message)
    
    def send_click(e):
        page.pubsub.send_all(Message(user=page.session.get('user_name'), text=new_message.value, message_type="chat_message"))
        new_message.value = ''
        page.update()
        
    user_name = ft.TextField(label="Enter your name")
    
    def join_click(e):
        if not user_name.value:
            user_name.error_text = "Name cannot be blank!"
            user_name.update()
        else:
            page.session.set("user_name", user_name.value)
            page.dialog.open = False
            page.pubsub.send_all(Message(user=user_name.value, text=f"{user_name.value} has joined the chat.", message_type="login_message"))
            page.update()
        
    page.dialog = ft.AlertDialog(
        open=True, # Trueだとプログラム開始時に開く
        modal=True,
        title=ft.Text("Welcome!"),
        content=ft.Column([user_name], tight=True),
        actions=[ft.ElevatedButton(text="Join chat", on_click=join_click)],
        actions_alignment="end"
    )
        
    page.add(
        chat, ft.Row(controls=[new_message, ft.ElevatedButton("Send", on_click=send_click)])
    )


ft.app(target=main, view=ft.AppView.WEB_BROWSER)

https://youtu.be/OKlb_HqvtiU


ユーザーインターフェース

チャットアプリを豪華に見せる機能追加します。

全体のコード:main.py

表示例

メッセージには、イニシャル入りのアイコンと、ユーザ名とメッセージを表示します。

アイコンにはCircleAvatarを使います。

チャットアプリは多くのメッセージを表示する必要があるため、再利用可能なカスタムコントロールを作成することが合理的です。
Rowを継承したChatMessageクラスを新しく作ります。

  • ChatMessageクラスはインスタンス作成時に、Messageオブジェクトからユーザ名とメッセージを取り出しそれらを表示する。
  • get_initialsメソッドは、ユーザ名の頭文字の取得
  • get_avatar_colorメソッドは、ユーザ名に基づきhash関数を使用して、事前に決めた色のリストからランダムに決める
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, uset_name: str):
        return uset_name[:1].capitalize()
    
    # ユーザ名に基づき、hash関数を使用して、事前に決めた色のリストからランダムに決める
    def get_avatar_color(self, uset_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(uset_name) % len(colors_lookup)]

いくつかの関数や変数を変更していきます。

Messageクラスのuserインスタンスをuser_nameに変更します。

class Message():
-   def __init__(self, user: str, text: str, message_type: str):
+   def __init__(self, user_name: str, text: str, message_type: str):
-       self.user = user
+       self.user_name = user_name
        self.text = text
        self.message_type = message_type
  • send_click関数名をsend_message_click名に変更します。
  • Messageクラスを使っている引数名useruser_nameに変更します。
- def send_click(e):
+ def send_message_click(e):
    if new_message.value != "":
-       page.pubsub.send_all(Message(user=page.session.get('user_name'), text=new_message.value, message_type="chat_message"))
+       page.pubsub.send_all(Message(user_name=page.session.get('user_name'), text=new_message.value, message_type="chat_message"))
        new_message.value = ''
        new_message.focus()
        page.update()
  • Messageクラスを使っている引数名useruser_nameに変更します。
  • user_namejoin_user_nameに変更します。
- user_name = ft.TextField(label="Enter your name")
+ join_user_name  = ft.TextField(label="Enter your name")

def join_click(e):
-    if not user_name.value:
-       user_name.error_text = "Name cannot be blank!"
-       user_name.update()
+    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", user_name.value)
+       page.session.set("user_name", join_user_name.value)
        page.dialog.open = False
-       page.pubsub.send_all(Message(user=user_name.value, text=f"{user_name.value} has joined the chat.", message_type="login_message"))
+       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()
    
page.dialog = ft.AlertDialog(
    open=True,
    modal=True,
    title=ft.Text("Welcome!"),
-   content=ft.Column([user_name], tight=True),
+   content = ft.Column([join_user_name], tight=True),
    actions=[ft.ElevatedButton(text="Join chat", on_click=join_click)],
    actions_alignment="end"
)

レイアウトをアップデートしていきます。

ChatMessageクラスがユーザ名やメッセージを作成するので、on_message関数を書き換えていきます。

def on_message(message: Message):
    if message.message_type == "chat_message":
-       chat.controls.append(ft.Text(f"{message.user}: {message.text}"))
+       m = ChatMessage(message)
    elif message.message_type =="login_message":
-       chat.controls.append(
-           ft.Text(message.text, italic=True, color=ft.colors.BLACK45, size=12)
-       )
+       m = ft.Text(message.text, italic=True, color=ft.colors.BLACK45, size=12)
    chat.controls.append(m)
    page.update()

新しいレイアウトにするためにいくつかの改善点があります。

  • メッセージを表示するColumnからListViewに変更します。ListViewにすることでスクロールできるようになります。
  • ListViewの周りにボーダーを表示するContainerの追加
  • ElevatedButtonからIconButtonに変更
  • スペースを埋めるためにContainerexpandを追加

変更追記したコード

新しいチャットメッセージ

- chat = ft.Column()

+ chat = ft.ListView(
+     expand = True,
+     spacing = 10,
+     auto_scroll = True
+ )
  • ListView
    • spacing:アイテム間の高さを指定できます。
    • auto_scrollTrueにした場合、スクロールバーの位置を自動的に端に移動させます。

新しい入力フォーム

- new_message = ft.TextField()

+ new_message = ft.TextField(
+     hint_text = "Write a message...",
+     autocorrect = True,
+     shift_enter = True,
+     min_lines = 1,
+     max_lines = 5,
+     filled = True,
+     expand = True,
+     on_submit = send_message_click
+ )
  • TextField
    • shift_entershift+enterで改行することができます。
    • filled:塗りつぶしをします。
    • on_submitTextFieldにフォーカスがあっているときにENTERキーを押すとイベントが発生します。

新しいページ

- page.add(
-     chat, ft.Row(controls=[new_message, ft.ElevatedButton("Send", on_click=send_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
+             )
+         ]
+     )
+ )
  • IconButton
    • tooltip:カーソルが置かれたときに表示されるテキスト

最後

以上になります。
GitHubにすべてのコードを上げています。

https://github.com/massao000/Flat-sample/tree/main/chat

電卓のチュートリアルも記事にしています。
https://zenn.dev/shiro_toy_box/articles/6a23618f77dc60

Discussion