本文将从 OpenXML 方面聊 PPT 的动画框架,本文是属于编程方面而不是 PPT 动画制作教程

开始之前,还请掌握一些基础知识,如阅读以下博客

  • C# dotnet 使用 OpenXml 解析 PPT 文件
  • Office 文档解析 文档格式和协议
  • dotnet OpenXML 解析 PPT 页面元素文档格式

本文不讨论 Slide Master 和 Slide Layout 的动画,关于这两个请参阅 dotnet OpenXML 的 Slide Master 和 Slide Layout 是什么

本文只讨论 Slide 页面里面的动画

元素主序列动画

在 OpenXML 中,如果一个动画是依靠翻页或点击页面进行触发的,那么这些动画有顺序的触发,这部分就是主序列动画,也叫 主动画序列 在 OpenXML 的 PPTX 文件里面的存放大概如下

  <p:timing><p:tnLst><p:par><p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst><p:seq concurrent="1" nextAc="seek"><p:cTn id="2" dur="indefinite" nodeType="mainSeq"></p:cTn></p:seq></p:childTnLst></p:cTn></p:par></p:tnLst></p:timing>

动画是存放在 Slide 页面里面的 Timing 属性里面,通过 OpenXML SDK 获取方法如下

            using var presentationDocument =DocumentFormat.OpenXml.Packaging.PresentationDocument.Open("Test.pptx", false);var presentationPart = presentationDocument.PresentationPart;var slidePart = presentationPart!.SlideParts.First();var slide = slidePart.Slide;var timing = slide.Timing;

默认的动画将会放在 NodeType 为 TmingRoot 的 cTn 也就是 CommonTimeNode 里面,获取代码如下

            var slide = slidePart.Slide;var timing = slide.Timing;// 第一级里面默认只有一项var commonTimeNode = timing?.TimeNodeList?.ParallelTimeNode?.CommonTimeNode;if (commonTimeNode?.NodeType?.Value == TimeNodeValues.TmingRoot){// 这是符合约定// nodeType="tmRoot"}

按照约定,页面里面的动画将放在 TmingRoot 的里层,而元素的主序列动画也属于页面里面的动画,因此也就放在 TmingRoot 的里层

如上面代码就是 nodeType="mainSeq" 主序列动画的定义,获取主序列动画的代码如下

            // <p:timing>//    <p:tnLst>//      <p:par>//        <p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot">// 第一级里面默认只有一项var commonTimeNode = timing?.TimeNodeList?.ParallelTimeNode?.CommonTimeNode;if (commonTimeNode?.NodeType?.Value == TimeNodeValues.TmingRoot){// 这是符合约定// nodeType="tmRoot"}if (commonTimeNode?.ChildTimeNodeList == null) return;// <p:childTnLst>//   <p:seq concurrent="1" nextAc="seek">// 理论上只有一项,而且一定是 SequenceTimeNode 类型var sequenceTimeNode = commonTimeNode.ChildTimeNodeList.GetFirstChild<SequenceTimeNode>();// <p:cTn id="2" dur="indefinite" nodeType="mainSeq">var mainSequenceTimeNode = sequenceTimeNode.CommonTimeNode;if (mainSequenceTimeNode?.NodeType?.Value == TimeNodeValues.MainSequence)

接下来讨论的就是放在主序列动画里面的动画的存储方式,以上代码放在 github 和 gitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 2c06ddf74e45c31ad7842dd06dc09bcc67b6142e

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git

获取代码之后,进入 PptxDemo 文件夹

单个主序列动画

放在主序列动画里面的单个动画,创建方式如新建一个 PPT 文件,然后拖入一个形状,点击一下飞入动画。此时的飞入动画就是属于放在主动画序列的一个动画,当然飞入动画在类型上属于进入动画。在 PPT 里面,有 进入动画、强调动画、退出动画等类型

以下是单个飞入动画的主序列动画的 OpenXML 文档的例子

  <p:timing><p:tnLst><p:par><p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst><p:seq concurrent="1" nextAc="seek"><p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="2" presetClass="entr" presetSubtype="4" fill="hold" grpId="0" nodeType="clickEffect"><!-- 飞入动画 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:seq></p:childTnLst></p:cTn></p:par></p:tnLst></p:timing>

可以看到单个动画放在单个主序列动画的两层 cTn 里面

如上面的内容,大概可以理解存放的方式了,只是在 PPT 里面,有多个 ParallelTimeNode 和 CommonTimeNode 的嵌套。从 mainSeq 也就是 MainSequence 主动画序列以下,获取到的实际的进入动画,是经过了如下路径才能获取

cTn (mainSeq) -> childTnLst -> par -> cTn (id="3") -> childTnLst -> par -> cTn (id="4") -> childTnLst -> par -> cTn (id="5" presetClass="entr" 飞入动画)

代码的获取方式如下

            // <p:cTn id="2" dur="indefinite" nodeType="mainSeq">var mainSequenceTimeNode = sequenceTimeNode.CommonTimeNode;if (mainSequenceTimeNode?.NodeType?.Value == TimeNodeValues.MainSequence){// [TimeLine 对象 (PowerPoint) | Microsoft Docs](https://docs.microsoft.com/zh-cn/office/vba/api/PowerPoint.TimeLine )//  MainSequence 主动画序列var mainParallelTimeNode = mainSequenceTimeNode.ChildTimeNodeList;foreach (var openXmlElement in mainParallelTimeNode){// 并行关系的if (openXmlElement is ParallelTimeNode parallelTimeNode){var timeNode = parallelTimeNode.CommonTimeNode.ChildTimeNodeList.GetFirstChild<ParallelTimeNode>().CommonTimeNode.ChildTimeNodeList.GetFirstChild<ParallelTimeNode>().CommonTimeNode;switch (timeNode.PresetClass.Value){case TimeNodePresetClassValues.Entrance:// 进入动画break;default:throw new ArgumentOutOfRangeException();}}}}

以上测试文件和测试代码 放在 github 和 gitee 可以通过以下命令获取

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin d47f1aec803bfd7adb32e82fb61916308d317fcd

除了进入动画之外,还有强调和退出动画,详细请看 dotnet OpenXML 读取 PPT 动画进入退出强调动画类型

主序列顺序动画

新建 PPT 课件,添加一个元素,然后分别设置元素的进入强调和退出动画,然后设置强调和退出动画是从上一项之后开始,如下图

根据上文描述,可以了解到此时元素的进入和强调和退出类型动画都放在主序列动画里面,如下图

<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><!-- 进入动画--></p:cTn></p:par></p:childTnLst></p:cTn></p:par><p:par><p:cTn id="7" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="8" presetID="25" presetClass="emph" presetSubtype="0" fill="hold" grpId="2" nodeType="clickEffect"><!-- 强调动画--></p:cTn></p:par></p:childTnLst></p:cTn></p:par><p:par><p:cTn id="13" fill="hold"><p:stCondLst><p:cond delay="500" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="14" presetID="10" presetClass="exit" presetSubtype="0" fill="hold" grpId="1" nodeType="afterEffect"><!-- 退出动画--></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

进一步简化的代码如下

<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:childTnLst><p:par><!-- 进入动画--></p:par><p:par><!-- 强调动画--></p:par><p:par><!-- 退出动画--></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

从以上可以看到,所有的动画都放在主序列动画的 childTnLst 也就是 ChildTimeNodeList 里面的里面,在 NodeType 为 MainSequence 的 CommonTimeNode 里面嵌套一个 p:par 和一个 id 为 3 的 p:cTn 之后,才是各个动画的内容

可以使用如下代码进行获取

            // <p:cTn id="2" dur="indefinite" nodeType="mainSeq">var mainSequenceTimeNode = sequenceTimeNode.CommonTimeNode;if (mainSequenceTimeNode?.NodeType?.Value == TimeNodeValues.MainSequence){// <p:childTnLst>// [TimeLine 对象 (PowerPoint) | Microsoft Docs](https://docs.microsoft.com/zh-cn/office/vba/api/PowerPoint.TimeLine )//  MainSequence 主动画序列ChildTimeNodeList mainChildTimeNodeList = mainSequenceTimeNode.ChildTimeNodeList!;// <p:par>var mainParallelTimeNode = mainChildTimeNodeList!.GetFirstChild<ParallelTimeNode>();// <p:cTn id="3" fill="hold">var subCommonTimeNode = mainParallelTimeNode!.CommonTimeNode;// <p:childTnLst>var subChildTimeNodeList = subCommonTimeNode!.ChildTimeNodeList;foreach (var openXmlElement in subChildTimeNodeList!){// 按照顺序获取// <p:par>// <!-- 进入动画-->// </p:par>// <p:par>// <!-- 强调动画-->// </p:par>// <p:par>// <!-- 退出动画-->// </p:par>if (openXmlElement is ParallelTimeNode parallelTimeNode){var timeNode = parallelTimeNode!.CommonTimeNode!.ChildTimeNodeList!.GetFirstChild<ParallelTimeNode>()!.CommonTimeNode;switch (timeNode!.PresetClass!.Value){case TimeNodePresetClassValues.Entrance:// 进入动画break;case TimeNodePresetClassValues.Exit:// 退出动画break;case TimeNodePresetClassValues.Emphasis:// 强调动画break;default:throw new ArgumentOutOfRangeException();}}}}

以上测试文件和测试代码 放在 github 和 gitee 可以通过以下命令获取

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin b0ad5eade0417cebf0df1cac77292df6ef035d1d

如果不是按照顺序连续播放的,而是按照每次点击进行顺序播放的,也就是每个动画的触发都是鼠标点击的,那么存储方式将会是如下

  <p:timing><p:tnLst><p:par><p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst><p:seq concurrent="1" nextAc="seek"><p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><!-- 进入动画--></p:par><p:par><!-- 强调动画--></p:par><p:par><!-- 退出动画--></p:par></p:childTnLst></p:cTn><!-- 忽略代码--></p:seq></p:childTnLst></p:cTn></p:par></p:tnLst><!-- 忽略代码--></p:timing>

来对比一下两个的差别吧,如果是单次点击,连续出现三个动画的,那么这三个动画将会被一个 cTn 包含出来,如下面代码,咱使用以 MainSequence 作为最顶层来看

<!-- 单次点击,连续出现三个动画 -->
<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:childTnLst><!-- 进入动画--><!-- 强调动画--><!-- 退出动画--></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

对比简化的单次点击出现单个动画,顺序点击三次,分别出现三个动画的框架,如以下代码

<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><!-- 进入动画--><!-- 强调动画--><!-- 退出动画--></p:childTnLst>
</p:cTn>

具体差别就在于,如上面代码,如果是单次点击,连续出现三个动画的,那么将会被放在一个 cTn 里面,如上面代码 id 为 3 的 cTn 里面。而如果是单个点击出现单个动画的,动画和动画之间不是连续播放的,那么就放在 MainSequence 的 childTnLst 里面

更多关于主序列进入退出强调动画,请看 dotnet OpenXML 读取 PPT 主序列进入退出强调动画

在了解多个动画的触发顺序和依赖关系之前,咱先继续聊聊单个动画的存储框架

单个动画的存储框架

在本文的一开始就聊到了单个主序列动画,但上文没有给出一个动画的范围,而在经过了主序列顺序动画,似乎可以了解每个独立动画存储的边界以及存储框架方式

假定动画之前没有依赖,单次点击只进行一个动画的,如上文,大的动画存储框架如下代码

<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><!-- 进入动画--><!-- 强调动画--><!-- 退出动画--></p:childTnLst>
</p:cTn>

以上被注释的 进入动画 部分的实际代码大概如下

<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:set><p:cBhvr><p:cTn id="6" dur="1" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="2" /></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="visible" /></p:to></p:set></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par><!-- 强调动画--><!-- 退出动画--></p:childTnLst>
</p:cTn>

也就是单个动画部分内容大概如下

<p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:set><p:cBhvr><p:cTn id="6" dur="1" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="2" /></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="visible" /></p:to></p:set></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn>
</p:par>

忽略动画实际的内容的代码如下

<p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><!-- 忽略动画实际内容 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn>
</p:par>

而通过 id 为 5 的 cTn 可以看到,这个才是实际的动画执行信息,这个 cTn 存放的层级如下

par -> cTn (id="3") -> childTnLst -> par -> cTn (id="4") -> childTnLst -> par -> cTn (id="5")

以上测试课件放在 github 和 gitee 可以通过以下命令获取

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 2c06ddf74e45c31ad7842dd06dc09bcc67b6142e

单个动画内的各个属性以及表示属于什么动画部分,将在下文告诉大家

但如果动画是有依赖的,如单次点击,然后连续出现三个动画的课件,如上文,存储的框架如下

<!-- 单次点击,连续出现三个动画 -->
<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:childTnLst><!-- 进入动画--><!-- 强调动画--><!-- 退出动画--></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

展开里面的进入动画,其内容大概如下

<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:set><p:cBhvr><p:cTn id="6" dur="1" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="2" /></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="visible" /></p:to></p:set></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par><!-- 强调动画--><!-- 退出动画--></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

也就是说进入动画的内容大概如下

<p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:set><p:cBhvr><p:cTn id="6" dur="1" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="2" /></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="visible" /></p:to></p:set></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn>
</p:par>

忽略动画实际的内容的代码如下

<p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><!-- 忽略动画实际内容 --></p:cTn></p:par></p:childTnLst></p:cTn>
</p:par>

对比一下代码可以看到,如单次点击,然后连续出现三个动画的课件,单个动画的距离 MainSequence 的层级要比每次点击只有一个动画的课件少了一层 par -> cTn -> childTnLst 的嵌套

原因是在外层将单次点击,然后连续出现三个动画的三个动画当成了一个主序列的动画。也就是说在 PPT 的存储里面,认为的框架如下

<!-- 单次点击,连续出现三个动画 -->
<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><!-- 有一个动画。这个动画是组合动画,里面包含了三个动画,分别是进入强调和退出的动画 --></p:childTnLst>
</p:cTn>

因此就比每次点击只有一个动画的课件少了一层。通过以上即可了解到,读取时,就应该采用判断组合的方法,将 MainSequence 里面的 childTnLst 的每一个 par 当成独立的动画。只是有一些独立的动画是组合动画,组合动画里面可以再包含多个动画

动画的触发顺序

回到动画的触发顺序,依然是在主序列上,如果是单次点击同时出现三个动画,也就是说第一个动画是点击触发,另外两个动画是设置 从上一项开始 的动画

如上图,三个动画分别是向下动画、不饱和动画、旋转动画。为什么这次不使用进入强调退出做例子?原因是同时进行的动画,如果设置了同时进行,不好调试

从文档的代码可以看到,动画如下

<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><!-- 向下动画 --><!-- 不饱和动画 --><!-- 旋转动画 --></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

展开各个动画的内容如下

<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="42" presetClass="path" presetSubtype="0" accel="50000" decel="50000" fill="hold" grpId="0" nodeType="clickEffect"><!-- 忽略代码 --></p:cTn></p:par><p:par><p:cTn id="7" presetID="25" presetClass="emph" presetSubtype="0" fill="hold" grpId="2" nodeType="withEffect"><!-- 忽略代码 --></p:cTn></p:par><p:par><p:cTn id="12" presetID="8" presetClass="emph" presetSubtype="0" fill="hold" grpId="1" nodeType="withEffect"><!-- 忽略代码 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

从以上代码可以看到,设置动画在从上一项开始的,和从上一项开始之后的动画的存储框架是不相同的,下面对比一下两个设置方式的代码

<!-- 单次点击,连续出现三个动画 -->
<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:childTnLst><!-- 进入动画--><!-- 强调动画--><!-- 退出动画--></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn><!-- 单次点击,同时出现三个动画 -->
<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:childTnLst><!-- 向下动画 --><!-- 不饱和动画 --><!-- 旋转动画 --></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

可以看到,如果设置的动画是同时出现的,将会被放入到 MainSequence 的里面两层,而如果是设置顺序出现的动画,将会被放入 MainSequence 的里面一层

以上测试课件放在 github 和 gitee 可以通过以下命令获取

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 5e241a0eaf6c560698bcef33e8884d72a4f2d724

主序列动画框架

主序列动画的顺序上,可以分为以下不同的方式

  • 动画之间是相互不影响,每个动画通过点击触发的方式,如 三次点击触发三次动画

  • 动画之间相互影响,动画连续触发,在一个动画执行完成之后,再继续下一个动画,如 单次点击连续触发三个动画

  • 动画之间相互影响,动画同时触发,在点击之后所有动画同时进行,如 单次点击同时触发三个动画

更复杂的部分是以上三个组合的复杂情况,咱先忽略复杂的组合情况,先聊以上的方式

下面是三个方式的框架对比

<!-- 三次点击触发三次动画 -->
<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><!-- 进入动画 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par><p:par><p:cTn id="7" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="8" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="9" presetID="25" presetClass="emph" presetSubtype="0" fill="hold" grpId="2" nodeType="clickEffect"><!-- 强调动画 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par><p:par><p:cTn id="14" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="15" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="16" presetID="10" presetClass="exit" presetSubtype="0" fill="hold" grpId="1" nodeType="clickEffect"><!-- 退出动画 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn><!-- 单次点击连续触发三个动画 -->
<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><!-- 进入动画 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par><p:par><p:cTn id="7" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="8" presetID="25" presetClass="emph" presetSubtype="0" fill="hold" grpId="2" nodeType="afterEffect"><!-- 强调动画 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par><p:par><p:cTn id="13" fill="hold"><p:stCondLst><p:cond delay="500" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="14" presetID="10" presetClass="exit" presetSubtype="0" fill="hold" grpId="1" nodeType="afterEffect"><!-- 退出动画 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn><!-- 单次点击同时触发三个动画 -->
<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:stCondLst><p:cond delay="0" /></p:stCondLst><p:childTnLst><p:par><p:cTn id="5" presetID="42" presetClass="path" presetSubtype="0" accel="50000" decel="50000" fill="hold" grpId="0" nodeType="clickEffect"><!-- 向下动画 --></p:cTn></p:par><p:par><p:cTn id="7" presetID="25" presetClass="emph" presetSubtype="0" fill="hold" grpId="2" nodeType="withEffect"><!-- 不饱和动画 --></p:cTn></p:par><p:par><p:cTn id="12" presetID="8" presetClass="emph" presetSubtype="0" fill="hold" grpId="1" nodeType="withEffect"><!-- 旋转动画 --></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

可以看到不同的动画触发方式将会影响动画的存储框架

触发序列框架

在 PPT 里面,除了主序列动画之后,还有触发序列。触发序列是由点击某个元素进行触发的动画

触发序列和主序列如命名,在 TmingRoot 之下的一层,和主序列相同的一层里面,采用 InteractiveSequence 标识。在 OpenXML 里面的文档内容大概如下

        <p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst><p:seq concurrent="1" nextAc="seek"><p:cTn id="2" restart="whenNotActive" fill="hold" evtFilter="cancelBubble" nodeType="interactiveSeq"></p:cTn></p:seq></p:childTnLst></p:cTn>

在 InteractiveSequence 之下的元素存储框架和主序列完全相同,只是在触发序列里面的各个动画会采用 stCondLst 里面的 TargetElement 来决定是由哪个元素点击触发的动画

如以下的 OpenXML 内容

<p:cTn id="2" restart="whenNotActive" fill="hold" evtFilter="cancelBubble" nodeType="interactiveSeq"><p:stCondLst><!-- 通过 onClick 表示是点击的时候触发 --><p:cond evt="onClick" delay="0"><p:tgtEl><!-- 决定由点击哪个元素来触发动画 --><p:spTgt spid="3" /> </p:tgtEl></p:cond></p:stCondLst><p:childTnLst><p:par><p:cTn id="3" fill="hold"><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:childTnLst><p:par><p:cTn id="5" presetID="10" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><p:childTnLst><p:set><p:cBhvr><p:cTn id="6" dur="1" fill="hold"></p:cTn><p:tgtEl><p:spTgt spid="4" /></p:tgtEl></p:set></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn>

以上的逻辑表示了采用 p:spTgt spid="3" 的元素来触发 p:spTgt spid="4" 的元素动画

获取触发序列的逻辑如下

using System.Linq;
using DocumentFormat.OpenXml.Presentation;
using NonVisualDrawingProperties = DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties;
using NonVisualShapeProperties = DocumentFormat.OpenXml.Presentation.NonVisualShapeProperties;using var presentationDocument =DocumentFormat.OpenXml.Packaging.PresentationDocument.Open("Test.pptx", false);
var presentationPart = presentationDocument.PresentationPart;
var slidePart = presentationPart!.SlideParts.First();
var slide = slidePart.Slide;
var timing = slide.Timing;
// 第一级里面默认只有一项
var commonTimeNode = timing?.TimeNodeList?.ParallelTimeNode?.CommonTimeNode;
if (commonTimeNode?.NodeType?.Value == TimeNodeValues.TmingRoot)
{// 这是符合约定// nodeType="tmRoot"
}if (commonTimeNode?.ChildTimeNodeList == null) return;
// 理论上只有一项,而且一定是 SequenceTimeNode 类型
var sequenceTimeNode = commonTimeNode.ChildTimeNodeList.GetFirstChild<SequenceTimeNode>();
var interactiveSequenceTimeNode = sequenceTimeNode.CommonTimeNode;
if (interactiveSequenceTimeNode?.NodeType?.Value == TimeNodeValues.InteractiveSequence)
{}

在触发序列里面,获取触发动画元素的方法如下

    // [TimeLine 对象 (PowerPoint) | Microsoft Docs](https://docs.microsoft.com/zh-cn/office/vba/api/PowerPoint.TimeLine )// 触发动画序列// 获取触发动画的元素var condition = interactiveSequenceTimeNode.StartConditionList.GetFirstChild<Condition>();if (condition.Event.Value == TriggerEventValues.OnClick){// 点击触发动画,还有其他的方式}var targetElement = condition.TargetElement;var shapeId = targetElement.ShapeTarget.ShapeId.Value;var shape = slide.CommonSlideData.ShapeTree.FirstOrDefault(t =>t.GetFirstChild<NonVisualShapeProperties>()?.GetFirstChild<NonVisualDrawingProperties>()?.Id?.Value.ToString() == shapeId);// 由 shape 点击触发的动画

以上拿到的 shape 就是用来触发动画的元素

接下来获取具体的动画逻辑和主序列相同

    foreach (var openXmlElement in interactiveSequenceTimeNode.ChildTimeNodeList){// 并行关系的if (openXmlElement is ParallelTimeNode parallelTimeNode){var timeNode = parallelTimeNode.CommonTimeNode.ChildTimeNodeList.GetFirstChild<ParallelTimeNode>().CommonTimeNode.ChildTimeNodeList.GetFirstChild<ParallelTimeNode>().CommonTimeNode;if (timeNode.NodeType.Value == TimeNodeValues.ClickEffect){// 点击触发}// 其他逻辑和主序列相同}}

以上测试课件和代码放在 github 和 gitee 可以通过以下命令获取

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin e48a633377bb933ad09e3782272b0a01ffd42ab5

对于触发序列,如果是通过相同的一个动画触发的多个动画,那么多个动画的存放是放在相同的触发序列之下。和主序列的不同在于,在 PPT 可以有多个触发序列。每个触发序列表示有一个元素触发的动画。每个触发序列里面,触发动画的元素触发的动画允许有多个

如多次点击相同的一个元素来分别触发三个元素的淡入动画的 OpenXML 文档

<p:cTn id="2" restart="whenNotActive" fill="hold" evtFilter="cancelBubble" nodeType="interactiveSeq"><p:stCondLst><p:cond evt="onClick" delay="0"><p:tgtEl><!-- 由 Id 是 3 的元素触发动画 --><p:spTgt spid="3" /></p:tgtEl></p:cond></p:stCondLst><p:childTnLst><!-- 第一个元素的淡出动画 --><p:par><p:cTn id="3" fill="hold"><p:childTnLst><p:par><p:cTn id="4" fill="hold"><p:childTnLst><p:par><p:cTn id="5" presetID="10" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><p:childTnLst><p:set><p:cBhvr><p:tgtEl><!-- 第一个动画的 Id 是 4 的元素 --><p:spTgt spid="4" /></p:tgtEl></p:cBhvr></p:set></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par><!-- 第二个元素的淡出动画 --><p:par><p:cTn id="8" fill="hold"><p:childTnLst><p:par><p:cTn id="9" fill="hold"><p:childTnLst><p:par><p:cTn id="10" presetID="10" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><p:childTnLst><p:set><p:cBhvr><p:tgtEl><!-- 第二个动画的 Id 是 5 的元素 --><p:spTgt spid="5" /></p:tgtEl></p:cBhvr></p:set></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par><!-- 第三个元素的淡出动画 --><p:par><p:cTn id="13" fill="hold"><p:childTnLst><p:par><p:cTn id="14" fill="hold"><p:childTnLst><p:par><p:cTn id="15" presetID="10" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect"><p:childTnLst><p:set><p:cBhvr><p:tgtEl><!-- 第三个动画的 Id 是 6 的元素 --><p:spTgt spid="6" /></p:tgtEl></p:cBhvr></p:set></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par></p:childTnLst>
</p:cTn>

以上文档就是点击 Id 是 3 的元素分别触发 Id 是 4 5 6 元素的淡入动画。对元素 Id 是 4 5 6 的元素的 NodeType 是 ClickEffect 因此是多次点击 Id 是 3 的元素进行分别触发

本文的属性是依靠 dotnet OpenXML 解压缩文档为文件夹工具 工具协助测试的,这个工具是开源免费的工具,欢迎使用

更多请看 Office 使用 OpenXML SDK 解析文档博客目录

我搭建了自己的博客 https://blog.lindexi.com/ 欢迎大家访问,里面有很多新的博客。只有在我看到博客写成熟之后才会放在csdn或博客园,但是一旦发布了就不再更新

如果在博客看到有任何不懂的,欢迎交流,我搭建了 dotnet 职业技术学院 欢迎大家加入

如有不方便在博客评论的问题,可以加我 QQ 2844808902 交流


本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

dotnet OpenXML PPT 动画框架入门相关推荐

  1. Lottie动画框架入门及源码简析

    现在越来越多的APP中添加动画来提升用户体验,下面简单介绍下Airbnb开源的动画框架Lottie的使用 一.基本使用 首先添加依赖 compile 'com.airbnb.android:lotti ...

  2. PPT学习整理(八)PPT动画全入门

    学习来源:B站资源https://www.bilibili.com/video/BV1w54y1Q7cZ?p=13&spm_id_from=pageDriver 动画:元素动画.组合动画.切换 ...

  3. dotnet OpenXML 读取 PPT 主序列进入退出强调动画

    本文告诉大家如何读取 PPT 文件里面,放在主动画序列 MainSequence 的进入和退出和强调的动画,和在 OpenXML 里面的存放方式 如以下的课件内容,给一个元素添加了进入强调退出的动画, ...

  4. dotnet OpenXML 读取 PPT 内嵌 ole 格式 Excel 表格的信息

    在 Office 中,可以在 PPT 里面插入表格,插入表格有好多不同的方法,对应 OpenXML 文档存储的更多不同的方式.本文来介绍如何读取 PPT 内嵌 ole 格式的 xls+ 表格的方法 在 ...

  5. dotnet OpenXML 读取 PPT 形状边框定义在 Style 的颜色画刷

    本文来和大家聊聊在 PPT 形状使用了 Style 样式的颜色画刷读取方法 在开始之前,期望大家已了解如何在 dotnet 应用里面读取 PPT 文件,如果还不了解读取方法,请参阅 C# dotnet ...

  6. Newbe.Claptrap 框架入门,第二步 —— 创建项目

    接上一篇 <Newbe.Claptrap 框架入门,第一步 -- 开发环境准备>,我们继续了解如何创建一个 Newbe.Claptrap 项目. 安装项目模板 打开控制台运行以下命令来安装 ...

  7. Android 动画框架详解

    Android 动画框架详解 基本原理 朱 韦伟, 软件工程师, IBM 李 浩, 软件工程师, 爱格码 简介: Android 平台提供了一套完整的动画框架,使得开发者可以用它来开发各种动画效果.A ...

  8. Taro 框架入门教学视频

    Taro 框架入门教学视频 千锋教育出品这个教程 https://www.bilibili.com/video/BV1W7411v7LU ,比系统学习文档更快捷(文档可能很多用不上)2020 年初,以 ...

  9. 【笔记-node】《Egg.js框架入门与实战》、《用 React+React Hook+Egg 造轮子 全栈开发旅游电商应用》

    20210226-20210227:<Egg.js框架入门与实战> 课程地址:https://www.imooc.com/learn/1185 第一章 课程导学 01-01 课程介绍 一. ...

  10. 『Scrapy』爬虫框架入门

    框架结构 引擎:处于中央位置协调工作的模块 spiders:生成需求url直接处理响应的单元 调度器:生成url队列(包括去重等) 下载器:直接和互联网打交道的单元 管道:持久化存储的单元 框架安装 ...

最新文章

  1. linux c内核开发,嵌入式uClinux的内核结构和开发环境
  2. JS的表单序列化,数组去重,判断数组是否重复等方法
  3. Google Colab使用详细教程
  4. C语言 输入一个正整数n,再输入n个字符,如果是小写字符就将其转换为大写字符,如果是大写字符就 转换为小写字符
  5. Java中的查找树和哈希表(一级)
  6. icd植入是大手术吗_白内障手术为何要植入人工晶体?便宜的人工晶体会影响视力吗?...
  7. 征途2无法显示服务器列表,解决win10系统玩征途2提示“DriverCommlnit驱动加载失败”的方法...
  8. java 内存机制(堆和栈),内存地址
  9. ffmpeg.c函数结构简单分析(画图)
  10. spring cloud 学习笔记(1)
  11. uni-app 开发微信小程序,网络断开,无网络情况,刷新检查,重新加载
  12. win10用linux命令关机,Win10使用PowerShell命令让局域网电脑重启关机操作
  13. CUDA中的Warp Shuffle
  14. 表白神器!程序员七夕情人节源码合集
  15. 怎么打开华硕电脑计算机功能,华硕笔记本小键盘怎么开(笔记本电脑虚拟键盘怎么打开)...
  16. 编译原理实验2(1)——自上而下语法分析
  17. python做运动控制_用 Python 写一个跟踪运动对象系统
  18. linux md文件 编辑,用Vim写md文档的简单姿势
  19. 安卓打开rpm文件_android打开文件及打开方式(打开程序列表) | 学步园
  20. 什么样的程序员才是牛逼的程序员

热门文章

  1. 晶振的匹配电容计算公式
  2. EFR32 晶振电容设置
  3. word2016添加题注|图注文献标号的交叉引用及引用的更新|添加不同类型的页码|文献自动编号|文献编号的自动引用|删除空白页
  4. (rec)机器人中的语音讲话TTS引擎
  5. H5 打开微信小程序 公众号
  6. 1999-2018年地级市经济增长数据(GDP、人均GDP、各产业产值占比等)
  7. 蓝桥杯质数的后代码c语言,质因数
  8. (六)Ps剪切蒙版/图框
  9. 【单例模式、多例模式、工厂模式和枚举】
  10. 黑马程序员_Java基础_枚举 和 单例模式实例