Keeping Canvas (or any other Element) from rendering outside its Bounds in UWP
Canvas
is a panel that enables absolute layouting. It therefore renders its children, per default, even if they are outside of its dimensions, as indicated by the documentation:
The children of a Canvas (if any) are still visible even if the Canvas has any of these conditions:
● Height or Width property of the Canvas is 0.
Sometimes this is not the desired behavior. For example, if the canvas is not the only UI element in your application, it would be pretty annoying if its children would overlap the other (more important) elements (like menus, data display and so on).
To achieve that, clipping has to be enabled. In UWP, setting the Clip
property does exactly that.
The most common usecase is to clip at the bounds of the canvas. In WPF, there was a special property for that, ClipToBounds
. Unfortunately, this property does not exist in UWP. But, happy for us, there are workarounds. I'll present 2 of them in this article.
Handling Canvas' Events
Probably the easiest way is to just handle the events Canvas
raises and set the Clip
property to the current size in the handlers.
In your code behind you would then have something like this:
private static void Clip(FrameworkElement element)
{
var clip = new RectangleGeometry { Rect = new Rect(0, 0, element.ActualWidth, element.ActualHeight) };
element.Clip = clip;
}
}
public CanvasContainingUserControl(){
canvas.Loaded += (s, e) => Clip(canvas);
canvas.SizeChanged += (s, e) => Clip(canvas);
}
This works, but is not particular well crafted code. It has to be repeated for every element (UserControl, Page, Window) that contains a Canvas
.
A simple fix would be to create a ClippingCanvas
, which inherits from Canvas
and adds exactly that behavior. A straightforward implementation of it would be this:
public class ClippingCanvas : Canvas
{
private static void Clip(FrameworkElement element)
{
var clip = new RectangleGeometry { Rect = new Rect(0, 0, element.ActualWidth, element.ActualHeight) };
element.Clip = clip;
}
}
public ClippingCanvas(){
this.Loaded += (s, e) => Clip(this);
this.SizeChanged += (s, e) => Clip(this);
}
}
This class can now be used instead of Canvas
to stop rendering outside its dimensions.
But it still is not the best way to achieve this. This code has to be repeated for each class that exhibits the same behavior as canvas. The next section implements that behavior with maximum code reuse using attached properties.
Adding an Attached Property
Attached properties allow implementing a behavior once which can then be reused for a wide variety of elements.
In this case, I created a ClipToBounds
class, which exposes exactly one attached property (also called ClipToBounds
).
public class ClipToBounds
{
public static bool GetClipToBounds(DependencyObject obj)
{
return (bool)obj.GetValue(ClipToBoundsProperty);
}
public static void SetClipToBounds(DependencyObject obj, bool value)
{
obj.SetValue(ClipToBoundsProperty, value);
}
// Using a DependencyProperty as the backing store for ClipToBounds. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ClipToBoundsProperty =
DependencyProperty.RegisterAttached("ClipToBounds", typeof(bool), typeof(ClipToBounds), new PropertyMetadata(false, ClipToBoundsChanged));
private static void ClipToBoundsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//use FrameworkElement because it is the highest abstraction that contains safe size
//UIElement does not contain save size data
var element = d as FrameworkElement;
if (element != null)
{
element.Loaded += (s, evt) => ClipElement(element);
element.SizeChanged += (s, evt) => ClipElement(element);
}
}
private static void ClipElement(FrameworkElement element)
{
if (GetClipToBounds(element))
{
var clip = new RectangleGeometry { Rect = new Rect(0, 0, element.ActualWidth, element.ActualHeight) };
element.Clip = clip;
}
}
}
It does essentially the same thing as described in Handling Canvas' Events, but is implemented in a more reusable way.
When ClipToBounds
is set to true, ClipToBoundsChanged
is invoked with the element on which the attached property was set as first parameter. DependencyObject
has no notion of clipping, but FrameworkElement
does, which is a subclass of DependencyObject
. Therefore, it first has to be checked if it is a FrameworkElement
.
After that, event handlers for Loaded
and SizeChanged
are added, just like before.
If you have that class included, you can set the attached property (assuming local
references the namespace in which the class ClipToBounds
is defined):
<Canvas local:ClipToBounds.ClipToBounds="True">
</Canvas>
Aside:
Technically, UIElement
, which is the base class for FrameworkElement
implements the Clip
property. So it would be event better if this class could be used. Unfortunately this is not possible, because there is no save way to get its size. Its RenderSize
property sounds like it, but its documentation explains that it should not be used for this purpose:
RenderSize is not the property to use to obtain size information about a UI element for most scenarios, because in the current implementation it doesn't have a safe technique for knowing when the value is current.