Używanie warstwy wizualnej z WPF

Interfejsy API kompozycji środowisko wykonawcze systemu Windows (nazywane również warstwą Visual layer) można używać w aplikacjach Windows Presentation Foundation (WPF), aby wprowadzać nowoczesne funkcje, które ożywiają się dla użytkowników Windows.

Kompletny kod tego samouczka jest dostępny na GitHubie: przykład WPF HelloComposition.

Wymagania wstępne

Interfejs API platformy UWP do hostingu XAML ma następujące wymagania wstępne.

Jak używać interfejsów API kompozycji w WPF

W tym samouczku utworzysz prosty interfejs użytkownika aplikacji WPF i dodasz do niego animowane elementy kompozycji. Zarówno komponenty WPF, jak i Kompozycja, są proste, ale pokazany kod międzyoperacyjności jest taki sam, niezależnie od złożoności komponentów. Gotowa aplikacja wygląda następująco.

interfejs użytkownika działającej aplikacji

Tworzenie projektu WPF

Pierwszym krokiem jest utworzenie projektu aplikacji WPF, który zawiera definicję aplikacji i stronę XAML interfejsu użytkownika.

Aby utworzyć nowy projekt aplikacji WPF w języku Visual C# o nazwie HelloComposition:

  1. Otwórz program Visual Studio i wybierz pozycję Plik >Nowy>Projekt.

    Zostanie otwarte okno dialogowe Nowy projekt .

  2. W kategorii Installed rozwiń kategorię Visual C# node, a następnie wybierz Windows Desktop.

  3. Wybierz szablon WPF App (.NET Framework).

  4. Wprowadź nazwę HelloComposition wybierz pozycję Framework .NET Framework 4.7.2 a następnie kliknij pozycję OK.

    Visual Studio tworzy projekt i otwiera edytor dla domyślnego okna aplikacji MainWindow.xaml.

Konfigurowanie projektu do używania interfejsów API środowiska uruchomieniowego systemu Windows

Aby używać interfejsów API środowisko wykonawcze systemu Windows (WinRT) w aplikacji WPF, należy skonfigurować projekt Visual Studio w celu uzyskania dostępu do środowisko wykonawcze systemu Windows. Ponadto wektory są szeroko używane przez interfejsy API kompozycji, dlatego należy dodać odwołania wymagane do użycia wektorów.

Pakiety NuGet są dostępne dla obu tych potrzeb. Zainstaluj najnowsze wersje tych pakietów, aby dodać niezbędne odwołania do projektu.

Note

Zalecamy skonfigurowanie projektu przy użyciu pakietów NuGet, ale można ręcznie dodać wymagane odwołania. Aby uzyskać więcej informacji, zobacz Ulepsz swoją aplikację na komputery stacjonarne dla Windows. W poniższej tabeli przedstawiono pliki, do których należy dodać odwołania.

File Lokalizacja
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

Konfigurowanie projektu pod kątem rozpoznawania dpi na monitor

Zawartość warstwy wizualnej dodawanej do aplikacji nie jest automatycznie skalowana w celu dopasowania do ustawień DPI ekranu, na którym jest wyświetlana. Należy włączyć rozpoznawanie DPI per-monitor dla aplikacji i upewnić się, że kod używany do tworzenia zawartości warstwy wizualnej uwzględnia bieżącą skalę DPI podczas uruchamiania aplikacji. W tym miejscu skonfigurujemy projekt tak, aby był świadomy dpi. W kolejnych sekcjach pokazano, jak używać informacji DPI do skalowania zawartości warstwy wizualizacji.

Aplikacje WPF są domyślnie świadome DPI systemowego, ale muszą zadeklarować się jako świadome DPI na monitorze w pliku "app.manifest". Aby włączyć rozpoznawanie DPI na poziomie systemu Windows dla każdego monitora w pliku manifestu aplikacji:

  1. W Eksploratorze rozwiązań kliknij prawym przyciskiem myszy projekt HelloComposition .

  2. W menu kontekstowym wybierz pozycję Dodaj>nowy element....

  3. W oknie dialogowym Dodawanie nowego elementu wybierz pozycję "Plik manifestu aplikacji", a następnie kliknij przycisk Dodaj. (Możesz pozostawić nazwę domyślną).

  4. W pliku app.manifest znajdź ten plik XML i usuń jego komentarz:

    <application xmlns="urn:schemas-microsoft-com:asm.v3">
        <windowsSettings>
          <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
        </windowsSettings>
      </application>
    
  5. Dodaj to ustawienie po tagu otwierania <windowsSettings> :

          <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
    
  6. Należy również ustawić ustawienie DoNotScaleForDpiChanges w pliku App.config.

    Otwórz plik App.Config i dodaj ten plik XML wewnątrz <configuration> elementu:

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

Note

Właściwości AppContextSwitchOverrides można ustawić tylko raz. Jeśli aplikacja ma już jedno ustawienie, należy oddzielić ten przełącznik średnikiem wewnątrz atrybutu wartości.

(Aby uzyskać więcej informacji, zobacz przewodnik dewelopera Per Monitor DPI Developer Guide and samples on GitHub.)

Tworzenie klasy pochodnej HwndHost do hostowania elementów kompozycji

Aby hostować zawartość tworzoną za pomocą warstwy wizualnej, należy utworzyć klasę pochodzącą z biblioteki HwndHost. W tym miejscu wykonujesz większość konfiguracji do hostowania interfejsów API Kompozycji. W ramach tej lekcji użyjesz Platform Invocation Services (PInvoke) i COM Interop w celu zintegrowania interfejsów API kompozycji z aplikacją WPF. Aby uzyskać więcej informacji na temat międzyoperacyjności PInvoke i COM, zobacz Interoperating with unmanaged code (Współdziałanie z niezarządzanymi kodami).

Wskazówka

Jeśli chcesz, sprawdź kompletny kod na końcu samouczka, aby upewnić się, że cały kod znajduje się w odpowiednich miejscach podczas pracy z samouczkiem.

  1. Dodaj nowy plik klasy do projektu, który pochodzi z HwndHost.

    • W Eksploratorze rozwiązań kliknij prawym przyciskiem myszy projekt HelloComposition .
    • W menu kontekstowym wybierz pozycję Dodaj>klasę....
    • W oknie dialogowym Dodawanie nowego elementu nadaj klasie nazwę CompositionHost.cs, a następnie kliknij przycisk Dodaj.
  2. W CompositionHost.cs edytuj definicję klasy, aby pochodzić z HwndHost.

    // Add
    // using System.Windows.Interop;
    
    namespace HelloComposition
    {
        class CompositionHost : HwndHost
        {
        }
    }
    
  3. Dodaj następujący kod i konstruktor do klasy .

    // 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. Zastąpi metody BuildWindowCore i DestroyWindowCore .

    Note

    W obszarze BuildWindowCore wywołasz metody InitializeCoreDispatcher i InitComposition . Te metody są tworzone w następnych krokach.

    // 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. Zainicjuj wątek za pomocą narzędzia CoreDispatcher. Główny dyspozytor jest odpowiedzialny za przetwarzanie komunikatów okien i wysyłanie zdarzeń dla interfejsów API WinRT. Nowe wystąpienia narzędzia CoreDispatcher należy utworzyć w wątku, który ma moduł CoreDispatcher.

    • Utwórz metodę o nazwie InitializeCoreDispatcher i dodaj kod w celu skonfigurowania kolejki dyspozytora.
    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;
    }
    
    • Kolejka dyspozytora wymaga również deklaracji PInvoke. Umieść tę deklarację wewnątrz regionu deklaracji PInvoke utworzonego w poprzednim kroku.
    //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);
    

    Masz teraz gotową kolejkę dyspozytora i możesz rozpocząć inicjowanie i tworzenie zawartości kompozycji.

  6. Zainicjuj kompositor. Kompositor to fabryka, która tworzy różnorodne typy w przestrzeni nazw Windows.UI.Composition, obejmując wizualizacje, system efektów oraz system animacji. Klasa Compositor zarządza również okresem istnienia obiektów utworzonych z fabryki.

    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 i ICompositionTarget wymagają importu modelu COM. Umieść ten kod po klasie CompositionHost , ale wewnątrz deklaracji przestrzeni nazw.
    #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
    

Tworzenie kontrolki UserControl w celu dodania zawartości do drzewa wizualizacji WPF

Ostatnim krokiem do skonfigurowania infrastruktury wymaganej do hostowania zawartości kompozycji jest dodanie HwndHost do drzewa wizualizacji WPF.

Stwórz element UserControl

Kontrolka UserControl to wygodny sposób tworzenia i zarządzania zawartością Composition oraz łatwego dodawania zawartości do kodu XAML.

  1. Dodaj nowy plik kontrolki użytkownika do projektu.

    • W Eksploratorze rozwiązań kliknij prawym przyciskiem myszy projekt HelloComposition .
    • W menu kontekstowym wybierz pozycję Dodaj>kontrolkę użytkownika....
    • W oknie dialogowym Dodawanie nowego elementu nadaj kontrolce użytkownika nazwę CompositionHostControl.xaml, a następnie kliknij przycisk Dodaj.

    Pliki CompositionHostControl.xaml i CompositionHostControl.xaml.cs są tworzone i dodawane do projektu.

  2. W pliku CompositionHostControl.xaml zastąp tagi <Grid> </Grid> elementem Border, który jest kontenerem XAML, w którym umieszczony będzie host HwndHost.

    <Border Name="CompositionHostElement"/>
    

W kodzie kontrolki utworzysz wystąpienie klasy CompositionHost utworzonej w poprzednim kroku i dodasz je jako element podrzędny do CompositionHostElement, obramowania utworzonego na stronie XAML.

  1. W CompositionHostControl.xaml.cs dodaj zmienne prywatne dla obiektów, które będą używane w kodzie kompozycji. Dodaj je po definicji klasy.

    CompositionHost compositionHost;
    Compositor compositor;
    Windows.UI.Composition.ContainerVisual containerVisual;
    DpiScale currentDpi;
    
  2. Dodaj procedurę obsługi dla zdarzenia załadowanego kontrolki użytkownika. W tym miejscu skonfigurujesz wystąpienie CompositionHost.

    • W konstruktorze podłącz program obsługi zdarzeń, jak pokazano tutaj (Loaded += CompositionHostControl_Loaded;).
    public CompositionHostControl()
    {
        InitializeComponent();
        Loaded += CompositionHostControl_Loaded;
    }
    
    • Dodaj metodę obsługi zdarzeń o nazwie 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;
        }
    }
    

    W tej metodzie skonfigurujesz obiekty, których będziesz używać w kodzie Kompozycja. Oto krótkie spojrzenie na to, co się dzieje.

    • Najpierw upewnij się, że konfiguracja jest wykonywana tylko raz, sprawdzając, czy instancja obiektu CompositionHost już istnieje.
    // 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)
    {
    
    }
    
    • Pobierz bieżące DPI. Służy do prawidłowego skalowania elementów kompozycji.
    currentDpi = VisualTreeHelper.GetDpi(this);
    
    • Utwórz wystąpienie elementu CompositionHost i przypisz je jako element podrzędny elementu Border, CompositionHostElement.
    compositionHost =
        new CompositionHost(ControlHostElement.ActualHeight, ControlHostElement.ActualWidth);
    ControlHostElement.Child = compositionHost;
    
    • Pobierz Compositor z elementu CompositionHost.
    compositor = compositionHost.Compositor;
    
    • Użyj kompositora, aby utworzyć wizualizację kontenera. Jest to kontener kompozycji, do którego są dodawane elementy kompozycji.
    containerVisual = compositor.CreateContainerVisual();
    compositionHost.Child = containerVisual;
    

Dodawanie elementów kompozycji

Po zainstalowaniu infrastruktury możesz teraz wygenerować zawartość kompozycji, którą chcesz wyświetlić.

W tym przykładzie dodasz kod, który tworzy i animuje prosty kwadrat SpriteVisual.

  1. Dodaj element kompozycji. W CompositionHostControl.xaml.cs dodaj te metody do klasy 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);
    }
    

Obsługa zmian DPI

Kod do dodawania i animowania elementu uwzględnia bieżącą skalę DPI podczas tworzenia elementów, ale należy również uwzględnić zmiany DPI podczas działania aplikacji. Możesz obsłużyć zdarzenie HwndHost.DpiChanged , aby otrzymywać powiadomienia o zmianach i dostosowywać obliczenia na podstawie nowego dpi.

  1. W metodzie CompositionHostControl_Loaded po ostatnim wierszu dodaj tę metodę, aby podłączyć program obsługi zdarzeń DpiChanged.

    compositionHost.DpiChanged += CompositionHost_DpiChanged;
    
  2. Dodaj metodę obsługi zdarzeń o nazwie CompositionHostDpiChanged. Ten kod dostosowuje skalę i przesunięcie każdego elementu i oblicza ponownie wszystkie nieukończone animacje.

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

Dodawanie kontrolki użytkownika do strony XAML

Teraz możesz dodać kontrolkę użytkownika do interfejsu użytkownika XAML.

  1. W pliku MainWindow.xaml ustaw wartość Wysokość okna na 600 i szerokość na 840.

  2. Dodaj kod XAML dla interfejsu użytkownika. W pliku MainWindow.xaml dodaj ten kod XAML między tagami głównymi <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"/>
    
  3. Zajmij się kliknięciem przycisku, aby utworzyć nowe elementy. (Zdarzenie Click zostało już podłączone do kodu XAML).

    W MainWindow.xaml.cs dodaj tę procedurę obsługi zdarzenia Button_Click. Ten kod wywołuje CompositionHost.AddElement, aby utworzyć nowy element z losowo wygenerowanym rozmiarem i przesunięciem.

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

Teraz możesz skompilować i uruchomić aplikację WPF. Jeśli chcesz, sprawdź kompletny kod na końcu samouczka, aby upewnić się, że cały kod znajduje się we właściwych miejscach.

Po uruchomieniu aplikacji i kliknięciu przycisku powinny zostać wyświetlone animowane kwadraty dodane do interfejsu użytkownika.

Następne kroki

Aby uzyskać bardziej kompletny przykład oparty na tej samej infrastrukturze, zobacz przykład integracji warstwy wizualnej WPF na GitHubie.

Dodatkowe zasoby

Kompletny kod

Oto kompletny kod tego samouczka.

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
}