Python Pyjamas

简介,第 1 部分: 协同使用 GWT 和 Python 的优势
发布者:豆豆网  日期: 2010-08-26 00:00:00 浏览次数:0 (共有_条评论) 查看评论 | 我要评论

  简介

  Google 的 Web Toolkit (GWT) 让我们能够完全用 Java™ 代码开发具有 Ajax 功能的 Rich Internet Application (RIA)。可以使用丰富的 Java 工具集(IDE、重构、代码补全、调试器等等)开发出可以部署在所有主流 Web 浏览器中的应用程序。在 GWT 的帮助下,可以编写出在浏览器中运行但是表现与桌面应用程序相似的应用程序。Pyjamas 是 GWT 的一个变体,是用于用 Python 开发 Ajax 应用程序的工具和框架。

  Pyjamas 包含一个单独的 Python 到 JavaScript 编译器以及 Ajax 框架和部件集。可以使用这些组件编写复杂的应用程序,而不需要编写任何 JavaScript 代码。

  本文讨论 Pyjamas 的背景知识、基本原理、相关工具和优点,演示如何创建一个示例应用程序,这个程序存储基本的联系信息(姓名、电子邮件地址、电话号码)。还可以 下载 这个示例应用程序的代码。

  本系列的第 2 部分将解释如何构建定制的 Pyjamas 组件。

  背景知识

  Python 是一种流行的编程语言,首先出现在 JVM 上 (Jython),后来移植到了 .Net (IronPython)。Python 语法已经可以生成与 C 程序兼容的机器代码 (Cython)。在 Google 宣布采用 Java 语言之后,Python 成为第一批可以转换为 JavaScript 以跨浏览器的方式运行的语言之一。

  强大的 XUL

  在 2009 年,Pyjamas-Desktop(现在是 Pyjamas 的组成部分)还转换为使用 XUL。XUL 和 Firefox 的关系或多或少相当于 WebKit 和 Safari 的关系。可以在 XUL 上运行 Pyjamas。据说,在 Hulahop 项目(来自 OLPC Sugar 团队)和 python-xpcom 开发人员的帮助下,把 Pyjamas 移植到 XUL 只花了两天时间。

  在不久之前,用 Ajax 编写整个应用程序的希望看起来还很渺茫。但是现在有了 GWT,我们可以完全用 Java 代码开发具有 Ajax 功能的 RIA。GWT 让我们能够编写出在浏览器中运行但是表现与桌面应用程序相似的应用程序。

  与之相反,Adobe AIR 和 Silverlight 让 Web 应用程序可以在桌面上运行。Android、Adobe AIR、Google Chrome、Safari 和 iPhone 都使用 WebKit 进行显示。GWT 的问题是,不允许编写作为桌面应用程序运行的应用程序(尽管用于显示的 GWT 开发工具集基于 WebKit)。

  Pyjamas 有一个与 GWT 相似的 Python 到 JavaScript 编译器,还包含一套 Ajax 部件,它们的 API 与对应的 GWT 部件相同。(实际上可以参考 GWT 文档开发 Pyjamas 应用程序。)Python 的语法非常简洁、强大;例如 GWT 1.2 有 80,000 行代码,而 Pyjamas 完成相同的任务只用了 8,000 行代码。

  Pyjamas 概述

  XUL 和 WebKit Python 绑定的问题

  MSHTML 似乎是最好的,而且 WebKit 和 XUL 的底层 Python 绑定是变化的。如果 WebKit 团队不将 Python 绑定移植到 WebKit GTK,就会造成数不清的麻烦。

  有时候,WebKit 和 xulrunner Python 绑定受到破坏,或至少被忽视。

  请记住,Pyjamas-Desktop 并非只与 WebKit 捆绑在一起。Pyjamas 为 Python 开发人员提供 WebKit、XUL 和 MSHTML。因此,Pyjamas-Desktop 可以使用这三种浏览器引擎中的任意一种。由于这个原因,Pyjamas 成了既跨浏览器又跨平台的 GUI 部件集。

  WebKit、XUL 和同类技术把现代技术带入了桌面应用程序。Pyjamas 为 Python 开发人员提供 WebKit。由于可以使用 Webkit,Pyjamas 成了既跨浏览器又跨平台的 GUI 部件集。可以开发出在运行 WebKit 和 XUL 的任何地方运行的部件。在能够运行 GWT 应用程序的任何地方,基于 Pyjamas API 的应用程序都可以运行。另外,Pyjamas 允许编写在 WebKit 和 XUL 上构建的桌面应用程序。这比在 Qt 或 GTK 上构建应用程序更好,因为 WebKit 支持 CSS,可以在许多其他地方可靠地显示(iPhone、Safari、Android 等等)。但是,在 XUL 和 WebKit 的 Python 绑定方面有点儿问题(见边栏)。

  与 GWT 一样,Pyjamas 是一个 GUI 组件框架。如果您使用过 Swing 或 GWT,应该觉得熟悉 Pyjamas 开发。与大多数 GUI 框架一样,Pyjamas 是事件驱动的。

  用 Pyjamas 创建容器,然后在容器中添加部件。部件可以是标签、文本框、按钮等等。按钮等部件有事件处理器,可以监听来自按钮的单击事件。

  用 Pyjamas 进行开发很容易,因为可以使用平时使用的 Python 调试工具。这些工具包括单元测试、打印语句和 Python 调试器(命令行调试器 pdb)。甚至可以使用 Eclipse 的 Python 支持进行调试。请记住,可以编写作为原生 Python 应用程序运行的 Pyjamas 应用程序。不一定要把 Pyjamas 应用程序转换为 JavaScript。可以像使用任何其他 Python GUI 工具集一样使用 Pyjamas。

  本文中示例应用程序的 GUI 的第一版是用从命令行运行的 Python 开发的。它最初甚至没有部署到 Web 上,而是作为桌面应用程序运行。这对于开发 RIA 应用程序很有好处,因为能够方便地调试程序。

  当准备好把应用程序部署到 Web 上时,需要注意程序包含的库。常常从在浏览器中运行的 Pyjamas 应用程序使用 JavaScript Object Notation (JSON)-RPC 服务。

  先决条件

  要想构建本文中的示例应用程序,需要下载并安装 Pyjamas。这个任务并不很简单。我曾经尝试在 Ubuntu 上安装 Pyjamas,但是失败了,只好放弃,改为在 Debian 上安装它。(据说 Pyjamas 也可以在 Windows® 上顺利地运行。)在 Debian 上安装的版本没什么问题。安装过程可能会有变动,所以您应该按照 Pyjamas 站点上针对您的环境的最新说明操作。

  为了构建服务层,使用了 MySQL、Apache、mod_python 和 Python JSON-RPC。

  构建示例应用程序

  示例联系人管理应用程序存储基本的联系信息,比如姓名、电子邮件地址和电话号码。首先创建一个简单的 Create, Read, Update, and Delete (CRUD) 应用程序,然后添加真正的存储。可以在一个简单的 Python 脚本中实现整个程序,使用内存中的 “数据库”。这个示例使用一个服务层,然后把这个内存中的服务层版本替换为由 JSON 支持的服务层版本,这个版本使用 MySQL 把联系人信息存储在关系数据库中。

  分而治之

  我喜欢让整个 GUI 与一个模拟层通信,这样可以把 GUI 开发与持久化和业务逻辑层分隔开。按照这种方式,我可以专心开发 GUI 逻辑,而不需要为调试远程 RPC 等问题操心。

  要想了解如何编写模拟服务,必须了解运行时应用程序的运行方式。程序异步地调用 JSON 服务。当把 Pyjamas 应用程序编译为 RIA 应用程序(HTML 和 JavaScript 代码)时,Ajax 调用会异步地返回结果。因此,在构建模拟服务时,要模拟 Ajax 库异步地回调 GUI。清单 1 说明 ContactService 通过调用 callback 方法回调 GUI。这模拟以后要添加的 JSON 异步行为。

清单 1. Contact Service

class Contact: 
  def init(self, name="", email="", phone=""): 
    self.name = name 
    self.email = email 
    self.phone = phone 
 
class ContactService: 
  def init(self, callback): 
    self.callback = callback 
    self.contacts = [] 
 
  def addContact(self, contact): 
    self.contacts.append(contact) 
    self.callback.service_eventAddContactSuccessful() 
   
  def updateContact(self, contact): 
    self.callback.service_eventUpdateContactSuccessful() 
 
  def removeContact(self, contact): 
    self.contacts.remove(contact) 
    self.callback.service_eventRemoveContactSuccessful() 
     
  def listContacts(self): 
    self.callback.service_eventListRetrievedFromService(self.contacts) 

  Contact 类代表一个联系人(姓名、电子邮件地址、电话号码)。ContactService 只有内存中的联系人列表(没有存储到磁盘)。这个简单的类让我们可以开发 GUI;在开发显示逻辑之后,只需经过简单的修改,就可以用真正的 JSON 服务测试 GUI。

  ContactService 使用名称以 service_eventXXX 开头的方法将服务事件通知给 ContactListGUI(在清单 2 中定义)。

  ContactListGUI 相当简单,只有 125 行代码,它管理 9 个 GUI 部件。它还与 ContactService 协作管理 CRUD 列表,见清单 2。

清单 2. ContactListGUI

import pyjd # this get stripped out for JavaScript translation 
from pyjamas.ui.RootPanel import RootPanel 
from pyjamas.ui.Button import Button 
from pyjamas.ui.Label import Label 
from pyjamas import Window 
 
from pyjamas.ui.Grid import Grid 
from pyjamas.ui.Hyperlink import Hyperlink 
from pyjamas.ui.TextBox import TextBox 
 
# Constants 
CONTACT_LISTING_ROOT_PANEL = "contactListing" 
CONTACT_FORM_ROOT_PANEL = "contactForm" 
CONTACT_STATUS_ROOT_PANEL = "contactStatus" 
CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar" 
EDIT_LINK = 3 
REMOVE_LINK = 4 
 
#Service code removed 
 
class ContactListGUI: 
 
  def init(self): 
    self.contactService = ContactService(self) 
    self.currentContact = Contact("Rick", "moc.liamg|rewothgihr#moc.liamg|rewothgihr", "555-555-5555") 
    self.addButton = Button("Add contact", self.gui_eventAddButtonClicked) 
    self.addNewButton = Button("Add new contact", self.gui_eventAddNewButtonClicked) 
    self.updateButton = Button("Update contact", self.gui_eventUpdateButtonClicked) 
 
    self.nameField = TextBox() 
    self.emailField = TextBox() 
    self.phoneField = TextBox() 
    self.status = Label() 
    self.contactGrid = Grid(2,5) 
    self.contactGrid.addTableListener(self) 
 
    self.buildForm() 
    self.placeWidgets() 
    self.contactService.listContacts() 
 
   
  def onCellClicked(self, sender, row, cell): 
    print "sender=%s row=%s cell=%s" % (sender, row, cell) 
    self.gui_eventContactGridClicked(row, cell) 
 
  def onClick(self, sender): 
    if sender == self.addButton: 
      self.gui_eventAddButtonClicked() 
    elif sender == self.addNewButton: 
      self.gui_eventAddNewButtonClicked() 
    elif sender == self.updateButton: 
      self.gui_eventUpdateButtonClicked() 
         
  def buildForm(self): 
    formGrid = Grid(4,3) 
    formGrid.setVisible(False) 
     
    formGrid.setWidget(0, 0, Label("Name")) 
    formGrid.setWidget(0, 1, self.nameField); 
 
    formGrid.setWidget(1, 0, Label("email")) 
    formGrid.setWidget(1, 1, self.emailField) 
     
    formGrid.setWidget(2, 0, Label("phone")) 
    formGrid.setWidget(2, 1, self.phoneField) 
     
    formGrid.setWidget(3, 0, self.updateButton) 
    formGrid.setWidget(3, 1, self.addButton) 
 
    self.formGrid = formGrid 
     
  def placeWidgets(self): 
    RootPanel(CONTACT_LISTING_ROOT_PANEL).add(self.contactGrid) 
    RootPanel(CONTACT_FORM_ROOT_PANEL).add(self.formGrid) 
    RootPanel(CONTACT_STATUS_ROOT_PANEL).add(self.status) 
    RootPanel(CONTACT_TOOL_BAR_ROOT_PANEL).add(self.addNewButton) 
 
  def loadForm(self, contact): 
    self.formGrid.setVisible(True) 
    self.currentContact = contact 
    self.emailField.setText(contact.email) 
    self.phoneField.setText(contact.phone) 
    self.nameField.setText(contact.name) 
   
  def copyFieldDateToContact(self): 
    self.currentContact.email = self.emailField.getText() 
    self.currentContact.name = self.nameField.getText() 
    self.currentContact.phone = self.phoneField.getText() 

  ContactListGUI init 方法通过调用 buildForm 方法创建一个新的表单,并在其中添加用于编辑联系人数据的字段。然后,init 方法调用 placeWidgets 方法,这个方法把 contactGrid、formGrid、status 和 addNewButton 部件放在驻留这个 GUI 应用程序的 HTML 页面中定义的位置,见清单 3。

  图 1 显示联系人管理应用程序中使用的部件的概况。

图 1. 联系人管理 GUI 中的部件

  查看原图(大图)

清单 3. ContactListGUI GUI 事件处理器

<html> 
  <head> 
   <meta name="pygwt:module" content="Contacts"> 
   <link rel='stylesheet' href='Contacts.css'> 
   <title>Contacts</title> 
  </head> 
  <body bgcolor="white"> 
 
   <script language="javascript" src="bootstrap.js"></script> 
 
   <h1>Contact List Example</h1> 
 
   <table align="center"> 
   <tr> 
    <td id="contactStatus"></td> 
   </tr> 
   <tr> 
    <td id="contactToolBar"></td> 
   </tr> 
   <tr> 
    <td id="contactForm"></td> 
   </tr> 
   <tr> 
    <td id="contactListing"></td> 
   </tr> 
   </table> 
  </body> 
</html> 

  常量(比如 CONTACT_LISTING_ROOT_PANEL="contactListing")对应于 HTML 页面中定义的元素的 ID(比如 id="contactListing")。这让页面设计者可以控制应用程序部件的布局。

  基本的应用程序现在构建好了。下一节讨论几个常见的使用场景。

  在装载页面时显示列表

  当首次装载示例应用程序的页面时,它调用 ContactListEntryPoint 的 init 方法。init 方法调用 ContactServiceDelegate 的 listContacts 方法,该方法又异步地调用服务的 listContact 方法。模拟的 ContactService 的 listContact 方法调用服务事件处理器方法 service_eventListRetrievedFromService,如清单 4 所示。

清单 4. ContactListGUI:调用 listContact 事件处理器

class ContactListGUI: 
  … 
  def service_eventListRetrievedFromService(self, results): 
    self.status.setText("Retrieved contact list") 
    self.contacts = results; 
    self.contactGrid.clear(); 
    self.contactGrid.resizeRows(len(self.contacts)) 
    row = 0 
     
    for contact in results: 
      self.contactGrid.setWidget(row, 0, Label(contact.name)) 
      self.contactGrid.setWidget(row, 1, Label (contact.phone)) 
      self.contactGrid.setWidget(row, 2, Label (contact.email)) 
      self.contactGrid.setWidget(row, EDIT_LINK, Hyperlink("Edit", None)) 
      self.contactGrid.setWidget(row, REMOVE_LINK, Hyperlink("Remove", None)) 
      row += 1 

  service_eventListRetrievedFromService 事件处理器方法存储服务器发送的联系人列表。然后:

  清空显示联系人列表的 contactGrid。

  调整行的数量,与服务器返回的联系人列表的规模匹配。

  循环处理联系人列表,把每个联系人的姓名、电话号码和电子邮件数据放进每行的前三列。

  为每个联系人提供 Edit 链接和 Remove 链接,让用户可以轻松地删除和编辑联系人。

  编辑现有的联系人

  当用户单击联系人列表中的 Edit 链接时,调用 gui_eventContactGridClicked,见清单 5。

清单 5. ContactListGUI 的 gui_eventContactGridClicked 事件处理器方法

class ContactListGUI: 
 
  … 
  def gui_eventContactGridClicked(self, row, col): 
     contact = self.contacts[row] 
     self.status.setText("Name was " + contact.name + " clicked ") 
     if col==EDIT_LINK: 
       self.addNewButton.setVisible(False) 
       self.updateButton.setVisible(True) 
       self.addButton.setVisible(False) 
       self.loadForm(contact) 
     elif (col==REMOVE_LINK): 
       self.contactService.removeContact(contact) 
 
  … 
  def loadForm(self, contact): 
    self.formGrid.setVisible(True) 
    self.currentContact = contact 
    self.emailField.setText(contact.email) 
    self.phoneField.setText(contact.phone) 
    self.nameField.setText(contact.name) 

  gui_eventContactGridClicked 方法检查用户单击的是哪一列,从而判断单击的是 Edit 链接还是 Remove 链接。然后,它隐藏 addNewButton 和 addButton,让 updateButton 可见。updateButton 显示在 formGrid 中,让用户能够把更新信息发送回 ContactService。然后,gui_eventContactGridClicked 调用 loadForm(见 清单 5),它:

  把 formGrid 设置为可见。

  设置正在编辑的联系人。

  把联系人属性复制到 emailField、phoneField 和 nameField 部件中。

  当用户单击 Update 按钮时,调用 gui_eventUpdateButtonClicked 事件处理器方法,见清单 6。这个方法:

  让 addNewButton 可见,让用户可以添加新的联系人。

  隐藏 formGrid。

  调用 copyFieldDateToContact,从而把 emailField、phoneField 和 nameField 部件中的文本复制回 currentContact 的属性。

  调用 ContactServiceDelegate 的 updateContact 方法,把更新的联系人信息传递回服务。

清单 6. ContactListGUI 的 gui_eventUpdateButtonClicked 事件处理器方法

class ContactListGUI: 
 
  … 
 
  def gui_eventUpdateButtonClicked(self, sender): 
    self.addNewButton.setVisible(True) 
    self.formGrid.setVisible(False) 
    self.copyFieldDateToContact() 
    self.contactService.updateContact(self.currentContact) 
 
  def copyFieldDateToContact(self): 
    self.currentContact.email = self.emailField.getText() 
    self.currentContact.name = self.nameField.getText() 
    self.currentContact.phone = self.phoneField.getText()