воскресенье, ноября 23, 2008

Создание собственных виджетов в PyGTK с помощью cairo

Свободная библиотека Gtk, как известно, не отличается очень большим выбором виджетов. Но никто не мешает создавать свои собственные.

Gtk, как известно, построена на принципах ООП, что хорошо ложится на объектную модель Python. В данном случае это означает, что наследование виджетов естественным образом соответствует наследованию классов в Питоне. Так, создав класс-потомок gtk.VBox, мы получим виджет со всеми свойствами VBox, и сможем добавлять в него нужную функциональность.

Покажу простейший пример. Пусть мы хотим создать виджет, выглядящий как комбинация gtk.Label и gtk.Entry, т.е. поле для ввода сразу с подписью. Чтобы сделать такое непосредственно средствами gtk, нужно создать gtk.HBox, а в него поместить Label и Entry. Т.е. HBox окажется родительским виджетом для всей конструкции. Вот от него и будем наследоваться:

class LabeledEntry(gtk.HBox):

Но наш виджет довольно сильно отличается от простого HBox, поэтому нужно переопределить инициализатор:


  def __init__(self,label=None):
gtk.HBox.__init__(self) # Вызываем инициализатор родительского класса
self.label = gtk.Label(label) # Создаём текстовую метку с нужной подписью
self.entry = gtk.Entry() # И поле для ввода текста
self.pack_start(self.label, expand=False) # Добавляем label в создаваемый виджет
self.pack_start(self.entry, expand=True) # Поле для ввода - туда же

Теперь можно дописывать методы по собственному усмотрению. Например, логично было бы видеть методы set_text и get_text:

  def get_text(self):
return self.entry.get_text()
  def set_text(self,text):
self.entry.set_text(text)

При желании можно добавить, например, get_label и set_label. Пример использования нашего виджета:

...
entry = LabeledEntry("Enter some text")
...

Таким образом, наследуясь от HBox или VBox, можно создавать виджеты, состоящие из нескольких готовых. Но иногда нужны виджеты, внешне не похожие ни на один из стандартных. И вот тогда выручает то, что все виджеты gtk отрисовываются с помощью Cairo, который имеет весьма простой API.

API этот имеет много общего со многими другими рисовальными API. Прежде всего, нужно получить контекст Cairo - объект, содержащий состояние изображения. Далее для собственно рисования вызываются методы этого объекта. Наиболее часто используемые:

  • cr.move_to(x,y) - переместить графический указатель в нужную точку холста,

  • cr.line_to(x,y) - провести линию от положения указателя до данной точки (указатель сдвинется в указанную точку),

  • cr.path_close() - делает текущую линию замкнутой,

  • cr.rectangle(x,y,w,h) - рисует прямоугольник; задаются координаты левого верхнего угла и размеры,

  • cr.set_source_rgb(r,g,b) - выбрать цвет для рисования; компоненты r,g,b измеряются от 0 до 1,

  • cr.stroke() - нарисовать контур текущей линии (выбранным цветом),

  • cr.fill() - закрасить текущую линию.

Координаты измеряются как обычно - от левого верхнего угла вправо и вниз, в пикселах.

Пусть нам, скажем, нужен виджет, который будет отображать простейшие линейные диаграммы. Должна быть возможность добавлять в него данные, а он должен соответственно перерисовывать диаграмму. Такие виджеты удобнее всего наследовать от gtk.DrawingArea:

  class Diagram(gtk.DrawingArea):

Сам виджет DrawingArea выглядит как белый прямоугольник. И на нём, в соответствии с названием, можно рисовать. Пока сделаем инициализацию нашего виджета:

  def __init__(self,max=10,color=(0.8,0.8,0.6)):
gtk.DrawingArea.__init__(self)
self.data = [1] # Это будут данные, отображаемые виджетом
self.max = max # Сколько максимум данных будет рисовать виджет
self.color = color # Цвет диаграммы
# Вот это, можно сказать, самое главное: привязываем рисующую процедуру к событию перерисовки виджета
self.connect('expose-event', self.on_expose)

Определяем собственно метод, который будет отрисовывать виджет:

  def on_expose(self, widget, event):

В аргументе widget передаётся сам виджет. Первое, что нам от него нужно - это размеры и положение:

    x,y, width,height,_ = widget.window.get_geometry()

Кроме того, нам понадобится контекст Cairo:

    cr = widget.window.cairo_create()

Вычислим некоторые размеры:

    xpad = 0.03*self.width           # Поля по горизонтали
ypad = 0.07*self.height # И по вертикали
w = float(self.width-2*xpad) # Ширина 'рабочей' части виджета
h = float(self.height-2*ypad) # и высота
M = max(self.data) # Максимум данных - он нужен, чтобы выставить масштаб по оси Y
n = len(self.data) # Количество данных
    cr.rectangle(0,0,self.width,self.height)   # Обозначаем прямоугольник, закрывающий весь виджет
cr.set_source_rgb(1,1,1) # Выбираем белый цвет
cr.fill() # Закрашиваем наш прямоугольник - это будет фон
    cr.move_to(xpad, ypad+h-h*float(self.data[0])/M)  # Ставим указатель в верхний левый угол будущей диаграммы
for x,y in enumerate(self.data[1:]): # Пробегаемся по всем данным
cr.line_to(xpad+w*float(x+1)/(n-1), ypad+h-h*float(y)/M) # Проводим очередной отрезок ломанной
cr.line_to(xpad+w, ypad+h) # Проводим правую границу диаграммы
cr.line_to(xpad,ypad+h) # Теперь нижнюю границу
cr.close_path() # Замыкаем ломанную - это проведёт левую границу диаграммы
cr.set_source_rgb(*self.color) # Выбираем цвет
cr.fill() # Закрашиваем ломанную

Этот метод будет вызываться каждый раз, когда нужно перерисовать виджет. Конечно, стоит иметь ввиду, что если он будет выполняться долго - перерисовка виджета будет тормозить. Так что вычислений и циклов в нём должно быть минимум. Всё, что можно, следует вычислять заранее, или кэшировать.

Ну и допишем метод для добавления данных в диаграмму:

def accept(self,n):
if len(self.data) == self.max:
del self.data[0] # Если данных слишком много - забываем самое старое значение
self.data.append(float(n)) # Добавляем число в список
self.queue_draw() # Этот вызов заставит виджет перерисоваться, т.е. лишний раз вызовет on_expose().

Пример использования:

...
dg = Diagram(max=20)
...
dg.accept(10)
dg.accept(20)
...

понедельник, ноября 10, 2008

Typewriter-like раскладки

По совету http://vonderer.blogspot.com/, решил попробовать использовать для русского языка раскладку пишущей машинки. Главное преимущество (для меня) - в том, что знаки препинания обычно в тексте встречаются гораздо чаще, чем цифры, а в typewriter набирать их становится проще. Заодно точка и запятая получают по отдельной клавише, и буква Ё - более удобное место. Вобщем, действительно, удобно. Правда, на привыкание ушло около недели.

Но кроме русской раскладки есть ещё и английская. Для неё в X-ах не предусмотрено tyewriter-варианта, а хочется, потому что на переключение режима в мозгах требуется слишком много времени (цифры набирать то с шифтом, то без, и знаки препинания скачут по всей клавиатуре). Раскладка Дворака (у которой есть вариант с цифрами на верхнем уровне) - слишком другая, а я не так много набираю англоязычных текстов, чтобы изучать совсем новую раскладку (да ещё и надписи на клавишах будут мешать). Вобщем, сделал я себе typewriter-вариант английской раскладки. Выглядит это так:



Соответствующий код (/usr/share/X11/xkb/symbols/ustw):

partial alphanumeric_keys
xkb_symbols "typewriter" {
include "us(basic)"
name[Group1]= "US - Typewriter";
key <AE01> { [exclam, 1 ] };
key <AE02> { [numbersign,2 ] };
key <AE03> { [slash, 3 ] };
key <AE04> { [semicolon, 4 ] };
key <AE05> { [colon, 5 ] };
key <AE06> { [comma, 6 ] };
key <AE07> { [period, 7 ] };
key <AE08> { [asterisk, 8 ] };
key <AE09> { [question, 9 ] };
key <AE10> { [percent, 0 ] };
key <BKSL> { [parenleft, parenright ] };

key <AC10> { [at, ampersand ] };
key <AB08> { [asciicircum, less ] };
key <AB09> { [dollar, greater ] };
key <AB10> { [bar, backslash ] };
};


Кроме того, ещё с давних пор я использую CapsLock как специальный модификатор, превращающий некоторые буквенные клавиши в стрелки итп. Сейчас ещё захотелось на Shift-Caps повесить переключение такого режима (чтоб в браузере тексты читать, листая кнопками j/k, итп). И ещё захотелось временный переключатель из русской раскладки в английскую - иногда >/< или ещё чего набрать быстро. И, раз уж пошла такая пьянка, чтоб можно было греческие буквы побыстрее набирать (временный переключатель в греческую раскладку) (правда, я не верю, что греки пользуются фонетической раскладкой, которая в иксах под именем gr, ну да это их проблемы).

Итак, текущие мои настройки, если кому интересно.

/usr/share/X11/xkb/symbols/addkeys - мои раскладки:


partial alphanumeric_keys
xkb_symbols "en" {
include "ustw"
name[Group1]= "US - Additional";
key.type[group1]="FOUR_LEVEL";
key <AC01> { [ a, A, Home, Home ] };
key <AD03> { [ e, E, End, End ] };
key <AC05> { [ g, G, Home, End ] };
key <AC06> { [ h, H, Left, Left ] };
key <AC07> { [ j, J, Down, Down ] };
key <AC08> { [ k, K, Up, Up ] };
key <AC09> { [ l, L, Right, Right ] };
key <AC03> { [ d, D, Delete, Delete ] };
key <AD10> { [ p, P, XF86ScrollUp, XF86ScrollUp ] };
key <AB06> { [ n, N, XF86ScrollDown, XF86ScrollDown ] };
include "addkeys(caps_switch)"
};

partial alphanumeric_keys
xkb_symbols "ru" {
include "ru(typewriter)"
name[Group1]= "Russia - Additional";
key.type[group1]="FOUR_LEVEL";
key <AC01> { [ Cyrillic_ef, Cyrillic_EF, Home, Home ] };
key <AD03> { [ Cyrillic_u, Cyrillic_U, End, End ] };
key <AC05> { [ Cyrillic_pe, Cyrillic_PE, Home, End ] };
key <AC06> { [ Cyrillic_er, Cyrillic_ER, Left, Left ] };
key <AC07> { [ Cyrillic_o, Cyrillic_O, Down, Down ] };
key <AC08> { [ Cyrillic_el, Cyrillic_EL, Up, Up ] };
key <AC09> { [ Cyrillic_de, Cyrillic_DE, Right, Right ] };
key <AE11> { [ minus, underscore, emdash, hyphen ] };
key <AE12> { [ equal, plus, notequal, plusminus ] };
key <AC03> { [ Cyrillic_ve, Cyrillic_VE, Delete, Delete ] };
key <AD11> { [ Cyrillic_ha, Cyrillic_HA, bracketleft, braceleft ] };
key <AD12> { [Cyrillic_hardsign,Cyrillic_HARDSIGN, bracketright, braceright ] };
key <AD10> { [ Cyrillic_ze, Cyrillic_ZE, XF86ScrollUp, XF86ScrollUp ] };
key <AB06> { [ Cyrillic_te, Cyrillic_TE, XF86ScrollDown, XF86ScrollDown ] };
include "addkeys(caps_switch)"
};

partial alphanumeric_keys
xkb_symbols "gr" {
include "gr"
name[Group1]= "Greek - Additional";
key.type[group1]="FOUR_LEVEL";
key <AC01> { [ Greek_alpha, Greek_ALPHA, Home, Home ] };
key <AD03> { [ Greek_epsilon, Greek_EPSILON, End, End ] };
key <AC05> { [ Greek_gamma, Greek_GAMMA, Home, End ] };
key <AC06> { [ Greek_eta, Greek_ETA, Left, Left ] };
key <AC07> { [ Greek_xi, Greek_XI, Down, Down ] };
key <AC08> { [ Greek_kappa, Greek_KAPPA, Up, Up ] };
key <AC09> { [ Greek_lambda, Greek_LAMBDA, Right, Right ] };
key <AC03> { [ Greek_delta, Greek_DELTA, Delete, Delete ] };
include "addkeys(caps_switch)"
};

xkb_symbols "caps_switch" {
key <CAPS> {
type[Group1]="ONE_LEVEL",
symbols[Group1] = [ ISO_Level3_Shift ]
};
modifier_map Mod5 { ISO_Level3_Shift };

replace key <II65> {
type[Group1]="ONE_LEVEL",
actions[Group1] = [ SetGroup(group=3) ],
actions[Group2] = [ SetGroup(group=3) ],
actions[Group3] = [ ],
actions[Group4] = [ SetGroup(group=3) ]
};

replace key <I21> {
type[Group1]="ONE_LEVEL",
type[Group2]="ONE_LEVEL",
actions[Group1] = [ SetGroup(group=1) ],
actions[Group2] = [ SetGroup(group=1) ],
actions[Group3] = [ SetGroup(group=1) ],
actions[Group4] = [ ]
};

replace key <RCTL> {
actions[Group1] = [ SetGroup(group=2) ],
actions[Group2] = [ SetGroup(group=1) ],
actions[Group3] = [ SetGroup(group=1) ],
actions[Group4] = [ ],
locks = yes
};
};


Ну и в /etc/X11/xorg.conf:


Option "XkbLayout" "addkeys(en),addkeys(ru),gr"
Option "XkbOptions" "grp_led:caps,compose:ralt"



Рус/лат переключается правым Ctrl, индикация лампочкой Caps. Временный переключатель в английскую раскладку на клавише <I21> (у меня она рядом с левым Ctrl). На клавише <II65> (у меня над <I21>) - временный переключатель в третью раскладку (греческие буквы иногда набрать). По Caps+буква - некоторые спецклавиши: Caps-hjkl - стрелки, Caps-a - Home, Caps-e - End, Caps-g - Home, Caps-G - End. На правом Alt - Compose.