基于ObjectArx的自定义矩形实体开发实战

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在CAD开发中,使用ObjectArx结合C++可实现高度定制化的自定义实体。本文以创建一个具备移动、向上拉伸和向右拉伸功能的矩形实体为例,深入讲解如何通过继承AcDbEntity类构建可交互的图形对象。内容涵盖夹点定义与响应、几何变换处理、图形绘制、数据持久化及模块注册加载等关键技术,适用于需要扩展AutoCAD功能的开发者。项目经过测试验证,可作为ObjectArx二次开发的典型范例,帮助开发者掌握自定义实体的完整实现流程。
自定义实体

1. 自定义实体基础:继承AcDbEntity类与虚函数实现

1.1 自定义实体的继承结构与核心要求

在ObjectARX中,所有图形实体必须从 AcDbEntity 派生。该基类定义了图形对象在AutoCAD数据库中的基本行为,包括绘制、选择、持久化等关键功能。创建合法自定义实体时,需重写其纯虚函数以满足系统调用契约。

class MyCustomEntity : public AcDbEntity {
    ACDB_DECLARE_MEMBERS(MyCustomEntity);
public:
    MyCustomEntity();              // 构造函数应初始化几何数据
    virtual ~MyCustomEntity();     // 析构函数需处理资源释放
};

代码说明 ACDB_DECLARE_MEMBERS 宏用于注册RTTI和类工厂支持,是实现动态创建的前提。构造函数中应避免复杂逻辑,确保线程安全与数据库上下文隔离。

1.2 必须重写的虚函数及其作用

函数名 作用说明
isA() 返回类的静态类型信息(如 AcDb::kMyCustomEntity
desc() 获取类描述器,用于动态实例创建
className() 返回字符串类名,便于调试与日志输出
clone() 实现深拷贝语义,支持复制、阵列等命令

这些函数构成ObjectARX运行时类型识别(RTTI)体系的基础,缺一不可。

1.3 类工厂与RTTI注册机制

为使AutoCAD能动态创建实例,必须通过 acrxBuildClassHierarchy() 注册类,并使用 AcDb::addToRtdcEr() 将类加入运行时对象字典:

AC_IMPLEMENT_CONS(MyCustomEntity, AcDbEntity, AcDb::kMTLoading);

此宏自动实现 constructor protocol class 绑定,确保在DWG打开时可正确反序列化对象。

此外,自定义实体应遵循内存对齐规范(通常为8字节),并在多线程环境中通过 AcDbDatabase::lockMode() 协调访问权限,防止并发修改引发崩溃。

2. 夹点机制原理与核心函数重写

在AutoCAD的交互式编辑环境中,夹点(Grips)是用户最频繁使用的图形操作接口之一。它们以可视化的小方块或圆形标记形式出现在实体的关键几何位置上,如端点、中点、圆心等,允许用户通过鼠标直接拖动来修改实体形状或位置。这种直观的操作方式极大地提升了设计效率和用户体验。然而,在自定义实体开发中,若未正确实现夹点相关逻辑,则会导致选择无响应、拖动异常甚至崩溃等问题。因此,深入理解夹点机制的工作原理,并精准重写 AcDbEntity 中的关键虚函数,是构建可交互、高稳定性的自定义图形对象的前提。

夹点系统并非简单的UI控件叠加,而是一个由状态机驱动、与数据库事务深度耦合的动态反馈机制。其背后涉及坐标变换、投影计算、实时更新、批量复制同步等多个技术层面。开发者必须掌握从获取夹点坐标到响应移动、再到视觉反馈控制的完整流程。本章将围绕 getGripPoints() moveGripPointsAt() gripStatus() bulkMoveReplication() 四大核心函数展开,结合代码实例、数据结构分析与流程图展示,全面揭示夹点系统的底层运行机制。

2.1 夹点交互的基本概念与工作流程

夹点作为AutoCAD中最基础的编辑辅助工具,承担着连接用户意图与几何变换之间的桥梁作用。当用户选中一个实体时,系统会自动调用该实体的夹点相关方法,提取可用于编辑的控制点,并在屏幕上渲染为可交互的视觉元素。这些控制点不仅是静态的位置标识,更是动态行为的触发器——一旦被激活,即可引发平移、拉伸、旋转等多种变换操作。

2.1.1 夹点在AutoCAD编辑操作中的作用

夹点的核心价值在于“免命令编辑”(Commandless Editing)。传统绘图操作通常需要先输入命令(如MOVE、STRETCH),再选择对象和参数;而夹点模式下,用户只需单击实体,即可进入编辑状态,通过拖动特定夹点完成常见修改,无需切换命令行。例如,拖动线段端点可延长线段,拖动矩形角点可缩放尺寸,拖动圆心可整体移动圆形。

对于自定义实体而言,合理设计夹点布局意味着赋予其智能编辑能力。比如一个表示门窗的复合实体,可以在四角设置尺寸控制夹点,在中心设置旋转夹点,在顶部中点设置高度调节夹点。每个夹点绑定不同的几何属性,使得用户能以直觉化的方式调整复杂对象。

更重要的是,夹点不仅仅是视觉提示,它还参与AutoCAD的撤销/重做机制、捕捉系统联动以及多对象协同编辑。因此,开发者在实现夹点逻辑时,不仅要关注位置准确性,还需确保其在整个编辑生命周期中保持一致性。

夹点类型 典型用途 触发操作
端点夹点 修改长度或位置 拉伸、移动
中点夹点 对称调整 镜像、对齐
圆心夹点 整体位移或旋转 移动、旋转
角点夹点 缩放边界框 拉伸、缩放
自定义控制点 特殊参数调节 动态尺寸调整

上述表格展示了不同夹点类型的语义含义及其对应的编辑行为。在实际开发中,应根据实体的功能需求定义合理的夹点集合,避免冗余或遗漏。

2.1.2 夹点状态机:选中、悬停、拖动与释放

夹点的行为本质上是由一个状态机构建的。当用户与实体交互时,夹点会在多个状态之间切换,每种状态对应不同的视觉表现和逻辑处理:

stateDiagram-v2
    [*] --> Idle
    Idle --> Hover: 鼠标进入夹点区域
    Hover --> Selected: 单击左键
    Selected --> Dragging: 开始拖动
    Dragging --> Updated: 应用变换
    Updated --> Idle: 释放鼠标
    Dragging --> Idle: 取消操作(ESC)
    note right of Hover
      显示高亮边框,
      提示可操作性
    end note
    note right of Selected
      夹点变为实心,
      进入编辑准备状态
    end note
    note right of Dragging
      实时调用 moveGripPointsAt()
      更新几何
    end note

该状态图清晰地描绘了夹点从静止到交互全过程的状态流转。值得注意的是,“Dragging”状态下系统会不断调用 moveGripPointsAt() 函数进行增量更新,而最终确认修改后才会提交至数据库。这一机制保证了操作的流畅性和可逆性。

此外,夹点状态也受到AutoCAD全局模式的影响,如极轴追踪、对象捕捉、夹点颜色方案等。开发者可通过查询 acedGetVar("GRIPOBJCOLOR") 等系统变量来自适应显示风格,提升集成度。

2.1.3 AcDbEntity中夹点相关虚函数调用时序

在ObjectARX框架中,夹点功能依赖于 AcDbEntity 类提供的四个主要虚函数,它们在编辑过程中按特定顺序被调用:

virtual Acad::ErrorStatus getGripPoints(
    AcGePoint3dArray& gripPoints,
    AcDbIntArray& osnapModes,
    AcDbStringArray& gStrips
) const;

virtual Acad::ErrorStatus moveGripPointsAt(
    const AcDbIntArray& indices,
    const AcGeVector3d& offset
);

virtual void gripStatus(const AcDbGripData* pGripData, 
                        AcDb::GripStat status);

virtual Acad::ErrorStatus bulkMoveReplication(
    const AcGeVector3d& vec,
    AcDbEntity*& newEnt
);

以下是典型夹点操作的函数调用序列:

  1. 选择实体 → 调用 getGripPoints() 获取所有夹点坐标;
  2. 鼠标悬停 → 触发 gripStatus() 报告当前状态为 AcDb::kGripHovering
  3. 点击夹点 → 状态变为 AcDb::kGripSelected ,准备拖动;
  4. 开始拖动 → 周期性调用 moveGripPointsAt() 应用偏移向量;
  5. 释放鼠标 → 完成编辑,执行事务提交;
  6. 复制操作(如阵列) → 若启用批量复制,则调用 bulkMoveReplication() 创建新实例并同步夹点。

此调用链构成了完整的夹点交互闭环。任何一环缺失或错误实现都可能导致行为异常。例如,若 getGripPoints() 返回空数组,则实体无法显示夹点;若 moveGripPointsAt() 未正确更新内部数据成员,则几何不会发生变化。

为了验证调用流程,可通过调试日志输出各阶段信息:

Acad::ErrorStatus MyCustomEntity::getGripPoints(
    AcGePoint3dArray& gripPoints,
    AcDbIntArray& osnapModes,
    AcDbStringArray& gStrips
) const {
    // 添加调试输出
    acutPrintf(L"\n[DEBUG] getGripPoints called for entity %ls\n", 
               this->className());

    // 假设我们有两个夹点:起点和终点
    gripPoints.append(startPoint);
    gripPoints.append(endPoint);

    // 设置对象捕捉模式(0 表示无特殊捕捉)
    osnapModes.append(0); 
    osnapModes.append(0);

    // 可选:添加标签条带(用于显示数值)
    gStrips.append(AcDbString(L"Start"));
    gStrips.append(AcDbString(L"End"));

    return Acad::eOk;
}

代码逻辑逐行解读:

  • 第5行:使用 acutPrintf 打印调试信息,便于跟踪函数调用时机;
  • 第9–10行:将实体的两个关键点(如线段起终点)加入 gripPoints 数组,这是夹点坐标的来源;
  • 第13–14行: osnapModes 用于指定每个夹点支持的对象捕捉类型(如 kOsModeEnd ),此处设为0表示默认行为;
  • 第17–18行: gStrips 可附加文本标签,在夹点附近显示说明,增强可读性;
  • 最终返回 Acad::eOk 表示成功。

该函数必须确保所有输出参数都被正确填充,否则可能引发访问违规。同时应注意, gripPoints 中的点必须位于世界坐标系(WCS)下,否则会出现投影错位问题。

2.2 获取夹点坐标:getGripPoints()函数详解

getGripPoints() 是夹点机制的入口函数,负责向AutoCAD报告当前实体可供操作的所有控制点位置。其执行结果直接影响用户能否看到夹点以及夹点是否位于正确的几何位置上。由于自定义实体往往具有非标准拓扑结构,如何准确生成夹点坐标成为实现高质量交互的关键。

2.2.1 函数原型解析与参数含义说明

函数声明如下:

virtual Acad::ErrorStatus getGripPoints(
    AcGePoint3dArray& gripPoints,
    AcDbIntArray& osnapModes,
    AcDbStringArray& gStrips
) const;

各参数意义如下:

参数名 类型 方向 说明
gripPoints AcGePoint3dArray& 输出 接收夹点的世界坐标列表
osnapModes AcDbIntArray& 输出 每个夹点对应的对象捕捉模式枚举值
gStrips AcDbStringArray& 输出(可选) 显示在夹点旁的文字标签

其中, AcGePoint3dArray 是ObjectARX中用于存储三维点的动态数组,基于 AcArray<AcGePoint3d> 封装; AcDbIntArray 则用于整数索引或标志位传递。

函数应在常量上下文中安全执行,不得修改实体状态。返回值应为 Acad::eOk 表示成功,其他错误码如 Acad::eNoMemory 表示失败。

2.2.2 gripPoints输出数组填充策略

填充策略需根据实体类型决定。以下是一个复杂自定义实体(如带标注的箭头符号)的夹点配置示例:

Acad::ErrorStatus ArrowSymbol::getGripPoints(
    AcGePoint3dArray& gripPoints,
    AcDbIntArray& osnapModes,
    AcDbStringArray& gStrips
) const {

    // 主要控制点:箭头尾部(位置控制)
    gripPoints.append(m_tailPoint);
    osnapModes.append(0);
    gStrips.append(L"Tail");

    // 箭头头部(方向与长度控制)
    gripPoints.append(m_headPoint);
    osnapModes.append(0);
    gStrips.append(L"Head");

    // 标注文字位置(独立调整)
    if (!m_textPosition.isZeroLength()) {
        gripPoints.append(m_textPosition);
        osnapModes.append(0);
        gStrips.append(L"Text");
    }

    return Acad::eOk;
}

逻辑分析:

  • 第7–9行:添加尾部控制点,用于整体移动;
  • 第11–13行:头部夹点允许拉伸改变箭头长度;
  • 第15–19行:条件性添加文字位置夹点,体现模块化设计思想;
  • 所有点均以WCS坐标存储,确保跨视图一致性。

建议对夹点编号建立常量映射,便于后续 moveGripPointsAt() 识别:

enum GripIndex {
    kTail = 0,
    kHead = 1,
    kText = 2
};

这样可在移动函数中快速判断操作目标。

2.2.3 投影点与世界坐标的转换关系处理

一个重要问题是:某些夹点可能位于局部坐标系中(如旋转后的矩形角点),需转换为WCS后再输出。例如:

AcGeMatrix3d xform = this->blockTransform(); // 获取实体变换矩阵
AcGePoint3d localPt(10.0, 5.0, 0.0);
AcGePoint3d worldPt = localPt.transformBy(xform);
gripPoints.append(worldPt);

其中 transformBy() 应用仿射变换,包括平移、旋转、缩放。忽略此步骤会导致夹点漂移。

此外,若实体包含嵌套几何(如块参照内的对象),还需考虑嵌套变换栈的累积效应。推荐使用 AcDbEntity::worldDraw() 中相同的变换逻辑,保持一致性。

graph TD
    A[Local Coordinates] --> B(Transform Matrix)
    B --> C{Apply transformBy?}
    C -->|Yes| D[World Coordinates]
    C -->|No| E[Incorrect Grip Position]
    D --> F[Proper Grip Display]

该流程图强调了坐标转换的必要性。实践中应始终验证夹点位置是否随视图旋转保持固定。

2.3 更新夹点位置:moveGripPointsAt()方法重写

2.3.1 移动向量的计算与应用逻辑

moveGripPointsAt() 是夹点编辑的核心响应函数,负责接收用户拖动产生的偏移向量并更新实体几何。

Acad::ErrorStatus MyRectangle::moveGripPointsAt(
    const AcDbIntArray& indices,
    const AcGeVector3d& offset
) {
    for (int i = 0; i < indices.length(); ++i) {
        int idx = indices[i];
        switch (idx) {
            case 0: // 左下角
                m_lowerLeft += offset;
                m_upperLeft.y += offset.y;
                m_lowerRight.x += offset.x;
                break;
            case 1: // 右上角
                m_upperRight += offset;
                m_upperLeft.x += offset.x;
                m_lowerRight.y += offset.y;
                break;
        }
    }
    return Acad::eOk;
}

参数说明:

  • indices : 被拖动的夹点索引数组(可能多个同时拖动);
  • offset : 当前步长的移动向量(WCS单位);

逐行分析:

  • 第5–6行:遍历所有受影响的夹点;
  • 第7–15行:根据索引更新对应顶点及关联边,实现联动变形;
  • 注意仅修改内存中的数据成员,不立即写入数据库。

2.3.2 多夹点联动更新的实现技巧

为支持多夹点协同编辑(如拉伸矩形对角),可引入“脏标记”机制延迟重绘:

bool m_bGeomDirty;
Acad::ErrorStatus moveGripPointsAt(...) {
    // ...
    m_bGeomDirty = true;
    return Acad::eOk;
}

并在 subWorldDraw() 中检测该标记以优化性能。

2.3.3 撤销/重做支持与数据库事务一致性保障

所有变更应在事务保护下进行:

AcTransaction* pTr = acdbHostApplicationServices()->workingDatabase()->getCurrentTransaction();
// 使用pTr记录旧状态,支持undo

或利用 AcDbObject upgradeOpen() 确保写权限。

2.4 夹点显示样式控制:gripStatus()与bulkMoveReplication()

2.4.1 不同编辑状态下夹点外观反馈机制

gripStatus() 用于监听状态变化:

void MyEntity::gripStatus(const AcDbGripData* pGripData, AcDb::GripStat status) {
    switch (status) {
        case AcDb::kGripSelected:
            acutPrintf(L"Grip selected!\n");
            break;
        case AcDb::kGripDragging:
            // 启动预览绘制
            break;
    }
}

可用于触发动画或临时图形叠加。

2.4.2 批量移动时复制实例的夹点同步问题

bulkMoveReplication() 用于阵列等操作:

Acad::ErrorStatus bulkMoveReplication(const AcGeVector3d& vec, AcDbEntity*& newEnt) {
    newEnt = new MyEntity(*this); // 复制自身
    newEnt->moveGripPointsAt(AcDbIntArray(), vec); // 同步位移
    return Acad::eOk;
}

确保新实体夹点位置正确。

3. 拉伸夹点逻辑设计与动态尺寸调整

在AutoCAD的交互式图形编辑环境中,拉伸操作是用户最频繁使用的几何修改手段之一。对于自定义实体而言,实现精确、直观且符合工程习惯的拉伸行为,不仅依赖于对 AcDbEntity 类体系的理解,更要求开发者深入掌握夹点机制与几何更新之间的耦合关系。本章聚焦于“拉伸”这一典型编辑动作,系统阐述如何通过顶部和右侧控制夹点实现高度与宽度的动态调整,并在此基础上构建具备实时反馈、多维度协调能力的高级拉伸逻辑。不同于简单的坐标移动,真正的拉伸夹点设计需综合考虑几何依赖、数据绑定、视觉预览以及事务一致性等多重因素。

拉伸的本质是对实体关键控制点的操作响应。当用户选中某个夹点并拖动时,系统会调用 moveGripPointsAt() 方法,传入一个位移向量,开发者需要根据该向量更新内部几何参数(如高度或宽度),同时确保其他相关元素(如文本标签、子图形)同步变化。此过程涉及从屏幕空间到模型空间的转换、局部坐标系的应用、边界约束判断以及撤销/重做支持等多个层面的技术细节。尤其在复杂实体中,多个夹点可能共享同一组数据源,若处理不当极易引发状态不一致或视觉错乱。

此外,现代CAD用户体验强调“所见即所得”,因此动态预览成为不可或缺的一环。传统的静态重绘方式已无法满足流畅交互的需求,必须借助临时实体叠加技术或直接操作图形子系统(如 AcGsView )来实现实时刷新。而当高度与宽度同时被拉伸时,还需引入优先级判定机制,避免因夹点冲突导致逻辑混乱。这些内容构成了本章的核心讨论范畴。

3.1 向上拉伸夹点:高度动态调整机制

向上拉伸夹点通常用于改变实体的垂直尺寸,例如柱体的高度、门框的净空或标注块的标注范围。该夹点一般位于实体的顶部中心或角点位置,其移动直接影响实体的高度属性。要实现这一功能,首先需明确定义顶部夹点的几何语义及其与主体结构的依赖关系。

3.1.1 定义顶部控制夹点及其几何依赖关系

顶部夹点并非孤立存在,而是与实体的底面基准线、中心轴或锚点构成明确的几何层级。以一个矩形柱体为例,假设其底部固定在Z=0平面上,顶部夹点位于 (x, y, h) ,其中 h 为当前高度。此时,该夹点的Z坐标直接由高度字段 m_height 计算得出:

AcGePoint3d topGripPoint = AcGePoint3d(baseX, baseY, m_height);

这种显式的数学映射关系称为 几何依赖 ,它是实现动态更新的基础。一旦 m_height 发生变化,所有依赖它的几何元素(包括夹点、轮廓线、填充区域等)都应随之刷新。

更重要的是,在持久化存储中,我们不应保存夹点坐标本身,而只保存原始参数(如 m_height )。这样可以避免数据冗余和不一致风险。例如,若将夹点坐标也写入DWG文件,则在反序列化后可能出现 m_height 与实际夹点Z值不符的情况。

字段名 类型 是否持久化 说明
m_basePoint AcGePoint3d 实体底部定位点
m_width double 实体宽度
m_height double 实体高度(决定顶部夹点)
m_topGripId int 夹点索引标识符

上述表格清晰地划分了数据层与表现层的职责边界:仅基础参数参与序列化,夹点作为运行时派生信息按需生成。

3.1.2 基于moveGripPointsAt()的高度变更响应

moveGripPointsAt() 是实现拉伸逻辑的关键虚函数。当用户拖动顶部夹点时,AutoCAD会调用此方法,并传入夹点ID和位移向量数组。以下是典型实现代码:

Adesk::Boolean MyCustomEntity::moveGripPointsAt(
    const AcDbVoidPtrArray& gripAppData,
    const AcGeVector3d& offset,
    const int bitflags)
{
    for (int i = 0; i < gripAppData.length(); i++) {
        void* pData = gripAppData[i];
        if (pData == &m_topGripTag) { // 使用唯一标记识别顶部夹点
            AcGePoint3d currentTop = getTopPoint(); // 获取当前顶部位置
            AcGePoint3d newTop = currentTop + offset;

            // 将新Z坐标映射回高度参数
            double newHeight = newTop.z;
            if (newHeight >= MIN_HEIGHT) {
                m_height = newHeight;
                updateBoundaries(); // 触发边界重算
                return Adesk::kTrue;
            }
        }
    }
    return Adesk::kFalse;
}
代码逻辑逐行解读:
  • 第2行 :函数签名遵循ObjectARX标准, gripAppData 用于区分不同夹点, offset 为世界坐标系下的位移。
  • 第4–5行 :遍历所有受影响的夹点数据指针。每个夹点可通过附加数据(如标签地址)进行标识。
  • 第6行 :使用 &m_topGripTag 作为唯一句柄匹配顶部夹点,避免硬编码索引带来的脆弱性。
  • 第8–9行 :获取当前顶部位置并应用偏移量,得到目标位置。
  • 第12–14行 :提取新的Z值作为候选高度,并检查是否满足最小尺寸约束。
  • 第15行 :更新成员变量并触发边界更新,确保后续绘制正确反映新尺寸。

此设计体现了“参数驱动几何”的思想——外部输入影响核心参数,再由参数驱动整体形态变化。

3.1.3 高度边界检测与最小尺寸约束实现

无限制的拉伸可能导致非法状态,如负高度或过小尺寸导致渲染异常。为此,必须设置合理的边界条件。常见的做法是在 moveGripPointsAt() 中加入校验逻辑:

double clampHeight(double rawHeight) {
    return std::max(rawHeight, MIN_HEIGHT);
}

其中 MIN_HEIGHT 可定义为0.1单位(防止退化为线段)。进一步地,还可引入最大高度限制:

const double MAX_HEIGHT = 1000.0; // 单位:毫米
double clampedHeight = std::clamp(newTop.z, MIN_HEIGHT, MAX_HEIGHT);

为了增强用户体验,还可以结合 AcEdUserInputInterface 在接近边界时发出视觉提示(如变色或震动反馈),但这需要额外的命令上下文支持。

flowchart TD
    A[用户拖动顶部夹点] --> B{调用 moveGripPointsAt}
    B --> C[解析位移向量]
    C --> D[计算新Z坐标]
    D --> E{是否 ≥ 最小高度?}
    E -- 是 --> F[更新 m_height]
    E -- 否 --> G[保持原高度]
    F --> H[触发 redraw]
    G --> I[忽略更改]
    H --> J[完成拉伸]
    I --> J

该流程图展示了从用户操作到内部状态更新的完整路径,突出了条件判断的关键作用。

3.2 向右拉伸夹点:宽度动态扩展逻辑

与高度类似,宽度的动态调整通过右侧夹点实现,常见于梁、墙、表格单元格等横向延展型实体。但由于方向性和坐标系的影响,其实现策略略有差异。

3.2.1 右侧控制点的位置设定与数据绑定

右侧夹点通常位于实体右边缘的中点或顶点。设实体左下角为 m_basePoint ,则右上角为:

AcGePoint3d rightGrip = AcGePoint3d(
    m_basePoint.x + m_width,
    m_basePoint.y + m_height * 0.5, // 中点
    m_basePoint.z
);

此处值得注意的是,夹点Y坐标取中间值是为了提升操作便利性,避免因过高或过低造成误触。该夹点的位置完全由 m_basePoint m_width 共同决定,形成 双向数据绑定 :夹点显示依赖宽度,而宽度又受夹点拖动影响。

3.2.2 宽度增长方向判断与局部坐标系变换

在某些场景下,实体可能旋转一定角度(非正交对齐),此时直接使用世界坐标的X分量会导致错误的宽度增量。解决方案是引入局部坐标系(LCS):

AcGeVector3d localXAxis = AcGeVector3d::kXAxis.rotateBy(m_rotationAngle, AcGeVector3d::kZAxis);
double projectedOffset = offset.dotProduct(localXAxis);
m_width += projectedOffset;

上述代码将全局位移向量投影到实体自身的X轴方向,从而准确捕获“横向”拉伸分量。这种方法适用于任意朝向的实体,显著提升了鲁棒性。

3.2.3 文本或子元素随宽度自动布局调整策略

许多自定义实体包含嵌入式文本(如编号、尺寸标注)或其他子图形。当宽度变化时,这些元素应自动重新排布。例如,文本居中对齐的实现如下:

void MyCustomEntity::updateTextPosition() {
    AcGePoint3d textCenter(
        m_basePoint.x + m_width / 2.0,
        m_basePoint.y + m_height / 2.0,
        m_basePoint.z
    );
    m_textPosition = textCenter;
}

此方法应在每次 m_width m_height 变更后调用,保证视觉一致性。更复杂的布局可采用仿 Flexbox 的弹性算法,根据剩余空间分配子元素间距。

// 示例:等间距分布多个子点
void distributeChildren(double totalWidth, int childCount) {
    double spacing = totalWidth / (childCount + 1);
    for (int i = 0; i < childCount; ++i) {
        m_childPoints[i].x = m_basePoint.x + (i + 1) * spacing;
    }
}

该逻辑可用于创建参数化栏杆、窗格等重复结构。

3.3 拉伸过程中视觉反馈优化

高质量的交互体验离不开即时、平滑的视觉反馈。传统做法是在每次 moveGripPointsAt() 后调用 worldDraw() 重绘,但这种方式延迟高、闪烁严重。更好的方案是利用临时实体或直接访问图形子系统。

3.3.1 实时预览绘制:临时实体叠加技术

一种成熟的做法是创建一个 临时实体(Transient Entity) ,在拉伸过程中动态显示预览效果:

AcDbEntity* pPreviewEnt = createPreviewEntity();
acedTransViewportToDisplay(&offset, &dispOffset);
pPreviewEnt->setPosition(m_currentPos + dispOffset);
acdbAddTransient(pPreviewEnt, AcDb::kNoneOpen, nullptr, nullptr);
  • acdbAddTransient() 将实体注册为瞬态对象,仅在当前视口中显示,不影响数据库。
  • 拉伸结束时调用 acdbRemoveTransients() 清除预览。

优点是无需侵入主实体绘制逻辑,缺点是需维护额外的预览实体类。

3.3.2 使用AcGsView进行动态刷新控制

更底层的方式是通过 AcGsView 接口直接操控图形管道:

AcGsView* pView = getCurrentGsView();
if (pView) {
    pView->invalidate(AcGsView::kGeometry);
    pView->sync();
}
  • invalidate() 标记几何区域失效,触发重绘。
  • sync() 强制同步刷新,减少延迟。

这种方式性能更高,适合高频更新场景,但需谨慎使用以免引起性能瓶颈。

graph LR
    U[用户拖动夹点] --> M[moveGripPointsAt]
    M --> C[计算新尺寸]
    C --> P[生成预览]
    P --> T[添加临时实体] 
    T --> R[调用 invalidate/sync]
    R --> V[屏幕刷新]
    V --> U

该流程图揭示了从输入到输出的闭环反馈机制。

3.4 多维度联合拉伸协调机制

当实体具有多个可拉伸夹点(如右上角同时控制宽高)时,必须解决协同更新问题。

3.4.1 高度与宽度同时变化时的数据一致性维护

若用户拖动角落夹点,需同时更新两个维度。此时应确保两个参数独立更新,互不干扰:

if (pData == &m_cornerGripTag) {
    AcGePoint3d newPos = getCurrentCorner() + offset;
    m_width  = std::max(newPos.x - m_basePoint.x, MIN_WIDTH);
    m_height = std::max(newPos.z - m_basePoint.z, MIN_HEIGHT);
    return Adesk::kTrue;
}

注意:使用 std::max 防止负值,且各自独立判断边界。

3.4.2 夹点优先级判定与冲突规避算法

当多个夹点响应同一事件时,可能发生冲突。建议引入优先级机制:

夹点类型 优先级值
角落夹点 100
边缘夹点 50
中心夹点 10

moveGripPointsAt() 中按优先级顺序处理,一旦命中高优先级夹点即终止后续判断,防止重复更新。

最终,完整的拉伸系统应具备:
- 参数驱动的几何更新
- 局部坐标系适应能力
- 实时视觉反馈
- 多夹点协同管理

唯有如此,才能打造出专业级的自定义实体交互体验。

4. 实体可视化绘制与OpenGL集成实现

在AutoCAD的ObjectARX开发中,自定义实体的可视化表现是用户交互体验的核心环节。一个图形对象是否能够准确、高效地渲染,直接决定了其可用性与专业度。 worldDraw() 函数作为 AcDbEntity 类中最关键的虚函数之一,承担着将几何语义转换为视觉呈现的桥梁作用。本章深入探讨基于 AcGi 接口的图形生成机制,并结合 OpenGL 原生绘图能力,构建高性能、高保真度的可视化体系。通过剖析 AutoCAD 图形管道的内部流程,掌握如何在不同视图模式下保持一致的显示效果,同时应对现代高DPI屏幕带来的挑战。

4.1 worldDraw()函数的作用与调用机制

worldDraw() 是每个从 AcDbEntity 派生的自定义实体必须重写的虚函数,用于描述该实体在模型空间或图纸空间中的几何外观。它并非直接操作像素,而是通过向 AcGi (AutoCAD Graphics Interface)接口提交绘图指令来间接完成绘制任务。这种抽象层设计使得 AutoCAD 可以灵活切换底层渲染引擎(如 GDI、OpenGL、Direct3D),而无需修改上层实体逻辑。

4.1.1 AutoCAD图形管道中的绘制阶段划分

AutoCAD 的图形渲染过程是一个多阶段流水线系统,主要包括以下四个阶段:

  1. Scene Graph Construction(场景图构建)
    数据库遍历所有可见实体,调用其 worldDraw() 方法,收集几何数据并构建成中间表示形式。
  2. View Culling(视锥剔除)
    根据当前视图方向和范围,剔除不可见对象,减少后续处理负担。
  3. Geometry Processing(几何处理)
    AcGi 提交的数据转换为底层渲染器可识别的格式(顶点、索引、材质等)。
  4. Rendering(渲染输出)
    使用 OpenGL 或其他后端执行实际绘制,最终合成到屏幕上。

这一流程可通过如下 mermaid 流程图展示:

graph TD
    A[Start Frame Rendering] --> B{Iterate Database Entities}
    B --> C[Call worldDraw() on Each Visible Entity]
    C --> D[Collect Geometry via AcGi Interface]
    D --> E[Build Scene Graph]
    E --> F[Perform View Frustum Culling]
    F --> G[Convert to Device-Specific Format]
    G --> H[Render with OpenGL/Direct3D]
    H --> I[Present to Screen]

在整个过程中, worldDraw() 被调用的时间点位于“场景图构建”阶段。此时,AutoCAD 并不保证任何特定的渲染上下文已激活,因此不能使用原生 OpenGL 调用——除非采取特殊手段绕过限制(详见 4.3 节)。

此外, worldDraw() 的调用频率极高。例如,在缩放、平移或旋转视图时,每一帧都可能触发一次完整的 worldDraw() 遍历。因此,其实现应尽可能轻量,避免复杂计算或内存分配操作。

绘制阶段 是否允许修改数据库 是否可访问OpenGL Context 性能敏感程度
worldDraw() 执行期 ❌ 不允许 ⚠️ 通常不可用(除非显式获取) 极高
subWorldDraw() ❌ 不允许 ✅ 可配合特定视图启用
Custom Preview ✅ 允许(事务内) ✅ 完全控制

表:AutoCAD 绘制阶段特性对比

为了提升性能,建议将复杂的几何预计算结果缓存于实体私有成员中,并在 AcDbEntity::subErase() 或状态变更时更新。这样可在 worldDraw() 中仅做快速提交,显著降低每帧开销。

4.1.2 worldDraw与subWorldDraw的区别与使用场景

虽然 worldDraw() 是标准绘制入口,但在某些高级应用中,开发者需要更精细的控制权。这时就需要引入 subWorldDraw() 方法。

函数原型定义
Adesk::Boolean MyCustomEntity::worldDraw(AcGiWorldDraw* wd)
{
    assert(wd != nullptr);

    // 获取AcGi接口
    AcGiCommonDraw* pDraw = wd->geometry();

    // 绘制一条直线示意
    AcGePoint3d startPt(0, 0, 0);
    AcGePoint3d endPt(100, 0, 0);

    pDraw->drawLine(startPt, endPt);

    return Adesk::kTrue;
}

void MyCustomEntity::subWorldDraw(AcGiWorldDraw* wd) const
{
    // 默认调用worldDraw,但可以扩展
    MyCustomEntity* pThis = const_cast<MyCustomEntity*>(this);
    pThis->worldDraw(wd);
}
功能差异分析
特性 worldDraw() subWorldDraw()
虚函数性质 纯虚函数(必须实现) 非纯虚,提供默认实现
调用时机 主要用于常规重绘 支持递归嵌套绘制(如块参照)
this指针权限 可修改对象状态 必须为 const 成员函数
应用场景 基础几何绘制 复杂结构体(如嵌套块、动态代理)

表: worldDraw() subWorldDraw() 对比

subWorldDraw() 的主要用途体现在对复合实体的支持上。例如,当你的自定义实体包含多个子图形组件(如标注文字 + 引线 + 符号),且这些组件也需要独立参与夹点编辑或拾取检测时,可以通过重写 subWorldDraw() 来分别提交各部分的几何信息。

更重要的是, subWorldDraw() 在处理 嵌套引用 (如插入块表记录)时会被自动调用。AutoCAD 内部会遍历块内所有实体并调用其 subWorldDraw() ,从而确保完整还原整个层次结构。

实际应用场景代码示例

假设我们有一个名为 MyAnnotatedBoxEntity 的复合实体,由矩形框和中心文本组成:

void MyAnnotatedBoxEntity::subWorldDraw(AcGiWorldDraw* wd) const
{
    AcGiCommonDraw* pDraw = wd->geometry();

    // 绘制外框
    AcGePoint3d corners[4] = {
        m_minCorner,
        AcGePoint3d(m_maxCorner.x, m_minCorner.y, 0),
        m_maxCorner,
        AcGePoint3d(m_minCorner.x, m_maxCorner.y, 0)
    };

    for (int i = 0; i < 4; ++i) {
        pDraw->drawLine(corners[i], corners[(i+1)%4]);
    }

    // 绘制文本(简化版)
    AcString textContent = L"注释";
    AcGePoint3d textPos = m_minCorner.midpointTo(m_maxCorner);
    AcGiTextStyle textStyle;
    textStyle.setTextHeight(5.0);
    pDraw->drawText(textPos, AcGeVector3d::kZAxis, 1.0, 1.0, 0.0, textContent, false, kAcGiTextLeft, &textStyle);
}

代码逐行解读:

  • 第2行:获取 AcGiCommonDraw 接口,它是 drawLine , drawText 等方法的载体;
  • 第6~9行:定义矩形四个角点,基于存储的最小/最大坐标;
  • 第12~14行:循环绘制四条边线,形成闭合矩形;
  • 第17~21行:设置文本样式并调用 drawText() 显示居中文本;
  • AcGeVector3d::kZAxis 指定文本法线方向;
  • 最后参数 false 表示非宽松对齐, kAcGiTextLeft 控制水平对齐方式。

该实现确保了即使此实体被作为块插入其他图形中,其内部结构仍能正确渲染。若未重写 subWorldDraw() ,则可能导致子元素丢失。

综上所述, worldDraw() 是绘制起点,而 subWorldDraw() 则提供了更强的组合表达能力。两者协同工作,构成了 AutoCAD 可扩展图形系统的基石。

4.2 利用AcGi库进行几何体绘制

AcGi (AutoCAD Graphics Interface)是 ObjectARX 中专用于图形输出的抽象接口集合,屏蔽了底层渲染技术细节,使开发者专注于几何语义而非设备适配。其中最核心的是 AcGiGeometry 接口,它提供了丰富的绘图原语,支持从基本线条到复杂曲面的全方位表达。

4.2.1 AcGiGeometry接口常用方法:drawLine, drawCircularArc等

AcGiGeometry AcGiWorldDraw::geometry() 返回的对象类型,封装了所有可用于 worldDraw() 中的绘图命令。以下是几个关键方法及其应用场景:

常用绘图方法列表
方法名 功能说明 参数复杂度
drawLine(p1, p2) 绘制两点间直线段 ★☆☆☆☆
drawCircularArc(center, radius, startAngle, endAngle, isCW) 绘制圆弧 ★★☆☆☆
drawPolygon(numPoints, points) 绘制多边形轮廓 ★★☆☆☆
drawMesh(rows, cols, vertices, ...) 绘制网格曲面 ★★★★☆
drawShell(numFaces, faceList, numVertices, vertexArray) 绘制壳体(三维封闭体) ★★★★★

表:AcGiGeometry 主要绘图方法功能概览

以绘制一个带圆角矩形为例:

void DrawRoundedRectangle(AcGiCommonDraw* pDraw, const AcGePoint3d& base, 
                          double width, double height, double cornerRadius)
{
    if (cornerRadius <= 0.0) {
        // 直角矩形
        AcGePoint3d pts[4] = {
            base,
            base + AcGeVector3d(width, 0, 0),
            base + AcGeVector3d(width, height, 0),
            base + AcGeVector3d(0, height, 0)
        };
        pDraw->drawPolygon(4, pts);
        return;
    }

    AcGePoint3d center;

    // 左下圆角弧
    center = base + AcGeVector3d(cornerRadius, cornerRadius, 0);
    pDraw->drawCircularArc(center, cornerRadius, M_PI, 1.5 * M_PI, true);

    // 绘制连接线段
    AcGePoint3d start = center - AcGeVector3d::kXAxis * cornerRadius;
    AcGePoint3d end   = center + AcGeVector3d::kYAxis * cornerRadius;
    pDraw->drawLine(base + AcGeVector3d(0, cornerRadius, 0), start); // 左侧竖线
    pDraw->drawLine(end, base + AcGeVector3d(cornerRadius, height, 0)); // 上侧横线
}

逻辑分析:

  • 此函数根据 cornerRadius 决定是否启用圆角;
  • 若启用,则分别计算四个角落的圆弧中心;
  • 每个 drawCircularArc 调用需指定起止角度(弧度制)及顺时针标志;
  • 圆弧之间用 drawLine 连接,构成连续路径;
  • 注意: AcGi 不支持自动闭合路径,需手动补线。

此类方法适用于创建具有工业美感的 UI 元素或建筑构件轮廓。

4.2.2 填充区域与轮廓线的分层绘制策略

在工程制图中,常常需要区分“轮廓”与“填充”,例如实心柱体需有边界线加内部阴影。 AcGi 支持通过 AcGiFill 接口实现区域填充。

分层绘制实现方案
void MyFilledRectangleEntity::worldDraw(AcGiWorldDraw* wd)
{
    AcGiCommonDraw* pDraw = wd->geometry();
    AcGiFill fill;

    // 设置填充样式
    fill.setPattern(AcGiFill::kPredefinedSolid); // 实体填充
    fill.setFillColor(AcCmColor::kRGB, 200, 200, 255); // 浅蓝色

    // 定义矩形顶点(顺时针)
    AcGePoint3d verts[4] = {m_p1, m_p2, m_p3, m_p4};

    // 提交填充请求
    pDraw->drawPolygon(4, verts, nullptr, &fill);

    // 单独绘制边框(避免被填充覆盖)
    pDraw->drawLine(m_p1, m_p2);
    pDraw->drawLine(m_p2, m_p3);
    pDraw->drawLine(m_p3, m_p4);
    pDraw->drawLine(m_p4, m_p1);
}

参数说明:

  • setPattern(kPredefinedSolid) :选择预设实心填充;
  • setFillColor :RGB 值设定颜色;
  • drawPolygon 第四个参数传入 &fill 触发填充行为;
  • 边框线单独绘制以确保清晰可见。

该策略实现了“先填后描”的视觉层级,符合 CAD 制图规范。

flowchart LR
    A[开始绘制] --> B[配置AcGiFill参数]
    B --> C[调用drawPolygon提交填充]
    C --> D[逐边调用drawLine绘制轮廓]
    D --> E[结束绘制]

图:填充与轮廓分层绘制流程

实践中,还可利用 AcGiLayerInfo 控制图层归属,或将透明度信息传递给支持 Alpha 混合的视图,进一步增强表现力。

4.3 OpenGL底层绘图支持与性能优化

尽管 AcGi 提供了良好的跨平台兼容性,但在面对大规模动态图形(如实时仿真、GIS热力图)时,其抽象开销成为瓶颈。此时,直接嵌入 OpenGL 指令流成为必要选择。

4.3.1 在worldDraw中嵌入原生OpenGL指令流

AutoCAD 自 R2018 起全面采用 OpenGL 作为默认显示引擎。这为我们提供了直接访问 GPU 渲染通道的可能性。

启用条件与安全检查
Adesk::Boolean MyGLAcceleratedEntity::worldDraw(AcGiWorldDraw* wd)
{
    AcGiRegenType rt = wd->regenType();

    // 仅在 FullReGen 或 IncrementalReGen 时启用 OpenGL
    if (rt == kAcGiSilentReGen || rt == kAcGiHideReGen)
        return Adesk::kTrue;

    // 获取OpenGL上下文
    void* hRC = wglGetCurrentContext();
    if (!hRC) return Adesk::kFalse;

    // 获取AcGi context中的投影与模型视图矩阵
    AcGeMatrix3d projMat, modelViewMat;
    wd->getOrthoFrustum(&projMat, &modelViewMat);

    glMatrixMode(GL_PROJECTION);
    glLoadMatrixd(projMat.asDouble());

    glMatrixMode(GL_MODELVIEW);
    glLoadMatrixd(modelViewMat.asDouble());

    // 开始OpenGL绘制
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glBegin(GL_TRIANGLES);
    glColor4f(1.0f, 0.0f, 0.0f, 0.8f);
    glVertex3d(0, 0, 0);
    glVertex3d(10, 0, 0);
    glVertex3d(5, 10, 0);
    glEnd();

    glDisable(GL_BLEND);

    return Adesk::kTrue;
}

逐行解析:

  • 第4–7行:排除静默再生情况,防止后台崩溃;
  • 第10–11行:验证当前是否存在有效的 OpenGL 渲染上下文;
  • 第15–16行:从 wd 提取当前视图的变换矩阵;
  • 第19–20行:加载投影矩阵至 OpenGL 状态机;
  • 第23–34行:标准 OpenGL 绘制红色半透明三角形;
  • glEnable(GL_BLEND) 启用混合以支持透明度;

⚠️ 警告 :直接调用 OpenGL 存在风险。若未来版本更换渲染后端(如 Vulkan),此代码将失效。务必做好降级处理:

#ifdef USE_OPENGL_FALLBACK
    // 使用AcGi回退路径
    pDraw->drawLine(...);
#else
    // 抛出异常或日志警告
#endif

4.3.2 VBO缓存与顶点数组在频繁重绘中的应用

对于静态或低频更新几何体(如地形网格),可使用 Vertex Buffer Object(VBO)缓存数据至 GPU 显存,大幅提升重绘效率。

示例:初始化VBO
class GLVertexBuffer {
public:
    GLuint vboId;
    int vertexCount;

    GLVertexBuffer(const std::vector<AcGePoint3d>& points) : vertexCount(points.size()) {
        glGenBuffers(1, &vboId);
        glBindBuffer(GL_ARRAY_BUFFER, vboId);
        glBufferData(GL_ARRAY_BUFFER, 
                     vertexCount * sizeof(AcGePoint3d), 
                     points.data(), 
                     GL_STATIC_DRAW);
    }

    ~GLVertexBuffer() {
        if (vboId) glDeleteBuffers(1, &vboId);
    }
};
在worldDraw中使用VBO
void MyVBORenderedEntity::worldDraw(AcGiWorldDraw* wd)
{
    if (!m_pVBO || !wglGetCurrentContext()) 
        return BasicDrawFallback(wd); // 回退到AcGi

    wd->pushModelTransform(&m_transform); // 应用局部变换

    glBindBuffer(GL_ARRAY_BUFFER, m_pVBO->vboId);
    glVertexPointer(3, GL_DOUBLE, 0, 0);
    glEnableClientState(GL_VERTEX_ARRAY);

    glColor3f(0.0f, 1.0f, 0.0f);
    glDrawArrays(GL_LINE_STRIP, 0, m_pVBO->vertexCount);

    glDisableClientState(GL_VERTEX_ARRAY);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    wd->popModelTransform();
}

优势分析:

  • 数据驻留 GPU,避免每帧 CPU 到 GPU 传输;
  • GL_STATIC_DRAW 提示驱动优化存储位置;
  • 结合模型变换栈( pushModelTransform ),支持局部坐标系偏移;

⚠️ 注意:必须在正确的 OpenGL 上下文中调用,且不得干扰 AutoCAD 自身的状态管理。

4.4 视图适配与缩放一致性处理

随着高分辨率显示器普及,传统基于固定像素的绘制方式已无法满足清晰度要求。必须建立模型单位与屏幕像素之间的动态映射关系。

4.4.1 屏幕像素尺寸与模型空间单位的映射

AutoCAD 提供 AcGiWorldDraw::rawInverseZoom() 函数估算当前缩放比例下的“每单位对应像素数”。

double GetScreenPixelSizeInModelUnits(AcGiWorldDraw* wd)
{
    double invZoom = wd->rawInverseZoom();
    return 1.0 / invZoom; // 每像素对应的模型单位长度
}

void DrawScaleAwareLine(AcGiWorldDraw* wd, AcGePoint3d start, AcGePoint3d end)
{
    double pixelSize = GetScreenPixelSizeInModelUnits(wd);
    double desiredWidthPixels = 2.0; // 希望线宽为2像素
    double lineWidthUnits = desiredWidthPixels * pixelSize;

    AcGiCommonDraw* pDraw = wd->geometry();
    pDraw->setLineWidth(static_cast<Adesk::UInt16>(lineWidthUnits * 1000));
    pDraw->drawLine(start, end);
}

此方法可使线条宽度在不同缩放下始终保持视觉一致性。

4.4.2 高DPI显示环境下的线条平滑处理

启用抗锯齿需谨慎操作,以免影响整体性能:

glEnable(GL_LINE_SMOOTH);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glLineWidth(2.0f);

但应在 worldDraw() 中判断是否处于高质量模式:

if (wd->regenType() == kAcGiStandardDisplay) {
    // 启用抗锯齿
} else {
    // 使用普通线条
}

结合 DPI 检测 API(如 GetDeviceCaps(hDC, LOGPIXELSX) ),可实现自适应渲染策略,兼顾清晰度与流畅性。

5. 数据持久化与文件格式兼容性支持

在AutoCAD的ObjectARX开发中,自定义实体不仅需要具备可视化的表现能力和交互逻辑,还必须能够在DWG或DXF等标准文件格式中被正确保存和恢复。这种能力被称为 数据持久化(Persistence) ,是确保用户工作成果不丢失、跨会话可用的核心机制。本章将深入剖析ObjectARX框架下对象序列化的底层原理,围绕 dwgOutFields() dwgInFields() dxfOutFields() dxfInFields() 四大核心函数展开,系统讲解如何安全高效地实现自定义实体的数据存储,并保障其在不同版本AutoCAD之间以及跨平台环境下的兼容性。

5.1 DWG文件中的对象序列化机制

DWG作为AutoCAD专有的二进制数据库格式,承载了图形对象的所有几何信息、拓扑关系、样式设置及自定义属性。每一个从 AcDbEntity 派生的类都必须参与这一序列化过程,否则其状态无法随图纸保存。ObjectARX通过提供一系列虚函数接口,允许开发者控制对象字段的读写行为,其中最关键的是 dwgOutFields() dwgInFields() 两个方法。

5.1.1 dwgOutFields()导出字段的组织结构

当一个图形对象被写入DWG文件时,AutoCAD调用该对象的 dwgOutFields(AcDbDwgFiler* pFiler) 函数。此函数接收一个指向 AcDbDwgFiler 的指针,用于执行类型安全的数据输出操作。开发者需按顺序调用 pFiler->writeXXX() 系列方法,将对象的关键成员变量逐个写入流中。

Adesk::Boolean MyCustomEntity::dwgOutFields(AcDbDwgFiler* pFiler) const
{
    assert(pFiler);
    // 先调用基类以保证父类数据也被保存
    if (AcDbEntity::dwgOutFields(pFiler) != Acad::eOk)
        return Adesk::kFalse;

    // 写出自定义字段
    pFiler->writeDouble(m_height);           // 高度
    pFiler->writeDouble(m_width);            // 宽度
    pFiler->writeInt16(static_cast<short>(m_style)); // 枚举类型转为int16
    pFiler->writeBoolean(m_isVisible);       // 可见性标志
    pFiler->writePoint3d(m_origin);          // 原点位置

    return pFiler->filerStatus() == Acad::eOk ? Adesk::kTrue : Adesk::kFalse;
}
代码逻辑逐行解读:
  • 第4行 :使用断言确保 pFiler 非空,避免运行时崩溃。
  • 第7~8行 :首先调用基类 AcDbEntity::dwgOutFields() ,这是必须的操作,否则父类定义的通用字段(如图层、颜色、线型等)不会被保存。
  • 第11~15行 :依次调用 writeDouble writeInt16 等方法,按照预定顺序输出当前类的私有成员变量。注意: 读写顺序必须严格一致
  • 第17行 :最终检查 filerStatus() 是否成功,返回布尔值表示整体操作结果。

⚠️ 重要提示:所有写入操作应遵循“先基类后派生类”、“先公共后私有”的原则,且不得跳过任何字段,即使某些字段当前为默认值也应写出,以保持结构稳定。

参数说明表:
方法 数据类型 对应C++类型 存储大小(近似)
writeReal() / writeDouble() 双精度浮点数 double 8字节
writeInt16() 16位整型 short 2字节
writeInt32() 32位整型 int , long 4字节
writeBoolean() 布尔值 bool 1字节
writeString() 字符串(Unicode) const wchar_t* 变长
writePoint3d() 三维点坐标 AcGePoint3d 24字节

这些类型的编码方式由AutoCAD内部统一管理,开发者无需关心字节序转换问题,但应注意不同类型在不同平台上的对齐差异。

序列化流程图(Mermaid)
graph TD
    A[开始 dwgOutFields] --> B{调用基类 dwgOutFields?}
    B -->|是| C[写入 AcDbEntity 公共字段]
    C --> D[写入 m_height (double)]
    D --> E[写入 m_width (double)]
    E --> F[写入 m_style (int16)]
    F --> G[写入 m_isVisible (boolean)]
    G --> H[写入 m_origin (Point3d)]
    H --> I{状态正常?}
    I -->|是| J[返回 true]
    I -->|否| K[返回 false]

该流程清晰展示了从入口到各个字段写入的完整路径,强调了错误传播机制的重要性。

5.1.2 dwgInFields()反序列化过程中的版本兼容处理

与导出相对应, dwgInFields(AcDbDwgFiler* pFiler) 负责从DWG流中读取并恢复对象状态。由于软件可能升级导致类结构变化(如新增字段、删除旧字段),因此必须设计具有 前向与后向兼容性 的读取逻辑。

Adesk::Boolean MyCustomEntity::dwgInFields(AcDbDwgFiler* pFiler)
{
    assert(pFiler);

    // 恢复基类字段
    if (AcDbEntity::dwgInFields(pFiler) != Acad::eOk)
        return Adesk::kFalse;

    // 读取原始字段(v1.0 版本)
    double height, width;
    short style;
    bool visible;
    AcGePoint3d origin;

    pFiler->readDouble(height);
    pFiler->readDouble(width);
    pFiler->readInt16(style);
    pFiler->readBoolean(visible);
    pFiler->readPoint3d(origin);

    // 赋值给成员变量
    m_height = height;
    m_width = width;
    m_style = static_cast<MyStyleEnum>(style);
    m_isVisible = visible;
    m_origin = origin;

    // 处理可选字段(v2.0 新增)
    if (pFiler->atEnd()) {
        m_opacity = 1.0;  // 默认值
    } else {
        double opacity;
        pFiler->readDouble(opacity);
        m_opacity = opacity;
    }

    return pFiler->filerStatus() == Acad::eOk ? Adesk::kTrue : Adesk::kFalse;
}
代码逻辑逐行解读:
  • 第4~7行 :同 dwgOutFields 一样,先恢复基类状态。
  • 第11~19行 :声明临时变量接收原始字段数据,然后赋值给实例成员。
  • 第22~27行 :关键兼容性处理段落 —— 使用 atEnd() 判断是否已到达流末尾。若为真,说明原文件来自旧版本(无 m_opacity 字段),则赋予默认值;否则继续读取新字段。

这种方式实现了 渐进式升级 ,即新版程序可以打开旧版文件而不报错,同时保留未来扩展空间。

版本兼容策略对比表:
策略 描述 适用场景
固定字段集 所有版本写入相同数量字段 结构极稳定的小型插件
条件读取 + atEnd() 根据流长度动态决定读多少 中小型项目推荐方案
使用 AcDbFilerGroups 分组标记字段块,支持跳跃读取 大型复杂对象,需高灵活性
自定义头信息记录版本号 在开头写入版本标识整数 强类型变更频繁的系统

对于大多数自定义实体,建议采用“条件读取 + atEnd()”模式,在简洁性与健壮性之间取得平衡。

错误恢复示意图(Mermaid)
graph LR
    A[调用 dwgInFields] --> B{能否读取基本字段?}
    B -->|失败| C[返回 false, 对象创建失败]
    B -->|成功| D{是否 atEnd()?}
    D -->|是| E[使用默认值填充新增字段]
    D -->|否| F[继续读取扩展字段]
    F --> G{读取成功?}
    G -->|是| H[完成初始化]
    G -->|否| I[设置默认值并警告]
    H --> J[返回 true]
    E --> J
    I --> J

此图体现了容错设计的思想:即使部分字段缺失或损坏,也不应导致整个对象加载失败,而是尽可能恢复可用状态。

5.2 自定义数据类型的字段编码规范

在实际开发中,常常需要存储结构体、枚举或多维数组等复合类型。直接传递给 AcDbDwgFiler 会导致编译错误或运行时异常,因此必须将其分解为基本类型进行编码。

5.2.1 使用AcDbDwgFiler派生类进行类型安全读写

虽然 AcDbDwgFiler 本身不可继承用于扩展功能,但我们可以通过封装辅助类来提升代码复用性和安全性。例如,定义一个名为 MyDataEncoder 的工具类,专门处理复杂结构的序列化。

class MyRectRegion {
public:
    AcGePoint3d minPt;
    AcGePoint3d maxPt;
    AcCmColor borderColor;
};

// 工具类:负责 MyRectRegion 的读写
class MyDataEncoder {
public:
    static void WriteRectRegion(AcDbDwgFiler* pFiler, const MyRectRegion& rect) {
        pFiler->writePoint3d(rect.minPt);
        pFiler->writePoint3d(rect.maxPt);
        pFiler->writeBytes(3, reinterpret_cast<const Adesk::UInt8*>(&rect.borderColor));
    }

    static void ReadRectRegion(AcDbDwgFiler* pFiler, MyRectRegion& rect) {
        pFiler->readPoint3d(rect.minPt);
        pFiler->readPoint3d(rect.maxPt);
        Adesk::UInt8 colorBytes[3];
        pFiler->readBytes(3, colorBytes);
        rect.borderColor.setRGB(colorBytes[0], colorBytes[1], colorBytes[2]);
    }
};
代码逻辑分析:
  • WriteRectRegion
  • 第7行:分别写出最小点和最大点。
  • 第8行:使用 writeBytes 写出RGB三个字节的颜色值。注意 AcCmColor 不能直接序列化,需拆解。
  • ReadRectRegion
  • 第14~16行:读回两点坐标。
  • 第17~19行:读取3字节颜色数据并重建 AcCmColor 对象。

✅ 最佳实践:将所有自定义结构的编码/解码逻辑集中到独立工具类中,便于维护和单元测试。

扩展思考:为何不用 writeRawBinaryBytes

尽管 writeRawBinaryBytes() 可用于直接写出内存块,但它存在严重风险:
- 平台相关性(小端/大端)
- 编译器填充字节(padding bytes)导致不一致
- 类布局变化后完全失效

因此,仅建议在性能极度敏感且结构绝对固定的场景中谨慎使用。

5.2.2 自定义结构体与枚举类型的打包策略

考虑如下枚举类型:

enum class FillPattern : unsigned char {
    Solid = 0,
    HatchDiag = 1,
    Crosshatch = 2,
    None = 255
};

它应在序列化时转换为 unsigned char 进行存储:

pFiler->writeByte(static_cast<Adesk::UInt8>(m_fillPattern));

而对于包含多个枚举或布尔标志的结构,可采用 位打包(bit packing) 技术减少存储开销:

struct EntityFlags {
    bool locked : 1;
    bool clipped : 1;
    bool mirrored : 1;
    FillPattern pattern : 3;  // 最多8种图案
    unsigned int reserved : 2;
};

此时可通过联合体(union)与位操作实现紧凑存储:

union FlagStorage {
    struct {
        Adesk::UInt8 locked : 1;
        Adesk::UInt8 clipped : 1;
        Adesk::UInt8 mirrored : 1;
        Adesk::UInt8 pattern : 3;
        Adesk::UInt8 reserved : 2;
    } bits;
    Adesk::UInt8 byte;
};

// 写入
FlagStorage fs;
fs.bits.locked = m_flags.locked;
fs.bits.clipped = m_flags.clipped;
fs.bits.mirrored = m_flags.mirrored;
fs.bits.pattern = static_cast<Adesk::UInt8>(m_flags.pattern);
pFiler->writeByte(fs.byte);

// 读取
Adesk::UInt8 raw;
pFiler->readByte(raw);
fs.byte = raw;
m_flags.locked = fs.bits.locked;
m_flags.clipped = fs.bits.clipped;
m_flags.mirrored = fs.bits.mirrored;
m_flags.pattern = static_cast<FillPattern>(fs.bits.pattern);

这种方法特别适用于大量轻量级对象的批量存储优化。

5.3 DXF格式支持与跨平台交换能力

除了二进制DWG格式,DXF(Drawing Exchange Format)作为一种ASCII文本格式,广泛用于与其他CAD系统交换数据。实现良好的DXF支持不仅能增强互操作性,还能方便调试和自动化脚本处理。

5.3.1 dxfOutFields()中组码分配与标准遵循

DXF文件由“组码(Group Code)”+“值”构成,每行代表一个字段。 dxfOutFields() 的作用就是将对象属性映射到标准组码上。

Adesk::Boolean MyCustomEntity::dxfOutFields(AcDbDxfFiler* pFiler) const
{
    assert(pFiler);

    if (!AcDbEntity::dxfOutFields(pFiler))
        return Adesk::kFalse;

    // 输出自定义组码(使用用户保留范围 1000-1071)
    pFiler->wrString(1000, L"MYAPP_CUSTOM_ENTITY"); // 类型标识
    pFiler->wrReal(1040, m_height);                 // 组码1040: real value
    pFiler->wrReal(1041, m_width);
    pFiler->wrInt(1070, static_cast<Adesk::Int16>(m_style)); // 1070用于整数
    pFiler->wrBool(1071, m_isVisible);

    return pFiler->filerStatus() == Acad::eOk;
}
组码使用规范表:
组码范围 含义 是否推荐用于自定义
0–99 实体类型、句柄、图层名等 ❌ 系统保留
100–199 子类标记 ✅ 可用
1000–1009 ASCII字符串 ✅ 推荐用于标识
1010–1059 三维点坐标(X/Y/Z) ✅ 几何数据专用
1060–1071 布尔、整数、实数 ✅ 推荐用于标量

📌 建议:始终使用1000–1071范围内的组码书写自定义数据,避免与未来AutoCAD版本冲突。

5.3.2 dxfInFields()解析外部DXF导入数据的健壮性设计

由于DXF可由第三方生成,内容可能不完整甚至恶意构造,因此 dxfInFields() 必须具备强健的容错能力。

Adesk::Boolean MyCustomEntity::dxfInFields(AcDbDxfFiler* pFiler)
{
    assert(pFiler);

    if (!AcDbEntity::dxfInFields(pFiler))
        return Adesk::kFalse;

    while (!pFiler->atEOF()) {
        switch (pFiler->groupCode()) {
            case 1000: {
                const TCHAR* str = pFiler->restofLine();
                if (_tcscmp(str, _T("MYAPP_CUSTOM_ENTITY")) != 0) {
                    pFiler->pushBackGroup(); // 不属于本类,退回
                    return Adesk::kTrue;     // 不报错,继续处理其他实体
                }
                break;
            }
            case 1040: pFiler->rdReal(m_height); break;
            case 1041: pFiler->rdReal(m_width); break;
            case 1070: {
                Adesk::Int16 val;
                pFiler->rdInt(val);
                if (val >= 0 && val <= 2) m_style = static_cast<MyStyleEnum>(val);
                else m_style = MyStyleEnum::Solid; // 安全降级
                break;
            }
            case 1071: pFiler->rdBool(m_isVisible); break;
            default:
                pFiler->pushBackGroup(); // 忽略未知组码
                return Adesk::kTrue;
        }
        pFiler->nextRecord();
    }

    return Adesk::kTrue;
}
关键设计点:
  • 第13~20行 :验证实体标识字符串,防止误读。
  • 第34行 :使用 pushBackGroup() 将未识别组码返还流中,供后续处理器使用。
  • 第25~29行 :对枚举输入做边界检查,防止非法值引发崩溃。
  • 第37行 nextRecord() 推进到下一组码,避免死循环。

该实现确保即使面对格式混乱的DXF也能优雅降级而非中断加载。


5.4 持久化过程中的异常处理与日志记录

尽管ObjectARX的 AcDbFiler 体系较为稳健,但在极端情况下仍可能发生I/O错误、内存不足或数据损坏等问题。为此,必须建立完善的异常监控与诊断机制。

5.4.1 文件损坏或格式错误时的降级恢复机制

filerStatus() 返回非 eOk 状态时,表明发生了严重错误。此时不应立即终止,而应尝试恢复至安全状态。

void HandleCorruptedData(MyCustomEntity* pEnt, AcDbDwgFiler* pFiler)
{
    Acad::ErrorStatus es = pFiler->filerStatus();
    acutPrintf(L"\n[WARNING] Data corruption detected in %s: %s",
               pEnt->isA()->className(),
               acadErrorStatusText(es));

    // 重置为默认值
    pEnt->m_height = 1.0;
    pEnt->m_width = 1.0;
    pEnt->m_style = MyStyleEnum::Solid;
    pEnt->m_isVisible = true;
    pEnt->m_origin = AcGePoint3d::kOrigin;

    // 触发事件通知用户
    RaiseDataRecoveryEvent(pEnt, es);
}

此外,可在模块初始化时注册全局钩子,捕获所有序列化异常:

static void OnFilerError(const AcDbObjectId& objId, Acad::ErrorStatus es)
{
    AcDbObjectPointer<MyCustomEntity> ent(objId, AcDb::kForRead);
    if (ent.openStatus() == Acad::eOk) {
        HandleCorruptedData(ent.object(), nullptr);
    }
}

5.4.2 调试模式下序列化数据的输出跟踪

为了便于排查问题,可在调试版本中启用序列化日志功能:

#ifdef _DEBUG
#define LOG_DWG_OP(op, field, value) \
    acutPrintf(L"[DWG %s] %s = %f\n", op, field, value)
#else
#define LOG_DWG_OP(op, field, value)
#endif

// 在 dwgOutFields 中插入:
LOG_DWG_OP("OUT", "height", m_height);
pFiler->writeDouble(m_height);

结合Visual Studio的输出窗口,可实时观察每个字段的写入过程,极大提升调试效率。

日志采样输出示例:
[DWG OUT] height = 5.000000
[DWG OUT] width = 3.000000
[DWG IN] height = 5.000000
[DWG IN] width = 3.000000

配合条件断点,可精准定位字段错位或类型混淆问题。

综上所述,数据持久化不仅是技术实现环节,更是保障用户体验与系统可靠性的关键防线。通过严谨的字段管理、灵活的版本控制、标准化的DXF映射以及健全的异常响应机制,开发者能够构建出既高性能又高可用的自定义实体,真正融入AutoCAD生态体系之中。

6. 模块注册、命令响应与系统集成

6.1 ARX模块加载机制与ArxApp初始化流程

在AutoCAD的ObjectARX开发中,模块的动态加载和初始化是实现自定义功能与宿主环境无缝集成的第一步。当用户通过 NETLOAD APPLOAD 命令加载ARX模块时,AutoCAD调用 acrxEntryPoint 作为入口函数,该函数由开发者实现,负责响应不同生命周期事件。

extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt)
{
    switch (msg) {
    case AcRx::kInitAppMsg:
        acrxDynamicLinker->registerAppData(
            acrxAppName, 
            new AcRxAppData(ArxApp::getInstance()));
        acedRegSvc(new ArxApp());
        break;

    case AcRx::kUnloadAppMsg:
        delete ArxApp::getInstance();
        break;

    default:
        break;
    }
    return AcRx::kRetOK;
}
  • acrxEntryPoint :核心入口点,接收系统消息(如加载、卸载、开始/结束文档等)。
  • acrxDynamicLinker :用于注册应用程序数据和服务,确保模块在整个AutoCAD会话期间保持状态一致。
  • ArxApp类 :通常实现为单例模式,封装命令注册、资源管理与服务监听逻辑。

此外,在初始化阶段必须完成自定义实体类的注册,使用 AcDb::addClass() 将派生类注入到运行时类系统中:

AcDb::addClass(ArxCustomEntity::desc());

此调用需在 kInitAppMsg 阶段执行,并保证调用顺序早于任何对象创建操作,否则会导致序列化失败或类型识别异常。

6.2 用户命令接入:acedRegCmds与acedAsdkCommand实现

要使用户能够在命令行触发自定义行为,必须通过 acedRegCmds 接口注册命令。以下示例展示如何绑定一个创建自定义实体的命令:

void cmdCreateCustomEntity()
{
    AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
    AcTransaction* pTran = pDb->transactionManager()->startTransaction();

    try {
        AcDbBlockTableRecordPointer modelSpace(acdbSymUtil()->blockModelSpaceId(pDb), AcDb::kForWrite, pTran);

        ArxCustomEntity* pEnt = new ArxCustomEntity();
        pEnt->setDatabaseDefaults(pDb);
        pEnt->setPosition(AcGePoint3d(0, 0, 0));
        pEnt->setHeight(100.0);
        pEnt->setWidth(200.0);

        AcDbObjectId objId;
        modelSpace->appendAcDbEntity(objId, pEnt);
        pTran->addNewlyCreatedDBRObject(pEnt);

        pTran->commit();
        acutPrintf(L"\n成功创建自定义实体,ID: %ls", objId.hexString().constPtr());
    }
    catch (...) {
        pTran->abort();
        acutPrintf(L"\n创建实体失败!");
    }
}

注册该命令的方法如下:

acedRegCmds->addCommand(
    L"MyAppGroup",           // 命令组名
    L"CCE",                  // 命令名称
    L"CCE",                  // 命令别名
    ACRX_CMD_MODAL,         // 模态执行
    &cmdCreateCustomEntity   // 回调函数指针
);
参数 类型 说明
groupName const TCHAR* 命令所属的应用程序组
globalName const TCHAR* 全局可调用命令名
localName const TCHAR* 本地化别名(支持多语言)
cmdFlags int 执行模式(模态、透明、同步等)
funcAddr AcEdCommandStack::Callback 函数指针

支持的输入交互包括:
- acedGetPoint() 获取用户点击坐标
- acedGetInt() / acedGetReal() 输入整数或浮点值
- acedPrompt() 输出提示信息

6.3 ObjectARX数据库服务与对象生命周期管理

所有图形对象都存储在 AcDbDatabase 实例中,访问它需要线程安全地获取当前工作数据库:

AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();

事务机制是操作数据库的核心:

AcTransaction* pTran = pDb->transactionManager()->startTransaction();
AcDbObject* pObj = nullptr;

try {
    acdbOpenObject(pObj, objId, AcDb::kForRead);
    // 或使用智能指针简化管理
    AcDbObjectPointer<ArxCustomEntity> entPtr(objId, AcDb::kForWrite);

    if (entPtr.openStatus() == Acad::eOk) {
        entPtr->setWidth(300.0);
    }

    pTran->commit(); // 自动释放所有打开的对象
}
catch (...) {
    pTran->abort(); // 回滚并释放资源
}

AcDbObjectPointer<T> 是模板化智能指针,遵循RAII原则,避免手动调用 close() 导致的内存泄漏风险。

操作 推荐方式 注意事项
打开对象 使用 AcDbObjectPointer 自动管理open/close
添加新对象 appendAcDbEntity() + addNewlyCreatedDBRObject() 必须加入事务跟踪
修改属性 在事务内获取写权限 避免跨事务修改
删除对象 erase(true) 设置 true 表示永久删除

6.4 C++面向对象特性在ARX开发中的深度应用

多态性在渲染中的体现

通过重写 worldDraw() ,不同子类可呈现差异化视觉效果:

class ArxRectEntity : public ArxCustomEntity {
public:
    virtual Adesk::Boolean worldDraw(AcGiWorldDraw* wd) override {
        AcGePoint3d minPt = getPosition();
        AcGePoint3d maxPt = minPt + AcGeVector3d(getWidth(), getHeight(), 0);

        wd->subEntityTraits().setFillColor(AcCmColor::kRed);
        wd->geometry().drawRectangle(minPt, maxPt);
        return true;
    }
};

class ArxCircleEntity : public ArxCustomEntity {
public:
    virtual Adesk::Boolean worldDraw(AcGiWorldDraw* wd) override {
        wd->subEntityTraits().setLineColor(AcCmColor::kGreen);
        wd->geometry().drawCircle(getPosition(), AcGeVector3d::kZAxis, getWidth()/2);
        return true;
    }
};

借助虚函数表, AcDbEntity* 指针可在遍历模型空间时自动调用对应绘制逻辑。

继承提升可维护性的实践案例

设计基类 ArxBaseBuildingElement 封装通用属性(高度、材质、图层),派生出墙、门、窗等具体类型,共享夹点逻辑与持久化代码:

class ArxBaseBuildingElement : public AcDbEntity {
protected:
    double m_height;
    CString m_material;
    AcDbHardPointerId m_layerId;

public:
    virtual Acad::ErrorStatus dwgOutFields(AcDbDwgFiler* filer) const override;
    virtual Acad::ErrorStatus dwgInFields(AcDbDwgFiler* filer) override;
};
classDiagram
    class AcDbEntity
    class ArxBaseBuildingElement {
        +double m_height
        +CString m_material
        +AcDbHardPointerId m_layerId
        +dwgOutFields()
        +dwgInFields()
    }
    class ArxWallEntity
    class ArxDoorEntity
    class ArxWindowEntity

    AcDbEntity <|-- ArxBaseBuildingElement
    ArxBaseBuildingElement <|-- ArxWallEntity
    ArxBaseBuildingElement <|-- ArxDoorEntity
    ArxBaseBuildingElement <|-- ArxWindowEntity

这种分层结构显著降低了重复代码量,提升了后期扩展能力,符合高内聚低耦合的设计原则。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在CAD开发中,使用ObjectArx结合C++可实现高度定制化的自定义实体。本文以创建一个具备移动、向上拉伸和向右拉伸功能的矩形实体为例,深入讲解如何通过继承AcDbEntity类构建可交互的图形对象。内容涵盖夹点定义与响应、几何变换处理、图形绘制、数据持久化及模块注册加载等关键技术,适用于需要扩展AutoCAD功能的开发者。项目经过测试验证,可作为ObjectArx二次开发的典型范例,帮助开发者掌握自定义实体的完整实现流程。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值