Die X.org-Entwickler haben mit Xinput 2 den Grundstein gelegt, die Macher des GUI-Toolkits GTK+ setzen darauf auf. Dieser Artikel zeigt, wie man damit Anwendungen für mehrere Zeigegeräte programmiert.
Parallel zur Einführung von Xinput 2 [1] begann die Arbeit an einem neuen GTK+-Zweig. Mittlerweile macht das Toolkit Anwendungsentwicklern die neuen Features zugänglich, etwa die Unterstützung für mehrere Eingabegeräte sowie rudimentäre Multitouch-Funktionen. Xinput 2 wurde eingeführt, um mehrere Zeiger-Tastatur-Paare zu unterstützen, also für Anwendungen, in denen mehrere Benutzer zusammenarbeiten. Für Multitouch, bei dem zu einer Tastatur viele Zeigepunkte gehören, etwa die Fingerspitzen auf einem Touchscreen, ist es eigentlich nicht gedacht.
Die im Artikel beschriebenen Beispiele lassen sich mit der GTK+-Version 2.90.1 oder neuer nachvollziehen, lediglich für den Multitouch-Code in Listing 7 ist ein Git-Auszug des Zweigs »xi2-playground« [2] erforderlich. Die Neuerungen werden in GTK+ 3.0 einfließen, dessen Veröffentlichung für Dezember 2010 geplant ist. Wer einen Multitouchscreen besitzt, zum Beispiel einen von N-Trig, benötigt mindestens Kernelversion 2.6.31 und einen Evdev-Treiber mit experimenteller Multitouch-Unterstützung [3].
Um Ordnung in die zahlreicher gewordenen Geräte zu bringen, verwendet Xinput 2 eine Gerätehierarchie: Master Devices, auch als virtuelle Geräte bezeichnet, entsprechen ihrem Abbild auf dem Bildschirm (Abbildung 1, oben), also dem Mauszeiger oder einem blinkenden Eingabecursor in einem Textfeld. Sie nehmen Events von den ihnen zugeordneten physischen Geräten entgegen. Die Slave Devices entsprechen den physischen Geräten, die an den Computer angeschlossen sind. Sie können einem Master Device zugeordnet sein, müssen aber nicht (Abbildung 1, unten).

Abbildung 1: Xinput 2 unterscheidet zwischen virtuellen Master Devices (oben) und Slave Devices (unten).
Zum Auslesen und Ändern der Gerätehierarchie dient das Xinput-Kommando. Der Befehl »xinput list« gibt die aktuelle Konfiguration aus (Abbildung 2), »xinput create-master« und »xinput reattach« legen ein neues Master-Paar (Tastatur und Zeigegerät) an und bestücken es mit dem Touchpad (Abbildung 3).

Abbildung 2: Xinput im Einsatz: Oben eine Hierarchie mit einer Tastatur und einer Maus, dann fügt der Anwender ein zweites Gerätepaar hinzu.

Abbildung 3: Neben dem zweiten Tastatur-Zeiger-Paar gibt es auch ungebundene Slave-Geräte – die Touchpoints des eingebauten Touchscreen.
Paarweise
Pointer und Keyboard sind gepaart, denn bestimmte Desktop-Aktionen verlangen den jeweiligen Gegenpart zu Maus oder Tastatur. Öffnet sich beispielsweise ein Popup-Menü, kann der Anwender es sowohl mit der Maus als auch mit der Tastatur bedienen.
GTK+ stellt die Gerätehierarchie über das Objekt »GdkDeviceManager« zur Verfügung. Es existiert nur einmal pro Display und erhält Signale, wenn der Benutzer ein Gerät hinzufügt oder entfernt. Listing 1 zeigt das Auslesen der Hierarchie: Die Zeilen 5 und 6 ermitteln den Device-Manager für das aktuelle Display, Zeile 8 holt die Liste der Geräte, die Zeile 17 schließlich wieder freigibt. Das Iterieren über die Device-Liste besorgt der For-Block ab Zeile 10.
|
Listing 1: Gerätehierarchie |
|---|
01 GdkDisplay *display;
02 GdkDeviceManager *device_manager;
03 GList *devices, *d;
04
05 display = gtk_widget_get_display (widget);
06 device_manager = gdk_display_get_device_manager (display);
07
08 devices = gdk_device_manager_get_devices (device_manager);
09
10 for (d = devices; d != NULL; d = d->next)
11 {
12 GdkDevice *device;
13
14 device = d->data;
15 }
16
17 g_list_free (devices);
|
Der Code in Listing 2 registriert, wenn ein neues Gerät auftaucht. Dabei aktivieren die Zeilen 8 und 9 die Events für das hinzugefügte Gerät. GTK+-Anwendungen sind Event-basiert. Das Windowing-Backend in Gdk, zwischen X11 und GTK+ angesiedelt, schickt Gdk-Events zur Verarbeitung an die GTK+-Widgets. Eine Reihe dieser Events bildet ab, was der Benutzer mit Maus und Tastatur macht, Tabelle 1 listet sie auf.
|
Listing 2: Neues Gerät |
|---|
01 static void
02 device_added_cb (GdkDeviceManager *device_manager,
03 GdkDevice *device,
04 gpointer user_data)
05 {
06 GtkWidget *widget = user_data;
07
08 gdk_window_set_device_events (gtk_widget_get_window (widget),
09 device, gtk_widget_get_events (widget));
10 }
11
12 ...
13
14 /* Folgendes aus dem Widget-Konstruktor aufrufen:
15 g_signal_connect (device_manager, "device-added",
16 G_CALLBACK (device_added_cb), NULL);
|
Da Anwendungen mit mehreren Eingabegeräten zusammenarbeiten, geben die Events auch das auslösende Gerät an. GTK+-Anwendungen mit Multipointer-Fähigkeiten können so die Geräte unterscheiden und jeweils angemessen reagieren. Listing 3 zeigt, wie der Programmierer das auslösende Eingabegerät herausfindet. Zeile 13 ermittelt die Event-Koordinaten, Zeile 16 das auslösende »device«. Anschließend wandelt der Code die Koordinaten in einen »GdkPoint« im Speicher um, der sich in einer Hashtabelle speichern lässt.
|
Listing 3: Auslösendes |
|---|
01 static gboolean
02 motion_notify_cb (GtkWidget *widget,
03 GdkEventMotion *event,
04 gpointer user_data)
05 {
06 MyWidgetPrivate *private_data;
07 GdkDevice *device;
08 GdkPoint coords;
09 gdouble x, y;
10
11 private_data = MY_WIDGET (widget)->private;
12
13 if (!gdk_event_get_coords (event, &x, &y))
14 return FALSE;
15
16 device = gdk_event_get_device ((GdkEvent *) event);
17
18 coords = g_new (GdkPoint);
19 coords->x = (gint) x;
20 coords->y = (gint) y;
21
22 g_hash_table_replace (private_data->device_coords,
23 device, coords);
24
25 return TRUE;
26 }
|
Grabs verarbeiten
Ein Grab, bei dem sich ein Fenster ein Eingabegerät schnappt, ist für den Programmierer ein wichtiges Mittel, um das Verhalten einer Anwendung zu steuern. Besteht ein Grab auf ein Eingabegerät, werden alle von diesem ausgelösten Events an das zugeordnete Fenster umgeleitet. Das kommt zum Beispiel bei modalen Dialogfenstern zum Einsatz: Auch wenn der Anwender neben das aufgegangene Fenster klickt oder Text eingibt, reagiert immer nur das modale Fenster (Abbildung 3).
Der Code in Listing 4 erzeugt im Block ab Zeile 12 einen Grab auf den Mausklick und speichert das betroffene Gerät für künftige Prüfungen. Die Zeilen 32 und 33 sorgen dafür, dass die Anwendung alle Eingaben der zugeordneten Tastatur ignoriert – außer der Escape-Taste, die den Grab aufhebt. In den Zeilen 37 und 38 bestimmt das Programm das Zeigegerät, das mit der Tastatur gepaart ist.
|
Listing 4: Grab auf |
|---|
01 static gboolean
02 button_press_cb (GtkWidget *widget,
03 GdkEventButton *event,
04 gpointer user_data)
05 {
06 GdkDevice *device;
07 MyWidgetPrivate *private_data;
08
09 device = gdk_event_get_device ((GdkEvent *) event);
10 private_data = MY_WIDGET (widget)->private;
11
12 if (!private_data->grabbed_device &&
13 gdk_device_grab (device, widget->window,
14 GDK_OWNERSHIP_NONE, FALSE,
15 GDK_POINTER_MOTION_MASK, NULL,
16 gdk_event_get_time ((GdkEvent *) event)) == GDK_GRAB_SUCCESS)
17 {
18 private_data->grabbed_device = device;
19 }
20
21 return TRUE;
22 }
23
24 static gboolean
25 key_press_cb (GtkWidget *widget,
26 GdkEventKey *event,
27 gpointer user_data)
28 {
29 GdkDevice *keyboard, *pointer;
30 MyWidgetPrivate *private_data;
31
32 if (event->key != GDK_Escape)
33 return FALSE;
34
35 private_data = MY_WIDGET (widget)->private;
36
37 keyboard = gdk_event_get_device ((GdkEvent *) event);
38 pointer = gdk_device_get_associated_device (keyboard);
39
40 if (private_data->grabbed_device != NULL &&
41 private_data->grabbed_device == pointer)
42 {
43 gdk_device_ungrab (device, gdk_event_get_time ((GdkEvent *) event));
44 private_data->grabbed_device = NULL;
45 }
46
47 return TRUE;
48 }
|
Der folgende If-Block prüft, ob dieses Zeigegerät auch jenes ist, auf das der Grab wirkt. Falls ja, heben die folgenden Codezeilen den Grab auf und setzen das gespeicherte Gerät zurück. Das in Tabelle 1 erwähnte Ereignis »GdkEventGrabBroken« tritt ein, wenn beispielsweise der Benutzer das Grab-Fenster schließt oder ein neuerer Grab auf dasselbe Gerät durch einen anderen Teil derselben Anwendung einsetzt. Die typische Reaktion darauf besteht darin, jene Aktion rückgängig zu machen, die den ersten Grab angefordert hat.

Abbildung 4: Modale Fenster wie diese Nachfrage ziehen per Grab alle Eingaben an sich – das darunter liegende Fenster ist nicht bedienbar.
Grab in Light-Version
Das GTK+-Toolkit bietet daneben mit dem GTK+-Grab noch eine weitere Variante an, sozusagen eine Light-Version. Sie leitet Events eines Geräts zu einem GTK+-Widget um, aber nur, wenn das Event dieselbe Anwendung zum Ziel hat. So sind in der Regel modale Dialoge umgesetzt – das Fenster unmittelbar darunter ignoriert Eingaben, andere Anwendungen reagieren aber.
Listing 5 zeigt die Anwendung eines GTK+-Grab: Die Zeilen 3 und 4 öffnen ein Dialogfenster, Zeile 6 setzt den Grab auf das gespeicherte Zeigegerät, damit dessen sämtliche Events außerhalb des Fensters wirkungslos bleiben. Andere Geräte können aber mit dem Rest der Anwendung interagieren. Der dritte Parameter zu »gtk_device_grab_add()« lautet hier »FALSE«. Wäre er true, wären auch die anderen Eingabegeräte blockiert.
|
Listing 5: GTK+-Grab auf |
|---|
01 GtkWidget *dialog; 02 03 dialog = create_dialog (); 04 gtk_widget_show (dialog); 05 06 gtk_device_grab_add (dialog, private_data->grabbed_device, FALSE); 07 private_data->dialog = dialog; |
Benachrichtigung
Das Gegenstück zu »GdkEventGrabBroken« ist in solchen Situationen das Signal »GtkWidget::grab-notify«, das bei Einsetzen eines GTK+-Grab ausgesandt wird. In einem Multidevice-Szenario muss ein Widget nämlich erst prüfen, ob ein anderes Widget bereits eines der Geräte, mit denen es interagiert, beansprucht.
Listing 6 demonstriert einen typischen »grab-notify«-Handler, der einen bestehenden GTK+-Grab rückgängig macht. Sind das Gerät und das Dialogfenster beide nicht »NULL« (Zeilen 10 und 11), besteht ein GTK+-Grab auf das Gerät. Dann überprüft der Code in Zeile 12, ob die Ereignisse nun zu einem anderen Widget umgeleitet werden. In diesem Fall gibt es einen neuen Grab, und die Zeilen 14 und 15 heben den alten auf.
|
Listing 6: |
|---|
01 static void
02 grab_notify_cb (GtkWidget *widget,
03 gboolean was_grabbed,
04 gpointer user_data)
05 {
06 MyWidgetPrivate *private_data;
07
08 private_data = MY_WIDGET (widget)->private;
09
10 if (private_data->grabbed_device &&
11 private_data->dialog &&
12 gtk_widget_device_is_shadowed (private_data->dialog, private_data->grabbed_device)
13 {
14 gtk_device_grab_remove (private_data->dialog, private_data->grabbed_device);
15 private_data->grabbed_device = NULL;
16 }
17 }
|
Arbeiten mehrere Anwender mit ihren Eingabegeräten in derselben Anwendung, wird die Verarbeitung der Eingabe-Ereignisse rasch kompliziert: Meier bedient das von Müller geöffnete Menü, Müller schließt aus Rache Meiers Dialog. In solchen Situationen hilft dem Anwendungsentwickler der Einsatz von Grabs. Damit steuert er das Verhalten anderer Eingabegeräte, die nicht Teil dieses Grab sind. Dabei kann ein Grab die übrigen Geräte daran hindern, Events innerhalb des Grab-Fensters oder der gesamten Anwendung zu erzeugen. Die Logik dahinter ist Sache des Programmierers, der damit verantwortungsvoll umgehen muss.
Wo bleibt aber Multitouch? Derzeit arbeiten die X.org-Entwickler an Version 2.1 der Xinput-Spezifikation [4], die sich mit direkten Multitouch-Geräten wie Touchscreens und indirekten wie Trackpads beschäftigt.
|
Listing 7: |
|---|
01 static void
02 multi_device_event_cb (GtkWidget *widget,
03 GtkDeviceGroup *group,
04 GtkMultiDeviceEvent *event,
05 gpointer user_data)
06 {
07 gdouble center_x, center_y, angle;
08
09 if (event->n_events != 2)
10 return;
11
12 gdk_events_get_center ((GdkEvent *) event->events[0],
13 (GdkEvent *) event->events[1],
14 ¢er_x, ¢er_y);
15
16 gdk_events_get_angle ((GdkEvent *) event->events[0],
17 (GdkEvent *) event->events[1],
18 &angle);
19 ...
20 }
21
22 static gboolean
23 button_cb (GtkWidget *widget,
24 GdkEventButton *event,
25 gpointer user_data)
26 {
27 GdkDevice *device;
28 MyWidgetPrivate *private_data;
29
30 device = gdk_event_get_device ((GdkEvent *) event);
31 private_data = MY_WIDGET (widget)->private;
32
33 if (event->type == GDK_BUTTON_PRESS)
34 gtk_device_group_add_device (private_data->group, device);
35 else if (event->type == GDK_BUTTON_RELEASE)
36 gtk_device_group_remove_device (private_data->group, device);
37
38 return TRUE;
39 }
40
41 static void
42 my_widget_init (GtkWidget *widget)
43 {
44 MyWidgetPrivate *private_data;
45
46 private_data = MY_WIDGET (widget)->private;
47 private_data->group = gtk_widget_create_device_group (widget);
48
49 g_signal_connect (widget, "multidevice-event",
50 G_CALLBACK (multidevice_cb), NULL);
51
52 g_signal_connect (widget, "button-press-event",
53 G_CALLBACK (button_cb), NULL);
54 g_signal_connect (widget, "button-release-event",
55 G_CALLBACK (button_cb), NULL);
56 ...
57 }
|
Ausblick auf Multitouch
In der Zwischenzeit gibt es einige experimentelle Ansätze unter X11, die Multitouch-Geräte als Gruppe von Slave-Geräten behandeln, die an keine Tastatur gebunden sind. In Abbildung 3 sind die Touch-Punkte des N-Trig-Touchscreen jeweils als »floating slave« gekennzeichnet. Auch die GTK+-Entwickler haben experimentiert und ein API für mehrere Zeigegeräte entwickelt. Listing 7 sammelt die Ereignisse aller in der »GtkDeviceGroup« gebündelten Geräte ein. Das Programm reagiert nur, wenn zwei Eingabegeräte im Spiel sind (Zeile 9), dann findet es ab Zeile 12 die Mitte und den relativen Winkel zwischen den Geräten heraus. Der Code ab Zeile 33 fügt der Device-Gruppe ein Gerät hinzu oder entfernt es. Es kann mehrere Gruppen geben, die alle ein Multidevice-Event erhalten, wenn sich eines der enthaltenen Geräte aktualisiert.
Die Anweisungen ab Zeile 52 verbinden das Drücken und Loslassen einer Taste mit der Funktion für die Gruppierung. Dabei kann ein einziges Widget mehrere Device-Gruppen erzeugen, etwa für die einzelnen Fotos in einem Bildbetrachter-Widget. Der experimentelle GTK+-Code für Listing 7 liegt im Zweig »xi2-playground« [2] bereit. Zum Ausprobieren ist kein teurer Touchscreen nötig – eine zweite Maus aus der Grabbelkiste tut’s auch. (mhu)
|
Infos |
|---|
|
[1] Xinput 2: [http://www.x.org/wiki/XI2] [2] GTK+-Zweig XI2-Playground: [http://git.gnome.org/browse/gtk+/?h=xi2-playground] [3] Evdev-Treiber mit Multitouch-Unterstützung: [http://cgit.freedesktop.org/~carlosg/xf86-input-evdev/log/?h=multitouch-subdevs] [4] Xinput 2.1: [http://cgit.freedesktop.org/~whot/inputproto/tree/XI2proto.txt?h=multitouch] |
|
Der Autor |
|---|
|
Carlos Garnacho arbeitet seit 2001 an Gnome mit. Er ist bei der Lanedo GmbH angestellt und entwickelt dort neue Features für GTK+ und Tracker. |





