UI 插件开发
原生视图、组件、事件回调与路由导航。
UI 插件通过 IDE 的原生渲染器展示界面。你在 plugin.toml 中声明视图贡献,在 Python 中创建视图模型、推送数据并响应事件。界面分为渲染器驱动视图(native.*)与原生组件节点两种形态。
两种渲染路径
一个视图在 IDE 中最终按以下规则渲染(plugin_view_surface.dart):
- 渲染器驱动:视图模型(
ViewModel)携带纯数据节点(树节点、表格行、日志条目等),宿主根据 manifest 中声明的renderer(如native.tree、native.log)选择对应的渲染器来绘制这些数据。这是self.views.tree(...)、self.views.form(...)等 facade 的默认形态。 - 组件树覆盖:视图模型的节点是一个独立的组件树(单个节点带
type字段,即Component的to_json()结果,或带component映射),宿主直接使用通用组件构建器渲染这棵组件树,完全忽略 manifest 中的renderer值。
# 方式一:渲染器驱动 —— manifest 的 renderer 决定绘制方式
view = self.views.tree("my-plugin.files", searchable=True)
view.set_items([
TreeItem(id="root", label="project", has_children=True),
])
view.open()
# 方式二:组件树覆盖 —— 单个组件树节点,忽略 renderer
model = self.views.create("my-plugin.main")
model.snapshot([
Column(
Text("Hello"),
TextField(id="name", label="用户名"),
Button(id="go", label="提交", on_press=lambda e: print("pressed")),
),
])
model.open()两种方式不能混用:若快照的第一个节点是组件树,整个视图都按组件树渲染;若节点是纯数据,则按 renderer 解释。renderer 在 manifest 中是必填字段且限定为 native.* 闭集,但组件树形态会覆盖它——因此 manifest 中 renderer 的值对组件树视图只是一个"合规占位"。
选择建议:
| 场景 | 推荐 |
|---|---|
| 表格、树、日志、表单等结构化数据 | 渲染器驱动(self.views.table/tree/log/form) |
| 精细自由布局、混排输入/文本/按钮 | 组件树(self.views.create + snapshot([...])) |
| 大纲、设备变量等 IDE 深度集成 | native.outline / native.variableInspector |
基本结构
以下是一个最小 UI 插件的完整结构:
from pyrite_sdk.api.native_views import FormField
from pyrite_sdk.core.plugin import UiPlugin
class MyPlugin(UiPlugin):
def __init__(self):
super().__init__()
self.main_view = self.views.form(
"my-plugin.main",
title="My Plugin",
)
self.main_view.set_items(
[FormField(id="message", label="Message", value="Hello World")]
)
def on_start(self):
print("started")
self.main_view.open()
def on_dispose(self):
print("disposed")
plugin = MyPlugin()
plugin.start()对应的 plugin.toml(manifest v2):
manifest_version = 2
id = "my-plugin"
name = "My Plugin"
version = "1.0.0"
type = "ui"
protocol_version = 1
python_version = "3.14"
author = "Your name"
activation_events = ["onView:my-plugin.main"]
permissions = ["ui.view"]
platforms = ["linux", "macos", "windows", "android"]
[[contributes.navigation_containers]]
id = "my-plugin"
title = "My Plugin"
icon = { material = "extension_outlined" }
[[contributes.views]]
id = "my-plugin.main"
container = "my-plugin"
title = "My Plugin"
renderer = "native.form"UI 插件必须至少声明一个 navigation_containers。视图 ID 必须使用 插件ID. 作为命名空间前缀。
视图声明(manifest)
[[contributes.views]] 中 renderer 决定 IDE 如何渲染该视图。when 字段可用条件表达式控制可见性(例如 when = "device.connected == true")。
渲染器类型
self.views 为每种渲染器提供对应的类型化 facade:
| 渲染器 | facade 方法 | 数据节点 | 典型用途 |
|---|---|---|---|
native.form | self.views.form(...) | FormField | 表单、配置面板 |
native.tree | self.views.tree(...) | TreeItem | 文件树、类结构树 |
native.virtualList | self.views.virtual_list(...) | VirtualListItem | 大量行的虚拟列表 |
native.table | self.views.table(...) | TableRowItem、TableColumn | 数据表格 |
native.markdown | self.views.markdown(...) | MarkdownContent | 文档、说明页 |
native.log | self.views.log(...) | LogEntry | 日志、串口输出 |
native.outline | self.views.outline(...) | OutlineItem | 大纲、符号列表 |
native.variableInspector | self.views.variable_inspector(...) | VariableEntry、VariableScope | 设备变量检查器 |
创建视图
所有 facade 的签名类似:
view = self.views.tree(
"my-plugin.files", # 对应 manifest 中的视图 ID
title="Files",
expanded_ids=["root"],
selected_id="root/main.py",
searchable=True,
empty_label="空目录",
)推送数据
from pyrite_sdk.api.native_views import TreeItem
view.set_items([
TreeItem(id="root", label="project", has_children=True),
TreeItem(id="root/main.py", label="main.py", parent_id="root"),
])列表类渲染器还提供 show_placeholder() 显示空状态:
view.show_placeholder("暂无数据", state="empty")配置属性
每种 facade 提供 configure() 或 set_props() 更新渲染配置:
view.configure(
item_height=32.0,
selected_id="item-1",
)视图模型
RendererView facade 内部封装一个 ViewModel(通过 view.model 访问)。ViewModel 负责快照/增量补丁的传输与修订号跟踪。你也可以直接用 self.views.create(view_id) 创建一个裸视图模型。
model = self.views.create("my-plugin.main")
model.snapshot([...]) # 发送完整快照
model.insert("id-1", {...}) # 增量插入节点
model.update("id-1", {...}) # 增量更新节点
model.remove("id-1") # 增量删除节点
model.move("id-1", 0) # 移动节点事件
行/节点事件
def on_select(item):
print("selected:", item.id)
view.on_select(on_select)
view.on_activate(lambda item: print("activated:", item.id))
view.on_request_children(lambda item: print("load children of", item.id))可见性
view.on_visibility(lambda visible: print("view visible:", visible))上下文菜单
from pyrite_sdk.api.components import MenuItem
def build_menu(item):
return [MenuItem(id="copy", label="复制"), MenuItem(id="delete", label="删除")]
view.on_context_menu(build_menu)操作按钮(AppBar Actions)
from pyrite_sdk.api.native_views import ViewAction, ViewMenuAction
from pyrite_sdk.api.icons import Icons
view.set_actions([
ViewAction(id="refresh", label="刷新", icon=Icons.refresh, on_trigger=lambda e: print("refresh")),
ViewMenuAction(
id="more", label="更多",
items=[
{"id": "a", "label": "选项 A", "type": "item"},
{"id": "b", "label": "选项 B", "type": "item"},
],
on_trigger=lambda e: print("menu"),
),
])路由导航
视图模型维护自己的路由栈。on_route() 注册处理器,在每次路由同步时以 (route, params) 调用:
model = self.views.create("my-plugin.main")
@model.on_route
def on_route(route, params):
print("route:", route, params)
model.push_route("detail", params={"id": "42"})
model.replace_route("detail", params={"id": "43"})
model.goto_route("home")
model.pop_route()| 方法 | 行为 |
|---|---|
push_route(route, params=..., callback=...) | 压栈导航,可返回 |
replace_route(route, params=...) | 替换当前路由 |
goto_route(route, params=...) | 重置栈并跳转 |
pop_route(callback=...) | 返回上一路由 |
路由栈由 IDE 持有并回推(ide.view.route.sync),插件端镜像最后同步结果。可读取 model.route、model.route_stack、model.route_params。
标签页(Tabs)
插件可以请求 IDE 将某个已贡献视图打开为标签页:
self.tabs.create_view(
"my-plugin.main",
title="我的视图",
expansion=True,
callback=lambda **cb: print("instance:", cb),
)回调收到 instance=TabViewInstance(包含 plugin_id、session_id、view_id、instance_id)。需要 permissions = ["tab.create"] 或 tab.manage。
原生组件
pyrite_sdk.api.components 提供声明式组件节点(普通字典,序列化为快照/补丁)。组件适合需要精细布局的界面,通过 ViewModel.snapshot() 推送,并由 IDE 的组件注册表校验。
from pyrite_sdk.api import components as c
from pyrite_sdk.core.plugin import UiPlugin
class ComponentPlugin(UiPlugin):
def __init__(self):
super().__init__()
self.model = self.views.create("my-plugin.main")
self._build()
def _build(self):
self.model.snapshot([
c.Column(
c.Text("Hello from PyriteSDK"),
c.TextField(
id="name",
label="用户名",
placeholder="请输入",
on_change=lambda payload: print("changed:", payload),
),
c.Button(
id="go", label="提交",
on_press=lambda payload: print("pressed"),
),
),
])组件分类
| 分类 | 组件 |
|---|---|
| 布局 | Row、Column、Flex、Grid、Wrap、SplitView、Tabs/Tab、Section、Toolbar |
| 文本与展示 | Text、Icon、Image、Video、Markdown、CodeBlock、Badge |
| 输入 | TextField、NumberField、Select、Checkbox、Switch、Slider |
| 菜单与对话框 | Menu、MenuBar、ContextMenu、Dropdown、Dialog、Tooltip |
| 操作 | Button、IconButton |
| 数据 | VirtualList、TreeView、DataTable、PropertyGrid |
事件绑定
组件事件有两种绑定方式:构造参数(如 on_press=、on_change=)或 .on(event, handler):
button = c.Button(id="go", label="提交")
button.on("press", lambda payload: print("pressed"))Icon、Image、Video 等组件使用 self.resources.asset() 生成的 PluginResource:
from pyrite_sdk.api.icons import Icons
c.Image(self.resources.asset("assets/preview.png"), width=320)
c.Video(
self.resources.asset("assets/demo.mp4"),
width=480,
height=270,
autoplay=True,
show_controls=True,
)
c.Icon(Icons.home, size=24)资源必须是 assets/ 目录下的相对路径,且必须在 plugin.toml 的图标/资源声明中被引用或随包分发。
命令式控制器
挂载的组件可通过 .controller 调用命令式方法(需要组件带 id 并已绑定到视图模型):
view.controller.play() # Video: 播放
view.controller.seek_to(3000) # Video: 跳转到 3000ms
text_field.controller.set_text("hello")
tree.controller.expand("root")
menu.controller.open()
dialog.controller.close(result="ok")页面结构
自由组件树可用 Scaffold 和 AppBar 形成与 IDE 原生页面一致的结构:
from pyrite_sdk.api import components as c
from pyrite_sdk.api.icons import Icons
self.model.snapshot([
c.Scaffold(
c.Canvas(
id="preview",
interactive=True,
ops=self._build_preview_ops(),
),
app_bar=c.AppBar(
c.IconButton(
id="refresh",
icon=Icons.refresh,
tooltip="刷新",
on_press=lambda event: self.refresh(),
),
title="预览",
),
),
])AppBar 的操作子组件限于 IconButton、Menu 和 Dropdown。组件树标题栏只渲染这里声明的操作,不合并 Manifest 视图菜单。Scaffold 会限制正文的主轴尺寸,Canvas、虚拟列表和表格可以直接占用剩余空间。
Canvas 的 ops 随视图模型持久同步,控制器写入的覆盖层用于光标、选框和临时预览。覆盖层在视图重新挂载或 resync 后清空。完整的绘制操作、视口、事件和 2 MiB 调用上限见 components API。
权限
UI 视图相关的权限在 plugin.toml 的 permissions 中声明,常见取值:
| 权限 | 用途 |
|---|---|
ui.view | 创建/渲染视图 |
ui.navigate | 视图路由导航 |
ui.notify | 显示消息通知 |
tab.create / tab.manage | 创建/管理标签页 |
editor.read / editor.write | 编辑器读写 |
file.read / file.write | 本地文件读写 |
board.read / board.write | 设备文件读写 |
serial.read / serial.write | 串口读写 |
runtime.inspect | 运行时检查 |