AutoCAD .NET 二次开发:深入理解 EntityJig 的工作原理与正确实现

AutoCAD .NET 二次开发:深入理解 EntityJig 的工作原理与正确实现

前言

在 AutoCAD .NET 二次开发中,EntityJig 是实现交互式拖拽(Jig)技术的重要基类。它能让用户在绘制实体时实时预览图形------移动鼠标,实体随之动态变化,所见即所得。这种交互模式广泛应用于直线绘制、墙体拖拽、圆半径调整、块插入定位等场景,对提升用户体验至关重要。

然而,开发者常因对 EntityJig 的内部机制理解不透彻,导致实现出错或遇到类似 eKeyNotFound 等诡异异常。其中最常见的误区之一便是混淆 EntityJigDrawJig 的角色:前者通过自动调用 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 类都必须从 EntityJigDrawJig 二者之一派生,并覆写相应的抽象方法

  • 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 对象自行绘制所有图形。


三、完整实现示例

以下示例严格遵循上述规范,针对不同实体类型给出相异的实现方法:在 LineJigCircleJig 中通过继承 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;
}

设计要点

  • 使用 JigPromptDistanceOptionsAcquireDistance(),以距离值(圆心到鼠标的投影距离)作为半径输入。
  • 设置 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 二次开发中实现动态交互的核心组件:

  1. 继承体系Jig → EntityJig / DrawJig,选对基类是成功的第一步。
  2. 方法重写Sampler() 负责采样用户输入,Update() 负责将采样数据应用到 Entity 上。不需要 重写 WorldDraw()
  3. 生命周期管理:Entity 从创建、更新到最终加入数据库或释放,需要开发者清晰把控。
  4. 性能优化 :在 Sampler() 中合理使用 SamplerStatus.NoChange 减少闪烁。
  5. 异常处理:始终考虑用户取消的场景,确保 Entity 被正确释放。

掌握了这些核心要点后,不仅能正确写出Line、Circle、Rectangle等实体的 Jig,还能理解为什么之前的墙体绘制代码中 WorldDraw() 会导致编译错误------因为 EntityJig 根本不应该重写 WorldDraw(),这正是 DrawJig 的职责。


参考资源

相关推荐
Bat U3 小时前
JavaEE|多线程初阶(七)
java·开发语言
谭欣辰3 小时前
C++ 排列组合完整指南
开发语言·c++·算法
foundbug9994 小时前
自适应滤除直达波干扰的MATLAB实现
开发语言·算法·matlab
XDH_CS4 小时前
MySQL 8.0 安装与 MySQL Workbench 使用全流程(超详细教程)
开发语言·数据库·mysql
小短腿的代码世界4 小时前
Qt实时盈亏计算深度解析:从持仓数据到动态盈亏展示
开发语言·qt
小康小小涵5 小时前
基于ESP32S3实现无人机RID模块底层源码编译
linux·开发语言·python
lzjava20245 小时前
Python的函数
开发语言·python
Awesome Baron5 小时前
skill、tool calling、MCP区别
开发语言·人工智能·python