最近在研究使用 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();-
-
稍微改了下代码,优化了一下拖动效果:
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(); -
-
-
大师,我想弄个鼠标悬停到节点是出现 信息提示,但是坐标不知道如何获取,信息显示到窗体外面了,请帮忙看看哈

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(); -
这个帖子得顶一下!很实用! 光神和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作为底座,继续完善这个《思维导图》程序💪💪💪
-
-
-
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();
-
-
-
-
可以参考这个帖子:[gdi] 绘制矩形和椭圆,并实现鼠标缩放、拖动
-
-
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(); -

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(); -
@近我者赤 ,👍👍👍! 越改越完善了,继续加油!!
建议:
第[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();
-
-
已实现移动时同步移动子节点:
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(); -







