Monday, July 2, 2012

A simple example of multiple GUIs working together

In this post, I will go over the implementation of a sample MATLAB application based on the comment left by a reader. A summary of the application's description is provided below, and the complete details can be found here. 

In this particular MATLAB application, there are three GUIs, namely, GUI-1, GUI-2 and GUI-3. A screenshot of the three GUIs are provided below, and the "starter code", which will be worked throughout this post, can be download here.

GUI-1 is the 'main' program and it serves as the gateway to the application. It provides a description of the application and has a push button, which when clicked on, will open up GUI-2 and close itself.

On GUI-2, there are multiple edit textboxes for accepting user inputs, there is also a push button, which when clicked on, will open up GUI-3 (if it hasn't been opened yet) and have the user inputs (or some results  computed based on the user inputs) pushed over to GUI-3 for display/presentation.

Note that once the GUI-3 is opened, any change in the user inputs on GUI-2 should trigger an update of the results displayed on GUI-3. There is also a push button on GUI-3, which when clicked on, will close GUI-2 and itself . 

Figure 1. Screenshot of the application 
Based on description above, the challenges faced in having these three GUIs working with each other can be posed as the follow three questions: 
1. How to open another GUI in an existing GUI?
2. How to close another GUI in an existing GUI?
3. How to pass data from one GUI to another GUI?

Opening GUI-2 from GUI-1


To be able to open GUI-2 by clicking a push button on GUI-1, simply call the m-file associated with GUI-2 in the callback of the push button on GUI-1. Using the sample code provided, find the callback function pb_open_Callback in GUI_1.m and add the following code. 

GUI_2_handle = GUI_2; % open GUI_2 and save the handle

To have GUI_1 close itself immediately after opening GUI_2, add the following code after the line above.

delete(get(hObject, 'parent')); % close GUI_1

Note that get(hObject, 'parent') returns the handle of GUI_1, since it is the parent of the push button, which is referred to as the  hObject in its callback. 


Now, save and run GUI_1. When you click on the push button, you should be able to see GUI_2 pops up and GUI_1 closes itself immediately after GUI_2. The updated code can be found here.



Opening GUI-3 from GUI-2, and closing GUI-2 from GUI-3

Opening GUI-3 with the push button on GUI-2 is very similar to what has been done above. However, the one key difference here is that there will be on-going interactions between GUI-2 and GUI-3, and thus, there are issues to be taken care of. For example, if the push button is clicked again after GUI-3 opens, it shouldn't be opening another instance of GUI-3, also, since GUI-3's push button is responsible for closing the whole application, the close functions  (executed when you clicked on the 'x' button at the upper right corner)  in GUI-2 and GUI-3 should not close themselves. 

1. In the opening function of GUI-2, initialize an empty field in the handles structure, which is to be used to hold the handle to GUI-3. The reason why we need to put it into the handles structure is that we will need to access the variable in other sub functions of GUI_2 (see step 2).


handles.GUI_3_handle = [];

2. In the callback of the push button on GUI-2, check to see if GUI-3 is already open, and if it is not, opens it the same way we did for GUI-2.

if isempty(handles. GUI_3_handle )
   % create GUI-3 
   handles. GUI_3_handle  = GUI_3; 
end

3. To be able to close GUI-2 within GUI-3, GUI-3 must have the handle to GUI-2 so that when the push button on GUI-3 is clicked, it will be able to use the delete function to first close GUI-2 and then GUI-3. There are many ways in which you can pass the handle (or any kind of data) from one GUI to another. Since this is a relatively simple example, it is acceptable to not to think too much about re-usability. Below shows the extra code that is responsible for passing the handle of GUI-2 to GUI-3.

if isempty(handles.GUI_3_handle)
   % create GUI-3 
   handles.GUI_3_handle = GUI_3;
   % obtain data in GUI-3
   GUI_3_data = guidata(handles.GUI_3_handle); 
   % store handle to GUI-2 as data in GUI-3
   GUI_3_data.GUI_2_handle = get(hObject,'parent');
   % save the GUI-3 data 
   guidata(handles.GUI_3_handle, GUI_3_data);

   % save the handles structure as data of GUI-2
   guidata(get(hObject,'parent'), handles);

end

4. To make sure the close function does not close the corresponding GUIs, replace the delete function with a dialog box that informs the user about the proper way to close the application. Note that to populate the close function in the m file, open the GUI using GUIDE. Under the figure property, click on the CloseRequestFcn (as shown in Figure 2 below).  

Figure. Creating the CloseRequestFcn in the figure property
In the CloseRequestFcn in the m-file (for both GUI-2 and GUI-3), replace the delete function, with the follow code. (You may want to pick a more appropriate dialog box.)

warndlg('Please click Close Application button on GUI-3','!! Warning !!')

If you try clicking the 'x' on the window to close the GUI, the warning dialog box will pop up and the GUI should stay opened.

5. To have the GUIs closed after the 'Close application' button on GUI-3 is clicked, simply put the following line in the call back of the push button.

delete(handles.GUI_2_handle);  % close GUI-2 
delete(get(hObject,'parent')); % close GUI-3 

Now, the opening and closing of all the GUIs in the application should function properly. The latest code can be downloaded here.

Updating results on GUI-3 from GUI-2


Based on the description of the application, when GUI-3 is opened from GUI-2, the results should be computed and then shown on GUI-3. In the callback of the push button on GUI-2, after the if-end block in which we open GUI-3 if it hasn't been created yet, we will add more code to take care of the computation of the results and the update of the results on GUI-3. The added code is highlighted in red below. 


if isempty(handles.GUI_3_handle)
   % create GUI-3 
   handles.GUI_3_handle = GUI_3; 
   % obtain data in GUI-3
   GUI_3_data = guidata(handles.GUI_3_handle); 
   % store handle to GUI-2 as data in GUI-3
   GUI_3_data.GUI_2_handle = get(hObject,'parent');
   % save the GUI-3 data 
   guidata(handles.GUI_3_handle, GUI_3_data);

   %save the handles structure as data of GUI-2
   guidata( get(hObject,'parent'), handles);

end
% GUI-3 must exist at this point. 

% obtain data stored in GUI_3 
GUI_3_data = guidata(handles.GUI_3_handle); 
% compute result based on input 1 on GUI_2 and update result 1 on GUI_3
input1 = str2double( get(handles.txt_input1, 'string'));
set(GUI_3_data.lbl_res1, 'string', num2str( input1 * rand));
% compute result based on input 2 on GUI_2 and update result 2 on GUI_3
input2 = str2double( get(handles.txt_input2, 'string'));
set(GUI_3_data.lbl_res2, 'string', num2str( input2 * rand));


For simplicity's sake, I compute the results as random numbers generated based on the inputs. Regardless of how complicated the computation can get, it is irrelevant to how the update is done. You can simply apply the set function on the static text fields on GUI-3, which are lbl_res1 and lbl_res2, to change the strings based on the "computed" results. Of course, it doesn't have to be static text fields, it can be an axes, it can be a list, as long as you can use the set function to modify the property of the uicontrol as a way to present the result, this approach would work. 


At this point, any time you click on the "Show results" button on GUI-2 , you should be able to see the results being updated on GUI-3. Next, we can work on the edit text on GUI-2, so that whenever a new input is entered (triggered by the focus leaving the text box or hitting the enter key in the text box), the corresponding result will be updated in GUI-3. First, go to the callback of input1's edit box, and add the following code.

% obtain data stored in GUI_3 
GUI_3_data = guidata(handles.GUI_3_handle); 
% compute result based on input 1 on GUI_2 and update result 1 on GUI_3
input1 = str2double( get(handles.txt_input1, 'string'));
set(GUI_3_data.lbl_res1, 'string', num2str( input1 * rand));

Then, for the callback of input2's edit box, add the following code

% obtain data stored in GUI_3 
GUI_3_data = guidata(handles.GUI_3_handle); 
% compute result based on input 2 on GUI_2 and update result 2 on GUI_3
input2 = str2double( get(handles.txt_input2, 'string'));
set(GUI_3_data.lbl_res2, 'string', num2str( input2 * rand));


At this point, if you update the inputs individually, you will see the corresponding results being updated on GUI-3 as well. The updated code can be downloaded here.


Things to consider

Note that what has been present here is a quick and easy way of getting things done. I would recommend this approach only if you have full knowledge of all the GUIs in the application and you don't plan to re-use any of the GUIs in the same or different application.

to be continued

Related links:
"Making Multiple GUIs Work Together" on mathworks.com