UMG, Custom Widget Components And Render Code, Usable In UMG Designer

Overview Original Author: () In this wiki I provide you with the full code that I used to create a custom Designer-positionable and editable UMG component! This is a UMG widget/component with compl...

Updated over 4 years ago Edit Page Revisions

Overview

File:RamaUMGCustomComponents.jpg

Original Author: ()

In this wiki I provide you with the full code that I used to create a custom Designer-positionable and editable UMG component!

This is a UMG widget/component with completely custom render code that works with UMG's render transform system!

Video of Custom UMG Component, Editable within Designer

Custom UMG Component Property: Thickness

In my code I am not only sharing with you how to do custom render code, I am also showing you how to make your own custom properties that will update in the UMG Designer!

As you see in the video, my custom property Thickness, which directly affects the rendering, does update instantly every time I enter new values within the UMG Designer!

Build CS

Make sure you have this in your project's build cs to ensure my code below will compile!

 PublicDependencyModuleNames.AddRange(new string[] { 
   "Core", "CoreUObject", "Engine", "InputCore",
   "UMG", "Slate", "SlateCore",
 });

C++ Code for You

You are welcome to use my C++ code as a template for your own custom UMG components, just please credit me as the original author/contributor of the code that you use! Mentioning me as "Rama" is sufficient.

Note that there are two new classes!

There is the Slate C++ Core widget, which has the custom render code and the custom property, thickness, for use with the render code.

This Slate widget extends SImage to minimize introduction of duplicate/unnecessary code while also allowing me to inherit any new features that Epic adds to the SImage in the future!

Then there is also a UMG Component which wraps my custom Slate widget!

The core additional code to this UMG component class is my code to enable the Thickness property to trigger dynamic updates of the visual appearance of the custom component while in the Designer! These dynamic updates are accomplished using SynchronizeProperties().

JoySoftEdgeImage.h

/*
    JoySoftEdgeImage by Rama
*/
#pragma once

//~~~~~~~~~~~~ UMG ~~~~~~~~~~~~~~~~
#include "Runtime/UMG/Public/UMG.h"
#include "Runtime/UMG/Public/UMGStyle.h"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

//Custom Slate Element
#include "SSoftEdgeImage.h"

#include "JoySoftEdgeImage.generated.h"
 
UCLASS()
class UJoySoftEdgeImage : public UWidget
{
    GENERATED_UCLASS_BODY()

//Custom Slate Element 
protected:
    TSharedPtr MyImage;
    
//~~~~~~~~~~
//Thickness
//  Rama's custom UMG Widget property!
//~~~~~~~~~~
public:
    /** Thickness */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Joy Soft Edge Image")
    float Thickness;
    
        //Delegate verison if you want it!
    //      -Rama
    //PROPERTY_BINDING_IMPLEMENTATION(float, Thickness);
    
    /** Update thickness of Soft Edge Image! Yay! */
    UFUNCTION(BlueprintCallable, Category="Joy Soft Edge Image")
    void SetThickness(float InThickness);
 
//~~~ Rest is copied from UMG Image.h ~~~
public:

    /** Image to draw */
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Appearance)
    FSlateBrush Brush;

    /** A bindable delegate for the Image. */
    UPROPERTY()
    FGetSlateBrush BrushDelegate;

    /** Color and opacity */
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Appearance)
    FLinearColor ColorAndOpacity;
    
    /** A bindable delegate for the ColorAndOpacity. */
    UPROPERTY()
    FGetLinearColor ColorAndOpacityDelegate;
    
public:

    UPROPERTY(EditDefaultsOnly, Category=Events)
    FOnPointerEvent OnMouseButtonDownEvent;

public:

    /**  */
    UFUNCTION(BlueprintCallable, Category="Appearance")
    void SetColorAndOpacity(FLinearColor InColorAndOpacity);

    /**  */
    UFUNCTION(BlueprintCallable, Category="Appearance")
    void SetOpacity(float InOpacity);

    /**  */
    UFUNCTION(BlueprintCallable, Category="Appearance")
    void SetBrushFromAsset(USlateBrushAsset* Asset);

    /**  */
    UFUNCTION(BlueprintCallable, Category="Appearance")
    void SetBrushFromTexture(UTexture2D* Texture);

    /**  */
    UFUNCTION(BlueprintCallable, Category="Appearance")
    void SetBrushFromMaterial(UMaterialInterface* Material);

    /**  */
    UFUNCTION(BlueprintCallable, Category="Appearance")
    UMaterialInstanceDynamic* GetDynamicMaterial();

    // UWidget interface
    virtual void SynchronizeProperties() override;
    // End of UWidget interface

    // UVisual interface
    virtual void ReleaseSlateResources(bool bReleaseChildren) override;
    // End of UVisual interface

#if WITH_EDITOR
    // UWidget interface
    virtual const FSlateBrush* GetEditorIcon() override;
    virtual const FText GetPaletteCategory() override;
    // End UWidget interface
#endif

protected:
    // UWidget interface
    virtual TSharedRef RebuildWidget() override;
    // End of UWidget interface

    /** Translates the bound brush data and assigns it to the cached brush used by this widget. */
    const FSlateBrush* ConvertImage(TAttribute InImageAsset) const;

    FReply HandleMouseButtonDown(const FGeometry& Geometry, const FPointerEvent& MouseEvent);
};

JoySoftEdgeImage.cpp

/*
    UJoySoftEdgeImage by Rama
*/
 
//Project Name
#include "Abatron.h"
#include "JoySoftEdgeImage.h"

//LOCTEXT
#define LOCTEXT_NAMESPACE "UMG"
 
/////////////////////////////////////////////////////
// UJoySoftEdgeImage

UJoySoftEdgeImage::UJoySoftEdgeImage(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
    , ColorAndOpacity(FLinearColor(0,0,1,0.0333))
    , Thickness(24)
{  
    //Default Values Set Here, see above
}

//Rebuild using custom Slate Widget 
//      -Rama
TSharedRef UJoySoftEdgeImage::RebuildWidget()
{
    MyImage = SNew(SSoftEdgeImage);
    return MyImage.ToSharedRef();
}

//Set Thickness
void UJoySoftEdgeImage::SetThickness(float InThickness)
{
    Thickness = InThickness;
    if ( MyImage.IsValid() )
    {
        MyImage->SetThickness(InThickness);
    }
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//This is where the SSoftEdgeImage slate widget gets updated 
//      when the UPROPERTY() is changed in the Editor
//      -Rama
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void UJoySoftEdgeImage::SynchronizeProperties()
{
    Super::SynchronizeProperties();

        //Thickness Delegate Version
    //TAttribute ThickBind = PROPERTY_BINDING(float, Thickness);
    //MyImage->SetThickness(ThickBind.Get());   
    
    //Thickness non-delegate version
    MyImage->SetThickness(Thickness);
    
    //Color and Opacity
    TAttribute ColorAndOpacityBinding = OPTIONAL_BINDING(FSlateColor, ColorAndOpacity);
    MyImage->SetColorAndOpacity(ColorAndOpacityBinding);
    
    //Image
    TAttribute ImageBinding = OPTIONAL_BINDING_CONVERT(FSlateBrush, Brush, const FSlateBrush*, ConvertImage);
    MyImage->SetImage(ImageBinding);
    
    //Mouse
    MyImage->SetOnMouseButtonDown(BIND_UOBJECT_DELEGATE(FPointerEventHandler, HandleMouseButtonDown));
}
 

//~~~ Rest is copied from UMG Image.h ~~~

void UJoySoftEdgeImage::ReleaseSlateResources(bool bReleaseChildren)
{
    Super::ReleaseSlateResources(bReleaseChildren);

    MyImage.Reset();
}

void UJoySoftEdgeImage::SetColorAndOpacity(FLinearColor InColorAndOpacity)
{
    ColorAndOpacity = InColorAndOpacity;
    if ( MyImage.IsValid() )
    {
        MyImage->SetColorAndOpacity(ColorAndOpacity);
    }
}

void UJoySoftEdgeImage::SetOpacity(float InOpacity)
{
    ColorAndOpacity.A = InOpacity;
    if ( MyImage.IsValid() )
    {
        MyImage->SetColorAndOpacity(ColorAndOpacity);
    }
}

const FSlateBrush* UJoySoftEdgeImage::ConvertImage(TAttribute InImageAsset) const
{
    UJoySoftEdgeImage* MutableThis = const_cast( this );
    MutableThis->Brush = InImageAsset.Get();

    return &Brush;
}

void UJoySoftEdgeImage::SetBrushFromAsset(USlateBrushAsset* Asset)
{
    Brush = Asset ? Asset->Brush : FSlateBrush();

    if ( MyImage.IsValid() )
    {
        MyImage->SetImage(&Brush);
    }
}

void UJoySoftEdgeImage::SetBrushFromTexture(UTexture2D* Texture)
{
    Brush.SetResourceObject(Texture);

    if ( MyImage.IsValid() )
    {
        MyImage->SetImage(&Brush);
    }
}

void UJoySoftEdgeImage::SetBrushFromMaterial(UMaterialInterface* Material)
{
    Brush.SetResourceObject(Material);

    //TODO UMG Check if the material can be used with the UI

    if ( MyImage.IsValid() )
    {
        MyImage->SetImage(&Brush);
    }
}

UMaterialInstanceDynamic* UJoySoftEdgeImage::GetDynamicMaterial()
{
    UMaterialInterface* Material = NULL;

    UObject* Resource = Brush.GetResourceObject();
    Material = Cast(Resource);

    if ( Material )
    {
        UMaterialInstanceDynamic* DynamicMaterial = Cast(Material);

        if ( !DynamicMaterial )
        {
            DynamicMaterial = UMaterialInstanceDynamic::Create(Material, this);
            Brush.SetResourceObject(DynamicMaterial);

            if ( MyImage.IsValid() )
            {
                MyImage->SetImage(&Brush);
            }
        }
        
        return DynamicMaterial;
    }

    //TODO UMG can we do something for textures?  General purpose dynamic material for them?

    return NULL;
}

FReply UJoySoftEdgeImage::HandleMouseButtonDown(const FGeometry& Geometry, const FPointerEvent& MouseEvent)
{
    if ( OnMouseButtonDownEvent.IsBound() )
    {
        return OnMouseButtonDownEvent.Execute(Geometry, MouseEvent).NativeReply;
    }

    return FReply::Unhandled();
}

#if WITH_EDITOR

const FSlateBrush* UJoySoftEdgeImage::GetEditorIcon()
{
    return FUMGStyle::Get().GetBrush("Widget.Image");
}
 
const FText UJoySoftEdgeImage::GetPaletteCategory()
{
    return LOCTEXT("Common", "Common");
}

#endif

/////////////////////////////////////////////////////

#undef LOCTEXT_NAMESPACE

.h

/*
    By Rama
*/

#pragma once

//Slate Core
#include "SlateCore.h"

class SSoftEdgeImage
    : public SImage
{
public:
    SLATE_BEGIN_ARGS( SSoftEdgeImage )
        : _Image( FCoreStyle::Get().GetDefaultBrush() )
        , _ColorAndOpacity( FLinearColor(0,0,1,0.02333) )
        , _Thickness(24)
        , _OnMouseButtonDown()
        {}

        /** Image resource */
        SLATE_ATTRIBUTE( const FSlateBrush*, Image )

        /** Color and opacity */
        SLATE_ATTRIBUTE( FSlateColor, ColorAndOpacity )

        /** Thickness */
        SLATE_ATTRIBUTE( float, Thickness )
        
        /** Invoked when the mouse is pressed in the widget. */
        SLATE_EVENT( FPointerEventHandler, OnMouseButtonDown )

    SLATE_END_ARGS()

    /**
     * Construct this widget
     *
     * @param   InArgs  The declaration data for this widget
     */
    void Construct( const FArguments& InArgs );

//~~~~~~~~
//Thickness
//~~~~~~~~
public:
    void SetThickness( float InThickness );
    
    /** How many times the image is repeated to give a softness to the edge */
    float Thickness;
    
public:

    // SWidget overrides

    virtual int32 OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const override;

};

.cpp

/*
    By Rama
*/

#include "Abatron.h"
#include "SSoftEdgeImage.h"

DECLARE_CYCLE_STAT( TEXT("OnPaint SSoftEdgeImage"), STAT_SlateOnPaint_SSoftEdgeImage, STATGROUP_Slate );

/**
 * Construct this widget
 *
 * @param   InArgs  The declaration data for this widget
 */
void SSoftEdgeImage::Construct( const FArguments& InArgs )
{
    Image               = InArgs._Image;
    ColorAndOpacity         = InArgs._ColorAndOpacity;
    Thickness           = InArgs._Thickness.Get();
    OnMouseButtonDownHandler    = InArgs._OnMouseButtonDown;
}
 
void SSoftEdgeImage::SetThickness( float InThickness )
{
    Thickness = InThickness;
}

int32 SSoftEdgeImage::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
#if SLATE_HD_STATS
    SCOPE_CYCLE_COUNTER( STAT_SlateOnPaint_SSoftEdgeImage );
#endif
    const FSlateBrush* ImageBrush = Image.Get();

    if ((ImageBrush != nullptr) && (ImageBrush->DrawAs != ESlateBrushDrawType::NoDrawType))
    {
        const bool bIsEnabled = ShouldBeEnabled(bParentEnabled);
        const uint32 DrawEffects = bIsEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect;

        const FColor FinalColorAndOpacity( InWidgetStyle.GetColorAndOpacityTint() * ColorAndOpacity.Get().GetColor(InWidgetStyle) * ImageBrush->GetTint( InWidgetStyle ) );

        //For Thickness
        for (int32 v = 0; v < Thickness; v++ ) 
        {           
            //Size
            FVector2D AdjustedSize = 
                FVector2D(
                    AllottedGeometry.GetLocalSize().X - v*2,
                    AllottedGeometry.GetLocalSize().Y - v*2
                );
                  
            //There's a warning about using this constructor in Geometry.h
            //      But it looks like the code was fixed in Geometry.cpp to use layout transform properly. Plus all render transforms work great in Editor.
            //          So keeping this for now :)
            FPaintGeometry PaintGeom = 
                AllottedGeometry.ToPaintGeometry(
                    FVector2D(v,v),     //Local Translation
                    AdjustedSize,       //Local Size
                    1           //Local Scale
                );
            
            FSlateDrawElement::MakeBox(
                OutDrawElements,    //Out
                LayerId, 
                PaintGeom,      //Paint Geom
                ImageBrush,         //Brush
                MyClippingRect,         //Clip
                DrawEffects, 
                FinalColorAndOpacity    //Color and Opacity
            );

        } //For loop
    }
    return LayerId;
}

Conclusion

In this wiki I have now given you the entire code that I used to create the custom UMG widget with custom render code that you saw in my video!

Have fun making your own UMG Widgets for use with the UMG Designer!

ÔÖÑ

()