本系列文章探究了如何使用 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 包的原始碼以及API(Application Programming Interface,API)文檔的連結。查看代碼還使您可以深入瞭解每一個函數可以通過調用參數做什麼以及返回什麼。
編寫簡單的 screenlet
Screenlet 的基本組成部分包括表徵圖檔案、原始碼檔案和主題檔案夾。主題檔案夾包含不同主題的附加檔案夾。您將在 screenlets.org 上發現範例模板和協助您入門所需的檔案和檔案夾。
對於第一個樣本來說,使用已提供的模板來建立基本的 “Hello World” 應用程式。此基本應用程式的代碼如清單 1 所示。
清單 1. Hello World screenlet 的 Python 代碼
#!/usr/bin/env pythonimport screenletsclass 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.sessionscreenlets.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 pythonimport screenletsimport cairoimport datetimeclass 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 = 25self.theme_name = 'default'self.on = Falseself.started = Nonedef 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 = Falseelse:if self.started:length = datetime.datetime.now() - self.startedscreenlets.show_message(None, '%s seconds' %length.seconds, 'Time')self.started = Noneself.image = 'start.png'self.on = Truedef on_draw_shape(self, ctx):self.on_draw(ctx)ctx.rectangle(0, 0, self.width, self.height)ctx.fill()if __name__ == "__main__":import screenlets.sessionscreenlets.session.create_session(TimeTrackerScreenlet)
此樣本引入了幾個在您開始構建任何有用的程式以前需要瞭解的概念。所有的 screenlet 應用程式都具有響應特定使用者操作或事件(如按一下滑鼠或拖放操作)的能力。在此樣本中,滑鼠按下事件用作更改表徵圖狀態的觸發器。在 screenlet 運行時,顯示 start.png 映像。單擊該映像將其變更為 stop.png 映像並在 self.started中記錄開始時間。單擊停止映像將該映像變更回 start.png 並顯示從單擊第一個映像開始所經過的時間量。
響應事件是另一個關鍵功能,使得構建任何數量的不同應用程式成為可能。雖然此樣本僅使用 mouse_down事件,但是您可以對由 screenlet 架構或系統事件(如計時器)產生的其他事件使用相同的方法。此處引入的第二個概念是持久狀態。因為您的應用程式是持續啟動並執行,等待事件來觸發一些操作,就能夠在記憶體中保持對項目的跟蹤,如單擊開始映像的時間。如有必要,您也可以將資訊儲存到磁碟以供日後檢索。
通過 screenlet 自動化任務
現在您已經瞭解了開發 screenlet 背後的概念,讓我們將所有這些放到一起。大多數使用者現在都在使用 Really Simple Syndication (RSS) 閱讀器來閱讀部落格和新聞提要。對於這個最後的樣本來說,您將構建可配置的 screenlet ,它監視特定提要來尋找關鍵字並在文字框中顯示任何重大新聞。結果將是可單擊的連結,可在您預設的 網頁瀏覽器中開啟文章。清單 3 顯示了 RSS Search screenlet 的原始碼。
清單 3. RSS Search screenlet 的 Python 代碼
#!/usr/bin/env pythonfrom screenlets.options import StringOption, IntOption, ListOptionimport xml.dom.minidomimport webbrowserimport screenletsimport urllib2import gobjectimport pangoimport cairoclass 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 = Nonedef __init__(self, **kwargs):# Customize the width and height.screenlets.Screenlet.__init__(self, width=250, height=300, **kwargs)self.y = 25def 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.dataif 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.dataself.__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 = 0self.__selected = Nonefor 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] += 20ctx.restore()x += 1def 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.scaley = event.y / self.scaleself.__mousesel = int((y -10 )/ (20)) -1self.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.sessionscreenlets.session.create_session(RSSSearchScreenlet)
基於前兩個樣本的概念構建,此 screenlet 使用了一些新的概念,包括 config 頁面。在 on_init常式中,為使用者添加三個選項來指定:用於跟蹤的 RSS 提要列表、用於搜尋的感興趣的主題以及更新間隔。然後更新常式在運行時使用所有這些內容。
Python 對於此種類型的任務來說是很好的語言。標準庫包括您從 RSS 提要載入可延伸標記語言 (XML)(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.dataif 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 的 網頁瀏覽器啟動。您可以在 on_mouse_down函數中看到此功能。Python 及其庫允許通過一行代碼來啟動預設瀏覽器顯示所期望的頁面。
圖 2. 樣本 screenlet
結束語
通過 Python 和 screenlet 構建有用的傳統型應用程式並不是困難的任務。最大的障礙是適應 screenlet API 以及在不同函數之間傳遞控制項的技巧。雖然該文檔可能不便於閱讀,但是包含了您使用不同的函數所需要的資訊。快速開始工作的更好辦法是修改接近您的要求的現有 screenlet。