Dieser und noch weitere Artikel wurde von evilissimo erstellt.


Folgende Themen werden von diesem Artikel berührt:


Druckversion des Artikels


Grafische Benutzerschnittstellen in C++ mit GTKmm betriebssystemunabhängig gestalten Teil 2


Nach einer langen Auszeit habe ich mich dazu aufgerafft, endlich am Tutorial weiterzuschreiben. Ich hoffe, ihr könnt mir diese Pause verzeihen. :)

Wie versprochen behandle ich in diesem Teil das GTK+ Boxensystem, Labels, Buttons und Signale (Events). Ich werde ein paar Anwendungsbeispiele zu dem jeweiligen Thema anführen. Zum Schluss werde ich ein Beispiel mit allen Widgets zeigen und die eventuell noch unbekannten Schritte zusätzlich kommentieren.


1.GTK+ Boxensystem


1.1 Einführung

In GTK+ und somit auch in GTKmm werden die Widgets mit so genannten „boxes“ angeordnet. Das sind Container, die die Widgets aufnehmen können. Mit diesem Konzept ordnet man Widgets nur relativ und nicht absolut an - im Gegensatz zu dem, wie man es von den bekannteren GUI-Designern kennt (z.b. Borland C++ Builder, Visual C++, Visual C# usw.)

Dieses Konzept ermöglicht ein dynamisches Anpassen der Größe der Widgets zur jeweiligen Fenstergröße, ohne das sich der Programmierer darum kümmern müsste.

Von diesen Containern gibt es verschiedene Arten.
Es gibt Container, die nur jeweils ein Widget aufnehmen können ( diese wurden von Gtk::Bin abgeleitet ), Container die mehrere Widgets aufnehmen können, aber nur in eine Richtung, sprich Horizontal oder Vertikal. Dies wären z.B. Gtk::Hbox für die horizontale und Gtk::VBox für die vertikale Richtung.
Und zu guter Letzt gibt es noch eine Tabelle ( Gtk::Table ), in der man n x m Widgets, je nach dem, wie man es braucht, unterbringen kann, eben in einer tabellarischen Anordnung.

Eine Besonderheit ist aber das Widget Gtk::Fixed. Dies kann Elemente an festen Koordinaten aufnehmen, welche dann aber nicht resizable sind.
Ich werde auf diese Widgets nicht näher eingehen, da ich der Meinung bin, dass man auf diese auch verzichten kann, und mir das Programmieren mit dem Boxensystem wesentlich einfacher und eleganter erscheint.


1.2 Widgets und Boxes

Die Container Gtk::HBox und Gtk::VBox haben neben dem Standard-Konstruktor einen Konstruktor mit folgenden Parametern:

C++:
Gtk::HBox( bool homogenous , int padding );
Gtk::VBox( bool homogenous , int padding );


homogenous gibt hier an, dass der Platz der Widgets gleichmäßig aufgeteilt wird, wenn man true übergibt. Der Parameter padding gibt an, wie viel Platz in Pixeln zwischen den Bereichen der Widgets sein soll.

Um Widgets in die Boxes einzufügen, haben diese folgende Methoden:
C++:
void pack_start(Gtk::Widget& child, PackOptions options = PACK_EXPAND_WIDGET, guint padding = 0);
void pack_end(Gtk::Widget& child, PackOptions options = PACK_EXPAND_WIDGET, guint padding = 0);


Der erste Parameter ist eine Referenz auf das Widget, das wir hinzufügen wollen, z.B. ein Gtk::Button Objekt.

Der zweite Parameter gibt die PackOption an. Davon hängt es ab, wie sich die Anordnung der Widgets abspielt.

Es gibt 3 verschiedene Optionen:



Gibt man PACK_SHRINK an, wird nur so viel Platz verwendet, wie das Widget wirklich braucht, und es wird niemals expandiert.

Bei PACK_EXPAND_WIDGET wird der freie Platz ausgefüllt, indem das Widget einfach vergrößert wird.

Bei PACK_EXPAND_PADDING wird der freie Platz einfach durch Abstände ausgefüllt; das Widget ist dann so gesehen zentriert in seinem Bereich. Der Bereich ist abhängig von dem Drumherum und das wird alles immer dynamisch angepasst.

Der dritte Parameter padding gibt an, wie viel Platz das Widget drum herum haben soll. Im Gegensatz zu dem Parameter des Konstruktors ist hier der Platz um das Widget herum gemeint.


1.3 Widgets und Tables

Gtk::Table hat folgenden Konstruktor:

C++:
Gtk::Table::Table(guint n_rows = 1, guint n_columns = 1, bool homogeneous = false)


Im Grunde ist dieser eigentlich selbsterklärend. n_rows gibt an, wie viele Zeilen das Table haben soll, und n_columns, wie viele Spalten.
homogenous gibt an, ob die Zellen homogen, sprich immer gleich groß, sein sollen.
Generell wäre ich vorsichtig mit der Verwendung von homogenous = true, da es viele Widgets gibt, die sehr viel Platz brauchen und es damit eine negative Auswirkung auf alle Widgets haben kann. Aber natürlich kann dieser Effekt auch erwünscht sein.
Das Beste ist, dass man sich generell erst mal mit den ganzen Containern auseinander setzt und damit etwas rumspielt, bis man das alles verstanden hat.
Das ist einfacher, als ellenlange Texte darüber zu lesen. Die eigene Erfahrung ist immer noch die lehrreichste. (Das ist meine Meinung ;) )


So und nun möchte ich euch erst mal schocken:

C++:
void Gtk::Table::attach(Gtk::Widget& child,
                        guint left_attach, guint right_attach,
                        guint top_attach, guint bottom_attach,
                        guint xoptions = Gtk::FILL | Gtk::EXPAND,
                        guint yoptions = Gtk::FILL | Gtk::EXPAND,
                        guint xpadding = 0, guint ypadding = 0);


Diese Methode ist natürlich auf den ersten Blick ziemlich abschreckend, aber die Verwendung ist einfacher, als es auf den ersten Blick aussehen mag.

child ist das Widget, das wir einfügen möchten und das als Referenz auf das Widget-Objekt übergeben wird.
left_attach gibt an, welche Zelle die Startposition auf der X-Achse ist.
right_attach gibt an, welche Zelle die Endposition auf der X-Achse ist, wobei die Endposition mindestens 1 größer als die Startposition sein muss.
top_attach gibt an, welche Zelle die Startposition auf der Y-Achse ist.
bottom_attach gibt an, welche Zelle die Endposition auf der Y-Achse ist, wobei die Endposition mindestens 1 größer als die Startposition sein muss.

xoptions gibt an, wie sich das Widget innerhalb seiner Zelle auf der X-Achse verhalten soll. Dies ist den PackOptions bei den Boxes ähnlich.
yoptions gibt an, wie sich das Widget innerhalb seiner Zelle auf der Y-Achse verhalten soll. Dies ist den PackOptions bei den Boxes ähnlich.

xpadding gibt an, wie viel Platz das Widget links und rechts von sich haben soll (freier Platz).
ypadding gibt an, wie viel Platz das Widget oben und unten von sich haben soll (freier Platz).


Um das Ganze zu verstehen, schauen wir uns einfach mal den Aufbau einer Tabelle an:

Code:
1
2
3
4
5
6
7
8
    0   1   2
  +---+---+---+
0 | a | b | c |
  +---+---+---+
1 | d | e | f |
  +---+---+---+
2 | g | h | i |
  +---+---+---+


Ich habe die Buchstaben in die Tabelle eingefügt, um besser erklären zu können, wie das Ganze dann beim Aufruf auszusehen hat. Die Zahlen entsprechen dem Zeilen- und Reihenindex.

Angenommen, wir wollen hier Widget 'a' in Zelle 0/0 einfügen, dann würde der Aufruf folgendermaßen aussehen:

C++:
 m_table.attach( a , 0 , 1 , 0 , 1 );


für b sieht das Ganze so aus:

C++:
 m_table.attach( b , 1 , 2 , 0 , 1 );


für e sieht das Ganze dann so aus:

C++:
 m_table.attach( e , 1 , 2 , 1 , 2 );


für i sieht das Ganze dann so aus:

C++:
 m_table.attach( i , 2 , 3 , 2 , 3 );


Nun möchte man aber, dass sich ein Widget über mehrere Zellen erstreckt. Wie das Widget 'd' in diesem Beispiel:
Code:
1
2
3
4
5
6
7
8
    0   1   2
  +---+---+---+
0 | a | b | c |
  +---+---+---+
1 |     d     |
  +---+---+---+
2 | e | f | g |
  +---+---+---+


Dann würde das Einfügen für das Widget 'd' folgendermaßen aussehen:

C++:
 m_table.attach( d , 0 , 3 , 1 , 2 );


Und für folgendes Layout
Code:
1
2
3
4
5
6
7
8
    0   1   2
  +---+---+---+
0 | a | b | c |
  +---+---+---+
1 |           |
  +     d     +
2 |           |
  +---+---+---+

so:
C++:
 m_table.attach( d , 0 , 3 , 1 , 3 );



Bei den xoptions und yoptions gibt es folgende Optionen:


Bei Gtk::FILL wird das Widget, sollte es kleiner als die Zelle selbst sein, auf Zellengröße vergrößert.
Gtk::EXPAND zwingt das Table zum Größerwerden, sollte der Platz für das Widget nicht ausreichen.
Wenn die Tabelle weniger Platz bekommt, als sie braucht, weil der Benutzer z.B. die Fenstergröße ändert, verschwindet das Wigdet einfach. Wenn man Gtk::SHRINK angibt, werden die Widgets in der Tabelle verkleinert, damit sie in den Bereich passen.


1.4 Single Widget Container

Es gibt auch Container-Widgets, die nur ein einziges Widget aufnehmen können. Bestes Beispiel ist Gtk::Window.

Diese Widgets haben nur die Methode add zum Hinzufügen der Widgets und nehmen als Parameter eine Referenz auf das Widget, das hinzugefügt werden soll.

Es gibt auch noch Ausnahmen bei den "Single Widget Containern": Das ist Gtk::Paned, aber auf das werde ich ein anderes Mal zu sprechen kommen ;)


1.5 Anwendung

In dem Beispiel werde ich bereits das erste Mal Gtk::Label benutzen, um zu verdeutlichen, wie man die Boxes verwendet.
C++:
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
struct MyWindow : Gtk::Window
{
    MyWindow();
 
    Gtk::VBox   m_vbox;
    Gtk::HBox   m_hbox;
    Gtk::Label  m_label1;
    Gtk::Label  m_label2;
    Gtk::Label  m_label3;
};    
 
MyWindow::MyWindow()
: Gtk::Window(),
  m_vbox(true,5),// homogene Ausrichtung und 5 px Abstand
  m_hbox(true,5),// homogene Ausrichtung und 5 px Abstand
  m_label1("Label 1"),
  m_label2("Label 2"),
  m_label3("Label 3")
{
    // Text der Titelleiste setzen
    set_title("GTKmm Tutorial Teil 2");
 
    // m_label1 der horizontalen box als erstes Element übergeben
    m_hbox.pack_start(m_label1);
    // m_label2 der horizontalen box als zweites Element übergeben
    m_hbox.pack_start(m_label2);
 
    // m_hbox der vertikalen box als erstes Element übergeben
    m_vbox.pack_start(m_hbox);
    // m_label3 der vertikalen box als zweites Element übergeben
    m_vbox.pack_start(m_label3);
 
    // die vertikale Box an das Fenster übergeben
    add(m_vbox);
 
    // sorgt dafür, dass alle Widgets angezeigt werden
    show_all_children();
}


Und so sieht's aus:



Ein Beispiel für die Verwendung eines Gtk::Table Containers findet ihr im Anwendungsbeispiel für die Buttons :)


2.Signale


2.1 Einführung

In GTKmm wird, um auf Signale/Events zu reagieren, die Bibliothek SigC++ verwendet. SigC++ ist sehr flexibel und mächtig und ist sehr vielseitig verwendbar. Es ist möglich, Funktionen und Methoden aller Art zu verwenden, und es besteht sogar die Möglichkeit, zusätzliche Parameter an ein Signal zu binden, um zusätzliche Daten an den eigenen Signalhandler zu übergeben.

Um nicht zu ausschweifend zu werden, zeige ich euch einfach ein paar Verwendungsbeispiele:

2.2 Anwendung


2.2.1 Eine Funktion als Signalhandler

C++:
void on_button_clicked(); // Unser Signalhandler
 
Gtk::Button button("Ich bin ein button"); // Wir verwenden einen Button zur Veranschaulichung
 
button.signal_clicked().connect(sigc::ptr_fun(&on_button_clicked));


Wie man sieht, ist die Verwendung recht unkompliziert. Die meisten Widgets haben spezielle Zugriffsmethoden, die es einem erlauben, Signalhandler zu setzen.

Gtk::Button hat in diesem Fall z.B. die Methode Gtk::Button::signal_clicked(), um den Handler zu setzen, der auf ein Klick-Ereignis reagiert.

Die Signale aller Widgets zu beschreiben, würde den Rahmen dieses Tutorials sprengen. Sie sollten daher der GTKmm-Dokumentation entnommen werden.


2.2.2 Methoden als Signalhandler

C++:
1
2
3
4
5
6
7
8
9
10
struct test
{
   void on_button_clicked(); // Unser Signalhandler
};
 
Gtk::Button button("Ich bin ein button"); // Wir verwenden einen Button zur Veranschaulichung
 
test test_obj;
 
button.signal_clicked().connect(sigc::mem_fun(test_obj,&test::on_button_clicked));

Um Methoden zu binden, bietet SigC++ die Funktion sigc::mem_fun.

Als ersten Parameter nimmt sigc::mem_fun eine Referenz auf das Objekt der Methode und als zweiten Parameter den Methodenzeiger auf die Methode an.


2.2.3 Binden von Parametern

Da man manchmal zusätzliche Parameter bei einem Ereignis braucht, um auf ein Signal zu reagieren, bietet SigC++ die Funktion sigc::bind an, um zusätzliche Parameter an den Signalhandler zu übergeben.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct data
{};
struct test
{
   void on_button_clicked(data d);
};
 
Gtk::Button button("Ich bin ein button"); // Wir verwenden einen Button zur Veranschaulichung
 
test test_obj;
data d;
 
button.signal_clicked().connect( sigc::bind<data>( sigc::mem_fun( test_obj , &test::on_button_clicked ), d ) );



3.Buttons


3.1 Einführung

In GTKmm gibt es neben den "normalen" Buttons auch RadioButtons, CheckButtons (aka CheckBox) und so genannte ToggleButtons. Viel zu erzählen gibt es hier nicht.

Schaut euch einfach mal das Beispiel an und ihr seht, dass die Verwendung sehr simpel ist. In diesem Beispiel kann man auch schön die Verwendung der oben besprochenen Parameterbindung sehen. Des Weiteren verwende ich ein Gtk::Table als Container, um dessen Verwendung auch darzustellen. :)


3.2 Anwendung

C++:
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
struct MyWindow : Gtk::Window
{
    MyWindow();
private:
    Gtk::ToggleButton     m_toggle_button;
 
    Gtk::CheckButton      m_check_button1;
    Gtk::CheckButton      m_check_button2;
 
    Gtk::RadioButtonGroup m_radiogroup;
    Gtk::RadioButton      m_radio_button1;
    Gtk::RadioButton      m_radio_button2;
   
    Gtk::Button           m_button;
 
    Gtk::Table            m_table;
 
private:
    void attach_widgets_to_table();
    void connect_signals();
    void on_toggle_button_clicked();
    void on_radio_button_clicked(int);
    void on_check_button_clicked(int,Gtk::CheckButton const *);
    void on_button_clicked();
};
 
MyWindow::MyWindow()
: Gtk::Window(),
  m_toggle_button("Gtk::ToggleButton"),
  m_check_button1("Gtk::CheckButton 1"),
  m_check_button2("Gtk::CheckButton 2"),
  m_radiogroup(),
  m_radio_button1(m_radiogroup,"Gtk::RadioButton 1"),// RadioButton einer Gruppe zuordnen und Beschriftung geben
  m_radio_button2(m_radiogroup,"Gtk::RadioButton 2"),// RadioButton einer Gruppe zuordnen und Beschriftung geben
  m_button("Gtk::Button"),
  m_table(3,2,true) // 3 Zeilen, 2 Spalten, homogene Aufteilung der Zellen
{
    // Standardfenstergröße setzen; Breite: 400px Höhe: 170px
    set_default_size(400,170);
 
    // Titel setzen
    set_title("GTKmm Tutorial Teil 2");
 
    // Widgets in das Table einfügen
    attach_widgets_to_table();
 
    // Signale verbinden
    connect_signals();
   
    // Tabelle dem Fenster übergeben
    add(m_table);
   
    // Alle Widgets anzeigen
    show_all_children();
}
 
void MyWindow::attach_widgets_to_table()
{
    // Button in Zelle(0,0) einfügen
    m_table.attach(m_button,0,1,0,1);
 
    // ToggleButton in Zelle(1,0) einfügen
    m_table.attach(m_toggle_button,1,2,0,1);
 
    // CheckButton in Zelle(0,1) einfügen
    m_table.attach(m_check_button1,0,1,1,2);
 
    // CheckButton in Zelle(1,1) einfügen
    m_table.attach(m_check_button2,1,2,1,2);
 
    // RadioButton in Zelle(0,2) einfügen
    m_table.attach(m_radio_button1,0,1,2,3);
 
    // RadioButton in Zelle(1,2) einfügen
    m_table.attach(m_radio_button2,1,2,2,3);
}
 
void MyWindow::connect_signals()
{
    // Signale verbinden:
 
    m_button.signal_clicked().connect(sigc::mem_fun(*this,&MyWindow::on_button_clicked));
 
    m_toggle_button.signal_clicked().connect(sigc::mem_fun(*this,&MyWindow::on_toggle_button_clicked));
 
    m_radio_button1.signal_clicked().connect(sigc::bind<int>(sigc::mem_fun(*this,&MyWindow::on_radio_button_clicked),1));
 
    m_radio_button2.signal_clicked().connect(sigc::bind<int>(sigc::mem_fun(*this,&MyWindow::on_radio_button_clicked),2));
 
    m_check_button1.signal_clicked().connect(sigc::bind<int,Gtk::CheckButton const *>(sigc::mem_fun(*this,&MyWindow::on_check_button_clicked),1,&m_check_button1));
 
    m_check_button2.signal_clicked().connect(sigc::bind<int,Gtk::CheckButton const *>(sigc::mem_fun(*this,&MyWindow::on_check_button_clicked),2,&m_check_button2));
}
 
void MyWindow::on_toggle_button_clicked()
{
    Glib::ustring msg = "ToggleButton wurde angeklickt. Neuer Status ist: ";
    if(m_toggle_button.get_active())
        msg += "gedrueckt";
    else
        msg += "nicht gedrueckt";
   
    Gtk::MessageDialog dia(*this,msg);
   
    dia.run();
}
 
void MyWindow::on_radio_button_clicked(int which)
{
    Glib::ustring msg = "Der Status von RadioButton ";
    if(which == 1)
        msg += "1 hat sich geaendert";
    else
        msg += "2 hat sich geaendert";
 
    Gtk::MessageDialog dia(*this,msg);
 
    dia.run();
}
 
void MyWindow::on_check_button_clicked(int which, Gtk::CheckButton const * cb)
{
    if(!cb)
    {
        Gtk::MessageDialog dia(*this,"Es ist ein Fehler aufgetreten",false,Gtk::MESSAGE_ERROR);
        dia.run();
        return;
    }
 
    Glib::ustring msg = "CheckButton ";
    if(which == 1)
        msg += "1";
    else
        msg += "2";
 
    if(cb->get_active())
        msg += " markiert";
    else
        msg += " Markierung aufgehoben";
 
    Gtk::MessageDialog dia(*this,msg);
 
    dia.run();
}
 
void MyWindow::on_button_clicked()
{
    Gtk::MessageDialog dia(*this,"Button wurde angeklickt :)");
   
    dia.run();
}
 
 
int main(int argc, char **argv)
{
    Gtk::Main main(argc,argv);
    MyWindow window;
    main.run(window);
    return 0;
}


Und so sieht's aus:




4.Implementation eines weiteren Beispiels

C++:
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
#include <gtkmm.h>
 
struct MyWindow : Gtk::Window
{
    MyWindow();
    ~MyWindow();
 
    void on_button1_clicked();
    void on_button2_clicked();
 
    Gtk::Button m_button1;
    Gtk::Button m_button2;
    Gtk::Label  m_label;
    Gtk::VBox   m_vbox;
    Gtk::HBox   m_hbox;
};
 
MyWindow::MyWindow()
: Gtk::Window(),
  m_button1("Klick mich1"),
  m_button2("Klick mich2"),
  m_label("<u><i><b>Ich bin ein Label</b></i></u>"),
  m_vbox(true,5),
  m_hbox(true,5)
{
    set_title("GTKmm Tutorial Teil 2");
 
    m_label.set_use_markup(true);
   
    m_hbox.pack_start(m_button1);
    m_hbox.pack_end(m_button2);
 
    m_button1.signal_clicked().connect(sigc::mem_fun(*this,&MyWindow::on_button1_clicked));
    m_button2.signal_clicked().connect(sigc::mem_fun(*this,&MyWindow::on_button2_clicked));
 
    m_vbox.pack_start(m_hbox);
    m_vbox.pack_start(m_label);
   
    add(m_vbox);
   
    set_default_size(200,100);
    show_all_children(true);
}
MyWindow::~MyWindow()
{
 
}
 
void MyWindow::on_button1_clicked()
{
    m_label.set_markup("<b><i>Button 1</i> wurde angeklickt</b>");
    m_label.set_use_markup(true);
}
void MyWindow::on_button2_clicked()
{
    m_label.set_markup("<b><u>Button 2</u> wurde angeklickt</b>");
    m_label.set_use_markup(true);
}
 
 
int main(int argc, char **argv)
{
    Gtk::Main main(argc,argv);
    MyWindow window;
    main.run(window);
    return 0;
}


Und last but not least: So sieht's aus:




So, diesmal gab's viel Code und wenig Erklärungen, so wird es auch im nächsten Teil aussehen, da ich nicht glaube, dass es zu den Widgets viel zu erzählen gibt. Ich hoffe, dass dieses Tutorial hilfreich war.

Ich schmeiße mich gleich auch ans nächste Tutorial, damit da nicht wieder so ne Ewigkeit dazwischen liegt :)

BR

evilissimo

Sie können Kommentare zu diesem Artikel im Forum schreiben. (Eine Registrierung ist nicht notwendig.)

Logo-Design: MastaMind Webdesign