1、await和.result/ .getwaiter() .getresult()的區別 await:Task.Run裡面的邏輯是新開的線程去執行的,await Task.Run後面邏輯都在新開的線程去執行。 private async void MainWindow_Loaded(object ...
本文告訴大家如何使用 OpenXML 解析 PPT 的圖表,以面積圖為入門例子告訴大家 OpenXML 的存儲
在 PPT 裡面,有強大的圖表功能,可以聯動 Excel 展示數據。在 PPT 裡面的圖表和 Excel 的圖表稍微有一些差別,本文只聊 PPT 的圖表
如下圖是本文將作為例子的圖表
對應的數據如圖
如上圖可以看到在 PPT 裡面的圖表是可以使用 Excel 的數據,將 Excel 文件內嵌到 PPT 裡面。但這不代表要解析圖表的數據就一定需要先瞭解 Excel 的內容,本文將繞過對 Excel 的任何讀取,通過 PPT 裡面的內容拿到圖表的數據
圖表的組成
開始之前,還請先讓我告訴大家一個圖表元素包含的基礎組件部分,也就是圖表元素由哪些部分組成
橫坐標軸 類別坐標軸數據
對於面積圖來說,預設的面積圖的橫坐標就是類別的坐標軸數據,對應的 Excel 表格的第一列的內容,也就是 A B C D E 這些數據
在 OpenXML SDK 裡面,採用 DocumentFormat.OpenXml.Drawing.Charts.CategoryAxisData
存放
本文以下將會告訴大家獲取方法,這裡只是寫上類型,方便大家瞭解
縱坐標軸
對於預設面積圖來說,縱坐標屬於一個運行時屬性,不會存放在 OpenXML 文檔裡面,需要根據每個系列的數值的最大值和最小值以及配置,計算出來縱坐標的內容,本文不會涉及具體的坐標軸計算方法
數據系列
在圖表裡面有數據系列的概念,每個系列的數據組成一個個的數據系列。對於大部分圖表來說,數據層都是由一個個數據系列組成的
每個數據系列可以有自己的系列名稱
系列名稱大部分時候都放在圖例裡面,也就是圖例裡面的內容就是由系列名稱提供的
在 OpenXML SDK 裡面,採用 DocumentFormat.OpenXml.Drawing.Charts.SeriesText
存放
在圖表裡面,核心就是對數據的處理,系列的數據內容就是核心的
如圖,面積圖有兩個數據系列,通過上面的 Excel 內容可以瞭解到兩個系列的數據分別如下
系列 1:32,32,28,12,15
系列 2:12,12,12,21,28
本文將重點告訴大家如何解析圖表的數據
效果
以下是本文的解析效果,可以解析出來圖表的類別坐標軸數據,和各個系列的系列名稱和系列數據
下麵將告訴大家如何根據 OpenXML SDK 提供的方法讀取到圖表的內容
讀取圖表
在開始之前,還請大家先瞭解 OpenXml 讀取 PPT 的基礎。本文將在 C# dotnet 使用 OpenXml 解析 PPT 文件 的基礎上進行開發
先讀取 PPT 文檔
var file = new FileInfo("Test.pptx");
using var presentationDocument = PresentationDocument.Open(file.FullName, false);
本文的測試文件和所有代碼都可以在本文最後獲取
在這份 Test.pptx 的圖表是放在第一個頁面,先獲取頁面,通過頁面的元素獲取到圖表
var slide = presentationDocument.PresentationPart!.SlideParts.First().Slide;
在 OpenXML 裡面的頁面存放的圖表的代碼如下
<p:cSld>
<p:spTree>
<p:graphicFrame>
...
</p:graphicFrame>
</p:spTree>
</p:cSld>
圖表也是一個元素,放在 SharpTree (p:spTree) 裡面,作為 GraphicFrame (p:graphicFrame) 存放。但不能說 GraphicFrame 就是圖表元素,在 OpenXML 的 GraphicFrame 是一個很通用的元素,如 OLE 元素或公式都會用到此元素
讀取 GraphicFrame 的內容,如果能讀取到 ChartReference (c:chart) 那就證明這個元素是圖表元素
// 獲取圖表元素,在這份課件里,有一個面積圖。以下使用 First 忽略細節,獲取圖表
var graphicFrame = slide.Descendants<GraphicFrame>().First();
// 獲取到對應的圖表信息,圖表是引用的,內容不是放在 Slide 頁面裡面,而是放在獨立的圖表 xml 文件里
var graphic = graphicFrame.Graphic;
var graphicData = graphic?.GraphicData;
var chartReference = graphicData?.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.ChartReference>();
在 OpenXML 里,圖表是引用的,內容不是放在 Slide 頁面裡面,而是放在獨立的圖表 xml 文件里。頁面的代碼如下
<p:graphicFrame>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart">
<c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="rId2" />
</a:graphicData>
</a:graphic>
</p:graphicFrame>
根據 dotnet OpenXML 為什麼資源使用 Relationship 引用 可以瞭解到,這裡的圖表引用,需要到 rels 文件裡面獲取關聯的內容。在 OpenXml SDK 里,封裝好了獲取方法,獲取時需要有兩個參數,一個是 id 另一個是去哪裡獲取的 Part 內容。獲取 id 的方法如下
// 獲取到 id 也就是 `r:id="rId2"` 根據 Relationship 的描述,可以知道去 rels 文件裡面獲取關聯的內容。在 OpenXml SDK 里,封裝好了獲取方法,獲取時需要有兩個參數,一個是 id 另一個是去哪裡獲取的 Part 內容
var id = chartReference?.Id?.Value;
在這份課件是圖表元素放在頁面上,可以通過頁面去獲取到圖表元素的存儲。在實際項目里,需要判斷圖表元素所在的是頁面還是頁面模版等,不能和以下代碼寫固定從頁面獲取
// 如果是放在模版裡面,記得要用模版的 Part 去獲取
var currentPart = slide.SlidePart!;
if (!currentPart.TryGetPartById(id!, out var openXmlPart))
{
// 在這份課件里,一定不會進入此分支
// 一定能從頁面找到對應的資源內容也就是圖表
return;
}
這裡拿到的 openXmlPart
是 ChartPart 對象,這裡面就存放了圖表的信息
if (openXmlPart is not ChartPart chartPart)
{
// 這裡拿到的一定是 ChartPart 對象,一定不會進入此分支。但是在實際項目的代碼,還是要做這個判斷
return;
}
這裡的 ChartPart 對應的就是 charts\chartN.xml 文件。這裡的 chartN.xml 表示的是 chart1.xml 或 chart2.xml 等文件
這個文件的存儲內容大概如下
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<c:chartSpace>
<c:chart>
...
<c:plotArea>
...
</c:plotArea>
</c:chart>
</c:chartSpace>
讀取圖表首先需要獲取 ChartSpace 對象,再獲取到 Chart 對象。在 OpenXML SDK 裡面,定義了很多個 Chart 類型,放在不同的命名空間,在獲取時,推薦寫全命名空間
using Chart = DocumentFormat.OpenXml.Drawing.Charts.Chart;
// 這裡的 ChartPart 對應的就是 charts\chartN.xml 文件。這裡的 chartN.xml 表示的是 chart1.xml 或 chart2.xml 等文件
var chartSpace = chartPart.ChartSpace;
// 這裡的 Chart 是 DocumentFormat.OpenXml.Drawing.Charts.Chart 類型,在 OpenXmlSDK 裡面,有多個同名的 Chart 類型,還請看具體的命名空間
/*
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<c:chartSpace>
<c:chart>
...
<c:plotArea>
...
</c:plotArea>
</c:chart>
</c:chartSpace>
*/
var chart = chartSpace.GetFirstChild<Chart>();
接著獲取 PlotArea 對象,這裡面就存放了圖表的內容
using PlotArea = DocumentFormat.OpenXml.Drawing.Charts.PlotArea;
var chart = chartSpace.GetFirstChild<Chart>();
var plotArea = chart?.GetFirstChild<PlotArea>();
如本文的面積圖就放在 PlotArea 元素里
<c:plotArea>
<c:areaChart>
...
</c:areaChart>
</c:plotArea>
在 Chart 里,有不同的圖表類型,例如 BarChart Bar3DChart LineChart PieChart Pie3DChart OfPieChart 不水字數了,就是很多不同的圖表。本文這裡只獲取面積圖
var areaChart = plotArea?.GetFirstChild<AreaChart>();
if (areaChart == null)
{
// 在這份課件里,一定存在面積圖,一定不會進入此分支
return;
}
獲取到面積圖,接下來就是讀取面積圖的數據系列
數據系列的存儲代碼如下
<c:plotArea>
<c:areaChart>
<c:ser>
...
</c:ser>
<c:ser>
...
</c:ser>
</c:areaChart>
</c:plotArea>
每個 DocumentFormat.OpenXml.Drawing.Charts.AreaChartSeries (c:ser) 就是一個系列的內容。一個圖表裡面可以有多個系列,每個系列包含下麵數據
- 系列名
- 系列數據
- 類別軸上的數據
- 樣式信息
樣式信息裡面包含了填充的畫刷,如純色填充。類別軸上的數據是面積圖橫坐標軸顯示內容,每個系列都有,這是重覆的數據,在 PPT 里,只取第一個系列的數據
數據系列里的橫坐標軸的類別坐標軸數據,在 OpenXML 裡面,是 DocumentFormat.OpenXml.Drawing.Charts.CategoryAxisData
類型,對應 c:cat
的內容
讀取類別軸上的數據方法如下
foreach (var areaChartChildElement in areaChart.ChildElements)
{
// 獲取系列
/*
<c:plotArea>
<c:areaChart>
<c:ser>
...
</c:ser>
<c:ser>
...
</c:ser>
</c:areaChart>
</c:plotArea>
*/
if (areaChartChildElement is DocumentFormat.OpenXml.Drawing.Charts.AreaChartSeries areaChartSeries)
{
// 類別軸上的數據 橫坐標軸上的數據
var categoryAxisData = areaChartSeries.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.CategoryAxisData>()!;
}
}
在 OpenXML SDK 的存儲如下
<c:plotArea>
<c:areaChart>
<c:ser>
<c:cat>
</c:cat>
</c:ser>
</c:areaChart>
</c:plotArea>
在類別軸上的數據存放的是數據引用,數據引用在 OpenXML 裡面有多個不同的存儲類型。例如 NumberReference 類型表示的是數值引用,例如 StringReference 表示字元串引用類型,在這份課件裡面存放的是 StringReference 類型,以下代碼只演示採用 StringReference 類型的讀取方式,還請在具體項目,自行判斷
var categoryAxisData = areaChartSeries.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.CategoryAxisData>()!;
var categoryAxisDataStringReference = categoryAxisData.GetFirstChild<StringReference>();
在 StringReference 裡面,大部分都有兩個部分,一個是公式,表示如何引用 Excel 的數據。通過公式讀取 Excel 可以獲取到正確的數據,但缺點是比較複雜。可以通過第二部分,也就是緩存數據部分讀取,雖然讀取緩存也許不對,不過優點在於簡單
存儲的代碼如下
<c:cat>
<c:strRef>
<c:f>Sheet1!$A$2:$A$6</c:f>
<c:strCache>
<c:ptCount val="5" />
<c:pt idx="0">
<c:v>A</c:v>
</c:pt>
<c:pt idx="1">
<c:v>B</c:v>
</c:pt>
<c:pt idx="2">
<c:v>C</c:v>
</c:pt>
<c:pt idx="3">
<c:v>D</c:v>
</c:pt>
<c:pt idx="4">
<c:v>E</c:v>
</c:pt>
</c:strCache>
</c:strRef>
</c:cat>
獲取公式的代碼如下
var categoryAxisDataStringReference = categoryAxisData.GetFirstChild<StringReference>();
if (categoryAxisDataStringReference != null)
{
// 這個公式表示是從 Excel 哪個數據獲取的,獲取的方式比較複雜。這裡還是先從緩存獲取
var categoryAxisDataFormula = categoryAxisDataStringReference.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.Formula>();
}
讀取緩存的方法如下
// 讀取緩存
var categoryAxisDataStringCache = categoryAxisDataStringReference.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.StringCache>()!;
讀取類別軸上的數據
var list = new List<string>();
foreach (var stringPoint in categoryAxisDataStringCache.Elements<DocumentFormat.OpenXml.Drawing.Charts.StringPoint>())
{
// 以下的 類別軸上的數據 橫坐標軸上的數據,各個列項的名稱
// 對於面積圖來說,多個系列的列項都是相同的。儘管在 OpenXml 存儲裡面存放了兩份,但以第零個系列的為準
var text = stringPoint.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.NumericValue>()!.Text;
list.Add(text);
}
上面代碼的 list 就存放了讀取類別軸上的數據,也就是 A B C D E 字元串
繼續讀取第二部分內容,系列的系列名稱,也就是系列標題
系列標題在 OpenXML 里,使用 DocumentFormat.OpenXml.Drawing.Charts.SeriesText
表示,對應 c:tx
類型。在圖表裡面的數據大部分都採用引用的方式,引用裡面基本都有兩個部分,如 類別軸上的數據 有引用 Excel 的公式,和緩存
這裡讀取系列標題也是通過緩存讀取,不會去解析 Excel 內容
// 獲取系列標題,放心,可以不讀取 Excel 的內容,通過緩存內容即可。但是緩存內容也許和 Excel 內容不對應
/*
<c:plotArea>
<c:areaChart>
<c:ser>
<c:tx>
<c:strRef>
<c:f>Sheet1!$B$1</c:f>
<c:strCache>
<c:ptCount val="1" />
<c:pt idx="0">
<c:v>系列 1</c:v>
</c:pt>
</c:strCache>
</c:strRef>
</c:tx>
...
</c:ser>
</c:areaChart>
</c:plotArea>
*/
var seriesText = areaChartSeries.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.SeriesText>()!;
var seriesTextStringReference = seriesText.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.StringReference>()!;
// 這個公式表示是從 Excel 哪個數據獲取的,獲取的方式比較複雜。這裡還是先從緩存獲取
var seriesTextFormula = seriesTextStringReference.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.Formula>();
使用緩存獲取系列名稱
// 有緩存的話,從緩存獲取就可以,緩存內容也許和 Excel 內容不對應
/*
<c:strCache>
<c:ptCount val="1" />
<c:pt idx="0">
<c:v>系列 1</c:v>
</c:pt>
</c:strCache>
*/
var seriesTextStringCache = seriesTextStringReference.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.StringCache>();
if (seriesTextStringCache != null)
{
var seriesTextStringPoint = seriesTextStringCache.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.StringPoint>();
var numericValue = seriesTextStringPoint!.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.NumericValue>();
// 系列1 標題
var title = numericValue!.Text;
}
上面的 title
就是系列的標題,如上面圖表,拿到的就是 系列1
或 系列2
字元串
完成獲取系列的標題獲取,下麵開始獲取系列的樣式。系列的樣式如系列的填充畫刷,畫刷是一個比較大的話題,本文使用的例子只用到純色畫刷
圖表的系列樣式存儲採用的是 DocumentFormat.OpenXml.Drawing.Charts.ChartShapeProperties
類型,圖表的形狀屬性的內容和 形狀屬性 的內容是差不多的
<c:plotArea>
<c:areaChart>
<c:ser>
<c:tx>
...
</c:tx>
<c:spPr>
<a:solidFill>
<a:srgbClr val="FF0000" />
</a:solidFill>
</c:spPr>
</c:ser>
</c:areaChart>
</c:plotArea>
獲取系列的填充顏色
// 圖表的形狀屬性的內容和 形狀屬性 的內容是差不多的
/*
<c:plotArea>
<c:areaChart>
<c:ser>
<c:tx>
...
</c:tx>
<c:spPr>
<a:solidFill>
<a:srgbClr val="FF0000" />
</a:solidFill>
</c:spPr>
</c:ser>
</c:areaChart>
</c:plotArea>
*/
var chartShapeProperties = areaChartSeries.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.ChartShapeProperties>()!;
// 獲取畫刷,畫刷有好多不同的類型,這個課件只用了純色
var solidFill = chartShapeProperties.GetFirstChild<SolidFill>()!;
// 畫刷純色顏色有很多個顏色表示方法,這個課件只用了 RGB 的純色
var rgbColorModelHex = solidFill.GetFirstChild<DocumentFormat.OpenXml.Drawing.RgbColorModelHex>()!;
// 這就是這個系列的顏色
var colorValue = rgbColorModelHex.Val!.Value;
以上的 colorValue
就是這個系列的填充。不同的系列可以有不同的填充
接下來獲取圖表最核心的內容,系列的數據
在 PPT 裡面,是允許數據為空的,如果是空,行為就是不繪製系列內容。本文使用的例子是存在數據,就沒有判斷數據為空
// 獲取系列的值
/*
<c:plotArea>
<c:areaChart>
<c:ser>
<c:tx>
...
</c:tx>
<c:cat>
...
</c:cat>
<c:val>
<c:numRef>
<c:f>Sheet1!$B$2:$B$6</c:f>
<c:numCache>
<c:formatCode>General</c:formatCode>
<c:ptCount val="5" />
<c:pt idx="0">
<c:v>32</c:v>
</c:pt>
<c:pt idx="1">
<c:v>32</c:v>
</c:pt>
<c:pt idx="2">
<c:v>28</c:v>
</c:pt>
<c:pt idx="3">
<c:v>12</c:v>
</c:pt>
<c:pt idx="4">
<c:v>15</c:v>
</c:pt>
</c:numCache>
</c:numRef>
</c:val>
</c:ser>
<c:ser>
...
</c:ser>
</c:areaChart>
</c:plotArea>
*/
// 這就是系列裡面最重要的數據。然而在 PPT 裡面,是允許為空的,如果是空,行為就是不繪製系列內容
var valueList = new List<string>();
var values = areaChartSeries.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.Values>();
在面積圖,數據理論上是數值類型。對應的是 NumberReference 引用,同樣可以使用公式引用 Excel 數據,也可以採用緩存獲取
var valuesNumberReference = values?.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.NumberReference>();
if (valuesNumberReference != null)
{
/*
<c:val>
<c:numRef>
<c:f>Sheet1!$B$2:$B$6</c:f>
<c:numCache>
<c:formatCode>General</c:formatCode>
<c:ptCount val="5" />
<c:pt idx="0">
<c:v>32</c:v>
</c:pt>
<c:pt idx="1">
<c:v>32</c:v>
</c:pt>
<c:pt idx="2">
<c:v>28</c:v>
</c:pt>
<c:pt idx="3">
<c:v>12</c:v>
</c:pt>
<c:pt idx="4">
<c:v>15</c:v>
</c:pt>
</c:numCache>
</c:numRef>
</c:val>
*/
// 這份課件一定存在 values 內容
// 和其他的一樣,存在引用 Excel 的內容。這裡同樣也是採用緩存
var valuesFormula = valuesNumberReference.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.Formula>();
本文只採用讀取緩存的方式。在緩存也有一個數據,表示數據如何格式化顯示,例如通過格式化字元串告訴 PPT 如何格式化日期內容等。本文使用的例子寫的是 General 表示不需要格式化
var valuesNumberingCache = valuesNumberReference.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.NumberingCache>()!;
// 通過 FormatCode 決定界面效果。這份課件是 General 表示不用格式化
var formatCode = valuesNumberingCache.FormatCode;
Debug.Assert(formatCode?.Text == "General");
接下來繼續獲取數據
var valueList = new List<string>();
foreach (var numericPoint in valuesNumberingCache.Elements<DocumentFormat.OpenXml.Drawing.Charts.NumericPoint>())
{
var numericValue = numericPoint.GetFirstChild<DocumentFormat.OpenXml.Drawing.Charts.NumericValue>()!;
var numericValueText = numericValue.Text;
valueList.Add(numericValueText);
}
通過上面例子,無論數據引用是數值引用還是字元串引用,具體的內容都是 DocumentFormat.OpenXml.Drawing.Charts.NumericValue
類型。如果不需要準確判斷內容,可以採用獲取此類型,簡化邏輯
上面代碼的 valueList
存放了系列數據內容
這就完成了讀取圖表的大部分數據內容
數據存儲
本文期望大家瞭解 OpenXML 里對圖表的存儲方式。在 OpenXML 裡面,圖表是放在頁面的一個元素,但是數據不放在頁面上,頁面上放的是引用。通過引用獲取到圖表的內容,對應的數據存儲如下
<c:plotArea>
<c:areaChart>
<c:ser>
<!-- 系列的數據 -->
</c:ser>
<c:ser>
<c:tx>
<!-- 系列標題 -->
</c:tx>
<c:spPr>
<!-- 系列樣式 -->
</c:spPr>
<c:cat>
<!-- 類別軸上的數據 -->
</c:cat>
<c:val>
<!-- 系列數據 -->
</c:val>
</c:ser>
</c:areaChart>
</c:plotArea>
以上是面積圖的存儲,面積圖裡面由多個系列組成。對於圖表來說,最重要的數據就是每個系列的內容。系列裡面包含了系列標題,系列樣式,和類別軸上的數據和系列數據。其中類別軸上的數據只有第零個系列的有用,但是在 OpenXML 里每個系列都重覆存放一份
在圖表裡存放的數據使用的是引用,可以用公式讀取 Excel 的數據,也可以使用緩存。如果想要數據正確,是需要通過公式讀取 Excel 的數據,如果想要讀取 Excel 的數據,前置的是讀取 PPT 裡面內嵌的 Excel 內容,請看 dotnet OpenXML 讀取 PPT 內嵌 xlsx 格式 Excel 表格的信息
圖表還有其他的內容,如圖表標題和樣式等。以及圖表的數據格式化展示邏輯,日期計算方法等,這些都沒有放在本文告訴大家。將在後續博客告訴大家這些內容和行為,請看 Office 使用 OpenXML SDK 解析文檔博客目錄
代碼
本文以上的測試文件和代碼放在github 和 gitee 歡迎訪問
可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接著使用命令行 cd 命令進入此空文件夾,在命令行裡面輸入以下代碼,即可獲取到本文的代碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 2f266d20916f784662d84a98d60b7e1bd097d11d
以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
獲取代碼之後,進入 MainWindow.xaml.cs 文件,在這個文件里就是本文的例子代碼
更多
更多請看 Office 使用 OpenXML SDK 解析文檔博客目錄
博客園博客只做備份,博客發佈就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/
本作品採用知識共用署名-非商業性使用-相同方式共用 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發佈,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請與我[聯繫](mailto:[email protected])。