WinUI 3 TableView 控件实战:基于 TableViewSampleApp 掌握 Tabular 数据表格的构建、主题资源与数据整形
【免费下载链接】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
导读
本文以 Samples/TableViewSampleApp/README.md 为主体,结合 controls/dev/TableView/TableView.idl、controls/dev/TableView/TableViewSource.idl 及示例源码,系统讲解 microsoft-ui-xaml 仓库中 WinUI 3 的Microsoft.UI.Xaml.Controls.Tabular.TableView控件。你将掌握:TableView 与 Tabular.dll 的发布形态与本地打包构建流程、在自己应用中正确合并主题资源字典的必备操作、列/宽度/外观/编辑/选择等核心 API,以及基于TableViewSource的过滤、排序、分组数据整形方案,并能定位两类最常见的运行时失败。
TableView 与 Tabular.dll:独立控件集的发布形态
TableView是一个 WinUI 3 桌面端数据表格控件,位于独立的Microsoft.UI.Xaml.Controls.Tabular.dll中,不随主框架 DLL 一起编译(README.md)。但这并不影响它在普通应用中被当作常规 NuGet 包消费:
- 类型信息会进入公开的 winmd(
MergedWinMD合并产物); <ActivatableClass>激活注册会随合并输入一并生成;- 控件的主题资源(theme XBF、
.pri)随包发布并可被解析。
因此 TableViewSampleApp 是一个普通消费者:只引用Microsoft.WindowsAppSDK.WinUI包,不做任何特殊接线,与仓库中 Samples/ChartApp 系列示例采用同一模式。从源码结构看,这正是本仓库对“独立构建的控件集”既定的示例组织方式。
示例应用总览:左侧调参、右侧实时出表
TableViewSampleApp 是一个小型的 WinUI 3 桌面应用,通过 MainWindow.xaml 中的NavigationView承载 9 个页面:
- Playground:核心演练页,左栏是每个 TableView API 对应的控件面板,右栏是实时渲染的表格与“Live data source”数据源回显(PlaygroundPage.xaml);
- All Auto columns / All Star columns / All Pixel columns / Mixed columns:分别演示单一宽度模型与混合列宽;
- Interactive cells:展示可交互、可展开的单元格模板;
- Selection:演示单选、
Select/Deselect/DeselectAll与“选中跟随数据项”的行为; - Cell tooltips:演示单元格 ToolTip;
- Filter / sort / group:演示
TableViewSource的数据整形能力。
行数据由 Data.cs 构造:约 150 行(Make(n = 150))使表格主体可以虚拟化滚动;姓名分为 Short/Medium/Wide 三档宽度,且宽姓名被“隔离”地安插在每 50 行的固定位置,从而让 Auto 列的起始宽度很窄、滚动到宽姓名时才明显变宽——这是特意设计的 Auto 列“增长”演示。Item实现了INotifyPropertyChanged,文本属性可写,配合双向绑定即可验证“编辑单元格 → 数据源更新 → 显示单元格刷新”的完整链路。
环境准备
- Visual Studio 2022,需安装Desktop development with C++与.NET Desktop两个工作负载;
- 在仓库根目录运行过一次完整初始化:
.\init.cmd构建与打包:先本地打包 Tabular 组件
为什么不能直接构建
TableView尚未进入任何已发布的Microsoft.WindowsAppSDK.WinUI包,因此单独构建本示例会从 feed 解析到旧版包并失败:
CS0234: The type or namespace name 'Tabular' does not exist包本身是真实存在的,只是其公开 winmd 早于 Tabular。支持的标准流程是先本地打包组件,再以匹配的版本号构建:
.\Build.cmd product .\Build.cmd samplesBuild.cmd samples内部会依次执行pack.component.cmd,并以匹配的/p:WinUIVersion构建全部示例(见 AGENTS.md)。
单应用快速迭代
只迭代这一个应用时,打包一次后直接构建:
.\pack.component.cmd /version 3.0.0-mylocal .\initrun.ps1 msb /q /restore Samples\TableViewSampleApp\TableViewSampleApp.csproj /p:Platform=x64 /p:WinUIVersion=3.0.0-mylocal注意:/p:WinUIVersion必须与pack.component.cmd /version给出的版本号一致,构建才能解析到本地刚打包的组件。
NuGet 版本缓存陷阱
每次改动控件后都要重新执行pack.component.cmd。NuGet 按版本号缓存包(缓存目录为packages\microsoft.windowsappsdk.winui\),因此:
- 复用同一版本号会被静默忽略——重新打包不会覆盖缓存;
- 要么在重新打包前删除
packages\microsoft.windowsappsdk.winui\目录,要么(更推荐)每次使用全新的版本字符串。
运行
BuildOutput\obj\amd64chk\Samples\TableViewSampleApp\TableViewSampleApp.exe构建产物说明:为什么应用的.pri很大
控件 DLL、其.pri与主题 XBF 均来自 NuGet 包。消费者应用构建时会展开所有被引用的.pri并重新索引进应用自己的TableViewSampleApp.pri——该文件之所以达数 MB,是因为它同时包含 MUXC、Tabular 与本示例三方的资源(AGENTS.md)。这是正常现象,不是配置错误。
在自己的应用中使用 TableView
步骤一:引用包并合并 TabularControlsResources
参考Microsoft.WindowsAppSDK.WinUI后,在App.xaml中同时合并XamlControlsResources与TabularControlsResources:
<Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> <!-- Tabular ships its own theme resources, exactly as MUXC ships XamlControlsResources. TableView's column-header style resolves SortIndicatorForeground from this dictionary, so activation fails without it. --> <TabularControlsResources xmlns="using:Microsoft.UI.Xaml.Controls.Tabular" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources>(完整文件见 App.xaml。)
TabularControlsResources是 Tabular 自己的主题资源字典,相当于 MUXC 的XamlControlsResources,必选而非可选。根因在源码层:TableView的列头样式会解析SortIndicatorForeground,而该资源定义在 Tabular 的主题资源中——因为SortIndicator控件随 Tabular.dll 发布而非 MUXC(controls/Tabular.ProjectImports.targets 是SortIndicator.vcxitems的唯一导入方,见 AGENTS.md)。
漏合并的症状:首次布局期间抛出XamlParseException 0x802B000A,在应用启动数秒后表现为0xC000027Bstowed exception。
步骤二:声明列
不声明列时,表格会渲染出行,但没有列头行、也没有单元格。最简声明方式:
<tabular:TableView x:Name="Table"> <tabular:TableViewTextColumn Header="Name" Binding="{Binding Name}" /> <tabular:TableViewTextColumn Header="Age" Binding="{Binding Age}" /> </tabular:TableView>TableViewTextColumn.Binding在 IDL 中是 CLR 属性(TableView.idl),因此 XAML 中的Binding值会经 setter 正确路由。示例同时使用SampleColumns.Text(...)工厂在代码中构建列,并提供了Auto()/Star(factor)/Pixels(px)三个GridLength便捷工厂(SampleColumns.cs)。
步骤三:处理 [MUX_PREVIEW] 警告
TableView标记为[MUX_PREVIEW]:
- C# 用法产生
CS8305(“for evaluation purposes only”),示例在 TableViewSampleApp.csproj 中通过NoWarn抑制; - XAML 用法每页产生一次
WMC1501,示例故意保留可见,提醒该 API 处于预览状态。
控件 API 全景:从 TableView.idl 看能力边界
列与宽度模型
TableViewColumn是未密封(unsealed)的DependencyObject,Width携带GridLength意图,ActualWidth是解析后的像素值(MinWidth/MaxWidth夹取,见 TableView.idl):
- Pixel:固定像素;
- Auto:等于已实现单元格中最宽者(在数据集内增长);
- Star:在扣除固定列后按比例瓜分视口宽度。
列级属性还包括:MinWidth(默认 20)、MaxWidth(默认无穷)、CanResize(默认 true)、FrozenEdge(None/Leading/Trailing,Leading 已实现,Trailing 预留)、Visibility(隐藏列)、ActualWidth(只读)。表格级开关CanUserResizeColumns是所有列的“总闸”,列自身可用CanResize单独退出。
外观与行为
HeadersVisibility:枚举为[flags],当前只定义None/Column(行头不在范围内,故无 Row/All,与 WPFDataGridHeadersVisibility的 Column 槽位对应);GridLinesVisibility:All/Horizontal/None/Vertical;Density:Compact/Standard/Comfortable,决定行高与内置单元格/列头内边距资源;RowBackground/AlternatingRowBackground:null 时保留主题行背景(行斑马纹);EmptyTemplate:ItemsSource为空时显示的空状态模板;HeaderTemplate/HeaderTemplateSelector/HeaderToolTip:列头定制(HeaderTemplateSelector优先级更高);- 列内单元格:
TableViewTextColumn(绑定文本)、TableViewTemplateColumn(CellTemplate自定义内容)、CellToolTipBinding(按行的 ToolTip 绑定)、CellEditingTemplate(编辑态模板)。
编辑
编辑是单单元格范围且默认关闭:表格级IsReadOnly默认 true;为某个单元格启用编辑需同时把表格与列设为非只读、并给列提供CellEditingTemplate或内置编辑器。IsEditing反映是否有编辑正在进行;CommitEdit()/CancelEdit()返回 false 表示被否决或校验失败、编辑保持打开。事件BeginningEdit(可Cancel)、CellEditEnding(携带EditAction.Commit/Cancel,可Cancel)。编辑由用户动作触发:双击或当前单元格按 F2;本版本没有编程式BeginEdit,也没有行级编辑事务。
选择
默认SelectionMode = Single(与 ItemsView、ListView、WPF DataGrid 一致);None为纯展示。SelectedItem/SelectedIndex是选择状态的只读投影,保持一致;通过Select(index)/Deselect(index)/IsSelected(index)/DeselectAll()驱动。示例的 SelectionPage.xaml.cs 专门演示了两类易错行为:选中跟随数据项(插入不触发SelectionChanged,仅SelectedIndex移位);删除被选中项是清除而非滑向邻居。
数据整形:TableViewSource 的过滤、排序与分组
TableViewSource是本控件的核心数据整形入口(完整契约见 TableViewSource.idl)。关键设计:Filter、GroupBy、Sort 是同一个源上的独立“阶段”,而非三份独立集合,因此三者可自由组合,且 reshape 保持行身份——选择与焦点按对象身份重新锚定,而不是按索引(ShapingPage.xaml.cs)。
创建源
_source = TableViewSource.From(_items); // _items 为 ObservableCollection<Item> Table.ItemsSource = _source;From接受与ItemsSourceView相同的集合接口(IVector<Object>/IObservableVector<Object>、IBindableVector、IIterable<Object>、IBindableIterable),返回时投影已填充完毕。注意TableViewSource密封(sealed):具体实现由 TableView 驱动,密封可防止子类绕过整形动词;将来解封是兼容的,反向则不兼容。
过滤
_source.Filter(new TableViewPredicate(item => Matches((Item)item, text, highOnly))); // 移除过滤时用 ClearFilter(),不要传“全通过”的谓词 _source.ClearFilter();- 谓词或 items 为 null 时抛
E_INVALIDARG(与 TableView 命令面“坏输入静默忽略”不同); - UI 线程亲和:底层集合必须在 UI 线程上发出变更通知,动词内部不持锁;
- 用
ClearFilter()移除过滤意味着“阶段被删除”,而不是对每个条目再跑一遍恒真谓词。
分组
// 值类型键(String/Int32/Int64/Guid/Boolean/enum)走内置组身份,无需 identity selector _source.GroupBy(new TableViewKeySelector(item => groupKey((Item)item))); // 移除分组 _source.ClearGroupBy();GroupByWithIdentity重载可选地接受TableViewIdentitySelector;引用类型分组键若无 selector,会在投影期快速失败。示例用Role、City及ScoreBand(score)(80-100 / 50-79 / 0-49 三档)演示按值类型键分组。分组的只读投影是TableViewGroupInfo(Key/ItemCount/Level/IsExpandable/IsExpanded/KeyText/ItemCountText,镜像ICollectionViewGroup),它是GroupHeaderTemplate的绑定源,原地更新并引发PropertyChanged,因此回收不会重算模板中的每个绑定。
排序:两种途径,一种“最后写入者胜出”规则
途径一:TableViewSource 层(数据层动词)
// 按属性路径排序(推荐,显示路径通常即排序路径) _source.Sort(nameof(Item.Name), SortDirection.Ascending); // 或按键选择器排序(计算/多字段/规范化键),该轴是“匿名”的,无法点亮列头的排序指示 _source.Sort(new TableViewKeySelector(item => item.Name), SortDirection.Ascending); _source.ClearSort();- 路径形式由与列
SortMemberPath相同的绑定求值器求值,支持点路径与索引器;绑定到列的 TableView 可以把路径归因到列并点亮其排序指示; - 多轴排序时第一个声明的轴为主排序,后续轴在前一轴内打破平局(对齐 WPF
SortDescriptions顺序); SortDirection.None会移除该键的轴而非播种新阶段。
途径二:TableView 控件层(命令面)
Table.SortByColumn(column, direction); // 先清掉其他排序状态 Table.ToggleSortDirection(column); // 按列的 SortCycle 循环 Table.ClearSort(); // 一次清空所有列 Table.CanUserSortColumns = true; // 列头点击排序总开关,默认 trueSorting(排序应用前,Cancel=true可交由应用自持排序)与Sorted(应用后,不可取消);- 控件排序与源排序互相替换而非叠加:任意时刻只有一个排序轴生效,最后写入者胜出。源码注释特别指出:控件的排序会“替换”之前的一切,而源的
Sort是“组合式”的,只有相同 token 的轴才被原位替换(ShapingPage.xaml.cs)。因此示例每次只声明单轴排序,先ClearSort()再排序; - 列级
SortCycle(AscendingDescending/AscendingDescendingNone/DescendingAscending/DescendingAscendingNone)决定列头重复点击时循环的方向序列——*None变体提供第三次点击回到未排序状态的步骤。SortCycle只约束列头点击,对编程式SortByColumn或源上的排序没有话语权。示例中 Score 列特意以DescendingAscendingNone开局(指标列最值得看的是最大值,见 TableView.idl); - 未显式设置时,
TableViewTextColumn的排序键回退到Binding.Path.Path,因此列头点击排序无需配置SortMemberPath。
组展开与组头模板
Table.ExpandAllGroups(); // 批量展开 Table.CollapseAllGroups(); // 批量折叠 Table.GroupHeaderTemplate = (DataTemplate)Resources["GroupHeader"]; // null 回退内置 KeyText/ItemCountText 头GroupHeaderTemplate为 null 时使用控件内置模板(KeyText/ItemCountText延迟计算,模板只绑Key/ItemCount时零开销)。示例还通过KeyboardAccelerator触发批量折叠/展开——因为按钮点击会先抢走焦点,破坏控件“批量命令后恢复焦点”的真实场景(ShapingPage.xaml.cs)。
示例中的模板实战:从 App.xaml 看高级单元格
App.xaml 集中展示了本控件支持的内容模板形态,可直接复用为自研表格的模板素材:
ScoreCell:ProgressBar 绑Score(TableViewTemplateColumn.CellTemplate);StarHeader:图标+文本的列头模板(Column.HeaderTemplate);EmptyState:空状态模板(EmptyTemplate);BioCell:TextWrapping="Wrap"的变高行文本(依赖 RebuildCells 重入修复后可变行高);JoinedCell:DatePicker双向编辑;NotesCell/NotesEditCell:显示 TextBlock + 编辑 TextBox 分离的“真编辑列”——编辑器只在编辑期换入,且编辑模板使用经典{Binding}而非{x:Bind},因为提交逻辑经GetBindingExpression只能看到{Binding};AutoEditCell:不固定宽度的多行 TextBox,输入更长的行会让 Auto列宽增长、增加行数会让 Auto行高增长,两个方向都可实时验证;ExpandNameCell等:每个单元格内嵌Expander(折叠/展开改变单元格期望尺寸,Auto 列随之增长、Star 列换行/裁切);GrowWidthCell:拖动 Slider 改变内部条形宽度,观察 Auto 列实时重排(Star/Pixel 列则裁切到固定列宽)。
常见失败速查表
| 症状 | 根因 | 处置 |
|---|---|---|
CS0234: ... 'Tabular' does not exist | 包早于 Tabular 的公开 winmd,未用本地打包版本 | 执行.\Build.cmd product+.\Build.cmd samples,或打包后用/p:WinUIVersion指定匹配版本 |
XamlParseException 0x802B000A(首帧布局),数秒后0xC000027Bstowed exception | App.xaml未合并TabularControlsResources,列头样式解析不到SortIndicatorForeground | 合并TabularControlsResources字典 |
| 表格出现但没有列头行、没有单元格 | 几乎总是未声明列,而非字典缺失 | 声明至少一个TableViewTextColumn/TableViewTemplateColumn |
重新pack.component.cmd后控件改动未生效 | NuGet 按版本缓存 | 换新版本号,或删除packages\microsoft.windowsappsdk.winui\ |
入口与维护备注
- 入口由 Program.cs 提供
Main,项目定义了DISABLE_XAML_GENERATED_MAIN,遵循 DisableXamlGeneratedMain 示例——这是常规 WinUI 模式而非变通方案; - 项目文件仅约 60 行(TableViewSampleApp.csproj),是普通包消费者。维护者若再次遇到需要“手工接线”的场景,说明产品可能回归,应按 AGENTS.md 中的顺序依次核查:① Tabular 类型在公开合并 winmd(
MergedWinMD)中未被剔除;②<ActivatableClass>注册随合并输入生成;③ 控件主题 XBF 随包发布、默认样式 URI 为无权威的ms-appx:///,与AppxPriInitialPath一致。
延伸阅读
- 构建与维护备注:Samples/TableViewSampleApp/AGENTS.md
- 控件完整 API 契约:controls/dev/TableView/TableView.idl
- 数据整形契约:controls/dev/TableView/TableViewSource.idl
- 同模式独立控件集示例:Samples/ChartApp
- 自定义入口模式:Samples/DisableXamlGeneratedMain
【免费下载链接】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),仅供参考