
简介面向AutoCAD二次开发者的objectARX示例工程核心解决视图窗口相关操作的可复用实现适用于需要用C在AutoCAD中扩展视图功能的插件开发者。代码模拟了ZOOM的窗口缩放、范围缩放与比例缩放并封装获取当前视图窗口信息、将视图区域截屏保存为图片、把选中实体写出为独立dwg文件三个常用能力。整个压缩包共35个文件以cpp源文件和h头文件为主另有props属性表、sln解决方案文件、rc资源文件等组成一个可在Visual Studio中直接打开的完整工程包体仅50KB结构紧凑。已有432人学习下载。代码模块划分清晰ViewUtil专注视图操作DwgDatabaseUtil处理数据库读写GePointUtil与GeRectangleUtil负责几何计算便于边读边改既适合objectARX新手理解视图与数据库交互的套路也能让有经验的开发者直接抽取相应函数嵌入自身项目在视图截屏、实体另存等典型场景中节省重复开发时间。1. 从 ZOOM 命令到 ARX 接口视图控制为什么要脱离命令行在 AutoCAD 里用滚轮和 ZOOM 命令缩放视图绝大多数人已经习惯了放大、缩小、平移或者输入Z回车后选个窗口。但当你需要做批量出图、图纸比对或者把当前视图批量导出成图片和 DWG 时每次手动敲ZOOM和VIEW就成了瓶颈。更不能接受的是acedCommand发的命令在跨图档切换时经常拿到过期的上下文视口没激活就放大到了错误的位置。ArxProjectChangeView 这套代码做的事就是在 objectARX 层接管视图这个对象把窗口缩放、范围缩放、比例缩放、视图信息读取、截屏、选中实体导出 DWG 这六件事全部用 SDK 接口直接操作。适合正在做 CAD 图纸处理工具链、批量水印、局部导出这类活儿的二次开发工程师。2. 视图窗口缩放的三种实现ViewTableRecord 与坐标换算2.1 视图在 objectARX 里的真实形态在 AutoCAD 的数据模型里每个布局可以有多个视口viewport而每个视口对应一条AcDbViewTableRecord。这条记录保存了视图的 center中心点、height高度、width宽度、target目标点和 viewDirection视线方向等参数。缩放的本质不是改变图形数据而是改变这条AcDbViewTableRecord里的数值。同样是放大窗口用acedCommandS发ZOOM_W是让 AutoCAD 内部的命令处理器去改数值而直接构造、修改AcDbViewTableRecord可以绕开命令栈获得确定性的结果还能在批量处理时把参数提前算好再一次性应用。acedCommand最大的问题在于它依赖当前的文档和当前空间。一旦切到图纸空间或者当前没有激活视口命令会静默失败或作用在错误的上下文上。而acdbCurViewportObjectId()拿到的是当前视口对象的 ID用这个 ID 打开实体拿到AcDbViewTableRecord始终是指向这个文档当前正在显示的那个视图与用户手动切换没有竞态问题。2.2 窗口缩放的核心代码窗口缩放是最常用的操作给定两个对角点让这两个点正好包住当前视口。核心做法是取出当前AcDbViewTableRecord计算对角点相对于视图中心点的偏移量再把高度设置成与宽度匹配的长边。下面这段代码来自 ViewUtil.cppbool ZoomToWindow(const AcGePoint2d pt1, const AcGePoint2d pt2) { // 获取当前文档活动视口的对象ID AcDbObjectId viewportId acdbCurViewportObjectId(); if (viewportId.isNull()) return false; // 以写权限打开视图表记录修改其数值 AcDbObjectPointerAcDbViewTableRecord pRecord( viewportId, AcDb::kForWrite); if (pRecord.openStatus() ! Acad::eOk) return false; double width fabs(pt2.x - pt1.x); double height fabs(pt2.y - pt1.y); if (width 1e-8 || height 1e-8) return false; // 退化窗口直接拒绝 AcGePoint2d mid((pt1.x pt2.x) / 2.0, (pt1.y pt2.y) / 2.0); // 视图高度必须保证窗口宽高比不变否则横向会被裁掉 double viewH max(width, height); pRecord-setCenter(mid); pRecord-setHeight(viewH); pRecord-setWidth(width * (height width ? 1.0 : height / width)); pRecord-downgradeOpen(); // 让出写权限触发视图缓存重建 return true; }代码里的setHeight和setWidth必须同时设置。如果只改高度AutoCAD 会保持宽高比不变而自动伸缩宽度导致你选的窗口最终超出屏幕范围。另一个容易被忽略的地方是downgradeOpen它告诉 AutoCAD 视图记录改动完毕可以重建显示缓存了如果一直持有写权限视图并不会立刻刷新。1e-8的判断是防止用户框选时手抖拉出一个几乎为零的矩形此时直接返回false比让视图跳变到不可用状态更友好。2.3 范围缩放与比例缩放范围缩放ZOOM_EXTENTS是批处理里出现频率最高的要截图得先保证整个图形都在视野内。实现上需要先拿到当前空间所有实体的包围盒再把这个包围盒当作窗口参数设置给 ViewTableRecord。bool ZoomToExtents() { // 打开模型空间的块表记录 AcDbBlockTableRecord* pBlock nullptr; AcDbDatabase* pDb acdbHostApplicationServices()-workingDatabase(); AcDbBlockTable* pTable nullptr; pDb-getBlockTable(pTable, AcDb::kForRead); pTable-getAt(ACDB_MODEL_SPACE, pBlock, AcDb::kForRead); pTable-close(); // 遍历所有实体求几何包围盒的并集 AcDbExtents totalExtents; AcDbBlockTableRecordIterator* pIter nullptr; pBlock-newIterator(pIter); for (; !pIter-done(); pIter-step()) { AcDbEntity* pEnt nullptr; pIter-getEntity(pEnt, AcDb::kForRead); AcDbExtents entExt; // 块引用、多段线、标注都会正确返回几何范围 if (pEnt-getGeomExtents(entExt) Acad::eOk) totalExtents.addExt(entExt); pEnt-close(); } delete pIter; pBlock-close(); // 给包围盒加 5% 边距避免图形顶到视口边缘 AcGeVector3d pad (totalExtents.maxPoint() - totalExtents.minPoint()) * 0.05; totalExtents.minPoint() - pad; totalExtents.maxPoint() pad; return ZoomToWindow( AcGePoint2d(totalExtents.minPoint().x, totalExtents.minPoint().y), AcGePoint2d(totalExtents.maxPoint().x, totalExtents.maxPoint().y)); }getGeomExtents对块引用、多段线、标注这些实体会各自返回正确的几何范围这是可靠的根本。但有两个例外要注意图案填充在超大图形上可能返回空范围这是已知现象以及某些代理实体根本不实现getGeomExtents。所以循环里不要因为某个实体取不到范围就中断直接跳过继续累计。边距系数0.05是个经验值标注和外框刚好贴边的图纸截出来很难看留 5% 能让导出图片的四周有呼吸感但如果是做 GIS 数据切片这个值建议改成0.0因为切片系统对边界像素有严格对齐要求。比例缩放用得相对少但做放大到 2 倍这类交互时逻辑很简单拿到当前 ViewTableRecord 的height乘以缩放系数再写回去。有一个容易踩的坑是模型空间和图纸空间的视口含义不同。图纸空间的视图高度就是图纸单位模型空间视口则还要乘上 zoom XP 比例。批量脚本里如果混用画面会突然跳一大截建议在代码里显式区分acdbPaperSpaceVport()和模型空间视口而不是用一个统一的函数处理所有空间。3. 视图信息读取与截屏把屏幕内容变成文件3.1 当前视图窗口的关键参数截屏、导出、加水印之前都得先知道现在屏幕上到底显示了什么。从AcDbViewTableRecord可以拿到以下关键参数参数获取方法用途中心点record-center()定位目标区域视图高度record-height()计算显示范围视图宽度record-width()配合高度做比例换算旋转角record-viewTwist()判断视图是否有 twist截图后要旋转补偿相机目标record-target()视图空间坐标原点视线方向record-viewDirection()判断是俯视还是透视图透视开关record-isPerspectiveEnabled()决定是否要做透视校正宽高比height() / width()用于截屏尺寸等比计算在实际项目中旋转角和透视开关这两个参数最容易被忽略。一个设置了 UCS 的图纸截屏出来的图片如果不做逆向旋转文字会全部歪掉。所以在截屏前先判断viewTwist()是否为 0不为 0 时要么调用acedSetCurrentView临时复位要么在截图后的图像上做旋转补偿。透视开关同理——透视图截出来是一个包含消失点的投影直接拿来当平面图用会误导看图人。3.2 截屏的两种实现路线第一种是用acedCommand发PLOT或SAVEIMG这类命令缺点和在视图缩放里讲的一样依赖交互式命令且新版 AutoCAD 对SAVEIMG的输出格式控制很少PNG 的 DPI 和背景色都不能精细调节。第二种是直接从 AutoCAD 的文档窗口取 HWND用 GDI 的BitBlt把窗口客户区拷出来。ArxProjectViewScreenshot.cpp 走的是第二种因为它不依赖命令回显在多文档环境下也能指定截取哪个文档的窗口。bool CaptureViewportToFile(const CString filePath, int pixelDpi) { // 获取AutoCAD主窗口句柄MDI模式下需要下钻到视图子窗口 HWND hMain adsw_acadMainWnd(); HWND hMdi FindWindowEx(hMain, nullptr, LMDIClient, nullptr); HWND hView FindWindowEx(hMdi, nullptr, LAcViewPort, nullptr); if (!hView) return false; RECT rc; GetClientRect(hView, rc); // 按目标DPI换算像素尺寸96是Windows默认逻辑DPI int width (rc.right - rc.left) * pixelDpi / 96; int height (rc.bottom - rc.top) * pixelDpi / 96; HDC hdcWindow GetDC(hView); HDC hdcMem CreateCompatibleDC(hdcWindow); HBITMAP hBmp CreateCompatibleBitmap(hdcWindow, width, height); SelectObject(hdcMem, hBmp); // 拉伸拷贝HALFTONE模式抗锯齿效果最好 SetStretchBltMode(hdcMem, HALFTONE); StretchBlt(hdcMem, 0, 0, width, height, hdcWindow, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SRCCOPY); // 存成PNG避免JPEG压缩导致标注文字发糊 CImage image; image.Attach(hBmp); image.Save(filePath, Gdiplus::ImageFormatPNG); image.Detach(); DeleteObject(hBmp); DeleteDC(hdcMem); ReleaseDC(hView, hdcWindow); return true; }核心参数是pixelDpi。AutoCAD 的窗口尺寸是按逻辑像素算的而你要的图片可能要用 300DPI 打印所以必须乘一个比例因子。如果直接用GetClientRect的尺寸存图在 4K 显示器或者 Windows 缩放比例不是 100% 的环境下出来的 PNG 会偏小打印时明显发虚。HALFTONE模式比COLORONCOLOR在拉伸缩小时抗锯齿效果好标注文字看起来更干净。提示高DPI环境下AutoCAD 2017 进程的 DPI 感知设置会直接影响GetClientRect的返回值。如果系统缩放是 150%而进程没声明SetProcessDPIAware拿到的客户区尺寸只有实际的三分之二截图右边和下边会被裁掉。建议在acrxEntryPoint初始化时调用一次SetProcessDPIAware()。3.3 截屏的边界处理与图层叠加问题新手最容易犯的错是把工具栏、命令行、状态栏一起截进图片。虽然通过FindWindowEx找AcViewPort已经过滤掉了大部分 UI 元素但视图内部还叠着 ViewCube、导航栏、坐标轴图标。这些属于 OpenGL 叠加层纯 GDI 的BitBlt截不到——这反而成了优势因为存储文件里不该有这些 UI 元素。如果你确实需要在截图里保留 ViewCube就得换IDocumentWindow的getGdiViewport接口走屏幕抓取性能和兼容性会差很多非必要不这么干。另一个场景是布局空间paper space的截屏。布局里的视口是一个一个的AcDbViewport实体它们叠加在图纸背景上BitBlt截出来直接就是最终的图纸效果这反而是模型空间截图做不到的。所以如果业务目标是把一张图纸变成一张图片对外发送从布局截图是更省事的路径——不需要手动控制图层开关和视口比例布局里怎么排的就怎么出图。4. 选中实体导出 DWG选择集与跨数据库拷贝4.1 选择集过滤与实体遍历导出 DWG 的第一步是拿到用户选中的实体。工程里的做法是把 SelectUtil 封装成统一的回调入口这样无论是命令里调用还是将来做成面板按钮都能复用同一套选择逻辑bool SelectEntities(AcDbObjectIdArray outIds, const CString prompt) { ads_name ssname; // 用户只能通过窗口方式选择排除点选和栏选 if (acedSSGet(L_W, nullptr, nullptr, nullptr, ssname) ! RTNORM) return false; Adesk::Int32 len 0; acedSSLength(ssname, len); for (Adesk::Int32 i 0; i len; i) { ads_name ent; acedSSName(ssname, i, ent); AcDbObjectId id; acdbGetObjectId(id, ent); // 过滤掉不可见的实体避免导出后出现空白块 AcDbEntity* pEnt nullptr; if (acdbOpenObject(pEnt, id, AcDb::kForRead) Acad::eOk) { if (!pEnt-isHidden() !pEnt-isErased()) outIds.append(id); pEnt-close(); } } acedSSFree(ssname); return outIds.length() 0; }acedSSGet的第一个参数_W表示仅窗口选择即实体必须完全落在窗口内才被选中交叉选择用_C。这里有一个交互细节acedSSGet的第三个参数传nullptr时会使用默认的点选方式用户可能会误点单个实体导致选择集超出预期。所以工程里固定传_W或_C配合窗口矩形行为可预期。如果用户直接回车或按 Esc返回的不是RTNORM代码直接退出不继续往下走。4.2 用 wblockCloneObjects 跨数据库复制拿到实体 ID 后目标是把它们写进一个新的*.dwg文件。最核心的 API 是wblockCloneObjects它能把源数据库里的一组对象连同相关依赖线型、图层、块定义一起克隆到目标数据库Acad::ErrorStatus ExportEntitiesToDwg( const AcDbObjectIdArray entIds, const CString destPath) { // 创建空数据库不基于任何模板 AcDbDatabase* pDestDb new AcDbDatabase(Adesk::kFalse); pDestDb-setFilename(destPath); // ID映射表记录新旧对象对应关系自动清理临时记录 AcDbIdMapping idMap; idMap.setAutoErase(true); // 深克隆实体连同图层、线型、块定义一起拷贝 Acad::ErrorStatus es pDestDb-wblockCloneObjects( entIds, pDestDb-getModelSpaceId(), idMap, AcDb::kDrcDeepCloneObjects | AcDb::kDrcCopyDwgProps, /*bLockSourceDb*/true); if (es ! Acad::eOk) { delete pDestDb; return es; } // 保存为DWG并释放目标数据库 es pDestDb-saveAs(destPath, AcDb::kDwg); delete pDestDb; return es; }参数说明Adesk::kFalse表示新数据库不基于任何模板纯空库之后克隆进来的实体自带图层和线型表记录。kDrcDeepCloneObjects是深克隆的关键没有它块引用的内部图元不会跟着出来导出的 DWG 打开后要么缺块内实体要么块引用直接变成代理图元。kDrcCopyDwgProps复制 DWG 属性比如创建时间戳和修订信息多用于图纸管理场景。4.3 常见失败模式与处理方法wblockCloneObjects有几个隐藏约束需要特别留意。源数据库必须处于读锁状态代码里bLockSourceDb传true就是让 SDK 内部替我们处理锁不需要额外调用lockDocument。如果目标数据库已经存在同名文件saveAs的行为取决于 AutoCAD 版本的实现有的版本直接覆盖有的版本报eFileAccessDenied建议在调用前用CFile::GetStatus检查目标文件是否存在存在就弹确认框。导出后最常见的验证方式是开一个干净 AutoCAD不带任何 ARX 加载把导出的文件拖进去重点检查三处——图层是否完整、线型是否保持、块定义是否可编辑。如果线型变成实线基本可以断定是 ID 映射里没有带上线型表记录回到调用处检查kDrcDeepCloneObjects是否传入如果块引用还在但内容为空多半是源数据库里块表记录没有同步克隆进来。5. 从视图到文件的完整链路命令注册与整体验证四个核心功能如果各自独立能跑通但用户每次要敲四个命令实际使用体验很割裂。ArxProjectChangeView 里把这几个能力串成了一条命令链用户在命令行敲一个命令就能完成范围缩放 → 框选 → 截屏 → 导出DW G的完整流程。关键在acrxEntryPoint.cpp里的命令注册方式// 全局命令注册命令名为ChangeView执行链式流程 static void ChangeViewCmd() { // 1. 范围缩放先把全图摆到视野内 ZoomToExtents(); // 2. 让用户框选目标区域并记录屏幕坐标 AcGePoint2d pt1, pt2; if (!GetUserWindow(pt1, pt2)) return; // 用户取消则退出 // 3. 按框选窗口重新缩放视图 ZoomToWindow(pt1, pt2); // 4. 截屏文件名带上时间戳 CString shotPath BuildOutputPath(Lshot, Lpng); CaptureViewportToFile(shotPath, 192); // 5. 导出框选区域内的实体为独立DWG AcDbObjectIdArray ids; SelectEntitiesInWindow(pt1, pt2, ids); if (ids.length() 0) { CString dwgPath BuildOutputPath(Lexport, Ldwg); ExportEntitiesToDwg(ids, dwgPath); } }ACRX_CMD_MODAL和ACRX_CMD_TRANSPARENT的区别在链式命令里体现得很明显。如果注册成ACRX_CMD_MODAL用户在执行这条命令期间无法做任何视图平移操作框选时如果发现视野不对只能取消重来。注册成ACRX_CMD_TRANSPARENT后用户可以在命令执行过程中随时滚轮缩放命令内部取到的视口参数始终是实时的。代价是透明命令内部不能调用其他交互式命令所以在实现里要避免在透明命令中再嵌套acedSSGet以外的交互调用。验证整条链路时我建议按这个顺序检查先跑ZoomToExtents确认全图能完整显示在视口内再框选一个包含块引用和标注的区域执行截屏打开 PNG 确认宽高比和当前屏幕一致最后导出 DWG在干净环境里打开检查图层和线型。如果导出后图面比源图偏移了几个单位检查目标数据库的setInsbase是否被重置如果截屏图片发白检查HALFTONE模式是否被系统策略覆盖。把这些检查点固化成一个几分钟的冒烟用例后面每次改代码重新编译都先跑一遍这条链路再谈新功能。本文还有配套的精品资源点击获取