前回の記事で WinUI3
を使った画像切り抜きサンプルコードを紹介した。どうせなら .NET MAUI
バージョンも作って公開しようと思い作り始めたのだが、かなり手こずった😭
追記:Blazor版も作った。
画像の上に線を描く
機能を満たすために、画像の上に切り抜き用の矩形を描画する必要がある。これまでのSystem.Drawing
にあるBitmap
やGraphics
を使うやり方を踏襲して、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
による描画が実行されないからだ。当初はこれに気がつけずに散々試行錯誤していた。
さらに付け加えると、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
を設定してあげてデータを受け取れるようにする。
一見これで問題なさそうに思えた。しかし遷移先オブジェクトは一度しか作られないことから、コンストラクタでいろいろ初期設定をしてしまうと画面間を行き来した際に、渡したデータで更新されない事態が発生する。
代わりに使ったのが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
の公式ドキュメントを読んでいたら、ちゃんとデータの渡し方について書いてあるではないか・・・。
ドキュメントを引用するが
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.Graphics
がSystem.Drawing
の後継にあたるらしいのでSystem.Drawing
系メソッドを使うことがためらわれた。
ちなみにここも散々試行錯誤したのだが、わかってみるとMaui.Graphics
で切り抜きは単純だった。実現するには以下のメソッドを使う。
ICanvas.ClipRectangle(RectF rect)
ICanvas.Rotate(float degrees, float x, float y)
ICanvas.DrawImage(IImage image, float x, float y, float width, float height)
ICanvas.ResetState()
なおメソッド実行順序は上の並び順でないとサンプルコードのような動きにならないので注意。切り抜きエリアを決め、描画エリアを逆回転し、画像を描画する。
さらに詳しく知りたい場合はサンプルコードを確認してほしい。
MAUI疲れる
調子に乗って作り始めてみたものの、WPFアプリと勝手が違って四苦八苦したMAUIアプリ作り。macOS対応アプリが作れる点がいいなと思いつつも、Windowsアプリだけ作りたいならWPFで十分だと思う。
おそらく今回の要件にMAUIは一致していなかったのかもしれない。しかし知見はいろいろ得られたので、苦労した甲斐はあったと思う。