如何动态重新创建带有可变数量项目的wxMenu(子菜单)? [英] How can I dynamically re-create a wxMenu (sub menu) with a variable number of items?

查看:251
本文介绍了如何动态重新创建带有可变数量项目的wxMenu(子菜单)?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



我的计划:


  1. 使用每个检测到的端口的数据创建一个对象列表,最多32个对象指针。示例: comDetected * COMsFound [MAX_COM_DETECT]; (working)

  2. 使用 AppendRadioItem()创建一个新菜单 (工作)

  3. 使用 EVT_MENU()为每个COM端口选择运行相同的功能

如何确定事件处理函数(从 wxCommandEvent?)哪个菜单选项引起的事件?没有这些信息,我将需要32个单独的函数。

是否有更动态的方式来创建对象和事件,以避免创建32的任意限制?



Edit - 这是我现在用于菜单重新创建,似乎是工作:
重新编辑 - 不是那么好,如bogdan解释



void FiltgenFrame :: OnMenuOpen(wxMenuEvent& event)
{
//打开COM端口菜单
if event.GetMenu()== COMSubMenu)
{
int i;
wxString comhelp;

//重新扫描端口
comport-> getPorts();

if(comport-> COMd​​etectChanged == 1)
{
comport-> currentCOMselection = 0; //当菜单被重新生成时,选择返回0
//去掉旧的菜单项
for(i = 0; i {
COMSubMenu-> Delete(FILTGEN_COM1 + i);
COMSubMenu-> Unbind(wxEVT_MENU,[i](wxCommandEvent&)
{
logMsg(DBG_MENUS,ACT_NORMAL,menu COM select index:%d\\\
,i)
},FILTGEN_COM1 + i);
}

//添加新菜单项
for(i = 0; i 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, index:%d \\\
,i);
},FILTGEN_COM1 + i);
}
}
}
}



- 重新工作代码1-29-15。由于与此问题无关的因素,分开 OnMenuOpen recreateCOMmenu

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

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

int i;
wxString comhelp;

//重新扫描端口
comport-> getPorts();

if(comport-> COMd​​etectChanged == 1)
{
comport-> currentCOMselection = 0; //当菜单被重新生成时,选择返回0
//去掉旧的菜单项
for(i = 0; i {
COMSubMenu-> Delete(FILTGEN_COM1 + i);
COMSubMenu-> Unbind(wxEVT_MENU,& FiltgenFrame :: COMselectionHandler,this,FILTGEN_COM1 + i);
}

//添加新菜单项
for(i = 0; i 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)
{
//填写COM端口菜单打开
if(event.GetMenu()== COMSubMenu)
{
recreateCOMmenu();
}
}


解决方案

动态似乎是这里的关键词,我会去处理动态事件(实际上,我总是使用 Bind 去处理动态事件比这些更好):

  auto pm = new wxMenu //我想你要添加到现有的菜单。 
std :: wstring port_str = LCOM;
int id_base = 77; //但是您要设置菜单项的ID。
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&)
{
//使用当前port_num执行操作;例如:
wxMessageBox(std :: to_wstring (port_num));
//你也可以捕获id,如果你喜欢,当然是
},id);
}

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






对象的数量,你可以简单地将它们存储在 std :: vector 。如果你想让向量拥有对象(当向量被破坏时它们会自动被破坏),那么你可以直接将它们存储在 std :: vector< comDetected> 。如果别的东西拥有对象并且分别对它们进行破坏,那么你可以使用 std :: vector< comDetected *>






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



这里的问题:就我所知,没有直接的方法, code> Unbind 一个lambda函数对象,它直接传递给 Bind ,就像我在示例中所做的那样。只需在更新的代码中调用解除绑定即可,因为解除绑定会尝试找到一个事件处理程序,该事件处理程序安装了相应的 Bind 完全相同的参数。这不会发生在下一部分解释的原因(也有一个解释为什么似乎工作),但你可能会对解决方案更感兴趣,所以我会从那些开始。



解决方案1 ​​(在您的案例中最好的):Forgo using lambdas;只需使用自由函数或成员函数指针。在这种情况下,您需要从 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 \\\
,i);
}

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

  void FiltgenFrame :: OnMenuOpen(wxMenuEvent& event)
{

/ * ... * /

COMSubMenu->取消绑定(wxEVT_MENU,handler_func,FILTGEN_COM1 + i);

/ * ... * /

COMSubMenu-> Bind(wxEVT_MENU,handler_func,FILTGEN_COM1 + i);

/ * ... * /

}

上面的例子使用了一个自由函数。您还可以使用成员函数 - 更多信息此处



解决方案2 :如果您可以在 EVT_MENU_OPEN(),你可以去破坏整个 wxMenu 并重建并将其插入到其父菜单中的正确位置。破坏旧的菜单对象将处理绑定到它的所有动态事件处理程序,因此您不需要解除绑定。但是,在显示之前破坏菜单听起来不是一个好主意 - 我没有尝试过,但只要我能告诉它不工作,或者以高度平台相关的方式行为。






以下是解除绑定无法直接与lambdas :


  1. 由lambda表达式生成的对象具有唯一类型。即使你将完全相同的lambda表达式复制粘贴到代码中的另一个地方,那么第二个lambda会生成一个类型不同于原始lambda生成的类型的闭包对象。因为 Unbind 会根据已安装的处理程序类型检查函数参数的类型,所以它永远不会找到匹配项。

  2. 我们得到了上面的问题,还有另一个:传递给解绑定的函数对象也需要有一个传递给的地址对应绑定。由lambda表达式生成的对象是一个临时的(通常会在堆栈上分配),因此对函数调用的地址进行任何假设都是不正确的。

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






似乎在代码中工作的原因如下:If Unbind 找不到要删除的事件处理程序,它只返回 false ;所有现有的处理程序仍然在那里。稍后, Bind 为事件处理程序列表的前面添加了相同事件类型和相同条目ID的新处理程序,因此较新的处理程序称为第一。除非在返回之前处理程序调用 evt.Skip(),该事件在处理程序返回后被处理,不会调用其他处理程序。



即使它的工作正如你所期望的,让所有那些旧的未使用的处理程序在列表中每次重建菜单时累积是不是一个好主意,显然。


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

解决方案

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 destructed automatically when the vector is destructed), then you can store them directly in a std::vector<comDetected>. If something else owns the objects and takes care of destructing them separately, then you can 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 with 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 by a lambda expression 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.


The reason it seems to work in your code is the following: 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.

这篇关于如何动态重新创建带有可变数量项目的wxMenu(子菜单)?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆