자습서: WinUI 3를 사용하여 간단한 사진 뷰어 만들기

참고 항목

WinUI 3의 이점과 다른 앱 유형 옵션에 대한 자세한 내용은 앱 개발 옵션 개요를 참조하세요.

이 토픽에서는 Visual Studio에서 새 WinUI 3 프로젝트를 만든 다음, 사진을 표시하는 간단한 앱을 빌드하는 과정을 안내합니다. 컨트롤, 레이아웃 패널 및 데이터 바인딩을 사용할 예정입니다. 그리고 XAML 태그(선언적)과 사용자가 선택한 C# 또는 C++ 코드(명령형 또는 절차적)를 모두 작성하게 됩니다. 항목 제목 위의 언어 선택기를 사용하여 C# 또는 C++/WinRT를 선택합니다.

이 항목의 소스 코드는 C# 및 C++/WinRT로 제공됩니다. C++ 개발자라면 여기에 표시된 코드의 작동 방식을 설명하는 자세한 내용과 개념을 보려면 C++/WinRT 설명서를 참조하세요. 관련 항목에는 XAML 컨트롤이 포함됩니다. C++/WinRT 속성에 바인딩, XAML 항목 컨트롤; C++/WinRT 컬렉션Photo 편집기 C++/WinRT 샘플 애플리케이션에 바인딩합니다.

1단계: Windows App SDK용 도구 설치

개발 컴퓨터를 설정하려면 Windows 앱 SDK용 도구 설치를 참조하세요. 그 후 필요에 따라 첫 번째 WinUI 3 프로젝트 만들기를 수행할 수 있습니다.

Important

Windows 앱 SDK 릴리스 채널 항목과 함께 릴리스 정보 항목을 찾을 수 있습니다. 각 채널에 대한 릴리스 정보가 있습니다. 해당 릴리스 정보에서 제한 사항 및 알려진 문제를 꼭 확인하세요. 이 자습서를 따라 하고/하거나 빌드할 앱을 실행한 결과에 영향을 미칠 수 있기 때문입니다.

2단계: 새 프로젝트 만들기

Visual Studio의 빈 앱, 패키지(데스크톱의 WinUI 3) 프로젝트 템플릿에서 원하는 새 C# 또는 C++ 프로젝트를 만듭니다. 프로젝트 이름을 SimplePhotos로 지정하고, (폴더 구조가 이 자습서에서 설명하는 구조와 일치하도록) 솔루션 및 프로젝트를 같은 디렉터리에 배치를 선택 취소합니다. 클라이언트 운영 체제의 최신 릴리스(미리 보기 아님)를 대상으로 지정할 수 있습니다.

3단계: 자산 파일 복사

우리가 빌드할 앱은 이미지 파일을 자산 파일 형식으로 보유하며, 자산 파일은 앱이 표시하는 사진입니다. 이 섹션에서는 이러한 자산을 프로젝트에 추가할 것입니다. 하지만 먼저 파일 복사본을 얻어야 합니다.

  1. 따라서 Windows 앱 SDK 샘플 리포지토리를 복제(또는 .zip 파일로 다운로드)합니다(WindowsAppSDK-Samples 참조). 그런 다음 \WindowsAppSDK-Samples\Samples\PhotoEditor\cs-winui\Assets\Samples 폴더에서 사용할 자산 파일을 찾을 수 있습니다(C# 및 C++/WinRT 프로젝트 모두에 이 폴더 사용). 리포지토리에 있는 해당 파일을 온라인으로 보려면 WindowsAppSDK-Samples/Samples/Photo편집기/cs-winui/Assets/Samples/를 방문하세요.

  2. 파일 탐색기에서 해당 Samples 폴더를 선택하고 클립보드에 복사합니다.

  1. Visual Studio에서 솔루션 탐색기로 이동합니다. Assets 폴더(프로젝트 노드의 자식 폴더)를 마우스 오른쪽 단추로 클릭하고 파일 탐색기에서 폴더 열기를 클릭합니다. 그러면 파일 탐색기에서 Assets 폴더가 열립니다.

  2. 방금 복사한 Samples 폴더를 Assets 폴더에 붙여넣습니다.

4단계: GridView 컨트롤 추가

앱은 사진의 행과 열을 표시해야 합니다. 즉, 이미지의 그리드를 표시해야 합니다. 이러한 UI의 경우 사용할 기본 컨트롤은 목록 보기 및 그리드 보기입니다.

  1. MainWindow.xaml을(를) 여십시오. 현재 Window 요소가 있고, 이 요소 안에는 StackPanel 레이아웃 패널이 있습니다. StackPanel 안에는 이벤트 처리기 메서드에 연결된 단추 컨트롤이 있습니다.

    앱의 메인 페이지에는 앱을 실행할 때 가장 먼저 보이는 보기가 표시됩니다. 우리가 빌드할 앱에서 주 창의 작업은 Samples 폴더의 사진을 로드하고, 해당 이미지의 타일식 보기를 다양한 관련 정보와 함께 표시하는 것입니다.

  2. StackPanel단추 태그를 아래에 표시된 그리드 레이아웃 패널 및 GridView 컨트롤로 바꿉니다.

    <Window ...>
        <Grid>
            <GridView x:Name="ImageGridView"/>
        </Grid>
    </Window>
    

    x:Name은 XAML 요소를 식별하므로 XAML의 다른 위치와 코드 숨김에서 참조할 수 있습니다.

  3. C#. MainWindow.xaml.cs 파일을 열고 myButton_Click 메서드를 삭제합니다.

  4. C++/WinRT. MainWindow.xaml.hMainWindow.xaml.cpp를 열고 myButton_Click 메서드를 삭제합니다.

지금 빌드하고 실행해도 되지만, 지금은 창이 비어 있습니다. GridView 컨트롤에서 항목을 표시하려면 표시할 데이터 컬렉션을 제공해야 합니다. 다음으로 이 내용을 다루겠습니다.

방금 언급한 몇 가지 유형에 대한 배경 정보는 레아아웃 패널Windows 앱용 컨트롤을 참조하세요.

5단계: ImageFileInfo 모델

모델은 (모델, 보기 및 보기 모델의 맥락에서) 실제 개체 또는 개념(예: 은행 계좌)을 어느 정도까지 나타내는 클래스입니다. 모델은 실제 존재의 추상화입니다. 이 섹션에서는 프로젝트에 ImageFileInfo라는 새 클래스를 추가하겠습니다. ImageFileInfo는 사진과 같은 이미지 파일의 모델이 됩니다. 이 섹션에서는 앱의 UI(사용자 인터페이스)에 사진을 표시할 수 있는 방법에 한 걸음 더 가까이 다가갈 것입니다.

아래 코드 예제를 준비하기 위해 관찰 가능한이라는 용어를 소개하겠습니다. XAML 컨트롤에 동적으로 바인딩할 수 있는 속성(속성 값이 변할 때마다 UI가 업데이트되도록)을 관찰 가능한 속성이라고 합니다. 이 아이디어는 ‘관찰자 패턴’이라고 알려진 소프트웨어 디자인 패턴에 바탕을 두고 있습니다. 이 자습서에서 빌드하는 앱에서는 ImageFileInfo 모델의 속성이 변하지 않습니다. 그럼에도 불구하고 INotifyPropertyChanged 인터페이스를 구현하도록 하여 ImageFileInfo를 관찰 가능하게 만드는 방법을 보여 드리겠습니다.

  1. 프로젝트 노드(SimplePhotos)를 마우스 오른쪽 단추로 클릭하고, 새 항목 추가>를 클릭합니다.... C# 항목>코드 아래에서 클래스를 선택합니다. 이름을 ImageFileInfo.cs로 설정하고 추가를 클릭합니다.

  2. ImageFileInfo.cs의 콘텐츠를 아래 코드 목록으로 바꿉니다.

    using Microsoft.UI.Xaml.Media.Imaging;
    using System;
    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    using System.Threading.Tasks;
    using Windows.Storage;
    using Windows.Storage.FileProperties;
    using Windows.Storage.Streams;
    
    namespace SimplePhotos
    {
        public class ImageFileInfo : INotifyPropertyChanged
        {
            public ImageFileInfo(ImageProperties properties,
                StorageFile imageFile,
                string name,
                string type)
            {
                ImageProperties = properties;
                ImageName = name;
                ImageFileType = type;
                ImageFile = imageFile;
                var rating = (int)properties.Rating;
                var random = new Random();
                ImageRating = rating == 0 ? random.Next(1, 5) : rating;
            }
    
            public StorageFile ImageFile { get; }
    
            public ImageProperties ImageProperties { get; }
    
            public async Task<BitmapImage> GetImageSourceAsync()
            {
                using IRandomAccessStream fileStream = await ImageFile.OpenReadAsync();
    
                // Create a bitmap to be the image source.
                BitmapImage bitmapImage = new();
                bitmapImage.SetSource(fileStream);
    
                return bitmapImage;
            }
    
            public async Task<BitmapImage> GetImageThumbnailAsync()
            {
                StorageItemThumbnail thumbnail = 
                    await ImageFile.GetThumbnailAsync(ThumbnailMode.PicturesView);
                // Create a bitmap to be the image source.
                var bitmapImage = new BitmapImage();
                bitmapImage.SetSource(thumbnail);
                thumbnail.Dispose();
    
                return bitmapImage;
            }
    
            public string ImageName { get; }
    
            public string ImageFileType { get; }
    
            public string ImageDimensions => $"{ImageProperties.Width} x {ImageProperties.Height}";
    
            public string ImageTitle
            {
                get => string.IsNullOrEmpty(ImageProperties.Title) ? ImageName : ImageProperties.Title;
                set
                {
                    if (ImageProperties.Title != value)
                    {
                        ImageProperties.Title = value;
                        _ = ImageProperties.SavePropertiesAsync();
                        OnPropertyChanged();
                    }
                }
            }
    
            public int ImageRating
            {
                get => (int)ImageProperties.Rating;
                set
                {
                    if (ImageProperties.Rating != value)
                    {
                        ImageProperties.Rating = (uint)value;
                        _ = ImageProperties.SavePropertiesAsync();
                        OnPropertyChanged();
                    }
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            protected void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    
  3. ImageFileInfo.cs 파일을 저장한 후 닫습니다.

6단계: 이미지 컬렉션에 대한 속성 정의 및 채우기

이 섹션에서는 MainWindow 클래스에 새 속성을 추가합니다. 속성(Images)은 표시하려는 이미지가 포함된 컬렉션 클래스가 됩니다.

  1. MainWindow.xaml.cs에서 다음과 같은 속성을 정의합니다.

    ...
    using System.Collections.ObjectModel;
    ...
    namespace SimplePhotos
    {
        public sealed partial class MainWindow : Window
        {
            public ObservableCollection<ImageFileInfo> Images { get; } = 
                new ObservableCollection<ImageFileInfo>();
            ...
        }
    }
    
  2. 새 컬렉션 속성을 이미지로 채우는 코드는 아래의 GetItemsAsyncLoadImageInfoAsync 메서드에 나와 있습니다. using 지시문과 두 메서드 구현을 MainWindow.xaml.cs에 붙여넣습니다. 이러한 메서드는 MainWindow 클래스의 멤버이므로 위의 Images 속성과 마찬가지로 그 안에 붙여넣습니다.

    ...
    using System.Threading.Tasks;
    using Windows.ApplicationModel;
    using Windows.Storage;
    using Windows.Storage.Search;
    ...
    private async Task GetItemsAsync()
    {
        StorageFolder appInstalledFolder = Package.Current.InstalledLocation;
        StorageFolder picturesFolder = await appInstalledFolder.GetFolderAsync("Assets\\Samples");
    
        var result = picturesFolder.CreateFileQueryWithOptions(new QueryOptions());
    
        IReadOnlyList<StorageFile> imageFiles = await result.GetFilesAsync();
        foreach (StorageFile file in imageFiles)
        {
            Images.Add(await LoadImageInfoAsync(file));
        }
    
        ImageGridView.ItemsSource = Images;
    }
    
    public async static Task<ImageFileInfo> LoadImageInfoAsync(StorageFile file)
    {
        var properties = await file.Properties.GetImagePropertiesAsync();
        ImageFileInfo info = new(properties, 
                                 file, file.DisplayName, file.DisplayType);
    
        return info;
    }
    
  3. 이 섹션에서 해야 하는 마지막 일은 GetItemsAsync를 호출하도록 MainWindow의 생성자를 업데이트하는 것입니다.

    public MainWindow()
    {
        ...
        GetItemsAsync();
    }
    

단계를 올바르게 따라 했는지 확인하고 싶다면 지금 앱을 빌드하고 앱을 실행해도 되지만, 현재 단계에서는 창에 볼 것이 별로 없습니다. 지금까지 수행한 작업은 GridViewImageFileInfo 형식의 개체 컬렉션을 렌더링하라고 요청하는 것입니다. 그리고 GridView는 아직 렌더링 방법을 모릅니다.

Images 속성은 ImageFileInfo 개체의 관찰 가능한 컬렉션입니다. 그리고 GetItemsAsync의 마지막 줄은 GridView(이름이 ImageGridView)에 해당 항목의 원본(ItemsSource)이 Images 속성임을 알려줍니다. GridView의 작업은 이러한 항목을 표시하는 것입니다.

하지만 우리는 아직 ImageFileInfo 클래스에 대해 GridView에 아무 것도 지시하지 않았습니다. 따라서 GridView가 현재 할 수 있는 최선은 컬렉션에 있는 각 ImageFileInfo 개체의 ToString 값을 표시하는 것입니다. 기본적으로 이 값은 형식의 이름일 뿐입니다. 다음 섹션에서는 ImageFileInfo 개체를 표시하는 방법을 정의하는 데이터 템플릿을 만듭니다.

위에서 관찰 가능한 컬렉션이라는 용어를 사용했습니다. 이 자습서에서 빌드하는 앱에서는 이미지 수가 변하지 않습니다(그리고 말했듯이 각 이미지의 속성 값도 변하지 않음). 하지만 데이터 바인딩을 사용하여 처음에 UI를 데이터에 연결하는 것은 여전히 편리하고 좋은 방법입니다. 따라서 우리도 이렇게 하겠습니다.

7단계: 데이터 템플릿 추가

먼저 스케치처럼 보이는 자리 표시자 데이터 템플릿을 사용하겠습니다. 레이아웃 옵션 탐색을 완료할 때까지 사용됩니다. 그 후에는 데이터 템플릿을 업데이트하여 실제 사진을 표시할 수 있습니다.

이것은 매우 실용적인 방법입니다. UI가 스케치처럼 보이는 경우(즉, 충실도가 낮음) 사람들은 빠른 아이디어를 제안 및/또는 기꺼이 테스트할 의향이 있으며, 때로는 상당한 변화가 수반되기도 합니다. 이 경우 사람들은 큰 부담 없이 변화를 시도해 볼 수 있다고 (맞게) 생각하기 때문입니다.

반대로 UI 모양의 완성도가 높을수록(높은 충실도) 현재 모양을 만들기 위해 많은 노력이 들어갔다고 사람들은 (다시, 맞게) 생각합니다. 따라서 사람들은 새로운 아이디어를 제안하거나 시도해 보고 싶은 마음이 적게 듭니다.

  1. MainWindow.xaml을 열고, 다음 태그처럼 보이도록 Window의 내용을 변경합니다.

    <Window ...>
        <Grid>
            <Grid.Resources>
                <DataTemplate x:Key="ImageGridView_ItemTemplate">
                    <Grid/>
                </DataTemplate>
            </Grid.Resources>
            <GridView x:Name="ImageGridView"
                    ItemTemplate="{StaticResource ImageGridView_ItemTemplate}">
            </GridView>
        </Grid>
    </Window>
    

    레이아웃 루트에는 간단한 DataTemplate 리소스를 추가하고 ImageGridView_ItemTemplate 키를 제공했습니다. 또한 동일한 키를 사용하여 GridViewItemTemplate을 설정했습니다. 앞에서 본 ItemsSource 속성이 있는 것과 마찬가지로 GridView와 같은 항목 컨트롤에는 ItemTemplate 속성이 있습니다. 항목 템플릿은 데이터 템플릿이며, 컬렉션의 각 항목을 표시하는 데 사용됩니다.

    자세한 내용은 항목 컨테이너 및 템플릿을 참조하세요.

  2. 이제 데이터 템플릿을 약간 편집(내부 요소를 추가하고 편집)하여 더 흥미롭고 유용하게 만들 수 있습니다. 루트 그리드의 높이와 너비를 300, 여백을 8로 지정하겠습니다. 그리고 행 정의 2개를 추가하고, 두 번째 행 정의의 높이를 자동으로 설정하겠습니다.

    <DataTemplate x:Key="ImageGridView_ItemTemplate">
        <Grid Height="300"
              Width="300"
              Margin="8">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
        </Grid>
    </DataTemplate>
    

    자세한 내용은 맞춤, 여백, 안쪽 여백을 참조하세요.

  3. 우리가 원하는 것은 데이터 템플릿에서 각 사진의 이미지, 이름, 파일 형식, 차원 및 등급을 표시하는 것입니다. 따라서 각각 Image 컨트롤 하나, TextBlock 컨트롤 몇 개, RatingControl 컨트롤 하나를 추가하겠습니다. StackPanel 레이아웃 패널 내부에 텍스트를 배치하겠습니다. Image는 처음에 프로젝트의 스케치처럼 보이는 Microsoft Store 로고를 자리 표시자로 표시합니다.

  4. 이렇게 편집을 마치면 데이터 템플릿의 모양은 다음과 같습니다.

    <DataTemplate x:Key="ImageGridView_ItemTemplate">
        <Grid Height="300"
              Width="300"
              Margin="8">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
    
            <Image x:Name="ItemImage"
                   Source="Assets/StoreLogo.png"
                   Stretch="Uniform" />
    
            <StackPanel Orientation="Vertical"
                        Grid.Row="1">
                <TextBlock Text="ImageTitle"
                           HorizontalAlignment="Center"
                           Style="{StaticResource SubtitleTextBlockStyle}" />
                <StackPanel Orientation="Horizontal"
                            HorizontalAlignment="Center">
                    <TextBlock Text="ImageFileType"
                               HorizontalAlignment="Center"
                               Style="{StaticResource CaptionTextBlockStyle}" />
                    <TextBlock Text="ImageDimensions"
                               HorizontalAlignment="Center"
                               Style="{StaticResource CaptionTextBlockStyle}"
                               Margin="8,0,0,0" />
                </StackPanel>
    
                <RatingControl Value="3" IsReadOnly="True"/>
            </StackPanel>
        </Grid>
    </DataTemplate>
    

이제 앱을 빌드하고 실행하면 방금 만든 항목 템플릿이 있는 GridView 컨트롤이 보입니다. 다음으로, 항목이 배치되는 방법을 살펴보겠습니다. 일부 브러시를 변경하고 항목 사이에 공백을 추가하겠습니다.

The placeholder item template.

8단계: 항목 컨테이너 스타일 편집

GridView와 같은 항목 컨트롤과 관련된 또 다른 개념은 항목 컨테이너입니다. 항목 컨테이너는 항목을 Content 속성의 값으로 표시하는 콘텐츠 컨트롤입니다. 항목 컨트롤은 언제든지 화면에 보이는 항목을 표시하기 위해 필요한 만큼 항목 컨테이너를 만듭니다.

컨트롤인 항목 컨테이너에는 스타일과 컨트롤 템플릿이 있습니다. 스타일 및 컨트롤 템플릿은 항목 컨테이너가 다양한 상태(예: 선택, 가리키기 및 포커스)에서 표시되는 방식을 결정합니다. 그리고 여기서 볼 수 있듯이, 항목 템플릿(데이터 템플릿)은 항목 자체의 모양을 결정합니다.

GridView의 경우 해당 항목 컨테이너의 형식은 GridViewItem입니다.

따라서 이 섹션에서는 항목 컨테이너의 스타일을 디자인하는 데 집중하겠습니다. 이를 위해 GridViewItem에 대한 Style 리소스를 만든 다음, *GridViewItemContainerStyle로 설정하겠습니다. 스타일에서는 항목 컨테이너의 BackgroundMargin 속성을 설정하여 회색 배경과 바깥쪽 주위에 약간의 여백을 지정합니다.

  1. MainWindow.xaml에서 데이터 템플릿을 배치한 동일한 Grid.Resources XML 요소에 새 Style 리소스를 추가합니다.

    <Grid>
        <Grid.Resources>
        ...
            <Style x:Key="ImageGridView_ItemContainerStyle"
                TargetType="GridViewItem">
                <Setter Property="Background" Value="Gray"/>
                <Setter Property="Margin" Value="8"/>
            </Style>
        </Grid.Resources>
    
  2. 다음으로, ImageGridView_ItemContainerStyle 키를 사용하여 GridViewItemContainerStyle을 설정합니다.

    <GridView x:Name="ImageGridView"
            ...
            ItemContainerStyle="{StaticResource ImageGridView_ItemContainerStyle}">
    </GridView>
    

앱을 빌드 및 실행하고, 어떤 모양인지 봅니다. 창의 크기를 조정할 때 GridView 컨트롤은 공간에 가장 잘 맞도록 항목을 다시 정렬합니다. 어떤 너비에서는 앱 창의 오른쪽에 공간이 많습니다. GridView 및/또는 해당 콘텐츠가 가운데에 있을 때 더 보기 좋습니다. 따라서 다음으로 이 부분을 처리하겠습니다.

실험하려면 BackgroundMargin 속성을 다른 값으로 설정하고 어떻게 되는지 확인해 보세요.

9단계: 레이아웃 실험

GridView 자체를 가운데에 두는 것이 가장 좋은지 아니면 콘텐츠를 가운데에 두는 것이 가장 좋은지 궁금할 수 있습니다. 먼저 GridView를 가운데에 배치해 보겠습니다.

  1. GridView가 창 안에서 정확히 어디에 있으며 레이아웃을 실험할 때 어떤 일이 벌어지는지 쉽게 확인할 수 있도록 Background 속성을 빨간색으로 설정합니다.

    <GridView x:Name="ImageGridView"
            ...
            Background="Red">
    </GridView>
    
  2. 이제 HorizontalAlignment 속성을 Center로 설정합니다.

    <GridView x:Name="ImageGridView"
            ...
            HorizontalAlignment="Center">
    </GridView>
    

    맞춤, 여백, 안쪽 여백도 참조하세요.

이제 앱을 빌드 및 실행하고, 창 너비를 조정하여 실험합니다. GridView의 빨간색 배경 양쪽에 동일한 크기의 빈 공간이 있음을 확인할 수 있습니다. 이미지를 가운데에 배치한다는 목표를 달성했습니다. 그러나 스크롤 막대가 창이 아닌 GridView에 속한다는 것이 이전보다 명확해졌습니다. 따라서 창을 채우도록 GridView를 다시 변경해야 합니다. 창에서 GridView를 가운데에 배치하는 대신 GridView에서 이미지를 가운데에 배치해야 한다는 것을 설명했습니다.

  1. 이제 이전 단계에서 추가한 HorizontalAlignment 특성을 삭제합니다.

10단계: 항목 패널 템플릿 편집

항목 컨트롤은 항목 컨테이너를 항목 패널 내부에 배치합니다. GridView항목 패널 템플릿을 편집하여 사용되는 패널 종류를 정의하고 해당 패널의 속성을 설정할 수 있습니다. 이번 섹션에서는 이러한 작업을 수행합니다.

  1. MainWindow.xaml에서 리소스 사전에 ItemsPanelTemplate 리소스를 추가합니다. 항목 패널은 ItemsWrapGrid 형식이며 HorizontalAlignment 속성을 Center로 설정하겠습니다.

    <Grid>
        <Grid.Resources>
        ...
            <ItemsPanelTemplate x:Key="ImageGridView_ItemsPanelTemplate">
                <ItemsWrapGrid Orientation="Horizontal"
                               HorizontalAlignment="Center"/>
            </ItemsPanelTemplate>
        </Grid.Resources>
    
  2. 다음으로, ImageGridView_ItemsPanelTemplate 키를 사용하여 GridViewItemsPanel을 설정합니다.

    <GridView x:Name="ImageGridView"
            ...
            ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}">
    </GridView>
    

앱을 빌드 및 실행하고 창의 너비를 조정하는 실험을 수행하면 이미지 양쪽에 GridView의 빨간색 배경이 같은 크기로 표시됩니다. GridView가 창을 채우기 때문에 스크롤 막대는 사용자가 예상할 수 있는 창 가장자리와 잘 맞춰집니다.

  1. 레이아웃 실험을 마쳤으므로 GridView에서 Background="Red"를 제거합니다.

11단계: 자리 표시자 이미지를 사진으로 바꾸기

이제 스케치의 충실도를 높일 시간입니다. 즉, 자리 표시자 이미지를 실제 이미지로 바꾸고 "lorem ipsum" 스타일 자리 표시자 텍스트를 실제 데이터로 바꿉니다. 이미지부터 처리하겠습니다.

Important

Assets\Samples 폴더에 사진을 표시하기 위해 사용할 기술은 GridView의 항목을 점진적으로 업데이트하는 것입니다. 특히 ContainerContentChangingEventArgs.InRecycleQueueContainerContentChangingEventArgs.Phase 속성 사용을 포함하여 아래 코드 예제의 ImageGridView_ContainerContentChangingShowImage 메서드에 있는 코드입니다. 자세한 내용은 ListView 및 GridView UI 최적화를 참조하세요. 그러나 간단히 말해서 GridView는 항목 컨테이너 중 하나가 항목을 표시할 준비가 되면 이벤트를 통해 알려 줍니다. 그러면 항목 컨테이너의 현재 업데이트 수명 주기 단계를 추적하여 사진 데이터를 표시할 준비가 완료되는 시기를 확인할 수 있습니다.

  1. MainWindow.xaml.cs에서 MainWindowImageGridView_ContainerContentChanging이라는 새 메서드를 추가합니다. 이 메서드는 이벤트 처리 메서드이며, 이 메서드가 처리하는 이벤트는 ContainerContentChanging입니다. 또한 ImageGridView_ContainerContentChanging이 의존하는 ShowImage 메서드의 구현을 제공해야 합니다. using 지시문과 두 메서드 구현을 MainWindow.xaml.cs에 붙여넣습니다.

    ...
    using Microsoft.UI.Xaml.Controls;
    ...
    private void ImageGridView_ContainerContentChanging(
        ListViewBase sender,
        ContainerContentChangingEventArgs args)
    {
        if (args.InRecycleQueue)
        {
            var templateRoot = args.ItemContainer.ContentTemplateRoot as Grid;
            var image = templateRoot.FindName("ItemImage") as Image;
            image.Source = null;
        }
    
        if (args.Phase == 0)
        {
            args.RegisterUpdateCallback(ShowImage);
            args.Handled = true;
        }
    }
    
    private async void ShowImage(ListViewBase sender, ContainerContentChangingEventArgs args)
    {
        if (args.Phase == 1)
        {
            // It's phase 1, so show this item's image.
            var templateRoot = args.ItemContainer.ContentTemplateRoot as Grid;
            var image = templateRoot.FindName("ItemImage") as Image;
            var item = args.Item as ImageFileInfo;
            image.Source = await item.GetImageThumbnailAsync();
        }
    }
    
  1. 그런 다음, MainWindow.xaml에서 ImageGridView_ContainerContentChanging 이벤트 처리기를 GridViewContainerContentChanging 이벤트에 등록합니다.

    <GridView x:Name="ImageGridView"
            ...
            ContainerContentChanging="ImageGridView_ContainerContentChanging">
    </GridView>
    

12단계: 개체 틀 텍스트를 실제 데이터로 바꾸기

이 섹션에서는 일회성 데이터 바인딩을 사용합니다. 일회성 바인딩은 런타임에 변하지 않는 데이터에 적합합니다. 즉, 일회성 바인딩은 고성능이며 쉽게 만들 수 있습니다.

  1. MainWindow.xaml에서 ImageGridView_ItemTemplate 데이터 템플릿 리소스를 찾습니다. 데이터 템플릿에 해당 작업은 ImageFileInfo 클래스의 템플릿이 될 것이라고 알리겠습니다. 이 클래스는 GridView가 표시하는 항목의 형식입니다.

  2. 이렇게 하려면 다음과 같이 템플릿에 x:DataType 값을 추가합니다.

    <DataTemplate x:Key="ImageGridView_ItemTemplate"
                  x:DataType="local:ImageFileInfo">
        ...
    

    위에 표시된 local: 구문(또는 이미 여는 태그에 있는 xmlns:local 구문)에 익숙하지 않은 경우 XAML 네임스페이스 및 네임스페이스 매핑을 참조하세요.

    이제 x:DataType을 설정했으므로, 데이터 템플릿에서 x:Bind 데이터 바인딩 식을 사용하여 지정한 데이터 형식의 속성(여기서는 ImageFileInfo)에 바인딩할 수 있습니다.

  3. 데이터 템플릿에서 첫 번째 TextBlock 요소(Text가 현재 ImageTitle로 설정된 요소)를 찾습니다. 아래와 같이 Text 값을 바꿉니다.

    아래의 태그를 복사하여 붙여넣어도 되고 Visual Studio에서 IntelliSense를 사용해도 됩니다. 이렇게 하려면 따옴표 안에 있는 현재 값을 선택하고 {를 입력합니다. IntelliSense가 닫는 중괄호를 자동으로 추가하고 코드 완성 목록을 표시합니다. 아래로 x:Bind가 나올 때까지 스크롤하여 두 번 클릭해도 됩니다. 그러나 x:를 입력하고(x:Bind가 완성 목록의 맨 위로 어떻게 필터링되는지 참고) Tab 키를 누르는 것이 더 효율적일 수 있습니다. 이제 스페이스 키를 누르고, ImageT를 입력하고(완료 목록의 맨 위로 이동하는 데 필요한 만큼 속성 이름 ImageTitle), TAB 키를 누릅니다.

    <TextBlock Text="{x:Bind ImageTitle}"
        ... />
    

    x:Bind 식은 UI 속성 값을 data-object 속성 값과 연결합니다. 물론 이것은 도구와 런타임이 바인딩할 수 있는 속성을 알 수 있도록 x:DataType을 해당 data-object의 형식으로 설정하는 방법에 따라 달라집니다.

    자세한 내용은 {x:Bind} 태그 확장데이터 바인딩 심층 분석을 참조하세요.

  4. 동일한 방식으로 다른 TextBlockRatingControl의 값을 바꿉니다. 결과:

    <TextBlock Text="{x:Bind ImageTitle}" ... />
    <StackPanel ... >
        <TextBlock Text="{x:Bind ImageFileType}" ... />
        <TextBlock Text="{x:Bind ImageDimensions}" ... />
    </StackPanel>
    <RatingControl Value="{x:Bind ImageRating}" ... />
    

이제 앱을 빌드하고 실행하면 자리 표시자 대신 실제 사진과 실제 텍스트(및 기타 데이터)가 표시됩니다. 이 간단하고 작은 앱이 시각적으로, 기능적으로 완성되었습니다. 하지만 마지막으로 데이터 바인딩을 약간만 해보겠습니다.

The finished app.

13단계: GridView를 Images 컬렉션에 바인딩(C#만 해당)

Important

C# 프로젝트를 만든 경우에만 이 마지막 단계를 수행합니다.

XAML 태그에서는 할 수 없는 몇 가지 작업(일반적으로 동적으로 생성된 UI와 관련됨)이 있음을 알 수 있습니다. 그러나 일반적으로 태그에서 무언가를 할 수 있다면 그 방법이 더 선호됩니다. 그 방법은 XAML 태그가 나타내는 보기와 명령적 코드가 나타내는 모델(또는 보기 모델)을 약간 더 깔끔하게 구분합니다. 또한 도구와 팀 구성원 간의 워크플로를 개선하는 경향이 있습니다.

현재 명령적 코드를 사용하여 GridViewItemsSource 속성을 MainWindowImages 속성과 연결 중입니다. 하지만 태그에서도 연결할 수 있습니다.

  1. MainWindow 클래스에서 ImageGridViewItemsSourceImages 속성의 값으로 설정하는 GetItemsAsync의 마지막 줄을 삭제(또는 주석 처리)합니다.

  2. 그리고 다음과 같이 MainWindow.xaml에서 ImageGridView라는 GridView를 찾은 다음, ItemsSource 특성을 추가합니다. 원한다면 IntelliSense를 사용하여 변경해도 됩니다.

    <GridView x:Name="ImageGridView"
              ...
              ItemsSource="{x:Bind Images}"
    

Images 속성 값은 이 앱의 런타임에 변하지 않습니다. 그러나 ImagesObservableCollection<T> 형식이므로 컬렉션의 콘텐츠가 변할 수 있으며(즉, 요소가 추가 또는 삭제될 수 있음) 바인딩은 변경 내용을 자동으로 확인하고 UI를 업데이트합니다.

결론

이 자습서에서는 Visual Studio를 사용하여 사진을 표시하는 간단한 WinUI 3 앱을 빌드하는 과정을 알아보았습니다. 이 자습서가 WinUI 3 앱에서 컨트롤, 레이아웃 패널, 데이터 바인딩 및 GridView UI 최적화를 경험하는 계기가 되었기를 바랍니다.

참고 항목

자습서: 여러 플랫폼을 대상으로 하는 간단한 사진 뷰어 빌드