
第一次打开 Papyrus 建模工程的目录很多人会愣一下明明只画了一张类图目录里却摊着好几个同名不同后缀的文件model.uml、model.di有时还夹着.notation。.uml大家多少能猜到是模型本身而.di这个后缀最容易被人当成临时文件、缓存文件甚至误以为删了也没事——后果是图直接打不开。这篇就来把 Papyrus 插件里的.di文件掰开揉碎讲一遍它到底存什么、XML 骨架长什么样、怎么用脚本和 API 把它读出来、插件里怎么正确地读写它以及我这些年在这个文件上踩过的坑。做 Papyrus 插件开发的、维护建模规范的、需要把模型图批量导出或做布局质检的都能从里面直接抄到能跑的东西只画图不写插件的同学看完也能明白为什么你的 Git 提交历史里总有一堆看不懂的坐标数字。1. 从工程目录说起.di 到底是个什么文件1.1 语义与视图分离三份文件各管一摊Papyrus 沿用的是经典建模工具的语义与视图分离思路一个模型工程里的核心文件通常是这样分工的.uml存语义模型也就是包、类、接口、属性、操作、关联、泛化这些 UML 元模型对象的 XMI 序列化结果它回答的是模型里有什么.di存图定义回答的是这些元素在哪张图上、摆在什么坐标、画成什么样而.notation是 GMF 那套 Notation 元模型的默认扩展名Papyrus 用自己的.di扩展名承载同一套元模型内容你在别的 GMF 工具里见到的.notation本质上和它是同一类文件。这个分离设计不是 Papyrus 独有的洁癖而是有实打实的收益。同一个Order类可以同时出现在类图、包图、状态机图引用的上下文里如果布局信息塞进语义模型一份元素就要被复制三遍改个属性名得同步改三处。分离之后语义元素只存一份每个图各自记录自己的坐标和样式语义模型还可以被代码生成器、文档工具直接读取完全不用管图形信息。反过来说这也意味着.di文件删掉不会丢模型但图会彻底丢掉——重新建图时所有人工调整过的排版全部归零这在实际项目里往往比丢代码还让人难受。我一般会跟新人这么解释.uml是内容.di是排版稿.notation是同一套排版稿换个封皮。内容可以单独给别人看排版稿只对画图的人有意义。1.2 .di 里有的和没有的搞清楚有什么之前先搞清楚没有什么更省事。.di里不存元素的名字、类型、可见性、属性列表、操作签名、关联两端的多重性——这些全在.uml里。你在类图的框子上看到Order这三个字母.di里根本没有这个字符串它只有一行elementmodel.uml#_aBc123的引用。所以你把类名从Order改成PurchaseOrder.di文件一个字节都不会变这就是为什么模型重命名在版本控制里只产生一条.uml的 diff。.di里存的是这些图的根对象哪张图、什么类型、坐标单位、指向哪个语义根、图上的每一个视图对象形状节点、连线、标签装饰节点、每个视图的位置尺寸、连线的拐点、字体与线条与填充等样式、锚点归属、以及一类特殊的规范化标记决定这个图元是否随语义模型自动增删同步。还有一类容易忽略的信息视图的类型编码它是一串数字字符串对应图类型定义里的 visual ID同一个数字在不同图种下未必是同一个含义所以你不要指望记住2001 就是类那不靠谱。理解了这一点很多行为就顺了为什么删掉模型里的一个类图上的框子有时还在视图成了孤儿为什么两个人分别改语义和改布局能同时提交为什么.di的 diff 里全是数字看不懂——因为它从头到尾就只讲位置和外观。1.3 为什么要费劲读它大部分建模的人一辈子不需要打开.di但下面这几类需求绕不开它。第一类是布局质检几十张图、上千个元素靠人眼看根本查不出重叠、越界、连线乱穿而这些都是纯几何数据完全可以脚本化算出来。第二类是批量排版项目交接或者规范统一时要把所有图的字体字号、线条粗细、填充色拉齐手动改几十张图是灾难。第三类是模型迁移和审计找出那些画在图上但语义已经不存在的孤儿视图或者反过来找出语义模型里有但没画到任何图上的元素做覆盖率统计。第四类是导出与集成把模型图渲染成图片塞进文档流水线或者把布局信息喂给别的可视化工具。这几类需求的共同点是它们都不关心模型对不对只关心图长什么样。这正是.di的主场。2. 拆开 XML.di 的骨架长什么样2.1 根节点与几个关键属性.di就是个 XML 文件根元素是一个 Notation 元模型里的Diagram对象。它开头一般是这样命名空间声明那几行由工具自动写入手写时直接复制现有文件头就行不要自己拼?xml version1.0 encodingUTF-8? notation:Diagram xmi:version2.0 xmlns:xmi... xmlns:notation... typePapyrusUMLClassDiagram nameMain Class Diagram measurementUnitPixel elementmodel.uml#_semanticRootId这几个属性值得逐个说。type决定这张图交给哪套元素映射器去渲染它不是一个抽象的名字而是和插件里注册的图类型一一对应的标识符改错了图会打不开或者打开成空白画布。name是图的显示名跟文件名无关你在模型树里看到的图节点名就来自这里。measurementUnit是坐标单位绝大多数情况是Pixel少数为打印或高精度排版准备的场景会用Himetric也就是 1/100 毫米这个值一旦被改同样的坐标数字渲染出来尺寸完全不同是排查图突然变大变小的第一嫌疑点。element指向语义模型的根用的是资源路径#元素ID的引用语法注意#后面那串是.uml里对应元素的xmi:id不是名字。顺带说一个观察不同 Papyrus 版本、不同图类型导出的.di属性集合会有差异多出一些自定义命名空间的属性很正常。所以写解析脚本时不要用属性必须恰好是这几个的强校验用取我需要的其余忽略的宽松策略兼容性会好很多。2.2 元素树View、Node、Connector、DecorationNodeDiagram下面挂的是一棵树节点都是View的子类型。最外层通常是children里面装的是形状节点Shape连线不走children而是单独放在edges里类型是Connector。还有一类第三种东西叫DecorationNode它是挂在形状下面的附属图元比如类框上的名字标签、 stereotype 标签、图标它自己没有独立的语义element通常继承父节点或者为空。一个典型形状节点长这样children xmi:typenotation:Shape xmi:id_n1 type2001 elementmodel.uml#_c1 layoutConstraint xmi:typenotation:Bounds x120 y80 width160 height90/ children xmi:typenotation:DecorationNode xmi:id_n1_name type5001 layoutConstraint xmi:typenotation:Location x8 y4/ /children styles xmi:typenotation:FontStyle fontNameSans fontHeight9/ /children连线则是这个结构edges xmi:typenotation:Connector xmi:id_e1 type4001 elementmodel.uml#_rel1 source_n1 target_n2 bendpoints xmi:typenotation:RelativeBendpoints points[0,0,-40,60]$[40,-60,0,0]/ styles xmi:typenotation:LineStyle lineColor8421504 lineWidth1 routingRectilinear/ /edges这里最关键的是xmi:id、source、target三者的关系。xmi:id是这个文件内部的唯一标识source和target引用的就是形状节点的xmi:id而不是语义元素 ID。这一点新手极容易搞混连线的两端连的是视图不是模型元素。所以你想知道一条连线两端是什么类得先顺着source找到形状节点再读形状节点的element才能定位到.uml里的元素中间隔了一层。bendpoints里的points是一串用$分隔、方括号包裹的坐标组本质上是相对于源端与目标端锚点的偏移量。用相对值而不是绝对值好处是图元整体平移时连线不用重算拖动一组元素不会把线扯乱。2.3 layoutConstraint 与 styles几何与外观布局约束统一挂在layoutConstraint上最常见的三种Bounds有 x/y/width/height用于形状Location只有 x/y用于标签和图标这类不需要独立尺寸的图元Ratio用于需要按比例定位的元素。要注意Bounds的坐标是相对父图元的顶层形状相对画布嵌套在容器比如一个大包框里的形状相对容器所以算绝对位置必须从根往下累加只读一层会算错。样式是一组并列的styles元素常见的有字体样式字体名、字号、粗体斜体、线条样式颜色、线宽、路由方式路由方式决定连线走直线、直角还是自动避让、填充样式填充色、透明度。颜色是整数注意不是 RGB 打包的那种直觉顺序实际是0xRRGGBB的十进制表示比如8421504换成十六进制是0x808080灰色。算的时候直接hex(8421504)最快别心算。还有一类 Papyrus 特有的样式叫规范化样式它标记这个图元是否随语义模型自动同步打开时语义模型里新增的元素会自动出现在图上删除的会自动消失。这个特性省事也惹事后面排查章节会专门讲。3. 动手解析把 .di 变成可读的布局清单3.1 用 Python 快速摸清结构不想开 IDE、不想配插件环境的时候一个几十行的 Python 脚本就能把.di摸个底朝天。要点是不依赖命名空间前缀因为不同版本的前缀可能不同用标签名的本地部分匹配最稳import xml.etree.ElementTree as ET def local(tag): return tag.split(})[-1] def attr(elem, name): for k, v in elem.attrib.items(): if k name or k.endswith(} name): return v return None def walk(elem, depth0): t attr(elem, type) if t: print( * depth, local(elem.tag), type, t, id, attr(elem, id)) for child in elem: if local(child.tag) in (children, edges, styles, layoutConstraint, bendpoints, element): walk(child, depth 1) tree ET.parse(model.di) root tree.getroot() print(diagram:, attr(root, name), attr(root, type), attr(root, measurementUnit)) walk(root)跑一遍就能看清这张图挂了哪些图元、什么类型编码、层级什么样。这一步别小看很多人上来就写复杂的遍历逻辑结果被DecorationNode混在children里搞乱计数——先看清楚结构再谈提取。3.2 提取形状与连线的完整脚本真正有用的是把几何信息导成表格。下面这段输出视图 ID、类型、x、y、宽、高、指向的语义元素直接可以喂给 Excel 或者做后续分析import csv import xml.etree.ElementTree as ET def local(tag): return tag.split(})[-1] def attr(elem, name): for k, v in elem.attrib.items(): if k name or k.endswith(} name): return v return None def bounds_of(node): for c in node: if local(c.tag) layoutConstraint and Bounds in (attr(c, type) or ): return (attr(c, x), attr(c, y), attr(c, width), attr(c, height)) return (None, None, None, None) rows [] tree ET.parse(model.di) for node in tree.iter(): if local(node.tag) ! children: continue if Shape not in (attr(node, type) or ) and attr(node, type) is None: continue x, y, w, h bounds_of(node) if x is None: continue rows.append([attr(node, id), attr(node, type), x, y, w, h, attr(node, element)]) with open(layout.csv, w, newline, encodingutf-8) as f: csv.writer(f).writerows(rows) print(shapes:, len(rows))拿到这份表之后重叠检测就是两两比矩形相交越界检测就是比 xw 和画布宽度孤立元素检测就是拿形状的id去和所有连线的source/target求差集。这三个指标凑在一起一张布局体检报告就出来了评审会上拿数据说话比我觉得这里有点挤有说服力得多。3.3 字段对照表写脚本时最容易卡住的地方是不知道该取哪个属性这里整理一张对照表按我实际用到的频次排序字段所在元素含义典型值xmi:id任意 View文件内唯一标识供 source/target 引用_aB3dEftypeView图元类型编码与图类型定义对应2001 / 4001 / 5001elementView指向语义元素model.uml#_c1layoutConstraintShape位置与尺寸Bounds(120,80,160,90)source / targetConnector引用两端形状的 xmi:id_n1 / _n2bendpointsConnector折线拐点相对锚点偏移[0,0,-40,60]stylesView字体、线条、填充外观FontStyle / LineStylemeasurementUnitDiagram坐标单位Pixel / HimetricvisibleView是否显示true / false注意最后一行的visible被隐藏的图元仍然在文件里坐标齐全只是没渲染。做覆盖率统计时如果把它算进去得出元素都画上了的结论是假的——这是我第一次做模型审计时栽的跟头报告交上去被一眼看穿。4. 插件里怎么正经地读写 .di4.1 EMF 资源加载的标准姿势脚本解析适合做离线分析插件开发就得走 EMF 那套资源加载因为你需要的是活的对象模型而不是字典。核心是两件事把.di的扩展名注册到 XMI 资源工厂把 Notation 和 UML 的元模型包注册进包注册表。顺序错了或者漏了注册加载出来的对象全是裸的EObject强转直接抛类型转换异常。ResourceSet rs new ResourceSetImpl(); rs.getResourceFactoryRegistry().getExtensionToFactoryMap() .put(uml, new UMLResourceFactoryImpl()); rs.getResourceFactoryRegistry().getExtensionToFactoryMap() .put(di, new XMIResourceFactoryImpl()); // 关键语义模型包和图示模型包都要注册 rs.getPackageRegistry().put(UMLPackage.eNS_URI, UMLPackage.eINSTANCE); rs.getPackageRegistry().put(NotationPackage.eNS_URI, NotationPackage.eINSTANCE); Resource diRes rs.getResource(URI.createFileURI(diPath), true); Diagram diagram (Diagram) diRes.getContents().get(0); System.out.println(diagram.getType() / diagram.getName());这里有三个隐性前提必须满足否则你会在后面被奇怪的空指针折磨。第一.di和.uml必须能被同一个ResourceSet找到因为element引用是跨文件解析的只加载.di的话引用解析不出来。第二加载.di时要用true触发按需加载但语义模型往往是被引用时才加载所以在做任何遍历之前最好显式把语义资源也getResource一次。第三插件运行时环境里必须已经注册了对应图类型的扩展点否则Diagram对象能读出来但图的渲染器找不到这类错误在纯 headless 单元测试里反而不会出现。4.2 遍历与提取Node / Connector / 语义元素拿到Diagram之后用eAllContents一把梭遍历是最省事的但要注意它会连styles、bendpoints这种非视图对象一起遍历出来必须做类型过滤。我一般用两层判断先判instanceof Node处理形状再判instanceof Connector处理连线剩下的一律跳过。TreeIteratorEObject it diagram.eAllContents(); while (it.hasNext()) { EObject o it.next(); if (o instanceof Node) { Node node (Node) o; EObject semantic node.getElement(); // 这是关键 Bounds b (Bounds) node.getLayoutConstraint(); System.out.println(semantic b.getX() , b.getY()); } else if (o instanceof Connector) { Connector c (Connector) o; View src c.getSource(); View tgt c.getTarget(); // 连线两端是 View要再跳一层到 element } }getElement()返回的对象状态有三种务必区分清楚正常返回真实语义元素返回null说明这个视图本来就没有语义绑定比如纯装饰返回一个eIsProxy()为真的代理对象说明引用断了。第三种最常见也最坑toString()出来是一串href文本不判一下直接拿去调getName()就是空指针。我的习惯是写一个resolve()小工具把 proxy 情况集中处理别在每个调用点重复写判断。连线的处理要多跳一层Connector.getSource()拿到的是View不是语义元素得再getElement()。很多人第一次写这里会写出连线两端是类的错误认知然后在跨图引用上翻车。4.3 改布局什么能动、什么不能动读和写是两回事。读的时候随便你怎么遍历写的时候必须守规矩否则轻则撤销栈错乱重则文件损坏。必须走事务的场景任何结构调整包括新增或删除视图、改变元素的父容器、修改element引用。这些操作会触发命令栈、监听器、规范化同步一整套机制绕过去直接eSet是自找麻烦。写法上用编辑域的事务包裹TransactionalEditingDomain domain ...; domain.getCommandStack().execute(new RecordingCommand(domain) { Override protected void doExecute() { node.setElement(newSemantic); node.setVisible(true); } }); diRes.save(null);可以手改 HTML/XML 的场景其实也有但要严格限定在纯数值、不改结构的范围内统一把所有Bounds的宽高按比例放大、把所有FontStyle的字号从 9 改成 10、把某类图元的visible批量置 false。这类改动的特点是元素数量不变、引用关系不变改完重新打开不会触发任何重建逻辑。我做过一次把三十多张图的字号统一的事就是脚本直接改 XML改完逐张打开验证全部正常比开图改省了两个小时。但是一定要先备份且改之前先确认这些图没有开启强制规范化否则打开的一瞬间自动重排你的改动等于白做。注意不要去改xmi:id。哪怕你觉得它长得丑想整理一下也绝对不要动。这个 ID 被source、target、element各种地方引用改一个漏一处就是断链而且断链报错的位置和真正的原因隔得很远排查成本极高。5. 踩过的坑与排查速查5.1 图能打开但元素错位或丢失症状是图能双击打开画布也在但元素全挤在左上角或者一部分框子根本看不见。最常见的根因是Bounds缺失或者父容器坐标没算对。前面说过坐标是相对的嵌套容器的子元素如果父容器的Bounds被删了子元素就全部退回 0,0 叠加在一起看起来就是全挤在左上角。排查方法很简单脚本导一遍 CSV按父层分组看坐标分布如果一整组的坐标全是 0基本可以锁定。另一个常见原因是visible被设成了 false。有时候是自动化脚本为了生成简化版视图批量隐藏了元素然后这个改动被提交上去了。这种看不见但存在的元素在模型评审时是灾难因为大家会以为它不在模型里。还有一种更隐蔽的type编码和图类型不匹配。比如把一张类图的内容复制到组件图的.di里坐标都在但渲染器不认识那些编码画出来是空的或者只画出外框。这种情况只能靠对比两张图的type值来定位。5.2 引用断了Proxy 与 href.di里所有指向语义元素的引用都是资源路径#元素ID的形式路径可以是相对的model.uml#_c1也可以是跨工程绝对路径。断链的原因无非三种.uml文件被改名或移动语义元素被彻底删除不只是从图上移除跨模型引用的工程目录结构调整了。排查思路是先把所有href抓出来做一遍存在性校验本地路径的检查目标文件在不在、目标 ID 在不在绝对路径的检查路径是否还能解析。这一步用 Python 十几行就能跑完比在 IDE 里一张张点开快得多。校验出来的孤儿视图有两种处理方式删掉视图语义已经没了留着没意义或者重新绑定到一个新元素上如果是重命名导致的。前者简单后者要小心引用完整性。提示如果只是从图上删掉一个元素但保留语义操作上应该是删除视图而不是删除模型元素。这两个动作在界面上挨得很近误操作概率不低。删完之后.di变小、.uml不变说明删对了两个都变小说明把模型删了。5.3 版本升级与合并冲突.di是个尴尬的存在它逻辑上是源文件手工排版的结果都在里面形态上又像生成物一堆坐标数字和自增 ID。这导致它在版本控制里的表现很难看——挪动一个框子diff 里可能出现几十行变化因为坐标变了会连带影响相对偏移和标签位置。我的处理经验有这么几条。第一布局相关的提交和语义相关的提交分开别混在一次提交里否则 review 的人根本分不清哪些改动是有意的。第二排版工作尽量串行同一张图不要两个人同时调这是暴力的办法但最有效。第三在.gitattributes里把.di标记成不适合自动合并的类型宁可冲突时人工挑一个版本重新排也不要让工具给你合并出一个坐标打架的中间态。第四升级 Papyrus 大版本前把所有图先打开一遍存一次让新版本把文件格式规整到统一形态再提交这样后续 diff 会干净很多。升级本身也有坑新版本可能引入新的样式节点或者改变measurementUnit的默认值老文件打开后虽然显示正常但保存一次就会写入大量新内容。所以升级后的第一次提交建议单独做方便出问题时回退。5.4 排查速查表把上面这些症状和处理方式整理成一张表出问题的时候按行对号能省掉不少瞎试的时间现象可能的根因快速验证方式处理办法双击图打不开图类型扩展点未加载或type值非法看日志里有没有图类型解析失败确认插件环境比对type值打开是空白画布根element引用断裂检查element指向的.uml是否存在修复引用或重建图元素全挤在左上角Bounds缺失或父容器坐标丢失导出 CSV 看坐标分布补Bounds或重新自动排版元素看不见但确实存在visible为 false搜visiblefalse批量置 true元素显示异常/问号语义元素已删除视图成孤儿校验所有href删除视图或重新绑定连线两端错乱手工改过source/target比对引用的xmi:id是否存在按语义关系重建连线打开后布局被重排规范化样式触发了同步检查是否有自动同步标记关闭该图元的自动同步合并后图错乱坐标被自动合并成中间态看 diff 是否两套坐标并存回退重排或标记为不自动合并最后分享一个小习惯凡是要对.di做批量操作我都会先把整个模型工程打个压缩包放在旁边。这个文件的特点是坏了不一定马上发现可能几周后打开某张图才发现布局全乱了那时候再想回退版本库里已经隔了几十个提交。留个快照成本几秒钟救急的时候是真管用。