AutoCAD .NET 二次开发:深入理解 EntityJig 的工作原理与正确实现
前言
在 AutoCAD .NET 二次开发中,EntityJig 是实现交互式拖拽(Jig)技术的重要基类。它能让用户在绘制实体时实时预览图形------移动鼠标,实体随之动态变化,所见即所得。这种交互模式广泛应用于直线绘制、墙体拖拽、圆半径调整、块插入定位等场景,对提升用户体验至关重要。
然而,开发者常因对 EntityJig 的内部机制理解不透彻,导致实现出错或遇到类似 eKeyNotFound 等诡异异常。其中最常见的误区之一便是混淆 EntityJig 与 DrawJig 的角色:前者通过自动调用 Entity.WorldDraw() 来绘制受管理的实体,不需要开发者重写 WorldDraw();后者才需要手动重写 WorldDraw() 用 Geometry 接口手绘几何图形。
本文将结合 ObjectARX 底层原理,系统分析 EntityJig 的继承体系、核心方法及其执行流程,并以 Line、Circle 等典型实体为例,给出规范的实现方法。
一、Jig 类体系:从 ObjectARX 到 .NET 托管封装
1.1 底层架构
AutoCAD 的 Jig 机制源自 ObjectARX C++ 类 AcEdJig。.NET API 对其进行了三层封装:
Autodesk.AutoCAD.EditorInput.Jig (抽象基类,托管封装层)
├── EntityJig (单实体 Jig,自动绘制托管 Entity)
│ └── 你的 MyLineJig / MyCircleJig ...
└── DrawJig (多实体/自定义 Jig,手动 WorldDraw)
└── 你的 MyRectJig / MyDoubleWallJig ...
以上即为.NET API 的官方继承层次结构。任何自定义 Jig 类都必须从 EntityJig 或 DrawJig 二者之一派生,并覆写相应的抽象方法。
- Jig (抽象基类) :对应
AcEdJig,提供拖拽循环的核心控制,包括Drag()方法的启动入口。 - EntityJig :管理 单个
Entity对象的生命周期。其内部在拖拽过程中自动调用所托管实体的WorldDraw()方法完成图形绘制,开发者无需且不应 自行覆写WorldDraw()。官方文档明确指出:"EntityJig 对象允许用户操纵自定义实体的图形表示,然后将用户的输入应用到该实体的实际实例上。"。 - DrawJig :不关联特定实体,允许绘制多个实体 或复杂图形(如动态预览的双线墙),开发者必须 覆写
WorldDraw()方法,通过WorldGeometry对象手动绘制所有几何图形。
1.2 EntityJig 的核心属性:Entity
EntityJig 维护了一个核心属性:
csharp
public Entity Entity { get; }
这个 Entity 对象是在派生类构造函数中通过 base(entity) 传递进去的。在整个拖拽过程中,EntityJig 负责管理该实体的生命周期:
| 阶段 | Entity 状态 | 位置 |
|---|---|---|
| 构造时 | 创建临时实体(未加入数据库) | new Line(...) / new Circle(...) |
| Jig 拖拽中 | 每次 Update 后自动重绘 Entity | EntityJig 内部调用 Entity.WorldDraw() |
| 用户确认 | 保留 Entity,外部将其加入数据库 | 开发者调用 AddNewlyCreatedDBObject |
| 用户取消 | Entity 应被 Dispose | 开发者负责释放 |
这一机制要求开发者必须明确理解 Entity 的生命周期边界------Entity 仅存在于 Jig 过程之中,数据库驻留的逻辑完全由外部代码控制。
二、EntityJig 的执行流程详解
2.1 三阶段循环:Jig 事件处理顺序
EntityJig 的拖拽过程本质上是一个由三个方法驱动的循环(结合官方文档和实际行为):
Drag() 开始
│
▼
┌──────────────────────────────────────────────────┐
│ Jig 循环 │
│ │
│ ① Sampler(JigPrompts prompts) │
│ ├── 获取用户输入(鼠标位置/键盘/距离/角度) │
│ ├── 返回 SamplerStatus.OK → 数据已更新 │
│ ├── 返回 SamplerStatus.NoChange → 无需刷新 │
│ └── 返回 SamplerStatus.Cancel → 终止 │
│ │
│ ② Update() │
│ ├── 仅当 Sampler 返回 SamplerStatus.OK 时调用 │
│ ├── 将新数据应用于 Entity 的属性 │
│ └── 返回 true 继续 / 返回 false 终止 │
│ │
│ ③ 自动重绘 Entity │
│ ├── EntityJig 内部调用 Entity.WorldDraw() │
│ └── 将更新后的 Entity 绘制到屏幕 │
│ │
└──────────────────────────────────────────────────┘
│
▼
Drag() 结束
以上流程拆解了 EntityJig 在鼠标移动时的内部处理细节,引用自权威博客。
下方结合代码逐一解析各阶段的实现要点。
2.2 Sampler() --- 数据采样
传入参数是一个 JigPrompts 对象(静态类),它提供一系列重载的 Acquire 方法。通过调用不同的方法,可以获取用户不同格式的输入数据:
| 方法签名 | 返回类型 | 用途示例 |
|---|---|---|
AcquirePoint(JigPromptPointOptions) |
PromptPointResult |
获取坐标点(制作直线、矩形或多段线时) |
AcquireDistance(JigPromptDistanceOptions) |
PromptDoubleResult |
获取距离值(动态修改圆半径) |
AcquireAngle(JigPromptAngleOptions) |
PromptDoubleResult |
获取角度值(旋转实体时跟踪) |
AcquireString(JigPromptStringOptions) |
PromptResult |
获取字符串(极少数场景使用) |
此表对应了 JigPrompts 类的四个重载方法,是开发动态交互功能时必须掌握的底层接口。
Sampler() 的返回值决定了 Jig 循环的下一步行为:
| 返回值 | 含义 |
|---|---|
SamplerStatus.OK |
获取到了新的有效数据,触发 Update() |
SamplerStatus.NoChange |
数据没有明显变化,不调用 Update()(优化性能,减少闪烁) |
SamplerStatus.Cancel |
用户取消了操作,结束 Jig 循环 |
2.3 Update() --- 更新实体
Update() 方法仅 在 Sampler() 返回 SamplerStatus.OK 时被调用。其职责是将 Sampler() 中临时存储的数据写入 Entity 的属性中。
csharp
protected override bool Update()
{
// 将 _currentPoint 写入 Entity 的几何属性
((Line)Entity).EndPoint = _currentPoint;
return true; // 返回 true 表示更新成功
}
关键规则:
- Update() 中只应修改 Entity 的属性,不应在此处添加实体到数据库
- 返回
true表示实体已更新,Jig 将继续下一轮循环 - 返回
false表示更新失败,Jig 终止
2.4 自动绘制 --- EntityJig 的图形刷新机制
对于 EntityJig,开发者不需要重写 WorldDraw() 方法 。EntityJig 内部在每次 Update() 返回 true 后,会自动调用其所持有 Entity 对象自身的图形绘制逻辑(Entity.WorldDraw()),将几何对象重新绘制到屏幕之上,实现动态预览效果。
而如果是 DrawJig,开发者才需要手动重写 WorldDraw() ,通过 WorldGeometry 对象自行绘制所有图形。
三、完整实现示例
以下示例严格遵循上述规范,针对不同实体类型给出相异的实现方法:在 LineJig 和 CircleJig 中通过继承 EntityJig 并覆写 Sampler() 和 Update() 方法实现动态绘制,在 RectJig 中展示如何通过继承 DrawJig 并使用 Geometry.Polygon() 统一绘制四条边。
3.1 LineJig --- 直线动态绘制
csharp
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
/// <summary>
/// 直线拖拽 Jig --- 继承 EntityJig,拖拽过程中自动绘制 Line 实体
/// </summary>
public class LineJig : EntityJig
{
private Point3d _startPoint;
private Point3d _endPoint;
/// <summary>
/// 构造函数 --- 将临时 Line 实体传给基类
/// </summary>
public LineJig(Point3d startPoint)
: base(new Line(startPoint, startPoint))
{
_startPoint = startPoint;
_endPoint = startPoint;
}
/// <summary>
/// Sampler() --- 获取用户鼠标位置
/// </summary>
protected override SamplerStatus Sampler(JigPrompts prompts)
{
// 配置点输入选项
JigPromptPointOptions opts = new JigPromptPointOptions("\n指定直线终点: ")
{
UseBasePoint = true,
BasePoint = _startPoint // 从起点引出橡皮筋线
};
PromptPointResult res = prompts.AcquirePoint(opts);
if (res.Status == PromptStatus.Cancel)
return SamplerStatus.Cancel;
// 距离过小则返回 NoChange,减少不必要的刷新
if (_endPoint.DistanceTo(res.Value) < Tolerance.Global.EqualPoint)
return SamplerStatus.NoChange;
_endPoint = res.Value;
return SamplerStatus.OK;
}
/// <summary>
/// Update() --- 将新数据写入 Entity
/// </summary>
protected override bool Update()
{
((Line)Entity).StartPoint = _startPoint;
((Line)Entity).EndPoint = _endPoint;
return true;
}
/// <summary>
/// 获取最终的 Line 实体(用户确认后调用)
/// </summary>
public Entity GetEntity() => Entity;
}
// ═════════════ 命令入口 ═════════════
[CommandMethod("JigLine")]
public static void JigLineCommand()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
Database db = doc.Database;
// 获取起点
PromptPointResult startRes = ed.GetPoint("\n指定直线起点: ");
if (startRes.Status != PromptStatus.OK) return;
// 创建 Jig 并开始拖拽
LineJig jig = new LineJig(startRes.Value);
PromptResult dragRes = ed.Drag(jig);
// 用户确认 ------ 将实体加入数据库
if (dragRes.Status == PromptStatus.OK)
{
using (Transaction tr = db.TransactionManager.StartTransaction())
{
BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
BlockTableRecord btr = (BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
Entity line = jig.GetEntity();
btr.AppendEntity(line);
tr.AddNewlyCreatedDBObject(line, true);
tr.Commit();
}
}
else
{
// 用户取消 ------ 必须释放 Entity
jig.GetEntity().Dispose();
}
}
设计要点:
- 构造函数 在调用
base(new Line(...))时传入了一个零长度直线,作为 Entity 的初始图形。 - Sampler()内部 将
BasePoint设置为起点,形成从起点出发的橡皮筋拖拽效果;当鼠标位置与终点几乎重合时返回NoChange,有效抑制了不必要的重绘闪烁。 Update()根据实时坐标更新Line.EndPoint,触发基类的自动重绘。- 命令入口处,通过检查
dragRes.Status判断用户是确认(OK)还是取消(Cancel),决定是加入模型空间还是调用Dispose()释放临时实体。
3.2 CircleJig --- 圆动态绘制(半径拖拽)
csharp
/// <summary>
/// 圆形拖拽 Jig --- 拖拽过程中动态改变圆半径
/// </summary>
public class CircleJig : EntityJig
{
private Point3d _center;
private double _radius;
public CircleJig(Point3d center)
: base(new Circle(center, Vector3d.ZAxis, 1.0))
{
_center = center;
_radius = 1.0;
}
protected override SamplerStatus Sampler(JigPrompts prompts)
{
JigPromptDistanceOptions opts = new JigPromptDistanceOptions("\n指定圆半径: ")
{
UseBasePoint = true,
BasePoint = _center,
Cursor = CursorType.RubberBand
};
PromptDoubleResult res = prompts.AcquireDistance(opts);
if (res.Status == PromptStatus.Cancel)
return SamplerStatus.Cancel;
double newRadius = res.Value;
if (Math.Abs(newRadius - _radius) < Tolerance.Global.EqualPoint)
return SamplerStatus.NoChange;
_radius = newRadius;
return SamplerStatus.OK;
}
protected override bool Update()
{
((Circle)Entity).Center = _center;
((Circle)Entity).Radius = _radius;
return true;
}
public Entity GetEntity() => Entity;
}
设计要点:
- 使用
JigPromptDistanceOptions和AcquireDistance(),以距离值(圆心到鼠标的投影距离)作为半径输入。 - 设置
Cursor = CursorType.RubberBand,显示橡皮筋光标,用户体验更佳。
3.3 RectJig --- 多段线矩形动态绘制
csharp
/// <summary>
/// 矩形拖拽 Jig --- 继承 DrawJig(多实体绘制)
/// </summary>
public class RectJig : DrawJig
{
private Point3d _basePoint; // 矩形第一角点(固定)
private Point3d _currentPoint; // 矩形第二角点(跟随鼠标)
private Point3d _corner1, _corner2, _corner3, _corner4;
public RectJig(Point3d basePoint)
{
_basePoint = basePoint;
_currentPoint = basePoint;
}
protected override SamplerStatus Sampler(JigPrompts prompts)
{
JigPromptPointOptions opts = new JigPromptPointOptions("\n指定矩形对角点: ")
{
UseBasePoint = true,
BasePoint = _basePoint
};
PromptPointResult res = prompts.AcquirePoint(opts);
if (res.Status == PromptStatus.Cancel)
return SamplerStatus.Cancel;
if (_currentPoint.DistanceTo(res.Value) < Tolerance.Global.EqualPoint)
return SamplerStatus.NoChange;
_currentPoint = res.Value;
return SamplerStatus.OK;
}
/// <summary>
/// WorldDraw() --- 手动绘制矩形的四条边
/// </summary>
protected override bool WorldDraw(Autodesk.AutoCAD.GraphicsInterface.WorldDraw draw)
{
// 计算四个角点
_corner1 = _basePoint;
_corner2 = new Point3d(_currentPoint.X, _basePoint.Y, 0);
_corner3 = _currentPoint;
_corner4 = new Point3d(_basePoint.X, _currentPoint.Y, 0);
// 使用 WorldGeometry 绘制矩形
draw.Geometry.Polygon(
new Point3d[] { _corner1, _corner2, _corner3, _corner4 }
);
return true;
}
/// <summary>
/// 获取四个角点(用于后续创建实体)
/// </summary>
public Point3d[] GetCorners() => new[] { _corner1, _corner2, _corner3, _corner4 };
}
设计要点:
- 使用
DrawJig而非EntityJig,因为需要同时预览多条线段。 - 必须重写
WorldDraw()(即WorldDraw),通过Geometry.Polygon()绘制四条边构成的矩形。 - 拖拽过程中没有创建任何实体,只做纯几何绘制,性能更优。
四、EntityJig 与 DrawJig 对比
基于官方文档和实践经验,两种Jig的选择标准如下:
| 特性 | EntityJig | DrawJig |
|---|---|---|
| 适用场景 | 单个实体拖拽 | 多实体 / 复杂图形 / 自定义绘制 |
| 实体管理 | 自动管理 Entity 生命周期 | 不自动管理,由开发者控制 |
| 绘制方式 | 自动调用 Entity.WorldDraw() |
必须手动重写 WorldDraw() |
| Entity 属性 | 可直接在 Update() 中修改 |
N/A |
| 代码量 | 较少 | 较多(需要手动绘制) |
| 典型例子 | Line、Circle、BlockReference | 矩形(多线)、双线墙、组合图形 |
选择建议:
- 若只绘制一个实体 (线、圆、弧、文字、块参照等),用
EntityJig。 - 若需同时绘制多个实体 或自定义几何图形 (如双线墙、带标注的矩形、预览符号等),用
DrawJig。
五、常见错误与注意事项
5.1 忘记在取消时释放 Entity
csharp
// ❌ 错误 ------ 取消时未释放 Entity,造成内存泄漏
if (dragRes.Status != PromptStatus.OK)
{
// 缺少 Dispose()
return;
}
// ✅ 正确
if (dragRes.Status != PromptStatus.OK)
{
jig.GetEntity()?.Dispose();
return;
}
5.2 Sampler() 中不检查 NoChange
csharp
// ❌ 错误 ------ 每次都返回 OK,导致频繁刷新闪烁
_endPoint = res.Value;
return SamplerStatus.OK;
// ✅ 正确
if (_endPoint.DistanceTo(res.Value) < Tolerance.Global.EqualPoint)
return SamplerStatus.NoChange; // 减少不必要的重绘
_endPoint = res.Value;
return SamplerStatus.OK;
5.3 混淆 EntityJig 和 DrawJig 的 WorldDraw
EntityJig 内部自动调用 Entity.WorldDraw() 进行重绘,开发者禁止 也不需要在 EntityJig 子类中重写 WorldDraw()。如果你需要手动绘制几何图形(如多段线或双线墙),应该改用 DrawJig。
5.4 用户取消时的处理
官方论坛有开发者报告平移(Pan)操作会触发 PromptStatus.Cancel,导致 Jig 非预期退出。解决方案是:在 Drag() 外层包装 do{}while() 循环,仅当检测到用户真正按 ESC 时才退出。
5.5 更新圆或弧时的半径保护
在 Update() 方法中要防止圆的半径为0,以免退化成点而导致程序崩溃:"对于圆实体,要避免在绘制初期或特定阶段给半径赋值为零,因为圆实体会退化成点"。
六、总结
EntityJig 是 AutoCAD .NET 二次开发中实现动态交互的核心组件:
- 继承体系 :
Jig → EntityJig / DrawJig,选对基类是成功的第一步。 - 方法重写 :
Sampler()负责采样用户输入,Update()负责将采样数据应用到Entity上。不需要 重写WorldDraw()。 - 生命周期管理:Entity 从创建、更新到最终加入数据库或释放,需要开发者清晰把控。
- 性能优化 :在
Sampler()中合理使用SamplerStatus.NoChange减少闪烁。 - 异常处理:始终考虑用户取消的场景,确保 Entity 被正确释放。
掌握了这些核心要点后,不仅能正确写出Line、Circle、Rectangle等实体的 Jig,还能理解为什么之前的墙体绘制代码中 WorldDraw() 会导致编译错误------因为 EntityJig 根本不应该重写 WorldDraw(),这正是 DrawJig 的职责。
参考资源
- Autodesk 官方文档 --- EntityJig Class
- Kean Walmsley --- A framework for defining AutoCAD entity jigs using .NET
- 明经CAD社区 --- EntityJig 与 DrawJig 区别讨论
AutoCAD.EntityTools