1. 写在前面:这个功能解决什么问题
搞过Revit二次开发的朋友应该都有过这种经历:写插件处理完模型,最后想给用户展示效果,结果用户还得自己手动去项目浏览器里翻视图、找剖面、再双击打开。一次两次还好,频繁切换视图的时候,体验是真的差。
我之前做一个机电管综项目的时候就踩过这个坑。当时写了个净高分析的插件,分析完需要让用户直接看到剖面效果,最开始只生成剖面却不管跳转,结果客户反馈“插件倒是生成了剖面,但我在哪看呀”。后来我加了视图切换的逻辑,一键从平面切到剖面,反馈立刻好了很多。
今天这篇就围绕“[Revit二次开发] 创建剖面并将视图切换至剖面”这个主题,把整个实现过程拆开揉碎了讲清楚。从API选型、参数计算,到实操代码、常见坑点,一次性说透。
先交代下这篇文章适合谁看:正在学Revit二次开发的新手,刚接触View/ViewSection相关API的开发者,以及做机电、建筑、结构插件时被视图切换问题卡住的朋友。文章既讲怎么实现,也讲为什么这样写,希望能帮你少走弯路。
2. 场景分析与总体设计思路
2.1 为什么需要“创建剖面 + 自动切换”
从用户角度来说,插件生成剖面后自动跳转,算是“所见即所得”的基本要求。用户不关心你背后调了什么API,他只关心:点一下按钮,能不能立刻看到剖面效果。如果不能,他就觉得这个插件不好用。
从开发者角度来说,生成剖面这个动作本身并不难,难的是两件事:
- 剖面的位置和方向怎么定。很多新手直接拿轴线或者任意两个点生成剖面,结果剖面切出来的位置根本不是用户想看的位置,要么切空,要么角度歪了。
- 生成剖面后,ViewSheet和Viewport的关系、视图是否被使用、是否添加到图纸,这些状态处理不当,会导致视图切换时出现“视图不可见”或“无法打开”的诡异问题。
所以我通常把整个需求拆成三步走:
- 确定剖面位置、方向和范围(核心是裁剪框计算)。
- 调用API创建剖面视图(核心是ViewSection.Create)。
- 将指定视图切换为当前活动视图(核心是UIApplication.ActiveUIDocument.View)。
这三步是递进关系,每一步都有不少细节。下面逐个展开。
2.2 技术选型:看API怎么选比较稳
Revit二次开发中,创建剖面最常用的是ViewSection类。它继承自View,专门表示剖面、立面和三维视图中的剖面视图。创建方法如下:
public static ViewSection Create( Document document, ElementId viewFamilyTypeId, ElementId viewFamilyId, ElementId sectionBoxId )参数含义分别是:
document:当前文档。viewFamilyTypeId:视图族类型Id,通常是剖面视图的族类型。viewFamilyId:视图族Id,剖面视图对应的族是ViewFamily.Section。sectionBoxId:SectionBox元素的Id,这个SectionBox用来定义剖面的范围、方向和裁剪框。
这个API看起来简单,但实际使用时,新手最常踩的坑就是viewFamilyTypeId和sectionBoxId不知道从哪来。
如果你直接在ViewSection.Create里传ElementId.InvalidElementId,多半会报错或者生成一个无法使用的视图。正确的做法是先查找到剖面视图的族类型,再用BoundingBoxXYZ创建SectionBox。
关于sectionBox,一句话解释:它就是一个带坐标系的立方体,定义了剖面视图的裁剪范围、观察方向、以及剖切深度。你可以把它理解成“相机取景框”,所有可见性都由这个框决定。
2.3 方案选型背后的思考
我在实现这个功能时,对比过两种思路:
方案A:直接创建ViewSection然后切换视图
ViewSection section = ViewSection.Create(doc, viewFamilyTypeId, ViewFamily.Section, sectionBoxId); uiApp.ActiveUIDocument.View = section;优势是代码量少、逻辑清晰,适合简单场景。劣势是剖切范围固定,后期如果想调整裁剪框,还得再写代码。
方案B:确定好裁剪框再创建,创建后立刻控制视图可见性
// 这里提前把裁剪框、视图族类型都准备好 // 创建视图 // 设置视图显示样式或示意剖切位置方案B的优势是代码可扩展性强,适合需要参数化调整的场景。劣势是前期计算量多,尤其是裁剪框的变换矩阵,写起来容易绕晕。
我在实际项目里综合采用了两种方案的优点:先用方案A的简单写法验证方向是否正确,确认无误后再用方案B细化裁剪框和视图设置。这个习惯建议你也保留,先跑通再优化,不然一上来就写复杂的矩阵变换,调试效率太低。
3. 核心细节解析:从裁剪框到视图切换
3.1 裁剪框(BoundingBoxXYZ)的构造逻辑
BoundingBoxXYZ是创建剖面的灵魂,它包含三个关键属性:
Min:裁剪框最小点。Max:裁剪框最大点。Transform:裁剪框坐标系变换矩阵。
这三个组合在一起,定义了一个“有方向的立方体”。Transform里的BasisX、BasisY、BasisZ分别代表剖面的三个轴方向,Origin代表剖面的原点。
我画个简单的类比帮大家理解:你拿手机拍照时,Transform的BasisX相当于镜头朝右的方向,BasisY相当于镜头朝上的方向,BasisZ相当于镜头正对的方向,Origin就是镜头所在的位置。Min和Max则是你变焦后看到的视野范围。这个类比理解到位了,剖面方向和裁剪范围的计算就不难了。
3.2 如何确定剖面线方向和位置
很多时候,我们希望剖面垂直于某条轴线或者某个构件边缘。这时可以通过两个已知点来构造方向向量。
假设我们有startPoint和endPoint两个点,剖面方向要垂直于这两个点的连线,那么剖面视图的裁剪框可以这么设定:
XYZ direction = (endPoint - startPoint).Normalize(); XYZ right = new XYZ(direction.Y, direction.X, 0); // 注意这里是横纵坐标交换,得到垂直方向注意这个细节:new XYZ(direction.Y, direction.X, 0),得到的是和direction垂直但在XY平面内的向量。如果你想得到逆时针旋转90度的向量,则用new XYZ(-direction.Y, direction.X, 0)。
我自己写的时候习惯封装一个CreateSectionBoxBetweenTwoPoints方法,传入起点、终点、视图深度、视图宽度,返回BoundingBoxXYZ。下面这段代码是我实际项目里的简化写法,你们可以参考:
private BoundingBoxXYZ CreateSectionBox(XYZ startPoint, XYZ endPoint, double width, double depth) { XYZ dir = (endPoint - startPoint).Normalize(); XYZ right = new XYZ(-dir.Y, dir.X, 0); // 逆时针旋转90度 BoundingBoxXYZ box = new BoundingBoxXYZ(); Transform tf = Transform.Identity; tf.BasisX = right; tf.BasisY = XYZ.BasisZ; tf.BasisZ = dir; tf.Origin = startPoint; box.Transform = tf; box.Min = new XYZ(-width / 2, -depth / 2, 0); box.Max = new XYZ(width / 2, depth / 2, 30); // 高度范围按需设置 return box; }这里有个容易混淆的点:Transform.BasisZ是剖面的观察方向。很多人以为BasisZ要垂直于剖面,实际上BasisZ恰恰是剖面视图的视线方向,BasisX才是剖面线的水平方向。这个搞反了,生成出来的剖面会歪。
3.3 剖面视图族类型的查找与选择
创建ViewSection前,需要先找到一个可用的剖面视图族类型。一般通过FilteredElementCollector和ViewFamily过滤:
private ElementId FindSectionViewFamilyType(Document doc) { FilteredElementCollector collector = new FilteredElementCollector(doc); ICollection<Element> viewTypes = collector .OfClass(typeof(ViewFamilyType)) .Cast<ViewFamilyType>() .Where(vft => vft.ViewFamily == ViewFamily.Section) .Cast<Element>() .ToList(); if (viewTypes.Count > 0) return viewTypes.First().Id; return ElementId.InvalidElementId; }之所以要Cast<Element>,是因为后续调用ViewSection.Create需要传ElementId,而有些老版本API对ViewFamilyType的Id支持没问题,但为了保险,统一走ElementId路线。
如果项目里没有剖面视图的族类型,返回InvalidElementId后,创建视图就会失败。处理方式有两种:一是直接提示用户“当前项目未加载剖面视图族类型”;二是尝试复制项目浏览器中现有剖面族类型。
我推荐第二种,用户在模板里通常都有剖面视图,代码里可以先判断是否存在,不存在则复制一个可用的。
3.4 视图切换的底层逻辑
视图切换的核心只有一行代码:
uiApp.ActiveUIDocument.View = sectionView;但要注意,这个属性只在UIApplication的ActiveUIDocument中存在,在Document级别是找不到的。这意味着你的插件必须先获取UIApplication对象,而不是只拿Document操作。
如果你用的是外部命令方式的ExternalCommand,入参自带UIApplication:
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) { UIApplication uiApp = commandData.Application; UIDocument uiDoc = uiApp.ActiveUIDocument; Document doc = uiDoc.Document; // 后续代码 return Result.Succeeded; }如果你是在外部应用中操作,比如写在WinForm/WPF里,则需要提前保存UIApplication实例,或者通过ExternalCommand的上下文获取。很多新手在非命令环境下找不到可用活动视图,就是因为没有UIApplication上下文。
3.5 视图切换前需要检查的可见性条件
视图切换不是任何时候都安全的。比如视图正在被某个编辑操作占用、视图尚未初始化、视图被隐藏等,直接赋值会抛异常或者切不过去。
我写成这样会先做一次安全校验:
private bool CanSwitchToView(UIDocument uiDoc, View targetView) { if (targetView == null) return false; if (!targetView.IsValidObject) return false; if (targetView.IsTemplate) return false; // 模板视图不能作为活动视图 if (targetView.ViewType == ViewType.ProjectBrowser) return false; // 防止切到非视图对象 return true; }实际项目里,ViewType.ProjectBrowser这个问题很少遇到,但会出现View对象本身有效却无法切换的情况,多半是因为视图是图纸视图或其他无法直接打开的视图。所以建议在切换前先判断ViewSection类型。
4. 实操过程:一步步从零实现
4.1 整体代码框架
下面我给一个相对完整的示例。这个示例的使用场景是:用户选中一条轴线或两个点,插件在这两个点之间创建一个剖面视图,并把当前界面切换到该剖面。
using System.Linq; using Autodesk.Revit.UI; using Autodesk.Revit.DB; using Autodesk.Revit.UI.Selection; namespace RevitSectionSample { [Autodesk.Revit.Attributes.Transaction(Autodesk.Revit.Attributes.TransactionMode.Manual)] public class CreateSectionCommand : IExternalCommand { public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) { UIApplication uiApp = commandData.Application; UIDocument uiDoc = uiApp.ActiveUIDocument; Document doc = uiDoc.Document; // 1. 获取用户选择的两个点 XYZ startPoint, endPoint; if (!TryGetTwoPoints(uiDoc, out startPoint, out endPoint)) { message = "请选择两个点来定义剖面位置"; return Result.Cancelled; } // 2. 确定剖面视图族类型 ElementId viewFamilyTypeId = FindSectionViewFamilyType(doc); if (viewFamilyTypeId == ElementId.InvalidElementId) { message = "当前文档没有可用的剖面视图族类型"; return Result.Failed; } // 3. 创建裁剪框 BoundingBoxXYZ sectionBox = CreateSectionBox(startPoint, endPoint, 20, 20); // 4. 创建剖面视图 using (Transaction ts = new Transaction(doc, "创建剖面视图")) { ts.Start(); ViewSection sectionView = ViewSection.Create(doc, viewFamilyTypeId, new ElementId((int)ViewFamily.Section), sectionBox.Id); ts.Commit(); if (sectionView == null) { message = "创建剖面视图失败"; return Result.Failed; } // 5. 切换当前视图到剖面 uiDoc.View = sectionView; } return Result.Succeeded; } } }4.2 注意SectionBox.Id的坑
很多人在网上找示例,看到ViewSection.Create的第三个参数传的是ViewFamily.Section,于是也直接用,但ViewFamily.Section是枚举,不是ElementId,这里需要转成ElementId。
真正容易踩坑的,是sectionBox.Id。SectionBox是Element的实例吗?严格来说,它并不完全等同于普通元素。在Revit API里,SectionBox作为BoundingBoxXYZ被创建后,需要先加入文档才能获得有效Id,或者在事务事务中通过doc.Create.NewSectionBox()来创建。
如果你直接new BoundingBoxXYZ(),没有把它的Id设置为有效值,那么ViewSection.Create第二个参数传sectionBox.Id就会传成无效Id。我之前就在这里卡了很久,最后发现要用doc.Create.NewSectionBox()创建。
正确的写法是:
using (Transaction ts = new Transaction(doc, "创建SectionBox")) { ts.Start(); BoundingBoxXYZ box = new BoundingBoxXYZ(); box.Transform = Transform.Identity; box.Min = new XYZ(...); box.Max = new XYZ(...); SectionBox sectionBox = doc.Create.NewSectionBox(box); ts.Commit(); // 再创建视图,此时sectionBox.Id有效 }为什么必须这样?因为NewSectionBox方法把外部传入的BoundingBoxXYZ包装成一个文档内的元素,它才会被Revit内部图解所识别。直接new BoundingBoxXYZ没有和文档关联,后面创建视图时Revit找不到合法的“剖切盒”。
4.3 真正的完整流程应该这么写
为了让大家可以直接拿去用,我把完整流程整合成一个稍微完善的版本:
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) { UIApplication uiApp = commandData.Application; UIDocument uiDoc = uiApp.ActiveUIDocument; Document doc = uiDoc.Document; // 用户交互:选择两点 XYZ p1, p2; try { Selection sel = uiDoc.Selection; p1 = sel.PickPoint("请选择剖面起点"); p2 = sel.PickPoint("请选择剖面终点"); } catch (Autodesk.Revit.Exceptions.OperationCanceledException) { return Result.Cancelled; } // 查找剖面视图族类型 ViewFamilyType sectionViewFamilyType = new FilteredElementCollector(doc) .OfClass(typeof(ViewFamilyType)) .Cast<ViewFamilyType>() .FirstOrDefault(vft => vft.ViewFamily == ViewFamily.Section); if (sectionViewFamilyType == null) { message = "未找到剖面视图族类型"; return Result.Failed; } BoundingBoxXYZ box = CreateSectionBox(p1, p2, 15, 30); using (Transaction ts = new Transaction(doc, "创建剖面并切换视图")) { ts.Start(); SectionBox sectionBox = doc.Create.NewSectionBox(box); ViewSection vSection = ViewSection.Create(doc, sectionViewFamilyType.Id, new ElementId((int)ViewFamily.Section), sectionBox.Id); uiDoc.View = vSection; ts.Commit(); } return Result.Succeeded; }为什么uiDoc.View = vSection在事务内执行?因为视图创建完成后才能作为活动视图,如果事务未提交,视图状态不稳定,部分版本会报“错误的视图状态”。我习惯在事务提交前直接赋值,提交后直接生效。
4.4 参数计算示例:让剖面范围正好框住模型
有时你不希望自己手动输入宽高,而是希望剖面范围能自动包裹构件范围。这个可以通过FilteredElementCollector获取构件边界盒再计算。
比如我们要框住房间内所有机电管道:
BoundingBoxXYZ modelBox = null; FilteredElementCollector collector = new FilteredElementCollector(doc, doc.ActiveView.Id); foreach (Element elem in collector) { BoundingBoxXYZ bb = elem.get_BoundingBox(null); if (bb == null) continue; if (modelBox == null) modelBox = bb; else { // 手动计算合并范围 } }拿到模型总范围后,再根据剖面的方向和位置把它投影到剖面坐标系的Min/Max里。这个操作听起来复杂,其实只要把模型角落点转换到剖面坐标系,再找最大最小值即可。这一步对新手来说难度较大,我建议暂不需要,先用固定宽度深度,等基本流程跑通后再扩展。
4.5 视图切换后视图深度和细节等级的细节
创建出来的剖面视图默认显示和项目设置有关,有时你会觉得视图太“空”。这是因为裁剪框的深度太小,或者视图详细程度被设为“粗略”。
切换视图前,可以先设置视图的详细程度。常见做法:
vSection.DetailLevel = ViewDetailLevel.Fine;也可以把视图关联的ViewScale调大或调小,不过这个不是必须的,仅当你有特定的出图需求时才需要。
记住,视图刚创建完可能还没完全计算可见性,所以我在代码里通常再加上一句:
doc.Regenerate();这样可以确保视图对象已经正确与模型关联,再切换过去不会出现空白画面。
5. 实操过程中遇到的常见问题与排查技巧
5.1 剖面创建成功却无法切换视图
这是最高频的问题。现象是:ViewSection.Create正常返回,但uiDoc.View = sectionView没反应,或者抛异常。
排查思路如下:
- 检查当前事务是否还开着。有些事务未提交时赋值会失败。
- 检查
sectionView.IsTemplate是否为true。如果是模板视图,不能作为活动视图。 - 检查视图是否在项目浏览器中被“隐藏”。视图创建后如果被手动隐藏或未归入某个方案,也可能无法切换。
常见处理方式是在事务提交前强制重新生成一次:
doc.Regenerate();5.2 剖面视图方向反了
这个问题通常出在BasisZ的方向上。很多人以为BasisZ是剖面的垂直方向,实际上它是视图的视线方向。如果要反转视线方向,把BasisZ乘以-1即可。
举个实际例子:如果你发现生成的剖面是从右下向左上看的,需要把dir取反方向,或者把startPoint和endPoint互换。我更喜欢用交换两个点的方式,因为它更直观,代码也不用改。
5.3 裁剪框太深或太浅
裁剪框的Min和Max是相对Transformer坐标系的,不是世界坐标系的。所以如果你直接拿世界坐标点去设置Min和Max,会发现视图偏到一边去了。
正确思路是:先确定Transform,再在局部坐标系里设置相对范围。比如局部坐标系的Min.X对应“左右”,Min.Y对应“上下”,Min.Z对应“视线方向深度”。
5.4 无法找到剖面视图族类型
如果你的项目模板没有加载剖面视图族类型,Collector返回空。解决方法有三个:
- 从默认项目模板中复制已经存在的剖面视图族类型过来。
- 让用户在项目浏览器中手动新建一个剖面视图,这样族类型就会自动出现。
- 通过复制现有剖面视图的方式获取族类型,然后再删除克隆视图。
我在插件里一般会在开头判断如果找不到,就自动创建一个剖面视图占位,拿走族类型后删除它。这样用户无感,体验最好。
5.5 视图切换后立即缩放位置不对
视图创建后,默认视图范围和裁剪框大致一致,但有时候切换到视图后,看到的画面是“无边无际的空白”,或者模型在视野边缘。原因多是裁剪框位置正确但视图的“视图范围”或“裁剪区域”未激活。
可以通过代码设置IsViewSection相关属性,或直接设置视图的裁剪框开启:
vSection.CropBoxActive = true; vSection.CropBoxVisible = false;但注意,CropBoxActive可能不是所有版本都直接用。建议在创建视图后手动观察,如果出现此问题,再设置这行的值。
5.6 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 创建视图失败 | SectionBox.Id无效 | 使用doc.Create.NewSectionBox()创建 |
| 视图方向反了 | BasisZ方向错误 | 交换两点或反转BasisZ |
| 视图无法切换 | 事务未提交或视图为模板 | 事务提交后赋值,排除模板视图 |
| 视图范围偏移 | Min/Max未相对Transform设置 | 在局部坐标系中设置范围 |
| 找不到剖面族类型 | 项目模板缺少 | 自动创建剖面视图后复制族类型 |
| 剖面视图是空白 | 裁剪框深度太浅 | 增大Min.Z和Max.Z的范围 |
| 视图显示“粗略”效果 | DetailLevel默认值过低 | 设置DetailLevel = ViewDetailLevel.Fine |
5.7 性能优化与内存清理
如果你的插件需要创建大量剖面视图,注意两点:
第一,每创建一次视图都会产生额外的内存占用,用完后如果不需要显示,可以删除视图或把视图的“永久性隐藏”开启。第二,大量Regenerate操作会拖慢运行速度,建议批量创建时放在一个事务里,最后统一刷新。
using (Transaction ts = new Transaction(doc, "批量创建剖面")) { ts.Start(); // 循环创建多个ViewSection ts.Commit(); }事务结束后,Revit自己会做一次完整的重新生成,你不需要每创建完一个就Regenerate()一次。很多初写代码的朋友在循环里反复调Regenerate(),导致十几秒的卡顿,完全没有必要。
6. 进阶技巧与更多玩法
6.1 根据选中构件自动生成剖面
与其让用户手点两点,不如让插件自动识别选中构件的最小边界,然后生成剖面边界。这个功能很适合机电管综项目里的碰撞检查或者净高检查。
我大概说一下思路:
- 取得选中构件所有元素的边界盒合并范围。
- 根据用户选择的剖切方向,计算剖面穿过构件的相对位置。
- 用该范围创建SectionBox。
- 创建视图并切换。
这个方案的难点依然是坐标转换,但只要理解了Transform的含义,剩下的就是数学运算。
6.2 剖面标注与剖面头的自动创建
Revit API中剖面视图本身不包含剖面头和剖面线。如果你希望生成的剖面在平面图中也显示为一条带剖切符号的线,需要在平面视图中创建剖面线,本质上是用ViewSection对象附带生成相应的图纸标记。
这个功能可以通过Viewport或者ViewSection本身的IsModelView相关设定来做,但不同Revit版本API名称稍有差异,建议在方案落地前先查验证一下SDK文档。大体思路是:剖面视图的get_Parameter参数中有一个"剖面标记类型",可以自动生成标记。
不过这个扩展功能较复杂,建议先把主流程跑顺,再按需添加。
6.3 结合过滤器只对指定类别生成剖面
有些场景下,你不希望整个模型的所有类别都被剖面覆盖,可以用BuiltInParameter或ViewSection的可见性设置来控制:
vSection.SetCategoryHidden(doc.Settings.Categories.get_Item(BuiltInCategory.OST_StructuralColumns), true);这样生成的剖面视图中,结构柱就隐藏了。这个技巧我在管线综合检查时经常用,让用户只看某一类管线,显得插件更专业。
6.4 剖面视图重命名与参数化
创建出来的视图默认叫“剖面 1”、“剖面 2”这样的名字。你可以通过View.Name属性重命名成更有意义的名字:
vSection.Name = "主卧剖面";注意,如果重名可能会抛出异常,需要先判断。
我用得比较顺手的是根据两个点的坐标或轴线名称自动生成视图名,比如“A-A剖面(走廊区域)”,这样用户在项目浏览器里也很容易找到视图。
7. 结尾:一点个人实操体会
我在Revit二次开发上写过不少视图相关的功能,创建剖面本身不算复杂,真正有价值的是那几条细节:
第一,SectionBox一定要通过doc.Create.NewSectionBox()创建,直接new BoundingBoxXYZ传Id会失败。这个坑我帮不少同事排查过,网上资料五花八门,只有亲手跑一遍才记得住。
第二,视图切换要在事务内进行,这个和Revit的内部机制有关,抄代码的时候容易忽略。
第三,不要把裁剪框的Min/Max想成世界坐标,要当成局部坐标来理解。凡是方向偏移的问题,十有八九都出在对Transform的理解上。
如果你正在做Revit二次开发,或者正准备做一个“生成剖面并跳转”的小工具,希望这篇实操笔记能帮你省下几个小时的排查时间。照着上面的代码框架跑一遍,再根据项目需求调整裁剪框范围,基本就能直接落地了。
最后再分享一个小技巧:测试这个功能时,建议打开一个新空白项目,先画一根模型线或一个简单体量,再运行命令。这样排查问题最快,不用每次都被复杂的模型干扰判断。