主题
自定义控件 (Custom Controls)
虽然 Flet 提供了 140 多个开箱即用的内置控件,但使用 Flet 进行编程的真正魅力在于,你可以利用 Python 的面向对象编程概念,将这些内置控件作为砖瓦,创建出属于你自己的、可复用的自定义 UI 组件。
你可以在 Python 中通过**修改样式(Styling)和/或进行组合(Combining)**现有的 Flet 控件来创建自定义控件。
样式化控件 (Styled controls)
最简单的自定义控件是样式化控件。例如,一个在应用中需要多次复用的、具有特定颜色和行为的按钮。
要创建样式化控件,你需要创建一个继承自需要自定义的 Flet 控件的 Python 数据类(Dataclass),本例中我们继承 Button 控件:
python
from dataclasses import field
@ft.control
class MyButton(ft.Button):
bgcolor: ft.Colors = ft.Colors.ORANGE_300
color: ft.Colors = ft.Colors.GREEN_800
style: ft.ButtonStyle = field(
default_factory=lambda: ft.ButtonStyle(
shape=ft.RoundedRectangleBorder(radius=10)
)
)
expand: int = 1你也可以在继承的类上定义 @dataclass 装饰器,其与 @ft.control 效果完全相同:
python
from dataclasses import dataclass, field
@dataclass
class MyButton(ft.Button):
bgcolor: ft.Colors = ft.Colors.ORANGE_300
color: ft.Colors = ft.Colors.GREEN_800
style: ft.ButtonStyle = field(
default_factory=lambda: ft.ButtonStyle(
shape=ft.RoundedRectangleBorder(radius=10)
)
)
expand: int = 1你还可以选择直接在 init() 方法中设置属性值:
python
@ft.control
class MyButton(ft.Button):
def init(self):
self.bgcolor = ft.Colors.ORANGE_300
self.color = ft.Colors.GREEN_800
self.style = ft.ButtonStyle(
shape=ft.RoundedRectangleBorder(radius=10)
)
self.expand = 1规则 (Rules)
- 你应当在继承类上定义
@dataclass或@ft.control装饰器 —— 两者的运行行为一致。 - 属性字段若要生效,必须提供类型注解。因此,
expand: int = 1会覆盖继承的父类属性,但expand = 1则不会。如果不确定使用什么类型,可以使用Any(例如expand: Any = 1)。 - 对于简单的数据类型(如
int、bool、str等),使用直接赋值作为默认值;对于不可变或复杂类型(如类、列表、字典等),请使用field(default_factory=lambda: <值>)。 - 你可以在
init()方法中设置初始属性值,但是如果在实例化控件时显式传入了参数,例如MyButton(expand=False),则可能无法通过该方法对其进行覆盖。
现在,你可以在应用中自由地使用全新的自定义按钮控件了:
python
import flet as ft
@ft.control
class MyButton(ft.Button):
bgcolor: ft.Colors = ft.Colors.ORANGE_300
color: ft.Colors = ft.Colors.GREEN_800
def main(page: ft.Page):
def ok_clicked(e):
print("OK clicked")
def cancel_clicked(e):
print("Cancel clicked")
page.add(
MyButton(content="OK", on_click=ok_clicked),
MyButton(content="Cancel", on_click=cancel_clicked),
)
ft.run(main)
你可以前往计算器教程查看使用样式化控件的完整教学实践。
组合控件 (Composite controls)
组合式自定义控件通常继承自容器类控件(如 Column、Row、Stack 甚至 View),用来将多个不同的 Flet 控件合并组合。以下是在待办事项(To-Do)应用中可以使用的自定义 Task(任务)控件:
python
import flet as ft
@ft.control
class Task(ft.Row):
text: str = ""
def init(self):
self.text_view = ft.Text(value=self.text)
self.text_edit = ft.TextField(value=self.text, visible=False)
self.edit_button = ft.IconButton(icon=ft.Icons.EDIT, on_click=self.edit)
self.save_button = ft.IconButton(
visible=False, icon=ft.Icons.SAVE, on_click=self.save
)
self.controls = [
ft.Checkbox(),
self.text_view,
self.text_edit,
self.edit_button,
self.save_button,
]
def edit(self, e):
self.edit_button.visible = False
self.save_button.visible = True
self.text_view.visible = False
self.text_edit.visible = True
self.update()
def save(self, e):
self.edit_button.visible = True
self.save_button.visible = False
self.text_view.visible = True
self.text_edit.visible = False
self.text_view.value = self.text_edit.value
self.update()
def main(page: ft.Page):
page.add(
Task(text="Do laundry"),
Task(text="Cook dinner"),
)
ft.run(main)生命周期方法 (Life-cycle methods)
自定义控件提供了生命周期“钩子”方法,你可以在不同的场景中重写它们以控制控件的生命周期行为。
build()
当控件被创建并成功关联其所在的 self.page 时,会触发调用 build() 方法。
如果你的业务逻辑需要在控件构造函数执行时无法获取的环境数据(例如,需要通过访问 self.page 才能判断的属性),请重写 build()。典型示例:在平台自适应应用中,根据 self.page.platform 来为不同平台返回对应的合适图标。
did_mount()
在控件被成功渲染添加至页面中并分配了其在底层的 transient uid 后调用。
如果需要在控件挂载上线后立即启动异步服务(例如,需要定时向外部 OpenWeather API 请求天气状态并在此控件上实时自我渲染的 Weather widget 气象组件),请重写 did_mount() 方法。
will_unmount()
在控件从页面视图树中卸载移除之前执行。
重写 will_unmount() 可以运行用于清理后台常驻服务、解除连接句柄的清理(clean-up)代码。
before_update()
每当控件进行状态刷新更新(update)时调用。
警告
请确保不要在 before_update() 内部调用控件自身的 update() 方法,否则会导致无限递归循环。
隔离控件
自定义控件都拥有一个 is_isolated 属性,默认情况下该值为 False。
如果将 is_isolated 设置为 True,你的自定义控件将会与外部父级布局渲染更新完成隔离。这意味着:当外部父级控件触发 update() 方法重新渲染时,该控件自身会被渲染更新,但它子节点控件中的内部属性变更将不会被打包合并在全局更新包(update digest)中去。被隔离的控件应当由自身在类方法内显式调用 self.update() 来将自己的状态更改推送到 Flet 页面。
作为开发中的最佳实践:任何在其类方法中主动触发 self.update() 刷新的自定义控件,都强烈建议设置成隔离控件。
在前面的例子中,简单的样式化控件 MyButton 不需要设置隔离,而需要触发内部输入栏和编辑按钮显隐更新的组合控件 Task 应当实现隔离:
python
class Task(ft.Row):
def __init__(self, text):
super().__init__()
def is_isolated(self):
return True