eec icon indicating copy to clipboard operation
eec copied to clipboard

模板导出咨询问题

Open cnscottluo opened this issue 3 months ago • 7 comments

使用模板导出时,支持列表横向遍历吗?我看wiki中的示例是纵向遍历的

Image

cnscottluo avatar Nov 26 '25 01:11 cnscottluo

还有个问题,是否支持分块写呢

Image

cnscottluo avatar Nov 26 '25 01:11 cnscottluo

  1. 目前不支持横向,只能在外部按横向布局做好数据给EEC。
  2. 分块不能直接支持,可以使用SimpleSheet直接输出,SimpleSheet不带样式,如果提前知道有多少组的话可以使用模板工作表

wangguanquan avatar Nov 26 '25 05:11 wangguanquan

2. SimpleSheet

如果通过自定义新的Sheet方式,能否可行,需要重写哪几个方法

cnscottluo avatar Nov 26 '25 06:11 cnscottluo

可以的,使用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"));

wangguanquan avatar Nov 26 '25 07:11 wangguanquan

结合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"));
    }

输出

Image

对了,现在有API设置打印参数吗

cnscottluo avatar Nov 26 '25 15:11 cnscottluo

优秀!!!

打印参数可以在excel里设置好,然后将从源文件复制出来添加到自定义的WorksheetWriter中即可

wangguanquan avatar Nov 27 '25 01:11 wangguanquan

希望可以引入到wiki示例中

LSL1618 avatar Dec 11 '25 03:12 LSL1618