C# + NanUI 实现思维导图小工具

项目仓库地址:https://github.com/MaverickGao/Mind_Map_Tool

一、背景

        公司的项目需要同步第三方数据源到本地数据库,在根据同步的数据进行一些指标的计算,随着指标越来越多,同事整理出来了一个文档,通过查阅文档可以快速定位指标的数据来源。但是这个文档数据量大,查看起来并不是那么容易,期望可以通过思维导图的形式将这些指标、数据来源关联起来,方便查阅。

以下是文档的样例:

 

        从上表可以看出,A列是表名或指标编码,可以通过D列类型来进行区分,0为基础指标,就是从表数据直接获取的指标;1为计算指标,就是通过表数据计算而来,或者通过不同的指标计算得来的。而C列则是将这些指标关联起来的,如果有多个父节点,则使用逗号“,”分隔。

二、结果演示

这里首先展示项目的最终成功,方便后面开发思路的讲解。

  1. 项目打包以后是一个桌面应用程序,可以通过打开exe文件来运行,通过替换Resource目录下的Excel文件来更新数据

  2. 项目启动以后,先是一个加载页面

  3. 加载页面关闭后进到我们的搜索页面,首先是一个搜索框,可以通过模糊搜索指标编码或者指标名称进行搜索

  4. 当在搜索框输入文字后,就会根据结果输出标签列表,注意列表中只展示标签,不展示表

  5. 当我们点击任一行指标编码后,就会在列表下方展示对应的思维导图,从这个指标一直指向它的数据来源

三、代码开发

3.1、需求分析

        公司项目同步数据源以及计算指标的逻辑大概如下:

        可以看出指标a的数据链路为:A表 → B表 → C表 → 指标a,同时指标a也可能同其他的表数据或指标计算出下一个指标,所以最终全部相关数据链路大致是下面的结构:

        而我们要做的就是以任一指标为起点,将其数据来源,即它的左边相关的结点全部展示为思维导图,方便查阅。

 

3.2、技术选型

最终选定了使用 C# + NanUI + NPOI + HTML + JsMind来实现这个小程序。

  1. 为什么使用C#

    之前一直使用Python脚本来做一些小程序,来简化工作,一来推荐给同事使用,他需要安装环境不太容易接受,再来我自己维护的话又要花费大量时间,所以想要做一个桌面应用,点击即用。刚巧这两天学了C#基础,学会的知识足够开发这款软件了。

  2. 为什么使用NanUI

    主要是不会前端代码,网上找了个现成的UI框架进行开发,这个NanUI还是很惊喜的,开发简单,功能齐全,而且还很好看。

  3. NPOI

    用来解析Excel。

  4. 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资源

  1. 首先准备好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)
    {
        ……
    }
  2. 第二,将这两个方法注册到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);
    }
  3. 第三,在html中调用对应方法

  4. 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需要将这些文件注册进去,比如我是将这些资源内嵌。

  1. 首先更改需要嵌入的文件属性

  2. 在Program中把资源路径映射假资源,相当于设定了一个域名,第三个参数就是文件夹目录

    // 映射一个假资源
    app.UseEmbeddedFileResource("http", "res.app.local", "Web");
  3. 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的协助下完成的,真是一次奇妙的体验。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值