<

Write a custom Panel in WPF

Published on
11,302 Points
5,202 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
Author:saragani
Enjoy this complimentary article view.

Get unlimited access to our entire library of technical procedures, guides, and tutorials written by certified industry professionals.

Get 7 days free
Click here to view the full article

Using this article for work? Experts Exchange can benefit your whole team.

Learn More
COLLABORATE WITH CERTIFIED PROFESSIONALS
Experts Exchange is a tech solutions provider where users receive personalized tech help from vetted certified professionals. These industry professionals also write and publish relevant articles on our site.
Ask questions about what you read
If you have a question about something within an article, you can receive help directly from the article author. Experts Exchange article authors are available to answer questions and further the discussion.
Learn from the best.