本文将從 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 解析文檔部落格目錄