ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ObjectARX视图操作实战:参数读取、缩放、截屏与DWG导出

ObjectARX视图操作实战:参数读取、缩放、截屏与DWG导出 简介在AutoCAD二次开发中视图管理是常见需求。这套资源面向CAD二次开发者提供一套基于ObjectARX的视图功能示例工程覆盖视图窗口缩放、窗口信息获取、当前视图截屏以及选中实体保存为dwg文件等典型场景适合具备C和ObjectARX基础、正在开发视图辅助工具的工程师阅读。压缩包共35个文件以13个cpp源码、12个头文件为主体配合props工程配置、sln解决方案及rc资源文件构成可编译的VS工程整体仅50KB结构紧凑便于分析。目前已有432人学习浏览。工程中封装了ViewUtil、DwgDatabaseUtil等工具类演示通过ZOOM命令族实现窗口缩放、范围缩放与比例缩放同时给出获取视图窗口信息、截屏保存图片、将选中实体导出为独立dwg文件的具体实现相关几何计算、实体选择与目录选择逻辑均可直接复用。1. objectARX 视图窗口操作不是一条 ZOOM 命令的事objectARX 里处理视图窗口最常见的误会是把它当成一条 ZOOM 命令发个 acedZoomExtents 就完事。实际做一轮 CAD 二次开发就会发现缩放只是显示层的一次状态刷新真正要拿住的是视图参数本身——中心点、高度、宽度、相机目标与方向以及这些参数在命名视图AcDbViewTableRecord和当前视口AcDbViewport里的两份落点。下面把获取视图窗口信息、按参数缩放视图、把当前视口截成 PNG、再把命名视图连同图形另存为独立 dwg 文件这四件事串成一条可复现的路径代码基于 ObjectARX 的 AcDb 数据库接口与 ADSRX 显示接口。适合要做图纸预览、批量出图前处理、视图快照归档的 CAD 二次开发工程师新手可以直接照着编译老手重点看坐标换算和导出边界这两个坑。2. 获取视图窗口信息AcDbViewTable 与 AcDbViewport 两份数据的读法2.1 视图参数存在哪里命名视图在 ViewTable当前屏幕在 Viewport“视图窗口”在 objectARX 里对应两个不同的对象。命名视图是用户在 VIEW 命令里保存下来的那套相机参数存放在当前数据库的视图表 AcDbViewTable 中每条记录是一个 AcDbViewTableRecord而屏幕上正在显示的窗口对应 AcDbViewport 对象模型空间下通过 acdbCurViewportObjectId() 取到。两者字段高度重合都包含 center、height、width、viewTarget、viewDirection、viewTwist但坐标含义有差别ViewTableRecord 的 center 存在 DCS显示坐标系viewTarget 与 viewDirection 存在 WCS。很多人在这里把 AcGePoint2d 与 AcGePoint3d 混用导致后面缩放和导出全部偏移。先明确需求只做缩放和截屏读当前 AcDbViewport 的状态就够要做批量出图或视图归档必须遍历 AcDbViewTable 把命名视图参数完整提取出来。下面两条读取路径都给出可直接编译的代码。2.2 遍历命名视图并打印视图参数的代码// 命令入口遍历当前库的所有命名视图逐条打印视图参数 static void listAllViews(void) { AcDbDatabase* pDb acdbHostApplicationServices()-workingDatabase(); AcDbViewTable* pViewTable nullptr; if (pDb-getViewTable(pViewTable, AcDb::kForRead) ! Acad::eOk) { acutPrintf(_T(\n无法打开视图表)); return; } AcDbViewTableIterator* pIter nullptr; pViewTable-newIterator(pIter); // 视图表迭代器 int n 0; for (; !pIter-done(); pIter-step()) { AcDbViewTableRecord* pRec nullptr; if (pIter-getRecord(pRec, AcDb::kForRead) ! Acad::eOk) continue; acutPrintf(_T(\n[%d] %s), n, pRec-name()); acutPrintf(_T(\n center (%f, %f)), pRec-center().x, pRec-center().y); acutPrintf(_T(\n height %f, width %f), pRec-height(), pRec-width()); acutPrintf(_T(\n target (%f, %f, %f)), pRec-viewTarget().x, pRec-viewTarget().y, pRec-viewTarget().z); acutPrintf(_T(\n direction (%f, %f, %f)), pRec-viewDirection().x, pRec-viewDirection().y, pRec-viewDirection().z); acutPrintf(_T(\n twist %f), pRec-viewTwist()); pRec-close(); } delete pIter; // 迭代器用 delete不用 close pViewTable-close(); }这段代码的关键点是 getViewTable 按读模式打开视图表迭代器的 getRecord 返回的是已打开的 AcDbViewTableRecord用完后要 close迭代器本身用 delete 释放。acutPrintf 打印的是视图参数的原始值建议先跑一遍确认库里视图名称与数值范围。若只判断某个视图是否存在直接用 pViewTable-has(_T(视图名))不用开迭代器。字段含义对照表如下后续缩放和导出全部复用这套字段。字段类型含义坐标基准centerAcGePoint2d视图中心点DCSheightdouble视图高度图形单位无widthdouble视图宽度由 height 与宽高比推viewTargetAcGePoint3d相机目标点WCSviewDirectionAcGeVector3d视线方向单位向量WCSviewTwistdouble视图扭转角弧度无命名视图的宽高比在保存时已经固定所以 width 多数情况下是只读参考值真正参与缩放计算的是 height。这与当前视口不同当前视口的宽度会随窗口拉伸实时变化读取方式见下一节。2.3 当前视口读取center、height 与 aspectRatio 的关系// 打印当前活动视口的视图状态 static void printCurrentViewport(void) { AcDbObjectId vpId acdbCurViewportObjectId(); // 当前活动视口 AcDbViewport* pVp nullptr; if (acdbOpenObject(pVp, vpId, AcDb::kForRead) ! Acad::eOk) return; AcGePoint2d c pVp-viewCenter(); // 视口中心DCS double h pVp-viewHeight(); // 当前视图高度 double w h * pVp-aspectRatio(); // 宽度由高与宽高比推算 acutPrintf(_T(\nvp center(%f,%f), height%f, width%f), c.x, c.y, h, w); pVp-close(); }acdbCurViewportObjectId() 在模型空间返回当前平铺视口在图纸空间返回图纸空间视口调用时必须在文档环境内否则返回空 idacdbOpenObject 随之失败。AcDbViewport 不直接暴露视图宽度用 viewHeight() 乘 aspectRatio() 推算与 ViewTableRecord 里的 width 语义同源。缩放时改的是 viewCenter 和 viewHeight宽度的同步由 AutoCAD 内部完成不需要手工写回。2.4 读不对先查两个前提DCS 坐标系与 objectARX 编译组合读取结果不对先查两个前提。第一是坐标系从命名视图读到的 center 是 DCS 坐标直接拿去做 WCS 几何判断会偏转换用 AcDbDatabase 的 dcs2wcs/ucs2wcs 一族函数并先确认当前 UCS 状态。第二是编译环境objectARX SDK 与 Visual Studio 版本绑定常见组合是 vs2022 编译 vs2017 的 objectarx SDK——代码能编过但链接到 AutoCAD 时符号版本对不上就会报 LNK2019。正确做法是让主版本匹配AutoCAD 2018 到 2020 用 VS2017 的 v141 平台工具集AutoCAD 2024 用 VS2022 17.5 之后的 v143。调试期把 Debug 与 Release 输出目录分开运行库统一 /MD避免 CRT 混用导致视口句柄异常。注意AcDbViewport 打开失败时先看 vpId.asOldId() 是否为 0为 0 说明 acdbCurViewportObjectId() 没拿到有效视口后续缩放与截屏都会跟着失效。3. 视图窗口缩放acedZoomXXX 与 Viewport 写权限两条路3.1 三种缩放 API 的选型依据缩放面前有三条路ADSRX 的 acedZoomXXX 系列、acedCommandS 发 ZOOM 命令、直接写 AcDbViewport 对象的视图字段。程序化操作优先用 acedZoomXXX它内部封装的就是 ZOOM 命令参数少、行为可预期需要按名称还原命名视图时走 ViewTableRecord 加 Viewport 写操作因为 acedZoom 系列没有“按视图名缩放”的入口acedCommandS 只用在缩放之外还要联动其他命令的场景性能最差。方案典型函数适用场景注意点窗口缩放acedZoomWindow(p1, p2)框选放大到两角点范围坐标是 WCS 的 AcGePoint2d按比例缩放acedZoomScale(2.0, 0)固定倍数放大缩小第二参数 0 相对当前视图按中心高度acedZoomCenter(c, h)回到已知范围高度用图形单位恢复命名视图Viewport 写字段切换预设视图需写权限改完刷新3.2 窗口缩放与比例缩放的落地代码// 框选缩放p1、p2 为 WCS 下两个角点 AcGePoint2d p1(0.0, 0.0), p2(1000.0, 800.0); acedZoomWindow(p1, p2); // 相对当前视图放大 2 倍 acedZoomScale(2.0, 0); // 以指定点为中心、高度为 600 缩放 AcGePoint2d center(500.0, 400.0); acedZoomCenter(center, 600.0); // 回到上一视图撤销本次缩放 acedZoomPrevious();acedZoomScale 的第二个参数是比例基准0 表示相对当前视图1 表示相对图纸空间限定范围对应命令行里手动输入的“2x”与“2xp”程序里不要写反。这些函数执行成功后视口会被同步刷新不需要额外 regen如果后续紧跟着截屏建议在缩放后加一次 acedUpdateDisplay() 强制完成显示更新避免截到垂直同步之前的残留帧。3.3 用 AcDbViewTableRecord 把命名视图恢复到当前视口按名称切换视图时标准做法是把视图记录里的字段逐项写回当前 AcDbViewport。注意视口必须用写模式打开写完 close 后再刷新显示static void restoreNamedView(const ACHAR* viewName) { AcDbDatabase* pDb acdbHostApplicationServices()-workingDatabase(); AcDbViewTable* pVT nullptr; if (pDb-getViewTable(pVT, AcDb::kForRead) ! Acad::eOk) return; AcDbViewTableRecord* pRec nullptr; if (pVT-getAt(viewName, pRec, AcDb::kForRead) ! Acad::eOk) { pVT-close(); acutPrintf(_T(\n视图 %s 不存在), viewName); return; } AcDbObjectId vpId acdbCurViewportObjectId(); AcDbViewport* pVp nullptr; if (acdbOpenObject(pVp, vpId, AcDb::kForWrite) Acad::eOk) { pVp-setViewCenter(pRec-center()); // DCS 中心 pVp-setViewHeight(pRec-height()); // 视图高度 pVp-setViewTarget(pRec-viewTarget()); // WCS 目标点 pVp-setViewDirection(pRec-viewDirection()); pVp-setViewTwist(pRec-viewTwist()); pVp-close(); } pRec-close(); pVT-close(); acedUpdateDisplay(); // 请求显示刷新 }setViewCenter 接收 DCS 坐标与 2.2 节读出的 center 字段直接对应不需要再转换。这个方法比 acedCommandS 发 VIEW 命令快且不依赖命令行环境适合在批量后台任务里调用。唯一要防的是视口被锁定或处于图纸空间不可写状态open 写模式失败后加个 es 判断再决定是否回退到 acedZoomCenter(pRec-center(), pRec-height())。3.4 缩放后读回参数确认是否生效缩放不是发完命令就结束紧接着做截屏或导出前把视口参数读回来验证一次。复用 2.3 节的 printCurrentViewport比对缩放前后的 viewHeight 即可判断。常见异常是 acedZoomWindow 两个角点相同或距离极近函数返回 Adesk::kFalse 但屏幕无变化此时应做合法性检查两角点距离小于 1e-6 就放弃缩放并提示用户。提示acedZoomXXX 系列在 64 位 AutoCAD 下仍可用但不要在无界面的服务化启动参数下调用那些环境没有活动视口直接写 Viewport 字段反而更可靠。4. 截屏PrintWindow 抓视口CImage 存高分辨率 PNG4.1 抓窗口与离屏渲染两条路线截屏有两条成熟路线。第一条是 Windows GDI拿主窗口 HWND用 PrintWindow 或 BitBlt 把客户端区域拷进 HDC再存成位图代码量小、所见即所得缺点是截到的就是当前屏幕像素。第二条是 AcGs 离屏渲染用 AcGsManager 创建 AcGsDevice 与 AcGsView把模型空间渲染到内存位图支持任意分辨率和无窗口后台出图但要初始化图形系统、处理视图矩阵代码量明显更大。给当前视图留快照用第一条就够批量图册或高清预览才值得上 AcGs。下面按第一条讲。4.2 PrintWindow 截取客户端区域并保存 PNG#include atlimage.h // CImageSave 时按扩展名选编码器 bool captureViewToPng(const wchar_t* filePath, int scale 1) { HWND hwnd NULL; if (adsw_acadMainWnd(hwnd) ! Adesk::kTrue || hwnd NULL) return false; RECT rc { 0 }; GetClientRect(hwnd, rc); int srcW rc.right - rc.left; int srcH rc.bottom - rc.top; if (srcW 0 || srcH 0) return false; HDC hdcWin GetDC(hwnd); HDC hdcMem CreateCompatibleDC(hdcWin); HBITMAP hbm CreateCompatibleBitmap(hdcWin, srcW * scale, srcH * scale); HGDIOBJ oldBmp SelectObject(hdcMem, hbm); // 先按原尺寸抓图再整体放大避免 PrintWindow 直接拉伸丢失线条细节 HDC hdcTmp CreateCompatibleDC(hdcWin); HBITMAP hbmTmp CreateCompatibleBitmap(hdcWin, srcW, srcH); HGDIOBJ oldTmp SelectObject(hdcTmp, hbmTmp); PrintWindow(hwnd, hdcTmp, PW_CLIENTONLY); SetStretchBltMode(hdcMem, HALFTONE); StretchBlt(hdcMem, 0, 0, srcW * scale, srcH * scale, hdcTmp, 0, 0, srcW, srcH, SRCCOPY); CImage img; img.Attach(hbm); HRESULT hr img.Save(filePath); // 按 .png/.jpg 后缀选编码器 img.Detach(); SelectObject(hdcTmp, oldTmp); DeleteDC(hdcTmp); DeleteObject(hbmTmp); SelectObject(hdcMem, oldBmp); DeleteDC(hdcMem); DeleteObject(hbm); ReleaseDC(hwnd, hdcWin); return SUCCEEDED(hr); }PrintWindow 比 BitBlt 更可靠的地方在于能抓到被其他窗口遮挡、甚至由系统窗口合成器管理的内容PW_CLIENTONLY 限定只抓客户区正好覆盖图纸区。scale 参数用于输出 2 倍图先在原尺寸 HDC 上 PrintWindow再 StretchBlt 放大比直接对放大尺寸的画布执行 PrintWindow 更清晰。CImage 的 Save 按文件后缀选择编码器图纸线条建议存 PNG 无损耗压缩照片类才用 JPG。4.3 高分屏与电脑长截屏分片平移后再拼接两件事单独说。一是高分屏Win10/11 下进程不声明 DPI AwarenessGetClientRect 返回的是缩放后的虚拟尺寸截出来的图明显发虚。在模块初始化处调用 SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)让 RECT 拿到真实物理像素再配合 PrintWindow 才能得到清晰截图。二是电脑长截屏的思路AutoCAD 视口只能截到当前范围要覆盖超出屏幕的图纸区域就用第 3 章的函数固定 viewHeight沿 X/Y 方向递增 viewCenter逐片截取然后在内存里按像素偏移用 GDI DrawImage 拼成一张长图。这套逻辑与截图工具里的卷动截屏同源只是平移动作从鼠标滚轮换成了程序改 viewCenter。4.4 截屏失败排查表现象常见原因处理方式截出来全黑硬件加速下 PrintWindow 抓不到 OpenGL 表面临时关闭硬件加速或改用 BitBlt尺寸只有预期一半未声明 DPI AwarenessRECT 为虚拟像素启动时设置 PerMonitorV2图片发虚糊边放大方式不对先原尺寸抓再 HALFTONE 拉伸保存文件为 0 字节窗口最小化或画布尺寸为 0先 IsIconic(hwnd) 判断窗口状态颜色整体偏灰位图像素格式与屏显不一致Attach 前转成 32 位 BGRA第二轮最隐蔽一台机器正常、另一台截出来半尺寸多半不是代码问题而是系统缩放比例不同。DPI 声明统一放到 DLL 入口或模块初始化处不要放在命令函数里因为主窗口创建时的上下文已经固定。5. 保存视图 dwg 文件整体另存与批量导出命名视图5.1 先判断需求副本另存还是视图抽取“保存视图 dwg”有两种语义一是保留当前图形全部内容另存一份副本二是只把某个命名视图相关的内容抽出来交付。前者直接让工作数据库另存acdbHostApplicationServices()-writeFile( acdbHostApplicationServices()-workingDatabase(), LD:\\view_snapshot.dwg);后者要用 wblock 克隆且只克隆视图记录不会带出任何实体新文件打开后视图表有参数模型空间却是空的。判断标准很简单下游要不要看到图形。要看到就把视图记录和模型空间实体一并加入源 id 数组只关心视图定义就只克隆视图表记录。5.2 批量导出所有命名视图的实现static void exportAllViews(const ACHAR* dir) { AcDbDatabase* pDb acdbHostApplicationServices()-workingDatabase(); AcDbViewTable* pVT nullptr; if (pDb-getViewTable(pVT, AcDb::kForRead) ! Acad::eOk) return; AcDbViewTableIterator* pIter nullptr; pVT-newIterator(pIter); for (; !pIter-done(); pIter-step()) { AcDbViewTableRecord* pRec nullptr; if (pIter-getRecord(pRec, AcDb::kForRead) ! Acad::eOk) continue; AcDbDatabase* pNewDb new AcDbDatabase(Adesk::kTrue); // 带默认表 AcDbObjectIdArray srcIds; srcIds.append(pRec-objectId()); AcDbObjectId ownerId pNewDb-viewTableId(); // 目标所有者 AcDbObjectIdMapping* pIdMap nullptr; Acad::ErrorStatus es acdbWblockCloneObjects( srcIds, ownerId, AcDb::kDrcContinue, AcDb::kDrcIgnore, pIdMap, Adesk::kTrue); if (es Acad::eOk) { CString path; path.Format(_T(%s\\%s.dwg), dir, pRec-name()); acdbHostApplicationServices()-writeFile(pNewDb, path); } delete pIdMap; delete pNewDb; pRec-close(); } delete pIter; pVT-close(); }acdbWblockCloneObjects 的参数依次是源对象数组、目标所有者、重复记录策略、所有者克隆策略、返回的 id 映射表、是否部分克隆。kDrcContinue 让重名时继续克隆不弹窗kDrcIgnore 忽略所有者映射差异idMap 用完后必须 delete。新库用 Adesk::kTrue 创建保证带上默认图层与线型否则打开时可能缺基础设施对象。5.3 验证导出文件读回并比对视图参数导出后把文件当普通 DWG 读回检查视图表记录是否与源库一致AcDbDatabase* pCheck new AcDbDatabase(Adesk::kFalse); if (pCheck-readDwgFile(LD:\\VIEW_1.dwg, Adesk::kFalse) Acad::eOk) { AcDbViewTable* pVT2 nullptr; pCheck-getViewTable(pVT2, AcDb::kForRead); AcDbViewTableRecord* pRec nullptr; if (pVT2-getAt(_T(VIEW_1), pRec, AcDb::kForRead) Acad::eOk) { acutPrintf(_T(\n导出 height%f center(%f,%f)), pRec-height(), pRec-center().x, pRec-center().y); pRec-close(); } pVT2-close(); } delete pCheck;比对源库同名视图的 center 与 height误差应在 1e-9 以内。批量导出时用 pIdMap-computedId(srcId, mappedId) 拿到“源 id 到新库 id”的映射把映射关系写进日志比逐个打开文件确认快得多。这也正好是四个能力的落点读取信息做数据源缩放负责定位视口截屏留图导出文件做交付四个函数共用同一套视图参数结构不会出现参数各算各的偏差。本文还有配套的精品资源点击获取
返回列表