【问题标题】:Apache POI: Elegant way to set borders to a column which contains different stylesApache POI:为包含不同样式的列设置边框的优雅方式
【发布时间】:2016-05-11 07:34:23
【问题描述】:

我正在使用 apache-poi 动态创建一个新的 xlsx 文件。任何列都可以包含不同的值类型(数字、字符串、布尔值...)。在将数据插入 poi 文档时,我根据数据的类型设置了 CellStyles:

public final XSSFCellStyle cellStyleString;
public final XSSFCellStyle cellStyleNumber;
public final XSSFCellStyle cellStyleDate;
public final XSSFCellStyle cellStyleHeader;

这是我的标题行的样子:

|   |   |   | Shared Header |
| H1| H2| H3|SH1|SH2|SH3|SH4|

有“简单”标题和包含“子标题”的“共享标题”。共享标题驻留在合并的单元格中。

不,我想在SH1 列有一个左边框,在SH4 列有一个右边框以强调分组。但由于任何列都可能包含所有单元格样式的混合,所以我似乎必须创建像

这样的单元格样式
public final XSSFCellStyle cellStyleString;
public final XSSFCellStyle cellStyleStringBorderLeft;
public final XSSFCellStyle cellStyleStringBorderRight;
//and so on for the other styles...

此外,我希望通过不同的边框大小来区分嵌套的共享标题。所以我需要像

public final XSSFCellStyle cellStyleString;
public final XSSFCellStyle cellStyleStringBorderLeftThickLine;
public final XSSFCellStyle cellStyleStringBorderRightThickLine;
public final XSSFCellStyle cellStyleStringBorderLeftThinLine;
public final XSSFCellStyle cellStyleStringBorderRightThinLine;
//and so on for the other styles...

是否有更优雅的方式来设置列的边框而不管现有样式如何?

编辑

虽然我更喜欢干净简单的方法,并且为了尽量减少创建样式的数量,但我偶然发现了HSSFOptimiser,它可以删除重复的单元格样式。我不知道那堂课。尽管我更喜欢避免使用此实用程序,但它适合问题并且值得在这里提及。

【问题讨论】:

    标签: java apache-poi


    【解决方案1】:

    我即将完成对 POI 的增强,它可以让您使用其特定样式填充值,然后在它们周围绘制边框,而无需为此手动创建所有必要的样式。同时,有一种方法可以使用CellUtil.setCellStyleProperties()。这使您可以将一组属性添加到单元格已存在的CellStyle

    来自 HSSF/XSSF 的 POI 快速指南:

    Workbook workbook = new XSSFWorkbook();  // OR new HSSFWorkbook()
    Sheet sheet = workbook.createSheet("Sheet1");
    Map<String, Object> properties = new HashMap<String, Object>();
    
    // create your spreadsheet without borders
    ...
    
    // create property set for vertical borders
    properties.put(CellUtil.BORDER_LEFT, CellStyle.BORDER_MEDIUM);
    properties.put(CellUtil.BORDER_RIGHT, CellStyle.BORDER_MEDIUM);
    
    // Apply the borders to a 3x3 region starting at D4
    for (int ix=3; ix <= 5; ix++) {
      row = sheet.createRow(ix);
      for (int iy = 3; iy <= 5; iy++) {
        cell = row.createCell(iy);
        CellUtil.setCellStyleProperties(cell, properties);
      }
    }
    

    这使您基本上可以填写电子表格,然后一次绘制一个单元格的边框。请注意,如果您的所有边框都相似(都很薄),那么这将适用于您的整个范围。但是,如果您想在表格外部绘制 MEDIUM 边框,则必须创建一些额外的属性集。请注意,您不必对电子表格中已有的行和单元格使用createRow()createCell()。这将解决合并的单元格。

    注意:CellUtil.setCellStyleProperties() 出现在 POI 3.14 中,允许您一次添加多个单元格属性,从而避免创建多个未使用的样式。较旧的CellUtil.setCellStyleProperty() 一次设置一个属性,并且意外地在电子表格中创建了中间CellStyle 对象,这些对象最终从未被使用过。这在较大的工作表中可能是一个问题。

    编辑: PropertyTemplate 是 POI 3.15 中添加的一个新对象,它允许您为单元格定义一组边框并将其标记到您想要应用它的任何工作表上。这个对象就像创建一个预打印的表单来覆盖数据。有关如何使用PropertyTemplate 的更多信息,请参阅 POI 电子表格快速指南。

    【讨论】:

    • 这个解决方案听起来很棒。遗憾的是,我现在无法测试它,因为我不得不继续执行另一项任务…… CellStyles 会以这种方式重用吗?换句话说:是否会为每个遇到的单元格或每个遇到的 cellStyle 创建一个新的 CellStyle?
    • 我刚刚阅读了我们的依赖层次结构:我们仍在使用 poi 3.9。但我认为使用旧方法setCellStyleProperty 创建未使用的中间样式是可以接受的。总共有很少的样式,唯一的修改是每个单元格最多 2 个边框。拥有 30% 未使用的样式仍然比拥有 >30.000 个伪唯一样式更好。我以前的问题现在已经过时了,因为我阅读了setCellStyleProperty 的源代码并找到了重用已知样式的部分。非常感谢!
    • 当您使用setCellStyleProperty 时会发生什么,它会从单元格中检索CellStyle,创建一个模拟CellStyle 并合并新属性,并尝试找到等效的CellStyle在工作簿中。如果不存在,则创建一个新的CellStyle 并将其应用于单元格。 setCellStyleProperties 允许使用一组属性而不是一次一个方法来执行此操作。如果您正在创建垂直边框,您最终会得到一堆未使用的样式,其中只有一个左边框或一个右边框,具体取决于您添加边框的顺序。
    • 您也可以使用它来设置其他CellStyle 属性,例如填充。
    【解决方案2】:

    正如您已经提到的,创建成千上万个类似的单元格样式对象是不好的。在我的项目中,我创建了一个简单的“样式助手”类,其中包含一个地图,它知道所有现有的样式实例

    private Workbook workbook;
    private HashMap<String, CellStyle> styleMap = new HashMap<>();
    
    public CellStyle getStyle(Font font, ExcelCellAlign hAlign, ExcelCellAlign vAlign, boolean wrapText, ExcelCellBorder border, Color color, ExcelCellFormat cellFormat) {
    
        //build unique which represents the style
        String styleKey = ((font != null) ? font.toString() : "") + "_" + hAlign + "_" + vAlign + (wrapText ? "_wrapText" : "") + ((border != null) ? "_" + border.toString() : "") + "_"
                + styleKeyColor + (cellFormat != null ? "_" + cellFormat.toString() : "");
    
        if (styleMap.containsKey(styleKey)) {
            //return existing instance from map
            return styleMap.get(styleKey);
        } else {
            //create new style from workbook
            CellStyle cellStyle = workbook.createCellStyle();
    
    
            // set all formattings to new cellStyle object
            if (font != null) {
                cellStyle.setFont(font);
            }
    
            // alignment
            if (vAlign != null) {
                cellStyle.setVerticalAlignment(vAlign.getAlign());
            }
    
            //... snip ...
    
            //border
            if (border != null) {
                if (border.getTop() > BorderFormatting.BORDER_NONE) {
                    cellStyle.setBorderTop(border.getTop());
                    cellStyle.setTopBorderColor(HSSFColor.BLACK.index);
                }
    
                //... snip ...
            }
    
                if (color != null) {
                    XSSFColor xssfColor = new XSSFColor(color);
                   ((XSSFCellStyle)cellStyle).setFillForegroundColor(xssfColor);
                }
            }
            cellStyle.setFillPattern(CellStyle.SOLID_FOREGROUND);
    
            styleMap.put(styleKey, cellStyle);
    
            return cellStyle;
        }
    }
    

    参数 ExcelCellAlign 是一个简单的枚举,它封装了 CellStyle.ALIGN_LEFT、CellStyle.ALIGN_RIGHT、...的值 ExcelCellBorder 类似于 Align。只需隐藏值:-) ExcelCellFormat 是一个枚举,它包含用于格式化值的默认模式。

    我希望这是您自己实施的良好开端。有什么不明白的欢迎追问

    【讨论】:

      【解决方案3】:

      编辑:

      那么如何利用 POI 对象的哈希值来缓存和跟踪装饰对象。其他未使用的已创建 CellStyles 将被垃圾回收器丢弃。

      这是我们的缓存:

      private Map<Integer, MyCellStyle> styleCache = new HashMap<>();
      

      还有我们自己的 CellStyle 类

      final class MyCellStyle implements Cloneable {
          private XSSFCellStyle xssfCellStyle;
      
          public MyCellStyle(XSSFCellStyle xssfCellStyle) {
              this.xssfCellStyle = xssfCellStyle;
          }
      
          @Override
          public MyCellStyle clone() {
              MyCellStyle clone = new MyCellStyle(xssfCellStyle);
              return clone;
          }
      
          public final MyCellStyle borderLeftMedium() {
              MyCellStyle result = clone();
              result.xssfCellStyle.setBorderLeft(XSSFCellStyle.BORDER_MEDIUM);
              return result;
          }
      
          ... further decorations
      
          public XSSFCellStyle getXSSFCellStyle() {
              return xssfCellStyle;
          }
      
      }
      

      现在为了避免创建新对象,我们编写了一个小函数

      private MyCellStyle getCellStyle(MyCellStyle targetStyle) {
          int targetHash = targetStyle.hashCode();
          if (styleCache.keySet().contains(targetHash)) {
              return styleCache.get(targetHash);
          } else {
              return styleCache.put(targetHash, targetStyle);
          }
      }
      

      然后我们可以像这样创建单元格:

      public void createCells() {
          Workbook wb = new XSSFWorkbook();
          Sheet sheet = wb.createSheet();
      
          Row row = sheet.createRow(1);
          Cell cell = row.createCell(1);
      
          MyCellStyle baseStyle = new MyCellStyle(
                  (XSSFCellStyle) wb.createCellStyle());
      
          MyCellStyle decoratedStyle = getCellStyle(baseStyle.borderLeftMedium());
      
          cell.setCellStyle(decoratedStyle.getXSSFCellStyle());
      
      }
      

      如果 hashCode 对于 MyCellStyle 对象的相同属性不是唯一的,我们可能必须重写 hashCode 函数:

      @Override
      public int hashCode() {
          return hashValue;
      }
      

      并在我们的每个装饰函数中添加样式值:

      public final MyCellStyle borderLeftMedium() {
              MyCellStyle result = clone();
              result.xssfCellStyle.setBorderLeft(XSSFCellStyle.BORDER_MEDIUM);
              hashValue += XSSFCellStyle.BORDER_MEDIUM; // simplified hash
              return result;
          }
      

      =========================

      原文:

      我喜欢创建装饰方法,将单元格的某个方面添加到单元格样式中。所以首先你创建你的基本样式

      public final XSSFCellStyle cellStyleStringBase = wb.createCellStyle();
      

      并创建装饰器方法来创建某种样式

      public XSSFCellStyle addBorderLeft(XSSFCellStyle style) {
          XSSFCellStyle result = style.clone();
          result.setBorderLeft(XSSFCellStyle.BORDER_MEDIUM);
          return result;
      }
      

      现在,如果你想避免创建新对象,你仍然必须将 cellStyles 保存在自己的变量中,你将无法避免这种情况,但根据我的经验,如果你只是装饰你的单元格,性能就足够了像这样

      cell1.setCellStyle(addBorderLeft(cellStyleStringBase);
      cell2.setCellStyle(addBorderRight(addBorderRight(cellStyleStringBase));
      ...
      

      如果你用很多样式来装饰,创建你自己的 CellStyle 类是有意义的

      public final MyCellStyle implements Cloneable {
      
          private XSSFCellStyle xssfCellStyle;
      
          public MyCellStyle(XSSFCellStyle xssfCellStyle) {
               this.xssfCellStyle = xssfCellStyle;
          }
      
          @Override
          public MyCellStyle clone() {
              MyCellStyle clone = new MyCellStyle(this.xssfCellStyle);
              return clone;
          }
      
          public final MyCellStyle borderLeftMedium() {
              return this.clone().setBorderLeft(XSSFCellStyle.BORDER_MEDIUM);
          }
      
          public final MyCellStyle borderRightThick() {
              ...
      
      }
      

      然后您可以以更易读的方式构建您的样式:

      MyCellStyle base = new MyCellStyle(cellStyleStringBase);    
      cell1.setCellStyle(base
          .addBorderLeftMedium()
          .addBorderRightThick()
          .addBorderBottomThin());
      

      未经测试,但希望对您有所帮助。

      【讨论】:

      • 如果我为每个有边框的单元格创建新样式,我最终会得到数以万计的冗余单元格样式。出于多种原因,这是不可接受的。
      • 感谢您的编辑。但由于它仍然依赖于为每个装饰创建新样式,因此这种解决方案也不可接受。
      • 这将创建许多、许多、许多相同 CellStyle 的实例,并将增加 excel 文件的大小。
      • 缓存似乎确实是答案。不过,我对手动创建独特的哈希感到不舒服。 cellstyles 的领域似乎非常大,因此 int 可能会很小以确保唯一性。我将采用您和@Christoph-Tobias Schenke 的想法并为地图创建一个自定义键对象(它将在内部覆盖散列和等于)
      • 在找到这个主题之前,我刚刚创建了类似的东西。我创建了一个CellStyleBuilder 类(构建器模式),它可以以可读的方式创建单元格样式。它会在地图中缓存已创建的单元格样式。地图的键是一个简单的字符串,其中包含单元格样式的所有属性以及用于分隔值的分隔符。这对我来说非常有效,并且单元格样式的创建变得更加容易。
      猜你喜欢
      • 2018-01-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-02
      • 1970-01-01
      • 1970-01-01
      • 2016-04-26
      相关资源
      最近更新 更多