Skip to content

即时聊天应用教程 ​


在本教程中,我们将创建一个简单的内存即时聊天(Chat)应用,以帮助你理解 Flet 框架的基础知识。该应用是一个很好的起点,可以帮助你后续去创建更复杂和有用的项目。

在本教程中,你将学习如何:

完整的应用程序将如下所示:

聊天应用

你可以在这里体验在线演示。

Flet 入门 ​

我们照例以 "Hello, world!" 应用开始!

要使用 Flet 在 Python 中创建多平台应用,你不需要了解 HTML、CSS 或 JavaScript,但确实需要掌握 Python 的基础知识和面向对象编程。

在创建你的第一个 Flet 应用之前,你需要配置开发环境。

安装好 Flet 后,让我们创建一个 Hello World 应用。

创建 hello.py 并写入以下内容:

hello.py

python
import flet as ft

def main(page: ft.Page):
    page.add(ft.Text(value="Hello, world!"))

ft.run(main)

运行该应用,你将看到一个包含问候语的新窗口:

hello-world

添加页面控件与事件处理 ​

首先,我们希望能够获取用户输入(聊天消息)并在屏幕上显示消息历史记录。该步骤的布局如下所示:

chat-layout-1

为了实现这个布局,我们将使用这些 Flet 控件:

  • Column - 一个用于垂直显示聊天消息(Text 控件)的容器。
  • Text - 在聊天 Column 中显示的聊天消息。
  • Row - 一个用于水平显示 TextField 和 Button 的容器。
  • TextField - 用于接收用户输入的新消息的输入控件。
  • Button - “发送”(Send)按钮,用于将新消息添加到聊天 Column 中。

创建 chat.py 文件并写入以下内容:

chat.py

python
import flet as ft


def main(page: ft.Page):
    chat = ft.Column()
    new_message = ft.TextField()

    def send_click(e):
        chat.controls.append(ft.Text(new_message.value))
        new_message.value = ""

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


ft.run(main)

当用户点击 “发送”(Send)按钮时,会触发 on_click 事件并调用 send_click 方法。send_click 随后向 Column.controls 列表中添加一个新的 Text 控件,并清除 new_message 文本输入框的值。

提示

在事件处理器中更新控件的任何属性后,无需手动调用 update() 方法,因为更新会自动完成。但如果在事件处理之外更新了控件的属性,则应该手动调用它。

现在的聊天应用如下所示:

chat-1

广播聊天消息 ​

在上一步中,我们创建了一个简单的应用程序,它可以获取用户的输入并在屏幕上显示聊天消息。

如果你在两个浏览器标签页中打开这个应用,它将创建两个应用会话(Sessions)。每个会话都拥有自己独立的消息列表。

提示

要在本地两个浏览器标签页中打开你的应用,请运行以下命令:

sh
flet run --web <path_to_your_app>

打开后,复制 URL 并将其粘贴到新标签页中。

要构建一个实时的聊天应用,你需要以某种方式在聊天应用会话之间传递消息。当一个用户发送消息时,它应该被广播到所有其他应用会话,并显示在它们的页面上。

Flet 提供了一个简单的内置 PubSub 机制,用于在页面会话之间进行异步通信。

首先,我们需要订阅该用户以接收广播消息:

python
    page.pubsub.subscribe(on_message)

pubsub.subscribe() 方法会将当前应用会话添加到订阅者列表中。它接受一个处理函数作为参数,当发布者调用 pubsub.send_all() 方法时,该处理函数随后会被调用。

在处理函数中,我们将把新消息(Text)添加到聊天 controls 列表中:

python
    def on_message(message: Message):
        chat.controls.append(ft.Text(f"{message.user}: {message.text}"))
        page.update()

最后,当用户点击 “发送”(Send)按钮时,你需要调用 pubsub.send_all() 方法:

python
    def send_click(e):
        page.pubsub.send_all(Message(user=page.session.index, text=new_message.value))
        new_message.value = ""
        page.update()

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

pubsub.send_all() 将调用 on_message() 并将 Message 对象传递给它。

以下是该步骤的完整代码:

chat.py

python
from dataclasses import dataclass

import flet as ft


@dataclass
class Message:
    user: str
    text: str


def main(page: ft.Page):
    chat = ft.Column()
    new_message = ft.TextField()

    def on_message(message: Message):
        chat.controls.append(ft.Text(f"{message.user}: {message.text}"))
        page.update()

    page.pubsub.subscribe(on_message)

    def send_click(e):
        page.pubsub.send_all(Message(user=page.session.id, text=new_message.value))
        new_message.value = ""

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


ft.run(main)

chat-2

用户名对话框 ​

你在上一步中创建的聊天应用已经具备了在用户会话之间交换消息的基本功能。不过它还不够用户友好,因为它显示的是发送消息的 session.id,这无法看出是在和谁交流。

让我们改进应用,使每条消息都显示用户名而不是 session.id。为了获取用户名,我们将使用 AlertDialog 控件。让我们将它添加到页面中:

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

    page.show_dialog(
        ft.AlertDialog(
            open=True,
            modal=True,
            title=ft.Text("Welcome!"),
            content=ft.Column([user_name], tight=True),
            actions=[ft.Button(content="Join chat", on_click=join_click)],
            actions_alignment=ft.MainAxisAlignment.END,
        )
    )

注意

由于我们将对话框的 open 属性设置为了 True,它将在程序启动时打开。

username-dialog

当用户点击 “加入聊天”(Join chat)按钮时,它将调用 join_click 方法,该方法应该向所有订阅者发送一条消息,通知他们该用户已加入聊天。这条消息看起来应该与常规的聊天消息不同,例如:

chat-4

让我们给 Message 类添加一个 message_type 属性,以区分登录消息和聊天消息:

python
@dataclass
class Message:
    user: str
    text: str
    message_type: str

我们将在 on_message 方法中检查 message_type:

python
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.BLACK_45, size=12)
        )
    page.update()

现在,“login_message” 和 “chat_message” 类型的消息将在两个事件发生时发送:当用户加入聊天以及当用户发送消息。

让我们创建 join_click 方法:

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

我们使用页面会话存储(Session Storage)来存储 user_name,以便后续在 send_click 方法中发送聊天消息时使用它。

注意

用户名对话框会在我们调用 page.pop_dialog() 方法后立即关闭。

最后,让我们更新 send_click 方法,使用之前通过 page.session.store 保存的 user_name:

python
def send_click(e):
    page.pubsub.send_all(Message(user=page.session.store.get('user_name'), text=new_message.value, message_type="chat_message"))
    new_message.value = ""
完整代码 ​

以下是加入用户名对话框后的完整代码:

chat.py

python
from dataclasses import dataclass
import flet as ft

@dataclass
class Message:
    user: str
    text: str
    message_type: str

def main(page: ft.Page):
    page.title = "Flet Chat"
    
    chat = ft.Column()
    new_message = ft.TextField(hint_text="Write a 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.BLACK_45, size=12)
            )
        page.update()
        
    page.pubsub.subscribe(on_message)
    
    def send_click(e):
        page.pubsub.send_all(
            Message(
                user=page.session.store.get("user_name"),
                text=new_message.value,
                message_type="chat_message"
            )
        )
        new_message.value = ""
        page.update()

    def join_click(e):
        if not user_name.value:
            user_name.error = "Name cannot be blank!"
            user_name.update()
        else:
            page.session.store.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()

    user_name = ft.TextField(label="Enter your name")
    
    page.dialog = ft.AlertDialog(
        open=True,
        modal=True,
        title=ft.Text("Welcome!"),
        content=ft.Column([user_name], tight=True),
        actions=[ft.Button(content="Join chat", on_click=join_click)],
        actions_alignment=ft.MainAxisAlignment.END,
    )
    
    page.add(
        chat,
        ft.Row([new_message, ft.Button("Send", on_click=send_click)])
    )

ft.run(main)

chat-3

增强用户界面 ​

你在上一步中创建的聊天应用已经能够满足用户在登录后交换消息的需求。

在部署应用之前,我们建议添加一些额外的功能,以提升用户体验并使应用看起来更专业。

可重用的用户控件 ​

你可能希望以不同的格式来展示消息,例如:

chat-layout-chatmessage

现在,一条聊天消息将包含一个 Row 控件,里面包含带有用户名首字母的 CircleAvatar 控件,以及一个包含两个 Text 控件(用户名和消息内容)的 Column 控件。

我们需要在聊天应用中展示很多聊天消息,因此有必要创建你自己的可重用控件。让我们创建一个继承自 Row 的新 ChatMessage 类。

创建 ChatMessage 类的实例时,我们将传入一个 Message 对象,然后 ChatMessage 将根据消息的用户名 user(或者 user_name 属性)和内容 text 进行渲染:

python
@ft.control
class ChatMessage(ft.Row):
    def __init__(self, message: Message):
        super().__init__()
        self.message = message
        self.vertical_alignment = ft.CrossAxisAlignment.START
        self.controls = [
            ft.CircleAvatar(
                content=ft.Text(self.get_initials(self.message.user)),
                color=ft.Colors.WHITE,
                bgcolor=self.get_avatar_color(self.message.user),
            ),
            ft.Column(
                tight=True,
                spacing=5,
                controls=[
                    ft.Text(self.message.user, weight=ft.FontWeight.BOLD),
                    ft.Text(self.message.text, selectable=True),
                ],
            ),
        ]

    def get_initials(self, user_name: str):
        if user_name:
            return user_name[:1].capitalize()
        else:
            return "Unknown"

    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)]

ChatMessage 控件会自动提取用户名首字母,并通过哈希算法为其生成头像颜色。以后如果你决定改进消息布局或其逻辑,它不会影响到程序的其他部分——这就是封装的力量!

控件布局 ​

现在你可以使用全新的 ChatMessage 来为聊天应用构建更好的布局:

chat-layout-2

在 on_message 方法中,我们将创建 ChatMessage 实例,而不是普通的 Text 控件:

python
    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.BLACK_45, size=12)
        chat.controls.append(m)
        page.update()

新布局中建议的其他改进包括:

  • 使用 ListView 替代 Column 来展示消息,以便后续支持滚动浏览消息;
  • 使用 Container 在 ListView 周围添加边框;
  • 使用 IconButton 替代普通 Button 发送消息;
  • 为控件使用 expand 属性以填充可用空间;

以下是实现该布局的代码:

python
    # 聊天消息列表
    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,
                ),
            ]
        ),
    )
完整代码 ​

你可以在这里找到并运行完整代码。这也是本教程最终版本的聊天应用。下面我们将详细介绍我们所做的各项增强功能。

键盘支持 ​
聚焦输入控件 ​

所有数据输入控件都拥有 autofocus 属性,设置该属性为 True 时会将初始焦点移动到该控件。如果页面上有多个控件设置了 autofocus,那么第一个被添加到页面中的控件将获得焦点。

我们在用户名输入对话框中的 TextField 上,以及用于输入聊天消息的 TextField 上都设置了 autofocus=True,以便在对话框关闭时初始焦点能自动回到输入框上。

当用户点击 “发送” 按钮或按 Enter 提交聊天消息时,TextField 将失去焦点。为了通过编程方式重新获取焦点,我们使用了 TextField.focus() 方法。

回车提交表单 ​

在键盘上直接按下 Enter 键来提交表单实在太方便了!在对话框中输入名字,按 Enter,输入新消息,按 Enter,再输入下一条,按 Enter——全程不需要鼠标操作! 🚀

Flet 对此提供了原生支持,即提供 TextField.on_submit 事件处理器,当用户在焦点位于该文本框时按下 Enter 键时,就会触发该事件。

输入多行消息 ​

但是如果是多行文本输入框,Enter 键需要用来将光标移至下一行怎么办?我们也为你考虑到了!TextField 控件拥有 shift_enter 属性,当设置其为 True 时,将启用类似 Discord 的行为:用户按下 Shift+Enter 可以换行,而单按 Enter 将直接提交表单。

自动平滑滚动到最新消息 ​

在聊天窗口中,当新消息到来时是否注意到页面平滑滚动到了最后?这可以通过将 ListView.auto_scroll 属性设置为 True 来轻松实现。作为顶层页面的 Page 类(本身也是一个可滚动容器),同样支持 auto_scroll。

页面标题 ​

最后一项完善——更新页面标题非常简单:

python
page.title = "Flet Chat"
page.update()

部署应用 ​

恭喜!你已经使用 Flet 编写了你的 Python 聊天应用,它看起来太酷了!现在是时候把它分享给世界了!

请按照相关说明将你的 Flet 应用作为网页应用部署到 Fly.io 或 Replit。

探索更多方向 ​

在未来的开发中,你可以为这个聊天应用添加许多新功能:

  • 用户断开连接、重新连接及会话超时处理;
  • 上传/下载图片;
  • 用户认证、头像管理;
  • 使用数据库进行消息持久化存储;
  • 聊天频道、主题分类;
  • 全文检索;
  • 表情符号(Emojis)、Markdown 支持;
  • 接入机器人(Bots);
  • 打包为移动应用;

如果你有兴趣为该应用/教程做出贡献,欢迎同其他 Flet 开发者进行分享。

总结 ​

在本教程中,你学习了如何:

  • 创建一个简单的 Flet 应用;
  • 添加页面控件并处理事件;
  • 使用内置的 PubSub 库;
  • 使用 AlertDialog 获取用户名;
  • 通过可重用的自定义控件构建布局;
  • 将你的 Flet 应用部署到网页端;

要进一步阅读,你可以探索 controls 和 examples。


关注我,学习大模型、大数据、Python等热门技术。