在 Java 中管理演示文稿形状

概述

Aspose.Slides for Java 将幻灯片上的形状表示为有序的 IShapeCollection。该集合既是查找和修改形状的地点,也是它们堆叠顺序的来源:索引 0 为最底层形状,最后一个索引为最前层形状。

本文遵循该模型。首先解释如何可靠地识别形状并修改预设的形状调整点,然后演示如何克隆、删除、隐藏和重新排序形状。最后的章节覆盖布局级别的格式化、SVG 导出、对齐以及翻转设置。每个示例都是独立的,您可以仅使用工作流所需的操作。

识别和查找形状

在处理已知文件时,集合索引很方便,但它们不是稳定的标识符。添加、删除或重新排序形状会更改其索引。请根据演示文稿的创作和维护方式选择标识符:

  • Name 对于开发者控制的模板很有用,并且可以在 PowerPoint 的“选择窗格”中轻松检查。名称可以编辑,但不保证唯一,因此如果代码依赖名称,请建立命名约定。
  • AlternativeText 当可访问性描述或作者提供的标签已经标识形状时很有用。它对用户可见,可能会本地化或为可访问性重写,但同样不保证唯一。不要在不知情的情况下将有意义的可访问性文本用作数据库键。
  • OfficeInteropShapeId 是只读标识符,在同一幻灯片内唯一,并对应 PowerPoint 互操作使用的形状 ID。将其用于与 PowerPoint 集成,或在形状生命周期内需要明确引用时使用。克隆或重新创建的形状是不同的形状,会获得自己的 ID。

相关的 getUniqueId 方法返回演示文稿范围内的标识符,但该标识符面向插件,可能会被重新分配,不应视为永久的外部键。如果长久身份至关重要,请在应用程序数据中保持映射并验证预期的形状仍然存在。

有关读取和更新替代文本标题与描述的实际示例,请参见 Manage Alternative Text Titles and Descriptions。使用替代文本向读者解释视觉内容的含义,并将其与代码用于查找形状的名称分开。

下面的示例使用精确比较按名称搜索,并报告幻灯片范围的互操作 ID。当模板不包含预期形状时,代码会报告该结果而不是继续使用错误对象。

import com.aspose.slides.*;

Presentation presentation = new Presentation("input.pptx");
try {
    ISlide slide = presentation.getSlides().get_Item(0);

    IShape targetShape = null;
    for (IShape shape : slide.getShapes()) {
        if ("RevenueChart".equals(shape.getName())) {
            targetShape = shape;
            break;
        }
    }

    if (targetShape == null) {
        System.out.println("The shape 'RevenueChart' was not found on slide 1.");
    } else {
        System.out.println("Found " + targetShape.getName() + "; interop ID: " + targetShape.getOfficeInteropShapeId());
    }
} finally {
    presentation.dispose();
}

当操作特定于某种形状类型时,请在使用类型特定成员前检查接口。此示例仅在命名对象是 IAutoShape 时更新文本和替代文本。

import com.aspose.slides.*;

Presentation presentation = new Presentation("input.pptx");
try {
    ISlide slide = presentation.getSlides().get_Item(0);

    IShape candidate = null;
    for (IShape shape : slide.getShapes()) {
        if ("StatusLabel".equals(shape.getName())) {
            candidate = shape;
            break;
        }
    }

    if (candidate instanceof IAutoShape) {
        IAutoShape autoShape = (IAutoShape) candidate;
        autoShape.getTextFrame().setText("Approved");
        autoShape.setAlternativeText("Approval status: approved");
        presentation.save("identified-shape.pptx", SaveFormat.Pptx);
    } else {
        System.out.println("'StatusLabel' is missing or is not an AutoShape.");
    }
} finally {
    presentation.dispose();
}

识别并修改预设形状调整

预设几何形状可以暴露用于控制角大小、箭头比例或弧度等特性的调整点。通过只读的 IGeometryShape.getAdjustments 集合访问它们。该集合由形状提供,但每个 IAdjustValue 包含可更改的值。

不要仅依赖固定的集合索引。遍历调整集合并检查只读的 getType 方法,其返回的 ShapeAdjustmentType 值描述了该调整控制的内容。只读的 getName 方法提供了额外的标识信息,在预设包含多个相同语义类型的调整时尤其有用。

使用与调整意义匹配的值方法:

Adjustment type Purpose Value to change
CornerSize 圆角的大小 setRawValue
ArrowTailThickness 箭尾的粗细 setRawValue
ArrowheadLength 箭头的长度 setRawValue
ArrowheadWidth 箭头的宽度 setRawValue
StartAngle 饼图或弧线的起始角度 setAngleValue
EndAngle 饼图或弧线的结束角度 setAngleValue

getType 和 getName 返回只读信息。getRawValue 和 setRawValue 使用预设原生几何单位的整数,而 getAngleValue 和 setAngleValue 使用度数。调整的数量、顺序、含义和有效范围取决于预设的 ShapeType。对一种预设有效的值在另一种预设上可能无效或产生不同效果。

当 getType 返回 ShapeAdjustmentType.Custom 时,API 未识别标准语义。检查 getName、预设类型和现有值,除非已知预期含义和范围,否则保持调整不变。即使是已识别的类型,在选择值之前也要检查同类型是否出现多次。Connector 文章展示了连接器弯曲调整的此类情况。

下面的完整示例创建了三个预设形状的默认和修改版本。它遍历每个调整,报告其名称和类型,通过 setRawValue 更改尺寸相关的值,通过 setAngleValue 更改角度,并保存结果。左侧列保留默认几何,右侧列显示调整后的圆角矩形、四向箭头和饼图。

import com.aspose.slides.*;

Presentation presentation = new Presentation();
try {
    ISlide slide = presentation.getSlides().get_Item(0);

    // 为默认列和已调整形状列添加标题。
    IAutoShape defaultColumnLabel = slide.getShapes().addAutoShape(ShapeType.Rectangle, 40, 20, 250, 30);
    defaultColumnLabel.getTextFrame().setText("Default preset geometry");
    IAutoShape adjustedColumnLabel = slide.getShapes().addAutoShape(ShapeType.Rectangle, 390, 20, 250, 30);
    adjustedColumnLabel.getTextFrame().setText("Modified adjustment values");

    slide.getShapes().addAutoShape(ShapeType.RoundCornerRectangle, 80, 70, 160, 70);
    IGeometryShape modifiedRoundedRectangle = slide.getShapes().addAutoShape(ShapeType.RoundCornerRectangle, 430, 70, 160, 70);
    modifiedRoundedRectangle.setName("ModifiedRoundedRectangle");

    slide.getShapes().addAutoShape(ShapeType.QuadArrow, 80, 180, 160, 110);
    IGeometryShape modifiedArrow = slide.getShapes().addAutoShape(ShapeType.QuadArrow, 430, 180, 160, 110);
    modifiedArrow.setName("ModifiedQuadArrow");

    slide.getShapes().addAutoShape(ShapeType.Pie, 95, 330, 130, 130);
    IGeometryShape modifiedPie = slide.getShapes().addAutoShape(ShapeType.Pie, 445, 330, 130, 130);
    modifiedPie.setName("ModifiedPie");

    IGeometryShape[] shapesToAdjust = {
        modifiedRoundedRectangle,
        modifiedArrow,
        modifiedPie
    };

    for (IGeometryShape shape : shapesToAdjust) {
        for (int adjustmentIndex = 0; adjustmentIndex < shape.getAdjustments().size(); adjustmentIndex++) {
            IAdjustValue adjustment = shape.getAdjustments().get_Item(adjustmentIndex);
            System.out.println(shape.getName() + " / " + adjustment.getName() + ": " + adjustment.getType());

            switch (adjustment.getType()) {
                case ShapeAdjustmentType.CornerSize:
                    adjustment.setRawValue(5000);
                    break;
                case ShapeAdjustmentType.ArrowTailThickness:
                    adjustment.setRawValue(25000);
                    break;
                case ShapeAdjustmentType.ArrowheadLength:
                    adjustment.setRawValue(30000);
                    break;
                case ShapeAdjustmentType.ArrowheadWidth:
                    adjustment.setRawValue(40000);
                    break;
                case ShapeAdjustmentType.StartAngle:
                    adjustment.setAngleValue(30);
                    break;
                case ShapeAdjustmentType.EndAngle:
                    adjustment.setAngleValue(300);
                    break;
                case ShapeAdjustmentType.Custom:
                    System.out.println("Custom adjustment '" + adjustment.getName() + "' was not changed.");
                    break;
            }
        }
    }

    presentation.save("preset-shape-adjustments.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

在更改值之前检查语义类型可以使代码的意图明确,并避免假设特定集合索引在不同预设形状之间具有相同含义。

修改形状集合

添加、克隆、删除和重新排序方法会立即作用于集合。如果操作改变了形状的数量或顺序,请不要继续依赖该操作前捕获的索引。

克隆形状

addClone 创建一个独立的副本并将其追加到目标集合。insertClone 也创建副本,但将其放置在指定的 Z 顺序索引。接受坐标的重载在不改变大小的情况下移动克隆;接受宽度和高度的重载则可以对其进行缩放。

示例创建了一个目标幻灯片,将标记矩形克隆到前面,并在后面插入第二个克隆。对任一克隆的更改都不会影响源形状。

import com.aspose.slides.*;

Presentation presentation = new Presentation();
try {
    ISlide sourceSlide = presentation.getSlides().get_Item(0);
    IAutoShape sourceShape = sourceSlide.getShapes().addAutoShape(ShapeType.Rectangle, 40, 40, 180, 60);
    sourceShape.setName("SourceLabel");
    sourceShape.getTextFrame().setText("Source");

    ILayoutSlide blankLayout = presentation.getMasters().get_Item(0).getLayoutSlides().getByType(SlideLayoutType.Blank);
    ISlide destinationSlide = presentation.getSlides().addEmptySlide(blankLayout);

    IShape frontCloneShape = destinationSlide.getShapes().addClone(sourceShape, 80, 80);
    frontCloneShape.setName("FrontClone");
    if (frontCloneShape instanceof IAutoShape) {
        IAutoShape frontClone = (IAutoShape) frontCloneShape;
        frontClone.getTextFrame().setText("Front clone");
    } else {
        System.out.println("The front clone is not an AutoShape; its text was not changed.");
    }

    IShape backCloneShape = destinationSlide.getShapes().insertClone(0, sourceShape, 80, 180);
    backCloneShape.setName("BackClone");
    if (backCloneShape instanceof IAutoShape) {
        IAutoShape backClone = (IAutoShape) backCloneShape;
        backClone.getTextFrame().setText("Back clone");
    } else {
        System.out.println("The back clone is not an AutoShape; its text was not changed.");
    }

    presentation.save("cloned-shapes.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

克隆会复制形状的内容和格式,包括名称和替代文本。当这些值必须唯一时,请为克隆分配新的逻辑标识符。复杂形状使用的资源由演示文稿处理,但克隆仍是集合中的新项,拥有新的形状标识。

删除形状

remove 从其集合中删除特定的形状对象。在索引遍历期间删除多个匹配项时,请从末尾向前遍历,以保持剩余索引有效。

此示例删除所有具有指定名称的形状。它读取当前索引处的形状,而不是固定的集合项,并且没有不必要地进行类型转换。

import com.aspose.slides.*;

Presentation presentation = new Presentation();
try {
    ISlide slide = presentation.getSlides().get_Item(0);

    IAutoShape keepShape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 40, 40, 140, 60);
    keepShape.setName("Keep");

    IAutoShape firstTemporaryShape = slide.getShapes().addAutoShape(ShapeType.Ellipse, 220, 40, 80, 80);
    firstTemporaryShape.setName("Temporary");

    IAutoShape secondTemporaryShape = slide.getShapes().addAutoShape(ShapeType.Triangle, 340, 40, 100, 80);
    secondTemporaryShape.setName("Temporary");

    for (int i = slide.getShapes().size() - 1; i >= 0; i--) {
        IShape shape = slide.getShapes().get_Item(i);
        if ("Temporary".equals(shape.getName())) {
            slide.getShapes().remove(shape);
        }
    }

    presentation.save("removed-shapes.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

删除后,形状计数以及后续形状的索引会改变。对未受影响的形状的引用比保存的索引更可靠。还需考虑连接器、动画及其他可能引用被删除对象的演示文稿特性;删除可见形状可能会改变幻灯片的外观之外的内容。

隐藏形状

将 Hidden 设置为 true 会保留形状在集合中,但阻止其在正常放映中出现。其索引、格式和内容仍可供代码使用,因此隐藏适用于可能稍后恢复的可选元素。

import com.aspose.slides.*;

Presentation presentation = new Presentation();
try {
    ISlide slide = presentation.getSlides().get_Item(0);

    IAutoShape visibleShape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 40, 40, 160, 60);
    visibleShape.setName("VisibleLabel");

    IAutoShape optionalShape = slide.getShapes().addAutoShape(ShapeType.Moon, 240, 40, 100, 100);
    optionalShape.setName("OptionalDecoration");

    for (IShape shape : slide.getShapes()) {
        if ("OptionalDecoration".equals(shape.getName())) {
            shape.setHidden(true);
        }
    }

    presentation.save("hidden-shape.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

隐藏并不等同于删除或安全保护。对象仍可被用户或代码发现并取消隐藏,且仍是演示文稿文件的一部分。

更改 Z 顺序

重叠的形状按集合顺序绘制。reorder 将已有形状移动到目标索引而不进行克隆。索引 0 为最底层,size() - 1 为最前层。

import com.aspose.slides.*;
import java.awt.Color;

Presentation presentation = new Presentation();
try {
    ISlide slide = presentation.getSlides().get_Item(0);

    IAutoShape blueRectangle = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 220, 120);
    blueRectangle.setName("BlueRectangle");
    blueRectangle.getFillFormat().setFillType(FillType.Solid);
    blueRectangle.getFillFormat().getSolidFillColor().setColor(Color.BLUE);

    IAutoShape orangeEllipse = slide.getShapes().addAutoShape(ShapeType.Ellipse, 180, 140, 220, 120);
    orangeEllipse.setName("OrangeEllipse");
    orangeEllipse.getFillFormat().setFillType(FillType.Solid);
    orangeEllipse.getFillFormat().getSolidFillColor().setColor(Color.ORANGE);

    slide.getShapes().reorder(slide.getShapes().size() - 1, blueRectangle);
    presentation.save("reordered-shapes.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

矩形最先创建,最初位于椭圆后面。将其移动到最终索引后会置于前面。请在添加或克隆所有相关形状后再确定 Z 顺序,因为这些操作会追加或插入新集合项,从而改变堆叠顺序。

检查布局幻灯片上的形状

普通幻灯片、布局幻灯片和母版幻灯片拥有各自的形状集合。布局集合中的形状与普通幻灯片上位置相同的形状并不是同一个对象。需要了解或更改布局提供的格式时,请检查布局形状。

下面的示例读取每个布局形状的 FillFormat 和 LineFormat,并未假设每个形状都是 AutoShape。

import com.aspose.slides.*;

Presentation presentation = new Presentation("input.pptx");
try {
    for (ILayoutSlide layoutSlide : presentation.getLayoutSlides()) {
        for (IShape shape : layoutSlide.getShapes()) {
            int fillType = shape.getFillFormat().getFillType();
            double lineWidth = shape.getLineFormat().getWidth();
            System.out.println(layoutSlide.getName() + " / " + shape.getName() + ": fill=" + fillType + ", line width=" + lineWidth);
        }
    }
} finally {
    presentation.dispose();
}

编辑布局可能会影响使用该布局的多个幻灯片。在更改布局形状之前,请确定普通幻灯片是继承该对象还是包含本地覆盖,并测试所有使用该布局的幻灯片。

将形状导出为 SVG

writeAsSvg 将单个形状的渲染内容写入流。结果仅包含该形状,而不包括整个幻灯片背景或相邻形状。

import com.aspose.slides.*;
import java.io.FileOutputStream;
import java.io.IOException;

Presentation presentation = new Presentation("input.pptx");
try {
    ISlide slide = presentation.getSlides().get_Item(0);

    if (slide.getShapes().size() == 0) {
        System.out.println("Slide 1 does not contain a shape to export.");
    } else {
        IShape shape = slide.getShapes().get_Item(0);
        try (FileOutputStream svgStream = new FileOutputStream("shape.svg")) {
            shape.writeAsSvg(svgStream);
        } catch (IOException exception) {
            System.out.println("The SVG file could not be written: " + exception.getMessage());
        }
    }
} finally {
    presentation.dispose();
}

渲染时保持演示文稿打开。输出受形状格式以及字体、图像等资源的影响。如果需要整个组合,请导出幻灯片而不是单个形状。调用者拥有流并负责关闭它。

对齐形状

SlideUtil.alignShapes 的重载可以对齐所有形状或选定的集合索引。ShapesAlignmentType 指定是对齐边缘、中心线还是分布模式。将 alignToSlide 设置为 true 使用幻灯片边缘;设置为 false 则相对于选定形状进行对齐。

此示例将三个形状对齐到幻灯片的顶部边缘。返回的形状引用在对齐前立即转换为当前索引。

import com.aspose.slides.*;

Presentation presentation = new Presentation();
try {
    ISlide slide = presentation.getSlides().get_Item(0);

    IAutoShape firstShape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 60, 80, 120, 50);
    IAutoShape secondShape = slide.getShapes().addAutoShape(ShapeType.Ellipse, 240, 160, 120, 50);
    IAutoShape thirdShape = slide.getShapes().addAutoShape(ShapeType.Triangle, 420, 240, 120, 50);
    firstShape.setName("FirstAlignedShape");
    secondShape.setName("SecondAlignedShape");
    thirdShape.setName("ThirdAlignedShape");

    int[] shapeIndexes = {slide.getShapes().indexOf(firstShape), slide.getShapes().indexOf(secondShape), slide.getShapes().indexOf(thirdShape)};

    SlideUtil.alignShapes(ShapesAlignmentType.AlignTop, true, slide, shapeIndexes);
    presentation.save("aligned-shapes.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

对齐会改变位置,而不是 Z 顺序。相对对齐通常至少需要两个形状,而水平或垂直分布则需要足够的形状来定义间距。如果在调用方法前修改了集合,请重新计算索引。

翻转形状

ShapeFrame 类存储位置、大小、水平和垂直翻转设置以及旋转。其 getFlipH 和 getFlipV 值使用 NullableBool:True 表示启用翻转,False 表示禁用,NotDefined 保持未指定/默认状态。

下面的输入演示文稿包含一个未翻转的形状。

The shape before flipping

示例保留了除两个翻转设置之外的所有帧值并仅替换这两个翻转设置。这很重要,因为为 Frame 赋新值会替换完整帧。

import com.aspose.slides.*;

Presentation presentation = new Presentation("sample.pptx");
try {
    IShape shape = presentation.getSlides().get_Item(0).getShapes().get_Item(0);
    IShapeFrame frame = shape.getFrame();

    System.out.println("Horizontal flip before change: " + frame.getFlipH());
    System.out.println("Vertical flip before change: " + frame.getFlipV());

    shape.setFrame(new ShapeFrame(frame.getX(), frame.getY(), frame.getWidth(), frame.getHeight(), NullableBool.True, NullableBool.True, frame.getRotation()));

    presentation.save("flipped-shape.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

保存后的形状在水平和垂直方向上均已镜像,同时保持其位置、大小和旋转。

The shape after flipping

常见问题

是否应该使用集合索引作为形状标识符?

仅在集合在使用索引前不会改变的短暂处理场景中使用。对作者模板更倾向使用经过验证的 Name 或 AlternativeText 约定,对幻灯片范围的互操作工作则使用 OfficeInteropShapeId。

隐藏形状会从 Z 顺序中移除吗?

不会。隐藏的形状仍位于集合的相同索引中。它仍可被查找、重新排序、编辑或再次显示。

为什么克隆的形状出现在另一个形状之前?

addClone 将克隆追加到集合末尾,即 Z 顺序的最前面。使用 insertClone 可选择初始索引,或在所有形状添加完后使用 reorder 调整顺序。

我可以使用固定索引来标识预设形状调整吗?

仅在验证了确切的预设和集合布局后可以。更推荐遍历 IGeometryShape.getAdjustments 并检查 IAdjustValue.getType;当同一语义类型出现多次时,使用 IAdjustValue.getName 作为额外信息。