534 lines
18 KiB
Markdown
534 lines
18 KiB
Markdown
了解组件的命名方式及其在编辑器可视化树中的角色对于理解代码和文档非常重要。
|
||
|
||
## 层次结构和术语
|
||
|
||
根组件是一个[编辑器(editor)](编辑器概述),它包含[节点(nodes)](节点概述)和[连接(connections)](连接概述)以及一些额外的UI元素,如[选择框(selection rectangle)](编辑器概述#选择)和一个[预备连接(pending connection)](连接概述#预备连接),以使编辑器具有交互性。
|
||
|
||
节点是[连接器(connectors)](连接器概述)的容器,有时候节点本身也可以作为连接器(比如 [状态节点](节点概述#4-状态节点(state-node)控件)).
|
||
|
||
连接器可以创建预备连接,预备连接在完成后可以成为实际的连接。
|
||
|
||
_一图胜千言_
|
||
|
||

|
||
|
||
## 内容层
|
||
|
||
你可能会好奇,一个节点如何既能作为连接器本身又能像普通节点一样运行。编辑器包含三个主要层次,这些层次有助于解决这个问题:
|
||
|
||
1. 项目层(NodifyEditor.ItemsSource)——在这里,每个控件都被包装在一个容器中,使其可以选择、拖动等,并且可以渲染任何控件(例如连接器、文本块)
|
||
2. 连接层(NodifyEditor.Connections)——这是所有连接共存的地方,并默认在项目层下面渲染。
|
||
3. 装饰层(NodifyEditor.Decorators)——在这里,每个控件在窗口中都有一个位置。
|
||
|
||
将这些层次分开,使得每个层次可以异步加载成为可能。
|
||
|
||
## 使用现有主题
|
||
|
||
将以下其中一个主题合并到 App.xaml 中的资源字典中:
|
||
|
||
- 深色主题(如果未指定,则为默认主题):
|
||
```xml
|
||
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Dark.xaml" />
|
||
```
|
||
|
||
- 浅色主题:
|
||
```xml
|
||
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Light.xaml" />
|
||
```
|
||
|
||
- Nodify主题:
|
||
```xml
|
||
<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 />`。如果你启动应用程序,你会看到一个可以创建选择矩形的空白区域。
|
||
> 提示:将选择矩形拖动到编辑器区域的边缘附近,屏幕将自动向该方向移动。
|
||
|
||
### 添加节点(nodes)
|
||
|
||
现在我们将显示一些节点。让我们创建视图模型并将它们绑定到视图。
|
||
|
||
```csharp
|
||
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 中也能实现相同的效果)
|
||
|
||
```xml
|
||
<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。要了解更多节点类型和自定义,请查看[节点概述](节点概述)。
|
||
|
||
### 连接节点(nodes)
|
||
|
||
好的,现在让我们添加更多节点并将它们连接起来。首先,我们需要一个连接器的表示以及节点上一些集合来存储我们的连接器。
|
||
|
||
```csharp
|
||
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` 作为视图,但也有[其他连接器](连接器概述)。或者根据需要创建自己的连接器。)
|
||
|
||
```xml
|
||
<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` 中添加连接列表。
|
||
|
||
```csharp
|
||
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。
|
||
|
||
```csharp
|
||
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` 更新。
|
||
|
||
```xml
|
||
<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`。有关更多自定义,请参阅[连接概述](连接概述)。
|
||
|
||
```xml
|
||
<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` 以便在构造时自动连接它们。
|
||
|
||
```csharp
|
||
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; }
|
||
}
|
||
```
|
||
|
||
并且不要忘记在连接器模板中绑定它。
|
||
|
||
```xml
|
||
IsConnected="{Binding IsConnected}"
|
||
```
|
||
|
||
### 将预备连接转换为实际连接。
|
||
|
||
[预备连接](连接概述#预备连接)从一个 `Source` 开始,当放置到一个 `Target` 上时将完成。源*始终*是一个连接器,目标可以是一个[连接器](连接器概述)、一个[项目容器](项目容器概述)或 `null`。我们现在只关心其他连接器。当连接开始时,执行 `StartedCommand`,该命令接收 `Source` 作为参数。当连接完成时,执行 `CompletedCommand`,该命令接收 `Target` 作为参数。
|
||
|
||
让我们实现预备连接的视图模型,并将其添加到 `EditorViewModel` 中。
|
||
|
||
```csharp
|
||
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));
|
||
}
|
||
}
|
||
```
|
||
|
||
并将其绑定到视图上
|
||
|
||
```xml
|
||
<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`。
|
||
|
||
```csharp
|
||
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);
|
||
});
|
||
|
||
...
|
||
}
|
||
}
|
||
```
|
||
|
||
现在我们将此命令绑定到编辑器试图上。
|
||
|
||
```xml
|
||
<nodify:NodifyEditor ItemsSource="{Binding Nodes}"
|
||
Connections="{Binding Connections}"
|
||
PendingConnection="{Binding PendingConnection}"
|
||
DisconnectConnectorCommand="{Binding DisconnectConnectorCommand}">
|
||
...
|
||
|
||
</nodify:NodifyEditor>
|
||
```
|
||
|
||
### 控制节点位置
|
||
|
||
如你所见,节点总是在屏幕的左上角。这是因为它们在图中的位置是 (0, 0)。让我们来改变这一点!
|
||
|
||
在 `NodeViewModel` 中添加一个 `Location` 属性,类型为 `System.Windows.Point`,并触发 `PropertyChanged` 事件。
|
||
|
||
```csharp
|
||
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;
|
||
|
||
...
|
||
}
|
||
```
|
||
|
||
并将其绑定到视图
|
||
|
||
```xml
|
||
<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` 中选择的主题提供的静态资源。
|
||
|
||
```xml
|
||
<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>
|
||
```
|
||
|
||
> 提示:右键单击并拖动屏幕以移动视图,使用鼠标滚轮放大和缩小。 |