瘦客户机框架简介

瘦客户机框架(Thin Client Framework,TCF)是一种用于 java 客户机应用程序的轻量级、灵活和功能强大的编程框架。在这一由两部分组成的系列文章中,您将从它的两位作者那里了解 TCF。请跟随 Barry Feigenbaum 博士和 PeterBahrs 博士,他们使用详细的讨论、工作示例以及生动的代码样本向您介绍了 TCF 体系结构、设计和实现。

瘦客户机框架(TCF)是一种设计、开发和部署方法,它使用 java 语言容易和快速地开发高性能的、可扩展的以及响应迅速的电子商务客户机。TCF 构建在模型-视图-控制器(Model-View-Controller)设计模式之上,并将该模式提升到了应用程序体系结构级别。通过基于事件的通信模型的统一实现,TCF 向客户机端应用程序开发提供了高度可插入的、基于组件的结构。

TCF 定义了编写 java 客户机应用程序的最佳实践,这些最佳实践强调编码问题与职责分离。其高度模块化提供了最大限度的重用。因为 TCF 与多种数据、服务器和网络模型相兼容,所以它极其灵活,可以使 java 客户机按需充当瘦客户机或胖客户机。最后,TCF 支持基于公式的方法来评估开发费用,并且在支持客户机和服务器的并发开发时,它还支持客户机上的并行组件开发。

在这个由两部分组成的系列文章中,我们将向初学者介绍 TCF。本系列文章的第 1 部分将使您熟悉该体系结构的基本元素。我们将解释 TCF 背后的概念,并描述该体系结构的组件。我们还将提供一个工作示例应用程序,所以您自己可以看到如何对这些组件编码。在第 2 部分中,我们将提供对 TCF 的设计和实现的更详细讨论。

TCF 的优点

TCF 已显示出它具有下列优点:

  • 使项目管理活动得以轻松进行
  • 提供可重复的开发模式
  • 支持对技能进行细致划分
  • 支持快速演示的开发
  • 缩短客户机开发时间,最多可减少 75% 的时间
  • 提供了比其它方法更佳的客户机组件重用,其重用性最多可相当于其它方法的两倍
  • 产生较小的 java 客户机程序(在字节码大小方面)
  • 促进程序员生产率
  • 通过多个数据模型、服务器和协议提供了灵活性
  • 支持因特网友好的客户机
  • 支持客户机和服务器端的并行开发

 

客户机应用程序设计

开发分布式应用程序客户机涉及许多相关的设计考虑事项。图 1 演示了这些考虑事项中的一些。一个成功的瘦客户机开发框架必须以简单明了的方式满足这些考虑事项。



图 1. 客户机端设计考虑事项
客户机端设计注意事项

在端对端系统开发的范围内,中间层和后端服务器通常是比较困难和耗时的部分。它们(不是客户机)要耗费大部分的设计工作。图 2 是端对端系统设计的一个示例:



图 2. 复杂的端对端系统
复杂的端到端系统

通过为客户机端开发提供已测试的模式,TCF 确保解决了客户机最重要的几个方面,同时极大地减少了在客户机端设计考虑事项上所花费的时间。

“胖”还是“瘦”?

一个重要的设计考虑事项是客户机应该是“瘦”还是“胖”。基于客户机拥有功能(或业务逻辑)的多少,可以将它定义为胖客户机或瘦客户机。可以从包括纯瘦型到纯胖型在内的广大范围上实现应用程序客户机,混合型也并不罕见。

TCF 应用程序的特点是将应用程序业务逻辑在客户机和服务器之间进行分离。一般情况下,在 TCF 应用程序中,消息的大小很小且很有效,不需要高速链接就可以产生很好的互操作性。TCF 应用程序另外为人称道的一点是客户机和服务器之间是松散耦合的,这样可以独立开发客户机端和服务器端。TCF 设计的分隔功能对于高度分布式应用程序最实用。图 3 显示了典型的混合型 TCF 配置:



图 3. 混合型瘦客户机配置
混合的瘦客户机配置




回页首


对客户机编码

在决定了客户机上需要一些业务逻辑之后,就必须决定如何实现它。两种最常见的方法是:

  1. 使用某种由浏览器激活的脚本语言形式,例如 JavaScript/动态 HTML(Dynamic HTML,DHTML)
  2. 用诸如 java 语言之类的健壮的编程语言将客户机编写成 applet 或应用程序

在本文中,我们将进行基于 java 的客户机实现。在以下情况中,Java 语言是首选的(或要求的)实现:

  • 应用程序必须考虑到未连接到网络的用户的需要
  • 应用程序需要比 DHTML 所能提供的更丰富或更高响应能力的 GUI
  • 需要在客户机端进行数据高速缓存,或者客户机必须能够操作大量数据
  • 在客户机端编写脚本无法满足需要
  • 服务器的会话状态如此巨大,以致必须将它分布到客户机端
  • 应用程序需求声明必须用 java 语言编写应用程序

我们是怎样做错的

清单 1 显示了典型的 java 应用程序编码风格。看一下您是否能看出代码何处有错。



清单 1. 典型的 Java 应用程序代码
import java.util.*;
import com.xyz.*;
Customer customer   = createCustomer();
  :
Socket socket       = createSocket();
customer = (Customer) socket.readObject();
String name         = customer.getName();
TextField tf        = new TextField();
Frame frame         = new Frame();
tf.setText(name);
frame.add(tf);
LogFile log         = createLogFile();
log.writeStatus("updated");

此时我们拥有的是一个难以管理的胖客户机的开始部分。

  • 为什么?因为整个应用程序混合了多种代码职责。
  • 如何进行修正?将代码按照职责进行分离。




回页首


TCF 和 MVC 模式

模型-视图-控制器(MVC)模式是一种经典的已被充分理解的设计模式。正如图 4 所示,MVC 模式在整个应用程序中可以在多个级别上实现。在其最细致级别,MVC 模式确定 GUI 如何控制工作。在中间级别,它结构化应用程序组件。在最广泛级别,MVC 模式将应用程序划分成多个层,来表示流行的三层应用程序模型。TCF 就是中间级别的 MVC 使用示例。



图 4. TCF 与 MVC 模式的关系
TCF 与 MVC 模式的关系

此时,您应该基本了解了瘦客户机框架在分布式应用程序开发过程中的作用;典型的 TCF 客户机看上去如何;如何 避免java 代码编码分布式应用程序客户机以及 MVC 模式在 TCF 中所担当的底层角色。从这里开始,我们准备研究 TCF 体系结构。





回页首


TCF 体系结构

图 5 显示了完整的 TCF 体系结构。正如您看到的,它相当小,且易于理解。椭圆型框中的名称代表主要的 TCF 接口或类,而矩形或菱形框中的名称代表 TCF 支持类。组件间的通信是通过事件进行处理的,如图 5 所示。基于事件的通信是 TCF 体系结构模式的基本特征。



图 5. TCF 体系结构
TCF 体系结构图

主要的 TCF 组件

主要 TCF 组件的角色是:

  • ViewController 接口定义了可重用的用户界面组件,该组件是整个客户机应用程序 GUI 的一部分。 ViewController 通常是由诸如 Panel 那样的 AWT(或 Swing)组件实现的。
  • PlacementListener 接口管理 ViewController 在屏幕上的布局。 PlacementListener 通常是由应用程序实现的。
  • ApplicationMediator接口定义了应用程序的控制逻辑。
  • Transporter 接口选择处理 RequestEvent 的适当目标位置。
  • Destination是 TCF 客户机需要的任何服务的抽象。
  • TopListener 接口用于将特定于业务或环境的职责与其它 TCF 组件部分分隔开。 TopListener 通常是由应用程序实现的。
  • ViewEvent 是 GUI 事件的抽象。 ViewEventViewControllerViewListener (通常是 ApplicationMediator )之间的通信机制。
  • RequestEvent 是轻量级事务请求。 RequestEventApplicationMediatorRequestListener (通常是 Transporter )之间的通信机制。
  • PlacementEventApplicationMediatorPlacementListener (通常是应用程序)之间的通信机制。
  • TopEventApplicationMediatorTopListener (通常是应用程序)之间的通信机制。

主要的 TCF 接口

在图 6 中,您可以看到主要的 TCF 接口。回忆一下 TCF 将开发过程划分成多个编码问题和职责。每个 TCF 接口代表不同方面的职责。开发人员可以根据他们在开发团队中的角色精通一个或多个接口。



图 6. TCF 的主要接口/类
TCF 模式主要接口/类

TCF 提供了每个接口的缺省或抽象实现。这样,就提供了您可以构建在其上的部分实现。许多要求使用 TCF 的常见任务都构建到了缺省接口实现中。由源接口实现所触发的事件处理接口间的所有交互。

基于事件的通信

因为 TCF 体系结构基于组件并由事件驱动,所以对 TCF 系统中的每个组件独立进行开发和单元测试都很容易。一旦定义了事件及其 majorminorcommand值,就可以通过简单的支架(scaffolding)代码来测试组件。这样,发送方和接收方之间的事件定义就是组件间唯一强大的纽带。

因为 TCF 系统中的组件都是松散耦合的,所以在运行时可以插入和删除它们。另外,这样还提供对流跟踪和数据过滤/转换的方便支持。

规模估计和费用估算

因为 TCF 使用松散耦合、高度可伸缩的开发范例,所以它使开发规模估计非常简单。如图 7 所示,标准化之后,简单的基于倍增的规模估计将满足大多数 TCF 项目。



图 7. 示例组件估算
示例组件评估

上面的估算基于低级设计和代码估算,并假设您正和有 java 经验的程序员一起工作。所显示的估算是示例;您的值可能会不同。





回页首


示例应用程序

在本文其余部分,我们将使用一个示例应用程序来演示 TCF 背后的概念。Example04 是一个非常简单的客户信息系统,它维护一个客户列表。应用程序是一个面板。可以将它封装到框架或 applet 中。下面的几张图中显示了 applet 表单。为本文起见,我们将只显示应用程序的“选择”部分。您可以在 alphaWorks 站点查看完整示例以及多个其它示例(请参阅 参考资料)。

请再次记住本文主要是一篇概述;我们将在本系列的第二部分进行更详细的描述。

Example04 由以下组件组成:

  • 四个 ViewControllerVC1VC2VC3StatusVC
  • 一个 ApplicationMediatorAM4
  • 一个 Transporter 和一个 Destination
  • 一个基于 键值对的数据模型
  • 一个主应用程序(Example04)
  • 一个 Codes 接口,定义符号常量

在接下来的各节中,我们将讨论除 Codes 接口之外的每个组件。





回页首


应用程序数据模型

简单的键值对(即 Map )结构用作应用程序数据模型。在应用程序中传递它,并根据用户的输入进行适当更新。清单 2 显示了两个简便的方法,这两个方法使通常从登录屏幕和客户信息屏幕上获得的信息复位:



清单 2. 数据模型类定义
public class Data extends Hashtable {
    public static final String FAMILY = "Example04";
    public void initCustomer() {
        put(Codes.TITLE, Codes.TITLE);
        put(Codes.FIRSTNAME, Codes.FIRSTNAME);
        put(Codes.LASTNAME, Codes.LASTNAME);
        put(Codes.FULLNAME, Codes.FIRSTNAME + " " +
             Codes.LASTNAME);
        put(Codes.WWW, Codes.WWW);
        put(Codes.OFFICE, Codes.OFFICE);
        put(Codes.PHONENUMBER, Codes.PHONENUMBER);
        put(Codes.EMAIL, Codes.EMAIL);
    }
    public void initLogin() {
        put(Codes.USERID, Codes.USERID);
        put(Codes.PASSWORD, Codes.PASSWORD);
    }
    public Data() {
        initLogin();
        initCustomer();
    }
}





回页首


ViewController

Example04 包含四个视图控制器:

  • VC1 是登录屏幕
  • VC2 是客户选择屏幕
  • VC3 是客户信息屏幕
  • StatusVC 显示前三个 ViewController 中哪个在当前是活动的

示例应用程序总是同时显示两个 ViewControllerStatusVC 总是显示在屏幕的顶部,而其余三个中的一个显示在屏幕的中央。图 8 让您查看登录屏幕:



图 8. VC1:登录屏幕
示例 ViewController 1




回页首


事件处理

常规的 java 事件处理用于处理 GUI 事件。每个 ViewController 将一个或多个 AWT 事件(例如,按钮按下)转换成 ViewEvent 。事件数据参数作为数据对象被传递。

在以下代码样本中,我们将查看每个 ViewControllerViewEvent 。每一节代码都封装在 xxxxButton_actionPerformed 方法中,其中的 xxxx 是按钮名称。为保持简洁(和简单),没有显示封装器。

StatusVC

StatusVC 显示了当前 ViewControllerjava 类名称。所有屏幕上都显示 StatusVC ,但只有在显示 VC2 时它的按钮才是活动的。

Save 触发 SAVE ViewEvent

// save customer records to a file
fireViewEvent( new ViewEvent(this, ViewEvent.SAVE));

Load 触发 LOAD ViewEvent

// load customer records
fireViewEvent( new ViewEvent(this, ViewEvent.LOAD));

登录:VC1

VC1 输入登录值,通常是用户标识和密码。下面显示它的 ViewEvent

登录将输入域添加到数据模型中并触发 LOGIN ViewEvent

// grab textfields, update data model
     Data d = this.data;
     d.put(Codes.USERID, getNameField().getText());
     d.put(Codes.PASSWORD, getPasswordField().getText());
     setEnabled(false);
     fireViewEvent(
         new ViewEvent(this, ViewEvent.LOGIN, d));

注:我们禁用了 ViewController 。这样可以防止在完全处理完 ViewEvent 之前接受新按钮的按下。一旦处理完 ViewEvent ,它将被 refresh() 方法重新启用。

Done 触发 CANCEL ViewEvent

    setEnabled(false);
     fireViewEvent( new ViewEvent(this, ViewEvent.CANCEL));

客户选择:VC2

当完成登录时,显示 VC2 ,如图 9 所示:



图 9. 客户选择屏幕
客户选择屏幕

正如您可看到的, VC2 显示了现有用户列表,以及管理和更新该列表的几个按钮。由于本屏幕上的 ViewEvent 处理非常类似于 VC1,所以我们只显示 Edit 事件。

Edit创建或更新用户信息:

    Data d = this.data;
     d.put( Codes.FULLNAME,
            getAccountList().getSelectedValue());
     fireViewEvent( new ViewEvent(this, ViewEvent.DETAILS, d));

被选中的用户名被放置在 FULLNAME 数据模型输入项中。

客户信息:VC3

在单击 EditNew 按钮时,显示 VC3 ,如图 10 所示:



图 10. 客户信息屏幕
客户信息屏幕

VC3 用于查看或编辑客户输入项。下面显示它的视图事件。

OK 获取 GUI 上的数据、更新模型并触发 UPDATE ViewEvent

Data d = this.data(); d.initCustomer();
String fn, ln;
d.put(Codes.FIRSTNAME, fn = getFirstNameField().getText());
d.put(Codes.LASTNAME, ln = getLastNameField().getText());
d.put(Codes.FULLNAME, fn + " " + ln);
d.put(Codes.TITLE, getTitleField().getText());
d.put(Codes.PHONENUMBER, getPhoneField().getText());
d.put(Codes.WWW, getWwwField().getText());
d.put(Codes.EMAIL, geteMailField().getText());
d.put(Codes.OFFICE, getOfficeField().getText());
fireViewEvent( new ViewEvent(this, ViewEvent.UPDATE, d) );

Cancel 触发 DONE ViewEvent

fireViewEvent( new ViewEvent(this, ViewEvent.DONE) );





回页首


ApplicationMediator

ApplicationMediator 处理由各种 ViewController 生成的 ViewEvent 。它对 ViewController 排顺序并生成 RequestEvent 以执行由各种 ViewController 请求的操作。我们将在下面查看 ApplicationMediator 如何处理两个典型事件请求。

初始化和激活

下面显示了初始化 ApplicationMediator 。这个初始化序列创建了四个 ViewController 并发出一个布局请求,使 VC1 成为活动的 ViewController 。清单 3 显示了 ApplicationMediator 如何处理这些请求:



清单 3. ApplicationMediator 请求处理
public void init() {
    super.init();
    String[] classes = {
        "com.ibm.jtc.examples.ex04.VC1",
        "com.ibm.jtc.examples.ex04.VC2",
        "com.ibm.jtc.examples.ex04.VC3",
        "com.ibm.jtc.examples.ex04.StatuVC"};
    try { initViewControllers(classes); }
    catch (Exception e) { return; }
    firePlacementEvent(
      new PlacementEvent(this, getVC(0), PlacementEvent.ADD, 0) );
}

将 ViewEvent 分派给处理程序

ApplicationMediator 可以用几种不同方法处理请求。下面显示了两种比较流行的解决方案:按事件源进行处理或按事件代码进行处理(注:这两个示例都使用同步事件分派)。

选项 1:按事件源处理 ViewEvent
在清单 4 中,方法 getVC() 返回如上面 init 方法的 classes 变量中列出的 ViewController



清单 4. 按事件源进行处理
public void processViewEvent(ViewEvent ve) {
    RequestEvent re = new RequestEvent(this, MY_FAMILY);
    ViewController vc = (ViewController)ve.getSource();
    if      (vc == getVC(0)) doVC1(re, ve);
    else if (vc == getVC(1)) doVC2(re, ve);
    else if (vc == getVC(2)) doVC3(re, ve);
    else if (vc == getVC(3)) doStatusVC(re, ve);
}

选项 2. 按代码值处理 ViewEvent
如清单 5 所示,代码值是 majorminor。清单 5 中显示的详细行为通常包含在上面的 doxxx 方法中。



清单 5. 按代码值进行处理
public void processViewEvent(ViewEvent ve) {
    RequestEvent re = new RequestEvent(this, MY_FAMILY);
    int major = ve.getMajor(), minor = ve.getMinor();
    try {
       switch ( major ) {       // process major code
            case ViewEvent.LOGIN :
              re.setCommand(Codes.LOGIN);    // do LOGIN
              re.setData(ve.getData());
              fireRequestEvent(re);
              re.setCommand(Codes.GETNAMES); // do GETNAMES
              re.setData(ve.getData());
              fireRequestEvent(re);
              firePlacementEvent(            // remove VC1
                  new PlacementEvent(this, getVC(0),
                         PlacementEvent.REMOVE) );
              firePlacementEvent(            // add VC2
                  new PlacementEvent(this, getVC(1),
                         PlacementEvent.ADD) );
              getVC(3).refresh(getVC(1).toString());  // update status
              getVC(1).refresh(re.getData());   // update VC2 fields
              getVC(3).setEnabled(true);        // enable user actions
              break;
            case ViewEvent.DETAILS :
              re.setCommand(Codes.GETDETAILS);  // do DETAILS
              re.setData(ve.getData());
              fireRequestEvent(re);
              firePlacementEvent(            // remove VC2
                  new PlacementEvent(this, getVC(1),
                           PlacementEvent.REMOVE) );
              firePlacementEvent(            // add VC3
                   new PlacementEvent(this, getVC(2),
                           PlacementEvent.ADD) );
              getVC(3).refresh(getVC(2).toString()); // update status
              getVC(2).refresh(re.getData());
              break;
            case ViewEvent.CANCEL :
              fireTopEvent(                 // end execution
                  new TopEvent(this, TopEvent.EXIT) );
              break;
            case ViewEvent.REFRESH :
              re.setCommand(Codes.GETNAMES);   // do GETNAMES
              fireRequestEvent(re);
              getVC(1).refresh(re.getData());  // update VC2 fields
              break;
            case ViewEvent.DELETE :
              re.setCommand(Codes.DELETE);     // do DELETE
              re.setData(ve.getData());
              fireRequestEvent(re);
              re.setCommand(Codes.GETNAMES);   // do GETNAMES
              re.setData(ve.getData());
              fireRequestEvent(re);
              getVC(1).refresh(re.getData()); // update VC2 fields
              break;
            case ViewEvent.UPDATE :
              re.setCommand(Codes.UPDATE);    // do UPDATE
              re.setData(ve.getData());
              fireRequestEvent(re);
              refresh(re.getData());         // update all VC fields
              firePlacementEvent(            // remove VC3
                  new PlacementEvent(this, getVC(2),
                             PlacementEvent.REMOVE) );
              firePlacementEvent(            // add VC2
                   new PlacementEvent(this, getVC(1),
                             PlacementEvent.ADD) );
              getVC(3).refresh(getVC(1).toString()); // update status
              break;
            case ViewEvent.DONE :
              firePlacementEvent(            // remove VC3
                   new PlacementEvent(this, getVC(2),
                             PlacementEvent.REMOVE) );
              firePlacementEvent(            // add VC2
                   new PlacementEvent(this, getVC(1),
                             PlacementEvent.ADD) );
              getVC(3).refresh(getVC(1).toString()); // update status
              break;
        }
    }
    catch(Exception e) {
       e.printStackTrace();
       return;
    }
}

清单 5 演示了如何对典型的 ApplicationMediator 编码:首先它对 majorminor值解码,随后它处理请求。请求处理通常由以下几部分组成;创建和触发一个或多个 RequestEvent 以处理输入操作、用事件结果刷新组件以及更改至新的屏幕。要在这几个屏幕之间进行切换, ApplicationMediator 指示 PlacementListener 除去当前的屏幕并添加新的屏幕。

Transporter 和 Destination

通过 Transporter 进行间接作用, Destination 实现了从 ApplicationMediator 触发的 RequestEvent 。注: RequestEventApplicationMediator 开始,通过 Transporter ,随后到达 DestinationTransporter 充当路由器来选择一个或多个 Destination 以处理 RequestEvent

Example04 提供的简单 Destination 的多个实现是可能的。为简单起见,本示例使用 Destination ,它将命令请求值解码到对封装数据提供访问的例程。清单 6 显示了解码逻辑:



清单 6. 示例 Destination 的解码逻辑
String cmd = request.getCommand();
if      (cmd.equals(Codes.GETNAMES))
    doGetNames(request);
else if (cmd.equals(Codes.GETDETAILS))
    doGetDetails(request);
else if (cmd.equals(Codes.UPDATE))
    doUpdate(request);
else if (cmd.equals(Codes.NEWRECORD))
    doNewRecord(request);
else if (cmd.equals(Codes.DELETE))
    doDelete(request);
else if (cmd.equals(Codes.LOGIN))
    doLogin(request);
else if (cmd.equals(Codes.NEWLOGIN))
    doNewLogin(request);
else if (cmd.equals(Codes.SAVE))
    doSaveData();
else if (cmd.equals(Codes.LOAD))
    doLoadData();





回页首


示例应用程序

我们以查看示例应用程序作为结束。从下面的简单代码样本中,您应该能够看到我们所描述的 TCF 体系结构的各种组件如何在一个分布式应用程序中协同工作。

清单 7 显示了 Example04 应用程序类:



清单 7. 公用类 Example04
public class Example04 extends JPanel
  implements JTC, PlacementListener, TopListener {
    LocalDestination dest = null;
    AM1 am = null;
    Transporter trans = null;
    Data data = null;
    List jtcs = new Vector();       // all JTC implementers
    String filename = null";
    JPanel cp = null;
    public Example04 app = null;    // convenient ref to me
    :
}

清单 8 显示了应用程序的 main() 方法:



清单 8. Example04 的 main() 方法
public static void main(java.lang.String[] args) {
    new Example04("Example04",
                  args.length > 0 ? args[0]
                                  : "example04.db");
}

清单 9 显示了应用程序的 constructor 方法:



清单 9. Example04 的 constructor 方法
public Example04(String title, String filename) {
    // setup overall GUI
    this.filename = filename;
    JFrame frame = new JFrame(title);
    frame.addWindowListener(this); // not shown
    cp = (JPanel)frame.getContentPane();
    cp.setLayout(new BorderLayout());
    cp.add(this, BorderLayout.CENTER);
    frame.pack();
    frame.setSize(525, 450);
    frame.setVisible(true);
    app = this;     // remember for JTC lists
    // setup Transporter and Destination mapping
    trans = new DefaultTransporter();
    jtcs.add(trans);
    dest = new LocalDestination();
    jtcs.add(dest);
    trans.addRequestListener(Data.FAMILY, dest);
    // setup ApplicationMediator
    am = new AM4();
    jtcs.add(am);
    am.addTopListener(this);
    am.addPlacementListener(this);
    am.addRequestListener(trans);
    //indirectly cause a PlacementEvent
    am.init();
    am.refresh(data = new Data()); // reset the data model
}

而处理 PlacementEvent 的应用程序 PlacementListener ,它显示在清单 10 中:



清单 10. 对 Example04 的布局支持
// the cp variable is the JFrame's ContentPane
public void placementEventPerformed(PlacementEvent pe) {
    ViewController vc = (ViewController)
          pe.getViewController();
    final Component c = pe.getComponent();
    if (vc instanceof StatusVC) {
          cp.add(c, BorderLayout.NORTH);
    }
    else {
        switch (pe.getMajor()) {
            case PlacementEvent.ADD :
                cp.add(c, BorderLayout.CENTER);
                validate();
                repaint();
                break;
            case PlacementEvent.REMOVE :
                cp.remove(c);
                break;
        }
    }
}

增加复杂性

尽管 Example04 极其简单,但它还是说明了基于 TCF 的应用程序的每个主要组件和它们之间的相互作用。可能存在复杂得多的应用程序。例如,可以使用多个 ApplicationMediator 和目标位置。 TopListenerTopDestination 可以提供对系统服务的访问。可以部署单一的 PlacementListener 以控制应用程序的屏幕位置。通过组合几个简单的 TCF 应用程序,我们可以轻松创建更复杂的应用程序,如图 11 所示:



图 11. 更复杂的应用程序
更复杂的应用程序 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值