WinUI NavigationView 渲染机制深度解析:从 ControlTemplate 部件到双显示模式的 UI 实现
2026/9/16 15:30:24 网站建设 项目流程

WinUI NavigationView 渲染机制深度解析:从 ControlTemplate 部件到双显示模式的 UI 实现

【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml

本指南以微软 WinUI 3(microsoft-ui-xaml)仓库中 NavigationView 渲染文档 为主体,结合 NavigationView.xaml 控制模板、NavigationView.cpp 代码后台与 NavigationViewItemsFactory.cpp 实现,完整剖析 NavigationView 的 UI 构成:ItemTemplate 如何与 NavigationViewItem 协作、左右两种 DisplayMode 各自使用哪些模板部件、代码后台如何引用这些部件完成布局与交互。读完你将能够理解 NavigationView 模板的每一块拼图,并具备自定义/复刻其模板结构的能力。

NavigationView 渲染概述

NavigationView 的 UI 渲染与DisplayMode(显示模式)强相关:左侧导航(Left)与顶部导航(Top)两种模式下,控件模板(ControlTemplate)中处于激活状态的可视化部件完全不同。因此在 rendering.md 中,作者将模板部件按"两种模式共用 / 仅 Left 使用 / 仅 Top 使用"三组分别说明。

实际的模板定义位于 NavigationView.xaml 的ControlTemplate TargetType="controls:NavigationView"中,模板根部是一个x:Name="RootGrid"Grid,内部通过VisualStateManager.VisualStateGroups声明了DisplayModeGroupTogglePaneGroupPaneStateGroupPaneOverlayGroup等状态组,再由VisualState.Setters动态切换各个命名部件的可见性、边距与样式——这正是渲染差异的实现机制。

ItemTemplates 与 NavigationViewItems:容器化的两条路径

文档明确了 NavigationView 同时支持MenuItemsSourceItemTemplate,二者叠加时的行为规则如下:

  • 如果提供的MenuItems本身就是NavigationViewItem,且未设置ItemTemplate,则这些NavigationViewItem原样直接使用,不做任何包装;
  • 如果设置了ItemTemplate,则NavigationViewItem根据模板内容行动:
    • 模板返回的是NavigationViewItem→ 直接使用该模板结果,不加包装
    • 模板返回的不是NavigationViewItem(例如一个Button)→ 返回的元素会被包装进一个NavigationViewItem内。

这套规则在源码中有直接对应的实现。NavigationView 内部为所有项容器统一使用NavigationViewItemsFactory(一个ElementFactory),其GetElementCore方法(见 NavigationViewItemsFactory.cpp)按如下逻辑解析数据项:

  1. 设置项(Settings)不做模板化,直接返回;
  2. 存在 ItemTemplateWrapper 时,用模板生成元素;
  3. 如果解析出的元素已经派生自NavigationViewItemBase直接返回——对应"模板返回 NavigationViewItem 时不加包装";
  4. 否则从对象池取出或新建一个NavigationViewItem,把模板结果作为其 Content 装入——对应"非 NavigationViewItem 元素被包装"。

此外,NavigationViewItemsFactory内部维护了一个navigationViewItemPool对象池,被包装的 NavigationViewItem 在回收后可以复用,这与容器化(wrapping)流程配合,减少了频繁创建容器的开销。另外文档还提示:设置(Settings)项会被渲染为 FooterMenu 项列表中的一个元素,对应代码中NavigationViewItemsFactory::SettingsItem的注入逻辑。

ControlTemplate 共用部件:两种模式都需要的模板结构

文档列出的两个 DisplayMode 都需要的模板部件及其在代码后台(code behind)中的用途如下(完整继承自原文档):

模板部件名代码后台中的用途
RootGrid模板根,用于键盘导航(XYFocusKeyboardNavigation)
PaneToggleButtonGrid未使用
TogglePaneTopPadding被引用,用于调整内边距(标题栏 TopPadding)
ButtonHolderGrid未使用
NavigationViewBackButton允许触发BackRequested事件
NavigationViewBackButtonToolTip返回按钮 ToolTip 的本地化
NavigationViewCloseButton关闭按钮功能
NavigationViewCloseButtonToolTip关闭按钮 ToolTip 的本地化
TogglePaneButton打开/关闭 Pane 的功能
PaneTitleTextBlockPane 标题的绑定与渲染
PaneTitleHolder承载 Pane 标题 presenter
PaneTitlePresenter渲染 Pane 标题

这些部件的实际引用逻辑位于 NavigationView.cpp 的OnApplyTemplate:代码通过GetTemplateChildT<T>(名称)逐一抓取模板子元素并挂接事件。例如:

  • TogglePaneButton抓取后挂接Click事件(OnPaneToggleButtonClick),并追加Win+Backspace键盘加速键(见 NavigationView.cpp);
  • NavigationViewBackButton抓取后挂接点击事件以触发BackRequested,并设置自动化名称(NavigationView.cpp);
  • NavigationViewCloseButtonTogglePaneButton共享同一个点击处理函数(NavigationView.cpp);
  • NavigationViewBackButtonToolTip/NavigationViewCloseButtonToolTip在模板应用时通过ResourceAccessor::GetLocalizedStringResource写入本地化文本;
  • RootGrid上开启XYFocusKeyboardNavigation(Enabled),而内容区域的ContentGrid被显式设为Disabled——这样游戏手柄/键盘方向键导航只作用于 Pane 与汉堡按钮区域,不进入内容区(NavigationView.cpp)。

模式切换时的部件"搬运"

文档特别指出:当在 Top 与 Left 两种显示模式之间切换时,NavigationView 会"移动"某些元素(如 PaneHeader 与 AutoSuggestBox),原因是 UI 元素只能被加入 VisualTree 一次,而这些元素必须出现在当前可见的区域中。

从模板结构可以印证这一点:模板中同时声明了两组"宿位"——左侧模式下的PaneHeaderContentBorderPaneAutoSuggestBoxPresenter与顶部模式下的PaneHeaderOnTopPaneTopPaneAutoSuggestBoxPresenter是各自独立的ContentControl。代码后台在OnApplyTemplate中同时抓取这两组宿位(NavigationView.cpp),并在切换显示模式时通过UpdatePaneTitleFrameworkElementParents等更新方法决定将同一个逻辑内容(如 AutoSuggestBox)挂到哪一组 presenter 上,实现单实例元素的"搬家"。

DisplayMode Left:基于 SplitView 的左侧导航渲染

左侧模式使用的模板部件

文档列出的 Left 模式部件及用途如下(完整继承):

模板部件名代码后台中的用途
RootSplitView渲染内容区与左侧 Pane
PaneContentGrid左侧 Pane 的布局,动画也需要它
ItemsContainerRow菜单项/页脚项的高度分配
ContentPaneTopPadding高度调整
PaneHeaderContentBorderRow尺寸调整时被引用
PaneHeaderCloseButtonColumn用于 CompactPaneLength 下的关闭按钮列宽调整
PaneHeaderToggleButtonColumn用于 CompactPaneLength 下的切换按钮列宽调整
PaneHeaderContentBorderPane 头的手动尺寸控制
AutoSuggestArea未被引用
PaneAutoSuggestBoxPresenter用于检查 AutoSuggest 区域是否有内容
PaneAutoSuggestButton按钮被调用时打开 Pane(搜索按钮)
PaneCustomContentBorderPane 头渲染(自定义内容)
ItemsContainerGrid菜单项与页脚项视图的尺寸计算
MenuItemsScrollViewer限制菜单项宿主的最大高度
MenuItemsHost渲染 NavigationViewItems
VisualItemsSeparator需要时动态显示/隐藏分隔线
FooterContentBorder渲染 Pane 页脚(PaneFooter)
FooterMenuItemsHost渲染页脚菜单项
ContentGrid阴影处理与焦点行为
ContentTopPadding未被引用
ContentLeftPadding内容区左内边距
HeaderContent未被引用

在 NavigationView.xaml 中可以看到这些部件的真实形态:RootSplitView使用DisplayMode="Inline"IsPaneOpenNavigationView.IsPaneOpen双向绑定;Pane 内PaneContentGrid以 7 行 RowDefinition 组织(顶栏 → 关闭/返回按钮行 → PaneHeader → AutoSuggestArea → 自定义内容 → 菜单/页脚区);MenuItemsHostFooterMenuItemsHost都是ItemsRepeater,分别以StackLayout纵向排布,并被包在ItemsRepeaterScrollHost+ScrollViewer中。VisualItemsSeparator是一个默认CollapsedNavigationViewItemSeparator,由PaneSeparatorStates状态组控制显隐。

SplitView 驱动的显隐逻辑

文档指出:Left 模式下由 SplitView 根据显示模式与 NavigationView 宽度来决定 Pane 的显示/隐藏;当PaneDisplayModeAuto时,Pane 依据宽度自动隐藏。

这一自适应逻辑在 NavigationView.cpp 的UpdateAdaptiveLayout中实现(由OnSizeChanged触发,见 NavigationView.cpp):

  • PaneDisplayMode == Auto:宽度>= ExpandedModeThresholdWidth时切换为Expanded;宽度> 0< CompactModeThresholdWidth时切换为Minimal;介于两者之间保持Compact
  • PaneDisplayMode == Left:恒定Expanded
  • PaneDisplayMode == LeftCompact:恒定Compact
  • PaneDisplayMode == LeftMinimal:恒定Minimal

切换过程中代码还会协同调用OpenPane/ClosePane(如进入Expanded且 Pane 可见时自动打开,进入Minimal时自动关闭),并同步更新NavigationViewTemplateSettings.OpenPaneLength供模板绑定使用(UpdateOpenPaneLength会把OpenPaneLength限制在不超过当前宽度范围内,见 NavigationView.cpp)。

菜单区与页脚区的空间分配算法

ItemsContainerRowMenuItemsScrollViewerFooterItemsScrollViewer的真正用途体现在 NavigationView.cpp 的UpdatePaneLayout中:代码将 Pane 内可用高度在"菜单项"与"页脚组(FooterItems + PaneFooter)"之间按优先级划分——页脚优先(因为通常包含设置、个人资料等重要入口)。其分配策略:

  • 没有页脚项且不显示设置项:收起分隔线,菜单项占用全部高度;
  • 没有菜单项:限制页脚滚动区域高度为全部可用高度,收起分隔线;
  • 空间足够容纳两者:各自按所需高度分配,收起分隔线;
  • 页脚超过一半高度:限制页脚高度,显示分隔线;
  • 菜单超过一半高度:限制菜单高度,显示分隔线;
  • 双方都超过一半:对半平分,显示分隔线。

这就是VisualItemsSeparator"动态显示/隐藏"的完整决策逻辑——它并不是装饰,而是"空间不足、双方开始争抢高度"时出现的信号。当m_footerItemsSource.Count() == 0 && !IsSettingsVisible()等条件变化、或OnSizeChangedOnItemsContainerSizeChanged、页脚集合变化时,UpdatePaneLayout都会被再次调用以重新计算。

DisplayMode Top:基于 ItemsRepeater 的顶部导航渲染

顶部模式使用的模板部件

文档列出的 Top 模式部件及用途如下(完整继承):

模板部件名代码后台中的用途
TopNavArea未被引用
TopNavLeftPadding未被引用
TopNavGrid用于低版本(down level)支持
BackButtonPlaceholderOnTopNav未被引用
PaneHeaderOnTopPane渲染头部(PaneHeader)
PaneTitleOnTopPane渲染 Pane 标题
TopNavMenuItemsHost在顶部模式下渲染 NavigationViewItems
TopNavOverflowButton空间不足时打开溢出菜单
TopNavMenuItemsOverflowHost渲染溢出 Flyout 中的项
PaneCustomContentOnTopPane渲染自定义 Pane 内容
TopPaneAutoSuggestArea为搜索框预留空间
TopPaneAutoSuggestBoxPresenter渲染 AutoSuggestBox
TopFooterMenuItemsHost渲染页脚菜单项
PaneFooterOnTopPane显示 Pane 页脚
TopNavContentOverlayAreaGrid用于内容叠加(ContentOverlay)

模板中,TopNavArea是一个StackPanel,内含TopNavGrid(NavigationView.xaml)。TopNavGrid是一个 9 列 Grid:返回按钮占位列、TopNavLeftPaddingPaneHeaderOnTopPanePaneTitleOnTopPane、横向ItemsRepeaterTopNavMenuItemsHostTopNavOverflowButtonPaneCustomContentOnTopPaneTopPaneAutoSuggestArea(内含TopPaneAutoSuggestBoxPresenter)、PaneFooterOnTopPane以及TopFooterMenuItemsHost

溢出(Overflow)机制的模板结构

TopNavOverflowButton在模板中是一个带Flyout的按钮,TopNavMenuItemsOverflowHost位于该 Flyout 内部(NavigationView.xaml)。从代码后台(NavigationView.cpp)可以看到:

  • 顶部主菜单与溢出菜单各自持有独立的ItemsRepeaterm_topNavRepeaterm_topNavRepeaterOverflowView),数据源分别来自TopNavigationViewDataProvider::GetPrimaryItems()GetOverflowItems()
  • 溢出按钮在模板应用时写入本地化文本与 ToolTip,并监听 Flyout 的Closing事件;
  • 当溢出集合变为空时,溢出按钮会被折叠(OnOverflowItemsSourceCollectionChanged,见 NavigationView.cpp)。

当用户在溢出菜单中选中某个叶子项时,NavigationView 会把该项从溢出区移入主菜单区(SelectandMoveOverflowItem,见 NavigationView.cpp),这正是"溢出项被选中后提升为主项"的行为来源。

顶部模式的层级结构(Hierarchical NavigationView)

与左栏不同,顶部导航的层级子项并非展开在面板内,而是通过ChildrenFlyout弹出。相关结构在 NavigationViewItem 渲染文档 中有配套说明:NavigationViewItem的模板由NVIRootGrid(根)、NavigationViewItemPresenter(负责项的实际渲染)、NavigationViewItemMenuItemsHost(渲染子项)与ChildrenFlyout(顶部模式下子项弹出的 Flyout)组成。在 NavigationView.xaml 的 NavigationViewItem 模板中可以看到:OnTopNavigationPrimary状态会把 presenter 切换为顶部专用样式、并把ChildrenFlyoutPlacement改为BottomEdgeAlignedLeftOnTopNavigationOverflow状态则使用溢出专用样式。NavigationViewItemPresenter在 NavigationViewItemPresenter.idl 中定义,其具体样式定义在 NavigationView_themeresources.xaml 中。

从仓库历史看,NavigationView 内部容器已从早期 ListView 全面迁移到 ItemsRepeater + SelectionModel(详见 NavigationView_Overview.md),顶部导航的所有项宿主(主菜单、溢出菜单、左右页脚菜单)均为ItemsRepeater,这为层级展开、溢出提升等复杂交互提供了基础。

总结:如何阅读与自定义 NavigationView 模板

把本文档与模板、代码后台对照阅读的路径可以归纳为:

  1. 看模板:NavigationView.xaml 中的ControlTemplate声明了全部命名部件,按TopNavArea(顶部区)与RootSplitView(左侧区)两大块组织;
  2. 对状态组:模板中的VisualStateGroupDisplayModeGroupPaneStateGroupPaneSeparatorStates等)决定了各部件在不同模式/状态下的可见性与样式;
  3. 追代码:NavigationView.cpp 的OnApplyTemplate(L392-L709)列出了所有被实际引用的部件名常量(L33-L102),凡是在这份名单里的部件才会被代码后台驱动;文档中标注"未被引用/Not referenced"的部件,则仅作为模板结构占位存在;
  4. 验行为UpdateAdaptiveLayout(自适应宽度)、UpdatePaneLayout(菜单/页脚空间分配)、NavigationViewItemsFactory::GetElementCore(项容器化)分别对应文档中"按宽度隐藏 Pane"、"分隔线动态显隐"、"模板返回非 NavigationViewItem 时自动包装"三条核心结论。

若你要为 NavigationView 编写自定义模板,最稳妥的做法是以本模板为基线,保留所有被代码后台引用的x:Name(尤其是上表与OnApplyTemplate常量表中出现的名称),再针对性地修改样式与视觉状态;删除或重命名被引用的部件会导致代码后台无法抓取到对应元素,进而使返回按钮、Pane 开关、搜索按钮等交互失效。进一步的控件实现细节可参考 NavigationView.cpp 全文、NavigationView.idl 的 API 定义以及 NavigationView 目录 下的交互测试(如 NavigationView_InteractionTests/PaneBehaviorTests.cs 与 TopModeTests.cs)中针对各模式行为的验证用例。

【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询