You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C# WPF中实现支持节点拖拽与边创建的交互式Dijkstra图?

嘿,我帮你梳理下在WPF里实现这个功能的关键步骤,拆解开来其实没那么复杂~

一、实现可拖拽的椭圆节点

首先得做一个能被拖拽的自定义节点控件,核心是处理鼠标事件:

  • 先定义一个Node类(可以直接继承Ellipse,或者封装成UserControl),给它加Id、Position这类属性,方便后续识别和绑定位置,还要加一个PositionChanged事件,用来通知边更新位置。
  • 处理三个关键鼠标事件:
    • MouseDown:记录拖拽起始点,开启拖拽状态并捕获鼠标
    • MouseMove:根据鼠标位移更新节点在Canvas上的位置
    • MouseUp:结束拖拽并释放鼠标捕获

代码示例:

public class Node : Ellipse
{
    public event EventHandler PositionChanged;
    private Point _dragStartPoint;
    private bool _isDragging;

    public Node()
    {
        // 给节点设置默认样式,比如椭圆大小、颜色
        Width = 40;
        Height = 40;
        Fill = Brushes.LightBlue;
        Stroke = Brushes.Black;

        // 绑定事件
        MouseDown += Node_MouseDown;
        MouseMove += Node_MouseMove;
        MouseUp += Node_MouseUp;
    }

    private void Node_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            _dragStartPoint = e.GetPosition(this);
            _isDragging = true;
            CaptureMouse();
        }
    }

    private void Node_MouseMove(object sender, MouseEventArgs e)
    {
        if (_isDragging && e.LeftButton == MouseButtonState.Pressed)
        {
            var canvas = Parent as Canvas;
            if (canvas == null) return;

            Point currentPoint = e.GetPosition(canvas);
            double offsetX = currentPoint.X - _dragStartPoint.X;
            double offsetY = currentPoint.Y - _dragStartPoint.Y;

            // 更新节点在Canvas上的位置
            Canvas.SetLeft(this, Canvas.GetLeft(this) + offsetX);
            Canvas.SetTop(this, Canvas.GetTop(this) + offsetY);

            _dragStartPoint = e.GetPosition(this);
            // 触发位置变化事件,让边跟着更新
            PositionChanged?.Invoke(this, EventArgs.Empty);
        }
    }

    private void Node_MouseUp(object sender, MouseButtonEventArgs e)
    {
        if (_isDragging)
        {
            _isDragging = false;
            ReleaseMouseCapture();
        }
    }
}
  • 添加节点到Canvas:可以做个按钮,点击时在鼠标位置生成新的Node实例,加到Canvas的Children集合里。
二、创建与节点位置绑定的边

边用Line控件实现,关键是让它的起点和终点自动跟随节点位置变化:

  • 定义Edge类继承Line,关联两个Node,并监听它们的位置变化事件:
public class Edge : Line
{
    public Node StartNode { get; set; }
    public Node EndNode { get; set; }
    public int Weight { get; set; } = 1; // 默认权重为1,后续可以让用户修改

    public Edge(Node start, Node end)
    {
        StartNode = start;
        EndNode = end;
        Stroke = Brushes.Black;
        StrokeThickness = 2;

        // 初始化边的位置
        UpdateLinePosition();

        // 监听节点位置变化,实时更新边
        StartNode.PositionChanged += (s, e) => UpdateLinePosition();
        EndNode.PositionChanged += (s, e) => UpdateLinePosition();
    }

    private void UpdateLinePosition()
    {
        // 计算节点中心位置(因为椭圆的定位是左上角,所以要加半宽半高)
        X1 = Canvas.GetLeft(StartNode) + StartNode.Width / 2;
        Y1 = Canvas.GetTop(StartNode) + StartNode.Height / 2;
        X2 = Canvas.GetLeft(EndNode) + EndNode.Width / 2;
        Y2 = Canvas.GetTop(EndNode) + EndNode.Height / 2;
    }
}
  • 创建边的逻辑:可以做个“添加边”的模式,比如点击按钮进入加边状态,先点击第一个节点作为起点,再点击第二个节点作为终点,然后生成Edge实例加到Canvas里。注意要把边的ZIndex设得比节点低,不然会挡住节点影响拖拽。
三、从Canvas读取元素生成图结构

把可视化的节点和边转换成算法能识别的图数据结构:

  • 遍历Canvas的Children,筛选出所有Node和Edge实例
  • 构建邻接表(最适合Dijkstra的结构),用字典存储每个节点的邻接节点和对应权重:
private Dictionary<Node, List<(Node Neighbor, int Weight)>> BuildGraph()
{
    var graph = new Dictionary<Node, List<(Node, int)>>();
    var nodes = MyCanvas.Children.OfType<Node>().ToList();
    var edges = MyCanvas.Children.OfType<Edge>().ToList();

    // 初始化每个节点的邻接列表
    foreach (var node in nodes)
    {
        graph[node] = new List<(Node, int)>();
    }

    // 填充邻接关系(如果是无向图,要添加双向边)
    foreach (var edge in edges)
    {
        graph[edge.StartNode].Add((edge.EndNode, edge.Weight));
        graph[edge.EndNode].Add((edge.StartNode, edge.Weight));
    }

    return graph;
}
四、Dijkstra算法的实现

有了图结构,算法部分就和常规实现一致了,只是用Node作为节点类型:

public Dictionary<Node, int> RunDijkstra(Node startNode)
{
    var graph = BuildGraph();
    var distances = new Dictionary<Node, int>();
    // 用优先队列存储(节点,当前距离),按距离排序
    var priorityQueue = new PriorityQueue<(Node, int), int>();

    // 初始化所有节点的距离为无穷大
    foreach (var node in graph.Keys)
    {
        distances[node] = int.MaxValue;
    }
    distances[startNode] = 0;
    priorityQueue.Enqueue((startNode, 0), 0);

    while (priorityQueue.Count > 0)
    {
        var (currentNode, currentDistance) = priorityQueue.Dequeue();

        // 如果当前记录的距离已经比队列里的小,直接跳过
        if (currentDistance > distances[currentNode])
            continue;

        // 遍历邻接节点,更新最短距离
        foreach (var (neighbor, weight) in graph[currentNode])
        {
            int newDistance = currentDistance + weight;
            if (newDistance < distances[neighbor])
            {
                distances[neighbor] = newDistance;
                priorityQueue.Enqueue((neighbor, newDistance), newDistance);
            }
        }
    }

    return distances;
}

内容的提问来源于stack exchange,提问作者doggoz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 08:53:17