Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 16.06.2014 | нет комментариев
Медленно, но правильно, я продолжаю делать серию туториалов о WxPython, где я хочу разглядеть разработку ферймворка для создания нодового интерфейса с нуля и до чего-то абсолютно функционального и рабочего. В прошлых частях теснее рассказано как добавлять ноды, в этой же части, мы их будем соединять, а на этой картинке показан итог, тот, что мы в этой статье получим:

Еще не безупречно, но теснее вырисовывается что-то абсолютно пригодное и рабочее.

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

13. Создаем простейшее соединение

В погоне за соединениями нод, мы начнем с ключевого компонента, класса соединения, тот, что в простейшем виде выглядит так:

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)

Выходит, если запустить это дело и немножко поиграться, то дозволено получить что-то как бы этого:

Не крепко прекрасно, но теснее функционально:) Нынешняя версия кода живет здесь.

14. Делаем прекрасные стрелочки

Линии, соединяющие углы нод — это отлично для теста, но не дюже прекрасно и эстетично. Ну да не жутко, теперь мы сделаем прекрасные и эстетичные стрелочки. Для начала, нам потребуется способ рисования стрелочек, тот, что я быстренько написал, припомнив школьную геометрию и использую 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». Так мы находим точки концов отрезков, соединяющих конец стрелочки с… не знаю как это назвать, с концами острия что ли.
Осталось в способе рендеринга заменить рисование линии на рисование стрелочки и дозволено будет созерцать такую картину:

Код живе здесь.

15. Получаем правильные точки концов соединений

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

    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

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

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

Код живет в здесь.

PS: Об опечатках пишите в личку.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB