Composition Visuals 構成了 Microsoft.UI.Composition API 所有其他功能所使用的視覺樹狀結構。 API 允許開發者定義並建立一個或多個視覺物件,每個物件代表視覺樹中的一個節點。
視覺效果
視覺樹結構是由多種視覺類型組成,並有一個基礎刷子類別及其多個子類別來影響視覺內容。
- Visual – 基礎物件,大部分屬性都在這裡,並由其他視覺物件繼承。
-
ContainerVisual – 源自 Visual,並新增創建子節點的能力。
- SpriteVisual – 源自 ContainerVisual。 具備將畫筆與 Visual 關聯的能力,使 Visual 能渲染包括影像、特效或純色在內的像素。
- LayerVisual – 源自 ContainerVisual。 視覺的子節點被壓扁成一層。
- ShapeVisual – 源自 ContainerVisual。 一個視覺樹節點,是 CompositionShape 的根節點。
- RedirectVisual – 源自 ContainerVisual。 視覺內容來自另一個視覺。
- SceneVisual – 源自 ContainerVisual。 用於 3D 場景節點的容器視覺表示。
你可以使用 CompositionBrush 及其子類別,包括 CompositionColorBrush、 CompositionSurfaceBrush 和 CompositionEffectBrush,將內容和效果套用到 SpriteVisuals。 想了解更多關於畫筆的資訊,請參考 CompositionBrush 概述。
作品視覺範例
在這裡,我們將看看一些 WinUI 3 範例程式碼,展示前面提到的三種不同視覺類型。 雖然這個範例不涵蓋動畫或更複雜的特效,但它包含了所有這些系統所使用的基礎組件。 (完整範例程式碼列於本文末尾。)
範例中有多個純色方格,可以點擊並在螢幕上拖曳。 當點擊方塊時,它會向前移動,旋轉45度,拖曳時會變得不透明。
此圖展示了多項使用 API 的基本概念,包括:
- 創建合成器
- 使用 CompositionColorBrush 建立 SpriteVisual
- 剪切視覺
- 旋轉顯示畫面
- 設定不透明度
- 變更視覺元素在集合中的位置。
創建合成器
建立 合成器 並將其儲存在變數中作為工廠使用是一項簡單的任務。 在 WinUI 應用程式中,通常你會從已連接到視覺樹的 XAML 元素中取得合成器:
Compositor compositor = ElementCompositionPreview.GetElementVisual(MyHost).Compositor;
如果你需要合成器而且沒有 UIElement,可以用 CompositionTarget.GetCompositorForCurrentThread() 代替。
建立 `SpriteVisual` 與 `ColorBrush`
使用 Compositor 可以輕鬆隨時建立物件,例如 SpriteVisual 和 CompositionColorBrush:
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。