Pull to refresh

Пример использования WxPython для создания нодового интерфейса. Часть 1: Учимся рисовать

Reading time 5 min
Views 19K
В небольшом цикле статей будет описано использование WxPython для решения вполне конкретной задачи по разработке пользовательского интерфейса, да еще и то, как сделать это решение универсальным. Туториал этот расчитан на тех, кто уже начал изучать эту библиотеку и хочет увидеть что-то более сложное и целостное, чем простейшие примеры (хотя начнется все с относительно простых вещей).

А начиналось все так: понадобилось мне для одного проекта сделать UI, где надо последовательность обработки сообщений редактировать. Что-то наподобии Simulink'а. Соответственно, полез искать готовые либы/фреймворки. Поначалу подумал, что задачка популярная и кто-нибудь уже сделал это велосипед, поискал, поискал и… не нашел. Точнее нашел много антикварных велосипедов, но кто же будет пользоваться чужим старым велосипедом, если можно сделать свой новый. Но раз уж делать новый велосипед, почему бы не сделать его универсальным, мало ли, где еще пригодится.

Так что попробую в нескольких статья описать процесс разработки с нуля до работающего примера. Ну и чтобы было интересно, а ферймворк был универсален, первая задача для него будет не подобие Simulink'а, а софтина для рисования блок-схем а-ля Visio, но со своим блек-джеком и остальными участниками:)

Часть 1: Учимся рисовать
Часть 2: Обработка событий мыши
Часть 3: Продолжаем добавлять фичи + обработка клавиатуры
Часть 4: Реализуем Drag&Drop
Часть 5: Соединяем ноды

Кому интересно, добро пожаловать под кат…


Маленький комментарий: я буду использовать английские имена в коде и часто буду использовать транслитерацию, т.е. вместо того, чтобы писать «canvas» или «холст», я буду писать «канвас». Да, я знаю, что это может выглядеть плохо и не правильно, но идеального способа смешивать русский и английский не существует, у всех способов есть свои недостатки (мое личное мнение).

1. Первый тест


Начнем с простых банальных вещей. Раз это фреймворк, значит кто-то будет на его основе делать приложения или части приложений, которые будут делать. Т.е. для простейшего теста, нам нужно изготовить простейшее приложение. Не сильно долго думая, я решил, что буду рисовать соединенные друг с другом прямоугольники и начал с такого кода (он будет жить в файле «ConnectedBoxes.py»):
import wx
from MoveMe.Canvas.Canvas import Canvas

class CanvasWindow(wx.Frame):
    def __init__(self, *args, **kw):
        wx.Frame.__init__(self, *args, **kw)
        s = wx.BoxSizer(wx.VERTICAL)
        s.Add(Canvas(self), 1, wx.EXPAND)
        self.SetSizer(s)

if __name__ == '__main__':
    app = wx.PySimpleApp()
    CanvasWindow(None).Show()
    app.MainLoop()

Тут все достаточно тривиально и очевидно, кроме пары моментов: MoveMe — это имя нашего фреймворка, а Canvas — это главный класс нашего фреймворка, отвечающий за рендеринг всего этого дела.

2. Учимся рисовать


Собственно с канваса и начинается наш фреймворк. Он отвечает за хранение объектов (будем называть их нодами), их рендеринг и обработку взаимодействия с пользователем. Соответственно, начнем с простенького рисования.
import wx

class Canvas(wx.PyScrolledWindow):
    """
    Canvas stores and renders all nodes and node connections.
    It also handles all user interaction.
    """
    def __init__(self, *args, **kw):
        super(Canvas, self).__init__(*args, **kw)
        self.scrollStep = kw.get("scrollStep", 10)
        self.canvasDimensions = kw.get("canvasDimensions", [800, 800])
        self.SetScrollbars(self.scrollStep, 
                           self.scrollStep, 
                           self.canvasDimensions[0]/self.scrollStep, 
                           self.canvasDimensions[1]/self.scrollStep)

        self._dcBuffer = wx.EmptyBitmap(*self.canvasDimensions)
        self.Render()
        self.Bind(wx.EVT_PAINT, 
                  lambda evt: wx.BufferedPaintDC(self, self._dcBuffer, wx.BUFFER_VIRTUAL_AREA)
                  )

    def Render(self):
        """Render all nodes and their connection in depth order."""
        cdc = wx.ClientDC(self)
        self.PrepareDC(cdc)
        dc = wx.BufferedDC(cdc, self._dcBuffer)
        dc.Clear()
        gc = wx.GraphicsContext.Create(dc)
        
        gc.SetPen(wx.Pen('#000000', 2, wx.SOLID))
        gc.DrawRoundedRectangle(12, 34, 56, 78, 10)
        gc.DrawRoundedRectangle(112, 134, 156, 178, 10)

Тут все становится немного интереснее:
  • Во-первых мы наследуем «wx.PyScrolledWindow» чтобы наше окно можно было скролить и задаем параметры скрола в «self.SetScrollbars».
  • Во-вторых, рисовать мы будем не напрямую, а в буфер, чтобы все это происходило быстрее и без мерцаний. Для этого используется «wx.BufferedDC» и буфер, который является битмэпой.
  • Ну и в-третьих, мы будем использовать «wx.GraphicsContext» для удобного сохранения состояния. Оно имеет методы «PushState» и «PopState», которые сохраняют настройки кистей, шрифтов, итд итп, что особенно полезно, так как рисовать блоки на экране будет пользовательский код и никто не гаррантирует, что пользователь вернет все на место.
Если запустить код сейчас, то мы сможем увидеть пару айфоновпрямоугольников с закругленными углами в окошке, которое можно скролить.


3. Упорядочиваем сцену


С простейшим рисованием разобрались, пора как-то упорядочить этот процесс и ввести понятие объекта на сцене. Все видимые объекты будут храниться в списке, который и будет определять порядок расположения объектов. Для поддержки этого, мы добавим поле "_canvasObjects" в класс канваса и немного изменим процесс рендеринга, т.е. вместо рисования напрямую, мы будем вызывать метод «Render» всех объектов на сцене. Теперь код выглядит так:
import wx
from MoveMe.Canvas.Objects.SimpleBoxNode import SimpleBoxNode

class Canvas(wx.PyScrolledWindow):
    """
    Canvas stores and renders all nodes and node connections.
    It also handles all user interaction.
    """
    def __init__(self, *args, **kw):
        super(Canvas, self).__init__(*args, **kw)
        self.scrollStep = kw.get("scrollStep", 10)
        self.canvasDimensions = kw.get("canvasDimensions", [800, 800])
        self.SetScrollbars(self.scrollStep, 
                           self.scrollStep, 
                           self.canvasDimensions[0]/self.scrollStep, 
                           self.canvasDimensions[1]/self.scrollStep)
        
        self._canvasObjects = [SimpleBoxNode([20,20]), SimpleBoxNode([140,40]), SimpleBoxNode([60,120])]

        self._dcBuffer = wx.EmptyBitmap(*self.canvasDimensions)
        self.Render()
        self.Bind(wx.EVT_PAINT, 
                  lambda evt: wx.BufferedPaintDC(self, self._dcBuffer, wx.BUFFER_VIRTUAL_AREA)
                  )

    def Render(self):
        """Render all nodes and their connection in depth order."""
        cdc = wx.ClientDC(self)
        self.PrepareDC(cdc)
        dc = wx.BufferedDC(cdc, self._dcBuffer)
        dc.Clear()
        gc = wx.GraphicsContext.Create(dc)
        
        for obj in self._canvasObjects:
            gc.PushState()
            obj.Render(gc)
            gc.PopState()

Кстати список объектов сцены сразу содержит несколько объектов класса «SimpleBoxNode», который пока что будет просто рисовать прямоугольники, с координатами объектов.
import wx

class SimpleBoxNode(object):
    """
    SimpleBoxNode class represents a simplest possible canvas object 
    that is basically a rectangular box.
    """
    def __init__(self, pos):
        self.position = pos
        self.boundingBoxDimensions = [90, 60]

    def Render(self, gc):
        gc.SetPen(wx.Pen('#000000', 2, wx.SOLID))
        gc.DrawRoundedRectangle(self.position[0], 
                                self.position[1], 
                                self.boundingBoxDimensions[0], 
                                self.boundingBoxDimensions[1], 10)
        gc.SetFont(wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT))
        gc.DrawText("(%d, %d)"%(self.position[0], self.position[1]), self.position[0]+10, self.position[1]+10)

Тут вроде все достаточно тривиально. Разве что надо обязательно указывать шрифт, так как «GraphicsContext» не имеет настроек по умолчанию (к этому факту и его исправлению мы еще вернемся). На данный момент, наш код рисует вот такую картинку:

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

Продолжение следует…

PS1: Код можно найти на GitHub'е
PS2: Об опечатках пишите в личку.
Tags:
Hubs:
+15
Comments 3
Comments Comments 3

Articles