可以在 Windows 窗体 应用中使用 Windows 运行时组合 API(也称为 Visual 层),为 Windows 用户打造焕然一新的现代体验。
本教程的全部代码可以在 GitHub 上找到:Windows 窗体 HelloComposition 样例。
先决条件
UWP 宿主 API 的先决条件如下。
- 我们假设你对使用 Windows 窗体 和 UWP 的应用开发有一些熟悉。 有关详细信息,请参阅:
- .NET Framework 4.7.2 或更高版本
- Windows 10 版本 1803 或更高版本
- Windows 10 SDK 17134 或更高版本
如何在 Windows 窗体 中使用合成 API
在本教程中,你将创建一个简单的Windows 窗体 UI,并向其中添加动画合成元素。 无论组件的复杂性如何,Windows 窗体组件和合成组件都保持简单,但显示的互操作代码是相同的。 完成的应用如下所示。
创建Windows 窗体项目
第一步是创建Windows 窗体应用项目,其中包括应用程序定义和 UI 的主窗体。
在 Visual C# 中创建一个名为 HelloComposition 的新的 Windows 窗体 应用程序项目:
- 打开 Visual Studio 并选择“文件”>“新建”>“项目”。
此时会打开 “新建项目 ”对话框。 - 在 installed 类别下,展开 Visual C# 节点,然后选择 Windows Desktop。
- 选择 Windows 窗体 应用(.NET Framework)模板。
- 输入名称 HelloComposition,选择 Framework .NET Framework 4.7.2,然后单击OK。
Visual Studio创建项目,并打开名为Form1.cs的默认应用程序窗口的设计器。
将项目配置为使用 Windows 运行时 API
若要在 Windows 窗体 应用中使用 Windows 运行时 (WinRT) API,需要配置Visual Studio项目以访问Windows 运行时。 此外,合成 API 广泛使用矢量,因此需要添加使用向量所需的引用。
NuGet 包可用于满足这两个需求。 安装这些包的最新版本,以添加对项目的必要引用。
- Microsoft.Windows。Sdk。Contracts(需要将默认包管理格式设置为 PackageReference。)
- System.Numerics.Vectors
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 互操作的详细信息,请参阅 与非托管代码的互操作。
小窍门
如果需要,请检查本教程末尾的完整代码,确保在完成本教程的过程中,所有代码位于正确的位置。
将一个新的自定义控件文件添加到派生自 Control 的项目。
- 在“解决方案资源管理器”中右键单击“HelloComposition”项目。
- 在上下文菜单中,选择“ 添加新>项...”。
- 在“ 添加新项 ”对话框中,选择“ 自定义控件”。
- 将控件 命名为CompositionHost.cs,然后单击“ 添加”。 CompositionHost.cs将在“设计”视图中打开。
切换到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; } }将代码添加到构造函数。
在构造函数中,调用 InitializeCoreDispatcher 和 InitComposition 方法。 将在后续步骤中创建这些方法。
public CompositionHost() { InitializeComponent(); // Get the window handle. hwndHost = Handle; // Create dispatcher queue. dispatcherQueue = InitializeCoreDispatcher(); // Build Composition tree of content. InitComposition(hwndHost); }使用 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现在调度队列已准备就绪,可以开始初始化和创建合成内容。
初始化 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; }- 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
创建自定义控件以承载合成元素
最好将生成和管理合成元素的代码放在派生自 CompositionHost 的单独控件中。 这会保留你在 CompositionHost 类中创建的互操作代码可重用。
在这里,将创建自定义控件,该控件派生自 CompositionHost。 你可以将此控件添加到窗体中,它已被包含在 Visual Studio 工具箱中。
将新的自定义控件文件(继承自 CompositionHost)添加到您的项目中。
- 在“解决方案资源管理器”中右键单击“HelloComposition”项目。
- 在上下文菜单中,选择“ 添加新>项...”。
- 在“ 添加新项 ”对话框中,选择“ 自定义控件”。
- 将控件 命名为CompositionHostControl.cs,然后单击“ 添加”。 CompositionHostControl.cs在“设计”视图中打开。
在CompositionHostControl.cs设计视图的“属性”窗格中,将 BackColor 属性设置为 ControlLight。
设置背景色是可选的。 我们在这里这样做,以便您可以将自定义控件与表单背景进行比较。
切换到CompositionHostControl.cs的代码视图,并更新类声明以派生自 CompositionHost。
class CompositionHostControl : CompositionHost更新构造函数以调用基构造函数。
public CompositionHostControl() : base() { }
添加合成元素
借助基础结构,现在可以将合成内容添加到应用 UI。
在此示例中,您需要在 CompositionHostControl 类中添加代码,以创建和对简单的 SpriteVisual 进行动画处理。
添加合成元素。
在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 组件下。
在Form1.CS设计视图中,向 UI 添加按钮。
- 将按钮从工具箱拖到 Form1 上。 将其置于窗体的左上角。 (请参阅教程开头的图像,检查控件的位置。
- 在“属性”窗格中,将 Text 属性从 button1 更改为 “添加合成”元素。
- 调整按钮的大小,以便显示所有文本。
(有关详细信息,请参阅 How to: Add Controls to Windows 窗体.)
将 CompositionHostControl 添加到 UI。
- 将 CompositionHostControl 从工具箱拖放到 Form1。 将其置于按钮右侧。
- 调整 CompositionHost 的大小,使其填充窗体的其余部分。
处理按钮单击事件。
- 在“属性”窗格中,单击闪电,切换到“事件”视图。
- 在事件列表中,选择 Click 事件,键入 Button_Click,然后按 Enter。
- 此代码将添加到Form1.cs:
private void Button_Click(object sender, EventArgs e) { }将代码添加到按钮单击处理程序以创建新元素。
- 在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 窗体可视化层集成示例。
其他资源
- 开始使用 Windows 窗体 (.NET)
- 使用非托管代码进行交互(.NET)
- Windows 应用入门 (UWP)
- 增强适用于 Windows 的桌面应用程序 (UWP)
- Windows.UI.Composition 命名空间 (UWP)
完整代码
下面是本教程的完整代码。
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
}