跳转到主要内容

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

所有提交的信息确保安全。

  • 关闭 [x]

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

所有提交的信息确保安全。

  • 关闭 [x]

基于 SWT Browser 与 Freemarker 的 Java 桌面开发

魏 强, 研究生, 东北大学
魏强,东北大学软件学院硕士研究生,现在主要从事 Eclipse 插件的开发,同时热爱着 Web 技术,尤其对 Java Web 相关技术,更是情有独钟。他的邮箱是:neuswc20063500@gmail.com。

简介: SWT 是 IBM 开发的一个 Java 桌面 UI 技术,著名的 IDE Eclipse 就是基于 SWT 开发的。然而使用 SWT 开发 UI,却并不容易,从各种控件的使用、布局、事件监听等,都需要程序员自己编写代码,而且 SWT 默认的样式较为单一。因此,本文提出了一个新的思路,将 FreeMarker 与 SWT 的 Browser 控件进行结合,使得 UI 的开发不再复杂,又不影响桌面应用的本质。本文使用文章发表、文章分页查看等例子作为展示,读者不仅可以从本文掌握 SWT Browser 的高级技术,还可以使用本文介绍的结构开发 SWT 桌面应用。

发布日期: 2011 年 12 月 29 日
级别: 初级
访问情况 : 6030 次浏览
评论: 


引言

SWT 是 IBM 开发的一个 Java 桌面技术,也就是 SWT 的出现,让我对 Java 在桌面应用上的发挥能力有了较大的改观。然而在我越来越深入了解 SWT 的同时,也感叹 SWT 对比 Web、.Net 开发的不便。小到一个简单的事件监听,大到界面的布局,都得亲历亲为。如果你不熟悉 SWT,那么布局的结果往往出人意料。后来,我注意到了 SWT 的 Browser 控件,它可以显示 HTML 的内容,使用这个控件允许程序员开发自己的“浏览器”。而 HTML 网页的开发,借助成熟的 JavaScript 和 CSS,可以将页面做的丰富多彩,这已经不言而喻了。如果想保持 Java 桌面的本质,又希望让 UI 的开发变得简单,我想到了一个新的思路,可以借助 Browser 显示 HTML,将用户对 HTML 控件的操作,转换为后台 Java 代码的触发事件,而 Java 代码则负责使用 FreeMarker 模板引擎生成新的 HTML 文档让 Browser 显示。如此一来,请求 -> 响应模型就被建立起来,无需 Web 容器。同时 UI 的开发就完全交给了 HTML/CSS/JavaScript,我们需要做的就是开发好后台的 Java 代码,搭建好网页与 Java 的桥梁。本文所要讲述的,正是上面介绍的结合 FreeMarker 与 Browser 开发 Java 桌面应用的方法和思想。


示例展示

为了让读者对本文所讲内容有一个实际的印象,提升对该技术的兴趣,文章将首先展示开发的示例程序,您可以先从附件区域下载工程项目,将其导入 RCP Eclipse 中,RCP Eclipse 可以从 这里下载。导入项目后右击项目,选择运行 Eclipse Application,显示的结果如图 1 所示。这是一个发布文章、查看文章的例子。


图 1. 示例程序主界面
图  1. 示例程序主界面

界面很简单,右半部分显示了两个按钮,分别是 Publish 和 View All,代表发布文章和查看所有的功能,它们属于 SWT 的按钮。默认的界面是发布文章的界面,如果点击 Publish 按钮,也会显示该界面。从发布文章的界面来看,如果让 SWT 去完全开发一个这样的界面,那将是一个浩大的工程,而借助丰富的 Web 组件库比如 FCKEditor,就可以轻松的实现编辑文章的 UI。

我们发布一篇文章,但是我们不输入内容,点击添加,结果如图 2 所示。


图 2. 不输入内容的提示

可以看到,利用 JavaScript 可以很轻松的实现错误提示,这对 Web 开发人员来讲是多么亲切的一件事。

利用 FCKEditor,可以编辑标准的 HTML 样式,我们发表一篇测试文章,如图 3 所示。


图 3. 测试的文章

利用 FCKEditor 的优势,可以让文章表现形式变得丰富多彩!这时,点击添加,就会显示所有的文章列表,如图 4 所示。


图 4. 文章列表

我们只发表了刚才的一篇,因此只有一个结果,我们再点击“查看详细”,结果如图 5 所示。


图 5. 查看详细

看到了吗,和刚才在 FCKEditor 中编辑的一样,内容被显示了出来。接下来,我们再连续发布 11 篇文章,再查看文章列表,如果你当前的页面不在文章列表,可以点击 View All 按钮,结果如图 6 所示。


图 6. 分页的文章列表

可以看到,这时的文章列表,提示总页数共有 2 页,并且显示出“下一页”的超链接,点击下一页,结果如图 7 所示。


图 7. 下一页的文章列表

可以看到,结果是正确的,一共是 12 条记录,每页 10 条,一共两页,图 7 此时出现了“上一页”的超链接。

读者看完这个示例程序,是否对本文介绍的方法有兴趣了呢,如果有,就下载附件自己操作试试吧,然后再看文章接下来的部分。


BrowserFunction 简介

为了让读者更好的理解后面的内容,在正式介绍之前,需要让读者简单了解一下 BrowserFunction。BrowserFunction 是个非常有意思的类,它可以为 Browser 永久绑定一个 JavaScript 方法,它的构造函数是 BrowserFunction(browser:Browser, name:String),其中 browser 代表 Browser 对象,而 name 则代表绑定该浏览器的 JavaScript 方法名,定义了该 BrowserFunction 对象以后,任何在 Browser 显示的网页,都可以访问名为 name 的 JavaScript 方法。BrowserFunction 只有一个接口 public Object function(Object[] arguments),它接收网页传来的参数,执行 Java 代码返回结果对象。注意:虽然参数和结果都是 Object 类型,但是他们不接收复杂对象,只接受基本数据类型,也就是 String、int 这样的。我估计定义成 Object 的原因是未来将要进行扩展吧。


技术简介

言归正传,文章的技术架构图如图 8 所示。


图 8. 技术架构图

在 Java 端,绑定了多个的 BrowserFunction,以他们的 name 作为标识。Browser 对象显示了一个 HTML 页面,当该页面想与 Java 端进行交互时,就发出了事件对象,该事件对象可能是表单提交,也可能是超链接跳转,也可能是 JavaScript 的方法调用,最后该事件触发了某个 BrowserFunction 执行 Java 代码。总体来说,BrowserFunction 可以分为三类:1、直接存储,比如保存到数据库,保存到文件,该类型无具体的反馈,如上图的 BrowserFunction2。2、回调 JavaScript,在调用完 Java 端代码后,需要回调 JavaScript,类似于 Ajax 的效果,可以保持不刷新页面而改变页面的内容,使用的是 Browser 的 exec 方法。3、操作完 Java 代码,需要定义结果对象作为 FreeMarker 的模板根对象,从模板库中选择模板,生成静态 HTML 网页,然后将 Browser 跳转到该网页,作为结果响应,形成类似 HTTP 的请求 -> 响应模型。


Java 端的基础结构

就像 Struts2 的 Action 机制,文章对用户的操作也进行了 ActionSupport 的抽象。我们对 Java 结构进行了设计,结构如下图所示。


图 9.Java 代码结构

BrowserFactory 负责创建绑定好一切的 Browser。Browser 具体绑定哪些 BrowserFunction,则由配置文件 functionConfig.cfg 决定,该文件中每一行代表一个 BrowserFunction,每行用逗号隔开,逗号左边是 BrowserFunction 的 name,右边是类的全路径,该路径的类必须继承于自定义的 ActionSupport。程序通过解析 functionConfig.cfg,使用解析的全路径,再利用反射机制生成 ActionSupport 实例,定义名称为 name 的 BrowserFunction 绑定到 Browser 上,该 BrowserFunction 的 function 方法里实际调用的是 ActionSupport 实例的 execute 方法。另外,还要将 ActionSupport 实例注册到 FunctionRegistry 中(调用 registryAction),这样当请求 -> 响应模型的请求到来时,可以从 FunctionRegistry 具体选择一个 ActionSupport 处理这个请求。BrowserFactory 的 createNewBrowser 函数展示了这个过程,如清单 1 所示。


清单 1. createNewBrowser 源代码
 
 public  static  Browser  createNewBrowser(Composite  parent,  int  style, 
      String  startPage)  { 

    Browser  b  =  new  Browser(parent,  style); 
    InputStream  is  =  BrowserFactory.class 
        .getResourceAsStream("functionConfig.cfg"); 
    // 以下几步是解析 functionConfig.cfg 文件
    BufferedReader  br  =  new  BufferedReader(new  InputStreamReader(is)); 

    String  str  =  null; 
    try  { 
      str  =  br.readLine(); 
      while  (str  !=  null  &&  !"".equals(str.trim()))  { 
        String[]  split  =  str.split(","); 
        if  (split.length  ==  2)  { 
          String  name  =  split[0]; 
          String  classPath  =  split[1]; 
          final  ActionSupport  as  =  (ActionSupport)  Class.forName( 
              classPath).newInstance(); 
          // 注册 action 
          FunctionRegistry.instance().registryAction(name,  as); 
          // 定义 BrowserFunction 绑定到 Browser 对象
          new  BrowserFunction(b,  name)  { 
            public  Object  function(Object[]  arguments)  { 
                            // 实际执行 execute 方法
              Result  execute  =  as.execute(arguments); 
              return  execute.toArray(); 
            } 
          }; 
        } 
        str  =  br.readLine(); 
      } 
    }  catch  (IOException  e)  { 
      e.printStackTrace(); 
    }  catch  (InstantiationException  e)  { 
      e.printStackTrace(); 
    }  catch  (IllegalAccessException  e)  { 
      e.printStackTrace(); 
    }  catch  (ClassNotFoundException  e)  { 
      e.printStackTrace(); 
    } 
    // 添加地址更改的事件监听器,有监听器 ActionSelector 来选择具体的 Action 
    b.addLocationListener(new  ActionSelector(b)); 
    b.setUrl(getPath("pages/publish.html")); 
    return  b; 
  } 

清单 1 中的 ActionSelector 继承于 LocationListener,它负责捕捉浏览器地址变化的事件,同时从 FunctionRegistry 中选择合适的 ActionSupport 对象处理请求。这在后一节的事件监听会详细介绍。另外,由于 BrowserFunction 的 function 方法的返回结果,不能是对象类型,而我们又希望返回多个结果,那么只能将结果 Result 对象转换为数组类型,调用 Result 的 toArray 方法,它的源代码如清单 2 所示。


清单 2. Result 类的 toArray 方法
 
 public  Object[]  toArray()  { 
 return  new  Object[]{getNextPage(),  success,  message,  
 (attachment  ==  null  ?  ""  :  attachment.toString())}; 
 } 

数组的首元素是跳转的目标页面,目的是让 JavaScript 可以自行跳转,success 代表操作成功与否的标志,message 代表 Java 端的反馈信息,attachment 是附件部分,可以携带其他附加信息。


事件监听

本文提到的事件,可以分为三种类型,分别是表单提交、超链接跳转、JavaScript 直接调用 BrowserFunction。其中 JavaScript 调用 BrowserFunction 由于无需刷新页面,可以直接执行,因此无需对其进行特殊监听。然而,Web 上最为常见的表单提交和超链接跳转,都需要刷新页面,而刷新页面时,则无法调用 BrowserFunction。那么如何对这些事件进行监听成为文章的一大难点。

Browser 提供了一个事件监听机制,可以为 Browser 添加地址变更监听器,也就是 LocationListener,这个接口有两个方法 changed(LocationEvent e) 和 changing(LocationEvent e),分别在地址修改后和地址修改过程中触发。当表单提交或者超链接跳转时,都需要改变 Browser 的地址,也就触发了 LocationListener 的函数。与 Struts2 类似,如果想调用 ActionSupport,可以用 file://{Plugin Path}/{BrowserFunction name}.action 访问,只要解析 Browser 的新地址,就可以知道用户调用了哪个 ActionSupport。ActionSelector 继承于 LocationListener,它实现了 changed 方法,负责捕获请求,changed 方法的实现如请单 3 所示。


清单 3. changed 方法的实现
 
 @Override 
  public  void  changed(LocationEvent  e)  { 
    String  location  =  e.location; 
    try  { 
      URL  u  =  new  URL(location); 
      String  protocol  =  u.getProtocol(); 
      // 解析文件协议
      if  (null  !=  protocol  &&  "file".equals(protocol.toLowerCase()))  { 
        String  path  =  u.getPath(); 
        String  query  =  u.getQuery(); 
        int  i1  =  path.lastIndexOf("/"); 
        int  i2  =  path.lastIndexOf(".action"); 
        // 获得 action 名
        if  (i2  >  i1  &&  i1  !=  -1)  { 
          Map<String,  String>  map  =  null; 
          String  name  =  path.substring(i1  +  1,  i2); 
          if  (!"".equals(name))  { 
            if  (query  !=  null) 
            { 
              String[]  split  =  query.split("&"); 
              if  (split  !=  null)  { 
                for  (int  i  =  0;  i  <  split.length;  i++)  { 
                  String[]  split2  =  split[i].split("="); 
                  map  =  new  HashMap<String,  String>(); 
                  if  (split2  !=  null  &&  2  ==  split2.length)  { 
                    map.put(split2[0],  split2[1]); 
                  } 
                } 
              } 
            } 
          } 
          // 获得 action 
          ActionSupport  action  =  FunctionRegistry 
          .instance().getAction(name); 
          if  (action  !=  null) 
          { 
            Result  execute  =  action.execute(map); 
            browser.setUrl(execute.getNextPage()); 
            return; 
          }  else  { 
            // 无此 action 的错误
            browser.setUrl(BrowserFactory.getPath("error.html")); 
          } 
        }  
        // 不是以 .action 结尾就继续执行
      } 
    }  catch  (MalformedURLException  e1)  { 
      
    } 
  } 

执行 ActionSupport,会返回 Result 对象,再调用 Browser 的 setUrl 方法跳转到结果界面。如果不是以 .action 结尾,则不加干预,继续执行。


表单提交

表单提交,是一个模拟的过程,由于 LocationListener 检测到的新地址中,无法获得参数列表(?a=b&b=c),也就无法获得表单的数据,因此选择了一个折中的过程。在例子中,文章的发布,用到了表单提交,查看 publish.html 的源代码,其中使用 JQuery 对表单进行了拦截,如清单 4 所示。


清单 4. 表单提交的拦截
 
 $(function(){ 
    $(formID).submit(function(){ 
            if  ($("#title").val()  ==  "") 
            { 
              alert("标题不能为空!"); 
              return  false; 
            }  else  if  ($("#content").val()  ==  "") 
            { 
              alert("内容不能为空!"); 
              return  false; 
            } 
            var  r  =  publish($("#title").val(),  $("#content").val()); 
            window.location.href  =  r[0]; 
              // 阻止表单提交
            return  false; 
    }); 
  }); 

可以看到,发表文章的表单事件被拦截 JQuery,return false 代表不提交(因为直接提交无法获得文章内容),表单的提交过程则模拟为先调用 publish 方法,然后再使用 window.location.href,将页面地址跳转到目标地址。Publish 方法是绑定到 Browser 上的一个 BrowserFunction。它的处理代码如清单 5 所示。


清单 5. publish 方法对应的 PublishAction 代码
 
 public  Result  execute(Object[]  params)  { 
  Result  r  =  new  Result(); 
  if  (params  !=  null  &&  params.length  ==  2) 
  { 
      Article  a  =  new  Article(); 
      a.setTitle((String)  params[0]); 
      a.setContent((String)  params[1]); 
      ArticleList.instance().add(a); 
      // 发布成功,跳到 viewAll.action,显示文章的列表
      r.setNextPage("viewAll.action"); 
  }  else  { 
      // 内容错误,跳到错误页面
      r.setNextPage("pages/publishError.html"); 
  } 
  return  r; 
 } 

Result 类存放的是调用结果,包含着跳转的目标界面。


生成页面与跳转

从上面我们可以看到,当发布完文章后,要跳转到 viewAll.action,它是以 .action 为后缀,因此处理这个请求的是 viewAll 的 ActionSupport,也就是示例代码中 ViewAllAction,它的 execute 方法代码如清单 6 所示。


清单 6. 查看文章列表代码
 
 Result  r  =  new  Result(); 
 int  from  =  0; 
 int  length  =  10; 
 if  (params  !=  null) 
 { 
  int  pageNum  =  Integer.parseInt((String)  params[0]); 
  from  =  (pageNum-1)*10; 
  length  =  10; 
 } 
    
 int  num  =  ArticleList.instance().getPageNum(length); 
 int  current  =  ArticleList.instance().getCurrentPage(from,  length); 
 List<Article>  subList  =  ArticleList.instance().subList(from,  length); 

 PageingModel  pm  =  new  PageingModel(subList,  num,  current); 
    
 String  resultPageName  =  "pages/showList_"  +  from  +  "_"  +  length  +  ".html"; 
 // 生成页面  
 Result  generatePage  =  generatePage(pm,  "showList.ftl",  resultPageName); 
 if  (generatePage.isSuccess()) 
 { 
  r.setNextPage(resultPageName); 
 }  else  { 
  r.setNextPage("pages/actionError.html"); 
 } 
    
 return  r; 

从上面的代码可以看出,该 action 可以接收两个参数,分别是 from 和 pageNum。这两个参数显然是为了分页显示。如果两个参数都没有,则默认显示第一页。获得文章列表后,就需要调用 FreeMarker 引擎,依据一定的命名规范生成 html 文件。上面的清单使用到了 showList.ftl 这个模板,调用的是 generatePage 这个方法,该方法是 ActionSupport 类提供的 final 方法,无法重写,是提供给子类的公用方法,该方法实现如清单 7 所示。


清单 7. generatePage 代码清单
 
 public  final  Result  generatePage(Object  model,  String  templateName, 
      String  resultPageName)  { 
  Result  r  =  new  Result(); 
  try  { 
    Template  temp  =  FunctionRegistry.instance().getConfiguration() 
          .getTemplate(templateName); 
    Writer  out  =  new  OutputStreamWriter(new  FileOutputStream(new  File( 
          BrowserFactory.BASEPATH  +  resultPageName))); 
    temp.process(model,  out); 
    out.flush(); 
  }  catch  (FileNotFoundException  e)  { 
    e.printStackTrace(); 
  }  catch  (IOException  e)  { 
    e.printStackTrace(); 
  }  catch  (TemplateException  e)  { 
    e.printStackTrace(); 
  } 
  return  r; 
 } 

对于文章列表中的每一项,模板的是这么定义的:<td>${ar.title}</td><td><a class="redirect" articleId="${ar.id}"> 查看详细 </a></td>,使用 articleId 属性存储文章的 ID,当用户点击“查看详细”超链接时,浏览器捕获这个请求,使用清单 8 的代码。


清单 8. 超链接跳转
 
 $(".redirect").live("click",  function(){ 
  var  aid  =  $(this).attr("articleId"); 
  var  r  =  viewOne(aid); 
  window.location.href  =  r[0]; 
    }); 
 $(".nextPage").live("click",  function(){ 
  var  pn  =  $(this).attr("pageNum"); 
  var  r  =  viewAll(pn); 
  window.location.href  =  r[0]; 
 }); 
 $(".prePage").live("click",  function(){ 
 var  pn  =  $(this).attr("pageNum"); 
  var  r  =  viewAll(pn); 
  window.location.href  =  r[0]; 
 }); 

viewAll 这个 BrowserFunction 已经在上文提到,在点击“上一页”“下一页”时依然会被调用。而点击“查看详细”时,先调用了 viewOne 方法,传递的参数是文章的 ID,后台 Java 代码生成显示该文章的 HTML 页面,然后页面端负责跳转到该界面。viewOne 对应类是 ViewOneAction,它的 execute 代码如清单 9 所示。


清单 9. viewOne 的清单
 
 public  Result  execute(Object[]  params)  { 
  int  id  =  Integer.parseInt((String)  params[0]); 
  Result  r  =  new  Result(); 
  Article  article  =  ArticleList.instance().getArticles().get(id); 
  String  resultPageName  =  "pages/oneArticle_"  +  id  +  ".html"; 
  Result  generatePage  =  generatePage(article,  "showOne.ftl",  resultPageName); 
  if  (generatePage.isSuccess()) 
  { 
    r.setNextPage(resultPageName); 
  }  else  { 
    r.setNextPage("pages/actionError.html"); 
  } 
  return  r; 
 } 

可以看到,根据文章的 ID 生成页面,然后将页面的地址返回给浏览器端,浏览器自行跳转。

总体来说,文章介绍方法的重点是 BrowserFunction 和 LocationListener 的配合使用,LocationListener 负责页面跳转的坚挺,BrowserFunction 负责网页与 Java 的直接交互。


总结

本文结合了 FreeMarker 与 SWT Browser 控件,将 Java 桌面开发的方式进行了重新定义,将复杂的 UI 开发交给成熟的 HTML/CSS/JavaScript,Java 程序员要做的只是将 Java 与 HTML 进行事件绑定,同时对网页表单、超链接的请求进行拦截,这些方式在文章中都已详细解释。本文使用文章发布、查看、分页显示的例子,展示了使用 FreeMarker 与 SWT Browser 控件结合开发 Java 桌面应用的优势,读者如有兴趣,可以继续完善,最终使它变成可用的应用开发框架。由于笔者水平有限,如果文章中存在错误,欢迎读者联系我批评指正,我的邮箱是:neuswc20063500@gmail.com


参考资料

关于作者

魏强,东北大学软件学院硕士研究生,现在主要从事 Eclipse 插件的开发,同时热爱着 Web 技术,尤其对 Java Web 相关技术,更是情有独钟。他的邮箱是:neuswc20063500@gmail.com。

关于报告滥用的帮助

报告滥用

谢谢! 此内容已经标识给管理员注意。


关于报告滥用的帮助

报告滥用

报告滥用提交失败。 请稍后重试。


developerWorks:登录


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 使用条款

 


当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

请选择您的昵称:

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

(长度在 3 至 31 个字符之间)


单击提交则表示您同意developerWorks 的条款和条件。 使用条款.

 


为本文评分

评论

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=783404
ArticleTitle=基于 SWT Browser 与 Freemarker 的 Java 桌面开发
publish-date=12292011

标签

Help
使用 搜索 文本框在 My developerWorks 中查找包含该标签的所有内容。

使用 滑动条 调节标签的数量。

热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。

我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。