中望CAD .NET二次开发从入门到实战:环境搭建、事务与批量操作
2026/9/6 23:35:57 网站建设 项目流程

简介:这份技术文档面向中望CAD二次开发人员,系统讲解基于.NET框架(支持VB.NET或C#)的开发流程。文档覆盖操作环境准备,包括ZWCAD 2021及以上版本、Visual Studio 2015+、.NET Framework 4.7,以及ZwManaged.dll、ZwDatabaseMgd.dll等核心类库的引用配置;随后说明项目创建、添加引用、设置复制到本地属性等关键步骤,并介绍通过NETLOAD加载编译生成的DLL、执行自定义命令的完整过程。还附有C#与VB两版示例代码,演示如何在模型空间创建圆形对象并设置颜色,便于开发者在实际项目中快速上手。资源体积仅307KB,共1个PDF文件,内容精炼但覆盖了从环境搭建到代码调试的核心环节,对需要扩展中望CAD功能的工程技术人员有直接参考价值。目前已有3513人学习使用,适合作为入门与日常查阅的速查手册。

1. 开发环境准备:版本怎么选,工程怎么建

1.1 中望CAD二次开发的底子:.NET、ZRX与API的关系

先说明一个很多人容易绕晕的点。中望CAD的二次开发,并不只有.NET这一条路,它还有ZRX(C++接口)和LISP等入口。但我们这篇主要说.NET方向的开发。中望CAD的.NET API,设计思路上高度兼容AutoCAD的托管API,命名空间从原本的Autodesk.AutoCAD.*换成了ZwSoft.ZwCAD.*,类名、方法名、调用节奏几乎是一一对应的。换句话说,你如果写过AutoCAD的C#插件,迁到中望CAD上,很多时候就是改个命名空间、重新编译的问题。

但“几乎对应”不等于“完全一样”。不同大版本里,有些接口被移动过位置,有些参数类型写法不同,还有些功能中望这边并没有开放出来。所以一个比较稳的认知是:把中望CAD的.NET开发,理解成“一套和AutoCAD API长得很像,但需要按自己版本来适配的独立平台”。这样你就不会一上来拿着AutoCAD的代码硬怼,而是会习惯性地去查对应版本的开发文档。

1.2 按目标CAD版本锁定开发环境

这些年我做下来,最大的坑就是版本匹配问题。中望CAD的.NET插件,本质上是一个托管DLL,由CAD进程在运行时加载。你的DLL编译目标是多少位的、用的哪个.NET运行时版本,必须和CAD自身的运行时匹配,否则加载时直接炸掉。

操作上,我建议按下面这个顺序来确定环境:

  1. 先确认你要兼容的中望CAD版本号。不同版本对应的.NET Framework版本要求不一样,比如比较旧一点的版本可能最高支持.NET Framework 4.0,新版可能要4.6以上甚至4.8。这一点务必打开中望CAD官方帮助文档,查“系统需求”或“.NET API 开发环境”章节。

  2. 按CAD版本选Visual Studio。一般用Visual Studio 2017到2022都行,关键是项目目标框架要选对。我的习惯是:项目属性里的“目标框架”选.NET Framework 4.7.24.8,这两个版本在Win10/Win11上都是自带或可装的状态,兼容性最稳。

  3. 平台目标务必设置成x64。现在主流中望CAD都是64位进程,如果你的类库编译成了AnyCPUx86,在本机调试时可能侥幸能跑,换一台机器就提示“未能加载文件或程序集”,排查起来非常浪费时间。

注意:中望CAD安装目录下,一般会自带开发用的DLL,比如ZwSoft.ZwCAD.dllZwSoft.ZwCAD.DatabaseServices.dll这些托管程序集。引用时直接浏览到安装目录去选DLL,不要手动去别的地方复制。这样能保证和你本机安装的版本严格一致。

1.3 创建第一个插件工程:类库 + 命令类

中望CAD的.NET插件,就是一个普通的C#类库项目。它不是EXE,不需要Main函数,入口全靠带有特性的命令方法。

步骤大致是这样:

  1. 新建一个“类库(.NET Framework)”项目,语言选C#。

  2. 项目名称随意,但我建议用有意义的命名,比如ZwToolkitBatchTools之类,方便后面打包和管理。

  3. 引用中望CAD的托管DLL。在解决方案资源管理器里,右键“引用”,选“添加引用”,再点“浏览”,去中望CAD安装目录下把ZwSoft.ZwCAD.dllZwSoft.ZwCAD.DatabaseServices.dllZwSoft.ZwCAD.Geometry.dllZwSoft.ZwCAD.EditorInput.dll这几个常用程序集加进来。如果目标版本更细,也可以看官方开发包里的Demo是怎么引用的,照着来。

  4. 把这几个DLL的“复制本地”属性,设置成False。这一点很关键,否则生成的目录里多出一堆几十上百兆的CAD运行时文件,看着就不对劲,而且容易把自己搞懵。

  5. 创建一个命令类,写上CommandMethod特性。这是插件注册命令的入口,和AutoCAD的用法一致。

using System; using ZwSoft.ZwCAD.ApplicationServices; using ZwSoft.ZwCAD.EditorInput; using ZwSoft.ZwCAD.Runtime; namespace ZwToolkit { public class Commands { [CommandMethod("HELLO_ZW")] public void HelloZw() { Document doc = Application.DocumentManager.MdiActiveDocument; Editor ed = doc.Editor; ed.WriteMessage("\nHello, 中望CAD .NET 插件已加载!"); } } }

这里HELLO_ZW就是你在CAD命令行里敲的命令名。编译成功后会生成一个DLL,在CAD里用NETLOAD命令把它选中加载,再输入HELLO_ZW,就会在命令行看到输出。跑到这一步,你的整个开发链路就算是通了。

2. 先从对象模型说起:不把Database和Transaction搞懂,后面寸步难行

2.1 “文档-数据库-块表-实体”这条主线

中望CAD底层的数据组织和AutoCAD一样,是个典型的四层结构:Document(文档)里面有一个Database(数据库),数据库里有块表BlockTable,块表里有块表记录BlockTableRecord,块表记录里装着具体的图形实体,比如Line、Circle、Text这些。

画图的时候,你屏幕上看到的东西,其实都存在数据库里。而我们做二次开发,绝大部分操作就是往数据库里写入实体、从数据库里读取实体、或者遍历数据库改实体属性。理解这条主线之后,很多API就好猜了,无非就是“通过Document拿到Database,通过Database拿到BlockTable,再通过BlockTable拿到ModelSpace——也就是模型空间那块块表记录,然后往里面Add实体”。

这就像你要往一个图书馆里放书:先找到图书馆,再找到对应的书架区,再找到一个具体的书架,然后把书摆上去。每一步都有固定的“路径”,一次走错,后面全乱。

2.2 为什么要用事务(Transaction)

很多刚接触的人会问:我往模型空间里加一个圆,直接Add不就好了,为什么非要套一个事务?

这里涉及到CAD数据库一个很重要的设计:任何对数据库内容的修改,都需要放在事务里统一管理。事务可以简单理解成一个“操作记录本”。你在这个本子里写下“我要画一条直线”,数据库先不真正提交,等你把本子合上(Commit),它才一次性把所有改动写进去。如果中途任何一步出错,可以整个回滚(Abort),数据库不会留下半截数据。

这样做的好处是:第一,安全,不会因为代码跑到一半崩溃导致图纸损坏;第二,性能,批量修改时能减少数据库内部的频繁通知和刷新;第三,可恢复,出错了能回到操作前状态。

所以一个标准的“往模型空间加实体”的代码骨架,几乎固定是这样的:

using (Transaction trans = db.TransactionManager.StartTransaction()) { BlockTable bt = trans.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable; BlockTableRecord btr = trans.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord; // 创建实体、设置属性、追加到块表记录 btr.AppendEntity(entity); trans.AddNewlyCreatedDBObject(entity, true); trans.Commit(); }

注意,事务一定要用using包裹,或者确保最后走到Commit()Abort()。如果买彩票一样忘了Commit,数据不会出现在图纸上,而且连接没释放,后面操作会越来越卡。

2.3 常用入口类速查

开发时高频用到的几个类,我整理了下:

作用常用获取方式
Application应用级入口,拿文档管理器用静态类,直接用
Document当前文档,能取到Editor和DatabaseApplication.DocumentManager.MdiActiveDocument
Editor命令行交互,写提示、取点、取关键字doc.Editor
Database当前图纸数据库,所有实体的家doc.Database
BlockTable块表,管理图纸里所有块定义事务中GetObject(db.BlockTableId)
BlockTableRecord块表记录,模型空间、图纸空间、块定义数据都在这事务中从BlockTable取
Transaction事务,修改数据库的统一入口db.TransactionManager.StartTransaction()

这些类不需要死记,但最好画一张“从Document能摸到什么”的关系图放脑子里。实际写代码的时候遇到不认识的类型,猜也猜得八九不离十。

3. 实战:从零写一个批量加圆工具

3.1 需求想清楚再动手

说太多理论容易飘,直接拿一个实际工具来走一遍全流程。

需求描述:在图纸上批量生成圆,圆的数量可以由用户输入,圆心位置直接鼠标在屏幕上点取,每个圆半径固定为50。这个功能看起来很简单,但涵盖了“命令行交互、输入参数校验、事务操作、创建实体、提交数据库”五个最基本环节。等你把这个工具理顺了,后面做更复杂的功能,骨架也是这套。

我先把功能拆成几个动作:

  1. 提示用户输入要生成几个圆。
  2. 提示用户指定第一个圆心位置。
  3. 提示用户指定第二个圆心位置、第三个……直到数量满足。
  4. 每点到一处,立刻在模型空间生成一个半径为50的圆。
  5. 全部结束后,在命令行提示统计信息。

3.2 关键代码拆开讲

先看完整代码,我再分段说明。

[CommandMethod("BATCH_CIRCLE")] public void BatchCircle() { Document doc = Application.DocumentManager.MdiActiveDocument; Database db = doc.Database; Editor ed = doc.Editor; // 第一步:接收数量 PromptIntegerOptions countOpt = new PromptIntegerOptions("\n请输入要生成的圆数量:"); countOpt.AllowNone = false; countOpt.LowerLimit = 1; countOpt.UpperLimit = 10000; PromptIntegerResult countRes = ed.GetInteger(countOpt); if (countRes.Status != PromptStatus.OK) return; int count = countRes.Value; // 第二步:循环取点并画圆 using (Transaction trans = db.TransactionManager.StartTransaction()) { BlockTable bt = trans.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable; BlockTableRecord btr = trans.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord; int added = 0; for (int i = 0; i < count; i++) { PromptPointOptions ptOpt = new PromptPointOptions($"\n请指定第{i + 1}个圆心位置:"); PromptPointResult ptRes = ed.GetPoint(ptOpt); if (ptRes.Status != PromptStatus.OK) break; using (Circle circle = new Circle()) { circle.Center = ptRes.Value.TransformBy(ed.CurrentUserCoordinateSystem); circle.Radius = 50; btr.AppendEntity(circle); trans.AddNewlyCreatedDBObject(circle, true); } added++; } trans.Commit(); ed.WriteMessage($"\n已生成 {added} 个圆。"); } }

下面这段是重点,你要理解每一句在干什么。

获取交互输入PromptIntegerOptions用来配置“请输入数字”这个交互提示。AllowNone = false表示用户不能直接回车跳过,LowerLimitUpperLimit是范围约束。这里特别说一下,CAD命令行交互和Web开发完全不一样,用户的每一次输入,都会返回一个PromptXxxResult,里面有个Status字段。你不用像写表单那样抛异常去校验,而是判断Status是不是PromptStatus.OK,不是就直接return,因为用户可能按了ESC取消。这个习惯要养成,不然按ESC时程序会直接报错。

创建圆实体CircleCenter是圆心点,Radius是半径。这里有个容易忽略的细节:取点拿到的是UCS(用户坐标系)坐标,而实体在数据库里存的是WCS(世界坐标系)坐标。如果当前图纸没动过坐标系,两者一致,你不会有感觉;可一旦用户把坐标系旋转过,直接赋值就会让圆跑偏。稳妥做法是调用TransformBy(ed.CurrentUserCoordinateSystem)把UCS坐标转换成WCS坐标。这个坑我在AutoCAD时代踩过一次,后来老实用TransformBy,从不出错。

用using包住实体对象。你可能会好奇,实体加到数据库里了,为什么还要using?因为如果后续代码出错了,事务中途异常退出,这个实体对象还没有被数据库接管,不及时释放会发生对象句柄泄漏。中望CAD的API文档建议,自己new出来的、还没被数据库对象完全管理的对象,最好用using兜底。等Commit()之后,数据库会自己接管实体生命周期,你就别再手动去碰它了。

3.3 加载调试的正确姿势

项目编译好后,调试方式有两种常见路子。

一种是直接在Visual Studio里按F5,把中望CAD设为启动外部程序。右键项目,属性,“调试”选项卡里,把“启动外部程序”设为D:\Program Files\ZWSOFT\ZWCAD 2024\ZWCAD.exe。按F5时,Visual Studio会启动CAD并自动附加调试器,代码里下断点就能像调试普通程序一样调试插件。这种体验最好,我日常开发主要用这种方式。

另一种是手动在CAD里NETLOAD加载DLL,再用Debug菜单的“附加到进程”,选中ZWCAD.exe进程。这个方式适合排查已安装插件的运行时问题,因为是手动加载,部署环境更接近用户现场。

调试时,建议把“异常设置”里的“Common Language Runtime Exceptions”勾上。这样代码里只要扔出异常,Visual Studio立刻中断,能直接看到堆栈。不然异常被CAD内部吞掉,你只会在命令行看到一段含糊的报错信息,排查效率低得多。

4. 踩坑记录:加载失败、事务问题与性能优化

4.1 加载失败十有八九是版本问题

NETLOAD命令加载DLL报“无法加载”或“System.BadImageFormatException”时,我基本不动脑子先查三件事:

  1. 目标平台是不是x64。前面提过,CAD是64位进程,DLL编译成32位或AnyCPU在运行时都可能出问题,直接把平台设为x64最干净。
  2. 目标框架版本是不是高于CAD实际支持的版本。比如CAD内嵌的是.NET Framework 4.6,你编译成4.8,加载时大概率报“方法找不到”或“架构不匹配”这类错误。解法是调低目标框架,或者升级CAD版本。
  3. DLL依赖的托管程序集版本和安装目录下是否一致。如果你引用的ZwSoft.ZwCAD.dll是从A版本拷贝的,但运行环境是B版本,有时候能跑,有时候不能跑,属于典型的“本机没问题,换台机就炸”。

4.2 事务里最容易犯的几个错

我在带新人时,发现事务相关的错误是最多的,而且报错往往不直观。整理几个高发的:

错误一:读取和写入模式搞混。事务里用GetObject打开对象时,第二个参数要传OpenMode.ForRead还是OpenMode.ForWrite。如果你只是读取实体坐标,用ForRead;如果你要修改实体属性,必须用ForWrite。用ForRead打开想要改写的对象,会在Write操作时抛eLockViolation异常。反过来,用ForWrite打开发送只读对象虽不报错,但会降低并发性能,不建议养成这习惯。

错误二:事务嵌套不当。中望CAD支持事务嵌套,但子事务的操作,只有外层事务提交时才真正生效。如果你在里面开了一个事务,又没提交就return,数据就丢了。我的建议是:单个操作尽量只开一层事务;如果实在要套多层,一定要理清哪个事务负责Commit、哪个负责Abort,别指望外层帮你兜底。

错误三:忘记处理用户取消。用户随时可能按ESC,你写的循环如果没判断PromptPointResult.Status,下一次循环时ptRes.Value是空坐标,直接创建实体就会出现无法预料的空引用异常或者画错位置。务必每次交互后立刻判断状态。

4.3 大批量操作时的性能优化思路

做二次开发,功能正确只能算第一步,大批量操作时性能才是分水岭。有次我同事写了个转图工具,处理一万个实体,耗时能快能慢差出20多倍,最后定位到问题出在循环里频繁调用Document.Editor.WriteMessage去刷进度条。命令行输出本身不是主要瓶颈,但每次输出都触发UI同步,量一上去就扛不住了。

几个真实有效的优化点:

  1. DocumentEditorDatabase提到循环外。有些代码会在循环里反复Application.DocumentManager.MdiActiveDocument,这个属性每次访问都要走COM交互,性能损耗不可忽略。循环开始前先取好引用,循环里直接用。

  2. 批量创建实体时,尽量只开一个事务。事务是重量级操作,每开一次都要做一堆资源分配和状态登记。批量添加一万个实体,拆成一万个事务,性能会惨不忍睹。正确做法是:循环外开一个事务,循环里追加实体,循环结束统一Commit。

  3. 避免循环里无意义的花哨输出。实时进度用百分比的形式很好,但最好每100个或1000个输出一次,而不是每一次都输出。终端刷屏本身也会拖慢速度。

  4. 对象用完后,该Dispose就Dispose。对新建但没加入数据库的临时实体,用using包裹;对从数据库GetObject拿到的对象,不需要手动Dispose,事务结束会自动处理。别乱调对象的Dispose()方法,引用还在的事务对象被提前释放,后面再访问它,异常会让你怀疑人生。

4.4 兼容性:AutoCAD代码迁移时的自查清单

如果你是从AutoCAD .NET开发转过来的,我最后给一份自查清单,按顺序核对完,大部分代码能顺利迁移:

  • 命名空间:Autodesk.AutoCAD.*改成ZwSoft.ZwCAD.*,这一步是全局替换,但注意别误伤其他第三方库的引用。
  • 程序集引用:删掉AutoCAD的dll引用,添加中望CAD安装目录下对应的托管dll。
  • CommandMethod特性:命名空间不一样,但用法一致,确认using ZwSoft.ZwCAD.Runtime;
  • 事务结构:基本原样可跑。
  • 事件机制:中望CAD的DocumentManager.DocumentCreated这些事件都有,名称基本一致,但触发细节可能略有差异,建议调试时看事件是否被正确触发。
  • 平台差异:个别中望CAD版本对某些系统API的支持不如AutoCAD完善,比如高版本中望CAD支持3D实体的能力一般,涉及复杂几何布尔运算时要特别测试。

根据我个人经验,真正让人头疼的往往不是API本身,而是“你以为一样,实际上有细节差异”的那部分。比如某个系统的字体映射行为、打印样式表的处理顺序,这一类问题,没有捷径,只能在真实CAD环境里多跑多测。

最后再分享一个小技巧:开发时最好在电脑上同时装两个版本的中望CAD,一个做主力开发,一个做兼容性验证。代码在主力版本上跑通之后,马上到另一个版本上重新编译加载测试一遍。不少看起来能在2023版上正常运行的功能,到2024版反而因为某个接口废弃而编不过。趁早发现,省得到交付的时候手忙脚乱。

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

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

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

立即咨询