Write a custom Panel in WPF

AID: 5850
  • Status: Published

1310 points

  • Bysaragani
  • TypeGeneral
  • Posted on2011-05-28 at 23:29:06
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
    {
    }
                                    
1:
2:
3:

Select allOpen 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);
        }
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:

Select allOpen 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);
        }
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:

Select allOpen 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>
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:

Select allOpen 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>
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:

Select allOpen 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);
        }
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:

Select allOpen 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.
Asked On
2011-05-28 at 23:29:06ID5850
Tags

WPF

,

Custom Panel.

Topic

WPF and Silverlight

Views
710

Comments

Add your Comment

Please Sign up or Log in to comment on this article.

Join Experts Exchange Today

Gain Access to all our Tech Resources

Get personalized answers

Ask unlimited questions

Access Proven Solutions

Search 3.2 million solutions

Read In-Depth How-To Guides

1000+ articles, demos, & tips

Watch Step by Step Tutorials

Learn direct from top tech pros

And Much More!

Your complete tech resource

See Plans and Pricing

30-day free trial. Register in 60 seconds.

Loading Advertisement...

Top WPF and Silverlight Experts

  1. apeter

    11,000

    0 points yesterday

    Profile
    Rank: Sage
  2. TheLearnedOne

    7,400

    0 points yesterday

    Profile
    Rank: Savant
  3. kalpesh2804

    5,575

    0 points yesterday

    Profile
    Rank: Wizard
  4. gauthampj

    4,480

    0 points yesterday

    Profile
    Rank: Genius
  5. CodeCruiser

    4,000

    0 points yesterday

    Profile
    Rank: Genius
  6. vbigham

    4,000

    0 points yesterday

    Profile
    Rank: Master
  7. Snarf0001

    3,800

    0 points yesterday

    Profile
    Rank: Sage
  8. vipin_soft

    2,375

    0 points yesterday

    Profile
  9. nishantcomp2512

    2,200

    0 points yesterday

    Profile
    Rank: Wizard
  10. agarwalrahul

    2,100

    0 points yesterday

    Profile
    Rank: Guru
  11. mikebirt

    2,000

    0 points yesterday

    Profile
    Rank: Master
  12. alagurjan

    2,000

    0 points yesterday

    Profile
  13. sathishinfotech

    2,000

    0 points yesterday

    Profile
  14. artkairos

    2,000

    0 points yesterday

    Profile
  15. MikeToole

    1,800

    0 points yesterday

    Profile
    Rank: Genius
  16. Mohamed_allabakash

    1,500

    0 points yesterday

    Profile
  17. nepaluz

    1,000

    0 points yesterday

    Profile
    Rank: Sage
  18. ukerandi

    1,000

    0 points yesterday

    Profile
    Rank: Guru
  19. nilhan

    1,000

    0 points yesterday

    Profile
  20. MeetuChoudhary

    980

    0 points yesterday

    Profile
  21. baskar_ram

    800

    0 points yesterday

    Profile
  22. navneethegde

    750

    0 points yesterday

    Profile
    Rank: Wizard
  23. webtubbs

    350

    0 points yesterday

    Profile
    Rank: Genius
  24. saragani

    330

    0 points yesterday

    Profile
    Rank: Guru

Hall Of Fame