Windows Presentation Foundation(WPF) 앱에서 Windows 런타임 컴퍼지션 API(비저 계층라고도 함)를 사용하여 Windows 사용자에게 밝은 최신 환경을 만들 수 있습니다.
이 자습서의 전체 코드는 GitHub WPF HelloComposition 샘플에서 사용할 수 있습니다.
사전 요구 사항
UWP XAML 호스팅 API에는 이러한 필수 구성 요소가 있습니다.
- WPF 및 UWP를 사용하는 앱 개발에 대해 잘 알고 있다고 가정합니다. 자세한 내용은 다음을 참조하세요.
- .NET Framework 4.7.2 이상
- Windows 10, 버전 1803 이상
- Windows 10 SDK 17134 이상
WPF 컴퍼지션 API를 사용하는 방법
이 자습서에서는 간단한 WPF 앱 UI를 만들고 애니메이션 컴퍼지션 요소를 추가합니다. WPF 구성 요소와 컴퍼지션 구성 요소는 모두 단순하게 유지되지만 표시된 interop 코드는 구성 요소의 복잡성에 관계없이 동일합니다. 완성된 앱은 다음과 같이 표시됩니다.
WPF 프로젝트 만들기
첫 번째 단계는 애플리케이션 정의 및 UI에 대한 XAML 페이지를 포함하는 WPF 앱 프로젝트를 만드는 것입니다.
Visual C#에서 HelloComposition이라는 새 WPF 애플리케이션 프로젝트를 만들려면 다음을 수행합니다.
Visual Studio를 열고 파일>새로 만들기>프로젝트를 선택합니다.
새 프로젝트 대화 상자가 열립니다.
사용 범주에서 비전 C# 노드를 확장한 다음, Windows Desktop 선택합니다.
WPF 앱(.NET Framework) 템플릿을 선택합니다.
이름 HelloComposition을 입력하고 프레임워크 .NET Framework 4.7.2 선택한 다음 OK 클릭합니다.
Visual Studio 프로젝트를 만들고 MainWindow.xaml이라는 기본 애플리케이션 창에 대한 디자이너를 엽니다.
Windows 런타임 API를 사용하도록 프로젝트 구성
WPF 앱에서 WinRT(Windows 런타임) API를 사용하려면 Windows 런타임 액세스하도록 Visual Studio 프로젝트를 구성해야 합니다. 또한 컴퍼지션 API에서 벡터를 광범위하게 사용하므로 벡터를 사용하는 데 필요한 참조를 추가해야 합니다.
NuGet 패키지는 이러한 두 요구 사항을 모두 해결하는 데 사용할 수 있습니다. 이러한 패키지의 최신 버전을 설치하여 프로젝트에 필요한 참조를 추가합니다.
- Microsoft.Windows. Sdk. Contracts(기본 패키지 관리 형식을 PackageReference로 설정해야 함)
- System.Numerics.Vectors
Note
NuGet 패키지를 사용하여 프로젝트를 구성하는 것이 좋지만 필요한 참조를 수동으로 추가할 수 있습니다. 자세한 내용은
| File | Location |
|---|---|
| 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 |
모니터별 DPI 인식되도록 프로젝트 구성
앱에 추가하는 시각적 계층 콘텐츠는 표시되는 화면의 DPI 설정과 일치하도록 자동으로 확장되지 않습니다. 앱에 대해 모니터별 DPI 인식을 사용하도록 설정한 다음, 시각적 계층 콘텐츠를 만드는 데 사용하는 코드가 앱이 실행되면 현재 DPI 규모를 고려해야 합니다. 여기서는 DPI를 인식하도록 프로젝트를 구성합니다. 이후 섹션에서는 DPI 정보를 사용하여 시각적 계층 콘텐츠의 크기를 조정하는 방법을 보여줍니다.
WPF 앱은 기본적으로 시스템 DPI를 인식하지만 app.manifest 파일에서 모니터별 DPI 인식으로 자신을 선언해야 합니다. 앱 매니페스트 파일에서 Windows 수준 모니터별 DPI 인식을 설정하려면 다음을 수행합니다.
솔루션 탐색기에서 HelloComposition 프로젝트를 마우스 오른쪽 버튼으로 클릭합니다.
상황에 맞는 메뉴에서새 항목>...를 선택합니다.
새 항목 추가 대화 상자에서 '애플리케이션 매니페스트 파일'을 선택한 다음 추가를 클릭합니다. (기본 이름을 그대로 둘 수 있습니다.)
app.manifest 파일에서 이 xml을 찾아 주석을 해제합니다.
<application xmlns="urn:schemas-microsoft-com:asm.v3"> <windowsSettings> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware> </windowsSettings> </application><windowsSettings>태그의 시작 후에 이 설정을 추가합니다.<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>또한 App.config 파일에서 DoNotScaleForDpiChanges 설정을 설정해야 합니다.
App.Config를 열고 요소 내에 다음 xml을 추가합니다
<configuration>.<runtime> <AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false"/> </runtime>
Note
AppContextSwitchOverrides는 한 번만 설정할 수 있습니다. 애플리케이션에 이미 하나의 집합이 있는 경우 값 특성 내에서 이 스위치를 세미콜론으로 구분해야 합니다.
자세한 내용은 GitHub Per Monitor DPI 개발자 가이드 및 샘플 참조하세요.)
HwndHost 파생 클래스를 만들어 컴퍼지션 요소를 호스트합니다.
시각적 계층을 사용하여 만든 콘텐츠를 호스트하려면 HwndHost에서 파생되는 클래스를 만들어야 합니다. 여기서 컴퍼지션 API를 호스트하기 위한 대부분의 구성을 수행합니다. 이 클래스에서는 PInvoke(Platform Invocation Services) 및 COM Interop을 사용하여 컴퍼지션 API를 WPF 앱으로 가져옵니다. PInvoke 및 COM Interop에 대한 자세한 내용은 관리되지 않는 코드와의 상호 운용을 참조하세요.
팁 (조언)
필요한 경우 튜토리얼의 끝 부분에 있는 전체 코드를 확인하여 튜토리얼 작업을 진행할 때 모든 코드가 올바른 위치에 있도록 합니다.
HwndHost에서 파생되는 새 클래스 파일을 프로젝트에 추가합니다.
- 솔루션 탐색기에서 HelloComposition 프로젝트를 마우스 오른쪽 버튼으로 클릭합니다.
- 상황에 맞는 메뉴에서 추가>클래스...를 선택합니다.
- 새 항목 추가 대화 상자에서 클래스 이름을 CompositionHost.cs 다음 추가를 클릭합니다.
CompositionHost.cs HwndHost에서 파생되도록 클래스 정의를 편집합니다.
// Add // using System.Windows.Interop; namespace HelloComposition { class CompositionHost : HwndHost { } }클래스에 다음 코드 및 생성자를 추가합니다.
// 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; }BuildWindowCore 및 DestroyWindowCore 메서드를 재정의합니다.
Note
BuildWindowCore에서 InitializeCoreDispatcher 및 InitComposition 메서드를 호출합니다. 다음 단계에서 이러한 메서드를 만듭니다.
// 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 및 DestroyWindow 에는 PInvoke 선언이 필요합니다. 클래스의 코드 끝에 이 선언을 배치합니다.
#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 declarationsCoreDispatcher를 사용하여 스레드를 초기화합니다. 핵심 디스패처는 창 메시지를 처리하고 WinRT API에 대한 이벤트를 디스패치합니다. CoreDispatcher가 있는 스레드에서 CoreDispatcher의 새 인스턴스를 만들어야 합니다.
- InitializeCoreDispatcher라는 메서드를 만들고 코드를 추가하여 디스패처 큐를 설정합니다.
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 선언도 필요합니다. 이전 단계에서 만든 PInvoke 선언 영역 내에 이 선언을 배치합니다.
//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);이제 디스패처 큐가 준비되었으며 컴퍼지션 콘텐츠를 초기화하고 만들 수 있습니다.
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); ICompositionTarget target = (ICompositionTarget)rawObject; if (raw == null) { throw new Exception("QI Failed"); } }- 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
UserControl을 만들어 WPF 시각적 트리에 콘텐츠 추가
컴퍼지션 콘텐츠를 호스트하는 데 필요한 인프라를 설정하는 마지막 단계는 WPF 시각적 트리에 HwndHost를 추가하는 것입니다.
UserControl 만들기
UserControl은 컴퍼지션 콘텐츠를 만들고 관리하는 코드를 패키지하고 XAML에 콘텐츠를 쉽게 추가하는 편리한 방법입니다.
프로젝트에 새 사용자 제어 파일을 추가합니다.
- 솔루션 탐색기에서 HelloComposition 프로젝트를 마우스 오른쪽 버튼으로 클릭합니다.
- 상황에 맞는 메뉴에서사용자 컨트롤>...를 선택합니다.
- 새 항목 추가 대화 상자에서 사용자 컨트롤의 이름을 CompositionHostControl.xaml로 지정하고 추가를 클릭합니다.
CompositionHostControl.xaml 및 CompositionHostControl.xaml.cs 파일이 만들어지고 프로젝트에 추가됩니다.
CompositionHostControl.xaml에서
<Grid> </Grid>태그를 HwndHost가 들어갈 XAML 컨테이너인 이 Border 요소로 교체하십시오.<Border Name="CompositionHostElement"/>
사용자 컨트롤에 대한 코드에서 이전 단계에서 만든 CompositionHost 클래스의 인스턴스를 만들고 XAML 페이지에서 만든 Border인 CompositionHostElement의 자식 요소로 추가합니다.
CompositionHostControl.xaml.cs 파일에, 컴퍼지션 코드에서 사용할 개체에 대한 프라이빗 변수를 추가하세요. 클래스 정의 뒤에 이것들을 추가하세요.
CompositionHost compositionHost; Compositor compositor; Windows.UI.Composition.ContainerVisual containerVisual; DpiScale currentDpi;사용자 컨트롤의 Loaded 이벤트에 대한 처리기를 추가합니다. 여기서 CompositionHost 인스턴스를 설정합니다.
- 생성자에서 여기에
Loaded += CompositionHostControl_Loaded;표시된 대로 이벤트 처리기를 연결합니다.
public CompositionHostControl() { InitializeComponent(); Loaded += CompositionHostControl_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(ControlHostElement.ActualHeight, ControlHostElement.ActualWidth); ControlHostElement.Child = compositionHost; compositor = compositionHost.Compositor; containerVisual = compositor.CreateContainerVisual(); compositionHost.Child = containerVisual; } }이 메서드에서는 컴퍼지션 코드에서 사용할 개체를 설정합니다. 다음은 무슨 일이 일어나고 있는지 간단히 살펴보겠습니다.
- 먼저 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) { }- 현재 DPI를 가져옵니다. 컴퍼지션 요소의 크기를 적절하게 조정하는 데 사용됩니다.
currentDpi = VisualTreeHelper.GetDpi(this);- CompositionHost의 인스턴스를 만들고 테두리의 자식인 CompositionHostElement로 할당합니다.
compositionHost = new CompositionHost(ControlHostElement.ActualHeight, ControlHostElement.ActualWidth); ControlHostElement.Child = compositionHost;- CompositionHost에서 Compositor를 가져옵니다.
compositor = compositionHost.Compositor;- Compositor를 사용하여 컨테이너 시각적 개체를 만듭니다. 컴포지션 요소를 추가하는 컴포지션 컨테이너입니다.
containerVisual = compositor.CreateContainerVisual(); compositionHost.Child = containerVisual;- 생성자에서 여기에
컴퍼지션 요소 추가
인프라가 준비되었습니다. 이제 표시하려는 컴퍼지션 콘텐츠를 생성할 수 있습니다.
이 예제에서는 간단한 Square SpriteVisual을 만들고 애니메이션 효과를 주는 코드를 추가합니다.
컴퍼지션 요소를 추가합니다. CompositionHostControl.xaml.cs 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); }
DPI 변경 처리
요소를 추가하고 애니메이션하는 코드는 요소를 만들 때 현재 DPI 크기를 고려하지만 앱이 실행되는 동안 DPI 변경 내용도 고려해야 합니다. HwndHost.DpiChanged 이벤트를 처리하여 변경 내용을 알리고 새 DPI에 따라 계산을 조정할 수 있습니다.
CompositionHostControl_Loaded 메서드에서 마지막 줄 뒤를 추가하여 DpiChanged 이벤트 처리기를 연결합니다.
compositionHost.DpiChanged += CompositionHost_DpiChanged;CompositionHostDpiChanged라는 이름으로 이벤트 처리기 메서드를 추가합니다. 이 코드는 각 요소의 배율과 오프셋을 조정하고 완료되지 않은 애니메이션을 다시 계산합니다.
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); } }
XAML 페이지에 사용자 컨트롤 추가
이제 XAML UI에 사용자 컨트롤을 추가할 수 있습니다.
MainWindow.xaml에서 창 높이를 600으로 설정하고 너비를 840으로 설정합니다.
UI에 대한 XAML을 추가합니다. MainWindow.xaml에서 루트
<Grid> </Grid>태그 사이에 이 XAML을 추가합니다.<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"/>버튼을 클릭하여 새 요소를 생성합니다. (Click 이벤트가 XAML에 이미 연결되었습니다.)
MainWindow.xaml.cs 이 Button_Click 이벤트 처리기 메서드를 추가합니다. 이 코드는 CompositionHost.AddElement 를 호출하여 임의로 생성된 크기와 오프셋을 사용하여 새 요소를 만듭니다.
// 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); }
이제 WPF 앱을 빌드하고 실행할 수 있습니다. 필요한 경우 튜토리얼의 끝 부분에 있는 전체 코드를 확인하여 모든 코드가 올바른 위치에 있도록 합니다.
앱을 실행하고 버튼을 클릭하면 UI에 애니메이션 효과가 있는 사각형이 추가된 것을 볼 수 있습니다.
다음 단계
동일한 인프라에서 빌드하는 보다 완전한 예제는 GitHub WPF 시각적 계층 통합 샘플 참조하세요.
추가 리소스
- 시작(WPF)(.NET)
- 관리되지 않는 코드로 상호 운용(.NET)
- Windows 앱 시작(UWP)
- Windows용 데스크톱 애플리케이션 개선(UWP)
- Windows.UI.Composition 네임스페이스(UWP)
전체 코드
이 자습서의 전체 코드는 다음과 같습니다.
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
}