wxPython FloatCanvas event Binding

Viewed 284

I am using Python 3.6 with wxPython 4.1.0 gtk3 (phoenix) wxWidgets 3.1.4. There is a delay in the Bind function on every 4th line that i try to plot and after about 12 lines the program crashs. Normally my code uses a grid to input the points for the lines but the attached code generates the data once the Draw button is clicked with exactly the same effect. The delay is about 2.5 seconds on the 4th line normally it takes about .1 secs to plot a line. I have tried two functions for the Bind call one uses the event the other calls the object. If anyone knows a work around can you please let me know.

import time
import string
import wx
from wx.lib.floatcanvas import NavCanvas, FloatCanvas
import wx.lib.colourdb


class InputForm(wx.Frame):
    '''set up the form and draw axis'''
    def __init__(self):
        super(InputForm, self).__init__(None, wx.ID_ANY, title='Plot Lines', size=(1300, 830))

        # set dictionary of points; key node letter, value tuple of point,
        self.pts = {}

        # create the form level sizer
        Main_Sizer = wx.BoxSizer(wx.HORIZONTAL)

        # add the sizer for the left side widgets
        sizerL = wx.BoxSizer(wx.VERTICAL)
        # add the grid and then set it ot he left panel

        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
        drw = wx.Button(self, -1, "Draw\nLines")
        btnsizer.Add(drw, 0, wx.ALL|wx.ALIGN_CENTER, 5)

        # bind the button events to handlers
        self.Bind(wx.EVT_BUTTON, self.OnDraw, drw)

        sizerL.Add((10, 20))
        sizerL.Add(btnsizer, 1, wx.ALIGN_CENTER)

        # add the draw panel
        rght = NavCanvas.NavCanvas(self,
                                   ProjectionFun=None,
                                   Debug=0,
                                   BackgroundColor="LIGHT GREY",
                                   )
        self.Canvas = rght.Canvas

        self.InitCanvas()

        Main_Sizer.Add(sizerL, 0, wx.EXPAND)
        Main_Sizer.Add((10, 10))
        Main_Sizer.Add(rght, 1, wx.EXPAND)
        self.SetSizer(Main_Sizer)

    def InitCanvas(self):
        # add the x & y axis
        self.Canvas.AddLine([(0, 0), (0, 5)], LineWidth=2, LineColor='Yellow')
        self.Canvas.AddLine([(0, 0), (5, 0)], LineWidth=2, LineColor='Green')
        origin = self.Canvas.AddScaledTextBox('origin', (0, 0),
                                              Color='blue',
                                              Size=.5,
                                              PadSize=0,
                                              Width=None,
                                              LineColor=None,
                                              Family=wx.MODERN,
                                              Position='tr',
                                              Alignment='bottom',
                                              InForeground=True)
        # first Bind of node to EvtLeftDown
        origin.Bind(FloatCanvas.EVT_FC_LEFT_DOWN,
                    lambda evt, selctEnd='Origin':
                    self.EvtLeftDown(evt, 'Origin'))

        wx.CallAfter(self.Canvas.ZoomToBB)

    def OnDraw(self, evt):
        pts1 = (0, 0)
        
        x = [i for i in range(5, 30, 2)]
        y = x[::-1]
        pts2 = [(x[i], y[i]) for i in range(0, len(x))]
        alph = string.ascii_uppercase

        LnLbls = [alph[i] for i in range(0, len(x))]

        New_EndPt = True
        n = 0
        for pt in pts2:
            points = []
            points.append(pts1)
            points.append(pt)
            LnLbl = LnLbls[n]
            New_EndPt = True
            n += 1
            self.DrawLine(points, LnLbl, New_EndPt)

    def DrawLine(self, points, LnLbl, New_EndPt):
        '''Draws the line object as specified in the VarifyData() function'''

        # label the end point of the line in lower case
        if New_EndPt is True:
            new_end = self.Canvas.AddScaledTextBox(LnLbl.lower(), tuple(points[1]),
                                                   Color='black',
                                                   Size=.5,
                                                   PadSize=.2,
                                                   Width=None,
                                                   LineColor=None,
                                                   Family=wx.MODERN,
                                                   Position='cc',
                                                   Alignment='bottom',
                                                   InForeground=True)

            new_end.Bind(FloatCanvas.EVT_FC_LEFT_DOWN,
                         lambda evt, selctEnd=LnLbl.lower():
                         self.EvtLeftDown(evt, selctEnd))

        # define the new line
        self.Canvas.AddLine(points, LineWidth=2, LineColor='red')
        # add the new line to the list of lines

        self.Canvas.AddPoint(tuple(points[1]), 'black', 8)

        # locate the center of the new line for the label location
        lncntr = ((int(points[0][0])+int(points[1][0]))//2,
                  (int(points[0][1])+int(points[1][1]))//2)

        # place the new line lable
        new_line = self.Canvas.AddScaledTextBox(LnLbl, lncntr,
                                                Color='red',
                                                Size=.5,
                                                PadSize=None,
                                                Width=None,
                                                LineColor=None,
                                                Family=wx.MODERN,
                                                Position='tc',
                                                Alignment='bottom',
                                                InForeground=True)
        new_line.Name = LnLbl

        tic = time.perf_counter()
        new_line.Bind(FloatCanvas.EVT_FC_LEFT_DOWN, self.ObjLeftDown)
        toc = time.perf_counter()
        print(f'time to execute BIND function for DrawLine line 136 = {toc-tic:0.2f}')

        wx.CallAfter(self.Canvas.ZoomToBB)

    def ObjLeftDown(self, object):
        lbl = object.Name

        if lbl == 'Origin':
            self.Node(lbl)
        elif 65 <= ord(lbl) <= 90:
            print('you have selected line ', lbl)
        elif 97 <= ord(lbl) <= 122:
            print('you have selected node ', lbl)

    def EvtLeftDown(self, evt, lbl):
        if lbl == 'Origin':
            print('you have selected the origin')
        elif 97 <= ord(lbl) <= 122:
            print('you have selected node ', lbl)

# Run the program
if __name__ == "__main__":
    app = wx.App(False)
    frame = InputForm()
    frame.Center()
    frame.Show()
    app.MainLoop()
2 Answers

With minor adjustments to your code, I am unable to replicate the delays that you refer to.
No matter how many times I re-draw the lines, the timings, always fall within roughly the same time frame.
The bindings are to different objects, and it's just binding an event to an object, so I doubt that that is the issue.

import time
import string
import wx
from wx.lib.floatcanvas import NavCanvas, FloatCanvas
import wx.lib.colourdb


class InputForm(wx.Frame):
    '''set up the form and draw axis'''
    def __init__(self):
        super(InputForm, self).__init__(None, wx.ID_ANY, title='Plot Lines', size=(1300, 830))

        # set dictionary of points; key node letter, value tuple of point,
        self.pts = {}
        self.draw_repetitions = 0
        # create the form level sizer
        Main_Sizer = wx.BoxSizer(wx.HORIZONTAL)

        # add the sizer for the left side widgets
        sizerL = wx.BoxSizer(wx.VERTICAL)
        # add the grid and then set it ot he left panel

        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
        drw = wx.Button(self, -1, "Draw\nLines")
        btnsizer.Add(drw, 0, wx.ALL|wx.ALIGN_CENTER, 5)

        # bind the button events to handlers
        self.Bind(wx.EVT_BUTTON, self.OnDraw, drw)

        sizerL.Add((10, 20))
        sizerL.Add(btnsizer, 1, wx.ALIGN_CENTER)

        # add the draw panel
        self.rght = NavCanvas.NavCanvas(self,
                                   ProjectionFun=None,
                                   Debug=0,
                                   BackgroundColor="LIGHT GREY",
                                   )
        #self.Canvas = self.rght.Canvas

        self.InitCanvas()

        Main_Sizer.Add(sizerL, 0, wx.EXPAND)
        Main_Sizer.Add((10, 10))
        Main_Sizer.Add(self.rght, 1, wx.EXPAND)
        self.SetSizer(Main_Sizer)

    def InitCanvas(self):
        # add the x & y axis
        self.Canvas = self.rght.Canvas
        self.Canvas.ClearAll()
        self.Canvas.AddLine([(0, 0), (0, 5)], LineWidth=2, LineColor='Yellow')
        self.Canvas.AddLine([(0, 0), (5, 0)], LineWidth=2, LineColor='Green')
        origin = self.Canvas.AddScaledTextBox('origin', (0, 0),
                                              Color='blue',
                                              Size=.5,
                                              PadSize=0,
                                              Width=None,
                                              LineColor=None,
                                              Family=wx.MODERN,
                                              Position='tr',
                                              Alignment='bottom',
                                              InForeground=True)
        # first Bind of node to EvtLeftDown
        origin.Bind(FloatCanvas.EVT_FC_LEFT_DOWN,
                    lambda evt, selctEnd='Origin':
                    self.EvtLeftDown(evt, 'Origin'))

        wx.CallAfter(self.Canvas.ZoomToBB)

    def OnDraw(self, evt):
        self.InitCanvas()
        pts1 = (0, 0)
        
        x = [i for i in range(5, 30, 2)]
        y = x[::-1]
        pts2 = [(x[i], y[i]) for i in range(0, len(x))]
        alph = string.ascii_uppercase

        LnLbls = [alph[i] for i in range(0, len(x))]

        New_EndPt = True
        n = 0
        for pt in pts2:
            points = []
            points.append(pts1)
            points.append(pt)
            LnLbl = LnLbls[n]
            New_EndPt = True
            n += 1
            self.DrawLine(points, LnLbl, New_EndPt)

    def DrawLine(self, points, LnLbl, New_EndPt):
        '''Draws the line object as specified in the VarifyData() function'''
        self.draw_repetitions += 1
        # label the end point of the line in lower case
        if New_EndPt is True:
            new_end = self.Canvas.AddScaledTextBox(LnLbl.lower(), tuple(points[1]),
                                                   Color='black',
                                                   Size=.5,
                                                   PadSize=.2,
                                                   Width=None,
                                                   LineColor=None,
                                                   Family=wx.MODERN,
                                                   Position='cc',
                                                   Alignment='bottom',
                                                   InForeground=True)

            new_end.Bind(FloatCanvas.EVT_FC_LEFT_DOWN,
                         lambda evt, selctEnd=LnLbl.lower():
                         self.EvtLeftDown(evt, selctEnd))

        # define the new line
        self.Canvas.AddLine(points, LineWidth=2, LineColor='red')
        # add the new line to the list of lines

        self.Canvas.AddPoint(tuple(points[1]), 'black', 8)

        # locate the center of the new line for the label location
        lncntr = ((int(points[0][0])+int(points[1][0]))//2,
                  (int(points[0][1])+int(points[1][1]))//2)

        # place the new line lable
        new_line = self.Canvas.AddScaledTextBox(LnLbl, lncntr,
                                                Color='red',
                                                Size=.5,
                                                PadSize=None,
                                                Width=None,
                                                LineColor=None,
                                                Family=wx.MODERN,
                                                Position='tc',
                                                Alignment='bottom',
                                                InForeground=True)
        new_line.Name = LnLbl

        tic = time.perf_counter()
        new_line.Bind(FloatCanvas.EVT_FC_LEFT_DOWN, self.ObjLeftDown)
        toc = time.perf_counter()
        print(f'time to execute BIND function for DrawLine line ',LnLbl, toc-tic)
        print(f'Draw repetitions ',self.draw_repetitions)

       # wx.CallAfter(self.Canvas.ZoomToBB)
        self.Canvas.ZoomToBB()

    def ObjLeftDown(self, object):
        lbl = object.Name

        if lbl == 'Origin':
            self.Node(lbl)
            print(dir(self.Node))
        elif 65 <= ord(lbl) <= 90:
            print('you have selected line ', lbl)
        elif 97 <= ord(lbl) <= 122:
            print('you have selected node ', lbl)

    def EvtLeftDown(self, evt, lbl):
        if lbl == 'Origin':
            print('you have selected the origin')
        elif 97 <= ord(lbl) <= 122:
            print('you have selected node ', lbl)
#        try:
#            evt.Skip()
#        except:
#            pass

# Run the program
if __name__ == "__main__":
    app = wx.App(False)
    frame = InputForm()
    frame.Center()
    frame.Show()
    app.MainLoop()

enter image description here

TL;DR: As a workaround don't install wxpython via pip inside a venv, install the ubuntu package python3-wxgtk4.0.

I was facing the same issue you ran into - at least I think it is the same issue - and was, in a way, able to solve it. Running the code provided in the answer by Rolf of Saxony, the program would freeze for minutes when trying to draw the 12th or so line. After some minutes however, I at least got an exception out of it:

    Traceback (most recent call last):
  File "/bug_test.py", line 92, in OnDraw
    self.DrawLine(points, LnLbl, New_EndPt)
  File "/bug_test.py", line 138, in DrawLine
    new_line.Bind(FloatCanvas.EVT_FC_LEFT_DOWN, self.ObjLeftDown)
  File "/env/lib/python3.8/site-packages/wx/lib/floatcanvas/FCObjects.py", line 236, in Bind
    self.HitColor = next(self._Canvas.HitColorGenerator)
StopIteration

Which to me looks like some problem with the generation of the hit color. I guess the iterator generating it is for some reason not able to come up with an adequate hit color and thus runs for almost ever. Although interesting, this did not help in solving the problem. What helped was leaving my virtual environment. Initially I was running everything inside an venv and installed wxpython via pip. When I left the venv and installed wxpython via the ubuntu package python3-wxgtk4.0, everything worked as expected. So this is the workaround I found. Nevertheless the problem inside the venv persists. I suspect it might have something to do with the installation via pip which builds wxpython from source.

Related