在演示文稿中回复批注

本主题演示如何使用 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 这些类。 下表列出了对应于 、、 sldLayoutsldMasternotesMaster 元素的类的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)
                            });
                        }
                    }
                }
            }
        }
    }
}

另请参阅