Search code examples
c++wxwidgets

How can I dynamically re-create a wxMenu (sub menu) with a variable number of items?


I want to create a list of COM ports in a sub menu that is updated every time the sub menu is viewed.

My plan:

  1. Create a list of objects with data about each detected port, up to 32 object pointers. Example: comDetected *COMsFound[MAX_COM_DETECT]; (working)
  2. Delete() old menu entries (working)
  3. Create a new menu upon EVT_MENU_OPEN() with AppendRadioItem() (working)
  4. Use EVT_MENU() to run the same function for each COM port selection

How do I determine in the event handling function (from wxCommandEvent?) which menu option caused the event? Without this information, I will need 32 separate functions.
Is there a more dynamic way to create the objects and events to avoid the arbitrary limit of 32 I have created?

Edit - This is what I have now for menu re-creation, which seems to be working: Re-edit - not so good, as explained by bogdan

void FiltgenFrame::OnMenuOpen(wxMenuEvent& event)
{   
    //fill in COM port menu when opened
    if(event.GetMenu() == COMSubMenu)
    {
        int i;
        wxString comhelp;

        //re-scan ports
        comport->getPorts();

        if(comport->COMdetectChanged == 1)
        {
            comport->currentCOMselection = 0; //when menu is regenerated, selection returns to 0
            //get rid of old menu entries
            for(i = 0; i < comport->oldnumCOMsFound; i++)
            {
                COMSubMenu->Delete(FILTGEN_COM1 + i);               
                COMSubMenu->Unbind(wxEVT_MENU, [i](wxCommandEvent&) 
                {   
                    logMsg(DBG_MENUS, ACT_NORMAL, "menu COM select index: %d\n", i); 
                }, FILTGEN_COM1 + i);
            }

            //add new menu entries
            for(i = 0; i < comport->numCOMsFound; i++)
            {
                comhelp.Printf("Use %s", comport->COMsFound[i]->name);              
                COMSubMenu->AppendRadioItem(FILTGEN_COM1 + i, comport->COMsFound[i]->name, comhelp);
                COMSubMenu->Bind(wxEVT_MENU, [i](wxCommandEvent&) 
                {   
                    comport->currentCOMselection = i;
                    logMsg(DBG_MENUS, ACT_NORMAL, "menu COM select index: %d\n", i); 
                }, FILTGEN_COM1 + i);
            }
        }
    }   
}

Edit - re-worked code 1-29-15. Broke apart OnMenuOpen and recreateCOMmenu due to factors unrelated to this question. Added COMselectionHandler because of advice.

void FiltgenFrame::COMselectionHandler(wxCommandEvent& event)
{   
    comport->currentCOMselection = event.GetId() - FILTGEN_COM1;
    logMsg(DBG_MENUS, ACT_NORMAL, "COM menu select index: %d\n", comport->currentCOMselection);
}

void FiltgenFrame::recreateCOMmenu()
{
    logMsg(DBG_MENUS, ACT_NORMAL, "FiltgenFrame::recreateCOMmenu():\n");

    int i;
    wxString comhelp;

    //re-scan ports
    comport->getPorts();

    if(comport->COMdetectChanged == 1)
    {
        comport->currentCOMselection = 0; //when menu is regenerated, selection returns to 0
        //get rid of old menu entries
        for(i = 0; i < comport->oldnumCOMsFound; i++)
        {
            COMSubMenu->Delete(FILTGEN_COM1 + i);                       
            COMSubMenu->Unbind(wxEVT_MENU, &FiltgenFrame::COMselectionHandler, this, FILTGEN_COM1 + i);
        }

        //add new menu entries
        for(i = 0; i < comport->numCOMsFound; i++)
        {
            comhelp.Printf("Use %s", comport->COMsFound[i]->name);
            COMSubMenu->AppendRadioItem(FILTGEN_COM1 + i, comport->COMsFound[i]->name, comhelp);
            COMSubMenu->Bind(wxEVT_MENU, &FiltgenFrame::COMselectionHandler, this, FILTGEN_COM1 + i);           
        }
    }
}

void FiltgenFrame::OnMenuOpen(wxMenuEvent& event)
{
    //fill in COM port menu when opened
    if(event.GetMenu() == COMSubMenu)
    {
        recreateCOMmenu();      
    }
}

Solution

  • Since dynamic seems to be the key word here, I would go for dynamic event handling (actually, I always go for dynamic event handling using Bind, it's so much nicer than the alternatives):

    auto pm = new wxMenu(); //I suppose you're adding this to an existing menu.
    std::wstring port_str = L"COM";
    int id_base = 77; //However you want to set up the IDs of the menu entries.
    for(int port_num = 1; port_num <= 32; ++port_num)
    {
       int id = id_base + port_num;
       pm->AppendRadioItem(id, port_str + std::to_wstring(port_num));
       pm->Bind(wxEVT_MENU, [port_num](wxCommandEvent&)
       {
          //Do something with the current port_num; for example:
          wxMessageBox(std::to_wstring(port_num));
          //You can also capture id if you prefer, of course.
       }, id);
    }
    

    In the lambda expression, we capture the port number by value, so, for each iteration, the current port_num will be captured. This achieves exactly what you asked for: the same function (the operator() of the lambda's closure type) associated with each menu entry. The function knows the entry for which it was called because it has access to the captured port_num value, stored in the lambda's closure object - a small object, most likely the size of one int in this case.


    To avoid a fixed limit on the number of objects, you can simply store them in a std::vector. If you want the vector to own the objects (have them destroyed automatically when the vector is destroyed), then you can store them directly in a std::vector<comDetected>. If something else owns the objects and takes care of destroying them separately, you could use std::vector<comDetected*>.


    UPDATE: When writing my first solution, I didn't realize you'll want to Unbind and re-bind those event handlers; pretty obvious in hindsight, really, but... anyway, my mistake, sorry.

    Here's the problem: as far as I can tell, there's no straightforward way to Unbind a lambda function object that was passed directly to Bind as I did in my example. Simply calling Unbind as you're doing in your updated code isn't going to work, because that Unbind will try to find an event handler that was installed by a corresponding call to Bind with the exact same arguments. That won't happen for the reasons explained in the next section (there's also an explanation for why it seems to work), but you might be more interested in solutions, so I'll start with those.

    Solution 1 (the best one in your case): Forgo using lambdas; just use either a free function or a member function pointer. In this case, you'll need to get the menu entry ID from evt.GetId() and get the port index from it; something like this:

    void handler_func(wxCommandEvent& evt) 
    {   
        int i = evt.GetId() - FILTGEN_COM1;
        comport->currentCOMselection = i;
        logMsg(DBG_MENUS, ACT_NORMAL, "menu COM select index: %d\n", i); 
    }
    

    Then, your code would look like this:

    void FiltgenFrame::OnMenuOpen(wxMenuEvent& event)
    {
    
        /* ... */
    
        COMSubMenu->Unbind(wxEVT_MENU, handler_func, FILTGEN_COM1 + i);
    
        /* ... */
    
        COMSubMenu->Bind(wxEVT_MENU, handler_func, FILTGEN_COM1 + i);
    
        /* ... */
    
    }
    

    The example above is using a free function. You can also use a member function - more info here.

    Solution 2: In case you can rebuild that menu at some other time than EVT_MENU_OPEN(), you could go for destroying the whole wxMenu and rebuilding and inserting it into its parent menu in the right place. Destroying the old menu object will take care of all the dynamic event handlers bound to it, so you don't need to Unbind them. However, destroying the menu just before it's displayed doesn't sound like a good idea - I haven't tried it, but as far as I can tell it won't work, or behave in highly platform-dependent ways.


    Here are the reasons for which Unbind won't work directly with lambdas:

    1. The object generated by a lambda expression has a unique type. Even if you copy-paste the exact same lambda expression to another place in your code, that second lambda will generate a closure object of a type different from the one generated by the original lambda. Since Unbind checks the type of the functor argument against the types of the installed handlers, it will never find a match.
    2. Even if we got around the problem above, there's another one: the function object passed to Unbind also needs to have the same address as the one passed to the corresponding Bind. The object generated when the lambda expression is passed directly to Bind is a temporary (it will typically be allocated on the stack), so making any assumptions about its address across function calls is just incorrect.

    We can get around the two problems above (store the closure objects separately somewhere and so on), but I think any such solution is far too cumbersome to be worth considering - it will negate all the advantages of the lambda-based solution.


    Here's why it seems to work in your code:

    If Unbind doesn't find an event handler to remove, it just returns false; all existing handlers remain in there. Later on, Bind adds a new handler for the same event type and same entry ID at the front of the event handler list, so newer handlers are called first. Unless a handler calls evt.Skip() before returning, the event is considered handled after the handler returns and no other handlers are called.

    Even though it sort of works as you expect, letting all those old unused handlers accumulate in the list every time the menu is rebuilt isn't a good idea, obviously.