null

Делаем свой виджет с помощью PyGTK и Cairo

В последнее время медленно и неторопливо разрабатываю одну тулзу и соответственно графический фронт-енд к ней. Тулза это слегка рисовательного характера, поэтому и компонент для рисования пришлось писать самому. Для рисования я использовал libcairo, а для самой тулзы - PyGTK (привязки GTK+ к Python), и в этой статье я расскажу как это все уживить и использовать на примере наипростейшего графического редактора.

Делаем виджет

Для того, чтобы создать свой виджет нужно унаследовать свой класс от одного потомков (или самого) класса gtk.Widget. В данном случае мы наследуемся от DrawingArea:
 

class MyDrawingArea(gtk.DrawingArea):
    def __init__(self):
        gtk.DrawingArea.__init__(self)
        
        self.bg_color = (1., 1., 1.)
        self.fg_color = (0., 0., 0.)
        
        self.tool = TOOL_SELECT
        
        # Устанавливаем маску интересущих нас событий
        self.add_events(gtk.gdk.BUTTON_PRESS_MASK |
                        gtk.gdk.BUTTON_RELEASE_MASK |  
                        gtk.gdk.POINTER_MOTION_MASK |
                        gtk.gdk.SCROLL_MASK)
        
        # Регистрируем обработчики событий
        self.connect("expose_event", self.expose)
        
        self.connect("button_press_event", self.on_button_press)
        self.connect("button_release_event", self.on_button_release)
        self.connect("motion_notify_event", self.on_motion)
        
        self.elements = []
        self.cur_element = None


Перерисовка выполняется по событию expose_event, поэтому зарегистрируем обработчик события:

self.connect("expose_event", self.expose)


В самом обработчике располагается следующий код:

def expose(self, widget, event):
        # Создаем графический контекст Cairo
        cr = widget.window.cairo_create()
        
        # Получаем координаты и размеры компонента
        self.rect = self.get_allocation()
        
        # Обрезаем область рисования для размеров, полученных в event.area
        # чтобы исключить излишнии отрисовки
        cr.rectangle(event.area.x, event.area.y, 
                     event.area.width, event.area.height)
        cr.clip()
        
        # Рисуем большой белый прямоугольник (очищаем область рисования)
        cr.set_source_rgb(1., 1., 1.)
        cr.rectangle(0., 0., self.rect.width, self.rect.height)
        cr.fill()
        
        # Последовательно рисуем элементы
        for el in self.elements:
            el.draw(cr)


Сигнал, соответствующий событию expose можно послать и самостоятельно (это нам потребуется при рисовании фигуры):

def redraw(self, area = None):
        expose_event = gtk.gdk.Event(gtk.gdk.EXPOSE)
        expose_event.window = self.window
        
        if area is not None:
            # Перерисовываем только требуемую область
            expose_event.area = area
        else:
            # Перерисовываем компонент целиком
            rect = self.get_allocation()
            expose_event.area = gtk.gdk.Rectangle(0, 0, rect.width, rect.height)
        
        self.send_expose(expose_event)


Здесь area - тип класса gtk.gdk.Rectangle (эта область нами используется для того, чтобы перерисовывать не целиком виджет, а только его часть).

Создание фигур мы будем осуществлять по левой кнопке мыши, а удаление по правой:

def on_button_press(self, widget, event):
        if event.button == 1:
            # Создание фигур
        elif event.button == 3:
            # Удаление фигур


В свою очередь при нажатии клавиши Shift необходимо чтобы ширина и высота фигур была одинаковой:

def on_motion(self, widget, event):
        if self.cur_element is not None:
            w, h = event.x - self.start_x, event.y - self.start_y
            
            if event.state & gtk.gdk.SHIFT_MASK:
                w, h = (min(w, h), ) * 2
            # ...

Рисуем с помощью Cairo

Итак, графический контекст получен, можно рисовать на нем! Строго говоря, графический контекст не обязательно должен быть привязан к виджету, с помощью cairo можно например визуализировать данные и сохранять в png-файлы.

Цвета и стили

Текущий цвет рисования задается с помощью функций set_source_rgb/set_source_rgba, которые принимают текущий RGB-цвет (вторая функция с alpha-каналом):
    cr.set_source_rgb(0.1, 0.1, 0.9)

Толщина линий задается с помощью функции set_line_width:
    cr.set_line_width(1.5)

Пунктирной линию можно сделать с помощью set_dash:
    cr.set_dash((10, 2), 1.5)
Первое значение определяет список длин штрихов, второе - смещение рисунка.

Более сложным является заполнение с помощью градиента:

grad = cairo.LinearGradient(0, 0, 1, 1)
    grad.add_color_stop_rgb(0, *self.fg)
    grad.add_color_stop_rgb
    
    ...
    
    cr.set_source(grad)


Матрица трансформаций

Работа с ней похожа на работу с таковой в OpenGL:

cr.save() # Сохранение старой матрицы в стек
    
    # Выполняем трансформации:
    cr.translate(tx, ty)        # Перенос начала координат на точку (tx, ty)
    cr.scale(sx, sy)            # Масштабирование с коэффициентами (sx, sy)
    cr.rotate(angle)            # Поворот оси.
    
    # Здесь рисуем
    
    cr.restore() # Восстановление старой матрицы из стека


    
Следует заметить, что при использовании scale() масштабируется все вплоть до толщины линий. Для того чтобы указывать толщину линии с учетом матрицы трансформации можно воспользоваться функцией преобразования расстояния device_to_user_distance:
     cr.set_line_width(max(cr.device_to_user_distance(3, 3)))

Рисуем фигуры

Теперь можно приступить непосредственно к рисованию графических объектов. Для этого надо вызвать пару функций: первая (их может быть несколько в случае ломанных линий) задает путь (path), вторая же - действие над этим путем: stroke (обводка линией) и fill (соответственно, заливка):
    cr.rectangle(x, y, width, height)
    cr.fill()

Данный код соответственно приведет к заполнению прямоугольника.

Линия в Cairo строится с помощью методов line_to / move_to:
    cr.move_to(x1, y1)
   cr.line_to(x2, y2)

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

Дуга строится с помощью функции arc
    cr.arc(x, y, r, angle1, angle2)
Для рисования полного круга нужно использовать диапазон углов 0., 2 * math.pi . Эллипс получается с помощью неравномерного масштабирования по осям x и y. Более сложный вариант - это кривая Безье (рисуется с помощью curve_to/rel_curve_to).

Завершить рисование пути можно с помощью close_path().

Рисуем текст

Для рисования текста нужно сначала переместить "курсор" в заданную позицию с помощью move_to (одного только translate() недостаточно). Т.к. текст рисуется снизу вверх (точка, переданная move_to обозначает линию шрифта (base line), то нужно сместить курсор еще на высоту шрифта вниз
    cr.move_to(0.0, h)
    cr.set_font_size(h)
    cr.show_text('HELLO, WORLD!')

    
Получить размер текста можно с помощью функции text_extents:
    xbearing, ybearing, width, height, xadvance, yadvance = (
                    cr.text_extents(letter))


Начертание можно изменить с помощью select_font_face:
    cr.select_font_face("Purisa", cairo.FONT_SLANT_NORMAL,
        cairo.FONT_WEIGHT_NORMAL)

Рисуем SVG-картинку

Ну и напоследок - рисование SVG-картинок. Это позволяет сделать библиотека rsvg, которая тоже имеет привязки к Python. Прочитать svg-файл можно как по имени файла так из строки:  

if file_name is not None:
        svg = rsvg.Handle(file=file_name)
    else:
        svg = rsvg.Handle(data=data)



После этого нужно вызвать функцию render_cairo:
    svg.render_cairo(cr)

Результаты

В результате получился такой вот простенький редактор: http://www.youtube.com/watch?v=UdfgVjaje1U

Скачать пример можно тут: pygtk-cairo.zip (20,8 Кб)

Ссылки

http://www.tortall.net/mu/wiki/CairoTutorial

К списку статей

 

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

Основная сфера моей деятельности связана с поддержкой Solaris и оборудования Sun/Oracle, хотя в последнее время к ним прибавились технологии виртуализации (линейка Citrix Xen) и всякое разное от IBM - от xSeries до Power. Учусь на кафедре Вычислительной Техники НИУ ИТМО.

See you...out there!

http://www.facebook.com/profile.php?id=100001947776045
https://twitter.com/AnnoyingBugs