다음을 통해 공유


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

간단한 시작용 사진 뷰어 WinUI 3 앱을 만든 후에는 앱을 다시 작성하지 않고도 더 많은 사용자가 사용할 수 있도록 하는 방법이 궁금할 수 있습니다. 이 자습서에서는 Uno 플랫폼을 통해 기존 C# WinUI 3 애플리케이션의 범위를 확장하여 네이티브 모바일, 웹 및 데스크톱에서 비즈니스 논리와 UI 계층을 재사용할 수 있도록 합니다. 간단한 사진 뷰어 앱을 최소한으로만 변경하면 이러한 플랫폼에 포팅된 앱의 픽셀 단위까지 완벽한 복사본을 실행할 수 있습니다.

웹 및 WinUI 데스크톱을 대상으로 하는 UnoSimplePhoto 앱의 스크린샷

필수 조건

  • Visual Studio 2022 17.4 이상

  • 개발 컴퓨터 설정(WinUI 시작 참조)

  • ASP.NET 및 웹 개발 워크로드(WebAssembly 개발용)

    Visual Studio의 웹 개발 워크로드 스크린샷

  • 설치된 .NET 다중 플랫폼 앱 UI 개발(iOS, Android, Mac Catalyst 개발용)

    Visual Studio의 dotnet 모바일 워크로드 스크린샷

  • 설치된 .NET 데스크톱 개발(Gtk, Wpf 및 Linux Framebuffer 개발용)

    Visual Studio의 dotnet 데스크톱 워크로드 스크린샷

환경 마무리

  1. 명령줄 프롬프트, Windows 터미널(설치된 경우)을 열거나 시작 메뉴에서 명령 프롬프트 또는 Windows Powershell을 엽니다.

  2. uno-check 도구를 설치하거나 업데이트합니다.

    • 다음 명령을 사용합니다.

      dotnet tool install -g uno.check
      
    • 이전에 이전 버전을 이미 설치한 경우 도구를 업데이트하려면 다음을 수행합니다.

      dotnet tool update -g uno.check
      
  3. 다음 명령을 사용하여 도구를 실행합니다.

    uno-check
    
  4. 도구에 표시된 지침을 따릅니다. 시스템을 수정해야 하기 때문에 높은 권한을 요청하는 메시지가 나타날 수 있습니다.

Uno 플랫폼 솔루션 템플릿 설치

Visual Studio를 시작한 다음 Continue without code를 클릭합니다. 메뉴 모음에서 Extensions ->Manage Extensions를 클릭합니다.

확장 관리를 읽는 Visual Studio 메뉴 모음 항목의 스크린샷

확장 관리자에서 온라인 노드 확장, Uno 검색, Uno Platform 확장 설치를 수행하거나, Visual Studio Marketplace에서 다운로드하여 설치한 다음 Visual Studio를 다시 시작합니다.

검색 결과로 Uno Platform 확장을 사용하는 Visual Studio의 확장 관리 창 스크린샷

애플리케이션 만들기

이제 다중 플랫폼 애플리케이션을 만들 준비가 되었으므로 수행할 방식은 새로운 Uno 플랫폼 애플리케이션을 만드는 것입니다. 이전 자습서의 SimplePhotos WinUI 3 프로젝트의 코드를 다중 플랫폼 프로젝트에 복사할 예정입니다. 이는 Uno 플랫폼을 통해 기존 코드베이스를 재사용할 수 있기 때문에 가능합니다. 각 플랫폼에서 제공하는 OS API에 의존하는 기능의 경우 시간이 지남에 따라 쉽게 작동하도록 할 수 있습니다. 이 방식은 다른 플랫폼으로 포팅하려는 기존 애플리케이션이 있는 경우 특히 유용합니다.

머지않아 이 방식의 이점을 활용할 수 있게 될 것입니다. 익숙한 XAML 특성과 이미 보유하고 있는 코드베이스를 사용하여 더 많은 플랫폼을 대상으로 할 수 있기 때문입니다.

Visual Studio를 열고 File>New>Project를 통해 새 프로젝트를 만듭니다.

새 프로젝트 만들기 대화 상자의 스크린샷

Uno를 검색하고 Uno 플랫폼 앱 프로젝트 템플릿을 선택합니다.

Uno Platform 앱을 선택한 프로젝트 유형으로 사용하여 새 프로젝트 만들기 대화 상자의 스크린샷

Visual Studio의 시작 페이지에서 Uno 플랫폼 앱 형식을 사용하여 새로운 C# 솔루션을 만듭니다. 이전 자습서의 코드와의 충돌을 피하기 위해 이 솔루션에 "UnoSimplePhotos"라는 다른 이름을 지정할 예정입니다. 프로젝트 이름, 솔루션 이름, 디렉터리를 지정합니다. 이 예에서 UnoSimplePhotos 다중 플랫폼 프로젝트는 C:\Projects에 있는 UnoSimplePhotos 솔루션에 속합니다.

새 Uno Platform 프로젝트에 대한 프로젝트 세부 정보를 지정하는 스크린샷

이제 Simple Photo 갤러리 애플리케이션을 다중 플랫폼으로 활용하기 위한 기본 템플릿을 선택할 예정입니다.

Uno 플랫폼 앱 템플릿에는 솔루션이나 Uno.Material 및 Uno.Toolkit 라이브러리에 대한 참조가 포함된 기본 구성을 빠르게 시작할 수 있는 두 가지 사전 설정 옵션이 함께 제공됩니다. 기본 구성에는 종속성 삽입, 구성, 탐색 및 로깅에 사용되는 Uno.Extensions도 포함됩니다. 또한 MVVM 대신 MVUX를 사용하므로 실제 애플리케이션을 빠르게 빌드하기 위한 훌륭한 출발점이 됩니다.

프로젝트 시작 유형에 대한 Uno 솔루션 템플릿의 스크린샷

작업을 단순하게 유지하려면 공백 사전 설정을 선택합니다. 그런 다음 만들기 단추를 클릭합니다. 프로젝트가 만들어지고 해당 종속성이 복원될 때까지 기다립니다.

편집기 상단의 배너에서 프로젝트를 다시 로드하라는 메시지가 표시될 수 있습니다. 프로젝트 다시 로드를 클릭합니다.

프로젝트를 다시 로드하여 변경 내용을 완료하는 Visual Studio 배너 제공 스크린샷

솔루션 탐색기에 다음과 같은 기본 파일 구조가 표시됩니다.

솔루션 탐색기 기본 파일 구조의 스크린샷

프로젝트에 이미지 자산 추가

앱에 표시하려면 일부 이미지가 필요합니다. 이전 자습서와 동일한 이미지를 사용할 수 있습니다.

UnoSimplePhotos 프로젝트에서 Assets라는 새 폴더를 만들고 JPG 이미지 파일을 Samples 하위 폴더에 복사합니다. 이제 Assets 폴더 구조는 다음과 같습니다.

새 파일 및 폴더가 추가된 Visual Studio의 솔루션 탐색기 창 스크린샷

Assets 폴더 만들기 및 폴더에 이미지 추가에 대한 자세한 내용은 자산 및 이미지 표시에 대한 Uno 플랫폼 설명서를 참조하세요.

앱 준비

이제 다중 플랫폼 WinUI 애플리케이션의 기능적 시작점을 생성했으므로 데스크톱 프로젝트에서 해당 애플리케이션에 코드를 복사할 수 있습니다.

뷰 복사

Uno 플랫폼을 사용하면 이미 익숙한 XAML 버전을 사용할 수 있으므로 이전 자습서에서 만든 것과 동일한 코드를 복사할 수 있습니다.

이전 자습서의 SimplePhotos 프로젝트로 돌아갑니다. 솔루션 탐색기에서 MainWindow.xaml이라는 파일을 찾아 엽니다. 뷰의 콘텐츠가 Page가 아닌 Window 요소 내에 정의되어 있음을 확인합니다. 이는 데스크톱 프로젝트가 Window 요소를 사용하여 뷰의 콘텐츠를 정의할 수 있는 WinUI 3 애플리케이션이기 때문입니다.

<Window x:Class="SimplePhotos.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:SimplePhotos"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">

    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="ImageGridView_ItemTemplate" 
                          x:DataType="local:ImageFileInfo">
                <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="{x:Bind ImageTitle}"
                                   HorizontalAlignment="Center"
                                   Style="{StaticResource SubtitleTextBlockStyle}" />
                        <StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Center">
                            <TextBlock Text="{x:Bind ImageFileType}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}" />
                            <TextBlock Text="{x:Bind ImageDimensions}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}"
                                       Margin="8,0,0,0" />
                        </StackPanel>

                        <RatingControl Value="{x:Bind ImageRating}" 
                                       IsReadOnly="True"/>
                    </StackPanel>
                </Grid>
            </DataTemplate>

            <Style x:Key="ImageGridView_ItemContainerStyle"
                   TargetType="GridViewItem">
                <Setter Property="Background" 
                        Value="Gray"/>
                <Setter Property="Margin" 
                        Value="8"/>
            </Style>

            <ItemsPanelTemplate x:Key="ImageGridView_ItemsPanelTemplate">
                    <ItemsWrapGrid Orientation="Horizontal"
                                   HorizontalAlignment="Center"/>
                </ItemsPanelTemplate>
        </Grid.Resources>

        <GridView x:Name="ImageGridView"
                  ItemsSource="{x:Bind Images}"
                  ItemTemplate="{StaticResource ImageGridView_ItemTemplate}"
                  ItemContainerStyle="{StaticResource ImageGridView_ItemContainerStyle}"
                  ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"
                  ContainerContentChanging="ImageGridView_ContainerContentChanging" />
    </Grid>
</Window>

GridView, ImageRatingControl과 같은 Window 요소에 있는 컨트롤에 대한 Uno 플랫폼의 다중 플랫폼 구현을 통해 뷰 자체가 약간의 활동만으로 지원되는 모든 플랫폼에서 작동하도록 보장합니다. 이 Window의 콘텐츠를 복사하여 UnoSimplePhotos Uno 플랫폼 프로젝트에 있는 MainPage.xaml 파일의 Page 요소에 붙여넣습니다. MainPage 뷰 XAML은 다음과 같습니다.

<Page x:Class="UnoSimplePhotos.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:UnoSimplePhotos"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="ImageGridView_ItemTemplate"
                          x:DataType="local:ImageFileInfo">
                <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="{x:Bind ImageTitle}"
                                   HorizontalAlignment="Center"
                                   Style="{StaticResource SubtitleTextBlockStyle}" />
                        <StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Center">
                            <TextBlock Text="{x:Bind ImageFileType}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}" />
                            <TextBlock Text="{x:Bind ImageDimensions}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}"
                                       Margin="8,0,0,0" />
                        </StackPanel>

                        <RatingControl Value="{x:Bind ImageRating}" 
                                       IsReadOnly="True"/>
                    </StackPanel>
                </Grid>
            </DataTemplate>

            <Style x:Key="ImageGridView_ItemContainerStyle"
                   TargetType="GridViewItem">
                <Setter Property="Background" 
                        Value="Gray"/>
                <Setter Property="Margin" 
                        Value="8"/>
            </Style>

            <ItemsPanelTemplate x:Key="ImageGridView_ItemsPanelTemplate">
                <ItemsWrapGrid Orientation="Horizontal"
                               HorizontalAlignment="Center"/>
            </ItemsPanelTemplate>
        </Grid.Resources>

        <GridView x:Name="ImageGridView"
                  ItemsSource="{x:Bind Images}"
                  ItemTemplate="{StaticResource ImageGridView_ItemTemplate}"
                  ItemContainerStyle="{StaticResource ImageGridView_ItemContainerStyle}"
                  ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"
                  ContainerContentChanging="ImageGridView_ContainerContentChanging">
        </GridView>
    </Grid>
</Page>

데스크톱 솔루션에는 뷰에 해당하는 코드 숨김이 포함된 MainWindow.xaml.cs 파일도 있다는 것을 기억할 것입니다. Uno 플랫폼 프로젝트에서 복사한 MainPage 뷰의 코드 숨김은 MainPage.xaml.cs 파일에 포함되어 있습니다.

이 코드 숨김 다중 플랫폼을 가져오려면 먼저 다음을 MainPage.xaml.cs 파일로 이동해야 합니다.

  • Images 속성: 관찰 가능한 이미지 파일 컬렉션을 GridView에 제공합니다.

  • 생성자의 콘텐츠: GetItemsAsync()를 호출하여 이미지 파일을 나타내는 항목으로 Images 컬렉션을 채웁니다.

  • ImageGridView 컨트롤의 ItemsSource 속성에 대한 수동 수정을 제거합니다.

  • ImageGridView_ContainerContentChanging 메서드: 전략의 일부로 사용되어 GridView 항목이 스크롤되어 표시될 때 점진적으로 로드합니다.

  • ShowImage 메서드: 이미지 파일을 GridView에 로드합니다.

  • GetItemsAsync 메서드: Samples 폴더에서 이미지 자산 파일을 가져옵니다.

  • LoadImageInfoAsync 메서드: 만들어진 StorageFile에서 ImageFileInfo 개체를 구성합니다.

모든 항목을 이동한 후 MainPage.xaml.cs는 이제 다음과 같아야 합니다.

using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel;
using Windows.Storage;
using Windows.Storage.Search;

namespace UnoSimplePhotos;

public sealed partial class MainPage : Page
{
    public ObservableCollection<ImageFileInfo> Images { get; } 
    = new ObservableCollection<ImageFileInfo>();

    public MainPage()
    {
        this.InitializeComponent();
        GetItemsAsync();
    }

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

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

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

참고 항목

Uno 앱 프로젝트의 파일은 UnoSimplePhotos를 네임스페이스로 사용해야 합니다.

지금까지 작업 중인 기본 보기의 파일에는 데스크톱 솔루션의 모든 기능이 포함되어 있습니다. ImageFileInfo.cs 모델 파일을 복사한 후 다중 플랫폼 호환성을 위해 데스크톱 지향 코드 블록을 수정하는 방법을 알아봅니다.

데스크톱 프로젝트에서 ImageFileInfo를 복사하여 ImageFileInfo.cs 파일에 붙여넣습니다. 다음과 같이 변경합니다.

  • 네임스페이스 이름을 SimplePhotos 대신 UnoSimplePhotos로 바꿉니다.

    // Found towards the top of the file
    namespace UnoSimplePhotos;
    
  • OnPropertyChanged 메서드의 매개 변수 형식을 널 입력 가능으로 변경합니다.

    // string -> string?
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    ...
    
  • PropertyChangedEventHandler를 null이 가능하게 만듭니다.

    // PropertyChangedEventHandler -> PropertyChangedEventHandler?
    public event PropertyChangedEventHandler? PropertyChanged;
    

정리하여 보면, 파일은 다음과 같아야 합니다.

using Microsoft.UI.Xaml.Media.Imaging;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Windows.Storage;
using Windows.Storage.FileProperties;
using Windows.Storage.Streams;
using ThumbnailMode = Windows.Storage.FileProperties.ThumbnailMode;

namespace UnoSimplePhotos;

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

이 클래스는 GridView의 이미지 파일을 나타내는 모델 역할을 합니다. 이 시점에서는 기술적으로 앱을 실행할 수 있지만 이미지가 올바르게 렌더링되지 않거나 해당 속성이 표시되지 않을 수 있습니다. 다음 섹션에서는 복사된 파일을 다중 플랫폼 컨텍스트에서 호환되도록 일련의 변경 작업을 수행합니다.

전처리기 지시문 사용

이전 자습서의 데스크톱 프로젝트에서 MainPage.xaml.cs 파일에는 설치된 패키지 위치를 나타내는 StorageFolder의 항목을 열거하는 GetItemsAsync 메서드가 포함되어 있습니다. WebAssembly와 같은 특정 플랫폼에서는 해당 위치를 사용할 수 없기 때문에 모든 플랫폼과 호환되도록 이 메서드를 변경해야 합니다. 이에 따라 호환성을 보장하기 위해 ImageFileInfo 클래스를 일부 변경할 예정입니다.

먼저, GetItemsAsync 메서드에 필요한 변경 내용을 적용합니다. MainPage.xaml.cs 파일의 GetItemsAsync 메서드를 다음 코드로 바꿉니다.

private async Task GetItemsAsync()
{
#if WINDOWS
    StorageFolder appInstalledFolder = Package.Current.InstalledLocation;
    StorageFolder picturesFolder = await appInstalledFolder.GetFolderAsync("UnoSimplePhotos\\Assets\\Samples");

    var result = picturesFolder.CreateFileQueryWithOptions(new QueryOptions());

    IReadOnlyList<StorageFile> imageFiles = await result.GetFilesAsync();
#else
    var imageFileNames = Enumerable.Range(1, 20).Select(i => new Uri($"ms-appx:///UnoSimplePhotos/Assets/Samples/{i}.jpg"));
    var imageFiles = new List<StorageFile>();

    foreach (var file in imageFileNames)
    {
        imageFiles.Add(await StorageFile.GetFileFromApplicationUriAsync(file));
    }
#endif
    foreach (StorageFile file in imageFiles)
    {
        Images.Add(await LoadImageInfoAsync(file));
    }
}

이제 이 메서드는 전처리기 지시문을 사용하여 플랫폼에 따라 실행할 코드를 결정합니다. Windows에서 이 메서드는 설치된 패키지 위치를 나타내는 StorageFolder를 가져오고 여기에서 Samples 폴더를 반환합니다. 다른 플랫폼에서는 메서드가 최대 20개까지 계산되며, 이미지 파일을 나타내기 위해 Uri를 사용하여 Samples 폴더에서 이미지 파일을 가져옵니다.

다음으로, GetItemsAsync 메서드에 대한 변경 내용을 수용하도록 LoadImageInfoAsync 메서드를 조정합니다. MainPage.xaml.cs 파일의 LoadImageInfoAsync 메서드를 다음 코드로 바꿉니다.

public async static Task<ImageFileInfo> LoadImageInfoAsync(StorageFile file)
{
#if WINDOWS
    var properties = await file.Properties.GetImagePropertiesAsync();
    ImageFileInfo info = new(properties,
                                file, file.DisplayName, $"{file.FileType} file");
#else
    ImageFileInfo info = new(file, file.DisplayName, $"{file.FileType} file");
#endif
    return info;
}

GetItemsAsync 메서드와 마찬가지로 이 메서드는 이제 전처리기 지시문을 사용하여 플랫폼에 따라 실행할 코드를 결정합니다. Windows에서 이 메서드는 StorageFile에서 ImageProperties를 가져와서 이를 사용하여 ImageFileInfo 개체를 만듭니다. 다른 플랫폼에서는 이 메서드가 ImageProperties 매개 변수 없이 ImageFileInfo 개체를 생성합니다. 나중에 이 변경 내용을 수용하기 위해 ImageFileInfo 클래스가 수정됩니다.

GridView와 같은 컨트롤을 사용하면 업데이트된 항목 컨테이너 콘텐츠가 표시 영역으로 스크롤될 때 점진적 로드가 가능합니다. 이는 ContainerContentChanging 이벤트를 사용하여 수행됩니다. 이전 자습서의 데스크톱 프로젝트에서 ImageGridView_ContainerContentChanging 메서드는 이 이벤트를 사용하여 이미지 파일을 GridView에 로드합니다. 이 이벤트의 특정 측면은 모든 플랫폼에서 지원되지 않으므로 이 메서드가 호환되도록 변경해야 합니다.

컬렉션 컨트롤 뷰포트의 다이어그램.

예를 들어, ContainerContentChangingEventArgs.Phase 속성은 현재 Windows 이외의 플랫폼에서는 지원되지 않습니다. 이 변경 내용을 수용하려면 ImageGridView_ContainerContentChanging 메서드를 변경해야 합니다. MainPage.xaml.cs 파일의 ImageGridView_ContainerContentChanging 메서드를 다음 코드로 바꿉니다.

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;
        if (image is not null)
        {
            image.Source = null;
        }
    }

#if WINDOWS
        if (args.Phase == 0)
        {
            args.RegisterUpdateCallback(ShowImage);
            args.Handled = true;
        }
#else
    ShowImage(sender, args);
#endif
}

특수 콜백은 이제 플랫폼이 Windows인 경우에만 ContainerContentChangingEventArgs.RegisterUpdateCallback()을 사용하여 등록됩니다. 그렇지 않으면 ShowImage 메서드가 직접 호출됩니다. 또한 ImageGridView_ContainerContentChanging 메서드의 변경 내용과 함께 작동하려면 ShowImage 메서드도 변경해야 합니다. MainPage.xaml.cs 파일의 ShowImage 메서드를 다음 코드로 바꿉니다.

private async void ShowImage(ListViewBase sender, ContainerContentChangingEventArgs args)
{
    if (
#if WINDOWS
            args.Phase == 1
#else
        true
#endif
        )
    {

        // 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;
#if WINDOWS
        if (image is not null && item is not null)
        {
            image.Source = await item.GetImageThumbnailAsync();
        }
#else
        if (item is not null)
        {
            await item.GetImageSourceAsync();
        }
#endif
    }
}

다시 말하지만, 전처리기 지시문은 ContainerContentChangingEventArgs.Phase 속성이 지원되는 플랫폼에서만 사용되도록 보장합니다. Windows 이외의 플랫폼에서는 이전에 사용되지 않은 GetImageSourceAsync() 메서드를 사용하여 이미지 파일을 GridView에 로드합니다. 이 시점에서는 ImageFileInfo 클래스를 편집하여 위에서 수행한 변경 내용을 수용할 예정입니다.

다른 플랫폼을 위한 별도의 코드 경로 만들기

이미지 파일을 로드하는 데 사용될 ImageSource라는 새 속성을 포함하도록 ImageFileInfo.cs를 업데이트합니다.

public BitmapImage? ImageSource { get; private set; }

웹과 같은 플랫폼은 Windows에서 쉽게 사용할 수 있는 고급 이미지 파일 속성을 지원하지 않으므로 ImageProperties 형식 매개 변수가 필요하지 않은 생성자 오버로드를 추가할 예정입니다. 다음 코드를 사용하여 기존 오버로드 뒤에 새 오버로드를 추가합니다.

public ImageFileInfo(StorageFile imageFile,
    string name,
    string type)
{
    ImageName = name;
    ImageFileType = type;
    ImageFile = imageFile;
}

이 생성자 오버로드는 Windows 이외의 플랫폼에서 ImageFileInfo 개체를 생성하는 데 사용됩니다. 이렇게 하였으므로 ImageProperties 속성을 null 허용으로 만드는 것이 합리적입니다. 다음 코드를 사용하여 null을 허용하도록 ImageProperties 속성을 업데이트합니다.

public ImageProperties? ImageProperties { get; }

BitmapImage 개체만 반환하는 대신 ImageSource 속성을 사용하도록 GetImageSourceAsync 메서드를 업데이트합니다. ImageFileInfo.cs 파일의 GetImageSourceAsync 메서드를 다음 코드로 바꿉니다.

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

    ImageSource = bitmapImage;
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageSource)));

    return bitmapImage;
}

null인 경우 ImageProperties 값을 가져오는 것을 방지하려면 다음과 같이 변경합니다.

  • null 조건부 연산자를 사용하도록 ImageDimensions 속성을 수정합니다.

    public string ImageDimensions => $"{ImageProperties?.Width} x {ImageProperties?.Height}";
    
  • null 조건부 연산자를 사용하도록 ImageTitle 속성을 변경합니다.

    public string ImageTitle
    {
        get => string.IsNullOrEmpty(ImageProperties?.Title) ? ImageName : ImageProperties?.Title;
        set
        {
            if (ImageProperties is not null)
            {
                if (ImageProperties.Title != value)
                {
                    ImageProperties.Title = value;
                    _ = ImageProperties.SavePropertiesAsync();
                    OnPropertyChanged();
                }
            }
        }
    }
    
  • 데모 목적으로 임의의 별 등급을 생성하여 ImageProperties에 의존하지 않도록 ImageRating을 변경합니다.

    public int ImageRating
    {
        get => (int)((ImageProperties?.Rating == null || ImageProperties.Rating == 0) ? (uint)Random.Shared.Next(1, 5) : ImageProperties.Rating);
        set
        {
            if (ImageProperties is not null)
            {
                if (ImageProperties.Rating != value)
                {
                    ImageProperties.Rating = (uint)value;
                    _ = ImageProperties.SavePropertiesAsync();
                    OnPropertyChanged();
                }
            }
        }
    }
    
  • 더 이상 이 작업을 수행하지 않도록 임의의 정수를 생성하는 생성자를 업데이트합니다.

    public ImageFileInfo(ImageProperties properties,
        StorageFile imageFile,
        string name,
        string type)
    {
        ImageProperties = properties;
        ImageName = name;
        ImageFileType = type;
        ImageFile = imageFile;
    }
    

이러한 편집을 통해 ImageFileInfo 클래스에는 다음 코드가 포함되어야 합니다. 이제 Windows 이외의 플랫폼에 대해 새로 구분된 코드 경로가 있습니다.

using Microsoft.UI.Xaml.Media.Imaging;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Windows.Storage;
using Windows.Storage.FileProperties;
using Windows.Storage.Streams;
using ThumbnailMode = Windows.Storage.FileProperties.ThumbnailMode;

namespace UnoSimplePhotos;

public class ImageFileInfo : INotifyPropertyChanged
{
    public BitmapImage? ImageSource { get; private set; }

    public ImageFileInfo(ImageProperties properties,
        StorageFile imageFile,
        string name,
        string type)
    {
        ImageProperties = properties;
        ImageName = name;
        ImageFileType = type;
        ImageFile = imageFile;
    }

    public ImageFileInfo(StorageFile imageFile,
        string name,
        string type)
    {
        ImageName = name;
        ImageFileType = type;
        ImageFile = imageFile;
    }

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

        ImageSource = bitmapImage;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageSource)));

        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 is not null)
            {
                if (ImageProperties.Title != value)
                {
                    ImageProperties.Title = value;
                    _ = ImageProperties.SavePropertiesAsync();
                    OnPropertyChanged();
                }
            }
        }
    }

    public int ImageRating
    {
        get => (int)((ImageProperties?.Rating == null || ImageProperties.Rating == 0) ? (uint)Random.Shared.Next(1, 5) : ImageProperties.Rating);
        set
        {
            if (ImageProperties is not null)
            {
                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));
}

ImageFileInfo 클래스는 GridView의 이미지 파일을 나타내는 데 사용됩니다. 마지막으로 모델 변경 내용을 수용하기 위해 MainPage.xaml 파일을 변경할 예정입니다.

플랫폼별 XAML 태그 사용

뷰 태그에는 Windows에서만 평가해야 하는 몇 가지 항목이 있습니다. 다음과 같이 MainPage.xaml 파일의 Page 요소에 새 네임스페이스를 추가합니다.

...
xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

이제 MainPage.xaml에서 GridView 요소의 ItemsPanel 속성 집합 설정자를 다음 코드로 바꿉니다.

win:ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"

속성 이름 앞에 win:을 추가하면 해당 속성이 Windows에서만 설정됩니다. ImageGridView_ItemTemplate 리소스 내에서 이 작업을 다시 수행합니다. Windows에서 ImageDimensions 속성을 사용하는 요소만 로드하려고 합니다. ImageDimensions 속성을 사용하는 TextBlock 요소를 다음 코드로 바꿉니다.

<win:TextBlock Text="{x:Bind ImageDimensions}"
               HorizontalAlignment="Center"
               Style="{StaticResource CaptionTextBlockStyle}"
               Margin="8,0,0,0" />

이제 MainPage.xaml 파일은 다음과 같아야 합니다.

<Page x:Class="UnoSimplePhotos.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:UnoSimplePhotos"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      mc:Ignorable="d"
      Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

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

                    <Image x:Name="ItemImage"
                           Source="{x:Bind ImageSource}"
                           Stretch="Uniform" />

                    <StackPanel Orientation="Vertical"
                                Grid.Row="1">
                        <TextBlock Text="{x:Bind ImageTitle}"
                                   HorizontalAlignment="Center"
                                   Style="{StaticResource SubtitleTextBlockStyle}" />
                        <StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Center">
                            <TextBlock Text="{x:Bind ImageFileType}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}" />
                            <win:TextBlock Text="{x:Bind ImageDimensions}"
                                           HorizontalAlignment="Center"
                                           Style="{StaticResource CaptionTextBlockStyle}"
                                           Margin="8,0,0,0" />
                        </StackPanel>

                        <RatingControl Value="{x:Bind ImageRating}"
                                       IsReadOnly="True" />
                    </StackPanel>
                </Grid>
            </DataTemplate>
            
            <Style x:Key="ImageGridView_ItemContainerStyle"
                   TargetType="GridViewItem">
                <Setter Property="Background"
                        Value="Gray" />
                <Setter Property="Margin" 
                        Value="8"/>
            </Style>

            <ItemsPanelTemplate x:Key="ImageGridView_ItemsPanelTemplate">
                <ItemsWrapGrid Orientation="Horizontal"
                               HorizontalAlignment="Center"/>
            </ItemsPanelTemplate>
        </Grid.Resources>

        <GridView x:Name="ImageGridView"
                  ItemsSource="{x:Bind Images, Mode=OneWay}"
                  win:ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"
                  ContainerContentChanging="ImageGridView_ContainerContentChanging"
                  ItemContainerStyle="{StaticResource ImageGridView_ItemContainerStyle}"
                  ItemTemplate="{StaticResource ImageGridView_ItemTemplate}" />
    </Grid>
</Page>

앱 실행하기

UnoSimplePhotos.Windows 대상을 실행합니다. 이 WinUI 앱은 이전 자습서와 매우 유사하게 보입니다.

이제 지원되는 모든 플랫폼에서 앱을 빌드하고 실행할 수 있습니다. 이렇게 하려면 디버그 도구 모음 드롭다운을 사용하여 배포할 대상 플랫폼을 선택하면 됩니다.

  • WebAssembly(Wasm) 헤드를 실행하려면 다음을 수행합니다.

    • UnoSimplePhotos.Wasm 프로젝트를 마우스 오른쪽 단추로 클릭하고 시작 프로젝트로 설정을 선택합니다.
    • UnoSimplePhotos.Wasm 단추를 눌러 앱을 배포합니다.
    • 원하는 경우 대안으로 UnoSimplePhotos.Server 프로젝트를 추가하고 사용할 수 있습니다.
  • iOS를 디버깅하려면 다음을 수행합니다.

    • UnoSimplePhotos.Mobile 프로젝트를 마우스 오른쪽 단추로 클릭하고 시작 프로젝트로 설정을 선택합니다.

    • 디버그 도구 모음 드롭다운에서 활성 iOS 디바이스 또는 시뮬레이터를 선택합니다. 이 기능이 작동하려면 Mac과 페어링되어 있어야 합니다.

      배포할 대상 프레임워크를 선택하는 Visual Studio 드롭다운의 스크린샷.

  • Mac Catalyst를 디버깅하려면 다음을 수행합니다.

    • UnoSimplePhotos.Mobile 프로젝트를 마우스 오른쪽 단추로 클릭하고 시작 프로젝트로 설정을 선택합니다.
    • 디버그 도구 모음 드롭다운에서 원격 macOS 디바이스를 선택합니다. 이 기능이 작동하려면 하나와 페어링되어야 합니다.
  • Android 플랫폼을 디버깅하려면 다음을 수행합니다.

    • UnoSimplePhotos.Mobile 프로젝트를 마우스 오른쪽 단추로 클릭하고 시작 프로젝트로 설정을 선택합니다.
    • 디버그 도구 모음 드롭다운에서 활성 Android 디바이스 또는 에뮬레이터를 선택합니다.
      • "디바이스" 하위 메뉴에서 활성 디바이스를 선택합니다.
  • Skia GTK를 사용하여 Linux에서 디버깅하려면 다음을 수행합니다.

    • UnoSimplePhotos.Skia.Gtk 프로젝트를 마우스 오른쪽 단추로 클릭하고 시작 프로젝트로 설정을 선택합니다.
    • UnoSimplePhotos.Skia.Gtk 단추를 눌러 앱을 배포합니다.

참고 항목