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:
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:
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: