- PVSM.RU - https://www.pvsm.ru -
Медленно, но верно, я продолжаю делать серию туториалов о WxPython, где я хочу рассмотреть разработку ферймворка для создания нодового интерфейса с нуля и до чего-то вполне функционального и рабочего. В прошлых частях уже рассказано как добавлять ноды, в этой же части, мы их будем соединять, а на этой картинке показан результат, который мы в этой статье получим:
Еще не идеально, но уже вырисовывается что-то вполне полезное и рабочее.
Прошлые части живут тут:
Часть 1: Учимся рисовать [1]
Часть 2: Обработка событий мыши [2]
Часть 3: Продолжаем добавлять фичи + обработка клавиатуры [3]
Часть 4: Реализуем Drag&Drop [4]
В погоне за соединениями нод, мы начнем с ключевого компонента, класса соединения, который в простейшем виде выглядит так:
class Connection(CanvasObject):
def __init__(self, source, destination, **kwargs):
super(Connection, self).__init__(**kwargs)
self.source = source
self.destination = destination
def Render(self, gc):
gc.SetPen(wx.Pen('#000000', 1, wx.SOLID))
gc.DrawLines([self.source.position, self.destination.position])
def RenderHighlighting(self, gc):
return
def ReturnObjectUnderCursor(self, pos):
return None
Все просто и тривиально, у нас есть начальный и конечный объекты и мы просто рисуем линию между позициями этих объектов. Вместо остальных методов пока заглушки.
Теперь нам надо реализовать процесс соединения нод. Интерфейс пользователя будет простым: удерживая Shift, пользователь нажимает на исходную ноду и тянет соединение к конечной. Для реализации мы запомним исходный объект при нажатии на него, добавив в «OnMouseLeftDown» следующий код:
if evt.ShiftDown() and self._objectUnderCursor.connectableSource:
self._connectionStartObject = self._objectUnderCursor
При отпускании же кнопки, мы также проверим, чтобы объект под курсором мог принять входящее соединение и соединим их, если все хорошо. Для этого в начале «OnMouseLeftUp» мы добавим соответствующий код:
if (self._connectionStartObject
and self._objectUnderCursor
and self._connectionStartObject != self._objectUnderCursor
and self._objectUnderCursor.connectableDestination):
self.ConnectNodes(self._connectionStartObject, self._objectUnderCursor)
Метод «ConnectNodes» занимается созданием соединения и его регистрацией в обеих соединяемых нодах:
def ConnectNodes(self, source, destination):
newConnection = Connection(source, destination)
self._connectionStartObject.AddOutcomingConnection(newConnection)
self._objectUnderCursor.AddIncomingConnection(newConnection)
Осталось научить ноды быть соединяемыми. Для этого мы введем соответствующий интерфейс, да не один, а целых 3. «ConnectableObject» будет общим интерфейсом для объекта, который может быть соединен с другим объектом. В данном случае, ему необходимо предоставлять точку соединения и центр ноды (чуть позже, мы это будем использовать).
class ConnectableObject(CanvasObject):
def __init__(self, **kwargs):
super(ConnectableObject, self).__init__(**kwargs)
def GetConnectionPortForTargetPoint(self, targetPoint):
"""
GetConnectionPortForTargetPoint method should return an end
point position for a connection object.
"""
raise NotImplementedError()
def GetCenter(self):
"""
GetCenter method should return a center of this object.
It is used during a connection process as a preview of a future connection.
"""
raise NotImplementedError()
Также мы наследуюем от «ConnectableObject» два класс для объектов подходящих для входящих и исходящих соединений:
class ConnectableDestination(ConnectableObject):
def __init__(self, **kwargs):
super(ConnectableDestination, self).__init__(**kwargs)
self.connectableDestination = True
self._incomingConnections = []
def AddIncomingConnection(self, connection):
self._incomingConnections.append(connection)
def DeleteIncomingConnection(self, connection):
self._incomingConnections.remove(connection)
class ConnectableSource(ConnectableObject):
def __init__(self, **kwargs):
super(ConnectableSource, self).__init__(**kwargs)
self.connectableSource = True
self._outcomingConnections = []
def AddOutcomingConnection(self, connection):
self._outcomingConnections.append(connection)
def DeleteOutcomingConnection(self, connection):
self._outcomingConnections.remove(connection)
def GetOutcomingConnections(self):
return self._outcomingConnections
Оба эти класса весьма похожи и позволяют хранить списки входящих и исходящих соединений соответственно. Плюс они устанавливают соответствующие флаги, чтобы канвас знал о том, что такой-то объект может быть соединен.
Остался последний шаг: немного модифицировать нашу ноду, добавив в ее родители соответствующие базовые классы и модифицировать процесс рендеринга. С рендерингом все интересно, можно хранить ноды в канвасе и там же их рендерить, а можно возложить эту задачу на ноду и заставить ее рендерить исходящие соединения. Это мы и сделаем, добавив в код рендеринг ноды вот такой код:
for connection in self.GetOutcomingConnections():
connection.Render(gc)
Итак, если запустить это дело и немного поиграться, то можно получить что-то вроде этого:
Не сильно красиво, но уже функционально:) Текущая версия кода живет тут [5].
Линии, соединяющие углы нод — это хорошо для теста, но не очень красиво и эстетично. Ну да не страшно, сейчас мы сделаем красивые и эстетичные стрелочки. Для начала, нам понадобится метод рисования стрелочек, который я быстренько написал, вспомнив школьную геометрию и использую NumPy:
def RenderArrow(self, gc, sourcePoint, destinationPoint):
gc.DrawLines([sourcePoint, destinationPoint])
#Draw arrow
p0 = np.array(sourcePoint)
p1 = np.array(destinationPoint)
dp = p0-p1
l = np.linalg.norm(dp)
dp = dp / l
n = np.array([-dp[1], dp[0]])
neck = p1 + self.arrowLength*dp
lp = neck + n*self.arrowWidth
rp = neck - n*self.arrowWidth
gc.DrawLines([lp, destinationPoint])
gc.DrawLines([rp, destinationPoint])
Мы тут отсчитываем «self.arrowLength» от конца стрелочки к началу и затем двигаемся в обе стороны по нормали на расстояние «self.arrowWidth». Так мы находим точки концов отрезков, соединяющих конец стрелочки с… не знаю как это назвать, с концами острия что ли.
Осталось в методе рендеринга заменить рисование линии на рисование стрелочки и можно будет созерцать такую картину:
Код живе тут [6].
Выглядит уже лучше, но еще не совсем красиво, так как концы стрелочке болтаются непонятно где. Для начала мы модифицируем класс нашего соединения, чтобы сделать все более универсальным и добавим туда методы вычисления начальной и конечной точек соединения:
def SourcePoint(self):
return np.array(self.source.GetConnectionPortForTargetPoint(self.destination.GetCenter()))
def DestinationPoint(self):
return np.array(self.destination.GetConnectionPortForTargetPoint(self.source.GetCenter()))
В данном случае, мы просим каждую ноду указать, откуда стоит начинать соединение, передавая ей центр противоположной ноды как другой конец. Это не идеальный и не самый универсальный способ, но для начала сойдет. Рендеринг соединения теперь выглядит так:
def Render(self, gc):
gc.SetPen(wx.Pen('#000000', 1, wx.SOLID))
self.RenderArrow(gc, self.SourcePoint(), self.DestinationPoint())
Осталось собственно реализовать метод «GetConnectionPortForTargetPoint» у ноды, который будет вычислять точку на границе ноды, откуда следует начинать соединение. Для прямоугольника без учета закругленных углов, можно использовать следующий метод:
def GetConnectionPortForTargetPoint(self, targetPoint):
targetPoint = np.array(targetPoint)
center = np.array(self.GetCenter())
direction = targetPoint - center
if direction[0] > 0:
#Check right border
borderX = self.position[0] + self.boundingBoxDimensions[0]
else:
#Check left border
borderX = self.position[0]
if direction[0] == 0:
t1 = float("inf")
else:
t1 = (borderX - center[0]) / direction[0]
if direction[1] > 0:
#Check bottom border
borderY = self.position[1] + self.boundingBoxDimensions[1]
else:
#Check top border
borderY = self.position[1]
if direction[1] == 0:
t2 = float("inf")
else:
t2 = (borderY - center[1]) / direction[1]
t = min(t1, t2)
boundaryPoint = center + t*direction
return boundaryPoint
Тут мы находим бпижайшее пересечение между лучом, выходящим из центра ноды в точку назначение, и сторонами прямоугольника. Этак точка лежит на границе прямоугольника и, в целом, нам подходит. Так мы можем получить что-нибудь такое:
Или что-то, похожее на картинку в самом начале статьи, которая представляет собой иерархию классов текстовой ноды, которая уже близка к чему-то вполне полезному.
Код живет в тут [7].
PS: Об опечатках пишите в личку.
Автор: Akson87
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/48200
Ссылки в тексте:
[1] Часть 1: Учимся рисовать: http://habrahabr.ru/post/201336/
[2] Часть 2: Обработка событий мыши: http://habrahabr.ru/post/201538/
[3] Часть 3: Продолжаем добавлять фичи + обработка клавиатуры: http://habrahabr.ru/post/201608/
[4] Часть 4: Реализуем Drag&Drop: http://habrahabr.ru/post/201784/
[5] тут: https://github.com/Akson/MoveMe/tree/a5166352fb03af271f6372613640ea8c6e457f48
[6] тут: https://github.com/Akson/MoveMe/tree/b90f4b806f71ba515baa4f259d23e2322f740350
[7] тут: https://github.com/Akson/MoveMe/tree/4b35c8834647102d016ffd76af3075d91d08ca27
[8] Источник: http://habrahabr.ru/post/201930/
Нажмите здесь для печати.