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

Пример применения WxPython для создания нодового интерфейса. Часть 2: Обработка событий мыши

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

В прошлой части я рассказал о задаче и начал описывать процесс реализации, а вернее рендеринг объектов. Сейчас же пришла пора реализовать взаимодействие с пользователем.

Часть 1: Учимся рисовать

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

Напомню, что в прошлый раз у нас получилась простая программа, которая рисует на канвасе простенькие ноды (пока что прямоугольники с текстом). Пришла пора сделать ноды передвигаемыми.

4. Подсветка объектов при наведении на них курсора

Но перед тем как реализовать перемещение нод, мы сделаем одну пригодную фичу: подсветку объекта при наведении на него курсора. Части этой фичи позднее сгодятся при реализации остального функционала. Для реализации нам необходимо исполнить 3 действия:
1) Отследить перемещение курсора
2) Обнаружить и сберечь самый верхний объект под курсором
3) Отрендерить выделение этого объекта

Для отслеживания перемещения курсора, нам нужно добавить обработчик соответствующего события к классу канваса:

        self.Bind(wx.EVT_MOTION, self.OnMouseMotion)

Сейчас при перемещении курсора будет вызываться способ:

    def OnMouseMotion(self, evt):
        pos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()
        self._objectUnderCursor = self.FindObjectUnderPoint(pos)
        self.Render()

Здесь происходят три действия: так как нам передаются координаты курсора касательно окна, необходимо сперва перевести их в координаты канваса (так как у нас есть скрол), после этого же мы обязаны обнаружить соответствующий объект и обновить изображение, Дабы возникла подсветка объекта на тот, что пользователь переместил курсор. Поиском объектов под курсором занимается способ:

    def FindObjectUnderPoint(self, pos):
        #Check all objects on a canvas. Some objects may have multiple components and connections.
        for obj in reversed(self._canvasObjects):
            objUnderCursor = obj.ReturnObjectUnderCursor(pos)
            if objUnderCursor: 
                return objUnderCursor
        return None

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

    def ReturnObjectUnderCursor(self, pos):
        if pos[0] < self.position[0]: return None
        if pos[1] < self.position[1]: return None
        if pos[0] > self.position[0] self.boundingBoxDimensions[0]: return None
        if pos[1] > self.position[1] self.boundingBoxDimensions[1]: return None
        return self

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

        if self._objectUnderCursor:
            gc.PushState()
            self._objectUnderCursor.RenderHighlighting(gc)
            gc.PopState()

Осталось добавить в ноду код для рендеринга подсветки:

    def RenderHighlighting(self, gc):
        gc.SetBrush(wx.Brush('#888888', wx.TRANSPARENT))
        gc.SetPen(wx.Pen('#888888', 3, wx.DOT))
        gc.DrawRectangle(self.position[0]-3, 
                         self.position[1]-3, 
                         self.boundingBoxDimensions[0] 6, 
                         self.boundingBoxDimensions[1] 6)

Здесь мы используем прозрачную кисть, Дабы при рендеринге не затереть то, что было отрендерено ранее (т.е. саму ноду).
В результате получается вот такая картинка:

Курсор пришлось дорисовать постфактум, следственно он немножко не обычный:)
Не буду приводить здесь каждый код, кому увлекательно, вот данный коммит на GitHub’е содержит его.

5. Маленький рефакторинг и добавление интерфейсов

И вновь мы отложим не на длинно реализацию перемещения наших объектов, в данный раз для проведения небольшого рефакторинга. Раз уж фреймворк данный должен быть универсальным, значит и ноды здесь могут быть каждые различные, в том числе, неперемещаемые (скажем соединения между объектами, которые задаются самими объектами либо какие-то компоненты нод, да и немного ли чего людям в голову взбредет). Так что нам необходим какой-то многофункциональный метод изложения того, что дозволено, а что невозможно делать с нодами. Да и вообще, хотелось бы какой-то многофункциональный интерфейс для нод ввести. Правда теперь мы пока не будем применять abc, zope.interface либо что-то сходственное, а легко сделаем базовый класс для объектов на канвасе:

class CanvasObject(object):
    def __init__(self):
        #Supported operations
        self.clonable = False
        self.movable = False
        self.connectable = False
        self.deletable = False
        self.selectable = False

    def Render(self, gc): 
        """
        Rendering method should draw an object.
        gc: GraphicsContext object that should be used for drawing.
        """
        raise NotImplementedError()

    def RenderHighlighting(self, gc):
        """
        RenderHighlighting method should draw an object 
        with a highlighting border around it.
        gc: GraphicsContext object that should be used for drawing.
        """ 
        raise NotImplementedError()

    def ReturnObjectUnderCursor(self, pos):
        """
        ReturnObjectUnderCursor method returns a top component 
        of this object at a given position or None if position 
        is outside of all objects.
        pos: tested position as a list of x, y coordinates such as [100, 200]
        """
        raise NotImplementedError()

Как вы можете видеть, у нас есть некоторое число стандартных действий, которые по умолчанию не поддерживаются. Но есть 3 способа, которые обязаны быть у всякого объекта на канвасе. Что разумно, для чего нам такие объекты на канвасе, которые мы не можем увидеть (Render), а как увидим, так потыкать их курсором (ReturnObjectUnderCursor, RenderHighlighting). И здесь мы припоминаем о том, что мы хотим перемещать наши ноды, т.е. они обязаны быть перемещаемыми, а для этого есть особый класс:

from MoveMe.Canvas.Objects.Base.CanvasObject import CanvasObject

class MovableObject(CanvasObject):
    def __init__(self, position):
        super(MovableObject, self).__init__()
        self.position = position

        self.movable = True

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

from MoveMe.Canvas.Objects.Base.CanvasObject import CanvasObject
from MoveMe.Canvas.Objects.Base.MovableObject import MovableObject

class SimpleBoxNode(MovableObject, CanvasObject):
...........

6. Перемещение нод

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

    def OnMouseLeftDown(self, evt):
        if not self._objectUnderCursor:
            return

        if self._objectUnderCursor.movable:
            self._lastDraggingPosition = self.CalcUnscrolledPosition(evt.GetPosition()).Get()
            self._draggingObject = self._objectUnderCursor

        self.Render()

Мы легко запоминаем позицию курсора и нынешний объект под курсором как перемещаемый, если он поддеживает перемещение. Разве что здесь еще есть проверка наличия объекта под курсором, так как смысла перемещать пустоту нет. Вторая же часть немножко увлекательнее

    def OnMouseMotion(self, evt):
        pos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()
        self._objectUnderCursor = self.FindObjectUnderPoint(pos)

        if not evt.LeftIsDown():
            self._draggingObject = None

        if evt.LeftIsDown() and evt.Dragging() and self._draggingObject:
            dx = pos[0]-self._lastDraggingPosition[0]
            dy = pos[1]-self._lastDraggingPosition[1]
            newX = self._draggingObject.position[0] dx
            newY = self._draggingObject.position[1] dy

            #Check canvas boundaries 
            newX = min(newX, self.canvasDimensions[0]-self._draggingObject.boundingBoxDimensions[0])
            newY = min(newY, self.canvasDimensions[1]-self._draggingObject.boundingBoxDimensions[1])
            newX = max(newX, 0)
            newY = max(newY, 0)

            self._draggingObject.position = [newX, newY]

            #Cursor will be at a border of a node if it goes out of canvas
            self._lastDraggingPosition = [min(pos[0], self.canvasDimensions[0]), min(pos[1], self.canvasDimensions[1])]

        self.Render()

Первая проверка гарантирует нам, что если пользователь в какой-то момент водит мышкой с отпущеной левой кнопкой, значит он теснее верно ничего не перемещает. Это отменнее, чем останавливать перемещение по событию отпускания кнопки, так как курсор может быть за пределами окно и тогда мы не получим это событие. Дальше мы проверяем, что мы подлинно что-то тащим и начинаем считать относительное перемещение нашего объекта. На данный момент мы не задумываемся о том, что происходит с клавиатурой (не нажат ли Ctrl либо еще что, это будет позднее). Еще есть проверка на выход за пределы канваса. С проверкой этой все не вовсе легко и ясно. С одной стороны, если размер канваса фиксирован, то все так и должно быть, а с иной стороны, отлично бы было растягивать канвас по ходу дела (правда и это не является совершенным решением). В всеобщем, на данный момент, размер канваса будет фиксированным и ноды будут упираться в границы канваса.
Вот и все, сейчас мы можем перемещать объекты по канвасу. Код живет в этом коммите на GitHub’е. А выглядит это так:

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

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

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