ClassViewer模块全面分析

ClassViewer 模块全面系统分析

源文档:https://gitee.com/chouchouxia/share-ue5

目录

  1. 写在前面
  2. 必须知道的问题
  3. 模块概述
  4. 模块整体架构解析
  5. 类级代码注释详解
  6. 功能使用示例编写
  7. 总结与最佳实践

文章目录

写在前面

不知道大家在日常使用 UE 编辑器时有没有注意过这样一个细节:当我们在蓝图的类设置里点击那个"父类"下拉框,或者在细节面板选择一个 TSubclassOf 属性时,弹出的小窗口里会列出一大堆 C++ 类和蓝图类,甚至还能按关键字搜索、按条件过滤。这个看似普通的"选类对话框",背后其实是一整套相当完善的工程实现——它就是 ClassViewer 模块。

还有一个更显而易见的入口——编辑器顶部菜单栏 Tools → Class Viewer,点开会看到一个独立的 NomadTab 面板,里面完整展示了整个项目里的所有类继承关系,以树形结构组织,大部分蓝图类甚至不需要提前加载就能出现在这里。这就是 ClassViewer 的完整形态。

在这里插入图片描述

实际上,ClassViewer 的代码量不小,关核心文件就有十来个,分布在 Public 和 Private 目录里。它不仅是编辑器里一个"看看类"的工具,更是一套供引擎内部各个子系统复用的类选择基础设施。PropertyEditor 用它来选属性类型、蓝图编辑器用它来选父类、各种自定义工具里也到处都是它的身影。

本文会从"为什么需要它"这个最朴素的问题出发,逐渐深入到它的架构设计、核心类的实现细节,最后再给出几个在实际项目中可能用得上的扩展示例。


必须知道的问题

1. ClassViewer 模块到底是做什么的?

核心定位

ClassViewer 是一个 Editor 模块,它的核心职责可以概括为三件事:

  1. 提供一个可嵌入的 Slate 控件(SClassViewer),用于以树形或列表形式浏览/选取项目中的类
  2. 构建并维护一套完整的类层次结构(FClassHierarchy),覆盖已加载的 C++ 类和尚未加载到内存中的蓝图类
  3. 提供一套可扩展的过滤系统(IClassViewerFilter),允许调用方自定义哪些类可见、哪些类不可见

它跟普通的"列出所有 UClass"有什么不同?

如果只需要列出所有已加载的类,用 TObjectIterator<UClass> 遍历一圈就够了。但 ClassViewer 面对的问题远比这个复杂:

  • 未加载的蓝图类怎么办? 项目里有成百上千个蓝图资产,不可能为列个表就把它们全部 Load 到内存里。ClassViewer 通过 AssetRegistry 在磁盘层面提取类信息,在根本不需要加载 BP 的前提下就能展示出来
  • 继承层级怎么展示? 单纯列个平铺列表没什么意义,必须按父子关系构建一棵树,而且这棵树里的节点有的是已加载的 C++ 类、有的是已加载的蓝图、有的是未加载的蓝图——三者的数据来源各不相同,但必须统一管理
  • 过滤条件怎么灵活配置? 不同使用场景有完全不同的过滤需求。PropertyEditor 需要"只显示跟这个属性类型兼容的类",蓝图编辑器需要"只显示可以作为父类的类",还有 Activate Only、Placeable Only 等选项——ClassViewer 的过滤系统必须支持这些层层叠加的条件

没有这个模块会怎样?

  • 编辑器里就没有那个方便的"选类对话框"了,选个父类得手动敲路径
  • 不在内存中的蓝图类无法出现在可选列表中(除非全部加载,那启动时间就完蛋了)
  • 每个需要选类的地方都要自己实现一套过滤逻辑,重复劳动且极易出现不一致的行为
  • 类继承关系的变化(编译蓝图、添加新类、重命名等)需要每个使用方自己监听和更新

ClassViewer 帮编辑器所有的子系统省掉了"展示和筛选类"这件重复又繁琐的事,让它们可以专注于各自的业务逻辑。


模块概述

基本信息

属性
模块名称ClassViewer
类型Editor
位置Engine/Source/Editor/ClassViewer/
依赖模块Core, CoreUObject, Engine, InputCore, Slate, SlateCore, EditorFramework, UnrealEd, PropertyEditor, ContentBrowserData, Settings
动态加载模块AssetRegistry, AssetTools, EditorWidgets, GameProjectGeneration

文件结构

ClassViewer/
├── ClassViewer.Build.cs                        # 模块构建规则
├── Public/
│   ├── ClassViewerModule.h                     # 模块接口 & SClassViewer 依赖的全部类型声明
│   ├── ClassViewerFilter.h                     # 过滤器接口 IClassViewerFilter / FClassViewerFilter / FClassViewerFilterFuncs / IUnloadedBlueprintData
│   ├── ClassViewerProjectSettings.h            # 项目级设置:InternalOnly 路径和类
│   └── SClassViewer.h                          # Slate Widget:核心 UI 控件
└── Private/
    ├── ClassViewerModule.cpp                   # 模块实现:注册 NomadTab、创建设置
    ├── ClassViewerNode.h / .cpp                # 树节点数据结构
    ├── ClassViewerFilter.cpp                   # 过滤器实现
    ├── SClassViewer.cpp                        # Slate Widget 实现 & FClassHierarchy & SClassItem
    ├── UnloadedBlueprintData.h / .cpp           # 未加载蓝图的数据代理
    └── ClassViewerProjectSettings.cpp          # 项目设置实现(空壳)

另外,ClassViewer 还依赖 UnrealEd 模块中的 UClassViewerSettings(位于 UnrealEd/Classes/Settings/ClassViewerSettings.h),用于存储"用户级别的显示偏好"(如是否显示 Internal 类、Developer Folder 过滤模式)。

模块职责边界

ClassViewer 做了这些事情:

  • 构建全局唯一的类层次结构(涵盖已加载和未加载的类)
  • 提供 SClassViewer Widget 作为核心 UI 入口
  • 提供 IClassViewerFilter 抽象接口让外部自定义过滤规则
  • 提供 FClassViewerInitializationOptions 作为配置参数
  • 注册编辑器 NomadTab “Class Viewer”
  • 注册项目设置 “Class Viewer”

ClassViewer 不做这些事情:

  • 不负责类的实际加载/卸载(那是 AssetRegistry / 引擎加载器的事)
  • 不负责类之间的关系验证(只展示继承链,不校验合法性)
  • 不提供"选择之后做什么"的业务逻辑(那是调用方的事,通过 FOnClassPicked 回调)

模块整体架构解析

ClassViewer 采用了分层架构,从上到下可以分为五个层次:

┌─────────────────────────────────────────────────────────────────┐
│                     外部调用者 (Consumers)                        │
│  PropertyEditor │ BlueprintEditor │ 自定义工具 │ NomadTab        │
└────────────────────────────┬────────────────────────────────────┘
                             │ FClassViewerModule::CreateClassViewer()
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                    SClassViewer (Slate UI 层)                     │
│  - 包含 TreeView 或 ListView                                       │
│  - 提供搜索框、过滤器菜单、ViewOptions 菜单                         │
│  - 持有 FClassViewerFilter                                        │
│  - 每帧 Tick:检查是否需要 Refresh,触发 Populate()                │
└────────────────────────────┬────────────────────────────────────┘
                             │ 使用
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                    FClassHierarchy (数据层)                        │
│  - 全局唯一实例(静态 TSharedPtr)                                 │
│  - 维护以 "Object" 为根的完整类树                                   │
│  - 同时管理已加载类节点和未加载蓝图节点                             │
│  - 监听 AssetRegistry / Blueprint 编译 / 模块变更等事件           │
└────────────────────────────┬────────────────────────────────────┘
                             │ 节点为
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                    FClassViewerNode (节点层)                       │
│  - 单个类在树中的表示                                              │
│  - 持有 TWeakObjectPtr<UClass> / TWeakObjectPtr<UBlueprint>     │
│  - 持有 IUnloadedBlueprintData(仅未加载蓝图节点)                 │
│  - 记录 bPassesFilter / bPassesFilterRegardlessTextFilter        │
│  - 维护父子关系 (ParentNode / ChildrenList)                       │
└────────────────────────────┬────────────────────────────────────┘
                             │ 过滤由
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                    FClassViewerFilter (过滤层)                     │
│  - 实现 IClassViewerFilter 接口                                   │
│  - 内嵌 FTextFilterExpressionEvaluator(文本搜索)                │
│  - 内嵌 FClassViewerFilterFuncs(辅助过滤函数)                    │
│  - 支持:Actors Only / Placeable Only / Blueprint Base Only /     │
│    Developer Folder 过滤 / Internal Classes 过滤 /               │
│    自定义 Filter 链 / 全局 Filter                                 │
└─────────────────────────────────────────────────────────────────┘

模块间依赖关系

FClassViewerModule (模块入口)
    │ CreateClassViewer() 创建
    ▼
SClassViewer (Slate Widget)
    │ 使用
    ├──▶ FClassHierarchy (全局类树)
    │        │ 节点为
    │        ▼
    │    FClassViewerNode
    │        │ 未加载时有
    │        ▼
    │    FUnloadedBlueprintData (实现 IUnloadedBlueprintData)
    │
    ├──▶ FClassViewerFilter (内建过滤器)
    │        │ 组合
    │        ├──▶ FTextFilterExpressionEvaluator
    │        ├──▶ FClassViewerFilterFuncs
    │        ├──▶ IAssetReferenceFilter
    │        └──▶ 外部 IClassViewerFilter 列表
    │
    └──▶ FClassViewerInitializationOptions (配置参数)

设置系统:
    UClassViewerSettings (用户级,位于 UnrealEd)
    UClassViewerProjectSettings (项目级,位于 ClassViewer)

数据流:类层次结构的构建

这是整个模块最核心的流程。FClassHierarchy::PopulateClassHierarchy() 负责将三个数据源合并成一棵完整的类树:

1. 从 AssetRegistry 获取所有蓝图类数据
   ↓
   遍历 UBlueprint 和 UBlueprintGeneratedClass 类型的 AssetData
   → 为每个蓝图创建/更新 FClassViewerNode
   → 填充 UnloadedBlueprintData(包含 ClassFlags、ImplementedInterfaces 等)
   → 记录到 ClassPathToNode Map

2. 从内存中的 UClass 获取所有已加载类
   ↓
   遍历 TObjectIterator<UClass>
   → 过滤掉 CLASS_Deprecated / CLASS_NewerVersionExists / CLASS_Hidden / REINST / 骨架类
   → 为每个类创建/更新 FClassViewerNode
   → 如果有对应的蓝图(ClassGeneratedBy),设置 Blueprint 指针

3. 建立父子关系
   ↓
   遍历 ClassPathToNode Map
   → 根据每个节点的 ParentClassPath 查找父节点
   → 调用 ParentNode->AddChild(Node)
   → 找不到父节点的类会被丢弃(日志 Warning)

4. 递归排序
   ↓
   SortChildren(ObjectClassRoot)
   → 对每层子节点按类名排序

5. 通知所有 Class Viewer 刷新
   ↓
   ClassViewer::Helpers::RefreshAll()

这个流程的关键设计在于:同一个类可能同时有 AssetRegistry 数据和 UClass 数据。 在这种情况下,CreateOrUpdateUnloadedClassNode 会先创建未加载版本的节点(填充 AssetRegistry 字段),随后 CreateNodesForLoadedClasses 再调用 SetClassFields 补充 UClass 独有的字段(如 Blueprint 指针、Class 指针)。两者互补而非覆盖,确保了信息的完整性。

数据流:过滤与展示

SClassViewer::Populate() 被调用时(可能是搜索文本改变、过滤条件切换等):

1. 保存当前选中项
   ↓
2. 根据 DisplayMode 选择构建树还是列表
   ↓
   树模式 (TreeView):
     GetClassTree(RootNode, ClassFilter, InInitOptions)
     → 从全局 ObjectClassRoot 复制一份(节点复制,子节点列表不复制)
     → 递归调用 AddChildren_Tree
     → 每个子节点先 IsNodeAllowed 判断是否通过过滤
     → 通过过滤的子节点才添加到父节点的 ChildrenList
     → 按类名排序

   列表模式 (ListView):
     GetClassList(NodeList, ClassFilter, InInitOptions)
     → 从全局 ObjectClassRoot 遍历所有子节点
     → 递归调用 AddChildren_List
     → 通过过滤的节点加入列表(扁平化)
     → 按类名或自定义排序谓词排序

3. 可选:添加 "None" 选项
   ↓
4. 请求 TreeView/ListView 刷新
   ↓
5. 恢复选中项 / 选中初始类 / 展开根节点

两类显示模式的设计差异

TreeView(树模式)ListView(列表模式)
默认使用场景ClassBrowsing(浏览模式)ClassPicker(选取模式)
展示形式按继承层级展开的树平铺的类名列表
过滤逻辑父节点通过过滤才展示,子孙节点递归检查每个节点独立判断是否通过过滤
展开/折叠支持,且会记忆展开状态不支持
键盘导航树形导航直接用上下箭头 + 回车选取
特殊情况列表模式下 Picker 会用 SListViewSelectorDropdownMenu 包裹,提供更好的键盘交互

类级代码注释详解

FClassViewerModule —— 模块入口

FClassViewerModule 是整个 ClassViewer 对外暴露的唯一入口点,它实现了 IModuleInterface。职责很清晰:

StartupModule():

  • 注册一个名为 “ClassViewerApp” 的 NomadTab(独立可停靠面板),点击 Tools → Class Viewer 时打开
  • 在该 Tab 中以 ClassBrowsing 模式 + TreeView 展示创建 SClassViewer
  • UClassViewerProjectSettings 注册到项目设置的 Editor 分类下

ShutdownModule():

  • 注销 NomadTab
  • 注销项目设置
  • 销毁全局的 ClassHierarchy 静态实例

提供给外部调用的核心 API:

CreateClassViewer()          → 创建 SClassViewer Widget 实例
CreateClassFilter()          → 创建 IClassViewerFilter(内建的 FClassViewerFilter)
CreateFilterFuncs()          → 创建 FClassViewerFilterFuncs
RegisterGlobalClassViewerFilter()  → 注册全局过滤器(影响所有 ClassViewer 实例)
GetGlobalClassViewerFilter()       → 获取全局过滤器

全局过滤器是一个很有意思的设计:它不依赖于个别 SClassViewer 实例,而是注册到模块级别。FClassViewerFilter::IsClassAllowed 的过滤链末尾会检查全局过滤器,这意味着某些项目级别的全局规则(比如"永远不显示实验性模块的类")只需注册一次就能作用于所有类选择器。


FClassViewerInitializationOptions —— 配置参数

这是使用 ClassViewer 时最需要关注的类之一。每创建一个 SClassViewer 实例,都需要提供这样一份初始化选项。它决定了这个实例的行为。

核心参数解析:

参数类型默认值含义
ModeEClassViewerModeClassPickerClassBrowsing:浏览模式,选择会同步到编辑器;ClassPicker:选取模式,选中后触发回调
DisplayModeEClassViewerDisplayModeDefaultViewTreeView:树形/ListView:列表/DefaultView:根据 Mode 自动选择
ClassFiltersTArray<TSharedRef<IClassViewerFilter>>自定义过滤器列表,会逐个调用 IsClassAllowed
bIsActorsOnlyboolfalse只显示 Actor 的子类
bIsPlaceableOnlyboolfalse只显示可放置的 Actor(强制 bIsActorsOnly = true
bIsBlueprintBaseOnlyboolfalse只显示可作为蓝图基类的类
bShowUnloadedBlueprintsbooltrue是否显示未加载的蓝图类
bShowNoneOptionboolfalse是否显示 “None” 选项(仅 Picker 模式有效)
bShowObjectRootClassboolfalse是否在树中显示 “Object” 根节点
bEnableClassDynamicLoadingbooltrue选中未加载的类时是否自动加载它
NameTypeToDisplayenumClassName显示名称策略:ClassName/DisplayName/Dynamic(都显示)
ViewerTitleStringFText标题文字
PropertyHandleTSharedPtr<IPropertyHandle>关联的属性句柄,用于获取引用资产和限制条件
bAllowViewOptionsbooltrue是否显示 View Options 菜单
bEditorClassesOnlyboolfalse是否只显示 Editor 模块的类
InitiallySelectedClassUClass*nullptr初始选中的类
ClassViewerSortPredicateTFunction列表模式下的自定义排序谓词
ExtraPickerCommonClassesTArray<UClass*>在 Picker 中额外标记为"常用类"

值得注意的几个点:

  • bIsPlaceableOnlybIsActorsOnly 之间有关联约束:Placeable Only 隐含 Actors Only
  • bEnableClassDynamicLoading 这个开关非常重要——如果关闭,未加载的蓝图类虽然会显示,但选中后不会自动加载,调用方拿到的 UClass* 就是 nullptr
  • ClassFilters 是一个数组而不是单个过滤器,这意味着可以叠加多层过滤规则,每一层各管各的

FClassHierarchy —— 全局类层次结构

FClassHierarchy 是整个模块的数据中枢。它是一个内部类(定义在 SClassViewer.cpp 的匿名命名空间之上),通过静态变量 ClassViewer::Helpers::ClassHierarchy 保持全局唯一实例。

构造与销毁

构造函数中做的事情就是监听所有可能导致类层次结构变化的外部事件

AssetRegistry::OnFilesLoaded    → 资产注册表加载完毕后重建
AssetRegistry::OnAssetAdded     → 有新蓝图资产加入时增量添加节点
AssetRegistry::OnAssetRemoved   → 有蓝图资产被删除时移除对应节点
FCoreUObjectDelegates::ReloadComplete  → Hot Reload 后重建
GEditor::OnBlueprintCompiled    → 蓝图编译后重建
GEditor::OnClassPackageLoadedOrUnloaded  → 包加载/卸载后重建
FModuleManager::OnModulesChanged  → 模块变更后重建

这意味着无论用户是编译蓝图、添加新 C++ 模块、删除资产还是执行 Hot Reload,ClassViewer 都能自动感知并更新其内部数据。

PopulateClassHierarchy() 核心逻辑

第一阶段:从 AssetRegistry 获取未加载蓝图数据

遍历所有 UBlueprint 类型的 AssetData
  → 获取 GeneratedClassPath(从 AssetRegistry 标签中读取)
  → 创建节点并填充 UnloadedBlueprintData
  → 存入 ClassPathToNode Map

遍历所有 UBlueprintGeneratedClass 类型的 AssetData
  → 同样创建节点,确保覆盖

这里有两轮遍历是为了覆盖不同情况:有些蓝图的数据以 UBlueprint 为 key 存储在 AssetRegistry 中,有些以 UBlueprintGeneratedClass 为 key。

第二阶段:从内存获取已加载类

遍历 TObjectIterator<UClass>
  → 过滤掉 Deprecated / NewerVersionExists / Hidden / Placeholder / Skeleton 类
  → 在 ClassPathToNode 中查找或创建节点
  → 调用 SetClassFields 补充 UClass 字段

第三阶段:建立父子关系

遍历 ClassPathToNode Map
  → 跳过 ObjectClassRoot 自己
  → 通过 ParentClassPath 查找父节点
  → 如果找不到父节点 → 输出 Warning 并跳过(该类不会出现在树中)
  → 调用 ParentNode->AddChild(Node)

增量更新:AddAsset / RemoveAsset

AddAsset 在 AssetRegistry 发现新蓝图资产时被调用。它会:

  1. 检查 AssetRegistry 是否还在加载中(如果是就跳过,因为等加载完会有全量重建)
  2. 检查该节点是否已经存在(防止重复添加)
  3. 创建未加载节点,查找并解析父节点
  4. 将新节点添加为父节点的子节点
  5. 通知所有 Viewer 刷新

RemoveAsset 在蓝图资产被删除时被调用。它会递归搜索整棵树,找到对应节点并删除,然后通知所有 Viewer 刷新。

AddAsset 时机与全量重建的关系

这里有一个值得留意的细节——AddAsset 在最开始会检查 AssetRegistry.IsLoadingAssets(),如果正在加载就 return。这样做的原因是:编辑器启动初期,AssetRegistry 会在很短时间内连续触发大量 OnAssetAdded 事件。 如果每个事件都去增量添加和刷新,开销巨大且毫无意义。所以增量更新只在"稳定期"(AssetRegistry 不在加载中)才生效,而启动阶段的数据最终会由 OnFilesLoaded 触发全量 PopulateClassHierarchy 一次性搞定。


FClassViewerNode —— 类节点数据结构

FClassViewerNode 是树中每个类节点的表示,它继承自 TSharedFromThis<FClassViewerNode> 以支持在 Slate 中共享使用。

三种构造方式

FClassViewerNode(UClass* Class);                                    // 从已加载的 UClass 构造
FClassViewerNode(const FString& InClassName, const FString& InClassDisplayName);  // 从名称构造(未加载 / None 选项)
FClassViewerNode(const FClassViewerNode& InCopyObject);             // 复制构造(不复制子节点列表)

复制构造函数的特点是明确不复制 ChildrenList。这是有意为之——在 AddChildren_Tree / GetClassTree 中,每个展示节点的子节点列表是在过滤过程中重新构建的,而不是从原始节点复制。

关键字段

字段类型说明
ClassTWeakObjectPtr<UClass>指向实际的 UClass,未加载时为 nullptr
BlueprintTWeakObjectPtr<UBlueprint>指向对应的蓝图资产
ClassPathFTopLevelAssetPath完整类路径(如 /Script/CoreUObject.Actor
ParentClassPathFTopLevelAssetPath父类路径
BlueprintAssetPathFSoftObjectPath蓝图的资产路径(仅蓝图有效)
UnloadedBlueprintDataTSharedPtr<IUnloadedBlueprintData>未加载蓝图的数据代理
bPassesFilterbool是否通过过滤(含文本过滤)
bPassesFilterRegardlessTextFilterbool是否通过过滤(不含文本过滤)
PropertyHandleTSharedPtr<IPropertyHandle>关联的属性句柄

bPassesFilterbPassesFilterRegardlessTextFilter 两个字段的区分非常关键。考虑这样一个场景:用户在搜索框中输入了 “Horse”,一个名为 AHorse 的 Actor 类当然通过了文本过滤。但它的父类 AActor 虽然不匹配 “Horse”,却必须出现在树中作为层级结构的桥梁。此时 AActorbPassesFilter 为 false(灰显),但 bPassesFilterRegardlessTextFilter 为 true(可选择但不高亮)。AddChildren_Tree 正是通过这种双重判断来决定节点的展示方式。

GetClassName 的 NameType 策略

EClassViewerNameTypeToDisplay 有三种取值:

  • ClassName:只显示 C++ 内部类名(如 MyActor
  • DisplayName:只显示显示名称(如 “我的 Actor”)
  • Dynamic:如果显示名称与类名不同,则同时显示(如 MyActor (我的 Actor)),否则只显示类名

Dynamic 模式下的实现为:先用 FName::NameToDisplayString 将类名转为显示形式,与 ClassDisplayName 比较,如果二者都不相同才拼接。

IsClassPlaceable() 的判断逻辑

一个类是否"可放置"需要同时满足三个条件:

  1. 没有 CLASS_AbstractCLASS_NotPlaceable 标记
  2. AActor 的子类
  3. 不是 ABrush 的子类(笔刷是特殊的 Actor,不应出现在放置列表中)

对于未加载的蓝图,通过 UnloadedBlueprintData 进行同样的逻辑判断。


IUnloadedBlueprintData / FUnloadedBlueprintData —— 未加载蓝图的数据代理

这是 ClassViewer 的一个关键抽象层。当一个蓝图类尚未被加载到内存时,无法通过 UClass 获取其信息,但 AssetRegistry 的资产标签中已经存储了足够多的元数据(ClassFlags、父类路径、实现的接口列表、是否 Normal Blueprint 等)。

IUnloadedBlueprintData 定义了未加载类的查询接口:

HasAnyClassFlags / HasAllClassFlags  → 检查类标记
ImplementsInterface                  → 检查是否实现某接口
IsChildOf / IsA                      → 检查继承关系
GetClassWithin / GetNativeParent     → 获取类范围 / 原生父类
IsNormalBlueprintType                → 是否 Normal 蓝图
GetClassName / GetClassPathName      → 获取类名 / 类路径

FUnloadedBlueprintData 的实现中,有两个值得留意的设计:

1. IsChildOf 通过遍历父节点链实现。 由于蓝图还未加载,不能用 UClass::IsChildOf。替代方案是沿着 ClassViewerNodeParentNode 链逐级向上查找,比较 Class.Get() 指针。

2. IsA 的实现比较简单但实用。 未加载的蓝图必然是一个 UBlueprintGeneratedClass,所以直接返回 UBlueprintGeneratedClass::StaticClass()->IsA(InClass)。这个近似在绝大多数情况下是正确的。

3. GetClassWithin 和 GetNativeParent 沿着父节点链向上查找。 一路向上直到找到一个 Class.IsValid() 的节点,从那里获取信息。这个假设基于"蓝图不会改变 ClassWithin"这个事实。


FClassViewerFilter —— 内建过滤器

FClassViewerFilter 是 ClassViewer 提供的默认过滤器实现,它同时实现了 IClassViewerFilter 接口中所有的过滤逻辑。它的构造函数接受 FClassViewerInitializationOptions,并持有以下关键成员:

TextFilter (FTextFilterExpressionEvaluator)  → 文本搜索
FilterFunctions (FClassViewerFilterFuncs)    → 辅助过滤函数(供自定义过滤器使用)
InternalClasses / InternalPaths              → "Internal Only" 的类/路径列表
AssetReferenceFilter (IAssetReferenceFilter) → 资产引用过滤器
AssetRegistry                                → 资产注册表引用

IsClassAllowed 过滤链

这是整个模块中逻辑最密集的函数之一,按顺序执行以下检查:

1. Actors Only           → InClass->IsChildOf(AActor)
2. Blueprint Base Only   → CanCreateBlueprintOfClass(InClass)
3. Editor Classes Only   → IsEditorOnlyObject(InClass)
4. Developer Folder 过滤  → 根据 UClassViewerSettings::DeveloperFolderType
5. Internal Classes 过滤  → 根据 InternalPaths / InternalClasses
6. AllowedClasses 白名单  → 如果配置了,只显示列表中的类
7. Placeable Only        → IsPlaceable(InClass) 且非 Brush
8. REINST 类过滤          → 排除 REINST_ 前缀类
9. 自定义 Filters         → 遍历 InInitOptions.ClassFilters
10. 旧版 ClassFilter      → 向后兼容(已声明 Deprecated)
11. 全局 Filter           → 模块级 GlobalClassViewerFilter
12. 文本搜索              → PassesTextFilter(类名、带前缀类名、显示名)
13. 资产引用过滤           → AssetReferenceFilter->PassesFilter(仅非 Native 类)

只有全部通过这 13 层检查,一个类才会出现在列表中。

Developer Folder 过滤

三种模式(EClassViewerDeveloperType):

  • CVDT_None:不显示任何 Developer 文件夹下的类
  • CVDT_CurrentUser:只显示当前用户的 Developer 文件夹
  • CVDT_All:显示全部

判断逻辑通过比较类的生成路径是否以 GameDevelopersDirGameUserDeveloperDir 开头来实现。

文本搜索的特殊处理

文本搜索会同时匹配三种名称:

PassesTextFilter(InClass->GetName(), TextFilter)              // 无前缀(如 "Actor")
PassesTextFilter(ClassNameWithCppPrefix, TextFilter)          // 带 C++ 前缀(如 "AActor")
PassesTextFilter(InClass->GetDisplayNameText().ToString(), ...) // 显示名

另外,如果类是 Deprecated 的,还会去掉名称中自动插入的 _DEPRECATED 标记后再匹配一次——这样用户输入原始类名依然能搜到被标记为废弃的类。

IsUnloadedClassAllowed —— 未加载类的过滤

这是 IsClassAllowed 的镜像函数,用于判断未加载蓝图是否通过过滤。大部分检查逻辑与已加载版本一致,但有几个区别:

  • Editor Classes Only 在未加载情况下无法判断(代码中有 TODO 注释)
  • 蓝图相关的检查(如 CheckIfBlueprintBase)需要读取 GEngineIni 中的 AllowDerivedBlueprints 配置
  • 文本搜索只检查类名(无法检查显示名,因为未加载时显示名可能不可靠)

FClassViewerFilterFuncs —— 辅助过滤函数集

这个类为自定义 IClassViewerFilter 实现提供了一系列便捷的判断函数。它们不是直接被 ClassViewer 调用的,而是供外部过滤器在 IsClassAllowed 中使用。

三态返回值 EFilterReturn

Passed  → 通过检查
Failed  → 未通过检查
NoItems → 检查集合为空,无法判断(通常意味着"不应该过滤")

主要函数类别:

函数族含义示例用途
IfInChildOfClassesSet类是否是集合中任一类的子类“只显示 GameplayAbility 的子类”
IfMatchesAllInChildOfClassesSet类是否同时是集合中所有类的子类“必须同时继承 A 和 B”
IfMatchesAll_ObjectsSetIsAClass集合中所有对象是否都是某类的实例验证引用资产
IfMatchesAll_ClassesSetIsAClass同样的逻辑应用于 UClass 集合
IfMatches_ClassesSetIsAClass集合中任一对象是否是某类的实例
IfInClassesSet类是否在指定集合中精确匹配某个类列表

每个函数都提供了 UClass*IUnloadedBlueprintData 两个重载版本,确保在类未加载时也能正确判断。


SClassViewer —— Slate UI 控件

SClassViewer 是一个 SCompoundWidget,它是用户最终看到和交互的界面。

UI 结构

SClassViewer
  └── SBox (MaxDesiredHeight: 800)
       └── SBorder
            └── SVerticalBox
                 ├── [标题文本] (可选)
                 ├── SHorizontalBox
                 │    ├── Filters 按钮 (仅 Browsing 模式)
                 │    ├── 搜索框 (SSearchBox)
                 │    └── View Options 按钮 (齿轮图标)
                 ├── SSeparator
                 ├── SOverlay
                 │    ├── STreeView / SListView (根据 DisplayMode)
                 │    └── AssetDiscoveryIndicator (底部的加载指示条)
                 └── [类数量文本] (如 "42 items")

ClassPicker + ListView 模式下,整个控件会被 SListViewSelectorDropdownMenu 包裹,提供更好的弹出菜单键盘导航体验。

双视图架构

SClassViewer 同时持有一个 STreeView 和一个 SListView,但同一时刻只有一个可见(通过 Visibility 控制)。两个视图共享同一组数据源 RootTreeItems

为什么不用同一个 View 切换模式?因为 STreeViewSListView 的接口差异很大:

  • STreeView 需要 OnGetChildren 回调来递归获取子节点
  • SListView 只需要一个扁平化的 ListItemsSource

强行用一个 View 同时支持两种模式会让代码变得复杂且效率低下。

Tick 周期

SClassViewer::Tick 每帧检查以下几件事:

  1. 调用 ClassViewer::Helpers::PopulateClassHierarchy() —— 如果标记位 bPopulateClassHierarchy 为 true,则重建全局类层次
  2. bPendingFocusNextFrame —— 将键盘焦点移到搜索框
  3. bNeedsRefresh —— 调用 Populate() 重新生成展示树/列表
  4. bPendingSetExpansionStates —— 恢复树的展开状态

bNeedsRefreshPopulate() 之间的延迟机制值得注意:Refresh() 只是设置 bNeedsRefresh = true,真正的 Populate 发生在下一帧 Tick 中。这样可以实现"合并多次 Refresh 请求"——如果在同一帧内搜索文本改变 + 过滤选项切换,两次 Refresh() 调用只会触发一次 Populate()

选中逻辑

OnClassViewerSelectionChanged 根据 Mode 有不同行为:

  • ClassBrowsing 模式:调用 GUnrealEd->SetCurrentClass() 将选中类同步到编辑器全局状态(用于右键放置等操作)
  • ClassPicker 模式
    • 如果类未加载且 bEnableClassDynamicLoading 为 true → 加载它
    • 如果节点通过过滤(bPassesFilterRegardlessTextFilter)→ 执行 OnClassPicked 回调
    • 如果节点未通过过滤(如灰显的父节点)→ 执行 OnClassPicked(nullptr)

右键菜单

BuildMenuWidget 根据选中节点的类型构建不同的菜单:

如果类是 Blueprint Base(可创建蓝图)且已有蓝图:

  • Edit Blueprint Class…
  • Find in Content Browser…

如果类是 C++ 原生类(无蓝图):

  • Open Source Code…(跳转到 IDE)
  • Create New C++ Class…(打开 AddCode 向导)

SClassItem —— 行视觉元素

SClassItemSTableRow 的子类,负责渲染树/列表中的每一行。它的视觉结构:

SHorizontalBox
  ├── SExpanderArrow (展开箭头,仅树模式)
  ├── SImage (类图标,从 FSlateIconFinder 查找)
  ├── STextBlock (类名,带高亮、灰显、斜体支持)
  └── SComboButton (操作下拉菜单,仅 Browsing 模式且存在蓝图时显示)

几个视觉细节:

  1. 灰显:未通过文本过滤的父节点以 Alpha=0.5 渲染
  2. 斜体:可放置的 Actor 类使用 NormalFontItalic 字体,与不可放置的 Actor 形成视觉区分
  3. Tooltip:如果节点是 Restricted 的(被 PropertyHandle 限制),Tooltip 显示限制原因;否则使用 FEditorClassUtils::GetTooltip 获取类的文档信息

双击行为

ClassBrowsing 模式

  • 如果类未加载 → 先加载
  • 如果有蓝图 → 打开蓝图编辑器
  • 否则 → 在 IDE 中打开源码

ClassPicker 模式

  • 触发 OnDoubleClicked 委托(默认行为是切换节点的展开/折叠状态)

UClassViewerSettings / UClassViewerProjectSettings —— 配置层

UClassViewerSettings(用户级,UnrealEd 模块)

存储在 EditorPerProjectUserSettings 配置文件中,每个开发者可以有自己的偏好:

AllowedClasses        → 允许显示的类列表(白名单,空表示全部允许)
DisplayInternalClasses → 是否显示 Internal Only 类
DeveloperFolderType    → Developer 文件夹的显示策略

它通过 FSettingChangedEvent 事件通知所有 SClassViewer 实例刷新。SClassViewerConstruct 时会订阅这个事件。

UClassViewerProjectSettings(项目级,ClassViewer 模块)

存储在 Engine 配置中,通常只有项目技术负责人才会修改:

InternalOnlyPaths   → 标记为 Internal Only 的目录列表
InternalOnlyClasses  → 标记为 Internal Only 的基类列表

当用户在 View Options 中关闭 “Show Internal Classes” 时,这两项配置会被读取并应用到过滤中。InternalOnlyClasses 的逻辑是:凡是继承自这些基类的类都视为 Internal。比如把 /Script/Engine.Info 设为 Internal Only,那么所有 Info 子类在关闭"Show Internal Classes"后都会隐藏。


功能使用示例编写

示例1:在自定义编辑器工具中嵌入类选择器

最常用的场景——在自己的编辑器中嵌入一个选类控件。

// MyCustomTool.cpp
#include "ClassViewerModule.h"
#include "SClassViewer.h"

void SMyCustomTool::Construct(const FArguments& InArgs)
{
    FClassViewerModule& ClassViewerModule = FModuleManager::LoadModuleChecked<FClassViewerModule>("ClassViewer");

    // 配置初始化选项
    FClassViewerInitializationOptions InitOptions;
    InitOptions.Mode = EClassViewerMode::ClassPicker;
    InitOptions.DisplayMode = EClassViewerDisplayMode::ListView;
    InitOptions.bIsActorsOnly = true;
    InitOptions.bIsPlaceableOnly = false;
    InitOptions.bShowNoneOption = true;
    InitOptions.NameTypeToDisplay = EClassViewerNameTypeToDisplay::Dynamic;
    InitOptions.ViewerTitleString = NSLOCTEXT("MyTool", "PickClass", "选择要生成的 Actor 类型");

    // 绑定选中回调
    FOnClassPicked OnPicked;
    OnPicked.BindLambda([](UClass* PickedClass)
    {
        if (PickedClass)
        {
            UE_LOG(LogTemp, Log, TEXT("用户选择了:%s"), *PickedClass->GetName());
        }
    });

    ChildSlot
    [
        ClassViewerModule.CreateClassViewer(InitOptions, OnPicked)
    ];
}

示例2:自定义过滤器 —— 只显示实现了特定接口的类

// MyInterfaceFilter.h
#include "ClassViewerFilter.h"

class FMyGameplayTagInterfaceFilter : public IClassViewerFilter
{
public:
    virtual bool IsClassAllowed(
        const FClassViewerInitializationOptions& InInitOptions,
        const UClass* InClass,
        TSharedRef<FClassViewerFilterFuncs> InFilterFuncs) override
    {
        // 只允许实现了 IMyGameplayTagAssetInterface 的类
        return InClass && InClass->ImplementsInterface(UMyGameplayTagAssetInterface::StaticClass());
    }

    virtual bool IsUnloadedClassAllowed(
        const FClassViewerInitializationOptions& InInitOptions,
        const TSharedRef<const IUnloadedBlueprintData> InUnloadedClassData,
        TSharedRef<FClassViewerFilterFuncs> InFilterFuncs) override
    {
        // 对未加载的蓝图也进行同样的检查
        return InUnloadedClassData->ImplementsInterface(UMyGameplayTagAssetInterface::StaticClass());
    }
};

使用时,把这个过滤器加入 InitOptions.ClassFilters 即可:

InitOptions.ClassFilters.Add(MakeShared<FMyGameplayTagAssetInterfaceFilter>());

示例3:带自定义过滤选项的过滤器

如果希望给用户在 View Options 菜单中提供一个开关来控制过滤行为:

// MyToggleableFilter.h
class FMyToggleableFilter : public IClassViewerFilter
{
    bool bFilterEnabled = true;

public:
    virtual void GetFilterOptions(TArray<TSharedRef<FClassViewerFilterOption>>& OutFilterOptions) override
    {
        TSharedRef<FClassViewerFilterOption> MyOption = MakeShared<FClassViewerFilterOption>();
        MyOption->bEnabled = bFilterEnabled;
        MyOption->LabelText = NSLOCTEXT("MyTool", "FilterLabel", "只显示 Gameplay 相关类");
        MyOption->ToolTipText = NSLOCTEXT("MyTool", "FilterTooltip", "启用后只显示继承自 UGameplayAbility 或 UAttributeSet 的类");
        MyOption->OnOptionChanged = FOnClassViewerFilterOptionChanged::CreateLambda(
            [this](bool bIsEnabled) { bFilterEnabled = bIsEnabled; }
        );
        OutFilterOptions.Add(MyOption);
    }

    virtual bool IsClassAllowed(
        const FClassViewerInitializationOptions& InInitOptions,
        const UClass* InClass,
        TSharedRef<FClassViewerFilterFuncs> InFilterFuncs) override
    {
        if (!bFilterEnabled)
        {
            return true; // 过滤器未激活,全部通过
        }

        return InClass && (
            InClass->IsChildOf(UGameplayAbility::StaticClass()) ||
            InClass->IsChildOf(UAttributeSet::StaticClass())
        );
    }

    virtual bool IsUnloadedClassAllowed(
        const FClassViewerInitializationOptions& InInitOptions,
        const TSharedRef<const IUnloadedBlueprintData> InUnloadedClassData,
        TSharedRef<FClassViewerFilterFuncs> InFilterFuncs) override
    {
        if (!bFilterEnabled)
        {
            return true;
        }

        return InUnloadedClassData->IsChildOf(UGameplayAbility::StaticClass()) ||
               InUnloadedClassData->IsChildOf(UAttributeSet::StaticClass());
    }
};

GetFilterOptions 中返回的 FClassViewerFilterOption 会被自动添加到 SClassViewer 的 View Options 菜单中,用户可以直接点击切换。

示例4:注册全局过滤器

如果某个规则需要作用于项目中的所有类选择器,可以注册为全局过滤器:

// 在编辑器模块的 StartupModule 中
void FMyEditorModule::StartupModule()
{
    FClassViewerModule& ClassViewerModule = FModuleManager::LoadModuleChecked<FClassViewerModule>("ClassViewer");
    ClassViewerModule.RegisterGlobalClassViewerFilter(MakeShared<FMyGlobalRestrictionFilter>());
}

全局过滤器在所有 ClassViewer 实例的内建过滤链末端执行,无需每个调用方单独配置。

示例5:通过 PropertyHandle 限制类的选择范围

ClassViewer 与 PropertyEditor 的集成非常紧密。当通过 FClassViewerInitializationOptions::PropertyHandle 传入一个属性句柄时,ClassViewer 会自动:

  1. 收集该属性的引用资产(用于 IAssetReferenceFilter 检查)
  2. 应用 IsRestricted 限制(由 IPropertyHandle 提供)
// 在自定义属性编辑器(IDetailCustomization)中
void FMyClassPropertyCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, 
    IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
    FClassViewerInitializationOptions InitOptions;
    InitOptions.Mode = EClassViewerMode::ClassPicker;
    InitOptions.DisplayMode = EClassViewerDisplayMode::ListView;
    InitOptions.PropertyHandle = PropertyHandle;  // 关键:传入属性句柄
    InitOptions.bShowNoneOption = true;

    TSharedRef<SWidget> ClassPicker = FModuleManager::LoadModuleChecked<FClassViewerModule>("ClassViewer")
        .CreateClassViewer(InitOptions, FOnClassPicked::CreateSP(this, &FMyClassPropertyCustomization::OnClassPicked));

    ChildBuilder.AddCustomRow(NSLOCTEXT("MyTool", "Class", "Class"))
        .NameContent()
        [
            PropertyHandle->CreatePropertyNameWidget()
        ]
        .ValueContent()
        [
            ClassPicker
        ];
}

总结与最佳实践

架构设计优势

  1. 数据与视图分离FClassHierarchy 维护全局唯一的数据层,所有 SClassViewer 实例共享同一份类树,各自通过 Populate() 生成自己的过滤后视图。这避免了数据冗余,也确保了数据一致性

  2. 未加载蓝图的无缝支持:通过 IUnloadedBlueprintData 抽象和 AssetRegistry 集成,用户可以在不加载任何蓝图的前提下浏览和搜索全部蓝图类,这对编辑器启动性能和内存占用至关重要

  3. 高度可扩展的过滤体系:从内建的 FClassViewerFilterIClassViewerFilter 接口,从实例级过滤器到模块级全局过滤器,从属性句柄限制到资产引用过滤器——过滤能力可以按需叠加

  4. 事件驱动的自动更新:AssetRegistry、蓝图编译、模块变更等事件都会自动触发类层次重建,使用者无需关心底层数据何时变化

  5. 灵活的双模式视图:TreeView 适合探索继承关系,ListView 适合快速选取,两者共享数据层但各自有独立的渲染和交互逻辑

使用要点

  • FClassViewerInitializationOptions 是调用的核心。 在创建 ClassViewer 之前,确保理解每个选项的含义,尤其是 ModebShowUnloadedBlueprintsbEnableClassDynamicLoading 这些会影响最终行为的选项

  • 自定义过滤器要记得实现 IsUnloadedClassAllowed 如果只实现了 IsClassAllowed 而忽略未加载版本,那么在未加载蓝图上过滤器会失效,导致不一致的展示结果

  • FStreamableHandle 的教训同样适用于 ClassViewer。 虽然 ClassViewer 本身不涉及异步加载,但当结合使用(比如在选中类后异步加载其资源)时,确保管理好引用生命周期

  • 善用全局过滤器来减少重复配置。 如果项目有统一的类显示策略(如永远隐藏某些内部模块的类),注册一次全局过滤器远比在每个调用方重复配置要干净

  • 警惕 PropertyHandleIsRestricted 影响。 当通过属性句柄创建 ClassViewer 时,某些类可能被标记为 Restricted 而灰显不可选,确保在测试时有正确的属性上下文

常见问题

问题1:某些蓝图类在 ClassViewer 中看不到

检查以下几点:

  • 该蓝图的 GeneratedClassPath 标签是否完整(在 AssetRegistry 中可以查看)
  • 父类是否在类层次中存在(如果父类本身因为某种原因被排除,子类也会跟着消失)
  • 是否被 Developer Folder 过滤或 Internal Classes 过滤拦截

问题2:过滤后的列表中出现了灰显的类

这些是"通过了除文本过滤以外的所有过滤条件,但文本搜索未命中"的节点。它们通常是为了维持树形结构而存在的父类节点。如果不希望它们出现,可以考虑在 AddChildren_Tree 逻辑(需要修改引擎源码)中进一步过滤

问题3:自定义过滤器在未加载的蓝图上不生效

检查是否同时实现了 IsClassAllowedIsUnloadedClassAllowed。未加载蓝图走的是后者

问题4:ClassViewer 的刷新不及时

Refresh() 是延迟到下一帧 Tick 中执行的。如果在同一帧内做了修改并对 Refresh() 的生效时机有强依赖,需要手动触发 Tick 或调整逻辑为异步等待


附录:文件结构参考

ClassViewer/
├── ClassViewer.Build.cs
├── Public/
│   ├── ClassViewerModule.h               # FClassViewerModule / FClassViewerInitializationOptions / 枚举定义
│   ├── ClassViewerFilter.h               # IClassViewerFilter / FClassViewerFilter / FClassViewerFilterFuncs / IUnloadedBlueprintData
│   ├── ClassViewerProjectSettings.h      # UClassViewerProjectSettings
│   └── SClassViewer.h                    # SClassViewer 声明
└── Private/
    ├── ClassViewerModule.cpp             # 模块注册 / NomadTab / Settings
    ├── ClassViewerNode.h / .cpp           # FClassViewerNode
    ├── ClassViewerFilter.cpp             # FClassViewerFilter / FClassViewerFilterFuncs 实现
    ├── SClassViewer.cpp                  # SClassViewer / FClassHierarchy / SClassItem 实现
    ├── UnloadedBlueprintData.h / .cpp     # FUnloadedBlueprintData
    └── ClassViewerProjectSettings.cpp     # UClassViewerProjectSettings

外部依赖:

UnrealEd/Classes/Settings/
└── ClassViewerSettings.h                 # UClassViewerSettings / EClassViewerDeveloperType
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值