spreadsheet 电子表格

该组件需要安装商业版 Vaadin Spreadsheet 插件。

spreadsheet 组件可以在视图中嵌入兼容 Excel 的电子表格。在浏览器中渲染 Apache POI 工作簿,支持用户编辑,并为视图提供对底层 POI 模型的完整编程式访问。

XML 元素

spreadsheet

Java 类

Spreadsheet

基本用法

在视图 XML 中声明 sprd 命名空间并添加 spreadsheet 组件:

<view xmlns="http://jmix.io/schema/flowui/view"
      xmlns:sprd="http://jmix.io/schema/vaadin-spreadsheet/ui"
      title="msg://spreadsheetBasicsView.title">
    <layout expand="spreadsheet">
        <sprd:spreadsheet id="spreadsheet" width="100%"/>
    </layout>
</view>

当使用顶部操作面板中的 Add Component 操作添加组件时,Studio 会自动添加该命名空间。请参见 组件面板。

以常规方式在控制器中注入组件:

@ViewComponent
private Spreadsheet spreadsheet;

电子表格不与 数据容器 绑定。其内容可以从 XLSX 文件 加载,或是将 实体数据 填充到单元格。这两种情况下,都在内存中保存工作簿,页面刷新后编辑内容会丢失。

加载 XLSX

使用 read() 方法可以将文件内容加载到组件中,并替换其当前内容。该方法接收 InputStream,因此可以是从任何来源读取的文件:如下例中的应用程序资源、文件存储,或使用 fileStorageUploadField 上传的文件。

@Subscribe
public void onInit(final InitEvent event) {
    try (InputStream inputStream =
                 resources.getResourceAsStream("com/company/vaadinspreadsheetex1/budget.xlsx")) {
        spreadsheet.read(inputStream);
    } catch (IOException e) {
        throw new RuntimeException("Unable to read the budget template", e);
    }
}

而 write() 方法则相反,将工作簿写入输出流。然后将字节传递给 下载器,即可让用户保存当前文档:

@Subscribe("downloadBtn")
public void onDownloadBtnClick(final ClickEvent<JmixButton> event) {
    try {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        spreadsheet.write(outputStream);
        downloader.download(outputStream.toByteArray(), "budget.xlsx", DownloadFormat.XLSX);
    } catch (IOException e) {
        throw new RuntimeException("Unable to write the spreadsheet", e);
    }
}

使用实体数据

电子表格还可以使用实体的数据填充。单元格没有与属性绑定,因此在打开时需要填充工作表,在保存时中表中读取值。

下面的示例中,从 集合容器 读取实体然后填充单元格:

@ViewComponent
private CollectionContainer<User> usersDc;

@Subscribe
public void onBeforeShow(final BeforeShowEvent event) {
    spreadsheet.createCell(0, 0, "Username");
    spreadsheet.createCell(0, 1, "First name");
    spreadsheet.createCell(0, 2, "Last name");

    List<User> users = usersDc.getItems();
    for (int i = 0; i < users.size(); i++) {
        User user = users.get(i);
        int row = i + 1;
        spreadsheet.createCell(row, 0, user.getUsername());
        spreadsheet.createCell(row, 1, user.getFirstName());
        spreadsheet.createCell(row, 2, user.getLastName());
    }

    spreadsheet.createFreezePane(1, 0);
}

保存时,遍历每一行获取单元格的值,设置给相应的属性。实体在数据容器中,因此视图的 数据上下文 可以跟踪实体的改动,调用其 save() 保存所有更改:

@Subscribe("saveBtn")
public void onSaveBtnClick(final ClickEvent<JmixButton> event) {
    List<User> users = usersDc.getItems();
    for (int i = 0; i < users.size(); i++) {
        User user = users.get(i);
        int row = i + 1;
        user.setFirstName(getCellValue(row, 1));
        user.setLastName(getCellValue(row, 2));
    }
    dataContext.save();
}

@Nullable
private String getCellValue(int row, int column) {
    Cell cell = spreadsheet.getCell(row, column);
    return cell != null
            // DataFormatter returns the text of a cell whatever its type
            ? spreadsheet.getDataFormatter().formatCellValue(cell)
            : null;
}
这种方式只保存值。对工作表本身的更改(例如新列、格式或公式)在视图关闭时会丢失。

配置单元格

通过组件(而不是 POI)创建单元格,以便组件知晓渲染的内容。行和列的索引从零开始,下面的代码填充范围 A1:B2:

spreadsheet.createCell(0, 0, "Product");
spreadsheet.createCell(0, 1, "Amount");
spreadsheet.createCell(1, 0, "Widget");
spreadsheet.createCell(1, 1, 1200);

值可以是 String、Double、Boolean、Date 或 Calendar 类型。

已有单元格通过 getCell() 获取,可以按索引或按 A1 表示法中的地址获取。如果通过 POI API 更改单元格,需要刷新以显示新值。refreshCells() 的参数可以是多个单元格或一个集合:

Cell cell = spreadsheet.getCell("B2");
cell.setCellValue(1500);
spreadsheet.refreshCells(cell);

公式由单独的方法创建并在服务器上计算。编写时不需要以 = 开头:

spreadsheet.createFormulaCell(3, 1, "SUM(B2:B3)");

若要一次性重新计算所有公式(例如在替换一组值之后),请调用 refreshAllCellValues()。

设置单元格样式

字体、颜色、边框和数字格式属于工作簿,因此使用 POI API。创建样式,然后应用在单元格,最后刷新单元格:

Workbook workbook = spreadsheet.getWorkbook();

CellStyle headerStyle = workbook.createCellStyle();
Font boldFont = workbook.createFont();
boldFont.setBold(true);
headerStyle.setFont(boldFont);

CellStyle amountStyle = workbook.createCellStyle();
DataFormat dataFormat = workbook.createDataFormat();
amountStyle.setDataFormat(dataFormat.getFormat("#,##0.00"));

Cell headerCell = spreadsheet.getCell(0, 0);
headerCell.setCellStyle(headerStyle);

Cell amountCell = spreadsheet.getCell(1, 1);
amountCell.setCellStyle(amountStyle);

spreadsheet.refreshCells(headerCell, amountCell);
工作簿对单元格样式有数量上的限制,因此,每种样式仅创建一次,而不是每个单元格都新建样式。

选择

初始选中的单元格或范围由 selection 属性以 A1 表示法设置,例如 A1 或 B2:D8。每次用户选择其他内容时,组件都会通知视图:

@Subscribe("spreadsheet")
public void onSpreadsheetSelectionChange(final Spreadsheet.SelectionChangeEvent event) {
    CellReference reference = event.getSelectedCellReference();
    selectionLabel.setText(reference.formatAsString());
}

事件还包含:getIndividualSelectedCells() 返回单独选中的单元格,getCellRangeAddresses() 返回选中的范围。

在单元格添加组件

单元格可以显示 UI 组件,例如布尔列显示复选框。电子表格会询问工厂在单元格中放置什么组件,提供一个 spreadsheetComponentFactory 并为需要组件的单元格返回一个组件:

@Supply(to = "spreadsheet", subject = "spreadsheetComponentFactory")
private SpreadsheetComponentFactory spreadsheetComponentFactory() {
    return new SpreadsheetComponentFactory() {

        @Override
        public Component getCustomComponentForCell(Cell cell, int rowIndex, int columnIndex,
                                                   Spreadsheet spreadsheet, Sheet sheet) {
            if (columnIndex == 3 && rowIndex > 0) {
                Checkbox checkbox = uiComponents.create(Checkbox.class);
                checkbox.setValue(cell != null && cell.getBooleanCellValue());
                return checkbox;
            }
            return null; // leave the cell as it is
        }

        @Override
        public Component getCustomEditorForCell(Cell cell, int rowIndex, int columnIndex,
                                                Spreadsheet spreadsheet, Sheet sheet) {
            return null;
        }

        @Override
        public void onCustomEditorDisplayed(Cell cell, int rowIndex, int columnIndex,
                                            Spreadsheet spreadsheet, Sheet sheet,
                                            Component customEditor) {
        }
    };
}

先调用 getCustomComponentForCell(),仅当返回 null 时才调用 getCustomEditorForCell()。

包含组件的行高不会低于 minimumRowHeightForComponents,因此组件总是可见。

添加和删除工作表

工作簿包含一个或多个工作表(Sheet),组件一次显示其中一个。新工作表通过名称、行数和列数创建:

spreadsheet.createNewSheet("Summary", 100, 20); // name, rows, columns
spreadsheet.setSheetName(1, "Details");
spreadsheet.setActiveSheetIndex(1);

工作表的删除:

if (spreadsheet.getNumberOfSheets() > 1) {
    spreadsheet.deleteSheet(spreadsheet.getActiveSheetIndex());
}

工作表按索引寻址,有两种索引。不带 POIIndex 后缀的方法仅计算可见工作表,即用户在工作表选择栏中看到的内容。带后缀的方法(如 setActiveSheetWithPOIIndex() 和 deleteSheetWithPOIIndex())计算工作簿的所有工作表,包括隐藏的。activeSheetIndex 和 activeSheetWithPOIIndex 属性也是同样的工作原理。

锁定行列

锁定的行和列在其他工作表滚动时保持不动,能在长表格中保持标题行可见:

spreadsheet.createFreezePane(1, 0); // the first row, no columns
参数依次是锁定的行数和列数。POI 的 Sheet.createFreezePane() 方法参数顺序相反。

行列分组

如果加载的文档使用了分组,则在应用程序中保留。分组会以标题旁的大纲控件显示,用户在浏览器中可以像在 Excel 中一样折叠和展开。

下面的示例加载一个将国家按地区分组的文档:

@Subscribe
public void onInit(final InitEvent event) {
    try (InputStream inputStream =
                 resources.getResourceAsStream("com/company/vaadinspreadsheetex1/countries.xlsx")) {
        spreadsheet.read(inputStream);
    } catch (IOException e) {
        throw new RuntimeException("Unable to read the countries document", e);
    }
}

分组也可以通过代码创建。需要使用 POI API(组件没有自己的分组 API),然后重新加载组件。重新加载才能显示大纲控件:

@Subscribe("groupBtn")
public void onGroupBtnClick(final ClickEvent<JmixButton> event) {
    Sheet sheet = spreadsheet.getActiveSheet();
    sheet.groupColumn(1, 2); // columns B and C

    spreadsheet.reload();
}
分组仅适用于 XLSX 文档。

报表模式

reportStyle 属性隐藏组件的两个工具栏:工作表上方的函数栏和下方的工作表选择栏。

<sprd:spreadsheet id="spreadsheet"
                  width="100%"
                  reportStyle="true"/>

网格线以及行和列标题不属于报表模式。它们属于工作表,从源文档中加载。每次加载或激活另一个工作表时,组件都会再次加载这些信息。加载文档后调用 setGridlinesVisible(false) 和 setRowColHeadingsVisible(false) 将其关闭,如果不希望用户更改工作表,还可以保护工作表:

spreadsheet.setGridlinesVisible(false);
spreadsheet.setRowColHeadingsVisible(false);
spreadsheet.setActiveSheetProtected("");

电子表格中的图表

spreadsheet 组件可以渲染 Excel 工作簿中已包含的图表。图表默认显示。设置 chartsEnabled 为 false 可隐藏。组件可以反映图表数据点的变化,但不支持更改图表配置、源单元格范围或位置。需要在原始 Excel 文件中进行此类更改。

更多信息,请参考 Vaadin 文档中的 电子表格中的图表。

XML 属性

下面是 spreadsheet 的特殊属性:

名称 描述 默认值

activeSheetIndex

最初显示的工作表索引,仅计算可见工作表。

—

activeSheetWithPOIIndex

最初显示的工作表索引,计算底层 POI 工作簿的所有工作表,包括隐藏的。

—

chartsEnabled

如果为 true,则渲染加载的工作簿中包含的图表。

true

colBufferSize

在可见区域左右渲染的水平缓冲区大小,单位为像素。较大的缓冲区使滚动更平滑,但会增加负载。

200

defaultColumnCount

新建工作表的列数。

52

defaultColumnWidth

列的默认宽度,单位为像素。

—

defaultPercentageFormat

默认的百分比值格式。

0.00%

defaultRowCount

新建工作表的行数。

200

defaultRowHeight

行的默认高度,单位为磅。

—

functionBarVisible

如果为 true,则在工作表上方显示公式栏。

true

invalidFormulaErrorMessage

用户输入无法解析的公式时显示的消息。支持消息键值。

—

maxColumns

活跃工作表中渲染的列数。

—

maxRows

活跃工作表中渲染的行数。

—

minimumRowHeightForComponents

包含嵌入组件的行的最小高度(单位为磅),需要保持组件可见。请参见 在单元格添加组件。

30

reportStyle

如果为 true,则隐藏函数栏和工作表选择栏。请参见 报表模式。

false

rowBufferSize

在可见区域上下渲染的垂直缓冲区大小,单位为像素。

200

selection

初始选中的单元格或范围,使用 A1 表示法,例如 A1 或 B2:D8。请参见 选择。

—

sheetSelectionBarVisible

如果为 true,则在组件底部显示工作表标签页。

true

statusLabelValue

组件状态区域中显示的文本。支持消息键值。

—

theme

电子表格的主题。支持:LUMO、VALO。LUMO 使组件与应用程序其他样式一致;VALO 是经典的电子表格外观。

VALO

下面是 spreadsheet 支持的其他通用属性:

事件和处理器

下面是 spreadsheet 的特殊事件和处理器:

在 Jmix Studio 生成处理器桩代码时,可以使用 Jmix UI 组件面板的 Handlers 标签页或者视图类顶部面板的 Generate Handler 添加,也可以通过 Code → Generate 菜单(Alt+Insert / Cmd+N)生成。

名称 描述

CellValueChangeEvent

当用户更改了一个或多个单元格的值时发送。

cellValueHandler

当用户通过默认文本编辑器更新单元格时调用 Spreadsheet.CellValueHandler。可以自定义输入值如何应用于单元格。返回 true 继续按组件的默认值解析,如果处理器有自定义逻辑处理该值则返回 false。

cellDeletionHandler

Spreadsheet.CellDeletionHandler,决定用户是否可以清除单元格的内容。主要用来保护计算单元格或引用单元格不被清空。

FormulaValueChangeEvent

当用户更改了一个或多个单元格中的公式时发送。

hyperlinkCellClickHandler

Spreadsheet.HyperlinkCellClickHandler,定义用户点击包含超链接的单元格时如何处理。可以用来导航到应用视图而不是打开外部 URL。

ProtectedEditEvent

当用户尝试编辑受保护工作表上的锁定单元格时发送。

RowHeaderDoubleClickEvent

当用户双击行标题时发送。事件的 getRowIndex() 方法提供行的索引。

SelectionChangeEvent

当选中的单元格或范围发生变化时发送。请参见 选择。

SheetChangeEvent

当用户切换到另一个工作表时发送。

spreadsheetComponentFactory

SpreadsheetComponentFactory,提供单元格内渲染的组件,用于显示或编辑。请参见 在单元格添加组件。

spreadsheetHandler

SpreadsheetHandlerImpl,处理组件的客户端-服务端协议。仅当无法通过任何其他扩展点实现所需行为时才使用。

下面是 spreadsheet 支持的其他通用事件:

参考

To fill the sheet, create cells from the items of a collection container:

@ViewComponent
private CollectionContainer<User> usersDc;

@Subscribe
public void onBeforeShow(final BeforeShowEvent event) {
    spreadsheet.createCell(0, 0, "Username");
    spreadsheet.createCell(0, 1, "First name");
    spreadsheet.createCell(0, 2, "Last name");

    List<User> users = usersDc.getItems();
    for (int i = 0; i < users.size(); i++) {
        User user = users.get(i);
        int row = i + 1;
        spreadsheet.createCell(row, 0, user.getUsername());
        spreadsheet.createCell(row, 1, user.getFirstName());
        spreadsheet.createCell(row, 2, user.getLastName());
    }

    spreadsheet.createFreezePane(1, 0);
}