写QCAD脚本这件事,最让我喜欢的地方在于:它真的能杀掉那些重复操作的时间。一套图纸画完之后,整理图层、排孔位、加编号、统计长度,这些活儿说难不难,但点鼠标点到手酸。这一篇是“QCAD 代码片段”系列的第二篇,按老规矩,我不讲太复杂的框架,只挑四个能直接拿来用的片段:按文本内容批量归图层、网格孔位自动生成、给选中实体加批量编号、统计选中线段的长度总和。每个片段都配有代码和注释,适合刚接触QCAD脚本的朋友,也适合已经会一些基础操作、想提高出图效率的人做参考。下面所有代码都是基于QCAD自带的ECMAScript接口写的,不同版本的类名和参数可能会有一点点差异,用到时以你本地的接口为准。
1. 内容整体设计与思路拆解
1.1 为什么自己写脚本而不是装现成插件
平时用QCAD出图,处理外来图纸时最常遇到的情况就是图形乱、图层乱、文字乱。市场上有不少现成的辅助工具,但实际用下来的体验往往是:装了一堆,能用上的没几个。比如你只是想“把文字批量归到某个图层”,为这个单一需求去装一个大而全的插件,还要忍受它带来的额外工具栏、快捷键甚至菜单,整体来说非常不划算。更关键的一点是,现成插件通常被设计成“通用方案”,不会完全贴合你的规则需求。你自己写脚本就不一样了,想按内容归类就按内容归类,想加前缀就加前缀,规则完全由你定。而且脚本写好一次,后面遇到类似图纸就是纯复用,这种长期积累的价值比装一个黑盒插件要大得多。
写脚本这件事本身也没有想象中那么难。QCAD的脚本语法是ECMAScript,和JavaScript基本没有区别,不需要额外的编译环境,也不用装任何工具链。哪怕你只写过几行网页脚本,也能很快看懂QCAD的脚本在做什么。刚开始不熟悉API没关系,先把别人的代码拿过来改,改着改着就摸清楚那一套逻辑了。我自己就是从“想复制几个圆都写不明白”的水平起步的,现在处理批量操作基本不慌。
1.2 这期片段的选题原则
这期选的四个片段,背后其实有一条共同标准:它们在出图流程里都够高频,而且每个片段都对应QCAD脚本的一个典型能力方向。按文本内容归图层,核心是遍历实体和修改属性;网格孔位自动生成,核心是批量创建实体;给选中实体批量编号,核心是读取选中集和创建标注;统计线段长度,核心是遍历数据并做数值计算。这四个方向几乎覆盖了日常脚本里最常用的操作类型。把这几个片段吃透,之后遇到“批量做什么”的需求,基本都能套同一套思路来解决。
另外我特意没有选择那些“看起来很炫但实际用不上”的脚本场景。比如自动生成复杂的参数化图案,这种需求本身就冷门,代码还长,不适合放进一个讲实操的系列里。系列内容的价值就在于解决真实问题,不是展示技巧。这一篇延续上一篇的基调:代码尽量短,注释尽量多,场景尽量贴近真实工作,让大家抄完就能跑起来。
1.3 脚本的运行方式先交代清楚
进入代码之前,有必要把运行环境讲清楚,否则代码拿过去不知道往哪儿放。QCAD从比较早的版本开始就内置了ECMAScript支持,不需要额外安装运行库。日常最快速的验证方式,是打开QCAD自带脚本控制台,把代码直接粘进去回车执行,适合临时跑一次。如果想长期使用,建议把脚本保存成独立的脚本文件,放到用户配置目录下,做成命令来调用。这个目录的具体位置以及怎么注册成命令,我在第三大节会展开讲,这里只需要记住:控制台适合试验,脚本文件适合复用。两种方式都不涉及复杂的配置,拿到代码后很快就能试起来。
2. 核心细节解析与实操要点
2.1 片段一:按文本内容批量归图层
适用场景
外来的DWG文件经常出现一种问题:所有文字都堆在0图层,没有分层,导致显示颜色、打印线宽完全不受控制。手动一个个选中再做属性修改,偶尔处理几个还行,几十上百个文字时就很折磨人。这段脚本的作用就是自动遍历当前图纸中的所有文本实体,找到包含指定关键词的文字,把它移动到目标图层。目标图层不存在时会自动创建,存在就直接复用。
function batchMoveTextByContent() { var doc = app.getDocument(); if (isNull(doc)) { qWarning("未获得活动文档"); return; } var storage = doc.getStorage(); var entityIds = storage.getEntityIds(); var op = new RAddObjectsOperation(); for (var i = 0; i < entityIds.length; i++) { var entity = storage.queryObject(entityIds[i]); if (entity.getType() !== RS.EntityText) { continue; } var txt = entity.getText(); if (txt.indexOf("标题") !== -1) { var layerId = doc.getLayerId("TEXT_TITLE"); if (layerId === -1) { layerId = doc.addLayer("TEXT_TITLE", RColor(255, 0, 0)); } entity.setLayerId(layerId); storage.updateObject(entity); } } } batchMoveTextByContent();关键解析
doc.getStorage()拿到的是当前图形数据库,也就是所有实体和图层信息存放的地方。storage.getEntityIds()返回全部实体ID数组,这是QCAD脚本里遍历图形的基础操作。拿到ID之后,通过storage.queryObject(entityIds[i])取回实体对象,再用getType()判断类型。这里匹配的是RS.EntityText,也就是文本实体;如果你想同时处理尺寸标注里的文字,需要额外判断其它类型。
图层判断这里有个细节。doc.getLayerId("TEXT_TITLE")是拿图层ID,如果返回的是无效ID,说明图层不存在,就用doc.addLayer()新建。我特意把新建操作放在循环里判断,而不是一开始就假设图层存在,这样脚本能兼容“图层已经有了”和“图层还没有”两种情况。新图层直接指定为红色(255,0,0),实际使用时可以根据公司图层标准改颜色。
注意事项
跑这种批量修改类脚本之前,一定要先另存一份图纸。我现在养成习惯是把原图复制一份叫“xxx_bak.dxf”,脚本风险再低也抵不过一次误操作。关键词匹配用的是indexOf("标题"),只要文本内容里包含“标题”两个字就会被选中;如果想精确前缀匹配,可以换成txt.startsWith("标题"),这样只有开头是“标题”的文字才会被移动。还有一个容易忽略的点:脚本运行完成后,记得检查目标图层的颜色和线型设置,否则文字虽然归层了,但显示效果不一定符合预期。
2.2 片段二:网格孔位批量生成
适用场景
通风板、挡板、导轨安装孔,这些孔要么排成整齐阵列,要么按固定间距错位分布。手动复制的话,孔稍微多一点就容易点得眼花,间距还不一定准。脚本按数学规则生成,间距误差几乎为零,而且孔的数量、直径、行列数都可以通过参数随时调整,改起来比手动重画快太多了。
function createHoleGrid() { var doc = app.getDocument(); if (isNull(doc)) { return; } var startX = 0.0, startY = 0.0; var holeDiameter = 8.0; var gapX = 25.0, gapY = 25.0; var cols = 5, rows = 3; var op = new RAddObjectsOperation(); for (var r = 0; r < rows; r++) { for (var c = 0; c < cols; c++) { var cx = startX + c * gapX; var cy = startY + r * gapY; var circle = new RCircle(new RVector(cx, cy), holeDiameter / 2.0); op.addObject(circle); } } doc.getDocumentInterface().applyOperation(op); doc.getDocumentInterface().regenerate(); } createHoleGrid();关键解析
双层循环的结构很直观:外层管行数rows,内层管列数cols,每次循环算出当前孔位的圆心坐标(cx, cy)。这里有一个很常见的坑:RCircle构造函数的第二个参数是半径,不是直径。我在写代码时先把holeDiameter定义成直径,然后在传入时除以2,这样参数含义清楚,也不太容易搞混。如果你直接拿直径传进去,生成的圆就会比预期大整整一倍。
RAddObjectsOperation是QCAD脚本里一个非常实用的对象,它可以把多个创建操作打包进同一个事务。这样做有两个直接好处:一是性能好,比逐个提交对象要快不少;二是撤销方便,执行完以后按一次撤销整批孔就全部消失,不会出现一格一格删的尴尬局面。代码末尾的applyOperation(op)是把事务提交到文档上,regenerate()则是强制刷新视图,让新对象立刻显示出来。
变体思路
这套网格生成逻辑很好扩展。错位排孔只需要加一个判断:奇数行时把cx整体偏移gapX / 2.0,就能生成常见的不锈钢网板孔位。如果只想在某些位置出孔,可以在循环里加if条件跳过特定行列。想控制孔位所在图层,也可以创建一个layerId,在new RCircle()之后调用circle.setLayerId(layerId)。目前代码是在默认图层生成,实际项目和图层标准配合时,这一步往往不能省。
2.3 片段三:给选中的实体批量编号
适用场景
装配图里给每个零件标序号,检测报告里给每个测点加编号,这些场景都有一个共同特点:实体已经摆在图里了,你需要的只是快速给它们赋上“N-1”“N-2”“N-3”这样的标签。手动输入文本、手动摆放位置,数量多且格式难以统一。这段脚本的思路是:读取当前选中的所有实体,在每个实体的端点位置自动放置一个有递进序号的文本。
function numberSelectedEntities() { var doc = app.getDocument(); if (isNull(doc)) { return; } var ids = doc.querySelectedEntities(); if (ids.length === 0) { qInfo("未选中任何实体"); return; } var op = new RAddObjectsOperation(); var prefix = "N-"; var startNo = 1; var textHeight = 3.0; for (var i = 0; i < ids.length; i++) { var entity = doc.queryObject(ids[i]); if (isNull(entity)) { continue; } var pos = entity.getEndPoint(); if (isNull(pos)) { continue; } var label = prefix + (startNo + i); var txt = new RText(pos, label, textHeight, 0.0); op.addObject(txt); } doc.getDocumentInterface().applyOperation(op); } numberSelectedEntities();关键解析
doc.querySelectedEntities()返回的是一个ID数组,内容就是当前图纸中被选中的实体。这里先做了一次空判断,没有选中任何东西就直接提示退出,避免脚本静默运行后让你一脸疑惑。getEndPoint()方法对线段、圆弧这类实体非常有效,会返回终点坐标;对文字或其它复杂实体,返回位置的含义可能会有差异。因此代码里加了isNull(pos)检查,拿不到有效位置就跳过,不让一个异常实体影响整批编号。
RText构造函数的前四个参数分别是放置点、文本内容、字高和旋转角度。这里字高统一用的3.0,实际项目中需要根据图纸比例调整。文本内容用prefix + (startNo + i)拼接,跑完以后会得到“N-1”“N-2”一直到“N-10”这样的序列。如果你希望从某个特定数字开始编,直接改startNo就行。
注意事项
文本都放在getEndPoint()的位置,实体的端点和文本插入点如果靠得太近,编号文字就会和图形本身重叠。这种情况下可以在代码里加一点偏移,比如生成pos后手动构造一个新的RVector(pos.x + 2.0, pos.y + 2.0)再传给文本对象。还有一个批次撤销的好消息:整个脚本的所有文本都在一个事务里,撤销一次就全没了,不会留下半截编号。但反过来也需要注意,如果你想把这次编号和之前的某个操作区分开撤销,就得拆成两次执行。
2.4 片段四:快速统计选中线段长度总和
适用场景
材料统计、线材预算、外框测量,这类需求经常要算“图中所有线段一共有多少长度”。手动打开属性面板,一个个看长度再加总,效率太低,而且中途接个电话就容易忘记算到哪一条。脚本直接输出两个结果:选中了几条线段、总长度是多少。尤其适合全部选中或者框选一块区域后统计,几秒钟出结果。
function sumSelectedLineLength() { var doc = app.getDocument(); if (isNull(doc)) { return; } var ids = doc.querySelectedEntities(); var total = 0.0; var count = 0; for (var i = 0; i < ids.length; i++) { var entity = doc.queryObject(ids[i]); if (entity.getType() === RS.EntityLine) { total += entity.getLength(); count++; } } qInfo("选中线段数量: " + count + ", 总长度: " + total.toFixed(3)); } sumSelectedLineLength();关键解析
getLength()是QCAD给线型实体提供的方法,调用后直接拿长度值。这里用getType() === RS.EntityLine筛掉其它类型的实体,只统计真正的直线段。total.toFixed(3)是JavaScript的数值格式化方法,会把结果保留到三位小数,避免输出一长串浮点数。打印用的qInfo()会输出到脚本控制台,信息量适中,适合做结果展示。
常见扩展
这个脚本改一改就能支持更多统计需求。比如想统计圆弧的总长度,在if条件里加上|| entity.getType() === RS.EntityArc,圆弧长度也会被累加。想统计多段线,则需要额外解析多段线的顶点,逐段求距离再加总。输出方式也可以升级,不只打印到控制台,而是生成一个文本实体放到图里,或者写进CSV文件留档。这种扩展思路其实就是把QCAD脚本当“带图形界面的数据处理器”来用,价值比单纯画圆画线高不少。
2.5 四个片段怎么选:一个速查表
整理成一张表,方便你按需求快速定位该用哪段脚本。
| 片段 | 核心API | 典型场景 | 建议先改的参数 |
|---|---|---|---|
| 按内容归图层 | getEntityIds / setLayerId | 外来图纸文字整理 | 关键词、目标图层名 |
| 网格孔位生成 | RCircle / RAddObjectsOperation | 钣金、通风板、安装孔 | 行列数、间距、孔径 |
| 批量编号 | querySelectedEntities / RText | 零件序号、测点编号 | 前缀、起始号、字高 |
| 统计线长 | getLength / qInfo | 材料统计、线材预算 | 统计类型、输出格式 |
3. 实操过程与核心环节实现
3.1 脚本文件应该放在哪里
长期使用的脚本不建议放在QCAD安装目录里,因为以后升级软件时可能被覆盖。个人建议放到用户配置目录下的scripts文件夹中。Windows下通常在用户目录下能找到QCAD相关配置,Linux和Mac下一般是~/.config/QCAD下对应的路径。你可以在QCAD菜单里查看脚本路径相关设置,确认自己机器上的准确位置。放进去之后,脚本就能被QCAD识别和调用,但如果你想让它出现在某个特定命令下,还需要做一点注册工作。这部分取决于你使用的QCAD版本,最简单可靠的参照是安装目录自带的一堆插件示例。
3.2 从控制台运行到脚本文件执行
最快的验证方式是打开脚本控制台。在QCAD里找到控制台入口,把代码整体粘贴进去,直接回车。脚本末尾的batchMoveTextByContent()、createHoleGrid()这类语句会立刻执行,控制台窗口会输出相应结果。这一段适合用来验证代码有没有写错,也适合临时处理一次图纸。
脚本文件的方式更适合高频复用。把代码保存成.js或.qs文件,放到scripts目录后,在QCAD的“运行脚本”入口选择该文件执行。两种方式跑出来的效果完全一致,区别只在于代码的存在位置。结合我自己经验,建议先用控制台跑通,再保存成文件,这样能把“代码没写对”和“路径放不对”这两个问题分开排查。
3.3 进阶:注册成菜单命令
如果想让自己的脚本像内置功能一样出现在菜单或工具栏里,就需要按照QCAD的插件写法注册。思路大体是这样:定义一个继承自EAction的动作类,重写相关生命周期方法,在初始化时通过registerAction注册命令,再用addPostListener挂载到启动流程中。下面是一个可以参照的骨架,具体到某些方法名和参数,要以你当前版本的官方示例为准。
include("scripts/Example.js"); function MyBatchAction() { EAction.call(this); } MyBatchAction.prototype = new EAction(); MyBatchAction.prototype.beginEvent = function(event) { EAction.prototype.beginEvent.call(this, event); batchMoveTextByContent(); this.terminate(); }; MyBatchAction.init = function() { var action = new MyBatchAction(); EAction.registerAction("batchMoveTextByContent", action); }; MyBatchAction.getMenuActions = function() { return []; }; addPostListener("MyBatchAction", MyBatchAction.init);这段骨架最大的作用是给读者一个印象:注册脚本没有想象中复杂,无非是把你的业务函数包到动作类里。实际上,QCAD安装目录自带的插件代码就是最好的参考手册,把官方的示例复制一份出来,替换自己的业务逻辑,比自己从零研究可靠得多。我以前有几次就是对着官方示例改的,效率极高,建议你也这么做。
3.4 调试脚本的几个习惯
调试QCAD脚本,最实用的习惯有几个。一个是多放qInfo打印,在循环内部、判断分支前后输出当前变量的值,能快速定位“哪一步没有按预期走”。另一个是做小范围验证,脚本刚写好时只选中两个实体先跑一次,别一上来就全图处理,出了问题也好恢复。还有一个被我念叨很多次的老话:批量改动前备份。哪怕脚本看起来再安全,出一次错就够你难受很久,备份是世界上成本最低的后悔药。
4. 常见问题与排查技巧实录
4.1 执行没反应,但也没有报错
脚本运行完没有报错,图形却一点变化都没有,这种情况多数是“事务没有提交”。确认一下代码里是否调用了applyOperation(op),如果只是创建了对象或修改了实体属性,但没有提交到文档,图形就不会有任何变化。另外一个常见原因是:函数定义了但没调用。脚本控制台粘贴代码后,只执行函数定义不会运行内部逻辑,必须在结尾写上batchMoveTextByContent()这样的调用语句才能触发执行。检查顺序通常是先看没调用,再看没提交,第三查没刷新。
4.2 中文匹配不上或控制台乱码
QCAD脚本文件如果保存的编码不是UTF-8,中文关键词很容易出现匹配不上的情况,控制台输出也可能乱码。解决办法很简单:保存脚本文件时选择UTF-8编码。如果你拿到的脚本是从旧项目里拷出来的,尤其要检查一下编码格式。还有一种比较保守的规避方式:在脚本调试阶段先把关键词换成英文,比如用"TITLE"代替"标题",验证整体流程没问题之后再改回中文。
4.3 API方法名或类型名对不上
QCAD不同版本之间,个别API方法名和常量类型名确实会有差异。遇到报错说“某某方法不存在”或者“某某常量未定义”,先别急着怀疑自己写错,去QCAD自带插件里搜一下类似的类名和调用方式。我多次靠这个办法解决了问题:官方示例代码就是活字典,直接搜某个类的关键词,看它怎么用,照搬过来往往就能跑通。需要注意QCAD对大小写敏感,getEntityIds和getEntityIDs可能就是不兼容的两个名字,复制代码时尽量保持和官方示例一致。
4.4 海量实体操作卡顿
图纸里有几千甚至上万个实体时,脚本运行时间会明显拉长。最有效的优化是减少事务提交次数。把创建和修改操作尽量塞进同一个RAddObjectsOperation,最后统一提交一次,不要循环一次就提交一次。还有一个经验是减少不必要的regenerate()调用,这种刷新操作很耗时,在最终提交后调用一次就够了。处理特别大的图纸时,还可以先备份原图,再用脚本在副本上做实验,避免卡住影响正图工作。
4.5 批量改动之后想反悔
因为整个脚本的改动都打包在一个事务里,撤销一次就能全部还原,这是批量脚本比较舒服的地方。但如果你在脚本执行后又做了其它操作,撤销顺序会叠加在一起,不一定能精确回到某个中间状态。所以最稳妥的方案仍然是执行前备份图纸。我自己就遇到过一种情况:脚本逻辑本身没有问题,但参数设错了,批量把一批文字移动到了错误图层,如果没有备份,恢复起来会非常麻烦。备份一下,几秒钟的事,能省下大量返工时间。
最后说点实际感受。这四个片段里,我平时用得最多的是统计线长那个脚本,因为做材料表几乎每周都要算长度,以前靠手动点属性面板,现在几秒钟出结果。踩过几次坑之后,我的体会是:QCAD脚本这条路,最怕的不是不会写,而是不敢写。先从这种几行的批量小工具练手,遇到搞不定的就翻官方示例,慢慢就能积累出自己的脚本库。东西不在于花哨,能解决手边的实际问题,就是好代码。这一篇先聊到这儿,后续要是有新的实用片段,我再继续补充。