[Solved]wxWidgets multithreaded and update UI. Topic is solved

If you are using the main C++ distribution of wxWidgets, Feel free to ask any question related to wxWidgets development here. This means questions regarding to C++ and wxWidgets, not compile problems.
Post Reply
kilasuelika
In need of some credit
In need of some credit
Posts: 6
Joined: Tue Sep 24, 2019 1:02 pm

[Solved]wxWidgets multithreaded and update UI.

Post by kilasuelika » Sun Oct 27, 2019 9:57 am

I want to change a element through a thread, then I find some strange problems.

Given this code(can be copied and run directly):

Code: Select all

#include<wx/wx.h>
#include <wx/msgdlg.h>
#include <wx/listctrl.h>
#include<thread>
class myFrame :public wxFrame {
private:
	wxButton* m,*m1;
	wxListCtrl* lct;
	std::thread test;
public:
	void replace() {
		//A new listctrl.
		wxListCtrl* lct1 = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDLG_UNIT(this, wxSize(-1, -1)), wxLC_REPORT);

		wxListItem col_cnt;
		col_cnt.SetId(1);
		col_cnt.SetText(wxT("New value"));
		col_cnt.SetWidth(100);
		col_cnt.SetAlign(wxLIST_FORMAT_CENTRE);
		lct1->InsertColumn(1, col_cnt);
		this->GetSizer()->Replace(lct, lct1);
	}
	void OnClick(wxCommandEvent& evt) {

		//test = std::thread(&myFrame::replace, this);
		//wxMessageBox("What");
		//test.join();

		replace();

	};

	myFrame() :wxFrame(nullptr, wxID_ANY, "test") {
		m = new wxButton(this, wxID_ANY, "Button");
		//wxPanel* pan = new wxPanel(this, wxID_ANY);
		wxBoxSizer* sz = new wxBoxSizer(wxVERTICAL);
		this->SetSizer(sz);
		lct= new wxListCtrl(this, wxID_ANY, wxDefaultPosition,
			wxDLG_UNIT(this, wxSize(-1, -1)), wxLC_REPORT);

		//Insert a new column.
		wxListItem col_cnt;
		col_cnt.SetId(1);
		col_cnt.SetText(wxT("old ctrl"));
		col_cnt.SetWidth(100);
		col_cnt.SetAlign(wxLIST_FORMAT_CENTRE);
		lct->InsertColumn(1, col_cnt);

		sz->Add(lct, 4, wxEXPAND);
		sz->Add(m, 1,wxEXPAND);
	
		m->Bind(wxEVT_BUTTON, &myFrame::OnClick, this);

		sz->Fit(this);
	};
};
class myApp : public wxApp {
private:
	myFrame* m_frame = new myFrame;

public:

	bool OnInit() {

		m_frame->Show();

		return true;
	}
};

I
Main GUI has a listrctrl and a button. When click the button, it will do something in OnClick(). OnClick() will call another function replace() in which I create a new listctrl and replace old one by sizer's Replace().

The problem is in the OnClick() which is:

Code: Select all

	void OnClick(wxCommandEvent& evt) {
//section 1:
		//test = std::thread(&myFrame::replace, this);
		//wxMessageBox("What");
		//test.join();
//Section 2:
		replace();

	};
1. If I only comment section 1 and only use replace(), I will see that the listctrl will show value "old ctrl". After click the button, it becomes "new value". It's what I wanted.

2. If I comment section 2 and change section 1 by remove wxMessageBox(), now it's:

Code: Select all

	void OnClick(wxCommandEvent& evt) {
	
		test = std::thread(&myFrame::replace, this);
		test.join();

	};
When click the button, the program will stop responding.

3. If I directly use section 1 (delete section 2) without remove the wxMessageBox() line. Then after click, program won't be blocked. But the value in listctrl doesn't change either. It's still "old ctrl".

I'm using window 10 and visual studio 16.4 preview.
Last edited by kilasuelika on Mon Oct 28, 2019 8:33 am, edited 1 time in total.

PB
Part Of The Furniture
Part Of The Furniture
Posts: 2355
Joined: Sun Jan 03, 2010 5:45 pm

Re: std::thread will block gui and more strange beheavior.

Post by PB » Sun Oct 27, 2019 10:16 am

kilasuelika wrote:
Sun Oct 27, 2019 9:57 am
I want to change a element through a thread, then I find some strange problems.
For starters: Are you aware that you can use GUI classes only from the main thread (= the thread wxWidgets were initialized from, i.e., where wxThread::IsMain() returns true)?
See e.g. here: https://docs.wxwidgets.org/trunk/overvi ... read_notes

As GUI access takes little time, it generally makes no sense to do that from a secondary thread. If you need to prepare data for GUI which make take a long time, you do it in a secondary thread and communicate with the controls via events handled in the main thread.

I just skimmed through your code and it seems to have MANY issues. TBH, I do not know how can you get away with creating main frame before initializing wxWidgets.

alys666
Super wx Problem Solver
Super wx Problem Solver
Posts: 317
Joined: Tue Oct 18, 2016 2:31 pm

Re: std::thread will block gui and more strange beheavior.

Post by alys666 » Sun Oct 27, 2019 10:52 am

Code: Select all

test = std::thread(&myFrame::replace, this);
test.join();
join blocks the current thread(main gui thread) till test finishes.
so
1. or test is not finished
2. or finished, but during its work has ruined GUI because of access to GUI functions.

---
if this is a function you used as thread body, then obviously gui has been ruined. you cannot create, show controls and handle events from secondary thread.

Code: Select all

void replace() {
		//A new listctrl.
		wxListCtrl* lct1 = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDLG_UNIT(this, wxSize(-1, -1)), wxLC_REPORT);

		wxListItem col_cnt;
		col_cnt.SetId(1);
		col_cnt.SetText(wxT("New value"));
		col_cnt.SetWidth(100);
		col_cnt.SetAlign(wxLIST_FORMAT_CENTRE);
		lct1->InsertColumn(1, col_cnt);
		this->GetSizer()->Replace(lct, lct1);
	}
ubuntu 16.04, wxWidgets 3.0.4

kilasuelika
In need of some credit
In need of some credit
Posts: 6
Joined: Tue Sep 24, 2019 1:02 pm

Re: std::thread will block gui and more strange beheavior.

Post by kilasuelika » Sun Oct 27, 2019 11:18 am

What if I really need to update GUI in another thread? Mainly because the task require some time and I want that after clicking the button,GUI can still respond to user's input.

alys666
Super wx Problem Solver
Super wx Problem Solver
Posts: 317
Joined: Tue Oct 18, 2016 2:31 pm

Re: std::thread will block gui and more strange beheavior.

Post by alys666 » Sun Oct 27, 2019 12:52 pm

you must run secondary thread, and it must send wxThreadMessage(or derived class) objects to main frame(or any derived class from wxEventHandler).
this works as;
thread "sends" a message - just puts an object to thread safe wxEvtHandler incoming queue.
when system switches to main(GUI) thread, it removes object from queue and raises an event
wxEVT_THREAD. handler registered to this event will be called(everything in main thread) and in his
body you can do everything with GUI objects.

Read about wxEVT_THREAD and wxThreadMessage in docs.
ubuntu 16.04, wxWidgets 3.0.4

PB
Part Of The Furniture
Part Of The Furniture
Posts: 2355
Joined: Sun Jan 03, 2010 5:45 pm

Re: std::thread will block gui and more strange beheavior.

Post by PB » Sun Oct 27, 2019 2:10 pm

Here is an example of a thread loading data in background. It does not use std::thread (as it is based on my code) but it should not matter, the principle should be the same. Try to compile and run as it is to see what it does.

Code: Select all

#include <wx/wx.h>
#include <wx/arrstr.h>
#include <wx/thread.h>

// the first event reports data load progress
// the second one reports that the data loading was cancelled
// the third one sends all loaded data at once
// wxDECLARE_EVENT macros should normally be in a .h file
wxDECLARE_EVENT(wxEVT_MY_DATA_LOADING, wxThreadEvent);
wxDECLARE_EVENT(wxEVT_MY_DATA_LOADING_CANCELLED, wxThreadEvent);
wxDECLARE_EVENT(wxEVT_MY_DATA_LOADED, wxThreadEvent);

wxDEFINE_EVENT(wxEVT_MY_DATA_LOADING, wxThreadEvent);
wxDEFINE_EVENT(wxEVT_MY_DATA_LOADING_CANCELLED, wxThreadEvent);
wxDEFINE_EVENT(wxEVT_MY_DATA_LOADED, wxThreadEvent);

class DataLoadingThread : public wxThread
{
public:
    DataLoadingThread(wxEvtHandler* sink) : wxThread(wxTHREAD_JOINABLE), m_sink(sink)
    {}

protected:
    wxEvtHandler* m_sink;
    
    ExitCode Entry() override
    {   
        const size_t total = 10;
        wxArrayString* data = new wxArrayString();
        
        for ( size_t i = 0; i < total; ++i )
        {                        
            if ( TestDestroy() ) // the user cancelled data loading
            {                
                delete data;
                wxQueueEvent(m_sink, new wxThreadEvent(wxEVT_MY_DATA_LOADING_CANCELLED));
                return static_cast<wxThread::ExitCode>(nullptr);
            }

            // simulates loading data
            wxMilliSleep(250);
            data->push_back(wxString::Format("Data %zu", i + 1));

            // sends progress info
            wxThreadEvent evt(wxEVT_MY_DATA_LOADING);

            evt.SetInt((double)data->size()/total * 100);
            wxQueueEvent(m_sink, evt.Clone());
        }

        // data loading has finished, send the data
        wxThreadEvent* evt = new wxThreadEvent(wxEVT_MY_DATA_LOADED);
        
        evt->SetPayload(data);        
        wxQueueEvent(m_sink, evt);
        
        return static_cast<wxThread::ExitCode>(nullptr);
    }
};

class MyFrame: public wxFrame
{
public:   
    MyFrame() : wxFrame (NULL, wxID_ANY, "Test", wxDefaultPosition, wxSize(800, 600))
    {        
        wxPanel* mainPanel = new wxPanel(this);
        wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);

        wxButton* button = new wxButton(mainPanel, wxID_ANY, "Load data");
        mainSizer->Add(button, wxSizerFlags().Expand().Border());
        button->Bind(wxEVT_BUTTON, &MyFrame::OnLoadData, this);

        button = new wxButton(mainPanel, wxID_ANY, "Cancel data loading");
        mainSizer->Add(button, wxSizerFlags().Expand().Border());
        button->Bind(wxEVT_BUTTON, &MyFrame::OnCancelLoadingData, this);

        m_dataView = new wxListBox(mainPanel, wxID_ANY);
        mainSizer->Add(m_dataView, wxSizerFlags().Proportion(1).Expand().Border());        
                                                                                
        mainPanel->SetSizer(mainSizer);

        Bind(wxEVT_MY_DATA_LOADING, &MyFrame::OnDataLoading, this);
        Bind(wxEVT_MY_DATA_LOADING_CANCELLED, &MyFrame::OnDataLoadingCancelled, this);
        Bind(wxEVT_MY_DATA_LOADED, &MyFrame::OnDataLoaded, this);
    }
    
    ~MyFrame()
    {
        StopDataLoadingThread();
    }

private:
    wxListBox* m_dataView;
    wxThread*  m_dataLoadingThread = nullptr;

    bool StartDataLoadingThread()
    {
        StopDataLoadingThread();

        m_dataLoadingThread = new DataLoadingThread(this);
        if ( m_dataLoadingThread->Run() != wxTHREAD_NO_ERROR )
        {
            delete m_dataLoadingThread;
            m_dataLoadingThread= nullptr;
            wxLogError(_("Could not create the thread needed to load the data."));
            return false;
        }            

        return true;
    }

    void StopDataLoadingThread()
    {
        if ( m_dataLoadingThread )
        {
            m_dataLoadingThread->Delete();
            delete m_dataLoadingThread;
            m_dataLoadingThread = nullptr;
        }
    }

    // button Load data clicked
    void OnLoadData(wxCommandEvent&)
    {        
        m_dataView->Clear();
        if ( StartDataLoadingThread() )
            m_dataView->AppendString(wxString::Format("<Loading data, please wait...>"));
    }

    // button Cancel loading data clicked
    void OnCancelLoadingData(wxCommandEvent&)
    {
        StopDataLoadingThread();
    }

    // handler for wxEVT_MY_DATA_LOADING, i.e., the progress report
    void OnDataLoading(wxThreadEvent& evt)
    {
        m_dataView->SetString(0, wxString::Format("<Loading data, finished %d%%>", evt.GetInt()));    
    }

    // handler for wxEVT_MY_DATA_LOADING_CANCELLED, i.e., when the user cancelled loading data
    void OnDataLoadingCancelled(wxThreadEvent&)
    {
        m_dataView->SetString(0, "<Loading data cancelled by user>");
    }

    // handler for wxEVT_MY_DATA_LOADED, i.e., all data loaded
    void OnDataLoaded(wxThreadEvent& evt)
    {
       wxArrayString* data = evt.GetPayload<wxArrayString*>();              

       m_dataView->Clear();
       m_dataView->Append(*data);
       delete data;
    }
};

class MyApp : public wxApp
{
public:         
    bool OnInit()
    {
        (new MyFrame())->Show();               
        return true;
    }   
}; wxIMPLEMENT_APP(MyApp);

kilasuelika
In need of some credit
In need of some credit
Posts: 6
Joined: Tue Sep 24, 2019 1:02 pm

Re: std::thread will block gui and more strange beheavior.

Post by kilasuelika » Mon Oct 28, 2019 8:32 am

I also figure out a std::thread version. See this:

Code: Select all

#include<wx/wx.h>
#include<thread>
#include<optional>
wxDEFINE_EVENT(wxEVT_CUSTOM_COMMAND, wxCommandEvent);

enum buttons {
	MY_BUTTON=1
};
class myFrame:public wxFrame {
private:
	wxButton* bt;
	wxBoxSizer* sz;
	wxPanel* pan;
	std::optional<std::thread> mythread;
public:
	
	//Define a custom event handler.
	void updateUI(wxCommandEvent &evt) {
		//New a button and replace it.
		wxButton* bt1 = new wxButton(pan, wxID_ANY, "New button");
		//use true to replace recurssively.
		sz->Replace(bt, bt1, true);
		bt->Hide();
	};
	//A async task which will be run in another thread.
	void async_task() {
		
		wxSleep(2);

		//Emit an event.
		wxCommandEvent evtcustom(wxEVT_CUSTOM_COMMAND);
		wxPostEvent(this, evtcustom);

	};
	//Button click event.
	void OnClick(wxCommandEvent& evt) {
		//If mythread has been initialized, then wait it to be finished.
		if (mythread) {
			mythread->join();
		};
		//Lanuch a new thread. Don't forget "this".
		mythread = std::thread(&myFrame::async_task, this);
	};

	myFrame() :wxFrame(nullptr, wxID_ANY, "Multi-threaded") {
		sz = new wxBoxSizer(wxVERTICAL);
		pan = new wxPanel(this, wxID_ANY);
		pan->SetSizer(sz);
		//Assign a my_button id to correctly recognize it.
		bt = new wxButton(pan, buttons::MY_BUTTON, "Click me");
		sz->Add(bt);
		sz->Fit(this);

		//Connect event and event handler.
		Connect(wxEVT_CUSTOM_COMMAND, 
			wxCommandEventHandler(myFrame::updateUI));
		Connect(buttons::MY_BUTTON,wxEVT_BUTTON, wxCommandEventHandler(myFrame::OnClick));

	};
	~myFrame() {
		//Dont't foget to wait the thread to be finished. Or there will be an exception.
		if (mythread) {
			mythread->join();
		};
		
	};
};
class myApp : public wxApp {
private:
	myFrame* m_frame = new myFrame;

public:

	bool OnInit() {

		m_frame->Show();

		return true;
	}
};

IMPLEMENT_APP(myApp)

PB
Part Of The Furniture
Part Of The Furniture
Posts: 2355
Joined: Sun Jan 03, 2010 5:45 pm

Re: [Solved]wxWidgets multithreaded and update UI.

Post by PB » Mon Oct 28, 2019 9:25 am

TBH, I do not understand why you did not learn from the example. The docs say that wxPostEvent() is not MT-safe. You should use wxThreadEvent and wxQueueEvent(). Connect() is de-facto deprecated and Bind() should be used instead.

I also suggest creating the main frame in the OnInit() as the custom dictates. Lastly, I strongly advise you to check some wxWidgets samples or tutorials to see how a good and maintainable C++ code using wxWidgets should be written.

alys666
Super wx Problem Solver
Super wx Problem Solver
Posts: 317
Joined: Tue Oct 18, 2016 2:31 pm

Re: [Solved]wxWidgets multithreaded and update UI.

Post by alys666 » Mon Oct 28, 2019 10:25 am

guys.
to send properly wxThreadEvent from user thread to main GUI thread, i just wrote this function.
it's from my code.
there are two variants(fast) and canonical :0)
chose which you like.

Code: Select all

void sendAsyncMessage(
		wxEvtHandler *frcv, //message reciever 
		int fuid, //message uid
		int fint, //integer data
		const wxString &fs //string data
	)
	{
#if 1
		if (!frcv) return; //no receiver given
		wxThreadEvent *le = new wxThreadEvent();	
		le->SetId(fuid); //set uid
		le->SetInt(fint); //set integer data
		le->SetString(fs.Clone()); //set string data, explicitly cloning given string							
		frcv->QueueEvent(le);  //enqueue an event object(here you must forget about it)
#else
	//this is more canonnical code(from docs) - 
	//but it looks slightly slower -because of cloning of entire event from stack
		if (!frcv) return;
		wxThreadEvent le; //we create and setup an event on stack
		le.SetId(fuid);
		le.SetInt(fint);
		le.SetString(fs);  
		frcv->QueueEvent(le.Clone()); //enqueue cloned event(create by new and copy)
#endif
	}
ubuntu 16.04, wxWidgets 3.0.4

Post Reply