谷歌-Web-工具包-GWT-全-
谷歌 Web 工具包:GWT(全)
原文:
zh.annas-archive.org/md5/4648A16837179E5128074558BBE7AB6A译者:飞龙
前言
客户端-服务器架构在短时间内发生了巨大变化。以前,每个应用程序都有不同的客户端软件,软件充当 UI。这些软件必须单独安装在每个客户端上,并且每次我们对应用程序进行更改时都需要进行更新。我们从那里转移到了网络时代,并在互联网上部署应用程序,然后互联网使我们能够使用无处不在的 Web 浏览器从任何地方访问我们的应用程序。这是一个巨大的变化,但我们仍然存在性能问题,应用程序没有与桌面应用程序相同的感觉或响应。然后出现了 AJAX,现在我们可以构建可以与桌面应用程序一样具有响应性和漂亮外观的网页。AJAX 支撑着当前称为 Web 2.0 的互联网应用程序开发的趋势。为了构建 Ajax 化的应用程序,您至少需要了解 HTML、XML 和 JavaScript。
Google Web Toolkit(GWT)使使用 Java 编程语言设计 AJAX 应用程序变得更加容易。它是一个开源的 Java 开发框架,最好的特点是我们不必太担心不同的网络浏览器和平台之间的不兼容性。在 GWT 中,我们用 Java 编写代码,然后 GWT 将其转换为符合浏览器的 JavaScript 和 HTML。这非常有帮助,因为我们不必再担心模块化编程。它提供了一个类似于使用 Swing、AWT 或 SWT 等 GUI 工具包构建 Java 应用程序的开发人员所使用的编程框架。GWT 提供了所有常见的用户界面小部件,监听器以对小部件中发生的事件做出反应,并将它们组合成更复杂的小部件以执行 GWT 团队可能从未设想过的操作!此外,它使得重用程序块变得容易。这大大减少了您需要掌握的不同技术的数量。如果您了解 Java,那么您可以使用您喜欢的 IDE(本书中使用 Eclipse)来使用 Java 编写和调试 AJAX GWT 应用程序。是的,这意味着您实际上可以在代码中设置断点,并且可以从客户端无缝地调试到服务器端。您可以在任何 servlet 容器中部署应用程序,创建和运行单元测试,并基本上像任何 Java 应用程序一样开发 GWT 应用程序。因此,请开始阅读本书,启动 Eclipse,并进入令人惊叹的 AJAX 和 GWT 编程世界!
在本书中,我们将从下载和安装 GWT 开始,然后逐步介绍创建、测试、调试和部署 GWT 应用程序。我们将创建许多高度交互和有趣的用户界面。我们还将自定义小部件,并使用 JSNI 将 GWT 与其他库(如 Rico 和 Moo.fx)集成。我们还将学习创建自定义小部件,并创建一个日历和一个天气小部件。我们将探索 GWT 中的 I18N 和 XML 支持,创建单元测试,并最终学习如何将 GWT 应用程序部署到诸如 Tomcat 之类的 servlet 容器中。本书采用了典型的基于任务的模式,首先展示如何实现任务,然后解释其工作原理。
本书内容
第一章介绍了 GWT,下载和安装 GWT 以及运行其示例应用程序。
第二章介绍了从头开始创建一个新的 GWT 应用程序,使用 Eclipse IDE 与 GWT 项目,创建一个新的 AJAX 随机引用应用程序,并运行新应用程序。
第三章介绍了 GWT 异步服务的概述和介绍,以及创建素数服务和地理编码服务。
第四章涉及使用 GWT 构建简单的交互式用户界面。本章包括的示例有实时搜索、自动填充表单、可排序的表格、动态列表和类似 flickr 的可编辑标签。
第五章介绍了 GWT 的一些更高级的功能,用于构建更复杂的用户界面。本章包括的示例有可分页的表格、可编辑的树节点、简单的日志监视器、便利贴和拼图游戏。
第六章包括对 JavaScript 本地接口(JSNI)的介绍,以及使用它来包装第三方 JavaScript 库,如Moo.fx和Rico。它还包括使用 gwt-widgets 项目及其对Script.aculo.us效果的支持。
第七章涉及创建自定义的 GWT 小部件。本章包括的示例有一个日历小部件和一个天气小部件。
第八章涉及为 GWT 服务和应用程序创建和运行单元测试。
第九章介绍了我们在 GWT 中使用国际化(I18N)和客户端 XML 支持。
第十章包括使用 Ant 和 Eclipse 部署 GWT 应用程序。
您需要为本书做好准备
GWT 需要安装 Java SDK。它可以从以下网站下载:java.sun.com/javase/downloads/。与 GWT 兼容的最安全版本是 Java 1.4.2。不同版本的 GWT 适用于不同的操作系统,因此您可以使用您喜欢的操作系统而不会遇到任何麻烦。
约定
在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。
代码有三种样式。文本中的代码单词显示如下:“GWT_HOME目录包含一个带有七个应用程序的samples文件夹。”
代码块将设置如下:
public interface PrimesService extends RemoteService
{
public boolean isPrimeNumber(int numberToVerify);
}
当我们希望引起您对代码块的特定部分的注意时,相关的行或项目将被加粗:
calendarPanel.add(calendarGrid);
calendarPanel.add(todayButton);
任何命令行输入和输出都将按照以下方式编写:
applicationCreator.cmd -out <directory location>\GWTBook\HelloGWT com.packtpub.gwtbook.HelloGWT.client.HelloGWT
新术语和重要单词以粗体字体引入。例如,屏幕上看到的单词,如菜单或对话框中的单词,会在我们的文本中出现,如:“单击点击我按钮,您将获得带有您消息的窗口。”
注意
警告或重要提示会以这样的方式出现在一个框中。
注意
提示和技巧会以这种方式出现。
第一章:入门
Google Web Toolkit(GWT)是一种革命性的构建异步 JavaScript 和 XML(AJAX)应用程序的方式,其响应速度和外观与桌面应用程序相媲美。
在本章中,我们将看到:
-
GWT 简介
-
下载 GWT
-
探索 GWT 示例
-
GWT 许可证
GWT 简介
AJAX应用程序非常适合创建高度交互且提供出色用户体验的 Web 应用程序,同时在功能上与桌面应用程序相媲美,而无需下载或安装任何内容。
AJAX 应用程序将 XML 数据交换与 HTML 和 CSS 相结合,用于为界面设置样式,XMLHttpRequest对象用于与服务器应用程序进行异步通信,JavaScript 用于与提供的数据进行动态交互。这使得我们能够构建 Web 2.0 革命的一部分-与桌面应用程序相媲美的应用程序。我们可以使用 AJAX 构建与服务器在后台通信的 Web 页面,而无需重新加载页面。我们甚至可以在不刷新页面的情况下替换显示的网页的不同部分。最后,AJAX 使我们能够将传统的面向桌面的应用程序(如文字处理器、电子表格和绘图程序)通过 Web 提供给用户。
GWT 提供了一个基于 Java 的开发环境,使您能够使用 Java 语言构建 AJAX 应用程序。它封装了XMLHttpRequest对象 API,并最小化了跨浏览器问题。因此,您可以快速高效地构建 AJAX 应用程序,而无需过多担心调整代码以在各种浏览器中运行。它允许您利用标准小部件工具包(SWT)或 Swing 样式编程,通过提供一个使您能够将小部件组合成用户界面的框架来提高生产力并缩短开发时间。这是一种通过利用您对 Java 编程语言的了解和对基于事件的接口开发框架的熟悉来提高生产力并缩短开发时间的好方法。
GWT 提供了一组可立即使用的用户界面小部件,您可以立即利用它们来创建新的应用程序。它还提供了一种通过组合现有小部件来创建创新小部件的简单方法。您可以使用 Eclipse IDE 来创建、调试和单元测试您的 AJAX 应用程序。您可以构建 RPC 服务,以提供可以从您的 Web 应用程序异步访问的某些功能,使用 GWT RPC 框架非常容易。GWT 使您能够轻松地与其他语言编写的服务器集成,因此您可以通过利用 AJAX 框架快速增强您的应用程序,从而提供更好的用户体验。
到本书结束时,您将:
-
了解 GWT 的工作原理
-
快速创建有效的 AJAX 应用程序
-
为您的应用程序创建自定义可重用小部件
-
创建易于从 AJAX 应用程序中使用的后端 RPC 服务
基本下载
我们将下载 GWT 及其先决条件,将它们安装到硬盘上,然后运行 GWT 分发的一个示例应用程序,以确保它能正常工作。
行动时间-下载 GWT
为了使用 GWT,您需要安装 Java SDK。如果您还没有安装 Java SDK,可以从java.sun.com/javase/downloads/下载最新版本。按照下载提供的说明在您的平台上安装 SDK。
注意
Java 1.4.2 是与 GWT 一起使用的最安全的 Java 版本,因为它与该版本完全兼容,您可以确保您的应用程序代码将正确编译。GWT 还适用于 Java 平台的两个较新版本-1.5 和 1.6;但是,您将无法在 GWT 应用程序代码中使用这些版本中引入的任何新功能。
现在,您已经准备好下载 GWT:
- GWT 可从 GWT 下载页面(
code.google.com/webtoolkit/download.html)下载,适用于 Windows XP/2000、Linux 和 Mac OS X 平台。此下载包括 GWT 编译器、托管 Web 浏览器、GWT 类库和几个示例应用程序。
请在下载之前阅读使用条款和条件。最新版本是 1.3 RC 1,发布于 2006 年 12 月 12 日。选择适合您平台的文件。以下是显示 GWT 可用版本的示例窗口:

-
将下载的 GWT 分发文件解压到硬盘上。它将在 Windows 上创建一个名为
gwt-windows-xxx的目录,在 Linux 上创建一个名为gwt-linux-xxx的目录,其中xxx是下载分发的版本号。我们将把包含解压分发的目录称为GWT_HOME。GWT_HOME目录包含一个包含七个应用程序的samples文件夹。 -
为了确保 GWT 已正确安装,请通过执行平台的启动脚本(Windows 的可执行脚本扩展名为
.cmd,Linux 的为.sh)来运行平台的Hello示例应用程序。
为您的平台执行Hello-shell脚本。以下是托管 GWT 浏览器中成功运行Hello应用程序的屏幕截图:

单击点击我按钮,您将会得到一个对话框,如下所示:

刚刚发生了什么?
GWT_HOME目录包含 GWT 开发所需的所有脚本、文件和库,如下所示:
-
doc:该目录包含各种 GWT 类的 API 文档。API 文档以两种格式提供——Google 自定义格式和熟悉的javadoc格式。 -
samples:包含示例应用程序的目录。 -
gwt-*.jar:这些是包含 GWT 类的 Java 库。 -
index.html:该文件用作 GWT 的自述文件。它还提供了 GWT 文档的起点,以及指向其他信息来源的指针。 -
gwt-ll.dll和swt-win32-3235.dll:这些是 Windows 的共享库(仅限 Windows)。 -
libgwt-11.so, libswt-gtk-3235.so, libswt-mozilla17-profile-gcc3-gtk-3235.so, libswt-mozilla17-profile-gtk-3235.so, libswt-mozilla-gcc3-gtk-3235.so, libswt-mozilla-gtk-3235.so和libswt-pi-gtk-3235.so:这些是 Linux 共享库(仅限 Linux)。 -
applicationCreator:这是一个用于创建新应用程序的脚本文件。 -
junitCreator:这是一个用于创建新的 JUnit 测试的脚本文件。 -
projectCreator:这是一个用于创建新项目的脚本文件。 -
i18nCreator:这是一个用于创建国际化脚本的脚本文件。
当您执行Hello-shell.cmd时,您启动了 GWT 开发 shell,并将Hello.html文件作为其参数提供。开发 shell 然后启动了一个特殊的托管 Web 浏览器,并在其中显示了Hello.html文件。托管 Web 浏览器是一个嵌入式 SWT Web 浏览器,它与 Java 虚拟机(JVM)有关联。这使得可以使用 Java 开发环境(如 Eclipse)来调试应用程序的 Java 代码。
这是启动的开发 shell 的屏幕截图:

还有更多!
您可以在启动时自定义 GWT 开发 shell 提供的几个选项。从命令提示符中在GWT_HOME目录下运行开发 shell,以查看各种可用选项:
@java -cp "gwt-user.jar;gwt-dev-windows.jar" com.google.gwt.dev. GWTShell help
您将看到类似于这样的屏幕:

如果您想尝试不同的设置,比如不同的端口号,您可以修改Hello-shell.cmd文件以使用这些选项。
GWT 的 Linux 版本包含了用于托管 Web 浏览器的 32 位 SWT 库绑定。为了在 AMD64 等 64 位平台上运行示例或使用 GWT 托管的浏览器,您需要执行以下操作:
-
使用启用了 32 位二进制兼容性的 32 位 JDK。
-
在启动 GWT shell 之前,将环境变量
LD_LIBRARY_PATH设置为您的 GWT 发行版中的 Mozilla 目录。
探索 GWT 示例
Google 提供了一组示例应用程序,演示了 GWT 的几个功能。本任务将解释如何运行这些示例之一——KitchenSink 应用程序。
行动时间——进入 KitchenSink
GWT 发行版提供了七个示例应用程序——Hello, DynaTable, I18N, JSON, KitchenSink, SimpleXML 和 Mail,每个应用程序都演示了一组 GWT 功能。在这个任务中,我们将探索 KitchenSink 示例应用程序,因为它演示了 GWT 提供的所有用户界面小部件。所以,让我们进入 KitchenSink:
-
通过在
GWT_HOME/samples/KitchenSink目录中执行KitchenSink-shell脚本来为您的平台运行KitchenSink应用程序。这是KitchenSink应用程序:![Time for Action—Getting into KitchenSink]()
-
点击编译/浏览按钮。
KitchenSink应用程序将自动编译,并且系统浏览器将启动并显示KitchenSink应用程序。 -
通过单击左侧导航树中的每个小部件名称来探索应用程序。右侧的框架将显示所选小部件及其变体。我们将在以后的任务中使用大多数这些小部件来构建 AJAX 应用程序。
-
您可以将
KitchenSink示例作为 Eclipse 项目添加到您的工作区,并浏览最终由 GWT 编译成 HTML 和 JavaScript 的 Java 源代码。我们可以使用 GWT 提供的projectCreator文件辅助脚本来生成KitchenSink应用程序的 Eclipse 项目文件。 -
导航到您的
GWT_HOME目录,并在命令提示符中运行以下命令。
projectCreator.cmd -eclipse -ignore -out samples\KitchenSink
这将创建 Eclipse 平台项目文件,可以导入到您的 Eclipse 工作区中。在下一章中,当我们从头开始创建一个新应用程序时,我们将更多地了解这个脚本。
- 将
samples/KitchenSink/.project文件导入到您的 Eclipse 工作区中。您可以按照上述步骤为每个示例项目生成其 Eclipse 项目文件,然后将其导入到您的工作区。这是一个显示KitchenSink.java文件的 Eclipse 工作区:![Time for Action—Getting into KitchenSink]()
如果您知道如何使用 Java 编程,您可以使用 GWT 构建 AJAX 应用程序,而不需要了解 XMLHttpRequest 对象 API 的复杂性,也不需要了解 XMLHttpRequest 对象 API 在各种浏览器中的差异。
刚刚发生了什么?
GWT 开发 shell 启动,并在其中运行托管 Web 浏览器,其中运行着 KitchenSink 应用程序。该 shell 包含一个嵌入式版本的 Tomcat servlet 容器,监听在端口 8888 上。当您在 Web 模式下运行时,应用程序将从 Java 编译为 HTML 和 JavaScript。编译后的应用程序存储在 KitchenSink/www 目录中,并且该目录本身被注册为 Tomcat 的 Web 应用程序。这就是 Tomcat 能够为请求的 Web 浏览器提供应用程序的原因。
只要开发 shell 在运行,您甚至可以使用其他外部 Web 浏览器通过 URL http://localhost:8888/com.google.gwt.sample.kitchensink.KitchenSink/KitchenSink.html 连接到 KitchenSink 应用程序。
然而,当我们使用外部浏览器连接到开发 shell 时,我们无法使用断点,因此失去了在使用托管浏览器运行应用程序时提供的调试功能。为了从另一台计算机访问应用程序,请确保您使用可解析 DNS 的机器名称或机器的 IP 地址,而不是 localhost。
GWT 由四个主要组件组成,这些组件层叠在一起,为使用工具包编写 AJAX 应用程序提供了框架:
-
GWT Java-to-JavaScript 编译器:您可以使用 GWT 编译器将 GWT 应用程序编译为 JavaScript。然后可以将应用程序部署到 Web 容器。这被称为在 Web 模式下运行。当您单击编译/浏览按钮时,
KitchenSink项目的 Java 代码将被 Java-to-JavaScript 编译器编译为纯 HTML 和 JavaScript。生成的构件会自动复制到KitchenSink/www文件夹中。 -
GWT 托管 Web 浏览器:这使您可以在 Java 虚拟机(JVM)中运行和执行 GWT 应用程序,而无需首先编译为 JavaScript。这被称为在托管模式下运行。GWT 通过嵌入一个特殊的 SWT 浏览器控件来实现这一点,该控件包含对 JVM 的钩子。这个特殊的浏览器在 Windows 上使用 Internet Explorer 控件,在 Linux 上使用 Mozilla 控件。当您运行
KitchenSink示例时,嵌入的 SWT 浏览器就是您看到显示应用程序的内容。 -
JRE 仿真库:这包含了
java.lang和java.util包中大多数常用类的 JavaScript 实现,来自 Java 标准类库。这两个包中的一些常用类得到了支持。JDK 中的其他 Java 包目前不包括在此仿真库中。这些是您可以在 AJAX 应用程序的客户端使用的唯一类。当然,您可以自由地在服务器端实现中使用整个 Java 类库。KitchenSink项目中的 Java 代码使用此仿真库编译为 JavaScript。 -
GWT Web UI 类库:这提供了一组自定义接口和类,使您能够创建各种小部件,如按钮、文本框、图像和文本。GWT 附带了大多数在 Web 应用程序中常用的小部件。这是提供了
KitchenSink应用程序中使用的 Java 小部件的类库。
GWT 许可证
检查 GWT 许可证是否适合您。这些是您需要牢记的主要功能:
-
GWT 是开源的,并在 Apache 开源许可证 2.0 下提供-
www.apache.org/licenses/。 -
与 GWT 分发捆绑在一起的第三方库和产品是根据此页面上详细说明的许可证提供的-
code.google.com/webtoolkit/terms.html#licenses。 -
您可以使用 GWT 构建任何类型的应用程序(商业或非商业)。
-
应用程序和应用程序的代码属于应用程序的开发人员,Google 对此没有任何权利。
您可以使用 GWT 构建任何应用程序,并在任何许可下分发该应用程序。您还可以分发由 GWT 生成的 Java、HTML、JavaScript 和任何其他内容,以及用于生成该内容的 GWT 工具,只要您遵循 Apache 许可证的条款。
摘要
在本章中,我们了解了 GWT 的基本组件。我们看到了如何下载和安装 GWT,并探索了 GWT 示例应用程序。最后,我们讨论了 GWT 的许可条款。
在下一章中,我们将学习如何从头开始创建一个新的 GWT 应用程序。
第二章:创建一个新的 GWT 应用程序
在本章中,我们将使用 GWT 工具生成一个骨架项目结构和文件,有时还会使用 Eclipse 支持。然后,我们将通过修改生成的应用程序来添加功能,最终在托管模式和 Web 模式下运行应用程序。
我们将要处理的任务是:
-
生成一个新应用程序
-
使用 Eclipse 支持生成一个新应用程序
-
创建一个随机引用 AJAX 应用程序
-
在托管模式下运行应用程序
-
在 Web 模式下运行应用程序
生成一个新应用程序
我们将使用 GWT 脚本之一生成一个新的 GWT 应用程序。GWT 提供的这些辅助脚本创建了一个带有基本文件夹结构和初始项目文件的 GWT 项目的骨架,以便我们可以尽快开始创建我们的新应用程序。
行动时间-使用 ApplicationCreator
GWT 分发包含一个名为applicationCreator的命令行脚本,可用于创建一个带有所有必要脚手架的骨架 GWT 项目。要创建一个新应用程序,请按照以下步骤进行:
-
创建一个名为
GWTBook的新目录。我们将把这个目录位置称为GWT_EXAMPLES_DIR。这个文件夹将包含在本书中执行各种任务时创建的所有项目。 -
现在创建一个子目录并将其命名为
HelloGWT。这个目录将包含我们将在本章中创建的新项目的代码和文件。 -
在命令提示符中提供以下参数运行
GWT_HOME\applicationCreator:
applicationCreator.cmd -out <directory location>\GWTBook\HelloGWT com.packtpub.gwtbook.HelloGWT.client.HelloGWT
-out参数指定所有工件生成在名为HelloGWT的目录中。作为最后一个参数提供的完全限定的类名被用作applicationCreator脚本生成的类的名称,并标记为此应用程序的EntryPoint类(我们将在下一节中介绍EntryPoint类)。
上述步骤将在GWT_EXAMPLES_DIR\HelloGWT目录中创建文件夹结构并生成多个文件,如下面的屏幕截图所示:

刚刚发生了什么?
applicationCreator脚本调用gwt‑dev‑xxx.jar中的ApplicationCreator类,后者又创建了文件夹结构并生成了应用程序文件。这使得在新项目上开始变得非常容易,因为整个项目的结构都会自动为您创建。您所需要做的就是开始用您的代码填写应用程序,以提供所需的功能。统一的项目创建方式还确保遵守标准的目录结构,这在您处理不同的 GWT 项目时会更加方便。
当我们运行applicationCreator命令时,在GWT_EXAMPLES_DIR\HelloGWT目录下自动创建的所有文件和文件夹如下:
-
src -
HelloGWT-compile.cmd -
HelloGWT-shell.cmd
src: 这个文件夹包含了所有为应用程序生成的源代码和配置文件,以熟悉的 Java 包结构进行组织,根包为com.packtpub.gwtbook.hellogwt。这个包名是由applicationCreator根据我们提供的完全限定的类名推断出来的。在这个目录下生成的文件有:
com\packtpub\gwtbook\hellogwt\HelloGWT.gwt.xml:这是项目模块——一个 XML 文件,包含了 GWT 项目所需的全部配置。inherits标签指定了该模块继承的模块。在这个简单的例子中,我们只继承了 GWT 内置的User模块提供的功能。在更复杂的项目中,模块继承提供了一种很好的重用功能的方式。EntryPoint指的是当模块加载时 GWT 框架将实例化的类。这是在创建项目时提供给applicationCreator命令的类名。以下代码可以在这个文件中找到:
<module>
<!-- Inherit the core Web Toolkit stuff.-->
<inherits name="com.google.gwt.user.User"/>
<!-- Specify the app entry point class. -->
<entry-point class=
"com.packtpub.gwtbook.hellogwt.client.HelloGWT"/>
</module>
com\packtpub\gwtbook\hellogwt\client\HelloGWT.java:这是我们应用程序的入口点。它扩展了EntryPoint类,当 GWT 框架加载HelloGWT模块时,这个类被实例化,并且它的onModuleLoad()方法会被自动调用。在这个生成的类中,onModuleLoad()方法创建了一个按钮和一个标签,然后将它们添加到页面上。它还为按钮添加了一个点击监听器。我们将在本章后面修改HellowGWT.java中的代码来创建一个新的应用程序。这个文件中的当前代码如下:
package com.packtpub.gwtbook.hellogwt.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
/** Entry point classes define <code>onModuleLoad()</code>. */
public class HelloGWT implements EntryPoint
{
/** This is the entry point method. */
public void onModuleLoad()
{
final Button button = new Button("Click me");
final Label label = new Label();
button.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
if (label.getText().equals(""))
label.setText("Hello World!");
else
label.setText("");
}
}
//Assume that the host HTML has elements defined whose
//IDs are "slot1", "slot2". In a real app, you probably
//would not want to hard-code IDs. Instead, you could,
//for example, search for all elements with a
//particular CSS class and replace them with widgets.
RootPanel.get("slot1").add(button);
RootPanel.get("slot2").add(label);
}
-
com\packtpub\gwtbook\hellogwt\public\HelloGWT.html:这是一个生成的 HTML 页面,加载了HelloGWT应用程序,并被称为主机页面,因为这是托管HelloGWT应用程序的网页。尽管这个 HTML 文件看起来非常简单,但有一些需要注意的地方: -
首先,它包含一个指向
HelloGWT模块目录的元标记。这个标记是 HTML 页面和HelloGWT应用程序之间的连接。以下代码表示了这个连接:
<meta name='gwt:module'
content='com.packtpub.gwtbook.hellogwt.HelloGWT'>
- 其次,
script标签导入了来自gwt.js文件的代码。这个文件包含了引导 GWT 框架所需的代码(如下所示)。它使用HelloGWT.gwt.xml文件中的配置,然后动态加载通过编译HelloGWT.java文件生成的 JavaScript 来呈现应用程序。当我们生成骨架项目时,gwt.js文件并不存在。它是在我们在托管模式下运行应用程序或者编译应用程序时由 GWT 框架生成的。
<script language="JavaScript" src="img/gwt.js"></script>
-
HelloGWT-compile.cmd:这个文件包含了一个用于将应用程序编译成 HTML 和 JavaScript 的命令脚本。 -
HelloGWT-shell.cmd:这个文件包含了一个用于在托管模式下运行应用程序的命令脚本。
这些生成的文件之间有着明确定义的关系。HelloGWT.html文件是加载gwt.js文件的主机页面。
还有更多!
applicationCreator提供了控制新应用程序的几个参数的选项。您可以通过从以下命令行执行它来查看这些选项:
applicationCreator.cmd -help

className是applicationCreator的唯一必需参数。所有其他参数都是可选的。以下是运行applicationCreator的一些不同方式:
- 不使用 Eclipse 调试支持创建一个新的应用程序:
applicationCreator.cmd -out C:\GWTBook\Test1 com.packtpub.gwtbook.Test1.client.Test1
- 使用 Eclipse 调试支持创建一个新的应用程序:
applicationCreator.cmd -eclipse -out C:\GWTBook\Test1 com.packtpub.gwtbook.Test1.client.Test1
- 使用 Eclipse 调试支持创建一个新的应用程序,覆盖任何先前生成的同名类:
applicationCreator.cmd -eclipse -overwrite -out C:\GWTBook\Test1 com.packtpub.gwtbook.Test1.client.Test1
Google 建议为 GWT 应用程序的源代码使用以下包命名约定。这将根据其功能将项目代码分离。
-
client:这个包含了所有与客户端相关的应用程序代码。这些代码只能使用 GWT 的JRE Emulation库提供的java.util和java.lang包中的 Java 类。 -
public:这包含应用程序所需的所有静态 web 资源,如 HTML 文件、样式表和图像文件。此目录包括主机页面,即包含 AJAX 应用程序的 HTML 文件(在上面的情况下为HelloGWT.html)。 -
server:这包含服务器端代码。这些类可以使用任何 Java 类和任何 Java 库来提供功能。
应用程序的模块,如HelloGWT.gwt.xml必须放在根包目录中,作为客户端、公共和服务器包的同级目录。
使用 Eclipse 支持生成新应用程序
GWT 默认支持在 Eclipse IDE 中调试 GWT 应用程序。这是一个非常有用和节省时间的功能。在本节中,我们将学习如何使用 Eclipse IDE 支持创建新应用程序。
行动时间-修改 HelloGWT
我们在上一个任务中创建的HelloGWT应用程序运行良好,我们可以对其进行修改,并且很容易地运行它。但是,我们没有充分利用 GWT 的最大优势之一-增强整个开发体验的 Eclipse IDE 支持。现在,我们将重新创建相同的HelloGWT应用程序,这次作为一个 Eclipse 项目。如果我们可以将上一个任务中创建的项目添加 Eclipse 支持就好了。但是,目前 GWT 不支持这样做。要做到这一点,请按照下一页上给出的步骤进行操作:
- GWT 提供了一个
projectCreator脚本,用于创建 Eclipse 项目文件。使用参数运行脚本,您将看到如下所示的屏幕:
projectCreator.cmd -out E:\GWTBook\HelloGWT -eclipse HelloGWT

- 现在,使用下面给出的参数再次运行
applicationCreator,以将 HelloGWT 项目创建为 Eclipse 项目:
applicationCreator.cmd -out E:\GWTBook\HelloGWT -eclipse HelloGWT -overwrite com.packtpub.gwtbook.hellogwt.client.HelloGWT
-overwrite参数将覆盖HelloGWT目录中的文件和文件夹。因此,如果您进行了任何想要保留的更改,请确保将其复制到其他目录。您将看到如下所示的屏幕:

-
将新创建的
HelloGWT项目导入 Eclipse。通过 Eclipse 的文件|导入菜单导航到现有项目到工作区屏幕。选择HelloGWT目录作为根目录,并单击完成按钮将项目导入到您的 Eclipse 工作区。现在,您可以在 Eclipse IDE 中编辑、调试和运行应用程序! -
完成此任务后创建的所有文件夹和文件如下:
![行动时间-修改 HelloGWT]()
刚刚发生了什么?
projectCreator脚本调用gwt‑dev‑xxx.jar中的ProjectCreator类,该类又创建 Eclipse 项目文件。然后,applicationCreator修改这些文件,添加项目名称和项目的类路径信息。
通过运行projectCreator命令创建的特定于 Eclipse 的文件如下:
-
.classpath:Eclipse 文件,用于设置项目类路径信息 -
.project:Eclipse 项目文件,带有项目名称和构建器信息 -
HelloGWT.launch:Eclipse 配置,用于从运行和调试 Eclipse 菜单启动项目
还有更多!
以下是从命令行运行projectCreator时显示的各种选项的屏幕截图,带有-help选项:
projectCreator.cmd -help

创建一个随机引用的 AJAX 应用程序
在本节中,我们将创建我们的第一个 AJAX 应用程序,在网页上显示一个随机引用。这个示例应用程序将使我们熟悉 GWT 应用程序中的各种部件和模块,并为本书的其余部分奠定基础。
行动时间-修改自动生成的应用程序
我们将通过修改上一个任务中自动生成的应用程序来创建上述应用程序。自动生成的项目结构为我们提供了一个快速入门,并演示了我们可以多快地使用 GWT 框架和工具提高生产力。
随机引用是从服务器上存储的引用列表中选择的。我们的应用程序每秒钟将检索服务器提供的随机引用,并以真正的 AJAX 样式在网页上显示它——无需刷新页面。
- 在
com.packtpub.gwtbook.hellogwt.client包中创建一个名为RandomQuoteService.java的新的 Java 文件。定义一个RandomQuoteService接口,其中包含一个检索引用的方法:
public interface RandomQuoteService extends RemoteService
{
public String getQuote();
}
- 在
com.packtpub.gwtbook.hellogwt.client包中创建一个名为RandomQuoteServiceAsync.java的新的 Java 文件。定义一个RandomQuoteServiceAsync接口:
public interface RandomQuoteServiceAsync
{
public void getQuote(AsyncCallback callback);
}
- 在
com.packtpub.gwtbook.hellogwt.server包中创建一个名为RandomQuoteServiceImpl.java的新的 Java 文件。定义一个RandomQuoteServiceImpl类,它继承RemoteService并实现先前创建的RandomQuoteService接口。为这个类添加功能,以便在客户端调用getQuote()方法时返回一个随机引用。
public class RandomQuoteServiceImpl extends RemoteServiceServlet implements RandomQuoteService
{
private Random randomizer = new Random();
private static final long serialVersionUID=
-1502084255979334403L;
private static List quotes = new ArrayList();
static
{
quotes.add("No great thing is created suddenly — Epictetus");
quotes.add("Well done is better than well said
— Ben Franklin");
quotes.add("No wind favors he who has no destined port
—Montaigne");
quotes.add("Sometimes even to live is an act of courage
— Seneca");
quotes.add("Know thyself — Socrates");
}
public String getQuote()
return (String) quotes.get(randomizer.nextInt(4));
}
这就是我们在服务器上实现功能所要做的全部。现在,我们将修改客户端以访问我们添加到服务器的功能。
- 修改
HelloGWT.java以删除现有的标签和按钮,并添加一个用于显示检索到的引用的标签。在onModuleload()中添加功能,创建一个定时器,每秒触发一次,并调用RandomQuoteService来检索引用,并在上一步中创建的标签中显示它。
public void onModuleLoad()
{
final Label quoteText = new Label();
//create the service
final RandomQuoteServiceAsync quoteService =
(RandomQuoteServiceAsync)GWT.create (RandomQuoteService.class);
//Specify the URL at which our service implementation is //running.
ServiceDefTarget endpoint = (ServiceDefTarget)quoteService; endpoint.setServiceEntryPoint("/");
Timer timer = new Timer()
{
public void run()
{
//create an async callback to handle the result.
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
//display the retrieved quote in the label
quoteText.setText((String) result);
}
public void onFailure(Throwable caught)
{
//display the error text if we cant get quote
quoteText.setText("Failed to get a quote.");
}
};
//Make the call.
quoteService.getQuote(callback);
}
};
//Schedule the timer to run once every second
timer.scheduleRepeating(1000);
RootPanel.get().add(quoteText);
}
我们现在有客户端应用程序访问服务器来检索引用。
- 修改
HelloGWT.html以添加描述我们的 AJAX 应用程序的段落。
<p>
This is an AJAX application that retrieves a random quote from the Random Quote service every second. The data is retrieved and the quote updated without refreshing the page !
application, GWTgenerating, AJAX used</p>
- 通过为标签添加 CSS 使标签看起来更漂亮。在
com.packtpub.gwtbook.hellogwt.public包中创建一个名为HelloGWT.css的新文件,并向其中添加以下样式类声明:
quoteLabel
{
color: white;
display: block;
width: 450px;
padding: 2px 4px;
text-decoration: none;
text-align: center;
font-family: Arial, Helvetica, sans-serif;
font-weight: bold;
border: 1px solid;
border-color: black;
background-color: #704968;
text-decoration: none;
}
- 在
HelloGWT.java文件中修改标签以使用这种样式:
quoteText.setStyleName("quoteLabel");
- 在
HelloGWT.html中添加对这个样式表的引用,以便页面可以找到样式表中定义的样式。
<link rel="stylesheet" href="HelloGWT.css">
- 我们要做的最后一件事是在
HelloGWT模块中注册我们的RandomQuoteServiceImplservlet 类,以便客户端可以找到它。在HelloGWT.gwt.xml中添加以下行:
<servlet path="/" class="com.packtpub.gwtbook.hellogwt.server. RandomQuoteServiceImpl"/>
这个 servlet 引用将由 GWT 框架在嵌入式 Tomcat servlet 容器中注册,因此当您在托管模式下运行它时,上下文路径/被映射,以便所有对它的请求都由RandomQuoteServiceImpl servlet 提供。
在完成所有上述修改后,HelloGWT项目中的文件夹和文件如下:

我们的第一个 AJAX 应用程序现在已经准备就绪,我们能够完全使用 Java 创建它,而不需要编写任何 HTML 代码!
刚刚发生了什么?
我们创建的RandomQuoteService接口是我们服务的客户端定义。我们还定义了RandomQuoteServiceAsync,它是我们服务的异步版本的客户端定义。它提供了一个回调对象,使服务器和客户端之间可以进行异步通信。RandomQuoteServiceImpl是一个实现了这个接口并提供通过 RPC 检索随机引用功能的 servlet。我们将在第三章中详细讨论创建服务。
HelloGWT.java创建用户界面——在这种情况下只是一个标签——实例化RandomQuote服务,并启动一个计时器,计划每秒触发一次。每次计时器触发时,我们都会异步与RandomQuoteService通信以检索引言,并使用引言更新标签。RootPanel是 HTML 页面主体的 GWT 包装器。我们将标签附加到它上面,以便显示。
我们通过使用级联样式表修改了标签的外观和感觉,并在HelloGWT.java中为标签分配了样式的名称。我们将在第六章中学习如何使用样式表和样式来美化 GWT。
该应用程序中的用户界面非常简单。因此,我们直接将标签添加到RootPanel。然而,在几乎任何非平凡的用户界面中,我们都需要更准确地定位小部件并布局它们。我们可以通过利用 GWT UI 框架中的各种布局和面板类轻松实现这一点。我们将在第四章和第五章学习如何使用这些类。
在托管模式下运行应用程序
GWT 提供了一种很好的方法来测试应用程序,而无需部署它,而是在托管模式下运行应用程序。在本节中,我们将学习如何在托管模式下运行HelloGWT应用程序。
执行 HelloGWT-Shell 脚本的操作时间
您可以通过执行HelloGWT-shell脚本在托管模式下运行HelloGWT应用程序。您可以通过以下三种不同的方式来执行此操作:
- 从 shell 中执行命令脚本:
打开命令提示符并导航到HelloGWT目录。运行HelloGWT-shell.cmd以在托管模式下启动HelloGWT应用程序。
- 从 Eclipse 内部执行命令脚本:
在 Eclipse 的Package Explorer或navigator视图中双击HelloGWT-shell.cmd文件。这将执行该文件并启动托管模式下的HelloGWT应用程序。
- 从 Eclipse 中运行
HelloGWT.launcher:
在 Eclipse 中,通过单击Run | Run链接导航到Run屏幕。展开Java Application节点。选择HelloGWT目录。单击Run链接以在托管模式下启动HelloGWT应用程序。
如果应用程序正常运行,您将看到以下屏幕:

刚刚发生了什么?
命令脚本通过提供应用程序类名作为参数来执行 GWT 开发 shell。Eclipse 启动器通过创建一个启动配置来模仿命令脚本,该启动配置从 Eclipse 环境中执行 GWT 开发 shell。启动的 GWT 开发 shell 在嵌入式浏览器窗口中加载指定的应用程序,显示应用程序。在托管模式下,项目中的 Java 代码不会被编译为 JavaScript。应用程序代码作为已编译的字节码在 Java 虚拟机中运行。
在 Web 模式下运行应用程序
在上一节中,我们学习了如何在托管模式下运行 GWT 应用程序而无需部署它们。这是测试和调试应用程序的好方法。然而,当您的应用程序在生产环境中运行时,它将部署到诸如 Tomcat 之类的 Servlet 容器中。本任务解释了如何编译HelloGWT应用程序,以便随后可以部署到任何 Servlet 容器中。在 GWT 术语中,这称为在 Web 模式下运行。
执行编译应用程序的操作时间
为了在 Web 模式下运行HelloGWT应用程序,我们需要执行以下操作:
- 首先通过运行
HelloGWT‑compile脚本编译HelloGWT应用程序。
HelloGWT-compile.cmd
-
上述步骤将在
HelloGWT目录中创建一个www文件夹。导航到www/com.packtpub.gwt.HelloGWT.HelloGWT目录。 -
在 Web 浏览器中打开
HelloGWT.html文件。
运行HelloGWT客户端应用程序所需的一切都包含在www文件夹中。您可以将文件夹的内容部署到任何 Servlet 容器,并提供HelloGWT应用程序。完成上述步骤后,以下是文件夹的内容:

刚刚发生了什么?
HelloGWT-compile脚本调用 GWT 编译器,并将com.packtpub.gwt.hellogwt.client包中的所有 Java 源代码编译成 HTML 和 JavaScript,并将其复制到www\com.packtpub.gwt.hellogwt.HelloGWT目录中。这个目录名是由 GWT 自动创建的,之前提供给applicationCreator的完全限定类名中去掉client部分。这个文件夹包含了HelloGWT客户端应用程序的一个准备部署的版本。它包括:
-
HelloGWT.html:作为HelloGWT应用程序的主 HTML 页面的主机页面。 -
gwt.js:包含用于加载和初始化 GWT 框架的引导代码的生成的 JavaScript 文件。 -
History.html:提供历史管理支持的 HTML 文件。 -
xxx-cache.html和xxx-cache.xml:每个受支持的浏览器生成一个 HTML 和 XML 文件。这些文件包含通过编译com.packtpub.gwtbook.hellogwt.client和com.packtpub.gwtbook.hellogwt.server包中的源 Java 文件生成的 JavaScript 代码。例如,在这种情况下,在 Windows 上,编译产生了这些文件:
0B0ADCCCE2B7E0273AD2CA032DE172D1.cache.html
0B0ADCCCE2B7E0273AD2CA032DE172D1.cache.xml
224EDC91CDCFD8793FCA1F211B325349.cache.html
224EDC91CDCFD8793FCA1F211B325349.cache.xml
546B5855190E25A30111DE5E5E2005C5.cache.html
546B5855190E25A30111DE5E5E2005C5.cache.xml
D802D3CBDE35D3663D973E88022BC653.cache.html
D802D3CBDE35D3663D973E88022BC653.cache.xml
每组 HTML 和 XML 文件代表一个受支持的浏览器:
0B0ADCCCE2B7E0273AD2CA032DE172D1 - Safari
224EDC91CDCFD8793FCA1F211B325349 Mozilla or Firefox
546B5855190E25A30111DE5E5E2005C5 Internet Explorer
D802D3CBDE35D3663D973E88022BC653 - Opera
文件名是通过生成全局唯一标识符(GUIDs)并将 GUID 作为名称的一部分来创建的。这些文件名在不同的计算机上会有所不同,并且每次在您的计算机上进行干净的重新编译时也会有所不同。还有一个生成的主 HTML 文件(com.packtpub.gwtbook.hellogwt.HelloGWT.nocache.html),它从上面的文件中选择正确的 HTML 文件并加载它,具体取决于运行应用程序的浏览器。
www文件夹不包含com.packtpub.gwtbook.hellogwt.server包中的代码。这个服务器代码需要被编译并部署到一个 Servlet 容器中,以便客户端应用程序可以与随机引用服务进行通信。我们将在第十章中学习如何部署到外部 Servlet 容器。在正常的开发模式下,我们将使用托管模式进行测试,该模式在 GWT 开发外壳中的嵌入式 Tomcat Servlet 容器中运行服务器代码。这使得从同一个 Eclipse 环境中运行和调试服务器代码变得非常容易,就像客户端应用程序代码一样。这是 GWT 的另一个特性,使其成为开发 AJAX 应用程序的极其高效的环境。
在 Web 模式下,我们的客户端 Java 代码已经编译成 JavaScript,不同于托管模式。此外,您会注意到HelloGWT.gwt.xml不在这个目录中。此模块的配置细节包含在上面生成的 HTML 和 XML 文件中。
在 Web 模式下,我们的客户端 Java 代码已经编译成 JavaScript,不同于托管模式。此外,您会注意到HelloGWT.gwt.xml不在这个目录中。此模块的配置细节包含在上面生成的 HTML 和 XML 文件中。
值得庆幸的是,当我们运行HelloGWT-compile脚本时,所有这些工作都会被 GWT 框架自动完成。我们可以专注于我们的 AJAX 应用程序提供的功能,并将与浏览器无关的代码生成和较低级别的 XmlHttpRequest API 留给 GWT。
我们将在第十章中学习如何将 GWT 应用程序部署到 Web 服务器和 Servlet 容器。
还有更多!
您还可以在托管模式下从 GWT 开发 shell 中编译HelloGWT应用程序。运行HelloGWT-shell命令脚本以在托管模式下运行应用程序。单击 GWT 开发 shell 窗口中的编译/浏览按钮。这将编译应用程序并在单独的 Web 浏览器窗口中启动应用程序。
所有这些动态的 JavaScript 魔法意味着当您尝试从 Web 浏览器查看应用程序的源代码时,您总是会看到来自主机页面的 HTML。当您试图调试问题时,这可能令人不安。但是 GWT 中的出色 Eclipse 支持意味着您可以通过设置断点并逐行浏览整个应用程序来从图形调试器的舒适环境中调试问题!我们将在第八章中了解更多关于 GWT 应用程序的调试。
摘要
在本章中,我们使用提供的辅助脚本如applicationCreator生成了一个新的 GWT 应用程序。然后为项目生成了 Eclipse 支持文件。我们还创建了一个新的随机引用 AJAX 应用程序。我们看到如何在托管模式和 Web 模式下运行这个新应用程序。
在下一章中,我们将学习如何创建 GWT 服务,这将使我们能够提供可以通过 GWT 应用程序网页通过 AJAX 访问的异步功能。
第三章:创建服务
在本章中,我们将学习如何创建服务,这是 GWT 术语用于提供服务器端功能的术语。在 GWT 上下文中使用的术语服务与 Web 服务没有任何关系。它指的是客户端在服务器端调用的代码,以便访问服务器提供的功能。我们开发的大多数应用程序都需要访问服务器以检索一些数据或信息,然后使用 AJAX 以直观和非侵入性的方式将其显示给用户。在 GWT 应用程序中实现这一点的最佳方式是通过服务。
在本章中,我们将介绍创建服务所需的必要步骤。我们将首先创建创建一个简单的素数服务所需的各种工件,该服务验证提供的数字是否为素数。该应用程序很简单,但是其中的概念适用于您将创建的任何 GWT 服务。我们还将创建一个简单的客户端,用于消费素数服务。
我们将要解决的任务是:
-
创建服务定义接口
-
创建异步服务定义接口
-
创建服务实现
-
消费服务
前三个任务需要为您创建的每个 GWT 服务完成。
创建服务定义接口
服务定义接口充当客户端和服务器之间的合同。这个接口将由我们稍后在本章中构建的实际服务来实现。它定义了服务应提供的功能,并为希望消费此服务提供的功能的客户端制定了基本规则。
行动时间-创建素数服务
我们将为我们的素数服务创建定义。我们还将创建一个名为Samples的新项目,以包含本章和本书中创建的代码。
-
使用
projectCreator和applicationCreator创建一个名为Samples的新 Eclipse GWT 项目。将应用程序类的名称指定为com.packtpub.gwtbook.samples.client.Samples。 -
将新创建的项目导入 Eclipse IDE。
-
在
com.packtpub.gwtbook.samples.client包中创建一个名为PrimesService.java的新 Java 文件。定义一个PrimesService接口,其中包含一个验证数字是否为素数的方法。它以整数作为参数,并在验证后返回一个布尔值:
public interface PrimesService extends RemoteService
{
public boolean isPrimeNumber(int numberToVerify);
}
刚刚发生了什么?
PrimesService是一个服务定义接口。它指定了支持的方法以及应该传递给它的参数,以便服务返回响应。在 GWT 上下文中,RPC 这个术语指的是一种通过 HTTP 协议在客户端和服务器之间轻松传递 Java 对象的机制。只要我们的方法参数和返回值使用了支持的类型,GWT 框架就会自动为我们执行此操作。目前,GWT 支持以下 Java 类型和对象:
-
原始类型-字符、字节、短整型、整型、长整型、布尔型、浮点型和双精度型
-
原始类型包装类-字符、字节、短整型、整型、长整型、布尔型、浮点型和双精度型
-
字符串
-
日期
-
任何这些
可序列化类型的数组 -
实现实现
isSerializable接口的自定义类,其非瞬态字段是上述支持的类型之一
您还可以使用支持的对象类型的集合作为方法参数和返回类型。但是,为了使用它们,您需要通过使用特殊的Javadoc注释@gwt.typeArgs明确提到它们预期包含的对象类型。例如,这是我们如何定义一个服务方法,它以整数列表作为输入参数,并返回一个字符串列表:
public interface MyRPCService extends RemoteService
{
/*
* @gwt.typeArgs numbers <java.lang.Integer>
* @gwt.typeArgs <java.lang.String>
*/
List myServiceMethod(List numbers);
}
第一个注解表示这个方法只接受一个整数对象列表作为参数,第二个注解表示这个方法的返回参数是一个字符串对象列表。
创建一个异步服务定义接口
在上一个任务中创建的接口是同步的。为了利用 GWT 中的 AJAX 支持,我们需要创建这个接口的异步版本,用于在后台向服务器进行远程调用。
行动时间-利用 AJAX 支持
在本节中,我们将创建服务定义接口的异步版本。
在com.packtpub.gwtbook.samples.client包中创建一个名为PrimesServiceAsync.java的新的 Java 文件。定义一个PrimesServiceAsync接口:
public interface PrimesServiceAsync
{
public void isPrimeNumber(inr numberToVerify, AsyncCallbackcallback);
}
刚刚发生了什么?
我们的服务定义接口的异步版本必须具有与同步接口相同的方法,除了所有方法都必须将AsyncCallback对象作为参数,并且方法可能不返回任何内容。回调对象充当客户端和服务器之间的绑定。一旦客户端发起异步调用,当服务器端完成处理时,通过此回调对象进行通知。基本上,这就是 AJAX 的魔法发生的地方!你不必为所有这些魔法做任何特殊的事情,只需确保为服务定义提供这个异步接口即可。GWT 框架将自动处理客户端和服务器之间的所有通信。使用此服务的客户端应用程序将通过此方法调用服务,传递一个回调对象,并将自动通过回调到客户端应用程序中的onSuccess()方法或onFailure()方法来通知成功或失败。当前版本的 GWT 只支持异步回调到服务器。即使服务定义接口是同步的,也不能使用它来对服务器进行同步调用。因此,目前只能通过 AJAX 异步访问使用 GWT 构建的任何服务。
创建服务实现
到目前为止,我们已经创建了定义质数服务功能的接口。在本节中,我们将开始实现和填充服务类,并创建质数服务的实际实现。
行动时间-实现我们的服务
我们将创建质数服务的实现。它通过确保提供的数字只能被 1 和它自己整除来检查提供的数字是否是质数。验证结果以布尔值返回。
在com.packtpub.gwtbook.samples.server包中创建一个名为PrimesServiceImpl.java的新的 Java 文件。定义一个PrimesServiceImpl类,它扩展RemoteServiceServlet并实现先前创建的PrimesService接口。为这个类添加功能,以验证提供的数字是否是质数。
public class PrimesServiceImpl extends RemoteServiceServlet
implements PrimesService
{
private static final long serialVersionUID = -8620968747002510678L;
public boolean isPrimeNumber(int numberToVerify)
{
boolean isPrime = true;
int limit = (int) Math.sqrt ( numberToVerify );
for ( int i = 2; i <= limit; i++ )
{
if(numberToVerify % i == 0 )
{
isPrime = false;
break;
}
}
return isPrime;
}
}
刚刚发生了什么?
由于这是素数服务的实现,这个类需要实现服务定义接口,并为实现的方法添加功能。这个任务和之前的任务勾画出了创建 GWT 服务时总是需要的步骤。创建和使用 RPC 服务是解锁 GWT 强大功能并有效使用它的关键步骤。GWT 应用的基本架构包括在 Web 浏览器中呈现的客户端用户界面,并与作为 RPC 服务实现的服务器端功能进行交互,以异步地检索数据和信息而不刷新页面。在 GWT 应用中,服务包装了应用的服务器端模型,因此通常映射到 MVC 架构中的模型角色。

让我们来看看我们为一个服务创建的各种类和接口之间的关系。每次我们创建一个 RPC 服务,我们都会利用一些 GWT 框架类,并创建一些新的类和接口。完成上述任务后创建的类和接口如下:
-
PrimesService:我们的服务定义接口。它定义了我们服务中的方法,并扩展了RemoteService标记接口,表示这是一个 GWT RPC 服务。这是同步定义,服务器端实现必须实现这个接口。 -
PrimesServiceAsync:我们接口的异步定义。它必须具有与同步接口相同的方法,除了所有方法都必须以AsyncCallback对象作为参数,并且方法可能不返回任何内容。建议为这个接口使用的命名约定是在我们的同步接口名称后缀加上Async这个词。 -
PrimesServiceImpl:这是我们服务的服务器端实现。它必须扩展RemoteServiceServlet并实现我们的同步接口——PrimesService。
我们使用的 GWT 框架类来创建PrimesService:
-
RemoteService:所有 RPC 服务都应该实现的标记接口。 -
RemoteServiceServlet:PrimesServiceImpl服务实现类扩展了这个类并添加了所需的功能。这个类支持序列化和反序列化请求,并确保请求调用PrimesServiceImpl类中的正确方法。
这里有一个图表,描述了在创建素数服务时涉及的各种类和接口之间的关系。

我们的服务实现扩展了RemoteServiceServlet,它继承自HttpServlet类。RemoteServiceServlet负责自动反序列化传入的请求和序列化传出的响应。GWT 可能选择使用基于 servlet 的方法,因为它简单,并且在 Java 社区中被广泛认可和使用。它还使得我们的服务实现在任何 servlet 容器之间移动变得容易,并为 GWT 与其他框架之间的各种集成可能性打开了大门。GWT 社区的几位成员已经使用它来实现 GWT 与其他框架(如 Struts 和 Spring)之间的集成。GWT 使用的 RPC wire 格式基本上是基于 JavaScript 对象表示法(JSON)的。这个协议是 GWT 专有的,目前没有文档记录。然而,RemoteServiceServlet提供了两个方法——onAfterResponseSerialized()和onBeforeRequestDeserialized(),你可以重写这些方法来检查和打印序列化的请求和响应。
创建任何 GWT 服务的基本模式和架构总是相同的,包括以下基本步骤:
-
创建服务定义接口。
-
创建服务定义接口的异步版本。
-
创建服务实现类。在服务实现类中,我们访问外部服务提供的功能,并将结果转换为符合我们要求的结果。
在下一节中,我们将创建一个简单的客户端来消费这个新服务。我们将学习如何将此服务部署到外部 servlet 容器,如 Tomcat,在第十章。这个例子中的概念适用于我们创建的每个 GWT 服务。我们将至少为我们创建的每个服务创建这两个接口和一个实现类。这将帮助我们提供可以通过 GWT 客户端以异步方式访问的服务器功能。我们上面创建的服务独立于 GWT 客户端应用程序,并且可以被多个应用程序使用。我们只需要确保在 servlet 容器中正确注册服务,以便我们的客户端应用程序可以访问它。
消费服务
我们已经完成了 Prime Number 服务的实现。现在我们将创建一个简单的客户端,可以消费PrimesService。这将帮助我们测试服务的功能,以确保它能够完成它应该完成的任务。
行动时间-创建客户端
我们将创建一个简单的客户端,连接到 Prime Number 服务,并检查给定的数字是否是质数。我们将添加一个文本框用于输入要检查的数字,以及一个按钮,当点击时将调用服务。它将在警报对话框中显示调用的结果。
- 在
com.packtpub.gwtbook.samples.client包中创建一个名为PrimesClient.java的新文件,该文件扩展了EntryPoint类。
public class PrimesClient implements EntryPoint
{
}
- 在这个新类中添加一个
onModuleLoad()方法,并创建一个文本框。
public void onModuleLoad()
{
final TextBox primeNumber = new TextBox();
}
- 在
onModuleLoad()方法中实例化PrimesService并将其存储在变量中。
final PrimesServiceAsync primesService =
(PrimesServiceAsync) GWT
GWT.create(PrimesService.class);
ServiceDefTarget endpoint = (ServiceDefTarget) primesService;
endpoint.setServiceEntryPoint(GWT.getModuleBaseURL()+"primes");
- 创建一个新按钮,并添加一个事件处理程序来监听按钮的点击。在处理程序中,使用文本框中输入的文本作为服务的输入参数来调用
PrimesService。在警报对话框中显示结果。
final Button checkPrime=new Button("Is this a prime number?",
new ClickListener())
{
public void onClick(Widget sender)
{
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
if(((Boolean) result).booleanValue())
{
Window.alert("Yes, "+ primeNumber.getText()
+ "' is a prime number.");
}
else
{
Window.alert("No, "+ primeNumber.getText()
+ "' is not a prime number.");
}
}
public void onFailure(Throwable caught)
{
Window.alert("Error while calling the Primes
Service.");
}
};
primesService.isPrimeNumber(Integer
parseInt(primeNumber.getText()), callback);
}
});
- 在应用程序的
module.xml文件中添加以下条目,以便客户端找到此服务。
<servlet path="/primes" class=
"com.packtpub.gwtbook.samples.server.PrimesServiceImpl"/>
这是客户端。输入一个数字,然后点击按钮检查这个数字是否是质数。

响应如下显示在警报对话框中:

刚刚发生了什么?
Prime Number服务客户端通过向PrimesService传递所需的参数来调用服务。我们在module.xml文件中为服务做了一个条目,以便 GWT 框架可以正确初始化并且客户端可以找到服务。我们遵循了创建简单客户端消费 GWT 服务的常见模式:
-
创建一个实现
EntryPoint类的类。 -
重写
onModuleLoad()方法以添加所需的用户界面小部件。 -
向用户界面小部件之一添加事件处理程序,以在触发处理程序时调用服务。
-
在事件处理程序中,处理对服务方法调用的成功和失败的
callbacks,并对调用结果采取一些操作。 -
在 GWT 应用程序
module.xml中添加一个条目以便消费服务。
我们将在本书中创建示例应用程序时使用这种常见模式以及一些变化。
总结
在本章中,我们看了一下创建新的 Prime Number GWT 服务所需的各种类和接口。我们还创建了一个可以使用质数服务的客户端。
在下一章中,我们将使用 GWT 创建交互式网络用户界面。
第四章:交互式表单
在本章中,我们将学习创建交互式表单的不同方式,这些方式利用 GWT 和 AJAX 在使用基于 Web 的用户界面时提供更加流畅的用户体验。本章以及接下来的两章将为我们探索 GWT 提供基础。
我们将要解决的任务包括:
-
实时搜索
-
密码强度检查器
-
自动填充表单
-
可排序的表格
-
动态列表
-
类似 Flickr 的可编辑标签
示例应用程序
我们将把本书中创建的所有示例应用程序都整合到上一章中创建的 Samples GWT 应用程序中。我们将以与我们在第一章中探讨的KitchenSink应用程序类似的方式进行。为了做到这一点,我们将按照以下步骤进行:
-
应用程序的用户界面将在一个类中创建,该类扩展了
com.packtpub.gwtbook.samples.client包中的SamplePanel类。 -
然后,该类将被初始化并添加到
com.packtpub.gwtbook.samples.client包中的Samples类的应用程序列表中。由于Samples类被设置为入口点类,当 GWT 启动时,它将加载这个类并显示所有示例应用程序,就像KitchenSink一样。
所有示例的源代码都可以从本书的下载站点获取。请参阅附录以获取有关下载和运行示例的说明。
实时搜索
“实时搜索”是一种用户界面,它会根据用户输入的搜索条件实时提供与之匹配的选择。这是一种非常流行的 AJAX 模式,用于在用户细化搜索查询时持续显示所有有效结果。由于用户的查询不断与显示的结果同步,为用户创造了非常流畅的搜索体验。它还使用户能够以高度互动的方式快速轻松地尝试不同的搜索查询。搜索结果是异步从服务器检索的,无需任何页面刷新或重新提交搜索条件。Google 搜索页面(google.com/)就是一个很好的例子。它甚至在您输入时告诉您与您的查询匹配的搜索结果数量!
“实时搜索”AJAX 模式提供的即时反馈也可以用于预先从服务器获取结果并用于预测用户的操作。这种即时响应可以使应用程序的用户体验更加流畅,并显著提高应用程序的延迟。Google 地图(maps.google.com/)是使用这种模式预先获取地图数据的很好的例子。
行动时间-搜索即时输入!
在这个“实时搜索”示例中,我们将创建一个应用程序,该应用程序检索以您在搜索文本中输入的字母开头的水果名称列表。您可以通过减少或增加输入的字母数量来细化查询条件,用户界面将实时显示匹配的结果集。
- 在
com.packtpub.gwtbook.samples.client包中创建一个名为LiveSearchService.java的新的 Java 文件。定义一个LiveSearchService接口,其中包含一个方法,用于检索与提供的字符串匹配的搜索结果。
public interface LiveSearchService extends RemoteService
{
public List getCompletionItems(String itemToMatch);
}
- 在
com.packtpub.gwtbook.samples.client包中的一个新的 Java 文件中创建此服务定义接口的异步版本,命名为LiveSearchServiceAsync.java:
public interface LiveSearchServiceAsync
{
public void getCompletionItems
(String itemToMatch, AsyncCallback callback);
}
- 在
com.packtpub.gwtbook.samples.server包中创建一个名为LiveSearchServiceImpl.java的新的 Java 文件,实现我们的实时搜索服务。我们将创建一个字符串数组,其中包含水果列表,当调用服务方法时,我们将返回该数组中以参数提供的字符串开头的水果的子列表。
public class LiveSearchServiceImpl extends RemoteServiceServlet
implements LiveSearchService
{
private String[] items = new String[]
{"apple", "peach", "orange", "banana", "plum", "avocado",
"strawberry", "pear", "watermelon", "pineapple", "grape",
"blueberry", "cantaloupe"
};
public List getCompletionItems(String itemToMatch)
{
ArrayList completionList = new ArrayList();
for (int i = 0; i < items.length; i++)
{
if (items[i].startsWith(itemToMatch.toLowerCase()))
{
completionList.add(items[i]);
}
}
return completionList;
}
}
- 我们的服务器端实现已经完成。现在我们将创建用户界面,与实时搜索服务进行交互。在
com.packtpub.gwtbook.samples.client.panels包中创建一个名为LiveSearchPanel.java的新的 Java 文件,该文件扩展了com.packtpub.gwtbook.samples.client.panels.SamplePanel类。正如本章开头所提到的,本书中创建的每个用户界面都将被添加到一个示例应用程序中,该应用程序类似于 GWT 下载中作为示例项目之一的KitchenSink应用程序。这就是为什么我们将每个用户界面创建为扩展SamplePanel类的面板,并将创建的面板添加到示例应用程序中的示例面板列表中。添加一个文本框用于输入搜索字符串,以及一个FlexTable,用于显示从服务中检索到的匹配项。最后,创建一个我们将要调用的LiveSearchService的实例。
public FlexTable liveResultsPanel = new FlexTable();
public TextBox searchText = new TextBox();
final LiveSearchServiceAsync
liveSearchService=(LiveSearchServiceAsync)
GWT.create(LiveSearchService.class);
- 在
LiveSearchPanel的构造函数中,创建服务目标并设置其入口点。还创建一个新的VerticalPanel,我们将使用它作为添加到用户界面的小部件的容器。设置搜索文本框的 CSS 样式。此样式在Samples.css文件中定义,并且是本书的源代码分发包的一部分。有关如何下载源代码包的详细信息,请参见附录。
ServiceDefTarget endpoint=(ServiceDefTarget) liveSearchService;
endpoint.setServiceEntryPoint("/Samples/livesearch");
VerticalPanel workPanel = new VerticalPanel();
searchText.setStyleName("liveSearch-TextBox");
- 在同一个构造函数中,为文本框添加一个监听器,该监听器将在用户在文本框中输入时异步调用
LiveSearchService,并持续更新弹出面板,显示与文本框中当前字符串匹配的最新结果。这是通过调用服务获取完成项列表的方法。
searchText.addKeyboardListener(new KeyboardListener()
{
public void onKeyPress
(Widget sender, char keyCode, int modifiers)
{
// not implemented
}
public void onKeyDown
(Widget sender, char keyCode, int modifiers)
{
for (int i = 0; i < liveResultsPanel.getRowCount(); i++)
{
liveResultsPanel.removeRow(i);
}
}
public void onKeyUp
(Widget sender, char keyCode, int modifiers)
{
for (int i = 0; i < liveResultsPanel.getRowCount(); i++)
{
liveResultsPanel.removeRow(i);
}
if (searchText.getText().length() > 0)
{
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
ArrayList resultItems = (ArrayList) result;
int row = 0;
for(Iterator iter=resultItems.iterator();
iter.hasNext();)
{
liveResultsPanel.setText
(row++, 0, (String) iter.next());
}
}
public void onFailure(Throwable caught)
{
Window.alert("Live search failed because "
+ caught.getMessage());
}
};
liveSearchService.getCompletionItems
(searchText.getText(),callback);
}
}
});
- 最后,在构造函数中,将搜索文本框和搜索结果面板添加到工作面板。创建一个小的信息面板,显示关于此应用程序的描述性文本,以便在我们的
Samples应用程序中选择此示例时显示此文本。将信息面板和工作面板添加到一个停靠面板,并初始化小部件。
liveResultsPanel.setStyleName("liveSearch-Results");
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML
("<div class='infoProse'>Type the first few letters
of the name of a fruit in the text box below. A
list of fruits with names starting with the typed
letters will be displayed. The list is retrieved
from the server asynchronously. This is nice AJAX
pattern for providing user-friendly search
functionality in an application.</div>"));
workPanel.add(searchText);
workPanel.add(liveResultsPanel);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
- 将服务添加到
Samples应用程序的模块文件Samples.gwt.xml中,该文件位于com.packtpub.gwtbook.samples包中。通过将此路径添加到模块文件中,让我们可以使用此路径创建并设置此服务的端点信息。
<servlet path="/livesearch" class=
"com.packtpub.gwtbook.samples.server.LiveSearchServiceImpl"/>
这是应用程序的用户界面:

一旦开始输入水果名称的前几个字母,以该字符串开头的水果名称将被检索并显示在文本框下方的面板中。

刚刚发生了什么?
应用程序的用户界面在浏览器中加载时显示一个文本框。当您在框中输入一个字母时,文本框上将触发onKeyUp()事件,并在此事件处理程序中,我们异步调用LiveSearchService中的getCompletionItems(),并传入当前在文本框中的文本。我们服务中此方法的实现返回一个包含所有匹配名称的列表。在这个例子中,匹配的名称是从服务本身包含的映射中检索出来的,但根据您的应用程序需求,它也可以很容易地从数据库、另一个应用程序或 Web 服务中检索出来。我们将列表中存在的项目添加到FlexTable部件中,该部件就在文本框的下方。FlexTable允许我们创建可以动态扩展的表格。如果文本框为空,或者我们删除了框中的所有文本,那么我们就清空表中的列表。我们使用一个面板作为此应用程序中所有部件的容器。
面板是 GWT 框架中部件的容器,用于对它们进行布局。您可以将任何部件甚至其他面板添加到面板中。这使我们能够通过将它们添加到面板中来组合部件,从而构建复杂的用户界面。GWT 框架中常用的面板有:
-
停靠面板:一个通过将其停靠或定位在边缘上的子部件进行布局,并允许最后添加的部件占据剩余空间的面板。
-
单元格面板:一个将其部件布局在表格的单元格中的面板。
-
选项卡面板:一个在选项卡页集中布局子部件的面板,每个选项卡页都有一个部件。
-
水平面板:一个将其所有子部件按从左到右的单个水平列布局的面板。
-
垂直面板:一个将其所有子部件按从上到下的单个垂直列布局的面板。
-
流动面板:一个将其部件从左到右布局的面板,就像文本在一行上流动一样。
-
弹出面板:一个通过弹出或覆盖在页面上的其他部件上显示其子部件的面板。
-
堆叠面板:一个通过垂直堆叠其子部件来布局其子部件的面板。所使用的隐喻与 Microsoft Outlook 的用户界面相同。
在本章和本书的其余部分,我们将使用大多数这些面板来布局我们的用户界面。这个任务的概念可以扩展并应用于几乎任何类型的搜索,您可以为您的应用程序提供。您甚至可以增强和扩展此应用程序,以向用户提供更多的信息,例如匹配结果的数量。GWT 提供的管道和工具使得提供此功能变得非常容易。实时搜索 AJAX 模式及其使用的最佳示例之一是 Google 建议服务。当您在文本字段中键入搜索查询字符串时,它会连续检索并显示匹配结果列表。您可以在www.google.com/webhp?complete=1&hl=en上看到它的运行情况。
密码强度检查器
视觉线索是通知用户应用程序中事物状态的好方法。消息框和警报经常被用于此目的,但它们通常会让用户感到烦躁。通过微妙地向用户指示应用程序使用状态,可以提供更流畅和愉快的用户体验。在本节中,我们将创建一个应用程序,通过使用颜色和复选框来向用户指示输入密码的强度。我们将以与它们正常用法非常不同的方式使用复选框。这是使用 GWT 部件的新颖和不同方式的示例,并混合和匹配它们以提供出色的用户体验。
行动时间-创建检查器
在当今时代,几乎所有事情都需要密码,选择安全密码非常重要。有许多标准建议创建一个免受大多数常见密码破解攻击的安全密码。这些标准从创建包含一定数量的小写字母和数字的 15 个字母密码到使用随机密码生成器创建密码。在我们的示例应用程序中,我们将创建一个非常简单的密码强度检查器,只检查密码中的字母数量。包含少于五个字母的密码字符串将被视为弱密码,而包含五到七个字母的密码将被视为中等强度。任何包含超过七个字母的密码将被视为强密码。标准故意保持简单,以便我们可以专注于创建应用程序,而不会陷入实际密码强度标准中。这将帮助我们理解概念,然后您可以扩展它以使用您的应用程序需要的任何密码强度标准。此示例使用服务来获取密码强度,但这也可以在客户端上完成,而无需使用服务器。
- 在
com.packtpub.gwtbook.samples.client包中创建一个名为PasswordStrengthService.java的新的 Java 文件。定义一个PasswordStrengthService接口,其中包含一个方法,用于检索作为方法参数提供的密码字符串的强度:
public interface PasswordStrengthService extends RemoteService
{
public int checkStrength(String password);
}
- 在
com.packtpub.gwtbook.samples.client包中的一个新的 Java 文件中创建这个服务定义接口的异步版本,命名为PasswordStrengthServiceAsync.java:
public interface PasswordStrengthServiceAsync
{
public void checkStrength
(String password, AsyncCallback callback);
}
- 在
com.packtpub.gwtbook.samples.server包中创建一个名为PasswordStrengthServiceImpl.java的新 Java 文件,实现我们的密码强度服务。
public class PasswordStrengthServiceImpl extends
RemoteServiceServlet implements PasswordStrengthService
{
private int STRONG = 9;
private int MEDIUM = 6;
private int WEAK = 3;
public int checkStrength(String password)
{
if (password.length() <= 4)
{
return WEAK;
}
else if (password.length() < 8)
{
return MEDIUM;
}else
{
return STRONG;
}
}
}
- 现在让我们为这个应用程序创建用户界面。在
com.packtpub.gwtbook.samples.client.panels包中创建一个名为PasswordStrengthPanel.java的新的 Java 文件,它扩展了com.packtpub.gwtbook.samples.client.panels.SamplePanel类。创建一个用于输入密码字符串的文本框,一个名为strengthPanel的ArrayList,用于保存我们将用于显示密码强度的复选框。还创建PasswordStrengthService对象。
public TextBox passwordText = new TextBox();
final PasswordStrengthServiceAsync pwStrengthService =
(PasswordStrengthServiceAsync) GWT.create(PasswordStrengthService.class);
public ArrayList strength = new ArrayList();
- 通过将它们的样式设置为默认样式来添加一个私有方法来清除所有复选框。
private void clearStrengthPanel()
{
for (Iterator iter = strength.iterator(); iter.hasNext();)
{
((CheckBox) iter.next()).
setStyleName(getPasswordStrengthStyle(0));
}
}
- 添加一个私有方法,根据密码强度返回 CSS 名称。这是一个很好的方法,可以根据强度动态设置复选框的样式。
private String getPasswordStrengthStyle(int passwordStrength)
{
if (passwordStrength == 3)
{
return "pwStrength-Weak";
}
else if (passwordStrength == 6)
{
return "pwStrength-Medium";
}
else if (passwordStrength == 9)
{
return "pwStrength-Strong";
}
else
{
return "";
}
}
- 在
PasswordStrengthPanel类的构造函数中,创建一个名为strengthPanel的HorizontalPanel,向其中添加九个复选框,并设置其样式。如前所述,我们在本书的示例应用程序中使用的样式可在文件Samples.css中找到,该文件是本书源代码分发的一部分。我们还将这些相同的复选框添加到strength对象中,以便稍后可以检索它们以设置它们的状态。这些复选框将用于直观显示密码强度。创建一个新的VerticalPanel,我们将用作向用户界面添加的小部件的容器。最后,创建服务目标并设置其入口点。
HorizontalPanel strengthPanel = new HorizontalPanel();
strengthPanel.setStyleName("pwStrength-Panel");
for (int i = 0; i < 9; i++)
{
CheckBox singleBox = new CheckBox();
strengthPanel.add(singleBox);
strength.add(singleBox);
}
VerticalPanel workPanel = new VerticalPanel();
ServiceDefTarget endpoint=(ServiceDefTarget) pwStrengthService;
endpoint.setServiceEntryPoint(GWT.getModuleBaseURL() +
"pwstrength");
- 在同一个构造函数中,设置密码文本框的样式,并添加一个事件处理程序来监听密码框的更改。
passwordText.setStyleName("pwStrength-Textbox");
passwordText.addKeyboardListener(new KeyboardListener()
{
public void onKeyDown
(Widget sender, char keyCode, int modifiers)
{
}
public void onKeyPress
(Widget sender, char keyCode, int modifiers)
{
}
public void onKeyUp(Widget sender, char keyCode, int modifiers)
{
if (passwordText.getText().length() > 0)
{
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
clearStrengthPanel();
int checkedStrength = ((Integer) result).intValue();
for (int i = 0; i < checkedStrength; i++)
{
((CheckBox) strength.get(i)).setStyleName
(getPasswordStrengthStyle(checkedStrength));
}
}
public void onFailure(Throwable caught)
{
Window.alert("Error calling the password strength service." + caught.getMessage());
}
};
pwStrengthService.checkStrength
(passwordText.getText(), callback);
}
else
{
clearStrengthPanel();
}
}
});
- 最后,在构造函数中,将密码文本框和强度面板添加到工作面板。创建一个小的信息面板,显示关于此应用程序的描述性文本,以便在我们的
Samples应用程序的可用示例列表中选择此示例时可以显示此文本。将信息面板和工作面板添加到一个停靠面板,并初始化小部件。
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML(
"<div class='infoProse'>Start typing a password
string. The strength of the password will be
checked and displayed below. Red indicates that the
password is Weak, Orange indicates a Medium
strength password and Green indicates a Strong
password. The algorithm for checking the strength
is very basic and checks the length of the password
string.</div>"));
workPanel.add(passwordText);
workPanel.add(infoPanel);
workPanel.add(strengthPanel);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
- 将服务添加到
Samples应用程序的模块文件中——com.packtpub.gwtbook.samples包中的Samples.gwt.xml。
<servlet path="/pwstrength" class=
"com.packtpub.gwtbook.samples.server.
PasswordStrengthServiceImpl"/>
这是密码强度检查应用程序的用户界面:

现在开始输入密码字符串以检查其强度。当您输入少于五个字符的密码字符串时,密码强度如下:

刚刚发生了什么?
密码强度服务检查提供的字符串的大小,并根据其弱、中、强返回一个整数值,分别为三、六或九。它通过使用以下标准来做出这一决定:如果密码字符串长度小于五个字符,则为弱密码;如果超过五个字符但不超过七个字符,则被视为中等强度密码。超过七个字符的任何密码都被视为强密码。
用户界面由一个文本框和一个包含九个复选框的面板组成,用于以密码形式输入密码字符串,并以密码的形式显示其强度。事件处理程序被注册用于监听由密码文本框生成的键盘事件。每当密码文本发生变化时,无论是在字段中输入或更改字符,我们都会异步与密码强度服务通信,并检索给定字符串作为密码的强度。返回的强度以颜色的形式显示给用户,以象征三种不同的密码强度。
密码强度显示在一个由九个复选框添加到HorizontalPanel创建的复合小部件中。根据密码字符串的强度,复选框的颜色会使用 CSS 进行更改。将 GWT 提供的基本小部件组合成更复杂的小部件以构建用户界面的过程是构建 GWT 应用程序中的常见模式。通过利用 GWT 框架的强大功能,可以以这种方式构建相当复杂的用户界面。随着我们在本章后面继续探索各种 GWT 应用程序以及整本书中的其他部分,我们将看到更多的例子。
自动表单填充
Web 上的表单是无处不在的,广泛用于从客户资料显示到在线填写申请等各种用途。我们不喜欢每次都要通过所有这些字段并在每次都要输入信息,尤其是如果我们之前在该网站上已经这样做过。加快这个过程的一个很好的方法是在填写关键表单字段时预填充以前收集的信息。这不仅节省了客户的一些输入,还是一个极大的可用性增强,提高了整个客户体验。在本节中,我们将构建一个表单,当我们在客户 ID 字段中输入一个已识别的值时,将自动填写各种字段。
操作时间—创建动态表单
我们将创建一个应用程序,使得在某个字段中提供特定值时,填写表单的各种字段变得容易。这在大多数基于 Web 的业务应用程序中是非常常见的情况,例如,需要提供用户信息以注册服务。对于新用户,这些信息需要由用户填写,但对于系统的先前用户,这些信息已经可用,并且可以在用户输入唯一标识符(识别他或她的 ID)时访问和用于填写所有字段。在这个应用程序中,当用户输入我们已知的CustomerID时,我们将自动填写表单的各种字段。
- 在
com.packtpub.gwtbook.samples.client包中创建名为AutoFormFillService.java的新 Java 文件。定义一个AutoFormFillService接口,其中包含一个方法,用于在提供键时检索表单信息:
public interface AutoFormFillService extends RemoteService
{
public HashMap getFormInfo(String formKey);
}
- 在
com.packtpub.gwtbook.samples.client包中创建名为AutoFormFillServiceAsync.java的新 Java 文件。定义一个AutoFormFillAsync接口:
public interface AutoFormFillServiceAsync
{
public void getFormInfo
(String formKey, AsyncCallback callback);
}
- 在
com.packtpub.gwtbook.samples.server包中创建名为AutoFormFillServiceImpl.java的新 Java 文件。定义一个AutoFormFillServiceImpl类,该类扩展RemoteServiceServlet并实现先前创建的AutoFormFillService接口。首先,我们将使用一个简单的HashMap来存储客户信息,并添加一个方法来填充映射。在您的应用程序中,您可以从任何外部数据源(如数据库)检索此客户信息。
private HashMap formInfo = new HashMap();
private void loadCustomerData()
{
HashMap customer1 = new HashMap();
customer1.put("first name", "Joe");
customer1.put("last name", "Customer");
customer1.put("address", "123 peachtree street");
customer1.put("city", "Atlanta");
customer1.put("state", "GA");
customer1.put("zip", "30339");
customer1.put("phone", "770-123-4567");
formInfo.put("1111", customer1);
HashMap customer2 = new HashMap();
customer2.put("first name", "Jane");
customer2.put("last name", "Customer");
customer2.put("address", "456 elm street");
customer2.put("city", "Miami");
customer2.put("state", "FL");
customer2.put("zip", "24156");
customer2.put("phone", "817-123-4567");
formInfo.put("2222", customer2);
HashMap customer3 = new HashMap();
customer3.put("first name", "Jeff");
customer3.put("last name", "Customer");
customer3.put("address", "789 sunset blvd");
customer3.put("city", "Los Angeles");
customer3.put("state", "CA");
customer3.put("zip", "90211");
customer3.put("phone", "714-478-9802");
formInfo.put("3333", customer3);
}
- 在
getFormInfo()中添加逻辑,以返回提供的表单键的表单信息。我们获取用户在表单中输入的提供的键,并使用它来查找用户信息,并将其异步返回给客户端应用程序。
public HashMap getFormInfo(String formKey)
{
if (formInfo.containsKey(formKey))
{
return (HashMap) formInfo.get(formKey);
}
else
{
return new HashMap();
}
}
- 在
com.packtpub.gwtbook.samples.client.panels包中的新 Java 文件AutoFormFillPanel.java中创建此应用程序的用户界面。为每个信息字段创建一个文本框和一个标签。
private TextBox custID = new TextBox();
private TextBox firstName = new TextBox();
private TextBox lastName = new TextBox();
private TextBox address = new TextBox();
private TextBox zip = new TextBox();
private TextBox phone = new TextBox();
private TextBox city = new TextBox();
private TextBox state = new TextBox();
private Label custIDLbl = new Label("Customer ID : ");
private Label firstNameLbl = new Label("First Name : ");
private Label lastNameLbl = new Label("Last Name : ");
private Label addressLbl = new Label("Address : ");
private Label zipLbl = new Label("Zip Code : ");
private Label phoneLbl = new Label("Phone Number : ");
private Label cityLbl = new Label("City : ");
private Label stateLbl = new Label("State : ");
HorizontalPanel itemPanel = new HorizontalPanel();
- 创建我们要调用的服务类。
final AutoFormFillServiceAsync autoFormFillService =
(AutoFormFillServiceAsync) GWT.create (AutoFormFillService.class);
- 创建用于设置和清除表单字段值的私有方法。我们将从构造函数中设置的事件处理程序中使用这些方法。
private void setValues(HashMap values)
{
if (values.size() > 0)
{
firstName.setText((String) values.get("first name"));
lastName.setText((String) values.get("last name"));
address.setText((String) values.get("address"));
city.setText((String) values.get("city"));
state.setText((String) values.get("state"));
zip.setText((String) values.get("zip"));
phone.setText((String) values.get("phone"));
}
else
{
clearValues();
}
}
private void clearValues()
{
firstName.setText(" ");
lastName.setText(" ");
address.setText(" ");
city.setText(" ");
state.setText(" ");
zip.setText(" ");
phone.setText(" ");
}
- 创建用于检索不同标签的访问器方法。当我们从服务中检索信息时,我们将使用这些方法来获取标签并设置其值。
public Label getAddressLbl()
{
return addressLbl;
}
public Label getCityLbl()
{
return cityLbl;
}
public Label getCustIDLbl()
{
return custIDLbl;
}
public Label getFirstNameLbl()
{
return firstNameLbl;
}
public Label getLastNameLbl()
{
return lastNameLbl;
}
public Label getPhoneLbl()
{
return phoneLbl;
}
public Label getStateLbl()
{
return stateLbl;
}
public Label getZipLbl()
{
return zipLbl;
}
- 为检索不同的文本框创建访问器方法。当我们从服务中检索信息时,我们将使用这些方法来获取文本框并设置其值。
public TextBox getAddress()
{
return address;
}
public TextBox getCity()
{
return city;
}
public TextBox getCustID()
{
return custID;
}
public TextBox getFirstName()
{
return firstName;
}
public TextBox getLastName()
{
return lastName;
}
public TextBox getPhone()
{
return phone;
}
public TextBox getState()
{
return state;
}
public TextBox getZip()
{
return zip;
}
- 在
AutoFormFillPanel的构造函数中,创建一个新的VerticalPanel,我们将使用它作为添加到用户界面的小部件的容器。还要创建服务目标并设置其入口点。
ServiceDefTarget endpoint = (ServiceDefTarget)
autoFormFillService;
endpoint.setServiceEntryPoint("/Samples/autoformfill");
- 同样在构造函数中,创建一个名为
itemPanel的HorizontalPanel,并将每个表单字段的小部件添加到其中。例如,这是我们如何将customerID字段添加到itemPanel,设置其样式,并将此itemPanel添加到workPanel,这是我们之前创建的用于容纳用户界面小部件的主容器。对于每个表单字段,您将创建一个新的HorizontalPanel并将其添加到workPanel。对于我们拥有的每个表单字段,重复此操作。
HorizontalPanel itemPanel = new HorizontalPanel();
itemPanel.setStyleName("autoFormItem-Panel");
custIDLbl.setStyleName("autoFormItem-Label");
itemPanel.add(custIDLbl);
custID.setStyleName("autoFormItem-Textbox");
itemPanel.add(custID);
workPanel.add(itemPanel);
- 在相同的构造函数中,向
custID文本框添加键盘监听器,并在事件处理程序中调用服务以检索键入客户 ID 的客户信息。从服务调用的返回值设置表单字段的值。
custID.addKeyboardListener(new KeyboardListener()
{
public void onKeyDown(Widget sender,
char keyCode, int modifiers)
{
}
public void onKeyPress(Widget sender,
char keyCode, int modifiers)
{
}
public void onKeyUp(Widget sender, char
keyCode, int modifiers)
{
if (custID.getText().length() > 0)
{
AsyncCallback callback = new
AsyncCallback()
{
public void onSuccess
(Object result)
{
setValues((HashMap) result);
}
};
autoFormFillService.getFormInfo
(custID.getText(), callback);
}
else
{
clearValues();
}
}
public void onFailure(Throwable caught)
{
Window.alert("Error while calling the
Auto Form Fill service."
+ caught.getMessage());
}
});
- 最后,在构造函数中,创建一个小的信息面板,显示关于此应用程序的描述性文本,以便在我们的
Samples应用程序的可用示例列表中选择此示例时显示此文本。将信息面板和工作面板添加到一个停靠面板中,并初始化小部件。
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML(
"<div class='infoProse'>This example
demonstrates how to automatically fill a
form by retrieving the data from the server
asynchronously. Start typing a customer ID
in the provided field, and corresponding
values for that customer are retrieved
asynchronously from the server and the form
filled for you.</div>"));
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
- 将服务添加到
Samples应用程序的模块文件Samples.gwt.xml中,该文件位于com.packtpub.gwtbook.samples包中。
<servlet path="/autoformfill" class=
"com.packtpub.gwtbook.samples.server. AutoFormFillServiceImpl"/>
当用户在我们的应用程序中输入已知的CustomerID(在本例中为 1111)时,应用程序的外观如下:

刚刚发生了什么?
我们创建一个包含存储在HashMap数据结构中的客户数据的服务。在一个真实的应用程序中,这些数据通常来自外部数据源,比如数据库。对于每个客户,我们创建一个包含客户信息字段存储为键值对的 map。然后,将这个客户 map 添加到一个主HashMap中,使用customerID作为键。这样,当我们提供键时,也就是customerID时,我们更容易检索到正确的客户信息。
HashMap customer2 = new HashMap();
customer2.put("first name", "Jane");
customer2.put("last name", "Customer");
customer2.put("address", "456 elm street");
customer2.put("city", "Miami");
customer2.put("state", "FL");
customer2.put("zip", "24156");
customer2.put("phone", "817-123-4567");
formInfo.put("2222", customer2);
当用户界面在浏览器中加载时,用户将看到一个包含与客户相关的字段的页面。用户需要在提供的文本框中输入一个唯一的客户 ID。在这个示例应用程序中只有三个已知的客户 ID——1111、2222 和 3333。我们在这里使用客户 ID 作为客户信息的键,但根据应用程序的要求,您也可以使用社会安全号码或任何其他唯一 ID。当用户在文本框中输入客户 ID,例如 1111,事件处理程序onKeyUp()被触发。在事件处理程序中,我们调用AutoFormFillService中的getFormInfo()方法,并将输入的文本作为参数传递。getFormInfo()方法搜索给定客户 ID 的客户信息,并将信息作为HashMap返回。如果由于未知 ID 而找不到信息,我们将返回一个空的 map。从这个 map 中检索值,并通过调用setValues()填充相应的字段。
firstName.setText((String) values.get("first name"));
lastName.setText((String) values.get("last name"));
address.setText((String) values.get("address"));
city.setText((String) values.get("city"));
state.setText((String) values.get("state"));
zip.setText((String) values.get("zip"));
phone.setText((String) values.get("phone"));
这是为用户与我们的系统交互提供良好体验的一种简单但非常强大和有效的方式。
可排序表格
表格可能是在应用程序中显示业务数据最常见的方式。它们为所有用户所熟知,并提供了一种通用的查看数据的方式。在网页上传统上很难实现这一点。GWT 为我们提供了在应用程序中轻松快速地提供这种功能的能力。我们将创建一个包含表格的应用程序,其中的行可以通过点击列标题以升序或降序排序。这为用户提供了更好的用户体验,因为用户可以修改显示的数据顺序以满足他们的需求。GWT 提供的表格小部件没有内置的方法来提供这种功能,但是 GWT 为我们提供了足够的工具来轻松地为表格添加支持。请记住,这只是使用 GWT 创建可排序的表格的一种方式。
行动时间——排序表格行
我们不需要为这个应用程序创建一个服务,因为数据的排序是在客户端上进行的。我们将创建一个包含表格种子数据的应用程序,然后添加支持通过点击列标题对数据进行排序。
- 在
com.packtpub.gwtbook.samples.client.panels包中创建一个名为SortableTablesPanel.java的新的 Java 文件。我们将为这个类添加支持,使包含的表格可以通过点击列标题进行排序。首先创建一个CustomerData类,它将代表表格中的一行,并为每个字段创建访问器。
private class CustomerData
{
private String firstName;
private String lastName;
private String country;
private String city;
public CustomerData(String firstName, String lastName,
String city, String country)
{
this.firstName = firstName;
this.lastName = lastName;
this.country = country;
this.city = city;
}
public String getCountry()
{
return country;
}
public String getCity()
{
return city;
}
public String getFirstName()
{
return firstName;
}
public String getLastName()
{
return lastName;
}
}
- 创建一个名为
customerData的ArrayList来存储客户数据。创建变量来存储排序方向、表格中列的标题、用于排序的临时数据结构,以及用于显示客户数据的FlexTable。
private int sortDirection = 0;
private FlexTable sortableTable = new FlexTable();
private String[] columnHeaders = new String[]
{ "First Name", "Last Name", "City", "Country" };
private ArrayList customerData = new ArrayList();
private HashMap dataBucket = new HashMap();
private ArrayList sortColumnValues = new ArrayList();
- 在
SortableTablesPanel的构造函数中,创建一个新的VerticalPanel,我们将使用它作为添加到用户界面的小部件的容器。设置表格的样式,并设置表格的列标题。
VerticalPanel workPanel = new VerticalPanel();
sortableTable.setWidth(500 + "px");
sortableTable.setStyleName("sortableTable");
sortableTable.setBorderWidth(1);
sortableTable.setCellPadding(4);
sortableTable.setCellSpacing(1);
sortableTable.setHTML(0, 0, columnHeaders[0]
+ " <img border='0' src='images/blank.gif'/>");
sortableTable.setHTML(0, 1, columnHeaders[1]
+ " <img border='0' src='images/blank.gif'/>");
sortableTable.setHTML(0, 2, columnHeaders[2]
+ " <img border='0' src='images/blank.gif'/>");
sortableTable.setHTML(0, 3, columnHeaders[3]
+ " <img border='0' src='images/blank.gif'/>");
- 同样在构造函数中,向
customerData列表添加五个客户。将此列表中的数据添加到表格中,并在表格上设置一个监听器,以在点击第一列时对行进行排序。我们将在表格中显示这些客户的列表,然后在点击列标题时对表格进行排序。
customerData.add(new CustomerData("Rahul","Dravid","Bangalore",
"India"));
customerData.add(new CustomerData("Nat", "Flintoff", "London",
"England"));
customerData.add(new CustomerData("Inzamamul", "Haq", "Lahore",
"Pakistan"));
customerData.add(new CustomerData("Graeme", "Smith", "Durban",
"SouthAfrica"));
customerData.add(new CustomerData("Ricky", "Ponting", "Sydney",
"Australia"));
int row = 1;
for (Iterator iter = customerData.iterator(); iter.hasNext();)
{
CustomerData element = (CustomerData) iter.next();
sortableTable.setText(row, 0, element.getFirstName());
sortableTable.setText(row, 1, element.getLastName());
sortableTable.setText(row, 2, element.getCity());
sortableTable.setText(row, 3, element.getCountry());
row++;
}
RowFormatter rowFormatter = sortableTable.getRowFormatter();
rowFormatter.setStyleName(0, "tableHeader");
sortableTable.addTableListener(new TableListener()
{
public void onCellClicked(SourcesTableEvents sender, int row,
int cell)
{
if (row == 0)
{
sortTable(row, cell);
}
}
});
- 最后,在构造函数中,将表格添加到工作面板。创建一个小的信息面板,显示关于此应用程序的描述性文本,以便在
Samples应用程序的可用样本列表中选择此样本时,我们可以显示此文本。将信息面板和工作面板添加到一个停靠面板,并初始化小部件。
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML(
"<div class='infoProse'>This example shows
how to create tables whose rows can be
sorted by clicking on the column
header.</div>"));
workPanel.setStyleName("sortableTables-Panel");
workPanel.add(sortableTable);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
sortTable(0, 0);
initWidget(workPane);
- 为表格的标题重新绘制一个私有方法。这是一个很好的方法,可以重新绘制表格列标题,以便我们可以更改标题中显示的图像,以匹配当前的排序方向。
private void redrawColumnHeaders(int column)
{
if (sortDirection == 0)
{
sortableTable.setHTML(0, column, columnHeaders[column]
+ " <img border='0' src='images/desc.gif'/>");
}
else if (sortDirection == 1)
{
sortableTable.setHTML(0, column, columnHeaders[column]
+ " <img border='0' src='images/asc.gif'/>");
}
else
{
sortableTable.setHTML(0, column, columnHeaders[column]
+ " <img border='0' src='images/blank.gif'/>");
}
for (int i = 0; i < 4; i++)
{
if (i != column)
{
sortableTable.setHTML(0, i, columnHeaders[i]
+ " <img border='0' src='images/blank.gif'/>");
}
}
}
- 添加一个私有方法,在更改排序顺序时重新绘制整个表格。
private void redrawTable()
{
int row = 1;
for (Iterator iter = sortColumnValues.iterator();
iter.hasNext();)
{
String key = (String) iter.next();
CustomerData custData = (CustomerData) dataBucket.get(key);
sortableTable.setText(row, 0, custData.getFirstName());
sortableTable.setText(row, 1, custData.getLastName());
sortableTable.setText(row, 2, custData.getCity());
sortableTable.setText(row, 3, custData.getCountry());
row++;
}
}
- 添加一个私有方法,可以按升序或降序对数据进行排序,并重新绘制带有排序行的表格。我们正在使用
Collections类提供的 sort 方法对数据进行排序,但也可以修改为使用Comparator类来比较两个数据,并将其用于排序。
public void sortTable(int row, int cell)
{
dataBucket.clear();
sortColumnValues.clear();
for (int i = 1; i < customerData.size() + 1; i++)
{
dataBucket.put(sortableTable.getText(i, cell), new
CustomerData(
sortableTable.getText(i, 0), sortableTable.getText(i, 1),
sortableTable.getText(i, 2), sortableTable.getText
(i, 3)));
sortColumnValues.add(sortableTable.getText(i, cell));
}
if (sortDirection == 0)
{
sortDirection = 1;
Collections.sort(sortColumnValues);
}
else
{
sortDirection = 0;
Collections.reverse(sortColumnValues);
}
redrawColumnHeader(cell);
resetColumnHeaders(cell);
redrawTable();
}
这是应用程序的屏幕截图。您可以点击任何列标题来对数据进行排序。

刚刚发生了什么?
我们创建了一个CustomerData类来表示FlexTable中的每一行。然后我们创建一些客户数据,并将其存储在ArrayList中。
customerData.add(new CustomerData("Rahul", "Dravid", "Bangalore",
"India"));
将此列表中的数据添加到表格中。我们需要指定行号和列号,以便将元素添加到表格中。
CustomerData element = (CustomerData) iter.next();
sortableTable.setText(row, 0, element.getFirstName());
sortableTable.setText(row, 1, element.getLastName());
sortableTable.setText(row, 2, element.getCity());
sortableTable.setText(row, 3, element.getCountry());
列标题包含在零行中,表格数据从第 1 行开始。我们通过设置该特定单元格的 HTML 来添加列标题,如下所示:
sortableTable.setHTML(0, 0, columnHeaders[0] + " <img border='0' src='images/blank.gif'/>");
这使我们能够向单元格添加一小段 HTML,而不仅仅是设置纯文本。我们添加列标题的文本以及一个带有空白图像文件的img标签。列标题旁边没有图像的列标题在视觉上向用户指示,该特定列没有指定排序顺序。当我们点击列标题时,我们将修改此图像以使用升序或降序图标。注册了一个事件处理程序来监听表格上的点击。GWT 不包含在某人点击特定单元格时注册处理程序的机制,因此我们使用通用表格点击监听器,并检查点击是否在零行,即包含列标题的行。如果用户确实点击了列标题,我们将继续对表格进行排序。
真正的魔法发生在sortTable()方法中。创建一个临时的名为dataBucket的HashMap来存储来自表格的行,每行都以被点击的列中的值为键,以及一个临时的名为sortColumnValues的ArrayList,它存储被点击的列中的列值。这意味着sortColumnValues列表包含作为dataBucket映射中键的值。
for (int i = 1; i < customerData.size() + 1; i++)
{
dataBucket.put(sortableTable.getText(i, cell), new CustomerData(
sortableTable.getText(i, 0), sortableTable.getText(i, 1),
sortableTable.getText(i, 2), sortableTable.getText(i, 3)));
sortColumnValues.add(sortableTable.getText(i, cell));
}
我们检查sortDirection变量的值,并根据该值对sortColumnValues列表进行升序或降序排序,以包含正确顺序的列值。使用Collections类的内置sort()和reverseSort()方法来提供排序机制。
if (sortDirection == 0)
{
sortDirection = 1;
Collections.sort(sortColumnValues);
}
else
{
sortDirection = 0;
Collections.reverse(sortColumnValues);
}
然后重新绘制表格列标题,以便被点击的列将具有正确的排序顺序的图标,而所有其他列标题只有纯文本和空白图像。最后,我们通过遍历sortColumnValues列表并从dataBucket中检索关联的CustomerData对象,将其作为表格中的一行添加,重新绘制表格。
这个应用程序展示了 GWT 框架提供的巨大能力,使您能够操纵表格以扩展其功能。GWT 提供了不同类型的表格来构建用户界面:
-
FlexTable: 一个按需创建单元格的表格。甚至可以有包含不同数量单元格的行。当您向其添加行和列时,此表格会根据需要扩展。
-
Grid: 一个可以包含文本、HTML 或子小部件的表格。但是,它必须明确地创建,指定所需的行数和列数。
我们将在本章和本书的其余部分中广泛使用这两个表小部件。
动态列表
我们将创建一个应用程序,使用动态列表向用户呈现一种过滤搜索条件的方式。在本节中,我们将创建动态表格,这将使我们能够在选择主表中的项目时填充子表格。我们将通过使用 GWT 的 AJAX 支持来实现这一点,并且只显示与主表中选择相关的子表中的项目。这个应用程序将使得轻松浏览和过滤搜索条件成为可能。在这个示例应用程序中,我们将使用户能够选择汽车制造商,这将自动填充第二个列表,其中包含该制造商生产的所有汽车品牌。当客户进一步在这些品牌列表中选择项目时,第三个列表将自动填充所选品牌的汽车型号。通过这种方式,用户可以交互式地选择和浏览搜索条件,以用户友好和直观的方式,而无需提交数据和刷新页面来呈现这些信息的一部分。
行动时间-过滤搜索条件
作为这个应用程序的一部分,我们还将创建一个服务,它将提供有关制造商、品牌和型号的信息,并创建一个用户界面,异步地从服务中检索这些信息,以显示给用户。
- 在
com.packtpub.gwtbook.samples.client包中创建一个名为DynamicListsService.java的新的 Java 文件。定义一个DynamicListsService接口,其中包含检索有关制造商、品牌和型号信息的方法:
public interface DynamicListsService extends RemoteService
{
public List getManufacturers();
public List getBrands(String manufacturer);
public List getModels(String manufacturer, String brand);
}
- 在
com.packtpub.gwtbook.samples.client包中创建一个名为DynamicListsServiceAsync.java的新的 Java 文件。定义一个DynamicListsServiceAsync接口:
public interface DynamicListsServiceAsync
{
public void getManufacturers(AsyncCallback callback);
public void getBrands(String manufacturer,
AsyncCallback callback);
public void getModels(String manufacturer, String brand,
AsyncCallback callback);
}
- 在
com.packtpub.gwtbook.samples.server包中创建一个名为DynamicListsServiceImpl.java的新的 Java 文件。定义一个扩展RemoteServiceServlet并实现先前创建的DynamicListsService接口的DynamicListsServiceImpl类。这个类将返回有关制造商、品牌和型号的信息。创建一个名为Manufacturer的类,封装有关每个制造商的信息,包括它们提供的汽车品牌和型号。
private class Manufacturer
{
private HashMap brands = new HashMap();
public Manufacturer(HashMap brands)
{
this.brands = brands;
}
public HashMap getBrands()
{
return brands;
}
}
- 创建一个私有方法,将制造商信息加载到
HashMap中。制造商的数据将稍后加载到第一个表中。当用户界面启动时,制造商表是唯一具有数据的表,为使用应用程序提供了起点。
private void loadData()
{
ArrayList brandModels = new ArrayList();
brandModels.add("EX");
brandModels.add("DX Hatchback");
brandModels.add("DX 4-Door");
HashMap manufacturerBrands = new HashMap();
manufacturerBrands.put("Civic", brandModels);
brandModels = new ArrayList();
brandModels.add("SX");
brandModels.add("Sedan");
manufacturerBrands.put("Accord", brandModels);
brandModels = new ArrayList();
brandModels.add("LX");
brandModels.add("Deluxe");
manufacturerBrands.put("Odyssey", brandModels);
Manufacturer manufacturer = new
Manufacturer(manufacturerBrands);
data.put("Honda", manufacturer);
brandModels = new ArrayList();
brandModels.add("LXE");
brandModels.add("LX");
manufacturerBrands = new HashMap();
manufacturerBrands.put("Altima", brandModels);
brandModels = new ArrayList();
brandModels.add("NX");
brandModels.add("EXE");
manufacturerBrands.put("Sentra", brandModels);
manufacturer = new Manufacturer(manufacturerBrands);
data.put("Nissan", manufacturer);
brandModels = new ArrayList();
brandModels.add("E300");
brandModels.add("E500");
manufacturerBrands = new HashMap();
manufacturerBrands.put("E-Class", brandModels);
brandModels = new ArrayList();
brandModels.add("C250");
brandModels.add("C300");
manufacturerBrands.put("C-Class", brandModels);
manufacturer = new Manufacturer(manufacturerBrands);
data.put("Mercedes", manufacturer);
}
- 实现用于检索制造商列表的服务方法。
public ArrayList getManufacturers()
{
ArrayList manufacturersList = new ArrayList();
for (Iterator iter=data.keySet().iterator(); iter.hasNext();)
{
manufacturersList.add((String) iter.next());
}
return manufacturersList;
}
- 实现用于检索制造商提供的品牌列表的服务方法。
public ArrayList getBrands(String manufacturer)
{
ArrayList brandsList = new ArrayList();
for (Iterator iter = ((Manufacturer)data.get(manufacturer))
.getBrands().keySet().iterator(); iter.hasNext();)
{
brandsList.add((String) iter.next());
}
return brandsList;
}
- 实现用于检索特定品牌制造商提供的型号的服务方法。
public ArrayList getModels(String manufacturer, String brand)
{
ArrayList modelsList = new ArrayList();
Manufacturer mfr = (Manufacturer) data.get(manufacturer);
HashMap mfrBrands = (HashMap) mfr.getBrands();
for (Iterator iter = ((ArrayList)
mfrBrands.get(brand)).iterator(); iter.hasNext();)
{
modelsList.add((String) iter.next());
}
return modelsList;
}
- 在
com.packtpub.gwtbook.samples.client.panels包中创建一个名为DynamicListsPanel.java的新的 Java 文件,为这个应用程序创建用户界面。创建三个 Grid 小部件来保存制造商、品牌和型号信息,并将它们添加到主面板中。创建我们将要调用的服务类。
Grid manufacturers = new Grid(5, 1);
Grid brands = new Grid(5, 1);
Grid models = new Grid(5, 1);
final DynamicListsServiceAsync dynamicListsService =
(DynamicListsServiceAsync) GWT.create (DynamicListsService.class);
- 添加一个用于清除面板的私有方法。
public void clearSelections(Grid grid, boolean clearData)
{
for (int i = 0; i < grid.getRowCount(); i++)
{
if (clearData)
{
grid.setText(i, 0, " ");
}
}
}
- 在
DynamicListsPanel的构造函数中,创建一个新的HorizontalPanel,我们将用它作为添加到用户界面的小部件的容器。同时,创建服务目标并设置其入口点。
HorizontalPanel workPanel = new HorizontalPanel();
ServiceDefTarget endpoint = (ServiceDefTarget)
dynamicListsService;
endpoint.setServiceEntryPoint("/Samples/dynamiclists");
- 在同一个构造函数中,添加一个事件处理程序来监听对“选择制造商”表格的点击。
manufacturers.addTableListener(new TableListener()
{
public void onCellClicked
(SourcesTableEvents sender,
int row, int cell)
{
clearSelections(manufacturers,
false);
clearSelections(brands, true);
clearSelections(models, true);
selectedManufacturer = row;
AsyncCallback callback = new
AsyncCallback()
{
public void onSuccess(Object
result)
{
brands.clear();
int row = 0;
for (Iterator iter =
((ArrayList) result).
iterator();
iter.hasNext();)
{
brands.setText(row++, 0,
(String) iter.next());
}
}
public void onFailure(Throwable
caught)
{
Window.alert("Error calling
the Dynamic Lists service to
get the brands." +
caught.getMessage());
}
};
dynamicListsService.getBrands
(manufacturers.getText(row,
cell),callback);
}
});
- 在同一个构造函数中,添加一个事件处理程序来监听对“选择品牌”表格的点击。
brands.addTableListener
(new TableListener()
{
public void onCellClicked
(SourcesTableEvents sender, int row, int cell)
{
clearSelections(brands, false);
clearSelections(models, true);
AsyncCallback callback = new
AsyncCallback()
{
public void onSuccess(Object result)
{
models.clear();
int row = 0;
for (Iterator iter = ((ArrayList)
result).iterator(); iter.hasNext();)
{
models.setText(row++, 0, (String)
iter.next());
}
}
public void onFailure(Throwable caught)
{
Window.alert("Error calling the Dynamic
Lists service to get the models." +
caught.getMessage());
}
};
dynamicListsService.getModels
(manufacturers.getText
(selectedManufacturer, cell),
brands.getText(row, cell), callback);
}
});
- 在构造函数中,还要添加一个监听器,以便在选择车型时清除选择。在应用程序启动时,加载“选择制造商”表格的数据。
models.addTableListener(new TableListener()
{
public void onCellClicked
(SourcesTableEvents sender, int row,
int cell)
{
clearSelections(models, false);
models.getCellFormatter()
.setStyleName(row, cell,
"dynamicLists-Selected");
}
});
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
int row = 0;
for (Iterator iter = ((ArrayList) result).iterator(); iter.hasNext();)
{
manufacturers.setText(row++, 0, (String) iter.next());
}
}
public void onFailure(Throwable caught)
{
Window.alert("Error calling the Dynamic Lists service to
get the manufacturers." + caught.getMessage());
}
};
dynamicListsService.getManufacturers(callback);
- 在构造函数中,创建一个名为
itemPanel的VerticalPanel,并将每个表格及其相关的标签添加到其中。为三个表格创建一个itemPanel,设置样式,并将它们添加到workPanel中。
VerticalPanel itemPanel = new VerticalPanel();
Label itemLabel = new Label("Select Manufacturer");
itemLabel.setStyleName("dynamicLists-Label");
itemPanel.add(itemLabel);
itemPanel.add(manufacturers);
workPanel.add(itemPanel);
itemPanel = new VerticalPanel();
itemLabel = new Label("Select Brand");
itemLabel.setStyleName("dynamicLists-Label");
itemPanel.add(itemLabel);
itemPanel.add(brands);
workPanel.add(itemPanel);
itemPanel = new VerticalPanel();
itemLabel = new Label("Models");
itemLabel.setStyleName("dynamicLists-Label");
itemPanel.add(itemLabel);
itemPanel.add(models);
workPanel.add(itemPanel);
manufacturers.setStyleName("dynamicLists-List");
brands.setStyleName("dynamicLists-List");
models.setStyleName("dynamicLists-List");
workPanel.setStyleName("dynamicLists-Panel");
- 最后,在构造函数中,创建一个小的信息面板,显示关于这个应用程序的描述性文本,这样当我们在
Samples应用程序的可用示例列表中选择此样本时,我们可以显示这个文本。将信息面板和工作面板添加到一个停靠面板中,并设置小部件。
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML(
"<div class='infoProse'>This example
demonstrates the creation of dynamic
lists. You select an item from the first
list and corresponding items are retrieved
asynchronously from the server to display
in the second list. You can then select an
item in the second list to get another
selection of items. In this particular
example, we retrieve car brand by
manufacturer, and then get and display the
specific models for the selected
brand.</div>"));
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
- 将服务添加到
Samples应用程序的模块文件中——com.packtpub.gwtbook.samples包中的Samples.gwt.xml。
<servlet path="/dynamiclists" class=
"com.packtpub.gwtbook.samples.server.DynamicListsServiceImpl"/>
这是一个应用程序的截图,当我们选择了其中一个制造商——奔驰,和它的一个品牌——E 级时:

刚刚发生了什么?
我们创建了一个制造商对象的列表,每个制造商一个。每个制造商对象都包含一个名为品牌的HashMap,其中包含该特定品牌的车型的ArrayList。我们刚刚创建的这个数据结构包含了关于制造商提供的品牌和车型的所有信息。在实际应用中,这些数据通常会从企业数据源中检索出来。例如,这是我们如何构建奔驰制造商的数据:
brandModels = new ArrayList();
brandModels.add("E300");
brandModels.add("E500");
manufacturerBrands = new HashMap();
manufacturerBrands.put("E-Class", brandModels);
brandModels = new ArrayList();
brandModels.add("C250");
brandModels.add("C300");
manufacturerBrands.put("C-Class", brandModels);
manufacturer = new Manufacturer(manufacturerBrands);
data.put("Mercedes", manufacturer);
然后,我们实现了接口中的三个服务方法,以返回制造商列表、给定制造商的品牌列表,最后是给定制造商和品牌的车型列表。这些方法中的每一个都导航到制造商对象,并检索并返回包含必要信息的列表。当我们请求给定品牌和制造商的车型列表时,服务方法的实现通过导航制造商列表返回列表,如下所示:
Manufacturer mfr = (Manufacturer) data.get(manufacturer);
HashMap mfrBrands = (HashMap) mfr.getBrands();
for (Iterator iter = ((ArrayList) mfrBrands.get(brand)).iterator();
iter.hasNext();)
{
modelsList.add((String) iter.next());
}
return modelsList;
用户界面由三个网格小部件组成。网格是另一种可以在其单元格中包含文本、HTML 或子小部件的表格小部件。当应用程序初始化时,首先从DynamicListsService中检索制造商列表,然后用数据填充制造商网格。注册了一个事件处理程序来监听网格中的点击。当制造商网格中的项目被点击时,我们首先清除品牌网格,然后调用服务的getBrands()方法,并用检索到的信息加载品牌网格。当用户通过点击在品牌网格中选择一个项目时,我们首先清除车型网格,然后调用服务的getModels()方法,并用检索到的信息加载车型网格。每当我们在任何网格中进行选择时,我们都能够使用 GWT 检索到所有这些信息,而无需进行任何页面刷新或提交!
Flickr 风格的可编辑标签
Flickr(flickr.com/)是互联网上最具创新性的 Web 2.0 网站之一。其使用 AJAX 使得这个网站非常愉快。一个典型的例子是在您添加到 flickr 帐户的任何图像下方显示的标签小部件。它看起来像一个简单的标签,但当您将光标悬停在其上时,它会改变颜色,表明它不仅仅是一个标签。当您单击它时,它会转换为一个文本框,您可以在其中编辑标签中的文本!您甚至可以获得按钮来使您的更改持久化或取消以放弃更改。保存或取消后,它会再次转换为标签。试一试。这真的很棒!这是将多个 HTML 控件-标签、文本框和按钮-组合成一个复合控件的绝佳方式,可以节省网页上的宝贵空间,同时以非常用户友好的方式提供必要的功能。在本节中,我们将使用 GWT 中可用的小部件重新创建 flickr 风格的标签。
行动时间-自定义可编辑标签
我们将创建一个标签,当您单击它时会动态转换为可编辑的文本框。它还将为您提供保存更改或丢弃更改的能力。如果您修改文本并保存更改,则标签文本将更改,否则原始文本将保留,并且文本框将转换回标签。这是一个非常创新的用户界面,您真的需要使用它来欣赏它!
- 在
com.packtpub.gwtbook.samples.client.panels包中创建一个名为FlickrEditableLabelPanel.java的新 Java 文件。为用户界面创建一个图像、一个标签、一个文本框和两个按钮。
private Label originalName;
private String originalText;
private Button saveButton;
private Button cancelButton;
private Image image = new Image("images/sample.jpg");
private Label orLabel = new Label("or");
- 创建一个私有方法来显示文本框以及按钮,同时隐藏标签。这将基本上将标签转换为带有按钮的文本框!
private void ShowText()
{
originalText = originalName.getText();
originalName.setVisible(false);
saveButton.setVisible(true);
orLabel.setVisible(true);
cancelButton.setVisible(true);
newName.setText(originalText);
newName.setVisible(true);
newName.setFocus(true);
newName.setStyleName("flickrPanel-textBox-edit");
}
- 在
FlickrEditableLabelPanel的构造函数中,创建一个事件处理程序,以侦听标签的单击,并调用上述方法。
originalName.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
ShowText();
}
});
- 此外,在构造函数中,创建一个事件处理程序,以侦听鼠标悬停并修改标签样式,为用户提供视觉提示,以便单击标签。
originalName.addMouseListener(new MouseListener()
{
public void onMouseDown
(Widget sender, int x, int y)
{
}
public void onMouseEnter
(Widget sender)
{
originalName.setStyleName
"flickrPanel-label-hover");
}
public void onMouseLeave
(Widget sender)
{
originalName.setStyleName
("flickrPanel-label");
}
public void onMouseMove
(Widget sender, int x, int y)
{
}
public void onMouseUp
(Widget sender, int x, int y)
{
}
});
- 在构造函数中为输入新名称创建一个文本框,并创建一个事件处理程序,以侦听文本框中的焦点的回车键和 ESC 键,并保存更改或取消更改。
newName.addKeyboardListener(new KeyboardListenerAdapter()
{
public void onKeyPress(Widget sender, char keyCode, int
modifiers)
{
switch (keyCode)
{
case KeyboardListenerAdapter. KEY_ENTER:saveChange();
break;
case KeyboardListenerAdapter. KEY_ESCAPE:cancelChange();
break;
}
}
});
- 在构造函数中创建一个事件处理程序,以侦听保存按钮的单击并保存更改。
saveButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
saveChange();
}
});
- 在构造函数中创建一个事件处理程序,以侦听取消按钮的单击并丢弃所做的任何更改。
cancelButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
cancelChange();
}
});
- 在构造函数中,设置应用程序首次加载时小部件的可见性。当首次显示用户界面时,我们希望显示标签,而隐藏其他所有内容。
originalName.setVisible(true);
newName.setVisible(false);
saveButton.setVisible(false);
orLabel.setVisible(false);
cancelButton.setVisible(false);
- 最后,在构造函数中,创建一个名为
buttonPanel的HorizontalPanel,并将我们创建的小部件添加到其中。创建一个名为workPanel的VerticalPanel,并将buttonPanel添加到其中。创建一个小信息面板,显示有关此应用程序的描述性文本,以便在我们的Samples应用程序的可用样本列表中选择此样本时显示此文本。将信息面板和工作面板添加到一个停靠面板,并初始化小部件。
HorizontalPanel buttonPanel = new HorizontalPanel();
buttonPanel.setStyleName("flickrPanel-buttonPanel");
buttonPanel.add(saveButton);
buttonPanel.add(orLabel);
buttonPanel.add(cancelButton);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
VerticalPanel workPanel = new VerticalPanel();
workPanel.setStyleName("flickrPanel");
workPanel.add(image);
workPanel.add(originalName);
workPanel.add(newName);
workPanel.add(buttonPanel);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
- 创建一个私有方法来显示标签并隐藏文本。现在我们正在隐藏标签,并显示我们漂亮的文本编辑界面,其中包括文本框和用于保存或放弃所做更改的按钮。
private void showLabel()
{
originalName.setVisible(true);
saveButton.setVisible(false);
orLabel.setVisible(false);
cancelButton.setVisible(false);
newName.setVisible(false);
}
- 创建一个私有方法来保存更改。
private void saveChange()
{
originalName.setText(newName.getText());
showLabel();
// This is where you can call an RPC service to update
// a db or call some other service to propagate
// the change. In this example we just change the
// text of the label.
}
- 创建一个丢弃更改的方法。
public void cancelChange()
{
originalName.setText(originalText);
showLabel();
}
当您访问页面时,应用程序的外观如下:

如果单击图像下方的标签,它将转换为带有保存和取消按钮的文本框。您可以修改文本并保存更改,或单击取消以将其更改回标签。

刚刚发生了什么?
我们创建了一个用户界面,其中包括一个带有标签的图像,一个文本框,一个保存按钮,一个标签和一个取消按钮。事件处理程序被注册用来监听标签的点击。当用户点击标签时,事件处理程序被触发,我们隐藏标签,并设置文本框和按钮可见。
originalText = originalName.getText();
originalName.setVisible(false);
saveButton.setVisible(true);
orLabel.setVisible(true);
cancelButton.setVisible(true);
newName.setText(originalText);
newName.setVisible(true);
newName.setFocus(true);
newName.setStyleName("flickrPanel-textBox-edit");
如果我们修改文本并点击保存,监听保存按钮点击的事件处理程序将保存文本作为标签的值,并再次显示标签并隐藏所有其他小部件。
originalName.setText(newName.getText());
originalName.setVisible(true);
saveButton.setVisible(false);
orLabel.setVisible(false);
cancelButton.setVisible(false);
newName.setVisible(false);
如果我们通过点击取消按钮放弃更改,监听取消按钮点击的事件处理程序将显示标签并隐藏所有其他小部件。
originalName.setText(originalText);
originalName.setVisible(true);
saveButton.setVisible(false);
orLabel.setVisible(false);
cancelButton.setVisible(false);
newName.setVisible(false);
在这个应用程序中,我们没有调用任何服务来传播更改到服务器端的过程,但我们可以很容易地通过添加代码来调用服务,以保存对文本所做的更改。
摘要
在本章中,我们看了创建一个实时搜索应用程序。然后我们看了创建一个密码强度检查器。此外,我们创建了可以从服务器自动填充信息的表单。我们还创建了对表进行排序的应用程序。然后在创建类似 flickr 风格的可编辑标签之前,我们创建了根据用户选择动态填充列表的应用程序。
在下一章中,我们将学习创建响应式复杂界面,使用 GWT 的一些更高级的功能。
第五章:响应式复杂界面
在本章中,我们将创建一些演示 GWT 高级功能的用户界面。
我们将要解决的任务是:
-
可分页表格
-
可编辑的树节点
-
日志监视
-
便利贴
-
拼图游戏
可分页表格
在本章中,我们将开始探索更复杂的 GWT 用户界面。在当今的商业世界中,我们经常遇到一些情况,需要使用表格来显示大量数据。一次性在表格中显示所有可用数据既不是一个可行的选项,从可用性的角度来看,也不是一个实际的选择。
我们还可以潜在地锁定显示表格的浏览器,如果检索到的数据集足够大。向用户显示这些数据的更好方法是首先显示固定数量的结果,然后提供他们浏览结果的机制;这样他们可以自由地在数据中向前或向后翻页。这样做可以提供更好的用户体验,同时也可以更快地加载较小的数据集。
在本节中,我们将创建一个提供此功能的应用程序。作为示例的一部分,我们还将学习如何在 GWT 应用程序中使用嵌入式数据库。
行动时间——接口数据集
我们将创建一个应用程序,让我们以分块或分页的方式检索数据,而不是一次性获取所有数据。我们将通过查询检索前十个项目作为结果,并为用户提供一种方法,让他们可以在这些结果中向前或向后翻页。具体步骤如下:
- 在
com.packtpub.gwtbook.samples.client包中创建一个名为PageableDataService.java的新的 Java 文件。定义PageableDataService接口,其中包含一个方法,通过提供起始索引和要检索的项目数量来检索客户数据:
public interface PageableDataService extends RemoteService
{
public List getCustomerData(int startIndex, int numItems );
}
- 在
com.packtpub.gwtbook.samples.client包中创建一个名为PageableDataServiceAsync.java的新的 Java 文件,创建这个服务定义接口的异步版本:
public interface PageableDataServiceAsync
{
public void getCustomerData(int startIndex, int numItems,
AsyncCallback callback);
}
- 在
com.packtpub.gwtbook.samples.server包中创建一个名为PageableDataServiceImpl.java的新的 Java 文件,实现我们的可分页数据服务。创建一个名为customerData的私有ArrayList对象,用于存储客户数据:
private ArrayList customerData = new ArrayList();
- 如果我们使用数据库来存储数据而不是在服务中管理数据结构,将会更简单。我们将使用 HSQLDB——一个用于存储我们将在此服务中访问的数据的小型嵌入式数据库。首先,从预先填充的数据库中加载数据到列表中:
private void loadData()
{
Class.forName("org.hsqldb.jdbcDriver");
Connection conn = DriverManager.getConnection
( "jdbc:hsqldb:file:samplesdb", "sa", "");
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM users");
for (; rs.next();)
{
ArrayList customer = new ArrayList();
customer.add((String) rs.getObject(2));
customer.add((String) rs.getObject(3));
customer.add((String) rs.getObject(4));
customer.add((String) rs.getObject(5));
customer.add((String) rs.getObject(6));
customerData.add(customer);
}
st.execute("SHUTDOWN");
conn.close();
}
- 我们在服务的构造函数中调用
loadData()函数,以便在服务初始化后加载所有所需的数据并可用:
public PageableDataServiceImpl()
{
super();
loadData();
}
- 现在添加一个服务实现方法,只返回请求的数据子集:
public ArrayList getCustomerData(int startIndex, int numItems)
{
ArrayList customers = new ArrayList();
for (int i = startIndex - 1; i < (startIndex + numItems); i++)
{
customers.add((ArrayList) customerData.get(i));
}
return customers;
}
- 现在创建与可分页数据服务交互的用户界面。在
com.packtpub.gwtbook.samples.client.panels包中创建一个名为PageableDataPanel.java的新的 Java 文件。正如在上一章开头提到的,本书中创建的每个用户界面都将被添加到一个类似于 GWT 下载中作为示例项目之一的KitchenSink应用程序的示例应用程序中。这就是为什么我们将每个用户界面创建为一个扩展SamplePanel类的面板,并将创建的面板添加到示例应用程序的示例面板列表中。SamplePanel类和我们的Samples应用程序的结构在上一章开头进行了讨论。添加一个FlexTable类来显示数据,以及用于向前或向后翻页的按钮。创建一个字符串数组来存储列标题,并创建一个整数变量来存储客户数据列表的起始索引:
private FlexTable customerTable = new FlexTable();
private Button backButton = new Button("<<<");
private Button forwardButton = new Button(">>");
private String[] customerTableHeaders = new String[]
{ "Name", "City","Zip Code", "State", "Phone" };
private int startIndex = 1;
- 创建我们将用于调用服务以获取数据的服务类:
final PageableDataServiceAsync pageableDataService =
(PageableDataServiceAsync)
GWT.create(PageableDataService.class);
ServiceDefTarget endpoint = (ServiceDefTarget)
pageableDataService;
endpoint.setServiceEntryPoint(GWT.getModuleBaseURL() +
"pageabledata");
- 添加一个私有方法,在我们用数据填充表格之前清空表格:
private void clearTable()
{
for (int row=1; row<customerTable.getRowCount(); row++)
{
for (int col=0; col<customerTable.getCellCount(row); col++)
{
customerTable.clearCell(row, col);
}
}
}
- 添加一个私有方法,用于使用从服务检索的数据更新表格:
private void update(int startIndex)
{
AsyncCallback callback = new AsyncCallback()
public void onSuccess(Object result)
{
ArrayList customerData = (ArrayList) result;
int row = 1;
clearTable();
for (Iterator iter=customerData.iterator(); iter.hasNext();)
{
ArrayList customer = (ArrayList) iter.next();
customerTable.setText(row, 0, (String) customer.get(0));
customerTable.setText(row, 1, (String) customer.get(1));
customerTable.setText(row, 2, (String) customer.get(2));
customerTable.setText(row, 3, (String) customer.get(3));
customerTable.setText(row, 4, (String) customer.get(4));
row++;
}
}
public void onFailure(Throwable caught)
{
Window.alert("Error when invoking the pageable data service
: " + caught.getMessage());
}
pageableDataService.getCustomerData(startIndex, 10, callback);
}
- 在
PageableDataPanel的构造函数中,创建一个VerticalPanel对象,它将是这个用户界面的容器面板,并初始化将保存客户数据的表格:
VerticalPanel workPanel = new VerticalPanel();
customerTable.setWidth(500 + "px");
customerTable.setBorderWidth(1);
customerTable.setCellPadding(4);
customerTable.setCellSpacing(1);
customerTable.setText(0, 0, customerTableHeaders[0]);
customerTable.setText(0, 1, customerTableHeaders[1]);
customerTable.setText(0, 2, customerTableHeaders[2]);
customerTable.setText(0, 3, customerTableHeaders[3]);
customerTable.setText(0, 4, customerTableHeaders[4]);
- 创建一个内部导航栏,其中包含后退和前进按钮:
HorizontalPanel innerNavBar = new HorizontalPanel();
innerNavBar.setStyleName("pageableData-NavBar");
innerNavBar.setSpacing(8);
innerNavBar.add(backButton);
innerNavBar.add(forwardButton);
- 在构造函数中添加一个事件处理程序,以便监听后退按钮的点击:
backButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
if (startIndex >= 10)
startIndex -= 10;
update(startIndex);
}
});
- 在构造函数中添加一个事件处理程序,以便监听前进按钮的点击:
forwardButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
if (startIndex < 40)
{
startIndex += 10;
update(startIndex);
}
}
});
- 最后,在构造函数中,将客户数据表和导航栏添加到工作面板中。创建一个小的信息面板,显示关于此应用程序的描述性文本,这样当我们在
Samples应用程序的可用样本列表中选择此样本时,我们可以显示文本。将信息面板和工作面板添加到一个停靠面板,并初始化小部件。调用update()方法,这样当页面最初加载时,我们可以获取第一批客户数据并显示它:
workPanel.add(innerNavBar);
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML("<div class='infoProse'>Create lists that can be paged by fetching data from the server on demand
we go forward and backward in the list.</div>"));
workPanel.add(customerTable);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
update(1);
- 将服务添加到
Samples应用程序的模块文件Samples.gwt.xml中,位于com.packtpub.gwtbook.samples包中:
<servlet path="/Samples/pageabledata" class=
"com.packtpub.gwtbook.samples.server.PageableDataServiceImpl"/>
这是应用程序的用户界面:

单击按钮以向前或向后浏览列表。
刚刚发生了什么?
我们正在使用一个嵌入式数据库(Hypersonic SQL—HSQLDB—www.hsqldb.org),其中包含我们将浏览的客户数据,每次仅显示十个结果。使用此数据库所需的所有组件都包含在hsqldb.jar文件中。为了在 GWT 项目中使用它,我们需要确保将hsqldb.jar文件添加到 Eclipse 项目的buildpath中。然后当您运行或调试项目时,它将在classpath上可用。
使用 HSQLDB 的内存版本,这意味着数据库在与我们的 GWT 应用程序相同的 Java 虚拟机中运行。在初始化 HSQLDB 的 JDBC 驱动程序之后,我们通过指定数据库文件路径获得到名为samplesdb的数据库的连接。如果此文件不存在,它将被创建,如果存在,则数据库将被数据库引擎加载。提供的文件路径是相对于启动此 JVM 的目录的;所以在我们的情况下,数据库文件将被创建在我们项目的根目录中。
Class.forName("org.hsqldb.jdbcDriver");
Connection conn = DriverManager.getConnection
("jdbc:hsqldb:file:samplesdb", "sa", "");
从客户表中检索数据并存储在本地的ArrayList中。这个列表数据结构包含客户表中每一行的一个ArrayList。它将被用作检索信息集的基础。每个检索客户数据的请求将提供一个起始索引和要检索的项目数。起始索引告诉我们在ArrayList中的偏移量,而项目数限制了返回的结果。
应用程序的用户界面显示了一个表格和两个按钮。后退按钮通过数据集向后翻页,而前进按钮让我们向前移动列表。页面加载时,会异步调用PageableDataService接口,以获取前十个项目并在表格中显示它们。注册事件处理程序以监听两个按钮的点击。单击任一按钮都会触发调用远程服务以获取下一组项目。我们将当前显示的表格项目的起始索引存储在一个私有变量中。单击后退按钮时,该变量递减;单击前进按钮时,该变量递增。在请求下一组数据时,它作为参数提供给远程方法。来自请求的结果用于填充页面上的表格。
ArrayList customerData = (ArrayList) result;
int row = 1;
clearTable();
for (Iterator iter = customerData.iterator(); iter.hasNext();)
{
ArrayList customer = (ArrayList) iter.next();
customerTable.setText(row, 0, (String) customer.get(0));
customerTable.setText(row, 1, (String) customer.get(1));
customerTable.setText(row, 2, (String) customer.get(2));
customerTable.setText(row, 3, (String) customer.get(3));
customerTable.setText(row, 4, (String) customer.get(4));
row++;
}
我们清除表中的数据,然后通过为每一列设置文本来添加新数据。
可编辑的树节点
树控件提供了一种非常用户友好的方式来显示一组分层数据,常见的例子包括文件系统中的目录结构或者 XML 文档中的节点。GWT 提供了一个可以显示这些数据的树形小部件,但是没有提供任何修改树节点本身的方法。修改树控件中显示的节点最常见的用途之一是重命名文件和文件夹,比如在您喜欢的平台上的文件资源管理器中。我们将创建一个应用程序,演示如何通过单击节点并输入新文本来编辑树中显示的节点。这个示例还演示了扩展 GWT 以使其执行一些默认情况下不提供的功能有多么容易。
行动时间——修改节点
我们将创建一个应用程序,其中包含一个树,其行为类似于 Windows 文件资源管理器,允许我们单击节点并编辑节点的文本。步骤如下:
- 在
com.packtpub.gwtbook.samples.client.panels包中的一个名为EditableTreeNodesPanel.java的新 Java 文件中为此应用程序创建用户界面。这个类也像本书中的所有其他用户界面一样扩展了SamplePanel类。SamplePanel类扩展了Composite类,是创建多个用户界面并将它们添加到我们的Samples应用程序的简单方法,这样我们就可以以类似于 GWT 发行版中的KitchenSink示例项目的方式显示所有应用程序的列表。我们在第四章的开头部分描述了示例应用程序的结构。创建一个树、一个文本框和一个标签。最后,创建工作面板和工作面板的变量:
private Tree editableTree = new Tree();
private TreeItem currentSelection = new TreeItem();
private TextBox textbox = new TextBox();
private AbsolutePanel workPanel = new AbsolutePanel();
private DockPanel workPane = new DockPanel();
- 创建一个私有方法,用一些节点填充树:
private void initTree()
{
TreeItem root = new TreeItem("root");
root.setState(true);
int index = 100;
for (int j = 0; j < 10; j++)
{
TreeItem item = new TreeItem();
item.setText("File " + index++);
root.addItem(item);
}
editableTree.addItem(root);
}
- 在
EditableTreeNodesPanel的构造函数中,初始化树并添加一个事件处理程序,用于监听树节点上的单击事件:
initTree();
editableTree.addTreeListener(new TreeListener()
{
public void onTreeItemSelected(TreeItem item)
{
if (textbox.isAttached())
{
if(!currentSelection.getText().equals(textbox.getText()))
{
currentSelection.setText(textbox.getText());
}
workPanel.remove(textbox);
}
textbox.setHeight(item.getOffsetHeight() + "px");
textbox.setWidth("90px");
int xpos = item.getAbsoluteLeft() - 133;
int ypos = item.getAbsoluteTop() - 115;
workPanel.add(textbox, xpos, ypos);
textbox.setText(item.getText());
textbox.setFocus(true);
currentSelection = item;
textbox.addFocusListener(new FocusListener()
{
public void onLostFocus(Widget sender)
{
if (sender.isAttached())
{
if (!currentSelection.getText()
.equals(textbox.getText()))
{
currentSelection.setText (textbox.getText());
}
workPanel.remove(textbox);
}
}
});
}
public void onTreeItemStateChanged(TreeItem item)
{
}
}
- 在构造函数中,创建一个小的信息面板,显示关于这个应用程序的描述性文本,这样当我们在
Samples应用程序的可用示例列表中选择此示例时,就可以显示文本。将信息面板和工作面板添加到停靠面板,并初始化小部件:
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML
("<div class='infoProse'>This sample shows a tree whose nodes
can be edited by clicking on a tree node.</div>"));
workPanel.add(editableTree);
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
运行应用程序:

您可以单击树节点并更改显示的文本框中的文本。
刚刚发生了什么?
树控件是可视化和探索分层数据的一种好方法。在这个示例中,我们创建了一个包含十个节点的树,每个节点包含一个字符串值。我们注册了一个事件处理程序,监听树节点的选择事件。当选择一个树节点时,我们创建一个包含与树节点相同文本的文本框,并将文本框定位在树节点上方。通过检索树节点的左侧和顶部坐标来定位文本框。当前选择的树节点存储在一个私有变量中。我们注册了一个事件处理程序,监听新添加的文本框的焦点事件。当文本框失去焦点时,我们获取当前文本并用它修改树节点的值:
public void onLostFocus(Widget sender)
{
if (sender.isAttached())
{
if (!currentSelection.getText().equals(textbox.getText()))
{
currentSelection.setText(textbox.getText());
}
workPanel.remove(textbox);
}
}
isAttached()函数使我们能够检查发送者小部件是否实际附加到根面板,或者是否已经被销毁。如果小部件不再附加到面板上,我们就避免对小部件进行任何设置。就是这样!GWT 使得为树节点的内联编辑添加支持变得如此简单。当前的 GWT 版本尚不支持向树添加除字符串以外的小部件作为树节点。一旦支持可用,就可以简单地重构此示例以使用文本框作为树节点,并根据单击事件使它们可编辑或不可编辑。
日志监视器
在这个例子中,我们将看到如何基于客户端设置的时间间隔轮询服务器。这将涉及使用 GWT 计时器对象,对于需要根据重复的时间间隔在服务器上执行操作,然后异步更新网页部分以显示操作结果的情况非常有用。我们将创建一个简单的应用程序,可以实时监视和显示日志文件的内容。
行动时间-更新日志文件
几乎每个应用程序都有包含调试信息的日志文件。通常通过登录服务器,导航到包含日志文件的文件夹,然后在文本编辑器中打开文件来查看内容。这是检查日志文件的繁琐方式。更好、更用户友好的方式是使用 GWT 创建一个可以在网页中显示日志文件内容的应用程序。随着消息被添加到日志文件中,内容将实时更新。以下步骤将给我们带来期望的结果:
- 在
com.packtpub.gwtbook.samples.client包中创建一个新的 Java 文件LogSpyService.java。定义一个LogSpyService接口,其中包含两个方法——一个用于检索所有日志条目,一个用于仅检索新条目:
public interface LogSpyService extends RemoteService
{
public ArrayList getAllLogEntries();
public ArrayList getNextLogEntries();
}
- 在
com.packtpub.gwtbook.samples.client包中的新的 Java 文件LogSpyServiceAsync.java中创建此服务定义接口的异步版本:
public interface LogSpyServiceAsync
{
public void getAllLogEntries(AsyncCallback callback);
public void getNextLogEntries(AsyncCallback callback);
}
- 在
com.packtpub.gwtbook.samples.server包中的新的 Java 文件LogSpyServiceImpl.java中创建日志监视服务的实现。首先创建一个用于读取日志文件的私有方法,一个用于保存文件指针的变量,以及一个包含要读取的日志文件的名称的变量:
private long filePointer = 0;
private File logfile = new File("test2.log");
private ArrayList readLogFile()
{
ArrayList entries = new ArrayList();
RandomAccessFile file = new RandomAccessFile(logfile, "r");
long fileLength = logfile.length();
if (fileLength > filePointer)
{
file.seek(filePointer);
String line = file.readLine();
while (line != null)
{
line = file.readLine();
if (line != null && line.length() > 0)
{
entries.add(line);
}
}
filePointer = file.getFilePointer();
}
file.close();
return entries;
}
- 添加实现服务接口的两个方法:
public ArrayList getAllLogEntries()
{
return readLogFile();
}
public ArrayList getNextLogEntries()
{
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return readLogFile();
}
- 现在为与日志监视服务交互创建用户界面。在
com.packtpub.gwtbook.samples.client.panels包中创建一个新的 Java 文件LogSpyPanel.java。创建工作面板的变量、用于设置监视间隔的文本框、一个标签和开始和停止按钮。我们还需要一个布尔标志来指示当前的监视状态。
Public VerticalPanel workPanel = new VerticalPanel();
public ListBox logSpyList = new ListBox();
public TextBox monitoringInterval = new TextBox();
public Label monitoringLabel = new Label( "Monitoring Interval :");
public Button startMonitoring = new Button("Start");
public Button stopMonitoring = new Button("Stop");
private boolean isMonitoring = false;
- 创建包含开始和停止按钮、文本框和监视间隔标签的面板,以及一个计时器:
private HorizontalPanel intervalPanel = new HorizontalPanel();
private HorizontalPanel startStopPanel = new HorizontalPanel();
private Timer timer;
- 创建一个列表框来显示日志消息,并且我们将调用的服务接口来获取日志条目:
public ListBox logSpyList = new ListBox();
ServiceDefTarget endpoint = (ServiceDefTarget) logSpyService;
endpoint.setServiceEntryPoint GWT.getModuleBaseURL()
+ "logspy");
- 在构造函数中,将监视间隔文本框的初始值设置为 1000,并禁用停止按钮:
monitoringInterval.setText("1000");
stopMonitoring.setEnabled(false);
- 为面板、文本框和标签设置样式:
intervalPanel.setStyleName("logSpyPanel");
startStopPanel.setStyleName("logSpyStartStopPanel");
monitoringLabel.setStyleName("logSpyLabel");
monitoringInterval.setStyleName("logSpyTextbox");
- 添加一个事件处理程序来监听开始按钮的点击,并从处理程序调用日志监视服务:
startMonitoring.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
if (!isMonitoring)
{
timer = new Timer()
{
public void run()
{
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
ArrayList resultItems = (ArrayList) result;
for (Iterator iter = resultItems.iterator();
iter.hasNext();)
{
logSpyList.insertItem(((String)
iter.next()), 0);
logSpyList.setSelectedIndex(0);
}
}
public void onFailure(Throwable caught)
{
Window.alert("Error while invoking the logspy
service " + caught.getMessage());
}
};
logSpyService.getNextLogEntries(callback);
}
};
timer.scheduleRepeating(Integer.parseInt
(monitoringInterval.getText()));
isMonitoring = true;
startMonitoring.setEnabled(false);
stopMonitoring.setEnabled(true);
}
}
});
- 添加一个事件处理程序来监听停止按钮的点击,并停止监视:
stopMonitoring.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
if (isMonitoring)
{
timer.cancel();
isMonitoring = false;
startMonitoring.setEnabled(true);
stopMonitoring.setEnabled(false);
}
}
});
- 将列表中可见项的数量限制为八项:
logSpyList.setVisibleItemCount(8);
- 最后,在构造函数中,创建一个小的信息面板,显示有关此应用程序的描述性文本,以便在
Samples应用程序的可用样本列表中选择此样本时显示此文本。将监视间隔面板和开始-停止按钮面板添加到工作面板。将信息面板和工作面板添加到停靠面板,并初始化小部件:
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML
("<div class='infoProse'>View a log file live as entries are
written to it. This is similar in concept to the unix
utility tail. The new entries are retrieved and added in
real time to the top of the list. You can start and stop
the monitoring, and set the interval in milliseconds for
how often you want to check the file for new entries.
</div>"));
intervalPanel.add(monitoringLabel);
intervalPanel.add(monitoringInterval);
startStopPanel.add(startMonitoring);
startStopPanel.add(stopMonitoring);
workPanel.add(intervalPanel);
workPanel.add(startStopPanel);
workPanel.add(logSpyList);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
- 将服务添加到
Samples应用程序的模块文件Samples.gwt.xml中,位于com.packtpub.gwtbook.samples包中:
<servlet path="/Samples/logspy"
class="com.packtpub.gwtbook.samples.server.LogSpyServiceImpl"/>
以下是显示日志文件条目的应用程序的屏幕截图-test.log:

当条目被添加到此文件时,它们将实时添加到列表中,列表中的第一项将是最新的日志条目。您可以监视任何文件。只需更改LogSpyServiceImpl类中的logFile变量的值,以包含所需的文件名。
刚刚发生了什么?
日志文件通常只是文本文件,其中应用程序将消息附加到其中。此示例使用简单的日志文件,并可以修改为使用要监视的任何文件。我们使用RandomAccessFile类读取文件,以便每次只访问我们想要的文件部分,而无需每次将整个文件读入内存。类中存储了一个包含最后文件指针的私有变量。该指针是文件中的光标。我们有一个readLogFile()方法,用于访问文件并仅从文件指针到文件末尾读取数据。每次读取文件时,指针都会更新以存储最后读取的位置。
RandomAccessFile file = new RandomAccessFile(logfile, "r");
long fileLength = logfile.length();
if (fileLength > filePointer)
{
file.seek(filePointer);
String line = file.readLine();
while (line != null)
{
line = file.readLine();
if (line != null && line.length() > 0)
{
entries.add(line);
}
}
filePointer = file.getFilePointer();
}
file.close();
如果自上次读取文件以来文件未被修改,则返回一个空列表,而不尝试读取文件。每当客户端发出请求以获取新的日志条目时,我们读取文件并返回新的条目。
用户界面包括一个列表框,一个文本框,用于指定监视日志文件的频率,以及用于启动和停止文件监视的按钮。当点击开始按钮时,我们启动一个定时器,计划在提供的时间间隔后触发。每次定时器触发时,我们发出请求以获取日志条目,然后在onSuccess()回调方法中将返回的条目添加到列表框中。我们将日志条目插入列表中,然后将最后添加的条目设置为选定项,以在列表中视觉上表示最新的条目:
logSpyList.insertItem(((String) iter.next()), 0);
logSpyList.setSelectedIndex(0);
如果点击停止按钮,则定时器被取消,监视被停止。与所有其他示例相比,我们在这里做了一些非常不同的事情。我们根据用户在文本框中设置的时间间隔,基于重复的时间间隔调用服务。因此,每次定时器触发时,我们都会进行异步请求。这种技术可用于通过定期向服务器发出同步调用来更新页面的部分或部分,以获取新的信息。
便签
文档对象模型(DOM)以树结构的形式描述 HTML 文档的结构,可以使用诸如 JavaScript 之类的语言进行访问。所有现代的 Web 浏览器都通过 DOM 脚本访问加载的网页。GWT 提供了丰富的方法集,使您能够操作 Web 页面的 DOM。我们甚至可以拦截和预览 DOM 事件。我们将学习如何使用 GWT DOM 方法和对话框,利用它们提供创建类似于无处不在的便条便签的能力,并将它们拖动到浏览器窗口的任何位置。
行动时间-玩转便签
我们将创建可以在浏览器窗口中移动并放置在任何位置的便签。步骤如下:
- 在
com.packtpub.gwtbook.samples.client.panels包中创建一个名为StickyNotesPanel.java的新的 Java 文件。创建一个工作面板,一个用于创建便签的按钮,一个用于便签名称的文本框,以及用于保存便签的 x 和 y 坐标的变量。还创建一个整数变量,用于保存新便签坐标的增量:
private HorizontalPanel workPanel = new HorizontalPanel();
private Button createNote = new Button("Create Note");
private TextBox noteTitle = new TextBox();
private int noteLeft = 300;
private int noteTop = 170;
private int increment = 10;
- 创建一个名为
StickyNote的新类,该类扩展DialogBox。在这个类的构造函数中,如果提供了便签标题,则设置便签的标题,并添加一个文本区域,用于输入实际的便签内容:
public StickyNote(String title)
{
super();
if (title.length() == 0)
{
setText("New Note");
}
else
{
setText(title);
}
TextArea text = new TextArea();
text.setText("Type your note here");
text.setHeight("80px");
setWidget(text);
setHeight("100px");
setWidth("100px");
setStyleName(text.getElement(), "notesText", true);
setStyleName("notesPanel");
}
- 在
StickyNote类中创建一个拦截 DOM 事件的方法:
public boolean onEventPreview(Event event)
{
int type = DOM.eventGetType(event);
switch (type)
{
case Event.ONKEYDOWN:
{
return onKeyDownPreview((char) DOM.eventGetKeyCode(event),
KeyboardListenerCollection.getKeyboardModifiers(event));
}
case Event.ONKEYUP:
{
return onKeyUpPreview((char) DOM.eventGetKeyCode(event),
KeyboardListenerCollection.getKeyboardModifiers(event));
}
case Event.ONKEYPRESS:
{
return onKeyPressPreview((char) DOM.eventGetKeyCode(event),
KeyboardListenerCollection.getKeyboardModifiers(event));
}
}
return true;
}
- 在
StickyNotesPanel类的构造函数中,创建一个小的信息面板,显示有关此应用程序的描述性文本,以便在我们的Samples应用程序中的可用示例列表中选择此示例时显示文本。将此类作为Create Note按钮上点击事件的监听器添加。将用于创建便利贴的按钮以及标题文本框添加到工作面板。将信息面板和工作面板添加到停靠面板,并初始化小部件:
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML
("<div class='infoProse'>Create sticky notes and drag them
around to position any where in your browser window. Go
ahead and try it !
</div>"));
createNote.addClickListener(this);
createNote.setStyleName("notesButton");
workPanel.add(createNote);
noteTitle.setStyleName("notesTitle");
workPanel.add(noteTitle);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
- 使
StickyNotesPanel类实现ClickListener接口,并在onClick()方法中添加代码,以在单击Create Note按钮时创建一个新的便利贴:
public void onClick(Widget sender)
{
StickyNote note = new StickyNote(noteTitle.getText());
note.setPopupPosition(noteLeft + increment, noteTop +
increment);
increment = increment + 40;
note.show();
}
这是应用程序的屏幕截图:

当您创建多个便利贴时,您可以拖动便利贴并将它们放在浏览器窗口的任何位置。
刚刚发生了什么?
这个示例演示了使用 GWT 可以生成一些非常酷的界面和应用程序的简易性。便利贴应用程序在屏幕上创建便利贴,您可以在 Web 浏览器内拖动它们并将它们放在任何位置。用户界面包含一个文本框,用于输入便利贴的名称,以及一个按钮,用于使用提供的名称创建一个新的便利贴。如果没有提供名称,则将创建一个默认名称New Note。
便利贴本身是DialogBox的子类。它有一个标题和一个用于输入便利贴的文本区域。DialogBox类继承自PopupPanel类,并实现了EventPreview接口。我们实现了来自该接口的onEventPreview()方法,如步骤 3 中所述,以便我们可以首先预览所有浏览器事件,然后再将它们发送到它们的目标。这基本上意味着我们的便利贴面板位于浏览器事件预览堆栈的顶部。
我们预览键盘事件,然后将其传递到目标。这使我们能够将模态对话框引入非模态行为。如果我们不这样做,一旦创建第一个便利贴,便利贴将是模态的,并且除非我们首先关闭便利贴,否则不允许我们通过单击Create按钮创建另一个便利贴。
现在,便利贴在预览事件后将事件传递给底层面板,我们可以创建任意数量的便利贴。已注册事件处理程序以侦听单击Create Note按钮。单击按钮时,将创建一个新的便利贴,并将其位置设置为相对于浏览器窗口,然后显示它。我们保持一个包含上一个创建的便利贴的左侧位置的私有变量,以便我们可以在创建它们时交错地放置便利贴的位置,就像我们在步骤 5 中所做的那样。这样可以很好地在屏幕上排列便利贴,以便便利贴不会相互覆盖。
由于我们的便利贴继承自DialogBox,因此它们是可拖动的;我们可以将它们拖动到屏幕上的任何位置!
拼图
上一个示例演示了 GWT 中的一些拖动功能和 DOM 事件预览。在这个示例中,我们将使用相同的 DOM 方法,但以不同的方式拦截或预览 DOM 事件。我们还将通过使用AbsolutePanel来演示 GWT 中的一些绝对定位功能。我们将创建一个简单的蒙娜丽莎拼图,可以通过拖动和重新排列拼图块来解决。
行动时间-让我们创建一个拼图!
我们将创建一个简单的拼图,其拼图块是通过将蒙娜丽莎图像分成九个部分而创建的。步骤如下:
- 在
com.packtpub.gwtbook.samples.client.panels包中创建一个名为JigsawPuzzlePanel.java的新 Java 文件,该文件实现MouseListener接口。创建一个AbsolutePanel类,它将是将添加所有小部件的主面板。还添加两个变量来存储鼠标光标的x和y位置:
private AbsolutePanel workPanel = new AbsolutePanel();
private boolean inDrag;
private int xOffset;
private int yOffset;
- 在
JigsawPuzzlePanel的构造函数中,将蒙娜丽莎的图像添加到面板,并将面板添加为图像的事件监听器:
Image monalisa = new Image("images/monalisa_face1_8.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 60, 20);
monalisa = new Image("images/monalisa_face1_7.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 60, 125);
monalisa = new Image("images/monalisa_face1_2.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 60, 230);
monalisa = new Image("images/monalisa_face1_3.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 170, 20);
monalisa = new Image("images/monalisa_face1_4.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 170, 125);
monalisa = new Image("images/monalisa_face1_1.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 170, 230);
monalisa = new Image("images/monalisa_face1_6.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 280, 20);
monalisa = new Image("images/monalisa_face1_9.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 280, 125);
monalisa = new Image("images/monalisa_face1_5.jpg");
monalisa.addMouseListener(this);
jigsaw puzzlecreatingworkPanel.add(monalisa, 280, 230);
- 在构造函数中注册拦截 DOM 鼠标事件:
DOM.addEventPreview(new EventPreview()
{
public boolean onEventPreview(Event event)
{
switch (DOM.eventGetType(event))
{
case Event.ONMOUSEDOWN:
case Event.ONMOUSEMOVE:
case Event.ONMOUSEUP:
DOM.eventPreventDefault(event);
}
return true;
}
});
- 在构造函数中实现监听鼠标按下事件的方法:
public void onMouseDown(Widget source, int x, int y)
{
DOM.setCapture(source.getElement());
xOffset = x;
yOffset = y;
inDrag = true;
}
- 在构造函数中实现监听鼠标移动事件的方法:
public void onMouseMove(Widget source, int x, int y)
{
if (inDrag)
{
int xAbs = x + source.getAbsoluteLeft() - 135;
int yAbs = y + source.getAbsoluteTop() - 120;
((AbsolutePanel)source.getParent()).
setWidgetPosition(source, xAbs- xOffset, yAbs - yOffset);
}
}
- 在构造函数中实现监听鼠标抬起事件的方法:
public void onMouseUp(Widget source, int x, int y)
{
DOM.releaseCapture(source.getElement());
inDrag = false;
}
- 最后,在构造函数中,创建一个小的信息面板,显示关于此应用程序的描述性文本,以便在我们的
Samples应用程序的可用示例列表中选择此示例时显示文本。将信息面板和工作面板添加到停靠面板,并初始化小部件:
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML
("<div class='infoProse'>This example demonstrates the use
of dragging to move things around and place them anywhere
in the window. It is easy to forget that you are actually
doing this in a web browser !
</div>"));
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
这是你第一次访问页面时的谜题:

这是已解决的谜题:

刚刚发生了什么?
此示例演示了 GWT 中的绝对定位功能。蒙娜丽莎的图像文件被分成九个大小相等的图像。我们混淆这些图像,并在应用程序呈现时在屏幕上以 3x3 的方形呈现它们。用户可以通过拖动它们并重新定位它们在屏幕上来重新创建蒙娜丽莎的图像。
在这个示例中,我们使用AbsolutePanel类作为我们的工作面板。它具有绝对定位其所有子部件的能力,甚至允许部件重叠。我们通过绝对定位将九个图像添加到面板中,使它们形成一个漂亮的 3x3 网格。
这是网格的一列:
Image monalisa = new Image("images/monalisa_face1_8.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 60, 20);
monalisa = new Image("images/monalisa_face1_7.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 60, 125);
monalisa = new Image("images/monalisa_face1_2.jpg");
monalisa.addMouseListener(this);
workPanel.add(monalisa, 60, 230);
在上一个示例中,我们能够实现onEventpreview()方法来预览浏览器事件,然后再发送到它们的目标。我们之所以能够做到这一点,是因为该注释是PopupPanel的子类,它提供了这种能力。但在当前示例中,我们没有使用弹出面板。因此,我们使用另一种方法将自己添加到事件预览堆栈的顶部。这次我们使用 DOM 对象中的addEvetnpreview()方法,如步骤 3 所示。
在第 4 步中,我们实现了MouseListener接口,并在面板中注册自己作为鼠标事件的事件处理程序。当用户在拖动图像之前单击图像时,我们获取被单击的元素并将其设置为鼠标捕获。这确保元素将接收所有鼠标事件,直到它从鼠标捕获中释放。我们将元素的x和y坐标存储在一个私有变量中。我们还设置了一个标志,告诉我们当前处于拖动元素的模式。
一旦用户开始拖动图像,我们检查是否处于拖动模式,并设置小部件的位置,这将使小部件移动到新位置。您只能通过调用包含小部件的绝对面板来设置绝对小部件位置;因此,我们必须获取图像的父对象,然后将其转换为正确的类。我们在第 5 步中已经涵盖了所有这些内容。
当用户完成将图像拖动到位置并释放鼠标时,我们将元素从鼠标捕获中释放,并将拖动标志设置为 false,如第 6 步所示。
GWT 中的绝对定位支持仍然需要一些工作,并且在 Firefox 和 Internet Explorer 以及它们的多个版本中可能表现出不同的行为。
总结
在本章中,我们学习了如何创建可以友好地浏览一组数据的表格,并扩展了树部件以添加对树节点进行简单编辑的支持。我们利用timer对象创建了一个日志监视应用程序,用于监视给定日志文件的新条目,并实时显示在更新的列表中。
我们学习了如何在 GWT 中使用一些 DOM 方法和 DOM 事件预览功能,并利用它来实现可拖动的便签应用程序。我们还学会了如何使对话框框非模态,以便我们可以自适应它们的使用。最后,利用绝对定位功能和另一种预览 DOM 事件的方法,我们创建了一个拼图应用程序。
在下一章中,我们将学习如何使用 JavaScript 本地接口将第三方 JavaScript 库与 GWT 集成。
第六章:使用 JSNI 和 JavaScript 库的浏览器效果
在本章中,我们将学习如何创建用户界面,利用一些知名的第三方 JavaScript 库提供的酷炫浏览器效果。我们将利用 GWT 提供的 JavaScript Native Interface (JSNI)来包装这些现有的 JavaScript 库,并在我们的 GWT 应用程序中使用它们。
我们将要解决的任务是:
-
Moo.Fx
-
Rico 圆角
-
Rico 颜色选择器
-
Script.aculo.us 效果
什么是 JSNI?
JSNI 提供了一种将 JavaScript 代码与 Java 代码混合的方法。它在概念上类似于 Sun 的 Java 环境提供的 Java Native Interface (JNI)。JNI 使您的 Java 代码能够调用 C 和 C++方法。JSNI 使您的 Java 代码能够调用 JavaScript 方法。这是一种非常强大的技术,它让我们能够直接从 Java 代码访问低级别的 JavaScript 代码,并为下面列出的各种用途和可能性打开了大门:
-
从 Java 调用 JavaScript 代码
-
从 JavaScript 调用 Java 代码
-
跨 Java/JavaScript 边界抛出异常
-
从 JavaScript 访问 Java 字段
然而,这种强大的技术应该谨慎使用,因为 JSNI 代码可能在不同浏览器之间不具备可移植性。当前的 GWT 编译器实现也无法对 JSNI 代码进行任何优化。JSNI 方法必须始终声明为 native,并且放置在 JSNI 方法中的 JavaScript 代码必须放置在特殊格式的注释块中。因此,每个 JSNI 方法将由两部分组成——一个 native 方法声明,以及嵌入在特殊格式的代码块中的方法的 JavaScript 代码。以下是一个调用alert() JavaScript 方法的 JSNI 方法的示例:
native void helloGWTBook()
/*-{
$wnd.alert("Hello, GWT book!");
}-*/;
在上面的示例中,JavaScript 代码嵌入在'/-{'和'}-/'块中。还要注意的一件事是使用$wnd和$doc变量。GWT 代码始终在浏览器中的嵌套框架内运行,因此无法在 JSNI 代码中以正常方式访问窗口或文档对象。您必须使用$wnd和$doc变量,这些变量由 GWT 自动初始化,用于引用主机页面的窗口和文档对象。GWT 编译器可以检查我们的 JSNI 代码。因此,如果在 Web 模式下运行并编译应用程序,编译器将标记 JSNI 代码中的任何错误。这是调试 JSNI 代码的一种好方法,因为这些错误直到运行时(在托管模式下运行时)才会显示出来。在本章中,我们将使用 JSNI 来包装一些第三方 JavaScript 库,并在我们的 GWT 用户界面中使用它们提供的酷炫浏览器效果。
注意
在最近的 GWT 版本中,JSNI 函数有时在托管模式下不起作用,但在部署时可以正常工作。
Moo.Fx
Moo.fx是一个超轻量级和快速的 JavaScript 库,为 Web 应用程序提供了几种酷炫的效果(moofx.mad4milk.net)。它体积小,适用于所有主要的 Web 浏览器。我们将使用 JSNI 来包装Moo.fx库提供的一些效果,并在我们的 GWT 应用程序中使用这些效果。
行动时间—使用 JSNI
我们将使用 GWT 框架提供的 JSNI 来包装Moo.fx库,并在我们的 GWT 用户界面中混合 Java 和 JavaScript 来使用其功能。
- 将原型和
Moo.fxJavaScript 文件添加到模块的 HTML 文件—Samples.html。
<script type="text/JavaScript"src="img/prototype.js">
</script>
<script type="text/JavaScript"src="img/moo.fx.js">
</script>
-
在
com.packtpub.gwtbook.samples.client.util包中创建一个名为MooFx.java的新 Java 类,用于包装Moo.fxJavaScript 库的效果。 -
在
MooFx.java中添加一个新的 JSNI 方法,用于创建一个opacity.fx对象。
public native static Element opacity(Element element)
/*-{
$wnd._nativeExtensions = false;
return new $wnd.fx.Opacity(element);
}-*/;
- 为切换不透明度效果添加一个 JSNI 方法。
public native static void toggleOpacity(Element element)
/*-{
$wnd._nativeExtensions = false;
element.toggle();
}-*/;
- 添加一个私有的 JSNI 方法,接受一个选项字符串参数并将其转换为 JavaScript 对象。
private static native JavaScriptObject buildOptions
(String opts)
/*-{
eval("var optionObject = new Object()");
var options = opts.split(',');
for (var i =0; i < options.length; i++)
{
var opt = options[i].split(':');
eval("optionObject." + opt[0] + "=" + opt[1]);
}
return optionObject;
}-*/;
- 添加一个静态的 Java 方法来创建一个高度效果,它使用上面的
buildOptions()来构建一个 JavaScript 对象,以便将选项传递给 JSNI 方法。
public static Element height(Element element, String opts)
{
return height(element, buildOptions(opts));
}
- 添加一个新的 JSNI 方法,用于创建高度效果对象。
private native static Element height
(Element element, JavaScriptObject opts)
/*-{
$wnd._nativeExtensions = false;
return new $wnd.fx.Height(element, opts);
}-*/;
- 添加一个新的 JSNI 方法来切换高度效果。
public native static void toggleHeight(Element element)
/*-{
$wnd._nativeExtensions = false;
element.toggle();
}-*/;
- 添加一个静态的 Java 方法来创建一个宽度效果,它使用上面的
buildOptions()来构建一个 JavaScript 对象,以便将选项传递给 JSNI 方法。
public static Element width(Element element, String opts)
{
return width(element, buildOptions(opts));
}
- 添加一个新的 JSNI 方法,用于创建宽度效果对象。
private native static Element width
(Element element, JavaScriptObject opts)
/*-{
$wnd._nativeExtensions = false;
return new $wnd.fx.Width(element, opts);
}-*/;
- 添加一个新的 JSNI 方法来切换宽度效果。
public native static void toggleWidth(Element element)
/*-{
$wnd._nativeExtensions = false;
element.toggle();
}-*/;
- 在
com.packtpub.gwtbook.samples.client.panels包中的一个新的 Java 文件中创建此应用程序的用户界面,命名为MooFxEffectsPanel.java。添加一个包含外部div元素和包含文本段落元素的内部div元素的 HTML 片段。添加三个包含此片段的不同变量。还为每个效果添加一个元素。
private HTML opacityBox = new HTML
("<div class='moofxBox'><div id=\"opacitybox\">
<p class=\"text\">
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</p></div></div>");
private HTML heightBox = new HTML
("<div class='moofxBox'><div id=\"heightbox\">
<p class=\"text\">
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</p></div></div>");
private HTML widthBox = new HTML
("<div class='moofxBox'><div id=\"widthbox\">
<p class=\"text\">
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</p></div></div>");
private Element widthBoxElement;
private Element heightBoxElement;
private Element opacityBoxElement;
- 创建三个按钮,一个用于切换每个
Moo.fx效果。
Button opacityButton = new Button("Toggle Opacity");
Button heightButton = new Button("Toggle Height");
Button widthButton = new Button("Toggle Width");
- 注册一个事件处理程序来监听每个按钮的点击,并调用适当的方法来切换效果。
opacityButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
MooFx.toggleOpacity
(opacityBoxElement);
}
});
heightButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
MooFx.toggleHeight
(heightBoxElement);
}
});
widthButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
MooFx.toggleWidth
(widthBoxElement);
}
});
- 创建一个
DeferredCommand,当执行时创建每个效果对象。
DeferredCommand.add(new Command()
{
public void execute()
{
opacityBoxElement = MooFx.opacity
(DOM.getElementById("opacitybox"));
}
});
DeferredCommand.add(new Command()
{
public void execute()
{
heightBoxElement =
MooFx.height(DOM.getElementById
("heightbox"), "duration:2500");
}
});
DeferredCommand.add(new Command()
{
public void execute()
{
widthBoxElement =
MooFx.width(DOM.getElementById
("widthbox"), "duration:2000");
}
});
- 在构造函数中,将每个效果的按钮和
divs添加到工作面板中。
opacityButton.setStyleName("moofxButton");
workPanel.add(opacityButton);
workPanel.add(opacityBox);
heightButton.setStyleName("moofxButton");
workPanel.add(heightButton);
workPanel.add(heightBox);
widthButton.setStyleName("moofxButton");
workPanel.add(widthButton);
workPanel.add(widthBox);
- 最后,创建一个小的信息面板,显示关于此应用程序的描述性文本,以便在我们的
Samples应用程序的可用示例列表中选择此示例时显示此文本。将信息面板和工作面板添加到一个停靠面板中,并初始化小部件。
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML("<div class='infoProse'>
Use cool Moo.fx effects in your
GWT application.</div>"));
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
这是应用程序的屏幕截图。单击每个按钮以查看效果。

刚刚发生了什么?
Moo.fx库提供的主要效果有:
-
不透明度:修改元素的不透明度或透明度。
-
高度:修改元素的高度。
-
宽度:修改元素的宽度。
在这个示例中,我们创建了一个名为MooFx的 Java 类,它使用 JSNI 封装了Moo.fx JavaScript 库。我们创建了一个名为opacity()的本机方法,用于实例化一个不透明度对象。在这个方法中,我们调用不透明度对象的 JavaScript 构造函数,并返回结果对象,其类型为Element。我们将其存储在一个变量中。
return new $wnd.fx.Opacity(element);
然后,我们创建了一个名为toggleOpacity()的本机方法,用于切换元素的不透明度从一个状态到另一个状态。这个方法使用我们之前存储的变量,并调用其切换方法来改变其当前状态。
element.toggle();
我们创建了height()和width()的 Java 方法,它们接受一个包含需要提供给Moo.fx高度和宽度构造函数的选项的字符串参数。这两个方法使用一个名为buildOptions()的本机方法来创建包含选项的 JavaScript 对象,然后将其传递给用于创建高度和宽度的本机方法。buildOptions()方法解析提供的字符串,并创建一个 JavaScript 对象并设置其属性和属性值。我们再次利用eval()函数来设置属性并返回对象。
private static native JavaScriptObject buildOptions(String opts)
/*-{
eval("var optionObject = new Object()");
var options = opts.split(',');
for (var i =0; i < options.length; i++)
{
var opt = options[i].split(':');
Moo.fxworkingeval("optionObject." + opt[0] + "=" + opt[1]);
}
return optionObject;
}-*/;
返回的 JavaScript 选项对象被传递给本机的height()和width()方法,以创建类似于opacity()方法的效果对象。然后,我们添加了用于切换高度和宽度的本机方法。这就是我们将库封装成易于使用的 Java 类所需要做的全部!
在用户界面中,我们创建一个带有外部div的 HTML 对象,其中包含一个带有文本段落的内部div。HTML 小部件使我们能够创建任意 HTML 并将其添加到面板中。我们在此示例中使用了 HTML 小部件,但我们也可以使用 GWT 框架中的 DOM 对象的方法来创建相同的元素。在下一个示例中,我们将使用该功能,以便熟悉 GWT 提供的不同工具。我们还创建了三个按钮,分别用于切换每个效果。为每个按钮注册了事件处理程序,以侦听单击事件,然后调用指定效果的适当切换方法。在创建效果的方法中,我们使用 DOM 对象上的getElementById()来获取我们感兴趣的div元素。我们需要这样做,因为我们无法访问添加到面板的div。我们感兴趣的div作为 HTML 小部件的一部分添加到面板上。
opacityBoxElement = MooFx.opacity(DOM.getElementById("opacitybox"));
然后切换元素上的必要效果。
MooFx.toggleOpacity(opacityBoxElement);
效果本身是通过在DeferredCommand内调用效果的相应构造函数来构建的。我们添加的元素尚不可通过其 ID 使用,直到所有事件处理程序都已完成。DeferredCommand在它们全部完成后运行,这确保了我们的元素已被添加到 DOM,并且可以通过其 ID 访问。我们获取元素,创建效果,并将其与元素关联起来。
DeferredCommand.add(new Command()
{
public void execute()
{
opacityBoxElement = MooFx.opacity
(DOM.getElementById("opacitybox"));
}
});
我们已成功从 Java 中访问了库,在我们的 GWT 应用程序中可以在任何地方重用这些效果。在本章后面的ColorSelector示例中,我们将使用Moo.fx效果之一与其他库的效果结合使用。
Rico 圆角
网页上带有圆角的元素在视觉上比直角更有吸引力,美学上更具吸引力。这也是网络应用外观和感觉中最热门的设计趋势之一。Rico (openrico.org/rico/home.page)是另一个出色的 JavaScript 库,对此提供了很好的支持,并且使用起来非常容易。它还提供了大量的功能,但我们只是包装和使用 Rico 的圆角效果部分。在此示例中,我们仅使用标签来应用圆角,但您也可以将其应用于文本段落和其他几种 HTML 元素。在此示例中,我们将包装 Rico 的圆角效果,并在我们的应用程序中使用它来显示具有不同类型圆角的多个标签。
行动时间-支持标签
我们将包装Rico库,并在我们的 GWT 用户界面中为带有圆角的标签提供支持。
- 在模块的 HTML 文件
Samples.html中添加所需的原型和 Rico JavaScript 文件。
<script type="text/JavaScript"src="img/prototype.js">
</script>
<script type="text/JavaScript"src="img/rico.fx.js">
</script>
-
在
com.packtpub.gwtbook.samples.client.util包中创建一个名为Rico.java的新 Java 类,该类将包装ricoJavaScript 库效果。 -
在
Rico.java中添加一个新的 JSNI 方法,用于将小部件的角进行四舍五入。
private native static void corner
(Element element, JavaScriptObject opts)
/*-{
$wnd._nativeExtensions = false;
$wnd.Rico.Corner.round(element, opts);
}-*/;
- 添加一个私有 JSNI 方法,该方法接受一个字符串选项参数并将其转换为 JavaScript 对象。
private static native JavaScriptObject buildOptions(String opts)
/*-{
eval("var optionObject = new Object()");
var options = opts.split(',');
for (var i =0; i < options.length; i++)
{
var opt = options[i].split(':');
eval("optionObject." + opt[0] + "=" + opt[1]);
}
return optionObject;
}-*/;
- 添加一个静态 Java 方法,用于创建一个圆角,该方法使用上述
buildOptions()来构建一个 JavaScript 对象,以便将选项传递给 JSNI 方法。
public static void corner(Widget widget, String opts)
{
corner(widget.getElement(), buildOptions(opts));
}
- 添加一个静态 Java 方法,用于创建一个不传递任何选项并使用默认值的圆角。
public static void corner(Widget widget)
{
corner(widget.getElement(), null);
}
- 在
com.packtpub.gwtbook.samples.client.panels包中的一个新的 Java 文件中创建此应用程序的用户界面,命名为RoundedCornersPanel.java。创建一个包含三行两列的网格。我们将向此网格添加标签。
private Grid grid = new Grid(3, 2);
- 添加六个标签,这些标签将分别应用六种不同的圆角。
private Label lbl1 = new Label("Label with rounded corners.");
private Label lbl2 = new Label
("Label with only the top corners rounded.");
private Label lbl3 = new Label("Label with only the
bottom corners rounded.");
private Label lbl4 = new Label("Label with only the
bottom right corner rounded.");
private Label lbl5 = new Label("Label with compact
rounded corners ");
private Label lbl6 = new Label("Label with rounded corners
and red border.");
- 调用方法为每个标签创建圆角,并向其传递不同的选项。
Rico.corner(lbl1);
Rico.corner(lbl2, "corners:\"top\"");
Rico.corner(lbl3, "corners:\"bottom\"");
Rico.corner(lbl4, "corners:\"br\"");
Rico.corner(lbl5, "compact:true");
Rico.corner(lbl6, "border: 'red'");
- 将标签添加到网格中。
grid.setWidget(0, 0, lbl1);
grid.setWidget(0, 1, lbl2);
grid.setWidget(1, 0, lbl3);
grid.setWidget(1, 1, lbl4);
grid.setWidget(2, 0, lbl5);
grid.setWidget(2, 1, lbl6);
- 最后,创建一个小的信息面板,显示关于这个应用程序的描述性文本,这样当我们在
Samples应用程序的可用示例列表中选择此样本时,我们可以显示这个文本。将信息面板和工作面板添加到一个停靠面板中,并初始化小部件。
HorizontalPanel infoPanel =
new HorizontalPanel();infoPanel.add(new HTML
("<div class='infoProse'>Labels with different
kinds of rounded corners.</div>"));
workPanel.add(grid);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
这里是一个显示不同类型圆角标签的屏幕截图:

刚刚发生了什么?
我们创建了一个 Java 类,使用 JSNI 提供对Rico JavaScript 库中圆角功能的访问。我们创建了一个buildOptions()方法,就像在前面的示例中一样,它可以接受一个包含选项字符串的参数,并将这些选项作为本机 JavaScript 对象的属性添加。然后将此选项对象传递给调用 Rico 库中提供的元素的corner()方法的 JSNI 方法。
private native static void corner
(Element element, JavaScriptObject opts)
/*-{
$wnd._nativeExtensions = false;
$wnd.Rico.Corner.round(element, opts);
}-*/;
在用户界面中,我们创建一个网格,并向其添加六个标签。这些标签中的每一个都应用了不同类型的圆角。Rico 支持在四个边上或特定边上的圆角。它还可以创建紧凑形式的角,其中角比默认版本略少圆。您甚至可以使两个或三个角变圆,而将第四个角保持为方形。Rico 提供了其他方法,您可以包装并在应用程序中使用,除了圆角之外。该过程与我们迄今为止所做的非常相似,通常只是实现您感兴趣的 JavaScript 库中的所有方法。在下一个示例中,我们将包装 Rico 中的更多功能,并在颜色选择器应用程序中使用它。
Rico 颜色选择器
我们已经成功地在上一个示例中从 Rico 中包装了圆角效果。在本节中,我们将添加支持使用 Rico 的 Color 对象访问颜色信息的功能。我们将使用 JSNI 包装这个功能,然后创建一个颜色选择器应用程序,该应用程序使用 Rico 颜色对象以及我们在本章前面创建的Moo.fx效果。
行动时间-包装颜色方法
我们将在Rico库中包装color方法,并使用它们创建一个选择颜色的应用程序。
- 在
Rico.java中添加一个新的 JSNI 方法,用于创建具有提供的red, green和blue值的color对象,并将其应用于提供的元素。
public native static void color
(Element element, int red, int green,int blue)
/*-{
$wnd._nativeExtensions = false;
eval('' + element.id +' = new $wnd.Rico.Color
(' + red +',' + green +',' + blue + ')');
element.style.backgroundColor=eval
(element.id + '.asHex()');
}-*/;
- 在
Rico.java中添加一个新的 JSNI 方法,用于获取 Rico 颜色对象的十六进制值。
public native static String getColorAsHex(Element element)
/*-{
$wnd._nativeExtensions = false;
return (eval(element.id + '.asHex()'));
}-*/;
- 在
com.packtpub.gwtbook.samples.client.panels包中的一个新的 Java 文件ColorSelectorPanel.java中为这个应用程序创建用户界面。创建一个包含三行三列的网格。创建三个文本字段用于输入值,以及工作面板和用于颜色框和颜色文本的divs。
private HorizontalPanel workPanel = new HorizontalPanel();
private Grid grid = new Grid(3, 3);
private TextBox redText = new TextBox();
private TextBox greenText = new TextBox();
private TextBox blueText = new TextBox();
private Element outerDiv = DOM.createDiv();
private Element colorDiv = DOM.createDiv();
private Element colorText = DOM.createElement("P");
private Element colorBox = DOM.createElement("P");
- 在构造函数中初始化网格,并将每个文本框中的值默认为零。
grid.setText(0, 0, "Red");
grid.setText(1, 0, "Green");
grid.setText(2, 0, "Blue");
redText.setText("0");
grid.setWidget(0, 1, redText);
greenText.setText("0");
grid.setWidget(1, 1, greenText);
blueText.setText("0");
grid.setWidget(2, 1, blueText);
grid.setText(0, 2, "(0-255)");
grid.setText(1, 2, "(0-255)");
grid.setText(2, 2, "(0-255)");
- 注册一个事件处理程序来监听键盘事件。
redText.addKeyboardListener(this);
blueText.addKeyboardListener(this);
greenText.addKeyboardListener(this);
- 创建一个段落元素来显示所选颜色。
DOM.setAttribute(colorBox, "className", "ricoColorBox");
DOM.setAttribute(colorBox, "id", "colorBox");
DOM.setInnerText(colorBox, "");
Rico.color(colorBox, 0, 0, 0);
- 创建用于显示所选颜色的十六进制值的元素。
DOM.setAttribute(outerDiv, "className", "heightBox");
DOM.setAttribute(colorDiv, "id", "colorDiv");
DOM.setAttribute(colorText, "className", "text");
DOM.appendChild(colorDiv, colorText);
DOM.appendChild(outerDiv, colorDiv);
DOM.appendChild(workPanel.getElement(), outerDiv);
- 创建一个
DeferredCommand来初始化来自Moo.fx的高度效果,并将初始选定的颜色设置为(0, 0, 0)。
DeferredCommand.add(new Command()
{
public void execute()
{
MooFx.height(DOM.getElementById("colorDiv"),
"duration:500");
DOM.setInnerText(colorText, Rico.getColorAsHex
(colorBox));
}
});
- 添加一个
onKeyPress()处理程序,以在用户输入新的 RGB 值时显示所选颜色,并将高度效果应用于显示所选颜色的div。
public void onKeyPress(Widget sender, char keyCode,
int modifiers)
{
MooFx.toggleHeight(DOM.getElementById("colorDiv"));
Timer t = new Timer()
{
public void run()
{
if ((redText.getText().length() > 0)
&& (greenText.getText().length() > 0)
&& (blueText.getText().length() > 0))
{
Rico.color(colorBox,
Integer.parseInt(redText.getText()),
Integer.parseInt(greenText.getText()),
Integer.parseInt(blueText.getText()));
DOM.setInnerText(colorText, Rico.getColorAsHex
(colorBox));
MooFx.toggleHeight(DOM.getElementById("colorDiv"));
}
}
};
t.schedule(500);
}
- 最后,创建一个小的信息面板,显示关于这个应用程序的描述性文本,这样当我们在
Samples应用程序的可用示例列表中选择此样本时,我们可以显示这个文本。将信息面板和工作面板添加到一个停靠面板中,并初始化小部件。
HorizontalPanel infoPanel = new HorizontalPanel();infoPanel.add
(new HTML("<div class='infoProse'>
Select a color by providing the red, green and blue values.
The selected color will be applied to the box on the screen
and the hex value of the color will be displayed below it
with an element sliding up and then sliding down to display
the value. Check it out by typing in the color
components!</div>"));
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
这就是应用程序。输入 RGB 的新值,当您停止输入时,观察所选颜色的显示,并且当前颜色的十六进制值以滑动窗口效果显示为上滑和下滑!

刚刚发生了什么?
我们首先从上一个示例中增强我们的 Rico 包装类,以添加对颜色功能的访问。Rico 为我们提供了使用一组红色、绿色和蓝色值创建颜色对象的能力。一旦构造了这个颜色对象,就可以将其十六进制值作为字符串检索出来。我们添加了一个 JSNI 方法来创建一个颜色对象。在这个方法内部,我们创建Rico.Color对象,然后将提供的元素的背景设置为新创建的颜色。颜色对象存储在一个变量中,变量的名称与元素的 ID 相同。我们使用eval()方法动态创建变量并设置背景颜色。我们为元素设置backgroundColor DHTML 属性:
eval('' + element.id +' = new $wnd.Rico.Color
(' + red +',' + green +',' + blue + ')');
element.style.backgroundColor=eval(element.id + '.asHex()');
我们还创建了一个 JSNI 方法,可以返回提供元素的背景颜色的十六进制值。
public native static String getColorAsHex(Element element)
/*-{
return (eval(element.id + '.asHex()'));
}-*/;
在用户界面中,我们创建一个网格,并用三个文本框填充它,用于输入颜色值,并为每个字段添加一些标识符。在这个示例中,我们使用 DOM 对象创建各种元素,而不是使用 HTML 小部件。DOM 对象包含用于创建各种元素和操作网页文档对象模型的静态方法。我们创建两个div元素和一个段落元素,并将它们添加到页面的面板中。这些将用于创建将对其应用高度效果以在选择的颜色的div上滑动并显示十六进制值之前滑动的元素。由于workPanel是一个 GWT 小部件,我们调用所有小部件提供的getElement()方法来访问底层 DOM 元素,然后将div元素附加到其中。
DOM.setAttribute(outerDiv, "className", "heightBox");
DOM.setAttribute(colorDiv, "id", "colorDiv");
DOM.setAttribute(colorText, "className", "text");
DOM.appendChild(colorDiv, colorText);
DOM.appendChild(outerDiv, colorDiv);
DOM.appendChild(workPanel.getElement(), outerDiv);
我们再次使用DeferredCommand来设置当前颜色的初始十六进制值,并设置来自Moo.fx的高度效果对象。由于我们使用段落元素来显示带有颜色十六进制值的字符串,我们必须使用 DOM 对象来设置其内部文本。如果我们使用 GWT 小部件,我们将通过调用setText()方法来设置值。
MooFx.height(DOM.getElementById("colorDiv"), "duration:500");
DOM.setInnerText(colorText, Rico.getColorAsHex(colorBox));
最后,在onKeyPress()方法中,我们首先切换colordiv的高度,使元素向上滑动。然后我们安排一个定时器在 500 毫秒后触发,当定时器触发时,我们使用红色、绿色和蓝色文本框中的当前值创建一个新的颜色对象,将colorText元素的文本设置为该颜色的十六进制值,然后切换colordiv的高度,使其向下滑动以显示这个值。定时器是必要的,以便稍微减慢速度,这样您可以清楚地看到过渡和效果。
MooFx.toggleHeight(DOM.getElementById("colorDiv"));
Timer t = new Timer()
{
public void run()
{
if((redText.getText().length() > 0)
&& (greenText.getText().length() > 0)
&& (blueText.getText().length() > 0))
{
Rico.color(colorBox, Integer.parseInt(redText.getText()),
Integer.parseInt(greenText.getText()),
Integer.parseInt(blueText.getText()));
DOM.setInnerText(colorText, Rico.getColorAsHex(colorBox));
MooFx.toggleHeight(DOM.getElementById("colorDiv"));
}
}
};
t.schedule(500);
Script.aculo.us 效果
Script.aculo.us(script.aculo.us/)是由 Thomas Fuchs 编写的令人惊叹的 JavaScript 库,可以在网页内实现各种时髦的过渡和视觉效果。它是一个跨浏览器兼容的库,建立在原型 JavaScript 框架之上。它也是最受欢迎的 Web 2.0 库之一,在各种应用中被广泛使用,最值得注意的是它还包含在 Ruby On Rails web 框架中。Script.aculo.us效果是由该库的一部分Effect类集成和提供的。我们将使用这个类来调用和使用 GWT 应用中的各种效果。与本章的其他部分不同,我们这里不使用 JSNI,而是展示如何在应用程序中使用现有的包装库来提供一些漂亮的浏览器效果。
行动时间-应用效果
gwt-widget库是由 Robert Hanson 维护的 GWT 框架的一组扩展和增强,它提供了一个包装效果的 Java 类,我们将在我们的应用程序中使用这个类。我们将添加一个包含两行四列的网格,每个包含一个小图像文件,并对每个图像应用一个效果。
我们需要引用提供库的 Java 包装器的gwt-widgets模块。这是利用了 GWT 的模块继承特性。我们将在本示例的刚刚发生了什么?部分对这个概念进行解释。按照以下步骤添加网格:
- 在
com.packtpub.gwtbook.samples包中的现有Samples.gwt.xml文件中添加以下条目:
<inherits name='org.gwtwidgets.WidgetLibrary'/>
- 添加上述模块使用的原型和
Script.aculo.usJavaScript 文件:
<script type="text/JavaScript"src="img/prototype.js">
</script>
<script type="text/JavaScript src="img/Scriptaculous.js">
</script>
- 在
com.packtpub.gwtbook.samples.client.panels包中的新的 Java 文件ScriptaculousEffectsPanel.java中创建这个应用程序的用户界面。创建一个包含两行四列的网格。创建八个图像,八个按钮和一个工作面板。
private HorizontalPanel workPanel = new HorizontalPanel();
private Grid grid = new Grid(2, 4);
private Image packtlogo1 = new Image("images/packtlogo.jpg");
private Image packtlogo2 = new Image("images/packtlogo.jpg");
private Image packtlogo3 = new Image("images/packtlogo.jpg");
private Image packtlogo4 = new Image("images/packtlogo.jpg");
private Image packtlogo5 = new Image("images/packtlogo.jpg");
private Image packtlogo6 = new Image("images/packtlogo.jpg");
private Image packtlogo7 = new Image("images/packtlogo.jpg");
private Image packtlogo8 = new Image("images/packtlogo.jpg");
private Button fadeButton = new Button("fade");
private Button puffButton = new Button("puff");
private Button shakeButton = new Button("shake");
private Button growButton = new Button("grow");
private Button shrinkButton = new Button("shrink");
private Button pulsateButton = new Button("pulsate");
private Button blindUpButton = new Button("blindup");
private Button blindDownButton = new Button("blinddown");
- 将淡出效果的按钮和图像添加到
VerticalPanel中,并将面板添加到网格中。
VerticalPanel gridCellPanel = new VerticalPanel();
gridCellPanel.add(packtlogo1);
gridCellPanel.add(fadeButton);
grid.setWidget(0, 0, gridCellPanel);
- 添加一个事件处理程序,监听淡出效果按钮的点击,并调用适当的
Script.aculo.us效果。
fadeButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
Effect.fade(packtlogo1);
}
});
- 将摇晃效果的按钮和图像添加到
VerticalPanel中,并将面板添加到网格中。
gridCellPanel = new VerticalPanel();
gridCellPanel.add(packtlogo3);
gridCellPanel.add(shakeButton);
grid.setWidget(0, 1, gridCellPanel);
- 添加一个事件处理程序,监听摇晃效果按钮的点击,并调用适当的
Script.aculo.us效果。
shakeButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
Scrip.aculo.useffects, applying{
Effect.shake(packtlogo3);
}
});
- 将增长效果的按钮和图像添加到
VerticalPanel中,并将面板添加到网格中。
gridCellPanel = new VerticalPanel();
gridCellPanel.add(packtlogo4);
gridCellPanel.add(growButton);
grid.setWidget(0, 2, gridCellPanel);
- 添加一个事件处理程序,监听增长效果按钮的点击,并调用适当的
Script.aculo.us效果。
growButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
Effect.grow(packtlogo4);
}
});
- 将盲目上升效果的按钮和图像添加到
VerticalPanel中,并将面板添加到网格中。
gridCellPanel = new VerticalPanel();
gridCellPanel.add(packtlogo8);
gridCellPanel.add(blindUpButton);
grid.setWidget(0, 3, gridCellPanel);
- 添加一个事件处理程序,监听盲目上升效果按钮的点击,并调用适当的
Script.aculo.us效果。
blindUpButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
Effect.blindUp(packtlogo8);
}
});
- 将膨胀效果的按钮和图像添加到
VerticalPanel中,并将面板添加到网格中。
gridCellPanel = new VerticalPanel();
gridCellPanel.add(packtlogo2);
gridCellPanel.add(puffButton);
grid.setWidget(1, 0, gridCellPanel);
- 添加一个事件处理程序,监听膨胀效果按钮的点击,并调用适当的
Script.aculo.us效果。
puffButton.addClickListener(new ClickListener()
{
Scrip.aculo.useffects, applyingpublic void onClick(Widget sender)
{
Effect.puff(packtlogo2);
}
});
- 将收缩效果的按钮和图像添加到
VerticalPanel中,并将面板添加到网格中。
gridCellPanel = new VerticalPanel();
gridCellPanel.add(packtlogo5);
gridCellPanel.add(shrinkButton);
grid.setWidget(1, 1, gridCellPanel);
- 添加一个事件处理程序,监听收缩效果按钮的点击,并调用适当的
Script.aculo.us效果。
shrinkButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
Effect.shrink(packtlogo5);
}
});
- 将脉动效果的按钮和图像添加到
VerticalPanel中,并将面板添加到网格中。
gridCellPanel = new VerticalPanel();
gridCellPanel.add(packtlogo6);
gridCellPanel.add(pulsateButton);
grid.setWidget(1, 2, gridCellPanel);
- 添加一个事件处理程序,监听脉动效果按钮的点击,并调用适当的
Script.aculo.us效果。
pulsateButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
Effect.pulsate(packtlogo6);
}
});
- 最后,创建一个小的信息面板,显示关于这个应用程序的描述性文本,这样当在我们的
Samples应用程序的可用样本列表中选择此样本时,我们就可以显示这个文本。将信息面板和工作面板添加到一个停靠面板,并初始化小部件。
HorizontalPanel infoPanel =
new HorizontalPanel();infoPanel.add
(new HTML("<div class='infoProse'>
Use nifty scriptaculous effects
in GWT applications.
</div>"));
workPanel.setStyleName("scriptaculouspanel");
workPanel.add(grid);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
- 在 Eclipse 中的构建路径中添加
gwt-widgets.jar,以便它可以找到引用的类。
这个应用程序中有以下各种效果:

点击每个按钮,看看应用于图像的相应效果。
刚刚发生了什么?
模块是包含 GWT 项目的配置设置的 XML 文件。我们已经看到并使用了我们的Samples项目的模块。这是我们引用了应用程序使用的外部 JavaScript 文件的文件,以及我们应用程序使用的 RPC 服务的条目等。GWT 模块还具有从其他模块继承的能力。这使得继承模块可以使用在继承模块中声明的资源。它可以防止重复资源映射的问题,并促进重用,使得很容易将 GWT 库打包为模块并在项目之间分发和重用。我们可以通过使用inherits标签并提供模块的完全限定名称来指定要继承的模块。所有 GWT 应用程序都必须继承自com.google.gwt.user.User模块,该模块提供了核心网络工具包项目。在这个例子中,我们继承自org.gwtwidgets.WidgetLibrary,该库提供了我们在应用程序中使用的Script.aculo.us效果类。以下是我们在Samples.gwt.xml文件中定义这种继承的方式:
<inherits name='org.gwtwidgets.WidgetLibrary'/>
Script.aculo.us效果分为两种不同类型——核心效果和组合效果。核心效果是该库的基础,组合效果混合并使用核心效果来创建组合效果。该库中的核心效果包括:
-
不透明度:设置元素的透明度。
-
缩放:平滑地缩放元素。
-
移动:将元素移动给定数量的像素。
-
突出显示:通过改变其背景颜色并闪烁来吸引元素的注意力。
-
并行:多个效果同时应用于元素。
上述核心效果混合在一起,创建以下组合效果:
-
淡化:使元素淡出。
-
膨胀:使元素在烟雾中消失。
-
摇动:将元素重复向左和向右移动。
-
盲目下降:模拟窗帘在元素上下降。
-
盲目上升:模拟窗帘在元素上升。
-
脉动:使元素淡入淡出,并使其看起来像是在脉动。
-
增长:增大元素的大小。
-
收缩:减小元素的大小。
-
压缩:通过将元素收缩到其左侧来减小元素。
-
折叠:首先将元素减少到其顶部,然后到其左侧,最终使其消失。
我们在每个网格单元格内放置一个图像和一个按钮。当单击按钮时,我们会对位于按钮上方的图像元素应用效果。我们通过在org.gwtwidgets.client.wrap.Effect类中的所需效果方法中提供小部件对象来调用效果。该类中的所有方法都是静态的,并且该类中的每个Script.aculo.us效果都有一个相应命名的方法。因此,为了淡化一个元素,我们调用Effect.fade()方法,并提供要应用效果的图像小部件。这些过渡效果是为我们的应用程序增添光彩并提供更好的用户体验的一种非常好的方式。您还可以以不同的方式混合和匹配提供的效果,以创建和使用自定义效果。
总结
我们已经介绍了几个 JavaScript 库及其在 GWT 应用程序中的使用。在使用所有这些库时非常重要的一点是,包含大量 JavaScript 将增加浏览器加载的冗余,并几乎肯定会增加页面加载时间,并使运行速度变慢。因此,请谨慎使用视觉效果,不要过度使用。另一个注意事项是,在应用程序中使用 JSNI 时缺乏可移植性。这可能导致您的应用程序在不同版本的浏览器中运行方式大不相同。
在本章中,我们学习了关于 JSNI。我们利用 JSNI 来包装Moo.fx库并使用其效果。我们还包装了Rico库的不同部分,并利用它来为标签创建圆角和颜色选择器应用程序。我们使用了gwt-widgets库提供的Script.aculo.us效果。在这种情况下,我们使用了现有的库来提供效果。我们还学习了如何在 GWT 中使用模块继承。
在下一章中,我们将学习如何创建可以在项目之间共享的自定义 GWT 小部件。
第七章:自定义小部件
GWT 提供了各种各样的小部件,例如标签,文本框,树等,供您在应用程序中使用。这些小部件为构建用户界面提供了一个良好的起点,但几乎总是不会提供您所需的一切。这就是通过组合现有的小部件以更新和创新的方式创建自定义小部件的概念,或者从头开始编写新的小部件变得方便的地方。在本章中,我们将解决网页中常用的两个功能——日历显示和天气状况显示。由于 GWT 当前未提供这两个功能,我们将创建这两个小部件。我们还将学习如何打包它们,以便在必要时可以在不同的 GWT 项目中重用它们。
我们将要解决的任务是:
-
日历小部件
-
天气小部件
日历小部件
我们将创建一个可重用的日历小部件,可以轻松地在多个 GWT 应用程序中使用。这个小部件基于 Alexei Sokolov 的简单日历小部件(gwt.components.googlepages.com/calendar)。我们将对其进行调整以满足我们的要求。日历将显示当前日期以及当前月份的列表,并将允许通过日历向前或向后导航。我们还将提供一种方法,无论我们在日历中导航到哪里,都可以返回到当前日期。
行动时间——创建日历
现在我们将创建一个日历小部件。步骤如下:
- 创建一个新的小部件项目,用于包含我们自定义小部件的构件。我们将在这个项目中创建我们的小部件,然后在我们原始的
Samples项目中的应用程序中使用它。当我们创建新项目时,Widgets.gwt.xml文件将自动为我们创建,并且默认情况下,它将包含从User模块继承的以下条目。这是每个 GWT 模块都需要继承的一个模块:
<inherits name='com.google.gwt.user.User'/>
- 在
com.packtpub.gwtbook.widgets.client包中创建一个名为CalendarWidget.java的新的 Java 文件,它扩展了com.google.gwt.user.client.ui.Composite类,并实现了com.google.gwt.user.client.ui.ClickListener接口:
public class CalendarWidget extends Composite implements
ClickListener
{
}
- 创建创建导航栏以在日历中前进和后退的元素,以及一个将是日历本身的容器的
DockPanel类:
private DockPanel navigationBar = new DockPanel();
private Button previousMonth = new Button("<", this);
private Button nextMonth = new Button(">", this);
private final DockPanel outerDockPanel = new DockPanel();
- 创建字符串数组来存储一周中的工作日名称和一年中月份的名称。我们将从这些数组中检索名称以在用户界面中显示:
private String[] daysInWeek = new String[] { "Sunday",
"Monday", "Tuesday","Wednesday", "Thursday", "Friday",
"Saturday"};
private String[] monthsInYear = new String[] { "January",
"February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"};
- 创建一个变量来保存用于显示日历标题的 HTML。创建标签以显示当前日期的工作日和日期。还要创建和初始化一个包含当前日期的私有变量:
private HTML calendarTitle = new HTML();
private Label dayOfWeek = new Label("");
private Label dateOfWeek = new Label("");
private Date currentDate = new Date();
- 创建一个新的
Grid对象,覆盖“clearCell()”方法以设置列单元格的文本:
private final Grid calendarGrid = new Grid(7, 7)
{
public boolean clearCell(int row, int column)
{
boolean retValue = super.clearCell(row, column);
Element td = getCellFormatter().getElement(row, column);
DOM.setInnerHTML(td, "");
return retValue;
}
};
- 创建一个名为
CalendarCell的私有静态类,它扩展了HTML类:
private static class CalendarCell extends HTML
{
private int day;
public CalendarCell(String cellText, int day)
{
super(cellText);
this.day = day;
}
public int getDay()
{
return day;
}
}
这个类的一个实例将被添加到我们之前创建的grid对象中,以在一个单元格中显示一个日历元素。
- 为
CalendarWidget类添加访问器,以获取当前日期以及当前日期的日,月和年组件:
public int getYear()
{
return 1900 + currentDate.getYear();
}
public int getMonth()
{
return currentDate.getMonth();
}
public int getDay()
{
return currentDate.getDate();
}
public Date getDate()
{
return currentDate;
}
这些方法将用于检索给定日历日期的个别数据。
- 为
CalendarWidget类添加修改currentDate变量的日,月和年组件的 mutators:
private void setDate(int year, int month, int day)
{
currentDate = new Date(year - 1900, month, day);
}
private void setYear(int year)
{
currentDate.setYear(year - 1900);
}
private void setMonth(int month)
{
currentDate.setMonth(month);
}
- 创建一个计算当前月份之前一个月的日历的方法:
public void computeCalendarForPreviousMonth()
{
int month = getMonth() - 1;
if (month < 0)
{
setDate(getYear() - 1, 11, getDay());
}
else
{
setMonth(month);
}
renderCalendar();
}
当用户点击按钮导航到上一个月时,我们将使用它。
- 创建一个计算当前月份之后一个月的日历的方法:
public void computeCalendarForNextMonth()
{
int month = getMonth() + 1;
if (month > 11)
{
setDate(getYear() + 1, 0, getDay());
}
else
{
setMonth(month);
}
renderCalendar();
}
当用户点击按钮导航到下一个月时,我们将使用它。
- 创建一个计算给定月份天数的方法。目前没有获取此信息的简单方法;因此我们需要计算它:
private int getDaysInMonth(int year, int month)
{
switch (month)
{
case 1:
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
return 29;
else
return 28;
case 3:
return 30;
case 5:
return 30;
case 8:
return 30;
case 10:
return 30;
default:
return 31;
}
}
- 创建一个
renderCalendar()方法,可以绘制日历及其所有元素。获取当前设置的date对象的各个组件,设置日历标题,并格式化日历网格。还要计算月份和当前日期的天数,并设置日期和工作日标签值。最后,将grid单元格的值设置为计算出的日历值:
private void renderCalendar()
{
int year = getYear();
int month = getMonth();
int day = getDay();
calendarTitle.setText(monthsInYear[month] + " " + year);
calendarGrid.getRowFormatter().setStyleName(0, "weekheader");
for (int i = 0; i < daysInWeek.length; i++)
{
calendarGrid.getCellFormatter().setStyleName(0, i, "days");
calendarGrid.setText(0, i, daysInWeek[i].substring(0, 1));
}
Date now = new Date();
int sameDay = now.getDate();
int today = (now.getMonth() == month && now.getYear() + 1900
== year) ? sameDay : 0;
int firstDay = new Date(year - 1900, month, 1).getDay();
int numOfDays = getDaysInMonth(year, month);
int weekDay = now.getDay();
dayOfWeek.setText(daysInWeek[weekDay]);
dateOfWeek.setText("" + day);
int j = 0;
for (int i = 1; i < 6; i++)
{
for (int k = 0; k < 7; k++, j++)
{
int displayNum = (j - firstDay + 1);
if (j < firstDay || displayNum > numOfDays)
{
calendarGrid.getCellFormatter().setStyleName(i, k,
"empty");
calendarGrid.setHTML(i, k, " ");
}
else
{
HTML html = new calendarCell("<span>"+
String.valueOf(displayNum) + "</span>",displayNum);
html.addClickListener(this);
calendarGrid.getCellFormatter().setStyleName(i, k,
"cell");
if (displayNum == today)
{
calendarGrid.getCellFormatter().addStyleName(i, k,
"today");
}
else if (displayNum == sameDay)
{
calendarGrid.getCellFormatter().addStyleName(i, k,
"day");
}
calendarGrid.setWidget(i, k, html);
}
}
}
}
- 创建构造函数
CalendarWidget(),以初始化和布局组成我们日历小部件的各种元素:
HorizontalPanel hpanel = new HorizontalPanel();
navigationBar.setStyleName("navbar");
calendarTitle.setStyleName("header");
HorizontalPanel prevButtons = new HorizontalPanel();
prevButtons.add(previousMonth);
HorizontalPanel nextButtons = new HorizontalPanel();
nextButtons.add(nextMonth);
navigationBar.add(prevButtons, DockPanel.WEST);
navigationBar.setCellHorizontalAlignment(prevButtons,
DockPanel.ALIGN_LEFT);
navigationBar.add(nextButtons, DockPanel.EAST);
navigationBar.setCellHorizontalAlignment(nextButtons,
DockPanel.ALIGN_RIGHT);
navigationBar.add(calendarTitle, DockPanel.CENTER);
navigationBar.setVerticalAlignment(DockPanel.ALIGN_MIDDLE);
navigationBar.setCellHorizontalAlignment(calendarTitle,
HasAlignment.ALIGN_CENTER);
navigationBar.setCellVerticalAlignment(calendarTitle,
HasAlignment.ALIGN_MIDDLE);
navigationBar.setCellWidth(calendarTitle, "100%");
- 在构造函数中,使用我们在第六章中创建的
Rico类来包装将容器面板。正如我们在第六章中学到的,Rico类具有可以用于访问舍入方法的静态方法。我们直接使用了之前创建的Rico类来保持简单,但另一种方法是将Rico相关功能拆分为自己的独立模块,然后在这里使用它。使用此容器面板初始化小部件:
initWidget(hpanel);
calendarGrid.setStyleName("table");
calendarGrid.setCellSpacing(0);
DOM.setAttribute(hpanel.getElement(), "id", "calDiv");
DOM.setAttribute(hpanel.getElement(), "className",
"CalendarWidgetHolder");
Rico.corner(hpanel.getElement(), null);
hpanel.add(outerDockPanel);
- 此外,在构造函数中,将导航栏、日历网格和今天按钮添加到垂直面板中:
VerticalPanel calendarPanel = new VerticalPanel();
calendarPanel.add(navigationBar);
VerticalPanel vpanel = new VerticalPanel();
calendarPanel.add(calendarGrid);
calendarPanel.add(todayButton);
- 注册事件处理程序以侦听今天按钮的点击事件,并重新绘制到当前日期的日历:
todayButton.setStyleName("todayButton");
todayButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
currentDate = new Date();
renderCalendar();
}
});
- 为日和工作日标签添加样式,并将小部件添加到垂直面板中:
dayOfWeek.setStyleName("dayOfWeek");
dateOfWeek.setStyleName("dateOfWeek");
vpanel.add(dayOfWeek);
vpanel.add(dateOfWeek);
- 将这两个面板添加到小部件的主面板中:
outerDockPanel.add(vpanel, DockPanel.CENTER);
outerDockPanel.add(calendarPanel, DockPanel.EAST);
- 绘制日历并注册以接收所有点击事件:
renderCalendar();
setStyleName("CalendarWidget");
this.sinkEvents(Event.ONCLICK);
-
创建一个包含我们创建的小部件的 JAR 文件。您可以使用 Eclipse 内置的 JAR Packager 工具导出 JAR 文件。从文件菜单中选择导出,您将看到一个类似于此的屏幕:
![执行时间-创建日历]()
-
填写下一个截图中显示的信息,以创建 JAR,并选择要包含在其中的资源:
![执行时间-创建日历]()
-
\创建 JAR 文件并另存为
widgets_jar_desc.jardesc,以便我们在需要时可以轻松重新创建 JAR。如下截图所示:![执行时间-创建日历]()
-
现在我们已经成功创建了名为
widgets.jar的 JAR 文件,其中包含我们的日历小部件,让我们实际在不同的项目中使用它。将此 JAR 添加到我们的SamplesEclipse 项目的buildpath中,以便可以在项目的classpath上找到我们需要的类。 -
我们还需要将
widgets.jar文件添加到托管模式和 Web 模式的脚本中。修改Samples-shell.cmd文件和Samples-compile.cmd文件,以添加此 JAR 文件的路径。 -
修改
Samples项目的模块 XML 文件Samples.gwt.xml,以继承自小部件模块。在文件中添加以下条目:
<inherits name='com.packtpub.gwtbook.widgets.Widgets'/>
这个条目是 GWT 框架的一个指示器,表明当前模块将使用来自com.packtpub.gwtbook.widgets.Widgets模块的资源。GWT 还提供了自动资源注入机制,自动加载模块使用的资源。这是通过创建具有对模块使用的外部 JavaScript 和 CSS 文件的引用的模块来实现的,当您创建可重用模块并希望确保模块的用户可以访问模块使用的特定样式表或 JavaScript 文件时,这将非常有用。
在我们的情况下,我们可能可以重写并拆分我们在第六章中添加的Rico支持为自己的模块,但为了简单起见,我们将其原样使用。
- 在
Samples项目的com.packtpub.gwtbook.samples.client.panels包中的新 Java 文件CalendarWidgetPanel.java中为日历小部件应用程序创建用户界面。创建一个工作面板来容纳日历示例:
private VerticalPanel workPanel = new VerticalPanel();
- 在构造函数中,创建一个新的
CalendarWidget类并将其添加到面板中。创建一个小信息面板,显示关于此应用程序的描述性文本,以便在我们的Samples应用程序的可用示例列表中选择此示例时显示文本。将信息面板和工作面板添加到一个停靠面板,并初始化小部件:
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML
("<div class='infoProse'>Click on the navigation buttons to
go forward and backward through the calendar. When you
want to come back to todays date, click on the Today
button.</div>"));
CalendarWidget calendar = new CalendarWidget();
workPanel.add(calendar);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
运行应用程序以查看日历小部件的操作:

刚刚发生了什么?
自定义小部件封装了功能并实现了在多个项目中的重用。创建自定义 GWT 小部件有三种方法:
-
Composite:
Composite是一个特殊的 GWT 类,它本身就是一个小部件,并且可以作为其他小部件的容器。这让我们可以轻松地组合包含任意数量组件的复杂小部件。 -
Java: 从头开始创建一个类似于 GWT 的所有基本小部件(如
Button)的小部件。 -
JavaScript: 实现一个小部件,其方法调用 JavaScript。应该谨慎选择此方法,因为代码需要仔细考虑跨浏览器的影响。
普通的 GWT 小部件只是 HTML 元素的包装器。复合小部件是由几个简单小部件组成的复杂小部件。它控制了对小部件的客户端公开访问的方法。因此,您可以仅公开您想要的事件。Composite是构建小部件的最简单和最快的方法。在这个例子中,我们通过扩展Composite类创建了一个日历小部件,并向其添加了各种组件。日历由两个主要面板组成——左侧显示工作日和实际日期,而右侧面板显示实际日历以及用于通过日历向前和向后导航的按钮。您可以使用这些按钮转到不同的日期。任何时候您想要返回到今天日期的日历,点击今天按钮,日历将再次呈现为当前日期。
我们创建了一个名为HorizontalPanel的容器,其中包含日历小部件的各种组件。通过使用我们在上一章中创建的Rico库,该面板被赋予了漂亮的圆角效果。
DOM.setAttribute(hpanel.getElement(), "id", "calDiv");
DOM.setAttribute(hpanel.getElement(), "className",
"CalendarWidgetHolder");
Rico.corner(hpanel.getElement(), null);
对于日历,我们使用了一个具有七行七列的Grid对象。我们重写了它的clearCell()方法,通过将TD元素的文本设置为空字符串来清除单元格的内容:
public boolean clearCell(int row, int column)
{
boolean retValue = super.clearCell(row, column);
Element td = getCellFormatter().getElement(row, column);
DOM.setInnerHTML(td, "");
return retValue;
}
这个网格是通过将每个单元格填充CalendarCell来创建的。这是一个我们创建的自定义类,其中每个单元格都可以采用 HTML 片段作为文本,并且让我们布局一个更好的网格。
private static class calendarCell extends HTML
{
private int day;
public calendarCell(String cellText, int day)
{
super(cellText);
this.day = day;
}
public int getDay()
{
return day;
}
}
renderCalendar()方法在这个小部件中完成了大部分工作。它设置了工作日和日期的值,并绘制了日历本身。当我们创建日历网格时,我们为每个单独的单元格设置样式。如果单元格恰好是当前日期,我们将其设置为不同的样式;因此在视觉上,我们可以立即通过查看网格来辨别当前日期。当日历小部件初始化时,它会自动绘制当前日期的日历。导航栏包含两个按钮——一个用于向前导航到下一个月,另一个按钮用于向后导航到上一个月。当点击其中一个导航按钮时,我们重新绘制日历。因此,例如,当我们点击上一个按钮时,我们计算上一个月并重新绘制日历。
public void computeCalendarForPreviousMonth()
{
int month = getMonth() - 1;
if (month < 0)
{
setDate(getYear() - 1, 11, getDay());
}
else
{
setMonth(month);
}
renderCalendar();
}
我们还在日历中添加了一个按钮,以便让我们将日历重绘到当前日期。在日历中向前或向后导航后,我们可以单击今天按钮,使日历呈现为当前日期:
todayButton.addClickListener(new ClickListener()
{
public void onClick(Widget sender)
{
currentDate = new Date();
renderCalendar();
}
});
我们利用 Eclipse 中的内置功能将我们的小部件资源导出为 JAR 文件。这个 JAR 文件可以在团队或项目之间共享和重复使用。我们在Samples项目中使用这个导出的widgets.jar文件,通过创建一个简单的面板,实例化日历小部件,并将其添加到面板中。该文件还需要添加到项目的compile和shell批处理文件中;以便在运行这些命令时可以在classpath上找到它。我们可以通过使用 JDK 1.4+版本中提供的Calendar类来以更简单的方式进行一些日期操作。然而,我们无法使用Calendar类,因为它目前不是 GWT 框架提供的 JRE 类之一。因此,如果我们使用它,就会出现编译错误。如果将来这个类得到 GWT 的支持,那么将很容易切换到使用Calendar类提供的功能来执行一些日期操作。
天气小部件
我们将创建一个天气小部件,使用 Yahoo Weather RSS 服务来检索天气信息并显示当前的天气状况。我们将创建一个提供此功能的 RPC 服务,然后在我们的小部件中使用 RPC 来显示给定美国 ZIP 码的天气信息。此小部件的用户界面将包含当前天气状况的图像,以及通过 Yahoo 天气服务可用的所有其他与天气相关的信息。
行动时间-创建天气信息服务
此小部件也将在我们在上一节中用来创建日历小部件的相同小部件项目中创建。步骤如下:
- 在
com.packtpub.gwtbook.widgets.client包中创建一个名为Weather.java的新的 Java 文件。这个类将封装给定 ZIP 码的所有与天气相关的信息,并将用作我们稍后在本示例中创建的 RPC 服务的返回参数。我们还可以使用最近添加的 GWT 支持客户端 XML 解析来读取返回给客户端的 XML 字符串。我们将在第九章中学习有关 GWT 的 XML 支持。现在,我们将使用一个简单的对象来封装返回的天气信息。这将使我们能够专注于自定义小部件功能并保持简单。为每个属性创建变量:
private String zipCode = "";
private String chill = "";
private String direction = "";
private String speed = "";
private String humidity = "";
private String visibility = "";
private String pressure = "";
private String rising = "";
private String sunrise = "";
private String sunset = "";
private String latitude = "";
private String longitude = "";
private String currentCondition = "";
private String currentTemp = "";
private String imageUrl = "";
private String city = "";
private String state = "";
private String error = "";
- 添加获取和设置此类的各种与天气相关的属性的方法。以下是获取和设置寒意、城市、当前状况和当前温度的方法:
public String getChill()
{
return chill;
}
public void setChill(String chill)
{
this.chill = chill;
}
public String getCity()
{
return city;
}
public void setCity(String city)
{
this.city = city;
}
public String getCurrentCondition()
{
return currentCondition;
}
public void setCurrentCondition(String currentCondition)
{
this.currentCondition = currentCondition;
}
public String getCurrentTemp()
{
return currentTemp;
}
public void setCurrentTemp(String currentTemp)
{
this.currentTemp = currentTemp;
}
- 添加获取和设置方向、错误、湿度和图像 URL 的方法:
public String getDirection()
{
return direction;
}
public void setDirection(String direction)
{
this.direction = direction;
}
public String getError()
{
return error;
}
public void setError(String error)
{
this.error = error;
}
public String getHumidity()
{
return humidity;
}
public void setHumidity(String humidity)
{
this.humidity = humidity;
}
public String getImageUrl()
{
return imageUrl;
}
public void setImageUrl(String imageUrl)
{
this.imageUrl = imageUrl;
}
- 添加获取和设置纬度、经度、压力和气压升高的方法:
public String getLatitude()
{
return latitude;
}
public void setLatitude(String latitude)
{
this.latitude = latitude;
}
public String getLongitude()
{
return longitude;
}
public void setLongitude(String longitude)
{
this.longitude = longitude;
}
public String getPressure()
{
return pressure;
}
public void setPressure(String pressure)
{
this.pressure = pressure;
}
public String getRising()
{
return rising;
}
public void setRising(String rising)
{
this.rising = rising;
}
- 为获取和设置速度、状态、日出和日落值添加方法:
public String getSpeed()
{
return speed;
}
public void setSpeed(String speed)
{
this.speed = speed;
}
public String getState()
{
return state;
}
public void setState(String state)
{
this.state = state;
}
public String getSunrise()
{
return sunrise;
}
public void setSunrise(String sunrise)
{
this.sunrise = sunrise;
}
public String getSunset()
{
return sunset;
}
public void setSunset(String sunset)
{
this.sunset = sunset;
}
- 添加获取和设置可见性和 ZIP 码的方法:
public String getVisibility()
{
return visibility;
}
public void setVisibility(String visibility)
{
this.visibility = visibility;
}
public String getZipCode()
{
return zipCode;
}
public void setZipCode(String zipCode)
{
this.zipCode = zipCode;
}
- 创建
Weather()构造函数来创建一个weather对象:
public Weather(String zipCode, String chill, String direction,
String speed, String humidity, String visibility, String
pressure, String rising, String sunrise, String sunset,
String latitude, String longitude, String currentCondition,
String currentTemp, String imageUrl, String city, String
state)
{
this.zipCode = zipCode;
this.chill = chill;
this.direction = direction;
this.speed = speed;
this.humidity = humidity;
this.visibility = visibility;
this.pressure = pressure;
this.rising = rising;
this.sunrise = sunrise;
this.sunset = sunset;
this.latitude = latitude;
this.longitude = longitude;
this.currentCondition = currentCondition;
this.currentTemp = currentTemp;
this.imageUrl = imageUrl;
this.city = city;
this.state = state;
}
- 在
com.packtpub.gwtbook.widgets.client包中创建一个名为WeatherService.java的新的 Java 文件。这是天气服务的服务定义。定义一个方法,通过提供 ZIP 码来检索天气数据:
public interface WeatherService extends RemoteService
{
public Weather getWeather(String zipCode);
}
- 在
com.packtpub.gwtbook.widgets.client包中的一个新的 Java 文件中创建此服务定义接口的异步版本,命名为WeatherServiceAsync.java:
public interface WeatherServiceAsync
{
public void getWeather(String zipCode, AsyncCallback
callback);
}
- 在
com.packtpub.gwtbook.widgets.server包中的一个新的 Java 文件WeatherServiceImpl.java中创建天气服务的实现。在这个示例中,我们将使用Dom4j(www.dom4j.org/)和Jaxen(jaxen.codehaus.org/)项目中的两个第三方库,以便更容易地解析 Yahoo RSS 源。下载这些库的当前版本到lib文件夹中。将dom4j-xxx.jar和jaxen-xxx.jar添加到 Eclipse 的buildpath中。添加必要的代码来通过访问 Yahoo Weather RSS 服务检索给定 ZIP 码的天气数据。
首先创建一个 SAX 解析器:
public Weather getWeather(String zipCode)
{
SAXReader reader = new SAXReader();
Weather weather = new Weather();
Document document;
}
- 检索所提供的 ZIP 码的 RSS 文档:
try
{
document = reader.read(new URL
("http://xml.weather.yahoo.com/forecastrss?p=" + z ipCode));
}
catch (MalformedURLException e)
{
e.printStackTrace();
}
catch (DocumentException e)
{
e.printStackTrace();
}
- 创建一个新的 XPath 表达式,并将我们感兴趣的命名空间添加到表达式中:
XPath expression = new Dom4jXPath("/rss/channel");
expression.addNamespace("yweather",
"http://xml.weather.yahoo.com/ns/rss/1.0");
expression.addNamespace("geo",
"http://www.w3.org/2003/01/geo/wgs84_pos#");
我们稍后将使用这个表达式来从文档中获取我们需要的数据。
- 选择检索到的 XML 文档中的根节点,并检查是否有任何错误。如果在 XML 中发现任何错误,则返回一个带有错误消息设置的
weather对象:
Node result = (Node) expression.selectSingleNode(document);
String error = result.valueOf("/rss/channel/description");
if (error.equals("Yahoo! Weather Error"))
{
weather.setError("Invalid zipcode "+ zipCode+
" provided. No weather information available for this
location.");
return weather;
}
- 使用 XPath 选择描述部分,然后解析它以确定与返回的天气数据相关的图像的 URL。将这些信息设置在
weather对象的ImageUrl属性中:
String descriptionSection = result.valueOf
("/rss/channel/item/description");
weather.setImageUrl(descriptionSection.substring
(descriptionSection.indexOf("src=") + 5,
descriptionSection.indexOf(".gif") + 4));
- 使用 XPath 表达式从 XML 文档中选择我们感兴趣的所有数据,并设置
weather对象的各种属性。最后,将对象作为我们服务的返回值返回:
weather.setCity(result.valueOf("//yweather:location/@city"));
weather.setState(result.valueOf
("//yweather:location/@region"));
weather.setChill(result.valueOf("//yweather:wind/@chill"));
weather.setDirection(result.valueOf
("//yweather:wind/@direction"));
weather.setSpeed(result.valueOf("//yweather:wind/@speed"));
weather.setHumidity(result.valueOf
("//yweather:atmosphere/@humidity"));
weather.setVisibility(result.valueOf
("//yweather:atmosphere/@visibility"));
weather.setPressure(result.valueOf
("//yweather:atmosphere/@pressure"));
weather.setRising(result.valueOf
("//yweather:atmosphere/@rising"));
weather.setSunrise(result.valueOf
("//yweather:astronomy/@sunrise"));
weather.setSunset(result.valueOf
("//yweather:astronomy/@sunset"));
weather.setCurrentCondition(result.valueOf
("//yweather:condition/@text"));
weather.setCurrentTemp(result.valueOf
("//yweather:condition/@temp"));
weather.setLatitude(result.valueOf("//geo:lat"));
weather.setLongitude(result.valueOf("//geo:long"));
return weather;
- 我们的服务器端实现现在已经完成。在
com.packtpub.gwtbook.widgets.client包中创建一个新的 Java 文件WeatherWidget.java,它扩展了com.google.gwt.user.client.ui.Composite类,并实现了com.google.gwt.user.client.ui.ChangeListener接口:
public class WeatherWidget extends Composite implements
ChangeListener
{
}
- 在
WeatherWidget类中,创建用于显示当前天气图像、条件以及大气、风、天文和地理测量的面板:
private VerticalPanel imagePanel = new VerticalPanel();
private HorizontalPanel tempPanel = new HorizontalPanel();
private VerticalPanel tempHolderPanel = new VerticalPanel();
private HorizontalPanel currentPanel = new HorizontalPanel();
private HorizontalPanel windPanel = new HorizontalPanel();
private HorizontalPanel windPanel2 = new HorizontalPanel();
private HorizontalPanel atmospherePanel = new
HorizontalPanel();
private HorizontalPanel atmospherePanel2 = new
HorizontalPanel();
private HorizontalPanel astronomyPanel = new HorizontalPanel();
private HorizontalPanel geoPanel = new HorizontalPanel();
private Image image = new Image();
private Label currentTemp = new Label("");
private Label currentCondition = new Label("");
- 创建用于显示所有这些信息的标签,以及一个文本框,允许用户输入要在小部件中显示天气的地方的 ZIP 码:
private Label windChill = new Label("");
private Label windDirection = new Label("");
private Label windSpeed = new Label("");
private Label atmHumidity = new Label("");
private Label atmVisibility = new Label("");
private Label atmpressure = new Label("");
private Label atmRising = new Label("");
private Label astSunrise = new Label("");
private Label astSunset = new Label("");
private Label latitude = new Label("");
private Label longitude = new Label("");
private Label windLabel = new Label("Wind");
private Label astLabel = new Label("Astronomy");
private Label atmLabel = new Label("Atmosphere");
private Label geoLabel = new Label("Geography");
private Label cityLabel = new Label("");
private TextBox zipCodeInput = new TextBox();
- 创建和初始化
WeatherService对象,并设置天气服务的入口 URL:
final WeatherServiceAsync weatherService =
(WeatherServiceAsync) GWT.create(WeatherService.class);
ServiceDefTarget endpoint = (ServiceDefTarget) weatherService;
endpoint.setServiceEntryPoint(GWT.getModuleBaseURL() +
"weather");
- 创建
WeatherWidget()构造函数。在构造函数中,创建工作面板;用我们的主面板初始化小部件,并注册接收所有更改事件:
VerticalPanel workPanel = new VerticalPanel();
initWidget(workPanel);
this.sinkEvents(Event.ONCHANGE);
- 为工作面板设置
id,并像之前的示例一样使用Rico库来圆角面板:
DOM.setAttribute(workPanel.getElement(), "id", "weatherDiv");
DOM.setAttribute(workPanel.getElement(), "className",
"weatherHolder");
Rico.corner(workPanel.getElement(), null);
- 为每个元素添加必要的样式,并将元素添加到各个面板中:
image.setStyleName("weatherImage");
imagePanel.add(image);
currentCondition.setStyleName("currentCondition");
imagePanel.add(currentCondition);
currentPanel.add(imagePanel);
currentTemp.setStyleName("currentTemp");
tempPanel.add(currentTemp);
tempPanel.add(new HTML("<div class='degrees'>°</div>"));
tempHolderPanel.add(tempPanel);
cityLabel.setStyleName("city");
tempHolderPanel.add(cityLabel);
currentPanel.add(tempHolderPanel);
windDirection.setStyleName("currentMeasurementsDegrees");
windChill.setStyleName("currentMeasurementsDegrees");
windSpeed.setStyleName("currentMeasurements");
windPanel.add(windDirection);
windPanel.add(new HTML
("<div class='measurementDegrees'>°</div>"));
windPanel.add(windSpeed);
windPanel2.add(windChill);
windPanel2.add(new HTML
("<div class='measurementDegrees'>°</div>"));
atmHumidity.setStyleName("currentMeasurements");
atmpressure.setStyleName("currentMeasurements");
atmVisibility.setStyleName("currentMeasurements");
atmRising.setStyleName("currentMeasurements");
atmospherePanel.add(atmHumidity);
atmospherePanel.add(atmVisibility);
atmospherePanel2.add(atmpressure);
astSunrise.setStyleName("currentMeasurements");
astSunset.setStyleName("currentMeasurements");
astronomyPanel.add(astSunrise);
astronomyPanel.add(astSunset);
latitude.setStyleName("currentMeasurements");
longitude.setStyleName("currentMeasurements");
geoPanel.add(latitude);
geoPanel.add(longitude);
windLabel.setStyleName("conditionPanel");
atmLabel.setStyleName("conditionPanel");
astLabel.setStyleName("conditionPanel");
geoLabel.setStyleName("conditionPanel");
- 将所有面板添加到主工作面板中:
workPanel.add(currentPanel);
workPanel.add(windLabel);
workPanel.add(windPanel);
workPanel.add(windPanel2);
workPanel.add(atmLabel);
workPanel.add(atmospherePanel);
workPanel.add(atmospherePanel2);
workPanel.add(astLabel);
workPanel.add(astronomyPanel);
workPanel.add(geoLabel);
workPanel.add(geoPanel);
- 创建一个小面板用于输入 ZIP 码,以及一个缓冲面板将其与组成此小部件的其他面板分开。最后调用
getAndRenderWeather()方法来获取天气信息。创建这个方法:
HorizontalPanel bufferPanel = new HorizontalPanel();
bufferPanel.add(new HTML("<div> </div>"));
HorizontalPanel zipCodeInputPanel = new HorizontalPanel();
Label zipCodeInputLabel = new Label("Enter Zip:");
zipCodeInputLabel.setStyleName("zipCodeLabel");
zipCodeInput.setStyleName("zipCodeInput");
zipCodeInput.setText("90210");
zipCodeInput.addChangeListener(this);
zipCodeInputPanel.add(zipCodeInputLabel);
zipCodeInputPanel.add(zipCodeInput);
workPanel.add(zipCodeInputPanel);
workPanel.add(bufferPanel);
getAndRenderWeather(zipCodeInput.getText());
- 创建一个名为
getAndRenderWeather()的私有方法,用于从服务中获取天气信息并在我们的用户界面中显示它:
private void getAndRenderWeather(String zipCode)
{
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
Weather weather = (Weather) result;
if (weather.getError().length() > 0)
{
Window.alert(weather.getError());
return;
}
image.setUrl(weather.getImageUrl());
currentTemp.setText(weather.getCurrentTemp());
currentCondition.setText(weather.getCurrentCondition());
windDirection.setText("Direction : " +
weather.getDirection());
windChill.setText("Chill : " + weather.getChill());
windSpeed.setText("Speed : " + weather.getSpeed() +
" mph");
atmHumidity.setText("Humidity : " + weather.getHumidity()
+ " %");
atmpressure.setText("Barometer : "+ weather.getPressure()
+ " in and "+ getBarometerState(
Integer.parseInt(weather.getRising())));
atmVisibility.setText("Visibility : "+
(Integer.parseInt(weather.getVisibility()) / 100) + " mi");
astSunrise.setText("Sunrise : " + weather.getSunrise());
astSunset.setText("Sunset : " + weather.getSunset());
latitude.setText("Latitude : " + weather.getLatitude());
longitude.setText("Longitude : " +
weather.getLongitude());
cityLabel.setText(weather.getCity() + ", " +
weather.getState());
}
public void onFailure(Throwable caught)
{
Window.alert(caught.getMessage());
}
weatherService.getWeather(zipCode, callback);
- 添加一个私有方法,根据上升属性的整数值返回显示文本:
private String getBarometerState(int rising)
{
if (rising == 0)
{
return "steady";
}
else if (rising == 1)
{
return "rising";
}
else
{
return "falling";
}
}
- 为文本框添加事件处理程序,当用户在文本框中输入新的 ZIP 码时,获取并渲染新的天气信息:
public void onChange(Widget sender)
{
if (zipCodeInput.getText().length() == 5)
{
getAndRenderWeather(zipCodeInput.getText());
}
}
-
重新构建
widgets.jar文件以包含新的天气小部件。现在我们可以使用我们的新 JAR 文件来创建一个用户界面,实例化并使用这个小部件。 -
在
Samples项目的com.packtpub.gwtbook.samples.client.panels包中的一个新的 Java 文件WeatherWidgetPanel.java中创建天气小部件应用的用户界面。创建一个用于容纳天气小部件的工作面板:
private VerticalPanel workPanel = new VerticalPanel();
- 在构造函数中,创建一个新的
WeatherWidget并将其添加到面板中。由于我们已经在Samples.gwt.xml文件中从 widgets 模块继承,所有必需的类应该被正确解析。创建一个小的信息面板,显示关于该应用程序的描述性文本,这样当我们在Samples应用程序的可用样本列表中选择该样本时,我们就可以显示文本。将信息面板和工作面板添加到一个停靠面板中,并初始化小部件:
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML
("<div class='infoProse'>A custom widget for viewing the
weather conditions for a US city by entering the zipcode
in the textbox.</div>"));:
WeatherWidget weather = new WeatherWidget();
workPanel.add(weather);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
这是天气小部件的屏幕截图:

输入一个新的美国邮政编码以查看该地区的天气状况。
刚刚发生了什么?
Yahoo!天气通过 RSS 为提供的美国邮政编码提供天气数据和信息。真正简单的联合(RSS)是一个轻量级的 XML 格式,主要用于分发网页内容,如头条。提供的服务可以通过基于 URL 的格式访问,并通过将 ZIP 码作为 URL 的参数来提供。响应是一个可以解析和搜索所需数据的 XML 消息。
我们创建了一个 RPCWeatherService,它访问 Yahoo 服务,解析数据,并以简单的weather对象的形式提供给我们。这个Weather类模拟了单个 ZIP 码的天气。Weather类的每个实例都包含以下由我们的WeatherService设置的属性:
-
邮政编码:需要检索天气的邮政编码。 -
当前温度:当前温度。 -
当前条件:反映当前天气状况的文本。 -
寒冷:该位置的风寒。 -
方向:风向。 -
风速:该位置的当前风速。 -
湿度:该位置的当前湿度。 -
能见度:当前的能见度。 -
气压:当前的气压。 -
上升:用于通知气压是上升、下降还是稳定的指示器。 -
日出时间:日出时间。 -
日落时间:日落时间。 -
纬度:该位置的纬度。 -
经度:该位置的经度。 -
城市:与该邮政编码对应的城市。 -
州:与该邮政编码对应的州。 -
图像 URL:代表当前天气状况的图像的 URL。 -
错误:如果在检索给定 ZIP 码的天气信息时遇到任何错误,将设置此属性。这使得 UI 可以显示带有此错误的消息框。
我们在WeatherServiceImpl类中实现了getWeather()方法。在这个服务中,我们使用了Dom4j和Jaxen库中的类。这也意味着我们需要将这两个项目的两个 JAR 文件添加到 Eclipse 项目的buildpath中。Dom4j是一个快速且易于使用的 XML 解析器,支持通过 XPath 表达式搜索 XML。XPath 支持本身是由Jaxen项目的类提供的。我们通过使用 ZIP 码参数调用 Yahoo 天气服务 URL 来检索响应 XML 文档。使用 XPath 表达式搜索返回的 XML。我们为 XPath 表达式添加了yweather和geo的命名空间,因为响应 XML 中的一些元素位于这个不同的命名空间下:
document = reader.read(new URL
("http://xml.weather.yahoo.com/forecastrss?p=" + zipCode));
XPath expression = new Dom4jXPath("/rss/channel");
expression.addNamespace
("yweather","http://xml.weather.yahoo.com/ns/rss/1.0");
expression.addNamespace
("geo","http://www.w3.org/2003/01/geo/wgs84_pos#");
然后,我们使用 XPath 搜索响应,获取我们感兴趣的值,并为weather对象设置适当的属性。例如,这是我们如何获取该位置的城市和州的值,并为weather对象设置这些属性的方式:
weather.setCity(result.valueOf("//yweather:location/@city"));
weather.setState(result.valueOf("//yweather:location/@region"));
我们必须采取不同的方法来获取当前条件的图像 URL。这个 URL 嵌入在响应的 CDATA 部分中。因此,我们使用 XPath 表达式来获取此节点的文本,然后访问包含我们正在寻找的IMG标签的子字符串:
String descriptionSection = result.valueOf
("/rss/channel/item/description");
weather.setImageUrl(descriptionSection.substring
(descriptionSection.indexOf("src=") + 5,
descriptionSection.indexOf(".gif") + 4));
带有所有这些属性设置的weather对象作为对此服务调用的响应返回。现在我们创建我们的实际小部件,它将利用并调用此服务。用户界面由一个包含以下组件的漂亮圆角面板组成:
-
用于当前条件的图像——图像 URL。
-
实际的当前条件文本——如多云、晴等。
-
当前温度。
-
一个用于显示当前风况的部分——风寒、方向和速度。
-
一个用于显示当前大气条件的部分——湿度、能见度和气压及其变化方向。
-
一个用于显示当前天文数据的部分——日出和日落。
-
一个用于显示当前地理数据的部分——该位置的纬度和经度。
-
一个用于输入新邮政编码的文本框。
温度以度数显示,并且度数符号在代码中通过实体版本°显示。因此,我们在小部件中显示当前温度如下:
tempPanel.add(new HTML("<div class='degrees'>°</div>"));
当初始化此小部件时,服务被异步调用,当从WeatherService接收到响应时,相应的显示元素将被设置为它们的值。我们重新创建 JAR 文件,以包含此小部件,并在Samples项目中使用此小部件,通过实例化它并将其添加到面板中。由于我们已经在上一节中将widgets.jar文件添加到了classpath中,因此它应该已经可以在Samples项目中使用。这个示例比日历小部件更复杂,因为它除了用户界面外还包括了一个 RPC 服务。因此,当我们使用它时,我们需要在项目的模块 XML 文件中为来自该小部件的服务添加一个条目,该小部件将被使用:
<servlet path="/Samples/weather" class=
weather widgetworking"com.packtpub.gwtbook.widgets.server.WeatherServiceImpl"/>
摘要
在本章中,我们学习了如何创建和重用自定义小部件。我们创建了一个日历小部件,可以在其中向前和向后导航,并返回到当前日期。
然后,我们创建了一个天气小部件,为特定地点提供了天气信息服务。
在下一章中,我们将学习如何为测试 GWT 应用程序和 RPC 服务创建和运行单元测试。
第八章:单元测试
JUnit 是一个广泛使用的开源 Java 单元测试框架,由 Erich Gamma 和 Kent Beck 创建(junit.org)。它允许您逐步构建一套测试,作为开发工作的一个组成部分,并在很大程度上增加了您对代码稳定性的信心。JUnit 最初设计和用于测试 Java 类,但后来被模拟并用于其他几种语言,如 Ruby、Python 和 C#。GWT 利用并扩展了 JUnit 框架,以提供一种测试 AJAX 代码的方式,就像测试任何其他 Java 代码一样简单。在本章中,我们将学习如何创建和运行用于测试 GWT 应用程序和 RPC 服务的单元测试。
我们将要处理的任务是:
-
测试 GWT 页面
-
测试异步服务
-
测试具有异步服务的 GWT 页面
-
创建并运行测试套件
测试 GWT 页面
GWT 页面基本上由小部件组成,我们可以通过检查小部件的存在以及检查我们想要的小部件值或参数来测试页面。在本节中,我们将学习如何为 GWT 页面创建单元测试。
操作时间-创建单元测试
我们将使用内置在 GWT 框架中的测试支持来编写我们的单元测试,测试我们在第四章中创建的AutoFormFillPanel页面。
步骤如下:
- 通过提供这些参数运行
GWT_HOME\junitCreator命令脚本:
junitCreator -junit junit.jar -module com.packtpub.gwtbook.samples. Samples -eclipse Samples -out ~pchaganti/dev/GWTBook/Samples com. packtpub.gwtbook.samples.client.panels.AutoFormFillPanelTest

- 在自动生成的 Java 文件
com.packtpub.gwtbook.samples.client.panels.AutoFormFillPanelTest.java中打开测试目录中自动创建的测试目录中的文件:
public void testPanel()
{
}
- 创建表单并添加断言以检查“客户 ID”标签的名称和与之关联的样式:
final AutoFormFillPanel autoFormFillPanel = new
AutoFormFillPanel();
assertEquals("Customer ID : ",
autoFormFillPanel.getCustIDLbl().getText());
assertEquals("autoFormItem-Label",
autoFormFillPanel.getCustIDLbl().getStyleName());
- 添加类似的断言以测试页面上的所有其他元素:
assertEquals("Address : ",
autoFormFillPanel.getAddressLbl().getText());
assertEquals("autoFormItem-Label",
autoFormFillPanel.getAddressLbl().getStyleName());
assertEquals("City : ",
autoFormFillPanel.getCityLbl().getText());
assertEquals("autoFormItem-Label",
autoFormFillPanel.getCityLbl().getStyleName());
assertEquals("First Name : ",
autoFormFillPanel.getFirstNameLbl().getText());
assertEquals("autoFormItem-Label",
autoFormFillPanel.getFirstNameLbl().getStyleName());
assertEquals("Last Name : ",
autoFormFillPanel.getLastNameLbl().getText());
assertEquals("autoFormItem-Label",
autoFormFillPanel.getLastNameLbl().getStyleName());
assertEquals("Phone Number : ",
autoFormFillPanel.getPhoneLbl().getText());
assertEquals("autoFormItem-Label",
autoFormFillPanel.getPhoneLbl().getStyleName());
assertEquals("State : ",
autoFormFillPanel.getStateLbl().getText());
assertEquals("autoFormItem-Label",
autoFormFillPanel.getStateLbl().getStyleName());
assertEquals("Zip Code : ",
autoFormFillPanel.getZipLbl().getText());
assertEquals("autoFormItem-Label",
autoFormFillPanel.getZipLbl()
- 在
Samples.gwt.xml文件中添加一个条目,以继承 JUnit 测试模块:
<inherits name='com.google.gwt.junit.JUnit' />
- 通过从“运行”菜单启动
AutoFormFillPanelTest-hosted启动配置在 Eclipse 中运行测试,并获得类似于这样的屏幕:![操作时间-创建单元测试]()
刚刚发生了什么?
GWT 框架支持单元测试,提供了从 JUnit 测试库中扩展的GWTTestCase基类。我们通过编译和运行从GWTTestCase扩展的类来执行单元测试。当我们运行这个子类时,GWT 框架会启动一个不可见的 Web 浏览器,并在浏览器实例内运行测试。
我们使用 GWT 提供的junitCreator命令脚本生成必要的脚手架,用于创建和运行单元测试。我们将测试类的名称作为此命令的参数之一。生成一个扩展自GWTTestCase类的示例测试用例,以及两个启动脚本——一个用于在主机模式下运行,另一个用于在 Web 模式下运行。这些启动配置以 Eclipse 格式生成,并可以直接从 Eclipse 环境内运行。
扩展GWTTestCase的类必须实现getModuleMethod()并从该方法返回包含测试类的 GWT 模块的完全限定名称。因此,在我们的情况下,我们从这个方法返回com.packtpub.gwtbook.samples.Samples。这使得 GWT 能够解析依赖项并正确加载运行测试所需的类。如果我们在一个完全独立的模块中创建测试,这个方法将需要返回包含模块的名称。我们还需要在项目的模块文件中继承 GWT JUnit 模块。这就是为什么我们需要将这一行添加到Samples.gwt.xml文件中的原因:
<inherits name='com.google.gwt.junit.JUnit' />
使用junitCreator是开始使用 GWT 中单元测试功能的最简单方法。但是,如果您决定自己创建此命令生成的各种工件,以下是创建和运行 GWT 项目中单元测试所涉及的步骤:
-
创建一个扩展
GWTTestCase的类。在这个类中实现getModuleName()方法,以返回包含此类的模块的完全限定名称。 -
编译测试用例。为了运行您的测试,必须首先编译它。
-
为了运行测试,您的
classpath必须包括junit-dev-linux.jar或gwt-dev-windows.jar文件,以及junit.jar文件,除了正常的要求。
由于GWTTestCase只是TestCase的子类,因此您可以访问来自 JUnit 库的所有正常断言方法。您可以使用这些方法来断言和测试关于页面的各种事物,例如文档的结构,包括表格和其他 HTML 元素及其布局。
测试异步服务
在前一节中,我们学习了如何为单元测试 GWT 页面创建简单的测试。但是,大多数非平凡的 GWT 应用程序将访问和使用 AJAX 服务以异步方式检索数据。在本节中,我们将介绍测试异步服务的步骤,例如我们在本书前面创建的AutoFormFillPanel服务。
进行操作的时间-测试异步服务
我们将测试我们在第四章中创建的AutoFormFillPanelService:
- 通过提供这些参数运行
GWT_HOME\junitCreator命令脚本:
junitCreator -junit junit.jar -module com.packtpub.gwtbook.samples. Samples -eclipse Samples -out ~pchaganti/dev/GWTBook/Samples com. packtpub.gwtbook.samples.client.panels.AutoFormFillServiceTest
- 在运行
junitCreator命令时自动生成的测试目录中打开生成的 Java 文件com.packtpub.gwtbook.samples.client.panels.AutoFormFillServiceTest.java。在文件中添加一个名为testService()的新方法:
public void testService()
{
}
- 在
testService()方法中,实例化AutoFormFillService并设置入口点信息:
final AutoFormFillServiceAsync autoFormFillService =
(AutoFormFillServiceAsync) GWT.create
(AutoFormFillService.class);
ServiceDefTarget endpoint = (ServiceDefTarget)
autoFormFillService;
endpoint.setServiceEntryPoint("/Samples/autoformfill");
- 创建一个新的异步回调,在
onSuccess()方法中添加断言来测试调用服务返回的数据:
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
HashMap formValues = (HashMap) result;
assertEquals("Joe", formValues.get("first name"));
assertEquals("Customer", formValues.get("last name"));
assertEquals("123 peachtree street",
formValues.get("address"));
assertEquals("Atlanta", formValues.get("city"));
assertEquals("GA", formValues.get("state"));
assertEquals("30339", formValues.get("zip"));
assertEquals("770-123-4567", formValues.get("phone"));
finishTest();
}
};
- 调用
delayTestFinish()方法并调用异步服务:
delayTestFinish(2000);
autoFormFillService.getFormInfo("1111", callback);
- 通过在 Eclipse 中启动Run菜单中的
AutoFormFillPanelService-hosted启动配置来运行测试。这是结果:![Time for Action—Testing the Asynchronous Service]()
刚刚发生了什么?
JUnit 支持测试普通的 Java 类,但缺乏对具有任何异步行为的模块进行测试的支持。单元测试将开始执行并按顺序运行模块中的所有测试。这种方法对于测试异步事物不起作用,其中您发出请求并且响应分别返回。GWT 具有这种独特的功能,并支持对异步服务进行测试;因此,您可以调用 RPC 服务并验证来自服务的响应。
您还可以测试其他长时间运行的服务,例如计时器。为了提供此支持,GWTTestCase扩展了TestCase类并提供了两个方法-delayTestFinish()和finishTest()-它们使我们能够延迟完成单元测试,并控制测试实际完成的时间。这本质上让我们将我们的单元测试置于异步模式中,因此我们可以等待来自对远程服务器的调用的响应,并在收到响应时通过验证响应来完成测试。
在这个示例中,我们使用了 GWT 中测试长时间事件的标准模式。步骤如下:
-
我们创建了一个异步服务的实例并设置了它的入口点。
-
我们设置了一个异步事件处理程序,即我们的回调。在此回调中,我们通过断言返回的值与我们期望的值匹配来验证接收到的响应。然后,我们通过调用
finishTest()完成测试,以指示 GWT 我们要离开测试中的异步模式:
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
HashMap formValues = (HashMap) result;
assertEquals("Joe", formValues.get("first name"));
assertEquals("Customer", formValues.get("last name"));
assertEquals("123 peachtree street",formValues.get
("address"));
assertEquals("Atlanta", formValues.get("city"));
assertEquals("GA", formValues.get("state"));
assertEquals("30339", formValues.get("zip"));
assertEquals("770-123-4567", formValues.get("phone"));
finishTest();
}
};
- 我们为测试设置了一个延迟时间。这使得 GWT 测试框架等待所需的时间。在这里,我们设置了 2000 毫秒的延迟:
delayTestFinish(2000);
这必须设置为一个比服务预计返回响应所需时间略长的时间段。
- 最后,我们调用异步事件,将
callback对象作为参数提供给它。在这种情况下,我们只调用AutoFormFillService上的必需方法:
autoFormFillService.getFormInfo("1111", callback);
您可以使用此模式测试所有使用定时器的异步 GWT 服务和类。
使用异步服务测试 GWT 页面
在本节中,我们将测试调用异步服务的页面。这将使我们创建一个结合了前两个示例的测试。
行动时间-合并两者
我们将在最后两个部分中编写的两个测试合并为一个,并为AutoFormFillPanel页面创建一个全面的测试,测试页面元素和页面使用的异步服务。步骤如下:
- 在
com.packtpub.gwtbook.samples.client.panels包中的现有AutoFormFillPanel类中添加一个名为simulateCustomerIDChanged()的新方法:
public void simulateCustIDChanged(String custIDValue)
{
if (custIDValue.length() > 0)
{
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess(Object result)
{
setValues((HashMap) result);
}
};
custID.setText(custIDValue);
autoFormFillService.getFormInfo(custIDValue, callback);
}
else
{
clearValues();
}
}
- 将
testPanel()方法名称修改为testEverything()。在方法底部,调用simulateCustIDChanged()方法,并提供一个 ID 参数为 1111:
autoFormFillPanel.simulateCustIDChanged("1111");
- 创建一个新的
Timer对象,并将以下内容添加到其run()方法中:
Timer timer = new Timer()
{
public void run()
GWT pagewith asynchronous service, testing{
assertEquals("Joe",
autoFormFillPanel.getFirstName().getText());
assertEquals("Customer",
autoFormFillPanel.getLastName().getText());
assertEquals("123 peachtree street",
autoFormFillPanel.getAddress().getText());
assertEquals("Atlanta",
autoFormFillPanel.getCity().getText());
assertEquals("GA", autoFormFillPanel.getState().getText());
assertEquals("30339",
autoFormFillPanel.getZip().getText());
assertEquals("770-123-4567",
autoFormFillPanel.getPhone().getText());
finishTest();
}
};
- 延迟测试完成并运行计时器:
delayTestFinish(2000);
timer.schedule(100);
- 通过启动
AutoFormFillPanelTest-hosted启动配置来运行测试,并获得类似于此的结果:![行动时间-合并两者]()
刚刚发生了什么?
到目前为止,我们已经编写了两个单独的测试-一个用于测试AutoFormFillPanel页面上的各种 HTML 元素,另一个用于测试AutoFormFillPanelService。我们可以将这两个测试合并为一个,并创建一个用于测试面板的单个测试。AutoFormFillPanel在更改CustomerID文本框中的文本时调用异步服务。为了在测试中模拟键盘监听器,我们在AutoFormFillPanel类中创建了一个名为simulateCustIDChanged()的新公共方法,它本质上与该类中的键盘监听器事件处理程序执行相同的操作。我们将调用此方法来模拟用户在键盘上输入以更改CustomerID文本。
一旦我们测试了页面上的各种 HTML 元素,我们调用simulateCustIDChanged()方法。然后,我们使用Timer对象设置一个异步事件处理程序。当计时器运行时,我们验证面板中是否有正确的值,如步骤 3 中所述。
我们为测试设置延迟以完成:
delayTestFinish(2000);
最后,我们安排计时器运行,因此当计时器在给定延迟后触发时,它将验证预期结果,然后完成测试:
timer.schedule(100);
创建并运行测试套件
到目前为止,我们已经学会了如何创建和运行单独的单元测试。随着代码库的增长,逐一运行所有测试非常繁琐。JUnit 提供了测试套件的概念,它允许您将一组测试组合成一个套件并运行它们。在本节中,我们将学习如何创建和运行多个单元测试作为套件的一部分。
行动时间-部署测试套件
到目前为止,我们为创建的每个测试生成了一个测试启动脚本,并分别运行了创建的每个测试。在本节中,我们将把我们的测试组合成一个测试套件,并在单个启动配置中运行所有测试。步骤如下:
- 运行
GWT_HOME\junitCreator命令脚本,并提供以下参数:
junitCreator -junit junit.jar -module com.packtpub.gwtbook.samples. Samples -eclipse Samples -out ~pchaganti/dev/GWTBook/Samplescom. packtpub.gwtbook.samples.client.SamplesTestSuite
- 修改
SamplesTestSuite类并添加一个suite()方法:
public static Test suite()
{
TestSuite samplesTestSuite = new TestSuite();
samplesTestSuite.addTestSuite(AutoFormFillServiceTest.class);
samplesTestSuite.addTestSuite(AutoFormFillPanelTest.class);
return samplesTestSuite;
}
- 通过启动
SamplesTestSuite-hosted启动配置来运行测试,并获得类似于此的结果:![行动时间-部署测试套件]()
刚刚发生了什么?
生成每个测试的单独启动脚本并分别运行每个测试可能会变得乏味。使用测试套件让我们可以有一个地方来收集所有的测试。然后我们可以使用套件的启动脚本来运行所有的测试。测试套件本质上是项目中所有测试的收集器。我们在项目中定义了一个名为suite()的静态工厂方法。在这个方法中,我们将所有的测试添加到suite对象中,并将suite对象作为返回值返回。
public static Test suite()
{
TestSuite samplesTestSuite = new TestSuite();
samplesTestSuite.addTestSuite(AutoFormFillServiceTest.class);
samplesTestSuite.addTestSuite(AutoFormFillPanelTest.class);
return samplesTestSuite;
}
当我们通过启动脚本运行这个测试时,JUnit 框架会识别出我们正在运行一组测试,并运行套件中定义的每个测试。目前还没有支持推断出 GWT 项目中所有测试并自动生成测试套件来包含这些测试的功能。因此,您必须手动将希望成为套件一部分的每个测试添加到这个方法中。现在我们已经让测试套件工作了,我们可以从Samples项目中删除所有其他测试启动配置,只使用这个配置来运行所有的测试。
总结
在本章中,我们学习了为 GWT 页面(AutoFormFillPanel)和异步服务(AutoFormFillPanelService)创建单元测试。然后我们将这两者结合起来,为使用异步服务的 GWT 页面创建了一个单元测试。
最后,我们将所有的测试组合成一个测试套件,并在单个启动配置中运行了所有的测试。
在下一章中,我们将学习 GWT 中的国际化(I18N)和 XML 支持。
第九章:I18N 和 XML
在本章中,我们将学习如何在 GWT 应用程序中使用国际化。我们还将创建展示 GWT 支持客户端创建和解析 XML 文档的示例。
我们将要处理的任务是:
-
国际化
-
创建 XML 文档
-
解析 XML 文档
国际化(I18N)
GWT 提供了广泛的支持,可以创建能够以多种语言显示文本的应用程序。在本节中,我们将利用 GWT 创建一个页面,可以根据给定的区域设置显示适当语言的文本。
行动时间-使用 I18N 支持
我们将创建一个简单的 GWT 用户界面,显示指定区域设置的适当图像和文本“欢迎”。显示的图像将是对应于所选区域设置的国旗。步骤如下:
- 在
com.packtpub.gwtbook.samples.client.util包中创建一个名为I18NSamplesConstants.java的新的 Java 文件,定义一个名为I18NSamplesConstants的接口。向接口添加以下两个方法-一个用于检索欢迎文本,一个用于检索图像:
public interface I18NSamplesConstants extends Constants
{
String welcome();
String flag_image();
}
- 在
com.packtpub.gwtbook.samples.client.util包中创建一个名为I18NSamplesConstants.properties的新文件。向其中添加欢迎文本和图像的属性:
welcome = Welcome
flag_image = flag_en.gif
这个属性文件代表了默认的区域设置,即美国英语。
- 在
com.packtpub.gwtbook.samples.client.util包中创建一个名为I18NSamplesConstants_el_GR.properties的新文件。向其中添加欢迎文本和图像的属性:
welcome = υποδοχή
flag_image = flag_el_GR.gif
这个属性文件代表了希腊的区域设置。
- 在
com.packtpub.gwtbook.samples.client.util包中创建一个名为I18NSamplesConstants_es_ES.properties的新文件。向其中添加欢迎文本和图像的属性:
welcome = recepción
flag_image = flag_es_ES.gif
这个属性文件代表了西班牙的区域设置。
- 在
com.packtpub.gwtbook.samples.client.util包中创建一个名为I18NSamplesConstants_zh_CN.properties的新文件。向其中添加欢迎文本和图像的属性:
welcome =
flag_image = flag_zh_CN.gif
这个属性文件代表了中文的区域设置。
- 在
com.packtpub.gwtbook.samples.client.panels包中创建一个名为I18NPanel.java的新的 Java 文件。创建一个将包含用户界面的VerticalPanel。我们将把这个面板添加到DockPanel中,并将其添加到我们的Samples应用程序中,就像我们在本书中一直在做的其他应用程序一样。添加一个标签,用于以提供的区域设置的适当语言显示欢迎文本消息:
private VerticalPanel workPanel = new VerticalPanel();
private Label welcome = new Label();
- 在构造函数中创建
I18NSamplesConstants的实例。添加一个图像小部件来显示国旗图像,以及一个标签来显示欢迎文本到面板上。通过使用I18NSamplesConstants来设置标签和图像文件的文本。最后,创建一个小的信息面板,显示关于这个应用程序的描述性文本,这样当我们在Samples应用程序的可用示例列表中选择此示例时,我们可以显示文本。将信息面板和工作面板添加到一个停靠面板中,并初始化小部件:
public I18nPanel()
{
I18NSamplesConstants myConstants = (I18NSamplesConstants)
GWT.create(I18NSamplesConstants.class);
// Always the same problem, samples are not "sound
and complete"
welcome.setText(myConstants.welcome());
welcome.setStyleName("flagLabel");
Image flag = new Image("images/" + myConstants.flag_image());
flag.setStyleName("flag");
workPanel.add(flag);
workPanel.add(welcome);
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
internationalization, GWTI18N support, using}
- 添加一个条目来导入 I18N 模块到
Samples.gwt.xml文件中:
<inherits name ="com.google.gwt.i18n.I18N"/>
- 为我们支持的每个区域设置添加一个条目到
Samples.gwt.xml文件中:
<extend-property name="locale" values="el_GR"/>
<extend-property name="locale" values="es_ES"/>
<extend-property name="locale" values="zh_CN"/>
运行应用程序。这是以默认区域设置显示的默认界面-en_US:

修改 URL,为我们支持的每个区域设置添加一个区域查询参数,以便以适当的语言显示用户界面。这是以希腊语显示的用户界面-el_GR:
http://localhost:8888/com.packtpub.gwtbook.samples.Samples/Samples.html?locale=el_GR#i18n

这是以西班牙语显示的用户界面-es_ES:
http://localhost:8888/com.packtpub.gwtbook.samples.Samples/Samples.html?locale=es_ES#i18n

这是以中文显示的用户界面-zh_CN:
http://localhost:8888/com.packtpub.gwtbook.samples.Samples/Samples.html?locale=zh_CN#i18n

刚刚发生了什么?
GWT 提供了各种工具和技术,帮助开发可以显示各种语言文本的国际化应用程序。使用 GWT 开发国际化应用程序有两种主要技术:
-
静态字符串国际化:这是一种依赖于 Java 接口和常规属性文件的类型安全技术。它从前两个组件生成代码,为应用程序提供了意识到其操作环境的区域设置的消息。这种技术推荐用于没有现有本地化属性文件的新应用程序。
-
动态字符串国际化:当您已经有现有的本地化系统时,例如您的 Web 服务器可以生成本地化字符串时,可以使用此技术。然后在 HTML 页面中打印这些翻译后的字符串。这种方法通常比静态方法慢,但由于它没有代码生成阶段,因此每次修改消息字符串或更改支持的区域设置列表时,您不需要重新编译应用程序。
在此示例中,我们使用静态国际化技术。我们创建一个接口I18NSamplesConstants,定义两个方法——一个方法返回欢迎消息,另一个方法返回标志图像文件名。然后为应用程序支持的每个区域设置创建一个属性文件,并将消息添加到适当语言的文件中。
locale是一个唯一标识特定语言和地区组合的对象。例如,en_US的区域设置指的是英语和美国。同样,fr_FR指的是法语和法国。属性文件名必须以区域标识符结尾,然后是properties扩展名。这是我们西班牙语区域西班牙属性文件的内容:
welcome = recepción
flag_image = flag_es_ES.gif
我们的用户界面非常简单,由一个图像和其下的标签组成。图像将显示使用的区域设置的国旗,标签将显示欢迎文本的语言。应用程序在启动时将以您的环境的默认区域设置显示页面。您可以通过附加一个查询参数,键为locale,值等于任何支持的区域设置,来更改这一点。因此,为了以希腊语查看页面,您将在相应的 URL 后附加locale=el_GR。
如果提供的区域设置不受支持,网页将以默认区域设置显示。我们通过创建I18NSamplesConstants类来访问适当的文本,使用访问器获取本地化消息,并为两个小部件设置值:
I18NSamplesConstants myConstants = (I18NSamplesConstants)
GWT.create(I18NSamplesConstants.class);
welcome.setText(myConstants.welcome());
Image flag = new Image("images/" + myConstants.flag_image());
I18NSamplesConstants类扩展自Constants类,它允许在编译时绑定到从简单属性文件获取的常量值。当我们使用GWT.create()方法实例化I18NSamplesConstants时,GWT 会自动生成使用适当区域设置的属性文件值的正确子类,并返回它。支持的区域设置本身由模块文件定义,使用 extend-property 标签。这通知 GWT 框架,我们要扩展默认属性"locale",提供其替代方案:
<extend-property name="locale" values="el_GR"/>
我们还在Samples.gwt.xml文件中继承自com.google.gwt.i18n.I18N,以便我们的模块可以访问 GWT 提供的 I18N 功能。
GWT 还提供了其他几种工具来增强 I18N 支持。有一个Messages类,当我们想要提供带有参数的本地化消息时可以使用它。我们也可以忽略本地化,使用常规的属性文件来存储配置信息。我们还有一个i18nCreator命令脚本,可以生成Constants或Messages接口和示例属性文件。最后,还有一个Dictionary类可用于动态国际化,因为它提供了一种动态查找在模块的 HTML 页面中定义的键值对字符串的方式。
GWT 中的 I18N 支持非常广泛,可以用于支持简单或复杂的国际化场景。
创建 XML 文档
XML 在企业中被广泛应用于各种应用程序,并且在集成不同系统时也非常常见。在本节中,我们将学习 GWT 的 XML 支持以及如何在客户端使用它来创建 XML 文档。
行动时间-创建 XML 文档
我们将获取存储在 CSV 文件中的客户数据,并创建一个包含客户数据的 XML 文档。步骤如下:
- 在
com.packtpub.gwtbook.samples.public包中创建一个简单的 CSV 文件,其中包含客户数据,文件名为customers.csv。向此文件添加两个客户的信息:
John Doe,222 Peachtree St,Atlanta
Jane Doe,111 10th St,New York
- 在
com.packtpub.gwtbook.samples.client.panels包中的新 Java 文件CreateXMLPanel.java中创建用户界面。创建一个私有的HTMLPanel变量,用于显示我们将要创建的 XML 文档。还创建一个VerticalPanel类,它将是用户界面的容器:
private HTMLPanel htmlPanel = new HTMLPanel("<pre></pre>");
private VerticalPanel workPanel = new VerticalPanel();
- 创建一个名为
createXMLDocument()的私有方法,它可以接受一个字符串并从中创建客户的 XML 文档。创建一个 XML 文档对象,添加 XML 版本的处理指令,并创建一个名为customers的根节点。循环遍历 CSV 文件中每一行的客户信息。创建适当的 XML 节点,设置它们的值,并将它们添加到根节点。最后返回创建的 XML 文档:
private Document createXMLDocument(String data)
{
String[] tokens = data.split("\n");
Document customersDoc = XMLParser.createDocument();
ProcessingInstruction procInstruction = customersDoc. createProcessingInstruction("xml", "version=\"1.0\"");
customersDoc.appendChild(procInstruction);
Element rootElement =
customersDoc.createElement("customers");
customersDoc.appendChild(rootElement);
for (int i = 0; i < tokens.length; i++)
{
String[] customerInfo = tokens[i].split(",");
Element customerElement =
customersDoc.createElement("customer");
Element customerNameElement =
customersDoc.createElement("name");
customerNameElement.appendChild
(customersDoc.createTextNode(customerInfo[0]));
XML support, Element customerAddressElement =
customersDoc.createElement("address");
customerAddressElement.appendChild
(customersDoc.createTextNode(customerInfo[1]));
Element customerCityElement =
customersDoc.createElement("city");
customerCityElement.appendChild
(customersDoc.createTextNode(customerInfo[2]));
customerElement.appendChild(customerNameElement);
customerElement.appendChild(customerAddressElement);
customerElement.appendChild(customerCityElement);
rootElement.appendChild(customerElement);
}
return customersDoc;
}
- 创建一个名为
createPrettyXML()的新方法,它将通过缩进节点来格式化我们的 XML 文档,然后在HTMLPanel中显示:
private String createPrettyXML(Document xmlDoc)
{
String xmlString = xmlDoc.toString();
xmlString = xmlString.replaceAll
("<customers", " <customers");
xmlString = xmlString.replaceAll
("</customers"," </customers");
xmlString = xmlString.replaceAll
("<customer>"," <customer>");
xmlString = xmlString.replaceAll
("</customer>"," </customer>");
xmlString = xmlString.replaceAll("<name>",
" <name>
");
xmlString = xmlString.replaceAll("</name>",
"\n </name>");
xmlString = xmlString.replaceAll("<address>",
" <address>
");
xmlString = xmlString.replaceAll("</address>",
"\n </address>");
xmlString = xmlString.replaceAll("<city>",
" <city>
");
xmlString = xmlString.replaceAll("</city>",
"\n </city>");
xmlString = xmlString.replaceAll(">", ">\n");
xmlString = xmlString.replaceAll("<", "");
xmlString = xmlString.replaceAll(">", "");
return xmlString;
}
这只是一种快速而粗糙的格式化 XML 文档的方式,因为 GWT 目前没有提供一个很好的方法来做到这一点。
- 在
com.packtpub.gwtbook.samples.client.panels包中的新 Java 文件CreateXMLPanel.java中为此应用程序创建用户界面。在构造函数CreateXMLPanel()中,进行异步 HTTP 请求以获取customers.csv文件。成功后,从 CSV 文件中的数据创建 XML 文档,并在HTMLPanel中显示它。最后,创建一个小的信息面板,显示关于此应用程序的描述性文本,以便在Samples应用程序的可用样本列表中选择此样本时显示文本。将信息面板和工作面板添加到一个停靠面板中,并初始化小部件:
public CreateXMLPanel()
{
HorizontalPanel infoPanel = new HorizontalPanel();
infoPanel.add(new HTML(
"<div class='infoProse'>Read a comma separated text file
and create an XML document from it.</div>"));
HTTPRequest.asyncGet("customers.csv",
new ResponseTextHandler()
{
public void onCompletion(String responseText)
{
Document customersDoc = createXMLDocument(responseText);
if (htmlPanel.isAttached())
{
workPanel.remove(htmlPanel);
}
htmlPanel = new HTMLPanel("<pre>" +
createPrettyXML(customersDoc) + "</pre>");
htmlPanel.setStyleName("xmlLabel");
workPanel.add(htmlPanel);
}
});
DockPanel workPane = new DockPanel();
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
}
- 在
Samples.gwt.xml文件中添加一个条目来导入 XML 模块:
<inherits name ="com.google.gwt.xml.XML"/>
这是显示从客户的 CSV 文件创建的 XML 文档的页面:

刚刚发生了什么?
GWT 在客户端提供了良好的支持,用于生成 XML 文档,并且与框架中的其他所有内容一样,它是与浏览器无关的。您可以利用XMLParser类来生成文档,并且可以确保在所有支持的浏览器中正确生成 XML 文档。在这个例子中,我们创建了一个包含客户数据的简单 CSV 文件。通过在HTTPRequest对象上使用asyncGet()方法检索此客户数据。由于 GWT 没有提供从文件系统中读取文件的支持,这是一种加载外部文件的解决方法,而不是使用 RPC 服务。我们将文件名和ResponseTextHandler作为此方法的参数。ResponseTextHandler提供了在同步调用完成时执行的回调。在回调中,我们读取响应的内容并使用这些值创建一个 XML 文档。通过使用XMLParser对象创建一个新文档:
Document customersDoc = XMLParser.createDocument();
首先向此文档添加了一个处理指令,以便 XML 格式良好:
ProcessingInstruction procInstruction =
customersDoc.createProcessingInstruction("XML", "version=\"1.0\"");
customersDoc.appendChild(procInstruction);
然后我们创建根节点和子节点。我们向新节点添加一个文本节点,该节点的值是我们从 CSV 文件中解析出的值:
customersDoc.createElement("name");
customerNameElement.appendChild
(customersDoc.createTextNode(customerInfo[0]));
这个新文档是通过在HTMLPanel中使用预格式化块来显示的。然而,在将其显示在面板中之前,我们需要对文本进行格式化和缩进,否则整个文档将显示为一行字符串。我们有一个私有方法,通过使用正则表达式来缩进和格式化文档。这有点繁琐。希望将来 GWT 将支持在框架本身创建漂亮的 XML 文档。在这个例子中,我们通过 HTTP 请求检索 CSV 文件的内容;我们可以使用 RPC 服务以任何我们喜欢的格式提供生成 XML 的数据。
解析 XML 文档
在上一节中,我们使用了 GWT 支持创建 XML 文档。在本节中,我们将学习如何读取 XML 文档。我们将创建一个可以解析 XML 文件并使用文件中的数据填充表格的应用程序。
Time for Action—Parsing XML on the Client
我们将创建一个 GWT 应用程序,该应用程序可以读取包含有关一些书籍信息的 XML 文件,并用该数据填充表格。步骤如下:
- 在
com.packtpub.gwtbook.samples.client.public包中创建一个名为books.xml的文件,其中包含书籍数据的简单 XML 文件:
<?xml version="1.0" encoding="US-ASCII"?>
<books>
<book id="1">
<title>I Claudius</title>
<author>Robert Graves</author>
<year>1952</year>
</book>
<book id="2">
<title>The Woman in white</title>
<author>Wilkie Collins</author>
<year>1952</year>
</book>
<book id="3">
<title>Shogun</title>
<author>James Clavell</author>
<year>1952</year>
</book>
<book id="4">
<title>City of Djinns</title>
<author>William Dalrymple</author>
<year>2003</year>
</book>
<book id="5">
<title>Train to pakistan</title>
<author>Kushwant Singh</author>
<year>1952</year>
</book>
</books>
- 在
com.packtpub.gwtbook.samples.client.panels包中的新 Java 文件ParseXMLPanel.java中为此应用程序创建用户界面。创建一个包含我们用户界面的VerticalPanel类,以及我们将用于显示来自 XML 文件的数据的FlexTable类:
private VerticalPanel workPanel = new VerticalPanel();
private FlexTable booksTable = new FlexTable();
- 创建一个名为
getElementTextValue()的私有方法,该方法可以接受一个父 XML 元素和一个标签名称,并返回该节点的文本值:
private String getElementTextValue
(Element parent, String elementTag)
{
return parent.getElementsByTagName
(elementTag).item(0).getFirstChild().getNodeValue();
}
- 在构造函数
ParseXMLPanel()中,为 flex 表添加表头和样式:
booksTable.setWidth(500 + "px");
booksTable.setStyleName("xmlParse-Table");
booksTable.setBorderWidth(1);
booksTable.setCellPadding(4);
booksTable.setCellSpacing(1);
booksTable.setText(0, 0, "Title");
booksTable.setText(0, 1, "Author");
booksTable.setText(0, 2, "Publication Year");
RowFormatter rowFormatter = booksTable.getRowFormatter();
rowFormatter.setStyleName(0, "xmlParse-TableHeader");
- 在同一个构造函数中,发出异步 HTTP 请求以获取
books.xml文件,并在完成后解析 XML 文档并用数据填充一个 flex 表。最后,创建一个小的信息面板,显示有关此应用程序的描述性文本,以便在我们的Samples应用程序的可用样本列表中选择此样本时显示文本。将信息面板和工作面板添加到一个停靠面板中,并初始化小部件:
HTTPRequest.asyncGet("books.xml", new ResponseTextHandler()
{
public void onCompletion(String responseText)
{
Document bookDom = XMLParser.parse(responseText);
Element booksElement = bookDom.getDocumentElement();
XMLParser.removeWhitespace(booksElement);
NodeList bookElements =
booksElement.getElementsByTagName("book");
for (int i = 0; i < bookElements.getLength(); i++)
{
Element bookElement = (Element) bookElements.item(i);
booksTable.setText(i + 1, 0, getElementTextValue(
bookElement, "title"));
booksTable.setText(i + 1, 1, getElementTextValue(
bookElement, "author"));
booksTable.setText(i + 1, 2, getElementTextValue(
bookElement, "year"));
}
}
});
DockPanel workPane = new DockPanel();
workPanel.add(booksTable);
workPane.add(infoPanel, DockPanel.NORTH);
workPane.add(workPanel, DockPanel.CENTER);
workPane.setCellHeight(workPanel, "100%");
workPane.setCellWidth(workPanel, "100%");
initWidget(workPane);
这是包含来自books.xml文件的数据的表格的页面:

刚刚发生了什么?
我们再次使用HTTPRequest对象从服务器检索文件的内容,在这种情况下是books.xml文件,其中包含一些关于已出版图书的数据,我们希望在页面上以表格的形式显示出来。XMLParser对象被用来将异步响应的内容读入文档中。然后使用熟悉的 DOM API 遍历这个 XML 文档,并检索和使用适当节点的文本值来填充 flex 表中的相应列单元格。我们使用getElementsByTagName()方法获取包含所有图书元素的NodeList:
NodeList bookElements = booksElement.getElementsByTagName("book");
一旦我们有了这个列表,我们只需遍历它的子节点,并访问我们感兴趣的值:
for (int i = 0; i < bookElements.getLength(); i++)
{
Element bookElement = (Element) bookElements.item(i);
booksTable.setText(i + 1, 0, getElementTextValue(
bookElement, "title"));
booksTable.setText(i + 1, 1, getElementTextValue(
bookElement, "author"));
booksTable.setText(i + 1, 2, getElementTextValue(
bookElement, "year"));
}
我们在Samples.gwt.xml文件中继承自com.google.gwt.xml.xml文件,以便我们的模块可以访问 GWT 提供的 XML 功能。
总结
在本章中,我们学习了如何创建支持国际化(I18N)的应用程序。我们创建了一个可以根据给定区域设置显示适当语言文本的页面。然后,我们使用 GWT 的 XML 支持在客户端创建了一个 XML 文档。
最后,我们创建了一个可以解析 XML 文件并使用文件中的数据填充表格的应用程序。
在下一章中,我们将学习如何在 Tomcat 中部署我们的 GWT 应用程序。
第十章:部署
在本章中,我们将首先学习如何手动部署 GWT 应用程序,以便熟悉部署的所有组件。然后,我们将使用 Apache Ant 自动化这个过程。
我们将要处理的任务是:
-
在 Tomcat 中手动部署
-
使用 Ant 进行自动部署
-
从 Eclipse 部署
在 Tomcat 中手动部署
我们将采取在本书中一直在进行的Samples应用程序,并逐步进行手动部署并在 Tomcat 中运行所需的各种步骤。
行动时间-部署 GWT 应用程序
以下是手动部署 GWT 应用程序到 Tomcat 所需的步骤:
-
下载并安装适用于您平台的 Apache Tomcat(
tomcat.apache.org)。从 5.x 系列中选择最新的稳定版本。我将把 Tomcat 安装的目录称为$TOMCAT_DIR,包含Samples项目的目录称为$SAMPLES_DIR。 -
运行
$SAMPLES_DIR/Samples-compile来编译整个应用程序。这将在$SAMPLES_DIR下创建一个名为www的新目录。 -
在
$SAMPLES_DIR目录中创建一个名为web.xml的新文件。为我们的应用程序添加一个显示名称和描述:
<display-name>
GWT Book Samples
</display-name>
<description>
GWT Book Samples
</description>
显示名称在使用 Tomcat 管理器浏览部署应用程序列表时显示。
- 在上一步创建的
web.xml文件中,为我们应用程序中使用的每个 RPC 服务添加条目,并为每个条目添加相应的 servlet 映射。为实时搜索服务添加一个条目:
<servlet>
<servlet-name>livesearch</servlet-name>
<servlet-class>
com.packtpub.gwtbook.samples.server.
LiveSearchServiceImpl
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>livesearch</servlet-name>
<url-pattern>/livesearch</url-pattern>
</servlet-mapping>
- 为密码强度服务添加一个条目:
<servlet>
<servlet-name>pwstrength</servlet-name>
<servlet-class>
com.packtpub.gwtbook.samples.server.
PasswordStrengthServiceImpl
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>pwstrength</servlet-name>
<url-pattern>/pwstrength</url-pattern>
</servlet-mapping>
- 为自动表单填充服务添加一个条目:
<servlet>
<servlet-name>autoformfill</servlet-name>
<servlet-class>
com.packtpub.gwtbook.samples.server.
AutoFormFillServiceImpl
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>autoformfill</servlet-name>
<url-pattern>/autoformfill</url-pattern>
</servlet-mapping>
- 为动态列表服务添加一个条目:
<servlet>
<servlet-name>dynamiclists</servlet-name>
<servlet-class>
com.packtpub.gwtbook.samples.server.
DynamicListsServiceImpl
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>dynamiclists</servlet-name>
<url-pattern>/dynamiclists</url-pattern>
</servlet-mapping>
- 为可分页数据服务添加一个条目:
<servlet>
<servlet-name>pageabledata</servlet-name>
<servlet-class>
com.packtpub.gwtbook.samples.server.
PageableDataServiceImpl
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>pageabledata</servlet-name>
<url-pattern>/pageabledata</url-pattern>
</servlet-mapping>
- 为实时数据网格服务添加一个条目:
<servlet>
<servlet-name>livedatagrid</servlet-name>
<servlet-class>
com.packtpub.gwtbook.samples.server.
LiveDatagridServiceImpl
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>livedatagrid</servlet-name>
<url-pattern>/livedatagrid</url-pattern>
</servlet-mapping>
- 为日志监听服务添加一个条目:
<servlet>
<servlet-name>logspy</servlet-name>
<servlet-class>
com.packtpub.gwtbook.samples.server.
LogSpyServiceImpl
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>logspy</servlet-name>
<url-pattern>/logspy</url-pattern>
</servlet-mapping>
- 为天气服务添加一个条目:
<servlet>
<servlet-name>weather</servlet-name>
<servlet-class>
com.packtpub.gwtbook.widgets.server.
WeatherServiceImpl
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>weather</servlet-name>
<url-pattern>/weather</url-pattern>
</servlet-mapping>
- 为欢迎文件添加一个条目,并将欢迎文件设置为我们应用程序的主 HTML 页面
Samples.html:
<welcome-file-list>
<welcome-file>
Samples.html
</welcome-file>
</welcome-file-list>
-
在
www/com.packtpub.gwtbook.samples.Samples目录下创建一个名为WEB-INF的新目录。在WEB-INF目录下创建两个子目录lib和classes。 -
将上述
web.xml文件复制到WEB-INF目录。 -
将
$SAMPLES_DIR/bin目录的内容复制到WEB-INF/classes目录。 -
将
$SAMPLES_DIR/lib目录的内容复制到WEB-INF/lib目录。 -
将
www/com.packtpub.gwtbook.samples.Samples目录复制到$TOMCAT_DIR/webapps。 -
启动 Tomcat。一旦它启动,转到以下 URL 以查看我们在本书中创建的
Samples应用程序:
http://localhost:8080/com.packtpub.gwtbook.samples.Samples/

刚刚发生了什么?
编译 GWT 应用程序会在www目录中生成应用程序的 HTML 和 JavaScript。这包含了用户界面所需的所有组件,并且实际上可以在任何 Web 服务器上运行。但是,如果您使用了任何 RPC 服务,则需要确保服务所需的任何第三方 JAR 文件以及服务和支持类与www目录的内容一起部署到 Servlet 容器中。我们选择了 Tomcat,因为它是最广泛使用的 Servlet 容器之一,并且是 JSP 和 Servlet 规范的参考实现。我们也可以将我们的应用程序部署到其他容器,如 Geronimo、JBoss、WebSphere、JOnAS 或 Weblogic。
部署到诸如 Tomcat 之类的 servlet 容器意味着我们需要结构化我们的部署以模仿 WAR 格式。因此,我们需要确保我们应用程序的所有 Java 类都在WEB-INF/classes目录中可用,并且我们应用程序使用的所有 JAR 文件都需要在WEB-INF/lib目录中。因此,我们将这些工件复制到这些目录。我们还创建一个部署描述符,Tomcat 需要识别我们的部署。这个文件是web.xml,它需要被复制到WEB-INF目录中。
一旦我们在www/com.packtpub.gwtbook.samples.Samples目录中准备好一切,我们将com.packtpub.gwtbook.samples.Samples复制到 Tomcat 的 Web 应用程序目录$TOMCAT_DIR/webapps。然后我们启动 Tomcat,启动时将从web.xml文件中注册应用程序,并使其在上下文com.packtpub.gwtbook.samples.Samples中可用。
使用 Ant 进行自动部署
我们将通过使用 Apache Ant 让 Ant 处理我们的 GWT 应用程序的部署,从而使我们的工作变得更加轻松,减少繁琐的工作。我们将通过使用 Apache Ant 自动化我们在上一节中所做的一切。
操作时间-创建 Ant 构建文件
以下是自动部署到 Tomcat 的步骤:
- 我们将修改在第三章中运行
applicationCreator创建的$SAMPLES_DIR/Samples.ant.xml文件,以创建全局属性来引用各种目录:
<property name="tmp" value="${basedir}/build" />
<property name="www" value=
"${basedir}/www/com.packtpub.gwtbook.samples.Samples" />
<property name="lib" value="${basedir}/lib" />
<property name="classes" value="${basedir}/bin" />
<property name="gwt-home" value="/gwt-windows-1.3.1" />
<property name="deploy-dir" value=
" /shonu/jakarta-tomcat-5.0.28/webapps" />
- 将我们在编译时需要的 JAR 文件添加到
classpath中:
<pathelement path="${lib}/junit.jar"/>
<pathelement path="${lib}/widgets.jar"/>
<pathelement path="${lib}/gwt-widgets-0.1.3.jar"/>
- 修改
clean目标以包括其他要清除的工件:
<target name="clean" description=
"Clean up the build artifacts">
<delete file="Samples.jar"/>
<delete file="Samples.war"/>
<delete>
<fileset dir="bin" includes="**/*.class"/>
<fileset dir="build" includes="**/*"/>
<fileset dir="www" includes="**/*"/>
</delete>
</target>
- 创建一个名为
create-war:的新目标。
<target name="create-war" depends="package" description=
"Create a war file">
<mkdir dir="${tmp}"/>
<exec executable="${basedir}/Samples-compile.cmd"
output="build-log.txt"/>
<copy todir="${tmp}">
<fileset dir="${www}" includes="**/*.*"/>
</copy>
<mkdir dir="${tmp}/WEB-INF" />
<copy todir="${tmp}/WEB-INF">
<fileset dir="${basedir}" includes="web.xml"/>
</copy>
<mkdir dir="${tmp}/WEB-INF/classes" />
<copy todir="${tmp}/WEB-INF/classes">
<fileset dir="${basedir}/bin" includes="**/*.*"/>
</copy>
<mkdir dir="${tmp}/WEB-INF/lib" />
<copy todir="${tmp}/WEB-INF/lib">
<fileset dir="${basedir}/lib" includes="**/*.jar" excludes=
"gwt-dev-*.jar,gwt-servlet.jar,gwt-user.jar,*.so"/>
</copy>
<jar destfile="${tmp}/WEB-INF/lib/gwt-user-deploy.jar">
<zipfileset src="img/gwt-user.jar">
<exclude name="javax/**"/>
<exclude name="META-INF/**"/>
<exclude name="**/*.java"/>
</zipfileset>
</jar>
<zip destfile="Samples.war" basedir="${tmp}" />
</target>
- 创建一个名为
deploy-war:的新目标
<target name="deploy-war" depends="clean,create-war"
description="Deploy the war file">
<copy todir="${deploy-dir}">
<fileset dir="${basedir}" includes="Samples.war"/>
</copy>
</target>
-
如果您还没有安装 Apache Ant,请安装它(
ant.apache.org)。确保 Ant 二进制文件在您的path上。 -
使用以下参数从
$SAMPLES_DIR运行 Ant:
ant -f Samples.ant.xml deploy-war
这将清除构建工件,编译整个应用程序,创建一个 WAR 文件,并将 WAR 文件部署到 Tomcat。您可以在 URLhttp://localhost:8080/Samples访问部署的应用程序。
当您运行 Ant 时,这是输出:

刚刚发生了什么?
Apache Ant 提供了一种很好的自动部署应用程序的方式。我们为清除旧的构建工件、创建 WAR 文件和将此 WAR 文件部署到 Tomcat 的webapps目录创建了目标。applicationCreator命令有一个选项用于生成一个简单的build.xml文件。我们使用此选项在第三章中为我们的Samples项目生成了一个骨架build.xml文件。我们拿到这个生成的文件并修改它以添加我们需要的所有其他目标。我们还将我们应用程序的所有class文件打包到Samples.jar中,而不是复制类本身。
从 Eclipse 部署
在上一节中,我们创建了与 Ant 一起使用的构建文件,以自动部署我们的应用程序到 Tomcat。但是,我们是从命令行运行 Ant 的。在本节中,我们将介绍从 Eclipse 内部运行 Ant 所需的步骤。
操作时间-从 Eclipse 运行 Ant
以下是从 Eclipse 内部运行我们的构建文件的步骤:
-
在 Eclipse 的Navigator视图中右键单击
Samples.ant.xml文件。这将显示运行 Ant 的选项。选择Run As | 1 Ant Build:![操作时间-从 Eclipse 运行 Ant]()
-
这将执行 Ant 并通过在 Eclipse 的Console视图中运行构建来显示输出:
![操作时间-从 Eclipse 运行 Ant]()
-
上一张截图显示了 Ant 脚本中
compile目标的输出,这是默认目标,如果您没有指定其他目标。现在我们将运行deploy-war目标。在 Eclipse 的导航器视图中再次右键单击Samples.ant.xml文件。这次选择运行为| 2 Ant 构建...选项,如下图所示:![操作时间-从 Eclipse 运行 Ant]()
-
这将显示一个窗口,您可以在其中选择要执行的目标:
![操作时间-从 Eclipse 运行 Ant]()
-
选择
deploy-war并单击运行以运行 Ant 构建。输出将显示在 Eclipse 的控制台视图中:![操作时间-从 Eclipse 运行 Ant]()
现在我们可以从 Eclipse 内部运行 Ant,并成功将应用程序部署到 Tomcat。
刚刚发生了什么?
Eclipse 为编辑和运行 Ant 构建文件提供了出色的支持。它识别build.xml文件,并在各个视图中添加上下文操作,以便您可以右键单击build.xml文件并执行 Ant 构建。它还为您提供了运行指定目标的选项,而不仅仅是运行文件中指定的默认目标。在本节中,我们学习了如何使用这种支持,以便我们可以直接从 Eclipse 环境中部署到 Tomcat。
总结
在本章中,我们学会了手动将 GWT 应用程序部署到 Tomcat。然后,我们看到了如何使用 Ant 自动化部署,这使我们可以从命令行部署我们的应用程序。
最后,我们利用了 Eclipse 内置的 Ant 支持,从 Eclipse 内部运行了我们的 Ant 构建文件。
附录 A. 运行示例
以下是下载和运行本书中开发的示例源代码所需的步骤:
-
从本书的网站(
www.packtpub.com/support)下载包含我们示例源代码的 ZIP 文件。将它们解压到硬盘上。解压文件时应该会创建两个目录——Samples和Widgets。这两个目录包含了本书中开发的应用程序的源代码。 -
启动 Eclipse 3.2。创建一个名为
GWT_HOME的新类路径变量。转到窗口 | 首选项 | Java | 构建路径 | 类路径变量。添加一个名为GWT_HOME的新变量条目,并将其设置为您解压 GWT 分发的目录,例如:C:\gwt-windows-1.3.1。这样可以确保 GWT JAR 文件对示例项目可用。 -
逐个将这两个项目导入到您的 Eclipse 工作区。您可以通过转到文件 | 导入 | 导入现有项目到工作区,然后选择项目的根目录来将现有项目导入 Eclipse。
Widgets项目用于创建打包在 JAR 文件中并被Samples项目使用的两个小部件。因此,它不定义入口点。您只需要运行/调试Samples项目。 -
你可以在 Eclipse 内部运行
Samples项目。转到运行 | 运行并选择Samples。这将启动熟悉的 GWT shell 并启动带有Samples应用程序的托管浏览器。 -
您可以在 Eclipse 内部调试
Samples项目。转到调试 | 调试并选择Samples。 -
如果您安装了 Apache Ant,您可以使用
Samples.ant.xml文件来构建应用程序并创建一个 WAR 文件,该文件可用于部署到诸如 Tomcat 之类的 Servlet 容器。 -
您还可以运行
Samples-compile.cmd来编译应用程序,以及运行Samples-shell.cmd来在 Windows 控制台上运行应用程序。

















浙公网安备 33010602011771号