Jeff Grimmett with some tweaks and changes from Robin git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@24889 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
		
			
				
	
	
		
			407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#---------------------------------------------------------------------------
 | 
						|
# Name:        wxPython.lib.mixins.rubberband
 | 
						|
# Purpose:     A mixin class for doing "RubberBand"-ing on a window.
 | 
						|
#
 | 
						|
# Author:      Robb Shecter and members of wxPython-users
 | 
						|
#
 | 
						|
# Created:     11-September-2002
 | 
						|
# RCS-ID:      $Id$
 | 
						|
# Copyright:   (c) 2002 by db-X Corporation
 | 
						|
# Licence:     wxWindows license
 | 
						|
#---------------------------------------------------------------------------
 | 
						|
# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net)
 | 
						|
#
 | 
						|
# o 2.5 compatability update.
 | 
						|
# o Tested, but there is an anomaly between first use and subsequent uses.
 | 
						|
#   First use is odd, subsequent uses seem to be OK. Init error?
 | 
						|
#   -- No, the first time it uses an aspect ratio, but after the reset it doesn't.
 | 
						|
#
 | 
						|
 | 
						|
"""
 | 
						|
A mixin class for doing "RubberBand"-ing on a window.
 | 
						|
"""
 | 
						|
 | 
						|
import  wx
 | 
						|
 | 
						|
#
 | 
						|
# Some miscellaneous mathematical and geometrical functions
 | 
						|
#
 | 
						|
 | 
						|
def isNegative(aNumber):
 | 
						|
    """
 | 
						|
    x < 0:   1
 | 
						|
    else:    0
 | 
						|
    """
 | 
						|
    return aNumber < 0
 | 
						|
 | 
						|
 | 
						|
def normalizeBox(box):
 | 
						|
    """
 | 
						|
    Convert any negative measurements in the current
 | 
						|
    box to positive, and adjust the origin.
 | 
						|
    """
 | 
						|
    x, y, w, h = box
 | 
						|
    if w < 0:
 | 
						|
        x += (w+1)
 | 
						|
        w *= -1
 | 
						|
    if h < 0:
 | 
						|
        y += (h+1)
 | 
						|
        h *= -1
 | 
						|
    return (x, y, w, h)
 | 
						|
 | 
						|
 | 
						|
def boxToExtent(box):
 | 
						|
    """
 | 
						|
    Convert a box specification to an extent specification.
 | 
						|
    I put this into a seperate function after I realized that
 | 
						|
    I had been implementing it wrong in several places.
 | 
						|
    """
 | 
						|
    b = normalizeBox(box)
 | 
						|
    return (b[0], b[1], b[0]+b[2]-1, b[1]+b[3]-1)
 | 
						|
 | 
						|
 | 
						|
def pointInBox(x, y, box):
 | 
						|
    """
 | 
						|
    Return True if the given point is contained in the box.
 | 
						|
    """
 | 
						|
    e = boxToExtent(box)
 | 
						|
    return x >= e[0] and x <= e[2] and y >= e[1] and y <= e[3]
 | 
						|
 | 
						|
 | 
						|
def pointOnBox(x, y, box, thickness=1):
 | 
						|
    """
 | 
						|
    Return True if the point is on the outside edge
 | 
						|
    of the box.  The thickness defines how thick the
 | 
						|
    edge should be.  This is necessary for HCI reasons:
 | 
						|
    For example, it's normally very difficult for a user
 | 
						|
    to manuever the mouse onto a one pixel border.
 | 
						|
    """
 | 
						|
    outerBox = box
 | 
						|
    innerBox = (box[0]+thickness, box[1]+thickness, box[2]-(thickness*2), box[3]-(thickness*2))
 | 
						|
    return pointInBox(x, y, outerBox) and not pointInBox(x, y, innerBox)
 | 
						|
 | 
						|
 | 
						|
def getCursorPosition(x, y, box, thickness=1):
 | 
						|
    """
 | 
						|
    Return a position number in the range 0 .. 7 to indicate
 | 
						|
    where on the box border the point is.  The layout is:
 | 
						|
 | 
						|
              0    1    2
 | 
						|
              7         3
 | 
						|
              6    5    4
 | 
						|
    """
 | 
						|
    x0, y0, x1, y1 = boxToExtent(box)
 | 
						|
    w, h  = box[2], box[3]
 | 
						|
    delta = thickness - 1
 | 
						|
    p     = None
 | 
						|
 | 
						|
    if pointInBox(x, y, (x0, y0, thickness, thickness)):
 | 
						|
        p = 0
 | 
						|
    elif pointInBox(x, y, (x1-delta, y0, thickness, thickness)):
 | 
						|
        p = 2
 | 
						|
    elif pointInBox(x, y, (x1-delta, y1-delta, thickness, thickness)):
 | 
						|
        p = 4
 | 
						|
    elif pointInBox(x, y, (x0, y1-delta, thickness, thickness)):
 | 
						|
        p = 6
 | 
						|
    elif pointInBox(x, y, (x0+thickness, y0, w-(thickness*2), thickness)):
 | 
						|
        p = 1
 | 
						|
    elif pointInBox(x, y, (x1-delta, y0+thickness, thickness, h-(thickness*2))):
 | 
						|
        p = 3
 | 
						|
    elif pointInBox(x, y, (x0+thickness, y1-delta, w-(thickness*2), thickness)):
 | 
						|
        p = 5
 | 
						|
    elif pointInBox(x, y, (x0, y0+thickness, thickness, h-(thickness*2))):
 | 
						|
        p = 7
 | 
						|
 | 
						|
    return p
 | 
						|
 | 
						|
 | 
						|
 | 
						|
 | 
						|
class RubberBand:
 | 
						|
    """
 | 
						|
    A stretchable border which is drawn on top of an
 | 
						|
    image to define an area.
 | 
						|
    """
 | 
						|
    def __init__(self, drawingSurface, aspectRatio=None):
 | 
						|
        self.__THICKNESS     = 5
 | 
						|
        self.drawingSurface  = drawingSurface
 | 
						|
        self.aspectRatio     = aspectRatio
 | 
						|
        self.hasLetUp        = 0
 | 
						|
        self.currentlyMoving = None
 | 
						|
        self.currentBox      = None
 | 
						|
        self.__enabled       = 1
 | 
						|
        self.__currentCursor = None
 | 
						|
 | 
						|
        drawingSurface.Bind(wx.EVT_MOUSE_EVENTS, self.__handleMouseEvents)
 | 
						|
        drawingSurface.Bind(wx.EVT_PAINT, self.__handleOnPaint)
 | 
						|
 | 
						|
    def __setEnabled(self, enabled):
 | 
						|
        self.__enabled = enabled
 | 
						|
 | 
						|
    def __isEnabled(self):
 | 
						|
        return self.__enabled
 | 
						|
 | 
						|
    def __handleOnPaint(self, event):
 | 
						|
        #print 'paint'
 | 
						|
        event.Skip()
 | 
						|
 | 
						|
    def __isMovingCursor(self):
 | 
						|
        """
 | 
						|
        Return True if the current cursor is one used to
 | 
						|
        mean moving the rubberband.
 | 
						|
        """
 | 
						|
        return self.__currentCursor == wx.CURSOR_HAND
 | 
						|
 | 
						|
    def __isSizingCursor(self):
 | 
						|
        """
 | 
						|
        Return True if the current cursor is one of the ones
 | 
						|
        I may use to signify sizing.
 | 
						|
        """
 | 
						|
        sizingCursors = [wx.CURSOR_SIZENESW,
 | 
						|
                         wx.CURSOR_SIZENS,
 | 
						|
                         wx.CURSOR_SIZENWSE,
 | 
						|
                         wx.CURSOR_SIZEWE,
 | 
						|
                         wx.CURSOR_SIZING,
 | 
						|
                         wx.CURSOR_CROSS]
 | 
						|
        try:
 | 
						|
            sizingCursors.index(self.__currentCursor)
 | 
						|
            return 1
 | 
						|
        except ValueError:
 | 
						|
            return 0
 | 
						|
 | 
						|
 | 
						|
    def __handleMouseEvents(self, event):
 | 
						|
        """
 | 
						|
        React according to the new event.  This is the main
 | 
						|
        entry point into the class.  This method contains the
 | 
						|
        logic for the class's behavior.
 | 
						|
        """
 | 
						|
        if not self.enabled:
 | 
						|
            return
 | 
						|
 | 
						|
        x, y = event.GetPosition()
 | 
						|
 | 
						|
        # First make sure we have started a box.
 | 
						|
        if self.currentBox == None and not event.LeftDown():
 | 
						|
            # No box started yet.  Set cursor to the initial kind.
 | 
						|
            self.__setCursor(wx.CURSOR_CROSS)
 | 
						|
            return
 | 
						|
 | 
						|
        if event.LeftDown():
 | 
						|
            if self.currentBox == None:
 | 
						|
                # No RB Box, so start a new one.
 | 
						|
                self.currentBox = (x, y, 0, 0)
 | 
						|
                self.hasLetUp   = 0
 | 
						|
            elif self.__isSizingCursor():
 | 
						|
                # Starting a sizing operation.  Change the origin.
 | 
						|
                position = getCursorPosition(x, y, self.currentBox, thickness=self.__THICKNESS)
 | 
						|
                self.currentBox = self.__denormalizeBox(position, self.currentBox)
 | 
						|
 | 
						|
        elif event.Dragging() and event.LeftIsDown():
 | 
						|
            # Use the cursor type to determine operation
 | 
						|
            if self.__isMovingCursor():
 | 
						|
                if self.currentlyMoving or pointInBox(x, y, self.currentBox):
 | 
						|
                    if not self.currentlyMoving:
 | 
						|
                        self.currentlyMoving = (x - self.currentBox[0], y - self.currentBox[1])
 | 
						|
                    self.__moveTo(x - self.currentlyMoving[0], y - self.currentlyMoving[1])
 | 
						|
            elif self.__isSizingCursor():
 | 
						|
                self.__resizeBox(x, y)
 | 
						|
 | 
						|
        elif event.LeftUp():
 | 
						|
            self.hasLetUp = 1
 | 
						|
            self.currentlyMoving = None
 | 
						|
            self.__normalizeBox()
 | 
						|
 | 
						|
        elif event.Moving() and not event.Dragging():
 | 
						|
            # Simple mouse movement event
 | 
						|
            self.__mouseMoved(x,y)
 | 
						|
 | 
						|
    def __denormalizeBox(self, position, box):
 | 
						|
        x, y, w, h = box
 | 
						|
        b = box
 | 
						|
        if position == 2 or position == 3:
 | 
						|
            b = (x, y + (h-1), w, h * -1)
 | 
						|
        elif position == 0 or position == 1 or position == 7:
 | 
						|
            b = (x + (w-1), y + (h-1), w * -1, h * -1)
 | 
						|
        elif position == 6:
 | 
						|
            b = (x + (w-1), y, w * -1, h)
 | 
						|
        return b
 | 
						|
 | 
						|
    def __resizeBox(self, x, y):
 | 
						|
        """
 | 
						|
        Resize and repaint the box based on the given mouse
 | 
						|
        coordinates.
 | 
						|
        """
 | 
						|
        # Implement the correct behavior for dragging a side
 | 
						|
        # of the box:  Only change one dimension.
 | 
						|
        if not self.aspectRatio:
 | 
						|
            if self.__currentCursor == wx.CURSOR_SIZENS:
 | 
						|
                x = None
 | 
						|
            elif self.__currentCursor == wx.CURSOR_SIZEWE:
 | 
						|
                y = None
 | 
						|
 | 
						|
        x0,y0,w0,h0 = self.currentBox
 | 
						|
        currentExtent = boxToExtent(self.currentBox)
 | 
						|
        if x == None:
 | 
						|
            if w0 < 1:
 | 
						|
                w0 += 1
 | 
						|
            else:
 | 
						|
                w0 -= 1
 | 
						|
            x = x0 + w0
 | 
						|
        if y == None:
 | 
						|
            if h0 < 1:
 | 
						|
                h0 += 1
 | 
						|
            else:
 | 
						|
                h0 -= 1
 | 
						|
            y = y0 + h0
 | 
						|
        x1,y1 = x, y
 | 
						|
        w, h = abs(x1-x0)+1, abs(y1-y0)+1
 | 
						|
        if self.aspectRatio:
 | 
						|
            w = max(w, int(h * self.aspectRatio))
 | 
						|
            h = int(w / self.aspectRatio)
 | 
						|
        w *= [1,-1][isNegative(x1-x0)]
 | 
						|
        h *= [1,-1][isNegative(y1-y0)]
 | 
						|
        newbox = (x0, y0, w, h)
 | 
						|
        self.__drawAndErase(boxToDraw=normalizeBox(newbox), boxToErase=normalizeBox(self.currentBox))
 | 
						|
        self.currentBox = (x0, y0, w, h)
 | 
						|
 | 
						|
    def __normalizeBox(self):
 | 
						|
        """
 | 
						|
        Convert any negative measurements in the current
 | 
						|
        box to positive, and adjust the origin.
 | 
						|
        """
 | 
						|
        self.currentBox = normalizeBox(self.currentBox)
 | 
						|
 | 
						|
    def __mouseMoved(self, x, y):
 | 
						|
        """
 | 
						|
        Called when the mouse moved without any buttons pressed
 | 
						|
        or dragging being done.
 | 
						|
        """
 | 
						|
        # Are we on the bounding box?
 | 
						|
        if pointOnBox(x, y, self.currentBox, thickness=self.__THICKNESS):
 | 
						|
            position = getCursorPosition(x, y, self.currentBox, thickness=self.__THICKNESS)
 | 
						|
            cursor   = [
 | 
						|
                wx.CURSOR_SIZENWSE,
 | 
						|
                wx.CURSOR_SIZENS,
 | 
						|
                wx.CURSOR_SIZENESW,
 | 
						|
                wx.CURSOR_SIZEWE,
 | 
						|
                wx.CURSOR_SIZENWSE,
 | 
						|
                wx.CURSOR_SIZENS,
 | 
						|
                wx.CURSOR_SIZENESW,
 | 
						|
                wx.CURSOR_SIZEWE
 | 
						|
                ] [position]
 | 
						|
            self.__setCursor(cursor)
 | 
						|
        elif pointInBox(x, y, self.currentBox):
 | 
						|
            self.__setCursor(wx.CURSOR_HAND)
 | 
						|
        else:
 | 
						|
            self.__setCursor()
 | 
						|
 | 
						|
    def __setCursor(self, id=None):
 | 
						|
        """
 | 
						|
        Set the mouse cursor to the given id.
 | 
						|
        """
 | 
						|
        if self.__currentCursor != id:  # Avoid redundant calls
 | 
						|
            if id:
 | 
						|
                self.drawingSurface.SetCursor(wx.StockCursor(id))
 | 
						|
            else:
 | 
						|
                self.drawingSurface.SetCursor(wx.NullCursor)
 | 
						|
            self.__currentCursor = id
 | 
						|
 | 
						|
    def __moveCenterTo(self, x, y):
 | 
						|
        """
 | 
						|
        Move the rubber band so that its center is at (x,y).
 | 
						|
        """
 | 
						|
        x0, y0, w, h = self.currentBox
 | 
						|
        x2, y2 = x - (w/2), y - (h/2)
 | 
						|
        self.__moveTo(x2, y2)
 | 
						|
 | 
						|
    def __moveTo(self, x, y):
 | 
						|
        """
 | 
						|
        Move the rubber band so that its origin is at (x,y).
 | 
						|
        """
 | 
						|
        newbox = (x, y, self.currentBox[2], self.currentBox[3])
 | 
						|
        self.__drawAndErase(boxToDraw=newbox, boxToErase=self.currentBox)
 | 
						|
        self.currentBox = newbox
 | 
						|
 | 
						|
    def __drawAndErase(self, boxToDraw, boxToErase=None):
 | 
						|
        """
 | 
						|
        Draw one box shape and possibly erase another.
 | 
						|
        """
 | 
						|
        dc = wx.ClientDC(self.drawingSurface)
 | 
						|
        dc.BeginDrawing()
 | 
						|
        dc.SetPen(wx.Pen(wx.WHITE, 1, wx.DOT))
 | 
						|
        dc.SetBrush(wx.TRANSPARENT_BRUSH)
 | 
						|
        dc.SetLogicalFunction(wx.XOR)
 | 
						|
        if boxToErase:
 | 
						|
            r = wx.Rect(*boxToErase)
 | 
						|
            dc.DrawRectangleRect(r)
 | 
						|
 | 
						|
        r = wx.Rect(*boxToDraw)
 | 
						|
        dc.DrawRectangleRect(r)
 | 
						|
        dc.EndDrawing()
 | 
						|
 | 
						|
    def __dumpMouseEvent(self, event):
 | 
						|
        print 'Moving:          ',event.Moving()
 | 
						|
        print 'Dragging:        ',event.Dragging()
 | 
						|
        print 'LeftDown:        ',event.LeftDown()
 | 
						|
        print 'LeftisDown:      ',event.LeftIsDown()
 | 
						|
        print 'LeftUp:          ',event.LeftUp()
 | 
						|
        print 'Position:        ',event.GetPosition()
 | 
						|
        print 'x,y:             ',event.GetX(),event.GetY()
 | 
						|
        print
 | 
						|
 | 
						|
 | 
						|
    #
 | 
						|
    # The public API:
 | 
						|
    #
 | 
						|
 | 
						|
    def reset(self, aspectRatio=None):
 | 
						|
        """
 | 
						|
        Clear the existing rubberband
 | 
						|
        """
 | 
						|
        self.currentBox   = None
 | 
						|
        self.aspectRatio  = aspectRatio
 | 
						|
        self.drawingSurface.Refresh()
 | 
						|
 | 
						|
    def getCurrentExtent(self):
 | 
						|
        """
 | 
						|
        Return (x0, y0, x1, y1) or None if
 | 
						|
        no drawing has yet been done.
 | 
						|
        """
 | 
						|
        if not self.currentBox:
 | 
						|
            extent = None
 | 
						|
        else:
 | 
						|
            extent = boxToExtent(self.currentBox)
 | 
						|
        return extent
 | 
						|
 | 
						|
    enabled = property(__isEnabled, __setEnabled, None, 'True if I am responding to mouse events')
 | 
						|
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    app   = wx.PySimpleApp()
 | 
						|
    frame = wx.Frame(None, -1, title='RubberBand Test', size=(300,300))
 | 
						|
 | 
						|
    # Add a panel that the rubberband will work on.
 | 
						|
    panel = wx.Panel(frame, -1)
 | 
						|
    panel.SetBackgroundColour(wx.BLUE)
 | 
						|
 | 
						|
    # Create the rubberband
 | 
						|
    frame.rubberBand = RubberBand(drawingSurface=panel)
 | 
						|
    frame.rubberBand.reset(aspectRatio=0.5)
 | 
						|
 | 
						|
    # Add a button that creates a new rubberband
 | 
						|
    def __newRubberBand(event):
 | 
						|
        frame.rubberBand.reset()
 | 
						|
    button = wx.Button(frame, 100, 'Reset Rubberband')
 | 
						|
    frame.Bind(wx.EVT_BUTTON, __newRubberBand, button)
 | 
						|
 | 
						|
    # Layout the frame
 | 
						|
    sizer = wx.BoxSizer(wx.VERTICAL)
 | 
						|
    sizer.Add(panel,  1, wx.EXPAND | wx.ALL, 5)
 | 
						|
    sizer.Add(button, 0, wx.ALIGN_CENTER | wx.ALL, 5)
 | 
						|
    frame.SetAutoLayout(1)
 | 
						|
    frame.SetSizer(sizer)
 | 
						|
    frame.Show(1)
 | 
						|
    app.MainLoop()
 |