With Silverlight, Panel
s do not clip their contents by default. See the following example:
Where we have a Grid
containing another Grid
which itself contains an ellipse, and a Canvas
which contains an ellipse:
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Background="Blue" Margin="20">
<Grid Background="Yellow" Margin="20,40,-20,20">
<Ellipse Fill="LightGreen" Width="80"
Height="80" Margin="-40, -40, 0, 0"/>
</Grid>
</Grid>
<Canvas Grid.Column="1" Background="Aqua" Margin="20" >
<Ellipse Fill="Red" Canvas.Top="-10"
Canvas.Left="-10" Width="130" Height="130"/>
</Canvas>
</Grid>
Often this is not the desired effect (although it is actually quite a useful feature of Canvas
; You can simply add a Canvas
to your visual tree without explicitly or implicitly setting its Size
and use it as a mechanism for absolute positioning its children).
Fortunately, Silverlight provides a Clip
property on UIElement
, allowing you to provide the clipping geometry for the element:
<Grid Width="200" Height="100">
<Grid.Clip>
<RectangleGeometry Rect="0, 0, 200, 100"/>
</Grid.Clip>
</Grid>
The above example creates a clipping geometry which matches the rectangular geometry of the Grid
itself. Clearly more funky clipping geometries can be created, allowing for really cool effects, however most of the time I simply want my Panel
clipped so that its children cannot escape!
The above example has a few problems. Firstly, it is a bit long-winded having to explicitly create the geometry each time I want to clip; Secondly, if my Grid
’s size is calculated from its parent’s layout, how can I define the clip geometry in my XAML? Finally, if my Grid
’s geometry changes, its clipped geometry does not change.
In order to solve this problem, I created a simple little attached behaviour, which allows you to define the clipping using the attached property Clip.ToBounds
as illustrated below:
<UserControl x:Class="SilverlightClipToBounds.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:util="clr-namespace:Util" Width="300" Height="200">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Background="Blue"
Margin="20" util:Clip.ToBounds="true">
<Grid Background="Yellow"
Margin="20,40,-20,20" util:Clip.ToBounds="true">
<Ellipse Fill="LightGreen" Width="80"
Height="80" Margin="-40, -40, 0, 0"/>
</Grid>
</Grid>
<Canvas Grid.Column="1" Background="Aqua"
Margin="20" util:Clip.ToBounds="true">
<Ellipse Fill="Red" Canvas.Top="-10"
Canvas.Left="-10" Width="130" Height="130"/>
</Canvas>
</Grid>
</UserControl>
The result can be seen below:
And here is the code for the attached behaviour itself:
public class Clip
{
public static bool GetToBounds(DependencyObject depObj)
{
return (bool)depObj.GetValue(ToBoundsProperty);
}
public static void SetToBounds(DependencyObject depObj, bool clipToBounds)
{
depObj.SetValue(ToBoundsProperty, clipToBounds);
}
public static readonly DependencyProperty ToBoundsProperty =
DependencyProperty.RegisterAttached("ToBounds", typeof(bool),
typeof(Clip), new PropertyMetadata(false, OnToBoundsPropertyChanged));
private static void OnToBoundsPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
FrameworkElement fe = d as FrameworkElement;
if (fe != null)
{
ClipToBounds(fe);
fe.Loaded += new RoutedEventHandler(fe_Loaded);
fe.SizeChanged += new SizeChangedEventHandler(fe_SizeChanged);
}
}
private static void ClipToBounds(FrameworkElement fe)
{
if (GetToBounds(fe))
{
fe.Clip = new RectangleGeometry()
{
Rect = new Rect(0, 0, fe.ActualWidth, fe.ActualHeight)
};
}
else
{
fe.Clip = null;
}
}
static void fe_SizeChanged(object sender, SizeChangedEventArgs e)
{
ClipToBounds(sender as FrameworkElement);
}
static void fe_Loaded(object sender, RoutedEventArgs e)
{
ClipToBounds(sender as FrameworkElement);
}
}
When the ToBounds
property is associated with an element, ClipToBounds
is invoked to create a rectangular clip geometry. We also add event handlers for Loaded
, which is a very useful event which is fired when an element has been laid out and rendered, in other words, its size will have been computed, and SizeChanged
. In the event handlers for both, we simply update the clipping geometry.
This can be seen in action here, where clicking on the Grid
s or Canvas
increases their size, with the clipping geometry growing accordingly:
[CodeProject does not support Silverlight applets, see it in action on my blog.]
You can download a demo project here.
Can I clip it?, Yes, you can!
Regards,
Colin E.