我如何动态地重新创建一个具有可变数量项目的 wxMenu(子菜单)?
How can I dynamically re-create a wxMenu (sub menu) with a variable number of items?
我想在每次查看子菜单时更新的子菜单中创建 COM 端口列表。
我的计划:
- 创建一个对象列表,其中包含有关每个检测到的端口的数据,最多 32 个对象指针。示例:
comDetected *COMsFound[MAX_COM_DETECT];
(工作)
Delete()
旧菜单条目(有效)
- 在
EVT_MENU_OPEN()
上用 AppendRadioItem()
创建一个新菜单(有效)
- 使用
EVT_MENU()
到运行每个COM端口选择相同的功能
如何在事件处理函数(来自 wxCommandEvent?
)中确定哪个菜单选项导致了事件?如果没有这些信息,我将需要 32 个单独的函数。
是否有更动态的方式来创建对象和事件以避免我创建的 32 的任意限制?
编辑 - 这是我现在用于重新创建菜单的内容,似乎有效:
重新编辑 - 不太好,正如 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);
}
}
}
}
编辑 - 重新处理代码 1-29-15。由于与此问题无关的因素,OnMenuOpen
和 recreateCOMmenu
分开了。添加 COMselectionHandler
因为建议。
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();
}
}
因为动态似乎是这里的关键词,我会选择动态事件处理(实际上,我总是使用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 闭包类型的 operator())。该函数知道调用它的条目,因为它可以访问捕获的 port_num
值,该值存储在 lambda 的闭包对象中 - 一个小对象,在本例中很可能是一个 int
的大小。
为避免对象数量的固定限制,您可以简单地将它们存储在 std::vector
中。如果你想让vector拥有对象(当vector被销毁时让它们自动销毁),那么你可以将它们直接存储在std::vector<comDetected>
中。如果其他人拥有这些对象并负责单独销毁它们,您可以使用 std::vector<comDetected*>
.
更新:在编写我的第一个解决方案时,我没有意识到您需要 Unbind
并重新绑定那些事件处理程序;事后看来很明显,真的,但是......无论如何,我的错误,抱歉。
这是问题所在:据我所知,没有像我在示例中那样直接将直接传递给 Bind
的 lambda 函数对象 Unbind
的直接方法。像您在更新的代码中所做的那样简单地调用 Unbind
是行不通的,因为 Unbind
将尝试查找通过对 Bind
的相应调用安装的事件处理程序与 exact 相同的参数。由于下一节中解释的原因,这不会发生(还有一个解释为什么它似乎有效),但你可能对解决方案更感兴趣,所以我将从这些开始。
解决方案 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: %d\n", i);
}
那么,您的代码将如下所示:
void FiltgenFrame::OnMenuOpen(wxMenuEvent& event)
{
/* ... */
COMSubMenu->Unbind(wxEVT_MENU, handler_func, FILTGEN_COM1 + i);
/* ... */
COMSubMenu->Bind(wxEVT_MENU, handler_func, FILTGEN_COM1 + i);
/* ... */
}
上面的例子使用了一个自由函数。您还可以使用成员函数 - 更多信息 here。
解决方案 2:如果您可以在 EVT_MENU_OPEN()
以外的其他时间重建该菜单,您可以销毁整个 wxMenu
并重建并将其插入到其父菜单的正确位置。销毁旧的菜单对象将处理绑定到它的所有动态事件处理程序,因此您不需要 Unbind
它们。但是,在菜单显示之前销毁菜单听起来不是个好主意 - 我没有尝试过,但据我所知它不会工作,或者以高度依赖平台的方式运行。
以下是 Unbind
不能直接使用 lambda 的原因:
- lambda 表达式生成的对象具有 唯一 类型。即使您将完全相同的 lambda 表达式复制粘贴到代码中的另一个位置,第二个 lambda 也会生成一个与原始 lambda 生成的类型不同的闭包对象。由于
Unbind
根据安装的处理程序的类型检查仿函数参数的类型,它永远不会找到匹配项。
- 即使我们绕过了上面的问题,还有另一个问题:传递给
Unbind
的函数对象也需要与传递给的函数对象具有 相同的地址对应的Bind
。将 lambda 表达式直接传递给 Bind
时生成的对象是一个临时对象(它通常会在堆栈上分配),因此对其跨函数调用的地址做出任何假设都是不正确的。
我们可以解决上面的两个问题(将闭包对象单独存储在某个地方等等),但我认为任何这样的解决方案都太麻烦了,不值得考虑——它会抵消 lambda 的所有优势——基于解决方案。
这就是它似乎在您的代码中起作用的原因:
如果 Unbind
没有找到要删除的事件处理程序,它只是 returns false
;所有现有的处理程序都保留在那里。稍后,Bind
在事件处理程序列表的前面为相同的事件类型和相同的条目 ID 添加了一个新的处理程序,因此首先调用较新的处理程序。除非处理程序在返回前调用 evt.Skip()
,否则事件被视为在处理程序 returns 之后处理,并且不会调用其他处理程序。
即使它有点像您预期的那样工作,但让所有那些旧的未使用的处理程序在每次重建菜单时都堆积在列表中显然不是一个好主意。
我想在每次查看子菜单时更新的子菜单中创建 COM 端口列表。
我的计划:
- 创建一个对象列表,其中包含有关每个检测到的端口的数据,最多 32 个对象指针。示例:
comDetected *COMsFound[MAX_COM_DETECT];
(工作) Delete()
旧菜单条目(有效)- 在
EVT_MENU_OPEN()
上用AppendRadioItem()
创建一个新菜单(有效) - 使用
EVT_MENU()
到运行每个COM端口选择相同的功能
如何在事件处理函数(来自 wxCommandEvent?
)中确定哪个菜单选项导致了事件?如果没有这些信息,我将需要 32 个单独的函数。
是否有更动态的方式来创建对象和事件以避免我创建的 32 的任意限制?
编辑 - 这是我现在用于重新创建菜单的内容,似乎有效: 重新编辑 - 不太好,正如 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);
}
}
}
}
编辑 - 重新处理代码 1-29-15。由于与此问题无关的因素,OnMenuOpen
和 recreateCOMmenu
分开了。添加 COMselectionHandler
因为建议。
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();
}
}
因为动态似乎是这里的关键词,我会选择动态事件处理(实际上,我总是使用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 闭包类型的 operator())。该函数知道调用它的条目,因为它可以访问捕获的 port_num
值,该值存储在 lambda 的闭包对象中 - 一个小对象,在本例中很可能是一个 int
的大小。
为避免对象数量的固定限制,您可以简单地将它们存储在 std::vector
中。如果你想让vector拥有对象(当vector被销毁时让它们自动销毁),那么你可以将它们直接存储在std::vector<comDetected>
中。如果其他人拥有这些对象并负责单独销毁它们,您可以使用 std::vector<comDetected*>
.
更新:在编写我的第一个解决方案时,我没有意识到您需要 Unbind
并重新绑定那些事件处理程序;事后看来很明显,真的,但是......无论如何,我的错误,抱歉。
这是问题所在:据我所知,没有像我在示例中那样直接将直接传递给 Bind
的 lambda 函数对象 Unbind
的直接方法。像您在更新的代码中所做的那样简单地调用 Unbind
是行不通的,因为 Unbind
将尝试查找通过对 Bind
的相应调用安装的事件处理程序与 exact 相同的参数。由于下一节中解释的原因,这不会发生(还有一个解释为什么它似乎有效),但你可能对解决方案更感兴趣,所以我将从这些开始。
解决方案 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: %d\n", i);
}
那么,您的代码将如下所示:
void FiltgenFrame::OnMenuOpen(wxMenuEvent& event)
{
/* ... */
COMSubMenu->Unbind(wxEVT_MENU, handler_func, FILTGEN_COM1 + i);
/* ... */
COMSubMenu->Bind(wxEVT_MENU, handler_func, FILTGEN_COM1 + i);
/* ... */
}
上面的例子使用了一个自由函数。您还可以使用成员函数 - 更多信息 here。
解决方案 2:如果您可以在 EVT_MENU_OPEN()
以外的其他时间重建该菜单,您可以销毁整个 wxMenu
并重建并将其插入到其父菜单的正确位置。销毁旧的菜单对象将处理绑定到它的所有动态事件处理程序,因此您不需要 Unbind
它们。但是,在菜单显示之前销毁菜单听起来不是个好主意 - 我没有尝试过,但据我所知它不会工作,或者以高度依赖平台的方式运行。
以下是 Unbind
不能直接使用 lambda 的原因:
- lambda 表达式生成的对象具有 唯一 类型。即使您将完全相同的 lambda 表达式复制粘贴到代码中的另一个位置,第二个 lambda 也会生成一个与原始 lambda 生成的类型不同的闭包对象。由于
Unbind
根据安装的处理程序的类型检查仿函数参数的类型,它永远不会找到匹配项。 - 即使我们绕过了上面的问题,还有另一个问题:传递给
Unbind
的函数对象也需要与传递给的函数对象具有 相同的地址对应的Bind
。将 lambda 表达式直接传递给Bind
时生成的对象是一个临时对象(它通常会在堆栈上分配),因此对其跨函数调用的地址做出任何假设都是不正确的。
我们可以解决上面的两个问题(将闭包对象单独存储在某个地方等等),但我认为任何这样的解决方案都太麻烦了,不值得考虑——它会抵消 lambda 的所有优势——基于解决方案。
这就是它似乎在您的代码中起作用的原因:
如果 Unbind
没有找到要删除的事件处理程序,它只是 returns false
;所有现有的处理程序都保留在那里。稍后,Bind
在事件处理程序列表的前面为相同的事件类型和相同的条目 ID 添加了一个新的处理程序,因此首先调用较新的处理程序。除非处理程序在返回前调用 evt.Skip()
,否则事件被视为在处理程序 returns 之后处理,并且不会调用其他处理程序。
即使它有点像您预期的那样工作,但让所有那些旧的未使用的处理程序在每次重建菜单时都堆积在列表中显然不是一个好主意。