EasyExcel 动态表头 + 数据单元格合并

前言

本文想要达到以及最终实现的效果:

在这里插入图片描述

要实现这种效果,包含两个部分的操作:

1. 动态表头

EasyExcel 生成 Excel 时要使表头有合并效果,可以采用**注解和非注解(动态表头)**的方法。因为要导出的数据没有实体类,所以采用动态表头的方法。表头用一个 List<List<String>> 对象来封装,其中外层 List 表示每一列的表头,内层 List 表示同一列的多行表头,当同一列或同一行的数据相同时,会进行表头的合并。

2. 数据合并

数据合并本文实现了两种方法:一是相邻行的数据相同时自动合并,二是指定要合并的行列范围。具体思路请看代码中的注释。合并相同数据行的方法与目标结果有些微差别:

在这里插入图片描述

pom.xml

<dependencies>
    <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>2.2.6</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.22</version>
    </dependency>
</dependencies>

MergeSameRowsStrategy

package com.kuang;

import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import lombok.Data;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.List;

/**
 * 合并相同数据行策略
 *
 * @author wangbo
 * @since 2022年01月25日 10:16
 */
@Data
public class MergeSameRowsStrategy implements CellWriteHandler {
    private int[] mergeColumnIndex;
    private int mergeRowIndex;

    public MergeSameRowsStrategy() {
    }

    public MergeSameRowsStrategy(int mergeRowIndex, int[] mergeColumnIndex) {
        this.mergeRowIndex = mergeRowIndex;
        this.mergeColumnIndex = mergeColumnIndex;
    }

    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer integer, Boolean aBoolean) {

    }

    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> list, Cell cell, Head head, Integer integer, Boolean aBoolean) {
        // 当前行
        int curRowIndex = cell.getRowIndex();
        // 当前列
        int curColIndex = cell.getColumnIndex();

        if (curRowIndex > mergeRowIndex) {
            for (int i = 0; i < mergeColumnIndex.length; i++) {
                if (curColIndex == mergeColumnIndex[i]) {
                    mergeWithPrevRow(writeSheetHolder, cell, curRowIndex, curColIndex);
                    break;
                }
            }
        }
    }

    /**
     * 当前单元格向上合并
     *
     * @param writeSheetHolder sheet保持对象
     * @param cell             当前单元格
     * @param curRowIndex      当前行
     * @param curColIndex      当前列
     */
    private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int curRowIndex, int curColIndex) {
        // 获取当前行的当前列的数据和上一行的当前列列数据,通过上一行数据是否相同进行合并
        Object curData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
        Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
        Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();

        // 比较当前行的第一列的单元格与上一行是否相同,相同合并当前单元格与上一行
        if (curData.equals(preData)) {
            Sheet sheet = writeSheetHolder.getSheet();
            List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
            boolean isMerged = false;
            for (int i = 0; i < mergedRegions.size() && !isMerged; i++) {
                CellRangeAddress cellRangeAddr = mergedRegions.get(i);
                // 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
                if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
                    sheet.removeMergedRegion(i);
                    cellRangeAddr.setLastRow(curRowIndex);
                    sheet.addMergedRegion(cellRangeAddr);
                    isMerged = true;
                }
            }
            // 若上一个单元格未被合并,则新增合并单元
            if (!isMerged) {
                CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
                sheet.addMergedRegion(cellRangeAddress);
            }
        }
    }
}

AssignRowsAndColumnsToMergeStrategy

import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.List;

/**
 * 指定合并行列范围策略
 *
 * @author wangbo
 * @since 2022年01月21日 20:31
 */

public class AssignRowsAndColumnsToMergeStrategy extends AbstractMergeStrategy {
    // 合并坐标集合
    private List<CellRangeAddress> cellRangeAddresses;

    // 构造
    public AssignRowsAndColumnsToMergeStrategy() {
    }

    public AssignRowsAndColumnsToMergeStrategy(List<CellRangeAddress> cellRangeAddresses) {
        this.cellRangeAddresses = cellRangeAddresses;
    }

    /**
     * 合并操作:对每个单元格执行!!!
     *
     * @param sheet            sheet对象
     * @param cell             当前单元格
     * @param head             表头对象
     * @param relativeRowIndex 相关行索引
     */
    @Override
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
        /*
         * 合并单元格
         *
         * 由于merge()方法会在写每个单元格(cell)时执行,因此需要保证合并策略只被添加一次。否则如果每个单元格都添加一次
         * 合并策略,则会造成重复合并。例如合并A2:A3,当cell为A2时,合并A2:A3,但是当cell为A3时,又要合并A2:A3,而此时
         * 的A2已经是之前的A2和A3合并后的结果了。
         * 由于此处的表头占了两行,因此数据单元格是从(2, 0)开始的,所以就对这个单元格(cell.getRowIndex() == 2 && cell.getColumnIndex() == 0)
         * 添加一次合并策略就可以了。如果表头只有一行,则判断条件改为「cell.getRowIndex() == 1 && cell.getColumnIndex() == 0」就可以了。
         */
        if (cell.getRowIndex() == 2 && cell.getColumnIndex() == 0) {
            for (CellRangeAddress item : cellRangeAddresses) {
                sheet.addMergedRegion(item);
            }
        }

        /*
         * 如果不作判断,可以使用addMergedRegionUnsafe()方法,
         * 这样生成的Excel文件可以打开,只是打开时会提示内容有问题,修复后可以打开
         */
        // for (CellRangeAddress item : cellRangeAddresses) {
        //     sheet.addMergedRegionUnsafe(item);
        // }
    }
}

MergeTest

import com.alibaba.excel.EasyExcel;
import com.kuang.MergeSameRowsStrategy;
import org.apache.poi.ss.util.CellRangeAddress;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author wangbo
 * @since 2022年01月21日 20:29
 */

public class MergeTest {

    private static String fileName = "D:\\" + System.currentTimeMillis() + ".xlsx";

    @Test
    public void test01() {
        EasyExcel.write(fileName)
                .head(header())
                // 合并策略:合并相同数据的行。第一个参数表示从哪一行开始进行合并,由于表头占了两行,因此从第2行开始(索引从0开始)
                // 第二个参数是指定哪些列要进行合并
                .registerWriteHandler(new MergeSameRowsStrategy(2, new int[]{0, 1, 2, 3, 8, 9}))
                // 注意:需要先调用registerWriteHandler()再调用sheet()方法才能使合并策略生效!!!
                .sheet("模板")
                .doWrite(data());
    }

    @Test
    public void test02() {
        // 合并策略:指定要合并的行列范围
        int[][] toMergeRows = {{2, 3}, {4, 6}};
        int[] toMergeColumns = {0, 1, 2, 3, 8, 9};
        List<CellRangeAddress> list = new ArrayList<>();
        for (int[] toMergeRow : toMergeRows) {
            for (int toMergeColumn : toMergeColumns) {
                list.add(new CellRangeAddress(toMergeRow[0], toMergeRow[1], toMergeColumn, toMergeColumn));
            }
        }

        EasyExcel.write(fileName)
                .head(header())
                .registerWriteHandler(new AssignRowsAndColumnsToMergeStrategy(list))
                .sheet("模板")
                .doWrite(data());
    }


    /**
     * 创建表头
     */
    private List<List<String>> header() {
        List<List<String>> headers = new ArrayList<>();
        headers.add(Arrays.asList("提交人用户名", "提交人用户名"));
        headers.add(Arrays.asList("提交人姓名", "提交人姓名"));
        headers.add(Arrays.asList("创建时间", "创建时间"));
        headers.add(Arrays.asList("更新时间", "更新时间"));
        headers.add(Arrays.asList("学习经历", "时间"));
        headers.add(Arrays.asList("学习经历", "学校"));
        headers.add(Arrays.asList("学习经历", "专业"));
        headers.add(Arrays.asList("学习经历", "学位"));
        headers.add(Arrays.asList("工作单位", "工作单位"));
        headers.add(Arrays.asList("国籍", "国籍"));
        headers.add(Arrays.asList("获奖经历", "时间"));
        headers.add(Arrays.asList("获奖经历", "何种奖励"));
        return headers;
    }

    /**
     * 创建数据
     */
    private List<List<Object>> data() {
        List<List<Object>> data = new ArrayList<>();
        data.add(Arrays.asList("fengqingyang", "风清扬", "2022-01-25 11:08", "2022-01-25 11:08",
                "2013.9 ~ 2017.7", "华山派", "剑宗", "剑宗高手", "隐居思过崖", "中国", "2015.12", "华山剑法高手"));
        data.add(Arrays.asList("fengqingyang", "风清扬", "2022-01-25 11:08", "2022-01-25 11:08",
                "2017.9 ~ 2020.7", "独孤求败", "独孤剑法", "剑术通神", "隐居思过崖", "中国", "2019.12", "剑法高手"));
        data.add(Arrays.asList("linghuchong", "令狐冲", "2022-01-25 12:08", "2022-01-25 12:08",
                "2020.9 ~ 2024.7", "华山派", "气宗", "气宗庸手", "漂泊江湖", "中国", "2022.12", "华山剑法庸手"));
        data.add(Arrays.asList("linghuchong", "令狐冲", "2022-01-25 12:08", "2022-01-25 12:08",
                "2024.9 ~ 2027.7", "风清扬", "独孤剑法", "剑法高手", "漂泊江湖", "中国", "2025.12", "剑法高手"));
        data.add(Arrays.asList("linghuchong", "令狐冲", "2022-01-25 12:08", "2022-01-25 12:08",
                "2027.9 ~ 2030.7", "少林寺", "易筋经", "内功高手", "漂泊江湖", "中国", "2029.12", "内功高手"));
        return data;
    }
}

参考

  1. EasyExcel · 语雀 (yuque.com)
  2. EasyExcel 动态表头生成且带合并
  3. easyexcel 复杂表头、动态表头、复杂数据导出(非注解方式)
  4. easyExcel实现动态表头的数据导出,合并单元格,列宽策略
  5. easyExcel 导出 合并表头 合并相同数据单元格
  6. easyexcel 合并单元格(非注解)
可以通过实现EasyExcel的WriteHandler接口来实现相同数据合并单元格的功能。具体步骤如下: 1. 新建一个类实现WriteHandler接口,并重写对应的方法。 2. 在实现的方法中,通过Excel的API获取到要合并单元格的起始行、起始、结束行、结束。可以使用Map来记录每一种相同数据的位置信息,然后再遍历Map合并单元格。 3. 使用EasyExcel的write方法时,通过指定Handler参数,将编写好的WriteHandler实例传递进去即可。 下面是示例代码: ```java public class MergeCellWriteHandler implements WriteHandler { private Map<String, List<CellRangeAddress>> mergeMap = new HashMap<>(); @Override public void row(int i, List<Object> list) { //处理行数据,将相同的数据合并单元格 String key = list.get(0).toString(); //以第一为key if (mergeMap.containsKey(key)) { List<CellRangeAddress> cellRangeList = mergeMap.get(key); CellRangeAddress lastCellRange = cellRangeList.get(cellRangeList.size() - 1); if (i - lastCellRange.getLastRow() == 1) { //如果上一个单元格的结尾行是当前行的上一行,则可以合并单元格 lastCellRange.setLastRow(i); } else { cellRangeList.add(new CellRangeAddress(i, i, 0, list.size() - 1)); //如果不连续,则新建一个单元格范围 } } else { List<CellRangeAddress> cellRangeList = new ArrayList<>(); cellRangeList.add(new CellRangeAddress(i, i, 0, list.size() - 1)); mergeMap.put(key, cellRangeList); } } @Override public void sheet(int i) { } @Override public void start() { } @Override public void end() { //处理完整个sheet后,将记录的单元格范围进行合并 Sheet sheet = EasyExcel.writerSheet().build().getSheet(); for (Map.Entry<String, List<CellRangeAddress>> entry : mergeMap.entrySet()) { String key = entry.getKey(); List<CellRangeAddress> cellRangeList = entry.getValue(); for (CellRangeAddress cellRange : cellRangeList) { sheet.addMergedRegion(cellRange); } } } } ``` 使用方法: ```java EasyExcel.write("test.xlsx") .sheet() .registerWriteHandler(new MergeCellWriteHandler()) .head(head) .doWrite(data); ``` 其中head和data分别是表头数据,可以从数据库或其他数据源中获取。要实现合并单元格,需要将第一相同的数据合并。示例中以第一为key,记录每一种相同数据所对应的单元格范围,最后进行合并
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值