主题
浏览器只会在处理一次点击或按键的瞬间,才允许某些操作:打开文件选择器、写入剪贴板、显示分享面板、打开新标签页。这个权限只在该次手势期间有效,过后即失效。
这对 Flet 通常的模式是个问题。当你在 on_click 事件处理器中调用 FilePicker.pick_files() 时,点击会先到达你的 Python 代码, 你的代码执行,然后打开对话框的指令再传回去——而到那时,浏览器已经不再认为有手势正在进行,于是悄悄地拒绝执行。
Safari 对此执行得非常严格,Chrome 和 Firefox 则比较宽松,因此症状令人困惑:同一个应用,在 Android 和桌面端都能正常工作, 在 iPhone 或 iPad 上却悄无声息地什么都不做。什么日志都不会输出,因为在浏览器看来一切正常。
客户端动作(client actions) 弥补了这个缺口。动作是绑定在控件上、而不是从事件处理器中调用的,因此客户端在点击到来时 就已经知道要做什么,并能在手势内立即执行:
python
ft.Button("Upload", action=ft.PickFiles(file_picker, allow_multiple=True))打开 URL
OpenUrl 用来打开一个链接。其中新标签页是浏览器重点防护的部分,因为那正是弹窗拦截器要阻止的行为。
python
import flet as ft
def main(page: ft.Page):
# `action` 由客户端在处理点击的过程中执行,因此打开新标签页不会被当作
# 未经请求的弹窗。对比 `UrlLauncher().launch_url()`,它必须先到达 Python,
# 因此在 iOS 的 Safari 上会被拦截。
page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Text("Both buttons open the same page:"),
ft.Button(
"Open in this tab",
action=ft.OpenUrl("https://flet.dev", target=ft.UrlTarget.SELF),
),
ft.Button(
"Open in a new tab",
action=ft.OpenUrl(
"https://flet.dev",
target=ft.UrlTarget.BLANK,
),
),
ft.Text(
"An action can be combined with on_click - the action "
"runs on the client, then your handler runs in Python."
),
ft.Button(
"Open and log",
action=ft.OpenUrl("https://flet.dev/docs"),
on_click=lambda e: page.show_dialog(
ft.SnackBar(ft.Text("Docs opened"))
),
),
],
),
)
)
if __name__ == "__main__":
ft.run(main)控件一直以来就拥有的 url 属性工作方式与此相同、保持不变—— OpenUrl 适用于你想把它和其他动作组合,或者让所有受手势门控的操作以同一种方式书写的场景。
复制到剪贴板
python
import flet as ft
def main(page: ft.Page):
# `action` 由客户端在处理点击的过程中执行。这是 Safari 允许页面写入剪贴板的
# 唯一时刻,也正因如此,`Clipboard().set()`——它必须先到达 Python——在 iOS 上
# 什么都不做。
token = "flet-1234-5678"
def handle_copied(e):
page.show_dialog(ft.SnackBar(ft.Text("Copied to clipboard")))
page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Text(f"Token: {token}", selectable=True),
ft.Button(
"Copy token",
icon=ft.Icons.CONTENT_COPY,
action=ft.CopyToClipboard(token),
on_click=handle_copied,
),
ft.Divider(),
ft.Text(
"An action's arguments are fixed before the click, so "
"to copy something typed just now, update the action "
"as the text changes."
),
note := ft.TextField(
label="Note",
value="Edit me, then copy",
on_change=lambda e: setattr(
copy_note, "action", ft.CopyToClipboard(note.value)
),
),
copy_note := ft.Button(
"Copy note",
icon=ft.Icons.CONTENT_COPY,
action=ft.CopyToClipboard("Edit me, then copy"),
on_click=handle_copied,
),
],
),
)
)
if __name__ == "__main__":
ft.run(main)分享
python
import flet as ft
def main(page: ft.Page):
# `action` 由客户端在处理点击的过程中执行。浏览器只在一次手势期间才会打开
# 分享面板,因此从 Python 调用的 `Share()` 在 Web 端没有任何效果。
page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Text("Share this page with someone:"),
ft.Button(
"Share",
icon=ft.Icons.SHARE,
action=ft.ShareText(
"Flet lets you build multi-platform apps in Python: "
"https://flet.dev",
subject="Flet",
),
),
ft.Text(
"The share sheet is a system dialog - what it offers "
"depends on the platform, and on desktop browsers it "
"may not be available at all."
),
],
),
)
)
if __name__ == "__main__":
ft.run(main)选择文件
PickFiles 是用于修复 Web 应用在 iOS 上文件选择问题的动作。
因为对话框是在你的代码看到点击之前就打开的,所选结果无法像 pick_files() 那样返回给调用者。它会被送到 FilePicker.on_result。被选中的文件仍然与 FilePicker 相关联,因此 upload() 的工作方式与之前完全一致。
python
#
# Picking and uploading files in a way that also works in a web app on iOS.
#
# Run this example with:
# export FLET_SECRET_KEY=<some_secret_key>
# uv run flet run --web examples/services/file_picker/pick_files_action/main.py
#
from dataclasses import dataclass, field
import flet as ft
@dataclass
class State:
picked_files: list[ft.FilePickerFile] = field(default_factory=list)
state = State()
def main(page: ft.Page):
prog_bars: dict[str, ft.ProgressRing] = {}
def handle_upload_progress(e: ft.FilePickerUploadEvent):
prog_bars[e.file_name].value = e.progress
def handle_result(e: ft.FilePickerResultEvent):
# A PickFiles action opens the dialog before Python sees the click, so
# the selection arrives here instead of being returned to a caller.
state.picked_files = e.files
prog_bars.clear()
upload_progress.controls.clear()
for f in e.files:
prog = ft.ProgressRing(value=0, bgcolor="#eeeeee", width=20, height=20)
prog_bars[f.name] = prog
upload_progress.controls.append(
ft.Row([prog, ft.Text(f"{f.name} ({f.size} bytes)")])
)
upload_button.disabled = len(e.files) == 0
async def handle_file_upload(e: ft.Event[ft.Button]):
upload_button.disabled = True
# The picked files stay on the FilePicker, so upload() takes them as-is.
await file_picker.upload(
files=[
ft.FilePickerUploadFile(
name=file.name,
upload_url=page.get_upload_url(f"dir/{file.name}", 60),
)
for file in state.picked_files
]
)
file_picker = ft.FilePicker(
on_result=handle_result,
on_upload=handle_upload_progress,
)
page.services.append(file_picker)
page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Button(
content="Select files...",
icon=ft.Icons.FOLDER_OPEN,
# Attaching the pick to the control - rather than
# calling file_picker.pick_files() from an on_click
# handler - is what makes the dialog open on iOS.
action=ft.PickFiles(file_picker, allow_multiple=True),
),
upload_progress := ft.Column(),
upload_button := ft.Button(
content="Upload",
icon=ft.Icons.UPLOAD,
on_click=handle_file_upload,
disabled=True,
),
],
),
)
)
if __name__ == "__main__":
ft.run(main, upload_dir="examples")每个动作对应什么
| 动作 | 等价方法 |
|---|---|
OpenUrl | UrlLauncher.launch_url() |
CopyToClipboard | Clipboard.set() |
ShareText | Share.share_text() |
PickFiles | FilePicker.pick_files() |
动作先执行,然后你的 on_click 事件处理器照常被调用——因此你仍然可以在 Python 中响应该次点击。
如果一个控件需要多个动作,它既接受单个动作,也接受动作列表:
python
ft.Button(
"Copy and open",
action=[ft.CopyToClipboard(link), ft.OpenUrl(link, target=ft.UrlTarget.BLANK)],
)限制
动作的参数在点击之前就已固定。 这源于动作的本质——客户端必须事先就掌握整个操作,因为它没有时间去询问。 要复制或分享一个会变化的值,请在值变化时更新动作,正如上面剪贴板示例所做的那样。对此毫无办法;这是浏览器的规则,而不是 Flet 的。
读取剪贴板依然会弹出提示。 无论你怎么做,Safari 都会为 Clipboard.get() 显示粘贴确认界面。 动作只是让读取成为可能,而不是让它不可见。
在浏览器之外,这不适用。 在桌面端、Android 和 iOS 应用中没有这种限制,普通的调用方式也能正常工作。 动作在任何地方都可用,因此如果你的应用也会运行在 Web 端,你可以无条件地使用它们——但如果不会,也就没有需要修复的问题。