UE 图表资产编辑器 (Graph Editor) 拓展模板
说明
在 UE 中拓展一个 Graph Asset Editor 时,有很多文件需要完成,每次都写还是有点重复,经常可能遗漏掉,因此考虑写一个简单的模板,再有开发相关编辑器的需求时时复制一份,改一改、删一删来实现相关功能。
GitHub: GraphSampleEditor (文中的代码只节选了一部分贴出,有需求还是查看 GitHub 吧)
文章里主要也只梳理一遍相关文件与部分拓展接口,当一个开发文档来用,拓展时方便来查一查。
运行时逻辑与业务需求密切相关,因此只简单添加了一个运行时的 Node,基于其提供的几个方法完成了显示与搜索,但没有实现具体的逻辑,逻辑还是根据具体业务来完成。
开发记录
一、资产相关
图表编辑器本质上也是用来编辑资产的,图表其实只是资产编辑器中的一部分,资产编辑器也可以有其他类型的编辑面板,例如 StateTree 使用的 TreeView 等。
创建 AssetEditor 有一些资产相关类需要完成拓展:
1. UGraphSampleAsset
继承 UObject 实现即可,往往 EdGraph 会直接存储在资产中。
UCLASS()
class GRAPHSAMPLE_API UGraphSampleAsset : public UObject
{
GENERATED_BODY()
public:
#if WITH_EDITOR
friend class UGraphSampleGraph;
UEdGraph* GetGraph() const { return GraphSampleGraph; }
#endif
#if WITH_EDITORONLY_DATA
UPROPERTY()
TObjectPtr<UEdGraph> GraphSampleGraph;
#endif
};
2. FAssetTypeActions_GraphSampleAsset
覆写相关方法可以完成资产属性的控制,如资产名称、资产颜色、资产类别、唤起资产编辑器的回调等。
其中的 OpenAssetEditor 方法就是唤起我们自定义的编辑器的位置,具体在编辑器类拓展中详细描述。
class GRAPHSAMPLEEDITOR_API FAssetTypeActions_GraphSampleAsset final : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual uint32 GetCategories() override;
virtual FColor GetTypeColor() const override { return FColor(255, 0, 255); }
virtual UClass* GetSupportedClass() const override;
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor = TSharedPtr<IToolkitHost>()) override;
};
这个类是一个 F 类,所以是需要注册的,笔者这里是在 FGraphSampleEditorModule::StartupModule 中注册的,返回的 GraphSampleAssetCategory 与 FAssetTypeActions_GraphSampleAsset 中的 GetCategories 需要保持一致,才能将资产注册到 ContentBrowser 右键菜单匹配的类别下:
void FGraphSampleEditorModule::RegisterAssets()
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
if (GraphSampleAssetCategory == EAssetTypeCategories::None)
{
GraphSampleAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("GraphSample")), LOCTEXT("GraphSampleEditorModule_GraphSampleCategory", "GraphSample"));
}
const TSharedRef<IAssetTypeActions> GraphSampleAssetActions = MakeShareable(new FAssetTypeActions_GraphSampleAsset());
RegisteredAssetActions.Add(GraphSampleAssetActions);
AssetTools.RegisterAssetTypeActions(GraphSampleAssetActions);
}
void FGraphSampleEditorModule::UnregisterAssets()
{
if (FModuleManager::Get().IsModuleLoaded("AssetTools"))
{
IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
for (const TSharedRef<IAssetTypeActions>& TypeAction : RegisteredAssetActions)
{
AssetTools.UnregisterAssetTypeActions(TypeAction);
}
}
RegisteredAssetActions.Empty();
}
3. UGraphSampleAssetFactory
资产的工厂方法,继承相关方法完成资产的创建逻辑。UFactory 中有一些方法提供拓展,注册外部关联文件、其他类型文件导入也都是通过拓展工厂类来实现的。
UCLASS(HideCategories = Object)
class GRAPHSAMPLEEDITOR_API UGraphSampleAssetFactory : public UFactory
{
GENERATED_UCLASS_BODY()
virtual bool ConfigureProperties() override;
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
};
构造中需要指定此工厂绑定的 Class:
UGraphSampleAssetFactory::UGraphSampleAssetFactory(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
SupportedClass = UGraphSampleAsset::StaticClass();
bCreateNew = true;
bEditorImport = false;
bEditAfterNew = true;
}
UObject* UGraphSampleAssetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
UGraphSampleAsset* NewGraphSampleAsset = NewObject<UGraphSampleAsset>(InParent, SupportedClass, Name, Flags | RF_Transactional, Context);
UGraphSampleGraph::CreateGraph(NewGraphSampleAsset);
return NewGraphSampleAsset;
}
因为是 U 类,内部使用反射处理的,不需要自己额外注册,具体可查看:
TArray<UFactory*> UAssetToolsImpl::GetNewAssetFactories() const
{
TArray<UFactory*> Factories;
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Class = *It;
if (!Class->IsChildOf(UFactory::StaticClass()) || Class->HasAnyClassFlags(CLASS_Abstract))
{
continue;
}
UFactory* Factory = Class->GetDefaultObject<UFactory>();
...
Factories.Add(Factory);
}
return MoveTemp(Factories);
}
4. FGraphSampleAssetEditor
实际的资产编辑器类,GraphEditor 往往只作为资产编辑器中的一个界面,一般还会包括 DetailsTab、PaletteTab (节点面板)、SearchTab(这个示例里没写,可以参考之前给 FlowGraph 添加的搜索功能) 等。
继承 FAssetEditorToolkit 方法,拓展相关方法即可:
class GRAPHSAMPLEEDITOR_API FGraphSampleAssetEditor : public FAssetEditorToolkit
{
...
// IToolkit
virtual FName GetToolkitFName() const override;
virtual FText GetBaseToolkitName() const override;
virtual FString GetWorldCentricTabPrefix() const override;
virtual FLinearColor GetWorldCentricTabColorScale() const override;
virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;
virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;
// --
// FAssetEditorToolkit
virtual void InitToolMenuContext(FToolMenuContext& MenuContext) override;
virtual void PostRegenerateMenusAndToolbars() override;
virtual bool CanSaveAsset() const override;
// --
}
核心的方法是 InitAssetEditor 方法:
/**
* Initializes this asset editor. Called immediately after construction.
* Override PostInitAssetEditor if you need to do additional initialization
*
* @param Mode Asset editing mode for this editor (standalone or world-centric)
* @param InitToolkitHost When Mode is WorldCentric, this is the level editor instance to spawn this editor within
* @param AppIdentifier When Mode is Standalone, this is the app identifier of the app that should host this toolkit
* @param StandaloneDefaultLayout The default layout for a standalone asset editor
* @param bCreateDefaultToolbar The default toolbar, which can be extended
* @param bCreateDefaultStandaloneMenu True if in standalone mode, the asset editor should automatically generate a default "asset" menu, or false if you're going to do this yourself in your derived asset editor's implementation
* @param ObjectToEdit The object to edit
* @param bInIsToolbarFocusable Whether the buttons on the default toolbar can receive keyboard focus
* @param bUseSmallToolbarIcons Whether the buttons on the default toolbar use the small icons
* @param InOpenMethod Override whether the Asset Editor is being opened in read only or edit mode (otherwise automatically set by the asset editor subsytem for any asset editors opened through it)
*/
UNREALED_API void InitAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr<IToolkitHost>& InitToolkitHost, const FName AppIdentifier, const TSharedRef<FTabManager::FLayout>& StandaloneDefaultLayout, const bool bCreateDefaultStandaloneMenu, const bool bCreateDefaultToolbar, const TArray<UObject*>& ObjectsToEdit, const bool bInIsToolbarFocusable = false, const bool bInUseSmallToolbarIcons = false, const TOptional<EAssetOpenMethod>& InOpenMethod = TOptional<EAssetOpenMethod>());
UNREALED_API void InitAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr<IToolkitHost>& InitToolkitHost, const FName AppIdentifier, const TSharedRef<FTabManager::FLayout>& StandaloneDefaultLayout, const bool bCreateDefaultStandaloneMenu, const bool bCreateDefaultToolbar, UObject* ObjectToEdit, const bool bInIsToolbarFocusable = false, const bool bInUseSmallToolbarIcons = false, const TOptional<EAssetOpenMethod>& InOpenMethod = TOptional<EAssetOpenMethod>());
其提供了很多参数,我们需要在资产被打开时,创建 AssetEditor 、填充核心参数调用此方法完成编辑器的初始化。
这个调用时机上文已经提及到了,就是我们创建的类 FAssetTypeActions_GraphSampleAsset 的 OpenAssetEditor 方法,在此处创建 AssetEditor 并初始化即可。其他情况下资产编辑器的唤起,通过 GEditor->GetEditorSubsystem <UAssetEditorSubsystem>()->OpenEditorForAsset(TargetAsset)
来完成即可。
笔者代码中提供了一个外部方法,用以在 OpenAssetEditor 时完成初始化逻辑,并组织参数调用了 InitAssetEditor 方法:
void FGraphSampleAssetEditor::InitGraphSampleAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr<class IToolkitHost>& InitToolkitHost, UObject* ObjectToEdit)
{
GraphSampleAsset = CastChecked<UGraphSampleAsset>(ObjectToEdit);
BindToolbarCommands();
RegisterDelegates();
CreateToolbar();
CreateWidgets();
const TSharedRef<FTabManager::FLayout> StandaloneDefaultLayout = FTabManager::NewLayout("GraphSampleAssetEditor_Layout_v0.1")
->AddArea
(
FTabManager::NewPrimaryArea()->SetOrientation(Orient_Horizontal)
->Split
(
FTabManager::NewStack()
->SetSizeCoefficient(0.225f)
->AddTab(DetailsTab, ETabState::OpenedTab)
)
->Split
(
FTabManager::NewStack()
->SetSizeCoefficient(0.6f)
->SetHideTabWell(true)
->AddTab(GraphTab, ETabState::OpenedTab)
)
->Split
(
FTabManager::NewStack()
->SetSizeCoefficient(0.175f)
->AddTab(PaletteTab, ETabState::OpenedTab)
)
);
constexpr bool bCreateDefaultStandaloneMenu = true;
constexpr bool bCreateDefaultToolbar = true;
InitAssetEditor(Mode, InitToolkitHost, TEXT("GraphSampleEditorApp"), StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, ObjectToEdit, false);
RegenerateMenusAndToolbars();
}
几个封装的方法完成了一些界面的逻辑,注册了一些命令、代理,然后创建了 Toolbar 和 各个界面的 Widget,这些在合适的时机完成即可。
然后完成了编辑器默认布局的创建,这里只横向划分了三块区域,分别是 DetailsTab、GraphTab 和 PaletteTab。这里创建的是编辑器的默认布局,一般 Layout 里会跟一个版本号,布局被用户修改后会保存在 C:\Users\wangxudong\AppData\Local\UnrealEngine\Editor\EditorLayout.json 文件中,例如笔者这里的样式如下:
{
"LayoutName": "NSLOCTEXT(\"LayoutNamespace\", \"Wxd_UE4\", \"Wxd UE4\")",
"LayoutDescription": "NSLOCTEXT(\"LayoutNamespace\", \"Copy_of_The_default_UE4_layout\", \"Copy of The default UE4 layout\")",
"UnrealEd_Layout_v1.5":
{
...
"GraphSampleAssetEditor_Layout_v0.1":
{
"Type": "Layout",
"Name": "GraphSampleAssetEditor_Layout_v0.1",
"PrimaryAreaIndex": 0,
"Areas": [
{
"SizeCoefficient": 1,
"Type": "Area",
"Orientation": "Orient_Horizontal",
"WindowPlacement": "Placement_NoWindow",
"Nodes": [
{
"SizeCoefficient": 0.24016501009464264,
"Type": "Stack",
"HideTabWell": false,
"ForegroundTab": "Details",
"Tabs": [
{
"TabId": "Details",
"TabState": "OpenedTab"
}
]
},
{
"SizeCoefficient": 0.54246246814727783,
"Type": "Stack",
"HideTabWell": true,
"ForegroundTab": "Graph",
"Tabs": [
{
"TabId": "Graph",
"TabState": "OpenedTab"
}
]
},
{
"SizeCoefficient": 0.21737273037433624,
"Type": "Stack",
"HideTabWell": false,
"ForegroundTab": "Palette",
"Tabs": [
{
"TabId": "Palette",
"TabState": "OpenedTab"
}
]
}
]
}
]
}
...
}
构建完 Layout 后,我们将其作为入参传入了 InitAssetEditor 方法,来初始化编辑器的默认布局。
这里还有其他几个参数,简单说明一下:
- const EToolkitMode::Type Mode 用来指定资产编辑器的模式,是独立窗口,还是嵌入当前的关卡编辑器中,一般由 Asset 发起的都作为独立窗口。
- const TSharedPtr
<IToolkitHost>
& InitToolkitHost 转入 OpenAssetEditor 方法中的入参即可 - const FName AppIdentifier 当模式为独立模式时,是托管该编辑器的应用程序的标识符
- const bool bCreateDefaultStandaloneMenu 是否在独立模式下自动生成默认的“资产”菜单。这里是指资产编辑器左上角菜单的 Asset 选项,原生提供了一些资产操作,如 CopyRef、SizeMap 等。
- const bool bCreateDefaultToolbar 是否创建默认工具栏。
- UObject ObjectToEdit 要编辑的对象。
- const bool bInIsToolbarFocusable = false 默认工具栏上的按钮是否可以接收键盘焦点。
- const bool bInUseSmallToolbarIcons = false 默认工具栏上的按钮是否使用小图标。
- const TOptional
<EAssetOpenMethod>
& InOpenMethod = TOptional<EAssetOpenMethod>
() 覆盖资产编辑器是以只读模式还是编辑模式打开。
完成了初始化逻辑后,还有一个核心的方法我们需要 override,即 RegisterTabSpawners 方法,这个方法会在 InitAssetEditor 的后续逻辑中调用,我们需要借助入参的 InTabManager,注册编辑器各个界面 Tab 创建的相关回调:
void FGraphSampleAssetEditor::RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_GraphSampleAssetEditor", "GraphSample Editor"));
const auto WorkspaceMenuCategoryRef = WorkspaceMenuCategory.ToSharedRef();
FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
InTabManager->RegisterTabSpawner(DetailsTab, FOnSpawnTab::CreateSP(this, &FGraphSampleAssetEditor::SpawnTab_Details))
.SetDisplayName(LOCTEXT("DetailsTab", "Details"))
.SetGroup(WorkspaceMenuCategoryRef)
.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details"));
InTabManager->RegisterTabSpawner(GraphTab, FOnSpawnTab::CreateSP(this, &FGraphSampleAssetEditor::SpawnTab_Graph))
.SetDisplayName(LOCTEXT("GraphTab", "Graph"))
.SetGroup(WorkspaceMenuCategoryRef)
.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "GraphEditor.EventGraph_16x"));
InTabManager->RegisterTabSpawner(PaletteTab, FOnSpawnTab::CreateSP(this, &FGraphSampleAssetEditor::SpawnTab_Palette))
.SetDisplayName(LOCTEXT("PaletteTab", "Palette"))
.SetGroup(WorkspaceMenuCategoryRef)
.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "Kismet.Tabs.Palette"));
}
在相应的注册的 SpawnTab 的方法中,完成相应 Tab 控件的创建并返回即可,例如 IDetailsView、SGraphEditor 等。
例:
void FGraphSampleAssetEditor::CreateWidgets() // 笔者这里在 InitGraphSampleAssetEditor 调用的这一方法,在 Spawn 的回调中创建也一样。
{
...
// Palette
Palette = SNew(SGraphSamplePalette, SharedThis(this));
}
TSharedRef<SDockTab> FGraphSampleAssetEditor::SpawnTab_Palette(const FSpawnTabArgs& Args) const
{
check(Args.GetTabId() == PaletteTab);
return SNew(SDockTab)
.Label(LOCTEXT("GraphSamplePaletteTitle", "Palette"))
[
Palette.ToSharedRef()
];
}
完成后,记得还要完成反注册的方法 UnregisterTabSpawners,其他的可以覆写的方法这里不再赘述,根据需求自行添加即可。
二、GraphTab
在创建 GraphTab 时,我们 SNew 了一个 SGraphSampleGraphEditor,这里就是图表编辑器的 S 类控件,也就是图表部分的入口:
SAssignNew(GraphEditor, SGraphSampleGraphEditor, SharedThis(this)).DetailsView(DetailsView);
TSharedRef<SDockTab> FGraphSampleAssetEditor::SpawnTab_Graph(const FSpawnTabArgs& Args) const
{
check(Args.GetTabId() == GraphTab);
TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
.Label(LOCTEXT("GraphSampleGraphTitle", "Graph"));
if (GraphEditor.IsValid())
{
SpawnedTab->SetContent(GraphEditor.ToSharedRef());
}
return SpawnedTab;
}
1. SGraphSampleGraphEditor
图表编辑器的 S 类,类的 Argument 我们定义了两个,GraphEvent 和 DetailsView,如果有一些图表操作的相关代理在资产编辑器中需要回调,可以在 GraphEvent 中绑定,笔者的案例里并没有使用,传入的 DetailsView 则是用于调整细节面板内容的显示,在选中节点与资产自身的 Details 之间切换。
class GRAPHSAMPLEEDITOR_API SGraphSampleGraphEditor : public SGraphEditor
{
public:
SLATE_BEGIN_ARGS(SGraphSampleGraphEditor) {}
SLATE_ARGUMENT(SGraphEditor::FGraphEditorEvents, GraphEvents)
SLATE_ARGUMENT(TSharedPtr<IDetailsView>, DetailsView)
SLATE_END_ARGS()
...
void Construct(const FArguments& InArgs, const TSharedPtr<FGraphSampleAssetEditor> InAssetEditor);
...
};
在 Construct 时,我们向 SGraphEditor::FArguments Arguments 的 _GraphToEdit 参数传入了我们的图表,这里就关联起来了图表的 U 类,也就是 UEdGraph。
void SGraphSampleGraphEditor::Construct(const FArguments& InArgs, const TSharedPtr<FGraphSampleAssetEditor> InAssetEditor)
{
GraphSampleAssetEditor = InAssetEditor;
GraphSampleAsset = GraphSampleAssetEditor.Pin()->GetGraphSampleAsset();
DetailsView = InArgs._DetailsView;
BindGraphCommands();
SGraphEditor::FArguments Arguments;
Arguments._AdditionalCommands = CommandList;
Arguments._Appearance = GetGraphAppearanceInfo();
Arguments._GraphToEdit = GraphSampleAsset->GetGraph();
Arguments._GraphEvents = InArgs._GraphEvents;
Arguments._GraphEvents.OnNodeDoubleClicked = FSingleNodeEvent::CreateSP(this, &SGraphSampleGraphEditor::OnNodeDoubleClicked);
Arguments._AutoExpandActionMenu = true;
Arguments._GraphEvents.OnSelectionChanged = FOnSelectionChanged::CreateSP(this, &SGraphSampleGraphEditor::OnSelectedNodesChanged);
SGraphEditor::Construct(Arguments);
}
BindGraphCommands 方法中主要就是组织了 CommandList,绑定了相关回调,笔者这里保留了比较通用的 FGenericCommands 和 FGraphEditorCommandsImpl,可根据业务调整:
void SGraphSampleGraphEditor::BindGraphCommands()
{
FGraphEditorCommands::Register();
const FGenericCommands& GenericCommands = FGenericCommands::Get();
const FGraphEditorCommandsImpl& GraphEditorCommands = FGraphEditorCommands::Get();
CommandList = MakeShareable(new FUICommandList);
// Graph commands
CommandList->MapAction(GraphEditorCommands.CreateComment,
FExecuteAction::CreateSP(this, &SGraphSampleGraphEditor::OnCreateComment),
FCanExecuteAction::CreateStatic(&SGraphSampleGraphEditor::CanEdit));
...
}
2. UGraphSampleGraph
在 Graph 的 S 类中,我们传入了 Graph 的 U 类对象,也就是 UGraphSampleGraph:
UCLASS()
class GRAPHSAMPLEEDITOR_API UGraphSampleGraph : public UEdGraph
{
GENERATED_BODY()
public:
static UEdGraph* CreateGraph(UGraphSampleAsset* InGraphSampleAsset);
static UEdGraph* CreateGraph(UGraphSampleAsset* InGraphSampleAsset, const TSubclassOf<UGraphSampleGraphSchema>& GraphSampleSchema);
UGraphSampleAsset* GetGraphSampleAsset() const;
void OnGraphSampleNodeChanged();
};
笔者这里只提供了几个帮助方法,类很简单,这是因为 UEdGraph 的对象会被依次传递到 SGraphEditorImpl、SGraphPanel,而相关界面中的回调大多数都是通过 UEdGraph 的 GetSchema 方法,获取到匹配的 Schema 类的 CDO 对象来执行相关交互操作回调,因此这个类核心是要关联到具体的 Scheme 类,所以笔者这里提供了一个创建 UEdGraph 的静态方法,当创建资产时使用这个方法来创建资产内部关联的 UEdGraph。
静态方法内部通过 FBlueprintEditorUtils::CreateNewGraph 来创建,入参中的 TSubclassOf <class UEdGraphSchema>
SchemaClass 传入了我们需要关联的 Schema 类,也就是 UGraphSampleGraphSchema,其会赋值到 UEdGraph 对象的成员中,以获取其 CDO 转调相关操作。
UEdGraph* UGraphSampleGraph::CreateGraph(UGraphSampleAsset* InGraphSampleAsset)
{
return CreateGraph(InGraphSampleAsset, UGraphSampleGraphSchema::StaticClass());
}
UEdGraph* UGraphSampleGraph::CreateGraph(UGraphSampleAsset* InGraphSampleAsset, const TSubclassOf<UGraphSampleGraphSchema>& GraphSampleSchema)
{
check(GraphSampleSchema);
UEdGraph* NewGraph = CastChecked<UGraphSampleGraph>(FBlueprintEditorUtils::CreateNewGraph(InGraphSampleAsset, NAME_None, StaticClass(), GraphSampleSchema));
NewGraph->bAllowDeletion = false;
InGraphSampleAsset->GraphSampleGraph = NewGraph;
NewGraph->GetSchema()->CreateDefaultNodesForGraph(*NewGraph);
return NewGraph;
}
FBlueprintEditorUtils::CreateNewGraph 方法:
UEdGraph* FBlueprintEditorUtils::CreateNewGraph(UObject* ParentScope, const FName& GraphName, TSubclassOf<class UEdGraph> GraphClass, TSubclassOf<class UEdGraphSchema> SchemaClass)
{
...
NewGraph = NewObject<UEdGraph>(ParentScope, GraphClass, NAME_None, RF_Transactional);
...
NewGraph->Schema = SchemaClass; // 传递的 Schema 类赋值到了 UEdGraph 对象的成员中
...
return NewGraph;
}
GetSchema 方法:
// EdGraph.h
/** The schema that this graph obeys */
UPROPERTY()
TSubclassOf<class UEdGraphSchema> Schema; //
// EdGraph.cpp
const UEdGraphSchema* UEdGraph::GetSchema() const
{
if (Schema == NULL)
{
return NULL;
}
return GetDefault<UEdGraphSchema>(Schema); // 这里取了 Schema 类的 CDO
}
3. UGraphSampleGraphSchema
因为很多回调转调到了这个类,所以很多的逻辑都需要在这里完成,这也是很多文章中会将 Schema 视为图表的 规则类 的原因。
UCLASS()
class GRAPHSAMPLEEDITOR_API UGraphSampleGraphSchema : public UEdGraphSchema
{
GENERATED_BODY()
public:
// EdGraphSchema
virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const override;
virtual void CreateDefaultNodesForGraph(UEdGraph& Graph) const override;
virtual const FPinConnectionResponse CanCreateConnection(const UEdGraphPin* PinA, const UEdGraphPin* PinB) const override;
virtual bool TryCreateConnection(UEdGraphPin* A, UEdGraphPin* B) const override;
virtual bool ShouldHidePinDefaultValue(UEdGraphPin* Pin) const override;
virtual FLinearColor GetPinTypeColor(const FEdGraphPinType& PinType) const override;
virtual FText GetPinDisplayName(const UEdGraphPin* Pin) const override;
virtual void BreakNodeLinks(UEdGraphNode& TargetNode) const override;
virtual void BreakPinLinks(UEdGraphPin& TargetPin, bool bSendsNodeNotification) const override;
virtual int32 GetNodeSelectionCount(const UEdGraph* Graph) const override;
virtual TSharedPtr<FEdGraphSchemaAction> GetCreateCommentAction() const override;
virtual void OnPinConnectionDoubleCicked(UEdGraphPin* PinA, UEdGraphPin* PinB, const FVector2D& GraphPosition) const override;
// --
...
};
可以继承覆写的方法很多,不再一一详细叙述,UEdGraphSchema 中的注释还是挺全的,这里只挑几个关键的总结下:
GetGraphContextActions
获取右键单击图表空白区域或从引脚拖拽释放按键时可以执行的操作列表。
/**
* Get all actions that can be performed when right clicking on a graph or drag-releasing on a graph from a pin
*
* @param [in,out] ContextMenuBuilder The context (graph, dragged pin, etc...) and output menu builder.
*/
ENGINE_API virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const;
通俗讲也就是右键空白区域时出现的节点列表框,列表里实际上是执行创建节点的一系列行为对象,需要我们注册到方法入参 ContextMenuBuilder 之中。
这里首先拆分了两部分,注释节点和普通的节点创建的行为对象,这里普通节点的方法传入了一个空字符串,这是因为在 Palette 面板构建时也会用到这一方法,并且面板中会提供筛选的功能,注释节点这里传入了一个 Graph,主要是为了区分是否有节点被选中,以提供不同的菜单文本。
<a id="UGraphSampleGraphSchema::GetGraphContextActions"></a>
void UGraphSampleGraphSchema::GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const
{
GetGraphSampleNodeActions(ContextMenuBuilder, FString());
GetCommentAction(ContextMenuBuilder, ContextMenuBuilder.CurrentGraph);
}
方法内,主要就是按照业务逻辑,添加负责创建节点的 Action 类,这里笔者参考了 FlowGraph 的方案,迭代寻找运行时节点子类的 CDO,并将其传入了自定义的 FGraphSampleGraphSchemaAction_NewNode 来完成新建 GraphNode 的操作,这里需要根据具体的业务调整。
入参的 CategoryName 在迭代子类时进行了判断,筛选需要添加到 ActionMenuBuilder 中的节点,这一 CategoryName 会在 Palette 面板中传递至此,在该面板时详细描述。
void UGraphSampleGraphSchema::GetGraphSampleNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const FString& CategoryName)
{
// In most cases, base node considered as an abstract node, for test add base node here
if (const UGraphSampleNode* BaseGraphNode = UGraphSampleNode::StaticClass()->GetDefaultObject<UGraphSampleNode>())
{
const TSharedPtr<FGraphSampleGraphSchemaAction_NewNode> NewNodeAction(new FGraphSampleGraphSchemaAction_NewNode(BaseGraphNode));
ActionMenuBuilder.AddAction(NewNodeAction);
}
TArray<UClass*> GraphSampleNodes;
GetDerivedClasses(UGraphSampleNode::StaticClass(), GraphSampleNodes);
for (const UClass* FocusesClass : GraphSampleNodes)
{
if (const UGraphSampleNode* FocusesGraphNode = FocusesClass->GetDefaultObject<UGraphSampleNode>())
{
if (CategoryName.IsEmpty() || CategoryName.Equals(FocusesGraphNode->GetNodeCategory()))
{
const TSharedPtr<FGraphSampleGraphSchemaAction_NewNode> NewNodeAction(new FGraphSampleGraphSchemaAction_NewNode(FocusesGraphNode));
ActionMenuBuilder.AddAction(NewNodeAction);
}
}
}
// Maybe need add BP extension categories here
}
在完成了这一方法,并将节点的创建行为类添加到 ActionMenuBuilder 后,节点面板就可以显示注册的行为类,并在交互时执行到行为类中的相关逻辑,来创建节点了。
CanCreateConnection
判断两个节点引脚之间是否可以创建连接,这里往往也是一个需要拓展的方法,来控制节点间的连接关系,根据其返回参数中的枚举值,可以控制相关连接的行为,例如某些新连接创建后,需要断开之前的连接,判断出入 Pin 后返回枚举中的 CONNECT_RESPONSE_BREAKOTHERS[AB] 即可。
/** This is the type of response the graph editor should take when making a connection */
UENUM()
enum ECanCreateConnectionResponse : int
{
/** Make the connection; there are no issues (message string is displayed if not empty). */
CONNECT_RESPONSE_MAKE,
/** Cannot make this connection; display the message string as an error. */
CONNECT_RESPONSE_DISALLOW,
/** Break all existing connections on A and make the new connection (it's exclusive); display the message string as a warning/notice. */
CONNECT_RESPONSE_BREAK_OTHERS_A,
/** Break all existing connections on B and make the new connection (it's exclusive); display the message string as a warning/notice. */
CONNECT_RESPONSE_BREAK_OTHERS_B,
/** Break all existing connections on A and B, and make the new connection (it's exclusive); display the message string as a warning/notice. */
CONNECT_RESPONSE_BREAK_OTHERS_AB,
/** Make the connection via an intermediate cast node, or some other conversion node. */
CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE,
/** Make the connection by promoting a lower type to a higher type. Ex: Connecting a Float -> Double, float should become a double */
CONNECT_RESPONSE_MAKE_WITH_PROMOTION,
CONNECT_RESPONSE_MAX,
};
virtual const FPinConnectionResponse CanCreateConnection(const UEdGraphPin* PinA, const UEdGraphPin* PinB) const override;
其他的不再赘述:
- CreateDefaultNodesForGraph:为新建的图表创建默认节点。
- GetCreateCommentAction: 添加创建注释的 Action。
- TryCreateConnection:尝试创建两个节点间的连接。
- OnPinConnectionDoubleCicked: 往往会在这里创建 Reroute 节点。
- ...
4. UGraphSampleGraphNode
Schema 中我们提到了右键菜单时注册的 Action,会产出一个我们的 GraphNode 节点,因此先来看下 GraphNode 中的逻辑。
UCLASS()
class GRAPHSAMPLEEDITOR_API UGraphSampleGraphNode : public UEdGraphNode
{
GENERATED_BODY()
public:
UPROPERTY(Instanced, VisibleAnywhere, BlueprintReadWrite, meta = (ShowOnlyInnerProperties))
UGraphSampleNode* GraphSampleNode;
virtual void GetNodeContextMenuActions(class UToolMenu* Menu, class UGraphNodeContextMenuContext* Context) const override;
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
virtual void AllocateDefaultPins() override;
virtual FLinearColor GetNodeTitleColor() const override;
virtual FSlateIcon GetIconAndTint(FLinearColor& OutColor) const override;
virtual bool CanUserDeleteNode() const override;
virtual bool CanDuplicateNode() const override;
virtual void AutowireNewNode(UEdGraphPin* FromPin) override;
virtual TSharedPtr<SGraphNode> CreateVisualWidget() override;
...
};
类里我们提供了一个成员 UGraphSampleNode*,这个也就是我们运行时的数据,这里应该根据业务调整。
笔者这里的 UGraphSampleNode 只提供了几个方法,返回一些字符串,来控制创建节点时菜单中的显示内容,实际没有添加运行时逻辑:
UCLASS(Blueprintable)
class GRAPHSAMPLE_API UGraphSampleNode : public UObject
{
GENERATED_BODY()
public:
...
virtual FString GetNodeCategory() const;
virtual FText GetNodeTitle() const;
virtual FText GetNodeToolTip() const;
virtual EGraphSampleNodeState GetActivationState() const;
...
/** When runtime execute node, we can use this state to changed debug style */
EGraphSampleNodeState ActivationState = EGraphSampleNodeState::Default;
...
};
UEdGraphNode 提供了很多可以覆写的方法,注释也挺完善的,这里只挑几个经常使用的简单说下:
- AllocateDefaultPins:分配节点引脚的方法,可以根据节点的类型来完成不同引脚的分配,例如首个(或称 Root) 节点往往没有入 Pin,那在这里区分类型完成即可。
- CreateVisualWidget:创建节点匹配的 S 类的方法,可以在这里指定节点使用的 S 类。
- GetNodeTitle:返回节点标题的方法,默认会显示在节点上方的颜色区块位置。
- GetNodeContextMenuActions:可以在这里拓展右键节点时显示的操作菜单。
- GetNodeTitleColor:标题部分的颜色,默认的 Widget 分上下两部分,标题部分的颜色由这里处理,可以考虑按节点类型在 Settings 中提供相关 Map 来控制不同节点颜色。
- ...
5. FGraphSampleGraphSchemaAction_NewNode
看完了节点的逻辑,可以来看下节点是如何在 Action 中被新建的了,GetTypeId 提供了一点运行时识别的能力,在一些 K2 节点中有些应用,这里可以不用关心,照着写即可。主要逻辑在我们自定义的构造方法这里,这里提供了一个运行时节点的入参,基于传入的运行时节点,我们获取了其 Category、Title、ToolTip 来构造父类,这里三个参数分别传递到了 Category、MenuDescription、TooltipDescription,进而控制了节点列表中显示与悬浮提示的内容。第四个参数是单独用于搜索的关键字,也可根据业务添加。
USTRUCT()
struct GRAPHSAMPLEEDITOR_API FGraphSampleGraphSchemaAction_NewNode : public FEdGraphSchemaAction
{
GENERATED_BODY()
UPROPERTY()
class UClass* NodeClass;
static FName StaticGetTypeId()
{
static FName Type("FGraphSampleGraphSchemaAction_NewNode");
return Type;
}
virtual FName GetTypeId() const override { return StaticGetTypeId(); }
...
explicit FGraphSampleGraphSchemaAction_NewNode(const UGraphSampleNode* InNodeTemplate)
: FEdGraphSchemaAction(FText::FromString(InNodeTemplate->GetNodeCategory()), InNodeTemplate->GetNodeTitle(), InNodeTemplate->GetNodeToolTip(), 0, FText::GetEmpty())
, NodeClass(InNodeTemplate->GetClass()) {}
// FEdGraphSchemaAction
virtual UEdGraphNode* PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode = true) override;
// --
static UGraphSampleGraphNode* CreateNode(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FVector2D Location, const bool bSelectNewNode = true);
};
PerformAction 是在列表中单击节点 Action 后触发的回调,也是实际创建节点的地方,因此我们要在这里创建 GraphNode 并返回,这里转调了一个静态方法,方便外部也能使用(案例中在创建 Reroute 节点时使用了),CreateNode 方法中根据缓存的运行时节点的 NodeClass,调用了 Schema 的 GetMappingGraphNodeClass 方法来获取到了匹配的 GraphNode,这个方法是笔者自己封的,应该根据业务来,这里是因为区分了一个 Reroute 节点,因为其样式不同,所以有单独的 GraphNode 类。其他的调用一致即可,包括触发相关回调、标记更改、分配 Pin 脚、修正位置等。
UEdGraphNode* FGraphSampleGraphSchemaAction_NewNode::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode /* = true*/)
{
// prevent adding new nodes while playing
if (GEditor->PlayWorld != nullptr)
{
return nullptr;
}
if (NodeClass)
{
return CreateNode(ParentGraph, FromPin, NodeClass, Location, bSelectNewNode);
}
return nullptr;
}
UGraphSampleGraphNode* FGraphSampleGraphSchemaAction_NewNode::CreateNode(UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FVector2D Location, const bool bSelectNewNode /*= true*/)
{
const FScopedTransaction Transaction(LOCTEXT("AddNode", "Add Node"));
if (NodeClass == nullptr)
{
NodeClass = UGraphSampleNode::StaticClass();
}
ParentGraph->Modify();
if (FromPin)
{
FromPin->Modify();
}
UGraphSampleAsset* GraphSampleAsset = CastChecked<UGraphSampleGraph>(ParentGraph)->GetGraphSampleAsset();
GraphSampleAsset->Modify();
// create new Graph node
const UClass* GraphNodeClass = UGraphSampleGraphSchema::GetMappingGraphNodeClass(NodeClass);
UGraphSampleGraphNode* NewGraphNode = NewObject<UGraphSampleGraphNode>(ParentGraph, GraphNodeClass, NAME_None, RF_Transactional);
UGraphSampleNode* NewGraphSampleNode = NewObject<UGraphSampleNode>(GraphSampleAsset, NodeClass, NAME_None, RF_Transactional);
NewGraphNode->SetGraphSampleNode(NewGraphSampleNode);
// register to the graph
NewGraphNode->CreateNewGuid();
ParentGraph->AddNode(NewGraphNode, false, bSelectNewNode);
// create pins and connections
NewGraphNode->AllocateDefaultPins();
NewGraphNode->AutowireNewNode(FromPin);
// set position
NewGraphNode->NodePosX = Location.X;
NewGraphNode->NodePosY = Location.Y;
// call notifies
NewGraphNode->PostPlacedNewNode();
ParentGraph->NotifyGraphChanged();
GraphSampleAsset->PostEditChange();
// select in editor UI
if (bSelectNewNode)
{
const TSharedPtr<SGraphSampleGraphEditor> GraphSampleGraphEditor = FGraphSampleEditorHelper::GetGraphSampleGraphEditor(ParentGraph);
if (GraphSampleGraphEditor.IsValid())
{
GraphSampleGraphEditor->SelectSingleNode(NewGraphNode);
}
}
return NewGraphNode;
}
通过如上逻辑,必须的逻辑基本都添加完成了,一个基本的图表编辑 Tab 也可以正常使用了,如下几个类往往也会添加,但不是必须的。
6. FGraphSampleGraphSchemaAction_NewComment
这个类是用来创建注释节点的行为类,在其中创建 UEdGraphNode_Comment 类即可,逻辑参考如下即可,一般也很少会针对注释节点修改。
UEdGraphNode* FGraphSampleGraphSchemaAction_NewComment::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, const bool bSelectNewNode/* = true*/)
{
// prevent adding new nodes while playing
if (GEditor->PlayWorld != nullptr)
{
return nullptr;
}
UEdGraphNode_Comment* CommentTemplate = NewObject<UEdGraphNode_Comment>();
FVector2D SpawnLocation = Location;
const TSharedPtr<SGraphSampleGraphEditor> GraphSampleGraphEditor = FGraphSampleEditorHelper::GetGraphSampleGraphEditor(ParentGraph);
if (GraphSampleGraphEditor.IsValid())
{
FSlateRect Bounds;
if (GraphSampleGraphEditor->GetBoundsForSelectedNodes(Bounds, 50.0f))
{
CommentTemplate->SetBounds(Bounds);
SpawnLocation.X = CommentTemplate->NodePosX;
SpawnLocation.Y = CommentTemplate->NodePosY;
}
}
return FEdGraphSchemaAction_NewNode::SpawnNodeFromTemplate<UEdGraphNode_Comment>(ParentGraph, CommentTemplate, SpawnLocation);
}
注释节点的 NewNodeAction 在两处有使用,一处是在我们创建图表的节点列表时,调用了一个 GetCommentAction 方法:
void UGraphSampleGraphSchema::GetCommentAction(FGraphActionMenuBuilder& ActionMenuBuilder, const UEdGraph* CurrentGraph)
{
if (!ActionMenuBuilder.FromPin)
{
const bool bIsManyNodesSelected = CurrentGraph ? (FGraphSampleEditorHelper::GetGraphSampleGraphEditor(CurrentGraph)->GetNumberOfSelectedNodes() > 0) : false;
const FText MenuDescription = bIsManyNodesSelected ? LOCTEXT("CreateCommentAction", "Create Comment from Selection") : LOCTEXT("AddCommentAction", "Add Comment...");
const FText ToolTip = LOCTEXT("CreateCommentToolTip", "Creates a comment.");
const TSharedPtr<FGraphSampleGraphSchemaAction_NewComment> NewAction(new FGraphSampleGraphSchemaAction_NewComment(FText::GetEmpty(), MenuDescription, ToolTip, 0));
ActionMenuBuilder.AddAction(NewAction);
}
}
一处是 GraphSchema 的一个虚方法:
TSharedPtr<FEdGraphSchemaAction> UGraphSampleGraphSchema::GetCreateCommentAction() const
{
return TSharedPtr<FEdGraphSchemaAction>(static_cast<FEdGraphSchemaAction*>(new FGraphSampleGraphSchemaAction_NewComment));
}
7. UGraphSampleGraphNode_Reroute
为了创建 Reroute 节点,笔者这里继承 UGraphSampleNode 添加了一个运行时节点:
UCLASS(NotBlueprintable, meta = (DisplayName = "Reroute"))
class GRAPHSAMPLE_API UGraphSampleNode_Reroute : public UGraphSampleNode
{
GENERATED_BODY()
#if WITH_EDITOR
virtual FString GetNodeCategory() const;
virtual FText GetNodeTitle() const;
virtual FText GetNodeToolTip() const;
#endif
};
并在 NewNodeAction 时根据 Runtime Node 的类型,来创建对应的 UGraphSampleGraphNode_Reroute 节点,Reroute 节点主要是覆写了 CreateVisualWidget 方法,使用 SGraphNodeKnot 作为节点样式类,运行时逻辑的话考虑直接直接转发给后继节点即可:
UCLASS()
class GRAPHSAMPLEEDITOR_API UGraphSampleGraphNode_Reroute : public UGraphSampleGraphNode
{
GENERATED_BODY()
// UEdGraphNode
virtual TSharedPtr<SGraphNode> CreateVisualWidget() override
{
return SNew(SGraphNodeKnot, this);
}
virtual bool ShouldDrawNodeAsControlPointOnly(int32& OutInputPinIndex, int32& OutOutputPinIndex) const override;
// --
public:
UEdGraphPin* GetInputPin() const;
UEdGraphPin* GetOutputPin() const;
};
当然不考虑在节点列表中显示或添加额外逻辑注册 (类似注释节点),也可以不创建运行时类,注意整理运行时数据时额外处理 GraphReroute 节点即可。
还有一处调用就是当在连线上双击时的回调,在 Schema 的 OnPinConnectionDoubleCicked 方法,参考如下逻辑即可,GetInputPin 与 GetOutputPin 是封装的两个方法,只是返回了找到的第一个出 Pin 和 入 Pin:
void UGraphSampleGraphSchema::OnPinConnectionDoubleCicked(UEdGraphPin* PinA, UEdGraphPin* PinB, const FVector2D& GraphPosition) const
{
const FScopedTransaction Transaction(LOCTEXT("CreateGraphSampleRerouteNodeOnWire", "Create GraphSample Reroute Node"));
const FVector2D NodeSpacerSize(42.0f, 24.0f);
const FVector2D KnotTopLeft = GraphPosition - (NodeSpacerSize * 0.5f);
UEdGraph* ParentGraph = PinA->GetOwningNode()->GetGraph();
if (const UGraphSampleGraphNode* NewGraphNode =
FGraphSampleGraphSchemaAction_NewNode::CreateNode(ParentGraph, nullptr, UGraphSampleNode_Reroute::StaticClass(), KnotTopLeft, false))
{
if (const UGraphSampleGraphNode_Reroute* NewRerouteGraphNode = Cast<UGraphSampleGraphNode_Reroute>(NewGraphNode))
{
TryCreateConnection(PinA, (PinA->Direction == EGPD_Output) ? NewRerouteGraphNode->GetInputPin() : NewRerouteGraphNode->GetOutputPin());
TryCreateConnection(PinB, (PinB->Direction == EGPD_Output) ? NewRerouteGraphNode->GetInputPin() : NewRerouteGraphNode->GetOutputPin());
}
}
}
UEdGraphPin* UGraphSampleGraphNode_Reroute::GetInputPin() const
{
for (UEdGraphPin* Pin : Pins)
{
if (Pin->Direction == EGPD_Input)
{
return Pin;
}
}
return nullptr;
}
8. SGraphSampleGraphNode
自定义的节点样式类,如果有需求可以拓展,这里要注意在 Construct 时主动调用 UpdateGraphNode 方法,其他的方法看父类注释添加即可:
class GRAPHSAMPLEEDITOR_API SGraphSampleGraphNode : public SGraphNode
{
public:
SLATE_BEGIN_ARGS(SGraphSampleGraphNode) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, UGraphSampleGraphNode* InNode);
protected:
virtual const FSlateBrush* GetShadowBrush(bool bSelected) const override;
virtual void GetNodeInfoPopups(FNodeInfoContext* Context, TArray<FGraphInformationPopupInfo>& Popups) const override;
virtual void UpdateGraphNode() override;
const FSlateBrush* GetNodeTitleIcon() const;
protected:
UGraphSampleGraphNode* GraphSampleGraphNode = nullptr;
};
这里的 GetShadowBrush 可以基于节点的运行时状态来修正,以边框来传达节点的执行状态。
还有一些其他的逻辑可以拓展,例如 FConnectionDrawingPolicy 来控制连接线的绘制策略,如果要断点调试,也有一些额外的逻辑要完成,作为简单的一个模板,先不添加这些了。
三、DetailsTab
在创建 DetailsTab 时,这里使用 FPropertyEditorModule 创建了一个 IDetailsView 对象(细节面板的文章可以查看笔者之前的一篇文章:自定义细节面板):
{
FDetailsViewArgs Args;
Args.bHideSelectionTip = true;
Args.bShowPropertyMatrixButton = false;
Args.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide;
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
DetailsView = PropertyModule.CreateDetailView(Args);
DetailsView->SetObject(GraphSampleAsset);
}
// Graph
SAssignNew(GraphEditor, SGraphSampleGraphEditor, SharedThis(this))
.DetailsView(DetailsView);
并把这个对象传入了 SGraphSampleGraphEditor 类中,借助 Arguments._GraphEvents.OnSelectionChanged 代理来完成相关逻辑。
逻辑其实很简单,只是在选中节点时显示节点的细节面板,不再选中节点时显示资产的细节面板,即如下逻辑,无节点选中时填充一个资产,其他情况显示节点的细节面板:
void SGraphSampleGraphEditor::OnSelectedNodesChanged(const TSet<UObject*>& Nodes)
{
TArray<UObject*> SelectedObjects = Nodes.Array();
if (Nodes.IsEmpty())
{
SelectedObjects.Add(GraphSampleAsset.Get());
}
if (DetailsView.IsValid())
{
DetailsView->SetObjects(SelectedObjects);
}
}
四、SGraphSamplePalette
节点列表,右键的菜单足够使用的话也可以不拓展此类。
在创建 PaletteTab 时,我们 SNew 了 SGraphSamplePalette 这一对象,并在 SpawnTab 时传入了对象实例。SGraphSamplePalette 构造时创建了一个 SGraphActionMenu 对象,这个对象其实就是一个针对 Action 类型的封装好的 TreeView 列表,所以也有一个列表元素对象需要拓展,即 SGraphSamplePaletteItem,相关拓展逻辑与 TreeView 基本一致,只是一些接口名字封装了一下,可以查看笔者之前的 UE 自定义树形结构控件,这里不再详细描述,只针对几处核心逻辑简单概述一下:
void SGraphSamplePalette::Construct(const FArguments& InArgs, TWeakPtr<FGraphSampleAssetEditor> InGraphSampleAssetEditor)
{
GraphSampleAssetEditor = InGraphSampleAssetEditor;
UpdateCategoryNames();
UGraphSampleGraphSchema::OnNodeListChanged.AddSP(this, &SGraphSamplePalette::Refresh);
struct LocalUtils
{
static TSharedRef<SExpanderArrow> CreateCustomExpanderStatic(const FCustomExpanderData& ActionMenuData, bool bShowFavoriteToggle)
{
TSharedPtr<SExpanderArrow> CustomExpander;
// in SBlueprintSubPalette here would be a difference depending on bShowFavoriteToggle
SAssignNew(CustomExpander, SExpanderArrow, ActionMenuData.TableRow);
return CustomExpander.ToSharedRef();
}
};
this->ChildSlot
[
SNew(SBorder)
.Padding(2.0f)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
[
SNew(SVerticalBox)
+ SVerticalBox::Slot() // Filter UI
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(0.8)
.VAlign(VAlign_Center)
[
SAssignNew(CategoryComboBox, STextComboBox)
.OptionsSource(&CategoryNames)
.OnSelectionChanged(this, &SGraphSamplePalette::CategorySelectionChanged)
.InitiallySelectedItem(CategoryNames[0])
]
]
+ SVerticalBox::Slot() // Content list
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SAssignNew(GraphActionMenu, SGraphActionMenu)
.OnActionDragged(this, &SGraphSamplePalette::OnActionDragged)
.OnActionSelected(this, &SGraphSamplePalette::OnActionSelected)
.OnCreateWidgetForAction(this, &SGraphSamplePalette::OnCreateWidgetForAction)
.OnCollectAllActions(this, &SGraphSamplePalette::CollectAllActions)
.OnCreateCustomRowExpander_Static(&LocalUtils::CreateCustomExpanderStatic, false)
.AutoExpandActionMenu(true)
]
]
];
}
构造时上方的下拉框我们传入了一个成员 CategoryNames,下方列表则是绑定到了 SGraphSamplePalette::CollectAllActions 方法来完成的。
CategoryNames 会在 Palette 创建时收集一次:
void SGraphSamplePalette::UpdateCategoryNames()
{
CategoryNames = { MakeShareable(new FString(GraphSampleEditor::Constants::SelectedAllCategory)) };
CategoryNames.Append(UGraphSampleGraphSchema::GetGraphSampleNodeCategories());
}
GetGraphSampleNodeCategories 方法就是收集了运行时节点类,使用其 CDO 调用了我们封装的 GetNodeCategory 方法来收集类别。
TArray<TSharedPtr<FString>> UGraphSampleGraphSchema::GetGraphSampleNodeCategories()
{
TSet<FString> UnsortedCategories;
TArray<UClass*> GraphSampleNodes;
GetDerivedClasses(UGraphSampleNode::StaticClass(), GraphSampleNodes);
for (const UClass* FocusesClass : GraphSampleNodes)
{
if (const UGraphSampleNode* FocusesGraphNode = FocusesClass->GetDefaultObject<UGraphSampleNode>())
{
UnsortedCategories.Emplace(FocusesGraphNode->GetNodeCategory());
}
}
TArray<FString> SortedCategories = UnsortedCategories.Array();
SortedCategories.Sort();
// Maybe need add BP extension categories here
// create list of categories
TArray<TSharedPtr<FString>> Result;
for (const FString& Category : SortedCategories)
{
if (!Category.IsEmpty())
{
Result.Emplace(MakeShareable(new FString(Category)));
}
}
return Result;
}
列表元素的收集转调了 UGraphSampleGraphSchema::GetPaletteActions 方法,并传入了当前筛选的类别:
void SGraphSamplePalette::CollectAllActions(FGraphActionListBuilderBase& OutAllActions)
{
ensureAlways(GraphSampleAssetEditor.Pin() && GraphSampleAssetEditor.Pin()->GetGraphSampleAsset());
FGraphActionMenuBuilder ActionMenuBuilder;
UGraphSampleGraphSchema::GetPaletteActions(ActionMenuBuilder, GetFilterCategoryName());
OutAllActions.Append(ActionMenuBuilder);
}
这里与 UGraphSampleGraphSchema::GetGraphContextActions 的方法基本一致,只是传入的 CategoryName 是否为空的区别:
void UGraphSampleGraphSchema::GetPaletteActions(FGraphActionMenuBuilder& ActionMenuBuilder, const FString& CategoryName)
{
GetGraphSampleNodeActions(ActionMenuBuilder, CategoryName);
GetCommentAction(ActionMenuBuilder);
}
具体的逻辑在上文已经提到了,不再赘述。
总结
记录本文档,主要也是作为一个工作文档,方便后续开发时查询,很多逻辑梳理的可能不是很有条理。
GitHub 的模板 : GraphSampleEditor
本篇里没有拓展搜索功能,如果需要可以参考:FlowGraph 添加一个不依赖引擎的搜索功能
Comments NOTHING