Creating a Custom Panel

I've spent time the last couple weeks trying to figure out how to create my own panel. At first I was completely confused and it seemed like I would change something small and the entire layout of my panel would radically shift. But after stumbling around in the dark for a while, I feel like the light has clicked on, so I'll see if I can give some advice on custom panels.

 

First you need to know about Measuring and Arranging. The Silverlight layout system starts by doing a Measure pass. This is when each child element tells the layout system its desired size. This includes the panel itself. Then there is an Arrange pass. Here the final size and position of each child's bounding box is specified as well as the final size of the panel itself.

 

So, to create your own panel, you need to create a class that derives from Panel and implements the MeasureOverride and ArrangeOverride methods. I've created a panel that I'm calling BlockPanel. It creates 3x3 blocks of elements and then lays each block side-by-side wrapping them around when it reaches the right edge of the panel. I've limited each element to be 100x100. I was going to do something more complicated where I figured out what size the element wanted to be and then adjusted my blocks but the code was getting too long and complex for an SDK sample. So, here is a picture of how the children will get layed out by my panel:

 

 

Now let's go through the code. Here is the MeasureOverride method for my panel:

 

public class BlockPanel : Panel

    {

        //First measure all children and return available size of panel

        protected override Size MeasureOverride(Size availableSize)

        {

            //Measure each child

            foreach (FrameworkElement child in Children)

            {

                child.Measure(new Size(100,100));

            }

            //return the available size

            return availableSize;

        }

}

What happens is that the layout system passes you the size available to your panel. If you have specifically set the height and width of the panel when you created it, that is what you get. Inside MeasureOverride, I'm calling Measure on each child passing the size that my panel is making available, in my case 100x100. In a lot of the samples I've seen, availableSize is passed back, which basically says that the child can take up the entire area of the panel. After Measure is called, the layout system will determine the desired size of the child. I haven't figured out all the details of this, but it seems like for the most part it takes the smaller of the available size and the native size. So, in my BlockPanel, if you have a child Rectangle that is 200x200, the desired size would be set to 100x100. If the Rectangle was 50x50, then the desired size would be 50x50.

After I measure all the children, then I return the size available to the whole panel. I'm returning the availableSize that was passed in, so I'm not restricting the size of the panel in Measure at all.

Next the layout system will look for ArrangeOverride. Here is the ArrangeOverride for my Panel:

public class BlockPanel : Panel

    {

//Second arrange all children and return final size of panel

protected override Size ArrangeOverride(Size finalSize)

{

//Get the collection of children

UIElementCollection mychildren = Children;

//Get the total number of children

int total = mychildren.Count;

//Calculate the number of 3x3 blocks needed

int blocks = (int)Math.Ceiling((double)total/9.00);

//Calculate how many 3x3 blocks fit on a row

int blocksInRow = (int)Math.Floor(finalSize.Width / 300); //assuming blocks of 9 element 300x300

//Arrange children

int i;

double maxWidth = 0;

double maxHeight = 0;

for (i = 0; i < total; i++)

{

//Find out which 3x3 block you are in

int block = FindBlock(i);

//Get (left, top) origin point for your 3x3 block

Point blockOrigin = GetOrigin(block, blocksInRow, new Size(300,300));

//Get (left, top) origin point for the element inside its 3x3 block

int numInBlock = i-9*block;

Point cellOrigin = GetOrigin(numInBlock, 3, new Size(100,100));

//Arrange child

//Get desired height and width. This will not be larger than 100x100 as set in MeasureOverride.

double dw = mychildren[i].DesiredSize.Width;

double dh = mychildren[i].DesiredSize.Height;

                

                mychildren[i].Arrange(new Rect(blockOrigin.X + cellOrigin.X, blockOrigin.Y + cellOrigin.Y, dw, dh));

                //Determine the maximum width and height needed for the panel

                maxWidth = Math.Max(blockOrigin.X + 300, maxWidth);

                maxHeight = Math.Max(blockOrigin.Y + 300, maxHeight);

            }

            //Return final size of the panel

            return new Size(maxWidth,maxHeight);

        }

}

Basically what I'm doing is calculating for each element:

1. which 3x3 block its in

2. which cell number in that 3x3 block its in

3. the location of the Left, Top corner of the 3x3 block

4. the location of the Left, Top corner of the cell inside the 3x3 block

5. the desired height and width of that element

Once I have all these pieces of information, then I can compute location and size of the bounding box for each child element. That bounding box is the Rect that I pass to Arrange.

I also keep track of the width and height needed to contain all the 3x3 blocks. Then I set the final size of the panel to be just big enough to contain all the blocks. I could also return finalSize and then my panel would take up the entire area available to it from its parent container.

So, a couple of points that had me confused for a while:

  1. When you call child.Measure, you are passing in the size that is available to that child. You are not actually setting the size of the child.
  2. After you call Measure, the layout system will determine the desired size (desiredSize) of the element. As far as I can tell, there is no way for you to set the desired size yourself.
  3. When you call child.Arrange, you are not setting the final size of the child. I thought for a while that this would set the size of the child, but it sets the bounding box that will contain the child. So, if the child is bigger than the bounding box, it gets clipped. If the child is smaller, it is positioned in the bounding box based on the default behavior or whatever alignment you have specified. Note that in my case, I set the bounding box to be the desired size of the child.

I've attached the rest of the code which includes the methods I'm calling from ArrangeOverride and a XAML file that creates my panel and puts some rectangles in it. There are some details of the layout system that I haven't really gotten into in this example, so I recommend reading Object Positioning and Layout if you haven't already. Also, you will need to map your namespace to create your custom panel in XAML, so check out Mapping to Custom Classes and Assemblies.

 

Margaret

CustomPanelFiles.zip