Основы работы с графикой

Пришло время поговорить о графических возможностях пакета wxPython и посмотреть как выполняется рисование графических примитивов в оконном интерфейсе программы.

Сам процесс рисования осуществляется через контекст устройства (device context), сокращенно по-английски DC. И wxPython поддерживает следующие основные их разновидности:

  • wx.MemoryDC – контекст памяти (рисование в объекте Bitmap, расположенном в памяти устройства);
  • wx.PrinterDC – контекст принтера (для ОС Windows и Mac);
  • wx.ScreenDC – контекст экрана устройства (рисование на экране без привязки к окну);
  • wx.ClientDC, wx.PaintDC – рисование в клиентской области окна;
  • wx.WindowDC – рисование во всем окне.

Здесь оба контекста wx.ClientDC, wx.PaintDC выполняют рисование в клиентской области окна, но, тем не менее, между ними есть принципиальная разница, о которой я скажу чуть позже.

Давайте для начала нарисуем в окне линию. В самом простом случае это можно сделать так:

import wx
 
class MyFrame(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title=title, pos=(0,0), size=(700, 400))
 
        wx.CallLater(100, self.onDraw)
 
    def onDraw(self):
        dc = wx.ClientDC(self)
        dc.DrawLine(0,0, 200, 100)
 
app = wx.App()
frame = MyFrame(None, 'wxPython')
frame.Show()
app.MainLoop()

Смотрите, мы в конструкторе MyFrame вызываем метод onDraw через 100 миллисекунд и в этом методе получаем контекст для клиентской области окна с помощью метода ClientDC и рисуем линию в этом контексте, то есть, в клиентской области. После запуска программы увидим следующее:

Здесь сразу возникает вопрос: зачем нужно было делать эту задержку на 100 миллисекунд? Дело в том, что пока мы находимся в конструкторе оконного класса, контекст для него не может быть получен, его еще пока попросту нет. Он будет доступен только при отображении окна на экране. Поэтому нам и нужна здесь эта задержка, чтобы окно успело появиться. Но, как вы понимаете, задержка – это плохая практика программирования: мы ее никогда не сможем точно подобрать, чтобы линия отображалась точно в момент показа окна. И, кроме того, если сместить это окно за пределы экрана, а затем, опять вернуть, то линия перерисовываться не будет. ОС не берут на себя такую функциональности и предоставляют самому приложению перерисовывать фрагменты интерфейса и другую графическую информацию. Но тогда как узнать: в какой момент выполнять эту перерисовку? Для этого в приложении на wxPython генерируется специальное событие

EVT_PAINT

которое и сигнализирует о необходимости перерисовать содержимое окна. То есть, в нашей программе вместо задержки нужно повесить обработчик на это событие:

self.Bind(wx.EVT_PAINT, self.OnPaint)

И уже в нем реализовывать всю прорисовку:

    def OnPaint(self, e):
        dc = wx.PaintDC(self)
        dc.DrawLine(50, 60, 190, 60)

Причем, мы используем контекст PaintDC, а не ClientDC, так как в момент возникновения события EVT_PAINT контекст PaintDC создается автоматически и это заметно ускоряет работу с графикой (здесь не тратится время на создание и удаление контекста устройства).

Теперь, при запуске программы мы можем совершенно спокойно перемещать окно и линия не будет пропадать. Итак, чтобы что-либо нарисовать непосредственно в окне, мы должны получить контекст DC для рисования и уже через него выполнять рисование изображений, текста и графических примитивов. Это основная логика работы с графикой в оконном интерфейсе.

wx.Pen

Для рисования графических примитивов с разными цветами линий используется класс Pen, который имеет следующий синтаксис:

wx.Pen(wx.Colour colour, width=1, style=wx.SOLID)

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

  • wx.SOLID – сплошная линия;
  • wx.DOT – линия из точек;
  • wx.LONG_DASH – линия из длинных отрезков;
  • wx.SHORT_DASH – линия из коротких отрезков;
  • wx.DOT_DASH – черточка-точка;
  • wx.TRANSPARENT – прозрачная линия.

В самом простом случае мы можем его использовать так:

dc.SetPen(wx.Pen('#fdc073'))
 
dc.DrawLine(0, 0, 200, 100)
dc.DrawRectangle(300, 10, 200, 100)

Мы здесь для контекста dc указали свой собственный объект Pen. В результате получили оранжевый цвет линии.

Теперь пару слов о цвете. Его можно определить или в виде строки формата

#RRGGBB

где RR, GG, BB – красная, зеленая и синяя компоненты цвета в шестнадцатиричной записи. Их комбинации определяют все возможные цвета. Также цвет можно задать через предопределенные константы, например:

  • wx.BLACK – черный;
  • wx.BLUE – синий;
  • wx.GREEN – зеленый;
  • wx.GREY – серый;
  • wx.RED – красный;
  • wx.YELLOW – желтый;
  • wx.WHITE – белый.

Полный их набор можно посмотреть на странице:

https://docs.wxpython.org/wx.ColourDatabase.html

Наконец, первым параметром в конструкторе класса Pen можно указать ссылку на объект wx.Colour, содержащий цвет. Но, чаще всего записывают строку в шестнадцатиричном формате.

Давайте для примера создадим класс Pen с синей линией толщиной 5 пикселей и стилем LONG_DASH:

dc.SetPen(wx.Pen(wx.BLUE, 5, wx.LONG_DASH))

При запуске программы увидим такую картину:

wx.Brush

Этот класс (Brush – кисть в переводе с англ.) определяет цвет заливки графических примитивов. Его конструктор, следующий:

Brush(colour, style=BRUSHSTYLE_SOLID)

Первый параметр также является цветом, а второй определяет стиль заливки:

  • wx.SOLID – непрерывная заливка;
  • wx.BDIAGONAL_HATCH – диагональная штриховка;
  • wx.CROSSDIAG_HATCH – диагональная штриховка внахлест;
  • wx.FDIAGONAL_HATCH – обратная диагональная штриховка;
  • wx.CROSS_HATCH – штриховка сеточкой;
  • wx.HORIZONTAL_HATCH – горизонтальная штриховка;
  • wx.VERTICAL_HATCH – вертикальная штриховка;
  • wx.TRANSPARENT – прозрачная заливка.

В качестве примера реализуем такие стили заливки:

    def OnPaint(self, e):
        dc = wx.PaintDC(self)
        dc.SetPen(wx.Pen(wx.BLUE, 5, wx.LONG_DASH))
 
        dc.DrawLine(0, 0, 200, 100)
 
        dc.SetBrush(wx.Brush('#3F4137', wx.BDIAGONAL_HATCH))
        dc.DrawRectangle(300, 10, 200, 100)
 
        dc.SetBrush(wx.Brush(wx.YELLOW, wx.CROSSDIAG_HATCH))
        dc.DrawRectangle(10, 150, 100, 100)
 
        dc.SetBrush(wx.Brush(wx.GREEN, wx.CROSS_HATCH))
        dc.DrawRectangle(200, 150, 100, 100)
 
        dc.SetBrush(wx.Brush(wx.RED, wx.VERTICAL_HATCH))
        dc.DrawRectangle(400, 150, 100, 100)

Дополнительные параметры при рисовании линий

Объект Pen дополнительно может оперировать двумя атрибутами:

  • join – способ сопряжения линий в точке соединения;
  • cap – способ прорисовки конца линии;

через методы SetJoin и SetCap. Первый метод принимает константы:

  • wx.JOIN_MITER – обычное сопряжение (значение по умолчанию);
  • wx.JOIN_BEVEL – создание фаски;
  • wx.JOIN_ROUND – создание скругления.

А второй:

  • wx.CAP_ROUND – скругленный конец линии;
  • wx.CAP_PROJECTING – продолжает линию за граничную точку на величину ее половинной толщины;
  • wx.CAP_BUTT – рисует линию до конечной точки (не переходя ее).

В качестве примера добавим в нашу программу вызовы этих методов. Вначале создадим отдельно объект класса Pen и установим его для контекста:

pen = wx.Pen(wx.BLUE, 10)
dc.SetPen(pen)

Далее, перед рисованием прямоугольника запишем такие строчки:

pen.SetJoin(wx.JOIN_BEVEL)
dc.SetPen(pen)

То есть, мы меняем инструмент Pen и снова передаем его контексту, обновляя его. Теперь, при запуске программы наши прямоугольники будут иметь скошенные углы. Причем, этот эффект будет наблюдаться, если толщина линии больше 1. А вот параметр JOIN_ROUND в моем случае не сработал?!

Для демонстрации эффекта параметра cap, нарисуем следующие линии:

    def OnPaint(self, e):
        dc = wx.PaintDC(self)
        pen = wx.Pen(wx.BLUE, 10)
 
        pen.SetCap(wx.CAP_BUTT)
        dc.SetPen(pen)
        dc.DrawLine(30, 150,  150, 150)
 
        pen.SetCap(wx.CAP_PROJECTING)
        dc.SetPen(pen)
        dc.DrawLine(30, 190,  150, 190)
 
        pen.SetCap(wx.CAP_ROUND)
        dc.SetPen(pen)
        dc.DrawLine(30, 230,  150, 230)
 
        pen2 = wx.Pen('#4c4c4c', 1, wx.SOLID)
        dc.SetPen(pen2)
        dc.DrawLine(30, 130, 30, 250)
        dc.DrawLine(150, 130, 150, 250)
        dc.DrawLine(155, 130, 155, 250)

При запуске программы увидим такое изображение:

Пользовательская заливка

С помощью класса Brush можно создавать свои собственные виды заливок. Для этого нужно заранее создать изображение, например, такое (pattern1.png):

И, затем, указать его в качестве рисунка у кисти:

    def OnPaint(self, e):
        dc = wx.PaintDC(self)
        rect = self.GetClientRect()
        pen = wx.Pen(wx.BLUE, 0, wx.TRANSPARENT)
        dc.SetPen(pen)
 
        dc.SetBrush(wx.Brush(wx.Bitmap('pattern1.png')))
        dc.DrawRectangle(0, 0, rect.width, rect.height)

При запуске программы увидим такое окно:

Градиентная заливка

Наконец, можно создавать вот такие прямоугольные градиентные заливки:

Это делается с помощью инструмента:

GradientFillLinear(self, rect, initialColour, destColour, nDirection=RIGHT)

  • rect – прямоугольная область заливки;
  • initialColour – начальный цвет заливки;
  • destColour – конечный цвет заливки;
  • nDirection – направление градиента.

Изображение окна выше создается вот таким обработчиком:

    def OnPaint(self, e):
        dc = wx.PaintDC(self)
        dc.GradientFillLinear((10, 10, 600, 50), '#00cc00', '#444444', wx.NORTH)
        dc.GradientFillLinear((10, 80, 600, 50), '#0000cc', '#444444', wx.SOUTH)
        dc.GradientFillLinear((10, 140, 600, 50), '#cc0000', '#444444', wx.EAST)
        dc.GradientFillLinear((10, 200, 600, 50), '#ffccff', '#444444', wx.WEST)

Как видите, все довольно просто. Конечно, здесь может возникнуть вопрос: а как сделать градиентные заливки у произвольных фигур? Об этом мы еще поговорим, когда будем рассматривать инструмент Clipping – ограничение области рисования.

Видео по теме