Hi David,
Before continuing, at this point you must decide how you want to implement your process:
(1) As a synchronous process. Think of your tool as if it was a special version of the NewImage process. NewImage creates new images from a set of user-defined parameters (image geometry, color space, etc.). Your process would create new images as a result of an acquisition task initiated by the user (we'll see how later).
Pros: Easy to implement, relatively simple and rock-solid implementation.
Cons: as this is a synchronous task, the whole PixInsight platform has to wait until the image acquisition process is either completed or aborted.
(2) As an asynchronous process. Think of it as a special variation of the Statistics tool. Statistics is an
observer. Your process isn't an observer, but it shares with Statistics the fact that it cannot be executed, neither in an image context nor in the global context. In this case your interface governs your process completely, something that Statistics also does. It has no standard execution button in its control bar (neither a triangle nor a sphere) and cannot generate instances (no process icons). You just have a button that reads 'Start' or something like that. The user presses 'Start' and your task is launched by the button's event handler. Your process must be implemented as an asynchronous thread that works in the background.
Pros: Versatile implementation. PixInsight continues working normally while your process runs in the background.
Cons: Complex and risky implementation. There are many potential stability issues. For example, what happens if the user terminates the application while your background thread is running? There are currently no provisions to assist you in this case (this may change in a future version though). There are possible interactions with other processes that you cannot know in advance. Basically, you have no control once your task has been launched because your interface returns to the main GUI thread immediately.
Option 2 may seem attractive, and it is doable, although it requires a lot of work that would distract you from your main development work. Personally I think option 2 is a bad option for other reasons. My recommendation is that you implement option 1. Then once you have your acquisition process well tested and debugged, converting it to option 2 isn't too difficult, although I doubt it is really interesting. I understand that what you don't like of option 1 is the fact that PI gets 'frozen' while your task is running. Well, that isn't as bad as it sounds; it is actually very good! After all, PI gets frozen with any regular process. For example, think of a long ImageIntegration process. Along with the fact that there is actually no freeze, remember that you can execute up to 256 simultaneous instances of the PI Core application, so you can have one instance acquiring images and another one working normally.
So let's assume that you think it ... ... ... OK, and you choose option 1
Several important considerations:
- Forget about ExecuteGlobal(). You don't want your process executed in the global context. The reason is that when a process is executed, the whole PI GUI —including your interface— are disabled and no user interaction, besides moving windows, is allowed. You definitely don't want this.
- As happens with option 2, you have a Start button (or an 'Acquire' button or whatever you want to call it). What happens when the user clicks Start? Easy: you open a modal dialog, launch your process as a thread, and enter a waiting loop very similar to your snippet. A modal dialog allows you: (a) ensure that you have a working GUI to provide feedback to the user, and (b) ensure that
only your GUI can be used during the whole acquisition process. In other words, you have full control.
- Your process interface allows you to perform maintenance tasks such as defining cameras, working parameters, etc. But once your Start button gets pressed, your modal dialog does the job. This is a typical divide and conquer strategy.
- You cannot create image windows from a thread. Only the GUI thread (the main PixInsight Core thread, from which your Start button's OnClick() event handler is called) can perform GUI operations. A thread cannot communicate using GUI resources in any way.
So your next question is how can I create an image window during the acquisition process? Well, the following code is an example of how the whole thing would look like:
struct ExposeImageData
{
ExposeImageData() :
mutex(), image(), imageProgress( 0 ), imageReady( false ),
abort( false ), error( false ), errorMessage()
{
}
Mutex mutex; // To protect data from concurrent accesses
UInt16Image image; // The image being acquired
int imageProgress; // Progress indicator, e.g. from 0 to 100
bool imageReady; // Flag true if a new image is now ready
bool abort; // Flag true if the user wants to abort
bool error; // Flag true if an error occurs
String errorMessage; // Error information
... // more stuff
};
ExposeImageData data;
class ExposeImageThread : public Thread
{
public:
...
virtual void Run()
{
while ( true )
{
// Check if the user has aborted the process
if ( data.abort )
{
... possibly perform some cleanup here
break;
}
... do your acquisition stuff here
// Check possible errors
if ( error )
{
data.mutex.Lock();
data.error = true;
data.errorMessage = "Hmm, something went wrong...";
data.mutex.Unlock();
break;
}
// Update the progress indicator
data.mutex.Lock();
data.imageProgress++;
data.mutex.Unlock();
// Do we have acquired a new image?
if ( acquisitionComplete )
{
data.mutex.Lock();
data.imageReady = true;
data.imageProgress = 100;
data.mutex.Unlock();
}
// Job done?
if ( finishedAcquiringImages )
break;
}
}
};
class ExposeImageDialog : public Dialog
{
public:
ExposeImageDialog() : Dialog()
{
... build your dialog here
// Handle Abort button click events
Abort_PushButton.OnClick( (Button::click_event_handler)&ExposeImageDialog::__Button_Click, *this );
// Attach your own dialog execution handler, so you gain control
// as soon as your dialog is executed modally.
OnExecute( (Dialog::execute_event_handler)&ExposeImageDialog::__Dialog_Execute, *this );
}
void __Dialog_Execute( Dialog& sender )
{
// Here we are. Now we are the *only* element of PixInsight
// that can be used interactively by the user. Since this has
// been called by the GUI thread, we can do anything we want
// with GUI resources.
// This is your original snippet
exposeThread = new ExposeImageThread( TheImageAcquisitionSettingsInterface->activeCamera, exposureDuration, exposureCount );
exposeThread->Start();
while ( exposeThread->IsExposing() )
{
//console << "exposing .... \n"; <-- ups, forget about this; there is no console available at this point!
pcl::Sleep( .01 );
ProcessInterface::ProcessEvents();
// Now let's see if we have an image ready
data.mutex.Lock();
bool myImageReady = data.imageReady;
data.mutex.Unlock();
if ( myImageReady )
{
... code to create a new image window
}
}
if ( data.error ) // note that the thread is not running at this point
{
MessageBox( data.errorMessage, ... ).Execute();
}
...
}
void __Button_Click( Button& sender, bool checked )
{
// Handle the Abort button
if ( sender == Abort_PushButton )
{
data.mutex.Lock();
data.abort = true;
data.mutex.Unlock();
}
else ... // handle other buttons
}
};
// This is your interface class
class ExposeImageInterface : public ProcessInterface
{
public:
...
private:
void __Button_Click( Button& sender, bool checked )
{
// Handle the Start button
if ( sender == Start_PushButton )
{
ExposeImageDialog dialog;
dialog.Execute();
}
}
};
This is just an example to show you how the different parts would work. Of course, the acquisition task could be implemented without a thread. however, a thread leads to a more modular implementation and allows for an easier adaptation of the whole engine to another paradigm in the future, such as option 2 above.
As always, code not tested at all. Hope this helps you to start running in the right direction