mouse-overs with graphical objects

Robin Dunn wrote:

> Hi,
>
> In Tkinter you can plot a line in a Canvas
> and then attach for example a mouseEnter event:
>
> self.mycurve=self.MyCanvas.create_line(liste,width=3,fill="blue")
>
> self.MyCanvas.tag_bind(mycurve, "<Enter>", self.mouseEnter)
>
>
> How could one achieve something similar with wxPython
(eg. for a wxClientDC and a line created by dc.DrawLineList)?

The wxDC classes are much lower level than the canvas in tkinter, and
are really only for drawing. You would have to work at a similarly low
level with the mouse events and calculate the mouse overs yourself.

That's dissapointing - it seems that for my purposes a Canvas a la Tkinter
is strongly lacking - I hope I don't have to return to Tkinter because
of this ... (And it is not just the mouse-over thing,
also moving an object like a circle seems to be much
more work with wxPython than with Tkinter; the same seems to apply
to plotting many points very quickly, ...). Sorry if this
sounds a bit negative - there are just too many buffers swirling
around in my head :wink: - Let's get more constructive:

So I aimed at a proof of concept for mouse-overs,
see the example code below.
The approach is: Every object is plotted in a second buffer with
a unique color. In an EVT_MOTION the "color" in the second buffer
at the mouse coordinate is checked and converted to an object_id.

It basically works fine on 24 Bit displays.
However, for displays with depth 16 it fails.
The point is that I would like to use
a bitmap of depth 24 to store up to 2**24 objects
by doing
  self.MouseOverBuffer=wxEmptyBitmap(self.wi, self.he,24)
Now this call fails on a system with a display depth of
for example 16.
Is it necessary that the depth of the BitMap is coupled to the
depth of the display?

Is there a better way to achieve this ?
(Remark: all this is on debian linux, but I really
want it to be platform and machine independent)

Another option is to use the wxOGL library which provides a simple
shaped object canvas, but it is old a crufty and sometimes difficult to
use.

Hmm, this sounded discouraging enough so I haven't tried this yet ...

There are also some other canvas-like classes available, you can
probably find pointers in the mail archives.

I searched around for quite a while, but I did not find anything
useful (maybe I used the wrong keywords ;-().

Many thanks,

Arnd

P.S.: sorry, if this starts a new thread, but I recieve wxPython-users
as digest and my mail programm could only answer to a whole digest issue
;-). Is there a better way for this ?

And here the example code:

···

arnd.baecker@web.de wrote:

##############################

""" Aim: plot a line etc. in a ClientDC canvas and
    associate a mouse-over with it.

    Strategy: plot as normal and at the same time
    the object is plotted into a second buffer with
    a color which is unique to the corresponding object.
    this should allow for 2**24=16777215 objects.

    Parts of the code (those with a lot of comments :wink:
    for the buffered drawing are
    from Jean-Michel Fauth - "Drawing with a buffered bitmap",
    postted on wxpython-users, 17 Jul 2003,
    (many thanks for that - really helpful!).

    Version 0.0.3, 02.09.2003, Arnd Baecker

"""

#import wx
from wxPython.wx import *
#from wxPython.lib.evtmgr import eventManager

from wxPython.lib.rcsizer import RowColSizer

# stupid IDs (IMHO)
ID_ABOUT=101
ID_OPEN=102
ID_BUTTON1=110
ID_EXIT=200

class MouseOverCanvas(wxWindow):
    """Subclass of a wxWindow to allow simple general plotting
    of data with zoom, labels, and automatic axis scaling."""

    def __init__(self, parent, id = -1, pos=wx.wxDefaultPosition,
            size=wx.wxDefaultSize, style= wx.wxDEFAULT_FRAME_STYLE, name= ""):
        """Constucts a window, which can be a child of a frame, dialog or
        any other non-control window"""

        wx.wxWindow.__init__(self, parent, id, pos, size, style, name)
        self.border = (14,14)
        self.SetCursor(wxCROSS_CURSOR)

        #self.SetBackgroundColour(wx.wxNamedColour("yellow"))

        self.BufferBmp = None

        self.MouseOverbuffer=None

        EVT_SIZE(self, self.OnSize)
        EVT_PAINT(self, self.OnPaint)
        EVT_MOTION(self,self.OnMotion)

    # OnSize is fired at the application start or when the frame is resized.
    # The OnSize event is fired BEFORE the OnPaint event. wxWindows
    # handles the events in this order. This is not due to the fact,
    # that the code line EVT_SIZE(...) is placed before the line
    # EVT_PAINT(...).
    # The procedure OnSize() is the right place to define the
    # BufferBmp and its size. self.BufferBmp is the picture in memory,
    # which contains your drawing. self.BufferBmp is also used as a flag,
    # a None value indicates no picture.
    #
    def OnSize(self, event):
        # Get the size of the drawing area in pixels.
        self.wi, self.he = self.GetSizeTuple()
        # Create BufferBmp and set the same size as the drawing area.
        self.BufferBmp = wxEmptyBitmap(self.wi, self.he)

        memdc = wxMemoryDC()
        memdc.SelectObject(self.BufferBmp)

        # second buffer for mouse-over
        # FIXME: this does not work on depth 16 displays
        #self.MouseOverBuffer=wxEmptyBitmap(self.wi, self.he,24)

        # FIXME: this makes it display depth dependent!!!!
        self.MouseOverBuffer=wxEmptyBitmap(self.wi, self.he)
        self.mouseoverdc= wxMemoryDC()
        self.mouseoverdc.SelectObject(self.MouseOverBuffer)
        self.lastobject=None # last object under mouse cursor

        # Drawing job
        ret = self.DoSomeDrawing(memdc)

    # OnPaint is executed at the app start, when resizing or when
    # the application windows becomes active. OnPaint copies the
    # buffered picture, instead of preparing (again) the drawing job.
    # This is the trick, copying is very fast.
    # Note: if you forget to define the dc in this procedure,
    # (no dc = ... code line), the application will run in
    # an infinite loop. This is a common beginner's error. (I think)
    # BeginDrawing() and EndDrawing() are for windows platforms (see doc).
    def OnPaint(self, event):
        print 'OnPaint in MyDrawingArea'
        dc = wxPaintDC(self)
        dc.BeginDrawing()
        if self.BufferBmp != None:
            print '...drawing'
            dc.DrawBitmap(self.BufferBmp, 0, 0, True)
        else:
            print '...nothing to draw'
        dc.EndDrawing()

    def DoSomeDrawing(self, dc):
        print 'DoSomeDrawing in MyDrawingArea'

        dc.BeginDrawing()

        # Clear everything
        objectcolor=wxColor(0,0,2)
        dc.SetBrush(wxBrush(objectcolor, wxSOLID))
        dc.Clear()

        # Draw the square with a fixed size.
        dc.SetBrush(wxBrush(wxCYAN, wxSOLID))
        dc.SetPen(wxPen(wxBLUE, 1, wxSOLID))
        dc.DrawRectangle(10, 10, 200, 200)

        # Mouse over buffer:
        self.mouseoverdc.SetBrush(wxBrush(wxBLACK, wxSOLID))
        self.mouseoverdc.Clear()

        objectcolor=wxColor(0,0,1) # color in mouseoverdc for object 1
        self.mouseoverdc.SetBrush(wxBrush(objectcolor, wxSOLID))
        self.mouseoverdc.SetPen(wxPen(objectcolor, 1, wxSOLID))
        self.mouseoverdc.DrawRectangle(10, 10, 200, 200)

        # Draw the square2 with a fixed size.
        dc.SetBrush(wxBrush(wxRED, wxSOLID))
        dc.SetPen(wxPen(wxGREEN, 1, wxSOLID))
        dc.DrawRectangle(80, 80, 100, 150)

        # Mouse over buffer:
        objectcolor=wxColor(0,0,10) # color in mouseoverdc for object 2
        self.mouseoverdc.SetBrush(wxBrush(objectcolor, wxSOLID))
        self.mouseoverdc.SetPen(wxPen(objectcolor, 1, wxSOLID))
        self.mouseoverdc.DrawRectangle(80, 80, 100, 150)

    def OnMotion(self,event):
        # get mouse position
        x,y=event.GetPosition()
        # get color (=object) below the mouse pointer
        overcol=self.mouseoverdc.GetPixel(x,y) # FIXME: system-dep ??
        r,g,b=overcol.Red(),overcol.Green(),overcol.Blue()
        #print "col=%s" % str(overcol)
        #print r,g,b
        object=r+g*256+b*256*256 # construct object id

        print "motion",x,y,object,r,g,b

        # Here we have to check, if we are inside an object
        if(self.lastobject!=None):
            if(object==self.lastobject):
                print "... stayed in %d" % (object )
            else:
                print "... left %d" % (self.lastobject)

                if(object!=256*255*256+256*255+255):
                    print "==> entered %d" % (object)
                else:
                    object=None
        else:
            if(object!=256*255*256+256*255+255):
                print "==> entered %d" % (object)
            else:
                object=None

        self.lastobject=object

class MainWindow(wxFrame):
    def __init__(self,parent,id,title):
        wxFrame.__init__(self,parent,-4, title,size=(500,600),
                         style=wxDEFAULT_FRAME_STYLE|
                                        wxNO_FULL_REPAINT_ON_RESIZE)

        self.CreateStatusBar() # A Statusbar in the bottom of the window
        # Setting up the menu.
        filemenu= wxMenu()
        filemenu.Append(ID_ABOUT, "&About"," Information about program")
        filemenu.AppendSeparator()
        filemenu.Append(ID_EXIT,"E&xit"," Terminate the program")

        # Creating the menubar.
        menuBar = wxMenuBar()
        menuBar.Append(filemenu,"&File")
        self.SetMenuBar(menuBar)
        EVT_MENU(self, ID_ABOUT, self.OnAbout)
        EVT_MENU(self, ID_EXIT, self.OnExit)

        sizer =RowColSizer()

        # Wigdet with Objects
        self.plot= MouseOverCanvas(self,size=(400,250))
        sizer.Add(self.plot,row=4,col=2,colspan=10,rowspan=1)

        #Layout sizers
        self.SetSizer(sizer)

        self.Show(1) # wozu ???

    def OnAbout(self,e):
        d= wxMessageDialog( self, "Mouse over test\n"
                            " with wxPython","About MouseOverTest", wxOK)
                            # Create a message dialog box
        d.ShowModal() # Shows it
        d.Destroy() # finally destroy it when finished.

    def OnExit(self,e):
        self.Close(true) # Close the frame.

app = wxPySimpleApp()
frame = MainWindow(None, -1, "MouseOverTest")
frame.Show(1)
app.MainLoop()

Robin Dunn wrote:

> The approach is: Every object is plotted in a second buffer with
> a unique color. In an EVT_MOTION the "color" in the second buffer
> at the mouse coordinate is checked and converted to an object_id.

> It basically works fine on 24 Bit displays.
> However, for displays with depth 16 it fails.
> The point is that I would like to use
> a bitmap of depth 24 to store up to 2**24 objects
> by doing

Yep, IIRC Chris Barker also tried this approach and had the same
problem. I think there is a workaround in his FloatCanvas module.

Yes, I did have the same problem, and an offscreen bitmap does have to
be the same color depth as the actual screen. The work around I did was
to make sure that the colors assigned to the objects were far enough
apart that they would map to distict colors on a 16 bit display as well.
That means tahat you can only get 2**15 (32762) distict objects which
should be plenty, I never did get that code integrated into my
FloatCanvas, however. I may do it in the next month or so.

I've adapted your code to use my scheme. It's enclosed here. I've put a
few other notes in the comments also.

Here are a few additional notes:

You set up all your buffers in your OnSize handler, which is where it
should go. However, I found that depending on the platform, OnSize may
not get called where you expect it to in initialization, so I called it
explicitly at the end of the __init__.

It's not considered a good idea to keep DCs around. For your main buffer
you don't, but for your mouseover buffer, you do. It's probably better
to keep just the bitmap around, and re-create the DC when you need to
draw to it.

FloatCanvas from Chris Barker

Here's something about that. The short version is that it is for drawing
in arbitrary coordinates, with zooming and scrolling. Let me know if you
want a copy.

    FloatCanvas.py

    This is a high level window for drawing maps and anything else in an
    arbitrary coordinate system.

    The goal is to provide a convenient way to draw stuff on the screen
    without having to deal with handling OnPaint events, converting to
pixel
    coordinates, knowing about wxWindows brushes, pens, and colors, etc.
It
    also provides virtually unlimited zooming and scrolling

    I am using it for two things:
    1) general purpose drawing in floating point coordinates
    2) displaying map data in Lat-long coordinates

    If the projection is set to None, it will draw in general purpose
    floating point coordinates. If the projection is set to 'FlatEarth',
it
    will draw a FlatEarth projection, centered on the part of the map
that
    you are viewing. You can also pass in your own projection function.

    It is double buffered, so re-draws after the window is uncovered by
something
    else are very quick.

    It relies on NumPy, which is needed for speed

    Bugs and Limitations:
        Lots: patches, fixes welcome

    For Map drawing: It ignores the fact that the world is, in fact, a
    sphere, so it will do strange things if you are looking at stuff
near
    the poles or the date line. so far I don't have a need to do that,
so I
    havn't bothered to add any checks for that yet.

    Zooming:
    I have set no zoom limits. What this means is that if you zoom in
really
    far, you can get integer overflows, and get wierd results. It
    doesn't seem to actually cause any problems other than wierd output,
at
    least when I have run it.

    Speed:
    I have done a couple of things to improve speed in this app. The one
    thing I have done is used NumPy Arrays to store the coordinates of
the
    points of the objects. This allowed me to use array oriented
functions
    when doing transformations, and should provide some speed
improvement
    for objects with a lot of points (big polygons, polylines,
pointsets).

    The real slowdown comes when you have to draw a lot of objects,
because
    you have to call the wxDC.DrawSomething call each time. This is
plenty
    fast for tens of objects, OK for hundreds of objects, but pretty
darn
    slow for thousands of objects.

    The solution is to be able to pass some sort of object set to the DC
    directly. I've used DC.DrawPointList(Points), and it helped a lot
with
    drawing lots of points. I havn't got a LineSet type object, so I
havn't
    used DC.DrawLineList yet. I'd like to get a full set of
DrawStuffList()
    methods implimented, and then I'd also have a full set of Object
sets
    that could take advantage of them. I hope to get to it some day.

    Copyright: Christopher Barker

    License: Same as wxPython

    Please let me know if you're using this!!!

    Contact me at:

    Chris.Barker@noaa.gov

HitTest.py (9.25 KB)

···

arnd.baecker@web.de wrote:

--
Christopher Barker, Ph.D.
Oceanographer
                                        
NOAA/OR&R/HAZMAT (206) 526-6959 voice
7600 Sand Point Way NE (206) 526-6329 fax
Seattle, WA 98115 (206) 526-6317 main reception

Chris.Barker@noaa.gov

This must be the Pythonic approach.

Peter

···

-----Original Message-----
From: cbarker [mailto:cbarker]On Behalf Of Chris Barker
Sent: 09 September 2003 21:48
To: wxPython-users@lists.wxwindows.org
Subject: Re: [wxPython-users] mouse-overs with graphical objects

For Map drawing: It ignores the fact that the world is, in fact, a
sphere ...