В последнее время медленно и неторопливо разрабатываю одну тулзу и соответственно графический фронт-енд к ней. Тулза это слегка рисовательного характера, поэтому и компонент для рисования пришлось писать самому. Для рисования я использовал 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
VIDEO
Скачать пример можно тут: pygtk-cairo.zip (20,8 Кб)
Ссылки
http://www.tortall.net/mu/wiki/CairoTutorial