
第一次写这系列笔记的时候我就想过一个问题Revit 二次开发里大家问得最多的一个点是什么不是创建图元也不是布置族实例而是“我怎么才能拿到我想要的图元”。而这个问题的一切答案基本都绕不开过滤器。过滤器之于 Revit 二次开发就相当于 SQL 的 WHERE 条件之于数据库查询你不会它等于还没入门。这篇是“Revit二次开发”系列的第一篇专门聊聊过滤器。我尽量把 FilteredElementCollector、ElementFilter 这一大家子讲透配合 C# 代码示例和实际项目里的坑给刚开始写 Revit 插件的朋友一份能直接抄作业的笔记。适合刚接触 Revit API、被图元遍历搞到崩溃的开发者也适合打算系统梳理过滤器的朋友当目录参考。1. 为什么 Revit 二次开发绕不开过滤器1.1 一个初学者的灵魂拷问你第一次写 Revit 插件十有八九是这么开始的新建一个外部命令然后想把这个项目里所有的墙体捞出来。于是你打开文档看到 FilteredElementCollector复制一行示例代码跑通了很兴奋。但接下来你要按用途筛选、按参数筛选、把某个区域内的图元剔除马上就卡住了。我刚开始学的时候习惯用一个特别笨的办法拿到所有 Element然后 foreach 一个个判断用 if 去筛。数据量小的时候还行一旦项目上了几百兆楼上楼下一堆机电管线那个遍历速度直接劝退用户。后来我认真把过滤器体系过了一遍才发现这玩意儿的设计比我最初的印象要讲究得多。这里先说个结论Revit 的过滤器本质上是一套为“高效筛选图元”设计的决策器。你告诉它“要什么”“不要什么”它用内部优化过的规则去匹配而不是把所有图元加载到内存里再靠你一个个跑业务判断。1.2 过滤器是什么不是什么先理清概念。在 Revit API 里有两个容易混淆的名字FilteredElementCollector这是“收集器”你可以理解为一个大筛子入口。它负责定义“从哪里筛”可选当前文档、某个视图或者某些链接模型的元素集合。ElementFilter类及其子类这是“过滤器”决定“筛什么”。比如ElementClassFilter筛类型ElementCategoryFilter筛类别ElementParameterFilter筛参数规则。实际使用顺序永远是Collector 决定数据源Filter 决定筛选条件最后用.WherePasses()把条件挂上去或者用OfClass()这类快捷方法直接筛。还有一个很多人都忽略的点FilteredElementCollector不一定只能拿全文档元素。你可以让你收集器只作用在当前视图上直接过滤掉不可见图元这种场景在构件统计、报验批次管理里特别常见。类似“只看得到的”“只在这个房间里的”都可以用 collector 作用域来实现。2. 过滤器的核心分类与设计逻辑2.1 三大家族快速、慢速、逻辑刚开始看 API 文档ElementFilter 下面一堆类很容易晕。我自己的分类习惯是把它分成三组快速过滤器不打开 Element直接判断类、类别、是否匹配某个唯一 Id。比如ElementClassFilter、ElementCategoryFilter、ElementIdFilter。它们快是因为 Revit 内部本来就有这些索引信息。慢速过滤器需要读取元素的某些数据比如ElementParameterFilter要遍历图元并读取参数值BoundingBoxIntersectsFilter要判断几何包围盒。不是不能用而是要明白它们相对耗时。逻辑过滤器用来组合上面两类形成“与、或、非”的判断。比如LogicalAndFilter、LogicalOrFilter、LogicalNotFilter。这个在你需要复合条件时比你以为的把元素取出来再用 if 判断要高效得多。这个结构其实很像认识一个人快速过滤器就像先看外表慢速过滤器就像再看履历逻辑过滤器则决定你到底是“要 A 又要 B”“A 或 B 都行”还是“只要不要 A 的”。三者组合起来才是你在项目里真正干活的样子。2.2 命中还是排除过滤器都在做判断题过滤器所有子类最终只回答一个问题这个图元要不要所以当你自己定义一个过滤器类继承ElementFilter其实只需要重写一个方法bool Passes(Element element)然后加上一个可选的判断条件。但在实际项目里绝大多数场景直接用现成子类就够了不需要造轮子。这里有个反直觉的细节WherePasses()并不直接在 C# 里返回一个 List而是返回一个 FilteredElementCollector。这个收集器是“惰性”的它到你真正开始遍历比如.ToList()、.FirstOrDefault()的时候才真正执行筛选。所以不要以为随便调用 WherePasses 就会立刻把全项目跑一遍它是可以被继续链式调用的。等你确定要取结果了再来一次迭代它才真正工作。提示能快速过滤器解决的就不要上慢速过滤器。这不仅是性能问题更关系到插件在大项目里的稳定性。快速过滤器失败不会导致 Revit 崩溃但慢速过滤器在大数据量下可能让你等得怀疑人生。3. 常用过滤器实操代码解析3.1 按类别的三件套Revit 二次开发里最常用的一个快速筛选操作是“按类别找图元”。比如我想拿到所有墙直接FilteredElementCollector collector new FilteredElementCollector(doc); ICollectionElement walls collector .OfClass(typeof(Wall)) .CastWall() .ToList();这个方法本质是内部创建了ElementClassFilter(typeof(Wall))你用起来简单但要知道它在干什么。OfClass(typeof(Wall))会找出所有 wall 类型的实例包括基于系统族、基于载入族创建的墙。但有时候你要的是“某个类别”而不是“某个类”。比如你要拿所有“门”门的 API 类型是FamilyInstance同时还想拿门所在的族类型信息。用 OfClass 的话拿到的只是 FamilyInstance你还得自己再去取类型和族。这时候用类别过滤器会更直接ElementCategoryFilter doorCategoryFilter new ElementCategoryFilter(BuiltInCategory.OST_Doors); FilteredElementCollector doorCollector new FilteredElementCollector(doc); ICollectionElement doors doorCollector .WherePasses(doorCategoryFilter) .ToList();这段代码筛出所有类别为“门”的图元。它不关心门具体是哪个类实现的FamilyInstance、IndependentTag等等只要类别匹配都会进来。所以做“族实例统计”类的工具时用ElementCategoryFilter往往比OfClass更省心。还有一个经常被提的组合先按类拿基础对象再按类别排除一些干扰项。比如我要项目里所有柱子但我不想要“建筑柱”里面的某些结构连接件那就可以FilteredElementCollector columnCollector new FilteredElementCollector(doc); ElementCategoryFilter columnCatFilter new ElementCategoryFilter(BuiltInCategory.OST_StructuralColumns); ICollectionElement columns columnCollector .WherePasses(columnCatFilter) .WherePasses(new ElementClassFilter(typeof(FamilyInstance))) .ToList();你可能会奇怪ElementCategoryFilter不是已经能筛出结构柱了吗为什么还要再按 FamilyInstance 筛一次因为有些类别下Revit 内部会包含一些“类”不同的辅助图元。再叠加一个类过滤器等于把范围逼近到“结构柱类别 FamilyInstance”两者同时满足而不是 shift 或 或的关系。这是我在做钢筋统计工具时踩过坑后总结出来的纪律能同时用快速过滤器缩窄范围绝对不要相信自己后续的 if 兜底。3.2 参数过滤器很多时候按类别拿完还远远不够。例如我们要统计“所有宽度为 900 的门”。传统思路是把门全取出来逐个 read 参数再判断。慢而且代码丑。Revit 提供的正式方式是用ElementParameterFilter。// 定义参数过滤器宽度 900mm ParameterValueProvider provider new ParameterValueProvider(new ElementId((int)BuiltInParameter.DOOR_WIDTH)); FilterNumericEquals rule new FilterNumericEquals(provider, 900 / 304.8, 1e-6); ElementParameterFilter widthFilter new ElementParameterFilter(rule); FilteredElementCollector doorCollector new FilteredElementCollector(doc); ICollectionElement wideDoors doorCollector .WherePasses(new ElementCategoryFilter(BuiltInCategory.OST_Doors)) .WherePasses(widthFilter) .ToList();这段要注意三个点第一DOOR_WIDTH返回的参数值单位是英尺feet而用户界面上看到的是毫米。你必须手动除以 304.8否则你的 900 拿不到任何结果而且运行时不报错。这种 bug 特别恶心因为看起来逻辑完全没问题。如果懒得记直接用UnitUtils.ConvertToInternalUnits(900, UnitTypeId.Millimeters)转换。第二FilterNumericEquals构造函数里有个精度参数。Revit 内部是 double 计算用 1e-6 就是用来容忍浮点误差。等你实际用的时候如果发现该筛出来的没筛出来首先检查是不是精度设得太严格。第三ElementParameterFilter是慢速过滤器。它不是等参数已经加载好再判断而是运行时去读每个元素的参数值。这意味着如果你在第一步已经用 OfClass 拿到很小范围了还勉强能接受如果你直接全文档用参数过滤器去筛所有图元那个性能损耗是非常明显的。实操心得做参数过滤前先用快速过滤器缩小范围到必要的类别上再挂参数过滤器。比如“先筛门再按参数筛宽度”这比“直接按宽度参数筛所有元素”快很多。别把参数过滤器当万能钥匙用。3.3 逻辑组合与排除实际业务里很少只有单一条件。比如你要找“所有的幕墙嵌板但不包含门窗嵌板”。这种场景用逻辑过滤器最优雅。// 要幕墙嵌板 ElementCategoryFilter curtainPanelFilter new ElementCategoryFilter(BuiltInCategory.OST_CurtainWallPanels); // 不要门嵌板 ElementCategoryFilter doorPanelFilter new ElementCategoryFilter(BuiltInCategory.OST_Doors); LogicalNotFilter notDoorFilter new LogicalNotFilter(doorPanelFilter); LogicalAndFilter finalFilter new LogicalAndFilter(curtainPanelFilter, notDoorFilter); FilteredElementCollector collector new FilteredElementCollector(doc); ICollectionElement finalPanels collector .WherePasses(finalFilter) .ToList();LogicalAndFilter支持把多个过滤器合成一个但这里如果你直接给它传数组数组里每个过滤器会被 AND 连接。如果你想要“A 或 B”就用LogicalOrFilter。再补充一个细节ExclusionFilter专门用来做“排除某组 Id”。比如你前面收集一堆元素想排除其中一些手选出来的重复项ICollectionElementId excludedIds new ListElementId { id1, id2 }; ExclusionFilter excludingFilter new ExclusionFilter(excludedIds); FilteredElementCollector collector new FilteredElementCollector(doc); ICollectionElement filteredElements collector .WherePasses(new ElementCategoryFilter(BuiltInCategory.OST_Walls)) .WherePasses(excludingFilter) .ToList();这个在高亮选中、批量操作、差异对比场景里很好用它避免了你写很多“if (excludedIds.Contains(element.Id))”这种代码。3.4 空间范围过滤还有一个经常遇到的需求找个区域内的图元。比如框选一个范围、按某个房间范围统计电器设备。这时候就要用BoundingBoxIntersectsFilter或BoundingBoxIsInsideFilter。// 定义范围假设是一个 10m x 10m x 3m 的立方体 XYZ min new XYZ(0, 0, 0); XYZ max new XYZ(10, 10, 3); // 注意单位也是英尺 Outline outline new Outline(min, max); BoundingBoxIntersectsFilter bboxFilter new BoundingBoxIntersectsFilter(outline); FilteredElementCollector collector new FilteredElementCollector(doc); ICollectionElement intersectedElements collector .WherePasses(bboxFilter) .ToList();这里要搞清楚两个过滤器的区别BoundingBoxIntersectsFilter只要元素的包围盒和给定范围有交集就通过。适合“范围框选”的场景碰到边缘也算。BoundingBoxIsInsideFilter要求元素整个包围盒都在给定范围内才算通过。适合统计严格位于盒子内的设备。两者的边界情况差异非常明显实际项目中你用方框圈一批构件时发现多了或少了多半是选错了这两个过滤器。还有一个注意点Revit 的BoundingBox是轴对齐包围盒不考虑图元朝向。旋转过的墙或者斜梁包围盒会大于实际几何体。如果你的筛选要求很精确需要配合几何相交检测二次确认。4. 组装一个真实场景批量提取项目中的门信息4.1 需求拆解与流程设计讲了这么多单个过滤器来一个综合场景小工具“门表速查”。需求很简单把当前项目中所有门按“楼层、门编号、宽度、高度、类型名称”输出到一个文本或 Excel 表格。听起来不难但实现过程中会用到好几类过滤器顺序很关键。我的流程是这样的用类别过滤器OST_Doors拿到所有门。用类过滤器FamilyInstance再缩一档排除掉类别里的其他杂项元素。对拿到的门集合按某个范围比如只看当前楼层平面视图再过滤避免输出一堆别的楼层的门。逐个读取参数门编号、宽度、高度、类型名称。输出并排序。第二步和第三步的顺序很重要先类别再类再空间。每一步都把候选集缩小后面的参数读取就快。4.2 代码实现public void ExportDoors(Document doc, string outputPath) { // 第一步类别过滤器拿到所有门 ElementCategoryFilter doorCategory new ElementCategoryFilter(BuiltInCategory.OST_Doors); // 第二步类过滤器限定 FamilyInstance ElementClassFilter familyInstanceClass new ElementClassFilter(typeof(FamilyInstance)); LogicalAndFilter doorScopeFilter new LogicalAndFilter(doorCategory, familyInstanceClass); FilteredElementCollector collector new FilteredElementCollector(doc); // 第三步先拿到初步的门的集合 ICollectionElement doors collector .WherePasses(doorScopeFilter) .ToList(); ListDictionarystring, string rows new ListDictionarystring, string(); foreach (Element door in doors) { FamilyInstance instance door as FamilyInstance; if (instance null) continue; string mark instance.get_Parameter(BuiltInParameter.ALL_MODEL_MARK)?.AsString() ?? ; double width instance.get_Parameter(BuiltInParameter.DOOR_WIDTH)?.AsDouble() ?? 0; double height instance.get_Parameter(BuiltInParameter.DOOR_HEIGHT)?.AsDouble() ?? 0; double widthMM UnitUtils.ConvertFromInternalUnits(width, UnitTypeId.Millimeters); double heightMM UnitUtils.ConvertFromInternalUnits(height, UnitTypeId.Millimeters); ElementType type instance.Document.GetElement(instance.GetTypeId()) as ElementType; string typeName type?.Name ?? ; rows.Add(new Dictionarystring, string { { mark, mark }, { width, widthMM.ToString(F0) }, { height, heightMM.ToString(F0) }, { type, typeName } }); } // 假定 rows 后续写入文件这里略 }这里值得注意的有几个ALL_MODEL_MARK就是构件“标记”参数一般用来存门编号。它不是所有项目都填了所以做工具时最好留个空值兜底。读取宽度高度时我显式用ConvertFromInternalUnits转成毫米否则输出一堆英尺小数表格没法看。GetTypeId()是FamilyInstance的方法可以拿到实例对应的类型 Id然后通过doc.GetElement拿到ElementType。这个套路在处理族实例时非常通用。我在循环里没有直接做二次空间过滤因为实际工具里通常会把“楼层”作为参数条件而不是用包围盒。但如果你的目标视图本身是激活的工作平面视图也可以用视图作用域的 Collector 一步到位。4.3 一个性能体感对比曾经在某项目上模型三百多兆几十个楼层总门数不多也就两千。我一开始偷懒想用一次性new FilteredElementCollector(doc).WherePasses(doorScopeFilter)拿所有门后面再逐层处理。实测结果一次遍历几毫秒就回来了非常快。真正慢的反而是后面的参数读取每扇门要读三个参数、取一次类型。两千扇门加起来也就 200-300 毫秒这个速度完全可接受。但如果你想全文档遍历所有图元再逐个判断 Element.Name 或者判断参数是否存在那基本就是几秒甚至十几秒的体验差距。这就是过滤器的价值所在——它把“查询”和“数据加工”两个阶段的成本分得非常清楚。5. 避坑实录我踩过的过滤器相关坑5.1 快速过滤器判断不了参数我记得有一次写个工具想筛出所有“含有某个扩展数据的墙”。我用的第一个版本是FilteredElementCollector collector new FilteredElementCollector(doc); ICollectionElement walls collector .OfClass(typeof(Wall)) .WherePasses(new ElementParameterFilter(rule)) .ToList();问题出在rule里我用了一个共享参数这个参数在某些墙上根本没被添加过。结果是我期望“没有该参数的墙被排除”实际上 Revit 对“参数不存在”和“参数值为空”的处理方式会让人意外。ElementParameterFilter 对不存在的参数它不会直接返回 false而是可能根据规则类型走“无参数也算一种状态”的逻辑。后来我的解决方案很土先快速过滤再在循环里手动判断该参数是否存在。foreach (Wall wall in walls) { Parameter targetParam wall.get_Parameter(sharedParamGuid); if (targetParam null) continue; // 处理 }这不是过滤器不好而是我理解错了它的语义。过滤器的职责是“按规则筛选”不是“按业务兜底”。你需要兜底的行为老老实实在循环里做。5.2 当心过滤器和事务的配合如果你在事务里改了某些参数然后用 FilteredElementCollector 再去取元素注意过滤器的结果可能不会实时反映未提交的事务修改。很多时候需要做完修改后再 new 一个新的 collector或者调用doc.Regenerate()后重新收集。我记得有一次在批量替换门的编号代码流程是收集所有门 → 循环修改 mark → 再收集所有门去验证。结果验证那步拿到的还是旧数据害得我排查了半天。最后先doc.Regenerate()再新建 Collector就正常了。注意把“收集图元”和“修改图元”分开两个阶段写修改完如果需要同批数据再操作一定重新收集别复用旧集合。5.3 空间过滤器的边界情况前面提到BoundingBoxIntersectsFilter和BoundingBoxIsInsideFilter的边界差异。我在写某项目的机电管线统计时用包围盒过滤风管结果发现有些明明在盒子内的风管没被筛进来。后来一查风管的系统族实例的 BoundingBox 可能是按管线连接件的外包围盒来算的而不是按实际的几何中线。斜向管路尤其严重。解决办法倒也不复杂空间过滤只作为第一轮粗筛第二轮用元素的get_Geometry或 Solid 相交判断再来一次精确验证。这笔账很划算第一轮快速缩小范围第二轮精确处理总时间比直接全量几何碰撞小得多。5.4 类别与类的混用会把自己坑哭有人写了这么多代码collector.OfClass(typeof(Wall)) .OfCategory(BuiltInCategory.OST_Walls) .ToList();这看起来是双重保证实际不会出问题但很容易让人误以为OfCategory比WherePasses(ElementCategoryFilter)有什么额外效果。两者行为基本一致。同样地有些人用WherePasses(new ElementClassFilter(typeof(FamilyInstance)))来筛门但把“没有门类别”也一起筛出来了。因为 FamilyInstance 类涵盖墙上的嵌板、柱、独立标记等一大堆东西你后面必须再用类别过滤收窄。把类过滤和类别过滤的顺序、含义都搞明白就不会犯这种错。我自己写项目的固定写法是先按类别快速过滤。再按类过滤往里收一层。如果条件需要参数、范围等再挂慢速过滤。索引级别的筛选全部在 collector 上做循环里只做数据加工。这样做下来插件稳定不容易漏也方便半年后自己回来看懂。5.5 版本差异带来的小心思不同的 Revit 版本里某些 BuiltInParameter 枚举有过变更比如DOOR_WIDTH、DOOR_HEIGHT这类参数在老版本存在新版本里可能废弃但还能用。你在新版本编译时的 warning 别忽略最好打开 API 文档看一眼替代参数。实际工作中我见过有人在一台机器上把插件编译通过换到项目方的 Revit 版本上一运行就崩原因之一就是用了当前环境不存在的 BuiltInParameter。所以多版本兼容的插件建议专门做一层“参数映射表”把旧枚举映射到新枚举运行时根据产品版本动态取。6. 一些顺手的小建议关于过滤器最后再补充几个不是过滤器本身、但直接影响你使用体验的小经验先确认要操作的文档和视图。如果你只想处理当前可见图元直接在 FilteredElementCollector 构造里传 view相比先收集全文档再逐个判断 IsHidden 要高效太多。我自己写“当前视图标记尺寸检查”工具时就是这么干的速度快得让甲方以为是假插件。元素 Id 的过滤很有用但容易被忽略。做联动选择、联动高亮时经常需要“拿一批 Id 对应的元素”。直接用ElementIdFilter传一个ICollectionElementId比循环取 Id 再 GetElement 快不少也省代码。养成看过滤结果计数的习惯。调试插件的时候每次 WherePasses 之后先看一下集合数量。数量在预期范围内再继续往下写逻辑。如果第一层就筛出离谱的数量趁早回头检查条件别等后面调试得焦头烂额才发现第一步就错了。我在实际项目里用过很多次过滤器组合以后最大的体会是Revit 二次开发的问题百分之六七十不是“代码怎么写”而是“你希望捞到什么、不应该捞到什么”定义清楚。过滤器恰好是逼着你思考这个问题的工具。你越能精确描述筛选目标插件的表现就越稳、越可控。下一篇笔记我会接着写图元几何操作到时候会大量复用今天讲的过滤器。先把这一步踩实了后面就顺了。