WPF实现关系图

该文档用于:WPF内嵌VIS.JS实现关系图,交互通过调用JS实现

1 安装

1.1 WPF 端安装以下包

csharp 复制代码
<package id="CefSharp.Common"/>
<package id="CefSharp.Wpf"/>

1.2 WPF 框架使用Prism

csharp 复制代码
<package id="Prism.Core" />
<package id="Prism.Unity" />
<package id="Prism.Wpf" />

1.3 关系图使用 VIS.JS
VIS.JS官方文档
VIS.JS官方示例

2 使用(部分代码)

2.1 XAML

csharp 复制代码
<cefSharp:ChromiumWebBrowser x:Name="browser"
                             Address="{Binding Address, Mode=TwoWay}"
                             AllowDrop="True" />

2.2 XAML.CS

csharp 复制代码
private ViewModel _vm;
public View()
{
    InitializeComponent();
    this.DataContext = _vm = (ViewModel)ServiceLocator.Current.GetInstance<ViewModel>(); ;

    browser.LoadError += Browser_LoadError;
    browser.IsBrowserInitializedChanged += Browser_IsBrowserInitializedChanged;
}

private void Browser_IsBrowserInitializedChanged(object sender, DependencyPropertyChangedEventArgs args)
{
    if (args.NewValue is bool isInitialized && isInitialized == true)
    {
        //browser.ShowDevTools();
        _vm.OnBrowserInitialized(browser);
    }
}

private void Browser_LoadError(object sender, LoadErrorEventArgs args)
{
    if (args.ErrorCode == CefErrorCode.Aborted)
        return;
    args.Frame.LoadHtml($"<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /></head><body><h2>无法展示该网页</h2><br><h3>错误代码:{args.ErrorCode}</h3></body></html>");
}
}

2.3 ViewModel
建议ViewModel注册为单例

csharp 复制代码
public class ViewModel: BindableBase{
	private ChromiumWebBrowser _webBrowser;

 private string _address;
 public string Address
   {
       get { return _address; }
       set
       {
           if (_address == value)
               return;
           SetProperty(ref _address, value, "Address");
       }
   }

	//初始化CEF
	private void InitBrowser()
	{
	    try
	      {
	          var setting = new CefSettingsBase();
	          setting.RegisterScheme(new CefCustomScheme
	          {
	              SchemeName = CefSharpSchemeHandlerFactory.SchemeName,
	              SchemeHandlerFactory = new CefSharpSchemeHandlerFactory()
	          });
	          setting.WindowlessRenderingEnabled = true;
	          setting.Locale = "zh-CN";
	          setting.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3870.400";
	
	          if (!Cef.IsInitialized)
	              Cef.Initialize(setting, true);
	      }
	      catch (Exception ex)
	      {
	      }
	}

	//
	public void OnBrowserInitialized(ChromiumWebBrowser webBrowser)
	{
	    try
	    {
	        _webBrowser = webBrowser;
	        _webBrowser.Load("index.html");
	        _webBrowser.FrameLoadEnd += (sender, e) =>
	        {
	            if (e.Frame.IsMain)
	            {
	                var str = "(function(){CefSharp.BindObjectAsync('boundAsync');})()";
	                _webBrowser.GetFocusedFrame().EvaluateScriptAsync(str);
	            }
	        };
	        _webBrowser.JavascriptObjectRepository.Register("boundAsync", ServiceLocator.Current.GetInstance<ViewModel>(), true, BindingOptions.DefaultBinder);
	    }
	    catch (Exception ex)
	    {
	    }
	}
}

2.4 WPF调用JS

csharp 复制代码
//传参
if (_webBrowser != null && _webBrowser.IsBrowserInitialized)
    var result = _webBrowser.EvaluateScriptAsync($"addNodes({json});");
//不传参      
if (_webBrowser != null && _webBrowser.IsBrowserInitialized)
    var result = _webBrowser.EvaluateScriptAsync($"clearNetwork();");

2.5 JS 调用C#方法

js 复制代码
//传参
window.boundAsync.downloadCommand(id);
//不传参
window.boundAsync.iterationsDoneCommand("");

2.6 C# 端方法

csharp 复制代码
public void DownloadAppCommand(object obj)
{//
}

2.7 HTML

index.html

html 复制代码
 <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script
      type="text/javascript"
      src="standalone/umd/vis-network.min.js"
    ></script>
    <script src="js/jquery-3.7.0.min.js"></script>
    
    <link rel="stylesheet" href="css/index.css">
    <script>
      document.addEventListener("contextmenu", function (e) {
        e.preventDefault();
      });
    </script>
    <title>HomologyView V1.0</title>
  </head>
  <body oncontextmenu="return false;">
    <div>
      <div id="mynetwork"></div>

      <div id="context-menu">
        <ul id="context-menu-list"></ul>
      </div>
    </div>

    <script src="js/index.js"></script>
  
  </body>
</html>

2.8 JS

index.js

javascript 复制代码
var container = document.getElementById("mynetwork");
var network = null;
var options;
var nodes_data = [];
var edges_data = [];

options = {
  nodes: {
    brokenImage: "images/app.svg",
    chosen: true,
    borderWidth: 0, // 默认边框宽度
    borderWidthSelected: 2, // 选中时的边框宽度
    opacity: 1,
    fixed: {
      x: false,
      y: false,
    },
    font: {
      size: 14, // px
      strokeWidth: 0, // px
      align: "center",
      multi: false,
    },
    image: "images/app.svg",
    imagePadding: {
      left: 1,
      top: 1,
      bottom: 1,
      right: 1,
    },

    labelHighlightBold: true,
    level: undefined,
    mass: 0.8,
    physics: true,
    shape: "image",
    shapeProperties: {
      borderDashes: false, // only for borders
      borderRadius: 6, // only for box shape
      interpolation: false, // only for image and circularImage shapes
      useImageSize: false, // only for image and circularImage shapes
      useBorderWithImage: true, // only for image shape
      coordinateOrigin: "center", // only for image and circularImage shapes
    },
    size: 30,
  },
  edges: {
    arrows: {
      to: {
        enabled: true,
      },
    },
    endPointOffset: {
      from: 0,
      to: 0,
    },
    arrowStrikethrough: true,
    chosen: true,
    color: {
      inherit: "from",
    },
    dashes: false,
    font: {
      vadjust: 0,
      size: 14, // px
      align: "middle", //horizontal,top,middle,bottom
      multi: false,
    },
    hidden: false,
    hoverWidth: 2,
    labelHighlightBold: true,
    physics: true,
    selectionWidth: 2,
    smooth: {
      enabled: false,
    },
  },

  physics: {
    enabled: true,
    // stabilization: false, //动态加载
    // timestep: 0.8, // 时间步长,越大越快
    timestep: 0.5, // 时间步长,越大越快
    solver: "barnesHut", // 使用 barnesHut 算法提高性能forceAtlas2Based、hierarchicalRepulsion
    barnesHut: {
      // gravitationalConstant: -50000, //引力吸引。所以该值为负。如果你想要更强的排斥力,请减小该值(因此为 -10000、-50000)
      gravitationalConstant: -30000, //-30000
      springConstant: 0.002, //这就是弹簧的"坚固程度"。值越高,弹簧越坚固0.001
      springLength: 80, //弹簧的静止长度。80
      damping: 0.1, //减小阻尼的值可以增加节点的移动速度
      avoidOverlap: 0.8, //[0-1]。当大于 0 时,值 1 表示最大重叠避免。
      //centralGravity: 0.3,
    },
    hierarchicalRepulsion: {
      centralGravity: 0.3,
      springLength: 100,
      springConstant: 0.005,
      nodeDistance: 320,
      damping: 0.1,
      avoidOverlap: 0.8
    },

    forceAtlas2Based: {
      gravitationalConstant: -2000,   // 减小节点间引力
      // centralGravity: 0.08,            // 增强中心引力
      springConstant: 0.005,           // 增加弹簧常数以平衡吸引力
      springLength: 40,               // 保持弹簧长度
      damping: 0.5,                   // 增加阻尼来减速停止振荡
      avoidOverlap: 0.8               // 避免节点重叠
    },

    adaptiveTimestep: true, // 启用自适应时间步长
    stabilization: {
      fit: true,
      enabled: true,
      iterations: 2000, //
      updateInterval: 500, // 调整更新间隔,以更快地看到布局变化50
      onlyDynamicEdges: false,
    },
  },

  interaction: {
    //相互作用
    dragNodes: true, //拖拽节点
    dragView: true, //是否可拖拽
    tooltipDelay: 200, //title延迟显示时间
    hideEdgesOnDrag: false, //拖拽隐藏线条
    hideEdgesOnZoom: false, //缩放隐藏线条
    hideNodesOnDrag: false, //拖拽隐藏节点
    hover: true, //悬停时显示颜色
    hoverConnectedEdges: true, //悬停突出显示边
    multiselect: false, //多选
    selectable: true, //可选节点和边
    selectConnectedEdges: true, //选中节点突出显示边
    zoomSpeed: 1, //缩放速度有多快/粗略或多慢/精确
    zoomView: true, //可放大
    navigationButtons: false, // 如果为真,则在网络画布上绘制导航按钮。这些是HTML按钮,可以使用CSS完全自定义。
  },

  layout: {
    randomSeed: 1, //布局种子
    improvedLayout: false, //执行聚类以减少节点数量
    clusterThreshold: 150,
    hierarchical: false,
  },
};


$(function () {
  clearNetwork();
});

Init();

function Init() {
  var data = {
    nodes: nodes_data,
    edges: edges_data,
  };
  network = new vis.Network(container, data, options);
  nodes_data = network.body.data.nodes;
  edges_data = network.body.data.edges;

  // 监听数据加载完成事件
  network.on("afterDrawing", function (e) {
    // 获取当前缩放比例
    var scale = network.getScale();
    // 自己添加的DOM元素跟随缩放和移动
    var imageElements = document.querySelectorAll('[id^="tag_"]');
    if (imageElements.length === 0) return;
    imageElements.forEach(function (imageElement) {
      var nodeId = imageElement.id.replace("tag_", "");
      var nodePosition = network.getPositions([nodeId])[nodeId];
      var convertPoint = network.canvasToDOM(nodePosition);
      var position = network.getBoundingBox(nodeId);

      imageElement.style.width = scale * 18 + "px";
      imageElement.style.height = scale * 18 + "px";
      imageElement.style.top =
        convertPoint.y -
        ((position.bottom - position.top) / 3) * scale +
        "px";
      imageElement.style.left =
        convertPoint.x +
        ((position.right - position.left) / 4) * scale +
        "px";
    });
  });

  //动画稳定后的处理事件
  var stabilizedTimer;
  network.on("stabilized", function (params) {
    // 会调用两次?
    console.log("动画稳定后的处理事件");
    window.clearTimeout(stabilizedTimer);
    stabilizedTimer = setTimeout(function () {
      exportNetworkPosition(network);
      options.physics.enabled = false; // 关闭物理系统
      network.setOptions(options);
    }, 2000);
  });

  network.on("stabilizationIterationsDone", function () {
    //通知C#端,节点迭代完成
    window.boundAsync.iterationsDoneCommand("");
  });

  //拦截系统右键菜单,显示自定义菜单
  network.on("oncontext", function (e) {
    e.event.preventDefault();
    var nodeId = network.getNodeAt(e.pointer.DOM);
    if (nodeId !== undefined) {
      var nodeData = nodes_data.get(nodeId);
      if (nodeData === undefined) return;
      showCustomMenu(e.pointer.DOM.x, e.pointer.DOM.y, nodeData);
    } else {
      HideCustomMenu();
    }
  });

  //选中节点
  network.on("selectNode", function (event) {
  });

  //双击节点 隐藏或者显示子节点
  network.on("doubleClick", function (params) {
    if (params.nodes.length !== 0) {
      var nodeId = params.nodes[0];
      var nodeData = nodes_data.get(nodeId);
      var nodeName = nodeData.title;
      var allChild = getAllChilds(network, nodeId, []);

      if (allChild.length > 0) {
        // 存在子节点
        if (!nodeData.ishidden) {
          // 当前节点未隐藏
          nodes_data.update([
            {
              id: nodeId,
              label: nodeName + " " + allChild.length,
              ishidden: true,
            },
          ]);

          for (var i = 0; i < allChild.length; i++) {
            nodes_data.update([{ id: allChild[i], hidden: true }]);
          }
        } else {
          // 当前节点已隐藏
          nodes_data.update([
            { id: nodeId, label: nodeName, ishidden: false },
          ]);
          for (var j = 0; j < allChild.length; j++) {
            nodes_data.update([{ id: allChild[j], hidden: false }]);
          }
        }
      }
    }
  });

  //单击节点
  network.on("click", function (params) {
    HideCustomMenu();

  });

  //拖动结束事件
  network.on("dragEnd", function (params) {
    HideCustomMenu();

    if (params.nodes.length != 0) {
      var arr = nodeMoveFun(params);
      exportNetworkPosition(network, arr);
    }
  });

  //拖动节点
  network.on("dragging", function (params) {
    //拖动进行中事件
    HideCustomMenu();
    if (params.nodes.length != 0) {
      nodeMoveFun(params);
    }
  });
}

//绘制节点
function drawNodes(jsonData) {
  options.physics.enabled = true; // 开启物理系统
  options.physics.stabilization.iterations = calculateIterations(0, jsonData.nodes.length);
  network.setOptions(options);
  
  var newData = {
    nodes: jsonData.nodes,
    edges: jsonData.edges,
  };

  // // 更新网络实例的数据并重新绘制
  network.setData(newData);
  nodes_data = network.body.data.nodes;
  edges_data = network.body.data.edges;
}

// 添加节点
function addNodes(jsonData) {
  options.physics.enabled = true; // 开启物理系统
  options.physics.stabilization.iterations = calculateIterations(nodes_data.length, jsonData.nodes.length);
  network.setOptions(options);

  // 更新网络实例中的数据
  nodes_data.add(jsonData.nodes);
  edges_data.add(jsonData.edges);

  var newData = {
    nodes: nodes_data,
    edges: edges_data,
  };

  network.setData(newData);
}

//设置节点被选中
function selectedNodeCommand(selectedNodeId) {
  if (selectedNodeId === undefined) return;
  network.selectNodes([selectedNodeId]);
  var selectedNode = nodes_data.get(selectedNodeId);
  if (selectedNode === undefined || selectedNode === null) return;

  //移至屏幕中间
  var options = {
    // scale: 1.0,
    animation: {
      duration: 1000, // 动画持续时间 (毫秒)
      easingFunction: "easeInOutQuad", // 缓动函数
    },
  };
  network.focus(selectedNode.id, options);
}

//获取当前所有节点
function GetAllNode() {
  var allNodes = network.body.data.nodes.get();

  var allCurrentNodes = allNodes.filter(function (node) {
    return !node.ishidden;
  });
  return JSON.stringify(allCurrentNodes);
}

//清空
function clearNetwork() {
  //关闭卡片
  var customMenu = document.querySelector(".card");
  customMenu.style.display = "none";

  if (network === null || network === undefined) return;
  
  // 更新网络实例中的数据
  var newData = {
    nodes: [],
    edges: [],
  };

  // // 更新网络实例的数据并重新绘制
  network.setData(newData);

  var allElements = document.querySelectorAll(
    '[id^="tag_"]'
  );
  // 从 DOM 中删除选定的元素
  allElements.forEach(function (element) {
    element.parentNode.removeChild(element);
  });
  
  console.log("初始化完成...");
}

//过滤节点
function filterNodes(jsonData) {
  console.log("过滤节点:");
  console.log(jsonData);

  var nodesToUpdate = jsonData.map(node => ({
      id: node.id,
      hidden: node.ishidden,
      ishidden: node.ishidden,
  }));
  nodes_data.update(nodesToUpdate);
}

//重绘
function redraw() {
  if (network === null || network === undefined) return;

  // network.stabilize()
  console.log("重置网络");
  options.physics.enabled = true; // 开启物理系统
  network.setOptions(options);
  network.redraw();
}

//显示自定义菜单
function showCustomMenu(x, y, nodeData) {
  if (nodeData === undefined) return;
  var customMenu = document.getElementById("context-menu");

  var contextMenuList = document.getElementById("context-menu-list");

  contextMenuList.innerHTML = "";
  var menuOptions = 
    [
      { id: "addNodeTag", text: "添加标记" },
      { id: "deleteNodeTag", text: "删除标记" },
      { id: "deleteNode", text: "删除节点" },
    ];

  menuOptions.forEach(function (item) {
    var li = document.createElement("li"); // 创建 <li> 元素
    li.id = item.id; // 设置 <li> 的 id
    li.textContent = item.text; // 设置 <li> 的文本内容
    contextMenuList.appendChild(li); // 将 <li> 插入到 <ul> 中
  });

  customMenu.style.left = x + "px";
  customMenu.style.top = y + "px";
  customMenu.style.display = "block";
  customMenu.setAttribute("data-selectednodes", JSON.stringify(nodeData));
}

//隐藏菜单
function HideCustomMenu() {
  var customMenu = document.getElementById("context-menu");
  customMenu.style.display = "none";
}

document
  .getElementById("context-menu-list")
  .addEventListener("click", function (e) {
    if (e.target && e.target.id) {
      handleMenuClick(e.target.id);
    }
  });

function handleMenuClick(action) {
  switch (action) {
    case "addNodeTag":
      addNodeTag();
      break;
    case "deleteNodeTag":
      deleteNodeTag();
      break;
    case "deleteNode":
      deleteNode();
      break;
  }
}

// //添加标签
function addNodeTag() {
  HideCustomMenu();
  var nodeInfo = document
    .getElementById("context-menu")
    .getAttribute("data-selectednodes");
  var selectedNodes = JSON.parse(nodeInfo);
  if (selectedNodes.isMarked) return;
  addMarkedNode(selectedNodes);
}

// //删除标签
function deleteNodeTag() {
  HideCustomMenu();
  var nodeInfo = document
    .getElementById("context-menu")
    .getAttribute("data-selectednodes");

  var selectedNodes = JSON.parse(nodeInfo);
  if (!selectedNodes.isMarked) return;
  console.log("删除标记:" + selectedNodes.id);
  var imageElement = document.getElementById("tag_" + selectedNodes.id);
  if (imageElement) {
    var parentNode = imageElement.parentNode;
    parentNode.removeChild(imageElement);
    nodes_data.update([
      {
        id: selectedNodes.id,
        isMarked: false,
      },
    ]);
    if (
      currentCardNode != null &&
      selectedNodes.id === currentCardNode.id
    ) {
      changeCardMarked(false);
      network.emit("selectNode", {
        nodes: [currentCardNode.id],
      });
    }
  } else {
    console.log("Image not found");
  }
}

// //添加标记
function addMarkedNode(nodeInfo) {
  //添加标记前先校验是否已经标记
  var imageId = "tag_" + nodeInfo.id;
  var imageElements = document.querySelector('[id="' + imageId + '"]');
  if (imageElements != null) {
    console.log("存在标记:" + nodeInfo.id);
    return;
  }

  var nodePosition = network.getPositions([nodeInfo.id])[nodeInfo.id];
  var convertPoint = network.canvasToDOM(nodePosition);
  var scale = network.getScale();
  var position = network.getBoundingBox(nodeInfo.id);

  var newImage = document.createElement("img");
  newImage.id = "tag_" + nodeInfo.id;
  newImage.src = "images/star.svg";
  newImage.style.width = scale * 18 + "px";
  newImage.style.height = scale * 18 + "px";
  newImage.style.position = "absolute";
  newImage.style.top =
    convertPoint.y - ((position.bottom - position.top) / 3) * scale + "px";
  newImage.style.left =
    convertPoint.x + ((position.right - position.left) / 4) * scale + "px";
  container.appendChild(newImage);

  nodeInfo.isMarked = true;
  nodes_data.update([
    {
      id: nodeInfo.id,
      isMarked: true,
    },
  ]);

  if (currentCardNode != null && nodeInfo.id === currentCardNode.id) {
    changeCardMarked(true);
    network.emit("selectNode", {
      nodes: [currentCardNode.id],
    });
  }

  console.log("添加标记成功:" + nodeInfo.id + " ==" + nodeInfo.isMarked);
}

//删除节点及其子节点
function deleteNode() {
  HideCustomMenu();

  var customMenu = document.querySelector(".card");
  customMenu.style.display = "none";

  var nodeInfo = document
    .getElementById("context-menu")
    .getAttribute("data-selectednodes");
  console.log(nodeInfo);
  var selectedNodes = JSON.parse(nodeInfo);
  if (selectedNodes.isMarked) {
    var imageElement = document.getElementById("tag_" + selectedNodes.id);
    if (imageElement) {
      var parentNode = imageElement.parentNode;
      parentNode.removeChild(imageElement);
    }
  }
  var edgesToRemove = [];
  var childNodess = [];
  var childNodes = removeNodeAndChildren(selectedNodes.id, childNodess);
  childNodes.forEach((element) => {
    var deleteNode = nodes_data.get(element);
    if (deleteNode.isMarked) {
      var imageElement = document.getElementById("tag_" + deleteNode.id);
      if (imageElement) {
        var parentNode = imageElement.parentNode;
        parentNode.removeChild(imageElement);
      }
    }

    var rootElement = document.getElementById("root_" + deleteNode.id);
    if (rootElement) {
      var parentNode = rootElement.parentNode;
      parentNode.removeChild(rootElement);
    }

    network.body.data.edges.forEach(function (edge) {
      if (edge.from === selectedNodes.id || edge.to === element) {
        edgesToRemove.push(edge.id);
      }
    });
  });

  network.body.data.nodes.remove(childNodes); 
  network.body.data.edges.remove(edgesToRemove); 
  console.log(JSON.stringify(childNodes));

  nodes_data = network.body.data.nodes;
  edges_data = network.body.data.edges;
  window.boundAsync.deleteNodesCommand(JSON.stringify(childNodes));
}

 
//大小改变事件
window.addEventListener("resize", function () {
  var customMenu = document.getElementById("context-menu");
  if (customMenu.style.display === "block") {
    customMenu.style.display = "none";
  }
});

function removeNodeAndChildren(nodeId, childNodes = []) {
  childNodes.push(nodeId);
  var connectedNodes = network.getConnectedNodes(nodeId, "to");

  connectedNodes.forEach(function (childNodeId) {
    removeNodeAndChildren(childNodeId, childNodes); // 递归调用自身来删除子节点及其子节点
  });
  return childNodes;
}

//计算迭代次数
function calculateIterations(initialNodeCount, additionalNodes, iterationFactor = 10, incrementFactor = 5, minIterations = 1000, maxIterations = 3000) {
  // 计算初始迭代次数
  let initialIterations = initialNodeCount * iterationFactor;
  
  // 计算新增节点的额外迭代次数
  let additionalIterations = additionalNodes * incrementFactor;
  
  // 总迭代次数
  let totalIterations = initialIterations + additionalIterations;
  
  // 限制最大迭代次数
  console.log("节点数量:" + (initialNodeCount + additionalNodes));
  if(totalIterations < minIterations){
    console.log("迭代次数:"+ minIterations);
    return minIterations;
  }
    
  console.log("迭代次数:"+ Math.min(totalIterations, maxIterations))
  return Math.min(totalIterations, maxIterations);
}

/*
 *获取所有子节点
 * network :图形对象
 * _thisNode :单击的节点(父节点)
 * _Allnodes :用来装子节点ID的数组
 * */
function getAllChilds(network, _thisNode, _Allnodes) {
  var _nodes = network.getConnectedNodes(_thisNode, "to");
  if (_nodes.length > 0) {
    for (var i = 0; i < _nodes.length; i++) {
      getAllChilds(network, _nodes[i], _Allnodes);
      _Allnodes.push(_nodes[i]);
    }
  }
  return _Allnodes;
}

/*
 *节点位置设置
 * network :图形对象
 * arr :本次移动的节点位置信息
 * */
function exportNetworkPosition(network, arr) {
  if (arr) {
    // 折叠过后  getPositions() 获取的位置信息里不包含隐藏的节点位置信息,这时候调用上次存储的全部节点位置,并修改这次移动的节点位置,最后保存
    var localtionPosition = JSON.parse(localStorage.getItem("position"));
    for (let index in arr) {
      localtionPosition[index] = {
        x: arr[index].x,
        y: arr[index].y,
      };
    }
    setLocal(localtionPosition);
  } else {
    var position = network.getPositions();
    setLocal(position);
  }
}

//处理本地存储,这里仅仅只能作为高级浏览器使用,ie9以下不能处理
function setLocal(position) {
  localStorage.removeItem("position");
  localStorage.setItem("position", JSON.stringify(position));
}

// 节点移动
function nodeMoveFun(params) {
  var click_node_id = params.nodes[0];
  var allsubidsarr = getAllChilds(network, click_node_id, []); // 获取所有的子节点

  if (allsubidsarr != null && allsubidsarr.length > 0) {
    // 如果存在子节点
    var positionThis = network.getPositions(click_node_id);
    var clickNodePosition = positionThis[click_node_id]; // 记录拖动后,被拖动节点的位置
    var position = JSON.parse(localStorage.getItem("position"));
    var startNodeX, startNodeY; // 记录被拖动节点的子节点,拖动前的位置
    var numNetx, numNety; // 记录被拖动节点移动的相对距离
    var positionObj = {}; // 记录移动的节点位置信息, 用于返回

    positionObj[click_node_id] = {
      x: clickNodePosition.x,
      y: clickNodePosition.y,
    }; // 记录被拖动节点位置信息
    numNetx = clickNodePosition.x - position[click_node_id].x; // 拖动的距离
    numNety = clickNodePosition.y - position[click_node_id].y;

    for (var j = 0; j < allsubidsarr.length; j++) {
      if (position[allsubidsarr[j]]) {
        startNodeX = position[allsubidsarr[j]].x; // 子节点开始的位置
        startNodeY = position[allsubidsarr[j]].y;
        network.moveNode(
          allsubidsarr[j],
          startNodeX + numNetx,
          startNodeY + numNety
        ); // 在视图上移动子节点
        positionObj[allsubidsarr[j]] = {
          x: startNodeX + numNetx,
          y: startNodeY + numNety,
        }; // 记录子节点位置信息
      }
    }
  }
  return positionObj;
}

2.9 CSS

index.css

css 复制代码
html,
body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  background-color: #f4f8fa;
}

#mynetwork {
  position: fixed;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0px solid lightgray;
}
#context-menu {
  display: none;
  position: absolute;
  z-index: 999;
  background: white;
  box-shadow: 0px 4px 16px 0px rgba(16, 47, 94, 0.16);
}

#context-menu-list {
  list-style: none;
  width: 120px;
  padding: 0;
  margin: 0;
}
#context-menu-list li {
  padding: 10px;
  cursor: pointer;
}

#context-menu-list li:hover {
  background: #f0f0f0;
}

2.10 JSON格式

javascript 复制代码
{
    "nodes": [
        {
            "id": "04c53dc6-297e-406c-bee2-022811f7a9b0",
            "label": "04c53dc6-297e-406c-bee2-022811f7a9b0",
            "image": "app.png",
            "title": "04c53dc6-297e-406c-bee2-022811f7a9b0",
            "hidden": false,
            "ishidden": false,
            "isMarked": false,
            "group": "0",
            "parents": []
        },
        {
            "id": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "label": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "image": "app.svg",
            "title": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "hidden": false,
            "ishidden": false,
            "isMarked": false,
            "group": "5",
            "parents": [
                "04c53dc6-297e-406c-bee2-022811f7a9b0"
            ]
        },
        {
            "id": "e134e243-6d02-4c88-beba-899d89e97eb4",
            "label": "e134e243-6d02-4c88-beba-899d89e97eb4",
            "image": "app.svg",
            "title": "e134e243-6d02-4c88-beba-899d89e97eb4",
            "hidden": false,
            "ishidden": false,
            "isMarked": false,
            "group": "5",
            "parents": [
                "803b96e0-2033-4fc2-95f8-699fa1daae3f"
            ]
        }
    ],
    "edges": [
        {
            "title": "title",
            "label": "label",
            "from": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "to": "e134e243-6d02-4c88-beba-899d89e97eb4",
            "ishidden": false
        },
        {
            "title": "title",
            "label": "label",
            "from": "04c53dc6-297e-406c-bee2-022811f7a9b0",
            "to": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "ishidden": false
        }
    ]
}
相关推荐
晚安苏州5 小时前
WPF DataTemplate 数据模板
wpf
甜甜不吃芥末1 天前
WPF依赖属性详解
wpf
Hat_man_1 天前
WPF制作图片闪烁的自定义控件
wpf
晚安苏州2 天前
WPF Binding 绑定
wpf·wpf binding·wpf 绑定
wangnaisheng2 天前
【WPF】RenderTargetBitmap的使用
wpf
dotent·3 天前
WPF 完美解决改变指示灯的颜色
wpf
orangapple5 天前
WPF 用Vlc.DotNet.Wpf实现视频播放、停止、暂停功能
wpf·音视频
ysdysyn5 天前
wpf mvvm 数据绑定数据(按钮文字表头都可以),根据长度进行换行,并把换行的文字居中
c#·wpf·mvvm
orangapple5 天前
WPF 使用LibVLCSharp.WPF实现视频播放、停止、暂停功能
wpf
晚安苏州5 天前
WPF ControlTemplate 控件模板
wpf