Silverlight VisualBrush and rounded corners

As you know Silverlight 3 doesn’t support VisualBrush, which can make things like this pretty tricky.

Here’s my attempt at a workaround 🙂  It’s a control called VisualImage which can be pointed at any element and exposes it as a WriteableBitmap.  You could bind an Image to this to create a reflection effect like this (don’t forget to look at Jeff Prosise’s sample):

image

You could even bind it to an ImageBrush… if it supported binding.  To work around this, VisualImage can be bound to the ImageBrush instead.  One application of this is for clipped, rounded corners on any element (here’s the WPF way):

image

You can try a live sample here (source on CodePlex).

Although VisualImage is a Control, it doesn’t render anything itself – it just acts as an intermediary between your visual and whatever you want to bind it to.  Here’s everything you need (also available here):

/// <summary>
/// VisualImage
/// </summary>
public class VisualImage : Control
{
    #region Visual DependencyProperty

    public static readonly DependencyProperty VisualProperty = DependencyProperty.Register(
        "Visual",
        typeof( FrameworkElement ),
        typeof( VisualImage ),
        new PropertyMetadata( OnVisualChanged ) );

    public FrameworkElement Visual
    {
        get { return (FrameworkElement)GetValue( VisualProperty ); }
        set { SetValue( VisualProperty, value ); }
    }

    private static void OnVisualChanged( DependencyObject obj, DependencyPropertyChangedEventArgs args )
    {
        var visualImage = obj as VisualImage;
        visualImage.OnVisualChanged( args );
    }

    private void OnVisualChanged( DependencyPropertyChangedEventArgs args )
    {
        if ( args.OldValue != null ) ( (FrameworkElement)args.OldValue ).SizeChanged -= VisualImage_SizeChanged;
        if ( args.NewValue != null )
        {
            var visual = (FrameworkElement)args.NewValue;
            visual.SizeChanged += VisualImage_SizeChanged;
            PrepareBitmap( (int)visual.RenderSize.Width, (int)visual.RenderSize.Height );
        }
    }

    private void VisualImage_SizeChanged( object sender, SizeChangedEventArgs e )
    {
        PrepareBitmap( (int)e.NewSize.Width, (int)e.NewSize.Height );
    }

    #endregion // Visual DependencyProperty

    #region Bitmap DependencyProperty

    public static readonly DependencyProperty BitmapProperty = DependencyProperty.Register(
        "Bitmap",
        typeof( WriteableBitmap ),
        typeof( VisualImage ),
        null );

    public WriteableBitmap Bitmap
    {
        get { return (WriteableBitmap)GetValue( BitmapProperty ); }
        set { SetValue( BitmapProperty, value ); }
    }

    #endregion // Bitmap DependencyProperty

    #region ImageBrush DependencyProperty

    public static readonly DependencyProperty ImageBrushProperty = DependencyProperty.Register(
        "ImageBrush",
        typeof( ImageBrush ),
        typeof( VisualImage ),
        null );

    public ImageBrush ImageBrush
    {
        get { return (ImageBrush)GetValue( ImageBrushProperty ); }
        set { SetValue( ImageBrushProperty, value ); }
    }

    #endregion // VisualBrush DependencyProperty

    /// <summary>
    /// Initializes a new instance of the <see cref="VisualImage"/> class.
    /// </summary>
    public VisualImage()
    {
    }

    /// <summary>
    /// Prepares the bitmap.
    /// </summary>
    /// <param name="width">The width.</param>
    /// <param name="height">The height.</param>
    private void PrepareBitmap( int width, int height )
    {
        Bitmap = new WriteableBitmap( width, height );
        Invalidate();
    }

    /// <summary>
    /// Invalidates the VisualImage and causes WriteableBitmap to be refreshed.
    /// </summary>
    public void Invalidate()
    {
        if ( Bitmap != null && Visual != null )
        {
            Array.Clear( Bitmap.Pixels, 0, Bitmap.Pixels.Length );
            Bitmap.Render( Visual, this.RenderTransform );
            Bitmap.Invalidate();

            if ( ImageBrush != null && ImageBrush.ImageSource != Bitmap )
            {
                ImageBrush.ImageSource = Bitmap;
            }
        }
    }
}

 

For performance reasons it only refreshes the WriteableBitmap when the target Visual’s size changes.  You can call the Invalidate() method to force a refresh (consider calling it from CompositionTarget.Rendering if you want it to refresh every frame).

Here’s how to get rounded corners on anything (similar to WPF technique, with added VisualImage and named ImageBrush):

                <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
                    <Border x:Name="mask" Background="White" CornerRadius="20" Padding="10"/>
                    <local:VisualImage Name="visualImage" Visual="{Binding ElementName=mask}" ImageBrush="{Binding ElementName=brush}"/>
                    <Image Source="http://farm2.static.flickr.com/1429/1430528819_edb63b79a6.jpg">
                        <Image.OpacityMask>
                            <ImageBrush x:Name="brush"/>
                        </Image.OpacityMask>
                    </Image>
                </Grid>

 

And here’s a reflection:

 

                <TextBlock x:Name="myText" FontSize="96">Hello</TextBlock>
                <local:VisualImage Name="reflectImage" Visual="{Binding ElementName=myText}"/>
                <Image Source="{Binding Bitmap, ElementName=reflectImage}" RenderTransformOrigin="0.5,0.2">
                    <Image.RenderTransform>
                        <ScaleTransform ScaleY="-0.8"/>
                    </Image.RenderTransform>
                    <Image.OpacityMask>
                        <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                            <GradientStop Offset="0" Color="#00FFFFFF"/>
                            <GradientStop Offset="1" Color="#80FFFFFF"/>
                        </LinearGradientBrush>
                    </Image.OpacityMask>
                </Image>
kick it

19 Comments

  1. Hi there,

    very good feature, but how can I set CornerRadius in code?

    It does not work for me if I set cornerRadius without binding or in code.

    Thank you.

    Reply

  2. If I try to apply a drop shadow effect to the control, it looks right in the preview window, however when I run it in browser it has weird effects along the right edge (code included below). If I comment out the Effect code then the weirdness goes away. If I instead comment out the VisualImage code it also fixes the issue. Can VisualImage and DropShadowEffect not co-exist?

    Reply

  3. <Grid
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
    xmlns:l=”clr-namespace:SLTest”
    x:Class=”SLTest.MainPage”
    Width=”180″ Height=”180″>
    <Grid.Effect>
    <DropShadowEffect BlurRadius=”5″ ShadowDepth=”5″/>
    </Grid.Effect>
    <Border Name=”mask” CornerRadius=”5″ BorderBrush=”Black” Background=”Black”/>
    <l:VisualImage x:Name=”myRoundedImage” Visual=”{Binding ElementName=mask}” ImageBrush=”{Binding ElementName=myRoundedBrush}”/>
    <Grid Background=”Gray” HorizontalAlignment=”Stretch”>
    <Grid.OpacityMask>
    <ImageBrush x:Name=”myRoundedBrush”/>
    </Grid.OpacityMask>
    <TextBlock Text=”Hello”/>
    </Grid>
    </Grid>

    Reply

    1. Ryan – Not sure if it’ll help, but did you try putting the DropShadowEffect on the “mask” border instead? Or maybe wrap the outer Grid with another grid, and give the outer its own Border with a DropShadowEffect? Thinking something like this (the outer border will stretch itself to the same size as its content)…

      Grid
      ..Border
      ….DropShadowEffect
      ..Grid
      ….Border (“mask”)
      ….Visualmage
      ….Grid
      ……OpacityMask
      ……Content…

      Reply

  4. I did try putting it on the mask before I posted and it caused the grid that i was attempting to mask with VisualImage to bleed through into the shadow (shadow area became part of the visible area of grid). I just tried your other suggestion though and it seems to be working. Placing a border in parallel with the grid containing the mask seems to fix the issue.

    Reply

  5. Hello Chris Cavanagh,
    i want to implement the VisualBrush in SL, where, I am not using reflection. I just want to create a wavy line using a path. I will show you the requirement.

    I want to paint the above visual brush repeatedly. so, that it forms a wavy line. Could please help me on this?

    Reply

Leave a reply to Tushar Cancel reply