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 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 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 pubsub project users group (or the wxPython users group of course).

What Objects are Involved

  1. Two wx.Panels
  2. Two wx.Menus to match those panels
  3. One wx.Notebook
  4. 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...

SELECTING AN OPTION IN THE TABMENU should...

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:

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

   1 """GUIPubSub.py
   2 Use pubsub to control the GUI.
   3 """
   4 
   5 import wx
   6 from wx.lib.pubsub import pub
   7 
   8 class BasePanel(wx.Panel):
   9     ### This panel assures the panel has a menu
  10     def __init__(self, parent, *args, **kwds):
  11         assert 'name' in kwds, "Panel must have a name"
  12         self.Menu = kwds.pop('menu', None)
  13         if not self.Menu: self.Menu = wx.Menu()
  14 
  15         wx.Panel.__init__(self, parent, *args, **kwds)
  16         sizer = wx.BoxSizer(wx.VERTICAL)
  17         self.label = wx.StaticText(self, label="This is a panel")
  18         sizer.Add(self.label)
  19         self.SetSizer(sizer)
  20 
  21     def GetMenu(self):
  22         """Returns the menu associated with this panel"""
  23         return self.Menu
  24 
  25 class BluePanel(BasePanel):
  26     ### Subclass BasePanel with the particulars
  27     def __init__(self, parent):
  28         blueMenu = wx.Menu()
  29         meanieId = wx.NewId()
  30         blueMenu.Append(meanieId, "Blue meanie", "It's a blue world, Max.")
  31         BasePanel.__init__(self, parent, menu = blueMenu, name="Blue")
  32         self.SetBackgroundColour("Light Blue")
  33 
  34         ###Menu events are processed at the frame level,
  35         ### but by the time the panel is created, we have
  36         ### a frame and a notebook, so we can get to it
  37         ### with GetTopLevelParent()
  38         ### It's better than trying to bind everything at the
  39         ### Frame level. do it here instead.
  40         top=self.GetTopLevelParent()
  41         top.Bind(wx.EVT_MENU, self.OnMeanie, id=meanieId)
  42 
  43     def OnMeanie(self, evt):
  44         pub.sendMessage('statusbar.update', status='Send in the Apple Bonkers!')
  45 
  46 class RedPanel(BasePanel):
  47     def __init__(self, parent):
  48         redMenu = wx.Menu()
  49         redMenu.Append(wx.ID_ANY, "Red Five", "I'm going in.")
  50         BasePanel.__init__(self, parent, menu=redMenu, name="Red")
  51         self.SetBackgroundColour("Pink")
  52         sizer = self.GetSizer()
  53         aButton = wx.Button(self, label="Luke, are you okay?")
  54         sizer.Add(aButton)
  55         self.Layout()
  56 
  57         self.Bind(wx.EVT_BUTTON, self.OnButton, aButton)
  58 
  59     def OnButton(self, evt):
  60         pub.sendMessage('statusbar.update', status="I'm okay but R2 is in trouble")
  61 
  62 class MyNotebook(wx.Notebook):
  63     ### The notebook keeps track of pages
  64     ### We are subclassing to keep track of events and accept messages
  65     def __init__(self, parent, *args, **kwds):
  66         wx.Notebook.__init__(self, parent, *args, **kwds)
  67 
  68         bluePanel = BluePanel(self)
  69         redPanel = RedPanel(self)
  70 
  71         self.AddPage(bluePanel, bluePanel.GetName())
  72         self.AddPage(redPanel, redPanel.GetName())
  73 
  74         ### Bind events
  75         ### We use the page changed event to catch when the user clicks
  76         ### on a tab.
  77         ### To prevent this from happening, catch the
  78         ###  wx.EVT_NOTEBOOK_PAGE_CHANGING event and Veto the event if needed.
  79         self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged)
  80 
  81         ### Subscribe to messages
  82         pub.subscribe(self.OnChange, 'notebook.select')
  83 
  84     def OnPageChanged(self, evt):
  85         name = self.GetPageText(evt.GetSelection())
  86         pub.sendMessage('tabmenu.change', itemLabel=name)
  87         pub.sendMessage('menubar.change', itemLabel=name)
  88         evt.Skip()
  89 
  90     def OnChange(self, index):
  91         name = self.ChangeSelection(index-1)
  92 
  93 
  94 class MyFrame(wx.Frame):
  95     def __init__(self, parent, *args, **kwds):
  96         wx.Frame.__init__(self, parent, *args, **kwds)
  97         mbar = wx.MenuBar()
  98         sbar = wx.StatusBar(self)
  99 
 100         self.Book = MyNotebook(self)
 101 
 102         self.SetMenuBar(mbar)
 103         self.SetStatusBar(sbar)
 104         ### Create the tab menu
 105         self.tabMenu = tb = wx.Menu()
 106         mbar.Append(tb, "&Tabs")
 107         ### Populate the tab menu and the menubar
 108         for idx in range(self.Book.GetPageCount()):
 109             page = self.Book.GetPage(idx)
 110             tb.Append(idx+1,
 111                       "%s\tCtrl-%d" % (page.GetName(), idx+1),
 112                       "Go to %s page" % (page.GetName()))
 113             ### Additions to the menu bar
 114             menu = page.GetMenu()
 115             mbar.Append(menu, page.GetName())
 116 
 117         tb.AppendSeparator()
 118         tb.Append(wx.ID_CLOSE, "Close\tAlt-X", "Run away ... terribly fast.")
 119         # Add a help menu
 120         hMenu = wx.Menu()
 121         hMenu.Append(wx.ID_ABOUT)
 122         mbar.Append(hMenu, "&Help")
 123         self.SetStatusText("Welcome",0)
 124 
 125         ### EVENT BINDINGS
 126         self.Bind(wx.EVT_MENU, self.OnClose, id=wx.ID_CLOSE)
 127         self.Bind(wx.EVT_MENU, self.OnAbout, id=wx.ID_ABOUT)
 128         self.Bind(wx.EVT_MENU, self.OnTabMenu, id=0, id2=self.Book.GetPageCount())
 129 
 130         ### MESSAGE SUBSCRIPTIONS
 131         pub.subscribe(self.OnStatusUpdate,   'statusbar.update')
 132         pub.subscribe(self.OnStatusClear,    'statusbar.clear')
 133         pub.subscribe(self.OnTabMenuMessage, 'tabmenu.change')
 134         pub.subscribe(self.OnMenuBarMessage, 'menubar.change')
 135 
 136         ### SEND INITIAL MESSAGES
 137         idx = self.Book.GetSelection()
 138         name = self.Book.GetPageText(idx)
 139         pub.sendMessage('tabmenu.change', itemLabel=name)
 140         pub.sendMessage('menubar.change', itemLabel=name)
 141 
 142     def OnAbout(self, evt):
 143         pub.sendMessage('statusbar.update', status='Bragging about this app')
 144         wx.MessageBox("This is my program. Muy Neato, huh?",
 145                       "About this App",
 146                       wx.OK)
 147 
 148     def OnClose(self, evt):
 149         self.Close(True)
 150 
 151     def OnStatusUpdate(self, status):
 152         self.SetStatusText(status,0)
 153 
 154     def OnStatusClear(self):
 155         self.SetStatusText('',0)
 156 
 157     def OnTabMenuMessage(self, itemLabel):
 158         ### Respond to a message to change the tabmenu
 159         ### Disable all tab menu items (except the close item)
 160         for idx in range(self.Book.GetPageCount()):
 161             mi = self.tabMenu.FindItemById(idx+1)
 162             if mi.GetLabel() == itemLabel:
 163                 mi.Enable(False) # Don't want to be able to switch to the current page
 164             else:
 165                 mi.Enable(True)
 166             ### Another way to do this:
 167             #mi.Enable(mi.GetLabel() != itemLabel)
 168             ### The problem with this is there are several logical steps taken in one line
 169 
 170     def OnMenuBarMessage(self, itemLabel):
 171         ### Disable all page menus but the current one
 172         mbar = self.GetMenuBar()
 173         for idx in range(self.Book.GetPageCount()):
 174             page = self.Book.GetPage(idx) ## returns the page
 175             menu = mbar.FindMenu(page.GetName())  ## returns an integer
 176 
 177             if page.GetName() == itemLabel:
 178                 mbar.EnableTop(menu, True)
 179             else:
 180                 mbar.EnableTop(menu, False)
 181 
 182     def OnTabMenu(self, evt):
 183         ### Get the label of the menu item
 184         mbar=self.GetMenuBar()
 185         mi = mbar.FindItemById(evt.GetId())
 186         ### Send the messages
 187         pub.sendMessage('notebook.select', index = evt.GetId())
 188         pub.sendMessage('menubar.change', itemLabel = mi.GetLabel())
 189         pub.sendMessage('tabmenu.change', itemLabel = mi.GetLabel())
 190 
 191 class App(wx.App):
 192     def OnInit(self):
 193         frame = MyFrame(None, title="Notebook Application")
 194         frame.CenterOnScreen()
 195         self.SetTopWindow(frame)
 196         frame.Show()
 197         return True
 198 
 199 a = App(False)
 200 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.

   1 import time
   2 import wx
   3 
   4 try:
   5     from pubsub import pub
   6 except ImportError:
   7     from wx.lib.pubsub import pub
   8 
   9 class ListeningGauge(wx.Gauge):
  10     def __init__(self, *args, **kwargs):
  11         wx.Gauge.__init__(self, *args, **kwargs)
  12         pub.subscribe(self.start_listening, "progress_awake")
  13         pub.subscribe(self.stop_listening, "progress_sleep")
  14 
  15     def _update(self, this, total):
  16         try:
  17             self.SetRange(total)
  18             self.SetValue(this)
  19         except Exception as e:
  20             print e
  21 
  22     def start_listening(self, listen_to):
  23         rect = self.Parent.GetFieldRect(1)
  24         self.SetPosition((rect.x+2, rect.y+2))
  25         self.SetSize((rect.width-4, rect.height-4))
  26         self.Show()
  27         pub.subscribe(self._update, listen_to)
  28 
  29     def stop_listening(self, listen_to):
  30         pub.unsubscribe(self._update, listen_to)
  31         self.Hide()
  32 
  33 
  34 class MainWindow(wx.Frame):
  35     def __init__(self, parent, title):
  36         wx.Frame.__init__(self, parent, title=title)
  37 
  38         status = self.statusbar = self.CreateStatusBar() # A StatusBar in the bottom of the window
  39         status.SetFieldsCount(3)
  40         status.SetStatusWidths([-2,200,-1])
  41 
  42         self.progress_bar = ListeningGauge(self.statusbar, style=wx.GA_HORIZONTAL|wx.GA_SMOOTH)
  43         rect = self.statusbar.GetFieldRect(1)
  44         self.progress_bar.SetPosition((rect.x+2, rect.y+2))
  45         self.progress_bar.SetSize((rect.width-4, rect.height-4))
  46         self.progress_bar.Hide()
  47 
  48         panel = wx.Panel(self)
  49         shortButton = wx.Button(panel, label="Run for 3 seconds")
  50         longButton = wx.Button(panel, label="Run for 6 seconds")
  51 
  52         sizer = wx.BoxSizer(wx.VERTICAL)
  53         sizer.Add(shortButton, 0, wx.ALL, 10)
  54         sizer.Add(longButton, 0, wx.ALL, 10)
  55 
  56         panel.SetSizerAndFit(sizer)
  57 
  58         shortButton.Bind(wx.EVT_BUTTON, self.Run3)
  59         longButton.Bind(wx.EVT_BUTTON, self.Run6)
  60 
  61     def Run3(self, event):
  62         pub.sendMessage('progress_awake', listen_to = 'short_update')
  63         wx.BeginBusyCursor()
  64         for x in range(6):
  65             pub.sendMessage('short_update', this = x+1, total = 6)
  66             time.sleep(0.5)
  67         wx.EndBusyCursor()
  68         pub.sendMessage('progress_sleep', listen_to = 'short_update')
  69 
  70     def Run6(self, event):
  71         pub.sendMessage('progress_awake', listen_to = 'long_update')
  72         wx.BeginBusyCursor()
  73         for x in range(12):
  74             pub.sendMessage('long_update', this = x+1, total = 12)
  75             time.sleep(0.5)
  76         wx.EndBusyCursor()
  77         pub.sendMessage('progress_sleep', listen_to = 'long_update')
  78 
  79 
  80 app = wx.App(False)
  81 frame = MainWindow(None, "Listening Gauge Demo")
  82 frame.Show()
  83 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 pubsub home page.

Controlling GUI with pubsub (last edited 2013-11-13 07:25:05 by 70)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.