## page was renamed from Controlling GUI with Publisher == Introduction == You are looking for a smoother solution to the problem described in [[Pairing Notebook Panel to Frame Menus]]? Or perhaps an example of how to {{{wx.lib.pubsub}}}? This page is a good place to start. See also the WxLibPubSub page for general info on pubsub and how it fits in wxPython, and the [[http://pubsub.sf.net|pubsub home page]] for additional documentation and examples. Note: Pubsub has two APIs for messaging: they are referred to as 'kwargs' and 'arg1'. The code on the this page uses the 'kwargs' API, which became the default in wx.lib.pubsub *after* wxPython 2.8.11.0. Prior to this, the default wx.lib.pubsub API was the 'arg1' API. If you you want to use the now deprecated 'arg1' API, the code below won't work. You will have to insert a {{{from wx.lib.pubsub import setuparg1}}} before the first pubsub import statement seen by your application (typically, in your startup script), and the {{{sendMessage()}}} calls will have to be changed (see the [[http://pubsub.sourceforge.net/usage/howtos/index.html|migration docs]] for more info). Pubsub is now a standalone library hosted on !SourceForge (wx.lib.pubsub is now a verbatim copy of the pubsub from that site) so if you are having trouble using the below code, post to the [[http://googlegroups.com/group/pypubsub|pubsub project users group]] (or the [[https://groups.google.com/forum/#!forum/wxpython-users|wxPython users group]] of course). == What Objects are Involved == 1. Two wx.Panels 1. Two wx.Menus to match those panels 1. One wx.Notebook 1. One wx.Frame with a wx.Menubar and wx.Statusbar The menubar will have a tab menu, and the matching menus == Process Overview == Build and connect the various parts with the following methods for switching tabs in the application: CHANGING A NOTEBOOK PAGE should... * enable the menu for the selected page * disable the menu for the other pages * disable the tabmenu item for the selected page * enable the tabmenu item for the other pages * change the status bar to note the change SELECTING AN OPTION IN THE TABMENU should... * change the notebook page * disable the tabmenu item for the selected page * enable the tabmenu item for the other pages * enable the menu for the current page * disable the menus for the other pages * change the status bar to note the change == Special Concerns == Finding the right usage of pubsub messages is key. It is tempting to have one pubsub message 'notebook.change' generated with either method, and having the frame and notebook respond. This can lead to confusion, so think of pubsub messages as specific messages to specific items: * The Notebook listens for messages of the 'notebook.select' topic * The Frame listens for messages of the 'statusbar.update', 'statusbar.clear', 'tabmenu.change' and 'menubar.change' It is a good idea to keep the Frame subscribed to general topics, and use the handler to decide what to do specifically based on the sub topics (if any) of the message. Sidenote: If you have problems getting the panels to respond to menu events, there is a simple solution in the panel code for this: {{{ top=self.GetTopLevelParent() top.Bind(wx.EVT_MENU,self.OnMeanie,id=meanieId) }}} == Code Sample == {{{ #!python """GUIPubSub.py Use pubsub to control the GUI. """ import wx from wx.lib.pubsub import pub class BasePanel(wx.Panel): ### This panel assures the panel has a menu def __init__(self, parent, *args, **kwds): assert 'name' in kwds, "Panel must have a name" self.Menu = kwds.pop('menu', None) if not self.Menu: self.Menu = wx.Menu() wx.Panel.__init__(self, parent, *args, **kwds) sizer = wx.BoxSizer(wx.VERTICAL) self.label = wx.StaticText(self, label="This is a panel") sizer.Add(self.label) self.SetSizer(sizer) def GetMenu(self): """Returns the menu associated with this panel""" return self.Menu class BluePanel(BasePanel): ### Subclass BasePanel with the particulars def __init__(self, parent): blueMenu = wx.Menu() meanieId = wx.NewId() blueMenu.Append(meanieId, "Blue meanie", "It's a blue world, Max.") BasePanel.__init__(self, parent, menu = blueMenu, name="Blue") self.SetBackgroundColour("Light Blue") ###Menu events are processed at the frame level, ### but by the time the panel is created, we have ### a frame and a notebook, so we can get to it ### with GetTopLevelParent() ### It's better than trying to bind everything at the ### Frame level. do it here instead. top=self.GetTopLevelParent() top.Bind(wx.EVT_MENU, self.OnMeanie, id=meanieId) def OnMeanie(self, evt): pub.sendMessage('statusbar.update', status='Send in the Apple Bonkers!') class RedPanel(BasePanel): def __init__(self, parent): redMenu = wx.Menu() redMenu.Append(wx.ID_ANY, "Red Five", "I'm going in.") BasePanel.__init__(self, parent, menu=redMenu, name="Red") self.SetBackgroundColour("Pink") sizer = self.GetSizer() aButton = wx.Button(self, label="Luke, are you okay?") sizer.Add(aButton) self.Layout() self.Bind(wx.EVT_BUTTON, self.OnButton, aButton) def OnButton(self, evt): pub.sendMessage('statusbar.update', status="I'm okay but R2 is in trouble") class MyNotebook(wx.Notebook): ### The notebook keeps track of pages ### We are subclassing to keep track of events and accept messages def __init__(self, parent, *args, **kwds): wx.Notebook.__init__(self, parent, *args, **kwds) bluePanel = BluePanel(self) redPanel = RedPanel(self) self.AddPage(bluePanel, bluePanel.GetName()) self.AddPage(redPanel, redPanel.GetName()) ### Bind events ### We use the page changed event to catch when the user clicks ### on a tab. ### To prevent this from happening, catch the ### wx.EVT_NOTEBOOK_PAGE_CHANGING event and Veto the event if needed. self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged) ### Subscribe to messages pub.subscribe(self.OnChange, 'notebook.select') def OnPageChanged(self, evt): name = self.GetPageText(evt.GetSelection()) pub.sendMessage('tabmenu.change', itemLabel=name) pub.sendMessage('menubar.change', itemLabel=name) evt.Skip() def OnChange(self, index): name = self.ChangeSelection(index-1) class MyFrame(wx.Frame): def __init__(self, parent, *args, **kwds): wx.Frame.__init__(self, parent, *args, **kwds) mbar = wx.MenuBar() sbar = wx.StatusBar(self) self.Book = MyNotebook(self) self.SetMenuBar(mbar) self.SetStatusBar(sbar) ### Create the tab menu self.tabMenu = tb = wx.Menu() mbar.Append(tb, "&Tabs") ### Populate the tab menu and the menubar for idx in range(self.Book.GetPageCount()): page = self.Book.GetPage(idx) tb.Append(idx+1, "%s\tCtrl-%d" % (page.GetName(), idx+1), "Go to %s page" % (page.GetName())) ### Additions to the menu bar menu = page.GetMenu() mbar.Append(menu, page.GetName()) tb.AppendSeparator() tb.Append(wx.ID_CLOSE, "Close\tAlt-X", "Run away ... terribly fast.") # Add a help menu hMenu = wx.Menu() hMenu.Append(wx.ID_ABOUT) mbar.Append(hMenu, "&Help") self.SetStatusText("Welcome",0) ### EVENT BINDINGS self.Bind(wx.EVT_MENU, self.OnClose, id=wx.ID_CLOSE) self.Bind(wx.EVT_MENU, self.OnAbout, id=wx.ID_ABOUT) self.Bind(wx.EVT_MENU, self.OnTabMenu, id=0, id2=self.Book.GetPageCount()) ### MESSAGE SUBSCRIPTIONS pub.subscribe(self.OnStatusUpdate, 'statusbar.update') pub.subscribe(self.OnStatusClear, 'statusbar.clear') pub.subscribe(self.OnTabMenuMessage, 'tabmenu.change') pub.subscribe(self.OnMenuBarMessage, 'menubar.change') ### SEND INITIAL MESSAGES idx = self.Book.GetSelection() name = self.Book.GetPageText(idx) pub.sendMessage('tabmenu.change', itemLabel=name) pub.sendMessage('menubar.change', itemLabel=name) def OnAbout(self, evt): pub.sendMessage('statusbar.update', status='Bragging about this app') wx.MessageBox("This is my program. Muy Neato, huh?", "About this App", wx.OK) def OnClose(self, evt): self.Close(True) def OnStatusUpdate(self, status): self.SetStatusText(status,0) def OnStatusClear(self): self.SetStatusText('',0) def OnTabMenuMessage(self, itemLabel): ### Respond to a message to change the tabmenu ### Disable all tab menu items (except the close item) for idx in range(self.Book.GetPageCount()): mi = self.tabMenu.FindItemById(idx+1) if mi.GetLabel() == itemLabel: mi.Enable(False) # Don't want to be able to switch to the current page else: mi.Enable(True) ### Another way to do this: #mi.Enable(mi.GetLabel() != itemLabel) ### The problem with this is there are several logical steps taken in one line def OnMenuBarMessage(self, itemLabel): ### Disable all page menus but the current one mbar = self.GetMenuBar() for idx in range(self.Book.GetPageCount()): page = self.Book.GetPage(idx) ## returns the page menu = mbar.FindMenu(page.GetName()) ## returns an integer if page.GetName() == itemLabel: mbar.EnableTop(menu, True) else: mbar.EnableTop(menu, False) def OnTabMenu(self, evt): ### Get the label of the menu item mbar=self.GetMenuBar() mi = mbar.FindItemById(evt.GetId()) ### Send the messages pub.sendMessage('notebook.select', index = evt.GetId()) pub.sendMessage('menubar.change', itemLabel = mi.GetLabel()) pub.sendMessage('tabmenu.change', itemLabel = mi.GetLabel()) class App(wx.App): def OnInit(self): frame = MyFrame(None, title="Notebook Application") frame.CenterOnScreen() self.SetTopWindow(frame) frame.Show() return True a = App(False) a.MainLoop() }}} == Discussion == Part of this program simply reports changes in the notebook in the frame's statusbar. It is just as easy to use: {{{ self.GetTopLevelParent().SetStatusText('page changed') }}} but why go through all that? pubsub gets the message across and makes for more readable code. One thing to note about pubsub-subscribe architectures is that in large applications, it can become difficult to keep track of message topics, sequences of listeners called and secondary messages created (a message created as a result of a first message). The !PyPubSub version 3 API attempts to support pubsub-subscribe architectures within larger applications by facilitating documented topic trees, requiring named message data arguments, providing a more generic notification system to track what pubsub is doing, and giving more descriptive error messages about pubsub errors. == Example: A Self-Hiding pubsub-aware statusbar gauge == Here is one way to place a gauge in a status bar that can accept updates from any long process. {{{ #!python import time import wx try: from pubsub import pub except ImportError: from wx.lib.pubsub import pub class ListeningGauge(wx.Gauge): def __init__(self, *args, **kwargs): wx.Gauge.__init__(self, *args, **kwargs) pub.subscribe(self.start_listening, "progress_awake") pub.subscribe(self.stop_listening, "progress_sleep") def _update(self, this, total): try: self.SetRange(total) self.SetValue(this) except Exception as e: print e def start_listening(self, listen_to): rect = self.Parent.GetFieldRect(1) self.SetPosition((rect.x+2, rect.y+2)) self.SetSize((rect.width-4, rect.height-4)) self.Show() pub.subscribe(self._update, listen_to) def stop_listening(self, listen_to): pub.unsubscribe(self._update, listen_to) self.Hide() class MainWindow(wx.Frame): def __init__(self, parent, title): wx.Frame.__init__(self, parent, title=title) status = self.statusbar = self.CreateStatusBar() # A StatusBar in the bottom of the window status.SetFieldsCount(3) status.SetStatusWidths([-2,200,-1]) self.progress_bar = ListeningGauge(self.statusbar, style=wx.GA_HORIZONTAL|wx.GA_SMOOTH) rect = self.statusbar.GetFieldRect(1) self.progress_bar.SetPosition((rect.x+2, rect.y+2)) self.progress_bar.SetSize((rect.width-4, rect.height-4)) self.progress_bar.Hide() panel = wx.Panel(self) shortButton = wx.Button(panel, label="Run for 3 seconds") longButton = wx.Button(panel, label="Run for 6 seconds") sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(shortButton, 0, wx.ALL, 10) sizer.Add(longButton, 0, wx.ALL, 10) panel.SetSizerAndFit(sizer) shortButton.Bind(wx.EVT_BUTTON, self.Run3) longButton.Bind(wx.EVT_BUTTON, self.Run6) def Run3(self, event): pub.sendMessage('progress_awake', listen_to = 'short_update') wx.BeginBusyCursor() for x in range(6): pub.sendMessage('short_update', this = x+1, total = 6) time.sleep(0.5) wx.EndBusyCursor() pub.sendMessage('progress_sleep', listen_to = 'short_update') def Run6(self, event): pub.sendMessage('progress_awake', listen_to = 'long_update') wx.BeginBusyCursor() for x in range(12): pub.sendMessage('long_update', this = x+1, total = 12) time.sleep(0.5) wx.EndBusyCursor() pub.sendMessage('progress_sleep', listen_to = 'long_update') app = wx.App(False) frame = MainWindow(None, "Listening Gauge Demo") frame.Show() app.MainLoop() }}} === Comments === Post questions here, on the wxPython users forum, or the pubsub help forum. Don't forget to look at WxLibPubSub and the [[http://pubsub.sf.net|pubsub home page]].