But the event handler isn't supposed to be multi-threaded... is it?! If that were the case then I would have to use Critical Sections when accessing member objects or global objects from within my event handler functions.
I've put together a minimalistic sample program that exhibits what I'm on about.
OK my program contains two threads:
1) The Main Thread (which displays a Dialog Box)
2) The Secondary Thread which is Joinable
The Secondary Joinable Thread is Create'd and Run'd from within the constructor of the Main Dialog Box.
The Secondary Joinable Thread doesn't actually do anything. It just runs in an eternal loop until the Main Thread invokes the Wait member function on the thread, at which point the Secondary Thread will stall for 5 seconds before exiting.
On the Main Dialog Box, there is a button that says "Sleep for 5 seconds". When this button is clicked, it invokes "Wait" on the secondary thread. The "Wait" member function will take at least 5 seconds to return, so you would expect the Main Dialog Box to freeze up for 5 seconds and not react to any input (I mean how can any other event handlers be called if the BUTTON_CLICKED event handler hasn't returned yet?)
My program works exactly as intended on Ubuntu using wxGTK (i.e. the GUI freezes for 5 seconds). However on MS-Windows using wxMSW, the Main Dialog Box doesn't freeze when the Wait function is called. Not only that, other event handlers are invoked before the BUTTON_CLICKED event handler returns! That makes me think that the Event Handler is multi-threaded and that it can invoke multiple event handlers simultaneously.
Run my sample program. A dialog box will appear. Do the following:
1) Click on the second tab on the Notebook. A message box will appear telling you that it's about to change tab (I call the wxMessageBox function from within the PAGE_CHANGING event handler). Click OK on the message box, and notice that the tab changes normally. Navigate back to the first tab, and again you'll see the same message box appear.
2) Now, click the "Sleep for 5 seconds" button, and then straight away try to navigate to the second tab on the Notebook. You would expect the dialog box to be frozen because the BUTTON_CLICKED event handler hasn't returned yet, however it lets you navigate to the second tab... and not only that, it even triggers the PAGE_CHANGING event which displays the message box! (But how the hell can the PAGE_CHANGING event be processed if the button's BUTTON_CLICKED event handler hasn't yet returned?????). The Notebook will immediately change its active tab... however it won't display the new panel until the BUTTON_CLICKED event handler returns.
I'm confused by all this. I've never thought that the Event Handler could invoke more than one event handler function at the same time. If this were the case, then we'd need to used Critical Sections all over the place in our GUI code.
Here's my sample program:
Code: Select all
/* ========== BEGIN: All the header files needed (auto-generated by wxFormBuilder) ========== */
#include "wx/wxprec.h"
#ifdef WX_PRECOMP
#include "wx_pch.h"
#endif
#include <wx/app.h>
#include <wx/thread.h>
#include <wx/msgdlg.h>
#include <wx/string.h>
#include <wx/button.h>
#include <wx/gdicmn.h>
#include <wx/font.h>
#include <wx/colour.h>
#include <wx/settings.h>
#include <wx/stattext.h>
#include <wx/sizer.h>
#include <wx/panel.h>
#include <wx/bitmap.h>
#include <wx/image.h>
#include <wx/icon.h>
#include <wx/notebook.h>
#include <wx/dialog.h>
/* ========== END: All the header files needed (auto-generated by wxFormBuilder) ========== */
/* ========== BEGIN: The global objects used by more than one thread ========== */
wxCriticalSection g_cs;
bool g_is_main_thread_waiting = false; /* Must use Critical Section when accessing this */
/* ========== END: The global objects used by more than one thread ========== */
class Class_For_Secondary_Joinable_Thread : public wxThread {
public:
Class_For_Secondary_Joinable_Thread() : wxThread(wxTHREAD_JOINABLE) {}
void *Entry()
{
for (;;)
{
/* Jut keep looping eternally until
the main thread changes the
value of g_is_main_thread_waiting */
g_cs.Enter();
bool const is_waiting = g_is_main_thread_waiting;
g_is_main_thread_waiting = false;
g_cs.Leave();
if (is_waiting)
{
/* When main thread calls Wait,
wait 5 seconds and then end
the thread */
this->Sleep(5000);
return 0;
}
}
}
};
class Main_Dialog : public wxDialog
{
protected:
wxThread *p_secondary_joinable_thread; /* Keep track of the secondary thread */
wxButton* m_button1;
wxNotebook* m_notebook1;
wxPanel* m_panel1;
wxStaticText* m_staticText2;
wxPanel* m_panel2;
wxStaticText* m_staticText3;
void OnClose( wxCloseEvent& event );
void OnButtonClick_Sleep( wxCommandEvent& event );
void OnTabChanging( wxNotebookEvent& event );
public:
Main_Dialog( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Blah blah blah"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 296,254 ), long style = wxDEFAULT_DIALOG_STYLE );
~Main_Dialog()
{
/* We can't just exit our program instantaneously,
we must wait for the secondary thread to end */
if (p_secondary_joinable_thread)
{
g_cs.Enter();
g_is_main_thread_waiting = true;
g_cs.Leave();
p_secondary_joinable_thread->Wait(); /* This function takes 5 seconds to return! */
}
}
};
class Class_For_My_App : public wxApp
{
public:
virtual bool OnInit()
{
Main_Dialog *dlg = new Main_Dialog(0L);
if (!dlg)
return false;
dlg->Show();
return true;
}
};
IMPLEMENT_APP(Class_For_My_App);
Main_Dialog::Main_Dialog( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style )
{
/* ========== BEGIN: The stuff auto-generated by wxFormBuilder ========== */
this->SetSizeHints( wxDefaultSize, wxDefaultSize );
wxBoxSizer* bSizer1;
bSizer1 = new wxBoxSizer( wxVERTICAL );
m_button1 = new wxButton( this, wxID_ANY, wxT("Sleep for 5 seconds"), wxDefaultPosition, wxDefaultSize, 0 );
bSizer1->Add( m_button1, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 );
m_notebook1 = new wxNotebook( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 );
m_panel1 = new wxPanel( m_notebook1, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
wxBoxSizer* bSizer3;
bSizer3 = new wxBoxSizer( wxVERTICAL );
m_staticText2 = new wxStaticText( m_panel1, wxID_ANY, wxT("\n\nThis is the wxPanel for Tab 1\n\nONE ONE ONE ONE ONE"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticText2->Wrap( -1 );
bSizer3->Add( m_staticText2, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 );
m_panel1->SetSizer( bSizer3 );
m_panel1->Layout();
bSizer3->Fit( m_panel1 );
m_notebook1->AddPage( m_panel1, wxT("Tab 1"), true );
m_panel2 = new wxPanel( m_notebook1, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
wxBoxSizer* bSizer4;
bSizer4 = new wxBoxSizer( wxVERTICAL );
m_staticText3 = new wxStaticText( m_panel2, wxID_ANY, wxT("\n\nThis is the wxPanel for Tab 2\n\nTWO TWO TWO TWO TWO"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticText3->Wrap( -1 );
bSizer4->Add( m_staticText3, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 );
m_panel2->SetSizer( bSizer4 );
m_panel2->Layout();
bSizer4->Fit( m_panel2 );
m_notebook1->AddPage( m_panel2, wxT("Tab 2"), false );
bSizer1->Add( m_notebook1, 1, wxEXPAND | wxALL, 5 );
this->SetSizer( bSizer1 );
this->Layout();
// Connect Events
this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( Main_Dialog::OnClose ) );
m_button1->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( Main_Dialog::OnButtonClick_Sleep ), NULL, this );
m_notebook1->Connect( wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGING, wxNotebookEventHandler( Main_Dialog::OnTabChanging ), NULL, this );
/* ========== END: The stuff auto-generated by wxFormBuilder ========== */
p_secondary_joinable_thread = new Class_For_Secondary_Joinable_Thread;
if ( !p_secondary_joinable_thread
|| (p_secondary_joinable_thread->Create() != wxTHREAD_NO_ERROR)
|| (p_secondary_joinable_thread->Run() != wxTHREAD_NO_ERROR)
)
{
delete p_secondary_joinable_thread; /* OK to call delete on a null pointer */
p_secondary_joinable_thread = 0;
this->Destroy();
}
}
void Main_Dialog::OnTabChanging( wxNotebookEvent& event )
{
wxMessageBox(wxT("About to change tab"));
}
void Main_Dialog::OnClose( wxCloseEvent& event )
{
this->Destroy();
}
void Main_Dialog::OnButtonClick_Sleep( wxCommandEvent& event )
{
g_cs.Enter();
g_is_main_thread_waiting = true;
g_cs.Leave();
p_secondary_joinable_thread->Wait(); /* This function takes at least 5 seconds to return! */
/* OK, now re-create the secondary thread */
delete p_secondary_joinable_thread;
p_secondary_joinable_thread = new Class_For_Secondary_Joinable_Thread;
if ( !p_secondary_joinable_thread
|| (p_secondary_joinable_thread->Create() != wxTHREAD_NO_ERROR)
|| (p_secondary_joinable_thread->Run() != wxTHREAD_NO_ERROR)
)
{
delete p_secondary_joinable_thread; /* OK to call delete on a null pointer */
p_secondary_joinable_thread = 0;
this->Destroy();
}
}
Now here's something else to try. In my code, look for the "Main_Dialog::OnButtonClick_Sleep" function, and replace it with this:
Code: Select all
void Main_Dialog::OnButtonClick_Sleep( wxCommandEvent& event )
{
::wxSleep(5);
}
After having made this change, re-run the program. This time around, when you click the "Sleep for 5 seconds" button, the GUI actually freezes us as you would expect it to. The GUI doesn't quite ignore all input though, instead it appears to add events to some sort of queue, and all of these events are processed once the BUTTON_CLICKED event handler has returned (this happens with wxGTK too).
So I've got two questions about wxMSW:
1) Why does the Event Handler system appear to be multi-threaded?
2) Why should the call to "::wxSleep(5)" be any different than the call to "p_secondary_thread->Wait()"? I would expect both calls to make the GUI freeze.
Can anyone shed some light on this?