【免费下载链接】Open-XML-SDK
Open XML SDK by Microsoft
本指南基于 Open-XML-SDK 仓库中的 samples/AnimatedModel3DExample 示例,完整讲解如何通过 C# 与 Open XML SDK 向 .pptx 演示文稿插入一个可播放的动画 3D 模型:既包含嵌入 .glb(glTF 二进制 3D 文件)与 .png(3D 图像预览图)两个部件,也包含构造 mc:AlternateContent(旧版 PowerPoint 回退为普通图片)与 p:timing(动画时间轴)的完整流程。读完本文,你将掌握动画 3D 模型在 OOXML 包结构中的落盘方式、SDK 对应强类型元素的使用方法,以及一个可直接运行的命令行示例程序。
一、示例概览与前置条件
在 samples/AnimatedModel3DExample/README.md 中,微软官方对该示例的描述非常精炼:
This sample code shows how to add an Animated Model 3D to a PowerPoint file. When adding an Animated Model 3D to a presentation, the SDK needs a .png file of the 3D image and a .glb file, which is a 3D file format for rendering in 3D.
示例目录结构如下:
Program.cs:示例唯一入口,包含全部插入逻辑;AddAnimatedModel3D.csproj:示例工程文件,引用src\DocumentFormat.OpenXml\DocumentFormat.OpenXml.csproj;README.md:说明文档。
向 PowerPoint 插入动画 3D 模型需要两类素材文件:
| 文件 | 格式说明 | 用途 |
|---|---|---|
.png | 3D 模型的 2D 图像 | 作为模型在幻灯片中的视觉预览/贴图(r:embed),同时也是旧版 PowerPoint 回退分支中的图片来源 |
.glb | glTF Binary(GL Transmission Format 的二进制封装) | 真正的 3D 模型数据,内嵌动画;SDK 将其写入 Model3DReferenceRelationshipPart,MIME 类型为 model/gltf.binary |
由于 3D 模型特性属于 Office 2019 引入的扩展(相关命名空间为 http://schemas.microsoft.com/office/drawing/2017/model3d,即 am3d),因此最终生成的演示文稿需要 PowerPoint 2019 及以上版本才能播放完整动画;在旧版本中,幻灯片会展示 mc:Fallback 分支中的静态图片。
二、示例工程与运行方式
AddAnimatedModel3D.csproj 非常简单,核心是引用仓库主库项目:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>$(SamplesFrameworks)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DocumentFormat.OpenXml\DocumentFormat.OpenXml.csproj" />
</ItemGroup>
</Project>
其中 $(SamplesFrameworks) 由根目录 Directory.Build.props 定义:在默认(非 net35/net40 专项构建)情况下为 net8.0;net10.0,若在 Windows 上还额外追加 net472。示例目录共享 samples/Directory.Build.targets,它会自动编译 samples/common/*.cs 并引用 src 下两个核心工程。
从仓库根目录运行:
dotnet run --project samples/AnimatedModel3DExample/AddAnimatedModel3D.csproj -- <OOXML> <png> <glb>
其中:
<OOXML>:目标.pptx文件路径(会被就地修改);<png>:3D 模型的.png图像文件路径;<glb>:3D 动画模型的.glb文件路径。
Main 方法会先校验参数个数,再调用公共工具类 Common.ExampleUtilities.CheckIfFilesExist(见 samples/common/ExampleUtilities.cs)逐个检查文件是否存在,全部通过后才执行插入逻辑:
private static void Main(string[] args)
{
if (args.Length < 3)
{
Common.ExampleUtilities.ShowHelp(
new string[]
{
"AnimatedModel3DExample: ",
"Usage: AnimatedModel3DExample <OOXML> <png> <glb>",
"Where <OOXML> is the file to add the animated model 3d to",
"and <png> is the .png file with the image for the Animated Model 3D",
"Where <glb> is the .glb file for the animated model 3d",
});
}
else if (Common.ExampleUtilities.CheckIfFilesExist(args))
{
InsertAnimatedModel3D(args[0], args[1], args[2]);
}
}
整个示例的核心方法正如 README 所强调的:
public static void InsertAnimatedModel3D(string pptxPath, string pngPath, string glbPath)
三、插入流程总览
InsertAnimatedModel3D(完整实现见 samples/AnimatedModel3DExample/Program.cs)按如下步骤工作:
- 打开演示文稿:以可读写方式
PresentationDocument.Open(pptxPath, true)打开; - 定位第一张幻灯片:通过
PresentationPart.Presentation.SlideIdList取得第一个SlideId的关系 ID,再用GetPartById拿到SlidePart; - 嵌入两个部件:把
.glb写入Model3DReferenceRelationshipPart,把.png写入ImagePart,并各自生成新的rId; - 构造
mc:AlternateContent:在幻灯片的CommonSlideData.ShapeTree上追加AlternateContent,其中:mc:Choice(Requires="am3d"):完整的 3D 模型GraphicFrame,包含模型几何、相机、光照、贴图与动画扩展;mc:Fallback:一张普通Picture,旧版 PowerPoint 显示此分支;
- 追加
p:timing:在SlidePart.RootElement(p:sld)上追加动画时间轴,驱动embedded1属性在 0→1 间线性变化,实现模型的嵌入动画播放。
下面按步骤逐层深入。
四、打开文档并定位目标幻灯片
// Open the presentation document
using PresentationDocument presentationDocument = PresentationDocument.Open(pptxPath, true);
// Get the relationship ID of the first slide.
PresentationPart presentationPart = presentationDocument.PresentationPart;
if (presentationPart != null && presentationPart.Presentation != null)
{
OpenXmlElementList slideIds = presentationPart.Presentation.SlideIdList.ChildElements;
if (slideIds.Count > 0)
{
string relId = (slideIds[0] as SlideId).RelationshipId;
SlidePart slidePart = (SlidePart)presentationPart.GetPartById(relId);
int partsCount = slidePart.Parts.Count();
...
}
}
要点说明:
PresentationDocument.Open(pptxPath, true)的第二个参数editable为true,表示以读写模式打开,后续所有修改会直接写回原文件;Presentation.SlideIdList是演示文稿中幻灯片的引用列表,SlideId.RelationshipId指向PresentationPart与对应SlidePart之间的关系;slidePart.Parts.Count()用于计算当前幻灯片已拥有的部件数量,作为生成新关系 ID 的基准;- 示例将 3D 模型插入第一张幻灯片,实际项目中你可以遍历
SlideIdList选择任意目标幻灯片。
五、嵌入 .glb 与 .png 部件
// Add the .png and .glb files for the 3D image
using FileStream glbFileStream = new FileStream(glbPath, FileMode.Open, FileAccess.Read);
glbFileStream.Seek(0, SeekOrigin.Begin);
string glbId = $"rId{++partsCount}";
Model3DReferenceRelationshipPart glbImagePart = slidePart.AddNewPart<Model3DReferenceRelationshipPart>(glbId);
glbImagePart.FeedData(glbFileStream);
using FileStream pngFileStream = new FileStream(pngPath, FileMode.Open, FileAccess.Read);
pngFileStream.Seek(0, SeekOrigin.Begin);
string pngId = $"rId{++partsCount}";
ImagePart pngImagePart = slidePart.AddImagePart(ImagePartType.Png, pngId);
pngImagePart.FeedData(pngFileStream);
这段代码体现了 Open XML SDK 部件模型的典型用法:
AddNewPart<Model3DReferenceRelationshipPart>(glbId)在SlidePart下新建 3D 模型引用部件,并指定关系 ID(rId从现有部件数递增,避免冲突);AddImagePart(ImagePartType.Png, pngId)新建 PNG 图片部件;- 两个部件都通过
FeedData(Stream)直接把文件流写入包(ZIP)内。
在 SDK 底层,Model3DReferenceRelationshipPart 被定义为 model/gltf.binary 的内容类型。源码 src/DocumentFormat.OpenXml/Packaging/Model3DReferenceRelationshipPart.cs 中的实现印证了这一点:
public partial class Model3DReferenceRelationshipPart
{
/// <inheritdoc/>
private protected override bool IsValidContentType(IPackagePart part)
{
if (base.IsValidContentType(part))
{
return true;
}
// model/gltf.binary is an acceptable MIME for this part, PowerPoint writes this on save.
return part.ContentType == "model/gltf.binary";
}
}
从源码注释可以看出:model/gltf.binary 是 PowerPoint 保存文件时实际使用的 MIME 类型,SDK 在读取时也兼容这种写法,这正是 .glb 文件在 OOXML 包中的标准落盘形态。ImagePartType.Png 则属于 ImagePart 支持的常规类型之一,其 MIME 为 image/png。
六、构造 mc:AlternateContent:3D 模型与回退图片
拿到 CommonSlideData 后,示例在其 ShapeTree 上追加 AlternateContent。这是 OOXML Markup Compatibility(mc)机制的典型应用:让同一位置在不同版本的 PowerPoint 中呈现不同内容。
CommonSlideData commonSlideDataPart = slidePart.RootElement.Descendants<CommonSlideData>().FirstOrDefault();
// Add an AlternateContent element to the shape tree
AlternateContent alternateContent = commonSlideDataPart.ShapeTree.AppendChild(new AlternateContent());
// Add mc:Choice element to AlternateContent
AlternateContentChoice alternateContentChoice = alternateContent.AppendChild(new AlternateContentChoice());
alternateContentChoice.Requires = "am3d";
alternateContentChoice.AddNamespaceDeclaration("am3d", "http://schemas.microsoft.com/office/drawing/2017/model3d");
// Add mc:Fallback element to AlternateContent
AlternateContentFallback alternateContentFallback = alternateContent.AppendChild(new AlternateContentFallback());
关键设置:
AlternateContentChoice.Requires = "am3d":声明该分支要求支持am3d命名空间,只有理解 3D 模型标记的 PowerPoint(2019+)才会选用;AddNamespaceDeclaration("am3d", "http://schemas.microsoft.com/office/drawing/2017/model3d"):为mc:Choice显式声明 3D 模型命名空间;AlternateContentFallback:作为通用回退分支,任何版本的 PowerPoint 都能解析其中的普通绘图元素。
6.1 mc:Choice 分支:完整的 3D 模型 GraphicFrame
mc:Choice 内嵌一个 GraphicFrame,其 XML 结构大致为:
p:graphicFrame
├─ p:nvGraphicFramePr
│ ├─ p:cNvPr (id="2" name="3D Model 1" descr="Flying bee")
│ │ └─ a:extLst → a:ext (a16:creationId) uri={FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}
│ ├─ p:cNvGraphicFramePr
│ └─ p:nvPr
│ └─ a:extLst → a:ext (p14:modId) uri={D42A27DB-BD31-4B8C-83A1-F6EECF244321}
├─ p:xfrm
│ ├─ a:off (x="4016654" y="1698584")
│ └─ a:ext (cx="4158691" cy="3460830")
└─ a:graphic
└─ a:graphicData (uri="http://schemas.microsoft.com/office/drawing/2017/model3d")
└─ am3d:model3d (r:embed="<glbId>")
├─ am3d:spPr (2D 变换 + 矩形预设几何)
├─ am3d:camera (透视投影、观察点、上向量)
├─ am3d:transform (比例、平移、旋转)
├─ am3d:raster (r:embed="<pngId>" rName="Office3DRenderer" rVer="16.0.8326")
├─ am3d:extLst
│ ├─ a:ext (a3danim:embedAnim) uri={9A65AA19-BECB-4387-8358-8AD5134E1D82}
│ └─ a:ext (a3danim:posterFrame) uri={E9DE012E-A134-456F-84FE-255F9AAD75C6}
├─ am3d:objViewport (viewportSz="5418666")
├─ am3d:ambientLight (环境光)
└─ am3d:ptLight ×3 (三盏点光源)
示例通过 GraphicFrame 的强类型构造函数把整棵元素树一次性构建出来,核心片段如下:
alternateContentChoice.AppendChild(
new GraphicFrame(
new NonVisualGraphicFrameProperties(
new NonVisualDrawingProperties(
new DocumentFormat.OpenXml.Drawing.NonVisualDrawingPropertiesExtensionList(
new DocumentFormat.OpenXml.Drawing.NonVisualDrawingPropertiesExtension(
creationId)
{ Uri = "{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}" }))
{ Id = threeDModelId, Name = "3D Model 1", Description = "Flying bee" },
new NonVisualGraphicFrameDrawingProperties(),
new ApplicationNonVisualDrawingProperties(
new ApplicationNonVisualDrawingPropertiesExtensionList(
new ApplicationNonVisualDrawingPropertiesExtension(
modificationId)
{ Uri = "{D42A27DB-BD31-4B8C-83A1-F6EECF244321}" }))),
new Transform(
offset,
aExtension),
new DocumentFormat.OpenXml.Drawing.Graphic(
new DocumentFormat.OpenXml.Drawing.GraphicData(
new DocumentFormat.OpenXml.Office2019.Drawing.Model3D.Model3D(...)
{ Embed = glbId })
{ Uri = "http://schemas.microsoft.com/office/drawing/2017/model3d" })));
其中 Model3D 的 Embed = glbId 通过 r:embed 属性把第 5 步嵌入的 .glb 部件关联到模型元素上。这些强类型元素均由 SDK 的源码生成器从官方 Schema 自动产出(见 generated/DocumentFormat.OpenXml/.../schemas_microsoft_com_office_drawing_2017_model3d.g.cs),例如 Model3D 类:
public partial class Model3D : OpenXmlCompositeElement
{
internal static readonly new OpenXmlQualifiedName ElementQName =
new("http://schemas.microsoft.com/office/drawing/2017/model3d", "model3d");
...
public StringValue? Embed
{
get => GetAttribute<StringValue>();
set => SetAttribute(value);
}
...
internal override void ConfigureMetadata(ElementMetadata.Builder builder)
{
...
builder.Availability = FileFormatVersions.Office2019;
...
}
}
从生成代码的 ConfigureMetadata 可以看到 Model3D 的元素可用性被标记为 FileFormatVersions.Office2019,其合法子元素顺序为 spPr → camera → transform → (可选 attributionSourceURL) → (可选 raster) → (可选 extLst) → (objViewport | winViewport) → (可选 ambientLight) → (可选一组点光/聚光/方向光/未知光)。示例中的元素顺序完全遵守这一粒子(Particle)约束,这正是 SDK 校验与序列化的依据。
Model3D 内部还细分了几组有明确分工的子元素(示例中都有体现):
- 几何与外观:
ShapeProperties(am3d:spPr,矩形预设几何 + 2D 变换)、Model3DCamera(am3d:camera,包含PosPoint3D、UpVector3D、LookAtPoint3D与PerspectiveProjection{Fov=2700000})、Model3DTransform(am3d:transform,米/模型单位比率、平移、缩放、旋转); - 贴图与动画扩展:
Model3DRaster(am3d:raster,Embed = pngId引用 PNG 预览图,并标注渲染器Office3DRenderer与版本16.0.8326)、Model3DExtensionList(挂接动画扩展); - 视口与光照:
ObjectViewport(ViewportSz = 5418666)、AmbientLight(50% 灰环境光)、三盏PointLight(暖橙、冷蓝、紫青三色点光源,各自带Rad = 0)。
6.2 mc:Fallback 分支:普通图片
为了让不支持 am3d 的 PowerPoint 版本仍能显示内容,mc:Fallback 中放了一张与 3D 模型位置、大小一致的普通 Picture,其图片即第 5 步嵌入的 PNG:
alternateContentFallback.AppendChild(
new DocumentFormat.OpenXml.Presentation.Picture(
new NonVisualPictureProperties(
new NonVisualDrawingProperties(
new DocumentFormat.OpenXml.Drawing.NonVisualDrawingPropertiesExtensionList(
new DocumentFormat.OpenXml.Drawing.NonVisualDrawingPropertiesExtension(
creationId2)
{ Uri = "{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}" }))
{ Id = threeDModelId, Name = "3D Model 1", Description = "Flying bee" },
new DocumentFormat.OpenXml.Presentation.NonVisualPictureDrawingProperties(
new DocumentFormat.OpenXml.Drawing.PictureLocks()
{
NoGrouping = true, NoRotation = true, NoChangeAspect = true,
NoMove = true, NoResize = true, NoEditPoints = true,
NoAdjustHandles = true, NoChangeArrowheads = true,
NoChangeShapeType = true, NoCrop = true
}),
new DocumentFormat.OpenXml.Presentation.ApplicationNonVisualDrawingProperties()),
new DocumentFormat.OpenXml.Presentation.BlipFill(
new DocumentFormat.OpenXml.Drawing.Blip() { Embed = pngId },
new DocumentFormat.OpenXml.Drawing.Stretch(
new DocumentFormat.OpenXml.Drawing.FillRectangle())),
new DocumentFormat.OpenXml.Presentation.ShapeProperties(
new DocumentFormat.OpenXml.Drawing.Transform2D(
offset2,
aExtension3),
new DocumentFormat.OpenXml.Drawing.PresetGeometry(
new DocumentFormat.OpenXml.Drawing.AdjustValueList())
{ Preset = DocumentFormat.OpenXml.Drawing.ShapeTypeValues.Rectangle })));
细节说明:
- 回退图片的
Blip.Embed = pngId与 3D 分支共用同一个 PNG 部件,无需额外资源; PictureLocks上几乎所有锁都置为true,防止用户在旧版 PowerPoint 中移动、缩放或编辑回退图片;creationId2是creationId的克隆(Clone()),保证两个分支持有相同内容的a16:creationId,使 PowerPoint 能识别二者是同一对象的两种表达;offset2、aExtension3同样来自Offset/Extension的克隆,确保回退图片与 3D 模型的包围盒(位置4016654, 1698584、尺寸4158691×3460830,EMU 单位)完全一致。
七、辅助元素:creationId、modId 与动画扩展
在构建 GraphicFrame 之前,示例先准备了几个跨命名空间的辅助元素:
// Create a creationId with the correct namespace
string guidId = "{" + System.Guid.NewGuid().ToString().ToUpper() + "}";
DocumentFormat.OpenXml.Office2016.Drawing.CreationId creationId = new DocumentFormat.OpenXml.Office2016.Drawing.CreationId();
creationId.AddNamespaceDeclaration("a16", "http://schemas.microsoft.com/office/drawing/2014/main");
creationId.Id = guidId;
// Clone the creationId
DocumentFormat.OpenXml.Office2016.Drawing.CreationId creationId2 =
(DocumentFormat.OpenXml.Office2016.Drawing.CreationId)creationId.Clone();
string creationIdPartenId = guidId;
UInt32Value threeDModelId = new UInt32Value(2U);
// Create modId with the correct namespace xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main"
DocumentFormat.OpenXml.Office2010.PowerPoint.ModificationId modificationId =
new DocumentFormat.OpenXml.Office2010.PowerPoint.ModificationId();
modificationId.AddNamespaceDeclaration("p14", "http://schemas.microsoft.com/office/powerpoint/2010/main");
modificationId.Val = 3636546711;
// Create an a:Extension with the correct attributes
DocumentFormat.OpenXml.Drawing.Extension aExtension = new DocumentFormat.OpenXml.Drawing.Extension();
aExtension.SetAttributes(
new OpenXmlAttribute[]
{
new OpenXmlAttribute("cx", string.Empty, "4158691"),
new OpenXmlAttribute("cy", string.Empty, "3460830"),
});
DocumentFormat.OpenXml.Drawing.Extension aExtension2 = (DocumentFormat.OpenXml.Drawing.Extension)aExtension.Clone();
DocumentFormat.OpenXml.Drawing.Extension aExtension3 = (DocumentFormat.OpenXml.Drawing.Extension)aExtension.Clone();
// Create a a3danim:posterFrame and add the namespace
DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.PosterFrame model3dPosterFrame =
new DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.PosterFrame() { AnimId = 0 };
model3dPosterFrame.AddNamespaceDeclaration("a3danim", "http://schemas.microsoft.com/office/drawing/2018/animation/model3d");
// Create a:off
DocumentFormat.OpenXml.Drawing.Offset offset = new DocumentFormat.OpenXml.Drawing.Offset() { X = 4016654, Y = 1698584 };
DocumentFormat.OpenXml.Drawing.Offset offset2 = (DocumentFormat.OpenXml.Drawing.Offset)offset.Clone();
// Create a3danim:embedAnim and assign its namespace
DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.EmbeddedAnimation embeddedAnimation =
new DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.EmbeddedAnimation() { AnimId = 0 };
embeddedAnimation.AddNamespaceDeclaration("a3danim", "http://schemas.microsoft.com/office/drawing/2018/animation/model3d");
embeddedAnimation.AppendChild(
new DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D.AnimationProperties()
{ Length = "1899", Count = "indefinite" });
这些元素的作用分别是:
a16:creationId:对象的全局唯一创建 ID(GUID),同一对象在 Choice 与 Fallback 分支中保持一致;p14:modId:修改 ID,值为3636546711,用于对象版本追踪;a:ext(cx/cy 属性):通过SetAttributes直接写入扩展属性,供图形框与图片共用尺寸信息;Clone()让三份拷贝保持一致;a3danim:embedAnim与a3danim:posterFrame:动画嵌入与海报帧扩展,挂到Model3DExtensionList的两个a:ext中(URI 分别为{9A65AA19-BECB-4387-8358-8AD5134E1D82}与{E9DE012E-A134-456F-84FE-255F9AAD75C6}),AnimationProperties指定动画时长1899ms且循环次数为indefinite。
EmbeddedAnimation 与 PosterFrame 均来自 DocumentFormat.OpenXml.Office2019.Drawing.Animation.Model3D 命名空间(对应 a3danim Schema),同样由源码生成器产出,见 generated/DocumentFormat.OpenXml/.../schemas_microsoft_com_office_drawing_2018_animation_model3d.g.cs。
八、追加 p:timing 动画时间轴
最后一步,在 SlidePart.RootElement(p:sld)上追加 Timing 元素,构造完整的动画时间轴,让 3D 模型真正“动起来”:
slidePart.RootElement.AppendChild(
new DocumentFormat.OpenXml.Presentation.Timing(
new DocumentFormat.OpenXml.Presentation.TimeNodeList(
new DocumentFormat.OpenXml.Presentation.ParallelTimeNode(
new DocumentFormat.OpenXml.Presentation.CommonTimeNode(
new DocumentFormat.OpenXml.Presentation.ChildTimeNodeList(
new DocumentFormat.OpenXml.Presentation.SequenceTimeNode(
new DocumentFormat.OpenXml.Presentation.CommonTimeNode(
new DocumentFormat.OpenXml.Presentation.ChildTimeNodeList(
new DocumentFormat.OpenXml.Presentation.ParallelTimeNode(
new DocumentFormat.OpenXml.Presentation.CommonTimeNode(
new DocumentFormat.OpenXml.Presentation.StartConditionList(
new DocumentFormat.OpenXml.Presentation.Condition() { Delay = "indefinite" },
new DocumentFormat.OpenXml.Presentation.Condition(
new DocumentFormat.OpenXml.Presentation.TimeNode() { Val = 2 })
{ Event = DocumentFormat.OpenXml.Presentation.TriggerEventValues.OnBegin, Delay = "0" }),
...
new DocumentFormat.OpenXml.Presentation.Animate(
new DocumentFormat.OpenXml.Presentation.CommonBehavior(
new DocumentFormat.OpenXml.Presentation.CommonTimeNode()
{ Id = 6, Duration = "1900", Fill = TimeNodeFillValues.Hold },
new DocumentFormat.OpenXml.Presentation.TargetElement(
new DocumentFormat.OpenXml.Presentation.ShapeTarget() { ShapeId = "2" }),
new DocumentFormat.OpenXml.Presentation.AttributeNameList(
new DocumentFormat.OpenXml.Presentation.AttributeName("embedded1"))),
new DocumentFormat.OpenXml.Presentation.TimeAnimateValueList(
new TimeAnimateValue(
new VariantValue(
new FloatVariantValue() { Val = SingleValue.FromSingle(0) }))
{ Time = "0" },
new TimeAnimateValue(
new VariantValue(
new FloatVariantValue() { Val = SingleValue.FromSingle(1) }))
{ Time = "100000" }))
{ CalculationMode = AnimateBehaviorCalculateModeValues.Linear,
ValueType = AnimateBehaviorValues.Number })
...
{ Id = 4, Fill = TimeNodeFillValues.Hold })
...))
{ Id = 3, Fill = TimeNodeFillValues.Hold })))
{ Id = 2, Duration = "indefinite", NodeType = TimeNodeValues.MainSequence },
...
{ Concurrent = true, NextAction = NextActionValues.Seek }))
{ Id = 1, Duration = "indefinite", Restart = TimeNodeRestartValues.Never, NodeType = TimeNodeValues.TmingRoot }));
时间轴的关键信息可以概括为以下结构树:
p:timing (id=1, dur=indefinite, nodeType=tmingRoot)
└─ p:tnLst
└─ p:par (id=2, dur=indefinite, nodeType=mainSequence, concurrent=true)
└─ p:cTn → p:childTnLst
└─ p:seq (nextAc=seek)
└─ p:cTn (id=3, fill=hold)
└─ p:childTnLst
└─ p:par (id=4, fill=hold)
└─ p:cTn (id=5, presetId=100, presetClass=emphasis,
presetSubtype=1, repeatCount=indefinite,
fill=hold, nodeType=withEffect)
└─ p:childTnLst
└─ p:par → p:cTn (startCond delay=0)
└─ p:childTnLst
└─ p:anim (calcMode=linear, valueType=number)
├─ p:cBhvr → p:cTn (id=6, dur=1900, fill=hold)
│ └─ p:tgtEl → p:spTgt (spid="2")
│ └─ p:attrNameLst → p:attrName "embedded1"
└─ p:tavLst
├─ p:tav (tm=0) → float 0
└─ p:tav (tm=100000) → float 1
要点解读:
- 目标对象:
ShapeTarget.ShapeId = "2"指向第 2 个形状,即刚插入的 3D 模型GraphicFrame(其Id = threeDModelId = 2); - 动画属性:
AttributeName("embedded1")驱动模型内嵌动画的播放进度属性,数值从0(Time = "0")线性过渡到1(Time = "100000",OOXML 时间基准为 1000 单位/秒,即 100000/1000 = 100 秒);CalculationMode = Linear表示线性插值; - 循环与触发:
RepeatCount = "indefinite"让动画无限循环,NodeType = WithEffect、PresetClass = Emphasis表示强调类动画效果; - 主时间线:根节点
nodeType = TmingRoot、主序列nodeType = MainSequence,StartConditionList中Event = OnBegin使动画在幻灯片播放时自动开始,无需点击。
示例中有一行被注释掉的代码 AttributeName.InnerXml = "embedded1"(见 Program.cs),它展示了构造 attrName 元素的另一种方式,正式代码则采用强类型 AttributeName("embedded1") 构造函数,两者效果等价。
九、底层原理:从 Schema 到强类型元素
通过上面的代码可以看出,整个示例几乎完全基于 SDK 的强类型 DOM API,而不是手写 XML。这些强类型类全部由仓库中的源码生成器(DocumentFormat.OpenXml.Generator)从官方 OOXML Schema 自动生成,其元素命名空间、粒子约束与版本可用性都编码在生成代码中。
以 Model3D 为例,在 schemas_microsoft_com_office_drawing_2017_model3d.g.cs 中可以清晰看到:
- 元素限定名:
{http://schemas.microsoft.com/office/drawing/2017/model3d}model3d; - 版本可用性:
builder.Availability = FileFormatVersions.Office2019——这与“动画 3D 模型需要 Office 2019+”的事实一致; - 子元素粒子序列:
spPr → camera → transform → (attributionSourceURL)? → (raster)? → (extLst)? → (objViewport|winViewport) → (ambientLight)? → (一组灯光)?,示例代码的子元素顺序与该序列严格对应。
同样的生成逻辑也覆盖了 a3danim(schemas_microsoft_com_office_drawing_2018_animation_model3d.g.cs)、a16:creationId(Office2016.Drawing)与 p14:modId(Office2010.PowerPoint)等类型。这意味着:只要遵循粒子约束构造元素树,SDK 就能在保存时输出 PowerPoint 认可的合法 OOXML,同时这些元数据也支撑着 SDK 的验证(Validation)与 FileFormatVersions 兼容性检查。
十、运行验证与常见问题
-
运行示例:准备好一个现有
.pptx、一张模型预览.png和一个.glb模型文件,然后执行:dotnet run --project samples/AnimatedModel3DExample/AddAnimatedModel3D.csproj -- demo.pptx model.png model.glb程序会就地修改
demo.pptx,并在第一张幻灯片中插入 3D 模型及其动画。 -
验证结果:用 PowerPoint 2019 及以上版本打开生成的文件,应能看到 3D 模型呈现并自动循环播放动画;若用旧版 PowerPoint 或兼容查看器打开,应显示
mc:Fallback中的静态 PNG 图片。 -
常见问题:
- 文件未找到:命令行参数中的路径不存在时,
CheckIfFilesExist会向标准错误输出file not found: <path>并中止执行; - 旧版 Office 显示空白:说明回退分支(
mc:Fallback)构造不完整或 PNG 部件未正确嵌入,请检查Blip.Embed = pngId与creationId克隆是否一致; - 动画不播放:确认
p:timing的ShapeTarget.ShapeId与GraphicFrame的Id一致,且attrName为embedded1、两个tav关键帧(0/100000)完整; - 版本兼容:3D 模型特性依赖 Office 2019+,代码中涉及的
Office2019.Drawing.Model3D、Office2019.Drawing.Animation.Model3D类型均标记为FileFormatVersions.Office2019,在更早版本的 PowerPoint 中只会显示回退图片。
- 文件未找到:命令行参数中的路径不存在时,
十一、小结
本文从 Open-XML-SDK 仓库的 samples/AnimatedModel3DExample 出发,完整拆解了“向 PowerPoint 插入动画 3D 模型”的全过程:嵌入式嵌入 .glb/.png 两个部件、用 mc:AlternateContent 组织 3D 模型与回退图片两种表达、以 a3danim:embedAnim/posterFrame 挂接动画扩展、最后用 p:timing 构造驱动 embedded1 属性的无限循环时间轴。配合源码生成器产出的强类型元素(如 Model3D、EmbeddedAnimation)以及 Model3DReferenceRelationshipPart.cs 中的 MIME 兼容实现,读者可以在此基础上扩展出:向任意幻灯片插入、自定义相机与光照、调整动画时长与循环次数、批量处理多个模型等更丰富的应用场景。
</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>
【免费下载链接】Open-XML-SDK
Open XML SDK by Microsoft
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



