内容


GWT 应用,第 2 部分

高级内容

实现高级 Google Web Toolkit 特性

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: GWT 应用,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:GWT 应用,第 2 部分

敬请期待该系列的后续内容。

通过一个类似 Swing 的 API,在 GWT 中可以实现在浏览器中运行的富用户界面,而无需任何诸如 Java™ Web Start 之类的其他软件。在这个分两部分的系列的 第 1 部分 中,我展示了 GWT 一些基础知识,包括使用小部件、调用远程过程调用(RPC)和实现复合小部件。

在本文中,我将继续 第 1 部分 未完成的部分,介绍 GWT 的一些更高级的方面:

  • 对话框
  • 接收事件和操纵文档对象模型(Document Object Model,DOM)元素属性
  • 图像装载(和忙光标)
  • 模块
  • 事件预览
  • 计时器

places 应用程序

第 1 部分,我介绍了 places 应用程序,如图 1 所示:

图 1. places 应用程序
places 应用程序
places 应用程序

当该应用程序启动时,它从一个数据库装载 6 个地址,并在右上角的列表框中显示它们。当单击其中一个地址时,应用程序将选中的地址装载到列表框右侧的格子中。然后,当单击格子中的 Show 按钮时,应用程序通过 Yahoo! Web Services 访问该地址的地图和天气信息,并在一个对话框中显示该信息。在对话框中,地图和天气信息位于一个可调整的水平分割面板中,如 图 1 所示。

第 1 部分 中,我讨论了从数据库装载地址,在列表框中显示它们,以及将从列表框中选中的项填入格子。第 1 部分中的清单 9 显示了在该文结束时 places 应用程序的代码。在此,我着重讨论对话框 —PlaceDialog 的一个实例 — 和一个定制的包含地图的 viewport 小部件。在向服务器发出两个 RPC 来从 Yahoo! Web Services 获取地图和天气信息之后,应用程序创建一个 PlaceDialog 实例,设置对话框的位置,并显示对话框:

PlaceDialog dialog = new PlaceDialog(addressGrid.getAddress(), urls, weatherHTML);
dialog.setPopupPosition(200, 200);
dialog.show();

在实现对话框及其 viewport 的过程中,可以探索 GWT 的一些更高级的方面。

对话框

在本文的后续部分,我将继续实现 places 应用程序。(可以 下载 完整的应用程序源代码。)首先,我要做的是实现 PlacesDialog 对话框,使之显示选中的地方的天气信息,如图 2 所示:

图 2. 初始 places 对话框
初始 places 对话框
初始 places 对话框

清单 1 显示初始的 PlacesDialog

清单 1. PlacesDialog.java,初始阶段
package com.clarity.client;
package com.clarity.client;

import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;

public class PlacesDialog extends DialogBox {    

  public PlacesDialog(Address address, String[] imageUrls, String weather) {
    super(false, false); // no auto-hide, and no modal

    setText(address.getAddress() + " " + address.getCity() + ", " + address.getState());
      
    HTML weatherHTML = new HTML();
    weatherHTML.setHTML(weather);
      
    setWidget(weatherHTML);
  }
}

清单 1 非常简单。PlacesDialog 扩展 DialogBox,其中包含一个 HTML 小部件,该小部件显示从 Web 服务获取的天气信息。在 PlacesDialog 构造函数中,我调用超类构造函数创建一个非模态对话框,当用户在对话框外单击鼠标时,对话框不会自动隐藏。

但是,清单 1 所示的对话框有一个问题。即无法通过单击对话框将其置于最外层。例如,在 图 2 中,永远无法在 Buffalo 对话框之上获得 San Francisco 对话框。为了实现该特性,需要为对话框接收(sink)鼠标事件,并处理鼠标按下(mouse-down)事件。

接收事件和操纵 DOM 元素属性

在更新的 PlacesDialog 类中,我通过调用 sinkEvents() 接收对话框中的鼠标事件,如 清单 2 所示。在调用 sinkEvents() 后,当对话框中发生鼠标事件时,GWT 将调用对话框的 onBrowserEvent() 方法。在该方法中,我通过调用 DOM.setIntStyleAttribute(),为与对话框关联的 DOM 元素增加 z 索引。

清单 2. PlacesDialog.java,第 2 阶段
package com.clarity.client;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;

public class PlacesDialog extends DialogBox {
  private static int z;
    
  public PlacesDialog(Address address, String[] imageUrls, String weather) {
    setText(address.getAddress() + " " + address.getCity() + ", " + address.getState());
      

    HTML weatherHTML = new HTML();
    weatherHTML.setHTML(weather);
      
    setWidget(weatherHTML);
      
    sinkEvents(Event.MOUSEEVENTS);
  }
    
  public void onBrowserEvent(Event event) {
   if (event.getTypeInt() == Event.ONMOUSEDOWN) {
      DOM.setIntStyleAttribute(PlacesDialog.this.getElement(), "zIndex", z++);    
    }
    super.onBrowserEvent(event);
  }
}

对于 清单 2 中的对话框,可以通过单击对话框将其置于最外层。

图像装载和忙光标

在清单 3 中,我增加一个水平分割面板,并将地图放在左侧,天气放在右侧,进一步丰富了 PlacesDialog 类的实现:

清单 3. PlacesDialog.java,第 3 阶段
package com.clarity.client;

import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalSplitPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Panel;

import com.clarity.widgets.client.Viewport;

public class PlacesDialog extends DialogBox {
    private static int z;
    private static final String[] zoomLevelItems = { 
      "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" 
    };
 
    private final Viewport viewport = new Viewport();
    private final Image[] images = new Image[12];
    private String[] imageUrls;
    
    final ListBox zoomLevels = new ListBox();
    
    public PlacesDialog(Address address, String[] imageUrls, String weather) {
      super(false, false); // no auto-hide, and no modal
      setText(address.getAddress() + " " 
          + address.getCity() + ", " 
          + address.getState());
      
      this.imageUrls = imageUrls;
      
      HorizontalSplitPanel hsp = new HorizontalSplitPanel();
      hsp.setPixelSize(600, 350);
      hsp.add(createMapPanel());
      hsp.add(createWeatherPanel(weather));
      
      zoomLevels.addChangeHandler(new ChangeHandler() {
        public void onChange(ChangeEvent event) {
          String v = zoomLevels.getItemText(zoomLevels.getSelectedIndex());
          setZoom(new Integer(v).intValue() - 1);
        }
      });
      
      setWidget(hsp);      
      sinkEvents(Event.MOUSEEVENTS);
    }

    public void onBrowserEvent(Event event) {
      if (event.getTypeInt() == Event.ONMOUSEDOWN) {
        DOM.setIntStyleAttribute(PlacesDialog.this.getElement(), "zIndex", z++);    
      }
      super.onBrowserEvent(event);
    }
    
    private Panel createMapPanel() {
      AbsolutePanel viewportPanel = new AbsolutePanel();
      
      for (String item : zoomLevelItems) {
        zoomLevels.addItem(item);
      }
      
      images[0] = new Image();
      images[0].setUrl(imageUrls[0]);
      
      viewport.setView(images[0]);
      
      viewportPanel.add(viewport);
     viewportPanel.add(zoomLevels, 10, 10);  // 10, 10 are x,y coordinates from ulhc
      
      DOM.setIntStyleAttribute(zoomLevels.getElement(), "zIndex", 
      DOM.getIntStyleAttribute(viewport.getElement(), "zIndex") + 1);
      
      return viewportPanel;
    }
    
    private HTML createWeatherPanel(String weather) {
      HTML weatherHTML = new HTML();
      weatherHTML.setHTML(weather);
      return weatherHTML;
    }
   
    public void setZoom(final int index) {
      if (images[index] == null) {
        images[index] = new Image();
        images[index].addLoadHandler(new LoadHandler() {
          public void onLoad(LoadEvent event) {
            zoomLevels.setEnabled(true);
            viewport.removeStyleName("waitCursor");
          }
        });
      zoomLevels.setEnabled(false);
      viewport.addStyleName("waitCursor");
    }
    images[index].setUrl(imageUrls[index]);
    viewport.setView(images[index]);
  }
}

清单 3 中,我在 createMapPanel() 方法中创建一个 viewport。从 import 语句可以看出,这个 viewport 是 com.clarity.widgets.client.Viewport 的实例。我将该 viewport 和 zoom-level 下拉框添加到一个 AbsolutePanel 实例。使用 Absolute 面板可以按像素位置固定其中的小部件 — 注意,我将 zoom-level 下拉框固定在面板左上角距顶部 10 像素、距左端 10 像素的位置。

如图 3 所示,当选定一个缩放级别时,应用程序将光标变为等待光标,同时按选定的缩放级别装载图像。当装载完选定缩放级别的图像时,应用程序将光标变回初始状态。

图 3. 装载图像
装载图像
装载图像

清单 4 摘自 清单 3,它展示如何监视图像的装载,并相应地操纵光标:

清单 4. 装载图像
package com.clarity.client;
public class PlacesDialog extends DialogBox {
    private static int z;
    private static final String[] zoomLevelItems = { 
      "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" 
    };
 
    private final Viewport viewport = new Viewport();
    private final Image[] images = new Image[12];
    private String[] imageUrls;
    
    final ListBox zoomLevels = new ListBox();
    
    public PlacesDialog(Address address, String[] imageUrls, String weather) {
      ...
      zoomLevels.addChangeHandler(new ChangeHandler() {
        public void onChange(ChangeEvent event) {
          String v = zoomLevels.getItemText(zoomLevels.getSelectedIndex());
          setZoom(new Integer(v).intValue() - 1);
        }
      });
      ...
    }

    ...
   
    public void setZoom(final int index) {
      if (images[index] == null) {
        zoomLevels.setEnabled(false);
       viewport.addStyleName("waitCursor");

        images[index] = new Image();

        images[index].addLoadHandler(new LoadHandler() {
          public void onLoad(LoadEvent event) {
            zoomLevels.setEnabled(true);
           viewport.removeStyleName("waitCursor");
          }
        });
    }
    images[index].setUrl(imageUrls[index]);
    viewport.setView(images[index]);
  }

清单 4 中,我为 zoom 下拉框增加一个修改处理器(change handler)。(请参阅 第 1 部分,获得关于事件处理器的更多信息。)该修改处理器调用 setZoom() 方法,该方法检查之前图像是否已装载;如果之前未装载,setZoom() 为图像增加一个装载处理器(load handler),将光标变为等待光标,并禁用 zoom 下拉框。然后,当图像装载完成后并且 GWT 调用装载处理器的 onLoad 方法时,装载处理器将光标恢复到初始状态,并启用 zoom 下拉框。应用程序的样式表中定义了 waitCursor CSS 样式,如下所示:

.waitCursor {
  cursor: wait;
}

模块

正如 第 1 部分 中所述,每个 GWT 应用程序都是一个模块。但是,反之却不成立:每个模块并不是一个 GWT 应用程序。模块是一种重用机制,GWT 应用程序可以使用任意数量的其他模块。

模块仅仅是一些工件 — 例如定制的 GWT 小部件、JavaScript 和 CSS — 加上一个模块定义 XML 文件。例如,我将 Viewport 类放在它自己的一个模块中。图 4 显示 Widgets 模块的源文件。

图 4. Widgets 模块的源文件
Widgets 模块的源文件

图 4 中显示的模块是一个最简单的模块。它有一个模块配置文件(Widgets.gwt.xml),以及 viewport 的实现(Viewport.java)。如清单 5 所示,配置文件也非常简单:

清单 5. Widgets.gwt.xml
<module rename-to='widgets'>
  <inherits name='com.google.gwt.user.User'/>
</module>

Widgets 模块继承 GWT User 模块,其中包含 GWT 的核心代码。

在创建模块配置文件并实现 Viewport 类之后,我创建了一个 JAR 文件,其中包含 Widgets 模块,如图 5 所示:

图 5. Widgets 模块的 JAR
Widgets 模块的 JAR

为了使用 places 应用程序中的 Widgets 模块,要做两件事:

  • 将 Widgets JAR 文件放在类路径上。
  • 在 places XML 配置文件中继承 Widgets 模块。

有了模块机制,便可以将定制的小部件及其相关工件打包,使其他开发人员可以使用它们。应用程序可以继承任意数量的模块,而模块又可以包含其他模块。实际上,可以以任意深度嵌套模块。

事件预览

利用 Viewport 类可以拖动 viewport 的视图,如图 6 所示:

图 6. 在 viewport 中拖动地图
在 viewport 中拖动地图
在 viewport 中拖动地图

现在已经知道,GWT 提供了一些鼠标处理器和 absolute 面板,在 absolute 面板中,可以按像素位置放置小部件。如果将鼠标处理器与 absolute 面板相结合,便可以拖动 absolute 面板中的小部件,如 图 6 所示。但是,如果试图拖动图像,浏览器将妨碍您拖动它,如图 7 所示:

图 7. 浏览器妨碍图像拖动
浏览器妨碍图像拖动
浏览器妨碍图像拖动

我所熟悉的所有浏览器都允许拖动图像,如果试图拖动 absolute 面板中的图像,浏览器将帮助拖动图像,如 图 7 所示。但是,在这种情况下,我并不希望浏览器帮助拖动图像,因为它会干扰我拖动图像。

我需要一种方法告诉浏览器不要干扰图像拖动。这种方法就是通过预览(preview) 事件,如清单 6 所示:

清单 6. 预览事件
  public class Viewport extends AbsolutePanel {
    ...
    private HandlerRegistration handlerRegistration = null;

    private Event.NativePreviewHandler preventDefaultMouseEvents = 
      new Event.NativePreviewHandler() {
      public void onPreviewNativeEvent(NativePreviewEvent event) {
        if (event.getTypeInt() == Event.ONMOUSEDOWN 
            || event.getTypeInt() == Event.ONMOUSEMOVE) {
          event.getNativeEvent().preventDefault();        
        }
      }
    };

    addDomHandler(new MouseOverHandler() {
      public void onMouseOver(MouseOverEvent event) {
        handlerRegistration = Event.addNativePreviewHandler(preventDefaultMouseEvents);
      }
    }, MouseOverEvent.getType());

    addDomHandler(new MouseOutHandler() {
      public void onMouseOut(MouseOutEvent event) {
        if (handlerRegistration != null) {
          handlerRegistration.removeHandler();
        }
      }
    }, MouseOutEvent.getType());
    ...
  }
}

清单 6 中,我实现了一个本地预览处理器(native preview handler),顾名思义,通过这个本地预览处理器,可以在 GWT 或浏览器应对事件之前预览事件。在事件预览中,对于本地事件,可以通过调用 preventDefault(),阻止浏览器对鼠标按下和鼠标移动事件作出反应。该方法阻止 浏览器对鼠标事件作出默认的 反应 — 该方法的名称 preventDefault() 也因此而来。

当光标进入 viewport 时,我将本地预览处理器添加到 GWT 事件栈的顶部。当光标离开 viewport 时,移除预览,从而使事件处理恢复正常。因此,当光标在 viewport 中时,GWT 阻止浏览器对鼠标按下和鼠标移动事件做出默认反应,从而防止浏览器干扰图像拖动。

计时器

如果在 viewport 中拖动鼠标不超过半秒,viewport 将沿着鼠标拖动方向移动地图,移动速度与拖动鼠标时涵盖的像素数相关。移动将一直继续,直到在 viewport 中单击鼠标。我使用一个 GWT 计时器执行这样的移动,如清单 7 所示:

清单 7. 用 GWT 计时器移动 viewport 的视图
private static final int TIMER_REPEAT_INTERVAL = 50;
private static final int SPEED_FACTOR_MULTIPLIER = 20;

...

timer = new Timer() {
  public void run() {
    // Calculate new X and Y locations for the
    // mouse panel, and reposition it.
    int newX = getWidgetLeft(view) - (int) (unitVectorX * speedFactor);
    int newY = getWidgetTop(view) - (int) (unitVectorY * speedFactor);

    repositionView(newX, newY);
  }
};

...

addDomHandler(new MouseUpHandler() {
  public void onMouseUp(MouseUpEvent event) {
    if (deltaTime < gestureTimeThreshold && 
       (deltaX > gesturePixelThreshold || deltaY > gesturePixelThreshold)) {
  
      speedFactor = ((deltaX + deltaY) / (timeUp - timeDown)) * SPEED_FACTOR_MULTIPLIER;
      timer.scheduleRepeating(TIMER_REPEAT_INTERVAL);
      timerRunning = true;
    }      
  }
}
...

addDomHandler(new MouseDownHandler() {
 public void onMouseDown(MouseDownEvent event) {
    System.out.println("mouse down " + event.getX() + ", " + event.getY());
    int x = event.getX(), y = event.getY();

    if (isGesturesEnabled() && timerRunning) {
      // On a mouse down, if the timer is running, stop it.
      timerRunning = false;
      timer.cancel();
    }
} 
...
private void repositionView(int newX, int newY) {
  // Check to see if the view scrolled out of sight;
  // if so, bring it back in view
  if (newX > 0) {
    newX = 0;
    unitVectorX = 0 - unitVectorX;
  } else if (newX < 0 - view.getOffsetWidth() + getOffsetWidth()) {
    newX = 0 - view.getOffsetWidth() + getOffsetWidth();
    unitVectorX = 0 - unitVectorX;
  }

  if (newY > 0) {
    newY = 0;
    unitVectorY = 0 - unitVectorY;
  } else if (newY < 0 - view.getOffsetHeight() + getOffsetHeight()) {
    newY = 0 - view.getOffsetHeight() + getOffsetHeight();
    unitVectorY = 0 - unitVectorY;
  }
    
  if (isRestrictDragVertical())
    setWidgetPosition(view, xstart, newY);
  else if (isRestrictDragHorizontal())
    setWidgetPosition(view, newX, ystart);
  else
    setWidgetPosition(view, newX, newY);
}

清单 7 非常简单。我实现了一个计时器,该计时器计算地图在 viewport 中的新位置,然后调用助手方法 — repositionView() — 重新放置地图。

在 viewport 的 mouse-up 处理器中,我调用计时器的 scheduleRepeating 方法,该方法启动计时器,并按照时间间隔 TIMER_REPEAT_INTERVAL(50 毫秒)周期性地调用它。最后,当鼠标在 viewport 中按下,并且计时器正在运行时,我调用计时器的 cancel() 方法停止计时器。

Viewport 类大约有 350 行,限于篇幅,本文无法列出,但是,如果 下载 了 places 应用程序的源文件,可以看一下该代码。

结束语

GWT 是一个令人兴奋的框架,您可以使用它实现可以想像到的任何功能。通过一个强大的、类似 Swing 的 API,可以创建在浏览器中运行的富客户机用户界面。在这个分两部分的系列中,我展示了 GWT 的一些基本的东西,也展示了一些更高级的方面,例如使用计时器和事件预览。在阅读本系列并 下载 示例应用程序之后,现在您可以用 GWT 来实现自己的富用户界面。

在这个简短的系列中,虽然我谈到了 GWT 的一些关键的基础和更高级的方面,但是可想而知,这个框架还有很多其他方面,包括最近它与 Google App Engine 的集成。GWT 的未来版本将包括更多令人兴奋的特性,包括内置的对拖放的支持。请阅读 参考资料,并继续关注 GWT!


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=450781
ArticleTitle=GWT 应用,第 2 部分: 高级内容
publish-date=11302009