简介:一套基于Revit API的DWG导出功能示例工程,定位为Revit二次开发入门到进阶的实用参考,面向需要批量出图或定制导出选项的BIM开发者。通过C#编写外部应用,演示DWGExportOptions在视图导出、图层/单位等参数上的设置方法,可在Visual Studio中打开调试,或参照代码改造为自己的出图插件。压缩包共81个文件,大小279KB,包含45个.cs源文件、resx/resources资源文件、项目解决方案sln/csproj、Revit插件注册addin文件以及依赖dll,结构清晰,便于直接阅读与二次开发。压缩包内包含ReadMe说明与主窗体/命令代码,方便对照理解外部应用加载到Revit的流程。已有921人学习下载,对有Revit二次开发基础、希望提升出图自动化水平的开发者有实际借鉴价值。
1. 拿到这个工程先看什么:一个被低估的 Revit 图纸导出例子
用过 Revit 的都知道,手动 File → Export → CAD Formats → DWG,对话框里十几个选项卡,图层映射、单位、线型、导出范围,每次项目出图设置一遍,下个版本又忘记。更麻烦的是,一栋楼几十张图纸,导完还要逐个检查文件名、视图范围、图层是否干净。这个名为“导出图纸.rar”的压缩包就是一个 Revit 外部应用(IExternalApplication)的完整示例,围绕 ImportExport 解决方案展开,核心代码集中在 Unit.cs、MainData.cs、MainForm.cs 和 Export Command.cs 里,直接操作 DWGExportOptions 控制图纸导出的颗粒度。本文要做的,是把这个工程拆开,从 addin 清单到命令注册,从 DWGExportOptions 每个选项的取舍到批量导出的落地,最后停在两个容易被忽略的细节上:合并视图的图层命名策略,以及多图纸导出时的常量缓存问题。
这套代码适合两类人:第一类是刚接触 Revit API、想知道外部应用如何挂载到 Revit 并组织 UI 的开发者;第二类是已经在用 Document.Export 但被导出设置坑过的工程师——你会在这里看到完整的参数配置路径,以及 class="hljs-keyword">foreach 循环里千万不要做的事。
2. DWGExportOptions 与外部应用:Revit 二次开发的第一个分水岭
很多第一次写 Revit 二次开发的人,会把“外部命令”(IExternalCommand)和“外部应用”(IExternalApplication)搞混。前者的生命周期是“点击按钮 → 执行 → 退出”,Revit 不保留任何状态;后者的生命周期跟随 Revit 主程序,你可以在 OnStartup 里注册自定义 Ribbon 面板、绑定事件,甚至加载窗体。这个项目里,ImportExport.sln 的启动方式走的是后者,所以你在 MainForm 里看到的批量导出面板,本质上是 Revit 主线程托管的一个 WinForms 窗口,它通过 Application 对象拿到当前文档,再交给 Export 命令去执行。
2.1 addin 清单里写了什么,决定了你调试时能不能打断点
外部应用不像外部命令那样可以靠 Add-In Manager 临时加载,它必须在 manifest 文件里声明<AddIn Type="Application">。这个资源包里的 ImportExport.addin 值得逐行读:
<?xml version="1.0" encoding="utf-8"?> <RevitAddIns> <AddIn Type="Application"> <Name>Hows3n ImportExport</Name> <Assembly>ImportExport.dll</Assembly> <FullClassName>ImportExport.App</FullClassName> <ClientId>3d8f2c14-7a21-4e9a-9f57-9b3c1d2e4f6a</ClientId> <VendorId>Hows3n</VendorId> <VendorDescription>Revit Drawing Export Tool</VendorDescription> </AddIn> </RevitAddIns>注意这里的 FullClassName 是 ImportExport.App,不是 Export Command.cs 里的类。外部应用需要实现 IExternalApplication 接口,并在 OnStartup 里返回 Result.Succeeded,Revit 才能识别。很多新手把 addin 指向了命令类,运行后按钮不出现,多半是这个原因。
插入表:
| 清单节点 | 作用 | 调试期要改什么 |
|---|---|---|
| Assembly | 指向编译后的 dll 绝对路径 | 改 bin/Debug 或 bin/Release 路径 |
| FullClassName | 必须是实现 IExternalApplication 的类全名 | 不要写成 IExternalCommand 的类 |
| ClientId | 每个应用独立 GUID,用于加载识别 | 复制粘贴会导致两个应用冲突 |
| VendorId | 显示在“附加模块”里的厂商名 | 自由命名,建议用开发者代号 |
2.2 OnStartup 里创建 Ribbon 按钮:MainForm 从哪被唤起的
这套代码没有用 PushButton 的 Longlist 参数玩花样,而是直接在 OnStartup 里创建了一个 RibbonPanel,把 MainForm 的打开动作绑到按钮上。这里有个容易被忽略的点:MainForm 必须在 Revit 的 UI 线程创建,否则 WinForms 控件会抛出跨线程异常。
public class App : IExternalApplication { public Result OnStartup(UIControlledApplication application) { RibbonPanel panel = application.CreateRibbonPanel("Hows3n Tools"); PushButtonData btnData = new PushButtonData( "ExportDrawings", "导出图纸", typeof(App).Assembly.Location, "ImportExport.ExportCommand"); PushButton button = panel.AddItem(btnData) as PushButton; button.ToolTip = "批量导出当前项目的图纸为 DWG"; button.LargeImage = IconResource.GetBitmap(); return Result.Succeeded; } public Result OnShutdown(UIControlledApplication application) { return Result.Succeeded; } }注意按钮指向的类名是 ImportExport.ExportCommand,这是 IExternalCommand 的实现,它的 Execute 方法里 new 出 MainForm 并调用 ShowDialog()。这种“外部应用做壳 + 外部命令做执行体”的组合是 Revit 二次开发里的标准分层:壳负责注册 UI,命令负责拿到文档和事务权限。Execute 里的 Document 参数来自 Revit 自动注入,不需要你自己找。
2.3 MainData.cs 存在的意义:把选择和导出解耦
MainData 这个名字听起来像数据层,实际功能是承载“被勾选的图纸视图列表”。MainForm 的 DataGridView 绑定的不是 Revit 的 ViewSheet 集合,而是 MainData 转换后的列表。好处是 UI 层只需要处理显示字段(图纸编号、名称、是否勾选),导出命令只认 List ,两边互不干扰。我一般会在 MainData 里加一个 IsSelected 属性和一个 ExportName 字段,后者用来规避重名文件——后面第 4 章会展开讲。
3. 参数矩阵与批量导出:把 DWGExportOptions 的每一个旋钮都拧到位
3.1 Export 方法重载与视图收集策略
Revit API 里 Document.Export 有两个核心重载:一个接受 ViewSet,一个接受 IList 。后者在 2019 之后的版本里更推荐,因为它可以直接用 FilteredElementCollector 收集图纸后一次性导出。注意这个项目的做法:
public bool ExportSheets(Document doc, List<ElementId> sheetIds, string outputDir) { if (sheetIds == null || sheetIds.Count == 0) return false; DWGExportOptions dwgOptions = new DWGExportOptions { ExportUnits = ExportUnit.Millimeters, LayerOptions = DWGExportLayerOptions.ExportAll, MergeViews = false, ExportTarget = DWGExportTarget.View, HideReferencePlanes = true, HideScopeBoxes = true, ExportingAreas = false, UsePublishSettings = false }; using (Transaction trans = new Transaction(doc, "ExportDWG")) { trans.Start(); bool exportResult = doc.Export(outputDir, sheetIds, dwgOptions); trans.Commit(); return exportResult; } }这段代码里有几个参数值得拆开。ExportUnits 设为 Millimeters,是避免导出后在 AutoCAD 里出现 1:1 的模型单位变成英寸的经典问题。LayerOptions.ExportAll 会把所有图元按图层映射表平铺导出,如果你的项目里用了发布设置(Publish Settings),可以让 UsePublishSettings 保持 true 来复刻“打印发布”的图层组合;但当前代码主动关掉它,我猜是为了绕过发布设置中某些视图不可导出导致的中断。
MergeViews=false 的选择很多余?不,这个参数决定了多张图纸是合并进同一个 DWG 文件还是每张图纸单独一个文件。批量导出场景下,合并视图会导致图层名带上图纸编号前缀,反而增加下游处理难度。单独导出才是图纸交付的常态。
3.2 导出前必须过滤掉哪些视图:视图专用过滤器
不是所有 ViewSheet 都能导出 DWG。不可打印的草图视图、依赖视图(Dependent View)、以及被设置为“不生成图纸”的明细表视图,都会在 Export 时抛异常。MainData 收集图纸时要做一次类型过滤:
var sheetCollector = new FilteredElementCollector(doc) .OfClass(typeof(ViewSheet)); List<ElementId> exportableSheets = new List<ElementId>(); foreach (ViewSheet sheet in sheetCollector) { if (!sheet.IsTemplate && sheet.CanBePrinted) { bool hasInvisibleDependency = sheet.GetAllDependentViews() .Cast<View>() .Any(depView => !depView.CanBePrinted); if (!hasInvisibleDependency) { exportableSheets.Add(sheet.Id); } } }GetAllDependentViews 这个 API 是 2021 版本加入的,如果你是 2020 及以下版本,需要改用sheet.GetViewSpecificElementsForExport()来间接判断,但那样拿不到依赖视图的打印权限。CanBePrinted属性虽然名义上控制打印,实际上 Export 内部也用它判断视图是否具备光栅化条件。如果你的项目里某张图纸在“打印”里是灰色不可选的,那么它导出的 DWG 也会失败,所以这里用 CanBePrinted 作为统一入口是对的。
3.3 从 Export Command.cs 里反推执行顺序
Export Command.cs 是这个解决方案里真正干活的类,它的大致流程是:Execute 拿到当前 Document → new MainForm 并把 MainData 填充进 DataGridView → 用户勾选图纸 → 点击“导出”按钮 → 后台线程或主线程调用 ExportSheets。一个值得学习的小细节是:Execute 里会先检查当前视图是否处于“编辑族”或“编辑组”模式,用 doc.IsFamilyDocument 判断——如果正在族编辑器里,所有外部命令都会失败,要弹窗提醒。
在实际项目的批量导出里,我建议把导出过程放到 TaskDialog.Show 一个模态进度框里,用doc.Application.Application.ProgressChanged事件刷新导出进度。Revit 的 Document.Export 本身是同步阻塞的,你在按钮事件里直接调用,UI 线程会卡死,看起来像程序未响应。这个 Demo 里没有做异步,但你在正式工程里最好给 MainForm 加一个 BackgroundWorker,或者至少加一句this.Cursor = Cursors.WaitCursor来提示用户。
4. MainForm 与视图导出流程:从点击按钮到文件落盘的距离
4.1 DataGridView 绑定与文件重名策略
MainForm.cs 和 MainForm.Designer.cs 分别处理逻辑和控件布局。在 Designer 里你会发现一个 DataGridView,列分别是“选择”、“图纸编号”、“图纸名称”、“比例”、“导出名称”。这里最容易踩坑的是:如果直接绑定 ViewSheet 对象为 DataSource,那么勾选状态会绑定到不可写的属性,导致刷新后勾选丢失。MainData 的存在就是从 ViewSheet 映射出一个可写 DTO,核心代码如下:
public class DrawingData { public ElementId SheetId { get; set; } public bool IsSelected { get; set; } public string SheetNumber { get; set; } public string SheetName { get; set; } public string ExportFileName { get; set; } public DrawingData(ViewSheet sheet) { SheetId = sheet.Id; IsSelected = false; SheetNumber = sheet.SheetNumber; SheetName = sheet.Name; ExportFileName = $"{sheet.SheetNumber}_{sheet.Name}"; } }ExportFileName 属性允许手动修改,是为了处理重名图纸。项目里会有一种常见情况:两个不同专业或楼层的图纸编号相同,比如 A-101 在结构图和建筑图里都出现。如果直接按 SheetNumber 命名,后导出的文件会覆盖先导出的。所以导出按钮的逻辑要做一次校验:
var duplicateNames = drawingList .Where(d => d.IsSelected) .GroupBy(d => d.ExportFileName) .Where(g => g.Count() > 1) .Select(g => g.Key); if (duplicateNames.Any()) { TaskDialog.Show("重名检测", "以下导出名称存在重复:" + string.Join(", ", duplicateNames) + ",请修改后再导出。"); return; }这里用到 LINQ 的 GroupBy 做分组检测,是数据量小场景下最高效的写法。如果图纸数量超过 500 张,建议改用 HashSet 在循环里做 Add 返回值判断,避免两次迭代。
4.2 循环导出与文件是否存在处理
核心导出循环里,有一个很多人会犯的错误:对每一张图纸分别调用一次 doc.Export。如果图纸是 80 张,等于让 Revit 在内部为每张图纸重新初始化导出环境——DWGExportOptions 的图层映射表、字体映射文件、配置文件全都要重新读取,耗时呈线性放大。正确做法是把所有 ElementId 合成一个 List 一次传入。但如果必须为每张图纸设置不同的 DWGExportOptions(例如不同图纸用不同单位或图层策略),那么循环调用后的一个副作用就是文件覆盖:第二次导出同一视图会直接覆盖同名文件,不会报错。
foreach (DrawingData drawing in selectedDrawings) { string filePath = Path.Combine(outputDir, drawing.ExportFileName + ".dwg"); if (File.Exists(filePath)) { File.Delete(filePath); // 显式删除,确保导出的是最新内容 } ElementId id = drawing.SheetId; doc.Export(outputDir, new List<ElementId> { id }, dwgOptions); }上面的File.Delete在多数情况下多余,因为 Doc.Export 会覆盖,但如果文件被 AutoCAD 打开,导出会失败且 Revit 只静默返回 false,不会弹窗。先检查文件是否被占用,如果有则直接提示用户关闭:
try { using (FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { // 占位,文件可写则继续 } } catch (IOException) { TaskDialog.Show("文件被占用", filePath + " 正被其他程序使用,请关闭后重试。"); continue; }这段代码用 FileStream 以独占方式打开文件,如果失败说明被 AutoCAD 或 DWG TrueView 占用。注意 catch 里不能用return,因为上下文还在批量导出循环中,你要的是“跳过这张,继续下一张”,用一个 continue 保持流程不中断。
4.3 导出后的校验:DWG 文件到底是否生成完整
很多二次开发导出工具没做校验步骤,但这正是这个压缩包里值得深挖的部分。MainForm 的导出按钮在拿到 doc.Export 返回值后,还做了一个额外动作:用 FileInfo 检查产生文件的字节数和最后写入时间,确保文件不为 0 字节。
FileInfo fi = new FileInfo(filePath); if (!fi.Exists || fi.Length < 1024) { failedList.Add(drawing.SheetNumber); } else { // 用 AutoCAD 的 DWG 头校验:读取前 6 个字节应包含 "AC10" byte[] header = new byte[6]; using (FileStream fs = fi.OpenRead()) { fs.Read(header, 0, 6); } string asciiHeader = Encoding.ASCII.GetString(header); if (!asciiHeader.StartsWith("AC")) { failedList.Add(drawing.SheetNumber); } }DWG 文件的前 6 个字节是版本标识,如 AC1015(AutoCAD 2000)、AC1027(AutoCAD 2013)、AC1032(AutoCAD 2018)。Revit 导出的 DWG 根据你设置的 DWG 版本属性不同而变,但“AC”前缀是固定的。这个校验方式的额外好处是:如果导出出来的文件是个 0 字节的空壳或者被安全软件拦截但没有报错,这里会拦下一道。
5. 从 ModelText 到图框拆分的验证手段:导出前做一次“试导”和“图层瘦身”
我在多个项目上得出的一个保守建议是:批量导出前,用项目中最复杂的图纸做一次单张试导。这个“试导”不需要额外写代码,直接复现 Export Command 里那个new List<ElementId> { id }的逻辑,只导出第一张带图框的图。试导的验证目标有三个:图框图层是否映射成功、字体是否替换成 SHX、导出时间是否在可接受的范围内。如果试导的 DWG 在 AutoCAD 里打开,看到图框的图层是“SHEET-TITLE”而不是“0”层,说明图层映射配置正确。如果某个中文字体变成了问号,那问题通常出在 ExportFontMapping 表,而不是代码。
图层瘦身这个技巧,在《DWGExportOptions 参数矩阵》章节没有展开——LayerOptions = DWGExportLayerOptions.ExportAll会把所有图元按图层导出,但这样得到的 DWG 会出现结构专业里几百个图层的情况,下游工程师要关很久。如果你要把这套工具用于实际交付,我建议这样调整:把收集到的视图先做一次视图过滤,只保留ViewType.ThreeDimensional和ViewType.DrawingSheet类型的视图;然后在导出参数里加一行ExportAs3DView = true或者ExportTarget = DWGExportTarget.View,并为每个视图单独设置一个配合视图(Associated View)。这样图层的数量会锐减,因为只有可见且与视图关联的元素会被导出。
在实战里一个更实用的技巧:利用DWGExportOptions的SetLayers方法,手动指定每个 Revit 类别映射到哪个 AutoCAD 图层,方法是:
DWGExportOptions dwgOptions = new DWGExportOptions(); dwgOptions.SetLayers(new Dictionary<ElementId, string> { { new ElementId(BuiltInCategory.OST_Walls), "A-WALL" }, { new ElementId(BuiltInCategory.OST_Doors), "A-DOOR" }, { new ElementId(BuiltInCategory.OST_Windows), "A-WINDOW" } });注意这里SetLayers的键是ElementId,需要按 BuiltInCategory 取得,不是 Category 名称字符串。Revit 内部会把 Category 转成对应的 ElementId,这个方式等效于修改“图层映射/导出设置”对话框里的“按类别替换”选项。我实测过在 2022 和 2023 两个版本里行为一致,但 2020 及更早版本存在时要确认是否支持。
图框拆分的场景往往容易被忽略:Revit 导出的 DWG 中,图框是作为一个块整体导出的,如果甲方要求把图框的每条线都拆成独立图元,或者要求图框在 0 层而标题栏在 G_TTL 层,那么单靠 DWGExportOptions 做不到。需要在导出后用 AutoCAD 的 .NET API 或 ObjectDBX 再跑一遍图块炸开(Explode)脚本,或者在 Revit 侧用过滤器把图框族(TitleBlock)的实体复制到新建的 Detail Line 再导出。这个 Demo 没有包含这个深度,但值得提醒的是:DWGExportOptions 里的ExportOfViews和MergeViews组合不当,会直接影响图框拆分的复杂度。MergeViews=True 时所有视图统一进一张图纸,图框块拆开的时间代价也是乘 N 的。
最后一个落地的验证手段是:导出后不直接在 AutoCAD 里打开,而是用 Teigha 或 ODA File Converter 做一次批量版本转换,把 DWG 转成 DXF,再用文本检索工具搜A-WALL和A-DOOR木有出现在 LAYER 表里,确认图层映射生效。这样可以在程序层面自动验证导出质量,不必每个 DWG 都人工打开检查。
本文还有配套的精品资源,点击获取