尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

中望CAD netload加载dll插件:配置驱动动态菜单实现指南

中望CAD netload加载dll插件:配置驱动动态菜单实现指南 简介针对中望CAD二次开发场景的DLL插件工程包面向需要扩展CAD功能、自定义菜单界面的开发者和工程设计师。工程演示了通过netload命令加载C#编写的动态库并依据外部配置动态生成菜单的全过程适合将常用工具集成到中望CAD工作台、提升设计效率的实际需求。压缩包为7z格式共48个文件约5MB包含13个已编译的DLL、9个C#源码、sln/csproj工程文件、PDB调试符号以及XML/TXT配置与用户反馈文本结构清晰可直接用Visual Studio打开。已有807人学习/下载。资源提供完整可编译的类库项目重点展示配置菜单的读取与构建逻辑结合调试文件可断点跟踪理解netload插件的注册与调用机制附带的反馈记录和崩溃日志对排查实际部署问题也有参考价值覆盖从环境搭建、DLL编写到注册调试的常见环节。1. 中望cad的netload加载dll插件把变动交还给配置CAD里变动频率最高的不是绘图功能而是菜单栏放哪些入口、按钮触发什么命令。菜单需求按周变插件发版按季度算中间隔着改代码、编包、分发、验证。我把菜单做成配置驱动是维护阶段省事的第一步中望CAD用netload加载dll插件程序集启动时去读XML配置文件把里面的菜单项和命令名映射成界面上真实可点的下拉菜单。这样一来“增加一个菜单项”退化成“在XML里加一行”不用重启CAD改完刷新菜单即可。这条路径适合两类人做设计工具链维护的二次开发工程师以及替技术部统一管理CAD环境的信息化人员。前者关心少发版后者关心用户提的需求当天能不能交付。下面从.NET程序集的加载机制讲起一直讲到配置文件变化后如何自动刷新菜单。2. netload加载dll插件之前先弄清中望CAD的.NET程序集加载方式2.1 netload加载dll插件认的是.NET程序集netload加载的是.NET Framework程序集不是传统意义上的原生dll。中望CAD同时提供ARX/SDS这类C接口但netload这条链路只面向托管世界。一个能被netload识别的dll内部至少要有一个入口类加载成功后中望CAD会反射这个类调用它的Initialize方法执行netunload卸载时对应调用Terminate。也就是说netload给出的是程序集生命周期钩子而不是菜单生成器菜单逻辑要自己写在Initialize触发后的执行链里。using ZwSoft.ZwCad.Runtime; namespace ZcadDynamicMenu { public class Entry : IExtensionApplication { public void Initialize() { // netload 加载完成后触发 // 不在这里建菜单文档和菜单系统此时未必就绪 } public void Terminate() { // netunload 卸载前触发用来移除动态菜单 } } }Initialize不是渲染菜单的最佳时机。命令行执行netload时图形环境经常还没完成窗口初始化这时候去操作菜单集合轻则找不到目标对象重则把CAD弄成假死。我一般只在Initialize里挂事件把真正的菜单构建推迟到空闲事件里做。提示Initialize里应尽量避免耗时的反射扫描和网络请求。netload加载如果卡在初始化阶段命令行会长时间失去响应而且没有进度提示用户只能重启CAD。2.2 为什么是dll插件而不是菜单宏、LISP、ARX先看四种常见方案的维护成本对比再解释为什么netload这条链路最适合配置驱动菜单。方案动态读取配置能力界面定制成本发版成本典型使用场景菜单宏cuix/mns弱宏基本是静态的低低但改菜单要重新分发界面文件按钮固定、命令固定的老环境LISP脚本能读文本和XML窗体体验弱中低轻量自动化、批量处理ARX/SDSC强高上手门槛高高版本适配工作量大核心图形算法、大数据量运算.NET dllnetload强XmlSerializer/Json均可中WinForm交互成熟低编译一次长期复用菜单、面板、命令批量注册菜单宏的静态性源于它本质是界面配置不是程序外部配置的读取能力几乎为零。LISP倒是能读文本文件但涉及多级弹窗、按钮图标、命令联动时写起来不如C#顺手而且LISP代码的版本差异比.NET API更乱。ARX是性能上限最高的方案为了菜单和配置文件这点需求出动C属于过度投入后续每换一个CAD年度版都够喝一壶。.NET dll是四者里开发和维护最平衡的程序负责把XML变成菜单业务改动只动XML程序集保持不变。这也是netload在中望CAD坐标系里区别于其他加载方式的定位程序与配置分离dll只做翻译层。2.3 建dll插件工程时的3个前置参数创建类库工程时有3个参数在开工前就要定好省得后期在加载阶段反复踩坑。目标框架优先对齐CAD进程内置的.NET运行时。64位中望CAD各年度版常见的是.NET Framework 4.6到4.8选.NET 5以上目标框架netload直接加载不了。平台目标设为x64。AnyCPU在64位CAD里多数情况能跑但一旦项目里混入引用本机组件进程位数变成32位时就会抛BadImageFormatException事先锁定x64最省心。引用Copy Local对ZwSoft.ZwCad.dll这类CAD自带程序集Copy Local设为false。强制复制进输出目录反而可能造成版本错配运行时优先加载CAD安装目录里的那个程序集以安装目录自带的为准。工程里引用中望CAD托管接口时dll就在CAD安装目录下不同年度版的小版本有差异不要从外部随意下载同名程序集塞进工程强名称对不上的情况经常发生。上面这3个参数确认完才开始写配置读取和菜单生成顺序反了会浪费很多调试时间。3. 动态读取配置菜单的dll插件实现从XML配置到命令行命令3.1 配置文件先定结构菜单名、菜单项、命令名三要素配置文件我选XML理由是.NET Framework自带XmlSerializer不用为JSON再引入第三方依赖。中望CAD二次开发环境里减少外部包依赖是长期维护的第一原则配置内容本身也只是菜单树XML的冗余度完全可以接受。?xml version1.0 encodingutf-8 ? menu name生产工具 item name导入工单 commandIMPORT_ORDER tip从MES读取当日工单 / item name批量打印 commandBATCH_PLOT tip按布局批量输出PDF / separator / item name重新读取配置 commandMENU_RELOAD tip不重启CAD刷新菜单 / /menu配置文件的字段含义如下表。字段必填含义menu/name是菜单栏上显示的顶层菜单名item/name是菜单项显示文字item/command是点击后要执行的CAD命令名item/tip否鼠标悬停提示separator否菜单分隔线占位配置文件放在与dll插件相同的目录路径用程序集位置反推不硬编码。硬编码D盘绝对路径在换机器、换用户目录时会突然找不到配置反推方式至少保证“dll在哪里配置就在哪里”部署单元只有一个文件夹。3.2 用C#实现配置读取与菜单生成数据载体类直接映射XML结构。XmlAttribute映射字段XmlElement映射item节点这样XML里写commandC#里对应Command属性序列化器会自动完成匹配。using System; using System.Collections.Generic; using System.Xml.Serialization; namespace ZcadDynamicMenu { [Serializable] public class MenuConfig { [XmlAttribute(name)] public string Name { get; set; } [XmlElement(item)] public ListMenuItemConfig Items { get; set; } } [Serializable] public class MenuItemConfig { [XmlAttribute(name)] public string Name { get; set; } [XmlAttribute(command)] public string Command { get; set; } [XmlAttribute(tip)] public string Tip { get; set; } } }这里有个容易忽略的点XmlSerializer对大小写敏感XML里是commandC#属性里写成CommandName序列化不会报错但值会是null界面上显示成空标题。出现这类问题时优先检查XML属性名与类的映射而不是改界面代码。配置读取类放在同一个程序集里方法也很短using System; using System.IO; using System.Reflection; using System.Windows.Forms; using System.Xml.Serialization; namespace ZcadDynamicMenu { public class ConfigLoader { public static string GetConfigDirectory() { // 用程序集位置反推配置所在目录 string asmPath Assembly.GetExecutingAssembly().Location; return Path.GetDirectoryName(asmPath); } public static MenuConfig Load() { string path Path.Combine(GetConfigDirectory(), MenuConfig.xml); if (!File.Exists(path)) { MessageBox.Show(未找到 MenuConfig.xml path); return null; } try { var serializer new XmlSerializer(typeof(MenuConfig)); using (var fs File.OpenRead(path)) { return (MenuConfig)serializer.Deserialize(fs); } } catch (InvalidOperationException ex) { // 捕获反序列化内部异常保留现场用于定位 MessageBox.Show(菜单配置解析失败 ex.InnerException?.Message ?? ex.Message); return null; } } } }返回null而不是抛异常是为了让上层菜单构建有一个明确的空分支配置坏了CAD里的旧菜单还能继续用而不是整个插件起不来。MessageBox在这里是合适应对话术比命令行提示更直观。菜单生成的核心是把配置对象转成界面对象先查顶层菜单组里有没有同名菜单有就删掉再重新添加。之所以要先删后建是因为连续加载两次dll后会在菜单栏累积出两个“生产工具”删除逻辑必须放在每次重建的最前面。using System.Linq; using ZwSoft.ZwCad.Application; using ZwApp ZwSoft.ZwCad.Application.Application; namespace ZcadDynamicMenu { public class MenuBuilder { private const string MacroPrefix ^C^C; public static void Apply(MenuConfig config) { if (config null) return; var app (ZwApp)ZwApp.AcadApplication; // 不同年度版暴露的集合类型略有差异这里按 MenuGroups 示例 var menuGroup app.MenuGroups[0]; // 删除同名旧菜单防止重复加载后菜单堆叠 var old menuGroup.Menus.Castobject() .FirstOrDefault(m GetMenuName(m) config.Name); if (old ! null) menuGroup.Menus.Remove(old); // 按配置重建菜单宏前缀保证菜单项点击时先取消当前命令 var topMenu menuGroup.Menus.Add(config.Name); foreach (var item in config.Items) { topMenu.Menus.Add(item.Name, MacroPrefix item.Command, item.Tip); } } private static string GetMenuName(object menu) { // 中望CAD不同版本的菜单对象用 Name/Title 取菜单名此处做适配 return menu.GetType().GetProperty(Name)?.GetValue(menu)?.ToString() ?? menu.GetType().GetProperty(Title)?.GetValue(menu)?.ToString(); } } }这里用反射适配Name和Title两种写法是因为我维护的CAD版本菜单对象取名字段不一致。实际工程里通常写两套代码分别编译比运行时反射更干净反射方案适合配置驱动的通用分发场景。宏前缀^C^C是CAD通用的命令取消控制符菜单项点击时先取消当前命令再执行目标命令避免嵌套命令状态下命令串线。3.3 从Initialize到菜单生成的事件链入口类与命令注册收尾。Initialize里只注册一次性Idle事件首次空闲时构建菜单。Idle事件触发一次就移除防止后续每次空闲都重建菜单。using System; using ZwSoft.ZwCad.Application; using ZwSoft.ZwCad.Runtime; namespace ZcadDynamicMenu { public class Entry : IExtensionApplication { public void Initialize() { Application.Idle OnFirstIdle; } private void OnFirstIdle(object sender, EventArgs e) { Application.Idle - OnFirstIdle; MenuBuilder.Apply(ConfigLoader.Load()); } public void Terminate() { // 卸载时移除动态菜单避免下一次加载重复 } } }命令注册用CommandMethod特性MENU_RELOAD是给用户手工刷新用的自定义命令MES导入、批量打印这些业务命令也按同样方式注册在同一个程序集里。配置里的command字段填的就是这些命令名两边对应关系维护在XML里程序不打补丁。4. 实测踩坑netload报错、程序集找不到、菜单重复生成的应对4.1 命令行里加载dll插件的执行顺序和前提netload后拉起的不是命令行参数是文件选择框。实际操作分两步命令行输入netload回车在弹出的文件选择框里选中编译好的dll。中望CAD的FILEDIA系统变量控制文件对话框行为脚本自动化时可以把它设为0改用脚本内直接给路径的方式但路径有空格时要处理引号。手动操作我不关FILEDIA直接从文件框选更直观。加载顺序是netload触发程序集加载反射入口类调用InitializeInitialize注册Idle事件空闲时执行菜单构建。过程中没有任何“安装”语义dll文件被CAD进程锁定。要覆盖dll重新编译得先netunload卸载或者干脆重启中望CAD否则会提示文件被占用。4.2 netload加载dll插件的常见报错与排查我维护这套方案期间遇到过的典型问题整理成一张排查表。现象可能原因排查方向netload后命令行提示无法加载文件或程序集目标框架高于CAD内置.NET运行时查CAD对应版本调低目标框架重新编译弹BadImageFormatExceptiondll平台目标与CAD进程位数不一致平台目标锁x64重新生成配置解析报InvalidOperationException内部提示XML错误XML字段名与C#映射不一致逐字段核对XmlAttribute/Element映射netload成功但菜单没出现Idle事件时机过晚或配置文件路径不对确认配置在dll同目录检查入口是否注册事件加载两次后菜单重复重复构建没有先删旧菜单Apply前先按名称移除旧菜单第三行看起来是运行时报错实际是配置问题。XmlSerializer解析失败时外层异常信息包裹在InvalidOperationException里真实原因在InnerException中调试时先看这一层。提示netload命令本身不带版本验证加载一个为旧版中望CAD编译的dll通常也能成功但访问新版独有API时会运行抛异常。切换CAD版本后先跑一遍MENU_RELOAD看命令行有没有异常回显。4.3 菜单重复与命令残留先删后建的标准手法菜单重复的根因是Add之前没查重Fix做法是每次Apply开始时按菜单名搜索已存在的顶级菜单找到后先Remove再重新Add。第3.2节代码里的删除逻辑已经包含这个动作这里补充一个关键点Remove之后要释放菜单对象引用不要用局部变量缓存旧对象再操作否则CAD的菜单集合可能出现清理不彻底表现为菜单消失但快捷键仍然可用。命令残留集中在netunload场景。动态菜单通过宏间接调用命令命令本体由CommandMethod特性注册netunload时中望CAD会自动回收该程序集的命令注册但如果你用了手动RegisterCommand这类显式注册方式就要在Terminate里手工注销然后再移除菜单。这个顺序不要反先注销命令后菜单还在指向它点击时就变成“未知命令”。5. 中望cad配置菜单热更新dll插件里加一道自动刷新5.1 改配置后执行MENU_RELOAD旧菜单不闪失手工刷新路径是命令行执行MENU_RELOAD对应的处理函数直接复用MenuBuilder.Apply。这里有一个被我反复强调的细节配置解析失败时不能中断刷新流程也不能清掉旧菜单。正确行为是保留上一次成功构建的界面只在提示框里给出具体解析错误等用户修好配置再手动刷一次。这个容错逻辑放在ConfigLoader.Load返回null的空分支里就已经成立ReloadMenu只负责调用using System; using ZwSoft.ZwCad.Runtime; namespace ZcadDynamicMenu { public static class ReloadCommand { [CommandMethod(MENU_RELOAD)] public static void Reload() { var config ConfigLoader.Load(); if (config null) return; MenuBuilder.Apply(config); } } }5.2 用FileSystemWatcher监听配置文件变化把手工命令也省掉单机部署时可以把MENU_RELOAD这一条手工操作也去掉。FileSystemWatcher监视MenuConfig.xml的LastWrite和Size变化写入事件触发后延迟600毫秒去重再回到UI线程执行Reload。延迟去重是因为编辑器保存时经常连续触发多次Change事件600毫秒能滤掉同一动作产生的重复通知。using System; using System.IO; using ZwSoft.ZwCad.Application; public static class ConfigWatcher { private static FileSystemWatcher _watcher; private static DateTime _lastChange DateTime.MinValue; public static void Start() { string dir ConfigLoader.GetConfigDirectory(); _watcher new FileSystemWatcher(dir, MenuConfig.xml) { NotifyFilter NotifyFilters.LastWrite | NotifyFilters.Size, EnableRaisingEvents true }; _watcher.Changed OnConfigChanged; } private static void OnConfigChanged(object sender, FileSystemEventArgs e) { // 去重保存动作可能触发多次事件600ms内的后续事件忽略 if ((DateTime.Now - _lastChange).TotalMilliseconds 600) return; _lastChange DateTime.Now; // 回到UI线程刷新避免在文件系统线程操作菜单对象 Application.Idle OnIdleReload; } private static void OnIdleReload(object sender, EventArgs e) { Application.Idle - OnIdleReload; ReloadCommand.Reload(); } }文件系统线程回到UI线程的这段不能省菜单集合是CAD主线程的对象直接在工作线程里操作会跨线程访问轻则不刷新重则触发未处理异常。文件监听适合单机或小部门共享只读目录不推荐放到多人同时写入的共享盘上Change事件在高频写入下可能连续触发十几轮600毫秒去重也拦不住。要多级子菜单的话把MenuConfig.Items改成菜单项与子菜单的递归结构separator加个类型标记解析归属一层层下探。初次上线的验证路径不复杂改一行XML等两秒看菜单栏是否跟着变化。本文还有配套的精品资源点击获取
返回列表