.NET MAUI 画像切り抜きサンプル。

jupitrisonlabs.hatenadiary.jp

前回の記事で WinUI3 を使った画像切り抜きサンプルコードを紹介した。どうせなら .NET MAUI バージョンも作って公開しようと思い作り始めたのだが、かなり手こずった😭

github.com

追記:Blazor版も作った。

github.com

画像の上に線を描く

機能を満たすために、画像の上に切り抜き用の矩形を描画する必要がある。これまでのSystem.DrawingにあるBitmapGraphicsを使うやり方を踏襲して、BitmapやGraphicsで作った画像データをMicrosoft.Maui.Controls.Image型に変換して画面に表示してあげればいいのかと思ったらそうではなかった。

MAUIではIDrawableを継承したクラスを作りpublic void Draw(ICanvas canvas, RectF dirtyRect) {...}メソッドを実装、canvasに対して画像描画やシェイプ描画をする必要がある。

単純に画像や線を描くだけならまだ簡単なのだが、MVVMを利用して角度や矩形パスをIDrawableを継承したクラスへ渡すためには、さらにGraphicsViewを継承しBindablePropertyを定義してXAML側からBindingを使ったデータバインドに対応させなければいけなかった。

public class GraphicsDrawable : GraphicsView, IDrawable
{
    public double Angle
    {
        get => (double)GetValue(AngleProperty);
        set => SetValue(AngleProperty, value);
    }

    public static readonly BindableProperty AngleProperty = BindableProperty.Create(nameof(Angle), typeof(double), typeof(GraphicsDrawable), propertyChanged: AngleChanged);

    private static void AngleChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is not GraphicsDrawable { Drawable: GraphicsDrawable drawable } view)
        {
            return;
        }

        drawable.Angle = (double)newValue;
        view.Invalidate();  // 画面更新
    }

}

ちなみにInvalidateはどちゃくそ重要だ。これを実行しないと受け取った値を使ったDrawによる描画が実行されないからだ。当初はこれに気がつけずに散々試行錯誤していた。

github.com

さらに付け加えると、GraphicsViewを継承した場合はXAMLで以下のようにコントロールを定義する必要がある。

... 略 ...
<ContentPage.Resources>
    <drawable:GraphicsDrawable x:Key="drawable" />
</ContentPage.Resources>
... 略 ...
<VerticalStackLayout>
    <drawable:GraphicsDrawable x:Name="MainImage"
                               Angle="{Binding Angle}"
                               WidthRequest="800"
                               HeightRequest="550">
        <GraphicsView.Drawable>
            <drawable:GraphicsDrawable />
        </GraphicsView.Drawable>
    </drawable:GraphicsDrawable>
</VerticalStackLayout>
... 略 ...

悩んだのはこんなものではなかったが、それらをすべて挙げて解決方法を書く気力がないので、なにかMAUIでお困り事があってこの記事へ辿り着いた方、あとはサンプルコードを見てほしい。

画面遷移

画像の上に矩形を描画できるようになったので、次の画面で切り抜き後の画像を表示するために画面遷移の実装に取り掛かった。

まずはawait Shell.Current.GoToAsync()で画面遷移できることを確認。続いて角度やパスのデータを渡すために、遷移先のContentPageを継承したクラスにQueryPropertyAttributeを設定してあげてデータを受け取れるようにする。

learn.microsoft.com

一見これで問題なさそうに思えた。しかし遷移先オブジェクトは一度しか作られないことから、コンストラクタでいろいろ初期設定をしてしまうと画面間を行き来した際に、渡したデータで更新されない事態が発生する。

代わりに使ったのがawait Navigation.PushAsync()。遷移するときはスタックに遷移先をプッシュ、戻るときはスタックから遷移元をポップ。 これを利用したことで、スタックにプッシュするたびに新しくオブジェクトが作られるので新しいデータで更新が可能になり、ポップして戻っても遷移前の状態を保つことができた。

ここでまた問題。ViewModelに設定したデータをどうやって遷移先に渡そうか。QueryPropertyAttribute的なものはなさそう。ここでも散々試行錯誤した挙げ句、遷移先のコンストラクタにViewModelの引数を付けて渡すことで解決した。

public CroppedImagePage(SecondPageViewModel viewModel)
{
    InitializeComponent();
    BindingContext = viewModel;
}

呼び出し方はこんな感じ。

SecondPageViewModel viewModel = new()
{
    Angle = FirstPageViewModel.Angle
}
Navigation.PushAsync(new SecondPage(viewModel));

なぜこんな単純なことに早く気が付かなかったのだろう😭
しかしさらにショックなことが・・・。

この項目を書いているときにNavigationPageの公式ドキュメントを読んでいたら、ちゃんとデータの渡し方について書いてあるではないか・・・。

learn.microsoft.com

ドキュメントを引用するが

Contact contact = new Contact
{
    Name = "Jane Doe",
    Age = 30,
    Occupation = "Developer",
    Country = "USA"
};

await Navigation.PushAsync(new DetailsPage
{
    BindingContext = contact  
});

BindingContextに直接データを渡せるらしい😭
やってみたらできた😭

切り抜き

もともとWinUI3版アプリを作ったときもそうだったのだが、Matrixを使った座標変換による切り抜きのサンプルを作ることが目的だった。

しかしMAUIではクロスプラットフォームに対応するため、単純にBitmapやGraphicsでどうこうできそうにもないし、Maui.GraphicsSystem.Drawingの後継にあたるらしいのでSystem.Drawing系メソッドを使うことがためらわれた。

ちなみにここも散々試行錯誤したのだが、わかってみるとMaui.Graphicsで切り抜きは単純だった。実現するには以下のメソッドを使う。

  1. ICanvas.ClipRectangle(RectF rect)
  2. ICanvas.Rotate(float degrees, float x, float y)
  3. ICanvas.DrawImage(IImage image, float x, float y, float width, float height)
  4. ICanvas.ResetState()

なおメソッド実行順序は上の並び順でないとサンプルコードのような動きにならないので注意。切り抜きエリアを決め、描画エリアを逆回転し、画像を描画する。

さらに詳しく知りたい場合はサンプルコードを確認してほしい。

MAUI疲れる

調子に乗って作り始めてみたものの、WPFアプリと勝手が違って四苦八苦したMAUIアプリ作り。macOS対応アプリが作れる点がいいなと思いつつも、Windowsアプリだけ作りたいならWPFで十分だと思う。

おそらく今回の要件にMAUIは一致していなかったのかもしれない。しかし知見はいろいろ得られたので、苦労した甲斐はあったと思う。