优化了中断功能,增加了节点变量的查看。

This commit is contained in:
fengjiayi
2024-09-22 14:10:13 +08:00
parent 3537a49784
commit c930c870a6
26 changed files with 1686 additions and 494 deletions

View File

@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Serein.WorkBench"
xmlns:custom="clr-namespace:Serein.WorkBench.Node.View"
xmlns:themes="clr-namespace:Serein.WorkBench.Themes"
Title="Dynamic Node Flow" Height="700" Width="1200"
AllowDrop="True" Drop="Window_Drop" DragOver="Window_DragOver"
Loaded="Window_Loaded"
@@ -38,37 +39,26 @@
<RowDefinition Height="2*"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<!--<Button Grid.Row="0" Content="Button 1"></Button>
<Button Grid.Row="1" Content="Button 2"></Button>
<Button Grid.Row="2" Content="Button 3"></Button>
<Button Grid.Row="3" Content="Button 4"></Button>-->
<!--DockPanel.Dock="Top"-->
<Grid Margin="2,2,2,5">
<Grid Margin="2,2,2,5" Grid.Row="0" >
<Button Grid.Row="0" Content="保存项目" Click="ButtonSaveFile_Click" HorizontalAlignment="Left" Margin="5,5,5,5"/>
<!--<Button Grid.Row="0" Content="卸载清空" Click="UnloadAllButton_Click" HorizontalAlignment="Right" Margin="5,5,5,5"/>-->
</Grid>
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto">
<StackPanel Orientation="Horizontal">
<!--<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>-->
<custom:ExpOpNodeControl x:Name="ExpOpNodeControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>
<custom:ConditionNodeControl x:Name="ConditionNodeControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>
<custom:ConditionRegionControl x:Name="ConditionRegionControl" Margin="10" AllowDrop="True" PreviewMouseMove="BaseNodeControl_PreviewMouseMove"/>
<!--<custom:ActionRegionControl x:Name="ActionRegionControl" Grid.Column="1" Margin="10" AllowDrop="True" Drop="ActionRegionControl_Drop" PreviewMouseMove="RegionControl_PreviewMouseMove"/>-->
<!--<TextBlock Text="触发器" Grid.Column="2"/>-->
<!--<custom:StateRegionControl x:Name="StateRegionControl" Grid.Column="2" Margin="10" AllowDrop="True" Drop="StateRegionControl_Drop" PreviewMouseMove="RegionControl_PreviewMouseMove"/>-->
<!--</Grid>-->
</StackPanel>
</ScrollViewer>
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto" MaxHeight="400">
<StackPanel x:Name="DllStackPanel" Margin="5"/>
</ScrollViewer>
<Grid Grid.Row="3" >
<themes:ObjectViewerControl x:Name="ObjectViewer"/>
</Grid>
</Grid>
</DockPanel>

View File

@@ -25,6 +25,7 @@ using System.Windows.Media.Animation;
using System.Windows.Media.Media3D;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Xml.Linq;
using DataObject = System.Windows.DataObject;
namespace Serein.WorkBench
@@ -62,10 +63,15 @@ namespace Serein.WorkBench
/// <summary>
/// 存储所有与节点有关的控件
/// 任何情景下都尽量避免直接操作 ViewModel 中的 NodeModel 节点,
/// 而是应该调用 FlowEnvironment 提供接口进行操作,
/// 因为 Workbench 应该更加关注UI视觉效果而非直接干扰流程环境运行的逻辑。
/// 之所以暴露 NodeModel 属性,因为有些场景下不可避免的需要直接获取节点的属性。
/// </summary>
private Dictionary<string, NodeControlBase> NodeControls { get; } = [];
/// <summary>
/// 存储所有的连接
/// 存储所有的连接。考虑集成在运行环境中。
/// </summary>
private List<Connection> Connections { get; } = [];
@@ -174,9 +180,14 @@ namespace Serein.WorkBench
FlowEnvironment.OnNodeRemote += FlowEnvironment_NodeRemoteEvent;
FlowEnvironment.OnFlowRunComplete += FlowEnvironment_OnFlowRunComplete;
FlowEnvironment.OnMonitorObjectChange += FlowEnvironment_OnMonitorObjectChange;
FlowEnvironment.OnNodeInterruptStateChange += FlowEnvironment_OnNodeInterruptStateChange;
FlowEnvironment.OnNodeInterruptTrigger += FlowEnvironment_OnNodeInterruptTrigger;
}
private void InitUI()
{
@@ -191,13 +202,6 @@ namespace Serein.WorkBench
//FlowChartCanvas.RenderTransformOrigin = new Point(0.5, 0.5);
}
//private void ButtonReflushCanvasConfig_Click(object sender, RoutedEventArgs e)
//{
// scaleTransform.ScaleX = 1;
// scaleTransform.ScaleY = 1;
// translateTransform.X = 0;
// translateTransform.Y = 0;
//}
#region
@@ -295,41 +299,6 @@ namespace Serein.WorkBench
}
/// <summary>
/// 运行环境成功加载了节点,需要在画布上创建节点控件
/// </summary>
/// <param name="nodeInfo"></param>
/// <param name="methodDetailss"></param>
//private void FlowEnvironment_NodeLoadEvent(LoadNodeEventArgs eventArgs)
//{
// if (!eventArgs.IsSucceed)
// {
// MessageBox.Show(eventArgs.ErrorTips);
// return;
// }
// NodeInfo nodeInfo = eventArgs.NodeInfo;
// MethodDetails methodDetailss = eventArgs.MethodDetailss;
// // 创建对应的实例包含NodeModelNodeControlNodeControlViewModel
// NodeControlBase? nodeControl = CreateNodeControlOfNodeInfo(nodeInfo, methodDetailss);
// if (nodeControl == null)
// {
// WriteLog($"无法为节点类型创建节点控件: {nodeInfo.MethodName}\r\n");
// return;
// // ConfigureNodeControl(nodeInfo, nodeControl, nodeControls, regionControls);
// }
// // 判断是否属于区域控件,如果是,则加载区域子项
// // if (nodeControl is ActionRegionControl || nodeControl is ConditionRegionControl)
// // {
// // AddNodeControlInRegeionControl(nodeControl, nodeInfo.ChildNodes);
// // }
// NodeControls.TryAdd(nodeInfo.Guid, nodeControl); // 存放对应的控件
// PlaceNodeOnCanvas(nodeControl, nodeInfo.Position.X, nodeInfo.Position.Y); // 配置节点,并放置在画布上
//}
/// <summary>
/// 节点连接关系变更
/// </summary>
@@ -342,10 +311,8 @@ namespace Serein.WorkBench
{
string fromNodeGuid = eventArgs.FromNodeGuid;
string toNodeGuid = eventArgs.ToNodeGuid;
if (!NodeControls.TryGetValue(fromNodeGuid, out var fromNode) || !NodeControls.TryGetValue(toNodeGuid, out var toNode))
{
return;
}
NodeControlBase fromNode = GuidToControl(fromNodeGuid);
NodeControlBase toNode = GuidToControl(toNodeGuid);
ConnectionType connectionType = eventArgs.ConnectionType;
if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Create)
{
@@ -375,7 +342,7 @@ namespace Serein.WorkBench
.ToList();
foreach (var connection in removeConnections)
{
connection.RemoveFromCanvas(FlowChartCanvas);
connection.RemoveFromCanvas();
Connections.Remove(connection);
}
}
@@ -389,14 +356,7 @@ namespace Serein.WorkBench
private void FlowEnvironment_NodeRemoteEvent(NodeRemoteEventArgs eventArgs)
{
var nodeGuid = eventArgs.NodeGuid;
if (!NodeControls.TryGetValue(nodeGuid, out NodeControlBase nodeControl))
{
return;
}
if(nodeControl is null)
{
return;
}
NodeControlBase nodeControl = GuidToControl(nodeGuid);
if (selectNodeControls.Count > 0)
{
if (selectNodeControls.Contains(nodeControl))
@@ -479,23 +439,13 @@ namespace Serein.WorkBench
{
string oldNodeGuid = eventArgs.OldNodeGuid;
string newNodeGuid = eventArgs.NewNodeGuid;
if (!NodeControls.TryGetValue(newNodeGuid, out var newStartNodeControl))
{
return;
}
if (newStartNodeControl == null)
{
return;
}
NodeControlBase newStartNodeControl = GuidToControl(newNodeGuid);
if (!string.IsNullOrEmpty(oldNodeGuid))
{
NodeControls.TryGetValue(oldNodeGuid, out var oldStartNodeControl);
if (oldStartNodeControl != null)
{
oldStartNodeControl.BorderBrush = Brushes.Black;
oldStartNodeControl.BorderThickness = new Thickness(0);
}
NodeControlBase oldStartNodeControl = GuidToControl(oldNodeGuid);
oldStartNodeControl.BorderBrush = Brushes.Black;
oldStartNodeControl.BorderThickness = new Thickness(0);
}
newStartNodeControl.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#04FC10"));
@@ -504,10 +454,89 @@ namespace Serein.WorkBench
}
/// <summary>
/// 被监视的对象发生改变(节点执行了一次)
/// </summary>
/// <param name="eventArgs"></param>
private void FlowEnvironment_OnMonitorObjectChange(MonitorObjectEventArgs eventArgs)
{
string nodeGuid = eventArgs.NodeGuid;
if (string.IsNullOrEmpty(ObjectViewer.NodeGuid)) // 如果没有加载过
{
ObjectViewer.NodeGuid = nodeGuid;
ObjectViewer.LoadObjectInformation(eventArgs.NewData); // 加载节点
}
else
{
// 加载过,如果显示的对象来源并非同一个节点,则停止监听之前的节点
if (!ObjectViewer.NodeGuid.Equals(nodeGuid))
{
FlowEnvironment.SetNodeFLowDataMonitorState(ObjectViewer.NodeGuid, false);
}
ObjectViewer.RefreshObjectTree(eventArgs.NewData);
}
}
/// <summary>
/// 节点中断状态改变。
/// </summary>
/// <param name="eventArgs"></param>
private void FlowEnvironment_OnNodeInterruptStateChange(NodeInterruptStateChangeEventArgs eventArgs)
{
string nodeGuid = eventArgs.NodeGuid;
NodeControlBase nodeControl = GuidToControl(nodeGuid);
if (eventArgs.Class == InterruptClass.None)
{
nodeControl.ViewModel.IsInterrupt = false;
}
else
{
nodeControl.ViewModel.IsInterrupt = true;
}
}
/// <summary>
/// 节点触发了中断
/// </summary>
/// <param name="eventArgs"></param>
/// <exception cref="NotImplementedException"></exception>
private void FlowEnvironment_OnNodeInterruptTrigger(NodeInterruptTriggerEventArgs eventArgs)
{
string nodeGuid = eventArgs.NodeGuid;
NodeControlBase nodeControl = GuidToControl(nodeGuid);
Console.WriteLine("节点触发了中断");
}
/// <summary>
/// Guid 转 NodeControl
/// </summary>
/// <param name="nodeGuid">节点Guid</param>
/// <returns>节点Model</returns>
/// <exception cref="ArgumentNullException">无法获取节点、Guid/节点为null时报错</exception>
private NodeControlBase GuidToControl(string nodeGuid)
{
if (string.IsNullOrEmpty(nodeGuid))
{
throw new ArgumentNullException("not contains - Guid没有对应节点:" + (nodeGuid));
}
if (!NodeControls.TryGetValue(nodeGuid, out NodeControlBase? nodeControl) || nodeControl is null)
{
throw new ArgumentNullException("null - Guid存在对应节点,但节点为null:" + (nodeGuid));
}
return nodeControl;
}
#endregion
#region
/// <summary>
/// 运行环节加载了项目文件,需要创建节点控件
/// </summary>
@@ -634,7 +663,7 @@ namespace Serein.WorkBench
/// <summary>
/// 配置节点右键菜单
/// </summary>
/// <param name="nodeControl"></param>
/// <param name="nodeControl"><para> 任何情景下都尽量避免直接操作 ViewModel 中的 NodeModel 节点,而是应该调用 FlowEnvironment 提供接口进行操作。</para> 因为 Workbench 应该更加关注UI视觉效果而非直接干扰流程环境运行的逻辑。<para> 之所以暴露 NodeModel 属性,因为有些场景下不可避免的需要直接获取节点的属性。</para> </param>
private void ConfigureContextMenu(NodeControlBase nodeControl)
{
var contextMenu = new ContextMenu();
@@ -650,29 +679,42 @@ namespace Serein.WorkBench
}
var nodeGuid = nodeControl?.ViewModel?.Node?.Guid;
MenuItem? debugMenu = null;
debugMenu = CreateMenuItem("在此中断", (s, e) =>
{
if (nodeControl!.ViewModel.DebugSetting.IsInterrupt)
{
nodeControl.ViewModel.IsInterrupt = false;
debugMenu!.Header = "在此中断";
}
else
{
nodeControl.ViewModel.IsInterrupt = true;
debugMenu!.Header = "取消中断";
}
});
contextMenu.Items.Add(debugMenu);
#region -
contextMenu.Items.Add(CreateMenuItem("在此中断", (s, e) =>
{
if ((s is MenuItem menuItem) && menuItem is not null)
{
if (nodeControl?.ViewModel?.Node?.DebugSetting?.InterruptClass == InterruptClass.None)
{
FlowEnvironment.NodeInterruptChange(nodeGuid, InterruptClass.Branch);
menuItem.Header = "取消中断";
}
else
{
FlowEnvironment.NodeInterruptChange(nodeGuid, InterruptClass.None);
menuItem.Header = "在此中断";
}
}
}));
#endregion
contextMenu.Items.Add(CreateMenuItem("查看数据", (s, e) =>
{
var node = nodeControl?.ViewModel?.Node;
if(node is not null)
{
FlowEnvironment.SetNodeFLowDataMonitorState(node.Guid, true); // 通知环境该节点的数据更新后需要传到UI
}
}));
contextMenu.Items.Add(CreateMenuItem("设为起点", (s, e) => FlowEnvironment.SetStartNode(nodeGuid)));
contextMenu.Items.Add(CreateMenuItem("删除", (s, e) => FlowEnvironment.RemoteNode(nodeGuid)));
contextMenu.Items.Add(CreateMenuItem("添加 真分支", (s, e) => StartConnection(nodeControl, ConnectionType.IsSucceed)));
contextMenu.Items.Add(CreateMenuItem("添加 假分支", (s, e) => StartConnection(nodeControl, ConnectionType.IsFail)));
contextMenu.Items.Add(CreateMenuItem("添加 异常分支", (s, e) => StartConnection(nodeControl, ConnectionType.IsError)));
@@ -680,20 +722,40 @@ namespace Serein.WorkBench
#region -
var AvoidMenu = new MenuItem();
//AvoidMenu.Items.Add(CreateMenuItem("水平对齐", (s, e) => AlignHorizontallyAvoidOverlap(selectNodeControls)));
//AvoidMenu.Items.Add(CreateMenuItem("垂直对齐", (s, e) => VerticalAlignAvoidOverlap(selectNodeControls)));
AvoidMenu.Items.Add(CreateMenuItem("群组对齐", (s, e) => {
AlignControlsWithGrouping(selectNodeControls);
UpdateConnectedLines();
AvoidMenu.Items.Add(CreateMenuItem("群组对齐", (s, e) =>
{
AlignControlsWithGrouping(selectNodeControls, AlignMode.Grouping);
}));
AvoidMenu.Items.Add(CreateMenuItem("规划对齐", (s, e) =>
{
AlignControlsWithDynamicProgramming(selectNodeControls);
UpdateConnectedLines();
AlignControlsWithGrouping(selectNodeControls, AlignMode.Planning);
}));
AvoidMenu.Items.Add(CreateMenuItem("水平中心对齐", (s, e) =>
{
AlignControlsWithGrouping(selectNodeControls, AlignMode.HorizontalCenter);
}));
AvoidMenu.Items.Add(CreateMenuItem("垂直中心对齐 ", (s, e) =>
{
AlignControlsWithGrouping(selectNodeControls, AlignMode.VerticalCenter);
}));
AvoidMenu.Items.Add(CreateMenuItem("垂直对齐时水平斜分布", (s, e) =>
{
AlignControlsWithGrouping(selectNodeControls, AlignMode.Vertical);
}));
AvoidMenu.Items.Add(CreateMenuItem("水平对齐时垂直斜分布", (s, e) =>
{
AlignControlsWithGrouping(selectNodeControls, AlignMode.Horizontal);
}));
AvoidMenu.Header = "对齐";
contextMenu.Items.Add(AvoidMenu);
contextMenu.Items.Add(AvoidMenu);
#endregion
nodeControl.ContextMenu = contextMenu;
}
@@ -747,6 +809,21 @@ namespace Serein.WorkBench
Console.WriteLine(ex);
}
}
//private void DisplayFlowDataTreeViewer(object @object)
//{
// try
// {
// var typeViewerWindow = new ObjectViewerWindow();
// typeViewerWindow.LoadObjectInformation(@object);
// typeViewerWindow.Show();
// }
// catch (Exception ex)
// {
// Console.WriteLine(ex);
// }
//}
#endregion
#region DLL文件到左侧功能区
@@ -1205,7 +1282,9 @@ namespace Serein.WorkBench
{
if (connection.Start == block || connection.End == block)
{
BezierLineDrawer.UpdateBezierLine(FlowChartCanvas, connection.Start, connection.End, connection.BezierPath, connection.ArrowPath);
connection.Refresh();
//connection.RemoveFromCanvas();
//BezierLineDrawer.UpdateBezierLine(FlowChartCanvas, connection.Start, connection.End, connection.BezierPath, connection.ArrowPath);
}
}
}
@@ -1620,10 +1699,14 @@ namespace Serein.WorkBench
//{
// UpdateConnections(nodeControl);
//}
foreach (var line in Connections)
this.Dispatcher.Invoke(() =>
{
line.Refresh();
}
foreach (var line in Connections)
{
line.Refresh();
}
});
}
@@ -1787,6 +1870,119 @@ namespace Serein.WorkBench
#endregion
public enum AlignMode
{
/// <summary>
/// 水平对齐
/// </summary>
Horizontal,
/// <summary>
/// 垂直对齐
/// </summary>
Vertical,
/// <summary>
/// 水平中心对齐
/// </summary>
HorizontalCenter,
/// <summary>
/// 垂直中心对齐
/// </summary>
VerticalCenter,
/// <summary>
/// 规划对齐
/// </summary>
Planning,
/// <summary>
/// 群组对齐
/// </summary>
Grouping,
}
public void AlignControlsWithGrouping(List<NodeControlBase> selectNodeControls, AlignMode alignMode, double proximityThreshold = 50, double spacing = 10)
{
if (selectNodeControls == null || selectNodeControls.Count < 2)
return;
switch (alignMode)
{
case AlignMode.Horizontal:
AlignHorizontally(selectNodeControls, spacing);// AlignToCenter
break;
case AlignMode.Vertical:
AlignVertically(selectNodeControls, spacing);
break;
case AlignMode.HorizontalCenter:
AlignToCenter(selectNodeControls, isHorizontal: false, spacing);
break;
case AlignMode.VerticalCenter:
AlignToCenter(selectNodeControls, isHorizontal: true, spacing);
break;
case AlignMode.Planning:
AlignControlsWithDynamicProgramming(selectNodeControls, spacing);
break;
case AlignMode.Grouping:
AlignControlsWithGrouping(selectNodeControls, proximityThreshold, spacing);
break;
}
}
// 垂直对齐并避免重叠
private void AlignHorizontally(List<NodeControlBase> controls, double spacing)
{
double avgY = controls.Average(c => Canvas.GetTop(c)); // 计算Y坐标平均值
double currentY = avgY;
foreach (var control in controls.OrderBy(c => Canvas.GetTop(c))) // 按Y坐标排序对齐
{
Canvas.SetTop(control, currentY);
currentY += control.ActualHeight + spacing; // 保证控件之间有足够的垂直间距
}
}
// 水平对齐并避免重叠
private void AlignVertically(List<NodeControlBase> controls, double spacing)
{
double avgX = controls.Average(c => Canvas.GetLeft(c)); // 计算X坐标平均值
double currentX = avgX;
foreach (var control in controls.OrderBy(c => Canvas.GetLeft(c))) // 按X坐标排序对齐
{
Canvas.SetLeft(control, currentX);
currentX += control.ActualWidth + spacing; // 保证控件之间有足够的水平间距
}
}
// 按中心点对齐
private void AlignToCenter(List<NodeControlBase> controls, bool isHorizontal, double spacing)
{
double avgCenter = isHorizontal
? controls.Average(c => Canvas.GetLeft(c) + c.ActualWidth / 2) // 水平中心点
: controls.Average(c => Canvas.GetTop(c) + c.ActualHeight / 2); // 垂直中心点
foreach (var control in controls)
{
if (isHorizontal)
{
double left = avgCenter - control.ActualWidth / 2;
Canvas.SetLeft(control, left);
}
else
{
double top = avgCenter - control.ActualHeight / 2;
Canvas.SetTop(control, top);
}
}
}
#endregion
#region
@@ -1936,6 +2132,7 @@ namespace Serein.WorkBench
private void ButtonDebugFlipflopNode_Click(object sender, RoutedEventArgs e)
{
FlowEnvironment?.Exit(); // 在运行平台上点击了退出
}
/// <summary>
@@ -2079,19 +2276,19 @@ namespace Serein.WorkBench
public static Connection Draw(Canvas canvas, Connection connection)
{
connection.Canvas = canvas;
UpdateBezierLine(canvas, connection);
UpdateBezierLineInDragging(canvas, connection);
//MakeDraggable(canvas, connection, connection.Start);
//MakeDraggable(canvas, connection, connection.End);
if (connection.BezierPath == null)
{
connection.BezierPath = new System.Windows.Shapes.Path { Stroke = BezierLineDrawer.GetStroke(connection.Type), StrokeThickness = 1 };
connection.BezierPath = new System.Windows.Shapes.Path { Stroke = BezierLineDrawer.GetLineColor(connection.Type), StrokeThickness = 1 };
Canvas.SetZIndex(connection.BezierPath, -1);
canvas.Children.Add(connection.BezierPath);
}
if (connection.ArrowPath == null)
{
connection.ArrowPath = new System.Windows.Shapes.Path { Stroke = BezierLineDrawer.GetStroke(connection.Type), Fill = BezierLineDrawer.GetStroke(connection.Type), StrokeThickness = 1 };
connection.ArrowPath = new System.Windows.Shapes.Path { Stroke = BezierLineDrawer.GetLineColor(connection.Type), Fill = BezierLineDrawer.GetLineColor(connection.Type), StrokeThickness = 1 };
Canvas.SetZIndex(connection.ArrowPath, -1);
canvas.Children.Add(connection.ArrowPath);
}
@@ -2106,7 +2303,7 @@ namespace Serein.WorkBench
// 拖动时重新绘制
public static void UpdateBezierLine(Canvas canvas, Connection connection)
public static void UpdateBezierLineInDragging(Canvas canvas, Connection connection)
{
if (isUpdating)
return;
@@ -2117,14 +2314,14 @@ namespace Serein.WorkBench
{
if (connection != null && connection.BezierPath == null)
{
connection.BezierPath = new System.Windows.Shapes.Path { Stroke = BezierLineDrawer.GetStroke(connection.Type), StrokeThickness = 1 };
connection.BezierPath = new System.Windows.Shapes.Path { Stroke = BezierLineDrawer.GetLineColor(connection.Type), StrokeThickness = 1 };
//Canvas.SetZIndex(connection.BezierPath, -1);
canvas.Children.Add(connection.BezierPath);
}
if (connection != null && connection.ArrowPath == null)
{
connection.ArrowPath = new System.Windows.Shapes.Path { Stroke = BezierLineDrawer.GetStroke(connection.Type), Fill = BezierLineDrawer.GetStroke(connection.Type), StrokeThickness = 1 };
connection.ArrowPath = new System.Windows.Shapes.Path { Stroke = BezierLineDrawer.GetLineColor(connection.Type), Fill = BezierLineDrawer.GetLineColor(connection.Type), StrokeThickness = 1 };
//Canvas.SetZIndex(connection.ArrowPath, -1);
canvas.Children.Add(connection.ArrowPath);
}
@@ -2188,22 +2385,24 @@ namespace Serein.WorkBench
public required NodeControlBase Start { get; set; } // 起始
public required NodeControlBase End { get; set; } // 结束
private Storyboard? _animationStoryboard; // 动画Storyboard
public void RemoveFromCanvas(Canvas canvas)
public void RemoveFromCanvas()
{
canvas.Children.Remove(BezierPath); // 移除线
canvas.Children.Remove(ArrowPath); // 移除线
_animationStoryboard?.Stop(); // 停止动画
Canvas.Children.Remove(BezierPath); // 移除线
Canvas.Children.Remove(ArrowPath); // 移除线
}
/// <summary>
/// 重新绘制
/// </summary>
public void Refresh()
{
// BsControl.Draw(Canvas, this);
BezierLineDrawer.UpdateBezierLine(Canvas, Start, End, BezierPath, ArrowPath);
}
}
public static class BezierLineDrawer
{
public enum Localhost
@@ -2214,7 +2413,14 @@ namespace Serein.WorkBench
Bottom,
}
// 绘制曲线
/// <summary>
/// 绘制曲线
/// </summary>
/// <param name="canvas">所在画布</param>
/// <param name="startElement">起始控件</param>
/// <param name="endElement">终点控件</param>
/// <param name="bezierPath">曲线</param>
/// <param name="arrowPath">箭头</param>
public static void UpdateBezierLine(Canvas canvas,
FrameworkElement startElement,
FrameworkElement endElement,
@@ -2223,6 +2429,8 @@ namespace Serein.WorkBench
{
Point startPoint = startElement.TranslatePoint(new Point(startElement.ActualWidth / 2, startElement.ActualHeight / 2), canvas);
Point endPoint = CalculateEndpointOutsideElement(endElement, canvas, startPoint, out Localhost localhost);
// 根据终点位置决定起点位置 (位于控件的边缘)
startPoint = CalculateEdgePoint(startElement, localhost, canvas);
PathFigure pathFigure = new PathFigure { StartPoint = startPoint };
BezierSegment bezierSegment;
@@ -2246,8 +2454,11 @@ namespace Serein.WorkBench
Point3 = endPoint,
};
}
var minZ = canvas.Children.OfType<UIElement>()//linq语句取Zindex的最大值
.Select(x => Grid.GetZIndex(x))
.Min();
Grid.SetZIndex(bezierPath, minZ - 1);
// Canvas.SetZIndex(bezierPath, 0);
pathFigure.Segments.Add(bezierSegment);
PathGeometry pathGeometry = new PathGeometry();
@@ -2260,7 +2471,7 @@ namespace Serein.WorkBench
private static Point CalculateBezierTangent(Point startPoint, Point controlPoint1, Point controlPoint2, Point endPoint)
{
double t = 10.0; // 末端点
double t = 11; // 末端点
// 计算贝塞尔曲线在 t = 1 处的一阶导数
double dx = 3 * Math.Pow(1 - t, 2) * (controlPoint1.X - startPoint.X) +
@@ -2301,6 +2512,31 @@ namespace Serein.WorkBench
arrowPath.Data = arrowGeometry;
}
// 计算起点位于控件边缘的四个中心点之一
private static Point CalculateEdgePoint(FrameworkElement element, Localhost localhost, Canvas canvas)
{
Point point = new Point();
switch (localhost)
{
case Localhost.Right:
point = new Point(0, element.ActualHeight / 2); // 左边中心
break;
case Localhost.Left:
point = new Point(element.ActualWidth, element.ActualHeight / 2); // 右边中心
break;
case Localhost.Bottom:
point = new Point(element.ActualWidth / 2, 0); // 上边中心
break;
case Localhost.Top:
point = new Point(element.ActualWidth / 2, element.ActualHeight); // 下边中心
break;
}
// 将相对控件的坐标转换到画布中的全局坐标
return element.TranslatePoint(point, canvas);
}
// 计算终点落点位置
private static Point CalculateEndpointOutsideElement(FrameworkElement element, Canvas canvas, Point startPoint, out Localhost localhost)
{
@@ -2322,35 +2558,57 @@ namespace Serein.WorkBench
(false, false) => Localhost.Top,
};
double halfWidth = element.ActualWidth / 2 + 6;
double halfHeight = element.ActualHeight / 2 + 6;
double margin = 0;
double halfWidth = element.ActualWidth / 2 + 10;
double halfHeight = element.ActualHeight / 2 + 10;
#region
//if (localhost == Localhost.Left)
//{
// centerPoint.X -= halfWidth;
//}
//else if (localhost == Localhost.Right)
//{
// centerPoint.X -= -halfWidth;
//}
//else if (localhost == Localhost.Top)
//{
// centerPoint.Y -= halfHeight;
//}
//else if (localhost == Localhost.Bottom)
//{
// centerPoint.Y -= -halfHeight;
//}
#endregion
#region
double margin = 0;
if (localhost == Localhost.Left)
{
centerPoint.X -= halfWidth;
centerPoint.Y -= direction.Y / Math.Abs(direction.X) * halfHeight - margin;
centerPoint.Y -= direction.Y / (1 + Math.Abs(direction.X)) * halfHeight - margin;
}
else if (localhost == Localhost.Right)
{
centerPoint.X -= -halfWidth;
centerPoint.Y -= direction.Y / Math.Abs(direction.X) * halfHeight - margin;
centerPoint.Y -= direction.Y / (1 + Math.Abs(direction.X)) * halfHeight - margin;
}
else if (localhost == Localhost.Top)
{
centerPoint.Y -= halfHeight;
centerPoint.X -= direction.X / Math.Abs(direction.Y) * halfWidth - margin;
centerPoint.X -= direction.X / (1 + Math.Abs(direction.Y)) * halfWidth - margin;
}
else if (localhost == Localhost.Bottom)
{
centerPoint.Y -= -halfHeight;
centerPoint.X -= direction.X / Math.Abs(direction.Y) * halfWidth - margin;
centerPoint.X -= direction.X / (1 + Math.Abs(direction.Y)) * halfWidth - margin;
}
#endregion
return centerPoint;
}
public static SolidColorBrush GetStroke(ConnectionType currentConnectionType)
public static SolidColorBrush GetLineColor(ConnectionType currentConnectionType)
{
return currentConnectionType switch
{

View File

@@ -67,23 +67,35 @@ namespace Serein.WorkBench.Node.ViewModel
}
}
private bool isInterrupt;
public bool IsInterrupt
{
get => Node.DebugSetting.IsInterrupt;
get => isInterrupt;
set
{
if (value)
{
Node.Interrupt();
}
else
{
Node.CancelInterrupt();
}
isInterrupt = value;
OnPropertyChanged(nameof(IsInterrupt));
}
}
//public bool IsInterrupt
//{
// get => Node.DebugSetting.IsInterrupt;
// set
// {
// if (value)
// {
// Node.Interrupt();
// }
// else
// {
// Node.CancelInterrupt();
// }
// OnPropertyChanged(nameof(IsInterrupt));
// }
//}
//public bool IsProtectionParameter
//{
// get => MethodDetails.IsProtectionParameter;

View File

@@ -14,7 +14,7 @@
<DockPanel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<!--<RowDefinition Height="*"/>-->
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
@@ -23,13 +23,13 @@
<!--<ColumnDefinition Width="*" />-->
</Grid.ColumnDefinitions>
<GroupBox Grid.Row="0" Header="条件" Margin="5">
<!--<GroupBox Grid.Row="0" Header="条件" Margin="5">
<ListBox x:Name="ConditionsListBox" Background="#A8D8EA"/>
</GroupBox>
<GroupBox Grid.Row="1" Header="动作" Margin="5">
</GroupBox>-->
<GroupBox Grid.Row="0" Header="动作" Margin="5">
<ListBox x:Name="ActionsListBox" Background="#FFCFDF"/>
</GroupBox>
<GroupBox Grid.Row="2" Header="触发器" Margin="5">
<GroupBox Grid.Row="1" Header="触发器" Margin="5">
<ListBox x:Name="FlipflopsListBox" Background="#FFFFD2"/>
</GroupBox>
</Grid>

View File

@@ -26,12 +26,14 @@
<Compile Remove="Themes\ConditionControl.xaml.cs" />
<Compile Remove="Themes\ConditionControlModel.cs" />
<Compile Remove="Themes\ExplicitDataControl.xaml.cs" />
<Compile Remove="Themes\ObjectViewerControl1.xaml.cs" />
</ItemGroup>
<ItemGroup>
<Page Remove="Node\FlipflopRegionControl.xaml" />
<Page Remove="Themes\ConditionControl.xaml" />
<Page Remove="Themes\ExplicitDataControl.xaml" />
<Page Remove="Themes\ObjectViewerControl1.xaml" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,16 @@
<Window x:Class="Serein.WorkBench.Themes.InputDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Serein.WorkBench.Themes"
mc:Ignorable="d"
Title="InputDialog" Height="450" Width="800">
<StackPanel Margin="10">
<TextBox x:Name="InputTextBox" Width="200" Margin="0,0,0,10" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="确认" Click="ConfirmButton_Click" Margin="5" />
<Button Content="取消" Click="CancelButton_Click" Margin="5" />
</StackPanel>
</StackPanel>
</Window>

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Serein.WorkBench.Themes
{
/// <summary>
/// InputDialog.xaml 的交互逻辑
/// </summary>
public partial class InputDialog : Window
{
public string InputValue { get; private set; }
public InputDialog()
{
InitializeComponent();
}
private void ConfirmButton_Click(object sender, RoutedEventArgs e)
{
InputValue = InputTextBox.Text;
DialogResult = true; // 设置返回结果为 true
Close(); // 关闭窗口
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false; // 设置返回结果为 false
Close(); // 关闭窗口
}
}
}

View File

@@ -0,0 +1,29 @@
<UserControl x:Class="Serein.WorkBench.Themes.ObjectViewerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Serein.WorkBench.Themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<!-- 按钮 -->
<RowDefinition Height="*" />
<!-- 树视图 -->
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<!--Click="RefreshButton_Click"
Click="TimerRefreshButton_Click"-->
<!--<Button Grid.Row="0" HorizontalAlignment="Left" Margin="4,2,4,2" Content="刷新" Width="100" Height="20" Name="RefreshButton"/>-->
<!--<Button Grid.Row="0" HorizontalAlignment="Left" Margin="14,2,4,2" Content="监视" Width="100" Height="20" Name="TimerRefreshButton"/>-->
<Button Grid.Row="0" HorizontalAlignment="Left" Margin="14,2,4,2" Content="添加监视表达式" Width="100" Height="20" Name="AddMonitorExpressionButton" Click="AddMonitorExpressionButton_Click"/>
</StackPanel>
<!-- 刷新按钮 -->
<!-- 树视图,用于显示对象属性 -->
<TreeView FontSize="13" x:Name="ObjectTreeView" Grid.Row="1" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,452 @@
using Serein.NodeFlow.Base;
using Serein.NodeFlow.Tool.SereinExpression;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Xml.Linq;
using static Serein.WorkBench.Themes.TypeViewerWindow;
using static System.Collections.Specialized.BitVector32;
namespace Serein.WorkBench.Themes
{
public class FlowDataDetails
{
/// <summary>
/// 属性名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 属性类型
/// </summary>
public TreeItemType ItemType { get; set; }
/// <summary>
/// 数据类型
/// </summary>
public Type DataType { get; set; }
/// <summary>
/// 数据
/// </summary>
public object DataValue { get; set; }
/// <summary>
/// 数据路径
/// </summary>
public string DataPath { get; set; }
}
/// <summary>
/// ObjectViewerControl.xaml 的交互逻辑
/// </summary>
public partial class ObjectViewerControl : UserControl
{
private object _objectInstance;
public string NodeGuid { get;set; }
// private NodeModelBase _nodeFlowData;
public ObjectViewerControl()
{
InitializeComponent();
}
/// <summary>
/// 加载对象信息,展示其成员
/// </summary>
/// <param name="obj">要展示的对象</param>
//public void LoadObjectInformation(NodeModelBase nodeModel)
public void LoadObjectInformation(object obj)
{
if (obj == null)
return;
//IsTimerRefres = false;
//TimerRefreshButton.Content = "定时刷新";
_objectInstance = obj;
RefreshObjectTree(obj);
}
private void AddMonitorExpressionButton_Click(object sender, RoutedEventArgs e)
{
}
/// <summary>
/// 刷新对象属性树
/// </summary>
public void RefreshObjectTree(object obj)
{
if (obj is null)
return;
// _objectInstance = obj;
var objectType = obj.GetType();
FlowDataDetails flowDataDetails = new FlowDataDetails
{
Name = objectType.Name,
DataType = objectType,
DataValue = obj
};
var rootNode = new TreeViewItem
{
Header = objectType.Name,
Tag = flowDataDetails,
};
// 添加占位符节点
AddPlaceholderNode(rootNode);
ObjectTreeView.Items.Clear();
ObjectTreeView.Items.Add(rootNode);
// 监听展开事件
rootNode.Expanded += TreeViewItem_Expanded;
// 自动展开第一层
rootNode.IsExpanded = true; // 直接展开根节点
// 加载根节点的属性和字段
if (rootNode.Items.Count == 1 && rootNode.Items[0] is TreeViewItem placeholder && placeholder.Header.ToString() == "Loading...")
{
rootNode.Items.Clear();
AddMembersToTreeNode(rootNode, obj, objectType);
}
}
/// <summary>
/// 添加父节点
/// </summary>
/// <param name="node"></param>
private static void AddPlaceholderNode(TreeViewItem node)
{
node.Items.Add(new TreeViewItem { Header = "Loading..." });
}
/// <summary>
/// 展开子项事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
var item = (TreeViewItem)sender;
if (item.Items.Count == 1 && item.Items[0] is TreeViewItem placeholder && placeholder.Header.ToString() == "Loading...")
{
item.Items.Clear();
if (item.Tag is FlowDataDetails flowDataDetails) // FlowDataDetails flowDataDetails object obj
{
AddMembersToTreeNode(item, flowDataDetails.DataValue, flowDataDetails.DataType);
}
}
}
/// <summary>
/// 反射对象数据添加子节点
/// </summary>
/// <param name="treeViewNode"></param>
/// <param name="obj"></param>
/// <param name="type"></param>
private static void AddMembersToTreeNode(TreeViewItem treeViewNode, object obj, Type type)
{
// 获取属性和字段
var members = type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
foreach (var member in members)
{
TreeViewItem memberNode = ConfigureTreeViewItem(obj, member);
treeViewNode.Items.Add(memberNode);
//if (ConfigureTreeItemMenu(memberNode, member, out ContextMenu? contextMenu))
//{
// memberNode.ContextMenu = contextMenu; // 设置子项节点的事件
//}
}
}
/// <summary>
/// 配置右键菜单功能
/// </summary>
/// <param name="obj"></param>
/// <param name="member"></param>
/// <returns></returns>
private static TreeViewItem ConfigureTreeViewItem(object obj, MemberInfo member)
{
TreeViewItem memberNode = new TreeViewItem { Header = member.Name };
if (member is PropertyInfo property)
{
FlowDataDetails flowDataDetails = new FlowDataDetails
{
ItemType = TreeItemType.Property,
DataType = property.PropertyType,
Name = property.Name,
DataValue = property,
};
memberNode.Tag = flowDataDetails;
string propertyValue = GetPropertyValue(obj, property);
memberNode.Header = $"{property.Name} : {property.PropertyType.Name} = {propertyValue}";
if (!property.PropertyType.IsPrimitive && property.PropertyType != typeof(string))
{
AddPlaceholderNode(memberNode);
memberNode.Expanded += TreeViewItem_Expanded;
}
}
else if (member is FieldInfo field)
{
FlowDataDetails flowDataDetails = new FlowDataDetails
{
ItemType = TreeItemType.Field,
DataType = field.FieldType,
Name = field.Name,
DataValue = field,
};
memberNode.Tag = flowDataDetails;
string fieldValue = GetFieldValue(obj, field);
memberNode.Header = $"{field.Name} : {field.FieldType.Name} = {fieldValue}";
if (!field.FieldType.IsPrimitive && field.FieldType != typeof(string))
{
AddPlaceholderNode(memberNode);
memberNode.Expanded += TreeViewItem_Expanded;
}
}
return memberNode;
}
/// <summary>
/// 获取属性类型的成员
/// </summary>
/// <param name="obj"></param>
/// <param name="property"></param>
/// <returns></returns>
private static string GetPropertyValue(object obj, PropertyInfo property)
{
try
{
var value = property.GetValue(obj);
return value?.ToString() ?? "null";
}
catch
{
return "Error";
}
}
/// <summary>
/// 获取字段类型的成员
/// </summary>
/// <param name="obj"></param>
/// <param name="field"></param>
/// <returns></returns>
private static string GetFieldValue(object obj, FieldInfo field)
{
try
{
var value = field.GetValue(obj);
return value?.ToString() ?? "null";
}
catch
{
return "Error";
}
}
/// <summary>
/// 根据成员类别配置右键菜单
/// </summary>
/// <param name="memberNode"></param>
/// <param name="member"></param>
/// <param name="contextMenu"></param>
/// <returns></returns>
private static bool ConfigureTreeItemMenu(TreeViewItem memberNode, MemberInfo member, out ContextMenu? contextMenu)
{
bool isChange = false;
if (member is PropertyInfo property)
{
//isChange = true;
contextMenu = new ContextMenu();
}
else if (member is MethodInfo method)
{
//isChange = true;
contextMenu = new ContextMenu();
}
else if (member is FieldInfo field)
{
isChange = true;
contextMenu = new ContextMenu();
contextMenu.Items.Add(MainWindow.CreateMenuItem($"取值表达式", (s, e) =>
{
string fullPath = ObjectViewerControl.GetNodeFullPath(memberNode);
string copyValue = "@Get " + fullPath;
Clipboard.SetDataObject(copyValue);
}));
//contextMenu.Items.Add(MainWindow.CreateMenuItem($"监视中断", (s, e) =>
//{
// string fullPath = GetNodeFullPath(memberNode);
// Clipboard.SetDataObject(fullPath);
// OpenInputDialog((exp) =>
// {
// if (node.DebugSetting.InterruptExpression.Contains(exp))
// {
// Console.WriteLine("表达式已存在");
// }
// else
// {
// node.DebugSetting.InterruptExpression.Add(exp);
// }
// });
//}));
}
else
{
contextMenu = new ContextMenu();
}
return isChange;
}
/// <summary>
/// 获取当前节点的完整路径,例如 "node1.node2.node3.node4"
/// </summary>
/// <param name="node">目标节点</param>
/// <returns>节点路径</returns>
private static string GetNodeFullPath(TreeViewItem node)
{
if (node == null)
return string.Empty;
FlowDataDetails flowDataDetails = (FlowDataDetails)node.Tag;
var parent = GetParentTreeViewItem(node);
if (parent != null)
{
// 递归获取父节点的路径,并拼接当前节点的 Header
return $"{GetNodeFullPath(parent)}.{flowDataDetails.Name}";
}
else
{
// 没有父节点,则说明这是根节点,直接返回 Header
return "";
// return typeNodeDetails.Name.ToString();
}
}
/// <summary>
/// 获取指定节点的父级节点
/// </summary>
/// <param name="node">目标节点</param>
/// <returns>父节点</returns>
private static TreeViewItem GetParentTreeViewItem(TreeViewItem node)
{
DependencyObject parent = VisualTreeHelper.GetParent(node);
while (parent != null && !(parent is TreeViewItem))
{
parent = VisualTreeHelper.GetParent(parent);
}
return parent as TreeViewItem;
}
private static InputDialog OpenInputDialog(Action<string> action)
{
var inputDialog = new InputDialog();
inputDialog.Closed += (s, e) =>
{
if (inputDialog.DialogResult == true)
{
string userInput = inputDialog.InputValue;
action?.Invoke(userInput);
}
};
inputDialog.ShowDialog();
return inputDialog;
}
///// <summary>
///// 刷新按钮的点击事件
///// </summary>
//private void RefreshButton_Click(object sender, RoutedEventArgs e)
//{
// RefreshObjectTree();
//}
//private bool IsTimerRefres = false;
//private void TimerRefreshButton_Click(object sender, RoutedEventArgs e)
//{
// if (IsTimerRefres)
// {
// IsTimerRefres = false;
// TimerRefreshButton.Content = "定时刷新";
// }
// else
// {
// IsTimerRefres = true;
// TimerRefreshButton.Content = "取消刷新";
// _ = Task.Run(async () => {
// while (true)
// {
// if (IsTimerRefres)
// {
// Application.Current.Dispatcher.Invoke(() =>
// {
// RefreshObjectTree(); // 刷新UI
// });
// await Task.Delay(100);
// }
// else
// {
// break;
// }
// }
// IsTimerRefres = false;
// });
// }
//}
}
}

View File

@@ -0,0 +1,8 @@
<UserControl x:Class="RealTimeObjectViewer.ObjectViewerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Serein.WorkBench.Themes">
<Grid>
<TreeView x:Name="ObjectTreeView" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Serein.WorkBench.Themes
{
/// <summary>
/// ObjectViewerWindow.xaml 的交互逻辑
/// </summary>
public partial class ObjectViewerControl : UserControl
{
public ObjectViewerControl()
{
InitializeComponent();
}
private object _objectInstance;
private Action _closeCallback;
public void LoadObjectInformation(object obj,Action closeCallback)
{
if (obj == null || closeCallback == null)
return;
_closeCallback = closeCallback;
_objectInstance = obj;
var objectType = obj.GetType();
var rootNode = new TreeViewItem { Header = objectType.Name, Tag = obj };
// 添加占位符节点
AddPlaceholderNode(rootNode);
ObjectTreeView.Items.Clear();
ObjectTreeView.Items.Add(rootNode);
// 监听展开事件
rootNode.Expanded += TreeViewItem_Expanded;
}
private void AddPlaceholderNode(TreeViewItem node)
{
node.Items.Add(new TreeViewItem { Header = "Loading..." });
}
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
var item = (TreeViewItem)sender;
if (item.Items.Count == 1 && item.Items[0] is TreeViewItem placeholder && placeholder.Header.ToString() == "Loading...")
{
item.Items.Clear();
if (item.Tag is object obj)
{
var objectType = obj.GetType();
AddMembersToTreeNode(item, obj, objectType);
}
}
}
private void AddMembersToTreeNode(TreeViewItem node, object obj, Type type)
{
// 获取属性和字段
var members = type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
foreach (var member in members)
{
TreeViewItem memberNode = ConfigureTreeViewItem(obj, member);
node.Items.Add(memberNode);
}
}
private TreeViewItem ConfigureTreeViewItem(object obj, MemberInfo member)
{
TreeViewItem memberNode = new TreeViewItem { Header = member.Name };
if (member is PropertyInfo property)
{
string propertyValue = GetPropertyValue(obj, property);
memberNode.Header = $"{property.Name} : {property.PropertyType.Name} = {propertyValue}";
if (!property.PropertyType.IsPrimitive && property.PropertyType != typeof(string))
{
AddPlaceholderNode(memberNode);
memberNode.Expanded += TreeViewItem_Expanded;
}
}
else if (member is FieldInfo field)
{
string fieldValue = GetFieldValue(obj, field);
memberNode.Header = $"{field.Name} : {field.FieldType.Name} = {fieldValue}";
if (!field.FieldType.IsPrimitive && field.FieldType != typeof(string))
{
AddPlaceholderNode(memberNode);
memberNode.Expanded += TreeViewItem_Expanded;
}
}
return memberNode;
}
private string GetPropertyValue(object obj, PropertyInfo property)
{
try
{
var value = property.GetValue(obj);
return value?.ToString() ?? "null";
}
catch
{
return "Error";
}
}
private string GetFieldValue(object obj, FieldInfo field)
{
try
{
var value = field.GetValue(obj);
return value?.ToString() ?? "null";
}
catch
{
return "Error";
}
}
private void Window_Closed(object sender, EventArgs e)
{
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
_closeCallback?.Invoke();
}
}
}

View File

@@ -6,6 +6,7 @@
xmlns:local="clr-namespace:Serein.WorkBench.Themes"
mc:Ignorable="d"
Topmost="True"
Title="TypeViewerWindow" Height="300" Width="300">
<Grid>
<Grid>

View File

@@ -32,7 +32,7 @@ namespace Serein.WorkBench.Themes
if (Type == null)
return;
TypeNodeDetails typeNodeDetails = new TypeNodeDetails
NodeFlowDataObjectDetails typeNodeDetails = new NodeFlowDataObjectDetails
{
Name = Type.Name,
DataType = Type,
@@ -64,7 +64,7 @@ namespace Serein.WorkBench.Themes
if (item.Items.Count == 1 && item.Items[0] is TreeViewItem placeholder && placeholder.Header.ToString() == "Loading...")
{
item.Items.Clear();
if (item.Tag is TypeNodeDetails typeNodeDetails)
if (item.Tag is NodeFlowDataObjectDetails typeNodeDetails)
{
AddMembersToTreeNode(item, typeNodeDetails.DataType);
}
@@ -103,7 +103,7 @@ namespace Serein.WorkBench.Themes
TreeViewItem memberNode = new TreeViewItem { Header = member.Name };
if (member is PropertyInfo property)
{
TypeNodeDetails typeNodeDetails = new TypeNodeDetails
NodeFlowDataObjectDetails typeNodeDetails = new NodeFlowDataObjectDetails
{
ItemType = TreeItemType.Property,
DataType = property.PropertyType,
@@ -124,7 +124,7 @@ namespace Serein.WorkBench.Themes
}
else if (member is MethodInfo method)
{
TypeNodeDetails typeNodeDetails = new TypeNodeDetails
NodeFlowDataObjectDetails typeNodeDetails = new NodeFlowDataObjectDetails
{
ItemType = TreeItemType.Method,
DataType = typeof(MethodInfo),
@@ -139,7 +139,7 @@ namespace Serein.WorkBench.Themes
}
else if (member is FieldInfo field)
{
TypeNodeDetails typeNodeDetails = new TypeNodeDetails
NodeFlowDataObjectDetails typeNodeDetails = new NodeFlowDataObjectDetails
{
ItemType = TreeItemType.Field,
DataType = field.FieldType,
@@ -201,7 +201,7 @@ namespace Serein.WorkBench.Themes
if (node == null)
return string.Empty;
TypeNodeDetails typeNodeDetails = (TypeNodeDetails)node.Tag;
NodeFlowDataObjectDetails typeNodeDetails = (NodeFlowDataObjectDetails)node.Tag;
var parent = GetParentTreeViewItem(node);
if (parent != null)
{
@@ -210,10 +210,8 @@ namespace Serein.WorkBench.Themes
}
else
{
return "";
// 没有父节点,则说明这是根节点,直接返回 Header
return "";
// return typeNodeDetails.Name.ToString();
}
}
@@ -235,7 +233,7 @@ namespace Serein.WorkBench.Themes
public class TypeNodeDetails
public class NodeFlowDataObjectDetails
{
/// <summary>
/// 属性名称
@@ -245,8 +243,6 @@ namespace Serein.WorkBench.Themes
/// 属性类型
/// </summary>
public TreeItemType ItemType { get; set; }
/// <summary>
/// 数据类型
/// </summary>