Разработка сценариев для рабочих столов Linux.: Часть 1. Знакомство с библиотекой screenlets

Использование Python и screenlet’ов для создания полезных приложений для рабочих столов в ОС Linux

В этой серии статей рассматривается использование языка Python, инфраструктуры screenlets и Nautilus для сценариев для рабочего стола GNOME с целью создания эффективной пользовательской рабочей среды.

Пол Феррилл, технический директор, ATAC

Пол Феррилл (Paul Ferrill) уже более 20 лет пишет статьи, посвященные различным аспектам информационных технологий. Пол начинал с написания обзоров сетевых продуктов, таких как LANtastic и ранние версии Novell Netware, для журнала PC Magazine. Пол обладает степенями магистра и бакалавра в области электротехники и имеет опыт разработки ПО для самых разнообразных платформ и архитектур.



24.01.2012

В качестве основы при разработке приложений для рабочего стола Linux обычно используется какая-нибудь библиотека для создания графического интерфейса пользователя (GUI framework). Для рабочего стола GNOME обычно используется библиотека GTK+, а для рабочего стола KDE (K Desktop Environment) - библиотека Qt. Обе эти платформы содержат всю функциональность, необходимую для разработки GUI-приложений, включая библиотеки и инструменты для проектирования окон приложений, которые будут использоваться пользователями. В этой статье рассказывается о том, как с помощью набора виджетов – screenlet’ов (см. ссылку в разделе Ресурсы) разрабатывать приложения, эффективно взаимодействующие с рабочим столом Linux.

В список приложений для повышения эффективности пользования рабочим столом, попадают такие программы, как GNOME Do и Tomboy. Обычно пользователи могут использовать эти приложения непосредственно с рабочего стола с помощью специального сочетания клавиш или через "drag-and-drop" из других приложений, например, Mozilla Firefox. Программа Tomboy работает как инструмент для создания заметок на рабочем столе и обладает возможностью получать текст из других окон.

Начало работы с screenlet’ами

Для того чтобы начать разрабатывать screenlet’ы, необходимо установить несколько компонентов. Сначала потребуется установить пакет screenlets с помощью центра управления ПО в ОС Ubuntu или непосредственно из командной строки. Для загрузки требуемого пакета в поле Search в окне центра управления ПО следует ввести screenlets. В результатах поиска будут представлены две опции: установить основной пакет или установить только документацию по screenlet’ам.

Python и Ubuntu

Для разработки screenlet’ов мы используем язык Python. В стандартной инсталляции Ubuntu 10.04 уже имеется среда Python 2.6, так как от неё зависит большое количество утилит. Однако, в зависимости от требований приложения, могут потребоваться дополнительные библиотеки. Все примеры, представленные в этой статье, были разработаны и протестированы в Ubuntu 10.04.

После выполнения этих действий необходимо загрузить исходный код тестовых screenlet’ов с Web-сайта screenlets.org. Тестовый screenlet находится в папке src/share/screenlets/Test и использует библиотеки Cairo и GTK, которые также потребуется установить. Структуру стандартного screenlet’a можно изучить на примере тестовой программы, исходный код которой находится в файле TestScreenlet.py.

Язык Python является высокоуровневым объектно-ориентированным языком программирования, поэтому для определения объектов в нем используется ключевое слово class. В представленном примере класс называется TestScreenlet, и в нем определено несколько методов. На строке 42 в файле TestScreenlet.py содержится следующий код:

def __init__(self, **keyword_args):

Двойным подчеркиванием спереди и сзади (__) в языке Python выделяются системные функции с заранее определенным поведением. В данном случае функция __init__ фактически является конструктором класса и содержит действия, которые должны быть выполнены при создании нового экземпляра объекта. Согласно стандартным требованиям первым аргументом любого метода, имеющегося в классе, должна быть ссылка self, указывающая на текущий экземпляр класса. Подобное поведение позволяет использовать ссылку self для обращения к методам и свойствам текущего объекта, как показано ниже:

self.theme_name = "default"

В инфрастуктуре screenlets установлены правила наименования и стандарты, с которыми можно познакомиться на странице разработчика на Web-сайте screenlets.org (ссылка указана в разделе Ресурсы). Там же находится ссылка на страницу с документацией, в которой описывается исходный код пакета screenlets и его API. Знакомство с исходным кодом поможет лучше понять, что именно функции делают с входными параметрами и что они возвращают пользователю.


Создание простого screenlet’a

Основными компонентами screenlet’а являются файл с пиктограммой, файл с исходным кодом и каталог themes. В этом каталоге содержатся дополнительные каталоги с различными стилями (или темами) отображения. С Web-сайта screenlets.org можно загрузить шаблон стандартного screenlet’a со всеми необходимыми файлами и каталогами.

Первый screenlet, разработанный на основе этого шаблона, будет представлять собой стандартное "Hello World"-приложение. Исходный код screenlet’a "Hello World" приведен в листинге 1.

Listing 1. Python code for the Hello World screenlet
#!/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):
        # установить ширину и высоту screenlet’a.
        screenlets.Screenlet.__init__(self, width=180, height=50, **kwargs)
    
    def on_draw(self, ctx):
        # заполнить задний фон в окне screenlet’a белым цветом.
        ctx.set_source_rgb(255, 255, 255)
        self.draw_rectangle(ctx, 0, 0, self.width, self.height)

        # вывести в окно текстовое сообщение, используя черный цвет.
        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)

Любое приложение должно импортировать библиотеку screenlets и создать новый сеанс. Существуют и другие обязательные требования, включающие действия по инициализации и базовую функцию draw для отображения виджета на экране. В примере TestScreenlet.py есть метод __init__, инициализирующий объект. В этом методе содержится единственная инструкция, выполняющая вызов метода __init__ screenlet’a, который устанавливает начальные значения для высоты и ширины окна, создаваемого для данного приложения.

Вторая функция, необходимая для работы приложения, - это метод on_draw. Эта процедура устанавливает белый цвет для фона прямоугольника и выводит его на экран в соответствии с заранее установленными размерами. Далее в этом методе задаются черный цвет шрифта и исходное значение "Hello World" для текста, который затем отрисовывается в окне приложения. На рисунке 1 показан результат запуска этого screenlet’a. Эта стандартная структура в дальнейшем будет использоваться в статье в качестве отправной точки для создания более сложных приложений.

Рисунок 1. Пример простого screenlet’a
Рисунок 1. Пример простого screenlet’a

Повторное использование кода в более сложном screenlet’e

Полезной особенностью при разработке screenlet’ов является возможность использования кода из других приложений. Повторное использование обеспечивает дополнительные возможности, открывая доступ к множеству существующих Python-проектов с открытым исходным кодом. Все screenlet’ы обладают одинаковой начальной структурой, но могут содержать дополнительные методы для поддержки различной функциональности. В листинге 2 приведен пример приложения TimeTrackerScreenlet.

Листинг 2. Исходный код TimeTrackerScreenlet
#!/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’a отображается пиктограмма start.png. При нажатии на изображение, оно изменяется на stop.png, а время запуска записывается в поле self.started. При повторном нажатии на изображение оно меняется обратно на start.png, а в окне приложения выводится время, прошедшее с момента первого нажатия.

Обработка событий - еще одна ключевая особенность screenlet’ов, которая делает возможной разработку разнообразных приложений. Хотя в данном примере используется только событие mouse_down, этот подход можно применять и для других событий, генерируемых инфраструктурой screenlet’ов, или системных событий, таких как таймеры. Другая идея, представленная в этом примере, - персистентное состояние. Поскольку приложение постоянно работает, ожидая события для активации определенного действия, оно может хранить информацию в памяти, например, время первого нажатия на пиктограмму. Также при необходимости можно сохранить информацию на диске для последующего использования.


Автоматизация задач с помощью screenlet’ов

После знакомства с основными возможностями screenlet’ов пришло время перейти к их практическому применению. Сегодня большинство пользователей использует программы для получения информации из RSS-каналов для чтения интересующих их блогов или новостей. В качестве очередного примера будет разработан настраиваемый screenlet, выполняющий мониторинг определенных RSS-потоков по ключевым словам и отображающий полученные результаты в текстовом окне. Эти результаты будут представлять собой интерактивные ссылки, нажатие на которые будет открывать соответствующий материал в Web-браузере. В листинге 3 приведен исходный код "RSS Search"-screenlet’a.

Листинг 3. Исходный код screenlet’a для работы с RSS-потоками
#!/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):
        # установить высоту и ширину screenlet’a.
        screenlets.Screenlet.__init__(self, width=250, height=300, **kwargs)
        self.y = 25
        
    def on_init(self):
        # добавить GUI-компонент со списком возможных вариантов поиска.
        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):
        # выполнить поиск в указанных RSS-каналах и обновить результаты.
        
        self.__items = []

        # выполнить поиск по всем определенным каналам.
        for feed_url in self.feeds:
            
            # загрузить необработанные данные из RSS-канала.    
            raw = urllib2.urlopen(feed_url).read()
            dom = xml.dom.minidom.parseString(raw)
            # и найти в этих данных необходимую информацию.    
            items = dom.getElementsByTagName('item')
            
            for item in items:
                
                # проверить, что название материала совпадает с критерием поиска.
                title = item.getElementsByTagName('title')[0].firstChild.data
                if self.topic.lower() not in title.lower(): continue
                
                # сократить длину названия до 30 символов.
                if len(title) > 30: title = title[:27]+'...'
                
                # найти ссылку и сохранить текущий элемент.
                link = item.getElementsByTagName('link')[0].firstChild.data
                self.__items.append((title, link))

        self.redraw_canvas()

        # обновить выводимую информацию по истечении определенного интервала
        self.__timeout = gobject.timeout_add(self.interval * 1000, self.update)
        
    def on_draw(self, ctx):
        # функция вызывается при каждой отрисовке screenlet’a на экране
        
        # вывести задний фон с градиентной заливкой
        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))
        
        # убедиться, что менеджер размещения GUI-компонентов инициализирован и обновлен
        if self.p_layout == None :
            self.p_layout = ctx.create_layout()
        else:
            ctx.update_layout(self.p_layout)
            
        # настроить шрифты
        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)

        # вывести требуемый текст
        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)
            
            # определить, ссылка на какой материал, находится под указателем мыши.
            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):
        # функция вызывается при перемещении мыши по окну screenlet’a
        
        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):
        # функция вызывается при нажатии мыши
        
        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’ам, в этом примере встречаются и новые идеи, включая конфигурационную страницу. В процедуре on_init добавляются три опции, которые должны быть установлены пользователем: список отслеживаемых RSS-потоков, интересующая тема, по которой будет производиться поиск, и период обновления. Установленные значения используются процедурой update во время работы.

Язык Python идеально подходит для решения подобных задач. Стандартная библиотека включает все необходимое для загрузки XML-данных из RSS-потока в список для дальнейшего поиска. В Python для этого потребуются всего три строчки кода:

raw = urllib2.urlopen(feed_url).read()
dom = xml.dom.minidom.parseString(raw)
items = dom.getElementsByTagName('item')

В приведенном фрагменте используются библиотеки urllib2 и xml. В первой строке кода содержимое, расположенное по адресу feed_url, полностью считывается в строку raw. Далее, так как известно, что эта строка содержит XML, используется метод dom.minidom.parseString XML-библиотеки Python, чтобы создать объект документа, состоящий из объектов узлов.

В итоге создается список из объектов, соответствующих отдельным XML-элементам с именем item. Теперь можно пройти по этому списку в поиске интересующей темы. В языке Python есть элегантный способ прохода по списку элементов с помощью цикла for, как показано ниже:

for item in items:
    # найти название элемента и сравнить его с критерием поиска.
    title = item.getElementsByTagName('title')[0].firstChild.data
    if self.topic.lower() not in title.lower(): continue

Каждый элемент, соответствующий критерию поиска, добавляется в отображаемый список, связанный с данным экземпляром screenlet’a. Такой подход позволяет иметь несколько одновременно работающих экземпляров screenlet’a, каждый из которых выполняет поиск по различным темам. Заключительная часть функции update перерисовывает текст в обновляемом списке и запускает новый таймер обновления, используя период обновления, указанный на конфигурационной странице. По умолчанию таймер срабатывает каждые 10 секунд, но это значение можно поменять на любое другое. Механизм для запуска таймеров берется из библиотеки gobject, входящей в инфраструктуру GTK.

Чтобы реализовать поставленные требования, потребовалось значительно увеличить функциональность метода on_draw в данном приложении. Обе библиотеки – Cairo и Pango - позволяют добавлять определенные эффекты в окно с текстом. Так, использование градиентной заливки фона позволяет создать «стильное» визуальное представление со скругленными углами и полупрозрачностью. С помощью Pango в менеджер размещения GUI-компонентов можно добавить несколько функций для сохранения и восстановления текущего контекста. Эта библиотека также предоставляет способ создания масштабируемых шрифтов в зависимости от текущего размера screenlet’a.

Самая сложная часть метода on_draw заключается в обработке события, когда пользователь наводит указатель мыши на элемент списка. С помощью ключевого слова for можно выполнить итерирование по элементам в screenlet’e, чтобы установить, не находится ли указатель мыши над каким либо элементом. Если это так, то необходимо установить свойство selected (выбрано) и изменить цвет, чтобы обеспечить обратную связь с действиями пользователя. Кроме того, с помощью разметки для свойства link устанавливается полужирный шрифт, хотя это и не самое элегантное решение. Когда пользователь нажимает на одну из ссылок в окне screenlet’a, Web-браузер открывает выбранный URL-адрес. Эта функциональность находится в функции on_mouse_down. С помощью языка Python и его библиотек одной строкой кода можно запустить стандартный Web-браузер для просмотра требуемой страницы. На рисунке 2 приведено изображение запущенного screenlet’a.

Рисунок 2. Пример screenlet’a
Рисунок 2. Пример screenlet’a

Заключение

С помощью Python и screenlet’ов легко создавать полезные приложения для рабочего стола. Вероятно, сложнее всего будет освоить API screenlet’ов и механизмы передачи управления между различными функциями. Хотя соответствующая документация - не самый удобный источник информации, в ней содержатся все сведения, необходимые для использования различных функций. Возможно, будет лучше сразу перейти к практическим экспериментам, модифицируя готовые screenlet’ы в соответствии с вашими требованиями.

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=788799
ArticleTitle=Разработка сценариев для рабочих столов Linux.: Часть 1. Знакомство с библиотекой screenlets
publish-date=01242012