jasperreport
JasperReport是一个强大、灵活的报表生成工具,能够展示丰富的页面内容,并将之转换成PDF,HTML,或者XML格式。该库完全由Java写成,可以用于在各种Java应用程序,包括J2EE,Web应用程序中生成动态内容。
1简介编辑
jasperreport是开源的,这给我们带来很大方便,但是文档却要钱,让人不爽。不过人总要生存,再说,做这么一个好东西,用户总不能一点代价也不付(虽然对于中国普通程序员来说太贵了点)。它还有一个相关的开源工程IReport,这是一个图形化的辅助工具,因为JasperReport仅提供了可使用的类库而未提供更好的开发工具,IReport的出现解决了这一难题。它们配合使用将会更大程度的提高效率。
该库完全由Java写成,可以用于在各种Java应用程序,包括J2EE,Web应用程序中生成动态内容。它的主要目的是辅助生成面向页面的(page oriented),准备付诸打印的文档。JasperReport借由定义于XML文档中的report design进行数据组织。这些数据可能来自不同的数据源,包括关系型数据库,collections,java对象数组。通过实现简单的接口,用户可以将report library插入到订制好的数据源中,在以后我们将提到有关内容。
其实这是一份JasperReport Ultimate Guide的简单翻译以及我的理解和例子。在最后,我将描述一个我正在做的工程,将其中用到的相关信息贡献出来。我这么做是因为当我在学这个类库的时候苦于很少有相关的中文文档,又或语言不详,希望其他人不再受苦。这个文档将分几次贴出来,与原文档的章节相对应。这份文档的Word形式将在全部完成之后放在我的公开邮箱中与各位共享。
2概览编辑
把涉及到的重要的类进行一一说明。
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);
//Query 定义查询
JRDesignQuery query = new JRDesignQuery();
query.setText("SELECT * FROM Address $P!");
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
这个类与报表的数据源有关。只要能够恰当的实现他的一些接口,用户就可以在报表中使用各种数据源,在报表装填的时候由报表引擎负责对数据进行解释和获取。当报表装填的时候,报表引擎都会在后台生成或提供一个该接口的实例。
这是一个JRDataSource的缺省实现,因为很多报表数据都来源于关系数据库,所以JasperReport缺省包含了这个外覆(wrap)了java.sql.ResultSet对象的实现。
顾名思义,这个类用于包裹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报表设计进行分析,编译,装填数据,预览结果和导出到其他格式的过程。
第一步
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深度历险〉。
第二步
编译报表设计(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 workingl directory)
Java编译器的使用l
Classpathl
为了能够编译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文件相同的目录中。
第三步
Report Design 预览
JasperReport类库并没有提供高级的GUI工具来辅助进行设计工作。然而,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更好)
第四步
报表装填(Filling Report)
报表装填(report filling)过程是JasperReport library最重要的功能。它体现了这个软件最主要的目的(main objective),因为这一过程可以自由的操作数据集(data set),以便可以产生高质量的文档。有3种材料需要装填过程中作为输入提供给JasperReport:
report design(reportl templet)
参数(parameters)l
数据源(datal 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唯一需要做的是获得JDBCconnection对象,并使用它来连接想要连接的数据库,执行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());
第五步
查看报表(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来避免这一情况的发生。
第六步
打印报表
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);
第七步
导出报表
在一些应用程序环境下,将JasperReport生成的文档从其特有的格式导出到其他更为流行的格式如PDF,HTML是非常有用的。这样一来,其他人就可以在没有安装JasperReport的情况下查看这些报表,特别是当这些文档要通过网络发送出去的时候。
JasperReport提供了JasperExportManager类来支持此项功能。这项功能将会在以后不断加入对新的格式的支持。HTML和XML类型的文档,下面是导出的代码片断:JasperExportManager.exortReportToHtmlFile(myReport);
注意:想要将自己的报表导出到其他格式的用户,需要实现JRExporter的接口,或继承相应的JRAbstractExporter类。
第八步
对象的载入和保存
当使用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就是很好的例子,但是我们通常不这么用。
DTD
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文件之前排除了所有可能发生的情况。遇到这样的问题是不太可能的,因为资源载入机制会随着时间不断改进。
XML
XML 编码
当要生成不同语言的XML报表设计的时候,在XMl文件的首部的编码属性需要特别关注一下。缺省情况下,如果这个属性的值没被订制,则XML解析器将会使用“UTF-8”作为XML文件的编码格式。这一点是非常重要的,因为报表设计通常包含了静态的本地化text。对于大多数西欧语言来说,ISO-8859-1编码,也就是我们常说的LATIN1将会很好的处理如法语中重音符号的显示问题。
在编辑XML文件的时候,要找到某种特殊语言的编码类型,你可以查看XML document.FIXME
报表属性
我们上面已经看到,斯XML报表设计的根元素。这一节我将介绍报表设计对象的Property的细节以及这些属性所对应的XML attributes(为避免混淆,我将不提供Property和Attribute的中文而直接使用英文)。
Report Name
每一个报表都必须有一个名字。这个名字是相当重要的,因为类库需要它来生成文件,尤其是当编译,装填,导出报表的默认行为被使用的时候,这个名的作用更为重要。这个名字是以元素的name attribute的形式提供的,并且是强制必须填写。
Column Count(列数)
JasperReport允许生成每页的列数超过一列的报表。默认情况下,报表引擎生成每页一列的报表。
Print Order(打印顺序)
对于拥有超过一列的报表,为其提供列将被以什么顺序填充是很重要的。你可以使用的printOrder attribute来进行设置。有如下两种情况:
Verticaln 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 可能会抛出一个错误。
Blankl page:表示生成的报表将仅含有一个空白页。(whenNoDataType=”BlankPage”)
All sectionsl 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 Data)
当我们谈到报表装填过程的时候,有三样东西需要作为输入提供给报表引擎:报表设计(report design),参数值(parameter values)和报表的数据源(data source)。
在先前的章节,我们已经看到了有关报表设计的某些方面,像你用其他报表工具所希望的一样,这些数据将会根据报表设计中的定义的模版(template)被组织起来,并被用来生成准备打印的、面向页面的文档。
表达式
表达式(expressions)
表达式是JasperReport的一个非常有用的特性。他们可以用来声明执行各种执行各种计算(calculations)的报表变量(report variables),进行报表的数据组织,定制报表文本字段(text field)的内容或者进一步定制报表对象的appearance。
所有报表表达式基本上都是Java表达式,他们可以以特殊的语法引用报表参数(parameter),报表字段(field)和报表变量(variable)。在XML报表设计中,有一些用于定义表达式的元素:,,,,,等等
因为所有的JasperReport表达式都是真正(real)的Java表达式,只要你用完整的类名(包括包名)来引用这些表达式,你就可以在任何class中使用他们。当你编译报表和装填数据的时候,你应该确定你在报表表达式中使用的类已经写入了classpath。
报表参数引用是通过$P{}序列引入的,例如:
$P
这个例子假定我们在报表设计中有一个名为ReportTitle的报表参数,这个参数是一个java.lang.String类。当报表进行装填的时候,文本字段将会显示这个参数的值。
$F + “ “ + $F
表达式可以更加复杂:
$F + “ “ + $F + “ was hired on “ +
(new SimpleDateFormat(“MM/DD/YYYY”)).format($F{HireDate)) + “.”
正如你所看到的一样,参数、字段和变量引用都是通过用JasperReport的特殊语法从一个真正地Java对象中引入的。(实际上是一个JREvaluator对象,该对象是在运行其生成的动态对象,并不能在本地磁盘上见到它的身影,不过如果你使用iReport的话,你就可以在生成报表文件的同一目录下看到它的本来面目)
参数
参数(Parameters)
Parameters是传递给报表装填操作的对象引用。这些参数主要作用于把那些不能从报表数据源中获得的数据传给报表引擎。例如,我们可能要把执行报表装填过程的用户名字专递给报表引擎,如果我们想让它显示在报表上或者在我们想在报表的title上动态的改变它,我们就可以以参数的形式传给报表引擎。
我们可以用如下的方式定义参数:
这里所提供的报表参数值可以被用到各种报表表达式中,在报表SQL查询中,甚至可以用到报表的scriptlet类里。下面是构成参数定义的全部组件(XML元素):
参数名
name属性是一个强制属性。JasperReport的命名习惯和Java语言的命名习惯是类似的,这意味着参数名应该是一个单词,其中不含特殊字符(如分号)。
参数类型
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()
在参数的默认表达式中,我们可以使用这个与定义的报表参数。
内置的报表参数
每一个报表设计中都含有一些与定义的报表参数,这些内置的参数的描述如下:
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例子中你可以看到更详细的使用过程。
数据源
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实现,我们来具体看一下:
这是一个非常有用的缺省实现,因为他外覆(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的情况。
这个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例子中你可以看到进一步的用法。
报表查询
报表查询(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 ORDER BY ShipCountry
]]>
2. 有时,我们需要使用参数来动态更改SQL查询的某些部分,或将整个SQL查询作为参数提供给装填过程。在这种情况下,语法稍微有些不同,向下面的例子,注意“!”
SELECT * FROM $P! ORDER BY $P!
]]>
在这个例子中,这个引入了参数值得特殊的语法确定了我们为这些参数所提供的值将会替代查询中的参数引用($P!{}的内容)。这些参数将被传给使用java.sql.PrepqredStatement对象的数据库服务器。
事实上,报表引擎首先处理$P!{}参数引用,通过使用他们的值来获取最重的SQL查询,并且仅当这件事完成之后,引擎才会将剩下的普通的$P{}参数引用传递给usual IN parameters。--实际上就是嵌套查询啦。
第二种用于SQL查询的参数引用允许你在运行期传递整个SQL查询语句:
$P!
注意:你不能在参数值中再加入参数引用,也就是说,参数引用不能嵌套使用。
更详细的信息可以参看工程所带的例程:jasper,subreport,scriptlet,webapp以及最有学习价值的query
相关推荐
2. **jasperreports**:这是Apache软件基金会的一个项目,它是一个强大的报表生成库,能够处理各种类型的输出格式,如PDF、HTML、Excel、CSV等。jasperreports的核心功能是将数据和设计模板结合,生成报表。ireport...
JasperReport是一款强大的开源报表工具,广泛应用于Java环境中,用于生成PDF、HTML、Excel、CSV等多种格式的报告。它提供了丰富的功能,包括复杂的布局设计、数据处理以及交互式报告。在学习JasperReport时,通过其...
jasperReport的包,可以很好的使用!
白色大气风格的建筑商业网站模板下载.rar
内容概要:本文详细介绍了面向对象编程语言Objective-C的基础语法,包括其历史背景、特点、环境搭建、基本语法、面向对象编程、高级特性和实际应用。具体涵盖的内容包括Objective-C的历史发展、面向对象编程的核心特性、变量和数据类型、控制结构、函数、数组和字典的使用,以及类、对象、属性和方法的定义与使用。此外,还介绍了高级特性如协议和委托、类别和扩展、ARC、块和GCD。最后,通过示例项目展示了如何在Xcode中创建和调试Objective-C程序,以及如何使用Cocoa和Cocoa Touch框架。 适合人群:具备一定的编程基础,希望学习或深入了解Objective-C编程的开发人员。 使用场景及目标:适用于需要开发macOS和iOS应用的开发者,帮助他们掌握Objective-C的基本语法和高级特性,提高编程效率和代码质量。 其他说明:本文不仅提供了详细的理论讲解,还通过实际代码示例展示了如何在Xcode中创建和调试Objective-C项目,适合初级到中级水平的开发人员学习和参考。
本次开发的微信小程球馆预约系统,有管理员,用户两个角色。管理员功能有个人中心,用户管理,场地类型管理,球馆信息管理,球馆预约管理,系统管理。用户可以在微信小程序上面注册登录,查看球馆信息,对球馆进行预约操作。 开发本程序后台用到了SSM开发技术,微信端用的是uni-app技术。数据库采用关系数据库市场占有率最高的MySQL作为本程序使用的数据库,完全符合程序使用并且有丰富的拓展余地。 用户在微信小程序注册登录后可以看到首页,首页可以搜索球馆名称,也可以查看球馆资讯,下面是导航栏。 用户点击球馆信息可以进行预约,预约需要输入相关时间等信息。 我的里面可以修改个人信息,可以退出,还可以查看球馆预约信息和我的收藏信息。
1、嵌入式物联网单片机项目开发例程,简单、方便、好用,节省开发时间。 2、代码使用KEIL 标准库开发,当前在STM32F030C8T6运行,如果是STM32F030其他型号芯片,依然适用,请自行更改KEIL芯片型号以及FLASH容量即可。 3、软件下载时,请注意keil选择项是jlink还是stlink。 4、有偿指导v:wulianjishu666; 5、如果接入其他传感器,请查看账号发布的其他资料。 6、单片机与模块的接线,在代码当中均有定义,请自行对照。 7、若硬件有差异,请根据自身情况调整代码,程序仅供参考学习。 8、代码有注释说明,请耐心阅读。 9、编译时请注意提示,请选择合适的编译器版本。
廖鹏盛 - 时代进行曲.zip
白色大气风格的人体艺术摄影网站模板下载.zip
白色大气风格的服装设计师模板下载.zip
白色大气风格的景观设计HTML网站模板.zip
优质的机器学习资源是当今科技领域的热点,其中TensorFlow作为谷歌公司的开源库,成为最受欢迎的深度学习框架之一,广泛应用于各类项目中。TensorFlow提供了丰富的功能和灵活性,使得开发者可以轻松构建和训练复杂的神经网络模型,处理图像、文本和其他类型的数据。由于其开源性质,拥有庞大的社区支持,用户可以放心使用,并从开源社区中获取宝贵的经验和资源。 mnist数据集是机器学习领域的经典数据集之一。它包含着大量的手写数字图像,供开发者用来训练和测试各种算法和模型。这个数据集的规模相对较小,因此对于绝大多数人来说,无论是数据的下载还是训练过程,都不会对电脑性能提出过高的要求。这使得mnist成为了理想的入门数据集,适合初学者探索和理解机器学习算法的基本原理。 结合Pygame与TensorFlow,你将能够为机器学习实验创建出图形化界面,以及实现交互式处理。Pygame是一款面向游戏和多媒体应用的Python库,但同样也可以用于数据可视化和图形化交互。利用Pygame,你可以展示训练过程中的图像输出、模型的预测结果等,增强对机器学习算法运行情况的直观认识。而且,Pygame的简单。内
基于两种坐标系的超螺旋滑模观测器的永磁同步电机pmsm无位置(速度)传感器控制模型 支持 dq旋转坐标系和静止坐标系建立smo 引入二阶滑模超螺旋算法替代一阶滑模 dq坐标系引入锁相环PLL估计转速及转子位置 有效削弱抖振 赠送超螺旋滑模搭建推导文档及相关参考资料 仿真模型
汇编实验算数运算程序设计.docx
小区监控视频监控方案.doc
白色大气风格的HTML商务模板下载.zip
白色大气风格响应式运动健身瑜伽企业网站模板.zip
单片机实验仿真设计报告
白色大气风格的设计公司整站网站模板下载.zip
白色大气风格的html商务模板.zip