Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Pode usar as APIs de Composição do Windows Runtime (também chamadas de Camada Visual) nas suas aplicações de Windows Presentation Foundation (WPF) para criar experiências modernas que proporcionam experiências dinâmicas para os utilizadores do Windows.
O código completo deste tutorial está disponível em GitHub: WPF exemplo HelloComposition.
Pré-requisitos
A API de alojamento UWP XAML tem estes pré-requisitos.
- Presumimos que tens alguma familiaridade com desenvolvimento de aplicações usando WPF e UWP. Para mais informações, consulte:
- .NET Framework 4.7.2 ou posterior
- Windows 10 versão 1803 ou posterior
- Windows 10 SDK 17134 ou posterior
Como usar APIs de Composições no WPF
Neste tutorial, crias uma interface simples de aplicação WPF e adicionas elementos animados de Composição. Tanto os componentes WPF como Composition são mantidos simples, mas o código de interoperabilidade mostrado é o mesmo independentemente da complexidade dos componentes. O aplicativo concluído tem esta aparência.
Criar um projeto WPF
O primeiro passo é criar o projeto de aplicação WPF, que inclui uma definição de aplicação e a página XAML para a interface.
Para criar um novo projeto de WPF Application em Visual C# chamado HelloComposition:
Abra o Visual Studio e selecione Arquivo>Novo Projeto>.
O diálogo do Novo Projeto abre-se.
Na categoria Instalado, expanda o nó Visual C# e depois selecione Ambiente de Trabalho do Windows.
Selecione o modelo da Aplicação WPF (.NET Framework).
Introduza o nome HelloComposition, selecione Framework .NET Framework 4.7.2 e depois clique em OK.
O Visual Studio cria o projeto e abre o designer da janela de aplicação padrão chamada MainWindow.xaml.
Configurar o projeto para usar APIs do Tempo de Execução do Windows
Para usar APIs do Windows Runtime (WinRT) na sua aplicação WPF, precisa de configurar o seu projeto Visual Studio para aceder ao Windows Runtime. Além disso, os vetores são amplamente usados pelas APIs de Composição, por isso é necessário adicionar as referências necessárias para usar vetores.
Existem pacotes NuGet disponíveis para responder a ambas as necessidades. Instale as versões mais recentes destes pacotes para adicionar as referências necessárias ao seu projeto.
- Microsoft.Windows.SDK.Contracts (Requer o formato de gestão de pacotes predefinido definido como PackageReference.)
- System.Numerics.Vectors
Note
Embora recomendemos usar os pacotes NuGet para configurar o seu projeto, pode adicionar manualmente as referências necessárias. Para mais informações, consulte Melhorar a sua aplicação de ambiente de trabalho para Windows. A tabela seguinte mostra os ficheiros aos quais precisa de adicionar referências.
| File | Local |
|---|---|
| System.Runtime.WindowsRuntime | C:\Windows\Microsoft.NET\Framework\v4.0.30319 |
| Windows.Foundation.UniversalApiContract.winmd | C:\Program Files (x86)\Windows Kits\10\References<sdk versão>\Windows.Foundation.UniversalApiContract<versão> |
| Windows.Foundation.FoundationContract.winmd | C:\Program Files (x86)\Windows Kits\10\References<versão do SDK>\Windows.Foundation.FoundationContract<versão> |
| 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 |
Configure o projeto para estar ciente do DPI por monitor
O conteúdo da camada visual que adiciona à sua aplicação não escala automaticamente para corresponder às definições de DPI do ecrã onde é mostrado. Tens de ativar o reconhecimento de DPI por monitor para a tua aplicação e depois garantir que o código que usas para criar o conteúdo da camada visual tem em conta a escala atual de DPI quando a aplicação corre. Aqui, configuramos o projeto para ser consciente do DPI. Nas secções seguintes, mostramos como usar a informação DPI para escalar o conteúdo da camada visual.
As aplicações WPF são conscientes do DPI do Sistema por defeito, mas precisam de se declarar conscientes do DPI por monitor num ficheiro app.manifest. Para ativar a sensibilidade a DPI por monitor a nível do Windows no ficheiro de manifesto da aplicação:
No Explorador de Soluções , clique com o botão direito do mouse no projeto HelloComposition.
No menu de contexto, selecione Adicionar>Novo Item....
No diálogo Adicionar Novo Item , selecione 'Application Manifest File' e depois clique em Adicionar. (Pode deixar o nome padrão.)
No ficheiro app.manifest, encontre este xml e deixe de comentar:
<application xmlns="urn:schemas-microsoft-com:asm.v3"> <windowsSettings> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware> </windowsSettings> </application>Adicione esta definição após a etiqueta de abertura
<windowsSettings>:<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>Também precisa de definir a definição DoNotScaleForDpiChanges no ficheiro App.config.
Abre o App.Config e adiciona este xml dentro do
<configuration>elemento:<runtime> <AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false"/> </runtime>
Note
AppContextSwitchOverrides só podem ser definidos uma vez. Se a sua aplicação já tiver um conjunto, deve delimitar este interruptor com ponto e vírgula dentro do atributo valor.
(Para mais informações, consulte o Guia de Desenvolvedor Per Monitor DPI e exemplos no GitHub.)
Criar uma classe derivada do HwndHost para hospedar elementos de composição
Para alojar conteúdo que crias com a camada visual, precisas de criar uma classe que derive do HwndHost. É aqui que fazes a maior parte da configuração para alojar APIs de Composição. Nesta aula, utilizas Platform Invocation Services (PInvoke) e COM Interop para integrar APIs de Composições na tua aplicação de WPF. Para mais informações sobre o Invoke e o COM Interop, veja Interoperar com código não gerido.
Sugestão
Se você precisar, verifique o código completo no final do tutorial para se certificar de que todo o código está nos lugares certos enquanto você trabalha no tutorial.
Adicione um novo ficheiro de classe ao seu projeto que derive do HwndHost.
- No Explorador de Soluções , clique com o botão direito do mouse no projeto HelloComposition.
- No menu de contexto, selecione Adicionar>classe....
- No diálogo Adicionar Novo Item , nomeie a classe CompositionHost.cs e depois clique em Adicionar.
Em CompositionHost.cs, edite a definição da classe para derivar de HwndHost.
// Add // using System.Windows.Interop; namespace HelloComposition { class CompositionHost : HwndHost { } }Adicione o seguinte código e construtor à classe.
// 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; }Sobrepor os métodos BuildWindowCore e DestroyWindowCore .
Note
No BuildWindowCore, chamas os métodos InitializeCoreDispatcher e InitComposition . Você cria esses métodos nas próximas etapas.
// 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); }- CreateWindowEx e DestroyWindow requerem uma declaração PInvoke. Coloque esta declaração no final do código da classe.
#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 declarationsInicializar um thread com um CoreDispatcher. O despachante principal é responsável pelo processamento de mensagens de janelas e por despachar eventos para APIs WinRT. Novas instâncias do CoreDispatcher devem ser criadas numa thread que tenha um CoreDispatcher.
- Crie um método chamado InitializeCoreDispatcher e adicione código para configurar a fila do despachante.
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; }- A fila do dispatcher também requer uma declaração PInvoke. Coloque esta declaração na região das declarações PInvoke que criou no passo anterior.
//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);Agora tem a fila do dispatcher pronta e pode começar a inicializar e criar Conteúdo de Composição.
Inicializar o Compositor. O Compositor é uma fábrica que cria uma variedade de tipos no namespace Windows.UI.Composition que inclui visuais, o sistema de efeitos e animação. A classe Compositor também gere a vida útil dos objetos criados a partir da fábrica.
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 e ICompositionTarget requerem importações COM. Coloque este código após a classe CompositionHost , mas dentro da declaração do namespace.
#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
Crie um UserControl para adicionar o seu conteúdo à árvore visual do WPF
O último passo para configurar a infraestrutura necessária para alojar conteúdo de Composição é adicionar o HwndHost à árvore visual do WPF.
Criar um Controlo de Utilizador
Um UserControl é uma forma conveniente de empacotar o seu código que cria e gere conteúdo de composição, adicionando facilmente o conteúdo ao seu XAML.
Adicione um novo ficheiro de controlo de utilizador ao seu projeto.
- No Explorador de Soluções , clique com o botão direito do mouse no projeto HelloComposition.
- No menu de contexto, selecione Adicionar>Controlo do Utilizador....
- No diálogo Adicionar Novo Item , nomeie o controlo do utilizador CompositionHostControl.xaml, depois clique em Adicionar.
Tanto os ficheiros CompositionHostControl.xaml como CompositionHostControl.xaml.cs são criados e adicionados ao seu projeto.
No CompositionHostControl.xaml, substitui as
<Grid> </Grid>etiquetas por este elemento Border , que é o contentor XAML onde o teu HwndHost irá entrar.<Border Name="CompositionHostElement"/>
No código do controlo do utilizador, crias uma instância da classe CompositionHost que criaste no passo anterior e adicionas-na como um elemento filho do CompositionHostElement, a borda que criaste na página XAML.
Em CompositionHostControl.xaml.cs, adiciona variáveis privadas para os objetos que vais usar no teu código de Composição. Adicione-os após a definição da classe.
CompositionHost compositionHost; Compositor compositor; Windows.UI.Composition.ContainerVisual containerVisual; DpiScale currentDpi;Adicione um handler para o evento Loaded do controlo do utilizador. É aqui que configuras a tua instância CompositionHost.
- No construtor, conecte o manipulador de eventos conforme mostrado aqui (
Loaded += CompositionHostControl_Loaded;).
public CompositionHostControl() { InitializeComponent(); Loaded += CompositionHostControl_Loaded; }- Adicione o método do gestor de eventos com o nome 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; } }Neste método, configuras os objetos que vais usar no teu código de composição. Aqui está uma rápida visão do que está a acontecer.
- Primeiro, certifique-se de que a configuração só é feita uma vez, verificando se já existe uma instância do 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) { }- Obtenha o DPI atual. Isto é usado para escalar corretamente os seus elementos de Composição.
currentDpi = VisualTreeHelper.GetDpi(this);- Crie uma instância de CompositionHost e atribua-a como Filho da Borda, CompositionHostElement.
compositionHost = new CompositionHost(ControlHostElement.ActualHeight, ControlHostElement.ActualWidth); ControlHostElement.Child = compositionHost;- Obtenha o Compositor no CompositionHost.
compositor = compositionHost.Compositor;- Usa o Compositor para criar um visual de container. Este é o container de composição ao qual adiciona os seus elementos.
containerVisual = compositor.CreateContainerVisual(); compositionHost.Child = containerVisual;- No construtor, conecte o manipulador de eventos conforme mostrado aqui (
Adicionar elementos de composição
Com a infraestrutura instalada, agora você pode gerar o conteúdo de composição que deseja mostrar.
Neste exemplo, adiciona-se código que cria e anima um simples quadrado SpriteVisual.
Adicione um elemento de composição. Em CompositionHostControl.xaml.cs, adicione estes métodos à classe 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); }
Controlar alterações no DPI
O código para adicionar e animar um elemento tem em conta a escala atual do DPI quando os elementos são criados, mas também precisa de ter em conta as alterações do DPI enquanto a aplicação está a correr. Pode gerir o evento HwndHost.DpiChanged para ser notificado das alterações e ajustar os seus cálculos com base no novo DPI.
No método CompositionHostControl_Loaded, após a última linha, adicione isto para ligar o handler de eventos DpiChanged.
compositionHost.DpiChanged += CompositionHost_DpiChanged;Adicione o método do gestor de eventos com o nome CompositionHostDpiChanged. Este código ajusta a escala e o deslocamento de cada elemento, e recalcula quaisquer animações que não estejam completas.
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); } }
Adicione o controlo do utilizador à sua página XAML
Agora, podes adicionar o controlo do utilizador à tua interface XAML.
No MainWindow.xaml, define a altura da janela para 600 e a largura para 840.
Adicione o XAML para a interface. No MainWindow.xaml, adicione este XAML entre as etiquetas de raiz
<Grid> </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"/>Manuseie o clique do botão para criar novos elementos. (O evento Click já está associado no XAML.)
Em MainWindow.xaml.cs, adicione este método Button_Click do gestor de eventos. Este código chama CompositionHost.AddElement para criar um novo elemento com tamanho e deslocamento gerados aleatoriamente.
// 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); }
Agora pode construir e executar a sua aplicação WPF. Se precisar, verifique o código completo no final do tutorial para se certificar de que todo o código está nos lugares certos.
Ao executar o aplicativo e clicar no botão, você verá quadrados animados adicionados à interface do usuário.
Passos seguintes
Para um exemplo mais completo que se baseia na mesma infraestrutura, veja o exemplo WPF Visual layer integration no GitHub.
Recursos adicionais
- Início (WPF) (.NET)
- Interoperar com código não gerenciado (.NET)
- Introdução às aplicações Windows (UWP)
- Aprimore o seu aplicativo de desktop para Windows (UWP)
- Espaço de nomes Windows.UI.Composition (UWP)
Código completo
Aqui está o código completo deste tutorial.
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
}