使用UE5原生方式实现常用图表控件之一——柱状图,可用于蓝图调用。
实现效果如下:
实现思路:
1、使用使用四个顶点组成2个三角形填充区域进行柱状绘制
2、根据X轴的类别个数分别计算出每个类别的宽度及每个柱状允许的宽度并取合适值
3、Y轴坐标与屏幕坐标值的转换
源码:
BarChartWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "BarChartWidget.generated.h"
/**
*
*/
UCLASS()
class UICHARTS2D_API UBarChartWidget : public UUserWidget
{
GENERATED_BODY()
protected:
virtual int32 NativePaint(const FPaintArgs& Args, const FGeometry& Geometry, const FSlateRect& Rect, FSlateWindowElementList& DrawElements,
int32 LayerId, const FWidgetStyle& Style, bool ParentEnable) const override;
virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
UPROPERTY(BlueprintReadOnly)
float Min = 0;
UPROPERTY(BlueprintReadOnly)
float Max = 1;
/*每条Bar的宽度*/
float BarWidth = 2;
/*每个Label的Bar宽度*/
float BarItemSpace = 2;
TArray<TArray<float>> ValuesArray;
float CurrentValue = 0.0f;
int32 WhichIdx = -1;
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FSlateFontInfo Font;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector2D AreaSize = FVector2D(240, 200);
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector2D LocationOffset = FVector2D(40, 30);
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FString> CategoryNames;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FString> LabelNames;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FColor> Colors;
UFUNCTION(BlueprintCallable)
virtual void ClearCategories();
UFUNCTION(BlueprintCallable)
virtual void ConfigXAxisLabels(const TArray<FString>& InLabels);
UFUNCTION(BlueprintCallable)
virtual void ConfigCategoryValues(const FString& InCategoryName, const TArray<float>& InValues);
};
BarChartWidget.cpp
#include "BarChartWidget.h"
int32 UBarChartWidget::NativePaint(const FPaintArgs& Args, const FGeometry& Geometry, const FSlateRect& Rect, FSlateWindowElementList& DrawElements, int32 LayerId, const FWidgetStyle& Style, bool ParentEnable) const
{
for (int idx = 0; idx < ValuesArray.Num(); idx++)
{
TArray<FSlateVertex> VertexArray;
TArray<SlateIndex> VertexIndex;
FSlateVertex LeftBottomVertex;
FSlateVertex LeftTopVertex;
FSlateVertex RightBottomVertex;
FSlateVertex RightTopVertex;
FVector2D LeftBottomPosition;
FVector2D LeftTopPosition;
FVector2D RightBottomPosition;
FVector2D RightTopPosition;
FColor color = Colors.IsValidIndex(idx) ? Colors[idx] : FColor::White;
LeftBottomVertex.Color = color;
LeftTopVertex.Color = color;
RightBottomVertex.Color = color;
RightTopVertex.Color = color;
int32 Start = 0;
int32 End = AreaSize.X;
int32 Width = FMath::RoundToInt32(BarWidth);
float Range = Max - Min;
TArray<FVector3d> TextPosition;
for (int32 X = 0; X < ValuesArray[idx].Num(); X++)
{
bool WriteText = true;
float value = ValuesArray[idx][X];
if (value > CurrentValue && CurrentValue < Max)
{
value = CurrentValue;
WriteText = false;
}
if (value < Min)
{
value = Min;
}
float Y = AreaSize.Y * (1.f - value / Range);
if (FMath::IsNearlyEqual(Y, AreaSize.Y))
{
Y = AreaSize.Y - 1;
}
LeftBottomPosition = FVector2D(
LocationOffset.X + Width * idx + BarItemSpace * X + Width / 2,
LocationOffset.Y + AreaSize.Y);
LeftTopPosition = FVector2D(
LocationOffset.X + Width * idx + BarItemSpace * X + Width / 2,
LocationOffset.Y + Y);
RightBottomPosition = FVector2D(
LocationOffset.X + Width * idx + BarItemSpace * X + Width + Width / 2,
LocationOffset.Y + AreaSize.Y);
RightTopPosition = FVector2D(
LocationOffset.X + Width * idx + BarItemSpace * X + Width + Width / 2,
LocationOffset.Y + Y);
if (WriteText && WhichIdx == X)
{
TextPosition.Add(FVector3d(X, LeftTopPosition.X, LeftTopPosition.Y - 12));
}
const FSlateRenderTransform& SlateRenderTransform = Geometry.ToPaintGeometry().GetAccumulatedRenderTransform();
LeftBottomPosition = SlateRenderTransform.TransformPoint(LeftBottomPosition);
LeftTopPosition = SlateRenderTransform.TransformPoint(LeftTopPosition);
RightBottomPosition = SlateRenderTransform.TransformPoint(RightBottomPosition);
RightTopPosition = SlateRenderTransform.TransformPoint(RightTopPosition);
LeftBottomVertex.Position = FVector2f(LeftBottomPosition.X, LeftBottomPosition.Y);
LeftTopVertex.Position = FVector2f(LeftTopPosition.X, LeftTopPosition.Y);
RightBottomVertex.Position = FVector2f(RightBottomPosition.X, RightBottomPosition.Y);
RightTopVertex.Position = FVector2f(RightTopPosition.X, RightTopPosition.Y);
int32 LBVertexIndex = VertexArray.Add(LeftBottomVertex);
int32 LTdVertexIndex = VertexArray.Add(LeftTopVertex);
int32 RBVertexIndex = VertexArray.Add(RightBottomVertex);
int32 RTVertexIndex = VertexArray.Add(RightTopVertex);
VertexIndex.Add(LBVertexIndex);
VertexIndex.Add(LTdVertexIndex);
VertexIndex.Add(RBVertexIndex);
VertexIndex.Add(RBVertexIndex);
VertexIndex.Add(RTVertexIndex);
VertexIndex.Add(LTdVertexIndex);
}
for (FSlateVertex& SlateVertex : VertexArray)
{
SlateVertex.TexCoords[0] = 0.f;
SlateVertex.TexCoords[1] = 0.f;
}
const FSlateBrush* Brush = FCoreStyle::Get().GetBrush(TEXT("PIECHART"));
const FSlateResourceHandle Handle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(*Brush);
FSlateDrawElement::MakeCustomVerts(DrawElements, LayerId, Handle, VertexArray, VertexIndex, nullptr, 0, 0);
TArray<FVector2D> Points;
Points.Add(FVector2D(LocationOffset.X, AreaSize.Y + LocationOffset.Y));
Points.Add(FVector2D(AreaSize.X + LocationOffset.X, AreaSize.Y + LocationOffset.Y));
FSlateDrawElement::MakeLines(DrawElements, LayerId, Geometry.ToPaintGeometry(), Points, ESlateDrawEffect::None,
FColor(255, 255, 255, 33),
true,
1);
for (int Pos = 0; Pos < TextPosition.Num(); Pos++)
{
auto position = TextPosition[Pos];
int32 x = FMath::CeilToInt32(position.X);
FVector2D TxtPos = FVector2D(position.Y, position.Z);
auto TextGeometry = Geometry.MakeChild(FVector2D(60, 12), FSlateLayoutTransform(TxtPos));
FString ValueLabel = FString::Printf(TEXT("%d"), FMath::CeilToInt32(ValuesArray[idx][x]));
FSlateDrawElement::MakeText(DrawElements, LayerId, TextGeometry.ToPaintGeometry(), FText::FromString(ValueLabel), Font);
}
}
return LayerId++;
}
void UBarChartWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
float Delta = InDeltaTime * 1000;
if (CurrentValue <= Max)
{
CurrentValue += Delta;
}
else
{
CurrentValue = Max;
}
Super::NativeTick(MyGeometry, InDeltaTime);
}
FReply UBarChartWidget::NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
auto HoverPostion = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
if (HoverPostion.X < LocationOffset.X || HoverPostion.Y < LocationOffset.Y)
{
WhichIdx = -1;
}
else if (HoverPostion.Y > (LocationOffset.Y + AreaSize.Y))
{
WhichIdx = -1;
}
else
{
WhichIdx = FMath::TruncToInt32((HoverPostion.X - LocationOffset.X) / BarItemSpace);
}
return Super::NativeOnMouseMove(InGeometry, InMouseEvent);
}
void UBarChartWidget::ClearCategories()
{
CategoryNames.Empty();
for (int32 Index = 0; Index < ValuesArray.Num(); Index++)
{
ValuesArray[Index].Empty();
}
ValuesArray.Empty();
BarWidth = 2;
BarItemSpace = 4;
Min = 0;
Max = 1;
}
void UBarChartWidget::ConfigXAxisLabels(const TArray<FString>& InLabels)
{
LabelNames = InLabels;
}
void UBarChartWidget::ConfigCategoryValues(const FString& InCategoryName, const TArray<float>& InValues)
{
CategoryNames.Add(InCategoryName);
ValuesArray.Add(InValues);
for (int32 Index = 0; Index < InValues.Num(); Index++)
{
float value = InValues[Index];
Min = FMath::Min(Min, value);
Max = FMath::Max(Max, value);
}
Min = FMath::FloorToInt32(Min);
Max = FMath::RoundToInt32(Max);
if (LabelNames.Num() > 0)
{
BarItemSpace = AreaSize.X / (LabelNames.Num());
BarWidth = BarItemSpace / (CategoryNames.Num() + 1);
}
CurrentValue = 0.0f;
}