Quick tutorial for beginners how to draw lines in user widget using native paint function in C++.
Let’s start with creating NativePaint:
.h filepublic: virtual int32 NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
.cpp fileint32 UMyUserWidget::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const { FPaintContext Context = FPaintContext(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); // this seems to work too // FPaintContext Context(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); //... add loop here // and // UWidgetBlueprintLibrary::DrawLine(Context, PointA, PointB, Tint, true, Thickness); Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); return LayerId; }
This is how it looks like in UserWidget.cpp:

Extras
C++ loops are so much faster then Blueprint loops, they can run between milliseconds & picoseconds if they are done right, so it’s worth doing entire widget in C++.
To go even further on optimization create a structure array containing Point A & Point B’s and fill the array in async task.
Structure:
.h file - put this above UCLASS()USTRUCT(BlueprintType) struct FExampleLineDraw{ GENERATED_BODY() public: FExampleLineDraw() : PointA(FVector2D()), PointB(FVector2D() {} UPROPERTY(BlueprintReadWrite) FVector2D PointA UPROPERTY(BlueprintReadWrite) FVector2D PointB };
And then:
.h filepublic: UPROPERTY(BlueprintReadOnly) TArray<FExampleLineDraw> MyArray;
And then the async and all other stuff:
Boilerplate for AsyncTask.
AsyncTask(ENamedThreads::AnyHiPriThreadNormalTask, []()
{
//...
// if you use [this] you don't need this:
AsyncTask(ENamedThreads::GameThread, []()
{
//...
});
});
void UMyPlayerWidget::AsyncWorker()
{
UWorld* world = GetWorld();
if (!::IsValid(world)) return;
AsyncAvailible = false;
ScannerTimer = 0.0f;
TArray<AActor*> FoundActors;
// #include "Kismet/GameplayStatics.h"
// this only works in gamethread
UGameplayStatics::GetAllActorsOfClass(world, AMyActor::StaticClass(), FoundActors);
AsyncTask(ENamedThreads::AnyHiPriThreadNormalTask, [this, FoundActors]()
{
TArray<FExampleLineDraw> tmparray;
APlayerController* PlayerController = this->GetOwningPlayer();
FExampleLineDraw tmpstruct;
for(const AActor* Item : FoundActors)
{
// do stuff here
// #include "Blueprint/WidgetLayoutLibrary.h"
UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition(PlayerController, Item->GetActorLocation(), tmpstruct.PointA, true);
UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition(PlayerController, Item->Mesh->GetCenterOfMass(), tmpstruct.PointB, true);
tmparray.Add(tmpstruct)
}
AsyncTask(ENamedThreads::GameThread, [this, tmparray]()
{
this->MyArray = tmparray;
this->AsyncAvailible = true;
});
});
}
void UMyPlayerWidget::Scan()
{
if (!AsyncAvailible) return;
AsyncWorker();
}
// .h file: virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
void UMyPlayerWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
Scan();
if(AsyncAvailible) ScannerTimer += InDeltaTime; // measure process time
}
And then go to native paint:
for(const FExampleLineDraw Item : MyArray)
{
UWidgetBlueprintLibrary::DrawLine(Context, Item.PointA, Item.PointB, FLinearColor::White, true, 0.5f);
}
More about multi-threading: