目录
使用 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: (Func<int, int>)(x => x + 1), inputs: textCount, outputs: textCount ); void RenderCount(int count) { var boxes = new List<Textbox>(); 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: (Func<string[], 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 atkeys=parameter)
key=用来告诉 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 List<string> { "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 List<TodoTask>()); var newTask = gr.Textbox(label: "Task Name", autofocus: true, maxLines: 1); object[] AddTask(List<TodoTask>? taskList, string? newTaskName) { var next = new List<TodoTask>(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(List<TodoTask>? taskList) { var list = taskList ?? new List<TodoTask>(); 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 用
task=task,C# 常见做法是 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: (Func<int, int>)(count => count + 1), inputs: trackCount, outputs: trackCount ); Button? mergeButton = null; Audio? outputAudio = null; void RenderTracks(int count) { var audios = new List<Audio>(); var volumes = new List<Slider>(); 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(Dictionary<object, 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 HashSet<Component>(audios.Cast<Component>().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驱动复杂动态应用。
下一篇
引入地址