Reader Level:
ARTICLE

Panels in Layout Mechanism for Windows Phone 7

Posted by Charles Petzold Articles | Windows Phone December 06, 2010
Panels are written entirely in code. There is no XAML involved. When you write a Panel derivative, you’ll probably be defining a couple properties to make the panel more flexible.
  • 0
  • 0
  • 6084
Download Files:
 

This chapter is taken from book "Programming Windows Phone 7" by Charles Petzold published by Microsoft press. http://www.charlespetzold.com/phone/index.html

Panels are written entirely in code. There is no XAML involved. When you write a Panel derivative, you'll probably be defining a couple properties to make the panel more flexible. Because these properties are almost always dependency properties, apart from defining those custom properties, a panel always overrides two methods: MeasureOverride and ArrangeOverride, which correspond to the two passes of layout. The first pass is for each parent to determine the size of its children; the second pass is for the parent to arrange its children relative to itself.

For both these jobs, the panel accesses the Children property that your panel inherits from Panel.

MeasureOverride and ArrangeOverride are protected virtual methods. Measure and Arrange are public sealed methods. Your panel overrides MeasureOverride and ArrangeOverride. In MeasureOverride, the panel calls Measure on all its children; within ArrangeOverride the panel calls Arrange on all its children.

A panel does not need to worry about the following properties that might be set on itself or its children:

  • HorizontalAlignment and VerticalAlignment
  • Margin
  • Visibility
  • Opacity (does not affect layout at all)
  • RenderTransform (does not affect layout at all)
  • Height, MinHeight, and MaxHeight
  • Width, MinWidth, and MaxWidth

A Single-Cell Grid Clone

Perhaps the simplest panel of all is the Grid that contains no rows or columns, commonly referred to as a "single-cell Grid." I've been using the Grid named ContentPanel as a single-cell Grid; as you've seen, the Grid can host multiple children, but they overlap within the same area. Let's duplicate the functionality of a single-cell Grid with a class named SingleCellGrid.

In a new project named SingleCellGridDemo, I right-clicked the project name, selected Add and New Item from the menu, and picked Class from the dialog box, naming it SingleCellGrid.cs. In the file, I made sure the class was public and derived from Panel.

This class overrides the two methods MeasureOverride and ArrangeOverride. Here's:

namespace SingleCellGridDemo
{
    public classSingleCellGrid :Panel
    {
        protected override Size MeasureOverride(Size availableSize)
        {
            Size compositeSize =new Size();
            foreach (UIElement child in Children)
            {
                child.Measure(availableSize); 
                compositeSize.Width = Math.Max(compositeSize.Width, child.DesiredSize.Width);
                compositeSize.Height = Math.Max(compositeSize.Height, child.DesiredSize.Height);
            } 
            return compositeSize;
        } 
        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach (UIElement child in Children)
            {
                child.Arrange(newRect(newPoint(), finalSize));
            } 
            return base.ArrangeOverride(finalSize);
        }
    }
}

Now to test it out. The MainPage.xaml file in the SingleCellGridDemo project needs to reference this custom class. In the root element, an XML namespace declaration associates the name "local" with the .NET namespace used by the project:

        xmlns:local="clr-namespace:SingleCellGridDemo"

The MainPage.xaml file nests the SingleCellGrid in the content grid, and then fills it with the same four elements:

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <local:SingleCellGrid>
                <TextBlock Text="TextBlock aligned at right bottom"
                           HorizontalAlignment="Right"
                           VerticalAlignment="Bottom" /> 
                <Image Source="Images/BuzzAldrinOnTheMoon.png" /> 
                <Ellipse Stroke="{StaticResource PhoneAccentBrush}"
                         StrokeThickness="24" /> 
                <TextBlock Text="TextBlock aligned at left top"
                           HorizontalAlignment="Left"
                           VerticalAlignment
="Top" />
            </local:SingleCellGrid>
        </Grid>

A Custom Vertical StackPanel

The next Panel derivative I'll show you is the StackPanel, and you'll see how it differs from the single-cell Grid. To keep the code simple, and to avoid defining properties, I'm going to call this custom class VerticalStackPanel. Here's the MeasureOverride method:

protectedoverrideSize MeasureOverride(Size availableSize)
{
    Size compositeSize = newSize();
    foreach (UIElement child in Children)
    {
        child.Measure(
newSize(availableSize.Width,Double.PositiveInfinity));
        compositeSize.Width =
Math.Max(compositeSize.Width, child.DesiredSize.Width);
        compositeSize.Height += child.DesiredSize.Height;
    }
    return compositeSize;
}

As usual, the MeasureOverride method loops through all its children and calls Measure on each of them. But notice that the Size offered to each child here consists of the width of the VerticalStackPanel itself and a height of infinity.

In SingleCellGrid, the ArrangeOverride method positioned each of its children in the same location. The VerticalStackPanel needs to stack its children. For that reason, it defines local variables named x and y:

protectedoverrideSize ArrangeOverride(Size finalSize)
{
    double x = 0, y = 0;
    foreach (UIElement child in Children)
   {
       child.Arrange(
newRect(x, y, finalSize.Width, child.DesiredSize.Height));
       y += child.DesiredSize.Height;
   }
    returnbase.ArrangeOverride(finalSize);
}

The x variable remains 0 throughout but the y variable is incremented based on the Height property of each child's DesiredSize. The Arrange measure is called with x and y indicating the location of the child relative to the panel's upper-left corner. The MainPage.xaml file in the VerticalStackPanelDemo project is the same as the one I showed at the outset of this chapter but using VerticalStackPanel:

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <local:VerticalStackPanel>
                <TextBlock Text="TextBlock aligned at right bottom"
                           HorizontalAlignment="Right"
                           VerticalAlignment
="Bottom" />
                <Image Source="Images/BuzzAldrinOnTheMoon.png" /> 
                <Ellipse Stroke="{StaticResource PhoneAccentBrush}"
                         StrokeThickness="24" /> 
                <TextBlock Text="TextBlock aligned at left top"
                           HorizontalAlignment="Left"
                           VerticalAlignment
="Top" />
            </local:VerticalStackPanel>
        </Grid>

The Retro Canvas

The Canvas is certainly the most old-fashioned sort of panel. To position elements within the Canvas you supply horizontal and vertical coordinates relative to the top-left corner.

The Canvas has two unusual characteristics:

  • In its MeasureOverride method, Canvas always calls Measure on its children with a size consisting of both an infinite width and an infinite height. (Accordingly, in ArrangeOverride,Canvas sizes each child based on the child's DesiredSize.)
  • From its MeasureOverride method, Canvas returns a size consisting of a zero width and a zero height.

Here's a program that uses a Canvas to display seven Ellipse elements in a type of overlapping chain in the shape of a catenary. A Style object (defined in the Resources collection of the Canvas itself) gives each Ellipse a finite Width and Height; otherwise they would not show up at all.

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="1 0 0 0">
            <Canvas>
                <Canvas.Resources>
                    <Style x:Key="ellipseStyle"
                           TargetType
="Ellipse">
                        <Setter Property="Width" Value="100" />
                        <Setter Property="Height" Value="100" />
                        <Setter Property="Stroke" Value="{StaticResource PhoneAccentBrush}" />
                        <Setter Property="StrokeThickness" Value="10" />
                    </Style>
                </Canvas.Resources>               
               
<Ellipse Style="{StaticResource ellipseStyle}"
                         Canvas.Left="0" Canvas.Top="0" />
                <Ellipse Style="{StaticResource ellipseStyle}"               
                         Canvas.Left="52" Canvas.Top
="53" />
          
               
<Ellipse Style="{StaticResource ellipseStyle}"
                         Canvas.Left="116" Canvas.Top="92" /> 
                <Ellipse Style="{StaticResource ellipseStyle}"
                         Canvas.Left="190" Canvas.Top="107" /> 
                <Ellipse Style="{StaticResource ellipseStyle}"
                         Canvas.Left="263" Canvas.Top="92" /> 
                <Ellipse Style="{StaticResource ellipseStyle}"
                         Canvas.Left="326" Canvas.Top="53" /> 
                <Ellipse Style="{StaticResource ellipseStyle}"
                         Canvas.Left="380" Canvas.Top="0" />
            </Canvas>
        </Grid>

Notice I've removed the Margin on the content panel so the math comes out to 480. Here's what it look like:

np0.gif

It's instructive to look at a program that sets these attached properties in code. The EllipseMesh program creates a bunch of overlapping ellipses in the content grid. The XAML file has an empty Canvas with a SizeChanged event handler assigned:

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Canvas Name="canvas"
                        SizeChanged="OnCanvasSizeChanged" />
        </Grid>

Although Canvas has no footprint in the layout system, it still has a size and a SizeChanged event. With every SizeChanged call, the event handler empties out the Canvas (just for convenience) and fills it up again with new Ellipse objects:

namespace EllipseMesh
{
    public partialclass MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
        }
        void OnCanvasSizeChanged(object sender, SizeChangedEventArgs args)
        {
            canvas.Children.Clear(); 
            for (double y = 0; y < args.NewSize.Height; y += 75)
                for (double x = 0; x < args.NewSize.Width; x += 75)
                {
                    Ellipse ellipse =new Ellipse
                    {
                        Width = 100,
                        Height = 100,
                        Stroke = this.Resources["PhoneAccentBrush"]as Brush,
                        StrokeThickness = 10
                    };
                    Canvas.SetLeft(ellipse, x);
                    Canvas.SetTop(ellipse, y); 
                    canvas.Children.Add(ellipse);
                }
        }
    }
}

Here's what it looks like:

np1.gif

Canvas and ZIndex

The Canvas has a third attached property named ZIndex that you can use to override the default layering of elements, the name refers to the imaginary Z axis that extends out from the screen. Elements with higher Z indices appear on top of (and might even completely obscure) siblings with lower Z indices. If two siblings have the same Canvas.ZIndex attached property-and by default no element has a Canvas.ZIndex value and hence is assumed to have a value of zero- then the ordering in the Children collection is used instead.

Canvas and Touch

You can also move elements around a Canvas by setting the Left and Top attached properties in code. Here's a simple program called TouchCanvas. A Canvas hosts three Ellipse elements colored red, green, and blue:

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Canvas Name="canvas">
                <Ellipse Canvas.Left="50"
                         Canvas.Top="50"
                         Width="100"
                         Height="100"
                         Fill="Red" />               
               
<Ellipse Canvas.Left="150"
                         Canvas.Top="150"
                         Width="100"
                         Height="100"
                         Fill="Green" />
                <Ellipse Canvas.Left="250"
                         Canvas.Top="250"
                         Width="100"
                         Height="100"
                         Fill="Blue" />
            </Canvas>
        </Grid>

The code file overrides the OnManipulationStarted and OnManipulationDelta methods in MainPage. Setting the ManipulationContainer property to the Canvas in the first override isn't strictly required.

namespace TouchCanvas
{
    public partialclass MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
        } 
        protected override void OnManipulationStarted(ManipulationStartedEventArgs args)
        {
            args.ManipulationContainer = canvas;
            base.OnManipulationStarted(args);
        } 
        protected override void OnManipulationDelta(ManipulationDeltaEventArgs args
        {
            UIElement element = args.OriginalSource asUIElement;
            Point translation = args.DeltaManipulation.Translation;
            Canvas.SetLeft(element,Canvas.GetLeft(element) + translation.X);
            Canvas.SetTop(element,Canvas.GetTop(element) + translation.Y);
 
            args.Handled = true;
            base.OnManipulationDelta(args);
        }
    }
}

The Mighty Grid

The Grid should be your default choice of panel. It is both flexible and powerful, both simple and versatile. The Grid is somewhat reminiscent of an HTML table, but with several differences: Unlike the HTML table, the Grid doesn't do formatting. It's strictly for layout. There's no concept of headers, for example, or built-in cell dividers. Also, unlike the HTML table, the use of the Grid is actually encouraged.

The Grid defines two properties named RowDefinitions and ColumnDefinitions. These are, respectively, collections of RowDefinition and ColumnDefinition objects. These objects define the height of each row and the width of each column, and you have three choices:

  • the word "Auto"
  • a fixed amount in pixels
  • an asterisk, or a number followed by an asterisk (called "star")

You indicate the particular row and column of an element with the attached properties Grid.Row and Grid.Column. Row and column numbers begin with zero at the upper-left. You can specify that a particular element occupies additional rows or additional columns with attached properties Grid.RowSpan and Grid.ColumnSpan.

Here's an example:

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>           
           
<Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>           
           
<TextBlock Grid.Row="0"
                       Grid.Column="0"
                       Grid.ColumnSpan="2"
                       Text="Heading at top of Grid"
                       HorizontalAlignment
="Center" />
           
           
<Image Grid.Row="1"
                   Grid.Column="0"
                   Source
="Images/BuzzAldrinOnTheMoon.png" />
           
           
<Ellipse Grid.Row="1"
                     Grid.Column="1"
                     Stroke="{StaticResource PhoneAccentBrush}"
                     StrokeThickness
="6" />
           
           
<TextBlock Grid.Row="2"
                       Grid.Column="0"
                       Grid.ColumnSpan="2"
                       Text="Footer at bottom of Grid"
                       HorizontalAlignment
="Center" />
        </Grid>

I just added the row and column definitions to the existing content grid. Each element in the Grid has explicit Grid.Row and Grid.Column settings, but you can omit them for values of zero. Both the TextBlock at the top and TextBlock at the bottom span the two columns to be centered in the whole grid.

The two columns were apportioned so the first column is twice as wide as the second. The width of that first column determines the size of the Image, which is then centered vertically in the cell:

np2.gif

COMMENT USING

Trending up