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;
}

Aucun commentaire: