跳转至

WPF 大型图标库:从硬编码到数据驱动与虚拟化

图标浏览器只有一百项时,直接创建卡片不会有明显问题;扩展到 1,403 项后,启动时间、滚动流畅度和维护成本都会暴露。Lemoo.UI 的图标系统重构,本质上分别解决了“数据怎么维护”和“界面怎么少创建对象”两个问题。

问题不只在 Path 绘制

每个图标卡片通常包含边框、Path、名称、命令和悬停状态。一次性为上千项创建完整视觉树,成本远高于单独绘制 1,403 个几何路径。

常见误区是继续微调模板,却忽略真正的数量级问题:用户当前只能看到几十项,剩下的控件根本不应该被实例化。

先让图标成为数据

早期实现中,图标名称、枚举和 Path 数据分散在代码里,新增资源需要同步修改多处。重构后由生成工具读取源数据,统一产出元数据:

public sealed record IconInfo(
    string Name,
    string Category,
    string PathData
);

浏览器、搜索、分类和 Icon 控件消费同一个数据源。这样做带来三个直接收益:

  1. 图标数量变化不再要求手工维护展示列表;
  2. 名称与 Path 的映射只有一个真值来源;
  3. 搜索和分类可以在纯数据层测试,不需要创建 WPF 控件。

虚拟化的前提是保留虚拟化链路

WPF 列表虚拟化通常依赖 VirtualizingStackPanel。但仅设置一个属性并不保证生效,以下做法可能破坏虚拟化:

  • 把 ItemsControl 放进外层 ScrollViewer;
  • 使用会测量全部子项的自定义面板;
  • 按分组展示但没有开启分组虚拟化;
  • 需要像素滚动却使用不兼容的布局组合;
  • 在 ItemTemplate 中执行昂贵的同步转换。

基本配置思路是:

<ListBox
    ItemsSource="{Binding FilteredIcons}"
    ScrollViewer.CanContentScroll="True"
    VirtualizingPanel.IsVirtualizing="True"
    VirtualizingPanel.VirtualizationMode="Recycling">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

Recycling 让滚出视口的容器被后续项目复用,减少持续创建与回收对象带来的 GC 压力。

如果产品一定需要二维瀑布或 Wrap 布局,就需要实现真正支持 IItemContainerGenerator 的虚拟化面板,或者改为按行虚拟化:先把数据切成固定列数的行,外层只虚拟化行容器。

搜索不要重建全部视图

每次输入一个字符就重新创建 1,403 个 ViewModel 和几何对象,会抵消虚拟化收益。更合适的方式是:

  • 元数据只加载一次;
  • 使用 ICollectionView 做过滤;
  • 对输入增加短暂防抖;
  • 名称与分类预先规范化,避免每次比较重复分配字符串;
  • Path Geometry 按需解析并缓存,能够冻结时调用 Freeze()

Freezable 冻结后不再监听变化,可减少线程检查和属性系统开销。前提是资源确实不会在运行时修改。

主题资源也要避免逐项重算

图标颜色应引用语义资源,例如 IconForegroundBrush,而不是在每个卡片 ViewModel 中保存一份颜色。主题切换时替换资源字典,由 WPF 资源系统通知当前可见控件更新;未实例化的项目自然没有更新成本。

如果所有图标都需要跟随主题,使用 DynamicResource 是合理的;不会变化的布局、尺寸和 Geometry 则尽量使用静态资源,避免不必要的动态查找。

如何确认虚拟化真的生效

“滚动感觉还行”不是验证。至少应该观察:

  1. 加载 1,403 项后,实际 ListBoxItem 数量是否接近可见项而不是总数;
  2. 快速滚动时容器是否复用;
  3. 搜索前后 UI 线程耗时和分配量;
  4. 主题切换时是否只有可见项更新;
  5. 多次搜索和清空后是否出现持续内存增长。

可以通过 Visual Studio Diagnostic Tools、WPF Visualizer 或简单的容器计数日志确认。性能优化应有可重复的场景和测量,而不是只看一次启动。

架构层面的收获

这次重构最终得到两条可以复用的经验:

  • 数据规模问题先从对象数量入手。 让不可见项不进入视觉树,通常比继续优化单个控件更有效。
  • 生成、存储和展示应解耦。 数据驱动不仅减少维护,也让搜索、分类、生成器和 UI 可以分别测试和演进。

从 406 个图标扩展到 1,403 个图标并不是简单增加资源;它迫使系统从“手写 Demo”转向可持续维护的组件。