Edit

Share via


Composition visuals

Composition Visuals make up the visual tree structure that all other features of the Microsoft.UI.Composition API use and build on. The API allows developers to define and create one or many visual objects, each representing a single node in a visual tree.

Visuals

There are several visual types that make up the visual tree structure plus a base brush class with multiple subclasses that affect the content of a visual:

You can apply content and effects to SpriteVisuals using the CompositionBrush and its subclasses including the CompositionColorBrush, CompositionSurfaceBrush and CompositionEffectBrush. To learn more about brushes see CompositionBrush Overview.

The CompositionVisual Sample

Here, we'll look at some WinUI 3 sample code that demonstrates the three different visual types listed previously. While this sample doesn't cover concepts like animations or more complex effects, it contains the building blocks that all of those systems use. (The full sample code is listed at the end of this article.)

In the sample are a number of solid color squares that can be clicked on and dragged about the screen. When a square is clicked on, it will come to the front, rotate 45 degrees, and become opaque when dragged about.

This shows a number of basic concepts for working with the API including:

  • Creating a compositor
  • Creating a SpriteVisual with a CompositionColorBrush
  • Clipping the Visual
  • Rotating the Visual
  • Setting Opacity
  • Changing the Visual's position in the collection.

Creating a Compositor

Creating a Compositor and storing it in a variable for use as a factory is a simple task. In a WinUI app, you typically retrieve the compositor from a XAML element that is already connected to the visual tree:

Compositor compositor = ElementCompositionPreview.GetElementVisual(MyHost).Compositor;

If you need a compositor and do not have a UIElement available, you can use CompositionTarget.GetCompositorForCurrentThread() instead.

Creating a SpriteVisual and ColorBrush

Using the Compositor it's easy to create objects whenever you need them, such as a SpriteVisual and a CompositionColorBrush:

var visual = _compositor.CreateSpriteVisual();
visual.Brush = _compositor.CreateColorBrush(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF));

While this is only a few lines of code, it demonstrates a powerful concept: SpriteVisual objects are the heart of the effects system. The SpriteVisual allows for great flexibility and interplay in color, image and effect creation. The SpriteVisual is a single visual type that can fill a 2D rectangle with a brush, in this case, a solid color.

Clipping a Visual

The Compositor can also be used to create clips to a Visual. Below is an example from the sample of using the InsetClip to trim each side of the visual:

var clip = _compositor.CreateInsetClip();
clip.LeftInset = 1.0f;
clip.RightInset = 1.0f;
clip.TopInset = 1.0f;
clip.BottomInset = 1.0f;
_currentVisual.Clip = clip;

Like other objects in the API, InsetClip can have animations applied to its properties.

Rotating a Clip

A Visual can be transformed with a rotation. Note that RotationAngle supports both radians and degrees. It defaults to radians, but it's easy to specify degrees as shown in the following snippet:

child.RotationAngleInDegrees = 45.0f;

Rotation is just one example of a set of transform components provided by the API to make these tasks easier. Others include Offset, Scale, Orientation, RotationAxis, and 4x4 TransformMatrix.

Setting Opacity

Setting the opacity of a visual is a simple operation using a float value. For example, in the sample all the squares start at .8 opacity:

visual.Opacity = 0.8f;

Like rotation, the Opacity property can be animated.

Changing the Visual's position in the collection

The Composition API allows for a Visual's position in a VisualCollection to be changed in a number of ways. It can be placed above another Visual with InsertAbove, placed below with InsertBelow, moved to the top with InsertAtTop, or the bottom with InsertAtBottom.

In the sample, a Visual that has been clicked is sorted to the top:

parent.Children.InsertAtTop(_currentVisual);

Full Example

In the full WinUI sample, all of the concepts above are used together to construct and walk a simple tree of Visual objects to change opacity without custom DirectX rendering. The sample uses a WinUI Grid for pointer input and hosts the composition tree with ElementCompositionPreview.

<Page
    x:Class="CompositionVisualSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Grid
        x:Name="MyHost"
        Background="Transparent"
        PointerPressed="OnPointerPressed"
        PointerMoved="OnPointerMoved"
        PointerReleased="OnPointerReleased" />
</Page>
using System;
using System.Numerics;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Input;
using Windows.Foundation;
using Windows.UI;

namespace CompositionVisualSample
{
    public sealed partial class MainPage : Page
    {
        private readonly Random _random = new();
        private Compositor _compositor;
        private ContainerVisual _root;
        private ContainerVisual _currentVisual;
        private Vector2 _offsetBias;
        private bool _dragging;

        public MainPage()
        {
            InitializeComponent();
            Loaded += OnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            if (_root != null)
            {
                return;
            }

            _compositor = ElementCompositionPreview.GetElementVisual(MyHost).Compositor;
            _root = _compositor.CreateContainerVisual();
            ElementCompositionPreview.SetElementChildVisual(MyHost, _root);

            for (int index = 0; index < 20; index++)
            {
                _root.Children.InsertAtTop(CreateChildElement());
            }
        }

        private ContainerVisual CreateChildElement()
        {
            var element = _compositor.CreateContainerVisual();
            element.Size = new Vector2(100.0f, 100.0f);

            var maxX = (float)Math.Max(MyHost.ActualWidth - element.Size.X, 0.0);
            var maxY = (float)Math.Max(MyHost.ActualHeight - element.Size.Y, 0.0);
            element.Offset = new Vector3(
                (float)(_random.NextDouble() * maxX),
                (float)(_random.NextDouble() * maxY),
                0.0f);

            var outer = _compositor.CreateSpriteVisual();
            outer.Size = new Vector2(100.0f, 100.0f);
            outer.Brush = _compositor.CreateColorBrush(Colors.White);
            element.Children.InsertAtTop(outer);

            var inner = _compositor.CreateSpriteVisual();
            inner.Offset = new Vector3(3.0f, 3.0f, 0.0f);
            inner.Size = new Vector2(94.0f, 94.0f);

            byte red = (byte)(0xFF * (0.2f + (_random.NextDouble() / 0.8f)));
            byte green = (byte)(0xFF * (0.2f + (_random.NextDouble() / 0.8f)));
            byte blue = (byte)(0xFF * (0.2f + (_random.NextDouble() / 0.8f)));
            inner.Brush = _compositor.CreateColorBrush(Color.FromArgb(0xFF, red, green, blue));
            outer.Children.InsertAtTop(inner);

            element.Opacity = 0.8f;
            return element;
        }

        private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
        {
            Point position = e.GetCurrentPoint(MyHost).Position;
            _currentVisual = null;

            foreach (var child in _root.Children)
            {
                Vector3 offset = child.Offset;
                Vector2 size = child.Size;

                if ((position.X >= offset.X) &&
                    (position.X < offset.X + size.X) &&
                    (position.Y >= offset.Y) &&
                    (position.Y < offset.Y + size.Y))
                {
                    _currentVisual = child as ContainerVisual;
                    _offsetBias = new Vector2(
                        (float)(offset.X - position.X),
                        (float)(offset.Y - position.Y));
                }
            }

            if (_currentVisual != null)
            {
                ContainerVisual parent = (ContainerVisual)_currentVisual.Parent;
                parent.Children.Remove(_currentVisual);
                parent.Children.InsertAtTop(_currentVisual);
            }
        }

        private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
        {
            if (_currentVisual == null)
            {
                return;
            }

            if (!_dragging)
            {
                _currentVisual.Opacity = 1.0f;

                foreach (var child in _currentVisual.Children)
                {
                    child.RotationAngleInDegrees = 45.0f;
                    child.CenterPoint = new Vector3(_currentVisual.Size.X / 2, _currentVisual.Size.Y / 2, 0.0f);
                    break;
                }

                var clip = _compositor.CreateInsetClip();
                clip.LeftInset = 1.0f;
                clip.RightInset = 1.0f;
                clip.TopInset = 1.0f;
                clip.BottomInset = 1.0f;
                _currentVisual.Clip = clip;

                _dragging = true;
            }

            Point position = e.GetCurrentPoint(MyHost).Position;
            _currentVisual.Offset = new Vector3(
                (float)(position.X + _offsetBias.X),
                (float)(position.Y + _offsetBias.Y),
                0.0f);
        }

        private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
        {
            if (_currentVisual == null)
            {
                return;
            }

            if (_dragging)
            {
                foreach (var child in _currentVisual.Children)
                {
                    child.RotationAngle = 0.0f;
                    child.CenterPoint = new Vector3(0.0f, 0.0f, 0.0f);
                    break;
                }

                _currentVisual.Opacity = 0.8f;
                _currentVisual.Clip = null;
                _dragging = false;
            }

            _currentVisual = null;
        }
    }
}

If you need to initialize a compositor before you have a XAML host element, use CompositionTarget.GetCompositorForCurrentThread() and then attach the visuals to your UI when a host element becomes available.