使用 POI v5 在 Word 中创建嵌套项目符号列表

Creating nested bullet lists in Word using POI v5

我在 Java 工作,使用以下 Maven 依赖项(没有其他):

  <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
  <dependencies>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.0.0</version>
    </dependency>
  </dependencies>

和以下 class,从另一个 SO post 中收集:

import java.io.FileOutputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;

import org.apache.poi.xwpf.usermodel.XWPFAbstractNum;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFNumbering;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTLvl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STNumberFormat;

public class CreateSimpleWordBulletList
{

  public static void main(String[] args) throws Exception
  {

    CTAbstractNum cTAbstractNum = CTAbstractNum.Factory.newInstance();
    
    // Next we set the AbstractNumId. This requires care.
    // Since we are in a new document we can start numbering from 0.
    // But if we have an existing document, we must determine the next free
    // number first.
    cTAbstractNum.setAbstractNumId(BigInteger.valueOf(0));

    // Bullet list
    CTLvl cTLvl = cTAbstractNum.addNewLvl();
    cTLvl.addNewNumFmt().setVal(STNumberFormat.BULLET);
    cTLvl.addNewLvlText().setVal("•");

    XWPFAbstractNum abstractNum = new XWPFAbstractNum(cTAbstractNum);
    XWPFDocument document = new XWPFDocument();
    XWPFNumbering numbering = document.createNumbering();

    BigInteger abstractNumID = numbering.addAbstractNum(abstractNum);
    BigInteger numID = numbering.addNum(abstractNumID);

    XWPFParagraph paragraph = document.createParagraph();

    XWPFRun run = paragraph.createRun();
    run.setText("A list having defined gap between bullet point and text:");

    ArrayList<String> documentList = new ArrayList<String>(Arrays.asList(new String[] { "One", "Two", "Three" }));
    for (String string : documentList)
    {
      paragraph = document.createParagraph();
      paragraph.setNumID(numID);
      // set indents in Twips (twentieth of an inch point, 1440 Twips = 1 inch
      paragraph.setIndentFromLeft(1440 / 4); // indent from left 360 Twips = 1/4
                                             // inch
      paragraph.setIndentationHanging(1440 / 4); // indentation hanging 360
                                                 // Twips = 1/4 inch
                                                 // so bullet point hangs 1/4
                                                 // inch before the text at
                                                 // indentation 0
      run = paragraph.createRun();
      run.setText(string);
    }

    paragraph = document.createParagraph();

    FileOutputStream out = new FileOutputStream("CreateWordSimplestBulletList.docx");
    document.write(out);
    out.close();
    document.close();

  }
}

这会创建一个项目符号列表,如我所愿;我希望缩进它,但那是次要的。

我需要对其进行的真正修改是再添加两级列表,这样在 Word 文档中的结果类似于以下内容:

* One
    - AAA
        o aaa
        o bbb
        o ccc
    - BBB
        o xyz
        o abc
* Two
    - AAA
        o mmm
        o nnn
    - ZZZ
        o bbb
        o nnn

等等

我希望不需要有人为此编写代码,但我不理解也找不到任何关于 CTAbstractNumCTLvl 或 [=15] 的文档=] classes。如果有足够的文档,那么有人可以指出我。

我从这里的其他评论中收集到 CTAbstractNum.setAbstractNumId() 中设置的值标识文档范围内的编号级别,因此它应该应用于最外层的所有项目,并且从概念上讲,另一个此类 ID 将应用于每个项目符号内同一级别的所有项目(例如,上图中的 'aaa'、'bbb'、'ccc' 字符串)。我猜会创建不同的 ID 并将其应用于每个此类内部列表。但是当我对 API 的概念模型一无所知时,我讨厌猜测那个。试错编程太无聊了。

我已经提供了关于如何创建 Word 编号的多个答案。还介绍了如何创建多级编号。例如:Apache poi multiline bullet point is working but not multiple paragaraph? and .

但是由于您也要求提供文档,我会尽量阐明这一点。

现代 Word 文档 (*.docx) 使用 Office Open XML 文件格式。该格式最初由 Ecma(如 ECMA-376)标准化,在后来的版本中由 ISO 和 IEC(如 ISO/IEC 29500)标准化。它是一个 ZIP 存档,包含 XML 和特殊目录结构中的其他文件。因此,可以轻松地解压缩 *.docx 文件并查看内部结构。

基于这些标准化,apacheorg.openxmlformats.schemas 类 中开发了 XML 个 bean。 apache poi 版本 4 之前 类 是在 ooxml-schemas. From apache poi version 5 on they are in poi-ooxml-full 中发布的。还有一个 poi-ooxml-lite 版本。但这只包含高层 apache poi 类 使用的那些 bean。因此,当涉及到更特殊的用例时,它缺少一些 bean。

很遗憾,org.openxmlformats.schemas 类 public 没有可用的文档。但是当然可以下载源代码并从这些源代码中执行 javadoc 以至少获得 API 文档。

XWPFapache poi 对 Microsoft Word 使用的 Office Open XML 部分的高级实现。它使用 org.openxmlformats.schemas beans 来实现更方便的方法。它记录在 https://poi.apache.org/: Apache POI - Javadocs, POI-XWPF - A Quick Guide 中。但是 XWPF 并未包含 Microsoft Word 迄今为止的所有可能性和功能。因此对于一些特殊的用例,需要了解 XML 和 org.openxmlformats.schemas bean 的用法。

由于 *.docx 只是一个 ZIP 存档,最方便的方法是使用 Microsoft Word 本身创建一个简单的 *.docx 文件,然后解压缩 *.docx 以获得 XML 已创建。然后尝试使用 XWPForg.openxmlformats.schemas bean 重新创建 XML。

当涉及到编号时,人们会发现 *.docx ZIP 存档中有一个 /word/numbering.xml,其中包含用于编号定义的 XML。每个定义都包含一个 abstractNum,其中甚至包含对编号的多个缩进级别的定义,以及一个链接到 abstractNumnumnum 有一个 numId,它在 /word/document.xml 中用于标记枚举中包含的那些段落。这些段落也可能有一个 ilv(缩进级别),表示它们在该枚举中的缩进深度。

XWPF没有完全提供创建abstractNum中编号的所有功能。它提供了 XWPFAbstractNum,它有一个采用 org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum 的构造函数。因此需要使用低级 bean 创建 CTAbstractNum。最简单的方法是从 String 给出的 XML 创建它。 XML 可以通过使用 Microsoft Word 本身创建一个具有编号的简单 *.docx 文件,然后解压缩 *.docx ZIP 存档来获得。

如果有人能读懂 XML 那么这个 XML 将不言自明。这些元素的名称很好。需要知道的是,缩进和悬挂的度量单位是twips(二十分之一英寸点)。并且有时用于要点的符号来自额外的 Windows 字体 Symbol and/or Wingdings。那些字体也需要在 XML 然后设置。这些值是 ASCII 值,它使用那些特殊字体的特殊字形。

以下完整示例说明了这一点。它一次创建您显示的枚举作为项目符号列表,一次作为编号列表。

import java.io.FileOutputStream;

import org.apache.poi.xwpf.usermodel.*;

import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumbering;

import java.math.BigInteger;

import java.util.Map; 
import java.util.TreeMap; 

public class CreateWordMultilevelLists {

 static String cTAbstractNumBulletXML = 
  "<w:abstractNum xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" w:abstractNumId=\"0\">"
+ "<w:multiLevelType w:val=\"hybridMultilevel\"/>"
+ "<w:lvl w:ilvl=\"0\"><w:start w:val=\"1\"/><w:numFmt w:val=\"bullet\"/><w:lvlText w:val=\"\uF0B7\"/><w:lvlJc w:val=\"left\"/><w:pPr><w:ind w:left=\"720\" w:hanging=\"360\"/></w:pPr><w:rPr><w:rFonts w:ascii=\"Symbol\" w:hAnsi=\"Symbol\" w:hint=\"default\"/></w:rPr></w:lvl>"
+ "<w:lvl w:ilvl=\"1\" w:tentative=\"1\"><w:start w:val=\"1\"/><w:numFmt w:val=\"bullet\"/><w:lvlText w:val=\"\u2013\"/><w:lvlJc w:val=\"left\"/><w:pPr><w:ind w:left=\"1440\" w:hanging=\"360\"/></w:pPr><w:rPr><w:rFonts w:ascii=\"Courier New\" w:hAnsi=\"Courier New\" w:cs=\"Courier New\" w:hint=\"default\"/></w:rPr></w:lvl>"
+ "<w:lvl w:ilvl=\"2\" w:tentative=\"1\"><w:start w:val=\"1\"/><w:numFmt w:val=\"bullet\"/><w:lvlText w:val=\"\u26Ac\"/><w:lvlJc w:val=\"left\"/><w:pPr><w:ind w:left=\"2160\" w:hanging=\"360\"/></w:pPr><w:rPr><w:rFonts w:ascii=\"Courier New\" w:hAnsi=\"Courier New\" w:hint=\"default\"/></w:rPr></w:lvl>"
+ "</w:abstractNum>";   

 static String cTAbstractNumDecimalXML = 
  "<w:abstractNum xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" w:abstractNumId=\"1\">"
+ "<w:multiLevelType w:val=\"hybridMultilevel\"/>"
+ "<w:lvl w:ilvl=\"0\"><w:start w:val=\"1\"/><w:numFmt w:val=\"decimal\"/><w:lvlText w:val=\"%1\"/><w:lvlJc w:val=\"left\"/><w:pPr><w:ind w:left=\"720\" w:hanging=\"360\"/></w:pPr></w:lvl>"
+ "<w:lvl w:ilvl=\"1\" w:tentative=\"1\"><w:start w:val=\"1\"/><w:numFmt w:val=\"decimal\"/><w:lvlText w:val=\"%1.%2\"/><w:lvlJc w:val=\"left\"/><w:pPr><w:ind w:left=\"1440\" w:hanging=\"360\"/></w:pPr></w:lvl>"
+ "<w:lvl w:ilvl=\"2\" w:tentative=\"1\"><w:start w:val=\"1\"/><w:numFmt w:val=\"decimal\"/><w:lvlText w:val=\"%1.%2.%3\"/><w:lvlJc w:val=\"left\"/><w:pPr><w:ind w:left=\"2160\" w:hanging=\"360\"/></w:pPr></w:lvl>"
+ "</w:abstractNum>";

 static BigInteger createNumbering(XWPFDocument document, String abstractNumXML) throws Exception {
  CTNumbering cTNumbering = CTNumbering.Factory.parse(abstractNumXML);
  CTAbstractNum cTAbstractNum = cTNumbering.getAbstractNumArray(0);
  XWPFAbstractNum abstractNum = new XWPFAbstractNum(cTAbstractNum);
  XWPFNumbering numbering = document.createNumbering();
  BigInteger abstractNumID = numbering.addAbstractNum(abstractNum);
  BigInteger numID = numbering.addNum(abstractNumID);
  return numID;
 }
 
 static void setIndentLevel(XWPFParagraph paragraph, BigInteger level) {
  if (paragraph.getCTP().isSetPPr()) {
   if (paragraph.getCTP().getPPr().isSetNumPr()) {
    if (paragraph.getCTP().getPPr().getNumPr().isSetIlvl()) {
     paragraph.getCTP().getPPr().getNumPr().getIlvl().setVal(level);
    } else {
     paragraph.getCTP().getPPr().getNumPr().addNewIlvl().setVal(level);
    }
   }
  }
 }
 
 static BigInteger getIndentLevelFromNumberingString(String numberingString) {
  String[] levels = numberingString.split("\.");
  int level = levels.length -1;
  return BigInteger.valueOf(level);
 }
 
 static void insertListContent(XWPFDocument document, TreeMap<String, String> listContent, BigInteger numID) {
  for (Map.Entry<String, String> entry : listContent.entrySet()) {
   String key = entry.getKey();
   String value = entry.getValue();
   XWPFParagraph paragraph = document.createParagraph();
   paragraph.setNumID(numID);
   setIndentLevel(paragraph, getIndentLevelFromNumberingString(key));
   XWPFRun run = paragraph.createRun();
   run.setText(value); 
   if (!entry.equals(listContent.lastEntry())) paragraph.setSpacingAfter(0);
  } 
 }

 public static void main(String[] args) throws Exception {
     
  TreeMap<String, String> listContent = new TreeMap<String, String>();
  listContent.put("1", "One");
  listContent.put("1.1", "AAA");
  listContent.put("1.1.1", "aaa");
  listContent.put("1.1.2", "bbb");
  listContent.put("1.1.3", "ccc");
  listContent.put("1.2", "BBB");
  listContent.put("1.2.1", "xyz");
  listContent.put("1.2.2", "abc");
  listContent.put("2", "Two");
  listContent.put("2.1", "AAA");
  listContent.put("2.1.1", "mmm");
  listContent.put("2.1.2", "nnn");
  listContent.put("2.2", "ZZZ");
  listContent.put("2.2.1", "bbb");
  listContent.put("2.2.2", "nnn");
  
  XWPFDocument document = new XWPFDocument();
  
  BigInteger numIDBulletList = createNumbering(document, cTAbstractNumBulletXML);
  BigInteger numIDDecimalList = createNumbering(document, cTAbstractNumDecimalXML);
  
  XWPFParagraph paragraph = document.createParagraph();
  XWPFRun run=paragraph.createRun();  
  run.setText("The bullet list:");
  
  insertListContent(document, listContent, numIDBulletList);
  
  paragraph = document.createParagraph();
  run=paragraph.createRun();  
  run.setText("Paragraph after the list.");
  
  paragraph = document.createParagraph();
  run=paragraph.createRun();  
  run.setText("The decimal list:");
  
  insertListContent(document, listContent, numIDDecimalList);
  
  paragraph = document.createParagraph();
  run=paragraph.createRun();  
  run.setText("Paragraph after the list.");

  FileOutputStream out = new FileOutputStream("./CreateWordMultilevelLists.docx");    
  document.write(out);
  out.close();
  document.close();

 }
}