做Revit算量插件的朋友应该都有过类似经历:造价同事拿着一个Excel列表跑来问,“你帮我看看,这个体积到底对应哪面墙?”“为什么这面墙算出来的面积和上个月对不上?”其实问题不在算量公式,而在算量结果缺了一个“可视化闭环”——数字算出来了,但用户看不见它对应模型中的哪个构件。
所以我在算量模块里补了一个构件预览功能:用户在算量结果列表里点选任意一行,Revit视图中的目标构件立刻高亮,右侧参数面板同步显示体积、面积、材质;如果觉得平面图不够直观,还能切到三维视图旋转查看。配上这个功能之后,核量效率提升非常明显。
这篇文章就把这个模块从需求拆解、技术选型到落地代码完整过一遍。重点围绕算量模块中的构件收集、参数提取、高亮标记、视图导航和三维预览展开,适合正在做Revit二次开发,尤其是涉及算量、造价、深化设计的开发者参考。
1. 需求解析与方案选型
1.1 构件预览到底在解决什么问题
把构件预览摆在算量模块里,首先得想清楚它解决的是“信任问题”,而不是“显示问题”。算量插件算出一堆数字,如果用户不能快速找到这些数字对应的模型对象,那就等于只做了一半交付。造价工程师拿到清单后,需要逐项核对构件是否被正确识别、材质是否取对、体积计算时扣减逻辑是否生效。这些核对动作都离不开构件的可视化定位。
我在实际开发中把预览拆成三个层次:定位、校验、复核。
- 定位:知道这个算量记录对应模型中哪个构件,点在哪儿,长什么样。
- 校验:确认算量规则真的应用到了目标构件上,比如墙体体积有没有扣掉门窗洞口、楼板面积有没有扣掉楼梯间空洞。
- 复核:把算量清单和模型对象对应起来之后,再结合楼层、区域等条件做人工抽查。
如果只做了算量汇总表但没做构件预览,前两个层次就完全是断的。用户只能拿一个数字反复去模型里翻找,效率低不说,还容易漏查。
1.2 三条技术路线怎么选
“预览”这个词本身有歧义,方案也完全不同。我整理过三条可落地的路线,对比之后才确定最终方向。
| 方案 | 实现思路 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 当前视图高亮 | 对构件设置OverrideGraphicSettings覆盖颜色和线宽 | 实现最快、数据直观、不动用新视图 | 平面视图中非本层构件不可见 | 绝大多数算量联动场景 |
| 临时三维视图 | 创建View3D并ShowElements隔离显示目标构件 | 立体感强、定位准确、空间关系一目了然 | 切换视图会打断用户当前操作流 | 需要看空间关系、复杂节点复核 |
| 外部渲染预览 | WPF窗口加载HelixToolkit等三维控件 | 独立窗口、视角自由、可做批量展示 | 开发量大,需要从Revit导出几何数据 | 对外展示、精美报告、高端算量演示 |
我最终选的是“主视图高亮 + 临时三维视图可选”的组合。原因很实在:开发成本可控,数据源始终是当前文档里的Element,不需要在Revit和外部渲染器之间同步几何,也不会出现两边数据不一致的问题。用户在表格里点一条记录,主视图立刻高亮;如果用户感觉平面视图限制大,再双击行自动进入临时三维视图,两个层级是递进关系,都不增加额外维护成本。
2. 算量数据准备:让预览有据可依
构件预览不能只做一个“空的高亮”,它必须把算量参数一起带出来。所以第一步不是写预览代码,而是把算量数据本身整理干净。
2.1 收集目标构件的正确姿势
Revit二次开发里收集构件最常用的就是FilteredElementCollector。它的优势在于可以叠加ElementFilter做复合过滤,比传统的遍历Categories再筛选快得多,代码也更清晰。
public List<Element> CollectQuantityTargets(Document doc) { FilteredElementCollector collector = new FilteredElementCollector(doc); ICollection<Element> walls = collector .OfClass(typeof(Wall)) .WhereElementIsNotElementType() .ToElements(); return walls.ToList(); }如果算量模块要同时覆盖墙、板、梁、柱这几个大类,直接按类过滤再Union也不复杂,但更好的做法是用LogicalOrFilter合并多个ElementFilter,一次WherePasses完成:
List<ElementFilter> filters = new List<ElementFilter> { new ElementClassFilter(typeof(Wall)), new ElementClassFilter(typeof(Floor)), new ElementCategoryFilter(BuiltInCategory.OST_StructuralColumns), new ElementCategoryFilter(BuiltInCategory.OST_StructuralFraming) }; LogicalOrFilter orFilter = new LogicalOrFilter(filters); FilteredElementCollector collector = new FilteredElementCollector(doc); ICollection<Element> candidates = collector .WherePasses(orFilter) .WhereElementIsNotElementType() .ToElements();这里有个细节,FamilyInstance里面既有结构柱、结构框架,也有门窗、家具、机械设备,所以用ElementClassFilter(typeof(FamilyInstance))不能直接了事,必须再叠加ElementCategoryFilter按内置类别收窄。否则算量清单里混入一个沙发族,预览高亮的时候会闹笑话。
还要加一层可见性判断。很多项目里模型会按阶段、按工作集控制构件的显示状态,如果用户当前视图根本看不到那个构件,预览时高亮也是白搭。可以先检查doc.GetElement(id).get_Visibility? 这个API在新版本中已经改名成IsHidden,或者简单一点,在收集阶段就增加一个condition:构件的参数或类别是当前视图中显示的。实在不行,就在预览说明里提示用户“请切到三维视图查看”。
2.2 提取算量参数与单位转换
算量模块的核心数据无非就是体积、面积、长度、材质这几类。Revit API中这些参数基本都能通过BuiltInParameter取到。有个非常容易踩的坑:Revit内部长度单位是英尺,体积是立方英尺,面积是平方英尺。直接拿内部值填到界面上,数值会小得离谱或者大得离谱。
建议统一做一个单位转换工具,把所有算量值在入库之前就转成项目常用的公制单位。转换要用UnitUtils类,Revit 2022之后接口还有变化。
public static double ConvertToCubicMeters(Document doc, double internalValue) { #if REVIT2022_OR_NEWER return UnitUtils.ConvertFromInternalUnits(internalValue, UnitTypeId.CubicMeters); #else return UnitUtils.ConvertFromInternalUnits(internalValue, DisplayUnitType.DUT_CUBIC_METERS); #endif }体积参数最常见的是BuiltInParameter.HOST_VOLUME_COMPUTED,这是Wall、Floor等宿主元素自带的“计算体积”。对于梁、柱这些族实例,有的参数名不一样,甚至不同项目模板里差异很大。最稳妥的做法是先判断参数是否存在,再决定取哪个:
public static double GetElementVolume(Element element) { Parameter volumeParam = element.get_Parameter(BuiltInParameter.HOST_VOLUME_COMPUTED); if (volumeParam == null) { volumeParam = element.get_Parameter(BuiltInParameter.RVT_MATERIAL_VOLUME); } if (volumeParam != null && volumeParam.HasValue) { return ConvertToCubicMeters(element.Document, volumeParam.AsDouble()); } return 0.0; }面积和长度也是同样的逻辑,用HOST_AREA_COMPUTED、HOST_AREA_COMPUTED对应面积,长度用CURVE_ELEM_LENGTH等参数。说到底,算量插件里“取参数”这部分一定要做DataCompleteness检查:有的构件没有体积参数,有的没有面积参数,不能假设每个元素都能返回有效值,宁可返回0并标记异常,也不要让整个预览窗口抛异常退出。
材质信息在构件预览里也很有用。造价人员经常关心“这面墙是混凝土还是加气块”。取材质的方式:
public static List<string> GetElementMaterialNames(Element element) { List<string> names = new List<string>(); ICollection<ElementId> materialIds = element.GetMaterialIds(false); Document doc = element.Document; foreach (ElementId id in materialIds) { Material material = doc.GetElement(id) as Material; if (material != null && !string.IsNullOrEmpty(material.Name)) { names.Add(material.Name); } } return names; }这里要注意GetMaterialIds方法在不同Revit版本上的行为基本稳定,但返回顺序不保证和物理结构一致。如果只想显示“主要材质”,可以按材质面积或体积排序,但真正算材质体积需要遍历Solid的面再做几何运算,代码量会多出不少。大多数算量预览场景下,把构件所有材质名列出来就够了,用户自己会看哪个是大头。
2.3 给每个构件建立“身份证”映射
算量列表里的“记录”和模型里的“构件”不总是一一对应的。常见情况是:按类型汇总算量——比如“200厚混凝土墙”归成一条记录,下面挂了十几面墙;或者按房间、按楼层汇总,也常常是多对一。
所以在设计数据结构时,我强烈建议用ElementId列表来承载“算量记录 → 构件集合”的映射:
public class QuantityRecord { public string CategoryName { get; set; } public string FamilyName { get; set; } public string TypeName { get; set; } public string Mark { get; set; } public string MaterialNames { get; set; } public double Volume { get; set; } public double Area { get; set; } public double Length { get; set; } public List<ElementId> ElementIds { get; set; } }ElementId在Revit单会话内是稳定的,做高亮、选中、视图导航都依赖它。为什么不用UniqueId?UniqueId跨文档稳定,但视图定位、选择集设置这些API全都认ElementId,每次都要转换反而麻烦。我倾向于在算量模块内部统一用ElementId为主键,UniqueId仅用于外部数据库关联或报告导出。
收集阶段就可以顺便把记录组装好:
public List<QuantityRecord> CollectQuantityRecords(Document doc) { List<QuantityRecord> records = new List<QuantityRecord>(); List<Element> targets = CollectQuantityTargets(doc); foreach (Element element in targets) { QuantityRecord record = new QuantityRecord { CategoryName = element.Category?.Name ?? "未知", FamilyName = GetFamilyName(element), TypeName = GetTypeName(element), Mark = element.get_Parameter(BuiltInParameter.ALL_MODEL_MARK)?.AsString() ?? "", MaterialNames = string.Join(", ", GetElementMaterialNames(element)), Volume = GetElementVolume(element), Area = GetElementArea(element), Length = GetElementLength(element), ElementIds = new List<ElementId> { element.Id } }; records.Add(record); } return records; }这里一步到位把构件信息和算量参数都塞进记录对象,后续做数据绑定和高亮联动会非常顺畅。预览界面绑定的就是这个记录集合。
3. 构件预览核心实现
3.1 列表联动:点击一行,高亮一屏
构件预览的交互核心是“列表选中 → 视图高亮”。我用WPF的DataGrid做结果列表,再用SelectionChanged事件触发高亮和参数面板刷新。
关键代码:
private void QuantityGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (QuantityGrid.SelectedItem is QuantityRecord record) { HighlightElements(record.ElementIds); ZoomToElements(record.ElementIds); ShowDetailPanel(record); } }高亮的操作必须放在Transaction里。Revit的视图覆盖图元样式属于文档变更,所以每次高亮都需要开个事务,即使它看起来只是“显示效果”。
private void HighlightElements(List<ElementId> ids) { using (Transaction trans = new Transaction(_doc)) { trans.Start("构件预览高亮"); _highlightManager.Highlight(ids); trans.Commit(); } }如果文档有外部改动或当前处于只读模式,事务开启可能失败,记得做try-catch。否则用户正在编辑其他命令时,弹出个“Transaction失败”的红色报错,体验很糟糕。
3.2 高亮标记与颜色还原:一个必须做成独立类的地方
高亮逻辑绝不能散落在窗口代码里。我单独写了一个HighlightManager类,职责只有两个:记录当前高亮了哪些构件、清空高亮状态。
public class HighlightManager { private readonly Document _doc; private readonly View _view; private readonly HashSet<ElementId> _currentIds = new HashSet<ElementId>(); private readonly Color _highlightColor = new Color(255, 0, 0); public HighlightManager(Document doc, View view) { _doc = doc; _view = view; } public void Highlight(IEnumerable<ElementId> ids) { ClearAll(); foreach (ElementId id in ids) { Element element = _doc.GetElement(id); if (element == null) { continue; } OverrideGraphicSettings overrides = new OverrideGraphicSettings(); overrides.SetProjectionLineColor(_highlightColor); overrides.SetProjectionLineWeight(6); overrides.SetCutLineColor(_highlightColor); overrides.SetCutLineWeight(6); _view.SetElementOverrides(id, overrides); _currentIds.Add(id); } } public void ClearAll() { foreach (ElementId id in _currentIds) { _view.SetElementOverrides(id, new OverrideGraphicSettings()); } _currentIds.Clear(); } }为什么投影线颜色和截断线颜色都要设置?这是踩过坑才明白的。在平面视图中,墙体被剖切后显示为截断线,如果你只设置了ProjectionLineColor,剖切部分仍然是原色,用户根本看不到高亮效果;而在立面视图中,墙体外轮廓显示为投影线,只设置截断线又不够。所以两个属性必须一起设置,不同视图模式下都能看出明显的红色轮廓。
高亮的还原一定不能省略。OverrideGraphicSettings如果不清理,就会一直覆盖在元素上,用户的视图会越看越“五彩斑斓”。HighlightManager在每次Highlight之前先ClearAll,保证任何时候只有一个算量记录处于高亮状态。
这里还要提醒一点:个别版本或视图模板会锁定图元覆盖。如果当前视图套了严格的视图模板并勾选了“模型类别替换”锁定,SetElementOverrides可能会无效或报错。遇到这种情况,最好是主动检测视图模板并把提示提示给用户,而不是默默失败。
3.3 视图导航与定位:怎么让用户一眼看到构件
高亮做好之后,还有一个很实际的问题:目标构件可能在屏幕外。如果用户点了一条记录,只是高亮了但屏幕上看不见,等于没做。
最简单的方案是用View.ShowElements方法。它会把传入的ElementId列表进行缩放定位,确保目标构件出现在当前视野内。
private void ZoomToElements(List<ElementId> ids) { View activeView = _uiDoc.ActiveView; if (activeView == null) { return; } activeView.ShowElements(ids); }ShowElements在大多数常规视图上都有效,包括平面、立面、三维。测试下来,如果构件不在当前视图的裁剪范围内,它也能自动调整视图范围。但如果当前视图的裁剪区域特别奇怪,或者构件分布跨多个楼层,ShowElements可能缩放得过于激烈。更可控的做法是自己合并BoundingBox:
private BoundingBoxXYZ UnionBounds(BoundingBoxXYZ a, BoundingBoxXYZ b) { if (a == null) return b; if (b == null) return a; XYZ min = new XYZ( Math.Min(a.Min.X, b.Min.X), Math.Min(a.Min.Y, b.Min.Y), Math.Min(a.Min.Z, b.Min.Z)); XYZ max = new XYZ( Math.Max(a.Max.X, b.Max.X), Math.Max(a.Max.Y, b.Max.Y), Math.Max(a.Max.Z, b.Max.Z)); return new BoundingBoxXYZ { Min = min, Max = max }; }然后把合并后的Box交给视图导航方法。不过在实际工程里,我发现最省事的还是ShowElements,Revit会自动处理裁剪区域和缩放比例,代码里少了两三百行。
如果想让用户更明确“这个构件被我选中了”,还可以同步设置选择集:
_uiDoc.Selection.SetElementIds(ids);这样在Revit状态栏和属性面板里也能看到对应构件的信息,预览效果更完整。
3.4 三维预览的轻量做法
有时候平面高亮还不够,因为多层构件叠在一起,平面视图里根本分不清上下。这时候就需要一个轻量三维预览方案。
最轻的路线是:用户当前已经在三维视图时,直接用ShowElements加高亮即可。更人性化的做法是自动创建一个临时三维视图,把目标构件隔离显示。
public View3D CreatePreviewView3D(Document doc, List<ElementId> elementIds) { View3D previewView = null; using (Transaction trans = new Transaction(doc)) { trans.Start("创建临时预览视图"); previewView = View3D.CreateIsometric(doc, ViewFamily.ThreeDimensional); previewView.Name = "算量构件预览"; // 部分版本支持将视图标记为临时,关闭文档时不保存 previewView.IsTemporary = true; previewView.ShowElements(elementIds); trans.Commit(); } return previewView; }注意,View3D.CreateIsometric在部分版本上需要设置Orientation才能得到合适的视角,否则默认等轴视角可能从底朝天看。可以手动设置:
XYZ eye = new XYZ(10, 10, 10); XYZ up = new XYZ(0, 0, 1); XYZ forward = new XYZ(-1, -1, -1); previewView.SetOrientation(new ViewOrientation3D(eye, up, forward));这个视线的选择看项目习惯。有的团队喜欢从西南等轴看,有的喜欢从东北等轴看。测试时多试几个方向,选一个用户反馈最顺眼的。
还有一点非常重要:临时三维视图创建完,用户看完之后要能一键还原。可以在预览窗口上加一个“关闭预览视图”按钮,遍历文档里所有名称以“算量构件预览”开头的视图,逐个删除。
public void RemovePreviewViews(Document doc) { FilteredElementCollector collector = new FilteredElementCollector(doc); ICollection<View3D> views = collector.OfClass(typeof(View3D)).Cast<View3D>() .Where(v => v.Name.StartsWith("算量构件预览")) .ToList(); using (Transaction trans = new Transaction(doc)) { trans.Start("清理预览视图"); foreach (View3D view in views) { doc.Delete(view.Id); } trans.Commit(); } }如果创建后忘了清理,模型里会堆一堆没用的临时视图,交付给甲方时显得很不专业。
4. 常见问题与避坑指南
4.1 预览/高亮问题的速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 高亮后构件颜色没变化 | 视图中存在视图模板或视图过滤器覆盖了图元样式 | 检查视图模板设置,或提示用户切换到无模板的视图 |
| 切换记录时旧高亮还在 | 没有在Highlight之前执行ClearAll | 统一用HighlightManager管理,在每次高亮前清空旧集合 |
| 平面视图中高亮不可见 | 构件不在当前平面图元所属楼层或裁剪范围内 | 自动提示切换到三维视图,或先用ShowElements缩放 |
| 高亮颜色太淡看不清 | 只设置了ProjectionLineColor没有设置CutLineColor | 两个颜色属性都要设置,平面视图才能显示清晰 |
| 构件在屏幕外但没自动跳转 | 只做了高亮没做导航 | 高亮后调用ShowElements或合并包围盒缩放 |
| 点击多处记录后视图卡顿 | 高亮构件的OverrideGraphicSettings数量过多 | 限制单次高亮数量、按需分批展示、考虑使用Isolate临时隔离 |
4.2 隐藏的单位与参数版本坑
Revit API内部单位是英尺、平方英尺、立方英尺,这个前面说了,但还是要再强调一次:所有提取到的算量值在显示给用户之前都要做UnitUtils转换。有一阵子我在界面上直接显示了内部值,结果一张500平米的地板显示成5382平方英尺,用户立刻觉得插件坏了。
参数名称的差异也是大问题。不同版本、不同项目模板下,同一个构件的体积参数可能在不同名称之间变化。更稳妥的做法是优先用BuiltInParameter枚举,其次是按参数名查找,两个都找不到时再返回0。
版本差异方面,Revit 2022之后UnitUtils类结构变化比较大,很多老代码直接编译不过。如果插件要同时支持2020和2024,建议用条件编译:
#if REVIT2022_OR_NEWER double volume = UnitUtils.ConvertFromInternalUnits(val, UnitTypeId.CubicMeters); #else double volume = UnitUtils.ConvertFromInternalUnits(val, DisplayUnitType.DUT_CUBIC_METERS); #endif这个“REVIT2022_OR_NEWER”前缀是C#项目条件编译符号,需要你在插件项目里根据不同Revit版本定义好。如果嫌麻烦,也可以做成运行时反射调用,但那样代码可读性差很多,我不推荐。
GetMaterialIds方法在多个Revit版本里表现稳定,但老版本Material类是继承自Element的,新版本接口变化后依然能强制转成Material。如果遇到个别材质无法解析,直接跳过就可以了,不要因为一条材质名抛异常终止整个预览。
4.3 性能优化:一次高亮一千个构件怎么办
用户按类型汇总算量时,一条记录可能对应上百甚至上千个构件。如果一次性对1000个ElementId逐个设置OverrideGraphicSettings,体验会很卡。
两个实际优化手段:
第一个,把“高亮数量”限制住。界面上一行“当前选中N个构件”,如果N大于200,就先展示200个,其余在状态栏提示“其余构件未高亮,请放大范围或分批查看”。这不是偷懒,而是为了保持视图操作的流畅性。算量核对本来就是逐批看的过程,一次给用户200个高亮构件,足够判断汇总趋势。
第二个,用Transaction把批量覆盖合并到一个事务中。虽然逐元素SetElementOverrides本身不重,但频繁开事务的开销不小。我上面Highlight方法里已经把整个循环包在一个Transaction里,多个元素的高亮就只提交一次事务,实测性能明显改善。
还有一个技巧:高亮时把非目标构件整体变半透明,而不是把目标构件高亮。这种方法在“看空间关系”时效果极好,但代价是要遍历视图里所有可见元素再逐一覆盖透明度,性能开销更大。我只在预览构件很少(比如1-10个)时提供这个选项,默认关闭。
5. 我的实际体会与后续扩展方向
构件预览这个功能,代码量说多不多,说少也不少,但它在整个算量模块里的价值其实是“信任放大器”。算量结果能不能让用户信,很大程度上取决于能不能让用户亲手点到每一个构件去确认。预览功能上线后,一个明显的变化是,造价同事再拿着Excel来找我的次数少了很多,因为他们自己就能在模型里核对数字了。
有几个小建议可以分享:
第一,把高亮管理、视图导航、临时视图清理这些逻辑封装成公共组件,不要散落在某个窗口的代码里。后续不管是做算量明细、进度模拟还是碰撞报告预览,都能直接复用,会很省时间。
第二,做构件预览时,尽量让用户“一条路径走完”。从表格选行、视图高亮、参数面板更新到切三维视图,全过程最好都能用鼠标完成,不要频繁让用户去手动画视图、选视图、改视图范围。这个交互流畅度直接决定了功能有没有人用。
第三,后续扩展方向很多。比如按楼层加入过滤条件,算量清单里加一列“楼层”,点击后只高亮该楼层构件;再比如按施工段、按分区汇总算量,预览时一键把整个分区的构件用颜色区分开来;还有导出报告时,把预览截图直接嵌到Excel或PDF里,这样图纸上每一笔算量都有据可查。
最后说一句比较实在的:不要把构件预览想得太复杂。很多团队一上来就试图搞一个独立三维浏览器窗口,结果开发周期拉到了几个月。实际上,把Revit视图内的高亮和导航做好,已经能满足80%以上的核量需求。先把这80%做得顺手、稳定、不卡,再考虑要不要上外部渲染方案。我做了这么久,最深的体会就是:好用的算量预览,不是三维效果多炫,而是用户点一下就能看见、能看懂、能核对。