Using the same callback function for many UButtons

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!

Posted: