From d28553d46d4ad12bda0946b621a9c17a3bfc8d63 Mon Sep 17 00:00:00 2001 From: DebugST <2212233137@qq.com> Date: Thu, 29 Apr 2021 21:32:54 +0800 Subject: [PATCH] publish 2.0 --- .../NodeEditor/FrmNodePreviewPanel.cs | 144 + .../NodeEditor/FrmSTNodePropertyInput.cs | 114 + .../NodeEditor/FrmSTNodePropertySelect.cs | 116 + .../{STNodeEditor => NodeEditor}/STNode.cs | 2070 +++++---- ST.Library.UI/NodeEditor/STNodeAttribute.cs | 121 + .../STNodeCollection.cs | 489 +- .../STNodeControl.cs | 582 +-- .../STNodeControlCollection.cs | 428 +- .../STNodeEditor.cs | 4063 +++++++++-------- .../STNodeEditorDataType.cs | 401 +- .../NodeEditor/STNodeEditorPannel.cs | 329 ++ .../{STNodeEditor => NodeEditor}/STNodeHub.cs | 367 +- .../STNodeOption.cs | 886 ++-- .../STNodeOptionCollection.cs | 467 +- .../NodeEditor/STNodePropertyAttribute.cs | 334 ++ .../NodeEditor/STNodePropertyGrid.cs | 860 ++++ ST.Library.UI/NodeEditor/STNodeTreeView.cs | 907 ++++ ST.Library.UI/Properties/AssemblyInfo.cs | 0 ST.Library.UI/ST.Library.UI.csproj | 42 +- WinNodeEditerTest.suo | Bin 75264 -> 0 bytes WinNodeEditerTest/DemoNode.cs | 25 - .../Demo_Image/FrmImage.Designer.cs | 87 - WinNodeEditerTest/Demo_Image/FrmImage.cs | 58 - WinNodeEditerTest/Demo_Image/FrmImage.resx | 120 - WinNodeEditerTest/Demo_Image/STNodeImage.cs | 52 - .../Demo_Image/STNodeImageChannel.cs | 57 - .../Demo_Image/STNodeImageInput.cs | 93 - WinNodeEditerTest/Form1.Designer.cs | 100 - WinNodeEditerTest/Form1.cs | 87 - WinNodeEditerTest/Form2.Designer.cs | 96 - WinNodeEditerTest/Form2.cs | 36 - WinNodeEditerTest/Form2.resx | 120 - WinNodeEditerTest/NodeNumberAdd.cs | 49 - WinNodeEditerTest/app.config | 3 - WinNodeEditorDemo/AttrTestNode.cs | 108 + .../Blender/BlenderMixColorNode.cs | 130 + WinNodeEditorDemo/Blender/FrmEnumSelect.cs | 75 + WinNodeEditorDemo/Blender/STNodeCheckBox.cs | 48 + .../Blender/STNodeColorButton.cs | 31 + WinNodeEditorDemo/Blender/STNodeProgress.cs | 66 + WinNodeEditorDemo/Blender/STNodeSelectBox.cs | 55 + WinNodeEditorDemo/CalcNode.cs | 70 + WinNodeEditorDemo/EmptyOptionTestNode.cs | 20 + WinNodeEditorDemo/Form1.Designer.cs | 173 + WinNodeEditorDemo/Form1.cs | 88 + .../Form1.resx | 241 +- WinNodeEditorDemo/ImageNode/ImageBaseNode.cs | 41 + .../ImageNode/ImageChannelNode.cs | 87 + WinNodeEditorDemo/ImageNode/ImageInputNode.cs | 84 + WinNodeEditorDemo/NumberNode/NumberAddNode.cs | 73 + .../NumberNode/NumberInputNode.cs | 52 + WinNodeEditorDemo/NumberNode/NumberNode.cs | 25 + .../Program.cs | 41 +- .../Properties/AssemblyInfo.cs | 72 +- .../Properties/Resources.Designer.cs | 127 +- .../Properties/Resources.resx | 232 +- .../Properties/Settings.Designer.cs | 54 +- .../Properties/Settings.settings | 14 +- WinNodeEditorDemo/STNodeHub.cs | 32 + WinNodeEditorDemo/ToolStripRendererEx.cs | 80 + .../WinNodeEditorDemo.csproj | 229 +- WinNodeEditorTest.7z | Bin 0 -> 291826 bytes ...odeEditerTest.sln => WinNodeEditorTest.sln | 84 +- WinNodeEditorTest.suo | Bin 0 -> 141312 bytes docs/STNodeEditor.zip | Bin 2162588 -> 0 bytes docs/api.html | 1681 +++++++ docs/back.png | Bin 11984 -> 0 bytes docs/css/doc.css | 185 - docs/css/index.css | 369 +- docs/css/stdoc.css | 270 ++ docs/doc.html | 927 ---- docs/doc_cn.html | 87 + docs/doc_en.html | 87 + docs/grid.png | Bin 21038 -> 0 bytes docs/images/api.png | Bin 0 -> 6247 bytes docs/images/channel.png | Bin 202072 -> 0 bytes docs/images/download.png | Bin 0 -> 5934 bytes docs/images/form1.png | Bin 154756 -> 0 bytes docs/images/formImage.png | Bin 209273 -> 0 bytes docs/images/gdip.png | Bin 0 -> 167627 bytes docs/images/gitee.png | Bin 0 -> 11558 bytes docs/images/github.png | Bin 0 -> 11301 bytes docs/images/layout_l.png | Bin 0 -> 4860 bytes docs/images/layout_lr.png | Bin 0 -> 4854 bytes docs/images/layout_r.png | Bin 0 -> 4860 bytes docs/images/lock.gif | Bin 761718 -> 0 bytes docs/images/node.gif | Bin docs/images/node.png | Bin 64977 -> 0 bytes docs/images/node_add.png | Bin 10673 -> 0 bytes docs/images/node_demo.png | Bin 15189 -> 0 bytes docs/images/node_scan.png | Bin 0 -> 108819 bytes docs/images/node_show_back.png | Bin 0 -> 134521 bytes docs/images/node_show_frm.png | Bin 0 -> 346870 bytes docs/images/node_show_mix.png | Bin 0 -> 90164 bytes docs/images/node_ui.png | Bin 0 -> 32809 bytes docs/images/object_type.png | Bin 14076 -> 0 bytes docs/images/open_image.png | Bin 29663 -> 0 bytes docs/images/page_top.png | Bin 0 -> 329697 bytes docs/images/sticker.png | Bin 0 -> 75685 bytes docs/images/stnodeeditor.gif | Bin 0 -> 1450196 bytes docs/images/stnodeeditorpannel.gif | Bin 0 -> 433187 bytes docs/images/stnodehub.gif | Bin 0 -> 1994235 bytes docs/images/stnodepropertygrid.gif | Bin 0 -> 783680 bytes docs/images/stnodetreeview.gif | Bin 0 -> 1083221 bytes docs/images/top_bg.jpeg | Bin 0 -> 178305 bytes docs/images/tu_clocknode_data.gif | Bin 0 -> 100329 bytes docs/images/tu_clockshowtime.gif | Bin 0 -> 136700 bytes docs/images/tu_colornode_1.png | Bin 0 -> 23154 bytes docs/images/tu_colornode_2.png | Bin 0 -> 23072 bytes docs/images/tu_colornode_3.png | Bin 0 -> 30298 bytes docs/images/tu_colornode_4.png | Bin 0 -> 22975 bytes docs/images/tu_editor_alert.png | Bin 0 -> 11747 bytes docs/images/tu_imagenode.png | Bin 0 -> 135455 bytes docs/images/tu_mynode_autosize.png | Bin 0 -> 14307 bytes docs/images/tu_mynode_autosize_onset.png | Bin 0 -> 15371 bytes docs/images/tu_mynode_btn.gif | Bin 0 -> 74371 bytes docs/images/tu_mynode_ctrl.png | Bin 0 -> 19190 bytes docs/images/tu_mynode_empty.png | Bin 0 -> 15601 bytes docs/images/tu_mynode_single.png | Bin 0 -> 19244 bytes docs/images/tu_property_1.png | Bin 0 -> 21384 bytes docs/images/tu_property_2.png | Bin 0 -> 24929 bytes docs/images/tu_property_3.png | Bin 0 -> 29799 bytes docs/images/tu_property_4.png | Bin 0 -> 21779 bytes docs/images/tu_property_5.png | Bin 0 -> 25066 bytes docs/images/tu_property_6.png | Bin 0 -> 29587 bytes docs/images/tu_stnodepropertygrid.png | Bin 0 -> 41892 bytes docs/images/tu_testnode.png | Bin 0 -> 12549 bytes docs/images/tu_testnode_adop.png | Bin 0 -> 14078 bytes docs/images/tu_testnode_adopclr.png | Bin 0 -> 14726 bytes docs/images/tu_treeview_1.png | Bin 0 -> 24090 bytes docs/images/tu_treeview_2.png | Bin 0 -> 25739 bytes docs/index.html | 255 +- docs/index_en.html | 179 + docs/js/doc.js | 0 docs/js/index.js | 0 docs/js/jquery-1.10.2.min.js | 0 docs/js/stdoc.js | 49 + docs/line.png | Bin 24981 -> 0 bytes docs/mark.png | Bin 18796 -> 0 bytes docs/node.png | Bin 49415 -> 0 bytes docs/out.png | Bin 12516 -> 0 bytes docs/result.png | Bin 27664 -> 0 bytes docs/tutorials_cn.html | 1225 +++++ docs/tutorials_en.html | 1228 +++++ 144 files changed, 15250 insertions(+), 7427 deletions(-) create mode 100644 ST.Library.UI/NodeEditor/FrmNodePreviewPanel.cs create mode 100644 ST.Library.UI/NodeEditor/FrmSTNodePropertyInput.cs create mode 100644 ST.Library.UI/NodeEditor/FrmSTNodePropertySelect.cs rename ST.Library.UI/{STNodeEditor => NodeEditor}/STNode.cs (66%) mode change 100755 => 100644 create mode 100644 ST.Library.UI/NodeEditor/STNodeAttribute.cs rename ST.Library.UI/{STNodeEditor => NodeEditor}/STNodeCollection.cs (91%) mode change 100755 => 100644 rename ST.Library.UI/{STNodeEditor => NodeEditor}/STNodeControl.cs (82%) mode change 100755 => 100644 rename ST.Library.UI/{STNodeEditor => NodeEditor}/STNodeControlCollection.cs (96%) mode change 100755 => 100644 rename ST.Library.UI/{STNodeEditor => NodeEditor}/STNodeEditor.cs (86%) mode change 100755 => 100644 rename ST.Library.UI/{STNodeEditor => NodeEditor}/STNodeEditorDataType.cs (95%) mode change 100755 => 100644 create mode 100644 ST.Library.UI/NodeEditor/STNodeEditorPannel.cs rename ST.Library.UI/{STNodeEditor => NodeEditor}/STNodeHub.cs (85%) mode change 100755 => 100644 rename ST.Library.UI/{STNodeEditor => NodeEditor}/STNodeOption.cs (90%) mode change 100755 => 100644 rename ST.Library.UI/{STNodeEditor => NodeEditor}/STNodeOptionCollection.cs (91%) mode change 100755 => 100644 create mode 100644 ST.Library.UI/NodeEditor/STNodePropertyAttribute.cs create mode 100644 ST.Library.UI/NodeEditor/STNodePropertyGrid.cs create mode 100644 ST.Library.UI/NodeEditor/STNodeTreeView.cs mode change 100755 => 100644 ST.Library.UI/Properties/AssemblyInfo.cs mode change 100755 => 100644 ST.Library.UI/ST.Library.UI.csproj delete mode 100755 WinNodeEditerTest.suo delete mode 100755 WinNodeEditerTest/DemoNode.cs delete mode 100755 WinNodeEditerTest/Demo_Image/FrmImage.Designer.cs delete mode 100755 WinNodeEditerTest/Demo_Image/FrmImage.cs delete mode 100755 WinNodeEditerTest/Demo_Image/FrmImage.resx delete mode 100755 WinNodeEditerTest/Demo_Image/STNodeImage.cs delete mode 100755 WinNodeEditerTest/Demo_Image/STNodeImageChannel.cs delete mode 100755 WinNodeEditerTest/Demo_Image/STNodeImageInput.cs delete mode 100755 WinNodeEditerTest/Form1.Designer.cs delete mode 100755 WinNodeEditerTest/Form1.cs delete mode 100755 WinNodeEditerTest/Form2.Designer.cs delete mode 100755 WinNodeEditerTest/Form2.cs delete mode 100755 WinNodeEditerTest/Form2.resx delete mode 100755 WinNodeEditerTest/NodeNumberAdd.cs delete mode 100755 WinNodeEditerTest/app.config create mode 100644 WinNodeEditorDemo/AttrTestNode.cs create mode 100644 WinNodeEditorDemo/Blender/BlenderMixColorNode.cs create mode 100644 WinNodeEditorDemo/Blender/FrmEnumSelect.cs create mode 100644 WinNodeEditorDemo/Blender/STNodeCheckBox.cs create mode 100644 WinNodeEditorDemo/Blender/STNodeColorButton.cs create mode 100644 WinNodeEditorDemo/Blender/STNodeProgress.cs create mode 100644 WinNodeEditorDemo/Blender/STNodeSelectBox.cs create mode 100644 WinNodeEditorDemo/CalcNode.cs create mode 100644 WinNodeEditorDemo/EmptyOptionTestNode.cs create mode 100644 WinNodeEditorDemo/Form1.Designer.cs create mode 100644 WinNodeEditorDemo/Form1.cs rename {WinNodeEditerTest => WinNodeEditorDemo}/Form1.resx (94%) mode change 100755 => 100644 create mode 100644 WinNodeEditorDemo/ImageNode/ImageBaseNode.cs create mode 100644 WinNodeEditorDemo/ImageNode/ImageChannelNode.cs create mode 100644 WinNodeEditorDemo/ImageNode/ImageInputNode.cs create mode 100644 WinNodeEditorDemo/NumberNode/NumberAddNode.cs create mode 100644 WinNodeEditorDemo/NumberNode/NumberInputNode.cs create mode 100644 WinNodeEditorDemo/NumberNode/NumberNode.cs rename {WinNodeEditerTest => WinNodeEditorDemo}/Program.cs (83%) mode change 100755 => 100644 rename {WinNodeEditerTest => WinNodeEditorDemo}/Properties/AssemblyInfo.cs (82%) mode change 100755 => 100644 rename {WinNodeEditerTest => WinNodeEditorDemo}/Properties/Resources.Designer.cs (86%) mode change 100755 => 100644 rename {WinNodeEditerTest => WinNodeEditorDemo}/Properties/Resources.resx (97%) mode change 100755 => 100644 rename {WinNodeEditerTest => WinNodeEditorDemo}/Properties/Settings.Designer.cs (88%) mode change 100755 => 100644 rename {WinNodeEditerTest => WinNodeEditorDemo}/Properties/Settings.settings (97%) mode change 100755 => 100644 create mode 100644 WinNodeEditorDemo/STNodeHub.cs create mode 100644 WinNodeEditorDemo/ToolStripRendererEx.cs rename WinNodeEditerTest/WinNodeEditerTest.csproj => WinNodeEditorDemo/WinNodeEditorDemo.csproj (70%) mode change 100755 => 100644 create mode 100644 WinNodeEditorTest.7z rename WinNodeEditerTest.sln => WinNodeEditorTest.sln (69%) mode change 100755 => 100644 create mode 100644 WinNodeEditorTest.suo delete mode 100755 docs/STNodeEditor.zip create mode 100644 docs/api.html delete mode 100755 docs/back.png delete mode 100755 docs/css/doc.css mode change 100755 => 100644 docs/css/index.css create mode 100644 docs/css/stdoc.css delete mode 100755 docs/doc.html create mode 100644 docs/doc_cn.html create mode 100644 docs/doc_en.html delete mode 100755 docs/grid.png create mode 100644 docs/images/api.png delete mode 100755 docs/images/channel.png create mode 100644 docs/images/download.png delete mode 100755 docs/images/form1.png delete mode 100755 docs/images/formImage.png create mode 100644 docs/images/gdip.png create mode 100644 docs/images/gitee.png create mode 100644 docs/images/github.png create mode 100644 docs/images/layout_l.png create mode 100644 docs/images/layout_lr.png create mode 100644 docs/images/layout_r.png delete mode 100755 docs/images/lock.gif mode change 100755 => 100644 docs/images/node.gif delete mode 100755 docs/images/node.png delete mode 100644 docs/images/node_add.png delete mode 100644 docs/images/node_demo.png create mode 100644 docs/images/node_scan.png create mode 100644 docs/images/node_show_back.png create mode 100644 docs/images/node_show_frm.png create mode 100644 docs/images/node_show_mix.png create mode 100644 docs/images/node_ui.png delete mode 100755 docs/images/object_type.png delete mode 100755 docs/images/open_image.png create mode 100644 docs/images/page_top.png create mode 100644 docs/images/sticker.png create mode 100644 docs/images/stnodeeditor.gif create mode 100644 docs/images/stnodeeditorpannel.gif create mode 100644 docs/images/stnodehub.gif create mode 100644 docs/images/stnodepropertygrid.gif create mode 100644 docs/images/stnodetreeview.gif create mode 100644 docs/images/top_bg.jpeg create mode 100644 docs/images/tu_clocknode_data.gif create mode 100644 docs/images/tu_clockshowtime.gif create mode 100644 docs/images/tu_colornode_1.png create mode 100644 docs/images/tu_colornode_2.png create mode 100644 docs/images/tu_colornode_3.png create mode 100644 docs/images/tu_colornode_4.png create mode 100644 docs/images/tu_editor_alert.png create mode 100644 docs/images/tu_imagenode.png create mode 100644 docs/images/tu_mynode_autosize.png create mode 100644 docs/images/tu_mynode_autosize_onset.png create mode 100644 docs/images/tu_mynode_btn.gif create mode 100644 docs/images/tu_mynode_ctrl.png create mode 100644 docs/images/tu_mynode_empty.png create mode 100644 docs/images/tu_mynode_single.png create mode 100644 docs/images/tu_property_1.png create mode 100644 docs/images/tu_property_2.png create mode 100644 docs/images/tu_property_3.png create mode 100644 docs/images/tu_property_4.png create mode 100644 docs/images/tu_property_5.png create mode 100644 docs/images/tu_property_6.png create mode 100644 docs/images/tu_stnodepropertygrid.png create mode 100644 docs/images/tu_testnode.png create mode 100644 docs/images/tu_testnode_adop.png create mode 100644 docs/images/tu_testnode_adopclr.png create mode 100644 docs/images/tu_treeview_1.png create mode 100644 docs/images/tu_treeview_2.png mode change 100755 => 100644 docs/index.html create mode 100644 docs/index_en.html delete mode 100755 docs/js/doc.js delete mode 100755 docs/js/index.js mode change 100755 => 100644 docs/js/jquery-1.10.2.min.js create mode 100644 docs/js/stdoc.js delete mode 100755 docs/line.png delete mode 100755 docs/mark.png delete mode 100755 docs/node.png delete mode 100755 docs/out.png delete mode 100755 docs/result.png create mode 100644 docs/tutorials_cn.html create mode 100644 docs/tutorials_en.html diff --git a/ST.Library.UI/NodeEditor/FrmNodePreviewPanel.cs b/ST.Library.UI/NodeEditor/FrmNodePreviewPanel.cs new file mode 100644 index 0000000..09b61f2 --- /dev/null +++ b/ST.Library.UI/NodeEditor/FrmNodePreviewPanel.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Windows.Forms; +using System.Runtime.InteropServices; + +namespace ST.Library.UI.NodeEditor +{ + internal class FrmNodePreviewPanel : Form + { + public Color BorderColor { get; set; } + public bool AutoBorderColor { get; set; } + + private bool m_bRight; + private Point m_ptHandle; + private int m_nHandleSize; + private Rectangle m_rect_handle; + private Rectangle m_rect_panel; + private Rectangle m_rect_exclude; + private Region m_region; + private Type m_type; + private STNode m_node; + private STNodeEditor m_editor; + private STNodePropertyGrid m_property; + + private Pen m_pen = new Pen(Color.Black); + private SolidBrush m_brush = new SolidBrush(Color.Black); + private static FrmNodePreviewPanel m_last_frm; + + [DllImport("user32.dll")] + private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw); + + public FrmNodePreviewPanel(Type stNodeType, Point ptHandle, int nHandleSize, bool bRight, STNodeEditor editor, STNodePropertyGrid propertyGrid) { + this.SetStyle(ControlStyles.UserPaint, true); + this.SetStyle(ControlStyles.ResizeRedraw, true); + this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); + this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + this.SetStyle(ControlStyles.SupportsTransparentBackColor, true); + + if (m_last_frm != null) m_last_frm.Close(); + m_last_frm = this; + + m_editor = editor; + m_property = propertyGrid; + m_editor.Size = new Size(200, 200); + m_property.Size = new Size(200, 200); + m_editor.Location = new Point(1 + (bRight ? nHandleSize : 0), 1); + m_property.Location = new Point(m_editor.Right, 1); + m_property.InfoFirstOnDraw = true; + this.Controls.Add(m_editor); + this.Controls.Add(m_property); + this.ShowInTaskbar = false; + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + this.Size = new Size(402 + nHandleSize, 202); + + m_type = stNodeType; + m_ptHandle = ptHandle; + m_nHandleSize = nHandleSize; + m_bRight = bRight; + + this.AutoBorderColor = true; + this.BorderColor = Color.DodgerBlue; + } + + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + m_node = (STNode)Activator.CreateInstance(m_type); + m_node.Left = 20; m_node.Top = 20; + m_editor.Nodes.Add(m_node); + m_property.SetNode(m_node); + + m_rect_panel = new Rectangle(0, 0, 402, 202); + m_rect_handle = new Rectangle(m_ptHandle.X, m_ptHandle.Y, m_nHandleSize, m_nHandleSize); + m_rect_exclude = new Rectangle(0, m_nHandleSize, m_nHandleSize, this.Height - m_nHandleSize); + if (m_bRight) { + this.Left = m_ptHandle.X; + m_rect_panel.X = m_ptHandle.X + m_nHandleSize; + } else { + this.Left = m_ptHandle.X - this.Width + m_nHandleSize; + m_rect_exclude.X = this.Width - m_nHandleSize; + m_rect_panel.X = this.Left; + } + if (m_ptHandle.Y + this.Height > Screen.GetWorkingArea(this).Bottom) { + this.Top = m_ptHandle.Y - this.Height + m_nHandleSize; + m_rect_exclude.Y -= m_nHandleSize; + } else this.Top = m_ptHandle.Y; + m_rect_panel.Y = this.Top; + m_region = new Region(new Rectangle(Point.Empty, this.Size)); + m_region.Exclude(m_rect_exclude); + using (Graphics g = this.CreateGraphics()) { + IntPtr h = m_region.GetHrgn(g); + FrmNodePreviewPanel.SetWindowRgn(this.Handle, h, false); + m_region.ReleaseHrgn(h); + } + + this.MouseLeave += Event_MouseLeave; + m_editor.MouseLeave += Event_MouseLeave; + m_property.MouseLeave += Event_MouseLeave; + this.BeginInvoke(new MethodInvoker(() => { + m_property.Focus(); + })); + } + + protected override void OnClosing(CancelEventArgs e) { + base.OnClosing(e); + this.Controls.Clear(); + m_editor.Nodes.Clear(); + m_editor.MouseLeave -= Event_MouseLeave; + m_property.MouseLeave -= Event_MouseLeave; + m_last_frm = null; + } + + void Event_MouseLeave(object sender, EventArgs e) { + Point pt = Control.MousePosition; + if (m_rect_panel.Contains(pt) || m_rect_handle.Contains(pt)) return; + this.Close(); + } + + protected override void OnPaint(PaintEventArgs e) { + base.OnPaint(e); + Graphics g = e.Graphics; + m_pen.Color = this.AutoBorderColor ? m_node.TitleColor : this.BorderColor; + m_brush.Color = m_pen.Color; + g.DrawRectangle(m_pen, 0, 0, this.Width - 1, this.Height - 1); + g.FillRectangle(m_brush, m_rect_exclude.X - 1, m_rect_exclude.Y - 1, m_rect_exclude.Width + 2, m_rect_exclude.Height + 2); + + Rectangle rect = this.RectangleToClient(m_rect_handle); + rect.Y = (m_nHandleSize - 14) / 2; + rect.X += rect.Y + 1; + rect.Width = rect.Height = 14; + m_pen.Width = 2; + g.DrawLine(m_pen, rect.X + 4, rect.Y + 3, rect.X + 10, rect.Y + 3); + g.DrawLine(m_pen, rect.X + 4, rect.Y + 6, rect.X + 10, rect.Y + 6); + g.DrawLine(m_pen, rect.X + 4, rect.Y + 11, rect.X + 10, rect.Y + 11); + g.DrawLine(m_pen, rect.X + 7, rect.Y + 7, rect.X + 7, rect.Y + 10); + m_pen.Width = 1; + g.DrawRectangle(m_pen, rect.X, rect.Y, rect.Width - 1, rect.Height - 1); + } + } +} diff --git a/ST.Library.UI/NodeEditor/FrmSTNodePropertyInput.cs b/ST.Library.UI/NodeEditor/FrmSTNodePropertyInput.cs new file mode 100644 index 0000000..d5574e4 --- /dev/null +++ b/ST.Library.UI/NodeEditor/FrmSTNodePropertyInput.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Windows.Forms; +using System.Drawing; +using ST.Library.UI.NodeEditor; + +namespace ST.Library.UI +{ + internal class FrmSTNodePropertyInput : Form + { + private STNodePropertyDescriptor m_descriptor; + private Rectangle m_rect; + private Pen m_pen; + private SolidBrush m_brush; + private TextBox m_tbx; + + public FrmSTNodePropertyInput(STNodePropertyDescriptor descriptor) { + this.SetStyle(ControlStyles.UserPaint, true); + this.SetStyle(ControlStyles.ResizeRedraw, true); + this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); + this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + this.SetStyle(ControlStyles.SupportsTransparentBackColor, true); + + m_rect = descriptor.RectangleR; + m_descriptor = descriptor; + this.ShowInTaskbar = false; + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + this.BackColor = descriptor.Control.AutoColor ? descriptor.Node.TitleColor : descriptor.Control.ItemSelectedColor; + m_pen = new Pen(descriptor.Control.ForeColor, 1); + m_brush = new SolidBrush(this.BackColor); + } + + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + Point pt = m_descriptor.Control.PointToScreen(m_rect.Location); + pt.Y += m_descriptor.Control.ScrollOffset; + this.Location = pt; + this.Size = new System.Drawing.Size(m_rect.Width + m_rect.Height, m_rect.Height); + + m_tbx = new TextBox(); + m_tbx.Font = m_descriptor.Control.Font; + m_tbx.ForeColor = m_descriptor.Control.ForeColor; + m_tbx.BackColor = Color.FromArgb(255, m_descriptor.Control.ItemValueBackColor); + m_tbx.BorderStyle = BorderStyle.None; + + m_tbx.Size = new Size(this.Width - 4 - m_rect.Height, this.Height - 2); + m_tbx.Text = m_descriptor.GetStringFromValue(); + this.Controls.Add(m_tbx); + m_tbx.Location = new Point(2, (this.Height - m_tbx.Height) / 2); + m_tbx.SelectAll(); + m_tbx.LostFocus += (s, ea) => this.Close(); + m_tbx.KeyDown += new KeyEventHandler(tbx_KeyDown); + } + + protected override void OnPaint(PaintEventArgs e) { + base.OnPaint(e); + Graphics g = e.Graphics; + m_brush.Color = m_tbx.BackColor; + g.FillRectangle(m_brush, 1, 1, this.Width - 2 - m_rect.Height, this.Height - 2); + m_brush.Color = m_descriptor.Control.ForeColor; + //Enter + g.FillPolygon(m_brush, new Point[]{ + new Point(this.Width - 21, this.Height - 2), + new Point(this.Width - 14, this.Height - 2), + new Point(this.Width - 14, this.Height - 8) + }); + g.DrawLine(m_pen, this.Width - 14, this.Height - 3, this.Width - 4, this.Height - 3); + g.DrawLine(m_pen, this.Width - 4, this.Height - 3, this.Width - 4, 14); + g.DrawLine(m_pen, this.Width - 8, 13, this.Width - 4, 13); + //---- + g.DrawLine(m_pen, this.Width - 19, 11, this.Width - 4, 11); + //E + g.DrawLine(m_pen, this.Width - 19, 3, this.Width - 16, 3); + g.DrawLine(m_pen, this.Width - 19, 6, this.Width - 16, 6); + g.DrawLine(m_pen, this.Width - 19, 9, this.Width - 16, 9); + g.DrawLine(m_pen, this.Width - 19, 3, this.Width - 19, 9); + //S + g.DrawLine(m_pen, this.Width - 13, 3, this.Width - 10, 3); + g.DrawLine(m_pen, this.Width - 13, 6, this.Width - 10, 6); + g.DrawLine(m_pen, this.Width - 13, 9, this.Width - 10, 9); + g.DrawLine(m_pen, this.Width - 13, 3, this.Width - 13, 6); + g.DrawLine(m_pen, this.Width - 10, 6, this.Width - 10, 9); + //C + g.DrawLine(m_pen, this.Width - 7, 3, this.Width - 4, 3); + g.DrawLine(m_pen, this.Width - 7, 9, this.Width - 4, 9); + g.DrawLine(m_pen, this.Width - 7, 3, this.Width - 7, 9); + } + + void tbx_KeyDown(object sender, KeyEventArgs e) { + if (e.KeyCode == Keys.Escape) this.Close(); + if (e.KeyCode != Keys.Enter) return; + try { + m_descriptor.SetValue(((TextBox)sender).Text, null); + m_descriptor.Control.Invalidate();//add rect; + } catch (Exception ex) { + m_descriptor.OnSetValueError(ex); + } + this.Close(); + } + + private void InitializeComponent() { + this.SuspendLayout(); + // + // FrmSTNodePropertyInput + // + this.ClientSize = new System.Drawing.Size(292, 273); + this.Name = "FrmSTNodePropertyInput"; + this.ResumeLayout(false); + } + } +} diff --git a/ST.Library.UI/NodeEditor/FrmSTNodePropertySelect.cs b/ST.Library.UI/NodeEditor/FrmSTNodePropertySelect.cs new file mode 100644 index 0000000..3adf24e --- /dev/null +++ b/ST.Library.UI/NodeEditor/FrmSTNodePropertySelect.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Windows.Forms; +using System.Drawing; + +namespace ST.Library.UI.NodeEditor +{ + internal class FrmSTNodePropertySelect : Form + { + private STNodePropertyDescriptor m_descriptor; + private int m_nItemHeight = 25; + + private static Type m_t_bool = typeof(bool); + private Pen m_pen; + private SolidBrush m_brush; + private StringFormat m_sf; + private Color m_clr_item_1 = Color.FromArgb(10, 0, 0, 0);// Color.FromArgb(255, 40, 40, 40); + private Color m_clr_item_2 = Color.FromArgb(10, 255, 255, 255);// Color.FromArgb(255, 50, 50, 50); + private object m_item_hover; + + public FrmSTNodePropertySelect(STNodePropertyDescriptor descriptor) { + this.SetStyle(ControlStyles.UserPaint, true); + this.SetStyle(ControlStyles.ResizeRedraw, true); + this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); + this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + this.SetStyle(ControlStyles.SupportsTransparentBackColor, true); + + m_descriptor = descriptor; + this.Size = descriptor.RectangleR.Size; + this.ShowInTaskbar = false; + this.BackColor = descriptor.Control.BackColor; + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + m_pen = new Pen(descriptor.Control.AutoColor ? descriptor.Node.TitleColor : descriptor.Control.ItemSelectedColor, 1); + m_brush = new SolidBrush(this.BackColor); + m_sf = new StringFormat(); + m_sf.LineAlignment = StringAlignment.Center; + m_sf.FormatFlags = StringFormatFlags.NoWrap; + } + + private List m_lst_item = new List(); + + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + Point pt = m_descriptor.Control.PointToScreen(m_descriptor.RectangleR.Location); + pt.Y += m_descriptor.Control.ScrollOffset; + this.Location = pt; + if (m_descriptor.PropertyInfo.PropertyType.IsEnum) { + foreach (var v in Enum.GetValues(m_descriptor.PropertyInfo.PropertyType)) m_lst_item.Add(v); + } else if (m_descriptor.PropertyInfo.PropertyType == m_t_bool) { + m_lst_item.Add(true); + m_lst_item.Add(false); + } else { + this.Close(); + return; + } + this.Height = m_lst_item.Count * m_nItemHeight; + Rectangle rect = Screen.GetWorkingArea(this); + if (this.Bottom > rect.Bottom) this.Top -= (this.Bottom - rect.Bottom); + this.MouseLeave += (s, ea) => this.Close(); + this.LostFocus += (s, ea) => this.Close(); + } + + protected override void OnPaint(PaintEventArgs e) { + base.OnPaint(e); + Graphics g = e.Graphics; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + Rectangle rect_back = new Rectangle(0, 0, this.Width, m_nItemHeight); + Rectangle rect_font = new Rectangle(10, 0, this.Width - 13, m_nItemHeight); + int nIndex = 0; + string strVal = m_descriptor.GetStringFromValue(); + foreach (var v in m_lst_item) { + m_brush.Color = nIndex++ % 2 == 0 ? m_clr_item_1 : m_clr_item_2; + g.FillRectangle(m_brush, rect_back); + if (v == m_item_hover) { + m_brush.Color = m_descriptor.Control.ItemHoverColor; + g.FillRectangle(m_brush, rect_back); + } + if (v.ToString() == strVal) { + m_brush.Color = m_descriptor.Control.ItemSelectedColor; + g.FillRectangle(m_brush, 4, rect_back.Top + 10, 5, 5); + } + m_brush.Color = m_descriptor.Control.ForeColor; + g.DrawString(v.ToString(), m_descriptor.Control.Font, m_brush, rect_font, m_sf); + rect_back.Y += m_nItemHeight; + rect_font.Y += m_nItemHeight; + } + g.DrawRectangle(m_pen, 0, 0, this.Width - 1, this.Height - 1); + } + + protected override void OnMouseMove(MouseEventArgs e) { + base.OnMouseMove(e); + int nIndex = e.Y / m_nItemHeight; + if (nIndex < 0 || nIndex >= m_lst_item.Count) return; + var item = m_lst_item[e.Y / m_nItemHeight]; + if (m_item_hover == item) return; + m_item_hover = item; + this.Invalidate(); + } + + protected override void OnMouseClick(MouseEventArgs e) { + base.OnMouseClick(e); + this.Close(); + int nIndex = e.Y / m_nItemHeight; + if (nIndex < 0) return; + if (nIndex > m_lst_item.Count) return; + try { + m_descriptor.SetValue(m_lst_item[nIndex], null); + } catch (Exception ex) { + m_descriptor.OnSetValueError(ex); + } + } + } +} diff --git a/ST.Library.UI/STNodeEditor/STNode.cs b/ST.Library.UI/NodeEditor/STNode.cs old mode 100755 new mode 100644 similarity index 66% rename from ST.Library.UI/STNodeEditor/STNode.cs rename to ST.Library.UI/NodeEditor/STNode.cs index c9598a7..07e422a --- a/ST.Library.UI/STNodeEditor/STNode.cs +++ b/ST.Library.UI/NodeEditor/STNode.cs @@ -1,953 +1,1117 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using System.Drawing; -using System.Windows.Forms; -using System.Collections; -/* -MIT License - -Copyright (c) 2021 DebugST@crystal_lz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - */ -/* - * time: 2021-01-06 - * Author: Crystal_lz - * blog: st233.com - * Github: DebugST.github.io - */ -namespace ST.Library.UI -{ - public abstract class STNode - { - private STNodeEditor _Owner; - /// - /// 获取当前 Node 所有者 - /// - public STNodeEditor Owner { - get { return _Owner; } - internal set { - if (value == _Owner) return; - if (_Owner != null) { - foreach (STNodeOption op in this._InputOptions.ToArray()) op.DisConnectionAll(); - foreach (STNodeOption op in this._OutputOptions.ToArray()) op.DisConnectionAll(); - } - _Owner = value; - this.BuildSize(true, true, false); - this.OnOwnerChanged(); - } - } - - private bool _IsSelected; - /// - /// 获取或设置 Node 是否处于被选中状态 - /// - public bool IsSelected { - get { return _IsSelected; } - set { - if (value == _IsSelected) return; - _IsSelected = value; - this.Invalidate(); - this.OnSelectedChanged(); - } - } - - private bool _IsActive; - /// - /// 获取 Node 是否处于活动状态 - /// - public bool IsActive { - get { return _IsActive; } - internal set { - if (value == _IsActive) return; - _IsActive = value; - this.OnActiveChanged(); - } - } - - private Color _TitleColor; - /// - /// 获取或设置标题背景颜色 - /// - public Color TitleColor { - get { return _TitleColor; } - protected set { - _TitleColor = value; - this.Invalidate(this.TitleRectangle); - } - } - - private Color _MarkColor; - /// - /// 获取或设置标记信息背景颜色 - /// - public Color MarkColor { - get { return _MarkColor; } - protected set { - _MarkColor = value; - this.Invalidate(this._MarkRectangle); - } - } - - private Color _ForeColor = Color.White; - /// - /// 获取或设置当前 Node 前景色 - /// - public Color ForeColor { - get { return _ForeColor; } - protected set { - _ForeColor = value; - this.Invalidate(); - } - } - - private Color _BackColor; - /// - /// 获取或设置当前 Node 背景色 - /// - public Color BackColor { - get { return _BackColor; } - protected set { - _BackColor = value; - this.Invalidate(); - } - } - - private string _Title; - /// - /// 获取或设置 Node 标题 - /// - public string Title { - get { return _Title; } - protected set { - _Title = value; - this.Invalidate(this.TitleRectangle); - } - } - - private string _Mark; - /// - /// 获取或设置 Node 标记信息 - /// - 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(); - this.BuildSize(false, true, true); - //if (this._Owner != null) this._Owner.Invalidate(); - //this.Invalidate(); - //this.Invalidate(this._MarkRectangle); - } - } - - private string[] _MarkLines;//单独存放行数据 不用每次在绘制中去拆分 - /// - /// 获取 Node 标记信息行数据 - /// - public string[] MarkLines { - get { return _MarkLines; } - } - - private int _Left; - /// - /// 获取或设置 Node 左边坐标 - /// - public int Left { - get { return _Left; } - set { - if (this._LockLocation) return; - _Left = value; - this.BuildSize(false, true, false); - //this._MarkRectangle = this.OnBuildMarkRectangle(); - if (this._Owner != null) { - this._Owner.BuildLinePath(); - this._Owner.BuildBounds(); - } - this.OnMove(new EventArgs()); - } - } - - private int _Top; - /// - /// 获取或设置 Node 上边坐标 - /// - public int Top { - get { return _Top; } - set { - if (this._LockLocation) return; - _Top = value; - this.BuildSize(false, true, false); - //this._MarkRectangle = this.OnBuildMarkRectangle(); - if (this._Owner != null) { - this._Owner.BuildLinePath(); - this._Owner.BuildBounds(); - } - this.OnMove(new EventArgs()); - } - } - - private int _Width; - /// - /// 获取或设置 Node 宽度 - /// - public int Width { - get { return _Width; } - } - - private int _Height; - /// - /// 获取或设置 Node 高度 - /// - public int Height { - get { return _Height; } - } - /// - /// 获取 Node 右边边坐标 - /// - public int Right { - get { return _Left + _Width; } - } - /// - /// 获取 Node 下边坐标 - /// - public int Bottom { - get { return _Top + _Height; } - } - /// - /// 获取 Node 矩形区域 - /// - public Rectangle Rectangle { - get { - return new Rectangle(this._Left, this._Top, this._Width, this._Height); - } - } - /// - /// 获取 Node 标题矩形区域 - /// - public Rectangle TitleRectangle { - get { - return new Rectangle(this._Left, this._Top, this._Width, this._TitleHeight); - } - } - - private Rectangle _MarkRectangle; - /// - /// 获取 Node 标记矩形区域 - /// - public Rectangle MarkRectangle { - get { return _MarkRectangle; } - } - - private int _TitleHeight = 20; - /// - /// 获取或设置 Node 标题高度 - /// - public int TitleHeight { - get { return _TitleHeight; } - protected set { _TitleHeight = value; } - } - - private STNodeOptionCollection _InputOptions; - /// - /// 获取输入选项集合 - /// - protected internal STNodeOptionCollection InputOptions { - get { return _InputOptions; } - } - /// - /// 获取输入选项集合个数 - /// - public int InputOptionsCount { get { return _InputOptions.Count; } } - - private STNodeOptionCollection _OutputOptions; - /// - /// 获取输出选项 - /// - protected internal STNodeOptionCollection OutputOptions { - get { return _OutputOptions; } - } - /// - /// 获取输出选项个数 - /// - public int OutputOptionsCount { get { return _OutputOptions.Count; } } - - private STNodeControlCollection _Controls; - /// - /// 获取 Node 所包含的控件集合 - /// - protected STNodeControlCollection Controls { - get { return _Controls; } - } - /// - /// 获取 Node 所包含的控件集合个数 - /// - public int ControlsCount { get { return _Controls.Count; } } - /// - /// 获取 Node 坐标位置 - /// - public Point Location { get { return new Point(this._Left, this._Top); } } - /// - /// 获取 Node 大小 - /// - public Size Size { get { return new Size(this._Width, this._Height); } } - - private Font _Font; - /// - /// 获取或设置 Node 字体 - /// - protected Font Font { - get { return _Font; } - set { - if (value == _Font) return; - this._Font.Dispose(); - _Font = value; - } - } - - private bool _LockOption; - /// - /// 获取或设置是否锁定Option选项 锁定后不在接受连接 - /// - public bool LockOption { - get { return _LockOption; } - set { - _LockOption = value; - this.Invalidate(new Rectangle(0, 0, this._Width, this._TitleHeight)); - } - } - - private bool _LockLocation; - /// - /// 获取或设置是否锁定Node位置 锁定后不可移动 - /// - public bool LockLocation { - get { return _LockLocation; } - set { - _LockLocation = value; - this.Invalidate(new Rectangle(0, 0, this._Width, this._TitleHeight)); - } - } - - private ContextMenuStrip _ContextMenuStrip; - /// - /// 获取或设置当前Node 上下文菜单 - /// - public ContextMenuStrip ContextMenuStrip { - get { return _ContextMenuStrip; } - set { _ContextMenuStrip = value; } - } - - private object _Tag; - /// - /// 获取或设置用户自定义保存的数据 - /// - public object Tag { - get { return _Tag; } - set { _Tag = value; } - } - - private Guid _Guid; - /// - /// 获取全局唯一标识 - /// - public Guid Guid { - get { return _Guid; } - } - - private static Point m_static_pt_init = new Point(10, 10); - - public STNode(/*string strTitle, int x, int y*/) { - //this._Title = strTitle; - this._Title = "Untitled"; - this._Height = this._TitleHeight; - this._MarkRectangle.Height = this._Height; - this._Left = this._MarkRectangle.X = m_static_pt_init.X; - this._Top = m_static_pt_init.Y; - this._MarkRectangle.Y = this._Top - 30; - this._InputOptions = new STNodeOptionCollection(this, true); - this._OutputOptions = new STNodeOptionCollection(this, false); - this._Controls = new STNodeControlCollection(this); - this._BackColor = Color.FromArgb(200, 64, 64, 64); - this._TitleColor = Color.FromArgb(200, Color.DodgerBlue); - this._MarkColor = Color.FromArgb(200, Color.Brown); - this._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(0, new float[] { 40 }); - m_static_pt_init.X += 10; - m_static_pt_init.Y += 10; - this._Guid = Guid.NewGuid(); - this.OnCreate(); - } - - private int m_nItemHeight = 20; - protected StringFormat m_sf; - /// - /// 当前Node中 活动的控件 - /// - protected STNodeControl m_ctrl_active; - /// - /// 当前Node中 悬停的控件 - /// - protected STNodeControl m_ctrl_hover; - - protected internal void BuildSize(bool bBuildNode, bool bBuildMark, bool bRedraw) { - if (this._Owner == null) return; - Pen p = new Pen(this._BackColor); - SolidBrush sb = new SolidBrush(this._BackColor); - using (Graphics g = this._Owner.CreateGraphics()) { - DrawingTools dt = new DrawingTools() { - Graphics = g, - Pen = p, - SolidBrush = sb - }; - if (bBuildNode) { - Size sz = this.OnBuildNodeSize(dt); - this._Width = sz.Width; - this._Height = sz.Height; - this.SetOptionLocation(); - this.OnResize(new EventArgs()); - } - if (bBuildMark) { - if (string.IsNullOrEmpty(this._Mark)) return; - this._MarkRectangle = this.OnBuildMarkRectangle(dt); - } - } - if (bRedraw) this._Owner.Invalidate(); - } - - internal Dictionary OnSaveNode() { - Dictionary dic = new Dictionary(); - dic.Add("Guid", this._Guid.ToByteArray()); - dic.Add("Left", BitConverter.GetBytes(this._Left)); - dic.Add("Top", BitConverter.GetBytes(this._Top)); - dic.Add("Mark", string.IsNullOrEmpty(this._Mark) ? new byte[] { 0 } : Encoding.UTF8.GetBytes(this._Mark)); - dic.Add("LockOption", new byte[] { (byte)(this._LockLocation ? 1 : 0) }); - dic.Add("LockLocation", new byte[] { (byte)(this._LockLocation ? 1 : 0) }); - this.OnSaveNode(dic); - return dic; - } - - internal virtual byte[] GetSaveData() { - List lst = new List(); - Type t = this.GetType(); - byte[] byData = Encoding.UTF8.GetBytes(t.Module.Name); - lst.Add((byte)byData.Length); - lst.AddRange(byData); - byData = Encoding.UTF8.GetBytes(t.GUID.ToString()); - lst.Add((byte)byData.Length); - lst.AddRange(byData); - - var dic = this.OnSaveNode(); - if (dic != null) { - foreach (var v in dic) { - byData = Encoding.UTF8.GetBytes(v.Key); - lst.AddRange(BitConverter.GetBytes(byData.Length)); - lst.AddRange(byData); - lst.AddRange(BitConverter.GetBytes(v.Value.Length)); - lst.AddRange(v.Value); - } - } - return lst.ToArray(); - } - - //internal virtual byte[] GetSaveData() { - // List lst = new List(); - // Type t = this.GetType(); - // //lst.AddRange(BitConverter.GetBytes(this._Left)); - // //lst.AddRange(BitConverter.GetBytes(this._Top)); - // byte[] byData = Encoding.UTF8.GetBytes(t.Module.Name); - // lst.Add((byte)byData.Length); - // lst.AddRange(byData); - // byData = Encoding.UTF8.GetBytes(t.FullName); - // lst.Add((byte)byData.Length); - // lst.AddRange(byData); - - // //if (!string.IsNullOrEmpty(this._Mark)) { - // // byData = Encoding.UTF8.GetBytes(this._Mark); - // // lst.AddRange(BitConverter.GetBytes(byData.Length)); - // // lst.AddRange(byData); - // //} else lst.AddRange(new byte[] { 0, 0, 0, 0 }); - // var dic = this.OnSaveNode(); - // if (dic != null) { - // foreach (var v in dic) { - // byData = Encoding.UTF8.GetBytes(v.Key); - // lst.AddRange(BitConverter.GetBytes(byData.Length)); - // lst.AddRange(byData); - // lst.AddRange(BitConverter.GetBytes(v.Value.Length)); - // lst.AddRange(v.Value); - // } - // } - // return lst.ToArray(); - //} - - #region protected - /// - /// 当Node被构造时候发生 - /// - protected virtual void OnCreate() { } - /// - /// 绘制整个Node - /// - /// 绘制工具 - protected internal virtual void OnDrawNode(DrawingTools dt) { - dt.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; - //Fill background - if (this._BackColor.A != 0) { - dt.SolidBrush.Color = this._BackColor; - dt.Graphics.FillRectangle(dt.SolidBrush, this._Left, this._Top + this._TitleHeight, this._Width, this.Height - this._TitleHeight); - } - this.OnDrawTitle(dt); - this.OnDrawBody(dt); - } - /// - /// 绘制Node标题部分 - /// - /// 绘制工具 - protected virtual void OnDrawTitle(DrawingTools dt) { - m_sf.Alignment = StringAlignment.Center; - m_sf.LineAlignment = StringAlignment.Center; - Graphics g = dt.Graphics; - SolidBrush brush = dt.SolidBrush; - if (this._TitleColor.A != 0) { - brush.Color = this._TitleColor; - g.FillRectangle(brush, this.TitleRectangle); - } - if (this._LockOption) { - dt.Pen.Color = this.ForeColor; - int n = this._Top + this._TitleHeight / 2 - 5; - g.DrawRectangle(dt.Pen, this._Left + 3, n + 0, 6, 3); - g.DrawRectangle(dt.Pen, this._Left + 2, n + 3, 8, 6); - g.DrawLine(dt.Pen, this._Left + 6, n + 5, this._Left + 6, n + 7); - - } - if (this._LockLocation) { - dt.Pen.Color = this.ForeColor; - brush.Color = this._ForeColor; - int n = this._Top + this._TitleHeight / 2 - 5; - g.FillRectangle(brush, this.Right - 9, n, 5, 7); - g.DrawLine(dt.Pen, this.Right - 10, n, this.Right - 4, n); - g.DrawLine(dt.Pen, this.Right - 11, n + 6, this.Right - 3, n + 6); - g.DrawLine(dt.Pen, this.Right - 7, n + 7, this.Right - 7, n + 9); - } - if (!string.IsNullOrEmpty(this._Title) && this._ForeColor.A != 0) { - brush.Color = this._ForeColor; - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; - g.DrawString(this._Title, this._Font, brush, this.TitleRectangle, m_sf); - } - } - /// - /// 绘制Node主体部分 除去标题部分 - /// - /// 绘制工具 - protected virtual void OnDrawBody(DrawingTools dt) { - SolidBrush brush = dt.SolidBrush; - foreach (STNodeOption op in this._InputOptions) { - this.OnDrawOptionDot(dt, op); - this.OnDrawOptionText(dt, op); - } - foreach (STNodeOption op in this._OutputOptions) { - this.OnDrawOptionDot(dt, op); - this.OnDrawOptionText(dt, op); - } - if (this._Controls.Count != 0) { //绘制子控件 - //将坐标原点与节点对齐 - dt.Graphics.TranslateTransform(this._Left, this._Top + this._TitleHeight); - Point pt = Point.Empty; //当前需要偏移的量 - Point pt_last = Point.Empty; //最后一个控件相对于节点的坐标 - foreach (STNodeControl v in this._Controls) { - pt.X = v.Left - pt.X; - pt.Y = v.Top - pt.Y; - pt_last = v.Location; - dt.Graphics.TranslateTransform(pt.X, pt.Y); //将原点坐标移动至控件位置 - v.OnPaint(dt); - } - //dt.Graphics.TranslateTransform(-pt_last.X, -pt_last.Y); 还原坐标 - dt.Graphics.TranslateTransform(-this._Left - pt_last.X, -this._Top - this._TitleHeight - pt_last.Y); - } - } - /// - /// 绘制标记信息 - /// - /// 绘制工具 - protected internal virtual void OnDrawMark(DrawingTools dt) { - if (string.IsNullOrEmpty(this._Mark)) return; - Graphics g = dt.Graphics; - SolidBrush brush = dt.SolidBrush; - m_sf.LineAlignment = StringAlignment.Center; - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; - brush.Color = this._MarkColor; - g.FillRectangle(brush, this._MarkRectangle); //填充背景色 - - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; //确定文本绘制所需大小 - var sz = g.MeasureString(this.Mark, this.Font, this._MarkRectangle.Width); - brush.Color = this._ForeColor; - if (sz.Height > m_nItemHeight || sz.Width > this._MarkRectangle.Width) { //如果超过绘图区 则绘制部分 - Rectangle rect = new Rectangle(this._MarkRectangle.Left + 2, this._MarkRectangle.Top + 2, this._MarkRectangle.Width - 20, 16); - m_sf.Alignment = StringAlignment.Near; - g.DrawString(this._MarkLines[0], this._Font, brush, rect, m_sf); - m_sf.Alignment = StringAlignment.Far; - rect.Width = this._MarkRectangle.Width - 5; - g.DrawString("+", this._Font, brush, rect, m_sf); // + 表示超过绘图区 - } else { - m_sf.Alignment = StringAlignment.Near; - g.DrawString(this._MarkLines[0].Trim(), this._Font, brush, this._MarkRectangle, m_sf); - } - } - /// - /// 绘制选项连线的点 - /// - /// 绘制工具 - /// 指定的选项 - protected virtual void OnDrawOptionDot(DrawingTools dt, STNodeOption op) { - Graphics g = dt.Graphics; - Pen pen = dt.Pen; - SolidBrush brush = dt.SolidBrush; - var t = typeof(object); - if (op.DotColor != Color.Transparent) //设置颜色 - brush.Color = op.DotColor; - else { - if (op.DataType == t) - pen.Color = this.Owner.UnknownTypeColor; - else - brush.Color = this.Owner.TypeColor.ContainsKey(op.DataType) ? this.Owner.TypeColor[op.DataType] : this.Owner.UnknownTypeColor; - } - if (op.IsSingle) { //单连接 圆形 - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; - if (op.DataType == t) { //未知类型绘制 否则填充 - g.DrawEllipse(pen, op.DotRectangle.X, op.DotRectangle.Y, op.DotRectangle.Width - 1, op.DotRectangle.Height - 1); - } else - g.FillEllipse(brush, op.DotRectangle); - } else { //多连接 矩形 - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; - if (op.DataType == t) { - g.DrawRectangle(pen, op.DotRectangle.X, op.DotRectangle.Y, op.DotRectangle.Width - 1, op.DotRectangle.Height - 1); - } else - g.FillRectangle(brush, op.DotRectangle); - } - } - /// - /// 绘制选项的文本 - /// - /// 绘制工具 - /// 指定的选项 - protected virtual void OnDrawOptionText(DrawingTools dt, STNodeOption op) { - Graphics g = dt.Graphics; - SolidBrush brush = dt.SolidBrush; - if (op.IsInput) { - m_sf.Alignment = StringAlignment.Near; - } else { - m_sf.Alignment = StringAlignment.Far; - } - brush.Color = op.TextColor; - g.DrawString(op.Text, this.Font, brush, op.TextRectangle, m_sf); - } - /// - /// 当计算Option连线点位置时候发生 - /// - /// 需要计算的Option - /// 自动计算出的位置 - /// 新的位置 - protected virtual Point OnSetOptionDotLocation(STNodeOption op, Point pt) { - return pt; - } - /// - /// 当计算Option文本区域时候发生 - /// - /// 需要计算的Option - /// 自动计算出的区域 - /// 新的区域 - protected virtual Rectangle OnSetOptionTextRectangle(STNodeOption op, Rectangle rect) { - return rect; - } - /// - /// 计算当前Node所需要的矩形区域 - /// 若需要自己重绘Node 则应当重写此函数 以确定绘图区域大小 - /// 返回的大小并不会限制绘制区域 任然可以在此区域之外绘制 - /// 但是并不会被STNodeEditor所接受 并触发对应事件 - /// - protected virtual Size OnBuildNodeSize(DrawingTools dt) { - int nInputHeight = 0, nOutputHeight = 0; - foreach (STNodeOption op in this._InputOptions) nInputHeight += m_nItemHeight; - foreach (STNodeOption op in this._OutputOptions) nOutputHeight += m_nItemHeight; - int nHeight = this._TitleHeight + (nInputHeight > nOutputHeight ? nInputHeight : nOutputHeight); - - SizeF szf_input = SizeF.Empty, szf_output = SizeF.Empty; - foreach (STNodeOption v in this._InputOptions) { - if (string.IsNullOrEmpty(v.Text)) continue; - SizeF szf = dt.Graphics.MeasureString(v.Text, this._Font); - if (szf.Width > szf_input.Width) szf_input = szf; - } - foreach (STNodeOption v in this._OutputOptions) { - if (string.IsNullOrEmpty(v.Text)) continue; - SizeF szf = dt.Graphics.MeasureString(v.Text, this._Font); - if (szf.Width > szf_output.Width) szf_output = szf; - } - int nWidth = (int)(szf_input.Width + szf_output.Width + 25); - if (!string.IsNullOrEmpty(this.Title)) szf_input = dt.Graphics.MeasureString(this.Title, this.Font); - if (szf_input.Width + 30 > nWidth) nWidth = (int)szf_input.Width + 30; - return new Size(nWidth, nHeight); - } - /// - /// 计算当前Mark所需要的矩形区域 - /// 若需要自己重绘Mark 则应当重写此函数 以确定绘图区域大小 - /// 返回的大小并不会限制绘制区域 任然可以在此区域之外绘制 - /// 但是并不会被STNodeEditor所接受 并触发对应事件 - /// - protected virtual Rectangle OnBuildMarkRectangle(DrawingTools dt) { - //if (string.IsNullOrEmpty(this._Mark)) return Rectangle.Empty; - return new Rectangle(this._Left, this._Top - 30, this._Width, 20); - } - /// - /// 当需要保存时候 此Node有哪些需要额外保存的数据 - /// 注意: 保存时并不会进行序列化 还原时候仅重新通过空参数构造器创建此Node - /// 然后调用 OnLoadNode() 将保存的数据进行还原 - /// - /// 需要保存的数据 - protected virtual void OnSaveNode(Dictionary dic) { } - /// - /// 当还原该节点时候会将 OnSaveNode() 所返回的数据重新传入此函数 - /// - /// 保存时候的数据 - protected internal virtual void OnLoadNode(Dictionary dic) { - if (dic.ContainsKey("Guid")) this._Guid = new Guid(dic["Guid"]); - if (dic.ContainsKey("Left")) this._Left = BitConverter.ToInt32(dic["Left"], 0); - if (dic.ContainsKey("Top")) this._Top = BitConverter.ToInt32(dic["Top"], 0); - if (dic.ContainsKey("Mark")) { - string strText = Encoding.UTF8.GetString(dic["Mark"]); - if (strText != "\0") this.Mark = strText; - } - if (dic.ContainsKey("LockOption")) this._LockOption = dic["LockOption"][0] == 1; - if (dic.ContainsKey("LockLocation")) this._LockLocation = dic["LockLocation"][0] == 1; - } - /// - /// 当编辑器加载完成所有的节点时候发生 - /// - protected internal virtual void OnEditorLoadCompleted() { - - } - - //[event]===========================[event]==============================[event]============================[event] - - 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(MouseEventArgs e) { - Point pt = e.Location; - pt.Y -= this._TitleHeight; - for (int i = this._Controls.Count - 1; i >= 0; i--) { - var c = this._Controls[i]; - if (c.DisplayRectangle.Contains(pt)) { - c.OnMouseDown(new MouseEventArgs(e.Button, e.Clicks, e.X - c.Left, pt.Y - c.Top, e.Delta)); - if (m_ctrl_active != c) { - c.OnGotFocus(new EventArgs()); - if (m_ctrl_active != null) m_ctrl_active.OnLostFocus(new EventArgs()); - m_ctrl_active = c; - } - return; - } - } - if (m_ctrl_active != null) m_ctrl_active.OnLostFocus(new EventArgs()); - m_ctrl_active = null; - } - - protected internal virtual void OnMouseMove(MouseEventArgs e) { - Point pt = e.Location; - pt.Y -= this._TitleHeight; - for (int i = this._Controls.Count - 1; i >= 0; i--) { - var c = this._Controls[i]; - if (c.DisplayRectangle.Contains(pt)) { - if (m_ctrl_hover != this._Controls[i]) { - c.OnMouseEnter(new EventArgs()); - if (m_ctrl_hover != null) m_ctrl_hover.OnMouseLeave(new EventArgs()); - m_ctrl_hover = c; - } - m_ctrl_hover.OnMouseMove(new MouseEventArgs(e.Button, e.Clicks, e.X - c.Left, pt.Y - c.Top, e.Delta)); - return; - } - } - if (m_ctrl_hover != null) m_ctrl_hover.OnMouseLeave(new EventArgs()); - m_ctrl_hover = null; - } - - protected internal virtual void OnMouseUp(MouseEventArgs e) { - Point pt = e.Location; - pt.Y -= this._TitleHeight; - if (m_ctrl_active != null) { - m_ctrl_active.OnMouseUp(new MouseEventArgs(e.Button, e.Clicks, - e.X - m_ctrl_active.Left, pt.Y - m_ctrl_active.Top, e.Delta)); - } - //for (int i = this._Controls.Count - 1; i >= 0; i--) { - // var c = this._Controls[i]; - // if (c.DisplayRectangle.Contains(pt)) { - // c.OnMouseUp(new MouseEventArgs(e.Button, e.Clicks, e.X - c.Left, pt.Y - c.Top, e.Delta)); - // return; - // } - //} - } - - protected internal virtual void OnMouseLeave(EventArgs e) { - if (m_ctrl_hover != null) m_ctrl_hover.OnMouseLeave(e); - m_ctrl_hover = null; - } - - protected internal virtual void OnMouseClick(MouseEventArgs e) { - Point pt = e.Location; - pt.Y -= this._TitleHeight; - if (m_ctrl_active != null) - m_ctrl_active.OnMouseClick(new MouseEventArgs(e.Button, e.Clicks, e.X - m_ctrl_active.Left, pt.Y - m_ctrl_active.Top, e.Delta)); - } - - protected internal virtual void OnMouseWheel(MouseEventArgs e) { - Point pt = e.Location; - pt.Y -= this._TitleHeight; - if (m_ctrl_hover != null) { - m_ctrl_hover.OnMouseWheel(new MouseEventArgs(e.Button, e.Clicks, e.X - m_ctrl_hover.Left, pt.Y - m_ctrl_hover.Top, e.Delta)); - return; - } - } - protected internal virtual void OnMouseHWheel(MouseEventArgs e) { - if (m_ctrl_hover != null) { - m_ctrl_hover.OnMouseHWheel(e); - return; - } - } - - protected internal virtual void OnKeyDown(KeyEventArgs e) { - if (m_ctrl_active != null) m_ctrl_active.OnKeyDown(e); - } - protected internal virtual void OnKeyUp(KeyEventArgs e) { - if (m_ctrl_active != null) m_ctrl_active.OnKeyUp(e); - } - protected internal virtual void OnKeyPress(KeyPressEventArgs e) { - if (m_ctrl_active != null) m_ctrl_active.OnKeyPress(e); - } - - protected internal virtual void OnMove(EventArgs e) { this.SetOptionLocation(); } - protected internal virtual void OnResize(EventArgs e) { this.SetOptionLocation(); } - - - /// - /// 当所有者发生改变时候发生 - /// - protected virtual void OnOwnerChanged() { } - /// - /// 当选中状态改变时候发生 - /// - protected virtual void OnSelectedChanged() { } - /// - /// 当活动状态改变时候发生 - /// - protected virtual void OnActiveChanged() { } - - #endregion protected - - private void SetOptionLocation() { - Rectangle rect = new Rectangle(this.Left + 10, this._Top + this._TitleHeight, this._Width - 20, m_nItemHeight); - foreach (STNodeOption op in this._InputOptions) { - Point pt = this.OnSetOptionDotLocation(op, new Point(this.Left - 5, rect.Y + 5)); - op.TextRectangle = this.OnSetOptionTextRectangle(op, rect); - op.DotLeft = pt.X; - op.DotTop = pt.Y; - rect.Y += m_nItemHeight; - } - rect.Y = this._Top + this._TitleHeight; - m_sf.Alignment = StringAlignment.Far; - foreach (STNodeOption op in this._OutputOptions) { - Point pt = this.OnSetOptionDotLocation(op, new Point(this._Left + this._Width - 5, rect.Y + 5)); - op.TextRectangle = this.OnSetOptionTextRectangle(op, rect); - op.DotLeft = pt.X; - op.DotTop = pt.Y; - rect.Y += m_nItemHeight; - } - } - - /// - /// 重绘Node - /// - public void Invalidate() { - if (this._Owner != null) { - this._Owner.Invalidate(this._Owner.CanvasToControl(new Rectangle(this._Left - 5, this._Top - 5, this._Width + 10, this._Height + 10))); - } - } - /// - /// 重绘 Node 指定区域 - /// - /// Node 指定区域 - public void Invalidate(Rectangle rect) { - rect.X += this._Left; - rect.Y += this._Top; - if (this._Owner != null) { - this._Owner.Invalidate(this._Owner.CanvasToControl(rect)); - } - } - /// - /// 获取此Node所包含的输入Option集合 - /// - /// Option集合 - public STNodeOption[] GetInputOptions() { - STNodeOption[] ops = new STNodeOption[this._InputOptions.Count]; - for (int i = 0; i < this._InputOptions.Count; i++) ops[i] = this._InputOptions[i]; - return ops; - } - /// - /// 获取此Node所包含的输出Option集合 - /// - /// Option集合 - public STNodeOption[] GetOutputOptions() { - STNodeOption[] ops = new STNodeOption[this._OutputOptions.Count]; - for (int i = 0; i < this._OutputOptions.Count; i++) ops[i] = this._OutputOptions[i]; - return ops; - } - /// - /// 设置Node的选中状态 - /// - /// 是否选中 - /// 是否重绘 - public void SetSelected(bool bSelected, bool bRedraw) { - if (this._IsSelected == bSelected) return; - this._IsSelected = bSelected; - if (bRedraw) this.Invalidate(); - this.OnSelectedChanged(); - } - public IAsyncResult BeginInvoke(Delegate method) { return this.BeginInvoke(method, null); } - public IAsyncResult BeginInvoke(Delegate method, params object[] args) { - if (this._Owner == null) return null; - return this._Owner.BeginInvoke(method, args); - } - public object Invoke(Delegate method) { return this.Invoke(method, null); } - public object Invoke(Delegate method, params object[] args) { - if (this._Owner == null) return null; - return this._Owner.Invoke(method, args); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; +using System.Windows.Forms; +using System.Collections; +/* +MIT License + +Copyright (c) 2021 DebugST@crystal_lz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +/* + * create: 2021-12-08 + * modify: 2021-03-02 + * Author: Crystal_lz + * blog: http://st233.com + * Gitee: https://gitee.com/DebugST + * Github: https://github.com/DebugST + */ +namespace ST.Library.UI.NodeEditor +{ + public abstract class STNode + { + private STNodeEditor _Owner; + /// + /// 获取当前 Node 所有者 + /// + public STNodeEditor Owner { + get { return _Owner; } + internal set { + if (value == _Owner) return; + if (_Owner != null) { + foreach (STNodeOption op in this._InputOptions.ToArray()) op.DisConnectionAll(); + foreach (STNodeOption op in this._OutputOptions.ToArray()) op.DisConnectionAll(); + } + _Owner = value; + if (!this._AutoSize) this.SetOptionsLocation(); + this.BuildSize(true, true, false); + this.OnOwnerChanged(); + } + } + + private bool _IsSelected; + /// + /// 获取或设置 Node 是否处于被选中状态 + /// + public bool IsSelected { + get { return _IsSelected; } + set { + if (value == _IsSelected) return; + _IsSelected = value; + this.Invalidate(); + this.OnSelectedChanged(); + if (this._Owner != null) this._Owner.OnSelectedChanged(EventArgs.Empty); + } + } + + private bool _IsActive; + /// + /// 获取 Node 是否处于活动状态 + /// + public bool IsActive { + get { return _IsActive; } + internal set { + if (value == _IsActive) return; + _IsActive = value; + this.OnActiveChanged(); + } + } + + private Color _TitleColor; + /// + /// 获取或设置标题背景颜色 + /// + public Color TitleColor { + get { return _TitleColor; } + protected set { + _TitleColor = value; + this.Invalidate(new Rectangle(0, 0, this._Width, this._TitleHeight)); + } + } + + private Color _MarkColor; + /// + /// 获取或设置标记信息背景颜色 + /// + public Color MarkColor { + get { return _MarkColor; } + protected set { + _MarkColor = value; + this.Invalidate(this._MarkRectangle); + } + } + + private Color _ForeColor = Color.White; + /// + /// 获取或设置当前 Node 前景色 + /// + public Color ForeColor { + get { return _ForeColor; } + protected set { + _ForeColor = value; + this.Invalidate(); + } + } + + private Color _BackColor; + /// + /// 获取或设置当前 Node 背景色 + /// + public Color BackColor { + get { return _BackColor; } + protected set { + _BackColor = value; + this.Invalidate(); + } + } + + private string _Title; + /// + /// 获取或设置 Node 标题 + /// + public string Title { + get { return _Title; } + protected set { + _Title = value; + if (this._AutoSize) this.BuildSize(true, true, true); + //this.Invalidate(this.TitleRectangle); + } + } + + private string _Mark; + /// + /// 获取或设置 Node 标记信息 + /// + 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(); + this.Invalidate(new Rectangle(-5, -5, this._MarkRectangle.Width + 10, this._MarkRectangle.Height + 10)); + } + } + + private string[] _MarkLines;//单独存放行数据 不用每次在绘制中去拆分 + /// + /// 获取 Node 标记信息行数据 + /// + public string[] MarkLines { + get { return _MarkLines; } + } + + private int _Left; + /// + /// 获取或设置 Node 左边坐标 + /// + public int Left { + get { return _Left; } + set { + if (this._LockLocation || value == _Left) return; + _Left = value; + this.SetOptionsLocation(); + this.BuildSize(false, true, false); + this.OnMove(EventArgs.Empty); + if (this._Owner != null) { + this._Owner.BuildLinePath(); + this._Owner.BuildBounds(); + } + } + } + + private int _Top; + /// + /// 获取或设置 Node 上边坐标 + /// + public int Top { + get { return _Top; } + set { + if (this._LockLocation || value == _Top) return; + _Top = value; + this.SetOptionsLocation(); + this.BuildSize(false, true, false); + this.OnMove(EventArgs.Empty); + if (this._Owner != null) { + this._Owner.BuildLinePath(); + this._Owner.BuildBounds(); + } + } + } + + private int _Width = 100; + /// + /// 获取或设置 Node 宽度 当AutoSize被设置时 无法设置此值 + /// + public int Width { + get { return _Width; } + protected set { + if (value < 50) return; + if (this._AutoSize || value == _Width) return; + _Width = value; + this.SetOptionsLocation(); + this.BuildSize(false, true, false); + this.OnResize(EventArgs.Empty); + if (this._Owner != null) { + this._Owner.BuildLinePath(); + this._Owner.BuildBounds(); + } + this.Invalidate(); + } + } + + private int _Height = 40; + /// + /// 获取或设置 Node 高度 当AutoSize被设置时 无法设置此值 + /// + public int Height { + get { return _Height; } + protected set { + if (value < 40) return; + if (this._AutoSize || value == _Height) return; + _Height = value; + this.SetOptionsLocation(); + this.BuildSize(false, true, false); + this.OnResize(EventArgs.Empty); + if (this._Owner != null) { + this._Owner.BuildLinePath(); + this._Owner.BuildBounds(); + } + this.Invalidate(); + } + } + + private int _ItemHeight = 20; + /// + /// 获取或设置 Node 每个选项的高度 + /// + 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 (this._AutoSize) { + this.BuildSize(true, false, true); + } else { + this.SetOptionsLocation(); + if (this._Owner != null) this._Owner.Invalidate(); + } + } + } + + private bool _AutoSize = true; + /// + /// 获取或设置 Node 是否自动计算宽高 + /// + public bool AutoSize { + get { return _AutoSize; } + protected set { _AutoSize = value; } + } + /// + /// 获取 Node 右边边坐标 + /// + public int Right { + get { return _Left + _Width; } + } + /// + /// 获取 Node 下边坐标 + /// + public int Bottom { + get { return _Top + _Height; } + } + /// + /// 获取 Node 矩形区域 + /// + public Rectangle Rectangle { + get { + return new Rectangle(this._Left, this._Top, this._Width, this._Height); + } + } + /// + /// 获取 Node 标题矩形区域 + /// + public Rectangle TitleRectangle { + get { + return new Rectangle(this._Left, this._Top, this._Width, this._TitleHeight); + } + } + + private Rectangle _MarkRectangle; + /// + /// 获取 Node 标记矩形区域 + /// + public Rectangle MarkRectangle { + get { return _MarkRectangle; } + } + + private int _TitleHeight = 20; + /// + /// 获取或设置 Node 标题高度 + /// + public int TitleHeight { + get { return _TitleHeight; } + protected set { _TitleHeight = value; } + } + + private STNodeOptionCollection _InputOptions; + /// + /// 获取输入选项集合 + /// + protected internal STNodeOptionCollection InputOptions { + get { return _InputOptions; } + } + /// + /// 获取输入选项集合个数 + /// + public int InputOptionsCount { get { return _InputOptions.Count; } } + + private STNodeOptionCollection _OutputOptions; + /// + /// 获取输出选项 + /// + protected internal STNodeOptionCollection OutputOptions { + get { return _OutputOptions; } + } + /// + /// 获取输出选项个数 + /// + public int OutputOptionsCount { get { return _OutputOptions.Count; } } + + private STNodeControlCollection _Controls; + /// + /// 获取 Node 所包含的控件集合 + /// + protected STNodeControlCollection Controls { + get { return _Controls; } + } + /// + /// 获取 Node 所包含的控件集合个数 + /// + public int ControlsCount { get { return _Controls.Count; } } + /// + /// 获取 Node 坐标位置 + /// + public Point Location { + get { return new Point(this._Left, this._Top); } + set { + this.Left = value.X; + this.Top = value.Y; + } + } + /// + /// 获取 Node 大小 + /// + public Size Size { + get { return new Size(this._Width, this._Height); } + set { + this.Width = value.Width; + this.Height = value.Height; + } + } + + private Font _Font; + /// + /// 获取或设置 Node 字体 + /// + protected Font Font { + get { return _Font; } + set { + if (value == _Font) return; + this._Font.Dispose(); + _Font = value; + } + } + + private bool _LockOption; + /// + /// 获取或设置是否锁定Option选项 锁定后不在接受连接 + /// + public bool LockOption { + get { return _LockOption; } + set { + _LockOption = value; + this.Invalidate(new Rectangle(0, 0, this._Width, this._TitleHeight)); + } + } + + private bool _LockLocation; + /// + /// 获取或设置是否锁定Node位置 锁定后不可移动 + /// + public bool LockLocation { + get { return _LockLocation; } + set { + _LockLocation = value; + this.Invalidate(new Rectangle(0, 0, this._Width, this._TitleHeight)); + } + } + + private ContextMenuStrip _ContextMenuStrip; + /// + /// 获取或设置当前Node 上下文菜单 + /// + public ContextMenuStrip ContextMenuStrip { + get { return _ContextMenuStrip; } + set { _ContextMenuStrip = value; } + } + + private object _Tag; + /// + /// 获取或设置用户自定义保存的数据 + /// + public object Tag { + get { return _Tag; } + set { _Tag = value; } + } + + private Guid _Guid; + /// + /// 获取全局唯一标识 + /// + public Guid Guid { + get { return _Guid; } + } + + private bool _LetGetOptions = false; + /// + /// 获取或设置是否允许外部访问STNodeOption + /// + public bool LetGetOptions { + get { return _LetGetOptions; } + protected set { _LetGetOptions = value; } + } + + private static Point m_static_pt_init = new Point(10, 10); + + public STNode() { + this._Title = "Untitled"; + this._MarkRectangle.Height = this._Height; + this._Left = this._MarkRectangle.X = m_static_pt_init.X; + this._Top = m_static_pt_init.Y; + this._MarkRectangle.Y = this._Top - 30; + this._InputOptions = new STNodeOptionCollection(this, true); + this._OutputOptions = new STNodeOptionCollection(this, false); + this._Controls = new STNodeControlCollection(this); + this._BackColor = Color.FromArgb(200, 64, 64, 64); + this._TitleColor = Color.FromArgb(200, Color.DodgerBlue); + this._MarkColor = Color.FromArgb(200, Color.Brown); + this._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(0, new float[] { 40 }); + m_static_pt_init.X += 10; + m_static_pt_init.Y += 10; + this._Guid = Guid.NewGuid(); + this.OnCreate(); + } + + //private int m_nItemHeight = 30; + protected StringFormat m_sf; + /// + /// 当前Node中 活动的控件 + /// + protected STNodeControl m_ctrl_active; + /// + /// 当前Node中 悬停的控件 + /// + protected STNodeControl m_ctrl_hover; + /// + /// 当前Node中 鼠标点下的控件 + /// + protected STNodeControl m_ctrl_down; + + protected internal void BuildSize(bool bBuildNode, bool bBuildMark, bool bRedraw) { + if (this._Owner == null) return; + using (Graphics g = this._Owner.CreateGraphics()) { + if (this._AutoSize && bBuildNode) { + Size sz = this.GetDefaultNodeSize(g); + if (this._Width != sz.Width || this._Height != sz.Height) { + this._Width = sz.Width; + this._Height = sz.Height; + this.SetOptionsLocation(); + this.OnResize(EventArgs.Empty); + } + } + if (bBuildMark && !string.IsNullOrEmpty(this._Mark)) { + this._MarkRectangle = this.OnBuildMarkRectangle(g); + } + } + if (bRedraw) this._Owner.Invalidate(); + } + + internal Dictionary OnSaveNode() { + Dictionary dic = new Dictionary(); + dic.Add("Guid", this._Guid.ToByteArray()); + dic.Add("Left", BitConverter.GetBytes(this._Left)); + dic.Add("Top", BitConverter.GetBytes(this._Top)); + dic.Add("Width", BitConverter.GetBytes(this._Width)); + dic.Add("Height", BitConverter.GetBytes(this._Height)); + dic.Add("AutoSize", new byte[] { (byte)(this._AutoSize ? 1 : 0) }); + if (this._Mark != null) dic.Add("Mark", Encoding.UTF8.GetBytes(this._Mark)); + dic.Add("LockOption", new byte[] { (byte)(this._LockLocation ? 1 : 0) }); + dic.Add("LockLocation", new byte[] { (byte)(this._LockLocation ? 1 : 0) }); + Type t = this.GetType(); + foreach (var p in t.GetProperties()) { + var attrs = p.GetCustomAttributes(true); + foreach (var a in attrs) { + if (!(a is STNodePropertyAttribute)) continue; + var attr = a as STNodePropertyAttribute; + object obj = Activator.CreateInstance(attr.DescriptorType); + if (!(obj is STNodePropertyDescriptor)) + throw new InvalidOperationException("[STNodePropertyAttribute.Type]参数值必须为[STNodePropertyDescriptor]或者其子类的类型"); + var desc = (STNodePropertyDescriptor)Activator.CreateInstance(attr.DescriptorType); + desc.Node = this; + desc.PropertyInfo = p; + byte[] byData = desc.GetBytesFromValue(); + if (byData == null) continue; + dic.Add(p.Name, byData); + } + } + this.OnSaveNode(dic); + return dic; + } + + internal byte[] GetSaveData() { + List lst = new List(); + Type t = this.GetType(); + byte[] byData = Encoding.UTF8.GetBytes(t.Module.Name + "|" + t.FullName); + lst.Add((byte)byData.Length); + lst.AddRange(byData); + byData = Encoding.UTF8.GetBytes(t.GUID.ToString()); + lst.Add((byte)byData.Length); + lst.AddRange(byData); + + var dic = this.OnSaveNode(); + if (dic != null) { + foreach (var v in dic) { + byData = Encoding.UTF8.GetBytes(v.Key); + lst.AddRange(BitConverter.GetBytes(byData.Length)); + lst.AddRange(byData); + lst.AddRange(BitConverter.GetBytes(v.Value.Length)); + lst.AddRange(v.Value); + } + } + return lst.ToArray(); + } + + #region protected + /// + /// 当Node被构造时候发生 + /// + protected virtual void OnCreate() { } + /// + /// 绘制整个Node + /// + /// 绘制工具 + protected internal virtual void OnDrawNode(DrawingTools dt) { + dt.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; + //Fill background + if (this._BackColor.A != 0) { + dt.SolidBrush.Color = this._BackColor; + dt.Graphics.FillRectangle(dt.SolidBrush, this._Left, this._Top + this._TitleHeight, this._Width, this.Height - this._TitleHeight); + } + this.OnDrawTitle(dt); + this.OnDrawBody(dt); + } + /// + /// 绘制Node标题部分 + /// + /// 绘制工具 + protected virtual void OnDrawTitle(DrawingTools dt) { + m_sf.Alignment = StringAlignment.Center; + m_sf.LineAlignment = StringAlignment.Center; + Graphics g = dt.Graphics; + SolidBrush brush = dt.SolidBrush; + if (this._TitleColor.A != 0) { + brush.Color = this._TitleColor; + g.FillRectangle(brush, this.TitleRectangle); + } + if (this._LockOption) { + //dt.Pen.Color = this.ForeColor; + brush.Color = this._ForeColor; + int n = this._Top + this._TitleHeight / 2 - 5; + g.FillRectangle(dt.SolidBrush, this._Left + 4, n + 0, 2, 4); + g.FillRectangle(dt.SolidBrush, this._Left + 6, n + 0, 2, 2); + g.FillRectangle(dt.SolidBrush, this._Left + 8, n + 0, 2, 4); + g.FillRectangle(dt.SolidBrush, this._Left + 3, n + 4, 8, 6); + //g.DrawLine(dt.Pen, this._Left + 6, n + 5, this._Left + 6, n + 7); + //g.DrawRectangle(dt.Pen, this._Left + 3, n + 0, 6, 3); + //g.DrawRectangle(dt.Pen, this._Left + 2, n + 3, 8, 6); + //g.DrawLine(dt.Pen, this._Left + 6, n + 5, this._Left + 6, n + 7); + + } + if (this._LockLocation) { + //dt.Pen.Color = this.ForeColor; + brush.Color = this._ForeColor; + int n = this._Top + this._TitleHeight / 2 - 5; + g.FillRectangle(brush, this.Right - 9, n, 4, 4); + g.FillRectangle(brush, this.Right - 11, n + 4, 8, 2); + g.FillRectangle(brush, this.Right - 8, n + 6, 2, 4); + //g.DrawLine(dt.Pen, this.Right - 10, n + 6, this.Right - 4, n + 6); + //g.DrawLine(dt.Pen, this.Right - 10, n, this.Right - 4, n); + //g.DrawLine(dt.Pen, this.Right - 11, n + 6, this.Right - 3, n + 6); + //g.DrawLine(dt.Pen, this.Right - 7, n + 7, this.Right - 7, n + 9); + } + if (!string.IsNullOrEmpty(this._Title) && this._ForeColor.A != 0) { + brush.Color = this._ForeColor; + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; + g.DrawString(this._Title, this._Font, brush, this.TitleRectangle, m_sf); + } + } + /// + /// 绘制Node主体部分 除去标题部分 + /// + /// 绘制工具 + protected virtual void OnDrawBody(DrawingTools dt) { + SolidBrush brush = dt.SolidBrush; + foreach (STNodeOption op in this._InputOptions) { + if (op == STNodeOption.Empty) continue; + this.OnDrawOptionDot(dt, op); + this.OnDrawOptionText(dt, op); + } + foreach (STNodeOption op in this._OutputOptions) { + if (op == STNodeOption.Empty) continue; + this.OnDrawOptionDot(dt, op); + this.OnDrawOptionText(dt, op); + } + if (this._Controls.Count != 0) { //绘制子控件 + //将坐标原点与节点对齐 + //dt.Graphics.ResetTransform(); + dt.Graphics.TranslateTransform(this._Left, this._Top + this._TitleHeight); + Point pt = Point.Empty; //当前需要偏移的量 + Point pt_last = Point.Empty; //最后一个控件相对于节点的坐标 + foreach (STNodeControl v in this._Controls) { + if (!v.Visable) continue; + pt.X = v.Left - pt_last.X; + pt.Y = v.Top - pt_last.Y; + pt_last = v.Location; + dt.Graphics.TranslateTransform(pt.X, pt.Y); //将原点坐标移动至控件位置 + dt.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; + v.OnPaint(dt); + } + //dt.Graphics.TranslateTransform(-pt_last.X, -pt_last.Y); 还原坐标 + dt.Graphics.TranslateTransform(-this._Left - pt_last.X, -this._Top - this._TitleHeight - pt_last.Y); + //dt.Graphics. + } + } + /// + /// 绘制标记信息 + /// + /// 绘制工具 + protected internal virtual void OnDrawMark(DrawingTools dt) { + if (string.IsNullOrEmpty(this._Mark)) return; + Graphics g = dt.Graphics; + SolidBrush brush = dt.SolidBrush; + m_sf.LineAlignment = StringAlignment.Center; + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; + brush.Color = this._MarkColor; + g.FillRectangle(brush, this._MarkRectangle); //填充背景色 + + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; //确定文本绘制所需大小 + var sz = g.MeasureString(this.Mark, this.Font, this._MarkRectangle.Width); + brush.Color = this._ForeColor; + if (sz.Height > this._ItemHeight || sz.Width > this._MarkRectangle.Width) { //如果超过绘图区 则绘制部分 + Rectangle rect = new Rectangle(this._MarkRectangle.Left + 2, this._MarkRectangle.Top + 2, this._MarkRectangle.Width - 20, 16); + m_sf.Alignment = StringAlignment.Near; + g.DrawString(this._MarkLines[0], this._Font, brush, rect, m_sf); + m_sf.Alignment = StringAlignment.Far; + rect.Width = this._MarkRectangle.Width - 5; + g.DrawString("+", this._Font, brush, rect, m_sf); // + 表示超过绘图区 + } else { + m_sf.Alignment = StringAlignment.Near; + g.DrawString(this._MarkLines[0].Trim(), this._Font, brush, this._MarkRectangle, m_sf); + } + } + /// + /// 绘制选项连线的点 + /// + /// 绘制工具 + /// 指定的选项 + protected virtual void OnDrawOptionDot(DrawingTools dt, STNodeOption op) { + Graphics g = dt.Graphics; + Pen pen = dt.Pen; + SolidBrush brush = dt.SolidBrush; + var t = typeof(object); + if (op.DotColor != Color.Transparent) //设置颜色 + brush.Color = op.DotColor; + else { + if (op.DataType == t) + pen.Color = this.Owner.UnknownTypeColor; + else + brush.Color = this.Owner.TypeColor.ContainsKey(op.DataType) ? this.Owner.TypeColor[op.DataType] : this.Owner.UnknownTypeColor; + } + if (op.IsSingle) { //单连接 圆形 + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; + if (op.DataType == t) { //未知类型绘制 否则填充 + g.DrawEllipse(pen, op.DotRectangle.X, op.DotRectangle.Y, op.DotRectangle.Width - 1, op.DotRectangle.Height - 1); + } else + g.FillEllipse(brush, op.DotRectangle); + } else { //多连接 矩形 + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; + if (op.DataType == t) { + g.DrawRectangle(pen, op.DotRectangle.X, op.DotRectangle.Y, op.DotRectangle.Width - 1, op.DotRectangle.Height - 1); + } else + g.FillRectangle(brush, op.DotRectangle); + } + } + /// + /// 绘制选项的文本 + /// + /// 绘制工具 + /// 指定的选项 + protected virtual void OnDrawOptionText(DrawingTools dt, STNodeOption op) { + Graphics g = dt.Graphics; + SolidBrush brush = dt.SolidBrush; + if (op.IsInput) { + m_sf.Alignment = StringAlignment.Near; + } else { + m_sf.Alignment = StringAlignment.Far; + } + brush.Color = op.TextColor; + g.DrawString(op.Text, this.Font, brush, op.TextRectangle, m_sf); + } + /// + /// 当计算Option连线点位置时候发生 + /// + /// 需要计算的Option + /// 自动计算出的位置 + /// 当前Option的索引 + /// 新的位置 + protected virtual Point OnSetOptionDotLocation(STNodeOption op, Point pt, int nIndex) { + return pt; + } + /// + /// 当计算Option文本区域时候发生 + /// + /// 需要计算的Option + /// 自动计算出的区域 + /// 当前Option的索引 + /// 新的区域 + protected virtual Rectangle OnSetOptionTextRectangle(STNodeOption op, Rectangle rect, int nIndex) { + return rect; + } + /// + /// 获取当前STNode所需要的默认大小 + /// 返回的大小并不会限制绘制区域 任然可以在此区域之外绘制 + /// 但是并不会被STNodeEditor所接受 并触发对应事件 + /// + /// 绘图面板 + /// 计算出来的大小 + protected virtual Size GetDefaultNodeSize(Graphics g) { + int nInputHeight = 0, nOutputHeight = 0; + foreach (STNodeOption op in this._InputOptions) nInputHeight += this._ItemHeight; + foreach (STNodeOption op in this._OutputOptions) nOutputHeight += this._ItemHeight; + int nHeight = this._TitleHeight + (nInputHeight > nOutputHeight ? nInputHeight : nOutputHeight); + + SizeF szf_input = SizeF.Empty, szf_output = SizeF.Empty; + foreach (STNodeOption v in this._InputOptions) { + if (string.IsNullOrEmpty(v.Text)) continue; + SizeF szf = g.MeasureString(v.Text, this._Font); + if (szf.Width > szf_input.Width) szf_input = szf; + } + foreach (STNodeOption v in this._OutputOptions) { + if (string.IsNullOrEmpty(v.Text)) continue; + SizeF szf = g.MeasureString(v.Text, this._Font); + if (szf.Width > szf_output.Width) szf_output = szf; + } + int nWidth = (int)(szf_input.Width + szf_output.Width + 25); + if (!string.IsNullOrEmpty(this.Title)) szf_input = g.MeasureString(this.Title, this.Font); + if (szf_input.Width + 30 > nWidth) nWidth = (int)szf_input.Width + 30; + return new Size(nWidth, nHeight); + } + /// + /// 计算当前Mark所需要的矩形区域 + /// 返回的大小并不会限制绘制区域 任然可以在此区域之外绘制 + /// 但是并不会被STNodeEditor所接受 并触发对应事件 + /// + /// 绘图面板 + /// 计算后的区域 + protected virtual Rectangle OnBuildMarkRectangle(Graphics g) { + //if (string.IsNullOrEmpty(this._Mark)) return Rectangle.Empty; + return new Rectangle(this._Left, this._Top - 30, this._Width, 20); + } + /// + /// 当需要保存时候 此Node有哪些需要额外保存的数据 + /// 注意: 保存时并不会进行序列化 还原时候仅重新通过空参数构造器创建此Node + /// 然后调用 OnLoadNode() 将保存的数据进行还原 + /// + /// 需要保存的数据 + protected virtual void OnSaveNode(Dictionary dic) { } + /// + /// 当还原该节点时候会将 OnSaveNode() 所返回的数据重新传入此函数 + /// + /// 保存时候的数据 + protected internal virtual void OnLoadNode(Dictionary dic) { + if (dic.ContainsKey("AutoSize")) this._AutoSize = dic["AutoSize"][0] == 1; + if (dic.ContainsKey("LockOption")) this._LockOption = dic["LockOption"][0] == 1; + if (dic.ContainsKey("LockLocation")) this._LockLocation = dic["LockLocation"][0] == 1; + if (dic.ContainsKey("Guid")) this._Guid = new Guid(dic["Guid"]); + if (dic.ContainsKey("Left")) this._Left = BitConverter.ToInt32(dic["Left"], 0); + if (dic.ContainsKey("Top")) this._Top = BitConverter.ToInt32(dic["Top"], 0); + if (dic.ContainsKey("Width") && !this._AutoSize) this._Width = BitConverter.ToInt32(dic["Width"], 0); + if (dic.ContainsKey("Height") && !this._AutoSize) this._Height = BitConverter.ToInt32(dic["Height"], 0); + if (dic.ContainsKey("Mark")) this.Mark = Encoding.UTF8.GetString(dic["Mark"]); + Type t = this.GetType(); + foreach (var p in t.GetProperties()) { + var attrs = p.GetCustomAttributes(true); + foreach (var a in attrs) { + if (!(a is STNodePropertyAttribute)) continue; + var attr = a as STNodePropertyAttribute; + object obj = Activator.CreateInstance(attr.DescriptorType); + if (!(obj is STNodePropertyDescriptor)) + throw new InvalidOperationException("[STNodePropertyAttribute.Type]参数值必须为[STNodePropertyDescriptor]或者其子类的类型"); + var desc = (STNodePropertyDescriptor)Activator.CreateInstance(attr.DescriptorType); + desc.Node = this; + desc.PropertyInfo = p; + try { + if (dic.ContainsKey(p.Name)) desc.SetValue(dic[p.Name]); + } catch (Exception ex) { + string strErr = "属性[" + this.Title + "." + p.Name + "]的值无法被还原 可通过重写[STNodePropertyAttribute.GetBytesFromValue(),STNodePropertyAttribute.GetValueFromBytes(byte[])]确保保存和加载时候的二进制数据正确"; + Exception e = ex; + while (e != null) { + strErr += "\r\n----\r\n[" + e.GetType().Name + "] -> " + e.Message; + e = e.InnerException; + } + throw new InvalidOperationException(strErr, ex); + } + } + } + } + /// + /// 当编辑器加载完成所有的节点时候发生 + /// + protected internal virtual void OnEditorLoadCompleted() { } + /// + /// 设置Option的文本信息 + /// + /// 目标Option + /// 文本 + /// 是否成功 + protected bool SetOptionText(STNodeOption op, string strText) { + if (op.Owner != this) return false; + op.Text = strText; + return true; + } + /// + /// 设置Option文本信息颜色 + /// + /// 目标Option + /// 颜色 + /// 是否成功 + protected bool SetOptionTextColor(STNodeOption op, Color clr) { + if (op.Owner != this) return false; + op.TextColor = clr; + return true; + } + /// + /// 设置Option连线点颜色 + /// + /// 目标Option + /// 颜色 + /// 是否成功 + protected bool SetOptionDotColor(STNodeOption op, Color clr) { + if (op.Owner != this) return false; + op.DotColor = clr; + return false; + } + + //[event]===========================[event]==============================[event]============================[event] + + 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(MouseEventArgs e) { + Point pt = e.Location; + pt.Y -= this._TitleHeight; + for (int i = this._Controls.Count - 1; i >= 0; i--) { + var c = this._Controls[i]; + if (c.DisplayRectangle.Contains(pt)) { + if (!c.Enabled) return; + if (!c.Visable) continue; + c.OnMouseDown(new MouseEventArgs(e.Button, e.Clicks, e.X - c.Left, pt.Y - c.Top, e.Delta)); + m_ctrl_down = c; + if (m_ctrl_active != c) { + c.OnGotFocus(EventArgs.Empty); + if (m_ctrl_active != null) m_ctrl_active.OnLostFocus(EventArgs.Empty); + m_ctrl_active = c; + } + return; + } + } + if (m_ctrl_active != null) m_ctrl_active.OnLostFocus(EventArgs.Empty); + m_ctrl_active = null; + } + + protected internal virtual void OnMouseMove(MouseEventArgs e) { + Point pt = e.Location; + pt.Y -= this._TitleHeight; + if (m_ctrl_down != null) { + if (m_ctrl_down.Enabled && m_ctrl_down.Visable) + m_ctrl_down.OnMouseMove(new MouseEventArgs(e.Button, e.Clicks, e.X - m_ctrl_down.Left, pt.Y - m_ctrl_down.Top, e.Delta)); + return; + } + for (int i = this._Controls.Count - 1; i >= 0; i--) { + var c = this._Controls[i]; + if (c.DisplayRectangle.Contains(pt)) { + if (m_ctrl_hover != this._Controls[i]) { + c.OnMouseEnter(EventArgs.Empty); + if (m_ctrl_hover != null) m_ctrl_hover.OnMouseLeave(EventArgs.Empty); + m_ctrl_hover = c; + } + m_ctrl_hover.OnMouseMove(new MouseEventArgs(e.Button, e.Clicks, e.X - c.Left, pt.Y - c.Top, e.Delta)); + return; + } + } + if (m_ctrl_hover != null) m_ctrl_hover.OnMouseLeave(EventArgs.Empty); + m_ctrl_hover = null; + } + + protected internal virtual void OnMouseUp(MouseEventArgs e) { + Point pt = e.Location; + pt.Y -= this._TitleHeight; + if (m_ctrl_down != null && m_ctrl_down.Enabled && m_ctrl_down.Visable) { + m_ctrl_down.OnMouseUp(new MouseEventArgs(e.Button, e.Clicks, e.X - m_ctrl_down.Left, pt.Y - m_ctrl_down.Top, e.Delta)); + } + //if (m_ctrl_active != null) { + // m_ctrl_active.OnMouseUp(new MouseEventArgs(e.Button, e.Clicks, + // e.X - m_ctrl_active.Left, pt.Y - m_ctrl_active.Top, e.Delta)); + //} + 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(MouseEventArgs e) { + Point pt = e.Location; + pt.Y -= this._TitleHeight; + if (m_ctrl_active != null && m_ctrl_active.Enabled && m_ctrl_active.Visable) + m_ctrl_active.OnMouseClick(new MouseEventArgs(e.Button, e.Clicks, e.X - m_ctrl_active.Left, pt.Y - m_ctrl_active.Top, e.Delta)); + } + + protected internal virtual void OnMouseWheel(MouseEventArgs e) { + Point pt = e.Location; + pt.Y -= this._TitleHeight; + if (m_ctrl_hover != null && m_ctrl_active.Enabled && m_ctrl_hover.Visable) { + m_ctrl_hover.OnMouseWheel(new MouseEventArgs(e.Button, e.Clicks, e.X - m_ctrl_hover.Left, pt.Y - m_ctrl_hover.Top, e.Delta)); + return; + } + } + protected internal virtual void OnMouseHWheel(MouseEventArgs e) { + if (m_ctrl_hover != null && m_ctrl_active.Enabled && m_ctrl_hover.Visable) { + m_ctrl_hover.OnMouseHWheel(e); + return; + } + } + + 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(KeyPressEventArgs 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) { /*this.SetOptionLocation();*/ } + protected virtual void OnResize(EventArgs e) { /*this.SetOptionLocation();*/ } + + + /// + /// 当所有者发生改变时候发生 + /// + protected virtual void OnOwnerChanged() { } + /// + /// 当选中状态改变时候发生 + /// + protected virtual void OnSelectedChanged() { } + /// + /// 当活动状态改变时候发生 + /// + protected virtual void OnActiveChanged() { } + + #endregion protected + /// + /// 计算每个Option的位置 + /// + protected virtual void SetOptionsLocation() { + int nIndex = 0; + Rectangle rect = new Rectangle(this.Left + 10, this._Top + this._TitleHeight, this._Width - 20, this._ItemHeight); + foreach (STNodeOption op in this._InputOptions) { + if (op != STNodeOption.Empty) { + Point pt = this.OnSetOptionDotLocation(op, new Point(this.Left - op.DotSize / 2, rect.Y + (rect.Height - op.DotSize) / 2), nIndex); + op.TextRectangle = this.OnSetOptionTextRectangle(op, rect, nIndex); + op.DotLeft = pt.X; + op.DotTop = pt.Y; + } + rect.Y += this._ItemHeight; + nIndex++; + } + rect.Y = this._Top + this._TitleHeight; + m_sf.Alignment = StringAlignment.Far; + foreach (STNodeOption op in this._OutputOptions) { + if (op != STNodeOption.Empty) { + Point pt = this.OnSetOptionDotLocation(op, new Point(this._Left + this._Width - op.DotSize / 2, rect.Y + (rect.Height - op.DotSize) / 2), nIndex); + op.TextRectangle = this.OnSetOptionTextRectangle(op, rect, nIndex); + op.DotLeft = pt.X; + op.DotTop = pt.Y; + } + rect.Y += this._ItemHeight; + nIndex++; + } + } + + /// + /// 重绘Node + /// + public void Invalidate() { + if (this._Owner != null) { + this._Owner.Invalidate(this._Owner.CanvasToControl(new Rectangle(this._Left - 5, this._Top - 5, this._Width + 10, this._Height + 10))); + } + } + /// + /// 重绘 Node 指定区域 + /// + /// Node 指定区域 + public void Invalidate(Rectangle rect) { + rect.X += this._Left; + rect.Y += this._Top; + if (this._Owner != null) { + rect = this._Owner.CanvasToControl(rect); + rect.Width += 1; rect.Height += 1;//坐标系统转换可能导致进度丢失 多加上一个像素 + this._Owner.Invalidate(rect); + } + } + /// + /// 获取此Node所包含的输入Option集合 + /// + /// Option集合 + public STNodeOption[] GetInputOptions() { + if (!this._LetGetOptions) return null; + STNodeOption[] ops = new STNodeOption[this._InputOptions.Count]; + for (int i = 0; i < this._InputOptions.Count; i++) ops[i] = this._InputOptions[i]; + return ops; + } + /// + /// 获取此Node所包含的输出Option集合 + /// + /// Option集合 + public STNodeOption[] GetOutputOptions() { + if (!this._LetGetOptions) return null; + STNodeOption[] ops = new STNodeOption[this._OutputOptions.Count]; + for (int i = 0; i < this._OutputOptions.Count; i++) ops[i] = this._OutputOptions[i]; + return ops; + } + /// + /// 设置Node的选中状态 + /// + /// 是否选中 + /// 是否重绘 + public void SetSelected(bool bSelected, bool bRedraw) { + if (this._IsSelected == bSelected) return; + this._IsSelected = bSelected; + if (this._Owner != null) { + if (bSelected) + this._Owner.AddSelectedNode(this); + else + this._Owner.RemoveSelectedNode(this); + } + if (bRedraw) this.Invalidate(); + this.OnSelectedChanged(); + if (this._Owner != null) this._Owner.OnSelectedChanged(EventArgs.Empty); + } + public IAsyncResult BeginInvoke(Delegate method) { return this.BeginInvoke(method, null); } + public IAsyncResult BeginInvoke(Delegate method, params object[] args) { + if (this._Owner == null) return null; + return this._Owner.BeginInvoke(method, args); + } + public object Invoke(Delegate method) { return this.Invoke(method, null); } + public object Invoke(Delegate method, params object[] args) { + if (this._Owner == null) return null; + return this._Owner.Invoke(method, args); + } + } +} diff --git a/ST.Library.UI/NodeEditor/STNodeAttribute.cs b/ST.Library.UI/NodeEditor/STNodeAttribute.cs new file mode 100644 index 0000000..2319e78 --- /dev/null +++ b/ST.Library.UI/NodeEditor/STNodeAttribute.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace ST.Library.UI.NodeEditor +{ + /// + /// STNode节点特性 + /// 用于描述STNode开发者信息 以及部分行为 + /// + public class STNodeAttribute : Attribute + { + private string _Path; + /// + /// 获取STNode节点期望在树形控件的路径 + /// + public string Path { + get { return _Path; } + } + + private string _Author; + /// + /// 获取STNode节点的作者名称 + /// + public string Author { + get { return _Author; } + } + + private string _Mail; + /// + /// 获取STNode节点的作者邮箱 + /// + public string Mail { + get { return _Mail; } + } + + private string _Link; + /// + /// 获取STNode节点的作者链接 + /// + public string Link { + get { return _Link; } + } + + private string _Description; + /// + /// 获取STNode节点的描述信息 + /// + public string Description { + get { return _Description; } + } + + private static char[] m_ch_splitter = new char[] { '/', '\\' }; + private static Regex m_reg = new Regex(@"^https?://", RegexOptions.IgnoreCase); + /// + /// 构造一个STNode特性 + /// + /// 期望路径 + public STNodeAttribute(string strPath) : this(strPath, null, null, null, null) { } + /// + /// 构造一个STNode特性 + /// + /// 期望路径 + /// 描述信息 + public STNodeAttribute(string strPath, string strDescription) : this(strPath, null, null, null, strDescription) { } + /// + /// 构造一个STNode特性 + /// + /// 期望路径 + /// STNode作者名称 + /// STNode作者邮箱 + /// STNode作者链接 + /// STNode节点描述信息 + public STNodeAttribute(string strPath, string strAuthor, string strMail, string strLink, string strDescription) { + if (!string.IsNullOrEmpty(strPath)) + strPath = strPath.Trim().Trim(m_ch_splitter).Trim(); + + this._Path = strPath; + + this._Author = strAuthor; + this._Mail = strMail; + this._Description = strDescription; + if (string.IsNullOrEmpty(strLink) || strLink.Trim() == string.Empty) return; + strLink = strLink.Trim(); + if (m_reg.IsMatch(strLink)) + this._Link = strLink; + else + this._Link = "http://" + strLink; + } + + private static Dictionary m_dic = new Dictionary(); + /// + /// 获取类型的帮助函数 + /// + /// 节点类型 + /// 函数信息 + public static MethodInfo GetHelpMethod(Type stNodeType) { + if (m_dic.ContainsKey(stNodeType)) return m_dic[stNodeType]; + var mi = stNodeType.GetMethod("ShowHelpInfo"); + if (mi == null) return null; + if (!mi.IsStatic) return null; + var ps = mi.GetParameters(); + if (ps.Length != 1) return null; + if (ps[0].ParameterType != typeof(string)) return null; + m_dic.Add(stNodeType, mi); + return mi; + } + /// + /// 执行对应节点类型的帮助函数 + /// + /// 节点类型 + public static void ShowHelp(Type stNodeType) { + var mi = STNodeAttribute.GetHelpMethod(stNodeType); + if (mi == null) return; + mi.Invoke(null, new object[] { stNodeType.Module.FullyQualifiedName }); + } + } +} diff --git a/ST.Library.UI/STNodeEditor/STNodeCollection.cs b/ST.Library.UI/NodeEditor/STNodeCollection.cs old mode 100755 new mode 100644 similarity index 91% rename from ST.Library.UI/STNodeEditor/STNodeCollection.cs rename to ST.Library.UI/NodeEditor/STNodeCollection.cs index 6d23e01..55a73dd --- a/ST.Library.UI/STNodeEditor/STNodeCollection.cs +++ b/ST.Library.UI/NodeEditor/STNodeCollection.cs @@ -1,236 +1,253 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Collections; -using System.Drawing; - -namespace ST.Library.UI -{ - public class STNodeCollection : IList, ICollection, IEnumerable - { - private int _Count; - public int Count { get { return _Count; } } - private STNode[] m_nodes; - private STNodeEditor m_owner; - - internal STNodeCollection(STNodeEditor owner) { - if (owner == null) throw new ArgumentNullException("所有者不能为空"); - m_owner = owner; - m_nodes = new STNode[4]; - } - - public int Add(STNode node) { - if (node == null) throw new ArgumentNullException("添加对象不能为空"); - this.EnsureSpace(1); - int nIndex = this.IndexOf(node); - if (-1 == nIndex) { - nIndex = this._Count; - node.Owner = m_owner; - //node.BuildSize(true, true, false); - m_nodes[this._Count++] = node; - m_owner.BuildBounds(); - m_owner.OnNodeAdded(new STNodeEditorEventArgs(node)); - m_owner.Invalidate(); - //m_owner.Invalidate(m_owner.CanvasToControl(new Rectangle(node.Left - 5, node.Top - 5, node.Width + 10, node.Height + 10))); - //Console.WriteLine(node.Rectangle); - } - return nIndex; - } - - public void AddRange(STNode[] nodes) { - if (nodes == null) throw new ArgumentNullException("添加对象不能为空"); - this.EnsureSpace(nodes.Length); - foreach (var n in nodes) { - if (n == null) throw new ArgumentNullException("添加对象不能为空"); - if (-1 == this.IndexOf(n)) { - n.Owner = m_owner; - m_nodes[this._Count++] = n; - } - m_owner.OnNodeAdded(new STNodeEditorEventArgs(n)); - } - m_owner.Invalidate(); - m_owner.BuildBounds(); - } - - public void Clear() { - for (int i = 0; i < this._Count; i++) { - m_nodes[i].Owner = null; - foreach (STNodeOption op in m_nodes[i].InputOptions) op.DisConnectionAll(); - foreach (STNodeOption op in m_nodes[i].OutputOptions) op.DisConnectionAll(); - m_owner.OnNodeRemoved(new STNodeEditorEventArgs(m_nodes[i])); - } - this._Count = 0; - m_nodes = new STNode[4]; - m_owner.BuildBounds(); - m_owner.ScaleCanvas(1, 0, 0); //当不存在节点时候 坐标系回归 - m_owner.MoveCanvas(10, 10, true, CanvasMoveArgs.All); - m_owner.Invalidate(); //如果画布位置和缩放处于初始状态 上面两行代码并不会造成控件重绘 - } - - public bool Contains(STNode node) { - return this.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 >= this._Count) - throw new IndexOutOfRangeException("索引越界"); - if (node == null) - throw new ArgumentNullException("插入对象不能为空"); - this.EnsureSpace(1); - for (int i = this._Count; i > nIndex; i--) - m_nodes[i] = m_nodes[i - 1]; - node.Owner = m_owner; - m_nodes[nIndex] = node; - this._Count++; - //node.BuildSize(true, true,false); - m_owner.Invalidate(); - m_owner.BuildBounds(); - } - - public bool IsFixedSize { - get { return false; } - } - - public bool IsReadOnly { - get { return false; } - } - - public void Remove(STNode node) { - int nIndex = this.IndexOf(node); - if (nIndex != -1) this.RemoveAt(nIndex); - } - - public void RemoveAt(int nIndex) { - if (nIndex < 0 || nIndex >= this._Count) - throw new IndexOutOfRangeException("索引越界"); - m_nodes[nIndex].Owner = null; - m_owner.OnNodeRemoved(new STNodeEditorEventArgs(m_nodes[nIndex])); - this._Count--; - for (int i = nIndex, Len = this._Count; i < Len; i++) - m_nodes[i] = m_nodes[i + 1]; - if (this._Count == 0) { //当不存在节点时候 坐标系回归 - m_owner.ScaleCanvas(1, 0, 0); - m_owner.MoveCanvas(10, 10, true, CanvasMoveArgs.All); - } else { - m_owner.Invalidate(); - m_owner.BuildBounds(); - } - } - - public STNode this[int nIndex] { - get { - if (nIndex < 0 || nIndex >= this._Count) - throw new IndexOutOfRangeException("索引越界"); - return m_nodes[nIndex]; - } - set { throw new InvalidOperationException("禁止重新赋值元素"); } - } - - public void CopyTo(Array array, int index) { - if (array == null) - throw new ArgumentNullException("数组不能为空"); - m_nodes.CopyTo(array, index); - } - - public bool IsSynchronized { - get { return true; } - } - - public object SyncRoot { - get { return this; } - } - - public IEnumerator GetEnumerator() { - for (int i = 0, Len = this._Count; i < Len; i++) - yield return m_nodes[i]; - } - /// - /// 确认空间是否足够 空间不足扩大容量 - /// - /// 需要增加的个数 - private void EnsureSpace(int elements) { - if (elements + this._Count > m_nodes.Length) { - STNode[] arrTemp = new STNode[Math.Max(m_nodes.Length * 2, elements + this._Count)]; - m_nodes.CopyTo(arrTemp, 0); - m_nodes = arrTemp; - } - } - //============================================================================ - int IList.Add(object value) { - return this.Add((STNode)value); - } - - void IList.Clear() { - this.Clear(); - } - - bool IList.Contains(object value) { - return this.Contains((STNode)value); - } - - int IList.IndexOf(object value) { - return this.IndexOf((STNode)value); - } - - void IList.Insert(int index, object value) { - this.Insert(index, (STNode)value); - } - - bool IList.IsFixedSize { - get { return this.IsFixedSize; } - } - - bool IList.IsReadOnly { - get { return this.IsReadOnly; } - } - - void IList.Remove(object value) { - this.Remove((STNode)value); - } - - void IList.RemoveAt(int index) { - this.RemoveAt(index); - } - - object IList.this[int index] { - get { - return this[index]; - } - set { - this[index] = (STNode)value; - } - } - - void ICollection.CopyTo(Array array, int index) { - this.CopyTo(array, index); - } - - int ICollection.Count { - get { return this._Count; } - } - - bool ICollection.IsSynchronized { - get { return this.IsSynchronized; } - } - - object ICollection.SyncRoot { - get { return this.SyncRoot; } - } - - IEnumerator IEnumerable.GetEnumerator() { - return this.GetEnumerator(); - } - - public STNode[] ToArray() { - STNode[] nodes = new STNode[this._Count]; - for (int i = 0; i < nodes.Length; i++) - nodes[i] = m_nodes[i]; - return nodes; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections; +using System.Drawing; + +namespace ST.Library.UI.NodeEditor +{ + public class STNodeCollection : IList, ICollection, IEnumerable + { + private int _Count; + public int Count { get { return _Count; } } + private STNode[] m_nodes; + private STNodeEditor m_owner; + + internal STNodeCollection(STNodeEditor owner) { + if (owner == null) throw new ArgumentNullException("所有者不能为空"); + m_owner = owner; + m_nodes = new STNode[4]; + } + + public void MoveToEnd(STNode node) { + if (this._Count < 1) return; + if (m_nodes[this._Count - 1] == node) return; + bool bFound = false; + for (int i = 0; i < _Count - 1; i++) { + if (m_nodes[i] == node) { + bFound = true; + } + if (bFound) m_nodes[i] = m_nodes[i + 1]; + } + m_nodes[this._Count - 1] = node; + } + + public int Add(STNode node) { + if (node == null) throw new ArgumentNullException("添加对象不能为空"); + this.EnsureSpace(1); + int nIndex = this.IndexOf(node); + if (-1 == nIndex) { + nIndex = this._Count; + node.Owner = m_owner; + //node.BuildSize(true, true, false); + m_nodes[this._Count++] = node; + m_owner.BuildBounds(); + m_owner.OnNodeAdded(new STNodeEditorEventArgs(node)); + m_owner.Invalidate(); + //m_owner.Invalidate(m_owner.CanvasToControl(new Rectangle(node.Left - 5, node.Top - 5, node.Width + 10, node.Height + 10))); + //Console.WriteLine(node.Rectangle); + } + return nIndex; + } + + public void AddRange(STNode[] nodes) { + if (nodes == null) throw new ArgumentNullException("添加对象不能为空"); + this.EnsureSpace(nodes.Length); + foreach (var n in nodes) { + if (n == null) throw new ArgumentNullException("添加对象不能为空"); + if (-1 == this.IndexOf(n)) { + n.Owner = m_owner; + m_nodes[this._Count++] = n; + } + m_owner.OnNodeAdded(new STNodeEditorEventArgs(n)); + } + m_owner.Invalidate(); + m_owner.BuildBounds(); + } + + public void Clear() { + for (int i = 0; i < this._Count; i++) { + m_nodes[i].Owner = null; + foreach (STNodeOption op in m_nodes[i].InputOptions) op.DisConnectionAll(); + foreach (STNodeOption op in m_nodes[i].OutputOptions) op.DisConnectionAll(); + m_owner.OnNodeRemoved(new STNodeEditorEventArgs(m_nodes[i])); + m_owner.InternalRemoveSelectedNode(m_nodes[i]); + } + this._Count = 0; + m_nodes = new STNode[4]; + m_owner.SetActiveNode(null); + m_owner.BuildBounds(); + m_owner.ScaleCanvas(1, 0, 0); //当不存在节点时候 坐标系回归 + m_owner.MoveCanvas(10, 10, true, CanvasMoveArgs.All); + m_owner.Invalidate(); //如果画布位置和缩放处于初始状态 上面两行代码并不会造成控件重绘 + } + + public bool Contains(STNode node) { + return this.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 >= this._Count) + throw new IndexOutOfRangeException("索引越界"); + if (node == null) + throw new ArgumentNullException("插入对象不能为空"); + this.EnsureSpace(1); + for (int i = this._Count; i > nIndex; i--) + m_nodes[i] = m_nodes[i - 1]; + node.Owner = m_owner; + m_nodes[nIndex] = node; + this._Count++; + //node.BuildSize(true, true,false); + m_owner.Invalidate(); + m_owner.BuildBounds(); + } + + public bool IsFixedSize { + get { return false; } + } + + public bool IsReadOnly { + get { return false; } + } + + public void Remove(STNode node) { + int nIndex = this.IndexOf(node); + if (nIndex != -1) this.RemoveAt(nIndex); + } + + public void RemoveAt(int nIndex) { + if (nIndex < 0 || nIndex >= this._Count) + throw new IndexOutOfRangeException("索引越界"); + m_nodes[nIndex].Owner = null; + m_owner.InternalRemoveSelectedNode(m_nodes[nIndex]); + if (m_owner.ActiveNode == m_nodes[nIndex]) m_owner.SetActiveNode(null); + m_owner.OnNodeRemoved(new STNodeEditorEventArgs(m_nodes[nIndex])); + this._Count--; + for (int i = nIndex, Len = this._Count; i < Len; i++) + m_nodes[i] = m_nodes[i + 1]; + if (this._Count == 0) { //当不存在节点时候 坐标系回归 + m_owner.ScaleCanvas(1, 0, 0); + m_owner.MoveCanvas(10, 10, true, CanvasMoveArgs.All); + } else { + m_owner.Invalidate(); + m_owner.BuildBounds(); + } + } + + public STNode this[int nIndex] { + get { + if (nIndex < 0 || nIndex >= this._Count) + throw new IndexOutOfRangeException("索引越界"); + return m_nodes[nIndex]; + } + set { throw new InvalidOperationException("禁止重新赋值元素"); } + } + + public void CopyTo(Array array, int index) { + if (array == null) + throw new ArgumentNullException("数组不能为空"); + m_nodes.CopyTo(array, index); + } + + public bool IsSynchronized { + get { return true; } + } + + public object SyncRoot { + get { return this; } + } + + public IEnumerator GetEnumerator() { + for (int i = 0, Len = this._Count; i < Len; i++) + yield return m_nodes[i]; + } + /// + /// 确认空间是否足够 空间不足扩大容量 + /// + /// 需要增加的个数 + private void EnsureSpace(int elements) { + if (elements + this._Count > m_nodes.Length) { + STNode[] arrTemp = new STNode[Math.Max(m_nodes.Length * 2, elements + this._Count)]; + m_nodes.CopyTo(arrTemp, 0); + m_nodes = arrTemp; + } + } + //============================================================================ + int IList.Add(object value) { + return this.Add((STNode)value); + } + + void IList.Clear() { + this.Clear(); + } + + bool IList.Contains(object value) { + return this.Contains((STNode)value); + } + + int IList.IndexOf(object value) { + return this.IndexOf((STNode)value); + } + + void IList.Insert(int index, object value) { + this.Insert(index, (STNode)value); + } + + bool IList.IsFixedSize { + get { return this.IsFixedSize; } + } + + bool IList.IsReadOnly { + get { return this.IsReadOnly; } + } + + void IList.Remove(object value) { + this.Remove((STNode)value); + } + + void IList.RemoveAt(int index) { + this.RemoveAt(index); + } + + object IList.this[int index] { + get { + return this[index]; + } + set { + this[index] = (STNode)value; + } + } + + void ICollection.CopyTo(Array array, int index) { + this.CopyTo(array, index); + } + + int ICollection.Count { + get { return this._Count; } + } + + bool ICollection.IsSynchronized { + get { return this.IsSynchronized; } + } + + object ICollection.SyncRoot { + get { return this.SyncRoot; } + } + + IEnumerator IEnumerable.GetEnumerator() { + return this.GetEnumerator(); + } + + public STNode[] ToArray() { + STNode[] nodes = new STNode[this._Count]; + for (int i = 0; i < nodes.Length; i++) + nodes[i] = m_nodes[i]; + return nodes; + } + } +} diff --git a/ST.Library.UI/STNodeEditor/STNodeControl.cs b/ST.Library.UI/NodeEditor/STNodeControl.cs old mode 100755 new mode 100644 similarity index 82% rename from ST.Library.UI/STNodeEditor/STNodeControl.cs rename to ST.Library.UI/NodeEditor/STNodeControl.cs index 3c0d4a3..4bd9cca --- a/ST.Library.UI/STNodeEditor/STNodeControl.cs +++ b/ST.Library.UI/NodeEditor/STNodeControl.cs @@ -1,263 +1,319 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Windows.Forms; -using System.Drawing; -/* -MIT License - -Copyright (c) 2021 DebugST@crystal_lz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - */ -/* - * time: 2021-01-06 - * Author: Crystal_lz - * blog: st233.com - * Github: DebugST.github.io - */ -namespace ST.Library.UI -{ - public class STNodeControl - { - private STNode _Owner; - - public STNode Owner { - get { return _Owner; } - internal set { _Owner = value; } - } - - private int _Left; - - public int Left { - get { return _Left; } - set { - _Left = value; - this.OnMove(new EventArgs()); - this.Invalidate(); - } - } - - private int _Top; - - public int Top { - get { return _Top; } - set { - _Top = value; - this.OnMove(new EventArgs()); - this.Invalidate(); - } - } - - private int _Width; - - public int Width { - get { return _Width; } - set { - _Width = value; - this.OnResize(new EventArgs()); - this.Invalidate(); - } - } - - private int _Height; - - public int Height { - get { return _Height; } - set { - _Height = value; - this.OnResize(new EventArgs()); - this.Invalidate(); - } - } - - public Point Location { - get { return new Point(this._Left, this._Top); } - } - public Size Size { - get { return new Size(this._Width, this._Height); } - } - public Rectangle DisplayRectangle { - get { return new Rectangle(this._Left, this._Top, this._Width, this._Height); } - } - public Rectangle ClientRectangle { - get { return new Rectangle(0, 0, this._Width, this._Height); } - } - - private Color _BackColor = Color.FromArgb(127, 0, 0, 0); - - public Color BackColor { - get { return _BackColor; } - set { - _BackColor = value; - this.Invalidate(); - } - } - - private Color _ForeColor = Color.White; - - public Color ForeColor { - get { return _ForeColor; } - set { - _ForeColor = value; - this.Invalidate(); - } - } - - private string _Text = "STNCTRL"; - - public string Text { - get { return _Text; } - set { - _Text = value; - this.Invalidate(); - } - } - - private Font _Font; - - public Font Font { - get { return _Font; } - set { - if (value == _Font) return; - if (value == null) throw new ArgumentNullException("值不能为空"); - _Font = value; - this.Invalidate(); - } - } - - protected StringFormat m_sf; - - public STNodeControl() { - m_sf = new StringFormat(); - m_sf.Alignment = StringAlignment.Center; - m_sf.LineAlignment = StringAlignment.Center; - this._Font = new Font("courier new", 8.25f); - this.Width = 75; - this.Height = 23; - } - - protected internal virtual void OnPaint(DrawingTools dt) { - Graphics g = dt.Graphics; - SolidBrush brush = dt.SolidBrush; - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; - g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; - brush.Color = this._BackColor; - g.FillRectangle(brush, 0, 0, this.Width, this.Height); - if (!string.IsNullOrEmpty(this._Text)) { - brush.Color = this._ForeColor; - g.DrawString(this._Text, this._Font, brush, this.ClientRectangle, m_sf); - } - } - - public void Invalidate() { - if (this._Owner == null) return; - this._Owner.Invalidate(new Rectangle(this._Left, this._Top + this._Owner.TitleHeight, this.Width, this.Height)); - } - - public void Invalidate(Rectangle rect) { - if (this._Owner == null) return; - this._Owner.Invalidate(this.RectangleToParent(rect)); - } - - public Rectangle RectangleToParent(Rectangle rect) { - return new Rectangle(this._Left, this._Top + this._Owner.TitleHeight, this.Width, this.Height); - } - - public event EventHandler GotFocus; - public event EventHandler LostFocus; - public event EventHandler MouseEnter; - public event EventHandler MouseLeave; - public event MouseEventHandler MouseDown; - public event MouseEventHandler MouseMove; - public event MouseEventHandler MouseUp; - public event MouseEventHandler MouseClick; - public event MouseEventHandler MouseWheel; - public event EventHandler MouseHWheel; - - public event KeyEventHandler KeyDown; - public event KeyEventHandler KeyUp; - public event KeyPressEventHandler KeyPress; - - public event EventHandler Move; - public event EventHandler Resize; - - - 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(MouseEventArgs e) { - if (this.MouseDown != null) this.MouseDown(this, e); - } - protected internal virtual void OnMouseMove(MouseEventArgs e) { - if (this.MouseMove != null) this.MouseMove(this, e); - } - protected internal virtual void OnMouseUp(MouseEventArgs e) { - if (this.MouseUp != null) this.MouseUp(this, e); - } - protected internal virtual void OnMouseClick(MouseEventArgs e) { - if (this.MouseClick != null) this.MouseClick(this, e); - } - protected internal virtual void OnMouseWheel(MouseEventArgs e) { - if (this.MouseWheel != null) this.MouseWheel(this, e); - } - protected internal virtual void OnMouseHWheel(MouseEventArgs 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(KeyPressEventArgs 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 this.BeginInvoke(method, null); } - public IAsyncResult BeginInvoke(Delegate method, params object[] args) { - if (this._Owner == null) return null; - return this._Owner.BeginInvoke(method, args); - } - public object Invoke(Delegate method) { return this.Invoke(method, null); } - public object Invoke(Delegate method, params object[] args) { - if (this._Owner == null) return null; - return this._Owner.Invoke(method, args); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Forms; +using System.Drawing; +/* +MIT License + +Copyright (c) 2021 DebugST@crystal_lz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +/* + * time: 2021-01-06 + * Author: Crystal_lz + * blog: st233.com + * Github: DebugST.github.io + */ +namespace ST.Library.UI.NodeEditor +{ + public class STNodeControl + { + private STNode _Owner; + + public STNode Owner { + get { return _Owner; } + internal set { _Owner = value; } + } + + private int _Left; + + public int Left { + get { return _Left; } + set { + _Left = value; + this.OnMove(EventArgs.Empty); + this.Invalidate(); + } + } + + private int _Top; + + public int Top { + get { return _Top; } + set { + _Top = value; + this.OnMove(EventArgs.Empty); + this.Invalidate(); + } + } + + private int _Width; + + public int Width { + get { return _Width; } + set { + _Width = value; + this.OnResize(EventArgs.Empty); + this.Invalidate(); + } + } + + private int _Height; + + public int Height { + get { return _Height; } + set { + _Height = value; + this.OnResize(EventArgs.Empty); + this.Invalidate(); + } + } + + public int Right { get { return this._Left + this._Width; } } + + public int Bottom { get { return this._Top + this._Height; } } + + public Point Location { + get { return new Point(this._Left, this._Top); } + set { + this.Left = value.X; + this.Top = value.Y; + } + } + public Size Size { + get { return new Size(this._Width, this._Height); } + set { + this.Width = value.Width; + this.Height = value.Height; + } + } + public Rectangle DisplayRectangle { + get { return new Rectangle(this._Left, this._Top, this._Width, this._Height); } + set { + this.Left = value.X; + this.Top = value.Y; + this.Width = value.Width; + this.Height = value.Height; + } + } + public Rectangle ClientRectangle { + get { return new Rectangle(0, 0, this._Width, this._Height); } + } + + private Color _BackColor = Color.FromArgb(127, 0, 0, 0); + + public Color BackColor { + get { return _BackColor; } + set { + _BackColor = value; + this.Invalidate(); + } + } + + private Color _ForeColor = Color.White; + + public Color ForeColor { + get { return _ForeColor; } + set { + _ForeColor = value; + this.Invalidate(); + } + } + + private string _Text = "STNCTRL"; + + public string Text { + get { return _Text; } + set { + _Text = value; + this.Invalidate(); + } + } + + private Font _Font; + + public Font Font { + get { return _Font; } + set { + if (value == _Font) return; + if (value == null) throw new ArgumentNullException("值不能为空"); + _Font = value; + this.Invalidate(); + } + } + + private bool _Enabled = true; + + public bool Enabled { + get { return _Enabled; } + set { + if (value == _Enabled) return; + _Enabled = value; + this.Invalidate(); + } + } + + private bool _Visable = true; + + public bool Visable { + get { return _Visable; } + set { + if (value == _Visable) return; + _Visable = value; + this.Invalidate(); + } + } + + protected StringFormat m_sf; + + public STNodeControl() { + m_sf = new StringFormat(); + m_sf.Alignment = StringAlignment.Center; + m_sf.LineAlignment = StringAlignment.Center; + this._Font = new Font("courier new", 8.25f); + this.Width = 75; + this.Height = 23; + } + + protected internal virtual void OnPaint(DrawingTools dt) { + Graphics g = dt.Graphics; + SolidBrush brush = dt.SolidBrush; + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + brush.Color = this._BackColor; + g.FillRectangle(brush, 0, 0, this.Width, this.Height); + if (!string.IsNullOrEmpty(this._Text)) { + brush.Color = this._ForeColor; + g.DrawString(this._Text, this._Font, brush, this.ClientRectangle, m_sf); + } + if (this.Paint != null) this.Paint(this, new STNodeControlPaintEventArgs(dt)); + } + + public void Invalidate() { + if (this._Owner == null) return; + this._Owner.Invalidate(new Rectangle(this._Left, this._Top + this._Owner.TitleHeight, this.Width, this.Height)); + } + + public void Invalidate(Rectangle rect) { + if (this._Owner == null) return; + this._Owner.Invalidate(this.RectangleToParent(rect)); + } + + public Rectangle RectangleToParent(Rectangle rect) { + return new Rectangle(this._Left, this._Top + this._Owner.TitleHeight, this.Width, this.Height); + } + + public event EventHandler GotFocus; + public event EventHandler LostFocus; + public event EventHandler MouseEnter; + public event EventHandler MouseLeave; + public event MouseEventHandler MouseDown; + public event MouseEventHandler MouseMove; + public event MouseEventHandler MouseUp; + public event MouseEventHandler MouseClick; + public event MouseEventHandler MouseWheel; + public event EventHandler MouseHWheel; + + public event KeyEventHandler KeyDown; + public event KeyEventHandler KeyUp; + public event KeyPressEventHandler KeyPress; + + public event EventHandler Move; + public event EventHandler Resize; + + public event STNodeControlPaintEventHandler Paint; + + 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(MouseEventArgs e) { + if (this.MouseDown != null) this.MouseDown(this, e); + } + protected internal virtual void OnMouseMove(MouseEventArgs e) { + if (this.MouseMove != null) this.MouseMove(this, e); + } + protected internal virtual void OnMouseUp(MouseEventArgs e) { + if (this.MouseUp != null) this.MouseUp(this, e); + } + protected internal virtual void OnMouseClick(MouseEventArgs e) { + if (this.MouseClick != null) this.MouseClick(this, e); + } + protected internal virtual void OnMouseWheel(MouseEventArgs e) { + if (this.MouseWheel != null) this.MouseWheel(this, e); + } + protected internal virtual void OnMouseHWheel(MouseEventArgs 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(KeyPressEventArgs 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 this.BeginInvoke(method, null); } + public IAsyncResult BeginInvoke(Delegate method, params object[] args) { + if (this._Owner == null) return null; + return this._Owner.BeginInvoke(method, args); + } + public object Invoke(Delegate method) { return this.Invoke(method, null); } + public object Invoke(Delegate method, params object[] args) { + if (this._Owner == null) return null; + return this._Owner.Invoke(method, args); + } + } + + public delegate void STNodeControlPaintEventHandler(object sender, STNodeControlPaintEventArgs e); + + public class STNodeControlPaintEventArgs : EventArgs + { + /// + /// 绘制工具 + /// + public DrawingTools DrawingTools { get; private set; } + + public STNodeControlPaintEventArgs(DrawingTools dt) { + this.DrawingTools = dt; + } + } +} diff --git a/ST.Library.UI/STNodeEditor/STNodeControlCollection.cs b/ST.Library.UI/NodeEditor/STNodeControlCollection.cs old mode 100755 new mode 100644 similarity index 96% rename from ST.Library.UI/STNodeEditor/STNodeControlCollection.cs rename to ST.Library.UI/NodeEditor/STNodeControlCollection.cs index b0a9ad1..822e01b --- a/ST.Library.UI/STNodeEditor/STNodeControlCollection.cs +++ b/ST.Library.UI/NodeEditor/STNodeControlCollection.cs @@ -1,214 +1,214 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Collections; - -namespace ST.Library.UI -{ - public class STNodeControlCollection: IList, ICollection, IEnumerable - { - /* - * 为了确保安全在STNode中 仅继承者才能够访问集合 - */ - private int _Count; - public int Count { get { return _Count; } } - private STNodeControl[] m_controls; - private STNode m_owner; - - 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("添加对象不能为空"); - this.EnsureSpace(1); - int nIndex = this.IndexOf(control); - if (-1 == nIndex) { - nIndex = this._Count; - control.Owner = m_owner; - m_controls[this._Count++] = control; - this.Redraw(); - } - return nIndex; - } - - public void AddRange(STNodeControl[] controls) { - if (controls == null) throw new ArgumentNullException("添加对象不能为空"); - this.EnsureSpace(controls.Length); - foreach (var op in controls) { - if (op == null) throw new ArgumentNullException("添加对象不能为空"); - if (-1 == this.IndexOf(op)) { - op.Owner = m_owner; - m_controls[this._Count++] = op; - } - } - this.Redraw(); - } - - public void Clear() { - for (int i = 0; i < this._Count; i++) m_controls[i].Owner = null; - this._Count = 0; - m_controls = new STNodeControl[4]; - this.Redraw(); - } - - public bool Contains(STNodeControl option) { - return this.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 >= this._Count) - throw new IndexOutOfRangeException("索引越界"); - if (control == null) - throw new ArgumentNullException("插入对象不能为空"); - this.EnsureSpace(1); - for (int i = this._Count; i > index; i--) - m_controls[i] = m_controls[i - 1]; - control.Owner = m_owner; - m_controls[index] = control; - this._Count++; - this.Redraw(); - } - - public bool IsFixedSize { - get { return false; } - } - - public bool IsReadOnly { - get { return false; } - } - - public void Remove(STNodeControl control) { - int nIndex = this.IndexOf(control); - if (nIndex != -1) this.RemoveAt(nIndex); - } - - public void RemoveAt(int index) { - if (index < 0 || index >= this._Count) - throw new IndexOutOfRangeException("索引越界"); - this._Count--; - m_controls[index].Owner = null; - for (int i = index, Len = this._Count; i < Len; i++) - m_controls[i] = m_controls[i + 1]; - this.Redraw(); - } - - public STNodeControl this[int index] { - get { - if (index < 0 || index >= this._Count) - throw new IndexOutOfRangeException("索引越界"); - return m_controls[index]; - } - set { throw new InvalidOperationException("禁止重新赋值元素"); } - } - - public void CopyTo(Array array, int index) { - if (array == null) - throw new ArgumentNullException("数组不能为空"); - m_controls.CopyTo(array, index); - } - - public bool IsSynchronized { - get { return true; } - } - - public object SyncRoot { - get { return this; } - } - - public IEnumerator GetEnumerator() { - for (int i = 0, Len = this._Count; i < Len; i++) - yield return m_controls[i]; - } - /// - /// 确认空间是否足够 空间不足扩大容量 - /// - /// 需要增加的个数 - private void EnsureSpace(int elements) { - if (elements + this._Count > m_controls.Length) { - STNodeControl[] arrTemp = new STNodeControl[Math.Max(m_controls.Length * 2, elements + this._Count)]; - m_controls.CopyTo(arrTemp, 0); - m_controls = arrTemp; - } - } - - protected void Redraw() { - if (m_owner != null && m_owner.Owner != null) { - //m_owner.BuildSize(); - m_owner.Owner.Invalidate(m_owner.Owner.CanvasToControl(m_owner.Rectangle)); - } - } - //=================================================================================== - int IList.Add(object value) { - return this.Add((STNodeControl)value); - } - - void IList.Clear() { - this.Clear(); - } - - bool IList.Contains(object value) { - return this.Contains((STNodeControl)value); - } - - int IList.IndexOf(object value) { - return this.IndexOf((STNodeControl)value); - } - - void IList.Insert(int index, object value) { - this.Insert(index, (STNodeControl)value); - } - - bool IList.IsFixedSize { - get { return this.IsFixedSize; } - } - - bool IList.IsReadOnly { - get { return this.IsReadOnly; } - } - - void IList.Remove(object value) { - this.Remove((STNodeControl)value); - } - - void IList.RemoveAt(int index) { - this.RemoveAt(index); - } - - object IList.this[int index] { - get { - return this[index]; - } - set { - this[index] = (STNodeControl)value; - } - } - - void ICollection.CopyTo(Array array, int index) { - this.CopyTo(array, index); - } - - int ICollection.Count { - get { return this._Count; } - } - - bool ICollection.IsSynchronized { - get { return this.IsSynchronized; } - } - - object ICollection.SyncRoot { - get { return this.SyncRoot; } - } - - IEnumerator IEnumerable.GetEnumerator() { - return this.GetEnumerator(); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections; + +namespace ST.Library.UI.NodeEditor +{ + public class STNodeControlCollection: IList, ICollection, IEnumerable + { + /* + * 为了确保安全在STNode中 仅继承者才能够访问集合 + */ + private int _Count; + public int Count { get { return _Count; } } + private STNodeControl[] m_controls; + private STNode m_owner; + + 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("添加对象不能为空"); + this.EnsureSpace(1); + int nIndex = this.IndexOf(control); + if (-1 == nIndex) { + nIndex = this._Count; + control.Owner = m_owner; + m_controls[this._Count++] = control; + this.Redraw(); + } + return nIndex; + } + + public void AddRange(STNodeControl[] controls) { + if (controls == null) throw new ArgumentNullException("添加对象不能为空"); + this.EnsureSpace(controls.Length); + foreach (var op in controls) { + if (op == null) throw new ArgumentNullException("添加对象不能为空"); + if (-1 == this.IndexOf(op)) { + op.Owner = m_owner; + m_controls[this._Count++] = op; + } + } + this.Redraw(); + } + + public void Clear() { + for (int i = 0; i < this._Count; i++) m_controls[i].Owner = null; + this._Count = 0; + m_controls = new STNodeControl[4]; + this.Redraw(); + } + + public bool Contains(STNodeControl option) { + return this.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 >= this._Count) + throw new IndexOutOfRangeException("索引越界"); + if (control == null) + throw new ArgumentNullException("插入对象不能为空"); + this.EnsureSpace(1); + for (int i = this._Count; i > index; i--) + m_controls[i] = m_controls[i - 1]; + control.Owner = m_owner; + m_controls[index] = control; + this._Count++; + this.Redraw(); + } + + public bool IsFixedSize { + get { return false; } + } + + public bool IsReadOnly { + get { return false; } + } + + public void Remove(STNodeControl control) { + int nIndex = this.IndexOf(control); + if (nIndex != -1) this.RemoveAt(nIndex); + } + + public void RemoveAt(int index) { + if (index < 0 || index >= this._Count) + throw new IndexOutOfRangeException("索引越界"); + this._Count--; + m_controls[index].Owner = null; + for (int i = index, Len = this._Count; i < Len; i++) + m_controls[i] = m_controls[i + 1]; + this.Redraw(); + } + + public STNodeControl this[int index] { + get { + if (index < 0 || index >= this._Count) + throw new IndexOutOfRangeException("索引越界"); + return m_controls[index]; + } + set { throw new InvalidOperationException("禁止重新赋值元素"); } + } + + public void CopyTo(Array array, int index) { + if (array == null) + throw new ArgumentNullException("数组不能为空"); + m_controls.CopyTo(array, index); + } + + public bool IsSynchronized { + get { return true; } + } + + public object SyncRoot { + get { return this; } + } + + public IEnumerator GetEnumerator() { + for (int i = 0, Len = this._Count; i < Len; i++) + yield return m_controls[i]; + } + /// + /// 确认空间是否足够 空间不足扩大容量 + /// + /// 需要增加的个数 + private void EnsureSpace(int elements) { + if (elements + this._Count > m_controls.Length) { + STNodeControl[] arrTemp = new STNodeControl[Math.Max(m_controls.Length * 2, elements + this._Count)]; + m_controls.CopyTo(arrTemp, 0); + m_controls = arrTemp; + } + } + + protected void Redraw() { + if (m_owner != null && m_owner.Owner != null) { + //m_owner.BuildSize(); + m_owner.Owner.Invalidate(m_owner.Owner.CanvasToControl(m_owner.Rectangle)); + } + } + //=================================================================================== + int IList.Add(object value) { + return this.Add((STNodeControl)value); + } + + void IList.Clear() { + this.Clear(); + } + + bool IList.Contains(object value) { + return this.Contains((STNodeControl)value); + } + + int IList.IndexOf(object value) { + return this.IndexOf((STNodeControl)value); + } + + void IList.Insert(int index, object value) { + this.Insert(index, (STNodeControl)value); + } + + bool IList.IsFixedSize { + get { return this.IsFixedSize; } + } + + bool IList.IsReadOnly { + get { return this.IsReadOnly; } + } + + void IList.Remove(object value) { + this.Remove((STNodeControl)value); + } + + void IList.RemoveAt(int index) { + this.RemoveAt(index); + } + + object IList.this[int index] { + get { + return this[index]; + } + set { + this[index] = (STNodeControl)value; + } + } + + void ICollection.CopyTo(Array array, int index) { + this.CopyTo(array, index); + } + + int ICollection.Count { + get { return this._Count; } + } + + bool ICollection.IsSynchronized { + get { return this.IsSynchronized; } + } + + object ICollection.SyncRoot { + get { return this.SyncRoot; } + } + + IEnumerator IEnumerable.GetEnumerator() { + return this.GetEnumerator(); + } + } +} diff --git a/ST.Library.UI/STNodeEditor/STNodeEditor.cs b/ST.Library.UI/NodeEditor/STNodeEditor.cs old mode 100755 new mode 100644 similarity index 86% rename from ST.Library.UI/STNodeEditor/STNodeEditor.cs rename to ST.Library.UI/NodeEditor/STNodeEditor.cs index aaed817..75bb669 --- a/ST.Library.UI/STNodeEditor/STNodeEditor.cs +++ b/ST.Library.UI/NodeEditor/STNodeEditor.cs @@ -1,1954 +1,2109 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using System.IO; -using System.Windows.Forms; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Threading; -using System.ComponentModel; -using System.Reflection; -using System.IO.Compression; -/* -MIT License - -Copyright (c) 2021 DebugST@crystal_lz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - */ -/* - * time: 2021-01-06 - * Author: Crystal_lz - * blog: st233.com - * Github: DebugST.github.io - */ -namespace ST.Library.UI -{ - public class STNodeEditor : Control - { - private const UInt32 WM_MOUSEHWHEEL = 0x020E; - protected static readonly Type m_type_node = typeof(STNode); - - #region protected enum,struct -------------------------------------------------------------------------------------- - - protected enum CanvasAction //当前鼠标移动操作表示进行下列哪一个行为 - { - None, //无 - MoveNode, //正在移动 Node - ConnectOption, //正在连接 Option - SelectRectangle, //正在选择矩形区域 - DrawMarkDetails //正在绘制标记信息详情 - } - - protected struct MagnetInfo - { - public bool XMatched; //X轴是否有磁铁匹配上 - public bool YMatched; - public int X; //与X轴那个数字匹配上 - public int Y; - public int OffsetX; //当前节点X位置与匹配上的X的相对偏移 - public int OffsetY; - } - - #endregion - - #region Properties ------------------------------------------------------------------------------------------------------ - - private float _CanvasOffsetX; - /// - /// 获取画布原点相对于控件 X 方向上的偏移位置 - /// - [Browsable(false)] - public float CanvasOffsetX { - get { return _CanvasOffsetX; } - } - - private float _CanvasOffsetY; - /// - /// 获取画布原点相对于控件 Y 方向上的偏移位置 - /// - [Browsable(false)] - public float CanvasOffsetY { - get { return _CanvasOffsetY; } - } - - private PointF _CanvasOffset; - /// - /// 获取画布原点相对于控件偏移位置 - /// - [Browsable(false)] - public PointF CanvasOffset { - get { - _CanvasOffset.X = _CanvasOffsetX; - _CanvasOffset.Y = _CanvasOffsetY; - return _CanvasOffset; - } - } - - private Rectangle _CanvasValidBounds; - /// - /// 获取画布中的有被用到的有效区域 - /// - [Browsable(false)] - public Rectangle CanvasValidBounds { - get { return _CanvasValidBounds; } - } - - private float _CanvasScale = 1; - /// - /// 获取画布的缩放比例 - /// - [Browsable(false)] - public float CanvasScale { - get { return _CanvasScale; } - } - - private float _Curvature = 0.3F; - /// - /// 获取或设置 Option 之间连线的曲度 - /// - [Browsable(false)] - public float Curvature { - get { return _Curvature; } - set { - if (value < 0) value = 0; - if (value > 1) value = 1; - _Curvature = value; - if (m_dic_gp_info.Count != 0) this.BuildLinePath(); - } - } - - private bool _Magnet = true; - /// - /// 获取或设置移动画布中 Node 时候 是否启用磁铁效果 - /// - [Description("获取或设置移动画布中 Node 时候 是否启用磁铁效果"), DefaultValue(true)] - public bool Magnet { - get { return _Magnet; } - set { _Magnet = value; } - } - - private bool _ShowBorder = true; - /// - /// 获取或设置 移动画布中是否显示 Node 边框 - /// - [Description("获取或设置 移动画布中是否显示 Node 边框"), DefaultValue(true)] - public bool ShowBorder { - get { return _ShowBorder; } - set { - _ShowBorder = value; - this.Invalidate(); - } - } - - private bool _ShowGrid = true; - /// - /// 获取或设置画布中是否绘制背景网格线条 - /// - [Description("获取或设置画布中是否绘制背景网格线条"), DefaultValue(true)] - public bool ShowGrid { - get { return _ShowGrid; } - set { - _ShowGrid = value; - this.Invalidate(); - } - } - - private bool _ShowLocation = true; - /// - /// 获取或设置是否在画布边缘显示超出视角的 Node 位置信息 - /// - [Description("获取或设置是否在画布边缘显示超出视角的 Node 位置信息"), DefaultValue(true)] - public bool ShowLocation { - get { return _ShowLocation; } - set { - _ShowLocation = value; - this.Invalidate(); - } - } - - private STNodeCollection _Nodes; - /// - /// 获取画布中 Node 集合 - /// - [Browsable(false)] - public STNodeCollection Nodes { - get { - return _Nodes; - } - } - - private STNode _ActiveNode; - /// - /// 获取或设置当前画布中被选中的活动 Node - /// - [Browsable(false)] - public STNode ActiveNode { - get { return _ActiveNode; } - //set { - // if (value == _ActiveSelectedNode) return; - // if (_ActiveSelectedNode != null) _ActiveSelectedNode.OnLostFocus(new EventArgs()); - // _ActiveSelectedNode = value; - // _ActiveSelectedNode.IsActive = true; - // this.Invalidate(); - // this.OnSelectedChanged(new EventArgs()); - //} - } - - private STNode _HoverNode; - /// - /// 获取当前画布中鼠标悬停的 Node - /// - [Browsable(false)] - public STNode HoverNode { - get { return _HoverNode; } - } - //========================================color================================ - private Color _GridColor = Color.Black; - /// - /// 获取或设置绘制画布背景时 网格线条颜色 - /// - [Description("获取或设置绘制画布背景时 网格线条颜色"), DefaultValue(typeof(Color), "Black")] - public Color GridColor { - get { return _GridColor; } - set { - _GridColor = value; - this.Invalidate(); - } - } - - private Color _BorderColor = Color.Black; - /// - /// 获取或设置画布中 Node 边框颜色 - /// - [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 = this.CreateBorderImage(value); - this.Invalidate(); - } - } - - private Color _BorderHoverColor = Color.Gray; - /// - /// 获取或设置画布中悬停 Node 边框颜色 - /// - [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 = this.CreateBorderImage(value); - this.Invalidate(); - } - } - - private Color _BorderSelectColor = Color.Orange; - /// - /// 获取或设置画布中选中 Node 边框颜色 - /// - [Description("获取或设置画布中选中 Node 边框颜色"), DefaultValue(typeof(Color), "Orange")] - public Color BorderSelectColor { - get { return _BorderSelectColor; } - set { - _BorderSelectColor = value; - if (m_img_border_selected != null) m_img_border_selected.Dispose(); - m_img_border_selected = this.CreateBorderImage(value); - this.Invalidate(); - } - } - - private Color _BorderActiveColor = Color.OrangeRed; - /// - /// 获取或设置画布中活动 Node 边框颜色 - /// - [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 = this.CreateBorderImage(value); - this.Invalidate(); - } - } - - private Color _MarkForeColor = Color.White; - /// - /// 获取或设置画布绘制 Node 标记详情采用的前景色 - /// - [Description("获取或设置画布绘制 Node 标记详情采用的前景色"), DefaultValue(typeof(Color), "White")] - public Color MarkForeColor { - get { return _MarkBackColor; } - set { - _MarkBackColor = value; - this.Invalidate(); - } - } - - private Color _MarkBackColor = Color.FromArgb(180, Color.Black); - /// - /// 获取或设置画布绘制 Node 标记详情采用的背景色 - /// - [Description("获取或设置画布绘制 Node 标记详情采用的背景色"), DefaultValue(typeof(Color), "Black")] - public Color MarkBackColor { - get { return _MarkBackColor; } - set { - _MarkBackColor = value; - this.Invalidate(); - } - } - - private Color _MagnetLineColor = Color.Magenta; - /// - /// 获取或设置画布中移动 Node 时候 磁铁标记线颜色 - /// - [Description("获取或设置画布中移动 Node 时候 磁铁标记线颜色"), DefaultValue(typeof(Color), "Magenta")] - public Color MagnetLineColor { - get { return _MagnetLineColor; } - set { _MagnetLineColor = value; } - } - - private Color _SelectedRectangleColor = Color.DodgerBlue; - /// - /// 获取或设置画布中选择矩形区域的颜色 - /// - [Description("获取或设置画布中选择矩形区域的颜色"), DefaultValue(typeof(Color), "DodgerBlue")] - public Color SelectedRectangleColor { - get { return _SelectedRectangleColor; } - set { _SelectedRectangleColor = value; } - } - - private Color _HighLineColor = Color.Cyan; - /// - /// 获取或设置画布中高亮连线的颜色 - /// - [Description("获取或设置画布中高亮连线的颜色"), DefaultValue(typeof(Color), "Cyan")] - public Color HighLineColor { - get { return _HighLineColor; } - set { _HighLineColor = value; } - } - - private Color _LocationForeColor = Color.Red; - /// - /// 获取或设置画布中边缘位置提示区域前景色 - /// - [Description("获取或设置画布中边缘位置提示区域前景色"), DefaultValue(typeof(Color), "Red")] - public Color LocationForeColor { - get { return _LocationForeColor; } - set { - _LocationForeColor = value; - this.Invalidate(); - } - } - - private Color _LocationBackColor = Color.FromArgb(120, Color.Black); - /// - /// 获取或设置画布中边缘位置提示区域背景色 - /// - [Description("获取或设置画布中边缘位置提示区域背景色")] - public Color LocationBackColor { - get { return _LocationBackColor; } - set { - _LocationBackColor = value; - this.Invalidate(); - } - } - - private Color _UnknownTypeColor = Color.Gray; - /// - /// 获取或设置画布中当 Node 中 Option 数据类型无法确定时应当使用的颜色 - /// - [Description("获取或设置画布中当 Node 中 Option 数据类型无法确定时应当使用的颜色"), DefaultValue(typeof(Color), "Gray")] - public Color UnknownTypeColor { - get { return _UnknownTypeColor; } - set { - _UnknownTypeColor = value; - this.Invalidate(); - } - } - - private Dictionary _TypeColor = new Dictionary(); - /// - /// 获取或设置画布中 Node 中 Option 数据类型预设颜色 - /// - [Browsable(false)] - public Dictionary TypeColor { - get { return _TypeColor; } - } - - #endregion - - #region protected properties ---------------------------------------------------------------------------------------- - /// - /// 当前鼠标在控件中的实时位置 - /// - 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; - /// - /// 用于保存连线过程中保存点下 Option 的起点坐标 - /// - protected Point m_pt_dot_down; - /// - /// 用于保存连线过程中鼠标点下的起点Option 当MouseUP时候 确定是否连接此节点 - /// - protected STNodeOption m_option_down; - - #endregion - - public STNodeEditor() { - this.SetStyle(ControlStyles.UserPaint, true); - this.SetStyle(ControlStyles.ResizeRedraw, true); - this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); - this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); - this.SetStyle(ControlStyles.SupportsTransparentBackColor, true); - this._Nodes = new STNodeCollection(this); - this.BackColor = Color.FromArgb(255, 34, 34, 34); - this.Size = new Size(200, 200); - } - - #region private fields -------------------------------------------------------------------------------------- - - private DrawingTools m_drawing_tools; - private NodeFindInfo m_find = new NodeFindInfo(); - private MagnetInfo m_mi = new MagnetInfo(); - - private RectangleF m_rect_select = new RectangleF(); - //节点边框预设图案 - private Image m_img_border; - private Image m_img_border_hover; - private Image m_img_border_selected; - private Image m_img_border_active; - //用于鼠标滚动或者触摸板移动画布时候的动画效果 该值为需要移动到的真实坐标地址 查看->MoveCanvasThread() - private float m_real_canvas_x; - private float m_real_canvas_y; - //用于移动节点时候 保存鼠标点下时候选中的节点初始坐标 - private Dictionary m_dic_pt_selected = new Dictionary(); - //用于磁铁效果 移动节点时候 非选择节点的统计出来的需要参与磁铁效果的坐标 查看->BuildMagnetLocation() - private List m_lst_magnet_x = new List(); - private List m_lst_magnet_y = new List(); - //用于磁铁效果 移动节点时候 活动选择节点统计出来需要参与磁铁效果的坐标 查看->CheckMagnet() - private List m_lst_magnet_mx = new List(); - private List m_lst_magnet_my = new List(); - //用于鼠标滚动中计算时间触发间隔 根据间隔不同 画布产生的位移不同 查看->OnMouseWheel(),OnMouseHWheel() - 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; //是否向下(Node or NodeControls)传递鼠标相关事件 如断开连接相关操作不应向下传递 - 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, 4f); //用于绘制鼠标悬停时候的线条 - private GraphicsPath m_gp_hover; //当前鼠标悬停的连线路径 - private StringFormat m_sf = new StringFormat(); //文本格式 用于Mark绘制时候 设置文本格式 - //保存每个连接线条与之对应的节点关系 - private Dictionary m_dic_gp_info = new Dictionary(); - //保存超出视觉区域的 Node 的位置 - private List m_lst_node_out = new List(); - //当前编辑器已加载的 Node 类型 用于从文件或者数据中加载节点使用 - private Dictionary m_dic_type = new Dictionary(); - - 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; - - #endregion - - #region event ---------------------------------------------------------------------------------------------------- - /// - /// 选择的节点发生变化时候发生 - /// - [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; - - protected virtual void OnSelectedChanged(EventArgs e) { - if (this.SelectedChanged != null) this.SelectedChanged(this, e); - } - protected virtual void OnHoverChanged(EventArgs e) { - if (this.HoverChanged != null) this.HoverChanged(this, e); - } - protected internal virtual void OnNodeAdded(STNodeEditorEventArgs e) { - if (this.NodeAdded != null) this.NodeAdded(this, e); - } - protected internal virtual void OnNodeRemoved(STNodeEditorEventArgs e) { - 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) { - if (this.OptionConnected != null) this.OptionConnected(this, e); - } - protected internal virtual void OnOptionDisConnected(STNodeEditorOptionEventArgs e) { - if (this.OptionDisConnected != null) this.OptionDisConnected(this, e); - } - protected internal virtual void OnOptionConnecting(STNodeEditorOptionEventArgs e) { - if (this.OptionConnecting != null) this.OptionConnecting(this, e); - } - protected internal virtual void OnOptionDisConnecting(STNodeEditorOptionEventArgs e) { - if (this.OptionDisConnecting != null) this.OptionDisConnecting(this, e); - } - - #endregion event - - #region override ----------------------------------------------------------------------------------------------------- - - protected override void OnCreateControl() { - m_drawing_tools = new DrawingTools() { - Pen = new Pen(Color.Black, 1), - SolidBrush = new SolidBrush(Color.Black) - }; - m_img_border = this.CreateBorderImage(this._BorderColor); - m_img_border_active = this.CreateBorderImage(this._BorderActiveColor); - m_img_border_hover = this.CreateBorderImage(this._BorderHoverColor); - m_img_border_selected = this.CreateBorderImage(this._BorderSelectColor); - base.OnCreateControl(); - new Thread(this.MoveCanvasThread) { IsBackground = true }.Start(); - new Thread(this.ShowAlertThread) { IsBackground = true }.Start(); - m_sf = new StringFormat(); - m_sf.Alignment = StringAlignment.Near; - m_sf.FormatFlags = StringFormatFlags.NoWrap; - m_sf.SetTabStops(0, new float[] { 40 }); - } - - protected override void WndProc(ref Message m) { - base.WndProc(ref m); - Point pt = new Point(((int)m.LParam) >> 16, (ushort)m.LParam); - pt = base.PointToClient(pt); - if (m.Msg == WM_MOUSEHWHEEL) { //获取水平滚动消息 - MouseButtons mb = MouseButtons.None; - int n = (ushort)m.WParam; - if ((n & 0x0001) == 0x0001) mb |= MouseButtons.Left; - if ((n & 0x0010) == 0x0010) mb |= MouseButtons.Middle; - if ((n & 0x0002) == 0x0002) mb |= MouseButtons.Right; - if ((n & 0x0020) == 0x0020) mb |= MouseButtons.XButton1; - if ((n & 0x0040) == 0x0040) mb |= MouseButtons.XButton2; - this.OnMouseHWheel(new MouseEventArgs(mb, 0, pt.X, pt.Y, ((int)m.WParam) >> 16)); - } - } - - protected override void OnPaint(PaintEventArgs e) { - base.OnPaint(e); - Graphics g = e.Graphics; - g.Clear(this.BackColor); - g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; - m_drawing_tools.Graphics = g; - SolidBrush brush = m_drawing_tools.SolidBrush; - - if (this._ShowGrid) this.OnDrawGrid(m_drawing_tools, this.Width, this.Height); - - g.TranslateTransform(this._CanvasOffsetX, this._CanvasOffsetY); //移动坐标系 - g.ScaleTransform(this._CanvasScale, this._CanvasScale); //缩放绘图表面 - - this.OnDrawConnectedLine(m_drawing_tools); - this.OnDrawNode(m_drawing_tools, this.ControlToCanvas(this.ClientRectangle)); - - if (m_ca == CanvasAction.ConnectOption) { //如果正在连线 - m_drawing_tools.Pen.Color = this._HighLineColor; - g.SmoothingMode = SmoothingMode.HighQuality; - if (m_option_down.IsInput) - this.DrawBezier(g, m_drawing_tools.Pen, m_pt_in_canvas, m_pt_dot_down, this._Curvature); - else - this.DrawBezier(g, m_drawing_tools.Pen, m_pt_dot_down, m_pt_in_canvas, this._Curvature); - } - //重置绘图坐标 我认为除了节点以外的其它 修饰相关的绘制不应该在Canvas坐标系中绘制 而应该使用控件的坐标进行绘制 不然会受到缩放比影响 - g.ResetTransform(); - - switch (m_ca) { - case CanvasAction.MoveNode: //移动过程中 绘制对齐参考线 - this.OnDrawMagnetLine(m_drawing_tools, m_mi); - break; - case CanvasAction.SelectRectangle: //绘制矩形选取 - this.OnDrawSelectedRectangle(m_drawing_tools, this.CanvasToControl(m_rect_select)); - break; - case CanvasAction.DrawMarkDetails: //绘制标记信息详情 - if (!string.IsNullOrEmpty(m_find.Mark)) this.OnDrawMark(m_drawing_tools); - break; - } - - if (this._ShowLocation) this.OnDrawNodeOutLocation(m_drawing_tools, this.Size, m_lst_node_out); - this.OnDrawAlert(g); - } - - protected override void OnMouseDown(MouseEventArgs e) { - base.OnMouseDown(e); - this.Focus(); - m_ca = CanvasAction.None; - m_mi.XMatched = m_mi.YMatched = false; - m_pt_down_in_control = e.Location; - m_pt_down_in_canvas.X = ((e.X - this._CanvasOffsetX) / this._CanvasScale); - m_pt_down_in_canvas.Y = ((e.Y - this._CanvasOffsetY) / this._CanvasScale); - m_pt_canvas_old.X = this._CanvasOffsetX; - m_pt_canvas_old.Y = this._CanvasOffsetY; - - if (m_gp_hover != null && e.Button == MouseButtons.Right) { //断开连接 - this.DisConnectionHover(); - m_is_process_mouse_event = false; //终止MouseClick与MouseUp向下传递 - return; - } - - NodeFindInfo nfi = this.FindNodeFromPoint(m_pt_down_in_canvas); - if (!string.IsNullOrEmpty(nfi.Mark)) { //如果点下的是标记信息 - m_ca = CanvasAction.DrawMarkDetails; - this.Invalidate(); - return; - } - - if (nfi.NodeOption != null) { //如果点下的Option的连接点 - this.StartConnect(nfi.NodeOption); - return; - } - - if (nfi.Node != null) { - nfi.Node.OnMouseDown(new MouseEventArgs(e.Button, e.Clicks, (int)m_pt_down_in_canvas.X - nfi.Node.Left, (int)m_pt_down_in_canvas.Y - nfi.Node.Top, e.Delta)); - bool bCtrlDown = (Control.ModifierKeys & Keys.Control) == Keys.Control; - if (bCtrlDown) { - if (nfi.Node.IsSelected) { - nfi.Node.SetSelected(false, false); - m_hs_node_selected.Remove(nfi.Node); - if (nfi.Node == this._ActiveNode) { - this.SetActiveNode(null); - } - } else { - nfi.Node.SetSelected(true, false); - m_hs_node_selected.Add(nfi.Node); - } - this.Invalidate(); - this.OnSelectedChanged(new EventArgs()); - return; - } else if (!nfi.Node.IsSelected) { - foreach (var n in m_hs_node_selected) n.SetSelected(false, false); - m_hs_node_selected.Clear(); - } - nfi.Node.SetSelected(true, false); - m_hs_node_selected.Add(nfi.Node); //添加到已选择节点 - if (this.PointInRectangle(nfi.Node.TitleRectangle, m_pt_down_in_canvas.X, m_pt_down_in_canvas.Y)) { - if (e.Button == MouseButtons.Right) { - if (nfi.Node.ContextMenuStrip != null) { - nfi.Node.ContextMenuStrip.Show(this.PointToScreen(e.Location)); - } - } else { - m_dic_pt_selected.Clear(); - foreach (STNode n in m_hs_node_selected)//记录已选择节点位置 如果需要移动已选中节点时候 将会有用 - m_dic_pt_selected.Add(n, n.Location); - m_ca = CanvasAction.MoveNode; //如果点下的是节点的标题 则可以移动该节点 - if (this._Magnet) this.BuildMagnetLocation(); //建立磁铁需要的坐标 如果需要移动已选中节点时候 将会有用 - } - } - //nfi.Node.OnMouseDown(new MouseEventArgs(e.Button, e.Clicks, (int)m_pt_down_in_canvas.X - nfi.Node.Left, (int)m_pt_down_in_canvas.Y - nfi.Node.Top, e.Delta)); - } else { - foreach (var n in m_hs_node_selected) n.SetSelected(false, false);//没有点下任何东西 清空已经选择节点 - m_ca = CanvasAction.SelectRectangle; //进入矩形区域选择模式 - m_rect_select.Width = m_rect_select.Height = 0; - } - this.SetActiveNode(nfi.Node); - } - - protected override void OnMouseMove(MouseEventArgs e) { - base.OnMouseMove(e); - m_pt_in_control = e.Location; - m_pt_in_canvas.X = ((e.X - this._CanvasOffsetX) / this._CanvasScale); - m_pt_in_canvas.Y = ((e.Y - this._CanvasOffsetY) / this._CanvasScale); - - if (e.Button == MouseButtons.Middle) { //鼠标中键移动画布 - this._CanvasOffsetX = m_real_canvas_x = m_pt_canvas_old.X + (e.X - m_pt_down_in_control.X); - this._CanvasOffsetY = m_real_canvas_y = m_pt_canvas_old.Y + (e.Y - m_pt_down_in_control.Y); - this.Invalidate(); - return; - } - if (e.Button == MouseButtons.Left) { //如果鼠标左键点下 判断行为 - m_gp_hover = null; - switch (m_ca) { - case CanvasAction.MoveNode: this.MoveNode(e.Location); return; //当前移动节点 - case CanvasAction.ConnectOption: this.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 n in this._Nodes) { - n.SetSelected(m_rect_select.IntersectsWith(n.Rectangle), false); - } - this.Invalidate(); - return; - } - } - //若不存在行为 则判断鼠标下方是否存在其他对象 - NodeFindInfo nfi = this.FindNodeFromPoint(m_pt_in_canvas); - bool bRedraw = false; - if (this._HoverNode != nfi.Node) { //鼠标悬停到Node上 - if (nfi.Node != null) nfi.Node.OnMouseEnter(new EventArgs()); - if (this._HoverNode != null) - this._HoverNode.OnMouseLeave(new MouseEventArgs(e.Button, e.Clicks, - (int)m_pt_in_canvas.X - this._HoverNode.Left, - (int)m_pt_in_canvas.Y - this._HoverNode.Top, e.Delta)); - this._HoverNode = nfi.Node; - this.OnHoverChanged(new EventArgs()); - bRedraw = true; - } - if (this._HoverNode != null) { - this._HoverNode.OnMouseMove(new MouseEventArgs(e.Button, e.Clicks, - (int)m_pt_in_canvas.X - this._HoverNode.Left, - (int)m_pt_in_canvas.Y - this._HoverNode.Top, e.Delta)); - m_gp_hover = null; - } else { - GraphicsPath gp = null; - foreach (var v in m_dic_gp_info) { //判断鼠标是否悬停到连线路径上 - if (v.Key.IsOutlineVisible(m_pt_in_canvas, m_p_line_hover)) { - gp = v.Key; - break; - } - } - if (m_gp_hover != gp) { - m_gp_hover = gp; - bRedraw = true; - } - } - if (bRedraw) this.Invalidate(); - } - - protected override void OnMouseUp(MouseEventArgs e) { - base.OnMouseUp(e); - var nfi = this.FindNodeFromPoint(m_pt_in_canvas); - switch (m_ca) { //鼠标抬起时候 判断行为 - case CanvasAction.MoveNode: //若正在移动Node 则重新记录当前位置 - foreach (STNode n in m_dic_pt_selected.Keys.ToList()) m_dic_pt_selected[n] = n.Location; - break; - case CanvasAction.ConnectOption: //若正在连线 则结束连接 - if (e.Location == m_pt_down_in_control) break; - if (nfi.NodeOption != null) { - if (m_option_down.IsInput) - nfi.NodeOption.ConnectOption(m_option_down); - else - m_option_down.ConnectOption(nfi.NodeOption); - } - break; - case CanvasAction.SelectRectangle: //若正在进行选取 则判断是否有选中目标 - m_hs_node_selected.Clear(); - foreach (STNode n in this._Nodes) { - if (n.IsSelected) m_hs_node_selected.Add(n); - } - if (m_hs_node_selected.Count != 0) this.OnSelectedChanged(new EventArgs()); - break; - } - if (m_is_process_mouse_event && this._ActiveNode != null) { - var mea = new MouseEventArgs(e.Button, e.Clicks, - (int)m_pt_in_canvas.X - this._ActiveNode.Left, - (int)m_pt_in_canvas.Y - this._ActiveNode.Top, e.Delta); - this._ActiveNode.OnMouseUp(mea); - } - m_is_process_mouse_event = true; //当前为断开连接操作不进行事件传递 下次将接受事件 - m_ca = CanvasAction.None; - this.Invalidate(); - } - - protected override void OnMouseLeave(EventArgs e) { - base.OnMouseLeave(e); - if (this._HoverNode != null) this._HoverNode.OnMouseLeave(e); - this._HoverNode = null; - this.Invalidate(); - } - - protected override void OnMouseWheel(MouseEventArgs e) { - base.OnMouseWheel(e); - if ((Control.ModifierKeys & Keys.Control) == Keys.Control) { - float f = this._CanvasScale + (e.Delta < 0 ? -0.1f : 0.1f); - this.ScaleCanvas(f, this.Width / 2, this.Height / 2); - } else { - var nfi = this.FindNodeFromPoint(m_pt_in_canvas); - if (this._HoverNode != null) { - this._HoverNode.OnMouseWheel(new MouseEventArgs(e.Button, e.Clicks, - (int)m_pt_in_canvas.X - this._HoverNode.Left, - (int)m_pt_in_canvas.Y - this._HoverNode.Top, e.Delta)); - return; - } - int t = (int)DateTime.Now.Subtract(m_dt_vw).TotalMilliseconds; - if (t <= 30) t = 40; - else if (t <= 100) t = 20; - else if (t <= 150) t = 10; - else if (t <= 300) t = 4; - else t = 2; - this.MoveCanvas(this._CanvasOffsetX, m_real_canvas_y + (e.Delta < 0 ? -t : t), true, CanvasMoveArgs.Top);//process mouse mid - m_dt_vw = DateTime.Now; - } - } - - protected virtual void OnMouseHWheel(MouseEventArgs e) { - if ((Control.ModifierKeys & Keys.Control) == Keys.Control) return; - if (this._HoverNode != null) { - this._HoverNode.OnMouseWheel(new MouseEventArgs(e.Button, e.Clicks, - (int)m_pt_in_canvas.X - this._HoverNode.Left, - (int)m_pt_in_canvas.Y - this._HoverNode.Top, e.Delta)); - return; - } - int t = (int)DateTime.Now.Subtract(m_dt_hw).TotalMilliseconds; - if (t <= 30) t = 40; - else if (t <= 100) t = 20; - else if (t <= 150) t = 10; - else if (t <= 300) t = 4; - else t = 2; - this.MoveCanvas(m_real_canvas_x + (e.Delta > 0 ? -t : t), this._CanvasOffsetY, true, CanvasMoveArgs.Left); - m_dt_hw = DateTime.Now; - } - //===========================for node other event================================== - protected override void OnMouseClick(MouseEventArgs e) { - base.OnMouseClick(e); - if (this._ActiveNode != null && m_is_process_mouse_event) { - if (!this.PointInRectangle(this._ActiveNode.Rectangle, m_pt_in_canvas.X, m_pt_in_canvas.Y)) return; - this._ActiveNode.OnMouseClick(new MouseEventArgs(e.Button, e.Clicks, - (int)m_pt_down_in_canvas.X - this._ActiveNode.Left, - (int)m_pt_down_in_canvas.Y - this._ActiveNode.Top, e.Delta)); - } - } - - protected override void OnKeyDown(KeyEventArgs e) { - base.OnKeyDown(e); - if (this._ActiveNode != null) this._ActiveNode.OnKeyDown(e); - } - - protected override void OnKeyUp(KeyEventArgs e) { - base.OnKeyUp(e); - if (this._ActiveNode != null) this._ActiveNode.OnKeyUp(e); - } - - protected override void OnKeyPress(KeyPressEventArgs e) { - base.OnKeyPress(e); - if (this._ActiveNode != null) this._ActiveNode.OnKeyPress(e); - } - - #endregion - - #region protected ---------------------------------------------------------------------------------------------------- - /// - /// 当绘制背景网格线时候发生 - /// - /// 绘制工具 - /// 需要绘制宽度 - /// 需要绘制高度 - protected virtual void OnDrawGrid(DrawingTools dt, int nWidth, int nHeight) { - Graphics g = dt.Graphics; - using (Pen p_2 = new Pen(Color.FromArgb(65, this._GridColor))) { - using (Pen p_1 = new Pen(Color.FromArgb(30, this._GridColor))) { - float nIncrement = (20 * this._CanvasScale);             //网格间的间隔 根据比例绘制 - int n = 5 - (int)(this._CanvasOffsetX / nIncrement); - for (float f = this._CanvasOffsetX % nIncrement; f < nWidth; f += nIncrement) - g.DrawLine((n++ % 5 == 0 ? p_2 : p_1), f, 0, f, nHeight); - n = 5 - (int)(this._CanvasOffsetY / nIncrement); - for (float f = this._CanvasOffsetY % nIncrement; f < nHeight; f += nIncrement) - g.DrawLine((n++ % 5 == 0 ? p_2 : p_1), 0, f, nWidth, f); - //原点两天线 - p_1.Color = Color.FromArgb(this._Nodes.Count == 0 ? 255 : 120, this._GridColor); - g.DrawLine(p_1, this._CanvasOffsetX, 0, this._CanvasOffsetX, nHeight); - g.DrawLine(p_1, 0, this._CanvasOffsetY, nWidth, this._CanvasOffsetY); - } - } - } - /// - /// 当绘制 Node 时候发生 - /// - /// 绘制工具 - /// 可视画布区域大小 - protected virtual void OnDrawNode(DrawingTools dt, Rectangle rect) { - m_lst_node_out.Clear(); //清空超出视觉区域的 Node 的坐标 - //var rect_canvas_display = this.ControlToCanvas(rect); - Image img_border = null; - foreach (STNode n in this._Nodes) { - //n.CheckSize(dt); - img_border = m_img_border; - if (this._ShowBorder) { //如果绘制边框 判断状态 - if (this._ActiveNode == n) img_border = m_img_border_active; - else if (/*m_hs_node_selected.Contains(n)*/n.IsSelected) img_border = m_img_border_selected; - else if (this._HoverNode == n) img_border = m_img_border_hover; - this.RenderBorder(dt.Graphics, n.Rectangle, img_border); - if (!string.IsNullOrEmpty(n.Mark)) this.RenderBorder(dt.Graphics, n.MarkRectangle, img_border); - } - n.OnDrawNode(dt); //调用 Node 进行自身绘制主体部分 - if (!string.IsNullOrEmpty(n.Mark)) n.OnDrawMark(dt); //调用 Node 进行自身绘制 Mark 区域 - if (!rect.IntersectsWith(n.Rectangle)) { - m_lst_node_out.Add(n.Location); //判断此 Node 是否超出视觉区域 - } - } - } - /// - /// 当绘制已连接路径时候发生 - /// - /// 绘制工具 - protected virtual void OnDrawConnectedLine(DrawingTools dt) { - Graphics g = dt.Graphics; - g.SmoothingMode = SmoothingMode.HighQuality; - m_p_line_hover.Color = Color.FromArgb(50, 0, 0, 0); - var t = typeof(object); - foreach (STNode n in this._Nodes) { - foreach (STNodeOption op in n.OutputOptions) { - if (op.DotColor != Color.Transparent) //确定线条颜色 - m_p_line.Color = op.DotColor; - else { - if (op.DataType == t) - m_p_line.Color = this._UnknownTypeColor; - else - m_p_line.Color = this._TypeColor.ContainsKey(op.DataType) ? this._TypeColor[op.DataType] : this._UnknownTypeColor;//value can not be null - } - foreach (var v in op.ConnectedOption) { - this.DrawBezier(g, m_p_line_hover, op.DotLeft + op.DotSize, op.DotTop + op.DotSize / 2, - v.DotLeft - 1, v.DotTop + v.DotSize / 2, this._Curvature); - this.DrawBezier(g, m_p_line, op.DotLeft + op.DotSize, op.DotTop + op.DotSize / 2, - v.DotLeft - 1, v.DotTop + v.DotSize / 2, this._Curvature); - if (m_is_buildpath) { //如果当前绘制需要重新建立已连接的路径缓存 - GraphicsPath gp = this.CreateBezierPath(op.DotLeft + op.DotSize, op.DotTop + op.DotSize / 2, - v.DotLeft - 1, v.DotTop + v.DotSize / 2, this._Curvature); - m_dic_gp_info.Add(gp, new ConnectionInfo() { Output = op, Input = v }); - } - } - } - } - m_p_line_hover.Color = this._HighLineColor; - if (m_gp_hover != null) { //如果当前有被悬停的连接路劲 则高亮绘制 - g.DrawPath(m_p_line_hover, m_gp_hover); - } - m_is_buildpath = false; //重置标志 下次绘制时候 不再重新建立路径缓存 - } - /// - /// 当绘制 Mark 详情信息时候发生 - /// - /// 绘制工具 - protected virtual void OnDrawMark(DrawingTools dt) { - Graphics g = dt.Graphics; - SizeF sz = g.MeasureString(m_find.Mark, this.Font); //确认文字需要的大小 - Rectangle rect = new Rectangle(m_pt_in_control.X + 15, - m_pt_in_control.Y + 10, - (int)sz.Width + 6, - 4 + (this.Font.Height + 4) * m_find.MarkLines.Length); //sz.Height并没有考虑文字的行距 所以这里高度自己计算 - - if (rect.Right > this.Width) rect.X = this.Width - rect.Width; - if (rect.Bottom > this.Height) rect.Y = this.Height - rect.Height; - if (rect.X < 0) rect.X = 0; - if (rect.Y < 0) rect.Y = 0; - - dt.SolidBrush.Color = this._MarkBackColor; - g.SmoothingMode = SmoothingMode.None; - g.FillRectangle(dt.SolidBrush, rect); //绘制背景区域 - rect.Width--; rect.Height--; - dt.Pen.Color = Color.FromArgb(255, this._MarkBackColor); - g.DrawRectangle(dt.Pen, rect); - dt.SolidBrush.Color = this._MarkForeColor; - - m_sf.LineAlignment = StringAlignment.Center; - //g.SmoothingMode = SmoothingMode.HighQuality; - rect.X += 2; rect.Width -= 3; - rect.Height = this.Font.Height + 4; - int nY = rect.Y + 2; - for (int i = 0; i < m_find.MarkLines.Length; i++) { //绘制文字 - rect.Y = nY + i * (this.Font.Height + 4); - g.DrawString(m_find.MarkLines[i], this.Font, dt.SolidBrush, rect, m_sf); - } - } - /// - /// 当移动 Node 时候 需要显示对齐参考线时候发生 - /// - /// 绘制工具 - /// 匹配的磁铁信息 - protected virtual void OnDrawMagnetLine(DrawingTools dt, MagnetInfo mi) { - Graphics g = dt.Graphics; - Pen pen = m_drawing_tools.Pen; - pen.Color = this._MagnetLineColor; - if (mi.XMatched) g.DrawLine(pen, this.CanvasToControl(mi.X, true), 0, this.CanvasToControl(mi.X, true), this.Height); - if (mi.YMatched) g.DrawLine(pen, 0, this.CanvasToControl(mi.Y, false), this.Width, this.CanvasToControl(mi.Y, false)); - } - /// - /// 绘制选择的矩形区域 - /// - /// 绘制工具 - /// 位于控件上的矩形区域 - protected virtual void OnDrawSelectedRectangle(DrawingTools dt, RectangleF rectf) { - Graphics g = dt.Graphics; - SolidBrush brush = dt.SolidBrush; - g.DrawRectangle(Pens.DodgerBlue, rectf.Left, rectf.Y, rectf.Width, rectf.Height); - brush.Color = Color.FromArgb(50, Color.DodgerBlue); - g.FillRectangle(brush, this.CanvasToControl(m_rect_select)); - } - /// - /// 绘制超出视觉区域的 Node 位置提示信息 - /// - /// 绘制工具 - /// 提示框边距 - /// 超出视觉区域的 Node 位置信息 - protected virtual void OnDrawNodeOutLocation(DrawingTools dt, Size sz, List lstPts) { - Graphics g = dt.Graphics; - SolidBrush brush = dt.SolidBrush; - brush.Color = this._LocationBackColor; - g.SmoothingMode = SmoothingMode.None; - if (lstPts.Count == this._Nodes.Count && this._Nodes.Count != 0) { //如果超出个数和集合个数一样多 则全部超出 绘制外切矩形 - g.FillRectangle(brush, this.CanvasToControl(this._CanvasValidBounds)); - } - g.FillRectangle(brush, 0, 0, 4, sz.Height); //绘制四边背景 - g.FillRectangle(brush, sz.Width - 4, 0, 4, sz.Height); - g.FillRectangle(brush, 4, 0, sz.Width - 8, 4); - g.FillRectangle(brush, 4, sz.Height - 4, sz.Width - 8, 4); - brush.Color = this._LocationForeColor; - foreach (var v in lstPts) { //绘制点 - var pt = this.CanvasToControl(v); - if (pt.X < 0) pt.X = 0; - if (pt.Y < 0) pt.Y = 0; - if (pt.X > sz.Width) pt.X = sz.Width - 4; - if (pt.Y > sz.Height) pt.Y = sz.Height - 4; - g.FillRectangle(brush, pt.X, pt.Y, 4, 4); - } - } - /// - /// 绘制提示信息 - /// - /// 绘制工具 - /// 需要绘制区域 - /// 需要绘制文本 - /// 信息前景色 - /// 信息背景色 - /// 信息位置 - protected virtual void OnDrawAlert(DrawingTools dt, Rectangle rect, string strText, Color foreColor, Color backColor, AlertLocation al) { - if (m_alpha_alert == 0) return; - Graphics g = dt.Graphics; - SolidBrush brush = dt.SolidBrush; - - g.SmoothingMode = SmoothingMode.None; - brush.Color = backColor; - dt.Pen.Color = brush.Color; - g.FillRectangle(brush, rect); - g.DrawRectangle(dt.Pen, rect.Left, rect.Top, rect.Width - 1, rect.Height - 1); - - brush.Color = foreColor; - m_sf.Alignment = StringAlignment.Center; - m_sf.LineAlignment = StringAlignment.Center; - g.SmoothingMode = SmoothingMode.HighQuality; - g.DrawString(strText, this.Font, brush, rect, m_sf); - } - /// - /// 获取提示信息需要绘制的矩形区域 - /// - /// 绘图表面 - /// 需要绘制文本 - /// 信息位置 - /// 矩形区域 - protected virtual Rectangle GetAlertRectangle(Graphics g, string strText, AlertLocation al) { - SizeF szf = g.MeasureString(m_str_alert, this.Font); - Size sz = new Size((int)Math.Round(szf.Width + 10), (int)Math.Round(szf.Height + 4)); - Rectangle rect = new Rectangle(4, this.Height - sz.Height - 4, sz.Width, sz.Height); - - switch (al) { - case AlertLocation.Left: - rect.Y = (this.Height - sz.Height) >> 1; - break; - case AlertLocation.Top: - rect.Y = 4; - rect.X = (this.Width - sz.Width) >> 1; - break; - case AlertLocation.Right: - rect.X = this.Width - sz.Width - 4; - rect.Y = (this.Height - sz.Height) >> 1; - break; - case AlertLocation.Bottom: - rect.X = (this.Width - sz.Width) >> 1; - break; - case AlertLocation.Center: - rect.X = (this.Width - sz.Width) >> 1; - rect.Y = (this.Height - sz.Height) >> 1; - break; - case AlertLocation.LeftTop: - rect.X = rect.Y = 4; - break; - case AlertLocation.RightTop: - rect.Y = 4; - rect.X = this.Width - sz.Width - 4; - break; - case AlertLocation.RightBottom: - rect.X = this.Width - sz.Width - 4; - break; - } - return rect; - } - - #endregion protected - - #region internal - - internal void BuildLinePath() { - foreach (var v in m_dic_gp_info) v.Key.Dispose(); - m_dic_gp_info.Clear(); - m_is_buildpath = true; - this.Invalidate(); - } - - internal void OnDrawAlert(Graphics g) { - m_rect_alert = this.GetAlertRectangle(g, m_str_alert, m_al); - Color clr_fore = Color.FromArgb((int)((float)m_alpha_alert / 255 * m_forecolor_alert.A), m_forecolor_alert); - Color clr_back = Color.FromArgb((int)((float)m_alpha_alert / 255 * m_backcolor_alert.A), m_backcolor_alert); - this.OnDrawAlert(m_drawing_tools, m_rect_alert, m_str_alert, clr_fore, clr_back, m_al); - } - - #endregion internal - - #region private ----------------------------------------------------------------------------------------------------- - - private void MoveCanvasThread() { - bool bRedraw; - while (true) { - bRedraw = false; - if (m_real_canvas_x != this._CanvasOffsetX) { - float nx = m_real_canvas_x - this._CanvasOffsetX; - float n = Math.Abs(nx) / 10; - float nTemp = Math.Abs(nx); - if (nTemp <= 4) n = 1; - else if (nTemp <= 12) n = 2; - else if (nTemp <= 30) n = 3; - if (nTemp < 1) this._CanvasOffsetX = m_real_canvas_x; - else - this._CanvasOffsetX += nx > 0 ? n : -n; - bRedraw = true; - } - if (m_real_canvas_y != this._CanvasOffsetY) { - float ny = m_real_canvas_y - this._CanvasOffsetY; - float n = Math.Abs(ny) / 10; - float nTemp = Math.Abs(ny); - if (nTemp <= 4) n = 1; - else if (nTemp <= 12) n = 2; - else if (nTemp <= 30) n = 3; - if (nTemp < 1) - this._CanvasOffsetY = m_real_canvas_y; - else - this._CanvasOffsetY += ny > 0 ? n : -n; - bRedraw = true; - } - if (bRedraw) { - m_pt_canvas_old.X = this._CanvasOffsetX; - m_pt_canvas_old.Y = this._CanvasOffsetY; - this.Invalidate(); - Thread.Sleep(30); - } else { - Thread.Sleep(100); - } - } - } - - private void ShowAlertThread() { - while (true) { - int nTime = m_time_alert - (int)DateTime.Now.Subtract(m_dt_alert).TotalMilliseconds; - if (nTime > 0) { - Thread.Sleep(nTime); - continue; - } - if (nTime < -1000) { - if (m_alpha_alert != 0) { - m_alpha_alert = 0; - this.Invalidate(); - } - Thread.Sleep(100); - } else { - m_alpha_alert = (int)(255 - (-nTime / 1000F) * 255); - this.Invalidate(m_rect_alert); - Thread.Sleep(50); - } - } - } - - private Image CreateBorderImage(Color clr) { - Image img = new Bitmap(12, 12); - using (Graphics g = Graphics.FromImage(img)) { - g.SmoothingMode = SmoothingMode.HighQuality; - using (GraphicsPath gp = new GraphicsPath()) { - gp.AddEllipse(new Rectangle(0, 0, 11, 11)); - using (PathGradientBrush b = new PathGradientBrush(gp)) { - b.CenterColor = Color.FromArgb(200, clr); - b.SurroundColors = new Color[] { Color.FromArgb(10, clr) }; - g.FillPath(b, gp); - } - } - } - return img; - } - - private ConnectionStatus DisConnectionHover() { - if (!m_dic_gp_info.ContainsKey(m_gp_hover)) return ConnectionStatus.DisConnected; - ConnectionInfo ci = m_dic_gp_info[m_gp_hover]; - var ret = ci.Output.DisConnectOption(ci.Input); - //this.OnOptionDisConnected(new STNodeOptionEventArgs(ci.Output, ci.Input, ret)); - if (ret == ConnectionStatus.DisConnected) { - m_dic_gp_info.Remove(m_gp_hover); - m_gp_hover.Dispose(); - m_gp_hover = null; - this.Invalidate(); - } - return ret; - } - - 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; - } - - private void MoveNode(Point pt) { - int nX = (int)((pt.X - m_pt_down_in_control.X) / this._CanvasScale); - int nY = (int)((pt.Y - m_pt_down_in_control.Y) / this._CanvasScale); - foreach (STNode v in m_hs_node_selected) { - v.Left = m_dic_pt_selected[v].X + nX; - v.Top = m_dic_pt_selected[v].Y + nY; - } - if (this._Magnet) { - MagnetInfo mi = this.CheckMagnet(this._ActiveNode); - if (mi.XMatched) { - foreach (STNode v in m_hs_node_selected) v.Left -= mi.OffsetX; - } - if (mi.YMatched) { - foreach (STNode v in m_hs_node_selected) v.Top -= mi.OffsetY; - } - } - this.Invalidate(); - } - - protected internal virtual void BuildBounds() { - if (this._Nodes.Count == 0) { - this._CanvasValidBounds = this.ControlToCanvas(this.DisplayRectangle); - return; - } - int x = int.MaxValue; - int y = int.MaxValue; - int r = int.MinValue; - int b = int.MinValue; - foreach (STNode n in this._Nodes) { - if (x > n.Left) x = n.Left; - if (y > n.Top) y = n.Top; - if (r < n.Right) r = n.Right; - if (b < n.Bottom) b = n.Bottom; - } - this._CanvasValidBounds.X = x - 60; - this._CanvasValidBounds.Y = y - 60; - this._CanvasValidBounds.Width = r - x + 120; - this._CanvasValidBounds.Height = b - y + 120; - } - - private bool PointInRectangle(Rectangle rect, float x, float y) { - if (x < rect.Left) return false; - if (x > rect.Right) return false; - if (y < rect.Top) return false; - if (y > rect.Bottom) return false; - return true; - } - - private void BuildMagnetLocation() { - m_lst_magnet_x.Clear(); - m_lst_magnet_y.Clear(); - foreach (STNode v in this._Nodes) { - if (v.IsSelected) continue; - m_lst_magnet_x.Add(v.Left); - m_lst_magnet_x.Add(v.Left + v.Width / 2); - m_lst_magnet_x.Add(v.Left + v.Width); - m_lst_magnet_y.Add(v.Top); - m_lst_magnet_y.Add(v.Top + v.Height / 2); - m_lst_magnet_y.Add(v.Top + v.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 bFlag = false; - foreach (var mx in m_lst_magnet_mx) { - foreach (var x in m_lst_magnet_x) { - if (Math.Abs(mx - x) <= 5) { - bFlag = true; - m_mi.X = x; - m_mi.OffsetX = mx - x; - m_mi.XMatched = true; - break; - } - } - if (bFlag) break; - } - bFlag = false; - foreach (var my in m_lst_magnet_my) { - foreach (var y in m_lst_magnet_y) { - if (Math.Abs(my - y) <= 5) { - bFlag = true; - m_mi.Y = y; - m_mi.OffsetY = my - y; - m_mi.YMatched = true; - break; - } - } - if (bFlag) break; - } - return m_mi; - } - - private void DrawBezier(Graphics g, Pen p, PointF ptStart, PointF ptEnd, float f) { - this.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) { - float n = (Math.Abs(x1 - x2) * f); - if (this._Curvature != 0 && n < 30) n = 30; - g.DrawBezier(p, - x1, y1, - x1 + n, y1, - x2 - n, y2, - x2, y2); - } - - private GraphicsPath CreateBezierPath(float x1, float y1, float x2, float y2, float f) { - GraphicsPath gp = new GraphicsPath(); - float n = (Math.Abs(x1 - x2) * f); - if (this._Curvature != 0 && n < 30) n = 30; - gp.AddBezier( - x1, y1, - x1 + n, y1, - x2 - n, y2, - x2, y2 - ); - return gp; - } - - 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); - } - - #endregion private - - #region public -------------------------------------------------------------------------------------------------------- - /// - /// 通过画布坐标进行寻找 - /// - /// 画布中的坐标 - /// 寻找到的数据 - public NodeFindInfo FindNodeFromPoint(PointF pt) { - m_find.Node = null; m_find.NodeOption = null; m_find.Mark = null; - for (int i = this._Nodes.Count - 1; i >= 0; i--) { - if (!string.IsNullOrEmpty(this._Nodes[i].Mark) && this.PointInRectangle(this._Nodes[i].MarkRectangle, pt.X, pt.Y)) { - m_find.Mark = this._Nodes[i].Mark; - m_find.MarkLines = this._Nodes[i].MarkLines; - return m_find; - } - foreach (STNodeOption v in this._Nodes[i].InputOptions) { - if (this.PointInRectangle(v.DotRectangle, pt.X, pt.Y)) m_find.NodeOption = v; - } - foreach (STNodeOption v in this._Nodes[i].OutputOptions) { - if (this.PointInRectangle(v.DotRectangle, pt.X, pt.Y)) m_find.NodeOption = v; - } - if (this.PointInRectangle(this._Nodes[i].Rectangle, pt.X, pt.Y)) { - m_find.Node = this._Nodes[i]; - } - if (m_find.NodeOption != null || m_find.Node != null) return m_find; - } - return m_find; - } - /// - /// 获取已经被选择的 Node 集合 - /// - /// Node 集合 - public STNode[] GetSelectedNode() { - return m_hs_node_selected.ToArray(); - } - /// - /// 将画布坐标转换为控件坐标 - /// - /// 参数 - /// 是否为 X 坐标 - /// 转换后的坐标 - public float CanvasToControl(float number, bool isX) { - return (number * this._CanvasScale) + (isX ? this._CanvasOffsetX : this._CanvasOffsetY); - } - /// - /// 将画布坐标转换为控件坐标 - /// - /// 坐标 - /// 转换后的坐标 - public PointF CanvasToControl(PointF pt) { - pt.X = (pt.X * this._CanvasScale) + this._CanvasOffsetX; - pt.Y = (pt.Y * this._CanvasScale) + this._CanvasOffsetY; - return pt; - } - /// - /// 将画布坐标转换为控件坐标 - /// - /// 坐标 - /// 转换后的坐标 - public Point CanvasToControl(Point pt) { - pt.X = (int)(pt.X * this._CanvasScale + this._CanvasOffsetX); - pt.Y = (int)(pt.Y * this._CanvasScale + this._CanvasOffsetY); - return pt; - } - /// - /// 将画布坐标转换为控件坐标 - /// - /// 矩形区域 - /// 转换后的矩形区域 - public Rectangle CanvasToControl(Rectangle rect) { - rect.X = (int)((rect.X * this._CanvasScale) + this._CanvasOffsetX); - rect.Y = (int)((rect.Y * this._CanvasScale) + this._CanvasOffsetY); - rect.Width = (int)(rect.Width * this._CanvasScale); - rect.Height = (int)(rect.Height * this._CanvasScale); - return rect; - } - /// - /// 将画布坐标转换为控件坐标 - /// - /// 矩形区域 - /// 转换后的矩形区域 - public RectangleF CanvasToControl(RectangleF rect) { - rect.X = (rect.X * this._CanvasScale) + this._CanvasOffsetX; - rect.Y = (rect.Y * this._CanvasScale) + this._CanvasOffsetY; - rect.Width = (rect.Width * this._CanvasScale); - rect.Height = (rect.Height * this._CanvasScale); - return rect; - } - /// - /// 将控件坐标转换为画布坐标 - /// - /// 参数 - /// 是否为 X 坐标 - /// 转换后的坐标 - public float ControlToCanvas(float number, bool isX) { - return (number - (isX ? this._CanvasOffsetX : this._CanvasOffsetY)) / this._CanvasScale; - } - /// - /// 将控件坐标转换为画布坐标 - /// - /// 坐标 - /// 转换后的坐标 - public Point ControlToCanvas(Point pt) { - pt.X = (int)((pt.X - this._CanvasOffsetX) / this._CanvasScale); - pt.Y = (int)((pt.Y - this._CanvasOffsetY) / this._CanvasScale); - return pt; - } - /// - /// 将控件坐标转换为画布坐标 - /// - /// 坐标 - /// 转换后的坐标 - public PointF ControlToCanvas(PointF pt) { - pt.X = ((pt.X - this._CanvasOffsetX) / this._CanvasScale); - pt.Y = ((pt.Y - this._CanvasOffsetY) / this._CanvasScale); - return pt; - } - /// - /// 将控件坐标转换为画布坐标 - /// - /// 矩形区域 - /// 转换后的区域 - public Rectangle ControlToCanvas(Rectangle rect) { - rect.X = (int)((rect.X - this._CanvasOffsetX) / this._CanvasScale); - rect.Y = (int)((rect.Y - this._CanvasOffsetY) / this._CanvasScale); - rect.Width = (int)(rect.Width / this._CanvasScale); - rect.Height = (int)(rect.Height / this._CanvasScale); - return rect; - } - /// - /// 将控件坐标转换为画布坐标 - /// - /// 矩形区域 - /// 转换后的区域 - public RectangleF ControlToCanvas(RectangleF rect) { - rect.X = ((rect.X - this._CanvasOffsetX) / this._CanvasScale); - rect.Y = ((rect.Y - this._CanvasOffsetY) / this._CanvasScale); - rect.Width = (rect.Width / this._CanvasScale); - rect.Height = (rect.Height / this._CanvasScale); - return rect; - } - /// - /// 移动画布原点坐标到指定的控件坐标位置 - /// 当不存在 Node 时候 无法移动 - /// - /// X 坐标 - /// Y 坐标 - /// 移动过程中是否启动动画效果 - /// 指定需要修改的坐标参数 - public void MoveCanvas(float x, float y, bool bAnimation, CanvasMoveArgs ma) { - if (this._Nodes.Count == 0) { - m_real_canvas_x = m_real_canvas_y = 10; - return; - } - int l = (int)((this._CanvasValidBounds.Left + 50) * this._CanvasScale); - int t = (int)((this._CanvasValidBounds.Top + 50) * this._CanvasScale); - int r = (int)((this._CanvasValidBounds.Right - 50) * this._CanvasScale); - int b = (int)((this._CanvasValidBounds.Bottom - 50) * this._CanvasScale); - if (r + x < 0) x = -r; - if (this.Width - l < x) x = this.Width - l; - if (b + y < 0) y = -b; - if (this.Height - t < y) y = this.Height - t; - if (bAnimation) { - if ((ma & CanvasMoveArgs.Left) == CanvasMoveArgs.Left) - m_real_canvas_x = x; - if ((ma & CanvasMoveArgs.Top) == CanvasMoveArgs.Top) - m_real_canvas_y = y; - } else { - m_real_canvas_x = this._CanvasOffsetX = x; - m_real_canvas_y = this._CanvasOffsetY = y; - } - this.OnCanvasMoved(new EventArgs()); - } - /// - /// 缩放画布 - /// 当不存在 Node 时候 无法缩放 - /// - /// 缩放比例 - /// 以指定控件坐标 X 为中心进行缩放 - /// 以指定控件坐标 Y 为中心进行缩放 - public void ScaleCanvas(float f, float x, float y) { - if (this._Nodes.Count == 0) { - this._CanvasScale = 1F; - return; - } - if (this._CanvasScale == f) return; - if (f < 0.5) f = 0.5f; else if (f > 3) f = 3; - float x_c = this.ControlToCanvas(x, true); - float y_c = this.ControlToCanvas(y, false); - this._CanvasScale = f; - this._CanvasOffsetX = m_real_canvas_x -= this.CanvasToControl(x_c, true) - x; - this._CanvasOffsetY = m_real_canvas_y -= this.CanvasToControl(y_c, false) - y; - this.OnCanvasScaled(new EventArgs()); - this.Invalidate(); - } - /// - /// 获取当前已连接的 Option 对应关系 - /// - /// 连接信息集合 - public ConnectionInfo[] GetConnectionInfo() { - return m_dic_gp_info.Values.ToArray(); - } - /// - /// 判断两个 Node 之间是否存在连接路径 - /// - /// 起始 Node - /// 目标 Node - /// 若存在路径返回true 否则false - public static bool CanFindNodePath(STNode nodeStart, STNode nodeFind) { - HashSet hs = new HashSet(); - return STNodeEditor.CanFindNodePath(nodeStart, nodeFind, hs); - } - private static bool CanFindNodePath(STNode nodeStart, STNode nodeFind, HashSet hs) { - foreach (STNodeOption op_1 in nodeStart.OutputOptions) { - foreach (STNodeOption op_2 in op_1.ConnectedOption) { - if (op_2.Owner == nodeFind) return true; - if (hs.Add(op_2.Owner)) { - if (STNodeEditor.CanFindNodePath(op_2.Owner, nodeFind)) return true; - } - } - } - return false; - } - /// - /// 获取画布中指定矩形区域图像 - /// - /// 画布中指定的矩形区域 - /// 图像 - public Image GetCanvasImage(Rectangle rect) { return this.GetCanvasImage(rect, 1f); } - /// - /// 获取画布中指定矩形区域图像 - /// - /// 画布中指定的矩形区域 - /// 缩放比例 - /// 图像 - public Image GetCanvasImage(Rectangle rect, float fScale) { - if (fScale < 0.5) fScale = 0.5f; else if (fScale > 3) fScale = 3; - Image img = new Bitmap((int)(rect.Width * fScale), (int)(rect.Height * fScale)); - using (Graphics g = Graphics.FromImage(img)) { - g.Clear(this.BackColor); - g.ScaleTransform(fScale, fScale); - m_drawing_tools.Graphics = g; - - if (this._ShowGrid) this.OnDrawGrid(m_drawing_tools, rect.Width, rect.Height); - g.TranslateTransform(-rect.X, -rect.Y); //移动坐标系 - this.OnDrawNode(m_drawing_tools, rect); - this.OnDrawConnectedLine(m_drawing_tools); - - g.ResetTransform(); - - if (this._ShowLocation) this.OnDrawNodeOutLocation(m_drawing_tools, img.Size, m_lst_node_out); - } - return img; - } - /// - /// 保存画布中的类容到文件中 - /// - /// 文件路径 - public void SaveCanvas(string strFileName) { - using (FileStream fs = new FileStream(strFileName, FileMode.Create, FileAccess.Write)) { - this.SaveCanvas(fs); - } - } - /// - /// 保存画布中的类容到数据流 - /// - /// 数据流对象 - public void SaveCanvas(Stream s) { - Dictionary dic = new Dictionary(); - s.Write(new byte[] { (byte)'S', (byte)'T', (byte)'N', (byte)'D' }, 0, 4); //file head - s.WriteByte(1); //ver - using (GZipStream gs = new GZipStream(s, CompressionMode.Compress)) { - gs.Write(BitConverter.GetBytes(this._CanvasOffsetX), 0, 4); - gs.Write(BitConverter.GetBytes(this._CanvasOffsetY), 0, 4); - gs.Write(BitConverter.GetBytes(this._CanvasScale), 0, 4); - gs.Write(BitConverter.GetBytes(this._Nodes.Count), 0, 4); - foreach (STNode node in this._Nodes) { - try { - byte[] byNode = node.GetSaveData(); - gs.Write(BitConverter.GetBytes(byNode.Length), 0, 4); - gs.Write(byNode, 0, byNode.Length); - foreach (STNodeOption op in node.InputOptions) dic.Add(op, dic.Count); - foreach (STNodeOption op in node.OutputOptions) dic.Add(op, dic.Count); - } catch (Exception ex) { - throw new Exception("获取节点数据出错-" + node.Title, ex); - } - } - gs.Write(BitConverter.GetBytes(m_dic_gp_info.Count), 0, 4); - foreach (var v in m_dic_gp_info.Values) - gs.Write(BitConverter.GetBytes(((dic[v.Output] << 32) | dic[v.Input])), 0, 8); - } - } - /// - /// 获取画布中内容二进制数据 - /// - /// 二进制数据 - public byte[] GetCanvasData() { - using (MemoryStream ms = new MemoryStream()) { - this.SaveCanvas(ms); - return ms.ToArray(); - } - } - /// - /// 加载程序集 - /// - /// 程序集集合 - /// 存在STNode类型的文件的个数 - public int LoadAssembly(string[] strFiles) { - int nCount = 0; - foreach (var v in strFiles) { - try { - if (this.LoadAssembly(v)) nCount++; - } catch { } - } - return nCount; - } - /// - /// 加载程序集 - /// - /// 指定需要加载的文件 - /// 是否加载成功 - public bool LoadAssembly(string strFile) { - bool bFound = false; - Assembly asm = Assembly.LoadFrom(strFile); - if (asm == null) return false; - foreach (var t in asm.GetTypes()) { - if (t.IsAbstract) continue; - if (t == m_type_node || t.IsSubclassOf(m_type_node)) { - if (m_dic_type.ContainsKey(t.GUID.ToString())) continue; - m_dic_type.Add(t.GUID.ToString(), t); - bFound = true; - } - } - return bFound; - } - /// - /// 获取当前编辑器中已加载的Node类型 - /// - /// 类型集合 - public Type[] GetTypes() { - return m_dic_type.Values.ToArray(); - } - /// - /// 从文件中加载数据 - /// 注意: 此方法并不会清空画布中数据 而是数据叠加 - /// - /// 文件路径 - public void LoadCanvas(string strFileName) { - using (MemoryStream ms = new MemoryStream(File.ReadAllBytes(strFileName))) - this.LoadCanvas(ms); - } - /// - /// 从二进制加载数据 - /// 注意: 此方法并不会清空画布中数据 而是数据叠加 - /// - /// 二进制数据 - public void LoadCanvas(byte[] byData) { - using (MemoryStream ms = new MemoryStream(byData)) - this.LoadCanvas(ms); - } - /// - /// 从数据流中加载数据 - /// 注意: 此方法并不会清空画布中数据 而是数据叠加 - /// - /// 数据流对象 - public void LoadCanvas(Stream s) { - int nLen = 0; - byte[] byLen = new byte[4]; - s.Read(byLen, 0, 4); - if (BitConverter.ToInt32(byLen, 0) != BitConverter.ToInt32(new byte[] { (byte)'S', (byte)'T', (byte)'N', (byte)'D' }, 0)) - throw new InvalidDataException("无法识别的文件类型"); - if (s.ReadByte() != 1) throw new InvalidDataException("无法识别的文件版本号"); - using (GZipStream gs = new GZipStream(s, CompressionMode.Decompress)) { - gs.Read(byLen, 0, 4); - float x = BitConverter.ToSingle(byLen, 0); - gs.Read(byLen, 0, 4); - float y = BitConverter.ToSingle(byLen, 0); - gs.Read(byLen, 0, 4); - float scale = BitConverter.ToSingle(byLen, 0); - gs.Read(byLen, 0, 4); - int nCount = BitConverter.ToInt32(byLen, 0); - Dictionary dic = new Dictionary(); - byte[] byData = null; - for (int i = 0; i < nCount; i++) { - gs.Read(byLen, 0, byLen.Length); - nLen = BitConverter.ToInt32(byLen, 0); - byData = new byte[nLen]; - gs.Read(byData, 0, byData.Length); - STNode node = null; - try { node = this.GetNodeFromData(byData); } catch (Exception ex) { - throw new Exception("加载节点时发生错误可能数据已损坏\r\n" + ex.Message, ex); - } - try { this._Nodes.Add(node); } catch (Exception ex) { - throw new Exception("加载节点出错-" + node.Title, ex); - } - foreach (STNodeOption op in node.InputOptions) dic.Add(dic.Count, op); - foreach (STNodeOption op in node.OutputOptions) dic.Add(dic.Count, op); - } - gs.Read(byLen, 0, 4); - nCount = BitConverter.ToInt32(byLen, 0); - byData = new byte[8]; - for (int i = 0; i < nCount; i++) { - gs.Read(byData, 0, byData.Length); - long id = BitConverter.ToInt64(byData, 0); - long op_out = id >> 32; - long op_in = (int)id; - dic[op_out].ConnectOption(dic[op_in]); - } - this.ScaleCanvas(scale, 0, 0); - this.MoveCanvas(x, y, false, CanvasMoveArgs.All); - } - this.BuildBounds(); - foreach (STNode node in this._Nodes) node.OnEditorLoadCompleted(); - } - - private STNode GetNodeFromData(byte[] byData) { - int nIndex = 0; - string strModel = Encoding.UTF8.GetString(byData, nIndex + 1, byData[nIndex]); - nIndex += byData[nIndex] + 1; - string strGUID = Encoding.UTF8.GetString(byData, nIndex + 1, byData[nIndex]); - nIndex += byData[nIndex] + 1; - - int nLen = 0; - - Dictionary dic = new Dictionary(); - while (nIndex < byData.Length) { - nLen = BitConverter.ToInt32(byData, nIndex); - nIndex += 4; - string strKey = Encoding.UTF8.GetString(byData, nIndex, nLen); - nIndex += nLen; - nLen = BitConverter.ToInt32(byData, nIndex); - nIndex += 4; - byte[] byValue = new byte[nLen]; - Array.Copy(byData, nIndex, byValue, 0, nLen); - nIndex += nLen; - dic.Add(strKey, byValue); - } - if (!m_dic_type.ContainsKey(strGUID)) throw new TypeLoadException("无法找到类型 {" + strGUID + "} 所在程序集 确保程序集{" + strModel + "}已被编辑器正确加载 可通过调用LoadAssembly()加载程序集"); - Type t = m_dic_type[strGUID]; ; - STNode node = (STNode)Activator.CreateInstance(t); - node.OnLoadNode(dic); - return node; - } - /// - /// 在画布中显示提示信息 - /// - /// 要显示的信息 - /// 信息前景色 - /// 信息背景色 - public void ShowAlert(string strText, Color foreColor, Color backColor) { - this.ShowAlert(strText, foreColor, backColor, 1000, AlertLocation.LeftBottom, true); - } - /// - /// 在画布中显示提示信息 - /// - /// 要显示的信息 - /// 信息前景色 - /// 信息背景色 - /// 信息要显示的位置 - public void ShowAlert(string strText, Color foreColor, Color backColor, AlertLocation al) { - this.ShowAlert(strText, foreColor, backColor, 1000, al, 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.Now; - m_alpha_alert = 255; - m_al = al; - if (bRedraw) this.Invalidate(); - } - /// - /// 设置画布中活动的节点 - /// - /// 需要被设置为活动的节点 - /// 设置前的活动节点 - public STNode SetActiveNode(STNode node) { - STNode ret = this._ActiveNode; - if (this._ActiveNode != node) { //重置活动选择节点 - if (node != null) { - node.IsSelected = node.IsActive = true; - node.OnGotFocus(new EventArgs()); - } - if (this._ActiveNode != null) { - this._ActiveNode.IsActive = this._ActiveNode.IsSelected = false; - this._ActiveNode.OnLostFocus(new EventArgs()); - } - this._ActiveNode = node; - this.Invalidate(); - this.OnSelectedChanged(new EventArgs()); - } - return ret; - } - /// - /// 向编辑器中添加默认数据类型颜色 - /// - /// 数据类型 - /// 对应颜色 - public void SetTypeColor(Type t, Color clr) { - if (this._TypeColor.ContainsKey(t)) - this._TypeColor[t] = clr; - else - this._TypeColor.Add(t, clr); - } - - #endregion public - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.IO; +using System.Windows.Forms; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Threading; +using System.ComponentModel; +using System.Reflection; +using System.IO.Compression; +/* +MIT License + +Copyright (c) 2021 DebugST@crystal_lz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +/* + * create: 2020-12-08 + * modify: 2021-04-12 + * Author: Crystal_lz + * blog: http://st233.com + * Gitee: https://gitee.com/DebugST + * Github: https://github.com/DebugST + */ +namespace ST.Library.UI.NodeEditor +{ + public class STNodeEditor : Control + { + private const UInt32 WM_MOUSEHWHEEL = 0x020E; + protected static readonly Type m_type_node = typeof(STNode); + + #region protected enum,struct -------------------------------------------------------------------------------------- + + protected enum CanvasAction //当前鼠标移动操作表示进行下列哪一个行为 + { + None, //无 + MoveNode, //正在移动 Node + ConnectOption, //正在连接 Option + SelectRectangle, //正在选择矩形区域 + DrawMarkDetails //正在绘制标记信息详情 + } + + protected struct MagnetInfo + { + public bool XMatched; //X轴是否有磁铁匹配上 + public bool YMatched; + public int X; //与X轴那个数字匹配上 + public int Y; + public int OffsetX; //当前节点X位置与匹配上的X的相对偏移 + public int OffsetY; + } + + #endregion + + #region Properties ------------------------------------------------------------------------------------------------------ + + private float _CanvasOffsetX; + /// + /// 获取画布原点相对于控件 X 方向上的偏移位置 + /// + [Browsable(false)] + public float CanvasOffsetX { + get { return _CanvasOffsetX; } + } + + private float _CanvasOffsetY; + /// + /// 获取画布原点相对于控件 Y 方向上的偏移位置 + /// + [Browsable(false)] + public float CanvasOffsetY { + get { return _CanvasOffsetY; } + } + + private PointF _CanvasOffset; + /// + /// 获取画布原点相对于控件偏移位置 + /// + [Browsable(false)] + public PointF CanvasOffset { + get { + _CanvasOffset.X = _CanvasOffsetX; + _CanvasOffset.Y = _CanvasOffsetY; + return _CanvasOffset; + } + } + + private Rectangle _CanvasValidBounds; + /// + /// 获取画布中的有被用到的有效区域 + /// + [Browsable(false)] + public Rectangle CanvasValidBounds { + get { return _CanvasValidBounds; } + } + + private float _CanvasScale = 1; + /// + /// 获取画布的缩放比例 + /// + [Browsable(false)] + public float CanvasScale { + get { return _CanvasScale; } + } + + private float _Curvature = 0.3F; + /// + /// 获取或设置 Option 之间连线的曲度 + /// + [Browsable(false)] + public float Curvature { + get { return _Curvature; } + set { + if (value < 0) value = 0; + if (value > 1) value = 1; + _Curvature = value; + if (m_dic_gp_info.Count != 0) this.BuildLinePath(); + } + } + + private bool _ShowMagnet = true; + /// + /// 获取或设置移动画布中 Node 时候 是否启用磁铁效果 + /// + [Description("获取或设置移动画布中 Node 时候 是否启用磁铁效果"), DefaultValue(true)] + public bool ShowMagnet { + get { return _ShowMagnet; } + set { _ShowMagnet = value; } + } + + private bool _ShowBorder = true; + /// + /// 获取或设置 移动画布中是否显示 Node 边框 + /// + [Description("获取或设置 移动画布中是否显示 Node 边框"), DefaultValue(true)] + public bool ShowBorder { + get { return _ShowBorder; } + set { + _ShowBorder = value; + this.Invalidate(); + } + } + + private bool _ShowGrid = true; + /// + /// 获取或设置画布中是否绘制背景网格线条 + /// + [Description("获取或设置画布中是否绘制背景网格线条"), DefaultValue(true)] + public bool ShowGrid { + get { return _ShowGrid; } + set { + _ShowGrid = value; + this.Invalidate(); + } + } + + private bool _ShowLocation = true; + /// + /// 获取或设置是否在画布边缘显示超出视角的 Node 位置信息 + /// + [Description("获取或设置是否在画布边缘显示超出视角的 Node 位置信息"), DefaultValue(true)] + public bool ShowLocation { + get { return _ShowLocation; } + set { + _ShowLocation = value; + this.Invalidate(); + } + } + + private STNodeCollection _Nodes; + /// + /// 获取画布中 Node 集合 + /// + [Browsable(false)] + public STNodeCollection Nodes { + get { + return _Nodes; + } + } + + private STNode _ActiveNode; + /// + /// 获取当前画布中被选中的活动 Node + /// + [Browsable(false)] + public STNode ActiveNode { + get { return _ActiveNode; } + //set { + // if (value == _ActiveSelectedNode) return; + // if (_ActiveSelectedNode != null) _ActiveSelectedNode.OnLostFocus(EventArgs.Empty); + // _ActiveSelectedNode = value; + // _ActiveSelectedNode.IsActive = true; + // this.Invalidate(); + // this.OnSelectedChanged(EventArgs.Empty); + //} + } + + private STNode _HoverNode; + /// + /// 获取当前画布中鼠标悬停的 Node + /// + [Browsable(false)] + public STNode HoverNode { + get { return _HoverNode; } + } + //========================================color================================ + private Color _GridColor = Color.Black; + /// + /// 获取或设置绘制画布背景时 网格线条颜色 + /// + [Description("获取或设置绘制画布背景时 网格线条颜色"), DefaultValue(typeof(Color), "Black")] + public Color GridColor { + get { return _GridColor; } + set { + _GridColor = value; + this.Invalidate(); + } + } + + private Color _BorderColor = Color.Black; + /// + /// 获取或设置画布中 Node 边框颜色 + /// + [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 = this.CreateBorderImage(value); + this.Invalidate(); + } + } + + private Color _BorderHoverColor = Color.Gray; + /// + /// 获取或设置画布中悬停 Node 边框颜色 + /// + [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 = this.CreateBorderImage(value); + this.Invalidate(); + } + } + + private Color _BorderSelectedColor = Color.Orange; + /// + /// 获取或设置画布中选中 Node 边框颜色 + /// + [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 = this.CreateBorderImage(value); + this.Invalidate(); + } + } + + private Color _BorderActiveColor = Color.OrangeRed; + /// + /// 获取或设置画布中活动 Node 边框颜色 + /// + [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 = this.CreateBorderImage(value); + this.Invalidate(); + } + } + + private Color _MarkForeColor = Color.White; + /// + /// 获取或设置画布绘制 Node 标记详情采用的前景色 + /// + [Description("获取或设置画布绘制 Node 标记详情采用的前景色"), DefaultValue(typeof(Color), "White")] + public Color MarkForeColor { + get { return _MarkBackColor; } + set { + _MarkBackColor = value; + this.Invalidate(); + } + } + + private Color _MarkBackColor = Color.FromArgb(180, Color.Black); + /// + /// 获取或设置画布绘制 Node 标记详情采用的背景色 + /// + [Description("获取或设置画布绘制 Node 标记详情采用的背景色")] + public Color MarkBackColor { + get { return _MarkBackColor; } + set { + _MarkBackColor = value; + this.Invalidate(); + } + } + + private Color _MagnetColor = Color.Lime; + /// + /// 获取或设置画布中移动 Node 时候 磁铁标记颜色 + /// + [Description("获取或设置画布中移动 Node 时候 磁铁标记颜色"), DefaultValue(typeof(Color), "Lime")] + public Color MagnetColor { + get { return _MagnetColor; } + set { _MagnetColor = value; } + } + + private Color _SelectedRectangleColor = Color.DodgerBlue; + /// + /// 获取或设置画布中选择矩形区域的颜色 + /// + [Description("获取或设置画布中选择矩形区域的颜色"), DefaultValue(typeof(Color), "DodgerBlue")] + public Color SelectedRectangleColor { + get { return _SelectedRectangleColor; } + set { _SelectedRectangleColor = value; } + } + + private Color _HighLineColor = Color.Cyan; + /// + /// 获取或设置画布中高亮连线的颜色 + /// + [Description("获取或设置画布中高亮连线的颜色"), DefaultValue(typeof(Color), "Cyan")] + public Color HighLineColor { + get { return _HighLineColor; } + set { _HighLineColor = value; } + } + + private Color _LocationForeColor = Color.Red; + /// + /// 获取或设置画布中边缘位置提示区域前景色 + /// + [Description("获取或设置画布中边缘位置提示区域前景色"), DefaultValue(typeof(Color), "Red")] + public Color LocationForeColor { + get { return _LocationForeColor; } + set { + _LocationForeColor = value; + this.Invalidate(); + } + } + + private Color _LocationBackColor = Color.FromArgb(120, Color.Black); + /// + /// 获取或设置画布中边缘位置提示区域背景色 + /// + [Description("获取或设置画布中边缘位置提示区域背景色")] + public Color LocationBackColor { + get { return _LocationBackColor; } + set { + _LocationBackColor = value; + this.Invalidate(); + } + } + + private Color _UnknownTypeColor = Color.Gray; + /// + /// 获取或设置画布中当 Node 中 Option 数据类型无法确定时应当使用的颜色 + /// + [Description("获取或设置画布中当 Node 中 Option 数据类型无法确定时应当使用的颜色"), DefaultValue(typeof(Color), "Gray")] + public Color UnknownTypeColor { + get { return _UnknownTypeColor; } + set { + _UnknownTypeColor = value; + this.Invalidate(); + } + } + + private Dictionary _TypeColor = new Dictionary(); + /// + /// 获取或设置画布中 Node 中 Option 数据类型预设颜色 + /// + [Browsable(false)] + public Dictionary TypeColor { + get { return _TypeColor; } + } + + #endregion + + #region protected properties ---------------------------------------------------------------------------------------- + /// + /// 当前鼠标在控件中的实时位置 + /// + 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; + /// + /// 用于保存连线过程中保存点下 Option 的起点坐标 + /// + protected Point m_pt_dot_down; + /// + /// 用于保存连线过程中鼠标点下的起点Option 当MouseUP时候 确定是否连接此节点 + /// + protected STNodeOption m_option_down; + /// + /// 当前鼠标点下的 STNode + /// + protected STNode m_node_down; + /// + /// 当前鼠标是否位于控件中 + /// + protected bool m_mouse_in_control; + + #endregion + + public STNodeEditor() { + this.SetStyle(ControlStyles.UserPaint, true); + this.SetStyle(ControlStyles.ResizeRedraw, true); + this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); + this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + this.SetStyle(ControlStyles.SupportsTransparentBackColor, true); + this._Nodes = new STNodeCollection(this); + this.BackColor = Color.FromArgb(255, 34, 34, 34); + this.MinimumSize = new Size(100, 100); + this.Size = new Size(200, 200); + this.AllowDrop = true; + + m_real_canvas_x = this._CanvasOffsetX = 10; + m_real_canvas_y = this._CanvasOffsetY = 10; + } + + #region private fields -------------------------------------------------------------------------------------- + + private DrawingTools m_drawing_tools; + private NodeFindInfo m_find = new NodeFindInfo(); + private MagnetInfo m_mi = new MagnetInfo(); + + private RectangleF m_rect_select = new RectangleF(); + //节点边框预设图案 + private Image m_img_border; + private Image m_img_border_hover; + private Image m_img_border_selected; + private Image m_img_border_active; + //用于鼠标滚动或者触摸板移动画布时候的动画效果 该值为需要移动到的真实坐标地址 查看->MoveCanvasThread() + private float m_real_canvas_x; + private float m_real_canvas_y; + //用于移动节点时候 保存鼠标点下时候选中的节点初始坐标 + private Dictionary m_dic_pt_selected = new Dictionary(); + //用于磁铁效果 移动节点时候 非选择节点的统计出来的需要参与磁铁效果的坐标 查看->BuildMagnetLocation() + private List m_lst_magnet_x = new List(); + private List m_lst_magnet_y = new List(); + //用于磁铁效果 移动节点时候 活动选择节点统计出来需要参与磁铁效果的坐标 查看->CheckMagnet() + private List m_lst_magnet_mx = new List(); + private List m_lst_magnet_my = new List(); + //用于鼠标滚动中计算时间触发间隔 根据间隔不同 画布产生的位移不同 查看->OnMouseWheel(),OnMouseHWheel() + 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; //是否向下(Node or NodeControls)传递鼠标相关事件 如断开连接相关操作不应向下传递 + 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, 4f); //用于绘制鼠标悬停时候的线条 + private GraphicsPath m_gp_hover; //当前鼠标悬停的连线路径 + private StringFormat m_sf = new StringFormat(); //文本格式 用于Mark绘制时候 设置文本格式 + //保存每个连接线条与之对应的节点关系 + private Dictionary m_dic_gp_info = new Dictionary(); + //保存超出视觉区域的 Node 的位置 + private List m_lst_node_out = new List(); + //当前编辑器已加载的 Node 类型 用于从文件或者数据中加载节点使用 + private Dictionary m_dic_type = new Dictionary(); + + 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; + + #endregion + + #region event ---------------------------------------------------------------------------------------------------- + /// + /// 活动的节点发生变化时候发生 + /// + [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; + + protected virtual internal void OnSelectedChanged(EventArgs e) { + if (this.SelectedChanged != null) this.SelectedChanged(this, e); + } + 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) { + if (this.NodeAdded != null) this.NodeAdded(this, e); + } + protected internal virtual void OnNodeRemoved(STNodeEditorEventArgs e) { + 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) { + if (this.OptionConnected != null) this.OptionConnected(this, e); + } + protected internal virtual void OnOptionDisConnected(STNodeEditorOptionEventArgs e) { + if (this.OptionDisConnected != null) this.OptionDisConnected(this, e); + } + protected internal virtual void OnOptionConnecting(STNodeEditorOptionEventArgs e) { + if (this.OptionConnecting != null) this.OptionConnecting(this, e); + } + protected internal virtual void OnOptionDisConnecting(STNodeEditorOptionEventArgs e) { + if (this.OptionDisConnecting != null) this.OptionDisConnecting(this, e); + } + + #endregion event + + #region override ----------------------------------------------------------------------------------------------------- + + protected override void OnCreateControl() { + m_drawing_tools = new DrawingTools() { + Pen = new Pen(Color.Black, 1), + SolidBrush = new SolidBrush(Color.Black) + }; + m_img_border = this.CreateBorderImage(this._BorderColor); + m_img_border_active = this.CreateBorderImage(this._BorderActiveColor); + m_img_border_hover = this.CreateBorderImage(this._BorderHoverColor); + m_img_border_selected = this.CreateBorderImage(this._BorderSelectedColor); + base.OnCreateControl(); + new Thread(this.MoveCanvasThread) { IsBackground = true }.Start(); + new Thread(this.ShowAlertThread) { IsBackground = true }.Start(); + m_sf = new StringFormat(); + m_sf.Alignment = StringAlignment.Near; + m_sf.FormatFlags = StringFormatFlags.NoWrap; + m_sf.SetTabStops(0, new float[] { 40 }); + } + + protected override void WndProc(ref Message m) { + base.WndProc(ref m); + try { + Point pt = new Point(((int)m.LParam) >> 16, (ushort)m.LParam); + pt = this.PointToClient(pt); + if (m.Msg == WM_MOUSEHWHEEL) { //获取水平滚动消息 + MouseButtons mb = MouseButtons.None; + int n = (ushort)m.WParam; + if ((n & 0x0001) == 0x0001) mb |= MouseButtons.Left; + if ((n & 0x0010) == 0x0010) mb |= MouseButtons.Middle; + if ((n & 0x0002) == 0x0002) mb |= MouseButtons.Right; + if ((n & 0x0020) == 0x0020) mb |= MouseButtons.XButton1; + if ((n & 0x0040) == 0x0040) mb |= MouseButtons.XButton2; + this.OnMouseHWheel(new MouseEventArgs(mb, 0, pt.X, pt.Y, ((int)m.WParam) >> 16)); + } + } catch { /*add code*/ } + } + + protected override void OnPaint(PaintEventArgs e) { + base.OnPaint(e); + Graphics g = e.Graphics; + g.Clear(this.BackColor); + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + m_drawing_tools.Graphics = g; + SolidBrush brush = m_drawing_tools.SolidBrush; + + if (this._ShowGrid) this.OnDrawGrid(m_drawing_tools, this.Width, this.Height); + + g.TranslateTransform(this._CanvasOffsetX, this._CanvasOffsetY); //移动坐标系 + g.ScaleTransform(this._CanvasScale, this._CanvasScale); //缩放绘图表面 + + this.OnDrawConnectedLine(m_drawing_tools); + this.OnDrawNode(m_drawing_tools, this.ControlToCanvas(this.ClientRectangle)); + + if (m_ca == CanvasAction.ConnectOption) { //如果正在连线 + m_drawing_tools.Pen.Color = this._HighLineColor; + g.SmoothingMode = SmoothingMode.HighQuality; + if (m_option_down.IsInput) + this.DrawBezier(g, m_drawing_tools.Pen, m_pt_in_canvas, m_pt_dot_down, this._Curvature); + else + this.DrawBezier(g, m_drawing_tools.Pen, m_pt_dot_down, m_pt_in_canvas, this._Curvature); + } + //重置绘图坐标 我认为除了节点以外的其它 修饰相关的绘制不应该在Canvas坐标系中绘制 而应该使用控件的坐标进行绘制 不然会受到缩放比影响 + g.ResetTransform(); + + switch (m_ca) { + case CanvasAction.MoveNode: //移动过程中 绘制对齐参考线 + if (this._ShowMagnet && this._ActiveNode != null) this.OnDrawMagnet(m_drawing_tools, m_mi); + break; + case CanvasAction.SelectRectangle: //绘制矩形选取 + this.OnDrawSelectedRectangle(m_drawing_tools, this.CanvasToControl(m_rect_select)); + break; + case CanvasAction.DrawMarkDetails: //绘制标记信息详情 + if (!string.IsNullOrEmpty(m_find.Mark)) this.OnDrawMark(m_drawing_tools); + break; + } + + if (this._ShowLocation) this.OnDrawNodeOutLocation(m_drawing_tools, this.Size, m_lst_node_out); + this.OnDrawAlert(g); + } + + protected override void OnMouseDown(MouseEventArgs e) { + base.OnMouseDown(e); + this.Focus(); + m_ca = CanvasAction.None; + m_mi.XMatched = m_mi.YMatched = false; + m_pt_down_in_control = e.Location; + m_pt_down_in_canvas.X = ((e.X - this._CanvasOffsetX) / this._CanvasScale); + m_pt_down_in_canvas.Y = ((e.Y - this._CanvasOffsetY) / this._CanvasScale); + m_pt_canvas_old.X = this._CanvasOffsetX; + m_pt_canvas_old.Y = this._CanvasOffsetY; + + if (m_gp_hover != null && e.Button == MouseButtons.Right) { //断开连接 + this.DisConnectionHover(); + m_is_process_mouse_event = false; //终止MouseClick与MouseUp向下传递 + return; + } + + NodeFindInfo nfi = this.FindNodeFromPoint(m_pt_down_in_canvas); + if (!string.IsNullOrEmpty(nfi.Mark)) { //如果点下的是标记信息 + m_ca = CanvasAction.DrawMarkDetails; + this.Invalidate(); + return; + } + + if (nfi.NodeOption != null) { //如果点下的Option的连接点 + this.StartConnect(nfi.NodeOption); + return; + } + + if (nfi.Node != null) { + nfi.Node.OnMouseDown(new MouseEventArgs(e.Button, e.Clicks, (int)m_pt_down_in_canvas.X - nfi.Node.Left, (int)m_pt_down_in_canvas.Y - nfi.Node.Top, e.Delta)); + bool bCtrlDown = (Control.ModifierKeys & Keys.Control) == Keys.Control; + if (bCtrlDown) { + if (nfi.Node.IsSelected) { + if (nfi.Node == this._ActiveNode) { + this.SetActiveNode(null); + } + } else { + nfi.Node.SetSelected(true, true); + } + return; + } else if (!nfi.Node.IsSelected) { + foreach (var n in m_hs_node_selected.ToArray()) n.SetSelected(false, false); + } + nfi.Node.SetSelected(true, false); //添加到已选择节点 + this.SetActiveNode(nfi.Node); + if (this.PointInRectangle(nfi.Node.TitleRectangle, m_pt_down_in_canvas.X, m_pt_down_in_canvas.Y)) { + if (e.Button == MouseButtons.Right) { + if (nfi.Node.ContextMenuStrip != null) { + nfi.Node.ContextMenuStrip.Show(this.PointToScreen(e.Location)); + } + } else { + m_dic_pt_selected.Clear(); + lock (m_hs_node_selected) { + foreach (STNode n in m_hs_node_selected) //记录已选择节点位置 如果需要移动已选中节点时候 将会有用 + m_dic_pt_selected.Add(n, n.Location); + } + m_ca = CanvasAction.MoveNode; //如果点下的是节点的标题 则可以移动该节点 + if (this._ShowMagnet && this._ActiveNode != null) this.BuildMagnetLocation(); //建立磁铁需要的坐标 如果需要移动已选中节点时候 将会有用 + } + } else + m_node_down = nfi.Node; + } else { + this.SetActiveNode(null); + foreach (var n in m_hs_node_selected.ToArray()) n.SetSelected(false, false);//没有点下任何东西 清空已经选择节点 + m_ca = CanvasAction.SelectRectangle; //进入矩形区域选择模式 + m_rect_select.Width = m_rect_select.Height = 0; + m_node_down = null; + } + //this.SetActiveNode(nfi.Node); + } + + protected override void OnMouseMove(MouseEventArgs e) { + base.OnMouseMove(e); + m_pt_in_control = e.Location; + m_pt_in_canvas.X = ((e.X - this._CanvasOffsetX) / this._CanvasScale); + m_pt_in_canvas.Y = ((e.Y - this._CanvasOffsetY) / this._CanvasScale); + + if (m_node_down != null) { + m_node_down.OnMouseMove(new MouseEventArgs(e.Button, e.Clicks, + (int)m_pt_in_canvas.X - m_node_down.Left, + (int)m_pt_in_canvas.Y - m_node_down.Top, e.Delta)); + return; + } + + if (e.Button == MouseButtons.Middle) { //鼠标中键移动画布 + this._CanvasOffsetX = m_real_canvas_x = m_pt_canvas_old.X + (e.X - m_pt_down_in_control.X); + this._CanvasOffsetY = m_real_canvas_y = m_pt_canvas_old.Y + (e.Y - m_pt_down_in_control.Y); + this.Invalidate(); + return; + } + if (e.Button == MouseButtons.Left) { //如果鼠标左键点下 判断行为 + m_gp_hover = null; + switch (m_ca) { + case CanvasAction.MoveNode: this.MoveNode(e.Location); return; //当前移动节点 + case CanvasAction.ConnectOption: this.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 n in this._Nodes) { + n.SetSelected(m_rect_select.IntersectsWith(n.Rectangle), false); + } + this.Invalidate(); + return; + } + } + //若不存在行为 则判断鼠标下方是否存在其他对象 + NodeFindInfo nfi = this.FindNodeFromPoint(m_pt_in_canvas); + bool bRedraw = false; + if (this._HoverNode != nfi.Node) { //鼠标悬停到Node上 + if (nfi.Node != null) nfi.Node.OnMouseEnter(EventArgs.Empty); + if (this._HoverNode != null) + this._HoverNode.OnMouseLeave(new MouseEventArgs(e.Button, e.Clicks, + (int)m_pt_in_canvas.X - this._HoverNode.Left, + (int)m_pt_in_canvas.Y - this._HoverNode.Top, e.Delta)); + this._HoverNode = nfi.Node; + this.OnHoverChanged(EventArgs.Empty); + bRedraw = true; + } + if (this._HoverNode != null) { + this._HoverNode.OnMouseMove(new MouseEventArgs(e.Button, e.Clicks, + (int)m_pt_in_canvas.X - this._HoverNode.Left, + (int)m_pt_in_canvas.Y - this._HoverNode.Top, e.Delta)); + m_gp_hover = null; + } else { + GraphicsPath gp = null; + foreach (var v in m_dic_gp_info) { //判断鼠标是否悬停到连线路径上 + if (v.Key.IsOutlineVisible(m_pt_in_canvas, m_p_line_hover)) { + gp = v.Key; + break; + } + } + if (m_gp_hover != gp) { + m_gp_hover = gp; + bRedraw = true; + } + } + if (bRedraw) this.Invalidate(); + } + + protected override void OnMouseUp(MouseEventArgs e) { + base.OnMouseUp(e); + var nfi = this.FindNodeFromPoint(m_pt_in_canvas); + switch (m_ca) { //鼠标抬起时候 判断行为 + case CanvasAction.MoveNode: //若正在移动Node 则重新记录当前位置 + foreach (STNode n in m_dic_pt_selected.Keys.ToList()) m_dic_pt_selected[n] = n.Location; + break; + case CanvasAction.ConnectOption: //若正在连线 则结束连接 + if (e.Location == m_pt_down_in_control) break; + if (nfi.NodeOption != null) { + if (m_option_down.IsInput) + nfi.NodeOption.ConnectOption(m_option_down); + else + m_option_down.ConnectOption(nfi.NodeOption); + } + break; + } + if (m_is_process_mouse_event && this._ActiveNode != null) { + var mea = new MouseEventArgs(e.Button, e.Clicks, + (int)m_pt_in_canvas.X - this._ActiveNode.Left, + (int)m_pt_in_canvas.Y - this._ActiveNode.Top, e.Delta); + this._ActiveNode.OnMouseUp(mea); + m_node_down = null; + } + m_is_process_mouse_event = true; //当前为断开连接操作不进行事件传递 下次将接受事件 + m_ca = CanvasAction.None; + this.Invalidate(); + } + + protected override void OnMouseEnter(EventArgs e) { + base.OnMouseEnter(e); + m_mouse_in_control = true; + } + + protected override void OnMouseLeave(EventArgs e) { + base.OnMouseLeave(e); + m_mouse_in_control = false; + if (this._HoverNode != null) this._HoverNode.OnMouseLeave(e); + this._HoverNode = null; + this.Invalidate(); + } + + protected override void OnMouseWheel(MouseEventArgs e) { + base.OnMouseWheel(e); + if ((Control.ModifierKeys & Keys.Control) == Keys.Control) { + float f = this._CanvasScale + (e.Delta < 0 ? -0.1f : 0.1f); + this.ScaleCanvas(f, this.Width / 2, this.Height / 2); + } else { + if (!m_mouse_in_control) return; + var nfi = this.FindNodeFromPoint(m_pt_in_canvas); + if (this._HoverNode != null) { + this._HoverNode.OnMouseWheel(new MouseEventArgs(e.Button, e.Clicks, + (int)m_pt_in_canvas.X - this._HoverNode.Left, + (int)m_pt_in_canvas.Y - this._HoverNode.Top, e.Delta)); + return; + } + int t = (int)DateTime.Now.Subtract(m_dt_vw).TotalMilliseconds; + if (t <= 30) t = 40; + else if (t <= 100) t = 20; + else if (t <= 150) t = 10; + else if (t <= 300) t = 4; + else t = 2; + this.MoveCanvas(this._CanvasOffsetX, m_real_canvas_y + (e.Delta < 0 ? -t : t), true, CanvasMoveArgs.Top);//process mouse mid + m_dt_vw = DateTime.Now; + } + } + + protected virtual void OnMouseHWheel(MouseEventArgs e) { + if ((Control.ModifierKeys & Keys.Control) == Keys.Control) return; + if (!m_mouse_in_control) return; + if (this._HoverNode != null) { + this._HoverNode.OnMouseWheel(new MouseEventArgs(e.Button, e.Clicks, + (int)m_pt_in_canvas.X - this._HoverNode.Left, + (int)m_pt_in_canvas.Y - this._HoverNode.Top, e.Delta)); + return; + } + int t = (int)DateTime.Now.Subtract(m_dt_hw).TotalMilliseconds; + if (t <= 30) t = 40; + else if (t <= 100) t = 20; + else if (t <= 150) t = 10; + else if (t <= 300) t = 4; + else t = 2; + this.MoveCanvas(m_real_canvas_x + (e.Delta > 0 ? -t : t), this._CanvasOffsetY, true, CanvasMoveArgs.Left); + m_dt_hw = DateTime.Now; + } + //===========================for node other event================================== + protected override void OnMouseClick(MouseEventArgs e) { + base.OnMouseClick(e); + if (this._ActiveNode != null && m_is_process_mouse_event) { + if (!this.PointInRectangle(this._ActiveNode.Rectangle, m_pt_in_canvas.X, m_pt_in_canvas.Y)) return; + this._ActiveNode.OnMouseClick(new MouseEventArgs(e.Button, e.Clicks, + (int)m_pt_down_in_canvas.X - this._ActiveNode.Left, + (int)m_pt_down_in_canvas.Y - this._ActiveNode.Top, e.Delta)); + } + } + + protected override void OnKeyDown(KeyEventArgs e) { + base.OnKeyDown(e); + if (this._ActiveNode != null) this._ActiveNode.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyEventArgs e) { + base.OnKeyUp(e); + if (this._ActiveNode != null) this._ActiveNode.OnKeyUp(e); + m_node_down = null; + } + + protected override void OnKeyPress(KeyPressEventArgs e) { + base.OnKeyPress(e); + if (this._ActiveNode != null) this._ActiveNode.OnKeyPress(e); + } + + #endregion + + protected override void OnDragEnter(DragEventArgs drgevent) { + base.OnDragEnter(drgevent); + if (this.DesignMode) return; + if (drgevent.Data.GetDataPresent("STNodeType")) + drgevent.Effect = DragDropEffects.Copy; + else + drgevent.Effect = DragDropEffects.None; + + } + + protected override void OnDragDrop(DragEventArgs drgevent) { + base.OnDragDrop(drgevent); + if (this.DesignMode) return; + if (drgevent.Data.GetDataPresent("STNodeType")) { + object data = drgevent.Data.GetData("STNodeType"); + if (!(data is Type)) return; + var t = (Type)data; + if (!t.IsSubclassOf(typeof(STNode))) return; + STNode node = (STNode)Activator.CreateInstance((t)); + Point pt = new Point(drgevent.X, drgevent.Y); + pt = this.PointToClient(pt); + pt = this.ControlToCanvas(pt); + node.Left = pt.X; node.Top = pt.Y; + this.Nodes.Add(node); + } + } + + #region protected ---------------------------------------------------------------------------------------------------- + /// + /// 当绘制背景网格线时候发生 + /// + /// 绘制工具 + /// 需要绘制宽度 + /// 需要绘制高度 + protected virtual void OnDrawGrid(DrawingTools dt, int nWidth, int nHeight) { + Graphics g = dt.Graphics; + using (Pen p_2 = new Pen(Color.FromArgb(65, this._GridColor))) { + using (Pen p_1 = new Pen(Color.FromArgb(30, this._GridColor))) { + float nIncrement = (20 * this._CanvasScale);             //网格间的间隔 根据比例绘制 + int n = 5 - (int)(this._CanvasOffsetX / nIncrement); + for (float f = this._CanvasOffsetX % nIncrement; f < nWidth; f += nIncrement) + g.DrawLine((n++ % 5 == 0 ? p_2 : p_1), f, 0, f, nHeight); + n = 5 - (int)(this._CanvasOffsetY / nIncrement); + for (float f = this._CanvasOffsetY % nIncrement; f < nHeight; f += nIncrement) + g.DrawLine((n++ % 5 == 0 ? p_2 : p_1), 0, f, nWidth, f); + //原点两天线 + p_1.Color = Color.FromArgb(this._Nodes.Count == 0 ? 255 : 120, this._GridColor); + g.DrawLine(p_1, this._CanvasOffsetX, 0, this._CanvasOffsetX, nHeight); + g.DrawLine(p_1, 0, this._CanvasOffsetY, nWidth, this._CanvasOffsetY); + } + } + } + /// + /// 当绘制 Node 时候发生 + /// + /// 绘制工具 + /// 可视画布区域大小 + protected virtual void OnDrawNode(DrawingTools dt, Rectangle rect) { + m_lst_node_out.Clear(); //清空超出视觉区域的 Node 的坐标 + foreach (STNode n in this._Nodes) { + if (this._ShowBorder) this.OnDrawNodeBorder(dt, n); + n.OnDrawNode(dt); //调用 Node 进行自身绘制主体部分 + if (!string.IsNullOrEmpty(n.Mark)) n.OnDrawMark(dt); //调用 Node 进行自身绘制 Mark 区域 + if (!rect.IntersectsWith(n.Rectangle)) { + m_lst_node_out.Add(n.Location); //判断此 Node 是否超出视觉区域 + } + } + } + /// + /// 当绘制 Node 边框时候发生 + /// + /// 绘制工具 + /// 目标node + protected virtual void OnDrawNodeBorder(DrawingTools dt, STNode node) { + Image img_border = null; + if (this._ActiveNode == node) img_border = m_img_border_active; + else if (node.IsSelected) img_border = m_img_border_selected; + else if (this._HoverNode == node) img_border = m_img_border_hover; + else img_border = m_img_border; + this.RenderBorder(dt.Graphics, node.Rectangle, img_border); + if (!string.IsNullOrEmpty(node.Mark)) this.RenderBorder(dt.Graphics, node.MarkRectangle, img_border); + } + /// + /// 当绘制已连接路径时候发生 + /// + /// 绘制工具 + protected virtual void OnDrawConnectedLine(DrawingTools dt) { + Graphics g = dt.Graphics; + g.SmoothingMode = SmoothingMode.HighQuality; + m_p_line_hover.Color = Color.FromArgb(50, 0, 0, 0); + var t = typeof(object); + foreach (STNode n in this._Nodes) { + foreach (STNodeOption op in n.OutputOptions) { + if (op == STNodeOption.Empty) continue; + if (op.DotColor != Color.Transparent) //确定线条颜色 + m_p_line.Color = op.DotColor; + else { + if (op.DataType == t) + m_p_line.Color = this._UnknownTypeColor; + else + m_p_line.Color = this._TypeColor.ContainsKey(op.DataType) ? this._TypeColor[op.DataType] : this._UnknownTypeColor;//value can not be null + } + foreach (var v in op.ConnectedOption) { + this.DrawBezier(g, m_p_line_hover, op.DotLeft + op.DotSize, op.DotTop + op.DotSize / 2, + v.DotLeft - 1, v.DotTop + v.DotSize / 2, this._Curvature); + this.DrawBezier(g, m_p_line, op.DotLeft + op.DotSize, op.DotTop + op.DotSize / 2, + v.DotLeft - 1, v.DotTop + v.DotSize / 2, this._Curvature); + if (m_is_buildpath) { //如果当前绘制需要重新建立已连接的路径缓存 + GraphicsPath gp = this.CreateBezierPath(op.DotLeft + op.DotSize, op.DotTop + op.DotSize / 2, + v.DotLeft - 1, v.DotTop + v.DotSize / 2, this._Curvature); + m_dic_gp_info.Add(gp, new ConnectionInfo() { Output = op, Input = v }); + } + } + } + } + m_p_line_hover.Color = this._HighLineColor; + if (m_gp_hover != null) { //如果当前有被悬停的连接路劲 则高亮绘制 + g.DrawPath(m_p_line_hover, m_gp_hover); + } + m_is_buildpath = false; //重置标志 下次绘制时候 不再重新建立路径缓存 + } + /// + /// 当绘制 Mark 详情信息时候发生 + /// + /// 绘制工具 + protected virtual void OnDrawMark(DrawingTools dt) { + Graphics g = dt.Graphics; + SizeF sz = g.MeasureString(m_find.Mark, this.Font); //确认文字需要的大小 + Rectangle rect = new Rectangle(m_pt_in_control.X + 15, + m_pt_in_control.Y + 10, + (int)sz.Width + 6, + 4 + (this.Font.Height + 4) * m_find.MarkLines.Length); //sz.Height并没有考虑文字的行距 所以这里高度自己计算 + + if (rect.Right > this.Width) rect.X = this.Width - rect.Width; + if (rect.Bottom > this.Height) rect.Y = this.Height - rect.Height; + if (rect.X < 0) rect.X = 0; + if (rect.Y < 0) rect.Y = 0; + + dt.SolidBrush.Color = this._MarkBackColor; + g.SmoothingMode = SmoothingMode.None; + g.FillRectangle(dt.SolidBrush, rect); //绘制背景区域 + rect.Width--; rect.Height--; + dt.Pen.Color = Color.FromArgb(255, this._MarkBackColor); + g.DrawRectangle(dt.Pen, rect); + dt.SolidBrush.Color = this._MarkForeColor; + + m_sf.LineAlignment = StringAlignment.Center; + //g.SmoothingMode = SmoothingMode.HighQuality; + rect.X += 2; rect.Width -= 3; + rect.Height = this.Font.Height + 4; + int nY = rect.Y + 2; + for (int i = 0; i < m_find.MarkLines.Length; i++) { //绘制文字 + rect.Y = nY + i * (this.Font.Height + 4); + g.DrawString(m_find.MarkLines[i], this.Font, dt.SolidBrush, rect, m_sf); + } + } + /// + /// 当移动 Node 时候 需要显示对齐参考线时候发生 + /// + /// 绘制工具 + /// 匹配的磁铁信息 + protected virtual void OnDrawMagnet(DrawingTools dt, MagnetInfo mi) { + if (this._ActiveNode == null) return; + Graphics g = dt.Graphics; + Pen pen = m_drawing_tools.Pen; + SolidBrush brush = dt.SolidBrush; + pen.Color = this._MagnetColor; + brush.Color = Color.FromArgb(this._MagnetColor.A / 3, this._MagnetColor); + g.SmoothingMode = SmoothingMode.None; + int nL = this._ActiveNode.Left, nMX = this._ActiveNode.Left + this._ActiveNode.Width / 2, nR = this._ActiveNode.Right; + int nT = this._ActiveNode.Top, nMY = this._ActiveNode.Top + this._ActiveNode.Height / 2, nB = this._ActiveNode.Bottom; + if (mi.XMatched) g.DrawLine(pen, this.CanvasToControl(mi.X, true), 0, this.CanvasToControl(mi.X, true), this.Height); + if (mi.YMatched) g.DrawLine(pen, 0, this.CanvasToControl(mi.Y, false), this.Width, this.CanvasToControl(mi.Y, false)); + g.TranslateTransform(this._CanvasOffsetX, this._CanvasOffsetY); //移动坐标系 + g.ScaleTransform(this._CanvasScale, this._CanvasScale); //缩放绘图表面 + if (mi.XMatched) { + //g.DrawLine(pen, this.CanvasToControl(mi.X, true), 0, this.CanvasToControl(mi.X, true), this.Height); + foreach (STNode n in this._Nodes) { + if (n.Left == mi.X || n.Right == mi.X || n.Left + n.Width / 2 == mi.X) { + //g.DrawRectangle(pen, n.Left, n.Top, n.Width - 1, n.Height - 1); + g.FillRectangle(brush, n.Rectangle); + } + } + } + if (mi.YMatched) { + //g.DrawLine(pen, 0, this.CanvasToControl(mi.Y, false), this.Width, this.CanvasToControl(mi.Y, false)); + foreach (STNode n in this._Nodes) { + if (n.Top == mi.Y || n.Bottom == mi.Y || n.Top + n.Height / 2 == mi.Y) { + //g.DrawRectangle(pen, n.Left, n.Top, n.Width - 1, n.Height - 1); + g.FillRectangle(brush, n.Rectangle); + } + } + } + g.ResetTransform(); + } + /// + /// 绘制选择的矩形区域 + /// + /// 绘制工具 + /// 位于控件上的矩形区域 + protected virtual void OnDrawSelectedRectangle(DrawingTools dt, RectangleF rectf) { + Graphics g = dt.Graphics; + SolidBrush brush = dt.SolidBrush; + dt.Pen.Color = this._SelectedRectangleColor; + g.DrawRectangle(dt.Pen, rectf.Left, rectf.Y, rectf.Width, rectf.Height); + brush.Color = Color.FromArgb(this._SelectedRectangleColor.A / 3, this._SelectedRectangleColor); + g.FillRectangle(brush, this.CanvasToControl(m_rect_select)); + } + /// + /// 绘制超出视觉区域的 Node 位置提示信息 + /// + /// 绘制工具 + /// 提示框边距 + /// 超出视觉区域的 Node 位置信息 + protected virtual void OnDrawNodeOutLocation(DrawingTools dt, Size sz, List lstPts) { + Graphics g = dt.Graphics; + SolidBrush brush = dt.SolidBrush; + brush.Color = this._LocationBackColor; + g.SmoothingMode = SmoothingMode.None; + if (lstPts.Count == this._Nodes.Count && this._Nodes.Count != 0) { //如果超出个数和集合个数一样多 则全部超出 绘制外切矩形 + g.FillRectangle(brush, this.CanvasToControl(this._CanvasValidBounds)); + } + g.FillRectangle(brush, 0, 0, 4, sz.Height); //绘制四边背景 + g.FillRectangle(brush, sz.Width - 4, 0, 4, sz.Height); + g.FillRectangle(brush, 4, 0, sz.Width - 8, 4); + g.FillRectangle(brush, 4, sz.Height - 4, sz.Width - 8, 4); + brush.Color = this._LocationForeColor; + foreach (var v in lstPts) { //绘制点 + var pt = this.CanvasToControl(v); + if (pt.X < 0) pt.X = 0; + if (pt.Y < 0) pt.Y = 0; + if (pt.X > sz.Width) pt.X = sz.Width - 4; + if (pt.Y > sz.Height) pt.Y = sz.Height - 4; + g.FillRectangle(brush, pt.X, pt.Y, 4, 4); + } + } + /// + /// 绘制提示信息 + /// + /// 绘制工具 + /// 需要绘制区域 + /// 需要绘制文本 + /// 信息前景色 + /// 信息背景色 + /// 信息位置 + protected virtual void OnDrawAlert(DrawingTools dt, Rectangle rect, string strText, Color foreColor, Color backColor, AlertLocation al) { + if (m_alpha_alert == 0) return; + Graphics g = dt.Graphics; + SolidBrush brush = dt.SolidBrush; + + g.SmoothingMode = SmoothingMode.None; + brush.Color = backColor; + dt.Pen.Color = brush.Color; + g.FillRectangle(brush, rect); + g.DrawRectangle(dt.Pen, rect.Left, rect.Top, rect.Width - 1, rect.Height - 1); + + brush.Color = foreColor; + m_sf.Alignment = StringAlignment.Center; + m_sf.LineAlignment = StringAlignment.Center; + g.SmoothingMode = SmoothingMode.HighQuality; + g.DrawString(strText, this.Font, brush, rect, m_sf); + } + /// + /// 获取提示信息需要绘制的矩形区域 + /// + /// 绘图表面 + /// 需要绘制文本 + /// 信息位置 + /// 矩形区域 + protected virtual Rectangle GetAlertRectangle(Graphics g, string strText, AlertLocation al) { + SizeF szf = g.MeasureString(m_str_alert, this.Font); + Size sz = new Size((int)Math.Round(szf.Width + 10), (int)Math.Round(szf.Height + 4)); + Rectangle rect = new Rectangle(4, this.Height - sz.Height - 4, sz.Width, sz.Height); + + switch (al) { + case AlertLocation.Left: + rect.Y = (this.Height - sz.Height) >> 1; + break; + case AlertLocation.Top: + rect.Y = 4; + rect.X = (this.Width - sz.Width) >> 1; + break; + case AlertLocation.Right: + rect.X = this.Width - sz.Width - 4; + rect.Y = (this.Height - sz.Height) >> 1; + break; + case AlertLocation.Bottom: + rect.X = (this.Width - sz.Width) >> 1; + break; + case AlertLocation.Center: + rect.X = (this.Width - sz.Width) >> 1; + rect.Y = (this.Height - sz.Height) >> 1; + break; + case AlertLocation.LeftTop: + rect.X = rect.Y = 4; + break; + case AlertLocation.RightTop: + rect.Y = 4; + rect.X = this.Width - sz.Width - 4; + break; + case AlertLocation.RightBottom: + rect.X = this.Width - sz.Width - 4; + break; + } + return rect; + } + + #endregion protected + + #region internal + + internal void BuildLinePath() { + foreach (var v in m_dic_gp_info) v.Key.Dispose(); + m_dic_gp_info.Clear(); + m_is_buildpath = true; + this.Invalidate(); + } + + internal void OnDrawAlert(Graphics g) { + m_rect_alert = this.GetAlertRectangle(g, m_str_alert, m_al); + Color clr_fore = Color.FromArgb((int)((float)m_alpha_alert / 255 * m_forecolor_alert.A), m_forecolor_alert); + Color clr_back = Color.FromArgb((int)((float)m_alpha_alert / 255 * m_backcolor_alert.A), m_backcolor_alert); + this.OnDrawAlert(m_drawing_tools, m_rect_alert, m_str_alert, clr_fore, clr_back, m_al); + } + + internal void InternalAddSelectedNode(STNode node) { + node.IsSelected = true; + lock (m_hs_node_selected) m_hs_node_selected.Add(node); + } + + internal void InternalRemoveSelectedNode(STNode node) { + node.IsSelected = false; + lock (m_hs_node_selected) m_hs_node_selected.Remove(node); + } + + #endregion internal + + #region private ----------------------------------------------------------------------------------------------------- + + private void MoveCanvasThread() { + bool bRedraw; + while (true) { + bRedraw = false; + if (m_real_canvas_x != this._CanvasOffsetX) { + float nx = m_real_canvas_x - this._CanvasOffsetX; + float n = Math.Abs(nx) / 10; + float nTemp = Math.Abs(nx); + if (nTemp <= 4) n = 1; + else if (nTemp <= 12) n = 2; + else if (nTemp <= 30) n = 3; + if (nTemp < 1) this._CanvasOffsetX = m_real_canvas_x; + else + this._CanvasOffsetX += nx > 0 ? n : -n; + bRedraw = true; + } + if (m_real_canvas_y != this._CanvasOffsetY) { + float ny = m_real_canvas_y - this._CanvasOffsetY; + float n = Math.Abs(ny) / 10; + float nTemp = Math.Abs(ny); + if (nTemp <= 4) n = 1; + else if (nTemp <= 12) n = 2; + else if (nTemp <= 30) n = 3; + if (nTemp < 1) + this._CanvasOffsetY = m_real_canvas_y; + else + this._CanvasOffsetY += ny > 0 ? n : -n; + bRedraw = true; + } + if (bRedraw) { + m_pt_canvas_old.X = this._CanvasOffsetX; + m_pt_canvas_old.Y = this._CanvasOffsetY; + this.Invalidate(); + Thread.Sleep(30); + } else { + Thread.Sleep(100); + } + } + } + + private void ShowAlertThread() { + while (true) { + int nTime = m_time_alert - (int)DateTime.Now.Subtract(m_dt_alert).TotalMilliseconds; + if (nTime > 0) { + Thread.Sleep(nTime); + continue; + } + if (nTime < -1000) { + if (m_alpha_alert != 0) { + m_alpha_alert = 0; + this.Invalidate(); + } + Thread.Sleep(100); + } else { + m_alpha_alert = (int)(255 - (-nTime / 1000F) * 255); + this.Invalidate(m_rect_alert); + Thread.Sleep(50); + } + } + } + + private Image CreateBorderImage(Color clr) { + Image img = new Bitmap(12, 12); + using (Graphics g = Graphics.FromImage(img)) { + g.SmoothingMode = SmoothingMode.HighQuality; + using (GraphicsPath gp = new GraphicsPath()) { + gp.AddEllipse(new Rectangle(0, 0, 11, 11)); + using (PathGradientBrush b = new PathGradientBrush(gp)) { + b.CenterColor = Color.FromArgb(200, clr); + b.SurroundColors = new Color[] { Color.FromArgb(10, clr) }; + g.FillPath(b, gp); + } + } + } + return img; + } + + private ConnectionStatus DisConnectionHover() { + if (!m_dic_gp_info.ContainsKey(m_gp_hover)) return ConnectionStatus.DisConnected; + ConnectionInfo ci = m_dic_gp_info[m_gp_hover]; + var ret = ci.Output.DisConnectOption(ci.Input); + //this.OnOptionDisConnected(new STNodeOptionEventArgs(ci.Output, ci.Input, ret)); + if (ret == ConnectionStatus.DisConnected) { + m_dic_gp_info.Remove(m_gp_hover); + m_gp_hover.Dispose(); + m_gp_hover = null; + this.Invalidate(); + } + return ret; + } + + 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; + } + + private void MoveNode(Point pt) { + int nX = (int)((pt.X - m_pt_down_in_control.X) / this._CanvasScale); + int nY = (int)((pt.Y - m_pt_down_in_control.Y) / this._CanvasScale); + lock (m_hs_node_selected) { + foreach (STNode v in m_hs_node_selected) { + v.Left = m_dic_pt_selected[v].X + nX; + v.Top = m_dic_pt_selected[v].Y + nY; + } + if (this._ShowMagnet) { + MagnetInfo mi = this.CheckMagnet(this._ActiveNode); + if (mi.XMatched) { + foreach (STNode v in m_hs_node_selected) v.Left -= mi.OffsetX; + } + if (mi.YMatched) { + foreach (STNode v in m_hs_node_selected) v.Top -= mi.OffsetY; + } + } + } + this.Invalidate(); + } + + protected internal virtual void BuildBounds() { + if (this._Nodes.Count == 0) { + this._CanvasValidBounds = this.ControlToCanvas(this.DisplayRectangle); + return; + } + int x = int.MaxValue; + int y = int.MaxValue; + int r = int.MinValue; + int b = int.MinValue; + foreach (STNode n in this._Nodes) { + if (x > n.Left) x = n.Left; + if (y > n.Top) y = n.Top; + if (r < n.Right) r = n.Right; + if (b < n.Bottom) b = n.Bottom; + } + this._CanvasValidBounds.X = x - 60; + this._CanvasValidBounds.Y = y - 60; + this._CanvasValidBounds.Width = r - x + 120; + this._CanvasValidBounds.Height = b - y + 120; + } + + private bool PointInRectangle(Rectangle rect, float x, float y) { + if (x < rect.Left) return false; + if (x > rect.Right) return false; + if (y < rect.Top) return false; + if (y > rect.Bottom) return false; + return true; + } + + private void BuildMagnetLocation() { + m_lst_magnet_x.Clear(); + m_lst_magnet_y.Clear(); + foreach (STNode v in this._Nodes) { + if (v.IsSelected) continue; + m_lst_magnet_x.Add(v.Left); + m_lst_magnet_x.Add(v.Left + v.Width / 2); + m_lst_magnet_x.Add(v.Left + v.Width); + m_lst_magnet_y.Add(v.Top); + m_lst_magnet_y.Add(v.Top + v.Height / 2); + m_lst_magnet_y.Add(v.Top + v.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 bFlag = false; + foreach (var mx in m_lst_magnet_mx) { + foreach (var x in m_lst_magnet_x) { + if (Math.Abs(mx - x) <= 5) { + bFlag = true; + m_mi.X = x; + m_mi.OffsetX = mx - x; + m_mi.XMatched = true; + break; + } + } + if (bFlag) break; + } + bFlag = false; + foreach (var my in m_lst_magnet_my) { + foreach (var y in m_lst_magnet_y) { + if (Math.Abs(my - y) <= 5) { + bFlag = true; + m_mi.Y = y; + m_mi.OffsetY = my - y; + m_mi.YMatched = true; + break; + } + } + if (bFlag) break; + } + return m_mi; + } + + private void DrawBezier(Graphics g, Pen p, PointF ptStart, PointF ptEnd, float f) { + this.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) { + float n = (Math.Abs(x1 - x2) * f); + if (this._Curvature != 0 && n < 30) n = 30; + g.DrawBezier(p, + x1, y1, + x1 + n, y1, + x2 - n, y2, + x2, y2); + } + + private GraphicsPath CreateBezierPath(float x1, float y1, float x2, float y2, float f) { + GraphicsPath gp = new GraphicsPath(); + float n = (Math.Abs(x1 - x2) * f); + if (this._Curvature != 0 && n < 30) n = 30; + gp.AddBezier( + x1, y1, + x1 + n, y1, + x2 - n, y2, + x2, y2 + ); + return gp; + } + + 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); + } + + #endregion private + + #region public -------------------------------------------------------------------------------------------------------- + /// + /// 通过画布坐标进行寻找 + /// + /// 画布中的坐标 + /// 寻找到的数据 + public NodeFindInfo FindNodeFromPoint(PointF pt) { + m_find.Node = null; m_find.NodeOption = null; m_find.Mark = null; + for (int i = this._Nodes.Count - 1; i >= 0; i--) { + if (!string.IsNullOrEmpty(this._Nodes[i].Mark) && this.PointInRectangle(this._Nodes[i].MarkRectangle, pt.X, pt.Y)) { + m_find.Mark = this._Nodes[i].Mark; + m_find.MarkLines = this._Nodes[i].MarkLines; + return m_find; + } + foreach (STNodeOption v in this._Nodes[i].InputOptions) { + if (v == STNodeOption.Empty) continue; + if (this.PointInRectangle(v.DotRectangle, pt.X, pt.Y)) m_find.NodeOption = v; + } + foreach (STNodeOption v in this._Nodes[i].OutputOptions) { + if (v == STNodeOption.Empty) continue; + if (this.PointInRectangle(v.DotRectangle, pt.X, pt.Y)) m_find.NodeOption = v; + } + if (this.PointInRectangle(this._Nodes[i].Rectangle, pt.X, pt.Y)) { + m_find.Node = this._Nodes[i]; + } + if (m_find.NodeOption != null || m_find.Node != null) return m_find; + } + return m_find; + } + /// + /// 获取已经被选择的 Node 集合 + /// + /// Node 集合 + public STNode[] GetSelectedNode() { + return m_hs_node_selected.ToArray(); + } + /// + /// 将画布坐标转换为控件坐标 + /// + /// 参数 + /// 是否为 X 坐标 + /// 转换后的坐标 + public float CanvasToControl(float number, bool isX) { + return (number * this._CanvasScale) + (isX ? this._CanvasOffsetX : this._CanvasOffsetY); + } + /// + /// 将画布坐标转换为控件坐标 + /// + /// 坐标 + /// 转换后的坐标 + public PointF CanvasToControl(PointF pt) { + pt.X = (pt.X * this._CanvasScale) + this._CanvasOffsetX; + pt.Y = (pt.Y * this._CanvasScale) + this._CanvasOffsetY; + //pt.X += this._CanvasOffsetX; + //pt.Y += this._CanvasOffsetY; + return pt; + } + /// + /// 将画布坐标转换为控件坐标 + /// + /// 坐标 + /// 转换后的坐标 + public Point CanvasToControl(Point pt) { + pt.X = (int)(pt.X * this._CanvasScale + this._CanvasOffsetX); + pt.Y = (int)(pt.Y * this._CanvasScale + this._CanvasOffsetY); + //pt.X += (int)this._CanvasOffsetX; + //pt.Y += (int)this._CanvasOffsetY; + return pt; + } + /// + /// 将画布坐标转换为控件坐标 + /// + /// 矩形区域 + /// 转换后的矩形区域 + public Rectangle CanvasToControl(Rectangle rect) { + rect.X = (int)((rect.X * this._CanvasScale) + this._CanvasOffsetX); + rect.Y = (int)((rect.Y * this._CanvasScale) + this._CanvasOffsetY); + rect.Width = (int)(rect.Width * this._CanvasScale); + rect.Height = (int)(rect.Height * this._CanvasScale); + //rect.X += (int)this._CanvasOffsetX; + //rect.Y += (int)this._CanvasOffsetY; + return rect; + } + /// + /// 将画布坐标转换为控件坐标 + /// + /// 矩形区域 + /// 转换后的矩形区域 + public RectangleF CanvasToControl(RectangleF rect) { + rect.X = (rect.X * this._CanvasScale) + this._CanvasOffsetX; + rect.Y = (rect.Y * this._CanvasScale) + this._CanvasOffsetY; + rect.Width = (rect.Width * this._CanvasScale); + rect.Height = (rect.Height * this._CanvasScale); + //rect.X += this._CanvasOffsetX; + //rect.Y += this._CanvasOffsetY; + return rect; + } + /// + /// 将控件坐标转换为画布坐标 + /// + /// 参数 + /// 是否为 X 坐标 + /// 转换后的坐标 + public float ControlToCanvas(float number, bool isX) { + return (number - (isX ? this._CanvasOffsetX : this._CanvasOffsetY)) / this._CanvasScale; + } + /// + /// 将控件坐标转换为画布坐标 + /// + /// 坐标 + /// 转换后的坐标 + public Point ControlToCanvas(Point pt) { + pt.X = (int)((pt.X - this._CanvasOffsetX) / this._CanvasScale); + pt.Y = (int)((pt.Y - this._CanvasOffsetY) / this._CanvasScale); + return pt; + } + /// + /// 将控件坐标转换为画布坐标 + /// + /// 坐标 + /// 转换后的坐标 + public PointF ControlToCanvas(PointF pt) { + pt.X = ((pt.X - this._CanvasOffsetX) / this._CanvasScale); + pt.Y = ((pt.Y - this._CanvasOffsetY) / this._CanvasScale); + return pt; + } + /// + /// 将控件坐标转换为画布坐标 + /// + /// 矩形区域 + /// 转换后的区域 + public Rectangle ControlToCanvas(Rectangle rect) { + rect.X = (int)((rect.X - this._CanvasOffsetX) / this._CanvasScale); + rect.Y = (int)((rect.Y - this._CanvasOffsetY) / this._CanvasScale); + rect.Width = (int)(rect.Width / this._CanvasScale); + rect.Height = (int)(rect.Height / this._CanvasScale); + return rect; + } + /// + /// 将控件坐标转换为画布坐标 + /// + /// 矩形区域 + /// 转换后的区域 + public RectangleF ControlToCanvas(RectangleF rect) { + rect.X = ((rect.X - this._CanvasOffsetX) / this._CanvasScale); + rect.Y = ((rect.Y - this._CanvasOffsetY) / this._CanvasScale); + rect.Width = (rect.Width / this._CanvasScale); + rect.Height = (rect.Height / this._CanvasScale); + return rect; + } + /// + /// 移动画布原点坐标到指定的控件坐标位置 + /// 当不存在 Node 时候 无法移动 + /// + /// X 坐标 + /// Y 坐标 + /// 移动过程中是否启动动画效果 + /// 指定需要修改的坐标参数 + public void MoveCanvas(float x, float y, bool bAnimation, CanvasMoveArgs ma) { + if (this._Nodes.Count == 0) { + m_real_canvas_x = m_real_canvas_y = 10; + return; + } + int l = (int)((this._CanvasValidBounds.Left + 50) * this._CanvasScale); + int t = (int)((this._CanvasValidBounds.Top + 50) * this._CanvasScale); + int r = (int)((this._CanvasValidBounds.Right - 50) * this._CanvasScale); + int b = (int)((this._CanvasValidBounds.Bottom - 50) * this._CanvasScale); + if (r + x < 0) x = -r; + if (this.Width - l < x) x = this.Width - l; + if (b + y < 0) y = -b; + if (this.Height - t < y) y = this.Height - t; + if (bAnimation) { + if ((ma & CanvasMoveArgs.Left) == CanvasMoveArgs.Left) + m_real_canvas_x = x; + if ((ma & CanvasMoveArgs.Top) == CanvasMoveArgs.Top) + m_real_canvas_y = y; + } else { + m_real_canvas_x = this._CanvasOffsetX = x; + m_real_canvas_y = this._CanvasOffsetY = y; + } + this.OnCanvasMoved(EventArgs.Empty); + } + /// + /// 缩放画布 + /// 当不存在 Node 时候 无法缩放 + /// + /// 缩放比例 + /// 缩放中心X位于控件上的坐标 + /// 缩放中心Y位于控件上的坐标 + public void ScaleCanvas(float f, float x, float y) { + if (this._Nodes.Count == 0) { + this._CanvasScale = 1F; + return; + } + if (this._CanvasScale == f) return; + if (f < 0.5) f = 0.5f; else if (f > 3) f = 3; + float x_c = this.ControlToCanvas(x, true); + float y_c = this.ControlToCanvas(y, false); + this._CanvasScale = f; + this._CanvasOffsetX = m_real_canvas_x -= this.CanvasToControl(x_c, true) - x; + this._CanvasOffsetY = m_real_canvas_y -= this.CanvasToControl(y_c, false) - y; + this.OnCanvasScaled(EventArgs.Empty); + this.Invalidate(); + } + /// + /// 获取当前已连接的 Option 对应关系 + /// + /// 连接信息集合 + public ConnectionInfo[] GetConnectionInfo() { + return m_dic_gp_info.Values.ToArray(); + } + /// + /// 判断两个 Node 之间是否存在连接路径 + /// + /// 起始 Node + /// 目标 Node + /// 若存在路径返回true 否则false + public static bool CanFindNodePath(STNode nodeStart, STNode nodeFind) { + HashSet hs = new HashSet(); + return STNodeEditor.CanFindNodePath(nodeStart, nodeFind, hs); + } + private static bool CanFindNodePath(STNode nodeStart, STNode nodeFind, HashSet hs) { + foreach (STNodeOption op_1 in nodeStart.OutputOptions) { + foreach (STNodeOption op_2 in op_1.ConnectedOption) { + if (op_2.Owner == nodeFind) return true; + if (hs.Add(op_2.Owner)) { + if (STNodeEditor.CanFindNodePath(op_2.Owner, nodeFind)) return true; + } + } + } + return false; + } + /// + /// 获取画布中指定矩形区域图像 + /// + /// 画布中指定的矩形区域 + /// 图像 + public Image GetCanvasImage(Rectangle rect) { return this.GetCanvasImage(rect, 1f); } + /// + /// 获取画布中指定矩形区域图像 + /// + /// 画布中指定的矩形区域 + /// 缩放比例 + /// 图像 + public Image GetCanvasImage(Rectangle rect, float fScale) { + if (fScale < 0.5) fScale = 0.5f; else if (fScale > 3) fScale = 3; + Image img = new Bitmap((int)(rect.Width * fScale), (int)(rect.Height * fScale)); + using (Graphics g = Graphics.FromImage(img)) { + g.Clear(this.BackColor); + g.ScaleTransform(fScale, fScale); + m_drawing_tools.Graphics = g; + + if (this._ShowGrid) this.OnDrawGrid(m_drawing_tools, rect.Width, rect.Height); + g.TranslateTransform(-rect.X, -rect.Y); //移动坐标系 + this.OnDrawNode(m_drawing_tools, rect); + this.OnDrawConnectedLine(m_drawing_tools); + + g.ResetTransform(); + + if (this._ShowLocation) this.OnDrawNodeOutLocation(m_drawing_tools, img.Size, m_lst_node_out); + } + return img; + } + /// + /// 保存画布中的类容到文件中 + /// + /// 文件路径 + public void SaveCanvas(string strFileName) { + using (FileStream fs = new FileStream(strFileName, FileMode.Create, FileAccess.Write)) { + this.SaveCanvas(fs); + } + } + /// + /// 保存画布中的类容到数据流 + /// + /// 数据流对象 + public void SaveCanvas(Stream s) { + Dictionary dic = new Dictionary(); + s.Write(new byte[] { (byte)'S', (byte)'T', (byte)'N', (byte)'D' }, 0, 4); //file head + s.WriteByte(1); //ver + using (GZipStream gs = new GZipStream(s, CompressionMode.Compress)) { + gs.Write(BitConverter.GetBytes(this._CanvasOffsetX), 0, 4); + gs.Write(BitConverter.GetBytes(this._CanvasOffsetY), 0, 4); + gs.Write(BitConverter.GetBytes(this._CanvasScale), 0, 4); + gs.Write(BitConverter.GetBytes(this._Nodes.Count), 0, 4); + foreach (STNode node in this._Nodes) { + try { + byte[] byNode = node.GetSaveData(); + gs.Write(BitConverter.GetBytes(byNode.Length), 0, 4); + gs.Write(byNode, 0, byNode.Length); + foreach (STNodeOption op in node.InputOptions) if (!dic.ContainsKey(op)) dic.Add(op, dic.Count); + foreach (STNodeOption op in node.OutputOptions) if (!dic.ContainsKey(op)) dic.Add(op, dic.Count); + } catch (Exception ex) { + throw new Exception("获取节点数据出错-" + node.Title, ex); + } + } + gs.Write(BitConverter.GetBytes(m_dic_gp_info.Count), 0, 4); + foreach (var v in m_dic_gp_info.Values) + gs.Write(BitConverter.GetBytes(((dic[v.Output] << 32) | dic[v.Input])), 0, 8); + } + } + /// + /// 获取画布中内容二进制数据 + /// + /// 二进制数据 + public byte[] GetCanvasData() { + using (MemoryStream ms = new MemoryStream()) { + this.SaveCanvas(ms); + return ms.ToArray(); + } + } + /// + /// 加载程序集 + /// + /// 程序集集合 + /// 存在STNode类型的文件的个数 + public int LoadAssembly(string[] strFiles) { + int nCount = 0; + foreach (var v in strFiles) { + try { + if (this.LoadAssembly(v)) nCount++; + } catch { } + } + return nCount; + } + /// + /// 加载程序集 + /// + /// 指定需要加载的文件 + /// 是否加载成功 + public bool LoadAssembly(string strFile) { + bool bFound = false; + Assembly asm = Assembly.LoadFrom(strFile); + if (asm == null) return false; + foreach (var t in asm.GetTypes()) { + if (t.IsAbstract) continue; + if (t == m_type_node || t.IsSubclassOf(m_type_node)) { + if (m_dic_type.ContainsKey(t.GUID.ToString())) continue; + m_dic_type.Add(t.GUID.ToString(), t); + bFound = true; + } + } + return bFound; + } + /// + /// 获取当前编辑器中已加载的Node类型 + /// + /// 类型集合 + public Type[] GetTypes() { + return m_dic_type.Values.ToArray(); + } + /// + /// 从文件中加载数据 + /// 注意: 此方法并不会清空画布中数据 而是数据叠加 + /// + /// 文件路径 + public void LoadCanvas(string strFileName) { + using (MemoryStream ms = new MemoryStream(File.ReadAllBytes(strFileName))) + this.LoadCanvas(ms); + } + /// + /// 从二进制加载数据 + /// 注意: 此方法并不会清空画布中数据 而是数据叠加 + /// + /// 二进制数据 + public void LoadCanvas(byte[] byData) { + using (MemoryStream ms = new MemoryStream(byData)) + this.LoadCanvas(ms); + } + /// + /// 从数据流中加载数据 + /// 注意: 此方法并不会清空画布中数据 而是数据叠加 + /// + /// 数据流对象 + public void LoadCanvas(Stream s) { + int nLen = 0; + byte[] byLen = new byte[4]; + s.Read(byLen, 0, 4); + if (BitConverter.ToInt32(byLen, 0) != BitConverter.ToInt32(new byte[] { (byte)'S', (byte)'T', (byte)'N', (byte)'D' }, 0)) + throw new InvalidDataException("无法识别的文件类型"); + if (s.ReadByte() != 1) throw new InvalidDataException("无法识别的文件版本号"); + using (GZipStream gs = new GZipStream(s, CompressionMode.Decompress)) { + gs.Read(byLen, 0, 4); + float x = BitConverter.ToSingle(byLen, 0); + gs.Read(byLen, 0, 4); + float y = BitConverter.ToSingle(byLen, 0); + gs.Read(byLen, 0, 4); + float scale = BitConverter.ToSingle(byLen, 0); + gs.Read(byLen, 0, 4); + int nCount = BitConverter.ToInt32(byLen, 0); + Dictionary dic = new Dictionary(); + HashSet hs = new HashSet(); + byte[] byData = null; + for (int i = 0; i < nCount; i++) { + gs.Read(byLen, 0, byLen.Length); + nLen = BitConverter.ToInt32(byLen, 0); + byData = new byte[nLen]; + gs.Read(byData, 0, byData.Length); + STNode node = null; + try { node = this.GetNodeFromData(byData); } catch (Exception ex) { + throw new Exception("加载节点时发生错误可能数据已损坏\r\n" + ex.Message, ex); + } + try { this._Nodes.Add(node); } catch (Exception ex) { + throw new Exception("加载节点出错-" + node.Title, ex); + } + foreach (STNodeOption op in node.InputOptions) if (hs.Add(op)) dic.Add(dic.Count, op); + foreach (STNodeOption op in node.OutputOptions) if (hs.Add(op)) dic.Add(dic.Count, op); + } + gs.Read(byLen, 0, 4); + nCount = BitConverter.ToInt32(byLen, 0); + byData = new byte[8]; + for (int i = 0; i < nCount; i++) { + gs.Read(byData, 0, byData.Length); + long id = BitConverter.ToInt64(byData, 0); + long op_out = id >> 32; + long op_in = (int)id; + dic[op_out].ConnectOption(dic[op_in]); + } + this.ScaleCanvas(scale, 0, 0); + this.MoveCanvas(x, y, false, CanvasMoveArgs.All); + } + this.BuildBounds(); + foreach (STNode node in this._Nodes) node.OnEditorLoadCompleted(); + } + + private STNode GetNodeFromData(byte[] byData) { + int nIndex = 0; + string strModel = Encoding.UTF8.GetString(byData, nIndex + 1, byData[nIndex]); + nIndex += byData[nIndex] + 1; + string strGUID = Encoding.UTF8.GetString(byData, nIndex + 1, byData[nIndex]); + nIndex += byData[nIndex] + 1; + + int nLen = 0; + + Dictionary dic = new Dictionary(); + while (nIndex < byData.Length) { + nLen = BitConverter.ToInt32(byData, nIndex); + nIndex += 4; + string strKey = Encoding.UTF8.GetString(byData, nIndex, nLen); + nIndex += nLen; + nLen = BitConverter.ToInt32(byData, nIndex); + nIndex += 4; + byte[] byValue = new byte[nLen]; + Array.Copy(byData, nIndex, byValue, 0, nLen); + nIndex += nLen; + dic.Add(strKey, byValue); + } + if (!m_dic_type.ContainsKey(strGUID)) throw new TypeLoadException("无法找到类型 {" + strModel.Split('|')[1] + "} 所在程序集 确保程序集 {" + strModel.Split('|')[0] + "} 已被编辑器正确加载 可通过调用LoadAssembly()加载程序集"); + Type t = m_dic_type[strGUID]; ; + STNode node = (STNode)Activator.CreateInstance(t); + node.OnLoadNode(dic); + return node; + } + /// + /// 在画布中显示提示信息 + /// + /// 要显示的信息 + /// 信息前景色 + /// 信息背景色 + public void ShowAlert(string strText, Color foreColor, Color backColor) { + this.ShowAlert(strText, foreColor, backColor, 1000, AlertLocation.RightBottom, true); + } + /// + /// 在画布中显示提示信息 + /// + /// 要显示的信息 + /// 信息前景色 + /// 信息背景色 + /// 信息要显示的位置 + public void ShowAlert(string strText, Color foreColor, Color backColor, AlertLocation al) { + this.ShowAlert(strText, foreColor, backColor, 1000, al, 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.Now; + m_alpha_alert = 255; + m_al = al; + if (bRedraw) this.Invalidate(); + } + /// + /// 设置画布中活动的节点 + /// + /// 需要被设置为活动的节点 + /// 设置前的活动节点 + public STNode SetActiveNode(STNode node) { + if (node != null && !this._Nodes.Contains(node)) return this._ActiveNode; + STNode ret = this._ActiveNode; + if (this._ActiveNode != node) { //重置活动选择节点 + if (node != null) { + this._Nodes.MoveToEnd(node); + node.IsActive = true; + node.SetSelected(true, false); + node.OnGotFocus(EventArgs.Empty); + } + if (this._ActiveNode != null) { + this._ActiveNode.IsActive /*= this._ActiveNode.IsSelected*/ = false; + this._ActiveNode.OnLostFocus(EventArgs.Empty); + } + this._ActiveNode = node; + this.Invalidate(); + this.OnActiveChanged(EventArgs.Empty); + //this.OnSelectedChanged(EventArgs.Empty); + } + return ret; + } + /// + /// 向画布中添加一个被选中的节点 + /// + /// 需要被选中的节点 + /// 是否添加成功 + public bool AddSelectedNode(STNode node) { + if (!this._Nodes.Contains(node)) return false; + bool b = !node.IsSelected; + node.IsSelected = true; + lock (m_hs_node_selected) return m_hs_node_selected.Add(node) || b; + } + /// + /// 向画布中移除一个被选中的节点 + /// + /// 需要被移除的节点 + /// 是移除否成功 + public bool RemoveSelectedNode(STNode node) { + if (!this._Nodes.Contains(node)) return false; + bool b = node.IsSelected; + node.IsSelected = false; + lock (m_hs_node_selected) return m_hs_node_selected.Remove(node) || b; + } + /// + /// 向编辑器中添加默认数据类型颜色 + /// + /// 数据类型 + /// 对应颜色 + /// 被设置后的颜色 + public Color SetTypeColor(Type t, Color clr) { + return this.SetTypeColor(t, clr, false); + } + /// + /// 向编辑器中添加默认数据类型颜色 + /// + /// 数据类型 + /// 对应颜色 + /// 若已经存在是否替换颜色 + /// 被设置后的颜色 + public Color SetTypeColor(Type t, Color clr, bool bReplace) { + if (this._TypeColor.ContainsKey(t)) { + if (bReplace) this._TypeColor[t] = clr; + } else { + this._TypeColor.Add(t, clr); + } + return this._TypeColor[t]; + } + + #endregion public + } +} diff --git a/ST.Library.UI/STNodeEditor/STNodeEditorDataType.cs b/ST.Library.UI/NodeEditor/STNodeEditorDataType.cs old mode 100755 new mode 100644 similarity index 95% rename from ST.Library.UI/STNodeEditor/STNodeEditorDataType.cs rename to ST.Library.UI/NodeEditor/STNodeEditorDataType.cs index 5a8cf03..d84fa18 --- a/ST.Library.UI/STNodeEditor/STNodeEditorDataType.cs +++ b/ST.Library.UI/NodeEditor/STNodeEditorDataType.cs @@ -1,198 +1,203 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.ComponentModel; -using System.Drawing; - -namespace ST.Library.UI -{ - public enum ConnectionStatus - { - /// - /// 不存在所有者 - /// - [Description("不存在所有者")] - NoOwner, - /// - /// 相同的所有者 - /// - [Description("相同的所有者")] - SameOwner, - /// - /// 均为输入或者输出选项 - /// - [Description("均为输入或者输出选项")] - SameInputOrOutput, - /// - /// 不同的数据类型 - /// - [Description("不同的数据类型")] - ErrorType, - /// - /// 单连接节点 - /// - [Description("单连接节点")] - SingleOption, - /// - /// 出现环形路径 - /// - [Description("出现环形路径")] - Loop, - /// - /// 已存在的连接 - /// - [Description("已存在的连接")] - Exists, - /// - /// 已经连接 - /// - [Description("已经连接")] - Connected, - /// - /// 连接被断开 - /// - [Description("连接被断开")] - DisConnected, - /// - /// 节点被锁定 - /// - [Description("节点被锁定")] - Locked, - /// - /// 操作被拒绝 - /// - [Description("操作被拒绝")] - Reject, - /// - /// 正在被连接 - /// - [Description("正在被连接")] - Connecting, - /// - /// 正在断开连接 - /// - [Description("正在断开连接")] - DisConnecting - } - - public enum AlertLocation - { - Left, - Top, - Right, - Bottom, - Center, - LeftTop, - RightTop, - RightBottom, - LeftBottom, - } - - public struct DrawingTools - { - public Graphics Graphics; - public Pen Pen; - public SolidBrush SolidBrush; - } - - public enum CanvasMoveArgs //移动画布时需要的参数 查看->MoveCanvas() - { - Left = 1, //表示 仅移动 X 坐标 - Top = 2, //表示 仅移动 Y 坐标 - All = 4 //表示 X Y 同时移动 - } - - public struct NodeFindInfo - { - public STNode Node; - public STNodeOption NodeOption; - public string Mark; - public string[] MarkLines; - } - - public struct ConnectionInfo - { - public STNodeOption Input; - public STNodeOption Output; - } - - public delegate void STNodeOptionEventHandler(object sender, STNodeOptionEventArgs e); - - public class STNodeOptionEventArgs : EventArgs - { - private STNodeOption _TargetOption; - /// - /// 触发此事件的对应Option - /// - public STNodeOption TargetOption { - get { return _TargetOption; } - } - - private ConnectionStatus _Status; - /// - /// Option之间的连线状态 - /// - public ConnectionStatus Status { - get { return _Status; } - internal set { _Status = value; } - } - - private bool _IsSponsor; - /// - /// 是否为此次行为的发起者 - /// - public bool IsSponsor { - get { return _IsSponsor; } - } - - public STNodeOptionEventArgs(bool isSponsor, STNodeOption opTarget, ConnectionStatus cr) { - this._IsSponsor = isSponsor; - this._TargetOption = opTarget; - this._Status = cr; - } - } - - public delegate void STNodeEditorEventHandler(object sender, STNodeEditorEventArgs e); - public delegate void STNodeEditorOptionEventHandler(object sender, STNodeEditorOptionEventArgs e); - - - public class STNodeEditorEventArgs : EventArgs - { - private STNode _Node; - - public STNode Node { - get { return _Node; } - } - - public STNodeEditorEventArgs(STNode node) { - this._Node = node; - } - } - - public class STNodeEditorOptionEventArgs : STNodeOptionEventArgs - { - - private STNodeOption _CurrentOption; - /// - /// 主动触发事件的Option - /// - public STNodeOption CurrentOption { - get { return _CurrentOption; } - } - - private bool _Continue = true; - /// - /// 是否继续向下操作 用于Begin(Connecting/DisConnecting)是否继续向后操作 - /// - public bool Continue { - get { return _Continue; } - set { _Continue = value; } - } - - public STNodeEditorOptionEventArgs(STNodeOption opTarget, STNodeOption opCurrent, ConnectionStatus cr) - : base(false, opTarget, cr) { - this._CurrentOption = opCurrent; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel; +using System.Drawing; + +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 + } + + public enum AlertLocation + { + Left, + Top, + Right, + Bottom, + Center, + LeftTop, + RightTop, + RightBottom, + LeftBottom, + } + + public struct DrawingTools + { + public Graphics Graphics; + public Pen Pen; + public SolidBrush SolidBrush; + } + + public enum CanvasMoveArgs //移动画布时需要的参数 查看->MoveCanvas() + { + Left = 1, //表示 仅移动 X 坐标 + Top = 2, //表示 仅移动 Y 坐标 + All = 4 //表示 X Y 同时移动 + } + + public struct NodeFindInfo + { + public STNode Node; + public STNodeOption NodeOption; + public string Mark; + public string[] MarkLines; + } + + public struct ConnectionInfo + { + public STNodeOption Input; + public STNodeOption Output; + } + + public delegate void STNodeOptionEventHandler(object sender, STNodeOptionEventArgs e); + + public class STNodeOptionEventArgs : EventArgs + { + private STNodeOption _TargetOption; + /// + /// 触发此事件的对应Option + /// + public STNodeOption TargetOption { + get { return _TargetOption; } + } + + private ConnectionStatus _Status; + /// + /// Option之间的连线状态 + /// + public ConnectionStatus Status { + get { return _Status; } + internal set { _Status = value; } + } + + private bool _IsSponsor; + /// + /// 是否为此次行为的发起者 + /// + public bool IsSponsor { + get { return _IsSponsor; } + } + + public STNodeOptionEventArgs(bool isSponsor, STNodeOption opTarget, ConnectionStatus cr) { + this._IsSponsor = isSponsor; + this._TargetOption = opTarget; + this._Status = cr; + } + } + + public delegate void STNodeEditorEventHandler(object sender, STNodeEditorEventArgs e); + public delegate void STNodeEditorOptionEventHandler(object sender, STNodeEditorOptionEventArgs e); + + + public class STNodeEditorEventArgs : EventArgs + { + private STNode _Node; + + public STNode Node { + get { return _Node; } + } + + public STNodeEditorEventArgs(STNode node) { + this._Node = node; + } + } + + public class STNodeEditorOptionEventArgs : STNodeOptionEventArgs + { + + private STNodeOption _CurrentOption; + /// + /// 主动触发事件的Option + /// + public STNodeOption CurrentOption { + get { return _CurrentOption; } + } + + private bool _Continue = true; + /// + /// 是否继续向下操作 用于Begin(Connecting/DisConnecting)是否继续向后操作 + /// + public bool Continue { + get { return _Continue; } + set { _Continue = value; } + } + + public STNodeEditorOptionEventArgs(STNodeOption opTarget, STNodeOption opCurrent, ConnectionStatus cr) + : base(false, opTarget, cr) { + this._CurrentOption = opCurrent; + } + } +} diff --git a/ST.Library.UI/NodeEditor/STNodeEditorPannel.cs b/ST.Library.UI/NodeEditor/STNodeEditorPannel.cs new file mode 100644 index 0000000..c4a1041 --- /dev/null +++ b/ST.Library.UI/NodeEditor/STNodeEditorPannel.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Windows.Forms; +using System.Drawing; +using System.Runtime.InteropServices; +using System.ComponentModel; + +namespace ST.Library.UI.NodeEditor +{ + public class STNodeEditorPannel : Control + { + private bool _LeftLayout = true; + /// + /// 获取或设置是否是左边布局 + /// + [Description("获取或设置是否是左边布局"), DefaultValue(true)] + public bool LeftLayout { + get { return _LeftLayout; } + set { + if (value == _LeftLayout) return; + _LeftLayout = value; + this.SetLocation(); + this.Invalidate(); + } + } + + private Color _SplitLineColor = Color.Black; + /// + /// 获取或这是分割线颜色 + /// + [Description("获取或这是分割线颜色"), DefaultValue(typeof(Color), "Black")] + public Color SplitLineColor { + get { return _SplitLineColor; } + set { _SplitLineColor = value; } + } + + private Color _HandleLineColor = Color.Gray; + /// + /// 获取或设置分割线手柄颜色 + /// + [Description("获取或设置分割线手柄颜色"), DefaultValue(typeof(Color), "Gray")] + public Color HandleLineColor { + get { return _HandleLineColor; } + set { _HandleLineColor = value; } + } + + private bool _ShowScale = true; + /// + /// 获取或设置编辑器缩放时候显示比例 + /// + [Description("获取或设置编辑器缩放时候显示比例"), DefaultValue(true)] + public bool ShowScale { + get { return _ShowScale; } + set { _ShowScale = value; } + } + + private bool _ShowConnectionStatus = true; + /// + /// 获取或设置节点连线时候是否显示状态 + /// + [Description("获取或设置节点连线时候是否显示状态"), DefaultValue(true)] + public bool ShowConnectionStatus { + get { return _ShowConnectionStatus; } + set { _ShowConnectionStatus = value; } + } + + private int _X; + /// + /// 获取或设置分割线水平宽度 + /// + [Description("获取或设置分割线水平宽度"), DefaultValue(201)] + public int X { + get { return _X; } + set { + if (value < 122) value = 122; + else if (value > this.Width - 122) value = this.Width - 122; + if (_X == value) return; + _X = value; + this.SetLocation(); + } + } + + private int _Y; + /// + /// 获取或设置分割线垂直高度 + /// + [Description("获取或设置分割线垂直高度")] + public int Y { + get { return _Y; } + set { + if (value < 122) value = 122; + else if (value > this.Height - 122) value = this.Height - 122; + if (_Y == value) return; + _Y = value; + this.SetLocation(); + } + } + /// + /// 获取面板中的STNodeEditor + /// + [Description("获取面板中的STNodeEditor"), Browsable(false)] + public STNodeEditor Editor { + get { return m_editor; } + } + /// + /// 获取面板中的STNodeTreeView + /// + [Description("获取面板中的STNodeTreeView"), Browsable(false)] + public STNodeTreeView TreeView { + get { return m_tree; } + } + /// + /// 获取面板中的STNodePropertyGrid + /// + [Description("获取面板中的STNodePropertyGrid"), Browsable(false)] + public STNodePropertyGrid PropertyGrid { + get { return m_grid; } + } + + private Point m_pt_down; + private bool m_is_mx; + private bool m_is_my; + private Pen m_pen; + + private bool m_nInited; + private Dictionary m_dic_status_key = new Dictionary(); + + private STNodeEditor m_editor; + private STNodeTreeView m_tree; + private STNodePropertyGrid m_grid; + + public override Size MinimumSize { + get { + return base.MinimumSize; + } + set { + value = new Size(250, 250); + base.MinimumSize = value; + } + } + + [DllImport("user32.dll")] + private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int w, int h, bool bRedraw); + + public STNodeEditorPannel() { + this.SetStyle(ControlStyles.UserPaint, true); + this.SetStyle(ControlStyles.ResizeRedraw, true); + this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); + this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + this.SetStyle(ControlStyles.SupportsTransparentBackColor, true); + + m_editor = new STNodeEditor(); + m_tree = new STNodeTreeView(); + m_grid = new STNodePropertyGrid(); + m_grid.Text = "NodeProperty"; + this.Controls.Add(m_editor); + this.Controls.Add(m_tree); + this.Controls.Add(m_grid); + this.Size = new Size(500, 500); + this.MinimumSize = new Size(250, 250); + this.BackColor = Color.FromArgb(255, 34, 34, 34); + + m_pen = new Pen(this.BackColor, 3); + + Type t = typeof(ConnectionStatus); + var vv = Enum.GetValues(t); + var vvv = vv.GetValue(0); + foreach (var f in t.GetFields()) { + if (!f.FieldType.IsEnum) continue; + foreach (var a in f.GetCustomAttributes(true)) { + if (!(a is DescriptionAttribute)) continue; + m_dic_status_key.Add((ConnectionStatus)f.GetValue(f), ((DescriptionAttribute)a).Description); + } + } + + m_editor.ActiveChanged += (s, e) => m_grid.SetNode(m_editor.ActiveNode); + m_editor.CanvasScaled += (s, e) => { + if (this._ShowScale) + m_editor.ShowAlert(m_editor.CanvasScale.ToString("F2"), Color.White, Color.FromArgb(127, 255, 255, 0)); + }; + m_editor.OptionConnected += (s, e) => { + if (this._ShowConnectionStatus) + m_editor.ShowAlert(m_dic_status_key[e.Status], Color.White, e.Status == ConnectionStatus.Connected ? Color.FromArgb(125, Color.Lime) : Color.FromArgb(125, Color.Red)); + }; + } + + protected override void OnResize(EventArgs e) { + base.OnResize(e); + if (!m_nInited) { + this._Y = this.Height / 2; + if (this._LeftLayout) + this._X = 201; + else + this._X = this.Width - 202; + m_nInited = true; + this.SetLocation(); + return; + } + this.SetLocation(); + } + + protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) { + if (width < 250) width = 250; + if (height < 250) height = 250; + base.SetBoundsCore(x, y, width, height, specified); + } + + protected override void OnPaint(PaintEventArgs e) { + base.OnPaint(e); + Graphics g = e.Graphics; + m_pen.Width = 3; + m_pen.Color = this._SplitLineColor; + g.DrawLine(m_pen, this._X, 0, this._X, this.Height); + int nX = 0; + if (this._LeftLayout) { + g.DrawLine(m_pen, 0, this._Y, this._X - 1, this._Y); + nX = this._X / 2; + } else { + g.DrawLine(m_pen, this._X + 2, this._Y, this.Width, this._Y); + nX = this._X + (this.Width - this._X) / 2; + } + m_pen.Width = 1; + this._HandleLineColor = Color.Gray; + m_pen.Color = this._HandleLineColor; + g.DrawLine(m_pen, this._X, this._Y - 10, this._X, this._Y + 10); + g.DrawLine(m_pen, nX - 10, this._Y, nX + 10, this._Y); + } + + private void SetLocation() { + if (this._LeftLayout) { + //m_tree.Location = Point.Empty; + //m_tree.Size = new Size(m_sx - 1, m_sy - 1); + STNodeEditorPannel.MoveWindow(m_tree.Handle, 0, 0, this._X - 1, this._Y - 1, false); + + //m_grid.Location = new Point(0, m_sy + 2); + //m_grid.Size = new Size(m_sx - 1, this.Height - m_sy - 2); + STNodeEditorPannel.MoveWindow(m_grid.Handle, 0, this._Y + 2, this._X - 1, this.Height - this._Y - 2, false); + + //m_editor.Location = new Point(m_sx + 2, 0); + //m_editor.Size = new Size(this.Width - m_sx - 2, this.Height); + STNodeEditorPannel.MoveWindow(m_editor.Handle, this._X + 2, 0, this.Width - this._X - 2, this.Height, false); + } else { + STNodeEditorPannel.MoveWindow(m_editor.Handle, 0, 0, this._X - 1, this.Height, false); + STNodeEditorPannel.MoveWindow(m_tree.Handle, this._X + 2, 0, this.Width - this._X - 2, this._Y - 1, false); + STNodeEditorPannel.MoveWindow(m_grid.Handle, this._X + 2, this._Y + 2, this.Width - this._X - 2, this.Height - this._Y - 2, false); + } + } + + protected override void OnMouseDown(MouseEventArgs e) { + base.OnMouseDown(e); + m_pt_down = e.Location; + m_is_mx = m_is_my = false; + if (this.Cursor == Cursors.VSplit) { + m_is_mx = true; + } else if (this.Cursor == Cursors.HSplit) { + m_is_my = true; + } + } + + protected override void OnMouseMove(MouseEventArgs e) { + base.OnMouseMove(e); + if (e.Button == MouseButtons.Left) { + int nw = 122;// (int)(this.Width * 0.1f); + int nh = 122;// (int)(this.Height * 0.1f); + if (m_is_mx) { + this._X = e.X;// -m_pt_down.X; + if (this._X < nw) this._X = nw; + else if (_X + nw > this.Width) this._X = this.Width - nw; + } else if (m_is_my) { + this._Y = e.Y; + if (this._Y < nh) this._Y = nh; + else if (this._Y + nh > this.Height) this._Y = this.Height - nh; + } + //m_rx = this.Width - m_sx;// (float)m_sx / this.Width; + //m_fh = (float)m_sy / this.Height; + this.SetLocation(); + this.Invalidate(); + return; + } + if (Math.Abs(e.X - this._X) < 2) + this.Cursor = Cursors.VSplit; + else if (Math.Abs(e.Y - this._Y) < 2) + this.Cursor = Cursors.HSplit; + else this.Cursor = Cursors.Arrow; + } + + protected override void OnMouseLeave(EventArgs e) { + base.OnMouseLeave(e); + m_is_mx = m_is_my = false; + this.Cursor = Cursors.Arrow; + } + /// + /// 向树控件中添加一个STNode + /// + /// STNode类型 + /// 是否添加成功 + public bool AddSTNode(Type stNodeType) { + return m_tree.AddNode(stNodeType); + } + /// + /// 从程序集中加载STNode + /// + /// 程序集路径 + /// 添加成功个数 + public int LoadAssembly(string strFileName) { + m_editor.LoadAssembly(strFileName); + return m_tree.LoadAssembly(strFileName); + } + /// + /// 设置编辑器显示连接状态的文本 + /// + /// 连接状态 + /// 对应显示文本 + /// 旧文本 + public string SetConnectionStatusText(ConnectionStatus status, string strText) { + string strOld = null; + if (m_dic_status_key.ContainsKey(status)) { + strOld = m_dic_status_key[status]; + m_dic_status_key[status] = strText; + return strOld; + } + m_dic_status_key.Add(status, strText); + return strText; + } + } +} diff --git a/ST.Library.UI/STNodeEditor/STNodeHub.cs b/ST.Library.UI/NodeEditor/STNodeHub.cs old mode 100755 new mode 100644 similarity index 85% rename from ST.Library.UI/STNodeEditor/STNodeHub.cs rename to ST.Library.UI/NodeEditor/STNodeHub.cs index 0ce6efe..32bb171 --- a/ST.Library.UI/STNodeEditor/STNodeHub.cs +++ b/ST.Library.UI/NodeEditor/STNodeHub.cs @@ -1,175 +1,192 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -/* -MIT License - -Copyright (c) 2021 DebugST@crystal_lz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - */ -/* - * time: 2021-01-06 - * Author: Crystal_lz - * blog: st233.com - * Github: DebugST.github.io - */ -namespace ST.Library.UI -{ - public class STNodeHub : STNode - { - private bool m_bSingle; - private string m_strIn; - private string m_strOut; - - public STNodeHub() : this(false, "IN", "OUT") { } - public STNodeHub(bool bSingle) : this(bSingle, "IN", "OUT") { } - public STNodeHub(bool bSingle, string strTextIn, string strTextOut) { - m_bSingle = bSingle; - m_strIn = strTextIn; - m_strOut = strTextOut; - this.Addhub(); - this.Title = "HUB"; - this.TitleColor = System.Drawing.Color.FromArgb(200, System.Drawing.Color.DarkOrange); - } - - private void Addhub() { - var input = new STNodeHubOption(m_strIn, typeof(object), m_bSingle); - var output = new STNodeHubOption(m_strOut, typeof(object), false); - this.InputOptions.Add(input); - this.OutputOptions.Add(output); - input.Connected += new STNodeOptionEventHandler(input_Connected); - input.DataTransfer += new STNodeOptionEventHandler(input_DataTransfer); - input.DisConnected += new STNodeOptionEventHandler(input_DisConnected); - output.Connected += new STNodeOptionEventHandler(output_Connected); - output.DisConnected += new STNodeOptionEventHandler(output_DisConnected); - } - - void output_DisConnected(object sender, STNodeOptionEventArgs e) { - STNodeOption op = sender as STNodeOption; - if (op.ConnectionCount != 0) return; - int nIndex = this.OutputOptions.IndexOf(op); - if (this.InputOptions[nIndex].ConnectionCount != 0) return; - this.InputOptions.RemoveAt(nIndex); - this.OutputOptions.RemoveAt(nIndex); - if (this.Owner != null) this.Owner.BuildLinePath(); - } - - void output_Connected(object sender, STNodeOptionEventArgs e) { - STNodeOption op = sender as STNodeOption; - int nIndex = this.OutputOptions.IndexOf(op); - var t = typeof(object); - if (this.InputOptions[nIndex].DataType == t) { - op.DataType = e.TargetOption.DataType; - this.InputOptions[nIndex].DataType = op.DataType; - foreach (STNodeOption v in this.InputOptions) { - if (v.DataType == t) return; - } - this.Addhub(); - } - } - - void input_DisConnected(object sender, STNodeOptionEventArgs e) { - STNodeOption op = sender as STNodeOption; - if (op.ConnectionCount != 0) return; - int nIndex = this.InputOptions.IndexOf(op); - if (this.OutputOptions[nIndex].ConnectionCount != 0) return; - this.InputOptions.RemoveAt(nIndex); - this.OutputOptions.RemoveAt(nIndex); - if (this.Owner != null) this.Owner.BuildLinePath(); - } - - void input_DataTransfer(object sender, STNodeOptionEventArgs e) { - STNodeOption op = sender as STNodeOption; - int nIndex = this.InputOptions.IndexOf(op); - if (e.Status != ConnectionStatus.Connected) - this.OutputOptions[nIndex].Data = null; - else - this.OutputOptions[nIndex].Data = e.TargetOption.Data; - this.OutputOptions[nIndex].TransferData(); - } - - void input_Connected(object sender, STNodeOptionEventArgs e) { - STNodeOption op = sender as STNodeOption; - int nIndex = this.InputOptions.IndexOf(op); - var t = typeof(object); - if (op.DataType == t) { - op.DataType = e.TargetOption.DataType; - this.OutputOptions[nIndex].DataType = op.DataType; - foreach (STNodeOption v in this.InputOptions) { - if (v.DataType == t) return; - } - this.Addhub(); - } else { - //this.OutputOptions[nIndex].Data = e.TargetOption.Data; - this.OutputOptions[nIndex].TransferData(e.TargetOption.Data); - } - } - - protected override void OnSaveNode(Dictionary dic) { - dic.Add("count", BitConverter.GetBytes(this.InputOptionsCount)); - dic.Add("single", new byte[] { (byte)(m_bSingle ? 1 : 0) }); - dic.Add("strin", Encoding.UTF8.GetBytes(m_strIn)); - dic.Add("strout", Encoding.UTF8.GetBytes(m_strOut)); - } - - protected internal override void OnLoadNode(Dictionary dic) { - base.OnLoadNode(dic); - int nCount = BitConverter.ToInt32(dic["count"], 0); - while (this.InputOptionsCount < nCount && this.InputOptionsCount != nCount) this.Addhub(); - m_bSingle = dic["single"][0] == 1; - m_strIn = Encoding.UTF8.GetString(dic["strin"]); - m_strOut = Encoding.UTF8.GetString(dic["strout"]); - } - - public class STNodeHubOption : STNodeOption - { - public STNodeHubOption(string strText, Type dataType, bool bSingle) : base(strText, dataType, bSingle) { } - - public override ConnectionStatus ConnectOption(STNodeOption op) { - var t = typeof(object); - if (this.DataType != t) return base.ConnectOption(op); - this.DataType = op.DataType; - var ret = base.ConnectOption(op); - if (ret != ConnectionStatus.Connected) this.DataType = t; - return ret; - } - - public override ConnectionStatus CanConnect(STNodeOption op) { - if (this.DataType != typeof(object)) return base.CanConnect(op); - if (this.IsInput == op.IsInput) return ConnectionStatus.SameInputOrOutput; - if (op.Owner == this.Owner) return ConnectionStatus.SameOwner; - if (op.Owner == null || this.Owner == null) return ConnectionStatus.NoOwner; - if (this.IsSingle && m_hs_connected.Count == 1) return ConnectionStatus.SingleOption; - if (op.IsInput && STNodeEditor.CanFindNodePath(op.Owner, this.Owner)) return ConnectionStatus.Loop; - if (m_hs_connected.Contains(op)) return ConnectionStatus.Exists; - if (op.DataType == typeof(object)) return ConnectionStatus.ErrorType; - - if (!this.IsInput) return ConnectionStatus.Connected; - foreach (STNodeOption owner_input in this.Owner.InputOptions) { - foreach (STNodeOption o in owner_input.ConnectedOption) { - if (o == op) return ConnectionStatus.Exists; - } - } - return ConnectionStatus.Connected; ; - } - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; +/* +MIT License + +Copyright (c) 2021 DebugST@crystal_lz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +/* + * create: 2021-12-08 + * modify: 2021-03-02 + * Author: Crystal_lz + * blog: http://st233.com + * Gitee: https://gitee.com/DebugST + * Github: https://github.com/DebugST + */ +namespace ST.Library.UI.NodeEditor +{ + public class STNodeHub : STNode + { + private bool m_bSingle; + private string m_strIn; + private string m_strOut; + + public STNodeHub() : this(false, "IN", "OUT") { } + public STNodeHub(bool bSingle) : this(bSingle, "IN", "OUT") { } + public STNodeHub(bool bSingle, string strTextIn, string strTextOut) { + m_bSingle = bSingle; + m_strIn = strTextIn; + m_strOut = strTextOut; + this.Addhub(); + this.Title = "HUB"; + this.AutoSize = false; + this.TitleColor = System.Drawing.Color.FromArgb(200, System.Drawing.Color.DarkOrange); + } + + protected override void OnOwnerChanged() { + base.OnOwnerChanged(); + if (this.Owner == null) return; + using (Graphics g = this.Owner.CreateGraphics()) { + this.Width = base.GetDefaultNodeSize(g).Width; + } + } + + private void Addhub() { + var input = new STNodeHubOption(m_strIn, typeof(object), m_bSingle); + var output = new STNodeHubOption(m_strOut, typeof(object), false); + this.InputOptions.Add(input); + this.OutputOptions.Add(output); + input.Connected += new STNodeOptionEventHandler(input_Connected); + input.DataTransfer += new STNodeOptionEventHandler(input_DataTransfer); + input.DisConnected += new STNodeOptionEventHandler(input_DisConnected); + output.Connected += new STNodeOptionEventHandler(output_Connected); + output.DisConnected += new STNodeOptionEventHandler(output_DisConnected); + this.Height = this.TitleHeight + this.InputOptions.Count * 20; + } + + void output_DisConnected(object sender, STNodeOptionEventArgs e) { + STNodeOption op = sender as STNodeOption; + if (op.ConnectionCount != 0) return; + int nIndex = this.OutputOptions.IndexOf(op); + if (this.InputOptions[nIndex].ConnectionCount != 0) return; + this.InputOptions.RemoveAt(nIndex); + this.OutputOptions.RemoveAt(nIndex); + if (this.Owner != null) this.Owner.BuildLinePath(); + this.Height -= 20; + } + + void output_Connected(object sender, STNodeOptionEventArgs e) { + STNodeOption op = sender as STNodeOption; + int nIndex = this.OutputOptions.IndexOf(op); + var t = typeof(object); + if (this.InputOptions[nIndex].DataType == t) { + op.DataType = e.TargetOption.DataType; + this.InputOptions[nIndex].DataType = op.DataType; + foreach (STNodeOption v in this.InputOptions) { + if (v.DataType == t) return; + } + this.Addhub(); + } + } + + void input_DisConnected(object sender, STNodeOptionEventArgs e) { + STNodeOption op = sender as STNodeOption; + if (op.ConnectionCount != 0) return; + int nIndex = this.InputOptions.IndexOf(op); + if (this.OutputOptions[nIndex].ConnectionCount != 0) return; + this.InputOptions.RemoveAt(nIndex); + this.OutputOptions.RemoveAt(nIndex); + if (this.Owner != null) this.Owner.BuildLinePath(); + this.Height -= 20; + } + + void input_DataTransfer(object sender, STNodeOptionEventArgs e) { + STNodeOption op = sender as STNodeOption; + int nIndex = this.InputOptions.IndexOf(op); + if (e.Status != ConnectionStatus.Connected) + this.OutputOptions[nIndex].Data = null; + else + this.OutputOptions[nIndex].Data = e.TargetOption.Data; + this.OutputOptions[nIndex].TransferData(); + } + + void input_Connected(object sender, STNodeOptionEventArgs e) { + STNodeOption op = sender as STNodeOption; + int nIndex = this.InputOptions.IndexOf(op); + var t = typeof(object); + if (op.DataType == t) { + op.DataType = e.TargetOption.DataType; + this.OutputOptions[nIndex].DataType = op.DataType; + foreach (STNodeOption v in this.InputOptions) { + if (v.DataType == t) return; + } + this.Addhub(); + } else { + //this.OutputOptions[nIndex].Data = e.TargetOption.Data; + this.OutputOptions[nIndex].TransferData(e.TargetOption.Data); + } + } + + protected override void OnSaveNode(Dictionary dic) { + dic.Add("count", BitConverter.GetBytes(this.InputOptionsCount)); + //dic.Add("single", new byte[] { (byte)(m_bSingle ? 1 : 0) }); + //dic.Add("strin", Encoding.UTF8.GetBytes(m_strIn)); + //dic.Add("strout", Encoding.UTF8.GetBytes(m_strOut)); + } + + protected internal override void OnLoadNode(Dictionary dic) { + base.OnLoadNode(dic); + int nCount = BitConverter.ToInt32(dic["count"], 0); + while (this.InputOptionsCount < nCount && this.InputOptionsCount != nCount) this.Addhub(); + //m_bSingle = dic["single"][0] == 1; + //m_strIn = Encoding.UTF8.GetString(dic["strin"]); + //m_strOut = Encoding.UTF8.GetString(dic["strout"]); + } + + public class STNodeHubOption : STNodeOption + { + public STNodeHubOption(string strText, Type dataType, bool bSingle) : base(strText, dataType, bSingle) { } + + public override ConnectionStatus ConnectOption(STNodeOption op) { + var t = typeof(object); + if (this.DataType != t) return base.ConnectOption(op); + this.DataType = op.DataType; + var ret = base.ConnectOption(op); + if (ret != ConnectionStatus.Connected) this.DataType = t; + return ret; + } + + public override ConnectionStatus CanConnect(STNodeOption op) { + if (op == STNodeOption.Empty) return ConnectionStatus.EmptyOption; + if (this.DataType != typeof(object)) return base.CanConnect(op); + if (this.IsInput == op.IsInput) return ConnectionStatus.SameInputOrOutput; + if (op.Owner == null || this.Owner == null) return ConnectionStatus.NoOwner; + if (op.Owner == this.Owner) return ConnectionStatus.SameOwner; + if (this.Owner.LockOption || op.Owner.LockOption) return ConnectionStatus.Locked; + if (this.IsSingle && m_hs_connected.Count == 1) return ConnectionStatus.SingleOption; + if (op.IsInput && STNodeEditor.CanFindNodePath(op.Owner, this.Owner)) return ConnectionStatus.Loop; + if (m_hs_connected.Contains(op)) return ConnectionStatus.Exists; + if (op.DataType == typeof(object)) return ConnectionStatus.ErrorType; + + if (!this.IsInput) return ConnectionStatus.Connected; + foreach (STNodeOption owner_input in this.Owner.InputOptions) { + foreach (STNodeOption o in owner_input.ConnectedOption) { + if (o == op) return ConnectionStatus.Exists; + } + } + return ConnectionStatus.Connected; ; + } + } + } +} diff --git a/ST.Library.UI/STNodeEditor/STNodeOption.cs b/ST.Library.UI/NodeEditor/STNodeOption.cs old mode 100755 new mode 100644 similarity index 90% rename from ST.Library.UI/STNodeEditor/STNodeOption.cs rename to ST.Library.UI/NodeEditor/STNodeOption.cs index db11c63..bf04cc9 --- a/ST.Library.UI/STNodeEditor/STNodeOption.cs +++ b/ST.Library.UI/NodeEditor/STNodeOption.cs @@ -1,430 +1,456 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using System.Drawing; - -namespace ST.Library.UI -{ - public class STNodeOption - { - #region Properties - - private STNode _Owner; - /// - /// 获取当前 Option 所属的 Node - /// - public STNode Owner { - get { return _Owner; } - internal set { - if (value == _Owner) return; - if (_Owner != null) this.DisConnectionAll(); //当所有者变更时 断开当前所有连接 - _Owner = value; - } - } - - private bool _IsSingle; - /// - /// 获取当前 Option 是否仅能被连接一次 - /// - public bool IsSingle { - get { return _IsSingle; } - } - - private bool _IsInput; - /// - /// 获取当前 Option 是否是输入选项 - /// - public bool IsInput { - get { return _IsInput; } - internal set { _IsInput = value; } - } - - private Color _TextColor = Color.White; - /// - /// 获取或设置当前 Option 文本颜色 - /// - public Color TextColor { - get { return _TextColor; } - protected set { - if (value == _TextColor) return; - _TextColor = value; - this.Invalidate(); - } - } - - private Color _DotColor = Color.Transparent; - /// - /// 获取或设置当前 Option 连接点的颜色 - /// - public Color DotColor { - get { return _DotColor; } - protected set { - if (value == _DotColor) return; - _DotColor = value; - this.Invalidate(); - } - } - - private string _Text; - /// - /// 获取或设置当前 Option 显示文本 - /// - public string Text { - get { return _Text; } - protected set { - if (value == _Text) return; - _Text = value; - this.Invalidate(); - } - } - - private int _DotLeft; - /// - /// 获取当前 Option 连接点的左边坐标 - /// - public int DotLeft { - get { return _DotLeft; } - internal set { _DotLeft = value; } - } - private int _DotTop; - /// - /// 获取当前 Option 连接点的上边坐标 - /// - public int DotTop { - get { return _DotTop; } - internal set { _DotTop = value; } - } - - private int _DotSize; - /// - /// 获取当前 Option 连接点的宽度 - /// - public int DotSize { - get { return _DotSize; } - protected set { _DotSize = value; } - } - - private Rectangle _TextRectangle; - /// - /// 获取当前 Option 文本区域 - /// - public Rectangle TextRectangle { - get { return _TextRectangle; } - internal set { _TextRectangle = value; } - } - - private object _Data; - /// - /// 获取或者设置当前 Option 所包含的数据 - /// - public object Data { - get { return _Data; } - set { - if (value != null) { - var t = value.GetType(); - if (t != this._DataType && !t.IsSubclassOf(this._DataType)) { - throw new ArgumentException("无效数据类型 数据类型必须为指定的数据类型或其子类"); - } - } - _Data = value; - } - } - - private Type _DataType; - /// - /// 获取当前 Option 数据类型 - /// - public Type DataType { - get { return _DataType; } - internal set { _DataType = value; } - } - - //private Rectangle _DotRectangle; - /// - /// 获取当前 Option 连接点的区域 - /// - public Rectangle DotRectangle { - get { - return new Rectangle(this._DotLeft, this._DotTop, this._DotSize, this._DotSize); - } - } - /// - /// 获取当前 Option 被连接的个数 - /// - public int ConnectionCount { - get { return m_hs_connected.Count; } - } - /// - /// 获取当前 Option 所连接的 Option 集合 - /// - internal HashSet ConnectedOption { - get { return m_hs_connected; } - } - - #endregion Properties - /// - /// 保存已经被连接的点 - /// - protected HashSet m_hs_connected; - - #region Constructor - - /// - /// 构造一个 Option - /// - /// 显示文本 - /// 数据类型 - /// 是否为单连接 - public STNodeOption(string strText, Type dataType, bool bSingle) { - if (dataType == null) throw new ArgumentNullException("指定的数据类型不能为空"); - this._DotSize = 10; - m_hs_connected = new HashSet(); - this._DataType = dataType; - this._Text = strText; - this._IsSingle = bSingle; - } - - #endregion Constructor - - #region Event - - /// - /// 当被连接时候发生 - /// - public event STNodeOptionEventHandler Connected; - /// - /// 当连接开始发生时发生 - /// - public event STNodeOptionEventHandler Connecting; - /// - /// 当连接断开时候发生 - /// - public event STNodeOptionEventHandler DisConnected; - /// - /// 当连接开始断开时发生 - /// - public event STNodeOptionEventHandler DisConnecting; - /// - /// 当有数据传递时候发生 - /// - public event STNodeOptionEventHandler DataTransfer; - - #endregion Event - - #region protected - /// - /// 重绘整个控件 - /// - protected void Invalidate() { - if (this._Owner == null) return; - this._Owner.Invalidate(); - } - /* - * 开始我认为应当只有输入类型的选项才具有事件 因为输入是被动的 而输出则是主动的 - * 但是后来发现 比如在STNodeHub中输出节点就用到了事件 - * 以防万一所以这里代码注释起来了 也并不是很有问题 输出选项不注册事件也是一样的效果 - */ - protected internal virtual void OnConnected(STNodeOptionEventArgs e) { - if (this.Connected != null/* && this._IsInput*/) 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._IsInput*/) 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._IsInput*/) this.DataTransfer(this, e); - } - protected void STNodeEidtorConnected(STNodeEditorOptionEventArgs e) { - if (this._Owner == null) return; - if (this._Owner.Owner == null) return; - this._Owner.Owner.OnOptionConnected(e); - } - protected void STNodeEidtorDisConnected(STNodeEditorOptionEventArgs e) { - if (this._Owner == null) return; - if (this._Owner.Owner == null) return; - this._Owner.Owner.OnOptionDisConnected(e); - } - /// - /// 当前 Option 开始连接目标 Option - /// - /// 需要连接的 Option - /// 是否允许继续操作 - protected virtual bool ConnectingOption(STNodeOption op) { - if (this._Owner == null) return false; - if (this._Owner.Owner == null) return false; - STNodeEditorOptionEventArgs e = new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Connecting); - this._Owner.Owner.OnOptionConnecting(e); - this.OnConnecting(new STNodeOptionEventArgs(true, op, ConnectionStatus.Connecting)); - op.OnConnecting(new STNodeOptionEventArgs(false, this, ConnectionStatus.Connecting)); - return e.Continue; - } - /// - /// 当前 Option 开始断开目标 Option - /// - /// 需要断开的 Option - /// 是否允许继续操作 - protected virtual bool DisConnectingOption(STNodeOption op) { - if (this._Owner == null) return false; - if (this._Owner.Owner == null) return false; - STNodeEditorOptionEventArgs e = new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.DisConnecting); - this._Owner.Owner.OnOptionDisConnecting(e); - this.OnDisConnecting(new STNodeOptionEventArgs(true, op, ConnectionStatus.DisConnecting)); - op.OnDisConnecting(new STNodeOptionEventArgs(false, this, ConnectionStatus.DisConnecting)); - return e.Continue; - } - - #endregion protected - - #region public - /// - /// 当前 Option 连接目标 Option - /// - /// 需要连接的 Option - /// 连接结果 - public virtual ConnectionStatus ConnectOption(STNodeOption op) { - if (!this.ConnectingOption(op)) { - this.STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Reject)); - return ConnectionStatus.Reject; - } - - var v = this.CanConnect(op); - if (v != ConnectionStatus.Connected) { - this.STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, v)); - return v; - } - v = op.CanConnect(this); - if (v != ConnectionStatus.Connected) { - this.STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, v)); - return v; - } - op.AddConnection(this, false); - this.AddConnection(op, true); - this.ControlBuildLinePath(); - - this.STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, v)); - return v; - } - /// - /// 检测当前 Option 是否可以连接目标 Option - /// - /// 需要连接的 Option - /// 检测结果 - public virtual ConnectionStatus CanConnect(STNodeOption op) { - if (this._IsInput == op.IsInput) return ConnectionStatus.SameInputOrOutput; - if (op.Owner == this._Owner) return ConnectionStatus.SameOwner; - if (op.Owner == null || this._Owner == null) return ConnectionStatus.NoOwner; - if (this._IsSingle && m_hs_connected.Count == 1) return ConnectionStatus.SingleOption; - if (op.IsInput && STNodeEditor.CanFindNodePath(op.Owner, this._Owner)) return ConnectionStatus.Loop; - if (m_hs_connected.Contains(op)) return ConnectionStatus.Exists; - if (this._IsInput && op.DataType != this._DataType && !op.DataType.IsSubclassOf(this._DataType)) return ConnectionStatus.ErrorType; - if (this._Owner.LockOption) return ConnectionStatus.Locked; - return ConnectionStatus.Connected; - } - /// - /// 当前 Option 断开目标 Option - /// - /// 需要断开的 Option - /// - public virtual ConnectionStatus DisConnectOption(STNodeOption op) { - if (!this.DisConnectingOption(op)) { - this.STNodeEidtorDisConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Reject)); - return ConnectionStatus.Reject; - } - - if (op.Owner == null) return ConnectionStatus.NoOwner; - if (this._Owner == null) return ConnectionStatus.NoOwner; - if (op.Owner.LockOption && this._Owner.LockOption) { - this.STNodeEidtorDisConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Locked)); - return ConnectionStatus.Locked; - } - op.RemoveConnection(this, false); - this.RemoveConnection(op, true); - this.ControlBuildLinePath(); - - this.STNodeEidtorDisConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.DisConnected)); - return ConnectionStatus.DisConnected; - } - /// - /// 断开当前 Option 的所有连接 - /// - public void DisConnectionAll() { - var arr = m_hs_connected.ToArray(); - foreach (var v in arr) { - this.DisConnectOption(v); - } - } - /// - /// 获取当前 Option 所连接的 Option 集合 - /// - /// 如果为null 则表示不存在所有者 否则返回集合 - public List GetConnectedOption() { - if (!this._IsInput) - return m_hs_connected.ToList(); - List lst = new List(); - if (this._Owner == null) return null; - if (this._Owner.Owner == null) return null; - foreach (var v in this._Owner.Owner.GetConnectionInfo()) { - if (v.Output == this) lst.Add(v.Input); - } - return lst; - } - /// - /// 向当前 Option 所连接的所有 Option 投递数据 - /// - public void TransferData() { - foreach (var v in m_hs_connected) { - v.OnDataTransfer(new STNodeOptionEventArgs(true, this, ConnectionStatus.Connected)); - } - } - /// - /// 向当前 Option 所连接的所有 Option 投递数据 - /// - /// 需要投递的数据 - public void TransferData(object data) { - this.Data = data; //不是this._Data - foreach (var v in m_hs_connected) { - v.OnDataTransfer(new STNodeOptionEventArgs(true, this, ConnectionStatus.Connected)); - } - } - - #endregion public - - #region internal - - private bool AddConnection(STNodeOption op, bool bSponsor) { - bool b = m_hs_connected.Add(op); - this.OnConnected(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.Connected)); - if (this._IsInput) this.OnDataTransfer(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.Connected)); - return b; - } - - private bool RemoveConnection(STNodeOption op, bool bSponsor) { - bool b = false; - if (m_hs_connected.Contains(op)) { - b = m_hs_connected.Remove(op); - if (this._IsInput) this.OnDataTransfer(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.DisConnected)); - this.OnDisConnected(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.Connected)); - } - return b; - } - - #endregion internal - - #region private - - private void ControlBuildLinePath() { - if (this.Owner == null) return; - if (this.Owner.Owner == null) return; - this.Owner.Owner.BuildLinePath(); - } - - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; + +namespace ST.Library.UI.NodeEditor +{ + public class STNodeOption + { + #region Properties + + public static readonly STNodeOption Empty = new STNodeOption(); + + private STNode _Owner; + /// + /// 获取当前 Option 所属的 Node + /// + public STNode Owner { + get { return _Owner; } + internal set { + if (value == _Owner) return; + if (_Owner != null) this.DisConnectionAll(); //当所有者变更时 断开当前所有连接 + _Owner = value; + } + } + + private bool _IsSingle; + /// + /// 获取当前 Option 是否仅能被连接一次 + /// + public bool IsSingle { + get { return _IsSingle; } + } + + private bool _IsInput; + /// + /// 获取当前 Option 是否是输入选项 + /// + public bool IsInput { + get { return _IsInput; } + internal set { _IsInput = value; } + } + + private Color _TextColor = Color.White; + /// + /// 获取或设置当前 Option 文本颜色 + /// + public Color TextColor { + get { return _TextColor; } + internal set { + if (value == _TextColor) return; + _TextColor = value; + this.Invalidate(); + } + } + + private Color _DotColor = Color.Transparent; + /// + /// 获取或设置当前 Option 连接点的颜色 + /// + public Color DotColor { + get { return _DotColor; } + internal set { + if (value == _DotColor) return; + _DotColor = value; + this.Invalidate(); + } + } + + private string _Text; + /// + /// 获取或设置当前 Option 显示文本 + /// 当AutoSize被设置时 无法修改此属性 + /// + public string Text { + get { return _Text; } + internal set { + if (value == _Text) return; + _Text = value; + if (this._Owner == null) return; + this._Owner.BuildSize(true, true, true); + } + } + + private int _DotLeft; + /// + /// 获取当前 Option 连接点的左边坐标 + /// + public int DotLeft { + get { return _DotLeft; } + internal set { _DotLeft = value; } + } + private int _DotTop; + /// + /// 获取当前 Option 连接点的上边坐标 + /// + public int DotTop { + get { return _DotTop; } + internal set { _DotTop = value; } + } + + private int _DotSize; + /// + /// 获取当前 Option 连接点的宽度 + /// + public int DotSize { + get { return _DotSize; } + protected set { _DotSize = value; } + } + + private Rectangle _TextRectangle; + /// + /// 获取当前 Option 文本区域 + /// + public Rectangle TextRectangle { + get { return _TextRectangle; } + internal set { _TextRectangle = value; } + } + + private object _Data; + /// + /// 获取或者设置当前 Option 所包含的数据 + /// + public object Data { + get { return _Data; } + set { + if (value != null) { + if (this._DataType == null) return; + var t = value.GetType(); + if (t != this._DataType && !t.IsSubclassOf(this._DataType)) { + throw new ArgumentException("无效数据类型 数据类型必须为指定的数据类型或其子类"); + } + } + _Data = value; + } + } + + private Type _DataType; + /// + /// 获取当前 Option 数据类型 + /// + public Type DataType { + get { return _DataType; } + internal set { _DataType = value; } + } + + //private Rectangle _DotRectangle; + /// + /// 获取当前 Option 连接点的区域 + /// + public Rectangle DotRectangle { + get { + return new Rectangle(this._DotLeft, this._DotTop, this._DotSize, this._DotSize); + } + } + /// + /// 获取当前 Option 被连接的个数 + /// + public int ConnectionCount { + get { return m_hs_connected.Count; } + } + /// + /// 获取当前 Option 所连接的 Option 集合 + /// + internal HashSet ConnectedOption { + get { return m_hs_connected; } + } + + #endregion Properties + /// + /// 保存已经被连接的点 + /// + protected HashSet m_hs_connected; + + #region Constructor + + private STNodeOption() { } + + /// + /// 构造一个 Option + /// + /// 显示文本 + /// 数据类型 + /// 是否为单连接 + public STNodeOption(string strText, Type dataType, bool bSingle) { + if (dataType == null) throw new ArgumentNullException("指定的数据类型不能为空"); + this._DotSize = 10; + m_hs_connected = new HashSet(); + this._DataType = dataType; + this._Text = strText; + this._IsSingle = bSingle; + } + + #endregion Constructor + + #region Event + + /// + /// 当被连接时候发生 + /// + public event STNodeOptionEventHandler Connected; + /// + /// 当连接开始发生时发生 + /// + public event STNodeOptionEventHandler Connecting; + /// + /// 当连接断开时候发生 + /// + public event STNodeOptionEventHandler DisConnected; + /// + /// 当连接开始断开时发生 + /// + public event STNodeOptionEventHandler DisConnecting; + /// + /// 当有数据传递时候发生 + /// + public event STNodeOptionEventHandler DataTransfer; + + #endregion Event + + #region protected + /// + /// 重绘整个控件 + /// + protected void Invalidate() { + if (this._Owner == null) return; + this._Owner.Invalidate(); + } + /* + * 开始我认为应当只有输入类型的选项才具有事件 因为输入是被动的 而输出则是主动的 + * 但是后来发现 比如在STNodeHub中输出节点就用到了事件 + * 以防万一所以这里代码注释起来了 也并不是很有问题 输出选项不注册事件也是一样的效果 + */ + protected internal virtual void OnConnected(STNodeOptionEventArgs e) { + if (this.Connected != null/* && this._IsInput*/) 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._IsInput*/) 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._IsInput*/) this.DataTransfer(this, e); + } + protected void STNodeEidtorConnected(STNodeEditorOptionEventArgs e) { + if (this._Owner == null) return; + if (this._Owner.Owner == null) return; + this._Owner.Owner.OnOptionConnected(e); + } + protected void STNodeEidtorDisConnected(STNodeEditorOptionEventArgs e) { + if (this._Owner == null) return; + if (this._Owner.Owner == null) return; + this._Owner.Owner.OnOptionDisConnected(e); + } + /// + /// 当前 Option 开始连接目标 Option + /// + /// 需要连接的 Option + /// 是否允许继续操作 + protected virtual bool ConnectingOption(STNodeOption op) { + if (this._Owner == null) return false; + if (this._Owner.Owner == null) return false; + STNodeEditorOptionEventArgs e = new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Connecting); + this._Owner.Owner.OnOptionConnecting(e); + this.OnConnecting(new STNodeOptionEventArgs(true, op, ConnectionStatus.Connecting)); + op.OnConnecting(new STNodeOptionEventArgs(false, this, ConnectionStatus.Connecting)); + return e.Continue; + } + /// + /// 当前 Option 开始断开目标 Option + /// + /// 需要断开的 Option + /// 是否允许继续操作 + protected virtual bool DisConnectingOption(STNodeOption op) { + if (this._Owner == null) return false; + if (this._Owner.Owner == null) return false; + STNodeEditorOptionEventArgs e = new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.DisConnecting); + this._Owner.Owner.OnOptionDisConnecting(e); + this.OnDisConnecting(new STNodeOptionEventArgs(true, op, ConnectionStatus.DisConnecting)); + op.OnDisConnecting(new STNodeOptionEventArgs(false, this, ConnectionStatus.DisConnecting)); + return e.Continue; + } + + #endregion protected + + #region public + /// + /// 当前 Option 连接目标 Option + /// + /// 需要连接的 Option + /// 连接结果 + public virtual ConnectionStatus ConnectOption(STNodeOption op) { + if (!this.ConnectingOption(op)) { + this.STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Reject)); + return ConnectionStatus.Reject; + } + + var v = this.CanConnect(op); + if (v != ConnectionStatus.Connected) { + this.STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, v)); + return v; + } + v = op.CanConnect(this); + if (v != ConnectionStatus.Connected) { + this.STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, v)); + return v; + } + op.AddConnection(this, false); + this.AddConnection(op, true); + this.ControlBuildLinePath(); + + this.STNodeEidtorConnected(new STNodeEditorOptionEventArgs(op, this, v)); + return v; + } + /// + /// 检测当前 Option 是否可以连接目标 Option + /// + /// 需要连接的 Option + /// 检测结果 + public virtual ConnectionStatus CanConnect(STNodeOption op) { + if (this == STNodeOption.Empty || op == STNodeOption.Empty) return ConnectionStatus.EmptyOption; + if (this._IsInput == op.IsInput) return ConnectionStatus.SameInputOrOutput; + if (op.Owner == null || this._Owner == null) return ConnectionStatus.NoOwner; + if (op.Owner == this._Owner) return ConnectionStatus.SameOwner; + if (this._Owner.LockOption || op._Owner.LockOption) return ConnectionStatus.Locked; + if (this._IsSingle && m_hs_connected.Count == 1) return ConnectionStatus.SingleOption; + if (op.IsInput && STNodeEditor.CanFindNodePath(op.Owner, this._Owner)) return ConnectionStatus.Loop; + if (m_hs_connected.Contains(op)) return ConnectionStatus.Exists; + if (this._IsInput && op._DataType != this._DataType && !op._DataType.IsSubclassOf(this._DataType)) return ConnectionStatus.ErrorType; + return ConnectionStatus.Connected; + } + /// + /// 当前 Option 断开目标 Option + /// + /// 需要断开的 Option + /// + public virtual ConnectionStatus DisConnectOption(STNodeOption op) { + if (!this.DisConnectingOption(op)) { + this.STNodeEidtorDisConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Reject)); + return ConnectionStatus.Reject; + } + + if (op.Owner == null) return ConnectionStatus.NoOwner; + if (this._Owner == null) return ConnectionStatus.NoOwner; + if (op.Owner.LockOption && this._Owner.LockOption) { + this.STNodeEidtorDisConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.Locked)); + return ConnectionStatus.Locked; + } + op.RemoveConnection(this, false); + this.RemoveConnection(op, true); + this.ControlBuildLinePath(); + + this.STNodeEidtorDisConnected(new STNodeEditorOptionEventArgs(op, this, ConnectionStatus.DisConnected)); + return ConnectionStatus.DisConnected; + } + /// + /// 断开当前 Option 的所有连接 + /// + public void DisConnectionAll() { + if (this._DataType == null) return; + var arr = m_hs_connected.ToArray(); + foreach (var v in arr) { + this.DisConnectOption(v); + } + } + /// + /// 获取当前 Option 所连接的 Option 集合 + /// + /// 如果为null 则表示不存在所有者 否则返回集合 + public List GetConnectedOption() { + if (this._DataType == null) return null; + if (!this._IsInput) + return m_hs_connected.ToList(); + List lst = new List(); + if (this._Owner == null) return null; + if (this._Owner.Owner == null) return null; + foreach (var v in this._Owner.Owner.GetConnectionInfo()) { + if (v.Output == this) lst.Add(v.Input); + } + return lst; + } + /// + /// 向当前 Option 所连接的所有 Option 投递数据 + /// + public void TransferData() { + if (this._DataType == null) return; + foreach (var v in m_hs_connected) { + v.OnDataTransfer(new STNodeOptionEventArgs(true, this, ConnectionStatus.Connected)); + } + } + /// + /// 向当前 Option 所连接的所有 Option 投递数据 + /// + /// 需要投递的数据 + public void TransferData(object data) { + if (this._DataType == null) return; + this.Data = data; //不是this._Data + foreach (var v in m_hs_connected) { + v.OnDataTransfer(new STNodeOptionEventArgs(true, this, ConnectionStatus.Connected)); + } + } + /// + /// 向当前 Option 所连接的所有 Option 投递数据 + /// + /// 需要投递的数据 + /// 是否释放旧数据 + public void TransferData(object data, bool bDisposeOld) { + if (bDisposeOld && this._Data != null) { + if (this._Data is IDisposable) ((IDisposable)this._Data).Dispose(); + this._Data = null; + } + this.TransferData(data); + } + + #endregion public + + #region internal + + private bool AddConnection(STNodeOption op, bool bSponsor) { + if (this._DataType == null) return false; + bool b = m_hs_connected.Add(op); + this.OnConnected(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.Connected)); + if (this._IsInput) this.OnDataTransfer(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.Connected)); + return b; + } + + private bool RemoveConnection(STNodeOption op, bool bSponsor) { + if (this._DataType == null) return false; + bool b = false; + if (m_hs_connected.Contains(op)) { + b = m_hs_connected.Remove(op); + if (this._IsInput) this.OnDataTransfer(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.DisConnected)); + this.OnDisConnected(new STNodeOptionEventArgs(bSponsor, op, ConnectionStatus.Connected)); + } + return b; + } + + #endregion internal + + #region private + + private void ControlBuildLinePath() { + if (this.Owner == null) return; + if (this.Owner.Owner == null) return; + this.Owner.Owner.BuildLinePath(); + } + + #endregion + } +} diff --git a/ST.Library.UI/STNodeEditor/STNodeOptionCollection.cs b/ST.Library.UI/NodeEditor/STNodeOptionCollection.cs old mode 100755 new mode 100644 similarity index 91% rename from ST.Library.UI/STNodeEditor/STNodeOptionCollection.cs rename to ST.Library.UI/NodeEditor/STNodeOptionCollection.cs index e0902f5..b074e55 --- a/ST.Library.UI/STNodeEditor/STNodeOptionCollection.cs +++ b/ST.Library.UI/NodeEditor/STNodeOptionCollection.cs @@ -1,231 +1,238 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using System.Collections; - -namespace ST.Library.UI -{ - public class STNodeOptionCollection : IList, ICollection, IEnumerable - { - /* - * 虽然该集合提供了完整的数据接口 如:Add,Remove,... - * 但是尽可能的不要使用移除的一些操作 如:Remove,RemoveAt,Clear,this[index] = value,... - * 因为在我的定义里面 每个Option的Owner是严格绑定的 一些移除或替换等操作会影响到Owner的变更 - * 所以原本的所有连线将会断开 并且触发DisConnect事件 - * 为了确保安全在STNode中 仅继承者才能够访问集合 - */ - private int _Count; - public int Count { get { return _Count; } } - private STNodeOption[] m_options; - private STNode m_owner; - - private bool m_isInput; //当前集合是否是存放的是输入点 - - internal STNodeOptionCollection(STNode owner, bool isInput) { - if (owner == null) throw new ArgumentNullException("所有者不能为空"); - m_owner = owner; - m_isInput = isInput; - m_options = new STNodeOption[4]; - } - - public int Add(STNodeOption option) { - if (option == null) throw new ArgumentNullException("添加对象不能为空"); - this.EnsureSpace(1); - int nIndex = this.IndexOf(option); - if (-1 == nIndex) { - nIndex = this._Count; - option.Owner = m_owner; - option.IsInput = m_isInput; - m_options[this._Count++] = option; - this.Redraw(); - } - return nIndex; - } - - public void AddRange(STNodeOption[] options) { - if (options == null) throw new ArgumentNullException("添加对象不能为空"); - this.EnsureSpace(options.Length); - foreach (var op in options) { - if (op == null) throw new ArgumentNullException("添加对象不能为空"); - if (-1 == this.IndexOf(op)) { - op.Owner = m_owner; - op.IsInput = m_isInput; - m_options[this._Count++] = op; - } - } - this.Redraw(); - } - - public void Clear() { - for (int i = 0; i < this._Count; i++) m_options[i].Owner = null; - this._Count = 0; - m_options = new STNodeOption[4]; - this.Redraw(); - } - - public bool Contains(STNodeOption option) { - return this.IndexOf(option) != -1; - } - - public int IndexOf(STNodeOption option) { - return Array.IndexOf(m_options, option); - } - - public void Insert(int index, STNodeOption option) { - if (index < 0 || index >= this._Count) - throw new IndexOutOfRangeException("索引越界"); - if (option == null) - throw new ArgumentNullException("插入对象不能为空"); - this.EnsureSpace(1); - for (int i = this._Count; i > index; i--) - m_options[i] = m_options[i - 1]; - option.Owner = m_owner; - m_options[index] = option; - this._Count++; - this.Redraw(); - } - - public bool IsFixedSize { - get { return false; } - } - - public bool IsReadOnly { - get { return false; } - } - - public void Remove(STNodeOption option) { - int nIndex = this.IndexOf(option); - if (nIndex != -1) this.RemoveAt(nIndex); - } - - public void RemoveAt(int index) { - if (index < 0 || index >= this._Count) - throw new IndexOutOfRangeException("索引越界"); - this._Count--; - m_options[index].Owner = null; - for (int i = index, Len = this._Count; i < Len; i++) - m_options[i] = m_options[i + 1]; - this.Redraw(); - } - - public STNodeOption this[int index] { - get { - if (index < 0 || index >= this._Count) - throw new IndexOutOfRangeException("索引越界"); - return m_options[index]; - } - set { throw new InvalidOperationException("禁止重新赋值元素"); } - } - - public void CopyTo(Array array, int index) { - if (array == null) - throw new ArgumentNullException("数组不能为空"); - m_options.CopyTo(array, index); - } - - public bool IsSynchronized { - get { return true; } - } - - public object SyncRoot { - get { return this; } - } - - public IEnumerator GetEnumerator() { - for (int i = 0, Len = this._Count; i < Len; i++) - yield return m_options[i]; - } - /// - /// 确认空间是否足够 空间不足扩大容量 - /// - /// 需要增加的个数 - private void EnsureSpace(int elements) { - if (elements + this._Count > m_options.Length) { - STNodeOption[] arrTemp = new STNodeOption[Math.Max(m_options.Length * 2, elements + this._Count)]; - m_options.CopyTo(arrTemp, 0); - m_options = arrTemp; - } - } - - protected void Redraw() { - if (m_owner != null && m_owner.Owner != null) { - m_owner.BuildSize(true, true, true); - //m_owner.Invalidate();//.Owner.Invalidate(); - } - } - //=================================================================================== - int IList.Add(object value) { - return this.Add((STNodeOption)value); - } - - void IList.Clear() { - this.Clear(); - } - - bool IList.Contains(object value) { - return this.Contains((STNodeOption)value); - } - - int IList.IndexOf(object value) { - return this.IndexOf((STNodeOption)value); - } - - void IList.Insert(int index, object value) { - this.Insert(index, (STNodeOption)value); - } - - bool IList.IsFixedSize { - get { return this.IsFixedSize; } - } - - bool IList.IsReadOnly { - get { return this.IsReadOnly; } - } - - void IList.Remove(object value) { - this.Remove((STNodeOption)value); - } - - void IList.RemoveAt(int index) { - this.RemoveAt(index); - } - - object IList.this[int index] { - get { - return this[index]; - } - set { - this[index] = (STNodeOption)value; - } - } - - void ICollection.CopyTo(Array array, int index) { - this.CopyTo(array, index); - } - - int ICollection.Count { - get { return this._Count; } - } - - bool ICollection.IsSynchronized { - get { return this.IsSynchronized; } - } - - object ICollection.SyncRoot { - get { return this.SyncRoot; } - } - - IEnumerator IEnumerable.GetEnumerator() { - return this.GetEnumerator(); - } - - public STNodeOption[] ToArray() { - STNodeOption[] ops = new STNodeOption[this._Count]; - for (int i = 0; i < ops.Length; i++) - ops[i] = m_options[i]; - return ops; - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Collections; + +namespace ST.Library.UI.NodeEditor +{ + public class STNodeOptionCollection : IList, ICollection, IEnumerable + { + /* + * 虽然该集合提供了完整的数据接口 如:Add,Remove,... + * 但是尽可能的不要使用移除的一些操作 如:Remove,RemoveAt,Clear,this[index] = value,... + * 因为在我的定义里面 每个Option的Owner是严格绑定的 一些移除或替换等操作会影响到Owner的变更 + * 所以原本的所有连线将会断开 并且触发DisConnect事件 + * 为了确保安全在STNode中 仅继承者才能够访问集合 + */ + private int _Count; + public int Count { get { return _Count; } } + private STNodeOption[] m_options; + private STNode m_owner; + + private bool m_isInput; //当前集合是否是存放的是输入点 + + 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) { + //not do this code -> out of bounds + //return m_options[this.Add(new STNodeOption(strText, dataType, bSingle))]; + int nIndex = this.Add(new STNodeOption(strText, dataType, bSingle)); + return m_options[nIndex]; + } + + public int Add(STNodeOption option) { + if (option == null) throw new ArgumentNullException("添加对象不能为空"); + this.EnsureSpace(1); + int nIndex = option == STNodeOption.Empty ? -1 : this.IndexOf(option); + if (-1 == nIndex) { + nIndex = this._Count; + option.Owner = m_owner; + option.IsInput = m_isInput; + m_options[this._Count++] = option; + this.Invalidate(); + } + return nIndex; + } + + public void AddRange(STNodeOption[] options) { + if (options == null) throw new ArgumentNullException("添加对象不能为空"); + this.EnsureSpace(options.Length); + foreach (var op in options) { + if (op == null) throw new ArgumentNullException("添加对象不能为空"); + if (-1 == this.IndexOf(op)) { + op.Owner = m_owner; + op.IsInput = m_isInput; + m_options[this._Count++] = op; + } + } + this.Invalidate(); + } + + public void Clear() { + for (int i = 0; i < this._Count; i++) m_options[i].Owner = null; + this._Count = 0; + m_options = new STNodeOption[4]; + this.Invalidate(); + } + + public bool Contains(STNodeOption option) { + return this.IndexOf(option) != -1; + } + + public int IndexOf(STNodeOption option) { + return Array.IndexOf(m_options, option); + } + + public void Insert(int index, STNodeOption option) { + if (index < 0 || index >= this._Count) + throw new IndexOutOfRangeException("索引越界"); + if (option == null) + throw new ArgumentNullException("插入对象不能为空"); + this.EnsureSpace(1); + for (int i = this._Count; i > index; i--) + m_options[i] = m_options[i - 1]; + option.Owner = m_owner; + m_options[index] = option; + this._Count++; + this.Invalidate(); + } + + public bool IsFixedSize { + get { return false; } + } + + public bool IsReadOnly { + get { return false; } + } + + public void Remove(STNodeOption option) { + int nIndex = this.IndexOf(option); + if (nIndex != -1) this.RemoveAt(nIndex); + } + + public void RemoveAt(int index) { + if (index < 0 || index >= this._Count) + throw new IndexOutOfRangeException("索引越界"); + this._Count--; + m_options[index].Owner = null; + for (int i = index, Len = this._Count; i < Len; i++) + m_options[i] = m_options[i + 1]; + this.Invalidate(); + } + + public STNodeOption this[int index] { + get { + if (index < 0 || index >= this._Count) + throw new IndexOutOfRangeException("索引越界"); + return m_options[index]; + } + set { throw new InvalidOperationException("禁止重新赋值元素"); } + } + + public void CopyTo(Array array, int index) { + if (array == null) + throw new ArgumentNullException("数组不能为空"); + m_options.CopyTo(array, index); + } + + public bool IsSynchronized { + get { return true; } + } + + public object SyncRoot { + get { return this; } + } + + public IEnumerator GetEnumerator() { + for (int i = 0, Len = this._Count; i < Len; i++) + yield return m_options[i]; + } + /// + /// 确认空间是否足够 空间不足扩大容量 + /// + /// 需要增加的个数 + private void EnsureSpace(int elements) { + if (elements + this._Count > m_options.Length) { + STNodeOption[] arrTemp = new STNodeOption[Math.Max(m_options.Length * 2, elements + this._Count)]; + m_options.CopyTo(arrTemp, 0); + m_options = arrTemp; + } + } + + protected void Invalidate() { + if (m_owner != null && m_owner.Owner != null) { + m_owner.BuildSize(true, true, true); + //m_owner.Invalidate();//.Owner.Invalidate(); + } + } + //=================================================================================== + int IList.Add(object value) { + return this.Add((STNodeOption)value); + } + + void IList.Clear() { + this.Clear(); + } + + bool IList.Contains(object value) { + return this.Contains((STNodeOption)value); + } + + int IList.IndexOf(object value) { + return this.IndexOf((STNodeOption)value); + } + + void IList.Insert(int index, object value) { + this.Insert(index, (STNodeOption)value); + } + + bool IList.IsFixedSize { + get { return this.IsFixedSize; } + } + + bool IList.IsReadOnly { + get { return this.IsReadOnly; } + } + + void IList.Remove(object value) { + this.Remove((STNodeOption)value); + } + + void IList.RemoveAt(int index) { + this.RemoveAt(index); + } + + object IList.this[int index] { + get { + return this[index]; + } + set { + this[index] = (STNodeOption)value; + } + } + + void ICollection.CopyTo(Array array, int index) { + this.CopyTo(array, index); + } + + int ICollection.Count { + get { return this._Count; } + } + + bool ICollection.IsSynchronized { + get { return this.IsSynchronized; } + } + + object ICollection.SyncRoot { + get { return this.SyncRoot; } + } + + IEnumerator IEnumerable.GetEnumerator() { + return this.GetEnumerator(); + } + + public STNodeOption[] ToArray() { + STNodeOption[] ops = new STNodeOption[this._Count]; + for (int i = 0; i < ops.Length; i++) + ops[i] = m_options[i]; + return ops; + } + } } \ No newline at end of file diff --git a/ST.Library.UI/NodeEditor/STNodePropertyAttribute.cs b/ST.Library.UI/NodeEditor/STNodePropertyAttribute.cs new file mode 100644 index 0000000..7151bfd --- /dev/null +++ b/ST.Library.UI/NodeEditor/STNodePropertyAttribute.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; + +namespace ST.Library.UI.NodeEditor +{ + /// + /// STNode节点属性特性 + /// 用于描述STNode节点属性信息 以及在属性编辑器上的行为 + /// + public class STNodePropertyAttribute : Attribute + { + private string _Name; + /// + /// 获取属性需要在属性编辑器上显示的名称 + /// + public string Name { + get { return _Name; } + } + + private string _Description; + /// + /// 获取属性需要在属性编辑器上显示的描述 + /// + public string Description { + get { return _Description; } + } + + private Type _ConverterType = typeof(STNodePropertyDescriptor); + /// + /// 获取属性描述器类型 + /// + public Type DescriptorType { + get { return _ConverterType; } + set { _ConverterType = value; } + } + + /// + /// 构造一个STNode属性特性 + /// + /// 需要显示的名称 + /// 需要显示的描述信息 + public STNodePropertyAttribute(string strKey, string strDesc) { + this._Name = strKey; + this._Description = strDesc; + } + //private Type m_descriptor_type_base = typeof(STNodePropertyDescriptor); + } + /// + /// STNode属性描述器 + /// 用于确定在属性编辑器上如何与属性的值进行交互 以及确定属性值在属性编辑器上将如何绘制并交互 + /// + public class STNodePropertyDescriptor + { + /// + /// 获取目标节点 + /// + 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; } + + 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 STNodePropertyDescriptor() { + m_sf = new StringFormat(); + m_sf.LineAlignment = StringAlignment.Center; + m_sf.FormatFlags = StringFormatFlags.NoWrap; + } + + /// + /// 当确定STNode属性在属性编辑器上的位置时候发生 + /// + protected internal virtual void OnSetItemLocation() { } + /// + /// 将字符串形式的属性值转换为属性目标类型的值 + /// 默认只支持 int float double string bool 以及上述类型的Array + /// 若目标类型不在上述中 请重写此函数自行转换 + /// + /// 字符串形式的属性值 + /// 属性真实目标类型的值 + protected internal virtual object GetValueFromString(string strText) { + Type t = this.PropertyInfo.PropertyType; + if (t == m_t_int) return int.Parse(strText); + if (t == m_t_float) return float.Parse(strText); + if (t == m_t_double) return double.Parse(strText); + if (t == m_t_string) return strText; + if (t == m_t_bool) return bool.Parse(strText); + if (t.IsEnum) { + return Enum.Parse(t, strText); + } else if (t.IsArray) { + var t_1 = t.GetElementType(); + if (t_1 == m_t_string) return strText.Split(','); + string[] strs = strText.Trim(new char[] { ' ', ',' }).Split(',');//add other place trim() + if (t_1 == m_t_int) { + int[] arr = new int[strs.Length]; + for (int i = 0; i < strs.Length; i++) arr[i] = int.Parse(strs[i].Trim()); + return arr; + } + if (t_1 == m_t_float) { + float[] arr = new float[strs.Length]; + for (int i = 0; i < strs.Length; i++) arr[i] = float.Parse(strs[i].Trim()); + return arr; + } + if (t_1 == m_t_int) { + double[] arr = new double[strs.Length]; + for (int i = 0; i < strs.Length; i++) arr[i] = double.Parse(strs[i].Trim()); + return arr; + } + if (t_1 == m_t_int) { + bool[] arr = new bool[strs.Length]; + for (int i = 0; i < strs.Length; i++) arr[i] = bool.Parse(strs[i].Trim()); + return arr; + } + } + throw new InvalidCastException("无法完成[string]到[" + t.FullName + "]的转换 请重载[STNodePropertyDescriptor.GetValueFromString(string)]"); + } + /// + /// 将属性目标类型的值转换为字符串形式的值 + /// 默认对类型值进行 ToString() 操作 + /// 如需特殊处理 请重写此函数自行转换 + /// + /// 属性值的字符串形式 + protected internal virtual string GetStringFromValue() { + var v = this.PropertyInfo.GetValue(this.Node, null); + var t = this.PropertyInfo.PropertyType; + if (v == null) return null; + if (t.IsArray) { + List lst = new List(); + foreach (var item in (Array)v) lst.Add(item.ToString()); + return string.Join(",", lst.ToArray()); + } + return v.ToString(); + } + /// + /// 将二进制形式的属性值转换为属性目标类型的值 用于从文件存储中的数据还原属性值 + /// 默认将其转换为字符串然后调用 GetValueFromString(string) + /// 此函数与 GetBytesFromValue() 相对应 若需要重写函数应当两个函数一起重写 + /// + /// 二进制数据 + /// 属性真实目标类型的值 + protected internal virtual object GetValueFromBytes(byte[] byData) { + if (byData == null) return null; + string strText = Encoding.UTF8.GetString(byData); + return this.GetValueFromString(strText); + } + /// + /// 将属性目标类型的值转换为二进制形式的值 用于文件存储时候调用 + /// 默认调用 GetStringFromValue() 然后将字符串转换为二进制数据 + /// 如需特殊处理 请重写此函数自行转换 并且重写 GetValueFromBytes() + /// + /// 属性值的二进制形式 + protected internal virtual byte[] GetBytesFromValue() { + string strText = this.GetStringFromValue(); + if (strText == null) return null; + return Encoding.UTF8.GetBytes(strText); + } + /// + /// 此函数对应 System.Reflection.PropertyInfo.GetValue() + /// + /// 索引属性的可选索引值 对于非索引属性 此值应为null + /// 属性值 + protected internal virtual object GetValue(object[] index) { + return this.PropertyInfo.GetValue(this.Node, index); + } + /// + /// 此函数对应 System.Reflection.PropertyInfo.SetValue() + /// + /// 需要设置的属性值 + protected internal virtual void SetValue(object value) { + this.PropertyInfo.SetValue(this.Node, value, null); + } + /// + /// 此函数对应 System.Reflection.PropertyInfo.SetValue() + /// 在调用之前会默认进行 GetValueFromString(strValue) 处理 + /// + /// 需要设置的属性字符串形式的值 + protected internal virtual void SetValue(string strValue) { + this.PropertyInfo.SetValue(this.Node, this.GetValueFromString(strValue), null); + } + /// + /// 此函数对应 System.Reflection.PropertyInfo.SetValue() + /// 在调用之前会默认进行 GetValueFromBytes(byte[]) 处理 + /// + /// 需要设置的属性二进制数据 + protected internal virtual void SetValue(byte[] byData) { + this.PropertyInfo.SetValue(this.Node, this.GetValueFromBytes(byData), null); + } + /// + /// 此函数对应 System.Reflection.PropertyInfo.SetValue() + /// + /// 需要设置的属性值 + /// 索引属性的可选索引值 对于非索引属性 此值应为null + protected internal virtual void SetValue(object value, object[] index) { + this.PropertyInfo.SetValue(this.Node, value, index); + } + /// + /// 此函数对应 System.Reflection.PropertyInfo.SetValue() + /// 在调用之前会默认进行 GetValueFromString(strValue) 处理 + /// + /// 需要设置的属性字符串形式的值 + /// 索引属性的可选索引值 对于非索引属性 此值应为null + protected internal virtual void SetValue(string strValue, object[] index) { + this.PropertyInfo.SetValue(this.Node, this.GetValueFromString(strValue), index); + } + /// + /// 此函数对应 System.Reflection.PropertyInfo.SetValue() + /// 在调用之前会默认进行 GetValueFromBytes(byte[]) 处理 + /// + /// 需要设置的属性二进制数据 + /// 索引属性的可选索引值 对于非索引属性 此值应为null + protected internal virtual void SetValue(byte[] byData, object[] index) { + this.PropertyInfo.SetValue(this.Node, this.GetValueFromBytes(byData), index); + } + /// + /// 当设置属性值发生错误时候发生 + /// + /// 异常信息 + protected internal virtual void OnSetValueError(Exception ex) { + this.Control.SetErrorMessage(ex.Message); + } + /// + /// 当绘制属性在属性编辑器上的值所在区域时候发生 + /// + /// 绘制工具 + protected internal virtual void OnDrawValueRectangle(DrawingTools dt) { + Graphics g = dt.Graphics; + SolidBrush brush = dt.SolidBrush; + STNodePropertyGrid ctrl = this.Control; + //STNodePropertyItem item = this._PropertyItem; + brush.Color = ctrl.ItemValueBackColor; + + g.FillRectangle(brush, this.RectangleR); + Rectangle rect = this.RectangleR; + rect.Width--; rect.Height--; + brush.Color = this.Control.ForeColor; + g.DrawString(this.GetStringFromValue(), ctrl.Font, brush, this.RectangleR, m_sf); + + if (this.PropertyInfo.PropertyType.IsEnum || this.PropertyInfo.PropertyType == m_t_bool) { + g.FillPolygon(Brushes.Gray, new Point[]{ + new Point(rect.Right - 13, rect.Top + rect.Height / 2 - 2), + new Point(rect.Right - 4, rect.Top + rect.Height / 2 - 2), + new Point(rect.Right - 9, rect.Top + rect.Height / 2 + 3) + }); + } + } + /// + /// 当鼠标进入属性值所在区域时候发生 + /// + /// 事件参数 + protected internal virtual void OnMouseEnter(EventArgs e) { } + /// + /// 当鼠标在属性值所在区域点击时候发生 + /// + /// 事件参数 + protected internal virtual void OnMouseDown(MouseEventArgs e) { + } + /// + /// 当鼠标在属性值所在区域移动时候发生 + /// + /// 事件参数 + protected internal virtual void OnMouseMove(MouseEventArgs e) { } + /// + /// 当鼠标在属性值所在区域抬起时候发生 + /// + /// 事件参数 + protected internal virtual void OnMouseUp(MouseEventArgs e) { } + /// + /// 当鼠标在属性值所在区域离开时候发生 + /// + /// 事件参数 + protected internal virtual void OnMouseLeave(EventArgs e) { } + /// + /// 当鼠标在属性值所在区域点击时候发生 + /// + /// 事件参数 + protected internal virtual void OnMouseClick(MouseEventArgs e) { + Type t = this.PropertyInfo.PropertyType; + if (t == m_t_bool || t.IsEnum) { + new FrmSTNodePropertySelect(this).Show(this.Control); + return; + } + Rectangle rect = this.Control.RectangleToScreen(this.RectangleR); + new FrmSTNodePropertyInput(this).Show(this.Control); + } + /// + /// 重绘选项区域 + /// + public void Invalidate() { + Rectangle rect = this.Rectangle; + rect.X -= this.Control.ScrollOffset; + this.Control.Invalidate(rect); + } + } +} diff --git a/ST.Library.UI/NodeEditor/STNodePropertyGrid.cs b/ST.Library.UI/NodeEditor/STNodePropertyGrid.cs new file mode 100644 index 0000000..9b07e37 --- /dev/null +++ b/ST.Library.UI/NodeEditor/STNodePropertyGrid.cs @@ -0,0 +1,860 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; +using System.ComponentModel; +/* +MIT License + +Copyright (c) 2021 DebugST@crystal_lz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +/* + * create: 2021-01-28 + * modify: 2021-03-02 + * Author: Crystal_lz + * blog: http://st233.com + * Gitee: https://gitee.com/DebugST + * Github: https://github.com/DebugST + */ +namespace ST.Library.UI.NodeEditor +{ + /// + /// STNode节点属性编辑器 + /// + public class STNodePropertyGrid : Control + { + #region properties ========== + + private STNode _STNode; + /// + /// 当前显示的STNode + /// + [Description("当前显示的STNode"), Browsable(false)] + public STNode STNode { + get { return _STNode; } + } + + private Color _ItemHoverColor = Color.FromArgb(50, 125, 125, 125); + /// + /// 获取或设置属性选项被鼠标悬停时候背景色 + /// + [Description("获取或设置属性选项被鼠标悬停时候背景色")] + public Color ItemHoverColor { + get { return _ItemHoverColor; } + set { _ItemHoverColor = value; } + } + + private Color _ItemSelectedColor = Color.DodgerBlue; + /// + /// 获取或设置属性选项被选中时候背景色 当AutoColor被设置时此属性不能被设置 + /// + [Description("获取或设置属性选项被选中时候背景色 当AutoColor被设置时此属性不能被设置"), DefaultValue(typeof(Color), "DodgerBlue")] + public Color ItemSelectedColor { + get { return _ItemSelectedColor; } + set { + if (this._AutoColor) return; + if (value == _ItemSelectedColor) return; + _ItemSelectedColor = value; + this.Invalidate(); + } + } + + private Color _ItemValueBackColor = Color.FromArgb(255, 80, 80, 80); + /// + /// 获取或设置属性选项值背景色 + /// + [Description("获取或设置属性选项值背景色")] + public Color ItemValueBackColor { + get { return _ItemValueBackColor; } + set { + _ItemValueBackColor = value; + this.Invalidate(); + } + } + + private Color _TitleColor = Color.FromArgb(127, 0, 0, 0); + /// + /// 获取或设置默认标题背景色 + /// + [Description("获取或设置默认标题背景色")] + public Color TitleColor { + get { return _TitleColor; } + set { + _TitleColor = value; + if (!this._ShowTitle) return; + this.Invalidate(m_rect_title); + } + } + + private Color _ErrorColor = Color.FromArgb(200, Color.Brown); + /// + /// 获取或设置属性设置错误时候提示信息背景色 + /// + [Description("获取或设置属性设置错误时候提示信息背景色")] + public Color ErrorColor { + get { return _ErrorColor; } + set { _ErrorColor = value; } + } + + private Color _DescriptionColor = Color.FromArgb(200, Color.DarkGoldenrod); + /// + /// 获取或设置属性描述信息背景色 + /// + [Description("获取或设置属性描述信息背景色")] + public Color DescriptionColor { + get { return _DescriptionColor; } + set { _DescriptionColor = value; } + } + + private bool _ShowTitle = true; + /// + /// 获取或设置是否显示节点标题 + /// + [Description("获取或设置是否显示节点标题")] + public bool ShowTitle { + get { return _ShowTitle; } + set { + _ShowTitle = value; + this.SetItemRectangle(); + this.Invalidate(); + } + } + + private bool _AutoColor = true; + /// + /// 获取或设置是否根据STNode自动设置控件高亮颜色 + /// + [Description("获取或设置是否根据STNode自动设置控件高亮颜色"), DefaultValue(true)] + public bool AutoColor { + get { return _AutoColor; } + set { _AutoColor = value; } + } + + private bool _InfoFirstOnDraw; + /// + /// 获取或当节点被设置时候 是否优先绘制信息面板 + /// + [Description("获取或设置当节点被设置时候 是否优先绘制信息面板"), DefaultValue(false)] + public bool InfoFirstOnDraw { + get { return _InfoFirstOnDraw; } + set { _InfoFirstOnDraw = value; } + } + + private bool _ReadOnlyModel; + /// + /// 获取或设置当前属性编辑器是否处于只读模式 + /// + [Description("获取或设置当前属性编辑器是否处于只读模式"), DefaultValue(false)] + public bool ReadOnlyModel { + get { return _ReadOnlyModel; } + set { + if (value == _ReadOnlyModel) return; + _ReadOnlyModel = value; + this.Invalidate(m_rect_title); + } + } + /// + /// 获取当前滚动条高度 + /// + [Description("获取当前滚动条高度")] + public int ScrollOffset { get { return m_nOffsetY; } } + + #endregion + + #region protected fields ========== + + /// + /// 作者链接地址区域 + /// + protected Rectangle m_rect_link; + /// + /// 查看帮助按钮区域 + /// + protected Rectangle m_rect_help; + /// + /// 编辑器标题区域 + /// + protected Rectangle m_rect_title; + /// + /// 面板切换按钮区域 + /// + protected Rectangle m_rect_switch; + + /// + /// 控件在绘制过程中使用的垂直滚动偏移 + /// + protected int m_nOffsetY; + /// + /// 保存的信息面板垂直滚动偏移 + /// + protected int m_nInfoOffsetY; + /// + /// 保存的属性面板垂直滚动偏移 + /// + protected int m_nPropertyOffsetY; + + /// + /// 控件在绘制过程中使用的绘图区域总高度 + /// + protected int m_nVHeight; + /// + /// 保存的信息面板需要的总高度 + /// + protected int m_nInfoVHeight; + /// + /// 保存的属性面板需要的总高度 + /// + protected int m_nPropertyVHeight; + /// + /// 信息面板中Key显示需要的水平宽度 + /// + protected int m_nInfoLeft; + + #endregion + + private Type m_type; + private string[] m_KeysString = new string[] { "作者", "邮箱", "链接", "查看帮助" }; + + private int m_nTitleHeight = 20; + private int m_item_height = 30; + private Color m_clr_item_1 = Color.FromArgb(10, 0, 0, 0); + private Color m_clr_item_2 = Color.FromArgb(10, 255, 255, 255); + //所有属性列表保存在此List中 + private List m_lst_item = new List(); + + private STNodePropertyDescriptor m_item_hover; //当前被鼠标悬停的选项 + private STNodePropertyDescriptor m_item_hover_value; //当前值区域被鼠标悬停的选项 + private STNodePropertyDescriptor m_item_down_value; //当前值区域被鼠标点击的选项 + private STNodePropertyDescriptor m_item_selected; //当前选中的选项 + private STNodeAttribute m_node_attribute; //节点参数信息 + private bool m_b_hover_switch; //是否鼠标悬停在面板切换按钮上 + private bool m_b_current_draw_info; //当前绘制的时候是信息面板 + + private Point m_pt_move; //鼠标在控件上的实时坐标 + private Point m_pt_down; //上次鼠标在控件上点下的坐标 + private string m_str_err; //当被设置时 绘制错误信息 + private string m_str_desc; //当被设置时 绘制描述信息 + + private Pen m_pen; + private SolidBrush m_brush; + private StringFormat m_sf; + private DrawingTools m_dt; + + /// + /// 构造一个节点属性编辑器 + /// + public STNodePropertyGrid() { + this.SetStyle(ControlStyles.UserPaint, true); + this.SetStyle(ControlStyles.ResizeRedraw, true); + this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); + this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + this.SetStyle(ControlStyles.SupportsTransparentBackColor, true); + + m_pen = new Pen(Color.Black, 1); + m_brush = new SolidBrush(Color.Black); + m_sf = new StringFormat(); + m_sf.LineAlignment = StringAlignment.Center; + m_sf.FormatFlags = StringFormatFlags.NoWrap; + m_dt.Pen = m_pen; + m_dt.SolidBrush = m_brush; + + this.ForeColor = Color.White; + this.BackColor = Color.FromArgb(255, 35, 35, 35); + + this.MinimumSize = new Size(120, 50); + this.Size = new Size(200, 150); + } + + #region private method ========== + + private List GetProperties(STNode node) { + List lst = new List(); + if (node == null) return lst; + Type t = node.GetType(); + foreach (var p in t.GetProperties()) { + var attrs = p.GetCustomAttributes(true); + foreach (var a in attrs) { + if (!(a is STNodePropertyAttribute)) continue; + var attr = a as STNodePropertyAttribute; + object obj = Activator.CreateInstance(attr.DescriptorType); + if (!(obj is STNodePropertyDescriptor)) + throw new ArgumentException("[STNodePropertyAttribute.DescriptorType]参数值必须为[STNodePropertyDescriptor]或者其子类的类型"); + var desc = (STNodePropertyDescriptor)Activator.CreateInstance(attr.DescriptorType); + desc.Node = node; + desc.Name = attr.Name; + desc.Description = attr.Description; + desc.PropertyInfo = p; + desc.Control = this; + lst.Add(desc); + } + } + return lst; + } + + private STNodeAttribute GetNodeAttribute(STNode node) { + if (node == null) return null; + Type t = node.GetType(); + foreach (var v in t.GetCustomAttributes(true)) { + if (!(v is STNodeAttribute)) continue; + return (STNodeAttribute)v; + } + return null; + } + + private void SetItemRectangle() { + int nw_p = 0, nw_h = 0; + using (Graphics g = this.CreateGraphics()) { + foreach (var v in m_lst_item) { + SizeF szf = g.MeasureString(v.Name, this.Font); + if (szf.Width > nw_p) nw_p = (int)Math.Ceiling(szf.Width); + } + for (int i = 0; i < m_KeysString.Length - 1; i++) { + SizeF szf = g.MeasureString(m_KeysString[i], this.Font); + if (szf.Width > nw_h) nw_h = (int)Math.Ceiling(szf.Width); + } + nw_p += 5; nw_h += 5; + nw_p = Math.Min(nw_p, this.Width >> 1); + m_nInfoLeft = Math.Min(nw_h, this.Width >> 1); + + int nTitleHeight = this._ShowTitle ? m_nTitleHeight : 0; + for (int i = 0; i < m_lst_item.Count; i++) { + STNodePropertyDescriptor item = m_lst_item[i]; + Rectangle rect = new Rectangle(0, i * m_item_height + nTitleHeight, this.Width, m_item_height); + item.Rectangle = rect; + rect.Width = nw_p; + item.RectangleL = rect; + rect.X = rect.Right; + rect.Width = this.Width - rect.Left - 1; + rect.Inflate(-4, -4); + item.RectangleR = rect; + item.OnSetItemLocation(); + } + m_nPropertyVHeight = m_lst_item.Count * m_item_height; + if (this._ShowTitle) m_nPropertyVHeight += m_nTitleHeight; + } + } + + #endregion + + #region override ========== + + /// + /// 当控件重绘时候发生 + /// + /// 事件参数 + protected override void OnPaint(PaintEventArgs e) { + base.OnPaint(e); + Graphics g = e.Graphics; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + m_dt.Graphics = g; + + m_nOffsetY = m_b_current_draw_info ? m_nInfoOffsetY : m_nPropertyOffsetY; + g.TranslateTransform(0, m_nOffsetY); + + if (m_b_current_draw_info) { + m_nVHeight = m_nInfoVHeight; + this.OnDrawInfo(m_dt); + } else { + m_nVHeight = m_nPropertyVHeight; + for (int i = 0; i < m_lst_item.Count; i++) { + this.OnDrawPropertyItem(m_dt, m_lst_item[i], i); + } + } + + g.ResetTransform(); + + if (this._ShowTitle) this.OnDrawTitle(m_dt); + m_sf.FormatFlags = 0; + if (!string.IsNullOrEmpty(m_str_err)) this.OnDrawErrorInfo(m_dt); + if (!string.IsNullOrEmpty(m_str_desc)) this.OnDrawDescription(m_dt); + } + /// + /// 当鼠标在控件上移动时候发生 + /// + /// 事件参数 + protected override void OnMouseMove(MouseEventArgs e) { + base.OnMouseMove(e); + m_pt_move = e.Location; + bool bHover = this._ShowTitle && m_rect_switch.Contains(e.Location); + if (bHover != m_b_hover_switch) { + m_b_hover_switch = bHover; + this.Invalidate(m_rect_switch); + } + Point pt = new Point(e.X, e.Y - (int)m_nOffsetY); + MouseEventArgs mea = new MouseEventArgs(e.Button, e.Clicks, pt.X, pt.Y, e.Delta); + if (m_b_current_draw_info) + this.OnProcessHelpMouseMove(mea); + else + this.OnProcessPropertyMouseMove(mea); + } + /// + /// 当鼠标在控件上点下时候发生 + /// + /// 事件参数 + protected override void OnMouseDown(MouseEventArgs e) { + base.OnMouseDown(e); + m_pt_down = e.Location; + this.Focus(); + bool bRedraw = false; + if (m_str_err != null) { + bRedraw = true; + m_str_err = null; + } + if (this._ShowTitle) { + if (m_rect_switch.Contains(e.Location)) { + if (m_node_attribute == null || m_lst_item.Count == 0) return; + m_b_current_draw_info = !m_b_current_draw_info; + this.Invalidate(); + return; + } else if (m_rect_title.Contains(e.Location)) { + return; + } + } + if (this._ShowTitle && m_rect_switch.Contains(e.Location)) { + if (m_node_attribute == null || m_lst_item.Count == 0) return; + m_b_current_draw_info = !m_b_current_draw_info; + this.Invalidate(); + return; + } + Point pt = new Point(e.X, e.Y - (int)m_nOffsetY); + MouseEventArgs mea = new MouseEventArgs(e.Button, e.Clicks, pt.X, pt.Y, e.Delta); + + if (m_b_current_draw_info) + this.OnProcessInfoMouseDown(mea); + else + this.OnProcessPropertyMouseDown(mea); + if (bRedraw) this.Invalidate(); + } + /// + /// 当鼠标在控件上抬起时候发生 + /// + /// 事件参数 + protected override void OnMouseUp(MouseEventArgs e) { + base.OnMouseUp(e); + m_str_desc = null; + if (m_item_down_value != null && !this._ReadOnlyModel) { + Point pt = new Point(e.X, e.Y - (int)m_nOffsetY); + MouseEventArgs mea = new MouseEventArgs(e.Button, e.Clicks, pt.X, pt.Y, e.Delta); + m_item_down_value.OnMouseUp(mea); + if (m_pt_down == e.Location && !this._ReadOnlyModel) { + m_item_down_value.OnMouseClick(mea); + } + } + m_item_down_value = null; + this.Invalidate(); + } + /// + /// 当鼠标离开控件时候发生 + /// + /// 事件参数 + protected override void OnMouseLeave(EventArgs e) { + base.OnMouseLeave(e); + m_b_hover_switch = false; + if (m_item_hover_value != null && !this._ReadOnlyModel) m_item_hover_value.OnMouseLeave(e); + m_item_hover = null; + this.Invalidate(); + } + /// + /// 当鼠标在控件上滚动滚轮时候发生 + /// + /// 事件参数 + protected override void OnMouseWheel(MouseEventArgs e) { + base.OnMouseWheel(e); + if (e.Delta > 0) { + if (m_nOffsetY == 0) return; + m_nOffsetY += m_item_height; + if (m_nOffsetY > 0) m_nOffsetY = 0; + } else { + if (this.Height - m_nOffsetY >= m_nVHeight) return; + m_nOffsetY -= m_item_height; + } + if (m_b_current_draw_info) + m_nInfoOffsetY = m_nOffsetY; + else + m_nPropertyOffsetY = m_nOffsetY; + this.Invalidate(); + } + /// + /// 当设置控件矩形区域时候发生 + /// + /// x坐标 + /// y坐标 + /// 宽度 + /// 高度 + /// 指定需要设置的标识 + //protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) { + // if (width < 120) width = 120; + // if (height < 50) height = 50; + // base.SetBoundsCore(x, y, width, height, specified); + //} + /// + /// 当控件尺寸发生改变时候发生 + /// + /// 事件参数 + protected override void OnResize(EventArgs e) { + base.OnResize(e); + m_rect_title.Width = this.Width; + m_rect_title.Height = m_nTitleHeight; + if (this._ShowTitle) + m_rect_switch = new Rectangle(this.Width - m_nTitleHeight + 2, 2, m_nTitleHeight - 4, m_nTitleHeight - 4); + if (this._STNode != null) this.SetItemRectangle(); + } + + #endregion + + #region virtual method ========== + + /// + /// 当绘制属性选项时候发生 + /// + /// 绘制工具 + /// 目标属性选项描述器 + /// 选项所在索引 + protected virtual void OnDrawPropertyItem(DrawingTools dt, STNodePropertyDescriptor item, int nIndex) { + Graphics g = dt.Graphics; + m_brush.Color = (nIndex % 2 == 0) ? m_clr_item_1 : m_clr_item_2; + g.FillRectangle(m_brush, item.Rectangle); + if (item == m_item_hover || item == m_item_selected) { + m_brush.Color = this._ItemHoverColor; + g.FillRectangle(m_brush, item.Rectangle); + } + if (m_item_selected == item) { + g.FillRectangle(m_brush, item.Rectangle.X, item.Rectangle.Y, 5, item.Rectangle.Height); + if (this._AutoColor && this._STNode != null) + m_brush.Color = this._STNode.TitleColor; + else + m_brush.Color = this._ItemSelectedColor; + g.FillRectangle(m_brush, item.Rectangle.X, item.Rectangle.Y + 4, 5, item.Rectangle.Height - 8); + } + m_sf.Alignment = StringAlignment.Far; + m_brush.Color = this.ForeColor; + g.DrawString(item.Name, this.Font, m_brush, item.RectangleL, m_sf); + + item.OnDrawValueRectangle(m_dt); + if (this._ReadOnlyModel) { + m_brush.Color = Color.FromArgb(125, 125, 125, 125); + g.FillRectangle(m_brush, item.RectangleR); + m_pen.Color = this.ForeColor; + //g.DrawLine(m_pen, + // item.RectangleR.Left - 2, item.RectangleR.Top + item.RectangleR.Height / 2, + // item.RectangleR.Right + 1, item.RectangleR.Top + item.RectangleR.Height / 2); + } + } + /// + /// 绘制属性窗口标题 + /// + /// 绘制工具 + protected virtual void OnDrawTitle(DrawingTools dt) { + Graphics g = dt.Graphics; + if (this._AutoColor) + m_brush.Color = this._STNode == null ? this._TitleColor : this._STNode.TitleColor; + else + m_brush.Color = this._TitleColor; + g.FillRectangle(m_brush, m_rect_title); + m_brush.Color = this._STNode == null ? this.ForeColor : this._STNode.ForeColor; + m_sf.Alignment = StringAlignment.Center; + g.DrawString(this._STNode == null ? this.Text : this._STNode.Title, this.Font, m_brush, m_rect_title, m_sf); + + if (this._ReadOnlyModel) { + m_brush.Color = this.ForeColor; + g.FillRectangle(dt.SolidBrush, 4, 5, 2, 4); + g.FillRectangle(dt.SolidBrush, 6, 5, 2, 2); + g.FillRectangle(dt.SolidBrush, 8, 5, 2, 4); + g.FillRectangle(dt.SolidBrush, 3, 9, 8, 6); + } + //是否绘制面板切换按钮 + if (m_node_attribute == null || m_lst_item.Count == 0) return; + if (m_b_hover_switch) { + m_brush.Color = this.BackColor; + g.FillRectangle(m_brush, m_rect_switch); + } + m_pen.Color = this._STNode == null ? this.ForeColor : this._STNode.ForeColor; + m_brush.Color = m_pen.Color; + int nT1 = m_rect_switch.Top + m_rect_switch.Height / 2 - 2; + int nT2 = m_rect_switch.Top + m_rect_switch.Height / 2 + 1; + g.DrawRectangle(m_pen, m_rect_switch.Left, m_rect_switch.Top, m_rect_switch.Width - 1, m_rect_switch.Height - 1); + + g.DrawLines(m_pen, new Point[]{ + new Point(m_rect_switch.Left + 2, nT1), new Point(m_rect_switch.Right - 3, nT1), + new Point(m_rect_switch.Left + 3, nT1 - 1), new Point(m_rect_switch.Right - 3, nT1 - 1) + }); + g.DrawLines(m_pen, new Point[]{ + new Point(m_rect_switch.Left + 2, nT2), new Point(m_rect_switch.Right - 3, nT2), + new Point(m_rect_switch.Left + 2, nT2 + 1), new Point(m_rect_switch.Right - 4, nT2 + 1), + }); + + g.FillPolygon(m_brush, new Point[]{ + new Point(m_rect_switch.Left + 2, nT1), + new Point(m_rect_switch.Left + 7, nT1), + new Point(m_rect_switch.Left + 7, m_rect_switch.Top ), + }); + g.FillPolygon(m_brush, new Point[]{ + new Point(m_rect_switch.Right - 2, nT2), + new Point(m_rect_switch.Right - 7, nT2), + new Point(m_rect_switch.Right - 7, m_rect_switch.Bottom - 2 ), + }); + } + /// + /// 当需要绘制属性描述信息时发生 + /// + /// 绘制工具 + protected virtual void OnDrawDescription(DrawingTools dt) { + if (string.IsNullOrEmpty(m_str_desc)) return; + Graphics g = dt.Graphics; + SizeF szf = g.MeasureString(m_str_desc, this.Font, this.Width - 4); + Rectangle rect_desc = new Rectangle(0, this.Height - (int)szf.Height - 4, this.Width, (int)szf.Height + 4); + m_brush.Color = this._DescriptionColor; + g.FillRectangle(m_brush, rect_desc); + m_pen.Color = this._DescriptionColor; + g.DrawRectangle(m_pen, 0, rect_desc.Top, rect_desc.Width - 1, rect_desc.Height - 1); + rect_desc.Inflate(-4, 0); + m_brush.Color = this.ForeColor; + m_sf.Alignment = StringAlignment.Near; + g.DrawString(m_str_desc, this.Font, m_brush, rect_desc, m_sf); + } + /// + /// 当需要绘制错误信息时发生 + /// + /// 绘制工具 + protected virtual void OnDrawErrorInfo(DrawingTools dt) { + if (string.IsNullOrEmpty(m_str_err)) return; + Graphics g = dt.Graphics; + SizeF szf = g.MeasureString(m_str_err, this.Font, this.Width - 4); + Rectangle rect_desc = new Rectangle(0, 0, this.Width, (int)szf.Height + 4); + m_brush.Color = this._ErrorColor; + g.FillRectangle(m_brush, rect_desc); + m_pen.Color = this._ErrorColor; + g.DrawRectangle(m_pen, 0, rect_desc.Top, rect_desc.Width - 1, rect_desc.Height - 1); + rect_desc.Inflate(-4, 0); + m_brush.Color = this.ForeColor; + m_sf.Alignment = StringAlignment.Near; + g.DrawString(m_str_err, this.Font, m_brush, rect_desc, m_sf); + } + /// + /// 当绘制节点信息时候发生 + /// + /// 绘制工具 + protected virtual void OnDrawInfo(DrawingTools dt) { + if (m_node_attribute == null) return; + var attr = m_node_attribute; + Graphics g = dt.Graphics; + Color clr_r = Color.FromArgb(this.ForeColor.A / 2, this.ForeColor); + m_sf.Alignment = StringAlignment.Near; + Rectangle rect = new Rectangle(0, this._ShowTitle ? m_nTitleHeight : 0, this.Width, m_item_height); + Rectangle rect_l = new Rectangle(2, rect.Top, m_nInfoLeft - 2, m_item_height); + Rectangle rect_r = new Rectangle(m_nInfoLeft, rect.Top, this.Width - m_nInfoLeft, m_item_height); + m_brush.Color = m_clr_item_2; + g.FillRectangle(m_brush, rect); + m_brush.Color = this.ForeColor; + m_sf.FormatFlags = StringFormatFlags.NoWrap; + m_sf.Alignment = StringAlignment.Near; + g.DrawString(m_KeysString[0], this.Font, m_brush, rect_l, m_sf); //author + m_brush.Color = clr_r; + g.DrawString(attr.Author, this.Font, m_brush, rect_r, m_sf); + rect.Y += m_item_height; rect_l.Y += m_item_height; rect_r.Y += m_item_height; + + m_brush.Color = m_clr_item_1; + g.FillRectangle(m_brush, rect); + m_brush.Color = this.ForeColor; + g.DrawString(m_KeysString[1], this.Font, m_brush, rect_l, m_sf); //mail + m_brush.Color = clr_r; + g.DrawString(attr.Mail, this.Font, m_brush, rect_r, m_sf); + rect.Y += m_item_height; rect_l.Y += m_item_height; rect_r.Y += m_item_height; + + m_brush.Color = m_clr_item_2; + g.FillRectangle(m_brush, rect); + m_brush.Color = this.ForeColor; + g.DrawString(m_KeysString[2], this.Font, m_brush, rect_l, m_sf); //link_key + m_brush.Color = clr_r; + g.DrawString(attr.Link, this.Font, Brushes.CornflowerBlue, rect_r, m_sf); //link + if (!string.IsNullOrEmpty(attr.Link)) m_rect_link = rect_r; + //fill left + m_brush.Color = Color.FromArgb(40, 125, 125, 125); + g.FillRectangle(m_brush, 0, this._ShowTitle ? m_nTitleHeight : 0, m_nInfoLeft - 1, m_item_height * 3); + + rect.X = 5; rect.Y += m_item_height; + rect.Width = this.Width - 10; + if (!string.IsNullOrEmpty(m_node_attribute.Description)) { + float h = g.MeasureString(m_node_attribute.Description, this.Font, rect.Width).Height; + rect.Height = (int)Math.Ceiling(h / m_item_height) * m_item_height; + m_brush.Color = clr_r; + m_sf.FormatFlags = 0; + g.DrawString(m_node_attribute.Description, this.Font, m_brush, rect, m_sf); + } + m_nInfoVHeight = rect.Bottom; + bool bHasHelp = STNodeAttribute.GetHelpMethod(m_type) != null; + rect.X = 5; rect.Y += rect.Height; + rect.Height = m_item_height; + m_sf.Alignment = StringAlignment.Center; + m_brush.Color = Color.FromArgb(125, 125, 125, 125); + g.FillRectangle(m_brush, rect); + if (bHasHelp) m_brush.Color = Color.CornflowerBlue; + g.DrawString(m_KeysString[3], this.Font, m_brush, rect, m_sf); + if (bHasHelp) m_rect_help = rect; + else { + int w = (int)g.MeasureString(m_KeysString[3], this.Font).Width + 1; + int x = rect.X + (rect.Width - w) / 2, y = rect.Y + rect.Height / 2; + m_pen.Color = m_brush.Color; + g.DrawLine(m_pen, x, y, x + w, y); + } + m_nInfoVHeight = rect.Bottom; + } + /// + /// 当在属性面板鼠标点下时候发生 + /// + /// 鼠标事件参数 + protected virtual void OnProcessPropertyMouseDown(MouseEventArgs e) { + bool bRedraw = false; + if (m_item_selected != m_item_hover) { + m_item_selected = m_item_hover; + bRedraw = true; + } + m_item_down_value = null; + if (m_item_selected == null) { + if (bRedraw) this.Invalidate(); + return; + } + if (m_item_selected.RectangleR.Contains(e.Location)) { + m_item_down_value = m_item_selected; + if (!this._ReadOnlyModel) + m_item_selected.OnMouseDown(e); + else { + return; + } + } else if (m_item_selected.RectangleL.Contains(e.Location)) { + m_str_desc = m_item_selected.Description; + bRedraw = true; + } + if (bRedraw) this.Invalidate(); + } + /// + /// 当在信息面板鼠标点下时候发生 + /// + /// 鼠标事件参数 + protected virtual void OnProcessInfoMouseDown(MouseEventArgs e) { + try { + if (m_rect_link.Contains(e.Location)) { + System.Diagnostics.Process.Start(m_node_attribute.Link); + } else if (m_rect_help.Contains(e.Location)) { + STNodeAttribute.ShowHelp(m_type); + } + } catch (Exception ex) { + this.SetErrorMessage(ex.Message); + } + } + /// + /// 当在属性面板鼠标移动时候发生 + /// + /// 鼠标事件参数 + protected virtual void OnProcessPropertyMouseMove(MouseEventArgs e) { + if (m_item_down_value != null) { + m_item_down_value.OnMouseMove(e); + return; + } + STNodePropertyDescriptor item = null; + foreach (var v in m_lst_item) { + if (v.Rectangle.Contains(e.Location)) { + item = v; + break; + } + } + if (item != null) { + if (item.RectangleR.Contains(e.Location)) { + if (m_item_hover_value != item) { + if (m_item_hover_value != null) m_item_hover_value.OnMouseLeave(e); + m_item_hover_value = item; + m_item_hover_value.OnMouseEnter(e); + } + m_item_hover_value.OnMouseMove(e); + } else { + if (m_item_hover_value != null) m_item_hover_value.OnMouseLeave(e); + } + } + if (m_item_hover != item) { + m_item_hover = item; + this.Invalidate(); + } + } + /// + /// 当在信息面板鼠标移动时候发生 + /// + /// 鼠标事件参数 + protected virtual void OnProcessHelpMouseMove(MouseEventArgs e) { + if (m_rect_link.Contains(e.Location) || m_rect_help.Contains(e.Location)) { + this.Cursor = Cursors.Hand; + } else this.Cursor = Cursors.Arrow; + } + + #endregion + + #region public ========== + + /// + /// 设置需要显示的STNode节点 + /// + /// 目标节点 + public void SetNode(STNode node) { + if (node == this._STNode) return; + m_nInfoOffsetY = m_nPropertyOffsetY = 0; + m_nInfoVHeight = m_nPropertyVHeight = 0; + m_rect_link = m_rect_help = Rectangle.Empty; + m_str_desc = m_str_err = null; + this._STNode = node; + if (node != null) { + m_type = node.GetType(); + m_lst_item = this.GetProperties(node); + m_node_attribute = this.GetNodeAttribute(node); + this.SetItemRectangle(); + m_b_current_draw_info = m_lst_item.Count == 0 || this._InfoFirstOnDraw; + if (this._AutoColor) this._ItemSelectedColor = this._STNode.TitleColor; + } else { + m_type = null; + m_lst_item.Clear(); + m_node_attribute = null; + } + this.Invalidate(); + } + /// + /// 设置信息页面Key的显示文本 + /// + /// 作者 + /// 邮箱 + /// 连接 + /// 查看帮助 + public void SetInfoKey(string strAuthor, string strMail, string strLink, string strHelp) { + m_KeysString = new string[] { strAuthor, strMail, strLink, strHelp }; + } + /// + /// 设置要显示的错误信息 + /// + /// 错误信息 + public void SetErrorMessage(string strText) { + m_str_err = strText; + this.Invalidate(); + } + + #endregion + } +} diff --git a/ST.Library.UI/NodeEditor/STNodeTreeView.cs b/ST.Library.UI/NodeEditor/STNodeTreeView.cs new file mode 100644 index 0000000..6b40ee3 --- /dev/null +++ b/ST.Library.UI/NodeEditor/STNodeTreeView.cs @@ -0,0 +1,907 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; +using System.ComponentModel; +using System.Collections; +/* +MIT License + +Copyright (c) 2021 DebugST@crystal_lz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +/* + * create: 2021-02-23 + * modify: 2021-03-02 + * Author: Crystal_lz + * blog: http://st233.com + * Gitee: https://gitee.com/DebugST + * Github: https://github.com/DebugST + */ +namespace ST.Library.UI.NodeEditor +{ + public class STNodeTreeView : Control + { + private Color _ItemBackColor = Color.FromArgb(255, 45, 45, 45); + /// + /// 获取或设置每行属性选项背景色 + /// + [Description("获取或设置每行属性选项背景色")] + public Color ItemBackColor { + get { return _ItemBackColor; } + set { + _ItemBackColor = value; + } + } + + private Color _ItemHoverColor = Color.FromArgb(50, 125, 125, 125); + /// + /// 获取或设置属性选项被鼠标悬停时候背景色 + /// + [Description("获取或设置属性选项被鼠标悬停时候背景色")] + public Color ItemHoverColor { + get { return _ItemHoverColor; } + set { _ItemHoverColor = value; } + } + + private Color _TitleColor = Color.FromArgb(255, 60, 60, 60); + /// + /// 获取或设置顶部检索区域背景色 + /// + [Description("获取或设置顶部检索区域背景颜色")] + public Color TitleColor { + get { return _TitleColor; } + set { + _TitleColor = value; + this.Invalidate(new Rectangle(0, 0, this.Width, m_nItemHeight)); + } + } + + /// + /// 获取或设置检索文本框的背景色 + /// + [Description("获取或设置检索文本框的背景色")] + public Color TextBoxColor { + get { return m_tbx.BackColor; } + set { + m_tbx.BackColor = value; + this.Invalidate(new Rectangle(0, 0, this.Width, m_nItemHeight)); + } + } + + private Color _HightLightTextColor = Color.Lime; + /// + /// 获取或设置检索时候高亮文本颜色 + /// + [Description("获取或设置检索时候高亮文本颜色"), DefaultValue(typeof(Color), "Lime")] + public Color HightLightTextColor { + get { return _HightLightTextColor; } + set { _HightLightTextColor = value; } + } + + private Color _InfoButtonColor = Color.Gray; + /// + /// 获取或设置信息显示按钮颜色 若设置AutoColor无法设置此属性值 + /// + [Description("获取或设置信息显示按钮颜色 若设置AutoColor无法设置此属性值"), DefaultValue(typeof(Color), "Gray")] + public Color InfoButtonColor { + get { return _InfoButtonColor; } + set { _InfoButtonColor = value; } + } + + private Color _FolderCountColor = Color.FromArgb(40, 255, 255, 255); + /// + /// 获取或设置统计个数的文本颜色 + /// + [Description("获取或设置统计个数的文本颜色")] + public Color FolderCountColor { + get { return _FolderCountColor; } + set { _FolderCountColor = value; } + } + + private Color _SwitchColor = Color.LightGray; + + private bool _ShowFolderCount = true; + /// + /// 获取或设置是否统计STNode的个数 + /// + [Description("获取或设置是否统计STNode的个数"), DefaultValue(typeof(Color), "LightGray")] + public bool ShowFolderCount { + get { return _ShowFolderCount; } + set { _ShowFolderCount = value; } + } + + private bool _ShowInfoButton = true; + /// + /// 获取或设置是否显示信息按钮 + /// + [Description("获取或设置是否显示信息按钮"), DefaultValue(true)] + public bool ShowInfoButton { + get { return _ShowInfoButton; } + set { _ShowInfoButton = value; } + } + + private bool _InfoPanelIsLeftLayout = true; + /// + /// 获取或设置预览窗口是否是向左布局 + /// + [Description("获取或设置预览窗口是否是向左布局"), DefaultValue(true)] + public bool InfoPanelIsLeftLayout { + get { return _InfoPanelIsLeftLayout; } + set { _InfoPanelIsLeftLayout = value; } + } + + private bool _AutoColor = true; + /// + /// 获取或设置控件中部分颜色来之对应的STNode的标题颜色 + /// + [Description("获取或设置控件中部分颜色来之对应的STNode的标题颜色"), DefaultValue(true)] + public bool AutoColor { + get { return _AutoColor; } + set { + _AutoColor = value; + this.Invalidate(); + } + } + + private STNodeEditor _Editor; + /// + /// 获取节点预览时候使用的STNodeEditor + /// + [Description("获取节点预览时候使用的STNodeEditor"), Browsable(false)] + public STNodeEditor Editor { + get { return _Editor; } + } + + private STNodePropertyGrid _PropertyGrid; + /// + /// 获取节点预览时候使用的STNodePropertyGrid + /// + [Description("获取节点预览时候使用的STNodePropertyGrid"), Browsable(false)] + public STNodePropertyGrid PropertyGrid { + get { return _PropertyGrid; } + } + + private int m_nItemHeight = 29; + + private static Type m_type_node_base = typeof(STNode); + private static char[] m_chr_splitter = new char[] { '/', '\\' }; + private STNodeTreeCollection m_items_draw; + private STNodeTreeCollection m_items_source = new STNodeTreeCollection("ROOT"); + private Dictionary m_dic_all_type = new Dictionary(); + + private Pen m_pen; + private SolidBrush m_brush; + private StringFormat m_sf; + private DrawingTools m_dt; + private Color m_clr_item_1 = Color.FromArgb(10, 0, 0, 0);// Color.FromArgb(255, 40, 40, 40); + private Color m_clr_item_2 = Color.FromArgb(10, 255, 255, 255);// Color.FromArgb(255, 50, 50, 50); + + private int m_nOffsetY; //控件绘制时候需要偏移的垂直高度 + private int m_nSourceOffsetY; //绘制源数据时候需要偏移的垂直高度 + private int m_nSearchOffsetY; //绘制检索数据时候需要偏移的垂直高度 + private int m_nVHeight; //控件中内容需要的总高度 + + private bool m_bHoverInfo; //当前鼠标是否悬停在信息显示按钮上 + private STNodeTreeCollection m_item_hover; //当前鼠标悬停的树节点 + private Point m_pt_control; //鼠标在控件上的坐标 + private Point m_pt_offsety; //鼠标在控件上锤子偏移后的坐标 + private Rectangle m_rect_clear; //清空检索按钮区域 + + private string m_str_search; //检索的文本 + private TextBox m_tbx = new TextBox(); //检索文本框 + /// + /// 构造一个STNode树控件 + /// + public STNodeTreeView() { + this.SetStyle(ControlStyles.UserPaint, true); + this.SetStyle(ControlStyles.ResizeRedraw, true); + this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); + this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + this.SetStyle(ControlStyles.SupportsTransparentBackColor, true); + + this.MinimumSize = new System.Drawing.Size(100, 60); + this.Size = new System.Drawing.Size(200, 150); + m_items_draw = m_items_source; + m_pen = new Pen(Color.Black); + m_brush = new SolidBrush(Color.White); + m_sf = new StringFormat(); + m_sf.LineAlignment = StringAlignment.Center; + m_dt.Pen = m_pen; + m_dt.SolidBrush = m_brush; + this.ForeColor = Color.FromArgb(255, 220, 220, 220); + this.BackColor = Color.FromArgb(255, 35, 35, 35); + m_tbx.Left = 6; + m_tbx.BackColor = Color.FromArgb(255, 30, 30, 30); + m_tbx.ForeColor = this.ForeColor; + m_tbx.BorderStyle = BorderStyle.None; + m_tbx.MaxLength = 20; + m_tbx.TextChanged += new EventHandler(m_tbx_TextChanged); + this.Controls.Add(m_tbx); + this.AllowDrop = true; + + this._Editor = new STNodeEditor(); + this._PropertyGrid = new STNodePropertyGrid(); + } + + #region private method ========== + + private void m_tbx_TextChanged(object sender, EventArgs e) { + m_str_search = m_tbx.Text.Trim().ToLower(); + m_nSearchOffsetY = 0; + if (m_str_search == string.Empty) { + m_items_draw = m_items_source; + this.Invalidate(); + return; + } + m_items_draw = m_items_source.Copy(); + this.Search(m_items_draw, new Stack(), m_str_search); + this.Invalidate(); + } + + private bool Search(STNodeTreeCollection items, Stack stack, string strText) { + bool bFound = false; + string[] strName = new string[items.Count]; + int nCounter = 0; + foreach (STNodeTreeCollection v in items) { + if (v.NameLower.IndexOf(strText) != -1) { + v.IsOpen = bFound = true; + } else { + if (!this.Search(v, stack, strText)) { + stack.Push(v.Name); + nCounter++; + } else { + v.IsOpen = bFound = true; + } + } + } + for (int i = 0; i < nCounter; i++) items.Remove(stack.Pop(), false); + return bFound; + } + + private bool AddSTNode(Type stNodeType, STNodeTreeCollection items, string strLibName, bool bShowException) { + if (m_dic_all_type.ContainsKey(stNodeType)) return false; + if (stNodeType == null) return false; + if (!stNodeType.IsSubclassOf(m_type_node_base)) { + if (bShowException) + throw new ArgumentException("不支持的类型[" + stNodeType.FullName + "] [stNodeType]参数值必须为[STNode]子类的类型"); + else return false; + } + var attr = this.GetNodeAttribute(stNodeType); + if (attr == null) { + if (bShowException) + throw new InvalidOperationException("类型[" + stNodeType.FullName + "]未被[STNodeAttribute]所标记"); + else return false; + } + string strPath = string.Empty; + items.STNodeCount++; + if (!string.IsNullOrEmpty(attr.Path)) { + var strKeys = attr.Path.Split(m_chr_splitter); + for (int i = 0; i < strKeys.Length; i++) { + items = items.Add(strKeys[i]); + items.STNodeCount++; + strPath += "/" + strKeys[i]; + } + } + try { + STNode node = (STNode)Activator.CreateInstance(stNodeType); + STNodeTreeCollection stt = new STNodeTreeCollection(node.Title); + stt.Path = (strLibName + "/" + attr.Path).Trim('/'); + stt.STNodeType = stNodeType; + items[stt.Name] = stt; + stt.STNodeTypeColor = node.TitleColor; + m_dic_all_type.Add(stNodeType, stt.Path); + this.Invalidate(); + } catch (Exception ex) { + if (bShowException) throw ex; + return false; + } + return true; + } + + private STNodeTreeCollection AddAssemblyPrivate(string strFile) { + strFile = System.IO.Path.GetFullPath(strFile); + var asm = Assembly.LoadFrom(strFile); + STNodeTreeCollection items = new STNodeTreeCollection(System.IO.Path.GetFileNameWithoutExtension(strFile)); + foreach (var v in asm.GetTypes()) { + if (v.IsAbstract) continue; + if (v.IsSubclassOf(m_type_node_base)) this.AddSTNode(v, items, items.Name, false); + } + return items; + } + + private STNodeAttribute GetNodeAttribute(Type stNodeType) { + if (stNodeType == null) return null; + foreach (var v in stNodeType.GetCustomAttributes(true)) { + if (!(v is STNodeAttribute)) continue; + return (STNodeAttribute)v; + } + return null; + } + + private STNodeTreeCollection FindItemByPoint(STNodeTreeCollection items, Point pt) { + foreach (STNodeTreeCollection t in items) { + if (t.DisplayRectangle.Contains(pt)) return t; + if (t.IsOpen) { + var n = FindItemByPoint(t, pt); + if (n != null) return n; + } + } + return null; + } + + #endregion + + #region overide method ========== + + protected override void OnCreateControl() { + base.OnCreateControl(); + m_tbx.Top = (m_nItemHeight - m_tbx.Height) / 2; + } + + protected override void OnResize(EventArgs e) { + base.OnResize(e); + m_tbx.Width = this.Width - 29; + m_rect_clear = new Rectangle(this.Width - 20, 9, 12, 12); + } + + protected override void OnPaint(PaintEventArgs e) { + base.OnPaint(e); + m_nOffsetY = string.IsNullOrEmpty(m_str_search) ? m_nSourceOffsetY : m_nSearchOffsetY; + Graphics g = e.Graphics; + m_dt.Graphics = g; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + g.TranslateTransform(0, m_nOffsetY); + int nCounter = 0; + foreach (STNodeTreeCollection v in m_items_draw) + nCounter = this.OnStartDrawItem(m_dt, v, nCounter, 0); + m_nVHeight = (nCounter + 1) * m_nItemHeight; + foreach (STNodeTreeCollection v in m_items_draw) + this.OnDrawSwitch(m_dt, v); + g.ResetTransform(); + this.OnDrawSearch(m_dt); + } + + protected override void OnMouseMove(MouseEventArgs e) { + base.OnMouseMove(e); + bool bRedraw = false; + m_pt_offsety = m_pt_control = e.Location; + m_pt_offsety.Y -= m_nOffsetY; + if (!string.IsNullOrEmpty(m_str_search) && m_rect_clear.Contains(e.Location)) + this.Cursor = Cursors.Hand; + else + this.Cursor = Cursors.Arrow; + var node = this.FindItemByPoint(m_items_draw, m_pt_offsety); + if (m_item_hover != node) { + m_item_hover = node; + bRedraw = true; + } + if (node != null) { + bool bHoverInfo = node.InfoRectangle.Contains(m_pt_offsety); + if (bHoverInfo != m_bHoverInfo) { + m_bHoverInfo = bHoverInfo; + bRedraw = true; + } + } + if (bRedraw) this.Invalidate(); + } + + protected override void OnMouseDown(MouseEventArgs e) { + base.OnMouseDown(e); + this.Focus(); + if (!string.IsNullOrEmpty(m_str_search) && m_rect_clear.Contains(e.Location)) { + m_tbx.Text = string.Empty; + return; + } + m_pt_offsety = m_pt_control = e.Location; + m_pt_offsety.Y -= m_nOffsetY; + if (m_item_hover == null) return; + if (m_item_hover.SwitchRectangle.Contains(m_pt_offsety)) { + m_item_hover.IsOpen = !m_item_hover.IsOpen; + this.Invalidate(); + } else if (m_item_hover.InfoRectangle.Contains(m_pt_offsety)) { + Rectangle rect = this.RectangleToScreen(m_item_hover.DisplayRectangle); + FrmNodePreviewPanel frm = new FrmNodePreviewPanel(m_item_hover.STNodeType, + new Point(rect.Right - m_nItemHeight, rect.Top + m_nOffsetY), + m_nItemHeight, + this._InfoPanelIsLeftLayout, + this._Editor, this._PropertyGrid); + frm.BackColor = this.BackColor; + frm.Show(this); + } else if (m_item_hover.STNodeType != null) { + DataObject d = new DataObject("STNodeType", m_item_hover.STNodeType); + this.DoDragDrop(d, DragDropEffects.Copy); + } + } + + protected override void OnMouseDoubleClick(MouseEventArgs e) { + base.OnMouseDoubleClick(e); + m_pt_offsety = m_pt_control = e.Location; + m_pt_offsety.Y -= m_nOffsetY; + STNodeTreeCollection item = this.FindItemByPoint(m_items_draw, m_pt_offsety); + if (item == null || item.STNodeType != null) return; + item.IsOpen = !item.IsOpen; + this.Invalidate(); + } + + protected override void OnMouseLeave(EventArgs e) { + base.OnMouseLeave(e); + if (m_item_hover != null) { + m_item_hover = null; + this.Invalidate(); + } + } + + protected override void OnMouseWheel(MouseEventArgs e) { + base.OnMouseWheel(e); + if (e.Delta > 0) { + if (m_nOffsetY == 0) return; + m_nOffsetY += m_nItemHeight; + if (m_nOffsetY > 0) m_nOffsetY = 0; + } else { + if (this.Height - m_nOffsetY >= m_nVHeight) return; + m_nOffsetY -= m_nItemHeight; + } + if (string.IsNullOrEmpty(m_str_search)) + m_nSourceOffsetY = m_nOffsetY; + else + m_nSearchOffsetY = m_nOffsetY; + this.Invalidate(); + } + + #endregion + + #region protected method ========== + /// + /// 当绘制检索文本区域时候发生 + /// + /// 绘制工具 + protected virtual void OnDrawSearch(DrawingTools dt) { + Graphics g = dt.Graphics; + m_brush.Color = this._TitleColor; + g.FillRectangle(m_brush, 0, 0, this.Width, m_nItemHeight); + m_brush.Color = m_tbx.BackColor; + g.FillRectangle(m_brush, 5, 5, this.Width - 10, m_nItemHeight - 10); + m_pen.Color = this.ForeColor; + if (string.IsNullOrEmpty(m_str_search)) { + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; + g.DrawEllipse(m_pen, this.Width - 17, 8, 8, 8); + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; + g.DrawLine(m_pen, this.Width - 13, 17, this.Width - 13, m_nItemHeight - 9); + } else { + m_pen.Color = this.ForeColor; + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; + g.DrawEllipse(m_pen, this.Width - 20, 9, 10, 10); + g.DrawLine(m_pen, this.Width - 18, 11, this.Width - 12, 17); + g.DrawLine(m_pen, this.Width - 12, 11, this.Width - 18, 17); + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; + } + } + /// + /// 当开始绘制树节点的每一个节点时候发生 + /// + /// 绘制工具 + /// 当前需要绘制的集合 + /// 已经绘制个数的计数器 + /// 当前位于第几级子集合 + /// 已经绘制个数 + protected virtual int OnStartDrawItem(DrawingTools dt, STNodeTreeCollection Items, int nCounter, int nLevel) { + Graphics g = dt.Graphics; + Items.DisplayRectangle = new Rectangle(0, m_nItemHeight * (nCounter + 1), this.Width, m_nItemHeight); + Items.SwitchRectangle = new Rectangle(5 + nLevel * 10, (nCounter + 1) * m_nItemHeight, 10, m_nItemHeight); + if (this._ShowInfoButton && Items.STNodeType != null) + Items.InfoRectangle = new Rectangle(this.Width - 18, Items.DisplayRectangle.Top + (m_nItemHeight - 14) / 2, 14, 14); + else Items.InfoRectangle = Rectangle.Empty; + this.OnDrawItem(dt, Items, nCounter++, nLevel); + if (!Items.IsOpen) return nCounter; + foreach (STNodeTreeCollection n in Items) { + if (n.STNodeType == null) + nCounter = this.OnStartDrawItem(dt, n, nCounter++, nLevel + 1); + } + foreach (STNodeTreeCollection n in Items) { + if (n.STNodeType != null) + nCounter = this.OnStartDrawItem(dt, n, nCounter++, nLevel + 1); + } + foreach (STNodeTreeCollection v in Items) this.OnDrawSwitch(dt, v); + return nCounter; + } + /// + /// 当绘制树节点每一个节点时候发生 + /// + /// 绘制工具 + /// 当前需要绘制的集合 + /// 已经绘制个数的计数器 + /// 当前位于第几级子集合 + protected virtual void OnDrawItem(DrawingTools dt, STNodeTreeCollection items, int nCounter, int nLevel) { + Graphics g = dt.Graphics; + m_brush.Color = nCounter % 2 == 0 ? m_clr_item_1 : m_clr_item_2; + g.FillRectangle(m_brush, items.DisplayRectangle); + if (items == m_item_hover) { + m_brush.Color = this._ItemHoverColor; + g.FillRectangle(m_brush, items.DisplayRectangle); + } + Rectangle rect = new Rectangle(45 + nLevel * 10, items.SwitchRectangle.Top, this.Width - 45 - nLevel * 10, m_nItemHeight); + m_pen.Color = Color.FromArgb(100, 125, 125, 125); + g.DrawLine(m_pen, 9, items.SwitchRectangle.Top + m_nItemHeight / 2, items.SwitchRectangle.Left + 19, items.SwitchRectangle.Top + m_nItemHeight / 2); + if (nCounter != 0) { + for (int i = 0; i <= nLevel; i++) { + g.DrawLine(m_pen, 9 + i * 10, items.SwitchRectangle.Top - m_nItemHeight / 2, 9 + i * 10, items.SwitchRectangle.Top + m_nItemHeight / 2 - 1); + } + } + this.OnDrawItemText(dt, items, rect); + this.OnDrawItemIcon(dt, items, rect); + } + /// + /// 当绘制树节点展开与关闭开关时候发生 + /// + /// 绘制工具 + /// 当前需要绘制的集合 + protected virtual void OnDrawSwitch(DrawingTools dt, STNodeTreeCollection items) { + Graphics g = dt.Graphics; + if (items.Count != 0) { + m_pen.Color = this._SwitchColor; + m_brush.Color = m_pen.Color; + int nT = items.SwitchRectangle.Y + m_nItemHeight / 2 - 4; + g.DrawRectangle(m_pen, items.SwitchRectangle.Left, nT, 8, 8); + g.DrawLine(m_pen, items.SwitchRectangle.Left + 1, nT + 4, items.SwitchRectangle.Right - 3, nT + 4); + if (items.IsOpen) return; + g.DrawLine(m_pen, items.SwitchRectangle.Left + 4, nT + 1, items.SwitchRectangle.Left + 4, nT + 7); + //if (items.IsOpen) { + // //g.FillPolygon(m_brush, new Point[]{ + // // new Point(items.DotRectangle.Left + 0, items.DotRectangle.Top + m_nItemHeight / 2 - 2), + // // new Point(items.DotRectangle.Left + 9, items.DotRectangle.Top + m_nItemHeight / 2 - 2), + // // new Point(items.DotRectangle.Left + 4, items.DotRectangle.Top + m_nItemHeight / 2 + 3) + // //}); + // g.DrawRectangle(m_pen, items.SwitchRectangle.Left, nT, 8, 8); + // g.DrawLine(m_pen, items.SwitchRectangle.Left + 1, nT + 4, items.SwitchRectangle.Right - 3, nT + 4); + //} else { + // //g.FillPolygon(m_brush, new Point[]{ + // // new Point(items.DotRectangle.Left + 2, items.DotRectangle.Top + m_nItemHeight / 2 - 5), + // // new Point(items.DotRectangle.Left + 2, items.DotRectangle.Top + m_nItemHeight / 2 + 5), + // // new Point(items.DotRectangle.Left + 7, items.DotRectangle.Top + m_nItemHeight / 2) + // //}); + // g.DrawRectangle(m_pen, items.SwitchRectangle.Left, nT, 8, 8); + // g.DrawLine(m_pen, items.SwitchRectangle.Left + 1, nT + 4, items.SwitchRectangle.Right - 3, nT + 4); + // g.DrawLine(m_pen, items.SwitchRectangle.Left + 4, nT + 1, items.SwitchRectangle.Left + 4, nT + 7); + //} + } + } + /// + /// 当绘制树节点的文本时候发生 + /// + /// 绘制工具 + /// 当前需要绘制的集合 + /// 文本域所在矩形区域 + protected virtual void OnDrawItemText(DrawingTools dt, STNodeTreeCollection items, Rectangle rect) { + Graphics g = dt.Graphics; + rect.Width -= 20; + m_sf.FormatFlags = StringFormatFlags.NoWrap; + if (!string.IsNullOrEmpty(m_str_search)) { + int nIndex = items.NameLower.IndexOf(m_str_search); + if (nIndex != -1) { + CharacterRange[] chrs = { new CharacterRange(nIndex, m_str_search.Length) };//global + m_sf.SetMeasurableCharacterRanges(chrs); + Region[] regions = g.MeasureCharacterRanges(items.Name, this.Font, rect, m_sf); + g.SetClip(regions[0], System.Drawing.Drawing2D.CombineMode.Intersect); + m_brush.Color = this._HightLightTextColor; + g.DrawString(items.Name, this.Font, m_brush, rect, m_sf); + g.ResetClip(); + g.SetClip(regions[0], System.Drawing.Drawing2D.CombineMode.Exclude); + m_brush.Color = items.STNodeType == null ? Color.FromArgb(this.ForeColor.A * 1 / 2, this.ForeColor) : this.ForeColor; + g.DrawString(items.Name, this.Font, m_brush, rect, m_sf); + g.ResetClip(); + return; + } + } + m_brush.Color = items.STNodeType == null ? Color.FromArgb(this.ForeColor.A * 2 / 3, this.ForeColor) : this.ForeColor; + g.DrawString(items.Name, this.Font, m_brush, rect, m_sf); + } + /// + /// 当绘制树节点图标时候发生 + /// + /// 绘制工具 + /// 当前需要绘制的集合 + /// 文本域所在矩形区域 + protected virtual void OnDrawItemIcon(DrawingTools dt, STNodeTreeCollection items, Rectangle rect) { + Graphics g = dt.Graphics; + if (items.STNodeType != null) { + m_pen.Color = this._AutoColor ? items.STNodeTypeColor : Color.DarkCyan; + m_brush.Color = Color.LightGray; + g.DrawRectangle(m_pen, rect.Left - 15, rect.Top + m_nItemHeight / 2 - 5, 11, 10); + g.FillRectangle(m_brush, rect.Left - 17, rect.Top + m_nItemHeight / 2 - 2, 5, 5); + g.FillRectangle(m_brush, rect.Left - 6, rect.Top + m_nItemHeight / 2 - 2, 5, 5); + if (m_item_hover == items && m_bHoverInfo) { + m_brush.Color = this.BackColor; + g.FillRectangle(m_brush, items.InfoRectangle); + } + m_pen.Color = this._AutoColor ? items.STNodeTypeColor : this._InfoButtonColor; + m_pen.Width = 2; + g.DrawLine(m_pen, items.InfoRectangle.X + 4, items.InfoRectangle.Y + 3, items.InfoRectangle.X + 10, items.InfoRectangle.Y + 3); + g.DrawLine(m_pen, items.InfoRectangle.X + 4, items.InfoRectangle.Y + 6, items.InfoRectangle.X + 10, items.InfoRectangle.Y + 6); + g.DrawLine(m_pen, items.InfoRectangle.X + 4, items.InfoRectangle.Y + 11, items.InfoRectangle.X + 10, items.InfoRectangle.Y + 11); + g.DrawLine(m_pen, items.InfoRectangle.X + 7, items.InfoRectangle.Y + 7, items.InfoRectangle.X + 7, items.InfoRectangle.Y + 10); + m_pen.Width = 1; + g.DrawRectangle(m_pen, items.InfoRectangle.X, items.InfoRectangle.Y, items.InfoRectangle.Width - 1, items.InfoRectangle.Height - 1); + } else { + if (items.IsLibraryRoot) { + Rectangle rect_box = new Rectangle(rect.Left - 15, rect.Top + m_nItemHeight / 2 - 5, 11, 10); + g.DrawRectangle(Pens.Gray, rect_box); + g.DrawLine(Pens.Cyan, rect_box.X - 2, rect_box.Top, rect_box.X + 2, rect_box.Top); + g.DrawLine(Pens.Cyan, rect_box.X, rect_box.Y - 2, rect_box.X, rect_box.Y + 2); + g.DrawLine(Pens.Cyan, rect_box.Right - 2, rect_box.Bottom, rect_box.Right + 2, rect_box.Bottom); + g.DrawLine(Pens.Cyan, rect_box.Right, rect_box.Bottom - 2, rect_box.Right, rect_box.Bottom + 2); + } else { + g.DrawRectangle(Pens.Goldenrod, new Rectangle(rect.Left - 16, rect.Top + m_nItemHeight / 2 - 6, 8, 3)); + g.DrawRectangle(Pens.Goldenrod, new Rectangle(rect.Left - 16, rect.Top + m_nItemHeight / 2 - 3, 13, 9)); + } + if (!this._ShowFolderCount) return; + m_sf.Alignment = StringAlignment.Far; + m_brush.Color = this._FolderCountColor; + rect.X -= 4; + g.DrawString("[" + items.STNodeCount.ToString() + "]", this.Font, m_brush, rect, m_sf); + m_sf.Alignment = StringAlignment.Near; + } + } + + #endregion + + #region public method ========== + /// + /// 在控件中检索STNode + /// + /// 需要检索的文本 + public void Search(string strText) { + if (strText == null) return; + if (strText.Trim() == string.Empty) return; + m_tbx.Text = strText.Trim(); + } + /// + /// 向控件中添加一个STNode类型 + /// + /// STNode类型 + /// 是否添加成功 + public bool AddNode(Type stNodeType) { return this.AddSTNode(stNodeType, m_items_source, null, true); } + /// + /// 从文件中向控件添加STNode类型 + /// + /// 指定文件路径 + /// 添加成功个数 + public int LoadAssembly(string strFile) { + strFile = System.IO.Path.GetFullPath(strFile); + var items = this.AddAssemblyPrivate(strFile); + if (items.STNodeCount == 0) return 0; + items.IsLibraryRoot = true; + m_items_source[items.Name] = items; + return items.STNodeCount; + } + /// + /// 清空控件中所有STNode类型 + /// + public void Clear() { + m_items_source.Clear(); + m_items_draw.Clear(); + m_dic_all_type.Clear(); + this.Invalidate(); + } + /// + /// 向控件中移除一个STNode类型 + /// + /// STNode类型 + /// 是否移除成功 + public bool RemoveNode(Type stNodeType) { + if (!m_dic_all_type.ContainsKey(stNodeType)) return false; + string strPath = m_dic_all_type[stNodeType]; + STNodeTreeCollection items = m_items_source; + if (!string.IsNullOrEmpty(strPath)) { + string[] strKeys = strPath.Split(m_chr_splitter); + for (int i = 0; i < strKeys.Length; i++) { + items = items[strKeys[i]]; + if (items == null) return false; + } + } + try { + STNode node = (STNode)Activator.CreateInstance(stNodeType); + if (items[node.Title] == null) return false; + items.Remove(node.Title, true); + m_dic_all_type.Remove(stNodeType); + } catch { return false; } + this.Invalidate(); + return true; + } + + #endregion + //================================================================================================= + /// + /// STNodeTreeView控件中每一项的集合 + /// + protected class STNodeTreeCollection : IEnumerable + { + private string _Name; + /// + /// 获取当前树节点显示名称 + /// + public string Name { + get { + return _Name; + } + } + /// + /// 获取当前树节点显示名称的小写字符串 + /// + public string NameLower { get; private set; } + /// + /// 获取当前树节点对应的STNode类型 + /// + public Type STNodeType { get; internal set; } + /// + /// 获取当前树节点的父级树节点 + /// + public STNodeTreeCollection Parent { get; internal set; } + + /// + /// 获取当前树节点下拥有的STNode类型个数 + /// + public int STNodeCount { get; internal set; } + /// + /// 获取当前树节点对应STNode类型在树控件中对应路径 + /// + public string Path { get; internal set; } + /// + /// 获取当前或设置树节点是否为打开状态 + /// + public bool IsOpen { get; set; } + /// + /// 获取当前树节点是否为加载模块的根路劲节点 + /// + public bool IsLibraryRoot { get; internal set; } + /// + /// 获取当前树节点在控件中的显示区域 + /// + public Rectangle DisplayRectangle { get; internal set; } + /// + /// 获取当前树节点在控件中的开关按钮区域 + /// + public Rectangle SwitchRectangle { get; internal set; } + /// + /// 获取当前树节点在控件中的信息按钮区域 + /// + public Rectangle InfoRectangle { get; internal set; } + /// + /// 获取当前树节点对应STNode类型的标题颜色 + /// + public Color STNodeTypeColor { get; internal set; } + /// + /// 获取当前树节点所包含子节点个数 + /// + public int Count { get { return m_dic.Count; } } + /// + /// 获取或设置指定名称的集合 + /// + /// 指定名称 + /// 集合 + public STNodeTreeCollection this[string strKey] { + get { + if (string.IsNullOrEmpty(strKey)) return null; + if (m_dic.ContainsKey(strKey)) return m_dic[strKey]; + return null; + } + set { + if (string.IsNullOrEmpty(strKey)) return; + if (value == null) return; + if (m_dic.ContainsKey(strKey)) { + m_dic[strKey] = value; + } else { + m_dic.Add(strKey, value); + } + value.Parent = this; + } + } + + private SortedDictionary m_dic = new SortedDictionary(); + /// + /// 构造一颗树节点集合 + /// + /// 当前树节点在控件中的显示名称 + public STNodeTreeCollection(string strName) { + if (strName == null || strName.Trim() == string.Empty) + throw new ArgumentNullException("显示名称不能为空"); + this._Name = strName.Trim(); + this.NameLower = this._Name.ToLower(); + } + /// + /// 向当前树节点中添加一个子节点 + /// + /// 节点显示名称 + /// 添加后的子节点集合 + public STNodeTreeCollection Add(string strName) { + if (!m_dic.ContainsKey(strName)) + m_dic.Add(strName, new STNodeTreeCollection(strName) { Parent = this }); + return m_dic[strName]; + } + /// + /// 向当前树节点中删除一个子集合 + /// + /// 子集合名称 + /// 是否递归向上自动清空无用节点 + /// 是否删除成功 + public bool Remove(string strName, bool isAutoDelFolder) { + if (!m_dic.ContainsKey(strName)) return false; + bool b = m_dic.Remove(strName); + var temp = this; + while (temp != null) { + temp.STNodeCount--; + temp = temp.Parent; + } + if (isAutoDelFolder && m_dic.Count == 0 && this.Parent != null) + return b && this.Parent.Remove(this.Name, isAutoDelFolder); + return b; + } + /// + /// 清空当前树节点中所有子节点 + /// + public void Clear() { this.Clear(this); } + + private void Clear(STNodeTreeCollection items) { + foreach (STNodeTreeCollection v in items) v.Clear(v); + m_dic.Clear(); + } + /// + /// 获取当前树节点中所有的名称数组 + /// + /// + public string[] GetKeys() { return m_dic.Keys.ToArray(); } + /// + /// 拷贝当前树节点集合中所有数据 + /// + /// 拷贝的副本 + public STNodeTreeCollection Copy() { + STNodeTreeCollection items = new STNodeTreeCollection("COPY"); + this.Copy(this, items); + return items; + } + + private void Copy(STNodeTreeCollection items_src, STNodeTreeCollection items_dst) { + foreach (STNodeTreeCollection v in items_src) { + this.Copy(v, items_dst.Add(v.Name)); + } + items_dst.Path = items_src.Path; + items_dst.STNodeType = items_src.STNodeType; + items_dst.IsLibraryRoot = items_src.IsLibraryRoot; + items_dst.STNodeCount = items_src.STNodeCount; + items_dst.STNodeTypeColor = items_src.STNodeTypeColor; + } + /// + /// 返回 System.Collections.IEnumerator 的 Array + /// + /// + public IEnumerator GetEnumerator() { + foreach (var v in m_dic.Values) yield return v; + } + + IEnumerator IEnumerable.GetEnumerator() { + return this.GetEnumerator(); + } + } + } +} diff --git a/ST.Library.UI/Properties/AssemblyInfo.cs b/ST.Library.UI/Properties/AssemblyInfo.cs old mode 100755 new mode 100644 diff --git a/ST.Library.UI/ST.Library.UI.csproj b/ST.Library.UI/ST.Library.UI.csproj old mode 100755 new mode 100644 index f847233..92a545a --- a/ST.Library.UI/ST.Library.UI.csproj +++ b/ST.Library.UI/ST.Library.UI.csproj @@ -21,6 +21,7 @@ DEBUG;TRACE prompt 4 + AnyCPU pdbonly @@ -35,24 +36,45 @@ + - - - - - - + + Form + + + Form + + + Component + + + Component + + + + Form + + + + + + + + Component + + + + + + + Component - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/WinNodeEditerTest/Demo_Image/STNodeImage.cs b/WinNodeEditerTest/Demo_Image/STNodeImage.cs deleted file mode 100755 index 43e7515..0000000 --- a/WinNodeEditerTest/Demo_Image/STNodeImage.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Drawing; - -namespace ST.Library.UI.Demo_Image -{ - public class STNodeImage : STNode - { - protected STNodeOption m_input_image; //输入输出点 - protected STNodeOption m_out_image; - - protected override void OnCreate() { - base.OnCreate(); - m_input_image = new STNodeOption("", typeof(Image), true); - this.InputOptions.Add(m_input_image); - m_out_image = new STNodeOption("", typeof(Image), false); - this.OutputOptions.Add(m_out_image); - m_input_image.DataTransfer += new STNodeOptionEventHandler(m_input_image_DataTransfer); - this.Title = "Image"; - } - //监听输入点接入事件 - void m_input_image_DataTransfer(object sender, STNodeOptionEventArgs e) { - if (e.Status != ConnectionStatus.Connected) - m_input_image.Data = null; - else - m_input_image.Data = e.TargetOption.Data; - m_out_image.TransferData(m_input_image.Data); //输出节点向下投递数据 - this.OnDataTransfer(); //通知子类 - this.Invalidate(); //重绘自己 - } - - protected override System.Drawing.Size OnBuildNodeSize(DrawingTools dt) { - //return base.OnBuildNodeSize(); - return new System.Drawing.Size(160, 120); //设定节点大小 - } - - protected override void OnDrawBody(DrawingTools dt) { //重绘节点主体部分 - base.OnDrawBody(dt); - Graphics g = dt.Graphics; - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; - if (m_input_image.Data != null) { - g.DrawImage((Image)m_input_image.Data, this.Left + 15, this.Top + 30, this.Width - 40, this.Height - 40); - } else { - g.FillRectangle(Brushes.Gray, this.Left + 15, this.Top + 30, this.Width - 40, this.Height - 40); - } - } - - protected virtual void OnDataTransfer() { } - } -} diff --git a/WinNodeEditerTest/Demo_Image/STNodeImageChannel.cs b/WinNodeEditerTest/Demo_Image/STNodeImageChannel.cs deleted file mode 100755 index 3708fd9..0000000 --- a/WinNodeEditerTest/Demo_Image/STNodeImageChannel.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Drawing; - -namespace ST.Library.UI.Demo_Image -{ - public class STNodeImageChannel : STNodeImage - { - private STNodeOption m_out_r; - private STNodeOption m_out_g; - private STNodeOption m_out_b; - - private Bitmap m_img_r; - private Bitmap m_img_g; - private Bitmap m_img_b; - - protected override void OnCreate() { - base.OnCreate(); - m_out_r = new STNodeOption("R", typeof(Image), false); - m_out_g = new STNodeOption("G", typeof(Image), false); - m_out_b = new STNodeOption("B", typeof(Image), false); - this.OutputOptions.Add(m_out_r); - this.OutputOptions.Add(m_out_g); - this.OutputOptions.Add(m_out_b); - this.Title = "Channel"; - } - - protected override void OnDataTransfer() { - base.OnDataTransfer(); - if (m_img_r != null) { - m_img_r.Dispose(); - m_img_g.Dispose(); - m_img_b.Dispose(); - m_img_r = m_img_g = m_img_b = null; - } - if (m_out_image.Data != null) { //分离通道 Demo 演示 Get/SetPixel() 效率极低 应当LockBitmap操作 - Bitmap img = (Bitmap)base.m_input_image.Data; - m_img_r = new Bitmap(img.Width, img.Height); - m_img_g = new Bitmap(img.Width, img.Height); - m_img_b = new Bitmap(img.Width, img.Height); - for (int x = 0; x < img.Width; x++) { - for (int y = 0; y < img.Height; y++) { - Color clr = img.GetPixel(x, y); - m_img_r.SetPixel(x, y, Color.FromArgb(255, clr.R, clr.R, clr.R)); - m_img_g.SetPixel(x, y, Color.FromArgb(255, clr.G, clr.G, clr.G)); - m_img_b.SetPixel(x, y, Color.FromArgb(255, clr.B, clr.B, clr.B)); - } - } - } - m_out_r.TransferData(m_img_r); - m_out_g.TransferData(m_img_b); - m_out_b.TransferData(m_img_b); - } - } -} diff --git a/WinNodeEditerTest/Demo_Image/STNodeImageInput.cs b/WinNodeEditerTest/Demo_Image/STNodeImageInput.cs deleted file mode 100755 index 940e237..0000000 --- a/WinNodeEditerTest/Demo_Image/STNodeImageInput.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Drawing; - -using System.Windows.Forms; - -namespace ST.Library.UI.Demo_Image -{ - public class STNodeImageInput : STNode - { - private STNodeOption m_option_out; - - private string m_str_file; - private Size m_sz = new Size(100, 60); - - protected override void OnCreate() { - base.OnCreate(); - this.Title = "ImageInput"; - m_option_out = new STNodeOption("", typeof(Image), false); - this.OutputOptions.Add(m_option_out); - STNodeButton btn = new STNodeButton(); - btn.Left = (m_sz.Width - btn.Width) / 2; - btn.Top = (m_sz.Height - 20 - btn.Height) / 2; - btn.Text = "OpenImage"; - btn.MouseClick += new MouseEventHandler(btn_MouseClick); - this.Controls.Add(btn); - } - - void btn_MouseClick(object sender, MouseEventArgs e) { - OpenFileDialog ofd = new OpenFileDialog(); - ofd.Filter = "*.jpg|*.jpg|*.png|*.png|*.bmp|*.bmp|*.*|*.*"; - if (ofd.ShowDialog() != DialogResult.OK) return; - m_option_out.TransferData(Image.FromFile(ofd.FileName)); - m_str_file = ofd.FileName; - } - - protected override Size OnBuildNodeSize(DrawingTools dt) { - //return base.OnBuildNodeSize(); - return m_sz; - } - - protected override Point OnSetOptionDotLocation(STNodeOption op, Point pt) { - return new Point(pt.X, pt.Y + 10); - //return base.OnSetOptionLocation(op); - } - - //protected override void OnDrawOptionDot(DrawingTools dt, STNodeOption op) { - // //if (op == m_option_out) op.DotTop = this.Top + 35; - // base.OnDrawOptionDot(dt, op); - //} - - protected override void OnSaveNode(Dictionary dic) { - if (m_str_file == null) m_str_file = string.Empty; - dic.Add("file", Encoding.UTF8.GetBytes(m_str_file)); - } - - protected override void OnLoadNode(Dictionary dic) { - base.OnLoadNode(dic); - m_str_file = Encoding.UTF8.GetString(dic["file"]); - if (System.IO.File.Exists(m_str_file)) { //如果文件存在加载并投递数据 - m_option_out.TransferData(Image.FromFile(m_str_file)); - } - } - - public class STNodeButton : STNodeControl //自定义一个Button控件 - { - private bool m_isHover; - - protected override void OnMouseEnter(EventArgs e) { - base.OnMouseEnter(e); - m_isHover = true; - this.Invalidate(); - } - - protected override void OnMouseLeave(EventArgs e) { - base.OnMouseLeave(e); - m_isHover = false; - this.Invalidate(); - } - - protected override void OnPaint(DrawingTools dt) { - //base.OnPaint(dt); - Graphics g = dt.Graphics; - SolidBrush brush = dt.SolidBrush; - brush.Color = m_isHover ? Color.DodgerBlue : this.BackColor; - g.FillRectangle(brush, 0, 0, this.Width, this.Height); - g.DrawString(this.Text, this.Font, Brushes.White, this.ClientRectangle, base.m_sf); - } - } - } -} diff --git a/WinNodeEditerTest/Form1.Designer.cs b/WinNodeEditerTest/Form1.Designer.cs deleted file mode 100755 index f1ed28a..0000000 --- a/WinNodeEditerTest/Form1.Designer.cs +++ /dev/null @@ -1,100 +0,0 @@ -namespace ST.Library.UI -{ - partial class Form1 - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) { - if (disposing && (components != null)) { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() { - this.button1 = new System.Windows.Forms.Button(); - this.button2 = new System.Windows.Forms.Button(); - this.button3 = new System.Windows.Forms.Button(); - this.stNodeEditor1 = new ST.Library.UI.STNodeEditor(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(0, 0); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 4; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // button2 - // - this.button2.Location = new System.Drawing.Point(0, 29); - this.button2.Name = "button2"; - this.button2.Size = new System.Drawing.Size(75, 23); - this.button2.TabIndex = 5; - this.button2.Text = "button2"; - this.button2.UseVisualStyleBackColor = true; - this.button2.Click += new System.EventHandler(this.button2_Click); - // - // button3 - // - this.button3.Location = new System.Drawing.Point(0, 58); - this.button3.Name = "button3"; - this.button3.Size = new System.Drawing.Size(75, 23); - this.button3.TabIndex = 6; - this.button3.Text = "button3"; - this.button3.UseVisualStyleBackColor = true; - this.button3.Click += new System.EventHandler(this.button3_Click); - // - // stNodeEditor1 - // - this.stNodeEditor1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(34)))), ((int)(((byte)(34)))), ((int)(((byte)(34))))); - this.stNodeEditor1.Curvature = 0.3F; - this.stNodeEditor1.Location = new System.Drawing.Point(0, 0); - this.stNodeEditor1.LocationBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); - this.stNodeEditor1.MarkBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(180)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); - this.stNodeEditor1.MarkForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(180)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); - this.stNodeEditor1.Name = "stNodeEditor1"; - this.stNodeEditor1.Size = new System.Drawing.Size(200, 200); - this.stNodeEditor1.TabIndex = 3; - this.stNodeEditor1.Text = "stNodeEditor1"; - // - // Form1 - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(292, 273); - this.Controls.Add(this.button3); - this.Controls.Add(this.button2); - this.Controls.Add(this.button1); - this.Controls.Add(this.stNodeEditor1); - this.Name = "Form1"; - this.Text = "Form1"; - this.Load += new System.EventHandler(this.Form1_Load); - this.ResumeLayout(false); - - } - - #endregion - - private STNodeEditor stNodeEditor1; - private System.Windows.Forms.Button button1; - private System.Windows.Forms.Button button2; - private System.Windows.Forms.Button button3; - } -} \ No newline at end of file diff --git a/WinNodeEditerTest/Form1.cs b/WinNodeEditerTest/Form1.cs deleted file mode 100755 index 2bdce0f..0000000 --- a/WinNodeEditerTest/Form1.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Windows.Forms; - -using ST.Library.UI; -using System.IO; - -namespace ST.Library.UI -{ - public partial class Form1 : Form - { - public Form1() { - InitializeComponent(); - button1.Text = "Lock"; - button2.Text = "Save"; - button3.Text = "Open"; - } - - private void Form1_Load(object sender, EventArgs e) { - stNodeEditor1.Dock = DockStyle.Fill; - stNodeEditor1.TypeColor.Add(typeof(string), Color.Yellow); - stNodeEditor1.TypeColor.Add(typeof(Image), Color.Red); - - stNodeEditor1.SelectedChanged += new EventHandler(stNodeEditor1_SelectedChanged); - stNodeEditor1.OptionConnected += new STNodeEditorOptionEventHandler(stNodeEditor1_OptionConnected); - stNodeEditor1.CanvasScaled += new EventHandler(stNodeEditor1_CanvasScaled); - for (int i = 0; i < 4; i++) { - STNode node = new DemoNode(); - //if (i == 2) - // node.Mark = "这里是标记信息\r\n\t新的一行数据"; - //else - // node.Mark = "this is mark Info\r\nthis is new line " + i; - stNodeEditor1.Nodes.Add(node); - } - stNodeEditor1.Nodes.Add(new STNodeHub()); - stNodeEditor1.Nodes.Add(new STNodeHub()); - - stNodeEditor1.Nodes.Add(new NodeNumberAdd()); - - stNodeEditor1.LoadAssembly(Application.ExecutablePath); - stNodeEditor1.LoadAssembly(Directory.GetFiles("./", "*.dll")); - } - - void stNodeEditor1_CanvasScaled(object sender, EventArgs e) { - stNodeEditor1.ShowAlert(stNodeEditor1.CanvasScale.ToString("F2"), Color.White, Color.Black); - } - - void stNodeEditor1_OptionConnected(object sender, STNodeEditorOptionEventArgs e) { - Console.WriteLine(e.CurrentOption.Text + " - " + e.TargetOption.Text + " - " + e.Status); - } - - void stNodeEditor1_SelectedChanged(object sender, EventArgs e) { - foreach (var v in stNodeEditor1.GetSelectedNode()) { - Console.WriteLine("Selected - " + v.Title); - } - } - - private void button1_Click(object sender, EventArgs e) { - stNodeEditor1.Nodes[0].LockOption = !stNodeEditor1.Nodes[0].LockOption; - stNodeEditor1.Nodes[1].LockOption = !stNodeEditor1.Nodes[1].LockOption; - - - stNodeEditor1.Nodes[0].LockLocation = !stNodeEditor1.Nodes[0].LockLocation; - stNodeEditor1.Nodes[1].LockLocation = !stNodeEditor1.Nodes[1].LockLocation; - } - - private void button2_Click(object sender, EventArgs e) { - SaveFileDialog sfd = new SaveFileDialog(); - sfd.Filter = "*.stn|*.stn"; - if (sfd.ShowDialog() != DialogResult.OK) return; - stNodeEditor1.SaveCanvas(sfd.FileName); - } - - private void button3_Click(object sender, EventArgs e) { - OpenFileDialog ofd = new OpenFileDialog(); - ofd.Filter = "*.stn|*.stn"; - if (ofd.ShowDialog() != DialogResult.OK) return; - stNodeEditor1.Nodes.Clear(); - stNodeEditor1.LoadCanvas(ofd.FileName); - } - } -} diff --git a/WinNodeEditerTest/Form2.Designer.cs b/WinNodeEditerTest/Form2.Designer.cs deleted file mode 100755 index 5392566..0000000 --- a/WinNodeEditerTest/Form2.Designer.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace ST.Library.UI -{ - partial class Form2 - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) { - if (disposing && (components != null)) { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() { - this.button1 = new System.Windows.Forms.Button(); - this.button2 = new System.Windows.Forms.Button(); - this.button3 = new System.Windows.Forms.Button(); - this.button4 = new System.Windows.Forms.Button(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(0, 0); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 0; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // button2 - // - this.button2.Location = new System.Drawing.Point(0, 29); - this.button2.Name = "button2"; - this.button2.Size = new System.Drawing.Size(75, 23); - this.button2.TabIndex = 1; - this.button2.Text = "button2"; - this.button2.UseVisualStyleBackColor = true; - this.button2.Click += new System.EventHandler(this.button2_Click); - // - // button3 - // - this.button3.Location = new System.Drawing.Point(0, 58); - this.button3.Name = "button3"; - this.button3.Size = new System.Drawing.Size(75, 23); - this.button3.TabIndex = 2; - this.button3.Text = "button3"; - this.button3.UseVisualStyleBackColor = true; - this.button3.Click += new System.EventHandler(this.button3_Click); - // - // button4 - // - this.button4.Location = new System.Drawing.Point(0, 87); - this.button4.Name = "button4"; - this.button4.Size = new System.Drawing.Size(75, 23); - this.button4.TabIndex = 3; - this.button4.Text = "button4"; - this.button4.UseVisualStyleBackColor = true; - this.button4.Click += new System.EventHandler(this.button4_Click); - // - // Form2 - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(292, 273); - this.Controls.Add(this.button4); - this.Controls.Add(this.button3); - this.Controls.Add(this.button2); - this.Controls.Add(this.button1); - this.Name = "Form2"; - this.Text = "Form2"; - this.ResumeLayout(false); - - } - - #endregion - - private System.Windows.Forms.Button button1; - private System.Windows.Forms.Button button2; - private System.Windows.Forms.Button button3; - private System.Windows.Forms.Button button4; - } -} \ No newline at end of file diff --git a/WinNodeEditerTest/Form2.cs b/WinNodeEditerTest/Form2.cs deleted file mode 100755 index c8457af..0000000 --- a/WinNodeEditerTest/Form2.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Windows.Forms; - -namespace ST.Library.UI -{ - public partial class Form2 : Form - { - public Form2() { - InitializeComponent(); - } - - Graphics m_g; - - private void button1_Click(object sender, EventArgs e) { - m_g = this.CreateGraphics(); - } - - private void button2_Click(object sender, EventArgs e) { - m_g.DrawRectangle(Pens.Red, 10, 10, 30, 30); - } - - private void button3_Click(object sender, EventArgs e) { - m_g.DrawRectangle(Pens.Yellow, 45, 45, 20, 20); - } - - private void button4_Click(object sender, EventArgs e) { - this.CreateGraphics().FillRectangle(Brushes.Black, 20, 20, 20, 20); - } - } -} diff --git a/WinNodeEditerTest/Form2.resx b/WinNodeEditerTest/Form2.resx deleted file mode 100755 index 29dcb1b..0000000 --- a/WinNodeEditerTest/Form2.resx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/WinNodeEditerTest/NodeNumberAdd.cs b/WinNodeEditerTest/NodeNumberAdd.cs deleted file mode 100755 index 7d30aa9..0000000 --- a/WinNodeEditerTest/NodeNumberAdd.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Drawing; - -namespace ST.Library.UI -{ - public class NodeNumberAdd : STNode - { - private STNodeOption m_in_num1; - private STNodeOption m_in_num2; - private STNodeOption m_out_num; - private int m_nNum1, m_nNum2; - - protected override void OnCreate() { - base.OnCreate(); - this.Title = "NumberAdd"; - m_in_num1 = new STNodeOption("num1", typeof(int), true);//只能有一个连线 - m_in_num2 = new STNodeOption("num2", typeof(int), true);//只能有一个连线 - m_out_num = new STNodeOption("result", typeof(int), false);//可以多个连线 - this.InputOptions.Add(m_in_num1); - this.InputOptions.Add(m_in_num2); - this.OutputOptions.Add(m_out_num); - m_in_num1.DataTransfer += new STNodeOptionEventHandler(m_in_num_DataTransfer); - m_in_num2.DataTransfer += new STNodeOptionEventHandler(m_in_num_DataTransfer); - } - //当有数据传入时 - void m_in_num_DataTransfer(object sender, STNodeOptionEventArgs e) { - //判断连线是否是连接状态(建立连线 断开连线 都会触发该事件) - if (e.Status == ConnectionStatus.Connected && e.TargetOption.Data != null) { - if (sender == m_in_num1) - m_nNum1 = (int)e.TargetOption.Data;//TargetOption为触发此事件的Option - else - m_nNum2 = (int)e.TargetOption.Data; - } else { - if (sender == m_in_num1) m_nNum1 = 0; else m_nNum2 = 0; - } - //向输出选项上的所有连线传输数据 输出选项上的所有连线都会触发 DataTransfer 事件 - m_out_num.TransferData(m_nNum1 + m_nNum2); //m_out_num.Data 将被自动设置 - } - - protected override void OnOwnerChanged() { - base.OnOwnerChanged();//通常刚被添加到节点编辑器时触发 如是以插件方式提供的节点 应当向编辑器提交数据类型颜色 - if (this.Owner == null) return; //或者通过m_in_num1.DotColor = Color.Red;进行设置 - this.Owner.SetTypeColor(typeof(int), Color.Red); - } - } -} diff --git a/WinNodeEditerTest/app.config b/WinNodeEditerTest/app.config deleted file mode 100755 index cb2586b..0000000 --- a/WinNodeEditerTest/app.config +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/WinNodeEditorDemo/AttrTestNode.cs b/WinNodeEditorDemo/AttrTestNode.cs new file mode 100644 index 0000000..b9c7fd2 --- /dev/null +++ b/WinNodeEditorDemo/AttrTestNode.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; +using System.Windows.Forms; + +namespace WinNodeEditorDemo +{ + [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) { + 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(MouseEventArgs e) { + //如果用户点击在 颜色预览区域 则弹出系统颜色对话框 + if (m_rect.Contains(e.Location)) { + ColorDialog cd = new ColorDialog(); + if (cd.ShowDialog() != DialogResult.OK) return; + this.SetValue(cd.Color, null); + this.Invalidate(); + return; + } + //否则其他区域将采用默认处理方式 弹出字符串输入框 + base.OnMouseClick(e); + } + } +} diff --git a/WinNodeEditorDemo/Blender/BlenderMixColorNode.cs b/WinNodeEditorDemo/Blender/BlenderMixColorNode.cs new file mode 100644 index 0000000..8de3399 --- /dev/null +++ b/WinNodeEditorDemo/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/WinNodeEditorDemo/Blender/FrmEnumSelect.cs b/WinNodeEditorDemo/Blender/FrmEnumSelect.cs new file mode 100644 index 0000000..3c8e87f --- /dev/null +++ b/WinNodeEditorDemo/Blender/FrmEnumSelect.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; +using System.Windows.Forms; + +namespace WinNodeEditorDemo.Blender +{ + /// + /// 此类仅演示 作为MixRGB节点的下拉选择框弹出菜单 + /// + public class FrmEnumSelect : Form + { + private Point m_pt; + private int m_nWidth; + private float m_scale; + private List m_lst = new List(); + private StringFormat m_sf; + + public Enum Enum { get; set; } + + private bool m_bClosed; + + public FrmEnumSelect(Enum e, Point pt, int nWidth, float scale) { + this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); + this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + foreach (var v in Enum.GetValues(e.GetType())) m_lst.Add(v); + this.Enum = e; + m_pt = pt; + m_scale = scale; + m_nWidth = nWidth; + m_sf = new StringFormat(); + m_sf.LineAlignment = StringAlignment.Center; + + this.ShowInTaskbar = false; + this.BackColor = Color.FromArgb(255, 34, 34, 34); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + } + + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + this.Location = m_pt; + this.Width = (int)(m_nWidth * m_scale); + this.Height = (int)(m_lst.Count * 20 * m_scale); + } + + protected override void OnPaint(PaintEventArgs e) { + base.OnPaint(e); + Graphics g = e.Graphics; + g.ScaleTransform(m_scale, m_scale); + Rectangle rect = new Rectangle(0, 0, this.Width, 20); + foreach (var v in m_lst) { + g.DrawString(v.ToString(), this.Font, Brushes.White, rect, m_sf); + rect.Y += rect.Height; + } + } + + protected override void OnMouseClick(MouseEventArgs e) { + base.OnMouseClick(e); + int nIndex = e.Y / (int)(20 * m_scale); + if (nIndex >= 0 && nIndex < m_lst.Count) this.Enum = (Enum)m_lst[nIndex]; + this.DialogResult = System.Windows.Forms.DialogResult.OK; + m_bClosed = true; + } + + protected override void OnMouseLeave(EventArgs e) { + base.OnMouseLeave(e); + if (m_bClosed) return; + //this.DialogResult = System.Windows.Forms.DialogResult.None; + this.Close(); + } + } +} diff --git a/WinNodeEditorDemo/Blender/STNodeCheckBox.cs b/WinNodeEditorDemo/Blender/STNodeCheckBox.cs new file mode 100644 index 0000000..db3bfeb --- /dev/null +++ b/WinNodeEditorDemo/Blender/STNodeCheckBox.cs @@ -0,0 +1,48 @@ +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(System.Windows.Forms.MouseEventArgs 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); + m_sf.Alignment = StringAlignment.Near; + 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/WinNodeEditorDemo/Blender/STNodeColorButton.cs b/WinNodeEditorDemo/Blender/STNodeColorButton.cs new file mode 100644 index 0000000..1d7aa70 --- /dev/null +++ b/WinNodeEditorDemo/Blender/STNodeColorButton.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using System.Drawing; +using System.Windows.Forms; +using ST.Library.UI.NodeEditor; + +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(System.Windows.Forms.MouseEventArgs e) { + base.OnMouseClick(e); + ColorDialog cd = new ColorDialog(); + if (cd.ShowDialog() != DialogResult.OK) return; + //this._Color = cd.Color; + this.BackColor = cd.Color; + this.OnValueChanged(new EventArgs()); + } + } +} diff --git a/WinNodeEditorDemo/Blender/STNodeProgress.cs b/WinNodeEditorDemo/Blender/STNodeProgress.cs new file mode 100644 index 0000000..5fa5587 --- /dev/null +++ b/WinNodeEditorDemo/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(System.Windows.Forms.MouseEventArgs e) { + base.OnMouseDown(e); + m_bMouseDown = true; + } + + protected override void OnMouseUp(System.Windows.Forms.MouseEventArgs e) { + base.OnMouseUp(e); + m_bMouseDown = false; + } + + protected override void OnMouseMove(System.Windows.Forms.MouseEventArgs 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/WinNodeEditorDemo/Blender/STNodeSelectBox.cs b/WinNodeEditorDemo/Blender/STNodeSelectBox.cs new file mode 100644 index 0000000..9b36e87 --- /dev/null +++ b/WinNodeEditorDemo/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(System.Windows.Forms.MouseEventArgs 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 != System.Windows.Forms.DialogResult.OK) return; + this.Enum = frm.Enum; + this.OnValueChanged(new EventArgs()); + } + } +} diff --git a/WinNodeEditorDemo/CalcNode.cs b/WinNodeEditorDemo/CalcNode.cs new file mode 100644 index 0000000..01fe541 --- /dev/null +++ b/WinNodeEditorDemo/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.Forms.MessageBox.Show(((STNodeControl)s).Text); + } + + this.OutputOptions.Add("Result", typeof(int), false); + } + } +} diff --git a/WinNodeEditorDemo/EmptyOptionTestNode.cs b/WinNodeEditorDemo/EmptyOptionTestNode.cs new file mode 100644 index 0000000..1c0449e --- /dev/null +++ b/WinNodeEditorDemo/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/WinNodeEditorDemo/Form1.Designer.cs b/WinNodeEditorDemo/Form1.Designer.cs new file mode 100644 index 0000000..76a8644 --- /dev/null +++ b/WinNodeEditorDemo/Form1.Designer.cs @@ -0,0 +1,173 @@ +namespace WinNodeEditorDemo +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) { + if (disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.components = new System.ComponentModel.Container(); + this.btn_open = new System.Windows.Forms.Button(); + this.btn_save = new System.Windows.Forms.Button(); + this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); + this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.lockLocationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.lockConnectionToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.stNodePropertyGrid1 = new ST.Library.UI.NodeEditor.STNodePropertyGrid(); + this.stNodeEditor1 = new ST.Library.UI.NodeEditor.STNodeEditor(); + this.stNodeTreeView1 = new ST.Library.UI.NodeEditor.STNodeTreeView(); + this.contextMenuStrip1.SuspendLayout(); + this.SuspendLayout(); + // + // btn_open + // + this.btn_open.Location = new System.Drawing.Point(12, 3); + this.btn_open.Name = "btn_open"; + this.btn_open.Size = new System.Drawing.Size(75, 23); + this.btn_open.TabIndex = 3; + this.btn_open.Text = "&Open"; + this.btn_open.UseVisualStyleBackColor = true; + this.btn_open.Click += new System.EventHandler(this.btn_open_Click); + // + // btn_save + // + this.btn_save.Location = new System.Drawing.Point(93, 3); + this.btn_save.Name = "btn_save"; + this.btn_save.Size = new System.Drawing.Size(75, 23); + this.btn_save.TabIndex = 4; + this.btn_save.Text = "&Save"; + this.btn_save.UseVisualStyleBackColor = true; + this.btn_save.Click += new System.EventHandler(this.btn_save_Click); + // + // contextMenuStrip1 + // + this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.removeToolStripMenuItem, + this.lockLocationToolStripMenuItem, + this.lockConnectionToolStripMenuItem}); + this.contextMenuStrip1.Name = "contextMenuStrip1"; + this.contextMenuStrip1.Size = new System.Drawing.Size(164, 70); + // + // removeToolStripMenuItem + // + this.removeToolStripMenuItem.Name = "removeToolStripMenuItem"; + this.removeToolStripMenuItem.Size = new System.Drawing.Size(163, 22); + this.removeToolStripMenuItem.Text = "&Remove"; + this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click); + // + // lockLocationToolStripMenuItem + // + this.lockLocationToolStripMenuItem.Name = "lockLocationToolStripMenuItem"; + this.lockLocationToolStripMenuItem.Size = new System.Drawing.Size(163, 22); + this.lockLocationToolStripMenuItem.Text = "U/Lock &Location"; + this.lockLocationToolStripMenuItem.Click += new System.EventHandler(this.lockLocationToolStripMenuItem_Click); + // + // lockConnectionToolStripMenuItem + // + this.lockConnectionToolStripMenuItem.Name = "lockConnectionToolStripMenuItem"; + this.lockConnectionToolStripMenuItem.Size = new System.Drawing.Size(163, 22); + this.lockConnectionToolStripMenuItem.Text = "U/Lock &Connection"; + this.lockConnectionToolStripMenuItem.Click += new System.EventHandler(this.lockConnectionToolStripMenuItem_Click); + // + // stNodePropertyGrid1 + // + this.stNodePropertyGrid1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(35)))), ((int)(((byte)(35)))), ((int)(((byte)(35))))); + this.stNodePropertyGrid1.DescriptionColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(184)))), ((int)(((byte)(134)))), ((int)(((byte)(11))))); + this.stNodePropertyGrid1.ErrorColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(165)))), ((int)(((byte)(42)))), ((int)(((byte)(42))))); + this.stNodePropertyGrid1.ForeColor = System.Drawing.Color.White; + this.stNodePropertyGrid1.ItemHoverColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(125)))), ((int)(((byte)(125)))), ((int)(((byte)(125))))); + this.stNodePropertyGrid1.ItemValueBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(80)))), ((int)(((byte)(80)))), ((int)(((byte)(80))))); + this.stNodePropertyGrid1.Location = new System.Drawing.Point(12, 273); + this.stNodePropertyGrid1.MinimumSize = new System.Drawing.Size(120, 50); + this.stNodePropertyGrid1.Name = "stNodePropertyGrid1"; + this.stNodePropertyGrid1.ShowTitle = true; + this.stNodePropertyGrid1.Size = new System.Drawing.Size(200, 228); + this.stNodePropertyGrid1.TabIndex = 2; + this.stNodePropertyGrid1.Text = "stNodePropertyGrid1"; + this.stNodePropertyGrid1.TitleColor = System.Drawing.Color.FromArgb(((int)(((byte)(127)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + // + // stNodeEditor1 + // + this.stNodeEditor1.AllowDrop = true; + this.stNodeEditor1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(34)))), ((int)(((byte)(34)))), ((int)(((byte)(34))))); + this.stNodeEditor1.Curvature = 0.3F; + this.stNodeEditor1.Location = new System.Drawing.Point(218, 32); + this.stNodeEditor1.LocationBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.stNodeEditor1.MarkBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(180)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.stNodeEditor1.MarkForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(180)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.stNodeEditor1.MinimumSize = new System.Drawing.Size(100, 100); + this.stNodeEditor1.Name = "stNodeEditor1"; + this.stNodeEditor1.Size = new System.Drawing.Size(432, 469); + this.stNodeEditor1.TabIndex = 1; + this.stNodeEditor1.Text = "stNodeEditor1"; + // + // stNodeTreeView1 + // + this.stNodeTreeView1.AllowDrop = true; + this.stNodeTreeView1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(35)))), ((int)(((byte)(35)))), ((int)(((byte)(35))))); + this.stNodeTreeView1.FolderCountColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(255)))), ((int)(((byte)(255)))), ((int)(((byte)(255))))); + this.stNodeTreeView1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(220)))), ((int)(((byte)(220))))); + this.stNodeTreeView1.ItemBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(45))))); + this.stNodeTreeView1.ItemHoverColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(125)))), ((int)(((byte)(125)))), ((int)(((byte)(125))))); + this.stNodeTreeView1.Location = new System.Drawing.Point(12, 32); + this.stNodeTreeView1.MinimumSize = new System.Drawing.Size(100, 60); + this.stNodeTreeView1.Name = "stNodeTreeView1"; + this.stNodeTreeView1.ShowFolderCount = true; + this.stNodeTreeView1.Size = new System.Drawing.Size(200, 235); + this.stNodeTreeView1.TabIndex = 0; + this.stNodeTreeView1.Text = "stNodeTreeView1"; + this.stNodeTreeView1.TextBoxColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(30)))), ((int)(((byte)(30))))); + this.stNodeTreeView1.TitleColor = System.Drawing.Color.FromArgb(((int)(((byte)(60)))), ((int)(((byte)(60)))), ((int)(((byte)(60))))); + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(662, 513); + this.Controls.Add(this.btn_save); + this.Controls.Add(this.btn_open); + this.Controls.Add(this.stNodePropertyGrid1); + this.Controls.Add(this.stNodeEditor1); + this.Controls.Add(this.stNodeTreeView1); + this.Name = "Form1"; + this.Text = "Form1"; + this.Load += new System.EventHandler(this.Form1_Load); + this.contextMenuStrip1.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private ST.Library.UI.NodeEditor.STNodeTreeView stNodeTreeView1; + private ST.Library.UI.NodeEditor.STNodeEditor stNodeEditor1; + private ST.Library.UI.NodeEditor.STNodePropertyGrid stNodePropertyGrid1; + private System.Windows.Forms.Button btn_open; + private System.Windows.Forms.Button btn_save; + private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; + private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem lockLocationToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem lockConnectionToolStripMenuItem; + + } +} + diff --git a/WinNodeEditorDemo/Form1.cs b/WinNodeEditorDemo/Form1.cs new file mode 100644 index 0000000..1fd3c49 --- /dev/null +++ b/WinNodeEditorDemo/Form1.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Windows.Forms; +using System.IO; +using ST.Library.UI.NodeEditor; + +namespace WinNodeEditorDemo +{ + public partial class Form1 : Form + { + public Form1() { + InitializeComponent(); + this.StartPosition = FormStartPosition.CenterScreen; + } + + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + stNodePropertyGrid1.Text = "Node_Property"; + stNodeTreeView1.LoadAssembly(Application.ExecutablePath); + stNodeEditor1.LoadAssembly(Application.ExecutablePath); + + stNodeEditor1.ActiveChanged += (s, ea) => stNodePropertyGrid1.SetNode(stNodeEditor1.ActiveNode); + //stNodeEditor1.SelectedChanged += (s, ea) => stNodePropertyGrid1.SetSTNode(stNodeEditor1.ActiveNode); + stNodeEditor1.OptionConnected += (s, ea) => stNodeEditor1.ShowAlert(ea.Status.ToString(), Color.White, ea.Status == ConnectionStatus.Connected ? Color.FromArgb(125, Color.Green) : Color.FromArgb(125, Color.Red)); + stNodeEditor1.CanvasScaled += (s, ea) => stNodeEditor1.ShowAlert(stNodeEditor1.CanvasScale.ToString("F2"), Color.White, Color.FromArgb(125, Color.Yellow)); + stNodeEditor1.NodeAdded += (s, ea) => ea.Node.ContextMenuStrip = contextMenuStrip1; + + stNodePropertyGrid1.SetInfoKey("Author", "Mail", "Link", "Show Help"); + stNodeTreeView1.PropertyGrid.SetInfoKey("Author", "Mail", "Link", "Show Help"); + + stNodeEditor1.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom; + + contextMenuStrip1.ShowImageMargin = false; + contextMenuStrip1.Renderer = new ToolStripRendererEx(); + } + + private void Form1_Load(object sender, EventArgs e) { + //int nLines = 0; + //foreach (var v in Directory.GetFiles("../../../", "*.cs", SearchOption.AllDirectories)) { + // nLines += File.ReadAllLines(v).Length; + //} + //MessageBox.Show(nLines.ToString()); + //this.Resize += (s, ea) => this.Text = this.Size.ToString(); + //this.BeginInvoke(new MethodInvoker(() => { + // //this.Size = new Size(488, 306); + // this.Size = new Size(488, 246); + // stNodeTreeView1.Visible = false; + // stNodePropertyGrid1.Top = stNodeEditor1.Top; + // stNodePropertyGrid1.Height = stNodeEditor1.Height; + // stNodeTreeView1.Height = stNodeEditor1.Height; + //})); + } + + private void btn_open_Click(object sender, EventArgs e) { + OpenFileDialog ofd = new OpenFileDialog(); + ofd.Filter = "*.stn|*.stn"; + if (ofd.ShowDialog() != DialogResult.OK) return; + stNodeEditor1.Nodes.Clear(); + stNodeEditor1.LoadCanvas(ofd.FileName); + } + + private void btn_save_Click(object sender, EventArgs e) { + SaveFileDialog sfd = new SaveFileDialog(); + sfd.Filter = "*.stn|*.stn"; + if (sfd.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; + stNodeEditor1.SaveCanvas(sfd.FileName); + } + + private void lockConnectionToolStripMenuItem_Click(object sender, EventArgs e) { + stNodeEditor1.ActiveNode.LockOption = !stNodeEditor1.ActiveNode.LockOption; + } + + private void lockLocationToolStripMenuItem_Click(object sender, EventArgs e) { + if (stNodeEditor1.ActiveNode == null) return; + stNodeEditor1.ActiveNode.LockLocation = !stNodeEditor1.ActiveNode.LockLocation; + } + + private void removeToolStripMenuItem_Click(object sender, EventArgs e) { + if (stNodeEditor1.ActiveNode == null) return; + stNodeEditor1.Nodes.Remove(stNodeEditor1.ActiveNode); + } + } +} diff --git a/WinNodeEditerTest/Form1.resx b/WinNodeEditorDemo/Form1.resx old mode 100755 new mode 100644 similarity index 94% rename from WinNodeEditerTest/Form1.resx rename to WinNodeEditorDemo/Form1.resx index 29dcb1b..73b0127 --- a/WinNodeEditerTest/Form1.resx +++ b/WinNodeEditorDemo/Form1.resx @@ -1,120 +1,123 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + \ No newline at end of file diff --git a/WinNodeEditorDemo/ImageNode/ImageBaseNode.cs b/WinNodeEditorDemo/ImageNode/ImageBaseNode.cs new file mode 100644 index 0000000..54010d1 --- /dev/null +++ b/WinNodeEditorDemo/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/WinNodeEditorDemo/ImageNode/ImageChannelNode.cs b/WinNodeEditorDemo/ImageNode/ImageChannelNode.cs new file mode 100644 index 0000000..15fbbea --- /dev/null +++ b/WinNodeEditorDemo/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); + Graphics g = dt.Graphics; + Rectangle rect = new Rectangle(this.Left + 10, this.Top + 30, 120, 80); + g.FillRectangle(Brushes.Gray, rect); + if (m_img_draw != null) g.DrawImage(m_img_draw, rect); + } + } +} diff --git a/WinNodeEditorDemo/ImageNode/ImageInputNode.cs b/WinNodeEditorDemo/ImageNode/ImageInputNode.cs new file mode 100644 index 0000000..be7fb01 --- /dev/null +++ b/WinNodeEditorDemo/ImageNode/ImageInputNode.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ST.Library.UI.NodeEditor; +using System.Drawing; +using System.Windows.Forms; +using System.Reflection; + +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); + Graphics g = dt.Graphics; + Rectangle rect = new Rectangle(this.Left + 10, this.Top + 30, 140, 80); + 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(System.Windows.Forms.MouseEventArgs e) { + if (m_rect_open.Contains(e.Location)) { //点击在"打开"区域 则弹出文件选择框 + OpenFileDialog ofd = new OpenFileDialog(); + ofd.Filter = "*.jpg|*.jpg|*.png|*.png"; + if (ofd.ShowDialog() != DialogResult.OK) 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/WinNodeEditorDemo/NumberNode/NumberAddNode.cs b/WinNodeEditorDemo/NumberNode/NumberAddNode.cs new file mode 100644 index 0000000..188be18 --- /dev/null +++ b/WinNodeEditorDemo/NumberNode/NumberAddNode.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; + +namespace WinNodeEditorDemo.NumberNode +{ + [STNode("/Number/", "Crystal_lz", "2212233137@qq.com", "www.st233.com", "This node can get two numbers add result")] + public class NumberAddNode : NumberNode + { + private STNodeOption m_in_num1; + private STNodeOption m_in_num2; + private STNodeOption m_out_num; + private int m_nNum1, m_nNum2; + private StringFormat m_sf; + + protected override void OnCreate() { + base.OnCreate(); + this.Title = "NumberAdd"; + m_sf = new StringFormat(); + m_sf.LineAlignment = StringAlignment.Center; + m_in_num1 = new STNodeOption("", typeof(int), true);//只能有一个连线 + m_in_num2 = new STNodeOption("", typeof(int), true);//只能有一个连线 + m_out_num = new STNodeOption("", typeof(int), false);//可以多个连线 + this.InputOptions.Add(m_in_num1); + this.InputOptions.Add(m_in_num2); + this.OutputOptions.Add(m_out_num); + m_in_num1.DataTransfer += new STNodeOptionEventHandler(m_in_num_DataTransfer); + m_in_num2.DataTransfer += new STNodeOptionEventHandler(m_in_num_DataTransfer); + } + //当有数据传入时 + void m_in_num_DataTransfer(object sender, STNodeOptionEventArgs e) { + //判断连线是否是连接状态(建立连线 断开连线 都会触发该事件) + if (e.Status == ConnectionStatus.Connected) { + if (sender == m_in_num1) { + if (e.TargetOption.Data != null) m_nNum1 = (int)e.TargetOption.Data;//TargetOption为触发此事件的Option + } else { + if (e.TargetOption.Data != null) m_nNum2 = (int)e.TargetOption.Data; + } + } else { + if (sender == m_in_num1) m_nNum1 = 0; else m_nNum2 = 0; + } + //向输出选项上的所有连线传输数据 输出选项上的所有连线都会触发 DataTransfer 事件 + m_out_num.TransferData(m_nNum1 + m_nNum2); //m_out_num.Data 将被自动设置 + this.Invalidate(); + } + /// + /// 当绘制选项文本时候 将数字绘制 因为STNodeOption.Text被protected修饰 STNode无法进行设置 + /// 因为作者并不建议对已经添加在STNode上的选项进行修改 尤其是在AutoSize被设置的情况下 + /// 若有需求 应当采用其他方式 比如:重绘 或者添加STNodeControl来显示变化的文本信息 + /// + /// 绘制工具 + /// 需要绘制的选项 + protected override void OnDrawOptionText(DrawingTools dt, STNodeOption op) { + base.OnDrawOptionText(dt, op); + string strText = ""; + if (op == m_in_num1) { + m_sf.Alignment = StringAlignment.Near; + strText = m_nNum1.ToString(); + } else if (op == m_in_num2) { + m_sf.Alignment = StringAlignment.Near; + strText = m_nNum2.ToString(); + } else { + m_sf.Alignment = StringAlignment.Far; + strText = (m_nNum1 + m_nNum2).ToString(); + } + dt.Graphics.DrawString(strText, this.Font, Brushes.White, op.TextRectangle, m_sf); + } + } +} diff --git a/WinNodeEditorDemo/NumberNode/NumberInputNode.cs b/WinNodeEditorDemo/NumberNode/NumberInputNode.cs new file mode 100644 index 0000000..335babe --- /dev/null +++ b/WinNodeEditorDemo/NumberNode/NumberInputNode.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; + +namespace WinNodeEditorDemo.NumberNode +{ + /// + /// 此节点通过Number属性提供一个整数的输入 + /// + [STNode("/Number","Crystal_lz","2212233137@qq.com","st233.com","Number input node")] + public class NumberInputNode : NumberNode + { + private int _Number; + [STNodeProperty("Input","this is input number")] + public int Number { + get { return _Number; } + set { + _Number = value; + m_op_number.TransferData(value); //将数据向下传递 + this.Invalidate(); + } + } + + private STNodeOption m_op_number; //输出选项 + private StringFormat m_sf = new StringFormat(); + + protected override void OnCreate() { + base.OnCreate(); + this.Title = "NumberInput"; + m_op_number = new STNodeOption("", typeof(int), false); + this.OutputOptions.Add(m_op_number); + m_sf = new StringFormat(); + m_sf.LineAlignment = StringAlignment.Center; + m_sf.Alignment = StringAlignment.Far; + } + /// + /// 当绘制选项文本时候 将数字绘制 因为STNodeOption.Text被protected修饰 STNode无法进行设置 + /// 因为作者并不建议对已经添加在STNode上的选项进行修改 尤其是在AutoSize被设置的情况下 + /// 若有需求 应当采用其他方式 比如:重绘 或者添加STNodeControl来显示变化的文本信息 + /// + /// 绘制工具 + /// 需要绘制的选项 + protected override void OnDrawOptionText(DrawingTools dt, STNodeOption op) { + base.OnDrawOptionText(dt, op); + dt.Graphics.DrawString(this._Number.ToString(), this.Font, Brushes.White, op.TextRectangle, m_sf); + } + } +} diff --git a/WinNodeEditorDemo/NumberNode/NumberNode.cs b/WinNodeEditorDemo/NumberNode/NumberNode.cs new file mode 100644 index 0000000..986fb42 --- /dev/null +++ b/WinNodeEditorDemo/NumberNode/NumberNode.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; +using System.Drawing; + +namespace WinNodeEditorDemo.NumberNode +{ + /// + /// Number节点基类 用于确定节点风格 标题颜色 以及 数据类型颜色 + /// + public abstract class NumberNode : STNode + { + protected override void OnCreate() { + base.OnCreate(); + this.TitleColor = Color.FromArgb(200, Color.CornflowerBlue); + } + protected override void OnOwnerChanged() { + base.OnOwnerChanged(); + if (this.Owner != null) this.Owner.SetTypeColor(typeof(int), Color.CornflowerBlue); + } + } +} diff --git a/WinNodeEditerTest/Program.cs b/WinNodeEditorDemo/Program.cs old mode 100755 new mode 100644 similarity index 83% rename from WinNodeEditerTest/Program.cs rename to WinNodeEditorDemo/Program.cs index cee5a20..8393bd3 --- a/WinNodeEditerTest/Program.cs +++ b/WinNodeEditorDemo/Program.cs @@ -1,21 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows.Forms; - -namespace ST.Library.UI -{ - static class Program - { - /// - /// The main entry point for the application. - /// - [STAThread] - static void Main() { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - new Demo_Image.FrmImage().Show(); - Application.Run(new Form1()); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; + +namespace WinNodeEditorDemo +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); + } + } +} diff --git a/WinNodeEditerTest/Properties/AssemblyInfo.cs b/WinNodeEditorDemo/Properties/AssemblyInfo.cs old mode 100755 new mode 100644 similarity index 82% rename from WinNodeEditerTest/Properties/AssemblyInfo.cs rename to WinNodeEditorDemo/Properties/AssemblyInfo.cs index 49f331d..c6099dd --- a/WinNodeEditerTest/Properties/AssemblyInfo.cs +++ b/WinNodeEditorDemo/Properties/AssemblyInfo.cs @@ -1,36 +1,36 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("WinNodeEditerTest")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("WinNodeEditerTest")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("2c0c84a0-fb66-4133-80f5-053356fa0f04")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WinNodeEditorDemo")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("WinNodeEditorDemo")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("04b761ae-c417-4bc8-807c-c009959e188c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/WinNodeEditerTest/Properties/Resources.Designer.cs b/WinNodeEditorDemo/Properties/Resources.Designer.cs old mode 100755 new mode 100644 similarity index 86% rename from WinNodeEditerTest/Properties/Resources.Designer.cs rename to WinNodeEditorDemo/Properties/Resources.Designer.cs index bad3293..307b927 --- a/WinNodeEditerTest/Properties/Resources.Designer.cs +++ b/WinNodeEditorDemo/Properties/Resources.Designer.cs @@ -1,63 +1,64 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace ST.Library.UI.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ST.Library.UI.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WinNodeEditorDemo.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if ((resourceMan == null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WinNodeEditorDemo.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/WinNodeEditerTest/Properties/Resources.resx b/WinNodeEditorDemo/Properties/Resources.resx old mode 100755 new mode 100644 similarity index 97% rename from WinNodeEditerTest/Properties/Resources.resx rename to WinNodeEditorDemo/Properties/Resources.resx index ffecec8..af7dbeb --- a/WinNodeEditerTest/Properties/Resources.resx +++ b/WinNodeEditorDemo/Properties/Resources.resx @@ -1,117 +1,117 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/WinNodeEditerTest/Properties/Settings.Designer.cs b/WinNodeEditorDemo/Properties/Settings.Designer.cs old mode 100755 new mode 100644 similarity index 88% rename from WinNodeEditerTest/Properties/Settings.Designer.cs rename to WinNodeEditorDemo/Properties/Settings.Designer.cs index 9187fcb..52bf6f2 --- a/WinNodeEditerTest/Properties/Settings.Designer.cs +++ b/WinNodeEditorDemo/Properties/Settings.Designer.cs @@ -1,26 +1,28 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace ST.Library.UI.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WinNodeEditorDemo.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/WinNodeEditerTest/Properties/Settings.settings b/WinNodeEditorDemo/Properties/Settings.settings old mode 100755 new mode 100644 similarity index 97% rename from WinNodeEditerTest/Properties/Settings.settings rename to WinNodeEditorDemo/Properties/Settings.settings index abf36c5..3964565 --- a/WinNodeEditerTest/Properties/Settings.settings +++ b/WinNodeEditorDemo/Properties/Settings.settings @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/WinNodeEditorDemo/STNodeHub.cs b/WinNodeEditorDemo/STNodeHub.cs new file mode 100644 index 0000000..70ff160 --- /dev/null +++ b/WinNodeEditorDemo/STNodeHub.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using ST.Library.UI.NodeEditor; + +namespace WinNodeEditorDemo +{ + /// + /// 类库自带的STNodeHub并未被STNodeAttribute标记 无法被STNodeTreeView显示 所以需要扩展 + /// + [STNode("/", "Crystal_lz", "2212233137@qq.com", "st233.com", "This is single Hub")] + public class STNodeHubSingle : STNodeHub + { + public STNodeHubSingle() + : base(true) { + this.Title = "S_HUB"; + } + } + /// + /// 类库自带的STNodeHub并未被STNodeAttribute标记 无法被STNodeTreeView显示 所以需要扩展 + /// + [STNode("/", "Crystal_lz", "2212233137@qq.com", "st233.com", "This multi is Hub")] + public class STNodeHubMulti : STNodeHub + { + public STNodeHubMulti() + : base(false) { + this.Title = "M_HUB"; + } + } +} diff --git a/WinNodeEditorDemo/ToolStripRendererEx.cs b/WinNodeEditorDemo/ToolStripRendererEx.cs new file mode 100644 index 0000000..64943d8 --- /dev/null +++ b/WinNodeEditorDemo/ToolStripRendererEx.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Text; +using System.Windows.Forms; +using System.Drawing.Drawing2D; + +namespace WinNodeEditorDemo +{ + public class ToolStripRendererEx : ToolStripRenderer + { + private SolidBrush m_brush = new SolidBrush(Color.FromArgb(255, 52, 86, 141)); + private StringFormat m_sf = new StringFormat(); + + public ToolStripRendererEx() { + m_sf.LineAlignment = StringAlignment.Center; + } + + protected override void InitializeItem(ToolStripItem item) { + base.InitializeItem(item); + item.AutoSize = false; + item.Size = new Size(item.Width, 30); + } + + protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) { + using (SolidBrush sb = new SolidBrush(Color.FromArgb(34, 34, 34))) { + e.Graphics.FillRectangle(sb, e.AffectedBounds); + } + base.OnRenderToolStripBackground(e); + } + + protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) { + e.Graphics.DrawRectangle(Pens.Black, e.AffectedBounds.X, e.AffectedBounds.Y, e.AffectedBounds.Width - 1, e.AffectedBounds.Height - 1); + base.OnRenderToolStripBorder(e); + } + + //protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) { + // using (SolidBrush sb = new SolidBrush(Color.FromArgb(50, 50, 50))) { + // e.Graphics.FillRectangle(sb, e.AffectedBounds); + // } + // base.OnRenderImageMargin(e); + //} + + protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) { + e.TextColor = e.Item.Selected ? Color.White : Color.LightGray; + e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + e.TextRectangle = new Rectangle(e.TextRectangle.Left, e.TextRectangle.Top, e.TextRectangle.Width, 30); + base.OnRenderItemText(e); + } + + protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) { + e.ArrowColor = e.Item.Selected ? Color.White : Color.LightGray; + base.OnRenderArrow(e); + } + + protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) { + Point ptEnd = new Point(e.Item.ContentRectangle.X + e.Item.Width / 2, e.Item.ContentRectangle.Y); + using (LinearGradientBrush lgb = new LinearGradientBrush(e.Item.ContentRectangle.Location, ptEnd, Color.Transparent, Color.Gray)) { + lgb.WrapMode = WrapMode.TileFlipX; + using (Pen p = new Pen(lgb)) { + e.Graphics.DrawLine(p, e.Item.ContentRectangle.Location, new Point(e.Item.ContentRectangle.Right, ptEnd.Y)); + } + } + //e.Graphics.DrawLine(Pens.Gray, e.Item.ContentRectangle.Location, new Point(e.Item.ContentRectangle.Right, ptEnd.Y)); + base.OnRenderSeparator(e); + } + + protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) { + if (e.Item.Selected) + e.Graphics.FillRectangle(m_brush, e.Item.ContentRectangle); + else + base.OnRenderMenuItemBackground(e); + } + + //protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) { + // //base.OnRenderItemImage(e); + // e.Graphics.DrawImage(e.Image, e.ImageRectangle.X, e.ImageRectangle.Y, 17, 17); + //} + } +} diff --git a/WinNodeEditerTest/WinNodeEditerTest.csproj b/WinNodeEditorDemo/WinNodeEditorDemo.csproj old mode 100755 new mode 100644 similarity index 70% rename from WinNodeEditerTest/WinNodeEditerTest.csproj rename to WinNodeEditorDemo/WinNodeEditorDemo.csproj index b13a2df..6600de5 --- a/WinNodeEditerTest/WinNodeEditerTest.csproj +++ b/WinNodeEditorDemo/WinNodeEditorDemo.csproj @@ -1,120 +1,111 @@ - - - - Debug - x86 - 8.0.30703 - 2.0 - {8B0A4516-45C9-4DE9-B947-64E8DA8A72A6} - Exe - Properties - ST.Library.UI - WinNodeEditerTest - v4.0 - 512 - - - - x86 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - x86 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - Form - - - FrmImage.cs - - - - - - Form - - - Form1.cs - - - Form - - - Form2.cs - - - - - - FrmImage.cs - - - Form1.cs - - - Form2.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - True - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - {EFFCC270-4999-4077-A543-56CCCCE92147} - ST.Library.UI - - - - + + + + Debug + x86 + 8.0.30703 + 2.0 + {4E1829B5-2160-48F5-ABD6-11914A6A25DD} + WinExe + Properties + WinNodeEditorDemo + WinNodeEditorDemo + v3.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + Form + + + + + + + + Form + + + Form1.cs + + + + + + + + + + + + + + Form1.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + {EFFCC270-4999-4077-A543-56CCCCE92147} + ST.Library.UI + + + + + \ No newline at end of file diff --git a/WinNodeEditorTest.7z b/WinNodeEditorTest.7z new file mode 100644 index 0000000000000000000000000000000000000000..8a22914f1603b95b4dbced3488dff4f3afe986ef GIT binary patch literal 291826 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%0AO&zCIdqV-B3Y#iop#69aKWcaMwKw-gaJ{c# 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