# py -3 import wx import time class FrameA(wx.Frame): def __init__(self, parent = None, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_FRAME_STYLE, name = 'FrameA'): # ------------------------------------------ super().__init__( parent, id, title, pos, size, style, name) # ------------ # gui widgets self.tc = wx.TextCtrl(self, value="This is Frame A", style=wx.TE_MULTILINE | wx.TE_RICH2) self.buttonSwitch = wx.Button(self,label='Switch') # ------------ # layout sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.tc, 1, wx.EXPAND | wx.ALL, 0) sizer.Add(self.buttonSwitch, 1, wx.EXPAND | wx.ALL, 0) self.SetSizer(sizer) # Something to generate "continuous mock data" self.Timer = wx.Timer(self) # HW/OS limited resource self.Bind(wx.EVT_TIMER, self.OnTick) self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) # stop the timer self.Bind(wx.EVT_CLOSE, self.OnClose) # stop the timer self.Timer.Start(1000) # 1000 milliseconds ''' # The button widget, a child widget of frame, chosen # for this tutorial, sends a wx.CommandEvent type, # wx.EVT_BUTTON when the mouse is clicked over it. # # See wxPython and Pheonix documentation to see # which event types are a subclass of CommandEvent() # and which are a subclass of Event(). # # wx.CommandEvent types automatically propogated up # to the parent widget. Although wx.CommandEvent CAN # automatically propogate to the parent widget, # this does not mean the event automatically gets # handled. You have to Bind() an event to a handler # for an event to be handled. # # If the parent widget does not have a handler # the wx.CommandEvent event continues up the parent # hierarchy. # # If no parent, grandparent, etc. is found, the event # gets passed to wx.App(). wx.App() is not a widget and # not considered a "parent". # # If wx.App() does not set a handler for the event # using reference_to_app.Bind(wx.EVT_...) then the # event goes unhandled (ignored). # # So the parent frame here -can- Bind() to this event # type, like so: # #self.Bind(wx.EVT_BUTTON, self.OnButton) # # By binding a parent frame's method to handle # the wx.EVT_BUTTON event the frame can handle # the event in its own way, independent of how the # button handles the event. # # The frame can, and usually should, but doesn't have to, # use a local method to handle this event. I commented- # out the local OnButton() method below to show this. # In this example I did not use the frame's local method # OnButton(). # # I chose to handle the event in the App() instance. # # WHY? # I felt that the App() instance was the logical place to # handle switch-to-a-different-top-level-frame events. # # If we handle the switch-to-a-different-top-level-frame # event locally in the frame, then frameA would need # to somehow call methods of frameB. And vice verse, # frameB would need to somehow call frameA's Raise(), # Show() or SetFocus(). # # With more than two frames this can get ugly. You could # alternatively use something like custom events or # pubsub. But it made sense to me to let the "application" # manage it's resources. # # So I put the switch-to-a-different-top-level-frame handler # in App(). The App() instance needs to Bind() to the event # so App() knows what callback-method will be called to # handle that event. # # See App.OnButton(). # # SIDE NOTES: # wx.App() is singleton-like and is not considered a widget. # wx.App() is not a "parent" of any visual widget. # # This will fail: # app = wx.App() # frame = wx.Frame(parent=app, ...) # # An exception will be raised because "parent=" can not be # an wx.App object. # # Here, the "frame" reference is globally scoped. # You don't have to have "frame" as an attribute of "app" # but you can; # app.frame = wx.Frame(parent=None, ...) # # NON-CommandEvent type: # # Plain Event() types are sent only to the widget that # generated them. If the widget doesn't handle its # own event (you can set this) then the event goes # unhandled. Unless you push it up to its parent # widget. (OR PostEvent(some_destination_widget, event)) # # If you want to handle plain Event() types somewhere # in the widgets parent hierarchy, you need to set the # progation level of the -specific- event to do so. # You can easily do this with an event handler/method # of the widget itself and within that method call # event.Skip(). This sets the event's propagation level # to +1, meaning "send event to parent". # # Remember, the parent widget also needs to bind its method # to handle the event. And you can propagate a plain Event() # all the way to the App object by setting a bind/handler # in each parent widget of its child. "Top-Level-Frames" # pass unhandled events to App, IF the frame has a binding # and IF the App has a binding to the plain Event() type. # # ----------------------------------------------------- # WHY NOT handle the event within each FRAME instead of APP?: # ----------------------------------------------------- # # You can. It's doable. It's not hard. I find it not as clean... # for this case. However event propagation is tied to a widget's # parent hierarchy. # # App <-> frameA.panelA.buttonA <=> wx.EVT_BUTTON.GetId() == ButtonA # <-> frameB.panelB.buttonB <=> wx.EVT_BUTTON.GetId() == ButtonB # # FrameB.panelB and FrameB do not get wx.EVT_BUTTON from ButtonA, # even with event.Skip() and even with using: # # frameB.Bind(wx.EVT_BUTTON, frameB.OnButtonHandler, # source=frameA.panelA.buttonA) # # ButtonA wx.EVT_BUTTON does not get propagated to a different # parent hierarchy (I am pretty sure but not absolutely sure.) # # BUT YOU CAN get it to any widget/app object. You can # use wx.PostEvent(destination_widget, event). Again though, each # widget would likely have a way to get access unrelated widgets. # Another danger with the wx.PostEvent(and kin) approach is # you get a PyDeadObject exception if you try to reference # a widget that has been Destroy()ed. frameA can be destroyed # and frameB would need to check for that or you get the # exception. # # With the handling of the event in App you only need to write # code in one place instead of having all the top-level frames # check every other top level frame if they still are valid # widgets. # # The current coding trend is to decouple unrealated objects. # Event-driven systems, like wxPython is a way to do that. # Therefore it -could- be deemed countertuitive to tightly couple # unrelated widgets. Hence I tried to link the frames via an event # handler in an object that needs to know about the frames, which # is the app. The frames themselves do not need to know about each # other this way. The frames just need to know about the data # they show. # ''' self.Show() #def OnButton(self, event): # event.Skip() def OnTick(self, event): self.tc.AppendText( 'FrameA ' + time.asctime() + '\n') # event.Skip() unneccessary; event is handled def OnDestroy(self, event): print('frameA.OnDestroy()') event.Skip() def OnClose(self, event): print('frameA.OnClose()') try: # a good practice. # You could close threads here too for example self.Timer.Stop() except: pass event.Skip() # let wx.app get the event. class FrameB(wx.Frame): def __init__(self, parent = None, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_FRAME_STYLE, name = 'FrameB'): # ------------------------------------------ super().__init__(parent, id, title, pos, size, style, name) self.tc = wx.TextCtrl(self, value="This is the Frame B", style=wx.TE_MULTILINE | wx.TE_RICH2) self.buttonSwitch = wx.Button(self,label='Switch') sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.buttonSwitch, 1, wx.EXPAND | wx.ALL, 0) sizer.Add(self.tc, 1, wx.EXPAND | wx.ALL, 0) self.SetSizer(sizer) #self.Bind(wx.EVT_BUTTON, self.OnButton) # unneccessary # Something to generate "continuous mock data" self.Timer = wx.Timer(self) # HW/OS limited resource self.Bind(wx.EVT_TIMER, self.OnTick) self.Bind(wx.EVT_CLOSE, self.OnClose) # allow veto handling self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) # stop the timer self.Timer.Start(500) # 1000 milliseconds self.Show() #def OnButton(self, event): # event.Skip() def OnTick(self, event): self.tc.AppendText( 'FrameB ' + time.asctime() + '\n') # event.Skip() unneccessary; event is handled def OnClose(self, event): print('frameB.OnClose()') event.Skip() def OnDestroy(self, event): event.Skip() print('frameB.OnDestroy()') try: self.Timer.Stop() except: pass class App(wx.App): """In this example an App() instance object acts as the "window manager".""" def OnInit(self): self.frameA = FrameA(None, title='Frame A') self.frameB = FrameB(None, title='Frame B') self.Bind(wx.EVT_BUTTON, self.OnButton) #self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) # stop the timer self.frameA.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) self.frameB.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) self.frameA.Bind(wx.EVT_CLOSE, self.OnCloseA) self.frameB.Bind(wx.EVT_CLOSE, self.OnCloseB) return True def OnCloseA(self, event): print('app.OnCloseA({})'.format(event.GetEventObject().GetName())) if not self.frameA: print(' frameA dead') if not self.frameB: print(' frameB dead') else: if hasattr(self, 'OnClosesA'): wx.CallAfter(self.frameB.Destroy) else: self.OnClosesB = True wx.CallAfter(self.frameB.Close) event.Skip() # if not issued, doesn't work def OnCloseB(self, event): print('app.OnCloseB({})'.format(event.GetEventObject().GetName())) if not self.frameA: print(' frameA dead') else: #self.frameA.Close() if hasattr(self, 'OnClosesB'): wx.CallAfter(self.frameA.Destroy) else: self.OnClosesA = True wx.CallAfter(self.frameA.Close) if not self.frameB: print(' frameB dead') event.Skip() # if not issued, doesn't work def OnButton(self, event): '''This is an event handler previously bound to wx.EVT_BUTTON. wx.EVT_BUTTON is a command event. Command events are passed to the parent widget automatically. Notice there are no "button" widgets as direct children of App(), as App() is not a widget. But also notice App() can get and handle events from widgets. The App() instance is basically the last stop for unhandled events, IF the event gets to it. Command Events automatically propagate up a widget's parent hierarchy. Plain Event() types do not automatically propagate up to a parent. You can set a flag of an event so that it does propagate upward. In an event handler use event.Skip() to set the flag. The flag will be checked when the event-handler (i.e. method) exits. If you wish a parent widget to handle Non-command events you need to pass (propagate) the event to the widget's parent from inside a local (to the child widget) event handler by using event.Skip(). Skip() just updates a flag. It can be called any where in an event handler to set the propogation to the parent.''' frame = event.GetEventObject().GetParent() if frame == self.frameA: if self.frameB: self.frameB.Raise() self.frameB.SetFocus() else: self.frameA.buttonSwitch.Disable() elif frame == self.frameB: if self.frameA: self.frameA.Raise() self.frameA.SetFocus() else: self.frameB.buttonSwitch.Disable() def OnDestroy(self, event): '''We are getting an event for destroying a wx widget. This does not 'del' a Python reference, just the wx-widget object the named-refernce points too. Basically, if we are closing a frame, close/destroy all the other top-level frames too. Also only "Destroy()" a widget if there is a valid wx widget, not a PyDeadObject. You could alternatively prevent destroying a widget, like a top-level frame, if you want the user to "close" the entire app only from (a) certain widget(s) or conditions. Instead you would call frame.Lower() or frame.Hide() or frame.Raise(). And persaude the user to close the app from where you want them to do it. I arbitrarily chose that closing any top-level frame, closed all top-level frames. WHY? If you hide any frame with no way to "Show()" it again the wx App will appear to hang when you close all the visible frames. In actuallity the hidden frame is still "running", it's just hidden. On a computer CTRL+C or CTRL+BREAK will "kill" the wxPython app, but this is messy. ''' print('app.OnDestroy( {} )'.format(event.GetEventObject().GetName())) event.Skip() if __name__ == "__main__": app = App() app.MainLoop()