Add project files.

This commit is contained in:
Ankitkumar Satapara
2026-04-17 22:31:58 +05:30
commit 21aaef6776
473 changed files with 50152 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
| [English](https://github.com/miroiu/nodify/wiki/Home) | [简体中文](https://github.com/miroiu/nodify/wiki/主页) |
| ----------------------------------------------------- | ------------------------------------------------------ |
---
## [主页](主页)
[开始](开始)
- [层次结构和术语](开始#层次结构和术语)
- [内容层](开始#内容层)
- [使用现有主题](开始#使用现有主题)
- [一个小案例](开始#一个小案例)
- [绘制轴网](开始#绘制轴网)
[编辑器概述](编辑器概述)
- [移动视口](编辑器概述#平移)
- [缩放视口](编辑器概述#缩放)
- [选择项目](编辑器概述#选择)
- [对齐轴网](编辑器概述#对齐)
- [命令](编辑器概述#命令)
[项目容器概述](项目容器概述)
- [选择](项目容器概述#选择)
- [移动](项目容器概述#移动和拖拽)
[节点概述](节点概述)
- [节点](节点概述#1-node-控件)
- [组节点](节点概述#2-groupingnode-控件)
- [结节点](节点概述#3-knotnode-控件)
- [状态节点](节点概述#4-statenode-控件)
[连接概述](连接概述)
- [基本连接](连接概述#基本连接)
- [预备连接](连接概述#预备连接)
[连接器概述](连接器概述)
- [NodeInput 和 NodeOutput](连接器概述#NodeInput和NodeOutput)
[剪切线概述](剪切线概述)
- [启用预览](剪切线概述#启用预览)
- [自定义连接](剪切线概述#自定义连接)
- [自定义外观](剪切线概述#自定义外观)
[小地图概述](小地图概述)
- [移动视口](小地图概述#移动视口)
- [缩放](小地图概述#缩放)
- [自定义外观](小地图概述#自定义外观)
[API 参考](API-Reference)
[(问答) 常见问题](问答)

View File

@@ -0,0 +1,36 @@
![editor-interaction](https://user-images.githubusercontent.com/12727904/192004838-ec6dd997-5e64-4c01-940c-1cd1b8d27837.gif)
# 欢迎来到Nodify!
Nodify是一个WPF基于节点的[编辑器控件](编辑器概述),其中包含一系列[节点](节点概述)、[连接](连接概述)和[连接器](连接器概述)组件,旨在简化构建基于节点的工具的过程。
这是受虚幻引擎的[蓝图视觉脚本](https://docs.unrealengine.com/en-US/ProgrammingAndScripting/Blueprints/index.html)系统启发但仅专注于用户界面和用户交互部分。与蓝图不同Nodify是一个通用库提供了一个节点图编辑器组件可以嵌入到任何 WPF 应用程序中。
该图形编辑器是一个无限区域,您可以在其中放置和移动节点,选择和拖动节点组,连接和断开节点或连接器,放大和缩小,以及在将节点或导线拖动到边缘附近时自动移动屏幕等。
Nodify功能丰富经过优化可以同时与数百个节点进行交互并且...它是为与MVVM一起工作而重新架构的。
### 要求
![IDE](https://img.shields.io/static/v1?label=%20&message=VS%202019%20or%20greater&color=informational&style=for-the-badge&logo=visual-studio)
![C#](https://img.shields.io/static/v1?label=%20&message=8.0&color=239120&style=for-the-badge&logo=c-sharp)
![.NET](https://img.shields.io/static/v1?label=%20&message=Framework%204.7.2%20to%20NET%206&color=5C2D91&style=for-the-badge&logo=.net)
### 从Nuget安装Nodify
[![Download Package](https://img.shields.io/nuget/v/Nodify?label=Download&logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/Nodify/)
```xml
Install-Package Nodify
```
### 应用案例
- [Playground](https://github.com/miroiu/nodify/tree/master/Examples/Nodify.Playground)
- [State machine](https://github.com/miroiu/nodify/tree/master/Examples/Nodify.StateMachine)
- [Calculator](https://github.com/miroiu/nodify/tree/master/Examples/Nodify.Calculator)
[![IDE](https://img.shields.io/static/v1?label=%20&message=GET%20STARTED&color=9cf&style=for-the-badge)](开始)

View File

@@ -0,0 +1,63 @@
## 目录
- [启用预览](#启用预览)
- [自定义连接](#自定义连接)
- [自定义外观](#自定义外观)
`CuttingLine` 控件是一个自定义控件,用于移除与其相交的连接线。
默认的剪切启动手势是 `SHIFT+ALT+左键单击` ,可以通过设置 `EditorGestures.Editor.Cutting` 手势进行配置。剪切操作可以通过按下 `Escape` 键或右键取消,右键的配置同样可以通过设置 `EditorGestures.Editor.CancelAction` 手势进行配置。
`NodifyEditor` 中可用的相关命令有::
- CuttingStartedCommand
- CuttingCompletedCommand
- RemoveConnectionCommand - 当剪切操作完成时,对每个相交的连接线调用
### 启用预览
若希望连接线在与剪切线相交时改变样式,需要将 `NodifyEditor.EnableCuttingLinePreview` 设置为 `true`
> [!警告]
> 根据连接线数量及其几何复杂度,此操作可能对性能造成较大影响。
![cutting](https://github.com/user-attachments/assets/22f705c8-3bf1-466b-8bbd-da007f30deb2)
## 自定义连接
若希望剪切自定义连接线,需要将连接类型添加到 `NodifyEditor.CuttingConnectionTypes`:
```csharp
// 示例:将 Line 形状添加到可剪切的连接类型中
NodifyEditor.CuttingConnectionTypes.Add(typeof(System.Windows.Shapes.Line));
```
可以如下自定义与剪切线相交的自定义连接线样式:
```xml
<Style TargetType="Line">
<Style.Triggers>
<Trigger Property="nodify:CuttingLine.IsOverElement" Value="True">
<Setter Property="Opacity"
Value="0.4" />
</Trigger>
</Style.Triggers>
</Style>
```
## 自定义外观
可使用 `CuttingLineStyle` 自定义剪切线的样式:
```xml
<Style x:Key="CuttingLineStyle"
TargetType="{x:Type nodify:CuttingLine}"
BasedOn="{StaticResource {x:Type nodify:CuttingLine}}">
<Setter Property="StrokeDashArray"
Value="1 1" />
<Setter Property="StrokeThickness"
Value="2" />
</Style>
<nodify:NodifyEditor CuttingLineStyle="{StaticResource CuttingLineStyle}" ... />
```

View File

@@ -0,0 +1,79 @@
## 目录
- [目录](#目录)
- [移动视口](#移动视口)
- [缩放](#缩放)
- [自定义外观](#自定义外观)
`Minimap` 控件是一个自定义控件,旨在为 `NodifyEditor` 提供同步的缩略视图。
它继承自 `ItemsControl`,通过 `ItemsSource` 属性显示项目。每个项目都包装在 `MinimapItem` 容器中,需在 `ItemContainerStyle` 中设置:
> [!TIP]
> 若希望在小地图中实时移动节点,需要将 `NodifyEditor.EnableDraggingContainersOptimizations` 设置为 `false`。
此外,该控件还会显示一个表示编辑器可见区域的视口矩形,需要设置 `ViewportLocation``ViewportSize` 属性。
```xml
<nodify:Minimap
ItemsSource="{Binding ItemsSource, ElementName=Editor}"
ViewportLocation="{Binding ViewportLocation, ElementName=Editor}"
ViewportSize="{Binding ViewportSize, ElementName=Editor}"
Zoom="OnMinimapZoom">
<nodify:Minimap.ItemContainerStyle>
<Style TargetType="nodify:MinimapItem">
<Setter Property="Location" Value="{Binding MyItemLocation}" />
</Style>
</nodify:Minimap.ItemContainerStyle>
</nodify:Minimap>
```
```csharp
private void OnMinimapZoom(object sender, ZoomEventArgs e)
{
Editor.ZoomAtPosition(e.Zoom, e.Location);
}
```
> [!IMPORTANT]
> `Width` 和 `Height` 应由父容器限制,或在 `Minimap` 上设置为常量值,以防止其根据内容大小改变。
## 移动视口
按住鼠标点击并拖动即可移动视口(平移)。若将 `IsReadOnly` 属性设置为 `true`,则无法移动。
在平移过程中,`ViewportLocation` 会更新,因此需要进行双向绑定(默认即为双向绑定)。
平移手势可通过设置 `EditorGestures.Mappings.Minimap.DragViewport` 为所需手势进行配置。
## 缩放
滚动鼠标滚轮即可进行缩放。若将 `IsReadOnly` 属性设置为 `true` 或未处理 `Zoom` 事件,则无法缩放。
缩放时的修饰键可通过设置 `EditorGestures.Mappings.Minimap.ZoomModifierKey` 为所需值进行配置。
## 自定义外观
可使用 `ViewportStyle` 自定义视口矩形的样式,例如:
```xml
<Style x:Key="MyViewportStyle" TargetType="Rectangle">
<Setter Property="Fill" Value="Transparent"/>
<Setter Property="Stroke" Value="White"/>
<Setter Property="StrokeThickness" Value="3"/>
</Style>
<nodify:Minimap ViewportStyle="{StaticResource MyViewportStyle}" ... />
```
`MaxViewportOffset` 属性用于限制在 [移动视口](#移动视口) 时,视口可偏离项目的最大距离。
`ResizeToViewport` 属性用于改变小地图的缩放行为。
若设置为 `true`,则小地图会始终与项目一起缩放,以显示视口。
![scale-with-viewport](https://github.com/user-attachments/assets/7a8887bf-f3f4-44d7-8311-6d9ba7869d78)
如果设置为 `false`,则小地图仅显示项目,视口可以超出项目范围。
![viewport-out-of-bounds](https://github.com/user-attachments/assets/5d3b388e-8e40-4bfe-af3b-4c12fb47548d)

View File

@@ -0,0 +1,534 @@
了解组件的命名方式及其在编辑器可视化树中的角色对于理解代码和文档非常重要。
## 层次结构和术语
根组件是一个[编辑器editor](编辑器概述),它包含[节点nodes](节点概述)和[连接connections](连接概述)以及一些额外的UI元素如[选择框selection rectangle](编辑器概述#选择)和一个[预备连接pending connection](连接概述#预备连接),以使编辑器具有交互性。
节点是[连接器(connectors)](连接器概述)的容器,有时候节点本身也可以作为连接器(比如 [状态节点](节点概述#4-状态节点(state-node)控件)).
连接器可以创建预备连接,预备连接在完成后可以成为实际的连接。
_一图胜千言_
![nodes-hierarchy](https://user-images.githubusercontent.com/12727904/192028123-e2847f29-6517-4731-8672-f5d8356dead0.png)
## 内容层
你可能会好奇,一个节点如何既能作为连接器本身又能像普通节点一样运行。编辑器包含三个主要层次,这些层次有助于解决这个问题:
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>
```
> 提示:右键单击并拖动屏幕以移动视图,使用鼠标滚轮放大和缩小。

View File

@@ -0,0 +1,102 @@
## 目录
* [移动视口](#平移)
* [缩放](#缩放)
* [选择项目](#选择)
* [选择功能相关API](#选择功能相关API)
* [对齐到轴网](#对齐)
* [命令](#命令)
* [编辑器API](#编辑器API)
## 平移
平移是通过按住鼠标右键并移动鼠标来完成的,可以通过将```DisablePanning```依赖属性设置为```true```来禁用平移功能。
> 注意:可以通过设置```ViewportLocation```依赖属性以编程方式更改它。
在平移过程中,```IsPanning```依赖属性将被设置为```true```,并且```ViewportSize```, ```ViewportLocation``` 和 ```ViewportTransform``` 依赖属性将会更新。
默认情况下,自动平移也是启用的,可以通过将```DisableAutoPanning```依赖属性设置为```true```来禁用。其行为是在选择或拖动选区或待连接物体靠近视口边缘时平移视口。
可以使用```AutoPanSpeed```依赖属性更改自动平移速度,使用```AutoPanEdgeDistance```依赖属性更改触发平移的边缘距离。
平移功能还可以与选择和缩放结合使用,而自动平移不仅可以与选择和缩放结合使用,还可以与拖动选区或预备连接一起使用。
默认值:
* ```DisablePanning```: false
* ```DisableAutoPanning```: false
* ```AutoPanSpeed```: 10 pixels per tick
* ```AutoPanEdgeDistance```: 15 pixels
* ```AutoPanningTickRate```: 1 millisecond
## 缩放
缩放是通过使用鼠标滚轮或按```CTRL +```来放大或按```CTRL -```来缩小完成的,并且可以通过将```DisableZooming```依赖属性设置为```true```来禁用缩放功能。
> 注意:可以通过将```ViewportZoom```依赖属性设置为```MinViewportZoom```和```MaxViewportZoom```之间的值来以编程方式更改它。
在缩放过程中,```ViewportLocation```, ```ViewportSize``` 和 ```ViewportTransform``` 依赖属性将会更新。
缩放功能还可以与平移、拖动选区或预备连接结合使用。
默认值:
* ```ViewportZoom```: 1
* ```MinViewportZoom```: 0.1
* ```MaxViewportZoom```: 2
## 选择
选择项目是通过按住鼠标左键并移动鼠标来完成的。当选择操作正在进行时,```IsSelecting```依赖属性将被设置为```true```,并且```SelectedArea```依赖属性将随着每次移动而更新。
> 注意:也可以通过将集合绑定到```SelectedItems```依赖属性来以编程方式设置选定的项目。
如果启用了实时选择(```EnableRealtimeSelection```: true则在调整选择矩形大小时项目将被选中和取消选中。否则在选择操作完成后才会选中```SelectedArea```中包含的项目。
当选择一个```ItemContainer```时,其```IsSelected```依赖属性将被设置为```true```。
根据开始选择时按住的```ModifierKeys```不同,使用不同的行为:
- ```Replace``` - 无修饰键(默认行为,清除已选项目并开始新选择)
- ```Append``` - shift键将选择添加到当前已选项目
- ```Remove``` - alt键从当前已选项目中移除选择
- ```Invert``` - control键移除选定项目并添加未选中项目
选择项目也可以与平移和缩放结合使用。
默认值:
* ```EnableRealtimeSelection```: true
### 选择功能相关API:
以下方法可以在 NodifyEditor 实例上调用。
* SelectArea
* InvertSelection
* UnselectArea
## 对齐
当移动选中项目时,```GridCellSize```依赖属性会决定将选定项目对齐到何处。
对齐是相对于选定项目的位置而不是虚拟网格的位置。
如果选定项目在初始创建时未对齐到网格,或者在运行时更改了```GridCellSize```,那么当```EnableSnappingCorrection```依赖属性为```true```时,则在移动选中项目后将会校正最终位置。
默认值:
* ```GridCellSize```: 1
* ```EnableSnappingCorrection```: true
## 命令
在```EditorCommands```类中可以看到以下```RoutedUICommand```
* ```ZoomIn``` - ```CTRL +```(相对于视口中心放大)
* ```ZoomOut``` - ```CTRL -```(相对于视口中心缩小)
* ```SelectAll``` - ```CTRL A```(选择所有项目)
* ```BringIntoView``` - 将视口移动到指定位置,默认为[0,0]。(```CommandParameter```为类型为```Point```或```string```的位置)
* ```Align``` - 使用指定的对齐方法对齐选定的项目,默认为顶部。(```CommandParameter```为类型为```Alignment```或```string```的对齐方法。可能的对齐方法Top、Left、Bottom、Right、Middle、Center
* ```FitToScreen``` - 缩放并移动```Viewport```以显示尽可能多的项目
## 编辑器API
您可以在`NodifyEditor`实例上以编程方式调用这些命令的相应方法。
* FitToScreen
* BringIntoView
* ZoomAtPosition
* ZoomIn
* ZoomOut

View File

@@ -0,0 +1,117 @@
节点是节点编辑器的基本组件。它们被包装在[`ItemContainer`](项目容器概述)中并且可以是任何自定义控件。例如TextBlock
以下是库中包含的节点:
### 1. ```Node``` 控件
这种类型的节点支持```Input```和```Output```连接器,可以随意移动。
```xml
<nodify:NodifyEditor xmlns:sys="clr-namespace:System;assembly=mscorlib">
<nodify:NodifyEditor.ItemsSource>
<CompositeCollection>
<nodify:Node Header="My Node">
<nodify:Node.Input>
<CompositeCollection>
<sys:String>In 0</sys:String>
<sys:String>In 1</sys:String>
</CompositeCollection>
</nodify:Node.Input>
<nodify:Node.Output>
<CompositeCollection>
<sys:String>Out 0</sys:String>
<sys:String>Out 1</sys:String>
</CompositeCollection>
</nodify:Node.Output>
<nodify:Node.InputConnectorTemplate>
<DataTemplate>
<nodify:NodeInput Header="{Binding}" />
</DataTemplate>
</nodify:Node.InputConnectorTemplate>
<nodify:Node.OutputConnectorTemplate>
<DataTemplate>
<nodify:NodeOutput Header="{Binding}" />
</DataTemplate>
</nodify:Node.OutputConnectorTemplate>
</nodify:Node>
</CompositeCollection>
</nodify:NodifyEditor.ItemsSource>
</nodify:NodifyEditor>
```
节点的`Header`可以使用`HeaderTemplate`进行自定义。同样,节点的`Footer`可以使用`FooterTemplate`进行自定义。
`Input`集合中的每个项目可以使用`InputConnectorTemplate`进行自定义。同样,`Output`可以使用`OutputConnectorTemplate`进行自定义。
![Node](https://i.imgur.com/VwAYlX3.gif)
### 2. ```GroupingNode``` 控件
这种类型的节点可以调整大小,如果通过```Header```拖动,它将移动其内部的节点。
如果按住```SwitchMovementModeModifierKey```(默认情况下为**Shift**键),它将移动而不会移动其子节点。
```xml
<nodify:NodifyEditor>
<nodify:NodifyEditor.ItemsSource>
<CompositeCollection>
<nodify:GroupingNode Header="Grouping node"
Width="300"
Height="250" />
<nodify:Node Header="My node" />
<nodify:Node Header="My other node" />
</CompositeCollection>
</nodify:NodifyEditor.ItemsSource>
</nodify:NodifyEditor>
```
节点的`Header`可以使用`HeaderTemplate`进行自定义。同样,节点的`Content`可以使用`ContentTemplate`进行自定义。
节点的大小可以通过更改`ActualSize`依赖属性的值来编程设置。
#### 默认值
* CanResize: true
* MovementMode: Grouped
#### 命令
* ResizeCompleted
* ResizeStarted
![Grouping Node](https://i.imgur.com/HYxt2cs.gif)
### 3. ```KnotNode``` 控件
这种类型的控件可以用于重新排布reroute```Connection```,因为它只支持一个```Connector```。
节点的`Content`可以使用`ContentTemplate`进行自定义。
```xml
<nodify:NodifyEditor>
<nodify:NodifyEditor.ItemsSource>
<CompositeCollection>
<nodify:KnotNode />
</CompositeCollection>
</nodify:NodifyEditor.ItemsSource>
</nodify:NodifyEditor>
```
![Knot Node](https://i.imgur.com/fMrEqY1.gif)
### 4. ```StateNode``` 控件
这种类型的节点本身就是一个```Connector```,这意味着它将在交互时引发```PendingConnection```事件。由于它继承自`Connector`,你需要将`Anchor`属性和`IsConnected`绑定到相应的状态。(如果`IsConnected`设置为false连接将不会更新
```xml
<nodify:NodifyEditor>
<nodify:NodifyEditor.ItemsSource>
<CompositeCollection>
<nodify:StateNode Content="My node" />
</CompositeCollection>
</nodify:NodifyEditor.ItemsSource>
</nodify:NodifyEditor>
```
节点的`Content`可以使用`ContentTemplate`进行自定义。
![State Node](https://i.imgur.com/FrI2epL.gif)

View File

@@ -0,0 +1,9 @@
连接器通过引发`PendingConnectionStartedEvent`事件来创建预备连接。连接器具有一个**必须**绑定的`Anchor`依赖属性,以便在节点位置更改时实时更新它们之间的连接。`IsConnected`依赖属性**必须**设置为true以接收`Anchor`更新。
按住`ALT`并点击连接器会触发`DisconnectCommand`
## NodeInput 和 NodeOutput
NodeInput和NodeOutput是带有`Header``Connector`的实现,可以通过自定义`HeaderTemplate`来显示文本或输入框。它们还公开了一个`ConnectorTemplate`来自定义连接器本身。通常,它们与`Node.InputConnectorTemplate``Node.OutputConnectorTemplate`一起使用。
![image](https://user-images.githubusercontent.com/12727904/192117525-a7e1b309-70d6-4ed7-bcd7-8210dbd680ce.png)

View File

@@ -0,0 +1,72 @@
连接是由两个点之间创建的。`Source``Target`依赖属性是`Point`类型,通常绑定到连接器的`Anchor`点。
## 基本连接
库中所有连接的基类是`BaseConnection`,它派生自`Shape`。在创建自定义连接时,可以不受任何限值地从`BaseConnection`派生。
它公开了两个命令及其对应的事件:
- `DisconnectCommand``DisconnectEvent` - 当按住`ALT`点击连接时触发
- `SplitCommand``SplitEvent` - 当双击连接时触发
连接的`Direction`有两种值:
- `Forward`
![image](https://user-images.githubusercontent.com/12727904/192101918-af9b0da6-ecc8-48f7-bf4d-8f9fdd005153.png)
![image](https://user-images.githubusercontent.com/12727904/192101959-2cb9a837-1642-4e96-b2ef-eea5502a587f.png)
- `Backward`
![image](https://user-images.githubusercontent.com/12727904/192101941-a00e23db-07ae-49ac-a907-72e35ef67877.png)
![image](https://user-images.githubusercontent.com/12727904/192101977-1afd69f1-dab0-478e-9c3d-7d601486c289.png)
`SourceOffset``TargetOffset``OffsetMode`一起使用,会控制与锚点的距离:
![image](https://user-images.githubusercontent.com/12727904/192102096-b20887d5-b7ba-450f-9cf3-7fa4086d9637.png)
连接还有一个`Spacing`属性,会使连接在一段距离后转折到沿着从`Source``Target`点的方向:
- 有间距:
![image](https://user-images.githubusercontent.com/12727904/192102286-9a79da8e-5e87-4f60-9e82-979bfabcd6f3.png)
- 无间距:
![image](https://user-images.githubusercontent.com/12727904/192102302-4125b44a-dfad-4d9e-9131-efb7c17cefbe.png)
`ArrowSize`设置为"0, 0"会移除箭头。
## 线连接
一条从`Source``Target`的直线。
![image](https://user-images.githubusercontent.com/12727904/192115137-d8d2145b-a769-4ee9-b4e0-8a362c94e9e7.png)
## 电路连接
有一个`Angle`依赖属性来控制转折的位置。角度以度为单位。
![image](https://user-images.githubusercontent.com/12727904/192115226-b0e515b4-5a21-46aa-956a-401f07b7d308.png)
## 曲线连接
`Source``Target`之间的贝塞尔曲线。
![image](https://user-images.githubusercontent.com/12727904/192115259-2fe56a68-b3e4-4f5d-aa5c-5ab83e84a84d.png)
## 预备连接
可以从连接器创建预备连接,并可以放置在`ItemContainer``Connector`上(如果`AllowOnlyConnectors`为false
预备连接的`Content`可以使用`ContentTemplate`进行自定义。如果`EnablePreview`为true`PreviewTarget`将更新为鼠标光标下的连接器或项目容器,或者为`null`(如果没有这样的元素)。
![image](https://user-images.githubusercontent.com/12727904/192115698-fbe29101-884f-4cec-9c25-e318701d30b1.png)
预备连接的可见性可以使用`IsVisible`依赖属性进行控制。
连接器的连接捕捉可以使用`EnableSnapping`依赖属性启用。
`Source``Target`属性是连接器的数据上下文,预备连接完成时`Target`将更新。
还有一个`StartedCommand`,参数是`Source`,以及一个`CompletedCommand`,参数是`Target`
> 提示:取消预备连接的方法是释放右键。

View File

@@ -0,0 +1,54 @@
#### 问:为什么我移动节点时连接没有跟随连接器?
答1您没有绑定连接器的`Anchor`属性,或者连接的`Source``Target`属性。
答2仅当连接器的`IsConnected`属性设置为true时`Anchor`才会更新。
***
#### 问:我可以更改鼠标/键盘绑定吗?
答:可以!您可以根据喜好配置[编辑器手势](https://github.com/miroiu/nodify/blob/master/Nodify/EditorGestures.cs)。
***
#### 问有Avalonia移植版本吗
有一个非常棒的Avalonia移植版本您可以在[这里](https://github.com/BAndysc/nodify-avalonia)查看。非常感谢[BAndysc](https://github.com/BAndysc)使这一切成为可能。
***
#### 问有没有非MVVM的方法添加节点和连接
答:没有。
***
#### 问:如何设置选中的节点始终在顶部?
答:[链接](https://github.com/miroiu/nodify/issues/57)
***
#### 问:如何更改节点的大小?
答:[链接](https://github.com/miroiu/nodify/issues/55)
***
#### 问如何限制X和Y方向的平移
答:[链接](https://github.com/miroiu/nodify/issues/53)
***
#### 问:视口滞后。如何修复?
答:[链接](https://github.com/miroiu/nodify/issues/60)
***
#### 问:如何将节点从工具箱拖入编辑器?
答:[链接](https://github.com/miroiu/nodify/issues/81)

View File

@@ -0,0 +1,53 @@
```项目容器ItemContainer``` 是编辑器中**最重要**的部分。它是一个内容控件,用于包装由 ```编辑器NodifyEditor``` 的 ```ItemsSource``` 生成的每个控件,并在图形坐标中具有 `Location`属性。
## 目录
- [选择](#选择)
- [API](#选择-api)
- [移动](#移动和拖拽)
## 选择
选择一个 ```ItemContainer``` 通过在容器的边界框上释放左键来完成。边界框是通过 ```DesiredSizeForSelection``` 依赖属性计算的(如果指定),否则使用 ```RenderSize``` 计算。
> 注意:可以通过将 ```IsSelected``` 依赖属性设置为 ```true``` 来编程选择容器。
如果左键没有释放并且鼠标移动,容器也会被选择,这将清除当前选择并在选定的容器上开始拖拽操作。
还可以通过在容器上释放右键来选择,右键点击会清除当前选择并选择目标容器。
根据选择容器时按下的 ```ModifierKeys``` 不同,会有不同的行为:
- ```Replace``` - 没有按下修饰键(默认行为,清除选定项并选择容器)
- ```Append``` - 按下 Shift 键(将容器添加到选定项)
- ```Invert``` - 按下 Ctrl 键(切换容器的 ```IsSelected``` 依赖属性)
只有在 ```IsSelectable``` 依赖属性为 ```true``` 时,```ItemContainer``` 才能被选择。
选择或取消选择容器会分别触发 ```Selected``` 和 ```Unselected``` 事件,并将 ```IsSelected``` 依赖属性设置为新值。
如果未重写默认样式,容器的边框将使用 ```SelectedBrush``` 依赖属性。
默认值:
- ```IsSelectable```: true
- ```IsSelected```: false
### 选择 API
* IsSelectableInArea
## 移动和拖拽
可以通过按住左键并移动鼠标来开始拖拽 ```ItemContainer``` 的操作。
只有在 ```IsDraggable``` 依赖属性设置为 ```true``` 时,```ItemContainer``` 才能被拖拽。
> 注意:可以通过设置它们的 ```Location``` 依赖属性来编程移动 ```ItemContainer```。
拖拽 ```ItemContainer``` 会触发一系列事件,这些事件由 ```NodifyEditor``` 处理并应用于所有选定的项:
- ```DragStarted``` - 将 ```IsPreviewingLocation``` 依赖属性设置为 ```true```;
- ```DragDelta``` - 在拖拽操作完成或取消之前持续触发,并将所有选定项移动;
- ```DragCompleted``` - 完成或取消拖拽操作,并将 ```Location``` 依赖属性设置为最终位置,```IsPreviewingLocation``` 设置为 ```false```。
> 注意:如果 ```AllowDraggingCancellation``` 设置为 ```true```,可以通过释放右键取消拖拽操作。
默认值:
- ```IsDraggable```: true
- ```AllowDraggingCancellation```: true