<

Write a custom Panel in WPF

Published on
10,785 Points
4,685 Views
1 Endorsement
Last Modified:
Approved
For most people, the WrapPanel seems like a magic when they switch from WinForms to WPF. Most of us will think that the code that is used to write a control like that would be difficult. However, most of the work is done by the WPF engine, and the WrapPanel is basically using it.

To understand Panels in general, we will need to understand 2 basic but very important functions: Measure and Arrange. Any UIElement (or Anything that derives from it) has those 2 functions.

Measure - gets a size as a parameter and returns a size as a result. This function gets the available size of the parent and returns the desired size of the element.

Arrange - gets a Rect, which is: Top, Left, Width and Height. It is not hard to understand that the caller forces the element to be located at position Top,Left... and have the size: Width, Height.

So if we are some kind of content container, like a panel, and we have children, in order to position them we only need to call Measure and Arrange for each child.


But how do I, as a Panel, know when I need to remeasure and to rearrange the elements?? For that we have MeasureOverride and ArrangeOverride :-)

Lets have a look... Open Visual Studio and create a new WPF application. It should also work on Silverlight (But instead of a Window, we will have a Page that derives from UserControl).

Now Lets add a class that will derive from a Panel. Call it: MyWrapPanel:

    public class MyWrapPanel : Panel
    {
    }

Open in new window


Now let start putting some code inside:
        protected override Size MeasureOverride(Size availableSize)
        {
            return base.MeasureOverride(availableSize);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            return base.ArrangeOverride(finalSize);
        }

Open in new window



We are going to implement a Horizontal WrapPanel only (We can also have a vertical implementation, but it would be almost the same, and will require use to add  DependencyProperty for the Orientation).

If we will have a look on a regular WrapPanel behavior, we will see that it checks how many elements fit to a specific width. The next elements will start on the next row which will be located on "current row" + "max element height on current row".

For iterating the children of the panel, we will use this.InternalChildren. Let's have a look:

        protected override Size MeasureOverride(Size availableSize)
        {
            double x = 0.0;
            double y = 0.0;
            double largestY = 0.0;

            foreach (FrameworkElement child in this.InternalChildren)
            {
                child.Measure(availableSize);
            }

            foreach (FrameworkElement child in this.InternalChildren)
            {
                if (x + child.DesiredSize.Width > availableSize.Width)
                {
                    if (x > 0)
                    {
                        x = 0;
                        y = largestY;
                        x += child.DesiredSize.Width;
                    }
                    else
                    {
                        x += child.DesiredSize.Width;
                    }
                }
                else
                {
                    x += child.DesiredSize.Width;
                }
                largestY = Math.Max(largestY, y + child.DesiredSize.Height);
            }

            return availableSize;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            double x = 0.0;
            double y = 0.0;
            double largestY = 0.0;
            foreach (FrameworkElement child in this.InternalChildren)
            {
                if (x + child.DesiredSize.Width > finalSize.Width)
                {
                    if (x > 0)
                    {
                        x = 0;
                        y = largestY;
                        child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
                        x += child.DesiredSize.Width;
                    }
                    else
                    {
                        child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
                        x += child.DesiredSize.Width;
                    }
                }
                else
                {
                    child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
                    x += child.DesiredSize.Width;
                }
                largestY = Math.Max(largestY, y + child.DesiredSize.Height);
            }

            return new Size(finalSize.Width, largestY);
        }

Open in new window



Now for the real test we will put both WrapPanel and MyWrapPanel in the window with the same elements and see that they behave the same:
<Window x:Class="WrapPanelDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WrapPanelDemo"
        Title="MainWindow" Height="450" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
            <WrapPanel>
                <Button Width="50" Height="50" VerticalAlignment="Top">1</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">2</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">3</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">4</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">5</Button>
                <Button Width="150" Height="150" VerticalAlignment="Top">6</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">7</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">8</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">9</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">10</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">11</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">12</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">13</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">14</Button>
            </WrapPanel>

            <local:MyWrapPanel Grid.Row="1">
                <Button Width="50" Height="50" VerticalAlignment="Top">1</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">2</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">3</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">4</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">5</Button>
                <Button Width="150" Height="150" VerticalAlignment="Top">6</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">7</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">8</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">9</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">10</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">11</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">12</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">13</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">14</Button>
            </local:MyWrapPanel>
    </Grid>
</Window>

Open in new window



It looks good, but actually something is still not OK. Let's have a ScrollViewer around each of the wrap panels:
<Window x:Class="WrapPanelDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WrapPanelDemo"
        Title="MainWindow" Height="450" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <ScrollViewer>
            <WrapPanel>
                <Button Width="50" Height="50" VerticalAlignment="Top">1</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">2</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">3</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">4</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">5</Button>
                <Button Width="150" Height="150" VerticalAlignment="Top">6</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">7</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">8</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">9</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">10</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">11</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">12</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">13</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">14</Button>
            </WrapPanel>
        </ScrollViewer>

        <ScrollViewer Grid.Row="1">
            <local:MyWrapPanel >
                <Button Width="50" Height="50" VerticalAlignment="Top">1</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">2</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">3</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">4</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">5</Button>
                <Button Width="150" Height="150" VerticalAlignment="Top">6</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">7</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">8</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">9</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">10</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">11</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">12</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">13</Button>
                <Button Width="50" Height="50" VerticalAlignment="Top">14</Button>
            </local:MyWrapPanel>
        </ScrollViewer>
    </Grid>
</Window>

Open in new window



You should get an exception: Layout measurement override of element 'WrapPanelDemo.MyWrapPanel' should not return PositiveInfinity as its DesiredSize, even if Infinity is passed in as available size.

So what exactly happened here?

A ScrollViewer gives it's content the impression that it has an infinite size to spread on. In this case, the Width is 500 and the height is Infinity. In order to fix this we need change our code a little bit so we will return the max height on the Measure:

        protected override Size MeasureOverride(System.Windows.Size availableSize)
        {
            double x = 0.0;
            double y = 0.0;
            double largestY = 0.0;
            double largestX = 0.0;
            foreach (FrameworkElement child in this.InternalChildren)
            {
                child.Measure(availableSize);
            }

            foreach (FrameworkElement child in this.InternalChildren)
            {
                if (x + child.DesiredSize.Width > availableSize.Width)
                {
                    if (x > 0)
                    {
                        x = 0;
                        y = largestY;
                        x += child.DesiredSize.Width;
                    }
                    else
                    {
                        x += child.DesiredSize.Width;
                    }
                }
                else
                {
                    x += child.DesiredSize.Width;
                }
                largestY = Math.Max(largestY, y + child.DesiredSize.Height);
                largestX = Math.Max(largestX, x + child.DesiredSize.Width);
            }

            return new Size(largestX, largestY);
        }

Open in new window



Thats it,  :-)


Btw, this Panel does not support UI Virtualizartion.

For Virtualization, our Panel will first have to implement IScrollInfo. IScrollInfo will allow us to get the actual viewable area that we get (So we would not get the exception above)... but implementing a UI Virtualization (if you don't know what it is then just google) is a different story, especially if we want a smooth scrolling.
1
Comment
Author:saragani
0 Comments

Featured Post

Free Tool: Site Down Detector

Helpful to verify reports of your own downtime, or to double check a downed website you are trying to access.

One of a set of tools we are providing to everyone as a way of saying thank you for being a part of the community.

Join & Write a Comment

This is Part 3 in a 3-part series on Experts Exchange to discuss error handling in VBA code written for Excel. Part 1 of this series discussed basic error handling code using VBA. http://www.experts-exchange.com/videos/1478/Excel-Error-Handlin…
When you have multiple client accounts to manage, it often feels like there aren’t enough hours in the day. With too many applications to juggle, you can’t focus on your clients, much less your growing to-do list. But that doesn’t have to be the cas…

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month