zlfing 2011-03-01
本系列文章探究了如何使用 Python 来为 GNOME 桌面、screenlet 框架和 Nautilus 创建脚本,进而提供高生产率环境。桌面上的脚本启用拖放功能,可快速访问您经常使用的信息和服务。本期我们将了解如何使用 screenlet 小部件工具包构建桌面应用程序。
为 Linux 桌面开发应用程序通常需要一些类型的图形用户界面(Graphical User Interface,GUI)框架作为构建基础。选项包括针对 GNOME 桌面的 GTK+,和针对 K 桌面环境(K Desktop Environment,KDE)的 Qt。这两个平台提供了开发人员构建 GUI 应用程序所需的一切,包括库和布局工具以便创建用户看到的窗口。本文向您展示如何基于 screenlet 小部件工具包构建桌面生产率应用程序。
一些现有的应用程序将归为桌面生产率类别,包括 GNOME Do 和 Tomboy。这些应用程序通常允许用户直接从桌面与它们进行交互,方法为通过特定的键组合或从另一个应用程序(如 Mozilla Firefox)进行拖放。Tomboy 用作桌面笔记工具,支持从其他窗口拖放文本。
Screenlet 入门
您需要安装一些程序以便开始开发 screenlet。首先,使用 Ubuntu 软件中心或命令行安装 screenlets 包。在 Ubuntu 软件中心内,在 Search框中键入 screenlets。您应该看到主要程序包和文档的独立安装包两个选项。
Python 和 Ubuntu您可使用 Python 对 screenlet 编程。Ubuntu 10.04 的基本安装已包含了 Python 2.6,因为许多实用程序都依赖它。您可能需要其他库,具体取决于您的应用程序要求。对于本文,我在 Ubuntu 10.04 上安装并测试了一切。下一步,从 screenlets.org 站点下载测试 screenlet 源代码。测试 screenlet 位于 src/share/screenlets/Test 文件夹并使用 Cairo 和 GTK,这些也是需要您安装的。测试程序的整个源代码位于 TestScreenlet.py 文件中。在您最喜爱的编辑器中打开此文件来查看 screenlet 的基础结构。
Python 高度面向对象,因此使用 class关键字来定义对象。在本示例中,该类被命名为 TestScreenlet并具有一些已定义的方法。在 TestScreenlet.py 中,请注意下面代码中的第 42 行:
def __init__(self, **keyword_args):
Python 使用前后双下划线(__)符号,通过预定义行为识别系统函数。在本例中,__init__函数针对类的构造函数的所有的意图和目的,且包含将要在创建对象的新实例时执行的任何数量的初始化步骤。按照惯例,每个类方法的第一个参数都是对该类的当前实例的引用,并命名为 self。通过此行为,可以方便地使用 self来引用它所在的实例的方法和属性:
self.theme_name = "default"
Screenlet 框架定义了一些命名惯例和标准,在 screenlets.org 中的开发人员页面有概述。还有 screenlet 包的源代码以及应用程序编程接口(Application Programming Interface,API)文档的链接。查看代码还使您可以深入了解每一个函数可以通过调用参数做什么以及返回什么。
编写简单的 screenlet
Screenlet 的基本组成部分包括图标文件、源代码文件和主题文件夹。主题文件夹包含不同主题的附加文件夹。您将在 screenlets.org 上发现样例模板和帮助您入门所需的文件和文件夹。
对于第一个示例来说,使用已提供的模板来创建基本的 “Hello World” 应用程序。此基本应用程序的代码如清单 1 所示。
清单 1. Hello World screenlet 的 Python 代码
#!/usr/bin/env python import screenlets class HelloWorldScreenlet(screenlets.Screenlet): __name__ = 'HelloWorld' __version__ = '0.1' __author__ = 'John Doe' __desc__ = 'Simple Hello World Screenlet' def __init__(self, **kwargs): # Customize the width and height. screenlets.Screenlet.__init__(self, width=180, height=50, **kwargs) def on_draw(self, ctx): # Change the color to white and fill the screenlet. ctx.set_source_rgb(255, 255, 255) self.draw_rectangle(ctx, 0, 0, self.width, self.height) # Change the color to black and write the message. ctx.set_source_rgb(0, 0, 0) text = 'Hello World!' self.draw_text(ctx, text, 10, 10, "Sans 9" , 20, self.width) if __name__ == "__main__": import screenlets.session screenlets.session.create_session(HelloWorldScreenlet)
每一个应用程序都必须导入 screenlet 框架并创建新的会话。还有一些其他的最低要求,包括任何初始化步骤以及基本绘图函数,以便在屏幕上呈现小部件。TestScreenlet.py 示例具有用来初始化对象的 __init__方法。在本例中,您将看到一行,包含对 screenlet 的 __init__方法的调用,它设置要为此应用程序创建的窗口的初始宽度和高度。
此应用程序惟一需要的其他函数是 on_draw方法。此例程将框的背景颜色设置为白色,并使用先前定义的维度绘制矩形。它将文本颜色设置为黑色,将源文本设置为 “Hello World!”,然后再绘制该文本。图 1 显示了在运行此 screenlet 时您应该看到什么。在文本的后面部分,您在这些简单块上创建更有用的应用程序时,此基本结构一直存在。
图 1. 基本 screenlet 结构
在更复杂的 screenlet 中重用代码
一个有关编写 screenlet 的好处就是能够重用来自其他应用程序的代码。通过基于 Python 语言的广泛开源项目,代码重用开拓了无限的可能性。虽然每一个 screenlet 都有相同的基本结构,但是定义了的更多方法来处理不同的行为。清单 2 显示了名为TimeTrackerScreenlet的样例应用程序。
清单 2. Time Tracker screenlet 的 Python 代码
#!/usr/bin/env python import screenlets import cairo import datetime class TimeTrackerScreenlet(screenlets.Screenlet): __name__ = 'TimeTrackerScreenlet' __version__ = '0.1' __author__ = 'John Doe' __desc__ = 'A basic time tracker screenlet.' theme_dir = 'themes/default' image = 'start.png' def __init__(self, **keyword_args): screenlets.Screenlet.__init__(self, width=250, height=50, **keyword_args) self.add_default_menuitems() self.y = 25 self.theme_name = 'default' self.on = False self.started = None def on_draw(self, ctx): self.draw_scaled_image(ctx, 0, 0, self.theme_dir + '/' + self.image, self.width, self.height) def on_mouse_down(self, event): if self.on: self.started = datetime.datetime.now() self.image = 'stop.png' self.on = False else: if self.started: length = datetime.datetime.now() - self.started screenlets.show_message(None, '%s seconds' % length.seconds, 'Time') self.started = None self.image = 'start.png' self.on = True def on_draw_shape(self, ctx): self.on_draw(ctx) ctx.rectangle(0, 0, self.width, self.height) ctx.fill() if __name__ == "__main__": import screenlets.session screenlets.session.create_session(TimeTrackerScreenlet)
此示例引入了几个在您开始构建任何有用的程序以前需要了解的概念。所有的 screenlet 应用程序都具有响应特定用户操作或事件(如鼠标单击或拖放操作)的能力。在此示例中,鼠标按下事件用作更改图标状态的触发器。在 screenlet 运行时,显示 start.png 图像。单击该图像将其变更为 stop.png 图像并在 self.started中记录开始时间。单击停止图像将该图像变更回 start.png 并显示从单击第一个图像开始所经过的时间量。
响应事件是另一个关键功能,使得构建任何数量的不同应用程序成为可能。虽然此示例仅使用 mouse_down事件,但是您可以对由 screenlet 框架或系统事件(如计时器)生成的其他事件使用相同的方法。此处引入的第二个概念是持久状态。因为您的应用程序是持续运行的,等待事件来触发一些操作,就能够在内存中保持对项目的跟踪,如单击开始图像的时间。如有必要,您也可以将信息保存到磁盘以供日后检索。
通过 screenlet 自动化任务
现在您已经了解了开发 screenlet 背后的概念,让我们将所有这些放到一起。大多数用户现在都在使用 Really Simple Syndication (RSS) 阅读器来阅读博客和新闻提要。对于这个最后的示例来说,您将构建可配置的 screenlet ,它监视特定提要来查找关键字并在文本框中显示任何重大新闻。结果将是可单击的链接,可在您默认的 Web 浏览器中打开文章。清单 3 显示了 RSS Search screenlet 的源代码。
清单 3. RSS Search screenlet 的 Python 代码
#!/usr/bin/env python from screenlets.options import StringOption, IntOption, ListOption import xml.dom.minidom import webbrowser import screenlets import urllib2 import gobject import pango import cairo class RSSSearchScreenlet(screenlets.Screenlet): __name__ = 'RSSSearch' __version__ = '0.1' __author__ = 'John Doe' __desc__ = 'An RSS search screenlet.' topic = 'Windows Phone 7' feeds = ['http://www.engadget.com/rss.xml', 'http://feeds.gawker.com/gizmodo/full'] interval = 10 __items = [] __mousesel = 0 __selected = None def __init__(self, **kwargs): # Customize the width and height. screenlets.Screenlet.__init__(self, width=250, height=300, **kwargs) self.y = 25 def on_init(self): # Add options. self.add_options_group('Search Options', 'RSS feeds to search and topic to search for.') self.add_option(StringOption('Search Options', 'topic', self.topic, 'Topic', 'Topic to search feeds for.')) self.add_option(ListOption('Search Options', 'feeds', self.feeds, 'RSS Feeds', 'A list of feeds to search for a topic.')) self.add_option(IntOption('Search Options', 'interval', self.interval, 'Update Interval', 'How frequently to update (in seconds)')) self.update() def update(self): """Search selected feeds and update results.""" self.__items = [] # Go through each feed. for feed_url in self.feeds: # Load the raw feed and find all item elements. raw = urllib2.urlopen(feed_url).read() dom = xml.dom.minidom.parseString(raw) items = dom.getElementsByTagName('item') for item in items: # Find the title and make sure it matches the topic. title = item.getElementsByTagName('title')[0].firstChild.data if self.topic.lower() not in title.lower(): continue # Shorten the title to 30 characters. if len(title) > 30: title = title[:27]+'...' # Find the link and save the item. link = item.getElementsByTagName('link')[0].firstChild.data self.__items.append((title, link)) self.redraw_canvas() # Set to update again after self.interval. self.__timeout = gobject.timeout_add(self.interval * 1000, self.update) def on_draw(self, ctx): """Called every time the screenlet is drawn to the screen.""" # Draw the background (a gradient). gradient = cairo.LinearGradient(0, self.height * 2, 0, 0) gradient.add_color_stop_rgba(1, 1, 1, 1, 1) gradient.add_color_stop_rgba(0.7, 1, 1, 1, 0.75) ctx.set_source(gradient) self.draw_rectangle_advanced (ctx, 0, 0, self.width - 20, self.height - 20, rounded_angles=(5, 5, 5, 5), fill=True, border_size=1, border_color=(0, 0, 0, 0.25), shadow_size=10, shadow_color=(0, 0, 0, 0.25)) # Make sure we have a pango layout initialized and updated. if self.p_layout == None : self.p_layout = ctx.create_layout() else: ctx.update_layout(self.p_layout) # Configure fonts. p_fdesc = pango.FontDescription() p_fdesc.set_family("Free Sans") p_fdesc.set_size(10 * pango.SCALE) self.p_layout.set_font_description(p_fdesc) # Display our text. pos = [20, 20] ctx.set_source_rgb(0, 0, 0) x = 0 self.__selected = None for item in self.__items: ctx.save() ctx.translate(*pos) # Find if the current item is under the mouse. if self.__mousesel == x and self.mouse_is_over: ctx.set_source_rgb(0, 0, 0.5) self.__selected = item[1] else: ctx.set_source_rgb(0, 0, 0) self.p_layout.set_markup('%s' % item[0]) ctx.show_layout(self.p_layout) pos[1] += 20 ctx.restore() x += 1 def on_draw_shape(self, ctx): ctx.rectangle(0, 0, self.width, self.height) ctx.fill() def on_mouse_move(self, event): """Called whenever the mouse moves over the screenlet.""" x = event.x / self.scale y = event.y / self.scale self.__mousesel = int((y -10 )/ (20)) -1 self.redraw_canvas() def on_mouse_down(self, event): """Called when the mouse is clicked.""" if self.__selected and self.mouse_is_over: webbrowser.open_new(self.__selected) if __name__ == "__main__": import screenlets.session screenlets.session.create_session(RSSSearchScreenlet)
基于前两个示例的概念构建,此 screenlet 使用了一些新的概念,包括 config 页面。在 on_init例程中,为用户添加三个选项来指定:用于跟踪的 RSS 提要列表、用于搜索的感兴趣的主题以及更新间隔。然后更新例程在运行时使用所有这些内容。
Python 对于此种类型的任务来说是很好的语言。标准库包括您从 RSS 提要加载可扩展标记语言(Extensible Markup Language,XML)到可搜索列表所需的一切。在 Python 中,此任务只需三行代码:
raw = urllib2.urlopen(feed_url).read() dom = xml.dom.minidom.parseString(raw) items = dom.getElementsByTagName('item')
这三行中使用的库包括 urllib2和 xml。在第一行中,在 feed_url地址上发现的完整内容被读取到字符串行。下一步,因为您知道该字符串包含 XML,所以使用 Python XML 库 dom.minidom.parseString方法来创建由节点对象构成的文档对象。
最后,创建与名为 item的单个 XML 元素对应的元素对象列表。然后,您可以迭代此列表以便搜索您的目标主题。使用 for关键字,Python 具有很好的迭代项目列表的方式,如以下代码段所示:
for item in items: # Find the title and make sure it matches the topic. title = item.getElementsByTagName('title')[0].firstChild.data if self.topic.lower() not in title.lower(): continue
将每个匹配您标准的项目添加到当前显示的列表,该列表与 screenlet 的实例关联。使用此方法可以运行相同 screenlet 的多个实例,每一个实例经配置都可搜索不同的主题。更新函数的最后部分用更新的列表重新绘制文本并基于 config 页面上的间隔触发新的更新计时器。在默认情况下,计时器每 10 秒触发一次,但是您可以将其更改为您想要的任何值。此计时器机制来自 gobject库,是 GTK 框架的一部分。
此应用程序大大扩展了 on_draw方法以便适应您的新功能。Cairo 和 Pango 库都允许创建一些用于文本窗口的效果。使用渐进使小部件的背景很漂亮且具有圆角和半透明。使用 Pango 为布局添加一些函数来轻松保存和存储当前上下文。它还提供了一种基于 screenlet 当前大小生成可缩放字体的方式。
on_draw方法中最棘手的部分是处理用户悬停在列表中的某个项目时。通过使用 for"关键字,您可以迭代 screenlet 中的项目以便查看用户是否悬停在特定项目上。如果悬停在特定项上,则设置已选择的属性并更改颜色以便提供视觉反馈。您还可以使用一点标记,将链接属性设置为粗体 —也许并不是处理问题最精致或有效的方式,但却是有用的。在用户单击框中的链接之一时,带有目标 URL 的 Web 浏览器启动。您可以在 on_mouse_down函数中看到此功能。Python 及其库允许通过一行代码来启动默认浏览器显示所期望的页面。
图 2. 示例 screenlet
结束语