jeudi 13 août 2015

GTK3 - le soucis des clicks de souris sur une image munie d'ascenseurs (scrolled window)

Voici un soucis qui m'a fait perdre une journée entière. Et pourtant la solution était si simple.

Description du besoin: traiter les click de souris sur une image, pour déterminer les coordonnées du click (x,y) relatif à cette image. Pas de soucis théoriquement, si vous utilisez la fonction permettant de paramétrer une boucle de traitement des événements concernant le bouton de la souris:
g_signal_connect(G_OBJECT(window),"button-press-event",
                    G_CALLBACK(on_button_press), NULL);
Vous pouvez ainsi récupérer le x,y (et plein d'autre info) concernant chaque click de souris.
Pour illustrer le problème, voici le code source pour afficher le x,y de chaque click sur la fenêtre contenant l'image (widget GTK IMAGE).

#include
#include

gboolean on_button_press (GtkWidget *widget, GdkEventButton *event);

static  GtkWidget *window;
static  GtkWidget *scrolled_window;
static  GtkWidget *image;

static void
activate (GtkApplication *app, gpointer user_data)
{
/* Création des widgets de l'application */
  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "ScrolledWindow");
  gtk_window_set_default_size (GTK_WINDOW (window), 220, 200);

/* Création de la "scrolled window". Paramètres NULL :
 * création automatique des ascenseurs verticaux et horizontaux.
 * "scrollbar policy" automatique également:
 * la visibilité des ascenseurs est automatique (cad si nécessaire).
 */
    scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy(    GTK_SCROLLED_WINDOW (scrolled_window),
                                    GTK_POLICY_AUTOMATIC,
                                    GTK_POLICY_AUTOMATIC);
/* Bordure permettant de ne pas coller les ascenseurs
 *  aux bords de la fenêtre principale (purement esthétique)
 */
    gtk_container_set_border_width (GTK_CONTAINER (scrolled_window), 10);
/* Création d'un Widget IMAGE à partir d'un fichier */
    image = gtk_image_new_from_file ("bjr.jpg");
/* Encapuslation des widget créés*/
    gtk_container_add( GTK_CONTAINER(scrolled_window),    image);
    gtk_container_add( GTK_CONTAINER(window),            scrolled_window);

/* Boucle de traitement des évènements concernant les boutons de la souris */
    g_signal_connect(G_OBJECT(window),    "button-press-event",
                    G_CALLBACK( on_button_press    ), NULL);

    gtk_widget_show_all (window);
}

gboolean on_button_press (GtkWidget *widget, GdkEventButton *event)
{
    gint x = 0;
    gint y = 0;
    GtkAllocation allocation;


    /* Determine la nature du click (simple, double ou triple) */
    switch (event->type)
    {
    case  GDK_2BUTTON_PRESS:
      printf("Click2 x=%g;y=%g\n",event->x,event->y);
    break;
    case  GDK_3BUTTON_PRESS:
      printf("Click3 x=%g;y=%g\n",event->x,event->y);
    break;
    case  GDK_BUTTON_PRESS:
        printf("\nClick x=%i;y=%i; button=%i; root=(%i,%i)\n",(int)event->x,(int)event->y,event->button,(int)event->x_root,(int)event->y_root);
        gtk_widget_get_allocation(image,&allocation);
        printf("    height=%i; width=%i; x=%i; y=%i; (image) \n",allocation.height, allocation.width, allocation.x, allocation.y);
        gtk_widget_get_allocation(scrolled_window,&allocation);
        printf("    height=%i; width=%i; x=%i; y=%i; (scrolled_window) \n",allocation.height, allocation.width, allocation.x, allocation.y);
        gtk_widget_get_allocation(window,&allocation);
        printf("    height=%i; width=%i; x=%i; y=%i; (window) \n",allocation.height, allocation.width, allocation.x, allocation.y);
     
    break;
    }
    /* Propagation de l'evenement */
    return FALSE;
}

int
main (int argc, char **argv)
{
  GtkApplication *app;
  int status;

  app = gtk_application_new ("vb.gtk.example_scrolling", G_APPLICATION_FLAGS_NONE);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}
Vous pouvez compiler l'exemple avec cette ligne:
    gcc `pkg-config gtk+-3.0 --cflags` scrolling_exemple.c -o scrolling_exemple `pkg-config gtk+-3.0 --libs`
N'oubliez pas de mettre dans le même répertoire une image relativement grande (800x800 au minimum), ou alors modifiez dans le code le nom du fichier chargé localement (bjr.jpg).

Ensuite, amusez vous à cliquer sur la fenêtre contenant l'image. Vous remarquerez rapidement que si vous jouez sur la position des ascenseurs, les coordonnées affichées ne sont pas influencées par la translation de l'image  asservie aux ascenseurs ("scrolled window" dans la terminologie GTK).

Pour résoudre le problème, j'ai tenté de jouer sur la position des ascenseurs, pour calculer manuellement la translation ("adjustment" dans la terminologie GTK), et ajouter cette translation aux (x,y) transmis dans l’événement. Malheureusement, cette stratégie est une impasse pour différentes raisons trop longues à expliquer (position du widget par rapport à la fenêtre, dilatation du widget Image, etc...).

La solution pour pouvoir récupérer les coordonnées relative à l'image (et non à la fenêtre principale hébergeant la "scrolled window" et le widget Image) est simplissime:
mettre la "scrolled window" dans un "eventBox" (Widget permettant de récupérer et de traiter les événements traditionnels d'une fenêtre pour les widgets ne pouvant pas le faire, à l'instar d'un widget Image).
En faisant cela, miracle, le (x,y) du click sur l'image est dorénavant relatif à l'image. Pour cela, il suffit d'ajouter deux lignes et d'en modifier une:

    /* Encapuslation des widget créés*/
    eventBox=gtk_event_box_new();
    gtk_container_add( GTK_CONTAINER(eventBox),            image);
    gtk_container_add( GTK_CONTAINER(scrolled_window),    eventBox);
    gtk_container_add( GTK_CONTAINER(window),    scrolled_window);

N'oubliez pas d'ajouter la variable static correspondante à ce nouveau widget:
static  GtkWidget *eventBox;
J'avoue avoir trouvé cette solution par hasard, sans vraiment la comprendre. Après réflexion, je crois pouvoir donner une explication: Sans le "EventBox", le click de la souris se comporte comme ci le widget Image n'existait pas, puisque ce Widget ne peut pas traiter ce type d’événement. Alors que si on transforme ce Widget Image en un objet pouvant le faire, son propre référentiel de coordonnées s'applique et prime alors sur celui de la fenêtre. CQFD (enfin je crois).

Pour finir, il reste un problème important: on reçoit également les événements en dehors de l'image. Pour cela, il faut  "filtrer" les événements qui ne concernent pas le Widget Image. Solution la plus simple: "connecter un signal" spécifique à l'objet "eventBox". Solution alternative : filtrer dans la "callBack" les événements avec un simple test sur la valeur du Widget de la CallBack.

    /* Boucle de traitement des événements concernant les boutons de la souris */
    g_signal_connect(G_OBJECT(eventBox),    "button-press-event",
                    G_CALLBACK( on_button_press    ), NULL);

Et pour être totalement franc avec vous, il restera encore un cas particulier à traiter: la dilatation automatique de l'objet Image, qui s’agrandit automatiquement si on agrandit la fenêtre principale au delà des limites de l'image (cad en faisant disparaître les ascenseurs de l'image). Pour résoudre cela, il faut alors tester la taille effective de l'objet Image (que vous pouvez tester avec l'exemple ci-dessous avec la fonction "location" (dans la terminologie GTK), et appliquer une translation des x,y en conséquence, dans le cas ou la taille du Widget Image dépasse celle de l'image originelle.

Pour finir, sachez qu'avec un Widget "Draw" (pour afficher l'image munie d'ascenseurs sur le même principe), le problème est le même, et la solution identique. En revanche, vous n'aurez pas à gérer la dilatation automatique de l'objet (pas besoin de translater si on agrandie la fenêtre). .

J'espère que ce petit exemple de code vous permettra d'économiser la journée que j'ai perdue sur ce si petit problème.

Doc concernant l’événement traité.
Exemple décrivant l'usage d'une "scrolled Window"
Code source complet avec la gestion de l'image dilatée.

Aucun commentaire: