AutoCAD选中实体另存DWG副本:AutoLISP与C# .NET双方案详解
在CAD工作流里,“把当前图纸中选中的一些实体,单独存成一份新DWG”这个需求,出现的频率远比想象中高:拆图交付给下游、把节点大样抽出来给现场、临时备份某个区域、模型空间里那批图要单独发给协作单位……以前大家的做法都是先“另存为”整图,再把多余实体删掉,最后保存,全程精神高度紧张,万一删错一个关键块,原图也跟着遭殃。我见过太多人浪费一上午在这种反反复复“另存为→删→存”的操作上。
这篇文章要讲的,就是用两条技术路线把“选择实体另存副本”做成一条命令的事。第一条路线是AutoLISP,轻量、免编译、改一行代码就能用,适合个人提效和临时救急;第二条路线是C# + AutoCAD .NET API,适合做成公司级工具,能加界面、能集成到现有插件体系里、能把操作日志和控制逻辑做得很完整。两条路线我都给完整代码,讲清楚原理,再附上一堆只有亲自踩过坑才会知道的注意事项。
先说清楚适用范围:AutoCAD 2016以上版本基本都能跑通,C#方案需要Visual Studio 2019或2022,以及对应版本的AutoCAD安装目录下的两个DLL(acdbmgd.dll和acmgd.dll),无需额外安装SDK。如果你之前完全没碰过CAD二次开发,也放心往下看,代码部分我会把每一行的作用拆开讲。
1. 需求解析与实现思路
1.1 先搞清楚:这个命令到底要解决什么问题
“选择实体另存副本”听起来简单,但真要落地,需求其实分三种,不同场景做法差别很大:
- 纯拷贝:把选中的实体原样复制到一个新DWG文件,新文件和源文件没有任何引用关系,这是最主流的用法。
- 带基点导出:导出后还要把副本插入回原图或其他图,这要求新图带一个明确的基点,相当于做成局部图块。
- 清理导出:某些实体依赖外部参照、代理对象、自定义对象,导出时这些依赖链要不要带过去,直接影响文件能否被别人正常打开。
本文实现的是第一种“纯拷贝”,但在代码里会照顾到块引用、图层、线型这些基础依赖,保证导出的文件不丢东西。后两种会在文末的扩展思路里给方向。
其实AutoCAD原生就有个 WBLOCK(写块)命令,可以从当前图里选对象另存成独立DWG。那我为什么还要写代码?原因有三个:
WBLOCK命令弹出的对话框选项太多,普通用户容易误操作,写成命令后可以固定参数。WBLOCK导出的文件默认会带上整图的图层、线型、文字样式等一大堆多余定义,文件体积偏大;通过二次开发可以只带必要数据,更干净。- 一体化和批量是两回事。批量化时你总不能手动一个个块存吧?代码化是后面自动化一切的基础。
所以这个功能的核心价值不是“替代WBLOCK”,而是“用编程方式把WBLOCK的能力做成可控、可复用、可扩展的自动化流程”。
1.2 两条路线的技术原理对比
AutoLISP路线的本质是:在命令行里调用AutoCAD自己的命令,把“选择对象→指定基点→指定文件名→回车”这一串交互动作封装成一个函数。它的优势是简单、零部署,任何一台装了AutoCAD的机器都能用,但也有天花板:跨文件批量处理时性能一般,异常处理能力弱,做不了漂亮的界面。
C#路线的本质是:通过AutoCAD .NET API直接操作数据库对象(Database),用 WblockCloneObjects 方法把选中的实体从一个数据库“克隆”到内存中的新数据库,再 SaveAs 落地成文件。它的优势是稳定、可控、可扩展,哪怕以后要做批处理、做自识别图层清理、做文件命名规则,都在这套框架里往上加就行。
这里的“数据库”概念是理解CAD二次开发的关键。一个DWG文件在AutoCAD内存里就是一个 Database 对象,里面装着块表、图层表、线型表、文字样式表这些容器,实体就放在块表记录里。你平时看到的“实体”,在API世界里就是一个一个带 ObjectId 的对象。搞清楚这个体系,C#代码的逻辑就顺了。
2. AutoLISP路线:轻量救急,十分钟上手
2.1 方案选型:为什么不自己写复制逻辑
AutoLISP里其实也有 vla-copy 一类的方法可以直接拷贝实体对象,CopyObjects 也存在于ActiveX接口里。但在这个需求里,我的建议是别折腾API复制,直接用 -WBLOCK 命令行版本封一层,又稳又快。
原因很简单:
-WBLOCK是AutoCAD内核功能,处理图层、线型、块表的依赖关系是原生能力,自己写代码反而容易漏。- 代码量少,出问题概率低,维护成本几乎为零。
- 命令行版本支持脚本化,LISP调用它很顺。
有人可能会问:WBLOCK 的交互流程不还是要弹对话框吗?不会,-WBLOCK 带减号前缀是纯命令行版本,通过LISP的 command 函数直接调用,不会弹任何对话框。
2.2 核心代码实现与逐行解释
先上完整代码,这段我实测在AutoCAD 2016、2020、2024上都能跑:
逐步解释:
第1行,(defun c:SaveSel (/ ss fname) 定义一个叫 SaveSel 的AutoCAD命令,c: 前缀表示它可以在命令行直接敲 SaveSel 执行。后面括号里的变量是局部变量,命令结束自动释放。
第2行,(ssget "_:L") 是选择实体并生成选择集。":L" 这个过滤参数表示只能选择当前图层没有被锁定的实体,这样避免导出时遇到锁定图层产生意外提示,是个很实用的细节。
第8到12行,getfiled 是AutoCAD内置的对话框函数,弹出保存文件对话框,默认文件名是在当前图名后加 _copy,文件类型过滤成dwg。getfiled 返回用户选择的文件路径,取消则返回 nil。
第15到17行,这个保底逻辑不能省:如果用户选的文件已存在,-WBLOCK 会弹出覆盖确认,直接中断脚本流程。更安全的做法是先删掉旧文件,避免命令行卡在交互状态。实测中这一步少了,部分CAD版本里命令会直接挂起。
第18行,核心调用:-WBLOCK 加文件名,第二个参数是空的表示用命令行模式,第三个参数 "0,0" 是导出基点,ss 是选择集,末尾空字符串表示结束选择。这里基点按团队约定来定,如果导出的是局部图,建议用用户点选的方式,代码改成:
2.3 AutoLISP方案的局限与补救
AutoLISP方案最大的问题出现在两个地方:
第一,依赖交互。每次运行都要人工点选实体、选路径,做不到全自动批量。要批量,可以再包一个 ssget "X"(全图选择)或者按图层名过滤的循环,但每个文件还是一次次弹对话框,体验一般。
第二,异常处理弱。如果目标文件被另一个进程占用、磁盘写保护、或者图纸本身有代理对象异常,LISP的报错信息很笼统,用户大概率看得一头雾水。补救办法是增加 vl-catch-all-apply 包裹关键调用:
说实话,AutoLISP这套方案最适合的场景是“我一个人用、偶尔跑一次、不想装任何插件环境”。一旦你的需求开始变得复杂,比如要批量处理、要记录日志、要给同事打包安装,就该切到C#路线了。
3. C#路线:用.NET API做出工程级命令
3.1 环境准备与项目搭建
C#二次开发的正式叫法是“使用AutoCAD .NET API编写托管插件”,对应的开发包叫ObjectARX for .NET(现在通常直接称AutoCAD .NET API)。核心依赖就两个DLL,在AutoCAD安装目录下:
acdbmgd.dll:托管入口,操作数据库、实体、图形对象。acmgd.dll:托管入口中的文档层,操作命令、编辑器、选择集、文件对话框。
不同AutoCAD版本引用的DLL版本不同,比如R24.0对应AutoCAD 2021,R24.3对应AutoCAD 2024。建议编译时把这两个DLL的“复制本地”设为 false,避免输出目录里出现一堆带版本差异的DLL,导致加载到不同版本CAD时报错。
开发环境我用的是Visual Studio 2022 + .NET Framework 4.8。注意:AutoCAD 2024及更早版本不直接支持.NET Core/.NET 5+,必须用.NET Framework 4.7或4.8系列,这是很多初学C#外挂的人容易踩的坑。如果用VS 2022新建项目默认是.NET 8.0,一定要手动改成.NET Framework。
创建项目步骤:
- 打开Visual Studio,新建“类库(.NET Framework)”项目,命名
CadTools。 - 项目属性里把目标框架选成 .NET Framework 4.8。
- 在“引用”里添加
acmgd.dll、acdbmgd.dll,右键属性,把“复制本地”改成false。 - 编译后把DLL放到某个固定目录,在AutoCAD命令行用
NETLOAD加载。 - 项目平台目标务必设置成 x64,AutoCAD从2013年开始就是64位程序。
3.2 C#核心代码实现:另存副本
完整代码奉上,这段代码是本文的核心,我尽量注释详细:
这段代码的流程我用一张简表先概括,再逐块解释:
| 步骤 | 对象/方法 | 作用 |
|---|---|---|
| 1 | ed.GetSelection() |
弹出选择交互,让用户选择实体 |
| 2 | ed.GetFileNameForSave() |
弹出保存文件对话框,拿到目标路径 |
| 3 | new Database(true, true) |
在内存中新建一个空数据库 |
| 4 | WblockCloneObjects() |
把选中实体从源库克隆到新库 |
| 5 | newDb.SaveAs() |
把内存数据库落盘为DWG文件 |
3.3 关键API详解:WblockCloneObjects到底做了什么
WblockCloneObjects 是AutoCAD .NET API里最核心的“克隆”方法,理解它是看懂整套代码的关键。
先看方法签名:
它的工作过程像是“打包搬家”:把你要的实体连同依赖的符号表记录(图层、线型、文字样式、块表记录等)一起复制进目标数据库。为什么强调“依赖”?举个例子:你选中了一个标注尺寸实体,它内部引用了文字样式“STANDARD”和标注样式“DIMSTYLE_1”,如果只复制实体本身,不复制这些样式定义,新文件打开时标注就会错乱。WblockCloneObjects 会自动把依赖链一并搬过去,这就是它比手动一个个复制实体强的地方。
参数 DuplicateRecordCloning.Replace 是实测中比较顺手的策略。它表示:如果目标库里已经有同名图层或线型,就用源库的记录替换掉目标库的,否则直接套用。你可以换成 DuplicateRecordCloning.Ignore,表示遇到同名定义时保留目标库的版本。第二个参数 deferTranslation 设 false,意思是现在就把映射关系计算好,不要延迟。
3.4 让命令更好用:增加图层过滤和批处理入口
基础版能用之后,我有两个项目实践中常用的增强点,顺手加进代码,让这个命令更“工程级”:
第一个增强:支持命令行过滤。有时候用户不想一个个点选,而是想“把某个图层上的所有实体导出去”。给 GetSelection 加一个过滤条件就行:
这段代码构建了一个“图层等于 PUB_LYR 或 STRUCT_LYR”的过滤条件。写的时候要注意第一行 DxfCode.Operator 是逻辑操作符开始,最后一行是逻辑结束,顺序不能反。
第二个增强:批量入口。如果需要一次性把当前图所有模型空间实体,按图层名拆成多个独立DWG文件,代码如下:
这个批处理入口,就是把基础版的 SaveSelectedToNewDwg 复用了一下。开发插件时,把核心逻辑单独拆成私有方法,然后让“手动选择版”“按图层版”“基于窗口矩形版”都去调用同一个核心方法,后面统一修bug时只改一处就行。这是整个C#方案里我认为最省心的设计习惯。
4. 两条路线如何选型与配合
4.1 功能与体验逐项对比
这个对比表我反复调整过很多次,拿它给团队做技术选型时,几乎能一下子说服犹豫不决的人:
| 维度 | AutoLISP方案 | C# .NET方案 |
|---|---|---|
| 开发门槛 | 低,会lisp基础语法即可 | 中,需要懂C#和面向对象 |
| 命令加载 | 直接APLOAD / 复制到支持目录 | 需要编译DLL,NETLOAD加载 |
| 分发部署 | 复制lsp文件即可 | 需要处理DLL版本、注册加载 |
| 批处理能力 | 弱,循环跑容易卡 | 强,可批量处理上百文件 |
| 界面能力 | 几乎为零 | WinForms/WPF任选 |
| 异常处理 | 弱,报错信息有限 | 强,可捕获详细异常 |
| 性能 | 一般,大量实体时慢 | 好,直接操作内存数据库 |
| 适合人群 | 个人提效、临时工具 | 团队工具、长期维护项目 |
4.2 我的选型建议:大部分时候两个都留
我个人的习惯是:在开发环境里两个都配置好。AutoLISP这条线用来快速试想法,比如“我今天不确定这个选择过滤要不要加”,先在LISP里跑通逻辑、验证交互流程,确认需求真的合理,再动手写C#的正式版。这套“先用LISP做原型,再用.NET做产品”的开发流,我用了七八年,一直很顺手。
如果是给团队其他同事用,务必提供C#编译的DLL版本,配合一个简单的注册表脚本或启动组加载,同事只需双击一下安装脚本就能用,完全不知道背后是C#还是LISP。反过来,如果是自己一个人装机,懒得动Visual Studio,就放一个LISP文件在启动组里,一行 appload 搞定。
5. 实战中的坑:真实故障与排查记录
5.1 我在两个方案中都遇到过的典型问题
先列一个速查表,详细展开后面说:
| 问题现象 | 可能原因 | 处理方式 |
|---|---|---|
| LISP的command卡住不动 | 目标文件已存在,WBLOCK弹覆盖确认 | 调用前先删除或改名旧文件 |
| LISP保存的文件打开后实体位置不对 | 基点选错 | 明确基点,建议用选择集最小外接矩形中心 |
| C#编译成功但NETLOAD报无法加载 | 平台目标不是x64 | 项目属性 -> 生成 -> 平台目标x64 |
| C#命令执行到WblockCloneObjects报eInvalidInput | ObjectId集合里混入非实体对象 | 遍历过滤仅保留Entity派生对象 |
| 新DWG缺失图层或文字样式 | 克隆参数选错 | 改DuplicateRecordCloning为Replace |
| 新DWG打开有外部参照报错 | 未克隆外部参照定义 | 需额外处理xref,见代码补充 |
5.2 C#排查技巧:把自己写的命令变成可调试程序
C#插件开发最容易头疼的问题就是“编译通过,但一NETLOAD就炸,又看不到堆栈”。分享一个我常用的调试方法:
用Visual Studio直接调试AutoCAD插件:
- 在Visual Studio中打开项目,右键项目 -> 属性 -> 调试。
- “启动外部程序”填
acad.exe的完整路径。 - 设置命令行参数,比如
/nologo。 - 在
SaveSelectionAsCopy入口处下断点,按F5,Visual Studio会启动AutoCAD并自动附加调试器。 - 在AutoCAD里输入命令后,断点就命中了,可以直接看变量值、单步进入
WblockCloneObjects内部。
这个方法我用了很多年。每次新环境第一次搭插件流程,或者改完代码出现不明原因报错,直接跑一遍调试,定位问题比靠 WriteMessage 猜快十倍。
5.3 一个被忽略的“清理范围”问题
导出副本时,如果你选中的实体里有 AttributeDefinition(属性定义)或者块表记录内部的实体,WblockCloneObjects 偶尔会有一些边缘情况,比如导出后新图里出现“孤儿对象”。我的处理经验是:在选择集生成后,加一个过滤函数:
另外,真正出现频率高的坑其实是:用户在模型空间选了一堆实体,结果点到了图纸空间的视口实体,导出后打开新图发现啥都没有或者多了一个视口对象。解决办法是加一个判断 ent.Database == sourceDb,并检查 ent.GetRXClass().Name != "AcDbViewport",把视口对象排除掉。别问我为什么知道要加这个,问就是被坑过。
5.4 再补充一个跨版本部署提醒
C#编译的DLL有一个容易踩的坑:不同版本AutoCAD所依赖的.NET API程序集版本不同。比如AutoCAD 2020用的是 acdbmgd.dll 22.0,AutoCAD 2024用的是 acdbmgd.dll 24.3。如果你的插件要同时给安装不同版本AutoCAD的同事用,有两个处理方式:
- 针对每个版本单独编译一份DLL,按版本号建目录分发。
- 写一个小启动器,在
NETLOAD前动态判断当前CAD版本,加载对应目录的DLL。
我在公司内部用的是第二种,目录结构长这样:
每个版本编译时的引用指向对应安装目录下的 acdbmgd.dll,这样每个DLL只认自己那一套API。分发给同事时,一个 Install.cmd 把当前CAD版本检测出来,再把对应DLL复制到固定插件目录,同事只需双击一次。
6. 后续扩展:从“单条命令”到“批量工具”
如果一个“选择实体另存副本”的命令已经不能满足你,那基本说明你已经走到了二次开发的下一阶段:批量自动化。这里我给几个我们团队实际用到的扩展方向,都是基于这篇文章的代码框架往上加:
把 SaveSelectedToNewDwg 这个核心方法,和 DocumentManager 配合,就能实现遍历多个DWG文件、每个文件抽取指定实体、统一导出到指定目录的全自动流程。整个过程可以纯后台跑,不需要打开CAD界面,用 Application.DocumentManager.Open() 逐个打开文件,操作完再关掉。
另一个实用扩展是“导出时重命名图层”。因为 WblockCloneObjects 是整体复制符号表记录,你可以先遍历克隆出来的新库,把图层名按映射规则改掉,再保存。这在给外部单位交付图纸时特别有用——不用手工去清理图层,命令自动做完,交付文件干净利落。
最后再加一个思路:如果你经常要“把当前视口中的可见区域导出成独立DWG”,可以结合 ViewTableRecord 获取当前视口的范围和坐标,把范围内的所有实体自动选中,再走同一套 WblockCloneObjects 流程。这个操作不用人工框选,一张大图按视口切分导出非常高效。
我在实际使用中发现,越是这种不起眼的小功能,越能看出一个CAD操作有没有被仔细想过。“选择实体另存副本”背后的本质是“局部数据抽取”,只要你掌握了 WblockCloneObjects 这条核心链路,很多更复杂的抽取、拆分、合并需求都会豁然开朗。建议你先把两条路线的代码各跑通一遍,感受一下AutoLISP的无缝快捷和C#的可控严谨,再根据自己的使用频率决定正式工具往哪个方向深入。