JasperReport 用户手册的翻译
1
简介
JasperReport
是一个强大、灵活的报表生成工具,能够展示丰富的页面内容,并将之转换成
PDF
,
HTML
,或者
XML
格式。最重要的是它是开源的,这给我们带来很大方便,但是文档却要钱,让人不爽。不过人总要生存,再说,做这么一个好东西,用户总不能一点代价也不付(虽然对于中国普通程序原来说太贵了点)。它还有一个相关的开源工程
—IReport
,这是一个图形化的辅助工具,因为
JasperReport
仅提供了可使用的类库而未提供更好的开发工具,
IReport
的出现解决了这一难题。它们配合使用将会更大程度的提高效率。
该库完全由
Java
写成,可以用于在各种
Java
应用程序,包括
J2EE
,
Web
应用程序中生成动态内容。它的主要目的是辅助生成面向页面的(
page oriented
),准备付诸打印的文档。
JasperReport
借由定义于
XML
文档中的
report design
进行数据组织。这些数据可能来自不同的数据源,包括关系型数据库,
collections
,
java
对象数组。通过实现简单的接口,用户可以将
report library
插入到订制好的数据源中,在以后我们将提到有关内容。
其实这是一份
JasperReport Ultimate Guide
的简单翻译以及我的理解和例子。在最后,我将描述一个我正在做的工程,将其中用到的相关信息贡献出来。我这么做是因为当我在学这个类库的时候苦于很少有相关的中文文档,诱惑语焉不详,希望其他人不再受苦。这个文档将分几次贴出来,与原文档的章节相对应。这份文档的
Word
形式将在全部完成之后放在我的公开邮箱中与各位共享。我的
EMail
是
jxuedi@gmail.com
有什么意见或想法请与我联系。
闲言少叙,进入正题。
2 API
概览
上图为一个生成报表并打印(导出)的全过程。我将会把涉及到的重要的类进行一一说明。
Class net.sf.jasper.engine.design.JasperDesign
这是一个未经加工的报表实例,供
JasperReport Library
使用。这个类可以在
JasperReport
类库内置的
XML
解析器对
XML report design
进行解析处理之后得到。如果你的程序不想对直接
XML
文件进行操作,在例子
noxmldesign
中有不使用
XML
设计文件而动态生成这个类的方法。我们稍稍看看这个例子:
import
略
public class NoXmlDesignApp
{
private static JasperDesign getJasperDesign() throws JRException
{
//JasperDesign
定义
JasperDesign
的头信息
JasperDesign jasperDesign = new JasperDesign();
jasperDesign.setName("NoXmlDesignReport");
.
剩余略
//Fonts
定义使用到的字体
JRDesignStyle normalStyle = new JRDesignStyle();
normalStyle.setName("Arial_Normal");
//Parameters
定义
Parameters
的内容
—
这个内容以后会提到
JRDesignParameter parameter = new JRDesignParameter();
parameter.setName("ReportTitle");
parameter.setValueClass(java.lang.String.class);
jasperDesign.addParameter(parameter);
parameter = new JRDesignParameter();
parameter.setName("OrderByClause");
parameter.setValueClass(java.lang.String.class);
jasperDesign.addParameter(parameter);
//Query
定义查询
JRDesignQuery query = new JRDesignQuery();
query.setText("SELECT * FROM Address $P!{OrderByClause}");
jasperDesign.setQuery(query);
//Fields
JRDesignField field = new JRDesignField();
field.setName("Id");
field.setValueClass(java.lang.Integer.class);
jasperDesign.addField(field);
//Variables
定义变量
JRDesignVariable variable = new JRDesignVariable();
variable.setName("CityNumber");
variable.setValueClass(java.lang.Integer.class);
variable.setResetType(JRVariable.RESET_TYPE_GROUP);
//Groups
定义组
group.setMinHeightToStartNewPage(60);
expression = new JRDesignExpression();
//
余下定义一个文档的其他内容,这里省略
return jasperDesign;
}
从
getJasperDesign()
方法我们可以看出,这个应用程序并没有从
XML
文件里面将
report design
提取出来在生成
JasperDesign
类,而是直接利用
JasperDesign
提供的函数生成了一个报表设计。这样做的原因是基于灵活性的考虑,你可以在程序中随时动态生成报表,而不需要去从硬盘或网络中读取
XML
设计文件。但通常我不这么做,因为比较麻烦,而且要对
JasperReport
的每个元素都非常熟悉才行。
Class net.sf.jasper.engine.JasperReport
这个类的实例包含了一个经过编译的
report design
对象。生成它的时机是对报表编译之后,但尚未对其填入数据的时候。编译过程中,
JasperReport
需要生成一个临时的类文件,用以保存
report expression
,如变量表达式,文本,图像表达式,组表达式等等。这个临时的
Java Source File
是被动态编译的,编译器使用的是
JDK
中用来执行应用程序的编译器类(
compiler class
)。如果
tools.jar
不在
classpath
中,编译过程将采用
javac.exe
来进行后台编译。编译后所得的字节码保存在
JasperReport
类中,用来在执行期装填数据(
filling the report with data
)和给表达式赋值(
evaluate various report expression
)。
Class net.sf.jasper.engine.JasperCompileManager
这是一个上面提到的与编译有关的类。利用它提供的一些方法,你将有能力编译从本地硬盘或一个
Input Stream
获得的
XML report
;还可以通过传给
JasperCompileManager
一个
JasperDesign
类,来对内存中的
report design
进行编译
—
功能很强大。
Class net.sf.jasper.engine.JasperPrint
当一个报表已经装填好数据之后,这个文档就以
JasperPrint
类的实例出现。这个类可以直接用
JasperReport
内置的
viewer
进行查看,也可以序列化到硬盘以备后用,或者发送到网上去。这个类的实例是报表装填过程后的产物,它可以被
JasperReport
类库中的导出方法导出成各种流行的格式
—PDF
,
HTML
,
XML
等等。
Interface net.sf.jasper.engine.JRDataSource
这个类与报表的数据源有关。只要能够恰当的实现他的一些接口,用户就可以在报表中使用各种数据源,在报表装填的时候由报表引擎负责对数据进行解释和获取。当报表装填的时候,报表引擎都会在后台生成或提供一个该接口的实例。
Class net.sf.jasper.engine.JRResultSetDataSource
这是一个
JRDataSource
的缺省实现,因为很多报表数据都来源于关系数据库,所以
JasperReport
缺省包含了这个外覆(
wrap
)了
java.sql.ResultSet
对象的实现。
这个类可以用来包裹(
wrap
)用以对报表进行装填的、已经载入的结果集,也可以被报表引擎用来包裹通过
JDBC
执行完查询后所得的数据
----
非常有用。
Class net.sf.jasper.engine.data.JRTableModelDataSource
顾名思义,这个类用于包裹
java.swing.table.TableModel
类中的数据,它也是实现了
JRDataSource
接口,用于在
Java Swing
程序中使用已经被载入到
table
中的数据来生成报表。
Class net.sf.jasper.engine.JREmptyDataSource
这是
JRDataSouce
接口的最简单实现,这个类用在不需要显示数据源数据而从参数中获取数据的报表和仅需要知道数据源中的实际行数(
number of virtual rows
)的报表中。
JasperReport
自带的例子:
fonts
,
images
,
shapes
和
unicode
中使用这个类对报表进行装填,来模拟没有任何
record
的数据源,这时所有的
field
都为
null
。例如:
JasperRunManager.runReportToPdfFile(fileName, null, new JREmptyDataSource());
Class net.sf.jasper.engine.JasperFillManager
这个类用来实现报表的数据装填。这个类提供了很多方法来接受各种类型的
report design--
可以是一个对象,一个文件,或一个输入流。它的输出结果也是多样的:
file
,
Object
,
output Stream
。
report
的装填引擎需要接收一个可以从中获取数据和
value
的数据源作为报表参数。参数值(
Parameters value
)通常使用
Java.util.Map
来提供,里面包含的
KEY
是报表的参数名。
数据源可以通过两种方式提供,这取决于你的解决方案:
通常情况下,用户应该提供一个
JRDataSource
对象,例如我前面提到的那些。
但是大多数的报表都是采用关系数据库中的值来装填数据,所以
JasperReport
拥有一个内置的缺省行为
—
让用户在报表设计的时候提供一个
SQL
查询。在运行期,这个查询将被执行以用来从数据库中获取要装填的数据。在这种情况下,
JasperReport
仅需要一个
java.sql.Connection
对象来取代通常的数据对象。
JasperReport
需要这个连接对象来连接数据库管理系统并执行查询操作。
在查询结束之后,
JasperReport
将自动生成一个
JRResultSetDataSource
,并将它返回给报表装填过程。
Class net.sf.jasper.engine.JRAbstractScriptlet
这个类同样用于报表装填期间,用户可以自己定义一些代码,并由报表引擎在装填过程中执行。这些用户代码可以处理报表数据操作,或在一些定义好的时刻执行,例如
page
,列,或组的分割处。
Class net.sf.jasper.engine.JRDefaultScriptlet
这是一个非常方便的
JRAbstractScriptlet
的子类。通常情况下你应该选择继承这个类。
Class net.sf.jasper.engine.JasperPrintManager
这个类用户提供打印方法,用户可以将整个文档或部分文档传递给它,也可以选择是否显示打印
Dialog
,这在他的
API
文档中可以找到,这里不再赘述。
Class net.sf.jasper.engine.JasperExportManager
顾名思义,这个类负责文档的导出。这个类的具体信息详见
API
文档。非常明显和清除,没什么好解释的,
Just use it
即可。
Class net.sf.jasper.engine.JasperRunManager
有时,我们仅仅需要构造一个流行的文档格式,例如
PDF
,或
HTML
,而不需要将装填过程后生成的
JasperPrint
对象保存到硬盘或其他中间媒体上。这时,可以使用这个类来直接将装填过程生成的文档导出到所需的格式。
Class net.sf.jasper.view.JRViewer
这是一个基于
Swing
的应用程序,你可以将它视为一个独立组件,用来进行打印预览。用户可以继承这个类,来构造满足自身要求的预览程序。
Class net.sf.jasper.view.JasperViewer
这个类更像是使用
JRViewer
的教学组件,它演示了一个
Swing
应用程序如何装在并显示报表。
Class net.sf.jasper.view.JasperDesignViewer
这个类用于报表的设计期间,用来预览报表模版。它仅作为一个开发工具存在于类库中。
Class net.sf.jasper.engine.util.JRLoader
装载器用于报表生成的各个主要阶段
—
编译,装填等等。用户和引擎都可以利用这个类来装载所需的序列化对象如
file
,
URLs
,
intput stream
等等。这个类最令人感兴趣的函数当属
loadOnjectFromLocation(String location)
。当用户使用这个类从指定地点装载对象的时候,该函数将首先将
location
解释为一个合法的
URL
,如果解析失败,函数将认为所提供的
location
是硬盘上的一个文件名,并将试图读取它。如果在指定地点没找到文件,它将通过
classpath
定位一个相应于该
location
的资源,所有努力失败之后,将抛出异常。
3
主要的任务和过程
这一节我们将看到对你的
XML
报表设计进行分析,编译,装填数据,预览结果和导出到其他格式的过程。
3.1 XML
解析
JasperReport
使用
SAX2.0 API
对
XML
文件进行解析。然而,这并不是必须的,用于可以在执行其自行决定使用哪一种
XML
解析器。
JasperReport
使用
org.xml.sax.helpers.XMLReaderFactory
类的
createXMLReader()
来获得解析器实例。在这种情况下,就像在
SAX2.0
文档中说的那样,在运行期,把
Java
系统属性
org.xml.sax.driver(
这是属性的
key)
的值
(value)
设定为
SAX driver
类的全限定名是必要的。用户可以通过两种方法做到这一点,我稍后将解释全部两种方法。如果你想使用不同的
SAX2.0XML
解析器,你需要指定相应的解析器类的名字。
设置系统属性的第一种方法是在你启动
Java
虚拟机的时候,在命令行使用
-D
开关:
java –Dorg.xml.sax.driver=org.apache.serces.parsers.SAXParser mySAXApp sample.xml
在
JasperReport
提供的所有例子中,都采用
ANT
构建工具来执行不同的任务。我们通过使用内置的
task
中的元素来提供这一系统属性:
第二种设置系统属性的方法是使用
java.lang.System.setProperty(String key, String value)
System.setProperty(“org.xml.sax.driver”,” org.apache.xerces.parsers.SAXParser”);
Jsp/compile.jsp
和
web-inf/class/servlets/CompileServlet.java
文件提供了这方面的例子。
注:对于第二种方法,我要说些题外话。有关于
JVM
的系统属性(我们可以通过
System.out.println
(
System.getProperty(“PropertyKey”)
来查看),可以在运行期像上面说所得那样用
System.setProperty(“propertyKey”,”propertyValue”);
来进行设置。但是一旦
JVM
已经启动之后,其内建的系统属性,如
user.dir
,就不能再被更改。奇怪的是我们仍可以用
System.setProperty()
方法对其进行设置,而在用
System.out.println(System.getProperty())
方法进行查看的时候发现,其值已经更改为我们设置的值,但事实上我们设置的值不会起任何作用。所以对于内置的属性,我们只能通过
-D
开关在
JVM
执行之前进行设置。对于
org.xml.sax.driver
,由于它不是系统内建属性,所以仍然可以在
JVM
启动之后加以设置。更详细的信息可以参考王森的〈
Java
深度历险〉。
3.2
编译报表设计
(Report Designs)
为了深成一个报表,用户需要首先生成报表的设计(
report’s design
),生成方法或采用直接编辑
XML
文件,或通过程序生成一个
net.sf.jasper.engine.design.JasperDesign
对象。本文中,我将主要采用编辑
XML
文件的方法,因为这种方法在目前是使用
JasperReport
类库的最好的方法,并且我们有机会更好的了解类库的行为。
先前提到过,
XML
报表设计是
JasperReport
用来生成报表的初级材料(
raw meterial
)。这是因为
XML
中的内容需要被编译并载入到
JasperDesign
对象中,这些对象将在报表引擎向其中填入数据之前经过编译过程。
注意:大多数时候,报表的编译被划归为开发时期的工作。你需要编译你的应用程序报表设计,就像你编译你的
Java
源文件一样。在部署的时候,你必须将编译好的报表,连同应用程序一起安装到要部署的平台上去。这是因为在大多数情况下报表设计都是静态的,很少用应用程序需要提供给用户在执行期编译的,需要动态生成的报表。
报表编译过程的主要目的是生成并装载含有所有报表表达式(
report expression
)的类的字节码。这个动态生成的类将会被用来在装填数据,并给所有报表表达式求值(
evaluate
)的时候使用。具体例子是,如果你用
IReport
生成一个报表名字叫
SimpleSheetTest
,它的
XML
设计文件名叫
SimpleSheetTest.jrxml
,同时和它在同一目录下
IReport
会自动生成一个文件名为
SimpleSheetTest.java
,里面主要是一些报表元素,如
Field
,
Parameters
,
Variables
的定义,以及一些求值表达式。当然,像上面提到的,这个文件在你直接使用
JasperReport API
的时候是看不到的,因为它是在执行期生成的一个
Class
。要想看到它的办法是:在
IDE
(
JBuilder
,
Eclipse
)中单步执行程序,在报表打印的阶段,你将能跟踪到这个类,它的名字就是
“
你的报表名
.java”
,按上面的例子就是
SimpleSheetTest.java
,这和
IReport
是一致的。当然也可以像下面说的那样,到生成这个类的临时目录里找到它。
在这个类生成过程之前,
JasperReport
引擎需要验证报表设计的一致性(
consistency
),哪怕存在一处验证检查失败都不会继续运行下面的工作。在下面的章节,我将会展示报表设计验证成功之后的状况。
对于这个包含了所有报表表达式(
report expressions
)的类的字节码,我们至少需要关心三个方面的内容:
临时工作目录(
temporary working
l
directory
)
Java
编译器的使用
l
Classpath
l
为了能够编译
Java
源文件,这个文件必须被创建并且被保存到磁盘上。
Java
编译过程的输出是一个
.class
文件,这个包含所有报表表达式的类在这个工作目录里被创建并编译,这也是为什么
JasperReport
需要访问这个临时目录的原因。当报表的编译过程结束之后,这些临时的类文件将被自动删除,而生成的字节码将保存在
net.sf.jasper.engine.JasperReport
对象中。如果需要的话,这个类可以将自己序列化(
serialized itself
)并保存到磁盘上。这就是
IReport
的做法。
缺省情况下,这个临时工作目录就是启动
JVM
时的当前目录,这却取决于
JVM
的系统属性
user.dir
。通过更改系统属性
jasper.report.compile.temp
,用户可以很容易更改这个工作目录。在
Web
环境下,特别是当你不想让含有启动
Web Server
的批处理文件的目录和报表编译过程的临时工作目录混在一起的时候,修改这个属性就可以了。
上面提到的第二个方面涉及用来编译报表表达式类的
Java
编译器。首先,报表引擎将试图使用
sun.tools.javac.Main
类来编译
Java
源文件。这个类包含在
tools.jar
中,当且仅当这个
jar
文件在
JDK
安装目录下的
bin/
目录中,或在
classpath
中时,
sun.tools.javac.Main
才能正常使用。
如果
JasperReport
不能成功装载
sun.tools.javac.Main
文件,程序将动态执行
java
编译过程,就像我们通常用命令行那样,使用
JDK
安装目录下的
bin/
目录下的
javac.exe
。这就是为什么将
JDK
安装目录
/lib/
下的
tools.jar
文件
copy
到
JasperReport
工程的
lib/
目录下是一个可选的操作(
optional operation
)。如果
tools.jar
不在
classpath
中,
JasperReport
将显示错误信息并继续上面提到的操作。
当编译
Java
源文件的时候,最重要的事情莫过于
classpath
。如果
Java
编译器不能在指定的
classpath
中找到它试图编译的所有相关类的源文件,则整个过程将失败并停止,错误信息将在控制台显示出来。同样的事情也将发生在
JasperReport
试图编译报表表达式类的时候。所以,在
runtime
为编译过程提供正确的
classpath
是非常重要的。例如,我们我们需要确认在
classpath
中,我们提供了在报表表达式中可能用到的类(
custom class
)。
在这个方面也有一个缺省的行为。如果没有为编译
report class
特殊指定
classpath
,引擎将会使用系统属性
java.class.path
的值来确定当前的
JVM classpath
。如果你指定了系统属性
jasper.reports.compile.class.path
的值,你可以用你定义的
classpath
来覆盖缺省行为。
大多数情况下,编译一个
report
只需要简单的调用
JasperReport
类库中的
JasperCompileManager.compileReport(myXmlFileName);
即可。调用之后将生成编译好的
report design
并存储在
.jasper
文件中,这个文件将会保存在和提供
XML report design
文件相同的目录中。
3.3 Report Design
预览
JasperReport
类库并没有提供高级的
GUI
工具来辅助进行设计工作。但是目前已经有至少
4
个
project
试图提供这样的工具。然而,
JasperReport
本身提供了一个很有用的可视化组件来帮助报表设计者在编译的时候预览报表设计(其实不如直接用
IReport
方便)。
net.sf.jasper.view.JasperDesigner
是一个基于
Swing
的
Java
应用程序,它可以载入并显示
XML
形式或编译后的报表设计。尽管它不是一个复杂的
GUI
应用程序,缺乏像拖拽可视化报表元素这样的高级功能,但是它仍然是一个有用的工具(
instrument
)。所有
JasperReport
工程提供的例子都利用了这个报表查看器(
report viewer
)。
如果你已经安装了
ANT
(别告诉我你不知道什么是
ANT
),想要查看一个简单的报表设计(
JasperReport
工程所带例子),你只需要到相应的文件夹下输入如下命令:
〉
ant viewDesignXML
或者
〉
ant viewDesign
如果你没安装
ANT
,要达到上面的效果就不是很容易,因为
JasperReport
本身需要一些其他辅助的
jar
包(在
JasperReport
安装目录
/lib
下),在运行的时候,你需要把这些
jar
包都包含到你的
classpath
里面,并且正确设计系统属性,如上面提到的
org.xml.sax.driver
。我可以展示一下在
windows
下的例子:
>java -classpath ./;../../../lib/commons-digester.jar;
../../../lib/commons-beanutils.jar;../../../lib/commons-collections.jar;
../../../lib/xerces.jar;../../../lib/jasperreports.jar
-Dorg.xml.sax.driver=org.apache.xerces.parsers.SAXParser
dori.jasper.view.JasperDesignViewer -XML -FFirstJasper.xml
很麻烦吧?还是赶快弄个
ANT
吧。下面是预览之后的结果(其实用
IReport
更好)
3.4
报表装填(
Filling Report
)
报表装填(
report filling
)过程是
JasperReport library
最重要的功能。它体现了这个软件最主要的目的(
main objective
),因为这一过程可以自由的操作数据集(
data set
),以便可以产生高质量的文档。有
3
种材料需要装填过程中作为输入提供给
JasperReport
:
report design
(
report
l
templet
)
参数(
parameters
)
l
数据源(
data
l
source
)
这一过程的输出通常是一个单一的最终要被查看,打印或导出到其他格式的文档。
要进行这一过程,我们需要采用
net.sf.jasper.engine.JasperFillManager
类。这个类提供了一些方法来让我们装填报表设计(
report design
),
report design
的来源可以是本地磁盘,输入流,或者直接就是一个已存在于内存中的
net.sf.jasper.engine.JasperReport
类。输出的产生是于输入类型相对应的,也就是说,如果
JasperFillManager
接到一个
report design
的文件名,装填结束后生成的
report
将会是一个放在磁盘上的文件;如果
JasperFillManager
收到的是一个输入流,则生成的
report
将会被写道一个输出流中。
有些时候,这些
JasperFillManager
提供的方法不能满足某些特定的应用的要求,例如可能有人希望他的
report design
被作为从
classpath
中得到的资源,并且输出的报表作为一个文件存放在一个指定的磁盘目录下。遇到这种情况时,开发人员需要考虑在将报表设计传递给报表装填过程之前,用
net.sf.jasper.engine.util.JRLoader
类来装载
report design
对象。这样,他们就能获得像报表名这样的
report design
属性,于是开发者就能生成最终文档的名字(
construct the name of the resulting document
),并将它存放到所需的位置上。
在现实中,有许多报表装填的情境(
scenarios
),而装填管理器仅试图覆盖其中最常被使用到的部分。然而对于想要自己定制装填过程的人来说,只要采用上面所说的方法,任何开发者都可以达到满意的结果。
报表参数通常作为
java.util.Map
的
value
提供给装填管理器,参数名为其键值(
key
)。
作为装填过程所需的第三种资源
—
数据源,有如下两种情况:
通常,引擎需要处理
net.sf.jasper.engine.JRDataSource
接口的一个实例,通过这个实例,引擎可以在装填过程中获取所需数据。
JasperFillManager
提供的方法支持所有的
JRDataSource
对象(这是一个
Interface
,上面一章提到过它的常用实现)。
然而,这个管理器还提供一些接受
java.sql.Connection
对象作为参数的方法集,来取代所需的数据源对象。这是因为在很多情况下,报表生成所需的数据都来源于某个关系型数据库中的表(
table
)。
在报表中,用户可以提供
SQL
查询语句来从数据库中取回报表数据(
report data
)。在执行期,
engine
唯一需要做的是获得
JDBC connection
对象,并使用它来连接想要连接的数据库,执行
SQL
查询并取回报表数据。在后台,引擎将使用一个特殊的
JRDataSource
对象,但是它对于调用它的程序来说是透明的。
JasperReport
工程提供了相关的例子,它们采用
HSQL
数据库服务器(在工程文件中,有一个相应的文件夹),要运行这些例子你需要首先启动该服务器,方法是:在
/demo/hsqldb
目录下输入如下命令:
>ant
或者
>ant runServer
没装
ANT
就麻烦点:
>java -classpath ./;../../lib/hsqldb.jar org.hsqldb.Server
一下代码片断显示了
query
例子是如何装填数据的:
//Preparing parameters
Map parameters = new HashMap();
parameters.put("ReportTitle", "Address Report");
parameters.put("FilterClause", "'Boston', 'Chicago', 'Oslo'");
parameters.put("OrderClause", "City");
//Invoking the filling process
JasperFillManager.fillReportToFile(fileName, parameters, getConnection());
3.5
查看报表(
Viewing Reports
)
报表填充阶段的输出通常是一个
JasperPrint
对象,如果把它保存在磁盘上,通常以一个
.jrprint
文件的形式存在。
JasperReport
拥有一个内置的查看器,用来查看用内置的
XML
导出器(
XML exporter
)获得的
XML
格式的报表文件。这个查看器就是以前提到过的
net.sf.jasper.niew.JRViewer—
一个基于
Swing
的应用程序组件,用户可以通过继承这个类来定制自己所需的查看器。
JasperReport
工程中自带的例子
webapp
中,你可以阅读
JRViewerPlus
类的代码来获取进一步内容。
注意:
JasperViewer
更像是一个教人们如何使用
JRViewer
组件的演示程序,这里要注意一点,当你调用
JasperViewer
的
viewReport()
方法来显示报表时,如果你关闭了预览
Frame
,整个应用程序将会随之结束,因为这个函数最后调用了
System.exit(0)
;你可以通过继承这个类,并重新在你的
Viewer
里注册
java.awt.event.WindowListener
来避免这一情况的发生。
3.6
打印报表
JasperReport
类库的主要目标,就是生成可打印的文档。而且多数应用程序生成的报表都是需要落实(或打印)到纸张上。我们可以用
net.sf.jasper.engine.JasperPrintManager
来打印
JasperReport
生成的文档。当然,报表也同样可以在被导出到其他格式如
PDF
,
HTML
之后再被打印。通过
JasperPrintManager
提供的方法,我们可以打印整个文档,打印单个文档或打印某一范围内的文档,可以显示打印对话框也可以不显示。下面的例子演示了不显示对话框,打印整个文档的方法:
JasperPrintManager.printReport(myReport,false);
这个例子显示了如何打印
5-11
页的文档,同时显示打印对话框:
net.sf.jasper.engine.JasperPrintManager.printPages(myReport,4,10,true);
3.7
导出报表
在一些应用程序环境下,将
JasperReport
生成的文档从其特有的格式导出到其他更为流行的格式如
PDF
,
HTML
是非常有用的。这样一来,其他人就可以在没有安装
JasperReport
的情况下查看这些报表,特别是当这些文档要通过网络发送出去的时候。
JasperReport
提供了
JasperExportManager
类来支持此项功能。这项功能将会在以后不断加入对新的格式的支持。目前,
JasperReport
主要支持导出
PDF
,
HTML
和
XML
类型的文档,下面是导出的代码片断:
JasperExportManager.exortReportToHtmlFile(myReport);
注意:想要将自己的报表导出到其他格式的用户,需要实现
JRExporter
的接口,或继承相应的
JRAbstractExporter
类。
3.8
对象的载入和保存
当使用
JasperReport
的时候,你经常会与序列化的对象,如以编译的报表设计,或已生成的报表打交道。有时,你需要手动载入从不同的
source
如
input stream
或你用类库核心功能(
lib’s core functionality
)产生的序列化类。
JasperReport
提供了两个特殊的工具类来提供上述操作的能力,这些类通常供报表引擎自己使用:
net.sf.jasper.engine.util.JRLoader
net.sf.jasper.engine.util.JRSaver
第一个类提供了一些方法让我们能够从不同类型的数据源如文件,
URL
,
input stream
和
classpath
里面获取序列化对象。最令人感兴趣的方法是
loadObjectFormLocation(String)
。它已经在上一章中介绍过了,这里不再赘述。
与上面的对象载入工具相反的部分是
JRSaver
类,它可以帮助程序员将自己的类序列化之后存放到本地磁盘或通过
Output Stream
发送到网络上去。
有时,开发人员可能想要载入已经生成好的
report
,或最终的已经被导出到
XML
格式的
JasperReport
文档,这与上面所说的直接
load
序列化对象有所不同。这时,我们需要载入的是将载入的
XML
内容进行编译,并生成
JasperPrint
对象,而并非仅仅是载入序列化对象。这时,我们可以通过
net.sf.jasper.engine.xml.JRPrintXmlLoader
类的一些静态方法,通过编译从
XML
文件中读取的内容构建出一个位于内存中的文档对象。
4
报表设计(
Report Designs
)
报表设计体现了一个模版,
JasperReport
引擎利用这个模版将同台生成的内容传递给打印机,屏幕或
Web
。存储在数据库中的数据在报表装填的过程过被组织起来,根据已有的报表设计来获得可以进行打印的,面向页面的(
page oriented
)文档。
总而言之,一个报表设计包含了所有的结构相关信息和将数据提供给报表所涉及的各个方面。这些信息涉及将在文档中显示出来的各种
text
或图像元素的位置和内容,自定义计算(
custom calculation
),数据组织,报表生成时的数据操作,等等。
报表设计通常都定义在一个拥有特殊格式的
XML
文档中,并且在被填充数据之前要经历
JasperReport
的编译过程,有关于这个
XML
文档的详细信息我们将在以后说明。然而
JasperReport
也允许用户通过
JasperReport
提供的
API
构造
in-memory
报表对象,例程
noxmldesign
就是很好的例子,但是我们通常不这么用。
4.1 DTD Reference
当使用
XML
文件进行报表设计的时候,
JasperReport
将使用内置的
DTD
文件来验证其受到的
XML
内容的有效性。如果
XML
验证通过,则说明所提供的报表设计符合
JasperReport
所需要的
XML
结构和语法规则,其引擎能够生成经过编译的
report design
。
有效的
XML
文档总是在验证时指向
JasperReport
的内部
DTD
文件。如果没有提供
DTD
文档的引用,报表的编译过程将会突然结束。这对所有人来说都是一个负担,因为
DTD
引用通常是相同的,并且这些引用可能会简单的被从以前的报表设计中
copy
过来。在一开始,你需要将这个引用从给定的例子中
copy
过来。
正如以前说的一样,报表引擎仅能识别指向其内部
DTD
文件的的引用。你不能随便从类库的源文件中将那些
DTD
文件
copy
到别的地方,再在你的报表设计文件中文件中指向你
copy
的那些
DTD
文件。如果你想那样做的话,你将需要调整类库中某些类,包括
net.sf.jasper.engine.xml.JRXmlDigester
类的某些代码。如果你遇到像引擎无法找到其内部的
DTD
文件而导致的无法载入资源的问题,请确定你已经在使用外部
DTD
文件之前排除了所有可能发生的情况。遇到这样的问题是不太可能的,因为资源载入机制会随着时间不断改进。
JasperReport
只有两种合法的
XML
报表设计的
DTD
引用,他们是:
或者
XML
报表的
root
元素是,下面是一个普通带
JasperReport
的样子:
4.2 XML
编码
当要生成不同语言的
XML
报表设计的时候,在
XMl
文件的首部的编码属性需要特别关注一下。缺省情况下,如果这个属性的值没被订制,则
XML
解析器将会使用
“UTF
-8”
作为
XML
文件的编码格式。这一点是非常重要的,因为报表设计通常包含了静态的本地化
text
。对于大多数西欧语言来说,
ISO-8859-1
编码,也就是我们常说的
LATIN1
将会很好的处理如法语中重音符号的显示问题。
在编辑
XML
文件的时候,要找到某种特殊语言的编码类型,你可以查看
XML document.FIXME
4.3
报表属性
我们上面已经看到,斯
XML
报表设计的根元素。这一节我将介绍报表设计对象的
Property
的细节以及这些属性所对应的
XML attributes
(为避免混淆,我将不提供
Property
和
Attribute
的中文而直接使用英文)。
Report Name
每一个报表都必须有一个名字。这个名字是相当重要的,因为类库需要它来生成文件,尤其是当编译,装填,导出报表的默认行为被使用的时候,这个名的作用更为重要。这个名字是以元素的
name attribute
的形式提供的,并且是强制必须填写。
Column Count
(列数)
JasperReport
允许生成每页的列数超过一列的报表,正如下面的图片,展示了拥有两列的报表:
默认情况下,报表引擎生成每页一列的报表。
Print Order
(打印顺序)
对于拥有超过一列的报表,为其提供列将被以什么顺序填充是很重要的。你可以使用的
printOrder attribute
来进行设置。有如下两种情况:
Vertical
n
Filling
:这个选项将导致列是自顶向下被填充(
printOrder=”Vertical”
)
n
Horzontal
:列将自左向右被填充(
printOrder=”Horizontal”
)
缺省设置将是
printOrder=Vertical
Page Size
(页面大小)
有两个
attribute
是用来提供要生成的文档大小的:
pageWith
,
pageHeight
。像所有其他显示元素位置和尺寸的
attribute
一样,这两个
attribute
是以像素为单位的。
JasperReport
采用
Java
默认的每英寸
72
点的设置。这意味着
pageWith=”
595”
将大约是
8.26
英寸
,这大概是
A4
纸的尺寸。
默认的纸张大小是
A4
纸:
pageWith=”
595”
pageHeight=”
842”
Page Orientation
(默认设置为
Portrait
)
orientation
属性用来设置文档打印格式是
“Portrait”
还是
“Landscape”
。
JasperReport
允许用户在从
“Portrait”
切换到
“Landscape”
的时候调整液面的宽度和高度。我们先看一个例子:我们假定要生成一个
A4
纸的报表,采用
“Protrait”
格式。
pageWidth=”
595”
pageHeight=”
842”
orientation=”Portrait”
如果我们决定用
A4
纸的
“Landscape”
布局,首先要调整相应的页面宽度和高度:
pageWidth=”
842”
pageHeight=”
595”
orientation=”Landscape”
这是因为
JasperReport
需要确切知道它所要绘制的报表页的宽度和高度,而不只看我们提供的
orientation
属性,至少在报表装填的时候是这样。
orientation
属性仅在报表打印时有用,来通知打印机或某些
exporters
页面的
orientation
设置。
Page Margins
(页边距)
一旦页面大小确定下来,用户就可以在生成报表的时候设定报表的边距。有四个属性来完成这项工作:
topMargin
,
LeftMargin
,
bottomMargin
和
rightMargin
。缺省的设置是上下边距
20
像素,左右边距
20
像素。
Column Size and Spacing
(列宽和列间距)
一个报表可能含有多列,我们可以通过上面提到的
columnCount
属性得到报表列数。
JasperReport
需要知道列的宽度和列间距的大小。有两个属性用于这项工作:
columnWidth
和
columnSpacing
。当我们对报表设计进行编译的时候,编译器会对这项设置进行有效性检查(
validation check
)
--
看列的宽度和列间距是否符合给定的页面宽度和页边距。因为缺省的列数为一,所以缺省的列间距为
0
像素,并且缺省的列宽等于页面宽度减去左右边距所得的值。在上面
A4
的例子中,列宽即为
555
像素。
Empty Data Source Behavior
有时我们提供给我们的报表的数据源可能会没有任何
record
。
whenNoDataType
属性可以让你选择当所提供的数据源中没有数据的时候入和察看生成的报表。如下有三种不同的可能性,你可以任选其一:
l
Empty Document
:生成的报表不含有页面(
no page in it
)。当你试图装载这样的文档(
whenNoDataType=”NoPages”
)的时候,
Viewer
可能会抛出一个错误。
Blank
l
page
:表示生成的报表将仅含有一个空白页。(
whenNoDataType=”BlankPage”
)
All sections
l
displayed
:除了
detail
部分的其他部分将在生成的文档中显示出来。(
whenNoDataType=”AllSectionNoDetail”
)。
缺省的设置是
whenNoDataType=”NoPages”
。
Title and Summary Sections Placement
(标题和摘要的放置)
如果你想让
title
部分和
summary
部分在单独的一页里显示,你所需要做的事情就是让下面的一个或两个属性的值为
“true”
:
isTitleNewPage
,
isSummaryNewPage
。这两个属性缺省情况下为
false
。
注意
:
即使你选择了在最后一页的剩余部分显示
summary
,如果列数超过一列,并且第二列已经在最后一页出现的时候(没试过,等有机会试验一下),新的一页将会被自动生成。
Scriptlet Class
scirptletClass
属性用于设置用于当前报表的
scriptlet
类的名字。在以后我会对
Scriptlet
进行详细讨论。如果你没为这个属性提供任何值,报表引擎将会使用
net.sf.jasper.engine.JRDefaultScriptlet
的实例。
5
报表数据(
Report Date
)
当我们谈到报表装填过程的时候,有三样东西需要作为输入提供给报表引擎:报表设计(
report design
),参数值(
parameter values
)和报表的数据源(
data source
)。
在先前的章节,我们已经看到了有关报表设计的某些方面,现在我们要更加详细的关注其他两方面的内容:参数(
parameter
)和报表数据源。他们描绘了报表引擎在装填报表过程中所用到的仅有的数据来源。像你用其他报表工具所希望的一样,这些数据将会根据报表设计中的定义的模版(
template
)被组织起来,并被用来生成准备打印的、面向页面的文档。
5.1
表达式(
expressions
)
表达式是
JasperReport
的一个非常有用的特性。他们可以用来声明执行各种执行各种计算(
calculations
)的报表变量(
report variables
),进行报表的数据组织,定制报表文本字段(
text field
)的内容或者进一步定制报表对象的
appearance
。
所有报表表达式基本上都是
Java
表达式,他们可以以特殊的语法引用报表参数(
parameter
),报表字段(
field
)和报表变量(
variable
)。在
XML
报表设计中,有一些用于定义表达式的元素:,,,,,等等
因为所有的
JasperReport
表达式都是真正(
real
)的
Java
表达式,只要你用完整的类名(包括包名)来引用这些表达式,你就可以在任何
class
中使用他们。当你编译报表和装填数据的时候,你应该确定你在报表表达式中使用的类已经写入了
classpath
。
报表参数引用是通过
$P{}
序列引入的,例如:
$P{ReportTitle}
这个例子假定我们在报表设计中有一个名为
ReportTitle
的报表参数,这个参数是一个
java.lang.String
类。当报表进行装填的时候,文本字段将会显示这个参数的值。
为了在一个表达式中使用报表字段,字段名必须放在
$F{}
的括号中。例如,如果我们想要在一个文本字段中显示两个数据源字段的连接值(
concatenated values
)
,
我们可以定义如下表达式:
$F{FirstName} + “ “ + $F{LastName}
表达式可以更加复杂:
$F{FirstName} + “ “ + $F{LastName}
+ “ was hired on “ +
(new SimpleDateFormat(“MM/DD/YYYY”)).format($F{HireDate)) + “.”
正如你所看到的一样,参数、字段和变量引用都是通过用
JasperReport
的特殊语法从一个真正地
Java
对象中引入的。(实际上是一个
JREvaluator
对象,该对象是在运行其生成的动态对象,并不能在本地磁盘上见到它的身影,不过如果你使用
iReport
的话,你就可以在生成报表文件的同一目录下看到它的本来面目)
5.2
参数(
Parameters
)
Parameters
是传递给报表装填操作的对象引用。这些参数主要作用于把那些不能从报表数据源中获得的数据传给报表引擎。例如,我们可能要把执行报表装填过程的用户名字专递给报表引擎,如果我们想让它显示在报表上或者在我们想在报表的
title
上动态的改变它,我们就可以以参数的形式传给报表引擎。
我们可以用如下的方式定义参数:
这里所提供的报表参数值可以被用到各种报表表达式中,在报表
SQL
查询中,甚至可以用到报表的
scriptlet
类里。下面是构成参数定义的全部组件(
XML
元素):
参数名
name
属性是一个强制属性。
JasperReport
的命名习惯和
Java
语言的命名习惯是类似的,这意味着参数名应该是一个单词,其中不含特殊字符(如分号)。
参数类型
报表参数的第二个强制属性是提供参数值的类型名。这个类型属性可以使任意的值,只要这个类型的名字在报表编译期和装填期能在
classpath
中找到即可。
Prompting for Parameter values
在
GUI
引用程序中,建立一个报表参数集,让用户在执行装填过程之前输入某些应用程序需要用户输入的报表参数是很有用的。可选参数
isForPropting
参数用来声明是否显示提示信息让用户输入某些参数。下面的例子中,我声明了一个文本参数,当需要用户输入参数值的时候,这个文本参数用来在一个已定制的对话框中描述需要用户输入什么样的参数。
Please type here the report comments if any
]]>
注意:相信大家都知道表示
“
内容
”
将不被
XML
解析器解析,也就是说,你可以在
“
内容
”
里加入
XML
的特殊字符,如
>
,
<
等等。
参数的默认值(
parameter default value
)
通常参数值都是使用
java.util.Map
对象传给装填过程的,其中参数名作为
Key
。这样,你就不用每次都为每个参数提供一个
value
了
—
可以批量放到
Map
对象里一起传给装填管理器。如果你没有为参数提供一个
value
,引擎就认为它是
null
。但是如果你为它提供了一个默认值,则引擎将在你没提供这个参数值的情况下使用这个默认值。如果你在装填时没提供数据,下面的
java.util.Date
将在装填是被引擎使用来表示当天日期:
new java.util.Date()
在参数的默认表达式中,我们可以使用这个与定义的报表参数。
5.2.1
内置的报表参数
每一个报表设计中都含有一些与定义的报表参数,这些内置的参数的描述如下:
REPORT_PARAMETERS_MAP
这是一个内置的参数,这个参数总是指向一个
java.util.Map
对象,该对象保存了用户调用报表装填过程时传递给报表引擎的用户定的参数。
REPORT_CONNECTION
这个报表参数指向一个
java.sql.Connection
对象,这个对象被提供给报表引擎用来通过
JDBC
来执行
SQL
报表查询。将
master
报表使用的
JDBC Connection
对象传递给
subreport
是非常有用的,有关这方面信息请查看
subreport
例子
REPORT_DATASOURCE
在报表装填的时候,我们可以或者直接由应用程序中提供,或由报表引擎从所提供的
JDBC
Connection
在后台
create
而获得一个数据源。这个内置的参数允许我们在报表表达式中或
scriptlet
中访问报表数据源,而不论我们为什么要这么做。
REPORT_SCRIPTLET
即使报表不使用
scriptlet
,这个内置的参数仍将指向一个
net.sf.jasper.engine.JRAbstracStriptlet
实例,该实例实际是一个
net.sf.jasper.engine.JRDefaultScriptlet
对象。
但是当使用
scriptlet
时,报表装填过程所生成的这个指向
scriptlet
类实例的引用允许我们调用其中的某些特殊函数,使用或控制
scriptlet
对象在装填过程中已经准备好的数据。在
scriptlet
例子中你可以看到更详细的使用过程。
5.3 Data Source
(数据源)
在进行报表装填的时候,
JasperReport
引擎迭代的从用户提供的数据源中提取
record
,并根据报表设计所提供的模版生成报表的各个部分(
section
)。通常情况下,引擎需要接收一个
net.sf.jasper.engine.JRDataSource
对象作为报表数据源。但是像我们即将看到的那样,当报表数据存储于关系型数据库的时候,
JasperReport
有了让用户提供一个
JDBC
链接对象来替代通常的数据源对象的特性。
JRDataSource
接口非常简单,如果我们想要实现它只需要实现下面两个方法:
public Boolean next() throw JRException;
public Object getFieldValue(JRField jrField) throw JRException
在报表装填的时候,
next()
方法将被报表引擎调用,迭代的从数据源中获取数据。第二个方法用来为每个在当前数据源记录(
data source record
)中的报表字段(
report field
)提供
value
。
应当知道,从数据源取得数据的唯一方法是使用
report field
。一个数据源对象更像是一个二维表,表中含有数据。这个二维表的行是一条一条的
record
,而每一列都映射为一个
report field
。所以我们可以在
report
表达式中使用数据源。
JasperReport
提供了一些缺省的
JRDataSource
实现,我们来具体看一下:
Class net.sf.jasper.engine.JRResultSetDataSource
这是一个非常有用的缺省实现,因为他外覆(
wrap
)了
java.sql.ResultSet
对象。由于多数报表的生成都采用关系数据库中存储的数据,所以这个类是被使用得最为广泛的数据源对象。然而在以下的两种情况下您可以不必在装填过程中自己生成这个对象:
如果你选择在你的报表中用
SQL
查询来获得在关系数据库的某个
table
中的数据,报表引擎将会通过执行给定的
SQL
查询并且将返回的
java.sql.ResultSet
外覆为一个
net.sf.jasper.engine.JRResultSetDataSource
实例来执行这项操作。引擎唯一需要的是一个
java.sql.Connection
对象来执行查询操作。这时你可以提供
connection
对象来作为通用数据源对象(
usual data source object
)。例子有:
jasper
,
scriptlet
,
subreport
和
query
。
当然你可以在应用程序中即
JasperReport
之外执行
SQL
查询。这样的话,你可以手动的外覆
java.sql.ResultSet
,再调用报表装填过程之前实例化这个数据源对象。当使用这种类型的数据源的时候,你需要为在
result set
中的每一列生命一个
report field
。
report field
的名字和类型必须和列的名字和类型匹配。
Class net.sf.jasper.engine.JREcptyDataSource
这个类主要用于当生成报表的数据不是来自数据源,而是来自参数或重要的仅是数据源中
virtual records
的数量的时候。例子
fonts
,
images
,
shapes
和
unicode
都使用了这个类来装填报表,来模拟数据源中没有一条记录,所有字段都为
null
的情况。
Class net.sf.jasper.engine.data.JRTableModelDataSource
这个
JRDataSource
接口的缺省实现外覆了
javax.swing.table.TableModel
对象,它可以用在
Java Swing
应用程序中从已经显示到屏幕上的
table
中得数据来生成报表。
--
我喜欢。
通常有两种方法来使用这种数据源:
通常,为了要从中取得数据,你需要为
javax.swing.table.TableModel
对象的每一列生命一个
report field
。但是有些情况下会出现问题,比如
report field
的命名需要遵照
Java
命名规范来声明变量,而
table
的列名则不需要。幸运的是,你仍然可以通过列的索引而不是它的名字来将
report field
与列进行映射。例如,一个列名为
“Produce Description”
不可能被映射到名为
“Produce Description”
的
report field
上,因为
report field
名中含有空格,这将引起一个编译错误。但是如果你知道这个列示
table model
对象的第三列(
index=2
),那么你就可以命名相应的字段
“COLUMN_
2”
并无误地使用这一列的数据。例子有:
datasource
Class net.sf.jasper.engine.data.JRBeanArrayDataSource
这个类外覆了一个
JavaBeans
数组,并且通过反射来获取
report field
的值。在这种数据源中,一个
JavaBean
对象描述了一条记录。如果我们有一个名为
“ProductDescription”
的
report field
,在获取这个字段的值的时候,程序将会试图通过反射机制调用一个当前
JavaBeans
对象中
]
名为
getProductDescription()
的方法。对于
boolean
字段,当调用
get
前缀的属性不能返回其属性值的时候,程序将会试图使用
is
前缀的方法来获得属性值。
Class net.sf.jasper.engine.data.JRBeanCollectionDataSource
这个类和上一个类非常类似,它也是使用反射机制和
JavaBean
命名规范,但是它外覆了一个
java.util.Collection
对象而不是一个
JavaBean
对象数组。在
datasource
例子中你可以看到进一步的用法。
5.4
报表查询(
Report Query
)
为了要为报表装填数据,我们需要为报表引擎提供所需的数据,或者至少告诉它怎样去获取数据。
JasperReport
通常需要接受一个
net.sf.jasper.engine.JRDataSource
对象作为报表的数据源,同时作为更为强大的功能,
JasperReport
能直接用
JDBC
从关系数据库总获取数据。类库允许用户在他们的报表设计中提供
SQL
查询以便可以自运行期从数据库中提取数据。要做到这一点,你只需要在装填的时候为装填管理器的
fillReport()
方法提供一个
java.sql.Connection
而不是
JRDataSource
对象即可。
在报表中,可以使用元素来引入查询。如果这个元素存在,则出现在报表参数声明之后,报表
field
之前。
如下是一个
SQL
查询的例子:
为了更好的定制从数据库中取回的数据集(
data set
),一个重要的方面是在报表查询字符串中报表参数的使用(
use of report parameters
)。在查询中,这些参数可能会像动态过滤器(
dynamic filter
)一样工作,它们用特殊的语法被引入进来为报表提供数据,很像
report expression
。
如下有两种在查询中的使用参数的方法:
1.
像通常的
java.sql.PreparedStatement
的参数那样使用,用如下语法:
SELECT * FROM Orders WHERE OrderID <= $P{MaxOrderID} ORDER BY ShipCountry
]]>
2.
有时,我们需要使用参数来动态更改
SQL
查询的某些部分,或将整个
SQL
查询作为参数提供给装填过程。在这种情况下,语法稍微有些不同,向下面的例子,注意
“
!
”
SELECT * FROM $P!{MyTable} ORDER BY $P!{OrderByClause}
]]>
在这个例子中,这个引入了参数值得特殊的语法确定了我们为这些参数所提供的值将会替代查询中的参数引用(
$P!{}
的内容)。这些参数将被传给使用
java.sql.PrepqredStatement
对象的数据库服务器。
事实上,报表引擎首先处理
$P!{}
参数引用,通过使用他们的值来获取最重的
SQL
查询,并且仅当这件事完成之后,引擎才会将剩下的普通的
$P{}
参数引用传递给
usual IN parameters
。
--
实际上就是嵌套查询啦。
第二种用于
SQL
查询的参数引用允许你在运行期传递整个
SQL
查询语句:
$P!{MySQLQuery}
注意:你不能在参数值中再加入参数引用,也就是说,参数引用不能嵌套使用。
更详细的信息可以参看工程所带的例程:
jasper
,
subreport
,
scriptlet
,
webapp
以及最有学习价值的
query
posted on 2006-06-07 09:30
matthew 阅读(1890)
评论(0) 编辑 收藏 所属分类:
报表设计与开发