前阵子接手一套老业务系统的报表,模板文件清一色是 Word 2003 XML 模板,后缀是 .xml,内部大量标记都是 <w:wordDocument>、<w:pict>、<v:shape>。第一次打开时我愣了半天:这到底是 HTML 里的 vector 图形,还是 Word 的绘图?等真正把 WordprocessingML 和 VML 形状的对应关系理清楚后才发现,这类模板里的 VML 常用属性就那么几组,搞清楚之后改起来其实很快。
这篇东西我不打算写成 API 文档,而是把它当作一个“从 XML 模板里认 VML 形状”的实操记录来写。如果你最近也在维护老 office 系统的模板,或者需要批量做 Word 2003 XML 模板的数据回填、图片替换,又或者是被 This XML file does not appear to have any style information 这类提示折腾过,那这篇内容应该能帮你省不少时间。
1. WordprocessingML 和 VML 纠缠了二十年,模板里真正在说什么
1.1 一篇 Word 2003 XML 和 docx 不是同一个“门”
很多人一听到“Word XML”,第一反应是 .docx 那种 Office Open XML。但 Word 2003 XML 模板用的是另一套东西,正式名称叫 WordprocessingML,根节点是 <w:wordDocument>,命名空间是 http://schemas.microsoft.com/office/word/2003/wordml。它和我们熟悉的 docx 虽然都是 XML,但 schema 完全不同、结构也不同,如果你拿处理 docx 的思路去处理 Word 2003 XML,很容易在文档结构上栽跟头。
更麻烦的是图形部分。Office 在 Word 2000 到 Word 2003 这段时期,推荐使用的矢量图形语言是 VML,也就是 Vector Markup Language。VML 在浏览器端早就被 HTML5 Canvas/SVG 淘汰了,但在 Word 文档内部,它反而以“牙白”的方式活了很多年。Word 2003 XML 模板里的自选图形、文本框、图片框、批注人签名框,底层大量使用 <v:shape> 描述。
所以在模板里你会看到两种差异明显的 XML 片段:
- 普通段落、表格、文字属性用
w:前缀; - 浮动形状、VML 图片框、手绘线条用
v:前缀,并且通常被包在<w:pict>容器里。
这句话听起来简单,但实际排查时很多人没意识到的是:文字内容可以用 w:t 随便填,形状却要遵循 VML 的属性规则,混着用是行不通的。
1.2 被 VML 绊住的常见姿势
我把这么多年遇到的模板问题归纳一下,基本绕不开三类:
第一类是替换文字。有些人拿正则直接在 XML 里搜索 w:t 然后替换,结果文字没变,却发现 <v:textbox> 里的文字没被找到。原因是文本框内部的文字并不是普通段落里的 w:r/w:t,而是包在 <v:textbox><w:txbxContent> 里的另一层 WordprocessingML 结构。如果你只扫 .//w:t,是会漏掉这一块的。
第二类是调位置。形状的位置不是写在 x、y 属性里,而是集中写在 style 属性中,靠 margin-left、margin-top、z-index 控制。字符 VML 形状看不到,于是很多人误以为属性没生效。
第三类是打不开。XML 模板下载下来后,双击默认浏览器打开,一看是个 XML 树,还弹出 This XML file does not appear to have any style information associated with it,以为模板坏了。其实这多半不是坏文件,而是没有用对打开方式。后面我会专门说这个问题。
想绕开这些坑,核心还是先把 <v:shape> 本身拆明白。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先拆一个 v:shape:w:pict 包裹下的形状骨架
2.1 一个可以直接对比的 XML 模板片断
如果你手头有 Word 2003 或者一个老的 .xml 模板,可以在里面搜索 <w:pict>。下面是我从真实模板里精简出来的结构,只保留了最核心的骨架:
xml复制<w:p>
<w:r>
<w:pict>
<v:shape
id="审批意见框"
o:spid="_x0000_s1026"
type="#_x0000_t202"
style="position:absolute;margin-left:45pt;margin-top:80pt;width:200pt;height:80pt;z-index:1;mso-wrap-style:none;"
fillcolor="#FFF2CC"
stroked="t"
strokecolor="#BF9000"
strokeweight="0.75pt">
<v:textbox>
<w:txbxContent>
<w:p>
<w:r>
<w:t>请在此填写审批意见</w:t>
</w:r>
</w:p>
</w:txbxContent>
</v:textbox>
</v:shape>
</w:pict>
</w:r>
</w:p>
注意看嵌套顺序:w:p 是段落,里面套 w:r 是一个 run,run 里面再放 w:pict,w:pict 里面才是 v:shape。VML 形状不能直接放在 body 下,必须挂在某个 run 里。这也解释了为什么这类模板在做 XML 数据合并时,需要对 paragraph 和 run 的层级格外小心,不能随便把 <v:shape> 抽出来单独保存。
在这个例子里 type="#_x0000_t202",这意味着它引用的是一个预定义的文本框形状类型。Word 在内部有一套 VML 形状类型注册表,图片框常用 #_x0000_t75,文本框常用 #_x0000_t202。这些编号会对应到文档前面定义出来的 <v:shapetype>,让 Word 知道这个形状的默认几何路长、控制点是几点、连接方式是什么。
2.2 type 与 o:spid:两个容易被忽略的身份字段
type 属性很多人改 VML 时会忽略,但它是 VML 形状能被 Word 正确识别的关键。type 的值以 # 开头,然后接 shapetype 的 id。#_x0000_t75 里的编号不是随便定的,它是 Office 内置 autoshape 类型编号。如果这个属性丢了,Word 打开时可能把形状当成一个没有 path 的通用对象,显示成空白或者默认矩形。
o:spid 则更像 Word 内部的形状实例 ID。SPID 是 Shape Placement ID 的缩写,在整个文档里必须唯一。你手动写模板时可以随便留一个值,但如果是程序生成文档,最好按顺序递增,避免 Word 在排版时把两个形状的锚点弄混。我见过有人从线上模板复制了一个 shape 片段,忘了改 o:spid,结果替换之后两个形状叠在一起,还以为是 z-index 没设置。
2.3 v:shape 直接属性:先记这些就能改出能用的形状
v:shape 上除了上面说到的 id 和 type,最常打交道的直接属性就这么几个:
| 属性 | 作用 | 常见值 |
|---|---|---|
id |
形状在文档中的唯一标识 | 可以带中文,如 id="审批框1" |
type |
引用的 autoshape 类型 | #_x0000_t75 图片、#_x0000_t202 文本框 |
o:spid |
Word 内部放置 ID | _x0000_s1026 这种格式 |
style |
位置、尺寸、环绕、旋转等 | 一组以分号分隔的 CSS 风格键值 |
fillcolor |
填充底色,十六进制表示 | #FFFFFF、#FFC000 |
filled |
是否填充 | t 或 f |
strokecolor |
边框颜色 | #000000 |
stroked |
是否显示边框 | t 或 f |
strokeweight |
边框宽度 | 0.75pt、1pt |
coordorigin |
形状内部坐标系原点 | 0,0 |
coordsize |
内部坐标系尺寸 | 21600,21600 |
path |
自定义路径,非通用形状才需要 | 自动生成的 path 值 |
你会发现它把很多图形的属性直接放在了 v:shape 标签的属性里,这一点和 SVG 很像。但要注意,Word 生成的 VML 不是我随便写一个 path 就能精确控制的,它依赖 type 对应的 shapetype 定义。你想实现一个矩形,最简单可靠的做法是保留 type="#_x0000_t202" 的文本框类型,或者找一个现成的矩形 shapetype 引用,不要自己去画 path。
3. style 才是形状布局的主战场:位置、尺寸、环绕、旋转
3.1 一段 style 的逐段注解
VML 的布局几乎全部集中在 style 属性里,所以你会看到一大串用分号隔开的键值。真实的模板里经常长这样:
xml复制style="position:absolute;margin-left:10.35pt;margin-top:32.95pt;width:197.3pt;height:197.3pt;z-index:251660288;mso-wrap-style:square;mso-position-horizontal:absolute;mso-position-vertical:absolute;mso-position-horizontal-relative:page;mso-position-vertical-relative:page;"
逐段解释一下:
position:absolute表示形状相对于某个锚点绝对定位,不是跟着文字流走。浮动形状基本都会带这个。margin-left和margin-top在这里不是 CSS 里的“外边距”,而是形状距锚点的偏移量,单位默认是 pt(磅),不是 px。一个 Word 页面宽约 794pt,如果你看到 197.3pt,那大概是四分之一页宽。width和height是形状最终的显示尺寸,同样的单位是 pt。z-index控制图层顺序,但 Word 的 z-index 值不一定是 1、2、3,很多用大整数 251660288,这是 Word 生成器自己的编号结果,手动写模板里用 0 或 1 也能生效。mso-wrap-style是 Word 私有的文字环绕属性,square表示文字四周环绕形状,none表示不参与文字环绕。mso-position-horizontal和mso-position-vertical表示水平/垂直方向的对齐基准。absolute就是完全看 margin 值,left、center、right则是相对页面或栏进行对齐。- 最后两个
mso-position-horizontal-relative和mso-position-vertical-relative表示这个对齐是相对于page(页面)还是margin(页边距)还是text(文字区域)。很多模板中形状“跑到页面外面”或“和正文对不齐”,就是因为这两个值写的是page,可实际模板里形状想相对段落定位。
如果你是从一个旧模板复制形状做二次开发,改尺寸时一定要改 width 和 height,不要只改 margin-left、margin-top。这两对属性一个管大小一个管位置,很容易搞混。
3.2 mso- 前缀属性代表 Word 的私房逻辑
由于 VML 本身是微软推的,style 里会出现大量 mso- 开头的私有属性。它们不是标准 CSS,只是 Office 为了把 Word 排版模型的细节塞进 VML 而设计的。常见的就这么几个:
| 私有属性 | 含义 | 备注 |
|---|---|---|
mso-wrap-style |
环绕方式 | square、none、tight 等 |
mso-position-horizontal |
水平定位方式 | absolute、left、center、right |
mso-position-vertical |
垂直定位方式 | absolute、top、center、bottom |
mso-position-horizontal-relative |
水平相对基准 | page、margin、text、column |
mso-position-vertical-relative |
垂直相对基准 | page、margin、text、line |
mso-width-percent |
宽度是否按百分比 | 多数是 0 |
mso-height-percent |
高度是否按百分比 | 多数是 0 |
mso-fit-shape-to-text |
是否按文字缩放形状 | t 或 f |
喜欢用 Word 画形状的人都知道,Word 对象锚点有“随文字移动”“锁定标记”“允许重叠”等选项。这些选项也会投影成 style 或者 o: 前缀属性。比如:
o:allowincell="f"表示不允许形状被放进表格单元格内部;mso-wrap-edited="f"表示这个环绕方式还没有被用户手动调整过,Word 可以自动优化;- 如果你把形状拖进文字里,却想让它保持不随文字换页,Style 里通常会有
mso-position-horizontal:absolute配合相对page。
我的建议是:改样式值前先备份一份原模板,每次只改一个键,然后在 Word 里打开看效果。 这些 mso 私有属性之间常常互相影响,比如你把 mso-position-horizontal-relative 从 page 改成 margin,视觉上可能没有任何变化,因为 position:absolute 还是优先读 margin 的具体值;但你如果把 margin 删了,定位基准就会立刻跃到属性上。
4. 让形状有颜色、有线条、有文字:fill、stroke、shadow、textbox 实战
4.1 v:fill:底色的深浅渐变与透明
v:shape 的 fillcolor 只负责基础底色,如果你想做渐变、透明、图案填充,还得在形状内部加一个 <v:fill> 子元素。很多从 Word 2003 模板里提出来的形状,内部都会带一个 v:fill,它是 VML 里做填充效果的入口。
v:fill 常用属性我整理如下:
xml复制<v:fill
type="gradient"
color="#FFF2CC"
color2="#F4B183"
angle="45"
focus="0.5"
opacity="0.8"/>
type是填充类型,常用solid表示纯色,gradient表示线性渐变,gradientRadial表示辐射渐变,tile和pattern用于纹理/图案。color是起始颜色,color2是结束颜色。如果只写了 color2 忘了写 color,Word 可能用默认黑色作为起点,效果会很怪。angle控制渐变方向,45 度就是从左上到右下的斜向渐变。focus控制渐变中心的位置,0.5 表示渐变中点居中,改大会把渐变色拉伸到某一侧。opacity是透明度,1 表示完全不透明,0 表示全透明。它也可以写成 50% 这种带百分号的写法,但我在 Word 生成文件里更常见的是 0~1 之间的小数。
为什么很多模板里你删了 fillcolor 却还是有色?就是因为 <v:fill> 子元素的优先级更高。改底色时不要光盯 v:shape 的 fillcolor,也要检查内部 v:fill 的 color 和 color2。这一步是新手最容易踩的坑。
4.2 v:stroke:线条粗细、颜色与虚线组合
边框属性也不一定全部体现在 v:shape 的 stroked、strokecolor 上。更精确的线条设置通常由 <v:stroke> 子元素完成。
看一个常见例子:
xml复制<v:stroke
color="#BF9000"
weight="1pt"
dashstyle="dash"
linestyle="single"
joinstyle="bevel"
endcap="round"/>
color和weight分别对应颜色、粗细,weight 常见0.75pt、1pt、1.5pt。dashstyle控制虚线和实线,值有solid、dash、dot、dashdot、longdash等。linestyle控制复合线型,single是单线,thinThin、thinThick这类用来画粗细组合边框。joinstyle是折线拐角处的处理方式,常用round、bevel、miter。endcap控制线头形状,flat、round、square三种在这里比较常见。
一个容易忽略的点是:如果你先给 <v:shape> 写了 strokecolor="#000000",又在子元素里写 <v:stroke color="#FF0000">,Word 实际显示的大概率是子元素里的红色。调试这种“改了颜色但没变化”的问题时,应该优先搜索 shape 内部有没有 v:stroke。
4.3 shadow 和 textbox:文字内容如何与 VML 打通
shadow 在模板里用得不多,但一旦用上,很容易看出效果。它控制的是形状的阴影:
xml复制<v:shadow on="t" type="perspective" color="#999999" offset="2pt,2pt" opacity="0.5"/>
on="t" 表示打开阴影,type 可以是 single、double、perspective、shape 等,offset 表示阴影和形状的错位量。
比 shadow 更重要的是 VML 文本框里的内容。一般模板里要动态写入审批意见、签名日期、说明文字,都是往 <v:textbox> 里塞 WordprocessingML 内容。
xml复制<v:shape id="签字栏" type="#_x0000_t202"
style="position:absolute;margin-left:20pt;margin-top:200pt;width:150pt;height:40pt;z-index:3;">
<v:fill type="solid" color="#FBE5D6" opacity="1"/>
<v:textbox style="mso-fit-shape-to-text:f">
<w:txbxContent>
<w:p>
<w:r>
<w:t>日期:{{Date}}</w:t>
</w:r>
</w:p>
</w:txbxContent>
</v:textbox>
</v:shape>
这里的关键是 <w:txbxContent>。你可以在它里面写多段 w:p、多个 w:r,甚至放表格,逻辑上它就是一个被“塞进图形”的小型 WordprocessingML 文档。我之前提到过,如果用 XPath 抽取所有 w:t 做文字替换,忘记带 txbxContent 这一支,就会漏掉整个文本框里的动态内容。
mso-fit-shape-to-text 这个属性在 textbox 的 style 中经常出现。如果设成 t,Word 会根据文字内容自动调整形状大小;设成 f,形状大小完全看外层 width 和 height。模板里如果有固定高度的签名框,不要轻易改成 t,否则文字一多就把框撑变形了。
5. 模板里插图片、换图片时的三个典型坑
5.1 先弄懂图片框的 v:imagedata 层级
Word 2003 XML 模板里的图片不是独立存在的。它通常也是 VML 形状的一种,先有一个 <v:shape type="#_x0000_t75">,然后内部放 <v:imagedata> 描述图片来源。缩小后的层级大概是:
xml复制<w:pict>
<v:shape id="图片1" type="#_x0000_t75"
style="position:absolute;margin-left:120pt;margin-top:300pt;width:120pt;height:90pt;z-index:5;"
filled="f" stroked="f">
<v:imagedata o:title="company_logo"/>
</v:shape>
</w:pict>
实际处理时你还需要知道图片数据本身存在哪里。在 Word 2003 XML 模板这类单文件 XML 里,图片经常以二进制块形式内嵌,或者通过某个 r:id 关联到文档关系文件。图片真正能不能显示,还要检查来源是否存在、关联 id 是否正确。手工往模板里塞图片最稳妥的路径不是凭空构造 VML,而是先在空白文档里插入图片,另存成一个 XML,再拿那个文件里的完整 VML 片断回来改。
图片框有几点是模板操作中最容易出问题的:
第一,filled="f" stroked="f" 会让形状
