☰
ObjectARX自定义实体实战:从显示、夹点到序列化全解析
2026/10/19 4:12:53 网站建设 项目流程

简介:面向ObjectARX二次开发者的自定义实体入门实例,定位于C++/VC++环境下从事AutoCAD插件开发的工程师,适合希望创建新型几何对象并理解ARX底层机制的学习者。资源以简单直线实体为范例,系统演示从新建ARX工程、派生AcDbLine子类、重写DWG/DXF序列化与worldDraw绘制,到注册应用、定义命令、编译加载和调试排错的全流程,让读者掌握自定义实体的完整实现路径。压缩包为RAR格式,共70个文件,大小约5.08MB,包含h/cpp完整源码、rc资源、dsp/dsw工程配置,以及编译生成的ARX与DBX模块、pdb调试文件等,两个子工程TestCmd与TestEntity结构清晰,便于按模块对照学习。目前已有1162人学习下载。通过研读这套实例,能够掌握自定义实体的类设计、图形数据库读写、DXF输入输出、实体注册与命令封装等关键技术,并能借鉴其工程组织方式,为后续开发更复杂的CAD扩展功能提供直接参考。

1. ObjectARX 自定义实体,不只是“画个矩形”

刚开始拆这份 ObjectARX 自定义实体代码时,我原以为它就是给某 CAD 平台画一个矩形那么简单。真正跑完一遍才发现,难的部分不是“画”,而是让这个矩形在保存、重新打开、夹点拖拽、炸开、写块这些操作里,表现得和原生实体一模一样。这个所谓“简单实例”,其实把自定义实体的类声明、worldDraw 显示、夹点响应、DWG/DXF 读写整个链条都串了一遍。如果你正在做 CAD 二次开发,被块参照和扩展数据的各种限制弄得头疼,想用 C++ 做一个真正“有逻辑”的图元,这份资源能在半天内给你铺好第一条路。新手先照代码跑通,老手直接看参数和坑。

2. 自定义实体机制:为什么需要它,以及最小骨架

2.1 图块、多段线不够用:你什么时候需要自定义实体

先说结论:80% 的图元用块 + 扩展数据就能应付,剩下的 20%,大概长这样。

你要画一个设备图例,矩形边长由两个参数控制,中间要根据房间面积自动调整对角线角度,双击之后还要弹出属性框。用块做,要么靠属性块 + 夹点慢慢凑,要么在 XData 里塞一堆自定义数据,然后每次修改都要用反应器去同步图形。这套方案初期能跑,等图例数量上千、文档来回保存另存为之后,就会开始出现数据同步漏掉、夹点行为诡异、炸开就全崩的玄学问题。

自定义实体解决的是这 20%。它的核心思路很简单:你不是放一堆图元进文档,而是把一个 C++ 类实例放进文档,这个类自己回答“我长什么样、夹点在哪、数据怎么存”。用 ObjectARX 写自定义实体,就是把曾经放在外部字典和反应器里的逻辑,集中到这个类内部。从某 CAD 平台的角度看,它和一条 LINE、一个 CIRCLE 没有本质区别,唯一的区别是它的显示代码由你提供。

2.2 自定义实体的最小骨架:必须重写的几个方法

一个能稳定驻留在图纸里的自定义实体,至少要有三组能力:显示、编辑、持久化。

显示靠 worldDraw,编辑靠 getGripPoints 和 moveGripPointsAt,持久化靠 dwgOutFields、dxfOutFields 以及对应的 In 方法。这套实例里的类声明如下:

class MyBoxEntity : public AcDbEntity { public: ACRX_DECLARE_MEMBERS(MyBoxEntity); MyBoxEntity(); virtual ~MyBoxEntity(); void setGeometry(double bx, double by, double w, double h, int idx); // 第一组:显示 Adesk::Boolean worldDraw(AcGiWorldDraw* mode) override; // 第二组:夹点编辑 Acad::ErrorStatus getGripPoints(AcGePoint3dArray& gripPoints, AcDbIntArray& osnapModes, AcDbIntArray& geomIds) const override; Acad::ErrorStatus moveGripPointsAt(const AcDbIntArray& indices, const AcGeVector3d& offset) override; // 第三组:序列化 Acad::ErrorStatus dwgInFields(AcDbDwgFiler* pFiler) override; Acad::ErrorStatus dwgOutFields(AcDbDwgFiler* pFiler) const override; Acad::ErrorStatus dxfInFields(AcDbDxfFiler* pFiler) override; Acad::ErrorStatus dxfOutFields(AcDbDxfFiler* pFiler) const override; private: double m_baseX, m_baseY; // 左下角坐标 double m_width, m_height; // 矩形尺寸 int m_index; // 设备编号 };

方法对应的职责见下表:

方法家族职责这个实例里做什么
worldDraw控制实体在视口里的显示画矩形、对角线和编号文字
getGripPoints给出可被鼠标选中的夹点返回矩形的四个角点
moveGripPointsAt用户拖夹点时更新数据按拖拽方向修改坐标和尺寸
dwgOutFields / dwgInFieldsDWG 保存与读取把几何参数和编号写入文件
dxfOutFields / dxfInFieldsDXF 保存与读取以组码形式读写参数

2.3 从 worldDraw 到 dwgOutFields:一个实体的一生

你看到的“实体能被选中、能保存、能拖拽”,其实是两个生命周期在交替。

第一个生命周期是显示周期。每次某 CAD 平台重绘视图,它就调一次 worldDraw。你在这个方法里画什么,用户屏幕上看就是什么。第二个生命周期是文档周期。打开图和保存图的时候,平台会把实体当成一组字节流写进文件,这个写入由 dwgOutFields 完成,读取由 dwgInFields 完成。很多时候初学者觉得“自家实体像是黑匣子”,就是因为只写了 worldDraw,没写 dwgInFields。内存里跑得挺好,一保存一打开,图形要么变成代理实体,要么干脆只剩一个夹点。

有两点选型建议给你。第一,能覆盖 subWorldDraw 的地方尽量用 subWorldDraw,它比 worldDraw 少踩一些派生类扩展时的坑,这个实例为了直观用了 worldDraw,你自己扩写时注意。第二,所有对外修改数据的入口,都要走事务或 assertWriteEnabled 保护,否则在高版本某 CAD 平台里很容易触发只读崩溃。

3. 开发环境与工程骨架:版本匹配、入口函数、DEF 导出

3.1 版本对应关系:VS 和 SDK 必须对齐

ObjectARX 最折磨人的不是 C++ 语法,而是版本匹配。SDK 版本、VS 版本、某 CAD 平台版本,三者只要错一个,编译出来的 arx 就加载不进去,而且报错信息往往只有一句“无法加载”。

常见的稳定组合见下表。注意,某个 CAD 版本具体对应哪个 VS,以你安装的 SDK 包内 Readme 为准,不同大版本偶尔有更新。

CAD 版本代际常用开发环境注意事项
CAD 2013 左右VS2010老项目迁移多,注意 wchar_t 设置
CAD 2016 左右VS2012 / VS2013需要关掉 SDL 检查
CAD 2018 左右VS2015 / VS2017工程属性里要手动加 _CRT_SECURE_NO_WARNINGS
CAD 2020 之后VS2019x64 是默认目标,不要再编 win32 版

这份资源的代码是向后兼容的写法,类声明里没有依赖太新的特性,你在上述任一组合里把工程属性配好就能编。网上很多教程还停留在 VC++ 6.0 时代,用MFC的CString和char*,拿到现代 SDK 上第一步就编不过,这份资源直接绕开了那堆老古董。

3.2 从向导创建工程到手动配置

用 ObjectARX Wizard 创建工程最省事,装完 Wizard 后新建项目选 ObjectARX,模版里会自动生成入口函数和 DEF 文件。没装 Wizard 也没关系,手动创建同样很快,核心步骤是三步。

第一步,新建一个空的动态链接库工程,目标平台 x64。第二步,在工程属性里把 SDK 的 include 和 lib 目录指进去。第三步,加一个模块定义文件,把两个入口符号导出。

# 假设 SDK 解压在 D:\objectarx # Include 目录:D:\objectarx\inc # 库目录:D:\objectarx\lib\win64 # 链接库按需加:acdb.lib、acge.lib、agi.lib、rxapi.lib

第三步最容易翻车。arx 和普通 DLL 不一样,它需要明确导出acrxEntryPoint,很多新人在工程里写了入口函数却漏了 DEF 文件,结果一 APPLOAD 就提示“无法找到入口”。模块定义文件内容很简单:

LIBRARY MYBOX EXPORTS acrxEntryPoint acrxGetApiVersion

如果你的 SDK 较新,建议把 acrxGetApiVersion 也导出,某些版本检查 API 版本时缺这个符号会直接拒载。

3.3 入口函数 acrxEntryPoint 与命令注册

入口函数是所有 ARX 程序的中枢,每次加载和卸载都会收到消息。一个最简入口长这样:

extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) { switch (msg) { case AcRx::kInitAppMsg: acrxDynamicLinker->unlockApplication(pkt); acrxRegisterAppMDIAware(pkt); MyBoxEntity::rxInit(); acedRegCmds->addCommand( L"MYBOX_COMMANDS", L"MYBOXCREATE", L"MYBOXCREATE", ACRX_CMD_MODAL, createBoxCmd); break; case AcRx::kUnloadAppMsg: acedRegCmds->removeGroup(L"MYBOX_COMMANDS"); deleteAcRxClass(MyBoxEntity::desc()); break; } return AcRx::kRetOK; }

第 11 行的 addCommand 四个参数分别是命令组名、全局命令名、本地化命令名、命令模式,最后是回调函数地址。命令组名相当于命名空间,卸载时按组移除最干净。第 16 行的 removeGroup 会自动清掉组内所有命令,比一个一个 removeCommand 省心。如果你在 kInitAppMsg 里的初始化顺序不对,比如 rxInit 放在 addCommand 之后,某些版本下加载会成功但实体类不可用,点实体时直接崩溃。这里的顺序是安全顺序。

3.4 调试技巧:让 CAD 直接当作调试器

ARX 调试和普通 DLL 不太一样,最常见的姿势是把 vs 的调试器直接指向某 CAD 平台的 exe。

具体的做法是:在 VS 工程属性里,调试一栏,把“命令”设为某 CAD 平台的 exe 完整路径,把“工作目录”设为它的安装目录。然后按 F5,某 CAD 平台会启动,你在里面 APPLOAD 这个 arx,断点就能命中。需要注意的是,arx 必须编成 Debug 版,并且加载的是 Debug 版程序,Release 版某 CAD 平台也能加载 Debug arx,但断点经常断不住,这是常见坑。加载之后如果崩了,先看调用栈,绝大部分问题不是算法错,而是某个方法没有 override 到。

4. 写一个带夹点和序列化的自定义实体:完整代码拆解

4.1 实体类注册宏:让平台认识你的类

类的成员函数放在第 2 章已经声明好了,但要让某 CAD 平台真正认识这个类,还需要一个注册宏。它负责给类分配一个运行时描述符,确保实体从 DWG 里读回来时,平台能通过类名找到对应的 C++ 类。

ACRX_DXF_DEFINE_MEMBERS( MyBoxEntity, // 类名 AcDbEntity, // 父类 AcDb::kDHL_CURRENT, // DWG 版本 AcDb::kMReleaseCurrent, // 维护版本 AcDb::kTransform_R0, // 变换类型 L"MYBOX_ENTITY" // 实体类名,保存在 DWG 里 );

最后那个 L"MYBOX_ENTITY" 是给平台识别用的字符串,会直接写进文件。这个字符串一旦发布就不要改,否则老图打开后平台在类字典里找不到对应类,实体直接变代理。

4.2 worldDraw:显示逻辑拆解

worldDraw 负责画图。每帧重绘都会调用,所以不要在里头做复杂计算,也不要做内存分配。本实例只画三样东西:矩形外框、对角线、编号文字。

Adesk::Boolean MyBoxEntity::worldDraw(AcGiWorldDraw* mode) { assertReadEnabled(); // 1. 画矩形外框,终点回到起点,形成闭合 AcGePoint3d pts[5]; pts[0].set(m_baseX, m_baseY, 0.0); pts[1].set(m_baseX + m_width, m_baseY, 0.0); pts[2].set(m_baseX + m_width, m_baseY + m_height, 0.0); pts[3].set(m_baseX, m_baseY + m_height, 0.0); pts[4].set(m_baseX, m_baseY, 0.0); mode->geometry().polyline(5, pts); // 2. 画右下到左上的对角线 AcGePoint3d diagPts[2]; diagPts[0].set(m_baseX, m_baseY, 0.0); diagPts[1].set(m_baseX + m_width, m_baseY + m_height, 0.0); mode->geometry().polyline(2, diagPts); // 3. 在中心画一个编号,用 ASCII 格式避免字体乱码 wchar_t buf[32]; swprintf_s(buf, 32, L"BOX-%d", m_index); AcGiTextStyle style; AcGePoint3d textPos(m_baseX + m_width * 0.5, m_baseY + m_height * 0.5, 0.0); mode->geometry().text(style, textPos, buf); return Adesk::kTrue; }

第 14 行polyline(5, pts)画的是五段闭合线,第五个点和第一个点重合。如果用 4 段,边框在视觉上没问题,但选中高亮时偶尔会漏一条边。第 27 行用 swprintf_s 生成编号文本,这里特意没拼中文,是为了让不配置字体的环境下也能正常显示。如果你要显示中文,必须在 style 上显式设置支持中文的字体,否则出来的就是问号或方块。

4.3 夹点逻辑:getGripPoints 与 moveGripPointsAt

夹点之所以重要,是因为它是用户跟自定义实体交互的主要入口。本实例把四个角点暴露给用户,拖哪个角,哪条边跟着动。

Acad::ErrorStatus MyBoxEntity::getGripPoints( AcGePoint3dArray& gripPoints, AcDbIntArray& osnapModes, AcDbIntArray& geomIds) const { assertReadEnabled(); gripPoints.setLogicalLength(0); gripPoints.append(AcGePoint3d(m_baseX, m_baseY, 0.0)); // 0 gripPoints.append(AcGePoint3d(m_baseX + m_width, m_baseY, 0.0)); // 1 gripPoints.append(AcGePoint3d(m_baseX, m_baseY + m_height, 0.0)); // 2 gripPoints.append(AcGePoint3d(m_baseX + m_width, m_baseY + m_height, 0.0)); // 3 return Acad::eOk; }

夹点顺序是有讲究的。我固定成左下、右下、左上、右上,后面 moveGripPointsAt 的判断完全依赖这个索引顺序。如果你返回的点和移动逻辑里的索引对不上,就会出现“夹点飘在图形外”或者“拖一下图形斜着飞出去”的问题。

拖拽逻辑如下:

Acad::ErrorStatus MyBoxEntity::moveGripPointsAt( const AcDbIntArray& indices, const AcGeVector3d& offset) { assertWriteEnabled(); for (int i = 0; i < indices.length(); ++i) { switch (indices[i]) { case 0: // 左下角 m_baseX += offset.x; m_width -= offset.x; m_baseY += offset.y; m_height -= offset.y; break; case 1: // 右下角 m_width += offset.x; m_baseY += offset.y; m_height -= offset.y; break; case 2: // 左上角 m_baseX += offset.x; m_width -= offset.x; m_height += offset.y; break; case 3: // 右上角 m_width += offset.x; m_height += offset.y; break; } } if (m_width < 1.0) m_width = 1.0; if (m_height < 1.0) m_height = 1.0; return Acad::eOk; }

offset 是拖拽的全局位移向量。以 case 3 为例,用户拖右上角向右偏移 50 个图纸单位,m_width 加 50,左上角位置不动,视觉上就是矩形被拉大了。case 2 拖左上角时,m_baseX 和 m_baseY 会跟着偏移,同时把宽高反向减掉,实现“固定右下角”的效果。这段逻辑如果只对某个角做特例会省很多代码,但实际使用中四个角都会被拖,这种对称写法最抗折腾。

4.4 序列化:DWG 和 DXF 读写

序列化是自定义实体能不能存得住的关键。先看 DWG 侧,写入和读取顺序必须严格一致。

Acad::ErrorStatus MyBoxEntity::dwgOutFields(AcDbDwgFiler* pFiler) const { assertReadEnabled(); AcDbEntity::dwgOutFields(pFiler); pFiler->writeInt16(1); // 版本号,后续扩展格式用 pFiler->writeDouble(m_baseX); pFiler->writeDouble(m_baseY); pFiler->writeDouble(m_width); pFiler->writeDouble(m_height); pFiler->writeInt32(m_index); return pFiler->filerStatus(); } Acad::ErrorStatus MyBoxEntity::dwgInFields(AcDbDwgFiler* pFiler) { assertWriteEnabled(); AcDbEntity::dwgInFields(pFiler); Adesk::Int16 version = 0; pFiler->readInt16(&version); if (version >= 1) { pFiler->readDouble(&m_baseX); pFiler->readDouble(&m_baseY); pFiler->readDouble(&m_width); pFiler->readDouble(&m_height); Adesk::Int32 index = 0; pFiler->readInt32(&index); m_index = index; } return pFiler->filerStatus(); }

第一次写版本号是这套写法里最重要的一步。你在 dwgOutFields 里只写 m_width 不写版本号,读回来也能跑,但后续加字段就会遇到老文件兼容问题。现在写死 1,以后加字段升到 2,老图进来自动走旧逻辑。

DXF 侧同样要覆盖,组码选择避开基类常用组码。这里用 70 存版本号,40 到 43 存坐标尺寸,90 存编号,这些组码在 AcDbEntity 基类里不会冲突。

Acad::ErrorStatus MyBoxEntity::dxfOutFields(AcDbDxfFiler* pFiler) const { assertReadEnabled(); AcDbEntity::dxfOutFields(pFiler); pFiler->wrInt16(70, 1); // 版本号 pFiler->wrDouble(40, m_baseX); pFiler->wrDouble(41, m_baseY); pFiler->wrDouble(42, m_width); pFiler->wrDouble(43, m_height); pFiler->wrInt32(90, m_index); return pFiler->filerStatus(); }

如果你只写 DWG 不写 DXF,在导出 DXF 或在低版本转存场景里,实体会丢失自定义属性。很多“保存为 DXF 再打开就变块”的报告,根因都是漏写了 dxfOutFields。

4.5 加载测试:从命令到夹点

测试入口我放在一个命令行函数里,负责创建实体并加入模型空间。

static void createBoxCmd(void* /*pData*/) { AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); MyBoxEntity* pEnt = new MyBoxEntity(); pEnt->setGeometry(100.0, 100.0, 800.0, 400.0, 1); AcDbBlockTableRecord* pRec = nullptr; Acad::ErrorStatus es = acdbOpenObject(pRec, pDb->currentSpaceId(), AcDb::kForWrite); if (es == Acad::eOk) { AcDbObjectId eId; es = pRec->appendAcDbEntity(eId, pEnt); pRec->close(); if (es == Acad::eOk) pEnt->close(); } }

setGeometry 的五个参数依次是左下角 X、左下角 Y、宽度、高度、编号。生产里我会把第二个参数做个命令行交互,让用户点两个点决定宽高,这个实例先写死。加载测试按这个顺序走:编译 → APPLOAD 选中 arx → 输入 MYBOXCREATE → 选中实体看夹点 → 拖拽 → 保存 → 重开图纸确认实体还是自定义实体。

5. 避坑排查:加载失败、乱码、夹点脱节的几条血泪记录

5.1 加载成功,但命令找不到

现象:APPLOAD 提示加载成功,输入 MYBOXCREATE 却提示未知命令。

原因:入口函数里没有在 kInitAppMsg 阶段注册命令,或者命令组因为重复加载被某 CAD 平台静默跳过。还有一个常见根因是 acedRegCmds 在当前文档环境下不可用,早期版本常踩。

解决:确认 addCommand 写在 kInitAppMsg 分支里,而不是写在某个按钮回调里。命令回调函数必须是文件级静态函数,不能是成员函数。卸载时一定要 removeGroup,组名和注册时保持一致。我一般还会加一句判断:if (acedRegCmds)再注册,避免某些特殊启动模式下指针为空。

5.2 自定义实体里的中文变成问号或方块

现象:实体图形正常,但文字部分全部显示为 ?。

原因:worldDraw 里创建 AcGiTextStyle 时没有指定支持中文字体的字体文件,默认字体里没有中文字形。这个坑在你第一次用自定义实体做中文标注时百分百会碰到。

解决:给 AcGiTextStyle 显式设置一个支持中文的字体,然后传给 text 方法。如果你不想在 C++ 里写死字体,也可以读取当前图形文字样式表里的中文字体,把样式 id 传进来。这个实例的编号故意用 ASCII 编码,就是为了让你在不配字体的环境下先把流程跑通,等你的业务确定要用中文,再按这个思路补字体。

5.3 夹点拖到一半,图形不跟随

现象:点击实体能看到夹点,拖拽时夹点跟着鼠标走,但矩形本身尺寸不变。

原因:getGripPoints 返回的坐标和 moveGripPointsAt 修改的属性没对上。最常见的情况是,你返回的夹点是用 m_width 临时算出来的,但 move 里改的是另一个字段,比如只改了 m_width 没改 m_baseX,拖左边夹点时视觉上右边在动,左边一点反应都没有。

解决:把夹点数组的索引当作契约来对待。每个索引对应的坐标,和 moveGripPointsAt 里每个 case 分支修改的变量,必须一一对应。改动字段后不要手动去做更新范围的调用,某 CAD 平台会把拖拽过程交给自己的事务管理,你多余的刷新反而可能造成闪烁。

5.4 保存再打开,实体变成代理图形

现象:新画的实体在内存里一切正常,关闭图纸再打开,图形变成一个带边框的代理对象,或者干脆只剩一个空夹点。

原因:dwgInFields 和 dwgOutFields 没写,或者只写了其中一个。某个 CAD 平台打开 DWG 时会反序列化实体,如果类里没有实现读取逻辑,平台只能把实体标记为代理。更隐蔽的是,读写字段顺序不一致,读到的全错位,实体虽然没有变成代理,但几何尺寸完全乱掉。

解决:两个方法必须成对实现,字段顺序保持一致。写完用最笨的办法验证:画一个实体,保存,关闭,重新打开,选中它,在命令行里查属性。你会发现,读写顺序错了的情况,图形没崩但尺寸突变,这种比崩溃还难排查。

5.5 改实体类名后,老图里的实体认不出来了

现象:开发中觉得类名不贴切,把 MyBoxEntity 改成 FanBoxEntity,重新编译加载后,旧图里的实体全部变代理,有的版本连代理边界都不显示。

原因:实体类名 L"MYBOX_ENTITY" 会写进 DWG,某 CAD 平台重新打开时,按这个字符串去类字典里找对应的类。你改了类名,等于告诉平台这个类不存在。

解决:一旦发布到测试环境,实体类名就不要改。如果确实要重构,最稳妥的办法是保留旧类名,新类继承旧类,在新类里做逻辑升级。这样老图打开时平台找到的还是旧类,图形不会丢。

6. 给实体加版本号:存得下、读得回、升级不崩

到这里,你的自定义实体已经能画、能拖、能存了。但开发还没结束,因为程序一定会有下一次改动。这第 6 章我专门说一个习惯:在 dwgOutFields 的第一个字段写版本号。

这个版本号不是类名,而是你自己定义的数据格式版本。比如第一版只存了宽高和编号。过两个月你发现图例还要存一个旋转角度,于是加了一个 double m_rotation。如果你直接往末尾写,老文件读取时就会读到错误的字节流,轻则角度变成随机值,重则整个实体读不出来。

我的习惯是从第一版就埋好版本位。写的时候先写 1,读的时候先读版本再决定读哪些字段。以后加字段把写入版本改成 2,读入时给一个分支:

Acad::ErrorStatus MyBoxEntity::dwgInFields(AcDbDwgFiler* pFiler) { assertWriteEnabled(); AcDbEntity::dwgInFields(pFiler); Adesk::Int16 version = 0; pFiler->readInt16(&version); if (version >= 1) { pFiler->readDouble(&m_baseX); pFiler->readDouble(&m_baseY); pFiler->readDouble(&m_width); pFiler->readDouble(&m_height); Adesk::Int32 index = 0; pFiler->readInt32(&index); m_index = index; } if (version >= 2) // 第二版增加旋转角 { pFiler->readDouble(&m_rotation); } else { m_rotation = 0.0; // 老文件给默认值 } return pFiler->filerStatus(); }

逻辑很简单:读取永远向前兼容。版本低的文件,把新字段设成默认值;版本高的文件,拒绝加载并提示升级程序。这套思路也适用于 DXF,70 组码里存版本号,读的时候先取版本,再按版本分支解析后面的组码。

验证方法也固定下来:资源包里有一套编译好的实体,我会拿它做“老图升级回归测试”。先在第一版程序里画一批实体,保存成测试图。然后切到第二版程序,打开这些老图,逐一拖拽和重新保存,确认没有出现代理实体和数据跳动,再往新功能上放。

从那以后,我每次给自定义实体加字段都强制自己先走一遍这个流程,代码写完还要拿老图纸实测一次。后来团队里有人问为什么他的实体一升级就崩,我先问一句“你第一版写版本号了吗”,十次里有九次答案都是没写。这个习惯帮你省掉大量以后返工的麻烦,值得在第一个实体刚跑通的时候就开始用。资源包里的工程和示例代码可以按这个顺序直接编译跑通,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询