各工点提交的电动风阀及防火阀需要进行分类统计汇总。编写了一个通用工具。
其中导出列索引为需要导出的列。
排序列索引为需要进行排序的列,其权重为最前的权重最高。
汇总列为需要累加的列。

java
package ghs.easyexcel.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import ghs.easyexcel.config.ExcelSummaryConfig;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFFont;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFont;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.*;
/**
* Excel通用汇总工具类(核心逻辑)
* 功能:分类小结、排序列全局统计、手动表头、数据排序
*/
public class GenericExcelSummaryUtil {
private static final Logger log = LoggerFactory.getLogger(GenericExcelSummaryUtil.class);
// 原始数据(行号→列索引→值)
private final Map<Integer, Map<Integer, Object>> rawDataMap = new LinkedHashMap<>();
// 分类数据(分类值→数据行列表)
private final Map<Object, List<Map<Integer, Object>>> categoryDataMap = new LinkedHashMap<>();
// 全局总计(汇总列索引→值)
private final Map<Integer, Double> globalSummaryMap = new HashMap<>();
// 源文件第二行表头(列索引→表头名)
private Map<Integer, String> sourceHeaderMap = new HashMap<>();
// 排序列全局统计(排序列值→汇总列索引→值)
private final Map<Object, Map<Integer, Double>> sortColumnGlobalSummaryMap = new HashMap<>();
// 配置类
private final ExcelSummaryConfig config;
// 缓存:导出列List(避免重复转换)
private List<Integer> exportColumnList;
// 缓存:汇总列Set(快速判断)
private Set<Integer> summaryColumnSet;
// 构造方法:初始化配置和缓存
public GenericExcelSummaryUtil(ExcelSummaryConfig config) {
this.config = config;
this.exportColumnList = config.getExportColumnIndexList();
this.summaryColumnSet = new HashSet<>(config.getSummaryColumnIndexList());
// 初始化全局总计为0
for (Integer colIdx : summaryColumnSet) {
globalSummaryMap.put(colIdx, 0.0);
}
log.info("📌 工具类初始化完成!导出列:{},汇总列:{}", exportColumnList, summaryColumnSet);
}
/**
* 主流程:执行汇总(验证→读数据→分组→生成数据→写Excel)
*/
public void executeSummary() throws Exception {
validateConfigAndFile();
readRawExcelData();
groupByCategoryAndCalcStat();
List<Map<Integer, Object>> summaryData = generateSummaryData();
writeSummaryExcel(summaryData);
log.info("🎉 汇总完成!源文件:{},目标文件:{}", config.getSourceFilePath(), config.getTargetFilePath());
}
// ------------------------------ 步骤1:验证配置和文件 ------------------------------
private void validateConfigAndFile() {
// 验证文件
File sourceFile = new File(config.getSourceFilePath());
if (!sourceFile.exists()) {
throw new RuntimeException("源文件不存在:" + config.getSourceFilePath());
}
if (!sourceFile.getName().endsWith(".xlsx") && !sourceFile.getName().endsWith(".xls")) {
throw new RuntimeException("源文件格式错误(仅支持.xlsx/.xls):" + config.getSourceFilePath());
}
// 验证配置
if (exportColumnList.isEmpty()) {
throw new RuntimeException("导出列索引不能为空!");
}
if (summaryColumnSet.isEmpty()) {
throw new RuntimeException("汇总列索引不能为空!");
}
if (config.getSortColumnIndex() < 0) {
throw new RuntimeException("排序列索引不能为负数!");
}
// 检查所有排序列索引
for (Integer sc : config.getSortColumnIndexList()) {
if (sc < 0) {
throw new RuntimeException("排序列索引不能为负数:" + sc);
}
}
log.info("✅ 配置和文件验证通过");
}
// ------------------------------ 步骤2:读取源Excel数据(含表头) ------------------------------
private void readRawExcelData() {
EasyExcel.read(config.getSourceFilePath(), new AnalysisEventListener<Map<Integer, Object>>() {
private int rowNum = 0; // 源文件原始行号(从1开始)
@Override
public void invoke(Map<Integer, Object> rowData, AnalysisContext context) {
rowNum++;
// 捕获源文件第二行作为表头(清理特殊字符)
if (rowNum == 2) {
sourceHeaderMap = new HashMap<>();
for (Map.Entry<Integer, Object> entry : rowData.entrySet()) {
int colIdx = entry.getKey();
String header = entry.getValue() == null ? "" : entry.getValue().toString().trim()
.replaceAll("[\r\n\t\f]", "") // 清理换行/制表符/换页符
.replaceAll("\\s+", " "); // 多个空格转单个
sourceHeaderMap.put(colIdx, header);
}
log.info("📝 捕获源文件第二行表头:{}", sourceHeaderMap);
return;
}
// 跳过配置的行数(如前2行)
if (rowNum <= config.getSkipRows()) {
log.debug("🔄 跳过第{}行(配置跳过前{}行)", rowNum, config.getSkipRows());
return;
}
// 过滤导出列数据
Map<Integer, Object> filteredRow = new LinkedHashMap<>();
for (Integer colIdx : exportColumnList) {
Object value = rowData.get(colIdx);
filteredRow.put(colIdx, value == null ? "" : value.toString().trim());
}
// 跳过空行
boolean isEmptyRow = filteredRow.values().stream()
.map(Object::toString)
.allMatch(StringUtils::isEmpty);
if (isEmptyRow) {
log.debug("🔄 跳过第{}行(空行)", rowNum);
return;
}
rawDataMap.put(rowNum, filteredRow);
log.debug("📥 读取第{}行数据:{}", rowNum, filteredRow);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (sourceHeaderMap.isEmpty()) {
log.warn("⚠️ 未捕获到源文件第二行表头,将使用默认列名(列_索引)");
}
log.info("📊 源Excel读取完成!有效数据行数:{}", rawDataMap.size());
}
}).headRowNumber(0).sheet().doRead(); // headRowNumber(0):按索引读所有行
}
// ------------------------------ 步骤3:按所有非汇总导出列联合分组 + 计算统计值 ------------------------------
private void groupByCategoryAndCalcStat() {
int sortColIdx = config.getSortColumnIndex();
Map<Object, Map<Integer, Double>> sortStatMap = new HashMap<>();
// 确定分组列:所有非汇总的导出列
List<Integer> groupKeyColumns = new ArrayList<>();
for (Integer colIdx : exportColumnList) {
if (!summaryColumnSet.contains(colIdx)) {
groupKeyColumns.add(colIdx);
}
}
log.info("📌 分组列(非汇总导出列):{}", groupKeyColumns);
// 遍历原始数据,分组并累加统计
for (Map<Integer, Object> rowData : rawDataMap.values()) {
// 1. 构建分组键(所有非汇总导出列的值拼接)
StringBuilder keyBuilder = new StringBuilder();
for (int i = 0; i < groupKeyColumns.size(); i++) {
Object val = rowData.get(groupKeyColumns.get(i));
String strVal = val == null ? "" : val.toString().trim();
if (i > 0) keyBuilder.append("|||");
keyBuilder.append(strVal);
}
String groupKey = keyBuilder.toString();
if (groupKey.replaceAll("\\|\\|\\|", "").trim().isEmpty()) {
log.warn("⚠️ 分组键全为空,跳过行:{}", rowData);
continue;
}
// 2. 按联合分组键分组
categoryDataMap.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(rowData);
// 3. 处理排序列(如耐温要求)
final String sortVal = rowData.get(sortColIdx) == null ? "" : rowData.get(sortColIdx).toString().trim();
final String sortValFinal = StringUtils.isEmpty(sortVal) ? "未分类" : sortVal;
// 4. 累加全局总计和排序列统计
for (Integer sumColIdx : summaryColumnSet) {
double value = parseDouble(rowData.get(sumColIdx), 0.0);
// 全局总计
globalSummaryMap.put(sumColIdx, globalSummaryMap.get(sumColIdx) + value);
// 排序列统计
sortStatMap.computeIfAbsent(sortValFinal, k -> new HashMap<>());
Map<Integer, Double> colStat = sortStatMap.get(sortValFinal);
colStat.put(sumColIdx, colStat.getOrDefault(sumColIdx, 0.0) + value);
}
}
// 保存排序列统计结果
sortColumnGlobalSummaryMap.clear();
sortColumnGlobalSummaryMap.putAll(sortStatMap);
// 分类内按排序列排序(支持多列)
List<Integer> sortColIdxList = config.getSortColumnIndexList();
categoryDataMap.forEach((catVal, rowList) -> rowList.sort((r1, r2) -> compareBySortColumns(r1, r2, sortColIdxList)));
log.info("📊 分组和统计完成!分组数:{},排序列统计类型数:{}",
categoryDataMap.size(), sortColumnGlobalSummaryMap.size());
}
// ------------------------------ 步骤4:生成汇总数据(每组合并为一行,汇总列相加) ------------------------------
private List<Map<Integer, Object>> generateSummaryData() {
List<Map<Integer, Object>> summaryList = new ArrayList<>();
int serialNum = 0;
// 每个分组合并为一行:非汇总列保持原值,汇总列求和
for (Map.Entry<Object, List<Map<Integer, Object>>> entry : categoryDataMap.entrySet()) {
List<Map<Integer, Object>> rowList = entry.getValue();
serialNum++;
Map<Integer, Object> mergedRow = new LinkedHashMap<>();
mergedRow.put(-1, serialNum); // 序号
for (Integer colIdx : exportColumnList) {
if (summaryColumnSet.contains(colIdx)) {
// 汇总列:求和
double total = rowList.stream().mapToDouble(r -> parseDouble(r.get(colIdx), 0.0)).sum();
mergedRow.put(colIdx, config.formatNumber(total));
} else {
// 非汇总列:取第一行的值(同组值相同)
mergedRow.put(colIdx, rowList.get(0).get(colIdx));
}
}
mergedRow.put(-2, "-"); // 备注列
summaryList.add(mergedRow);
}
// 按排序列索引多列排序(优先级按输入顺序)
List<Integer> sortColIdxList = config.getSortColumnIndexList();
summaryList.sort((r1, r2) -> compareBySortColumns(r1, r2, sortColIdxList));
// 排序后重新编序号
for (int i = 0; i < summaryList.size(); i++) {
summaryList.get(i).put(-1, i + 1);
}
// 最后一行:全局小结,所有汇总列的数值加一起
Map<Integer, Object> totalRow = new LinkedHashMap<>();
totalRow.put(-1, "");
for (Integer colIdx : exportColumnList) {
if (summaryColumnSet.contains(colIdx)) {
double grandTotal = summaryList.stream()
.mapToDouble(r -> parseDouble(r.get(colIdx), 0.0))
.sum();
totalRow.put(colIdx, config.formatNumber(grandTotal));
} else {
totalRow.put(colIdx, "");
}
}
totalRow.put(-2, config.getGlobalTotalRemark());
summaryList.add(totalRow);
log.info("📋 汇总数据生成完成!合并后行数:{}(共{}个分组,已按排序列{}排序,含全局小结行)",
summaryList.size(), categoryDataMap.size(), sortColIdxList);
return summaryList;
}
// ------------------------------ 步骤5:写入汇总Excel(手动表头) ------------------------------
// 步骤5:写入汇总Excel(手动表头) - 修复后
// 步骤5:写入汇总Excel(手动表头) - 最终修复版
// 步骤5:写入汇总Excel(兜底方案:POI手动写入表头)
private void writeSummaryExcel(List<Map<Integer, Object>> summaryData) {
// 1. 准备表头内容 (EasyExcel 格式:List<List<String>>)
List<List<String>> head = new ArrayList<>();
// 序号列表头
List<String> head0 = new ArrayList<>();
head0.add(config.getSerialNumberColumnName());
head.add(head0);
// 导出列对应的表头
for (Integer colIdx : exportColumnList) {
List<String> h = new ArrayList<>();
h.add(sourceHeaderMap.getOrDefault(colIdx, "列_" + colIdx));
head.add(h);
}
// 备注列表头
List<String> headLast = new ArrayList<>();
headLast.add("备注");
head.add(headLast);
// 2. 准备目标文件
File targetFile = new File(config.getTargetFilePath());
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
// 3. 使用 EasyExcel 统一写入表头和数据
EasyExcel.write(targetFile)
.head(head) // 设置表头结构
.registerWriteHandler(getContentCellStyleStrategy()) // 设置单元格样式
.registerWriteHandler(new CellWriteHandler() {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
Cell cell = context.getCell();
Row row = context.getRow();
// 1. 处理中西文双字体逻辑
CellStyle cellStyle = cell.getCellStyle();
Font font = workbook.getFontAt(cellStyle.getFontIndexAsInt());
if (font instanceof XSSFFont) {
XSSFFont xssfFont = (XSSFFont) font;
CTFont ctFont = xssfFont.getCTFont();
if (ctFont.sizeOfNameArray() == 0) {
ctFont.addNewName().setVal("Times New Roman");
} else {
ctFont.getNameArray(0).setVal("Times New Roman");
}
if (ctFont.sizeOfCharsetArray() == 0) {
ctFont.addNewCharset().setVal(134);
} else {
ctFont.getCharsetArray(0).setVal(134);
}
if (ctFont.sizeOfFamilyArray() == 0) {
ctFont.addNewFamily().setVal(2);
} else {
ctFont.getFamilyArray(0).setVal(2);
}
}
// 2. 特殊逻辑:如果是空行(用于分隔的空行),移除边框
if (!context.getHead()) {
Cell snCell = row.getCell(0);
int remarkColIdx = exportColumnList.size() + 1;
Cell remarkCell = row.getCell(remarkColIdx);
boolean isSeparatorRow = isCellEmpty(snCell) && isCellEmpty(remarkCell);
if (isSeparatorRow) {
// 创建一个无边框的样式(建议在实际项目中缓存此样式以提高性能)
CellStyle noBorderStyle = workbook.createCellStyle();
noBorderStyle.cloneStyleFrom(cellStyle);
noBorderStyle.setBorderLeft(BorderStyle.NONE);
noBorderStyle.setBorderRight(BorderStyle.NONE);
noBorderStyle.setBorderTop(BorderStyle.NONE);
noBorderStyle.setBorderBottom(BorderStyle.NONE);
cell.setCellStyle(noBorderStyle);
}
}
}
private boolean isCellEmpty(Cell cell) {
if (cell == null) return true;
if (cell.getCellType() == CellType.BLANK) return true;
if (cell.getCellType() == CellType.STRING) return cell.getStringCellValue().trim().isEmpty();
return false;
}
})
.registerWriteHandler(new RowWriteHandler() {
@Override
public void afterRowDispose(RowWriteHandlerContext context) {
// 仅处理数据行(非表头)
if (!context.getHead()) {
Row row = context.getRow();
// 获取备注列(最后一列)的值。索引:1(序号) + exportColumnList.size()
int remarkColIdx = exportColumnList.size() + 1;
Cell remarkCell = row.getCell(remarkColIdx);
if (remarkCell != null && remarkCell.getCellType() == CellType.STRING
&& config.getCategorySummaryRemark().equals(remarkCell.getStringCellValue())) {
// 找到最后一个非汇总列在导出列表中的位置
int lastNonSummaryPosInExport = -1;
for (int i = exportColumnList.size() - 1; i >= 0; i--) {
if (!summaryColumnSet.contains(exportColumnList.get(i))) {
lastNonSummaryPosInExport = i;
break;
}
}
if (lastNonSummaryPosInExport >= 0) {
// 合并范围:从第一个导出列到最后一个非汇总列
// Excel列坐标 = 导出列表位置 + 1(+1是因为第0列是序号)
int startCol = 1; // 第一个导出列
int endCol = lastNonSummaryPosInExport + 1; // 最后一个非汇总列
if (endCol > startCol) {
Sheet sheet = context.getWriteSheetHolder().getSheet();
// 执行合并:CellRangeAddress(行号, 行号, 起始列, 结束列)
sheet.addMergedRegion(new CellRangeAddress(row.getRowNum(), row.getRowNum(), startCol, endCol));
// 合并后将最后一个单元格居中显示(小结文字在最后一个单元格)
Cell lastCell = row.getCell(endCol);
if (lastCell != null) {
CellStyle style = sheet.getWorkbook().createCellStyle();
style.cloneStyleFrom(lastCell.getCellStyle());
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
lastCell.setCellStyle(style);
}
}
}
}
}
}
})
.sheet("Excel汇总表")
.doWrite(() -> {
List<List<Object>> excelData = new ArrayList<>();
for (Map<Integer, Object> row : summaryData) {
List<Object> excelRow = new ArrayList<>();
// 序号
excelRow.add(row.get(-1));
// 各导出列数据
for (Integer colIdx : exportColumnList) {
excelRow.add(row.get(colIdx));
}
// 备注
excelRow.add(row.get(-2));
excelData.add(excelRow);
}
return excelData;
});
log.info("✅ Excel写入完成!目标文件:{}", targetFile.getAbsolutePath());
}
// ------------------------------ 辅助方法 ------------------------------
/**
* 多列排序比较:按排序列优先级依次比较
* 先比较第一个排序列,若相同则比较第二个,以此类推
*/
private int compareBySortColumns(Map<Integer, Object> r1, Map<Integer, Object> r2, List<Integer> sortColIdxList) {
for (Integer sortColIdx : sortColIdxList) {
String s1 = config.getSortValue(r1.get(sortColIdx));
String s2 = config.getSortValue(r2.get(sortColIdx));
int cmp = s1.compareTo(s2);
if (cmp != 0) {
return cmp;
}
}
return 0;
}
/**
* 安全转换double(处理空值/非数字)
*/
private double parseDouble(Object value, double defaultValue) {
if (value == null || StringUtils.isEmpty(value.toString())) {
return defaultValue;
}
try {
return Double.parseDouble(value.toString().replaceAll("[^0-9.-]", ""));
} catch (NumberFormatException e) {
log.warn("⚠️ 值转换为double失败,使用默认值{}:{}", defaultValue, value);
return defaultValue;
}
}
/**
* 创建空行(用于分隔)
*/
private Map<Integer, Object> createEmptyRow() {
Map<Integer, Object> emptyRow = new LinkedHashMap<>();
emptyRow.put(-1, "");
for (Integer colIdx : exportColumnList) {
emptyRow.put(colIdx, "");
}
emptyRow.put(-2, "-");
return emptyRow;
}
/**
* 获取排序列的列名(用于统计标题)
*/
private String getSortColumnName() {
return sourceHeaderMap.getOrDefault(config.getSortColumnIndex(), "排序列");
}
/**
* 获取汇总列的前一列在导出列表中的位置对应的源列索引(用于显示统计文本)
*/
private int getSummaryPrevColIdx() {
// 找到导出列表中第一个汇总列的位置,返回它前一个位置的源列索引
int firstSummaryPos = -1;
for (int i = 0; i < exportColumnList.size(); i++) {
if (summaryColumnSet.contains(exportColumnList.get(i))) {
firstSummaryPos = i;
break;
}
}
if (firstSummaryPos > 0) {
return exportColumnList.get(firstSummaryPos - 1);
}
return config.getSortColumnIndex();
}
/**
* 获取内容单元格样式(居中 + 宋体/Times New Roman 10号)
*/
private HorizontalCellStyleStrategy getContentCellStyleStrategy() {
// 内容样式
WriteCellStyle contentStyle = new WriteCellStyle();
contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
contentStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置内容边框
contentStyle.setBorderLeft(BorderStyle.THIN);
contentStyle.setBorderRight(BorderStyle.THIN);
contentStyle.setBorderTop(BorderStyle.THIN);
contentStyle.setBorderBottom(BorderStyle.THIN);
// 主字体设为 Times New Roman (用于英文/数字)
WriteFont contentFont = new WriteFont();
contentFont.setFontName("Times New Roman");
contentFont.setFontHeightInPoints((short) 10);
contentStyle.setWriteFont(contentFont);
// 表头样式
WriteCellStyle headStyle = new WriteCellStyle();
headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
headStyle.setVerticalAlignment(VerticalAlignment.CENTER);
headStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
// 设置表头边框
headStyle.setBorderLeft(BorderStyle.THIN);
headStyle.setBorderRight(BorderStyle.THIN);
headStyle.setBorderTop(BorderStyle.THIN);
headStyle.setBorderBottom(BorderStyle.THIN);
// 表头主字体设为 Times New Roman
WriteFont headFont = new WriteFont();
headFont.setFontName("Times New Roman");
headFont.setFontHeightInPoints((short) 10);
headFont.setBold(true);
headStyle.setWriteFont(headFont);
return new HorizontalCellStyleStrategy(headStyle, contentStyle);
}
// 在GenericExcelSummaryUtil中定义回调接口
public interface ProgressCallback {
void onProgress(int progress, String msg);
}
}
java
package ghs.easyexcel.swing;
// import ch.qos.logback.classic.LoggerContext; // 暂时注释,不使用 Logback 的 LoggerContext
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.formdev.flatlaf.*;
import com.formdev.flatlaf.themes.FlatMacDarkLaf;
import com.formdev.flatlaf.themes.FlatMacLightLaf;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import ghs.easyexcel.config.ExcelSummaryConfig;
import ghs.easyexcel.util.GenericExcelSummaryUtil;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Excel汇总工具Swing界面(乱码修复版)
* 解决日志窗口乱码问题:统一UTF-8编码 + 适配中文字体 + 字节流转字符流
*/
public class ExcelSummarySwingApp extends JFrame {
// ========== 常量配置 ==========
private static final String LOG_SEP = "----------------------------------------\n";
private static final String DEFAULT_SOURCE_FILE = "data" + File.separator + "电动风阀导入示例.xlsx";
private static final int PROGRESS_MAX = 100;
// 统一使用微软雅黑字体
private static final String CHINESE_FONT = "微软雅黑";
private final org.slf4j.Logger log = LoggerFactory.getLogger(ExcelSummarySwingApp.class);
// 日志队列用于异步更新UI
private final BlockingQueue<String> logQueue = new LinkedBlockingQueue<>();
private final Font UI_FONT = new Font("微软雅黑", Font.PLAIN, 14);
private final ExcelSummarySwingApp instance; // 保存实例引用以供日志回调
// ========== GUI Designer 绑定的组件变量 ==========
private JPanel mainPanel; // 主面板(GUI Designer根组件)
private final Color categoryColumnColor = new Color(255, 200, 200); // 分类列 - 浅红色
private final Color exportColumnsColor = new Color(200, 255, 200); // 导出列 - 浅绿色
private final Color sortColumnColor = new Color(200, 200, 255); // 排序列 - 浅蓝色
private final Color summaryColumnsColor = new Color(255, 255, 200); // 汇总列 - 浅黄色
private final Set<Integer> categoryColumns = new HashSet<>(); // 分类列集合
private final Set<Integer> exportColumns = new HashSet<>(); // 导出列集合
private final Set<Integer> sortColumns = new HashSet<>(); // 排序列集合
private final Set<Integer> summaryColumns = new HashSet<>(); // 汇总列集合
private final boolean logUpdaterRunning = true;
// 设备类型快速选择按钮
private JButton btn_deviceDamper;
private JButton btn_deviceFireDamper;
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off
private JPanel pnlTop;
private JPanel pnlFile;
private JTextField t_sourceFile;
private JPanel pnlFileButtons;
private JButton btn_selectFile;
private JButton btn_previewHeader;
private JPanel pnlHeaderPreview;
private JScrollPane spHeaderPreview;
private JTable jTableHeaderPreview;
private JPanel pnlParams;
private JTextField t_skipRows;
private JTextField t_categoryColumn;
private JTextField t_exportColumns;
private JTextField t_sortColumn;
private JTextField t_summaryColumns;
private JTextField t_decimalPlaces;
private JPanel pnlBottom;
private JPanel pnlButtonProgress;
private JPanel pnlButton;
private JButton btn_execute;
private JButton btn_clear;
private JPanel pnlProgress;
private JProgressBar progressBar;
private JPanel pnlLog;
private JScrollPane spLog;
private JTextArea t_log;
public ExcelSummarySwingApp() {
// 0. 保存实例引用
initComponents();
instance = this;
// 1. 初始化界面(GUI Designer生成的UI)
initUI();
// 2. 初始化表头预览表格
initHeaderPreviewTable();
// 3. 初始化菜单栏
initMenuBar();
// 4. 绑定事件
bindEvents();
// 5. 重定向日志(UTF-8编码)
redirectSystemOutToTextArea();
// 6. 启动日志更新线程
startLogUpdaterThread();
// 7. 配置自定义日志Appender
setupCustomLogAppender();
}
/**
* 程序入口
*/
public static void main(String[] args) {
// 1. 初始化风格(放在最前,确保 UI 组件创建前生效)
initLookAndFeelStatic();
try {
// 2. 强制设置 UTF-8 环境属性
System.setProperty("file.encoding", "UTF-8");
System.setProperty("sun.jnu.encoding", "UTF-8");
System.setProperty("user.language", "zh");
System.setProperty("user.region", "CN");
} catch (SecurityException e) {
// 忽略权限限制
}
SwingUtilities.invokeLater(() -> {
ExcelSummarySwingApp app = new ExcelSummarySwingApp();
app.setVisible(true);
app.logToUI("💻 Excel汇总工具(大字深色增强版)启动完成!\n");
app.logToUI("📢 当前系统编码已强制设为: UTF-8\n");
});
}
/**
* 静态风格初始化:设置 FlatLaf 和全局大字体
*/
private static void initLookAndFeelStatic() {
try {
// 开启抗锯齿和窗口装饰
System.setProperty("flatlaf.useWindowDecorations", "true");
// 强制 UI 缩放 (1.25倍)
System.setProperty("flatlaf.uiScale", "1.25");
// 设置默认字体 (14号),使用英文名称提高兼容性
Font globalFont = new Font("Microsoft YaHei UI", Font.PLAIN, 14);
UIManager.put("defaultFont", globalFont);
// 应用深色主题
// FlatDarkLaf.setup();
// set look and feel
try {
UIManager.setLookAndFeel(GlazzedTheme.class.getName());
} catch (Exception e) {
// fallback
GlazzedTheme.setup();
GlazzedTheme.updateUI();
throw new RuntimeException(e);
}
// 覆盖所有组件字体(确保 L&F setup 后不被重置)
String[] components = {
"Label.font", "Button.font", "Table.font", "TableHeader.font",
"TextField.font", "TextArea.font", "TitledBorder.font",
"ComboBox.font", "List.font", "CheckBox.font", "RadioButton.font",
"ProgressBar.font", "TabbedPane.font", "Menu.font", "MenuItem.font",
"ToolTip.font", "OptionPane.messageFont", "OptionPane.buttonFont",
"TitledBorder.font", "Panel.font", "ScrollPane.font"
};
for (String key : components) {
UIManager.put(key, globalFont);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 初始化菜单栏,添加风格切换功能
*/
private void initMenuBar() {
JMenuBar menuBar = new JMenuBar();
JMenu viewMenu = new JMenu("视图(V)");
viewMenu.setMnemonic('V');
JMenu themeMenu = new JMenu("界面风格");
// 定义主题选项
addThemeMenuItem(themeMenu, "IntelliJ (亮色)", FlatIntelliJLaf.class.getName());
addThemeMenuItem(themeMenu, "Darcula (深色)", FlatDarculaLaf.class.getName());
addThemeMenuItem(themeMenu, "Flat Light", FlatLightLaf.class.getName());
addThemeMenuItem(themeMenu, "Flat Dark", FlatDarkLaf.class.getName());
addThemeMenuItem(themeMenu, "macOS Light", FlatMacLightLaf.class.getName());
addThemeMenuItem(themeMenu, "macOS Dark", FlatMacDarkLaf.class.getName());
addThemeMenuItem(themeMenu, "GlazzedTheme", GlazzedTheme.class.getName());
viewMenu.add(themeMenu);
menuBar.add(viewMenu);
this.setJMenuBar(menuBar);
}
/**
* 添加主题菜单项
*/
private void addThemeMenuItem(JMenu menu, String name, String lafClassName) {
JMenuItem menuItem = new JMenuItem(name);
menuItem.addActionListener(e -> {
try {
UIManager.setLookAndFeel(lafClassName);
FlatLaf.updateUI(); // 通知所有窗口更新 UI
log.info("🎨 已切换界面风格为:{}", name);
} catch (Exception ex) {
log.error("❌ 切换界面风格失败:{}", ex.getMessage());
}
});
menu.add(menuItem);
}
/**
* 初始化界面(使用GUI Designer生成的mainPanel)
*/
private void initUI() {
// 如果 mainPanel 为 null,说明 GUI Designer 的自动初始化(字节码增强)未生效
// 常见原因:在非 IntelliJ 环境下运行,或者 Maven/Gradle 未配置相关插件
// if (mainPanel == null) {
// log.error("❌ mainPanel 为 null!GUI Designer 自动初始化失败。请检查 IDE 设置或手动初始化组件。");
// // 紧急回退:创建一个基础面板防止崩溃,但 UI 可能会缺失内容
// mainPanel = new JPanel(new BorderLayout());
// t_log = new JTextArea();
// // ... 如果需要手动完全初始化,代码量会非常大。建议检查 IDE 设置。
// }
this.setTitle("东莞2号线三期电动风阀防火阀汇总工具---读入的文件只能有一个表头!!!中间不要插入表头!!!");
this.setSize(1000, 750); // 调大初始窗口大小
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
// 使用GUI Designer生成的mainPanel
this.setContentPane(mainPanel);
// 设置日志区域属性
if (t_log != null) {
t_log.getDocument().putProperty("charset", StandardCharsets.UTF_8.name());
t_log.setBackground(Color.BLACK);
t_log.setForeground(new Color(0, 255, 0)); // 亮绿色
t_log.setCaretColor(Color.GREEN);
// 确保使用支持中文的字体,解决乱码。使用 Microsoft YaHei UI 提高稳定性
Font logFont = new Font("Microsoft YaHei UI", Font.PLAIN, 14);
t_log.setFont(logFont);
t_log.setLineWrap(true);
t_log.setWrapStyleWord(true);
}
}
/**
* 初始化表头预览表格
*/
private void initHeaderPreviewTable() {
// 防御性检查:如果 jTableHeaderPreview 为 null,说明 GUI Designer 初始化未生效
if (jTableHeaderPreview == null) {
log.error("❌ jTableHeaderPreview 为 null!跳过预览表格初始化。请按前述步骤调整 IDE 设置。");
return;
}
// 创建表头预览表格模型
String[] columnNames = {"0A", "1B", "2C", "3D", "4E", "5F", "6G", "7H", "8I", "9J", "10K", "11L", "12M", "13N"};
DefaultTableModel tableModel = new DefaultTableModel(columnNames, 1);
jTableHeaderPreview.setModel(tableModel);
// 设置自定义渲染器
ColoredCellRenderer renderer = new ColoredCellRenderer();
for (int i = 0; i < jTableHeaderPreview.getColumnCount(); i++) {
jTableHeaderPreview.getColumnModel().getColumn(i).setCellRenderer(renderer);
}
}
/**
* 绑定按钮事件(GUI Designer组件)
*/
private void bindEvents() {
// 防御性检查:确保按钮组件已由 GUI Designer 初始化
if (btn_previewHeader == null || btn_selectFile == null || btn_execute == null || btn_clear == null) {
log.error("❌ 按钮组件未初始化(null)!无法绑定事件。请检查 GUI Designer 设置并重新构建项目。");
return;
}
// 预览表头按钮事件
btn_previewHeader.addActionListener(e -> previewHeader());
// 选择文件按钮
btn_selectFile.addActionListener(e -> {
JFileChooser fileChooser = new JFileChooser();
// 默认打开程序所在目录
fileChooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
File defaultFile = new File(DEFAULT_SOURCE_FILE).getAbsoluteFile();
if (defaultFile.exists()) {
fileChooser.setSelectedFile(defaultFile);
}
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"Excel文件 (*.xlsx, *.xls)", "xlsx", "xls");
fileChooser.setFileFilter(filter);
if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File selectedFile = fileChooser.getSelectedFile();
t_sourceFile.setText(selectedFile.getAbsolutePath());
log.info("📂 已选择源文件:{}", selectedFile.getAbsolutePath());
}
});
// 执行汇总按钮
btn_execute.addActionListener(e -> {
progressBar.setValue(0);
progressBar.setString("准备执行...");
btn_execute.setEnabled(false);
btn_selectFile.setEnabled(false);
t_log.setText("");
log.info(LOG_SEP + "🚀 开始执行Excel汇总..." + LOG_SEP);
new Thread(() -> {
try {
updateProgress(10, "校验输入参数");
ExcelSummaryConfig config = parseParams();
if (config == null) {
updateProgress(0, "参数校验失败");
return;
}
updateProgress(20, "初始化汇总工具");
GenericExcelSummaryUtil summaryUtil = new GenericExcelSummaryUtil(config);
updateProgress(30, "读取源Excel数据");
Thread.sleep(500);
updateProgress(60, "数据读取完成,开始汇总计算");
summaryUtil.executeSummary();
updateProgress(90, "汇总计算完成,写入Excel");
updateProgress(100, "汇总完成");
SwingUtilities.invokeLater(() -> {
UIManager.put("OptionPane.messageFont", UI_FONT); // 设置弹窗字体
UIManager.put("OptionPane.buttonFont", UI_FONT); // 设置弹窗按钮字体
int result = JOptionPane.showConfirmDialog(this,
"汇总完成!\n输出文件:" + config.getTargetFilePath() + "\n是否打开输出文件?",
"成功", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE);
if (result == JOptionPane.YES_OPTION) {
try {
File outputFile = new File(config.getTargetFilePath());
if (outputFile.exists()) {
Desktop.getDesktop().open(outputFile);
} else {
log.error("❌ 输出文件不存在:{}", config.getTargetFilePath());
}
} catch (Exception ex) {
log.error("❌ 打开输出文件失败:{}", ex.getMessage(), ex);
JOptionPane.showMessageDialog(this, "打开输出文件失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
log.info(LOG_SEP + "🎉 汇总执行完成!" + LOG_SEP);
});
} catch (Exception ex) {
updateProgress(0, "执行失败");
String errorMsg = "汇总失败:" + ex.getMessage();
log.error("❌ " + errorMsg, ex);
SwingUtilities.invokeLater(() -> {
UIManager.put("OptionPane.messageFont", UI_FONT); // 设置弹窗字体
UIManager.put("OptionPane.buttonFont", UI_FONT); // 设置弹窗按钮字体
JOptionPane.showMessageDialog(this, errorMsg, "错误", JOptionPane.ERROR_MESSAGE);
});
} finally {
SwingUtilities.invokeLater(() -> {
btn_execute.setEnabled(true);
btn_selectFile.setEnabled(true);
});
}
}).start();
});
// 清空按钮
btn_clear.addActionListener(e -> {
t_sourceFile.setText(new File(DEFAULT_SOURCE_FILE).getAbsolutePath());
t_skipRows.setText("2");
t_exportColumns.setText("1,5,8,9,10,6,13");
t_summaryColumns.setText("6,13");
t_categoryColumn.setText("1");
t_sortColumn.setText("9,8,5");
t_decimalPlaces.setText("2");
progressBar.setValue(0);
progressBar.setString("未执行");
t_log.setText("");
// 清空后也更新高亮列
updateHighlightedColumns();
log.info("🗑️ 已清空所有参数和日志,恢复默认配置");
});
// 设备类型快速选择按钮
btn_deviceDamper.addActionListener(e -> {
t_exportColumns.setText("1,5,8,9,10,6,13");
t_sortColumn.setText("9,8,5");
t_summaryColumns.setText("6,13");
t_categoryColumn.setText("1");
t_skipRows.setText("2");
t_decimalPlaces.setText("2");
updateHighlightedColumns();
log.info("📌 已选择设备类型:电动风阀,参数已自动填充");
});
btn_deviceFireDamper.addActionListener(e -> {
t_exportColumns.setText("1,2,5,8,6,11");
t_sortColumn.setText("2,5");
t_summaryColumns.setText("6,11");
t_categoryColumn.setText("1");
t_skipRows.setText("2");
t_decimalPlaces.setText("2");
updateHighlightedColumns();
log.info("📌 已选择设备类型:防火阀,参数已自动填充");
});
// 为参数输入框添加监听器,当参数改变时更新高亮列
addParameterChangeListener(t_categoryColumn);
addParameterChangeListener(t_exportColumns);
addParameterChangeListener(t_sortColumn);
addParameterChangeListener(t_summaryColumns);
}
/**
* 为参数输入框添加监听器,当参数改变时更新高亮列
*/
private void addParameterChangeListener(JTextField textField) {
textField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
public void insertUpdate(javax.swing.event.DocumentEvent e) {
updateHighlightedColumns();
}
public void removeUpdate(javax.swing.event.DocumentEvent e) {
updateHighlightedColumns();
}
public void changedUpdate(javax.swing.event.DocumentEvent e) {
updateHighlightedColumns();
}
});
}
/**
* 线程安全更新进度条
*/
private void updateProgress(int progress, String msg) {
SwingUtilities.invokeLater(() -> {
progressBar.setValue(progress);
progressBar.setString(msg + " (" + progress + "%)");
});
log.info("📊 进度{}%:{}", progress, msg);
}
/**
* 解析参数
*/
private ExcelSummaryConfig parseParams() {
try {
String sourcePath = t_sourceFile.getText().trim();
if (sourcePath.isEmpty()) {
JOptionPane.showMessageDialog(this, "请选择源Excel文件!", "错误", JOptionPane.ERROR_MESSAGE);
return null;
}
File sourceFile = new File(sourcePath);
if (!sourceFile.exists()) {
JOptionPane.showMessageDialog(this, "源文件不存在:" + sourcePath, "错误", JOptionPane.ERROR_MESSAGE);
return null;
}
String targetPath = generateTargetFilePath(sourcePath);
int skipRows = Integer.parseInt(t_skipRows.getText().trim());
int categoryCol = Integer.parseInt(t_categoryColumn.getText().trim());
int[] sortCols = parseIntArray(t_sortColumn.getText().trim());
int decimalPlaces = Integer.parseInt(t_decimalPlaces.getText().trim());
int[] exportCols = parseIntArray(t_exportColumns.getText().trim());
int[] summaryCols = parseIntArray(t_summaryColumns.getText().trim());
if (skipRows < 0) throw new RuntimeException("跳过行数不能为负数!");
if (categoryCol < 0) throw new RuntimeException("分类列索引不能为负数!");
for (int sc : sortCols) { if (sc < 0) throw new RuntimeException("排序列索引不能为负数!"); }
if (decimalPlaces < 0 || decimalPlaces > 10) throw new RuntimeException("小数位数需在0-10之间!");
if (exportCols.length == 0) throw new RuntimeException("导出列索引不能为空!");
if (summaryCols.length == 0) throw new RuntimeException("汇总列索引不能为空!");
ExcelSummaryConfig config = new ExcelSummaryConfig();
config.setSourceFilePath(sourcePath);
config.setTargetFilePath(targetPath);
config.setSkipRows(skipRows);
config.setExportColumnIndexes(exportCols);
config.setSummaryColumnIndexes(summaryCols);
config.setCategoryColumnIndex(categoryCol);
config.setSortColumnIndexes(sortCols);
config.setDecimalPlaces(decimalPlaces);
log.info("📋 参数解析完成:");
log.info(" 源文件:{}", sourcePath);
log.info(" 输出文件:{}", targetPath);
log.info(" 跳过行数:{}", skipRows);
log.info(" 导出列:{}", Arrays.toString(exportCols));
log.info(" 汇总列:{}", Arrays.toString(summaryCols));
log.info(" 分类列:{},排序列:{},小数位数:{}", categoryCol, Arrays.toString(sortCols), decimalPlaces);
return config;
} catch (NumberFormatException ex) {
JOptionPane.showMessageDialog(this, "参数格式错误:请输入整数!\n" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
log.error("❌ 参数格式错误", ex);
return null;
} catch (RuntimeException ex) {
JOptionPane.showMessageDialog(this, ex.getMessage(), "参数错误", JOptionPane.ERROR_MESSAGE);
log.error("❌ 参数校验失败", ex);
return null;
}
}
/**
* 生成输出文件路径
*/
private String generateTargetFilePath(String sourcePath) {
int dotIndex = sourcePath.lastIndexOf(".");
return sourcePath.substring(0, dotIndex) + "-out" + sourcePath.substring(dotIndex);
}
/**
* 逗号分隔字符串转int数组
*/
private int[] parseIntArray(String str) {
if (str.isEmpty()) return new int[0];
String[] strArr = str.replaceAll("\\s+", "").split(",");
int[] intArr = new int[strArr.length];
for (int i = 0; i < strArr.length; i++) {
intArr[i] = Integer.parseInt(strArr[i]);
}
return intArr;
}
/**
* 预览表头功能:读取源文件表头并在日志区域显示
*/
private void previewHeader() {
String filePath = t_sourceFile.getText().trim();
if (filePath.isEmpty()) {
JOptionPane.showMessageDialog(this, "请先选择源Excel文件!", "提示", JOptionPane.WARNING_MESSAGE);
return;
}
File file = new File(filePath);
if (!file.exists()) {
JOptionPane.showMessageDialog(this, "源文件不存在:" + filePath, "错误", JOptionPane.ERROR_MESSAGE);
return;
}
try {
// 临时禁用日志输出,避免调试信息显示在UI上
java.util.logging.Logger excelLogger = java.util.logging.Logger.getLogger("com.alibaba.excel");
java.util.logging.Level originalLevel = excelLogger.getLevel();
excelLogger.setLevel(java.util.logging.Level.WARNING);
try {
// 使用AnalysisEventListener来处理读取的数据
List<List<String>> allRows = new ArrayList<>();
EasyExcel.read(file.getAbsolutePath(), new AnalysisEventListener<Map<Integer, Object>>() {
@Override
public void invoke(Map<Integer, Object> data, AnalysisContext context) {
// 将Map转换为List
List<String> row = new ArrayList<>();
if (!data.isEmpty()) {
// 按顺序从0开始添加,直到遇到空的键
int maxIndex = data.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1);
for (int i = 0; i <= maxIndex; i++) {
Object cellValue = data.get(i);
row.add(cellValue != null ? cellValue.toString() : "");
}
}
allRows.add(row);
// 只读取前几行
if (context.readRowHolder() != null && context.readRowHolder().getRowIndex() > 0) { // 只读取第0行(首行)
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 读取完成后的操作
}
}).sheet().doRead();
if (allRows.isEmpty()) {
logToUI("⚠️ 无法读取到表头数据,请检查文件格式\n");
return;
}
// 使用第一行作为表头
List<String> headers = allRows.get(0);
// 更新JTable中的表头信息
updateHeaderPreviewTable(headers);
} finally {
// 恢复日志级别
excelLogger.setLevel(originalLevel);
}
} catch (Exception e) {
log.error("❌ 预览表头失败:{}", e.getMessage(), e);
logToUI("❌ 预览表头失败:" + e.getMessage() + "\n");
}
}
/**
* 以表格格式显示表头信息(按行显示)
*/
private void displayHeadersInTableFormat(List<String> headers) {
if (headers.isEmpty()) return;
// 构建表格(按行显示)
StringBuilder table = new StringBuilder();
table.append("\n📋 文件表头预览:\n\n");
// 按行显示每个列的信息
for (int col = 0; col < headers.size(); col++) {
String cellValue = headers.get(col);
String cellStr = cellValue != null ? cellValue : "";
table.append(String.format("第%d列:%s\n", col, cellStr));
}
table.append("\n");
logToUI(table.toString());
}
/**
* 更新表头预览表格
*/
private void updateHeaderPreviewTable(List<String> headers) {
if (headers == null || headers.isEmpty()) {
logToUI("⚠️ 无法读取到表头数据,请检查文件格式\n");
return;
}
// 创建表格模型并更新数据
DefaultTableModel model = (DefaultTableModel) jTableHeaderPreview.getModel();
// 清空现有数据
for (int col = 0; col < model.getColumnCount(); col++) {
model.setValueAt("", 0, col);
}
// 设置新的表头数据
for (int i = 0; i < Math.min(headers.size(), model.getColumnCount()); i++) {
model.setValueAt(headers.get(i), 0, i);
}
// 更新高亮列
updateHighlightedColumns();
logToUI("✅ 表头预览已更新\n");
}
/**
* 更新高亮列
*/
private void updateHighlightedColumns() {
// 清空所有高亮集合
categoryColumns.clear();
exportColumns.clear();
sortColumns.clear();
summaryColumns.clear();
try {
// 获取分类列索引
int categoryCol = Integer.parseInt(t_categoryColumn.getText().trim());
if (categoryCol >= 0) {
categoryColumns.add(categoryCol);
}
// 获取导出列索引
int[] exportCols = parseIntArray(t_exportColumns.getText().trim());
for (int col : exportCols) {
if (col >= 0) {
exportColumns.add(col);
}
}
// 获取排序列索引(支持多列)
int[] sortColsArr = parseIntArray(t_sortColumn.getText().trim());
for (int sc : sortColsArr) {
if (sc >= 0) {
sortColumns.add(sc);
}
}
// 获取汇总列索引
int[] summaryCols = parseIntArray(t_summaryColumns.getText().trim());
for (int col : summaryCols) {
if (col >= 0) {
summaryColumns.add(col);
}
}
// 重新绘制表格以更新颜色
jTableHeaderPreview.repaint();
} catch (NumberFormatException e) {
log.error("参数格式错误,无法更新高亮列:{}", e.getMessage());
}
}
/**
* 计算字符串的显示宽度(中文字符按2个单位计算,英文字符按1个单位计算)
*/
private int getStringDisplayWidth(String str) {
if (str == null) return 0;
int width = 0;
for (char c : str.toCharArray()) {
if (isChineseCharacter(c)) {
width += 2; // 中文字符宽度为2
} else {
width += 1; // 英文字符宽度为1
}
}
return width;
}
/**
* 判断字符是否为中文字符
*/
private boolean isChineseCharacter(char c) {
return c >= 0x4E00 && c <= 0x9FFF; // 基本中文字符范围
}
/**
* 右对齐填充字符串(考虑中文字符宽度)
*/
private String padStringRight(String str, int totalDisplayWidth) {
if (str == null) str = "";
int currentWidth = getStringDisplayWidth(str);
int padding = totalDisplayWidth - currentWidth;
if (padding <= 0) return str;
StringBuilder sb = new StringBuilder(str);
for (int i = 0; i < padding; i++) {
sb.append(' ');
}
return sb.toString();
}
/**
* 左对齐填充字符串(考虑中文字符宽度)
*/
private String padStringLeft(String str, int totalDisplayWidth) {
if (str == null) str = "";
int currentWidth = getStringDisplayWidth(str);
int padding = totalDisplayWidth - currentWidth;
if (padding <= 0) return str;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < padding; i++) {
sb.append(' ');
}
sb.append(str);
return sb.toString();
}
/**
* 配置自定义日志Appender,将SLF4J日志输出到UI
*/
private void setupCustomLogAppender() {
try {
// 尝试配置 Logback Appender
Object iLoggerFactory = LoggerFactory.getILoggerFactory();
// 使用字符串比较避免编译时依赖
if (iLoggerFactory.getClass().getName().equals("ch.qos.logback.classic.LoggerContext")) {
// 使用反射调用方法
Object rootLogger = iLoggerFactory.getClass().getMethod("getLogger", String.class).invoke(iLoggerFactory, "ROOT");
// 移除原有的控制台 Appender,防止日志重复且确保级别翻译生效
try {
rootLogger.getClass().getMethod("detachAppender", String.class).invoke(rootLogger, "console");
rootLogger.getClass().getMethod("detachAppender", String.class).invoke(rootLogger, "CONSOLE");
} catch (Exception ex) {
// 忽略移除失败
}
// 创建并配置自定义Appender
CustomLogAppender customAppender = new CustomLogAppender();
customAppender.setContext((ch.qos.logback.core.Context) iLoggerFactory);
customAppender.setName("CustomUIAppender");
customAppender.setLogQueue(logQueue);
customAppender.setApp(this);
customAppender.start();
// 添加到根日志记录器
rootLogger.getClass().getMethod("addAppender", Class.forName("ch.qos.logback.core.Appender")).invoke(rootLogger, customAppender);
logToUI("✅ 自定义日志Appender配置完成\n");
} else {
logToUI("⚠️ 当前日志框架不是 Logback,使用 System.out 重定向\n");
}
} catch (Exception e) {
logToUI("⚠️ 配置自定义日志Appender失败:" + e.getMessage() + "\n");
}
}
/**
* 启动日志更新线程,处理来自SLF4J的日志消息
*/
private void startLogUpdaterThread() {
Thread logUpdater = new Thread(() -> {
while (logUpdaterRunning) {
try {
String logMessage = logQueue.take(); // 阻塞等待日志消息
SwingUtilities.invokeLater(() -> {
t_log.append(logMessage);
t_log.setCaretPosition(t_log.getText().length());
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
logUpdater.setDaemon(true); // 设置为守护线程
logUpdater.start();
}
/**
* 自定义日志记录方法,将日志消息添加到队列
*/
private void logToUI(String message) {
try {
logQueue.put(message + "\n");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 核心修复:重定向日志(强制使用 UTF-8 编码,彻底解决导出后的乱码)
*/
private void redirectSystemOutToTextArea() {
try {
// 统一使用 UTF-8 编码进行内部管道传输
// 这样无论系统默认编码是 GBK 还是其他,我们的内部重定向始终保持一致
String encoding = StandardCharsets.UTF_8.name();
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
// 启动读取线程
Thread readerThread = new Thread(() -> {
// 使用 UTF-8 解析管道流
try (BufferedReader reader = new BufferedReader(new InputStreamReader(pis, encoding))) {
String line;
while ((line = reader.readLine()) != null) {
logToUI(line);
}
} catch (IOException e) {
// 流正常关闭
}
}, "日志流读取线程");
readerThread.setDaemon(true);
readerThread.start();
// 强制设置系统输出编码为 UTF-8
PrintStream ps = new PrintStream(pos, true, encoding);
System.setOut(ps);
System.setErr(ps);
log.info("✅ 系统日志重定向成功 (已强制使用 UTF-8 编码)");
} catch (IOException e) {
log.error("❌ 设置系统输出重定向失败", e);
}
}
private void initComponents() {
mainPanel = new JPanel(new BorderLayout(10, 10));
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off
pnlTop = new JPanel();
pnlFile = new JPanel();
t_sourceFile = new JTextField();
pnlFileButtons = new JPanel();
btn_selectFile = new JButton();
btn_previewHeader = new JButton();
pnlHeaderPreview = new JPanel();
spHeaderPreview = new JScrollPane();
jTableHeaderPreview = new JTable();
pnlParams = new JPanel();
var label1 = new JLabel();
t_skipRows = new JTextField();
var label2 = new JLabel();
t_categoryColumn = new JTextField();
var label3 = new JLabel();
t_exportColumns = new JTextField();
var label4 = new JLabel();
t_sortColumn = new JTextField();
var label5 = new JLabel();
t_summaryColumns = new JTextField();
var label6 = new JLabel();
t_decimalPlaces = new JTextField();
var label7 = new JLabel();
var label8 = new JLabel();
var label9 = new JLabel();
var label10 = new JLabel();
pnlBottom = new JPanel();
pnlButtonProgress = new JPanel();
pnlButton = new JPanel();
btn_execute = new JButton();
btn_clear = new JButton();
pnlProgress = new JPanel();
progressBar = new JProgressBar();
pnlLog = new JPanel();
spLog = new JScrollPane();
t_log = new JTextArea();
//======== this ========
setLayout(new BorderLayout(10, 10));
//======== pnlTop ========
{
pnlTop.setLayout(new BorderLayout(10, 10));
//======== pnlFile ========
{
pnlFile.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u6587\u4ef6\u9009\u62e9"));
pnlFile.setLayout(new BorderLayout(5, 5));
//---- t_sourceFile ----
t_sourceFile.setEditable(false);
t_sourceFile.setText(new File(DEFAULT_SOURCE_FILE).getAbsolutePath());
pnlFile.add(t_sourceFile, BorderLayout.CENTER);
//======== pnlFileButtons ========
{
pnlFileButtons.setLayout(new FlowLayout(FlowLayout.LEFT, 5, 0));
//---- btn_selectFile ----
btn_selectFile.setText("\u9009\u62e9\u6e90\u6587\u4ef6");
pnlFileButtons.add(btn_selectFile);
//---- btn_previewHeader ----
btn_previewHeader.setText("\u9884\u89c8\u8868\u5934");
pnlFileButtons.add(btn_previewHeader);
// 设备类型快速选择按钮组
pnlFileButtons.add(new javax.swing.JToolBar.Separator());
btn_deviceDamper = new JButton("电动风阀");
btn_deviceDamper.setForeground(new Color(0, 100, 0));
pnlFileButtons.add(btn_deviceDamper);
btn_deviceFireDamper = new JButton("防火阀");
btn_deviceFireDamper.setForeground(new Color(180, 0, 0));
pnlFileButtons.add(btn_deviceFireDamper);
}
pnlFile.add(pnlFileButtons, BorderLayout.EAST);
}
pnlTop.add(pnlFile, BorderLayout.NORTH);
//======== pnlHeaderPreview ========
{
pnlHeaderPreview.setMaximumSize(new Dimension(-1, 100));
pnlHeaderPreview.setMinimumSize(new Dimension(850, 80));
pnlHeaderPreview.setPreferredSize(new Dimension(850, 90));
pnlHeaderPreview.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u8868\u5934\u9884\u89c8"));
pnlHeaderPreview.setLayout(new BorderLayout());
//======== spHeaderPreview ========
{
spHeaderPreview.setPreferredSize(new Dimension(850, 80));
//---- jTableHeaderPreview ----
jTableHeaderPreview.setEnabled(false);
jTableHeaderPreview.setRowHeight(30);
spHeaderPreview.setViewportView(jTableHeaderPreview);
}
pnlHeaderPreview.add(spHeaderPreview, BorderLayout.CENTER);
}
pnlTop.add(pnlHeaderPreview, BorderLayout.CENTER);
//======== pnlParams ========
{
pnlParams.setMinimumSize(new Dimension(850, 140));
pnlParams.setPreferredSize(new Dimension(850, 140));
pnlParams.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u6c47\u603b\u53c2\u6570"));
pnlParams.setLayout(new GridLayoutManager(4, 4, new Insets(0, 0, 0, 0), 5, 5));
//---- label1 ----
label1.setText("\u8df3\u8fc7\u884c\u6570\uff08\u59822\uff09\uff1a");
pnlParams.add(label1, new GridConstraints(0, 0, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- t_skipRows ----
t_skipRows.setText("2");
pnlParams.add(t_skipRows, new GridConstraints(0, 1, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- label2 ----
label2.setText("\u5206\u7c7b\u5217\u7d22\u5f15\uff1a");
pnlParams.add(label2, new GridConstraints(0, 2, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- t_categoryColumn ----
t_categoryColumn.setText("1");
pnlParams.add(t_categoryColumn, new GridConstraints(0, 3, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- label3 ----
label3.setText("\u5bfc\u51fa\u5217\u7d22\u5f15\uff08\u9017\u53f7\u5206\u9694\uff09\uff1a");
pnlParams.add(label3, new GridConstraints(1, 0, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- t_exportColumns ----
t_exportColumns.setText("1,5,8,9,10,6,13");
pnlParams.add(t_exportColumns, new GridConstraints(1, 1, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- label4 ----
label4.setText("排序列索引(多列逗号分隔):");
pnlParams.add(label4, new GridConstraints(1, 2, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- t_sortColumn ----
t_sortColumn.setText("9");
pnlParams.add(t_sortColumn, new GridConstraints(1, 3, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- label5 ----
label5.setText("\u6c47\u603b\u5217\u7d22\u5f15\uff08\u9017\u53f7\u5206\u9694\uff09\uff1a");
pnlParams.add(label5, new GridConstraints(2, 0, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- t_summaryColumns ----
t_summaryColumns.setText("6,13");
pnlParams.add(t_summaryColumns, new GridConstraints(2, 1, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- label6 ----
label6.setText("\u5c0f\u6570\u4f4d\u6570\uff1a");
pnlParams.add(label6, new GridConstraints(2, 2, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- t_decimalPlaces ----
t_decimalPlaces.setText("2");
pnlParams.add(t_decimalPlaces, new GridConstraints(2, 3, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- label7 ----
label7.setText("");
pnlParams.add(label7, new GridConstraints(3, 0, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- label8 ----
label8.setText("");
pnlParams.add(label8, new GridConstraints(3, 1, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- label9 ----
label9.setText("");
pnlParams.add(label9, new GridConstraints(3, 2, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
//---- label10 ----
label10.setText("");
pnlParams.add(label10, new GridConstraints(3, 3, 1, 1,
GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null, null, null));
}
pnlTop.add(pnlParams, BorderLayout.SOUTH);
}
// add(pnlTop, BorderLayout.NORTH);
mainPanel.add(pnlTop, BorderLayout.NORTH);
//======== pnlBottom ========
{
pnlBottom.setLayout(new BorderLayout(5, 5));
//======== pnlButtonProgress ========
{
pnlButtonProgress.setLayout(new BorderLayout(10, 5));
//======== pnlButton ========
{
pnlButton.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 5));
//---- btn_execute ----
btn_execute.setText("\u6267\u884c\u6c47\u603b");
pnlButton.add(btn_execute);
//---- btn_clear ----
btn_clear.setText("\u6e05\u7a7a\u53c2\u6570");
pnlButton.add(btn_clear);
}
pnlButtonProgress.add(pnlButton, BorderLayout.WEST);
//======== pnlProgress ========
{
pnlProgress.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u6267\u884c\u8fdb\u5ea6"));
pnlProgress.setLayout(new BorderLayout(5, 5));
//---- progressBar ----
progressBar.setMaximum(100);
progressBar.setMinimum(0);
progressBar.setString("\u672a\u6267\u884c");
progressBar.setStringPainted(true);
progressBar.setValue(0);
pnlProgress.add(progressBar, BorderLayout.CENTER);
}
pnlButtonProgress.add(pnlProgress, BorderLayout.CENTER);
}
pnlBottom.add(pnlButtonProgress, BorderLayout.NORTH);
//======== pnlLog ========
{
pnlLog.setBorder(new TitledBorder(LineBorder.createBlackLineBorder(), "\u6267\u884c\u65e5\u5fd7"));
pnlLog.setLayout(new BorderLayout(5, 5));
//======== spLog ========
{
//---- t_log ----
t_log.setBackground(Color.black);
t_log.setCaretColor(Color.green);
t_log.setEditable(false);
t_log.setFont(new Font("Yu Gothic UI", t_log.getFont().getStyle(), t_log.getFont().getSize()));
t_log.setForeground(Color.green);
spLog.setViewportView(t_log);
}
pnlLog.add(spLog, BorderLayout.CENTER);
}
pnlBottom.add(pnlLog, BorderLayout.CENTER);
}
//add(pnlBottom, BorderLayout.CENTER);
mainPanel.add(pnlBottom, BorderLayout.CENTER);
this.setContentPane(mainPanel);
// JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on
}
/**
* 自定义表格单元格渲染器,用于显示不同颜色的列
*/
class ColoredCellRenderer implements TableCellRenderer {
private final DefaultTableCellRenderer defaultRenderer = new DefaultTableCellRenderer();
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int column) {
Component component = defaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
// 如果选中,则使用选中状态的颜色
if (isSelected) {
component.setBackground(table.getSelectionBackground());
component.setForeground(table.getSelectionForeground());
return component;
}
// 根据列的用途设置不同背景色
if (categoryColumns.contains(column)) {
component.setBackground(categoryColumnColor);
} else if (exportColumns.contains(column)) {
component.setBackground(exportColumnsColor);
} else if (sortColumns.contains(column)) {
component.setBackground(sortColumnColor);
} else if (summaryColumns.contains(column)) {
component.setBackground(summaryColumnsColor);
} else {
component.setBackground(table.getBackground());
}
component.setForeground(table.getForeground());
return component;
}
}
// JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on
}