Differences between revisions 8 and 9
Revision 8 as of 2010-06-29 02:37:50
Size: 11815
Editor: bas8-ottawa23-1177550949
Comment:
Revision 9 as of 2012-05-30 20:02:29
Size: 14715
Editor: c-71-236-231-109
Comment: Added Listening Gauge Example
Deletions are marked like this. Additions are marked like this.
Line 277: Line 277:
== 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.

{{{
import time

import wx


from 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, topicName):

        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, topicName)

    def stop_listening(self, topicName):
        pub.unsubscribe(self._update, topicName)
        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', topicName = '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', topicName = 'short_update')



    def Run6(self, event):
        pub.sendMessage('progress_awake', topicName = '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', topicName = 'long_update')


app = wx.App(False)
frame = MainWindow(None, "Listening Gauge Demo")
frame.Show()
app.MainLoop()
}}}

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.

Notes:

  1. This page was originally created by JoshEnglish. Josh wrote it in the first person ("I") which made the text awkward to edit, so it was changed to second-person.

  2. See the WxLibPubSub page for general info on pubsub and it fits in wxPython.

  3. The pubsub API described on the this page is the API that was in use until (and including) wxPython 2.8.11.0. It is called the pubsub "version 1" API. It is deprecated as it has been replaced by a more expressive and powerful API, but as of 2.8.11.0 it is still the default pubsub API used in wxPython. This may change in the future and will be noted on WxLibPubSub.

  4. Therefore, if you are starting a new wxPython application with wxPython version 2.8.11.0 or later, you should avoid the default API by inserting a "from wx.lib.pubsub import setupkwargs" just before the first pubsub import. A couple other changes will be required in the example below, more details will be given in a other wiki page to be created shortly.

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...

  • 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.tabmenu.menubar'

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

Toggle line numbers
   1 """GUIPubSub.py
   2 Use pubsub to control the GUI.
   3 """
   4 
   5 import wx
   6 from wx.lib.pubsub import Publisher as 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', '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', "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', name)
  87         pub.sendMessage('menubar.change', name)
  88         evt.Skip()
  89 
  90     def OnChange(self, msg):
  91         name = self.ChangeSelection(msg.data-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.OnStatusMessage, 'statusbar')
 132         pub.subscribe(self.OnTabMenuMessage, 'tabmenu')
 133         pub.subscribe(self.OnMenuBarMessage, 'menubar')
 134 
 135         ### SEND INITIAL MESSAGES
 136         idx = self.Book.GetSelection()
 137         name = self.Book.GetPageText(idx)
 138         pub.sendMessage('tabmenu.change', name)
 139         pub.sendMessage('menubar.change', name)
 140 
 141     def OnAbout(self, evt):
 142         pub.sendMessage('statusbar.update', 'Bragging about this app')
 143         wx.MessageBox("This is my program. Muy Neato, huh?",
 144                       "About this App",
 145                       wx.OK)
 146 
 147     def OnClose(self, evt):
 148         self.Close(True)
 149 
 150     def OnStatusMessage(self, msg):
 151         ### Must compare topics as tuples
 152         if msg.topic == ('statusbar', 'update'):
 153             self.SetStatusText(msg.data,0)
 154         elif msg.topic == ('statusbar', 'clear'):
 155             self.SetStatusText('',0)
 156 
 157     def OnTabMenuMessage(self, msg):
 158         ### Respond to a message to change the tabmenu
 159         if msg.topic == ('tabmenu', 'change'):
 160             ### Disable all tab menu items (except the close item)
 161             for idx in range(self.Book.GetPageCount()):
 162                 mi = self.tabMenu.FindItemById(idx+1)
 163                 if mi.GetLabel() == msg.data:
 164                     mi.Enable(False) # Don't want to be able to switch to the current page
 165                 else:
 166                     mi.Enable(True)
 167                 ### Another way to do this:
 168                 #mi.Enable(mi.GetLabel() != msg.data)
 169                 ### The problem with this is there are several logical steps taken in one line
 170 
 171     def OnMenuBarMessage(self, msg):
 172         if msg.topic == ('menubar', 'change'):
 173             ### Disable all page menus but the current one
 174             mbar = self.GetMenuBar()
 175             for idx in range(self.Book.GetPageCount()):
 176                 page = self.Book.GetPage(idx) ## returns the page
 177                 menu = mbar.FindMenu(page.GetName())  ## returns an integer
 178 
 179                 if page.GetName() == msg.data:
 180                     mbar.EnableTop(menu, True)
 181                 else:
 182                     mbar.EnableTop(menu, False)
 183 
 184     def OnTabMenu(self, evt):
 185         ### Get the label of the menu item
 186         mbar=self.GetMenuBar()
 187         mi = mbar.FindItemById(evt.GetId())
 188         ### Send the messages
 189         pub.sendMessage('notebook.select', evt.GetId())
 190         pub.sendMessage('menubar.change', mi.GetLabel())
 191         pub.sendMessage('tabmenu.change', mi.GetLabel())
 192 
 193 class App(wx.App):
 194     def OnInit(self):
 195         frame = MyFrame(None, title="Notebook Application")
 196         frame.CenterOnScreen()
 197         self.SetTopWindow(frame)
 198         frame.Show()
 199         return True
 200 
 201 a = App(False)
 202 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.

import time

import wx


from 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, topicName):

        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, topicName)

    def stop_listening(self, topicName):
        pub.unsubscribe(self._update, topicName)
        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', topicName = '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', topicName = 'short_update')



    def Run6(self, event):
        pub.sendMessage('progress_awake', topicName = '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', topicName = '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 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.