gdip 绘制主从关系节点图

mndsoft 2024-8-30 2438

最近在研究使用  aardio如何实现一个以前用VB做的节点从属关系的图,实现如下图功能。

gdi GDI绘图库需要慢慢研究,弄了简单示例,还不支持拖动,信息提示。而且还闪烁,让AI也没有解决,看来绘图有点难度哈。希望高手建言,学习。

也在研究 光庆大师的 画板 paint库,这个感觉比较全面,实现这个功能应该没问题,慢慢研究学习。

还有目前还不熟悉 aardio 使用 ActiveX 这些ocx库,接下来也研究下,如果可以,其实使用这个 Xtreme SuitePro ActiveX  的界面库中的 FlowGraph 组件也不错。下面这个图就是这个OCX的示例,这个组件的官网地址:https://codejock.com/products/suitepro/?2yn6s14z=p1z ,感兴趣的可以看看。

这个是Xtreme SuitePro ActiveX  的界面库中的 FlowGraph 组件的示例:


这个是 aardio弄得简单节点图:

import win.ui;
import gdip;

/*DSG{{*/
var winform = win.form(text="节点关系图";right=759;bottom=469)
winform.add(
btnAddMain={cls="button";text="添加主节点";left=588;top=16;right=716;bottom=55;z=1};
btnAddSub={cls="button";text="添加从属节点";left=588;top=88;right=716;bottom=127;z=2}
)
/*}}*/

var nodes = {};
var mainNode = null;
var draggingNode = null;

class Node {
    ctor(x, y, text, isMain = false) {
        this.x = x;
        this.y = y;
        this.text = text;
        this.children = {};
        this.isMain = isMain;
    }
}

function drawNodes(graphics) {
    var backgroundBrush = gdip.solidBrush(0xFFFFFFFF);
    graphics.fillRectangle(backgroundBrush, 0, 0, winform.width, winform.height);
    
    for(i=1; #nodes) {
        var node = nodes[i];
        var brush = node.isMain ? gdip.solidBrush(0xFFFF0000) : gdip.solidBrush(0xFF0000FF);
        graphics.fillEllipse(brush, node.x-25, node.y-25, 50, 50);
        
        var family = gdip.family("宋体");
        var font = family.createFont(15, 0/*_GdipFontStyleRegular*/, 2/*_GdipUnitPixel*/);
        var format = gdip.stringformat();
        format.align = 1;
        format.lineAlign = 1;
        graphics.drawString(node.text, font, gdip.RECTF(node.x-25, node.y-25, 50, 50), format, gdip.solidBrush(0xFFFFFFFF));
        
        if(node.isMain) {
            for(j=1; #node.children) {
                var child = node.children[j];
                var pen = gdip.pen(0xFF000000, 2);
                graphics.drawCurve(pen,
                    node.x, node.y,
                    (node.x + child.x)/2, node.y,
                    (node.x + child.x)/2, child.y,
                    child.x, child.y
                );
            }
        }
    }
}

winform.btnAddMain.oncommand = function(id,event){
    if(mainNode === null) {
        mainNode = Node(300, 200, "主节点", true);
        table.push(nodes, mainNode);
        
        var graphics = gdip.graphics(winform);
        graphics.smoothingMode = 4/*_GdipSmoothingModeAntiAlias*/;
        drawNodes(graphics);
        
       // win.msgbox("已添加主节点,节点总数:" ++ tostring(#nodes));
    } else {
        win.msgbox("已存在主节点!");
    }
}

winform.btnAddSub.oncommand = function(id,event){
    if(mainNode === null) {
        win.msgbox("请先添加主节点!");
        return;
    }
    var x = math.random(100, 500);
    var y = math.random(100, 300);
    var newNode = Node(x, y, "节点" ++ tostring(#nodes + 1));
    table.push(nodes, newNode);
    table.push(mainNode.children, newNode);

    var graphics = gdip.graphics(winform);
    graphics.smoothingMode = 4/*_GdipSmoothingModeAntiAlias*/;
    drawNodes(graphics);
    
   // win.msgbox("已添加从属节点,节点总数:" ++ tostring(#nodes));
}

winform.wndproc = function(hwnd,message,wParam,lParam){
    select(message) {
        case 0x201/*_WM_LBUTTONDOWN*/ {
            var x, y = win.getMessagePos(lParam);
            for(i=1; #nodes) {
                if((x-nodes[i].x)^2 + (y-nodes[i].y)^2 <= 25^2) {
                    draggingNode = nodes[i];
                    break;
                }
            }
        }
        case 0x202/*_WM_LBUTTONUP*/ {
            draggingNode = null;
        }
        case 0x200/*_WM_MOUSEMOVE*/ {
            if(draggingNode) {
                var x, y = win.getMessagePos(lParam);
                draggingNode.x = x;
                draggingNode.y = y;
                var graphics = gdip.graphics(winform);
                graphics.smoothingMode = 4/*_GdipSmoothingModeAntiAlias*/;
                drawNodes(graphics);
            }
        }
    }
}

winform.onPaint = function(hdc) {
    var graphics = gdip.graphics(winform);
    graphics.smoothingMode = 4/*_GdipSmoothingModeAntiAlias*/;
    drawNodes(graphics);
}

winform.show();
win.loopMessage();


最新回复 (20)
  • 光庆 2024-8-30
    0 2

    画曲线的函数使用不对,所以画出来的线不美观。

    这样改一下试试:

                    graphics.drawBezier(pen,
                        node.x, node.y, 
                        (child.x+node.x)/2, node.y,
                        (child.x+node.x)/2,child.y,
                        child.x, child.y
                    );

  • 光庆 2024-8-30
    0 3

    稍微改了下代码,优化了一下拖动效果:

    import win.ui;
    import gdip;
    /*DSG{{*/
    var winform = win.form(text="节点关系图";right=759;bottom=469)
    winform.add(
    btnAddMain={cls="button";text="添加主节点";left=588;top=16;right=716;bottom=55;z=1};
    btnAddSub={cls="button";text="添加从属节点";left=588;top=88;right=716;bottom=127;z=2}
    )
    /*}}*/
    
    var nodes = {};
    var mainNode = null;
    var draggingNode = null;
    var graphics = gdip.graphics(winform);
    graphics.smoothingMode = 4/*_GdipSmoothingModeAntiAlias*/;
    
    class Node {
        ctor(x, y, text, isMain = false) {
            this.x = x;
            this.y = y;
            this.text = text;
            this.children = {};
            this.isMain = isMain;
            this.rect = ::RECT(x-25,y-25,x+25,y+25);
            this.rectf = ::RECTF(x-25,y-25,50,50);
        }
    }
    
    function drawNodes() {
        var backgroundBrush = gdip.solidBrush(0xFFFFFFFF);
        graphics.fillRectangle(backgroundBrush, 0, 0, winform.width, winform.height);
        
        for(i=1; #nodes) {
            var node = nodes[i];
            var brush = node.isMain ? gdip.solidBrush(0xFFFF0000) : gdip.solidBrush(0xFF0000FF);
            graphics.fillEllipse(brush, node.rect.xywh());
            var family = gdip.family("宋体");
            var font = family.createFont(15, 0/*_GdipFontStyleRegular*/, 2/*_GdipUnitPixel*/);
            var format = gdip.stringformat();
            format.align = 1;
            format.lineAlign = 1;
            graphics.drawString(node.text, font, node.rectf, format, gdip.solidBrush(0xFFFFFFFF));
            if(node.isMain) {
                for(j=1; #node.children) {
                    var child = node.children[j];
                    var pen = gdip.pen(0xFF000000, 2);
                    graphics.drawBezier(pen,
                        node.x+25, node.y, 
                        (child.x+node.x)/2, node.y,
                        (child.x+node.x)/2,child.y,
                        child.x-25, child.y
                    );
                }
            }
        }
    }
    
    winform.btnAddMain.oncommand = function(id,event){
        if(mainNode === null) {
            mainNode = Node(300, 200, "主节点", true);
            table.push(nodes, mainNode);
            drawNodes();
           // win.msgbox("已添加主节点,节点总数:" ++ tostring(#nodes));
        } else {
            win.msgbox("已存在主节点!");
        }
    }
    
    winform.btnAddSub.oncommand = function(id,event){
        if(mainNode === null) {
            win.msgbox("请先添加主节点!");
            return;
        }
        var x = math.random(100, 500);
        var y = math.random(100, 300);
        var newNode = Node(x, y, "节点" ++ tostring(#nodes + 1));
        table.push(nodes, newNode);
        table.push(mainNode.children, newNode);
        drawNodes(graphics);
       // win.msgbox("已添加从属节点,节点总数:" ++ tostring(#nodes));
    }
    
    winform.wndproc = function(hwnd,message,wParam,lParam){
        select(message) {
            case 0x201/*_WM_LBUTTONDOWN*/ {
                draggingNode = null;
                offsetx,offsety=0,0;
                var x, y = win.getMessagePos(lParam);
                for(i=1; #nodes) {
                    if ::PtInRect(nodes[i].rect,x,y){
                        draggingNode = nodes[i];
    		            offsetx,offsety=draggingNode.x-x,draggingNode.y-y;
                        break;
                    }
                }
            }
            case 0x202/*_WM_LBUTTONUP*/ {
                draggingNode = null;
            }
            case 0x200/*_WM_MOUSEMOVE*/ {
                if(draggingNode) {
                    var x, y = win.getMessagePos(lParam);
                    x+=offsetx;
                    y+=offsety;
                    draggingNode.x = x;
                    draggingNode.y = y;
             		draggingNode.rect = ::RECT(x-25,y-25,x+25,y+25);
            		draggingNode.rectf = ::RECTF(x-25,y-25,50,50);
                    drawNodes();
                }
            }
        }
    }
    
    winform.onPaint = function(hdc) {
        drawNodes();
    }
    
    winform.show();
    win.loopMessage();


  • 光庆 2024-8-30
    0 4

  • mndsoft 2024-8-30
    0 5
    帅! 学习! 大师太牛了,正在研究大师的那个 画板库,大师对绘图也有很深的造诣!
  • mndsoft 2024-8-30
    0 6

    大师,我想弄个鼠标悬停到节点是出现 信息提示,但是坐标不知道如何获取,信息显示到窗体外面了,请帮忙看看哈

    import win.ui;
    import gdip;
    import win.ui.tooltip;
    import console
    /*DSG{{*/
    var winform = win.form(text="节点关系图";right=759;bottom=469)
    winform.add(
    btnAddMain={cls="button";text="添加主节点";left=588;top=16;right=716;bottom=55;z=1};
    btnAddSub={cls="button";text="添加从属节点";left=588;top=88;right=716;bottom=127;z=2}
    )
    /*}}*/
    // 创建 tooltip 控件
    var tooltip = win.ui.tooltip(winform);
    
    // 创建跟踪型气泡提示控件
    var balloonTipCtrl = win.ui.tooltip.tracking(winform,false);
    
    var nodes = {};
    var mainNode = null;
    var draggingNode = null;
    var graphics = gdip.graphics(winform);
    graphics.smoothingMode = 4/*_GdipSmoothingModeAntiAlias*/;
    
    class Node {
        ctor(x, y, text, isMain = false) {
            this.x = x;
            this.y = y;
            this.text = text;
            this.children = {};
            this.isMain = isMain;
            this.rect = ::RECT(x-25,y-25,x+25,y+25);
            this.rectf = ::RECTF(x-25,y-25,50,50);
        }
    }
    
    function drawNodes() {
        var backgroundBrush = gdip.solidBrush(0xFFFFFFFF);
        graphics.fillRectangle(backgroundBrush, 0, 0, winform.width, winform.height);
        
        for(i=1; #nodes) {
            var node = nodes[i];
            var brush = node.isMain ? gdip.solidBrush(0xFFFF0000) : gdip.solidBrush(0xFF0000FF);
            graphics.fillEllipse(brush, node.rect.xywh());
            var family = gdip.family("宋体");
            var font = family.createFont(15, 0/*_GdipFontStyleRegular*/, 2/*_GdipUnitPixel*/);
            var format = gdip.stringformat();
            format.align = 1;
            format.lineAlign = 1;
            graphics.drawString(node.text, font, node.rectf, format, gdip.solidBrush(0xFFFFFFFF));
            if(node.isMain) {
                for(j=1; #node.children) {
                    var child = node.children[j];
                    var pen = gdip.pen(0xFF000000, 2);
                    graphics.drawBezier(pen,
                        node.x+25, node.y, 
                        (child.x+node.x)/2, node.y,
                        (child.x+node.x)/2,child.y,
                        child.x-25, child.y
                    );
                }
            }
        }
    }
    
    winform.btnAddMain.oncommand = function(id,event){
        if(mainNode === null) {
            mainNode = Node(300, 200, "主节点", true);
            table.push(nodes, mainNode);
            drawNodes();
           // win.msgbox("已添加主节点,节点总数:" ++ tostring(#nodes));
        } else {
            win.msgbox("已存在主节点!");
        }
    }
    
    winform.btnAddSub.oncommand = function(id,event){
        if(mainNode === null) {
            win.msgbox("请先添加主节点!");
            return;
        }
        var x = math.random(100, 500);
        var y = math.random(100, 300);
        var newNode = Node(x, y, "节点" ++ tostring(#nodes + 1));
        table.push(nodes, newNode);
        table.push(mainNode.children, newNode);
        drawNodes(graphics);
       // win.msgbox("已添加从属节点,节点总数:" ++ tostring(#nodes));
    }
    
    winform.wndproc = function(hwnd,message,wParam,lParam){
        select(message) {
            case 0x201/*_WM_LBUTTONDOWN*/ {
                draggingNode = null;
                offsetx,offsety=0,0;
                var x, y = win.getMessagePos(lParam);
                for(i=1; #nodes) {
                    if ::PtInRect(nodes[i].rect,x,y){
                        draggingNode = nodes[i];
                        offsetx,offsety=draggingNode.x-x,draggingNode.y-y;
                        break;
                    }
                }
            }
            case 0x202/*_WM_LBUTTONUP*/ {
                draggingNode = null;
            }
            case 0x200/*_WM_MOUSEMOVE*/ {   //鼠标悬停信息显示 
                if(draggingNode) {
                    var x, y = win.getMessagePos(lParam);
                    x+=offsetx;
                    y+=offsety;
                    draggingNode.x = x;
                    draggingNode.y = y;
                     draggingNode.rect = ::RECT(x-25,y-25,x+25,y+25);
                    draggingNode.rectf = ::RECTF(x-25,y-25,50,50);
                    drawNodes();
                } else {
                
                    var x, y = win.getMessagePos(lParam);
                    var hoveredNode = null;
                    for(i=1; #nodes) {
                        if ::PtInRect(nodes[i].rect,x,y){
                            hoveredNode = nodes[i];
                            break;
                        }
                    }
                    
                    if(hoveredNode) {
                        var info = hoveredNode.isMain ? "主节点" : "从属节点";
                        tooltip.setText(hoveredNode.text + "\n类型: " + info);
                        
                        // 使用节点矩形的右下角作为 tooltip 的位置
                        var screenX, screenY = win.toScreen(winform.hwnd, hoveredNode.rect.right, hoveredNode.rect.bottom);
                        
                        // 显示 tooltip,稍微偏移一点以避免遮挡节点
                        tooltip.trackPopup(screenX + 5, screenY + 5);
                    }
                    else {
                        tooltip.trackPopup(); // 隐藏 tooltip
                    }
                }
            }
        }
    }
    
    
    winform.onPaint = function(hdc) {
        drawNodes();
    }
    
    winform.show();
    win.loopMessage();


  • Mr_MAO 14天前
    0 7

    这个帖子得顶一下!很实用! 光神和mndsoft两位大神的代码很棒!!👍👍👍

    楼主这种需求的应用场景很多,其实代码完善完善,还可以用来做思维导图软件的...



    import win.ui;
    /*DSG{{*/
    var winform = win.form(text="aardio 制作思维导图示例 (* 按tab键添加子节点 | 按del键删除子节点 | 鼠标左键/中键移动节点 *)";right=839;bottom=495)
    winform.add(
    /* plus控件开双缓冲了,绘图不闪烁 */
    canvas={cls="plus";left=0;top=0;right=839;bottom=495;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
    )
    /*}}*/
    
    // 定义子节点颜色列表
    var childColorValues = {
        0xFF4CAF50; // 绿色
        0xFF2196F3; // 蓝色
        0xFFFF9800; // 橙色
        0xFF9C27B0; // 紫色
        0xFF00BCD4; // 青色
        0xFF795548; // 棕色
    }
    
    var childBrushes = table.map(childColorValues, function(v){
        return gdip.solidBrush(v); // 将颜色数值转为固态画刷
    })
    
    var mainBrush   = gdip.solidBrush(0xFFFF4444); // 主节点红色
    var textBrush   = gdip.solidBrush(0xFFFFFFFF); // 文字白色
    var linePen     = gdip.pen(0xAAB2A1FF, 2);     // 连线淡紫色
    var selectPen   = gdip.pen(0xFFFFFF00, 3);     // 选中高亮黄色
    
    var family = gdip.family("Microsoft YaHei");
    var font = family.createFont(14, 0, 2);
    var fontRoot = family.createFont(19, 0, 2);
    var format = gdip.stringformat();
    format.align = 1;
    format.lineAlign = 1;
    
    var nodes = {};         // 存储所有节点
    var mainNode = null;    // 根节点引用
    var draggingNode = null;// 当前正在拖拽的节点
    var selectedNode = null;// 当前选中的节点(新子节点的父级)
    var hoveredIndex = null;
    var hoverTimerId = null;// 控制tooltip显示的定时器
    var offsetX, offsetY = 0, 0;
    var middleDraggingNode = null; // 中键拖动中的根节点
    var middleSubTreeRel = {};     // 子树中每个节点相对于根节点的偏移 [{node, relX, relY}, ...]
    var middleStartX, middleStartY = 0, 0;
    var middleBaseX, middleBaseY = 0, 0;
    
    import win.ui.tooltip;    // 创建气泡提示
    var tooltip = win.ui.tooltip(winform);
    
    // 节点类定义
    class Node {
        ctor(x, y, text, isMain = false, level = 0) {
            this.x = x;
            this.y = y;
            this.text = text;
            this.children = {}; // 存储子节点对象的数组
            this.isMain = isMain;
            this.level = level; // 0是根,1是子,2是孙...
        }
        
        getRadius = function(){
            var r = this.isMain ? 50 : (40 - (this.level * 3));
            return r < 25 ? 25 : r;    // 限制最小半径
        };
        
        getRect = function(){
            var r = this.getRadius();
            return ::RECT(this.x - r, this.y - r, this.x + r, this.y + r);
        }
    }
    
    winform.canvas.onDrawBackground = function(graphics,rc,backgroundColor,color){
        graphics.clear(0xFFFFFFFF);
        //画网格
        var pen = gdip.pen(0x55B1FDAD, 2); 
        for(i=30,rc.height,30){
            graphics.drawLine(pen,0,i,rc.right,i);
        }
        for(i=30,rc.width,30){
            graphics.drawLine(pen,i,0,i,rc.bottom);
        }
    }
    
    winform.canvas.onDrawForeground = function(graphics, rc, foregroundRect) {
        // 1. 绘制所有连线
        for(i=1; #nodes) {
            var node = nodes[i];
            for(j=1; #node.children) {
                var child = node.children[j];
                // 使用贝塞尔曲线连接父子节点中心
                graphics.drawBezier(linePen,
                    node.x, node.y, 
                    (child.x + node.x) / 2, node.y,
                    (child.x + node.x) / 2, child.y,
                    child.x, child.y
                );
            }
        }
    
        // 2. 绘制所有节点
        for(i=1; #nodes) {
            var node = nodes[i];
            var rect = node.getRect();
    
            //A 确定背景画刷
            var brush;
            if(node.isMain) {
                brush = mainBrush;
            } else {
                var colorIdx = ((node.level - 1) % #childBrushes) + 1;// 根据层级循环取色
                brush = childBrushes[colorIdx];
            }
    
            //B 绘制选中高亮边框
            if(node == selectedNode){
                graphics.drawEllipse(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            }
    
            //C 绘制节点圆体
            graphics.fillEllipse(brush, rect.left, rect.top, rect.width(), rect.height());
    
            //D 绘制文字
            graphics.drawString(node.text,
                node.isMain ? fontRoot : font,
                rect.float(), format, textBrush
            );
        }
    }
    
    // 添加主节点
    addMain = function(){
        if(!mainNode) {
            mainNode = Node(winform.canvas.clientWidth/4, winform.canvas.clientHeight/2, "根节点", true, 0);
            table.push(nodes, mainNode);
            selectedNode = mainNode;
            
            winform.canvas.setFocus();
            winform.canvas.redraw();
        } else {
            winform.msgbox("主节点已存在!","提示");
        }
    }
    
    // 添加子节点
    addSub = function(){
        var parent = selectedNode : mainNode;
        if(!parent) return winform.msgbox("请先添加主节点!","提示");
    
        var newLevel = parent.level + 1;
        var newNode = Node(
            parent.x + math.random(90, 150), 
            parent.y + math.random(-90, 90), 
            "节点" ++ (#nodes + 1),
            false,
            newLevel
        );
    
        table.push(nodes, newNode);
        table.push(parent.children, newNode);
    
        selectedNode = newNode;
        winform.canvas.redraw();
    }
    
    // 删除节点及其所有子孙
    deleteNode = function(node) {
        if(!node || node.isMain) return; // 根节点不可删除
    
        // 1. 递归获取所有要删除的节点(包含自身及其所有后代)
        var toDelete = {};
        var collectDescendants;
        collectDescendants = function(node) {
            table.push(toDelete, node);
            for(i=1;#node.children){
                collectDescendants(node.children[i]);
            }
        }
        collectDescendants(node);
    
        // 2. 从父节点的 children 列表中移除自己
        for(i=1;#nodes){
            var p = nodes[i];
            var idx = table.find(p.children, node);
            if(idx){
                table.remove(p.children, idx);
                break; 
            }
        }
    
        // 3. 从全局 nodes 数组中移除所有相关节点
        for(i=1;#toDelete){
            var idx = table.find(nodes, toDelete[i]);
            if(idx) table.remove(nodes, idx);
        }
    
        // 重置选中状态并重绘
        selectedNode = null;
        winform.canvas.redraw();
    }
    
    // 重命名节点
    renameNode = function(node){
        if(!node) return;
        var popform = win.form(text="修改节点文本"; right=320; bottom=120; border="dialog");
        popform.add(
            edit={cls="edit"; left=15; top=15; right=305; bottom=42; text=node.text; edge=1};
            btnOk={cls="button"; left=160; top=70; right=240; bottom=100; text="确定"};
            btnCancel={cls="button"; left=245; top=70; right=305; bottom=100; text="取消"}
        );
        popform.btnOk.oncommand = function(){
            var t = popform.edit.text;
            if(#t){ node.text = t; winform.canvas.redraw(); }
            popform.close();
        };
        popform.btnCancel.oncommand = function(){ popform.close(); };
        popform.doModal(winform.hwnd);
    }
    
    // 双击节点:修改节点文字
    winform.canvas.onMouseDoubleClick = function(wParam,lParam){
        var x,y = win.getMessagePos(lParam);
        for(i=#nodes; 1; -1){
            if(::PtInRect(nodes[i].getRect(), x, y)){
                renameNode(nodes[i]);
                break;
            }
        }
    }
    
    // 鼠标按下:选择与准备拖拽
    winform.canvas.onMouseDown = function(wParam, lParam) {
        var x, y = win.getMessagePos(lParam);
        draggingNode = null; 
    
        var lastSelected = selectedNode;
        selectedNode = null; 
    
        for(i=#nodes; 1; -1) {
            if(::PtInRect(nodes[i].getRect(), x, y)) {
                selectedNode = nodes[i];
                draggingNode = nodes[i];
                offsetX, offsetY = draggingNode.x - x, draggingNode.y - y;
                winform.canvas.capture = true; 
                break;
            }
        }
        if(lastSelected != selectedNode) winform.canvas.redraw();
    }
    
    // 鼠标移动:执行拖拽或更新气泡提示
    winform.canvas.onMouseMove = function(wParam, lParam) {
        var x, y = win.getMessagePos(lParam);
    
        // 中键拖拽:移动节点及其所有子节点
        if(middleDraggingNode) {
            var x, y = win.getMessagePos(lParam);
        
            // 计算移动偏移量
            var dx = x - middleStartX;
            var dy = y - middleStartY;
        
            // 新根节点坐标
            var newRootX = middleBaseX + dx;
            var newRootY = middleBaseY + dy;
        
            // 边界限制(仅限制根节点,子节点自动跟随)
            var cw = winform.canvas.clientWidth;
            var ch = winform.canvas.clientHeight;
            var r = middleDraggingNode.getRadius();
            if(newRootX < r) newRootX = r;
            if(newRootX > cw - r) newRootX = cw - r;
            if(newRootY < r) newRootY = r;
            if(newRootY > ch - r) newRootY = ch - r;
        
            // 更新根节点坐标
            middleDraggingNode.x = newRootX;
            middleDraggingNode.y = newRootY;
        
            // 更新所有子节点坐标(保持相对位置)
            for(i=1; #middleSubTreeRel) {
                var info = middleSubTreeRel[i];
                info.node.x = newRootX + info.relX;
                info.node.y = newRootY + info.relY;
            }
        
            winform.canvas.redraw();
            return; // 拖拽时跳过悬停提示处理
        }
    
        elseif(draggingNode) {
            // 防止节点移出画布
            var newX = x + offsetX;
            var newY = y + offsetY;
            var cw = winform.canvas.clientWidth;
            var ch = winform.canvas.clientHeight;
            
            var r = draggingNode.getRadius();
    
            var rx = draggingNode.x + draggingNode.getRect().width/2;
            var ry = draggingNode.y + draggingNode.getRect().height/2;
            
            if(newX < r) newX = r;
            if(newX > cw - r) newX = cw - r;
            if(newY < r) newY = r;
            if(newY > ch - r) newY = ch - r;
            
            draggingNode.x = newX; 
            draggingNode.y = newY;
            winform.canvas.redraw();
        } else {
            // 检测悬停节点
            var lastHover = hoveredIndex;
            hoveredIndex = null;
            for(i=#nodes; 1; -1) {
                if(::PtInRect(nodes[i].getRect(), x, y)) {
                    hoveredIndex = i;
                    break;
                }
            }
            
    		// 处理悬停变化
        	if(hoveredIndex && lastHover != hoveredIndex) {
            	// 取消之前的定时器
            	if(hoverTimerId) {
                	winform.clearTimeout(hoverTimerId);
                	hoverTimerId = null;
            	}
            
            	// 设置2秒后显示 Tooltip
            	var node = nodes[hoveredIndex];
            	hoverTimerId = winform.setTimeout(
                	function(nd) {
                    	// 只有当鼠标仍在当前节点上时才显示
                    	if(hoveredIndex && nodes[hoveredIndex] == nd) {
                        	tooltip.setText(nd.text + '\n层级: ' + nd.level);
                        	var rect = nd.getRect();
                        	var tx = rect.right + 10;
                        	var ty = rect.top + rect.height / 4;
                        	var sx, sy = win.toScreen(winform.canvas.hwnd, tx, ty);
                        	tooltip.trackPopup(true, sx, sy);//显示提示
                        	
                        	winform.setTimeout(function(){
                            	tooltip.trackPopup(false);    // 1秒后自动隐藏
                        	}, 1000);
                    	}
                	}, 2000,  node   //回调函数参数
            	);
            } elseif(!hoveredIndex) {
                if(hoverTimerId) {
                    winform.clearTimeout(hoverTimerId);
                    hoverTimerId = null;
                }
                tooltip.trackPopup(false);
            }
        }
    }
    
    winform.canvas.onMouseUp = function(wParam, lParam) {
        draggingNode = null;
        winform.canvas.capture = false;
    }
    
    // 递归收集子树节点(包括根节点自己),用于整体移动
    var collectSubTree = null;
    collectSubTree = function(node, nodesList, baseX, baseY) {
        table.push(nodesList, {
            node = node,
            relX = node.x - baseX,
            relY = node.y - baseY
        });
        for(j=1; #node.children) {
            collectSubTree(node.children[j], nodesList, baseX, baseY);
        }
    };
    
    // 按鼠标中键,移动选中节点及其所有子孙节点
    winform.canvas.onMiddleMouseDown = function(wParam, lParam) {
        var x, y = win.getMessagePos(lParam);
    
        // 寻找鼠标下的节点
        middleDraggingNode = null;
        for(i=#nodes; 1; -1) {
            if(::PtInRect(nodes[i].getRect(), x, y)) {
                middleDraggingNode = nodes[i];
                // 将该节点设为选中(直观反馈)
                selectedNode = middleDraggingNode;
                break;
            }
        }
    
        if(!middleDraggingNode) return;
    
        // 记录起点状态
        middleStartX = x;
        middleStartY = y;
        middleBaseX = middleDraggingNode.x;
        middleBaseY = middleDraggingNode.y;
    
        // 收集整棵子树中所有节点的相对偏移
        middleSubTreeRel = {};
        collectSubTree(middleDraggingNode, middleSubTreeRel, middleBaseX, middleBaseY);
    
        // 清除左键拖拽状态,防止冲突
        draggingNode = null;
        winform.canvas.capture = true;
        winform.canvas.redraw();
    }
    
    winform.canvas.onMiddleMouseUp = function(wParam, lParam) {
        middleDraggingNode = null;
        middleSubTreeRel = {};
        winform.canvas.capture = false;
    }
    
    // 快捷键
    winform.canvas.onKeyUp = function(keyCode,lParam){
        if(keyCode == 0x9/*_VK_TAB*/){          //按 Tab 键添加子节点
          if(mainNode) addSub();
        }elseif(keyCode == 0x2E/*_VK_DELETE*/){ //按 DEL 键删除子节点
            if(selectedNode && !selectedNode.isMain){
                deleteNode(selectedNode);
            }
        }
    }
    
    winform.show();
    addMain();    // 先添加根节点
    
    win.loopMessage();


    后记:看了下面几个帖子,兄弟们热情度很高,AI让我们更简洁的实现自己的想法!我抽空完善了一下代码,增加了一些基础的功能。大家也可以把以上代码喂给AI作为底座,继续完善这个《思维导图》程序💪💪💪


  • 我高兴 14天前
    0 8
    都是神人
  • 近我者赤 13天前
    0 9
    缺个删除节点的功能啊
  • mfk 8天前
    1 10

    AI 改写的

    import win.ui;
    import gdip; 
    import web.json;     
    import fsys.dlg;    
    /*DSG{{*/
    var mainForm = win.form(text="aardio 思维导图 (* 右键:弹出菜单 | 双击:改名 | Tab:加子节点 | Delete:删除 | Ctrl+S:保存 Ctrl+O:打开 *)";right=839;bottom=495)
    mainForm.add(
    canvas={cls="plus";left=0;top=0;right=839;bottom=495;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
    )
    /*}}*/
    
    // 创建气泡提示
    import win.ui.tooltip;
    var tooltip = win.ui.tooltip(mainForm);
    tooltip.activate(true);
    
    // 定义子节点颜色列表
    var childColorValues = {
        0xFF4CAF50; // 绿色
        0xFF2196F3; // 蓝色
        0xFFFF9800; // 橙色
        0xFF9C27B0; // 紫色
        0xFF00BCD4; // 青色
        0xFF795548; // 棕色
    }
    
    var childBrushes = table.map(childColorValues, function(v){
        return gdip.solidBrush(v); // 将颜色数值转为固态画刷
    })
    
    var mainBrush   = gdip.solidBrush(0xFFFF4444); // 主节点红色
    var textBrush   = gdip.solidBrush(0xFFFFFFFF); // 文字白色
    var linePen     = gdip.pen(0xAAB2A1FF, 2);     // 连线淡紫色
    var selectPen   = gdip.pen(0xFFFFFF00, 3);     // 选中高亮黄色
    
    var family = gdip.family("Microsoft YaHei");
    var font = family.createFont(14, 0, 2);
    var fontRoot = family.createFont(19, 0, 2);
    var format = gdip.stringformat();
    format.align = 1;
    format.lineAlign = 1;
    
    var nodes = {};         
    var mainNode = null;    
    var draggingNode = null;
    var selectedNode = null;
    var hoveredIndex = null;
    var offsetX, offsetY = 0, 0;
    var mouseMoved = false;      
    var lastClickTime = 0;       
    var lastClickNode = null;    
    
    // 节点类定义
    class Node {
        ctor(x, y, text, isMain = false, level = 0) {
            this.x = x;
            this.y = y;
            this.text = text;
            this.children = {}; 
            this.isMain = isMain;
            this.level = level; 
        }
    
        getRect = function(){
            var r = this.isMain ? 50 : (40 - (this.level * 3));
            if(r < 25) r = 25; 
            return ::RECT(this.x - r, this.y - r, this.x + r, this.y + r);
        }
    }
    
    drawContent = function(g, w, h){
        // 抗锯齿
        g.smoothingMode = 4;       
        g.textRenderingHint = 4;   
        // 网格
        var pen = gdip.pen(0x55B1FDAD, 2); 
        for(i=30; h; 30){
            g.drawLine(pen, 0, i, w, i);
        }
        for(i=30; w; 30){
            g.drawLine(pen, i, 0, i, h);
        }
        // 连线
        for(i=1; #nodes) {
            var node = nodes[i];
            for(j=1; #node.children) {
                var child = node.children[j];
                g.drawBezier(linePen,
                    node.x, node.y, 
                    (child.x + node.x) / 2, node.y,
                    (child.x + node.x) / 2, child.y,
                    child.x, child.y
                );
            }
        }
        // 节点
        for(i=1; #nodes) {
            var node = nodes[i];
            var rect = node.getRect();
            var brush;
            if(node.isMain) {
                brush = mainBrush;
            } else {
                var colorIdx = ((node.level - 1) % #childBrushes) + 1;
                brush = childBrushes[colorIdx];
            }
            if(node == selectedNode){
                g.drawEllipse(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            }
            g.fillEllipse(brush, rect.left, rect.top, rect.width(), rect.height());
            g.drawString(node.text,
                node.isMain ? fontRoot : font,
                rect.float(), format, textBrush
            );
        }
    }
    
    mainForm.canvas.onDrawBackground = function(graphics, rc, backgroundColor, color){
        graphics.clear(0xFFFFFFFF);
    }
    
    mainForm.canvas.onDrawForeground = function(graphics, rc, foregroundRect) {
        drawContent(graphics, rc.width, rc.height);
    }
    
    // 添加主节点
    addMain = function(){
        if(!mainNode) {
            mainNode = Node(mainForm.canvas.clientWidth/4, mainForm.canvas.clientHeight/2, "根节点", true, 0);
            table.push(nodes, mainNode);
            selectedNode = mainNode;
            
            mainForm.canvas.setFocus();
            mainForm.canvas.redraw();
        } else {
            mainForm.msgbox("主节点已存在!","提示");
        }
    }
    
    // 添加子节点
    addSub = function(){
        var parent = selectedNode or mainNode; 
        if(!parent) return mainForm.msgbox("请先添加主节点!","提示");
    
        var newLevel = parent.level + 1;
        var newNode = Node(
            parent.x + math.random(90, 150), 
            parent.y + math.random(-90, 90), 
            "节点" ++ (#nodes + 1),
            false,
            newLevel
        );
    
        table.push(nodes, newNode);
        table.push(parent.children, newNode);
    
        selectedNode = newNode;
        mainForm.canvas.redraw();
    }
    
    var popmenu = win.ui.popmenu(mainForm);
    popmenu.add("保存\tCtrl+S", function(){ saveToFile(); });
    popmenu.add("打开\tCtrl+O", function(){ loadFromFile(); });
    popmenu.add("添加子节点\tTab", function(){ if(mainNode) addSub(); });
    popmenu.add("重命名", function(){ if(selectedNode) renameNode(selectedNode); });
    popmenu.add("删除节点", function(){ if(selectedNode) deleteNode(selectedNode); });
    popmenu.add("导出 PNG", function(){ exportPNG(); });
    popmenu.add("退出", function(){ mainForm.close() });
    
    
    mainForm.canvas.wndproc = function(hwnd, msg, wParam, lParam){
        if(msg == 0x205/*_WM_RBUTTONUP*/){
    		var x, y = win.getCursorPos()  
    		popmenu.popup(x, y, true);
            return true; 
        }
        if(msg == 0x203/*_WM_LBUTTONDBLCLK*/){
            var x = lParam & 0xFFFF;
            var y = (lParam >> 16) & 0xFFFF;
            for(i=#nodes; 1; -1){
                if(::PtInRect(nodes[i].getRect(), x, y)){
                    renameNode(nodes[i]);
                    break;
                }
            }
            return true;
        }
    }
    
    
    saveToFile = function(){
        if(!mainNode) return mainForm.msgbox("请先添加主节点!","提示");
        var parentMap = {};
        for(p=1; #nodes){
            for(c=1; #nodes[p].children){
                parentMap[ nodes[p].children[c] ] = p;
            }
        }
        var data = { nodes = {} };
        for(i=1; #nodes){
            var n = nodes[i];
            table.push(data.nodes, {
                id = i;
                parentId = parentMap[n] or 0;
                x = n.x;
                y = n.y;
                text = n.text;
                level = n.level;
                isMain = n.isMain;
            });
        }
    
        var json = web.json.stringify(data);
        var path = fsys.dlg.save("JSON 文件(*.json)|*.json|所有文件(*.*)|*.*", , winform);
        if(path){
            string.save(path, json);
            mainForm.msgbox("已保存:" ++ path, "提示");
        }
    }
    
    loadFromFile = function(){
        var path = fsys.dlg.open("JSON 文件(*.json)|*.json|所有文件(*.*)|*.*", , winform);
        if(!path) return;
        var json = string.load(path);
        if(!json) return mainForm.msgbox("文件读取失败:" ++ path,"提示");
        var data = web.json.parse(json);
        if(!data || !data.nodes) return mainForm.msgbox("文件格式不正确!","提示");
    
        nodes = {};
        mainNode = null;
        selectedNode = null;
        var map = {};
        for(i=1; #data.nodes){
            var d = data.nodes[i];
            var n = Node(d.x, d.y, d.text, d.isMain, d.level);
            map[d.id] = n;
            table.push(nodes, n);
            if(d.isMain) mainNode = n;
        }
        for(i=1; #data.nodes){
            var d = data.nodes[i];
            if(d.parentId && d.parentId != 0){
                var parent = map[d.parentId];
                if(parent) table.push(parent.children, map[d.id]);
            }
        }
        selectedNode = mainNode;
        mainForm.canvas.redraw();
        mainForm.msgbox("已打开:" ++ path, "提示");
    }
    
    renameNode = function(node){
        if(!node) return;
        var f = win.form(text="重命名节点"; right=320; bottom=120; border="dialog");
        f.add(
            edit={cls="edit"; left=15; top=15; right=305; bottom=42; text=node.text; edge=1};
            btnOk={cls="button"; left=160; top=70; right=240; bottom=100; text="确定"};
            btnCancel={cls="button"; left=245; top=70; right=305; bottom=100; text="取消"}
        );
        f.btnOk.oncommand = function(){
            var t = f.edit.text;
            if(#t){ node.text = t; mainForm.canvas.redraw(); }
            f.close();
        };
        f.btnCancel.oncommand = function(){ f.close(); };
        f.doModal(mainForm.hwnd);
    }
    
    deleteNode = function(node){
        if(!node) return;
        if(node.isMain) return mainForm.msgbox("根节点不可删除!", "提示");
        var toRemove = {};
        var stack = {node};
        while(#stack){
            var n = table.pop(stack);
            table.push(toRemove, n);
            for(k=1; #n.children) table.push(stack, n.children[k]);
        }
        for(i=1; #nodes){
            for(j=1; #nodes[i].children){
                if(nodes[i].children[j] === node){ table.remove(nodes[i].children, j); i = #nodes + 1; break; }
            }
        }
        for(i=#nodes; 1; -1){
            for(k=1; #toRemove){
                if(nodes[i] === toRemove[k]){ table.remove(nodes, i); break; }
            }
        }
        for(k=1; #toRemove){
            if(selectedNode === toRemove[k]){ selectedNode = null; break; }
        }
        mainForm.canvas.redraw();
    }
    
    exportPNG = function(){
        if(!mainNode) return mainForm.msgbox("请先添加主节点!", "提示");
        var w = mainForm.canvas.clientWidth;
        var h = mainForm.canvas.clientHeight;
        var scale = 2;  
        var bmp = gdip.bitmap(w * scale, h * scale);
        var g = bmp.getGraphics();
        g.scaleTransform(scale, scale);   
        g.clear(0xFFFFFFFF);
        drawContent(g, w, h);
        g.dispose();
        var path = fsys.dlg.save("PNG 图片(*.png)|*.png|所有文件(*.*)|*.*", , winform);
        if(path){
            var ok, err = bmp.save(path);
            if(ok) mainForm.msgbox("已导出:" ++ path, "完成");
            else mainForm.msgbox("导出失败:" ++ (err or "未知错误"), "错误");
        }
        bmp.dispose();
    }
    
    
    
    mainForm.canvas.onMouseDown = function(wParam, lParam) {
        var x = lParam & 0xFFFF;          
        var y = (lParam >> 16) & 0xFFFF;
        draggingNode = null; 
        mouseMoved = false;
    
        var lastSelected = selectedNode;
        selectedNode = null; 
    
        for(i=#nodes; 1; -1) {
            if(::PtInRect(nodes[i].getRect(), x, y)) {
                selectedNode = nodes[i];
                draggingNode = nodes[i];
                offsetX, offsetY = draggingNode.x - x, draggingNode.y - y;
                win.setCapture(mainForm.canvas.hwnd); 
                break;
            }
        }
        if(lastSelected != selectedNode) mainForm.canvas.redraw();
    }
    
    
    mainForm.canvas.onMouseMove = function(wParam, lParam) {
        var x = lParam & 0xFFFF;
        var y = (lParam >> 16) & 0xFFFF;
    
        if(draggingNode) {
            draggingNode.x = x + offsetX;
            draggingNode.y = y + offsetY;
            mouseMoved = true;            
            mainForm.canvas.redraw();
        } else {
            var lastHover = hoveredIndex;
            hoveredIndex = null;
            for(i=#nodes; 1; -1) {
                if(::PtInRect(nodes[i].getRect(), x, y)) {
                    hoveredIndex = i;
                    break;
                }
            }
    
            if(hoveredIndex && lastHover != hoveredIndex) {
                var node = nodes[hoveredIndex];
                var r = node.getRect();
                tooltip.setText(node.text ++ '\n层级: ' ++ node.level);
                var sx, sy = win.toScreen(mainForm.canvas.hwnd, r.right + 10, r.top + r.height()/4);
                tooltip.trackPopup(true, sx, sy); 
            } elseif(!hoveredIndex) {
                tooltip.trackPopup(false);
            }
        }
    }
    
    mainForm.canvas.onMouseLeave = function(){
        hoveredIndex = null;
        tooltip.trackPopup(false);
    }
    
    mainForm.canvas.onMouseUp = function(wParam, lParam) {
        draggingNode = null;
        win.releaseCapture();
    }
    
    
    mainForm.canvas.onKeyUp = function(keyCode,lParam){
        if(keyCode == 0x9/*_VK_TAB*/){
          if(mainNode) addSub();
        }
    }
    
    // Ctrl+S 保存 / Ctrl+O 打开 / Delete 删除节点
    mainForm.canvas.onKeyDown = function(keyCode, lParam){
        if(::GetAsyncKeyState(0x11) & 0x8000){     
            if(keyCode == 0x53){ saveToFile(); return; }   
            if(keyCode == 0x4F){ loadFromFile(); return; }  
        }
        if(keyCode == 0x2E/*_VK_DELETE*/){
            if(selectedNode) deleteNode(selectedNode);
        }
    }
    
    mainForm.show();
    addMain();    
    
    return win.loopMessage();

  • 光庆 8天前
    0 11
    mfk AI 改写的import&nbsp;win.ui; import&nbsp;gdip;&nbsp; import&nbsp;web.json;&nbsp;& ...

    漂亮 

  • 近我者赤 8天前
    0 12

  • 近我者赤 6天前
    0 13

    再提个需求啊,能不能选中节点后能改变表示节点的圆形的大小,比如在其四周出现可拖动的框:

  • Mr_MAO 6天前
    0 14
  • 近我者赤 5天前
    0 15
    Mr_MAO 可以参考这个帖子:[gdi] 绘制矩形和椭圆,并实现鼠标缩放、拖动
    参考了,没成功😬
  • 近我者赤 5天前
    1 16
    Mr_MAO 可以参考这个帖子:[gdi] 绘制矩形和椭圆,并实现鼠标缩放、拖动

    还得是AI,实现了:

    import win.ui;
    import gdip; 
    import web.json;     
    import fsys.dlg;    
    /*DSG{{*/
    var mainForm = win.form(text="aardio 思维导图 (* 右键:弹出菜单 | 双击:改名 | Tab:加子节点 | Delete:删除 | Ctrl+S:保存 Ctrl+O:打开 *)";right=839;bottom=495)
    mainForm.add(
    canvas={cls="plus";left=0;top=0;right=839;bottom=495;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
    )
    /*}}*/
    
    // 创建气泡提示
    import win.ui.tooltip;
    var tooltip = win.ui.tooltip(mainForm);
    tooltip.activate(true);
    
    // 定义子节点颜色列表
    var childColorValues = {
        0xFF4CAF50; // 绿色
        0xFF2196F3; // 蓝色
        0xFFFF9800; // 橙色
        0xFF9C27B0; // 紫色
        0xFF00BCD4; // 青色
        0xFF795548; // 棕色
    }
    
    var childBrushes = table.map(childColorValues, function(v){
        return gdip.solidBrush(v); 
    })
    
    var mainBrush   = gdip.solidBrush(0xFFFF4444); // 主节点红色
    var textBrush   = gdip.solidBrush(0xFFFFFFFF); // 文字白色
    var linePen     = gdip.pen(0xAAB2A1FF, 2);     // 连线淡紫色
    var selectPen   = gdip.pen(0xFFFFFF00, 3);     // 选中高亮黄色
    var handleBrush = gdip.solidBrush(0xFF00A5FF); // 新增:控制点蓝色画刷
    
    var family = gdip.family("Microsoft YaHei");
    var font = family.createFont(14, 0, 2);
    var fontRoot = family.createFont(19, 0, 2);
    var format = gdip.stringformat();
    format.align = 1;
    format.lineAlign = 1;
    
    var nodes = {};         
    var mainNode = null;    
    var draggingNode = null;
    var resizingNode = null; // 新增:记录当前正在被改变大小的节点
    var selectedNode = null;
    var hoveredIndex = null;
    var offsetX, offsetY = 0, 0;
    var mouseMoved = false;      
    var lastClickTime = 0;       
    var lastClickNode = null;    
    
    // 节点类定义
    class Node {
        ctor(x, y, text, isMain = false, level = 0) {
            this.x = x;
            this.y = y;
            this.text = text;
            this.children = {}; 
            this.isMain = isMain;
            this.level = level; 
            
            // 初始计算半径并保存为属性,便于后续拖拽修改
            this.r = isMain ? 50 : (40 - (level * 3));
            if(this.r < 25) this.r = 25; 
        }
    
        getRect = function(){
            // 改为读取 this.r 属性
            return ::RECT(this.x - this.r, this.y - this.r, this.x + this.r, this.y + this.r);
        }
    }
    
    drawContent = function(g, w, h){
        // 抗锯齿
        g.smoothingMode = 4;       
        g.textRenderingHint = 4;   
        // 网格
        var pen = gdip.pen(0x55B1FDAD, 2); 
        for(i=30; h; 30){
            g.drawLine(pen, 0, i, w, i);
        }
        for(i=30; w; 30){
            g.drawLine(pen, i, 0, i, h);
        }
        // 连线
        for(i=1; #nodes) {
            var node = nodes[i];
            for(j=1; #node.children) {
                var child = node.children[j];
                g.drawBezier(linePen,
                    node.x, node.y, 
                    (child.x + node.x) / 2, node.y,
                    (child.x + node.x) / 2, child.y,
                    child.x, child.y
                );
            }
        }
        // 节点
        for(i=1; #nodes) {
            var node = nodes[i];
            var rect = node.getRect();
            var brush;
            if(node.isMain) {
                brush = mainBrush;
            } else {
                var colorIdx = ((node.level - 1) % #childBrushes) + 1;
                brush = childBrushes[colorIdx];
            }
            
            g.fillEllipse(brush, rect.left, rect.top, rect.width(), rect.height());
            
            if(node == selectedNode){
                // 绘制选中高亮光圈
                g.drawEllipse(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
                
                // 新增:绘制四周的缩放控制小方框
                var hr = 4; // 方框的半边长
                g.fillRectangle(handleBrush, rect.left - hr, node.y - hr, hr*2, hr*2);   // 左
                g.fillRectangle(handleBrush, rect.right - hr, node.y - hr, hr*2, hr*2);  // 右
                g.fillRectangle(handleBrush, node.x - hr, rect.top - hr, hr*2, hr*2);    // 上
                g.fillRectangle(handleBrush, node.x - hr, rect.bottom - hr, hr*2, hr*2); // 下
            }
            
            g.drawString(node.text,
                node.isMain ? fontRoot : font,
                rect.float(), format, textBrush
            );
        }
    }
    
    mainForm.canvas.onDrawBackground = function(graphics, rc, backgroundColor, color){
        graphics.clear(0xFFFFFFFF);
    }
    
    mainForm.canvas.onDrawForeground = function(graphics, rc, foregroundRect) {
        drawContent(graphics, rc.width, rc.height);
    }
    
    // 添加主节点
    addMain = function(){
        if(!mainNode) {
            mainNode = Node(mainForm.canvas.clientWidth/4, mainForm.canvas.clientHeight/2, "根节点", true, 0);
            table.push(nodes, mainNode);
            selectedNode = mainNode;
            
            mainForm.canvas.setFocus();
            mainForm.canvas.redraw();
        } else {
            mainForm.msgbox("主节点已存在!","提示");
        }
    }
    
    // 添加子节点
    addSub = function(){
        var parent = selectedNode or mainNode; 
        if(!parent) return mainForm.msgbox("请先添加主节点!","提示");
    
        var newLevel = parent.level + 1;
        var newNode = Node(
            parent.x + math.random(90, 150), 
            parent.y + math.random(-90, 90), 
            "节点" ++ (#nodes + 1),
            false,
            newLevel
        );
    
        table.push(nodes, newNode);
        table.push(parent.children, newNode);
    
        selectedNode = newNode;
        mainForm.canvas.redraw();
    }
    
    var popmenu = win.ui.popmenu(mainForm);
    popmenu.add("保存\tCtrl+S", function(){ saveToFile(); });
    popmenu.add("打开\tCtrl+O", function(){ loadFromFile(); });
    popmenu.add("添加子节点\tTab", function(){ if(mainNode) addSub(); });
    popmenu.add("重命名", function(){ if(selectedNode) renameNode(selectedNode); });
    popmenu.add("删除节点", function(){ if(selectedNode) deleteNode(selectedNode); });
    popmenu.add("导出 PNG", function(){ exportPNG(); });
    popmenu.add("退出", function(){ mainForm.close() });
    
    mainForm.canvas.wndproc = function(hwnd, msg, wParam, lParam){
        if(msg == 0x205/*_WM_RBUTTONUP*/){
            var x, y = win.getCursorPos()  
            popmenu.popup(x, y, true);
            return true; 
        }
        if(msg == 0x203/*_WM_LBUTTONDBLCLK*/){
            var x = lParam & 0xFFFF;
            var y = (lParam >> 16) & 0xFFFF;
            for(i=#nodes; 1; -1){
                if(::PtInRect(nodes[i].getRect(), x, y)){
                    renameNode(nodes[i]);
                    break;
                }
            }
            return true;
        }
    }
    
    saveToFile = function(){
        if(!mainNode) return mainForm.msgbox("请先添加主节点!","提示");
        var parentMap = {};
        for(p=1; #nodes){
            for(c=1; #nodes[p].children){
                parentMap[ nodes[p].children[c] ] = p;
            }
        }
        var data = { nodes = {} };
        for(i=1; #nodes){
            var n = nodes[i];
            table.push(data.nodes, {
                id = i;
                parentId = parentMap[n] or 0;
                x = n.x;
                y = n.y;
                text = n.text;
                level = n.level;
                isMain = n.isMain;
                r = n.r; // 保存自定义半径大小
            });
        }
    
        var json = web.json.stringify(data);
        var path = fsys.dlg.save("JSON 文件(*.json)|*.json|所有文件(*.*)|*.*", , winform);
        if(path){
            string.save(path, json);
            mainForm.msgbox("已保存:" ++ path, "提示");
        }
    }
    
    loadFromFile = function(){
        var path = fsys.dlg.open("JSON 文件(*.json)|*.json|所有文件(*.*)|*.*", , winform);
        if(!path) return;
        var json = string.load(path);
        if(!json) return mainForm.msgbox("文件读取失败:" ++ path,"提示");
        var data = web.json.parse(json);
        if(!data || !data.nodes) return mainForm.msgbox("文件格式不正确!","提示");
    
        nodes = {};
        mainNode = null;
        selectedNode = null;
        var map = {};
        for(i=1; #data.nodes){
            var d = data.nodes[i];
            var n = Node(d.x, d.y, d.text, d.isMain, d.level);
            if(d.r) n.r = d.r; // 恢复自定义半径大小
            map[d.id] = n;
            table.push(nodes, n);
            if(d.isMain) mainNode = n;
        }
        for(i=1; #data.nodes){
            var d = data.nodes[i];
            if(d.parentId && d.parentId != 0){
                var parent = map[d.parentId];
                if(parent) table.push(parent.children, map[d.id]);
            }
        }
        selectedNode = mainNode;
        mainForm.canvas.redraw();
        mainForm.msgbox("已打开:" ++ path, "提示");
    }
    
    renameNode = function(node){
        if(!node) return;
        var f = win.form(text="重命名节点"; right=320; bottom=120; border="dialog");
        f.add(
            edit={cls="edit"; left=15; top=15; right=305; bottom=42; text=node.text; edge=1};
            btnOk={cls="button"; left=160; top=70; right=240; bottom=100; text="确定"};
            btnCancel={cls="button"; left=245; top=70; right=305; bottom=100; text="取消"}
        );
        f.btnOk.oncommand = function(){
            var t = f.edit.text;
            if(#t){ node.text = t; mainForm.canvas.redraw(); }
            f.close();
        };
        f.btnCancel.oncommand = function(){ f.close(); };
        f.doModal(mainForm.hwnd);
    }
    
    deleteNode = function(node){
        if(!node) return;
        if(node.isMain) return mainForm.msgbox("根节点不可删除!", "提示");
        var toRemove = {};
        var stack = {node};
        while(#stack){
            var n = table.pop(stack);
            table.push(toRemove, n);
            for(k=1; #n.children) table.push(stack, n.children[k]);
        }
        for(i=1; #nodes){
            for(j=1; #nodes[i].children){
                if(nodes[i].children[j] === node){ table.remove(nodes[i].children, j); i = #nodes + 1; break; }
            }
        }
        for(i=#nodes; 1; -1){
            for(k=1; #toRemove){
                if(nodes[i] === toRemove[k]){ table.remove(nodes, i); break; }
            }
        }
        for(k=1; #toRemove){
            if(selectedNode === toRemove[k]){ selectedNode = null; break; }
        }
        mainForm.canvas.redraw();
    }
    
    exportPNG = function(){
        if(!mainNode) return mainForm.msgbox("请先添加主节点!", "提示");
        var w = mainForm.canvas.clientWidth;
        var h = mainForm.canvas.clientHeight;
        var scale = 2;  
        var bmp = gdip.bitmap(w * scale, h * scale);
        var g = bmp.getGraphics();
        g.scaleTransform(scale, scale);   
        g.clear(0xFFFFFFFF);
        drawContent(g, w, h);
        g.dispose();
        var path = fsys.dlg.save("PNG 图片(*.png)|*.png|所有文件(*.*)|*.*", , winform);
        if(path){
            var ok, err = bmp.save(path);
            if(ok) mainForm.msgbox("已导出:" ++ path, "完成");
            else mainForm.msgbox("导出失败:" ++ (err or "未知错误"), "错误");
        }
        bmp.dispose();
    }
    
    mainForm.canvas.onMouseDown = function(wParam, lParam) {
        var x = lParam & 0xFFFF;          
        var y = (lParam >> 16) & 0xFFFF;
        draggingNode = null; 
        resizingNode = null; // 重置
        mouseMoved = false;
    
        // 优先检测:是否点击了当前选中节点的"缩放控制点"
        if(selectedNode) {
            var rect = selectedNode.getRect();
            var hr = 6; // 稍微扩大点击判定区域
            var handles = {
                ::RECT(rect.left - hr, selectedNode.y - hr, rect.left + hr, selectedNode.y + hr); // 左
                ::RECT(rect.right - hr, selectedNode.y - hr, rect.right + hr, selectedNode.y + hr); // 右
                ::RECT(selectedNode.x - hr, rect.top - hr, selectedNode.x + hr, rect.top + hr); // 上
                ::RECT(selectedNode.x - hr, rect.bottom - hr, selectedNode.x + hr, rect.bottom + hr); // 下
            }
            
            for(i=1; #handles){
                if(::PtInRect(handles[i], x, y)){
                    resizingNode = selectedNode;
                    win.setCapture(mainForm.canvas.hwnd);
                    return; // 如果点击了控制点则直接返回,不触发节点拖拽逻辑
                }
            }
        }
    
        var lastSelected = selectedNode;
        selectedNode = null; 
    
        // 检测常规节点点击(节点拖拽与选中)
        for(i=#nodes; 1; -1) {
            if(::PtInRect(nodes[i].getRect(), x, y)) {
                selectedNode = nodes[i];
                draggingNode = nodes[i];
                offsetX, offsetY = draggingNode.x - x, draggingNode.y - y;
                win.setCapture(mainForm.canvas.hwnd); 
                break;
            }
        }
        if(lastSelected != selectedNode) mainForm.canvas.redraw();
    }
    
    mainForm.canvas.onMouseMove = function(wParam, lParam) {
        var x = lParam & 0xFFFF;
        var y = (lParam >> 16) & 0xFFFF;
    
        // 如果处于调整大小模式
        if(resizingNode) {
            var dx = x - resizingNode.x;
            var dy = y - resizingNode.y;
            var newR = math.sqrt(dx*dx + dy*dy);
            if(newR < 20) newR = 20; // 限制节点的最小半径
            resizingNode.r = newR;
            mainForm.canvas.redraw();
        }
        // 如果处于拖动节点模式
        elseif(draggingNode) {
            draggingNode.x = x + offsetX;
            draggingNode.y = y + offsetY;
            mouseMoved = true;            
            mainForm.canvas.redraw();
        } 
        // 悬停提示
        else {
            var lastHover = hoveredIndex;
            hoveredIndex = null;
            for(i=#nodes; 1; -1) {
                if(::PtInRect(nodes[i].getRect(), x, y)) {
                    hoveredIndex = i;
                    break;
                }
            }
    
            if(hoveredIndex && lastHover != hoveredIndex) {
                var node = nodes[hoveredIndex];
                var r = node.getRect();
                tooltip.setText(node.text ++ '\n层级: ' ++ node.level);
                var sx, sy = win.toScreen(mainForm.canvas.hwnd, r.right + 10, r.top + r.height()/4);
                tooltip.trackPopup(true, sx, sy); 
            } elseif(!hoveredIndex) {
                tooltip.trackPopup(false);
            }
        }
    }
    
    mainForm.canvas.onMouseLeave = function(){
        hoveredIndex = null;
        tooltip.trackPopup(false);
    }
    
    mainForm.canvas.onMouseUp = function(wParam, lParam) {
        draggingNode = null;
        resizingNode = null; // 释放调整大小状态
        win.releaseCapture();
    }
    
    mainForm.canvas.onKeyUp = function(keyCode,lParam){
        if(keyCode == 0x9/*_VK_TAB*/){
          if(mainNode) addSub();
        }
    }
    
    // Ctrl+S 保存 / Ctrl+O 打开 / Delete 删除节点
    mainForm.canvas.onKeyDown = function(keyCode, lParam){
        if(::GetAsyncKeyState(0x11) & 0x8000){     
            if(keyCode == 0x53){ saveToFile(); return; }   
            if(keyCode == 0x4F){ loadFromFile(); return; }  
        }
        if(keyCode == 0x2E/*_VK_DELETE*/){
            if(selectedNode) deleteNode(selectedNode);
        }
    }
    
    mainForm.show();
    addMain();    
    
    return win.loopMessage();


  • 近我者赤 5天前
    0 17


    import win.ui;
    import gdip; 
    import web.json;     
    import fsys.dlg;    
    /*DSG{{*/
    var mainForm = win.form(text="aardio 思维导图 (* 右键:弹出菜单 | 双击:改名 | Tab:加子节点 | Delete:删除 | Ctrl+S:保存 Ctrl+O:打开 *)";right=839;bottom=495)
    mainForm.add(
    canvas={cls="plus";left=0;top=0;right=839;bottom=495;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
    )
    /*}}*/
    
    // 创建气泡提示
    import win.ui.tooltip;
    var tooltip = win.ui.tooltip(mainForm);
    tooltip.activate(true);
    
    // 定义子节点背景颜色列表
    var childColorValues = {
        0xFF4CAF50; // 绿色
        0xFF2196F3; // 蓝色
        0xFFFF9800; // 橙色
        0xFF9C27B0; // 紫色
        0xFF00BCD4; // 青色
        0xFF795548; // 棕色
    }
    
    var childBrushes = table.map(childColorValues, function(v){
        return gdip.solidBrush(v); 
    })
    
    var mainBrush   = gdip.solidBrush(0xFFFF4444); // 主节点红色
    var textBrush   = gdip.solidBrush(0xFFFFFFFF); // 默认文字白色
    var linePen     = gdip.pen(0xAA3B2AFF, 3);     // 连线淡紫色
    var selectPen   = gdip.pen(0xFFFFFF00, 3);     // 选中高亮黄色
    var handleBrush = gdip.solidBrush(0xFF00A5FF); // 控制点蓝色画刷
    
    var family = gdip.family("Microsoft YaHei");
    var font = family.createFont(16, 1, 2);     // 默认子节点字体
    var fontRoot = family.createFont(22, 1, 2); // 默认主节点字体
    var format = gdip.stringformat();
    format.align = 1;
    format.lineAlign = 1;
    
    var nodes = {};         
    var mainNode = null;    
    var draggingNode = null;
    var resizingNode = null; 
    var selectedNode = null;
    var hoveredIndex = null;
    var offsetX, offsetY = 0, 0;
    var mouseMoved = false;      
    
    var hoveredHandleType = null;
    var resizingHandleType = null;
    
    // 节点类定义
    class Node {
        ctor(x, y, text, isMain = false, level = 0) {
            this.x = x;
            this.y = y;
            this.text = text;
            this.children = {}; 
            this.isMain = isMain;
            this.level = level; 
            
            this.rx = isMain ? 50 : (40 - (level * 3));
            if(this.rx < 25) this.rx = 25; 
            this.ry = this.rx;
            
            this.shape = "circle"; // 形状:circle, ellipse, rect
            this.color = null;     // 自定义背景颜色
            this.textColor = null; // 自定义文字颜色
            this.textSize = null;  // 自定义文字大小
        }
    
        getRect = function(){
            return ::RECT(this.x - this.rx, this.y - this.ry, this.x + this.rx, this.y + this.ry);
        }
    }
    
    drawContent = function(g, w, h){
        g.smoothingMode = 4;       
        g.textRenderingHint = 4;   
        var pen = gdip.pen(0x55B1FDAD, 2); 
        for(i=30; h; 30){
            g.drawLine(pen, 0, i, w, i);
        }
        for(i=30; w; 30){
            g.drawLine(pen, i, 0, i, h);
        }
        
        // 连线
        for(i=1; #nodes) {
            var node = nodes[i];
            for(j=1; #node.children) {
                var child = node.children[j];
                g.drawBezier(linePen,
                    node.x, node.y, 
                    (child.x + node.x) / 2, node.y,
                    (child.x + node.x) / 2, child.y,
                    child.x, child.y
                );
            }
        }
        
        // 节点
        for(i=1; #nodes) {
            var node = nodes[i];
            var rect = node.getRect();
            var brush;
            var customBrush = false;
            
            // --- 绘制图形背景 ---
            if(node.color){
                brush = gdip.solidBrush(node.color);
                customBrush = true;
            } else {
                if(node.isMain) brush = mainBrush;
                else brush = childBrushes[((node.level - 1) % #childBrushes) + 1];
            }
            
            if(node.shape == "circle") {
                g.fillEllipse(brush, rect.left, rect.top, rect.width(), rect.height());
                if(node == selectedNode) g.drawEllipse(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            } elseif(node.shape == "rect") {
                g.fillRectangle(brush, rect.left, rect.top, rect.width(), rect.height());
                if(node == selectedNode) g.drawRectangle(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            } elseif(node.shape == "ellipse") {
                g.fillEllipse(brush, rect.left, rect.top, rect.width(), rect.height());
                if(node == selectedNode) g.drawEllipse(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            } elseif(node.shape == "line"){
                g.fillRectangle(gdip.solidBrush(0xFFFEFEFE), rect.left, rect.top, rect.width(), rect.height());
                g.fillRectangle(brush, rect.left, rect.bottom, rect.width(), 3);
                if(node == selectedNode) g.drawRectangle(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            }
            
            
            if(customBrush && brush) brush.dispose();
            
            // --- 绘制控制点 ---
            if(node == selectedNode){
                var hr = 4;
                g.fillRectangle(handleBrush, rect.left - hr, node.y - hr, hr*2, hr*2);   
                g.fillRectangle(handleBrush, rect.right - hr, node.y - hr, hr*2, hr*2);  
                g.fillRectangle(handleBrush, node.x - hr, rect.top - hr, hr*2, hr*2);    
                g.fillRectangle(handleBrush, node.x - hr, rect.bottom - hr, hr*2, hr*2); 
            }
            
            // --- 绘制文字 ---
            var curTextBrush = textBrush;
            var curFont = node.isMain ? fontRoot : font;
            var customTextBrush = false;
            var customFont = false;
            
            if(node.textColor){
                curTextBrush = gdip.solidBrush(node.textColor);
                customTextBrush = true;
            }
            
            if(node.textSize){
                curFont = family.createFont(node.textSize, 0, 2);
                customFont = true;
            }
            
            g.drawString(node.text, curFont, rect.float(), format, curTextBrush);
            
            // 释放临时画刷和字体,防止内存泄漏
            if(customTextBrush) curTextBrush.dispose();
            if(customFont) curFont.dispose();
        }
    }
    
    mainForm.canvas.onDrawBackground = function(graphics, rc, backgroundColor, color){
        graphics.clear(0xFFFFFFFF);
    }
    
    mainForm.canvas.onDrawForeground = function(graphics, rc, foregroundRect) {
        drawContent(graphics, rc.width, rc.height);
    }
    
    addMain = function(){
        if(!mainNode) {
            mainNode = Node(mainForm.canvas.clientWidth/4, mainForm.canvas.clientHeight/2, "根节点", true, 0);
            table.push(nodes, mainNode);
            selectedNode = mainNode;
            mainForm.canvas.setFocus();
            mainForm.canvas.redraw();
        } else {
            mainForm.msgbox("主节点已存在!","提示");
        }
    }
    
    addSub = function(){
        var parent = selectedNode or mainNode; 
        if(!parent) return mainForm.msgbox("请先添加主节点!","提示");
        var newLevel = parent.level + 1;
        var newNode = Node(
            parent.x + math.random(90, 150), 
            parent.y + math.random(-90, 90), 
            "节点" ++ (#nodes + 1),
            false,
            newLevel
        );
        table.push(nodes, newNode);
        table.push(parent.children, newNode);
        selectedNode = newNode;
        mainForm.canvas.redraw();
    }
    
    // ----------------- 右键菜单构建 -----------------
    var popmenu = win.ui.popmenu(mainForm);
    
    // 形状子菜单
    var shapeMenu = win.ui.popmenu(mainForm);
    shapeMenu.add("圆形", function(){ if(selectedNode){ selectedNode.shape = "circle"; mainForm.canvas.redraw(); }});
    shapeMenu.add("椭圆", function(){ if(selectedNode){ selectedNode.shape = "ellipse"; mainForm.canvas.redraw(); }});
    shapeMenu.add("矩形", function(){ if(selectedNode){ selectedNode.shape = "rect"; mainForm.canvas.redraw(); }});
    shapeMenu.add("直线", function(){ if(selectedNode){ selectedNode.shape = "line"; mainForm.canvas.redraw(); }});
    
    // 图形颜色子菜单
    var colorMenu = win.ui.popmenu(mainForm);
    var presetColors = {
        {"恢复默认", null}, {"经典红", 0xFFFF4444}, {"自然绿", 0xFF4CAF50}, {"天空蓝", 0xFF2196F3}, {"活力橙", 0xFFFF9800}, 
        {"优雅紫", 0xFF9C27B0}, {"活泼青", 0xFF00BCD4}, {"沉稳棕", 0xFF795548}, {"月光白", 0xFFEEEEFE}, {"酷炫黑", 0xFF333333}
    }
    for(i=1; #presetColors){
        var c = presetColors[i];
        colorMenu.add(c[1], function(){
            if(selectedNode){ selectedNode.color = c[2]; mainForm.canvas.redraw(); }
        });
    }
    
    // 文字颜色子菜单
    var textColorMenu = win.ui.popmenu(mainForm);
    var presetTextColors = {
        {"恢复默认", null}, {"纯白色", 0xFFFFFFFF}, {"纯黑色", 0xFF000000}, {"鲜红色", 0xFFFF0000},
        {"深蓝色", 0xFF0000FF}, {"亮黄色", 0xFFFFFF00}, {"深灰色", 0xFF666666}
    }
    for(i=1; #presetTextColors){
        var tc = presetTextColors[i];
        textColorMenu.add(tc[1], function(){
            if(selectedNode){ selectedNode.textColor = tc[2]; mainForm.canvas.redraw(); }
        });
    }
    
    // 文字大小子菜单
    var textSizeMenu = win.ui.popmenu(mainForm);
    var presetTextSizes = {
        {"恢复默认", null}, {"特小 (10)", 10}, {"小号 (12)", 12}, {"中号 (14)", 14},
        {"大号 (16)", 16}, {"特大 (18)", 18}, {"超大 (24)", 24}, {"极巨 (32)", 32}
    }
    for(i=1; #presetTextSizes){
        var ts = presetTextSizes[i];
        textSizeMenu.add(ts[1], function(){
            if(selectedNode){ selectedNode.textSize = ts[2]; mainForm.canvas.redraw(); }
        });
    }
    
    popmenu.add('添加子节点\tTab', function(){ if(mainNode) addSub(); });
    popmenu.add("重命名", function(){ if(selectedNode) renameNode(selectedNode); });
    popmenu.add(); // 分隔线
    popmenu.add("设置图形形状", shapeMenu);
    popmenu.add("设置图形颜色", colorMenu);
    popmenu.add("设置文字颜色", textColorMenu);
    popmenu.add("设置文字大小", textSizeMenu);
    popmenu.add(); // 分隔线
    popmenu.add("删除节点", function(){ if(selectedNode) deleteNode(selectedNode); });
    popmenu.add('保存\tCtrl+S', function(){ saveToFile(); });
    popmenu.add('打开\tCtrl+O', function(){ loadFromFile(); });
    popmenu.add("导出 PNG", function(){ exportPNG(); });
    popmenu.add("退出", function(){ mainForm.close() });
    // ------------------------------------------------
    
    mainForm.canvas.wndproc = function(hwnd, msg, wParam, lParam){
        if(msg == 0x20/*_WM_SETCURSOR*/){
            var curType = resizingHandleType or hoveredHandleType;
            if(curType){
                var curId = (curType == "horz") ? 32644 : 32645;
                ::SetCursor(::LoadCursor(null, topointer(curId)));
                return true; 
            }
        }
        
        if(msg == 0x205/*_WM_RBUTTONUP*/){
            var x, y = win.getCursorPos()  
            popmenu.popup(x, y, true);
            return true; 
        }
        if(msg == 0x203/*_WM_LBUTTONDBLCLK*/){
            var x = lParam & 0xFFFF;
            var y = (lParam >> 16) & 0xFFFF;
            for(i=#nodes; 1; -1){
                if(::PtInRect(nodes[i].getRect(), x, y)){
                    renameNode(nodes[i]);
                    break;
                }
            }
            return true;
        }
    }
    
    saveToFile = function(){
        if(!mainNode) return mainForm.msgbox("请先添加主节点!","提示");
        var parentMap = {};
        for(p=1; #nodes){
            for(c=1; #nodes[p].children){
                parentMap[ nodes[p].children[c] ] = p;
            }
        }
        var data = { nodes = {} };
        for(i=1; #nodes){
            var n = nodes[i];
            table.push(data.nodes, {
                id = i;
                parentId = parentMap[n] or 0;
                x = n.x;
                y = n.y;
                text = n.text;
                level = n.level;
                isMain = n.isMain;
                rx = n.rx; 
                ry = n.ry;
                shape = n.shape;
                color = n.color;
                textColor = n.textColor; // 持久化文字颜色
                textSize = n.textSize;   // 持久化文字大小
            });
        }
        var json = web.json.stringify(data);
        var path = fsys.dlg.save("JSON 文件(*.json)|*.json|所有文件(*.*)|*.*", , winform);
        if(path){
            string.save(path, json);
            mainForm.msgbox("已保存:" ++ path, "提示");
        }
    }
    
    loadFromFile = function(){
        var path = fsys.dlg.open("JSON 文件(*.json)|*.json|所有文件(*.*)|*.*", , winform);
        if(!path) return;
        var json = string.load(path);
        if(!json) return mainForm.msgbox("文件读取失败:" ++ path,"提示");
        var data = web.json.parse(json);
        if(!data || !data.nodes) return mainForm.msgbox("文件格式不正确!","提示");
        nodes = {};
        mainNode = null;
        selectedNode = null;
        var map = {};
        for(i=1; #data.nodes){
            var d = data.nodes[i];
            var n = Node(d.x, d.y, d.text, d.isMain, d.level);
            
            n.rx = d.rx or d.r or n.rx; 
            n.ry = d.ry or d.r or n.ry;
            n.shape = d.shape or "circle";
            n.color = d.color;
            n.textColor = d.textColor; 
            n.textSize = d.textSize;
            
            map[d.id] = n;
            table.push(nodes, n);
            if(d.isMain) mainNode = n;
        }
        for(i=1; #data.nodes){
            var d = data.nodes[i];
            if(d.parentId && d.parentId != 0){
                var parent = map[d.parentId];
                if(parent) table.push(parent.children, map[d.id]);
            }
        }
        selectedNode = mainNode;
        mainForm.canvas.redraw();
        mainForm.msgbox("已打开:" ++ path, "提示");
    }
    
    renameNode = function(node){
        if(!node) return;
        var f = win.form(text="重命名节点"; right=320; bottom=120; border="dialog");
        f.add(
            edit={cls="edit"; left=15; top=15; right=305; bottom=42; text=node.text; edge=1};
            btnOk={cls="button"; left=160; top=70; right=240; bottom=100; text="确定"};
            btnCancel={cls="button"; left=245; top=70; right=305; bottom=100; text="取消"}
        );
        f.btnOk.oncommand = function(){
            var t = f.edit.text;
            if(#t){ node.text = t; mainForm.canvas.redraw(); }
            f.close();
        };
        f.btnCancel.oncommand = function(){ f.close(); };
        f.doModal(mainForm.hwnd);
    }
    
    deleteNode = function(node){
        if(!node) return;
        if(node.isMain) return mainForm.msgbox("根节点不可删除!", "提示");
        var toRemove = {};
        var stack = {node};
        while(#stack){
            var n = table.pop(stack);
            table.push(toRemove, n);
            for(k=1; #n.children) table.push(stack, n.children[k]);
        }
        for(i=1; #nodes){
            for(j=1; #nodes[i].children){
                if(nodes[i].children[j] === node){ table.remove(nodes[i].children, j); i = #nodes + 1; break; }
            }
        }
        for(i=#nodes; 1; -1){
            for(k=1; #toRemove){
                if(nodes[i] === toRemove[k]){ table.remove(nodes, i); break; }
            }
        }
        for(k=1; #toRemove){
            if(selectedNode === toRemove[k]){ selectedNode = null; break; }
        }
        mainForm.canvas.redraw();
    }
    
    exportPNG = function(){
        if(!mainNode) return mainForm.msgbox("请先添加主节点!", "提示");
        var w = mainForm.canvas.clientWidth;
        var h = mainForm.canvas.clientHeight;
        var scale = 2;  
        var bmp = gdip.bitmap(w * scale, h * scale);
        var g = bmp.getGraphics();
        g.scaleTransform(scale, scale);   
        g.clear(0xFFFFFFFF);
        drawContent(g, w, h);
        g.dispose();
        var path = fsys.dlg.save("PNG 图片(*.png)|*.png|所有文件(*.*)|*.*", , winform);
        if(path){
            var ok, err = bmp.save(path);
            if(ok) mainForm.msgbox("已导出:" ++ path, "完成");
            else mainForm.msgbox("导出失败:" ++ (err or "未知错误"), "错误");
        }
        bmp.dispose();
    }
    
    mainForm.canvas.onMouseDown = function(wParam, lParam) {
        var x = lParam & 0xFFFF;          
        var y = (lParam >> 16) & 0xFFFF;
        draggingNode = null; 
        resizingNode = null; 
        resizingHandleType = null;
        mouseMoved = false;
    
        if(selectedNode) {
            var rect = selectedNode.getRect();
            var hr = 6; 
            var handles = {
                { rc = ::RECT(rect.left - hr, selectedNode.y - hr, rect.left + hr, selectedNode.y + hr); type = "horz" };
                { rc = ::RECT(rect.right - hr, selectedNode.y - hr, rect.right + hr, selectedNode.y + hr); type = "horz" };
                { rc = ::RECT(selectedNode.x - hr, rect.top - hr, selectedNode.x + hr, rect.top + hr); type = "vert" };
                { rc = ::RECT(selectedNode.x - hr, rect.bottom - hr, selectedNode.x + hr, rect.bottom + hr); type = "vert" };
            }
            
            for(i=1; #handles){
                if(::PtInRect(handles[i].rc, x, y)){
                    resizingNode = selectedNode;
                    resizingHandleType = handles[i].type; 
                    win.setCapture(mainForm.canvas.hwnd);
                    return; 
                }
            }
        }
    
        var lastSelected = selectedNode;
        selectedNode = null; 
    
        for(i=#nodes; 1; -1) {
            if(::PtInRect(nodes[i].getRect(), x, y)) {
                selectedNode = nodes[i];
                draggingNode = nodes[i];
                offsetX, offsetY = draggingNode.x - x, draggingNode.y - y;
                win.setCapture(mainForm.canvas.hwnd); 
                break;
            }
        }
        if(lastSelected != selectedNode) mainForm.canvas.redraw();
    }
    
    mainForm.canvas.onMouseMove = function(wParam, lParam) {
        var x = lParam & 0xFFFF;
        var y = (lParam >> 16) & 0xFFFF;
    
        hoveredHandleType = null;
        if(selectedNode && !resizingNode && !draggingNode) {
            var rect = selectedNode.getRect();
            var hr = 6;
            var handles = {
                { rc = ::RECT(rect.left - hr, selectedNode.y - hr, rect.left + hr, selectedNode.y + hr); type = "horz" };
                { rc = ::RECT(rect.right - hr, selectedNode.y - hr, rect.right + hr, selectedNode.y + hr); type = "horz" };
                { rc = ::RECT(selectedNode.x - hr, rect.top - hr, selectedNode.x + hr, rect.top + hr); type = "vert" };
                { rc = ::RECT(selectedNode.x - hr, rect.bottom - hr, selectedNode.x + hr, rect.bottom + hr); type = "vert" };
            }
            for(i=1; #handles){
                if(::PtInRect(handles[i].rc, x, y)){
                    hoveredHandleType = handles[i].type;
                    break;
                }
            }
        }
    
        if(resizingNode) {
            var dx = math.abs(x - resizingNode.x);
            var dy = math.abs(y - resizingNode.y);
    
            if (resizingNode.shape == "circle") {
                var newR = math.sqrt(dx*dx + dy*dy);
                if(newR < 20) newR = 20; 
                resizingNode.rx = newR;
                resizingNode.ry = newR;
            } else {
                if(resizingHandleType == "horz") {
                    if(dx < 20) dx = 20;
                    resizingNode.rx = dx;
                } else if (resizingHandleType == "vert") {
                    if(dy < 20) dy = 20;
                    resizingNode.ry = dy;
                }
            }
            
            mainForm.canvas.redraw();
        }
        elseif(draggingNode) {
            draggingNode.x = x + offsetX;
            draggingNode.y = y + offsetY;
            mouseMoved = true;            
            mainForm.canvas.redraw();
        } 
        else {
            var lastHover = hoveredIndex;
            hoveredIndex = null;
            for(i=#nodes; 1; -1) {
                if(::PtInRect(nodes[i].getRect(), x, y)) {
                    hoveredIndex = i;
                    break;
                }
            }
            if(hoveredIndex && lastHover != hoveredIndex) {
                var node = nodes[hoveredIndex];
                var r = node.getRect();
                tooltip.setText(node.text ++ '\n层级: ' ++ node.level);
                var sx, sy = win.toScreen(mainForm.canvas.hwnd, r.right + 10, r.top + r.height()/4);
                tooltip.trackPopup(true, sx, sy); 
            } elseif(!hoveredIndex) {
                tooltip.trackPopup(false);
            }
        }
    }
    
    mainForm.canvas.onMouseLeave = function(){
        hoveredIndex = null;
        hoveredHandleType = null;
        tooltip.trackPopup(false);
    }
    
    mainForm.canvas.onMouseUp = function(wParam, lParam) {
        draggingNode = null;
        resizingNode = null; 
        resizingHandleType = null;
        win.releaseCapture();
    }
    
    mainForm.canvas.onKeyUp = function(keyCode,lParam){
        if(keyCode == 0x9/*_VK_TAB*/){
          if(mainNode) addSub();
        }
    }
    
    mainForm.canvas.onKeyDown = function(keyCode, lParam){
        if(::GetAsyncKeyState(0x11) & 0x8000){     
            if(keyCode == 0x53){ saveToFile(); return; }   
            if(keyCode == 0x4F){ loadFromFile(); return; }  
        }
        if(keyCode == 0x2E/*_VK_DELETE*/){
            if(selectedNode) deleteNode(selectedNode);
        }
    }
    
    mainForm.show();
    addMain();    
    
    return win.loopMessage();


  • Mr_MAO 5天前
    0 18

    @近我者赤 ,👍👍👍! 越改越完善了,继续加油!!

    建议:

    第[439~440]、[482~483]行:建议改为:

    var x, y = win.getMessagePos(lParam);  //更健壮,避免因lParam取值造成"越界对称错位"

    第[525~528]行:建议增加【防止节点移出画布】的判断,可改为:

    // 防止节点移出画布
    var newX = x + offsetX;
    var newY = y + offsetY;
    var cw = mainForm.canvas.clientWidth;
    var ch = mainForm.canvas.clientHeight;
    var rx = draggingNode.rx;
    var ry = draggingNode.ry;
    if(newX < rx) newX = rx;
    if(newX > cw - rx) newX = cw - rx;
    if(newY < ry) newY = ry;
    if(newY > ch - ry) newY = ch - ry;
    draggingNode.x = newX; draggingNode.y = newY;
    mouseMoved = true;
    mainForm.canvas.redraw();


  • Mr_MAO 5天前
    0 19
    建议:还可增加“中键按下”→移动选中节点及其子节点功能。 😊
  • 近我者赤 4天前
    0 20

    已实现移动时同步移动子节点:

    import win.ui;
    import gdip; 
    import web.json;     
    import fsys.dlg;
    /*DSG{{*/
    var mainForm = win.form(text="aardio 思维导图 (* 右键:弹出菜单 | 双击:改名 | Tab:加子节点 | Delete:删除 | Ctrl+S:保存 Ctrl+O:打开 *)";right=839;bottom=495)
    mainForm.add(
    canvas={cls="plus";left=0;top=0;right=839;bottom=495;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
    )
    /*}}*/
    
    // 创建气泡提示
    import win.ui.tooltip;
    var tooltip = win.ui.tooltip(mainForm);
    tooltip.activate(true);
    
    // 定义子节点背景颜色列表
    var childColorValues = {
        0xFF4CAF50; // 绿色
        0xFF2196F3; // 蓝色
        0xFFFF9800; // 橙色
        0xFF9C27B0; // 紫色
        0xFF00BCD4; // 青色
        0xFF795548; // 棕色
    }
    
    var childBrushes = table.map(childColorValues, function(v){
        return gdip.solidBrush(v); 
    })
    
    var mainBrush   = gdip.solidBrush(0xFFFF4444); // 主节点红色
    var textBrush   = gdip.solidBrush(0xFFFFFFFF); // 默认文字白色
    var linePen     = gdip.pen(0xCC4A3BEF, 3);     // 连线淡紫色
    var selectPen   = gdip.pen(0xFFFFFF00, 3);     // 选中高亮黄色
    var handleBrush = gdip.solidBrush(0xFF00A5FF); // 控制点蓝色画刷
    
    var family = gdip.family("Microsoft YaHei");
    var font = family.createFont(16, 0, 2);     // 默认子节点字体
    var fontRoot = family.createFont(22, 0, 2); // 默认主节点字体
    var format = gdip.stringformat();
    format.align = 1;
    format.lineAlign = 1;
    
    var nodes = {};         
    var mainNode = null;    
    var draggingNode = null;
    var resizingNode = null; 
    var selectedNode = null;
    var hoveredIndex = null;
    var offsetX, offsetY = 0, 0;
    var mouseMoved = false;      
    
    var hoveredHandleType = null;
    var resizingHandleType = null;
    
    // 节点类定义
    class Node {
        ctor(x, y, text, isMain = false, level = 0) {
            this.x = x;
            this.y = y;
            this.text = text;
            this.children = {}; 
            this.isMain = isMain;
            this.level = level; 
            
            this.rx = isMain ? 50 : (40 - (level * 3));
            if(this.rx < 25) this.rx = 25; 
            this.ry = this.rx;
            
            this.shape = "circle"; // 形状:circle, ellipse, rect
            this.color = null;     // 自定义背景颜色
            this.textColor = null; // 自定义文字颜色
            this.textSize = null;  // 自定义文字大小
        }
    
        getRect = function(){
            return ::RECT(this.x - this.rx, this.y - this.ry, this.x + this.rx, this.y + this.ry);
        }
    }
    
    drawContent = function(g, w, h){
        g.smoothingMode = 4;       
        g.textRenderingHint = 4;   
        var pen = gdip.pen(0x55B1FDAD, 2); 
        for(i=30; h; 30){
            g.drawLine(pen, 0, i, w, i);
        }
        for(i=30; w; 30){
            g.drawLine(pen, i, 0, i, h);
        }
        
        // 连线
        for(i=1; #nodes) {
            var node = nodes[i];
            for(j=1; #node.children) {
                var child = node.children[j];
                g.drawBezier(linePen,
                    node.x, node.y, 
                    (child.x + node.x) / 2, node.y,
                    (child.x + node.x) / 2, child.y,
                    child.x, child.y
                );
            }
        }
        
        // 节点
        for(i=1; #nodes) {
            var node = nodes[i];
            var rect = node.getRect();
            var brush;
            var customBrush = false;
            
            // --- 绘制图形背景 ---
            if(node.color){
                brush = gdip.solidBrush(node.color);
                customBrush = true;
            } else {
                if(node.isMain) brush = mainBrush;
                else brush = childBrushes[((node.level - 1) % #childBrushes) + 1];
            }
            
            if(node.shape == "circle") {
                g.fillEllipse(brush, rect.left, rect.top, rect.width(), rect.height());
                if(node == selectedNode) g.drawEllipse(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            } elseif(node.shape == "rect") {
                g.fillRectangle(brush, rect.left, rect.top, rect.width(), rect.height());
                if(node == selectedNode) g.drawRectangle(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            } elseif(node.shape == "ellipse") {
                g.fillEllipse(brush, rect.left, rect.top, rect.width(), rect.height());
                if(node == selectedNode) g.drawEllipse(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            } elseif(node.shape == "line"){
                g.fillRectangle(gdip.solidBrush(0xFFFEFEFE), rect.left, rect.top, rect.width(), rect.height());
                g.fillRectangle(brush, rect.left, rect.bottom, rect.width(), 3);
                if(node == selectedNode) g.drawRectangle(selectPen, rect.left - 3, rect.top - 3, rect.width() + 6, rect.height() + 6);
            }
            
            if(customBrush && brush) brush.dispose();
            
            // --- 绘制控制点 ---
            if(node == selectedNode){
                var hr = 4;
                g.fillRectangle(handleBrush, rect.left - hr, node.y - hr, hr*2, hr*2);   
                g.fillRectangle(handleBrush, rect.right - hr, node.y - hr, hr*2, hr*2);  
                g.fillRectangle(handleBrush, node.x - hr, rect.top - hr, hr*2, hr*2);    
                g.fillRectangle(handleBrush, node.x - hr, rect.bottom - hr, hr*2, hr*2); 
            }
            
            // --- 绘制文字 ---
            var curTextBrush = textBrush;
            var curFont = node.isMain ? fontRoot : font;
            var customTextBrush = false;
            var customFont = false;
            
            if(node.textColor){
                curTextBrush = gdip.solidBrush(node.textColor);
                customTextBrush = true;
            }
            
            if(node.textSize){
                curFont = family.createFont(node.textSize, 0, 2);
                customFont = true;
            }
            
            g.drawString(node.text, curFont, rect.float(), format, curTextBrush);
            
            if(customTextBrush) curTextBrush.dispose();
            if(customFont) curFont.dispose();
        }
    }
    
    mainForm.canvas.onDrawBackground = function(graphics, rc, backgroundColor, color){
        graphics.clear(0xFFFFFFFF);
    }
    
    mainForm.canvas.onDrawForeground = function(graphics, rc, foregroundRect) {
        drawContent(graphics, rc.width, rc.height);
    }
    
    addMain = function(){
        if(!mainNode) {
            mainNode = Node(mainForm.canvas.clientWidth/4, mainForm.canvas.clientHeight/2, "根节点", true, 0);
            table.push(nodes, mainNode);
            selectedNode = mainNode;
            mainForm.canvas.setFocus();
            mainForm.canvas.redraw();
        } else {
            mainForm.msgbox("主节点已存在!","提示");
        }
    }
    
    addSub = function(){
        var parent = selectedNode or mainNode; 
        if(!parent) return mainForm.msgbox("请先添加主节点!","提示");
        var newLevel = parent.level + 1;
        var newNode = Node(
            parent.x + math.random(90, 150), 
            parent.y + math.random(-90, 90), 
            "节点" ++ (#nodes + 1),
            false,
            newLevel
        );
        newNode.shape = parent.shape;
        table.push(nodes, newNode);
        table.push(parent.children, newNode);
        selectedNode = newNode;
        mainForm.canvas.redraw();
    }
    
    // ----------------- 右键菜单构建 -----------------
    var popmenu = win.ui.popmenu(mainForm);
    
    // 形状子菜单
    var shapeMenu = win.ui.popmenu(mainForm);
    shapeMenu.add("圆形", function(){ if(selectedNode){ selectedNode.shape = "circle"; mainForm.canvas.redraw(); }});
    shapeMenu.add("椭圆", function(){ if(selectedNode){ selectedNode.shape = "ellipse"; mainForm.canvas.redraw(); }});
    shapeMenu.add("矩形", function(){ if(selectedNode){ selectedNode.shape = "rect"; mainForm.canvas.redraw(); }});
    shapeMenu.add("直线", function(){ if(selectedNode){ selectedNode.shape = "line"; selectedNode.textColor = 0xFF000000; mainForm.canvas.redraw(); }});
    
    // 图形颜色子菜单
    var colorMenu = win.ui.popmenu(mainForm);
    var presetColors = {
        {"恢复默认", null}, {"经典红", 0xFFFF4444}, {"自然绿", 0xFF4CAF50}, {"天空蓝", 0xFF2196F3}, {"活力橙", 0xFFFF9800}, 
        {"优雅紫", 0xFF9C27B0}, {"活泼青", 0xFF00BCD4}, {"沉稳棕", 0xFF795548}, {"月光白", 0xFFEEEEFE}, {"酷炫黑", 0xFF333333}
    }
    for(i=1; #presetColors){
        var c = presetColors[i];
        colorMenu.add(c[1], function(){
            if(selectedNode){ selectedNode.color = c[2]; mainForm.canvas.redraw(); }
        });
    }
    
    // 文字颜色子菜单
    var textColorMenu = win.ui.popmenu(mainForm);
    var presetTextColors = {
        {"恢复默认", null}, {"纯白色", 0xFFFFFFFF}, {"纯黑色", 0xFF000000}, {"鲜红色", 0xFFFF0000},
        {"深蓝色", 0xFF0000FF}, {"亮黄色", 0xFFFFFF00}, {"深灰色", 0xFF666666}
    }
    for(i=1; #presetTextColors){
        var tc = presetTextColors[i];
        textColorMenu.add(tc[1], function(){
            if(selectedNode){ selectedNode.textColor = tc[2]; mainForm.canvas.redraw(); }
        });
    }
    
    // 文字大小子菜单
    var textSizeMenu = win.ui.popmenu(mainForm);
    var presetTextSizes = {
        {"恢复默认", null}, {"特小 (10)", 10}, {"小号 (12)", 12}, {"中号 (14)", 14},
        {"大号 (16)", 16}, {"特大 (18)", 18}, {"超大 (24)", 24}, {"极巨 (32)", 32}
    }
    for(i=1; #presetTextSizes){
        var ts = presetTextSizes[i];
        textSizeMenu.add(ts[1], function(){
            if(selectedNode){ selectedNode.textSize = ts[2]; mainForm.canvas.redraw(); }
        });
    }
    
    popmenu.add('添加子节点\tTab', function(){ if(mainNode) addSub(); });
    popmenu.add("重命名", function(){ if(selectedNode) renameNode(selectedNode); });
    popmenu.add(); // 分隔线
    popmenu.add("设置图形形状", shapeMenu);
    popmenu.add("设置图形颜色", colorMenu);
    popmenu.add("设置文字颜色", textColorMenu);
    popmenu.add("设置文字大小", textSizeMenu);
    popmenu.add(); // 分隔线
    popmenu.add("删除节点", function(){ if(selectedNode) deleteNode(selectedNode); });
    popmenu.add('保存\tCtrl+S', function(){ saveToFile(); });
    popmenu.add('打开\tCtrl+O', function(){ loadFromFile(); });
    popmenu.add("导出 PNG", function(){ exportPNG(); });
    popmenu.add("退出", function(){ mainForm.close() });
    // ------------------------------------------------
    
    mainForm.canvas.wndproc = function(hwnd, msg, wParam, lParam){
        if(msg == 0x20/*_WM_SETCURSOR*/){
            var curType = resizingHandleType or hoveredHandleType;
            if(curType){
                var curId = (curType == "horz") ? 32644 : 32645;
                ::SetCursor(::LoadCursor(null, topointer(curId)));
                return true; 
            }
        }
        
        if(msg == 0x205/*_WM_RBUTTONUP*/){
            // 在弹出菜单前,通过位置索引(_MF_BYPOSITION = 0x400)精确打勾
            // 注意:aardio 封装的位置索引是从 1 开始的,所以这里传 1,2,3...
            if(selectedNode){
                // 1. 形状菜单打勾 (位置 1, 2, 3)
                shapeMenu.check(1, selectedNode.shape == "circle", 0x400);
                shapeMenu.check(2, selectedNode.shape == "ellipse", 0x400);
                shapeMenu.check(3, selectedNode.shape == "rect", 0x400);
                shapeMenu.check(4, selectedNode.shape == "line", 0x400);
                
                // 2. 背景颜色打勾 (位置从 1 开始匹配,i 刚好对应 1-based 索引)
                for(i=1; #presetColors){
                    var isChecked = false;
                    if(presetColors[i][2] == null){
                        // 如果菜单项是默认项,只有节点的属性为 null 时才打勾
                        isChecked = (selectedNode.color == null);
                    } else {
                        // 如果菜单项是具体颜色,判断属性不为空且完全匹配时打勾
                        isChecked = (selectedNode.color != null && selectedNode.color == presetColors[i][2]);
                    }
                    colorMenu.check(i, isChecked, 0x400);
                }
                
                // 3. 文字颜色打勾
                for(i=1; #presetTextColors){
                    var isChecked = false;
                    if(presetTextColors[i][2] == null){
                        isChecked = (selectedNode.textColor == null);
                    } else {
                        isChecked = (selectedNode.textColor != null && selectedNode.textColor == presetTextColors[i][2]);
                    }
                    textColorMenu.check(i, isChecked, 0x400);
                }
                
                // 4. 文字大小打勾
                for(i=1; #presetTextSizes){
                    var isChecked = false;
                    if(presetTextSizes[i][2] == null){
                        isChecked = (selectedNode.textSize == null);
                    } else {
                        isChecked = (selectedNode.textSize != null && selectedNode.textSize == presetTextSizes[i][2]);
                    }
                    textSizeMenu.check(i, isChecked, 0x400);
                }
            }
            
            var x, y = win.getCursorPos()  
            popmenu.popup(x, y, true);
            return true; 
        }
        
        if(msg == 0x203/*_WM_LBUTTONDBLCLK*/){
            var x = lParam & 0xFFFF;
            var y = (lParam >> 16) & 0xFFFF;
            for(i=#nodes; 1; -1){
                if(::PtInRect(nodes[i].getRect(), x, y)){
                    renameNode(nodes[i]);
                    break;
                }
            }
            return true;
        }
    }
    
    saveToFile = function(){
        if(!mainNode) return mainForm.msgbox("请先添加主节点!","提示");
        var parentMap = {};
        for(p=1; #nodes){
            for(c=1; #nodes[p].children){
                parentMap[ nodes[p].children[c] ] = p;
            }
        }
        var data = { nodes = {} };
        for(i=1; #nodes){
            var n = nodes[i];
            table.push(data.nodes, {
                id = i;
                parentId = parentMap[n] or 0;
                x = n.x;
                y = n.y;
                text = n.text;
                level = n.level;
                isMain = n.isMain;
                rx = n.rx; 
                ry = n.ry;
                shape = n.shape;
                color = n.color;
                textColor = n.textColor;
                textSize = n.textSize;
            });
        }
        var json = web.json.stringify(data);
        var path = fsys.dlg.save("JSON 文件(*.json)|*.json|所有文件(*.*)|*.*", , winform);
        if(path){
            string.save(path, json);
            mainForm.msgbox("已保存:" ++ path, "提示");
        }
    }
    
    loadFromFile = function(){
        var path = fsys.dlg.open("JSON 文件(*.json)|*.json|所有文件(*.*)|*.*", , winform);
        if(!path) return;
        var json = string.load(path);
        if(!json) return mainForm.msgbox("文件读取失败:" ++ path,"提示");
        var data = web.json.parse(json);
        if(!data || !data.nodes) return mainForm.msgbox("文件格式不正确!","提示");
        nodes = {};
        mainNode = null;
        selectedNode = null;
        var map = {};
        for(i=1; #data.nodes){
            var d = data.nodes[i];
            var n = Node(d.x, d.y, d.text, d.isMain, d.level);
            
            n.rx = d.rx or d.r or n.rx; 
            n.ry = d.ry or d.r or n.ry;
            n.shape = d.shape or "circle";
            n.color = d.color;
            n.textColor = d.textColor; 
            n.textSize = d.textSize;
            
            map[d.id] = n;
            table.push(nodes, n);
            if(d.isMain) mainNode = n;
        }
        for(i=1; #data.nodes){
            var d = data.nodes[i];
            if(d.parentId && d.parentId != 0){
                var parent = map[d.parentId];
                if(parent) table.push(parent.children, map[d.id]);
            }
        }
        selectedNode = mainNode;
        mainForm.canvas.redraw();
        mainForm.msgbox("已打开:" ++ path, "提示");
    }
    
    renameNode = function(node){
        if(!node) return;
        var f = win.form(text="重命名节点"; right=360; bottom=120; border="dialog";max=false;);
        f.add(
            edit={cls="edit"; left=15; top=15; right=345; bottom=42; text=node.text; edge=1;db=1;dl=1;dr=1;dt=1;};
            btnOk={cls="button"; left=160; top=70; right=240; bottom=100; text="确定";dr=1;db=1;};
            btnCancel={cls="button"; left=245; top=70; right=325; bottom=100; text="取消";dr=1;db=1;}
        );
        f.btnOk.oncommand = function(){
            var t = f.edit.text;
            if(#t){ node.text = t; mainForm.canvas.redraw(); }
            f.close();
        };
        f.btnCancel.oncommand = function(){ f.close(); };
        f.doModal(mainForm.hwnd);
    }
    
    deleteNode = function(node){
        if(!node) return;
        if(node.isMain) return mainForm.msgbox("根节点不可删除!", "提示");
        var toRemove = {};
        var stack = {node};
        while(#stack){
            var n = table.pop(stack);
            table.push(toRemove, n);
            for(k=1; #n.children) table.push(stack, n.children[k]);
        }
        for(i=1; #nodes){
            for(j=1; #nodes[i].children){
                if(nodes[i].children[j] === node){ table.remove(nodes[i].children, j); i = #nodes + 1; break; }
            }
        }
        for(i=#nodes; 1; -1){
            for(k=1; #toRemove){
                if(nodes[i] === toRemove[k]){ table.remove(nodes, i); break; }
            }
        }
        for(k=1; #toRemove){
            if(selectedNode === toRemove[k]){ selectedNode = null; break; }
        }
        mainForm.canvas.redraw();
    }
    
    exportPNG = function(){
        if(!mainNode) return mainForm.msgbox("请先添加主节点!", "提示");
        var w = mainForm.canvas.clientWidth;
        var h = mainForm.canvas.clientHeight;
        var scale = 2;  
        var bmp = gdip.bitmap(w * scale, h * scale);
        var g = bmp.getGraphics();
        g.scaleTransform(scale, scale);   
        g.clear(0xFFFFFFFF);
        drawContent(g, w, h);
        g.dispose();
        var path = fsys.dlg.save("PNG 图片(*.png)|*.png|所有文件(*.*)|*.*", , winform);
        if(path){
            var ok, err = bmp.save(path);
            if(ok) mainForm.msgbox("已导出:" ++ path, "完成");
            else mainForm.msgbox("导出失败:" ++ (err or "未知错误"), "错误");
        }
        bmp.dispose();
    }
    
    mainForm.canvas.onMouseDown = function(wParam, lParam) {
        var x, y = win.getMessagePos(lParam);
        draggingNode = null; 
        resizingNode = null; 
        resizingHandleType = null;
        mouseMoved = false;
    
        if(selectedNode) {
            var rect = selectedNode.getRect();
            var hr = 6; 
            var handles = {
                { rc = ::RECT(rect.left - hr, selectedNode.y - hr, rect.left + hr, selectedNode.y + hr); type = "horz" };
                { rc = ::RECT(rect.right - hr, selectedNode.y - hr, rect.right + hr, selectedNode.y + hr); type = "horz" };
                { rc = ::RECT(selectedNode.x - hr, rect.top - hr, selectedNode.x + hr, rect.top + hr); type = "vert" };
                { rc = ::RECT(selectedNode.x - hr, rect.bottom - hr, selectedNode.x + hr, rect.bottom + hr); type = "vert" };
            }
            
            for(i=1; #handles){
                if(::PtInRect(handles[i].rc, x, y)){
                    resizingNode = selectedNode;
                    resizingHandleType = handles[i].type; 
                    win.setCapture(mainForm.canvas.hwnd);
                    return; 
                }
            }
        }
    
        var lastSelected = selectedNode;
        selectedNode = null; 
    
        for(i=#nodes; 1; -1) {
            if(::PtInRect(nodes[i].getRect(), x, y)) {
                selectedNode = nodes[i];
                draggingNode = nodes[i];
                offsetX, offsetY = draggingNode.x - x, draggingNode.y - y;
                win.setCapture(mainForm.canvas.hwnd); 
                break;
            }
        }
        if(lastSelected != selectedNode) mainForm.canvas.redraw();
    }
    
    mainForm.canvas.onMouseMove = function(wParam, lParam) {
        var x, y = win.getMessagePos(lParam);
    
        hoveredHandleType = null;
        if(selectedNode && !resizingNode && !draggingNode) {
            var rect = selectedNode.getRect();
            var hr = 6;
            var handles = {
                { rc = ::RECT(rect.left - hr, selectedNode.y - hr, rect.left + hr, selectedNode.y + hr); type = "horz" };
                { rc = ::RECT(rect.right - hr, selectedNode.y - hr, rect.right + hr, selectedNode.y + hr); type = "horz" };
                { rc = ::RECT(selectedNode.x - hr, rect.top - hr, selectedNode.x + hr, rect.top + hr); type = "vert" };
                { rc = ::RECT(selectedNode.x - hr, rect.bottom - hr, selectedNode.x + hr, rect.bottom + hr); type = "vert" };
            }
            for(i=1; #handles){
                if(::PtInRect(handles[i].rc, x, y)){
                    hoveredHandleType = handles[i].type;
                    break;
                }
            }
        }
    
        if(resizingNode) {
            var dx = math.abs(x - resizingNode.x);
            var dy = math.abs(y - resizingNode.y);
    
            if (resizingNode.shape == "circle") {
                var newR = math.sqrt(dx*dx + dy*dy);
                if(newR < 20) newR = 20; 
                resizingNode.rx = newR;
                resizingNode.ry = newR;
            } else {
                if(resizingHandleType == "horz") {
                    if(dx < 20) dx = 20;
                    resizingNode.rx = dx;
                } else if (resizingHandleType == "vert") {
                    if(dy < 20) dy = 20;
                    resizingNode.ry = dy;
                }
            }
            
            mainForm.canvas.redraw();
        }
        elseif(draggingNode) {
            var newX = x + offsetX;
            var newY = y + offsetY;
            // 以下防止节点移出画布
    		var cw = mainForm.canvas.clientWidth;
    		var ch = mainForm.canvas.clientHeight;
    		var rx = draggingNode.rx;
    		var ry = draggingNode.ry;
    		if(newX < rx) newX = rx;
    		if(newX > cw - rx) newX = cw - rx;
    		if(newY < ry) newY = ry;
    		if(newY > ch - ry) newY = ch - ry;
    		//---------
            var dx = newX - draggingNode.x;
            var dy = newY - draggingNode.y;
            if(dx != 0 || dy != 0) {
                var moveTree;
                moveTree = function(n, deltaX, deltaY){
                    n.x += deltaX;
                    n.y += deltaY;
                    for(k=1; #n.children){
                        moveTree(n.children[k], deltaX, deltaY);
                    }
                }
                
                moveTree(draggingNode, dx, dy);
                mouseMoved = true;            
                mainForm.canvas.redraw();
            }
        } 
        else {
            var lastHover = hoveredIndex;
            hoveredIndex = null;
            for(i=#nodes; 1; -1) {
                if(::PtInRect(nodes[i].getRect(), x, y)) {
                    hoveredIndex = i;
                    break;
                }
            }
            if(hoveredIndex && lastHover != hoveredIndex) {
                var node = nodes[hoveredIndex];
                var r = node.getRect();
                tooltip.setText(node.text ++ '\n层级: ' ++ node.level);
                var sx, sy = win.toScreen(mainForm.canvas.hwnd, r.right + 10, r.top + r.height()/4);
                tooltip.trackPopup(true, sx, sy); 
            } elseif(!hoveredIndex) {
                tooltip.trackPopup(false);
            }
        }
    }
    
    mainForm.canvas.onMouseLeave = function(){
        hoveredIndex = null;
        hoveredHandleType = null;
        tooltip.trackPopup(false);
    }
    
    mainForm.canvas.onMouseUp = function(wParam, lParam) {
        draggingNode = null;
        resizingNode = null; 
        resizingHandleType = null;
        win.releaseCapture();
    }
    
    mainForm.canvas.onKeyUp = function(keyCode,lParam){
        if(keyCode == 0x9/*_VK_TAB*/){
          if(mainNode) addSub();
        }
    }
    
    mainForm.canvas.onKeyDown = function(keyCode, lParam){
        if(::GetAsyncKeyState(0x11) & 0x8000){     
            if(keyCode == 0x53){ saveToFile(); return; }   
            if(keyCode == 0x4F){ loadFromFile(); return; }  
        }
        if(keyCode == 0x2E/*_VK_DELETE*/){
            if(selectedNode) deleteNode(selectedNode);
        }
    }
    
    mainForm.show();
    addMain();    
    
    return win.loopMessage();


  • nanyi 4天前
    0 21
    近我者赤 已实现移动时同步移动子节点:import&nbsp;win.ui; import&nbsp;gdip;&nbsp; import&nbsp;web.json;&am ...
    感觉可以做成一个思维导图的工具了。
返回