☰
中望CAD二次开发:netload加载DLL实现动态菜单配置
2026/10/22 3:16:25 网站建设 项目流程

简介:这份资源面向中望CAD二次开发人员与.NET插件开发者,聚焦通过netload命令加载DLL插件并动态读取配置菜单这一实用场景,帮助解决CAD功能扩展、自定义界面与项目适配问题。压缩包共48个文件,约5MB,以13个dll动态库、9个cs源码文件为主,辅以pdb调试符号、cache缓存、sln与csproj工程文件及Ribbon.xml配置等,构成一套可直接参考的完整插件工程。资源围绕DLL项目搭建、接口函数编写、XML配置解析、菜单动态生成、插件注册与netload加载测试等环节展开,读者可从中获取可复用的工程结构、Ribbon菜单配置思路与调试排错经验。目前已有809人学习下载,适合具备C#或VB.NET基础、希望掌握中望CAD插件开发技能的中高级开发者参考借鉴。

1. 中望CAD里用 netload 加载 DLL:动态菜单为什么值得折腾

如果你正在用中望CAD做二次开发,大概率遇到过这个场景:插件功能写好了,编译出一个 DLL,但每次改菜单结构、加一条命令、调一个参数,都得重新编译、重新 netload,甚至重启 CAD。更麻烦的是,交付给同事或客户时,对方的环境路径、菜单项、命令别名都不一样,你不可能为每个人单独出一个 DLL。这时候,“用 netload 加载 DLL 插件,并让插件动态读取配置菜单”就成了一个很实际的需求。

它的核心思路并不复杂:DLL 只负责“读配置、建菜单、绑命令”,菜单长什么样、有哪些项、对应哪个命令,全部放在外部配置文件里。改菜单不用重编译,改配置就行。适合两类人:一是做内部工具集、需要频繁调整菜单的开发者;二是要把插件交付到不同环境、希望一套 DLL 适配多套菜单的交付方。下面按“能复现”的标准,把这条路走一遍。

2. 先搞清楚 netload 能加载什么、动态菜单靠什么撑起来

2.1 netload 加载的是程序集,不是脚本

中望CAD 的NETLOAD命令本质是让 CAD 进程加载一个 .NET 程序集(DLL),然后从里面找实现了特定接口的类并执行。它和加载 LISP、VBA 不一样,不是解释执行,而是把 DLL 挂进当前进程。常见做法是:DLL 里定义一个入口类,实现IExtensionApplication接口,CAD 在 netload 之后会调用Initialize()方法。菜单的创建、配置的读取,都放在这个方法里。

这里有个容易翻车的点:NETLOAD加载的 DLL 必须和当前 CAD 进程的 .NET 运行时版本匹配。中望CAD 不同版本对 .NET Framework 的要求不同,常见的是 4.7.2 或 4.8。如果你编译时用的是 .NET 6/7/8,加载会直接失败,报“无法加载程序集”之类的错。所以第一步不是写代码,而是确认目标 CAD 版本对应的 .NET 版本,并在项目里锁死。

2.2 动态菜单的三种落地方式

“动态读取配置菜单”听起来很玄,实际落地就三种常见做法:

第一种,配置文件驱动菜单结构。用一个 JSON 或 XML 文件描述菜单树,DLL 启动时读文件、遍历节点、调用 CAD 的菜单 API 逐条创建。改菜单只改文件,DLL 不动。这是最推荐的方式,灵活且容易排查。

第二种,约定式扫描。DLL 扫描自身程序集里所有带自定义特性的命令方法,按特性里的分组信息自动生成菜单。这种方式不用外部配置,但改菜单等于改代码,违背了“动态”的初衷,只适合菜单极其稳定的场景。

第三种,混合式。菜单骨架写在配置里,具体命令的绑定关系通过反射从 DLL 里找。配置只负责“显示什么、顺序如何”,命令实现仍在代码里。这种方式在交付场景里比较平衡。

我一般会选第一种,因为排查问题时可以明确区分“配置错了”还是“代码错了”。下面也按第一种展开。

2.3 配置文件该放哪、怎么读

配置文件的位置不能写死。常见做法是放在 DLL 同目录下,或者放在一个由环境变量指定的目录里。读取时用Assembly.GetExecutingAssembly().Location拿到 DLL 路径,再拼出配置文件路径。这样无论 DLL 被 netload 到哪个目录,都能找到同级的配置。

配置格式推荐 JSON,因为 .NET 里解析方便,而且结构清晰。一个典型的菜单配置长这样:

{ "menus": [ { "title": "绘图工具", "items": [ { "title": "批量偏移", "command": "BATCHOFFSET", "enabled": true }, { "title": "快速标注", "command": "QUICKDIM", "enabled": true } ] }, { "title": "图层工具", "items": [ { "title": "图层归并", "command": "MERGELAYER", "enabled": false } ] } ] }

title是菜单显示名,command是 CAD 命令名,enabled控制是否可用。DLL 读取后,按这个结构创建菜单。注意command必须是已经在 CAD 里注册过的命令,否则点了没反应。

2.4 菜单 API 的调用顺序有讲究

中望CAD 的 .NET API 里,菜单相关操作通常通过Autodesk.AutoCAD.ApplicationServices或中望对应的命名空间下的MenuBar、Menu、MenuItem类完成。创建顺序必须是:先拿到主菜单栏,再创建顶级菜单,再往顶级菜单里加子项。顺序反了会抛异常。

一个常见的坑是:在Initialize()里创建菜单时,CAD 的主窗口可能还没完全初始化。这时候直接操作菜单栏,可能拿到 null。稳妥做法是用Application.Idle事件延迟一次执行,或者判断Application.MainWindow是否可用。这个细节后面避坑章节会展开。

3. 从零写一个能 netload 的动态菜单 DLL

3.1 项目配置:目标框架和引用

新建一个 .NET Framework 类库项目,目标框架选 4.7.2 或 4.8(按你的 CAD 版本定)。需要引用的程序集通常包括:

  • acdbmgd.dll
  • acmgd.dll
  • 中望CAD 对应的 .NET API 程序集(名称随版本不同,常见的是ZwSoft.ZwCAD.dll或类似)

这些 DLL 在 CAD 安装目录下能找到。引用时把“复制到本地”设为 false,避免和 CAD 自带的冲突。

<!-- 项目文件里锁定目标框架,避免运行时版本不匹配 --> <TargetFrameworkVersion>v4.8</TargetFrameworkVersion> <Reference Include="acdbmgd"> <HintPath>$(CADPath)\acdbmgd.dll</HintPath> <Private>false</Private> </Reference>

Private=false很关键。如果设成 true,编译时会把 CAD 的程序集复制到输出目录,netload 时可能加载到两份不同版本的同一程序集,直接崩。

3.2 入口类:实现 IExtensionApplication

DLL 被 netload 后,CAD 会找实现了IExtensionApplication的类。这个类必须有无参构造函数,且Initialize()和Terminate()都要实现。

using System; using ZwSoft.ZwCAD.Runtime; // 中望CAD 的运行时命名空间,按实际版本调整 public class MenuLoader : IExtensionApplication { public void Initialize() { // 延迟到 CAD 空闲时再建菜单,避免主窗口未就绪 Application.Idle += OnIdle; } private void OnIdle(object sender, EventArgs e) { Application.Idle -= OnIdle; // 只执行一次 try { var configPath = GetConfigPath(); var config = MenuConfig.Load(configPath); MenuBuilder.Build(config); } catch (Exception ex) { // 写日志,不要弹窗打断用户 Logger.Error("菜单初始化失败", ex); } } public void Terminate() { // 清理菜单,避免重复 netload 时菜单叠加 MenuBuilder.Cleanup(); } private string GetConfigPath() { var dllPath = System.Reflection.Assembly.GetExecutingAssembly().Location; var dir = System.IO.Path.GetDirectoryName(dllPath); return System.IO.Path.Combine(dir, "menu.config.json"); } }

Initialize()里没有直接建菜单,而是挂到Application.Idle上。这是血泪经验:直接建菜单在部分版本里会拿到 null 的菜单栏。Terminate()里做清理,是因为反复 netload 同一个 DLL 时,如果不清理,菜单会重复添加,越点越多。

3.3 读配置:JSON 解析与容错

配置读取要处理三种情况:文件不存在、JSON 格式错、字段缺失。任何一种都不应该让 CAD 崩。

using System.IO; using System.Text.Json; // 或 Newtonsoft.Json,按项目已有依赖选 public class MenuConfig { public MenuNode[] Menus { get; set; } public static MenuConfig Load(string path) { if (!File.Exists(path)) { // 文件不存在时返回空配置,不抛异常 return new MenuConfig { Menus = new MenuNode[0] }; } var json = File.ReadAllText(path); try { return JsonSerializer.Deserialize<MenuConfig>(json); } catch (JsonException ex) { Logger.Error($"配置文件格式错误: {path}", ex); return new MenuConfig { Menus = new MenuNode[0] }; } } } public class MenuNode { public string Title { get; set; } public MenuItemNode[] Items { get; set; } } public class MenuItemNode { public string Title { get; set; } public string Command { get; set; } public bool Enabled { get; set; } = true; }

Enabled默认给 true,避免配置里漏写导致菜单项全灰。Menus为空时也不报错,只是不建菜单。这样即使配置写错,CAD 本身还能正常用,不会因为一个插件把整个环境搞崩。

3.4 建菜单:遍历配置并绑定命令

建菜单的核心是遍历Menus,每个顶级节点创建一个菜单,每个子项创建一个菜单项,并把Command绑定到点击事件。

using ZwSoft.ZwCAD.ApplicationServices; public static class MenuBuilder { private static readonly List<MenuItem> _created = new List<MenuItem>(); public static void Build(MenuConfig config) { var menuBar = Application.MenuBar; if (menuBar == null) return; foreach (var menuNode in config.Menus) { var menu = menuBar.Add(menuNode.Title); foreach (var item in menuNode.Items) { var menuItem = menu.Add(item.Title); menuItem.Enabled = item.Enabled; var cmd = item.Command; // 闭包捕获,避免循环变量问题 menuItem.Click += (s, e) => SendCommand(cmd); _created.Add(menuItem); } } } private static void SendCommand(string command) { if (string.IsNullOrWhiteSpace(command)) return; // 通过命令行方式触发已注册命令 Application.DocumentManager.MdiActiveDocument? .SendStringToExecute(command + "\n", true, false, false); } public static void Cleanup() { foreach (var item in _created) { item.Remove(); } _created.Clear(); } }

SendStringToExecute是把命令当字符串发给命令行执行。注意命令名后面要加换行符,否则 CAD 会等你按回车。第三个参数控制是否回显,一般设 false 避免命令行刷屏。

Cleanup()在Terminate()里调用,把本次创建的菜单项移除。如果不做,反复 netload 会看到菜单越来越多,最后分不清哪个是哪个。

3.5 命令注册:让配置里的 command 真正可用

配置里的command必须是 CAD 认识的名字。有两种方式让它可用:一是用CommandMethod特性在 DLL 里注册,二是通过 LISP 或其他方式预先注册。推荐第一种,因为和 DLL 一起交付,不依赖外部文件。

public class Commands { [CommandMethod("BATCHOFFSET")] public void BatchOffset() { var doc = Application.DocumentManager.MdiActiveDocument; doc?.Editor.WriteMessage("\n批量偏移命令已触发"); // 实际业务逻辑 } [CommandMethod("QUICKDIM")] public void QuickDim() { // 快速标注逻辑 } }

CommandMethod里的名字就是配置里command要填的值。大小写不敏感,但建议统一大写,避免配置里写错大小写导致找不到命令。

4. 避坑:netload 动态菜单最常见的五个翻车点

4.1 现象:netload 报“无法加载程序集”,原因:目标框架不匹配

这是最高频的问题。DLL 编译时用的 .NET 版本和 CAD 进程的不一致,netload 直接拒绝。解决方法是查清楚目标 CAD 版本要求的 .NET Framework 版本,在项目属性里锁死。不要用 .NET Standard 或 .NET Core 去编译,CAD 的 netload 只认 .NET Framework。

4.2 现象:菜单创建了但点击没反应,原因:命令未注册或命令名拼错

配置里的command和CommandMethod里的名字必须完全对应。常见错误是配置里写了小写,代码里是大写,或者配置里多了一个空格。排查方法是先在 CAD 命令行手动输入配置里的命令名,看能不能执行。不能执行就是命令注册的问题,和菜单无关。

4.3 现象:反复 netload 后菜单重复,原因:Terminate 没清理或清理不彻底

Terminate()里必须把本次创建的菜单项移除。如果只移除顶级菜单,子项可能还挂在菜单栏上。稳妥做法是记录所有创建过的菜单项,逐个Remove()。另外,如果 DLL 被强制卸载(比如 CAD 崩溃),清理不会执行,下次启动时可能残留。可以在Initialize()里先做一次清理,再建新菜单。

4.4 现象:配置文件改了但菜单没变,原因:DLL 缓存或配置路径不对

netload 之后,DLL 被加载进进程,改配置文件不会自动刷新。需要重新 netload 或者提供一个“重载配置”的命令。另外,配置路径如果用的是相对路径,可能因为 CAD 的工作目录不同而找不到文件。用Assembly.Location拼绝对路径最稳。

4.5 现象:菜单项显示为灰色不可点,原因:Enabled 属性被覆盖或命令未就绪

Enabled设了 true 但菜单还是灰的,常见原因是命令所在的类还没被 CAD 扫描到。CommandMethod的注册是在程序集加载时完成的,如果菜单创建早于命令注册,就会灰。解决办法是把菜单创建延迟到Idle事件里,确保命令已经注册。另外,如果配置里enabled字段缺失,默认值要给 true,不要给 false。

5. 进阶:让配置菜单支持热重载和权限分级

5.1 热重载:不重新 netload 也能刷新菜单

前面提到改配置要重新 netload,实际交付时这很不方便。可以在 DLL 里注册一个RELOADMENU命令,执行时先Cleanup()再重新Build()。这样用户改完配置,在 CAD 里敲一下命令就能看到新菜单。

[CommandMethod("RELOADMENU")] public void ReloadMenu() { MenuBuilder.Cleanup(); var config = MenuConfig.Load(GetConfigPath()); MenuBuilder.Build(config); Application.DocumentManager.MdiActiveDocument? .Editor.WriteMessage("\n菜单已重载"); }

这个命令本身也可以放在配置里,形成一个“菜单里有一项叫重载菜单”的自举结构。注意Cleanup()和Build()之间不要有异步操作,否则可能出现菜单栏短暂为空。

5.2 权限分级:按用户角色显示不同菜单项

配置里可以加一个role字段,DLL 读取当前用户的环境变量或登录信息,过滤掉不匹配的菜单项。这样一套 DLL 可以同时服务普通用户和管理员,管理员看到更多工具。

{ "title": "高级工具", "command": "ADVTOOL", "enabled": true, "role": "admin" }

读取时判断role是否为空或是否匹配当前角色,不匹配就跳过。角色信息可以从环境变量读,也可以从一个单独的本地文件读。不要硬编码在 DLL 里,否则改权限又要重编译。

5.3 验证方法:用日志确认菜单构建过程

动态菜单出问题时,最难的是不知道哪一步断了。建议在Build()的每个关键节点写日志:配置加载了几项、创建了几个菜单、绑定了几个命令。日志写到 DLL 同目录下的menu.log,出问题时直接看文件。

Logger.Info($"配置加载完成,顶级菜单 {config.Menus.Length} 个"); Logger.Info($"菜单项创建完成,共 {_created.Count} 项");

日志不要用弹窗,弹窗会打断用户操作,而且 CAD 里弹窗多了很烦。写文件最安静,排查时也方便。

5.4 一个我常犯的错:忘了处理空配置

早期版本里,配置加载失败时我直接return,结果菜单栏是空的,用户以为插件没加载。后来改成:配置为空时也建一个顶级菜单,里面放一项“配置未加载,请检查 menu.config.json”。这样用户至少知道插件在工作,只是配置有问题。这个习惯一直保留到现在,比静默失败强得多。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询