如何利用Adobe AIR创建桌面对话应用程序

桌面程序是Adobe® Integrated Runtime(AIR)的杀手级应用(killer app)。在与其他 工程师 谈论Adobe AIR时,他们经常会提出同一个问题:什么是Adobe AIR的杀手级应用?对这个问题考虑的越深入,我就越觉得,除了工程师或架构师以外,其他人只是想使用某些 应用程序 中的桌面功能。

以对话应用程序为例。当然,可以通过web进行对话。AOL就有基于web的AOL Instant Messenger(AIM),还有其他不计其数的web对话小部件。 但人们仍然喜欢使用AIM、iChat或Trillian 客户 机,因为他们觉得用着方便。用户无须打开浏览器即可启动对话客户机。这些客户机的特点与桌面应用程序一样:拥有本地菜单;在系统托盘中显示;拥有弹出式本地窗口;可与其他本地应用程序合作;用户双击即可打开等等。

用户能够通过Adobe AIR访问跨平台技术中的所有桌面功能,同时还能使用开发web应用程序所用的那些开发工具。这真是太完美了!

本文将演示如何创建基于Adobe AIR的 简单 对话应用程序。它将与利用JSON传输 数据 的Ruby on Rails web应用程序进行通讯。为执行与Ajax有关的任务,它将利用广受欢迎的Prototype.js库与Rails应用程序进行通讯。

Adobe AIR对话应用程序还符合Adobe AIR新安全模型,这可确保从 服务器 上下载且在客户机上执行的JavaScript代码不会直接访问Adobe AIR应用程序编程接口(API)。通过这种方法,就不会通过强大的Adobe AIR接口下载恶意代码,以至于危害用户的计算机。

从图1可以看到,这个Adobe AIR对话应用程序实际上是由不同的HTML页面构成的:root.htm页面,它可访问Adobe AIR API并可利用这些API维护对话消息的本地数据库缓存;ui.htm页面,它可利用Prototype.js库与Rails服务器进行通讯。

 
图1. 处理与Ajax有关的任务以及本地数据库缓存的两个AIR页面
在Rails新安全模型中,root.html页面禁止运行解释JSON代码所必需的eval命令。这可防止JSON代码诱骗客户机运行Adobe AIR方法,以至对客户机器造成损害。

ui.html页面可运行eval命令,以解释来自服务器的JSON响应。但它不能直接访问Adobe AIR接口。在root.html和ui.html这两个页面之间有一个特殊“桥梁”,当它下载了新消息时,ui.html页面就能通知root.html页面。这种更小型、更安全的“桥梁”就是使客户机免受恶意代码侵害的机制。

对整个对话系统有了基本理解后,接下来将着手创建Rails服务器后台应用程序。

创建Rails服务器
要创建Rails应用程序,首先选择Macintosh中很好用的Locomotive应用程序。在Windows平台中,可使用Instant Rails。这些套装应用程序都拥有创建Rails应用程序所需的一切功能。它们的界面很简单,如图2所示,其中将显示机器中的所有应用程序,并可创建新应用程序。

 
图2. Locomotive界面

我要求Locomotive创建名为chat的新应用程序。然后利用chat应用程序中的上下文菜单在主页面中加载浏览器,如图3所示。

 
图3. Rails加载页面
好了,一切就绪。现在要创建数据库和对话模型了。首先创建三个MySQL数据库:chat_development、chat_test和chat_production。然后利用Rails生成器创建名为messages的新模型。
然后编辑生成的001_create_messages.rb文件,并添加要在表格中显示的数据列,如清单1所示。
清单 1. 001_create_messages.rb
class CreateMessages < ActiveRecord::Migration
  def self.up
    create_table :messages do |t|
      t.column :user, :string
      t.column :posted, :datetime
      t.column :message, :string
    end
  end

  def self.down
    drop_table :messages
  end
end
我添加了三列:用户名字段、消息提交日期和时间字段以及消息正文字段。如果想使用其他对话主题扩展本例,只需在此处添加其他表格并在 控制器 中增加新方法。
我还对models目录下的message.rb文件进行了必要的修改,如清单2所示。
清单 2. message.rb
class Message < ActiveRecord::Base
end
瞧瞧,我没进行任何修改。Rails的功能难道不齐全吗?
接着要创建对话控制器,Adobe AIR对话应用程序将利用该控制器发送新消息并选择新消息。清单3就是该控制器的代码。
清单 3. chat_controller.rb
class ChatController < ApplicationController
  scaffold :message

  def post
    msg = Message.new
    msg.user = params[:user]
    msg.message = params[:message]
    msg.posted = DateTime.now
    msg.save
   
    render( :text => { :id => msg.id }.to_json )
  end
 
  def getall
    render( :text => Message.find(:all).to_json )
  end

  def getsince
    msgs = Message.find( :all, :conditions => [ "id > ?", params[:id ] ] )
    render( :text => msgs.to_json )
  end
end
在代码开始部分,调用了经典的Rails scaffolding方法,以在浏览器中显示这些消息。Adobe AIR应用程序开始运行后,就可删除该方法。我还添加了新的post方法,它使用“user”和“message”两个参数,然后将新消息的提交日期设为当日。
getall方法返回一个JSON数组,数组中包含消息表中的所有数据。getsince方法也返回消息的JSON数组,其中只包含在指定id之后创建的消息。这样可提高Adobe AIR应用程序中的选择效率。
为测试该控制器,我在Rails应用程序中打开对话控制器。结果如图4所示。

 
图4. scaffolding列表界面
由于此时的数据库中没有任何消息,因此列表为空。单击New message链接添加消息,这将打开数据输入表单,如图5所示。

 
图5.使用scaffolding创建新消息
在其中输入示例消息并单击Create,这将返回如图6所示的列表。
 
图5.创建消息后的列表
该列表显示我有了一条新记录。很好,这表示数据库连接运行正常。但Adobe AIR应用程序不会使用该接口,因此要测试JSON接口。首先是getall方法,我在浏览器中修改URL,使其指向getall动作,就是这样!JSON返回了该条记录,如图7所示。

 
图 1-7.JSON getall动作
效果真不错。在导出JSON数据时,to_json方法的确非常方便。
接下来测试post方法,Adobe AIR应用程序将利用该方法提交对话消息。只要在URL中输入post动作,并指定“user”和“message”作为URL参数,就可测试该方法。
post动作返回以JSON编码的新建记录的id值,如图8所示。
 
图8. 消息提交动作产生的JSON响应
最后测试getsince动作。它与getall类似,但使用id参数,并且只返回id值大于指定值的消息。这里我将id指定为0,这将返回所有消息。可以看到返回了两条记录,一条是利用scaffolding添加的消息,另一条是刚刚提交的消息。
图9就是getsince动作的结果。
 
图9. getsince动作的JSON响应
以上就是对话示例的服务器部分。认真地讲,Ruby实现这种功能需要多少行代码?也许15行以上。真疯狂!而Rails绝不会让我失望。
接着创建在用户桌面计算机中运行的AIR对话应用程序。
创建Adobe AIR对话应用程序
我设想的对话应用程序其实很简单:在窗口顶端显示对话消息,首先显示用户名,然后显示消息。在窗口底部,我想有个地方可以输入用户名和消息,然后可以单击一个按钮将其添加到对话窗口中。我的要求如图10所示。
 
图10. AIR对话窗口
我将其设计得很简单,以方便读者可以根据需要对其进行修改,并创建自己的对话应用程序。
代码开始部分是XML文件,它告诉应用程序有关Adobe AIR的信息。清单4就是application.xml文件的代码。
清单 4. application.xml
<?xml version="1.0" encoding="utf-8" ?>
<application xmlns="http://ns.adobe.com/air/application/1.0.M5"
  appId="com.example.SimpleChat" version="1.0">
<name>Simple Chat</name>
<description></description>
<copyright></copyright>

<initialWindow>
  <title>Chat Window</title>
  <content>root.html</content>
  <systemChrome>standard</systemChrome>
  <transparent>false</transparent>
  <visible>true</visible>
  <minimizable>true</minimizable>
  <maximizable>true</maximizable>
  <resizable>true</resizable>
  <width>250</width>
  <height>450</height>
  <x>150</x>
  <y>150</y>
</initialWindow>
<icon />
<fileTypes />
</application>
其中只有两个关键元素:在<title>标签中指定的对话应用程序的标题;在<width>和<height>标签中指定的窗口初始大小。其中设置了该页面的用户界面大小,以使其适应250x450的面积。接着是大小调整代码,它用来调整窗口的大小,并修改窗口中所有元素的形状,使其保持清晰。
清单5就是root.html页面的代码,用以处理数据库。
清单 5. root.html
<html>
<head>
<title>Chat Window</title>
<script type="text/javascript" src="AIRAliases.js"></script>
<script type="text/javascript">
var db;

var Exposed = {};

Exposed.trace = function(str) { air.trace(str); }

Exposed.alertNewMessage = function() {
  if ( air.Shell.supportsDockIcon ) {
    air.Shell.shell.icon.bounce();
  }
}

Exposed.addMessage = function( id, user, posted, message ) {
  var del = new air.SQLStatement();
  del.text = "DELETE FROM messages WHERE id = :id;";           
  del.sqlConnection = db;
  del.parameters[':id'] = id;
  del.execute( );

  var add = new air.SQLStatement();
  add.text = "INSERT INTO messages (id, user, posted, message) VALUES ( :id, :user, :posted, :message );";           
  add.sqlConnection = db;
  add.parameters[':id'] = id;
  add.parameters[':user'] = user;
  add.parameters[':posted'] = posted;
  add.parameters[':message'] = message;
  add.execute( );
}

Exposed.getMessages = function() {
  var get = new air.SQLStatement();
  get.text = "SELECT * FROM messages;";
  get.sqlConnection = db;
  get.execute();
  var data = get.getResult().data;
  return ( data == null ) ? [] : data;
}

function doLoad() {
  document.getElementById('UI').contentWindow.parentSandboxBridge = Exposed;
  window.resize = document.getElementById('UI').contentWindow.childSandboxBridge.onResizeEvent;

  db = new air.SQLConnection( true );
  db.open( air.File.applicationResourceDirectory.resolvePath( "chat.db" ), true );

  var cr_exp = new air.SQLStatement();
  cr_exp.text = "CREATE TABLE IF NOT EXISTS messages ( id, user, posted, message );";
  cr_exp.sqlConnection = db;
  cr_exp.execute();

  if ( window.addEventListener != null )
    window.addEventListener( 'resize', function() { window.resize( ); }, false );   
  if ( document.addEventListener != null )
    document.addEventListener( 'resize', function() { window.resize( ); }, false );   

  document.getElementById('UI').contentWindow.childSandboxBridge.startMessages();
}
</script>
</head>
<body οnlοad="doLoad();" style="margin:2px;padding;0px;">
 <iframe id="UI"
     src="http://muttmansion.com/ui.html"
     sandboxRoot="http://muttmansion.com/"
     documentRoot="app-resource:/"
     width="95%"
     height="95%"
     style="border: 0px; margin: 0px; padding: 0px; width: 95%; height:95%">
 </iframe>
</body>
</html>
您也许想知道为何要在对话应用程序中使用本地数据库。道理很简单:为了快速启动。如果旧消息存储在本地,则当每次加载应用程序时就不必回头查找它们。所要做的就是首先显示本地存储的消息,然后要求用户提供排在最后一个id值之后的新消息,这样即可得到所有的新消息。
doLoad()函数创建了该数据库。然后,为提供给ui.html页面的Exposed变量增加两个方法,分别用于添加消息以及获取消息列表。这就是本文开头提到的root.html沙盒和ui.html沙盒之间的“桥梁”。这可确保在ui.html中运行的JSON评估代码不会运行任何Adobe AIR方法。它只可访问消息添加和消息获取方法。
alertNewMessage是root.html的另一个函数,它既有趣又有用。当一条需要用户关注的新消息到达时,alertNewMessage就会使Dock栏中的应用程序图标(适用于Mac OS X机器)上下跳动。这是AIR的一项强大功能。它可以方便地访问诸如此类的操作系统特性,使用户感觉是在使用一种全面集成的应用程序。
root.html的最后一项重要功能是,它可将大小调整消息转发给ui.html,以确保将窗口调整到适当大小。
清单6就是ui.html页面的代码。
清单 6. ui.html
<html>
<head>
<head><title>Chat Window</title>
<script src="prototype.js"></script>
<style>
body { font-family: arial, verdana, sans-serif; font-size:small; margin:0px; padding:0px; }
#chat td { font-size: small; }
</style>
</head>
<body οnlοad="doLoad()">
<div id="chatcontainer" style="height:315px;overflow:auto;">
<table id="chat">
</table>
</div>

<form id="chatmessage" style="margin:0px;">
Username: <input type="text" name="user" value="Jack" size="7" /><br/>
<textarea name="message" id="messagetext" style="width:100%;">
</textarea>
</form>
<button οnclick="addmessage()">Add Message</button>

<script>
var lastid = 0;

function addmessage()
{
  new Ajax.Request( 'http://192.168.1.192:3005/chat/post', {
     method: 'post',
     parameters: $('chatmessage').serialize(),
     onSuccess: function( transport ) {
       $('messagetext').value = '';
     }
  } );
}

function startMessages()
{
    var msgs = parentSandboxBridge.getMessages();
    for( var i = 0; i < msgs.length; i++ )
    {
        var elTR = $('chat').insertRow( -1 );
        var elTD1 = elTR.insertCell( -1 );
        elTD1.appendChild( document.createTextNode( msgs[i].user ) );
        var elTD2 = elTR.insertCell( -1 );
        elTD2.appendChild( document.createTextNode( msgs[i].message ) );
        lastid = parseInt( msgs[i].id );
    }
    window.setTimeout( getMessages, 1000 );
}

function getMessages()
{
  new Ajax.Request( 'http://192.168.1.192:3005/chat/getsince?id='+lastid, {
    onSuccess: function( transport ) {
      var msgs = eval( transport.responseText );
      for( var i = 0; i < msgs.length; i++ )
      {
        var message = msgs[i].attributes.message;
        var user = msgs[i].attributes.user;
        var posted = msgs[i].attributes.posted;
        var id = parseInt( msgs[i].attributes.id );

        parentSandboxBridge.addMessage( id, user, posted, message );

        if ( id > lastid )
        {
          var elTR = $('chat').insertRow( -1 );
          var elTD1 = elTR.insertCell( -1 );
          elTD1.appendChild( document.createTextNode( user ) );
          var elTD2 = elTR.insertCell( -1 );
          elTD2.appendChild( document.createTextNode( message ) );

          lastid = id;

            parentSandboxBridge.alertNewMessage();
        }
      }
      window.setTimeout( getMessages, 1000 );
    }
  } );
}

function onResizeEvent() {
    $('chatcontainer').style.width = (document.width-5)+'px';
    $('chatcontainer').style.height = (document.height-100)+'px';
}

childSandboxBridge = { onResizeEvent: onResizeEvent, startMessages: startMessages };

function doLoad()
{
  onResizeEvent();
}
</script>

</body>
</html>
该页面首先设置onResizeEvent 回调函数。然后利用startMessages()函数从数据库中获取缓存消息的当前列表,并预加载消息表格。startMessages()函数启动一个计时器,它将调用getMessages()函数以从服务器中获取所有新消息。
getMessages()函数使用了Ajax.Request类,这是Prototype.js库中一个很好用的类,以向服务器请求执行getsince动作。当JSON返回时,代码将利用eval函数从JSON数组中获取数据,然后向表中添加新消息。
当用户单击Add Message按钮时将调用addmessage()函数,它利用Ajax.Request向对话控制器中的post动作发送用户名和消息。然后,当getMessages()函数下一次执行选择时,它就会挑选这条新消息,而该函数每秒运行一次。
最后一段代码是onResizeEvent函数,它按照窗口的新宽度和高度调整消息表。
我利用Adobe AIR SDK中的adl函数加载对话应用程序。接着输入“Cool!”并单击Add Message按钮,最后出现如图11所示的窗口。
 
图11.添加消息之后   
要测试大小调整代码,可选中窗口右下角的控件,然后调整形状,使其更宽及更短。 图12就是窗口的最终效果。
 
图12.调整窗口大小之后
大小调整代码修改了表的大小,以大体适应窗口的目前面积。

后续工作
显然,这个对话应用程序远不如iChat或Trillian那样完善。但它是一个不错的应用程序基础,读者可在开发自己的应用程序时将其作为模板。就是说,通过Adobe AIR特性集,读者可以创建适用于任何协议的、功能完善的对话客户机。Adobe AIR可启动本地窗口、使任务栏图标上下跳动、播放声音、管理本地菜单、读取并写入本地文件系统、处理拖放操作等等。它拥有创建高端对话客户机所需的一切功能,且所有功能均可以JavaScript或Flex/Flash的形式提供,而这取决于您的需要。

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值