模板

添加模板

可以在报表详情视图添加报表模板。在报表列表视图新建或编辑报表时,切换至 Templates(模板) 标签页:

template list

一个报表可以有多个模板,但是必须在 Report details(报表详情) 标签页选择一个模板作为默认模板。

下面是添加模板的表单:

report template detail view
  • Template code(模板编码) - 模板的标识符。

  • Template file(模板文件) - 从文件系统加载并与报表结构描述一起保存到数据库中。

  • Output type(输出类型) - 报表输出类型。该配置应该根据 输出格式兼容性 中描述的规则与模板文件类型相一致。

  • Output name pattern(输出名称格式) - 可选的文件名格式,将用于生成的报表文件名。可以是常量字符串,也可以包含报表参数变量,例如 ${header.authorName}.xlsx。可以创建具有若干参数和字符串拼接的更复杂的格式,格式需要由报表结构中任意带区的脚本创建,例如,${Root.title}.xlsx,其中 title 是在脚本中定义:

    [['title' : ('Report for '+params['author'].firstName+' '+params['author'].lastName)]]
  • Is custom(是否自定义) - 表示格式化逻辑是自定义的,不是系统提供的格式化器。当选中该选项时,你还需要定义两个附件的字段:

    • Defined by(定义方式) - 自定义模板的定义方式:class、脚本或 URL

    • Custom definition(自定义内容) - Java 类的全限定名称、Groovy 脚本内容或者用于创建模板的 URL。如果选择 Script(脚本),则在 Groovy 脚本中可以使用下列参数:

      • params - 参数映射,通过别名 params 访问。

      • rootBand - BandData 类型的对象,提供对带区数据集的访问。

      • applicationContext - ApplicationContext 类型的对象,提供对托管 bean 的访问。

  • Is alterable output(输出格式可变) - 支持用户在运行时通过对话框选择输出类型。

    如果此标志启用,则在运行报表时将显示输出类型选择对话框。如果报表包含多个模板,还会显示模板选择的下拉列表。

模板类型

XLS(X) 模板

可以使用 Microsoft OfficeLibreOffice 创建 XLS(X) 模板。

每个 报表带区 必须在模板中具有相应的区域,该区域名称与带区相同。例如,报表有两个区 - HeaderData。这表示模板也应具有 HeaderData 命名区域。如需创建命名区域,选择相应的单元格区域,然后在应用程序左上角的字段中输入名称。要编辑已有的命名区域,请在 Microsoft Office 中使用 FormulasName Manager 菜单命令,在 LibreOffice 中使用 InsertNamesManage 命令。反之亦然,Excel 表格中想展示在报表中的每个区域也需要有报表结构中对应的带区(至少是要有一个空带区)。

报表带区以 带区 中指定的顺序输出。

报表带区可以是水平或垂直的。如果报表区是水平的,相应的命名区域将向下扩展;如果是垂直的,将向右扩展。水平报表带区可以用树状结构组织并包含子报表带区(嵌套或子报表带区)。因此,对于子报表带区,需要直接在与父报带表区对应的区域下创建命名区域。

XLSX 格式化器使用以下算法渲染子报表带区:

  • 渲染父报表带区的第一行 →

  • 渲染第一行的所有子行 →

  • 渲染父报表区的下一行。

带区数据集字段以 ${field_name} 格式放置在模板中,其中 field_name 是相应的报表带区字段名称。示例:

report template xls

可以在报表模板中使用变量。变量需要以 ${<BandName>.<variableName>} 的格式插入到 XLSX 模板的工作表名称或页眉/页脚中。

单元格中可能包含格式以及多个变量字段。如需输出图像或公式,需要将它们完全放入与报表带区链接的相应命名区域。

公式可以引用相同报表带区或另一报表带区的单元格。如需由格式化程序处理,公式应该使用报表区中的单元格范围,或直接使用单元格坐标,例如,(A1*B1)($B:$B)

如需将数据作为 Excel 图表处理,在报表结构中创建一个空报表区,并在模板中创建一个具有相同名称的命名区域。然后在此命名区域内创建一个图表,并使用图表右键菜单中的 Select data(选择数据) 按钮引用关联的报表数据区域。如果图表数据位于连续的单元格范围内,请选择该范围内的任意单元格。图表将包含该范围内的所有数据。如果数据不在连续范围内,请选择不相邻的单元格或范围。

将 XLSX 转换为 PDF 和 CSV

XLSX 报表可以自动转换为 CSV 和 PDF 格式。转换成 PDF 需要安装 LibreOffice

CSV 模板

可以使用 Microsoft OfficeLibreOffice 创建 CSV 模板。

CSV 模板中的报表带区只能是水平方向的,相应的命名区域将向下延展。此外,报表带区应属于第一级数据,即 Root 区的第一级子区。其他方面,应使用与 XLS(X) 模板 相同的规则。

csv template

内联编辑器

CSV 模板支持内联编辑。可以直接在 Report template(报表模板) 窗口中编辑模板,无需重新上传模板文件即可查看更改。

csv report editor

DOC(X) 模板

可以使用 Microsoft OfficeLibreOffice 创建 DOC(X) 模板。

DOC(X) 类型的模板可以包括文档文本或者表格。文档文本可以输出任意区的第一行数据。而在表格中,可以输出任意数量的报表带区行。

要在文档文本中使用字段变量,可以用 ${band_name.field_name} 格式的字符串,其中 band_name 是带区名称,field_name 是带区字段的名称。

如需将数据输出到表格中,需要将表格与一个报表带区关联。通过在表格的第一个单元格中指定 ##band=band_name 实现,其中 band_name 是带区名。表格字段以 ${field_name} 格式获取,其中 field_name 是与表格关联的带区的字段名称。与文档文本字段相同,也可以使用带区名称前缀来访问其他带区的字段。并支持在一个单元格中输出多个字段。

DOC(X) 中的水平报表带区不能包含子区。如果需要子区,请使用 XLS(X) 格式。

模板中表格必须包含一行或两行。如果表格有两行,则相应的带区字段必须在第二行。第一行应包含带有相应报表区名称的标记,如果需要,还可以包含静态文本或其他报表带区字段。

下面是一个模板示例,输出一个由两个带区(BookAuthors)组成的报表。第一个带区(表格上方)输出书名和分类,第二个带区(表格)输出本书的作者列表。

report template doc

DOC(X) 模板不支持单元格数据格式化。要避免由于用户的语言环境而导致的数字或日期格式问题,例如不必要的数字分隔符,请尝试将数据转换为字符串。

例如,

select e.year as "year"

转换为

select cast(e.year as varchar(4)) as "year"

HTML 模板

HTML 模板在 .html 文件(无 BOMUTF-8 编码)中定义。可以使用 Flying Saucer 库的 HTML/CSS 功能;其主要功能指南请参考 Github 网页

如需控制页面尺寸、页眉和页脚,请使用特殊的 CSS 规则和属性。

有两种方法可以在模板中放入数据:

  • 使用 FreeMarker 标签。

  • 使用 Groovy 模板引擎。

默认情况下,报表向导会生成带有 FreeMarker 标签的 HTML 模板。

在报表模板对话框中使用 Template type(模板类型) 单选按钮切换这两种方法。

HTML 模板支持内联编辑。可以直接在 Report template(报表模板) 窗口中编辑模板,无需重新上传模板文件即可查看更改。

html template editor
Groovy 模板引擎

可以将 HTML 报表模板作为 Groovy 模板进行预处理,通过 GStringTemplateEngine 来处理模板。

模板引擎使用 JSP 格式的 <% %> 脚本和 <%= %> 表达式语法或者 GString 格式的表达式。out 变量用来绑定模板将的 writer。因此,如果定义没有错误,模板可以使用任何 Groovy 代码。GStringTemplateEngine 能访问:

  • 外部参数:BandName.fields.ParamName

  • 区域:BandName.bands.ChildBandName

  • 字段:BandName.fields.FieldName

方便起见,也可以使用变量,示例:

<% def headerRow = Root.bands.HeaderRow %>
<p>Date: ${headerRow.fields.reportDate}</p>

下面示例模板输出单一用户的报表:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru">
<head>
    <title> Report User </title>
    <style type="text/css">
        body {font-family: 'Charis SIL', sans-serif;} tbody tr {height:20px; min-height:20px}
    </style>
</head>
<body>
    <% def user = Root.bands.User %>
    <p>Login: ${user.fields.login.first()}</p>
    <p>Active: ${user.fields.active.first()}</p>
</body>
</html>

可以在 Groovy 报表示例 部分找到使用 Groovy 模板的示例。

FreeMarker

FreeMarker 文档可以参阅 freemarker.apache.org

FreeMarker 文档模型的结构如下:

Band {
      bands [ bandName : [ band, .. ], .. ]
      fields [ fieldName : fieldValue, .. ]
}

例如,应该使用以下表达式访问 band 带区中索引为 0 的第一行数据的 name 字段:

Root.bands.band[0].fields.name

为方便起见,可以使用变量,示例:

<#assign headerRow = Root.bands.Header[0]>
<p>Date: ${headerRow.fields.reportDate}</p>

还可以使用 getMessage() 方法获取本地化消息值:

getMessage() 可以接收一个或两个参数。如果只传一个参数,则获取枚举的本地化值。如果传两个参数,则将两个参数分别作为消息的 group 和 key,用来从消息包中获取 本地化消息值

+

${getMessage(order.status)}

下面是一个模板示例,输出一个由两个带区(BookAuthors)组成的报表。第一个区输出书名和分类,第二个区输出本书的作者列表。

<!doctype html>
<html>
<head></head>
<body>
    <#assign book = Root.bands.Book[0] />
    <#assign authors = Root.bands.Authors />
    <p>Name: ${book.fields.name}</p>
    <p>Genre: ${book.fields.literatureType.name}</p>
    <table border="1" cellpadding="5" cellspacing="0" width="200">
        <thead>
            <tr>
                <td>First name</td>
                <td>Last name</td>
            </tr>
        </thead>
    <tbody>
        <#list authors as author>
        <tr>
            <td>${author.fields.firstName}</td>
            <td>${author.fields.lastName}</td>
        </tr>
        </#list>
    </tbody>
</table>
</body>
</html>

下面是一个更复杂的例子。假设我们有以下报表带区结构:

Root {
    HeaderBand {
        query = return [[ "name" : "Column1" ],[ "name" : "Column2" ]]
    }
    Band1 {
        query = return [
                ["field1" : "Value 11", "field2" : "Value 12"],
                ["field1" : "Value 21" , "field2" : "Value 22"]
        ]
    }
    Band2 {
        query = return [[ "header" : "Header1" ], [ "header" : "Header2" ]]
        SubBand1 {
            query = return [["header" : 'SubHeader1'] , [ "header" : 'SubHeader2' ]]
        }
    }
}
  • 插入字段:

<!doctype html>
<html>
    <head>
        <title> Simple template </title>
    </head>
    <body>
    <#assign Tree1 = Root.bands.Band2>
        <h1> Header </h1>
        <p>
            ${Tree1[1].bands.SubBand1[0].fields.header}
        </p>
    </body>
</html>
  • 插入列表:

<!doctype html>
<html>
    <head>
        <title> List </title>
    </head>
    <body>
    <#assign Table1Header = Root.bands.HeaderBand>

        <#if Table1Header?has_content>
            <ol>
                <#list Table1Header as header>
                    <li> ${header.fields.name} </li>
                </#list>
            </ol>
        </#if>
    </body>
</html>
  • 插入表格:

<!doctype html>
<html>
<head>
    <title> Table </title>
</head>
<body>
<#assign Table1Header = Root.bands.HeaderBand>
    <#assign Table1 = Root.bands.Band1>
        <table border="1" cellpadding="5" cellspacing="0" width="200">
            <thead>
            <tr>
                <#list Table1Header as header>
                    <td> ${header.fields.name} </td>
                </#list>
            </tr>
            </thead>
            <tbody>
            <#list Table1 as row>
                <tr>
                    <td>
                        ${row.fields.field1}
                    </td>
                    <td>
                        ${row.fields.field2}
                    </td>
                </tr>
            </#list>
            </tbody>
        </table>
</body>
</html>
  • 插入多级列表:

<!doctype html>
<html>
<head>
    <title> Multi-level list </title>
</head>
<body>
<#assign Tree1 = Root.bands.Band2>
    <ul>
        <#list Tree1 as item>
            <li>
                <h2> ${item.fields.header} </h2>
                <#if item.bands.SubBand1?has_content>
                    <ul>
                        <#list item.bands.SubBand1 as subitem>
                            <li>
                                <h3> ${subitem.fields.header} </h3>
                            </li>
                        </#list>
                    </ul>
                </#if>
            </li>
        </#list>
    </ul>
</body>
</html>
嵌入图片

目前,Jmix 报表组件在 HTML 报表中不提供类似于 DOCX/XLSX 可以插入图片的功能。但是,图片仍然可以使用 img 标签并在 src 属性中指定图片链接的方式嵌入。使用下面的任一种方法都能为 HTML 报表添加图片。

  • 通过 URL

    图片可以由应用程序静态托管。例如,build\resources\static\images\ 中的图片可以这样引入:

    <img src="http://localhost:8080/images/SomeImage.jpg"
         height="68"
         width="199"
         border="0"
         align="right"/>
  • 通过 Bitmap

    图片在 src 属性中以字节数组形式添加。此方法可以使用实体的 byte[] 属性变量。甚至可以将字节数组直接添加到模板中,虽然不建议使用此方法:

    <img alt="SomePicture.png"
         src="data:image/png;base64,iVBORw0K ..... AcEP9PwxD0hNKK1FCAAAAAElFTkSuQmCC"/>
  • 通过自定义的前缀:

    • 可以使用 resource:// 前缀引用 conf 文件夹或 classpath 中的图片文件。仅可用在输出为 PDF 的 HTML 模板中。例如,abc.jpgresource\com\company\app\images 文件夹,则可以这样嵌入:

      <img src="resource://com/company/app/images/abc.jpg"
          height="68"
          width="199"
          border="0"
          align="right"/>
    • fs:// 前缀支持使用 FileRef。示例:

      <img src="fs://fede432a-4f5d-3bab-71a0-b98133759b0f"
          height="68"
          width="199"
          border="0"
          align="right"/>
HTML 转换为 PDF

具有 HTML 格式模板和 PDF 输出的报表有时候会显示错误的字体。要解决此问题,将带有所需 .ttf 字体的 jmix/fonts 子目录添加到 Jmix 配置目录(默认为 ${user.dir}/.jmix/conf/)。此外,可以通过在 jmix.reports.pdfFontsDirectory 应用程序属性中指定路径来使用已有的操作系统字体。

要解决 Ubuntu 服务器上的字体问题,应该执行以下操作:

  • 安装 ttf-mscorefonts-installer 包:

    $ sudo apt-get install ttf-mscorefonts-installer
  • 设置 jmix.reports.pdfFontsDirectory 应用程序属性:

    jmix.reports.pdfFontsDirectory = /usr/share/fonts/truetype/msttcorefonts
  • 在 HTML 模板中明确指定字体,例如:

    <html>
    <head>
        <style type="text/css">
            * {
                font-family: Times New Roman;
            }
        </style>

另外要提到的是解析特殊字符。为避免将 HTML 转换为 PDF 时出错,建议将字段封装在 HTML 模板文件的 <![CDATA[ ]]> 结构中:

<tr>
	<td> <![CDATA[${(row.fields('book_name'))!?string!}]]> </td>
	<td> <![CDATA[${(row.fields('author'))!?string!}]]> </td>
</tr>

JasperReports 模板

JasperReports 格式化器支持使用 JasperReports 模板输出 Jmix 报表提取的信息。模板将由报表引擎处理,并支持几种输出类型,请参阅 输出格式兼容性矩阵

可以使用 JasperReports 工具(例如,Jaspersoft Studio)或在文本编辑器中创建 JRXML 模板。报表数据结构中定义的每个 报表带区 必须在模板中有相应的 band 元素,band 元素(在 JasperReports 术语中也称为带区)放置在标准 JasperReports 报表区内:titlepageHeadercolumnHeaderdetail 等。

报表引擎将所有报表带区数据放在一个数据源中:JRBandDataDataSource,以树形结构组织数据 - Root 带区为根带区,并将 CubaJRFunction 实例作为主数据源传递给模板,可以通过参数 REPORTING 使用。在报表模板中可以不声明此参数,未声明的情况下会自动添加,但如果要在 JasperReports IDE 中编译模板,则需要显式声明此参数。

REPORTING 参数提供两个功能:

  • dataset - 从主数据源获取可用的子数据源,例如,在表格或子报表中作为子数据集。此方法在根带区的子节点中搜索具有指定名称的报表带区,并使用搜索到的报表带区数据作为新的根创建新数据源。示例:

    <subDataset name="Product">
    	<field name="name" class="java.lang.String"/>
    	<field name="price" class="java.lang.Long"/>
    </subDataset>
    ...
    <dataSourceExpression><![CDATA[$P{REPORTING}.dataset("Product")]]></dataSourceExpression>
  • bitmap - 将给定的字节数组转换为 ByteArrayInputStream,可用于将图片嵌入到报表中。示例:

<field name="Main.image" class="java.lang.Object"/> //image from DB as byte array
...
<imageExpression><![CDATA[$P{REPORTING}.bitmap($F{Main.image})]]></imageExpression>

每个 报表带区 只能在模板中使用一次,因此如果需要在一个报表中以不同的形式表示相同的数据(例如,作为表格和图表),需要创建与模板中 band 元素一样多的报表带区。不支持嵌套报表带区,所有带区都应该是 Root 区的直接子节点。

可以使用 $F{<field name>} 格式从数据源获取数据。示例:

<textField>
    <textFieldExpression><![CDATA[$F{library_department_name}]]></textFieldExpression>
</textField>

可以在 Jasper 示例报表 章节参考报表示例。

类定义模板

当很难或无法使用 SQL、JPQL 或 Groovy 选择数据时,可以使用类定义模板。例如,当报表是需要组合几个报表的结果时,就可以使用类定义模板。

创建一个类并实现 io.jmix.reports.yarg.formatters.CustomReport 接口。定义 createReport() 方法,该方法返回一个字节数组并接收以下输入参数:

  • report - io.jmix.reports.yarg.structure.Report 类型的报表描述。

  • rootBand - io.jmix.reports.yarg.structure.BandData 类型的根带区数据。

  • params - 外部报表参数映射。

下面是一个简单的类定义模板的示例。会创建一个 HTML 文档,显示从参数中获取的书籍的名称:

package com.company.library.reports;

import com.company.library.entity.Book;
import io.jmix.reports.yarg.formatters.CustomReport;
import io.jmix.reports.yarg.structure.BandData;
import io.jmix.reports.yarg.structure.Report;

import java.util.Map;

public class BookReport implements CustomReport {
    @Override
    public byte[] createReport(Report report, BandData rootBand, Map<String, Object> params) {
        Book book = (Book) params.get("book");
        String html = "<html><body>";
        html += "<p>Name: " + book.getName() + "</p>";
        html += "</body></html>";
        return html.getBytes();
    }
}

在模板详情视图中,选中 Is custom(是否自定义) 复选框,在 Defined by(定义方式) 字段中选择 Class(类),并设置自定义内容为 Java 类的完全限定名:

class defined template

表格

当在向导中选择表格输出时,系统会自动创建一个模板。运行报表时,数据会在应用程序中的一个特殊界面视图。

也可以手动为一个已存在的报表创建表格模板,在报表模板详情视图中选择 Table(表格) 作为输出类型。

report template table

Band(数据区) 列,添加需要在结果表格中显示的带区名称。对于每个带区,设置一个 key-value 映射,key 是数据集属性,value 是本地化名称。

报表扩展组件会获取报表带区数据,并从报表带区树的第一级为每个带区绘制一个可排序表格。

表格列显示 SQL 和 JPQL 数据集的所有属性。如果使用实体/实体列表数据集,则表格仅显示选择的属性。

表格结果可通过应用程序的 Reports(报表) → Show report table(展示报表表格) 视图查看。使用 Excel(生成 Excel) 按钮可以将显示的表格导出为 Excel 文件。

show report table

执行外部报表

报表扩展组件支持运行外部服务的报表,例如,Microsoft Reporting Services,并下载输出文件。如需定义外部报表,按照下列步骤:

  • 在报表模板对话框勾选 Is custom(是否自定义) 复选框。

  • Defined by(定义方式) 选择 URL

  • Custom definition(自定义内容) 字段指定外部报表的 URL。报表参数以 ${param Alias} 格式传递给 URL。

  • 配置 jmix.reports.curlPath 应用程序属性,设置为 curl 命令行工具的路径。

输出格式兼容性矩阵

下面表格中展示输入模板格式和输出格式的关系,例如,输入 XLSX 模板可以输出 XLSX、CSV、PDF 和 HTML 格式。

Template / Output XLSX XLS CSV DOCX DOC PDF HTML

XLSX

+

+

+ 1

+ 1

XLS

+

+ 1

CSV

+

DOCX

+

+ 2

+ 2

DOC

+

+ 1

HTML

+

+

JRXML

+

+

+

+

+

+

+

1 - 必须 安装 LibreOffice。

2 - 根据 jmix.reports.useOfficeForDocumentConversion 应用程序属性的配置,输出可以选择是否使用 LibreOffice。如果不使用 LibreOffice,则需要自己提供所需的字体,参阅 HTML 转换 PDF