Управление комментариями презентации в .NET

Обзор

В этой статье объясняется, как управлять комментариями презентации с помощью Aspose.Slides для .NET. Она вводит основные типы, связанные с комментариями, и демонстрирует, как добавлять комментарии на слайды, получать доступ к существующим комментариям, работать с ответами и современными комментариями, а также удалять комментарии из презентации.

Примеры охватывают типичные сценарии рецензирования и совместной работы в PowerPoint, такие как назначение комментариев авторам, чтение текста комментариев и метаданных, построение цепочек ответов и удаление выбранных комментариев или всех комментариев.

В PowerPoint комментарии отображаются как аннотации на слайдах. При выборе комментария отображаются его текст и связанная дискуссия.

Для того чтобы запросить отображение или скрытие комментариев при открытии презентации без изменения самих комментариев, см. Показать или скрыть комментарии при открытии презентации.

Зачем добавлять комментарии в презентации?

Вы можете использовать комментарии для предоставления обратной связи и совместной работы с коллегами при рецензировании презентаций.

Aspose.Slides для .NET предоставляет следующие API для работы с комментариями:

  • The Presentation класс, который предоставляет доступ к авторам комментариев презентации.
  • The ICommentCollection интерфейс, который представляет комментарии, связанные с отдельным автором.
  • The IComment интерфейс, который предоставляет информацию о комментарии, включая его автора, время создания, позицию и текст.
  • The CommentAuthor класс, который предоставляет информацию об авторе, включая его имя, инициалы и связанные комментарии.

Добавление комментариев к слайдам

Пример создает два слайда и регистрирует автора комментариев с помощью AddAuthor. Он использует AddComment чтобы разместить один комментарий на каждом слайде, затем получает комментарии первого слайда для этого автора с помощью GetSlideComments. Консоль выводит текст первого комментария дважды, демонстрируя доступ как через слайд, так и через автора, а сохраненная презентация содержит оба комментария.

using System;
using System.Drawing;
using Aspose.Slides;
using Aspose.Slides.Export;

using var presentation = new Presentation();
var firstSlide = presentation.Slides[0];
var secondSlide = presentation.Slides.AddEmptySlide(presentation.LayoutSlides[0]);

var author = presentation.CommentAuthors.AddAuthor("Jawad", "MF");
var position = new PointF(0.2f, 0.2f);
var createdTime = DateTime.Now;

author.Comments.AddComment("Hello Jawad, this is a slide comment", firstSlide, position, createdTime);
author.Comments.AddComment("Hello Jawad, this is the second slide comment", secondSlide, position, createdTime);

var comments = firstSlide.GetSlideComments(author);
if (comments.Length > 0)
{
    var firstComment = comments[0];
    Console.WriteLine(firstComment.Text);

    var commentText = firstComment.Author.Comments[0].Text;
    Console.WriteLine(commentText);
}

presentation.Save("Comments_out.pptx", SaveFormat.Pptx);

Найти или создать автора комментариев

Когда автоматический обзор многократно обрабатывает одну и ту же презентацию, перед добавлением автора следует искать в её коллекции CommentAuthors. FindByNameAndInitials сопоставляет и имя, и инициалы и возвращает массив соответствующих авторов, либо пустой массив, если совпадений нет. В отличие от этого, FindByName ищет только по имени, независимо от инициалов. Вызов AddAuthor с именем и инициалами, уже присутствующими в коллекции, генерирует исключение.

Пример требует файл презентации минимум с одним слайдом. На каждом из двух проходов он переиспользует первого совпадающего автора или создает нового лишь когда результат пустой. Если найдено несколько совпадений, выбирается первый элемент массива без объединения или удаления остальных авторов. Затем вызывается AddComment для выбранного автора и сохраняется презентация. На втором проходе открывается вывод первого прохода и операция повторяется.

using System;
using System.Drawing;
using Aspose.Slides;
using Aspose.Slides.Export;

const string authorName = "Reviewer";
const string authorInitials = "RV";

var inputFile = "input.pptx";
var position = new PointF(20, 20);

for (var pass = 1; pass <= 2; pass++)
{
    using var presentation = new Presentation(inputFile);

    var matches = presentation.CommentAuthors.FindByNameAndInitials(authorName, authorInitials);
    
    var author = matches.Length > 0 ? matches[0] : presentation.CommentAuthors.AddAuthor(authorName, authorInitials);
    author.Comments.AddComment("Please review this slide.", presentation.Slides[0], position, DateTime.Now);

    var outputFile = $"review_pass_{pass}.pptx";
    presentation.Save(outputFile, SaveFormat.Pptx);
    inputFile = outputFile;
}

Второй проход добавляет еще один комментарий без создания нового автора и без удаления существующих комментариев. Финальный файл, review_pass_2.pptx, содержит оба новых комментария вместе с любыми оригинальными комментариями. Повторное использование автора не объединяет дубликаты комментариев: чтобы избежать многократного добавления одного и того же комментария, реализуйте отдельное обнаружение дубликатов перед добавлением его.

Доступ к комментариям слайдов

Пример открывает существующую презентацию и читает её коллекцию CommentAuthors. Для каждого автора он проходит через Comments и выводит номер слайда, текст комментария, имя автора и время создания. Входная презентация должна содержать комментарии, чтобы получить вывод в консоль.

using System;
using Aspose.Slides;

using var presentation = new Presentation("Comments1.pptx");

foreach (var author in presentation.CommentAuthors)
{
    foreach (var comment in author.Comments)
    {
        Console.WriteLine($"Slide: {comment.Slide.SlideNumber}");
        Console.WriteLine($"Comment: {comment.Text}");
        Console.WriteLine($"Author: {comment.Author.Name}");
        Console.WriteLine($"Posted at: {comment.CreatedTime}");
        Console.WriteLine();
    }
}

Ответы на комментарии

Основным (родительским) комментарием считается исходный комментарий в вершине иерархии ответов. Свойство ParentComment интерфейса IComment позволяет получить или задать родительский комментарий.

Пример создает комментарии от двух авторов и использует ParentComment для построения ответов, включая ответ на другой ответ. Передача null в GetSlideComments получает комментарии от всех авторов; консоль отступает каждую запись в соответствии с её глубиной в иерархии ответов. Пример сохраняет полную дискуссию, вызывая Remove для первого корневого комментария, и сохраняет вторую презентацию, в которой этот комментарий и его ответы удалены.

using System;
using System.Drawing;
using Aspose.Slides;
using Aspose.Slides.Export;

using var presentation = new Presentation();
var slide = presentation.Slides[0];

var position = new PointF(10, 10);
var createdTime = DateTime.Now;

var originalAuthor = presentation.CommentAuthors.AddAuthor("Author_1", "A.A.");
var rootComment = originalAuthor.Comments.AddComment("comment 1", slide, position, createdTime);

var reviewer = presentation.CommentAuthors.AddAuthor("Author_2", "B.B.");
var firstReply = reviewer.Comments.AddComment("reply 1 for comment 1", slide, position, createdTime);
firstReply.ParentComment = rootComment;

var secondReply = reviewer.Comments.AddComment("reply 2 for comment 1", slide, position, createdTime);
secondReply.ParentComment = rootComment;

var subReply = originalAuthor.Comments.AddComment("subreply 3 for reply 2", slide, position, createdTime);
subReply.ParentComment = secondReply;

reviewer.Comments.AddComment("comment 2", slide, position, createdTime);
var separateComment = reviewer.Comments.AddComment("comment 3", slide, position, createdTime);

var separateReply = originalAuthor.Comments.AddComment("reply 4 for comment 3", slide, position, createdTime);
separateReply.ParentComment = separateComment;

var comments = slide.GetSlideComments(null);
for (var i = 0; i < comments.Length; i++)
{
    var comment = comments[i];
    while (comment.ParentComment != null)
    {
        Console.Write("\t");
        comment = comment.ParentComment;
    }

    Console.WriteLine($"{comments[i].Author.Name}: {comments[i].Text}");
}

presentation.Save("parent_comment.pptx", SaveFormat.Pptx);

rootComment.Remove();
presentation.Save("remove_comment.pptx", SaveFormat.Pptx);

Добавление современных комментариев

Современные комментарии могут быть связаны со слайдом, с определенной фигурой или с диапазоном текста внутри AutoShape. Метод ICommentCollection.AddModernComment принимает аргумент IShape, помимо слайда и координат маркера комментария.

Когда в аргумент shape передается null, комментарий становится комментариев уровня слайда. Его маркер позиционируется заданными координатами, но не привязан к конкретной фигуре, поэтому IModernComment.Shape возвращает null. Когда передается IShape, комментарий фиксируется к этой фигуре. Координаты по‑прежнему определяют положение маркера комментария на слайде, а ассоциацию с фигурой можно получить через IModernComment.Shape.

Привязка современного комментария к фигуре

Пример вызывает AddModernComment дважды: один раз с фигурой null для комментария уровня слайда и один раз с прямоугольником в качестве привязки. Чтение Shape выводит True для проверки null у комментария уровня слайда и Revenue title для привязанного комментария. Оба комментария сохраняются вместе с презентацией.

using System;
using System.Drawing;
using Aspose.Slides;
using Aspose.Slides.Export;

using var presentation = new Presentation();
var slide = presentation.Slides[0];

var author = presentation.CommentAuthors.AddAuthor("Reviewer", "RV");

var shape = slide.Shapes.AddAutoShape(ShapeType.Rectangle, 50, 50, 300, 80);
shape.Name = "Revenue title";
shape.TextFrame.Text = "Quarterly revenue";

var createdTime = DateTime.Now;
var slideCommentPosition = new PointF(20, 20);
var shapeCommentPosition = new PointF(60, 60);
var slideComment = author.Comments.AddModernComment("Review the overall slide layout.", slide, null, slideCommentPosition, createdTime);
var shapeComment = author.Comments.AddModernComment("Check this title.", slide, shape, shapeCommentPosition, createdTime);

Console.WriteLine(slideComment.Shape == null);
Console.WriteLine(shapeComment.Shape?.Name);

presentation.Save("modern_comments.pptx", SaveFormat.Pptx);

Привязка комментариев к различным типам фигур

Любой объект слайда, реализующий IShape, может использоваться в качестве привязки к фигуре. Распространённые примеры включают IAutoShape, IPictureFrame, IGroupShape, IConnector и экземпляры IGraphicalObject, такие как диаграммы.

Пример использует AddModernComment для связывания комментария с каждым из пяти типов фигур: AutoShape, PictureFrame, Group, Connector и Chart. Изображение использует встроенные данные, поэтому внешний файл изображения не требуется. Каждый комментарий имеет своё собственное положение маркера, и сохранённая презентация сохраняет все пять привязок к фигурам.

using System;
using System.Drawing;
using Aspose.Slides;
using Aspose.Slides.Charts;
using Aspose.Slides.Export;

using var presentation = new Presentation();
var slide = presentation.Slides[0];

var author = presentation.CommentAuthors.AddAuthor("Reviewer", "RV");
var createdTime = DateTime.Now;

var autoShape = slide.Shapes.AddAutoShape(ShapeType.Rectangle, 20, 20, 180, 60);
autoShape.TextFrame.Text = "AutoShape";

var autoShapeCommentPosition = new PointF(30, 30);
author.Comments.AddModernComment("Comment on an AutoShape.", slide, autoShape, autoShapeCommentPosition, createdTime);

var imageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVR4nGP8//8/AwMDEwMDAwMDAwAkBgMB/DXemwAAAABJRU5ErkJggg==";
var imageData = Convert.FromBase64String(imageBase64);
var image = presentation.Images.AddImage(imageData);
var pictureFrame = slide.Shapes.AddPictureFrame(ShapeType.Rectangle, 220, 20, 120, 80, image);
var pictureCommentPosition = new PointF(230, 30);
author.Comments.AddModernComment("Comment on a picture.", slide, pictureFrame, pictureCommentPosition, createdTime);

var groupShape = slide.Shapes.AddGroupShape();
groupShape.Shapes.AddAutoShape(ShapeType.Rectangle, 0, 0, 80, 40);
groupShape.Shapes.AddAutoShape(ShapeType.Ellipse, 100, 0, 80, 40);
var groupCommentPosition = new PointF(40, 150);
author.Comments.AddModernComment("Comment on a group.", slide, groupShape, groupCommentPosition, createdTime);

var connector = slide.Shapes.AddConnector(ShapeType.StraightConnector1, 220, 150, 140, 40);
var connectorCommentPosition = new PointF(240, 150);
author.Comments.AddModernComment("Comment on a connector.", slide, connector, connectorCommentPosition, createdTime);

var chart = slide.Shapes.AddChart(ChartType.ClusteredColumn, 400, 20, 250, 180);
var chartCommentPosition = new PointF(420, 40);
author.Comments.AddModernComment("Comment on a graphical object.", slide, chart, chartCommentPosition, createdTime);

presentation.Save("modern_comment_shape_types.pptx", SaveFormat.Pptx);

Привязка комментария к тексту и установка его статуса

Для современного комментария, связанного с IAutoShape, свойство IModernComment.TextSelectionStart указывает начальную позицию выбранного текста во фрейме текста фигуры, а IModernComment.TextSelectionLength задаёт длину выделения. Вместе эти свойства связывают комментарий с конкретным диапазоном текста внутри AutoShape.

Свойство IModernComment.Status можно читать или изменять значением из перечисления ModernCommentStatus:

  • NotDefined — статус современного комментария не определён.
  • Active — комментарий активен.
  • Resolved — комментарий отмечен как решённый.
  • Closed — комментарий закрыт.

Пример привязывает современный комментарий к словам «quarterly revenue» в тексте прямоугольника. Он вычисляет нулевой индекс TextSelectionStart из текста и задаёт TextSelectionLength равным количеству выбранных символов. После установки Status в Resolved, презентация сохраняется и открывается заново. Четыре проверки в консоли должны вывести True, подтверждая, что привязка к фигуре, диапазон текста и статус были сохранены.

using System;
using System.Drawing;
using Aspose.Slides;
using Aspose.Slides.Export;

const string outputFile = "modern_comment_text_anchor.pptx";
const string shapeText = "Review the quarterly revenue forecast.";
const string selectedText = "quarterly revenue";
var expectedSelectionStart = shapeText.IndexOf(selectedText, StringComparison.Ordinal);

using var presentation = new Presentation();
var slide = presentation.Slides[0];

var shape = slide.Shapes.AddAutoShape(ShapeType.Rectangle, 50, 50, 400, 100);
shape.Name = "Forecast text";
shape.TextFrame.Text = shapeText;

var author = presentation.CommentAuthors.AddAuthor("Reviewer", "RV");
var commentPosition = new PointF(60, 60);
var comment = author.Comments.AddModernComment("Verify this forecast wording.", slide, shape, commentPosition, DateTime.Now);
comment.TextSelectionStart = expectedSelectionStart;
comment.TextSelectionLength = selectedText.Length;
comment.Status = ModernCommentStatus.Resolved;

presentation.Save(outputFile, SaveFormat.Pptx);

using var reopenedPresentation = new Presentation(outputFile);
var reopenedSlide = reopenedPresentation.Slides[0];
var reopenedComments = reopenedSlide.GetSlideComments(null);

foreach (var reopenedComment in reopenedComments)
{
    if (reopenedComment is not IModernComment modernComment)
    {
        continue;
    }

    var shapeMatches = modernComment.Shape?.Name == "Forecast text";
    var selectionStartMatches = modernComment.TextSelectionStart == expectedSelectionStart;
    var selectionLengthMatches = modernComment.TextSelectionLength == selectedText.Length;
    var statusMatches = modernComment.Status == ModernCommentStatus.Resolved;

    Console.WriteLine($"Shape anchor preserved: {shapeMatches}");
    Console.WriteLine($"Text selection start preserved: {selectionStartMatches}");
    Console.WriteLine($"Text selection length preserved: {selectionLengthMatches}");
    Console.WriteLine($"Resolved status preserved: {statusMatches}");
}

Исследование существующих современных комментариев

Пример требует существующую презентацию, содержащую современные комментарии, и выводит их текст, статус и детали привязки. Для их изучения проверьте, какие комментарии реализуют IModernComment, затем изучите IModernComment.Shape, IModernComment.TextSelectionStart, IModernComment.TextSelectionLength и IModernComment.Status. Фигура null указывает на комментарий уровня слайда. Для привязки к IAutoShape свойства выбора текста определяют соответствующий диапазон во фрейме текста фигуры.

using System;
using Aspose.Slides;

using var presentation = new Presentation("comments.pptx");

foreach (var slide in presentation.Slides)
{
    var comments = slide.GetSlideComments(null);
    foreach (var comment in comments)
    {
        if (comment is not IModernComment modernComment)
        {
            continue;
        }

        Console.WriteLine($"Slide: {slide.SlideNumber}");
        Console.WriteLine($"Text: {modernComment.Text}");
        Console.WriteLine($"Status: {modernComment.Status}");

        var shape = modernComment.Shape;
        if (shape == null)
        {
            Console.WriteLine("Anchor: slide level");
        }
        else
        {
            Console.WriteLine($"Anchor shape: {shape.Name}");
            Console.WriteLine($"Anchor type: {shape.GetType().Name}");

            if (shape is IAutoShape)
            {
                Console.WriteLine($"Text selection start: {modernComment.TextSelectionStart}");
                Console.WriteLine($"Text selection length: {modernComment.TextSelectionLength}");
            }
        }

        Console.WriteLine();
    }
}

Удаление комментариев

Aspose.Slides для .NET позволяет удалить все комментарии и их авторов из презентации или удалить выбранные комментарии, оставив остальные. Ниже приведённые примеры демонстрируют оба подхода и сохраняют обновлённую презентацию.

Удалить все комментарии и их авторов

Пример открывает существующую презентацию и вызывает ICommentCollection.Clear для каждого автора, затем вызывает ICommentAuthorCollection.Clear для удаления самих авторов. Сохранённая презентация не содержит ни комментариев, ни их авторов. Используйте презентацию с комментариями, чтобы увидеть удаление.

using Aspose.Slides;
using Aspose.Slides.Export;

using var presentation = new Presentation("example.pptx");

foreach (var author in presentation.CommentAuthors)
{
    author.Comments.Clear();
}

presentation.CommentAuthors.Clear();

presentation.Save("example_out.pptx", SaveFormat.Pptx);

Удалить конкретные комментарии

Пример создаёт два комментария на одном слайде и использует GetSlideComments для поиска комментариев каждого автора. Он собирает комментарии, текст которых равен “comment 1”, перед вызовом ICommentCollection.Remove, избегая изменений в коллекции во время выбора. Сохранённая презентация сохраняет только “comment 2”.

using System;
using System.Collections.Generic;
using System.Drawing;
using Aspose.Slides;
using Aspose.Slides.Export;

using var presentation = new Presentation();
var slide = presentation.Slides[0];

var author = presentation.CommentAuthors.AddAuthor("Author", "A");
var createdTime = DateTime.Now;

var firstCommentPosition = new PointF(0.2f, 0.2f);
var secondCommentPosition = new PointF(0.3f, 0.2f);
author.Comments.AddComment("comment 1", slide, firstCommentPosition, createdTime);
author.Comments.AddComment("comment 2", slide, secondCommentPosition, createdTime);

foreach (var commentAuthor in presentation.CommentAuthors)
{
    var commentsToRemove = new List<IComment>();
    var comments = slide.GetSlideComments(commentAuthor);

    foreach (var comment in comments)
    {
        if (comment.Text == "comment 1")
        {
            commentsToRemove.Add(comment);
        }
    }

    foreach (var comment in commentsToRemove)
    {
        commentAuthor.Comments.Remove(comment);
    }
}

presentation.Save("pres.pptx", SaveFormat.Pptx);

FAQ

Поддерживает ли Aspose.Slides статус «решён» для современных комментариев?

Да. Свойство IModernComment.Status можно читать и задавать значением из ModernCommentStatus, включая Resolved. Статус сохраняется в презентации и может быть вновь прочитан после повторного открытия файла.

Поддерживаются ли дискуссии в виде веток (цепочки ответов) и существует ли ограничение вложенности?

Да. Каждый комментарий может ссылаться на свой parent comment, что позволяет создавать цепочки ответов. API не определяет конкретного ограничения глубины вложенности.

В какой системе координат определяется позиция маркера комментария на слайде?

Позиция маркера задаётся координатами с плавающей запятой в системе координат слайда, что позволяет точно разместить её на слайде.