Update: This issue is now resolved with Common UI buttons.
One thing that I often hit my head against when starting C++ and UMG:
I have a bunch of buttons, and I want them to all call the same function when they are clicked. Then from that function, I want to know which button was clicked.
… so how the heck do I do that?
There are a few ways to do this!
Why can't we do this by default?
The standard UMG Button class UButton
provides OnClicked
, a Dynamic
Multicast Delegate that is called when a user clicks on the button.
Standard UButton example
void UBUIUWTestWindow::NativeConstruct()
{
Super::NativeConstruct();
// Imagine we have an array of buttons already populated
for (UButton* Button : LotsOfButtons)
{
// Here we are binding our "OnButtonWasClicked" function to the dynamic
// multicast delegate "OnClicked"
Button->OnClicked.AddUniqueDynamic(this, &ThisClass::OnButtonWasClicked);
}
}
// The function _has_ to be this signature, with no parameters
void UBUIUWTestWindow::OnButtonWasClicked()
{
// Uh oh, which button was clicked?
}
Dynamic Multicast Delegates are great for working with Blueprints, but
unfortunately the one in UButton
doesn't provide us with any arguments. It
also doesn't allow us to use C++ lambdas to add our own arguments. So we can't
tell which button was clicked.
So what can we do instead?
a) Make a Button UserWidget
While this method doesn't technically require us to get our hands dirty in C++, I'm going to show you how to do it in C++ because I like getting dirty.
First we are going to create a new UserWidget subclass that is going to be our
generic wrapper around UButton
. It will conceptually represent a Button with
some styling, and importantly let us make our own dynamic multicast delegate
that provides us a way of knowing which button was clicked.
We do this by defining a new delegate that has our new button class as
a parameter, and using this new delegate in our new UUserWidget
button class.
Note on Naming Convention: I like to preface all my classes with BUI
to make it
clear that I made them. At Brace Yourself Games we use the preface BYG
for
our code.
Then UW
is what I use for UserWidget subclasses.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FBUIOnClickedSignature, class UBUIUWButton*, Button);
BUIUWButton.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "BUIUWButton.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FBUIOnClickedSignature, class UBUIUWButton*, Button);
UCLASS()
class UBUIUWButton : public UUserWidget
{
GENERATED_BODY()
public:
// Bind a function with the signature "void OnClicked(UBUIUWButton* Button);
FBUIOnClickedSignature OnClickedDelegate;
protected:
virtual void NativeConstruct() override;
// In the Blueprint subclass, make sure you create aButton called "MainButton"
UPROPERTY(meta=(BindWidget))
class UButton* MainButton;
UFUNCTION()
void OnButtonClicked();
};
BUIUWButton.cpp
#include "BUIUWButton.h"
#include "Components/Button.h"
void UBUIUWButton::NativeConstruct()
{
Super::NativeConstruct();
MainButton->OnClicked.AddUniqueDynamic(this, &ThisClass::OnButtonClicked);
}
void UBUIUWButton::OnButtonClicked()
{
OnClickedDelegate.Broadcast(this);
}
Then instead of creating an array of UButton
instances, we can create an
array of UBUIUWButton
instances.
Updated example using UUserWidget subclass
void UBUIUWTestWindow::NativeConstruct()
{
Super::NativeConstruct();
for (UBUIUWButton* Button : LotsOfButtons)
{
Button->OnClickedDelegate.AddUniqueDynamic(this, &ThisClass::OnButtonWasClicked);
}
}
// Yay, we now have a parameter
void UBUIUWTestWindow::OnButtonWasClicked(UBUIUWButton* Button)
{
// We know which button was clicked!
}
b) Bind to the Slate Button Widget
As discussed, the UMG UButton
class dynamic multicast delegate does not
provide us with any arguments, and does not allow us to use lambdas.
On the other hand, the Slate SButton
that is contained within UButton
, has
a non-dynamic delegate that does allow us to use lambdas.
Taking our earlier example, here's how we could re-write it to bind to the
SButton
's delegate instead.
Binding to SButton instead of UButton
void UBUIUWTestWindow::NativeConstruct()
{
Super::NativeConstruct();
// Imagine we have an array of buttons already populated
for (int32 i = 0; i < Buttons.Num(); ++i)
{
UButton* Button = Buttons[i];
SButton* ButtonWidget = (SButton*)&(Button->TakeWidget().Get());
ButtonWidget->SetOnClicked(FOnClicked::CreateLambda([this, i]()
{
OnClicked(i);
return FReply::Handled();
}));
}
}
void UBUIUWTestWindow::OnClicked(int32 Index)
{
// Find the button and do whatever we want
// The argument passed in here doesn't have to be an int32
}
c) Use CommonUI Button
CommonUI is a plugin from Epic released with Unreal Engine 4.27 and 5.0. It is still somewhat experimental but has been in use by Epic for a while.
One of the things it adds is a new button class that addresses some of the
issues with UButton
:
- On-Click delegates pass a pointer to the button that clicked them, useful when binding many button instances to the same function.
- Centralized styling using assets.
- Support for a Selected state, useful for making toggle-able buttons.
- Centralized text styling, using the same text style asset as the Common Text widget.
- Tooltip shows even when the button is disabled.
- Minimum desired width/height properties to ensure a standard size for buttons.
So using CommonUI's UCommonButtonBase
can solve our problem:
Using CommonUI's button class
void UBUIUWTestWindow::NativeConstruct()
{
Super::NativeConstruct();
// Now we have an array of UCommonButtonBase*
for (UCommonButtonBase* Button : Buttons)
{
Button->OnClicked.AddUniqueDynamic(this, &ThisClass::OnClicked);
}
}
void UBUIUWTestWindow::OnClicked(UCommonButtonBase* Button)
{
// We now know which button was clicked!
}
d) Write your own Button class
This is the most time-consuming of the solutions but it has its benefits. By
writing your own alternative to SButton
and UButton
, you can add whatever
functionality you like, including changing the signatures of any OnClicked
delegates you might add.
Rather than writing entirely new button classes from scratch, for Industries
of Titan I duplicated SButton
,
UButton
and their slot classes, renamed a stuff till it compiled and then
gradually added the functionality I needed.
Conclusion
Hopefully this should give you at least one solution that works for you!
Thank you to Hash Buoy for asking this question on the benui Discord. If you are interested in joining the discussion, come and say hi!