eec
eec copied to clipboard
模板导出咨询问题
使用模板导出时,支持列表横向遍历吗?我看wiki中的示例是纵向遍历的
还有个问题,是否支持分块写呢
- 目前不支持横向,只能在外部按横向布局做好数据给EEC。
- 分块不能直接支持,可以使用SimpleSheet直接输出,SimpleSheet不带样式,如果提前知道有多少组的话可以使用模板工作表
2. SimpleSheet
如果通过自定义新的Sheet方式,能否可行,需要重写哪几个方法
可以的,使用SimpleSheet需要使用自定义CellValueAndStyle来设置好样式,使用ListSheet同样可以做到
List<Object> data = new ArrayList<>();
data.add(Arrays.asList("姓名", "性别"));
data.add(Arrays.asList("张三", 1));
data.add(Arrays.asList("张四", 1));
data.add(null);
data.add(Arrays.asList("姓名", "性别"));
data.add(Arrays.asList("李四", 2));
data.add(Arrays.asList("李五", 2));
// 记录表头所在行,可以在for循环里记录
int[] headerRows = {0, 4};
new Workbook().addSheet(new SimpleSheet<>(data).setCellValueAndStyle(new XMLCellValueAndStyle() {
Integer headerXf = null, bodyXf = null;
public int getStyleIndex(org.ttzero.excel.entity.Row row, Column hc, Object o) {
int xf;
// 表头
if (Arrays.binarySearch(headerRows, row.index) >= 0) {
if (headerXf == null) {
// 初始化表头样式
Font font = new Font("宋体", 14).bold(); // 加粗
Fill fill = new Fill(PatternType.solid, new Color(228, 226, 226));
Border border = new Border(BorderStyle.THIN, Color.BLACK);
int style = hc.styles.addFont(font) | hc.styles.addBorder(border) | hc.styles.addFill(fill) | Horizontals.CENTER | Verticals.CENTER;
headerXf = hc.styles.of(style);
}
xf = headerXf;
row.height = 20.; // 表头设置20行高
}
// Body行
else {
if (bodyXf == null) {
// 初始化Body样式
Font font = new Font("宋体", 12);
Border border = new Border(BorderStyle.THIN, Color.BLACK);
int style = hc.styles.addFont(font) | hc.styles.addBorder(border) | Horizontals.CENTER | Verticals.CENTER;
bodyXf = hc.styles.of(style);
}
xf = bodyXf;
}
return xf;
}
})).writeTo(Paths.get("./ABC.xlsx"));
结合AI改写了一个Writer
package org.ttzero.excel.example;
import java.io.IOException;
import org.ttzero.excel.entity.Row;
import org.ttzero.excel.entity.RowBlock;
import org.ttzero.excel.entity.e7.XMLWorksheetWriter;
import org.ttzero.excel.reader.Cell;
/**
* 自定义分块写入器 BlockXMLWorksheetWriter
* <p>
* 扩展了标准的 XMLWorksheetWriter,支持在多次调用 writeData 时:
* 1. 自动插入间隔行 (Gap)。
* 2. 在间隔行中绘制分割线(如虚线)。
* 3. 自动重复打印表头。
* 4. 保持间隔行与数据行的高度一致性。
* <p>
* 核心概念:每一次调用 {@link #writeData} 都被视为一个独立的“分块”。
*/
public class BlockXMLWorksheetWriter extends XMLWorksheetWriter {
/**
* 分块之间的间隔行数,默认为 0(即不插入间隔)
*/
private int gap = 0;
/**
* 分割线(Gap中间的虚线)的样式索引 (Style Index)。
* -1 表示未设置,即使有 Gap 也不画线。
* 该索引通常通过 workbook.getStyles().addStyle(...) 获取。
*/
private int gapStyleIndex = -1;
/**
* 记录 Excel 中上一行实际写入的绝对行号 (Physical Row Number)。
* <p>
* 作用:在分块写入时,我们需要知道上一分块写到了哪里,以便计算下一分块数据的起始位置
* (包括 Gap 和 Header 的位置)。
*/
private int lastPhysicalRowNum = 0;
/**
* 状态标记:指示当前是否处于新分块数据的“起始”状态。
* <p>
* true: 表示下一行写入的数据是新分块的第一行,需要先触发 handleGapAndHeader 逻辑。
* false: 表示正在写入分块内的普通数据行。
*/
private boolean isBlockStart = false;
/**
* 设置分块间的间隔行数
*
* @param gap 行数 (必须 >= 0)
* @return this 用于链式调用
*/
public BlockXMLWorksheetWriter setGap(int gap) {
this.gap = Math.max(0, gap);
return this;
}
/**
* 设置分割线的样式索引
*
* @param gapStyleIndex 样式索引
* @return this 用于链式调用
*/
public BlockXMLWorksheetWriter setGapStyleIndex(int gapStyleIndex) {
this.gapStyleIndex = gapStyleIndex;
return this;
}
/**
* 重写数据块写入逻辑。
* 这是外部调用的主要入口,支持分块传入数据。
*
* @param rowBlock 数据行块
*/
@Override
public void writeData(RowBlock rowBlock) throws IOException {
// 如果数据块为空,直接返回,防止误触发表头写入或状态变更
if (!rowBlock.hasNext()) {
return;
}
// !ready 表示这是第一次调用 writeData (即第一分块数据)
// 此时还没有初始化文件流
if (!ready) {
// 执行父类标准逻辑:初始化文件 -> 写文件头 -> 写第一次表头 -> 写数据
// 父类内部会初始化 startRow
super.writeData(rowBlock);
} else {
// ready=true 表示这是后续的调用 (即第二分块及之后的数据)
// 标记“分块开始”,这将在 writeRow 方法中触发 Gap 插入逻辑
isBlockStart = true;
// 写入数据
// 根据是否有进度监听器选择不同的写入方法,逻辑与父类保持一致
if (progressConsumer == null) {
writeRowBlock(rowBlock);
} else {
writeRowBlockFireProgress(rowBlock);
}
// 更新总行数记录,供 Workbook 统计使用
totalRows = rowBlock.getTotal();
}
}
/**
* 重写单行写入逻辑。
* <p>
* 核心拦截点:
* 1. 拦截新分块的第一行,插入 Gap 和 Header。
* 2. 实时更新 lastPhysicalRowNum。
*/
@Override
protected void writeRow(Row row) throws IOException {
// 检测是否是新分块的第一行数据
if (isBlockStart) {
// 暂停当前数据的写入,先处理间隔行和表头
// 我们将当前行作为“模板 (Template)”,用于克隆行高等属性,
// 这样空白行的高度就会和数据行完全一致,视觉更美观。
handleGapAndHeader(row);
// 处理完毕,重置标记,后续行将正常写入
isBlockStart = false;
}
// 记录当前行在 Excel 中的绝对物理位置
// 公式:当前物理行 = 相对索引(row.index) + 偏移量(startRow)
this.lastPhysicalRowNum = row.getIndex() + startRow;
// 调用父类方法执行实际的 XML 写入
super.writeRow(row);
}
/**
* 处理间隔行(Gap)和表头(Header)的核心逻辑。
*
* @param templateRow 模板行(即新分块的第一行数据),用于克隆属性
*/
private void handleGapAndHeader(Row templateRow) throws IOException {
// 计算 Gap 区域的起始绝对行号:紧接着上一行数据
int gapStartRowNum = lastPhysicalRowNum + 1;
// 计算哪一行需要画分割线
final int separatorLineRow = getSeparatorLineRowNum(gapStartRowNum);
// 1. 写入 Gap 行
// 显式循环 gap 次,利用父类 super.writeRow 写入真实的空行
for (int i = 0; i < gap; i++) {
int targetPhysicalRow = gapStartRowNum + i;
// 判断当前行是否是需要画分割线的那一行
boolean isSeparator = (targetPhysicalRow == separatorLineRow);
// 克隆一个空行对象(复用模板行的行高,设置特定样式)
Row gapRow = cloneAndResetRow(templateRow, targetPhysicalRow, isSeparator);
// 调用父类写入 XML
// 注意:gapRow 内部已经调整了 index,配合当前的 startRow 能够定位到 targetPhysicalRow
super.writeRow(gapRow);
}
// 2. 写入表头
// Gap 写完后,紧接着是表头,计算表头的起始物理行号
int headerPhysicalRow = gapStartRowNum + gap;
// 临时调整 startRow 指针,让 writeHeaderRow() 知道从哪里开始写
startRow = headerPhysicalRow;
// 执行写表头操作,并获取表头占用的行数(支持多行表头)
int headerLines = writeHeaderRow();
// 3. 校准数据写入位置
// 表头写完后,下一行就是数据开始的位置
int dataStartPhysicalRow = headerPhysicalRow + headerLines;
// 反推 startRow (核心算法):
// 父类写入逻辑是:PhysicalRow = row.getIndex() + startRow
// 我们已知目标位置是 dataStartPhysicalRow,已知 row.getIndex()
// 所以:startRow = dataStartPhysicalRow - row.getIndex()
// 这样无论传入的分块数据 index 是从 0 开始还是从 100 开始,都能严丝合缝地接在表头后面
startRow = dataStartPhysicalRow - templateRow.getIndex();
}
/**
* 计算分割线所在的绝对行号
*
* @param gapStartRowNum Gap 区域的起始行号
* @return 分割线的绝对行号,-1 表示不画线
*/
private int getSeparatorLineRowNum(int gapStartRowNum) {
int separatorLineRow = -1;
if (gapStyleIndex != -1 && gap > 0) {
// 算法逻辑:将 Gap 分为两部分,多出的一行归上部(左部),线画在下部(右部)的第一行
// 示例 Gap=5: 5/2=2(整除), 5%2=1(余数) -> 左部大小=3.
// 偏移量 lineOffset = 3 + 1 = 4. (即在 Gap 的第4行画线)
int leftPartSize = (gap / 2) + (gap % 2);
int lineOffset = leftPartSize + 1;
// 边界保护:防止 Gap=1 时计算出 2 导致越界
lineOffset = Math.min(lineOffset, gap);
// 计算画线的绝对行号
separatorLineRow = gapStartRowNum + (lineOffset - 1);
}
return separatorLineRow;
}
/**
* 克隆并重置行对象。
* 创建一个用于写入 Gap 的伪造 Row 对象。
*
* @param source 模板源行 (分块数据行)
* @param targetPhysicalRow 该行在 Excel 中的目标绝对行号
* @param isSeparatorLine 是否是分割线行 (如果是,应用特殊样式)
* @return 构造好的 Row 对象
*/
private Row cloneAndResetRow(Row source, int targetPhysicalRow, boolean isSeparatorLine) {
// 1. 创建新 Row 对象
// 使用匿名内部类实例化,因为 Row 可能是抽象类或受保护的
Row newRow = new Row() {};
// 2. 设置行的相对索引
// 为了让 super.writeRow(newRow) 计算出 targetPhysicalRow
// newRow.index 必须等于 targetPhysicalRow - startRow
newRow.index = targetPhysicalRow - startRow;
// 3. 复制基础属性
// 继承数据行的行高,保证 Gap 行和数据行看起来高度一致,视觉更整齐
newRow.setHeight(source.getHeight());
// 4. 深度复制 Cells (构造空单元格)
Cell[] sourceCells = source.getCells();
// 处理空指针防御
int len = sourceCells == null ? 0 : sourceCells.length;
Cell[] newCells = new Cell[len];
if (sourceCells != null) {
for (int i = 0; i < len; i++) {
Cell srcCell = sourceCells[i];
// 防御 sourceCells 中可能存在的 null 元素
if (srcCell == null) {
continue;
}
// 创建新 Cell,保留列号 (i)
Cell newCell = new Cell(srcCell.i);
// 设置样式
if (isSeparatorLine) {
// 如果是分割线行,强制使用传入的边框样式 (gapStyleIndex)
newCell.xf = gapStyleIndex;
} else {
// 普通 Gap 行:
// 此时不设置 xf,或者保留默认值。
// 如果需要继承数据行的背景色,可以使用 newCell.xf = srcCell.xf;
}
// 注意:我们不设置 Cell 的值 (Value),也不调用 setString/setInt。
// 这样 Cell 的类型默认为空,写入 Excel 时仅作为占位符存在。
newCells[i] = newCell;
}
}
newRow.cells = newCells;
// 复制列范围指针,确保 writeRow 能够正确遍历列
newRow.fc = source.fc;
newRow.lc = source.lc;
return newRow;
}
}
调用
@Test
public void test2() throws IOException {
final Workbook workbook = new Workbook();
final Styles styles = workbook.getStyles();
final int gapStyleIndex = styles.of(styles.addBorder(new Border().setBorderTop(BorderStyle.DASHED, Color.black)));
ListSheet<User> sheet = new ListSheet<>();
final BlockXMLWorksheetWriter worksheetWriter = new BlockXMLWorksheetWriter().setGap(2).setGapStyleIndex(gapStyleIndex);
sheet.setSheetWriter(worksheetWriter);
sheet.setColumns(new Column[] {
new Column("姓名", "name"),
new Column("性别", "gender")
});
sheet.setRowHeight(25);
workbook.addSheet(sheet);
for (User user : User.mockUsers()) {
sheet.writeData(Collections.singletonList(user));
}
worksheetWriter.close();
final String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
workbook.writeTo(Paths.get("BlockListSheet-" + date + ".xlsx"));
}
输出
对了,现在有API设置打印参数吗
优秀!!!
打印参数可以在excel里设置好,然后将从源文件复制出来添加到自定义的WorksheetWriter中即可
希望可以引入到wiki示例中