Click here to Skip to main content
Email Password   helpLost your password?

FishEye demo

FishEye demo

Introduction

Recently, I needed to write some custom panels (like Grid and StackPanel, but with different arrangements). These turned out pretty cool and seemed like good examples of how to go about writing a custom panel, so I thought I'd share them. These were written as part of a Proof-Of-Concept rather than production quality code, so of course, in the code cleanup for submission, I ended up doing a re-write but that's normal!

There're two different panels - FishEyePanel and FanPanel. They were both designed by Martin Grayson who drew the ideas as wireframes - I then wrote the code.

The FishEyePanel is pretty complete, and implements a variation on the Hyperbar sample found in the Expression Interactive Designer samples, and also as featured on the MAC taskbar. The difference with this control is the interesting children growth, but the other children shrink to make room - unlike Hyperbar and MAC. This means the panel stays a constant width.

The FanPanel arranges its children into a stack which explodes out when you mouse over it. Then, when you click, it expands to a full view. The panel needs a bit more work as feeding it children that are not about 300 square results in the wrong effects.

Now just because I work for Microsoft doesn't mean that these samples are correct. I'm not in the product groups or anything, and don't have any insider information. I came up with the strategy of arranging the children on top of one another at (0,0) and then using RenderTransforms attached to the children to move them to where I wanted them. I don't know if this is a pukka thing to do, but it seems to work quite nicely.

Using the code

To compile the code, you need VS2005, .NET Framework 3.0 (RC1), and the Visual Studio "Orcas" extensions. If you don't have the extensions, VS won't recognise the project type.

I've included pre-built EXEs so you can play with just the RC1 .NET Framework. The code should just require re-compiling for later versions of the framework.

Writing a custom panel

To get your own custom panel off the ground, you need to derive from System.Windows.Controls.Panel and implement two overrides: MeasureOverride and LayoutOverride. These implement the two-pass layout system where during the Measure phase, you are called by your parent to see how much space you'd like. You normally ask your children how much space they would like, and then pass the result back to the parent. In the second pass, somebody decides on how big everything is going to be, and passes the final size down to your ArrangeOverride method where you tell the children their size and lay them out. Note that every time you do something that affects layout (e.g., resize the window), all this happens again with new sizes.

protected override Size MeasureOverride(Size availableSize)
{
    Size idealSize = new Size(0, 0);

    // Allow children as much room as they want - then scale them

    Size size = new Size(Double.PositiveInfinity, Double.PositiveInfinity);
    foreach (UIElement child in Children)
    {
        child.Measure(size);
        idealSize.Width += child.DesiredSize.Width;
        idealSize.Height = Math.Max(idealSize.Height, 
                           child.DesiredSize.Height);
    }

    // EID calls us with infinity, but framework

    // doesn't like us to return infinity

    if (double.IsInfinity(availableSize.Height) || 
        double.IsInfinity(availableSize.Width))
        return idealSize;
    else
        return availableSize;
}

In our MeasureOverride, we throw discipline to the wind and let our children have all the space they want. We then tell our parent our ideal size, which it ignores in our case. The children tell us what size they want to be, through the child.DesiredSize property that is set during the Measure call. We are going to scale our children to fit whatever size is imposed on us.

protected override Size ArrangeOverride(Size finalSize)
{
    if (this.Children == null || this.Children.Count == 0)
        return finalSize;

    ourSize = finalSize;
    totalWidth = 0;

    foreach (UIElement child in this.Children)
    {
        // If this is the first time

        // we've seen this child, add our transforms

        if (child.RenderTransform as TransformGroup == null)
        {
            child.RenderTransformOrigin = new Point(0, 0.5);
            TransformGroup group = new TransformGroup();
            child.RenderTransform = group;
            group.Children.Add(new ScaleTransform());
            group.Children.Add(new TranslateTransform());
//                    group.Children.Add(new RotateTransform());

        }

        child.Arrange(new Rect(0, 0, child.DesiredSize.Width, 
                      child.DesiredSize.Height));

        totalWidth += child.DesiredSize.Width;
    }

    AnimateAll();

    return finalSize;
}

In our ArrangeOverride, we add scale, and translate transforms to every child, and total up how wide they want to be. The panel works with different sized children, and you can either automatically scale them individually so they are the same width, or have them displayed at all different sizes. This is controlled by the ScaleToFit property. Note: we just pile up all the children at (0,0), and we will later use RenderTransforms to move them around.

The heart of the FishEyePanel is the following code:

// These next few lines took two of us hours to write!

double mag = Magnification;
double extra = 0;
if (theChild != null)
    extra += mag - 1;

if (prevChild == null)
    extra += ratio * (mag - 1);
else if (nextChild == null)
    extra += ((mag - 1 ) * (1 - ratio));
else
    extra += mag - 1;

double prevScale = this.Children.Count * (1 + ((mag - 1) * 
                  (1 - ratio))) / (this.Children.Count + extra);
double theScale = (mag * this.Children.Count) / 
                  (this.Children.Count + extra);
double nextScale = this.Children.Count * (1 + ((mag - 1) * ratio)) / 
                                      (this.Children.Count + extra);
double otherScale = this.Children.Count / 
                   (this.Children.Count + extra);
                   // Applied to all non-interesting children

This is some of the hardest code I've ever written, and took about half a day with two of us with pens and paper to figure out! I'm not going to explain the math - suffice it to say that it scales three children to be larger than the others, dependant on where the mouse is, and resizes all the others to take up the remaining space.

The heart of the FanPanel is the following code:

if (!IsWrapPanel)
{
    if (!this.IsMouseOver)
    {
        // Rotate all the children into a stack

        double r = 0;
        int sign = +1;
        foreach (UIElement child in this.Children)
        {
            if (foundNewChildren)
                child.SetValue(Panel.ZIndexProperty, 0);

            AnimateTo(child, r, 0, 0, scaleFactor);
            r += sign * 15;         // +-15 degree intervals

            if (Math.Abs(r) > 90)
            {
                r = 0;
                sign = -sign;
            }
        }
    }
    else
    {
        // On mouse over explode out the children and don't rotate them

        Random rand = new Random();
        foreach (UIElement child in this.Children)
        {
            child.SetValue(Panel.ZIndexProperty, 
                           rand.Next(this.Children.Count));
            double x = (rand.Next(16) - 8) * ourSize.Width / 32;
            double y = (rand.Next(16) - 8) * ourSize.Height / 32;
            AnimateTo(child, 0, x, y, scaleFactor);
        }
    }
}
else
{
    // Pretend to be a wrap panel

    double maxHeight = 0, x = 0, y = 0;
    foreach (UIElement child in this.Children)
    {
        if (child.DesiredSize.Height > maxHeight)
        // Row height

            maxHeight = child.DesiredSize.Height;
        if (x + child.DesiredSize.Width > this.ourSize.Width)
        {
            x = 0;
            y += maxHeight;
        }

        if (y > this.ourSize.Height - maxHeight)
            child.Visibility = Visibility.Hidden;
        else
            child.Visibility = Visibility.Visible;

        AnimateTo(child, 0, x, y, 1);
        x += child.DesiredSize.Width;
    }
}

Again, this scales/rotates/transforms all the children into their three possible arrangements: stacked up, exploded, or wrap panel style. Note that we animate between the different states so adding children looks pretty funky too (not shown in demo).

The FanPanel is not as clean as the FishEyePanel, as I wrote it first. Most of the grunge is, however, in the Favourites.xaml and Favourites.xaml.cs files, not in the control. I don't really like how you have to change the size of the panel when you expand it, and getting the two sets of animations to look right together was tricky. The effect looks pretty cool though, so I thought I'd submit it anyway. If you want to base your own panel on one of these controls, go with the FishEyePanel.

Points of interest

There are a few neat things which took a while to figure out, and are used in both samples.

If you need to get a reference to the panel used in an ItemsControl, it is not straightforward. You can't just give it a name, because it's in a template and that is not the real control. I eventually came up with the idea of hooking the Loaded event and casting the sender to the relevant Panel type.

The way I do the test data is pretty cool too. Martin Grayson came up with this method - take a look at TestData.xaml. It uses an XmlDataProvider. I found the XPath and the Binding statements hard to get right, but it's a great way to quickly dummy up some data when you are waiting for the middle-tier to deliver some real stuff.

<XmlDataProvider x:Key="Things" XPath="Things/Thing">
 <x:XData>
  <Things xmlns="">
    <Thing Image="Aquarium.jpg"/>
    <Thing Image="Ascent.jpg"/>
    <Thing Image="Autumn.jpg"/>
    <Thing Image="Crystal.jpg"/>
    <Thing Image="DaVinci.jpg"/>
    <Thing Image="Follow.jpg"/>
    <Thing Image="Friend.jpg"/>
    <Thing Image="Home.jpg"/>
    <Thing Image="Moon flower.jpg"/>
  </Things>
 </x:XData>
</XmlDataProvider>

Also note how the resources are referenced in the App.xaml so they are globally available.

Another really wonderful property is setting IsHitTestVisible = false. This means that for all mouse hit testing, it is invisible, and all the events go to the parent as if it wasn't there. This is especially useful when implementing drag and drop, if you are moving an item, so it tracks under the mouse. You can set this property and the mouse moves go to the underlying parent. That took two days of screwing around to discover!

Another cool thing is the ImagePathConverter. This allows you to specify relative paths to the images - code in the convertor hunts for the Images folder and remaps the references.

public class ImagePathConverter : IValueConverter
{
    #region IValueConverter Members

    private static string path;

    public object Convert(object value, Type targetType, 
                  object parameter, 
                  System.Globalization.CultureInfo culture)
    {
        if (path == null)
        {
            path = Path.GetDirectoryName(Path.GetDirectoryName(
                   Path.GetDirectoryName(
                   Assembly.GetExecutingAssembly().Location))) + 
                   "\\Images";
            if (!Directory.Exists(path))
            {
                path = Path.GetDirectoryName(
                       Assembly.GetExecutingAssembly().Location) + 
                       "\\Images";
                if (!Directory.Exists(path))
                    throw new FileNotFoundException("Can't " + 
                                  "find images folder", path);
            }
            path += "\\";
        }
        return string.Format("{0}{1}", path, (string)value);
    }

    public object ConvertBack(object value, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        throw new Exception("The method or operation is not implemented.");
    }

    #endregion
}

One last point to note is, in the FishEyePanel, there is a difference between feeding it children that are the same width, and asking it to rescale them to the same width - note how on the pink house at the end of the top row, the margins are slightly larger than the others. This is because it started off as a smaller child and was scaled, margins and all. For best results, feed it children that are the same size.

Summary

Implementing your own panels can be a lot of fun, but the layout code can be really hard to write. You need to solve simultaneous equations and the like.

Please use this code as you like. You can even include it in a product which you sell if you want. I'm goaled on driving WPF adoption, which is why I publish sample code.

History

Fixed to handle different sized children - added ScaleToFit property.

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralHow i can load xmldataprovider dynamically
Sandeep kumar M
0:56 2 Feb '10  
Hi, I want to update FanPanel dynamically, so that i need to load dataprovider dynamically, how is it possible, please let me know.
GeneralNice piece of code, anyway a small hint for the mathematic behind the scenes
NachbarsLumpi
0:40 14 Jan '10  
You could use a Gauss function (see: http://en.wikipedia.org/wiki/Normal_distribution[^]) to calculate the scaling of elements. This "plugged" into the transformation matrices of the items and you have it ready once you move the center of the virtual coordinate system when you mouse over. Mathematically a bit more complex in the beginning but it has the advantage to be a uniform calculation with complexity O(1).

This is the basic method producing the gauss normal distribution with amplitude a and the inflection points at +-b (on the wanted axis).
static double gauss(double a, double b, double axisValue)
{
return a * Math.Exp(-b * Math.Pow(axisValue, 2));
}

GeneralDrag Images From FishEye View
d1r2r3
0:54 25 Aug '09  
Hi, I want to be able to drag and drop images from the fisheye view out onto another canvas or something.
To determine which child is hit when i click my mouse down, i need to use hit testing with the input of the mouse's coordinates. I've tried using InputHittest and a callback as well but both dont return the child element i was clicking on. Does anyone know how to do this? can you provide so code?
thanks
GeneralBut can you put a fruit pastel in your mouth without chewing it?
Mootah
11:53 3 May '09  
Hi Paul, I'm at TVP in a couple of weeks doing "SalesPresenter 2"..this time it's "Multi-touch" enabled. How about popping along and getting your Fisheye panel to scroll with a swipe of your finger!
Contact me or Frazer, it would be good to have your input.
John Taylor (YELL)
GeneralButton option for image...
IamHuM
8:16 3 Apr '09  
hi...

Paul... Gr8 article...

If i want to use buttons for the same effects and handle button click effect then what will be the modifications. I am new in WPF.
I want to use images as button background but at the same time i want to process buton click events seperately for each button. How i can do this...?


Thanks for such a gr8 article once again...
IamHuM
QuestionScrollbar feature
rajeshaz09
19:38 9 Feb '09  
How to add scrollbar feature? I tried to add it. But scrollbar not displaying



Thanks
Rajesh A
QuestionRe: Scrollbar feature
Member 3147978
19:11 16 Feb '09  
fan panel is really cool. I tried to customize it for which I needed to add scrollbar support so the panel can show more items that it is now. So far I could not do it... any suggestion is appreciated. thanks
AnswerRe: Scrollbar feature
rajeshaz09
19:07 22 Feb '09  
Finally i got it : Big Grin




protected override Size MeasureOverride(Size availableSize)
{
// Allow children as much room as they want - then scale them
Size size = new Size(Double.PositiveInfinity, Double.PositiveInfinity);
foreach (UIElement child in Children)
{
child.Measure(size);
}

// EID calls us with infinity, but framework doesn't like us to return infinity
//if (double.IsInfinity(availableSize.Height) || double.IsInfinity(availableSize.Width))
// return new Size(600, 600);
//else
// return availableSize;
if (double.IsInfinity(availableSize.Width)) availableSize.Width = 600;
if (double.IsInfinity(availableSize.Height)) availableSize.Height = CaliculateSize(false, availableSize.Width);
return availableSize;
}

protected override Size ArrangeOverride(Size finalSize)
{
RaiseEvent(new RoutedEventArgs(FanPanel.RefreshEvent, null));
if (this.Children == null || this.Children.Count == 0)
return finalSize;

ourSize = finalSize;
foundNewChildren = false;

foreach (UIElement child in this.Children)
{
// If this is the first time we've seen this child, add our transforms
if (child.RenderTransform as TransformGroup == null)
{
foundNewChildren = true;
child.RenderTransformOrigin = new Point(0.5, 0.5);
TransformGroup group = new TransformGroup();
child.RenderTransform = group;
group.Children.Add(new ScaleTransform());
group.Children.Add(new TranslateTransform());
group.Children.Add(new RotateTransform());
}

// Don't allow our children any clicks in icon form
child.IsHitTestVisible = IsWrapPanel;
child.Arrange(new Rect(0, 0, child.DesiredSize.Width, child.DesiredSize.Height));

// Scale the children so they fit in our size
double sf = (Math.Min(ourSize.Width, ourSize.Height) * 0.4) / Math.Max(child.DesiredSize.Width, child.DesiredSize.Height);
scaleFactor = Math.Min(scaleFactor, sf);
}

CaliculateSize(true, this.ourSize.Width);

return finalSize;
}

private double CaliculateSize(bool canAnimate, double width)
{
System.Diagnostics.Debug.WriteLine("AnimateAll()");
double maxHeight = 0, x = 0, y = 0;
if (IsWrapPanel)
{
// Pretend to be a wrap panel
foreach (UIElement child in this.Children)
{
if (child.DesiredSize.Height > maxHeight) // Row height
maxHeight = child.DesiredSize.Height;
if (x + child.DesiredSize.Width > width)
{
x = 0;
y += maxHeight;
}

if (canAnimate)
{
child.Visibility = Visibility.Visible;
AnimateTo(child, 0, x, y, 1);
}
x += child.DesiredSize.Width;
}
}
return y + maxHeight;
}

GeneralRe: Scrollbar feature
Mocotrah_
4:11 20 Mar '09  
Hi rajeshaz09.
I have tried your modifications in FanPanel but I don't see any scroll bars at all;
the cards are rendered all the way down.
Additionally the start animation is broken.
Could you share your modifications in a project please?
GeneralHow can I create the items into the FishEyePanel dynamically??
Moim Hossain
3:36 6 Feb '09  
Hi this is indeed a cool work!! Congrats!

BTW, Can any one tell me, if I need to populate the items dynamically using Binding, how can I do so?
A panel has got a Children property, unlike other collection control ListView, Listbox that offers a property named ItemsSource or stuffs like that. How can i make this thing work for a panel.

To clarify the question- How to bind the items displayed inside a FishEyePanel ..or is it possible at all?

Moim Hossain
R&D Project Manager
BlueCielo ECM Solutions BV

GeneralRe: How can I create the items into the FishEyePanel dynamically??
ColbyDane
20:05 9 Feb '09  
This should fix you up!

<ItemsControl ItemsSource="{Binding YourPath}">
      <ItemsControl.ItemsPanel >
               <ItemsPanelTemplate >
                        <FishEyePanel />
               </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
</ItemsControl>
GeneralRe: How can I create the items into the FishEyePanel dynamically??
IamHuM
8:18 3 Apr '09  
hi...

How i can get the images from resource files...?

Thanks,
IamHuM
GeneralVertical Orientation
Derkesthai
5:38 22 Jan '09  
Hi!

Is there any (easy :P) way of making the FishEyePanel vertical?
I tried to RotateTransform, but that's obviously not that useful, since the contents also rotate Wink
Some hints would be appriciated, the control itself is awesome!
GeneralRe: Vertical Orientation
Paul Tallett
6:22 22 Jan '09  
Someone else had acheived this by rotating the content back again.

It really should be fairly simple to swap all the Width and Heights around.

Cheers,
Paul
GeneralRe: Vertical Orientation
Derkesthai
9:43 22 Jan '09  
Hmm, I guess that's all that it takes. Sounds like a nice job for a rainy sunday afternoon, hehe. Thanks for the hint.
Generalvb version?
bflosabre91
6:11 3 Dec '08  
anyone has the fisheyepanel in VB? I don't know enough c# to convert. thanks anyone?!?! Big Grin
QuestionFishEyePanel - Without Animation!
MohdAshi
13:19 22 Oct '08  
Hi Paul,

You and your partner have done a wonderful job Smile Thanks very much for giving a good example.

I was playing with this control to figure out its secrets. Then 2 questions poped up Sniff :

Q1) I set the properties directly without animation (learning purpose) but it did not work. Why?

        private void AnimateTo( ...
{
TransformGroup group = (TransformGroup)child.RenderTransform;
ScaleTransform scale = (ScaleTransform)group.Children[0];
TranslateTransform trans = (TranslateTransform)group.Children[1];
// RotateTransform rot = (RotateTransform)group.Children[2];
trans.X = x;
trans.Y = y;
scale.ScaleX = s;
scale.ScaleY = s;
}
Q2) I thought that in ArrangeOverride we are only allowed to modify the children using their function Arrange . How far this statement is correct?
AnswerRe: FishEyePanel - Without Animation!
Paul Tallett
13:25 22 Oct '08  
1) Setting the property directly probably has no effect because there is an animation overriding your setting. If you get rid of the animation, then setting directly should work.

2) Dunno, sorry.

Cheers,
Paul
GeneralRe: FishEyePanel - Without Animation!
MohdAshi
4:17 24 Oct '08  
Thank you for the quick answer.
1) After tracing actually I discovered that my problem was the flag animation_completed was set to false, so the MouseMove event did not invalidate the control !!! Smile .

Thanks
Mohamed Ashi
GeneralLicense Query
Sacha Barber
4:32 3 Jul '08  
Paul

Hello there, I would like to know if its ok to use your Fisheye panel in a commercial app?

Could you let me know, if this is ok.

Many thanks

Sacha Barber
  • Microsoft Visual C# MVP 2008
  • Codeproject MVP 2008
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

GeneralRe: License Query
EeeEff
6:33 17 Nov '09  
These are pretty cool panels - yeah, was there an answer to this query regarding commercial use?
GeneralRe: License Query
Paul Tallett
7:28 22 Nov '09  
No probs with commercial use.

Paul
Generalmultiple fans on one page
JayDSol
2:19 18 Oct '07  
First of all, great control. Very impressive.

I want to add 8 fan panels in one drid, each with different sets of images. Can this be done?

I will have 8 panels with different images

Cheers


GeneralA small addition to Fisheye
Tasosval
4:13 23 Aug '07  
Great work on both panels, they sure are a must for everyone who tries to learn how to write custom panels. I have one small addition that I would like to propose, that came up while playing with fisheye panel. When you try to use it as a dock (see MacOS) the panel with it's default handling of scale transform will put a portion of your items out of the visible area. The solution to this problem is very simple and can be given with a few lines of code.

First we add a DepedencyProperty that will be used to control the mode of expansion (we also add the corresponding enum):
public enum ScaleFromEnum { Middle, Up, Down };

public ScaleFromEnum ScaleFrom
{
get { return (ScaleFromEnum)GetValue(ResizeFromProperty); }
set { SetValue(ResizeFromProperty, value); }
}

public static readonly DependencyProperty ResizeFromProperty =
DependencyProperty.Register("ScaleFrom", typeof(ScaleFromEnum), typeof(FishEyePanel), new UIPropertyMetadata(ScaleFromEnum.Middle));

Then add the following inside the ArrangeOverride method and the loop that gives the appropriate transforms:
...
child.RenderTransform = group;
ScaleTransform st = new ScaleTransform();
switch ( ScaleFrom )
{
case ScaleFromEnum.Middle:
break;
case ScaleFromEnum.Down:
st.CenterY = child.DesiredSize.Height/2;
break;
case ScaleFromEnum.Up:
st.CenterY = -child.DesiredSize.Height/2;
break;
}
group.Children.Add(st);
...

And now you are ready. Just use the ScaleFrom property like this
<fish:FishEyePanel ScaleFrom="Down">
GeneralMulti column support in Fish Eye Panel
venugopalm
2:38 23 May '07  
Hi ,
I need Multi column support , could anyone please send me the code snippet for this concept.

My Expectation is :

I want display 10 images in Fish Ey Demo with two rows. 5 Images in first row another in second row. with same animation.

Thanks,
Venugopal M.

Venugopal works as a Junior Software Engineer for a MNC Company. He hold a Masters in Computer Applications and has one year experience in various technology.

Venugopal firmly believes "technology is impermanent, change is not"!. While he has strong technical inclination towards MS technologies, he is deeply passionate about pursuing his career in Vista-WPF Technology.

Venu engages in a wide gamut of temperamental hobbies ranging from making friends, traveling, helping his


Last Updated 25 Sep 2006 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010