本文是將演示如何解析pptx檔案的多路徑的形狀轉換到WPF,繪製多個Shape的Path
Shape Path
這是Pptx的【標註:彎曲曲線(無邊框)】形狀的OpenXml定義部分:
<callout2>
<avLst xmlns="http://schemas.openxmlformats.org/drawingml/2006/main">
<gd name="adj1" fmla="val 18750" />
<gd name="adj2" fmla="val -8333" />
<gd name="adj3" fmla="val 18750" />
<gd name="adj4" fmla="val -16667" />
<gd name="adj5" fmla="val 112500" />
<gd name="adj6" fmla="val -46667" />
</avLst>
<gdLst xmlns="http://schemas.openxmlformats.org/drawingml/2006/main">
<gd name="y1" fmla="*/ h adj1 100000" />
<gd name="x1" fmla="*/ w adj2 100000" />
<gd name="y2" fmla="*/ h adj3 100000" />
<gd name="x2" fmla="*/ w adj4 100000" />
<gd name="y3" fmla="*/ h adj5 100000" />
<gd name="x3" fmla="*/ w adj6 100000" />
</gdLst>
<pathLst xmlns="http://schemas.openxmlformats.org/drawingml/2006/main">
<path stroke="false" extrusionOk="false">
<moveTo>
<pt x="l" y="t" />
</moveTo>
<lnTo>
<pt x="r" y="t" />
</lnTo>
<lnTo>
<pt x="r" y="b" />
</lnTo>
<lnTo>
<pt x="l" y="b" />
</lnTo>
<close />
</path>
<path fill="none" extrusionOk="false">
<moveTo>
<pt x="x1" y="y1" />
</moveTo>
<lnTo>
<pt x="x2" y="y2" />
</lnTo>
<lnTo>
<pt x="x3" y="y3" />
</lnTo>
</path>
</pathLst>
</callout2>
然後以下OpenXml Shape Path的子屬性:
屬性 | 型別 | 備註 |
---|---|---|
extrusionOk (3D Extrusion Allowed) | bool | 指定使用 3D 拉伸可能在此路徑,預設false或0 |
fill (Path Fill) | PathFillMode | 路徑填充模式:Norm(預設)、None、Lighten、LightenLess、Darken、DarkenLess |
stroke (Path Stroke) | bool | 是否存在輪廓:預設false |
h (Path Height) | int | 指定框架的高度或在路徑座標系統中應在使用的最大的 y 座標 |
w (Path Width) | int | 指定的寬度或在路徑座標系統中應在使用的最大的 x 座標 |
首先為什麼是要轉為多個Shape呢?因為OpenXml每條路徑,都能設定是否有輪廓、填充等屬性,而該屬性設定只能在Shape層,而不能在Geometry層,就算是通過PathGeometry的PathFigure也只能設定IsFilled(是否填充),不能設定IsStroke(是否有輪廓)
解析Pptx形狀
首先我們來建立對應Shape Path的類:
public readonly struct ShapePath
{
public ShapePath(string path, FillMode fillMode = FillMode.Norm, bool isStroke = true)
{
Path = path;
IsStroke = isStroke;
FillMode = fillMode;
IsFilled = fillMode is not FillMode.None;
}
/// <summary>
/// 是否填充
/// </summary>
public bool IsFilled { get; }
/// <summary>
/// 是否有邊框
/// </summary>
public bool IsStroke { get; }
public FillMode FillMode { get; }
/// <summary>
/// Geometry的Path
/// </summary>
public string Path { get; }
}
public enum FillMode
{
/// <summary>
///Darken Path Fill
/// </summary>
Darken,
/// <summary>
/// Darken Path Fill Less
/// </summary>
DarkenLess,
/// <summary>
/// Lighten Path Fill
/// </summary>
Lighten,
/// <summary>
/// Lighten Path Fill Less
/// </summary>
LightenLess,
/// <summary>
/// None Path Fill
/// </summary>
None,
/// <summary>
/// Normal Path Fill
/// </summary>
Norm
}
解析pptx形狀的關鍵程式碼:
private void PptxMultiPathToGeometry(string filePath)
{
if (!File.Exists(filePath) || !filePath.EndsWith(".pptx", StringComparison.OrdinalIgnoreCase))
{
MessageBox.Show("請輸入正確的pptx檔案路徑");
return;
}
using (var presentationDocument = PresentationDocument.Open(filePath, false))
{
var presentationPart = presentationDocument.PresentationPart;
var presentation = presentationPart?.Presentation;
var slideIdList = presentation?.SlideIdList;
if (slideIdList == null)
{
return;
}
foreach (var slideId in slideIdList.ChildElements.OfType<SlideId>())
{
var slidePart = (SlidePart)presentationPart.GetPartById(slideId.RelationshipId);
var slide = slidePart.Slide;
foreach (var shapeProperties in slide.Descendants<ShapeProperties>())
{
var presetGeometry = shapeProperties.GetFirstChild<PresetGeometry>();
if (presetGeometry != null && presetGeometry.Preset.HasValue)
{
if (presetGeometry.Preset == ShapeTypeValues.BorderCallout2)
{
var transform2D = shapeProperties.GetFirstChild<Transform2D>();
var extents = transform2D?.GetFirstChild<Extents>();
if (extents != null)
{
var width = extents.Cx;
var height = extents.Cy;
if (width.HasValue && height.HasValue)
{
var geometryPaths = GetGeometryPathFromCallout2(new Emu(width).EmuToPixel().Value, new Emu(height).EmuToPixel().Value);
RenderGeometry(geometryPaths);
}
}
}
}
}
}
}
}
根據openxml的定義算出Shape Path:
/// <summary>
/// 獲取【標註:彎曲線形】的Shape Path
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
public static List<ShapePath> GetGeometryPathFromCallout2(double width, double height)
{
var (h, w, l, r, t, b, hd2, hd4, hd5, hd6, hd8, ss, hc, vc, ls, ss2, ss4, ss6, ss8, wd2, wd4, wd5, wd6, wd8, wd10, cd2, cd4, cd8) = GetFormulaProperties(width, height);
//<avLst xmlns="http://schemas.openxmlformats.org/drawingml/2006/main">
// <gd name="adj1" fmla="val 18750" />
// <gd name="adj2" fmla="val -8333" />
// <gd name="adj3" fmla="val 18750" />
// <gd name="adj4" fmla="val -16667" />
// <gd name="adj5" fmla="val 112500" />
// <gd name="adj6" fmla="val -46667" />
//</avLst>
var adj1 = 18750d;
var adj2 = -8333d;
var adj3 = 18750d;
var adj4 = -16667d;
var adj5 = 112500d;
var adj6 = -46667;
//<gdLst xmlns="http://schemas.openxmlformats.org/drawingml/2006/main">
// <gd name="y1" fmla="*/ h adj1 100000" />
// <gd name="x1" fmla="*/ w adj2 100000" />
// <gd name="y2" fmla="*/ h adj3 100000" />
// <gd name="x2" fmla="*/ w adj4 100000" />
// <gd name="y3" fmla="*/ h adj5 100000" />
// <gd name="x3" fmla="*/ w adj6 100000" />
//</gdLst>
// <gd name="y1" fmla="*/ h adj1 100000" />
var y1 = h * adj1 / 100000;
// <gd name="x1" fmla="*/ w adj2 100000" />
var x1 = w * adj2 / 100000;
// <gd name="y2" fmla="*/ h adj3 100000" />
var y2 = h * adj3 / 100000;
// <gd name="x2" fmla="*/ w adj4 100000" />
var x2 = w * adj4 / 100000;
// <gd name="y3" fmla="*/ h adj5 100000" />
var y3 = h * adj5 / 100000;
// <gd name="x3" fmla="*/ w adj6 100000" />
var x3 = w * adj6 / 100000;
// <pathLst xmlns="http://schemas.openxmlformats.org/drawingml/2006/main">
// <path extrusionOk="false">
// <moveTo>
// <pt x="l" y="t" />
// </moveTo>
// <lnTo>
// <pt x="r" y="t" />
// </lnTo>
// <lnTo>
// <pt x="r" y="b" />
// </lnTo>
// <lnTo>
// <pt x="l" y="b" />
// </lnTo>
// <close />
// </path>
// <path fill="none" extrusionOk="false">
// <moveTo>
// <pt x="x1" y="y1" />
// </moveTo>
// <lnTo>
// <pt x="x2" y="y2" />
// </lnTo>
// <lnTo>
// <pt x="x3" y="y3" />
// </lnTo>
// </path>
//</pathLst>
var pathLst = new List<ShapePath>();
// <path stroke="false" extrusionOk="false">
// <moveTo>
// <pt x="l" y="t" />
// </moveTo>
var currentPoint = new EmuPoint(l, t);
var stringPath = new StringBuilder();
stringPath.Append($"M {EmuToPixelString(currentPoint.X)},{EmuToPixelString(currentPoint.Y)} ");
// <lnTo>
// <pt x="r" y="t" />
// </lnTo>
currentPoint = LineToToString(stringPath, r, t);
// <lnTo>
// <pt x="r" y="b" />
// </lnTo>
currentPoint = LineToToString(stringPath, r, b);
// <lnTo>
// <pt x="l" y="b" />
// </lnTo>
currentPoint = LineToToString(stringPath, l, b);
// <close />
stringPath.Append("z ");
pathLst.Add(new ShapePath(stringPath.ToString(),isStroke:false));
// <path fill="none" extrusionOk="false">
// <moveTo>
// <pt x="x1" y="y1" />
// </moveTo>
stringPath.Clear();
currentPoint = new EmuPoint(x1, y1);
stringPath.Append($"M {EmuToPixelString(currentPoint.X)},{EmuToPixelString(currentPoint.Y)} ");
// <lnTo>
// <pt x="x2" y="y2" />
// </lnTo>
currentPoint = LineToToString(stringPath, x2, y2);
// <lnTo>
// <pt x="x3" y="y3" />
// </lnTo>
_ = LineToToString(stringPath, x3, y3);
pathLst.Add(new ShapePath(stringPath.ToString(), FillMode.None));
return pathLst;
}
將解析好的Shape Path轉為WPF的形狀Path:
/// <summary>
/// 將解析好的Shape Path轉為Path的形狀集合
/// </summary>
/// <param name="geometryPaths"></param>
/// <returns></returns>
private List<System.Windows.Shapes.Path> CreatePathLst(List<ShapePath> geometryPaths)
{
var pathLst = new List<System.Windows.Shapes.Path>();
foreach (var geometryPath in geometryPaths)
{
var geometry = Geometry.Parse(geometryPath.Path);
var path = new System.Windows.Shapes.Path
{
Data = geometry,
Fill = geometryPath.IsFilled ? new SolidColorBrush(Color.FromRgb(68, 114, 196)) : null,
Stroke = geometryPath.IsStroke ? new SolidColorBrush(Color.FromRgb(47, 82, 143)) : null,
};
pathLst.Add(path);
}
return pathLst;
}
然後渲染到介面:
/// <summary>
/// 渲染形狀到介面
/// </summary>
/// <param name="geometryPaths"></param>
private void RenderGeometry(List<ShapePath> geometryPaths)
{
if (PathGrid.Children.Count > 0)
{
PathGrid.Children.Clear();
}
var pathLst = CreatePathLst(geometryPaths);
foreach (var path in pathLst)
{
PathGrid.Children.Add(path);
}
}
效果演示
pptx和WPF渲染結果對比:
我們會發現,pptx的形狀和wpf的形狀是一模一樣的,同樣的左邊線條的Path是無填充的,而右邊的矩形則是無輪廓有填充的