Click here to Skip to main content
Click here to Skip to main content
Go to top

FishEyePanel/FanPanel - Examples of custom layout panels in WPF

, 25 Sep 2006
Rate this:
Please Sign up or sign in to vote.
This article describes how to implement your own WPF layout panels like Grid and StackPanel.

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.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Paul Tallett
Web Developer
Europe Europe
I am a principal development consultant at Microsoft in the UK specialising in UI development. Recently I've been doing a lot of WPF work including the BBC iMP project shown at MIX06. I've been developing software for over 20 years - VAX, WIN16, MFC, ASP.NET, WinForms, WPF.
 
My main hobby is cars and my favourite day out is at Thruxton race track driving the Porsche 911 Turbo.

Comments and Discussions

 
QuestionA question PinmemberFlashBond10-Jun-13 2:05 
GeneralThank you!! Pinmemberbubifengyun30-May-12 20:18 
GeneralJust perfect ! PinmemberMazen el Senih4-Apr-12 6:20 
QuestionHow to make fish eye panel vertical Pinmembervibhutisharma15-Feb-12 0:15 
QuestionGreat Examples PinmemberSchnizit10-Feb-12 5:05 
GeneralMy vote of 5 PinmemberMember 404687411-Dec-11 17:50 
GeneralMy vote of 5 PinmvpHenry Minute9-Oct-11 4:43 
GeneralMy vote of 4 Pinmembershelby6715-Aug-11 8:39 
QuestionImage Click? Pinmembershelby6715-Aug-11 8:26 
AnswerRe: Image Click? PinmemberPaul Tallett15-Aug-11 19:45 
GeneralRe: Image Click? Pinmembershelby6716-Aug-11 22:51 
GeneralMy vote of 5 PinmemberRoland Ebner7-Jun-11 22:22 
GeneralGreat article with Great Demos! Question about running without Visual Studio "Orcas" extensions Pinmemberrich123xyz11-Jan-11 11:19 
GeneralIn WinForms Pinmembersecurigy29-Nov-10 21:35 
GeneralRe: In WinForms Pinmembershelby6715-Aug-11 8:36 
Generalwindows forms visual basic 2008 Pinmemberaddaddy27-Oct-10 16:15 
GeneralThanks Pinmemberarapillai5-Oct-10 2:50 
QuestionHow i can load xmldataprovider dynamically PinmemberSandeep kumar M1-Feb-10 23:56 
GeneralNice piece of code, anyway a small hint for the mathematic behind the scenes PinmemberNachbarsLumpi13-Jan-10 23:40 
GeneralDrag Images From FishEye View Pinmemberd1r2r324-Aug-09 23:54 
QuestionBut can you put a fruit pastel in your mouth without chewing it? PinmemberMootah3-May-09 10:53 
GeneralButton option for image... PinmemberIamHuM3-Apr-09 7:16 
QuestionScrollbar feature Pinmemberrajeshaz099-Feb-09 18:38 
QuestionRe: Scrollbar feature PinmemberMember 314797816-Feb-09 18:11 
AnswerRe: Scrollbar feature Pinmemberrajeshaz0922-Feb-09 18:07 
GeneralRe: Scrollbar feature PinmemberMocotrah_20-Mar-09 3:11 
QuestionHow can I create the items into the FishEyePanel dynamically?? PinmemberMoim Hossain6-Feb-09 2:36 
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

AnswerRe: How can I create the items into the FishEyePanel dynamically?? PinmemberColbyDane9-Feb-09 19:05 
GeneralRe: How can I create the items into the FishEyePanel dynamically?? PinmemberIamHuM3-Apr-09 7:18 
GeneralVertical Orientation PinmemberDerkesthai22-Jan-09 4:38 
GeneralRe: Vertical Orientation PinmemberPaul Tallett22-Jan-09 5:22 
GeneralRe: Vertical Orientation PinmemberDerkesthai22-Jan-09 8:43 
Questionvb version? Pinmemberbflosabre913-Dec-08 5:11 
QuestionFishEyePanel - Without Animation! PinmemberMohdAshi22-Oct-08 12:19 
AnswerRe: FishEyePanel - Without Animation! PinmemberPaul Tallett22-Oct-08 12:25 
GeneralRe: FishEyePanel - Without Animation! PinmemberMohdAshi24-Oct-08 3:17 
GeneralLicense Query PinmvpSacha Barber3-Jul-08 3:32 
GeneralRe: License Query PinmemberEeeEff17-Nov-09 5:33 
GeneralRe: License Query PinmemberPaul Tallett22-Nov-09 6:28 
Generalmultiple fans on one page PinmemberJayDSol18-Oct-07 1:19 
GeneralA small addition to Fisheye PinmemberTasosval23-Aug-07 3:13 
GeneralMulti column support in Fish Eye Panel Pinmembervenugopalm23-May-07 1:38 
QuestionFishEyePanel vertically! Pinmemberluisimp21-Mar-07 7:34 
AnswerRe: FishEyePanel vertically! Pinmemberblakemanster24-Apr-07 8:50 
Questionnice work Pinmembercvalston.net14-Mar-07 14:11 
AnswerRe: nice work PinmemberSilvana Gaia30-Oct-08 9:44 
QuestionFish-eye panel in two dimensions... does it make sense? PinmemberIvan Onuchin20-Jan-07 10:43 
GeneralSo Cool! PinmemberNic Oughton18-Dec-06 12:04 
GeneralMissing feature PinmemberJosh Smith4-Oct-06 14:59 
GeneralRe: Missing feature PinmemberMoim Hossain6-Feb-09 2:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140916.1 | Last Updated 25 Sep 2006
Article Copyright 2006 by Paul Tallett
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid