共用方式為


視覺構圖

Composition Visuals 構成了 Microsoft.UI.Composition API 所有其他功能所使用的視覺樹狀結構。 API 允許開發者定義並建立一個或多個視覺物件,每個物件代表視覺樹中的一個節點。

視覺效果

視覺樹結構是由多種視覺類型組成,並有一個基礎刷子類別及其多個子類別來影響視覺內容。

你可以使用 CompositionBrush 及其子類別,包括 CompositionColorBrushCompositionSurfaceBrushCompositionEffectBrush,將內容和效果套用到 SpriteVisuals。 想了解更多關於畫筆的資訊,請參考 CompositionBrush 概述

作品視覺範例

在這裡,我們將看看一些 WinUI 3 範例程式碼,展示前面提到的三種不同視覺類型。 雖然這個範例不涵蓋動畫或更複雜的特效,但它包含了所有這些系統所使用的基礎組件。 (完整範例程式碼列於本文末尾。)

範例中有多個純色方格,可以點擊並在螢幕上拖曳。 當點擊方塊時,它會向前移動,旋轉45度,拖曳時會變得不透明。

此圖展示了多項使用 API 的基本概念,包括:

  • 創建合成器
  • 使用 CompositionColorBrush 建立 SpriteVisual
  • 剪切視覺
  • 旋轉顯示畫面
  • 設定不透明度
  • 變更視覺元素在集合中的位置。

創建合成器

建立 合成器 並將其儲存在變數中作為工廠使用是一項簡單的任務。 在 WinUI 應用程式中,通常你會從已連接到視覺樹的 XAML 元素中取得合成器:

Compositor compositor = ElementCompositionPreview.GetElementVisual(MyHost).Compositor;

如果你需要合成器而且沒有 UIElement,可以用 CompositionTarget.GetCompositorForCurrentThread() 代替。

建立 `SpriteVisual` 與 `ColorBrush`

使用 Compositor 可以輕鬆隨時建立物件,例如 SpriteVisualCompositionColorBrush

var visual = _compositor.CreateSpriteVisual();
visual.Brush = _compositor.CreateColorBrush(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF));

雖然這只是幾行程式碼,但它展現了一個強大的概念: SpriteVisual 物件是特效系統的核心。 SpriteVisual 在色彩、影像與特效創作上提供極大的彈性與互動。 SpriteVisual 是一種單一視覺類型,可以用筆刷(此處為純色)填充 2D 矩形。

剪裁圖像

合成器也可以用來製作視覺化的片段。 以下是使用 InsetClip 裁切視覺兩側的範例:

var clip = _compositor.CreateInsetClip();
clip.LeftInset = 1.0f;
clip.RightInset = 1.0f;
clip.TopInset = 1.0f;
clip.BottomInset = 1.0f;
_currentVisual.Clip = clip;

和 API 中的其他物件一樣, InsetClip 也可以套用動畫到其屬性。

旋轉剪輯

圖像可以透過旋轉來進行轉換。 請注意 ,RotationAngle 支援弧度和度數。 預設為弧度,但如下片段所示,可以簡單地指定度數:

child.RotationAngleInDegrees = 45.0f;

旋轉只是 API 提供的一組轉換元件之一,用以簡化這些任務。 其他包括偏移量、縮放比例、方向、旋轉軸和 4x4 變換矩陣。

設定不透明度

設定視覺化的不透明度是一個使用浮點數值的簡單操作。 例如,樣本中所有方格的不透明度皆為 0.8:

visual.Opacity = 0.8f;

像旋轉一樣, 不透明度 屬性也可以被動畫化。

改變圖像在收藏中的位置

Composition API 允許在 VisualCollection 中使用多種方式更改 Visual 的位置。 它可以用 InsertAbove 放在另一個視覺上方,用 InsertBelow 放在下方,用 InsertAtTop 移到頂部,或用 InsertAtBottom 放到底部。

在範例中,被點擊的Visual會被排序到最上方:

parent.Children.InsertAtTop(_currentVisual);

完整範例

在完整的 WinUI 範例中,將上述所有概念結合使用,以構建並遍歷簡單的 可視化 物件樹狀結構,無需自訂 DirectX 渲染即可改變不透明度。 範例使用 WinUI Grid 進行指標輸入,並使用 ElementCompositionPreview 主控組合樹。

<Page
    x:Class="CompositionVisualSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Grid
        x:Name="MyHost"
        Background="Transparent"
        PointerPressed="OnPointerPressed"
        PointerMoved="OnPointerMoved"
        PointerReleased="OnPointerReleased" />
</Page>
using System;
using System.Numerics;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Input;
using Windows.Foundation;
using Windows.UI;

namespace CompositionVisualSample
{
    public sealed partial class MainPage : Page
    {
        private readonly Random _random = new();
        private Compositor _compositor;
        private ContainerVisual _root;
        private ContainerVisual _currentVisual;
        private Vector2 _offsetBias;
        private bool _dragging;

        public MainPage()
        {
            InitializeComponent();
            Loaded += OnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            if (_root != null)
            {
                return;
            }

            _compositor = ElementCompositionPreview.GetElementVisual(MyHost).Compositor;
            _root = _compositor.CreateContainerVisual();
            ElementCompositionPreview.SetElementChildVisual(MyHost, _root);

            for (int index = 0; index < 20; index++)
            {
                _root.Children.InsertAtTop(CreateChildElement());
            }
        }

        private ContainerVisual CreateChildElement()
        {
            var element = _compositor.CreateContainerVisual();
            element.Size = new Vector2(100.0f, 100.0f);

            var maxX = (float)Math.Max(MyHost.ActualWidth - element.Size.X, 0.0);
            var maxY = (float)Math.Max(MyHost.ActualHeight - element.Size.Y, 0.0);
            element.Offset = new Vector3(
                (float)(_random.NextDouble() * maxX),
                (float)(_random.NextDouble() * maxY),
                0.0f);

            var outer = _compositor.CreateSpriteVisual();
            outer.Size = new Vector2(100.0f, 100.0f);
            outer.Brush = _compositor.CreateColorBrush(Colors.White);
            element.Children.InsertAtTop(outer);

            var inner = _compositor.CreateSpriteVisual();
            inner.Offset = new Vector3(3.0f, 3.0f, 0.0f);
            inner.Size = new Vector2(94.0f, 94.0f);

            byte red = (byte)(0xFF * (0.2f + (_random.NextDouble() / 0.8f)));
            byte green = (byte)(0xFF * (0.2f + (_random.NextDouble() / 0.8f)));
            byte blue = (byte)(0xFF * (0.2f + (_random.NextDouble() / 0.8f)));
            inner.Brush = _compositor.CreateColorBrush(Color.FromArgb(0xFF, red, green, blue));
            outer.Children.InsertAtTop(inner);

            element.Opacity = 0.8f;
            return element;
        }

        private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
        {
            Point position = e.GetCurrentPoint(MyHost).Position;
            _currentVisual = null;

            foreach (var child in _root.Children)
            {
                Vector3 offset = child.Offset;
                Vector2 size = child.Size;

                if ((position.X >= offset.X) &&
                    (position.X < offset.X + size.X) &&
                    (position.Y >= offset.Y) &&
                    (position.Y < offset.Y + size.Y))
                {
                    _currentVisual = child as ContainerVisual;
                    _offsetBias = new Vector2(
                        (float)(offset.X - position.X),
                        (float)(offset.Y - position.Y));
                }
            }

            if (_currentVisual != null)
            {
                ContainerVisual parent = (ContainerVisual)_currentVisual.Parent;
                parent.Children.Remove(_currentVisual);
                parent.Children.InsertAtTop(_currentVisual);
            }
        }

        private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
        {
            if (_currentVisual == null)
            {
                return;
            }

            if (!_dragging)
            {
                _currentVisual.Opacity = 1.0f;

                foreach (var child in _currentVisual.Children)
                {
                    child.RotationAngleInDegrees = 45.0f;
                    child.CenterPoint = new Vector3(_currentVisual.Size.X / 2, _currentVisual.Size.Y / 2, 0.0f);
                    break;
                }

                var clip = _compositor.CreateInsetClip();
                clip.LeftInset = 1.0f;
                clip.RightInset = 1.0f;
                clip.TopInset = 1.0f;
                clip.BottomInset = 1.0f;
                _currentVisual.Clip = clip;

                _dragging = true;
            }

            Point position = e.GetCurrentPoint(MyHost).Position;
            _currentVisual.Offset = new Vector3(
                (float)(position.X + _offsetBias.X),
                (float)(position.Y + _offsetBias.Y),
                0.0f);
        }

        private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
        {
            if (_currentVisual == null)
            {
                return;
            }

            if (_dragging)
            {
                foreach (var child in _currentVisual.Children)
                {
                    child.RotationAngle = 0.0f;
                    child.CenterPoint = new Vector3(0.0f, 0.0f, 0.0f);
                    break;
                }

                _currentVisual.Opacity = 0.8f;
                _currentVisual.Clip = null;
                _dragging = false;
            }

            _currentVisual = null;
        }
    }
}

如果你需要在有 XAML 主機元素前先初始化合成器,請先使用 CompositionTarget.GetCompositorForCurrentThread(),然後在主機元素可用時將視覺元素附加到你的 UI。