项目仓库地址:https://github.com/MaverickGao/Mind_Map_Tool
一、背景
公司的项目需要同步第三方数据源到本地数据库,在根据同步的数据进行一些指标的计算,随着指标越来越多,同事整理出来了一个文档,通过查阅文档可以快速定位指标的数据来源。但是这个文档数据量大,查看起来并不是那么容易,期望可以通过思维导图的形式将这些指标、数据来源关联起来,方便查阅。
以下是文档的样例:
从上表可以看出,A列是表名或指标编码,可以通过D列类型来进行区分,0为基础指标,就是从表数据直接获取的指标;1为计算指标,就是通过表数据计算而来,或者通过不同的指标计算得来的。而C列则是将这些指标关联起来的,如果有多个父节点,则使用逗号“,”分隔。
二、结果演示
这里首先展示项目的最终成功,方便后面开发思路的讲解。
-
项目打包以后是一个桌面应用程序,可以通过打开exe文件来运行,通过替换Resource目录下的Excel文件来更新数据
-
项目启动以后,先是一个加载页面
-
加载页面关闭后进到我们的搜索页面,首先是一个搜索框,可以通过模糊搜索指标编码或者指标名称进行搜索
-
当在搜索框输入文字后,就会根据结果输出标签列表,注意列表中只展示标签,不展示表
-
当我们点击任一行指标编码后,就会在列表下方展示对应的思维导图,从这个指标一直指向它的数据来源
三、代码开发
3.1、需求分析
公司项目同步数据源以及计算指标的逻辑大概如下:
可以看出指标a的数据链路为:A表 → B表 → C表 → 指标a,同时指标a也可能同其他的表数据或指标计算出下一个指标,所以最终全部相关数据链路大致是下面的结构:
而我们要做的就是以任一指标为起点,将其数据来源,即它的左边相关的结点全部展示为思维导图,方便查阅。
3.2、技术选型
最终选定了使用 C# + NanUI + NPOI + HTML + JsMind来实现这个小程序。
-
为什么使用C#
之前一直使用Python脚本来做一些小程序,来简化工作,一来推荐给同事使用,他需要安装环境不太容易接受,再来我自己维护的话又要花费大量时间,所以想要做一个桌面应用,点击即用。刚巧这两天学了C#基础,学会的知识足够开发这款软件了。
-
为什么使用NanUI
主要是不会前端代码,网上找了个现成的UI框架进行开发,这个NanUI还是很惊喜的,开发简单,功能齐全,而且还很好看。
-
NPOI
用来解析Excel。
-
HTML + JsMind
根据网上的推荐选择了JsMind来实现思维导图。
3.3、开发设计
根据3.1需求分析可知,首先我需要将Excel进行解析,然后将读取的Excel数据分别组装成搜索列表数据对象、以及思维导图数据对象。
四、代码实现
4.1、代码架构搭建
使用 Windows窗体应用 .NET7.0 以及NanUI 0.9.90版本搭建小程序的框架,这里查看NanUI官网文档就可以了,就不过多赘述,可以看以下连接:
https://www.cnblogs.com/linxuanchen/p/NanUI-Examples-01-Start-Using-NanUI.html
https://github.com/XuanchenLin/NanUI/blob/master/docs/zh-CN/README.md
本项目的加载动画就是粘的NanUIDemo代码。
4.2、使用NPOI解析Excel文件
解析Excel非常简单,可以查看以下文档:https://blog.51cto.com/u_15057843/2635634
public static DataTable excelToTable(string file)
{
DataTable dt = new DataTable();
IWorkbook workbook;
string fileExt = Path.GetExtension(file).ToLower();
using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read))
{
if (fileExt == ".xlsx")
{
workbook = new XSSFWorkbook(fs);
}
else if (fileExt == ".xls")
{
workbook = new HSSFWorkbook(fs);
}
else
{
workbook = null;
}
if (workbook == null)
{
return new DataTable();
}
// 拿到第一个Sheet页
ISheet sheet = workbook.GetSheetAt(0);
//表头
IRow header = sheet.GetRow(sheet.FirstRowNum);
List<int> columns = new List<int>();
for (int i = 0; i < header.LastCellNum; i++)
{
object obj = getValueType(header.GetCell(i));
if (obj == null || obj.ToString() == string.Empty)
{
dt.Columns.Add(new DataColumn("Columns" + i.ToString()));
}
else
dt.Columns.Add(new DataColumn(obj.ToString()));
columns.Add(i);
}
//数据
for (int i = sheet.FirstRowNum + 1; i <= sheet.LastRowNum; i++)
{
DataRow dr = dt.NewRow();
bool hasValue = false;
foreach (int j in columns)
{
dr[j] = getValueType(sheet.GetRow(i).GetCell(j));
if (dr[j] != null && dr[j].ToString() != string.Empty)
{
hasValue = true;
}
}
if (hasValue)
{
dt.Rows.Add(dr);
}
}
}
return dt;
}
/// <summary>
/// 获取单元格类型
/// </summary>
/// <param name="cell">目标单元格</param>
/// <returns></returns>
private static object getValueType(ICell cell)
{
if (cell == null)
return null;
switch (cell.CellType)
{
case CellType.Blank:
return null;
case CellType.Boolean:
return cell.BooleanCellValue;
case CellType.Numeric:
return cell.NumericCellValue;
case CellType.String:
return cell.StringCellValue;
case CellType.Error:
return cell.ErrorCellValue;
case CellType.Formula:
default:
return "=" + cell.CellFormula;
}
}
4.3、将解析好的数据组装为页面展示数据对象
搜索列表只展示 指标编码和指标名称就好,只要 类型为 0-基础指标,1-计算指标的数据即可;
// 搜索框数据
public static Dictionary<string, string> listData = new Dictionary<string, string>();
思维导图的数据结构很好想,不是树结构,就是链表结构,都是以某一个指标作为根结点,然后父节点列作为子树,或者下一结点。
原本打算做成思维导图展示某一结点的所有相关结点,即以c指标作为根节点,展示他所有的相关结点,即下图:
即将思维导图做成一个变形的树结构,c指标作为根节点,父节点作为左子树(C、C1、C2),子节点作为右子树(b);他的下一层b指标,左子树(a、d),没有右子树。
internal class Tree
{
public class Node
{
// 元素
public TreeInfo Item { get; set; }
// 左子树
public List<Node>? LeftNodes { get; set; }
// 右子树
public List<Node>? RightNodes { get; set; }
public Node(TreeInfo Item)
{
this.Item = Item;
}
public void AddNode(Node node)
{
……
}
}
// 根节点
public Node Root { get; set; }
}
实际做下来发现树对象勉强可以实现,但是JsMind渲染思维导图的话,还要改他的JS组件,这个实在做不来。所以简化结构。思维导图仅展示当前结点以及其父节点,不再向外扩展,即以c结点作为根节点,只展示它的父节点:
这样一来就是一个变形的链表结构,即:
internal class Tree
{
public class Node
{
// 元素
public TreeInfo Item { get; set; }
// 上一个结点
public List<Node>? LastNodes { get; set; }
public Node(TreeInfo Item)
{
this.Item = Item;
}
public void AddNode(Node node)
{
……
}
}
// 头节点
public Node Head { get; set; }
/// <summary>
/// 添加元素
/// </summary>
/// <param name="node">新增元素</param>
public void Add(TreeInfo element)
{
// 1、先创建节点
Node node = new Node(element);
// 2、判断是否根节点
if (this.Head == null)
{
this.Head = node;
}
else
{
// 从根节点开始存放
this.Head.AddNode(node);
}
}
}
4.4、JsMind渲染思维导图
NanUI其实相当于一个浏览器的框架,可以承载HTML或者VUE……,所以思维导图的实现,不需要C#代码去实现,即使用HTML + JsMind渲染就可以了。可以看下面的连接:
http://hizzgdev.github.io/jsmind/docs/zh/1.usage.html
#mindmap {
margin-top: 20px;
display: none;
width: 800px;
height: 400px;
border: solid 1px #ccc;
-webkit-app-region: no-drag;
}
<div id="mindmap"></div>
<script>
const mindmap = document.getElementById('mindmap');
let jm = null;
function showMindmap(labelCode) {
var mindmap = document.getElementById("mindmap");
mindmap.style.display = 'block';
var result = Formium.external.tree.GetTreeMap(labelCode);
const jsonData = JSON.parse(result);
var options = {
container: mindmap,
theme: 'primary',
editable: false
};
if (!jm) {
jm = new jsMind(options);
}
var mindData = {
"meta": {
"name": "gaozhiheng",
"author": "zhiheng222@126.com",
"version": "0.2"
},
"format": "node_tree",
"data": jsonData
}
jm.show(mindData);
}
document.addEventListener('click', (event) => {
if (event.target.classList.contains('tag')) {
// 在这里展示思维导图
showMindmap(event.target.textContent);
} else {
mindmap.style.display = 'none';
}
});
</script>
五、代码踩坑
5.1、NanUI 0.9.90怎么和JS相互调用
一开始还以为要写接口,前端页面发起http请求就可以了,后面发现NanUI和浏览器还不太一样,NanUI提供了一些代码,可以实现C#和JS相互调用的功能。可以参考以下链接:
https://blog.csdn.net/qq_38693757/article/details/113334277
NanUI文档 - 打包并使用内嵌式的HTML/CSS/JS资源
-
首先准备好C#接口代码,如以下代码准备了两个方法:GetTreeList、GetTreeMap
/// <summary> /// 根据 标签Code 或 标签Name 匹配标签列表 /// </summary> /// <param name="keyWord">标签Code 或 标签Name</param> /// <returns></returns> public static string GetTreeList(string keyWord) { …… } public static string GetTreeMap(string labelCode) { …… }
-
第二,将这两个方法注册到NanUI中,在ManWindow.cs的OnReady()实现方法中加入下述代码
protected override void OnReady() { …… // 注册方法到JS RegisterJavaScript(); } private void RegisterJavaScript() { // 创建 JS 对象 var obj = new JavaScriptObject(); obj.Add("GetTreeList", args => { var keyWord = args.FirstOrDefault(x => x.IsString); if (null == keyWord) { return null; } var result = WebController.GetTreeList(keyWord); return new JavaScriptValue(result); }); obj.Add("GetTreeMap", args => { var keyWord = args.FirstOrDefault(x => x.IsString); if (null == keyWord) { return null; } var result = WebController.GetTreeMap(keyWord); return new JavaScriptValue(result); }); // 把对象注册到 JS 环境 RegisterJavaScriptObject("tree", obj); }
-
第三,在html中调用对应方法
-
function showMindmap(labelCode) { …… var result = Formium.external.tree.GetTreeMap(labelCode); const jsonData = JSON.parse(result); …… }
5.2、NanUI 0.9.90怎么加载静态资源JS、CSS
HTML中引入了JsMind的JS和CSS,就是怎么也加载不了,使用绝对路径、相对路径都不好使,最后发现,NanUI需要将这些文件注册进去,比如我是将这些资源内嵌。
-
首先更改需要嵌入的文件属性
-
在Program中把资源路径映射假资源,相当于设定了一个域名,第三个参数就是文件夹目录
// 映射一个假资源 app.UseEmbeddedFileResource("http", "res.app.local", "Web");
-
html中调用即可
<script type="text/javascript" src="http://res.app.local/static/jsmind.js"></script> <link rel="stylesheet" type="text/css" href="http://res.app.local/static/jsmind.css" />
5.3、NanUI窗体无法移动
NanUI无法移动,只需要在对应的样式上面添加 -webkit-app-region: drag; 属性就好。
.draggable-area {
-webkit-app-region: drag;
}
<body class="draggable-area"></body>
加了以后,的确可以移动了,但是其他样式也无法点击了,例如最大化、关闭按钮等。只需要在对应样式中添加 -webkit-app-region: no-drag;属性就好了。
#mindmap {
margin-top: 20px;
display: none;
width: 800px;
height: 400px;
border: solid 1px #ccc;
-webkit-app-region: no-drag;
}
六、项目感想
学习代码还是要学以致用,边写代码边学习是最快的学习方式。能够使用简单的代码解决工作生活中的问题,还是有极大的成就感,等以后有时间了,再优化出来一个编辑功能,这样就不用更改Excel了。还有因为实在不会前端代码,这个小项目的HTML代码是再GPT4的协助下完成的,真是一次奇妙的体验。