[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

GUI-Model communication



Martin Waller recently posted a question regarding methods used to communicate between a simulation class instance and a controlling GUI. I have done this recently for a model, in which the model updates a progress bar, displays a message in a text canvas displaying the simulation time and recent actions, and accepts a "cancel" request generated by the user pressing a "cancel" button on the progress bar dialog. 

I have attached a short HTML document explaining how I did this. The method is a bit more involved than I like, but I could not think of a briefer method. (If you have suggestions to make the process less complex, please post them for us.)

Essentially it works like this:

1. The GUI class makes an instance of the simulation class, and gives it pointers to global functions to which will either send or receive messages from the simulation instance.

2. The GUI class tells the simulation to run, and concurrently displays a progress bar with a Cancel button.

3. The simulation has a time-step loop, within which it sends a message to the progress bar dialog containing the simulation time, sends messages as needed to be displayed to the user, and checks for received ("posted") messages at each time step.



Tom Hilinski
e-mail:  hilinski@lamar.colostate.edu

Title: Century-GUI Communication

Communication Between the Century class and its GUI


Tom Hilinski hilinski@lamar.colostate.edu 19Sep99

Introduction

The Century soil organic matter model is implemented as a C++ class with two types of controllers: a command-line interface and a GUI using the V library. The command-line controller writes all messages for the user to stdout (cout). Interaction with the GUI controller is much more complex. The model must interact with the GUI at specified time steps to display a progress bar and to provide the user with status messages.

If not run as a stand-alone process, the Century model, class TCentury, needs to communicate with its controlling functions, e.g., the GUI. Ideally this should be implemented using a platform-portable threaded class. In a rather crude attempt to allow messaging without threads, I have implemented the following:

msgFunction
A TCentury member variable which points to an external function accepting a string argument. This allows Century to pass messages to an external function for display to the user. e.g., the V library class, vNotice. Use the member function UseMsgFunction to set this pointer.
yearFunction
A TCentury member variable which points to an external function accepting a long argument containing the current simulation year. This allows an external function to monitor the progress of the simulation. e.g., a progress bar display. Use the member function UseYearMsgFunction to set this pointer.
cancelRequest
A TCentury member variable which is true when a request to cancel the simulation is made. Each iteration of the simulation loop checks for this request. If true, the simulation loop is exited. Use the member function PostCancelRequest to post this message. This request is generated when the user clicks the Cancel button on the Progress Bar dialog.

Member Variables and Functions

The declarations for these variables and their inline functions are:

//--- data - for asynchronous communication with external functions
protected:
void (*msgFunction)(const char*);	// function to display a msg
void (*yearFunction)(const long);	// function to get sim. year
bool cancelRequest;			// true if request to cancel
					//   the simulation posted.
bool simCompleted;			// true if the simulation ran
					//   to completion.
//--- functions
public:
//	UseMsgFunction
//	Sets the external function which will receive messages from Century.
void UseMsgFunction (
	void (*useMsgFunction)(const char* msg) )
{
	if ( useMsgFunction )
		msgFunction = useMsgFunction;
}
//	UseYearMsgFunction
//	Sets the external function which will receive the current
//	simulation year from Century.
void UseYearMsgFunction (
	void (*useYearFunction)(const long year) )
{
	if ( useYearFunction )
		yearFunction = useYearFunction;
}
//	PostCancelRequest
//	Posts a simulation cancel request.
void PostCancelRequest ()
{
	cancelRequest = true;
}
//	SimulationCompleted
//	Returns true if the simulation has completed, else false.
bool SimulationCompleted () const
{
	return simCompleted;
}

Displaying Messages

The Century model is built in two forms, a command-line form that runs from a shell prompt, and a GUI form, in which a GUI controller initiates the model simulation. In order to display message to the user the model has to distinguish, at compile-time, between sending messages to stdout or to some external function which will display the message. As shown below, a #define is used to distiguish two different global functions for this purpose. The function declarations are:

#ifdef STANDALONE
  // assume that Century DOES have direct access to the console for I/O
  void DisplayUserString (
    const char *str);			// string to display to user
#else
  // assume that Century DOES NOT have direct access to the console for I/O
  void DisplayUserString (
    void (*msgFunction)(const char*),	// pointer to external function
    const char *str );			// string to display to user
#endif

The function definitions are brief

#include 

void DisplayUserString (
	const char *str)
{
	if (str && *str)
		cout << str << endl;
}

void DisplayUserString (
	void (*msgFunction)(const char*), const char *str )
{
	if (str && *str && msgFunction )
		(*msgFunction)(str);
}

Optionally, rather than using a #define , the model could be using a run-time check to deterimine which function to call.

In the Century model, the simulation has a set of tasks it has to perform at the start of each simulation year. This is a convenient place to call the external function which accepts the current simulation year value. The first few lines of this simulation loop is shown below. A class TSimTime manages the simulation time calculations; the instance of this class used in the snippet is named st. This snippet also shows handling of an asynchronous Cancel request, and the displaying of simulation time to the user.

char msg[161];				// String for user messages
ostrstream os (msg, sizeof(msg));	// ...and a stream to create them
while (moreToDo)			// Do for each time step (month)
{
	if ( st.IsStartOfYear() )	// Perform annual tasks
	{
	    if ( yearFunction && st.year % updateInterval == 0 )
		(*yearFunction)(st.year);
	    EachYearsTasks ();
	}
	// Now do the model's work for this time step...

	// Write output variables
	if ( st.TimeForOutput() )
	{
		os << "output time: year " << st.year
		   << ", month " << (short)st.month << ends;
		#ifdef STANDALONE
		  ::DisplayUserString (msg);
		#else
		  ::DisplayUserString (msgFunction, msg);
		#endif
		os.seekp (ios::beg);
	}

	// Check for cancelation requested posted asynchronously
	// via PostCancelRequest().
	if ( cancelRequest )
		break;

	// Update simulation time, then quit or continue...
}

How the GUI Handles User Messages to the Model

In order to receive messages from the model, the V application class cannot use member functions, since their pointers cannot be used by the Century class. So, global functions are use, as defined below.

//	DisplayToMsgWindow
//	Writes the string contents to the message window.
//	This is for Century to have a function pointer that is not
//	a class member for its century->UseMsgFunction() method.
static void DisplayToMsgWindow (const char* str)
{
	if ( str && *str )
	{
		MsgText (str);
		::myApp.CheckEvents ();		// V will process the event
	}
}

//	UpdateProgressDialog
//	Updates the progress bar in the progress dialog.
//	This is for Century to have a function pointer that is not
//	a class member for its century->UseYearMsgFunction() method.
static void UpdateProgressDialog (const long year)
{
	if ( !::progDlg )				// anything to do?
		return;

	static long numYears = 0, yearStart = 0;

	// calc only once
	if ( !numYears )
	{
		yearStart = ::curMgmt->GetSimInfo().yearStart;
		numYears = ::curMgmt->GetSimInfo().yearEnd - yearStart + 1;
	}
	// calc percent completed
	int percent = (float)(year - yearStart + 1) / numYears * 100.0 + 0.5;
	::progDlg->Update (percent);
	::myApp.CheckEvents ();			// V will process the event
}

//	PostCancelRequest
//	Posts a cancel request to the Century process.
//	This is used by class TProgressDlg in its UseCancelFunction() method.
static void PostCancelRequest ()
{
	if ( ::century )				// anything to do?
		::century->PostCancelRequest ();
}

The member function which starts the simulation also sets up the message functions. In the snippet below, the global pointer to the Century class instance is called to assign the message function pointers. Also, the progress dialog class, TProgressDlg, has a Cancel button, which, when pressed, results in the posting of a Cancel request to the simulation instance, via the global function PostCancelRequest, shown above.

//	RunCentury
//	Run the Century simulation using the current configuration.
//	Returns false if run w/out error, else true if error or canceled.
bool TApp::RunCentury ()
{
	// Initialize the model instance...
	::century = new TCentury ( /* ... */ );

	// Assign pointers to the message functions
	::century->UseMsgFunction ( ::DisplayToMsgWindow );
	::century->UseYearMsgFunction ( ::UpdateProgressDialog );

	// Set up the progress bar dialog
	::progDlg = new TProgressDlg (this, /* ... */);
	::progDlg->UseCancelFunction ( ::PostCancelRequest );

	// Run simulation (does not show exception handling)
	::century->RunSimulation ();
}


Copyright © 1999, Thomas E. Hilinski. All rights reserved


Reply to: