UI 插件开发

原生视图、组件、事件回调与路由导航。

UI 插件通过 IDE 的原生渲染器展示界面。你在 plugin.toml 中声明视图贡献,在 Python 中创建视图模型、推送数据并响应事件。界面分为渲染器驱动视图native.*)与原生组件节点两种形态。

两种渲染路径

一个视图在 IDE 中最终按以下规则渲染(plugin_view_surface.dart):

  • 渲染器驱动:视图模型(ViewModel)携带纯数据节点(树节点、表格行、日志条目等),宿主根据 manifest 中声明的 renderer(如 native.treenative.log)选择对应的渲染器来绘制这些数据。这是 self.views.tree(...)self.views.form(...) 等 facade 的默认形态。
  • 组件树覆盖:视图模型的节点是一个独立的组件树(单个节点带 type 字段,即 Componentto_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.formself.views.form(...)FormField表单、配置面板
native.treeself.views.tree(...)TreeItem文件树、类结构树
native.virtualListself.views.virtual_list(...)VirtualListItem大量行的虚拟列表
native.tableself.views.table(...)TableRowItemTableColumn数据表格
native.markdownself.views.markdown(...)MarkdownContent文档、说明页
native.logself.views.log(...)LogEntry日志、串口输出
native.outlineself.views.outline(...)OutlineItem大纲、符号列表
native.variableInspectorself.views.variable_inspector(...)VariableEntryVariableScope设备变量检查器

创建视图

所有 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.routemodel.route_stackmodel.route_params

标签页(Tabs)

插件可以请求 IDE 将某个已贡献视图打开为标签页:

self.tabs.create_view(
    "my-plugin.main",
    title="我的视图",
    expansion=True,
    callback=lambda **cb: print("instance:", cb),
)

回调收到 instance=TabViewInstance(包含 plugin_idsession_idview_idinstance_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"),
                ),
            ),
        ])

组件分类

分类组件
布局RowColumnFlexGridWrapSplitViewTabs/TabSectionToolbar
文本与展示TextIconImageVideoMarkdownCodeBlockBadge
输入TextFieldNumberFieldSelectCheckboxSwitchSlider
菜单与对话框MenuMenuBarContextMenuDropdownDialogTooltip
操作ButtonIconButton
数据VirtualListTreeViewDataTablePropertyGrid

事件绑定

组件事件有两种绑定方式:构造参数(如 on_press=on_change=)或 .on(event, handler)

button = c.Button(id="go", label="提交")
button.on("press", lambda payload: print("pressed"))

IconImageVideo 等组件使用 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")

页面结构

自由组件树可用 ScaffoldAppBar 形成与 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 的操作子组件限于 IconButtonMenuDropdown。组件树标题栏只渲染这里声明的操作,不合并 Manifest 视图菜单。Scaffold 会限制正文的主轴尺寸,Canvas、虚拟列表和表格可以直接占用剩余空间。

Canvas 的 ops 随视图模型持久同步,控制器写入的覆盖层用于光标、选框和临时预览。覆盖层在视图重新挂载或 resync 后清空。完整的绘制操作、视口、事件和 2 MiB 调用上限见 components API

权限

UI 视图相关的权限在 plugin.tomlpermissions 中声明,常见取值:

权限用途
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运行时检查

On this page