MinimapControl.cs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. using System;
  2. using System.Linq;
  3. using System.Windows;
  4. using System.Windows.Input;
  5. using System.Windows.Media;
  6. using System.Windows.Threading;
  7. using TeamAAS.FlowEditor.Models;
  8. namespace TeamAAS.FlowEditor.Controls
  9. {
  10. /// <summary>
  11. /// 鸟瞰图控件 - 显示流程图缩略概览,支持点击导航
  12. /// </summary>
  13. public class MinimapControl : System.Windows.Controls.Control
  14. {
  15. private FlowCanvas _canvas;
  16. private DispatcherTimer _refreshTimer;
  17. private Point _lastMousePos;
  18. private bool _isDragging;
  19. static MinimapControl()
  20. {
  21. DefaultStyleKeyProperty.OverrideMetadata(typeof(MinimapControl),
  22. new FrameworkPropertyMetadata(typeof(MinimapControl)));
  23. }
  24. #region Canvas 依赖属性
  25. public FlowCanvas Canvas
  26. {
  27. get => (FlowCanvas)GetValue(CanvasProperty);
  28. set => SetValue(CanvasProperty, value);
  29. }
  30. public static readonly DependencyProperty CanvasProperty =
  31. DependencyProperty.Register("Canvas", typeof(FlowCanvas), typeof(MinimapControl),
  32. new PropertyMetadata(null, OnCanvasChanged));
  33. private static void OnCanvasChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  34. {
  35. if (d is MinimapControl ctrl)
  36. {
  37. ctrl._canvas = e.NewValue as FlowCanvas;
  38. ctrl.StartRefresh();
  39. }
  40. }
  41. #endregion
  42. #region 刷新
  43. private void StartRefresh()
  44. {
  45. _refreshTimer?.Stop();
  46. _refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(60) };
  47. _refreshTimer.Tick += (s, e) => InvalidateVisual();
  48. _refreshTimer.Start();
  49. }
  50. #endregion
  51. #region 渲染
  52. protected override void OnRender(DrawingContext dc)
  53. {
  54. base.OnRender(dc);
  55. // 裁剪到控件边界,防止内容溢出
  56. dc.PushClip(new RectangleGeometry(new Rect(0, 0, ActualWidth, ActualHeight)));
  57. // 背景
  58. var bgBrush = new SolidColorBrush(Color.FromArgb(235, 37, 37, 38));
  59. var borderPen = new Pen(new SolidColorBrush(Color.FromRgb(63, 63, 70)), 1);
  60. dc.DrawRoundedRectangle(bgBrush, borderPen, new Rect(0, 0, ActualWidth, ActualHeight), 4, 4);
  61. if (_canvas?.Graph == null || _canvas.Graph.Nodes.Count == 0)
  62. {
  63. var hintBrush = new SolidColorBrush(Color.FromRgb(90, 90, 90));
  64. var text = new FormattedText("暂无节点",
  65. System.Globalization.CultureInfo.CurrentCulture,
  66. FlowDirection.LeftToRight,
  67. new Typeface("Microsoft YaHei"), 11, hintBrush, 1.25);
  68. dc.DrawText(text, new Point((ActualWidth - text.Width) / 2, (ActualHeight - text.Height) / 2));
  69. return;
  70. }
  71. // 计算内容边界
  72. var bounds = CalculateContentBounds();
  73. if (bounds.Width <= 0 || bounds.Height <= 0) return;
  74. // 计算缩放
  75. double padding = 8;
  76. double availW = ActualWidth - padding * 2;
  77. double availH = ActualHeight - padding * 2;
  78. double scale = Math.Min(availW / bounds.Width, availH / bounds.Height);
  79. double offsetX = padding + (availW - bounds.Width * scale) / 2 - bounds.X * scale;
  80. double offsetY = padding + (availH - bounds.Height * scale) / 2 - bounds.Y * scale;
  81. // 绘制连线
  82. var linePen = new Pen(new SolidColorBrush(Color.FromArgb(180, 85, 153, 255)), 0.8);
  83. linePen.Freeze();
  84. foreach (var conn in _canvas.Graph.Connections)
  85. {
  86. var srcNode = _canvas.Graph.GetNode(conn.SourceNodeId);
  87. var dstNode = _canvas.Graph.GetNode(conn.TargetNodeId);
  88. if (srcNode == null || dstNode == null) continue;
  89. var srcPos = FlowCanvas.GetPortPosition(srcNode, conn.SourceSide);
  90. var dstPos = FlowCanvas.GetPortPosition(dstNode, conn.TargetSide);
  91. var src = ToMinimap(srcPos.X, srcPos.Y, scale, offsetX, offsetY);
  92. var dst = ToMinimap(dstPos.X, dstPos.Y, scale, offsetX, offsetY);
  93. dc.DrawLine(linePen, src, dst);
  94. }
  95. // 绘制节点
  96. foreach (var node in _canvas.Graph.Nodes)
  97. {
  98. var color = GetCategoryColor(node.Category);
  99. var brush = new SolidColorBrush(color);
  100. brush.Freeze();
  101. var rect = new Rect(
  102. node.X * scale + offsetX,
  103. node.Y * scale + offsetY,
  104. Math.Max(4, node.NodeWidth * scale),
  105. Math.Max(3, node.NodeHeight * scale));
  106. dc.DrawRoundedRectangle(brush, null, rect, 1.5, 1.5);
  107. // 选中节点高亮
  108. if (node.IsSelected)
  109. {
  110. var selPen = new Pen(Brushes.White, 1.2);
  111. selPen.Freeze();
  112. dc.DrawRoundedRectangle(null, selPen, rect, 1.5, 1.5);
  113. }
  114. }
  115. // 绘制视口框
  116. var viewport = _canvas.GetViewport();
  117. var vpRect = new Rect(
  118. viewport.X * scale + offsetX,
  119. viewport.Y * scale + offsetY,
  120. viewport.Width * scale,
  121. viewport.Height * scale);
  122. var vpBrush = new SolidColorBrush(Color.FromArgb(30, 0, 122, 204));
  123. vpBrush.Freeze();
  124. var vpPen = new Pen(new SolidColorBrush(Color.FromArgb(200, 0, 122, 204)), 1.5);
  125. vpPen.Freeze();
  126. dc.DrawRectangle(vpBrush, vpPen, vpRect);
  127. // 结束裁剪
  128. dc.Pop();
  129. }
  130. private Point ToMinimap(double x, double y, double scale, double offsetX, double offsetY)
  131. {
  132. return new Point(x * scale + offsetX, y * scale + offsetY);
  133. }
  134. private Color GetCategoryColor(NodeCategory category)
  135. {
  136. switch (category)
  137. {
  138. case NodeCategory.Decision: return Color.FromRgb(0xC2, 0x77, 0x1A);
  139. case NodeCategory.ToolBlock: return Color.FromRgb(0x7B, 0x1F, 0xA2);
  140. case NodeCategory.ExceptionBranch: return Color.FromRgb(0xB7, 0x1C, 0x1C);
  141. default: return Color.FromRgb(0x00, 0x7A, 0xCC);
  142. }
  143. }
  144. private Rect CalculateContentBounds()
  145. {
  146. if (_canvas == null) return new Rect(0, 0, 0, 0);
  147. // 鸟瞰图视野 = 画幅大小(FlowCanvas 的实际宽高)
  148. double w = _canvas.Width > 0 ? _canvas.Width : 800;
  149. double h = _canvas.Height > 0 ? _canvas.Height : 600;
  150. return new Rect(0, 0, w, h);
  151. }
  152. #endregion
  153. #region 鼠标交互
  154. protected override void OnMouseDown(MouseButtonEventArgs e)
  155. {
  156. base.OnMouseDown(e);
  157. _isDragging = true;
  158. _lastMousePos = e.GetPosition(this);
  159. NavigateTo(_lastMousePos);
  160. CaptureMouse();
  161. e.Handled = true;
  162. }
  163. protected override void OnMouseMove(MouseEventArgs e)
  164. {
  165. base.OnMouseMove(e);
  166. if (_isDragging)
  167. {
  168. _lastMousePos = e.GetPosition(this);
  169. NavigateTo(_lastMousePos);
  170. }
  171. }
  172. protected override void OnMouseUp(MouseButtonEventArgs e)
  173. {
  174. base.OnMouseUp(e);
  175. _isDragging = false;
  176. ReleaseMouseCapture();
  177. }
  178. private void NavigateTo(Point minimapPoint)
  179. {
  180. if (_canvas?.Graph == null || _canvas.Graph.Nodes.Count == 0) return;
  181. var bounds = CalculateContentBounds();
  182. if (bounds.Width <= 0 || bounds.Height <= 0) return;
  183. double padding = 8;
  184. double availW = ActualWidth - padding * 2;
  185. double availH = ActualHeight - padding * 2;
  186. double scale = Math.Min(availW / bounds.Width, availH / bounds.Height);
  187. double offsetX = padding + (availW - bounds.Width * scale) / 2 - bounds.X * scale;
  188. double offsetY = padding + (availH - bounds.Height * scale) / 2 - bounds.Y * scale;
  189. double canvasX = (minimapPoint.X - offsetX) / scale;
  190. double canvasY = (minimapPoint.Y - offsetY) / scale;
  191. _canvas.CenterOn(new Point(canvasX, canvasY));
  192. }
  193. #endregion
  194. }
  195. }