Воскресенье, 2024-05-19, 3:20 PM
Статьи - Python
Приветствую Вас Гость | RSS
Главная страница Каталог статей Регистрация Вход
Меню сайта

Категории каталога
Python [18]
Статьи по Python

Наш опрос
Какую технологию вы предпочитаете?
Всего ответов: 250

Начало » Статьи » Python » Python

Создание апплета GNOME
Этот цикл посвящен теме создания апплетов для панели GNOME. Если кратко, апплет - это маленькое приложение, которое встраивается в панель и либо информирует о чем-либо (например, погоде, или о состоянии батареи), либо выполняет какие-либо одноэтапные действия (запускает поиск, изменяет громкость и т.д.).

Я буду создавать простой апплет для включения/выключения прокси в GNOME.

Прежде чем начать, стоит упомянуть один документ, который описывает создание апплета средствами Python и PyGTK: это GNOME applets with Python. Однако, на мой взгляд, у него есть ряд недостатков, которые и побудили меня осветить эту тему по-своему.

Итак, приступим.

В первой части я буду создавать скелет апплета и регистрировать его в GNOME, во второй буду писать функциональную часть, а в третьей - заниматься "полировкой" и украшательствами.

Скелет апплета
Перво-наперво, выделю то, что необходимо для функционирования любого апплета, вне зависимости от его природы:

некий виджет-контейнер (например, HBox)
некий "полезный" виджет (у меня это будет Label)
всплывающая подсказка
некое действие по левой кнопки мыши
контекстное меню
возможность запуска как отдельного приложения (для отладки)
регистрация в GNOME как апплета к панели
Займемся реализацией:

import sys
import gtk
import gtk.gdk
import gnome.ui
import gnomeapplet

class GnomeAppletSkeleton(gnomeapplet.Applet):
"""Simple applet skeleton"""

def __init__(self, applet):
"""Create applet"""

self.applet = applet
self.__init_core_widgets()
self.init_additional_widgets()
self.init_ppmenu()
self.__connect_events()
self.applet.connect("destroy", self._cleanup)
self.after_init()
self.applet.show_all()

Прежде чем приступить к пояснениям, скажу о конвенции насчет имен методов. Если имя метод начинается с двух подчеркиваний, то перегружать (переопределять) такой метод нежелательно. Если же имя метода начинается с буквы, то такой метод можно практически безболезненно перегружать. Но все же, если Вы будете писать свой апплет, то все же гляньте код соответствующего метода GnomeAppletSkeleton прежде чем перегружать его.

Итак, первым делом инициализирую ключевые виджеты, без которых не обойдется ни один апплет:

def __init_core_widgets(self):
"""Create internal widgets"""
self.tooltips = gtk.Tooltips()
self.hbox = gtk.HBox()
self.ev_box = gtk.EventBox()
self.applet.add(self.hbox)
self.hbox.add(self.ev_box)

Поскольку апплет - безоконный виджет (у него нет окна), то для того, чтобы была возможность реагировать на события, я помещаю EventBox в него. А уж все дополнительные виджеты (в моем случае это будет только Label) добавляются к ev_box.

def init_additional_widgets(self):
"""Create additional widgets"""
self.label = gtk.Label("Dummy")
self.ev_box.add(self.label)

Далее, указываю необходимую информацию для контекстного меню (popup menu):

def init_ppmenu(self):
"""Create popup menu"""
self.ppmenu_xml = """
<popup name="button3">

<menuitem name="About Item" verb="About" stockid="gtk-about" />
</popup>
"""

self.ppmenu_verbs = [
("About", self.on_ppm_about),
]

Заметьте, что в XML-описании пункта меню "О программе" нет собственно названия пункта, а лишь его StockID. Это сделано по той простой причине, что пункт меню "О программе" стандартен для большинства приложений и в случае указания StockID Вы получаете:

стандартную иконку для данного пункта (причем, с изменением темы оформления GNOME эта иконка может меняться)
стандартное название пункта меню, причем автоматически переведенное на нужный язык
Каждый пункт меню имеет "глагол"-действие, который ставится ему в соответствие. self.ppmenu_verbs же задает соответствие между "глаголом"-действием и callback-функцией.

Следующий шаг по созданию апплета - "соединение" callback-функций и событий:

def __connect_events(self):
"""Connect applet's events to callbacks"""
self.ev_box.connect("button-press-event", self.on_button)
self.ev_box.connect("enter-notify-event", self.on_enter)
self.button_actions = {
1: lambda: None,
2: lambda: None,
3: self._show_ppmenu,
}

Еще раз отмечу, что апплет - безоконный виджет, поэтому все события генерирует ev_box. В данном случае, я соединил события "нажатие на кнопку" с callback-функцией self.on_button и событие "попадание курсора в область виджета" с callback-функцией self.on_enter. Здесь же при помощи словаря self.button_actions задал соответствие между кнопками мыши и функциями-действиями. Стоит заметить, что callback-функции, соединенные с событиями, должны быть определенной сигнатуры (об этом чуть позже), а функции-действия не должны принимать ни один параметр.

Следующий по порядку вызов - это метод after_init. В скелете он пустой, предназначен специально для переопределения в потомках.

С этапами создания апплета вроде завершил, остались callback-функции… Я не буду пересказывать PyGTK reference, лишь перечислю типы callback-функций и их сигнатуры, которые встречаются у меня:

callback-функция на событие destroy апплета. Сигнатура function(event). Реализация - _cleanup
callback-функция на события ev_box. Сигнатура function(widget, event). Реализации - on_enter, on_button
callback-функция на пункт меню. Сигнатура function(event, data=None). Реализация - on_ppm_about
функция-действие (мое название) на нажатие одной из кнопок мыши. Сигнатура function(). Реализация - _show_ppmenu.
Содержимое callback-функции _cleanup не буду приводить - оно слишком тривиально (удаляется объект self.applet) для того, чтобы занимать место, а кому интересно - гляньте в полном исходном тексте апплета. Что касается остальных callback-функций, я их приведу и прокомментирую, поскольку они все же представляют интерес.

def on_button(self, widget, event):
"""Action on pressing button in applet"""

if event.type == gtk.gdk.BUTTON_PRESS:
self.button_actions[event.button]()

Callback-функция on_button вызывается при нажатии любой кнопки мыши внутри виджета. И внутри этой функции я, во-первых, убеждаюсь, что присоединили к правильному событию (нажатию на клавишу), а, во-вторых, вызываю нужную функцию-действие, выбирая (в event.button хранится номер кнопки, нажатие на которую и вызвало появление данного события) из ранее описанного словаря self.button_actions. Для кнопок 1 и 2 у меня пустые действия, для 3 - контекстное меню. Показ контекстного меню - специальный метод класса Applet - setup_menu. Первый аргумент - XML-описание меню, второй - "глаголы"-действия, третий - пользовательские данные (передаются третьим параметром в callback-функцию).

def _show_ppmenu(self):
"""Show popup menu"""
self.applet.setup_menu(self.ppmenu_xml, self.ppmenu_verbs, None)

Что касается события "попадание курсора в область виджета", то на него я реагировать буду так: показывать какую-нибудь простенькую подсказку, ради разнообразия сделав ее динамической.

def on_enter(self, widget, event):
"""Action on entering"""
info = "Hey, it just skeletonnAnd on_enter event time is %d" %
event.time
self.tooltips.set_tip(self.ev_box, info)

И последняя callback-функция - на вызов пункта меню "О программе". Здесь я воспользуюсь стандартным диалогом из модуля gnome.ui:

def on_ppm_about(self, event, data=None):
"""Action on choosing 'about' in popup menu"""
gnome.ui.About("GNOME Applet Skeleton", "0.1", "GNU General Public License v.2",
"Simple skeleton for Python powered GNOME applet",
["Pythy <the.pythy@gmail.com>",]
).show()

Класс-костяк апплета написан, теперь нужно описать его "фабрику":

def applet_factory(applet, iid):
GnomeAppletSkeleton(applet, iid)
return True

Ух. С первым этапом закончил. Костяк апплета сделан. Осталось дело за малым. Запустить и посмотреть, что же получилось :)

Запуск апплета в отдельном окне
Для начала нужно отладить апплет, для этого пишу код, позволяющий запускать апплет в отдельном окне:

def run_in_window():
main_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
main_window.set_title("GNOME Applet Skeleton")
main_window.connect("destroy", gtk.main_quit)
app = gnomeapplet.Applet()
applet_factory(app, None)
app.reparent(main_window)
main_window.show_all()
gtk.main()
sys.exit()

def main(args):
if len(args) == 2 and args[1] == "run-in-window":
run_in_window()
else:
run_in_panel()

if __name__ == '__main__':
main(sys.argv)

Небольшой комментарий по коду: если файл запускается как скрипт, то выполняется функция main, в ней, в зависимости от того, передан ли аргумент run-in-window, апплет запускается либо в окне (функция run_in_window), либо в панели (run_in_panel). Про функцию run_in_panel чуть ниже, а в run_in_window стоит обратить внимание на строчку app.reparent(main_window). Этим собственно и достигается, что апплет запускается в отдельном окне.

Регистрация апплета в панели GNOME
Если выше был обычный Python-код, с некоторой PyGTK-спецификой, то сейчас будет сплошная магия ;) Это, кстати, одна из слабых сторон GNOME-Python - отсутствие систематической документации (для gnomeapplet вообще никакой документации нет, за исключением пары примеров и вышеупомянутой "методички"). К примеру, при регистрации апплета вызывается функция applet_bonobo_factory, однако нигде не упоминается, какие параметры в нее передаются. Чтобы узнать это, нужно лезть в исходные тексты. Я, конечно, понимаю, что "Use code, Luke!", но все же качество документации по PyGTK в целом хромает (например, сплошь и рядом в документации рекомендуются методы, которые уже пару версий назад как уже объявлены устаревшими).

Общая идеология регистрации апплета такова:

описываем мета-информацию в специальном .server файле
в апплете вызываем специальный интерфейс
Вначале закончу дело с кодом апплета:

def run_in_panel():
gnomeapplet.bonobo_factory("OAFIID:GNOME_AppletSkeleton_Factory",
GnomeAppletSkeleton.__gtype__,
"Applet skeleton",
"0",
applet_factory)

это и есть вызов "специального интерфейса". Параметры такие: IID (уникальный идентификатор сервиса в GNOME), тип (это остатки C-природы GTK, тип GObject), имя, версия, callback-функция.

Теперь, что касается "описания мета-информации". Пишем следующий XML (он для всех апплетов будет идентичным, специфичные для моего апплета данные я выделил полужирным):

<oaf_info>

<oaf_server iid="OAFIID:GNOME_AppletSkeleton_Factory"
type="exe" location="/usr/local/lib/pygnomeapplet/applet_skeleton.py">

<oaf_attribute name="repo_ids" type="stringv">
<item value="IDL:Bonobo/GenericFactory:1.0" />
<item value="IDL:Bonobo/Unknown:1.0" />

</oaf_attribute>
<oaf_attribute name="name" type="string" value="Applet skeleton factory" />
<oaf_attribute name="name-ru" type="string" value="Фабрика скелета апплета" />

<oaf_attribute name="description" type="string" value="Factory of simple applet skeleton" />
<oaf_attribute name="description-ru" type="string" value="Фабрика скелета простейшего апплета" />

</oaf_server>
<oaf_server iid="OAFIID:GNOME_AppletSkeleton"
type="factory" location="OAFIID:GNOME_AppletSkeleton_Factory">
<oaf_attribute name="repo_ids" type="stringv">

<item value="IDL:GNOME/Vertigo/PanelAppletShell:1.0" />
<item value="IDL:Bonobo/Control:1.0" />
<item value="IDL:Bonobo/Unknown:1.0" />

</oaf_attribute>
<oaf_attribute name="name" type="string" value="Applet skeleton" />
<oaf_attribute name="name-ru" type="string" value="Скелет апплета" />

<oaf_attribute name="description" type="string" value="Simple applet skeleton, do nothing" />
<oaf_attribute name="description-ru" type="string" value="Скелет простого апплета, ни делает ни чего" />

<oaf_attribute name="panel:category" type="string" value="Accessories" />
<oaf_attribute name="panel:icon" type="string" value="gnome-panel.png" />

</oaf_server>
</oaf_info>

Так, что тут: два раздела, фабрика и сам апплет. Для каждого определены IID, у фабрики IID должен совпадать с тем, что указали в вызове bonobo_factory в апплете. Дополнительно отмечу, что тут же можно задавать переводы названия/описания апплета (в данном случае будет на русском, если у Вас русская локаль и на английском во всех остальных случаях). Называем этот файл GNOME_AppletSkeleton.server и "скармливаем" его Bonobo Activation Server. Существует несколько вариантов этого "действа":

Поместить .server в каталог /usr/lib/bonobo/servers
Изменить /etc/bonobo-activation/bonobo-activation-config.xml (там есть несколько примеров), добавить нужный путь (скажем, /usr/local/lib/bonobo/servers) и положить .server туда
В переменную BONOBO_ACTIVATION_PATH добавить каталог, где лежит .server.
Мне наиболее правильным показался второй вариант, я его и использовал.

После этого скрещиваем пальцы и пытаемся добавить апплет на панель. Если .server правильно "скормили", то апплет-скелет появляется в списке кандидатов на добавление. Если и все остальное сделали верно, то добавление пройдет гладко.

Стоит отметить, что контекстное меню в "режиме окна" и в "режиме панели" отличаются - для панели появляются дополнительные пункты меню.
GConf
Напомню, что конечная цель - создание апплета для переключения прокси (вкл/выкл). Для этих целей есть диалог "Параметры прокси-серверов" (Система->Параметры->Сервис Прокси), но им не удобно пользоваться, поскольку нужно совершать много лишних действий.

Сам диалог настройки ничего не делает, он лишь изменяет значение соответствующего параметра в GConf (/system/proxy/mode), а программы, использующие прокси, следят за значением этого параметра. Поэтому, для того чтобы включить или выключить прокси, апплету достаточно изменить нужное значение в GConf.

У GConf есть как положительные, так и отрицательные стороны. Из положительных хочу отметить такие приятные вещи как:

информирование всех "подписанных" на параметр программ об изменении его значения (внешне это выглядит так: открываете диалог "Параметры прокси-серверов", меняете при помощи Python значение нужного параметра и переключатель в диалоге сам "прыгает" на нужную позицию)
глобальность изменения: не нужно думать о том, что нужно еще проставить значение переменной http_proxy для wget - GConf делает это автоматически. Ну а то, что все GNOME-программы берут настройки о использовании/не использовании прокси из GConf, я думаю, понятно без объяснений.
Из отрицательных я выделяю:

gconfd "гадит" в журналы. Мне не нравится, что gconfd пишет в общесистемный журнал (/var/log/messages) на русском языке.
несогласованность настроек различных программ. Например, Gajim и Firefox используют собственные настройки соединений. И если для Gajim можно найти оправдание (поддержка нескольких аккаунтов), то для Firefox я не вижу причин игнорировать GConf. Хотя для Gajim адекватен был бы выбор между "использовать глобальные настройки Gnome" и "использовать отдельные настройки для соединений".
невозможность (или просто я не знаю о таком) изменить настройки только для отдельных приложений
Прежде чем писать код, советую "поиграться" в интерактивной сессии (в качестве индикатора о смене настроек можно использовать диалог "Параметры прокси-сервера", либо редактор `gconf-editor`, но на мой взгляд, диалог прокси более показателен)

>>> import gconf
>>> gc = gconf.client_get_default()
>>> gc.get_string('/system/proxy/mode')
'none'

>>> gc.set_string('/system/proxy/mode', 'manual')
True
>>> gc.get_string()
'manual'

Что происходит, если попытаться получить значение несуществующего ключа, или извлечь значение не того типа, предлагаю изучить самостоятельно, вооружившись gconf-editor и Python.

Что касается возможных значений ключа ‘/system/proxy/mode’, то допустимые значения здесь таковы: none, manual, auto. Если значение не входит в список разрешенных, оно интерпретируется как none.

Последняя оговорка и можно приступать к написанию кода: для того, чтобы GConf сообщал об изменении того или иного ключа, нужно "подгрузить" одну из веток GConf для "прослушки" и добавить callback-функцию на изменение нужного ключа.

В коде изложено всё вышесказанное:

class ProxyGconfClient(object):
"""Get/set proxy states"""
proxy_dir = "/system/proxy"

proxy_key = "/system/proxy/mode"
on_state = 'manual'
off_state = 'none'

def __init__(self, callback=None):
"""
GConf client for getting/setting proxy states

@param callback: callback function. Executing
when proxy state changed. It calls with params:
* client - GConf client
* cnxn_id - connection ID
* entry - changed entry
* params - additional params
@type callback: callable
"""
if callback is None:
callback = lambda client, cnxn_id, entry, params: None
# make connection to GConfD
self.client = gconf.client_get_default()
# add proxy_dir for inspection, without preload
self.client.add_dir(self.proxy_dir,
gconf.CLIENT_PRELOAD_NONE)
# add callback for notifying about changes
self.client.notify_add(self.proxy_key,
callback)

def get_state(self):
"""Returns state of proxy"""
return self.client.get_string(self.proxy_key)

def set_state(self, value):
"""

Set state of proxy

@param value: state of proxy, may be
* 'none' - direct connection, proxy off
* 'manual' - manual settings, proxy on
* 'auto' - auto settings, proxy on
if value neither 'manual', no 'auto', it means
direct connection, i.e. proxy off.
@raise RuntimeError: cannot set value to GConf's key
"""
if not self.client.set_string(self.proxy_key,
value):
raise RuntimeError("Unable to change key %s" %
self.proxy_key)

def on(self):
"""Turn proxy on (i.e. set proxy mode 'manual')"""
self.set_state(self.on_state)

def is_on(self):
"""Is proxy on? (i.e. proxy in 'manual' mode)"""
return self.get_state() == self.on_state

def off(self):
"""Turn proxy off (i.e. set direct connection)"""

self.set_state(self.off_state)

Заполнение скелета
Собственно для реализации работающего апплета почти всё готово: скелет апплета, объект-переключатель -

class ProxyGnomeApplet(GnomeAppletSkeleton):

def after_init(self):
self.proxy = ProxyGconfClient(callback=self._cb_proxy_change)
self.proxy_state = self.proxy.get_state()
self.button_actions[1] = self.switch_proxy
self.label.set_text(self.proxy_state)

def _cb_proxy_change(self, client, cnxn_id, entry, params):
"""Callback for changing proxy"""
self.proxy_state = self.proxy.get_state()
self.label.set_text(self.proxy_state)

def on_enter(self, widget, event):
info = "Proxy mode: %s" % self.proxy_state
self.tooltips.set_tip(self.ev_box, info)

def switch_proxy(self):
if self.proxy.is_on():
self.proxy.off()
else:
self.proxy.on()

Поясняю написанное:

Во-первых, напоминаю, что after_init специально создавался в скелете для переопределения в потомках, так что это правильное место для добавления прокси-переключателя (атрибут proxy), определения действия на левую кнопку мыши (button_actions[1]) и установки начального текста для label.

Во-вторых, в качестве callback-функции, которая выполняется при смене состояния ключа GConf, я использую _cb_proxy_change (сигнатура этой функции такова: GConf-клиент, идентификатор соединения, измененный ключ, дополнительные параметры). По идее, идеологически более правильно здесь использовать конструкцию entry.get_value().get_string(), но мне не нравится такой стиль записи, он не Pythonic. Поэтому я использую информацию от объекта прокси-переключателя.

Далее, переопределенная callback-функция on_enter, теперь она показывает состояние прокси, а не просто "Привет мир".

Ну и последний метод - switch_proxy - выполняется по нажатию левой кнопки, переключает состояние прокси.

Действия по регистрации этого, уже работающего, апплета абсолютно аналогичны таковым для скелета, так что я не описываю их. Рабочий код можете взять отсюда.

Фактически, апплет уже функционирует, однако выглядит он не притязательно.

Категория: Python | Добавил: webmaster (2006-12-14)
Просмотров: 422 | Рейтинг: 0.0 |

Всего комментариев: 0
Имя *:
Email *:
Код *:
Форма входа

Сервисы

Поиск по каталогу

Друзья сайта

| Ссылки 1 | Ссылки 2 | Ссылки 3 |
www.webmaster.clan.su Каталог+поисковая система be number one Bakililar.az Top Sites Сервис авто регистрации в
каталогах, статьи про раскрутку сайтов, web дизайн, flash, 
photoshop, хостинг, рассылки; форум, баннерная сеть, каталог 
сайтов, услуги продвижения и рекламы сайтов Скрипт для определения тиц (Яндекс CY: индекс цитирования). Определение pr (Google Pagerank). Проверить тиц pr сайта.
Copyright WebMaster.Clan © 2006 Бесплатный хостинг uCoz