使用視覺層搭配 Windows Forms

您可在 Windows Forms 應用程式中使用 Windows 執行階段 Composition API (又稱為視覺層),以建立適用於 Windows 使用者的現代化體驗。

您可在 GitHub 取得本教學課程的完整程式碼: Windows Forms HelloComposition 範本

必要條件

UWP 主控 API 具備下列先決條件。

如何在 Windows Forms 中使用 Composition API

在本教學課程中,您會建立簡單的 Windows Forms UI,並在其中加入動畫 Composition 元素。 Windows Forms 和 Composition 元件都保持簡單,但不論元件的複雜度為何,顯示的 Interop 程式碼都相同。 完成的應用程式看起來像這樣。

The running app UI

建立 Windows Forms 專案

第一步是建立 Windows Forms 應用程式專案,其包含應用程式定義和 UI 的主要表單。

若要使用 Visual C# 建立名稱為 HelloComposition 的 Windows Forms 應用程式專案,請執行下列動作:

  1. 開啟 Visual Studio,然後選取 [檔案]>[新增]>[專案]
    [新增專案] 對話方塊隨即開啟。
  2. 在 [已安裝] 類別下,展開 [Visual C#] 節點,然後選取 [Windows Desktop]
  3. 選取 [Windows Forms 應用程式 (.NET Framework)] 範本。
  4. 輸入名稱 HelloComposition,選取 [Framework .NET Framework 4.7.2],然後按一下 [確定]

Visual Studio 會建立專案,並針對名為 Form1.cs 的預設應用程式視窗,開啟設計工具。

將專案設定為使用 Windows 執行階段 API

若要在您的 Windows Forms 應用程式中使用 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 版本>\Windows.Foundation.UniversalApiContract<版本>
Windows.Foundation.FoundationContract.winmd C:\Program Files (x86)\Windows Kits\10\References<sdk 版本>\Windows.Foundation.FoundationContract<版本>
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

建立自訂控制項來管理 Interop

若要裝載使用視覺層所建立的內容,可以建立衍生自 Control 的自訂控制項。 此控制項可讓您存取視窗 Handle,以便建立視覺層內容的容器。

您可以在這裡進行用於託管 Composition API 的大部分組態。 在此控制項中,您會使用 平台叫用服務 (PInvoke)COM Interop,將 Composition API 帶入您的 Windows Forms 應用程式。 如需 PInvoke 和 COM Interop 的詳細資訊,請參閱與非受控程式碼互通

提示

如有需要,請在本教學課程最後檢查完整的程式碼,以確保當您逐步進行教學課程時,所有程式碼皆位於正確的位置。

  1. 將新的自訂控制項檔案新增到衍生自 Control 的專案。

    • 在 [方案總管] 中,以滑鼠右鍵按一下 [HelloComposition] 專案。
    • 在操作功能表中,選取 [新增]Add>[新增項目...]
    • 在 [新增項目] 對話方塊中,選取 [報表控制項]
    • 將控制項命名為 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 的視窗訊息和分派事件。 Compositor 的新執行個體必須建立在具有 CoreDispatcher 的執行緒上。

    • 建立名為 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
    

    現在已準備好發送器佇列,可以開始初始化並建立 Composition 內容。

  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 類別中建立的 Interop 程式碼得以重複使用。

在此,您會建立衍生自 CompositionHost 的自訂控制項。 此控制項會新增至 Visual Studio 工具箱,讓您可將其新增至表單。

  1. 將新的自訂控制項檔案新增到衍生自 CompositionHost 的專案。

    • 在 [方案總管] 中,以滑鼠右鍵按一下 [HelloComposition] 專案。
    • 在操作功能表中,選取 [新增]Add>[新增項目...]
    • 在 [新增項目] 對話方塊中,選取 [報表控制項]
    • 將控制項命名為 CompositionHostControl.cs,然後按一下 [新增]。 CompositionHostControl.cs 會在 [設計] 檢視中開啟。
  2. 在 CompositionHostControl.cs 設計檢視的 [屬性] 窗格中,將 BackColor 屬性設定為 ControlLight

    設定背景色彩是選擇性作業。 我們在此執行此作業,讓您可以看到表單背景的自訂控制項。

  3. 切換至 CompositionHostControl.cs 的程式碼檢視,並將類別宣告更新為衍生自 CompositionHost。

    class CompositionHostControl : CompositionHost
    
  4. 更新建構函式以呼叫基底建構函式。

    public CompositionHostControl() : base()
    {
    
    }
    

新增組合元素

基礎結構已就緒之後,即可將組合內容新增至應用程式 UI。

在此範例中,您將程式碼新增至 CompositionHostControl 類別,以建立簡單的 SpriteVisual並產生其動畫。

  1. 新增 Composition 元素。

    在 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 工具箱的 [專案名稱][元件] 之下。

  1. 在 Form1.CS 設計檢視中,將 Button 新增至 UI。

    • 從工具箱將 Button 拖曳至 Form1。 將其放在表單的左上角。 (請參閱教學課程開頭的影像,以檢查控制項的位置。)
    • 在 [屬性] 窗格中,將 [Text] 屬性從 [button1] 變更為 [新增組合元素]
    • 調整 Button 的大小,以便顯示所有文字。

    (如需詳細資訊,請參閱作法:將控制項新增至 Windows Forms。)

  2. 將 CompositionHostControl 新增至 UI。

    • 將 CompositionHostControl 從工具箱拖曳至 Form1。 將其放在 Button 的右邊。
    • 調整 CompositionHost 的大小,使其填滿表單的其餘部分。
  3. 處理 button click 事件。

    • 在 [屬性] 窗格中,按一下閃電以切換至 [事件] 檢視。
    • 在事件清單中,選取 [Click] 事件,鍵入 Button_Click,然後按 Enter。
    • 此程式碼會在 Form1.cs 中新增:
    private void Button_Click(object sender, EventArgs e)
    {
    
    }
    
  4. 將程式碼新增至 button click 處理常式,以建立新的元素。

    • 在 Form1.cs 中,將程式碼新增至您先前建立的 Button_Click 事件處理常式。 此程式碼會呼叫 CompositionHostControl1.AddElement,以建立具有隨機產生大小和位移的新元素。 (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 Forms 應用程式。 當您按一下按鈕時,應該會看見新增到 UI 的動畫方塊。

下一步

如需在相同基礎結構上建置的更完整範例,請參閱 GitHub 上的 Windows Forms 視覺層整合範例

其他資源

完整程式碼

這裡提供本教學課程的完整程式碼。

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
}