Friday, July 17, 2009

Fisheye for the Silverlight Guy

Do you like the fisheye control that you have seen in the flash or mac world? Would you like to see some of your Silverlight custom user controls show up as a fisheye menu item? Lets all say it together, "You can Do eeeeeeit".

Shinedraw.com
http://www.shinedraw.com/ created the best Silverlight version of a fisheye control for images I have seen. You can download it here: shinedraw image fisheye control I wanted to take it further by not having simple images as items, but complex Silverlight user controls as menu items in the fisheye control. That is where this blog post comes in handy :) I modified shinedraw's control into a reusable user control that allows you to pass a generic list of user controls into the constructor to become fisheye items. And guess what. It works for older Silverlight user controls that have a Canvas as the root element as well as newer Silverlight controls that have Grid as the root element.
Lets Build It!
(Note: if you want to simply add the fisheye to your existing project, follow step 2 to add the FishEyeMenu user control to your own project. Look at step 1 pagexaml.cs for reference on usage)

1) Create a new Silverlight project or download shinedraw.com' s fisheye sample code here. Copy the following page.xaml/page.xaml.cs and paste into your page.xaml file


Page.xaml:
<UserControl x:Class="FishEyeMenu.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="550" Height="400">
<Canvas x:Name="LayoutRoot">
</Canvas>
</UserControl>


Page.xaml.cs:
using System.Collections.Generic;
using System.Windows.Controls;
using System.Windows.Media;

namespace FishEyeMenu
{
public partial class Page : UserControl
{
private const double Margin = 15;
private const double ItemWidth = 130;
private const double ItemHeight = 155;
private const double ScaleFactor = 1.4;
private const double MouseEffectiveness = 130;
private const string ItemRootName = "LayoutRoot";


public Page()
{
InitializeComponent();


FishEyeMenu fishEyeMenu =
new FishEyeMenu(CreateTestCanvases(),
Margin,
ItemWidth,
ItemHeight,
ScaleFactor,
MouseEffectiveness,
ItemRootName);
LayoutRoot.Children.Add(fishEyeMenu);
}

private List<UserControl> CreateTestCanvases()
{
int ItemCountTest = 4;
List<UserControl> xamlItems = new List<UserControl>();

for (int i = 0; i < ItemCountTest; i++)
{
UserControl myUserControl = new MyUserControl();
xamlItems.Add(myUserControl);
}
return xamlItems;
}
}
}


2) Add a new silverlight usercontrol to your project and name it "FishEyeMenu.xaml". It will create the xaml and xaml.cs file for you. Then copy the following FishEyeMenu.xaml/.xaml.cs code and paste it into the respective files:

FishEyeMenu.xaml:

<UserControl x:Class="FishEyeMenu.FishEyeMenu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="750" Height="400">
<Canvas x:Name="LayoutRoot" Background="Transparent">
</Canvas>
</UserControl>


FishEyeMenu.xaml.cs:


using System;
using System.Collections.Generic;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace FishEyeMenu
{
public partial class FishEyeMenu : UserControl
{
#region private properties
//List of xaml objects to display in the fisheye control
private List<UserControl> XamlItems
{
get;
set;
}
// Margin between xaml items
private new double Margin
{
get;
set;
}
private double ItemWidth
{
get;
set;
}
private double ItemHeight
{
get;
set;
}
private double ScaleFactor
{
get;
set;
}
private double MouseEffectiveness
{
get;
set;
}

private string RootXamlName
{
get;
set;
}
#endregion

#region constructor
/// <summary>
/// Displays a list of items made of xaml in a fisheye viewer
/// </summary>
/// <param name="xamlItems">List of Canvas Xaml Items</param>
/// <param name="margin">Margin between items</param>
/// <param name="itemWidth">Item Width</param>
/// <param name="itemHeight">Item Height</param>
/// <param name="scaleFactor">Multiplier of size when hovering over</param>
/// <param name="mouseEffectiveness"></param>
public FishEyeMenu(List<UserControl> xamlItems,
double margin,
double itemWidth,
double itemHeight,
double scaleFactor,
double mouseEffectiveness,
string rootXamlName)
{
InitializeComponent();
//initialize test sizes
XamlItems = xamlItems;
Margin = margin;
ItemWidth = itemWidth;
ItemHeight = itemHeight;
ScaleFactor = scaleFactor;
MouseEffectiveness = mouseEffectiveness;
RootXamlName = rootXamlName;
AddXamlItems();

// start the mouse event handler
this.MouseMove +=new MouseEventHandler(FishEyeMenu_MouseMove);
}

#endregion

#region private methods
private void AddXamlItems()
{
for (int i = 0; i < XamlItems.Count; i++)
{
UserControl userControl = XamlItems[i];

// resize the xaml item
resizeItem(userControl, ItemWidth, ItemHeight, i, XamlItems.Count);
LayoutRoot.Children.Add(userControl);
}

}

private void FishEyeMenu_MouseMove(object sender, MouseEventArgs e)
{
for (int i = 0; i < XamlItems.Count; i++)
{
UserControl canvas = XamlItems[i];

// compute the scale of each item according to the mouse position
double itemScale =
ScaleFactor -
Math.Min(ScaleFactor - 1,
Math.Abs(
e.GetPosition(this).X -
((double) canvas.GetValue(Canvas.LeftProperty) + canvas.Width / 2)
)
/ MouseEffectiveness
);

//Resize the user control
resizeItem(
canvas,
ItemWidth * itemScale,
ItemHeight * itemScale,
i,
XamlItems.Count);

//update the scaletransform
UpdateScale(canvas, itemScale);

// set the z order
canvas.SetValue(Canvas.ZIndexProperty, (int) Math.Round(ItemWidth * itemScale));
}
}

private void resizeItem(UserControl userControl, double itemWidth, double itemHeight, int index, int total)
{
userControl.Width = itemWidth;
userControl.Height = itemHeight;
userControl.SetValue(Canvas.TopProperty, Height / 2 - userControl.Height / 2);
userControl.SetValue(Canvas.LeftProperty, Width / 2 + (index - (total - 1) / 2) * (Margin + ItemWidth) - userControl.Width / 2);

}

private void UpdateScale(UserControl userControl,
double scaleFactor)
{
if (scaleFactor < 1)
scaleFactor = 1;
var objectToScale = userControl.FindName(RootXamlName);
if (objectToScale == null) throw new NotImplementedException();
ScaleTransform st = new ScaleTransform();
st.CenterX = 0;
st.CenterY = 0;
st.ScaleX = scaleFactor;
st.ScaleY = scaleFactor;
if (objectToScale.GetType() == typeof(Canvas))
{
((Canvas)objectToScale).RenderTransform = st;
}
if (objectToScale.GetType() == typeof(Grid))
{
((Grid)objectToScale).RenderTransform = st;
}

}
#endregion
}
}


3) Create another silverlight user control (or user your own existing silverlight user control). This is the user control that will be used as a fisheye menu item. If you want to use my example silverlight user control, name it MyUserControl.xaml and copy the following code to past into theMyUserControl.xaml/xaml.cs files.

MyUserControl.xaml

<UserControl x:Class="FishEyeMenu.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400"
Height="300">
<Canvas x:Name="LayoutRoot" Background="Gray" Opacity=".5">
<TextBlock Text="FishEyeMe"></TextBlock>
</Canvas>
</UserControl>


MyUserControl.xaml.cs

using System.Windows.Controls;
namespace FishEyeMenu
{
public partial class MyUserControl : UserControl
{
public MyUserControl() { InitializeComponent(); }
}
}

4) Compile and run the application.

Note: if you get a compile error in app.xaml, overwrite your Application_Startup method with the following: (This fixes a namespace issue that you have if you copied and pasted the all of the above code. If you selectively copy and paste the above code to keep your own namespace names you will not have this issue)

private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new FishEyeMenu.Page();
}

Remarks:

What shinedraws fisheye does it calculate the scale and resizes the height and width of the images. What I have done is organized the code into a reusable class that takes params during the instantiation of the control. I also changed the array of images into a generic list of user controls.


ResizeItem updates the height and width of the user control, but that does not scale the contents of the user control. UpdateScale takes care of this by adding a ScaleTransform to the root of the custom user controls from the generic list if the root element is a Canvas or Grid.


Happy Coding!



Friday, July 10, 2009

my first blog post and insight into future posts

Hello, my name is Bryon. I love those stickers!

This blog will be focused primarily on Software Development practices and techniques utilizing .net 3.5, silverlight, Blend, xaml, wcf, windows mobile development, and a slew of other technologies that I don't want to list out in this post.

I am going to try posting once a week. Happy reading!