Extend content browser
创建自定义菜单入口的步骤:create custom menu entry. steps:
Load content browser module ->
PathViewContextMenuExtenders ->
Add in our own delegate ->
Bind to our own member functions
基础概念(本文实时穿插蓝色词条概念解释,可跳过)
Delegateshttps://dev.epicgames.com/documentation/en-us/unreal-engine/delegates?application_version=4.27https://dev.epicgames.com/documentation/en-us/unreal-engine/delegates?application_version=4.27why:
委托(Delegates) 是一种允许以类型安全的方式调用C++对象成员函数的机制。它们可以动态绑定到任意对象的成员函数,并在未来调用该函数,而不需要事先知道对象的具体类型。
假设你有一个类Car
,类中定义了几个成员变量(例如汽车的颜色和速度),还有一个成员函数Drive
。这个函数是属于Car
类的,它可以操作Car
对象的状态。
Drive()
函数就是Car
类的一个成员函数。每个Car
类的对象(比如一辆汽车)都可以调用Drive()
函数来启动驾驶。
what:
- 单播(Single Delegate):只能绑定一个函数,通常用于一对一的函数调用场景。
- 多播(Multicast Delegate):可以绑定多个函数,调用时会依次执行所有绑定的函数,常用于需要通知多个观察者的情况。
- 动态委托(Dynamic Delegate):这些委托适用于UObject,支持序列化,能在蓝图中使用。
how:
声明委托
声明委托时,你需要使用虚幻提供的宏,这些宏会根据你要绑定的函数的返回类型和参数来生成相应的委托类型。以下是一些常见的声明委托的宏:
DECLARE_DELEGATE(DelegateName)
:声明一个无参数、无返回值的委托。DECLARE_DELEGATE_OneParam(DelegateName, Param1Type)
:声明一个带一个参数的委托。DECLARE_DELEGATE_TwoParams(DelegateName, Param1Type, Param2Type)
:声明一个带两个参数的委托。
对于动态和多播委托,宏的格式略有不同,比如:
DECLARE_DYNAMIC_MULTICAST_DELEGATE...
用于动态多播委托,支持蓝图。DECLARE_DYNAMIC_DELEGATE...
用于动态单播委托。
绑定委托
在虚幻引擎中,委托可以通过多种方式绑定到函数:
BindStatic()
:绑定到全局静态函数。BindRaw()
:绑定到原始C++指针,但使用时要注意内存管理问题。BindLambda()
:绑定到Lambda表达式,适合简洁的函数。BindSP()
:绑定到共享指针(shared pointer)的成员函数。BindUObject()
:绑定到UObject类型的成员函数,虚幻引擎会自动处理UObject的生命周期管理。
执行委托
执行委托时,通常会使用Execute()
或ExecuteIfBound()
方法。Execute()
会直接执行绑定的函数,如果委托未绑定则会触发断言,而ExecuteIfBound()
会在执行前检查委托是否绑定了函数。
示例:当用户点击按钮时,委托可以动态绑定不同的处理函数,实现按钮点击的不同反应。
Button->OnClick.AddDynamic(this, &MyClass::OnButtonClicked);
Bulk Operation
Bulk Operation(批量操作)
- 重点:主要关注一次处理大量数据的操作。
- 应用:通常用于需要高效处理大量数据或资源的场景中,比如数据库操作、文件操作或内存操作。
- 实时性:批量操作通常是实时执行的,意味着在需要时可以立即开始处理大量项。例如,数据库中的批量插入操作就是在一个事务中立即完成多条数据的插入。
- 示例:一次性插入多条数据库记录、一次性复制大块内存数据、一次性移动多个文件等。
2. Batch Operation(批处理操作)
- 重点:主要关注一组任务或作业的批处理,通常是延迟或计划执行的。
- 应用:批处理操作通常是在后台或非高峰时段执行的,比如在银行系统或数据处理系统中,可能会在夜间批量处理积累的任务或数据。
- 实时性:批处理操作往往是定时执行的,通常不是实时的。例如,批处理系统可以在一天结束后处理所有待处理的订单、生成报表等。
- 示例:夜间处理一整天的银行交易、定时运行的报表生成、离线数据分析任务等。
auto
在 C++ 中,auto
关键字用来让编译器根据初始化表达式的类型自动推断变量的类型。也就是说,当你使用 auto
声明一个变量时,你不需要明确地写出它的类型,编译器会根据你赋给这个变量的值推断出类型。
auto ContentBrowserModuleMenuExtenders = ContentBrowserModule.GetAllPathViewContextMenuExtenders();
以reference方式存储变量
TArray<FContentBrowserMenuExtender_SelectedPaths>& ContentBrowserModuleMenuExtenders =
ContentBrowserModule.GetAllPathViewContextMenuExtenders();
When we mark a variable as reference, technically this variable doesn't exist. This is just an alias and this is an alias of the array that we want. for our custom menu entry to work, it is very important that we are accessing this original array and not making a copy.
if without this & we would just be holding this array in another copy and by adding our delegate into that copy, we won't be able to see any changes.
当我们将一个变量标记为引用时,技术上来说,这个变量并不存在。它只是一个别名(alias),是我们想要访问的数组的别名。为了使我们的自定义菜单项正常工作,访问这个原始数组非常重要,而不是创建一个副本。
如果没有使用引用符号(&
),我们实际上是在持有这个数组的一个副本,向这个副本中添加我们的委托时,我们将无法看到任何变化。
在这个上下文中,确保操作的是原始数组,而不是副本,是实现功能所必需的。这样才能正确反映出对数组的更改或操作。
#pragma region ContentBrowserMenuExtension
void FSuperManagerModule::InitContentBrowserExtension()
{
FContentBrowserModule& ContentBrowserModule =
FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
TArray<FContentBrowserMenuExtender_SelectedPaths>& ContentBrowserModuleMenuExtenders =
ContentBrowserModule.GetAllPathViewContextMenuExtenders();
FContentBrowserMenuExtender_SelectedPaths CustomContentBrowserMenuDelegate;
ContentBrowserModuleMenuExtenders.Add(CustomContentBrowserMenuDelegate);
//add our own delegate, but so far it is only an empty delegate, there's no binding
//we need to bind it to our own member function, in there we can define the title for our menu entry, and the function
}
#pragma endregion
Binding Member Functions
- First Binding: Define the position for inserting menu entry
- Second Binding: Define the detals for the menu entry ( Title, Tooltip, Function...)
https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Slate/Framework/MultiBox/FBaseMenuBuilder/AddMenuEntryhttps://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Slate/Framework/MultiBox/FBaseMenuBuilder/AddMenuEntry
- Third Binding: the actual function to excute
TShareRef is a type of small pointer
(will go over small pointer more in depth when dealing with slave widgets)
"slave widgets" 通常指的是那些依赖于其他主控组件(通常称为 "master widget")的子组件或控件。它们的行为、状态或外观可能会受到主控组件的影响。
FContentBrowserMenuExtender_SelectedPaths CustomContentBrowserMenuDelegate;
CustomContentBrowserMenuDelegate.BindRaw(this, &FSuperManagerModule::CustomContentBrowserMenuExtender);
ContentBrowserModuleMenuExtenders.Add(CustomContentBrowserMenuDelegate);
//more understandable way
BindRaw: 这个方法用于将一个成员函数绑定到委托上。BindRaw
表示绑定的是一个原始指针(raw pointer),即直接指向一个对象的指针,而不是使用智能指针。这样做时要注意,确保绑定的对象在委托执行时仍然存在。
更简洁的写法:(使用CreateRaw)
ContentBrowserModuleMenuExtenders.Add(FContentBrowserMenuExtender_SelectedPaths::
CreateRaw(this, &FSuperManagerModule::CustomContentBrowserMenuExtender));
TSharedRef<FExtender> MenuExtender(new FExtender());//We use the new keyword to create an FExtender
be extremely careful when you use the new keyword in unreal, otherwise you'll have memory leak.
这句话的意思是:在 Unreal Engine 中使用 new
关键字时要非常小心,否则可能会导致内存泄漏(memory leak)。
内存泄漏的解释:
内存泄漏是指程序在运行过程中动态分配内存,但未能释放这些内存的情况。具体来说,这意味着:
-
分配内存:程序使用
new
或类似方法申请了一块内存空间,用于存储数据。 -
未释放内存:当程序结束使用这块内存时,应该调用
delete
或相应的方法来释放它。如果忘记了这一操作,分配的内存将无法被程序重新利用,导致系统可用内存逐渐减少。 -
后果:内存泄漏会导致程序占用越来越多的内存,最终可能导致系统性能下降、程序崩溃,甚至在长期运行的情况下可能耗尽系统内存。
在 Unreal Engine 中的注意事项:
-
使用智能指针:为了避免内存泄漏,Unreal Engine 提供了智能指针(如
TSharedPtr
和TUniquePtr
),它们会自动管理内存的分配和释放。使用这些智能指针可以显著降低内存泄漏的风险。 -
对象管理:确保在使用
new
创建对象后,适当地管理其生命周期,及时调用delete
,以释放内存。 -
检查指针:在使用指针之前,始终检查它们是否有效,确保不会访问已释放或未分配的内存。
使用“内存泄漏”(memory leak)这个术语是因为其隐含的特征与物理世界中的“泄漏”有相似之处。以下是为什么使用“泄漏”这个词的原因:
1. 隐喻的意义
- 泄漏通常指的是液体或气体从容器中意外流出,无法被回收或利用。在编程中,内存泄漏意味着程序分配了一些内存,但由于没有适当地释放或管理,这些内存就无法再被程序使用或回收。
new关键字
new
关键字在 C++ 中用于动态分配内存。具体来说,它的主要作用是:
1. 动态内存分配
- 当你使用
new
关键字时,它会在堆(heap)上分配一块内存,以存储你所需的对象或数据。 - 这意味着你可以在程序运行时根据需要分配内存,而不必在编译时确定所需的内存大小。
2. 创建对象
new
不仅分配内存,还会调用构造函数来初始化对象。例如:
MyClass* myObject = new MyClass();
- 在这个例子中,
new MyClass()
分配了一块内存,用于存储MyClass
类型的对象,并调用该对象的构造函数。 -
3. 返回指针
new
关键字会返回一个指向分配内存块的指针。这使得你能够通过该指针访问和操作对象。例如:
int* myArray = new int[10]; // 动态分配一个包含10个整数的数组
4. 释放内存
- 使用
new
动态分配的内存需要在不再使用时通过delete
关键字手动释放
TSharedRef<FExtender> FSuperManagerModule::CustomContentBrowserMenuExtender(const TArray<FString>& SelectedPaths)
{
TSharedRef<FExtender> MenuExtender(new FExtender());
//if check
return MenuExtender;
}
CustomContentBrowserMenuExtender,whenever this function is called, a TArray of Fstring will be passed in, this will tell as how many folders are currently selected inside of our content browser
Display UI Extension Points
在编辑器偏好设置里启用Display UI Extension Points,并重启编辑器,会得到Extension Hooks
最终示例代码
SuperManager.h
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FSuperManagerModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
#pragma region ContentBrowserMenuExtension
void InitContentBrowserExtension();
TSharedRef<FExtender>CustomContentBrowserMenuExtender(const TArray<FString>& SelectedPaths);
void AddContentBrowserMenuEntry(class FMenuBuilder& MenuBuilder);
void OnDeleteUnusedAssetButtonClicked();
#pragma endregion
};
SuperManager.cpp
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SuperManager.h"
#include "ContentBrowserModule.h"
#define LOCTEXT_NAMESPACE "FSuperManagerModule"
void FSuperManagerModule::StartupModule()
{
InitContentBrowserExtension();
}
void FSuperManagerModule::ShutdownModule()
{
//This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
//we call this function before unloading the module.
}
#pragma region ContentBrowserMenuExtension
void FSuperManagerModule::InitContentBrowserExtension()
{
FContentBrowserModule& ContentBrowserModule =
FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
TArray<FContentBrowserMenuExtender_SelectedPaths>& ContentBrowserModuleMenuExtenders =
ContentBrowserModule.GetAllPathViewContextMenuExtenders();
//FContentBrowserMenuExtender_SelectedPaths CustomContentBrowserMenuDelegate;
//CustomContentBrowserMenuDelegate.BindRaw(this, &FSuperManagerModule::CustomContentBrowserMenuExtender);
//ContentBrowserModuleMenuExtenders.Add(CustomContentBrowserMenuDelegate);
//***same with the two lines below
ContentBrowserModuleMenuExtenders.Add(FContentBrowserMenuExtender_SelectedPaths::
CreateRaw(this, &FSuperManagerModule::CustomContentBrowserMenuExtender));
}
TSharedRef<FExtender> FSuperManagerModule::CustomContentBrowserMenuExtender(const TArray<FString>& SelectedPaths)
{
TSharedRef<FExtender> MenuExtender(new FExtender());
if (SelectedPaths.Num() > 0) {
//define the position
//-> 是一个运算符,用于访问指针所指向对象的成员。
MenuExtender->AddMenuExtension(FName("Delete"),//name of extension hook
//we want to insert after DELETE(extension hook)
EExtensionHook::After,
TSharedPtr<FUICommandList>(),//no hotkey
//first binding
FMenuExtensionDelegate::CreateRaw(this, &FSuperManagerModule::AddContentBrowserMenuEntry));
//second binding
};
return MenuExtender;
}
void FSuperManagerModule::AddContentBrowserMenuEntry(FMenuBuilder& MenuBuilder)
{//in this function we can define all the details for our menu entry //and we can use the menu builder to do so
MenuBuilder.AddMenuEntry
(
FText::FromString(TEXT("Delete Unused Assets")),
FText::FromString(TEXT("Safely delete all unused assets under folder")),
FSlateIcon(),//empty placeholder
//SECOND BINDING
FExecuteAction::CreateRaw(this, &FSuperManagerModule::OnDeleteUnusedAssetButtonClicked)
);
}
void FSuperManagerModule::OnDeleteUnusedAssetButtonClicked()
{
//not finished yet
}
#pragma endregion
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FSuperManagerModule, SuperManager)