尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

C# OpenXml 操作 Word 嵌入对象(OLE)的完整指南

发布时间:2026/9/9 19:59:03

资讯中心
01
ARTICLE

C# OpenXml 操作 Word 嵌入对象(OLE)的完整指南

C# OpenXml 操作 Word 嵌入对象(OLE)的完整指南
如果你用C#做过Word文档的自动化处理应该没少碰DocumentFormat.OpenXml这个SDK。它最大的价值就是让你在不安装Office的情况下直接读取和生成.docx文件。我这套“学习C#调用OpenXml操作word文档的基本用法”系列写到第21篇今天这块内容比较冷门但实际项目里非常有用——嵌入对象类EmbeddedObject。说白了就是Word里通过“插入-对象”塞进去的那个东西一份Excel报表、一个PDF文件、一个Visio绘图。很多文档自动化场景里客户要求把数据表格以“可双击编辑的OLE对象”形式嵌进Word或者在服务端批量读取历史文档里嵌入的附件这时候OpenXml就是唯一靠谱的路子。本篇我会从OLE对象的底层结构讲起把OpenXml SDK里与嵌入对象相关的类捋清楚再给完整的读取、写入、删除代码最后把我在真实项目里踩过的坑列成速查表。适合两类读者一类是正在做Word批处理工具的C#开发者另一类是运维或上位机方向的工程师想在服务端解析docx里的嵌入文件。1. 嵌入对象类到底是什么先搞清楚OLE对象在Word里的存在方式1.1 OLE对象的基本概念与典型场景OLE是Object Linking and Embedding的缩写中文叫“对象链接与嵌入”。这个技术很老Windows 95时代就有了但直到今天Word还在大量使用。你用Word打开“插入-对象”能看到“新建”和“由文件创建”两个选项卡前者创建一个空的Excel/PPT/Paint对象后者把现有文件整体塞进Word。这两种操作的结果就是文档里出现了一个可以在Word内部被宿主程序激活的OLE对象。嵌入和链接是两回事。嵌入是把原文件的完整二进制数据复制进docx包内和源文件断了联系链接只是在文档里保存一个外部文件路径源文件变了文档跟着变。所以处理嵌入对象时你能从docx里提取出完整的原始文件这个特性在自动化场景中很具价值。典型场景我举三个财务系统导出的Word报价单里嵌着Excel详细计算表结算时需要把Excel提取出来归档。招投标文件里嵌入PDF资质文件需要批量校验PDF内容是否过期。批量生成合同时要把动态生成的Excel数据表以OLE对象形式嵌入Word客户双击就能编辑而不是贴一张静态图。1.2 OpenXml SDK中与嵌入对象相关的类与部件很多人一开始学OpenXml会懵因为它的对象模型并不像Word的COM组件那样直观。处理嵌入对象时你至少要接触这几个重要类型类型所在命名空间作用EmbeddedObjectDocumentFormat.OpenXml.Wordprocessing对应WordprocessingML中的w:object元素是嵌入对象在文档正文中的根节点OleObjectDocumentFormat.OpenXml.Vml.Office对应VML中的o:OLEObject元素记录OLE类型、ProgId、ShapeId等属性EmbeddedPackagePartDocumentFormat.OpenXml.Packagingdocx包中的一个部件存放OLE对象的二进制数据通常在word/embeddings目录下ImagePartDocumentFormat.OpenXml.Packaging嵌入对象显示的图标或预览图通常在word/media目录下DataPartReferenceRelationshipDocumentFormat.OpenXml.Packaging文档正文部件与数据部件之间的引用关系理解这些类和部件的关系是操作嵌入对象的第一步。1.3 为什么要理解“关系部件”模型docx本质上是一个zip压缩包。打开任意一个含嵌入对象的docx你会看到类似这样的目录结构word/ document.xml embeddings/ oleObject1.bin media/ image1.png _rels/ document.xml.rels [Content_Types].xmldocument.xml里只是一个w:object元素它通过r:id属性引用一个关系ID这个关系在document.xml.rels里定义指向word/embeddings/oleObject1.bin。真正的Excel/PDF二进制数据完全在文档正文之外。OpenXml SDK提供的对象模型正是对这个zip包的封装WordprocessingDocument对应整个包MainDocumentPart对应document.xmlEmbeddedPackagePart对应oleObject1.bin关系则通过part.GetPartById()这类方法访问。很多初学者在这块翻车是因为他们以为嵌入对象的数据直接写在w:object元素里面。实际上不是你操作EmbeddedObject获得的只是一个“入口”真正的内容要顺着关系拿。我后面两节的代码会把这个链路完整展示出来你可以把它理解成EmbeddedObject是门牌号关系ID是地图EmbeddedPackagePart才是屋子里放东西的保险柜。2. 读取Word文档中的嵌入对象从文档结构到二进制文件2.1 遍历文档中的EmbeddedObject读取的第一步是拿到文档里所有的EmbeddedObject。代码非常简单用DescendantsT()沿文档树往下找就行了using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; public static ListEmbeddedObject GetAllEmbeddedObjects(string docxPath) { using var wordDoc WordprocessingDocument.Open(docxPath, false); var mainPart wordDoc.MainDocumentPart; var body mainPart?.Document?.Body; if (body null) return new ListEmbeddedObject(); return body.DescendantsEmbeddedObject().ToList(); }这里有几个要点。首先using语句不能省WordprocessingDocument实现了IDisposable不释放会占用文件句柄其次打开模式必须是false只读打开防止误改源文档第三如果文档没有任何段落Body可能为null不做防空处理就会报空引用异常。遍历结果里可能有多个EmbeddedObject它们的排列顺序通常和Word界面上显示的顺序一致但这不绝对。如果文档里既有OLE对象又有“包”对象Package你得通过下一步的关系ID区分类型。2.2 通过关系ID获取EmbeddedPackagePart并保存文件拿到EmbeddedObject之后取它的Id属性。这个属性对应XML里的r:id就是关系ID。然后用MainDocumentPart.GetPartById()就能拿到关联部件public static Listbyte[] ExtractEmbeddedFiles(string docxPath) { var result new Listbyte[](); using var wordDoc WordprocessingDocument.Open(docxPath, false); var mainPart wordDoc.MainDocumentPart; var body mainPart?.Document?.Body; if (body null) return result; foreach (var obj in body.DescendantsEmbeddedObject()) { var relId obj.Id?.Value; if (string.IsNullOrEmpty(relId)) continue; var part mainPart.GetPartById(relId); if (part is EmbeddedPackagePart pkgPart) { using var stream pkgPart.GetStream(); using var ms new MemoryStream(); stream.CopyTo(ms); result.Add(ms.ToArray()); } } return result; }关键判断在这行if (part is EmbeddedPackagePart pkgPart)。为什么需要这个判断因为GetPartById()返回的OpenXmlPart类型很多可能是ImagePart预览图、EmbeddedPackagePartOLE数据、甚至自定义XML部件。只有EmbeddedPackagePart才是嵌入对象的二进制内容。拿到byte[]之后怎么处理最简单的方案是直接落盘但文件扩展名需要你自己定。OCX、OLE对象的文件头五花八门扩展名没法靠一个固定的规则判断。我建议你把提取结果和ProgId关联起来下面一节细说。2.3 获取嵌入对象的ProgId等元数据光有二进制数据不够很多时候你还需要知道这个嵌入对象是什么类型。OleObject元素里有个ProgId属性就是程序标识符比如Excel.Sheet.12代表Office 2007以上版本的Excel工作簿Word.Document.12代表Word文档Acrobat.pdf代表PDF。获取方式public static string GetEmbeddedObjectProgId(EmbeddedObject obj) { var oleObject obj.DescendantsDocumentFormat.OpenXml.Vml.Office.OleObject().FirstOrDefault(); return oleObject?.ProgId?.Value ?? string.Empty; }注意OleObject不在Wordprocessing命名空间里它在DocumentFormat.OpenXml.Vml.Office命名空间。实际项目里我强烈建议把ProgId作为判断文件类型的依据ProgId含义建议扩展名Excel.Sheet.8Excel 97-2003工作簿.xlsExcel.Sheet.12Excel 2007工作簿.xlsxWord.Document.8Word 97-2003文档.docWord.Document.12Word 2007文档.docxPowerPoint.Show.12PowerPoint 2007演示文稿.pptxAcrobat.pdfPDF文档.pdfPackage通用OLE包可能是任意文件.bin如果你拿一个docx让OpenXml SDK读取提取出Excel文件后直接命名成.xlsx双击能正常打开的概率很高但如果是Package类型建议统一存成.bin或根据文件头再做识别别硬猜扩展名。还有个细节有些嵌入对象在w:object下面没有OleObject元素而是直接用一个w:oleObject或者o:OLEObject大小写不同。OpenXml SDK对所有VML Office元素都会解析成OleObject类所以Descendants方式最保险。3. 在Word文档中写入嵌入对象完整落地实现3.1 环境准备与OpenXml SDK基础动手之前先把环境准备好。我用的是Visual Studio 2022加.NET 8NuGet里安装DocumentFormat.OpenXml当前稳定版是2.20以上。这个包支持.NET Framework 3.5到.NET 8Windows和Linux都能跑这点比Office COM组件强太多——COM组件只能在Windows上装Office服务端部署又重又贵还容易出权限问题。建一个控制台项目引入两个命名空间using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing;还要额外引入一个别名因为OleObject在DocumentFormat.OpenXml.Vml.Office命名空间里和Wordprocessing里的一些类容易混淆using VmlOffice DocumentFormat.OpenXml.Vml.Office;3.2 创建EmbeddedPackagePart并写入文件数据嵌入对象的数据必须通过AddNewPartT()方法挂到MainDocumentPart上。这一步会把二进制数据写入docx包内的word/embeddings目录并自动在document.xml.rels里建立一条关系public static void AddEmbeddedObject(string docxPath, string targetDocxPath, string fileToEmbed) { using var wordDoc WordprocessingDocument.Open(docxPath, true); var mainPart wordDoc.MainDocumentPart ?? wordDoc.AddMainDocumentPart(); var body mainPart.Document?.Body ?? mainPart.Document.AppendChild(new Body()); // 1. 创建EmbeddedPackagePart数据流里写入要嵌入的文件 var embedPart mainPart.AddNewPartEmbeddedPackagePart( application/vnd.openxmlformats-officedocument.oleObject, rIdEmbedded1); using (var partStream embedPart.GetStream(FileMode.Create)) using (var fileStream File.OpenRead(fileToEmbed)) { fileStream.CopyTo(partStream); } // 2. 后续步骤创建EmbeddedObject并挂到正文 // ... }AddNewPartT的第一个参数是ContentType第二个参数是希望用的关系ID。如果第二个参数不传SDK会自动生成rId1这样的编号但为了可读性我习惯自定义。GetStream(FileMode.Create)获取部件数据流把源文件内容拷贝进去嵌入对象的二进制数据部分就完成了。需要注意AddNewPart返回的EmbeddedPackagePart的RelationshipId属性才是我们最终要在XML里引用的ID不是我们传入的“rIdEmbedded1”这个字符串本身。虽然多数情况下两者一致但保险起见还是用embedPart.RelationshipId来取。3.3 创建EmbeddedObject与OleObject并挂入文档嵌入对象在正文里需要放在一个Run内Run放在Paragraph内。EmbeddedObject本身对应w:object子元素是OleObject和可选的Shape用于显示图标、预览图。// 2. 构造OleObject var shapeId _x0000_i1025; var oleObject new VmlOffice.OleObject { Type VmlOffice.OleObjectValues.Embed, ProgId Excel.Sheet.12, ShapeId shapeId }; // 3. 构造EmbeddedObject var embeddedObject new EmbeddedObject { Id embedPart.RelationshipId, DxaOrig 6000, DyaOrig 3000 }; embeddedObject.AppendChild(oleObject); // 4. 挂入正文 var run new Run(embeddedObject); var paragraph new Paragraph(run); body.AppendChild(paragraph); mainPart.Document.Save();这里几个属性逐个说Type OleObjectValues.Embed表示这是一个嵌入对象而非链接对象。ProgId必须和嵌入的文件类型匹配。比如你想嵌入xlsx就填Excel.Sheet.12想嵌入docx就填Word.Document.12。填错的话Word打开文档时可能找不到对应的源应用程序。ShapeId是你为这个对象起的唯一标识通常在同一个文档里不重复。这个ID在VML渲染时用来绑定图标、鼠标事件等。DxaOrig和DyaOrig是对象在Word界面中显示时的原始宽度和高度单位是“缇”Twip1英寸等于1440缇。A4页面常用6000x3000差不多就是10.4cm宽x5.2cm高。这一步做完再用Word打开生成的文档你会看到一个Excel对象图标出现在文档末尾双击可以调用本地Excel编辑。但有一个细节如果你没有添加Shape子元素Word会自动生成一个默认图标样式比较简陋但功能没问题。如果想完全控制图标和预览图需要额外创建ImagePart这一块我在下一节展开。3.4 控制显示尺寸与图标嵌入对象的显示效果主要由w:object里的VMLShape元素和ImagePart共同决定。Shape描述对象框的位置、大小、旋转等ImagePart提供图标或缩略图。如果你希望嵌入对象显示成“Excel表格的缩略图”而不是默认的Excel图标需要做三件事用GDI或第三方库把Excel文件转换成一张预览图片PNG/JPG。创建ImagePart把图片数据写进去。在EmbeddedObject里添加一个VMLShapeShape内部通过imagedata元素引用ImagePart的关系ID。代码片段var imagePart mainPart.AddNewPartImagePart(image/png); using (var imgStream imagePart.GetStream(FileMode.Create)) using (var imgFile File.OpenRead(preview.png)) { imgFile.CopyTo(imgStream); } var shape new DocumentFormat.OpenXml.Vml.Shape { Id shapeId, Style width:120pt;height:60pt }; var imageData new DocumentFormat.OpenXml.Vml.Imagedata { RelationshipId imagePart.RelationshipId, Title preview }; shape.AppendChild(imageData); embeddedObject.InsertAt(shape, 0); // shape必须在OleObject之前顺序很关键。OOXML的schema规定w:object的子元素顺序是先形状后OLE对象顺序反了OpenXml SDK不一定报错但Word打开时可能提示文档损坏。我用InsertAt(shape, 0)就是为了保证shape插在OleObject前面。显示尺寸的单位转换经验VML的style里用的是CSS长度单位一般用pt磅1英寸72磅。前面DxaOrig用的是缇1磅20缇。我习惯让DxaOrig、DyaOrig和VML的宽高保持一致比如6000缇对应300磅VML里写width:300pt。4. 修改、删除嵌入对象时的常见坑4.1 替换已有嵌入对象的二进制数据有时候不需要新建嵌入对象只想把文档里已有的Excel内容换成新数据。做法比想象中简单找到EmbeddedPackagePart直接覆盖它的数据流。public static void ReplaceEmbeddedFile(string docxPath, int index, string newFilePath) { using var wordDoc WordprocessingDocument.Open(docxPath, true); var mainPart wordDoc.MainDocumentPart; var embeddedObjects mainPart.Document.Body.DescendantsEmbeddedObject().ToList(); if (index 0 || index embeddedObjects.Count) throw new ArgumentOutOfRangeException(nameof(index)); var relId embeddedObjects[index].Id?.Value; var part mainPart.GetPartById(relId) as EmbeddedPackagePart ?? throw new InvalidOperationException(目标位置不是可替换的嵌入对象); using var partStream part.GetStream(FileMode.Create); using var fileStream File.OpenRead(newFilePath); fileStream.CopyTo(partStream); mainPart.Document.Save(); }这里有一个隐性问题如果新文件和旧文件的类型不一致比如原来嵌的是Excel现在换成一个PDF那ProgId还是Excel.Sheet.12Word就会用Excel去打开一个PDF结果必然报错。所以替换时如果文件类型变了必须同步更新OleObject的ProgId或者删除原对象重新插入。4.2 删除嵌入对象不清理关系等于白删删除嵌入对象比添加更容易踩坑。很多人只删了w:object元素发现文档体积没变小甚至打开文档提示关系错误。原因很简单EmbeddedPackagePart以及它对应的关系还残留在docx包里没有随正文元素一起移除。正确的删除姿势是两条线并行一条线从XML树里移除EmbeddedObject所在的Run另一条线用DeletePart()删除关联部件和关系。public static void RemoveEmbeddedObject(string docxPath, int index) { using var wordDoc WordprocessingDocument.Open(docxPath, true); var mainPart wordDoc.MainDocumentPart; var embeddedObjects mainPart.Document.Body.DescendantsEmbeddedObject().ToList(); if (index 0 || index embeddedObjects.Count) throw new ArgumentOutOfRangeException(nameof(index)); var obj embeddedObjects[index]; // 1. 删除关联部件和关系 var relId obj.Id?.Value; if (!string.IsNullOrEmpty(relId)) { var part mainPart.GetPartById(relId); mainPart.DeletePart(part); } // 2. 从正文中删除所在Run var run obj.AncestorsRun().FirstOrDefault(); run?.Remove(); mainPart.Document.Save(); }这里有个陷阱obj.Id可能指向的不只是EmbeddedPackagePart如果对象带预览图Shape内部还有一个Imagedata.RelationshipId指向ImagePart。如果只删了EmbeddedPackagePart没删ImagePart预览图文件会残留。完整的删除应该遍历EmbeddedObject下所有引用关系ID的元素把所有引用到的部件都清理掉。我列出两种常见情况元素关系属性关联部件EmbeddedObjectIdEmbeddedPackagePartVML ImagedataRelationshipIdImagePart写删除逻辑时建议用DescendantsT()统一收集这两种元素的关系ID然后逐个DeletePart。4.3 常见问题速查表现象可能原因解决方案Word提示“无法读取此文档文档可能已损坏”EmbeddedObject的Id指向不存在的部件或部件类型不是EmbeddedPackagePart用OpenXml SDK检查document.xml.rels里的关系是否完整双击嵌入对象提示“找不到服务器应用程序”ProgId和实际文件类型不匹配提取二进制文件并通过文件头判断真实类型改对ProgId嵌入对象显示为空白或没有图标ImagePart缺失或者Shape中Imagedata的r:id指向了不存在的部件补建ImagePart或删除Shape让Word使用默认图标删了嵌入对象体积没变小只删了XML元素没删EmbeddedPackagePart和ImagePart用DeletePart清理所有关联部件添加后嵌入对象大小和预期不符DxaOrig/DyaOrig单位错误或VML style单位错误确认缇、磅、像素的换算关系别搞混文档打开后嵌入对象是“链接”而不是“嵌入”OleObject的Type设成了Link而不是Embed设置Type OleObjectValues.Embed4.4 与链接对象的区别处理嵌入对象时很容易遇到“链接对象”。链接对象在OOXML里有另一套实现w:object仍然存在但内部可能是o:OLEObject TypeLink并且没有EmbeddedPackagePart而是通过ExternalRelationship指向外部文件路径。这意味着用mainPart.GetPartById()根据EmbeddedObject.Id拿到的部件可能不是EmbeddedPackagePart而是ExternalRelationship对应的外部资源引用。链接对象的数据不在docx包里提取时拿不到完整二进制内容。服务器上如果原文件路径失效链接对象就打不开。所以判断一个对象到底是“嵌入”还是“链接”不要只看w:object是否存在要看OleObject的Type属性再看是否关联到EmbeddedPackagePart。结尾经验分享我最初接触嵌入对象时也试图通过word COM组件去搞但服务端没装Office每次操作都要申请一个带Office的Windows服务器成本高、维护烦。后来切到OpenXml SDK把这些逻辑封装成一个工具类放在Linux容器里跑稳定运行一年。我的体会有两点一是OpenXml学习曲线不像COM组件那么直观但一旦理解了“关系部件”模型就会发现它逻辑非常干净关键是学会用OpenXml SDK Productivity Tool把样例docx拖进去自动生成C#代码这是最快的学习路径。二是操作嵌入对象时始终把“数据在部件里、元数据在XML里”这句话刻在脑子里遇到任何异常先看关系ID是否正确、部件类型是否符合预期问题往往就能定位个八九不离十。如果你也做过类似需求或者在读这篇时遇到了其他奇怪的坑欢迎在评论区留言我看到了会抽空回复。这个系列下一篇文章我计划写OpenXml操作Word里的内容控件Content Control也就是带标签的可编辑区域那又是另一个实用的模块。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。