Shadows in Windows (UWP) XAML Applications – Part 4 – Custom Shadows

In part 2 of this series of posts on Shadows in Windows (UWP) XAML Applications (parts 1, 1b, 2 and 3) we saw that the composition APIs could be used to generate a DropShadow. However, what wasn’t immediately clear is that this mechanism only works for a limited set of controls, namely Shape (including Ellipse, Line, Path, Polygon, Polyline, Rectangle), Image and TextBlock. This is because these controls are the only ones that expose the GetAlphaMask method (and frustratingly this method isn’t even part of a common interface that the controls share). This challenge has previously been pointed out by Mike Taulty in his post about creating shadows, back in 2016. I’m still amazed that there doesn’t seem to be any improvement on this over 4 years later (and no, the ThemeShadow, as I pointed out in my post, isn’t the solution to this problem).

So how do we create a shadow for elements that don’t have the GetAlphaMask? Well the answer presented by Mike was to generate an image from the element, and then use a CompositionBrush generated from the image in order to define the mask for the DropShadow. To do this, we’ll start by adding the CompositionImageBrush to our application (code taken from Mike’s post).

public class CompositionImageBrush : IDisposable
{
    CompositionGraphicsDevice graphicsDevice;
    CompositionDrawingSurface drawingSurface;
    CompositionSurfaceBrush drawingBrush;

    public CompositionBrush Brush => drawingBrush;

    private CompositionImageBrush()
    {
    }

    private void CreateDevice(Compositor compositor)
    {
        graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(
            compositor, CanvasDevice.GetSharedDevice());
    }

    private void CreateDrawingSurface(Size drawSize)
    {
        drawingSurface = graphicsDevice.CreateDrawingSurface(
            drawSize,
            DirectXPixelFormat.B8G8R8A8UIntNormalized,
            DirectXAlphaMode.Premultiplied);
    }

    private void CreateSurfaceBrush(Compositor compositor)
    {
        drawingBrush = compositor.CreateSurfaceBrush(drawingSurface);
    }

    public static CompositionImageBrush FromBGRASoftwareBitmap(
        Compositor compositor,
        SoftwareBitmap bitmap,
        Size outputSize)
    {
        CompositionImageBrush brush = new CompositionImageBrush();

        brush.CreateDevice(compositor);

        brush.CreateDrawingSurface(outputSize);
        brush.DrawSoftwareBitmap(bitmap, outputSize);
        brush.CreateSurfaceBrush(compositor);

        return (brush);
    }

    private void DrawSoftwareBitmap(SoftwareBitmap softwareBitmap, Size renderSize)
    {
        using (var drawingSession = CanvasComposition.CreateDrawingSession(drawingSurface))
        using (var bitmap = CanvasBitmap.CreateFromSoftwareBitmap(drawingSession.Device, softwareBitmap))
        {
            drawingSession.DrawImage(bitmap,
                new Rect(0, 0, renderSize.Width, renderSize.Height));
        }
    }
        
    public void Dispose()
    {
        drawingBrush.Dispose();
        drawingSurface.Dispose();
        graphicsDevice.Dispose();
    }
}

Next, we’re going to create an extension method for UIElement that will either return the result from GetAlphaMask for those elements where it’s defined, or it will return a brush generated from taking an image shapshot of the element.

public static class UIElementHelpers
{
    public static async Task<CompositionBrush> ShadowAlphaMask(this UIElement uiElement)
    {
        CompositionBrush mask = null;
        if (uiElement is Shape shapeElement)
        {
            mask = shapeElement.GetAlphaMask();
        }
        else if (uiElement is Image imageElement)
        {
            mask = imageElement.GetAlphaMask();
        }
        else if (uiElement is TextBlock textElement)
        {
            mask = textElement.GetAlphaMask();
        }
        else if (uiElement is FrameworkElement frameworkElement)
        {
            var gridVisual = ElementCompositionPreview.GetElementVisual(uiElement);
            var elementVisual = gridVisual.Compositor.CreateSpriteVisual();
            elementVisual.Size = uiElement.RenderSize.ToVector2();
            var bitmap = new RenderTargetBitmap();
            await bitmap.RenderAsync(
                uiElement,
                (int)frameworkElement.ActualWidth,
                (int)frameworkElement.ActualHeight);
            var pixels = await bitmap.GetPixelsAsync();
            using (var softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(
                pixels,
                BitmapPixelFormat.Bgra8,
                bitmap.PixelWidth,
                bitmap.PixelHeight,
                BitmapAlphaMode.Premultiplied))
            {
                var brush = CompositionImageBrush.FromBGRASoftwareBitmap(
                    gridVisual.Compositor,
                    softwareBitmap,
                    new Size(bitmap.PixelWidth, bitmap.PixelHeight));
                mask = brush.Brush;
            }
        }
        return mask;
    }
}

Lastly we need to modify the code for generating the DropShadow to use this new extension method.

private async void Grid_Loaded(object sender, RoutedEventArgs e)
{
    var shadowColor = (Resources["ApplicationForegroundThemeBrush"] as SolidColorBrush).Color;
    var compositor = ElementCompositionPreview.GetElementVisual(Host).Compositor;

    // Create the drop shadow
    var dropShadow = compositor.CreateDropShadow();
    dropShadow.Color = shadowColor;
    dropShadow.BlurRadius = 16;
    dropShadow.Opacity = 20.0f;

    // Use the shape of the element (in this case ShadowContent) to 
    // control shape of shadow
    var mask = await ShadowContent.ShadowAlphaMask();
    dropShadow.Mask = mask;

    // Set the shadow on the visual
    var spriteVisual = compositor.CreateSpriteVisual();
    spriteVisual.Size = new Vector2((float)Host.ActualWidth, (float)Host.ActualHeight);
    spriteVisual.Shadow = dropShadow;
    ElementCompositionPreview.SetElementChildVisual(Host, spriteVisual);
}

Previously we were generating the shadow for an element called Rectangle2 – we’ve updated the code to use ShadowContent and the XAML now looks like.

<Grid Margin="50"
        Height="200"
        Width="200"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Left">
    <Grid x:Name="Host" />
    <StackPanel x:Name="ShadowContent"
                Background="Pink">
        <Rectangle x:Name="Rectangle2"
                    Fill="Turquoise" />
        <TextBox />
        <Button Content="Press me!" />
    </StackPanel>
</Grid>

Ok, so I guess the only thing let to do is to run the application and show you the output.

And there you have it – a nice, easy to use extension method can can return a brush from any element that can be used to mask the DropShadow.

Leave a comment