[Effective WX] wxGTK上popup wxMenu的一个crash问题分析及解决方案

在GUI应用编程中,我们通常会提供给用户一些右键菜单选项。假如有这样的编程案例:
右键菜单依托于某个窗口,如果某个右键菜单项提供这样的功能:当用户选择它之后,GUI代码做了一些事情后,需要跳转到其它窗口,在跳转之前或之后,我们不得不销毁之前右键菜单依托的窗口类对象。
1. 问题描述:
在wxGTK版本的程序中,当跳转到另一个窗口之后,GUI程序会crash。位置为$wxsrc/gtk/menu.cpp:145行。
[Effective WX] wxGTK上popup wxMenu的一个crash问题分析及解决方案_第1张图片
你可以猜到win这时已经是野指针了,对的。menu的Invoking Window就是那个已经销毁的依托的窗口。
2. 原因分析:
popup的wxMenu的代码通常会这样写:

wxMenu menu;
menu.Append(...);
...
invokingwin->PopupMenu(&menu);

当调用PopupMenu时,函数将会进入一个事件循环。

1660 bool wxWindowGTK::DoPopupMenu( wxMenu *menu, int x, int y )
1661 {
1662     wxCHECK_MSG( m_widget != NULL, false, wxT("invalid window") );
1663 
1664     wxCHECK_MSG( menu != NULL, false, wxT("invalid popup-menu") );
1665 
1666     // NOTE: if you change this code, you need to update
1667     //       the same code in taskbar.cpp as well. This
1668     //       is ugly code duplication, I know.
1669 
1670     SetInvokingWindow( menu, this );
1671 
1672     menu->UpdateUI();
1673 
1674     bool is_waiting = true;
1675 
1676     gulong handler = g_signal_connect (menu->m_menu, "hide",
1677                                        G_CALLBACK    (gtk_pop_hide_callback),
1678                                        &is_waiting);
1679 
1680     wxPoint pos;
1681     gpointer userdata;
1682     GtkMenuPositionFunc posfunc;
1683     if ( x == -1 && y == -1 )
1684     {
1685         // use GTK's default positioning algorithm
1686         userdata = NULL;
1687         posfunc = NULL;
1688     }
1689     else
1690     {
1691         pos = ClientToScreen(wxPoint(x, y));
1692         userdata = &pos;
1693         posfunc = wxPopupMenuPositionCallback;
1694     }
1695 
1696     wxMenuEvent eventOpen(wxEVT_MENU_OPEN, -1, menu);
1697     DoCommonMenuCallbackCode(menu, eventOpen);
1698 
1699     gtk_menu_popup(
1700                   GTK_MENU(menu->m_menu),
1701                   (GtkWidget *) NULL,           // parent menu    shell
1702                   (GtkWidget *) NULL,           // parent menu item
1703                   posfunc,                      // function to    position it
1704                   userdata,                     // client data
1705                   0,                            // button used to  activate it
1706                   gtk_get_current_event_time()
1707                 );
1708 
1709     while (is_waiting)
1710     {
1711         gtk_main_iteration();
1712     }
1713 
1714     g_signal_handler_disconnect (menu->m_menu, handler);
1715 
1716     wxMenuEvent eventClose(wxEVT_MENU_CLOSE, -1, menu);
1717     DoCommonMenuCallbackCode(menu, eventClose);
1718 
1719     return true;
1720 }

1) 注意到1670行,set menu's invoking window
2) 1709-1712行,就类似于一个事件循环。
3) 当退出事件循环后,才会发送wxEVT_MENU_CLOSE事件。
4) 什么时候退出事件循环?可能在用户选择了一个菜单项,执行了菜单项的事件响应函数,或者用户点击了其它地方,没选择任何菜单项。
所以在执行一个菜单项的事件响应函数过程中,如果它的invoking window销毁了,menu中记录的invoking window指针就会变成野指针。事件循环退出后,调用1717行的函数时,就会发生crash。

3. 一种可能的解决方案:
不知你注意到$wxsrc/gtk/menu.cpp:139-141行代码了没?不嫌麻烦地将代码贴在下面:
[Effective WX] wxGTK上popup wxMenu的一个crash问题分析及解决方案_第2张图片
如果在代码crash之前,我们让执行流从141行代码退出,程序就不会crash。我想这是可以做到的。
注意到140代码是一个事件处理,这是提供给客户端代码处理那个event的一个机会(通常plugin编程可以利用这样的方式)。想到如果我们能够让menu->GetEventHandler()返回的event handler能够process这个event,而且返回true(通常代表已经处理这个事件),这个问题就可以比较顺利的解决了。
因此,直接提供下面的event handler,push到menu的event handler chain的链表头:

class PopupMenuCloseEvtHandler : public wxEvtHandler
{
public:
    explicit PopupMenuCloseEvtHandler(wxMenu* pMenu)
     : m_pMenu(pMenu)
    {
        if (m_pMenu)
        {
            m_pMenu->SetEventHandler(this);
            Connect(wxEVT_MENU_CLOSE,
                wxMenuEventHandler(PopupMenuCloseEvtHandler::HandlePopupMenuClose),
                NULL, this);
         }
     }

     ~PopupMenuCloseEvtHandler()
     {
        if (m_pMenu)
        {
           m_pMenu->SetEventHandler(NULL);
           Disconnect(wxEVT_MENU_CLOSE,
                wxMenuEventHandler(PopupMenuCloseEvtHandler::HandlePopupMenuClose),
                NULL, this);
           }
     }
    protected:
        void HandlePopupMenuClose(wxMenuEvent& evt)
        {
            evt.Skip(false); // means handled
        }
    private:
        wxMenu* m_pMenu;
};

在PopupMenu调用之前,添加下面类似的代码:

wxMenu menu;
menu.Append(...);
...
#ifdef _WXGTK_
PopupMenuCloseEvtHandler menuEvtHandler(&menu);
#endif
invokingwin->PopupMenu(&menu);

之所以说上面的方案是一个可能的方案,是因为:你的invoking window在正常流程下,如果需要处理弹出菜单的wxEVT_MENU_CLOSE事件的话,上面方案会break你的代码流程。

你可能感兴趣的:(wxwidgets)