Apache POI 导出 Word 踩坑实录:Word 分栏为什么做不好左右平铺?
文章目录:
- [Apache POI 导出 Word 踩坑实录:Word 分栏为什么做不好左右平铺?](#Apache POI 导出 Word 踩坑实录:Word 分栏为什么做不好左右平铺?)
-
- 前言
- 一、需求本质:要的是网格,不是分栏
- [二、为什么直接用 Word 分栏不靠谱?](#二、为什么直接用 Word 分栏不靠谱?)
-
- [1)POI 对分栏支持并不友好](#1)POI 对分栏支持并不友好)
- 2)双栏是流式的,不是左右配对的
- 3)内容高度不一致时,布局很容易失衡
- 4)分页控制难,兼容性也不稳定
- 5)绝对定位不适合动态数据
- [三、最终稳定方案:用 2 列表格模拟"左右平铺"](#三、最终稳定方案:用 2 列表格模拟“左右平铺”)
- 四、为什么表格方案更稳?
- 五、实现思路
- [六、Maven 依赖](#六、Maven 依赖)
- 七、完整示例代码
- 八、几个很容易忽略的细节
- 九、结论
-
- [1. Word 的"分栏"不是"左右平铺"](#1. Word 的“分栏”不是“左右平铺”)
- [2. 动态内容场景下,真分栏很难稳定](#2. 动态内容场景下,真分栏很难稳定)
- [3. 2 列表格才是更适合业务场景的方案](#3. 2 列表格才是更适合业务场景的方案)
- [4. 真正影响稳定性的关键点是这些](#4. 真正影响稳定性的关键点是这些)
- 十、最后一句
前言
最近做一个 Word 导出需求:一页内容左右排布,每行展示两个数据块,数据条数不固定。
理想效果如下:
text
设备A 设备B
设备C 设备D
设备E 设备F
一开始我以为这就是 Word 的"双栏"场景,直接用分栏即可。
真正实践后才发现:
Word 分栏适合文章排版,不适合业务数据平铺。
如果你也在用 Java 导出 Word,并且遇到"左右分栏平铺"的需求,这篇文章可以帮你少踩不少坑。
一、需求本质:要的是网格,不是分栏
业务期望的是这种布局:
text
1 | 2
3 | 4
5 | 6
也就是说:
- 第 1 条放左边
- 第 2 条放右边
- 第 3 条换到下一行左边
- 第 4 条换到下一行右边
这本质上是 二维网格布局。
但 Word 的"分栏"并不是按行排,它是 流式排版,逻辑更像报纸:
- 先填满左栏
- 左栏满了再流到右栏
- 不保证同一行左右对应
所以结论很明确:
Word 分栏 ≠ 左右平铺
二、为什么直接用 Word 分栏不靠谱?
1)POI 对分栏支持并不友好
在 XWPFDocument、XWPFParagraph 这些常用 API 里,你基本找不到直接设置"双栏"的高层方法。
如果一定要做,只能下钻到底层 XML,操作 CTSectPr 和 CTColumns。
例如:
java
CTSectPr sectPr = document.getDocument().getBody().isSetSectPr()
? document.getDocument().getBody().getSectPr()
: document.getDocument().getBody().addNewSectPr();
CTColumns cols = sectPr.isSetCols() ? sectPr.getCols() : sectPr.addNewCols();
cols.setNum(BigInteger.valueOf(2));
cols.setSpace(BigInteger.valueOf(720));
看起来好像设置成功了,但这只是告诉 Word:这是双栏结构 。
它并不会帮你实现"左右配对平铺"。
2)双栏是流式的,不是左右配对的
你以为它会排成:
text
A | B
C | D
实际上更可能是:
text
A
B
C
D
...
左栏排满后才进入右栏
这就是最核心的问题:
业务要的是"同行左右对应",Word 给的是"内容自动流动"。
3)内容高度不一致时,布局很容易失衡
真实业务里,数据块很少完全一样高:
- 标题长短不同
- 文本行数不同
- 某些块带图片、二维码
- 字段可能换行
一旦左右内容高度不一致,视觉上就会非常乱。
左边可能很长,右边很短,整页看起来不整齐。
4)分页控制难,兼容性也不稳定
当你把 分栏 + 动态内容 + 分页 放在一起时,问题会更多:
- 内容被拆到下一页
- 一页留大块空白
- 左右栏分页不同步
- Word 和 WPS 显示效果不完全一致
如果还涉及打印场景,维护成本会更高。
5)绝对定位不适合动态数据
有些人会想到用文本框、浮动对象、绝对坐标来排版。
理论上可行,但不适合动态列表场景。
因为它会涉及:
- 锚点
- 坐标偏移
- 文本环绕
- 换页行为
- 软件兼容性
如果不是固定模板、固定数据量,一般不建议这样做。
三、最终稳定方案:用 2 列表格模拟"左右平铺"
踩完坑之后,最终最稳的方案其实很简单:
不要真用 Word 分栏,而是用 2 列表格模拟左右平铺。
实现方式:
- 每次取 2 条数据
- 生成 1 行 2 列
- 左单元格放第 1 条
- 右单元格放第 2 条
- 如果是奇数条,最后一个单元格留空
这样天然就是:
text
A | B
C | D
E | F
这才是真正符合业务语义的布局。
四、为什么表格方案更稳?
因为这个需求本质上就是 网格布局,而 Word 里最稳定的网格容器就是表格。
表格方案的优势
1. 天然支持左右成对展示
它本来就是行列结构,不是流式布局。
2. 内容高度不一致也不会跑位
左右内容在同一行,整体更稳定。
3. 分页更可控
你可以按页生成表格,而不是交给 Word 自动处理。
4. Word / WPS 兼容性更好
相比浮动对象和复杂分栏,表格通常更稳定。
五、实现思路
核心思路只有一句话:
不要让 Word 自己排"分栏",而是你自己按"2 列 1 行"喂数据。
流程如下:
- 创建一个 2 列表格
- 设置固定布局,防止列宽被内容撑开
- 每两条数据生成一行
- 左右单元格分别渲染卡片内容
- 奇数条时,最后一个右单元格置空
六、Maven 依赖
xml
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
七、完整示例代码
java
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
public class WordTileDemo {
// A4 纸张大小(twips)
private static final int A4_WIDTH = 11906;
private static final int A4_HEIGHT = 16838;
private static final int MARGIN = 720;
private static final int PAGE_WIDTH = A4_WIDTH - MARGIN * 2;
private static final int COL_WIDTH = PAGE_WIDTH / 2;
public static void main(String[] args) throws Exception {
List<CardData> list = Arrays.asList(
new CardData("设备A", "温室1", "在线", "25.6℃"),
new CardData("设备B", "温室2", "离线", "23.1℃"),
new CardData("设备C", "温室3", "在线", "24.8℃"),
new CardData("设备D", "温室4", "在线", "26.0℃"),
new CardData("设备E", "温室5", "维护中", "22.5℃")
);
try (XWPFDocument document = new XWPFDocument()) {
setupPage(document);
XWPFTable table = createTileTable(document);
int rowIndex = 0;
for (int i = 0; i < list.size(); i += 2) {
XWPFTableRow row = rowIndex == 0 ? table.getRow(0) : table.createRow();
row.setCantSplitRow(true);
XWPFTableCell leftCell = row.getCell(0);
XWPFTableCell rightCell = row.getCell(1);
setCellWidth(leftCell, COL_WIDTH);
setCellWidth(rightCell, COL_WIDTH);
renderCard(leftCell, list.get(i));
if (i + 1 < list.size()) {
renderCard(rightCell, list.get(i + 1));
} else {
clearCell(rightCell);
}
rowIndex++;
}
try (FileOutputStream out = new FileOutputStream("左右平铺-demo.docx")) {
document.write(out);
}
}
}
private static void setupPage(XWPFDocument document) {
CTSectPr sectPr = document.getDocument().getBody().isSetSectPr()
? document.getDocument().getBody().getSectPr()
: document.getDocument().getBody().addNewSectPr();
CTPageSz pageSize = sectPr.isSetPgSz() ? sectPr.getPgSz() : sectPr.addNewPgSz();
pageSize.setW(BigInteger.valueOf(A4_WIDTH));
pageSize.setH(BigInteger.valueOf(A4_HEIGHT));
CTPageMar pageMar = sectPr.isSetPgMar() ? sectPr.getPgMar() : sectPr.addNewPgMar();
pageMar.setTop(BigInteger.valueOf(MARGIN));
pageMar.setBottom(BigInteger.valueOf(MARGIN));
pageMar.setLeft(BigInteger.valueOf(MARGIN));
pageMar.setRight(BigInteger.valueOf(MARGIN));
}
private static XWPFTable createTileTable(XWPFDocument document) {
XWPFTable table = document.createTable(1, 2);
CTTblPr tblPr = table.getCTTbl().getTblPr();
if (tblPr == null) {
tblPr = table.getCTTbl().addNewTblPr();
}
// 表格总宽度
CTTblWidth tblWidth = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
tblWidth.setType(STTblWidth.DXA);
tblWidth.setW(BigInteger.valueOf(PAGE_WIDTH));
// 固定布局,防止内容撑乱列宽
CTTblLayoutType layoutType = tblPr.isSetTblLayout() ? tblPr.getTblLayout() : tblPr.addNewTblLayout();
layoutType.setType(STTblLayoutType.FIXED);
// 去掉边框
CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders();
borders.addNewTop().setVal(STBorder.NONE);
borders.addNewBottom().setVal(STBorder.NONE);
borders.addNewLeft().setVal(STBorder.NONE);
borders.addNewRight().setVal(STBorder.NONE);
borders.addNewInsideH().setVal(STBorder.NONE);
borders.addNewInsideV().setVal(STBorder.NONE);
table.setCellMargins(80, 80, 80, 80);
return table;
}
private static void setCellWidth(XWPFTableCell cell, int width) {
CTTcPr tcPr = cell.getCTTc().isSetTcPr()
? cell.getCTTc().getTcPr()
: cell.getCTTc().addNewTcPr();
CTTblWidth tcW = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
tcW.setType(STTblWidth.DXA);
tcW.setW(BigInteger.valueOf(width));
cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.TOP);
}
private static void renderCard(XWPFTableCell cell, CardData data) {
clearCell(cell);
XWPFParagraph title = cell.getParagraphs().get(0);
title.setSpacingBefore(0);
title.setSpacingAfter(80);
XWPFRun titleRun = title.createRun();
titleRun.setBold(true);
titleRun.setFontSize(12);
titleRun.setText(data.getName());
addLine(cell, "区域:", data.getArea());
addLine(cell, "状态:", data.getStatus());
addLine(cell, "温度:", data.getTemp());
}
private static void addLine(XWPFTableCell cell, String label, String value) {
XWPFParagraph p = cell.addParagraph();
p.setSpacingBefore(0);
p.setSpacingAfter(0);
XWPFRun labelRun = p.createRun();
labelRun.setBold(true);
labelRun.setText(label);
XWPFRun valueRun = p.createRun();
valueRun.setText(value == null ? "" : value);
}
private static void clearCell(XWPFTableCell cell) {
int size = cell.getParagraphs().size();
for (int i = size - 1; i >= 0; i--) {
cell.removeParagraph(i);
}
cell.addParagraph();
}
static class CardData {
private final String name;
private final String area;
private final String status;
private final String temp;
public CardData(String name, String area, String status, String temp) {
this.name = name;
this.area = area;
this.status = status;
this.temp = temp;
}
public String getName() {
return name;
}
public String getArea() {
return area;
}
public String getStatus() {
return status;
}
public String getTemp() {
return temp;
}
}
}
八、几个很容易忽略的细节
1)一定要设置表格固定布局
这行代码非常关键:
java
layoutType.setType(STTblLayoutType.FIXED);
如果不设置固定布局,Word 会根据内容自动调整列宽。
一旦某列出现长文本,整个表格就可能被撑变形。
2)单元格默认段落要先清掉
Word 单元格默认会自带一个段落。
如果不先清理,常见问题就是:
- 顶部多空行
- 内容不对齐
- 间距异常
所以在渲染内容前,建议先执行:
java
clearCell(cell);
3)段前段后间距尽量手动控制
Word 默认段落间距并不稳定,建议统一设置:
java
paragraph.setSpacingBefore(0);
paragraph.setSpacingAfter(0);
标题如果需要留白,可以单独设置 spacingAfter。
4)图片、二维码尺寸要统一
如果卡片里还有:
- 二维码
- logo
- 设备图片
建议统一宽高,否则单元格高度会差异很大,排版效果容易变差。
5)如果要控制"每页固定几个块",建议按页生成表格
例如你想做到:
- 每页固定 8 个块
- 每页 4 行 2 列
最稳的做法不是在一张超长表格里强行分页,而是:
- 每页生成一个独立表格
- 填满后插入分页符
- 再生成下一页
这样稳定性会明显更好。
九、结论
这次实践之后,我的结论非常明确:
1. Word 的"分栏"不是"左右平铺"
它是流式排版,不是网格布局。
2. 动态内容场景下,真分栏很难稳定
尤其是内容高度不一致、还要分页时,控制成本很高。
3. 2 列表格才是更适合业务场景的方案
它天然符合"左右并排、按行展示"的需求。
4. 真正影响稳定性的关键点是这些
- 固定表格布局
- 固定列宽
- 清理默认段落
- 控制段落间距
- 按页分表
十、最后一句
分栏是文章排版,表格才是业务布局。
如果你的需求是 Java 导出 Word,实现左右平铺、标签打印、卡片列表、二维码并排展示,优先考虑表格方案,通常会更稳,也更容易维护。