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

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

基本演示文稿文档结构

PresentationML 文档的基本文档结构包含大量部件,在这些部件中,主部件是包含演示文稿定义的部件。 ISO/IEC 29500(该链接可能指向英文页面) 规范中的以下文本介绍了 PresentationML 包的整体形式。

PresentationML 包的主部件以演示文稿根元素开头。 该元素包含演示文稿,演示文稿又引用幻灯片 列表、幻灯片母版 列表、备注母版 列表和讲义母版 列表。 幻灯片列表指的是演示文稿中的所有幻灯片;幻灯片母版列表指的是演示文稿中使用的全部幻灯片母版;备注母版包含有关备注页格式的信息;讲义母版描述讲义的外观。

讲义 是可提供给访问群体 的一组打印的幻灯片。

除了文本和图形,每个幻灯片还可以包含注释备注,可以具有布局,并且可以是一个或多个自定义演示文稿 的组成部分。 注释是供维护演示文稿幻灯片平台的人员参考的批注。 备注是供演示者或访问群体参考的提醒信息或一段文字。

PresentationML 文档可以包括以下其他功能:幻灯片之间的动画音频视频切换效果。

PresentationML 文档不会存储为单个部件中的一个大型正文。 而实现某些功能组合的元素会存储在各个部件中。 例如,文档中的所有注释都存储在一个注释部件中,而每个幻灯片都有自己的部件。

© ISO/IEC29500: 2008.

以下 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 命名空间中找到这些类。 下表列出了 sldsldLayoutsldMasternotesMaster 元素所对应类的类名称。

PresentationML 元素 Open XML SDK 类 说明
Sld Slide 演示文稿幻灯片。 它是 SlidePart 的根元素。
sldLayout SlideLayout 幻灯片版式。 它是 SlideLayoutPart 的根元素。
sldMaster SlideMaster 幻灯片母版。 它是 SlideMasterPart 的根元素。
notesMaster NotesMaster 备注母版(或讲义母版)。 它是 NotesMasterPart 的根元素。

Comment 元素的结构

注释是附加到幻灯片的文本备注,主要用途是使演示文稿的读者能够向演示文稿作者提供反馈。 每个注释包含无格式文本字符串及有关其作者的信息,并附加到幻灯片上的特定位置。 在编辑演示文稿时,注释是可见的,但在播放幻灯片时不会出现。 显示幻灯片的应用程序决定何时显示注释并确定其外观。

以下 XML 元素指定了附加到幻灯片的单个注释。 它包含注释文本 (text)、注释在幻灯片上的位置 (pos) 以及引用其作者 (authorId)、日期和时间 (dt) 及注释索引 (idx) 的属性。

    <p:cm authorId="0" dt="2006-08-28T17:26:44.129" idx="1">
        <p:pos x="10" y="10"/>
        <p:text>Add diagram to clarify.</p:text>
    </p:cm>

下表包含了 cm(注释)元素的成员和属性的定义。

成员/属性 定义
authorId 引用文档的注释作者列表中的作者的 ID。
Dt 上次修改此注释的日期和时间。
idx 此注释的标识符,在此文档中由此作者创建的所有注释的列表中是唯一的。 作者在文档中的第一个注释的索引是 1。
Pos 注释在幻灯片表面的放置位置信息。
text 注释文本。
extLst 指定具有修改能力的扩展列表,其中定义了元素类型 ext 的所有未来扩展。 扩展列表以及相应的未来扩展用于扩展 PresentationML 框架的存储功能。 这允许在框架中本机存储各种新类型的数据。

下面的 XML 架构代码示例定义了 cm 元素的成员以及必需属性和可选属性。

    <complexType name="CT_Comment">
       <sequence>
           <element name="pos" type="a:CT_Point2D" minOccurs="1" maxOccurs="1"/>
           <element name="text" type="xsd:string" minOccurs="1" maxOccurs="1"/>
           <element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/>
       </sequence>
       <attribute name="authorId" type="xsd:unsignedInt" use="required"/>
       <attribute name="dt" type="xsd:dateTime" use="optional"/>
       <attribute name="idx" type="ST_Index" use="required"/>
    </complexType>

示例代码

下面的代码示例演示如何向演示文稿文档添加注释。 若要运行程序,可以传入参数:

dotnet run -- [filePath] [initials] [name] [test ...]

注意

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

using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Presentation;
using System;
using System.Linq;

AddCommentToPresentation(args[0], args[1], args[2], string.Join(' ', args[3..]));

static void AddCommentToPresentation(string file, string initials, string name, string text)
{
    using (PresentationDocument doc = PresentationDocument.Open(file, true))
    {

        // Declare a CommentAuthorsPart object.
        CommentAuthorsPart authorsPart;

        // If the presentation does not contain a comment authors part, add a new one.
        PresentationPart presentationPart = doc.PresentationPart ?? doc.AddPresentationPart();

        // Verify that there is an existing comment authors part and add a new one if not.
        authorsPart = presentationPart.CommentAuthorsPart ?? presentationPart.AddNewPart<CommentAuthorsPart>();

        // Verify that there is a comment author list in the comment authors part and add one if not.
        CommentAuthorList authorList = authorsPart.CommentAuthorList ?? new CommentAuthorList();
        authorsPart.CommentAuthorList = authorList;

        // Declare a new author ID as either the max existing ID + 1 or 1 if there are no existing IDs.
        uint authorId = authorList.Elements<CommentAuthor>().Select(a => a.Id?.Value).Max() ?? 0;
        authorId++;
        // If there is an existing author with matching name and initials, use that author otherwise create a new CommentAuthor.
        var foo = authorList.Elements<CommentAuthor>().Where(a => a.Name == name && a.Initials == initials).FirstOrDefault();
        CommentAuthor author = foo ??
            authorList.AppendChild
                (new CommentAuthor()
                {
                    Id = authorId,
                    Name = name,
                    Initials = initials,
                    ColorIndex = 0
                });
        // get the author id
        authorId = author.Id ?? authorId;

        // Get the first slide, using the GetFirstSlide method.
        SlidePart slidePart1 = GetFirstSlide(doc);

        // Declare a comments part.
        SlideCommentsPart commentsPart;

        // Verify that there is a comments part in the first slide part.
        if (slidePart1.GetPartsOfType<SlideCommentsPart>().Count() == 0)
        {
            // If not, add a new comments part.
            commentsPart = slidePart1.AddNewPart<SlideCommentsPart>();
        }
        else
        {
            // Else, use the first comments part in the slide part.
            commentsPart = slidePart1.GetPartsOfType<SlideCommentsPart>().First();
        }

        // If the comment list does not exist.
        if (commentsPart.CommentList is null)
        {
            // Add a new comments list.
            commentsPart.CommentList = new CommentList();
        }

        // Get the new comment ID.
        uint commentIdx = author.LastIndex is null ? 1 : author.LastIndex + 1;
        author.LastIndex = commentIdx;

        // Add a new comment.
        Comment comment = commentsPart.CommentList.AppendChild<Comment>(
            new Comment()
            {
                AuthorId = authorId,
                Index = commentIdx,
                DateTime = DateTime.Now
            });

        // Add the position child node to the comment element.
        comment.Append(
            new Position() { X = 100, Y = 200 },
            new Text() { Text = text });

        // Save the comment authors part.
        authorList.Save();

        // Save the comments part.
        commentsPart.CommentList.Save();
    }
}

// Get the slide part of the first slide in the presentation document.
static SlidePart GetFirstSlide(PresentationDocument? presentationDocument)
{
    // Get relationship ID of the first slide
    PresentationPart? part = presentationDocument?.PresentationPart;
    SlideId? slideId = part?.Presentation?.SlideIdList?.GetFirstChild<SlideId>();
    string? relId = slideId?.RelationshipId;
    if (relId is null)
    {
        throw new ArgumentNullException("The first slide does not contain a relationship ID.");
    }
    // Get the slide part by the relationship ID.
    SlidePart? slidePart = part?.GetPartById(relId) as SlidePart;

    if (slidePart is null)
    {
        throw new ArgumentNullException("The slide part is null.");
    }

    return slidePart;
}