用Silverlight做雷达图
很多游戏都用雷达图来表示角色的能力值,比如主角的体智敏贤。接下来介绍一下我做的Silverlight雷达图还包含了动画功能。虽然很简单,但不失为一次很好的Silverlight开发体验。
示例:
首先创建一个叫Star的UserControl,作为独立可重用的组件。不需要改动前端的XAML Code,所有的绘画动作都有后台代码完成。假设现在是一个五星图,绘制五个端点的逻辑其实就是从正上方的点开始,每隔360/5放置下个点。
Silverlight有一个多边形的类Polygon可以很好的完成任务。可是这里选用更加通用的Path类主要是为动画效果,由于Polygon的端点无法绑定到Storyboard,在下面会有解释。
在Silverlight中可以用RotateTransform做到基于圆心的点的旋转。
var rotate = new RotateTransform();
rotate.Angle = (360 / 5);
rotate.Transform(new Point());
如此重复5次五个点就可以定位了。由于Path支持许多复杂的图形,能力越大功能越通用也就意味着结构上的复杂。往Path里添加线段需要几个步骤:
1. 设置Path的绘图数据Path.Data为一个几何数据类型PathGeometry
2. PathGeometry可以包含若干个图形,比如同一个Path可以包含一个圆形和一个多边形。由于这里只需要画一个五角形所以只需要一个PathFigure。
3. PathFigure需要很多线段组成,我们往PathFigure.Segments里添加LineSegment
整个流程大致这样
var path = new Path { Data = new PathGeometry() };
var geo = new PathGeometry();
path.Data = geo;
var fig = new PathFigure();
geo.Figures.Add(fig);
fig.StartPoint = new Point();
var seg = new LineSegment();
seg.Point = new Point();
fig.Segments.Add(seg);
有了Path勾勒出的五角形就可以绘制线段或者填充了。Path使用起来虽然麻烦但是对动画的支持很好,接下来就可以体现它的优势了。
制作动画的关键是动画效果Animation以及动画播放器Storyboard。这里用到了点的位移所以选用PointAnimation。为了绑定多边形的点,Silverlight提供了强大的路径选择器,整个的语法就像解构上面添加点的过程。
var anima = new PointAnimation();
anima.To = new Point();
anima.Duration = TimeSpan.FromMilliseconds(AnimaDuration);
Storyboard.SetTarget(anima, new Path());
Storyboard.SetTargetProperty(anima, new PropertyPath(
"(Path.Data).(PathGeometry.Figures)[0].(PathFigure.Segments)[0].(LineSegment.Point)"));
注意最后的字符串,对比之前构建五星图的过程,不难看出这就是一个层层访问属性的路径,数字代表集合元素的位置。整个动画设置了终止点以及持续时间。
好了所有关键技术点都介绍完毕,我们就可以搭建整个五星图类,当然可以很方便的推广到一般雷达图。下面是注释和代码
public partial class Star : UserControl
{
Path instance; //内脏 //显示的数据
Point center; //图的中心
double radius; //半径
const double AnimaDuration = 800; //动画时长
Brush stroke_color = new SolidColorBrush(Colors.Black); //骨架色
Brush fill_color = new SolidColorBrush(Colors.Blue); //填充色
public Star()
{
InitializeComponent();
Loaded += new RoutedEventHandler(Star_Loaded);
}
//画骨架,初始化内脏
void Star_Loaded(object sender, RoutedEventArgs e)
{
radius = 300 / 2;
if (Height < Width)
radius = 300 / 2;
center = new Point(300 / 2, 300 / 2);
double step = radius / 5;
for (int i = 0; i < 5; i++)
{
var star = AddStar(center, radius - i * step);
star.Stroke = stroke_color;
}
AddLines(radius, center);
InitInstance(center);
}
//设置五个0-1的值,按半径比例显示五个点的位置
public void SetStarValues(double ratio1, double ratio2, double ratio3, double ratio4, double ratio5)
{
var newPoints = CalcStarByRatio(center, radius, ratio1, ratio2, ratio3, ratio4, ratio5);
var storyboard = new Storyboard();
//起始点和线段点要分开处理
for (int i = 0; i < 6; i++)
{
var anima = new PointAnimation();
anima.To = newPoints[i % 5];
anima.Duration = TimeSpan.FromMilliseconds(AnimaDuration);
Storyboard.SetTarget(anima, instance);
if (i == 5)
Storyboard.SetTargetProperty(anima, new PropertyPath("(Path.Data).(PathGeometry.Figures)[0].(Point.StartPoint)"));
else
Storyboard.SetTargetProperty(anima, new PropertyPath("(Path.Data).(PathGeometry.Figures)[0].(PathFigure.Segments)[" + i + "].(LineSegment.Point)"));
storyboard.Children.Add(anima);
}
storyboard.Begin();
}
//初始内脏,五个值都为0
private void InitInstance(Point center)
{
instance = AddStar(center, 0);
instance.Fill = fill_color;
}
//画直线
private void AddLines(double radius, Point center)
{
var outsidePoints = CalcFiveVertice(center, radius);
foreach (var p in outsidePoints)
{
var line = new Line();
line.X1 = p.X;
line.Y1 = p.Y;
line.X2 = center.X;
line.Y2 = center.Y;
line.Stroke = stroke_color;
LayoutRoot.Children.Add(line);
}
}
//画五角形的Path,可以用来填充或者画线段
Path AddStar(Point center, double radius)
{
var points = CalcFiveVertice(center, radius);
var star = new Path();
var geo = new PathGeometry();
star.Data = geo;
var fig = new PathFigure();
geo.Figures.Add(fig);
fig.StartPoint = points[0];
//最后一个点要回到起始点
for (int i = 1; i < points.Length + 1; i++)
{
var p = points[i % points.Length];
var seg = new LineSegment();
seg.Point = p;
fig.Segments.Add(seg);
}
LayoutRoot.Children.Add(star);
return star;
}
//定位点
internal static Point[] CalcStarByRatio(Point center, double radius, params double[] r)
{
var points = new List<Point>();
for (int i = 0; i < r.Length; i++)
{
var radians = i * 2 * Math.PI / r.Length + Math.PI / 2;
points.Add(new Point(r[i] * Math.Cos(radians) * radius + center.X, -r[i] * Math.Sin(radians) * radius + center.Y));
}
return points.ToArray();
}
//同比的五个点,助手方法
internal static Point[] CalcFiveVertice(Point center, double radius)
{
return CalcStarByRatio(center, radius, 1, 1, 1, 1, 1);
}
}
这里要注意区分处理Path的起始点和普通点。只要在外部调用类的CalcStarValues并填入对应值就可以工作了。
PS: 1. 本程序在SL4中编译调试通过
2. 如果换用Polygon可以简化很多图形处理步骤,可惜Polygon的端点不是DependencyProperty,所以没有办法是用动画绑定了。