easyExcel 单元格合并

需求

现在有一张员工表,需要将员工信息导出为excel,同一个部门放在一起,同一个工资段放在一起。

case

在这里插入图片描述

员工表

package com.tx.test.testeasyexcel.excel;import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import com.tx.test.testeasyexcel.excel.converter.LocalDateTimeConverter;
import lombok.Builder;
import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;/*** @description: 用户excel* @author: tx* @date: 2024/8/27 14:38* @version: 1.0*/
@Data
@Builder
@ColumnWidth(25)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class UserExcel implements Serializable {private static final long serialVersionUID = -5905730761779621234L;@ExcelProperty(value = {"用户信息", "部门"})private String dept;@ExcelProperty(value = {"用户信息", "部门描述"})private String deptDescription;@ExcelProperty(value = {"用户信息", "工资范围"})private String salaryRange;@ExcelProperty(value = {"用户信息", "工资"})private Double salary;@ExcelProperty(value = {"用户信息", "id"})private Long id;@ExcelProperty(value = {"用户信息", "姓名"})private String name;@ExcelProperty(value = {"用户信息", "生日"},converter = LocalDateTimeConverter.class)private LocalDateTime birthday;
}

处理

需要实现 easyExcel 的 CellWriteHandler 接口的 afterCellDispose 方法。
根据自定义的合并条件 添加或修改 Sheet 单元格范围地址信息(MergedRegions)

  • 我们先定义一个接口,用于自定义合并条件。接口有2个参数,一个是当前行,一个是上一行;返回结果是一个boolean值,true表示这两行的指定列需要合并需要,false表示不需要合并。
package com.tx.test.testeasyexcel.excel.handler;import org.apache.poi.ss.usermodel.Row;/*** @description: 合并条件接口* @author: tx* @date: 2024/8/27 16:49* @version: 1.0*/
public interface MergeCondition {/*** 判断是否需要合并* @param cur 当前行* @param pre 前一行* @return 是否需要合并*/boolean merged(Row cur, Row pre);
}
  • 实现easyexcel的CellWriteHandler 接口,实现其中的afterCellDispose 方法。

为了方便使用以及自定义条件合并,该类定义了4个成员变量,在使用该类进行处理时需要通过构造函数传入这4个变量。4个变量分别是:
private int optLastColumnIdx;// 合并列的条件判断中需要使用到的最后一列,避免在进行判断时无法获取到该列的数据
private int[] mergeColumns; // 需要合并的列的下标数组
private MergeCondition mc; // 合并条件
private int startIdx; // 数据的起始行
大致流程如下

Created with Raphaël 2.3.0 开始 获取当前单元格的坐标 rowIndex 和 columnIndex rowIndex <= startIdx 或者 optLastColumnIdx != columnIndex boolean merged 合并 no yes

值得注意的是单元格合并如果合并的上一行已经被合并了,需要先移除上一行单元格所在的合并范围地址信息,然后最后一行加1再添加进去。

package com.tx.test.testeasyexcel.excel.handler;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.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
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;/*** @description: 自定义单元格合并处理* @author: tx* @date: 2024/8/27 15:04* @version: 1.0*/
@Slf4j
public class customCellWriteHandler implements CellWriteHandler {private int optLastColumnIdx;// 需要合并的列private int[] mergeColumns;// 合并条件private MergeCondition mc;// 数据起始行private int startIdx;/*** @param optLastColumnIdx 操作需要使用的最后一列,*                         主要用于将合并操作定位到这一列,避免一些使用的到数据还未被填充到excel中,导致无法正常判断。* @param startIdx         数据的起始行,从0开始计数* @param mergeColumns     需要合并的列* @param mc               合并条件*/public customCellWriteHandler(int optLastColumnIdx, int startIdx, int[] mergeColumns, MergeCondition mc) {this.optLastColumnIdx = optLastColumnIdx;this.startIdx = startIdx;this.mergeColumns = mergeColumns;this.mc = mc;}@Overridepublic void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer integer, Integer integer1, Boolean aBoolean) {}@Overridepublic void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer integer, Boolean aBoolean) {}@Overridepublic void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer integer, Boolean aBoolean) {}@Overridepublic void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> list, Cell cell, Head head, Integer integer, Boolean aBoolean) {// 获取行号int rowIndex = cell.getRowIndex();// 获取列号int columnIndex = cell.getColumnIndex();// 行号 <= 数据所在的行,不做处理,直接返回,行号为数据行的第二行时才继续。 列号不等于需要使用的最后一列,直接返回if (rowIndex <= startIdx || optLastColumnIdx != columnIndex) return;// 获取处理结果boolean merged = mc.merged(cell.getSheet().getRow(rowIndex), cell.getSheet().getRow(rowIndex - 1));// 需要合并if (merged) {// 遍历需要合并的列for (int mergeColumn : mergeColumns) {// 标志位boolean isMerged = false;// 获取所有合并配置Sheet sheet = writeSheetHolder.getSheet();List<CellRangeAddress> mergeRegions = sheet.getMergedRegions();// 遍历合并配置for (int i = 0; i < mergeRegions.size(); i++) {CellRangeAddress cellRangeAddr = mergeRegions.get(i);// 上一行的单元格在配置中,移除这个配置,修改末尾行下标后重新加入一下。if (cellRangeAddr.isInRange(rowIndex - 1, mergeColumn)) {sheet.removeMergedRegion(i);cellRangeAddr.setLastRow(rowIndex);sheet.addMergedRegion(cellRangeAddr);// 修改标志位,表示在已经存在的合并配置中存在当前需要合并的信息。isMerged = true;break;}}// 新增合并单元if (!isMerged) {CellRangeAddress cellRangeAddress = new CellRangeAddress(rowIndex - 1, rowIndex, mergeColumn,mergeColumn);sheet.addMergedRegion(cellRangeAddress);}}}}
}

简单测试

  • 构造数据

这里设置了四个部门,每条数据随机选择。
2个工资范围段,分别是5000-10000,10000-15000。
名字是tx + 循环增加 i 。
数据创建完成后需要根据部门 和 工资范围排序。

        // 构造部门String dept[] = {"部门1", "部门2", "部门3", "bumen4"};// 工资范围int salaryRange[] = {5000, 10000, 15000};// 构造listList<UserExcel> excel = new ArrayList<>();for (int i = 0; i < 100; i++) {Instant instant = LocalDateTime.now().minusYears(Math.round(Math.random() * 20)).toInstant(ZoneOffset.ofHours(8));String randomDept = dept[(int) (Math.random() * 4)];String randomDeptDescription = randomDept + "的描述信息";Double salary = 5000 + BigDecimal.valueOf(Math.random() * 10000).setScale(2, BigDecimal.ROUND_UP).doubleValue();String salaryRangeString = "未知";for (int i1 = 0; i1 < salaryRange.length; i1++) {int end = salaryRange[i1];if (salary < end) {salaryRangeString = salaryRange[i1 - 1] + "-" + end;break;}}UserExcel user = UserExcel.builder().dept(randomDept).deptDescription(randomDeptDescription).id((long) i).name("tx-" + i).birthday(LocalDateTime.ofInstant(instant, ZoneId.systemDefault())).salary(salary).salaryRange(salaryRangeString).build();excel.add(user);}// 按照部门、工资范围排序excel.sort((one, two) -> {int i = one.getDept().compareTo(two.getDept());if (i == 0) {return one.getSalaryRange().compareTo(two.getSalaryRange());}return i;});
  • 设置文件保存路径
        // 文件保存路径String baseDir = System.getProperty("user.dir");String fileName = Paths.get(baseDir, "testExcel.xlsx").toString();
  • 合并单元格的excel导出
        EasyExcel.write(fileName, UserExcel.class).registerWriteHandler(new customCellWriteHandler(2, 2, new int[]{// 需要合并部门列、部门描述列、工资范围列0, 1, 2}, ((cur, pre) -> {// 部门名称相同 && 工资范围相同才需要合并String curDept = cur.getCell(0).getStringCellValue();String preDept = pre.getCell(0).getStringCellValue();String curSalaryRange = cur.getCell(2).getStringCellValue();String preSalaryRange = pre.getCell(2).getStringCellValue();return curDept.equals(preDept) && curSalaryRange.equals(preSalaryRange) ? true : false;}))).sheet("用户信息").doWrite(excel);

代码地址

https://gitee.com/tian_xiong/test-easy-excel

参考

https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write#%E5%90%88%E5%B9%B6%E5%8D%95%E5%85%83%E6%A0%BC
https://sensationg.github.io/blog/%E4%BD%BF%E7%94%A8EasyExcel%E8%87%AA%E5%AE%9A%E4%B9%89%E5%AF%BC%E5%87%BA/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/414330.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HSE软件组件有哪些?如何实现HSE与主机的通信(同步/异步)?如何使用HSE提供的安全服务?

《S32G3系列芯片——Boot详解》系列——HSE软件组件有哪些&#xff1f;如何实现HSE与主机的通信&#xff08;同步/异步&#xff09;&#xff1f;如何使用HSE提供的安全服务&#xff1f; 一、HSE子系统软件组件1.1 NXP交付用户的HSE固件内容1.2 HSE固件提供的安全服务1.3 HSE固件…

最新!yolov10+deepsort的目标跟踪实现

目录 yolov10介绍——实时端到端物体检测 概述 主要功能 型号 性能 方法 一致的双重任务分配&#xff0c;实现无 NMS 培训 效率-精度驱动的整体模型设计 提高效率 精度提升 实验和结果 比较 deepsort介绍&#xff1a; yolov10结合deepsort实现目标跟踪 效果展示…

记一次学习--webshell绕过(利用清洗函数)

目录 样本 样本修改 样本 <?php $a array("t", "system"); shuffle($a); $a[0]($_POST[1]); 通过 shuffle 函数打乱数组,然后通过$a[0]取出第一个元素&#xff0c;打乱后第一个元素可能是t也可能是system。然后再进行POST传参进行命令执行。 这里抓…

kubeadm部署 Kubernetes(k8s) 高可用集群【V1.28 】

kubeadm是官方社区推出的一个用于快速部署kubernetes集群的工具。 calico.yaml kubernertes-dashboard.yaml 1. 安装要求 在开始之前&#xff0c;部署Kubernetes集群机器需要满足以下几个条件&#xff1a; 10台机器&#xff0c;操作系统Openeuler22.03 LTS SP4硬件配置&…

豆包MarsCode编程助手:让编程更简单

在编程的浩瀚宇宙中&#xff0c;每一个开发者都在寻找那把能够开启高效与创意之门的钥匙。随着AI技术的飞速发展&#xff0c;智能编程助手应运而生&#xff0c;为开发者们带来了前所未有的便捷与灵感。今天&#xff0c;我们将以五子棋小游戏开发为例&#xff0c;深入解读豆包Ma…

Android 10.0 mtk平板camera2横屏预览旋转90度功能实现

1.前言 在10.0的系统rom定制化开发中,在进行一些平板等默认横屏的设备开发的过程中,需要在进入camera2的 时候,默认预览图像也是需要横屏显示的,所以就需要看下mtk的camera2的相关预览功能,然后看下进入 launcher camera的时候看下如何实现预览横屏显示 如图所示: 2.mtk平…

Android使用内容提供器(ContentProvider)实现跨程序数据共享

文章目录 Android使用内容提供器&#xff08;ContentProvider&#xff09;实现跨程序数据共享新建内容提供器DatabaseProvider修改DatabaseProvider中的代码AndroidManifest.xml文件中注册provider修改activity_main.xml中的代码修改MainActivity中的代码运行ProviderTest项目 …

WEB开发---使用HTML CSS开发网页实时显示当前日期和时间

自己刚开始学习html css知识&#xff0c;临时做个网页&#xff0c;实时显示当前日期和时间功能。 代码如下&#xff1a; test.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport&q…

32力扣 最长有效括号

dp方法&#xff1a; class Solution { public:int longestValidParentheses(string s) {int ns.size();vector<int> dp(n,0);if(n0 || n1) return 0;if(s[0]( && s[1])){dp[1]2;}for(int i2;i<n;i){if(s[i])){if(s[i-1](){dp[i]dp[i-2]2;}else if(s[i-1])){i…

【Kubernetes】持久卷 PV

《持久化存储》系列&#xff0c;共包含以下文章&#xff1a; K8s 持久化存储方式持久卷 PV持久卷声明 PVC持久卷的动态供给 Dynamic Provisioning &#x1f60a; 如果您觉得这篇文章有用 ✔️ 的话&#xff0c;请给博主一个一键三连 &#x1f680;&#x1f680;&#x1f680; …

前端进阶| 深入学习面向对象设计原则

引言 面向对象编程&#xff08;Object-Oriented Programming&#xff0c;OOP&#xff09;是一种常用的编程范式&#xff0c;它通过将数据和与之相关的操作封装在一起&#xff0c;提供了一种更有组织和易于理解的方式来构建应用程序。在JavaScript中&#xff0c;我们可以使用面…

【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)

看到这句话的时候证明&#xff1a;此刻你我都在努力 加油陌生人 个人主页&#xff1a;Gu Gu Study 专栏&#xff1a;用Java学习数据结构系列 喜欢的一句话&#xff1a; 常常会回顾努力的自己&#xff0c;所以要为自己的努力留下足迹 喜欢的话可以点个赞谢谢了。 作者&#xff…

Sql查询优化--索引设计与sql优化(包含慢查询定位+explain解释计划+左匹配原则+索引失效)

本文介绍了数据库查询的索引优化方法&#xff0c;依次介绍了慢查询语句定位方法、索引设计与sql语句优化方法&#xff0c;并介绍了左匹配原则和索引失效的场景&#xff0c;最后介绍了explain执行计划要怎么看以调整检验索引设计是否生效和效率情况&#xff0c;创新介绍了如何以…

Visual Studio Code 自定义字体大小

常用编程软件自定义字体大全首页 文章目录 前言具体操作1. 打开首选项设置对话框2. 在Font Family里面输入字体 前言 Visual Studio Code 自定义字体大小&#xff0c;统一设置为 Cascadia Code SemiBold &#xff0c;大小为 14 具体操作 【文件】>【首选项】>【设置】&…

18037 20秒后的时间

### 思路 1. 读取输入的时间&#xff0c;格式为小时:分钟:秒。 2. 将时间转换为秒数。 3. 增加20秒。 4. 将增加后的秒数转换回小时:分钟:秒格式。 5. 输出结果&#xff0c;确保小时、分钟和秒均占两个数字位&#xff0c;不足位用0补足。 ### 伪代码 1. 读取输入的时间字符串。…

day35-测试之性能测试JMeter的测试报告、并发数计算和性能监控

目录 一、JMeter的测试报告 1.1.聚合报告 1.2.html报告 二、JMeter的并发数计算 2.1.性能测试时的TPS&#xff0c;大都是根据用户真实的业务数据&#xff08;运营数据&#xff09;来计算的 2.2.运营数据 2.3.普通计算方法 2.4.二八原则计算方法 2.5.计算稳定性测试并发量 2.6…

vscode中如何设置不显示隐藏文件

在vscode中&#xff0c;有时候&#xff0c;会显示一些隐藏文件&#xff0c;如何设置让其不显示呢&#xff1f; 解决办法 例如&#xff1a;我这里有一个.vscode隐藏文件夹&#xff0c;是vscode默认生成的一个配置目录&#xff0c;我想要它不在资源管理器中进行显示。 操作步骤&a…

Java 入门指南:Java 并发编程 —— Condition 灵活管理线程间的同步

Condition Condition 是 Java 并发编程中的一种高级同步工具&#xff0c;它可以协助线程之间进行等待和通信。提供了一种比传统的 wait() 和 notify() 更加灵活的方式来管理线程间的同步。Condition 接口通常与 Lock 接口一起使用&#xff0c;允许更细粒度的控制线程的等待和唤…

Python 从入门到实战4(序列的操作)

我们的目标是&#xff1a;通过这一套资料学习下来&#xff0c;通过熟练掌握python基础&#xff0c;然后结合经典实例、实践相结合&#xff0c;使我们完全掌握python&#xff0c;并做到独立完成项目开发的能力。 上篇文章我们通过举例学习了python 中列表的简单操作&#xff0c;…

Android CCodec Codec2 (六)C2InterfaceHelper

通过前面几篇文章的学习&#xff0c;我们知道了Codec2参数结构&#xff0c;以及如何定义一个Codec2参数。接下来的几篇文章我们将简单了解上层是如何请求组件支持的参数、如何配置参数&#xff0c;以及参数是如何反射给上层的。本篇文章我们将了解接口参数实例化。 1、C2Interf…