コンテンツにスキップ

Top

GTK+3入門 (C++版)

gtkmmを使った、C++版のGTKの入門。

わいはC言語版の情報がほしいんや!という人は以下のページへ。
GTK+3入門(C言語版)

gtkmm のインストール

gtk+はすでにインストール済みと思うが、ここで以下を追加する。

1
sudo apt install -y libgtkmm-3.0-dev

そう、gtkmm。

なに?このmmって?と思ったかもしれないが、これはC++版のgtkなのだ。

C言語でGTKを使いたいならlibgtkを、C++で使いたいならlibgtkmmを入れる必要がある。

なんでgtkmm?gtkppとかのほうがわかりやすいのに、というのはみんな思ってるけど言っちゃだめなやつ。

とりあえずウィンドウの表示

難しく考えずにソース見れ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 240);
    return app->run(sample_window);
}

コンパイルはgtk+-3.0をgtkmm-3.0に変更してとうぜんg++で。

1
2
$ g++ sample1.cpp `pkg-config --cflags --libs gtkmm-3.0` -o sample1
./sample1

実行結果
タイトル

フルスクリーン

次はフルスクリーンにしてみる。
フルスクリーンにするには、 fullscreen() という関数を呼ぶだけ。これは Gtk::Window で用意されているメソッド。

ソースコード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    fullscreen();
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(0, 0); 
    return app->run(sample_window);
}

実行すると画面いっぱいが真っ白になるが終了させる方法はないので、Alt+Tabでコンソールに戻してCtrl+Cで殺して。

ラベル

ラベルを表示する。
ラベルは Gtk::Label クラスというクラスになっているので、そのインスタンスを作成してウィンドウにaddする形になる。

ソースコード

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

private:
    Gtk::Label m_label;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // ラベルの文字を設定して、表示する
    m_label.set_text("aaa");
    m_label.show();

    // Windowにラベルを追加
    add(m_label);
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 240);
    return app->run(sample_window);
}

実行結果
タイトル

文字を右寄せしたい!

以下のような感じで、右寄せができる。set_size_requestまでしているのはデフォルトでラベルが文字幅になるので右寄せにならないから。

1
2
    m_label.set_alignment(Gtk::ALIGN_END, Gtk::ALIGN_CENTER);
    m_label.set_size_request (500, 50);

set_alignment の第一引数が水平位置、第二引数が垂直位置なので、右寄せの高さ真ん中になる。
他の変数名はstartやbottomでぐぐって。

画像

画像を表示する。画像は Gtk::Image クラス。

ソースコード

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

private:
    Gtk::Image m_image;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // 画像ファイルを読み込んで表示
    m_image.set("./couple.bmp");
    m_image.show();

    // ウィンドウに画像を追加
    add(m_image);
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(256, 256);
    return app->run(sample_window);
}

実行結果
タイトル

画像のサイズが変更できない!

Gtk::Imageに set_size_request をしても画像サイズは変更されない!!
(ファイルサイズのまま)

このような場合、画像ファイルをいったんPixbufにして、それをサイズ変更してからGtk::Imageにセットすればサイズは変更される。

1
2
3
4
5
6
7
8
9
    m_image.set("./couple.bmp");
    m_image.set_size_request(128, 128); // これだと描画サイズが変更しない!!



    Glib::RefPtr<Gdk::Pixbuf> pixbuf =
        Gdk::Pixbuf::create_from_file("./couple.bmp");
    pixbuf = pixbuf->scale_simple(128, 128, Gdk::INTERP_BILINEAR);
    m_image.set(pixbuf); // サイズがちゃんと変更された!!(まぁ当然といえば当然

みたいに。
これで画像のサイズが変わったはず。

なおGtk::Imageはsetでファイルパスを指定することもできるし、Pixbuf(のポインタ)を指定することもできる。

黒一色とか白一色とかの四角とかをつくりたい!

画像ファイル読み込みではなく、単色の■をただ作りたい場合。

以下のようにすればできる。

1
2
3
Glib::RefPtr<Gdk::Pixbuf> pixbuf =
    Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB, false, 8, 160, 120);
pixbuf->fill(0x000000ff); // rgba

createの第一引数は現状 Gdk::COLORSPACE_RGB しかない。
第二引数はアルファを持つか否か。持たないならfalse、持つならtrueにする。
trueにしとくと、次の行のfillのときに設定したアルファ値が有効になる。
第三引数は8固定、第四、第五は幅と高さ。

なお、createで作った場合、値は不定。今回のようにfillとかで初期化しないと中に何が入ってるかわからん。
(パッと見、黒になるからわかりにくい。なんかデータ入れてみて、別のところでなんかcreateとかしてみたらゴミが入っているのがわかるはず)

細かいことはマニュアルみれ。

ボタン

ボタンを表示する。ボタンは Gtk::Button クラス。
ボタンを押したら終了するようにする。
signal_clicked に引数を渡せれない。
けどコールバックされる関数もクラス内のメソッドなので、引数で渡さなくてもメンバ変数でやり取り可能。

ソースコード

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

    void callback_button();

private:
    Gtk::Button m_button;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // ボタンの文字を設定して、表示する
    m_button.set_label("shutdown");
    m_button.show();

    // ボタンが押されたときに呼び出されるコールバック関数との紐付けを行う
    m_button.signal_clicked().connect(sigc::mem_fun(*this, &SampleWindow::callback_button));

    // Windowにボタンを追加
    add(m_button);
}

void SampleWindow::callback_button()
{
    close();
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 240);
    return app->run(sample_window);
}

実行結果
タイトル

ボックスを使ってラベルと画像とボタンをまとめて表示する。

いままでの方法だと一度にラベルと画像とボタンを表示することができない。
ウィンドウに追加できるウィジェットは1つだからだ。
なので、複数のウィジェットをまとめるボックスというのを使って、3つのウィジェットをまとめて表示できるようにする。

ボックスのクラスは、Gtk::Box 。
デフォルトが水平(Gtk::ORIENTATION_HORIZONTAL)なのでこのサンプルでは垂直(Gtk::ORIENTATION_VERTICAL)に変更している。

ソースコード

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

    void callback_button();

private:
    Gtk::Label  m_label;
    Gtk::Image  m_image;
    Gtk::Button m_button;
    Gtk::Box    m_box;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // ラベルと画像とボタンを作成
    m_label.set_text("aaa");
    m_label.show();
    m_image.set("./couple.bmp");
    m_image.show();
    m_button.set_label("shutdown");
    m_button.show();
    m_button.signal_clicked().connect(sigc::mem_fun(*this, &SampleWindow::callback_button));

    // ボックスに詰め込む
    m_box.set_orientation(Gtk::ORIENTATION_VERTICAL); // デフォルトが水平(Gtk::ORIENTATION_HORIZONTAL)
    m_box.pack_start(m_label,  Gtk::PACK_SHRINK);
    m_box.pack_start(m_image,  Gtk::PACK_SHRINK);
    m_box.pack_start(m_button, Gtk::PACK_SHRINK);
    m_box.show();

    // Windowにボックスを追加
    add(m_box);
}

void SampleWindow::callback_button()
{
    close();
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 240);
    return app->run(sample_window);
}

実行結果
タイトル

座標を指定してウィジェットを配置する

box を pack する方法だとちょっと試すのには便利だけど、ちゃんとしたアプリを作るのは大変。

Gtk::Fixed を使えば座標指定でウィジェットを配置することができる。

ソースコード

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

private:
    Gtk::Label m_label1;
    Gtk::Label m_label2;
    Gtk::Label m_label3;
    Gtk::Fixed m_fixed;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // ラベルの文字を設定して、表示する
    m_label1.set_text("aaa");
    m_label2.set_text("bbb");
    m_label3.set_text("ccc");
    m_label1.show();
    m_label2.show();
    m_label3.show();

    // ラベルを座標指定で表示する
    m_fixed.put(m_label1,  20,  20);
    m_fixed.put(m_label2, 150, 160);
    m_fixed.put(m_label3, 280, 280);
    m_fixed.show(); 

    // WindowにFixedを追加
    add(m_fixed);
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 320);
    return app->run(sample_window);
}

実行結果
タイトル

ウィジェットのサイズを変更する

ウィジェットのサイズを変更するには、set_size_request を使う。
が、これが有効になるのは Gtk::Fixed のときで、Gtk::Boxにpackした場合はうまく行かないので注意すること。

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

    void callback_button();

private:
    Gtk::Button m_button1;
    Gtk::Button m_button2;
    Gtk::Button m_button3;
    Gtk::Fixed  m_fixed;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // ボタンの文字を設定して、表示する
    m_button1.set_label("150x100");
    m_button2.set_label("100x150");
    m_button3.set_label("100x100");
    m_button1.set_size_request(150,100);
    m_button2.set_size_request(100,150);
    m_button3.set_size_request(100,100);
    m_button1.show();
    m_button2.show();
    m_button3.show();

    // ボタンが押されたときに呼び出されるコールバック関数との紐付けを行う
    m_button1.signal_clicked().connect(sigc::mem_fun(*this, &SampleWindow::callback_button));
    m_button2.signal_clicked().connect(sigc::mem_fun(*this, &SampleWindow::callback_button));
    m_button3.signal_clicked().connect(sigc::mem_fun(*this, &SampleWindow::callback_button));

    // Fixedに追加(boxにpackで追加しても反映されない)
    m_fixed.put(m_button1, 10,  10);
    m_fixed.put(m_button2, 10, 120);
    m_fixed.put(m_button3, 10, 280);
    m_fixed.show(); 

    // Windowにボタンを追加
    add(m_fixed);
}

void SampleWindow::callback_button()
{
    close();
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(170, 390);
    return app->run(sample_window);
}

実行結果
タイトル

ウィジェットの表示/非表示

ウィジェットを表示したり非表示したりする。

まずはウィジェットを pack した場合、非表示したらどうなるかを見てみる。

ソースコード

 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 <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

    void callback_button();

private:
    bool        m_show_flag;
    Gtk::Image  m_image;
    Gtk::Button m_button;
    Gtk::Box    m_box;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    m_show_flag = true;

    // ラベルと画像とボタンを作成
    m_image.set("./couple.bmp");
    m_image.show();
    m_button.set_label("hide");
    m_button.show();
    m_button.signal_clicked().connect(sigc::mem_fun(*this, &SampleWindow::callback_button));

    // ボックスに詰め込む
    m_box.set_orientation(Gtk::ORIENTATION_VERTICAL); // デフォルトが水平(Gtk::ORIENTATION_HORIZONTAL)
    m_box.pack_start(m_image,  Gtk::PACK_SHRINK);
    m_box.pack_start(m_button, Gtk::PACK_SHRINK);
    m_box.show();

    // Windowにボックスを追加
    add(m_box);
}

void SampleWindow::callback_button()
{
    if (m_show_flag) {
        m_show_flag = false;
        m_image.hide();
        m_button.set_label("show");
    } else {
        m_show_flag = true;
        m_image.show();
        m_button.set_label("hide");
    }   
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 240);
    return app->run(sample_window);
}

実行結果
タイトル タイトル

みての通り、hideした結果、詰められている。

それに対し、Gtk::Fixedで座標を指定している場合、

ソースコード

 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 <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

    void callback_button();

private:
    bool        m_show_flag;
    Gtk::Image  m_image;
    Gtk::Button m_button;
    Gtk::Fixed  m_fixed;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    m_show_flag = true;

    // 画像とボタンを作成
    m_image.set("./couple.bmp");
    m_image.show();
    m_button.set_label("hide");
    m_button.set_size_request(256,20);
    m_button.show();
    m_button.signal_clicked().connect(sigc::mem_fun(*this, &SampleWindow::callback_button));

    // 固定座標配置する
    m_fixed.put(m_image, 0,0);
    m_fixed.put(m_button, 0, 256);
    m_fixed.show();

    // WindowにFixedを追加
    add(m_fixed);
}

void SampleWindow::callback_button()
{
    if (m_show_flag) {
        m_show_flag = false;
        m_image.hide();
        m_button.set_label("show");
    } else {
        m_show_flag = true;
        m_image.show();
        m_button.set_label("hide");
    }   
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(256, 276);
    return app->run(sample_window);
}

実行結果
タイトル タイトル

このように、Gtk::Fixedの場合は当然だが座標を固定しているのでhideをしても詰められない。
どちらが良いか、というよりも詰めたい場合はpack(かhide時に詰めたかのように座標も変更)、詰めたくない場合はfixedを使うといった使い分けをすればいい。

スライダー

スライダーを表示する。スライダーと言っているが、Gtkの世界だとGtk::Scale。

ソースコード

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

    void callback_scale();

private:
    Gtk::Scale m_scale;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // スケールの範囲を設定、表示する
    m_scale.set_range(0,256);
    m_scale.set_value(128);
    m_scale.set_digits(0); // 小数点以下の数
    m_scale.set_increments(1,1); // スライダーを動かしたときに増減する量。第一引数で指定した値で増減する。第二引数はPage Up/Page Downボタンを押下したときの増減数。  
    m_scale.set_draw_value(true); // trueにすると現在の値が表示される。表示される場所はposで設定する
    m_scale.set_value_pos(Gtk::POS_BOTTOM);

    m_scale.show();

    // スケールのスライダーが動かされたときに呼び出されるコールバック関数との紐付けを行う
    m_scale.signal_value_changed().connect(sigc::mem_fun(*this, &SampleWindow::callback_scale));

    // Windowにボタンを追加
    add(m_scale);
}

void SampleWindow::callback_scale()
{
    printf("%d\n", (int)m_scale.get_value());
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 100);
    return app->run(sample_window);
}

実行結果
タイトル

スライダーを動かすとコールバック関数が呼ばれる

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ ./sample_scale 
129
130
131
132
133
134
136
137
140

チェックボックス

チェックボックスを表示する。チェックボックスと行っているが、gtkだと Gtk::CheckButton。

ソースコード

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

    void callback_check();

private:
    Gtk::CheckButton m_check;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // チェックボックスを表示
    m_check.set_label("チェックボックス");
    m_check.set_active(true);
    m_check.show();

    // チェックボックスのチェック状態がトグルされるたびに呼び出されるコールバック関数との紐付けを行う
    m_check.signal_toggled().connect(sigc::mem_fun(*this, &SampleWindow::callback_check));

    // Windowにボタンを追加
    add(m_check);
}

void SampleWindow::callback_check()
{
    printf("%d\n", (int)m_check.get_active());
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 100);
    return app->run(sample_window);
}

実行結果
タイトル

ラジオボタン

ラジオボタンを記述する。
ラジオボタンは Gtk::radioButtonである。
ラジオボタンは他のボタンと連携しないといけないので、最初のラジオボタンを他のボタンとグループ化しないといけない。
また、ラジオボタンはその性質上、どれかをONにした場合、もともとONだったものがOFFになるため、コールバックが2回呼ばれてしまう。
のでコールバック関数を分けるか、呼び出されたコールバック関数内でどのradioボタンが呼び出されたのかをチェックしないといけなくなる。

ソースコード

 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
61
62
63
64
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

    void callback_radio(Gtk::RadioButton& button);

private:
    Gtk::RadioButton m_radio[3];
    Gtk::Box         m_box;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // チェックボックスを表示
    m_radio[0].set_label("ラジオボタン1");
    m_radio[1].set_label("ラジオボタン2");
    m_radio[2].set_label("ラジオボタン3");
    m_radio[0].show();
    m_radio[1].show();
    m_radio[2].show();
    m_radio[0].set_active(true);

    // ラジオボタン1のグループに、2と3を追加する
    Gtk::RadioButton::Group group = m_radio[0].get_group();
    m_radio[1].set_group(group);
    m_radio[2].set_group(group);

    // チェックボックスのチェック状態がトグルされるたびに呼び出されるコールバック関数との紐付けを行う
    // (ラジオボタンはどれからオンになるときどれかがオフになるので2回呼び出される。受け取り側で判定する
    m_radio[0].signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SampleWindow::callback_radio), sigc::ref(m_radio[0])));
    m_radio[1].signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SampleWindow::callback_radio), sigc::ref(m_radio[1])));
    m_radio[2].signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SampleWindow::callback_radio), sigc::ref(m_radio[2])));

    // ラジオボタンをまとめるボックスを作る
    m_box.set_orientation(Gtk::ORIENTATION_VERTICAL); // デフォルトが水平(Gtk::ORIENTATION_HORIZONTAL)
    m_box.pack_start(m_radio[0], Gtk::PACK_SHRINK);
    m_box.pack_start(m_radio[1], Gtk::PACK_SHRINK);
    m_box.pack_start(m_radio[2], Gtk::PACK_SHRINK);
    m_box.show();

    // Windowにボックスを追加
    add(m_box);
}

void SampleWindow::callback_radio(Gtk::RadioButton& button)
{
    if (button.get_active()) {
        printf("%s\n", button.get_label().c_str());
    }
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 100);
    return app->run(sample_window);
}

実行結果
タイトル

プログレスバー

Gtk::ProgressBar を使う。
set_fraction()で値を設定。0から100じゃなく、0.0から1.0で指定する。

何かの関数のなかでループとかをしながらプログレスバーを更新したい場合、
スレッドとかで処理してGtkのメインループでは値のみを定期的に拾ってプログレスバーを更新するのが正しいけど、
スレッドとか面倒だし、いっきにやりたい!という感じであれば、

gtk_main_iteration()

を呼び出せば即時に反映される。
tkinterで言うところの self.root.update_idletasks() と同じ。
ただ、当然だがそれだけ長い関数だと他の操作もできないし、あまり好ましくないので面倒でも別スレッドにすべきと思うが。

ダイアログ

ダイアログの表示。
いろんな引数とかがあるのでそれはドキュメントみて設定して。
基本的にはButtonのコールバックでダイアログを生成して云々、といった作りになると思う。
モーダルにするかとかもダイアログの引数で決めることができる。

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // ダイアログ表示
    Gtk::MessageDialog Hello("Hello world!", false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK);
    Hello.set_secondary_text("This is an example dialog.");
    Hello.run();

    // 終了ダイアログ表示
    Gtk::MessageDialog dialog(*this, "確認", Gtk::DIALOG_MODAL | Gtk::DIALOG_DESTROY_WITH_PARENT, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO);
    dialog.set_secondary_text("終了しますか?");
    int response = dialog.run();
    if (response == Gtk::RESPONSE_YES) {
        // 終了
        close();
    }
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 240);
    return app->run(sample_window);
}

実行結果
タイトル

■メッセージの種類
Gtk::MESSAGE_INFO
Gtk::MESSAGE_WARNING
Gtk::MESSAGE_QUESTION
Gtk::MESSAGE_ERROR
Gtk::MESSAGE_OTHER

■ボタンの種類
Gtk::BUTTONS_NONE
Gtk::BUTTONS_OK
Gtk::BUTTONS_CLOSE
Gtk::BUTTONS_CANCEL
Gtk::BUTTONS_YES_NO
Gtk::BUTTONS_OK_CANCEL

■戻り値の種類
Gtk::RESPONSE_NONE
Gtk::RESPONSE_REJECT
Gtk::RESPONSE_ACCEPT
Gtk::RESPONSE_DELETE_EVENT
Gtk::RESPONSE_OK
Gtk::RESPONSE_CANCEL
Gtk::RESPONSE_CLOSE
Gtk::RESPONSE_YES
Gtk::RESPONSE_NO
Gtk::RESPONSE_APPLY
Gtk::RESPONSE_HELP

コードで文字の色やフォントやサイズを変更する

override_colorで色を、override_fontでフォントを変更できる。

1
2
    m_label.override_color(Gdk::RGBA("red"), Gtk::STATE_FLAG_NORMAL);
    m_label.override_font(Pango::FontDescription("IPA Pゴシック 28"));

みたいな感じで。

cssで文字の色やフォントやサイズを変更する

Gtk::StyleContext と Gtk::StyleProvider を用いてcssファイルを読み込むことで設定する。

以下みたいな感じでmainに入れたらstyle.cssの内容が反映される。

1
2
3
4
5
6
7
    Glib::RefPtr<Gtk::CssProvider> provider = Gtk::CssProvider::create();
    provider->load_from_path("style.css");
    Glib::RefPtr<Gtk::StyleContext> context = Gtk::StyleContext::create();
    Glib::RefPtr<Gdk::Screen> screen = Gdk::Screen::get_default();
    context->add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);

    return app->run(sample_window);

style.css

1
2
3
4
5
6
7
8
window {
    background-color: rgb(0,255,0);
}
label {
    color: red;
    font-size: 50px;
    font-family:'メイリオ', 'Meiryo', sans-serif;
}

背景がグリーンになって、ラベルが赤になったと思います。

キーボード入力を取得する

キーボードを拾うためには、 add_events(Gdk::KEY_PRESS_MASK); を呼ばないといけない。
これを設定すると、 on_key_press_event(GdkEventKey* event) がコールバックで呼ばれる。
この関数はgtk::Windowにそもそも入っているabstract関数。
キーボードとかは例えばaはGDK_KEY_aだったりしてわかりやすいが、CtrlキーとかはGDK_CONTROL_MASKとかを使ったり、なんか面倒。

ソースコード

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

protected:
    bool on_key_press_event(GdkEventKey* event) override;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // キーのイベントを受け取るための設定をする
    add_events(Gdk::KEY_PRESS_MASK);
}

bool SampleWindow::on_key_press_event(GdkEventKey* event) {
    switch (event->keyval) {
        // Ctrl + c
        case GDK_KEY_c:
            if ((event->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) {
                get_application()->quit();
            }
            return true;

        // a
        case GDK_KEY_a:
            printf("press a!\n");
            return true;

        // escape key
        case GDK_KEY_Escape:
            close();
            return true;
    }   
    return false;
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 240);
    return app->run(sample_window);
}

クリックされた座標を取得する

クリックされたときのイベントを拾いたい場合は、 add_events(Gdk::BUTTON_PRESS_MASK); を
リリースされたときのイベントをひいたい場合は、 add_events(Gdk::BUTTON_RELEASE_MASK); を
両方拾いたい場合は、 add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK); と記述する。
(GtkじゃなくGdkなのに注意!)

んで、クリックされたときのコールバックを設定するための関数名は、 signal_button_release_event() で、
リリースされたときのコールバックを設定するための関数は、 signal_button_release_event() 。

以下はリリースされたときにコールバックが呼ばれて座標が標準出力されるサンプル。

ソースコード

 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
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int width, int height);
    virtual ~SampleWindow() = default;

protected:
    bool on_release(GdkEventButton* event);
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // マウスのクリック(リリース)のイベントを受け取るための設定をする
    // (BUTTON_PRESS_MASKは押したときになるので押したときに実行したいならこっち。その場合関数はsignal_button_press_eventになる)
    add_events(Gdk::BUTTON_RELEASE_MASK);
    signal_button_release_event().connect(sigc::mem_fun(*this, &SampleWindow::on_release));
}

bool SampleWindow::on_release(GdkEventButton* event) {
    printf("(%d,%d)\n", (int)event->x, (int)event->y);
    return true;
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 240);
    return app->run(sample_window);
}

実行して出てきたウィンドウをクリックすると座標が出力される。

ウィンドウじゃなくGtk::Image上の座標がほしい!

上記の方法だとWindow全体における座標が取得できるわけだが、そうじゃなく、一部の範囲、例えばGtk::Imageで表示している範囲上における座標がほしい!という要求もあると思う。

が、Gtk::Imageはクリックのイベントを拾えるわけではないのでそのままではできない。

ので、イベントをひろう Gtk::EventBox に Gtk::Imageのウィジェットをaddしてあげればイベントを拾えるようになる。

1
2
3
4
5
    m_event_box; // Gtk::EventBoxのメンバ変数
    m_event_box.set_events(Gdk::BUTTON_RELEASE_MASK); // ボタンリリースイベントをセット
    m_event_box.signal_button_release_event().connect(sigc::mem_fun(*this, &MainWindow::cb_button_release)); // コールバック関数をセット
    m_event_box.add(m_image); // Gtk::Imageのメンバ変数を追加
    m_event_box.show();

これにより、本来であればイベントを拾うことのできない m_image がイベントを拾えるようになる。

画面遷移

画面遷移は実際には複数の画面を生成しておき、それらを表示、非表示することによってあたかも画面遷移したかのように見せかけることで実現する。

  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
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
#include <gtkmm.h>

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int, int);
    virtual ~SampleWindow() = default;

    void callback_button_a();
    void callback_button_b();

private:
    // 画面A
    void _create_a();
    Gtk::Label  m_label_a;
    Gtk::Image  m_image_a;
    Gtk::Button m_button_a;
    Gtk::Box    m_box_a;

    // 画面B
    void _create_b();
    Gtk::Label  m_label_b;
    Gtk::Image  m_image_b;
    Gtk::Button m_button_b;
    Gtk::Box    m_box_b;

    Gtk::Fixed m_fixed;
};

SampleWindow::SampleWindow(int width, int height)
{
    set_title("タイトル");
    set_default_size(width, height);

    // 画面A作成
    _create_a();

    // 画面B作成
    _create_b();

    // 画面A,Bを同じ座標で配置
    m_fixed.put(m_box_a, 0, 0); 
    m_fixed.put(m_box_b, 0, 0); 
    m_fixed.show(); 

    // 初期値としてBを隠す
    m_box_b.hide();

    // windowに追加
    add(m_fixed);
}

// 画面A作成
void SampleWindow::_create_a() {
    m_label_a.set_text("画面A");
    m_label_a.show();
    m_image_a.set("./couple.bmp");
    m_image_a.show();
    m_button_a.set_label("画面Bへ");
    m_button_a.show();
    m_button_a.signal_clicked().connect(sigc::mem_fun(*this, &SampleWindow::callback_button_a));
    m_box_a.set_orientation(Gtk::ORIENTATION_VERTICAL);
    m_box_a.pack_start(m_label_a,  Gtk::PACK_SHRINK);
    m_box_a.pack_start(m_image_a,  Gtk::PACK_SHRINK);
    m_box_a.pack_start(m_button_a, Gtk::PACK_SHRINK);
    m_box_a.show();
}

// 画面B作成
void SampleWindow::_create_b() {
    m_label_b.set_text("画面B");
    m_label_b.show();
    m_image_b.set("./girl.bmp");
    m_image_b.show();
    m_button_b.set_label("画面Aへ");
    m_button_b.show();
    m_button_b.signal_clicked().connect(sigc::mem_fun(*this, &SampleWindow::callback_button_b));
    m_box_b.set_orientation(Gtk::ORIENTATION_VERTICAL);
    m_box_b.pack_start(m_label_b,  Gtk::PACK_SHRINK);
    m_box_b.pack_start(m_image_b,  Gtk::PACK_SHRINK);
    m_box_b.pack_start(m_button_b, Gtk::PACK_SHRINK);
    //m_box_b.show();
    m_box_b.hide();
}

// 画面Bを表示
void SampleWindow::callback_button_a() {
    m_box_a.hide();
    m_box_b.show();
}

// 画面Aを表示
void SampleWindow::callback_button_b() {
    m_box_b.hide();
    m_box_a.show();
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(256, 276);
    return app->run(sample_window);
}

実行結果
タイトル タイトル

ボタンを押したら画面が切り替わるよ!

どの画面が現在表示されているか知るには

show()、hide()を駆使して画面遷移をするプログラムを作成していると、今表示されている画面はどれ?というのをプログラム内から知りたくなる。
知るのは容易で、 get_visible() という関数を使えば良い。

上記の例だと、 画面Aが表示されているときに m_box_a.get_visible() を呼び出すと true が返却され、 m_box_b.get_visible() を呼び出すと false が返却される。
画面Bが表示されているときは逆にm_box_b.get_visible() が true になる。

cv::MatのデータをGtkのイメージとして表示する

https://github.com/raspberry-cpp-tutorials/gtk-opencv-simple
から必要な部分だけを抜粋して実装。

ソース見れば何してるかはわかるので、ソースコード。

  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
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <gtkmm.h>
#include "opencv2/opencv.hpp"

class OpenCVDrawingArea : public Gtk::DrawingArea {
public:
    OpenCVDrawingArea();
    virtual ~OpenCVDrawingArea();

protected:
    bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
    void on_size_allocate(Gtk::Allocation& allocation) override;
    bool update();

private:
    sigc::connection m_conn;
    cv::VideoCapture m_capture;
    cv::Mat m_original_image;
    cv::Mat m_resize_image;
    int m_width;
    int m_height;
};

OpenCVDrawingArea::OpenCVDrawingArea() {
    m_width = 0;
    m_height = 0;
    m_capture.open(0);
    if (!m_capture.isOpened()){
        printf("error\n");
        return;
    }   

    // 100msecごとに画面を更新するシグナルを設定
    m_conn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &OpenCVDrawingArea::update), 100);
}

OpenCVDrawingArea::~OpenCVDrawingArea() {
    // シグナルを終了
    m_conn.disconnect();
}

// 100msecごとに呼びだされる関数
bool OpenCVDrawingArea::update() {
    auto window = get_window();
    if (window) {
        Gdk::Rectangle r(0, 0, m_width, m_height);
        window->invalidate_rect(r, false);
    }   
    return true;
}

void OpenCVDrawingArea::on_size_allocate(Gtk::Allocation& allocation) {
    DrawingArea::on_size_allocate(allocation);
    m_width  = allocation.get_width();
    m_height = allocation.get_height();
}

bool OpenCVDrawingArea::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
    // Windowができてないときは何もしない
    if (m_width == 0 || m_height == 0) {
        return true;
    }

    // OpenCVの機能を使ってキャプチャする
    m_capture.read(m_original_image);

    // OpenCVはBGRなのでRGBに変換する
    cv::cvtColor(m_original_image, m_original_image, cv::COLOR_BGR2RGB);

    // リサイズ
    resize(m_original_image, m_resize_image, cv::Size(m_width, m_height), 0, 0, cv::INTER_LINEAR);

    // cv::Mat形式のデータを、GTKで使うGdk::Pixbuf形式に変換する
    Glib::RefPtr<Gdk::Pixbuf> pixbuf =
        Gdk::Pixbuf::create_from_data(
            (guint8*)m_resize_image.data,
            Gdk::COLORSPACE_RGB,
            false,
            8,
            m_resize_image.cols,
            m_resize_image.rows,
            (int) m_resize_image.step);

    // 表示
    Gdk::Cairo::set_source_pixbuf(cr, pixbuf);
    cr->paint();

    return true;
}

class SampleWindow : public Gtk::Window
{
public:
    SampleWindow(int, int);
    virtual ~SampleWindow() = default;

private:
    OpenCVDrawingArea m_drawing;
};

SampleWindow::SampleWindow(int width, int height) {
    set_title("タイトル");
    set_default_size(width, height);
    m_drawing.show();
    add(m_drawing);
}

int main(int argc, char* argv[])
{
    auto app = Gtk::Application::create(argc, argv, "work.jitaku.gtkmm.sample");
    SampleWindow sample_window(320, 240);
    return app->run(sample_window);
}

OpenCVを使っているのでコンパイルオプションにOpenCVが追加される。

1
$ g++ sample_opencv.cpp `pkg-config --cflags --libs gtkmm-3.0 opencv` -o sample_opencv

実行すると、OpenCVの機能を使ってキャプチャした画像がGTKのGUI上に表示されるようになる。

これにより、OpenCVでは実装しにくかったGUI部分をGtkで補完することができる。

また、USBカメラから画像を取得する部分の実装(uvcを使った実装)がOpenCVの機能のおかげで不要となり実装がより簡潔になる。

OpenCVで取得した画面はcv::MatなのでGdk::Pixbufに変換する必要はあるが、言い換えれば元はcv::Mat形式なのでそれをOpenCVのライブラリを使って画像処理できる。

以上!