add writeHeader

This commit is contained in:
Looly 2024-08-29 20:24:30 +08:00
parent 54c858b8f1
commit b8991dbb76
6 changed files with 407 additions and 98 deletions

View File

@ -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 {
* 扫描指定包路径下所有包含指定注解的类<br>
* 如果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 {
* 扫描指定包路径下所有指定类或接口的子类或实现类不包括指定父类本身<br>
* 如果classpath下已经有类不再扫描其他加载的jar或者类
*
* @param packageName 包路径
* @param packageName 包路径{@code null}表示扫描全部
* @param superClass 父类或接口不包括
* @return 类集合
*/

View File

@ -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;
/**
* 分组行<br>
* 用于标识和写出复杂表头
* 分组概念灵感来自于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<RowGroup> 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<RowGroup> getChildren() {
return children;
}
/**
* 设置子分组
*
* @param children 子分组
* @return this
*/
public RowGroup setChildren(final List<RowGroup> 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();
}
/**
* 获取最大行数取决于子分组行数<br>
* 结果为标题行占用行数 + 子分组占用行数
*
* @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;
}
}

View File

@ -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<ExcelWriter, ExcelWriteConfig> {
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<ExcelWriter, ExcelWriteConfig> {
}
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<RowGroup> 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<ExcelWriter, ExcelWriteConfig> {
* @param rowData 一行的数据
* @return this
*/
public ExcelWriter writeHeadRow(final Iterable<?> rowData) {
public ExcelWriter writeHeaderRow(final Iterable<?> rowData) {
checkClosed();
getSheetDataWriter().writeHeadRow(rowData);
return this;
}
/**
* 写出复杂标题的第二行标题数据<br>
* 本方法只是将数据写入Workbook中的Sheet并不写出到文件<br>
* 写出的起始行为当前行号可使用{@link #getCurrentRow()}方法调用根据写出的的行数当前行号自动+1
*
* <p>
* 此方法的逻辑是将一行数据写出到当前行遇到已存在的单元格跳过不存在的创建并赋值
* </p>
*
* @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;
}

View File

@ -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();

View File

@ -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<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<String> 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);

View File

@ -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();
}
}