将视觉层与Windows 窗体配合使用

可以在 Windows 窗体 应用中使用 Windows 运行时组合 API(也称为 Visual 层),为 Windows 用户打造焕然一新的现代体验。

本教程的全部代码可以在 GitHub 上找到:Windows 窗体 HelloComposition 样例

先决条件

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

如何在 Windows 窗体 中使用合成 API

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

正在运行的应用 UI

创建Windows 窗体项目

第一步是创建Windows 窗体应用项目,其中包括应用程序定义和 UI 的主窗体。

在 Visual C# 中创建一个名为 HelloComposition 的新的 Windows 窗体 应用程序项目:

  1. 打开 Visual Studio 并选择“文件”>“新建”>“项目”。
    此时会打开 “新建项目 ”对话框。
  2. installed 类别下,展开 Visual C# 节点,然后选择 Windows Desktop
  3. 选择 Windows 窗体 应用(.NET Framework)模板。
  4. 输入名称 HelloComposition,选择 Framework .NET Framework 4.7.2,然后单击OK

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

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

若要在 Windows 窗体 应用中使用 Windows 运行时 (WinRT) API,需要配置Visual Studio项目以访问Windows 运行时。 此外,合成 API 广泛使用矢量,因此需要添加使用向量所需的引用。

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

Note

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

File 货位
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

创建自定义控件以管理互操作

若要托管使用视觉层创建的内容,请创建派生自 Control 的自定义 控件。 此控件允许你访问窗口 句柄,你需要该窗口才能为视觉层内容创建容器。

这是你为托管合成 API 执行大部分配置的地方。 在此控件中,使用 Platform 调用服务 (PInvoke)COM 互操作将合成 API 引入Windows 窗体应用中。 有关 PInvoke 和 COM 互操作的详细信息,请参阅 与非托管代码的互操作

小窍门

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

  1. 将一个新的自定义控件文件添加到派生自 Control 的项目。

    • 在“解决方案资源管理器”中右键单击“HelloComposition”项目。
    • 在上下文菜单中,选择“ 添加新>项...”
    • 在“ 添加新项 ”对话框中,选择“ 自定义控件”。
    • 将控件 命名为CompositionHost.cs,然后单击“ 添加”。 CompositionHost.cs将在“设计”视图中打开。
  2. 切换到CompositionHost.cs的代码视图,并将以下代码添加到类。

    // Add
    // using Windows.UI.Composition;
    
    IntPtr hwndHost;
    object dispatcherQueue;
    protected ContainerVisual containerVisual;
    protected Compositor compositor;
    
    private ICompositionTarget compositionTarget;
    
    public Visual Child
    {
        set
        {
            if (compositor == null)
            {
                InitComposition(hwndHost);
            }
            compositionTarget.Root = value;
        }
    }
    
  3. 将代码添加到构造函数。

    在构造函数中,调用 InitializeCoreDispatcherInitComposition 方法。 将在后续步骤中创建这些方法。

    public CompositionHost()
    {
        InitializeComponent();
    
        // Get the window handle.
        hwndHost = Handle;
    
        // Create dispatcher queue.
        dispatcherQueue = InitializeCoreDispatcher();
    
        // Build Composition tree of content.
        InitComposition(hwndHost);
    }
    
  4. 使用 CoreDispatcher 初始化线程。 核心调度程序负责处理 WinRT API 的窗口消息和调度事件。 必须在具有 CoreDispatcher 的线程上创建 Compositor 的新实例。

    • 创建名为 InitializeCoreDispatcher 的方法,并添加代码以设置调度程序队列。
    // Add
    // using System.Runtime.InteropServices;
    
    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 声明。 将此声明放在类的代码末尾。 (我们将此代码放置在区域内以保持类代码整洁。
    #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);
    
    #endregion PInvoke declarations
    

    现在调度队列已准备就绪,可以开始初始化和创建合成内容。

  5. 初始化 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);
        compositionTarget = (ICompositionTarget)rawObject;
    
        if (raw == null) { throw new Exception("QI Failed"); }
    
        containerVisual = compositor.CreateContainerVisual();
        Child = containerVisual;
    }
    
    • ICompositorDesktopInteropICompositionTarget 需要 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
    

创建自定义控件以承载合成元素

最好将生成和管理合成元素的代码放在派生自 CompositionHost 的单独控件中。 这会保留你在 CompositionHost 类中创建的互操作代码可重用。

在这里,将创建自定义控件,该控件派生自 CompositionHost。 你可以将此控件添加到窗体中,它已被包含在 Visual Studio 工具箱中。

  1. 将新的自定义控件文件(继承自 CompositionHost)添加到您的项目中。

    • 在“解决方案资源管理器”中右键单击“HelloComposition”项目。
    • 在上下文菜单中,选择“ 添加新>项...”
    • 在“ 添加新项 ”对话框中,选择“ 自定义控件”。
    • 将控件 命名为CompositionHostControl.cs,然后单击“ 添加”。 CompositionHostControl.cs在“设计”视图中打开。
  2. 在CompositionHostControl.cs设计视图的“属性”窗格中,将 BackColor 属性设置为 ControlLight

    设置背景色是可选的。 我们在这里这样做,以便您可以将自定义控件与表单背景进行比较。

  3. 切换到CompositionHostControl.cs的代码视图,并更新类声明以派生自 CompositionHost。

    class CompositionHostControl : CompositionHost
    
  4. 更新构造函数以调用基构造函数。

    public CompositionHostControl() : base()
    {
    
    }
    

添加合成元素

借助基础结构,现在可以将合成内容添加到应用 UI。

在此示例中,您需要在 CompositionHostControl 类中添加代码,以创建和对简单的 SpriteVisual 进行动画处理。

  1. 添加合成元素。

    在CompositionHostControl.cs中,将这些方法添加到 CompositionHostControl 类。

    // Add
    // using Windows.UI.Composition;
    
    public void AddElement(float size, float offsetX, float offsetY)
    {
        var visual = compositor.CreateSpriteVisual();
        visual.Size = new Vector2(size, size); // Requires references
        visual.Brush = compositor.CreateColorBrush(GetRandomColor());
        visual.Offset = new Vector3(offsetX, offsetY, 0);
        containerVisual.Children.InsertAtTop(visual);
    
        AnimateSquare(visual, 3);
    }
    
    private void AnimateSquare(SpriteVisual visual, int delay)
    {
        float offsetX = (float)(visual.Offset.X);
        Vector3KeyFrameAnimation animation = compositor.CreateVector3KeyFrameAnimation();
        float bottom = Height - visual.Size.Y;
        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);
    }
    

将控件添加到窗体

有了用于托管合成内容的自定义控件后,即可将其添加到应用 UI。 在这里,添加在上一步中创建的 CompositionHostControl 实例。 CompositionHostControl 会自动添加到 Visual Studio 工具箱中 project name 组件下。

  1. 在Form1.CS设计视图中,向 UI 添加按钮。

    • 将按钮从工具箱拖到 Form1 上。 将其置于窗体的左上角。 (请参阅教程开头的图像,检查控件的位置。
    • 在“属性”窗格中,将 Text 属性从 button1 更改为 “添加合成”元素
    • 调整按钮的大小,以便显示所有文本。

    (有关详细信息,请参阅 How to: Add Controls to Windows 窗体.)

  2. 将 CompositionHostControl 添加到 UI。

    • 将 CompositionHostControl 从工具箱拖放到 Form1。 将其置于按钮右侧。
    • 调整 CompositionHost 的大小,使其填充窗体的其余部分。
  3. 处理按钮单击事件。

    • 在“属性”窗格中,单击闪电,切换到“事件”视图。
    • 在事件列表中,选择 Click 事件,键入 Button_Click,然后按 Enter。
    • 此代码将添加到Form1.cs:
    private void Button_Click(object sender, EventArgs e)
    {
    
    }
    
  4. 将代码添加到按钮单击处理程序以创建新元素。

    • 在Form1.cs中,将代码添加到之前创建的 Button_Click 事件处理程序。 此代码调用 CompositionHostControl1.AddElement 以创建具有随机生成的大小和偏移量的新元素。 (当将 CompositionHostControl 拖到窗体上时,CompositionHostControl 的实例会自动命名为 compositionHostControl1
    // 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.Width - size));
        float offsetY = random.Next(0, (int)(compositionHostControl1.Height/2 - size));
        compositionHostControl1.AddElement(size, offsetX, offsetY);
    }
    

现在可以生成并运行Windows 窗体应用。 单击该按钮时,应会看到添加到 UI 的动画方块。

后续步骤

有关基于同一基础结构构建的更完整示例,请参阅 GitHub 上的 Windows 窗体可视化层集成示例

其他资源

完整代码

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

Form1.cs

using System;
using System.Windows.Forms;

namespace HelloComposition
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, EventArgs e)
        {
            Random random = new Random();
            float size = random.Next(50, 150);
            float offsetX = random.Next(0, (int)(compositionHostControl1.Width - size));
            float offsetY = random.Next(0, (int)(compositionHostControl1.Height/2 - size));
            compositionHostControl1.AddElement(size, offsetX, offsetY);
        }
    }
}

CompositionHostControl.cs

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

namespace HelloComposition
{
    class CompositionHostControl : CompositionHost
    {
        public CompositionHostControl() : base()
        {

        }

        public void AddElement(float size, float offsetX, float offsetY)
        {
            var visual = compositor.CreateSpriteVisual();
            visual.Size = new Vector2(size, size); // Requires references
            visual.Brush = compositor.CreateColorBrush(GetRandomColor());
            visual.Offset = new Vector3(offsetX, offsetY, 0);
            containerVisual.Children.InsertAtTop(visual);

            AnimateSquare(visual, 3);
        }

        private void AnimateSquare(SpriteVisual visual, int delay)
        {
            float offsetX = (float)(visual.Offset.X);
            Vector3KeyFrameAnimation animation = compositor.CreateVector3KeyFrameAnimation();
            float bottom = Height - visual.Size.Y;
            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.Forms;
using Windows.UI.Composition;

namespace HelloComposition
{
    public partial class CompositionHost : Control
    {
        IntPtr hwndHost;
        object dispatcherQueue;
        protected ContainerVisual containerVisual;
        protected Compositor compositor;
        private ICompositionTarget compositionTarget;

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

        public CompositionHost()
        {
            // Get the window handle.
            hwndHost = Handle;

            // Create dispatcher queue.
            dispatcherQueue = InitializeCoreDispatcher();

            // Build Composition tree of content.
            InitComposition(hwndHost);
        }

        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"); }

            containerVisual = compositor.CreateContainerVisual();
            Child = containerVisual;
        }

        protected override void OnPaint(PaintEventArgs pe)
        {
            base.OnPaint(pe);
        }

        #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);

        #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
}