dega
March 11, 2024, 7:16pm
1
Hi!
I’m beginning with wxPython.
Environment: Linux (GTK), wxPython 4.2.0
I need to implement a mouse-event listener monitoring the whole screen (my app is running fullscreen).
A way I used with other GUIs was to bind a listener to a transparent panel covering the whole screen.
I’m discovering that it’s not possible with wxPython because mouse events do not propagate through the chain of event handlers. So only the lowest level will receive the event.
I read that I can subclass EventHandler and use tryBefore() or tryAfter() but it looks like an ugly hack.
Is there a better way to solve my problem?
Here’s a runnable program (Matplotlob is required).
#!/usr/bin/python3
import matplotlib as mpl
mpl.use( 'wxAgg')
import wx
class UpperPanel( wx.Panel):
def __init__( self, parent):
wx.Panel.__init__( self, parent)
self.create_MPL()
self.createTools()
self.Bind( wx.EVT_MOUSE_EVENTS, self.actMouse)
self.sz1 = wx.BoxSizer( wx.VERTICAL)
self.sz1.Add( self.canvas, proportion=1, flag=wx.TOP | wx.EXPAND)
self.sz1.Add( self.tools_MPL, proportion=0, flag=wx.BOTTOM)
self.sz2 = wx.BoxSizer( wx.VERTICAL)
self.sz2.Add( self.tools, proportion=0, flag=wx.TOP )
self.sz3 = wx.BoxSizer( wx.HORIZONTAL)
self.sz3.Add( self.sz1, proportion=1, flag=wx.EXPAND)
self.sz3.Add( self.sz2, proportion=0)
self.SetSizer( self.sz3)
def create_MPL( self):
import matplotlib.figure
import matplotlib.backends.backend_wxagg
self.fig = matplotlib.figure.Figure( layout='constrained')
self.ax = self.fig.add_subplot(111)
self.canvas = mpl.backends.backend_wxagg.FigureCanvasWxAgg( self, -1, self.fig)
self.tools_MPL = mpl.backends.backend_wxagg.NavigationToolbar2WxAgg( self.canvas)
self.tools_MPL.Realize()
def toolButton( self, label, handler):
btn = wx.Button( self, wx.ID_ANY, label=label)
btn.Bind( wx.EVT_BUTTON, handler, btn)
self.tools.Add( btn, proportion=0, flag=wx.ALL | wx.CENTER, border=5)
def createTools( self):
self.tools = wx.BoxSizer( wx.VERTICAL) # create toolbox
self.toolButton( 'T', self.action)
self.toolButton( 'F', self.action)
def action( self, event):
print( 'Action on', event.GetEventObject().LabelText)
def actMouse( self, év):
cmd = év.GetEventObject()
print( "Pos:",wx.GetMousePosition())
év.Skip( True)
class Win( wx.Frame):
def __init__( self):
wx.Frame.__init__( self, None, -1, size=(1200,900))
self.panel = UpperPanel( self)
class App( wx.App):
def OnInit( self):
win = Win()
self.SetTopWindow( win)
win.Show( True)
return True
if __name__ == "__main__" :
app = App( False)
app.MainLoop()
I’m expecting mouse motion events over the whole app but I don’t get any over the Matplotlib canvas or Matplotlib toolbar and even over my own buttons (on right side).
Where’s my problem?
well, what about Event.Skip()
dega
March 11, 2024, 9:32pm
3
Thanks for your answer.
what about Event.Skip()
Two reasons:
Document Events and Event Handling — wxPython Phoenix 4.2.2 documentation mention:
“the events of the classes deriving from [ wx.CommandEvent] are propagated by default to the parent window.”
Mouse events do not derive from CommandEvent.
Second: In our application, one of the low-level panels is a Matplotlib canvas. I don’t want to modify their event handler to add a Skip().
dega:
Two reasons
well, your transparent window has to Skip (Skip is just the manual command behaviour)
It’s always best to provide a runnable code sample. Otherwise you won’t get solutions, except if someone else had exactly the same problem.
dega
March 12, 2024, 1:11pm
6
@DietmarSchwertberger I edited my post to include a curated version of the program.
To get the events for the buttons and the canvas you can bind the events like this:
self.canvas.Bind( wx.EVT_MOUSE_EVENTS, self.actMouse)
self.btn1.Bind( wx.EVT_MOUSE_EVENTS, self.actMouse)
self.btn2.Bind( wx.EVT_MOUSE_EVENTS, self.actMouse)
self.tools_MPL.Bind( wx.EVT_MOUSE_EVENTS, self.actMouse)
( https://wiki.wxpython.org/self.Bind%20vs.%20self.button.Bind )
If the number of widgets is limited, I would suggest you do this.
A more generic way would be to create the windows and then recursively walk through your hierarchy of windows and children and bind each.
You can see how to walk through here in StructurePrinter
: https://github.com/wxGlade/wxGlade/blob/206a3679e431f927435e3f72d264087e3d857ea9/utilities.py
The filtering approach with TryBefore
or EventFilter
will probably create more overhead as each and every event will go through your Python code.
wx.EventFilter — wxPython Phoenix 4.2.1 documentation
wx.EvtHandler — wxPython Phoenix 4.2.1 documentation
I have never used these, I think, but you don’t need to subclass, just add a TryBefore
method to your panel, which is derived from wx.EventHandler
class UpperPanel( wx.Panel):
...
def TryBefore(self, event):
print(event)
return wx.Panel.TryBefore(self, event)
dega:
I edited my post
well, Matplotlib has its own AI
oops, I forgot the playground
import sys
import wx
from wx import aui
import matplotlib as mpl
class MplPanel( wx.Panel):
def __init__( self, parent):
super().__init__(parent)
self.create_MPL()
self.createTools()
self.Bind( wx.EVT_MOUSE_EVENTS,
lambda _: print(f'mpl wx Pos: {wx.GetMousePosition()}'))
hbox = wx.BoxSizer(wx.HORIZONTAL)
vbox = wx.BoxSizer( wx.VERTICAL)
vbox.Add( self.canvas, wx.EXPAND)
vbox.Add( self.tools_MPL)
hbox.Add(vbox, wx.EXPAND)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add( self.tools)
hbox.Add(vbox)
self.SetSizer(hbox)
def create_MPL( self):
import matplotlib.figure
import matplotlib.backends.backend_wxagg
self.fig = matplotlib.figure.Figure( layout='constrained')
self.ax = self.fig.add_subplot(111)
self.canvas = mpl.backends.backend_wxagg.FigureCanvasWxAgg(
self, -1, self.fig)
self.canvas.mpl_connect(
'motion_notify_event', lambda e: print(f'mpl Pos: ({e.x}, {e.y})'))
self.tools_MPL = mpl.backends.backend_wxagg.NavigationToolbar2WxAgg(
self.canvas)
self.tools_MPL.Realize()
def toolButton( self, label, handler):
btn = wx.Button(self, label=label)
btn.Bind( wx.EVT_BUTTON, handler, btn)
self.tools.Add( btn, proportion=0, flag=wx.ALL | wx.CENTER, border=5)
def createTools( self):
self.tools = wx.BoxSizer( wx.VERTICAL) # create toolbox
self.toolButton( 'T', self.action)
self.toolButton( 'F', self.action)
def action( self, evt):
print( 'Action on', evt.GetEventObject().LabelText)
class Log(wx.Panel):
def __init__(self, parent):
super().__init__(parent)
log = wx.TextCtrl(
self,
style=wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL)
log.SetBackgroundColour(wx.SystemSettings.GetColour(
wx.SYS_COLOUR_HIGHLIGHT))
log.SetForegroundColour(wx.SystemSettings.GetColour(
wx.SYS_COLOUR_HIGHLIGHTTEXT))
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(log, 1, wx.LEFT|wx.TOP|wx.RIGHT|wx.EXPAND, 5)
self.SetSizer(vbox)
sys.stdout = log
print('Logging started..')
self.Show()
log.Bind( wx.EVT_MOUSE_EVENTS,
lambda _: print(f'wx Pos: {wx.GetMousePosition()}'))
class Gui(wx.Frame):
def __init__(self):
super().__init__(None, title='Mpl events')
mb = wx.MenuBar()
help_menu = wx.Menu()
ID_About = wx.NewIdRef()
help_menu.Append(ID_About, "restore pains...")
mb.Append(help_menu, "Help")
def menu_about(_):
for entry in self.mgr.GetAllPanes():
if not entry.IsShown():
entry.Show()
self.mgr.Update()
self.Bind(wx.EVT_MENU, menu_about, id=ID_About)
self.SetMenuBar(mb)
self.mgr = aui.AuiManager(
flags=wx.aui.AUI_MGR_DEFAULT|wx.aui.AUI_MGR_LIVE_RESIZE)
self.mgr.SetManagedWindow(self)
self.mgr.Bind(aui.EVT_AUI_PANE_CLOSE,
lambda evt: print(f"on close: '{evt.GetPane().caption}'"))
p0 = MplPanel(self)
p1 = Log(self)
s = self.GetSize()
self.mgr.AddPane(p0, aui.AuiPaneInfo().Floatable().
Caption('My Plot').MinSize(s - (int(s[0]*0.5), 0)).Dock())
self.mgr.AddPane(p1, aui.AuiPaneInfo().Float().
Caption("Log").MinSize(s - (int(s[0]*0.5), 0)).Dockable())
self.mgr.Update()
self.Show()
self.Bind( wx.EVT_MOUSE_EVENTS,
lambda _: print(f'wx Pos: {wx.GetMousePosition()}'))
app = wx.App()
Gui()
app.MainLoop()