diff --git a/NativeWpf/README.md b/NativeWpf/README.md new file mode 100644 index 0000000..79ee3fc --- /dev/null +++ b/NativeWpf/README.md @@ -0,0 +1,28 @@ +# Native WPF implementation + +This directory is self-contained. It adds a native WPF implementation without changing the repository's existing WinForms library, WinForms demo, legacy WPF host demo, or original solution. + +## Projects + +- `ST.Library.UI.WPF`: dependency-free native WPF controls rendered through the original `System.Drawing.Graphics` contract and presented by a DPI-aware WPF `WriteableBitmap`. +- `WpfNodeEditorDemo`: full-window node canvas with context menus, automatic layout, save/open actions, an execution command, and a node-anchored reflection property panel provided by the `ColorVision.UI` 1.5.7 NuGet package. +- `STNodeEditor.Wpf.sln`: standalone solution for the two projects above. + +The control library has no NuGet dependency and does not reference `System.Windows.Forms`, `WindowsFormsIntegration`, or a third-party rendering package. The demo uses a native WPF visual tree with no `WindowsFormsHost`; only the demo references `ColorVision.UI` for its reflection-based property editors and theme resources. + +Node creation is available from the integrated `+` button and the canvas context menu, so the demo does not reserve space for a node tree. Selecting one node opens its reflected `STNodePropertyAttribute` properties beside the node while leaving the rest of the window available to the canvas. + +The demo uses direct cursor-centered wheel zoom in `0.05` steps across the editor's full `0.2` to `5.0` scale range. The canvas lock button controls blank-area left-drag explicitly: unlocked pans the canvas, locked draws a selection rectangle, and middle-button drag always pans. Manual lock state is not changed by clicking a node. + +The demo keeps the historical `WpfNodeEdittorDemo` assembly name so existing STND files retain the same node module identity; only the project folder and UI namespace use the corrected spelling. + +## Run + +Open `STNodeEditor.Wpf.sln`, select `WpfNodeEditorDemo`, and press F5. From PowerShell, the equivalent commands are: + +```powershell +dotnet build .\STNodeEditor.Wpf.sln +dotnet run --project .\WpfNodeEditorDemo\WpfNodeEditorDemo.csproj +``` + +The WPF library targets both `net8.0-windows` and `net10.0-windows`; the demo targets `net8.0-windows` and runs as x64 because the published `ColorVision.UI` assemblies are AMD64. This does not alter the target framework or source of any existing project. diff --git a/NativeWpf/ST.Library.UI.WPF/Lang.cs b/NativeWpf/ST.Library.UI.WPF/Lang.cs new file mode 100644 index 0000000..b5441d9 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/Lang.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Resources; + +namespace ST.Library.UI; + +public static class Lang +{ + private static readonly List _externalManagers = new(); + + public static void RegisterResourceManager(ResourceManager manager) + { + if (manager != null && !_externalManagers.Contains(manager)) + { + _externalManagers.Add(manager); + } + } + + public static string Get(string key) + { + return GetOrDefault(key); + } + + public static string GetOrDefault(string key) + { + for (int i = _externalManagers.Count - 1; i >= 0; i--) + { + try + { + string value = _externalManagers[i].GetString(key); + if (value != null) return value; + } + catch { } + } + + return key; + } + +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/AlertLocation.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/AlertLocation.cs new file mode 100644 index 0000000..5d4d50f --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/AlertLocation.cs @@ -0,0 +1,14 @@ +namespace ST.Library.UI.NodeEditor; + +public enum AlertLocation +{ + Left, + Top, + Right, + Bottom, + Center, + LeftTop, + RightTop, + RightBottom, + LeftBottom +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/CanvasMoveArgs.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/CanvasMoveArgs.cs new file mode 100644 index 0000000..966463c --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/CanvasMoveArgs.cs @@ -0,0 +1,8 @@ +namespace ST.Library.UI.NodeEditor; + +public enum CanvasMoveArgs +{ + Left = 1, + Top = 2, + All = 4 +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/ConnectionInfo.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/ConnectionInfo.cs new file mode 100644 index 0000000..21a7351 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/ConnectionInfo.cs @@ -0,0 +1,8 @@ +namespace ST.Library.UI.NodeEditor; + +public struct ConnectionInfo +{ + public STNodeOption Input; + + public STNodeOption Output; +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/ConnectionStatus.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/ConnectionStatus.cs new file mode 100644 index 0000000..d3d729a --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/ConnectionStatus.cs @@ -0,0 +1,35 @@ +using System.ComponentModel; + +namespace ST.Library.UI.NodeEditor; + +public enum ConnectionStatus +{ + [Description("不存在所有者")] + NoOwner, + [Description("相同的所有者")] + SameOwner, + [Description("均为输入或者输出选项")] + SameInputOrOutput, + [Description("不同的数据类型")] + ErrorType, + [Description("单连接节点")] + SingleOption, + [Description("出现环形路径")] + Loop, + [Description("已存在的连接")] + Exists, + [Description("空白选项")] + EmptyOption, + [Description("已经连接")] + Connected, + [Description("连接被断开")] + DisConnected, + [Description("节点被锁定")] + Locked, + [Description("操作被拒绝")] + Reject, + [Description("正在被连接")] + Connecting, + [Description("正在断开连接")] + DisConnecting +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/DrawingTools.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/DrawingTools.cs new file mode 100644 index 0000000..f928be5 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/DrawingTools.cs @@ -0,0 +1,12 @@ +using System.Drawing; + +namespace ST.Library.UI.NodeEditor; + +public struct DrawingTools +{ + public Graphics Graphics; + + public Pen Pen; + + public SolidBrush SolidBrush; +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/NodeFindInfo.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/NodeFindInfo.cs new file mode 100644 index 0000000..c6a7412 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/NodeFindInfo.cs @@ -0,0 +1,12 @@ +namespace ST.Library.UI.NodeEditor; + +public struct NodeFindInfo +{ + public STNode Node; + + public STNodeOption NodeOption; + + public string Mark; + + public string[] MarkLines; +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNode.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNode.cs new file mode 100644 index 0000000..58b2582 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNode.cs @@ -0,0 +1,1705 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Windows.Input; + +namespace ST.Library.UI.NodeEditor; + +public abstract class STNode : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler PropertyChanged; + + private STNodeEditor _Owner; + + private bool _IsSelected; + + private bool _IsActive; + + private Color _TitleColor; + + private Color _TitleProgressColor; + + private float _TitleProgress = -1f; + + private Color _MarkColor; + + private Color _ForeColor = Color.White; + + private Color _BackColor; + + private string _Title; + + private string _Mark; + + private string[] _MarkLines; + + private int _Left; + + private int _Top; + + private int _Width = 100; + + private int _Height = 40; + + private int _ItemHeight = 20; + + private bool _AutoSize = true; + + private Rectangle _MarkRectangle; + + private int _TitleHeight = 22; + + private STNodeOptionCollection _InputOptions; + + private STNodeOptionCollection _OutputOptions; + + private STNodeControlCollection _Controls; + + private Font _Font; + + private bool _ShowControls = true; + + private bool _LockOption; + + private bool _LockLocation; + + private object _Tag; + + private Guid _Guid; + + private bool _LetGetOptions; + + private int m_create_state; + + private static Point m_static_pt_init = new Point(10, 10); + + protected StringFormat m_sf; + + protected STNodeControl m_ctrl_active; + + protected STNodeControl m_ctrl_hover; + + protected STNodeControl m_ctrl_down; + + [Browsable(false)] + public STNodeEditor Owner + { + get + { + return _Owner; + } + internal set + { + if (value == _Owner) + { + return; + } + if (_Owner != null) + { + STNodeOption[] array = _InputOptions.ToArray(); + foreach (STNodeOption sTNodeOption in array) + { + sTNodeOption.DisConnectionAll(); + } + STNodeOption[] array2 = _OutputOptions.ToArray(); + foreach (STNodeOption sTNodeOption2 in array2) + { + sTNodeOption2.DisConnectionAll(); + } + } + _Owner = value; + if (!_AutoSize) + { + SetOptionsLocation(); + } + BuildSize(bBuildNode: true, bBuildMark: true, bRedraw: false); + OnOwnerChanged(); + } + } + [Browsable(false)] + public bool IsSelected + { + get + { + return _IsSelected; + } + set + { + if (value != _IsSelected) + { + _IsSelected = value; + Invalidate(); + OnSelectedChanged(); + if (_Owner != null) + { + _Owner.OnSelectedChanged(EventArgs.Empty); + } + } + } + } + [Browsable(false)] + public bool IsActive + { + get + { + return _IsActive; + } + internal set + { + if (value != _IsActive) + { + _IsActive = value; + OnActiveChanged(); + } + } + } + + public Color TitleColor + { + get + { + return _TitleColor; + } + set + { + _TitleColor = value; + Invalidate(new Rectangle(0, 0, _Width, _TitleHeight)); + } + } + + [Browsable(false)] + public Color TitleProgressColor + { + get + { + return _TitleProgressColor; + } + set + { + _TitleProgressColor = value; + Invalidate(new Rectangle(0, 0, _Width, _TitleHeight)); + } + } + + [Browsable(false)] + public float TitleProgress + { + get + { + return _TitleProgress; + } + set + { + float progress = float.IsNaN(value) || value < 0f ? -1f : (value > 1f ? 1f : value); + if (Math.Abs(_TitleProgress - progress) < 0.0001f) + { + return; + } + _TitleProgress = progress; + Invalidate(new Rectangle(0, 0, _Width, _TitleHeight)); + } + } + + public Color MarkColor + { + get + { + return _MarkColor; + } + set + { + _MarkColor = value; + Invalidate(_MarkRectangle); + } + } + + public Color ForeColor + { + get + { + return _ForeColor; + } + protected set + { + _ForeColor = value; + Invalidate(); + } + } + + public Color BackColor + { + get + { + return _BackColor; + } + protected set + { + _BackColor = value; + Invalidate(); + } + } + + [STNodeProperty("Title", "Title", true)] + [Browsable(true)] + public string Title + { + get + { + return _Title; + } + set + { + _Title = value; + if (_AutoSize) + { + BuildSize(bBuildNode: true, bBuildMark: true, bRedraw: true); + } + OnPropertyChanged(); + } + } + + protected void OnPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public string Mark + { + get + { + return _Mark; + } + set + { + _Mark = value; + if (value == null) + { + _MarkLines = null; + } + else + { + _MarkLines = (from s in value.Split('\n') + select s.Trim()).ToArray(); + } + Invalidate(new Rectangle(-5, -5, _MarkRectangle.Width + 10, _MarkRectangle.Height + 10)); + } + } + + public string[] MarkLines => _MarkLines; + [Browsable(false)] + public int Left + { + get + { + return _Left; + } + set + { + if (!_LockLocation && value != _Left) + { + Point oldLocation = new Point(_Left, _Top); + _Left = value; + SetOptionsLocation(); + BuildSize(bBuildNode: false, bBuildMark: true, bRedraw: false); + OnMove(EventArgs.Empty); + if (_Owner != null) + { + _Owner.BuildLinePath(); + _Owner.BuildBounds(); + _Owner.OnNodeLocationChanged(this, oldLocation, new Point(_Left, _Top)); + } + } + } + } + [Browsable(false)] + public int Top + { + get + { + return _Top; + } + set + { + if (!_LockLocation && value != _Top) + { + Point oldLocation = new Point(_Left, _Top); + _Top = value; + SetOptionsLocation(); + BuildSize(bBuildNode: false, bBuildMark: true, bRedraw: false); + OnMove(EventArgs.Empty); + if (_Owner != null) + { + _Owner.BuildLinePath(); + _Owner.BuildBounds(); + _Owner.OnNodeLocationChanged(this, oldLocation, new Point(_Left, _Top)); + } + } + } + } + + public int Width + { + get + { + return _Width; + } + protected set + { + if (value >= 50 && !_AutoSize && value != _Width) + { + _Width = value; + SetOptionsLocation(); + BuildSize(bBuildNode: false, bBuildMark: true, bRedraw: false); + OnResize(EventArgs.Empty); + if (_Owner != null) + { + _Owner.BuildLinePath(); + _Owner.BuildBounds(); + } + Invalidate(); + } + } + } + + public int Height + { + get + { + return _Height; + } + protected set + { + if (value >= 40 && !_AutoSize && value != _Height) + { + _Height = value; + SetOptionsLocation(); + BuildSize(bBuildNode: false, bBuildMark: true, bRedraw: false); + OnResize(EventArgs.Empty); + if (_Owner != null) + { + _Owner.BuildLinePath(); + _Owner.BuildBounds(); + } + Invalidate(); + } + } + } + + public int ItemHeight + { + get + { + return _ItemHeight; + } + protected set + { + if (value < 16) + { + value = 16; + } + if (value > 200) + { + value = 200; + } + if (value == _ItemHeight) + { + return; + } + _ItemHeight = value; + if (_AutoSize) + { + BuildSize(bBuildNode: true, bBuildMark: false, bRedraw: true); + return; + } + SetOptionsLocation(); + if (_Owner != null) + { + _Owner.Invalidate(); + } + } + } + + public bool AutoSize + { + get + { + return _AutoSize; + } + protected set + { + _AutoSize = value; + } + } + + public int Right => _Left + _Width; + + public int Bottom => _Top + _Height; + + public Rectangle Rectangle => new Rectangle(_Left, _Top, _Width, _Height); + + public Rectangle TitleRectangle => new Rectangle(_Left, _Top, _Width, _TitleHeight); + + public Rectangle MarkRectangle => _MarkRectangle; + + public int TitleHeight + { + get + { + return _TitleHeight; + } + protected set + { + _TitleHeight = value; + } + } + + protected internal STNodeOptionCollection InputOptions => _InputOptions; + + public int InputOptionsCount => _InputOptions.Count; + + protected internal STNodeOptionCollection OutputOptions => _OutputOptions; + + public int OutputOptionsCount => _OutputOptions.Count; + + protected STNodeControlCollection Controls => _Controls; + + public int ControlsCount => _Controls.Count; + + protected bool ShowControls + { + get + { + return _ShowControls; + } + set + { + if (_ShowControls == value) + { + return; + } + _ShowControls = value; + if (!_ShowControls) + { + m_ctrl_active = null; + m_ctrl_hover = null; + m_ctrl_down = null; + } + Invalidate(); + } + } + + public Point Location + { + get + { + return new Point(_Left, _Top); + } + set + { + Left = value.X; + Top = value.Y; + } + } + + public Size Size + { + get + { + return new Size(_Width, _Height); + } + set + { + Width = value.Width; + Height = value.Height; + } + } + + public void SetFixedWidth(int width) + { + SetFixedSize(width, _Height); + } + + public void SetAutoSize(bool autoSize) + { + if (_AutoSize == autoSize) + { + if (_AutoSize) + { + BuildSize(bBuildNode: true, bBuildMark: true, bRedraw: false); + } + return; + } + _AutoSize = autoSize; + if (_AutoSize) + { + BuildSize(bBuildNode: true, bBuildMark: true, bRedraw: false); + } + else + { + SetOptionsLocation(); + BuildSize(bBuildNode: false, bBuildMark: true, bRedraw: false); + OnResize(EventArgs.Empty); + } + if (_Owner != null) + { + _Owner.BuildLinePath(); + _Owner.BuildBounds(); + } + Invalidate(); + } + + public void SetFixedSize(int width, int height) + { + if (width < 50) + { + width = 50; + } + if (height < 40) + { + height = 40; + } + if (!_AutoSize && width == _Width && height == _Height) + { + return; + } + _AutoSize = false; + _Width = width; + _Height = height; + SetOptionsLocation(); + BuildSize(bBuildNode: false, bBuildMark: true, bRedraw: false); + OnResize(EventArgs.Empty); + if (_Owner != null) + { + _Owner.BuildLinePath(); + _Owner.BuildBounds(); + } + Invalidate(); + } + + protected Font Font + { + get + { + return _Font; + } + set + { + if (value != _Font) + { + _Font.Dispose(); + _Font = value; + } + } + } + + [Browsable(false)] + public bool LockOption + { + get + { + return _LockOption; + } + set + { + _LockOption = value; + Invalidate(new Rectangle(0, 0, _Width, _TitleHeight)); + } + } + [Browsable(false)] + public bool LockLocation + { + get + { + return _LockLocation; + } + set + { + _LockLocation = value; + Invalidate(new Rectangle(0, 0, _Width, _TitleHeight)); + } + } + + public object Tag + { + get + { + return _Tag; + } + set + { + _Tag = value; + } + } + + public Guid Guid => _Guid; + + internal void RegenerateGuid() + { + Guid oldGuid = _Guid; + _Guid = Guid.NewGuid(); + OnGuidRegenerated(oldGuid, _Guid); + } + + protected virtual void OnGuidRegenerated(Guid oldGuid, Guid newGuid) + { + } + + public bool LetGetOptions + { + get + { + return _LetGetOptions; + } + protected set + { + _LetGetOptions = value; + } + } + + /// + /// Gets whether the node creation lifecycle has completed. + /// + [Browsable(false)] + public bool IsCreated => Volatile.Read(ref m_create_state) == 2; + + public STNode() + { + _Title = "Untitled"; + _MarkRectangle.Height = _Height; + _Left = (_MarkRectangle.X = m_static_pt_init.X); + _Top = m_static_pt_init.Y; + _MarkRectangle.Y = _Top - 30; + _InputOptions = new STNodeOptionCollection(this, isInput: true); + _OutputOptions = new STNodeOptionCollection(this, isInput: false); + _Controls = new STNodeControlCollection(this); + _BackColor = Color.FromArgb(200, 64, 64, 64); + _TitleColor = Color.FromArgb(200, Color.DodgerBlue); + _TitleProgressColor = Color.FromArgb(230, Color.DeepSkyBlue); + _MarkColor = Color.FromArgb(200, Color.Brown); + _Font = new Font("courier new", 8.25f); + m_sf = new StringFormat(); + m_sf.Alignment = StringAlignment.Near; + m_sf.LineAlignment = StringAlignment.Center; + m_sf.FormatFlags = StringFormatFlags.NoWrap; + m_sf.SetTabStops(0f, new float[1] { 40f }); + m_static_pt_init.X += 10; + m_static_pt_init.Y += 10; + _Guid = Guid.NewGuid(); + Create(); + } + + protected internal void BuildSize(bool bBuildNode, bool bBuildMark, bool bRedraw) + { + STNodeEditor owner = _Owner; + if (owner == null || owner.IsDisposed) + { + return; + } + if (!owner.Dispatcher.CheckAccess()) + { + // Runtime nodes update option captions from thread-pool callbacks. + // WPF layout and its shared measurement bitmap belong to the editor + // dispatcher, so defer only the visual work and let execution continue. + owner.BeginInvoke(new Action(() => + { + if (ReferenceEquals(_Owner, owner)) + { + BuildSize(bBuildNode, bBuildMark, bRedraw); + } + })); + return; + } + if (_AutoSize && bBuildNode) + { + Size defaultNodeSize = GetDefaultNodeSize(); + if (_Width != defaultNodeSize.Width || _Height != defaultNodeSize.Height) + { + _Width = defaultNodeSize.Width; + _Height = defaultNodeSize.Height; + SetOptionsLocation(); + OnResize(EventArgs.Empty); + } + } + if (bBuildMark && !string.IsNullOrEmpty(_Mark)) + { + _MarkRectangle = OnBuildMarkRectangle(); + } + if (bRedraw) + { + owner.Invalidate(); + } + } + + internal Dictionary OnSaveNode() + { + Dictionary dictionary = new Dictionary(); + dictionary.Add("Guid", _Guid.ToByteArray()); + dictionary.Add("Left", BitConverter.GetBytes(_Left)); + dictionary.Add("Top", BitConverter.GetBytes(_Top)); + dictionary.Add("Width", BitConverter.GetBytes(_Width)); + dictionary.Add("Height", BitConverter.GetBytes(_Height)); + dictionary.Add("AutoSize", new byte[1] { _AutoSize ? ((byte)1) : ((byte)0) }); + if (_Mark != null) + { + dictionary.Add("Mark", Encoding.UTF8.GetBytes(_Mark)); + } + dictionary.Add("LockOption", new byte[1] { _LockOption ? ((byte)1) : ((byte)0) }); + dictionary.Add("LockLocation", new byte[1] { _LockLocation ? ((byte)1) : ((byte)0) }); + Type type = GetType(); + PropertyInfo[] properties = type.GetProperties(); + foreach (PropertyInfo propertyInfo in properties) + { + object[] customAttributes = propertyInfo.GetCustomAttributes(inherit: true); + object[] array = customAttributes; + foreach (object obj in array) + { + if (obj is STNodePropertyAttribute) + { + STNodePropertyAttribute sTNodePropertyAttribute = obj as STNodePropertyAttribute; + object obj2 = Activator.CreateInstance(sTNodePropertyAttribute.DescriptorType); + if (!(obj2 is STNodePropertyDescriptor)) + { + throw new InvalidOperationException("[STNodePropertyAttribute.Type]参数值必须为[STNodePropertyDescriptor]或者其子类的类型"); + } + STNodePropertyDescriptor sTNodePropertyDescriptor = (STNodePropertyDescriptor)Activator.CreateInstance(sTNodePropertyAttribute.DescriptorType); + sTNodePropertyDescriptor.Node = this; + sTNodePropertyDescriptor.PropertyInfo = propertyInfo; + byte[] bytesFromValue = sTNodePropertyDescriptor.GetBytesFromValue(); + if (bytesFromValue != null) + { + dictionary.Add(propertyInfo.Name, bytesFromValue); + } + } + } + } + OnSaveNode(dictionary); + return dictionary; + } + + public byte[] GetSaveData() + { + List list = new List(); + Type type = GetType(); + byte[] bytes = Encoding.UTF8.GetBytes(STNodeTypeRegistry.GetModelByType(type)); + list.Add((byte)bytes.Length); + list.AddRange(bytes); + bytes = Encoding.UTF8.GetBytes(type.GUID.ToString()); + list.Add((byte)bytes.Length); + list.AddRange(bytes); + Dictionary dictionary = OnSaveNode(); + if (dictionary != null) + { + foreach (KeyValuePair item in dictionary) + { + bytes = Encoding.UTF8.GetBytes(item.Key); + list.AddRange(BitConverter.GetBytes(bytes.Length)); + list.AddRange(bytes); + list.AddRange(BitConverter.GetBytes(item.Value.Length)); + list.AddRange(item.Value); + } + } + return list.ToArray(); + } + + protected virtual void OnCreate() + { + } + + protected virtual void OnCreated() + { + } + + protected internal virtual void OnDrawNode(DrawingTools dt) + { + Graphics graphics = dt.Graphics; + int cornerRadius = _Owner?.NodeCornerRadius ?? 0; + if (_BackColor.A != 0) + { + dt.SolidBrush.Color = _BackColor; + if (cornerRadius > 0) + { + graphics.SmoothingMode = SmoothingMode.AntiAlias; + using GraphicsPath nodePath = CreateRoundedRectanglePath(Rectangle, cornerRadius); + GraphicsState graphicsState = graphics.Save(); + graphics.SetClip(nodePath, CombineMode.Intersect); + graphics.FillRectangle(dt.SolidBrush, _Left, _Top + _TitleHeight, _Width, Height - _TitleHeight); + graphics.Restore(graphicsState); + } + else + { + graphics.SmoothingMode = SmoothingMode.None; + graphics.FillRectangle(dt.SolidBrush, _Left, _Top + _TitleHeight, _Width, Height - _TitleHeight); + } + } + OnDrawTitle(dt); + OnDrawBody(dt); + } + + public virtual string OnGetDrawTitle() + { + return _Title; + } + + protected virtual void OnDrawTitle(DrawingTools dt) + { + m_sf.Alignment = StringAlignment.Center; + m_sf.LineAlignment = StringAlignment.Center; + Graphics graphics = dt.Graphics; + SolidBrush solidBrush = dt.SolidBrush; + if (_TitleColor.A != 0) + { + solidBrush.Color = _TitleColor; + int cornerRadius = _Owner?.NodeCornerRadius ?? 0; + if (cornerRadius > 0) + { + graphics.SmoothingMode = SmoothingMode.AntiAlias; + using GraphicsPath nodePath = CreateRoundedRectanglePath(Rectangle, cornerRadius); + GraphicsState graphicsState = graphics.Save(); + graphics.SetClip(nodePath, CombineMode.Intersect); + graphics.FillRectangle(solidBrush, TitleRectangle); + graphics.Restore(graphicsState); + } + else + { + graphics.FillRectangle(solidBrush, TitleRectangle); + } + } + if (_TitleProgress > 0f && _TitleProgressColor.A != 0) + { + Rectangle progressRectangle = TitleRectangle; + progressRectangle.Width = (int)Math.Round(progressRectangle.Width * _TitleProgress); + if (progressRectangle.Width > 0) + { + solidBrush.Color = _TitleProgressColor; + int cornerRadius = _Owner?.NodeCornerRadius ?? 0; + if (cornerRadius > 0) + { + graphics.SmoothingMode = SmoothingMode.AntiAlias; + using GraphicsPath nodePath = CreateRoundedRectanglePath(Rectangle, cornerRadius); + GraphicsState graphicsState = graphics.Save(); + graphics.SetClip(nodePath, CombineMode.Intersect); + graphics.FillRectangle(solidBrush, progressRectangle); + graphics.Restore(graphicsState); + } + else + { + graphics.FillRectangle(solidBrush, progressRectangle); + } + } + } + if (_LockOption) + { + solidBrush.Color = _ForeColor; + int num = _Top + _TitleHeight / 2 - 5; + graphics.FillRectangle(dt.SolidBrush, _Left + 4, num, 2, 4); + graphics.FillRectangle(dt.SolidBrush, _Left + 6, num, 2, 2); + graphics.FillRectangle(dt.SolidBrush, _Left + 8, num, 2, 4); + graphics.FillRectangle(dt.SolidBrush, _Left + 3, num + 4, 8, 6); + } + if (_LockLocation) + { + solidBrush.Color = _ForeColor; + int num2 = _Top + _TitleHeight / 2 - 5; + graphics.FillRectangle(solidBrush, Right - 9, num2, 4, 4); + graphics.FillRectangle(solidBrush, Right - 11, num2 + 4, 8, 2); + graphics.FillRectangle(solidBrush, Right - 8, num2 + 6, 2, 4); + } + string text = OnGetDrawTitle(); + if (!string.IsNullOrEmpty(text) && _ForeColor.A != 0) + { + solidBrush.Color = _ForeColor; + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.DrawString(text, _Font, solidBrush, GetTitleTextRectangle(), m_sf); + } + } + + protected virtual Rectangle GetTitleTextRectangle() + { + Rectangle rectangle = TitleRectangle; + rectangle.Offset(0, 2); + return rectangle; + } + + private static GraphicsPath CreateRoundedRectanglePath(Rectangle rectangle, int radius) + { + GraphicsPath path = new GraphicsPath(); + int maxRadius = Math.Min(rectangle.Width, rectangle.Height) / 2; + radius = Math.Min(radius, maxRadius); + if (radius <= 0) + { + path.AddRectangle(rectangle); + return path; + } + + int diameter = radius * 2; + path.AddArc(rectangle.Left, rectangle.Top, diameter, diameter, 180f, 90f); + path.AddArc(rectangle.Right - diameter, rectangle.Top, diameter, diameter, 270f, 90f); + path.AddArc(rectangle.Right - diameter, rectangle.Bottom - diameter, diameter, diameter, 0f, 90f); + path.AddArc(rectangle.Left, rectangle.Bottom - diameter, diameter, diameter, 90f, 90f); + path.CloseFigure(); + return path; + } + + protected virtual void OnDrawBody(DrawingTools dt) + { + SolidBrush solidBrush = dt.SolidBrush; + foreach (STNodeOption inputOption in _InputOptions) + { + if (inputOption != STNodeOption.Empty) + { + OnDrawOptionDot(dt, inputOption); + OnDrawOptionText(dt, inputOption); + } + } + foreach (STNodeOption outputOption in _OutputOptions) + { + if (outputOption != STNodeOption.Empty) + { + OnDrawOptionDot(dt, outputOption); + OnDrawOptionText(dt, outputOption); + } + } + if (!_ShowControls || _Controls.Count == 0) + { + return; + } + dt.Graphics.TranslateTransform(_Left, _Top + _TitleHeight); + Point empty = Point.Empty; + Point point = Point.Empty; + foreach (STNodeControl control in _Controls) + { + if (control.Visable) + { + empty.X = control.Left - point.X; + empty.Y = control.Top - point.Y; + point = control.Location; + dt.Graphics.TranslateTransform(empty.X, empty.Y); + dt.Graphics.SmoothingMode = SmoothingMode.None; + control.OnPaint(dt); + } + } + dt.Graphics.TranslateTransform(-_Left - point.X, -_Top - _TitleHeight - point.Y); + } + + protected internal virtual void OnDrawMark(DrawingTools dt) + { + if (!string.IsNullOrEmpty(_Mark)) + { + Graphics graphics = dt.Graphics; + SolidBrush solidBrush = dt.SolidBrush; + m_sf.LineAlignment = StringAlignment.Center; + graphics.SmoothingMode = SmoothingMode.None; + solidBrush.Color = _MarkColor; + graphics.FillRectangle(solidBrush, _MarkRectangle); + graphics.SmoothingMode = SmoothingMode.HighQuality; + SizeF sizeF = graphics.MeasureString(Mark, Font, _MarkRectangle.Width); + solidBrush.Color = _ForeColor; + if (sizeF.Height > (float)_ItemHeight || sizeF.Width > (float)_MarkRectangle.Width) + { + Rectangle rectangle = new Rectangle(_MarkRectangle.Left + 2, _MarkRectangle.Top + 2, _MarkRectangle.Width - 20, 16); + m_sf.Alignment = StringAlignment.Near; + graphics.DrawString(_MarkLines[0], _Font, solidBrush, rectangle, m_sf); + m_sf.Alignment = StringAlignment.Far; + rectangle.Width = _MarkRectangle.Width - 5; + graphics.DrawString("+", _Font, solidBrush, rectangle, m_sf); + } + else + { + m_sf.Alignment = StringAlignment.Near; + graphics.DrawString(_MarkLines[0].Trim(), _Font, solidBrush, _MarkRectangle, m_sf); + } + } + } + + protected virtual void OnDrawOptionDot(DrawingTools dt, STNodeOption op) + { + Graphics graphics = dt.Graphics; + Pen pen = dt.Pen; + SolidBrush solidBrush = dt.SolidBrush; + Type typeFromHandle = typeof(object); + if (op == null || Owner == null) + { + return; + } + if (op.DotColor != Color.Transparent) + { + solidBrush.Color = op.DotColor; + } + else if (op.DataType == typeFromHandle) + { + pen.Color = Owner.UnknownTypeColor; + } + else + { + solidBrush.Color = (Owner.TypeColor.ContainsKey(op.DataType) ? Owner.TypeColor[op.DataType] : Owner.UnknownTypeColor); + } + if (op.IsSingle) + { + graphics.SmoothingMode = SmoothingMode.HighQuality; + if (op.DataType == typeFromHandle) + { + graphics.DrawEllipse(pen, op.DotRectangle.X, op.DotRectangle.Y, op.DotRectangle.Width - 1, op.DotRectangle.Height - 1); + } + else + { + graphics.FillEllipse(solidBrush, op.DotRectangle); + } + } + else + { + graphics.SmoothingMode = SmoothingMode.None; + if (op.DataType == typeFromHandle) + { + graphics.DrawRectangle(pen, op.DotRectangle.X, op.DotRectangle.Y, op.DotRectangle.Width - 1, op.DotRectangle.Height - 1); + } + else + { + graphics.FillRectangle(solidBrush, op.DotRectangle); + } + } + } + + protected virtual void OnDrawOptionText(DrawingTools dt, STNodeOption op) + { + Graphics graphics = dt.Graphics; + SolidBrush solidBrush = dt.SolidBrush; + if (op.IsInput) + { + m_sf.Alignment = StringAlignment.Near; + } + else + { + m_sf.Alignment = StringAlignment.Far; + } + solidBrush.Color = op.TextColor; + graphics.DrawString(op.Text, Font, solidBrush, op.TextRectangle, m_sf); + } + + protected virtual Point OnSetOptionDotLocation(STNodeOption op, Point pt, int nIndex) + { + return pt; + } + + protected virtual Rectangle OnSetOptionTextRectangle(STNodeOption op, Rectangle rect, int nIndex) + { + return rect; + } + + protected virtual Size GetDefaultNodeSize(Graphics g) + { + int num = 0; + int num2 = 0; + foreach (STNodeOption inputOption in _InputOptions) + { + num += _ItemHeight; + } + foreach (STNodeOption outputOption in _OutputOptions) + { + num2 += _ItemHeight; + } + int height = _TitleHeight + ((num > num2) ? num : num2); + SizeF sizeF = SizeF.Empty; + SizeF sizeF2 = SizeF.Empty; + foreach (STNodeOption inputOption2 in _InputOptions) + { + if (!string.IsNullOrEmpty(inputOption2.Text)) + { + SizeF sizeF3 = g.MeasureString(inputOption2.Text, _Font); + if (sizeF3.Width > sizeF.Width) + { + sizeF = sizeF3; + } + } + } + foreach (STNodeOption outputOption2 in _OutputOptions) + { + if (!string.IsNullOrEmpty(outputOption2.Text)) + { + SizeF sizeF4 = g.MeasureString(outputOption2.Text, _Font); + if (sizeF4.Width > sizeF2.Width) + { + sizeF2 = sizeF4; + } + } + } + int num3 = (int)(sizeF.Width + sizeF2.Width + 25f); + if (!string.IsNullOrEmpty(Title)) + { + sizeF = g.MeasureString(Title, Font); + } + if (sizeF.Width + 30f > (float)num3) + { + num3 = (int)sizeF.Width + 30; + } + return new Size(num3, height); + } + + protected virtual Size GetDefaultNodeSize() + { + if (_Owner != null) + { + using Graphics graphics = _Owner.CreateGraphics(); + return GetDefaultNodeSize(graphics); + } + using Bitmap bitmap = new Bitmap(1, 1); + using Graphics fallbackGraphics = Graphics.FromImage(bitmap); + return GetDefaultNodeSize(fallbackGraphics); + } + + protected virtual Rectangle OnBuildMarkRectangle(Graphics g) + { + return new Rectangle(_Left, _Top - 30, _Width, 20); + } + + protected virtual Rectangle OnBuildMarkRectangle() + { + if (_Owner != null) + { + using Graphics graphics = _Owner.CreateGraphics(); + return OnBuildMarkRectangle(graphics); + } + using Bitmap bitmap = new Bitmap(1, 1); + using Graphics fallbackGraphics = Graphics.FromImage(bitmap); + return OnBuildMarkRectangle(fallbackGraphics); + } + + protected virtual void OnSaveNode(Dictionary dic) + { + } + + protected internal virtual void OnLoadNode(Dictionary dic) + { + if (dic.ContainsKey("AutoSize")) + { + _AutoSize = dic["AutoSize"][0] == 1; + } + if (dic.ContainsKey("LockOption")) + { + _LockOption = dic["LockOption"][0] == 1; + } + if (dic.ContainsKey("LockLocation")) + { + _LockLocation = dic["LockLocation"][0] == 1; + } + if (dic.ContainsKey("Guid")) + { + _Guid = new Guid(dic["Guid"]); + } + if (dic.ContainsKey("Left")) + { + _Left = BitConverter.ToInt32(dic["Left"], 0); + } + if (dic.ContainsKey("Top")) + { + _Top = BitConverter.ToInt32(dic["Top"], 0); + } + if (dic.ContainsKey("Width") && !_AutoSize) + { + _Width = BitConverter.ToInt32(dic["Width"], 0); + } + if (dic.ContainsKey("Height") && !_AutoSize) + { + _Height = BitConverter.ToInt32(dic["Height"], 0); + } + if (dic.ContainsKey("Mark")) + { + Mark = Encoding.UTF8.GetString(dic["Mark"]); + } + Type type = GetType(); + PropertyInfo[] properties = type.GetProperties(); + foreach (PropertyInfo propertyInfo in properties) + { + object[] customAttributes = propertyInfo.GetCustomAttributes(inherit: true); + object[] array = customAttributes; + foreach (object obj in array) + { + if (!(obj is STNodePropertyAttribute)) + { + continue; + } + STNodePropertyAttribute sTNodePropertyAttribute = obj as STNodePropertyAttribute; + object obj2 = Activator.CreateInstance(sTNodePropertyAttribute.DescriptorType); + if (!(obj2 is STNodePropertyDescriptor)) + { + throw new InvalidOperationException("[STNodePropertyAttribute.Type]参数值必须为[STNodePropertyDescriptor]或者其子类的类型"); + } + STNodePropertyDescriptor sTNodePropertyDescriptor = (STNodePropertyDescriptor)Activator.CreateInstance(sTNodePropertyAttribute.DescriptorType); + sTNodePropertyDescriptor.Node = this; + sTNodePropertyDescriptor.PropertyInfo = propertyInfo; + try + { + if (dic.ContainsKey(propertyInfo.Name)) + { + sTNodePropertyDescriptor.SetValue(dic[propertyInfo.Name]); + } + } + catch (Exception ex) + { + string text = "属性[" + Title + "." + propertyInfo.Name + "]的值无法被还原 可通过重写[STNodePropertyAttribute.GetBytesFromValue(),STNodePropertyAttribute.GetValueFromBytes(byte[])]确保保存和加载时候的二进制数据正确"; + for (Exception ex2 = ex; ex2 != null; ex2 = ex2.InnerException) + { + text = text + "\r\n----\r\n[" + ex2.GetType().Name + "] -> " + ex2.Message; + } + throw new InvalidOperationException(text, ex); + } + } + } + SetOptionsLocation(); + } + + protected internal virtual void OnEditorLoadCompleted() + { + } + + protected bool SetOptionText(STNodeOption op, string strText) + { + if (op.Owner != this) + { + return false; + } + op.Text = strText; + return true; + } + + protected bool SetOptionTextColor(STNodeOption op, Color clr) + { + if (op.Owner != this) + { + return false; + } + op.TextColor = clr; + return true; + } + + protected bool SetOptionDotColor(STNodeOption op, Color clr) + { + if (op.Owner != this) + { + return false; + } + op.DotColor = clr; + return false; + } + + protected internal virtual void OnGotFocus(EventArgs e) + { + } + + protected internal virtual void OnLostFocus(EventArgs e) + { + } + + protected internal virtual void OnMouseEnter(EventArgs e) + { + } + + protected internal virtual void OnMouseDown(STNodeMouseEventArgs e) + { + if (!_ShowControls) + { + if (m_ctrl_active != null) + { + m_ctrl_active.OnLostFocus(EventArgs.Empty); + } + m_ctrl_active = null; + return; + } + Point location = e.Location; + location.Y -= _TitleHeight; + for (int num = _Controls.Count - 1; num >= 0; num--) + { + STNodeControl sTNodeControl = _Controls[num]; + if (sTNodeControl.DisplayRectangle.Contains(location)) + { + if (!sTNodeControl.Enabled) + { + return; + } + if (sTNodeControl.Visable) + { + sTNodeControl.OnMouseDown(e.WithLocation(e.X - sTNodeControl.Left, location.Y - sTNodeControl.Top)); + m_ctrl_down = sTNodeControl; + if (m_ctrl_active != sTNodeControl) + { + sTNodeControl.OnGotFocus(EventArgs.Empty); + if (m_ctrl_active != null) + { + m_ctrl_active.OnLostFocus(EventArgs.Empty); + } + m_ctrl_active = sTNodeControl; + } + return; + } + } + } + if (m_ctrl_active != null) + { + m_ctrl_active.OnLostFocus(EventArgs.Empty); + } + m_ctrl_active = null; + } + + protected internal virtual void OnMouseMove(STNodeMouseEventArgs e) + { + if (!_ShowControls) + { + m_ctrl_down = null; + if (m_ctrl_hover != null) + { + m_ctrl_hover.OnMouseLeave(EventArgs.Empty); + } + m_ctrl_hover = null; + return; + } + Point location = e.Location; + location.Y -= _TitleHeight; + if (m_ctrl_down != null) + { + if (m_ctrl_down.Enabled && m_ctrl_down.Visable) + { + m_ctrl_down.OnMouseMove(e.WithLocation(e.X - m_ctrl_down.Left, location.Y - m_ctrl_down.Top)); + } + return; + } + for (int num = _Controls.Count - 1; num >= 0; num--) + { + STNodeControl sTNodeControl = _Controls[num]; + if (sTNodeControl.DisplayRectangle.Contains(location)) + { + if (m_ctrl_hover != _Controls[num]) + { + sTNodeControl.OnMouseEnter(EventArgs.Empty); + if (m_ctrl_hover != null) + { + m_ctrl_hover.OnMouseLeave(EventArgs.Empty); + } + m_ctrl_hover = sTNodeControl; + } + m_ctrl_hover.OnMouseMove(e.WithLocation(e.X - sTNodeControl.Left, location.Y - sTNodeControl.Top)); + return; + } + } + if (m_ctrl_hover != null) + { + m_ctrl_hover.OnMouseLeave(EventArgs.Empty); + } + m_ctrl_hover = null; + } + + protected internal virtual void OnMouseUp(STNodeMouseEventArgs e) + { + if (!_ShowControls) + { + m_ctrl_down = null; + return; + } + Point location = e.Location; + location.Y -= _TitleHeight; + if (m_ctrl_down != null && m_ctrl_down.Enabled && m_ctrl_down.Visable) + { + m_ctrl_down.OnMouseUp(e.WithLocation(e.X - m_ctrl_down.Left, location.Y - m_ctrl_down.Top)); + } + m_ctrl_down = null; + } + + protected internal virtual void CancelMouseInteraction() + { + m_ctrl_down = null; + } + + protected internal virtual void OnMouseLeave(EventArgs e) + { + if (m_ctrl_hover != null && m_ctrl_hover.Enabled && m_ctrl_hover.Visable) + { + m_ctrl_hover.OnMouseLeave(e); + } + m_ctrl_hover = null; + } + + protected internal virtual void OnMouseClick(STNodeMouseEventArgs e) + { + if (!_ShowControls) + { + return; + } + Point location = e.Location; + location.Y -= _TitleHeight; + if (m_ctrl_active != null && m_ctrl_active.Enabled && m_ctrl_active.Visable) + { + m_ctrl_active.OnMouseClick(e.WithLocation(e.X - m_ctrl_active.Left, location.Y - m_ctrl_active.Top)); + } + } + + protected internal virtual void OnMouseWheel(STNodeMouseEventArgs e) + { + if (!_ShowControls) + { + return; + } + Point location = e.Location; + location.Y -= _TitleHeight; + if (m_ctrl_hover != null && m_ctrl_hover.Enabled && m_ctrl_hover.Visable) + { + m_ctrl_hover.OnMouseWheel(e.WithLocation(e.X - m_ctrl_hover.Left, location.Y - m_ctrl_hover.Top)); + } + } + + protected internal virtual void OnMouseHWheel(STNodeMouseEventArgs e) + { + if (m_ctrl_hover != null && m_ctrl_hover.Enabled && m_ctrl_hover.Visable) + { + m_ctrl_hover.OnMouseHWheel(e); + } + } + + protected internal virtual void OnKeyDown(KeyEventArgs e) + { + if (m_ctrl_active != null && m_ctrl_active.Enabled && m_ctrl_active.Visable) + { + m_ctrl_active.OnKeyDown(e); + } + } + + protected internal virtual void OnKeyUp(KeyEventArgs e) + { + if (m_ctrl_active != null && m_ctrl_active.Enabled && m_ctrl_active.Visable) + { + m_ctrl_active.OnKeyUp(e); + } + } + + protected internal virtual void OnKeyPress(STNodeKeyPressEventArgs e) + { + if (m_ctrl_active != null && m_ctrl_active.Enabled && m_ctrl_active.Visable) + { + m_ctrl_active.OnKeyPress(e); + } + } + + protected virtual void OnMove(EventArgs e) + { + } + + protected virtual void OnResize(EventArgs e) + { + } + + protected virtual void OnOwnerChanged() + { + } + + protected virtual void OnSelectedChanged() + { + } + + protected virtual void OnActiveChanged() + { + } + + protected virtual void SetOptionsLocation() + { + int num = 0; + Rectangle rect = new Rectangle(Left + 10, _Top + _TitleHeight, _Width - 20, _ItemHeight); + foreach (STNodeOption inputOption in _InputOptions) + { + if (inputOption != STNodeOption.Empty) + { + Point point = OnSetOptionDotLocation(inputOption, new Point(Left - inputOption.DotSize / 2, rect.Y + (rect.Height - inputOption.DotSize) / 2), num); + inputOption.TextRectangle = OnSetOptionTextRectangle(inputOption, rect, num); + inputOption.DotLeft = point.X; + inputOption.DotTop = point.Y; + } + rect.Y += _ItemHeight; + num++; + } + rect.Y = _Top + _TitleHeight; + m_sf.Alignment = StringAlignment.Far; + foreach (STNodeOption outputOption in _OutputOptions) + { + if (outputOption != STNodeOption.Empty) + { + Point point2 = OnSetOptionDotLocation(outputOption, new Point(_Left + _Width - outputOption.DotSize / 2, rect.Y + (rect.Height - outputOption.DotSize) / 2), num); + outputOption.TextRectangle = OnSetOptionTextRectangle(outputOption, rect, num); + outputOption.DotLeft = point2.X; + outputOption.DotTop = point2.Y; + } + rect.Y += _ItemHeight; + num++; + } + } + + public void Invalidate() + { + if (_Owner != null) + { + _Owner.Invalidate(_Owner.CanvasToControl(new Rectangle(_Left - 5, _Top - 5, _Width + 10, _Height + 10))); + } + } + + public void Invalidate(Rectangle rect) + { + rect.X += _Left; + rect.Y += _Top; + if (_Owner != null) + { + rect = _Owner.CanvasToControl(rect); + rect.Width++; + rect.Height++; + _Owner.Invalidate(rect); + } + } + + public STNodeOption[] GetInputOptions() + { + if (!_LetGetOptions) + { + return null; + } + STNodeOption[] array = new STNodeOption[_InputOptions.Count]; + for (int i = 0; i < _InputOptions.Count; i++) + { + array[i] = _InputOptions[i]; + } + return array; + } + + public STNodeOption[] GetOutputOptions() + { + if (!_LetGetOptions) + { + return null; + } + STNodeOption[] array = new STNodeOption[_OutputOptions.Count]; + for (int i = 0; i < _OutputOptions.Count; i++) + { + array[i] = _OutputOptions[i]; + } + return array; + } + + /// + /// Returns all input options regardless of LetGetOptions setting. + /// Used for serialization/copy-paste operations. + /// + public STNodeOption[] GetAllInputOptions() + { + STNodeOption[] array = new STNodeOption[_InputOptions.Count]; + for (int i = 0; i < _InputOptions.Count; i++) + { + array[i] = _InputOptions[i]; + } + return array; + } + + public bool ReorderInputOptions(IReadOnlyList orderedOptions) + { + if (!_InputOptions.Reorder(orderedOptions)) + { + return false; + } + + SetOptionsLocation(); + BuildSize(bBuildNode: true, bBuildMark: true, bRedraw: false); + _Owner?.BuildLinePath(); + Invalidate(); + return true; + } + + /// + /// Returns all output options regardless of LetGetOptions setting. + /// Used for serialization/copy-paste operations. + /// + public STNodeOption[] GetAllOutputOptions() + { + STNodeOption[] array = new STNodeOption[_OutputOptions.Count]; + for (int i = 0; i < _OutputOptions.Count; i++) + { + array[i] = _OutputOptions[i]; + } + return array; + } + + public void SetSelected(bool bSelected, bool bRedraw) + { + if (_IsSelected == bSelected) + { + return; + } + _IsSelected = bSelected; + if (_Owner != null) + { + if (bSelected) + { + _Owner.AddSelectedNode(this); + } + else + { + _Owner.RemoveSelectedNode(this); + } + } + if (bRedraw) + { + Invalidate(); + } + OnSelectedChanged(); + if (_Owner != null) + { + _Owner.OnSelectedChanged(EventArgs.Empty); + } + } + + public IAsyncResult BeginInvoke(Delegate method) + { + return BeginInvoke(method, null); + } + + public IAsyncResult BeginInvoke(Delegate method, params object[] args) + { + if (_Owner == null) + { + return null; + } + return _Owner.BeginInvoke(method, args); + } + + public object Invoke(Delegate method) + { + return Invoke(method, null); + } + + public object Invoke(Delegate method, params object[] args) + { + if (_Owner == null) + { + return null; + } + return _Owner.Invoke(method, args); + } + + public void Create() + { + if (Interlocked.CompareExchange(ref m_create_state, 1, 0) != 0) + { + return; + } + + try + { + OnCreate(); + OnCreated(); + Volatile.Write(ref m_create_state, 2); + } + catch + { + Volatile.Write(ref m_create_state, 0); + throw; + } + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeAttribute.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeAttribute.cs new file mode 100644 index 0000000..cfb62f6 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeAttribute.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; +using ST.Library.UI; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeAttribute : Attribute +{ + private string _Path; + + private string _Author; + + private string _Mail; + + private string _Link; + + private string _Description; + + private static char[] m_ch_splitter = new char[2] { '/', '\\' }; + + private static Regex m_reg = new Regex("^https?://", RegexOptions.IgnoreCase); + + private static Dictionary m_dic = new Dictionary(); + + public string Path => _Path; + + public string Author => _Author; + + public string Mail => _Mail; + + public string Link => _Link; + + public string Description => _Description; + + public string DisplayDescription => Lang.GetOrDefault(_Description); + + public STNodeAttribute(string strPath) + : this(strPath, null, null, null, null) + { + } + + public STNodeAttribute(string strPath, string strDescription) + : this(strPath, null, null, null, strDescription) + { + } + + public STNodeAttribute(string strPath, string strAuthor, string strMail, string strLink, string strDescription) + { + if (!string.IsNullOrEmpty(strPath)) + { + strPath = strPath.Trim().Trim(m_ch_splitter).Trim(); + } + _Path = strPath; + _Author = strAuthor; + _Mail = strMail; + _Description = strDescription; + if (!string.IsNullOrEmpty(strLink) && !(strLink.Trim() == string.Empty)) + { + strLink = strLink.Trim(); + if (m_reg.IsMatch(strLink)) + { + _Link = strLink; + } + else + { + _Link = "http://" + strLink; + } + } + } + + public static MethodInfo GetHelpMethod(Type stNodeType) + { + if (m_dic.ContainsKey(stNodeType)) + { + return m_dic[stNodeType]; + } + MethodInfo method = stNodeType.GetMethod("ShowHelpInfo"); + if (method == null) + { + return null; + } + if (!method.IsStatic) + { + return null; + } + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length != 1) + { + return null; + } + if (parameters[0].ParameterType != typeof(string)) + { + return null; + } + m_dic.Add(stNodeType, method); + return method; + } + + public static void ShowHelp(Type stNodeType) + { + MethodInfo helpMethod = GetHelpMethod(stNodeType); + if (!(helpMethod == null)) + { + helpMethod.Invoke(null, new object[1] { stNodeType.Module.FullyQualifiedName }); + } + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeCanvasReader.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeCanvasReader.cs new file mode 100644 index 0000000..2d16df7 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeCanvasReader.cs @@ -0,0 +1,395 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace ST.Library.UI.NodeEditor; + +/// +/// Decodes the version-1 STN envelope without mutating a live canvas. +/// Keeping decoding separate from commit makes truncated or corrupt input +/// fail before any node is appended to the currently displayed graph. +/// +internal static class STNodeCanvasReader +{ + private const int MaximumNodeCount = 10_000; + private const int MaximumConnectionCount = 100_000; + private const int MaximumNodeDataLength = 16 * 1024 * 1024; + private const long MaximumTotalNodeDataLength = 128L * 1024 * 1024; + private const int MaximumCompressedGraphLength = 160 * 1024 * 1024; + private const int MaximumDecompressedGraphLength = 160 * 1024 * 1024; + private static readonly uint[] Crc32Table = CreateCrc32Table(); + + internal sealed class Document + { + public float CanvasOffsetX { get; init; } + public float CanvasOffsetY { get; init; } + public float CanvasScale { get; init; } + public List Nodes { get; } = new(); + public List Connections { get; } = new(); + + public void ConnectDetachedNodes() + { + foreach (Connection connection in Connections) + { + STNode outputNode = connection.Output.Owner; + STNode inputNode = connection.Input.Owner; + bool outputLocked = outputNode.LockOption; + bool inputLocked = inputNode.LockOption; + outputNode.LockOption = false; + inputNode.LockOption = false; + try + { + ConnectionStatus status = connection.Output.ConnectOption( + connection.Input, + isOwnerOfOwner: false); + if (status != ConnectionStatus.Connected) + { + throw new InvalidDataException( + $"无法恢复流程连接:{status}"); + } + } + finally + { + outputNode.LockOption = outputLocked; + inputNode.LockOption = inputLocked; + } + } + } + } + + internal sealed class Connection + { + public Connection(STNodeOption output, STNodeOption input) + { + Output = output; + Input = input; + } + + public STNodeOption Output { get; } + + public STNodeOption Input { get; } + } + + public static Document Read(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + + byte[] header = ReadBytes(stream, STNodeConstant.NodeFlag.Length + 1); + for (int i = 0; i < STNodeConstant.NodeFlag.Length; i++) + { + if (header[i] != STNodeConstant.NodeFlag[i]) + throw new InvalidDataException("无法识别的文件类型"); + } + if (header[STNodeConstant.NodeFlag.Length] != STNodeConstant.Version) + throw new InvalidDataException("无法识别的文件版本号"); + + byte[] compressed = ReadToEnd( + stream, + MaximumCompressedGraphLength); + byte[] decompressed = DecompressGZip(compressed); + using var bodyStream = new MemoryStream( + decompressed, + writable: false); + var document = new Document + { + CanvasOffsetX = ReadSingle(bodyStream), + CanvasOffsetY = ReadSingle(bodyStream), + CanvasScale = ReadSingle(bodyStream), + }; + if (float.IsNaN(document.CanvasOffsetX) + || float.IsInfinity(document.CanvasOffsetX) + || float.IsNaN(document.CanvasOffsetY) + || float.IsInfinity(document.CanvasOffsetY) + || float.IsNaN(document.CanvasScale) + || float.IsInfinity(document.CanvasScale) + || document.CanvasScale <= 0) + { + throw new InvalidDataException("画布视图参数无效"); + } + + int nodeCount = ReadCount(bodyStream, MaximumNodeCount, "节点"); + var options = new Dictionary(); + var indexedOptions = new HashSet(); + long totalNodeDataLength = 0; + for (int i = 0; i < nodeCount; i++) + { + int nodeDataLength = ReadInt32(bodyStream); + if (nodeDataLength <= 0 || nodeDataLength > MaximumNodeDataLength) + { + throw new InvalidDataException( + $"第 {i + 1} 个节点数据长度无效:{nodeDataLength}"); + } + totalNodeDataLength += nodeDataLength; + if (totalNodeDataLength > MaximumTotalNodeDataLength) + throw new InvalidDataException("节点数据总长度超过限制"); + + STNode node; + try + { + node = CreateNode(ReadBytes(bodyStream, nodeDataLength)); + } + catch (Exception ex) + { + throw new InvalidDataException( + $"第 {i + 1} 个节点无法加载", + ex); + } + document.Nodes.Add(node); + AddOptions(options, indexedOptions, node); + } + + int connectionCount = ReadCount( + bodyStream, + MaximumConnectionCount, + "连接"); + var connectionKeys = new HashSet(); + for (int i = 0; i < connectionCount; i++) + { + long packed = ReadInt64(bodyStream); + long outputIndex = packed >> 32; + long inputIndex = unchecked((uint)packed); + if (!options.TryGetValue(outputIndex, out STNodeOption output) + || !options.TryGetValue(inputIndex, out STNodeOption input)) + { + throw new InvalidDataException( + $"第 {i + 1} 条连接引用了不存在的端口"); + } + if (output.IsInput || !input.IsInput || output.Owner == input.Owner) + { + throw new InvalidDataException( + $"第 {i + 1} 条连接方向无效"); + } + if (!connectionKeys.Add(packed)) + throw new InvalidDataException($"第 {i + 1} 条连接重复"); + + document.Connections.Add(new Connection(output, input)); + } + + if (bodyStream.ReadByte() != -1) + throw new InvalidDataException("流程数据包含未识别的尾部内容"); + + return document; + } + + private static byte[] ReadToEnd(Stream stream, int maximumLength) + { + using var output = new MemoryStream(); + byte[] buffer = new byte[81_920]; + while (true) + { + int read = stream.Read(buffer, 0, buffer.Length); + if (read <= 0) + break; + if (output.Length + read > maximumLength) + throw new InvalidDataException("压缩流程数据超过限制"); + output.Write(buffer, 0, read); + } + return output.ToArray(); + } + + private static byte[] DecompressGZip(byte[] compressed) + { + if (compressed.Length < 18) + throw new InvalidDataException("压缩流程数据不完整"); + + using var input = new MemoryStream(compressed, writable: false); + using var gzip = new GZipStream( + input, + CompressionMode.Decompress, + leaveOpen: true); + using var output = new MemoryStream(); + byte[] buffer = new byte[81_920]; + while (true) + { + int read = gzip.Read(buffer, 0, buffer.Length); + if (read <= 0) + break; + if (output.Length + read > MaximumDecompressedGraphLength) + throw new InvalidDataException("解压后的流程数据超过限制"); + output.Write(buffer, 0, read); + } + + byte[] decompressed = output.ToArray(); + uint expectedCrc = BitConverter.ToUInt32( + compressed, + compressed.Length - 8); + uint expectedLength = BitConverter.ToUInt32( + compressed, + compressed.Length - 4); + if (expectedLength != unchecked((uint)decompressed.Length) + || expectedCrc != ComputeCrc32(decompressed)) + { + throw new InvalidDataException("压缩流程数据校验失败"); + } + return decompressed; + } + + private static uint ComputeCrc32(byte[] data) + { + uint crc = uint.MaxValue; + foreach (byte value in data) + { + crc = Crc32Table[(crc ^ value) & byte.MaxValue] + ^ crc >> 8; + } + return ~crc; + } + + private static uint[] CreateCrc32Table() + { + uint[] table = new uint[256]; + for (uint i = 0; i < table.Length; i++) + { + uint value = i; + for (int bit = 0; bit < 8; bit++) + { + value = (value & 1) != 0 + ? 0xEDB88320u ^ value >> 1 + : value >> 1; + } + table[i] = value; + } + return table; + } + + private static STNode CreateNode(byte[] data) + { + int offset = 0; + string modelKey = ReadByteLengthString( + data, + ref offset, + "节点类型"); + string typeKey = ReadByteLengthString( + data, + ref offset, + "节点类型标识"); + var properties = new Dictionary(); + while (offset < data.Length) + { + int keyLength = ReadInt32(data, ref offset, "属性名称长度"); + string propertyName = Encoding.UTF8.GetString( + ReadBytes(data, ref offset, keyLength, "属性名称")); + int valueLength = ReadInt32(data, ref offset, "属性值长度"); + byte[] propertyValue = ReadBytes( + data, + ref offset, + valueLength, + "属性值"); + if (!properties.TryAdd(propertyName, propertyValue)) + { + throw new InvalidDataException( + $"节点数据包含重复属性:{propertyName}"); + } + } + + STNodeTypeRegistry.TryGetNodeType( + typeKey, + modelKey, + out Type nodeType); + if (nodeType == null) + { + throw new TypeLoadException( + $"无法找到节点类型 {{{modelKey}}},请确认对应程序集已加载"); + } + + var node = (STNode)Activator.CreateInstance(nodeType); + node.OnLoadNode(properties); + return node; + } + + private static void AddOptions( + Dictionary options, + HashSet indexedOptions, + STNode node) + { + foreach (STNodeOption option in node.GetAllInputOptions()) + { + if (option != null && indexedOptions.Add(option)) + options.Add(options.Count, option); + } + foreach (STNodeOption option in node.GetAllOutputOptions()) + { + if (option != null && indexedOptions.Add(option)) + options.Add(options.Count, option); + } + } + + private static int ReadCount( + Stream stream, + int maximum, + string valueName) + { + int count = ReadInt32(stream); + if (count < 0 || count > maximum) + throw new InvalidDataException($"{valueName}数量无效:{count}"); + return count; + } + + private static float ReadSingle(Stream stream) + { + return BitConverter.ToSingle(ReadBytes(stream, sizeof(float)), 0); + } + + private static int ReadInt32(Stream stream) + { + return BitConverter.ToInt32(ReadBytes(stream, sizeof(int)), 0); + } + + private static long ReadInt64(Stream stream) + { + return BitConverter.ToInt64(ReadBytes(stream, sizeof(long)), 0); + } + + private static byte[] ReadBytes(Stream stream, int count) + { + byte[] buffer = new byte[count]; + int offset = 0; + while (offset < count) + { + int read = stream.Read(buffer, offset, count - offset); + if (read <= 0) + throw new EndOfStreamException("流程数据意外结束"); + offset += read; + } + return buffer; + } + + private static string ReadByteLengthString( + byte[] data, + ref int offset, + string valueName) + { + if (offset >= data.Length) + throw new InvalidDataException($"{valueName}缺失"); + int length = data[offset++]; + return Encoding.UTF8.GetString( + ReadBytes(data, ref offset, length, valueName)); + } + + private static int ReadInt32( + byte[] data, + ref int offset, + string valueName) + { + return BitConverter.ToInt32( + ReadBytes(data, ref offset, sizeof(int), valueName), + 0); + } + + private static byte[] ReadBytes( + byte[] data, + ref int offset, + int length, + string valueName) + { + if (length < 0 || offset < 0 || offset > data.Length - length) + throw new InvalidDataException($"{valueName}长度无效:{length}"); + + byte[] value = new byte[length]; + Buffer.BlockCopy(data, offset, value, 0, length); + offset += length; + return value; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeCanvasWriter.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeCanvasWriter.cs new file mode 100644 index 0000000..0bb0b69 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeCanvasWriter.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace ST.Library.UI.NodeEditor; + +/// +/// Writes the version-1 STND canvas envelope shared by visual and headless +/// graph hosts. +/// +public static class STNodeCanvasWriter +{ + public static void Write( + Stream stream, + IEnumerable nodes, + IEnumerable connections, + float canvasOffsetX, + float canvasOffsetY, + float canvasScale) + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(nodes); + ArgumentNullException.ThrowIfNull(connections); + if (!stream.CanWrite) + throw new ArgumentException("The canvas stream must be writable.", nameof(stream)); + if (!float.IsFinite(canvasOffsetX) + || !float.IsFinite(canvasOffsetY) + || !float.IsFinite(canvasScale) + || canvasScale <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(canvasScale), + "Canvas coordinates and scale must be finite, and scale must be positive."); + } + + List nodeList = nodes.ToList(); + if (nodeList.Any(node => node == null)) + throw new ArgumentException("Canvas nodes cannot contain null.", nameof(nodes)); + if (nodeList.Distinct().Count() != nodeList.Count) + throw new ArgumentException("Canvas nodes cannot contain duplicates.", nameof(nodes)); + + var nodeIndices = nodeList + .Select((node, index) => (node, index)) + .ToDictionary(item => item.node, item => item.index); + var optionIndices = new Dictionary(); + var nodeData = new List(nodeList.Count); + foreach (STNode node in nodeList) + { + try + { + nodeData.Add(node.GetSaveData()); + AddOptions(optionIndices, node.GetAllInputOptions()); + AddOptions(optionIndices, node.GetAllOutputOptions()); + } + catch (Exception ex) + { + throw new InvalidDataException( + $"Failed to serialize node '{node.Title}'.", + ex); + } + } + + var uniqueConnections = new HashSet<(STNodeOption Output, STNodeOption Input)>(); + ConnectionInfo[] orderedConnections = connections + .Where(connection => + connection.Output != null + && connection.Input != null + && uniqueConnections.Add((connection.Output, connection.Input))) + .OrderBy(connection => GetNodeIndex(nodeIndices, connection.Output.Owner)) + .ThenBy(connection => GetOptionIndex(connection.Output.Owner.GetAllOutputOptions(), connection.Output)) + .ThenBy(connection => GetNodeIndex(nodeIndices, connection.Input.Owner)) + .ThenBy(connection => GetOptionIndex(connection.Input.Owner.GetAllInputOptions(), connection.Input)) + .ToArray(); + var packedConnections = new long[orderedConnections.Length]; + for (int i = 0; i < orderedConnections.Length; i++) + { + ConnectionInfo connection = orderedConnections[i]; + ValidateConnection(nodeIndices, optionIndices, connection); + packedConnections[i] = + optionIndices[connection.Output] << 32 + | unchecked((uint)optionIndices[connection.Input]); + } + + WriteRaw( + stream, + nodeData, + packedConnections, + canvasOffsetX, + canvasOffsetY, + canvasScale); + } + + public static ConnectionInfo[] GetConnections(IEnumerable nodes) + { + ArgumentNullException.ThrowIfNull(nodes); + List nodeList = nodes.ToList(); + var nodeSet = new HashSet(nodeList); + var connections = new List(); + var uniqueConnections = new HashSet<(STNodeOption Output, STNodeOption Input)>(); + foreach (STNode node in nodeList) + { + if (node == null) + throw new ArgumentException("Canvas nodes cannot contain null.", nameof(nodes)); + foreach (STNodeOption output in node.GetAllOutputOptions()) + { + if (output == null || ReferenceEquals(output, STNodeOption.Empty)) + continue; + foreach (STNodeOption input in output.ConnectedOption) + { + if (input == null + || !input.IsInput + || input.Owner == null + || !nodeSet.Contains(input.Owner) + || !uniqueConnections.Add((output, input))) + { + continue; + } + connections.Add(new ConnectionInfo + { + Output = output, + Input = input + }); + } + } + } + return connections.ToArray(); + } + + /// + /// Writes already serialized node payloads and packed global option + /// indices using the unchanged STND v1 envelope. + /// + public static void WriteRaw( + Stream stream, + IReadOnlyList nodeData, + IReadOnlyList packedConnections, + float canvasOffsetX, + float canvasOffsetY, + float canvasScale) + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(nodeData); + ArgumentNullException.ThrowIfNull(packedConnections); + if (!stream.CanWrite) + throw new ArgumentException("The canvas stream must be writable.", nameof(stream)); + + stream.Write(STNodeConstant.NodeFlag, 0, STNodeConstant.NodeFlag.Length); + stream.WriteByte(STNodeConstant.Version); + using GZipStream gzip = new GZipStream(stream, CompressionMode.Compress); + WriteSingle(gzip, canvasOffsetX); + WriteSingle(gzip, canvasOffsetY); + WriteSingle(gzip, canvasScale); + WriteInt32(gzip, nodeData.Count); + foreach (byte[] payload in nodeData) + { + if (payload == null || payload.Length == 0) + throw new InvalidDataException("A serialized node payload is empty."); + WriteInt32(gzip, payload.Length); + gzip.Write(payload, 0, payload.Length); + } + WriteInt32(gzip, packedConnections.Count); + foreach (long connection in packedConnections) + { + byte[] bytes = BitConverter.GetBytes(connection); + gzip.Write(bytes, 0, bytes.Length); + } + } + + private static void AddOptions( + Dictionary optionIndices, + IEnumerable options) + { + foreach (STNodeOption option in options) + { + if (option != null && !optionIndices.ContainsKey(option)) + optionIndices.Add(option, optionIndices.Count); + } + } + + private static int GetNodeIndex( + Dictionary nodeIndices, + STNode node) + { + if (node == null || !nodeIndices.TryGetValue(node, out int index)) + throw new InvalidDataException("A canvas connection references a node outside the canvas."); + return index; + } + + private static int GetOptionIndex( + STNodeOption[] options, + STNodeOption option) + { + for (int i = 0; i < options.Length; i++) + { + if (ReferenceEquals(options[i], option)) + return i; + } + throw new InvalidDataException("A canvas connection references an unknown node option."); + } + + private static void ValidateConnection( + Dictionary nodeIndices, + Dictionary optionIndices, + ConnectionInfo connection) + { + if (connection.Output == null + || connection.Input == null + || connection.Output.IsInput + || !connection.Input.IsInput + || ReferenceEquals(connection.Output, STNodeOption.Empty) + || ReferenceEquals(connection.Input, STNodeOption.Empty) + || connection.Output.Owner == null + || connection.Input.Owner == null + || ReferenceEquals(connection.Output.Owner, connection.Input.Owner) + || !nodeIndices.ContainsKey(connection.Output.Owner) + || !nodeIndices.ContainsKey(connection.Input.Owner) + || !optionIndices.ContainsKey(connection.Output) + || !optionIndices.ContainsKey(connection.Input)) + { + throw new InvalidDataException("The canvas contains an invalid connection."); + } + } + + private static void WriteSingle(GZipStream stream, float value) + { + byte[] bytes = BitConverter.GetBytes(value); + stream.Write(bytes, 0, bytes.Length); + } + + private static void WriteInt32(GZipStream stream, int value) + { + byte[] bytes = BitConverter.GetBytes(value); + stream.Write(bytes, 0, bytes.Length); + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeCollection.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeCollection.cs new file mode 100644 index 0000000..cc94ea6 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeCollection.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeCollection : IList, ICollection, IEnumerable +{ + private int _Count; + + private STNode[] m_nodes; + + private STNodeEditor m_owner; + + public int Count => _Count; + + public bool IsFixedSize => false; + + public bool IsReadOnly => false; + + public STNode this[int nIndex] + { + get + { + if (nIndex < 0 || nIndex >= _Count) + { + throw new IndexOutOfRangeException("索引越界"); + } + return m_nodes[nIndex]; + } + set + { + throw new InvalidOperationException("禁止重新赋值元素"); + } + } + + public bool IsSynchronized => true; + + public object SyncRoot => this; + + bool IList.IsFixedSize => IsFixedSize; + + bool IList.IsReadOnly => IsReadOnly; + + object IList.this[int index] + { + get + { + return this[index]; + } + set + { + this[index] = (STNode)value; + } + } + + int ICollection.Count => _Count; + + bool ICollection.IsSynchronized => IsSynchronized; + + object ICollection.SyncRoot => SyncRoot; + + internal STNodeCollection(STNodeEditor owner) + { + if (owner == null) + { + throw new ArgumentNullException("所有者不能为空"); + } + m_owner = owner; + m_nodes = new STNode[4]; + } + + public void MoveToEnd(STNode node) + { + if (_Count < 1 || m_nodes[_Count - 1] == node) + { + return; + } + bool flag = false; + for (int i = 0; i < _Count - 1; i++) + { + if (m_nodes[i] == node) + { + flag = true; + } + if (flag) + { + m_nodes[i] = m_nodes[i + 1]; + } + } + m_nodes[_Count - 1] = node; + } + + public int Add(STNode node) + { + if (node == null) + { + throw new ArgumentNullException("添加对象不能为空"); + } + using STNodeEditTransaction transaction = m_owner.BeginEditTransaction("添加节点"); + int num = IndexOf(node); + if (-1 == num) + { + EnsureSpace(1); + num = _Count; + node.Owner = m_owner; + m_nodes[_Count++] = node; + m_owner.RecordNodeAdded(node, num); + m_owner.BuildBounds(); + m_owner.OnNodeAdded(new STNodeEditorEventArgs(node)); + m_owner.Invalidate(); + } + return num; + } + + public void AddRange(STNode[] nodes) + { + if (nodes == null) + { + throw new ArgumentNullException("添加对象不能为空"); + } + using STNodeEditTransaction transaction = m_owner.BeginEditTransaction("添加节点"); + foreach (STNode sTNode in nodes) + { + if (sTNode == null) + { + throw new ArgumentNullException("添加对象不能为空"); + } + Add(sTNode); + } + } + + public void Clear() + { + if (_Count == 0) + { + return; + } + using STNodeEditTransaction transaction = m_owner.BeginEditTransaction("清空画布"); + while (_Count > 0) + { + RemoveAt(_Count - 1); + } + } + + public bool Contains(STNode node) + { + return IndexOf(node) != -1; + } + + public int IndexOf(STNode node) + { + return Array.IndexOf(m_nodes, node); + } + + public void Insert(int nIndex, STNode node) + { + if (nIndex < 0 || nIndex > _Count) + { + throw new IndexOutOfRangeException("索引越界"); + } + if (node == null) + { + throw new ArgumentNullException("插入对象不能为空"); + } + using STNodeEditTransaction transaction = m_owner.BeginEditTransaction("添加节点"); + int existingIndex = IndexOf(node); + if (existingIndex >= 0) + { + return; + } + EnsureSpace(1); + for (int num = _Count; num > nIndex; num--) + { + m_nodes[num] = m_nodes[num - 1]; + } + node.Owner = m_owner; + m_nodes[nIndex] = node; + _Count++; + m_owner.RecordNodeAdded(node, nIndex); + m_owner.OnNodeAdded(new STNodeEditorEventArgs(node)); + m_owner.Invalidate(); + m_owner.BuildBounds(); + } + + public void Remove(STNode node) + { + int num = IndexOf(node); + if (num != -1) + { + RemoveAt(num); + } + } + + public void RemoveAt(int nIndex) + { + if (nIndex < 0 || nIndex >= _Count) + { + throw new IndexOutOfRangeException("索引越界"); + } + using STNodeEditTransaction transaction = m_owner.BeginEditTransaction("删除节点"); + STNode node = m_nodes[nIndex]; + var nodeState = m_owner.CaptureNodeStateForRemoval(node); + var connectionOperations = m_owner.CaptureNodeConnectionsForRemoval(node); + bool lockOption = node.LockOption; + node.LockOption = false; + try + { + using (m_owner.SuspendHistoryRecording()) + { + node.Owner = null; + } + } + finally + { + node.LockOption = lockOption; + } + m_owner.InternalRemoveSelectedNode(node); + if (m_owner.ActiveNode == node) + { + m_owner.SetActiveNode(null); + } + m_owner.RecordNodeConnectionsRemoved(connectionOperations); + m_owner.RecordNodeRemoved(node, nIndex, nodeState); + m_owner.OnNodeRemoved(new STNodeEditorEventArgs(node)); + _Count--; + int i = nIndex; + for (int count = _Count; i < count; i++) + { + m_nodes[i] = m_nodes[i + 1]; + } + m_nodes[_Count] = null; + if (_Count == 0) + { + m_owner.ScaleCanvas(1f, 0f, 0f); + m_owner.MoveCanvas(10f, 10f, bAnimation: true, CanvasMoveArgs.All); + } + else + { + m_owner.Invalidate(); + m_owner.BuildBounds(); + } + } + + public void CopyTo(Array array, int index) + { + if (array == null) + { + throw new ArgumentNullException("数组不能为空"); + } + m_nodes.CopyTo(array, index); + } + + public IEnumerator GetEnumerator() + { + int i = 0; + for (int Len = _Count; i < Len; i++) + { + yield return m_nodes[i]; + } + } + + private void EnsureSpace(int elements) + { + if (elements + _Count > m_nodes.Length) + { + STNode[] array = new STNode[Math.Max(m_nodes.Length * 2, elements + _Count)]; + m_nodes.CopyTo(array, 0); + m_nodes = array; + } + } + + int IList.Add(object value) + { + return Add((STNode)value); + } + + void IList.Clear() + { + Clear(); + } + + bool IList.Contains(object value) + { + return Contains((STNode)value); + } + + int IList.IndexOf(object value) + { + return IndexOf((STNode)value); + } + + void IList.Insert(int index, object value) + { + Insert(index, (STNode)value); + } + + void IList.Remove(object value) + { + Remove((STNode)value); + } + + void IList.RemoveAt(int index) + { + RemoveAt(index); + } + + void ICollection.CopyTo(Array array, int index) + { + CopyTo(array, index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public STNode[] ToArray() + { + STNode[] array = new STNode[_Count]; + for (int i = 0; i < array.Length; i++) + { + array[i] = m_nodes[i]; + } + return array; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeConstant.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeConstant.cs new file mode 100644 index 0000000..fa5bb0c --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeConstant.cs @@ -0,0 +1,12 @@ +using System; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeConstant +{ + public const byte Version = 1; + + public static byte[] NodeFlag = new byte[4] { 83, 84, 78, 68 }; + + public static int NodeFlagInt = BitConverter.ToInt32(NodeFlag, 0); +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControl.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControl.cs new file mode 100644 index 0000000..38402bd --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControl.cs @@ -0,0 +1,470 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; +using System.Windows.Input; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeControl +{ + private STNode _Owner; + + private int _Left; + + private int _Top; + + private int _Width; + + private int _Height; + + private Color _BackColor = Color.FromArgb(127, 0, 0, 0); + + private Color _ForeColor = Color.White; + + private string _Text = "STNCTRL"; + + private Font _Font; + + private bool _Enabled = true; + + private bool _Visable = true; + + protected StringFormat m_sf; + + public STNode Owner + { + get + { + return _Owner; + } + internal set + { + _Owner = value; + } + } + + public int Left + { + get + { + return _Left; + } + set + { + _Left = value; + OnMove(EventArgs.Empty); + Invalidate(); + } + } + + public int Top + { + get + { + return _Top; + } + set + { + _Top = value; + OnMove(EventArgs.Empty); + Invalidate(); + } + } + + public int Width + { + get + { + return _Width; + } + set + { + _Width = value; + OnResize(EventArgs.Empty); + Invalidate(); + } + } + + public int Height + { + get + { + return _Height; + } + set + { + _Height = value; + OnResize(EventArgs.Empty); + Invalidate(); + } + } + + public int Right => _Left + _Width; + + public int Bottom => _Top + _Height; + + public Point Location + { + get + { + return new Point(_Left, _Top); + } + set + { + Left = value.X; + Top = value.Y; + } + } + + public Size Size + { + get + { + return new Size(_Width, _Height); + } + set + { + Width = value.Width; + Height = value.Height; + } + } + + public Rectangle DisplayRectangle + { + get + { + return new Rectangle(_Left, _Top, _Width, _Height); + } + set + { + Left = value.X; + Top = value.Y; + Width = value.Width; + Height = value.Height; + } + } + + public Rectangle ClientRectangle => new Rectangle(0, 0, _Width, _Height); + + public Color BackColor + { + get + { + return _BackColor; + } + set + { + _BackColor = value; + Invalidate(); + } + } + + public Color ForeColor + { + get + { + return _ForeColor; + } + set + { + _ForeColor = value; + Invalidate(); + } + } + + public string Text + { + get + { + return _Text; + } + set + { + _Text = value; + Invalidate(); + } + } + + public Font Font + { + get + { + return _Font; + } + set + { + if (value != _Font) + { + if (value == null) + { + throw new ArgumentNullException("值不能为空"); + } + _Font = value; + Invalidate(); + } + } + } + + public bool Enabled + { + get + { + return _Enabled; + } + set + { + if (value != _Enabled) + { + _Enabled = value; + Invalidate(); + } + } + } + + public bool Visable + { + get + { + return _Visable; + } + set + { + if (value != _Visable) + { + _Visable = value; + Invalidate(); + } + } + } + + public event EventHandler GotFocus; + + public event EventHandler LostFocus; + + public event EventHandler MouseEnter; + + public event EventHandler MouseLeave; + + public event STNodeMouseEventHandler MouseDown; + + public event STNodeMouseEventHandler MouseMove; + + public event STNodeMouseEventHandler MouseUp; + + public event STNodeMouseEventHandler MouseClick; + + public event STNodeMouseEventHandler MouseWheel; + + public event STNodeMouseEventHandler MouseHWheel; + + public event KeyEventHandler KeyDown; + + public event KeyEventHandler KeyUp; + + public event STNodeKeyPressEventHandler KeyPress; + + public event EventHandler Move; + + public event EventHandler Resize; + + public event STNodeControlPaintEventHandler Paint; + + public STNodeControl() + { + m_sf = new StringFormat(); + m_sf.Alignment = StringAlignment.Center; + m_sf.LineAlignment = StringAlignment.Center; + _Font = new Font("courier new", 8.25f); + Width = 75; + Height = 23; + } + + protected internal virtual void OnPaint(DrawingTools dt) + { + Graphics graphics = dt.Graphics; + SolidBrush solidBrush = dt.SolidBrush; + graphics.SmoothingMode = SmoothingMode.None; + graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; + solidBrush.Color = _BackColor; + graphics.FillRectangle(solidBrush, 0, 0, Width, Height); + if (!string.IsNullOrEmpty(_Text)) + { + solidBrush.Color = _ForeColor; + graphics.DrawString(_Text, _Font, solidBrush, ClientRectangle, m_sf); + } + if (this.Paint != null) + { + this.Paint(this, new STNodeControlPaintEventArgs(dt)); + } + } + + public void Invalidate() + { + if (_Owner != null) + { + _Owner.Invalidate(new Rectangle(_Left, _Top + _Owner.TitleHeight, Width, Height)); + } + } + + public void Invalidate(Rectangle rect) + { + if (_Owner != null) + { + _Owner.Invalidate(RectangleToParent(rect)); + } + } + + public Rectangle RectangleToParent(Rectangle rect) + { + return new Rectangle(_Left, _Top + _Owner.TitleHeight, Width, Height); + } + + protected internal virtual void OnGotFocus(EventArgs e) + { + if (this.GotFocus != null) + { + this.GotFocus(this, e); + } + } + + protected internal virtual void OnLostFocus(EventArgs e) + { + if (this.LostFocus != null) + { + this.LostFocus(this, e); + } + } + + protected internal virtual void OnMouseEnter(EventArgs e) + { + if (this.MouseEnter != null) + { + this.MouseEnter(this, e); + } + } + + protected internal virtual void OnMouseLeave(EventArgs e) + { + if (this.MouseLeave != null) + { + this.MouseLeave(this, e); + } + } + + protected internal virtual void OnMouseDown(STNodeMouseEventArgs e) + { + if (this.MouseDown != null) + { + this.MouseDown(this, e); + } + } + + protected internal virtual void OnMouseMove(STNodeMouseEventArgs e) + { + if (this.MouseMove != null) + { + this.MouseMove(this, e); + } + } + + protected internal virtual void OnMouseUp(STNodeMouseEventArgs e) + { + if (this.MouseUp != null) + { + this.MouseUp(this, e); + } + } + + protected internal virtual void OnMouseClick(STNodeMouseEventArgs e) + { + if (this.MouseClick != null) + { + this.MouseClick(this, e); + } + } + + protected internal virtual void OnMouseWheel(STNodeMouseEventArgs e) + { + if (this.MouseWheel != null) + { + this.MouseWheel(this, e); + } + } + + protected internal virtual void OnMouseHWheel(STNodeMouseEventArgs e) + { + if (this.MouseHWheel != null) + { + this.MouseHWheel(this, e); + } + } + + protected internal virtual void OnKeyDown(KeyEventArgs e) + { + if (this.KeyDown != null) + { + this.KeyDown(this, e); + } + } + + protected internal virtual void OnKeyUp(KeyEventArgs e) + { + if (this.KeyUp != null) + { + this.KeyUp(this, e); + } + } + + protected internal virtual void OnKeyPress(STNodeKeyPressEventArgs e) + { + if (this.KeyPress != null) + { + this.KeyPress(this, e); + } + } + + protected internal virtual void OnMove(EventArgs e) + { + if (this.Move != null) + { + this.Move(this, e); + } + } + + protected internal virtual void OnResize(EventArgs e) + { + if (this.Resize != null) + { + this.Resize(this, e); + } + } + + public IAsyncResult BeginInvoke(Delegate method) + { + return BeginInvoke(method, null); + } + + public IAsyncResult BeginInvoke(Delegate method, params object[] args) + { + if (_Owner == null) + { + return null; + } + return _Owner.BeginInvoke(method, args); + } + + public object Invoke(Delegate method) + { + return Invoke(method, null); + } + + public object Invoke(Delegate method, params object[] args) + { + if (_Owner == null) + { + return null; + } + return _Owner.Invoke(method, args); + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControlCollection.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControlCollection.cs new file mode 100644 index 0000000..406544e --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControlCollection.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeControlCollection : IList, ICollection, IEnumerable +{ + private int _Count; + + private STNodeControl[] m_controls; + + private STNode m_owner; + + public int Count => _Count; + + public bool IsFixedSize => false; + + public bool IsReadOnly => false; + + public STNodeControl this[int index] + { + get + { + if (index < 0 || index >= _Count) + { + throw new IndexOutOfRangeException("索引越界"); + } + return m_controls[index]; + } + set + { + throw new InvalidOperationException("禁止重新赋值元素"); + } + } + + public bool IsSynchronized => true; + + public object SyncRoot => this; + + bool IList.IsFixedSize => IsFixedSize; + + bool IList.IsReadOnly => IsReadOnly; + + object IList.this[int index] + { + get + { + return this[index]; + } + set + { + this[index] = (STNodeControl)value; + } + } + + int ICollection.Count => _Count; + + bool ICollection.IsSynchronized => IsSynchronized; + + object ICollection.SyncRoot => SyncRoot; + + internal STNodeControlCollection(STNode owner) + { + if (owner == null) + { + throw new ArgumentNullException("所有者不能为空"); + } + m_owner = owner; + m_controls = new STNodeControl[4]; + } + + public int Add(STNodeControl control) + { + if (control == null) + { + throw new ArgumentNullException("添加对象不能为空"); + } + EnsureSpace(1); + int num = IndexOf(control); + if (-1 == num) + { + num = _Count; + control.Owner = m_owner; + m_controls[_Count++] = control; + Redraw(); + } + return num; + } + + public void AddRange(STNodeControl[] controls) + { + if (controls == null) + { + throw new ArgumentNullException("添加对象不能为空"); + } + EnsureSpace(controls.Length); + foreach (STNodeControl sTNodeControl in controls) + { + if (sTNodeControl == null) + { + throw new ArgumentNullException("添加对象不能为空"); + } + if (-1 == IndexOf(sTNodeControl)) + { + sTNodeControl.Owner = m_owner; + m_controls[_Count++] = sTNodeControl; + } + } + Redraw(); + } + + public void Clear() + { + for (int i = 0; i < _Count; i++) + { + m_controls[i].Owner = null; + } + _Count = 0; + m_controls = new STNodeControl[4]; + Redraw(); + } + + public bool Contains(STNodeControl option) + { + return IndexOf(option) != -1; + } + + public int IndexOf(STNodeControl option) + { + return Array.IndexOf(m_controls, option); + } + + public void Insert(int index, STNodeControl control) + { + if (index < 0 || index >= _Count) + { + throw new IndexOutOfRangeException("索引越界"); + } + if (control == null) + { + throw new ArgumentNullException("插入对象不能为空"); + } + EnsureSpace(1); + for (int num = _Count; num > index; num--) + { + m_controls[num] = m_controls[num - 1]; + } + control.Owner = m_owner; + m_controls[index] = control; + _Count++; + Redraw(); + } + + public void Remove(STNodeControl control) + { + int num = IndexOf(control); + if (num != -1) + { + RemoveAt(num); + } + } + + public void RemoveAt(int index) + { + if (index < 0 || index >= _Count) + { + throw new IndexOutOfRangeException("索引越界"); + } + _Count--; + m_controls[index].Owner = null; + int i = index; + for (int count = _Count; i < count; i++) + { + m_controls[i] = m_controls[i + 1]; + } + Redraw(); + } + + public void CopyTo(Array array, int index) + { + if (array == null) + { + throw new ArgumentNullException("数组不能为空"); + } + m_controls.CopyTo(array, index); + } + + public IEnumerator GetEnumerator() + { + int i = 0; + for (int Len = _Count; i < Len; i++) + { + yield return m_controls[i]; + } + } + + private void EnsureSpace(int elements) + { + if (elements + _Count > m_controls.Length) + { + STNodeControl[] array = new STNodeControl[Math.Max(m_controls.Length * 2, elements + _Count)]; + m_controls.CopyTo(array, 0); + m_controls = array; + } + } + + protected void Redraw() + { + if (m_owner != null && m_owner.Owner != null) + { + m_owner.Owner.Invalidate(m_owner.Owner.CanvasToControl(m_owner.Rectangle)); + } + } + + int IList.Add(object value) + { + return Add((STNodeControl)value); + } + + void IList.Clear() + { + Clear(); + } + + bool IList.Contains(object value) + { + return Contains((STNodeControl)value); + } + + int IList.IndexOf(object value) + { + return IndexOf((STNodeControl)value); + } + + void IList.Insert(int index, object value) + { + Insert(index, (STNodeControl)value); + } + + void IList.Remove(object value) + { + Remove((STNodeControl)value); + } + + void IList.RemoveAt(int index) + { + RemoveAt(index); + } + + void ICollection.CopyTo(Array array, int index) + { + CopyTo(array, index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControlPaintEventArgs.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControlPaintEventArgs.cs new file mode 100644 index 0000000..af441c0 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControlPaintEventArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeControlPaintEventArgs : EventArgs +{ + public DrawingTools DrawingTools { get; private set; } + + public STNodeControlPaintEventArgs(DrawingTools dt) + { + DrawingTools = dt; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControlPaintEventHandler.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControlPaintEventHandler.cs new file mode 100644 index 0000000..9487145 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeControlPaintEventHandler.cs @@ -0,0 +1,3 @@ +namespace ST.Library.UI.NodeEditor; + +public delegate void STNodeControlPaintEventHandler(object sender, STNodeControlPaintEventArgs e); diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditor.Clipboard.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditor.Clipboard.cs new file mode 100644 index 0000000..a76edf9 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditor.Clipboard.cs @@ -0,0 +1,564 @@ +#pragma warning disable CA1859 +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Windows.Input; + +namespace ST.Library.UI.NodeEditor; + +public partial class STNodeEditor +{ + public const string ClipboardFormatV1 = "STNodeEditor_Nodes_V1"; + + private const int MaximumImportedNodeCount = 10000; + private const int MaximumImportedConnectionCount = 100000; + private const int MaximumNodeDataLength = 16 * 1024 * 1024; + private const long MaximumTotalNodeDataLength = 128L * 1024 * 1024; + private const int MaximumDecompressedGraphLength = 160 * 1024 * 1024; + private static readonly uint[] Crc32Table = CreateCrc32Table(); + + private sealed class GraphConnectionReference + { + public STNodeOption Output { get; } + + public STNodeOption Input { get; } + + public GraphConnectionReference(STNodeOption output, STNodeOption input) + { + Output = output; + Input = input; + } + } + + private sealed class GraphImportPlan + { + public List Nodes { get; } + + public List Connections { get; } + + public Point SourceOrigin { get; } + + public GraphImportPlan(List nodes, List connections) + { + Nodes = nodes; + Connections = connections; + SourceOrigin = nodes.Count == 0 + ? Point.Empty + : new Point(nodes.Min(node => node.Left), nodes.Min(node => node.Top)); + } + } + + public byte[] GetSelectedNodesData() + { + return GetNodesData(GetSelectedNode()); + } + + public byte[] GetNodesData(IEnumerable nodes) + { + if (nodes == null) + { + throw new ArgumentNullException(nameof(nodes)); + } + HashSet requestedNodes = new HashSet(nodes); + List orderedNodes = Nodes + .Cast() + .Where(requestedNodes.Contains) + .ToList(); + if (orderedNodes.Count == 0) + { + return Array.Empty(); + } + + Dictionary optionIndexes = BuildOptionIndexes(orderedNodes); + List connections = GetInternalConnections(orderedNodes, requestedNodes, optionIndexes); + using MemoryStream stream = new MemoryStream(); + using (GZipStream gzip = new GZipStream(stream, CompressionMode.Compress, leaveOpen: true)) + { + WriteInt32(gzip, orderedNodes.Count); + WriteInt32(gzip, orderedNodes.Min(node => node.Left)); + WriteInt32(gzip, orderedNodes.Min(node => node.Top)); + foreach (STNode node in orderedNodes) + { + byte[] nodeData = node.GetSaveData(); + WriteInt32(gzip, nodeData.Length); + gzip.Write(nodeData, 0, nodeData.Length); + } + WriteInt32(gzip, connections.Count); + foreach (GraphConnectionReference connection in connections) + { + long packed = optionIndexes[connection.Output] << 32 + | optionIndexes[connection.Input] & uint.MaxValue; + byte[] bytes = BitConverter.GetBytes(packed); + gzip.Write(bytes, 0, bytes.Length); + } + } + return stream.ToArray(); + } + + public IReadOnlyList ImportSelectionData(byte[] data, Point targetCanvasPoint) + { + GraphImportPlan plan = DecodeSelectionData(data); + MovePlanTo(plan, targetCanvasPoint); + return CommitImportPlan(plan, "粘贴节点"); + } + + public IReadOnlyList ImportSelectionData(byte[] data) + { + GraphImportPlan plan = DecodeSelectionData(data); + foreach (STNode node in plan.Nodes) + { + MoveDetachedNode(node, node.Left + 30, node.Top + 30); + } + return CommitImportPlan(plan, "粘贴节点"); + } + + public IReadOnlyList ImportCanvasAsModule(byte[] canvasData, Point targetCanvasPoint) + { + GraphImportPlan plan = DecodeCanvasData(canvasData); + MovePlanTo(plan, targetCanvasPoint); + return CommitImportPlan(plan, "导入流程模块"); + } + + public bool CopySelectionToClipboard() + { + byte[] data = GetSelectedNodesData(); + if (data.Length == 0) + { + return false; + } + try + { + System.Windows.Clipboard.SetData(ClipboardFormatV1, Convert.ToBase64String(data)); + return true; + } + catch + { + return false; + } + } + + public bool CutSelectionToClipboard() + { + if (!EnableEdit || !CopySelectionToClipboard()) + { + return false; + } + return DeleteSelectedNodes(); + } + + public IReadOnlyList PasteFromClipboard() + { + if (!EnableEdit || !ClipboardContainsGraph()) + { + return Array.Empty(); + } + string base64 = System.Windows.Clipboard.GetData(ClipboardFormatV1) as string; + if (string.IsNullOrWhiteSpace(base64)) + { + return Array.Empty(); + } + byte[] data = Convert.FromBase64String(base64); + if (IsMouseOver) + { + System.Windows.Point position = Mouse.GetPosition(this); + Point target = ControlToCanvas(new Point((int)Math.Round(position.X), (int)Math.Round(position.Y))); + return ImportSelectionData(data, target); + } + return ImportSelectionData(data); + } + + private void ExecutePasteCommand() + { + try + { + PasteFromClipboard(); + } + catch + { + // Routed commands must not tear down the WPF input pipeline. + } + } + + internal bool ClipboardContainsGraph() + { + try + { + return System.Windows.Clipboard.ContainsData(ClipboardFormatV1); + } + catch + { + return false; + } + } + + private GraphImportPlan DecodeSelectionData(byte[] data) + { + if (data == null || data.Length == 0) + { + throw new InvalidDataException("节点数据为空"); + } + using MemoryStream stream = new MemoryStream(DecompressGZip(data, 0), writable: false); + int nodeCount = ReadCount(stream, MaximumImportedNodeCount, "节点"); + ReadInt32(stream); + ReadInt32(stream); + return DecodeGraphBody(stream, nodeCount); + } + + private GraphImportPlan DecodeCanvasData(byte[] data) + { + if (data == null || data.Length < STNodeConstant.NodeFlag.Length + 1) + { + throw new InvalidDataException("流程模块数据为空或不完整"); + } + byte[] header = new byte[STNodeConstant.NodeFlag.Length + 1]; + Array.Copy(data, header, header.Length); + for (int i = 0; i < STNodeConstant.NodeFlag.Length; i++) + { + if (header[i] != STNodeConstant.NodeFlag[i]) + { + throw new InvalidDataException("无法识别的流程模块格式"); + } + } + if (header[STNodeConstant.NodeFlag.Length] != STNodeConstant.Version) + { + throw new InvalidDataException("无法识别的流程模块版本"); + } + using MemoryStream stream = new MemoryStream(DecompressGZip(data, header.Length), writable: false); + ReadBytes(stream, 12); + int nodeCount = ReadCount(stream, MaximumImportedNodeCount, "节点"); + return DecodeGraphBody(stream, nodeCount); + } + + private GraphImportPlan DecodeGraphBody(Stream stream, int nodeCount) + { + List nodes = new List(nodeCount); + Dictionary options = new Dictionary(); + long totalNodeDataLength = 0; + for (int i = 0; i < nodeCount; i++) + { + int nodeDataLength = ReadInt32(stream); + if (nodeDataLength <= 0 || nodeDataLength > MaximumNodeDataLength) + { + throw new InvalidDataException($"节点数据长度无效:{nodeDataLength}"); + } + totalNodeDataLength += nodeDataLength; + if (totalNodeDataLength > MaximumTotalNodeDataLength) + { + throw new InvalidDataException("节点数据总长度超过限制"); + } + byte[] nodeData = ReadBytes(stream, nodeDataLength); + STNode node; + try + { + node = GetNodeFromData(nodeData); + node.RegenerateGuid(); + } + catch (Exception ex) + { + throw new InvalidDataException($"第 {i + 1} 个节点无法加载", ex); + } + nodes.Add(node); + AddOptions(options, node); + } + + int connectionCount = ReadCount(stream, MaximumImportedConnectionCount, "连接"); + List connections = new List(connectionCount); + HashSet connectionKeys = new HashSet(StringComparer.Ordinal); + for (int i = 0; i < connectionCount; i++) + { + long packed = BitConverter.ToInt64(ReadBytes(stream, 8), 0); + long outputIndex = packed >> 32; + long inputIndex = unchecked((uint)packed); + if (!options.TryGetValue(outputIndex, out STNodeOption output) + || !options.TryGetValue(inputIndex, out STNodeOption input)) + { + throw new InvalidDataException($"第 {i + 1} 条连接引用了不存在的端口"); + } + if (output.IsInput || !input.IsInput || output.Owner == input.Owner) + { + throw new InvalidDataException($"第 {i + 1} 条连接方向无效"); + } + string key = outputIndex + ":" + inputIndex; + if (!connectionKeys.Add(key)) + { + throw new InvalidDataException($"第 {i + 1} 条连接重复"); + } + connections.Add(new GraphConnectionReference(output, input)); + } + if (stream.ReadByte() != -1) + { + throw new InvalidDataException("节点数据包含未识别的尾部内容"); + } + return new GraphImportPlan(nodes, connections); + } + + private IReadOnlyList CommitImportPlan(GraphImportPlan plan, string description) + { + if (plan.Nodes.Count == 0) + { + return Array.Empty(); + } + STNode[] selectedBefore = GetSelectedNode(); + STNode activeBefore = ActiveNode; + List addedNodes = new List(plan.Nodes.Count); + using STNodeEditTransaction transaction = BeginEditTransaction(description); + try + { + foreach (STNode node in plan.Nodes) + { + try + { + Nodes.Add(node); + } + finally + { + if (Nodes.Contains(node)) + { + addedNodes.Add(node); + } + } + } + foreach (GraphConnectionReference connection in plan.Connections) + { + bool outputLocked = connection.Output.Owner.LockOption; + bool inputLocked = connection.Input.Owner.LockOption; + connection.Output.Owner.LockOption = false; + connection.Input.Owner.LockOption = false; + try + { + ConnectionStatus status = connection.Output.ConnectOption(connection.Input); + if (status != ConnectionStatus.Connected) + { + throw new InvalidOperationException($"无法恢复导入节点的连接:{status}"); + } + } + finally + { + connection.Output.Owner.LockOption = outputLocked; + connection.Input.Owner.LockOption = inputLocked; + } + } + foreach (STNode node in addedNodes) + { + node.OnEditorLoadCompleted(); + } + foreach (STNode node in selectedBefore) + { + RemoveSelectedNode(node); + } + foreach (STNode node in addedNodes) + { + AddSelectedNode(node); + } + SetActiveNode(addedNodes[addedNodes.Count - 1]); + Invalidate(); + return addedNodes.AsReadOnly(); + } + catch + { + transaction.Cancel(); + ReplayHistory(() => + { + for (int i = addedNodes.Count - 1; i >= 0; i--) + { + Nodes.Remove(addedNodes[i]); + } + }); + foreach (STNode node in selectedBefore) + { + if (Nodes.Contains(node)) + { + AddSelectedNode(node); + } + } + SetActiveNode(activeBefore != null && Nodes.Contains(activeBefore) ? activeBefore : null); + throw; + } + } + + private static void MovePlanTo(GraphImportPlan plan, Point target) + { + int offsetX = target.X - plan.SourceOrigin.X; + int offsetY = target.Y - plan.SourceOrigin.Y; + foreach (STNode node in plan.Nodes) + { + MoveDetachedNode(node, node.Left + offsetX, node.Top + offsetY); + } + } + + private static void MoveDetachedNode(STNode node, int left, int top) + { + bool locked = node.LockLocation; + node.LockLocation = false; + node.Location = new Point(left, top); + node.LockLocation = locked; + } + + private static Dictionary BuildOptionIndexes(IEnumerable nodes) + { + Dictionary indexes = new Dictionary(); + foreach (STNode node in nodes) + { + foreach (STNodeOption option in node.GetAllInputOptions()) + { + if (option != null && !indexes.ContainsKey(option)) + { + indexes.Add(option, indexes.Count); + } + } + foreach (STNodeOption option in node.GetAllOutputOptions()) + { + if (option != null && !indexes.ContainsKey(option)) + { + indexes.Add(option, indexes.Count); + } + } + } + return indexes; + } + + private static List GetInternalConnections( + IEnumerable nodes, + HashSet nodeSet, + Dictionary optionIndexes) + { + List connections = new List(); + foreach (STNode node in nodes) + { + foreach (STNodeOption output in node.GetAllOutputOptions()) + { + if (!optionIndexes.ContainsKey(output)) + { + continue; + } + IEnumerable inputs = output.ConnectedOption + .Where(input => input != null && input.IsInput && nodeSet.Contains(input.Owner) && optionIndexes.ContainsKey(input)) + .OrderBy(input => optionIndexes[input]); + foreach (STNodeOption input in inputs) + { + connections.Add(new GraphConnectionReference(output, input)); + } + } + } + return connections; + } + + private static void AddOptions(Dictionary options, STNode node) + { + foreach (STNodeOption option in node.GetAllInputOptions()) + { + if (option != null) + { + options.Add(options.Count, option); + } + } + foreach (STNodeOption option in node.GetAllOutputOptions()) + { + if (option != null) + { + options.Add(options.Count, option); + } + } + } + + private static int ReadCount(Stream stream, int maximum, string valueName) + { + int count = ReadInt32(stream); + if (count < 0 || count > maximum) + { + throw new InvalidDataException($"{valueName}数量无效:{count}"); + } + return count; + } + + private static int ReadInt32(Stream stream) + { + return BitConverter.ToInt32(ReadBytes(stream, 4), 0); + } + + private static byte[] ReadBytes(Stream stream, int count) + { + byte[] buffer = new byte[count]; + int offset = 0; + while (offset < count) + { + int read = stream.Read(buffer, offset, count - offset); + if (read <= 0) + { + throw new EndOfStreamException("节点数据意外结束"); + } + offset += read; + } + return buffer; + } + + private static void WriteInt32(Stream stream, int value) + { + byte[] bytes = BitConverter.GetBytes(value); + stream.Write(bytes, 0, bytes.Length); + } + + private static byte[] DecompressGZip(byte[] data, int offset) + { + int compressedLength = data.Length - offset; + if (compressedLength < 18) + { + throw new InvalidDataException("压缩节点数据不完整"); + } + using MemoryStream input = new MemoryStream(data, offset, compressedLength, writable: false); + using GZipStream gzip = new GZipStream(input, CompressionMode.Decompress); + using MemoryStream output = new MemoryStream(); + byte[] buffer = new byte[81920]; + while (true) + { + int read = gzip.Read(buffer, 0, buffer.Length); + if (read <= 0) + { + break; + } + if (output.Length + read > MaximumDecompressedGraphLength) + { + throw new InvalidDataException("解压后的节点数据超过限制"); + } + output.Write(buffer, 0, read); + } + + byte[] decompressed = output.ToArray(); + uint expectedCrc = BitConverter.ToUInt32(data, data.Length - 8); + uint expectedLength = BitConverter.ToUInt32(data, data.Length - 4); + if (expectedLength != unchecked((uint)decompressed.Length) || expectedCrc != ComputeCrc32(decompressed)) + { + throw new InvalidDataException("压缩节点数据校验失败"); + } + return decompressed; + } + + private static uint ComputeCrc32(byte[] data) + { + uint crc = uint.MaxValue; + foreach (byte value in data) + { + crc = Crc32Table[(crc ^ value) & byte.MaxValue] ^ crc >> 8; + } + return ~crc; + } + + private static uint[] CreateCrc32Table() + { + uint[] table = new uint[256]; + for (uint i = 0; i < table.Length; i++) + { + uint value = i; + for (int bit = 0; bit < 8; bit++) + { + value = (value & 1) != 0 ? 0xEDB88320u ^ value >> 1 : value >> 1; + } + table[i] = value; + } + return table; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditor.Edit.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditor.Edit.cs new file mode 100644 index 0000000..58c65a9 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditor.Edit.cs @@ -0,0 +1,1078 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Windows.Input; + +namespace ST.Library.UI.NodeEditor; + +public sealed class STNodeEditHistoryEntry +{ + internal ISTNodeEditOperation Operation { get; } + + internal long BeforeStateId { get; } + + internal long AfterStateId { get; set; } + + internal DateTime LastChangedUtc { get; set; } + + public string Description { get; } + + internal STNodeEditHistoryEntry(string description, ISTNodeEditOperation operation, long beforeStateId, long afterStateId) + { + Description = string.IsNullOrWhiteSpace(description) ? "编辑流程" : description; + Operation = operation; + BeforeStateId = beforeStateId; + AfterStateId = afterStateId; + LastChangedUtc = DateTime.UtcNow; + } + + public override string ToString() + { + return Description; + } +} + +public sealed class STNodeEditTransaction : IDisposable +{ + private STNodeEditor _editor; + private readonly bool _active; + private bool _cancelled; + + internal STNodeEditTransaction(STNodeEditor editor, bool active) + { + _editor = editor; + _active = active; + } + + public void Cancel() + { + _cancelled = true; + } + + public void Dispose() + { + STNodeEditor editor = _editor; + _editor = null; + if (_active && editor != null) + { + editor.EndEditTransaction(_cancelled); + } + } +} + +internal interface ISTNodeEditOperation +{ + void Undo(STNodeEditor editor); + + void Redo(STNodeEditor editor); + + bool TryMerge(ISTNodeEditOperation operation); +} + +internal sealed class STNodeCompositeEditOperation : ISTNodeEditOperation +{ + private readonly IReadOnlyList _operations; + + public STNodeCompositeEditOperation(IReadOnlyList operations) + { + _operations = operations; + } + + public void Undo(STNodeEditor editor) + { + for (int i = _operations.Count - 1; i >= 0; i--) + { + _operations[i].Undo(editor); + } + } + + public void Redo(STNodeEditor editor) + { + for (int i = 0; i < _operations.Count; i++) + { + _operations[i].Redo(editor); + } + } + + public bool TryMerge(ISTNodeEditOperation operation) + { + return false; + } +} + +internal sealed class STNodeAddedEditOperation : ISTNodeEditOperation +{ + private readonly STNode _node; + private readonly int _index; + + public STNodeAddedEditOperation(STNode node, int index) + { + _node = node; + _index = index; + } + + public void Undo(STNodeEditor editor) + { + editor.Nodes.Remove(_node); + } + + public void Redo(STNodeEditor editor) + { + editor.Nodes.Insert(Math.Min(_index, editor.Nodes.Count), _node); + } + + public bool TryMerge(ISTNodeEditOperation operation) + { + return false; + } +} + +internal sealed class STNodeRemovedEditOperation : ISTNodeEditOperation +{ + private readonly STNode _node; + private readonly int _index; + private readonly Dictionary _state; + + public STNodeRemovedEditOperation(STNode node, int index, Dictionary state) + { + _node = node; + _index = index; + _state = state; + } + + public void Undo(STNodeEditor editor) + { + _node.OnLoadNode(CloneState(_state)); + editor.Nodes.Insert(Math.Min(_index, editor.Nodes.Count), _node); + } + + public void Redo(STNodeEditor editor) + { + editor.Nodes.Remove(_node); + } + + public bool TryMerge(ISTNodeEditOperation operation) + { + return false; + } + + private static Dictionary CloneState(Dictionary state) + { + return state.ToDictionary(pair => pair.Key, pair => (byte[])pair.Value.Clone()); + } +} + +internal sealed class STNodeOptionReference +{ + private readonly STNode _node; + private readonly bool _isInput; + private readonly int _index; + private readonly STNodeOption _option; + private readonly int _optionCount; + + private STNodeOptionReference(STNode node, bool isInput, int index, STNodeOption option, int optionCount) + { + _node = node; + _isInput = isInput; + _index = index; + _option = option; + _optionCount = optionCount; + } + + public static STNodeOptionReference Create(STNodeOption option) + { + if (option == null || option.Owner == null) + { + return null; + } + STNodeOption[] options = option.IsInput + ? option.Owner.GetAllInputOptions() + : option.Owner.GetAllOutputOptions(); + int index = Array.IndexOf(options, option); + return index < 0 ? null : new STNodeOptionReference(option.Owner, option.IsInput, index, option, options.Length); + } + + public STNodeOption Resolve(Dictionary state = null) + { + STNodeOption[] options = _isInput ? _node.GetAllInputOptions() : _node.GetAllOutputOptions(); + int currentIndex = Array.IndexOf(options, _option); + if (currentIndex >= 0) + { + return options[currentIndex]; + } + if (state != null && _option.Owner == null) + { + STNodeOptionCollection collection = _isInput ? _node.InputOptions : _node.OutputOptions; + if (collection.Count >= _optionCount && _index < collection.Count) + { + STNodeOption replacement = collection[_index]; + if (replacement.ConnectionCount == 0) + { + collection.RemoveAt(_index); + } + } + collection.Insert(Math.Min(_index, collection.Count), _option); + return _option; + } + if ((_index < 0 || _index >= options.Length) && state != null) + { + _node.OnLoadNode(state.ToDictionary(pair => pair.Key, pair => (byte[])pair.Value.Clone())); + options = _isInput ? _node.GetAllInputOptions() : _node.GetAllOutputOptions(); + } + if (_index < 0 || _index >= options.Length) + { + throw new InvalidOperationException("无法恢复节点端口,端口结构已发生变化"); + } + return options[_index]; + } +} + +internal sealed class STNodeConnectionEditOperation : ISTNodeEditOperation +{ + private readonly STNodeOptionReference _output; + private readonly STNodeOptionReference _input; + private readonly bool _connected; + private readonly Dictionary _outputStateBefore; + private readonly Dictionary _inputStateBefore; + + public STNodeConnectionEditOperation( + STNodeOptionReference output, + STNodeOptionReference input, + bool connected, + Dictionary outputStateBefore, + Dictionary inputStateBefore) + { + _output = output; + _input = input; + _connected = connected; + _outputStateBefore = outputStateBefore; + _inputStateBefore = inputStateBefore; + } + + public void Undo(STNodeEditor editor) + { + SetConnected(!_connected); + } + + public void Redo(STNodeEditor editor) + { + SetConnected(_connected); + } + + private void SetConnected(bool connected) + { + STNodeOption output = _output.Resolve(connected ? _outputStateBefore : null); + STNodeOption input = _input.Resolve(connected ? _inputStateBefore : null); + bool currentlyConnected = output.ConnectedOption.Contains(input) && input.ConnectedOption.Contains(output); + if (currentlyConnected == connected) + { + return; + } + + STNode outputNode = output.Owner; + STNode inputNode = input.Owner; + bool outputLocked = outputNode.LockOption; + bool inputLocked = inputNode.LockOption; + outputNode.LockOption = false; + inputNode.LockOption = false; + try + { + ConnectionStatus status = connected + ? output.ConnectOption(input) + : output.DisConnectOption(input); + ConnectionStatus expected = connected ? ConnectionStatus.Connected : ConnectionStatus.DisConnected; + if (status != expected) + { + throw new InvalidOperationException($"无法{(connected ? "恢复" : "撤销")}节点连接:{status}"); + } + } + finally + { + outputNode.LockOption = outputLocked; + inputNode.LockOption = inputLocked; + } + } + + public bool TryMerge(ISTNodeEditOperation operation) + { + return false; + } +} + +internal sealed class STNodeMoveEditOperation : ISTNodeEditOperation +{ + private readonly Dictionary _before; + private Dictionary _after; + + public STNodeMoveEditOperation(Dictionary before, Dictionary after) + { + _before = before; + _after = after; + } + + public void Undo(STNodeEditor editor) + { + Apply(editor, _before); + } + + public void Redo(STNodeEditor editor) + { + Apply(editor, _after); + } + + private static void Apply(STNodeEditor editor, Dictionary locations) + { + foreach (KeyValuePair pair in locations) + { + if (!editor.Nodes.Contains(pair.Key)) + { + continue; + } + bool locked = pair.Key.LockLocation; + pair.Key.LockLocation = false; + pair.Key.Location = pair.Value; + pair.Key.LockLocation = locked; + } + editor.BuildBounds(); + editor.BuildLinePath(); + editor.Invalidate(); + } + + public bool TryMerge(ISTNodeEditOperation operation) + { + STNodeMoveEditOperation other = operation as STNodeMoveEditOperation; + if (other == null || _before.Count != other._before.Count || _before.Keys.Any(node => !other._before.ContainsKey(node))) + { + return false; + } + _after = new Dictionary(other._after); + return true; + } +} + +internal sealed class STNodeStateEditOperation : ISTNodeEditOperation +{ + private readonly STNode _node; + private readonly Dictionary _before; + private Dictionary _after; + private readonly string _propertyName; + + public STNodeStateEditOperation(STNode node, Dictionary before, Dictionary after, string propertyName) + { + _node = node; + _before = before; + _after = after; + _propertyName = propertyName ?? string.Empty; + } + + public void Undo(STNodeEditor editor) + { + Apply(editor, _before); + } + + public void Redo(STNodeEditor editor) + { + Apply(editor, _after); + } + + private void Apply(STNodeEditor editor, Dictionary state) + { + if (!editor.Nodes.Contains(_node)) + { + return; + } + _node.OnLoadNode(CloneState(state)); + editor.BuildBounds(); + editor.BuildLinePath(); + editor.Invalidate(); + } + + public bool TryMerge(ISTNodeEditOperation operation) + { + STNodeStateEditOperation other = operation as STNodeStateEditOperation; + if (other == null || !ReferenceEquals(_node, other._node) || !string.Equals(_propertyName, other._propertyName, StringComparison.Ordinal)) + { + return false; + } + _after = CloneState(other._after); + return true; + } + + private static Dictionary CloneState(Dictionary state) + { + return state.ToDictionary(pair => pair.Key, pair => (byte[])pair.Value.Clone()); + } +} + +public partial class STNodeEditor +{ + private const int DefaultMaximumHistoryEntries = 100; + private readonly ObservableCollection _undoHistory = new ObservableCollection(); + private readonly ObservableCollection _redoHistory = new ObservableCollection(); + private readonly Dictionary> _nodeStateCache = new Dictionary>(); + private ReadOnlyObservableCollection _readOnlyUndoHistory; + private ReadOnlyObservableCollection _readOnlyRedoHistory; + private Dictionary _transactionSnapshots; + private List _transactionOperations; + private string _transactionDescription; + private int _transactionDepth; + private int _historySuppressionDepth; + private int _historyReplayDepth; + private bool _transactionCancelled; + private bool _enableHistory; + private long _nextStateId; + private long _currentStateId; + private long _savedStateId; + private STNodeEditTransaction _pointerEditTransaction; + private STNodeOption _pendingConnectionFirst; + private STNodeOption _pendingConnectionSecond; + private STNodeOptionReference _pendingConnectionOutput; + private STNodeOptionReference _pendingConnectionInput; + private Dictionary _pendingConnectionOutputState; + private Dictionary _pendingConnectionInputState; + + private sealed class NodeEditSnapshot + { + public Point Location { get; } + + public Dictionary State { get; } + + public NodeEditSnapshot(Point location, Dictionary state) + { + Location = location; + State = state; + } + } + + public bool EnableHistory + { + get => _enableHistory; + set + { + if (_enableHistory == value) + { + return; + } + _enableHistory = value; + ClearHistory(); + RefreshNodeStateCache(); + } + } + + public int MaximumHistoryEntries { get; set; } = DefaultMaximumHistoryEntries; + + public bool CanUndo => _undoHistory.Count > 0 && _transactionDepth == 0; + + public bool CanRedo => _redoHistory.Count > 0 && _transactionDepth == 0; + + public bool IsModified => _currentStateId != _savedStateId; + + public bool IsReplayingHistory => _historyReplayDepth > 0; + + public ReadOnlyObservableCollection UndoHistory => _readOnlyUndoHistory; + + public ReadOnlyObservableCollection RedoHistory => _readOnlyRedoHistory; + + public event EventHandler HistoryChanged; + + public event EventHandler NodeLocationChanged; + + private void InitializeEditing() + { + _readOnlyUndoHistory = new ReadOnlyObservableCollection(_undoHistory); + _readOnlyRedoHistory = new ReadOnlyObservableCollection(_redoHistory); + CommandBindings.Add(new CommandBinding(ApplicationCommands.Undo, (_, _) => Undo(), (_, e) => e.CanExecute = CanUndo)); + CommandBindings.Add(new CommandBinding(ApplicationCommands.Redo, (_, _) => Redo(), (_, e) => e.CanExecute = CanRedo)); + CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, (_, _) => CutSelectionToClipboard(), (_, e) => e.CanExecute = EnableEdit && GetSelectedNode().Length > 0)); + CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, (_, _) => CopySelectionToClipboard(), (_, e) => e.CanExecute = GetSelectedNode().Length > 0)); + CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, (_, _) => ExecutePasteCommand(), (_, e) => e.CanExecute = EnableEdit && ClipboardContainsGraph())); + CommandBindings.Add(new CommandBinding(ApplicationCommands.Delete, (_, _) => DeleteSelectedNodes(), (_, e) => e.CanExecute = EnableEdit && GetSelectedNode().Length > 0)); + CommandBindings.Add(new CommandBinding(ApplicationCommands.SelectAll, (_, _) => SelectAllNodes(), (_, e) => e.CanExecute = Nodes.Count > 0)); + ClearHistory(); + } + + public STNodeEditTransaction BeginEditTransaction(string description) + { + if (!_enableHistory || _historySuppressionDepth > 0) + { + return new STNodeEditTransaction(this, active: false); + } + if (_transactionDepth == 0) + { + _transactionDescription = description; + _transactionOperations = new List(); + _transactionSnapshots = CaptureNodeSnapshots(); + _transactionCancelled = false; + } + _transactionDepth++; + return new STNodeEditTransaction(this, active: true); + } + + public void ExecuteEditTransaction(string description, Action action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + using STNodeEditTransaction transaction = BeginEditTransaction(description); + try + { + action(); + } + catch + { + transaction.Cancel(); + throw; + } + } + + internal void EndEditTransaction(bool cancel) + { + if (_transactionDepth <= 0) + { + return; + } + _transactionCancelled |= cancel; + _transactionDepth--; + if (_transactionDepth != 0) + { + return; + } + + try + { + if (_transactionCancelled) + { + RollbackTransaction(); + } + else + { + AppendSnapshotChanges(); + if (_transactionOperations.Count > 0) + { + ISTNodeEditOperation operation = _transactionOperations.Count == 1 + ? _transactionOperations[0] + : new STNodeCompositeEditOperation(_transactionOperations.ToArray()); + AddHistoryEntry(_transactionDescription, operation, allowMerge: false); + } + } + } + finally + { + _transactionOperations = null; + _transactionSnapshots = null; + _transactionDescription = null; + _transactionCancelled = false; + RefreshNodeStateCache(); + } + } + + private void RollbackTransaction() + { + ReplayHistory(() => + { + for (int i = _transactionOperations.Count - 1; i >= 0; i--) + { + _transactionOperations[i].Undo(this); + } + foreach (KeyValuePair pair in _transactionSnapshots) + { + STNode node = pair.Key; + if (!Nodes.Contains(node)) + { + continue; + } + node.OnLoadNode(CloneState(pair.Value.State)); + bool locked = node.LockLocation; + node.LockLocation = false; + node.Location = pair.Value.Location; + node.LockLocation = locked; + } + BuildBounds(); + BuildLinePath(); + Invalidate(); + }); + } + + public IDisposable SuspendHistoryRecording() + { + _historySuppressionDepth++; + return new DelegateDisposable(() => + { + _historySuppressionDepth--; + if (_historySuppressionDepth == 0) + { + RefreshNodeStateCache(); + } + }); + } + + public void Undo() + { + if (!CanUndo) + { + return; + } + STNodeEditHistoryEntry entry = _undoHistory[_undoHistory.Count - 1]; + ReplayHistory(() => entry.Operation.Undo(this)); + _undoHistory.RemoveAt(_undoHistory.Count - 1); + _redoHistory.Add(entry); + _currentStateId = entry.BeforeStateId; + NotifyHistoryChanged(); + } + + public void Redo() + { + if (!CanRedo) + { + return; + } + STNodeEditHistoryEntry entry = _redoHistory[_redoHistory.Count - 1]; + ReplayHistory(() => entry.Operation.Redo(this)); + _redoHistory.RemoveAt(_redoHistory.Count - 1); + _undoHistory.Add(entry); + _currentStateId = entry.AfterStateId; + NotifyHistoryChanged(); + } + + public void ClearHistory() + { + _undoHistory.Clear(); + _redoHistory.Clear(); + _currentStateId = ++_nextStateId; + _savedStateId = _currentStateId; + RefreshNodeStateCache(); + NotifyHistoryChanged(); + } + + public void MarkSaved() + { + _savedStateId = _currentStateId; + NotifyHistoryChanged(); + } + + public bool DeleteSelectedNodes() + { + STNode[] nodes = GetSelectedNode() + .OrderByDescending(node => Nodes.IndexOf(node)) + .ToArray(); + if (!EnableEdit || nodes.Length == 0) + { + return false; + } + using STNodeEditTransaction transaction = BeginEditTransaction("删除节点"); + foreach (STNode node in nodes) + { + Nodes.Remove(node); + } + return true; + } + + public bool MoveSelectedNodes(int offsetX, int offsetY) + { + STNode[] nodes = GetSelectedNode(); + if (!EnableEdit || nodes.Length == 0 || offsetX == 0 && offsetY == 0) + { + return false; + } + using STNodeEditTransaction transaction = BeginEditTransaction("移动节点"); + bool moved = false; + foreach (STNode node in nodes) + { + Point before = node.Location; + node.Location = new Point(before.X + offsetX, before.Y + offsetY); + moved |= node.Location != before; + } + return moved; + } + + public void SelectAllNodes() + { + foreach (STNode node in Nodes) + { + AddSelectedNode(node); + } + } + + internal void RecordNodeAdded(STNode node, int index) + { + TrackNode(node); + RecordOperation(new STNodeAddedEditOperation(node, index), "添加节点"); + } + + internal Dictionary CaptureNodeStateForRemoval(STNode node) + { + return !_enableHistory || _historySuppressionDepth > 0 + ? null + : CloneState(CapturePersistentState(node)); + } + + internal List CaptureNodeConnectionsForRemoval(STNode node) + { + if (!_enableHistory || _historySuppressionDepth > 0) + { + return null; + } + return GetConnections() + .Where(connection => ReferenceEquals(connection.Output.Owner, node) || ReferenceEquals(connection.Input.Owner, node)) + .Select(connection => new STNodeConnectionEditOperation( + STNodeOptionReference.Create(connection.Output), + STNodeOptionReference.Create(connection.Input), + connected: false, + CloneState(CapturePersistentState(connection.Output.Owner)), + CloneState(CapturePersistentState(connection.Input.Owner)))) + .ToList(); + } + + internal void RecordNodeConnectionsRemoved(List operations) + { + if (operations == null) + { + return; + } + foreach (STNodeConnectionEditOperation operation in operations) + { + RecordOperation(operation, "断开连接"); + } + } + + internal void RecordNodeRemoved(STNode node, int index, Dictionary state) + { + if (state == null) + { + return; + } + RecordOperation(new STNodeRemovedEditOperation(node, index, state), "删除节点"); + } + + internal void PrepareConnectionChange(STNodeOption first, STNodeOption second) + { + _pendingConnectionFirst = first; + _pendingConnectionSecond = second; + _pendingConnectionOutput = null; + _pendingConnectionInput = null; + _pendingConnectionOutputState = null; + _pendingConnectionInputState = null; + if (!_enableHistory || _historySuppressionDepth > 0) + { + return; + } + if (first == null || second == null || first.IsInput == second.IsInput) + { + return; + } + STNodeOption output = first.IsInput ? second : first; + STNodeOption input = first.IsInput ? first : second; + _pendingConnectionOutput = STNodeOptionReference.Create(output); + _pendingConnectionInput = STNodeOptionReference.Create(input); + _pendingConnectionOutputState = CapturePersistentState(output.Owner); + _pendingConnectionInputState = CapturePersistentState(input.Owner); + } + + internal void CompleteConnectionChange(STNodeOption first, STNodeOption second, bool? connected) + { + STNodeOptionReference outputReference = _pendingConnectionOutput; + STNodeOptionReference inputReference = _pendingConnectionInput; + Dictionary outputState = _pendingConnectionOutputState; + Dictionary inputState = _pendingConnectionInputState; + bool matches = ReferenceEquals(first, _pendingConnectionFirst) && ReferenceEquals(second, _pendingConnectionSecond) + || ReferenceEquals(first, _pendingConnectionSecond) && ReferenceEquals(second, _pendingConnectionFirst); + _pendingConnectionFirst = null; + _pendingConnectionSecond = null; + _pendingConnectionOutput = null; + _pendingConnectionInput = null; + _pendingConnectionOutputState = null; + _pendingConnectionInputState = null; + if (!matches || !connected.HasValue || outputReference == null || inputReference == null) + { + return; + } + STNodeOption output = first.IsInput ? second : first; + STNodeOption input = first.IsInput ? first : second; + bool exists = output.ConnectedOption.Contains(input) && input.ConnectedOption.Contains(output); + if (exists != connected.Value) + { + return; + } + RecordOperation( + new STNodeConnectionEditOperation( + outputReference, + inputReference, + connected.Value, + CloneState(outputState), + CloneState(inputState)), + connected.Value ? "连接节点" : "断开连接"); + } + + internal void OnNodeLocationChanged(STNode node, Point before, Point after) + { + if (before == after) + { + return; + } + NodeLocationChanged?.Invoke(this, EventArgs.Empty); + if (_transactionDepth > 0) + { + return; + } + RecordOperation( + new STNodeMoveEditOperation( + new Dictionary { [node] = before }, + new Dictionary { [node] = after }), + "移动节点", + allowMerge: true); + } + + internal void TrackNode(STNode node) + { + if (node == null || _nodeStateCache.ContainsKey(node)) + { + return; + } + node.PropertyChanged += Node_PropertyChanged; + _nodeStateCache[node] = CapturePersistentState(node); + } + + internal void UntrackNode(STNode node) + { + if (node == null) + { + return; + } + node.PropertyChanged -= Node_PropertyChanged; + _nodeStateCache.Remove(node); + } + + private void Node_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + STNode node = sender as STNode; + if (node == null) + { + return; + } + Dictionary after = CapturePersistentState(node); + if (!_nodeStateCache.TryGetValue(node, out Dictionary before)) + { + _nodeStateCache[node] = after; + return; + } + if (_transactionDepth > 0 || !_enableHistory || _historySuppressionDepth > 0) + { + return; + } + if (StatesEqual(before, after)) + { + return; + } + _nodeStateCache[node] = after; + string propertyName = e.PropertyName ?? string.Empty; + RecordOperation( + new STNodeStateEditOperation(node, CloneState(before), CloneState(after), propertyName), + string.IsNullOrEmpty(propertyName) ? "修改节点属性" : $"修改 {propertyName}", + allowMerge: true); + } + + private void BeginPointerEdit(string description) + { + if (_pointerEditTransaction == null) + { + _pointerEditTransaction = BeginEditTransaction(description); + } + } + + private void EndPointerEdit() + { + STNodeEditTransaction transaction = _pointerEditTransaction; + _pointerEditTransaction = null; + transaction?.Dispose(); + } + + private void DisposeEditing() + { + STNodeEditTransaction transaction = _pointerEditTransaction; + _pointerEditTransaction = null; + transaction?.Cancel(); + transaction?.Dispose(); + foreach (STNode node in _nodeStateCache.Keys.ToArray()) + { + UntrackNode(node); + } + _undoHistory.Clear(); + _redoHistory.Clear(); + HistoryChanged = null; + NodeLocationChanged = null; + } + + private void RecordOperation(ISTNodeEditOperation operation, string description, bool allowMerge = false) + { + if (!_enableHistory || _historySuppressionDepth > 0 || operation == null) + { + return; + } + if (_transactionDepth > 0) + { + _transactionOperations.Add(operation); + return; + } + AddHistoryEntry(description, operation, allowMerge); + RefreshNodeStateCache(); + } + + private void AddHistoryEntry(string description, ISTNodeEditOperation operation, bool allowMerge) + { + _redoHistory.Clear(); + if (allowMerge && _undoHistory.Count > 0) + { + STNodeEditHistoryEntry previous = _undoHistory[_undoHistory.Count - 1]; + if (previous.AfterStateId != _savedStateId + && DateTime.UtcNow - previous.LastChangedUtc <= TimeSpan.FromMilliseconds(750) + && previous.Operation.TryMerge(operation)) + { + previous.AfterStateId = ++_nextStateId; + previous.LastChangedUtc = DateTime.UtcNow; + _currentStateId = previous.AfterStateId; + NotifyHistoryChanged(); + return; + } + } + + long beforeStateId = _currentStateId; + long afterStateId = ++_nextStateId; + _undoHistory.Add(new STNodeEditHistoryEntry(description, operation, beforeStateId, afterStateId)); + while (_undoHistory.Count > Math.Max(1, MaximumHistoryEntries)) + { + _undoHistory.RemoveAt(0); + } + _currentStateId = afterStateId; + NotifyHistoryChanged(); + } + + private void ReplayHistory(Action action) + { + _historyReplayDepth++; + _historySuppressionDepth++; + try + { + action(); + } + finally + { + _historySuppressionDepth--; + _historyReplayDepth--; + RefreshNodeStateCache(); + } + } + + private Dictionary CaptureNodeSnapshots() + { + Dictionary snapshots = new Dictionary(); + foreach (STNode node in Nodes) + { + snapshots[node] = new NodeEditSnapshot(node.Location, CapturePersistentState(node)); + } + return snapshots; + } + + private void AppendSnapshotChanges() + { + if (_transactionSnapshots == null) + { + return; + } + Dictionary beforeLocations = new Dictionary(); + Dictionary afterLocations = new Dictionary(); + foreach (KeyValuePair pair in _transactionSnapshots) + { + STNode node = pair.Key; + if (!Nodes.Contains(node)) + { + continue; + } + if (node.Location != pair.Value.Location) + { + beforeLocations[node] = pair.Value.Location; + afterLocations[node] = node.Location; + } + Dictionary afterState = CapturePersistentState(node); + if (!StatesEqual(pair.Value.State, afterState)) + { + _transactionOperations.Add(new STNodeStateEditOperation(node, CloneState(pair.Value.State), CloneState(afterState), string.Empty)); + } + } + if (beforeLocations.Count > 0) + { + _transactionOperations.Add(new STNodeMoveEditOperation(beforeLocations, afterLocations)); + } + } + + private static Dictionary CapturePersistentState(STNode node) + { + Dictionary state = node.OnSaveNode() + .ToDictionary(pair => pair.Key, pair => (byte[])pair.Value.Clone()); + state.Remove("Guid"); + state.Remove("Left"); + state.Remove("Top"); + return state; + } + + private static Dictionary CloneState(Dictionary state) + { + return state.ToDictionary(pair => pair.Key, pair => (byte[])pair.Value.Clone()); + } + + private static bool StatesEqual(Dictionary first, Dictionary second) + { + if (first.Count != second.Count) + { + return false; + } + foreach (KeyValuePair pair in first) + { + if (!second.TryGetValue(pair.Key, out byte[] value) || !pair.Value.SequenceEqual(value)) + { + return false; + } + } + return true; + } + + private void RefreshNodeStateCache() + { + STNode[] staleNodes = _nodeStateCache.Keys.Where(node => !Nodes.Contains(node)).ToArray(); + foreach (STNode node in staleNodes) + { + UntrackNode(node); + } + foreach (STNode node in Nodes) + { + TrackNode(node); + _nodeStateCache[node] = CapturePersistentState(node); + } + } + + private void NotifyHistoryChanged() + { + HistoryChanged?.Invoke(this, EventArgs.Empty); + CommandManager.InvalidateRequerySuggested(); + } + + private sealed class DelegateDisposable : IDisposable + { + private Action _dispose; + + public DelegateDisposable(Action dispose) + { + _dispose = dispose; + } + + public void Dispose() + { + Action dispose = _dispose; + _dispose = null; + dispose?.Invoke(); + } + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditor.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditor.cs new file mode 100644 index 0000000..3cb2a1f --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditor.cs @@ -0,0 +1,3304 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Drawing.Text; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Windows.Input; +using System.Windows.Media.Imaging; +using System.Windows.Threading; +using DrawingContext = System.Windows.Media.DrawingContext; +using WpfDpiScale = System.Windows.DpiScale; +using WpfPixelFormats = System.Windows.Media.PixelFormats; +using WpfVisualTreeHelper = System.Windows.Media.VisualTreeHelper; +using WpfDragEventArgs = System.Windows.DragEventArgs; +using WpfKeyEventArgs = System.Windows.Input.KeyEventArgs; +using WpfMouseButtonEventArgs = System.Windows.Input.MouseButtonEventArgs; +using WpfMouseEventArgs = System.Windows.Input.MouseEventArgs; +using WpfMouseWheelEventArgs = System.Windows.Input.MouseWheelEventArgs; +using WpfPoint = System.Windows.Point; +using WpfRect = System.Windows.Rect; +using WpfTextCompositionEventArgs = System.Windows.Input.TextCompositionEventArgs; + +namespace ST.Library.UI.NodeEditor; + +public partial class STNodeEditor : System.Windows.Controls.Control, IDisposable +{ + protected enum CanvasAction + { + None, + MoveNode, + MoveCanvas, + ConnectOption, + SelectRectangle, + DrawMarkDetails + } + + protected struct MagnetInfo + { + public bool XMatched; + + public bool YMatched; + + public int X; + + public int Y; + + public int OffsetX; + + public int OffsetY; + } + + protected static readonly Type m_type_node = typeof(STNode); + + private float _CanvasOffsetX; + + private float _CanvasOffsetY; + + private PointF _CanvasOffset; + + private Rectangle _CanvasValidBounds; + + private float _CanvasScale = 1f; + + private float _Curvature = 0.3f; + + private bool _ShowMagnet = true; + + private bool _ShowBorder = true; + + private bool _ShowNodeShadow = true; + + private int _NodeCornerRadius; + + private bool _ShowGrid = true; + + private bool _HighlightGridOrigin = true; + + private bool _ShowLocation = true; + + private bool _LimitCanvasToContentBounds = true; + + private bool _EnableBlankLeftDragCanvas = true; + + private bool _AutoSwitchCanvasDragBySelection; + + private bool _ShowCanvasDragLockButton = true; + + private STNodeCollection _Nodes; + + private STNode _ActiveNode; + + private STNode _HoverNode; + + private Color _GridColor = Color.Black; + + private Color _BorderColor = Color.Black; + + private Color _BorderHoverColor = Color.Gray; + + private Color _BorderSelectedColor = Color.Orange; + + private Color _BorderActiveColor = Color.OrangeRed; + + private Color _MarkForeColor = Color.White; + + private Color _MarkBackColor = Color.FromArgb(180, Color.Black); + + private Color _MagnetColor = Color.Lime; + + private Color _SelectedRectangleColor = Color.DodgerBlue; + + private Color _HighLineColor = Color.Cyan; + + private Color _LocationForeColor = Color.Red; + + private Color _LocationBackColor = Color.FromArgb(120, Color.Black); + + private Color _UnknownTypeColor = Color.Gray; + + private Dictionary _TypeColor = new Dictionary(); + + private bool m_enableEdit; + + protected Point m_pt_in_control; + + protected PointF m_pt_in_canvas; + + protected Point m_pt_down_in_control; + + protected PointF m_pt_down_in_canvas; + + protected PointF m_pt_canvas_old; + + protected Point m_pt_dot_down; + + protected STNodeOption m_option_down; + + protected STNode m_node_down; + + protected bool m_mouse_in_control; + + private DrawingTools m_drawing_tools; + + private NodeFindInfo m_find; + + private MagnetInfo m_mi; + + private RectangleF m_rect_select; + + private readonly HashSet m_rectangle_selection_baseline = new HashSet(); + + private Image m_img_border; + + private Image m_img_border_hover; + + private Image m_img_border_selected; + + private Image m_img_border_active; + + private float m_real_canvas_x; + + private float m_real_canvas_y; + + private Dictionary m_dic_pt_selected = new Dictionary(); + + private List m_lst_magnet_x = new List(); + + private List m_lst_magnet_y = new List(); + + private List m_lst_magnet_mx = new List(); + + private List m_lst_magnet_my = new List(); + + private DateTime m_dt_vw = DateTime.Now; + + private DateTime m_dt_hw = DateTime.Now; + + private CanvasAction m_ca; + + private HashSet m_hs_node_selected = new HashSet(); + + private bool m_is_process_mouse_event = true; + + private long m_suppress_context_menu_until; + + private bool m_is_buildpath; + + private Pen m_p_line = new Pen(Color.Cyan, 2f); + + private Pen m_p_line_hover = new Pen(Color.Cyan, 12f); + + private GraphicsPath m_gp_hover; + + private StringFormat m_sf = new StringFormat(); + + private Rectangle m_rect_canvas_drag_lock; + + private Dictionary m_dic_gp_info = new Dictionary(); + + private List m_lst_node_out = new List(); + + private int m_time_alert; + + private int m_alpha_alert; + + private string m_str_alert; + + private Color m_forecolor_alert; + + private Color m_backcolor_alert; + + private DateTime m_dt_alert; + + private Rectangle m_rect_alert; + + private AlertLocation m_al; + + private Color _BackColor = Color.FromArgb(255, 34, 34, 34); + + private Color _ForeColor = Color.White; + + private Font _Font = new Font("Segoe UI", 9f); + + private Size _ClientSize = new Size(200, 200); + + private readonly Bitmap m_measurement_bitmap = new Bitmap(1, 1, PixelFormat.Format32bppPArgb); + + private readonly DispatcherTimer m_animation_timer; + + private Bitmap m_render_bitmap; + + private Graphics m_render_graphics; + + private WriteableBitmap m_render_target; + + private volatile bool m_disposed; + + private bool m_is_loaded; + + internal bool IsDisposed => m_disposed; + + [Browsable(false)] + public float CanvasOffsetX => _CanvasOffsetX; + + [Browsable(false)] + public float CanvasOffsetY => _CanvasOffsetY; + + [Browsable(false)] + public PointF CanvasOffset + { + get + { + _CanvasOffset.X = _CanvasOffsetX; + _CanvasOffset.Y = _CanvasOffsetY; + return _CanvasOffset; + } + } + + [Browsable(false)] + public Rectangle CanvasValidBounds => _CanvasValidBounds; + + [Browsable(false)] + public float CanvasScale => _CanvasScale; + + [Browsable(false)] + public Size ClientSize + { + get + { + int width = ActualWidth > 0 ? (int)Math.Ceiling(ActualWidth) : _ClientSize.Width; + int height = ActualHeight > 0 ? (int)Math.Ceiling(ActualHeight) : _ClientSize.Height; + return new Size(Math.Max(0, width), Math.Max(0, height)); + } + set + { + _ClientSize = value; + Invalidate(); + } + } + + [Browsable(false)] + public Rectangle ClientRectangle => new Rectangle(Point.Empty, ClientSize); + + [Description("获取或设置画布背景色")] + public Color BackColor + { + get => _BackColor; + set + { + _BackColor = value; + Invalidate(); + } + } + + [Description("获取或设置画布前景色")] + public Color ForeColor + { + get => _ForeColor; + set + { + _ForeColor = value; + Invalidate(); + } + } + + [Browsable(false)] + public Font Font + { + get => _Font; + set + { + if (value == null || ReferenceEquals(_Font, value)) + { + return; + } + _Font.Dispose(); + _Font = value; + Invalidate(); + } + } + + [Browsable(false)] + public float Curvature + { + get + { + return _Curvature; + } + set + { + if (value < 0f) + { + value = 0f; + } + if (value > 1f) + { + value = 1f; + } + _Curvature = value; + if (m_dic_gp_info.Count != 0) + { + BuildLinePath(); + } + } + } + + [Description("获取或设置移动画布中 Node 时候 是否启用磁铁效果")] + [DefaultValue(true)] + public bool ShowMagnet + { + get + { + return _ShowMagnet; + } + set + { + _ShowMagnet = value; + } + } + + [Description("获取或设置 移动画布中是否显示 Node 边框")] + [DefaultValue(true)] + public bool ShowBorder + { + get + { + return _ShowBorder; + } + set + { + _ShowBorder = value; + Invalidate(); + } + } + + [Description("获取或设置是否绘制 Node 的阴影")] + [DefaultValue(true)] + public bool ShowNodeShadow + { + get + { + return _ShowNodeShadow; + } + set + { + if (_ShowNodeShadow == value) + { + return; + } + _ShowNodeShadow = value; + Invalidate(); + } + } + + [Description("获取或设置画布中 Node 的圆角半径")] + [DefaultValue(0)] + public int NodeCornerRadius + { + get + { + return _NodeCornerRadius; + } + set + { + if (value < 0) + { + value = 0; + } + if (_NodeCornerRadius == value) + { + return; + } + _NodeCornerRadius = value; + Invalidate(); + } + } + + [Description("获取或设置画布中是否绘制背景网格线条")] + [DefaultValue(true)] + public bool ShowGrid + { + get + { + return _ShowGrid; + } + set + { + _ShowGrid = value; + Invalidate(); + } + } + + [Description("获取或设置是否突出显示画布网格原点")] + [DefaultValue(true)] + public bool HighlightGridOrigin + { + get + { + return _HighlightGridOrigin; + } + set + { + if (_HighlightGridOrigin == value) + { + return; + } + _HighlightGridOrigin = value; + Invalidate(); + } + } + + [Description("获取或设置是否在画布边缘显示超出视角的 Node 位置信息")] + [DefaultValue(true)] + public bool ShowLocation + { + get + { + return _ShowLocation; + } + set + { + _ShowLocation = value; + Invalidate(); + } + } + + [Description("获取或设置移动和缩放画布时是否限制在 Node 内容范围内")] + [DefaultValue(true)] + public bool LimitCanvasToContentBounds + { + get + { + return _LimitCanvasToContentBounds; + } + set + { + _LimitCanvasToContentBounds = value; + } + } + + [Browsable(false)] + public STNodeCollection Nodes => _Nodes; + + [Browsable(false)] + public STNode ActiveNode => _ActiveNode; + + [Browsable(false)] + public STNode HoverNode => _HoverNode; + + [Description("获取或设置绘制画布背景时 网格线条颜色")] + [DefaultValue(typeof(Color), "Black")] + public Color GridColor + { + get + { + return _GridColor; + } + set + { + _GridColor = value; + Invalidate(); + } + } + + [Description("获取或设置画布中 Node 边框颜色")] + [DefaultValue(typeof(Color), "Black")] + public Color BorderColor + { + get + { + return _BorderColor; + } + set + { + _BorderColor = value; + if (m_img_border != null) + { + m_img_border.Dispose(); + } + m_img_border = CreateBorderImage(value); + Invalidate(); + } + } + + [Description("获取或设置画布中悬停 Node 边框颜色")] + [DefaultValue(typeof(Color), "Gray")] + public Color BorderHoverColor + { + get + { + return _BorderHoverColor; + } + set + { + _BorderHoverColor = value; + if (m_img_border_hover != null) + { + m_img_border_hover.Dispose(); + } + m_img_border_hover = CreateBorderImage(value); + Invalidate(); + } + } + + [Description("获取或设置画布中选中 Node 边框颜色")] + [DefaultValue(typeof(Color), "Orange")] + public Color BorderSelectedColor + { + get + { + return _BorderSelectedColor; + } + set + { + _BorderSelectedColor = value; + if (m_img_border_selected != null) + { + m_img_border_selected.Dispose(); + } + m_img_border_selected = CreateBorderImage(value); + Invalidate(); + } + } + + [Description("获取或设置画布中活动 Node 边框颜色")] + [DefaultValue(typeof(Color), "OrangeRed")] + public Color BorderActiveColor + { + get + { + return _BorderActiveColor; + } + set + { + _BorderActiveColor = value; + if (m_img_border_active != null) + { + m_img_border_active.Dispose(); + } + m_img_border_active = CreateBorderImage(value); + Invalidate(); + } + } + + [Description("获取或设置画布绘制 Node 标记详情采用的前景色")] + [DefaultValue(typeof(Color), "White")] + public Color MarkForeColor + { + get + { + return _MarkForeColor; + } + set + { + _MarkForeColor = value; + Invalidate(); + } + } + + [Description("获取或设置画布绘制 Node 标记详情采用的背景色")] + public Color MarkBackColor + { + get + { + return _MarkBackColor; + } + set + { + _MarkBackColor = value; + Invalidate(); + } + } + + [Description("获取或设置画布中移动 Node 时候 磁铁标记颜色")] + [DefaultValue(typeof(Color), "Lime")] + public Color MagnetColor + { + get + { + return _MagnetColor; + } + set + { + _MagnetColor = value; + } + } + + [Description("获取或设置画布中选择矩形区域的颜色")] + [DefaultValue(typeof(Color), "DodgerBlue")] + public Color SelectedRectangleColor + { + get + { + return _SelectedRectangleColor; + } + set + { + _SelectedRectangleColor = value; + } + } + + [Description("获取或设置画布中高亮连线的颜色")] + [DefaultValue(typeof(Color), "Cyan")] + public Color HighLineColor + { + get + { + return _HighLineColor; + } + set + { + _HighLineColor = value; + } + } + + [Description("获取或设置画布中边缘位置提示区域前景色")] + [DefaultValue(typeof(Color), "Red")] + public Color LocationForeColor + { + get + { + return _LocationForeColor; + } + set + { + _LocationForeColor = value; + Invalidate(); + } + } + + [Description("获取或设置画布中边缘位置提示区域背景色")] + public Color LocationBackColor + { + get + { + return _LocationBackColor; + } + set + { + _LocationBackColor = value; + Invalidate(); + } + } + + [Description("获取或设置画布中当 Node 中 Option 数据类型无法确定时应当使用的颜色")] + [DefaultValue(typeof(Color), "Gray")] + public Color UnknownTypeColor + { + get + { + return _UnknownTypeColor; + } + set + { + _UnknownTypeColor = value; + Invalidate(); + } + } + + [Browsable(false)] + public Dictionary TypeColor => _TypeColor; + + [Browsable(false)] + public bool EnableEdit + { + get + { + return m_enableEdit; + } + set + { + m_enableEdit = value; + } + } + + public bool EnableBlankLeftDragCanvas + { + get + { + return _EnableBlankLeftDragCanvas; + } + set + { + if (_EnableBlankLeftDragCanvas == value) + { + return; + } + _EnableBlankLeftDragCanvas = value; + Invalidate(); + EnableBlankLeftDragCanvasChanged?.Invoke(this, EventArgs.Empty); + } + } + + [Description("根据节点选中状态自动切换空白区域左键拖动画布:无选中节点时启用,有选中节点时禁用")] + [DefaultValue(false)] + public bool AutoSwitchCanvasDragBySelection + { + get + { + return _AutoSwitchCanvasDragBySelection; + } + set + { + if (_AutoSwitchCanvasDragBySelection == value) + { + return; + } + _AutoSwitchCanvasDragBySelection = value; + UpdateCanvasDragModeFromSelection(); + } + } + + [DefaultValue(true)] + public bool ShowCanvasDragLockButton + { + get + { + return _ShowCanvasDragLockButton; + } + set + { + if (_ShowCanvasDragLockButton == value) + { + return; + } + _ShowCanvasDragLockButton = value; + if (!value) + { + m_rect_canvas_drag_lock = Rectangle.Empty; + } + Invalidate(); + } + } + + public event EventHandler EnableBlankLeftDragCanvasChanged; + + [Description("活动的节点发生变化时候发生")] + public event EventHandler ActiveChanged; + + [Description("选择的节点发生变化时候发生")] + public event EventHandler SelectedChanged; + + [Description("悬停的节点发生变化时候发生")] + public event EventHandler HoverChanged; + + [Description("当节点被添加时候发生")] + public event STNodeEditorEventHandler NodeAdded; + + [Description("当节点被移除时候发生")] + public event STNodeEditorEventHandler NodeRemoved; + + [Description("移动画布原点时候发生")] + public event EventHandler CanvasMoved; + + [Description("缩放画布时候发生")] + public event EventHandler CanvasScaled; + + [Description("连接节点选项时候发生")] + public event STNodeEditorOptionEventHandler OptionConnected; + + [Description("正在连接节点选项时候发生")] + public event STNodeEditorOptionEventHandler OptionConnecting; + + [Description("断开节点选项时候发生")] + public event STNodeEditorOptionEventHandler OptionDisConnected; + + [Description("正在断开节点选项时候发生")] + public event STNodeEditorOptionEventHandler OptionDisConnecting; + + public STNodeEditor() + { + Focusable = true; + ClipToBounds = true; + SnapsToDevicePixels = true; + UseLayoutRounding = true; + AllowDrop = true; + MinWidth = 100; + MinHeight = 100; + _Nodes = new STNodeCollection(this); + m_enableEdit = true; + m_real_canvas_x = (_CanvasOffsetX = 10f); + m_real_canvas_y = (_CanvasOffsetY = 10f); + STNodeTypeRegistry.Initialize(); + InitializeEditing(); + InitializeDrawingResources(); + m_animation_timer = new DispatcherTimer(DispatcherPriority.Render) + { + Interval = TimeSpan.FromMilliseconds(30) + }; + m_animation_timer.Tick += AnimationTimer_Tick; + Loaded += (_, _) => + { + m_is_loaded = true; + UpdateAnimationTimerState(); + }; + Unloaded += (_, _) => + { + m_is_loaded = false; + m_animation_timer.Stop(); + }; + } + + protected internal virtual void OnSelectedChanged(EventArgs e) + { + UpdateCanvasDragModeFromSelection(); + if (this.SelectedChanged != null) + { + this.SelectedChanged(this, e); + } + } + + private void UpdateCanvasDragModeFromSelection() + { + if (!_AutoSwitchCanvasDragBySelection) + { + return; + } + bool hasSelection; + lock (m_hs_node_selected) + { + hasSelection = m_hs_node_selected.Count > 0; + } + EnableBlankLeftDragCanvas = !hasSelection; + } + + protected virtual void OnActiveChanged(EventArgs e) + { + if (this.ActiveChanged != null) + { + this.ActiveChanged(this, e); + } + } + + protected virtual void OnHoverChanged(EventArgs e) + { + if (this.HoverChanged != null) + { + this.HoverChanged(this, e); + } + } + + protected internal virtual void OnNodeAdded(STNodeEditorEventArgs e) + { + TrackNode(e.Node); + if (this.NodeAdded != null) + { + this.NodeAdded(this, e); + } + } + + protected internal virtual void OnNodeRemoved(STNodeEditorEventArgs e) + { + UntrackNode(e.Node); + if (this.NodeRemoved != null) + { + this.NodeRemoved(this, e); + } + } + + protected virtual void OnCanvasMoved(EventArgs e) + { + if (this.CanvasMoved != null) + { + this.CanvasMoved(this, e); + } + } + + protected virtual void OnCanvasScaled(EventArgs e) + { + if (this.CanvasScaled != null) + { + this.CanvasScaled(this, e); + } + } + + protected internal virtual void OnOptionConnected(STNodeEditorOptionEventArgs e) + { + CompleteConnectionChange(e.CurrentOption, e.TargetOption, e.Status == ConnectionStatus.Connected ? true : (bool?)null); + if (this.OptionConnected != null) + { + this.OptionConnected(this, e); + } + } + + protected internal virtual void OnOptionDisConnected(STNodeEditorOptionEventArgs e) + { + CompleteConnectionChange(e.CurrentOption, e.TargetOption, e.Status == ConnectionStatus.DisConnected ? false : (bool?)null); + if (this.OptionDisConnected != null) + { + this.OptionDisConnected(this, e); + } + } + + protected internal virtual void OnOptionConnecting(STNodeEditorOptionEventArgs e) + { + PrepareConnectionChange(e.CurrentOption, e.TargetOption); + if (IsReplayingHistory) + { + return; + } + if (this.OptionConnecting != null) + { + this.OptionConnecting(this, e); + } + } + + protected internal virtual void OnOptionDisConnecting(STNodeEditorOptionEventArgs e) + { + PrepareConnectionChange(e.CurrentOption, e.TargetOption); + if (IsReplayingHistory) + { + return; + } + if (this.OptionDisConnecting != null) + { + this.OptionDisConnecting(this, e); + } + } + + private void InitializeDrawingResources() + { + m_drawing_tools = new DrawingTools + { + Pen = new Pen(Color.Black, 1f), + SolidBrush = new SolidBrush(Color.Black) + }; + m_img_border = CreateBorderImage(_BorderColor); + m_img_border_active = CreateBorderImage(_BorderActiveColor); + m_img_border_hover = CreateBorderImage(_BorderHoverColor); + m_img_border_selected = CreateBorderImage(_BorderSelectedColor); + m_sf?.Dispose(); + m_sf = new StringFormat + { + Alignment = StringAlignment.Near, + FormatFlags = StringFormatFlags.NoWrap + }; + m_sf.SetTabStops(0f, new float[1] { 40f }); + } + + private void AnimationTimer_Tick(object sender, EventArgs e) + { + if (m_disposed) + { + m_animation_timer.Stop(); + return; + } + + bool redraw = false; + float nextX = MoveTowards(_CanvasOffsetX, m_real_canvas_x); + float nextY = MoveTowards(_CanvasOffsetY, m_real_canvas_y); + if (nextX != _CanvasOffsetX || nextY != _CanvasOffsetY) + { + _CanvasOffsetX = nextX; + _CanvasOffsetY = nextY; + m_pt_canvas_old.X = nextX; + m_pt_canvas_old.Y = nextY; + redraw = true; + } + + double remaining = m_time_alert - DateTime.UtcNow.Subtract(m_dt_alert).TotalMilliseconds; + int alpha = remaining >= 0 ? 255 : remaining <= -1000 ? 0 : (int)(255d + remaining / 1000d * 255d); + if (alpha != m_alpha_alert) + { + m_alpha_alert = alpha; + redraw = true; + } + if (redraw) + { + Invalidate(); + } + UpdateAnimationTimerState(); + } + + private bool HasPendingAnimation() + { + return _CanvasOffsetX != m_real_canvas_x + || _CanvasOffsetY != m_real_canvas_y + || m_alpha_alert > 0; + } + + private void UpdateAnimationTimerState() + { + if (!m_disposed && m_is_loaded && HasPendingAnimation()) + { + if (!m_animation_timer.IsEnabled) + m_animation_timer.Start(); + } + else if (m_animation_timer.IsEnabled) + { + m_animation_timer.Stop(); + } + } + + private static float MoveTowards(float current, float target) + { + float delta = target - current; + float distance = Math.Abs(delta); + if (distance < 1f) + { + return target; + } + float step = distance <= 4f ? 1f : distance <= 12f ? 2f : distance <= 30f ? 3f : distance / 10f; + return current + (delta > 0f ? step : -step); + } + + protected override void OnRender(DrawingContext drawingContext) + { + base.OnRender(drawingContext); + if (m_disposed) + { + return; + } + + Size clientSize = ClientSize; + if (clientSize.Width <= 0 || clientSize.Height <= 0) + { + return; + } + + double width = ActualWidth > 0 ? ActualWidth : clientSize.Width; + double height = ActualHeight > 0 ? ActualHeight : clientSize.Height; + WpfDpiScale dpi = WpfVisualTreeHelper.GetDpi(this); + int pixelWidth = Math.Max(1, (int)Math.Ceiling(width * dpi.DpiScaleX)); + int pixelHeight = Math.Max(1, (int)Math.Ceiling(height * dpi.DpiScaleY)); + + EnsureRenderTarget(pixelWidth, pixelHeight, dpi.PixelsPerInchX, dpi.PixelsPerInchY); + RenderToGraphics( + m_render_graphics, + clientSize.Width, + clientSize.Height, + (float)dpi.DpiScaleX, + (float)dpi.DpiScaleY); + BitmapData bitmapData = m_render_bitmap.LockBits( + new Rectangle(0, 0, pixelWidth, pixelHeight), + ImageLockMode.ReadOnly, + PixelFormat.Format32bppPArgb); + try + { + m_render_target.WritePixels( + new System.Windows.Int32Rect(0, 0, pixelWidth, pixelHeight), + bitmapData.Scan0, + Math.Abs(bitmapData.Stride) * pixelHeight, + bitmapData.Stride); + } + finally + { + m_render_bitmap.UnlockBits(bitmapData); + } + + drawingContext.DrawImage(m_render_target, new WpfRect(0, 0, width, height)); + } + + private void EnsureRenderTarget(int pixelWidth, int pixelHeight, double dpiX, double dpiY) + { + if (m_render_bitmap != null && + m_render_bitmap.Width == pixelWidth && + m_render_bitmap.Height == pixelHeight && + Math.Abs(m_render_target.DpiX - dpiX) < 0.01 && + Math.Abs(m_render_target.DpiY - dpiY) < 0.01) + { + return; + } + m_render_graphics?.Dispose(); + m_render_bitmap?.Dispose(); + m_render_bitmap = new Bitmap(pixelWidth, pixelHeight, PixelFormat.Format32bppPArgb); + m_render_graphics = Graphics.FromImage(m_render_bitmap); + m_render_target = new WriteableBitmap(pixelWidth, pixelHeight, dpiX, dpiY, WpfPixelFormats.Pbgra32, null); + } + + private void RenderToGraphics(Graphics graphics, int width, int height, float dpiScaleX, float dpiScaleY) + { + ResetRenderTransform(graphics, dpiScaleX, dpiScaleY); + graphics.Clear(BackColor); + graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; + graphics.SmoothingMode = SmoothingMode.HighQuality; + m_drawing_tools.Graphics = graphics; + if (_ShowGrid) + { + OnDrawGrid(m_drawing_tools, width, height); + } + graphics.TranslateTransform(_CanvasOffsetX, _CanvasOffsetY); + graphics.ScaleTransform(_CanvasScale, _CanvasScale); + OnDrawConnectedLine(m_drawing_tools); + OnDrawNode(m_drawing_tools, ControlToCanvas(new Rectangle(0, 0, width, height))); + if (m_ca == CanvasAction.ConnectOption && m_option_down != null) + { + m_drawing_tools.Pen.Color = _HighLineColor; + if (m_option_down.IsInput) + { + DrawBezier(graphics, m_drawing_tools.Pen, m_pt_in_canvas, m_pt_dot_down, _Curvature); + } + else + { + DrawBezier(graphics, m_drawing_tools.Pen, m_pt_dot_down, m_pt_in_canvas, _Curvature); + } + } + ResetRenderTransform(graphics, dpiScaleX, dpiScaleY); + switch (m_ca) + { + case CanvasAction.MoveNode: + if (_ShowMagnet && _ActiveNode != null) + { + OnDrawMagnet(m_drawing_tools, m_mi); + } + break; + case CanvasAction.SelectRectangle: + OnDrawSelectedRectangle(m_drawing_tools, CanvasToControl(m_rect_select)); + break; + case CanvasAction.DrawMarkDetails: + if (!string.IsNullOrEmpty(m_find.Mark)) + { + OnDrawMark(m_drawing_tools); + } + break; + } + if (_ShowLocation) + { + OnDrawNodeOutLocation(m_drawing_tools, new Size(width, height), m_lst_node_out); + } + OnDrawAlert(graphics); + if (_ShowCanvasDragLockButton) + { + OnDrawCanvasDragLockButton(m_drawing_tools); + } + } + + private static void ResetRenderTransform(Graphics graphics, float dpiScaleX, float dpiScaleY) + { + graphics.ResetTransform(); + graphics.ScaleTransform(dpiScaleX, dpiScaleY); + } + + protected override void OnMouseDown(WpfMouseButtonEventArgs e) + { + base.OnMouseDown(e); + STNodeMouseEventArgs nodeEvent = CreateMouseEventArgs(e); + Focus(); + CaptureMouse(); + if (_ShowCanvasDragLockButton + && nodeEvent.Button == STMouseButtons.Left + && m_rect_canvas_drag_lock.Contains(nodeEvent.Location)) + { + EnableBlankLeftDragCanvas = !EnableBlankLeftDragCanvas; + m_ca = CanvasAction.None; + e.Handled = true; + return; + } + m_ca = CanvasAction.None; + if (!ShouldActivateNodeFromMouse(nodeEvent.Button)) + { + SetActiveNode(null); + } + m_mi.XMatched = (m_mi.YMatched = false); + m_pt_down_in_control = nodeEvent.Location; + m_pt_down_in_canvas.X = ((float)nodeEvent.X - _CanvasOffsetX) / _CanvasScale; + m_pt_down_in_canvas.Y = ((float)nodeEvent.Y - _CanvasOffsetY) / _CanvasScale; + m_pt_canvas_old.X = _CanvasOffsetX; + m_pt_canvas_old.Y = _CanvasOffsetY; + if (m_gp_hover != null && nodeEvent.Button == STMouseButtons.Right) + { + NodeFindInfo preCheck = FindNodeFromPoint(m_pt_down_in_canvas); + if (preCheck.Node != null) + { + m_gp_hover = null; + } + else + { + if (m_enableEdit) + { + bool hadHoveredConnection = m_gp_hover != null && m_dic_gp_info.ContainsKey(m_gp_hover); + ConnectionStatus disconnectStatus; + using (BeginEditTransaction("断开连接")) + { + disconnectStatus = DisConnectionHover(); + } + if (hadHoveredConnection && disconnectStatus == ConnectionStatus.DisConnected) + { + m_suppress_context_menu_until = Stopwatch.GetTimestamp() + Stopwatch.Frequency; + } + m_is_process_mouse_event = false; + } + return; + } + } + NodeFindInfo nodeFindInfo = FindNodeFromPoint(m_pt_down_in_canvas); + if (nodeFindInfo.Node != null && !ShouldActivateNodeFromMouse(nodeEvent.Button)) + { + return; + } + if (!string.IsNullOrEmpty(nodeFindInfo.Mark)) + { + m_ca = CanvasAction.DrawMarkDetails; + Invalidate(); + } + else if (nodeFindInfo.NodeOption != null) + { + if (m_enableEdit) + { + StartConnect(nodeFindInfo.NodeOption); + } + } + else if (nodeFindInfo.Node != null) + { + if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) + { + if (nodeFindInfo.Node.IsSelected) + { + if (nodeFindInfo.Node == _ActiveNode) + { + SetActiveNode(null); + } + nodeFindInfo.Node.SetSelected(bSelected: false, bRedraw: true); + } + else + { + nodeFindInfo.Node.SetSelected(bSelected: true, bRedraw: true); + } + m_node_down = null; + m_is_process_mouse_event = false; + e.Handled = true; + return; + } + nodeFindInfo.Node.OnMouseDown(nodeEvent.WithLocation((int)m_pt_down_in_canvas.X - nodeFindInfo.Node.Left, (int)m_pt_down_in_canvas.Y - nodeFindInfo.Node.Top)); + if (!nodeFindInfo.Node.IsSelected) + { + STNode[] array = m_hs_node_selected.ToArray(); + foreach (STNode sTNode in array) + { + sTNode.SetSelected(bSelected: false, bRedraw: false); + } + } + nodeFindInfo.Node.SetSelected(bSelected: true, bRedraw: false); + SetActiveNode(nodeFindInfo.Node); + if (PointInRectangle(nodeFindInfo.Node.TitleRectangle, m_pt_down_in_canvas.X, m_pt_down_in_canvas.Y)) + { + m_dic_pt_selected.Clear(); + lock (m_hs_node_selected) + { + foreach (STNode item in m_hs_node_selected) + { + m_dic_pt_selected.Add(item, item.Location); + } + } + m_ca = CanvasAction.MoveNode; + BeginPointerEdit("移动节点"); + if (_ShowMagnet && _ActiveNode != null) + { + BuildMagnetLocation(); + } + } + else + { + m_node_down = nodeFindInfo.Node; + } + } + else + { + bool enableBlankLeftDragCanvasAtMouseDown = EnableBlankLeftDragCanvas; + ModifierKeys modifiers = Keyboard.Modifiers; + SetActiveNode(null); + bool panCanvas = ShouldPanBlankCanvas(nodeEvent.Button, enableBlankLeftDragCanvasAtMouseDown, modifiers); + m_rectangle_selection_baseline.Clear(); + if (!panCanvas) + { + bool appendSelection = (modifiers & ModifierKeys.Control) == ModifierKeys.Control; + if (appendSelection) + { + foreach (STNode selectedNode in m_hs_node_selected) + { + m_rectangle_selection_baseline.Add(selectedNode); + } + } + else + { + STNode[] array2 = m_hs_node_selected.ToArray(); + foreach (STNode sTNode2 in array2) + { + sTNode2.SetSelected(bSelected: false, bRedraw: false); + } + } + } + m_ca = panCanvas ? CanvasAction.MoveCanvas : CanvasAction.SelectRectangle; + ref RectangleF rect_select = ref m_rect_select; + float num = (m_rect_select.Height = 0f); + rect_select.Width = num; + m_node_down = null; + } + e.Handled = nodeEvent.Button != STMouseButtons.Right; + } + + protected internal static bool ShouldActivateNodeFromMouse(STMouseButtons button) + { + return button == STMouseButtons.Left; + } + + protected internal static bool ShouldPanBlankCanvas(STMouseButtons button, bool enableBlankLeftDragCanvasAtMouseDown, ModifierKeys modifiers) + { + return button == STMouseButtons.Middle + || button == STMouseButtons.Left + && enableBlankLeftDragCanvasAtMouseDown + && (modifiers & ModifierKeys.Control) != ModifierKeys.Control; + } + + protected internal static bool ShouldSelectNodeFromRectangle(bool intersectsSelectionRectangle, bool wasSelectedBeforeDrag) + { + return intersectsSelectionRectangle || wasSelectedBeforeDrag; + } + + protected override void OnMouseMove(WpfMouseEventArgs e) + { + base.OnMouseMove(e); + STNodeMouseEventArgs nodeEvent = CreateMouseEventArgs(e); + m_pt_in_control = nodeEvent.Location; + m_pt_in_canvas.X = ((float)nodeEvent.X - _CanvasOffsetX) / _CanvasScale; + m_pt_in_canvas.Y = ((float)nodeEvent.Y - _CanvasOffsetY) / _CanvasScale; + if (m_node_down != null) + { + m_node_down.OnMouseMove(nodeEvent.WithLocation((int)m_pt_in_canvas.X - m_node_down.Left, (int)m_pt_in_canvas.Y - m_node_down.Top)); + return; + } + if (nodeEvent.Button == STMouseButtons.Middle) + { + _CanvasOffsetX = (m_real_canvas_x = m_pt_canvas_old.X + (float)(nodeEvent.X - m_pt_down_in_control.X)); + _CanvasOffsetY = (m_real_canvas_y = m_pt_canvas_old.Y + (float)(nodeEvent.Y - m_pt_down_in_control.Y)); + Invalidate(); + return; + } + if (nodeEvent.Button == STMouseButtons.Left) + { + m_gp_hover = null; + switch (m_ca) + { + case CanvasAction.MoveNode: + if (m_enableEdit) + { + MoveNode(nodeEvent.Location); + } + return; + case CanvasAction.MoveCanvas: + _CanvasOffsetX = (m_real_canvas_x = m_pt_canvas_old.X + (float)(nodeEvent.X - m_pt_down_in_control.X)); + _CanvasOffsetY = (m_real_canvas_y = m_pt_canvas_old.Y + (float)(nodeEvent.Y - m_pt_down_in_control.Y)); + Invalidate(); + return; + case CanvasAction.ConnectOption: + Invalidate(); + return; + case CanvasAction.SelectRectangle: + m_rect_select.X = ((m_pt_down_in_canvas.X < m_pt_in_canvas.X) ? m_pt_down_in_canvas.X : m_pt_in_canvas.X); + m_rect_select.Y = ((m_pt_down_in_canvas.Y < m_pt_in_canvas.Y) ? m_pt_down_in_canvas.Y : m_pt_in_canvas.Y); + m_rect_select.Width = Math.Abs(m_pt_in_canvas.X - m_pt_down_in_canvas.X); + m_rect_select.Height = Math.Abs(m_pt_in_canvas.Y - m_pt_down_in_canvas.Y); + foreach (STNode node in _Nodes) + { + node.SetSelected( + ShouldSelectNodeFromRectangle( + m_rect_select.IntersectsWith(node.Rectangle), + m_rectangle_selection_baseline.Contains(node)), + bRedraw: false); + } + Invalidate(); + return; + } + } + NodeFindInfo nodeFindInfo = FindNodeFromPoint(m_pt_in_canvas); + bool flag = false; + if (_HoverNode != nodeFindInfo.Node) + { + if (nodeFindInfo.Node != null) + { + nodeFindInfo.Node.OnMouseEnter(EventArgs.Empty); + } + if (_HoverNode != null) + { + _HoverNode.OnMouseLeave(nodeEvent.WithLocation((int)m_pt_in_canvas.X - _HoverNode.Left, (int)m_pt_in_canvas.Y - _HoverNode.Top)); + } + _HoverNode = nodeFindInfo.Node; + OnHoverChanged(EventArgs.Empty); + flag = true; + } + if (_HoverNode != null) + { + _HoverNode.OnMouseMove(nodeEvent.WithLocation((int)m_pt_in_canvas.X - _HoverNode.Left, (int)m_pt_in_canvas.Y - _HoverNode.Top)); + m_gp_hover = null; + } + else + { + GraphicsPath graphicsPath = null; + foreach (KeyValuePair item in m_dic_gp_info) + { + if (item.Key.IsOutlineVisible(m_pt_in_canvas, m_p_line_hover)) + { + graphicsPath = item.Key; + break; + } + } + if (m_gp_hover != graphicsPath) + { + m_gp_hover = graphicsPath; + flag = true; + } + } + if (flag) + { + Invalidate(); + } + } + + protected override void OnMouseUp(WpfMouseButtonEventArgs e) + { + base.OnMouseUp(e); + STNodeMouseEventArgs nodeEvent = CreateMouseEventArgs(e); + m_pt_in_control = nodeEvent.Location; + m_pt_in_canvas.X = ((float)nodeEvent.X - _CanvasOffsetX) / _CanvasScale; + m_pt_in_canvas.Y = ((float)nodeEvent.Y - _CanvasOffsetY) / _CanvasScale; + int dotPadding = (m_ca == CanvasAction.ConnectOption) ? 14 : 6; + NodeFindInfo nodeFindInfo = FindNodeFromPoint(m_pt_in_canvas, dotPadding); + switch (m_ca) + { + case CanvasAction.MoveNode: + break; + case CanvasAction.ConnectOption: + if (!(nodeEvent.Location == m_pt_down_in_control) && nodeFindInfo.NodeOption != null) + { + if (m_option_down.IsInput) + { + nodeFindInfo.NodeOption.ConnectOption(m_option_down); + } + else + { + m_option_down.ConnectOption(nodeFindInfo.NodeOption); + } + } + break; + } + EndPointerEdit(); + if (m_is_process_mouse_event && _ActiveNode != null) + { + STNodeMouseEventArgs e2 = nodeEvent.WithLocation((int)m_pt_in_canvas.X - _ActiveNode.Left, (int)m_pt_in_canvas.Y - _ActiveNode.Top); + _ActiveNode.OnMouseUp(e2); + m_node_down = null; + } + if (Math.Abs(nodeEvent.X - m_pt_down_in_control.X) <= 2 && Math.Abs(nodeEvent.Y - m_pt_down_in_control.Y) <= 2) + { + ProcessMouseClick(nodeEvent); + } + m_is_process_mouse_event = true; + m_ca = CanvasAction.None; + m_rectangle_selection_baseline.Clear(); + ReleaseMouseCapture(); + Invalidate(); + } + + protected override void OnContextMenuOpening(System.Windows.Controls.ContextMenuEventArgs e) + { + long suppressUntil = m_suppress_context_menu_until; + m_suppress_context_menu_until = 0; + if (suppressUntil >= Stopwatch.GetTimestamp()) + { + e.Handled = true; + return; + } + base.OnContextMenuOpening(e); + } + + protected override void OnLostMouseCapture(WpfMouseEventArgs e) + { + EndPointerEdit(); + _ActiveNode?.CancelMouseInteraction(); + m_node_down?.CancelMouseInteraction(); + m_rectangle_selection_baseline.Clear(); + m_node_down = null; + m_option_down = null; + m_rect_select = RectangleF.Empty; + m_ca = CanvasAction.None; + m_is_process_mouse_event = true; + Invalidate(); + base.OnLostMouseCapture(e); + } + + protected override void OnMouseEnter(WpfMouseEventArgs e) + { + base.OnMouseEnter(e); + m_mouse_in_control = true; + } + + protected override void OnMouseLeave(WpfMouseEventArgs e) + { + base.OnMouseLeave(e); + m_mouse_in_control = false; + if (_HoverNode != null) + { + _HoverNode.OnMouseLeave(e); + } + _HoverNode = null; + Invalidate(); + } + + protected override void OnMouseWheel(WpfMouseWheelEventArgs e) + { + base.OnMouseWheel(e); + STNodeMouseEventArgs nodeEvent = CreateMouseEventArgs(e); + m_pt_in_control = nodeEvent.Location; + m_pt_in_canvas.X = ((float)nodeEvent.X - _CanvasOffsetX) / _CanvasScale; + m_pt_in_canvas.Y = ((float)nodeEvent.Y - _CanvasOffsetY) / _CanvasScale; + float scale = _CanvasScale + (nodeEvent.Delta < 0 ? -0.05f : 0.05f); + ScaleCanvas(scale, nodeEvent.X, nodeEvent.Y); + e.Handled = true; + } + + protected virtual void OnMouseHWheel(STNodeMouseEventArgs e) + { + if (m_mouse_in_control && _HoverNode != null) + { + _HoverNode.OnMouseHWheel(e.WithLocation((int)m_pt_in_canvas.X - _HoverNode.Left, (int)m_pt_in_canvas.Y - _HoverNode.Top)); + } + } + + private void ProcessMouseClick(STNodeMouseEventArgs e) + { + if (_ActiveNode != null && m_is_process_mouse_event && PointInRectangle(_ActiveNode.Rectangle, m_pt_in_canvas.X, m_pt_in_canvas.Y)) + { + _ActiveNode.OnMouseClick(e.WithLocation((int)m_pt_down_in_canvas.X - _ActiveNode.Left, (int)m_pt_down_in_canvas.Y - _ActiveNode.Top)); + } + } + + protected override void OnKeyDown(WpfKeyEventArgs e) + { + base.OnKeyDown(e); + if (m_enableEdit && Keyboard.Modifiers == ModifierKeys.None) + { + int offsetX = 0; + int offsetY = 0; + switch (e.Key) + { + case Key.Left: + offsetX = -10; + break; + case Key.Right: + offsetX = 10; + break; + case Key.Up: + offsetY = -10; + break; + case Key.Down: + offsetY = 10; + break; + } + if ((offsetX != 0 || offsetY != 0) && MoveSelectedNodes(offsetX, offsetY)) + { + e.Handled = true; + return; + } + } + if (_ActiveNode != null) + { + _ActiveNode.OnKeyDown(e); + } + } + + protected override void OnKeyUp(WpfKeyEventArgs e) + { + base.OnKeyUp(e); + if (_ActiveNode != null) + { + _ActiveNode.OnKeyUp(e); + } + m_node_down = null; + } + + protected override void OnTextInput(WpfTextCompositionEventArgs e) + { + base.OnTextInput(e); + if (_ActiveNode != null && !string.IsNullOrEmpty(e.Text)) + { + var args = new STNodeKeyPressEventArgs(e.Text[0]); + _ActiveNode.OnKeyPress(args); + e.Handled = args.Handled; + } + } + + protected override void OnDragEnter(WpfDragEventArgs e) + { + base.OnDragEnter(e); + if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) + { + return; + } + e.Effects = e.Data.GetDataPresent("STNodeType") + ? System.Windows.DragDropEffects.Copy + : System.Windows.DragDropEffects.None; + e.Handled = true; + } + + protected override void OnDrop(WpfDragEventArgs e) + { + base.OnDrop(e); + if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this) || !e.Data.GetDataPresent("STNodeType")) + { + return; + } + if (e.Data.GetData("STNodeType") is Type type && type.IsSubclassOf(typeof(STNode))) + { + STNode node = (STNode)Activator.CreateInstance(type); + node.Create(); + WpfPoint position = e.GetPosition(this); + Point canvasPoint = ControlToCanvas(new Point((int)Math.Round(position.X), (int)Math.Round(position.Y))); + node.Left = canvasPoint.X; + node.Top = canvasPoint.Y; + Nodes.Add(node); + e.Handled = true; + } + } + + private STNodeMouseEventArgs CreateMouseEventArgs(WpfMouseEventArgs e) + { + WpfPoint position = e.GetPosition(this); + STMouseButtons buttons; + int clicks = 0; + if (e is WpfMouseButtonEventArgs buttonEvent) + { + buttons = buttonEvent.ChangedButton switch + { + MouseButton.Left => STMouseButtons.Left, + MouseButton.Right => STMouseButtons.Right, + MouseButton.Middle => STMouseButtons.Middle, + MouseButton.XButton1 => STMouseButtons.XButton1, + MouseButton.XButton2 => STMouseButtons.XButton2, + _ => STMouseButtons.None + }; + clicks = buttonEvent.ClickCount; + } + else + { + buttons = STMouseButtons.None; + if (e.LeftButton == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.Left; + } + if (e.RightButton == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.Right; + } + if (e.MiddleButton == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.Middle; + } + if (e.XButton1 == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.XButton1; + } + if (e.XButton2 == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.XButton2; + } + } + int delta = e is WpfMouseWheelEventArgs wheelEvent ? wheelEvent.Delta : 0; + return new STNodeMouseEventArgs( + buttons, + clicks, + (int)Math.Round(position.X), + (int)Math.Round(position.Y), + delta); + } + + protected virtual void OnDrawGrid(DrawingTools dt, int nWidth, int nHeight) + { + Graphics graphics = dt.Graphics; + using Pen pen = new Pen(Color.FromArgb(65, _GridColor)); + using Pen pen2 = new Pen(Color.FromArgb(30, _GridColor)); + float num = 20f * _CanvasScale; + int num2 = 5 - (int)(_CanvasOffsetX / num); + for (float num3 = _CanvasOffsetX % num; num3 < (float)nWidth; num3 += num) + { + graphics.DrawLine((num2++ % 5 == 0) ? pen : pen2, num3, 0f, num3, nHeight); + } + num2 = 5 - (int)(_CanvasOffsetY / num); + for (float num4 = _CanvasOffsetY % num; num4 < (float)nHeight; num4 += num) + { + graphics.DrawLine((num2++ % 5 == 0) ? pen : pen2, 0f, num4, nWidth, num4); + } + if (_HighlightGridOrigin) + { + pen2.Color = Color.FromArgb((_Nodes.Count == 0) ? 255 : 120, _GridColor); + graphics.DrawLine(pen2, _CanvasOffsetX, 0f, _CanvasOffsetX, nHeight); + graphics.DrawLine(pen2, 0f, _CanvasOffsetY, nWidth, _CanvasOffsetY); + } + } + + protected virtual void OnDrawNode(DrawingTools dt, Rectangle rect) + { + m_lst_node_out.Clear(); + foreach (STNode node in _Nodes) + { + if (!rect.IntersectsWith(node.Rectangle)) + { + m_lst_node_out.Add(node.Location); + continue; + } + if (_ShowBorder) + { + OnDrawNodeBorder(dt, node); + } + node.OnDrawNode(dt); + if (!string.IsNullOrEmpty(node.Mark)) + { + node.OnDrawMark(dt); + } + if (_ShowBorder) + { + OnDrawNodeSelection(dt, node); + } + } + } + + protected virtual void OnDrawNodeBorder(DrawingTools dt, STNode node) + { + bool isActive = _ActiveNode == node; + bool isSelected = node.IsSelected; + bool isHovered = _HoverNode == node; + if (!_ShowNodeShadow) + { + if (!isHovered || isActive || isSelected) + { + return; + } + DrawNodeOutline(dt.Graphics, node.Rectangle, _BorderHoverColor, 1f, inset: true); + return; + } + + Image image = isActive ? m_img_border_active : (isSelected ? m_img_border_selected : (isHovered ? m_img_border_hover : m_img_border)); + RenderBorder(dt.Graphics, node.Rectangle, image); + if (!string.IsNullOrEmpty(node.Mark)) + { + RenderBorder(dt.Graphics, node.MarkRectangle, image); + } + } + + protected virtual void OnDrawNodeSelection(DrawingTools dt, STNode node) + { + bool isActive = _ActiveNode == node; + if (!isActive && !node.IsSelected) + { + return; + } + + float canvasScale = Math.Max(_CanvasScale, 0.2f); + Color outlineColor = isActive ? _BorderActiveColor : _BorderSelectedColor; + float outlineWidth = (isActive ? 2f : 1.5f) / canvasScale; + DrawNodeOutline(dt.Graphics, node.Rectangle, outlineColor, outlineWidth, inset: true); + } + + private void DrawNodeOutline(Graphics graphics, Rectangle rectangle, Color color, float width, bool inset) + { + GraphicsState graphicsState = graphics.Save(); + graphics.SmoothingMode = SmoothingMode.AntiAlias; + float pathInset = inset ? width / 2f : 0f; + RectangleF outlineRectangle = new RectangleF( + rectangle.Left + pathInset, + rectangle.Top + pathInset, + Math.Max(0f, rectangle.Width - pathInset * 2f), + Math.Max(0f, rectangle.Height - pathInset * 2f)); + using GraphicsPath outlinePath = CreateRoundedRectanglePath(outlineRectangle, _NodeCornerRadius); + using Pen outlinePen = new Pen(color, width); + graphics.DrawPath(outlinePen, outlinePath); + graphics.Restore(graphicsState); + } + + private static GraphicsPath CreateRoundedRectanglePath(RectangleF rectangle, float radius) + { + GraphicsPath path = new GraphicsPath(); + float maxRadius = Math.Min(rectangle.Width, rectangle.Height) / 2f; + radius = Math.Min(radius, maxRadius); + if (radius <= 0f) + { + path.AddRectangle(rectangle); + return path; + } + + float diameter = radius * 2f; + path.AddArc(rectangle.Left, rectangle.Top, diameter, diameter, 180f, 90f); + path.AddArc(rectangle.Right - diameter, rectangle.Top, diameter, diameter, 270f, 90f); + path.AddArc(rectangle.Right - diameter, rectangle.Bottom - diameter, diameter, diameter, 0f, 90f); + path.AddArc(rectangle.Left, rectangle.Bottom - diameter, diameter, diameter, 90f, 90f); + path.CloseFigure(); + return path; + } + + protected virtual void OnDrawConnectedLine(DrawingTools dt) + { + Graphics graphics = dt.Graphics; + graphics.SmoothingMode = SmoothingMode.HighQuality; + m_p_line_hover.Color = Color.FromArgb(10, 0, 0, 0); + Type typeFromHandle = typeof(object); + foreach (STNode node in _Nodes) + { + foreach (STNodeOption outputOption in node.OutputOptions) + { + if (outputOption == STNodeOption.Empty) + { + continue; + } + if (outputOption.DotColor != Color.Transparent) + { + m_p_line.Color = outputOption.DotColor; + } + else if (outputOption.DataType == typeFromHandle) + { + m_p_line.Color = _UnknownTypeColor; + } + else + { + m_p_line.Color = (_TypeColor.ContainsKey(outputOption.DataType) ? _TypeColor[outputOption.DataType] : _UnknownTypeColor); + } + foreach (STNodeOption item in outputOption.ConnectedOption) + { + float x1 = outputOption.DotLeft + outputOption.DotSize; + float y1 = outputOption.DotTop + outputOption.DotSize / 2; + float x2 = item.DotLeft - 1; + float y2 = item.DotTop + item.DotSize / 2; + DrawBezier(graphics, m_p_line_hover, x1, y1, x2, y2, _Curvature); + DrawBezier(graphics, m_p_line, x1, y1, x2, y2, _Curvature); + if (m_is_buildpath) + { + GraphicsPath key = CreateBezierPath(x1, y1, x2, y2, _Curvature); + m_dic_gp_info.Add(key, new ConnectionInfo + { + Output = outputOption, + Input = item + }); + } + } + } + } + m_p_line_hover.Color = _HighLineColor; + if (m_gp_hover != null && m_dic_gp_info.ContainsKey(m_gp_hover)) + { + graphics.DrawPath(m_p_line_hover, m_gp_hover); + } + else + { + m_gp_hover = null; + } + m_is_buildpath = false; + } + + protected virtual void OnDrawMark(DrawingTools dt) + { + Graphics graphics = dt.Graphics; + SizeF sizeF = graphics.MeasureString(m_find.Mark, Font); + Rectangle rectangle = new Rectangle(m_pt_in_control.X + 15, m_pt_in_control.Y + 10, (int)sizeF.Width + 6, 4 + (Font.Height + 4) * m_find.MarkLines.Length); + if (rectangle.Right > ClientSize.Width) + { + rectangle.X = ClientSize.Width - rectangle.Width; + } + if (rectangle.Bottom > ClientSize.Height) + { + rectangle.Y = ClientSize.Height - rectangle.Height; + } + if (rectangle.X < 0) + { + rectangle.X = 0; + } + if (rectangle.Y < 0) + { + rectangle.Y = 0; + } + dt.SolidBrush.Color = _MarkBackColor; + graphics.SmoothingMode = SmoothingMode.None; + graphics.FillRectangle(dt.SolidBrush, rectangle); + rectangle.Width--; + rectangle.Height--; + dt.Pen.Color = Color.FromArgb(255, _MarkBackColor); + graphics.DrawRectangle(dt.Pen, rectangle); + dt.SolidBrush.Color = _MarkForeColor; + m_sf.LineAlignment = StringAlignment.Center; + rectangle.X += 2; + rectangle.Width -= 3; + rectangle.Height = Font.Height + 4; + int num = rectangle.Y + 2; + for (int i = 0; i < m_find.MarkLines.Length; i++) + { + rectangle.Y = num + i * (Font.Height + 4); + graphics.DrawString(m_find.MarkLines[i], Font, dt.SolidBrush, rectangle, m_sf); + } + } + + protected virtual void OnDrawMagnet(DrawingTools dt, MagnetInfo mi) + { + if (_ActiveNode == null) + { + return; + } + Graphics graphics = dt.Graphics; + GraphicsState state = graphics.Save(); + Pen pen = m_drawing_tools.Pen; + SolidBrush solidBrush = dt.SolidBrush; + pen.Color = _MagnetColor; + solidBrush.Color = Color.FromArgb(_MagnetColor.A / 3, _MagnetColor); + graphics.SmoothingMode = SmoothingMode.None; + int left = _ActiveNode.Left; + int num = _ActiveNode.Left + _ActiveNode.Width / 2; + int right = _ActiveNode.Right; + int top = _ActiveNode.Top; + int num2 = _ActiveNode.Top + _ActiveNode.Height / 2; + int bottom = _ActiveNode.Bottom; + if (mi.XMatched) + { + graphics.DrawLine(pen, CanvasToControl(mi.X, isX: true), 0f, CanvasToControl(mi.X, isX: true), ClientSize.Height); + } + if (mi.YMatched) + { + graphics.DrawLine(pen, 0f, CanvasToControl(mi.Y, isX: false), ClientSize.Width, CanvasToControl(mi.Y, isX: false)); + } + graphics.TranslateTransform(_CanvasOffsetX, _CanvasOffsetY); + graphics.ScaleTransform(_CanvasScale, _CanvasScale); + if (mi.XMatched) + { + foreach (STNode node in _Nodes) + { + if (node.Left == mi.X || node.Right == mi.X || node.Left + node.Width / 2 == mi.X) + { + graphics.FillRectangle(solidBrush, node.Rectangle); + } + } + } + if (mi.YMatched) + { + foreach (STNode node2 in _Nodes) + { + if (node2.Top == mi.Y || node2.Bottom == mi.Y || node2.Top + node2.Height / 2 == mi.Y) + { + graphics.FillRectangle(solidBrush, node2.Rectangle); + } + } + } + graphics.Restore(state); + } + + protected virtual void OnDrawSelectedRectangle(DrawingTools dt, RectangleF rectf) + { + Graphics graphics = dt.Graphics; + SolidBrush solidBrush = dt.SolidBrush; + dt.Pen.Color = _SelectedRectangleColor; + graphics.DrawRectangle(dt.Pen, rectf.Left, rectf.Y, rectf.Width, rectf.Height); + solidBrush.Color = Color.FromArgb(_SelectedRectangleColor.A / 3, _SelectedRectangleColor); + graphics.FillRectangle(solidBrush, CanvasToControl(m_rect_select)); + } + + protected virtual void OnDrawNodeOutLocation(DrawingTools dt, Size sz, List lstPts) + { + Graphics graphics = dt.Graphics; + SolidBrush solidBrush = dt.SolidBrush; + solidBrush.Color = _LocationBackColor; + graphics.SmoothingMode = SmoothingMode.None; + if (lstPts.Count == _Nodes.Count && _Nodes.Count != 0) + { + graphics.FillRectangle(solidBrush, CanvasToControl(_CanvasValidBounds)); + } + graphics.FillRectangle(solidBrush, 0, 0, 4, sz.Height); + graphics.FillRectangle(solidBrush, sz.Width - 4, 0, 4, sz.Height); + graphics.FillRectangle(solidBrush, 4, 0, sz.Width - 8, 4); + graphics.FillRectangle(solidBrush, 4, sz.Height - 4, sz.Width - 8, 4); + solidBrush.Color = _LocationForeColor; + foreach (Point lstPt in lstPts) + { + Point point = CanvasToControl(lstPt); + if (point.X < 0) + { + point.X = 0; + } + if (point.Y < 0) + { + point.Y = 0; + } + if (point.X > sz.Width) + { + point.X = sz.Width - 4; + } + if (point.Y > sz.Height) + { + point.Y = sz.Height - 4; + } + graphics.FillRectangle(solidBrush, point.X, point.Y, 4, 4); + } + } + + protected virtual void OnDrawAlert(DrawingTools dt, Rectangle rect, string strText, Color foreColor, Color backColor, AlertLocation al) + { + if (m_alpha_alert != 0) + { + Graphics graphics = dt.Graphics; + SolidBrush solidBrush = dt.SolidBrush; + graphics.SmoothingMode = SmoothingMode.None; + solidBrush.Color = backColor; + dt.Pen.Color = solidBrush.Color; + graphics.FillRectangle(solidBrush, rect); + graphics.DrawRectangle(dt.Pen, rect.Left, rect.Top, rect.Width - 1, rect.Height - 1); + solidBrush.Color = foreColor; + m_sf.Alignment = StringAlignment.Center; + m_sf.LineAlignment = StringAlignment.Center; + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.DrawString(strText, Font, solidBrush, rect, m_sf); + } + } + + protected virtual void OnDrawCanvasDragLockButton(DrawingTools dt) + { + const int size = 28; + const int margin = 8; + m_rect_canvas_drag_lock = new Rectangle(ClientSize.Width - size - margin, margin, size, size); + + Graphics graphics = dt.Graphics; + Color backColor = EnableBlankLeftDragCanvas + ? Color.FromArgb(220, 40, 120, 60) + : Color.FromArgb(190, 35, 35, 35); + Color borderColor = EnableBlankLeftDragCanvas + ? Color.FromArgb(240, 92, 196, 112) + : Color.FromArgb(220, 110, 110, 110); + + using SolidBrush backgroundBrush = new SolidBrush(backColor); + using Pen borderPen = new Pen(borderColor); + graphics.FillRectangle(backgroundBrush, m_rect_canvas_drag_lock); + graphics.DrawRectangle(borderPen, m_rect_canvas_drag_lock); + + using Font iconFont = new Font("Segoe MDL2 Assets", 13f, System.Drawing.FontStyle.Regular, GraphicsUnit.Point); + using SolidBrush iconBrush = new SolidBrush(Color.White); + using StringFormat iconFormat = new StringFormat + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Center + }; + graphics.DrawString(EnableBlankLeftDragCanvas ? "\uE72E" : "\uE785", iconFont, iconBrush, m_rect_canvas_drag_lock, iconFormat); + } + + protected virtual Rectangle GetAlertRectangle(Graphics g, string strText, AlertLocation al) + { + SizeF sizeF = g.MeasureString(m_str_alert, Font); + Size size = new Size((int)Math.Round(sizeF.Width + 10f), (int)Math.Round(sizeF.Height + 4f)); + Rectangle result = new Rectangle(4, ClientSize.Height - size.Height - 4, size.Width, size.Height); + switch (al) + { + case AlertLocation.Left: + result.Y = ClientSize.Height - size.Height >> 1; + break; + case AlertLocation.Top: + result.Y = 4; + result.X = ClientSize.Width - size.Width >> 1; + break; + case AlertLocation.Right: + result.X = ClientSize.Width - size.Width - 4; + result.Y = ClientSize.Height - size.Height >> 1; + break; + case AlertLocation.Bottom: + result.X = ClientSize.Width - size.Width >> 1; + break; + case AlertLocation.Center: + result.X = ClientSize.Width - size.Width >> 1; + result.Y = ClientSize.Height - size.Height >> 1; + break; + case AlertLocation.LeftTop: + { + int num = (result.Y = 4); + result.X = num; + break; + } + case AlertLocation.RightTop: + result.Y = 4; + result.X = ClientSize.Width - size.Width - 4; + break; + case AlertLocation.RightBottom: + result.X = ClientSize.Width - size.Width - 4; + break; + } + return result; + } + + internal void BuildLinePath() + { + m_gp_hover = null; + foreach (KeyValuePair item in m_dic_gp_info) + { + item.Key.Dispose(); + } + m_dic_gp_info.Clear(); + m_is_buildpath = true; + Invalidate(); + } + + internal void OnDrawAlert(Graphics g) + { + m_rect_alert = GetAlertRectangle(g, m_str_alert, m_al); + Color foreColor = Color.FromArgb((int)((float)m_alpha_alert / 255f * (float)(int)m_forecolor_alert.A), m_forecolor_alert); + Color backColor = Color.FromArgb((int)((float)m_alpha_alert / 255f * (float)(int)m_backcolor_alert.A), m_backcolor_alert); + OnDrawAlert(m_drawing_tools, m_rect_alert, m_str_alert, foreColor, backColor, m_al); + } + + internal void InternalAddSelectedNode(STNode node) + { + node.IsSelected = true; + lock (m_hs_node_selected) + { + m_hs_node_selected.Add(node); + } + UpdateCanvasDragModeFromSelection(); + } + + internal void InternalRemoveSelectedNode(STNode node) + { + node.IsSelected = false; + lock (m_hs_node_selected) + { + m_hs_node_selected.Remove(node); + } + UpdateCanvasDragModeFromSelection(); + } + + private Image CreateBorderImage(Color clr) + { + Image image = new Bitmap(12, 12); + using Graphics graphics = Graphics.FromImage(image); + graphics.SmoothingMode = SmoothingMode.HighQuality; + using GraphicsPath graphicsPath = new GraphicsPath(); + graphicsPath.AddEllipse(new Rectangle(0, 0, 11, 11)); + using PathGradientBrush pathGradientBrush = new PathGradientBrush(graphicsPath); + pathGradientBrush.CenterColor = Color.FromArgb(200, clr); + pathGradientBrush.SurroundColors = new Color[1] { Color.FromArgb(10, clr) }; + graphics.FillPath(pathGradientBrush, graphicsPath); + return image; + } + + private ConnectionStatus DisConnectionHover() + { + GraphicsPath gpHover = m_gp_hover; + if (gpHover == null || !m_dic_gp_info.TryGetValue(gpHover, out ConnectionInfo connectionInfo)) + { + return ConnectionStatus.DisConnected; + } + ConnectionStatus connectionStatus = connectionInfo.Output.DisConnectOption(connectionInfo.Input); + if (connectionStatus == ConnectionStatus.DisConnected) + { + m_dic_gp_info.Remove(gpHover); + gpHover.Dispose(); + if (ReferenceEquals(m_gp_hover, gpHover)) + { + m_gp_hover = null; + } + Invalidate(); + } + return connectionStatus; + } + + private void StartConnect(STNodeOption op) + { + if (op.IsInput) + { + m_pt_dot_down.X = op.DotLeft; + m_pt_dot_down.Y = op.DotTop + 5; + } + else + { + m_pt_dot_down.X = op.DotLeft + op.DotSize; + m_pt_dot_down.Y = op.DotTop + 5; + } + m_ca = CanvasAction.ConnectOption; + m_option_down = op; + BeginPointerEdit("连接节点"); + } + + public void AlignTop() + { + if (m_hs_node_selected.Count <= 1) + { + return; + } + using STNodeEditTransaction transaction = BeginEditTransaction("顶部对齐"); + STNode sTNode = m_hs_node_selected.First(); + lock (m_hs_node_selected) + { + foreach (STNode item in m_hs_node_selected) + { + if (item != sTNode) + { + item.Top = sTNode.Top; + } + } + } + } + + public void AlignLeft() + { + if (m_hs_node_selected.Count <= 1) + { + return; + } + using STNodeEditTransaction transaction = BeginEditTransaction("左对齐"); + STNode sTNode = m_hs_node_selected.First(); + lock (m_hs_node_selected) + { + foreach (STNode item in m_hs_node_selected) + { + if (item != sTNode) + { + item.Left = sTNode.Left; + } + } + } + } + + public void AlignVerticalCenter() + { + if (m_hs_node_selected.Count <= 1) + { + return; + } + using STNodeEditTransaction transaction = BeginEditTransaction("垂直居中"); + STNode sTNode = m_hs_node_selected.First(); + int num = sTNode.Left + sTNode.Width / 2; + lock (m_hs_node_selected) + { + foreach (STNode item in m_hs_node_selected) + { + if (item != sTNode) + { + item.Left = num - item.Width / 2; + } + } + } + } + + public void AlignHorizontalCenter() + { + if (m_hs_node_selected.Count <= 1) + { + return; + } + using STNodeEditTransaction transaction = BeginEditTransaction("水平居中"); + STNode sTNode = m_hs_node_selected.First(); + int num = sTNode.Top + sTNode.Height / 2; + lock (m_hs_node_selected) + { + foreach (STNode item in m_hs_node_selected) + { + if (item != sTNode) + { + item.Top = num - item.Height / 2; + } + } + } + } + + public void AlignHorizontalDistance() + { + if (m_hs_node_selected.Count <= 1) + { + return; + } + using STNodeEditTransaction transaction = BeginEditTransaction("水平等距"); + List source = m_hs_node_selected.ToList(); + int num = source.Sum((STNode x) => x.Width); + List list = source.OrderBy((STNode p) => p.Left).ToList(); + int num2 = list.Last().Right - list.First().Left; + int num3 = (num2 - num) / (list.Count - 1); + if (num3 < 50) + { + num3 = 50; + } + lock (m_hs_node_selected) + { + for (int num4 = 1; num4 < list.Count; num4++) + { + STNode sTNode = list[num4]; + STNode sTNode2 = list[num4 - 1]; + sTNode.Left = sTNode2.Left + sTNode2.Width + num3; + } + } + } + + public void AlignVerticalDistance() + { + if (m_hs_node_selected.Count <= 1) + { + return; + } + using STNodeEditTransaction transaction = BeginEditTransaction("垂直等距"); + List source = m_hs_node_selected.ToList(); + int num = source.Sum((STNode x) => x.Height); + List list = source.OrderBy((STNode p) => p.Top).ToList(); + int num2 = list.Last().Bottom - list.First().Top; + int num3 = (num2 - num) / (list.Count - 1); + if (num3 < 20) + { + num3 = 20; + } + lock (m_hs_node_selected) + { + for (int num4 = 1; num4 < list.Count; num4++) + { + STNode sTNode = list[num4]; + STNode sTNode2 = list[num4 - 1]; + sTNode.Top = sTNode2.Top + sTNode2.Height + num3; + } + } + } + + private void MoveNode(Point pt) + { + int num = (int)((float)(pt.X - m_pt_down_in_control.X) / _CanvasScale); + int num2 = (int)((float)(pt.Y - m_pt_down_in_control.Y) / _CanvasScale); + lock (m_hs_node_selected) + { + foreach (STNode item in m_hs_node_selected) + { + item.Left = m_dic_pt_selected[item].X + num; + item.Top = m_dic_pt_selected[item].Y + num2; + } + if (_ShowMagnet) + { + MagnetInfo magnetInfo = CheckMagnet(_ActiveNode); + if (magnetInfo.XMatched) + { + foreach (STNode item2 in m_hs_node_selected) + { + item2.Left -= magnetInfo.OffsetX; + } + } + if (magnetInfo.YMatched) + { + foreach (STNode item3 in m_hs_node_selected) + { + item3.Top -= magnetInfo.OffsetY; + } + } + } + } + Invalidate(); + } + + protected internal virtual void BuildBounds() + { + if (_Nodes.Count == 0) + { + _CanvasValidBounds = ControlToCanvas(ClientRectangle); + return; + } + int num = int.MaxValue; + int num2 = int.MaxValue; + int num3 = int.MinValue; + int num4 = int.MinValue; + foreach (STNode node in _Nodes) + { + if (num > node.Left) + { + num = node.Left; + } + if (num2 > node.Top) + { + num2 = node.Top; + } + if (num3 < node.Right) + { + num3 = node.Right; + } + if (num4 < node.Bottom) + { + num4 = node.Bottom; + } + } + _CanvasValidBounds.X = num - 60; + _CanvasValidBounds.Y = num2 - 60; + _CanvasValidBounds.Width = num3 - num + 120; + _CanvasValidBounds.Height = num4 - num2 + 120; + } + + private bool PointInRectangle(Rectangle rect, float x, float y) + { + if (x < (float)rect.Left) + { + return false; + } + if (x > (float)rect.Right) + { + return false; + } + if (y < (float)rect.Top) + { + return false; + } + if (y > (float)rect.Bottom) + { + return false; + } + return true; + } + + private void BuildMagnetLocation() + { + m_lst_magnet_x.Clear(); + m_lst_magnet_y.Clear(); + foreach (STNode node in _Nodes) + { + if (!node.IsSelected) + { + m_lst_magnet_x.Add(node.Left); + m_lst_magnet_x.Add(node.Left + node.Width / 2); + m_lst_magnet_x.Add(node.Left + node.Width); + m_lst_magnet_y.Add(node.Top); + m_lst_magnet_y.Add(node.Top + node.Height / 2); + m_lst_magnet_y.Add(node.Top + node.Height); + } + } + } + + private MagnetInfo CheckMagnet(STNode node) + { + m_mi.XMatched = (m_mi.YMatched = false); + m_lst_magnet_mx.Clear(); + m_lst_magnet_my.Clear(); + m_lst_magnet_mx.Add(node.Left + node.Width / 2); + m_lst_magnet_mx.Add(node.Left); + m_lst_magnet_mx.Add(node.Left + node.Width); + m_lst_magnet_my.Add(node.Top + node.Height / 2); + m_lst_magnet_my.Add(node.Top); + m_lst_magnet_my.Add(node.Top + node.Height); + bool flag = false; + foreach (int item in m_lst_magnet_mx) + { + foreach (int item2 in m_lst_magnet_x) + { + if (Math.Abs(item - item2) <= 5) + { + flag = true; + m_mi.X = item2; + m_mi.OffsetX = item - item2; + m_mi.XMatched = true; + break; + } + } + if (flag) + { + break; + } + } + flag = false; + foreach (int item3 in m_lst_magnet_my) + { + foreach (int item4 in m_lst_magnet_y) + { + if (Math.Abs(item3 - item4) <= 5) + { + flag = true; + m_mi.Y = item4; + m_mi.OffsetY = item3 - item4; + m_mi.YMatched = true; + break; + } + } + if (flag) + { + break; + } + } + return m_mi; + } + + private void DrawBezier(Graphics g, Pen p, PointF ptStart, PointF ptEnd, float f) + { + DrawBezier(g, p, ptStart.X, ptStart.Y, ptEnd.X, ptEnd.Y, f); + } + + private void DrawBezier(Graphics g, Pen p, float x1, float y1, float x2, float y2, float f) + { + using GraphicsPath connectionPath = CreateBezierPath(x1, y1, x2, y2, f); + g.DrawPath(p, connectionPath); + } + + protected internal static GraphicsPath CreateBezierPath(float x1, float y1, float x2, float y2, float f) + { + GraphicsPath graphicsPath = new GraphicsPath(); + if (x2 < x1 && Math.Abs(y2 - y1) < 60f) + { + float routeY = Math.Max(y1, y2) + 70f; + float startGutterX = x1 + 30f; + float endGutterX = x2 - 30f; + float midpointX = (x1 + x2) / 2f; + graphicsPath.AddBezier(x1, y1, startGutterX, y1, startGutterX, routeY, midpointX, routeY); + graphicsPath.AddBezier(midpointX, routeY, endGutterX, routeY, endGutterX, y2, x2, y2); + return graphicsPath; + } + + float num = Math.Abs(x1 - x2) * f; + if (f != 0f && num < 30f) + { + num = 30f; + } + graphicsPath.AddBezier(x1, y1, x1 + num, y1, x2 - num, y2, x2, y2); + return graphicsPath; + } + + private void RenderBorder(Graphics g, Rectangle rect, Image img) + { + g.DrawImage(img, new Rectangle(rect.X - 5, rect.Y - 5, 5, 5), new Rectangle(0, 0, 5, 5), GraphicsUnit.Pixel); + g.DrawImage(img, new Rectangle(rect.Right, rect.Y - 5, 5, 5), new Rectangle(img.Width - 5, 0, 5, 5), GraphicsUnit.Pixel); + g.DrawImage(img, new Rectangle(rect.X - 5, rect.Bottom, 5, 5), new Rectangle(0, img.Height - 5, 5, 5), GraphicsUnit.Pixel); + g.DrawImage(img, new Rectangle(rect.Right, rect.Bottom, 5, 5), new Rectangle(img.Width - 5, img.Height - 5, 5, 5), GraphicsUnit.Pixel); + g.DrawImage(img, new Rectangle(rect.X - 5, rect.Y, 5, rect.Height), new Rectangle(0, 5, 5, img.Height - 10), GraphicsUnit.Pixel); + g.DrawImage(img, new Rectangle(rect.X, rect.Y - 5, rect.Width, 5), new Rectangle(5, 0, img.Width - 10, 5), GraphicsUnit.Pixel); + g.DrawImage(img, new Rectangle(rect.Right, rect.Y, 5, rect.Height), new Rectangle(img.Width - 5, 5, 5, img.Height - 10), GraphicsUnit.Pixel); + g.DrawImage(img, new Rectangle(rect.X, rect.Bottom, rect.Width, 5), new Rectangle(5, img.Height - 5, img.Width - 10, 5), GraphicsUnit.Pixel); + } + + public void Invalidate() + { + if (m_disposed || Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished) + { + return; + } + if (Dispatcher.CheckAccess()) + { + InvalidateVisual(); + return; + } + _ = Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(InvalidateVisual)); + } + + public void Invalidate(Rectangle rectangle) + { + Invalidate(); + } + + public Graphics CreateGraphics() + { + if (m_disposed) + { + throw new ObjectDisposedException(nameof(STNodeEditor)); + } + return Graphics.FromImage(m_measurement_bitmap); + } + + public IAsyncResult BeginInvoke(Delegate method) + { + return BeginInvoke(method, null); + } + + public IAsyncResult BeginInvoke(Delegate method, params object[] args) + { + if (method == null + || m_disposed + || Dispatcher.HasShutdownStarted + || Dispatcher.HasShutdownFinished) + { + return null; + } + try + { + DispatcherOperation operation = Dispatcher.BeginInvoke( + DispatcherPriority.Normal, + new Action(() => + { + if (!m_disposed) + { + method.DynamicInvoke(args ?? Array.Empty()); + } + })); + return operation.Task; + } + catch (InvalidOperationException) when ( + m_disposed + || Dispatcher.HasShutdownStarted + || Dispatcher.HasShutdownFinished) + { + return null; + } + } + + public object Invoke(Delegate method) + { + return Invoke(method, null); + } + + public object Invoke(Delegate method, params object[] args) + { + if (method == null || m_disposed) + { + return null; + } + if (Dispatcher.CheckAccess()) + { + return method.DynamicInvoke(args ?? Array.Empty()); + } + return Dispatcher.Invoke(() => method.DynamicInvoke(args ?? Array.Empty())); + } + + public Point PointToClient(Point point) + { + try + { + WpfPoint result = PointFromScreen(new WpfPoint(point.X, point.Y)); + return new Point((int)Math.Round(result.X), (int)Math.Round(result.Y)); + } + catch (InvalidOperationException) + { + return point; + } + } + + public Point PointToScreen(Point point) + { + try + { + WpfPoint result = base.PointToScreen(new WpfPoint(point.X, point.Y)); + return new Point((int)Math.Round(result.X), (int)Math.Round(result.Y)); + } + catch (InvalidOperationException) + { + return point; + } + } + + public Rectangle RectangleToScreen(Rectangle rectangle) + { + Point topLeft = PointToScreen(rectangle.Location); + Point bottomRight = PointToScreen(new Point(rectangle.Right, rectangle.Bottom)); + return Rectangle.FromLTRB(topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y); + } + + public NodeFindInfo FindNodeFromPoint(PointF pt) + { + return FindNodeFromPoint(pt, 6); + } + + public NodeFindInfo FindNodeFromPoint(PointF pt, int dotPadding) + { + m_find.Node = null; + m_find.NodeOption = null; + m_find.Mark = null; + for (int num = _Nodes.Count - 1; num >= 0; num--) + { + if (!string.IsNullOrEmpty(_Nodes[num].Mark) && PointInRectangle(_Nodes[num].MarkRectangle, pt.X, pt.Y)) + { + m_find.Mark = _Nodes[num].Mark; + m_find.MarkLines = _Nodes[num].MarkLines; + return m_find; + } + foreach (STNodeOption inputOption in _Nodes[num].InputOptions) + { + if (inputOption != STNodeOption.Empty && PointInRectangle(Rectangle.Inflate(inputOption.DotRectangle, dotPadding, dotPadding), pt.X, pt.Y)) + { + m_find.NodeOption = inputOption; + } + } + foreach (STNodeOption outputOption in _Nodes[num].OutputOptions) + { + if (outputOption != STNodeOption.Empty && PointInRectangle(Rectangle.Inflate(outputOption.DotRectangle, dotPadding, dotPadding), pt.X, pt.Y)) + { + m_find.NodeOption = outputOption; + } + } + if (PointInRectangle(_Nodes[num].Rectangle, pt.X, pt.Y)) + { + m_find.Node = _Nodes[num]; + } + if (m_find.NodeOption != null || m_find.Node != null) + { + return m_find; + } + } + return m_find; + } + + public STNode[] GetSelectedNode() + { + return m_hs_node_selected.ToArray(); + } + + public float CanvasToControl(float number, bool isX) + { + return number * _CanvasScale + (isX ? _CanvasOffsetX : _CanvasOffsetY); + } + + public PointF CanvasToControl(PointF pt) + { + pt.X = pt.X * _CanvasScale + _CanvasOffsetX; + pt.Y = pt.Y * _CanvasScale + _CanvasOffsetY; + return pt; + } + + public Point CanvasToControl(Point pt) + { + pt.X = (int)((float)pt.X * _CanvasScale + _CanvasOffsetX); + pt.Y = (int)((float)pt.Y * _CanvasScale + _CanvasOffsetY); + return pt; + } + + public Rectangle CanvasToControl(Rectangle rect) + { + rect.X = (int)((float)rect.X * _CanvasScale + _CanvasOffsetX); + rect.Y = (int)((float)rect.Y * _CanvasScale + _CanvasOffsetY); + rect.Width = (int)((float)rect.Width * _CanvasScale); + rect.Height = (int)((float)rect.Height * _CanvasScale); + return rect; + } + + public RectangleF CanvasToControl(RectangleF rect) + { + rect.X = rect.X * _CanvasScale + _CanvasOffsetX; + rect.Y = rect.Y * _CanvasScale + _CanvasOffsetY; + rect.Width *= _CanvasScale; + rect.Height *= _CanvasScale; + return rect; + } + + public float ControlToCanvas(float number, bool isX) + { + return (number - (isX ? _CanvasOffsetX : _CanvasOffsetY)) / _CanvasScale; + } + + public Point ControlToCanvas(Point pt) + { + pt.X = (int)(((float)pt.X - _CanvasOffsetX) / _CanvasScale); + pt.Y = (int)(((float)pt.Y - _CanvasOffsetY) / _CanvasScale); + return pt; + } + + public PointF ControlToCanvas(PointF pt) + { + pt.X = (pt.X - _CanvasOffsetX) / _CanvasScale; + pt.Y = (pt.Y - _CanvasOffsetY) / _CanvasScale; + return pt; + } + + public Rectangle ControlToCanvas(Rectangle rect) + { + rect.X = (int)(((float)rect.X - _CanvasOffsetX) / _CanvasScale); + rect.Y = (int)(((float)rect.Y - _CanvasOffsetY) / _CanvasScale); + rect.Width = (int)((float)rect.Width / _CanvasScale); + rect.Height = (int)((float)rect.Height / _CanvasScale); + return rect; + } + + public RectangleF ControlToCanvas(RectangleF rect) + { + rect.X = (rect.X - _CanvasOffsetX) / _CanvasScale; + rect.Y = (rect.Y - _CanvasOffsetY) / _CanvasScale; + rect.Width /= _CanvasScale; + rect.Height /= _CanvasScale; + return rect; + } + + public void MoveCanvas(float x, float y, bool bAnimation, CanvasMoveArgs ma) + { + if (_LimitCanvasToContentBounds && _Nodes.Count == 0) + { + m_real_canvas_x = (m_real_canvas_y = 10f); + UpdateAnimationTimerState(); + return; + } + if (_LimitCanvasToContentBounds) + { + int num = (int)((float)(_CanvasValidBounds.Left + 50) * _CanvasScale); + int num2 = (int)((float)(_CanvasValidBounds.Top + 50) * _CanvasScale); + int num3 = (int)((float)(_CanvasValidBounds.Right - 50) * _CanvasScale); + int num4 = (int)((float)(_CanvasValidBounds.Bottom - 50) * _CanvasScale); + if ((float)num3 + x < 0f) + { + x = -num3; + } + if ((float)(ClientSize.Width - num) < x) + { + x = ClientSize.Width - num; + } + if ((float)num4 + y < 0f) + { + y = -num4; + } + if ((float)(ClientSize.Height - num2) < y) + { + y = ClientSize.Height - num2; + } + } + if (bAnimation) + { + bool moveAll = ma == CanvasMoveArgs.All; + if (moveAll || (ma & CanvasMoveArgs.Left) == CanvasMoveArgs.Left) + { + m_real_canvas_x = x; + } + if (moveAll || (ma & CanvasMoveArgs.Top) == CanvasMoveArgs.Top) + { + m_real_canvas_y = y; + } + } + else + { + m_real_canvas_x = (_CanvasOffsetX = x); + m_real_canvas_y = (_CanvasOffsetY = y); + Invalidate(); + } + UpdateAnimationTimerState(); + OnCanvasMoved(EventArgs.Empty); + } + + public void ScaleCanvas(float f, float x, float y) + { + if (_LimitCanvasToContentBounds && _Nodes.Count == 0) + { + _CanvasScale = 1f; + } + else if (_CanvasScale != f) + { + if ((double)f < 0.2) + { + f = 0.2f; + } + else if (f > 5f) + { + f = 5f; + } + float number = ControlToCanvas(x, isX: true); + float number2 = ControlToCanvas(y, isX: false); + _CanvasScale = f; + _CanvasOffsetX = (m_real_canvas_x -= CanvasToControl(number, isX: true) - x); + _CanvasOffsetY = (m_real_canvas_y -= CanvasToControl(number2, isX: false) - y); + OnCanvasScaled(EventArgs.Empty); + Invalidate(); + } + } + + public void FitCanvasToNodes(float maximumScale = 1f) + { + if (_Nodes.Count == 0 || ClientSize.Width <= 0 || ClientSize.Height <= 0 || _CanvasValidBounds.Width <= 0 || _CanvasValidBounds.Height <= 0) + { + return; + } + float scaleX = (float)ClientSize.Width / _CanvasValidBounds.Width; + float scaleY = (float)ClientSize.Height / _CanvasValidBounds.Height; + float scale = Math.Min(Math.Min(scaleX, scaleY), maximumScale); + float centerX = ClientSize.Width / 2f; + float centerY = ClientSize.Height / 2f; + ScaleCanvas(scale, centerX, centerY); + float contentCenterX = _CanvasValidBounds.Left + _CanvasValidBounds.Width / 2f; + float contentCenterY = _CanvasValidBounds.Top + _CanvasValidBounds.Height / 2f; + MoveCanvas(centerX - contentCenterX * _CanvasScale, centerY - contentCenterY * _CanvasScale, bAnimation: false, CanvasMoveArgs.All); + } + + public ConnectionInfo[] GetConnectionInfo() + { + return GetConnections(); + } + + public ConnectionInfo[] GetConnections() + { + List connections = new List(); + foreach (STNode node in _Nodes) + { + foreach (STNodeOption output in node.GetAllOutputOptions()) + { + foreach (STNodeOption input in output.ConnectedOption) + { + if (input != null && input.IsInput && input.Owner != null && input.Owner.Owner == this) + { + connections.Add(new ConnectionInfo { Output = output, Input = input }); + } + } + } + } + return connections + .OrderBy(connection => _Nodes.IndexOf(connection.Output.Owner)) + .ThenBy(connection => Array.IndexOf(connection.Output.Owner.GetAllOutputOptions(), connection.Output)) + .ThenBy(connection => _Nodes.IndexOf(connection.Input.Owner)) + .ThenBy(connection => Array.IndexOf(connection.Input.Owner.GetAllInputOptions(), connection.Input)) + .ToArray(); + } + + public static bool CanFindNodePath(STNode nodeStart, STNode nodeFind) + { + HashSet hs = new HashSet(); + return CanFindNodePath(nodeStart, nodeFind, hs); + } + + private static bool CanFindNodePath(STNode nodeStart, STNode nodeFind, HashSet hs) + { + foreach (STNodeOption outputOption in nodeStart.OutputOptions) + { + if (outputOption.ConnectedOption == null) + { + continue; + } + foreach (STNodeOption item in outputOption.ConnectedOption) + { + if (item.Owner == nodeFind) + { + return true; + } + if (hs.Add(item.Owner) && CanFindNodePath(item.Owner, nodeFind)) + { + return true; + } + } + } + return false; + } + + public Image GetCanvasImage(Rectangle rect) + { + return GetCanvasImage(rect, 1f); + } + + public Image GetCanvasImage(Rectangle rect, float fScale) + { + if ((double)fScale < 0.5) + { + fScale = 0.5f; + } + else if (fScale > 3f) + { + fScale = 3f; + } + int width = Math.Max(1, (int)Math.Ceiling(rect.Width * fScale)); + int height = Math.Max(1, (int)Math.Ceiling(rect.Height * fScale)); + Image image = new Bitmap(width, height); + using (Graphics graphics = Graphics.FromImage(image)) + { + graphics.Clear(BackColor); + graphics.ScaleTransform(fScale, fScale); + m_drawing_tools.Graphics = graphics; + if (_ShowGrid) + { + OnDrawGrid(m_drawing_tools, rect.Width, rect.Height); + } + graphics.TranslateTransform(-rect.X, -rect.Y); + OnDrawConnectedLine(m_drawing_tools); + OnDrawNode(m_drawing_tools, rect); + graphics.ResetTransform(); + if (_ShowLocation) + { + OnDrawNodeOutLocation(m_drawing_tools, image.Size, m_lst_node_out); + } + } + return image; + } + + public void SaveCanvas(string strFileName) + { + using FileStream s = new FileStream(strFileName, FileMode.Create, FileAccess.Write); + SaveCanvas(s); + } + + public void SaveCanvas(Stream s) + { + STNodeCanvasWriter.Write( + s, + _Nodes.Cast(), + GetConnections(), + _CanvasOffsetX, + _CanvasOffsetY, + _CanvasScale); + } + + public byte[] GetCanvasData() + { + using MemoryStream memoryStream = new MemoryStream(); + SaveCanvas(memoryStream); + return memoryStream.ToArray(); + } + + public int LoadAssembly(string[] strFiles) + { + int num = 0; + foreach (string strFile in strFiles) + { + try + { + if (LoadAssembly(strFile)) + { + num++; + } + } + catch + { + } + } + return num; + } + + public int LoadAssembly() + { + return STNodeTypeRegistry.LoadAssemblies(AppDomain.CurrentDomain.GetAssemblies()); + } + + public bool LoadAssembly(string strFile) + { + return STNodeTypeRegistry.LoadAssembly(strFile); + } + + public bool LoadAssembly(Assembly asm) + { + return STNodeTypeRegistry.LoadAssembly(asm); + } + + public bool LoadAssemblyFromBase64(string base64Assembly) + { + byte[] rawAssembly = Convert.FromBase64String(base64Assembly); + Assembly asm = Assembly.Load(rawAssembly); + return LoadAssembly(asm); + } + + public Type[] GetTypes() + { + return STNodeTypeRegistry.GetTypes(); + } + + public void LoadCanvas(string strFileName) + { + LoadCanvas(File.ReadAllBytes(strFileName)); + } + + public void LoadCanvas(byte[] byData) + { + using MemoryStream s = new MemoryStream(byData); + LoadCanvas(s); + } + + public void LoadCanvas(Stream s) + { + using (SuspendHistoryRecording()) + { + LoadCanvasCore(s); + } + } + + private void LoadCanvasCore(Stream s) + { + STNodeCanvasReader.Document document = STNodeCanvasReader.Read(s); + document.ConnectDetachedNodes(); + + // Parsing and connection validation are complete before the live graph + // is changed, so truncated/corrupt input cannot append a partial graph. + // Loading intentionally preserves the upstream STND v1 append semantics. + foreach (STNode node in document.Nodes) + _Nodes.Add(node); + ScaleCanvas(document.CanvasScale, 0f, 0f); + MoveCanvas( + document.CanvasOffsetX, + document.CanvasOffsetY, + bAnimation: false, + CanvasMoveArgs.All); + BuildBounds(); + foreach (STNode node in _Nodes) + { + node.OnEditorLoadCompleted(); + } + } + + internal STNode GetNodeFromData(byte[] byData) + { + if (byData == null) + { + throw new ArgumentNullException(nameof(byData)); + } + int offset = 0; + string modelKey = ReadNodeByteLengthString(byData, ref offset, "节点类型"); + string typeKey = ReadNodeByteLengthString(byData, ref offset, "节点类型标识"); + Dictionary dictionary = new Dictionary(); + while (offset < byData.Length) + { + int keyLength = ReadNodeInt32(byData, ref offset, "属性名称长度"); + string propertyName = Encoding.UTF8.GetString(ReadNodeBytes(byData, ref offset, keyLength, "属性名称")); + int valueLength = ReadNodeInt32(byData, ref offset, "属性值长度"); + byte[] propertyValue = ReadNodeBytes(byData, ref offset, valueLength, "属性值"); + if (dictionary.ContainsKey(propertyName)) + { + throw new InvalidDataException($"节点数据包含重复属性:{propertyName}"); + } + dictionary.Add(propertyName, propertyValue); + } + Type type = null; + STNodeTypeRegistry.TryGetNodeType(typeKey, modelKey, out type); + if (type == null) + { + throw new TypeLoadException($"无法找到节点类型 {{{modelKey}}},请确认对应程序集已由编辑器加载"); + } + STNode sTNode = (STNode)Activator.CreateInstance(type); + sTNode.Create(); + sTNode.OnLoadNode(dictionary); + return sTNode; + } + + private static string ReadNodeByteLengthString(byte[] data, ref int offset, string valueName) + { + if (offset >= data.Length) + { + throw new InvalidDataException($"{valueName}缺失"); + } + int length = data[offset++]; + return Encoding.UTF8.GetString(ReadNodeBytes(data, ref offset, length, valueName)); + } + + private static int ReadNodeInt32(byte[] data, ref int offset, string valueName) + { + byte[] value = ReadNodeBytes(data, ref offset, sizeof(int), valueName); + return BitConverter.ToInt32(value, 0); + } + + private static byte[] ReadNodeBytes(byte[] data, ref int offset, int length, string valueName) + { + if (length < 0 || offset < 0 || offset > data.Length - length) + { + throw new InvalidDataException($"{valueName}长度无效:{length}"); + } + byte[] value = new byte[length]; + Buffer.BlockCopy(data, offset, value, 0, length); + offset += length; + return value; + } + + public void ShowAlert(string strText, Color foreColor, Color backColor) + { + ShowAlert(strText, foreColor, backColor, 1000, AlertLocation.RightBottom, bRedraw: true); + } + + public void ShowAlert(string strText, Color foreColor, Color backColor, AlertLocation al) + { + ShowAlert(strText, foreColor, backColor, 1000, al, bRedraw: true); + } + + public void ShowAlert(string strText, Color foreColor, Color backColor, int nTime, AlertLocation al, bool bRedraw) + { + m_str_alert = strText; + m_forecolor_alert = foreColor; + m_backcolor_alert = backColor; + m_time_alert = nTime; + m_dt_alert = DateTime.UtcNow; + m_alpha_alert = 255; + m_al = al; + if (bRedraw) + { + Invalidate(); + } + UpdateAnimationTimerState(); + } + + public STNode SetActiveNode(STNode node) + { + if (node != null && !_Nodes.Contains(node)) + { + return _ActiveNode; + } + STNode activeNode = _ActiveNode; + if (_ActiveNode != node) + { + if (node != null) + { + _Nodes.MoveToEnd(node); + node.IsActive = true; + node.SetSelected(bSelected: true, bRedraw: false); + node.OnGotFocus(EventArgs.Empty); + } + if (_ActiveNode != null) + { + _ActiveNode.IsActive = false; + _ActiveNode.OnLostFocus(EventArgs.Empty); + } + _ActiveNode = node; + Invalidate(); + OnActiveChanged(EventArgs.Empty); + } + return activeNode; + } + + public bool AddSelectedNode(STNode node) + { + if (!_Nodes.Contains(node)) + { + return false; + } + bool flag = !node.IsSelected; + node.IsSelected = true; + bool changed; + lock (m_hs_node_selected) + { + changed = m_hs_node_selected.Add(node) || flag; + } + UpdateCanvasDragModeFromSelection(); + return changed; + } + + public bool RemoveSelectedNode(STNode node) + { + if (!_Nodes.Contains(node)) + { + return false; + } + bool isSelected = node.IsSelected; + node.IsSelected = false; + bool changed; + lock (m_hs_node_selected) + { + changed = m_hs_node_selected.Remove(node) || isSelected; + } + UpdateCanvasDragModeFromSelection(); + return changed; + } + + public Color SetTypeColor(Type t, Color clr) + { + return SetTypeColor(t, clr, bReplace: false); + } + + public Color SetTypeColor(Type t, Color clr, bool bReplace) + { + if (_TypeColor.ContainsKey(t)) + { + if (bReplace) + { + _TypeColor[t] = clr; + } + } + else + { + _TypeColor.Add(t, clr); + } + return _TypeColor[t]; + } + + public void Dispose() + { + if (m_disposed) + { + return; + } + m_disposed = true; + m_is_loaded = false; + DisposeEditing(); + m_animation_timer.Stop(); + m_animation_timer.Tick -= AnimationTimer_Tick; + ReleaseMouseCapture(); + foreach (GraphicsPath path in m_dic_gp_info.Keys) + { + path.Dispose(); + } + m_dic_gp_info.Clear(); + m_gp_hover = null; + m_p_line?.Dispose(); + m_p_line_hover?.Dispose(); + m_sf?.Dispose(); + m_img_border?.Dispose(); + m_img_border_hover?.Dispose(); + m_img_border_selected?.Dispose(); + m_img_border_active?.Dispose(); + m_drawing_tools.Pen?.Dispose(); + m_drawing_tools.SolidBrush?.Dispose(); + m_render_graphics?.Dispose(); + m_render_bitmap?.Dispose(); + m_measurement_bitmap.Dispose(); + _Font?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorEventArgs.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorEventArgs.cs new file mode 100644 index 0000000..72abe92 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeEditorEventArgs : EventArgs +{ + private STNode _Node; + + public STNode Node => _Node; + + public STNodeEditorEventArgs(STNode node) + { + _Node = node; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorEventHandler.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorEventHandler.cs new file mode 100644 index 0000000..eef626d --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorEventHandler.cs @@ -0,0 +1,3 @@ +namespace ST.Library.UI.NodeEditor; + +public delegate void STNodeEditorEventHandler(object sender, STNodeEditorEventArgs e); diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorOptionEventArgs.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorOptionEventArgs.cs new file mode 100644 index 0000000..dc54b5e --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorOptionEventArgs.cs @@ -0,0 +1,28 @@ +namespace ST.Library.UI.NodeEditor; + +public class STNodeEditorOptionEventArgs : STNodeOptionEventArgs +{ + private STNodeOption _CurrentOption; + + private bool _Continue = true; + + public STNodeOption CurrentOption => _CurrentOption; + + public bool Continue + { + get + { + return _Continue; + } + set + { + _Continue = value; + } + } + + public STNodeEditorOptionEventArgs(STNodeOption opTarget, STNodeOption opCurrent, ConnectionStatus cr) + : base(isSponsor: false, opTarget, cr) + { + _CurrentOption = opCurrent; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorOptionEventHandler.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorOptionEventHandler.cs new file mode 100644 index 0000000..261d34b --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorOptionEventHandler.cs @@ -0,0 +1,3 @@ +namespace ST.Library.UI.NodeEditor; + +public delegate void STNodeEditorOptionEventHandler(object sender, STNodeEditorOptionEventArgs e); diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorPannel.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorPannel.cs new file mode 100644 index 0000000..3837563 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeEditorPannel.cs @@ -0,0 +1,404 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using DrawingColor = System.Drawing.Color; +using DrawingSize = System.Drawing.Size; +using MediaColor = System.Windows.Media.Color; + +namespace ST.Library.UI.NodeEditor; + +/// +/// WPF composite editor containing the node catalog, canvas and property editor. +/// The historical type name is retained for source compatibility. +/// +public class STNodeEditorPannel : UserControl, IDisposable +{ + private readonly Grid m_root = new Grid(); + private readonly STNodeEditor m_editor = new STNodeEditor(); + private readonly STNodeTreeView m_tree = new STNodeTreeView(); + private readonly STNodePropertyGrid m_grid = new STNodePropertyGrid(); + private readonly Dictionary m_status_text = new Dictionary(); + + private bool m_left_layout = true; + private DrawingColor m_split_line_color = DrawingColor.Black; + private DrawingColor m_handle_line_color = DrawingColor.Gray; + private DrawingColor m_back_color = DrawingColor.FromArgb(255, 34, 34, 34); + private bool m_show_scale = true; + private bool m_show_connection_status = true; + private int m_x = 201; + private int m_y = 250; + private bool m_disposed; + private GridSplitter m_vertical_splitter; + private GridSplitter m_horizontal_splitter; + private Grid m_side_grid; + + [DefaultValue(true)] + public bool LeftLayout + { + get => m_left_layout; + set + { + if (m_left_layout == value) + { + return; + } + m_left_layout = value; + double width = GetViewportWidth(); + m_x = value ? 201 : Math.Max(122, (int)width - 202); + BuildLayout(); + } + } + + [DefaultValue(typeof(DrawingColor), "Black")] + public DrawingColor SplitLineColor + { + get => m_split_line_color; + set + { + m_split_line_color = value; + ApplySplitterColors(); + } + } + + [DefaultValue(typeof(DrawingColor), "Gray")] + public DrawingColor HandleLineColor + { + get => m_handle_line_color; + set + { + m_handle_line_color = value; + ApplySplitterColors(); + } + } + + [DefaultValue(true)] + public bool ShowScale + { + get => m_show_scale; + set => m_show_scale = value; + } + + [DefaultValue(true)] + public bool ShowConnectionStatus + { + get => m_show_connection_status; + set => m_show_connection_status = value; + } + + [DefaultValue(201)] + public int X + { + get => m_x; + set + { + m_x = Clamp(value, 122, Math.Max(122, (int)GetViewportWidth() - 122)); + BuildLayout(); + } + } + + public int Y + { + get => m_y; + set + { + m_y = Clamp(value, 122, Math.Max(122, (int)GetViewportHeight() - 122)); + BuildLayout(); + } + } + + [Browsable(false)] + public STNodeEditor Editor => m_editor; + + [Browsable(false)] + public STNodeTreeView TreeView => m_tree; + + [Browsable(false)] + public STNodePropertyGrid PropertyGrid => m_grid; + + public DrawingColor BackColor + { + get => m_back_color; + set + { + m_back_color = value; + Background = ToBrush(value); + } + } + + public DrawingSize MinimumSize + { + get => new DrawingSize((int)MinWidth, (int)MinHeight); + set + { + MinWidth = Math.Max(250, value.Width); + MinHeight = Math.Max(250, value.Height); + } + } + + public STNodeEditorPannel() + { + Width = 500; + Height = 500; + MinWidth = 250; + MinHeight = 250; + Content = m_root; + Background = ToBrush(m_back_color); + m_grid.Text = "NodeProperty"; + + foreach (ConnectionStatus status in Enum.GetValues(typeof(ConnectionStatus))) + { + FieldInfo field = typeof(ConnectionStatus).GetField(status.ToString()); + string text = field?.GetCustomAttributes(typeof(DescriptionAttribute), inherit: true) + .OfType() + .FirstOrDefault()?.Description ?? status.ToString(); + m_status_text[status] = text; + } + + m_editor.ActiveChanged += OnEditorActiveChanged; + m_editor.CanvasScaled += OnEditorCanvasScaled; + m_editor.OptionConnected += OnEditorOptionConnected; + SizeChanged += OnPanelSizeChanged; + BuildLayout(); + } + + public bool AddSTNode(Type nodeType) + { + return m_tree.AddNode(nodeType); + } + + public int LoadAssembly() + { + m_editor.LoadAssembly(); + return m_tree.LoadAssembly(); + } + + public int LoadAssembly(string fileName) + { + m_editor.LoadAssembly(fileName); + return m_tree.LoadAssembly(fileName); + } + + public string SetConnectionStatusText(ConnectionStatus status, string text) + { + if (m_status_text.TryGetValue(status, out string previous)) + { + m_status_text[status] = text; + return previous; + } + m_status_text.Add(status, text); + return text; + } + + private void BuildLayout() + { + if (m_root == null) + { + return; + } + + // Removing the side grid from the root does not detach its children from + // their logical parent. Detach the reusable controls before rebuilding it. + if (m_side_grid != null) + { + m_side_grid.Children.Remove(m_tree); + m_side_grid.Children.Remove(m_grid); + } + + m_root.Children.Clear(); + m_root.ColumnDefinitions.Clear(); + m_root.RowDefinitions.Clear(); + + double width = GetViewportWidth(); + double sideWidth = m_left_layout + ? Clamp(m_x, 122, Math.Max(122, (int)width - 122)) + : Clamp((int)width - m_x, 122, Math.Max(122, (int)width - 122)); + + m_root.ColumnDefinitions.Add(new ColumnDefinition + { + Width = m_left_layout ? new GridLength(sideWidth) : new GridLength(1, GridUnitType.Star) + }); + m_root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(5) }); + m_root.ColumnDefinitions.Add(new ColumnDefinition + { + Width = m_left_layout ? new GridLength(1, GridUnitType.Star) : new GridLength(sideWidth) + }); + + m_side_grid = new Grid(); + double height = GetViewportHeight(); + double topHeight = Clamp(m_y, 122, Math.Max(122, (int)height - 122)); + m_side_grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(topHeight) }); + m_side_grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(5) }); + m_side_grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + m_side_grid.Children.Add(m_tree); + Grid.SetRow(m_grid, 2); + m_side_grid.Children.Add(m_grid); + m_horizontal_splitter = new GridSplitter + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + ResizeDirection = GridResizeDirection.Rows, + ResizeBehavior = GridResizeBehavior.PreviousAndNext, + Cursor = System.Windows.Input.Cursors.SizeNS + }; + m_horizontal_splitter.DragCompleted += OnHorizontalSplitterDragCompleted; + Grid.SetRow(m_horizontal_splitter, 1); + m_side_grid.Children.Add(m_horizontal_splitter); + + int sideColumn = m_left_layout ? 0 : 2; + Grid.SetColumn(m_side_grid, sideColumn); + m_root.Children.Add(m_side_grid); + + int editorColumn = m_left_layout ? 2 : 0; + Grid.SetColumn(m_editor, editorColumn); + m_root.Children.Add(m_editor); + + m_vertical_splitter = new GridSplitter + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + ResizeDirection = GridResizeDirection.Columns, + ResizeBehavior = GridResizeBehavior.PreviousAndNext, + Cursor = System.Windows.Input.Cursors.SizeWE + }; + m_vertical_splitter.DragCompleted += OnVerticalSplitterDragCompleted; + Grid.SetColumn(m_vertical_splitter, 1); + m_root.Children.Add(m_vertical_splitter); + ApplySplitterColors(); + } + + private void OnPanelSizeChanged(object sender, SizeChangedEventArgs e) + { + double width = GetViewportWidth(); + m_x = Clamp(m_x, 122, Math.Max(122, (int)width - 122)); + m_y = Clamp(m_y, 122, Math.Max(122, (int)GetViewportHeight() - 122)); + } + + private void OnVerticalSplitterDragCompleted(object sender, DragCompletedEventArgs e) + { + if (m_root.ColumnDefinitions.Count < 3) + { + return; + } + m_x = m_left_layout + ? (int)Math.Round(m_root.ColumnDefinitions[0].ActualWidth) + : (int)Math.Round(GetViewportWidth() - m_root.ColumnDefinitions[2].ActualWidth); + } + + private void OnHorizontalSplitterDragCompleted(object sender, DragCompletedEventArgs e) + { + if (m_side_grid?.RowDefinitions.Count >= 3) + { + m_y = (int)Math.Round(m_side_grid.RowDefinitions[0].ActualHeight); + } + } + + private void OnEditorActiveChanged(object sender, EventArgs e) + { + m_grid.SetNode(m_editor.ActiveNode); + } + + private void OnEditorCanvasScaled(object sender, EventArgs e) + { + if (m_show_scale) + { + m_editor.ShowAlert( + m_editor.CanvasScale.ToString("F2"), + DrawingColor.White, + DrawingColor.FromArgb(127, 255, 255, 0)); + } + } + + private void OnEditorOptionConnected(object sender, STNodeEditorOptionEventArgs e) + { + if (!m_show_connection_status) + { + return; + } + string text = m_status_text.TryGetValue(e.Status, out string value) ? value : e.Status.ToString(); + m_editor.ShowAlert( + text, + DrawingColor.White, + e.Status == ConnectionStatus.Connected + ? DrawingColor.FromArgb(125, DrawingColor.Lime) + : DrawingColor.FromArgb(125, DrawingColor.Red)); + } + + private void ApplySplitterColors() + { + if (m_vertical_splitter != null) + { + m_vertical_splitter.Background = ToBrush(m_split_line_color); + m_vertical_splitter.BorderBrush = ToBrush(m_handle_line_color); + } + if (m_horizontal_splitter != null) + { + m_horizontal_splitter.Background = ToBrush(m_split_line_color); + m_horizontal_splitter.BorderBrush = ToBrush(m_handle_line_color); + } + } + + private double GetViewportWidth() + { + if (ActualWidth > 0) + { + return ActualWidth; + } + return double.IsNaN(Width) || Width <= 0 ? 500 : Width; + } + + private double GetViewportHeight() + { + if (ActualHeight > 0) + { + return ActualHeight; + } + return double.IsNaN(Height) || Height <= 0 ? 500 : Height; + } + + private static int Clamp(int value, int minimum, int maximum) + { + if (maximum < minimum) + { + maximum = minimum; + } + return Math.Max(minimum, Math.Min(value, maximum)); + } + + private static System.Windows.Media.SolidColorBrush ToBrush(DrawingColor color) + { + var brush = new System.Windows.Media.SolidColorBrush(MediaColor.FromArgb(color.A, color.R, color.G, color.B)); + brush.Freeze(); + return brush; + } + + public void Dispose() + { + if (m_disposed) + { + return; + } + m_disposed = true; + SizeChanged -= OnPanelSizeChanged; + m_editor.ActiveChanged -= OnEditorActiveChanged; + m_editor.CanvasScaled -= OnEditorCanvasScaled; + m_editor.OptionConnected -= OnEditorOptionConnected; + if (m_vertical_splitter != null) + { + m_vertical_splitter.DragCompleted -= OnVerticalSplitterDragCompleted; + } + if (m_horizontal_splitter != null) + { + m_horizontal_splitter.DragCompleted -= OnHorizontalSplitterDragCompleted; + } + m_editor.Dispose(); + m_tree.Dispose(); + m_grid.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeHub.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeHub.cs new file mode 100644 index 0000000..6990b37 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeHub.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeHub : STNode +{ + private bool m_bSingle; + + private string m_strIn; + + private string m_strOut; + + public STNodeHub() + : this(bSingle: false, "IN", "OUT", "HUB") + { + } + + public STNodeHub(bool bSingle) + : this(bSingle, "IN", "OUT", "HUB") + { + } + + public STNodeHub(bool bSingle, string title) + : this(bSingle, "IN", "OUT", title) + { + } + + public STNodeHub(bool bSingle, string strTextIn, string strTextOut) + : this(bSingle, strTextIn, strTextOut, "HUB") + { + } + + public STNodeHub(bool bSingle, string strTextIn, string strTextOut, string title) + { + m_bSingle = bSingle; + m_strIn = strTextIn; + m_strOut = strTextOut; + Addhub(); + base.Title = Lang.Get(title); + base.AutoSize = false; + base.TitleColor = Color.FromArgb(200, Color.DarkOrange); + } + + protected override void OnOwnerChanged() + { + base.OnOwnerChanged(); + if (base.Owner == null) + { + return; + } + base.Width = base.GetDefaultNodeSize().Width; + } + + private void Addhub() + { + STNodeHubOption sTNodeHubOption = new STNodeHubOption(m_strIn, typeof(object), m_bSingle); + STNodeHubOption sTNodeHubOption2 = new STNodeHubOption(m_strOut, typeof(object), bSingle: false); + base.InputOptions.Add(sTNodeHubOption); + base.OutputOptions.Add(sTNodeHubOption2); + sTNodeHubOption.Connected += input_Connected; + sTNodeHubOption.DataTransfer += input_DataTransfer; + sTNodeHubOption.DisConnected += input_DisConnected; + sTNodeHubOption2.Connected += output_Connected; + sTNodeHubOption2.DisConnected += output_DisConnected; + base.Height = base.TitleHeight + base.InputOptions.Count * 20; + } + + protected virtual void output_DisConnected(object sender, STNodeOptionEventArgs e) + { + STNodeOption sTNodeOption = sender as STNodeOption; + if (sTNodeOption.ConnectionCount != 0) + { + return; + } + int index = base.OutputOptions.IndexOf(sTNodeOption); + if (base.InputOptions[index].ConnectionCount == 0) + { + base.InputOptions.RemoveAt(index); + base.OutputOptions.RemoveAt(index); + if (base.Owner != null) + { + base.Owner.BuildLinePath(); + } + base.Height -= 20; + } + } + + protected virtual void output_Connected(object sender, STNodeOptionEventArgs e) + { + STNodeOption sTNodeOption = sender as STNodeOption; + int index = base.OutputOptions.IndexOf(sTNodeOption); + Type typeFromHandle = typeof(object); + if (!(base.InputOptions[index].DataType == typeFromHandle)) + { + return; + } + sTNodeOption.DataType = e.TargetOption.DataType; + base.InputOptions[index].DataType = sTNodeOption.DataType; + foreach (STNodeOption inputOption in base.InputOptions) + { + if (inputOption.DataType == typeFromHandle) + { + return; + } + } + Addhub(); + } + + protected virtual void input_DisConnected(object sender, STNodeOptionEventArgs e) + { + STNodeOption sTNodeOption = sender as STNodeOption; + if (sTNodeOption.ConnectionCount != 0) + { + return; + } + int index = base.InputOptions.IndexOf(sTNodeOption); + if (base.OutputOptions[index].ConnectionCount == 0) + { + base.InputOptions.RemoveAt(index); + base.OutputOptions.RemoveAt(index); + if (base.Owner != null) + { + base.Owner.BuildLinePath(); + } + base.Height -= 20; + } + } + + protected virtual void input_DataTransfer(object sender, STNodeOptionEventArgs e) + { + STNodeOption option = sender as STNodeOption; + int index = base.InputOptions.IndexOf(option); + if (e.Status != ConnectionStatus.Connected) + { + base.OutputOptions[index].Data = null; + } + else + { + base.OutputOptions[index].Data = e.TargetOption.Data; + } + base.OutputOptions[index].TransferData(); + } + + protected virtual void input_Connected(object sender, STNodeOptionEventArgs e) + { + STNodeOption sTNodeOption = sender as STNodeOption; + int index = base.InputOptions.IndexOf(sTNodeOption); + Type typeFromHandle = typeof(object); + if (sTNodeOption.DataType == typeFromHandle) + { + sTNodeOption.DataType = e.TargetOption.DataType; + base.OutputOptions[index].DataType = sTNodeOption.DataType; + foreach (STNodeOption inputOption in base.InputOptions) + { + if (inputOption.DataType == typeFromHandle) + { + return; + } + } + Addhub(); + } + else + { + base.OutputOptions[index].TransferData(e.TargetOption.Data); + } + } + + protected override void OnSaveNode(Dictionary dic) + { + dic.Add("count", BitConverter.GetBytes(base.InputOptionsCount)); + } + + protected internal override void OnLoadNode(Dictionary dic) + { + base.OnLoadNode(dic); + int num = BitConverter.ToInt32(dic["count"], 0); + while (base.InputOptionsCount < num && base.InputOptionsCount != num) + { + Addhub(); + } + } + + public class STNodeHubOption : global::ST.Library.UI.NodeEditor.STNodeHubOption + { + public STNodeHubOption(string strText, Type dataType, bool bSingle) + : base(strText, dataType, bSingle) + { + } + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeHubOption.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeHubOption.cs new file mode 100644 index 0000000..32ae3fa --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeHubOption.cs @@ -0,0 +1,106 @@ +using System; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeHubOption : STNodeOption +{ + public STNodeHubOption(string strText, Type dataType, bool bSingle) + : base(strText, dataType, bSingle) + { + } + + public override ConnectionStatus ConnectOption(STNodeOption op) + { + Type typeFromHandle = typeof(object); + if (base.DataType != typeFromHandle) + { + return base.ConnectOption(op); + } + base.DataType = op.DataType; + ConnectionStatus connectionStatus = base.ConnectOption(op); + if (connectionStatus != ConnectionStatus.Connected) + { + base.DataType = typeFromHandle; + } + return connectionStatus; + } + + public override ConnectionStatus ConnectOption(STNodeOption op, bool isOwnerOfOwner) + { + if (isOwnerOfOwner) + { + return ConnectOption(op); + } + Type typeFromHandle = typeof(object); + if (base.DataType != typeFromHandle) + { + return base.ConnectOption(op, isOwnerOfOwner: false); + } + base.DataType = op.DataType; + ConnectionStatus connectionStatus = base.ConnectOption(op, isOwnerOfOwner: false); + if (connectionStatus != ConnectionStatus.Connected) + { + base.DataType = typeFromHandle; + } + return connectionStatus; + } + + public override ConnectionStatus CanConnect(STNodeOption op) + { + if (op == STNodeOption.Empty) + { + return ConnectionStatus.EmptyOption; + } + if (base.DataType != typeof(object)) + { + return base.CanConnect(op); + } + if (base.IsInput == op.IsInput) + { + return ConnectionStatus.SameInputOrOutput; + } + if (op.Owner == null || base.Owner == null) + { + return ConnectionStatus.NoOwner; + } + if (op.Owner == base.Owner) + { + return ConnectionStatus.SameOwner; + } + if (base.Owner.LockOption || op.Owner.LockOption) + { + return ConnectionStatus.Locked; + } + if (base.IsSingle && m_hs_connected.Count == 1) + { + return ConnectionStatus.SingleOption; + } + if (op.IsInput && STNodeEditor.CanFindNodePath(op.Owner, base.Owner)) + { + return ConnectionStatus.Loop; + } + if (m_hs_connected.Contains(op)) + { + return ConnectionStatus.Exists; + } + if (op.DataType == typeof(object)) + { + return ConnectionStatus.ErrorType; + } + if (!base.IsInput) + { + return ConnectionStatus.Connected; + } + foreach (STNodeOption inputOption in base.Owner.InputOptions) + { + foreach (STNodeOption item in inputOption.ConnectedOption) + { + if (item == op) + { + return ConnectionStatus.Exists; + } + } + } + return ConnectionStatus.Connected; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeInHub.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeInHub.cs new file mode 100644 index 0000000..077aa52 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeInHub.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeInHub : STNode +{ + private bool m_bSingle; + + private string m_strIn; + + public STNodeInHub() + : this(bSingle: false) + { + } + + public STNodeInHub(string title) + : this(bSingle: false, title) + { + } + + public STNodeInHub(bool bSingle) + : this(bSingle, "InHUB") + { + } + + public STNodeInHub(bool bSingle, string title) + : this(bSingle, "NodeIN", title) + { + } + + public STNodeInHub(bool bSingle, string strTextIn, string title) + { + m_bSingle = bSingle; + m_strIn = strTextIn; + Addhub(); + base.Title = Lang.Get(title); + base.AutoSize = true; + base.TitleColor = Color.FromArgb(200, Color.DarkOrange); + } + + protected override void OnOwnerChanged() + { + base.OnOwnerChanged(); + if (base.Owner == null) + { + return; + } + base.Width = base.GetDefaultNodeSize().Width; + } + + private void Addhub() + { + STNodeHubOption sTNodeHubOption = new STNodeHubOption(m_strIn, typeof(object), m_bSingle); + base.InputOptions.Add(sTNodeHubOption); + sTNodeHubOption.Connected += input_Connected; + sTNodeHubOption.DataTransfer += input_DataTransfer; + sTNodeHubOption.DisConnected += input_DisConnected; + base.Height = base.TitleHeight + base.InputOptions.Count * 20; + } + + protected virtual void DoInputDisConnected(STNodeOption sender, STNodeOptionEventArgs e) + { + } + + private void input_DisConnected(object sender, STNodeOptionEventArgs e) + { + STNodeOption sTNodeOption = sender as STNodeOption; + if (sTNodeOption.ConnectionCount != 0) + { + DoInputDisConnected(sTNodeOption, e); + return; + } + int index = base.InputOptions.IndexOf(sTNodeOption); + base.InputOptions.RemoveAt(index); + if (base.Owner != null) + { + base.Owner.BuildLinePath(); + } + base.Height -= 20; + DoInputDisConnected(sTNodeOption, e); + } + + protected virtual void DoInputDataTransfer(STNodeOption sender, STNodeOptionEventArgs e) + { + } + + private void input_DataTransfer(object sender, STNodeOptionEventArgs e) + { + DoInputDataTransfer(sender as STNodeOption, e); + } + + protected virtual void DoInputConnected(STNodeOption sender, STNodeOptionEventArgs e) + { + } + + private void input_Connected(object sender, STNodeOptionEventArgs e) + { + STNodeOption sTNodeOption = sender as STNodeOption; + Type typeFromHandle = typeof(object); + if (sTNodeOption.DataType == typeFromHandle) + { + sTNodeOption.DataType = e.TargetOption.DataType; + foreach (STNodeOption inputOption in base.InputOptions) + { + if (inputOption.DataType == typeFromHandle) + { + DoInputConnected(sTNodeOption, e); + return; + } + } + Addhub(); + } + DoInputConnected(sTNodeOption, e); + } + + protected override void OnSaveNode(Dictionary dic) + { + dic.Add("count", BitConverter.GetBytes(base.InputOptionsCount)); + } + + protected internal override void OnLoadNode(Dictionary dic) + { + base.OnLoadNode(dic); + int num = BitConverter.ToInt32(dic["count"], 0); + while (base.InputOptionsCount < num && base.InputOptionsCount != num) + { + Addhub(); + } + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeInputEventArgs.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeInputEventArgs.cs new file mode 100644 index 0000000..3e56aa1 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeInputEventArgs.cs @@ -0,0 +1,60 @@ +using System; +using System.Drawing; + +namespace ST.Library.UI.NodeEditor; + +[Flags] +public enum STMouseButtons +{ + None = 0, + Left = 1, + Right = 2, + Middle = 4, + XButton1 = 8, + XButton2 = 16 +} + +public sealed class STNodeMouseEventArgs : EventArgs +{ + public STMouseButtons Button { get; } + + public int Clicks { get; } + + public int X { get; } + + public int Y { get; } + + public int Delta { get; } + + public Point Location => new Point(X, Y); + + public STNodeMouseEventArgs(STMouseButtons button, int clicks, int x, int y, int delta) + { + Button = button; + Clicks = clicks; + X = x; + Y = y; + Delta = delta; + } + + public STNodeMouseEventArgs WithLocation(int x, int y) + { + return new STNodeMouseEventArgs(Button, Clicks, x, y, Delta); + } +} + +public delegate void STNodeMouseEventHandler(object sender, STNodeMouseEventArgs e); + +public sealed class STNodeKeyPressEventArgs : EventArgs +{ + public char KeyChar { get; } + + public bool Handled { get; set; } + + public STNodeKeyPressEventArgs(char keyChar) + { + KeyChar = keyChar; + } +} + +public delegate void STNodeKeyPressEventHandler(object sender, STNodeKeyPressEventArgs e); diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOption.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOption.cs new file mode 100644 index 0000000..db4a48a --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOption.cs @@ -0,0 +1,596 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeOption +{ + public static readonly STNodeOption Empty = new STNodeOption(); + + private STNode _Owner; + + private bool _IsSingle; + + private bool _IsInput; + + private Color _TextColor = Color.White; + + private Color _DotColor = Color.Transparent; + + private string _Text; + + private int _DotLeft; + + private int _DotTop; + + private int _DotSize; + + private Rectangle _TextRectangle; + + private object _Data; + + private Type _DataType; + + protected HashSet m_hs_connected; + + public STNode Owner + { + get + { + return _Owner; + } + internal set + { + if (value != _Owner) + { + if (_Owner != null) + { + DisConnectionAll(); + } + _Owner = value; + } + } + } + + public bool IsSingle => _IsSingle; + + public bool IsInput + { + get + { + return _IsInput; + } + internal set + { + _IsInput = value; + } + } + + public Color TextColor + { + get + { + return _TextColor; + } + internal set + { + if (!(value == _TextColor)) + { + _TextColor = value; + Invalidate(); + } + } + } + + public Color DotColor + { + get + { + return _DotColor; + } + internal set + { + if (!(value == _DotColor)) + { + _DotColor = value; + Invalidate(); + } + } + } + + public string Text + { + get + { + return _Text; + } + internal set + { + if (!(value == _Text)) + { + _Text = value; + if (_Owner != null) + { + _Owner.BuildSize(bBuildNode: true, bBuildMark: true, bRedraw: true); + } + } + } + } + + public int DotLeft + { + get + { + return _DotLeft; + } + internal set + { + _DotLeft = value; + } + } + + public int DotTop + { + get + { + return _DotTop; + } + internal set + { + _DotTop = value; + } + } + + public int DotSize + { + get + { + return _DotSize; + } + protected set + { + _DotSize = value; + } + } + + public Rectangle TextRectangle + { + get + { + return _TextRectangle; + } + internal set + { + _TextRectangle = value; + } + } + + public object Data + { + get + { + return _Data; + } + set + { + if (value != null) + { + if (_DataType == null) + { + return; + } + Type type = value.GetType(); + if (type != _DataType && !type.IsSubclassOf(_DataType)) + { + throw new ArgumentException("无效数据类型 数据类型必须为指定的数据类型或其子类"); + } + } + _Data = value; + } + } + + public Type DataType + { + get + { + return _DataType; + } + internal set + { + _DataType = value; + } + } + + public Rectangle DotRectangle => new Rectangle(_DotLeft, _DotTop, _DotSize, _DotSize); + + public int ConnectionCount => m_hs_connected.Count; + + public HashSet ConnectedOption => m_hs_connected; + + public event STNodeOptionEventHandler Connected; + + public event STNodeOptionEventHandler Connecting; + + public event STNodeOptionEventHandler DisConnected; + + public event STNodeOptionEventHandler DisConnecting; + + public event STNodeOptionEventHandler DataTransfer; + + private STNodeOption() + { + m_hs_connected = new HashSet(); + } + + public STNodeOption(string strText, Type dataType, bool bSingle) + { + if (dataType == null) + { + throw new ArgumentNullException("指定的数据类型不能为空"); + } + _DotSize = 10; + m_hs_connected = new HashSet(); + _DataType = dataType; + _Text = strText; + _IsSingle = bSingle; + } + + protected void Invalidate() + { + if (_Owner != null) + { + _Owner.Invalidate(); + } + } + + protected internal virtual void OnConnected(STNodeOptionEventArgs e) + { + if (this.Connected != null) + { + this.Connected(this, e); + } + } + + protected internal virtual void OnConnecting(STNodeOptionEventArgs e) + { + if (this.Connecting != null) + { + this.Connecting(this, e); + } + } + + protected internal virtual void OnDisConnected(STNodeOptionEventArgs e) + { + if (this.DisConnected != null) + { + this.DisConnected(this, e); + } + } + + protected internal virtual void OnDisConnecting(STNodeOptionEventArgs e) + { + if (this.DisConnecting != null) + { + this.DisConnecting(this, e); + } + } + + protected internal virtual void OnDataTransfer(STNodeOptionEventArgs e) + { + if (this.DataTransfer != null) + { + this.DataTransfer(this, e); + } + } + + protected void STNodeEidtorConnected(STNodeEditorOptionEventArgs e) + { + if (_Owner != null && _Owner.Owner != null) + { + _Owner.Owner.OnOptionConnected(e); + } + } + + protected void STNodeEidtorDisConnected(STNodeEditorOptionEventArgs e) + { + if (_Owner != null && _Owner.Owner != null) + { + _Owner.Owner.OnOptionDisConnected(e); + } + } + + protected virtual bool ConnectingOption(STNodeOption op) + { + return ConnectingOptionCore(op, isOwnerOfOwner: true); + } + + protected virtual bool ConnectingOption(STNodeOption op, bool isOwnerOfOwner) + { + return isOwnerOfOwner + ? ConnectingOption(op) + : ConnectingOptionCore(op, isOwnerOfOwner: false); + } + + private bool ConnectingOptionCore(STNodeOption op, bool isOwnerOfOwner) + { + if (_Owner == null) + { + return false; + } + if (isOwnerOfOwner && _Owner.Owner == null) + { + return false; + } + STNodeEditorOptionEventArgs e = new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Connecting); + if (isOwnerOfOwner) + { + _Owner.Owner.OnOptionConnecting(e); + } + OnConnecting(new STNodeOptionEventArgs(isSponsor: true, op, ConnectionStatus.Connecting)); + op.OnConnecting(new STNodeOptionEventArgs(isSponsor: false, this, ConnectionStatus.Connecting)); + return e.Continue; + } + + protected virtual bool DisConnectingOption(STNodeOption op) + { + if (_Owner == null) + { + return false; + } + if (_Owner.Owner == null) + { + return false; + } + STNodeEditorOptionEventArgs e = new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.DisConnecting); + _Owner.Owner.OnOptionDisConnecting(e); + OnDisConnecting(new STNodeOptionEventArgs(isSponsor: true, op, ConnectionStatus.DisConnecting)); + op.OnDisConnecting(new STNodeOptionEventArgs(isSponsor: false, this, ConnectionStatus.DisConnecting)); + return e.Continue; + } + + public virtual ConnectionStatus ConnectOption(STNodeOption op) + { + return ConnectOptionCore(op, ConnectingOption(op)); + } + + public virtual ConnectionStatus ConnectOption(STNodeOption op, bool isOwnerOfOwner) + { + return isOwnerOfOwner + ? ConnectOption(op) + : ConnectOptionCore(op, ConnectingOption(op, isOwnerOfOwner: false)); + } + + private ConnectionStatus ConnectOptionCore(STNodeOption op, bool continueConnecting) + { + if (!continueConnecting) + { + STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Reject)); + return ConnectionStatus.Reject; + } + ConnectionStatus connectionStatus = CanConnect(op); + if (connectionStatus != ConnectionStatus.Connected) + { + STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, connectionStatus)); + return connectionStatus; + } + connectionStatus = op.CanConnect(this); + if (connectionStatus != ConnectionStatus.Connected) + { + STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, connectionStatus)); + return connectionStatus; + } + op.AddConnection(this, bSponsor: false); + AddConnection(op, bSponsor: true); + ControlBuildLinePath(); + STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, connectionStatus)); + return connectionStatus; + } + + public virtual ConnectionStatus CanConnect(STNodeOption op) + { + if (this == Empty || op == Empty) + { + return ConnectionStatus.EmptyOption; + } + if (_IsInput == op.IsInput) + { + return ConnectionStatus.SameInputOrOutput; + } + if (op.Owner == null || _Owner == null) + { + return ConnectionStatus.NoOwner; + } + if (op.Owner == _Owner) + { + return ConnectionStatus.SameOwner; + } + if (_Owner.LockOption || op._Owner.LockOption) + { + return ConnectionStatus.Locked; + } + if (_IsSingle && m_hs_connected.Count == 1) + { + return ConnectionStatus.SingleOption; + } + if (op.IsInput && STNodeEditor.CanFindNodePath(op.Owner, _Owner)) + { + return ConnectionStatus.Loop; + } + if (m_hs_connected.Contains(op)) + { + return ConnectionStatus.Exists; + } + if (_IsInput && op._DataType != _DataType && !op._DataType.IsSubclassOf(_DataType)) + { + return ConnectionStatus.ErrorType; + } + return ConnectionStatus.Connected; + } + + public virtual ConnectionStatus DisConnectOption(STNodeOption op) + { + if (!DisConnectingOption(op)) + { + STNodeEidtorDisConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Reject)); + return ConnectionStatus.Reject; + } + if (op.Owner == null) + { + return ConnectionStatus.NoOwner; + } + if (_Owner == null) + { + return ConnectionStatus.NoOwner; + } + if (op.Owner.LockOption && _Owner.LockOption) + { + STNodeEidtorDisConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Locked)); + return ConnectionStatus.Locked; + } + op.RemoveConnection(this, bSponsor: false); + RemoveConnection(op, bSponsor: true); + ControlBuildLinePath(); + STNodeEidtorDisConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.DisConnected)); + return ConnectionStatus.DisConnected; + } + + public void DisConnectionAll() + { + if (!(_DataType == null)) + { + STNodeOption[] array = m_hs_connected.ToArray(); + STNodeOption[] array2 = array; + foreach (STNodeOption op in array2) + { + DisConnectOption(op); + } + } + } + + internal void DisconnectAllDetached() + { + if (_DataType == null) + { + return; + } + foreach (STNodeOption option in m_hs_connected.ToArray()) + { + option.RemoveConnection(this, bSponsor: false); + RemoveConnection(option, bSponsor: true); + } + ControlBuildLinePath(); + } + + public List GetConnectedOption() + { + if (_DataType == null) + { + return null; + } + if (!_IsInput) + { + return m_hs_connected.ToList(); + } + List list = new List(); + if (_Owner == null) + { + return null; + } + if (_Owner.Owner == null) + { + return m_hs_connected.ToList(); + } + ConnectionInfo[] connectionInfo = _Owner.Owner.GetConnectionInfo(); + for (int i = 0; i < connectionInfo.Length; i++) + { + ConnectionInfo connectionInfo2 = connectionInfo[i]; + if (connectionInfo2.Output == this) + { + list.Add(connectionInfo2.Input); + } + } + return list; + } + + public void TransferData() + { + if (_DataType == null) + { + return; + } + foreach (STNodeOption item in m_hs_connected) + { + item.OnDataTransfer(new STNodeOptionEventArgs(isSponsor: true, this, ConnectionStatus.Connected)); + } + } + + public void TransferData(object data) + { + if (_DataType == null) + { + return; + } + Data = data; + foreach (STNodeOption item in m_hs_connected) + { + item.OnDataTransfer(new STNodeOptionEventArgs(isSponsor: true, this, ConnectionStatus.Connected)); + } + } + + public void TransferData(object data, bool bDisposeOld) + { + if (bDisposeOld && _Data != null) + { + if (_Data is IDisposable) + { + ((IDisposable)_Data).Dispose(); + } + _Data = null; + } + TransferData(data); + } + + private bool AddConnection(STNodeOption op, bool bSponsor) + { + if (_DataType == null) + { + return false; + } + bool result = m_hs_connected.Add(op); + OnConnected(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.Connected)); + if (_IsInput) + { + OnDataTransfer(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.Connected)); + } + return result; + } + + private bool RemoveConnection(STNodeOption op, bool bSponsor) + { + if (_DataType == null) + { + return false; + } + bool result = false; + if (m_hs_connected.Contains(op)) + { + result = m_hs_connected.Remove(op); + if (_IsInput) + { + OnDataTransfer(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.DisConnected)); + } + OnDisConnected(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.Connected)); + } + return result; + } + + private void ControlBuildLinePath() + { + if (Owner != null && Owner.Owner != null) + { + Owner.Owner.BuildLinePath(); + } + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOptionCollection.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOptionCollection.cs new file mode 100644 index 0000000..8f00bd6 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOptionCollection.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeOptionCollection : IList, ICollection, IEnumerable +{ + private int _Count; + + private STNodeOption[] m_options; + + private STNode m_owner; + + private bool m_isInput; + + public int Count => _Count; + + public bool IsFixedSize => false; + + public bool IsReadOnly => false; + + public STNodeOption this[int index] + { + get + { + if (index < 0 || index >= _Count) + { + throw new IndexOutOfRangeException("索引越界"); + } + return m_options[index]; + } + set + { + throw new InvalidOperationException("禁止重新赋值元素"); + } + } + + public bool IsSynchronized => true; + + public object SyncRoot => this; + + bool IList.IsFixedSize => IsFixedSize; + + bool IList.IsReadOnly => IsReadOnly; + + object IList.this[int index] + { + get + { + return this[index]; + } + set + { + this[index] = (STNodeOption)value; + } + } + + int ICollection.Count => _Count; + + bool ICollection.IsSynchronized => IsSynchronized; + + object ICollection.SyncRoot => SyncRoot; + + internal STNodeOptionCollection(STNode owner, bool isInput) + { + if (owner == null) + { + throw new ArgumentNullException("所有者不能为空"); + } + m_owner = owner; + m_isInput = isInput; + m_options = new STNodeOption[4]; + } + + public STNodeOption Add(string strText, Type dataType, bool bSingle) + { + int num = Add(new STNodeOption(strText, dataType, bSingle)); + return m_options[num]; + } + + public int Add(STNodeOption option) + { + if (option == null) + { + throw new ArgumentNullException("添加对象不能为空"); + } + EnsureSpace(1); + int num = ((option == STNodeOption.Empty) ? (-1) : IndexOf(option)); + if (-1 == num) + { + num = _Count; + option.Owner = m_owner; + option.IsInput = m_isInput; + m_options[_Count++] = option; + Invalidate(); + } + return num; + } + + public void AddRange(STNodeOption[] options) + { + if (options == null) + { + throw new ArgumentNullException("添加对象不能为空"); + } + EnsureSpace(options.Length); + foreach (STNodeOption sTNodeOption in options) + { + if (sTNodeOption == null) + { + throw new ArgumentNullException("添加对象不能为空"); + } + if (-1 == IndexOf(sTNodeOption)) + { + sTNodeOption.Owner = m_owner; + sTNodeOption.IsInput = m_isInput; + m_options[_Count++] = sTNodeOption; + } + } + Invalidate(); + } + + public void Clear() + { + for (int i = 0; i < _Count; i++) + { + m_options[i].Owner = null; + } + _Count = 0; + m_options = new STNodeOption[4]; + Invalidate(); + } + + public bool Contains(STNodeOption option) + { + return IndexOf(option) != -1; + } + + public int IndexOf(STNodeOption option) + { + return Array.IndexOf(m_options, option, 0, _Count); + } + + public void Insert(int index, STNodeOption option) + { + if (index < 0 || index > _Count) + { + throw new IndexOutOfRangeException("索引越界"); + } + if (option == null) + { + throw new ArgumentNullException("插入对象不能为空"); + } + if (option != STNodeOption.Empty && IndexOf(option) >= 0) + { + return; + } + EnsureSpace(1); + for (int num = _Count; num > index; num--) + { + m_options[num] = m_options[num - 1]; + } + option.Owner = m_owner; + option.IsInput = m_isInput; + m_options[index] = option; + _Count++; + Invalidate(); + } + + public void Remove(STNodeOption option) + { + int num = IndexOf(option); + if (num != -1) + { + RemoveAt(num); + } + } + + public void RemoveAt(int index) + { + if (index < 0 || index >= _Count) + { + throw new IndexOutOfRangeException("索引越界"); + } + _Count--; + m_options[index].Owner = null; + int i = index; + for (int count = _Count; i < count; i++) + { + m_options[i] = m_options[i + 1]; + } + m_options[_Count] = null; + Invalidate(); + } + + public void CopyTo(Array array, int index) + { + if (array == null) + { + throw new ArgumentNullException("数组不能为空"); + } + m_options.CopyTo(array, index); + } + + public IEnumerator GetEnumerator() + { + int i = 0; + for (int Len = _Count; i < Len; i++) + { + yield return m_options[i]; + } + } + + private void EnsureSpace(int elements) + { + if (elements + _Count > m_options.Length) + { + STNodeOption[] array = new STNodeOption[Math.Max(m_options.Length * 2, elements + _Count)]; + m_options.CopyTo(array, 0); + m_options = array; + } + } + + protected void Invalidate() + { + if (m_owner != null && m_owner.Owner != null) + { + m_owner.BuildSize(bBuildNode: true, bBuildMark: true, bRedraw: true); + } + } + + int IList.Add(object value) + { + return Add((STNodeOption)value); + } + + void IList.Clear() + { + Clear(); + } + + bool IList.Contains(object value) + { + return Contains((STNodeOption)value); + } + + int IList.IndexOf(object value) + { + return IndexOf((STNodeOption)value); + } + + void IList.Insert(int index, object value) + { + Insert(index, (STNodeOption)value); + } + + void IList.Remove(object value) + { + Remove((STNodeOption)value); + } + + void IList.RemoveAt(int index) + { + RemoveAt(index); + } + + void ICollection.CopyTo(Array array, int index) + { + CopyTo(array, index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public STNodeOption[] ToArray() + { + STNodeOption[] array = new STNodeOption[_Count]; + for (int i = 0; i < array.Length; i++) + { + array[i] = m_options[i]; + } + return array; + } + + internal bool Reorder(IReadOnlyList orderedOptions) + { + if (orderedOptions == null) + { + throw new ArgumentNullException(nameof(orderedOptions)); + } + if (orderedOptions.Count != _Count) + { + return false; + } + + HashSet seen = new HashSet(); + for (int i = 0; i < orderedOptions.Count; i++) + { + STNodeOption option = orderedOptions[i]; + if (option == null || IndexOf(option) == -1 || !seen.Add(option)) + { + return false; + } + } + + bool changed = false; + for (int i = 0; i < _Count; i++) + { + if (!ReferenceEquals(m_options[i], orderedOptions[i])) + { + changed = true; + break; + } + } + if (!changed) + { + return false; + } + + for (int i = 0; i < _Count; i++) + { + m_options[i] = orderedOptions[i]; + } + Invalidate(); + return true; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOptionEventArgs.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOptionEventArgs.cs new file mode 100644 index 0000000..134d005 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOptionEventArgs.cs @@ -0,0 +1,35 @@ +using System; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeOptionEventArgs : EventArgs +{ + private STNodeOption _TargetOption; + + private ConnectionStatus _Status; + + private bool _IsSponsor; + + public STNodeOption TargetOption => _TargetOption; + + public ConnectionStatus Status + { + get + { + return _Status; + } + internal set + { + _Status = value; + } + } + + public bool IsSponsor => _IsSponsor; + + public STNodeOptionEventArgs(bool isSponsor, STNodeOption opTarget, ConnectionStatus cr) + { + _IsSponsor = isSponsor; + _TargetOption = opTarget; + _Status = cr; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOptionEventHandler.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOptionEventHandler.cs new file mode 100644 index 0000000..f4d6488 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOptionEventHandler.cs @@ -0,0 +1,3 @@ +namespace ST.Library.UI.NodeEditor; + +public delegate void STNodeOptionEventHandler(object sender, STNodeOptionEventArgs e); diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOutHub.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOutHub.cs new file mode 100644 index 0000000..cdbe25f --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeOutHub.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace ST.Library.UI.NodeEditor; + +public class STNodeOutHub : STNode +{ + private bool m_bSingle; + + private string m_strOut; + + public STNodeOutHub() + : this(bSingle: false) + { + } + + public STNodeOutHub(bool bSingle) + : this(bSingle, "HUB") + { + } + + public STNodeOutHub(bool bSingle, string title) + : this(bSingle, "OUT", title) + { + } + + public STNodeOutHub(string title) + : this(bSingle: false, title) + { + } + + public STNodeOutHub(bool bSingle, string strTextOut, string title) + { + m_bSingle = bSingle; + m_strOut = strTextOut; + Addhub(); + base.Title = Lang.Get(title); + base.AutoSize = false; + base.TitleColor = Color.FromArgb(200, Color.DarkOrange); + } + + protected override void OnOwnerChanged() + { + base.OnOwnerChanged(); + if (base.Owner == null) + { + return; + } + base.Width = base.GetDefaultNodeSize().Width; + } + + protected virtual void Addhub() + { + STNodeHubOption sTNodeHubOption = new STNodeHubOption(m_strOut, typeof(object), m_bSingle); + base.OutputOptions.Add(sTNodeHubOption); + sTNodeHubOption.Connected += output_Connected; + sTNodeHubOption.DisConnected += output_DisConnected; + base.Height = base.TitleHeight + base.OutputOptions.Count * 20; + } + + protected virtual void DoOutputDisConnected(STNodeOption sender, STNodeOptionEventArgs e) + { + } + + private void output_DisConnected(object sender, STNodeOptionEventArgs e) + { + STNodeOption sTNodeOption = sender as STNodeOption; + if (sTNodeOption.ConnectionCount != 0) + { + DoOutputDisConnected(sTNodeOption, e); + return; + } + int index = base.OutputOptions.IndexOf(sTNodeOption); + if (base.OutputOptions[index].ConnectionCount == 0) + { + base.OutputOptions.RemoveAt(index); + if (base.Owner != null) + { + base.Owner.BuildLinePath(); + } + base.Height -= 20; + DoOutputDisConnected(sTNodeOption, e); + } + } + + protected virtual void DoOutputConnected(STNodeOption sender, STNodeOptionEventArgs e) + { + } + + private void output_Connected(object sender, STNodeOptionEventArgs e) + { + STNodeOption sender2 = sender as STNodeOption; + Type typeFromHandle = typeof(object); + foreach (STNodeOption outputOption in base.OutputOptions) + { + if (outputOption.DataType == typeFromHandle) + { + DoOutputConnected(sender2, e); + return; + } + } + Addhub(); + DoOutputConnected(sender2, e); + } + + protected override void OnSaveNode(Dictionary dic) + { + dic.Add("count", BitConverter.GetBytes(base.OutputOptionsCount)); + } + + protected internal override void OnLoadNode(Dictionary dic) + { + base.OnLoadNode(dic); + int num = BitConverter.ToInt32(dic["count"], 0); + while (base.OutputOptionsCount < num && base.OutputOptionsCount != num) + { + Addhub(); + } + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyAttribute.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyAttribute.cs new file mode 100644 index 0000000..946b141 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyAttribute.cs @@ -0,0 +1,74 @@ +using System; + +namespace ST.Library.UI.NodeEditor; + +public class STNodePropertyAttribute : Attribute +{ + private string _Name; + + private string _Description; + + private Type _ConverterType = typeof(STNodePropertyDescriptor); + + private bool _IsEditEnable; + + private bool _IsReadOnly; + + private bool _IsHide; + + public string Name => _Name; + + public string Description => _Description; + + public Type DescriptorType + { + get + { + return _ConverterType; + } + set + { + _ConverterType = value; + } + } + + public bool IsEditEnable => _IsEditEnable; + + public bool IsReadOnly => _IsReadOnly; + + public bool IsHide + { + get + { + return _IsHide; + } + set + { + _IsHide = value; + } + } + + public STNodePropertyAttribute(string strKey, string strDesc) + : this(strKey, strDesc, isEditEnable: false) + { + } + + public STNodePropertyAttribute(string strKey, string strDesc, bool isEditEnable) + : this(strKey, strDesc, isEditEnable, isHide: false) + { + } + + public STNodePropertyAttribute(string strKey, string strDesc, bool isEditEnable, bool isHide) + : this(strKey, strDesc, isEditEnable, isHide, isReadOnly: false) + { + } + + public STNodePropertyAttribute(string strKey, string strDesc, bool isEditEnable, bool isHide, bool isReadOnly) + { + _Name = strKey; + _Description = strDesc; + _IsEditEnable = isEditEnable; + _IsHide = isHide; + _IsReadOnly = isReadOnly; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyDescriptor.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyDescriptor.cs new file mode 100644 index 0000000..74460cf --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyDescriptor.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace ST.Library.UI.NodeEditor; + +public class STNodePropertyDescriptor +{ + private static Type m_t_int = typeof(int); + + private static Type m_t_float = typeof(float); + + private static Type m_t_double = typeof(double); + + private static Type m_t_string = typeof(string); + + private static Type m_t_bool = typeof(bool); + + private StringFormat m_sf; + + public STNode Node { get; internal set; } + + public STNodePropertyGrid Control { get; internal set; } + + public Rectangle Rectangle { get; internal set; } + + public Rectangle RectangleL { get; internal set; } + + public Rectangle RectangleR { get; internal set; } + + public string Name { get; internal set; } + + public string Description { get; internal set; } + + public PropertyInfo PropertyInfo { get; internal set; } + + public bool IsEditEnable { get; internal set; } + + public bool IsReadOnly { get; internal set; } + + public STNodePropertyDescriptor() + { + m_sf = new StringFormat(); + m_sf.LineAlignment = StringAlignment.Center; + m_sf.FormatFlags = StringFormatFlags.NoWrap; + IsEditEnable = true; + IsReadOnly = false; + } + + protected internal virtual void OnSetItemLocation() + { + } + + protected internal virtual object GetValueFromString(string strText) + { + Type propertyType = PropertyInfo.PropertyType; + if (propertyType == m_t_int) + { + return int.Parse(strText); + } + if (propertyType == m_t_float) + { + return float.Parse(strText); + } + if (propertyType == m_t_double) + { + return double.Parse(strText); + } + if (propertyType == m_t_string) + { + return strText; + } + if (propertyType == m_t_bool) + { + return bool.Parse(strText); + } + if (propertyType.IsEnum) + { + string value = Regex.Replace(strText, "[\\[\\]]", ""); + return Enum.Parse(propertyType, value); + } + if (propertyType.IsArray) + { + Type elementType = propertyType.GetElementType(); + if (elementType == m_t_string) + { + return strText.Split(','); + } + string[] array = strText.Trim(' ', ',').Split(','); + if (elementType == m_t_int) + { + int[] array2 = new int[array.Length]; + for (int i = 0; i < array.Length; i++) + { + array2[i] = int.Parse(array[i].Trim()); + } + return array2; + } + if (elementType == m_t_float) + { + float[] array3 = new float[array.Length]; + for (int j = 0; j < array.Length; j++) + { + array3[j] = float.Parse(array[j].Trim()); + } + return array3; + } + if (elementType == m_t_int) + { + double[] array4 = new double[array.Length]; + for (int k = 0; k < array.Length; k++) + { + array4[k] = double.Parse(array[k].Trim()); + } + return array4; + } + if (elementType == m_t_int) + { + bool[] array5 = new bool[array.Length]; + for (int l = 0; l < array.Length; l++) + { + array5[l] = bool.Parse(array[l].Trim()); + } + return array5; + } + } + throw new InvalidCastException("无法完成[string]到[" + propertyType.FullName + "]的转换 请重载[STNodePropertyDescriptor.GetValueFromString(string)]"); + } + + protected internal virtual string GetStringFromValue() + { + object value = PropertyInfo.GetValue(Node, null); + Type propertyType = PropertyInfo.PropertyType; + if (value == null) + { + return null; + } + if (propertyType.IsArray) + { + List list = new List(); + foreach (object item in (Array)value) + { + list.Add(item.ToString()); + } + return string.Join(",", list.ToArray()); + } + return value.ToString(); + } + + private string GetLocalizedStringFromValue() + { + string value = GetStringFromValue(); + return value != null && PropertyInfo.PropertyType.IsEnum ? Lang.Get(value) : value; + } + + protected internal virtual object GetValueFromBytes(byte[] byData) + { + if (byData == null) + { + return null; + } + string strText = Encoding.UTF8.GetString(byData); + return GetValueFromString(strText); + } + + protected internal virtual byte[] GetBytesFromValue() + { + string stringFromValue = GetStringFromValue(); + if (stringFromValue == null) + { + return null; + } + return Encoding.UTF8.GetBytes(stringFromValue); + } + + protected internal virtual object GetValue(object[] index) + { + return PropertyInfo.GetValue(Node, index); + } + + protected internal virtual void SetValue(object value) + { + PropertyInfo.SetValue(Node, value, null); + } + + protected internal virtual void SetValue(string strValue) + { + PropertyInfo.SetValue(Node, GetValueFromString(strValue), null); + } + + protected internal virtual void SetValue(byte[] byData) + { + PropertyInfo.SetValue(Node, GetValueFromBytes(byData), null); + } + + protected internal virtual void SetValue(object value, object[] index) + { + PropertyInfo.SetValue(Node, value, index); + } + + protected internal virtual void SetValue(string strValue, object[] index) + { + PropertyInfo.SetValue(Node, GetValueFromString(strValue), index); + } + + protected internal virtual void SetValue(byte[] byData, object[] index) + { + PropertyInfo.SetValue(Node, GetValueFromBytes(byData), index); + } + + protected internal virtual void OnSetValueError(Exception ex) + { + Control?.SetErrorMessage(ex.Message); + } + + protected internal virtual void OnDrawValueRectangle(DrawingTools dt) + { + if (Control == null) + { + return; + } + Graphics graphics = dt.Graphics; + SolidBrush solidBrush = dt.SolidBrush; + STNodePropertyGrid control = Control; + solidBrush.Color = control.ItemValueBackColor; + graphics.FillRectangle(solidBrush, RectangleR); + Rectangle rectangleR = RectangleR; + rectangleR.Width--; + rectangleR.Height--; + solidBrush.Color = Control.ForeColor; + graphics.DrawString(GetLocalizedStringFromValue(), control.Font, solidBrush, RectangleR, m_sf); + if (PropertyInfo.PropertyType.IsEnum || PropertyInfo.PropertyType == m_t_bool) + { + graphics.FillPolygon(Brushes.Gray, new Point[3] + { + new Point(rectangleR.Right - 13, rectangleR.Top + rectangleR.Height / 2 - 2), + new Point(rectangleR.Right - 4, rectangleR.Top + rectangleR.Height / 2 - 2), + new Point(rectangleR.Right - 9, rectangleR.Top + rectangleR.Height / 2 + 3) + }); + } + } + + protected internal virtual void OnMouseEnter(EventArgs e) + { + } + + protected internal virtual void OnMouseDown(STNodeMouseEventArgs e) + { + } + + protected internal virtual void OnMouseMove(STNodeMouseEventArgs e) + { + } + + protected internal virtual void OnMouseUp(STNodeMouseEventArgs e) + { + } + + protected internal virtual void OnMouseLeave(EventArgs e) + { + } + + protected internal virtual void OnMouseClick(STNodeMouseEventArgs e) + { + if (IsShowFrm()) + { + Control?.BeginEdit(this); + } + } + + private bool IsShowFrm() + { + if (!IsReadOnly) + { + return IsEditEnable; + } + return false; + } + + public void Invalidate() + { + if (Control == null) + { + return; + } + Control.Invalidate(Rectangle); + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyGrid.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyGrid.cs new file mode 100644 index 0000000..eed91c6 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyGrid.cs @@ -0,0 +1,838 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using DrawingColor = System.Drawing.Color; +using DrawingFont = System.Drawing.Font; +using MediaBrushes = System.Windows.Media.Brushes; +using MediaColor = System.Windows.Media.Color; +using MediaFontFamily = System.Windows.Media.FontFamily; + +namespace ST.Library.UI.NodeEditor; + +/// +/// WPF property editor for values. +/// +public class STNodePropertyGrid : UserControl, IDisposable +{ + private readonly Border m_title_border; + private readonly TextBlock m_title_text; + private readonly Button m_switch_button; + private readonly Border m_error_border; + private readonly TextBlock m_error_text; + private readonly ScrollViewer m_scroll_viewer; + private readonly StackPanel m_content; + private readonly Border m_description_border; + private readonly TextBlock m_description_text; + private readonly List m_descriptors = new List(); + private readonly Dictionary m_value_hosts = new Dictionary(); + private readonly string[] m_info_keys = new string[4] { "作者", "邮箱", "链接", "查看帮助" }; + + private STNode _node; + private STNodeAttribute m_node_attribute; + private bool m_show_info; + private bool m_show_title = true; + private bool m_auto_color = true; + private bool m_info_first_on_draw = true; + private bool m_read_only_model; + private bool m_is_edit_enable = true; + private bool m_disposed; + private string m_error_message; + private DrawingFont m_font = new DrawingFont("Segoe UI", 9f); + + private DrawingColor m_item_hover_color = DrawingColor.FromArgb(50, 125, 125, 125); + private DrawingColor m_item_selected_color = DrawingColor.DodgerBlue; + private DrawingColor m_item_value_back_color = DrawingColor.FromArgb(255, 50, 50, 50); + private DrawingColor m_title_color = DrawingColor.FromArgb(255, 60, 60, 60); + private DrawingColor m_error_color = DrawingColor.IndianRed; + private DrawingColor m_description_color = DrawingColor.Gray; + private DrawingColor m_back_color = DrawingColor.FromArgb(255, 35, 35, 35); + private DrawingColor m_fore_color = DrawingColor.FromArgb(255, 220, 220, 220); + + [Browsable(false)] + public STNode STNode => _node; + + public DrawingColor ItemHoverColor + { + get => m_item_hover_color; + set => m_item_hover_color = value; + } + + public DrawingColor ItemSelectedColor + { + get => m_item_selected_color; + set => m_item_selected_color = value; + } + + public DrawingColor ItemValueBackColor + { + get => m_item_value_back_color; + set + { + m_item_value_back_color = value; + RebuildContent(); + } + } + + public DrawingColor TitleColor + { + get => m_title_color; + set + { + m_title_color = value; + ApplyColors(); + } + } + + public DrawingColor ErrorColor + { + get => m_error_color; + set + { + m_error_color = value; + ApplyColors(); + } + } + + public DrawingColor DescriptionColor + { + get => m_description_color; + set + { + m_description_color = value; + ApplyColors(); + } + } + + public DrawingColor BackColor + { + get => m_back_color; + set + { + m_back_color = value; + ApplyColors(); + } + } + + public DrawingColor ForeColor + { + get => m_fore_color; + set + { + m_fore_color = value; + ApplyColors(); + } + } + + public DrawingFont Font + { + get => m_font; + set + { + if (value == null || ReferenceEquals(m_font, value)) + { + return; + } + m_font.Dispose(); + m_font = value; + ApplyFont(); + } + } + + public string Text { get; set; } = "NodeProperty"; + + [DefaultValue(true)] + public bool ShowTitle + { + get => m_show_title; + set + { + m_show_title = value; + m_title_border.Visibility = value ? Visibility.Visible : Visibility.Collapsed; + } + } + + [DefaultValue(true)] + public bool AutoColor + { + get => m_auto_color; + set + { + m_auto_color = value; + ApplyColors(); + } + } + + [DefaultValue(true)] + public bool InfoFirstOnDraw + { + get => m_info_first_on_draw; + set => m_info_first_on_draw = value; + } + + [DefaultValue(false)] + public bool ReadOnlyModel + { + get => m_read_only_model; + set + { + m_read_only_model = value; + RebuildContent(); + } + } + + [Browsable(false)] + public int ScrollOffset => -(int)Math.Round(m_scroll_viewer.VerticalOffset); + + [DefaultValue(true)] + public bool IsEditEnable + { + get => m_is_edit_enable; + set + { + m_is_edit_enable = value; + if (_node != null) + { + BuildDescriptors(); + RebuildContent(); + } + } + } + + public STNodePropertyGrid() + { + Focusable = false; + MinWidth = 120; + MinHeight = 50; + + var root = new DockPanel(); + Content = root; + + var title_grid = new Grid(); + title_grid.ColumnDefinitions.Add(new ColumnDefinition()); + title_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + m_title_text = new TextBlock + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + Margin = new Thickness(8, 2, 4, 2) + }; + title_grid.Children.Add(m_title_text); + m_switch_button = new Button + { + Content = "↔", + MinWidth = 24, + Padding = new Thickness(4, 0, 4, 0), + Margin = new Thickness(2), + Visibility = Visibility.Collapsed, + ToolTip = "切换节点信息与属性" + }; + m_switch_button.Click += OnSwitchClick; + Grid.SetColumn(m_switch_button, 1); + title_grid.Children.Add(m_switch_button); + m_title_border = new Border + { + MinHeight = 24, + Child = title_grid + }; + DockPanel.SetDock(m_title_border, Dock.Top); + root.Children.Add(m_title_border); + + m_error_text = new TextBlock + { + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(6, 4, 6, 4) + }; + m_error_border = new Border + { + Child = m_error_text, + Visibility = Visibility.Collapsed + }; + DockPanel.SetDock(m_error_border, Dock.Top); + root.Children.Add(m_error_border); + + m_description_text = new TextBlock + { + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(6, 4, 6, 4) + }; + m_description_border = new Border + { + Child = m_description_text, + Visibility = Visibility.Collapsed + }; + DockPanel.SetDock(m_description_border, Dock.Bottom); + root.Children.Add(m_description_border); + + m_content = new StackPanel(); + m_scroll_viewer = new ScrollViewer + { + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + Content = m_content + }; + root.Children.Add(m_scroll_viewer); + + ApplyColors(); + ApplyFont(); + UpdateTitle(); + } + + public void SetNode(STNode node) + { + _node = node; + m_node_attribute = node?.GetType() + .GetCustomAttributes(typeof(STNodeAttribute), inherit: true) + .OfType() + .FirstOrDefault(); + m_error_message = null; + m_show_info = node != null && (m_info_first_on_draw || !HasVisibleProperties(node)); + m_scroll_viewer.ScrollToTop(); + BuildDescriptors(); + UpdateTitle(); + UpdateError(); + RebuildContent(); + ApplyColors(); + } + + public void SetInfoKey(string author, string mail, string link, string help) + { + m_info_keys[0] = author; + m_info_keys[1] = mail; + m_info_keys[2] = link; + m_info_keys[3] = help; + if (m_show_info) + { + RebuildContent(); + } + } + + public void SetErrorMessage(string text) + { + m_error_message = text; + UpdateError(); + } + + public void Invalidate(Rectangle rectangle) + { + InvalidateVisual(); + foreach (STNodePropertyValueHost host in m_value_hosts.Values) + { + host.InvalidatePresenter(); + } + } + + private void OnSwitchClick(object sender, RoutedEventArgs e) + { + m_show_info = !m_show_info; + m_scroll_viewer.ScrollToTop(); + RebuildContent(); + } + + private bool HasVisibleProperties(STNode node) + { + return node.GetType().GetProperties().Any(property => + { + var attribute = property.GetCustomAttributes(typeof(STNodePropertyAttribute), inherit: true) + .OfType() + .FirstOrDefault(); + return attribute != null && (m_is_edit_enable || !attribute.IsHide); + }); + } + + private void BuildDescriptors() + { + m_descriptors.Clear(); + if (_node == null) + { + return; + } + + foreach (PropertyInfo property in _node.GetType().GetProperties()) + { + var attribute = property.GetCustomAttributes(typeof(STNodePropertyAttribute), inherit: true) + .OfType() + .FirstOrDefault(); + if (attribute == null || (!m_is_edit_enable && attribute.IsHide)) + { + continue; + } + + if (Activator.CreateInstance(attribute.DescriptorType) is not STNodePropertyDescriptor descriptor) + { + throw new ArgumentException("[STNodePropertyAttribute.DescriptorType]参数值必须为[STNodePropertyDescriptor]或者其子类的类型"); + } + + descriptor.Node = _node; + descriptor.Name = Lang.Get(attribute.Name); + descriptor.Description = Lang.GetOrDefault(attribute.Description); + descriptor.PropertyInfo = property; + descriptor.IsEditEnable = m_is_edit_enable || attribute.IsEditEnable; + descriptor.IsReadOnly = attribute.IsReadOnly; + descriptor.Control = this; + m_descriptors.Add(descriptor); + } + } + + private void RebuildContent() + { + if (m_content == null) + { + return; + } + + m_value_hosts.Clear(); + m_content.Children.Clear(); + if (_node == null) + { + m_switch_button.Visibility = Visibility.Collapsed; + return; + } + + m_switch_button.Visibility = m_node_attribute != null && m_descriptors.Count > 0 + ? Visibility.Visible + : Visibility.Collapsed; + if (m_show_info) + { + BuildInfoPanel(); + return; + } + + for (int index = 0; index < m_descriptors.Count; index++) + { + m_content.Children.Add(CreatePropertyRow(m_descriptors[index], index)); + } + } + + private Grid CreatePropertyRow(STNodePropertyDescriptor descriptor, int index) + { + var row = new Grid + { + MinHeight = 32, + Background = index % 2 == 0 + ? ToBrush(DrawingColor.FromArgb(20, 0, 0, 0)) + : ToBrush(DrawingColor.FromArgb(20, 255, 255, 255)) + }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(2, GridUnitType.Star) }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3, GridUnitType.Star) }); + + var name = new TextBlock + { + Text = descriptor.Name, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Right, + TextTrimming = TextTrimming.CharacterEllipsis, + Margin = new Thickness(4) + }; + name.MouseEnter += (s, e) => ShowDescription(descriptor.Description); + name.MouseLeave += (s, e) => HideDescription(); + row.Children.Add(name); + + FrameworkElement editor = CreateValueEditor(descriptor, row); + Grid.SetColumn(editor, 1); + row.Children.Add(editor); + ApplyFont(row); + return row; + } + + private FrameworkElement CreateValueEditor(STNodePropertyDescriptor descriptor, Grid row) + { + bool readOnly = m_read_only_model || descriptor.IsReadOnly || !descriptor.IsEditEnable; + if (descriptor.GetType() != typeof(STNodePropertyDescriptor)) + { + var host = new STNodePropertyValueHost(this, descriptor, row, readOnly); + m_value_hosts[descriptor] = host; + return host; + } + + Type propertyType = descriptor.PropertyInfo.PropertyType; + if (propertyType == typeof(bool)) + { + var checkBox = new CheckBox + { + IsChecked = descriptor.GetValue(null) is bool value && value, + IsEnabled = !readOnly, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(8, 2, 4, 2) + }; + checkBox.Click += (s, e) => CommitValue(descriptor, checkBox.IsChecked == true); + return checkBox; + } + + if (propertyType.IsEnum) + { + var comboBox = new ComboBox + { + ItemsSource = Enum.GetNames(propertyType), + SelectedItem = descriptor.GetValue(null)?.ToString(), + IsEnabled = !readOnly, + Margin = new Thickness(4, 3, 4, 3) + }; + comboBox.SelectionChanged += (s, e) => + { + if (comboBox.SelectedItem is string text) + { + CommitText(descriptor, text); + } + }; + return comboBox; + } + + var textBox = new TextBox + { + Text = descriptor.GetStringFromValue() ?? string.Empty, + IsReadOnly = readOnly, + VerticalContentAlignment = VerticalAlignment.Center, + Background = ToBrush(m_item_value_back_color), + Foreground = ToBrush(m_fore_color), + BorderThickness = new Thickness(0), + Margin = new Thickness(4, 3, 4, 3), + Padding = new Thickness(4, 1, 4, 1) + }; + if (!readOnly) + { + textBox.KeyDown += (s, e) => + { + if (e.Key == Key.Enter) + { + CommitText(descriptor, textBox.Text); + e.Handled = true; + } + }; + textBox.LostKeyboardFocus += (s, e) => CommitText(descriptor, textBox.Text); + } + + if (descriptor.GetType() == typeof(STNodePropertyDescriptor) || readOnly) + { + return textBox; + } + + var panel = new Grid(); + panel.ColumnDefinitions.Add(new ColumnDefinition()); + panel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + panel.Children.Add(textBox); + var customButton = new Button + { + Content = "…", + MinWidth = 24, + Margin = new Thickness(0, 3, 4, 3), + Padding = new Thickness(3, 0, 3, 0) + }; + customButton.Click += (s, e) => + { + try + { + var location = new System.Drawing.Point(descriptor.RectangleR.Right - 12, descriptor.RectangleR.Top + descriptor.RectangleR.Height / 2); + descriptor.OnMouseClick(new STNodeMouseEventArgs(STMouseButtons.Left, 1, location.X, location.Y, 0)); + textBox.Text = descriptor.GetStringFromValue() ?? string.Empty; + _node?.Owner?.Invalidate(); + SetErrorMessage(null); + } + catch (Exception ex) + { + descriptor.OnSetValueError(ex); + } + }; + Grid.SetColumn(customButton, 1); + panel.Children.Add(customButton); + return panel; + } + + internal void CommitText(STNodePropertyDescriptor descriptor, string text) + { + try + { + descriptor.SetValue(text); + _node?.Owner?.Invalidate(); + SetErrorMessage(null); + RefreshDescriptor(descriptor); + } + catch (Exception ex) + { + descriptor.OnSetValueError(ex); + } + } + + internal void BeginEdit(STNodePropertyDescriptor descriptor) + { + if (m_read_only_model || descriptor == null) + { + return; + } + if (m_value_hosts.TryGetValue(descriptor, out STNodePropertyValueHost host)) + { + host.BeginEdit(); + } + } + + internal void RefreshDescriptor(STNodePropertyDescriptor descriptor) + { + if (descriptor != null && m_value_hosts.TryGetValue(descriptor, out STNodePropertyValueHost host)) + { + host.InvalidatePresenter(); + } + _node?.Owner?.Invalidate(); + } + + internal void UpdateDescriptorLayout( + STNodePropertyDescriptor descriptor, + Grid row, + FrameworkElement valueElement) + { + if (descriptor == null + || row == null + || valueElement == null + || !m_content.IsAncestorOf(row) + || row.ActualWidth <= 0d + || row.ActualHeight <= 0d + || valueElement.ActualWidth <= 0d + || valueElement.ActualHeight <= 0d) + { + return; + } + + System.Windows.Point rowOrigin = row.TranslatePoint(new System.Windows.Point(0d, 0d), m_content); + System.Windows.Point valueOrigin = valueElement.TranslatePoint(new System.Windows.Point(0d, 0d), row); + int titleHeight = m_show_title ? (int)Math.Round(m_title_border.ActualHeight) : 0; + int rowTop = titleHeight + (int)Math.Round(rowOrigin.Y); + int rowHeight = Math.Max(1, (int)Math.Round(row.ActualHeight)); + int valueLeft = (int)Math.Round(valueOrigin.X); + int valueTop = rowTop + (int)Math.Round(valueOrigin.Y); + var rectangle = new Rectangle(0, rowTop, Math.Max(1, (int)Math.Round(row.ActualWidth)), rowHeight); + var rectangleLeft = new Rectangle(0, rowTop, Math.Max(0, valueLeft), rowHeight); + var rectangleRight = new Rectangle( + valueLeft, + valueTop, + Math.Max(1, (int)Math.Round(valueElement.ActualWidth)), + Math.Max(1, (int)Math.Round(valueElement.ActualHeight))); + if (descriptor.Rectangle == rectangle + && descriptor.RectangleL == rectangleLeft + && descriptor.RectangleR == rectangleRight) + { + return; + } + + descriptor.Rectangle = rectangle; + descriptor.RectangleL = rectangleLeft; + descriptor.RectangleR = rectangleRight; + try + { + descriptor.OnSetItemLocation(); + } + catch (Exception ex) + { + descriptor.OnSetValueError(ex); + } + } + + private void CommitValue(STNodePropertyDescriptor descriptor, object value) + { + try + { + descriptor.SetValue(value); + _node?.Owner?.Invalidate(); + SetErrorMessage(null); + } + catch (Exception ex) + { + descriptor.OnSetValueError(ex); + } + } + + private void BuildInfoPanel() + { + if (m_node_attribute == null) + { + return; + } + + AddInfoRow(m_info_keys[0], m_node_attribute.Author); + AddInfoRow(m_info_keys[1], m_node_attribute.Mail); + AddInfoRow(m_info_keys[2], m_node_attribute.Link, isLink: true); + + if (!string.IsNullOrWhiteSpace(m_node_attribute.DisplayDescription)) + { + m_content.Children.Add(new TextBlock + { + Text = m_node_attribute.DisplayDescription, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(8) + }); + } + + var helpButton = new Button + { + Content = m_info_keys[3], + Margin = new Thickness(8), + IsEnabled = STNodeAttribute.GetHelpMethod(_node.GetType()) != null + }; + helpButton.Click += (s, e) => + { + try + { + STNodeAttribute.ShowHelp(_node.GetType()); + } + catch (Exception ex) + { + SetErrorMessage(ex.Message); + } + }; + m_content.Children.Add(helpButton); + ApplyFont(m_content); + } + + private void AddInfoRow(string key, string value, bool isLink = false) + { + var row = new Grid { MinHeight = 30 }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(2, GridUnitType.Star) }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3, GridUnitType.Star) }); + row.Children.Add(new TextBlock + { + Text = key, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(6) + }); + FrameworkElement valueElement; + if (isLink && !string.IsNullOrWhiteSpace(value)) + { + var button = new Button + { + Content = value, + HorizontalContentAlignment = HorizontalAlignment.Left, + BorderThickness = new Thickness(0), + Background = MediaBrushes.Transparent, + Foreground = MediaBrushes.CornflowerBlue, + Padding = new Thickness(4), + Cursor = Cursors.Hand + }; + button.Click += (s, e) => + { + try + { + Process.Start(new ProcessStartInfo(value) { UseShellExecute = true }); + } + catch (Exception ex) + { + SetErrorMessage(ex.Message); + } + }; + valueElement = button; + } + else + { + valueElement = new TextBlock + { + Text = value ?? string.Empty, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(6), + Opacity = 0.75 + }; + } + Grid.SetColumn(valueElement, 1); + row.Children.Add(valueElement); + m_content.Children.Add(row); + } + + private void UpdateTitle() + { + m_title_text.Text = _node?.Title ?? Text; + } + + private void UpdateError() + { + m_error_text.Text = m_error_message ?? string.Empty; + m_error_border.Visibility = string.IsNullOrWhiteSpace(m_error_message) + ? Visibility.Collapsed + : Visibility.Visible; + } + + private void ShowDescription(string description) + { + if (string.IsNullOrWhiteSpace(description)) + { + return; + } + m_description_text.Text = description; + m_description_border.Visibility = Visibility.Visible; + } + + private void HideDescription() + { + m_description_border.Visibility = Visibility.Collapsed; + } + + private void ApplyColors() + { + Background = ToBrush(m_back_color); + Foreground = ToBrush(m_fore_color); + DrawingColor title = m_auto_color && _node != null ? _node.TitleColor : m_title_color; + m_title_border.Background = ToBrush(title); + m_error_border.Background = ToBrush(DrawingColor.FromArgb(210, m_error_color)); + m_description_border.Background = ToBrush(DrawingColor.FromArgb(210, m_description_color)); + foreach (STNodePropertyValueHost host in m_value_hosts.Values) + { + host.InvalidatePresenter(); + } + } + + private void ApplyFont() + { + ApplyFont(this); + foreach (STNodePropertyValueHost host in m_value_hosts.Values) + { + host.InvalidatePresenter(); + } + } + + private void ApplyFont(DependencyObject root) + { + if (root is Control control) + { + control.FontFamily = new MediaFontFamily(m_font.Name); + control.FontSize = Math.Max(1d, m_font.SizeInPoints * 96d / 72d); + } + else if (root is TextBlock text) + { + text.FontFamily = new MediaFontFamily(m_font.Name); + text.FontSize = Math.Max(1d, m_font.SizeInPoints * 96d / 72d); + } + + int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(root); + for (int i = 0; i < count; i++) + { + ApplyFont(System.Windows.Media.VisualTreeHelper.GetChild(root, i)); + } + } + + private static SolidColorBrush ToBrush(DrawingColor color) + { + var brush = new SolidColorBrush(MediaColor.FromArgb(color.A, color.R, color.G, color.B)); + brush.Freeze(); + return brush; + } + + public void Dispose() + { + if (m_disposed) + { + return; + } + m_disposed = true; + m_switch_button.Click -= OnSwitchClick; + m_content.Children.Clear(); + m_descriptors.Clear(); + m_value_hosts.Clear(); + m_font?.Dispose(); + m_font = null; + GC.SuppressFinalize(this); + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyValuePresenter.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyValuePresenter.cs new file mode 100644 index 0000000..6cdb8c5 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodePropertyValuePresenter.cs @@ -0,0 +1,467 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Windows; +using System.Windows.Automation; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Threading; +using DrawingColor = System.Drawing.Color; +using DrawingPen = System.Drawing.Pen; +using DrawingPixelFormat = System.Drawing.Imaging.PixelFormat; +using DrawingSolidBrush = System.Drawing.SolidBrush; +using WpfDrawingContext = System.Windows.Media.DrawingContext; +using WpfPixelFormats = System.Windows.Media.PixelFormats; +using WpfRect = System.Windows.Rect; +using WpfVisualTreeHelper = System.Windows.Media.VisualTreeHelper; +using MediaFontFamily = System.Windows.Media.FontFamily; + +namespace ST.Library.UI.NodeEditor; + +/// +/// Hosts the GDI-rendered value surface used by custom property descriptors +/// and a native WPF editor when the descriptor delegates to its base click. +/// +internal sealed class STNodePropertyValueHost : Grid +{ + private readonly STNodePropertyGrid _grid; + private readonly STNodePropertyDescriptor _descriptor; + private readonly Grid _row; + private readonly STNodePropertyValuePresenter _presenter; + private readonly bool _readOnly; + private FrameworkElement _editor; + private bool _closingEditor; + + public STNodePropertyValueHost( + STNodePropertyGrid grid, + STNodePropertyDescriptor descriptor, + Grid row, + bool readOnly) + { + _grid = grid; + _descriptor = descriptor; + _row = row; + _readOnly = readOnly; + Margin = new Thickness(4, 3, 4, 3); + ClipToBounds = true; + _presenter = new STNodePropertyValuePresenter(this, grid, descriptor); + Children.Add(_presenter); + Loaded += OnLayoutChanged; + SizeChanged += OnLayoutChanged; + AutomationProperties.SetName(_presenter, descriptor.Name ?? descriptor.PropertyInfo?.Name ?? "Property value"); + } + + internal bool IsReadOnly => _readOnly; + + internal void UpdateDescriptorLayout() + { + _grid.UpdateDescriptorLayout(_descriptor, _row, this); + } + + internal void InvalidatePresenter() + { + _presenter.InvalidateVisual(); + } + + internal void BeginEdit() + { + if (_readOnly || _editor != null) + { + return; + } + + _presenter.ReleaseMouseCapture(); + Type propertyType = _descriptor.PropertyInfo.PropertyType; + if (propertyType == typeof(bool) || propertyType.IsEnum) + { + BeginSelectionEdit(propertyType); + return; + } + + var textBox = new TextBox + { + Text = _descriptor.GetStringFromValue() ?? string.Empty, + VerticalContentAlignment = VerticalAlignment.Center, + BorderThickness = new Thickness(1), + Padding = new Thickness(3, 0, 3, 0), + Background = CreateValueBrush(), + Foreground = _grid.Foreground, + FontFamily = new MediaFontFamily(_grid.Font.Name), + FontSize = Math.Max(1d, _grid.Font.SizeInPoints * 96d / 72d) + }; + textBox.KeyDown += OnTextEditorKeyDown; + textBox.LostKeyboardFocus += OnEditorLostKeyboardFocus; + ShowEditor(textBox); + textBox.SelectAll(); + } + + private void BeginSelectionEdit(Type propertyType) + { + string[] items = propertyType == typeof(bool) + ? new[] { bool.TrueString, bool.FalseString } + : Enum.GetNames(propertyType); + var comboBox = new ComboBox + { + ItemsSource = items, + SelectedItem = _descriptor.GetValue(null)?.ToString(), + Background = CreateValueBrush(), + Foreground = _grid.Foreground, + FontFamily = new MediaFontFamily(_grid.Font.Name), + FontSize = Math.Max(1d, _grid.Font.SizeInPoints * 96d / 72d) + }; + comboBox.SelectionChanged += OnSelectionChanged; + comboBox.KeyDown += OnSelectionEditorKeyDown; + comboBox.LostKeyboardFocus += OnEditorLostKeyboardFocus; + ShowEditor(comboBox); + _ = comboBox.Dispatcher.BeginInvoke( + DispatcherPriority.Input, + new Action(() => + { + if (ReferenceEquals(_editor, comboBox)) + { + comboBox.IsDropDownOpen = true; + } + })); + } + + private void ShowEditor(FrameworkElement editor) + { + _editor = editor; + Panel.SetZIndex(editor, 1); + Children.Add(editor); + editor.Focus(); + } + + private void OnTextEditorKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + CloseEditor(commit: true); + e.Handled = true; + } + else if (e.Key == Key.Escape) + { + CloseEditor(commit: false); + e.Handled = true; + } + } + + private void OnSelectionEditorKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + CloseEditor(commit: false); + e.Handled = true; + } + else if (e.Key == Key.Enter) + { + CloseEditor(commit: true); + e.Handled = true; + } + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_editor != null && !_closingEditor) + { + CloseEditor(commit: true); + } + } + + private void OnEditorLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + if (sender is not FrameworkElement editor) + { + return; + } + _ = editor.Dispatcher.BeginInvoke( + DispatcherPriority.Input, + new Action(() => + { + if (ReferenceEquals(_editor, editor) && !editor.IsKeyboardFocusWithin) + { + CloseEditor(commit: true); + } + })); + } + + private void CloseEditor(bool commit) + { + if (_editor == null || _closingEditor) + { + return; + } + + _closingEditor = true; + FrameworkElement editor = _editor; + try + { + if (commit) + { + string text = editor switch + { + TextBox textBox => textBox.Text, + ComboBox comboBox => comboBox.SelectedItem?.ToString(), + _ => null + }; + if (text != null) + { + _grid.CommitText(_descriptor, text); + } + } + } + finally + { + if (editor is TextBox textBox) + { + textBox.KeyDown -= OnTextEditorKeyDown; + textBox.LostKeyboardFocus -= OnEditorLostKeyboardFocus; + } + else if (editor is ComboBox comboBox) + { + comboBox.SelectionChanged -= OnSelectionChanged; + comboBox.KeyDown -= OnSelectionEditorKeyDown; + comboBox.LostKeyboardFocus -= OnEditorLostKeyboardFocus; + } + Children.Remove(editor); + _editor = null; + _closingEditor = false; + InvalidatePresenter(); + _presenter.Focus(); + } + } + + private SolidColorBrush CreateValueBrush() + { + DrawingColor color = _grid.ItemValueBackColor; + return new SolidColorBrush(System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B)); + } + + private void OnLayoutChanged(object sender, EventArgs e) + { + UpdateDescriptorLayout(); + } +} + +internal sealed class STNodePropertyValuePresenter : FrameworkElement +{ + private readonly STNodePropertyValueHost _host; + private readonly STNodePropertyGrid _grid; + private readonly STNodePropertyDescriptor _descriptor; + private System.Drawing.Point? _mouseDownLocation; + + public STNodePropertyValuePresenter( + STNodePropertyValueHost host, + STNodePropertyGrid grid, + STNodePropertyDescriptor descriptor) + { + _host = host; + _grid = grid; + _descriptor = descriptor; + Focusable = true; + Cursor = Cursors.Arrow; + } + + protected override void OnRender(WpfDrawingContext drawingContext) + { + base.OnRender(drawingContext); + _host.UpdateDescriptorLayout(); + if (ActualWidth <= 0d || ActualHeight <= 0d) + { + return; + } + + DpiScale dpi = WpfVisualTreeHelper.GetDpi(this); + int pixelWidth = Math.Max(1, (int)Math.Ceiling(ActualWidth * dpi.DpiScaleX)); + int pixelHeight = Math.Max(1, (int)Math.Ceiling(ActualHeight * dpi.DpiScaleY)); + using Bitmap bitmap = new Bitmap(pixelWidth, pixelHeight, DrawingPixelFormat.Format32bppPArgb); + using Graphics graphics = Graphics.FromImage(bitmap); + graphics.Clear(_grid.ItemValueBackColor); + graphics.ScaleTransform((float)dpi.DpiScaleX, (float)dpi.DpiScaleY); + graphics.TranslateTransform(-_descriptor.RectangleR.Left, -_descriptor.RectangleR.Top); + try + { + using DrawingPen pen = new DrawingPen(DrawingColor.Black, 1f); + using DrawingSolidBrush brush = new DrawingSolidBrush(DrawingColor.Black); + var drawingTools = new DrawingTools + { + Graphics = graphics, + Pen = pen, + SolidBrush = brush + }; + _descriptor.OnDrawValueRectangle(drawingTools); + if (_host.IsReadOnly) + { + using DrawingSolidBrush overlay = new DrawingSolidBrush(DrawingColor.FromArgb(125, 125, 125, 125)); + graphics.FillRectangle(overlay, _descriptor.RectangleR); + } + } + catch (Exception ex) + { + _descriptor.OnSetValueError(ex); + } + + BitmapData bitmapData = bitmap.LockBits( + new Rectangle(0, 0, pixelWidth, pixelHeight), + ImageLockMode.ReadOnly, + DrawingPixelFormat.Format32bppPArgb); + try + { + var renderTarget = new WriteableBitmap( + pixelWidth, + pixelHeight, + dpi.PixelsPerInchX, + dpi.PixelsPerInchY, + WpfPixelFormats.Pbgra32, + null); + renderTarget.WritePixels( + new Int32Rect(0, 0, pixelWidth, pixelHeight), + bitmapData.Scan0, + Math.Abs(bitmapData.Stride) * pixelHeight, + bitmapData.Stride); + drawingContext.DrawImage(renderTarget, new WpfRect(0d, 0d, ActualWidth, ActualHeight)); + } + finally + { + bitmap.UnlockBits(bitmapData); + } + } + + protected override void OnMouseEnter(MouseEventArgs e) + { + base.OnMouseEnter(e); + Dispatch(() => _descriptor.OnMouseEnter(EventArgs.Empty)); + } + + protected override void OnMouseLeave(MouseEventArgs e) + { + base.OnMouseLeave(e); + Dispatch(() => _descriptor.OnMouseLeave(EventArgs.Empty)); + } + + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + STNodeMouseEventArgs args = CreateMouseArgs(e, GetPressedButtons(e), clicks: 0); + Dispatch(() => _descriptor.OnMouseMove(args)); + } + + protected override void OnMouseDown(MouseButtonEventArgs e) + { + base.OnMouseDown(e); + if (_host.IsReadOnly) + { + return; + } + + Focus(); + STNodeMouseEventArgs args = CreateMouseArgs(e, ToMouseButton(e.ChangedButton), e.ClickCount); + _mouseDownLocation = args.Location; + CaptureMouse(); + Dispatch(() => _descriptor.OnMouseDown(args)); + e.Handled = true; + } + + protected override void OnMouseUp(MouseButtonEventArgs e) + { + base.OnMouseUp(e); + if (_host.IsReadOnly || _mouseDownLocation == null) + { + return; + } + + STNodeMouseEventArgs args = CreateMouseArgs(e, ToMouseButton(e.ChangedButton), e.ClickCount); + System.Drawing.Point downLocation = _mouseDownLocation.Value; + _mouseDownLocation = null; + Dispatch(() => _descriptor.OnMouseUp(args)); + ReleaseMouseCapture(); + if (downLocation == args.Location) + { + Dispatch(() => _descriptor.OnMouseClick(args)); + } + _grid.RefreshDescriptor(_descriptor); + e.Handled = true; + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (!_host.IsReadOnly && (e.Key == Key.Enter || e.Key == Key.F2 || e.Key == Key.Space)) + { + _host.BeginEdit(); + e.Handled = true; + } + } + + protected override void OnLostMouseCapture(MouseEventArgs e) + { + base.OnLostMouseCapture(e); + _mouseDownLocation = null; + } + + private STNodeMouseEventArgs CreateMouseArgs(MouseEventArgs e, STMouseButtons buttons, int clicks) + { + _host.UpdateDescriptorLayout(); + System.Windows.Point point = e.GetPosition(this); + return new STNodeMouseEventArgs( + buttons, + clicks, + _descriptor.RectangleR.Left + (int)Math.Round(point.X), + _descriptor.RectangleR.Top + (int)Math.Round(point.Y), + e is MouseWheelEventArgs wheel ? wheel.Delta : 0); + } + + private void Dispatch(Action action) + { + try + { + action(); + InvalidateVisual(); + } + catch (Exception ex) + { + _descriptor.OnSetValueError(ex); + } + } + + private static STMouseButtons GetPressedButtons(MouseEventArgs e) + { + STMouseButtons buttons = STMouseButtons.None; + if (e.LeftButton == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.Left; + } + if (e.RightButton == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.Right; + } + if (e.MiddleButton == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.Middle; + } + if (e.XButton1 == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.XButton1; + } + if (e.XButton2 == MouseButtonState.Pressed) + { + buttons |= STMouseButtons.XButton2; + } + return buttons; + } + + private static STMouseButtons ToMouseButton(MouseButton button) + { + return button switch + { + MouseButton.Left => STMouseButtons.Left, + MouseButton.Right => STMouseButtons.Right, + MouseButton.Middle => STMouseButtons.Middle, + MouseButton.XButton1 => STMouseButtons.XButton1, + MouseButton.XButton2 => STMouseButtons.XButton2, + _ => STMouseButtons.None + }; + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeTreeView.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeTreeView.cs new file mode 100644 index 0000000..516563b --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeTreeView.cs @@ -0,0 +1,749 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using DrawingColor = System.Drawing.Color; +using DrawingFont = System.Drawing.Font; +using DrawingSize = System.Drawing.Size; +using MediaBrushes = System.Windows.Media.Brushes; +using MediaColor = System.Windows.Media.Color; +using MediaFontFamily = System.Windows.Media.FontFamily; +using WpfPoint = System.Windows.Point; + +namespace ST.Library.UI.NodeEditor; + +/// +/// WPF node catalog with search, drag-and-drop creation and node preview. +/// +public class STNodeTreeView : UserControl, IDisposable +{ + private sealed class CatalogNode + { + public string Name { get; set; } + + public Type NodeType { get; set; } + + public DrawingColor NodeColor { get; set; } = DrawingColor.DarkCyan; + + public List Children { get; } = new List(); + + public int NodeCount => NodeType == null + ? Children.Sum(child => child.NodeCount) + : 1; + } + + private readonly Dictionary m_dic_all_type = new Dictionary(); + private readonly Dictionary m_dic_node_title = new Dictionary(); + private readonly Dictionary m_dic_node_color = new Dictionary(); + private readonly STNodeEditor _editor; + private readonly STNodePropertyGrid _property_grid; + private readonly TextBox m_search_box; + private readonly Button m_clear_button; + private readonly System.Windows.Controls.TreeView m_tree; + private readonly Popup m_preview_popup; + + private string m_search_text = string.Empty; + private WpfPoint m_drag_start; + private Type m_drag_type; + private bool m_disposed; + private DrawingFont m_font = new DrawingFont("Segoe UI", 9f); + + private DrawingColor m_item_back_color = DrawingColor.FromArgb(255, 45, 45, 45); + private DrawingColor m_item_hover_color = DrawingColor.FromArgb(50, 125, 125, 125); + private DrawingColor m_title_color = DrawingColor.FromArgb(255, 60, 60, 60); + private DrawingColor m_text_box_color = DrawingColor.FromArgb(255, 30, 30, 30); + private DrawingColor m_highlight_text_color = DrawingColor.Lime; + private DrawingColor m_info_button_color = DrawingColor.Gray; + private DrawingColor m_folder_count_color = DrawingColor.FromArgb(100, 255, 255, 255); + private DrawingColor m_back_color = DrawingColor.FromArgb(255, 35, 35, 35); + private DrawingColor m_fore_color = DrawingColor.FromArgb(255, 220, 220, 220); + private bool m_show_folder_count = true; + private bool m_show_info_button = true; + private bool m_info_panel_is_left_layout = true; + private bool m_auto_color = true; + + public DrawingColor ItemBackColor + { + get => m_item_back_color; + set + { + m_item_back_color = value; + ApplyColors(); + } + } + + public DrawingColor ItemHoverColor + { + get => m_item_hover_color; + set => m_item_hover_color = value; + } + + public DrawingColor TitleColor + { + get => m_title_color; + set + { + m_title_color = value; + ApplyColors(); + } + } + + public DrawingColor TextBoxColor + { + get => m_text_box_color; + set + { + m_text_box_color = value; + ApplyColors(); + } + } + + public DrawingColor HightLightTextColor + { + get => m_highlight_text_color; + set + { + m_highlight_text_color = value; + RefreshTree(); + } + } + + public DrawingColor InfoButtonColor + { + get => m_info_button_color; + set + { + m_info_button_color = value; + RefreshTree(); + } + } + + public DrawingColor FolderCountColor + { + get => m_folder_count_color; + set + { + m_folder_count_color = value; + RefreshTree(); + } + } + + public DrawingColor BackColor + { + get => m_back_color; + set + { + m_back_color = value; + ApplyColors(); + } + } + + public DrawingColor ForeColor + { + get => m_fore_color; + set + { + m_fore_color = value; + ApplyColors(); + } + } + + public DrawingFont Font + { + get => m_font; + set + { + if (value == null || ReferenceEquals(value, m_font)) + { + return; + } + m_font.Dispose(); + m_font = value; + ApplyFont(); + } + } + + [DefaultValue(true)] + public bool ShowFolderCount + { + get => m_show_folder_count; + set + { + m_show_folder_count = value; + RefreshTree(); + } + } + + [DefaultValue(true)] + public bool ShowInfoButton + { + get => m_show_info_button; + set + { + m_show_info_button = value; + RefreshTree(); + } + } + + [DefaultValue(true)] + public bool InfoPanelIsLeftLayout + { + get => m_info_panel_is_left_layout; + set => m_info_panel_is_left_layout = value; + } + + [DefaultValue(true)] + public bool AutoColor + { + get => m_auto_color; + set + { + m_auto_color = value; + RefreshTree(); + } + } + + [Browsable(false)] + public STNodeEditor Editor => _editor; + + [Browsable(false)] + public STNodePropertyGrid PropertyGrid => _property_grid; + + [Browsable(false)] + public IReadOnlyDictionary NodeTypes => m_dic_all_type; + + public STNodeTreeView() + { + MinWidth = 100; + MinHeight = 60; + + var root = new DockPanel(); + Content = root; + + var search_border = new Border + { + Padding = new Thickness(5) + }; + var search_grid = new Grid(); + search_grid.ColumnDefinitions.Add(new ColumnDefinition()); + search_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + m_search_box = new TextBox + { + MaxLength = 50, + BorderThickness = new Thickness(0), + Padding = new Thickness(5, 2, 5, 2), + VerticalContentAlignment = VerticalAlignment.Center + }; + m_search_box.TextChanged += OnSearchTextChanged; + search_grid.Children.Add(m_search_box); + m_clear_button = new Button + { + Content = "×", + MinWidth = 24, + Padding = new Thickness(3, 0, 3, 0), + Margin = new Thickness(4, 0, 0, 0), + Visibility = Visibility.Collapsed, + ToolTip = "清除搜索" + }; + m_clear_button.Click += OnClearSearchClick; + Grid.SetColumn(m_clear_button, 1); + search_grid.Children.Add(m_clear_button); + search_border.Child = search_grid; + DockPanel.SetDock(search_border, Dock.Top); + root.Children.Add(search_border); + + m_tree = new System.Windows.Controls.TreeView + { + BorderThickness = new Thickness(0), + Padding = new Thickness(2) + }; + m_tree.PreviewMouseLeftButtonDown += OnTreeMouseLeftButtonDown; + m_tree.PreviewMouseMove += OnTreeMouseMove; + root.Children.Add(m_tree); + + _editor = new STNodeEditor + { + LimitCanvasToContentBounds = false, + ShowLocation = false, + ShowBorder = false, + ClientSize = new DrawingSize(360, 280) + }; + _property_grid = new STNodePropertyGrid + { + Width = 260 + }; + var preview_grid = new Grid + { + Width = 620, + Height = 300, + Background = ToBrush(m_back_color) + }; + preview_grid.ColumnDefinitions.Add(new ColumnDefinition()); + preview_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(260) }); + preview_grid.Children.Add(_editor); + Grid.SetColumn(_property_grid, 1); + preview_grid.Children.Add(_property_grid); + m_preview_popup = new Popup + { + AllowsTransparency = true, + StaysOpen = false, + Child = new Border + { + BorderBrush = MediaBrushes.DimGray, + BorderThickness = new Thickness(1), + Background = ToBrush(m_back_color), + Child = preview_grid + } + }; + + ApplyColors(); + ApplyFont(); + LoadAssembly(); + } + + public void Search(string text) + { + m_search_box.Text = text?.Trim() ?? string.Empty; + } + + public Type[] GetVisibleTypes() + { + return m_dic_all_type + .Where(entry => MatchesSearch(entry.Key, entry.Value)) + .Select(entry => entry.Key) + .ToArray(); + } + + public bool AddNode(Type nodeType) + { + if (nodeType == null) + { + return false; + } + if (!nodeType.IsSubclassOf(typeof(STNode))) + { + throw new ArgumentException($"不支持的类型[{nodeType.FullName}] [nodeType]参数值必须为[STNode]子类的类型", nameof(nodeType)); + } + if (nodeType.IsAbstract || nodeType.IsDefined(typeof(ObsoleteAttribute), inherit: false) || m_dic_all_type.ContainsKey(nodeType)) + { + return false; + } + + var attribute = nodeType.GetCustomAttributes(typeof(STNodeAttribute), inherit: true) + .OfType() + .FirstOrDefault(); + if (attribute == null) + { + throw new InvalidOperationException($"类型[{nodeType.FullName}]未被[STNodeAttribute]所标记"); + } + + string path = attribute.Path?.Trim('/', '\\') ?? string.Empty; + CacheNodeMetadata(nodeType); + m_dic_all_type.Add(nodeType, path); + RefreshTree(); + return true; + } + + public int LoadAssembly() + { + int count = 0; + foreach (Assembly assembly in STNodeTypeRegistry.GetAssemblies()) + { + count += AddAssembly(assembly); + } + if (count > 0) + { + RefreshTree(); + } + return count; + } + + public int LoadAssembly(string fileName) + { + Assembly assembly = Assembly.LoadFrom(Path.GetFullPath(fileName)); + STNodeTypeRegistry.LoadAssembly(assembly); + int count = AddAssembly(assembly); + if (count > 0) + { + RefreshTree(); + } + return count; + } + + public void Clear() + { + m_dic_all_type.Clear(); + m_dic_node_title.Clear(); + m_dic_node_color.Clear(); + RefreshTree(); + } + + public bool RemoveNode(Type nodeType) + { + bool removed = m_dic_all_type.Remove(nodeType); + if (removed) + { + m_dic_node_title.Remove(nodeType); + m_dic_node_color.Remove(nodeType); + RefreshTree(); + } + return removed; + } + + private int AddAssembly(Assembly assembly) + { + int count = 0; + foreach (Type nodeType in STNodeTypeRegistry.GetTypes(assembly)) + { + try + { + if (AddNodeWithoutRefresh(nodeType)) + { + count++; + } + } + catch + { + } + } + return count; + } + + private bool AddNodeWithoutRefresh(Type nodeType) + { + if (nodeType == null + || nodeType.IsAbstract + || !nodeType.IsSubclassOf(typeof(STNode)) + || nodeType.IsDefined(typeof(ObsoleteAttribute), inherit: false) + || m_dic_all_type.ContainsKey(nodeType)) + { + return false; + } + + var attribute = nodeType.GetCustomAttributes(typeof(STNodeAttribute), inherit: true) + .OfType() + .FirstOrDefault(); + if (attribute == null) + { + return false; + } + string assemblyName = nodeType.Assembly.GetName().Name ?? "Unknown"; + string path = string.IsNullOrWhiteSpace(attribute.Path) + ? assemblyName + : $"{assemblyName}/{attribute.Path.Trim('/', '\\')}"; + CacheNodeMetadata(nodeType); + m_dic_all_type.Add(nodeType, path); + return true; + } + + private void CacheNodeMetadata(Type nodeType) + { + if (Activator.CreateInstance(nodeType) is not STNode node) + { + throw new InvalidOperationException($"无法创建节点类型[{nodeType.FullName}]"); + } + + m_dic_node_title[nodeType] = string.IsNullOrWhiteSpace(node.Title) ? nodeType.Name : node.Title; + m_dic_node_color[nodeType] = node.TitleColor; + } + + private void OnSearchTextChanged(object sender, TextChangedEventArgs e) + { + m_search_text = m_search_box.Text.Trim(); + m_clear_button.Visibility = m_search_text.Length == 0 ? Visibility.Collapsed : Visibility.Visible; + RefreshTree(); + } + + private void OnClearSearchClick(object sender, RoutedEventArgs e) + { + m_search_box.Clear(); + m_search_box.Focus(); + } + + private void RefreshTree() + { + if (m_tree == null) + { + return; + } + + m_tree.Items.Clear(); + foreach (CatalogNode node in BuildCatalog()) + { + m_tree.Items.Add(CreateTreeItem(node)); + } + } + + private List BuildCatalog() + { + var roots = new List(); + foreach (KeyValuePair entry in m_dic_all_type.Where(item => MatchesSearch(item.Key, item.Value))) + { + List level = roots; + foreach (string segment in entry.Value.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries)) + { + CatalogNode folder = level.FirstOrDefault(item => item.NodeType == null && item.Name == segment); + if (folder == null) + { + folder = new CatalogNode { Name = segment }; + level.Add(folder); + } + level = folder.Children; + } + + string title = m_dic_node_title.TryGetValue(entry.Key, out string cachedTitle) + ? cachedTitle + : entry.Key.Name; + DrawingColor nodeColor = m_dic_node_color.TryGetValue(entry.Key, out DrawingColor cachedColor) + ? cachedColor + : DrawingColor.DarkCyan; + level.Add(new CatalogNode + { + Name = title, + NodeType = entry.Key, + NodeColor = nodeColor + }); + } + + SortCatalog(roots); + return roots; + } + + private static void SortCatalog(List nodes) + { + nodes.Sort((left, right) => + { + if ((left.NodeType == null) != (right.NodeType == null)) + { + return left.NodeType == null ? -1 : 1; + } + return StringComparer.CurrentCultureIgnoreCase.Compare(left.Name, right.Name); + }); + foreach (CatalogNode node in nodes) + { + SortCatalog(node.Children); + } + } + + private TreeViewItem CreateTreeItem(CatalogNode node) + { + var item = new TreeViewItem + { + Header = CreateTreeHeader(node), + Tag = node.NodeType, + IsExpanded = m_search_text.Length > 0, + Foreground = ToBrush(m_fore_color) + }; + foreach (CatalogNode child in node.Children) + { + item.Items.Add(CreateTreeItem(child)); + } + return item; + } + + private Grid CreateTreeHeader(CatalogNode node) + { + var grid = new Grid { MinHeight = 26 }; + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + grid.ColumnDefinitions.Add(new ColumnDefinition()); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var icon = new Border + { + Width = 12, + Height = 12, + Margin = new Thickness(2, 0, 7, 0), + VerticalAlignment = VerticalAlignment.Center, + BorderThickness = new Thickness(1), + BorderBrush = ToBrush(node.NodeType == null + ? DrawingColor.Goldenrod + : m_auto_color ? node.NodeColor : DrawingColor.DarkCyan), + Background = node.NodeType == null ? MediaBrushes.Transparent : MediaBrushes.LightGray + }; + grid.Children.Add(icon); + + string displayName = node.NodeType == null ? Lang.GetOrDefault(node.Name) : node.Name; + var name = new TextBlock + { + Text = displayName, + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + Foreground = ToBrush(m_search_text.Length > 0 + && displayName.IndexOf(m_search_text, StringComparison.CurrentCultureIgnoreCase) >= 0 + ? m_highlight_text_color + : m_fore_color) + }; + Grid.SetColumn(name, 1); + grid.Children.Add(name); + + if (node.NodeType == null && m_show_folder_count) + { + var count = new TextBlock + { + Text = $"[{node.NodeCount}]", + VerticalAlignment = VerticalAlignment.Center, + Foreground = ToBrush(m_folder_count_color), + Margin = new Thickness(8, 0, 4, 0) + }; + Grid.SetColumn(count, 2); + grid.Children.Add(count); + } + else if (node.NodeType != null && m_show_info_button) + { + var info = new Button + { + Content = "ⓘ", + Foreground = ToBrush(m_auto_color ? node.NodeColor : m_info_button_color), + Background = MediaBrushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(5, 0, 5, 0), + Margin = new Thickness(6, 0, 0, 0), + ToolTip = "预览节点和属性" + }; + info.Click += (s, e) => + { + ShowPreview(node.NodeType, info); + e.Handled = true; + }; + Grid.SetColumn(info, 2); + grid.Children.Add(info); + } + return grid; + } + + private bool MatchesSearch(Type type, string path) + { + if (string.IsNullOrWhiteSpace(m_search_text)) + { + return true; + } + if (type.Name.IndexOf(m_search_text, StringComparison.CurrentCultureIgnoreCase) >= 0 + || path.IndexOf(m_search_text, StringComparison.CurrentCultureIgnoreCase) >= 0 + || path.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries) + .Any(segment => Lang.GetOrDefault(segment).IndexOf(m_search_text, StringComparison.CurrentCultureIgnoreCase) >= 0)) + { + return true; + } + return m_dic_node_title.TryGetValue(type, out string title) + && title.IndexOf(m_search_text, StringComparison.CurrentCultureIgnoreCase) >= 0; + } + + private void OnTreeMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + m_drag_start = e.GetPosition(m_tree); + m_drag_type = GetNodeType(e.OriginalSource as DependencyObject); + } + + private void OnTreeMouseMove(object sender, MouseEventArgs e) + { + if (e.LeftButton != MouseButtonState.Pressed || m_drag_type == null) + { + return; + } + WpfPoint point = e.GetPosition(m_tree); + if (Math.Abs(point.X - m_drag_start.X) < SystemParameters.MinimumHorizontalDragDistance + && Math.Abs(point.Y - m_drag_start.Y) < SystemParameters.MinimumVerticalDragDistance) + { + return; + } + + var data = new System.Windows.DataObject(); + data.SetData("STNodeType", m_drag_type); + System.Windows.DragDrop.DoDragDrop(m_tree, data, DragDropEffects.Copy); + m_drag_type = null; + } + + private Type GetNodeType(DependencyObject source) + { + TreeViewItem item = ItemsControl.ContainerFromElement(m_tree, source) as TreeViewItem; + return item?.Tag as Type; + } + + private void ShowPreview(Type nodeType, UIElement placementTarget) + { + try + { + _editor.Nodes.Clear(); + if (Activator.CreateInstance(nodeType) is not STNode node) + { + return; + } + node.Left = 30; + node.Top = 30; + _editor.Nodes.Add(node); + _editor.SetActiveNode(node); + _editor.FitCanvasToNodes(); + _property_grid.SetNode(node); + m_preview_popup.PlacementTarget = placementTarget; + m_preview_popup.Placement = m_info_panel_is_left_layout + ? PlacementMode.Left + : PlacementMode.Right; + m_preview_popup.IsOpen = true; + } + catch + { + m_preview_popup.IsOpen = false; + } + } + + private void ApplyColors() + { + Background = ToBrush(m_back_color); + Foreground = ToBrush(m_fore_color); + m_search_box.Background = ToBrush(m_text_box_color); + m_search_box.Foreground = ToBrush(m_fore_color); + m_clear_button.Foreground = ToBrush(m_fore_color); + m_tree.Background = ToBrush(m_item_back_color); + m_tree.Foreground = ToBrush(m_fore_color); + if (m_preview_popup.Child is Border preview) + { + preview.Background = ToBrush(m_back_color); + } + } + + private void ApplyFont() + { + MediaFontFamily family = new MediaFontFamily(m_font.Name); + double size = Math.Max(1d, m_font.SizeInPoints * 96d / 72d); + FontFamily = family; + FontSize = size; + m_search_box.FontFamily = family; + m_search_box.FontSize = size; + m_tree.FontFamily = family; + m_tree.FontSize = size; + } + + private static SolidColorBrush ToBrush(DrawingColor color) + { + var brush = new SolidColorBrush(MediaColor.FromArgb(color.A, color.R, color.G, color.B)); + brush.Freeze(); + return brush; + } + + public void Dispose() + { + if (m_disposed) + { + return; + } + m_disposed = true; + m_preview_popup.IsOpen = false; + m_search_box.TextChanged -= OnSearchTextChanged; + m_clear_button.Click -= OnClearSearchClick; + m_tree.PreviewMouseLeftButtonDown -= OnTreeMouseLeftButtonDown; + m_tree.PreviewMouseMove -= OnTreeMouseMove; + _editor.Dispose(); + _property_grid.Dispose(); + m_font?.Dispose(); + m_font = null; + GC.SuppressFinalize(this); + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeTypeRegistry.cs b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeTypeRegistry.cs new file mode 100644 index 0000000..da06234 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/NodeEditor/STNodeTypeRegistry.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace ST.Library.UI.NodeEditor; + +internal static class STNodeTypeRegistry +{ + private const int InitializationNotStarted = 0; + private const int InitializationInProgress = 1; + private const int InitializationCompleted = 2; + private static readonly Type NodeType = typeof(STNode); + private static readonly string NodeAssemblyName = NodeType.Assembly.GetName().Name; + private static readonly object InitializationSyncRoot = new object(); + private static readonly object SyncRoot = new object(); + private static readonly ConcurrentQueue PendingAssemblies = new ConcurrentQueue(); + private static readonly HashSet NodeTypes = new HashSet(); + private static readonly Dictionary GuidTypes = new Dictionary(); + private static readonly Dictionary ModelTypes = new Dictionary(); + private static readonly HashSet AmbiguousModels = new HashSet(); + private static readonly Dictionary> AssemblyTypes = new Dictionary>(); + private static int _initializationState; + private static bool _assemblyLoadSubscribed; + [ThreadStatic] + private static bool _isScanningLoadedAssemblies; + + public static void Initialize() + { + if (System.Threading.Volatile.Read(ref _initializationState) == InitializationCompleted) + { + return; + } + + lock (InitializationSyncRoot) + { + if (!_assemblyLoadSubscribed) + { + AppDomain.CurrentDomain.AssemblyLoad += CurrentDomain_AssemblyLoad; + _assemblyLoadSubscribed = true; + } + } + + int previousState = System.Threading.Interlocked.CompareExchange( + ref _initializationState, + InitializationInProgress, + InitializationNotStarted); + if (previousState == InitializationCompleted) + { + return; + } + if (previousState == InitializationInProgress) + { + ScanLoadedAssemblies(); + return; + } + + try + { + ScanLoadedAssemblies(); + System.Threading.Volatile.Write(ref _initializationState, InitializationCompleted); + } + catch + { + System.Threading.Volatile.Write(ref _initializationState, InitializationNotStarted); + throw; + } + } + + private static void ScanLoadedAssemblies() + { + // Reflection may load more assemblies and re-enter Initialize on the same thread. + if (_isScanningLoadedAssemblies) + { + return; + } + + _isScanningLoadedAssemblies = true; + try + { + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + RegisterAssemblyCore(assembly); + } + RegisterPendingAssemblies(); + } + finally + { + _isScanningLoadedAssemblies = false; + } + } + + public static int LoadAssemblies(IEnumerable assemblies) + { + Initialize(); + int count = 0; + foreach (Assembly assembly in assemblies) + { + if (RegisterAssemblyCore(assembly)) + { + count++; + } + } + RegisterPendingAssemblies(); + return count; + } + + public static bool LoadAssembly(string strFile) + { + Assembly assembly = Assembly.LoadFrom(strFile); + return LoadAssembly(assembly); + } + + public static bool LoadAssembly(Assembly assembly) + { + if (assembly == null) + { + return false; + } + Initialize(); + bool containsNodeTypes = RegisterAssemblyCore(assembly); + RegisterPendingAssemblies(); + return containsNodeTypes; + } + + public static Type[] GetTypes() + { + Initialize(); + RegisterPendingAssemblies(); + lock (SyncRoot) + { + return NodeTypes.ToArray(); + } + } + + public static Type[] GetTypes(Assembly assembly) + { + Initialize(); + RegisterPendingAssemblies(); + lock (SyncRoot) + { + if (assembly != null && AssemblyTypes.TryGetValue(assembly, out List types)) + { + return types.ToArray(); + } + return Array.Empty(); + } + } + + public static Assembly[] GetAssemblies() + { + Initialize(); + RegisterPendingAssemblies(); + lock (SyncRoot) + { + return AssemblyTypes + .Where(pair => pair.Value.Count > 0) + .Select(pair => pair.Key) + .ToArray(); + } + } + + public static bool TryGetNodeType(string guid, string model, out Type type) + { + Initialize(); + RegisterPendingAssemblies(); + lock (SyncRoot) + { + if (!string.IsNullOrEmpty(guid) && GuidTypes.TryGetValue(guid, out type)) + { + return true; + } + if (!string.IsNullOrEmpty(model) && ModelTypes.TryGetValue(model, out type)) + { + return true; + } + if (TryGetNodeTypeByLegacySuffix(model, out type)) + { + return true; + } + type = null; + return false; + } + } + + public static string GetModelByType(Type type) + { + return $"{type.Module.Name}|{type.FullName}"; + } + + private static bool TryGetNodeTypeByLegacySuffix(string model, out Type type) + { + type = null; + if (string.IsNullOrEmpty(model)) + { + return false; + } + + int moduleSeparator = model.IndexOf('|'); + if (moduleSeparator <= 0 || moduleSeparator >= model.Length - 1) + { + return false; + } + string legacyTypeName = model.Substring(moduleSeparator + 1); + int typeNameSeparator = Math.Max(legacyTypeName.LastIndexOf('.'), legacyTypeName.LastIndexOf('+')); + if (typeNameSeparator < 0 || typeNameSeparator >= legacyTypeName.Length - 1) + { + return false; + } + + string currentModel = string.Concat(model.AsSpan(0, moduleSeparator + 1), legacyTypeName.AsSpan(typeNameSeparator + 1)); + return ModelTypes.TryGetValue(currentModel, out type); + } + + private static void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args) + { + // AssemblyLoad runs inside runtime loader coordination; never reflect or wait here. + PendingAssemblies.Enqueue(args.LoadedAssembly); + } + + private static void RegisterPendingAssemblies() + { + while (PendingAssemblies.TryDequeue(out Assembly assembly)) + { + RegisterAssemblyCore(assembly); + } + } + + private static bool RegisterAssemblyCore(Assembly assembly) + { + if (!ShouldScanAssembly(assembly)) + { + return false; + } + + lock (SyncRoot) + { + if (AssemblyTypes.TryGetValue(assembly, out List existingTypes)) + { + return existingTypes.Count > 0; + } + } + + NodeTypeRegistration[] registrations = GetLoadableTypes(assembly) + .Where(IsNodeType) + .Select(type => new NodeTypeRegistration( + type, + type.GUID.ToString(), + GetModelByType(type), + $"{type.Module.Name}|{type.Name}")) + .ToArray(); + + lock (SyncRoot) + { + if (AssemblyTypes.TryGetValue(assembly, out List existingTypes)) + { + return existingTypes.Count > 0; + } + + List registeredTypes = new List(); + AssemblyTypes.Add(assembly, registeredTypes); + foreach (NodeTypeRegistration registration in registrations) + { + if (!NodeTypes.Add(registration.Type)) + { + continue; + } + + registeredTypes.Add(registration.Type); + if (!GuidTypes.ContainsKey(registration.Guid)) + { + GuidTypes.Add(registration.Guid, registration.Type); + } + + RegisterModelKey(registration.Model, registration.Type); + RegisterModelKey(registration.ShortModel, registration.Type); + } + return registeredTypes.Count > 0; + } + } + + private static void RegisterModelKey(string model, Type type) + { + if (AmbiguousModels.Contains(model)) + { + return; + } + if (ModelTypes.TryGetValue(model, out Type existingType) && existingType != type) + { + ModelTypes.Remove(model); + AmbiguousModels.Add(model); + return; + } + ModelTypes[model] = type; + } + + private static bool ShouldScanAssembly(Assembly assembly) + { + if (assembly == null || assembly.IsDynamic) + { + return false; + } + if (assembly == NodeType.Assembly) + { + return true; + } + try + { + foreach (AssemblyName referencedAssembly in assembly.GetReferencedAssemblies()) + { + if (string.Equals(referencedAssembly.Name, NodeAssemblyName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + catch + { + } + return false; + } + + private static Type[] GetLoadableTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where(type => type != null).ToArray(); + } + catch + { + return Array.Empty(); + } + } + + private static bool IsNodeType(Type type) + { + return type != null + && type.IsClass + && !type.IsAbstract + && NodeType.IsAssignableFrom(type); + } + + private sealed class NodeTypeRegistration + { + public Type Type { get; } + public string Guid { get; } + public string Model { get; } + public string ShortModel { get; } + + public NodeTypeRegistration(Type type, string guid, string model, string shortModel) + { + Type = type; + Guid = guid; + Model = model; + ShortModel = shortModel; + } + } +} diff --git a/NativeWpf/ST.Library.UI.WPF/Properties/AssemblyInfo.cs b/NativeWpf/ST.Library.UI.WPF/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..cca330d --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +[assembly: AssemblyTitle("ST.Library.UI.WPF")] +[assembly: AssemblyDescription("Native WPF controls for STNodeEditor")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("DebugST")] +[assembly: AssemblyProduct("ST.Library.UI.WPF")] +[assembly: AssemblyCopyright("Copyright © Crystal_lz")] +[assembly: AssemblyTrademark("")] +[assembly: ComVisible(false)] +[assembly: Guid("5a7c4557-5eb1-435a-84d2-49cd67d1ac10")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: NeutralResourcesLanguage("zh-Hans")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: SupportedOSPlatform("windows6.1")] diff --git a/NativeWpf/ST.Library.UI.WPF/README.md b/NativeWpf/ST.Library.UI.WPF/README.md new file mode 100644 index 0000000..9bf0ff5 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/README.md @@ -0,0 +1,37 @@ +# ST.Library.UI.WPF + +Native WPF controls for STNodeEditor. The editor is a WPF `Control`; it does +not host a WinForms control and has no third-party rendering dependency. + +This project is intentionally separate from `ST.Library.UI`: the existing project remains the WinForms implementation, while WPF applications reference this assembly instead. Both assemblies expose the `ST.Library.UI.NodeEditor` namespace so existing node source can keep the same node, option, drawing, and serialization contracts. + +A consumer must reference only one platform implementation in a single application. + +## Usage + +Reference `ST.Library.UI.WPF.csproj` from a WPF application and place the +controls directly in XAML: + +```xml + + + + + +``` + +`STNodeEditor`, `STNodeTreeView`, `STNodePropertyGrid`, and the historical +`STNodeEditorPannel` type are all native WPF elements. Custom node drawing +continues to use the original `DrawingTools.Graphics` contract. The WPF +control copies that GDI-rendered surface into a DPI-aware `WriteableBitmap`. + +Custom property descriptors keep the upstream drawing and interaction model. +Derived descriptors render through a native WPF presenter, receive their logical +property-grid rectangles, and receive neutral mouse enter/move/leave/down/up/ +click events. Default editing is provided by native WPF text and selection +controls. + +Mouse callbacks use `STNodeMouseEventArgs` and `STMouseButtons` so the WPF +assembly has no dependency on WinForms assemblies. Node construction keeps +the upstream lifecycle: `OnCreate` runs once during construction, and repeated +compatibility calls to `Create()` are safe and idempotent. diff --git a/NativeWpf/ST.Library.UI.WPF/ST.Library.UI.WPF.csproj b/NativeWpf/ST.Library.UI.WPF/ST.Library.UI.WPF.csproj new file mode 100644 index 0000000..16d4438 --- /dev/null +++ b/NativeWpf/ST.Library.UI.WPF/ST.Library.UI.WPF.csproj @@ -0,0 +1,15 @@ + + + net8.0-windows;net10.0-windows + true + ST.Library.UI.WPF + ST.Library.UI + false + disable + disable + 11.0 + + + + + diff --git a/NativeWpf/STNodeEditor.Wpf.sln b/NativeWpf/STNodeEditor.Wpf.sln new file mode 100644 index 0000000..a8befa9 --- /dev/null +++ b/NativeWpf/STNodeEditor.Wpf.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfNodeEditorDemo", "WpfNodeEditorDemo\WpfNodeEditorDemo.csproj", "{9CED6F5F-183B-4BB8-9355-86E292415716}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ST.Library.UI.WPF", "ST.Library.UI.WPF\ST.Library.UI.WPF.csproj", "{2C7997CB-F943-49BD-9688-BFA7C7BA1B22}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Debug|x64.ActiveCfg = Debug|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Debug|x64.Build.0 = Debug|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Debug|x86.ActiveCfg = Debug|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Debug|x86.Build.0 = Debug|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Release|Any CPU.Build.0 = Release|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Release|x64.ActiveCfg = Release|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Release|x64.Build.0 = Release|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Release|x86.ActiveCfg = Release|Any CPU + {2C7997CB-F943-49BD-9688-BFA7C7BA1B22}.Release|x86.Build.0 = Release|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Debug|x64.ActiveCfg = Debug|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Debug|x64.Build.0 = Debug|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Debug|x86.ActiveCfg = Debug|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Debug|x86.Build.0 = Debug|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Release|Any CPU.Build.0 = Release|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Release|x64.ActiveCfg = Release|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Release|x64.Build.0 = Release|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Release|x86.ActiveCfg = Release|Any CPU + {9CED6F5F-183B-4BB8-9355-86E292415716}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/NativeWpf/WpfNodeEditorDemo/App.xaml b/NativeWpf/WpfNodeEditorDemo/App.xaml new file mode 100644 index 0000000..3723930 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/App.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/NativeWpf/WpfNodeEditorDemo/App.xaml.cs b/NativeWpf/WpfNodeEditorDemo/App.xaml.cs new file mode 100644 index 0000000..286e369 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/App.xaml.cs @@ -0,0 +1,14 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace WpfNodeEditorDemo +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } + +} diff --git a/NativeWpf/WpfNodeEditorDemo/AssemblyInfo.cs b/NativeWpf/WpfNodeEditorDemo/AssemblyInfo.cs new file mode 100644 index 0000000..a8dc727 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/NativeWpf/WpfNodeEditorDemo/AttrTestNode.cs b/NativeWpf/WpfNodeEditorDemo/AttrTestNode.cs new file mode 100644 index 0000000..414e290 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/AttrTestNode.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; +using WpfNodeEditorDemo; + +namespace WinNodeEditorDemo +{ + // Keeps the original demo property's enum shape without taking a WinForms dependency. + public enum FormBorderStyle + { + None, + FixedSingle, + Fixed3D, + FixedDialog, + Sizable, + FixedToolWindow, + SizableToolWindow + } + + [STNode("/", "Crystal_lz", "2212233137@qq.com", "www.st233.com", "关于此节点的描述信息\r\n此类为\r\nSTNodeAttribute\r\nSTNodePropertyAttribute\r\n效果演示类")] + public class AttrTestNode : STNode + { + //因为属性编辑器默认并不支持Color类型数据 所以这里重写一个描述器并指定 + [STNodeProperty("颜色", "颜色信息", DescriptorType = typeof(DescriptorForColor))] + public Color Color { get; set; } + + [STNodeProperty("整型数组", "整型数组测试")] + public int[] IntArr { get; set; } + + [STNodeProperty("布尔", "布尔类型测试")] + public bool Bool { get; set; } + + [STNodeProperty("字符串", "字符串类型测试")] + public string String { get; set; } + + [STNodeProperty("整型", "整型测试")] + public int Int { get; set; } + + [STNodeProperty("浮点数", "浮点数类型测试")] + public float Float { get; set; } + + [STNodeProperty("枚举值", "枚举类型测试 -> FormBorderStyle")] + public FormBorderStyle STYLE { get; set; } + + public AttrTestNode() { + this.String = "string"; + IntArr = new int[] { 10, 20 }; + base.InputOptions.Add("string", typeof(string), false); + base.OutputOptions.Add("string", typeof(string), false); + this.Title = "AttrTestNode"; + this.TitleColor = Color.FromArgb(200, Color.Goldenrod); + } + /// + /// 此方法为魔术方法(Magic function) + /// 若存在 static void ShowHelpInfo(string) 且此类被STNodeAttribute标记 + /// 则此方法将作为属性编辑器上 查看帮助 功能 + /// + /// 此类所在的模块所在的文件路径 + public static void ShowHelpInfo(string strFileName) { + System.Windows.MessageBox.Show("this is -> ShowHelpInfo(string);\r\n" + strFileName); + } + + protected override void OnOwnerChanged() { + base.OnOwnerChanged(); + if (this.Owner == null) return; + this.Owner.SetTypeColor(typeof(string), Color.Goldenrod); + } + } + /// + /// 因为属性编辑器默认并不支持Color类型数据 所以这里重写一个描述器 + /// + public class DescriptorForColor : STNodePropertyDescriptor + { + private Rectangle m_rect;//此区域用作 属性窗口上绘制颜色预览 + //当此属性在属性窗口中被确定位置时候发生 + protected override void OnSetItemLocation() { + base.OnSetItemLocation(); + Rectangle rect = base.RectangleR; + m_rect = new Rectangle(rect.Right - 25, rect.Top + 5, 19, 12); + } + //将属性值转换为字符串 属性窗口值绘制时将采用此字符串 + protected override string GetStringFromValue() { + Color clr = (Color)this.GetValue(null); + return clr.A + "," + clr.R + "," + clr.G + "," + clr.B; + } + //将属性窗口中输入的字符串转化为Color属性 当属性窗口中用户确认输入时调用 + protected override object GetValueFromString(string strText) { + string[] strClr = strText.Split(','); + return Color.FromArgb( + int.Parse(strClr[0]), //A + int.Parse(strClr[1]), //R + int.Parse(strClr[2]), //G + int.Parse(strClr[3])); //B + } + //绘制属性窗口值区域时候调用 + protected override void OnDrawValueRectangle(DrawingTools dt) { + base.OnDrawValueRectangle(dt);//先采用默认的绘制 并再绘制颜色预览 + dt.SolidBrush.Color = (Color)this.GetValue(null); + dt.Graphics.FillRectangle(dt.SolidBrush, m_rect);//填充颜色 + dt.Graphics.DrawRectangle(Pens.Black, m_rect); //绘制边框 + } + + protected override void OnMouseClick(STNodeMouseEventArgs e) { + //如果用户点击在 颜色预览区域 则弹出系统颜色对话框 + if (m_rect.Contains(e.Location)) { + Color current = (Color)this.GetValue(null); + if (!WpfColorPicker.TryPick(current, out Color selected)) return; + this.SetValue(selected, null); + this.Invalidate(); + return; + } + //否则其他区域将采用默认处理方式 弹出字符串输入框 + base.OnMouseClick(e); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/Blender/BlenderMixColorNode.cs b/NativeWpf/WpfNodeEditorDemo/Blender/BlenderMixColorNode.cs new file mode 100644 index 0000000..b7c8f45 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/Blender/BlenderMixColorNode.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; + +namespace WinNodeEditorDemo.Blender +{ + /// + /// 此类仅仅是演示 并不包含颜色混合功能 + /// + [STNode("/Blender/", "Crystal_lz", "2212233137@qq.com", "st233.com", "this is blender mixrgb node")] + public class BlenderMixColorNode : STNode + { + private ColorMixType _MixType; + [STNodeProperty("MixType","This is MixType")] + public ColorMixType MixType { + get { return _MixType; } + set { + _MixType = value; + m_ctrl_select.Enum = value; //当属性被赋值后 对应控件状态值也应当被修改 + } + } + + private bool _Clamp; + [STNodeProperty("Clamp","This is Clamp")] + public bool Clamp { + get { return _Clamp; } + set { _Clamp = value; m_ctrl_checkbox.Checked = value; } + } + + private int _Fac = 50; + [STNodeProperty("Fac", "This is Fac")] + public int Fac { + get { return _Fac; } + set { + if (value < 0) value = 0; + if (value > 100) value = 100; + _Fac = value; m_ctrl_progess.Value = value; + } + } + + private Color _Color1 = Color.LightGray;//默认的DescriptorType不支持颜色的显示 需要扩展 + [STNodeProperty("Color1", "This is color1", DescriptorType = typeof(WinNodeEditorDemo.DescriptorForColor))] + public Color Color1 { + get { return _Color1; } + set { _Color1 = value; m_ctrl_btn_1.BackColor = value; } + } + + private Color _Color2 = Color.LightGray; + [STNodeProperty("Color2", "This is color2", DescriptorType = typeof(WinNodeEditorDemo.DescriptorForColor))] + public Color Color2 { + get { return _Color2; } + set { _Color2 = value; m_ctrl_btn_2.BackColor = value; } + } + + public enum ColorMixType { + Mix, + Value, + Color, + Hue, + Add, + Subtract + } + + private STNodeSelectEnumBox m_ctrl_select; //自定义控件 + private STNodeProgress m_ctrl_progess; + private STNodeCheckBox m_ctrl_checkbox; + private STNodeColorButton m_ctrl_btn_1; + private STNodeColorButton m_ctrl_btn_2; + + protected override void OnCreate() { + base.OnCreate(); + this.TitleColor = Color.FromArgb(200, Color.DarkKhaki); + this.Title = "MixRGB"; + this.AutoSize = false; + this.Size = new Size(140, 142); + + this.OutputOptions.Add("Color", typeof(Color), true); + + this.InputOptions.Add(STNodeOption.Empty); //空白节点 仅站位 不参与绘制与事件触发 + this.InputOptions.Add(STNodeOption.Empty); + this.InputOptions.Add(STNodeOption.Empty); + this.InputOptions.Add("", typeof(float), true); + this.InputOptions.Add("Color1", typeof(Color), true); + this.InputOptions.Add("Color2", typeof(Color), true); + + m_ctrl_progess = new STNodeProgress(); //创建控件并添加到节点中 + m_ctrl_progess.Text = "Fac"; + m_ctrl_progess.DisplayRectangle = new Rectangle(10, 61, 120, 18); + m_ctrl_progess.ValueChanged += (s, e) => this._Fac = m_ctrl_progess.Value; + this.Controls.Add(m_ctrl_progess); + + m_ctrl_checkbox = new STNodeCheckBox(); + m_ctrl_checkbox.Text = "Clamp"; + m_ctrl_checkbox.DisplayRectangle = new Rectangle(10, 40, 120, 20); + m_ctrl_checkbox.ValueChanged += (s, e) => this._Clamp = m_ctrl_checkbox.Checked; + this.Controls.Add(m_ctrl_checkbox); + + m_ctrl_btn_1 = new STNodeColorButton(); + m_ctrl_btn_1.Text = ""; + m_ctrl_btn_1.BackColor = this._Color1; + m_ctrl_btn_1.DisplayRectangle = new Rectangle(80, 82, 50, 16); + m_ctrl_btn_1.ValueChanged += (s, e) => this._Color1 = m_ctrl_btn_1.BackColor; + this.Controls.Add(m_ctrl_btn_1); + + m_ctrl_btn_2 = new STNodeColorButton(); + m_ctrl_btn_2.Text = ""; + m_ctrl_btn_2.BackColor = this._Color2; + m_ctrl_btn_2.DisplayRectangle = new Rectangle(80, 102, 50, 16); + m_ctrl_btn_2.ValueChanged += (s, e) => this._Color2 = m_ctrl_btn_2.BackColor; + this.Controls.Add(m_ctrl_btn_2); + + m_ctrl_select = new STNodeSelectEnumBox(); + m_ctrl_select.DisplayRectangle = new Rectangle(10, 21, 120, 18); + m_ctrl_select.Enum = this._MixType; + m_ctrl_select.ValueChanged += (s, e) => this._MixType = (ColorMixType)m_ctrl_select.Enum; + this.Controls.Add(m_ctrl_select); + } + + protected override void OnOwnerChanged() { //当控件被添加时候 向编辑器提交自己的数据类型希望显示的颜色 + base.OnOwnerChanged(); + if (this.Owner == null) return; + this.Owner.SetTypeColor(typeof(float), Color.Gray); + this.Owner.SetTypeColor(typeof(Color), Color.Yellow); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/Blender/FrmEnumSelect.cs b/NativeWpf/WpfNodeEditorDemo/Blender/FrmEnumSelect.cs new file mode 100644 index 0000000..7120bc2 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/Blender/FrmEnumSelect.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using DrawingPoint = System.Drawing.Point; +using WpfPoint = System.Windows.Point; + +namespace WinNodeEditorDemo.Blender +{ + /// + /// Native WPF popup used by the MixRGB node's enum control. + /// + public class FrmEnumSelect : Window + { + private readonly DrawingPoint _screenPoint; + private readonly ListBox _listBox; + private bool _isClosing; + + public Enum Enum { get; private set; } + + public FrmEnumSelect(Enum value, DrawingPoint screenPoint, int width, float scale) + { + Enum = value ?? throw new ArgumentNullException(nameof(value)); + _screenPoint = screenPoint; + + WindowStyle = WindowStyle.None; + ResizeMode = ResizeMode.NoResize; + ShowInTaskbar = false; + WindowStartupLocation = WindowStartupLocation.Manual; + Background = new SolidColorBrush(Color.FromRgb(34, 34, 34)); + Width = Math.Max(80, width * scale); + + var values = new List(); + foreach (object item in System.Enum.GetValues(value.GetType())) + { + values.Add(item); + } + Height = Math.Max(24, values.Count * 24 * scale); + + Window? owner = Application.Current?.MainWindow; + if (owner != null && !ReferenceEquals(owner, this)) + { + Owner = owner; + } + + _listBox = new ListBox + { + Background = Background, + Foreground = Brushes.White, + BorderThickness = new Thickness(0), + Padding = new Thickness(0), + FontSize = Math.Max(11, 12 * scale) + }; + foreach (object item in values) + { + _listBox.Items.Add(item); + } + _listBox.SelectedItem = value; + _listBox.SelectionChanged += OnSelectionChanged; + Content = _listBox; + + Loaded += OnLoaded; + Deactivated += OnDeactivated; + Closing += (_, _) => + { + _isClosing = true; + Deactivated -= OnDeactivated; + }; + PreviewKeyDown += OnPreviewKeyDown; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + Window? owner = Owner ?? Application.Current?.MainWindow; + if (owner != null && !ReferenceEquals(owner, this)) + { + PresentationSource? source = PresentationSource.FromVisual(owner); + WpfPoint screenPoint = new WpfPoint(_screenPoint.X, _screenPoint.Y); + WpfPoint screenDip = source?.CompositionTarget != null + ? source.CompositionTarget.TransformFromDevice.Transform(screenPoint) + : new WpfPoint( + screenPoint.X / VisualTreeHelper.GetDpi(owner).DpiScaleX, + screenPoint.Y / VisualTreeHelper.GetDpi(owner).DpiScaleY); + Left = screenDip.X; + Top = screenDip.Y; + } + else + { + Left = _screenPoint.X; + Top = _screenPoint.Y; + } + + _listBox.Focus(); + _listBox.ScrollIntoView(_listBox.SelectedItem); + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (!IsLoaded || _listBox.SelectedItem is not Enum selected) + { + return; + } + + Enum = selected; + DialogResult = true; + } + + private void OnDeactivated(object? sender, EventArgs e) + { + if (IsVisible && DialogResult != true) + { + CloseOnce(); + } + } + + private void OnPreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key != Key.Escape) + { + return; + } + + e.Handled = true; + CloseOnce(); + } + + private void CloseOnce() + { + if (_isClosing) + { + return; + } + + _isClosing = true; + Deactivated -= OnDeactivated; + Close(); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/Blender/STNodeCheckBox.cs b/NativeWpf/WpfNodeEditorDemo/Blender/STNodeCheckBox.cs new file mode 100644 index 0000000..01b7a26 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/Blender/STNodeCheckBox.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; +using ST.Library.UI.NodeEditor; + +namespace WinNodeEditorDemo.Blender +{ + /// + /// 此类仅演示 作为MixRGB节点的复选框控件 + /// + public class STNodeCheckBox : STNodeControl + { + private bool _Checked; + + public bool Checked { + get { return _Checked; } + set { + _Checked = value; + this.Invalidate(); + } + } + + public event EventHandler ValueChanged; + protected virtual void OnValueChanged(EventArgs e) { + if (this.ValueChanged != null) this.ValueChanged(this, e); + } + + protected override void OnMouseClick(STNodeMouseEventArgs e) { + base.OnMouseClick(e); + this.Checked = !this.Checked; + this.OnValueChanged(new EventArgs()); + } + + protected override void OnPaint(DrawingTools dt) { + //base.OnPaint(dt); + Graphics g = dt.Graphics; + g.FillRectangle(Brushes.Gray, 0, 5, 10, 10); + g.DrawString(this.Text, this.Font, Brushes.LightGray, new Rectangle(15, 0, this.Width - 20, 20), m_sf); + if (this.Checked) { + g.FillRectangle(Brushes.Black, 2, 7, 6, 6); + } + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/Blender/STNodeColorButton.cs b/NativeWpf/WpfNodeEditorDemo/Blender/STNodeColorButton.cs new file mode 100644 index 0000000..5f3c705 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/Blender/STNodeColorButton.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; +using ST.Library.UI.NodeEditor; +using WpfNodeEditorDemo; + +namespace WinNodeEditorDemo.Blender +{ + /// + /// 此类仅演示 作为MixRGB节点的颜色选择按钮 + /// + public class STNodeColorButton : STNodeControl + { + public event EventHandler ValueChanged; + protected virtual void OnValueChanged(EventArgs e) { + if (this.ValueChanged != null) this.ValueChanged(this, e); + } + + protected override void OnMouseClick(STNodeMouseEventArgs e) { + base.OnMouseClick(e); + if (!WpfColorPicker.TryPick(this.BackColor, out Color selected)) return; + this.BackColor = selected; + this.OnValueChanged(new EventArgs()); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/Blender/STNodeProgress.cs b/NativeWpf/WpfNodeEditorDemo/Blender/STNodeProgress.cs new file mode 100644 index 0000000..2e44244 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/Blender/STNodeProgress.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; + +namespace WinNodeEditorDemo.Blender +{ + /// + /// 此类仅演示 作为MixRGB节点的进度条控件 + /// + public class STNodeProgress : STNodeControl + { + private int _Value = 50; + + public int Value { + get { return _Value; } + set { + _Value = value; + this.Invalidate(); + } + } + + private bool m_bMouseDown; + + public event EventHandler ValueChanged; + protected virtual void OnValueChanged(EventArgs e) { + if (this.ValueChanged != null) this.ValueChanged(this, e); + } + + protected override void OnPaint(DrawingTools dt) { + base.OnPaint(dt); + Graphics g = dt.Graphics; + g.FillRectangle(Brushes.Gray, this.ClientRectangle); + g.FillRectangle(Brushes.CornflowerBlue, 0, 0, (int)((float)this._Value / 100 * this.Width), this.Height); + m_sf.Alignment = StringAlignment.Near; + g.DrawString(this.Text, this.Font, Brushes.White, this.ClientRectangle, m_sf); + m_sf.Alignment = StringAlignment.Far; + g.DrawString(((float)this._Value / 100).ToString("F2"), this.Font, Brushes.White, this.ClientRectangle, m_sf); + + } + + protected override void OnMouseDown(STNodeMouseEventArgs e) { + base.OnMouseDown(e); + m_bMouseDown = true; + } + + protected override void OnMouseUp(STNodeMouseEventArgs e) { + base.OnMouseUp(e); + m_bMouseDown = false; + } + + protected override void OnMouseMove(STNodeMouseEventArgs e) { + base.OnMouseMove(e); + if (!m_bMouseDown) return; + int v = (int)((float)e.X / this.Width * 100); + if (v < 0) v = 0; + if (v > 100) v = 100; + this._Value = v; + this.OnValueChanged(new EventArgs()); + this.Invalidate(); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/Blender/STNodeSelectBox.cs b/NativeWpf/WpfNodeEditorDemo/Blender/STNodeSelectBox.cs new file mode 100644 index 0000000..74e35c3 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/Blender/STNodeSelectBox.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; +using ST.Library.UI.NodeEditor; + +namespace WinNodeEditorDemo.Blender +{ + /// + /// 此类仅演示 作为MixRGB节点的下拉框控件 + /// + public class STNodeSelectEnumBox : STNodeControl + { + private Enum _Enum; + public Enum Enum { + get { return _Enum; } + set { + _Enum = value; + this.Invalidate(); + } + } + + public event EventHandler ValueChanged; + protected virtual void OnValueChanged(EventArgs e) { + if (this.ValueChanged != null) this.ValueChanged(this, e); + } + + protected override void OnPaint(DrawingTools dt) { + Graphics g = dt.Graphics; + dt.SolidBrush.Color = Color.FromArgb(80, 0, 0, 0); + g.FillRectangle(dt.SolidBrush, this.ClientRectangle); + m_sf.Alignment = StringAlignment.Near; + g.DrawString(this.Enum.ToString(), this.Font, Brushes.White, this.ClientRectangle, m_sf); + g.FillPolygon(Brushes.Gray, new Point[]{ + new Point(this.Right - 25, 7), + new Point(this.Right - 15, 7), + new Point(this.Right - 20, 12) + }); + } + + protected override void OnMouseClick(STNodeMouseEventArgs e) { + base.OnMouseClick(e); + Point pt = new Point(this.Left + this.Owner.Left, this.Top + this.Owner.Top + this.Owner.TitleHeight); + pt = this.Owner.Owner.CanvasToControl(pt); + pt = this.Owner.Owner.PointToScreen(pt); + FrmEnumSelect frm = new FrmEnumSelect(this.Enum, pt, this.Width, this.Owner.Owner.CanvasScale); + var v = frm.ShowDialog(); + if (v != true) return; + this.Enum = frm.Enum; + this.OnValueChanged(new EventArgs()); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/CalcNode.cs b/NativeWpf/WpfNodeEditorDemo/CalcNode.cs new file mode 100644 index 0000000..7e2b63d --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/CalcNode.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; + +namespace WinNodeEditorDemo +{ + /// + /// 此节点仅演示UI自定义以及控件 并不包含功能 + /// + [STNode("/", "DebugST", "2212233137@qq.com", "st233.com", "此节点仅演示UI自定义以及控件,并不包含功能.")] + public class CalcNode : STNode + { + private StringFormat m_f; + + protected override void OnCreate() { + base.OnCreate(); + m_sf = new StringFormat(); + m_sf.LineAlignment = StringAlignment.Center; + this.Title = "Calculator"; + this.AutoSize = false; //注意需要先设置AutoSize=false 才能够进行大小设置 + this.Size = new Size(218, 308); + + var ctrl = new STNodeControl(); + ctrl.Text = ""; //此控件为显示屏幕 + ctrl.Location = new Point(13, 31); + ctrl.Size = new Size(190, 50); + this.Controls.Add(ctrl); + + ctrl.Paint += (s, e) => { + m_sf.Alignment = StringAlignment.Far; + STNodeControl c = s as STNodeControl; + Graphics g = e.DrawingTools.Graphics; + g.DrawString("0", ctrl.Font, Brushes.White, c.ClientRectangle, m_sf); + }; + + string[] strs = { //按钮文本 + "MC", "MR", "MS", "M+", + "M-", "←", "CE", "C", "+", "√", + "7", "8", "9", "/", "%", + "4", "5", "6", "*", "1/x", + "1", "2", "3", "-", "=", + "0", " ", ".", "+" }; + Point p = new Point(13, 86); + for (int i = 0; i < strs.Length; i++) { + if (strs[i] == " ") continue; + ctrl = new STNodeControl(); + ctrl.Text = strs[i]; + ctrl.Size = new Size(34, 27); + ctrl.Left = 13 + (i % 5) * 39; + ctrl.Top = 86 + (i / 5) * 32; + if (ctrl.Text == "=") ctrl.Height = 59; + if (ctrl.Text == "0") ctrl.Width = 73; + this.Controls.Add(ctrl); + if (i == 8) ctrl.Paint += (s, e) => { + m_sf.Alignment = StringAlignment.Center; + STNodeControl c = s as STNodeControl; + Graphics g = e.DrawingTools.Graphics; + g.DrawString("_", ctrl.Font, Brushes.White, c.ClientRectangle, m_sf); + }; + ctrl.MouseClick += (s, e) => System.Windows.MessageBox.Show(((STNodeControl)s).Text); + } + + this.OutputOptions.Add("Result", typeof(int), false); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/EmptyOptionTestNode.cs b/NativeWpf/WpfNodeEditorDemo/EmptyOptionTestNode.cs new file mode 100644 index 0000000..1c0449e --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/EmptyOptionTestNode.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; + +namespace WinNodeEditorDemo +{ + [STNode("/")] + public class EmptyOptionTestNode : STNode + { + protected override void OnCreate() { + base.OnCreate(); + this.Title = "EmptyTest"; + this.InputOptions.Add(STNodeOption.Empty); + this.InputOptions.Add("string", typeof(string), false); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/ImageNode/ImageBaseNode.cs b/NativeWpf/WpfNodeEditorDemo/ImageNode/ImageBaseNode.cs new file mode 100644 index 0000000..54010d1 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/ImageNode/ImageBaseNode.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; + +namespace WinNodeEditorDemo.ImageNode +{ + /// + /// 图片节点基类 用于确定节点风格 标题颜色 以及 数据类型颜色 + /// + public abstract class ImageBaseNode : STNode + { + /// + /// 需要作为显示绘制的图片 + /// + protected Image m_img_draw; + /// + /// 输出节点 + /// + protected STNodeOption m_op_img_out; + + protected override void OnCreate() { + base.OnCreate(); + m_op_img_out = this.OutputOptions.Add("", typeof(Image), false); + this.AutoSize = false; //此节点需要定制UI 所以无需AutoSize + //this.Size = new Size(320,240); + this.Width = 160; //手动设置节点大小 + this.Height = 120; + this.TitleColor = Color.FromArgb(200, Color.DarkCyan); + } + + protected override void OnOwnerChanged() { //向编辑器提交数据类型颜色 + base.OnOwnerChanged(); + if (this.Owner == null) return; + this.Owner.SetTypeColor(typeof(Image), Color.DarkCyan); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/ImageNode/ImageChannelNode.cs b/NativeWpf/WpfNodeEditorDemo/ImageNode/ImageChannelNode.cs new file mode 100644 index 0000000..e02cfd8 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/ImageNode/ImageChannelNode.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; +using System.Drawing.Imaging; + +namespace WinNodeEditorDemo.ImageNode +{ + [STNode("/Image")] + public class ImageChannelNode : ImageBaseNode + { + private STNodeOption m_op_img_in; //输入的节点 + private STNodeOption m_op_img_r; //R图 输出节点 + private STNodeOption m_op_img_g; //G图 输出节点 + private STNodeOption m_op_img_b; //B图 输出节点 + + protected override void OnCreate() { + base.OnCreate(); + this.Title = "ImageChannel"; + + m_op_img_in = this.InputOptions.Add("", typeof(Image), true); + m_op_img_r = this.OutputOptions.Add("R", typeof(Image), false); + m_op_img_g = this.OutputOptions.Add("G", typeof(Image), false); + m_op_img_b = this.OutputOptions.Add("B", typeof(Image), false); + //当输入节点有数据输入时候 + m_op_img_in.DataTransfer += new STNodeOptionEventHandler(m_op_img_in_DataTransfer); + } + + void m_op_img_in_DataTransfer(object sender, STNodeOptionEventArgs e) { + //如果当前不是连接状态 或者 接受到的数据为空 + if (e.Status != ConnectionStatus.Connected || e.TargetOption.Data == null) { + m_op_img_out.TransferData(null); //向所有输出节点输出空数据 + m_op_img_r.TransferData(null); + m_op_img_g.TransferData(null); + m_op_img_b.TransferData(null); + m_img_draw = null; //需要绘制显示的图片置为空 + } else { + Bitmap bmp = (Bitmap)e.TargetOption.Data; //否则计算图片的RGB图像 + Bitmap bmp_r = new Bitmap(bmp.Width, bmp.Height); + Bitmap bmp_g = new Bitmap(bmp.Width, bmp.Height); + Bitmap bmp_b = new Bitmap(bmp.Width, bmp.Height); + BitmapData bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + BitmapData bmpData_r = bmp_r.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + BitmapData bmpData_g = bmp_g.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + BitmapData bmpData_b = bmp_b.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + byte[] byColor = new byte[bmpData.Height * bmpData.Stride]; + byte[] byColor_r = new byte[byColor.Length]; + byte[] byColor_g = new byte[byColor.Length]; + byte[] byColor_b = new byte[byColor.Length]; + System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, byColor, 0, byColor.Length); + for (int y = 0; y < bmpData.Height; y++) { + int ny = y * bmpData.Stride; + for (int x = 0; x < bmpData.Width; x++) { + int nx = x << 2; + byColor_b[ny + nx] = byColor[ny + nx]; + byColor_g[ny + nx + 1] = byColor[ny + nx + 1]; + byColor_r[ny + nx + 2] = byColor[ny + nx + 2]; + byColor_r[ny + nx + 3] = byColor_g[ny + nx + 3] = byColor_b[ny + nx + 3] = byColor[ny + nx + 3]; + } + } + bmp.UnlockBits(bmpData); + System.Runtime.InteropServices.Marshal.Copy(byColor_r, 0, bmpData_r.Scan0, byColor_r.Length); + System.Runtime.InteropServices.Marshal.Copy(byColor_g, 0, bmpData_g.Scan0, byColor_g.Length); + System.Runtime.InteropServices.Marshal.Copy(byColor_b, 0, bmpData_b.Scan0, byColor_b.Length); + bmp_r.UnlockBits(bmpData_r); + bmp_g.UnlockBits(bmpData_g); + bmp_b.UnlockBits(bmpData_b); + m_op_img_out.TransferData(bmp); //out选项 输出原图 + m_op_img_r.TransferData(bmp_r); //R选项输出R图 + m_op_img_g.TransferData(bmp_g); + m_op_img_b.TransferData(bmp_b); + m_img_draw = bmp; //需要绘制显示的图片 + } + } + + protected override void OnDrawBody(DrawingTools dt) { + base.OnDrawBody(dt); + Rectangle rect = new Rectangle(this.Left + 10, this.Top + 30, 120, 80); + Graphics g = dt.Graphics; + g.FillRectangle(Brushes.Gray, rect); + if (m_img_draw != null) g.DrawImage(m_img_draw, rect); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/ImageNode/ImageInputNode.cs b/NativeWpf/WpfNodeEditorDemo/ImageNode/ImageInputNode.cs new file mode 100644 index 0000000..56f7106 --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/ImageNode/ImageInputNode.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ST.Library.UI.NodeEditor; +using System.Drawing; +using Microsoft.Win32; + +namespace WinNodeEditorDemo.ImageNode +{ + + [STNode("Image", "Crystal_lz", "2212233137@qq.com", "st233.com", "Image Node")] + public class ImageInputNode : ImageBaseNode + { + private string _FileName;//默认的DescriptorType不支持文件路径的选择 所以需要扩展 + [STNodeProperty("InputImage", "Click to select a image", DescriptorType = typeof(OpenFileDescriptor))] + public string FileName { + get { return _FileName; } + set { + Image img = null; //当文件名被设置时 加载图片并 向输出节点输出 + if (!string.IsNullOrEmpty(value)) { + img = Image.FromFile(value); + } + if (m_img_draw != null) m_img_draw.Dispose(); + m_img_draw = img; + _FileName = value; + m_op_img_out.TransferData(m_img_draw, true); + this.Invalidate(); + } + } + + protected override void OnCreate() { + base.OnCreate(); + this.Title = "ImageInput"; + } + + protected override void OnDrawBody(DrawingTools dt) { + base.OnDrawBody(dt); + Rectangle rect = new Rectangle(this.Left + 10, this.Top + 30, 140, 80); + Graphics g = dt.Graphics; + g.FillRectangle(Brushes.Gray, rect); + if (m_img_draw != null) g.DrawImage(m_img_draw, rect); + } + } + /// + /// 对默认Descriptor进行扩展 使得支持文件路径选择 + /// + public class OpenFileDescriptor : STNodePropertyDescriptor + { + private Rectangle m_rect_open; //需要绘制"打开"按钮的区域 + private StringFormat m_sf; + + public OpenFileDescriptor() { + m_sf = new StringFormat(); + m_sf.Alignment = StringAlignment.Center; + m_sf.LineAlignment = StringAlignment.Center; + } + + protected override void OnSetItemLocation() { //当在STNodePropertyGrid上确定此属性需要显示的区域时候 + base.OnSetItemLocation(); //计算出"打开"按钮需要绘制的区域 + m_rect_open = new Rectangle( + this.RectangleR.Right - 20, + this.RectangleR.Top, + 20, + this.RectangleR.Height); + } + + protected override void OnMouseClick(STNodeMouseEventArgs e) { + if (m_rect_open.Contains(e.Location)) { //点击在"打开"区域 则弹出文件选择框 + OpenFileDialog ofd = new OpenFileDialog(); + ofd.Filter = "Image files (*.jpg;*.jpeg;*.png)|*.jpg;*.jpeg;*.png"; + if (ofd.ShowDialog(System.Windows.Application.Current?.MainWindow) != true) return; + this.SetValue(ofd.FileName); + } else base.OnMouseClick(e); //否则默认处理方式 弹出文本输入框 + } + + protected override void OnDrawValueRectangle(DrawingTools dt) { + base.OnDrawValueRectangle(dt); //在STNodePropertyGrid绘制此属性区域时候将"打开"按钮绘制上去 + dt.Graphics.FillRectangle(Brushes.Gray, m_rect_open); + dt.Graphics.DrawString("+", this.Control.Font, Brushes.White, m_rect_open, m_sf); + } + } +} diff --git a/NativeWpf/WpfNodeEditorDemo/MainWindow.xaml b/NativeWpf/WpfNodeEditorDemo/MainWindow.xaml new file mode 100644 index 0000000..f2c462e --- /dev/null +++ b/NativeWpf/WpfNodeEditorDemo/MainWindow.xaml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + +