В этом проекте я хотел бы рассказать о том, как можно сделать красивое облако тегов на Silverlight.
Сразу хочу признаться, что идею и часть реализации я подсмотрел у других. Однако, приведенный там пример слегка глючил. Поэтому я решил написать данную статью.
Поставим себе задачу следующим образом:
К сожалению, последняя версия Silverlight не включает в себя трехмерные возможности как в WPF. К счастью, часть необходимого функционала написана сторонними разработчиками. В данном проекте будет используется библиотека Axelerate3D.
XAML
<UserControl x:Class="Mercury.Web.Silverlight.Tags.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid x:Name="LayoutRoot" Background="White">
<Canvas x:Name="RootCanvas" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
</Canvas>
</Grid>
</UserControl>
Тут приведен XAML-код основного класса Silverlight приложения (MainPage). Как видите, здесь все просто. Один единственный Canvas, который будет заниматься рендерингом облака.
Отображение тегов: Перейдем к созданию класса работы с тегом. Он (класс) будет представлять метку с нужным текстом, которая будет расположена в координатах (x, y, z) относительно центра облака.
public class Tag3D
{
public Tag3D(double x, double y, double z, string text, Uri uri)
{
CenterPoint = new Point3D(x, y, z);
TextBlock = new TextBlock { Text = text };
BtnLink = new HyperlinkButton
{
Content = TextBlock,
BorderThickness = new Thickness(0),
Padding = new Thickness(0),
NavigateUri = uri,
Visibility = Visibility.Collapsed
};
}
public HyperlinkButton BtnLink { get; set; }
public Point3D CenterPoint { get; set; }
private TextBlock TextBlock { get; set; }
public void Redraw(UISize size) { ... }
private void UpdatePosition(double rx, double ry, double xOffset, double yOffset) { ... }
private void UpdateLayout() { ... }
private void UpdateSize() { ... }
private void UpdateColor() { ... }
private void UpdateVisibility() { ... }
}
Затем нам необходимо сделать методы для определения размеров и цвета выбранного тега:
private void UpdateSize()
{
BtnLink.FontSize = 16 + CenterPoint.Z * 4;
}
private void UpdateColor()
{
var color = 160 + CenterPoint.Z * 96;
color = Math.Max(0, color);
color = Math.Min(255, color);
BtnLink.Foreground = new SolidColorBrush(Color.FromArgb(Convert.ToByte(color), 0, 0, 0));
}
Теперь позиционируем тег в пространстве:
// rx, ry - размеры эллипса облака (в проекции на плоскость)
// xOffset, yOffset - координаты центра облака, относительно Canvas
private void UpdatePosition(double rx, double ry, double xOffset, double yOffset)
{
var maxZ = Math.Min(rx, ry);
var x = (xOffset + CenterPoint.X * rx) - (BtnLink.ActualWidth / 2.0);
var y = (yOffset - CenterPoint.Y * ry) - (BtnLink.ActualHeight / 2.0);
var z = CenterPoint.Z * maxZ;
Canvas.SetLeft(BtnLink, x);
Canvas.SetTop(BtnLink, y);
Canvas.SetZIndex(BtnLink, Convert.ToInt32(z));
}
Размещение тегов на поверхности сферы: Вернемся к классу MainPage.
Для понимания следующего фрагмента кода необходимо некоторое знание математики. Этот метод распределяет теги по поверхности сферы.
private void FillTags()
{
RootCanvas.UpdateLayout();
RootCanvas.Children.Clear();
tagBlocks = new List<Tag3D>();
var length = tagList.Count;
for (var i = 1; i <= length; i++)
{
var phi = Math.Acos(-1.0 + (2.0 * i - 1.0) / length);
var theta = Math.Sqrt(length Math.PI) phi;
var x = Math.Cos(theta) * Math.Sin(phi);
var y = Math.Sin(theta) * Math.Sin(phi);
var z = Math.Cos(phi);
var tag = tagList[i - 1];
var uri = UrlHelper.GetTagUri(UrlEncode(tag));
var item = new Tag3D(x, y, z, tag, uri);
RootCanvas.Children.Add(item.BtnLink);
tagBlocks.Add(item);
}
}
Вращение облака: Для вращения облака мы будем использовать положение курсора мыши. Чем больше расстояние от центра облака, до курсора, тем больше угол поворота облака, а значит и скорость его вращения.
В момент инициализации придаем небольшое начальное вращение:
private readonly RotateTransform3D rotateTransform = new RotateTransform3D();
public void Run()
{
CompositionTarget.Rendering += OnCompositionTargetRendering;
LayoutRoot.MouseEnter += OnLayoutRootMouseEnter;
LayoutRoot.MouseLeave += OnLayoutRootMouseLeave;
slowDownCounter = 500.0;
runRotation = true;
rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(0.8, 0.6, 0), 0.5);
FillTags();
}
Определяем направление и скорость вращения облака:
private void OnLayoutRootMouseMove(object sender, MouseEventArgs e)
{
var position = e.GetPosition(RootCanvas);
SetRotateTransform(position);
}
private void SetRotateTransform(Point position)
{
var size = GetUISizes();
var x = (position.X - size.XOffset) / size.XRadius;
var y = (position.Y - size.YOffset) / size.YRadius;
var angle = Math.Sqrt(x x + y y);
rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(-y, -x, 0.0), angle);
}
private UISize GetUISizes()
{
return new UISize
{
XOffset = RootCanvas.ActualWidth / 2.0,
YOffset = RootCanvas.ActualHeight / 2.0
};
}
Теперь займемся отрисовкой тегов.
private void OnCompositionTargetRendering(object sender, EventArgs e)
{
if (!(runRotation || (slowDownCounter <= 0.0)))
{
var rotation = (AxisAngleRotation3D)rotateTransform.Rotation;
rotation.Angle *= slowDownCounter / 500.0;
rotateTransform.Rotation = rotation;
slowDownCounter--;
}
if (((AxisAngleRotation3D)rotateTransform.Rotation).Angle > 0.05)
{
RotateBlocks();
}
}
private void RotateBlocks()
{
var size = GetUISizes();
foreach (var tagd in tagBlocks)
{
Point3D pointd;
if (rotateTransform.TryTransform(tagd.CenterPoint, out pointd))
{
tagd.CenterPoint = pointd;
tagd.Redraw(size);
}
}
}
Загрузка списка тегов:
Теперь можно позаботиться о том, чтобы наполнить наше облака данными о тегах.
Путь некоторый Url возвращает нам XML вот такой структуры:
<?xml version="1.0" encoding="utf-8"?>
<taglist>
<tag>IIS7</tag>
<tag>.NET</tag>
<tag>Silverlihgt</tag>
<tag>3D</tag>
</taglist>
Тогда код для получения списка тегов может выглядеть так:
private void LoadTagList(Uri uri)
{
try
{
// Получаем XML со списком тегов.
var request = (HttpWebRequest)WebRequest.Create(uri);
var waitingEvent = new AutoResetEvent(false);
var callback = new AsyncCallback(result => ((EventWaitHandle)result.AsyncState).Set());
var asyncResult = request.BeginGetResponse(callback, waitingEvent);
waitingEvent.WaitOne();
var response = request.EndGetResponse(asyncResult);
// Парсим XML и составляем список тегов.
var doc = XDocument.Load(response.GetResponseStream());
tagList.AddRange(doc.Descendants("tag").Select(item => item.Value));
Dispatcher.BeginInvoke(Run);
}
catch
{
}
}
Все!
Исходники можно загрузить здесь.
HTML код:
<object width="500" height="300" type="application/x-silverlight-2" data="data:application/x-silverlight-2,">
<param name="source" value="/ClientBin/Mercury.Web.Silverlight.Tags.xap" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="3.0.40624.0" />
<param name="autoUpgrade" value="true" />
<param name="windowless" value="true" />
<param name="enableGPUAcceleration" value="True" />
<param name="initParams" value="url=/Node/Tags/{0},service=/Node/TagList" />
<a style="text-decoration: none" href="http://go.microsoft.com/fwlink/?LinkID=149156&amp;v=3.0.40624.0">
<img style="border-style: none"
src="http://go.microsoft.com/fwlink/?LinkId=108181"
alt="Get Microsoft Silverlight" />
</a>
</object>
Вот и все!