如何使用Apache POI获取并修改Word文档文本框内的XWPFParagraph段落
Apache POI 4.1.2 读取与回写Word文本框内段落解决方案
你提取到文本框段落之后无法回写,本质是因为之前的读取方法没有让拿到的XWPFParagraph和原文档的底层XML节点做绑定,修改的是独立对象,自然不会同步到原文档。下面给出可直接读写、修改自动生效的实现方案:
实现步骤
1. 定义绑定上下文的文本框段落结构
我们需要同时保存提取到的段落和它所属的文本框内容节点,方便后续如果需要新增/替换段落时操作父容器:
import org.apache.poi.xwpf.usermodel.* import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTxbxContent // 存储文本框段落和它的父容器 data class TextBoxParagraph( val paragraph: XWPFParagraph, val parentTxbxContent: CTTxbxContent )
2. 扩展XWPFParagraph提取内部所有文本框段落
这个方法会提取段落内嵌的两种格式文本框(新版drawing型、旧版pict型)中的所有段落,且生成的XWPFParagraph直接绑定原文档的XML节点,修改内容会自动同步:
import org.openxmlformats.schemas.drawingml.x2006.main.CTGraphicalObject import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPict import com.microsoft.schemas.vml.CTShape fun XWPFParagraph.extractTextBoxParagraphs(): List<TextBoxParagraph> { val result = mutableListOf<TextBoxParagraph>() runs.forEach { run -> val ctRun = run.getCTR() // 处理mc:AlternateContent下的所有分支 ctRun.getAlternateContentList().forEach { alternateContent -> // 处理Choice分支下的drawing型文本框 alternateContent.getChoiceList().forEach { choice -> choice.getDrawingList().forEach { drawing -> drawing.getAnchorList().forEach { anchor -> val graphicData = anchor.graphic.graphicData val txbxNodes = graphicData.selectNodes("*/*/wps:txbx/w:txbxContent") txbxNodes.forEach { node -> if (node is CTTxbxContent) { val paras = node.pList.map { ctP -> XWPFParagraph(ctP, this.document) } result.addAll(paras.map { TextBoxParagraph(it, node) }) } } } } } // 处理Fallback分支下的pict型旧文本框 alternateContent.getFallbackList().forEach { fallback -> fallback.getPictList().forEach { pict -> val txbxNodes = pict.selectNodes("v:shape/v:textbox/w:txbxContent") txbxNodes.forEach { node -> if (node is CTTxbxContent) { val paras = node.pList.map { ctP -> XWPFParagraph(ctP, this.document) } result.addAll(paras.map { TextBoxParagraph(it, node) }) } } } } } } return result }
3. 集成到原有全局段落提取逻辑
把文本框段落提取逻辑加到你之前的全局段落遍历代码里即可:
val allBodyElements = bodyElements .plus(headerList.flatMap { it.bodyElements }) .plus(footerList.flatMap { it.bodyElements }) val allParagraphs = allBodyElements.flatMap { when (it) { is XWPFParagraph -> { // 普通段落 + 该段落内嵌的所有文本框段落 listOf(it) + it.extractTextBoxParagraphs().map { tbp -> tbp.paragraph } } is XWPFTable -> { it.rows .flatMap { row -> row.tableCells } .flatMap { cell -> // 表格单元格内的普通段落 + 单元格段落内嵌的文本框段落 cell.paragraphs + cell.paragraphs.flatMap { p -> p.extractTextBoxParagraphs().map { tbp -> tbp.paragraph } } } } else -> emptyList() } }
修改后保存说明
- 如果你只是修改已有文本框段落的内容、样式,不需要做额外的保存操作:因为上面方法提取到的
XWPFParagraph绑定的就是原文档的XML节点,修改会直接同步到底层结构,最后调用document.write(输出流)保存整个文档时就会自动生效。 - 如果你需要替换/新增文本框内的段落,可以通过
TextBoxParagraph里的parentTxbxContent对象操作:
// 示例:替换文本框内索引为0的段落 val textBoxPara: TextBoxParagraph = // 你提取到的文本框段落对象 val document: XWPFDocument = // 你的文档对象 // 新建段落 val newCTP = CTTxbxContent.Factory.newInstance().addNewP() val newParagraph = XWPFParagraph(newCTP, document) newParagraph.createRun().setText("新的文本框内容") // 替换原有段落 val parent = textBoxPara.parentTxbxContent parent.removeP(0) // 移除索引为0的原有段落 parent.addP(0, newCTP) // 插入新段落到索引0位置
注意事项
- Apache POI 4.1.2对WPS格式文本框的样式支持有限,如果修改后出现样式异常,可以手动给
XWPFParagraph对应的CTP节点复制原有段落的样式属性即可。 - 该方案仅支持docx格式文档,不支持doc格式。
内容的提问来源于stack exchange,提问作者Ranil Wijeyratne
相关产品推荐
相关产品推荐

