Buttons: Callbacks vs. wasTriggered()

The Button component behaves slightly different from most of the other components. If we consider a slider, that is used to adjust a certain value, it is completely clear, how this value, even though it is adapted in the application’s GUI thread, can be accessed in the working thread. Here, our implementation will just keep a registered int variable up to date, whose value is returned when the slider value is queried from the GUI instance. In contrast, a button usually triggers some kind of event. However, we can not simply register a callback to the buttons click event, since the callback would then be executed in the GUI thread, leaving the user all the extra work for explicit synchronization with the application’s working thread.

Actually, in some special situations, a callback registration is exactly what we want, in particular, if e.g. a button-click affects some other Qt-GUI components directly. For this, the GUI class provides the ability to register callbacks to nearly all components.

However, as introduced above, most of the time, a button-click is not to be executed immediately, but the next time, the working-thread run through. For this purpose the button-component’s handle class ButtonHandle provides the wasTriggered method, that returns true if the button was pressed since the method was called before.

A Simple Example

Lets just implement a simple image processing application, that applies one of the morphological operators provided by ICL in real-time on a given input image. The application has three buttons:

  1. use next filter
  2. save current result image
  3. show an extra GUI for the input image

For the the buttons 1 and 2 we will use the ButtonHandle and its wasTriggered method. Since button 3 affects a Qt-GUI component, we will use the callback mechanism for this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <ICLQt/Common.h>
#include <ICLFilter/MorphologicalOp.h>

HSplit gui;
GUI input;
GenericGrabber grabber;
MorphologicalOp op;
int currFilter = 0;

void init(){
  grabber.init(pa("-i"));
  gui << Image().handle("result") 
      << ( VBox() 
           << Button("next filter").handle("next")
           << Button("save").handle("save")
           << Button("show src").handle("show")
           )
      << Show();

  input << Image().handle("image") << Create();

  gui["show"].registerCallback(function(input,
                      &GUI::switchVisibility));
}

void run(){
  static ButtonHandle next = gui["next"];
  static ButtonHandle save = gui["save"];

  const ImgBase *image = grabber.grab();

  if(next.wasTriggered()){
    currFilter = (currFilter+1)%10;
    op.setOptype((MorphologicalOp::optype)currFilter);
  }
  
  const ImgBase *result = op.apply(image);
  
  gui["result"] = result;

  if(input.isVisible()){
    input["image"] = image;
  } 
  
  if(save.wasTriggered()){
    qt::save(*result,"current-image.png");
  }   
}
int main(int n, char **args){
   return ICLApp(n,args,"-input|-i(2)",init,run).exec();
}

main GUI

shadow

extra GUI

shadow

started with:

./example -input create cameraman

Step by Step

After including the prototyping header ICLQt/Common.h, and the header for the filter::MorphologicalOp that is used, the static application data is declared. We use two GUI instances, one of the GUI-subclass HSplit. Furthermore, a GenericGrabber is used for image acquisition and a simple integer variable for the current morphological filter type.

#include <ICLQt/Common.h>
#include <ICLFilter/MorphologicalOp.h>

HSplit gui;
GUI input;
GenericGrabber grabber;
MorphologicalOp op;
int currFilter = 0;

As usual, the application uses the default initrunmain combination. In init, we first initialize the GenericGrabber instance by linking it to our program argument “-input”. Now, two seperate GUI instances are to be created: our main GUI called gui and a simple extra GUI for the optional visualization of input images, called input. The main HSplit GUI is filled with two sub-components, an Image for the visualization of the result images and an extra VBox container for the buttons, each set up with a unique handle for later access. The GUI is immediately created and shown by streaming a Show-instance at the end of the GUI definition expression.

void init(){
  grabber.init(pa("-i"));
  gui << Image().handle("result") 
      << ( VBox() 
           << Button("next filter").handle("next")
           << Button("save").handle("save")
           << Button("show src").handle("show")
           )
      << Show();

The input-GUI is only filled with a simple extra image visualization component. Here, we use Create instead of Show, which creates the GUI without directly showing it.

  input << Image().handle("image") << Create();

Finally, we register a callback to the “show src” Button. This is necessary because we cannot affect Qt-Windows and Widgets from the applications working thread. Registered callbacks are always processed in the GUI thread, where this is allowed. We simply create an anonymous member function here, that changes the input GUI’s visible-flag at each klick.

  gui["show"].registerCallback(function(input,
                      &GUI::switchVisibility));

Once, the initialization is finished, the run method is implemented. Here, we first extract two ButtonHandle instances from the applications main GUI. These are later used for the wasTriggered()-mechanism.

  static ButtonHandle next = gui["next"];
  static ButtonHandle save = gui["save"];

Now, the actual image processing loop is implemented. The next image is acquired form the GenericGrabber instance. Before the morphological operator is applied to the input image, we check whether the next filter button has been pressed since the last loop cycle.

  const ImgBase *image = grabber.grab();

  if(next.wasTriggered()){
    currFilter = (currFilter+1)%10;
    op.setOptype((MorphologicalOp::optype)currFilter);
  }

If this is true, the next valid MorphologicalOp::optype value is estimated and used to set up our operator. Now, the filter is applied and the result image can be visualized.

  const ImgBase *result = op.apply(image);
  
  gui["result"] = result;

Only if the input-GUI is visible, the source image is visualized as well. This is not completely necessary, but leads to a cheap optimization in case of the input-GUI is not visible, because the image data must not be transferred to the GUI-thread in this case.

  if(input.isVisible()){
    input["image"] = image;
  } 

Finally, we check the save-button. If this was triggered, the current result image is saved.

  if(save.wasTriggered()){
    qt::save(*result,"current-image.png");
  }   

Note

It is very important to know, that we cannot make use of a Qt-Dialog here to ask the user for a desired file-name. If a GUI-based dialog is to be used, this step has to be transferred to the GUI-thread by using a GUI callback for this button as well.

GUI-Dialogs

As motivated and discussed before, GUI-Dialogs can only be instantiated in the application’s GUI thread, but not in the working thread. However, if dialogs processed in the GUI-thread by using the GUI‘s callback mechanism, temporary values from the working thread cannot be accessed directly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <ICLQt/Common.h>
#include <ICLFilter/MorphologicalOp.h>

HSplit gui;
GUI input;
GenericGrabber grabber;
MorphologicalOp op;
int currFilter = 0;

Mutex resultMutex;
ImgBase *result = 0;

void saveImage(){
  try{
    const std::string &filename = saveFileDialog();
    Mutex::Locker lock(resultMutex);
    qt::save(*result, filename);
  }catch(...){}
}

void init(){
  grabber.init(pa("-i"));
  gui << Image().handle("result") 
      << ( VBox() 
           << Button("next filter").handle("next")
           << Button("save").handle("save")
           << Button("show src").handle("show")
           )
      << Show();

  input << Image().handle("image") << Create();

  gui["show"].registerCallback(function(input,
                      &GUI::switchVisibility));
  
  gui["save"].registerCallback(saveImage);
}

void run(){
  static ButtonHandle next = gui["next"];
  const ImgBase *image = grabber.grab();

  if(next.wasTriggered()){
    currFilter = (currFilter+1)%10;
    op.setOptype((MorphologicalOp::optype)currFilter);
  }
  
  resultMutex.lock();
  op.apply(image,&result);
  resultMutex.unlock();
  
  gui["result"] = result;

  if(input.isVisible()){
    input["image"] = image;
  } 
}
int main(int n, char **args){
   return ICLApp(n,args,"-input|-i(2)",init,run).exec();
}

As we can see, only a few changes were to be made here. First of all, we use a global result image instance here that is protected by a Mutex instance.

Mutex resultMutex;
ImgBase *result = 0;

Furthermore, we add an extra saveImage function, that is linked as a callback to the button click event. The function uses the simple to used qt::saveFileDialog() function, that simply wraps a Qt file dialog. Its call is set into a try-catch block to react to the case if the user presses the cancel or the window-close button in this dialog. If a valid image file name was provided, the result image mutex is locked (here using a scoped lock of type Mutex::Locker) and the current result image can be saved just like in the former example. Moving the scoped lock on top of the call to qt::saveFileDialog would suspend the working thread while the user selects a file name, which is usually more practical.

void saveImage(){
  try{
    const std::string &filename = saveFileDialog();
    Mutex::Locker lock(resultMutex);
    qt::save(*result, filename);
  }catch(...){}
}

In the init method, we only need to register an extra callback:

  gui["save"].registerCallback(saveImage);

In run, we no longer need to handle the save button explicitly. Instead, we have to use the global result-image, to make it accessible from the GUI-thread as well. While the new result image is created, we lock the corresponding Mutex variable in order to avoid that the image is saved while it is updated by the working thread. Without the locking mechanism, the image could either be reallocated by the morphological operator, which would lead to a possible segmentation fault in the saveImage callback function. But also if the image data is not reallocated, saveImage would not be synchronized with the working thread and therefore possibly save have-adapted images.

  resultMutex.lock();
  op.apply(image,&result);
  resultMutex.unlock();