用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,所以没有办法是用动画绑定了。

posted @ 2011-01-28 02:16  dragonpig  阅读(...)  评论(...编辑  收藏