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.
The first item means that children
of a Canvas
are always displayed in their smallest possible sizes, which is nothing
at all for an Ellipse and Rectangle, and the
native pixel size of a bitmap for an Image.
Any HorizontalAlignment of VerticalAlignment
properties set on children of a Canvas
have no effect.
The second item implies that Canvas has no
footprint of its own in the Silverlight layout system. (You can override
that with explicit Width
or Height settings on the Canvas.) This is
actually very useful in some circumstances where you want an element to
exist somewhere “outside” of the layout system and not affect the positioning of other elements.
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.
Example 1. Silverlight
Project: EllipseChain File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1"> <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:
The Canvas is ideal
for the arbitrary positioning
of elements, which of course is much more associated with vector
graphics programming than with control layout.
But get a load of that
odd-looking syntax, rather different from anything in XAML I’ve yet
described:
<Ellipse Style="{StaticResource ellipseStyle}"
Canvas.Left="190" Canvas.Top="107" />
Those Left and Top properties position
the upper-right corner of the element relative to the upper-right
corner of the Canvas.
The properties appear to be defined by the Canvas class, and yet they are set on the Ellipse element! When I
first saw this syntax many years ago, I was baffled. Why does the Canvas class need to
define Left and Top properties? Shouldn’t FrameworkElement define these properties?
Of course, in graphical
programming environments of days gone by, everybody has Left and Top properties because that’s how the system
works.
But it doesn’t quite make sense for Silverlight. Canvas needs for its children to have Left and Top properties set, but other panels do not. In
fact, other panels—including custom panels that you have yet to write or
even conceive—might need quite different properties set on their
children.
For this reason, Silverlight
supports the concept of attached properties.
The Left and Top properties are indeed defined by the Canvas classbut you set these properties on the
children on the Canvas.
(You can set them on elements that are not actually children of a Canvas, but they
will be ignored.)
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:
Example 2. Silverlight
Project: EllipseMesh File: MainPage.xaml (excerpt)
<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:
Example 3. Silverlight
Project: EllipseMesh File: MainPage.xaml.cs (excerpt)
public partial class 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:
These two statements set the Left and Top
attached properties:
Canvas.SetLeft(ellipse, x);
Canvas.SetTop(ellipse, y);
These are two static
methods defined by the Canvas class. You can call these methods either before or
after you add the child to the Children collection of the Canvas. Because these
methods are static, you can even call them when a Canvas object does
not yet exist.
Even more revealing is
knowing how these two static methods are defined in the Canvas class. Right in the EllipseMesh
program you can replace the two static method calls with the following statements:
ellipse.SetValue(Canvas.LeftProperty, x);
ellipse.SetValue(Canvas.TopProperty, y);
These equivalent calls make it clear that something is actually
being set on the Ellipse objects. The SetValue method is defined
by DependencyObject—a very basic class in the Silverlight class
hierarchy—and LeftProperty and RightProperty are
(despite their names) actually static fields of type DependencyProperty defined by Canvas.
My guess is that SetValue
accesses an internal dictionary created and maintained by DependencyObject where the first argument to SetValue is the
dictionary key and the second is the value. When Canvas is laying out its
children in its ArrangeOverride method, it can access these values for a
particular child element using either:
double x = GetLeft(child);
double y = GetTop(child);
or the equivalent:
double x = (double)child.GetValue(LeftProperty);
double y = (double)child.GetValue(TopProperty);
The GetValue method accesses the internal dictionary in the
child and returns an object of type object that needs to be cast here to a double.
Watch out: I described how to
replace the Canvas.SetLeft and Canvas.SetTop
calls in EllipseMesh with equivalent calls to SetValue.
But this call:
Canvas.SetLeft(ellipse, 57);
is not equivalent to
this call:
ellipse.SetValue(Canvas.LeftProperty, 57);
The second argument of Canvas.SetLeft is defined to be of type double
but the second argument of the general-purpose SetValue method is defined to be of type object. When the C# compiler parses that SetValue call it will assume the number is
an int. Only at
runtime will the error be caught. You can avoid the problem by making
it explicitly a double:
ellipse.SetValue(Canvas.LeftProperty, 57.0);
Although we speak of the Left
and Top attached
properties of Canvas, nothing defined
by Canvas is actually named Left or Top!
Canvas defines static fields named LeftProperty and TopProperty,
and static methods named SetLeft, SetTop,
GetLeft and GetTop, but nothing
named Left or Top.
The XAML syntax shown here
<Ellipse Style="{StaticResource ellipseStyle}"
Canvas.Left="190" Canvas.Top="107" />
is actually rendered by making calls to Canvas.SetLeft
and Canvas.SetTop.
You’ll see other attached
properties around. The standard MainPage.xaml file has an attached
property set on its root element:
shell:SystemTray.IsVisible="True"
In fact, the entire SystemTray class exists for the sole purpose of defining this
attached property so you can set it on the PhoneApplicationPage
derivative. It’s probably the PageApplicationFrame that hunts for this property on each page to
determine whether the system tray should be visible.