如何动态重新创建具有可变数量的项的 wxMenu(子菜单)

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

本文关键字:wxMenu 菜单 动态 何动态 新创建 创建      更新时间:2023-10-16

我想在每次查看子菜单时都会更新的子菜单中创建一个COM端口列表。

我的计划:

  1. 创建一个对象列表,其中包含有关每个检测到的端口的数据,最多 32 个对象指针。示例:comDetected *COMsFound[MAX_COM_DETECT];(工作)
  2. Delete()旧菜单项(工作)
  3. 使用AppendRadioItem() EVT_MENU_OPEN()创建新菜单(工作)
  4. 使用EVT_MENU()为每个COM端口选择运行相同的函数

如何在事件处理功能(从wxCommandEvent?)中确定哪个菜单选项导致了事件? 如果没有这些信息,我将需要 32 个单独的函数。
有没有一种更动态的方式来创建对象和事件,以避免我创建的 32 个任意限制?

编辑 - 这是我现在用于重新创建菜单的功能,它似乎正在工作:重新编辑 - 不太好,正如波格丹所解释的那样

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: %dn", 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: %dn", i); 
                }, FILTGEN_COM1 + i);
            }
        }
    }   
}

编辑 - 重新设计的代码 1-29-15。 由于与此问题无关的因素,OnMenuOpenrecreateCOMmenu分手。因为建议而增加了COMselectionHandler

void FiltgenFrame::COMselectionHandler(wxCommandEvent& event)
{   
    comport->currentCOMselection = event.GetId() - FILTGEN_COM1;
    logMsg(DBG_MENUS, ACT_NORMAL, "COM menu select index: %dn", 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();      
    }
}

由于动态似乎是这里的关键词,我会选择动态事件处理(实际上,我总是使用 Bind 进行动态事件处理,它比替代方案好得多):

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);
}

在 lambda 表达式中,我们按值捕获端口号,因此,对于每次迭代,将捕获当前port_num。这完全实现了您的要求:与每个菜单项关联的相同函数(lambda 闭包类型的运算符()。该函数知道调用它的条目,因为它可以访问捕获的port_num值,该值存储在 lambda 的闭包对象中 - 一个小对象,在这种情况下很可能是一个int的大小。


为避免对对象数量的固定限制,您可以简单地将它们存储在 std::vector 中。如果您希望矢量拥有对象(在销毁矢量时自动销毁它们),则可以将它们直接存储在std::vector<comDetected>中。如果其他人拥有这些对象并负责单独销毁它们,则可以使用 std::vector<comDetected*> .


更新:在编写我的第一个解决方案时,我没有意识到您会想要Unbind并重新绑定这些事件处理程序;事后看来很明显,真的,但是......无论如何,我的错误,对不起。

问题是:据我所知,没有直接的方法可以像我在示例中所做的那样Unbind直接传递给Bind的 lambda 函数对象。简单地像在更新的代码中那样调用Unbind是行不通的,因为该Unbind将尝试查找由具有完全相同参数的相应调用Bind安装的事件处理程序。由于下一节中解释的原因,这不会发生(也有解释为什么它似乎有效),但您可能对解决方案更感兴趣,所以我将从这些开始。

解决方案

1(在您的情况下最好的解决方案):放弃使用 lambda;只需使用自由函数或成员函数指针。在这种情况下,您需要从evt.GetId()获取菜单项 ID,并从中获取端口索引;像这样:

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

然后,您的代码将如下所示:

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

上面的例子是使用自由函数。您还可以使用成员函数 - 更多信息在这里。

解决方案2:如果您可以在EVT_MENU_OPEN()以外的其他时间重建该菜单,则可以销毁整个wxMenu并将其重建并将其插入到正确的位置的父菜单中。销毁旧菜单对象将处理绑定到它的所有动态事件处理程序,因此您无需Unbind它们。但是,在显示菜单之前销毁菜单听起来不是一个好主意 - 我没有尝试过,但据我所知,它不起作用,或者以高度依赖平台的方式运行。


以下是Unbind不能直接使用 lambda 的原因:

  1. 由 lambda 表达式生成的对象具有唯一的类型。即使您将完全相同的 lambda 表达式复制粘贴到代码中的其他位置,第二个 lambda 也会生成一个闭包对象,其类型与原始 lambda 生成的对象不同。由于Unbind根据已安装处理程序的类型检查函子参数的类型,因此它永远不会找到匹配项。
  2. 即使我们解决了上面的问题,还有另一个问题:传递给Unbind的函数对象也需要与传递给相应Bind的函数对象具有相同的地址。将 lambda 表达式直接传递给 Bind 时生成的对象是临时对象(通常会在堆栈上分配),因此在函数调用中对其地址做出任何假设都是不正确的。

我们可以绕过上面的两个问题(将闭包对象分别存储在某个地方等等),但我认为任何这样的解决方案都太麻烦了,不值得考虑 - 它将否定基于 lambda 的解决方案的所有优点。


这就是为什么它似乎在你的代码中工作:

如果Unbind没有找到要删除的事件处理程序,它只返回false;所有现有的处理程序都保留在那里。稍后,Bind在事件处理程序列表的前面为相同的事件类型和相同的条目 ID 添加新的处理程序,因此首先调用较新的处理程序。除非处理程序在返回之前调用evt.Skip(),否则该事件被视为在处理程序返回后处理,并且不会调用其他处理程序。

尽管它按您的预期工作,但每次重建菜单时让所有这些旧的未使用的处理程序累积在列表中显然不是一个好主意。