首页 / 资讯中心 / 文章详情

AutoLISP用户输入处理全攻略:从getpoint到防御性编程

AutoLISP用户输入处理全攻略:从getpoint到防御性编程 ★ FEATURED ARTICLE
搞CAD二次开发的人都知道一个插件写到最后最难伺候的不是算法、不是图层管理而是autoCAD 提示用户输入这一步。你辛辛苦苦把逻辑写好了结果用户在手型光标下乱点两下、按个ESC、或者在命令行里输了个负数程序直接崩了连错在哪都看不出来。这篇文章就来聊聊在AutoLISP里怎么把提示用户输入这件事做扎实——从最常用的getpoint、getint、getstring到initget、error这些防御手段再配合一个完整案例和实测坑位帮你把这块短板补上。适合刚开始写LISP的人也给写过一阵子但总觉得交互不顺手的人做参考。1. 先说清楚用户输入提示在CAD开发里到底扮演什么角色1.1 从一次翻车说起去年我做了一个批量布置图块的小工具功能不复杂用户先选一个基点输入行数、列数和间距程序就把选中的图块复制成阵列。逻辑写起来一小时搞定结果测试的时候直接傻眼。图纸里全是道路标记块我请一个同事帮忙试他在命令行弹出来之后对着动态输入框输了一个500,500然后程序自作主张地把这个点当成了相对上一基点的坐标阵列的位置全乱了几百个块铺得整个图纸外面都是。说实话那会儿我第一反应是CAD真难用后来冷静下来才明白问题不在CAD而在我的程序没有把用户到底会被什么样的输入方式影响这件事想清楚。所谓提示用户输入并不是在命令行里打一句话那么简单它是一整套交互通道的设计提示语的写法、输入函数的选型、返回值的含义、用户的习惯性操作哪一环掉链子都会出问题。1.2 输入交互的底层机制命令行是唯一中立语言AutoCAD里有好几条交互路线点功能区按钮、点菜单、点工具栏、在命令行打字、在动态输入框里填数。但对一个AutoLISP程序来说它能主动控制、能稳定捕获的只有命令行和动态输入这一路。你写的getpoint、getint之类的函数本质上是往命令行缓冲区里写一段提示字符串然后程序挂起等待用户用鼠标点一下、或者用键盘敲一个值、或者干脆按个空格和回车。理解这个机制很重要。它决定了三件事提示语里最好直接告诉用户能输入什么、默认是什么否则用户在命令行前面愣住体验很差。getxxx系列函数接受的是命令行的值它不负责校验业务逻辑只负责把输入转成LISP能用的数据。用户的按ESC、敲错键、输一半又取消这些行为最终都会让getxxx返回nil而不是抛异常。所以与其说提示用户输入是等待答案不如说它是程序与用户之间一次有防御的对话。下面我就把常用的输入函数一个一个拆开讲。2. 输入函数选型不同场景该用哪个2.1 点类输入getpoint与getcorner的取舍最常用的是getpoint它的作用就是等待用户点一个点或者输入一个坐标返回一个三维点表格式是(x y z)如果用户有Z轴标高会带上z值。基本用法(setq pt (getpoint \n请指定插入点))如果用户在命令行输入了100,200返回的就是(100.0 200.0 0.0)。如果用户用鼠标在屏幕上点返回的就是当前UCS下的坐标。getpoint还支持第二参数作为基点这时候屏幕上会出现一条从基点连到光标的橡皮线配合提示语用户能很直观地看到我要从这个点拉到那个点。比如(setq base (getpoint \n指定第一个角点)) (setq other (getpoint base \n指定对角点))getcorner则是框选专用它需要一个基准点作为第一角然后用户拖动鼠标拉出矩形返回的是矩形对角点。亮点是拖动过程中能看到实时的矩形框选范围的时候比getpoint直观得多(setq base (getpoint \n指定基点)) (setq corner (getcorner base \n指定对角点))选型逻辑很简单只要是想让用户拉一个范围优先getcorner只是单纯取一个点用getpoint。但有个细节我踩过坑getcorner的两个角点第二个角点的x和y不一定比第一个大它完全取决于用户怎么拖所以后面计算宽度高度时别偷懒直接相减记得用abs处理。2.2 数值与距离输入getint、getreal与getdistgetint返回整数getreal返回实数这两兄弟一个管整数一个管小数。要说的是如果你让用户输入行数、列数、数量这种只能数整的东西用getint如果是比例系数、面积、材料厚度这种可能带小数用getreal。但真正干活的时候我更推荐getdist。这函数表面上是获取距离但它有个很人性化的特性既允许用户直接敲数字也允许用户在屏幕上点两个点让程序量取实际距离。举个例子你要让用户输入一个排距用getreal的话用户只能老老实实敲3200但用getdist用户可以直接在图纸上点两点程序自动算出距离省去查图的时间。(setq dist_val (getdist \n指定间距))这里有个经典坑getdist输入3,4的时候AutoCAD会把它当成一个二维坐标点然后返回的是从原点到这个点的距离而不是3或者4。如果你压根没想让用户用坐标形式输入距离最好在提示语里写一句请输入数值或点取两点。别嫌啰嗦一句话能省掉大量售后。同样的逻辑也适用于getangle它是获取角度但用户既可以输入一个数值也可以在屏幕上点两个点程序会返回第一个点到第二个点的方向夹角。跟getdist是同一个套路。2.3 字符串与角度输入getstring、getangle的细节getstring是获取文字输入的但它有个极其容易踩坑的签名(getstring \n请输入名称) ; 不支持空格 (getstring T \n请输入名称) ; 支持空格第一个参数如果不传或传nil用户输入的文字遇到空格就截断了比如用户明明输入的是道路 中线得到的却是道路。我之前写标注程序时想让用户输入图框编号结果所有编号里带空格的都被砍掉一半查了半天才想起来是getstring没加T。规则记死允许空格就传T。getangle和getorient的区别也值得一提。getangle返回的是相对当前角度基准ANGBASE系统变量的角度getorient返回的是相对正东方向的角度。如果你的程序要计算零件旋转了多少度用getorient更靠谱因为它不受绘图环境的ANGBASE影响结果恒定。别问我怎么知道的有一版程序在某个图纸里突然全部旋转量差了90度查完系统变量才发现是图档的ANGBASE被改过。下面这张表可以对应着记函数返回类型适用场景一句话提醒getpoint点表取一个点返回的是UCS坐标getcorner点表框选范围配合基点使用getint整数行数、列数、数量不接受小数getreal实数系数、厚度、比例可以带小数getdist实数距离、间距、长度支持点两点量距离getangle弧度旋转角度受ANGBASE影响getorient弧度绝对旋转方向固定正东为0度getstring字符串名称、编号允许空格必须传Tgetkword关键字选项分支配合initget使用3. 从提示到校验让程序扛得住乱输入3.1 initget给用户输入提前戴上紧箍咒getxxx系列函数看起来是一个人畜无害的等待输入实际上它把所有校验压力都丢给了后面的代码。你不拦截用户按个回车、输个0、输个负数程序照样往下跑。这时候就轮到initget出场了。initget的作用是在下一个输入函数执行之前给输入提前设好规定比如不允许空输入、不允许0、不允许负数。位码规则常用这几个1不允许直接回车空输入2不允许输入04不允许输入负数8不允许外部边界32画虚线拖引线这几个位码可以相加组合。比如想禁止空、0、负数就写(initget 7)原理是1247。看起来像魔法数字但这是AutoLISP的老传统记习惯就好。一个实际例子(initget 7) (setq rows (getint \n请输入行数))用户要是敢直接回车、输0、输-3系统直接报无效选项并且重新提示根本走不到你的业务逻辑。但要注意initget只对它后面的那一个输入函数有效用完即焚别指望设一次管三个输入。每次输入前都得重新initget。initget还有一个压箱底的能力指定关键字。配合getkword使用可以实现多选题式的交互(initget A B) (setq mode (getkword \n选择布置方式 [自动(A)/手动(B)] A))用户输入A或Bgetkword返回大写关键字字符串如果用户直接回车返回nil你再自己给默认值。这种带选项的提示比单纯让用户输入数字舒服得多因为用户不用记代码看提示就知道该怎么选。3.2error错误陷阱程序不会因为用户按了ESC就崩这是很多LISP教程不爱讲、但实际开发必须面对的残酷现实用户从来不会按你设计的剧本来操作。你以为他会老老实实输入结果他可能中途觉得这个工具没意义直接按ESC。然后呢你的程序可能已经改了一堆系统变量没恢复后面所有命令在环境异常的情况下运行画出来全是错的。AutoLISP提供error这个系统函数作为错误入口当程序出问题或者用户按ESC的时候它会自动被调用。你可以重新定义error在里面做收尾工作。我在写工具时有个固定习惯程序开头先保存系统变量然后重定义error在error里恢复系统变量、把捕捉关闭、退出UNDO标记。大概长这样(defun c:TT (/ old_os old_echo) (setq old_os (getvar OSMODE) old_echo (getvar CMDECHO)) (defun *error* (msg) (setvar OSMODE old_os) (setvar CMDECHO old_echo) (princ \nTT 命令已退出) (princ) ) (setvar CMDECHO 0) ;; 业务代码 (princ) )这句话值五毛error里面千万不能再调用可能出错的函数也别再做复杂业务逻辑否则会在错误处理里递归报错。老实的做法就是恢复环境变量、打印一句提示然后退出。关于用户的取消还有一个细节getxxx在用户按ESC时会返回nil但AutoLISP会把这种取消当成一个错误抛出进入error。所以你在error里不要写用户一定犯了什么错这种话它就是一次正常取消。3.3 完整示例做一个安全的批量布置工具光讲函数不清不痒我把前面这些串成一个能直接跑的示例批量复制选中的对象按用户输入的基地点、行数、列数、间距生成矩形阵列。这个工具我用来布置设备块代码不长但五脏俱全。(defun c:BLKARR ( / old_os old_echo base rows cols gap_x gap_y pt i j ename) (setq old_os (getvar OSMODE) old_echo (getvar CMDECHO)) (defun *error* (msg) (setvar OSMODE old_os) (setvar CMDECHO old_echo) (princ \nBLKARR 已取消) (princ) ) (setvar CMDECHO 0) (setq ename (car (entsel \n选择要阵列的对象))) (if (null ename) (progn (princ \n未选择对象程序退出) (exit)) ) (setq base (getpoint \n指定阵列基点)) (if (null base) (exit)) (initget 7) (setq rows (getint \n输入行数)) (if (null rows) (exit)) (initget 7) (setq cols (getint \n输入列数)) (if (null cols) (exit)) (initget 6) (setq gap_x (getdist base \n输入列间距)) (if (null gap_x) (exit)) (initget 6) (setq gap_y (getdist base \n输入行间距)) (if (null gap_y) (exit)) (setq i 0) (while ( i rows) (setq j 0) (while ( j cols) (if (not (and ( i 0) ( j 0))) (command _.COPY ename base (list ( (car base) (* j gap_x)) ( (cadr base) (* i gap_y)) 0.0)) ) (setq j (1 j)) ) (setq i (1 i)) ) (setvar OSMODE old_os) (setvar CMDECHO old_echo) (princ \n阵列完成) (princ) )注意中间gap_x和gap_y我用了(initget 6)意思是禁止0和负数。间距为0的阵列没有任何意义这个拦截帮我在源头上杜绝了用户输了个0然后几千个对象叠在一起的惨案。还有一点是entsel的返回它是(图元名 . 点)的关联表所以要加个(car ...)取图元名。数组起点是(0,0)循环的时候跳过第一个对象不然会在原位置多复制一份。4. 实测中最容易踩的坑4.1 动态输入与getdist的相对距离幻觉动态输入DYNMODE是CAD给新手准备的便利功能但对LISP程序来说它有时会变成坑。拿getdist举例你提示输入列间距用户在屏幕上看到动态输入框直接输入3000看起来一切正常。但动态输入开启时这个数值可以被解释为相对上一点的位移也可能被解释为绝对距离取决于用户的输入习惯和当前的输入模式。我自己实测过一遍开着动态输入调用getdist时输入3000有些情况下程序返回的是3000有些情况下会带出偏移导致结果异常。这跟版本、变量设置都有关系不展开讲太深但结论很明确——凡是涉及距离、坐标的插件在提示语里明确写一句请使用命令行输入数字是有必要的。别怕这句提示影响体验真出错了更影响信任。4.2 拿到UCS坐标就用trans函数不能忘getpoint返回的是当前UCS下的坐标但很多内置命令比如CIRCLE、INSERT期望的是WCS坐标。当你把UCS旋转过之后getpoint拿到的点直接传给command画出来的东西就会原地跑偏。我建议所有从getpoint拿到的点在进入业务计算之前统一做一次转换(setq wcs_pt (trans pt 1 0))trans的三个参数第一个是要转换的点第二个是原坐标系代码1代表UCS0代表WCS第三个是目标坐标系代码。这样无论用户当前UCS怎么转程序拿到的始终是统一坐标系下的点。这个习惯养成后各种点不对位的问题会少很多。4.3 空输入与负值的处理别相信任何一个用户我把用户行为分成了四类每一类都需要防御直接回车getxxx返回nil必须判断。输入负数或0getint、getreal照单全收必须initget拦截。输入非数字字符getint会自动报错并重新提示这个倒不用太操心。中途按ESC触发error必须在error里恢复环境。有一种情况特别隐蔽用户先输入了几个字符又删掉最后回车。getstring拿到的是空字符串这个不是nil你用(if (null ...))判断不出来得用(if ( str ) ...)再拦一道。4.4 连续输入时的缓冲残留连续调用两个getstring时偶尔会遇到上一次的回车被下一次读取直接消费的现象。有一次我让用户先输入图号再输入说明文字测试时发现第二个提示刚弹出来还没等用户敲键盘程序就像被按了回车一样跳过去了。排查了一下是上一个getstring没加T截断了输入之后把剩余字符和换行留在了输入缓冲区下一个getstring读到就当作回车处理了。解决方案有两个要么所有字符串都用(getstring T ...)确保整个逻辑清晰要么在每次getstring之前加一个(princ \n)刷新提示行。我更推荐前者因为T模式行为稳定不容易被半截输入干扰。5. 进阶组合玩法5.1 用关键字实现带默认值的提示很多用户希望提示里带默认值直接回车就采用。getxxx本身不提供默认值参数但可以自己实现先用initget给一个关键字然后getkword判断如果返回nil说明用户直接回车了就设置成默认值。(initget 10 20 50) (setq val (getkword \n输入间距 [10/20/50] 10)) (if (null val) (setq val 10)) (setq val (atoi val))这样提示语直接列出了合法的几个选项用户既可以直接回车选默认值也可以输入关键字完全不依赖记忆。我做过的小工具里凡是参数可选项少于等于4个的都用这个模式。5.2 与命令前顶点选联动有时候程序要先让用户选一批对象再进入参数输入。这种情况下ssget比entsel更适合而且ssget可以在提示里呼出栏选窗选多边形选等选项。配合前面说的getpoint整个流程可以做到选对象、点基点、输参数一气呵成。有一点值得注意ssget执行时用户按ESC同样会返回nil而且它不会触发error至少表现不同所以判断返回值时不能偷懒。(setq ss (ssget ((0 . INSERT)))) (if (null ss) (progn (princ \n未选择对象程序退出) (exit)) )过滤器((0 . INSERT))表示只允许选图块其他对象点不中。这样用户的误操作会在选择阶段就被挡住从源头上减少后面流程的异常。5.3 用grread做拖拽预览grread是AutoLISP里读取用户原始设备输入的函数比getxxx底层得多。它可以实时获取光标位置和键盘按键配合它可以实现鼠标移动时预览矩形大小这种动态效果。虽然写起来比getxxx复杂但效果是真的唬人展示给你的同事看他们会觉得这工具是专业级的。思路很简单循环调用(grread T 1 0)判断返回值的第一个元素是5表示鼠标移动还是3表示点击是5就把光标点转成坐标点重画预览图形是3就该干嘛干嘛。代码不展开了但有一个原则预览图形的更新别直接用command画实体那样会产生大量垃圾实体用grvecs或者redraw临时显示更干净。结尾再分享一个小技巧写LISP这些年我最深的体会是一个工具的可靠性不取决于它功能多花哨而取决于它怎么处理那些用户不按套路出牌的时刻。尤其是提示用户输入这一步你花在initget、error和坐标转换上的时间永远比花在业务算法上的时间值钱得多。最后送大家一个我一直在用的小习惯所有命令的提示语里都把合法的输入范围或者默认值写进去比如请输入行数正整数默认3。这既是写给用户看的也是写给自己以后维护看的。等哪天真有个插件半年没人动你重新翻代码看提示语一眼就能想起当初的设计意图。就冲这一点提示语写清楚值了。
阅读完成 · 觉得有帮助?
咨询建站