次の方法で共有


チュートリアル: 複数のプラットフォームを対象とするシンプルなフォト ビューアーを構築する

スターターのシンプルなフォト ビューアー WinUI 3 アプリを作成したら、アプリを書き換えることなく、より多くのユーザーにリーチするにはどうすればよいか、思いを巡らせるかもしれません。 このチュートリアルでは、Uno Platform を使用して、既存の C# WinUI 3 アプリケーションのリーチ範囲を拡大し、ネイティブ モバイル、Web、デスクトップ全体にわたってビジネス ロジックと UI レイヤーを再利用できるようにします。 シンプルなフォト ビューアー アプリに最小限の変更を加えるだけで、これらのプラットフォームに移植されたアプリのピクセル パーフェクトなコピーを実行できます。

Web と WinUI デスクトップを対象にした UnoSimplePhoto アプリのスクリーンショット。

前提条件

  • Visual Studio 2022 17.4 以降

  • 開発用コンピューターをセットアップする (「 WinUI の概要」を参照してください)

  • ASP.NET と Web 開発ワークロード (WebAssembly 開発用)

    Visual Studio での Web 開発ワークロードのスクリーンショット。

  • .NET マルチプラットフォームアプリ UI 開発インストール済み (iOS、Android、Mac Catalyst 開発用)

    Visual Studio での .NET モバイル ワークロードのスクリーンショット。

  • .NET デスクトップ開発がインストールされている (Gtk、Wpf、および Linux フレームバッファ開発用)

    Visual Studio での .NET デスクトップ ワークロードのスクリーンショット。

環境の最終処理

  1. コマンド ライン プロンプト、インストールされている場合は Windows ターミナル、それ以外の場合は [スタート] メニューから [コマンド プロンプト] または [Windows Powershell] を開きます。

  2. uno-check ツールを次のようにインストールまたは更新します。

    • 次のコマンドを使用します:

      dotnet tool install -g uno.check
      
    • 前のバージョンが既にインストールされている場合にツールを更新するには次のようにします。

      dotnet tool update -g uno.check
      
  3. 次のコマンドでツールを実行します。

    uno-check
    
  4. ツールから示されている指示に従います。 システムを変更する必要があるため、管理者特権のアクセス許可を求められる場合があります。

Uno Platform ソリューション テンプレートをインストールする

Visual Studio を起動し、Continue without code をクリックします。 メニュー バーの Extensions ->Manage Extensions をクリックします。

拡張機能の管理といる名前の Visual Studio メニュー バー項目のスクリーンショット。

拡張機能マネージャーで [オンライン] ノードを展開し、Uno を検索して Uno Platform 拡張機能をインストールするか、Visual Studio Marketplace からダウンロードしてインストールしてから、Visual Studio を再起動します。

検索結果として Uno Platform 拡張機能が表示されている Visual Studio の [拡張機能の管理] ウィンドウのスクリーンショット。

アプリケーションの作成

マルチプラットフォーム アプリケーションを作成する準備ができたので、次に実行する手順は、新しい Uno Platform アプリケーションを作成することです。 前のチュートリアルの SimplePhotos WinUI 3 プロジェクトのコードをマルチプラットフォーム プロジェクトにコピーします。 これが可能なのは、Uno Platform で既存のコードベースを再利用できるからです。 各プラットフォームによって提供される OS API に依存する機能については、簡単に時間をかけて機能するようにできます。 この方法は、他のプラットフォームに移植する既存のアプリケーションがある場合に特に便利です。

使い慣れた XAML フレーバーと既にあるコードベースを使用して、より多くのプラットフォームをターゲットにできるため、十分短期間で、このアプローチの利点を得ることができます。

Visual Studio を開き、File>New>Project で新しいプロジェクトを作成します。

[新しいプロジェクト] ダイアログのスクリーン ショット。

Uno を検索し、Uno Platform App プロジェクト テンプレートを選択します。

Uno Platform アプリがプロジェクトの種類として選択されている新規プロジェクト作成ダイアログのスクリーンショット。

Visual Studio の開始ページから Uno Platform App の種類を使用して、新しい C# ソリューションを作成します。 前のチュートリアルのコードとの競合を回避するために、このソリューションに異なる名前 "UnoSimplePhotos" を付けます。 プロジェクト名、ソリューション名、ディレクトリを指定します。 この例では、UnoSimplePhotos マルチプラットフォーム プロジェクトは UnoSimplePhotos ソリューションに属していて、C:\Projects に配置されます。

新しい Uno Platform プロジェクトの詳細を指定するスクリーンショット。

次に、基本テンプレートを選択して、シンプルなフォト ギャラリー アプリケーションをマルチプラットフォームに移植します。

Uno Platform App テンプレートには、すぐに作業を開始できる 2 つのプリセット オプション (空白りソリューション、または Uno.Material および Uno.Toolkit ライブラリへの参照を含む既定の構成) が用意されています。 既定の構成には、依存関係の挿入、構成、ナビゲーション、ログ記録に使用される Uno.Extensions も含まれています。 さらに、MVVM の代わりに MVUX を使用するため、現実世界のアプリケーションを迅速に構築するための優れた出発点になります。

プロジェクトのスタートアップの種類用の Uno ソリューション テンプレートのスクリーンショット。

簡略化するために、[空白] プリセットを選択します。 次に [作成] ボタンをクリックします。 プロジェクトが作成され、それらの依存関係が復元されるまで待ちます。

次のように、プロジェクトの再読み込みを求めるバナーがエディターの上部に表示されることがあります。その場合は、[Reload projects](プロジェクトの再読み込み) をクリックしてください。

変更を完了するためにプロジェクトの再読み込みを求める Visual Studio バナーのスクリーンショット。

次の既定のファイル構造がソリューション エクスプローラーに表示されます。

ソリューション エクスプローラーの既定のファイル構造のスクリーンショット。

プロジェクトにイメージ アセットを追加する

アプリには、表示するイメージがいくつか必要です。 前のチュートリアルと同じイメージを使用できます。

UnoSimplePhotos プロジェクトで、Assets という名前の新しいフォルダーを作成し、JPG イメージ ファイルを Samples サブフォルダーにコピーします。 これで、Assets フォルダーの構造が次のようになります。

新しいファイルとフォルダーが追加されている、Visual Studio のソリューション エクスプローラー ウィンドウのスクリーンショット。

Assets フォルダーを作成し、それに画像を追加する方法の詳細については、アセットとイメージの表示に関する Uno Platform のドキュメントを参照してください。

アプリを準備する

マルチプラットフォーム WinUI アプリケーションの機能的な開始点を生成したので、そこにデスクトップ プロジェクトからコードをコピーできます。

ビューをコピーする

Uno Platform では、既に使い慣れた 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>

Uno Platform の Window 要素で見つかったコントロール (GridViewImageRatingControl など) のマルチプラットフォーム実装により、わずかな労力で、サポートされているすべてのプラットフォームでビュー自体が動作するようになります。 この Window の内容をコピーし、UnoSimplePhotos Uno Platform プロジェクトの 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 Platform プロジェクトでは、コピー先の 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;
    }
}

Note

Uno アプリ プロジェクト内のファイルは、名前空間として UnoSimplePhotos を使用する必要があります。

これまで、作業しているメイン ビューのファイルには、デスクトップ ソリューションのすべての機能が含まれています。 ImageFileInfo.cs モデル ファイルをコピーしたら、マルチプラットフォームの互換性のためにデスクトップ指向のコード ブロックを変更する方法について説明します。

デスクトップ プロジェクトから ImageFileInfo をコピーし、ImageFileInfo.cs ファイルに貼り付けます。 次の変更を行います。

  • 名前空間の名前を SimplePhotos の代わりに UnoSimplePhotos に変更します。

    // Found towards the top of the file
    namespace UnoSimplePhotos;
    
  • OnPropertyChanged メソッドのパラメーターの種類を null 許容に変更します。

    // 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 フォルダーを返します。 他のプラットフォームでは、このメソッドは、イメージ ファイルを表す Uri を使用して Samples フォルダーから、最大 20 個を数えるイメージ ファイルを取得します。

次に、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 プロパティは、サポートされているプラットフォームでのみ使用されるようになります。 前は使用しなかった GetImageSourceAsync() メソッドを利用して、Windows 以外のプラットフォーム上の GridView にイメージ ファイルを読み込みます。 この時点で、ImageFileInfo クラスを編集して、上記で加えた変更に対応します。

他のプラットフォーム用に別個のコード パスを作成する

イメージ ファイルの読み込みに使用される ImageSource という新しいプロパティを含むように ImageFileInfo.cs を更新します。

public BitmapImage? ImageSource { get; private set; }

Web などのプラットフォームでは、Windows ですぐに使用できる高度なイメージ ファイル プロパティはサポートされていないため、ImageProperties 型指定のパラメーターを必要としないコンストラクター オーバーロードを追加します。 次のコードを使用して、既存のオーバーロードの後に新しいものを追加します。

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

このコンストラクター オーバーロードは、Windows 以外のプラットフォームで ImageFileInfo オブジェクトを構築するために使用されます。 この操作を行ったので、ImageProperties プロパティを null 許容にするのは合理的です。 次のコードを使用して、ImageProperties プロパティを null 許容に更新します。

public ImageProperties? ImageProperties { get; }

BitmapImage オブジェクトを返すのみではなく、GetImageSourceAsync プロパティを使用するように ImageSource メソッドを更新します。 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 ボタンを押してアプリをデプロイします

関連項目