本主题演示如何使用 Open XML SDK for Office 中的类以编程方式回复演示文稿中的现有注释。
基本演示文稿文档结构
文档的基本文档结构 PresentationML 由多个部分组成,其中是包含表示定义的主要部分。
ISO/IEC 29500 规范中的以下文本介绍了包的整体PresentationML形式。
包的主要部分
PresentationML以演示文稿根元素开头。 该元素包含演示文稿,演示文稿又引用幻灯片 列表、幻灯片母版 列表、备注母版 列表和讲义母版 列表。 幻灯片列表指的是演示文稿中的所有幻灯片;幻灯片母版列表指的是演示文稿中使用的全部幻灯片母版;备注母版包含有关备注页格式的信息;讲义母版描述讲义的外观。讲义 是可提供给访问群体 的一组打印的幻灯片。
除了文本和图形,每个幻灯片还可以包含注释 和备注,可以具有布局,并且可以是一个或多个自定义演示文稿 的组成部分。 注释是供维护演示文稿幻灯片平台的人员参考的批注。 备注是供演示者或访问群体参考的提醒信息或一段文字。
文档可以包括以下其他功能
PresentationML: 动画、 音频、 视频和幻灯片之间的 切换 效果。文档
PresentationML不作为一个大正文存储在单个部件中。 而实现某些功能组合的元素会存储在各个部件中。 例如,文档中的所有作者都存储在一个作者部件中,而每个幻灯片都有自己的部分。ISO/IEC 29500:2016
以下 XML 代码示例代表包含用 ID 267 和 256 表示的两个幻灯片的演示文稿。
<p:presentation xmlns:p="…" … >
<p:sldMasterIdLst>
<p:sldMasterId
xmlns:rel="https://…/relationships" rel:id="rId1"/>
</p:sldMasterIdLst>
<p:notesMasterIdLst>
<p:notesMasterId
xmlns:rel="https://…/relationships" rel:id="rId4"/>
</p:notesMasterIdLst>
<p:handoutMasterIdLst>
<p:handoutMasterId
xmlns:rel="https://…/relationships" rel:id="rId5"/>
</p:handoutMasterIdLst>
<p:sldIdLst>
<p:sldId id="267"
xmlns:rel="https://…/relationships" rel:id="rId2"/>
<p:sldId id="256"
xmlns:rel="https://…/relationships" rel:id="rId3"/>
</p:sldIdLst>
<p:sldSz cx="9144000" cy="6858000"/>
<p:notesSz cx="6858000" cy="9144000"/>
</p:presentation>
使用 Open XML SDK,可以使用对应于 PresentationML 元素的强类型类创建文档结构和内容。 可以在 命名空间中找到 DocumentFormat.OpenXml.Presentation 这些类。 下表列出了对应于 、、 sldLayoutsldMaster和 notesMaster 元素的类的sld类名。
| PresentationML 元素 | Open XML SDK 类 | 说明 |
|---|---|---|
<sld/> |
Slide | 演示文稿幻灯片。 它是 SlidePart 的根元素。 |
<sldLayout/> |
SlideLayout | 幻灯片版式。 它是 SlideLayoutPart 的根元素。 |
<sldMaster/> |
SlideMaster | 幻灯片母版。 它是 SlideMasterPart 的根元素。 |
<notesMaster/> |
NotesMaster | 备注母版(或讲义母版)。 它是 NotesMasterPart 的根元素。 |
新式注释元素的结构
以下 XML 元素指定单个注释。
它包含注释的文本 (t) 和属性引用其作者 (authorId) 、 () created 创建的日期时间,以及注释 ID (id) 。
<p188:cm id="{62A8A96D-E5A8-4BFC-B993-A6EAE3907CAD}" authorId="{CD37207E-7903-4ED4-8AE8-017538D2DF7E}" created="2024-12-30T20:26:06.503">
<p188:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:r>
<a:t>Needs more cowbell</a:t>
</a:r>
</a:p>
</p188:txBody>
</p188:cm>
下表列出了 (注释) 元素的可能子元素和属性 cm 的定义。 有关完整定义,请参阅 MS-PPTX 2.16.3.3 CT_Comment
| 属性 | 定义 |
|---|---|
| id | 指定批注或批注回复的 ID。 |
| authorId | 指定批注或批注回复的作者 ID。 |
| status | 指定批注或批注答复的状态。 |
| 已创建 | 指定创建批注或批注回复的日期时间。 |
| startDate | 指定注释的开始日期。 |
| dueDate | 指定注释的截止日期。 |
| assignedTo | 指定批注分配到的作者列表。 |
| complete | 指定注释的完成百分比。 |
| title | 指定批注的标题。 |
| 子元素 | 定义 |
|---|---|
| pc:sldMkLst | 指定用于标识批注定位到的幻灯片的内容名字对象。 |
| ac:deMkLst | 指定用于标识注释定位到的绘图元素的内容名字对象。 |
| ac:txMkLst | 指定一个内容名字对象,用于标识注释定位到的文本字符范围。 |
| unknownAnchor | 指定注释定位到的未知定位点。 |
| pos | 指定注释相对于注释定位到的第一个对象的左上角的位置。 |
| replyLst | 指定批注的答复列表。 |
| txBody | 指定批注或批注答复的文本。 |
| extLst | 指定批注或批注答复的扩展列表。 |
下面的 XML 架构示例除了定义必需属性和可选属性外,还定义了 元素的成员 cm 。
<xsd:complexType name="CT_Comment">
<xsd:sequence>
<xsd:group ref="EG_CommentAnchor" minOccurs="1" maxOccurs="1"/>
<xsd:element name="pos" type="a:CT_Point2D" minOccurs="0" maxOccurs="1"/>
<xsd:element name="replyLst" type="CT_CommentReplyList" minOccurs="0" maxOccurs="1"/>
<xsd:group ref="EG_CommentProperties" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_CommentProperties"/>
<xsd:attribute name="startDate" type="xsd:dateTime" use="optional"/>
<xsd:attribute name="dueDate" type="xsd:dateTime" use="optional"/>
<xsd:attribute name="assignedTo" type="ST_AuthorIdList" use="optional" default=""/>
<xsd:attribute name="complete" type="s:ST_PositiveFixedPercentage" default="0%" use="optional"/>
<xsd:attribute name="title" type="xsd:string" use="optional" default=""/>
</xsd:complexType>
示例代码的工作方式
示例代码在 using 语句中打开演示文稿文档。 然后,它获取或创建 CommentAuthorsPart,并验证是否存在现有的注释作者部件。 如果没有,则添加一个。
// Open the PowerPoint presentation for editing
using (PresentationDocument presentationDocument = PresentationDocument.Open(path, true))
{
// Check if the presentation part exists
if (presentationDocument.PresentationPart is null)
{
Console.WriteLine("No presentation part found in the presentation");
return;
}
else
{
// Prompt the user for the author's name
Console.WriteLine("Please enter the author's name");
string? authorName = Console.ReadLine();
// Ensure the author name is provided
while (authorName is null)
{
Console.WriteLine("Author's name is required. Please enter author's name below");
authorName = Console.ReadLine();
}
// Generate initials from the author's name
string[] splitName = authorName.Split(" ");
string authorInitials = string.Concat(splitName[0].Substring(0, 1), splitName[splitName.Length - 1].Substring(0, 1));
// Get or create the authors part for comment authorship
PowerPointAuthorsPart authorsPart = presentationDocument.PresentationPart.authorsPart ?? presentationDocument.AddNewPart<PowerPointAuthorsPart>();
authorsPart.AuthorList ??= new AuthorList();
接下来,代码确定传入的作者是否在现有作者列表中;如果是,则分配现有作者 ID。 如果没有,它将新作者添加到作者列表中,并分配作者 ID 和参数值。
// Try to find an existing author by name, otherwise create a new author
string? authorId = authorsPart.AuthorList.Descendants<Author>().Where(author => author.Name == authorName).FirstOrDefault()?.UserId;
if (authorId is null)
{
authorId = Guid.NewGuid().ToString("B");
authorsPart.AuthorList.AppendChild(new Author() { Id = authorId, Name = authorName, Initials = authorInitials, UserId = authorId, ProviderId = "Me" });
}
接下来,代码获取第一个幻灯片部件并验证它是否存在,然后检查是否有任何注释部分与幻灯片相关联。
// Get the first slide part in the presentation
SlidePart? slidePart = presentationDocument.PresentationPart.SlideParts?.FirstOrDefault();
if (slidePart is null)
{
Console.WriteLine("No slide part found in the presentation.");
return;
}
else
{
// Check if the slide has any comment parts
if (slidePart.commentParts is null || slidePart.commentParts.Count() == 0)
{
Console.WriteLine("No comments part found for slide 1");
return;
}
然后,代码检索批注列表,然后循环访问注释列表中的每个注释,向用户显示批注文本,并提示他们是否要回复每个批注。
// Get the comment list
DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.CommentList? commentList =
(DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.CommentList?)(slidePart.commentParts.FirstOrDefault()?.CommentList);
if (commentList is null)
{
Console.WriteLine("No comments found for slide 1");
return;
}
else
{
// Iterate through each comment in the comment list
foreach (DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.Comment comment in commentList)
{
// Display the comment text to the user
Console.WriteLine("Comment:");
Console.WriteLine(comment.ChildElements.Where(c => c is TextBodyType).FirstOrDefault()?.InnerText);
Console.WriteLine("Do you want to reply Y/N");
string? leaveReply = Console.ReadLine();
当用户选择回复批注时,代码会提示输入回复文本,然后获取或创建一个 CommentReplyList 用于批注的 ,并添加具有相应作者信息和时间戳的新答复。
// If the user wants to reply, prompt for the reply text
if (leaveReply is not null && leaveReply.ToUpper() == "Y")
{
Console.WriteLine("What is your reply?");
string? reply = Console.ReadLine();
if (reply is not null)
{
// Get or create the reply list for the comment
CommentReplyList? commentReplyList = comment.Descendants<CommentReplyList>()?.FirstOrDefault();
if (commentReplyList is null)
{
commentReplyList = new CommentReplyList();
comment.AddChild(commentReplyList);
}
// Add the user's reply to the comment
commentReplyList.AppendChild(new CommentReply(
new TextBodyType(
new BodyProperties(),
new Paragraph(
new Run(
new DocumentFormat.OpenXml.Drawing.Text(reply)))))
{
Id = Guid.NewGuid().ToString("B"),
AuthorId = authorId,
Created = new DateTimeValue(DateTime.Now)
});
}
}
示例代码
下面是完整的代码示例,演示如何使用新式 PowerPoint 批注回复演示文稿幻灯片中的现有批注。
// Open the PowerPoint presentation for editing
using (PresentationDocument presentationDocument = PresentationDocument.Open(path, true))
{
// Check if the presentation part exists
if (presentationDocument.PresentationPart is null)
{
Console.WriteLine("No presentation part found in the presentation");
return;
}
else
{
// Prompt the user for the author's name
Console.WriteLine("Please enter the author's name");
string? authorName = Console.ReadLine();
// Ensure the author name is provided
while (authorName is null)
{
Console.WriteLine("Author's name is required. Please enter author's name below");
authorName = Console.ReadLine();
}
// Generate initials from the author's name
string[] splitName = authorName.Split(" ");
string authorInitials = string.Concat(splitName[0].Substring(0, 1), splitName[splitName.Length - 1].Substring(0, 1));
// Get or create the authors part for comment authorship
PowerPointAuthorsPart authorsPart = presentationDocument.PresentationPart.authorsPart ?? presentationDocument.AddNewPart<PowerPointAuthorsPart>();
authorsPart.AuthorList ??= new AuthorList();
// Try to find an existing author by name, otherwise create a new author
string? authorId = authorsPart.AuthorList.Descendants<Author>().Where(author => author.Name == authorName).FirstOrDefault()?.UserId;
if (authorId is null)
{
authorId = Guid.NewGuid().ToString("B");
authorsPart.AuthorList.AppendChild(new Author() { Id = authorId, Name = authorName, Initials = authorInitials, UserId = authorId, ProviderId = "Me" });
}
// Get the first slide part in the presentation
SlidePart? slidePart = presentationDocument.PresentationPart.SlideParts?.FirstOrDefault();
if (slidePart is null)
{
Console.WriteLine("No slide part found in the presentation.");
return;
}
else
{
// Check if the slide has any comment parts
if (slidePart.commentParts is null || slidePart.commentParts.Count() == 0)
{
Console.WriteLine("No comments part found for slide 1");
return;
}
// Get the comment list
DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.CommentList? commentList =
(DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.CommentList?)(slidePart.commentParts.FirstOrDefault()?.CommentList);
if (commentList is null)
{
Console.WriteLine("No comments found for slide 1");
return;
}
else
{
// Iterate through each comment in the comment list
foreach (DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.Comment comment in commentList)
{
// Display the comment text to the user
Console.WriteLine("Comment:");
Console.WriteLine(comment.ChildElements.Where(c => c is TextBodyType).FirstOrDefault()?.InnerText);
Console.WriteLine("Do you want to reply Y/N");
string? leaveReply = Console.ReadLine();
// If the user wants to reply, prompt for the reply text
if (leaveReply is not null && leaveReply.ToUpper() == "Y")
{
Console.WriteLine("What is your reply?");
string? reply = Console.ReadLine();
if (reply is not null)
{
// Get or create the reply list for the comment
CommentReplyList? commentReplyList = comment.Descendants<CommentReplyList>()?.FirstOrDefault();
if (commentReplyList is null)
{
commentReplyList = new CommentReplyList();
comment.AddChild(commentReplyList);
}
// Add the user's reply to the comment
commentReplyList.AppendChild(new CommentReply(
new TextBodyType(
new BodyProperties(),
new Paragraph(
new Run(
new DocumentFormat.OpenXml.Drawing.Text(reply)))))
{
Id = Guid.NewGuid().ToString("B"),
AuthorId = authorId,
Created = new DateTimeValue(DateTime.Now)
});
}
}
}
}
}
}
}