将视觉层与 WPF 结合使用

可以在 Windows Presentation Foundation (WPF) 应用中使用 Windows 运行时 Composition API(也称为视觉层)来创建面向 Windows 用户的现代特色体验。

GitHub 上提供了本教程的完整代码:WPF HelloComposition 示例

先决条件

UWP XAML 宿主 API 的先决条件如下。

如何在 WPF 中使用 Composition API

在本教程中,你将创建一个简单的 WPF 应用 UI,并在其中添加动画合成元素。 WPF 和合成组件都比较简单,但无论组件的复杂性如何,显示的互操作代码都是相同的。 完成的应用如下所示。

The running app UI

创建 WPF 项目

第一步是创建 WPF 应用项目,包括应用程序定义以及 UI 的 XAML 页面。

若要在 Visual C# 中创建名为 HelloComposition 的新 WPF 应用程序项目:

  1. 打开 Visual Studio 并选择“文件”>“新建”>“项目”。

    此时会打开“新建项目”对话框。

  2. 在“安装”类别下展开“Visual C#”节点,然后选择“Windows 桌面”。

  3. 选择“WPF 应用(.NET Framework)”模板。

  4. 输入名称 HelloComposition,选择框架“.NET Framework 4.7.2”,然后单击“确定”。

    Visual Studio 将创建该项目,并打开名为 MainWindow.xaml 的默认应用程序窗口的设计器。

将项目配置为使用 Windows 运行时 API

若要在 WPF 应用中使用 Windows 运行时 (WinRT) API,需将 Visual Studio 项目配置为访问 Windows 运行时。 此外,Composition API 广泛使用向量,因此你需要添加所需的引用来使用向量。

NuGet 包可用于满足这两项需求。 请安装这些包的最新版本,以添加对项目的必要引用。

注意

尽管我们建议使用 NuGet 包来配置项目,但你可以手动添加所需的引用。 有关详细信息,请参阅增强适用于 Windows 的桌面应用程序。 下表显示了需要对其添加引用的文件。

文件 位置
System.Runtime.WindowsRuntime C:\Windows\Microsoft.NET\Framework\v4.0.30319
Windows.Foundation.UniversalApiContract.winmd C:\Program Files (x86)\Windows Kits\10\References<sdk version>\Windows.Foundation.UniversalApiContract<version>
Windows.Foundation.FoundationContract.winmd C:\Program Files (x86)\Windows Kits\10\References<sdk version>\Windows.Foundation.FoundationContract<version>
System.Numerics.Vectors.dll C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Numerics.Vectors\v4.0_4.0.0.0__b03f5f7f11d50a3a
System.Numerics.dll C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework.NETFramework\v4.7.2

将项目配置为感知每个监视器的 DPI

添加到应用中的视觉层内容不会根据显示这些内容的屏幕的 DPI 设置自动缩放。 需要为应用启用每个监视器的 DPI 感知,并确保在应用运行时,用于创建视觉层内容的代码考虑到当前 DPI 比例。 此处我们会将项目配置为感知 DPI。 后续部分将介绍如何使用 DPI 信息来缩放视觉层内容。

WPF 应用默认可以感知系统 DPI,但需要在 app.manifest 文件中将自身声明为感知每个监视器的 DPI。 若要在应用清单文件中启用 Windows 级别的每个监视器的 DPI 感知:

  1. 在“解决方案资源管理器”中右键单击“HelloComposition”项目。

  2. 在上下文菜单中,选择“添加”>“新建项...”。

  3. 在“添加新项”对话框中选择“应用程序清单文件”,然后单击“添加”。 (可以保留默认名称。)

  4. 在 app.manifest 文件中,找到以下 xml 并将其取消注释:

    <application xmlns="urn:schemas-microsoft-com:asm.v3">
        <windowsSettings>
          <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
        </windowsSettings>
      </application>
    
  5. 将以下设置添加到 <windowsSettings> 开始标记的后面:

          <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
    
  6. 还需要在 App.config 文件中指定 DoNotScaleForDpiChanges 设置。

    打开 App.config 并将以下 xml 添加到 <configuration> 元素中:

    <runtime>
      <AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false"/>
    </runtime>
    

注意

只能设置 AppContextSwitchOverrides 一次。 如果应用程序已有一个集,则必须在 value 特性中以分号分隔此开关。

(有关详细信息,请参阅 GitHub 上的每监视器 DPI 开发人员指南和示例。)

创建一个 HwndHost 派生类用于承载合成元素

若要使用视觉层承载创建的内容,需要创建一个派生自 HwndHost 的类。 将在此类中完成用于承载 Composition API 的大部分配置。 在此类中,使用平台调用服务 (PInvoke)COM 互操作性将 Composition API 引入 WPF 应用中。 有关 PInvoke 和 COM 互操作性的详细信息,请参阅互操作性与非托管代码

提示

如果需要,请检查本教程末尾的完整代码,确保在完成本教程的过程中,所有代码位于正确的位置。

  1. 将派生自 HwndHost 的新类文件添加到项目。

    • 在“解决方案资源管理器”中右键单击“HelloComposition”项目。
    • 在上下文菜单中,选择“添加”>“类...”。
    • 在“添加新项”对话框中,将类命名为 CompositionHost.cs,然后单击“添加”。
  2. 在 CompositionHost.cs 中,编辑类定义以派生自 HwndHost。

    // Add
    // using System.Windows.Interop;
    
    namespace HelloComposition
    {
        class CompositionHost : HwndHost
        {
        }
    }
    
  3. 将以下代码和构造函数添加到该类。

    // Add
    // using Windows.UI.Composition;
    
    IntPtr hwndHost;
    int hostHeight, hostWidth;
    object dispatcherQueue;
    ICompositionTarget compositionTarget;
    
    public Compositor Compositor { get; private set; }
    
    public Visual Child
    {
        set
        {
            if (Compositor == null)
            {
                InitComposition(hwndHost);
            }
            compositionTarget.Root = value;
        }
    }
    
    internal const int
      WS_CHILD = 0x40000000,
      WS_VISIBLE = 0x10000000,
      LBS_NOTIFY = 0x00000001,
      HOST_ID = 0x00000002,
      LISTBOX_ID = 0x00000001,
      WS_VSCROLL = 0x00200000,
      WS_BORDER = 0x00800000;
    
    public CompositionHost(double height, double width)
    {
        hostHeight = (int)height;
        hostWidth = (int)width;
    }
    
  4. 重写 BuildWindowCoreDestroyWindowCore 方法。

    注意

    在 BuildWindowCore 中调用 InitializeCoreDispatcher 和 InitComposition 方法。 将在后续步骤中创建这些方法。

    // Add
    // using System.Runtime.InteropServices;
    
    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        // Create Window
        hwndHost = IntPtr.Zero;
        hwndHost = CreateWindowEx(0, "static", "",
                                  WS_CHILD | WS_VISIBLE,
                                  0, 0,
                                  hostWidth, hostHeight,
                                  hwndParent.Handle,
                                  (IntPtr)HOST_ID,
                                  IntPtr.Zero,
                                  0);
    
        // Create Dispatcher Queue
        dispatcherQueue = InitializeCoreDispatcher();
    
        // Build Composition tree of content
        InitComposition(hwndHost);
    
        return new HandleRef(this, hwndHost);
    }
    
    protected override void DestroyWindowCore(HandleRef hwnd)
    {
        if (compositionTarget.Root != null)
        {
            compositionTarget.Root.Dispose();
        }
        DestroyWindow(hwnd.Handle);
    }
    
    #region PInvoke declarations
    
    [DllImport("user32.dll", EntryPoint = "CreateWindowEx", CharSet = CharSet.Unicode)]
    internal static extern IntPtr CreateWindowEx(int dwExStyle,
                                                  string lpszClassName,
                                                  string lpszWindowName,
                                                  int style,
                                                  int x, int y,
                                                  int width, int height,
                                                  IntPtr hwndParent,
                                                  IntPtr hMenu,
                                                  IntPtr hInst,
                                                  [MarshalAs(UnmanagedType.AsAny)] object pvParam);
    
    [DllImport("user32.dll", EntryPoint = "DestroyWindow", CharSet = CharSet.Unicode)]
    internal static extern bool DestroyWindow(IntPtr hwnd);
    
    #endregion PInvoke declarations
    
  5. 使用 CoreDispatcher 初始化某个线程。 核心调度程序负责处理窗口消息并调度 WinRT API 的事件。 必须在具有 CoreDispatcher 的线程中创建 CoreDispatcher 的新实例。

    • 创建名为 InitializeCoreDispatcher 的方法,并添加用于设置调度程序队列的代码。
    private object InitializeCoreDispatcher()
    {
        DispatcherQueueOptions options = new DispatcherQueueOptions();
        options.apartmentType = DISPATCHERQUEUE_THREAD_APARTMENTTYPE.DQTAT_COM_STA;
        options.threadType = DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT;
        options.dwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions));
    
        object queue = null;
        CreateDispatcherQueueController(options, out queue);
        return queue;
    }
    
    • 调度程序队列也需要 PInvoke 声明。 请将此声明置于在上一步骤中创建的 PInvoke 声明区域中。
    //typedef enum DISPATCHERQUEUE_THREAD_APARTMENTTYPE
    //{
    //    DQTAT_COM_NONE,
    //    DQTAT_COM_ASTA,
    //    DQTAT_COM_STA
    //};
    internal enum DISPATCHERQUEUE_THREAD_APARTMENTTYPE
    {
        DQTAT_COM_NONE = 0,
        DQTAT_COM_ASTA = 1,
        DQTAT_COM_STA = 2
    };
    
    //typedef enum DISPATCHERQUEUE_THREAD_TYPE
    //{
    //    DQTYPE_THREAD_DEDICATED,
    //    DQTYPE_THREAD_CURRENT
    //};
    internal enum DISPATCHERQUEUE_THREAD_TYPE
    {
        DQTYPE_THREAD_DEDICATED = 1,
        DQTYPE_THREAD_CURRENT = 2,
    };
    
    //struct DispatcherQueueOptions
    //{
    //    DWORD dwSize;
    //    DISPATCHERQUEUE_THREAD_TYPE threadType;
    //    DISPATCHERQUEUE_THREAD_APARTMENTTYPE apartmentType;
    //};
    [StructLayout(LayoutKind.Sequential)]
    internal struct DispatcherQueueOptions
    {
        public int dwSize;
    
        [MarshalAs(UnmanagedType.I4)]
        public DISPATCHERQUEUE_THREAD_TYPE threadType;
    
        [MarshalAs(UnmanagedType.I4)]
        public DISPATCHERQUEUE_THREAD_APARTMENTTYPE apartmentType;
    };
    
    //HRESULT CreateDispatcherQueueController(
    //  DispatcherQueueOptions options,
    //  ABI::Windows::System::IDispatcherQueueController** dispatcherQueueController
    //);
    [DllImport("coremessaging.dll", EntryPoint = "CreateDispatcherQueueController", CharSet = CharSet.Unicode)]
    internal static extern IntPtr CreateDispatcherQueueController(DispatcherQueueOptions options,
                                            [MarshalAs(UnmanagedType.IUnknown)]
                                            out object dispatcherQueueController);
    

    现已准备好调度程序队列,接下来可以开始初始化并创建合成内容。

  6. 初始化 Compositor。 Compositor 是一个工厂,它在跨视觉对象、效果系统和动画系统的 Windows.UI.Composition 命名空间中创建各种类型。 Compositor 类还用于管理从工厂创建的对象的生存期。

    private void InitComposition(IntPtr hwndHost)
    {
        ICompositorDesktopInterop interop;
    
        compositor = new Compositor();
        object iunknown = compositor as object;
        interop = (ICompositorDesktopInterop)iunknown;
        IntPtr raw;
        interop.CreateDesktopWindowTarget(hwndHost, true, out raw);
    
        object rawObject = Marshal.GetObjectForIUnknown(raw);
        ICompositionTarget target = (ICompositionTarget)rawObject;
    
        if (raw == null) { throw new Exception("QI Failed"); }
    }
    
    • ICompositorDesktopInterop 和 ICompositionTarget 需要 COM 导入。 请将此代码置于 CompositionHost 类的后面,但请确保它位于命名空间声明的内部。
    #region COM Interop
    
    /*
    #undef INTERFACE
    #define INTERFACE ICompositorDesktopInterop
        DECLARE_INTERFACE_IID_(ICompositorDesktopInterop, IUnknown, "29E691FA-4567-4DCA-B319-D0F207EB6807")
        {
            IFACEMETHOD(CreateDesktopWindowTarget)(
                _In_ HWND hwndTarget,
                _In_ BOOL isTopmost,
                _COM_Outptr_ IDesktopWindowTarget * *result
                ) PURE;
        };
    */
    [ComImport]
    [Guid("29E691FA-4567-4DCA-B319-D0F207EB6807")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface ICompositorDesktopInterop
    {
        void CreateDesktopWindowTarget(IntPtr hwndTarget, bool isTopmost, out IntPtr test);
    }
    
    //[contract(Windows.Foundation.UniversalApiContract, 2.0)]
    //[exclusiveto(Windows.UI.Composition.CompositionTarget)]
    //[uuid(A1BEA8BA - D726 - 4663 - 8129 - 6B5E7927FFA6)]
    //interface ICompositionTarget : IInspectable
    //{
    //    [propget] HRESULT Root([out] [retval] Windows.UI.Composition.Visual** value);
    //    [propput] HRESULT Root([in] Windows.UI.Composition.Visual* value);
    //}
    
    [ComImport]
    [Guid("A1BEA8BA-D726-4663-8129-6B5E7927FFA6")]
    [InterfaceType(ComInterfaceType.InterfaceIsIInspectable)]
    public interface ICompositionTarget
    {
        Windows.UI.Composition.Visual Root
        {
            get;
            set;
        }
    }
    
    #endregion COM Interop
    

创建 UserControl 用于将内容添加到 WPF 视觉树

设置承载合成内容所需的基础结构的最后一步是将 HwndHost 添加到 WPF 视觉树中。

创建用户控件

使用 UserControl 可以方便地打包用于创建和管理合成内容的代码,并将内容轻松添加到 XAML。

  1. 将新的用户控件文件添加到项目。

    • 在“解决方案资源管理器”中右键单击“HelloComposition”项目。
    • 在上下文菜单中,选择“添加”>“用户控件...”。
    • 在“添加新项”对话框中,将用户控件命名为 CompositionHostControl.xaml,然后单击“添加”。

    将会创建 CompositionHostControl.xaml 和 CompositionHostControl.xaml.cs 文件,并将其添加到项目中。

  2. 在 CompositionHostControl.xaml 中,将 <Grid> </Grid> 标记替换为此 Border 元素,该元素是 HwndHost 所要进入到的 XAML 容器。

    <Border Name="CompositionHostElement"/>
    

在用户控件的代码中,创建在上一步骤中创建的 CompositionHost 类的实例,并将其添加为 CompositionHostElement(在 XAML 页面中创建的边框)的子元素。

  1. 在 CompositionHostControl.xaml.cs 中,为要在合成代码中使用的对象添加专用变量。 请在类定义后面添加这些变量。

    CompositionHost compositionHost;
    Compositor compositor;
    Windows.UI.Composition.ContainerVisual containerVisual;
    DpiScale currentDpi;
    
  2. 为用户控件的 Loaded 事件添加一个处理程序。 将在此处理程序中设置 CompositionHost 实例。

    • 在构造函数中,按如下所示挂接事件处理程序 (Loaded += CompositionHostControl_Loaded;)。
    public CompositionHostControl()
    {
        InitializeComponent();
        Loaded += CompositionHostControl_Loaded;
    }
    
    • 添加名为 CompositionHostControl_Loaded 的事件处理程序方法。
    private void CompositionHostControl_Loaded(object sender, RoutedEventArgs e)
    {
        // If the user changes the DPI scale setting for the screen the app is on,
        // the CompositionHostControl is reloaded. Don't redo this set up if it's
        // already been done.
        if (compositionHost is null)
        {
            currentDpi = VisualTreeHelper.GetDpi(this);
    
            compositionHost =
                new CompositionHost(ControlHostElement.ActualHeight, ControlHostElement.ActualWidth);
            ControlHostElement.Child = compositionHost;
            compositor = compositionHost.Compositor;
            containerVisual = compositor.CreateContainerVisual();
            compositionHost.Child = containerVisual;
        }
    }
    

    在此方法中,设置要在合成代码中使用的对象。 下面简要说明了发生的情况。

    • 首先,请通过检查 CompositionHost 的实例是否已存在,来确保只指定了该设置一次。
    // If the user changes the DPI scale setting for the screen the app is on,
    // the CompositionHostControl is reloaded. Don't redo this set up if it's
    // already been done.
    if (compositionHost is null)
    {
    
    }
    
    • 获取当前 DPI。 此操作用于正确缩放合成元素。
    currentDpi = VisualTreeHelper.GetDpi(this);
    
    • 创建 CompositionHost 的实例,并将其分配为边框 CompositionHostElement 的子级。
    compositionHost =
        new CompositionHost(ControlHostElement.ActualHeight, ControlHostElement.ActualWidth);
    ControlHostElement.Child = compositionHost;
    
    • 从 CompositionHost 获取 Compositor。
    compositor = compositionHost.Compositor;
    
    • 使用 Compositor 创建容器视觉对象。 这是合成元素所添加到的合成容器。
    containerVisual = compositor.CreateContainerVisual();
    compositionHost.Child = containerVisual;
    

添加合成元素

准备好基础结构后,接下来可以生成要显示的合成内容。

对于本示例,请添加用于创建并以动画显示一个简单正方形 SpriteVisual 的代码。

  1. 添加合成元素。 在 CompositionHostControl.xaml.cs 中,将这些方法添加到 CompositionHostControl 类。

    // Add
    // using System.Numerics;
    
    public void AddElement(float size, float offsetX, float offsetY)
    {
        var visual = compositor.CreateSpriteVisual();
        visual.Size = new Vector2(size, size);
        visual.Scale = new Vector3((float)currentDpi.DpiScaleX, (float)currentDpi.DpiScaleY, 1);
        visual.Brush = compositor.CreateColorBrush(GetRandomColor());
        visual.Offset = new Vector3(offsetX * (float)currentDpi.DpiScaleX, offsetY * (float)currentDpi.DpiScaleY, 0);
    
        containerVisual.Children.InsertAtTop(visual);
    
        AnimateSquare(visual, 3);
    }
    
    private void AnimateSquare(SpriteVisual visual, int delay)
    {
        float offsetX = (float)(visual.Offset.X); // Already adjusted for DPI.
    
        // Adjust values for DPI scale, then find the Y offset that aligns the bottom of the square
        // with the bottom of the host container. This is the value to animate to.
        var hostHeightAdj = CompositionHostElement.ActualHeight * currentDpi.DpiScaleY;
        var squareSizeAdj = visual.Size.Y * currentDpi.DpiScaleY;
        float bottom = (float)(hostHeightAdj - squareSizeAdj);
    
        // Create the animation only if it's needed.
        if (visual.Offset.Y != bottom)
        {
            Vector3KeyFrameAnimation animation = compositor.CreateVector3KeyFrameAnimation();
            animation.InsertKeyFrame(1f, new Vector3(offsetX, bottom, 0f));
            animation.Duration = TimeSpan.FromSeconds(2);
            animation.DelayTime = TimeSpan.FromSeconds(delay);
            visual.StartAnimation("Offset", animation);
        }
    }
    
    private Windows.UI.Color GetRandomColor()
    {
        Random random = new Random();
        byte r = (byte)random.Next(0, 255);
        byte g = (byte)random.Next(0, 255);
        byte b = (byte)random.Next(0, 255);
        return Windows.UI.Color.FromArgb(255, r, g, b);
    }
    

处理 DPI 变更

用于添加并以动画显示某个元素的代码会在创建元素时考虑到当前 DPI 比例,但你还需要考虑到运行应用时的 DPI 变化。 可以处理 HwndHost.DpiChanged 事件以便在发生变化时收到通知,并根据新的 DPI 调整计算。

  1. 在 CompositionHostControl_Loaded 方法中的最后一行后面,添加以下代码以挂接 DpiChanged 事件处理程序。

    compositionHost.DpiChanged += CompositionHost_DpiChanged;
    
  2. 添加名为 CompositionHostDpiChanged 的事件处理程序方法。 此代码将调整每个元素的比例和偏移量,并重新计算任何未完成的动画。

    private void CompositionHost_DpiChanged(object sender, DpiChangedEventArgs e)
    {
        currentDpi = e.NewDpi;
        Vector3 newScale = new Vector3((float)e.NewDpi.DpiScaleX, (float)e.NewDpi.DpiScaleY, 1);
    
        foreach (SpriteVisual child in containerVisual.Children)
        {
            child.Scale = newScale;
            var newOffsetX = child.Offset.X * ((float)e.NewDpi.DpiScaleX / (float)e.OldDpi.DpiScaleX);
            var newOffsetY = child.Offset.Y * ((float)e.NewDpi.DpiScaleY / (float)e.OldDpi.DpiScaleY);
            child.Offset = new Vector3(newOffsetX, newOffsetY, 1);
    
            // Adjust animations for DPI change.
            AnimateSquare(child, 0);
        }
    }
    

将用户控件添加到 XAML 页面

现在,可将用户控件添加到 XAML UI。

  1. 在 MainWindow.xaml 中,将窗口高度设置为 600,将宽度设置为 840。

  2. 添加 UI 的 XAML。 在 MainWindow.xaml 中,在根 <Grid> </Grid> 标记之间添加以下 XAML。

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="210"/>
        <ColumnDefinition Width="600"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="46"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Button Content="Add composition element" Click="Button_Click"
            Grid.Row="1" Margin="12,0"
            VerticalAlignment="Top" Height="40"/>
    <TextBlock Text="Composition content" FontSize="20"
               Grid.Column="1" Margin="0,12,0,4"
               HorizontalAlignment="Center"/>
    <local:CompositionHostControl x:Name="CompositionHostControl1"
                                  Grid.Row="1" Grid.Column="1"
                                  VerticalAlignment="Top"
                                  Width="600" Height="500"
                                  BorderBrush="LightGray"
                                  BorderThickness="3"/>
    
  3. 处理按钮单击以创建新元素。 (已在 XAML 中挂接 Click 事件。)

    在 MainWindow.xaml.cs 中,添加此 Button_Click 事件处理程序方法。 此代码将调用 CompositionHost.AddElement 来创建使用随机生成的大小和偏移量的新元素。

    // Add
    // using System;
    
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Random random = new Random();
        float size = random.Next(50, 150);
        float offsetX = random.Next(0, (int)(CompositionHostControl1.ActualWidth - size));
        float offsetY = random.Next(0, (int)(CompositionHostControl1.ActualHeight/2 - size));
        CompositionHostControl1.AddElement(size, offsetX, offsetY);
    }
    

现在,可以生成并运行你的 WPF 应用。 如果需要,请检查本教程末尾的完整代码,确保所有代码位于正确的位置。

运行应用并单击按钮时,应会看到已添加到 UI 中的动画正方形。

后续步骤

有关在同一基础结构上构建的更完整示例,请参阅 GitHub 上的 WPF 视觉层集成示例

其他资源

完成代码

下面是本教程的完整代码。

MainWindow.xaml

<Window x:Class="HelloComposition.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HelloComposition"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="840">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="210"/>
            <ColumnDefinition Width="600"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="46"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Button Content="Add composition element" Click="Button_Click"
                Grid.Row="1" Margin="12,0"
                VerticalAlignment="Top" Height="40"/>
        <TextBlock Text="Composition content" FontSize="20"
                   Grid.Column="1" Margin="0,12,0,4"
                   HorizontalAlignment="Center"/>
        <local:CompositionHostControl x:Name="CompositionHostControl1"
                                      Grid.Row="1" Grid.Column="1"
                                      VerticalAlignment="Top"
                                      Width="600" Height="500"
                                      BorderBrush="LightGray" BorderThickness="3"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Windows;

namespace HelloComposition
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Random random = new Random();
            float size = random.Next(50, 150);
            float offsetX = random.Next(0, (int)(CompositionHostControl1.ActualWidth - size));
            float offsetY = random.Next(0, (int)(CompositionHostControl1.ActualHeight/2 - size));
            CompositionHostControl1.AddElement(size, offsetX, offsetY);
        }
    }
}

CompositionHostControl.xaml

<UserControl x:Class="HelloComposition.CompositionHostControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:HelloComposition"
             mc:Ignorable="d"
             d:DesignHeight="450" d:DesignWidth="800">
    <Border Name="CompositionHostElement"/>
</UserControl>

CompositionHostControl.xaml.cs

using System;
using System.Numerics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Windows.UI.Composition;

namespace HelloComposition
{
    /// <summary>
    /// Interaction logic for CompositionHostControl.xaml
    /// </summary>
    public partial class CompositionHostControl : UserControl
    {
        CompositionHost compositionHost;
        Compositor compositor;
        Windows.UI.Composition.ContainerVisual containerVisual;
        DpiScale currentDpi;

        public CompositionHostControl()
        {
            InitializeComponent();
            Loaded += CompositionHostControl_Loaded;
        }

        private void CompositionHostControl_Loaded(object sender, RoutedEventArgs e)
        {
            // If the user changes the DPI scale setting for the screen the app is on,
            // the CompositionHostControl is reloaded. Don't redo this set up if it's
            // already been done.
            if (compositionHost is null)
            {
                currentDpi = VisualTreeHelper.GetDpi(this);

                compositionHost = new CompositionHost(CompositionHostElement.ActualHeight, CompositionHostElement.ActualWidth);
                CompositionHostElement.Child = compositionHost;
                compositor = compositionHost.Compositor;
                containerVisual = compositor.CreateContainerVisual();
                compositionHost.Child = containerVisual;
            }
        }

        protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
        {
            base.OnDpiChanged(oldDpi, newDpi);
            currentDpi = newDpi;
            Vector3 newScale = new Vector3((float)newDpi.DpiScaleX, (float)newDpi.DpiScaleY, 1);

            foreach (SpriteVisual child in containerVisual.Children)
            {
                child.Scale = newScale;
                var newOffsetX = child.Offset.X * ((float)newDpi.DpiScaleX / (float)oldDpi.DpiScaleX);
                var newOffsetY = child.Offset.Y * ((float)newDpi.DpiScaleY / (float)oldDpi.DpiScaleY);
                child.Offset = new Vector3(newOffsetX, newOffsetY, 1);

                // Adjust animations for DPI change.
                AnimateSquare(child, 0);
            }
        }

        public void AddElement(float size, float offsetX, float offsetY)
        {
            var visual = compositor.CreateSpriteVisual();
            visual.Size = new Vector2(size, size);
            visual.Scale = new Vector3((float)currentDpi.DpiScaleX, (float)currentDpi.DpiScaleY, 1);
            visual.Brush = compositor.CreateColorBrush(GetRandomColor());
            visual.Offset = new Vector3(offsetX * (float)currentDpi.DpiScaleX, offsetY * (float)currentDpi.DpiScaleY, 0);

            containerVisual.Children.InsertAtTop(visual);

            AnimateSquare(visual, 3);
        }

        private void AnimateSquare(SpriteVisual visual, int delay)
        {
            float offsetX = (float)(visual.Offset.X); // Already adjusted for DPI.

            // Adjust values for DPI scale, then find the Y offset that aligns the bottom of the square
            // with the bottom of the host container. This is the value to animate to.
            var hostHeightAdj = CompositionHostElement.ActualHeight * currentDpi.DpiScaleY;
            var squareSizeAdj = visual.Size.Y * currentDpi.DpiScaleY;
            float bottom = (float)(hostHeightAdj - squareSizeAdj);

            // Create the animation only if it's needed.
            if (visual.Offset.Y != bottom)
            {
                Vector3KeyFrameAnimation animation = compositor.CreateVector3KeyFrameAnimation();
                animation.InsertKeyFrame(1f, new Vector3(offsetX, bottom, 0f));
                animation.Duration = TimeSpan.FromSeconds(2);
                animation.DelayTime = TimeSpan.FromSeconds(delay);
                visual.StartAnimation("Offset", animation);
            }
        }

        private Windows.UI.Color GetRandomColor()
        {
            Random random = new Random();
            byte r = (byte)random.Next(0, 255);
            byte g = (byte)random.Next(0, 255);
            byte b = (byte)random.Next(0, 255);
            return Windows.UI.Color.FromArgb(255, r, g, b);
        }
    }
}

CompositionHost.cs

using System;
using System.Runtime.InteropServices;
using System.Windows.Interop;
using Windows.UI.Composition;

namespace HelloComposition
{
    class CompositionHost : HwndHost
    {
        IntPtr hwndHost;
        int hostHeight, hostWidth;
        object dispatcherQueue;
        ICompositionTarget compositionTarget;

        public Compositor Compositor { get; private set; }

        public Visual Child
        {
            set
            {
                if (Compositor == null)
                {
                    InitComposition(hwndHost);
                }
                compositionTarget.Root = value;
            }
        }

        internal const int
          WS_CHILD = 0x40000000,
          WS_VISIBLE = 0x10000000,
          LBS_NOTIFY = 0x00000001,
          HOST_ID = 0x00000002,
          LISTBOX_ID = 0x00000001,
          WS_VSCROLL = 0x00200000,
          WS_BORDER = 0x00800000;

        public CompositionHost(double height, double width)
        {
            hostHeight = (int)height;
            hostWidth = (int)width;
        }

        protected override HandleRef BuildWindowCore(HandleRef hwndParent)
        {
            // Create Window
            hwndHost = IntPtr.Zero;
            hwndHost = CreateWindowEx(0, "static", "",
                                      WS_CHILD | WS_VISIBLE,
                                      0, 0,
                                      hostWidth, hostHeight,
                                      hwndParent.Handle,
                                      (IntPtr)HOST_ID,
                                      IntPtr.Zero,
                                      0);

            // Create Dispatcher Queue
            dispatcherQueue = InitializeCoreDispatcher();

            // Build Composition Tree of content
            InitComposition(hwndHost);

            return new HandleRef(this, hwndHost);
        }

        protected override void DestroyWindowCore(HandleRef hwnd)
        {
            if (compositionTarget.Root != null)
            {
                compositionTarget.Root.Dispose();
            }
            DestroyWindow(hwnd.Handle);
        }

        private object InitializeCoreDispatcher()
        {
            DispatcherQueueOptions options = new DispatcherQueueOptions();
            options.apartmentType = DISPATCHERQUEUE_THREAD_APARTMENTTYPE.DQTAT_COM_STA;
            options.threadType = DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT;
            options.dwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions));

            object queue = null;
            CreateDispatcherQueueController(options, out queue);
            return queue;
        }

        private void InitComposition(IntPtr hwndHost)
        {
            ICompositorDesktopInterop interop;

            Compositor = new Compositor();
            object iunknown = Compositor as object;
            interop = (ICompositorDesktopInterop)iunknown;
            IntPtr raw;
            interop.CreateDesktopWindowTarget(hwndHost, true, out raw);

            object rawObject = Marshal.GetObjectForIUnknown(raw);
            compositionTarget = (ICompositionTarget)rawObject;

            if (raw == null) { throw new Exception("QI Failed"); }
        }

        #region PInvoke declarations

        //typedef enum DISPATCHERQUEUE_THREAD_APARTMENTTYPE
        //{
        //    DQTAT_COM_NONE,
        //    DQTAT_COM_ASTA,
        //    DQTAT_COM_STA
        //};
        internal enum DISPATCHERQUEUE_THREAD_APARTMENTTYPE
        {
            DQTAT_COM_NONE = 0,
            DQTAT_COM_ASTA = 1,
            DQTAT_COM_STA = 2
        };

        //typedef enum DISPATCHERQUEUE_THREAD_TYPE
        //{
        //    DQTYPE_THREAD_DEDICATED,
        //    DQTYPE_THREAD_CURRENT
        //};
        internal enum DISPATCHERQUEUE_THREAD_TYPE
        {
            DQTYPE_THREAD_DEDICATED = 1,
            DQTYPE_THREAD_CURRENT = 2,
        };

        //struct DispatcherQueueOptions
        //{
        //    DWORD dwSize;
        //    DISPATCHERQUEUE_THREAD_TYPE threadType;
        //    DISPATCHERQUEUE_THREAD_APARTMENTTYPE apartmentType;
        //};
        [StructLayout(LayoutKind.Sequential)]
        internal struct DispatcherQueueOptions
        {
            public int dwSize;

            [MarshalAs(UnmanagedType.I4)]
            public DISPATCHERQUEUE_THREAD_TYPE threadType;

            [MarshalAs(UnmanagedType.I4)]
            public DISPATCHERQUEUE_THREAD_APARTMENTTYPE apartmentType;
        };

        //HRESULT CreateDispatcherQueueController(
        //  DispatcherQueueOptions options,
        //  ABI::Windows::System::IDispatcherQueueController** dispatcherQueueController
        //);
        [DllImport("coremessaging.dll", EntryPoint = "CreateDispatcherQueueController", CharSet = CharSet.Unicode)]
        internal static extern IntPtr CreateDispatcherQueueController(DispatcherQueueOptions options,
                                                [MarshalAs(UnmanagedType.IUnknown)]
                                               out object dispatcherQueueController);


        [DllImport("user32.dll", EntryPoint = "CreateWindowEx", CharSet = CharSet.Unicode)]
        internal static extern IntPtr CreateWindowEx(int dwExStyle,
                                                      string lpszClassName,
                                                      string lpszWindowName,
                                                      int style,
                                                      int x, int y,
                                                      int width, int height,
                                                      IntPtr hwndParent,
                                                      IntPtr hMenu,
                                                      IntPtr hInst,
                                                      [MarshalAs(UnmanagedType.AsAny)] object pvParam);

        [DllImport("user32.dll", EntryPoint = "DestroyWindow", CharSet = CharSet.Unicode)]
        internal static extern bool DestroyWindow(IntPtr hwnd);


        #endregion PInvoke declarations

    }
    #region COM Interop

    /*
    #undef INTERFACE
    #define INTERFACE ICompositorDesktopInterop
        DECLARE_INTERFACE_IID_(ICompositorDesktopInterop, IUnknown, "29E691FA-4567-4DCA-B319-D0F207EB6807")
        {
            IFACEMETHOD(CreateDesktopWindowTarget)(
                _In_ HWND hwndTarget,
                _In_ BOOL isTopmost,
                _COM_Outptr_ IDesktopWindowTarget * *result
                ) PURE;
        };
    */
    [ComImport]
    [Guid("29E691FA-4567-4DCA-B319-D0F207EB6807")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface ICompositorDesktopInterop
    {
        void CreateDesktopWindowTarget(IntPtr hwndTarget, bool isTopmost, out IntPtr test);
    }

    //[contract(Windows.Foundation.UniversalApiContract, 2.0)]
    //[exclusiveto(Windows.UI.Composition.CompositionTarget)]
    //[uuid(A1BEA8BA - D726 - 4663 - 8129 - 6B5E7927FFA6)]
    //interface ICompositionTarget : IInspectable
    //{
    //    [propget] HRESULT Root([out] [retval] Windows.UI.Composition.Visual** value);
    //    [propput] HRESULT Root([in] Windows.UI.Composition.Visual* value);
    //}

    [ComImport]
    [Guid("A1BEA8BA-D726-4663-8129-6B5E7927FFA6")]
    [InterfaceType(ComInterfaceType.InterfaceIsIInspectable)]
    public interface ICompositionTarget
    {
        Windows.UI.Composition.Visual Root
        {
            get;
            set;
        }
    }

    #endregion COM Interop
}