-
-
Notifications
You must be signed in to change notification settings - Fork 227
了解组件的命名方式及其在编辑器可视化树中的角色对于理解代码和文档非常重要。
根组件是一个编辑器(editor),它包含节点(nodes)和连接(connections)以及一些额外的UI元素,如选择框(selection rectangle)和一个预备连接(pending connection),以使编辑器具有交互性。
节点是连接器(connectors)的容器,有时候节点本身也可以作为连接器(比如 状态节点).
连接器可以创建预备连接,预备连接在完成后可以成为实际的连接。
一图胜千言
你可能会好奇,一个节点如何既能作为连接器本身又能像普通节点一样运行。编辑器包含三个主要层次,这些层次有助于解决这个问题:
- 项目层(NodifyEditor.ItemsSource)——在这里,每个控件都被包装在一个容器中,使其可以选择、拖动等,并且可以渲染任何控件(例如连接器、文本块)
- 连接层(NodifyEditor.Connections)——这是所有连接共存的地方,并默认在项目层下面渲染。
- 装饰层(NodifyEditor.Decorators)——在这里,每个控件在窗口中都有一个位置。
将这些层次分开,使得每个层次可以异步加载成为可能。
将以下其中一个主题合并到 App.xaml 中的资源字典中:
- 深色主题(如果未指定,则为默认主题):
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Dark.xaml" />
- 浅色主题:
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Light.xaml" />
- Nodify主题:
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Nodify.xaml" />
导入 nodify
命名空间:xmlns:nodify="https://miroiu.github.io/nodify"
或 xmlns:nodify="clr-namespace:Nodify;assembly=Nodify"
到你的文件中,并创建一个编辑器实例 <nodify:NodifyEditor />
。如果你启动应用程序,你会看到一个可以创建选择矩形的空白区域。
提示:将选择矩形拖动到编辑器区域的边缘附近,屏幕将自动向该方向移动。
现在我们将显示一些节点。让我们创建视图模型并将它们绑定到视图。
public class NodeViewModel
{
public string Title { get; set; }
}
public class EditorViewModel
{
public ObservableCollection<NodeViewModel> Nodes { get; } = new ObservableCollection<NodeViewModel>();
public EditorViewModel()
{
Nodes.Add(new NodeViewModel { Title = "Welcome" });
}
}
视图模型可以是任何形状,但节点的视图由 ItemTemplate
生成。(将 DataTemplate 放在 NodifyEditor.Resources 中也能实现相同的效果)
<nodify:NodifyEditor ItemsSource="{Binding Nodes}">
<nodify:NodifyEditor.DataContext>
<local:EditorViewModel />
</nodify:NodifyEditor.DataContext>
<nodify:NodifyEditor.ItemTemplate>
<DataTemplate DataType="{x:Type local:NodeViewModel}">
<nodify:Node Header="{Binding Title}" />
</DataTemplate>
</nodify:NodifyEditor.ItemTemplate>
</nodify:NodifyEditor>
请注意,我们绑定 Node 的 Header 属性来显示 Title。要了解更多节点类型和自定义,请查看节点概述。
好的,现在让我们添加更多节点并将它们连接起来。首先,我们需要一个连接器的表示以及节点上一些集合来存储我们的连接器。
public class ConnectorViewModel
{
public string Title { get; set; }
}
public class NodeViewModel
{
public string Title { get; set; }
public ObservableCollection<ConnectorViewModel> Input { get; set; } = new ObservableCollection<ConnectorViewModel>();
public ObservableCollection<ConnectorViewModel> Output { get; set; } = new ObservableCollection<ConnectorViewModel>();
}
public class EditorViewModel
{
public ObservableCollection<NodeViewModel> Nodes { get; } = new ObservableCollection<NodeViewModel>();
public EditorViewModel()
{
Nodes.Add(new NodeViewModel
{
Title = "Welcome",
Input = new ObservableCollection<ConnectorViewModel>
{
new ConnectorViewModel
{
Title = "In"
}
},
Output = new ObservableCollection<ConnectorViewModel>
{
new ConnectorViewModel
{
Title = "Out"
}
}
});
}
}
然后将它们绑定到视图。(我们使用了内置的 NodeInput
和 NodeOutput
作为视图,但也有其他连接器。或者根据需要创建自己的连接器。)
<nodify:Node Header="{Binding Title}"
Input="{Binding Input}"
Output="{Binding Output}">
<nodify:Node.InputConnectorTemplate>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<nodify:NodeInput Header="{Binding Title}" />
</DataTemplate>
</nodify:Node.InputConnectorTemplate>
<nodify:Node.OutputConnectorTemplate>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<nodify:NodeOutput Header="{Binding Title}" />
</DataTemplate>
</nodify:Node.OutputConnectorTemplate>
</nodify:Node>
Node
控件支持 Input
和 Output
连接器,您可以通过重写 InputConnectorTemplate
和 OutputConnectorTemplate
的默认模板来自定义这些连接器。
从 Input
或 Output
连接器点击并拖动一根线将创建一个预备连接,我们可以将其转换为实际连接。
Nodify 最复杂的部分是如何将连接绑定到它们的连接器。 让我们为连接创建 ViewModel,并在 EditorViewModel
中添加连接列表。
public class ConnectionViewModel
{
public ConnectorViewModel Source { get; set; }
public ConnectorViewModel Target { get; set; }
}
public class EditorViewModel
{
public ObservableCollection<NodeViewModel> Nodes { get; } = new ObservableCollection<NodeViewModel>();
public ObservableCollection<ConnectionViewModel> Connections { get; } = new ObservableCollection<ConnectionViewModel>();
public EditorViewModel()
{
var welcome = new NodeViewModel
{
Title = "Welcome",
Input = new ObservableCollection<ConnectorViewModel>
{
new ConnectorViewModel
{
Title = "In"
}
},
Output = new ObservableCollection<ConnectorViewModel>
{
new ConnectorViewModel
{
Title = "Out"
}
}
};
var nodify = new NodeViewModel
{
Title = "To Nodify",
Input = new ObservableCollection<ConnectorViewModel>
{
new ConnectorViewModel
{
Title = "In"
}
}
};
Nodes.Add(welcome);
Nodes.Add(nodify);
Connections.Add(new ConnectionViewModel
{
Source = welcome.Output[0],
Target = nodify.Input[0]
});
}
}
然后更新 ConnectorViewModel
以具有连接可以附加的 Anchor
点。(这需要是响应式的,因此我们将在视图模型中实现 INotifyPropertyChanged 接口)。
注意:Point 类型必须来自 System.Windows。
public class ConnectorViewModel : INotifyPropertyChanged
{
private Point _anchor;
public Point Anchor
{
set
{
_anchor = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Anchor)));
}
get => _anchor;
}
public string Title { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
}
将 Anchor
绑定到连接器的视图,并设置 Mode=OneWayToSource
。还需将 IsConnected
设置为 True
以接收 Anchor
更新。
<nodify:Node.InputConnectorTemplate>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<nodify:NodeInput Header="{Binding Title}"
IsConnected="True"
Anchor="{Binding Anchor, Mode=OneWayToSource}" />
</DataTemplate>
</nodify:Node.InputConnectorTemplate>
<nodify:Node.OutputConnectorTemplate>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<nodify:NodeOutput Header="{Binding Title}"
IsConnected="True"
Anchor="{Binding Anchor, Mode=OneWayToSource}" />
</DataTemplate>
</nodify:Node.OutputConnectorTemplate>
并将连接绑定到视图,让它们在 ConnectionTemplate
中使用我们 ConnectorViewModel
的 Anchor
。有关更多自定义,请参阅连接概述。
<nodify:NodifyEditor ItemsSource="{Binding Nodes}"
Connections="{Binding Connections}">
...
<nodify:NodifyEditor.ConnectionTemplate>
<DataTemplate DataType="{x:Type local:ConnectionViewModel}">
<nodify:LineConnection Source="{Binding Source.Anchor}"
Target="{Binding Target.Anchor}" />
</DataTemplate>
</nodify:NodifyEditor.ConnectionTemplate>
...
如果你现在启动应用程序,你会看到有一个连接,并且如果你拖动节点,连接会跟随它们移动。
现在让我们在 ConnectorViewModel
中添加 IsConnected
属性,以便在实际连接时设置它。并更新 ConnectionViewModel
以便在构造时自动连接它们。
public class ConnectorViewModel : INotifyPropertyChanged
{
private Point _anchor;
public Point Anchor
{
set
{
_anchor = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Anchor)));
}
get => _anchor;
}
private bool _isConnected;
public bool IsConnected
{
set
{
_isConnected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsConnected)));
}
get => _isConnected;
}
public string Title { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
}
public class ConnectionViewModel
{
public ConnectionViewModel(ConnectorViewModel source, ConnectorViewModel target)
{
Source = source;
Target = target;
Source.IsConnected = true;
Target.IsConnected = true;
}
public ConnectorViewModel Source { get; }
public ConnectorViewModel Target { get; }
}
并且不要忘记在连接器模板中绑定它。
IsConnected="{Binding IsConnected}"
预备连接从一个 Source
开始,当放置到一个 Target
上时将完成。源始终是一个连接器,目标可以是一个连接器、一个项目容器或 null
。我们现在只关心其他连接器。当连接开始时,执行 StartedCommand
,该命令接收 Source
作为参数。当连接完成时,执行 CompletedCommand
,该命令接收 Target
作为参数。
让我们实现预备连接的视图模型,并将其添加到 EditorViewModel
中。
public class PendingConnectionViewModel
{
private readonly EditorViewModel _editor;
private ConnectorViewModel _source;
public PendingConnectionViewModel(EditorViewModel editor)
{
_editor = editor;
StartCommand = new DelegateCommand<ConnectorViewModel>(source => _source = source);
FinishCommand = new DelegateCommand<ConnectorViewModel>(target =>
{
if (target != null)
_editor.Connect(_source, target);
});
}
public ICommand StartCommand { get; }
public ICommand FinishCommand { get; }
}
public class EditorViewModel
{
public PendingConnectionViewModel PendingConnection { get; }
...
public EditorViewModel()
{
PendingConnection = new PendingConnectionViewModel(this);
...
}
...
public void Connect(ConnectorViewModel source, ConnectorViewModel target)
{
Connections.Add(new ConnectionViewModel(source, target));
}
}
并将其绑定到视图上
<nodify:NodifyEditor PendingConnection="{Binding PendingConnection}">
...
<nodify:NodifyEditor.PendingConnectionTemplate>
<DataTemplate DataType="{x:Type local:PendingConnectionViewModel}">
<nodify:PendingConnection StartedCommand="{Binding StartCommand}"
CompletedCommand="{Binding FinishCommand}"
AllowOnlyConnectors="True" />
</DataTemplate>
</nodify:NodifyEditor.PendingConnectionTemplate>
...
</nodify:NodifyEditor>
这就是创建连接的全部内容。现在你应该可以在连接器之间创建连接了。
要删除连接,只需监听来自连接器本身或编辑器的断开连接事件,并删除具有连接器作为源或目标的连接。为了简单起见,我们将为 NodifyEditor
实现 DisconnectConnectorCommand
。首先让我们将其添加到 EditorViewModel
。
public class EditorViewModel
{
public ICommand DisconnectConnectorCommand { get; }
...
public EditorViewModel()
{
DisconnectConnectorCommand = new DelegateCommand<ConnectorViewModel>(connector =>
{
var connection = Connections.First(x => x.Source == connector || x.Target == connector);
connection.Source.IsConnected = false; // This is not correct if there are multiple connections to the same connector
connection.Target.IsConnected = false;
Connections.Remove(connection);
});
...
}
}
现在我们将此命令绑定到编辑器试图上。
<nodify:NodifyEditor ItemsSource="{Binding Nodes}"
Connections="{Binding Connections}"
PendingConnection="{Binding PendingConnection}"
DisconnectConnectorCommand="{Binding DisconnectConnectorCommand}">
...
</nodify:NodifyEditor>
如你所见,节点总是在屏幕的左上角。这是因为它们在图中的位置是 (0, 0)。让我们来改变这一点!
在 NodeViewModel
中添加一个 Location
属性,类型为 System.Windows.Point
,并触发 PropertyChanged
事件。
public class NodeViewModel : INotifyPropertyChanged
{
private Point _location;
public Point Location
{
set
{
_location = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Location)));
}
get => _location;
}
public event PropertyChangedEventHandler PropertyChanged;
...
}
并将其绑定到视图
<nodify:NodifyEditor ItemsSource="{Binding Nodes}"
Connections="{Binding Connections}"
PendingConnection="{Binding PendingConnection}">
<nodify:NodifyEditor.ItemContainerStyle>
<Style TargetType="{x:Type nodify:ItemContainer}">
<Setter Property="Location"
Value="{Binding Location}" />
</Style>
</nodify:NodifyEditor.ItemContainerStyle>
...
</nodify:NodifyEditor>
注意:我使用了 ItemContainerStyle 来绑定节点的位置。请查看项目容器概述获取更多信息。
现在你可以在构造节点时设置它们的位置。
绘制简单的网格只需创建一个网格画笔,同时将编辑器的变换持续应用于它,并将该画笔用作编辑器的 Background
。
因为我们绘制的网格是由线条组成的,而不是填充的,所以编辑器的 Background
将具有一些透明度,这意味着我们会看到下面控件的背景颜色。为了解决这个问题,将编辑器包装在一个 Grid
中,并设置其 Background
,或者设置 Window
的 Background
。
使用 ViewportTransform
依赖属性使网格随视图移动。
注意:示例使用了在
App.xaml
中选择的主题提供的静态资源。
<Window x:Class="MyProject.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:nodify="https://miroiu.github.io/nodify"
mc:Ignorable="d">
<Window.Resources>
<GeometryDrawing x:Key="SmallGridGeometry"
Geometry="M0,0 L0,1 0.03,1 0.03,0.03 1,0.03 1,0 Z"
Brush="{StaticResource NodifyEditor.SelectionRectangleBackgroundBrush}" />
<GeometryDrawing x:Key="LargeGridGeometry"
Geometry="M0,0 L0,1 0.015,1 0.015,0.015 1,0.015 1,0 Z"
Brush="{StaticResource NodifyEditor.SelectionRectangleBackgroundBrush}" />
<DrawingBrush x:Key="SmallGridLinesDrawingBrush"
TileMode="Tile"
ViewportUnits="Absolute"
Viewport="0 0 20 20"
Transform="{Binding ViewportTransform, ElementName=Editor}"
Drawing="{StaticResource SmallGridGeometry}" />
<DrawingBrush x:Key="LargeGridLinesDrawingBrush"
TileMode="Tile"
ViewportUnits="Absolute"
Opacity="0.5"
Viewport="0 0 100 100"
Transform="{Binding ViewportTransform, ElementName=Editor}"
Drawing="{StaticResource LargeGridGeometry}" />
</Window.Resources>
<Grid Background="{StaticResource NodifyEditor.BackgroundBrush}">
<nodify:NodifyEditor x:Name="Editor" Background="{StaticResource SmallGridLinesDrawingBrush}" />
<Grid Background="{StaticResource LargeGridLinesDrawingBrush}"
Panel.ZIndex="-2" />
</Grid>
</Window>
提示:右键单击并拖动屏幕以移动视图,使用鼠标滚轮放大和缩小。