dotnet OpenXML PPT 动画框架入门

article/2025/10/25 4:07:51

本文将从 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 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。


http://chatgpt.dhexx.cn/article/9XAG6MtG.shtml

相关文章

java openxml word_【转】【OpenXml】OpenXml操作Word的一些操作总结

OpenXml相对于用MS提供的COM组件来生成WORD,有如下优势: 1.相对于MS 的COM组件,因为版本带来的不兼容问题,及各种会生成WORD半途会崩溃的问题. 2.对比填满一张30多页的WORD来说(包含图,表等),用COM组件来生成会占用20秒,Openxml1秒. 3.MS Word软件太贵了,你的客户装的是开源WOR…

java openxml 操作 word_OpenXML操作word

OpenXML概述 项目中经常需要操作word,之前的方式是采用COM接口,这个接口很不稳定,经常报错。现在开始采用OpenXML。OpenXML(OOXML)是微软在Office 2007中提出的一种新的文档格式,Office 2007中的Word、Excel、PowerPoint默认均采用OpenXML格式 。 OpenXml相对于用MS提供的C…

openXMl

openXMl注意点 一、基础标签 w:pStyle 定义样式ID w:keepNext &#xff1a;段落是否在一个页面上 w:keepLines &#xff1a;此元素指定在页面视图中呈现该文档时&#xff0c;尽可能将该段落的所有行维护在单个页面上 w:widowControl &#xff1a;这个元素指定消费者是否应该通…

OpenXML库(office文档读写库)的安装

本体安装 OpenXml库是由微软维护的一个开源的Office文档读写库&#xff0c;其与其他类似用途的库的比较可以看到这篇文章。 在C#中使用OpenXml非常简单&#xff0c;只需要使用NuGet安装其程序包即可&#xff0c;流程如下(NuGet这东西真的是个神器啊&#xff01;)&#xff1a;…

java e.getmessage() null_Java e.getMessage 错误信息为null

解决方案&#xff1a;用e.printStackTrace() e.getMessage()为null也很正常&#xff0c;NullPointException的getMessage()就为null 我在抛出Exception时也可以直接 throw new MyException(); 这样getMessage()返回的就是null了 这几天做的项目&#xff0c;测试小组说测出来的…

java中getmessage函数_PeekMessage、GetMessage的区别

在Windows编程中经常使用这两个函数来处理消息&#xff0c;它们之间的区别就是GetMessage是阻塞的&#xff0c;PeekMessage是非阻塞的。 GetMessage原型如下&#xff1a;BOOL GetMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax); PeekMessage原型如下&a…

java e.getmessage() null,Java 求助! 为什么我拿不到错误信息,e.getMessage()

Java 求助! 为什么我拿不到错误信息,e.getMessage() Java 求助! 为什么我拿不到错误信息,e.getMessage() 我明明打印了错误信息的啊? e.getMessage() 是空,为什么? 一声声带着惊讶,带着恐慌,带着质疑,带着无奈的话语,从某个角落里传出。 议论纷纷...... 似乎拿不到异…

Java 异常中 e.getMessage() 和 e.toString() e.printStackTrace()的区别常见的几种异常

Java 异常中 e.getMessage() 和 e.toString() e.printStackTrace()的区别 一、概述 在java异常体系中&#xff0c;要打印异常信息&#xff0c;可以通过&#xff1a;e.getMessage() 、 e.toString() e.printStackTrace() 等方法打印出 一些 异常信息。已知的是这些方法都可以打…

GetMessage以及消息循环说明

GetMessage以及消息循环说明 在创建窗口、显示窗口、更新窗口后&#xff0c;我们需要编写一个消息循环&#xff0c;不断地从消息队列中取出消息&#xff0c;并进行响应。要从消息队列中取出消息&#xff0c;我们需要调用GetMessage()函数&#xff0c;该函数的原型声明如下&…

redis使用配置文件的方式启动

文章目录 为什么用配置文件启动呢使用配置文件启动1.创建配置文件2.修改配置文件里面的参数3.以配置文件的方式启动4.查看日志文件 为什么用配置文件启动呢 因为在企业项目中不会只有单个redis实例&#xff0c;每个redis的config配置都可以能不一样&#xff0c;所以这个时候就需…

.NET Core使用Redis

环境Windows 首先安装Redis 1、下载最新版redis&#xff0c;选择.zip则是免安装的版本 下载地址&#xff1a;https://github.com/MicrosoftArchive/redis/releases 2、解压到指定目录&#xff0c;并运行cmd命令 3、在该文件夹下运行命令&#xff1a;redis-server.exe redis.…

flask使用redis

文章目录 前言一、环境二、使用步骤1.安装redis1.下载2.运行 2.安装并使用redis包1.安装2.使用 运行 前言 有人会有疑问&#xff0c;不是已经连上了mysql数据库了吗&#xff0c;为什么还要用redis数据库?有时候用户访问网页的时候&#xff0c;会产生一些临时性的数据&#xf…

最新版本Redis:Redis 7.0 安装使用,编译安装Redis,Redis使用场景-2022年最新Redis图解安装

最新版本Redis&#xff1a;Redis 7.0 安装使用&#xff0c;编译安装Redis&#xff0c;Redis使用场景。 本章主要目标&#xff1a; 1.Redis介绍 2.下载Redis&#xff0c;编译安装Redis 3.启动Redis使用 4.Redis常用数据类型 5.使用场景介绍 下面开始本章内容的学习 1.Redi…

TP5 使用redis

1.打开tp5官方手册&#xff0c;在缓存下面找到使用多个缓存类型 粘贴以下代码到tp框架config下面的缓存设置下 // 切换到redis操作 Cache::store(redis)->set(name,value);//获取redis Cache::store(redis)->get(rename); //实现redis 消息队列 在 /thinkphp/library/t…

C#使用Redis

Redis是一种key-value型数据库与非持久化数据,用来存于频繁操作的数据&#xff0c;与Sqlserver、Mysql等关系型数据库有着很大的区别&#xff0c;如Redis存储的数据结构不相同&#xff0c;还有Redis数据存于内存中&#xff0c;它也与Mongodb、Azure Cosmos Db、Azure TableStor…

springboot使用redis

springboot使用redis redis-service.exe : 服务端,启动后不要关闭 redis-cli.exe : 客户端,访问redis中的数据 redisclient-win32.x86_64.2.0.jar : redis的图形界面客户端,执行方式是在这个文件的目录执行 java -jar redisclient-win32.x86_64.2.0.jar或者在这个jar包的目录…

springboot集成redis使用注解

redis简介&#xff1a; Redis是当前比较热门的NOSQL系统之一&#xff0c;它是一个开源的使用ANSI c语言编写的key-value存储系统 &#xff08;区别于MySQL的二维表格的形式存储。&#xff09;。和Memcache类似&#xff0c;但很大程度补偿了Memcache的不 足。和Memcache一样&am…

Another Redis使用手册

Another Redis DeskTop Manager 跨平台、免费的redis可视化工具&#xff0c;完胜Redis DeskTop Manager。 下载地址&#xff1a;https://github.com/qishibo/AnotherRedisDesktopManager 界面、排版引起极度舒适&#xff0c;只是命令行的入口相对隐蔽&#xff0c;在此截图标出…

Redis使用认证密码登录

Redis默认配置是不需要密码认证的&#xff0c;也就是说只要连接的Redis服务器的host和port正确&#xff0c;就可以连接使用。这在安全性上会有一定的问题&#xff0c;所以需要启用Redis的认证密码&#xff0c;增加Redis服务器的安全性。 1. 修改配置文件 Redis的配置文件默认…

Redis使用密码登录

&#xff08;1&#xff09;进入redis客户端安装目录&#xff1a; &#xff08;2&#xff09;输入命令&#xff1a;redis-cli.exe -h 主机号 -p 端口号 如&#xff1a;redis-cli.exe -h 127.0.0.1-p 6379 &#xff08;3&#xff09;输入密码命令&#xff1a;auth 密码 若显示…