简介:一份基于.NET C#的CAD二次开发源码包,面向ZWCAD与AutoCAD,可实现按需求自动绘制图块、批量生成图形对象,有效减轻人工绘图的重复操作与出错率。该程序系作者在实际工作场景中实践开发,已完成基本功能测试,具备较好的稳定性。压缩包共68个文件,总大小2.83MB,核心内容以C#窗体源码(.cs文件)为主,配合编译生成的exe/dll程序集、界面资源(.resx)及Visual Studio解决方案文件(.sln)等,项目结构完整,方便直接打开调试。源码完全开放,读者可基于自身业务进行定制化二次开发,例如修改图块尺寸、增加新的绘图命令或对接其他数据源。已有6793人学习下载,对有CAD绘图提效需求或正在学习C#二次开发的工程师而言,是一份贴近实战的参考资料。 做工业自动化或者机械设计的朋友,应该都有过这种经历:CAD里一堆重复性的绘图工作,框选、偏移、修剪、标注,一天下来眼都快花了。我之前做设备和产线图纸的时候,也被这种重复劳动折磨得不轻,后来干脆用C#对CAD做了一轮二次开发,把批量出图这种活儿直接扔给程序去跑。这篇文章就把我在.NET C#环境下做CAD二次开发、实现自动绘图功能的过程整理一下,从环境搭建、基础框架到自动绘图的核心代码,再到我踩过的那些坑,一次性说清楚。
这篇文章适合两类人看:一类是刚接触CAD二次开发、想用C#做自动绘图但不知道怎么入门的同学;另一类是已经写过一些LISP或VBA脚本,想往更正规的.NET方向迁移的工程师。我会尽量少讲理论废话,多给能直接复制的代码和思路。整个方案的核心是:用Visual Studio + AutoCAD .NET API,通过编写托管DLL的方式,让CAD在启动后加载我们的程序,然后通过自定义命令自动完成绘图任务。下面进入正题。
1. 为什么选.NET C#做CAD二次开发
1.1 主流的CAD二次开发方式对比
先说结论:AutoCAD二次开发目前主流有四种方式,AutoLISP/Visual LISP、VBA、ObjectARX(C++)和.NET API。很多老工程师习惯用AutoLISP写小工具,因为语法简单、加载快,几行代码就能改个图元属性。VBA在早期版本里也很流行,但Autodesk后来对VBA的支持越来越弱,现在的版本默认都不带VBA模块了。ObjectARX性能最强,能直接操作CAD内核,但开发门槛高,一个简单的功能可能就要写一堆C++代码,调试起来也很痛苦。
.NET API是这几个方案里综合性价比最高的:它本质上是Autodesk对ObjectARX封装出来的一套托管接口,C#写起来比C++顺手得多,语法上又比LISP严谨,适合做稍微复杂一点的工程化功能。更关键的是,你写的DLL可以直接被CAD用NETLOAD命令加载,不需要安装额外运行时,分发部署非常干净。我选C#还有一层原因:同一个团队还要做上位机、数据处理这些工具,C#一套技能能通吃,维护成本低。
1.2 .NET API的版本兼容逻辑
这里有一个必须提前搞清楚的点:AutoCAD .NET API和调用它的程序集版本、.NET Framework版本是强绑定的。AutoCAD 2014开始引入了AcCoreMgd.dll,2015版之后基本稳定;到了AutoCAD 2020,官方要求的.NET Framework版本是4.8。也就是说,如果你用VS 2022创建项目,选择.NET Framework 4.8,目标平台为AnyCPU或x64,然后引用对应CAD安装目录下的AcMgd.dll、AcDbMgd.dll、AcCoreMgd.dll,基本就能覆盖AutoCAD 2020到2025这批主流版本。
如果你的现场还在用AutoCAD 2016到2019,那.NET Framework用4.7同样能跑。最新版CAD对.NET Core的支持还处于过渡期,不建议在正式项目里赌未来,稳定优先,老老实实用.NET Framework。我自己习惯的策略是:开发机上装一个高版本CAD(比如2024),但代码里只调用低版本就有的API,这样生成的DLL在2016-2025之间都能通用,部署的时候省很多事。
1.3 我能用什么方式加载和运行
加载方式有两种,一种是CAD启动时通过注册表或启动组自动加载,另一种是手动在CAD命令行输入NETLOAD加载dll文件。调试阶段我建议手动NETLOAD,改完代码重新编译再加载,不用每次重启CAD。到了交付阶段,可以把dll路径写进启动组,或者做一个安装包,在CAD启动时自动引用,用户打开CAD就能直接用自定义命令,完全感觉不到背后有个程序在运行。后面这些实操细节,我会在第3部分详细展开。
2. 环境准备:一步步把开发环境搭起来
2.1 安装和引用的版本匹配
开发环境的核心就三步:装Visual Studio、装AutoCAD、建项目引用dll。VS推荐2019或2022社区版,免费的够用。AutoCAD用你目标现场的主版本,比如2020以上。建项目的时候,注意选择“类库(.NET Framework)”,不要选“类库(.NET Core)”,否则加载进CAD会报找不到程序集。
引用路径一般在CAD安装目录下,比如:C:\Program Files\Autodesk\AutoCAD 2024\AcMgd.dll、AcDbMgd.dll、AcCoreMgd.dll。这三个dll是.NET API的核心库:AcMgd管应用程序和文档,AcDbMgd管数据库和实体,AcCoreMgd里面是命令定义、编辑器交互这些基础服务。三个都要引用,缺一个编译能过,运行时报错。
引用添加完之后,记得在项目里把这三个dll的“复制本地”属性设为False。如果不设,编译输出目录会多出几十MB的dll,而且加载时可能因为版本冲突导致CAD崩溃。
2.2 第一个能跑起来的命令框架
在项目里新建一个类,加一个静态方法,方法上面用[CommandMethod("HELLO")]标注,这就是注册给CAD的命令。代码如下:
using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Runtime; namespace CadAutoDraw { public class Commands { [CommandMethod("HELLO")] public static void Hello() { Document doc = Application.DocumentManager.MdiActiveDocument; Editor ed = doc.Editor; ed.WriteMessage("\nHello from C# CAD 二次开发!"); } } }编译成功后,在CAD命令行输入NETLOAD,定位到bin目录下的dll文件,然后输入HELLO命令,如果命令行打印出了那行字,恭喜你,环境已经通了。这就是整个二次开发的最小闭环:编译dll -> 加载dll -> 命令触发。
2.3 设置调试时自动启动CAD
上面的流程手动操作没问题,但开发阶段每次改代码都手动NETLOAD很烦。这里教你一个偷懒的办法:在VS里右键项目 -> 属性 -> 调试 -> 启动外部程序,选到acad.exe路径。这样你按F5,VS会自动拉起CAD并附加上调试器,你可以在C#代码里打断点,直接在CAD里看变量值、堆栈信息,排查问题效率提升一倍。
有一点要提醒:调试模式下CAD启动后,要先用NETLOAD加载你编译出来的dll(路径一般在项目bin\Debug下),再执行自定义命令,断点才会命中。VS的调试器附加的是整个CAD进程,dll加载后代码才会进入断点范围。
3. 自动绘图的核心原理和示例代码
3.1 事务处理:CAD数据库写操作的基本单位
自动绘图的本质,就是往CAD的数据库(Database)里添加图元(Entity),比如直线、圆、多段线。这个数据库操作不是直接写入就能完事的,必须包在一个“事务”(Transaction)里。我用一个生活化的类比:CAD的数据库就像一本账本,你要往里面记一笔账,不能直接拿笔在页面中间乱画,得先打开账本、找到当前页、用笔写下内容、然后合上账本,整个过程要么全部成功,要么全部不写。
在.NET API里,这个流程对应如下:
using (Transaction tr = doc.Database.TransactionManager.StartTransaction()) { BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable; BlockTableRecord btr = tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite) as BlockTableRecord; // 创建实体并添加到 btr tr.Commit(); }这里有两个关键点:第一,块表记录(BlockTableRecord)对应的是模型空间或图纸空间的“当前页”;第二,写完一定要调用Commit(),否则事务回滚,白画了。很多新手漏掉Commit,折腾半天发现图没画出来,基本都是这个原因。
3.2 坐标系统和单位:为什么画出来的东西位置不对
做CAD开发离不开坐标系统。CAD里有世界坐标系(WCS)、用户坐标系(UCS)、显示坐标系(DCS),还有图纸空间坐标系(PSDCS)。我们代码里用Point3d创建实体,默认是在WCS下工作。但用户执行命令时,如果他切了UCS,你程序里返回的点坐标可能是UCS下的值,直接用会导致图元位置错乱。
解决思路很直观:要么在绘制前强制把当前坐标系切回WCS(不推荐,会影响用户现场视图),要么用Editor.CurrentUserCoordinateSystem把用户输入点转换到WCS里。我习惯的做法是写一个转换工具函数:
private static Point3d ToWcs(Point3d pt) { Editor ed = Application.DocumentManager.MdiActiveDocument.Editor; CoordinateSystem3d ucs = ed.CurrentUserCoordinateSystem; return pt.TransformBy(Matrix3d.AlignCoordinateSystem( Point3d.Origin, Vector3d.XAxis, Vector3d.YAxis, Vector3d.ZAxis, ucs.Origin, ucs.Xaxis, ucs.Yaxis, ucs.Zaxis)); }至于单位,CAD默认图纸单位是毫米还是英寸,取决于模板文件(DWT)。.NET API内部所有坐标都以“图形单位”存储,你从用户输入或界面拿到的值不会是带单位的,它就是图形单位。所以自动绘图时,生成大批坐标点,你首先要和图纸约定好单位,跟客户对齐,否则尺寸会差25.4倍这种低级错误,可太常见了。
3.3 实际画三个常用图元:直线、圆、多段线
理解了事务和坐标,画图就是往当前块表记录里Add实体的问题。我直接给出三个最常用的示例代码,包含直线、圆和带圆弧的多段线:
[CommandMethod("DRAWSAMPLE")] public static void DrawSample() { Document doc = Application.DocumentManager.MdiActiveDocument; Database db = doc.Database; Editor ed = doc.Editor; using (Transaction tr = db.TransactionManager.StartTransaction()) { BlockTableRecord btr = tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite) as BlockTableRecord; // 1. 直线:从 (0,0) 到 (100,50) Line line = new Line(new Point3d(0, 0, 0), new Point3d(100, 50, 0)); btr.AppendEntity(line); tr.AddNewlyCreatedDBObject(line, true); // 2. 圆:圆心 (50,50),半径 30 Circle circle = new Circle(new Point3d(50, 50, 0), Vector3d.ZAxis, 30); btr.AppendEntity(circle); tr.AddNewlyCreatedDBObject(circle, true); // 3. 多段线:从 (0,0) 到 (200,0),再画圆弧回到起点形成闭合图形 Polyline pline = new Polyline(); pline.AddVertexAt(0, new Point2d(0, 0), 0, 0, 0); pline.AddVertexAt(1, new Point2d(200, 0), 0, 0, 0); pline.SetBulgeAt(1, -1); // 凸度值 -1 表示半圆圆弧 pline.Closed = true; btr.AppendEntity(pline); tr.AddNewlyCreatedDBObject(pline, true); tr.Commit(); } ed.WriteMessage("\n示例图形已生成: 直线、圆、闭合半圆多段线"); }AddNewlyCreatedDBObject(entity, true)这行很多人会忽略,但其实很关键:实体被AppendEntity加入块表记录后,还需要通过事务管理器把它标记为“新增对象”,事务提交时才会真正写进数据库。漏掉这一行,你运行命令虽然不报错,但图里什么都没有。
3.4 多段线凸度(Bulge)的数学原理
上面代码里pline.SetBulgeAt(1, -1),Bulge是CAD多段线里一个很特别的概念,很多新手在这里翻车。凸度表示顶点到下一个顶点之间圆弧的弯曲程度,等于圆心角四分之一的正切值。如果凸度是0,说明这一段是直线;凸度绝对值越大,圆弧越陡;正负号决定圆弧往哪边弯,逆时针为正,顺时针为负。
比如,我想从(0,0)到(200,0)画一个经过点(100,100)的凸圆弧,它的凸度值是tan(圆心角/4),圆心角90度,所以凸度是tan(22.5°) = 0.41421356。这个值不需要你背公式,但你要懂得怎么反向推算,因为很多从Excel或外部数据接口读取的数据,给的可能是半径、圆心、起点终点,你要先把这些几何参数换算成凸度才能塞进多段线里,不然画出来的圆弧方向完全不对。
4. 让自动绘图更“能用”的关键设计
4.1 先分层再画图:线型、颜色、图层的工程化处理
直接往模型空间扔一堆图元,满足演示没问题,但放到真实项目里会被同事骂死,因为图面完全失控。工程上自动绘图的第一步永远是建图层、设颜色、定线型。
下面这个函数可以帮你保证“图层存在且属性正确”:
public static ObjectId CreateLayer(Database db, string layerName, short colorIndex, string linetype = "Continuous") { using (Transaction tr = db.TransactionManager.StartTransaction()) { LayerTable lt = tr.GetObject(db.LayerTableId, OpenMode.ForRead) as LayerTable; if (lt.Has(layerName)) { tr.Commit(); return lt[layerName]; } tr.Commit(); } using (Transaction tr = db.TransactionManager.StartTransaction()) { LayerTable lt = tr.GetObject(db.LayerTableId, OpenMode.ForWrite) as LayerTable; LayerTableRecord ltr = new LayerTableRecord(); ltr.Name = layerName; ltr.Color = Color.FromColorIndex(ColorMethod.ByAci, colorIndex); ltr.LinetypeObjectId = db.GetLinetype(linetype); lt.Add(ltr); tr.AddNewlyCreatedDBObject(ltr, true); tr.Commit(); return ltr.Id; } }注意两个细节:线型必须先存在于数据库中,如果图纸里没有DASHED这个线型,赋给图层的LinetypeObjectId会是无效的。所以要么先用db.LoadLineTypeFile("DASHED", "acad.lin")加载,要么通过db.GetLinetype确认存在性;图层名称尽量统一用标准前缀,比如“S_设备区”“T_标注区”,方便图面管理,也方便后续按图层批量选择。
4.2 命令交互:让用户点选插入点或选对象
自动绘图不意味着死板地一把梭,更多时候你需要给用户一个选择机会。比如批量插入设备符号时,用户希望先在图纸上点一个位置,程序再根据这个位置做阵列或偏移。用Editor类的GetPoint就能实现点选交互:
[CommandMethod("DRAWATCLICK")] public static void DrawAtClick() { Document doc = Application.DocumentManager.MdiActiveDocument; Editor ed = doc.Editor; PromptPointOptions ppo = new PromptPointOptions("\n请指定插入点: "); PromptPointResult ppr = ed.GetPoint(ppo); if (ppr.Status != PromptStatus.OK) { ed.WriteMessage("\n用户取消操作"); return; } Point3d insertPt = ppr.Value; // 这里将 insertPt 传入绘图函数即可 }如果你是做上位机开发的,这里的设计思路和串口/网口通讯里的“事件触发”很像:程序不主动猜用户意图,而是以用户输入作为触发条件。再进一步,你可以把扫码枪扫码的结果转成坐标或参数,然后触发自动绘图,这个在产线数据采集和图纸可视化场景里非常实用,本质上就是一个“外部数据 -> CAD绘图”的管道。
4.3 批量绘图:不要让命令一条条执行
自动绘图最大的价值在于批量。比如你有上千个坐标点,要一次性全部画成圆或块。最简单的错误做法是在CAD命令行循环调用DRAWCIRCLE命令,这样每执行一次命令都要做一次事务、刷新一次屏幕,性能惨不忍睹。
正确做法是:一个事务里循环创建实体,最后提交一次。
using (Transaction tr = db.TransactionManager.StartTransaction()) { BlockTableRecord btr = tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite) as BlockTableRecord; foreach (var pt in pointList) { DBPoint dbPoint = new DBPoint(new Point3d(pt.X, pt.Y, 0)); btr.AppendEntity(dbPoint); tr.AddNewlyCreatedDBObject(dbPoint, true); } tr.Commit(); }一次性创建几千上万个对象都在一个事务里完成,速度非常快。如果再配合图层关闭、锁定等操作,把视觉更新降到最低,性能还能再提升一截。实测下来,一万个点绘制基本能做到秒级完成。这里有个经验:不要每画一个实体就调一次doc.Editor.Update()或doc.Editor.Regen(),那会让事务边界被强制刷新画面,性能会退化几十倍。
4.4 插入块参照:从模板文件复制图框和符号
如果你要自动往图纸里插入图框、标准件符号,用“块”(Block)是最高效的方式。块的定义可以提前放在模板DWT中,程序只需要在目标数据库里找到块定义,然后创建BlockReference插入到模型空间。
读取DWT里的块定义需要一点技巧:要用Database templateDb = new Database(false, true); templateDb.ReadDwgFile(templatePath, FileOpenMode.OpenForReadAndAllShare, false, "");先打开模板文件,然后把块表记录拖拽到目标数据库中(用IdMapping做对象克隆)。完成之后,再通过BlockReference插入块引用:
BlockReference bref = new BlockReference(insertPt, blockTableRecordId); btr.AppendEntity(bref); tr.AddNewlyCreatedDBObject(bref, true);这套做法适用于设备布置图、工艺流程图里那种大量重复的符号插入。你在模板文件里把符号画好,程序只管“摆位置”,图面质量和绘制效率都远高于代码一行行画几何线条。
5. 常见问题与排查技巧实录
5.1 问题速查表
我在实际项目中遇到过不少问题,先列个表格,后面挑几个典型的展开说。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| NETLOAD后命令找不到 | dll没有编译成.NET Framework | 检查项目目标框架,重新生成dll |
| 运行命令报“拒绝访问/没有权限” | 程序访问了注册表或系统目录、dll在受保护路径 | 以管理员身份运行CAD;dll放用户目录或项目目录 |
| 运行命令没反应,图也没变化 | 没有调用Commit,或AddNewlyCreatedDBObject缺失 | 检查事务是否提交,实体是否被追加到BlockTableRecord |
| 图纸上的文字显示成“???” | 样式字体缺失或编码问题 | 统一字体样式,确保代码写入字符串编码正确,文件保存为UTF-8 with BOM |
| 画出来的圆/弧方向反了 | 凸度(Bulge)正负号反了 | 用正负号规则判断圆弧方向,逆时针为正 |
| CAD 2016加载dll提示“api-ms-win-crt-runtime-l1-1-0.dll丢失” | 系统缺少VC++运行库 | 安装Windows对应版Universal CRT补丁、或用sysinternals检查系统dll |
| CAD打开或安装过程报0x80070005 | 系统目录或注册表权限不足 | 以管理员方式安装;关闭杀毒软件误拦截;修复.NET Framework |
5.2 权限错误0x80070005这一类的问题排查
这个错误在Win10系统上装.NET Framework 3.5或CAD相关组件时特别常见,错误本身的意思是“拒绝访问”,本质上就是当前进程没有权限去访问注册表或C:\Windows\System32下的系统文件。
我的排查顺序是:先确认Windows Installer服务是否正常启动,再用管理员权限运行命令提示符,执行dism /online /enable-feature /featurename:NetFx3 /All或sfc /scannow修复系统文件。如果还不行,检查杀毒软件是否把CAD或VS的安装进程拦住了。很多CAD加载dll时报的权限问题,其实真正原因是目标dll放在了类似C:\Program Files下,而CAD进程没有管理员权限去动态加载它。解决办法很简单:把dll放到用户的Application Data目录或项目自身的bin目录下,再执行NETLOAD。
5.3 CAD画直线显示2.1616e+这种数字是什么原因
这个我有发言权,因为被问过起码三次。CAD里输入直线命令后,命令行提示显示“2.1616e+...”,这不是程序问题,也不是二次开发特有的bug,纯粹是数字精度显示问题。CAD默认的线性精度(LUPREC变量)可能比较低,导致坐标或长度用科学计数法显示,看着像报错。
解决方式很简单:在CAD命令行输入UNITS,把线性精度位数调高,或者直接在代码里设置Application.SetSystemVariable("LUPREC", 8)。这样绘图过程中命令行显示的点坐标、长度数据就不再出现e+这种鬼样子了。做二次开发时,很多用户看到这种显示就以为是程序画错位置,其实是显示精度在捣乱。
5.4 一个容易忽略的性能陷阱:循环里反复启动事务
我见过不少初学二次开发的朋友,代码逻辑清晰,但画一万个点要等半天。原因就是循环体内部每次都StartTransaction加Commit,相当于CAD要不断把内存数据同步到显示系统,性能自然崩了。
正确的思路是尽量让一个事务处理一批同类操作。事务开得越大,提交次数越少,速度越快。但也有一个边界:比如在一个事务里创建几十万个实体,事务提交前数据一直占用内存,提交时也会有一次明显的卡顿。所以我会按5000-10000个实体一批的方式切分事务,既能保证速度,又不会因为一次提交太多数据导致内存暴涨或崩溃。
5.5 跨CAD版本部署的兼容性避坑
最后说一个部署层面的经验。很多人在自己电脑上开发调试没问题,拷到现场电脑就报错,多半是版本不匹配。有三种常见情况:一是现场CAD版本比自己开发版本低,代码里调用了高版本API;二是现场电脑没有安装对应的.NET Framework运行时;三是dll引用了其他第三方库,而现场没有一起copy。
我的建议是:开发时尽可能以低版本CAD作为主目标,比如用2020做开发测试,但代码逻辑里避免调用2020之后新增的API,这样高版本也能跑。部署时把.NET Framework运行时一起打包,或者直接做安装程序检测并静默安装。第三方依赖尽量少用,如果非用不可,把对应dll也放进同一目录,并在程序集解析事件中指定本地加载路径。
最后说两句
做CAD二次开发这件事,真正难的不是API本身,而是要把现场画图的那套业务规则翻译成代码逻辑。比如图框要套哪个模板、设备符号要插到哪层、标注格式要用多少号字,这些规则化得越细,自动绘图的效果越惊艳。我自己的习惯是,每次开发前先把手动画一遍的流程录成视频,再一步步拆成可执行的动作表,最后才动手写C#代码,这个习惯帮我省掉了大量返工时间。
如果你正准备做一个CAD自动绘图的项目,我建议别一开始就追求大而全的系统,先把“NETLOAD加载dll -> 执行命令 -> 画出第一个图元”这个闭环打通,然后再逐步加图层管理、批量绘制、块插入、与上位机数据联调这些能力。一套能跑起来的最小系统,永远比一堆设计文档更能帮你摸清问题在哪。最后分享一个小技巧:开发过程中在代码里预留一个“日志开关”,把每次自动绘图的坐标点数量、耗时、图层名这些关键信息写到txt文件,后期排查现场问题时会比逐个问用户“你刚才做了什么操作”高效得多。这个看起来土办法,在几次交付中帮我把沟通成本降了一半以上。
本文还有配套的精品资源,点击获取