做NX二次开发这几年,我发现自己写过的几乎所有实用工具,都绕不开一个看似基础、实则暗藏门道的问题——怎么让用户正确地选到想要的对象。你写了个自动打孔程序,结果用户一眼花点到了片体边缘;你写了个清根刀路辅助工具,用户多选了几个碎面导致程序崩溃。这时候你才意识到,Selection::GetSelectedObjects这种最粗暴的写法根本不够用,过滤器(Filter)才是整个交互体验的命门。网上关于C# NX二次开发过滤器的讨论很零散,大多是贴一段代码就完事,没人讲清楚为什么这样写、什么时候用哪种方案。这篇文章我就把实际项目中总结出来的两套过滤器设置方案完整拆开来讲,包括适用场景、底层逻辑、踩过的坑,希望能帮你少走弯路。
1. 过滤器问题的本质:不是“选不选得到”,而是“该不该被选到”
很多刚接触NX二次开发的朋友会把过滤器理解成一个简单的类型判断,比如“只允许选面”。实际上,在NX的交互体系里,过滤器的作用远比这复杂——它决定的是整个选择交互的可用域。你要理解这一点,先得知道NX对象选择和鼠标点击之间发生了什么。
当你鼠标在绘图区移动时,NX的显示层会做一次快速的“预筛选”,把鼠标经过范围内的可选中对象反馈给Selection对象;真正点击鼠标后,NX再根据当前设置的过滤器规则,对候选对象做二次判定。如果对象不满足规则,哪怕你鼠标正对着它,它也不会被高亮,点击后也不会进入选择集。
这意味着,过滤器设置方案决定了三件事:
- 用户能选什么:面的类型、边的类型、特征的类型。
- 用户选得有多准:是否能屏蔽掉无关的片体、草图曲线、基准平面等干扰项。
- 选择后的数据好不好用:拿到Tag或者NxObject后,要不要再做二次类型转换。
C#环境下,NX开放了NXOpen.Selection这一整套托管API,而过滤器主要挂在Selection对象的方法和事件上。实际开发中,用得最多的有两套方案:一套是基于Selection::SetFilter配合IMenuFilter或ISelectionFilter回调的方式,另一套是基于Selection::SelectObjectsWithTag和NXOpen::Selection::SelectionAction这类阻塞式选择对话框参数控制的方式。这两套方案不是替代关系,而是针对不同交互场景的两种思维。
搞清楚过滤器本质之后,我再带你看一个我在刻字工具里遇到的真实案例。有次我在做产品序列号自动刻字程序,要求用户选择一个零件的上表面作为刻字放置面。最初直接用了SelectObject默认过滤器,结果用户反馈“选不中,老是选到旁边的曲面”。排查后发现,NX默认选择状态下,片体、实体面、基准面全都处于可选状态,用户在密集装配体里用鼠标滚轮缩放半天才选中目标面,体验极差。后来我改用方案一的过滤器回调,把候选对象范围精确限定为“实体平面”,问题立刻解决了。
2. 方案一解剖:Selection::SetFilter过滤器回调的完整工作机制
这套方案的核心思路是:先注册过滤器回调函数,再启动选择会话。每一次用户点击时,NX都会把鼠标位置的候选对象列表交给你的回调函数,你返回Selection.Response枚举值来决定这个对象是否可被选中。这个机制非常灵活,因为它不仅可以按类型过滤,还可以按对象的几何属性、特征类型、图层状态等自定义条件过滤。
2.1 从一段可运行的最小代码说起
在C#环境里,最小可用的过滤器回调代码大概长这样:
using NXOpen; using NXOpen.UF; public class FaceFilter : ISelectionFilter { public Selection.Response Evaluate(Selection selectedObj) { // selectedObj.View 是鼠标点击时的视图对象 // selectedObj.Object 是候选的 NXObject NXObject obj = selectedObj.Object; if (obj is Face face) { // 只允许平面通过,排除圆柱面、B曲面、球面等 if (face.GetType() == Face.FaceType.Planar) { return Selection.Response.Selected; } } return Selection.Response.Reject; } } // 使用时: FaceFilter filter = new FaceFilter(); theSession.Selection.SetFilter("MyFaceFilter", filter); try { NXObject[] objects = theSession.Selection.SelectObjects("请选择刻字平面", "选择面"); // 处理选中结果... } finally { theSession.Selection.RemoveFilter("MyFaceFilter"); }这段代码看起来简单,但有几个关键点必须注意。第一,SetFilter方法的第一个参数是过滤器名称,这个名称在同一个Selection实例中应该是唯一的,重复注册同名过滤器会抛出异常。第二,ISelectionFilter接口只有一个Evaluate方法,返回值Selection.Response除了Selected和Reject之外,还有Selectable和NotSelectable等状态,我后面会细讲。第三,过滤器注册之后是全局生效的,它会影响当前会话中所有的SelectObjects、SelectObject、SelectWithSingleDialog等调用,所以用完必须及时RemoveFilter,不然后面别的功能会莫名其妙选不了对象。
2.2 Evaluate返回值:不只是“选”和“不选”
大部分教程只会讲Selected和Reject,但实际工程里,Selection.Response的其它返回值有更细腻的应用价值。我整理了一个表格方便你参考:
| 返回值 | 含义 | 典型应用场景 |
|---|---|---|
Selected | 该对象可以被正常选中 | 默认情况下的放行判定 |
Reject | 该对象不可被选中,点击无效 | 排除不需要的对象类型 |
Selectable | 对象可被选择但是光标经过时不自动高亮 | 与Selected配合实现“弱高亮”效果 |
NotSelectable | 对象在当前视图中不可选择 | 隐藏密集区域的干扰对象 |
None | 尚未做出决定,继续传递判定 | 多级过滤器链式处理 |
举个实际的使用场景。比如我做了一个批量修改圆角半径的工具,要求用户只能选中恒定半径的圆角面。如果返回Selected,用户鼠标移到圆角面上就高亮,点击后进入选择集;但有些变半径圆角面虽然挨在一起,也应该显示出来让用户知道“它的确存在,只是不满足条件”。这种情况下就不好直接Reject,因为用户会以为自己没点中,反复点击产生困惑。更好的做法是先对变半径圆角面返回Selectable,让它在光标经过时有弱反馈,但点击时不会进入选择集。不过这个技巧需要配合Selection.PreHighlight相关设置,否则默认情况下Selectable和Selected的反馈差异不明显,用户还是容易混淆。
2.3 过滤器回调里的“异常黑洞”,必须自行兜底
这是一个我吃过亏的地方。Evaluate回调运行在NX的交互线程里,一旦回调里抛出未捕获的异常,NX不会弹出错误提示,而是静默吞掉异常并且默认拒绝该对象。你说诡异不诡异?用户点一个面,啥反应都没有,还以为程序卡了。后来我在回调里第一行就加了try-catch,通过日志记录异常信息。至于日志写到哪里,建议写到UF_UGMGR的日志或者自定义的txt文件里,别用UI.GetUI().NXMessageBox弹窗——回调里弹窗会导致选择会话被中断,引发更严重的问题。
public Selection.Response Evaluate(Selection selectedObj) { try { // 你的过滤逻辑 } catch (Exception ex) { LogHelper.WriteError("过滤回调异常: " + ex); return Selection.Response.Reject; } }这段经验特别适合新手借鉴。团队里有个同事写过滤器没做异常处理,结果用户操作时偶发选不中对象,查了两天愣是没找到原因,最后定位到是Face.GetFaceType在某些特定拓扑条件下抛了内部异常。从那以后,我所有回调函数第一件事就是兜底日志。
3. 方案二解剖:SelectObjects系列对话框参数里的掩码式过滤
和方案一这种“回调拦截”的思路不同,方案二是在NX的预定义选择对话框上用**“参数掩码”**做过滤。它没有回调函数,而是在调用SelectObjects之类的API时传入Selection.SelectionAction和一组条件选项,由NX的选择引擎自己完成判定。这种方案的好处是代码简单、性能好、适合大装配体下的快速选择;缺点是不够灵活,只能做类型和状态级别的过滤,做不了“只选恒定半径圆角面”这种几何属性的深度过滤。
3.1 类型掩码与对象状态参数
在C# API中,这组参数主要通过Selection.SelectObjects的重载传入。典型的写法是:
// 创建选择条件 Selection.SelectionAction action = Selection.SelectionAction.ClearAndEnableSpecific; Selection.View view = null; // null 表示当前工作视图 // 设置类型掩码 Type[] types = new Type[] { typeof(Face), typeof(Edge) }; Selection.SelectObjects("选择目标对象", "提示", types, action, false, out NXObject[] selected);但要特别提醒的是,Type[]参数在NX不同版本里的行为有差异。早期的NX版本(例如NX1847之前)对泛型Type[]的支持并不好,更常见的是通过Selection对象的SetSelectionFilter或者SelectionType枚举设置。到了高版本(NX1980、NX2206等),Type[]的写法才稳定下来。所以你在看网上代码时,会发现有些老教程用UF_UI_lock_ug_object或者UF_UI_set_sel_mask这类UFUN函数,就是因为历史原因。
如果要更精确地控制在SelectObjects对话框中哪些对象可选,我一般直接用Selection.SelectWithSingleDialog配合Selection.JournalSelectionOptions,这类方法允许设置更多的布尔选项,比如是否允许选择隐藏对象、是否允许选择已选对象、选择范围是“仅工作部件”还是“整个装配”等。
3.2 掩码式方案的典型应用:批量选孔与“忽略内部边”
举一个很实用的例子——自动创建中心线。这个工具要批量选择圆孔的内圆柱面来生成中心线符号。如果用方案一的回调,每点一个孔面都要触发一次回调,在大装配里反馈有些迟钝;但用掩码式过滤就快得多,只需把类型掩码限制为“圆柱面”,再叠加“仅可见面”、“仅工作部件中的面”两个选项,用户就可以在图形区快速框选一堆孔面。代码大概是:
using NXOpen; using NXOpen.UF; public void CreateCenterlines() { Session theSession = Session.GetSession(); UI theUI = UI.GetUI(); // 设置选择参数 Selection selection = theUI.SelectionManager.NewSelection(); selection.SetSelectionFilter("CylinderFaces", new CylinderFaceFilter()); NXObject[] objects = theUI.SelectionManager.SelectObjects("选择内孔面", "选择面", selection); // 处理选中的面... }这里用了SelectionManager.NewSelection()创建独立的selection实例,好处是不影响全局的theSession.Selection,也就不需要在用完RemoveFilter,算是我个人比较喜欢的一种隔离方式。如果你用同一个Selection实例,记得做好过滤器的清理。
3.3 两种掩码模式的取舍:ClearAndEnableSpecificvsEnableSpecific
写代码的时候,你还会遇到选择动作的设置问题。Selection.SelectionAction有两个看起来很像的枚举值:
ClearAndEnableSpecific:清空当前选择集,并启用特定的掩码类型。适用于每次选择前希望重置用户已选对象的场景。EnableSpecific:不清空选择集,只在当前已有选择基础上增加新的可选类型。适用于多轮累加选择的场景。
我举个例子帮你理解。假设你的程序流程是“先选择基准点,再选择放置面”,基准点选完之后如果直接调用下一步选择,并且用了EnableSpecific,那么用户已经选中的基准点会继续保留高亮,而且如果视图里还有基准点,鼠标移上去也会继续显示可选状态,这在分步选择的交互中容易造成混乱。反之,如果每步都用ClearAndEnableSpecific,选择集会在每次调用时清空,界面反馈更干净,但你就需要自己在代码里保存上一步的选择结果,并在后续逻辑中拼接。
我的习惯是:凡是分步骤需要用户确认的,优先用ClearAndEnableSpecific,因为NX这个选择会话本身会停留在界面上等待输入,用户很难理解“为什么我第一步选的点还亮着”;凡是同一个步骤内多次追加选择(比如先选几条线,再选一个方向面),用EnableSpecific,减少不必要的交互跳转。
4. 实战场景适配:按交互需求选择方案,而不是按代码复杂度
这一节没有太多新语法,重点讲讲我在真实项目里是怎么做选择的。因为很多时候,不是写不出过滤器,而是不知道该用哪一套——最后写出来的代码要么绕远路,要么交互体验别别扭扭。我按项目类型拆成三类典型场景,分别给出推荐方案。
4.1 场景一:单对象精确定位(选一个面写刻字、选一条边做倒角)
这类场景的核心诉求是精准,要求用户只能选到目标类型,其他任何对象都不可高亮。此时我强烈推荐方案一的回调过滤器。原因很简单:掩码式过滤只能控制到“面的类型”,控制不到“面的几何属性”,但回调里你能获取完整的Face对象,从Face.GetFaceType()、Face.GetEdgeCount()到Face.Normal等几何数据,想怎么判断都行。
我之前做序列号刻字工具就是典型。面的要求是:实体平面、面积必须大于字体的包围盒面积、面的法向必须和指定的刻字方向一致。这个逻辑在掩码式方案里根本不可能实现,但回调式方案里就是几行代码的事:
public Selection.Response Evaluate(Selection selectedObj) { if (selectedObj.Object is Face face) { // 1. 必须是平面 if (face.GetFaceType() != Face.FaceType.Planar) return Selection.Response.Reject; // 2. 计算面积是否足够 double area = face.GetArea(); if (area < m_minArea) return Selection.Response.Reject; // 3. 检查法向是否满足要求 Point3d normal = face.GetNormal(face.GetPointOnFace()); double dot = normal.X * m_dir.X + normal.Y * m_dir.Y + normal.Z * m_dir.Z; if (dot < m_minDot) return Selection.Response.Reject; return Selection.Response.Selected; } return Selection.Response.Reject; }这里m_minArea和m_dir可以在注册过滤器之前通过外部传入,让过滤器对象成为有状态的。实际跑下来,用户体验非常顺滑——不满足条件的面根本不会高亮,用户不需要自己判断哪个符合要求,减少了大量误操作。
4.2 场景二:批量快速选择(框选孔面、批量选曲线生成加工路径)
批量选择的场景更看重速度和流畅度。如果每个候选对象都要走一遍回调判断几何,框选几百个对象时界面会明显卡顿。这个时候优先用掩码式方案,直接让NX原生选择引擎干活。NX在框选、套索选择时,自身对掩码筛过的类型做了很好的并行优化,比你回调里一个个GetFaceType快得多。
需要补充一个点:Selection类型的掩码里有一个“IncludeFacesOfSolids”和“IncludeFacesOfSheetBodies”这样的细分开关。如果你只允许选实体面,务必把片体面的开关关掉,不然框选的时候会选到一大堆片体上的面,后期还得再做一次剔除。
UFSelect.SelectTypeMask mask = new UFSelect.SelectTypeMask(); mask.type = UFConstants.UF_face_type; mask.subtype = UFConstants.UF_face_planar_subtype; // 平面 mask.solid_type = UFConstants.UF_UI_SEL_FEATURE_SOLID_BODY; // 仅实体 mask.is_curve = 0;4.3 场景三:混合多选(既选几何对象,又需要输入参数)
还有一种更复杂的场景:选择不是一个独立步骤,而是嵌入在一个包含参数输入的BlockUI对话框里。比如“选择孔面并输入深度”“选择曲线并指定偏移距离”。这种情况下,过滤器方案的选择受限于NX对话框的类型。
如果你用的是NXOpen.BlockStyler对话框,并且里面控件是SelectObject或SelectObjectList类型的UI控件,那么你需要在initialize_cb回调里设置SelectionFilter,并且这个过滤器不能是全局的Session.Selection过滤器,而是控件自己的过滤器。一般用BlockStyler.PropertyList.SetSelectionFilter来绑定:
BlockStyler.PropertyList selectProp = selectControl.GetProperties(); selectProp.SetSelectionFilter("HoleFaceFilter", new HoleFaceFilter());这里有个坑:控件绑定的过滤器生命周期和对话框一致,对话框销毁之后过滤器自动释放。如果你在对话框初始化时注册了全局过滤器,却忘了移除,那对话框关闭之后用户可能没法正常选择任何对象。我遇到过这个问题,最后排查到是对话框关闭时过滤器没移除造成的。所以BlockUI场景下,能绑定控件级过滤器就不碰全局过滤器。
5. 过滤器的“隐藏技能”:多级联动、自定义判定与性能优化
这一节算是全篇的进阶部分。单纯会写“选面”和“选边”的过滤器,在真实项目里还远远不够。我在做更复杂的工具时,往往需要过滤器之间、过滤器与其它NX模块之间协同工作。下面几个技巧是从实际项目中沉淀下来的。
5.1 多级过滤器:先粗筛类型,再细筛属性
有人觉得ISelectionFilter只能注册一个,不够用。其实完全可以在Evaluate里自己实现多级判定。比如先判断对象类型,再根据特征类型进一步过滤,最后再判断几何属性。这里我建议把判定逻辑抽象成几个独立的小函数,避免回调函数里堆一大坨。
public Selection.Response Evaluate(Selection selectedObj) { NXObject obj = selectedObj.Object; // 第一级:类型必须满足 if (!IsAllowedType(obj)) return Selection.Response.Reject; // 第二级:所在特征必须满足 if (!IsAllowedFeature(obj)) return Selection.Response.Reject; // 第三级:几何属性必须满足 if (!IsAllowedGeometry(obj)) return Selection.Response.Reject; return Selection.Response.Selected; }这样写的好处是每个判定阶段都可以单独调试,加日志定位问题也更方便。而且当需求变更(比如新增一种可选的孔类型)时,只需要改IsAllowedFeature这一个函数,不会动到其它逻辑。
5.2 过滤器与AskObject联动:选择时实时反馈对象信息
过滤器回调不仅仅能做“是/否”的判定,你完全可以在这里调用NX的查询函数,拿到对象的更多信息,缓存在过滤器对象内部。配合一个UI控件的刷新,甚至可以做到“用户鼠标移到一个面上时,旁边的Label立刻显示该面的面积”这种效果。不过要提醒一点:Evaluate回调的执行频率极高,鼠标随便一晃就调用几十次,如果你在回调里做重计算(比如遍历整个装配找关联对象),性能会很难看。
我的实践是:在Evaluate里只做轻量级缓存更新,把重计算放到Selection.SetSelectionCallback的OnSelect事件或者按钮点击之后去做。有一个小技巧你可能会用到:通过selectedObj.SelectedObjects(某些委托参数里会提供当前选择集状态)判断这是不是第一次选中该对象,再决定是否触发一次“信息显示”刷新,避免每次鼠标移动都刷新。
5.3 大装配下过滤器的性能调优
做汽车、模具行业的项目,经常遇到几千个零件的装配体。有一次我在做“批量修改钣金孔参数”工具,装配体里有两万多个面,用方案一的回调过滤器做“只能选孔内圆柱面”,用户每次框选都要等两三秒才有反馈,完全没法用。后来我用了一个优化组合拳:
- 优先使用掩码式过滤(方案二),通过
solid_type和subtype让NX原生引擎跳过大多数无关面。 - 自定义过滤器中引入包围盒预判:
Evaluate里可以用obj.GetBoundingBox()先做一次粗略空间判断,如果包围盒和目标区域不交叉,直接Reject,省掉后面昂贵的几何计算。 - 需要高亮时用
Selection.FilterReference:这个对象可以预先缓存候选对象列表,避免每次鼠标移动都重新遍历场景。
public Selection.Response Evaluate(Selection selectedObj) { NXObject obj = selectedObj.Object; if (obj is Face face) { // 包围盒快速排除 BoundingBox bbox = face.GetBoundingBox(); if (bbox.Max.X < m_regionMin.X || bbox.Min.X > m_regionMax.X || bbox.Max.Y < m_regionMin.Y || bbox.Min.Y > m_regionMax.Y || bbox.Max.Z < m_regionMin.Z || bbox.Min.Z > m_regionMax.Z) { return Selection.Response.Reject; } // 再做精确几何判断 if (IsHoleFace(face)) return Selection.Response.Selected; } return Selection.Response.Reject; }优化之后,即使在大装配体里框选几百个孔面,反馈也能保持在几百毫秒以内,用户基本无感知。
5.4 过滤器的调试手段:日志与选择集观察器
最后分享一个调试技巧。过滤器写完之后“选不中对象”或“选多了对象”,是二次开发中最难排查的问题。因为NX不会告诉你为什么某个对象被拒绝了。我的做法是在过滤器里加上log级别的输出开关,用环境变量控制。
private bool m_debug = bool.Parse(Environment.GetEnvironmentVariable("NX_FILTER_DEBUG") ?? "false"); public Selection.Response Evaluate(Selection selectedObj) { if (m_debug) { LogHelper.WriteInfo($"过滤判定: 对象={selectedObj.Object}, 类型={selectedObj.Object.GetType()}, " + $"所在特征={GetFeatureName(selectedObj.Object)}"); } // ... 详细的判定逻辑 }调试的时候在外部设置好环境变量,跑一轮操作,把日志导出来分析。这样能清楚看到每个被拒绝对象的特征名和类型,很快就能定位到哪一条判定逻辑写错了。这个方法在我一次排查“过滤了平面但球面也被选进来”的bug时帮了大忙——后来发现是Face.FaceType枚举值和实际不符,其实那个“球面”在NX内部是被建模为B-surface的,只有调用GetFaceType返回的才是Spherical。
6. 两种方案的选型清单与我的个人经验
写到这里,核心内容基本都讲完了。最后我用一个表格把这篇文章的要点汇总一下,方便你以后查阅和做技术选型。
| 比较维度 | 方案一:SetFilter回调过滤器 | 方案二:SelectObjects掩码式过滤 |
|---|---|---|
| 核心机制 | 注册ISelectionFilter回调,逐对象判定 | NX原生选择引擎按照掩码筛选类型 |
| 灵活性 | 极高,可做几何、特征、空间关系等任意判定 | 较低,只能做类型、子类型、主体类别过滤 |
| 性能 | 调用密集,大装配下需自行优化 | 原生优化,框选大场景表现更好 |
| 代码复杂度 | 略高,需要实现接口并处理异常 | 较低,几行参数设置即可 |
| 适用场景 | 单对象精确定位、属性相关的条件过滤 | 批量多选、只求类型区分的大场景 |
| 副作用注意 | 全局生效,需及时移除 | 需要正确设置掩码,防止选到多余类型 |
| 调试难度 | 较高,需要日志辅助 | 较低,通过Journal录制可快速确认 |
从我个人的开发经验来说,一个新工具上手写选择逻辑时,先默认用方案二,因为代码量小、不易出错。等用户或需求方反馈“选不准”“选多了”的时候,再针对性地换成方案一,把真正需要精确判定的几何条件写进回调里。不要一上来就整复杂的过滤器回调,否则前期调试成本会拖慢整个项目的进度。
再补充两个容易被忽视的细节。第一个是Selection.Response.Selected的对象,在进入选择集之后,如果用户做了二次点击(取消选择),NX会自动将该对象从选择集中移除,不需要你专门处理;但如果你的回调在用户重复选择同一个对象时返回了Selected而对象已在选择集内,NX的行为是一次点击选中、再次点击取消,这种默认行为不要尝试去改,改了反而会让用户觉得不自然。第二个是无论用哪种方案,都要记得在交互结束后调用selection.Clear()或者释放相关选择会话,避免选择集残留在界面上干扰后续操作。
这篇文章算是把我这几年做C# NX二次开发时,在过滤器设置上攒下的心得都倒出来了。每一个坑背后都是一段调试到凌晨的回忆——尤其是那个静默吞异常的回调,真是让人又爱又恨。如果你也在做类似的工具开发,希望这篇文章能帮你避开我已经踩过的雷。实际跑代码的时候有什么奇怪的反馈,欢迎带着具体现象来聊,解决问题的思路往往比标准答案更能打开局面。