LoginSignup
2
2

More than 5 years have passed since last update.

GTK+3で別スレッドから画面を更新する

Last updated at Posted at 2018-12-07

初めに

アドベントカレンダー8日目

C++経験は半年ですが、頑張って書こうと思います。

前の記事は@t10471さんのboost::signals2とかboost::asio::io_serviceをEOSのプログラムを見ながら理解するです。
次の記事は@niinaさんのC++しか書かずにPythonライブラリを作るです。

やること

秒数をカウントするアプリを作ります。
画面内に秒数を表示するラベルを配置し、別スレッドから1秒周期で画面の更新を行い、秒数を増やしていきます。
スレッドはGThreadを使用し、画面はgladeで作ってコード内で読み込みます。
timecounter.glade.png
微妙に切れとるやん

最初に困ったこと

私の使用しているGTK+3のバージョンは3.18.9なのですが、このバージョンだとgdk_threadが使用できません。
代わりにg_threadというものを使うのですが、調べても情報が少ない…。
とりあえずReference Manualを見ながら書いてみた。抜けあるかも…

main.cpp
int main(int argc, char* argv[]) {
    TimeCounter *counter = new TimeCounter();
    counter->run(argc, argv);
    return 0;
}
timecounter.hpp
class TimeCounter{
    public:
        void Run(int argc, char* argv[]);
        void UpdateDisplay();

        static void OnUpdateDisplay(GtkWidget* widget, gpointer data, TimeCounter* counter);
        static gpointer TimerThread(gpointer data);

    private:
        GtkWidget* my_window_;
        GtkWidget* timer_label_;
        int counter_;
        static gboolean thread_exit_;
};
timecounter.cpp
// スレッド停止用のフラグ.
gboolean TimeCounter::thread_exit_ = FALSE;

void TimeCounter::Run(int argc, char* argv[]){
    gtk_init(&argc, &argv);

    GtkBuilder *builder = gtk_builder_new();
    GError *error;

    if( !gtk_builder_add_from_file(builder, "timecounter.glade", &error)){
        printf("Gladeファイル読み込み失敗");
        return;
    }

    // ローカル変数に格納.
    my_window_ = GTK_WIDGET(gtk_builder_get_object(builder, "timecount_window"));
    timer_label_ = GTK_WIDGET(gtk_builder_get_object(builder, "timer_label"));

    // 初期値として0をセット.
    gtk_label_set_text(GTK_LABEL(timer_label), "0"); 
    counter_ = 0;

    // イベントシグナルの作成.
    g_signal_new("update_window", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST, 0,
    NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1,
    G_TYPE_POINTER);

    // イベントシグナルの紐づけ.自クラスのインスタンスを渡すようにする.
    g_signal_connect(my_window_, "update_window", G_CALLBACK(OnUpdateDisplay), this);

    // 別スレッド作成.
    GThread* sub_thread_ = g_thread_try_new("SubThread", TimerThread, my_window_, &error);

    if( error == NULL ){
        printf("スレッド起動エラー");
        return;
    }

    // 画面表示.
    gtk_widget_show(my_window_);

    // GtkBuilder破棄.
    g_object_unref(builder);

    gtk_main();

    thread_exit_ = TRUE;
    g_thread_join(sub_thread_);
}

void UpdateDisplay(){
    counter_ += 1;
    std::string str = std::to_string(counter_);
    gtk_label_set_text(GTK_LABEL(timer_label_),str.c_str());
}

// 以下static関数.

// スレッド.
gpointer TimeCounter::TimerThread(gpointer data){
    GtkWidget* window = GTK_WIDGET(data);

    // ループでひたすらシグナル送信する.
    while(true){
        if( thread_exit_ )
            break;

        // 画面に対してシグナルを送信.
        g_signal_emit_by_name(G_OBJECT(window ), "update_window",
            nullptr);

        // 1秒スリープ.
        usleep(1000000);
    }

    return nullptr;
}

// コールバック関数.
void TimeCounter::OnUpdateDisplay(GtkWidget* widget, gpointer data, TimeCounter* counter){
    counter->UpdateDisplay();
}

よし動かすぞ!
・・・・あれ、アプリ落ちたぞ・・・なんで?

調べ始めてすぐに気が付きました。
「そういえば別スレッドから画面更新かけとるやんけ!」
自分でそういうアプリ作るって言ったんだろ完全に忘れてました。
簡単に言えば、別スレッドから画面の更新を行うこと自体がNG。
メインスレッドから更新するのはOKです。

じゃあメインスレッドで更新処理させるか!
・・・どうやって?

解決編

調べても欲しい情報はあまり得られませんでした。
悩みながらReferenceを片っ端から見ていると、使えそうなものが見つかりました。

「g_idle_add_full」

GTK+3 ReferenceManualより引用

Adds a function to be called whenever there are no higher priority events pending. If the function returns FALSE it is automatically removed from the list of event sources and will not be called again.
See memory management of sources for details on how to handle the return value and memory management of data .
This internally creates a main loop source using g_idle_source_new() and attaches it to the global GMainContext using g_source_attach(), so the callback will be invoked in whichever thread is running that main context. You can do these steps manually if you need greater control or to use a custom main context.

メインコンテキストを実行しているスレッドに処理を追加して実行できるのか。
これ使えばできるかも。

早速入れてみる。

timecounter.hpp
class TimeCounter{
    public:
        void Run(int argc, char* argv[]);
        void UpdateDisplay();

        static void OnUpdateDisplay(GtkWidget* widget, gpointer data, TimeCounter* counter);
        // g_idle_add_fullで呼び出す関数.
        static int IdleUpdateDisplay(gpointer data);
        static gpointer TimerThread(gpointer data);

    private:
        GtkWidget* my_window_;
        GtkWidget* timer_label_;
        int counter_;
        static gboolean thread_exit_;
};
timecounter.cpp
// スレッド停止用のフラグ.
gboolean TimeCounter::thread_exit_ = FALSE;

void TimeCounter::Run(int argc, char* argv[]){
    gtk_init(&argc, &argv);

    GtkBuilder *builder = gtk_builder_new();
    GError *error;

    if( !gtk_builder_add_from_file(builder, "timecounter.glade", &error)){
        printf("Gladeファイル読み込み失敗");
        return;
    }

    // ローカル変数に格納.
    my_window_ = GTK_WIDGET(gtk_builder_get_object(builder, "timecount_window"));
    timer_label_ = GTK_WIDGET(gtk_builder_get_object(builder, "timer_label"));

    // 初期値として0をセット.
    gtk_label_set_text(GTK_LABEL(timer_label), "0"); 
    counter_ = 0;

    // イベントシグナルの作成.
    g_signal_new("update_window", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST, 0,
    NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1,
    G_TYPE_POINTER);

    // イベントシグナルの紐づけ.自クラスのインスタンスを渡すようにする.
    g_signal_connect(my_window_, "update_window", G_CALLBACK(OnUpdateDisplay), this);

    // 別スレッド作成.
    GThread* sub_thread_ = g_thread_try_new("SubThread", TimerThread, my_window_, &error);

    if( error == NULL ){
        printf("スレッド起動エラー");
        return;
    }

    // 画面表示.
    gtk_widget_show(my_window_);

    // GtkBuilder破棄.
    g_object_unref(builder);

    gtk_main();

    thread_exit_ = TRUE;
    g_thread_join(sub_thread_);
}

void UpdateDisplay(){
    counter_ += 1;
    std::string str = std::to_string(counter_);
    gtk_label_set_text(GTK_LABEL(timer_label_),str.c_str());
}

// 以下static関数.

// スレッド.
gpointer TimeCounter::TimerThread(gpointer data){
    GtkWidget* window = GTK_WIDGET(data);

    // ループでひたすらシグナル送信する.
    while(true){
        if( thread_exit_ )
            break;

        // 画面に対してシグナルを送信.
        g_signal_emit_by_name(G_OBJECT(window ), "update_window",
            nullptr);

        // 1秒スリープ.
        usleep(1000000);
    }

    return nullptr;
}

// コールバック関数.
void TimeCounter::OnUpdateDisplay(GtkWidget* widget, gpointer data, TimeCounter* counter){
    // メインスレッドに処理を追加.
    g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, (sr::int32 (*)(void *))IdleUpdateDisplay, counter, NULL);
}

int TimeCounter::IdleUpdateDisplay(gpointer data){
    ((TimeCounter*)data)->UpdateDisplay();
    return 0;
}

実行・・・ 動いた!
1時間くらい放置してても落ちないやったぜ。

これでアプリケーションが落ちたりせずに秒数のカウントができるようになりました。

まとめ

・別スレッドから画面更新はしない。
・g_idle_add(今回はfullを使用)でメインスレッドに処理を任せられる

別件
・別スレッドから画面いじるサンプルとか見つからなかったのはなんで?
 →検索の仕方悪かったんだと思う。

感想

書いてから無理やりなやり方では?と感じております。
もっといいやり方が見つかったら追記・修正します。

動いてよかった

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2