Al generar informes en Word desde Java con Apache POI, es frecuente necesitar títulos numerados con un formato propio y que la numeración se rienicie en cada sección principal. La estrategia consiste en definir estilos personalizados y crear definiciones de lista abstracta independientes para cada bloque que deba comenzar desde 1.
Dependencias
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>1.4</version>
</dependency>
Ejemplo de generación de documento
La clase siguiente crea un documento con dos secciones principales y sus respectivas subsecciones. Cada sección principal utiliza su propia numeración abstracta, lo que provoca que el contador se reinicie.
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
public class WordReportBuilder {
private int currentSection = 0;
public void create(String outputPath) throws IOException {
try (XWPFDocument document = new XWPFDocument()) {
configureDefaultFonts(document);
registerStyles(document);
// Sección principal 1
currentSection = 1;
BigInteger mainList1 = registerNumbering(document, 10L, "%1.");
addNumberedHeading(document, "Resumen ejecutivo", "Heading1", mainList1);
addBodyParagraph(document, "Párrafo introductorio de la primera sección.");
BigInteger subList1 = registerNumbering(document, 100L, currentSection + ".%1");
addNumberedHeading(document, "Alcance del proyecto", "Heading2", subList1);
// Sección principal 2: la numeración se reinicia
currentSection = 2;
BigInteger mainList2 = registerNumbering(document, 11L, "%1.");
addNumberedHeading(document, "Detalle operativo", "Heading1", mainList2);
addBodyParagraph(document, "La numeración principal comienza de nuevo desde 1.");
BigInteger subList2 = registerNumbering(document, 101L, currentSection + ".%1");
addNumberedHeading(document, "Actividades planificadas", "Heading2", subList2);
try (FileOutputStream out = new FileOutputStream(outputPath)) {
document.write(out);
}
}
}
private void configureDefaultFonts(XWPFDocument doc) {
XWPFStyles styles = doc.createStyles();
CTFonts fonts = CTFonts.Factory.newInstance();
fonts.setAscii("Calibri");
fonts.setHAnsi("Calibri");
fonts.setEastAsia("Microsoft YaHei");
styles.setDefaultFonts(fonts);
}
private void registerStyles(XWPFDocument doc) {
XWPFStyles styles = doc.createStyles();
addStyle(styles, "Heading1", 1, 28, "000000", "Microsoft YaHei");
addStyle(styles, "Heading2", 2, 24, "000000", "Microsoft YaHei");
addStyle(styles, "BodyText", 0, 22, "000000", "Microsoft YaHei");
}
private void addStyle(XWPFStyles styles, String styleId, int outlineLevel,
int halfPoints, String hexColor, String fontName) {
CTStyle ctStyle = CTStyle.Factory.newInstance();
ctStyle.setStyleId(styleId);
CTString name = CTString.Factory.newInstance();
name.setVal(styleId);
ctStyle.setName(name);
CTDecimalNumber priority = CTDecimalNumber.Factory.newInstance();
priority.setVal(BigInteger.valueOf(outlineLevel));
ctStyle.setUiPriority(priority);
CTOnOff flag = CTOnOff.Factory.newInstance();
ctStyle.setUnhideWhenUsed(flag);
ctStyle.setQFormat(flag);
if (outlineLevel > 0) {
CTPPr pPr = CTPPr.Factory.newInstance();
CTDecimalNumber lvl = CTDecimalNumber.Factory.newInstance();
lvl.setVal(BigInteger.valueOf(outlineLevel - 1));
pPr.setOutlineLvl(lvl);
ctStyle.setPPr(pPr);
}
XWPFStyle style = new XWPFStyle(ctStyle);
style.setType(STStyleType.PARAGRAPH);
CTFonts fonts = CTFonts.Factory.newInstance();
fonts.setAscii(fontName);
fonts.setHAnsi(fontName);
fonts.setEastAsia(fontName);
CTHpsMeasure size = CTHpsMeasure.Factory.newInstance();
size.setVal(BigInteger.valueOf(halfPoints));
CTColor color = CTColor.Factory.newInstance();
color.setVal(hexToBytes(hexColor));
CTRPr rPr = CTRPr.Factory.newInstance();
rPr.setRFonts(fonts);
rPr.setSz(size);
rPr.setSzCs(size);
rPr.setColor(color);
style.getCTStyle().setRPr(rPr);
styles.addStyle(style);
}
private BigInteger registerNumbering(XWPFDocument doc, long abstractId, String levelText) {
CTAbstractNum abstractNum = CTAbstractNum.Factory.newInstance();
abstractNum.setAbstractNumId(BigInteger.valueOf(abstractId));
CTLvl lvl = abstractNum.addNewLvl();
lvl.addNewNumFmt().setVal(STNumberFormat.DECIMAL);
lvl.addNewLvlText().setVal(levelText);
lvl.addNewStart().setVal(BigInteger.valueOf(1));
XWPFNumbering numbering = doc.createNumbering();
BigInteger abstractNumId = numbering.addAbstractNum(new XWPFAbstractNum(abstractNum));
return numbering.addNum(abstractNumId);
}
private void addNumberedHeading(XWPFDocument doc, String text,
String styleName, BigInteger numId) {
XWPFParagraph p = doc.createParagraph();
p.setAlignment(ParagraphAlignment.LEFT);
p.setStyle(styleName);
p.setNumID(numId);
XWPFRun r = p.createRun();
r.setText(text);
r.setBold(true);
}
private void addBodyParagraph(XWPFDocument doc, String text) {
XWPFParagraph p = doc.createParagraph();
p.setStyle("BodyText");
XWPFRun r = p.createRun();
r.setText(text);
}
private static byte[] hexToBytes(String hex) {
return new HexBinaryAdapter().unmarshal(hex);
}
public static void main(String[] args) throws IOException {
new WordReportBuilder().create("informe.docx");
}
}
Conceptos clave
- Cada lamada a
registerNumberingcrea una nueva definición abstracta de lista con su propioabstractNumId, lo que permite reiniciar el contador. - El patrón
%1.produce numeraciones del tipo 1., 2., etc. Para subsecciones se utilizasección.%1, obteniendo 1.1, 1.2, 2.1, etc. - Los estilos se registran en el documento con
XWPFStyles, asignando fuente, tamaño en medios de punto y color.