AYU MAX

プログラミングとか作ったもの

UE4 MarketplaceにJsonパーサープラグインをリリースしました

EasyJsonParser

自作のUE4プラグイン3作目としてEasyJsonParserをリリースしました。

www.unrealengine.com

これは2作目のEasyXMLParserとコンセプトは同じで、ブループリント上から簡単なアクセス方法でJson内の値を取得できます。

f:id:ayuma0913:20190616151043p:plain

こちらもGitHubのほうにサンプルプロジェクトをアップロードしてますので、ご興味ある方は参考にしてください。

github.com

使い方

Json文字列 or Jsonファイルロードした後、アクセス文字列を指定して値の取得を行います。

f:id:ayuma0913:20190615222610p:plain

アクセス文字列の指定

基本的には取得したい値までのパスをドットでつなげて指定します。

シンプルなケース

以下のシンプルな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つの関数が用意されます。

f:id:ayuma0913:20190615225443p:plain

  • ReadInt(int)
  • ReadFloat(float)
  • ReadString(string)
  • ReadBool(bool)

「AccessString」にはアクセス文字列を入力します。

「DefaultValue」にはデフォルト値を入力します。指定された値がJsonに存在しない場合は、デフォルト値が返されます。

オブジェクトの取得

値ではなくオブジェクトとして取得する "ReadObject"および "ReadObjects"メソッドもあります。

このメソッドで取得できるのはオブジェクトプロパティのみです。

ReadObjectは1つのノードオブジェクトを取得します。

ReadObjectsは複数のオブジェクトの配列を取得します。

以下のように、いったん階層途中のオブジェクトを取得してから、そのオブジェクトのプロパティを取得する使い方もできます。

f:id:ayuma0913:20190615230219p:plain

f:id:ayuma0913:20190615230333p:plain

今できることと今後やりたいこと

今回はJsonから値を取得するという機能のみを、いかにシンプルに実装するかを考えて作成しました。

今はJsonはConfig値を設定ファイルに保存して利用したり、Web APIのデータのやりとりなど様々なところで使われていると思います。

このプラグインを使用すると、そのようなJsonからの値の取り出しをブループリントから楽にできるようになります。

今後の機能UPとしては以下のようなものを候補に考えています。 特に今は値の取得しかできないので、値の設定側もできるようになると幅が広がるのかなあと思っています。

  • マルチプラットフォーム対応
  • 値の設定

ObjectDeliverer v1.2.0リリース

ObjectDelivererのV1.2.0をリリースしました。

変更点は以下の2つです。

  • PacketRuleNodivisionを使用してUDPの受信を行うと無限ループしてしまう不具合修正

  • TCP/IPクライアントでサーバーが切断されると自動的に接続を試みるオプション追加

PacketRuleNodivisionを使用してUDPの受信を行うと無限ループしてしまう不具合修正

f:id:ayuma0913:20190611214024p:plain

上記のようにPacketRuleNodivisionを使用してUDPの受信機能を使うと、一度受信したタイミングで無限ループに入ってしまう不具合が起きていましたので修正しました。

UDPの受信はPacketRuleNodivisionを使うことが一番多いと思うので、ホント致命的な不具合で申し訳ないです。

TCP/IPクライアントでサーバーが切断されると自動的に接続を試みるオプション追加

f:id:ayuma0913:20190611214849p:plain

以前からTCP/IPクライアントを利用時にRetryフラグをONにすると、自動でサーバーが起動するまでConnectを繰り返す機能が実装されていました。

ただし、この機能はいったんサーバーと接続確立後にサーバー終了した場合は働かない仕様でした。

そこで今回新たにAutoConnectAfterDisconnectフラグを作成しました。

このフラグをONにすると、接続済みのサーバーとの接続がなくなった場合に自動でConnectを繰り返すようになります。

そのため、従来のRetryフラグを合わせて使うことで、サーバーとつながっていない時はいつでもConnectを繰り返すようになります。

常に接続を確立しておきたいケースに便利です。

UE4 MarketplaceにXMLパーサープラグインを出品しました

EasyXMLParser

本日Unreal EngineのMarketplaceにEasyXMLParserという名前でプラグインを出品しました。

前回リリースしたObjectDelivererに続いて2作品目となります。

前回は無料で出品したのですが、今回はお値段をつけさせていただきました。 使いやすさを重視して作りましたので、よろしければ是非ご検討ください。

このプラグインを使ったサンプルはGitHubに公開しています。

github.com

機能紹介

EasyXMLParserの機能を紹介します。

テキストの取得

テキストノードを取得するには、取得したい値のアクセス文字列を作成します。

アクセス文字列はタグ名をドットで結ぶことによって作成されます。

同じタグ名を持つノードが複数ある場合は、0から始まるインデックス番号を指定できます(例:[2] - > 3番目)。

インデックスを指定しないと、最初の要素が取得されます。

GALLERY 4

属性を取得するためのアクセス文字列は(アットマーク+属性名)で指定します。

GALLERY5

型を指定しての値の取得

XMLから値を取得するために、次の4つの機能が用意されます。

  • ReadInt(int)
  • ReadFloat(float)
  • ReadString(string)
  • ReadBool(bool)

「AccessString」にはアクセス文字列を入力します。

「DefaultValue」にはデフォルト値を入力します。指定された値がXMLに存在しない場合は、デフォルト値が返されます。

GALLERY 1

オブジェクトの取得

ノードを値としてではなくオブジェクトとして取得する "ReadElement"および "ReadElements"メソッドもあります。

ReadElementは1つのノードを取得します。

ReadElementsは複数のノードの配列を取得します。

GALLERY 2

XMLパース手法

XMLをロードする方法は2つあります。XMLファイルをロードするための "LoadFromFile"と、XML文字列をロードするための "LoadFromString"です。

バックグラウンドで解析を実行する非同期バージョンもあります。

GALLERY 3

XMLの次は

個人的にはたまにXMLをパースする機会があるので、今回プラグイン化してみました。

ただ現在はXMLよりもJsonの方が使われているような気もしています。

ということで、次のプラグインとしてJsonパーサーを出すつもりです。

このEasyXMLParserと同様の使い勝手で設計しておりMarketPlaceに現在審査してもらっています。

ObjectDelivererの共有メモリサンプル

共有メモリのサンプル

以前からGitHubにObjectDelivererのサンプルを用意していたのですが、共有メモリについては読み込み側しか実装しておらず処理が完結していなかったので新規で書き込み側も実装しました。

ここではサンプルの内容を簡単に紹介します。

概要

このサンプルではSceneCapture2Dで撮った画像をTexurueRenderTargetから取り出し、そこから共有メモリを経由して別のマテリアルに表示します。

203fc2438cc4488ddf9567ad67496f63

  1. まずはSharedMemoryレベルを開いてください。 sharedmemory1

  2. 全実装はBox Actorブループリント内にあります。

読み込み側の実装

  1. 共有メモリから読み込んだピクセルバッファを入れるテクスチャとマテリアルを用意します。 sharedmemory2

  2. 共有メモリの読み込み用ObjectDelivererのインスタンスを生成し処理を開始します。 sharedmemory3

共有メモリのバッファサイズは画像バッファサイズ(800x450x4 byte)を設定しています。

  1. ReceiveDataイベント(共有メモリの中身に変化があった時におこるイベント)でテクスチャの中身を更新します。 sharedmemory4

書き込み側の実装

  1. TextureRenderTargetから取得したピクセルバッファを入れておくバッファを用意します。
  2. 共有メモリへの書き込み用ObjectDelivererのインスタンスを生成し処理を開始します。 sharedmemory3

共有メモリのバッファサイズは画像バッファサイズ(800x450x4 byte)を設定しています。

sharedmemory4

  1. Tickイベント時にTextureRenderTargetからピクセルバッファを取り出し共有メモリに書き込みます。 sharedmemory4

使い道

この例では1つのプロセス内で完結しているためあまり意味はないですが、 例えばUE4のプロジェクトを2つ用意して、片方は共有メモリへの書き込みを担当し、もう片方は共有メモリからの読み込みを担当することで映像の受け渡しができます。

共有メモリはTCP/IPやUDPと違ってネットワーク経由でのデータの受け渡しはできませんが、その分大容量のデータを高速に渡せるため映像などの重いデータのプロセス間通信に適していると思います。

ご興味ある方はぜひ試してみてください。

Editor Utility Widgetを使ってお絵かきして動的メッシュ生成機能を作った

f:id:ayuma0913:20190530234709p:plain

Editor Utility Widget

今回UE4.22から利用できるようになった、Editor Utility Widgetという機能を試してみました。 Editor Utility Widgetを利用するとUMGとブループリントでエディタ拡張が簡単に作れます。

Editor Utility Widgetの基本的な説明はおかずさんの記事を参考にしました。

qiita.com

つくったもの

Editor Utility Widget上をマウスドラッグすることで線を一筆書きで書き、 書き終わった線に沿ってスプラインメッシュを生成する機能を作りました。

できたスプラインメッシュは、ボタンを押すとスタティックメッシュにできます。

Editor Scripting Utilities Pluginの有効化

このプラグインを有効にすると、Editor Utility Widgetからアセットの操作などができるようになります。 f:id:ayuma0913:20190530234123p:plain

Widget上で線を引く

線を引く部分はUserWidgetを1つ作ってその中で実装しました。

まず、変数に線を引くための座標(Vector2D)の配列を用意します。 f:id:ayuma0913:20190530230810p:plain

次にMouseDownで毎回座標配列を空っぽに。 f:id:ayuma0913:20190530230738p:plain

MouseMoveでは座標の配列にマウスの座標をいれます。

ただし、すべてのMouseMoveイベントで処理すると点の数が多すぎるので前回格納した点から5pixel以上離れた場合のみ点を格納しています。

f:id:ayuma0913:20190530230956p:plain

あとはOnPaintでDrawLinesを呼べば線が引かれます。 f:id:ayuma0913:20190530231131p:plain

MouseUpでは線が引かれ終わったことを示すイベントを発火しています。

このイベントをEditor Utility Widgetが監視してメッシュ生成をこの後していきます。

f:id:ayuma0913:20190530231259p:plain

Editor Utility Widgetの実装

次にEditor Utility Widgetを作成します。

先ほど作ったUserWidgetも配置します。

f:id:ayuma0913:20190530231520p:plain

ComboBoxにメッシュ生成機能をもったActorの一覧を出したいので、 ComboBoxが開いたタイミングでActorの一覧を取得し追加しています。

またComboBoxの選択が変わった時にカレントのActorを設定しています。

f:id:ayuma0913:20190530231727p:plain

先ほど作ったUserWidgetの線の引き終わりイベントを監視して、カレントのActorに線に沿った座標の配列を渡します。 f:id:ayuma0913:20190530231813p:plain

スプラインメッシュ生成

スプラインメッシュを生成するActorを作っていきます。

SplineComponentを2つと、SplineMeshComponentの親になるSceneComponentを1つ追加しておきます。 SplineComponentは点の補正をするために贅沢に2つ使ってます。

f:id:ayuma0913:20190530232107p:plain

UserWidgetの線が引き終わったタイミングで呼ばれる関数を作成していきます。

まずは、直前に生成されていたSplineMeshComponentを全て破棄し、SplineComponentの点も空にします。 f:id:ayuma0913:20190530232243p:plain

次に渡されて点の配列をそのままSplineComponentの点に追加していきます。 1pixelを10cmとして設定しています。

f:id:ayuma0913:20190530232409p:plain

次に1つ目のSplineComponentのラインに沿って等間隔の点を取り出し2つ目のSplineComponentへ点を追加していきます。

ここで2つ目のSplineComponentを作っているのは、等間隔に点を作ったほうがこの後作成するSplineMeshがきれいになるからです。

f:id:ayuma0913:20190530232549p:plain

最後に2つ目のSplineComponentの点の位置とTangentを使ってSplineMeshComponentを作っていきます。

これでUserWidget上に描いた線に沿ったメッシュが出来上がります。 f:id:ayuma0913:20190530232744p:plain

スプラインメッシュをスタティックメッシュに変換

SplineMeshComponentを含むActorをStaticMeshにするには、いつもMergeActorの機能を使っているのでこれをEditor Utility Widgetから呼べないか探してみました。

すると以下のノードが見つかりましたが、これはインプットがStaticMeshActorになっているので型が合いません。

f:id:ayuma0913:20190530232947p:plain

なので何とかならないか、このノードの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にマージされました。 f:id:ayuma0913:20190530233704p:plain

ただ、マテリアルもコピーされちゃうのは何故だろう??

手動でMerge Actorしたときは元のマテリアルを参照したままマージしてくれてるので、そっちの挙動の方がうれしいのですがちょっとやり方が分かりませんでした。 f:id:ayuma0913:20190530233817p:plain

ちゃんとスタティックメッシュになっています。 f:id:ayuma0913:20190530233948p:plain

まとめ

今回Editor Utility Widgetを初めて使ってみましたが、とても楽しかったです。

慣れ親しんだUMGとブループリントを使ってエディタ拡張が作れるので、日々の作業の自動化が誰でも簡単に作れると思います。

私はちょっと前に大量のSplineMeshComponentを含むActorを手動でStaticMeshにした事があるので、その時にこの機能をしっていればどれだけ楽だったか。。。

自動化はただ作業が楽になるだけでなく、手作業によるケアレスミスも防げるので品質面でも有用だと思います。

今後も自動化したい作業にはどんどん積極的に使っていこうと思いました。

UE4Pluginをマーケットプレイスに出すときのMarketplaceURL

先日2つ目のコードプラグインをマーケットプレイスに申請したのですが、 upluginへ各種プロパティを書いていく中で"MarketplaceURL"のみ空白で提出していました。

そもそもこのURLは実際にマーケットプレイスに公開されるまで決まらないURLだと思っていたので、 私は前回と今回の2回とも以下のフローでコードプラグインの申請をしていました。

  1. MarketplaceURLは空白で提出
  2. 審査の人からメールで「upluginにこのURLを書いて再提出してね」を受け取る
  3. upluginを修正して再度パッケージ化→アップロードする
  4. 再申請

なんか助長なめんどくさいフローだなあと感じていたのですが、 2回ともこの方法でやっており、 ほっておいたら次の3回目もこの方法でやるつもりでした。

ただ、今日ふと審査待ちのプラグインのWebページを開いたら URLの末尾がupluginに書くMarketplaceURLの値になっているのに気づきました。

f:id:ayuma0913:20190527215202p:plain

そっか、そりゃそうだよね。

こんなめんどくさいフローのはずはないよね。

ひょっとしたらちゃんとどこかに書いてあったのかもしれないのですが、見逃していたのかもしれません。

今まではこの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が正式版になればこのチェックはいらなくなると思います。

f:id:ayuma0913:20190512064056p:plain

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」をテーマにしました。

f:id:ayuma0913:20190513175842p:plain

ただノードをつなげるだけだと面白くないので、電卓機能をいれています。

作ったものは、この記事を書いた時点で以下のような感じです。

現状ノードをつなげて足し算と引き算ができます。

プロジェクト一式はGitHubで公開しています。

github.com

※ このノードベース電卓ですが、まだまだ作成中の段階のため頻繁に修正しています。

NuGetからの参照

今回NuGetからは以下の4つを参照に加えています。

  • Extended.Wpf.Toolkit
  • gong-wpf-dragdrop
  • Microsoft.Xaml.Behaviors.Wpf
  • ReactiveProperty

f:id:ayuma0913:20190513062057p:plain

この中で、Extended.Wpf.ToolkitMicrosoft.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.IsDropTargetdd: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を使ったデスクトップアプリケーションの開発も問題ないなと感じました。