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