主题
RawImage 继承自: LayoutControl, Control, BaseControl
以全带宽推送来自 Python 的像素帧进行显示。
与 Image 不同,Image 的 src 在每次更新时都通过常规 Flet 协议传输,而 RawImage 通过专用的 DataChannel 流式传输帧:字节完全跳过 MsgPack 的编码/解码;在本地传输 (桌面应用、flet run、Pyodide)中,帧直接从原始 RGBA 像素显示, 不经过任何图片编码或解码。这使得它适合用于动画、生成的图形、 相机帧以及 Pillow 输出,可达到交互式帧率。
帧通过可等待的 render、render_rgba 和 render_encoded 方法推送。每次调用在客户端 实际显示该帧后才会返回,因此一个简单的循环会自动根据显示速度来调节节奏:
python
raw_image = ft.RawImage(width=400, height=300)
page.add(raw_image)
while True:
await raw_image.render(produce_pil_image())传输方式会自动选择:
- 本地传输(
local_data_transport连接):帧以未压缩的预乘 RGBA8888 形式发送,并直接上传到客户端的 GPU 纹理。 - 远程传输(基于 WebSocket 的
flet-web):Pillow 和 NumPy 帧会在事件循环之外编码为 PNG 以节省带宽;对于没有 straight-alpha 源的原始像素帧,则以未压缩形式发送。
预乘 alpha。 原始像素路径会以 Flutter 的 rgba8888 格式将帧直接作为 GPU 纹理上传,该格式要求 RGB 值已经乘以 alpha(即「预乘」)。Pillow、numpy 代码以及图片文件通常产生的是 straight alpha(直乘 alpha),即颜色与透明度相互独立:半透明的白色像素在 straight 形式下为 (255, 255, 255, 128),而在预乘形式下为 (128, 128, 128, 128)(每个颜色通道变为 value * alpha // 255)。render 方法的 premultiplied 参数用于表明该转换是否已经完成:
premultiplied=False——像素携带 straight alpha;发送前 RGB 会乘以 alpha。该转换在快速的 C 循环中执行,当帧完全不透明时会跳过。premultiplied=True——跳过转换。当像素确实为预乘形式时,或在常见情况下当帧完全不透明时使用:此时所有 alpha 均为 255,两种形式完全相同,声明这一点可在流式循环中为每个帧省去一次 alpha 扫描。
使用 premultiplied=True 传入 straight-alpha 像素会导致半透明区域显示得过亮;而对于完全不透明的帧,无论该标志如何设置都不会出错。
当客户端控件重新挂载(页面重建、路由导航)时,最后一帧会被保留并自动重放,这与 Image.src 的持久化行为一致。
RawImage 与 Image
对于来自文件、URL、静态资源或一次性字节串的图片,请使用 Image。当你的 Python 代码生成像素——Pillow 绘图、NumPy 数组、相机帧、图表、程序化动画——并且你想快速、反复地将它们推送到屏幕时,请使用 RawImage。
Image.src 的每次更新都通过常规 Flet 协议传输,并在客户端从头解码。而 RawImage 通过专用数据通道流式传输帧:字节完全跳过协议;当客户端运行在同一台机器上(桌面应用、flet run、Pyodide)时,帧以原始 RGBA 像素传输并直接上传到 GPU 纹理——两端都不进行图片编码或解码。远程 Web 客户端则会自动接收紧凑的 PNG 帧。
render 方法是可等待的,并在客户端显示该帧后返回,因此一个简单的循环会自动根据显示速度来调节节奏:
python
raw_image = ft.RawImage(expand=True)
page.add(raw_image)
while True:
await raw_image.render(produce_pil_image())示例
图片查看器
常规的 PNG/JPEG/WebP 字节——无论是下载的、从文件读取的,还是从数据库取出的——都可通过 render_encoded 显示;客户端会使用自己的图片编解码器对其进行解码。
python
import asyncio
import urllib.request
import flet as ft
PHOTOS = [
f"https://picsum.photos/seed/{seed}/800/500.jpg"
for seed in ("flet", "raw", "image", "viewer", "photo")
]
async def main(page: ft.Page):
page.title = "RawImage photo viewer"
raw_image = ft.RawImage(expand=True, fit=ft.BoxFit.CONTAIN)
status_text = ft.Text("", size=12)
index = 0
cache: dict[str, bytes] = {}
async def fetch(url: str) -> bytes:
if url not in cache:
cache[url] = await asyncio.to_thread(
lambda: urllib.request.urlopen(url, timeout=20).read()
)
return cache[url]
async def show(i: int):
nonlocal index
index = i % len(PHOTOS)
status_text.value = f"loading {index + 1}/{len(PHOTOS)}…"
page.update()
data = await fetch(PHOTOS[index])
# render_encoded displays PNG/JPEG/WebP bytes from anywhere: an
# HTTP response like here, a file (Path("photo.jpg").read_bytes()),
# a database blob. The client decodes them with its image codecs.
await raw_image.render_encoded(data)
status_text.value = f"{index + 1}/{len(PHOTOS)} — {len(data) // 1024} KB JPEG"
page.update()
def go(delta: int):
asyncio.create_task(show(index + delta))
page.add(
ft.Row(
[
ft.IconButton(ft.Icons.CHEVRON_LEFT, on_click=lambda: go(-1)),
ft.IconButton(ft.Icons.CHEVRON_RIGHT, on_click=lambda: go(1)),
status_text,
],
spacing=10,
),
raw_image,
)
await show(0)
if __name__ == "__main__":
ft.run(main)Plasma 动画
以程序化方式生成 plasma 效果,并通过实时 FPS 计数器和渲染分辨率滑块进行流式播放。
python
import asyncio
import time
from collections import deque
import numpy as np
import flet as ft
FPS_WINDOW_SECONDS = 2.0
def plasma_frame(width: int, height: int, t: float) -> np.ndarray:
"""
Renders a classic demoscene plasma: several drifting sine fields summed
and mapped through a rotating RGB palette. Fully vectorized in numpy.
"""
x = np.linspace(0.0, 3.0 * np.pi, width, dtype=np.float32)[None, :]
y = np.linspace(0.0, 3.0 * np.pi, height, dtype=np.float32)[:, None]
cx = x - 1.5 * np.pi + np.sin(t / 3.0) * np.pi
cy = y - 1.5 * np.pi + np.cos(t / 2.0) * np.pi
v = (
np.sin(x + t)
+ np.sin((y + t) / 2.0)
+ np.sin((x + y + t) / 2.0)
+ np.sin(np.sqrt(cx * cx + cy * cy + 1.0) + t)
) / 4.0
frame = np.empty((height, width, 4), dtype=np.uint8)
frame[..., 0] = np.sin(v * np.pi) * 127 + 128
frame[..., 1] = np.sin(v * np.pi + 2.0 * np.pi / 3.0) * 127 + 128
frame[..., 2] = np.sin(v * np.pi + 4.0 * np.pi / 3.0) * 127 + 128
frame[..., 3] = 255
return frame
async def main(page: ft.Page):
page.title = "RawImage plasma"
page.padding = 0
raw_image = ft.RawImage(expand=True, fit=ft.BoxFit.FILL)
fps_text = ft.Text("fps: —", size=12)
resolution_text = ft.Text("res: —", size=12)
detail_slider = ft.Slider(
min=1,
max=8,
divisions=7,
value=2,
width=200,
label="downscale 1/{value}",
)
status_bar = ft.Container(
content=ft.Row(
[fps_text, resolution_text, ft.Text("detail:", size=12), detail_slider],
spacing=20,
),
padding=ft.Padding.symmetric(horizontal=12, vertical=2),
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGH,
)
page.add(
ft.SafeArea(
content=ft.Column([raw_image, status_bar], expand=True, spacing=0),
expand=True,
)
)
frame_times: deque[float] = deque()
frame_size = (0, 0)
async def animate():
nonlocal frame_size
started = time.monotonic()
while True:
downscale = int(detail_slider.value)
width = max(2, int(page.width or 800) // downscale)
height = max(2, (int(page.height or 600) - 50) // downscale)
frame_size = (width, height)
frame = plasma_frame(width, height, time.monotonic() - started)
try:
# The frame is opaque, so it is premultiplied by definition —
# saying so skips an alpha scan per frame. The await resolves
# when the frame is on screen, pacing the loop to display
# speed.
await raw_image.render(frame, premultiplied=True)
except (RuntimeError, TimeoutError):
return # window closed — session destroyed
now = time.monotonic()
frame_times.append(now)
while frame_times and frame_times[0] < now - FPS_WINDOW_SECONDS:
frame_times.popleft()
async def refresh_stats():
# Refresh labels at ~4 Hz instead of once per frame, which would
# thrash the layout.
while True:
now = time.monotonic()
while frame_times and frame_times[0] < now - FPS_WINDOW_SECONDS:
frame_times.popleft()
fps_text.value = f"fps: {len(frame_times) / FPS_WINDOW_SECONDS:.1f}"
resolution_text.value = f"res: {frame_size[0]}x{frame_size[1]}"
try:
page.update()
except RuntimeError:
return
await asyncio.sleep(0.25)
asyncio.create_task(animate())
asyncio.create_task(refresh_stats())
if __name__ == "__main__":
ft.run(main)Pillow 绘画
一个可交互的绘画应用:通过平移手势在 Pillow 图像上绘制笔触,该图像通过脏标记渲染循环流式推送到屏幕。
python
import asyncio
from PIL import Image, ImageDraw
import flet as ft
CANVAS_WIDTH = 700
CANVAS_HEIGHT = 450
# ImageDraw has no anti-aliasing: lines and ellipses get hard, jagged
# edges. Painting on a canvas this many times larger and letting the
# client downscale it into the control's box averages each screen pixel
# from SUPERSAMPLE^2 canvas pixels — cheap, GPU-side anti-aliasing.
SUPERSAMPLE = 2
PALETTE = [
"#1d1d21",
"#e53935",
"#fb8c00",
"#fdd835",
"#43a047",
"#1e88e5",
"#8e24aa",
]
async def main(page: ft.Page):
page.title = "RawImage paint"
image = Image.new(
"RGBA", (CANVAS_WIDTH * SUPERSAMPLE, CANVAS_HEIGHT * SUPERSAMPLE), "white"
)
draw = ImageDraw.Draw(image)
# Frames arrive at SUPERSAMPLE times the control's size; fit=FILL
# scales them down and filter_quality blends the extra pixels away.
raw_image = ft.RawImage(
width=CANVAS_WIDTH,
height=CANVAS_HEIGHT,
fit=ft.BoxFit.FILL,
filter_quality=ft.FilterQuality.MEDIUM,
)
brush_color = PALETTE[0]
last_point: tuple[float, float] | None = None # in canvas pixels
# Pan events arrive faster than frames can be displayed, so handlers
# only mutate the Pillow image and flag it dirty; a single loop below
# streams the latest state as fast as the display confirms frames.
dirty = asyncio.Event()
def stroke_to(point: ft.Offset):
nonlocal last_point
# Pointer coordinates are in control (logical) pixels; the canvas
# lives at SUPERSAMPLE resolution.
x = point.x * SUPERSAMPLE
y = point.y * SUPERSAMPLE
radius = brush_size_slider.value / 2 * SUPERSAMPLE
if last_point is not None:
draw.line(
(last_point[0], last_point[1], x, y),
fill=brush_color,
width=int(radius * 2),
)
# Round cap: a line alone leaves flat, jagged joints.
draw.ellipse(
(x - radius, y - radius, x + radius, y + radius),
fill=brush_color,
)
last_point = (x, y)
dirty.set()
def pan_start(e: ft.DragStartEvent):
nonlocal last_point
last_point = None
stroke_to(e.local_position)
def pan_update(e: ft.DragUpdateEvent):
stroke_to(e.local_position)
def select_color(color: str):
nonlocal brush_color
brush_color = color
for dot in palette_row.controls:
dot.border = (
ft.Border.all(3, ft.Colors.BLUE_GREY_200) if dot.data == color else None
)
palette_row.update()
def clear_canvas():
draw.rectangle(
(0, 0, CANVAS_WIDTH * SUPERSAMPLE, CANVAS_HEIGHT * SUPERSAMPLE),
fill="white",
)
dirty.set()
palette_row = ft.Row(
[
ft.Container(
width=28,
height=28,
border_radius=14,
bgcolor=color,
data=color,
on_click=lambda e: select_color(e.control.data),
)
for color in PALETTE
],
spacing=8,
)
brush_size_slider = ft.Slider(min=2, max=40, value=8, width=150)
page.add(
ft.Row(
[
palette_row,
ft.Text("size:"),
brush_size_slider,
ft.OutlinedButton("Clear", on_click=clear_canvas),
],
spacing=10,
),
ft.Container(
content=ft.GestureDetector(
content=raw_image,
on_pan_start=pan_start,
on_pan_update=pan_update,
drag_interval=10,
),
border=ft.Border.all(1, ft.Colors.BLUE_GREY_200),
width=CANVAS_WIDTH,
height=CANVAS_HEIGHT,
),
)
select_color(brush_color)
async def render_loop():
while True:
await dirty.wait()
dirty.clear()
try:
# The canvas is opaque, so it is premultiplied by definition.
# Awaiting the render paces this loop to display speed: any
# strokes drawn meanwhile coalesce into the next frame.
await raw_image.render(image, premultiplied=True)
except (RuntimeError, TimeoutError):
return # window closed — session destroyed
dirty.set() # show the blank canvas
asyncio.create_task(render_loop())
if __name__ == "__main__":
ft.run(main)Mandelbrot 探索
点击即可放大进入 Mandelbrot 集合——每一次放大都是一串在后台线程中计算的 NumPy 渲染帧。
python
import asyncio
import time
from collections import deque
import numpy as np
import flet as ft
WIDTH = 640
HEIGHT = 480
MAX_ITER = 96
ZOOM_PER_CLICK = 8.0
ZOOM_FRAMES = 30
HOME = (-0.65, 0.0, 1.6) # center x, center y, half-width of the view
FPS_WINDOW_SECONDS = 2.0
def mandelbrot_frame(center_x: float, center_y: float, scale: float) -> np.ndarray:
"""
Escape-time Mandelbrot render, fully vectorized: points that stay
bounded for MAX_ITER iterations are painted black, escaped points get
a sine palette keyed on the escape iteration.
"""
aspect = HEIGHT / WIDTH
x = np.linspace(center_x - scale, center_x + scale, WIDTH)
y = np.linspace(center_y - scale * aspect, center_y + scale * aspect, HEIGHT)
c = x[None, :] + 1j * y[:, None]
z = np.zeros_like(c)
escape_iter = np.full(c.shape, MAX_ITER, dtype=np.int32)
alive = np.ones(c.shape, dtype=bool)
for i in range(MAX_ITER):
z[alive] = z[alive] ** 2 + c[alive]
escaped = alive & (np.abs(z) > 2.0)
escape_iter[escaped] = i
alive &= ~escaped
t = escape_iter / MAX_ITER
frame = np.zeros((HEIGHT, WIDTH, 4), dtype=np.uint8)
outside = ~alive
frame[outside, 0] = np.sin(t[outside] * 9.0) * 127 + 128
frame[outside, 1] = np.sin(t[outside] * 9.0 + 1.2) * 127 + 128
frame[outside, 2] = np.sin(t[outside] * 9.0 + 2.4) * 127 + 128
frame[..., 3] = 255
return frame
async def main(page: ft.Page):
page.title = "RawImage Mandelbrot"
# No fixed size: the control fills the window (expand=True) and CONTAIN
# scales the 640x480 frame into whatever box the layout provides, so the
# image grows and shrinks with the window.
raw_image = ft.RawImage(expand=True, fit=ft.BoxFit.CONTAIN)
fps_text = ft.Text("fps: —", size=12)
zoom_text = ft.Text("zoom: 1x", size=12)
hint_text = ft.Text("click to zoom in", size=12, italic=True)
view = HOME
frame_times: deque[float] = deque()
zoom_task: asyncio.Task | None = None
box_size = (float(WIDTH), float(HEIGHT)) # laid-out size of the control
def track_size(e: ft.LayoutSizeChangeEvent):
nonlocal box_size
box_size = (e.width, e.height)
raw_image.on_size_change = track_size
async def show(center_x: float, center_y: float, scale: float):
nonlocal view
view = (center_x, center_y, scale)
# The render itself is CPU-heavy — keep it off the event loop.
frame = await asyncio.to_thread(mandelbrot_frame, center_x, center_y, scale)
await raw_image.render(frame, premultiplied=True)
now = time.monotonic()
frame_times.append(now)
while frame_times and frame_times[0] < now - FPS_WINDOW_SECONDS:
frame_times.popleft()
fps_text.value = f"fps: {len(frame_times) / FPS_WINDOW_SECONDS:.1f}"
zoom_text.value = f"zoom: {HOME[2] / scale:,.0f}x"
page.update()
async def zoom_to(target_x: float, target_y: float, factor: float):
# A burst of ZOOM_FRAMES awaited renders: exponential interpolation
# of the view towards the target, each frame displayed as soon as
# the client acks the previous one.
start_x, start_y, start_scale = view
for i in range(1, ZOOM_FRAMES + 1):
k = i / ZOOM_FRAMES
f = factor**k
scale = start_scale / f
x = target_x + (start_x - target_x) / f
y = target_y + (start_y - target_y) / f
try:
await show(x, y, scale)
except (RuntimeError, TimeoutError):
return
def start_zoom(target_x: float, target_y: float, factor: float):
nonlocal zoom_task
if zoom_task is not None and not zoom_task.done():
return # ignore clicks while a zoom animation is running
zoom_task = asyncio.create_task(zoom_to(target_x, target_y, factor))
def tap(e: ft.TapEvent):
if e.local_position is None:
return
# With CONTAIN the frame is centered in the box with letterbox
# bands; undo that mapping to get frame-pixel coordinates.
box_width, box_height = box_size
display_scale = min(box_width / WIDTH, box_height / HEIGHT)
offset_x = (box_width - WIDTH * display_scale) / 2
offset_y = (box_height - HEIGHT * display_scale) / 2
frame_x = (e.local_position.x - offset_x) / display_scale
frame_y = (e.local_position.y - offset_y) / display_scale
if not (0 <= frame_x < WIDTH and 0 <= frame_y < HEIGHT):
return # tap landed on a letterbox band
center_x, center_y, scale = view
aspect = HEIGHT / WIDTH
x = center_x + (frame_x / WIDTH * 2 - 1) * scale
y = center_y + (frame_y / HEIGHT * 2 - 1) * scale * aspect
start_zoom(x, y, ZOOM_PER_CLICK)
def reset():
nonlocal zoom_task
if zoom_task is not None:
zoom_task.cancel()
zoom_task = None
asyncio.create_task(show(*HOME))
page.add(
ft.Row(
[
ft.OutlinedButton("Reset", on_click=reset),
zoom_text,
fps_text,
hint_text,
],
spacing=20,
),
ft.GestureDetector(content=raw_image, on_tap_down=tap, expand=True),
)
await show(*HOME)
if __name__ == "__main__":
ft.run(main)生命游戏
在微小网格上运行的康威生命游戏,使用清晰的邻近采样(nearest-neighbor)滤波放大;在模拟运行期间可用指针绘制格子。
python
import asyncio
import time
from collections import deque
import numpy as np
import flet as ft
GRID_WIDTH = 160
GRID_HEIGHT = 100
CELL_SIZE = 5 # logical pixels per cell on screen
FPS_WINDOW_SECONDS = 2.0
ALIVE_COLOR = (80, 250, 123)
DEAD_COLOR = (30, 32, 44)
def step(world: np.ndarray) -> np.ndarray:
"""One Game of Life generation on a wrapping (toroidal) grid."""
neighbors = sum(
np.roll(np.roll(world, dy, axis=0), dx, axis=1)
for dy in (-1, 0, 1)
for dx in (-1, 0, 1)
if (dy, dx) != (0, 0)
)
return (neighbors == 3) | (world & (neighbors == 2))
def world_frame(world: np.ndarray) -> np.ndarray:
frame = np.empty((GRID_HEIGHT, GRID_WIDTH, 4), dtype=np.uint8)
frame[..., :3] = np.where(world[..., None], ALIVE_COLOR, DEAD_COLOR)
frame[..., 3] = 255
return frame
async def main(page: ft.Page):
page.title = "RawImage Game of Life"
rng = np.random.default_rng(42)
world = rng.random((GRID_HEIGHT, GRID_WIDTH)) < 0.2
# One frame pixel per cell: the client upscales with nearest-neighbor
# (filter_quality=NONE), so each cell stays a crisp square and frames
# remain tiny (grid-sized) regardless of the on-screen size.
raw_image = ft.RawImage(
width=GRID_WIDTH * CELL_SIZE,
height=GRID_HEIGHT * CELL_SIZE,
fit=ft.BoxFit.FILL,
filter_quality=ft.FilterQuality.NONE,
)
playing = True
dirty = asyncio.Event()
frame_times: deque[float] = deque()
def toggle_playing():
nonlocal playing
playing = not playing
play_button.icon = ft.Icons.PAUSE if playing else ft.Icons.PLAY_ARROW
play_button.update()
dirty.set()
def randomize():
nonlocal world
world = rng.random((GRID_HEIGHT, GRID_WIDTH)) < 0.2
dirty.set()
def clear():
nonlocal world
world = np.zeros((GRID_HEIGHT, GRID_WIDTH), dtype=bool)
dirty.set()
def paint_cell(position: ft.Offset):
# Draw with the pointer, also while the simulation is running.
x = int(position.x // CELL_SIZE)
y = int(position.y // CELL_SIZE)
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
world[y, x] = True
dirty.set()
play_button = ft.IconButton(ft.Icons.PAUSE, on_click=toggle_playing)
speed_slider = ft.Slider(min=1, max=60, value=20, width=150)
fps_text = ft.Text("fps: —", size=12)
page.add(
ft.Row(
[
play_button,
ft.Text("speed:"),
speed_slider,
ft.OutlinedButton("Randomize", on_click=randomize),
ft.OutlinedButton("Clear", on_click=clear),
fps_text,
],
spacing=10,
),
ft.GestureDetector(
content=raw_image,
on_tap_down=lambda e: paint_cell(e.local_position),
on_pan_update=lambda e: paint_cell(e.local_position),
drag_interval=10,
),
)
async def run_loop():
nonlocal world
while True:
if playing:
world = step(world)
dirty.clear()
else:
# Paused: wait for pointer edits or the play button.
await dirty.wait()
dirty.clear()
try:
await raw_image.render(world_frame(world), premultiplied=True)
except (RuntimeError, TimeoutError):
return # window closed — session destroyed
now = time.monotonic()
frame_times.append(now)
while frame_times and frame_times[0] < now - FPS_WINDOW_SECONDS:
frame_times.popleft()
fps_text.value = f"fps: {len(frame_times) / FPS_WINDOW_SECONDS:.1f}"
try:
page.update()
except RuntimeError:
return
if playing:
await asyncio.sleep(1 / speed_slider.value)
asyncio.create_task(run_loop())
if __name__ == "__main__":
ft.run(main)属性
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fit | Optional[BoxFit] | None | Defines how to inscribe the current frame into the space allocated during layout. |
filter_quality | FilterQuality | FilterQuality.LOW | The rendering quality of the displayed frame. Defaults to LOW (bilinear), a good trade-off for streamed frames. Use NONE for crisp nearest-neighbor scaling of pixel-art-style content. |
scale | Number | 1.0 | How many physical frame pixels correspond to one logical pixel. Only affects the intrinsic size of this control when it is laid out without tight constraints: a 800x600 frame with scale=2 measures 400x300 logical pixels. Set it to the device pixel ratio when frames are rendered at physical resolution. |
ready_timeout | Optional[Number] | 5.0 | Seconds the render methods wait for the client widget to attach its data channel before raising TimeoutError. Set to None to wait indefinitely. |
ack_timeout | Optional[Number] | 10.0 | Seconds a render call waits for the client's frame-applied acknowledgment before raising TimeoutError. The ack normally arrives within milliseconds of the frame being displayed. The timeout is a liveness guard: transports drop frames sent while the client is disconnected (closed browser tab, network loss), and without it a while True: await raw_image.render(...) loop would wait for the lost ack forever. Raise it when streaming very large frames to slow remote clients, or set to None to wait indefinitely. |
width | Optional[Number] | None | Imposed Control width in virtual pixels. |
height | Optional[Number] | None | Imposed Control height in virtual pixels. |
left | Optional[Number] | None | The distance that the child's left edge is inset from the left of the stack. Note: Effective only if this control is a descendant of one of the following: Stack control, overlay list. |
top | Optional[Number] | None | The distance that the child's top edge is inset from the top of the stack. Note: Effective only if this control is a descendant of one of the following: Stack control, overlay list. |
right | Optional[Number] | None | The distance that the child's right edge is inset from the right of the stack. Note: Effective only if this control is a descendant of one of the following: Stack control, overlay list. |
bottom | Optional[Number] | None | The distance that the child's bottom edge is inset from the bottom of the stack. Note: Effective only if this control is a descendant of one of the following: Stack control, overlay list. |
align | Optional[Alignment] | None | Alignment of the control within its parent. |
margin | Optional[MarginValue] | None | Sets the margin of the control. |
rotate | Optional[RotateValue] | None | Transforms this control using a rotation around its center. The value of rotate property could be one of the following types: * number - a rotation in clockwise radians. Full circle 360° is math.pi * 2 radians, 90° is pi / 2, 45° is pi / 4, etc. * Rotate - allows to specify rotation angle as well as alignment - the location of rotation center. Example: python ft.Image( src="https://picsum.photos/100/100", width=100, height=100, border_radius=5, rotate=Rotate(angle=0.25 * pi, alignment=ft.Alignment.CENTER_LEFT) ) |
offset | Optional[OffsetValue] | None | Applies a translation transformation before painting the control. The translation is expressed as an Offset scaled to the control's size. So, Offset(x=0.25, y=0), for example, will result in a horizontal translation of one quarter the width of this control. Example The following example displays container at 0, 0 top left corner of a stack as transform applies -1 * 100, -1 * 100 (offset * control's size) horizontal and vertical translations to the control: python import flet as ft def main(page: ft.Page): page.add( ft.Stack( width=1000, height=1000, controls=[ ft.Container( bgcolor=ft.Colors.RED, width=100, height=100, left=100, top=100, offset=ft.Offset(-1, -1), ) ], ) ) ft.run(main) |
flip | Optional[Flip] | None | Flips this control horizontally and/or vertically. Set to an instance of Flip to mirror across x-axis, y-axis, or both. |
transform | Optional[Transform] | None | Applies a generic matrix transform to this control. Set to an instance of Transform with a recorded Matrix4 to describe arbitrary transform sequences. |
aspect_ratio | Optional[Number] | None | The aspect ratio of the control. It is defined as the ratio of width to height. Note: In current implementation, if aspect_ratio is set, width and height on the same control are ignored for final rendered size. |
animate_opacity | Optional[AnimationValue] | None | 启用 opacity 属性的隐式动画。 详见 此处. |
animate_size | Optional[AnimationValue] | None | 启用此控件尺寸的隐式动画。 If width or height is set, changes to those dimensions are animated. If neither dimension is set, changes to the control's natural size are animated instead. 详见 此处. |
animate_position | Optional[AnimationValue] | None | 启用定位属性(left、right、top、bottom)的隐式动画。 详见 此处. |
animate_align | Optional[AnimationValue] | None | 启用 align 属性的隐式动画。 详见 此处. |
animate_margin | Optional[AnimationValue] | None | 启用 margin 属性的隐式动画。 详见 此处. |
animate_rotation | Optional[AnimationValue] | None | 启用 rotate 属性的隐式动画。 详见 此处. |
animate_scale | Optional[AnimationValue] | None | 启用 scale 属性的隐式动画。 详见 此处. |
animate_offset | Optional[AnimationValue] | None | 启用 offset 属性的隐式动画。 详见 此处. |
size_change_interval | int | 10 | Sampling interval in milliseconds for on_size_change event. Setting to 0 calls on_size_change immediately on every change. |
expand | Annotated[Optional[Union[bool, int]], V.instance_of((bool, int))] | None | 指定此控件是否/如何扩展以填充父布局中的可用空间。 详见 此处. Note: Has effect only if the direct parent of this control is one of the following controls, or their subclasses: Column, Row, View, Page. Raises: ValueError: If it is not of type bool or int. |
expand_loose | bool | False | 允许控件在主轴方向上有可用空间时扩展,但不要求填满所有可用空间。 详见 此处. Note: 若 expand_loose 为 True,则仅当同时满足以下条件时生效: - expand 不为 None,且 - the direct parent of this control is one of the following controls, or their subclasses: Column, Row, View, Page. |
col | ResponsiveNumber | 12 | 若此控件的父控件是 ResponsiveRow,此属性用于决定该控件横跨多少个虚拟列。 可以是数字,也可以是为不同断点配置不同值的字典,例如 col={"sm": 6}。 0 表示在该断点下隐藏控件,不占用任何列。 默认横跨 12 个虚拟列。 \ |
opacity | Annotated[Number, V.between(0.0, 1.0)] | 1.0 | 定义控件的透明度,取值范围 0.0(完全透明)到 1.0(完全不透明)。 抛出异常:ValueError:若不在 0.0 到 1.0 的闭区间内。 |
tooltip | Optional[TooltipValue] | None | 鼠标悬停时显示的 tooltip。 |
badge | Optional[BadgeValue] | None | 显示在控件顶部的徽标。 |
visible | bool | True | Every control has visible property which is True by default - control is rendered on the page. Setting visible to False 完全阻止该控件(及其所有子控件)渲染到页面画布上. 隐藏的控件不能通过键盘或鼠标聚焦或选中,也不会触发任何事件。 |
disabled | bool | False | Every control has disabled property which is False by default - control and all its children are enabled. Note: The value of this property will be propagated down to all children controls recursively. Example: For example, if you have a form with multiple entry controls you can disable them all together by disabling container: python ft.Column( disabled = True, controls=[ ft.TextField(), ft.TextField() ] ) |
rtl | bool | False | 控件的文字方向是否为从右到左(RTL)。 |
data | Any | skip_ | 任意类型的自定义数据。 |
key | Optional['KeyValue'] | None | 用于在更新之间保持控件身份稳定的 key。 |
只读属性
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
parent | Optional['BaseControl'] | — | The direct ancestor(parent) of this control. It defaults to None and will only have a value when this control is mounted (added to the page tree). Page 控件(树的根)为例外 —— 它始终 parent=None。 |
page | 'Page' | — | 此控件所属的页面。 |
事件
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
on_data_channel_open | Optional[EventHandler[DataChannelOpenEvent]] | None | Framework hook — Dart fires this when it opens the data channel on mount. The default handler captures the channel used by the render methods; override only to do something extra at attach-time. |
on_size_change | Optional[EventHandler[LayoutSizeChangeEvent['LayoutControl']]] | None | Called when the size of this control changes. size_change_interval defines how often this event is called. |
on_animation_end | Optional[ControlEventHandler['LayoutControl']] | None | 动画结束时调用,可用于串联多个动画。 事件参数 data 属性包含动画的名称。 详见 此处. |
方法
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
init | None | — | — |
render | None | — | Display a Pillow image or a NumPy-style array and wait until the client has shown it. Args: image: A PIL.Image.Image (any mode; converted to RGBA as needed), or an object exposing __array_interface__ (e.g. a NumPy array) of shape (height, width, 4) or (height, width, 3) with uint8 values. premultiplied: Set to True if RGB values are already premultiplied by alpha to skip the premultiplication step. Raises: TypeError: If image is neither a Pillow image nor an array-like object. TimeoutError: If the client widget doesn't attach within ready_timeout. |
render_rgba | None | — | Display a raw RGBA8888 frame and wait until the client has shown it. Args: width: Frame width in physical pixels. height: Frame height in physical pixels. pixels: Tightly packed RGBA8888 bytes, width * height * 4 long. RGB values must be premultiplied by alpha (fully opaque frames are premultiplied by definition). premultiplied: Set to False if RGB values are straight (non-premultiplied); Pillow is then required to premultiply them. Raises: ValueError: If pixels has the wrong length. TimeoutError: If the client widget doesn't attach within ready_timeout. |
render_encoded | None | — | Display an encoded image (PNG, JPEG, WebP, ...) and wait until the client has shown it. The bytes still travel over the data channel (skipping the Flet protocol), but the client decodes them with its image codecs instead of a raw pixel upload. Args: data: Encoded image bytes. Raises: TimeoutError: If the client widget doesn't attach within ready_timeout. |
clear | None | — | Clear the displayed frame. |
is_isolated | — | — | Return whether this control is marked as isolated. 被隔离的控件不会参与父级驱动的更新遍历,需要自行管理更新边界。 |
build | — | — | 在控件初始化期间调用一次,用于定义其子控件。 page 属性在此方法中可用。 |
before_update | — | — | 每次控件即将更新时调用。 Note: 不要在此处调用 update()。 |
before_event | — | — | 在事件处理器执行前拦截事件。 返回 False 取消分发,返回 True 或 None 则继续正常的事件处理。 |
did_mount | — | — | 在控件挂载到页面树之后调用。 可重写以启动需要已挂载页面的资源,例如订阅、定时器或服务监听。 |
will_unmount | — | — | 在控件从页面树移除之前调用。 可重写以释放 did_mount() 中创建的资源,例如订阅、定时器或外部句柄。 |
get_data_channel | — | — | Resolve the [DataChannel] allocated on the Dart side for this widget. Pattern: on_data_channel_open: Optional[ft.EventHandler[DataChannelOpenEvent]] = None def init(self): self.on_data_channel_open = self._on_open def _on_open(self, e): self._channel = self.get_data_channel(e.channel_id) Idempotent — the underlying Connection caches DataChannels by id, so repeated calls return the same instance. No error path: the id always comes from a framework-fired event, so by the time this runs the channel exists on both sides. |
update | None | — | 请求更新该控件的 UI。 在修改控件状态或属性后调用。 控件必须已挂载到页面,并且未被冻结。 |