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.

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.

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.

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.

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.

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.

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.

Share

About the Author

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.