mardi 18 août 2015

GTK3: rudiment pour déplacer un objet sur une fenêtre (drag sans le drop)


GTK3 n'est pas la panacée, mais ayant décidé d'investir du temps et de l'énergie, je persévère à élaborer de simples exemples (qui manque parois cruellement pour GTK3 spécifiquement).

Le but de cet exemple de code, est de pouvoir afficher un "insert" (cad une image qui s'insert sur une autre), et de pouvoir déplacer cette image à l'aide de la souris. Pour varier les plaisirs, j'ai décider de changer l'insert quand on le déplace.
Pour cela, il faut 3 fichiers PNG, le premier pour l'image de fond (sur lequel va se déplacer l'insert), le deuxième pour l'insert "au repos", et un troisième identique en forme et en taille au précédent, mais plus transparent ou plus sombre que le précédent. Le dernier PNG est l'insert affiché quand on le déplace. Le but est d'obtenir un effet visuel pendant la phase de déplacement de l'insert.

Il existe j'imagine de très nombreuses stratégies, plus où moins documentées, pour effectuer ce genre de tache. Le problème est d'économiser les ressources de la machine, car théoriquement, à chaque micro-déplacement de la souris (bouton appuyé), on devrait ré-afficher la totalité du contenu de la fenêtre.

Voici ma proposition de stratégie, résumée en quelques points:
  • Utilisation d'un élément Widget de type DRAW pour "dessiner" le fond de l'image et l'insert qui doit pouvoir se déplacer.
      image = gtk_drawing_area_new();
  • Utilisation d'une "surface" (au sens CAIRO du terme) pour dessiner préalablement la totalité de la fenêtre (cad le widget DRAW.
      cairo_image_surface_create(CAIRO_FORMAT_ARGB32, l, h )
  • La modification effective du Widget ne se fait qu'à travers l'événement "draw" que l'on récupère dans la callBack, avec la fonction suivante
        g_signal_connect(image, "draw",    G_CALLBACK(on_draw), NULL); 
  • Quant on veut "redessiner" le widget 'image', on provoque l'envoie d'un événement 'draw' avec la fonction asynchrone suivante:
       gtk_widget_queue_draw_area(image, 0, 0, l_image, h_image);
  • On traite individuellement les 3 événements concernant la souris et nécessaire à notre besoin, le déplacement, le bouton appuyé, le bouton relâché:
     g_signal_connect(image,"motion-notify-event",G_CALLBACK(on_motion),NULL);
     g_signal_connect(image,"button-release-event", G_CALLBACK(on_button_release),NULL);
     g_signal_connect(image, "button-press-event",G_CALLBACK(on_button_press), NULL);
  • L'événement 'bouton appuyé' permet de positionner le programme en mode 'déplacement', et de modifier l'affichage de l'insert (celui-ci est remplacé par le troisième PNG).
  • L'événement 'déplacement de la souris' permet de repositionner tout simplement l'insert (uniquement si l'on est en mode déplacement).
  • L’événement 'bouton relâché' permet alors de redessiner l'insert à l'aide du deuxième PNG, pour redevenir un insert banal.
  • Pour chacun des trois événements traités (concernant la souris), on adopte le même principe en deux étapes:
     1- on modifie la surface 'image' sans modifier le Widget, à l'aide de routines CAIRO classiques.
     2- on provoque de manière asynchrone un événement 'draw' qui se chargera de modifier effectivement le Widget 'image' (de type Draw), à l'aide de la surface 'image' préalablement modifiée.
Cette méthode qui consiste à ne pas modifier directement le wiget 'image' (contrairement à ce que je fais habituellement avec un pixbuf classique), fût à l'origine une obligation, car je n'ai pas été foutu de trouver la fonction GTK permettant de créer un 'context' (au sens CAIRO du terme) dynamiquement à partir du Widget, qui m'aurait donc permis de dessiner directement sur le Widget. J'ai pour cela utilisé la callback de l'événement draw, par dépit en quelque sorte. Mais cette contrainte est finalement une bonne chose, car la méthode asynchrone est plutôt vertueuse, même si elle n'est peut-être pas efficace en terme de réactivité, car gourmande en ressource machine.

Avant de vous donner le code complet, je dois avouer avoir essayé d'optimiser le code en limitant la modification de la surface à redessiner à la zone rectangulaire de la surface réellement impacté par le déplacement (en calculant la zone entre le futur déplacement et la précédente position de l'insert). Malheureusement, cette gestion oblige à ajouter beaucoup de code relativement complexe et difficile à vérifier. En considérant le bénéfice apporté par rapport à la complexité induite, j'ai lâchement abandonné cette partie du code. En finale, j'ai donc opté pour le plus raisonnable (mais le plus coûteux en ressource machine): à chaque déplacement de la souris bouton appuyé, je redessine la totalité de l'image (en tout cas la partie affichée en fonction de la taille de la fenêtre), tant sur la surface préalable, que sur le Widget.

Voici donc le code. Il est très verbeux, peu optimisé, et provoque énormément d'écriture sur la console, mais tout cela dans un but pédagogique (ou de recherche d'erreur). Il doit bien rester quelques dizaines de bug, mais cela est une autre histoire. Et n'oubliez pas de positionner dans le répertoire de compilation, 3 fichiers PNG ('bjr.png'; 'insert.png', et 'insert_T.png').

A votre bon cœur pour vos remarques.

#include <cairo.h>
#include <gtk/gtk.h>

//--- prototypes internes
static gboolean on_motion (GtkWidget *widget, GdkEventMotion *event, gpointer data);
static gboolean on_button_press (GtkWidget *widget, GdkEventButton *event, gpointer data);
static gboolean on_button_release (GtkWidget *widget, GdkEventButton *event, gpointer data);
static gboolean on_draw( GtkWidget *    widget, cairo_t *cr, gpointer user_data);
static gboolean on_configure(GtkWidget *widget, GdkEventConfigure *event, gpointer data);
static void activate (GtkApplication *app, gpointer user_data);
static void on_close_window (void);
void    dessiner(cairo_surface_t *surface, int x, int y, float ratio);
void redessiner( int x, int y, int l, int h );
cairo_surface_t * dupliquer_surface(cairo_surface_t *surface, float ratio);

// variable static
static  cairo_surface_t * surface_image;    //-- surface destiné à être affichée sur le widget de type "Draw" (image)
static  cairo_surface_t * surface_fond;     //-- surface provenant du PNG servant d'image de fond sur laquelle on ajoute des inserts
static  cairo_surface_t * surface_insert;    //-- surface provenant du PNG représentant un insert
static  cairo_surface_t * surface_insert_T; //-- surface provenant du PNG représentant le même insert mais sombre et plus transparent
static  cairo_surface_t * surface_insert_T_m; //-- insert modifié en applicant un ration pour réduire sa taille affichée
static    GtkWidget *window;
static    GtkWidget *image;
static int h_insert, l_insert;
static int h_image, l_image;
static int flag_deplacement=0;    //-- mode de déplacement (en laissant appuyé le bouton de la souris et en la déplaçant)
static int flag_deplacement_precis=0;    //-- mode de déplacement plus précis (avec le bouton droit)
int compteur=0;                    //-- pour suivre le nombre d'évènement "Draw" traité
int x_precedent=0,y_precedent=0;    //position de l'insert validé       
int x_reference=0,y_reference=0;    // position de référence du tout premier click (pour un déplacement par rapport à l'insert validé précédement
float ratio_insert=0.5;
float coef_precision=0.3;

#define max(a,b) (a>=b?a:b)
#define min(a,b) (a<=b?a:b)



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

    app = gtk_application_new ("vb.gtk.example", 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;
}

static void activate (GtkApplication *app, gpointer user_data)

        cairo_t *cr;

    //----- chargement des PNG et superposition  du PNG secondaire (insert)
    // la surface "image" est celle sur laquelle on dessine pour ensuite l'afficher
    // Les 3 autres surfaces "fond", "insert", "insert_t" sont des images
    //   que l'on dessine sur la surface de référence, et sont respectivement
    //   le fond de l'image, un insert transparent, et un insert sombre transparent
    surface_image = cairo_image_surface_create_from_png("bjr.png");
    surface_fond = cairo_image_surface_create_from_png("bjr.png");
    surface_insert = cairo_image_surface_create_from_png("insert.png");
    surface_insert_T = cairo_image_surface_create_from_png("insert_T.png");
    l_insert= cairo_image_surface_get_width(surface_insert_T);
    h_insert= cairo_image_surface_get_height(surface_insert_T);
    surface_insert_T_m= dupliquer_surface(surface_insert_T,ratio_insert);
    if (cairo_surface_status(surface_fond)!=CAIRO_STATUS_SUCCESS)        printf("\n---image de fond introuvable\n");
    if (cairo_surface_status(surface_insert)!=CAIRO_STATUS_SUCCESS)        printf("\n---image 'insert' introuvable\n");
    if (cairo_surface_status(surface_insert_T)!=CAIRO_STATUS_SUCCESS)    printf("\n---image 'insert sombre' introuvable\n");
    if (cairo_surface_status(surface_insert_T_m)!=CAIRO_STATUS_SUCCESS)    printf("\n---image 'insert sombre' non adaptable\n");
   
    printf("\n(insert) l=%i, h=%i\n", l_insert, h_insert);
   
      window = gtk_application_window_new (app);
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);
      image = gtk_drawing_area_new();
    gtk_container_add(GTK_CONTAINER (window), image);


  /* Event signals */
    g_signal_connect(window, "destroy",        G_CALLBACK(on_close_window), NULL);
    g_signal_connect(image, "draw",    G_CALLBACK(on_draw), NULL);
    g_signal_connect(image, "motion-notify-event", G_CALLBACK (on_motion), NULL);
    g_signal_connect(image, "button-release-event", G_CALLBACK (on_button_release), NULL);
    g_signal_connect(image, "button-press-event", G_CALLBACK (on_button_press), NULL);
    g_signal_connect(image,"configure-event", G_CALLBACK (on_configure), NULL);
    gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
    gtk_window_set_default_size(GTK_WINDOW(window), 450, 300);
    gtk_window_set_title(GTK_WINDOW(window), "Drag image");
   
/* Ask to receive events the drawing area doesn't normally
 * subscribe to. In particular, we need to ask for the
 * button press and motion notify events that want to handle.
 */
  gtk_widget_set_events (image, gtk_widget_get_events (image)
                                     | GDK_BUTTON_PRESS_MASK
                                     | GDK_BUTTON_RELEASE_MASK
                                     | GDK_POINTER_MOTION_MASK);

    gtk_widget_show_all(window);
}


static gboolean on_button_press (GtkWidget *widget, GdkEventButton *event, gpointer data)
{
    //printf("\non_button_press()\n");
    // event->button: GDK_BUTTON_SECONDARY  GDK_BUTTON_PRIMARY
   
    flag_deplacement_precis=0;   
    if ( event->button==GDK_BUTTON_SECONDARY )
    {
        //---on double la précision du déplacement
        flag_deplacement_precis=1;
        printf("\n--déplacement de précision---\n");
    }
    switch(event->type)
    {
    case GDK_2BUTTON_PRESS:
    case GDK_3BUTTON_PRESS:
        x_precedent=0;
        y_precedent=0;
    case GDK_BUTTON_PRESS:
        if (!x_precedent && !y_precedent)    //--on modifie la position de l'insert
        {
            x_precedent= event->x - l_insert*ratio_insert/2;
            y_precedent= event->y - h_insert*ratio_insert/2;
        }
        x_reference=event->x;    //-- on sauvegarde la position du click
        y_reference=event->y;    // pour réaliser un déplacement realtif par la suite
        printf(" Déplacement ON; (click) x=%i,y=%i (precedent) x=%i,y=%i  (image: l=%i;h=%i)\n",(int)event->x, (int)event->y, x_precedent, y_precedent,l_image,h_image);
        flag_deplacement=1;
        redessiner(0,0,0,0);    //--redessiner le fond dans la surface destinées à être afichée
        dessiner(surface_insert_T_m, x_precedent, y_precedent, 1);
        gtk_widget_queue_draw_area(image,0, 0, l_image, h_image); // on force l'affichage complet

    }

  /* pas de propagation */
  return TRUE;
}
static gboolean on_button_release (GtkWidget *widget, GdkEventButton *event, gpointer data)
{
    char c[100];
    int t_x=(int)(event->x-x_reference)*(flag_deplacement_precis?coef_precision:1);
    int t_y=(event->y-y_reference)*(flag_deplacement_precis?coef_precision:1);
    int x =x_precedent+t_x, y=y_precedent+t_y;
    flag_deplacement=0;
    printf(" Déplacement OFF; x,y=%i,%i (t=%i;%i)\n",x, y, t_x, t_y);

    //event->button == GDK_BUTTON_PRIMARY; GDK_BUTTON_SECONDARY
    dessiner(surface_fond, 0, 0, 1);    //-- redessine totalement le fond
    dessiner(surface_insert, x, y, ratio_insert);
    gtk_widget_queue_draw_area(image,0, 0, l_image, h_image);
    x_precedent=x;
    y_precedent=y;
    sprintf(c, "Drag Image - insert déplacé=>(%i,%i)", x,y);
    gtk_window_set_title(GTK_WINDOW(window), c);


  /* Pas de propagation */
  return TRUE;
}

/* Handle motion events by continuing to draw if button 1 is
 * still held down. The ::motion-notify signal handler receives
 * a GdkEventMotion struct which contains this information.
 */
static gboolean on_motion(GtkWidget *widget, GdkEventMotion *event, gpointer data)
{
    // if (event->state & GDK_BUTTON1_MASK);
    if (flag_deplacement)
    {
        int x =(int)event->x, y=(int)event->y;
        int t_x=(x-x_reference)*(flag_deplacement_precis?coef_precision:1);
        int t_y=(y-y_reference)*(flag_deplacement_precis?coef_precision:1);
        int x_dest=x_precedent+t_x, y_dest=t_y+y_precedent;
        if (flag_deplacement_precis)
        {
            t_x=t_x/4;
            t_y=t_y/4;
        }

        printf("  déplacement (t=%i,%i) x,y=(%i,%i); précédent=(%i,%i)\n",t_x,t_y, x_dest, y_dest,x_precedent,y_precedent);

        if ( (x_dest+l_insert*ratio_insert)>-1 && (y_dest+h_insert*ratio_insert)>-1 && x_dest<=l_image && (y_dest<=h_image) )
        {
            redessiner(    0, 0, 0,0);
            dessiner(surface_insert_T_m, x_dest, y_dest, 1);
            gtk_widget_queue_draw_area(image, 0, 0, l_image, h_image);
        }
        /* Pas de propagation de cet événement */
         return TRUE;
    }
    //-- propagation de l'événement
    return FALSE;
}

/* CallBack pour traiter les changement de dimensionde la fenêtre */
static gboolean on_configure(GtkWidget *widget, GdkEventConfigure *event, gpointer data)
{
    printf("\non_configure()\n");
   
    h_image=gtk_widget_get_allocated_height(image);
    l_image=gtk_widget_get_allocated_width(image);

 /*--copier la surface image (buffer contenant le plateau)
  *  sur le contexte de l'objet de type Draw
  * avec les inserts déjà positionnés----*/
    cairo_t *cr;
    cr= cairo_create(surface_image);
    cairo_set_source_surface(cr, surface_fond, 0, 0);
    cairo_paint(cr);
    cairo_set_source_surface(cr, surface_insert, 50, 50);
    cairo_paint(cr);
    cairo_scale(cr,ratio_insert,ratio_insert);   
    cairo_set_source_surface(cr, surface_insert_T, 450, 150);
    cairo_paint(cr);

   
    //--- écriture sur le PNG principal
    cairo_scale(cr,1/ratio_insert,1/ratio_insert);    //--on reveient à l'échelle 1 (cad ratio=1)
    cairo_set_font_size(cr, 20);
    cairo_set_source_rgb(cr, 0.9 , 0.9 , 0.9);
    cairo_move_to(cr, 20, 20);
    cairo_show_text(cr, " flippermania ");
    cairo_stroke(cr);
   
    cairo_destroy(cr);
   
  /* Propagation */
  return FALSE;
}

static gboolean on_draw( GtkWidget *    widget, cairo_t *cr, gpointer user_data)
{
    printf("\non_draw() #%i\n", compteur++);
 /*--copier la  surface (buffer contenant le plateau) sur le contexte de l'objet ----*/

    cairo_set_source_surface(cr, surface_image, 0, 0);
    cairo_paint(cr);
   
    return TRUE;
}

static void on_close_window (void)
{

    if (surface_image)    cairo_surface_destroy (surface_image);
    if (surface_insert)   cairo_surface_destroy (surface_insert);

    // gtk_main_quit (); provoque une erreur
}

//---- pour dessiner une image sur la surface destinée à être affichée
void dessiner(cairo_surface_t *surface, int x, int y, float ratio )
{
   
    cairo_t *cr;

    cr= cairo_create(surface_image);
    if (ratio!=1)    cairo_scale(cr,ratio,ratio);
    cairo_set_source_surface(cr, surface, x/ratio, y/ratio);
    cairo_paint(cr);
    cairo_destroy(cr);
}

//--- cad redessiner le fond de l'image (pour faire disparaitre les inserts)
//    sur la surface destiné à être affiché dans l'évènement "Draw"
void redessiner( int x, int y, int l, int h )
{
    cairo_t *cr_out;
    cairo_surface_t *surface;
    if (l==0)    l=l_image;
    if (h==0)    h=h_image;
    surface= cairo_surface_create_for_rectangle (surface_fond, (double)x, (double)y, (double) l, (double) h);
    dessiner(surface,x,y, 1);
    cairo_surface_destroy(surface);
}

//---- duppliquer une surface en appliquant un ratio (réducteur ou non)
cairo_surface_t * dupliquer_surface(cairo_surface_t *surface, float ratio)
{
    cairo_surface_t * s= NULL;
    int l,h;
    cairo_t *cr;
   
    l= cairo_image_surface_get_width(surface_insert_T);
    h= cairo_image_surface_get_height(surface_insert_T);

    if (cairo_surface_status(surface_insert_T)!=CAIRO_STATUS_SUCCESS || l<1 || h<1 || ratio<=0)       return s;
   
    s= cairo_image_surface_create(CAIRO_FORMAT_ARGB32, l*ratio, h * ratio);
    cr= cairo_create(s);
    cairo_scale(cr,ratio,ratio);
    cairo_set_source_surface(cr, surface, 0, 0);
    cairo_paint(cr);
    cairo_destroy(cr);
    return s;
}

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.

GTK3 - attention à bien débuter, sinon vous risquez de perdre beaucoup de temps.


J'ai eu la faiblesse de croire que GTK, la librairie graphique sensée permettre de programmer facilement des applications graphiques, me ferait gagner du temps. Au départ, j'ai commencé à programmer avec Xlib (cad au niveau du serveur X Window). Par la suite, j'ai basculé mon développement en GTK (pour sa popularité et son ancienneté). Malheureusement, j'ai très vite déchanté: le gain de temps (cad en terme de réduction de code) ne fut pas évidente, car les appels aux fonctions à GTK sont finalement presque aussi nombreux que ceux à Xlib.

Mais le point le plus négatif fut le problème initialement sous-estimé des différentes versions de GTK. Pour faire simple, 3 versions majeurs de GTK ont donné lieu par deux fois, à un cortège de fonctions obsolètes ou disparues, et de modules remplaçant certains autres, et de concepts fondamentalement transformés. Le principal inconvénient concerne la documentation, mais surtout les différents exemples présents sur le Net. J'ai donc trop souvent perdu du temps à étudier un exemple de code écrit pour GTK1 ou GTK2, sans même le savoir, car trop souvent cette information n'est même pas mentionnée. On le découvre bien souvent lors de la compilation avec les options et librairies GTK3.

Ayant pris la décision dès le départ de développer en GTK 3, j'ai ainsi perdu trop d'énergie avec la documentation disponible sur le Net, qui très rarement ne concerne pas réellement GTK3. Idem pour les livres, qui bien souvent ne signale même pas le n° de version traitée par l'auteur.

Mon premier conseil: n'achetez pas de livre sur GTK dont la première édition date d'avant GTK3 (début 2011). Ils sont rares, voir inexistant. Sinon, vous risquez de perdre du temps à étudier des fondamentaux inutiles et de programmer avec des concepts de GTK2 (voir GTK1), dorénavant obsolètes.

Mon deuxième conseil est simple:
Pour étudier des exemples de code, n'utilisez que ceux présents sur gnome.org (ci-dessous). Malheureusement, tous les autres risquent de vous envoyer vers des solutions inadaptées, et de vous faire perdre beaucoup de temps. A noter que même sur les exemples GTK3 officiels, vous serez susceptible de rencontrer des fonctions obsolètes, car GTK3 évolue très rapidement.

Donc, ne perdez pas de temps (comme moi) avec des PDF ou documentation "offline" qui ne soit pas téléchargeable sur le site officiel, avec la dernière version en date. Oubliez donc dans un premier temps vos habitudes de recherche de code sur le Net "au petit bonheur la chance". Limitez-vous strictement vous au exemple de code du site officiel (ci-dessous). Tous les concepts y sont présents sans exception.

Et oubliez les conseils des anciens qui vous diront que GTK2 est bien suffisant, et que le passage de GTK2 vers GTK3 peut se faire à peu de frais. Les conseilleurs ne sont pas les payeurs.

Avec un peu de chance, vous ne perdrez pas les jours que j'ai pour ma part dépensé inutilement à cause de cela. Bonne continuation à vous.

Liens vers les exemples de code pour GTK3
Liens vers la référence pour GTK
Wiki concernant GTK


L'intelligence, c'est d'avoir raison la deuxième fois. Avoir raison la première fois, c'est simplement de la chance.