[Effective WX] wxGTK上popup wxMenu的一個crash問題分析及解決方案

在GUI應用編程中,我們通常會提供給用戶一些右鍵菜單選項。加入有這樣的編程案例:

右鍵菜單是依託某個窗口,如果某個右鍵菜單項提供這樣的功能:當用戶選擇它之後,GUI代碼做了一些事情後,需要跳轉到其它窗口,在跳轉之前或之後,我們不得不銷燬之前右鍵菜單依託的窗口類對象。


1. 問題描述:

在wxGTK版本的程序中,當跳轉到另一個窗口之後,GUI程序會crash。位置爲$wxsrc/gtk/menu.cpp:145行。


你可以猜到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行代碼了沒?不嫌麻煩地將代碼貼在下面:

如果在代碼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你的代碼流程。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章