本文告訴大家如何使用 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 = https://www.cnblogs.com/lindexi/p/graphic?.GraphicData;
var chartReference = graphicData?.GetFirstChild();
在 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 = https://www.cnblogs.com/lindexi/p/areaChartSeries.GetFirstChild()!;
}
}
在 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 = https://www.cnblogs.com/lindexi/p/areaChartSeries.GetFirstChild()!;
var categoryAxisDataStringReference = categoryAxisData.GetFirstChild();
在 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 = https://www.cnblogs.com/lindexi/p/seriesTextStringPoint!.GetFirstChild();
// 系列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 = https://www.cnblogs.com/lindexi/p/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 = https://www.cnblogs.com/lindexi/p/numericPoint.GetFirstChild()!;
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]),
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/501024.html
標籤:.NET Core
