本文将从 OpenXML 方面聊 PPT 的动画框架,本文是属于编程方面而不是 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)

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

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 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();
                        }
                    }
                }
            }

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

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();
                        }
                    }
                }
            }

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

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")

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

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 的里面一层

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

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)
            {
                // 点击触发
            }

            // 其他逻辑和主序列相同
           
        }
    }

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

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/post/dotnet-OpenXML-PPT-%E5%8A%A8%E7%94%BB%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

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

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利