diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/reflect/ClassScanner.java b/hutool-core/src/main/java/org/dromara/hutool/core/reflect/ClassScanner.java index 796643d1c..61862f1ea 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/reflect/ClassScanner.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/reflect/ClassScanner.java @@ -102,7 +102,7 @@ public class ClassScanner implements Serializable { /** * 扫描指定包路径下所有包含指定注解的类,包括其他加载的jar或者类 * - * @param packageName 包路径 + * @param packageName 包路径,{@code null}表示扫描全部 * @param annotationClass 注解类 * @return 类集合 */ @@ -114,7 +114,7 @@ public class ClassScanner implements Serializable { * 扫描指定包路径下所有包含指定注解的类
* 如果classpath下已经有类,不再扫描其他加载的jar或者类 * - * @param packageName 包路径 + * @param packageName 包路径,{@code null}表示扫描全部 * @param annotationClass 注解类 * @return 类集合 */ @@ -125,7 +125,7 @@ public class ClassScanner implements Serializable { /** * 扫描指定包路径下所有指定类或接口的子类或实现类,不包括指定父类本身,包括其他加载的jar或者类 * - * @param packageName 包路径 + * @param packageName 包路径,{@code null}表示扫描全部 * @param superClass 父类或接口(不包括) * @return 类集合 */ @@ -137,7 +137,7 @@ public class ClassScanner implements Serializable { * 扫描指定包路径下所有指定类或接口的子类或实现类,不包括指定父类本身
* 如果classpath下已经有类,不再扫描其他加载的jar或者类 * - * @param packageName 包路径 + * @param packageName 包路径,{@code null}表示扫描全部 * @param superClass 父类或接口(不包括) * @return 类集合 */ diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/RowGroup.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/RowGroup.java new file mode 100644 index 000000000..c02e27cc7 --- /dev/null +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/RowGroup.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.poi.excel; + +import org.apache.poi.ss.usermodel.CellStyle; +import org.dromara.hutool.core.collection.CollUtil; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; + +/** + * 分组行
+ * 用于标识和写出复杂表头。 + * 分组概念灵感来自于EasyPOI的设计理念,见:https://blog.csdn.net/qq_45752401/article/details/121250993 + * + * @author Looly + * @since 6.0.0 + */ +public class RowGroup implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 创建分组 + * + * @param name 分组名称 + * @return RowGroup + */ + public static RowGroup of(final String name) { + return new RowGroup(name); + } + + private String name; + private CellStyle style; + private List children; + + /** + * 构造 + * + * @param name 分组名称 + */ + public RowGroup(final String name) { + this.name = name; + } + + /** + * 获取分组名称 + * + * @return 分组名称 + */ + public String getName() { + return name; + } + + /** + * 设置分组名称 + * + * @param name 分组名称 + * @return this + */ + public RowGroup setName(final String name) { + this.name = name; + return this; + } + + /** + * 获取样式 + * + * @return 样式 + */ + public CellStyle getStyle() { + return style; + } + + /** + * 设置样式 + * + * @param style 样式 + * @return this + */ + public RowGroup setStyle(final CellStyle style) { + this.style = style; + return this; + } + + /** + * 获取子分组 + * + * @return 子分组 + */ + public List getChildren() { + return children; + } + + /** + * 设置子分组 + * + * @param children 子分组 + * @return this + */ + public RowGroup setChildren(final List children) { + this.children = children; + return this; + } + + /** + * 添加指定名臣的子分组,最终分组 + * + * @param name 子分组的名称 + * @return this + */ + public RowGroup addChild(final String name) { + return addChild(of(name)); + } + + /** + * 添加子分组 + * + * @param child 子分组 + * @return this + */ + public RowGroup addChild(final RowGroup child) { + if (null == this.children) { + // 无随机获取节点,节省空间 + this.children = new LinkedList<>(); + } + this.children.add(child); + return this; + } + + /** + * 分组占用的最大列数,取决于子分组占用列数 + * + * @return 列数 + */ + public int maxColumnCount() { + if (CollUtil.isEmpty(this.children)) { + // 无子分组,1列 + return 1; + } + return children.stream().mapToInt(RowGroup::maxColumnCount).sum(); + } + + /** + * 获取最大行数,取决于子分组行数
+ * 结果为:标题行占用行数 + 子分组占用行数 + * + * @return 最大行数 + */ + public int maxRowCount() { + int maxRowCount = childrenMaxRowCount(); + if (null != this.name) { + maxRowCount++; + } + + if (0 == maxRowCount) { + throw new IllegalArgumentException("Empty RowGroup!, please set the name or add children."); + } + + return maxRowCount; + } + + /** + * 获取子分组最大占用行数 + * @return 子分组最大占用行数 + */ + public int childrenMaxRowCount() { + int maxRowCount = 0; + if (null != this.children) { + maxRowCount = this.children.stream().mapToInt(RowGroup::maxRowCount).max().orElse(0); + } + return maxRowCount; + } +} diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java index 173c34492..54a15d6f4 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java @@ -21,6 +21,7 @@ import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellRangeAddressList; import org.apache.poi.ss.util.CellReference; import org.dromara.hutool.core.bean.BeanUtil; +import org.dromara.hutool.core.collection.CollUtil; import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.io.file.FileUtil; @@ -29,7 +30,6 @@ import org.dromara.hutool.poi.POIException; import org.dromara.hutool.poi.excel.*; import org.dromara.hutool.poi.excel.cell.CellRangeUtil; import org.dromara.hutool.poi.excel.cell.CellUtil; -import org.dromara.hutool.poi.excel.cell.editors.CellEditor; import org.dromara.hutool.poi.excel.style.*; import java.awt.Color; @@ -37,7 +37,7 @@ import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.Comparator; -import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -644,7 +644,9 @@ public class ExcelWriter extends ExcelBase { public ExcelWriter merge(final CellRangeAddress cellRangeAddress, final Object content, final CellStyle cellStyle) { checkClosed(); - CellUtil.mergingCells(this.getSheet(), cellRangeAddress, cellStyle); + if(cellRangeAddress.getNumberOfCells() > 1){ + CellUtil.mergingCells(this.getSheet(), cellRangeAddress, cellStyle); + } // 设置内容 if (null != content) { @@ -744,6 +746,63 @@ public class ExcelWriter extends ExcelBase { } return this; } + + /** + * 写出分组标题行 + * + * @param rowGroup 分组行 + * @return this + */ + public ExcelWriter writeHeader(final RowGroup rowGroup) { + return writeHeader(0, getCurrentRow(), 1, rowGroup); + } + + /** + * 写出分组标题行 + * + * @param x 开始的列,下标从0开始 + * @param y 开始的行,下标从0开始 + * @param rowCount 当前分组行所占行数,此数值为标题占用行数+子分组占用的最大行数,不确定传1 + * @param rowGroup 分组行 + * @return this + */ + @SuppressWarnings("resource") + public ExcelWriter writeHeader(int x, int y, int rowCount, final RowGroup rowGroup) { + + // 写主标题 + final String name = rowGroup.getName(); + final List children = rowGroup.getChildren(); + if (null != name) { + if(!CollUtil.isEmpty(children)){ + // 有子节点,标题行只占用除子节点占用的行数 + rowCount = Math.max(1, rowCount - rowGroup.childrenMaxRowCount()); + //nameRowCount = 1; + } + + // 如果无子节点,则标题行占用所有行 + final CellRangeAddress cellAddresses = CellRangeUtil.of(y, y + rowCount - 1, x, x + rowGroup.maxColumnCount() - 1); + final CellStyle style = rowGroup.getStyle(); + if(null == style){ + merge(cellAddresses, name, true); + } else{ + merge(cellAddresses, name, style); + } + // 子分组写到下N行 + y += rowCount; + } + + // 写分组 + final int childrenMaxRowCount = rowGroup.childrenMaxRowCount(); + if(childrenMaxRowCount > 0){ + for (final RowGroup child : children) { + // 子分组行高填充为当前分组最大值 + writeHeader(x, y, childrenMaxRowCount, child); + x += child.maxColumnCount(); + } + } + + return this; + } // endregion // region ----- writeImg @@ -859,49 +918,9 @@ public class ExcelWriter extends ExcelBase { * @param rowData 一行的数据 * @return this */ - public ExcelWriter writeHeadRow(final Iterable rowData) { + public ExcelWriter writeHeaderRow(final Iterable rowData) { checkClosed(); - getSheetDataWriter().writeHeadRow(rowData); - return this; - } - - /** - * 写出复杂标题的第二行标题数据
- * 本方法只是将数据写入Workbook中的Sheet,并不写出到文件
- * 写出的起始行为当前行号,可使用{@link #getCurrentRow()}方法调用,根据写出的的行数,当前行号自动+1 - * - *

- * 此方法的逻辑是:将一行数据写出到当前行,遇到已存在的单元格跳过,不存在的创建并赋值。 - *

- * - * @param rowData 一行的数据 - * @return this - */ - @SuppressWarnings("resource") - public ExcelWriter writeSecHeadRow(final Iterable rowData) { - checkClosed(); - final Row row = getOrCreateRow(getCurrentRow()); - passCurrentRow(); - - final Iterator iterator = rowData.iterator(); - //如果获取的row存在单元格,则执行复杂表头逻辑,否则直接调用writeHeadRow(Iterable rowData) - if (row.getLastCellNum() != 0) { - final CellEditor cellEditor = this.config.getCellEditor(); - for (int i = 0; ; i++) { - Cell cell = row.getCell(i); - if (cell != null) { - continue; - } - if (iterator.hasNext()) { - cell = row.createCell(i); - CellUtil.setCellValue(cell, iterator.next(), this.styleSet, true, cellEditor); - } else { - break; - } - } - } else { - writeHeadRow(rowData); - } + getSheetDataWriter().writeHeaderRow(rowData); return this; } diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/SheetDataWriter.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/SheetDataWriter.java index 7be7792dc..0f90353e9 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/SheetDataWriter.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/SheetDataWriter.java @@ -149,7 +149,7 @@ public class SheetDataWriter { final Table aliasTable = this.config.aliasTable(rowMap); if (isWriteKeyAsHead) { // 写出标题行,并记录标题别名和列号的关系 - writeHeadRow(aliasTable.columnKeys()); + writeHeaderRow(aliasTable.columnKeys()); // 记录原数据key和别名对应列号 int i = 0; for (final Object key : aliasTable.rowKeySet()) { @@ -183,7 +183,7 @@ public class SheetDataWriter { * @param rowData 一行的数据 * @return this */ - public SheetDataWriter writeHeadRow(final Iterable rowData) { + public SheetDataWriter writeHeaderRow(final Iterable rowData) { this.headLocationCache = new SafeConcurrentHashMap<>(); final int rowNum = this.currentRow.getAndIncrement(); diff --git a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java index b54c2762d..2519f00db 100644 --- a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java +++ b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java @@ -662,52 +662,6 @@ public class ExcelWriteTest { writer.close(); } - @Test - @Disabled - public void writeSecHeadRowTest() { - final List row1 = ListUtil.of(1, "aa", "bb", "cc", "dd", "ee"); - final List row2 = ListUtil.of(2, "aa1", "bb1", "cc1", "dd1", "ee1"); - final List row3 = ListUtil.of(3, "aa2", "bb2", "cc2", "dd2", "ee2"); - final List row4 = ListUtil.of(4, "aa3", "bb3", "cc3", "dd3", "ee3"); - final List row5 = ListUtil.of(5, "aa4", "bb4", "cc4", "dd4", "ee4"); - - final List> rows = ListUtil.of(row1, row2, row3, row4, row5); - - // 通过工具类创建writer - final ExcelWriter writer = ExcelUtil.getWriter("d:/test/writeSecHeadRowTest.xlsx"); - - final CellStyle cellStyle = writer.getWorkbook().createCellStyle(); - cellStyle.setWrapText(false); - cellStyle.setAlignment(HorizontalAlignment.CENTER); - cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); - //设置标题内容字体 - final Font font = writer.createFont(); - font.setBold(true); - font.setFontHeightInPoints((short) 15); - font.setFontName("Arial"); - //设置边框样式 - StyleUtil.setBorder(cellStyle, BorderStyle.THICK, IndexedColors.RED); - cellStyle.setFont(font); - - // 合并单元格后的标题行,使用设置好的样式 - writer.merge(new CellRangeAddress(0, 1, 0, row1.size() - 1), "标题XXXXXXXX", cellStyle); - Console.log(writer.getCurrentRow()); - - //设置复杂表头 - writer.merge(new CellRangeAddress(2, 3, 0, 0), "序号", true); - writer.merge(new CellRangeAddress(2, 2, 1, 2), "AABB", true); - writer.merge(new CellRangeAddress(2, 3, 3, 3), "CCCC", true); - writer.merge(new CellRangeAddress(2, 2, 4, 5), "DDEE", true); - writer.setCurrentRow(3); - - final List sechead = ListUtil.of("AA", "BB", "DD", "EE"); - writer.writeSecHeadRow(sechead); - // 一次性写出内容,使用默认样式 - writer.write(rows); - // 关闭writer,释放内存 - writer.close(); - } - /** * issue#1659@Github * 测试使用BigWriter写出,ExcelWriter修改失败 @@ -782,7 +736,7 @@ public class ExcelWriteTest { @Disabled public void changeHeaderStyleTest() { final ExcelWriter writer = ExcelUtil.getWriter("d:/test/headerStyle.xlsx"); - writer.writeHeadRow(ListUtil.view("姓名", "性别", "年龄")); + writer.writeHeaderRow(ListUtil.view("姓名", "性别", "年龄")); final CellStyle headCellStyle = ((DefaultStyleSet)writer.getStyleSet()).getHeadCellStyle(); headCellStyle.setFillForegroundColor(IndexedColors.YELLOW1.index); headCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); diff --git a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/RowGroupTest.java b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/RowGroupTest.java new file mode 100644 index 000000000..24e6d3b14 --- /dev/null +++ b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/RowGroupTest.java @@ -0,0 +1,148 @@ +package org.dromara.hutool.poi.excel.writer; + +import org.apache.poi.ss.usermodel.*; +import org.dromara.hutool.core.io.file.FileUtil; +import org.dromara.hutool.poi.excel.ExcelUtil; +import org.dromara.hutool.poi.excel.RowGroup; +import org.dromara.hutool.poi.excel.style.StyleUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * https://blog.csdn.net/qq_45752401/article/details/121250993 + */ +public class RowGroupTest { + + @Test + @Disabled + void writeSingleRowGroupTest() { + final RowGroup rowGroup = RowGroup.of("分组表格测试"); + + final ExcelWriter writer = ExcelUtil.getWriter(); + writer.writeHeader(rowGroup); + writer.flush(FileUtil.file("d:/test/poi/singleRowGroup.xlsx"), true); + writer.close(); + } + + @Test + @Disabled + void writeListRowGroupTest() { + final RowGroup rowGroup = RowGroup.of(null) + .addChild("标题1") + .addChild("标题2") + .addChild("标题3") + .addChild("标题4"); + + final ExcelWriter writer = ExcelUtil.getWriter(); + writer.writeHeader(rowGroup); + writer.flush(FileUtil.file("d:/test/poi/listRowGroup.xlsx"), true); + writer.close(); + } + + @Test + @Disabled + void writeOneRowGroupTest() { + final RowGroup rowGroup = RowGroup.of("基本信息") + .addChild(RowGroup.of("名称2")) + .addChild(RowGroup.of("名称3")); + + final ExcelWriter writer = ExcelUtil.getWriter(); + writer.writeHeader(rowGroup); + writer.flush(FileUtil.file("d:/test/poi/oneRowGroup.xlsx"), true); + writer.close(); + } + + @Test + @Disabled + void writeOneRowGroupWithStyleTest() { + final ExcelWriter writer = ExcelUtil.getWriter(); + final CellStyle cellStyle = writer.getWorkbook().createCellStyle(); + cellStyle.setWrapText(false); + cellStyle.setAlignment(HorizontalAlignment.CENTER); + cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); + //设置标题内容字体 + final Font font = writer.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 15); + font.setFontName("Arial"); + //设置边框样式 + StyleUtil.setBorder(cellStyle, BorderStyle.THICK, IndexedColors.RED); + cellStyle.setFont(font); + + final RowGroup rowGroup = RowGroup.of("基本信息") + .setStyle(cellStyle) + .addChild(RowGroup.of("名称2")) + .addChild(RowGroup.of("名称3")); + + writer.writeHeader(rowGroup); + writer.flush(FileUtil.file("d:/test/poi/oneRowGroupWithStyle.xlsx"), true); + writer.close(); + } + + @Test + @Disabled + void writeRowGroupTest() { + final RowGroup rowGroup = RowGroup.of("分组表格测试") + .addChild(RowGroup.of("序号")) + .addChild( + RowGroup.of("基本信息") + .addChild(RowGroup.of("名称1")) + .addChild(RowGroup.of("名称15") + .addChild(RowGroup.of("名称2")) + .addChild(RowGroup.of("名称3")) + ) + .addChild(RowGroup.of("信息16") + .addChild(RowGroup.of("名称4")) + .addChild(RowGroup.of("名称5")) + .addChild(RowGroup.of("名称6")) + ) + .addChild(RowGroup.of("信息7")) + ) + .addChild(RowGroup.of("特殊信息") + .addChild(RowGroup.of("名称9")) + .addChild(RowGroup.of("名称17") + .addChild(RowGroup.of("名称10")) + .addChild(RowGroup.of("名称11")) + ) + .addChild(RowGroup.of("名称18") + .addChild(RowGroup.of("名称12")) + .addChild(RowGroup.of("名称13")) + ) + ) + .addChild(RowGroup.of("名称14")); + + final ExcelWriter writer = ExcelUtil.getWriter(); + writer.writeHeader(rowGroup); + writer.flush(FileUtil.file("d:/test/poi/rowGroup.xlsx"), true); + writer.close(); + } + + @Test + @Disabled + void writeRowGroupTest2() { + final RowGroup rowGroup = RowGroup.of("分组表格测试") + .addChild(RowGroup.of("序号")) + .addChild( + RowGroup.of("基本信息") + .addChild(RowGroup.of("名称1")) + .addChild(RowGroup.of("名称15") + .addChild(RowGroup.of("名称2")) + .addChild(RowGroup.of("名称3") + .addChild(RowGroup.of("名称3-1")) + .addChild(RowGroup.of("名称3-2")) + ) + ) + .addChild(RowGroup.of("信息16") + .addChild(RowGroup.of("名称4")) + .addChild(RowGroup.of("名称5")) + .addChild(RowGroup.of("名称6")) + ) + .addChild(RowGroup.of("信息7")) + ); + + final ExcelWriter writer = ExcelUtil.getWriter(); + writer.writeHeader(rowGroup); + writer.flush(FileUtil.file("d:/test/poi/rowGroup2.xlsx"), true); + writer.close(); + } +}