Unity UI Toolkit カスタムコントロールサンプル
始めに
UI Toolkit でのカスタムコントロールです。
グラフを作りました。
- Unity 6000.3.2f1
内容
MyGraph.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Properties;
using UnityEngine;
using UnityEngine.UIElements;
namespace Assets.MyUI
{
[UxmlElement]
public partial class MyGraph : VisualElement
{
public static readonly string ussClassName = "my-graph";
Color m_LineColor = Color.white;
static readonly CustomStyleProperty<Color> s_LineColor = new("--line-color");
float m_LineWidth = 1f;
static readonly CustomStyleProperty<float> s_LineWidth = new("--line-width");
Color m_BaseLineColor = transparentColor;
static readonly CustomStyleProperty<Color> s_BaseLineColor = new("--base-line-color");
float m_BaseLineWidth = 0f;
static readonly CustomStyleProperty<float> s_BaseLineWidth = new("--base-line-width");
Color m_OverBaseAreaColor = transparentColor;
static readonly CustomStyleProperty<Color> s_OverBaseAreaColor = new("--over-base-area-color");
Color m_UnderBaseAreaColor = transparentColor;
static readonly CustomStyleProperty<Color> s_UnderBaseAreaColor = new("--under-base-area-color");
static readonly Color transparentColor = new(0, 0, 0, 0);
public MyGraph()
{
AddToClassList(ussClassName);
RegisterCallback<CustomStyleResolvedEvent>(evt => CustomStylesResolved(evt));
generateVisualContent += OnGenerateVisualContent;
}
static void CustomStylesResolved(CustomStyleResolvedEvent evt)
{
(evt.currentTarget as MyGraph)?.UpdateCustomStyles();
}
void UpdateCustomStyles()
{
bool repaint = false;
if (customStyle.TryGetValue(s_LineColor, out m_LineColor)) repaint = true;
if (customStyle.TryGetValue(s_LineWidth, out m_LineWidth)) repaint = true;
if (customStyle.TryGetValue(s_BaseLineColor, out m_BaseLineColor)) repaint = true;
if (customStyle.TryGetValue(s_BaseLineWidth, out m_BaseLineWidth)) repaint = true;
if (customStyle.TryGetValue(s_OverBaseAreaColor, out m_OverBaseAreaColor)) repaint = true;
if (customStyle.TryGetValue(s_UnderBaseAreaColor, out m_UnderBaseAreaColor)) repaint = true;
if (repaint) MarkDirtyRepaint();
}
private void OnGenerateVisualContent(MeshGenerationContext context)
{
if (Mathf.Approximately(MinData.X, MaxData.X)) return;
if (Mathf.Approximately(MinData.Y, MaxData.Y)) return;
if (MaxData.X < MinData.X) return;
if (MaxData.Y < MinData.Y) return;
var painter = context.painter2D;
if (0 < Datas.Count)
{
DrawOverBaseArea(painter);
DrawUnderBaseArea(painter);
DrawLine(painter);
}
DrawBaseValueLine(painter);
}
float GetLayoutX(float x) => layout.width * (x - MinData.X) / (MaxData.X - MinData.X);
float GetLayoutY(float y) => layout.height - layout.height * (y - MinData.Y) / (MaxData.Y - MinData.Y);// 上がゼロ
Vector2 GetLayoutPos(float x, float y) => new(GetLayoutX(x), GetLayoutY(y));
Vector2 GetLayoutPos(MyGraphData data) => GetLayoutPos(data.X, data.Y);
bool IsZero(float v) => Mathf.Approximately(v, 0f);
bool IsTransparent(Color color) => IsZero(color.a);
private void DrawOverBaseArea(Painter2D painter)
{
DrawLimitArea(painter, m_OverBaseAreaColor, y => Mathf.Max(y, BaseValue));
}
private void DrawUnderBaseArea(Painter2D painter)
{
DrawLimitArea(painter, m_UnderBaseAreaColor, y => Mathf.Min(y, BaseValue));
}
private void DrawLimitArea(Painter2D painter, Color fillColor, Func<float, float> func)
{
if (IsTransparent(fillColor)) return;
painter.strokeColor = transparentColor;
painter.lineWidth = 0f;
painter.fillColor = fillColor;
bool bFirst = true;
MyGraphData prevData = new();
var baseLine1 = new Vector2(MinData.X, BaseValue);
var baseLine2 = new Vector2(MaxData.X, BaseValue);
foreach (var data in Datas)
{
float y = func(data.Y);
var pos = GetLayoutPos(data.X, y);
if (bFirst)
{
bFirst = false;
painter.BeginPath();
painter.MoveTo(pos);
}
else
{
if (FindIntersection(baseLine1, baseLine2, prevData.ToVector2(), data.ToVector2(), out var result))
{
var pos2 = GetLayoutPos(new MyGraphData(result));
painter.LineTo(pos2);
}
painter.LineTo(pos);
}
prevData = data;
}
if (!bFirst)
{
var pos = GetLayoutPos(MaxData.X, BaseValue);
painter.LineTo(pos);
pos = GetLayoutPos(MinData.X, BaseValue);
painter.LineTo(pos);
painter.Fill();
}
}
private void DrawLine(Painter2D painter)
{
if (IsTransparent(m_LineColor)) return;
if (IsZero(m_LineWidth)) return;
painter.strokeColor = m_LineColor;
painter.lineWidth = m_LineWidth;
bool bFirst = true;
foreach (var data in Datas)
{
var pos = GetLayoutPos(data);
if (bFirst)
{
bFirst = false;
painter.BeginPath();
painter.MoveTo(pos);
}
else
{
painter.LineTo(pos);
}
}
if (!bFirst) painter.Stroke();
}
void DrawBaseValueLine(Painter2D painter)
{
if (IsTransparent(m_BaseLineColor)) return;
if (IsZero(m_BaseLineWidth)) return;
if (BaseValue < MinData.Y) return;
if (MaxData.Y < BaseValue) return;
painter.strokeColor = m_BaseLineColor;
painter.lineWidth = m_BaseLineWidth;
painter.BeginPath();
painter.MoveTo(GetLayoutPos(MinData.X, 0));
painter.LineTo(GetLayoutPos(MaxData.X, 0));
painter.Stroke();
}
[CreateProperty]
[UxmlAttribute]
public Color LineColor
{
get => m_LineColor;
set
{
if (m_LineColor == value) return;
m_LineColor = value;
MarkDirtyRepaint();
}
}
[CreateProperty]
[UxmlAttribute]
public float LineWidth
{
get => m_LineWidth;
set
{
if (Mathf.Approximately(m_LineWidth, value)) return;
m_LineWidth = value;
MarkDirtyRepaint();
}
}
[CreateProperty]
[UxmlAttribute]
public Color BaseLineColor
{
get => m_BaseLineColor;
set
{
if (m_BaseLineColor == value) return;
m_BaseLineColor = value;
MarkDirtyRepaint();
}
}
[CreateProperty]
[UxmlAttribute]
public float BaseLineWidth
{
get => m_BaseLineWidth;
set
{
if (Mathf.Approximately(m_BaseLineWidth, value)) return;
m_BaseLineWidth = value;
MarkDirtyRepaint();
}
}
[CreateProperty]
[UxmlAttribute]
public Color OverBaseAreaColor
{
get => m_OverBaseAreaColor;
set
{
if (m_OverBaseAreaColor == value) return;
m_OverBaseAreaColor = value;
MarkDirtyRepaint();
}
}
[CreateProperty]
[UxmlAttribute]
public Color UnderBaseAreaColor
{
get => m_UnderBaseAreaColor;
set
{
if (m_UnderBaseAreaColor == value) return;
m_UnderBaseAreaColor = value;
MarkDirtyRepaint();
}
}
MyGraphData _minData = new();
[CreateProperty]
[UxmlObjectReference("min-data")]
public MyGraphData MinData
{
get => _minData;
set
{
if (_minData.Equals(value)) return;
_minData = value ?? new();
MarkDirtyRepaint();
}
}
MyGraphData _maxData = new();
[CreateProperty]
[UxmlObjectReference("max-data")]
public MyGraphData MaxData
{
get => _maxData;
set
{
if (_maxData.Equals(value)) return;
_maxData = value ?? new();
MarkDirtyRepaint();
}
}
List<MyGraphData> datas = new();
[CreateProperty]
[UxmlObjectReference("items-data")]
public List<MyGraphData> Datas
{
get => datas;
set
{
if (datas.SequenceEqual(value)) return;
datas = value ?? new();
MarkDirtyRepaint();
}
}
float _baseValue = 0f;
[CreateProperty]
[UxmlAttribute]
public float BaseValue
{
get => _baseValue;
set
{
if (Mathf.Approximately(_baseValue, value)) return;
_baseValue = value;
MarkDirtyRepaint();
}
}
static bool FindIntersection(Vector2 A1, Vector2 A2, Vector2 B1, Vector2 B2, out Vector2 intersectionPoint)
{
intersectionPoint = new Vector2(float.NaN, float.NaN);
Vector2 directionA = A2 - A1;
Vector2 directionB = B2 - B1;
float crossProduct = (directionA.x * directionB.y) - (directionA.y * directionB.x);
if (Mathf.Abs(crossProduct) < 0.0001f)
{
return false;
}
Vector2 toB1 = B1 - A1;
float t = (toB1.x * directionB.y - toB1.y * directionB.x) / crossProduct;
float u = (toB1.x * directionA.y - toB1.y * directionA.x) / crossProduct;
if (t >= 0 && t <= 1 && u >= 0 && u <= 1)
{
intersectionPoint = A1 + directionA * t;
return true;
}
return false;
}
}
[UxmlObject]
public partial class MyGraphData : IEquatable<MyGraphData>
{
public MyGraphData()
{
}
public MyGraphData(float x, float y)
{
X = x;
Y = y;
}
public MyGraphData(Vector2 vector2)
: this(vector2.x, vector2.y)
{
}
[UxmlAttribute]
public float X { get; set; }
[UxmlAttribute]
public float Y { get; set; }
public bool Equals(MyGraphData other)
{
return other is not null && Mathf.Approximately(X, other.X) && Mathf.Approximately(Y, other.Y);
}
public override bool Equals(object obj) => Equals(obj as MyGraphData);
public override int GetHashCode() => (X, Y).GetHashCode();
public Vector2 ToVector2() => new(X, Y);
}
}
結果

終わりに
前に作ったカスタムコントロールが 2024/08/24 で、一年以上前であることに驚愕