Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【读取excel】样式获取 #312

Open
2259289435 opened this issue Dec 5, 2022 · 27 comments
Open

【读取excel】样式获取 #312

2259289435 opened this issue Dec 5, 2022 · 27 comments
Labels

Comments

@2259289435
Copy link
Contributor

请教读取excel的时候,通过现有 api 或者某种方式怎么获取到单元格样式?
方便后续使用。

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 5, 2022 via email

@2259289435
Copy link
Contributor Author

嗯,我可以先用哪个api呢,后续可以关注项目了解进度,现在样式只有numfmt也没关系。

@wangguanquan
Copy link
Owner

  1. 首先在ExcelReader#init方法大约570有一段Styles.load(s);代码初始化加载样式文件
  2. 解析Cell的时候样式保存在org.ttzero.excel.reader.Cell对象的xf属性中,xf为style index并不是style
  3. 在Row对象中需要增加public int getStyle(){ }方法,调用Styles#getStyleByIndex方法使用第2步的style index换取style
  4. 然后就可以Styles#getNumFmt, Styles#getFill, Styles#getFont, Styles#getBorder 等方法获取具体的样式了

有兴趣的话可以尝试实现xlsx格式的样式解析

@2259289435
Copy link
Contributor Author

  1. 首先在ExcelReader#init方法大约570有一段Styles.load(s);代码初始化加载样式文件
  2. 解析Cell的时候样式保存在org.ttzero.excel.reader.Cell对象的xf属性中,xf为style index并不是style
  3. 在Row对象中需要增加public int getStyle(){ }方法,调用Styles#getStyleByIndex方法使用第2步的style index换取style
  4. 然后就可以Styles#getNumFmt, Styles#getFill, Styles#getFont, Styles#getBorder 等方法获取具体的样式了

有兴趣的话可以尝试实现xlsx格式的样式解析

3q, 对第 3 点 还有点疑问。我搜索代码发现 ExcelReader#init 570 获取到的 styles 在599设置到了 sheet 中,是不是我如果需要获取 cell 的 style,可以通过 sheet.getStyles().getStyleByIndex(row.getCell(0).xf).
不明白为什么说通过 row 去获取style。把业务样板代码写好了来尝试做除fmt外的样式解析。

@2259289435
Copy link
Contributor Author

  1. 首先在ExcelReader#init方法大约570有一段Styles.load(s);代码初始化加载样式文件
  2. 解析Cell的时候样式保存在org.ttzero.excel.reader.Cell对象的xf属性中,xf为style index并不是style
  3. 在Row对象中需要增加public int getStyle(){ }方法,调用Styles#getStyleByIndex方法使用第2步的style index换取style
  4. 然后就可以Styles#getNumFmt, Styles#getFill, Styles#getFont, Styles#getBorder 等方法获取具体的样式了

有兴趣的话可以尝试实现xlsx格式的样式解析

请教一下这种用法有问题吗?

image

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 5, 2022

写法是正确的,但是基建好像不对,并没有拿到样式,晚点我尝试下

@wangguanquan
Copy link
Owner

不好意思可能误导你了,因为Styles.load(s);并没有装载到styleIndex,所以styles.getStyleByIndex(cell.xf)返回的是-1,无法拿到样式,需要实现Styles#load方法才可以的

@2259289435
Copy link
Contributor Author

不好意思可能误导你了,因为Styles.load(s);并没有装载到styleIndex,所以styles.getStyleByIndex(cell.xf)返回的是-1,无法拿到样式,需要实现Styles#load方法才可以的

Styles#load 要实现才可以,是不是意味着现在没有任何方法可以拿到任何样式?
有什么变通的方案没,希望能尽快拿到 numfmt 跑通业务。

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 6, 2022 via email

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 6, 2022 via email

@2259289435
Copy link
Contributor Author

int style = styles.of(cell.xf); NumFmt numFmt= styles.getNumFmt(style); 这样写就可以了

多谢支持,已经取到了。
int style = styles.of(cell.xf),这里获取的 style 是不是就是完整的 style,只是暂时只有 NumFmt 的位数设置了,其他的还没有实现而没有设置。
我需要做个样式复制的功能,所以如果此 style 代表的就是完整的 style,那就可以直接使用,后续再去补充其他位数的实现。

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 6, 2022 via email

@2259289435
Copy link
Contributor Author

继续请教,从 a excel 的单元格获取到多个 style(styles.of(cell.xf)),然后现在需要写 b excel,希望能用从 a 里的 style 直接设置 b。不知道最佳实践如何处理比较好。
或者说这个场景现在框架未考虑,需要去实现?

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 6, 2022 via email

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 6, 2022 via email

wangguanquan pushed a commit that referenced this issue Dec 6, 2022
@wangguanquan
Copy link
Owner

fix#312分支增增加对样式的解析但并未经过完全测试,你可以fork本项目并切到fix#312分支进行BUG修改,欢迎提交PR。

@2259289435
Copy link
Contributor Author

需要完全解析才可以的,当前虽然可以拿到完全的样式值但是各个主体(字体,边框,填充,和格式化)并没有解析,通过当前的style只能得到水平/垂直对其,是否折行。 当然如果目标和原有着相同样式是可以通过of方法复制的,我说的是整个styles.xml完全一样,比如模版

目标是我要生成的一个新文件,在新文件内写入内容,这种目标的styles.xml和模板不相同呀,能请教下你说的模块复制怎么处理吗,我是需要在生成的新文件内完全复用模板的样式。

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 7, 2022 via email

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 8, 2022 via email

@2259289435
Copy link
Contributor Author

testTemplate

我其实就是想用模板这种,但我的模板只有样式,和你预设的这种场景有点不匹配。

比如模板原来有 a,b 两个 sheet,我可以基于当前这个模板再去创建 c, d, e 三个 sheet,希望在写入 c,d,e三个 sheet 的的cell可以直接用 a,b 两个 sheet 中的样式,写完 c, d, e sheet 后再去删除 a, b 两个sheet。

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 9, 2022 via email

@2259289435
Copy link
Contributor Author

可以加我微信聊吧,听你描述好像并不太难的样子,如果没有敏感信息的话可以将样本和预期的效果发邮箱我详细看下,邮箱地址点我头像查看

已发示例邮件

@wangguanquan
Copy link
Owner

wangguanquan commented Dec 12, 2022 via email

@wangguanquan
Copy link
Owner

我写了一个示例,读取模板某一行做为范本追写数据

  1. 自定义模板实现
 public static class MyEmbedTemplate<T> extends EmbedTemplate {

    private int sheetId, copyRowNum; // 指定要复制的Sheet和要复制的行(从1开始)
    private final Supplier<List<T>> supplier; // 获取数据用
    private ExtBufferedWriter bw; // 写文件
    private int rowNum; // 记录当前行号
    private TemplateRow tr;

    public MyEmbedTemplate(Path zipPath, Workbook wb, int sheetId, int copyRowNum, Supplier<List<T>> supplier) {
        super(zipPath, wb);
        this.sheetId = sheetId;
        this.copyRowNum = copyRowNum;
        this.supplier = supplier;
        this.rowNum = copyRowNum;
    }

    @Override
    protected int bindSstData() {
        return 0;
    }

    @Override
    protected int bindSheetData() {
        // Read content
        Path contentTypePath = zipPath.resolve("[Content_Types].xml");
        SAXReader reader = new SAXReader();
        Document document;
        try {
            document = reader.read(Files.newInputStream(contentTypePath));
        } catch (DocumentException | IOException e) {
            return 0;
        }

        // Find Override
        List<Element> overrides = document.getRootElement().elements("Override");
        for (Element e : overrides) {
            if (Const.ContentType.SHEET.equals(e.attributeValue("ContentType"))) {
                Path sheetPath = zipPath.resolve(e.attributeValue("PartName").substring(1));
                String sheetId = sheetPath.getFileName().toString();
                sheetId = sheetId.substring(5, sheetId.length() - 4);
                System.out.println("读取 " + sheetId);

                // 只操作第一个Sheet页
                if (sheetId.equals(String.valueOf(this.sheetId))) {
                    try {
                        writeSheet1(sheetPath);
                    } catch (IOException ex) {
                        throw new ExcelWriteException("Write data failed", ex);
                    }
                }
            }
        }
        return 0;
    }

    // Write template sheet, returns rows
    protected int writeSheet1(Path path) throws IOException {
        // 解析模板行
        this.tr = parseTemplateRow(path);

        bw = new ExtBufferedWriter(Files.newBufferedWriter(path, StandardCharsets.UTF_8));

        // 写头部
        bw.write(tr.header);

        // 循环写行数据
        List<T> list = supplier.get();
        if (list != null && !list.isEmpty()) {
            // 解析实体与列进行匹配,这里可以借助于ListSheet
            ListSheet<T> listSheet = new ListSheet<>(list);
            listSheet.setSheetWriter(new XMLWorksheetWriter());
            listSheet.setWorkbook(new Workbook());
            org.ttzero.excel.entity.Column[] columns = listSheet.getAndSortHeaderColumns();
            Cell[] cells = tr.getCells();

            for (org.ttzero.excel.entity.Column col : columns) {
                if (col.getRealColIndex() <= cells.length) {
                    Cell c = cells[col.getRealColIndex() - 1];
                    Class<?> clazz = col.getClazz();
                    if (isString(clazz)) {
                        c.t = Cell.INLINESTR;
                    }
                    else if (isInt(clazz) || isLong(clazz) || isFloat(clazz) || isDouble(clazz) || isBigDecimal(clazz)) {
                        c.t = Cell.NUMERIC;
                    } else c.t = Cell.INLINESTR;
                }
            }
            tr.columns = columns;

            for (T t : list) writeRow(t);
            for (; (list = supplier.get()) != null && !list.isEmpty();)
                for (T t : list) writeRow(t);
        } else bw.write(tr.tmp); // 如果没有数据的话写入原本的模板行

        // 写尾部
        bw.write(tr.tail);
        bw.close();

        return 0;
    }

    private TemplateRow parseTemplateRow(Path path) {
        SAXReader reader = new SAXReader();
        Document document;
        try {
            document = reader.read(Files.newInputStream(path));
        } catch (DocumentException | IOException e) {
            throw new ExcelWriteException("Miss the template Worksheet " + path);
        }

        List<Element> rows = document.getRootElement().element("sheetData").elements("row");
        if (rows.size() < copyRowNum) {
            throw new ExcelWriteException("Specify row " + copyRowNum + " is large than template sheet rows " + rows.size());
        }

        Element row = rows.get(copyRowNum - 1);
        String span = Styles.getAttr(row, "spans");
        String customHeight = Styles.getAttr(row, "customHeight"), ht = Styles.getAttr(row, "ht"); // 如果customHeight为1或者true则ht属性生效
        String customFormat = Styles.getAttr(row, "customFormat"), rs = Styles.getAttr(row, "s"); // 如果customFormat为1或者true则rs属性生效

        int i = span.indexOf(':');
        int[] spans = i > 0 ? new int[]{Integer.parseInt(span.substring(0, i)), Integer.parseInt(span.substring(i + 1))} : new int[]{0, Integer.parseInt(span)};
        int[] xfs = new int[spans[1]];
        List<Element> list = row.elements();
        for (Element c : list) {
            String cr = Styles.getAttr(c, "r"), cs = Styles.getAttr(c, "s");
            long r = ExcelReader.cellRangeToLong(cr);
            int ci = (int) (r & 0x7FFF), s = StringUtil.isNotEmpty(cs) ? Integer.parseInt(cs) : 0;
            xfs[ci - 1] = s;
        }

        String sheetData;
        try {
            InputStream is = Files.newInputStream(path);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int n;
            byte[] bytes = new byte[1 << 11];
            while ((n = is.read(bytes)) > 0) {
                bos.write(bytes, 0, n);
            }
            sheetData = new String(bos.toByteArray(), 0, bos.size(), StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new ExcelWriteException("");
        }

        for (int n = 1; n <= copyRowNum; n++) i = sheetData.indexOf("<row ", i + 1);
        int j = sheetData.indexOf("</row>", i) + 6;
        String header = sheetData.substring(0, i), tmp = sheetData.substring(i, j), tail = sheetData.substring(j);

        TemplateRow tr = new TemplateRow();
        tr.header = header;
        tr.tail = tail;
        tr.tmp = tmp;
        tr.ht = ht;
        tr.customHeight = ("1".equals(customHeight) || "true".equalsIgnoreCase(customHeight)) && StringUtil.isNotEmpty(ht);
        tr.rs = rs;
        tr.customFormat = ("1".equals(customFormat) || "true".equalsIgnoreCase(customFormat)) && StringUtil.isNotEmpty(rs);
        tr.calloc(spans[1]); // 分配列
        Cell[] cells = tr.getCells();
        int col = 0;
        for (Cell c : cells) {
            c.i = (short) ++col;
            c.xf = xfs[col - 1]; // Style index
        }

        return tr;
    }

    private void writeRow(T o) throws IOException {
        Cell[] cells = tr.getCells();
        org.ttzero.excel.entity.Column[] columns = tr.columns;
        int len = cells.length;

        // 写行
        startRow(len);
        try {
            boolean notNull = o != null;
            for (int i = 0; i < columns.length; i++) {
                Object e;
                ListSheet.EntryColumn column = (ListSheet.EntryColumn) columns[i];
                // Clear cells
                Cell cell = cells[column.getRealColIndex() - 1];
                if (column.isIgnoreValue())
                    e = null;
                else if (notNull) {
                    if (column.getMethod() != null)
                        e = column.getMethod().invoke(o);
                    else if (column.getField() != null)
                        e = column.getField().get(o);
                    else e = o;
                }
                else e = null;

                if (e != null) {
                    if (cell.t == Cell.NUMERIC) {
                        writeNumeric(Long.parseLong(e.toString()), rowNum, column.getRealColIndex(), cell.xf);
                    } else {
                        writeString(e.toString(), rowNum, column.getRealColIndex(), cell.xf);
                    }
                } else writeNull(rowNum, column.getRealColIndex(), cell.xf);
            }
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new ExcelWriteException(e);
        }

        // 行尾
        endRow();
    }

    protected int startRow(int columns) throws IOException {
        bw.write("<row r=\"");
        bw.writeInt(rowNum);
        // default data row height 16.5
        bw.write("\" spans=\"1:");
        bw.writeInt(columns);
        if (tr.customHeight) {
            bw.write("\" ht=\"");
            bw.write(tr.ht);
        }
        if (tr.customFormat) {
            bw.write("\" s=\"");
            bw.write(tr.rs);
        }
        bw.write("\">");
        return rowNum;
    }

    protected void endRow() throws IOException {
        rowNum++; // 行+1
        bw.write("</row>");
    }

    protected void writeNumeric(long l, int row, int col, int xf) throws IOException {
        bw.write("<c r=\"");
        bw.write(int2Col(col));
        bw.writeInt(row);
        bw.write("\" s=\"");
        bw.writeInt(xf);
        bw.write("\"><v>");
        bw.write(l);
        bw.write("</v></c>");
    }

    protected void writeNull(int row, int col, int xf) throws IOException {
        bw.write("<c r=\"");
        bw.write(int2Col(col));
        bw.writeInt(row);
        bw.write("\" s=\"");
        bw.writeInt(xf);
        bw.write("\"/>");
    }

    protected void writeString(String s, int row, int col, int xf) throws IOException {
        // The limit characters per cell check
        if (s != null && s.length() > Const.Limit.MAX_CHARACTERS_PER_CELL) {
            throw new ExcelWriteException("Characters per cell out of limit. size=" + s.length()
                + ", limit=" + Const.Limit.MAX_CHARACTERS_PER_CELL);
        }
        bw.write("<c r=\"");
        bw.write(int2Col(col));
        bw.writeInt(row);
        if (StringUtil.isEmpty(s)) {
            bw.write("\" s=\"");
            bw.writeInt(xf);
            bw.write("\"/>");
        } else {
            bw.write("\" t=\"inlineStr\" s=\"");
            bw.writeInt(xf);
            bw.write("\"><is><t>");
            bw.escapeWrite(s); // escape text
            bw.write("</t></is></c>");
        }
    }

    public static class TemplateRow extends Row {
        public String header, tail, tmp;
        public String ht, rs;
        public boolean customHeight, customFormat;
        public org.ttzero.excel.entity.Column[] columns;
    }
}
  1. 指定需要复制的worksheet的列
@Test public void testTemplate312() throws IOException {
    try (InputStream fis = Files.newInputStream(Paths.get("template.xlsx"))) {
        Workbook workbook = new Workbook("Issue#312").withTemplate(fis, new Object()); // 这里不需要指定数据

        // 查询条件
        GoodsQueryVo vo = new GoodsQueryVo();
        vo.setPage(1);
        vo.setPageSize(100);

        workbook.setWorkbookWriter(new XMLWorkbookWriter() {
            @Override
            public Path template() throws IOException {
                // Store template stream as zip file
                Path temp = FileUtil.mktmp(Const.EEC_PREFIX);
                ZipUtil.unzip(workbook.getTemplate(), temp);

                // Bind data  其中参数1表示第一个Sheet页 4表示第4行做为模板行
                EmbedTemplate bt = new MyEmbedTemplate<>(temp, workbook, 1, 4, () -> {
                    List<Goods> goods = goodsService.query(vo); // 查询数据
                    vo.setPage(vo.getPage() + 1); // 页码+1
                    return goods;
                });

                if (bt.check()) { // Check files
                    bt.bind(workbook.getBind());
                }

                // Zip compress
                Path zipFile = ZipUtil.zipExcludeRoot(temp, temp);

                // Delete source files
                FileUtil.rm_rf(temp.toFile(), true);

                // Close shared string table
                workbook.getSst().close();

                return zipFile;
            }
        }).writeTo(defaultTestPath);
    }
}

如果想要使用其它单元格的样式,可以通过ExcelReader读取拿到Cell#xf,写数据的时候使用该xf即可。

@wangguanquan
Copy link
Owner

导出的对象字段上需要添加@ExcelColumn(colIndex = x) 顺序与模板一样

@zhangmuto
Copy link

zhangmuto commented Mar 23, 2023

刚刚看到这个问题,刚好我写过应该能解决你的问题,希望对你有帮助,以下代码是完整复刻sheet,即完整复制样式

//样式类
@NoArgsConstructor
@DaTa
@AllArgsConstructor
public class MyStyleProcessor implements StyleProcessor {
List cellStyles;
@OverRide
public int build(Object o, int style, Styles styles) {
CellStyle cellStyle = cellStyles.get(0);
cellStyles.remove(0);
return cellStyle.getStyle(style,styles);
}

}

//样式类
@DaTa
@accessors(chain = true)
@NoArgsConstructor
public class CellStyle {

public CellStyle(String name,int style,Styles styles){
    // 获取字体
    this.font = styles.getFont(style);
    // 获取边框
    this.border = styles.getBorder(style);
    // 获取填充
    this.fill = styles.getFill(style);
    // 获取格式化
    this.numFmt = styles.getNumFmt(style);
    this.horizontal = styles.getHorizontal(style);
    this.vertica = styles.getVertical(style);
}

public int getStyle(int style,Styles styles){
    styles.clearFont(style);
    styles.clearFill(style);
    styles.clearBorder(style);

    if (numFmt==null){
        return  styles.addFont(font)|styles.addBorder(border)|styles.addFill(fill)|Horizontals.CENTER|Verticals.CENTER;
    }
    styles.clearNumFmt(style);
    return  styles.addNumFmt(numFmt)|styles.addFont(font)|styles.addBorder(border)|styles.addFill(fill)|Horizontals.CENTER|Verticals.CENTER;
}

private String name;
private Font font;
private Border border;
private  Fill fill;
private  NumFmt numFmt;
private  int horizontal = Horizontals.CENTER;
private  int vertica = Verticals.CENTER;

}

//核心代码
public static void main(String[] args) {

    Path path = Paths.get(你的模板excel);
    try (ExcelReader reader = ExcelReader.read(path,COPY_ON_MERGED)) {

        Workbook wb = new Workbook();

        ArrayList<Map<String,?>> objects = new ArrayList<>();

        List<Column> columns = new ArrayList<>(); //每一列
        List<List<CellStyle>> cellStylesList = new ArrayList<>(); //每一列
        Sheet sheet = reader.sheet(0); //MergeSheet
        List<Dimension> mergeCells = getMergeCells(sheet);


        reader.sheet(0).rows().forEach(row -> {
            HashMap<String, String> dataMap = new HashMap<>();

            for (int i = columns.size(); i < row.getLastColumnIndex(); i++) { //创建列
                ArrayList<CellStyle> cellStyles = new ArrayList<>();
                columns.add(new Column(i+"",i+"").setStyleProcessor(new MyStyleProcessor(cellStyles)));
                cellStylesList.add(cellStyles);
            }

            for (int i = 0; i < row.getLastColumnIndex(); i++) { //行的每一格
                Cell cell = row.getCell(i);
                String name = row.getString(cell);
                dataMap.put(i+"", StringUtils.isNoneBlank(name)? name:"");
                // 第一步 获取Styles对象
                Styles styles = row.getStyles();
                // 第二步 获取指定单元格样式
                int style = row.getCellStyle(cell);
                cellStylesList.get(i).add(new CellStyle(name,style,styles));
            }
            objects.add(dataMap);
        });

        ListMapSheet tmpSheet = new ListMapSheet("sheet", objects, columns.toArray(new Column[columns.size()]));
        tmpSheet.ignoreHeader();
        tmpSheet.putExtProp(Const.ExtendPropertyKey.MERGE_CELLS, mergeCells);
        wb.addSheet(tmpSheet);
        wb.writeTo(new File(excel输出路径));


    } catch (IOException e) {
        e.printStackTrace();
    }
}
public static  List<Dimension>  getMergeCells(Object o){ //获取合并格
    try {
        Method[] methods = ReflectUtils.getMethods(o.getClass()); //TODO 注意这里要使用 getDeclaredMethods,工具类使用的是hutool
        Method method = Arrays.stream(methods).filter(m -> m.getName().equals("getMergeCells")).findFirst().get();
        method.setAccessible(true);
        return (List<Dimension>) method.invoke(o);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

@wangguanquan
Copy link
Owner

@zhangmuto 获取哪些单元格被合并了可以通过MergeSheet#getMergeCells直接获取,像这样List<Dimension> mergeCells = reader.sheet(0).asMergeSheet().getMergeCells();,不需要反射遍历。

像上面的代码你读取时候已经指定了COPY_ON_MERGED属性,所以调用asMergedSheet并不会增加额外开销,这里使用类型强转也可以,如果未指定COPY_ON_MERGED属性调用asMergedSheet方法会跳到尾部读取合并信息,所以asMergeSheet方法是安全的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants