UE4 MarketplaceにJsonパーサープラグインをリリースしました
EasyJsonParser
自作のUE4プラグイン3作目としてEasyJsonParserをリリースしました。
これは2作目のEasyXMLParserとコンセプトは同じで、ブループリント上から簡単なアクセス方法でJson内の値を取得できます。
こちらもGitHubのほうにサンプルプロジェクトをアップロードしてますので、ご興味ある方は参考にしてください。
使い方
Json文字列 or Jsonファイルロードした後、アクセス文字列を指定して値の取得を行います。
アクセス文字列の指定
基本的には取得したい値までのパスをドットでつなげて指定します。
シンプルなケース
以下のシンプルなJsonから"prop"の値をとる場合のアクセス文字列はprop
になります。
{ "prop":"abc" }
オブジェクトの階層になっている場合
以下のように階層になっている場合は、ドットでつなげてアクセス文字列を作ります。
以下のケースではobj
というオブジェクトの中のprop
プロパティを取りたいため、アクセス文字列はobj.prop
になります。
{ "obj": { "prop":"abc" } }
配列が含まれる場合
以下のように配列になっている場合は、何番目のものを取りたいかを指定します。
例えば2個目のprop
を取りたい場合は、obj[1].prop
となります。
1個目のprop
を取りたい場合は、obj[0].prop
となります。
{ "obj":[ { "prop":"abc" }, { "prop":"def" } ] }
型を指定しての値の取得
Jsonから値を取得するために、次の4つの関数が用意されます。
- ReadInt(int)
- ReadFloat(float)
- ReadString(string)
- ReadBool(bool)
「AccessString」にはアクセス文字列を入力します。
「DefaultValue」にはデフォルト値を入力します。指定された値がJsonに存在しない場合は、デフォルト値が返されます。
オブジェクトの取得
値ではなくオブジェクトとして取得する "ReadObject"および "ReadObjects"メソッドもあります。
このメソッドで取得できるのはオブジェクトプロパティのみです。
ReadObjectは1つのノードオブジェクトを取得します。
ReadObjectsは複数のオブジェクトの配列を取得します。
以下のように、いったん階層途中のオブジェクトを取得してから、そのオブジェクトのプロパティを取得する使い方もできます。
今できることと今後やりたいこと
今回はJsonから値を取得する
という機能のみを、いかにシンプルに実装するかを考えて作成しました。
今はJsonはConfig値を設定ファイルに保存して利用したり、Web APIのデータのやりとりなど様々なところで使われていると思います。
このプラグインを使用すると、そのようなJsonからの値の取り出しをブループリントから楽にできるようになります。
今後の機能UPとしては以下のようなものを候補に考えています。 特に今は値の取得しかできないので、値の設定側もできるようになると幅が広がるのかなあと思っています。
- マルチプラットフォーム対応
- 値の設定
ObjectDeliverer v1.2.0リリース
ObjectDelivererのV1.2.0をリリースしました。
変更点は以下の2つです。
PacketRuleNodivisionを使用してUDPの受信を行うと無限ループしてしまう不具合修正
TCP/IPクライアントでサーバーが切断されると自動的に接続を試みるオプション追加
PacketRuleNodivisionを使用してUDPの受信を行うと無限ループしてしまう不具合修正
上記のようにPacketRuleNodivisionを使用してUDPの受信機能を使うと、一度受信したタイミングで無限ループに入ってしまう不具合が起きていましたので修正しました。
UDPの受信はPacketRuleNodivisionを使うことが一番多いと思うので、ホント致命的な不具合で申し訳ないです。
TCP/IPクライアントでサーバーが切断されると自動的に接続を試みるオプション追加
以前からTCP/IPクライアントを利用時にRetryフラグをONにすると、自動でサーバーが起動するまでConnectを繰り返す機能が実装されていました。
ただし、この機能はいったんサーバーと接続確立後にサーバー終了した場合は働かない仕様でした。
そこで今回新たにAutoConnectAfterDisconnectフラグを作成しました。
このフラグをONにすると、接続済みのサーバーとの接続がなくなった場合に自動でConnectを繰り返すようになります。
そのため、従来のRetryフラグを合わせて使うことで、サーバーとつながっていない時はいつでもConnectを繰り返すようになります。
常に接続を確立しておきたいケースに便利です。
UE4 MarketplaceにXMLパーサープラグインを出品しました
EasyXMLParser
本日Unreal EngineのMarketplaceにEasyXMLParserという名前でプラグインを出品しました。
前回リリースしたObjectDelivererに続いて2作品目となります。
前回は無料で出品したのですが、今回はお値段をつけさせていただきました。 使いやすさを重視して作りましたので、よろしければ是非ご検討ください。
このプラグインを使ったサンプルはGitHubに公開しています。
機能紹介
EasyXMLParserの機能を紹介します。
テキストの取得
テキストノードを取得するには、取得したい値のアクセス文字列を作成します。
アクセス文字列はタグ名をドットで結ぶことによって作成されます。
同じタグ名を持つノードが複数ある場合は、0から始まるインデックス番号を指定できます(例:[2] - > 3番目)。
インデックスを指定しないと、最初の要素が取得されます。
属性を取得するためのアクセス文字列は(アットマーク+属性名)で指定します。
型を指定しての値の取得
XMLから値を取得するために、次の4つの機能が用意されます。
- ReadInt(int)
- ReadFloat(float)
- ReadString(string)
- ReadBool(bool)
「AccessString」にはアクセス文字列を入力します。
「DefaultValue」にはデフォルト値を入力します。指定された値がXMLに存在しない場合は、デフォルト値が返されます。
オブジェクトの取得
ノードを値としてではなくオブジェクトとして取得する "ReadElement"および "ReadElements"メソッドもあります。
ReadElementは1つのノードを取得します。
ReadElementsは複数のノードの配列を取得します。
XMLパース手法
XMLをロードする方法は2つあります。XMLファイルをロードするための "LoadFromFile"と、XML文字列をロードするための "LoadFromString"です。
バックグラウンドで解析を実行する非同期バージョンもあります。
XMLの次は
個人的にはたまにXMLをパースする機会があるので、今回プラグイン化してみました。
ただ現在はXMLよりもJsonの方が使われているような気もしています。
ということで、次のプラグインとしてJsonパーサーを出すつもりです。
このEasyXMLParserと同様の使い勝手で設計しておりMarketPlaceに現在審査してもらっています。
ObjectDelivererの共有メモリサンプル
共有メモリのサンプル
以前からGitHubにObjectDelivererのサンプルを用意していたのですが、共有メモリについては読み込み側しか実装しておらず処理が完結していなかったので新規で書き込み側も実装しました。
ここではサンプルの内容を簡単に紹介します。
概要
このサンプルではSceneCapture2Dで撮った画像をTexurueRenderTargetから取り出し、そこから共有メモリを経由して別のマテリアルに表示します。
まずはSharedMemoryレベルを開いてください。
全実装はBox Actorブループリント内にあります。
読み込み側の実装
共有メモリから読み込んだピクセルバッファを入れるテクスチャとマテリアルを用意します。
共有メモリの読み込み用ObjectDelivererのインスタンスを生成し処理を開始します。
共有メモリのバッファサイズは画像バッファサイズ(800x450x4 byte)を設定しています。
- ReceiveDataイベント(共有メモリの中身に変化があった時におこるイベント)でテクスチャの中身を更新します。
書き込み側の実装
- TextureRenderTargetから取得したピクセルバッファを入れておくバッファを用意します。
- 共有メモリへの書き込み用ObjectDelivererのインスタンスを生成し処理を開始します。
共有メモリのバッファサイズは画像バッファサイズ(800x450x4 byte)を設定しています。
- Tickイベント時にTextureRenderTargetからピクセルバッファを取り出し共有メモリに書き込みます。
使い道
この例では1つのプロセス内で完結しているためあまり意味はないですが、 例えばUE4のプロジェクトを2つ用意して、片方は共有メモリへの書き込みを担当し、もう片方は共有メモリからの読み込みを担当することで映像の受け渡しができます。
共有メモリはTCP/IPやUDPと違ってネットワーク経由でのデータの受け渡しはできませんが、その分大容量のデータを高速に渡せるため映像などの重いデータのプロセス間通信に適していると思います。
ご興味ある方はぜひ試してみてください。
Editor Utility Widgetを使ってお絵かきして動的メッシュ生成機能を作った
Editor Utility Widget
今回UE4.22から利用できるようになった、Editor Utility Widgetという機能を試してみました。 Editor Utility Widgetを利用するとUMGとブループリントでエディタ拡張が簡単に作れます。
Editor Utility Widgetの基本的な説明はおかずさんの記事を参考にしました。
つくったもの
Editor Utility Widget上をマウスドラッグすることで線を一筆書きで書き、 書き終わった線に沿ってスプラインメッシュを生成する機能を作りました。
できたスプラインメッシュは、ボタンを押すとスタティックメッシュにできます。
Editor Scriptingでお絵かきして作ったSplineMeshComponentをそのままスクリプトでStaticMesh化するのに成功した。#UE4 #ue4study pic.twitter.com/sCfMMPdC1J
— ayuma (@ayuma_x) 2019年5月30日
Editor Scripting Utilities Pluginの有効化
このプラグインを有効にすると、Editor Utility Widgetからアセットの操作などができるようになります。
Widget上で線を引く
線を引く部分はUserWidgetを1つ作ってその中で実装しました。
まず、変数に線を引くための座標(Vector2D)の配列を用意します。
次にMouseDownで毎回座標配列を空っぽに。
MouseMoveでは座標の配列にマウスの座標をいれます。
ただし、すべてのMouseMoveイベントで処理すると点の数が多すぎるので前回格納した点から5pixel以上離れた場合のみ点を格納しています。
あとはOnPaintでDrawLinesを呼べば線が引かれます。
MouseUpでは線が引かれ終わったことを示すイベントを発火しています。
このイベントをEditor Utility Widgetが監視してメッシュ生成をこの後していきます。
Editor Utility Widgetの実装
次にEditor Utility Widgetを作成します。
先ほど作ったUserWidgetも配置します。
ComboBoxにメッシュ生成機能をもったActorの一覧を出したいので、 ComboBoxが開いたタイミングでActorの一覧を取得し追加しています。
またComboBoxの選択が変わった時にカレントのActorを設定しています。
先ほど作ったUserWidgetの線の引き終わりイベントを監視して、カレントのActorに線に沿った座標の配列を渡します。
スプラインメッシュ生成
スプラインメッシュを生成するActorを作っていきます。
SplineComponentを2つと、SplineMeshComponentの親になるSceneComponentを1つ追加しておきます。 SplineComponentは点の補正をするために贅沢に2つ使ってます。
UserWidgetの線が引き終わったタイミングで呼ばれる関数を作成していきます。
まずは、直前に生成されていたSplineMeshComponentを全て破棄し、SplineComponentの点も空にします。
次に渡されて点の配列をそのままSplineComponentの点に追加していきます。 1pixelを10cmとして設定しています。
次に1つ目のSplineComponentのラインに沿って等間隔の点を取り出し2つ目のSplineComponentへ点を追加していきます。
ここで2つ目のSplineComponentを作っているのは、等間隔に点を作ったほうがこの後作成するSplineMeshがきれいになるからです。
最後に2つ目のSplineComponentの点の位置とTangentを使ってSplineMeshComponentを作っていきます。
これでUserWidget上に描いた線に沿ったメッシュが出来上がります。
スプラインメッシュをスタティックメッシュに変換
SplineMeshComponentを含むActorをStaticMeshにするには、いつもMergeActorの機能を使っているのでこれをEditor Utility Widgetから呼べないか探してみました。
すると以下のノードが見つかりましたが、これはインプットがStaticMeshActorになっているので型が合いません。
なので何とかならないか、このノードのC++実装をのぞいてみました。
bool UEditorLevelLibrary::MergeStaticMeshActors(const TArray<AStaticMeshActor*>& ActorsToMerge, const FEditorScriptingMergeStaticMeshActorsOptions& MergeOptions, AStaticMeshActor*& OutMergedActor) { TGuardValue<bool> UnattendedScriptGuard(GIsRunningUnattendedScript, true); OutMergedActor = nullptr; if (!EditorScriptingUtils::CheckIfInEditorAndPIE()) { return false; } FString FailureReason; FString PackageName = EditorScriptingUtils::ConvertAnyPathToLongPackagePath(MergeOptions.BasePackageName, FailureReason); if (PackageName.IsEmpty()) { UE_LOG(LogEditorScripting, Error, TEXT("MergeStaticMeshActors. Failed to convert the BasePackageName. %s"), *FailureReason); return false; } TArray<AStaticMeshActor*> AllActors; TArray<UPrimitiveComponent*> AllComponents; FVector PivotLocation; if (!InternalEditorLevelLibrary::FindValidActorAndComponents(ActorsToMerge, AllActors, AllComponents, PivotLocation, FailureReason)) { UE_LOG(LogEditorScripting, Error, TEXT("MergeStaticMeshActors failed. %s"), *FailureReason); return false; } // // See MeshMergingTool.cpp // const IMeshMergeUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshMergeModule>("MeshMergeUtilities").GetUtilities(); FVector MergedActorLocation; TArray<UObject*> CreatedAssets; const float ScreenAreaSize = TNumericLimits<float>::Max(); MeshUtilities.MergeComponentsToStaticMesh(AllComponents, AllActors[0]->GetWorld(), MergeOptions.MeshMergingSettings, nullptr, nullptr, PackageName, CreatedAssets, MergedActorLocation, ScreenAreaSize, true); UStaticMesh* MergedMesh = nullptr; if (!CreatedAssets.FindItemByClass(&MergedMesh)) { UE_LOG(LogEditorScripting, Error, TEXT("MergeStaticMeshActors failed. No mesh was created.")); return false; } FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>("AssetRegistry"); for (UObject* Obj : CreatedAssets) { AssetRegistry.AssetCreated(Obj); } //Also notify the content browser that the new assets exists if (!IsRunningCommandlet()) { FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>("ContentBrowser"); ContentBrowserModule.Get().SyncBrowserToAssets(CreatedAssets, true); } // Place new mesh in the world if (MergeOptions.bSpawnMergedActor) { FActorSpawnParameters Params; Params.OverrideLevel = AllActors[0]->GetLevel(); OutMergedActor = AllActors[0]->GetWorld()->SpawnActor<AStaticMeshActor>(MergedActorLocation, FRotator::ZeroRotator, Params); if (!OutMergedActor) { UE_LOG(LogEditorScripting, Error, TEXT("MergeStaticMeshActors failed. Internal error while creating the merged actor.")); return false; } OutMergedActor->GetStaticMeshComponent()->SetStaticMesh(MergedMesh); OutMergedActor->SetActorLabel(MergeOptions.NewActorLabel); AllActors[0]->GetWorld()->UpdateCullDistanceVolumes(OutMergedActor, OutMergedActor->GetStaticMeshComponent()); } // Remove source actors if (MergeOptions.bDestroySourceActors) { UWorld* World = AllActors[0]->GetWorld(); for (AActor* Actor : AllActors) { GEditor->Layers->DisassociateActorFromLayers(Actor); World->EditorDestroyActor(Actor, true); } } //Select newly created actor GEditor->SelectNone(false, true, false); GEditor->SelectActor(OutMergedActor, true, false); GEditor->NoteSelectionChange(); return true; }
すると引数で渡されたStaticMeshActorの配列から、UPrimitiveComponentの配列を取り出してその後マージしていることが分かります。
なので、このソースを参考に引数をActorに改造してActor内のMeshComponentをマージする関数を作成しました。
bool UEditorUtilExtention::MergeStaticMeshComponents(const AActor* Actor, const FString& PackageName, const FMeshMergingSettings& MergeOptions) { const IMeshMergeUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshMergeModule>("MeshMergeUtilities").GetUtilities(); TInlineComponentArray<UStaticMeshComponent*> ComponentArray; Actor->GetComponents<UStaticMeshComponent>(ComponentArray); TArray<UPrimitiveComponent*> allComponents; bool bActorIsValid = false; for (UStaticMeshComponent* MeshCmp : ComponentArray) { if (MeshCmp->GetStaticMesh() && MeshCmp->GetStaticMesh()->RenderData.IsValid()) { allComponents.Add(MeshCmp); } } FVector MergedActorLocation; TArray<UObject*> CreatedAssets; const float ScreenAreaSize = TNumericLimits<float>::Max(); MeshUtilities.MergeComponentsToStaticMesh(allComponents, allComponents[0]->GetOwner()->GetWorld(), MergeOptions, nullptr, nullptr, PackageName, CreatedAssets, MergedActorLocation, ScreenAreaSize, true); UStaticMesh* MergedMesh = nullptr; if (!CreatedAssets.FindItemByClass(&MergedMesh)) { UE_LOG(LogTemp, Error, TEXT("MergeStaticMeshComponents failed. No mesh was created.")); return false; } FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>("AssetRegistry"); for (UObject* Obj : CreatedAssets) { AssetRegistry.AssetCreated(Obj); } //Also notify the content browser that the new assets exists if (!IsRunningCommandlet()) { FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>("ContentBrowser"); ContentBrowserModule.Get().SyncBrowserToAssets(CreatedAssets, true); } return true; }
引数で渡されたActorからUPrimitiveComponentの配列を取り出し、その後のマージの処理は元のソースのロジックをそのまま使っています。
完成した関数をEditor Utility Widgetから呼ぶとちゃんとSplineMeshがStaticMeshにマージされました。
ただ、マテリアルもコピーされちゃうのは何故だろう??
手動でMerge Actorしたときは元のマテリアルを参照したままマージしてくれてるので、そっちの挙動の方がうれしいのですがちょっとやり方が分かりませんでした。
ちゃんとスタティックメッシュになっています。
まとめ
今回Editor Utility Widgetを初めて使ってみましたが、とても楽しかったです。
慣れ親しんだUMGとブループリントを使ってエディタ拡張が作れるので、日々の作業の自動化が誰でも簡単に作れると思います。
私はちょっと前に大量のSplineMeshComponentを含むActorを手動でStaticMeshにした事があるので、その時にこの機能をしっていればどれだけ楽だったか。。。
自動化はただ作業が楽になるだけでなく、手作業によるケアレスミスも防げるので品質面でも有用だと思います。
今後も自動化したい作業にはどんどん積極的に使っていこうと思いました。
UE4Pluginをマーケットプレイスに出すときのMarketplaceURL
先日2つ目のコードプラグインをマーケットプレイスに申請したのですが、 upluginへ各種プロパティを書いていく中で"MarketplaceURL"のみ空白で提出していました。
そもそもこのURLは実際にマーケットプレイスに公開されるまで決まらないURLだと思っていたので、 私は前回と今回の2回とも以下のフローでコードプラグインの申請をしていました。
- MarketplaceURLは空白で提出
- 審査の人からメールで「upluginにこのURLを書いて再提出してね」を受け取る
- upluginを修正して再度パッケージ化→アップロードする
- 再申請
なんか助長なめんどくさいフローだなあと感じていたのですが、 2回ともこの方法でやっており、 ほっておいたら次の3回目もこの方法でやるつもりでした。
ただ、今日ふと審査待ちのプラグインのWebページを開いたら URLの末尾がupluginに書くMarketplaceURLの値になっているのに気づきました。
そっか、そりゃそうだよね。
こんなめんどくさいフローのはずはないよね。
ひょっとしたらちゃんとどこかに書いてあったのかもしれないのですが、見逃していたのかもしれません。
今まではこのURLを空白にしてたので、必ず再申請ありきのやり方だったのですが、 次からは1回で審査完了めざします。
※2019/0615追記 実際にこの手法で申請したところ一度も修正することなく審査に合格できました。
.NET Core 3.0 WPFを試す(ノードベース電卓をつくってみた)
試してみた
Visual Studio2019 + .NET Core 3.0の組み合わせではWPFが使えるぞということで新規プロジェクトを作成して試してみました。
とりあえず.NET Coreを使っているということを忘れて今までのWPFを使っている気持ちで書いてみて、どんな感じか見てみようと思います。
利用方法は色んな方が紹介されていますが、Visual Studio2019と.NET Core 3.0をインストールして、Visual Studioのオプションから「プレビューの.NET Core SDKを使用」にチェックをONすると使えるようになります。
.NET Core 3.0が正式版になればこのチェックはいらなくなると思います。
C# 8.0 null許容参照型
同時にC# 8.0で導入されるnull許容参照型も試してみました。
今回は新規プロジェクトを作って試すので、.csprojに以下のように記載をしてプロジェクト全体でnull許容参照型を有効にしています。
<PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <UseWPF>true</UseWPF> <LangVersion>8.0</LangVersion> <NullableContextOptions>enable</NullableContextOptions> </PropertyGroup>
この設定にすることで、今までの参照型の書き方ではnullをいれることができなくなります。 またnullである可能性がある変数にアクセスすると警告を出してくれるようになります。
作ったもの
新規でプロジェクトを作るので、何かテーマを決めようと思い、
仕事で触っているUnreal Engine(UE4)や最近注目しているHoudiniというツールでノードベースの作成環境をよく見ているので「ノードベースタイプのUI」をテーマにしました。
ただノードをつなげるだけだと面白くないので、電卓機能をいれています。
作ったものは、この記事を書いた時点で以下のような感じです。
現状ノードをつなげて足し算と引き算ができます。
ノードベース計算機を結構バージョンアップ。
— ayuma (@ayuma_x) 2019年5月11日
・ノード間の線は3次ベジェ曲線に
・ノードのプロパティはポップアップで変更
・入力ピン数の動的変更#wpf #dotnetcore pic.twitter.com/2RFaWWyjXf
プロジェクト一式はGitHubで公開しています。
※ このノードベース電卓ですが、まだまだ作成中の段階のため頻繁に修正しています。
NuGetからの参照
今回NuGetからは以下の4つを参照に加えています。
- Extended.Wpf.Toolkit
- gong-wpf-dragdrop
- Microsoft.Xaml.Behaviors.Wpf
- ReactiveProperty
この中で、Extended.Wpf.Toolkit
とMicrosoft.Xaml.Behaviors.Wpf
は.NET Core 3.0にまだ対応していないようで以下の警告がでました。
NU1701: パッケージ 'Extended.Wpf.Toolkit 3.5.0' はプロジェクトのターゲット フレームワーク '.NETCoreApp,Version=v3.0' ではなく '.NETFramework,Version=v4.6.1' を使用して復元されました。このパッケージは、使用しているプロジェクトとの完全な互換性がない可能性があります。 NU1701: パッケージ 'Microsoft.Xaml.Behaviors.Wpf 1.0.1' はプロジェクトのターゲット フレームワーク '.NETCoreApp,Version=v3.0' ではなく '.NETFramework,Version=v4.6.1' を使用して復元されました。このパッケージは、使用しているプロジェクトとの完全な互換性がない可能性があります。
とりあえず使っている範囲での動作には問題がないのと、.NET Core 3.0はまだプレビュー版なので気にしない事にしています。
.NET Core 3.0が正式版になってもこの警告が残っているようでしたら、自分で対応してビルドするなどの対応が必要かもです。
作りを少し紹介
今回作成した中でノードの新規追加の機能部分にしぼって紹介します。 (線を引く部分とか計算をする部分も書こうと思ったのですが、絶賛作成中のため今後変更する可能性が高いので)
ノードの配置
ノードを配置するViewのxamlは以下の通りです。
<ItemsControl ItemsSource="{Binding Nodes}" Background="Transparent" dd:DragDrop.IsDropTarget="True" dd:DragDrop.DropHandler="{Binding}" > <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <node:NodeContainer /> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemContainerStyle> <Style> <Setter Property="Canvas.Top" Value="{Binding PositionY.Value}"/> <Setter Property="Canvas.Left" Value="{Binding PositionX.Value}"/> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl>
ItemsPanelにCanvasを指定して絶対座標での配置を有効にしています。
<ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel>
ノードの位置設定はItemContainerStyleでCanvas.Top, LeftをノードのViewModelのプロパティにバインドすることで対応しています。
<ItemsControl.ItemContainerStyle> <Style> <Setter Property="Canvas.Top" Value="{Binding PositionY.Value}"/> <Setter Property="Canvas.Left" Value="{Binding PositionX.Value}"/> </Style> </ItemsControl.ItemContainerStyle>
またこのItemsControlへはドラッグアンドドロップによるノード生成も組み込んでいるので、
dd:DragDrop.IsDropTarget
とdd:DragDrop.DropHandler
の指定をしています。
これはNuGetでいれたgong-wpf-dragdrop
の機能で、こう書くことでItemsControlにバインドしているViewModelでドロップ時の処理を書くことができます。
<ItemsControl ItemsSource="{Binding Nodes}" Background="Transparent" dd:DragDrop.IsDropTarget="True" dd:DragDrop.DropHandler="{Binding}" >
ノードの新規生成
以下のソースはノードを配置するViewへドロップした際の処理です。
dropInfo.Data
にはドロップされたViewのDataContext(BindされたViewModel)が入っているので、その型に応じて処理を変えています。
public void Drop(IDropInfo dropInfo) { switch (dropInfo.Data) { case NodeViewModel node: { nowDragItem = null; } break; case NodeConnectionViewModel connection: { connection.LineToX.Value = 0; connection.LineToY.Value = 0; connection.Visible.Value = Visibility.Hidden; } break; case ToolBoxItemViewModel item: { var newItem = Activator.CreateInstance(item.NodeModelType) as NodeBase; if (newItem != null) { newItem.PositionX = dropInfo.DropPosition.X; newItem.PositionY = dropInfo.DropPosition.Y; mainModel.Nodes.Add(newItem); } } break; default: nowDragItem = null; break; } }
ノードを新規作成する際のDrop処理はToolBoxItemViewModel
を処理する部分になります。
ドロップされたViewModelにはType型のNodeModelTypeというプロパティがあり、これをもとにノードのインスタンスを生成します。
生成されたノードにはドロップされた位置をもとに座標が与えられて、モデルのNodes
配列へ追加されます。
この配列への追加はModel->ViewModel->Viewへと伝わり、先に説明したItemsControlで新規ノードが表示されます。
またここのActivator.CreateInstance
しているところでは、当初サボってnullチェックをしていなかったのですが、null許容参照型の機能をONにしていたので警告がでました。
nullチェックをいれることで警告は消えましたが、これはコンパイラがフロー解析を行ってくれているからだそうです。すごい!
case ToolBoxItemViewModel item: { var newItem = Activator.CreateInstance(item.NodeModelType) as NodeBase; if (newItem != null) { newItem.PositionX = dropInfo.DropPosition.X; newItem.PositionY = dropInfo.DropPosition.Y; mainModel.Nodes.Add(newItem); } } break;
まとめ
この記事を書く中で.NET CoreでWPFを使うときは「こんな事に気を付けたほうがいいよ」って事があったらまとめようと思っていたのですが、、、特にありませんでした。
以前までの.NET Framework版WPFを使っている時と同じ感じで利用できます。
.NET Core版ではxamlのデザイナが利用できない事がデメリットだと言われていましたが、VS2019のプレビュー3では使えるようになったらしいので(まだ試してない)これもそのうち問題ではなくなると思います。
またnull許容参照型は、当初警告が邪魔になるかなと思っていたのですがそんな事はなく、むしろ自分がスルーしていたnullアクセスするかもしれない箇所を教えてくれるのでありがたい機能だと感じました。
既存のプロジェクトに導入すると警告祭りになってしまい大変かもしれませんが、新規で作成するプロジェクトは有効にしたほうがコードの安全性が高まると思います。
今後のC#を使った開発は.NET Coreが主流になってくると思いますが、WPFを使ったデスクトップアプリケーションの開発も問題ないなと感じました。