本文提供一种使用UE的原生方式创建常用图表控件之一——曲线图。
实现效果如下:
实现思路:
1、圆滑曲线的可以依靠FRichCurve来对关键点进行定位后进行曲线模拟
2、坐标轴的曲线点与绘图的屏幕坐标点的转换
3、使用FSlateDrawElement::MakeLines进行画线
实现方式:
第一步:新建一个Class类,继承自UUserWidget类(依赖模块需要添加SlateCore和UMG)
第二步:重载NativePaint方法,实现曲线图绘制
第三步:重载NativeTick方法,当重载数据后添加曲线绘制的动画实现
第四步:重载NativeOnMouseMove方法,判断当前鼠标位置,控制是否显示曲线点的值标签
源码如下:
SmoothedLineWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ChartData.h"
#include "SmoothedLineWidget.generated.h"
/**
*
*/
UCLASS()
class UICHARTS2D_API USmoothedLineWidget : public UUserWidget
{
GENERATED_BODY()
public:
USmoothedLineWidget(const FObjectInitializer& ObjectInitializer);
UFUNCTION(BlueprintCallable)
void AddCategoryValues(const FString& CateName,const TArray<float>& InValues);//计算对应的key值,默认在绘制前已调用
UFUNCTION(BlueprintCallable)
void ClearCateries();
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector2D Size = FVector2D(300, 250);//大小
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector2D LocationOffset = FVector2D(10, 10);//位置
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float BrushSize = 2;//笔刷大小
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FColor> Colors;//颜色
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FSlateFontInfo Font;
UPROPERTY(BlueprintReadWrite)
float Min = 0;
UPROPERTY(BlueprintReadWrite)
float Max = 1;
UPROPERTY(BlueprintReadWrite)
bool DrawPoint=false;
protected:
virtual int32 NativePaint(
const FPaintArgs& Args,
const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements,
int32 LayerId,
const FWidgetStyle& InWidgetStyle,
bool bParentEnabled) const override;//绘制函数
virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
void GeneratePoints();
FColor GetColorByIndex(int32 Index) const;
int32 WhichIdx = -1;
private:
float BarItemSpace = 1.0f;
float CurrentValue = 0.0f;
TArray<FChartCategoryData> CategoryArray;
void DrawSmoothedLine(
FSlateWindowElementList& OutDrawElement,
int InLayerId,
const FGeometry& InAllottedGeometry,
FChartCategoryData InData,
float InThickness,
FColor InColor
)const;//线的绘制
};
SmoothedLineWidget.cpp
#include "SmoothedLineWidget.h"
#include "Components/CanvasPanelSlot.h"
USmoothedLineWidget::USmoothedLineWidget(const FObjectInitializer& ObjectInitializer)
:Super(ObjectInitializer)
{
}
void USmoothedLineWidget::ClearCateries()
{
CategoryArray.Empty();
Min = 0;
Max = 1;
}
FColor USmoothedLineWidget::GetColorByIndex(int32 Index) const
{
if (Colors.Num() > 0)
{
return Colors[Index % Colors.Num()];
}
return FColor::White;
}
void USmoothedLineWidget::AddCategoryValues(const FString& CateName, const TArray<float>& InValues)
{
if (InValues.Num() < 2)
return;
FChartCategoryData CategoryData = FChartCategoryData();
CategoryData.CategoryName = CateName;
CategoryData.Color = GetColorByIndex(CategoryArray.Num());
for (int32 Index = 0; Index < InValues.Num(); Index++)
{
float value = InValues[Index];
Min = FMath::Min(Min, value);
Max = FMath::Max(Max, value);
FChartCategoryItem Item = FChartCategoryItem();
Item.Value = value;
CategoryData.Data.Add(Item);
CategoryData.Total += value;
}
CategoryArray.Add(CategoryData);
Min = FMath::FloorToInt32(Min);
Max = FMath::RoundToInt32(Max);
BarItemSpace = Size.X / InValues.Num();
GeneratePoints();
}
void USmoothedLineWidget::GeneratePoints()
{
float WidgetWidth = Size.X;
float WidgetHeight = Size.Y;
float Range = Max - Min;
for (int idx = 0; idx < CategoryArray.Num(); idx++)
{
auto InValues = CategoryArray[idx];
float Space = WidgetWidth / (InValues.Data.Num() - 1);
for (int32 Index = 0; Index < InValues.Data.Num(); Index++)
{
float value = InValues.Data[Index].Value;
float ScaleValue = WidgetHeight * (value / Range);
FVector2D KeyPosition(Space * Index, WidgetHeight - ScaleValue);
InValues.Data[Index].PositionX = KeyPosition.X;
InValues.Data[Index].PositionY = KeyPosition.Y;
}
CategoryArray[idx] = MoveTemp(InValues);
}
}
int32 USmoothedLineWidget::NativePaint(const FPaintArgs& Args,
const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements,
int32 LayerId,
const FWidgetStyle& InWidgetStyle,
bool bParentEnabled) const
{
TArray<FVector2D> XYLines;
XYLines.Add(FVector2D(LocationOffset.X, Size.Y + LocationOffset.Y));
XYLines.Add(FVector2D(Size.X + LocationOffset.X, Size.Y + LocationOffset.Y));
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
XYLines,
ESlateDrawEffect::None,
FColor::FromHex(TEXT("FFFFFF33")),
true,
1
);
for (int32 idx = 0; idx < CategoryArray.Num(); idx++)
{
auto Category = CategoryArray[idx];
DrawSmoothedLine(
OutDrawElements,
LayerId,
AllottedGeometry,
Category,
BrushSize,
CategoryArray[idx].Color);
}
return LayerId++;
}
void USmoothedLineWidget::DrawSmoothedLine(
FSlateWindowElementList& OutDrawElement,
int InLayerId,
const FGeometry& InAllottedGeometry,
FChartCategoryData InData,
float InThickness,
FColor InColor) const
{
if (InData.Data.Num() < 2)
return;
TArray<FVector2D> InPoints;
for (int32 i = 0; i < InData.Data.Num(); i++)
{
auto item = InData.Data[i];
InPoints.Add(FVector2D(item.PositionX, item.PositionY));
}
FRichCurve* RichCurve = new FRichCurve();
for (FVector2D InPoint : InPoints)
{
FKeyHandle KeyHandle = RichCurve->AddKey(InPoint.X, InPoint.Y);
RichCurve->SetKeyInterpMode(KeyHandle, ERichCurveInterpMode::RCIM_None);
}
TArray<FVector2D> ResultPoints;
float WidgetWidth = Size.X;
float WidgetHeight = Size.Y;
int32 Begin = 0;
int32 End = WidgetWidth;
for (int32 X = Begin; X <= End && X <= CurrentValue; X++)
{
float Y = RichCurve->Eval(X);
FVector2D ResultPoint(X + LocationOffset.X, Y + LocationOffset.Y);
ResultPoints.Add(ResultPoint);
}
delete RichCurve;
FSlateDrawElement::MakeLines(
OutDrawElement,
InLayerId,
InAllottedGeometry.ToPaintGeometry(),
ResultPoints,
ESlateDrawEffect::None,
InColor,
true,
InThickness
);
if (FMath::IsNearlyEqual(CurrentValue, WidgetWidth))
{
FSlateBrush* Brush = new FSlateBrush();
Brush->ImageSize = FVector2D(6, 6);
Brush->DrawAs = ESlateBrushDrawType::Image;
Brush->TintColor = FLinearColor(1.0f, 1.0f, 1.0f, 1.0f);
for (int32 Index = 0; Index < InData.Data.Num(); Index++)
{
auto Item = InData.Data[Index];
auto Point = FVector2D(Item.PositionX, Item.PositionY);
if (DrawPoint)
{
FVector2D Position = FVector2D(Point.X + LocationOffset.X - 3, Point.Y + LocationOffset.Y - 3);
auto Geometry = InAllottedGeometry.MakeChild(FVector2D(6, 6), FSlateLayoutTransform(Position));
FSlateDrawElement::MakeBox(OutDrawElement, InLayerId, Geometry.ToPaintGeometry(), Brush, ESlateDrawEffect::None, FLinearColor(InData.Color));
}
if (Index == WhichIdx)
{
FVector2D TextPosition = FVector2D(Point.X + LocationOffset.X - 5, Point.Y + LocationOffset.Y - 15);
auto TextGeometry = InAllottedGeometry.MakeChild(FVector2D(60, 12), FSlateLayoutTransform(TextPosition));
FSlateDrawElement::MakeText(OutDrawElement, InLayerId, TextGeometry.ToPaintGeometry(), FText::FromString(FString::Printf(TEXT("%d"), FMath::CeilToInt32(Item.Value))), Font);
}
}
delete Brush;
}
}
void USmoothedLineWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
float Delta = InDeltaTime * 1000;
if (CurrentValue < Size.X)
{
CurrentValue += Delta;
}
else
{
CurrentValue = Size.X;
}
Super::NativeTick(MyGeometry, InDeltaTime);
}
FReply USmoothedLineWidget::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 + Size.Y))
{
WhichIdx = -1;
}
else
{
WhichIdx = FMath::TruncToInt32((HoverPostion.X - LocationOffset.X) / BarItemSpace);
}
return Super::NativeOnMouseMove(InGeometry, InMouseEvent);
}