Skip to content

To-Do 待办事项应用教程 ​


在本教程中,我们将逐步向你展示如何使用 Flet 框架在 Python 中创建一个 To-Do(待办事项)应用,然后将其发布为桌面、移动或网页应用。虽然该应用只是一个仅有 164 行(格式化后!)Python 代码的单文件程序,但它却是一个具有丰富、响应式 UI 的多平台应用程序:

ToDo Demo

你可以在这里查看在线演示。

我们之所以选择 To-Do 应用作为教程,是因为它涵盖了创建 Flet 应用所需的所有基本概念:构建页面布局、添加控件、处理事件、显示和编辑列表、制作可重用的 UI 组件以及发布选项。

本教程包含以下步骤:

Flet 入门 ​

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

在创建你的第一个 Flet 应用之前,你需要配置开发环境,这需要 Python 3.10 或更高版本以及 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

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

首先,我们需要一个 TextField 控件用于输入任务名称,以及一个带事件处理器的 "+" FloatingActionButton 按钮,用于添加并显示一个带有新任务的 Checkbox 控件。

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

todo.py

python
import flet
from flet import Checkbox, FloatingActionButton, Icons, Page, TextField


def main(page: Page):
    def add_clicked(e):
        page.add(Checkbox(label=new_task.value))
        new_task.value = ""
        page.update()

    new_task = TextField(hint_text="Whats needs to be done?")

    page.add(new_task, FloatingActionButton(icon=Icons.ADD, on_click=add_clicked))


flet.run(main)

运行应用,你应该会看到类似下面的页面:

app-1

页面布局 ​

现在让我们把应用变得美观!我们希望整个应用位于页面顶部的中央,宽度为 600 像素。TextField 和 "+" 按钮应该水平对齐,并占用整个应用的宽度:

diagram-1

Row 控件用于在页面上将其子控件水平排列。Column 控件用于将其子控件垂直排列。

将 todo.py 的内容替换为以下代码:

todo.py

python
import flet as ft


def main(page: ft.Page):
    def add_clicked(e):
        tasks_view.controls.append(ft.Checkbox(label=new_task.value))
        new_task.value = ""
        view.update()

    new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
    tasks_view = ft.Column()
    view = ft.Column(
        width=600,
        controls=[
            ft.Row(
                controls=[
                    new_task,
                    ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
                ],
            ),
            tasks_view,
        ],
    )

    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.add(view)


ft.run(main)

运行应用,你应该会看到类似下面的页面:

app-2

可重用的 UI 组件 ​

虽然我们可以继续在 main 函数中编写应用,但最佳实践是创建一个可重用的 UI 组件(Reusable UI Component)。想象一下,你正在开发一个应用头部、一个侧边栏菜单,或者是一个将作为更大项目一部分的 UI。即使你现在还没有想到这样的用途,我们仍然建议你在创建所有 Flet 应用时都要考虑到组合性和可重用性。

为了制作一个可重用的 To-Do 应用组件,我们将把它的状态和展示逻辑封装在一个独立的类中:

todo.py

python
import flet as ft


@ft.control
class TodoApp(ft.Column):
    # 应用的根控件是一个包含所有其他控件的 Column 控件
    def init(self):
        self.new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
        self.tasks_view = ft.Column()
        self.width = 600
        self.controls = [
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(
                        icon=ft.Icons.ADD, on_click=self.add_clicked
                    ),
                ],
            ),
            self.tasks_view,
        ]

    def add_clicked(self, e):
        self.tasks_view.controls.append(ft.Checkbox(label=self.new_task.value))
        self.new_task.value = ""
        self.update()


def main(page: ft.Page):
    page.title = "To-Do App"
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.update()

    # 创建应用实例
    todo = TodoApp()

    # 将应用的根控件添加到页面中
    page.add(todo)


ft.run(main)

快来尝试一下吧!

查看、编辑和删除列表项 ​

在上一步中,我们创建了一个基本的 To-Do 应用,任务项显示为复选框。接下来,让我们通过在任务名称旁边添加 “编辑”(Edit)和 “删除”(Delete)按钮来改进这个应用。点击 “编辑” 按钮将使该任务项切换到编辑模式。

每个任务项由两行组成:展示视图行 display_view(包含 Checkbox、“编辑” 和 “删除” 按钮)和编辑视图行 edit_view(包含 TextField 和 “保存” 按钮)。Column 控件作为 display_view 和 edit_view 两行的容器。

diagram-2

为了封装任务项的视图和操作,我们引入了一个新的 Task 类。

此外,我们修改了 TodoApp 类,使其在点击 “添加” 按钮时创建并持有 Task 实例。

对于 “删除” 任务的操作,我们在 TodoApp 类中实现了 task_delete() 方法,该方法接收一个任务控件实例作为参数。

然后,我们将 task_delete 方法的引用传递到 Task 类的构造函数中,并在 “删除” 按钮的事件处理器中调用它。

todo.py

python
from dataclasses import field
from typing import Callable

import flet as ft


@ft.control
class Task(ft.Column):
    task_name: str = ""
    on_task_delete: Callable[["Task"], None] = field(default=lambda task: None)

    def init(self):
        self.display_task = ft.Checkbox(value=False, label=self.task_name)
        self.edit_name = ft.TextField(expand=1)

        self.display_view = ft.Row(
            alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
            vertical_alignment=ft.CrossAxisAlignment.CENTER,
            controls=[
                self.display_task,
                ft.Row(
                    spacing=0,
                    controls=[
                        ft.IconButton(
                            icon=ft.Icons.CREATE_OUTLINED,
                            tooltip="Edit To-Do",
                            on_click=self.edit_clicked,
                        ),
                        ft.IconButton(
                            ft.Icons.DELETE_OUTLINE,
                            tooltip="Delete To-Do",
                            on_click=self.delete_clicked,
                        ),
                    ],
                ),
            ],
        )

        self.edit_view = ft.Row(
            visible=False,
            alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
            vertical_alignment=ft.CrossAxisAlignment.CENTER,
            controls=[
                self.edit_name,
                ft.IconButton(
                    icon=ft.Icons.DONE_OUTLINE_OUTLINED,
                    icon_color=ft.Colors.GREEN,
                    tooltip="Update To-Do",
                    on_click=self.save_clicked,
                ),
            ],
        )
        self.controls = [self.display_view, self.edit_view]

    def edit_clicked(self, e):
        self.edit_name.value = self.display_task.label
        self.display_view.visible = False
        self.edit_view.visible = True
        self.update()

    def save_clicked(self, e):
        self.display_task.label = self.edit_name.value
        self.display_view.visible = True
        self.edit_view.visible = False
        self.update()

    def delete_clicked(self, e):
        self.on_task_delete(self)


@ft.control
class TodoApp(ft.Column):
    # 应用的根控件是一个包含所有其他控件的 Column 控件
    def init(self):
        self.new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
        self.tasks = ft.Column()
        self.width = 600
        self.controls = [
            ft.Row(
                controls=[
                    self.new_task,
                    ft.FloatingActionButton(
                        icon=ft.Icons.ADD, on_click=self.add_clicked
                    ),
                ],
            ),
            self.tasks,
        ]

    def add_clicked(self, e):
        task = Task(task_name=self.new_task.value, on_task_delete=self.task_delete)
        self.tasks.controls.append(task)
        self.new_task.value = ""
        self.update()

    def task_delete(self, task):
        self.tasks.controls.remove(task)
        self.update()


def main(page: ft.Page):
    page.title = "To-Do App"
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.update()

    # 创建应用实例
    app = TodoApp()

    # 将应用的根控件添加到页面中
    page.add(app)


if __name__ == "__main__":
    ft.run(main)

运行应用,并尝试编辑和删除任务:

view-edit-delete

筛选列表项 ​

我们现在已经拥有了一个功能完善的 To-Do 应用,可以创建、编辑和删除任务。为了提高效率,我们希望能够根据任务的状态来筛选任务。

你可以从这里复制本步骤的完整代码。下面我们将解释为实现筛选而进行的更改。

我们使用 Tabs 控件来显示筛选器选项:

todo.py

python
# ...

class TodoApp(ft.Column):
    # 应用的根控件是一个包含所有其他控件的 Column 控件
    def init(self):
        self.new_task = ft.TextField(hint_text="Whats needs to be done?", expand=True)
        self.tasks = ft.Column()

        self.filter = ft.TabBar(
            scrollable=False,
            tabs=[
                ft.Tab(label="all"),
                ft.Tab(label="active"),
                ft.Tab(label="completed"),
            ],
        )

        self.filter_tabs = ft.Tabs(
            length=3,
            selected_index=0,
            on_change=lambda e: self.update(),
            content=self.filter,
        )

    # ...

为了根据任务状态显示不同的任务列表,我们可以维护三个不同的列表,分别包含 “全部”、“活动中” 和 “已完成” 任务。然而,我们选择了一种更简单的方法:我们只维护同一个列表,并仅根据任务的状态来改变它的 visibility 属性。

在 TodoApp 类中,我们重写了每次更新控件时都会被调用的 before_update() 方法。它会遍历所有任务,并根据任务状态更新其 visible 属性:

todo.py

python
class TodoApp(ft.Column):

    # ...

    def before_update(self):
        status = self.filter.tabs[self.filter.selected_index].text
        for task in self.tasks.controls:
            task.visible = (
                status == "all"
                or (status == "active" and task.completed == False)
                or (status == "completed" and task.completed)
            )

当我们点击标签页(Tab)或改变某个任务状态时,应该触发筛选。在选项卡选定的值发生改变,或点击任务项的复选框时,都会调用 TodoApp.before_update() 方法:

todo.py

python
class TodoApp(ft.Column):

    # ...

    def tabs_changed(self, e):
        self.update()

    def task_status_change(self, e):
        self.update()

    def add_clicked(self, e):
        task = Task(
            task_name=self.new_task.value,
            on_status_change=self.task_status_change,
            on_delete=self.task_delete,
        )
        self.tasks.controls.append(task)
        self.new_task.value = ""
        self.update()
    # ...

class Task(ft.Column):
    task_name: str = ""
    on_status_change: Callable[[], None] = field(default=lambda: None)
    on_delete: Callable[["Task"], None] = field(default=lambda task: None)

    # ...

    def status_changed(self, e):
        self.completed = self.display_task.value
        self.task_status_change()

运行应用,尝试点击各标签页来筛选任务:

filtering

最终完善 ​

我们的 Todo 应用现在几乎完成了。作为最后的完善,我们将添加一个页脚(页脚由 Column 控件组成),用来显示未完成任务的数量(Text 控件)以及一个 “清除已完成任务”(Clear completed)按钮。

完整代码 ​

你可以在这里找到并运行完整代码。

app-4

发布应用 ​

恭喜!你已经使用 Flet 创建了你的第一个 Python 应用,它看起来棒极了!

现在是时候与世界分享你的应用了!

按照这些说明将你的 Flet 应用发布为移动、桌面或网页应用。

总结 ​

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

  • 创建一个简单的 Flet 应用;
  • 使用可重用的 UI 组件;
  • 使用 Column 和 Row 控件设计 UI 布局;
  • 操作列表:查看、编辑、删除以及筛选项目;
  • 将你的 Flet 应用发布到多个平台;

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


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