Gradio.Net 开发指南 -- 使用 Render 方法构建动态应用

发布时间:2026/8/5 13:48:48
Gradio.Net 开发指南 -- 使用 Render 方法构建动态应用 目录使用 Render 方法构建动态应用动态组件数量动态事件监听器进一步理解 key 参数Closer Look at keys parameter综合示例总结上一篇Gradio.Net (https://github.com/feiyun0112/Gradio.Net)是一个开源的 .NET 库它是 Gradio 的 .NET 移植版本允许你为机器学习模型、API 或任何 C# 函数快速构建演示或 Web 应用程序无需任何 JavaScript、CSS 或 Web 开发经验使用 Render 方法构建动态应用到目前为止你在Blocks中定义的组件和事件监听器通常是固定的应用启动后不能再新增监听器或组件也不能移除已有的。gr.Render让 UI 可以根据输入动态变化。下面我们一步步来看。动态组件数量下面这个例子会动态创建 Textbox当用户修改输入框内容时我们为每个字符生成一个 Textbox。using Gradio.Net; using Gradio.Net.Components; using Gradio.Net.Core; using var demo gr.Blocks(); var inputText gr.Textbox(label: input); void ShowSplit(string? text) { if (string.IsNullOrEmpty(text)) { _ gr.Markdown(## No Input Provided); } else { foreach (var letter in text) { _ gr.Textbox(letter.ToString()); } } } gr.Render(fn: ShowSplit, inputs: inputText); await demo.Launch();可以看到我们通过自定义逻辑这里是for循环实现了组件数量随输入变化。gr.Render的核心步骤是创建函数并将其传递给 ‎gr.Render。在 ‎inputs里传入输入组件并在函数参数中接收它们。每当输入组件发生变化函数会自动重新执行。把需要动态渲染的组件写在渲染函数内部。每次输入变化时渲染函数都会重新执行并替换上一次渲染出来的组件。下面增加一点复杂度using Gradio.Net; using Gradio.Net.Components; using Gradio.Net.Core; using Gradio.Net.Events; using var demo gr.Blocks(); var inputText gr.Textbox(label: input); var mode gr.Radio(new[] { textbox, button }, value: textbox); void ShowSplit(string? text, string? selectedMode) { if (string.IsNullOrEmpty(text)) { _ gr.Markdown(## No Input Provided); return; } foreach (var letter in text) { if (selectedMode textbox) { _ gr.Textbox(letter.ToString()); } else { _ gr.Button(letter.ToString()); } } } var submitTrigger new EventListener(submit, hasTrigger: true) { Self inputText }; gr.Render( fn: ShowSplit, inputs: new Component[] { inputText, mode }, triggers: new [] { submitTrigger }); await demo.Launch();默认情况下gr.Render会在应用.load和输入组件.change时触发。你也可以像上面这样显式设置触发器例如只在inputText.submit时触发。如果你自定义了触发器又希望应用启动时先自动渲染一次请把demo.load也加入触发列表。动态事件监听器动态生成组件时通常也需要动态绑定事件。下面这个例子会根据数量创建多个 Textbox并在点击按钮后合并文本。using Gradio.Net; using Gradio.Net.Components; using Gradio.Net.Core; using var demo gr.Blocks(); var textCount gr.State(value: 1); var addButton gr.Button(Add Box); var mergeButton gr.Button(Merge); var output gr.Textbox(label: Merged Output); _ addButton.Click( fn: (Funcint, int)(x x 1), inputs: textCount, outputs: textCount ); void RenderCount(int count) { var boxes new ListTextbox(); for (var i 0; i count; i) { var box gr.Textbox(key: i, label: $Box {i}); boxes.Add(box); } string Merge(params string[] args) { return string.Join( , args.Where(x !string.IsNullOrWhiteSpace(x))); } _ mergeButton.Click( fn: (Funcstring[], string)Merge, inputs: boxes, outputs: output ); } gr.Render(fn: RenderCount, inputs: textCount); await demo.Launch();这个例子有三个关键点‎textCount记录需要创建多少个输入框点击 Add 后状态变化触发重新渲染。每个动态创建的 Textbox 都设置了 ‎key这样重渲染时可以保留组件值。如果你在某个 Textbox 中输入内容后点击 Add所有 Textbox 都会重新渲染但值不会丢失因为 ‎key在渲染间维持了组件的值。使用渲染函数内创建的组件的事件监听器也必须定义在同一个渲染函数里。监听器仍然可以引用渲染函数外部的组件例如这里的 ‎mergeButton和 ‎output。和组件一样重渲染时旧监听器会被清理新监听器会被重新绑定。进一步理解key参数Closer Look atkeysparameterkey用来告诉 Gradio重渲染后这个组件仍然是同一个组件。这会带来两点好处浏览器可以复用同一个 DOM 元素性能更好且保留浏览器侧状态。如果组件嵌套在 ‎gr.Row等布局中布局也需要设置 ‎key因为父级的 key 也必须匹配。用户修改过的属性可被保留。默认保留 ‎value也可以通过 ‎preserved_by_key指定更多属性。using Gradio.Net; using Gradio.Net.Components; using Gradio.Net.Core; using Gradio.Net.Core.Layouts; var random new Random(); using var demo gr.Blocks(); var numberOfBoxes gr.Slider(1, 5, step: 1, value: 3, label: Number of Boxes); void CreateBoxes(double countValue) { var count Math.Max(1, (int)countValue); for (var i 0; i count; i) { using (gr.Row(key: $row-{i})) { var numberBox gr.Textbox( label: Default Label, key: $box-{i}, preservedByKey: new Liststring { label, value }, interactive: true, info: Default Info ); var changeLabelButton gr.Button(Change Label, key: $btn-{i}); _ changeLabelButton.Click( fn: () gr.Textbox( label: random.Next(0, 5) switch { 0 A, 1 B, 2 C, 3 D, _ E }, info: random.Next(0, 5) switch { 0 A, 1 B, 2 C, 3 D, _ E } ), outputs: numberBox ); } } } gr.Render(fn: CreateBoxes, inputs: new [] { numberOfBoxes }); await demo.Launch();在这个例子中当你改变numberOfBoxes滑块时会触发重渲染。如果你点击Change Label按钮会随机修改对应 Textbox 的label和info属性你也可以在 Textbox 中输入文本修改其值。再次改变滑块数量后info会被重置但label和输入值仍会保留因为preserved_by_key指定了要保留的属性。另外事件监听器也可以设置key如button.click(key: ...)在频繁重渲染场景下可获得更稳定的行为与更好的性能也能避免上一次渲染触发的事件在新渲染后返回数据时出现路由错误。综合示例下面先看一个待办事项应用using Gradio.Net; using Gradio.Net.Components; using Gradio.Net.Core; using Gradio.Net.Core.Layouts; using var demo gr.Blocks(); var tasks gr.State(value: new ListTodoTask()); var newTask gr.Textbox(label: Task Name, autofocus: true, maxLines: 1); object[] AddTask(ListTodoTask? taskList, string? newTaskName) { var next new ListTodoTask(taskList ?? new()); if (!string.IsNullOrWhiteSpace(newTaskName)) { next.Add(new TodoTask { Name newTaskName, Complete false }); } return new object[] { next, string.Empty }; } _ newTask.Submit( fn: AddTask, inputs: new object[] { tasks, newTask }, outputs: new object[] { tasks, newTask } ); void RenderTodos(ListTodoTask? taskList) { var list taskList ?? new ListTodoTask(); var complete list.Where(t t.Complete).ToList(); var incomplete list.Where(t !t.Complete).ToList(); _ gr.Markdown($### Incomplete Tasks ({incomplete.Count})); foreach (var task in incomplete) { var currentTask task; using (gr.Row()) { _ gr.Textbox(currentTask.Name, showLabel: false, container: false); var doneButton gr.Button(Done, scale: 0); _ doneButton.Click( fn: () { currentTask.Complete true; return list; }, outputs: tasks ); var deleteButton gr.Button(Delete, scale: 0, variant: stop); _ deleteButton.Click( fn: () { list.Remove(currentTask); return list; }, outputs: tasks ); } } _ gr.Markdown($### Complete Tasks ({complete.Count})); foreach (var task in complete) { _ gr.Textbox(task.Name, showLabel: false, container: false); } } gr.Render(fn: RenderTodos, inputs: tasks); await demo.Launch(); class TodoTask { public string Name { get; set; } string.Empty; public bool Complete { get; set; } }可以看到几乎整个 UI 都放在一个响应gr.State的渲染函数里。对于这种列表/字典结构状态请特别注意会改变状态并希望触发重渲染的监听器必须把状态组件作为输出让 Gradio 感知到变化并触发重渲染。在循环中给监听器闭包捕获变量时要冻结当前循环变量Python 用 ‎tasktaskC# 常见做法是 ‎var currentTask task。最后看一个多轨音频混合器示例using Gradio.Net; using Gradio.Net.Components; using Gradio.Net.Core; using Gradio.Net.Core.Layouts; using var demo gr.Blocks(); var trackCount gr.State(value: 1); var addTrackButton gr.Button(Add Track); _ addTrackButton.Click( fn: (Funcint, int)(count count 1), inputs: trackCount, outputs: trackCount ); Button? mergeButton null; Audio? outputAudio null; void RenderTracks(int count) { var audios new ListAudio(); var volumes new ListSlider(); using (gr.Row()) { for (var i 0; i count; i) { using (gr.Column(variant: panel, minWidth: 200)) { _ gr.Textbox(placeholder: Track Name, key: $name-{i}, showLabel: false); var trackAudio gr.Audio(label: $Track {i}, key: $track-{i}, type: filepath); var trackVolume gr.Slider(0, 100, value: 100, label: Volume, key: $volume-{i}); audios.Add(trackAudio); volumes.Add(trackVolume); } } } string? Merge(Dictionaryobject, object? data) { var selected string.Empty; var bestVolume double.MinValue; for (var i 0; i audios.Count; i) { var audioObj data.TryGetValue(audios[i], out var a) ? a : null; var volumeObj data.TryGetValue(volumes[i], out var v) ? v : null; var path audioObj?.ToString(); if (string.IsNullOrWhiteSpace(path)) { continue; } var volume 100.0; if (volumeObj ! null double.TryParse(volumeObj.ToString(), out var parsed)) { volume parsed; } if (volume bestVolume) { bestVolume volume; selected path; } } return selected; } var allInputs new HashSetComponent(audios.CastComponent().Concat(volumes)); _ mergeButton!.Click(fn: Merge, inputs: allInputs, outputs: outputAudio!); } gr.Render(fn: RenderTracks, inputs: trackCount); mergeButton gr.Button(Merge Tracks); outputAudio gr.Audio(label: Output, interactive: false, type: filepath); await demo.Launch();这个示例也展示了两个实践点动态组件建议全部设置 ‎key防止重渲染后已输入值丢失。当输入组件数量和类型都很多时使用集合与字典方式组织输入而不是固定参数列表通常更容易维护。上面代码中我们将所有 ‎Audio和 ‎Slider组件合并为一个集合传给 ‎Merge函数在函数体内通过字典查询各组件的值。gr.Render显著扩展了 Gradio.Net 的表达能力可以让 UI 真正按状态生长总结本章你学习了如何使用gr.Render实现动态创建/替换组件在重渲染中正确绑定事件通过 ‎key与 ‎preserved_by_key保持状态用 ‎gr.State驱动复杂动态应用。下一篇引入地址