UWP Composition - Grid with rounded corners DropSh

2020-07-18 11:16发布

问题:

I have a UWP app, which I should start by pointing out that it uses very little XAML. The views are built from JSON object recieved from an API. This means that the vast majority of everything is done in C#, and therefore adds a little complexity to my problem.

I basically want to have a panel (e.g. Grid) that can have rounded corners and have a drop shadow applied to it. The drop shadow should also have the rounded corners, this can be seen in the sample below.

I have looked at the DropShadowPanel as part of the Windows Community Toolkit, but this from what I can tell doesn't do the rounded corners unless I change the content to be a rectangle or some other shape.

To use this as a solution would mean the XAML equivalent of something like:

<Grid>
    <toolkit:DropShadowPanel>
         <Rectangle />
    <toolkit:DropShadowPanel>
    <Grid CornerRadius="30">
        <!-- My Content -->
    </Grid>
</Grid>

To me, this seems like an inefficient use of XAML!

I have also discovered the Composition Pro Toolkit, which to me looks bery interesting as it is all code behind. In particular the ImageFrame control looks to achieve the basis of what I require - although far more advanced than my needs.

The below has been based on the ImageFrame, but doesn't work (content is my grid):

protected FrameworkElement AddDropShadow(FrameworkElement content)
{
    var container = new Grid { HorizontalAlignment = content.HorizontalAlignment, VerticalAlignment = content.VerticalAlignment, Width = content.Width, Height = content.Height };

    var canvas = new Canvas { HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch };

    content.Loaded += (s, e) =>
        {
            var compositor = ElementCompositionPreview.GetElementVisual(canvas).Compositor;

            var root = compositor.CreateContainerVisual();
            ElementCompositionPreview.SetElementChildVisual(canvas, root);

            var shadowLayer = compositor.CreateSpriteVisual();
            var frameLayer = compositor.CreateLayerVisual();
            var frameContent = compositor.CreateShapeVisual();

            root.Children.InsertAtBottom(shadowLayer);
            root.Children.InsertAtTop(frameLayer);

            frameLayer.Children.InsertAtTop(frameContent);

            var rectangle = root.Compositor.CreateRoundedRectangleGeometry();
            rectangle.Size = new Vector2((float)content.ActualWidth, (float)content.ActualHeight);
            rectangle.CornerRadius = new Vector2(30f);

            var shape = root.Compositor.CreateSpriteShape(rectangle);
            shape.FillBrush = root.Compositor.CreateColorBrush(Colors.Blue);

            //var visual = root.Compositor.CreateShapeVisual();
            frameContent.Size = rectangle.Size;
            frameContent.Shapes.Add(shape);

            //create mask layer
            var layerEffect = new CompositeEffect
            {
                Mode = Microsoft.Graphics.Canvas.CanvasComposite.DestinationIn,
                Sources = { new CompositionEffectSourceParameter("source"), new CompositionEffectSourceParameter("mask") }
            };

            var layerEffectFactory = compositor.CreateEffectFactory(layerEffect);
            var layerEffectBrush = layerEffectFactory.CreateBrush();


            //CompositionDrawingSurface
            var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, new Microsoft.Graphics.Canvas.CanvasDevice(forceSoftwareRenderer: false));
            var frameLayerMask = graphicsDevice.CreateDrawingSurface(new Size(0, 0), Windows.Graphics.DirectX.DirectXPixelFormat.B8G8R8A8UIntNormalized, Windows.Graphics.DirectX.DirectXAlphaMode.Premultiplied);
            layerEffectBrush.SetSourceParameter("mask", compositor.CreateSurfaceBrush(frameLayerMask));
            frameLayer.Effect = layerEffectBrush;

            var shadow = root.Compositor.CreateDropShadow();
            //shadow.SourcePolicy = CompositionDropShadowSourcePolicy.InheritFromVisualContent;
            shadow.Mask = layerEffectBrush.GetSourceParameter("mask");
            shadow.Color = Colors.Black;
            shadow.BlurRadius = 25f;
            shadow.Opacity = 0.75f;
            shadow.Offset = new Vector3(0, 0, 0);
            shadowLayer.Shadow = shadow;

            content.Opacity = 0; //hiding my actual content to see the results of this
        };


    container.Children.Add(canvas);
    container.Children.Add(content);
    return container;
}

In these tests, I am doing the same inefficient use of object, creating another container that has both the composition canvas, and also the grid. If possible, I'd like to apply the composition directly to the original content grid.

I am completely new to composition, so any thoughts, pointers, glaring errors or solutions would be most welcomed.

A Hack Solution?

I have changed my method to the following, visually it works - but is it right?

protected FrameworkElement AddDropShadow(FrameworkElement content)
{
    var container = new Grid { HorizontalAlignment = content.HorizontalAlignment, VerticalAlignment = content.VerticalAlignment };
    var rectangle = new Rectangle { Fill = new SolidColorBrush(Colors.Transparent) };

    content.Loaded += (s, e) =>
        {
            rectangle.Fill = new SolidColorBrush(Colors.Black);
            rectangle.Width = content.ActualWidth;
            rectangle.Height = content.ActualHeight;
            rectangle.RadiusX = 30;
            rectangle.RadiusY = 30;

            var compositor = ElementCompositionPreview.GetElementVisual(rectangle).Compositor;
            var visual = compositor.CreateSpriteVisual();
            visual.Size = new Vector2((float)content.ActualWidth, (float)content.ActualHeight);

            var shadow = compositor.CreateDropShadow();
            shadow.BlurRadius = 30f;
            shadow.Mask = rectangle.GetAlphaMask();
            shadow.Opacity = 0.75f;
            visual.Shadow = shadow;

            ElementCompositionPreview.SetElementChildVisual(rectangle, visual);
        };

    container.Children.Add(rectangle);
    container.Children.Add(content);
    return container;
}

The concept here is that my container grid holds a rectangle and my content grid (or other element).

The first error of this method is that is assumes my input FrameworkElement will be rectangular. I imagine that this could be improved upon by creating a bitmap render of the content as highlighted in this blog - but this will likely be quite costly. I also have to ensure that the rectangle size and shape exactly matches that of my main content!

It feels very wrong that there is a rectangle drawn on the screen (even though hidden by my main content). The rectangle is purely there to create the alpha mask so I guess it could be scrapped if the mask is created from the renderof the content.

I've tried setting the visibility of the rectangle to collapsed to remove it from the visual tree. This means that I can attach the visual to the container instead:

ElementCompositionPreview.SetElementChildVisual(container, visual)

However, doing this means that the shadow displays in front of the main content, which means I need some other ui element to attach it too - may as well be the rectangle!

回答1:

Your solution to use Rectangle is my current workaround everywhere I need rounded shadow under Grid or Border. It's simple and it's plain, why should I complain :) But if it's not your choice you can draw a rounded rectangle and blur it:

GraphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(Compositor, CanvasDevice.GetSharedDevice());                

var roudRectMaskSurface = GraphicsDevice.CreateDrawingSurface(new Size(SurfaceWidth + BlurMargin * 2, SurfaceHeight + BlurMargin * 2), DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
using (var ds = CanvasComposition.CreateDrawingSession(roudRectMaskSurface))
{
    ds.Clear(Colors.Transparent);
    ds.FillRoundedRectangle(new Rect(BlurMargin, BlurMargin, roudRectMaskSurface.Size.Width + BlurMargin, roudRectMaskSurface.Size.Height + BlurMargin), YourRadius, YourRadius, Colors.Black);
}

var rectangleMask = Compositor.CreateSurfaceBrush(roudRectMaskSurface);

Now you can apply this surface in the EffectBrush with blur effect to obtain custom shadow.

BlurMargin - corresponds to the blur amount, you need it because your blurred surface will be bigger than initial source rectangle (to avoid blur clip).