NodeModelノードを作ってみよう!中級編
概要
初級編ではZeroTouchノードの仕組みについて学びました。中級編は少しレベルアップをして、WPFを用いたカスタムUIノードを作成します!
WPFとは
WPFはXAML(Extensible Application Markup Language)という言語を用いてUIを開発することが出来るライブラリです。処理部分の実装はC#で記述して、フォーム部分とのデータバインディングを行うことが出来ます。
まず、WPFアプリを作成して、スライダーやチェックボックスの動作を確認しましょう。
File > New > Project
から、新しいWPFアプリプロジェクトを作成します。
MainWindow.xaml
に以下のコードをコピー&ペーストしてください。チェックボックス、スライダー、ボタンが作成されます。
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Width="200" SizeToContent="Height">
<StackPanel>
<CheckBox Name="EnabledCheckBox" Content="Enable" IsChecked="False" Margin="7"/>
<StackPanel Orientation="Horizontal">
<Slider x:Name="slider" Width="140" IsSnapToTickEnabled="True" TickFrequency="1" Minimum="0" Maximum="100" Foreground="Black" Margin="7"/>
<TextBox Name="textbox" Text="{Binding ElementName=slider, Path=Value}" Margin="10" VerticalAlignment="Center"/>
</StackPanel>
<Button Width="75" Margin="5" HorizontalAlignment="Left" VerticalAlignment="Top" Click="Button_Click" Content="Run" IsEnabled="{Binding ElementName=EnabledCheckBox, Path=IsChecked}" />
</StackPanel>
</Window>
コードビハインド
コードビハインドは、GUI のイベント処理部分を別ファイルで管理する方法です。
実際に、ボタンがクリックされた時にスライダー値を表示する以下の関数を、MainWindow.xmal.cs
に追加しましょう!
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("値は" + slider.Value + "です!");
}
アプリケーションをスタートして以下の画像が出てきたら成功です!!
ユーザーコントロール
ユーザーコントロールとは、コンポーネントを組み立てて、大きな一つのコンポーネントとして扱う的なものです。
Project > Add > UserControl
をクリックして、ユーザーコントロールを作成します。名前はMyCustomControl.xaml
とします。
MainWindow.xamlの<StackPanel>...</StackPanel>
の内容をMyCustomControl.xamlの内容に置き換えます。MainWindow.xaml.cs
のButton_Click関数も同様にして、MyCustomControl.xaml.csに移動します。
これで、他のWPFコントロールの中に埋め込むことができる、再利用可能なカスタムコントロールを作成しました。
MainWindow.xaml
では、<StackPanel>...</StackPanel>
に<local:MyCustomControl />
を追加します。
NodeModelノードとは
NodeModelベースのノードは Zero-Touchノードとの大きな違いがいくつかあり、Zero-Touch ノードよりも大幅に柔軟性とパワーを提供してくれます。
Dynamoの設計思想
DynamoはMVVM(model-view-view-model)ソフトウェアアーキテクチャパターンに基づいており、UIをバックエンドから分離しています。ZeroTouchノードを作成する際、DynamoはノードのデータとUIの間のデータバインドを行います。カスタムUIを作成するには、データバインドロジックを追加する必要があります。
MVVMの図
- ノードのコアロジックを確立するためのNodeModelクラス
- NodeModel の表示方法をカスタマイズするための INodeViewCustomizationクラス
NodeModelを実装しよう!
それでは、カスタムUIノード内でUser Controlを使用する方法を見てみましょう。初級編と同様に、クラスライブラリプロジェクト(DLL) を作成します。名前はDynamoUnchained.ExplicitNode とします。NuGet経由でWpfUILibrary
を追加でインストールします。
HelloUI.cs
(NodeModelクラス)とHelloUINodeView
(INodeViewCustomizationクラス)を追加します。
NodeModelノードは、ZeroTouchノードといくつかの重要な違いがあります。NodeModelノードは関数のみを呼び出すことができるため、NodeModelと関数を異なるライブラリに分離する必要があります。なので以下のようなノードの処理に関するコードは、個別のプロジェクトに作成します。
public static string SayHello(string Name)
{
return "Hello " + Name + "!";
}
File > Add > NewProject
からクラスライブラリを追加します。名前は、DynamoUnchained.ExplicitNode.Functionsとします。このFunctionsクラスに先ほど説明したノードの処理にあたる部分を書いていきます。
早速NodeModelを実装していきます!HelloUI.cs
に、以下のコードをコピー&ペーストしてください。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dynamo.Graph.Nodes;
using ProtoCore.AST.AssociativeAST;
namespace DynamoUnchained.ExplicitNode
{
[NodeName("HelloUI")]
[NodeDescription("Sample Explicit Node")]
[NodeCategory("DynamoUnchained")]
[InPortNames("A")]
[InPortTypes("double")]
[InPortDescriptions("Number A")]
[OutPortNames("Output")]
[OutPortTypes("double")]
[OutPortDescriptions("Product of two numbers")]
[IsDesignScriptCompatible]
public class HelloUI : NodeModel
{
public HelloUI()
{
RegisterAllPorts();
}
private double _sliderValue;
public double SliderValue
{
get { return _sliderValue; }
set
{
_sliderValue = value;
RaisePropertyChanged("SliderValue");
OnNodeModified(false);
}
}
public override IEnumerable<AssociativeNode> BuildOutputAst(List<AssociativeNode> inputAstNodes)
{
if (!InPorts[0].IsConnected)
{
return new[] { AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), AstFactory.BuildNullNode()) };
}
var sliderValue = AstFactory.BuildDoubleNode(SliderValue);
var functionCall =
AstFactory.BuildFunctionCall(
new Func<double, double, double>(DynamoUnchained.ExplicitNode.Functions.Functions.MultiplyTwoNumbers),
new List<AssociativeNode> { inputAstNodes[0], sliderValue });
return new[] { AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), functionCall) };
}
}
}
各部分で何が行われているのか理解しましょう。
[NodeName("HelloUI")]
[NodeDescription("Sample Explicit Node")]
[NodeCategory("DynamoUnchained")]
[InPortNames("A")]
[InPortTypes("double")]
[InPortDescriptions("Number A")]
[OutPortNames("Output")]
[OutPortTypes("double")]
[OutPortDescriptions("Product of two numbers")]
[IsDesignScriptCompatible]
名前、カテゴリ、InPort / OutPort名、InPort / OutPortタイプ、説明などのノード属性を指定します。
public class HelloUI : NodeModel
{
}
カスタム UI ノードはネイティブノードと同じように NodeModel インターフェースを実装します。インターフェースを実装するクラスには、クラス名の後に :NodeModel を追加し、クラス内に特定の機能を持たせる必要があります。
public HelloUI()
{
RegisterAllPorts();
}
ノードの入力と出力を登録するコンストラクターです。
private double _sliderValue;
public double SliderValue
{
get { return _sliderValue; }
set
{
_sliderValue = value;
RaisePropertyChanged("SliderValue");
OnNodeModified(false);
}
}
ここではスライダーが変化したときに値を更新する設定をしています。
public override IEnumerable<AssociativeNode> BuildOutputAst(List<AssociativeNode> inputAstNodes)
{
}
BuildOutputAst()は、NodeModelノードからデータを返すために必要な構造であるAST(抽象構文木)を返します。
if (!InPorts[0].IsConnected)
{
return new[] { AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), AstFactory.BuildNullNode())
};
入力ポートが接続されていない場合、nullを返します。これによって、ノードに警告が表示されないようになります。
var sliderValue = AstFactory.BuildDoubleNode(SliderValue);
スライダーの値を表すノードをAST内に作成します。
var functionCall =
AstFactory.BuildFunctionCall(
new Func<double, double, double>(DynamoUnchained.ExplicitNode.Functions.Functions.MultiplyTwoNumbers),
new List<AssociativeNode> { inputAstNodes[0], sliderValue });
return new[] { AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), functionCall) };
Functions.csから MultiplyTwoNumbers関数を呼び出します。
関数を呼び出そう!
ここでは、入力ポートにつながれた数値とスライダーをかけて、その値を出力するという処理を書きます。NuGetパッケージマネージャーからDynamoVisualProgramming.DynamoServices
をインストールしましょう。Functions.csに以下のコードをコピー&ペーストします。
using Autodesk.DesignScript.Runtime;
namespace DynamoUnchained.ExplicitNode.Functions
{
[IsVisibleInDynamoLibrary(false)]
public static class Functions
{
public static double MultiplyTwoNumbers(double a, double b)
{
return a * b;
}
}
}
CustomNodeModel はMultiplyTwoNumbers関数を呼び出すために Functions.csを参照する必要があります。
Add > Reference > Project > solution > Functions.cs
にチェックを入れ、OKを押します。
UIをカスタマイズしよう!
先ほど作成した以下のファイルを、DynamoUnchained.ExplicitNodeにドラッグ&ドロップします。
また、これら2つのファイルの名前空間をWpfApp
からDynamoUnchained.ExplicitNode
に置き換える必要があります。
INodeViewCustomizationを実装しよう!
INodeViewCustomization<HelloUI>
では、UIをカスタマイズするために必要な機能を定義しています。
using Dynamo.Controls;
using Dynamo.Wpf;
namespace DynamoUnchained.ExplicitNode
{
public class HelloUINodeView : INodeViewCustomization<HelloUI>
{
public void CustomizeView(HelloUI model, NodeView nodeView)
{
var ui = new MyCustomControl();
nodeView.inputGrid.Children.Add(ui);
ui.DataContext = model;
}
public void Dispose()
{
}
}
}
スライダーの変更
MyCustomControl.XAMLのスライダーを以下のように変更します。
<Slider
x:Name="slider"
Width="140"
IsSnapToTickEnabled="True"
TickFrequency="1"
Minimum="0"
Maximum="100"
Foreground="Black"
Margin="7"
Value="{Binding SliderValue}" />
その他
jsonの変更
初級編と同様にjsonファイルを作成し、"node_libraries"を以下のように書き換えます。
"node_libraries": [
"DynamoUnchained.ExplicitNode, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"DynamoUnchained.ExplicitNode.Functions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
]
Dynamoパッケージフォルダーへのデプロイ
初級編と同様にデプロイの設定をします。
xcopy /Y "$(TargetDir)*.*" "$(AppData)\Dynamo\Dynamo Revit\2.3\packages\$(ProjectName)\bin\"
xcopy /Y "$(ProjectDir)pkg.json" "$(AppData)\Dynamo\Dynamo Revit\2.3\packages\$(ProjectName)"