Skip to content

WinUI 参考: 完整的属性表面与设计建议,参见 Text Controls

文本与媒体控件是 Microsoft.UI.Reactor(Reactor)目录里负责展示、富文本和媒体的那一半:呈现用户正在阅读、观看或检视的内容,外加一个用于富文本编辑的 RichEditBox。它们大多是 WinUI 文本与媒体表面之上的薄包装,因此修饰符名称和无障碍行为与 WinUI 开发者所熟悉的完全一致。两个例外是 Markdown(string) —— 一个 Reactor 原创渲染器,把 GFM 风格的 Markdown 解析成与其余 UI 相同的元素树,无需绕道 WebView —— 以及语义化文本变体 Heading / SubHeading,它们预设了排版,使无障碍工具无需手工设置 AutomationProperties 就能推断文档大纲。目录在这里做的取舍偏向组合:丰富版式来自 VStack 里许多小的文本元素,而不是一个靠手工调校 Inline 记录的巨型 RichTextBlock。先扫一遍修饰符表格再看正文,然后跳到你需要的控件。

文本与媒体

本页覆盖 Reactor 中的展示文本、内联富文本、富文本编辑和媒体控件。单行输入控件(TextBoxPasswordBox)见表单。数据绑定集合见集合

文本变体

Title(string)           // WinUI title 文本样式,约 28pt
Heading(string)         // 语义化标题,约 28pt
SubHeading(string)      // 子分区标题头,约 20pt
Subtitle(string)        // 标题下方的辅助行
BodyLarge(string)       // 引导段落
TextBlock(string)       // 正文散文
BodyStrong(string)      // 正文散文,半粗
Caption(string)         // 约 12pt 的元信息文本
class TextVariantsDemo : Component
{
    public override Element Render() => VStack(8,
        Title("Title — the largest variant"),
        Heading("Heading — page or section title"),
        SubHeading("SubHeading — region header"),
        Subtitle("Subtitle — supporting line under a heading"),
        BodyLarge("BodyLarge — lead paragraph."),
        TextBlock("Body text. The default size and weight for prose."),
        BodyStrong("BodyStrong — emphasis within body copy."),
        Caption("Caption — secondary metadata, dates, labels.")
    ).Padding(24);
}

堆叠的 Heading / SubHeading / TextBlock / Caption

HeadingSubHeadingCaption 返回带预设字号的 TextBlockElementHeadingSubHeading 还设置了标题的无障碍级别。它们是建立文档大纲的正确工具 —— 屏幕阅读器和无障碍扫描器把它们当作层级地标,而一个用 .FontSize(24).Bold() 装扮的裸 TextBlock 只是视觉上大而已。优先用变体;只有当大纲中的位置与你想要的视觉重量不匹配时,才去用 TextBlock 上的修饰符。

工厂方法 默认字号 何时使用
Title 约 28pt,半粗 需要 WinUI title 级字号但不需要标题语义时。
Heading 约 28pt,粗 每页一个 —— 语义化的文档标题。
SubHeading 约 20pt,半粗 长页面内的分区标题头。
Subtitle 约 20pt,常规 紧贴在标题下方的辅助行。
BodyLarge 约 18pt 引导段落或强调性散文。
TextBlock 正文 段落、标签、行内帮助。
BodyStrong 正文,半粗 正文中的强调,且不跳字号。
Caption 约 12pt 时间戳、元信息、字段下方的辅助文本。

WinUI 设计页:Typography in Windows 11

TextBlock 修饰符

class TextBlockModifiersDemo : Component
{
    public override Element Render() => VStack(8,
        TextBlock("Bold + sized").Bold().FontSize(18),
        TextBlock("Selectable so the user can copy.").IsTextSelectionEnabled(),
        TextBlock(
            "A long paragraph that demonstrates wrapping behavior. " +
            "Without TextWrapping, content stays on one line and is " +
            "clipped or scrolls. With TextWrapping.Wrap, the block " +
            "flows across multiple lines inside its width.")
            .TextWrapping()
            .MaxLines(2)
            .TextTrimming(Microsoft.UI.Xaml.TextTrimming.WordEllipsis)
            .Width(320)
    ).Padding(24);
}

粗体/字号、可选中、换行+截断三种变体的 TextBlock

最常够到的流畅方法:

流畅方法 效果
.Bold() / .SemiBold() 设置 FontWeight。如果是分区标题头,改用 Heading
.FontSize(double) 覆盖变体默认值。
.FontFamily(string) 字体族名或 Microsoft.UI.Xaml.Media.FontFamily
.TextWrapping() 默认不换行;调用它才会跨行流动。
.MaxLines(int) 限制可见行数 —— 与 TextTrimming 搭配。
.TextTrimming(mode) CharacterEllipsis / WordEllipsis / Clip / None
.TextAlignment(alignment) Left / Right / Center / Justify
.LineHeight(double) 覆盖行盒高度(对密集列表很有用)。
.IsTextSelectionEnabled() 让用户可选中并复制文本。
.CharacterSpacing(int) 以 em 的百分之一为单位 —— 30 ≈ 0.3em 字距。

注意: TextBlock 有一条快路径渲染器,只有在你通过工厂方法的字符串重载设置 Text 时(TextBlock("…"))才会启用。一旦你改用 RichTextBlock 走内联、把 CharacterSpacing 改成非零值,或把 TextTrimming 设为 Clip,布局就会回落到慢路径,每次测量的 CPU 开销大约翻倍。上千行的列表上这很要紧;静态散文则无所谓。WinUI 的调试属性 IsTextPerformanceVisualizationEnabled 会把快路径文本高亮成绿色 —— 剖析一个滚动密集的界面时把它打开,把凡是没变绿的控件降级为带修饰符的普通 TextBlock

RichTextBlock

RichTextBlock(string text)
RichTextBlock(RichTextParagraph[] paragraphs)
class RichTextDemo : Component
{
    public override Element Render() => VStack(8,
        SubHeading("Inline-formatted prose"),
        RichTextBlock([
            Paragraph(
                Run("Tap the "),
                Hyperlink("docs",
                    new Uri("https://learn.microsoft.com/windows/apps/")),
                Run(" to keep reading.")),
            Paragraph(
                Run("Reactor builds the paragraph tree from value-typed " +
                    "records. No XAML inlines, no DataTemplate."))
        ]).LineHeight(22).Width(420)
    ).Padding(24);
}

跨两段、带内联超链接的 RichTextBlock

RichTextBlock 用于混合 run 与内联元素的段落 —— 超链接、句中的粗体、颜色切换。请节制使用:一组字段放在表单里更合适,一篇跨屏的文章放在 Markdown(string) 里更合适。当结构是动态时(一条带 @提及的聊天消息、一条把查询词加粗的搜索结果),RichTextBlock 才是对的工具 —— 因为你每次渲染都从数据构建 Paragraph[]

辅助器 用途
Paragraph(params RichTextInline[]) 一个段落;每个视觉断行一个。
Run(string text) 一段纯文本片段。
Hyperlink(string text, Uri target) 带导航目标的内联链接。
Hyperlink(string text, Action onClick) 可点击的内联片段 —— 触发委托并抑制平台导航。用于让单个富文本 run 变得可交互(打开编辑器、派发命令),而无须逃逸到托管的原生子树。
InlineUI(Element child) 通过 InlineUIContainer 在段落中间嵌入一个活的控件(图表、滑块、按钮)。

修饰符 —— .MaxLines.LineHeight.TextAlignment.TextTrimming.CharacterSpacing —— 与 TextBlock 一致。内联 run 不走快路径渲染器,因此静态文本优先用普通 TextBlock

注意:InlineUI(...) 嵌入活控件、然后再修改同一段落里的某个 Run,曾经会把外围的 ScrollViewer/ScrollView 滚到顶部:WinUI 的文本引擎会从头重新测量该段落,而嵌入元素在其中一次布局传递里贡献零高度,于是滚动宿主从容地把偏移悄悄夹到 0,且再不恢复(issue #487)。Reactor 现在已经替你处理了 —— ScrollViewer(RichTextBlock(...)) 会自动在内联 UI 变更之间保持用户的滚动偏移,不需要额外 API,也不需要每个应用各写一套绕法。像往常一样把 RichTextBlock 包进滚动宿主即可。

WinUI 设计页:Rich text block

RichEditBox

RichEditBox(Optional<string> text = default, Action<string>? onTextChanged = null)
class RichEditDemo : Component
{
    public override Element Render()
    {
        var (text, setText) = UseState(
            "Edit me. RichEditBox supports paste-with-formatting, " +
            "spell-check, and Enter for new paragraphs.");

        return VStack(8,
            SubHeading("RichEditBox"),
            RichEditBox(text, setText)
                .AcceptsReturn()
                .IsSpellCheckEnabled()
                .TextWrapping()
                .Height(160).Width(420)
        ).Padding(24);
    }
}

按段落编辑尺寸设置的 RichEditBox

RichEditBoxRichTextBlock 的可编辑对应物 —— 多行文本,支持从带格式来源粘贴、拼写检查、输入法组合和选区。变更处理器带的是纯文本内容;要拿带格式的输出,你得通过 .Set(...)RichEditBox.Document,自己序列化那个文本区间。单行文本输入属于 TextBox,不在这里。

流畅方法 效果
.AcceptsReturn(bool) 回车开启新段落,而不是提交。
.TextWrapping() 换行(默认)或不换行。
.MaxLength(int) 限制输入长度。
.IsSpellCheckEnabled(bool) 开关波浪下划线。
.SelectionHighlightColor(brush) 覆盖选区背景色。
.TextChanged(Action<string>) 在构造参数之外订阅。

WinUI 设计页:Rich edit box

Markdown(Reactor 原创)

Markdown(string markdown)
Markdown(string markdown, MarkdownOptions options)

包说明。 Markdown(...) 由可选的 Microsoft.UI.Reactor.Advanced 包提供(spec 062 §7)。加上 <PackageReference Include="Microsoft.UI.Reactor.Advanced" Version="0.1.0-preview.15" />,并把它与核心工厂一并导入:using static Microsoft.UI.Reactor.Advanced.Factories;。两个重载都返回基类型 Element,而 MarkdownOptions 仍在 Microsoft.UI.Reactor.Markdown 里 —— 所以用 options 重载时还需要 using Microsoft.UI.Reactor.Markdown;

class MarkdownDemo : Component
{
    public override Element Render()
    {
        const string source =
            "# Release notes\n\n" +
            "Reactor **0.42** ships:\n\n" +
            "- Compositor animations via `UseAnimation`.\n" +
            "- A new [Markdown](https://example.com) renderer.\n" +
            "- Bug fixes for `LazyVStack` keyed reorder.\n\n" +
            "> Migration guide lives in the spec.\n";

        return VStack(8,
            SubHeading("Markdown"),
            Markdown(source)
        ).Padding(24).Width(440);
    }
}

Markdown 渲染 —— 标题、列表、链接、引用块

Markdown 是本页最大的 Reactor 原创控件。它用内嵌的 md4c 解析器解析 GitHub 风格的 Markdown,并产出一棵 Reactor 元素树:标题变成带标题变体的 TextBlock,列表项变成两列 GridAuto 标记 / * 内容),链接变成内联超链接,代码段变成等宽 TextBlock。没有 WebView,没有 HTML 往返 —— 产出物与本页其他所有修饰符(.Padding.Width.TextWrapping)都能组合。

WebView2 相比的取舍是保真度:Markdown 不支持任意 HTML、内嵌 <script> 或 CSS。它支持段落、标题(h1–h6)、列表(无序、有序、嵌套)、行内代码、可选语言标记的围栏代码块、链接、图像、粗体、斜体、删除线、引用块、硬换行和表格。超出这些的 —— 内嵌视频、自定义布局、带主题的语法高亮代码 —— 就得退回 WebView2 加一份 HTML 渲染的兜底,或另做一个查看器。

它适用于发布说明、应用内帮助、对话式 AI 输出、用户创作的长文(提交说明、知识库文章),或任何源文本是纯字符串、读者只需要一层薄薄格式的表面。

用 Markdown 的时机 用 RichTextBlock 的时机
源内容以字符串形式创作(LLM 输出、README、用户评论) 内联结构由带类型的数据算出
你需要列表、代码块、引用块 带行内格式的纯段落就够了
完整的 GFM 子集可以接受 你需要任意类型的 Inline 元素
.md 文件往返 RichTextParagraph[] 模式往返

没有 WinUI 对应物:WinUI 在 Community Toolkit 里提供了一个 MarkdownTextBlock,但 API 表面和行为都不同。本控件以 Reactor 工厂方法为规范参考。

Image

Image(string source)
class ImageDemo : Component
{
    public override Element Render() => VStack(8,
        SubHeading("Image"),
        // 资源 Uri —— 打包资源用 ms-appx://,磁盘用 file://,远程用 https://。
        Image("ms-appx:///Assets/StoreLogo.png")
            .AutomationName("Reactor app logo")
            .Set(img => img.Stretch = Microsoft.UI.Xaml.Media.Stretch.UniformToFill)
            .Width(96).Height(96),
        TextBlock("Stretch.UniformToFill for cover art; " +
                  "ImageFailed to detect missing assets.").Opacity(0.6)
    ).Padding(24);
}

渲染一个打包的 96x96 资源的 Image 控件

Image 接受标准的 WinUI URI 方案 —— 随应用打包的资源用 ms-appx:///,应用数据文件用 ms-appdata:///,任意磁盘路径用 file:///,远程来源用 http(s)://。工厂方法把源的解码交给 WinUI BitmapImage —— 同样的缓存、同样的 DPI 感知。

流畅方法 效果
.Width(double) / .Height(double) 布局尺寸。两者都不设时,图像取自然像素尺寸。
.NineGrid(Thickness) 拉伸中间、保留边框(chrome 式背景)。
.Set(img => img.Stretch = ...) 拉伸模式:None / Uniform(默认) / UniformToFill / Fill
.ImageOpened(Action) 解码成功。
.ImageFailed(Action<string>) 解码失败 —— 触发它来换上兜底图。

不要: 在紧凑的列表项渲染里把 Source 设成全尺寸的 http:// URL,从而在 UI 线程上解码大型远程图像。WinUI 解码器确实会把工作甩到 UI 线程之外,但网络获取依然会拦着可视化树,在字节到齐之前画不出那一行。虚拟化列表里要懒加载媒体,请走一个 UseResource,由它去取一份降采样的数据,等 Pending 解决后再渲染 Image

WinUI 设计页:Images and image brushes

MediaPlayerElement

MediaPlayerElement(string? source = null)
class MediaPlayerDemo : Component
{
    public override Element Render() => VStack(8,
        SubHeading("MediaPlayerElement"),
        MediaPlayerElement(
            "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4")
            .Width(420).Height(240)
            .Set(m =>
            {
                m.AreTransportControlsEnabled = true;
                m.AutoPlay = false;
            }),
        TextBlock("Use AreTransportControlsEnabled for play/pause UI.")
            .Opacity(0.6)
    ).Padding(24);
}

带传输控件的 MediaPlayerElement

MediaPlayerElement 包装了 WinUI 的 MediaPlayerElement,后者本身又托管一个驱动音视频播放的 MediaPlayer。工厂方法在常见场景下接受字符串 URL;对于流来源,通过 .Set(m => m.MediaPlayer.Source = ...) 设置播放器。

开关 设置方式
AreTransportControlsEnabled .Set(m => m.AreTransportControlsEnabled = true)
AutoPlay .Set(m => m.AutoPlay = false)
IsFullWindow .Set(m => m.IsFullWindow = true)
MediaOpened / MediaEnded / MediaFailed 专用流畅方法重载

不要: 为了驱动播放而在每次渲染时挂载/卸载 MediaPlayerElement。这个 WinUI 控件持有一个底层 MediaPlayer,每次重挂载都会重新初始化硬件解码器;反复折腾会毁掉播放流畅度。让元素在树里以稳定位置只渲染一次,通过 Hook(用 UseRef 持有播放器)驱动 Source / 播放状态,而不是靠卸载重挂。

WinUI 设计页:Media player

WebView2

WebView2(Uri? source = null)
class WebViewDemo : Component
{
    public override Element Render()
    {
        var (loaded, setLoaded) = UseState(false);

        return VStack(8,
            SubHeading("WebView2"),
            WebView2(new Uri("about:blank"))
                .NavigationCompleted(_ => setLoaded(true))
                .Width(420).Height(240),
            TextBlock(loaded ? "Loaded." : "Loading…").Opacity(0.6)
        ).Padding(24);
    }
}

承载 about:blank 的 WebView2

WebView2 嵌入一个基于 Chromium 的浏览器。把它用于真正需要 HTML/CSS/JavaScript 的内容 —— 一个 markdown 页面不需要,一个内嵌的 Office 查看器需要。该控件为导航提供生命周期事件,并暴露一个 WebMessageReceived 通道用于 postMessage 互操作。

事件流畅方法 触发时机
.NavigationStarting(Action<Uri>) 导航开始 —— 可通过 .Set 取消。
.NavigationCompleted(Action<Uri>) 页面加载完成。
.WebMessageReceived(Action<string>) 页面调用了 window.chrome.webview.postMessage(...)
.CoreWebView2Initialized(Action) 底层 CoreWebView2 就绪 —— 通过 .Set(w => w.CoreWebView2....) 配置它。

不要:WebView2 放进 HStack 这类尺寸不确定的父容器却不给显式尺寸。WebView2 按其内容测量,而真实网页的内容就是视口 —— 没有边界时它会膨胀填满可用空间,并在页面重排时引发布局振荡。始终钉住 .Width.Height(或把控件放进固定尺寸的 Grid 单元格)。

WinUI 设计页:WebView2

MapControl

MapControl(string? mapServiceToken = null, double zoomLevel = 1)
class MapControlDemo : Component
{
    public override Element Render() => VStack(8,
        SubHeading("MapControl"),
        // 令牌留空 —— 请替换为真实的 Bing Maps 密钥以获取瓦片。
        // 没有令牌时控件只渲染网格背景。
        MapControl(mapServiceToken: null, zoomLevel: 4)
            .Width(420).Height(240)
    ).Padding(24);
}

缩放级别 4 的 MapControl

MapControl 包装 Microsoft.UI.Xaml.Controls.Maps.MapControl。服务令牌来自 Bing Maps 开发者门户 —— 没有它,网格背景会渲染但拿不到瓦片。要做完整定制(图钉、覆盖层、场景),通过 .Set(m => ...) 够到底层控件;Reactor 目前暴露的是工厂参数加直接直通。

不要:MapControl 发到一个需要容忍离线的应用里却不给兜底。瓦片获取失败不抛异常 —— 控件只是显示网格。通过 .Set 订阅底层的 MapControl.MapServiceErrorOccurred,离线时换上一张静态地图的 Image(或一个"地图不可用"面板)。

WinUI 设计页:Map control(Windows App SDK)。

参考

控件 工厂方法 Reactor 原创? WinUI 文档
TextBlock TextBlock(string) Text block
Title / Heading / SubHeading / Subtitle / BodyLarge / BodyStrong / Caption Heading(string) 变体预设
RichTextBlock RichTextBlock(string)RichTextBlock(RichTextParagraph[]) Rich text block
RichEditBox RichEditBox(text, onChanged) Rich edit box
Markdown Markdown(string) —— Microsoft.UI.Reactor.Advanced
Image Image(string) Images and image brushes
MediaPlayerElement MediaPlayerElement(string?) Media player
WebView2 WebView2(Uri?) WebView2
MapControl MapControl(token, zoom) Map control
InkCanvas 未包装 —— 参见差距分析 Ink controls

模式

用 Markdown 呈现长文

发布说明 / 变更日志模式:把一段 GFM 字符串解析成元素树,然后给容器做样式,而不是给内部元素。加内边距、把宽度约束到约 640 逻辑像素以保证可读性,剩下的层级交给 Markdown 渲染器。当渲染器位于一个频繁渲染的父级内时,用 UseMemo 以源字符串为键把调用包起来;解析器便宜但会分配内存,记忆化能让 GC 压力保持平稳:

class LongFormProseDemo : Component
{
    const string Source = "# Release notes\n\nShipped **today**.\n";

    public override Element Render()
    {
        // 解析器便宜但会分配;以源字符串为键做记忆化,这样一个频繁渲染的
        // 父级不会反复解析未变动的散文。
        var rendered = UseMemo(() => Markdown(Source), Source);
        return Border(rendered).Padding(20).Width(640);
    }
}

这与 recipes/master-detail 的内容面板、以及 chat 示例 的消息气泡是同一个形状。

用 RichTextBlock 做行内数据展示

当某个列表行需要 Name (status) — 3 hours ago(状态加粗、时间戳变暗)时,用带类型的数据为每行构建一个 RichTextBlock。把段落构造放在行 Component 里内联完成,这样它只在该行数据变化时才重跑;中等行数下 RichTextBlock 不是瓶颈,但当 LazyVStack 以 60fps 滚动上千行时,内联元素的分配会显现在剖析结果里。

常见错误

什么都用 TextBlock

// 不要这样:
VStack(8,
    TextBlock("Settings").FontSize(28).Bold(),
    TextBlock("Display").FontSize(20).Bold(),
    TextBlock("Adjust resolution and orientation.")
)
class TextVariantsDemo : Component
{
    public override Element Render() => VStack(8,
        Title("Title — the largest variant"),
        Heading("Heading — page or section title"),
        SubHeading("SubHeading — region header"),
        Subtitle("Subtitle — supporting line under a heading"),
        BodyLarge("BodyLarge — lead paragraph."),
        TextBlock("Body text. The default size and weight for prose."),
        BodyStrong("BodyStrong — emphasis within body copy."),
        Caption("Caption — secondary metadata, dates, labels.")
    ).Padding(24);
}

"不要"的写法交付了视觉层级却没有语义层级 —— 无障碍扫描器报告没有任何标题,屏幕阅读器用户拿不到文档大纲。正确的形式是用 Heading / SubHeading / TextBlock 工厂方法,好让 accessibility.md 的地标检测生效。

在紧凑滚动循环里渲染 Markdown 而不记忆化

// 不要这样:
LazyVStack<Message>(messages, m => m.Id, (m, i) =>
    Markdown(m.Body))  // 每次滚动回收都重新解析
class MarkdownRowsDemo : Component
{
    public override Element Render()
    {
        var messages = new List<Message>
        {
            new("m1", "**First** message."),
            new("m2", "Second message with `code`."),
        };

        // Memo(key, factory) —— 跨回收的行缓存。一次滚动回收会
        // 再次请求该键并拿回同一个元素实例,于是解析器对未变动的行
        // 再也不会运行第二次。
        //
        // 键要同时包含行的身份 *和* 正文,不能只有正文:这是逐行缓存,
        // 否则两条恰好文本相同的消息会撞在同一条缓存条目上。
        // 把 Id 也放进去,还能在单条消息正文被编辑时正确重新解析。
        return LazyVStack<Message>(messages, m => m.Id, (m, i) =>
            Memo((m.Id, m.Body), () => Markdown(m.Body))).Height(200);
    }
}

Markdown 解析很快但不是免费的 —— 在 60fps × 数百个可见行 × 每次状态变化之下,解析器的分配会出现在 GC 里。这里要用带键的 Memo(key, factory) 重载,而不是 Memo(ctx => …, deps)。两者同名,但编译器按参数形状选择,只有带键的那一版拥有跨回收的行缓存:一次滚动回收会再次请求该键并拿回同一个元素实例,于是解析器对未变动的行再也不会运行。Memo(ctx => …, deps) 只在父级重新渲染时跳过工作,而纯滚动并不发生这种事。

提示

挑能完成任务的最小控件。 按布局开销递增:TextBlock > RichTextBlock > Markdown > WebView2。只有当低一档表达不了那个结构时才升级。

凡是用户可能想复制的文本,都加 .IsTextSelectionEnabled() 错误消息、ID、路径、日志行、命令输出 —— 能拖拽选中再 Ctrl+C 都会有用得多。代价只是每个 TextBlock 一个修饰符。

给媒体控件钉住尺寸。 ImageMediaPlayerElementWebView2MapControl 默认都是"填满可用空间"或"取内容自然尺寸",这与 VStack/HStack 的自动布局配合得很糟。设置 .Width.Height(或把它们放进固定尺寸的 Grid 单元格)。

Markdown 是渲染 LLM 输出的正确选择。 把 AI 生成的文本当作一个 markdown 流 —— 解析它、渲染它、给容器套主题。渲染器能与目录其余部分组合,并且保持无障碍。

下一步

  • 表单与输入 —— 上一篇:可编辑输入控件与校验系统。
  • 状态与信息 —— 下一篇:Progress、InfoBar、徽标与非交互反馈。
  • 样式 —— 应用到每个文本元素上的主题令牌与字体流畅方法。
  • 无障碍 —— Heading / SubHeading 如何映射到文档地标。
  • Markdown 示例 —— chat 示例中的端到端 LLM 输出渲染。