c++gtkgtkmmlong-running-processesgtkmm3

Animated gif image isn't being animated in my modeless Gtk::Dialog


My goal is to show a brief "Please Wait..." dialog with an animated gif (spinner) in a Gtk::Dialog.

My problem is that when I do not use Gtk:Dialog::run(), the gif won't be animated, and when I do use the Gtk:Dialog::run() method it completely blocks my running code afterwards. And since I don't have any buttons in my dialog it would hang there indefinitely. Is there a way around that? I have had no success in getting the animated gif to work in a non-modal dialog, i.e without using the run() method.

I'm using gtkmm 3.0

Compile with : g++ examplewindow.cc main.cc -o main `pkg-config gtkmm-3.0 --cflags --libs`

main.cc

#include "examplewindow.h"
#include <gtkmm/application.h>
#include <iostream>

int main(int argc, char *argv[])
{
  auto app = Gtk::Application::create("org.gtkmm.example");

  ExampleWindow window;    

  //Shows the window and returns when it is closed.
  //return app->make_window_and_run<ExampleWindow>(argc, argv);
  return app->run(window);
}

examplewindow.h

#ifndef GTKMM_EXAMPLEWINDOW_H
#define GTKMM_EXAMPLEWINDOW_H

#include <gtkmm.h>

class ExampleWindow : public Gtk::Window
{
public:
  ExampleWindow();
  virtual ~ExampleWindow();

protected:
  //Signal handlers:
  void on_button_clicked();

  //Child widgets:
  Gtk::Box m_VBox;
  Gtk::Box m_ButtonBox;
  Gtk::Button m_Button;
};

#endif //GTKMM_EXAMPLEWINDOW_H

examplewindow.cc

#include "examplewindow.h"
#include <iostream>

ExampleWindow::ExampleWindow()
    : m_VBox(Gtk::Orientation::ORIENTATION_VERTICAL),
      m_ButtonBox(Gtk::Orientation::ORIENTATION_VERTICAL),
      m_Button("Show Dialog")
{
  set_title("Test animated gif");
  set_default_size(800, 600);

  add(m_VBox);

  m_VBox.pack_start(m_ButtonBox);
  m_ButtonBox.pack_start(m_Button);
  m_Button.set_hexpand(true);
  m_Button.set_halign(Gtk::Align::ALIGN_CENTER);
  m_Button.set_valign(Gtk::Align::ALIGN_CENTER);
  m_Button.grab_focus();
  m_Button.signal_clicked().connect(sigc::mem_fun(*this, &ExampleWindow::on_button_clicked));

  show_all_children();
}

ExampleWindow::~ExampleWindow()
{
}

void ExampleWindow::on_button_clicked()
{
  Gtk::Dialog m_Dialog;
  m_Dialog.set_transient_for(*this);
  m_Dialog.set_size_request(200, 200);
  m_Dialog.set_decorated(false);
  Gtk::Image imageLoading = Gtk::Image();
  imageLoading.property_pixbuf_animation() = Gdk::PixbufAnimation::create_from_file("gtkmm_logo.gif");
  m_Dialog.get_vbox()->pack_start(imageLoading);
  m_Dialog.show_all();

  m_Dialog.run();

  /******** This, below, never gets executed as run() is blocking the program...********/
  
  // Dummy "long" operation
  for (int i = 0; i <= 2010101010; i++)
  {
    if (i == 2010101010)
      std::cout << "Done" << std::endl;
  }

  m_Dialog.response(Gtk::RESPONSE_ACCEPT);
  m_Dialog.hide();
}

Solution

  • Let us look at the original problem. You created a dialog, called show() on it, did some long-running process, then closed the dialog. The process worked, but your program froze during the processing. Why is that?

    A graphical interface works by processing messages (events). Some events run off a timer, such as the ones that tell an animation to go to the next frame. Some are generated as needed, such as the ones that tell an image to draw the current frame. These events need to be both triggered and processed to be effective. You triggered the appropriate events with your call to show_all(), but you did not give your program a chance to handle those events.

    You used a button click to start your long-running process. That click is an event that was handled by your main event handling loop. That loop then waited for the click to be fully handled before moving on to the next event. However, you have your long-running process in the handler. The main event loop had to wait for that process to finish before it could handle new events, such as the ones to show and animate your image. You never gave your dialog a chance to do its job before you destroyed it.

    Calling the dialog's run() method partially fixed the situation by starting a new event loop for the dialog. So even though the main event loop was still blocked by your click handler, new events could be handled. The dialog's event loop received the events required to show an animation, hence your program was again responsive. Unfortunately, run() blocked your long-running process, so we're not really any better off.


    The simplest fix is to no longer completely block your main event loop. You could have your long-running process periodically allow events to be processed via Gtk::Main::iteration(). This function invokes an iteration of the main event loop, allowing your program to stay responsive. Pass it a false argument so that it only processes events if there are some to process (rather than waiting for an event to occur).

        for (unsigned long i = 0; i <= 2010101010; i++)
        {
            if (i == 2010101010)
              std::cout << "Done" << std::endl;
    
            // Periodically process events
            if ( i % 10000 == 0 )                    // <---- after some suitable amount of work
                if ( !Gtk::Main::iteration(false) )  // <---- allow events to be processed
                    // Abort the work.
                    break;
        }
    

    The return value is supposed to tell you if you should quit or not, but I didn't get this working in my test (and the return value seemed to have the opposite meaning compared to the documentation). Maybe the dialog itself was keeping the app alive? Eh, that can be the next question, once this part is working.


    Other approaches would move your long-running process out of the click handler. If you let the click handler end quickly, the main event loop can do its job without the extra prompting from you. However, this requires a few adjustments so that the Gtk::Dialog outlives the call to on_button_clicked(). That's a bit of refactoring, but it might be worth the time. I'll present two options (without code).

    1. You could have your work operate on multiple timeout signals. Divide your long-running process into smaller chunks, each chunk suitably sized for a callback. (How big is that? Not sure. For now, let's say at most a few milliseconds.) Have the button click event start the first timeout signal with a priority that allows the GUI to update. (As I recall, PRIORITY_DEFAULT_IDLE should work.) For the interval, I would try 0 if that does not overly confuse Gtk+. (I have not tried it, but it seems plausible.) If the 0-interval works, it might be wise to use connect_once() instead of connect(), and have each chunk schedule the next with another timeout. The final chunk would be responsible for closing the dialog.

    2. You could move your long-running process to another thread. Multi-threaded programming has its own set of problems and sometimes a lot of setup, but this is something it is well-suited for. If your long-running process is in a different thread than your main event loop, the operating system becomes responsible for making sure each thread gets some CPU time. Your long-running process can chug away, and the main event loop would simultaneously be able to process events with no special intervention from you.


    Final notes:
    If your dialog is for one-way communication to the user, it seems more like a monologue than a dialogue. Excuse me, more like an ordinary window than a dialog. Also, I'll make sure you are aware of Gtk::ProgressBar, which "is typically used to display the progress of a long running operation." Just an option; preferring your image is understandable.