简介:这份资源是 Autodesk 官方出品的 Revit 2019.2 二次开发案例合集,面向具备一定 C# 或 VB.NET 基础、希望系统掌握 Revit API 的开发者与 BIM 工程师。147 个官方案例覆盖基本元素操作、参数读写、图形显示控制、事件响应、工作流集成、界面扩展与性能优化等方向,可帮助读者从零散摸索转向按官方最佳实践理解插件开发全流程。压缩包共约 2000 个文件、256.85MB,以 1105 个 cs 源码、186 个 addin 清单、167 个 csproj 工程文件为主,辅以 resx 资源、rvt 与 rfa 模型样例、xaml 界面及少量 vb 项目,每个案例基本可独立编译运行。目前已有 4468 人学习下载,适合作为案头工具书反复查阅,通过对照源码调试,快速积累可复用的开发经验与排错思路。
1. Revit 2019.2 二次开发:147 个官方案例到底该怎么啃
很多人第一次接触 Revit 二次开发,都是被“147 例官方案例+源代码”这种标题吸引进来的。我也不例外。当年我拿到这套案例时,第一反应是兴奋,第二反应是懵——147 个文件夹,每个里面都有.cs、.addin、.csproj,但真正跑起来能编译通过的不到三分之一。问题不在案例本身,而在于大多数人不知道这些案例是按什么逻辑组织的,也不知道哪些该先看、哪些可以直接跳过。
这套官方案例的价值在于:它覆盖了 Revit API 从最基础的文档读写、元素过滤,到进阶的族实例操作、几何计算、事件响应、外部应用与命令注册等几乎所有核心接口。适合两类人:一是刚学完 C# 基础、想通过真实代码理解 API 调用方式的开发者;二是已经能写简单插件、但遇到“某个功能不知道用哪个接口”时想快速查参考的熟手。但前提是,你得知道怎么拆它。
2. 先搞清楚这 147 例的组织逻辑:按 API 域分类而不是按难度
2.1 官方案例的目录结构通常长什么样
常见的组织方式是按功能域分文件夹,比如Application、Document、Elements、Family、Geometry、Events、Parameters、Selection、Transaction等。每个文件夹下再按具体接口或场景分子目录。比如Elements下面可能有FilteredElementCollector、ElementParameter、ElementTransform等。
我一般会先做一件事:把整个案例目录树打印出来,用一张表标记每个案例涉及的核心 API 类。这样做的目的是建立索引,而不是盲目地从头编译到尾。
| 文件夹名 | 典型案例数 | 核心 API 类 | 建议优先级 |
|---|---|---|---|
| Application | 8 | IExternalApplication、UIControlledApplication | 高 |
| Command | 12 | IExternalCommand、ExternalCommandData | 高 |
| Elements | 25 | FilteredElementCollector、Element | 高 |
| Family | 18 | FamilyInstance、FamilySymbol | 中 |
| Geometry | 15 | Options、Solid、Face | 中 |
| Events | 10 | DocumentChanged、DialogBoxShowing | 中 |
| Parameters | 12 | Parameter、SharedParameter | 高 |
| Transaction | 9 | Transaction、SubTransaction | 高 |
| Selection | 7 | ISelectionFilter、PickObject | 中 |
| Ribbon | 11 | RibbonPanel、PushButton | 低 |
| Others | 20 | 杂项 | 低 |
这张表不是官方给的,是我自己踩坑后整理的。优先级高的意思是:这些案例涉及的 API 在你写任何插件时都会用到,必须先吃透。
2.2 为什么不能按文件夹顺序从 1 编译到 147
因为很多案例依赖特定的 Revit 版本、特定的.rvt示例文件,甚至依赖你本地已经安装的某些族库。如果你按顺序编译,会在第 3 个案例就卡住,然后花大量时间解决环境问题,而不是学 API。
正确的做法是:先挑出那些不依赖外部.rvt文件、只靠代码就能跑通的案例。这类案例通常会在Execute方法里直接创建元素或读取当前文档信息。比如Command文件夹下的HelloWorld、Elements下的CollectAllWalls、Parameters下的ReadParameter。
2.3 建立自己的案例索引表
我建议你花半小时做一张表,字段包括:案例编号、文件夹、核心类、依赖文件、是否可独立运行、一句话功能描述。这张表后面会反复用到。
// 示例:读取当前文档中所有墙的类型名称 // 这个案例不依赖外部文件,适合第一次跑通 using Autodesk.Revit.DB; using Autodesk.Revit.UI; using Autodesk.Revit.Attributes; [Transaction(TransactionMode.ReadOnly)] public class CollectAllWalls : IExternalCommand { public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) { Document doc = commandData.Application.ActiveUIDocument.Document; FilteredElementCollector collector = new FilteredElementCollector(doc); collector.OfClass(typeof(Wall)); foreach (Wall wall in collector) { TaskDialog.Show("Wall", wall.WallType.Name); } return Result.Succeeded; } }这段代码的逻辑是:通过FilteredElementCollector收集当前文档中所有Wall类的实例,然后遍历输出墙类型名称。参数说明:TransactionMode.ReadOnly表示只读操作,不需要开启事务;OfClass是过滤条件,只保留指定类型的元素。如果你把Wall换成Floor,就能收集楼板。
3. 把官方案例跑起来的最小环境配置
3.1 Revit 2019.2 对应的 .NET 版本和项目模板
Revit 2019.2 的 API 基于 .NET Framework 4.7。你需要在 Visual Studio 里新建一个Class Library (.NET Framework)项目,目标框架选 4.7 或 4.7.1。不要选 .NET Core 或 .NET 5+,否则引用RevitAPI.dll会报错。
项目创建后,需要添加两个引用:RevitAPI.dll和RevitAPIUI.dll。它们通常位于C:\Program Files\Autodesk\Revit 2019\目录下。添加引用后,把这两个 DLL 的Copy Local属性设为False,避免编译时把 Revit 的 DLL 复制到输出目录。
3.2 .addin 文件的写法与常见错误
.addin文件是 Revit 识别插件的入口。它必须放在%AppData%\Autodesk\Revit\Addins\2019\或C:\ProgramData\Autodesk\Revit\Addins\2019\下。前者只对当前用户生效,后者对所有用户生效。
<?xml version="1.0" encoding="utf-8"?> <RevitAddIns> <AddIn Type="Command"> <Name>CollectAllWalls</Name> <Assembly>C:\MyRevitAddins\CollectAllWalls.dll</Assembly> <AddInId>8a3c5b2e-1f4d-4a6b-9c7e-2d8f0a1b3c4d</AddInId> <FullClassName>CollectAllWalls</FullClassName> <VendorId>MYCO</VendorId> <VendorDescription>My Company</VendorDescription> </AddIn> </RevitAddIns>参数说明:AddInId必须是一个 GUID,不能重复;FullClassName是包含命名空间的完整类名,比如MyNamespace.CollectAllWalls;Assembly路径必须是绝对路径。常见错误是FullClassName写错,导致 Revit 启动时提示“无法加载插件”。
3.3 调试时怎么附加到 Revit 进程
在 Visual Studio 中,右键项目 → 属性 → 调试 → 启动外部程序,选择C:\Program Files\Autodesk\Revit 2019\Revit.exe。然后在Execute方法里打断点,按 F5 启动 Revit。Revit 启动后,手动触发你的命令,断点就会命中。
注意:Revit 启动时会加载所有.addin文件,如果你的插件有编译错误,Revit 会直接崩溃或弹出一堆错误对话框。所以每次修改代码后,先编译通过,再启动调试。
4. 从 147 例中挑出最值得精读的 20 个案例
4.1 元素过滤与收集:FilteredElementCollector 的六种用法
FilteredElementCollector是 Revit API 里使用频率最高的类,没有之一。官方案例里至少有 8 个案例涉及它。我建议你重点看这几种用法:
OfClass(typeof(Wall)):按类过滤OfCategory(BuiltInCategory.OST_Walls):按类别过滤WhereElementIsNotElementType():排除类型元素WhereElementIsElementType():只保留类型元素OfType<FamilyInstance>():按 .NET 类型过滤WherePasses(new ElementParameterFilter(...)):按参数值过滤
// 收集所有可见的、非类型的墙,且墙厚大于 200mm FilteredElementCollector collector = new FilteredElementCollector(doc); collector.OfClass(typeof(Wall)) .WhereElementIsNotElementType() .WherePasses(new ElementParameterFilter( new FilterDoubleRule( new ElementId(BuiltInParameter.WALL_ATTR_WIDTH_PARAM), new GreaterThanOrEqualRule(200.0 / 304.8), // 200mm 转英尺 1e-6)));逻辑说明:WhereElementIsNotElementType排除墙类型,只保留墙实例;ElementParameterFilter配合FilterDoubleRule实现数值比较。参数说明:Revit 内部长度单位是英尺,所以 200mm 要除以 304.8;1e-6是容差。
4.2 事务与事务组:为什么你的代码改了模型却没保存
Revit API 里任何修改模型的操作都必须放在事务里。官方案例里关于事务的案例有 9 个,但很多人只看了Transaction,忽略了TransactionGroup和SubTransaction。
Transaction是最基本的,Start和Commit必须成对出现。SubTransaction用于在一个大事务里做局部回滚。TransactionGroup用于把多个事务合并成一个可撤销的操作。
using (TransactionGroup tg = new TransactionGroup(doc, "批量修改")) { tg.Start(); using (Transaction t1 = new Transaction(doc, "修改墙厚")) { t1.Start(); // 修改墙厚 t1.Commit(); } using (Transaction t2 = new Transaction(doc, "修改墙高")) { t2.Start(); // 修改墙高 t2.Commit(); } tg.Assimilate(); // 合并为一个撤销步骤 }逻辑说明:TransactionGroup把多个事务打包,Assimilate后用户按一次 Ctrl+Z 就能撤销所有修改。参数说明:Assimilate和RollBack二选一,前者提交,后者回滚。
4.3 族实例的创建与参数读写
族实例操作是二次开发里最复杂的部分之一。官方案例里Family文件夹下有 18 个案例,我建议重点看NewFamilyInstance的几种重载,以及LookupParameter和Set方法。
// 在指定点创建一个门实例,并设置其标记参数 FamilySymbol symbol = ...; // 通过过滤器获取 XYZ location = new XYZ(0, 0, 0); FamilyInstance instance = doc.Create.NewFamilyInstance( location, symbol, Level.Create(doc, 0), StructuralType.NonStructural); Parameter markParam = instance.LookupParameter("标记"); if (markParam != null && !markParam.IsReadOnly) { markParam.Set("D-01"); }逻辑说明:NewFamilyInstance有多个重载,这里用的是“点+符号+标高”的版本;LookupParameter按名称查找参数,返回 null 表示不存在;Set之前必须检查IsReadOnly。参数说明:Level.Create(doc, 0)创建一个标高为 0 的标高,实际项目中应该用已有的标高。
4.4 几何计算:Solid、Face、Edge 的提取与求交
几何相关的案例有 15 个,很多人觉得难,是因为不熟悉 Revit 的几何 API 体系。核心类是Solid、Face、Edge、Curve、XYZ。提取几何的入口是Element.get_Geometry(new Options())。
Options opt = new Options(); opt.ComputeReferences = true; opt.DetailLevel = ViewDetailLevel.Fine; GeometryElement geoElem = element.get_Geometry(opt); foreach (GeometryObject obj in geoElem) { if (obj is Solid solid && solid.Volume > 0) { foreach (Face face in solid.Faces) { // 处理面 } } }逻辑说明:Options控制几何提取的精度和是否计算引用;Solid.Volume > 0用于排除空实体。参数说明:ComputeReferences = true会生成可用于尺寸标注的引用,但会降低性能,不需要时设为 false。
5. 避坑:跑官方案例时最容易翻车的五个地方
5.1 现象:编译通过但 Revit 里找不到命令
原因:.addin文件里的FullClassName没有包含命名空间,或者Assembly路径指向了错误的 DLL。解决:打开.addin文件,确认FullClassName是命名空间.类名,Assembly是绝对路径且文件存在。
5.2 现象:运行时报 “Transaction is not open”
原因:修改模型的操作没有放在Transaction里,或者Transaction没有Start。解决:检查所有doc.Create、parameter.Set、element.get_Parameter().Set调用,确保它们都在t.Start()和t.Commit()之间。
5.3 现象:收集元素时返回空集合
原因:FilteredElementCollector默认只收集当前视图可见的元素,如果元素被隐藏或不在当前视图,就收集不到。解决:使用new FilteredElementCollector(doc)而不是new FilteredElementCollector(doc, doc.ActiveView.Id),前者收集整个文档。
5.4 现象:修改参数后模型没有更新
原因:参数是只读的,或者参数类型不匹配。解决:先检查param.IsReadOnly,再用param.StorageType判断是String、Integer、Double还是ElementId,然后用对应的Set方法。
5.5 现象:Revit 启动时崩溃或卡死
原因:.addin文件里的AddInId重复,或者插件在IExternalApplication.OnStartup里做了耗时操作。解决:用 GUID 生成器重新生成AddInId;把耗时操作放到IExternalCommand.Execute里,而不是OnStartup。
6. 进阶:把官方案例改造成自己的工具集
官方案例的代码是“教学级”的,不是“生产级”的。你要做的是把它们拆开,提取出可复用的方法,封装成自己的工具库。我一般会建一个RevitToolkit项目,里面按功能分静态类:ElementUtils、ParameterUtils、GeometryUtils、TransactionUtils。
public static class ParameterUtils { public static string GetStringValue(Element elem, string paramName) { Parameter p = elem.LookupParameter(paramName); if (p == null || p.StorageType != StorageType.String) return null; return p.AsString(); } public static bool SetDoubleValue(Element elem, string paramName, double value) { Parameter p = elem.LookupParameter(paramName); if (p == null || p.IsReadOnly || p.StorageType != StorageType.Double) return false; return p.Set(value); } }逻辑说明:GetStringValue和SetDoubleValue封装了参数读写前的检查逻辑,避免每次调用都写一堆 if。参数说明:StorageType必须匹配,否则Set会抛异常。
验证方法:写一个简单的命令,遍历当前文档所有墙,用ParameterUtils读取“注释”参数,然后批量设置“标记”参数。如果能在 3 秒内处理 1000 面墙,说明封装是有效的。
我自己的习惯是:每从官方案例里学到一个新 API,就立刻把它封装进RevitToolkit,然后写一个单元测试。这样半年下来,工具库覆盖了 80% 的日常需求,再遇到新项目时,直接调工具库,而不是翻 147 个案例。希望帮到你。
本文还有配套的精品资源,点击获取