Help creating a wxProcess/wxGenericProgressDialog combo!

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
supaycha
In need of some credit
In need of some credit
Posts: 4
Joined: Thu Aug 13, 2020 7:07 pm

Help creating a wxProcess/wxGenericProgressDialog combo!

Post by supaycha »

I am working with Windows/VS 2019/latest wxWidgets version available from vcpkg. I've been wanting to create a wxProcess that runs a cmd script asynchronously with the GUI updating the user through a wxGenericProgressDialog until the script has returned, prompting the user to dismiss the dialog once a message confirming script completion appears. I have everything working but with the single concession being that I had to set the wxPD_AUTO_HIDE flag to get the wxGenericProgressDialog instance to be removed and grant control back to the user. Without that flag set, the wxGenericProgressDialog becomes "unresponsive" upon completion: the message I have set to appear appears, but while the dialog can be moved around during the script execution, the dialog freezes in place and can no longer be moved around.
Below are the two classes I have at the moment, both are work in progress mainly because I have reached a huge knowledge ceiling and have been resorting to trial and error for a few days now. I could use some help identifying all the major errors and/or misunderstandings in the code below.
P.S. The code below had all of the formatting removed. If someone could point out how to add it back in to make the code below legible, let me know and I'll fix it ASAP. Thanks to anyone who takes the time to read and/or offer feedback.
//CrawlerProcess.h

Code: Select all

enum class WorkerEvent {
	SPIDERS_FINISHED,
	END_OF_PROCESS,
};

class CrawlerProcess : public wxProcess {
protected:
	wxWindow* m_parent;
	wxString m_cmd;
public:
	CrawlerProcess(wxWindow* parent, const wxString& cmd) :
		m_cmd{ cmd },
		wxProcess(parent)
	{
		m_parent = parent;
		Redirect();
	}

	virtual bool HasInput();
};
//CrawlerProcess.cpp

Code: Select all

bool CrawlerProcess::HasInput() {
	bool hasInput = false;
	if (IsInputAvailable()) {
		wxTextInputStream tis(*GetInputStream());

		// this assumes that the output is always line buffered	
		wxString msg1;
		msg1 << tis.ReadLine();
		if (std::string::npos != msg1.find("DONE")) {
			wxThreadEvent* te = new wxThreadEvent();
			te->SetInt((int)WorkerEvent::SPIDERS_FINISHED);
			wxQueueEvent(m_parent, te);
		}

		hasInput = true;
	}

	if (IsErrorAvailable()) {
		wxTextInputStream tis(*GetErrorStream());

		// this assumes that the output is always line buffered
		wxString msg2;
		msg2 << tis.ReadLine();
		hasInput = true;
	}

	return hasInput;
}
//ProcessFrame.h

Code: Select all

class ProcessFrame : public wxGenericProgressDialog {
private:
	// the idle event wake up timer
	wxTimer m_timerIdleWakeUp;

	CrawlerProcess* process{};
	wxTextInputStream* aasdf{};
protected:
	// the checking if proc is done timer
	wxTimer m_timerProcDone;

	// the flag that indicates when process is done
	bool proc_done = false;
	BatFile m_bf;	
	int m_crawler_select;

public:
	ProcessFrame(wxWindow* parent, int style, BatFile bf);
	~ProcessFrame() {}

	void Execute(wxString command);
	void CreateAndShowProgressDlg();
	void OnProcDoneTimer(wxTimerEvent& event);
	void OnIdle(wxIdleEvent& event);
	void OnComms(wxThreadEvent& event);
	void OnProcessTerminated(wxProcessEvent& event);
};
//ProcessFrame.cpp

Code: Select all

enum {
	Exec_TimerIdle = 10,
	Exec_TimerProcDone = 11
};

ProcessFrame::ProcessFrame(wxWindow* parent, int style, BatFile bf) :
	m_timerIdleWakeUp(this, Exec_TimerIdle),
	m_timerProcDone(this, Exec_TimerProcDone),
	m_bf{bf},
	//m_crawler_select{crawler_select},
	wxGenericProgressDialog("In Progress", "Beginning", 100, parent, style)
{

	//crawling process starts here asynchronously
	this->Execute(m_bf.GetCommand());
	m_timerProcDone.Start(500);

	Bind(wxEVT_TIMER, &ProcessFrame::OnProcDoneTimer, this, Exec_TimerProcDone);
	Bind(wxEVT_THREAD, &ProcessFrame::OnComms, this);
}

void ProcessFrame::Execute(wxString command) {
	process = new CrawlerProcess(this, command);
	process->Bind(wxEVT_END_PROCESS, &ProcessFrame::OnProcessTerminated, this);
	process->Bind(wxEVT_END_PROCESS, &ProcessFrame::OnAsyncTerminated, this);
	process->Redirect();

	if (!wxExecute(command, wxEXEC_ASYNC, process)) {
		delete process;
	}
	else {
		m_timerIdleWakeUp.Start(100);
		Bind(wxEVT_IDLE, &ProcessFrame::OnIdle, this);
		CreateAndShowProgressDlg();
	}
}

void ProcessFrame::CreateAndShowProgressDlg() {
	Bind(wxEVT_TIMER, &ProcessFrame::OnProcDoneTimer, this, Exec_TimerProcDone);
	Bind(wxEVT_THREAD, &ProcessFrame::OnComms, this);
}

void ProcessFrame::OnProcDoneTimer(wxTimerEvent & WXUNUSED(event)) {
	if (proc_done) {
		m_timerProcDone.Stop();
	}
}

void ProcessFrame::OnIdle(wxIdleEvent & event) {
	if (m_timerIdleWakeUp.IsRunning()) {
		if (process->HasInput()) {
			event.RequestMore();
		}
	}
}

void ProcessFrame::OnComms(wxThreadEvent & event) {
	WorkerEvent we = (WorkerEvent)event.GetInt();
	switch (we) {
		case WorkerEvent::SPIDERS_FINISHED: {
			this->Update(100, spider_close);
			Unbind(wxEVT_IDLE, &ProcessFrame::OnIdle, this);
			proc_done = true;
			break;
		}
		case WorkerEvent::END_OF_PROCESS: {
			this->Update(100, spider_close);
			break;
		}
	}
}

void ProcessFrame::OnProcessTerminated(wxProcessEvent& event) {
	m_timerIdleWakeUp.Stop();
}
Last edited by doublemax on Mon Apr 05, 2021 7:28 pm, edited 1 time in total.
Reason: Added code tags
User avatar
doublemax
Moderator
Moderator
Posts: 19116
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: Help creating a wxProcess/wxGenericProgressDialog combo!

Post by doublemax »

I can't pinpoint exactly what's wrong, but subclassing wxGenericProgressDialog doesn't sound like a good idea. A progress dialog has a very narrow usage pattern and it can't even be used like a "normal" wxDialog, let alone a wxFrame.

Code: Select all

{
  wxGenericProgressDialog dlg(..);
  dlg.Show();
  while (something) 
  {
    dlg.Update() or dlg.Progress()
  }
}
How does the code look like where you use ProcessFrame?
Use the source, Luke!
supaycha
In need of some credit
In need of some credit
Posts: 4
Joined: Thu Aug 13, 2020 7:07 pm

Re: Help creating a wxProcess/wxGenericProgressDialog combo!

Post by supaycha »

I deeply apologize for not responding to your own response, as I realized later that day that my mistake was in fact completely unrelated to wxWidgets. I wasn't sure how to notify this site that I wanted to close this request so I, immaturely, just walked away from it. I won't do this again. I appreciate your advice about not subclassing from wxGenericProgressDialog though. Generally speaking, I have been looking for the most applicable controls to suit my needs and then subclassing from those controls in an effort to minimize the amount of work (given I'm trying to make each learning step as small as possible to maximize the likelihood that what I learn sticks) needed to implement. Should I start from more general controls, such as wxDialog, instead?
ONEEYEMAN
Part Of The Furniture
Part Of The Furniture
Posts: 7459
Joined: Sat Apr 16, 2005 7:22 am
Location: USA, Ukraine

Re: Help creating a wxProcess/wxGenericProgressDialog combo!

Post by ONEEYEMAN »

Hi,
It depends on the task at hand.
Usually subclassing wxWidgets controls have very small use-case scenarios especially with the latest release where you can dynamically bind the event from anywhere
The only 2 GUI classes that aer make sense subcklassing are wxFrame/wxTLW and wxDialog.

Good luck.
User avatar
doublemax
Moderator
Moderator
Posts: 19116
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: Help creating a wxProcess/wxGenericProgressDialog combo!

Post by doublemax »

I don't think you have to derive from any GUI class for this. Assuming you already have a main frame from which the action is triggered by the user somehow, just start the thread and show a progress dialog until the thread is finished.

I would do it like this:

Demo thread and simple class for thread status:

Code: Select all

class ThreadStatus
{
public:
  ThreadStatus() : progress(0), finished(false) {};

  int progress;
  bool finished;
};

class DemoThread : public wxThread
{
public:
  DemoThread( ThreadStatus *status ) : wxThread(wxTHREAD_DETACHED)
  {
    m_status = status;
  };

  virtual void *Entry()
  {
    for( int i = 0; i < 30 && !TestDestroy(); i++ )
    {
      m_status->progress = i * 100 / 30;
      wxThread::Sleep(100);
    }

    m_status->finished = true;

    return NULL;
  };

protected:
  ThreadStatus *m_status;
};
Demo code for thread usage and progress display:

Code: Select all

void MyFrame::OnAction(wxCommandEvent& WXUNUSED(event))
{
  ThreadStatus threadStatus;
  DemoThread *thread = new DemoThread( &threadStatus );

  if( thread->Create() == wxTHREAD_NO_ERROR )
  {
    thread->Run();
    wxGenericProgressDialog dlg("Caption", "Processing", 100, this);
    dlg.CenterOnScreen();
    dlg.Show();

    while( !threadStatus.finished )
    {
      wxLogDebug("%d %d", threadStatus.finished, threadStatus.progress);
      dlg.Update( threadStatus.progress, wxString::Format("processing: %d%%", threadStatus.progress) );
      ::wxMilliSleep(250);
    }
  }
}
And if you ask yourself "why is the thread status external and not part of the thread?":
Detached threads delete themselves when they're done, so any call like thread->GetStatus() would be unsafe because the thread could already be destroyed. Having the thread write into a variable that lives in the main thread avoids this issue.
Use the source, Luke!
Post Reply