AYU MAX

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

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