向演示文稿中的幻灯片添加批注

本主题演示如何使用 Open XML SDK for Office 中的类以编程方式向演示文稿中的第一张幻灯片添加注释。

注意

此示例适用于 PowerPoint 新式注释。 有关经典注释,请查看 GitHub 上的存档示例

基本演示文稿文档结构

文档的基本文档结构PresentationML由多个部分组成,其中包括包含表示定义的main部分。 ISO/IEC 29500 规范中的以下文本介绍了包的整体PresentationML形式。

包的main部分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 进行实例化,验证是否有现有的注释作者部件。 如果没有,则添加一个。

// create missing PowerPointAuthorsPart if it is null
if (presentationDocument.PresentationPart.authorsPart is null)
{
    presentationDocument.PresentationPart.AddNewPart<PowerPointAuthorsPart>();
}

代码确定演示文稿部件中是否存在现有的 PowerPoint 作者部件;如果没有,则添加一个,然后检查是否存在作者列表,并在缺少作者列表时添加一个。 它还验证传入的作者是否在现有作者列表中:如果是,则分配现有作者 ID。 如果没有,它将新作者添加到作者列表中,并分配作者 ID 和参数值。

// Add missing AuthorList if it is null
if (presentationDocument.PresentationPart.authorsPart!.AuthorList is null)
{
    presentationDocument.PresentationPart.authorsPart.AuthorList = new AuthorList();
}

// Get the author or create a new one
Author? author = presentationDocument.PresentationPart.authorsPart.AuthorList
    .ChildElements.OfType<Author>().Where(a => a.Name?.Value == name).FirstOrDefault();

if (author is null)
{
    string authorId = string.Concat("{", Guid.NewGuid(), "}");
    string userId = string.Concat(name.Split(" ").FirstOrDefault() ?? "user", "@example.com::", Guid.NewGuid());
    author = new Author() { Id = authorId, Name = name, Initials = initials, UserId = userId, ProviderId = string.Empty };

    presentationDocument.PresentationPart.authorsPart.AuthorList.AppendChild(author);
}

接下来,代码确定是否存在幻灯片 ID,如果不存在幻灯片 ID,则返回

// Get the Id of the slide to add the comment to
SlideId? slideId = presentationDocument.PresentationPart.Presentation.SlideIdList?.Elements<SlideId>()?.FirstOrDefault();

// If slideId is null, there are no slides, so return
if (slideId is null) return;

在下面的段中,代码获取关系 ID。 如果存在,则用于查找幻灯片部件,否则将拍摄可枚举的幻灯片部件中的第一张幻灯片。 然后,它会验证幻灯片是否有 PowerPoint 注释部分,如果没有,则添加一个。

// Get the relationship id of the slide if it exists
string? relId = slideId.RelationshipId;

// Use the relId to get the slide if it exists, otherwise take the first slide in the sequence
SlidePart slidePart = relId is not null ? (SlidePart)presentationPart.GetPartById(relId) 
    : presentationDocument.PresentationPart.SlideParts.First();

// If the slide part has comments parts take the first PowerPointCommentsPart
// otherwise add a new one
PowerPointCommentPart powerPointCommentPart = slidePart.commentParts.FirstOrDefault() ?? slidePart.AddNewPart<PowerPointCommentPart>();

代码下方创建新的新式批注,然后向 PowerPoint 注释部件添加注释列表(如果不存在),并将注释添加到该批注列表。

// Create the comment using the new modern comment class DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.Comment
DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.Comment comment = new DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.Comment(
        new SlideMonikerList(
            new DocumentMoniker(),
            new SlideMoniker()
            {
                CId = cid,
                SldId = slideId.Id,
            }),
        new TextBodyType(
            new BodyProperties(),
            new ListStyle(),
            new Paragraph(new Run(new DocumentFormat.OpenXml.Drawing.Text(text)))))
{
    Id = string.Concat("{", Guid.NewGuid(), "}"),
    AuthorId = author.Id,
    Created = DateTime.Now,
};

// If the comment list does not exist, add one.
powerPointCommentPart.CommentList ??= new DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.CommentList();
// Add the comment to the comment list
powerPointCommentPart.CommentList.AppendChild(comment);

使用新式注释时,幻灯片需要具有正确的扩展列表和扩展。 以下代码确定幻灯片是否已有 SlideExtensionList 和 SlideExtension,如果它们不存在,则将其添加到幻灯片中。

// Get the presentation extension list if it exists
SlideExtensionList? presentationExtensionList = slidePart.Slide.ChildElements.OfType<SlideExtensionList>().FirstOrDefault();
// Create a boolean that determines if this is the slide's first comment
bool isFirstComment = false;

// If the presentation extension list is null, add one and set this as the first comment for the slide
if (presentationExtensionList is null)
{
    isFirstComment = true;
    slidePart.Slide.AppendChild(new SlideExtensionList());
    presentationExtensionList = slidePart.Slide.ChildElements.OfType<SlideExtensionList>().First();
}

// Get the slide extension if it exists
SlideExtension? presentationExtension = presentationExtensionList.ChildElements.OfType<SlideExtension>().FirstOrDefault();

// If the slide extension is null, add it and set this as a new comment
if (presentationExtension is null)
{
    isFirstComment = true;
    presentationExtensionList.AddChild(new SlideExtension() { Uri = "{6950BFC3-D8DA-4A85-94F7-54DA5524770B}" });
    presentationExtension = presentationExtensionList.ChildElements.OfType<SlideExtension>().First();
}

// If this is the first comment for the slide add the comment relationship
if (isFirstComment)
{
    presentationExtension.AddChild(new CommentRelationship()
    { Id = slidePart.GetIdOfPart(powerPointCommentPart) });
}

示例代码

下面是完整的代码示例,演示如何将包含新作者或现有作者的新批注添加到包含或不带现有批注的幻灯片。

注意

[!注释] 若要获取确切的作者姓名和缩写,请打开演示文稿文件,单击"文件"菜单项,然后单击"选项"。 PowerPointOptions 窗口随即打开,并显示“常规”选项卡的内容。 作者姓名和缩写必须与此选项卡中的"用户名"和"缩写"相匹配。

static void AddCommentToPresentation(string file, string initials, string name, string text)
{
    using (PresentationDocument presentationDocument = PresentationDocument.Open(file, true))
    {
        PresentationPart presentationPart = presentationDocument?.PresentationPart ?? throw new MissingFieldException("PresentationPart");

        // create missing PowerPointAuthorsPart if it is null
        if (presentationDocument.PresentationPart.authorsPart is null)
        {
            presentationDocument.PresentationPart.AddNewPart<PowerPointAuthorsPart>();
        }

        // Add missing AuthorList if it is null
        if (presentationDocument.PresentationPart.authorsPart!.AuthorList is null)
        {
            presentationDocument.PresentationPart.authorsPart.AuthorList = new AuthorList();
        }

        // Get the author or create a new one
        Author? author = presentationDocument.PresentationPart.authorsPart.AuthorList
            .ChildElements.OfType<Author>().Where(a => a.Name?.Value == name).FirstOrDefault();

        if (author is null)
        {
            string authorId = string.Concat("{", Guid.NewGuid(), "}");
            string userId = string.Concat(name.Split(" ").FirstOrDefault() ?? "user", "@example.com::", Guid.NewGuid());
            author = new Author() { Id = authorId, Name = name, Initials = initials, UserId = userId, ProviderId = string.Empty };

            presentationDocument.PresentationPart.authorsPart.AuthorList.AppendChild(author);
        }

        // Get the Id of the slide to add the comment to
        SlideId? slideId = presentationDocument.PresentationPart.Presentation.SlideIdList?.Elements<SlideId>()?.FirstOrDefault();
        
        // If slideId is null, there are no slides, so return
        if (slideId is null) return;
        Random ran = new Random();
        UInt32Value cid = Convert.ToUInt32(ran.Next(100000000, 999999999));

        // Get the relationship id of the slide if it exists
        string? relId = slideId.RelationshipId;

        // Use the relId to get the slide if it exists, otherwise take the first slide in the sequence
        SlidePart slidePart = relId is not null ? (SlidePart)presentationPart.GetPartById(relId) 
            : presentationDocument.PresentationPart.SlideParts.First();

        // If the slide part has comments parts take the first PowerPointCommentsPart
        // otherwise add a new one
        PowerPointCommentPart powerPointCommentPart = slidePart.commentParts.FirstOrDefault() ?? slidePart.AddNewPart<PowerPointCommentPart>();

        // Create the comment using the new modern comment class DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.Comment
        DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.Comment comment = new DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.Comment(
                new SlideMonikerList(
                    new DocumentMoniker(),
                    new SlideMoniker()
                    {
                        CId = cid,
                        SldId = slideId.Id,
                    }),
                new TextBodyType(
                    new BodyProperties(),
                    new ListStyle(),
                    new Paragraph(new Run(new DocumentFormat.OpenXml.Drawing.Text(text)))))
        {
            Id = string.Concat("{", Guid.NewGuid(), "}"),
            AuthorId = author.Id,
            Created = DateTime.Now,
        };

        // If the comment list does not exist, add one.
        powerPointCommentPart.CommentList ??= new DocumentFormat.OpenXml.Office2021.PowerPoint.Comment.CommentList();
        // Add the comment to the comment list
        powerPointCommentPart.CommentList.AppendChild(comment);
        
        // Get the presentation extension list if it exists
        SlideExtensionList? presentationExtensionList = slidePart.Slide.ChildElements.OfType<SlideExtensionList>().FirstOrDefault();
        // Create a boolean that determines if this is the slide's first comment
        bool isFirstComment = false;

        // If the presentation extension list is null, add one and set this as the first comment for the slide
        if (presentationExtensionList is null)
        {
            isFirstComment = true;
            slidePart.Slide.AppendChild(new SlideExtensionList());
            presentationExtensionList = slidePart.Slide.ChildElements.OfType<SlideExtensionList>().First();
        }

        // Get the slide extension if it exists
        SlideExtension? presentationExtension = presentationExtensionList.ChildElements.OfType<SlideExtension>().FirstOrDefault();

        // If the slide extension is null, add it and set this as a new comment
        if (presentationExtension is null)
        {
            isFirstComment = true;
            presentationExtensionList.AddChild(new SlideExtension() { Uri = "{6950BFC3-D8DA-4A85-94F7-54DA5524770B}" });
            presentationExtension = presentationExtensionList.ChildElements.OfType<SlideExtension>().First();
        }

        // If this is the first comment for the slide add the comment relationship
        if (isFirstComment)
        {
            presentationExtension.AddChild(new CommentRelationship()
            { Id = slidePart.GetIdOfPart(powerPointCommentPart) });
        }
    }
}

另请参阅