wxpopupmenu with varying sets of menu items

I've gotten confused at how to use the wxpopupmenu
when there are few possible sets of menu items that could
pop up. I want to:

1. set up a popup menu made from one of a few lists of
titles, with that list used depending on conditions in the app.
(because I will have a few different possible menus that
could pop up)

3. Bind the popup menu to an event handler which can be passed
the ID of the item selected in the popup.

4. From that ID, get the title of the menu item, and then do
things based on that title.

This seems like it should be easy, but I keep hitting a wall.
I'm trying to make a hybrid of the code in the wxPython demo
and an older one on the wiki found here:
http://wiki.wxpython.org/PopupMenuOnRightClick

...but because it is using unordered dictionaries I have a
problem: the menu the items are in a random order (which
I don't want). Below is a runnable sample that shows this.
Click on either of the colored panels multiple times and you'll
get inconsistent orderings of the titles.

I'd use just a list to build the popup menu, but then I would
have no mapping between the title and its ID for use in the
event handler. I need the event handler to know which of
the titles was the one clicked on. So right now I feel stuck
with either a consistently ordered popup menu that has a
useless event handler, or one that does but whose titles
are randomly ordered. What am I doing wrong?

Thank you, and code below...

···

#-----------------------------------------------------------------------------
#Boa:Frame:Frame1

import wx

def create(parent):
    return Frame1(parent)

[wxID_FRAME1, wxID_FRAME1PANEL1, wxID_FRAME1PANEL2, wxID_FRAME1PANEL3,
] = [wx.NewId() for _init_ctrls in range(4)]

class Frame1(wx.Frame):
    def _init_ctrls(self, prnt):
        # generated method, don't edit
        wx.Frame.__init__(self, id=wxID_FRAME1, name='', parent=prnt,
              pos=wx.Point(177, 196), size=wx.Size(437, 245),
              style=wx.DEFAULT_FRAME_STYLE, title='Frame1')
        self.SetClientSize(wx.Size(429, 211))

        self.panel1 = wx.Panel(id=wxID_FRAME1PANEL1, name='panel1', parent=self,
              pos=wx.Point(0, 0), size=wx.Size(429, 211),
              style=wx.TAB_TRAVERSAL)

        self.panel2 = wx.Panel(id=wxID_FRAME1PANEL2, name='panel2',
              parent=self.panel1, pos=wx.Point(24, 24), size=wx.Size(184, 160),
              style=wx.TAB_TRAVERSAL)
        self.panel2.SetBackgroundColour(wx.Colour(0, 128, 255))
        self.panel2.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel2RightDown)

        self.panel3 = wx.Panel(id=wxID_FRAME1PANEL3, name='panel3',
              parent=self.panel1, pos=wx.Point(216, 24), size=wx.Size(192, 160),
              style=wx.TAB_TRAVERSAL)
        self.panel3.SetBackgroundColour(wx.Colour(0, 128, 128))
        self.panel3.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel3RightDown)

    def __init__(self, parent):
        self._init_ctrls(parent)

    def OnPanel2RightDown(self, event):
        self.menu_titles = ["one", "two", "three"]
        self.mypopupmenu()

    def OnPanel3RightDown(self, event):
        self.menu_titles = ["four", "five", "six"]
        self.mypopupmenu()

    def mypopupmenu(self):

        menu = wx.Menu()

        #A dict to access menu titles by ID--will be used in event handler
        self.menu_title_by_id = {}
        for title in self.menu_titles:
            self.menu_title_by_id[ wx.NewId() ] = title

        #make the menu
        for (popID,title) in self.menu_title_by_id.items():
            item = wx.MenuItem(menu, popID, title)
            menu.AppendItem(item)

        #bind to a single event handler
            self.Bind(wx.EVT_MENU, self.OnPopupMenu, item)

        self.PopupMenu( menu)

    def OnPopupMenu(self,event):
        title = self.menu_title_by_id[ event.GetId() ]
        print title

if __name__ == '__main__':
    app = wx.PySimpleApp()
    frame = create(None)
    frame.Show()

    app.MainLoop()

Hi,

I guess that to get the menu sorted, you’d want it sorted on menu-item name, not on ID. I have something similar in my code. There are some dependancies on other methods; below is not a standalone example, but I hope it will serve to show how I solved it.

Please note the important points:

  1. I’m sorting on the original list of items, not the ID list
  2. I’m using partial functions to effectively give each menu it’s own private instance of the event-handler, while still having only 1 implementation of it.

Cheers,

–Tim

def map_control(self, pos, edit):
    # Event Handler
    def on_select_edit_name(edit_name, event):
        print 'Selected "%s", event:' % edit_name, dir(event)

    # Create menu
    menu = wx.Menu()
    from functools import partial
    for edit_name in sorted(self.get_all_names_for_edit(edit)):
        id = wx.NewId()
        menu.Append(id, edit_name)
        self.Bind(wx.EVT_MENU, partial(on_select_edit_name, edit_name), id=id)
    self.PopupMenu(menu, pos)
    menu.Destroy()
    return

Thanks, Mike for the effort. But I can't get it to work fully. Does
your code have the event handler working? Mine with your changes and
my original event handler doesn't. Also, I will need in my app for my
order of the menu items to have set orderings...I just gave "one",
"two", "three" as a toy example. So in reality it might be something
like: "Stop", "Edit" in one menu but "Restart", "Edit" in another...
Will this code work with that?

Tim, I have no understanding of what a "partial function" is in your
code. Could you elaborate a bit?

···

On Fri, Apr 25, 2008 at 10:22 AM, Mike Driscoll <mdriscoll@co.marshall.ia.us> wrote:

C M wrote:

> I've gotten confused at how to use the wxpopupmenu
> when there are few possible sets of menu items that could
> pop up. I want to:
>
> 1. set up a popup menu made from one of a few lists of
> titles, with that list used depending on conditions in the app.
> (because I will have a few different possible menus that
> could pop up)
>
> 3. Bind the popup menu to an event handler which can be passed
> the ID of the item selected in the popup.
>
> 4. From that ID, get the title of the menu item, and then do
> things based on that title.
>
> This seems like it should be easy, but I keep hitting a wall.
> I'm trying to make a hybrid of the code in the wxPython demo
> and an older one on the wiki found here:
> PopupMenuOnRightClick - wxPyWiki
>
> ...but because it is using unordered dictionaries I have a
> problem: the menu the items are in a random order (which
> I don't want). Below is a runnable sample that shows this.
> Click on either of the colored panels multiple times and you'll
> get inconsistent orderings of the titles.
>
> I'd use just a list to build the popup menu, but then I would
> have no mapping between the title and its ID for use in the
> event handler. I need the event handler to know which of
> the titles was the one clicked on. So right now I feel stuck
> with either a consistently ordered popup menu that has a
> useless event handler, or one that does but whose titles
> are randomly ordered. What am I doing wrong?
>
> Thank you, and code below...
>
>
#-----------------------------------------------------------------------------
> #Boa:Frame:Frame1
>
> import wx
>
> def create(parent):
> return Frame1(parent)
>
> [wxID_FRAME1, wxID_FRAME1PANEL1, wxID_FRAME1PANEL2, wxID_FRAME1PANEL3,
> ] = [wx.NewId() for _init_ctrls in range(4)]
>
> class Frame1(wx.Frame):
> def _init_ctrls(self, prnt):
> # generated method, don't edit
> wx.Frame.__init__(self, id=wxID_FRAME1, name='', parent=prnt,
> pos=wx.Point(177, 196), size=wx.Size(437, 245),
> style=wx.DEFAULT_FRAME_STYLE, title='Frame1')
> self.SetClientSize(wx.Size(429, 211))
>
> self.panel1 = wx.Panel(id=wxID_FRAME1PANEL1, name='panel1',
parent=self,
> pos=wx.Point(0, 0), size=wx.Size(429, 211),
> style=wx.TAB_TRAVERSAL)
>
> self.panel2 = wx.Panel(id=wxID_FRAME1PANEL2, name='panel2',
> parent=self.panel1, pos=wx.Point(24, 24), size=wx.Size(184,
160),
> style=wx.TAB_TRAVERSAL)
> self.panel2.SetBackgroundColour(wx.Colour(0, 128, 255))
> self.panel2.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel2RightDown)
>
> self.panel3 = wx.Panel(id=wxID_FRAME1PANEL3, name='panel3',
> parent=self.panel1, pos=wx.Point(216, 24), size=wx.Size(192,
160),
> style=wx.TAB_TRAVERSAL)
> self.panel3.SetBackgroundColour(wx.Colour(0, 128, 128))
> self.panel3.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel3RightDown)
>
> def __init__(self, parent):
> self._init_ctrls(parent)
>
> def OnPanel2RightDown(self, event):
> self.menu_titles = ["one", "two", "three"]
> self.mypopupmenu()
>
> def OnPanel3RightDown(self, event):
> self.menu_titles = ["four", "five", "six"]
> self.mypopupmenu()
>
> def mypopupmenu(self):
>
> menu = wx.Menu()
>
> #A dict to access menu titles by ID--will be used in event handler
> self.menu_title_by_id = {}
> for title in self.menu_titles:
> self.menu_title_by_id[ wx.NewId() ] = title
>
> #make the menu
> for (popID,title) in self.menu_title_by_id.items():
> item = wx.MenuItem(menu, popID, title)
> menu.AppendItem(item)
>
> #bind to a single event handler
> self.Bind(wx.EVT_MENU, self.OnPopupMenu, item)
>
> self.PopupMenu( menu)
>
> def OnPopupMenu(self,event):
> title = self.menu_title_by_id[ event.GetId() ]
> print title
>
> if __name__ == '__main__':
> app = wx.PySimpleApp()
> frame = create(None)
> frame.Show()
>
> app.MainLoop()
>
>
>
C M,

It took me a bit, but I found one solution. It's probably not the most
elegant, but it's working on my Windows box. Just change the your methods to
look like the following:

<code>

def OnPanel2RightDown(self, event):
   self.menu_titles = {1:"one", 2:"two", 3:"three"}

   self.mypopupmenu()

def OnPanel3RightDown(self, event):
   self.menu_titles = {4:"four", 5:"five", 6:"six"}

   self.mypopupmenu()

def mypopupmenu(self):

   menu = wx.Menu()

   #A dict to access menu titles by ID--will be used in event handler
   self.menu_title_by_id =
   keys = self.menu_titles.keys()
   keys.sort()
   for key in keys:
       self.menu_title_by_id.append((wx.NewId(), key))

   #make the menu
   for popID, key in self.menu_title_by_id:
       title = self.menu_titles[key]

       item = wx.MenuItem(menu, popID, title)
       menu.AppendItem(item)

   #bind to a single event handler
       self.Bind(wx.EVT_MENU, self.OnPopupMenu, item)

   self.PopupMenu( menu)

</code>

Basically, I'm creating a list of tuples that contains a random id and a
key that corresponds to a menu item title. You'll notice that I sort my keys
to force them to be in the correct order, which only works if you make the
dictionaries in ascending order to begin with. Then I loop through the
sorted keys creating the list of tuples. Finally, I loop through the list
and extract the popID and the key and translate the key into the correct
title.

Maybe someone else will come up with something cool too. I'm guessing this
is a place where a generator or decorator would work too, but I haven't
gotten those figured out to my satisfaction.

Mike

C M,

<snip>

Thanks, Mike for the effort. But I can't get it to work fully. Does
your code have the event handler working? Mine with your changes and
my original event handler doesn't. Also, I will need in my app for my
order of the menu items to have set orderings...I just gave "one",
"two", "three" as a toy example. So in reality it might be something
like: "Stop", "Edit" in one menu but "Restart", "Edit" in another...
Will this code work with that?

Tim, I have no understanding of what a "partial function" is in your
code. Could you elaborate a bit?

The idea is that you would decide on the order you want and then number them. So, if I wanted to have "Stop", and "Edit" (in that order), I would do this:

self.menu_titles = {1:"Stop", 2:"Edit"}

and in the second menu, I could do this:

self.menu_titles = {1:"Restart", 2:"Edit"}

Here's the entire code with my changes:

<code>

import wx

def create(parent):
    return Frame1(parent)

[wxID_FRAME1, wxID_FRAME1PANEL1, wxID_FRAME1PANEL2, wxID_FRAME1PANEL3,
] = [wx.NewId() for _init_ctrls in range(4)]

class Frame1(wx.Frame):
    def _init_ctrls(self, prnt):
        # generated method, don't edit
        wx.Frame.__init__(self, id=wxID_FRAME1, name='', parent=prnt,
              pos=wx.Point(177, 196), size=wx.Size(437, 245),
              style=wx.DEFAULT_FRAME_STYLE, title='Frame1')
        self.SetClientSize(wx.Size(429, 211))

        self.panel1 = wx.Panel(id=wxID_FRAME1PANEL1, name='panel1', parent=self,
              pos=wx.Point(0, 0), size=wx.Size(429, 211),
              style=wx.TAB_TRAVERSAL)

        self.panel2 = wx.Panel(id=wxID_FRAME1PANEL2, name='panel2',
              parent=self.panel1, pos=wx.Point(24, 24), size=wx.Size(184, 160),
              style=wx.TAB_TRAVERSAL)
        self.panel2.SetBackgroundColour(wx.Colour(0, 128, 255))
        self.panel2.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel2RightDown)

        self.panel3 = wx.Panel(id=wxID_FRAME1PANEL3, name='panel3',
              parent=self.panel1, pos=wx.Point(216, 24), size=wx.Size(192, 160),
              style=wx.TAB_TRAVERSAL)
        self.panel3.SetBackgroundColour(wx.Colour(0, 128, 128))
        self.panel3.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel3RightDown)

    def __init__(self, parent):
        self._init_ctrls(parent)

def OnPanel2RightDown(self, event):
    self.menu_titles = {1:"one", 2:"two", 3:"three"}
    self.mypopupmenu()

def OnPanel3RightDown(self, event):
    self.menu_titles = {4:"four", 5:"five", 6:"six"}
    self.mypopupmenu()

def mypopupmenu(self):

    menu = wx.Menu()

    #A dict to access menu titles by ID--will be used in event handler
    self.menu_title_by_id =
    keys = self.menu_titles.keys()
    keys.sort()
    for key in keys:
        self.menu_title_by_id.append((wx.NewId(), key))

    #make the menu
    for popID, key in self.menu_title_by_id:
        title = self.menu_titles[key]
        item = wx.MenuItem(menu, popID, title)
        menu.AppendItem(item)

    #bind to a single event handler
        self.Bind(wx.EVT_MENU, self.OnPopupMenu, item)

    self.PopupMenu( menu)

    def OnPopupMenu(self,event):
        title = self.menu_title_by_id[ event.GetId() ]
        print title

if __name__ == '__main__':
    app = wx.PySimpleApp()
    frame = create(None)
    frame.Show()

    app.MainLoop()

</code>

Mike

Mike, for this code, when I click on the popupmenu item it does not
print or give me an error. The event handler isn't doing anything.
Am I doing something wrong?

···

On Fri, Apr 25, 2008 at 4:30 PM, Mike Driscoll <mdriscoll@co.marshall.ia.us> wrote:

C M,

<snip>

> Thanks, Mike for the effort. But I can't get it to work fully. Does
> your code have the event handler working? Mine with your changes and
> my original event handler doesn't. Also, I will need in my app for my
> order of the menu items to have set orderings...I just gave "one",
> "two", "three" as a toy example. So in reality it might be something
> like: "Stop", "Edit" in one menu but "Restart", "Edit" in another...
> Will this code work with that?
>
> Tim, I have no understanding of what a "partial function" is in your
> code. Could you elaborate a bit?
>
>
>
The idea is that you would decide on the order you want and then number
them. So, if I wanted to have "Stop", and "Edit" (in that order), I would do
this:

self.menu_titles = {1:"Stop", 2:"Edit"}

and in the second menu, I could do this:

self.menu_titles = {1:"Restart", 2:"Edit"}

Here's the entire code with my changes:

<code>

import wx

def create(parent):
   return Frame1(parent)

[wxID_FRAME1, wxID_FRAME1PANEL1, wxID_FRAME1PANEL2, wxID_FRAME1PANEL3,
] = [wx.NewId() for _init_ctrls in range(4)]

class Frame1(wx.Frame):
   def _init_ctrls(self, prnt):
       # generated method, don't edit
       wx.Frame.__init__(self, id=wxID_FRAME1, name='', parent=prnt,
             pos=wx.Point(177, 196), size=wx.Size(437, 245),
             style=wx.DEFAULT_FRAME_STYLE, title='Frame1')
       self.SetClientSize(wx.Size(429, 211))

       self.panel1 = wx.Panel(id=wxID_FRAME1PANEL1, name='panel1',
parent=self,
             pos=wx.Point(0, 0), size=wx.Size(429, 211),
             style=wx.TAB_TRAVERSAL)

       self.panel2 = wx.Panel(id=wxID_FRAME1PANEL2, name='panel2',
             parent=self.panel1, pos=wx.Point(24, 24), size=wx.Size(184,
160),
             style=wx.TAB_TRAVERSAL)
       self.panel2.SetBackgroundColour(wx.Colour(0, 128, 255))
       self.panel2.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel2RightDown)

       self.panel3 = wx.Panel(id=wxID_FRAME1PANEL3, name='panel3',
             parent=self.panel1, pos=wx.Point(216, 24), size=wx.Size(192,
160),
             style=wx.TAB_TRAVERSAL)
       self.panel3.SetBackgroundColour(wx.Colour(0, 128, 128))
       self.panel3.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel3RightDown)

   def __init__(self, parent):
       self._init_ctrls(parent)

def OnPanel2RightDown(self, event):
   self.menu_titles = {1:"one", 2:"two", 3:"three"}
   self.mypopupmenu()

def OnPanel3RightDown(self, event):
   self.menu_titles = {4:"four", 5:"five", 6:"six"}
   self.mypopupmenu()

def mypopupmenu(self):

   menu = wx.Menu()

   #A dict to access menu titles by ID--will be used in event handler
   self.menu_title_by_id =
   keys = self.menu_titles.keys()
   keys.sort()
   for key in keys:
       self.menu_title_by_id.append((wx.NewId(), key))

   #make the menu
   for popID, key in self.menu_title_by_id:
       title = self.menu_titles[key]
       item = wx.MenuItem(menu, popID, title)
       menu.AppendItem(item)

   #bind to a single event handler
       self.Bind(wx.EVT_MENU, self.OnPopupMenu, item)

   self.PopupMenu( menu)

   def OnPopupMenu(self,event):
       title = self.menu_title_by_id[ event.GetId() ]
       print title

if __name__ == '__main__':
   app = wx.PySimpleApp()
   frame = create(None)
   frame.Show()

   app.MainLoop()

</code>

Mike

CM,

<snip>

Mike, for this code, when I click on the popupmenu item it does not
print or give me an error. The event handler isn't doing anything.
Am I doing something wrong?

Looks like the indentation got mangled somehow. I've straightened it out and attached it as a file, but I'll also post it below. I recommend using the attached Python script though.

<code>

import wx

def create(parent):
    return Frame1(parent)

[wxID_FRAME1, wxID_FRAME1PANEL1, wxID_FRAME1PANEL2, wxID_FRAME1PANEL3,
] = [wx.NewId() for _init_ctrls in range(4)]

class Frame1(wx.Frame):
    def _init_ctrls(self, prnt):
        # generated method, don't edit
        wx.Frame.__init__(self, id=wxID_FRAME1, name='', parent=prnt,
              pos=wx.Point(177, 196), size=wx.Size(437, 245),
              style=wx.DEFAULT_FRAME_STYLE, title='Frame1')
        self.SetClientSize(wx.Size(429, 211))

        self.panel1 = wx.Panel(id=wxID_FRAME1PANEL1, name='panel1', parent=self,
              pos=wx.Point(0, 0), size=wx.Size(429, 211),
              style=wx.TAB_TRAVERSAL)

        self.panel2 = wx.Panel(id=wxID_FRAME1PANEL2, name='panel2',
              parent=self.panel1, pos=wx.Point(24, 24), size=wx.Size(184, 160),
              style=wx.TAB_TRAVERSAL)
        self.panel2.SetBackgroundColour(wx.Colour(0, 128, 255))
        self.panel2.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel2RightDown)

        self.panel3 = wx.Panel(id=wxID_FRAME1PANEL3, name='panel3',
              parent=self.panel1, pos=wx.Point(216, 24), size=wx.Size(192, 160),
              style=wx.TAB_TRAVERSAL)
        self.panel3.SetBackgroundColour(wx.Colour(0, 128, 128))
        self.panel3.Bind(wx.EVT_RIGHT_DOWN, self.OnPanel3RightDown)

    def __init__(self, parent):
        self._init_ctrls(parent)

    def OnPanel2RightDown(self, event):
        self.menu_titles = {1:"one", 2:"two", 3:"three"}
        self.mypopupmenu()

    def OnPanel3RightDown(self, event):
        self.menu_titles = {4:"four", 5:"five", 6:"six"}
        self.mypopupmenu()

    def mypopupmenu(self):

        menu = wx.Menu()

        #A dict to access menu titles by ID--will be used in event handler
        self.menu_title_by_id =
        keys = self.menu_titles.keys()
        keys.sort()
        for key in keys:
            self.menu_title_by_id.append((wx.NewId(), key))

        #make the menu
        for popID, key in self.menu_title_by_id:
            title = self.menu_titles[key]
            item = wx.MenuItem(menu, popID, title)
            menu.AppendItem(item)

        #bind to a single event handler
        self.Bind(wx.EVT_MENU, self.OnPopupMenu, item)

        self.PopupMenu( menu)

    def OnPopupMenu(self,event):
        title = self.menu_title_by_id[ event.GetId() ]
        print title

if __name__ == '__main__':
    app = wx.PySimpleApp()
    frame = create(None)
    frame.Show()

    app.MainLoop()

</code>

Sorry about the mix-up.

Mike

menu_items.py (2.45 KB)

John Femiani wrote:

Is there a way to layout controls in wxpython so that they flow like
text would. For esample, they keep stacking until the edge of the window
is reached, and then a new colomn (or row) is started and the controls
continue to stack in that row. I am thinking of the way windows
explorer lays out icons, except I ant to ba able to do this with any
control, like a Java FlowLayout.

There was a patch being discussed a while back that adds a sizer that works this way. I'm not sure if it's been added to wx yet, but if so it won't show up until 2.9.

···

--
Robin Dunn
Software Craftsman
http://wxPython.org Java give you jitters? Relax with wxPython!

Stani wrote:

···

On Sat, Apr 26, 2008 at 2:28 AM, Robin Dunn <robin@alldunn.com> wrote:

John Femiani wrote:

Is there a way to layout controls in wxpython so that they flow like
text would. For esample, they keep stacking until the edge of the window
is reached, and then a new colomn (or row) is started and the controls
continue to stack in that row. I am thinking of the way windows
explorer lays out icons, except I ant to ba able to do this with any
control, like a Java FlowLayout.

There was a patch being discussed a while back that adds a sizer that works
this way. I'm not sure if it's been added to wx yet, but if so it won't
show up until 2.9.

Would it not be possible to achieve this with a wxHtml control?

Yes that would probably work, although it would be a bit cumbersome.

--
Robin Dunn
Software Craftsman
http://wxPython.org Java give you jitters? Relax with wxPython!