THE REQUEST
There is currently no native support for creating UI’s with a uniform look. If one wants to create a consistent UI, he has to copy and paste all the style settings he has into each and every widget that he is placing into his widget blueprints. This is really tiresome… It’s also error prone. One could easily forget to copy and paste parts of his style. And it also does not provide the ability to uniformly change the style afterwards. So basically, we are lacking a theme editor… Or at least a way to set up themes.
So, what I am going to describe/request here is something that will adress all those issues. I was actually planning to make it into a plugin and put it on the marketplace for a small fee.
I also have it running already, but I realized that it would make much more sense to integrate this into the engine. Especially because the necessary code base would be much cleaner and smaller if it could be integrated into the existing widget classes.
Going onwards, I will explain the feature I want to request and continue with suggesting how to implement it. Critics and feedback are welcome.
And I heard that Nick Darnell is in charge of UMG and its integration into the engine, so I hope that particulary he will reply to this feature request and either reject or consider it…
THE FEATURE
The feature/workflow that I want to propose is simple:
- The user right-clicks in the Content Browser, navigates to Miscellaneous > Data Asset and creates an Asset that derives from UMGThemeContainer and calls this new asset “MyAwesomeTheme”.
- In the project or editor settings or somewhere else, the user sets an AssetPtr<UMGThemeContainer> to this new Data Asset, similar to how one sets up his SingletonClass. This way, this AssetPtr can be retrieved in C++ through some function, lets call it UGameSettings::GetTheme().
- In “MyAwesomeTheme”, the user sets up a few colours (and giving them names) and also style templates, that will be applied to UTextBlock widgets (also with names). This UMGThemeContainer asset actually holds TMaps<FString, SomeTemplateType> for various widgets and colours and maybe even more properties. See below for the source.
- The user opens a widgetblueprint asset and drops in a TextBlock widget. He toggles the “Template” property (which is an FString) and enters a template name. After hitting enter, the template will be looked up in the “MyAwesomeTheme” asset and written into the TextBlock.
I think step 3 and 4 are better illustrated with the following pictures:
Step 3:
[SPOILER]
[/SPOILER]
Step 4:
[SPOILER]
[/SPOILER]
IMPLEMENTATION
The above can easily be implemented like this:
First, we add some editor-only properties and methods to the UWidget class. We also add some editor-only code to some of the existing methods. This code will ensure that up-to-date templates will be applied whenever we change anything. It also provides the option to set the template from the UMG editor
Inside Widget.h
[SPOILER]
UCLASS(Abstract, BlueprintType)
class UMG_API UWidget : public UVisual
{
/** I am omitting the current implementation of this class, you can add the following stuff simply at the end of the class definition */
#if WITH_EDITOR
public:
UPROPERTY(EditAnywhere, Category = "Behavior", meta = (InlineEditConditionToggle))
uint32 bApply_Template : 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Candy", meta = (editcondition = "bApply_Template"))
FString Template;
struct FWidgetTemplate* WidgetTemplate; // will be initialized to null
AssetPtr<UMGThemeContainer> Theme;
virtual void ApplyTemplate();
virtual void GetTemplate();
#endif
};
[/SPOILER]
Inside Widget.cpp
[SPOILER]
void UWidget::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
/** I am omitting the current implementation again. The following code will be added at the end of this function */
#if WITH_EDITOR
FName PropertyName = (PropertyChangedEvent.Property != NULL) ? PropertyChangedEvent.Property->GetFName() : NAME_None;
if (PropertyName == GET_MEMBER_NAME_CHECKED(UWidget, Template))
{
ApplyTemplate();
}
#endif
}
#if WITH_EDITOR
void UWidget::PostLoad()
{
/** I am omitting the current implementation again. The following code will be added at the end of this function */
#if WITH_EDITOR
ApplyTemplate();
#endif
}
void UWidget::ApplyTemplate()
{
GetTemplate(); // Get the templae anew, because we might have changed the theme asset and template name in the meantime. So we need to assure that we are acessing up to date data.
if(bApply_Template)
{
if(WidgetTemplate) // this check is enough to ensure that we are accessing a valid struct
{
/** Here, we can apply the properties that are exposed by FWidgetTemplate. I dont have any in my example for the sake of simplicity.
This will be overriden in e.g. UTextBlock in order to apply properties that belong to UTextBlock. See below for an example.
*/
}
}
}
void UWidget::GetTemplate()
{
/** Here, we can get and set the WidgetTemplate. This will be overriden by childs like UTextBlock in order to fetch the template from the right TMap Property.
*/
auto themePtr = UGameSettings::GetTheme();
if (IsValid(themePtr))
{
Theme = themePtr.LoadSynchronous();
if (IsValid(theme))
{
// I am commenting out the next line because I dont include a WidgetTemplates property in the UMGThemeContainer class (in my example).
// WidgetTemplate = theme->WidgetTemplates.Find(Template);// This returns either nullptr or a pointer to a valid struct
}
}
}
#endif
[/SPOILER]
I am omitting the code that I would add in TextLayoutWidget.cpp and TextLayoutWidget.h because we will do exactly the same thing in TextBlock.h and TextBlock.cpp anyway. All other classes that derive from UWidget only need to override the GetTemplate() method and provide an extension of the implementation of the ApplyTemplate() method.
Inside TextBlock.h
[SPOILER]
UCLASS(meta=(DisplayName="Text"))
class UMG_API UTextBlock : public UTextLayoutWidget
{
/** I am omitting the current implementation of this class, you can add the following stuff simply at the end of the class definition */
#if WITH_EDITOR
public:
UPROPERTY(EditAnywhere, Category = "Behavior", meta = (InlineEditConditionToggle))
uint32 bApply_Template : 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Candy", meta = (editcondition = "bApply_Template"))
FString Template;
struct FWidgetTemplate* WidgetTemplate; // will be initialized to null
AssetPtr<UMGThemeContainer> Theme;
virtual void ApplyTemplate() override;
virtual void GetTemplate() override;
#endif
};
[/SPOILER]
Inside TextBlock.cpp
[SPOILER]
#if WITH_EDITOR
void UWidget::ApplyTemplate()
{
/** I am calling the parent implementation of ApplyTemplate and then apply the template properties that begong to UTextBlock.
I dont call GetTemplate() because this will be done in UWidget::ApplyTemplate()
*/
Super::ApplyTemplate()
if(bApply_Template)
{
if(WidgetTemplate)
{
auto Template = (FTextBlockTemplate*) WidgetTemplate; // This cast is safe, because we override GetTemplate()
if(Template->bOverride_ColorName)
{
// retrieve the colour from the theme the same way we retrieved the template in GetTemplate(), you can use the Theme property of UWidget for this.
}
else if (Template->bOverride_ColorAndOpacity) // note the else if. This will only be applied if the color name property isnt supposed to be used. This is just taste though.
{
this->ColorAndOpacity = Template->ColorAndOpacity;
}
if (Template->bOverride_Font)
{
this->Font = Template->Font;
}
}
}
}
void UWidget::GetTemplate()
{
/** Here, I will override the parent method without calling it, because we want to access the TextBlockTemplates property of UMGThemeContainer this time.
*/
auto themePtr = UGameSettings::GetTheme();
if (IsValid(themePtr))
{
Theme = themePtr.LoadSynchronous();
if (IsValid(theme))
{
WidgetTemplate = theme->TextBlockTemplates.Find(Template); // This returns either nullptr or a pointer to a valid struct
}
}
}
#endif
[/SPOILER]
And this is the UMGThemeContainer class. There is no content in it’s .cpp file.
UMGThemeContainer.h
[SPOILER]
UCLASS()
class MYPROJECT_API UUMGThemeContainer : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Theme")
TMap<FString, struct FSlateColor> Colours;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Theme")
TMap<FString, struct FTextBlockTemplate> TextBlockTemplates;
};
[/SPOILER]
And these are the Template structs, that are being used. We need them to store the styles as templates. I omitted most of the properties for simplicity. But basically, all I did was to copy and paste the properties from their respective lasses and made them toggable…
[SPOILER]
USTRUCT(BlueprintType)
struct FWidgetTemplate
{
GENERATED_BODY()
public:
/** Nothing here because I omitted it */
};
USTRUCT(BlueprintType)
struct FTextLayoutWidgetTemplate : public FWidgetTemplate
{
GENERATED_BODY()
public:
// This is the only property that I have included in this example. This would be applied in the ApplyTemplate() of UTextLayoutWidget.
/** */
UPROPERTY(EditAnywhere, Category = "Behavior", meta = (InlineEditConditionToggle))
uint32 bOverride_Margin : 1;
/** The amount of blank space left around the edges of text area. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Appearance", AdvancedDisplay, meta = (editcondition = "bOverride_Margin"))
FMargin Margin;
};
USTRUCT(BlueprintType)
struct FTextBlockTemplate : public FTextLayoutWidgetTemplate
{
GENERATED_BODY()
public:
// These are the only properties that I have included in this example. This is applied in the ApplyTemplate() of UTextBlock.
/** */
UPROPERTY(EditAnywhere, Category = "Behavior", meta = (InlineEditConditionToggle))
uint32 bOverride_ColorAndOpacity : 1;
/** The color of the text */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Appearance", meta = (editcondition = "bOverride_ColorAndOpacity"))
FSlateColor ColorAndOpacity;
UPROPERTY(EditAnywhere, Category = "Behavior", meta = (InlineEditConditionToggle))
uint32 bOverride_ColorName : 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Appearance", meta = (editcondition = "bOverride_ColorName"))
FString ColorName;
/** */
UPROPERTY(EditAnywhere, Category = "Behavior", meta = (InlineEditConditionToggle))
uint32 bOverride_Font : 1;
/** The font to render the text with */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Appearance", meta = (editcondition = "bOverride_Font"))
FSlateFontInfo Font;
};
[/SPOILER]
MY COMMENT ON THE IMPLEMENTATION
I left out many properties of UWidget, UTextLayoutWidget and UTextBlock in my Template structs. Those can be added as one sees fit. I am currently exposing all properties just because… I have also omitted the way we retrieve the theme asset and how we set the engine up to allow us to select the theme inside the project settings window. I am going a different way to retrieve my theme because I implemented this logic in a plugin.
Also, since I cant edit the engine code, I need to create a new widget for each widget that is exposed to the UMG Palette. And I need to implement all this logic in a redundant manner for the same reason… That is why it makes much more sense to integrate all this stuff into the engine code rather than in a plugin.
POINTS TO IMPROVE
- Well, for starters, we would need to implement the template structs for all the widget classes. With inheritance, we can avoid redundant code and also make use of polymorphism. This is basically just copy and paste…
- Also, I noticed that the way we edit some widget properties in the blueprint window is different than the way we edit properties from the UMG editor. It would be sweet if we could edit the templates the same way as the properties in the UMG editor…
- We could also split up the theme container, so that every widget has its own widget theme DataAsset class. The theme container asset would then only point to the widget theme assets, that are to be used.
- Also, I think it would be smart to add TMaps for paddings/margins and more stuff.
- For the Template Property of UWidget, one could offer dropdown menus instead of a textbox. The dropdown menu would then list all the possible template names. The code above doesnt crash for invalid template names, but it would still be handy.
- In the implementation above, the template is applied only OnLoad() of a widget and and when the template property has changed. This means that if I have a widget blueprint asset opened and change a template, I would either need to close the asset and open it again or change and revert the template property. One could also track changes for the theme container asset and update all currently instantiated widgets when hitting a certain button. I have something similar already implemented, but didnt include it.
- I made all those additions to the classes editor only, but there is actually no need to. By removing the “WITH_EDITOR” conditions, one could setup and apply templates at runtime as well. Though for the runtime, one should move the function call of ApplyTemplate() to the constructor of the widget due to performance reasons.
CONCLUSION
Well, thats it. What do you guys think? Implementing all this should be feasible in one, max two work days.