Infoforall

Python 31 : Créer un système de click-and-drop pour vos applications Tkinter

Version finale

Cette activité propose de créer un système minimaliste de clic-and-drop.

Il ne s'agit pas d'un système clé-en-main mais d'un système minimaliste permettant d'adapter le système à votre propre interface. Mais une fois qu'on sait gérer les cas simples, on peut espérer gérer les cas plus compliqués ou plus généralistes.

Nous allons partir d'un interface basique fixe : on propose 3 nombres. Ces trois nombres devront être replacés du plus petit au plus grand.

1 - Création de l'interface

Commençons par créer une fenêtre, avec 6 widgets : trois pour les nombres affichés et 3 pour les zones à sélectionner.

Le principe du drag-and-drop est de cliquer sur un élément, de le déplacer (en appuyant sur le bouton) jusqu'à la zone voulue.

01° Utiliser le code suivant pour créer votre interface Tkinter basique :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


# Création de la fenetre principale

fen_princ = Tk()

fen_princ.title("Ca drag et ça drop")

fen_princ.geometry("900x600")


# Création des 3 Labels sélectionnables

choix1 = Label(fen_princ, text="10",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

choix2 = Label(fen_princ, text="20",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

choix3 = Label(fen_princ, text="15",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

choix1.place(x=50,y=50)

choix2.place(x=200,y=50)

choix3.place(x=350,y=50)


# Création des 3 Labels dropables

zoneDrop1 = Label(fen_princ, text="",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

zoneDrop2 = Label(fen_princ, text="",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

zoneDrop3 = Label(fen_princ, text="",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

zoneDrop1.place(x=50,y=250)

zoneDrop2.place(x=200,y=250)

zoneDrop3.place(x=350,y=250)


# Lancement de la surveillance sur la fenêtre

fen_princ.mainloop()

Vous devriez obtenir ceci :

Premier menu

Il est temps de voir comment on peut faire fonctionner cela. Nous allons simplement rajouter une classe :

2 - Création d'une classe pour gérer le Drag-And-Drop

02° Rajouter cette classe sous votre importation de Tkinter :

class DragManager():


    def __init__(self,widget,drag=True,drop=True):

        self.widget = widget

        self.drag = drag

        self.drop = drop

        if drag:

            self.add_dragable(self.widget)


    def add_dragable(self, widget):

        self.widget = widget

        self.widget.bind("<ButtonPress-1>", self.on_start)

        self.widget.bind("<B1-Motion>", self.on_drag)

        self.widget.bind("<ButtonRelease-1>", self.on_drop)

        self.widget.configure(cursor="hand1")


    def on_start(self, event):

        pass


    def on_drag(self, event):

        pass


    def on_drop(self, event):

        # commencons par trouver le widget sous le curseur de la souris

        x,y = event.widget.winfo_pointerxy()

        target = event.widget.winfo_containing(x,y)

        try:

            pass

        except:

            pass

Que contient cette classe DragManager ?

03° Trouver clairement dans quel cas on active chaque des trois fonctions.

...CORRECTION...

Il faut regarder les trois créations d'événements.

On active on_start lorsqu'on appuie sur le bouton de la souris.

On active on_drag lorsqu'on déplace la souris en gardant le bouton enfoncé.

On active on_drop lorsqu'on relache le bouton.

Pour l'instant, nous n'avons pas créer d'instance de notre classe. Elle ne sert donc à rien.

04° Rajouter la ligne ci-dessous juste avant le mainloop final : créons un nouvel objet, instance de la classe DragManager en fournissant en argument le premier widget Label.

drag1 = DragManager(choix1)

05° Vérifier que le code ne déclenche pas d'erreur (l'icône devrait changer si vous survoler le premier Label). Pourquoi peut-on donner un seul argument alors que le constructeur attend 3 paramètres (widget, drag, drop) ?

...CORRECTION...

Lors de la création de la méthode constructeur __init__, nous avons transmis des valeurs par défaut aux paramètres drag et drop.

06° Remplacer les pass par quelques print("Quelque chose") de test pour vérifier que le code fonctionne correctement.

3 - Version basique

07° Rajouter la ligne ci-dessous juste après la première instanciation de DragManager.

Il est temps de tenter d'amener notre premier Label dans le Label qui est juste en dessous.

drop1 = DragManager(zoneDrop1)

Un peu rappel :

Pour modifier les valeurs d'un option d'un widget Tkinter, deux solutions existent. Exemple :

variable_widget.configure(cursor="hand1")

variable_widget["cursor"] = "hand1")

Pour lire le contenu d'une option, deux solutions existent :

valeur_lue = variable_widget.cget("cursor")

valeur_lue = variable_widget["cursor")

Attention néanmoins : l'utilisation des méthodes est souvent plus sécurisées.

08° Modifier la fonction on_start de façon à ce qu'elle affiche le texte contenue dans le Label sur lequel on vient de cliquer.

...CORRECTION...

Pensez au fait que le widget de l'objet sur lequel on vient de cliquer est stocké dans self.widget.

09° Stocker ce résultat dans un attribut qu'on nommera self.memoire.

10° Lors de l'activation de la fonction on_drop, modifier le widget de destinatino de façon à ce qu'il contienne le bon texte.

Et voilà. Vous devriez avoir compris comment transferer le texte.

11° Finaliser le code pour que les trois zones du haut et du bas soient dragables et dropables.

Tester bien pour vérifier qu'on puisse bien tout drag-and-droper partout !

4 - Améliorations sur les possibilités de drag ou drop

Bon, c'est bien beau, mais si on regarde le code, on pourrait voir qu'il faut utiliser l'un des Labels pour le rendre dragables mais que tous les Labels sont dropables. Ca peut vite devenir génant ...

Voici une correction de la partie précédente :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


class DragManager():


    def __init__(self,widget,drag=True,drop=True):

        self.widget = widget

        self.drag = drag

        self.drop = drop

        if drag:

            self.add_dragable(self.widget)


    def add_dragable(self, widget):

        self.widget = widget

        self.widget.bind("<ButtonPress-1>", self.on_start)

        self.widget.bind("<B1-Motion>", self.on_drag)

        self.widget.bind("<ButtonRelease-1>", self.on_drop)

        self.widget["cursor"] = "hand1"


    def on_start(self, event):

        self.memoire = self.widget.cget("text")


    def on_drag(self, event):

        print("Deplacement")


    def on_drop(self, event):

        # commencons par trouver le widget sous le curseur de la souris

        x,y = event.widget.winfo_pointerxy()

        target = event.widget.winfo_containing(x,y)

        try:

            target.configure(text=self.memoire)

        except:

            pass


# Création de la fenetre principale

fen_princ = Tk()

fen_princ.title("Ca drag et ça drop")

fen_princ.geometry("900x600")


# Création des 3 Labels sélectionnables

choix1 = Label(fen_princ, text="10",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

choix2 = Label(fen_princ, text="20",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

choix3 = Label(fen_princ, text="15",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

choix1.place(x=50,y=50)

choix2.place(x=200,y=50)

choix3.place(x=350,y=50)


# Création des 3 Labels dropables

zoneDrop1 = Label(fen_princ, text="",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

zoneDrop2 = Label(fen_princ, text="",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

zoneDrop3 = Label(fen_princ, text="",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

zoneDrop1.place(x=50,y=250)

zoneDrop2.place(x=200,y=250)

zoneDrop3.place(x=350,y=250)


# Création des 6 objets autorisant le drag

drag1 = DragManager(choix1)

drag2 = DragManager(choix2)

drag3 = DragManager(choix3)

drop1 = DragManager(zoneDrop1)

drop2 = DragManager(zoneDrop2)

drop3 = DragManager(zoneDrop3)


# Lancement de la surveillance sur la fenêtre

fen_princ.mainloop()

Alors comment éviter les drop sur des widgets qui n'ont pas été utilisés pour instancier un objet de la classe DropManager ?

Premier indice : seuls nos objets possèdent un attribut drop qui contient quelque chose.

Deuxième indice : le try permet d'éviter de planter le programme si, par hasard, le widget de destination ne possède pas d'attribut drop.

Troisième indice : c'est lors de l'instanciation qu'on peut décider des valeurs des attributs drag et drop.

12° Modifier le programme pour qu'on ne puisse plus modifier les labels du haut, ni aucun autre label à part ceux qui ont servi à créer un DragManager avec self.drop = True.

Vous devriez vous rendre compte que ce n'est pas facile : la variable target contient l'adresse du widget et pas l'adresse de l'objet DrapManager correspondant.

Et donc, notre programme ne peut pas tester actuellement si un widget autorise le drop ou non ...

Contraitement, le mieux serait de nous créer une classe basée sur Label. Mais cela demanderait de reconstruire tout le programme...

J'ai une méthode plus facile : nous allons créer une liste de Classe (pas d'instance) qui contiendra la liste de tous les widgets autorisés à accepter un Drop.

Voici les modifications à faire (en rose-violet gras) :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


class DragManager():


    listeDrop = []


    def __init__(self,widget,drag=True,drop=True):

        self.widget = widget

        self.drag = drag

        self.drop = drop

        if drag:

            self.add_dragable(self.widget)

        if drop:

            DragManager.listeDrop.append(self.widget)


    def add_dragable(self, widget):

        self.widget = widget

        self.widget.bind("<ButtonPress-1>", self.on_start)

        self.widget.bind("<B1-Motion>", self.on_drag)

        self.widget.bind("<ButtonRelease-1>", self.on_drop)

        self.widget["cursor"] = "hand1"


    def on_start(self, event):

        self.memoire = self.widget.cget("text")


    def on_drag(self, event):

        print("Deplacement")


    def on_drop(self, event):

        # commencons par trouver le widget sous le curseur de la souris

        x,y = event.widget.winfo_pointerxy()

        target = event.widget.winfo_containing(x,y)

        if target in DragManager.listeDrop:

            try:

                target.configure(text=self.memoire)

            except:

                pass


13° Quelle type de structure de données crée-t-on avec listeDrop = [] ?

14° A quoi sert la méthode append dans DragManager.listeDrop.append(self.widget)?

15° Que réalise le test if target in DragManager.listeDrop: ?

Vérifiez que cela fonctionne maintenant.

5 - Déplacement d'un widget

Il nous reste à déplacer le widget, comme dans un drag-and-drop usuel.

Ici, on ne veut pas faire déplacer le premier widget mais juste en faire une copie.

Or, copier un objet n'est pas facile : on copie son adresse à l'aide d'un égal, pas le contenu.

Comment faire alors ? Créer un nouvel widget, qui ne servira qu'à afficher le déplacement et supprimer une fois sur place.

16° Créer un copie du widget lors de l'activation de la fonction onStart :

    def on_start(self, event):

        self.memoire = self.widget.cget("text")

        widgetDeplace = Label(fen_princ, text=self.memoire,fg="yellow", bg="grey",font=("Helvetica", 20),height = 3, width=5)

        self.icone = widgetDeplace

Pour l'instant, nous n'en faisons rien. Nous l'affichons même pas.

17° Dans la fonction onDrag, aller cherche le nouveau widget dans notre objet et placer le avec la méthode place. Pour trouver les valeurs de x et y, souvenez-vous que l'événement event contient les coordonnées de l'événement. Attention néanmoins à la différence entre x et x_root !

Si vous avez testé votre code, vous devriez vous rendre compte d'un problème.

Les objets créés ne disparaissent pas !

Ca peut être pratique notez bien. Mais pas ici.

Comment détruire un widget ? C'est facile, si on connait la méthode : destroy().

18° Finalisez votre dernière fonction.

Il vous reste à réaliser votre mini-projet du jour : faire un tirage au sort sur le contenu des trois widgets, tester le contenu des trois widgets du bas et afficher un message de félicitations lorsque les nombres sont bien placés. A vous de jouer !

En attendant, voici la correction jusqu'à la question 18 :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

class DragManager():

    listeDrop = []

    def __init__(self,widget,drag=True,drop=True):

        self.widget = widget

        self.drag = drag

        self.drop = drop

        if drag:

            self.add_dragable(self.widget)

        if drop:

            DragManager.listeDrop.append(self.widget)

    def add_dragable(self, widget):

        self.widget = widget

        self.widget.bind("<ButtonPress-1>", self.on_start)

        self.widget.bind("<B1-Motion>", self.on_drag)

        self.widget.bind("<ButtonRelease-1>", self.on_drop)

        self.widget["cursor"] = "hand1"

    def on_start(self, event):

        self.memoire = self.widget.cget("text")

        widgetDeplace = Label(fen_princ, text=self.memoire,fg="yellow", bg="grey",font=("Helvetica", 20),height = 3, width=5)

        self.icone = widgetDeplace

    def on_drag(self, event):

        xd = event.x_root

        yd = event.y_root

        self.icone.place(x=xd,y=yd)

    def on_drop(self, event):

        # commencons par trouver le widget sous le curseur de la souris

        x,y = event.widget.winfo_pointerxy()

        self.icone.destroy()

        target = event.widget.winfo_containing(x,y)

        if target in DragManager.listeDrop:

            try:

                target.configure(text=self.memoire)

            except:

                pass

# Création de la fenetre principale

fen_princ = Tk()

fen_princ.title("Ca drag et ça drop")

fen_princ.geometry("900x600")

# Création des 3 Labels sélectionnables

choix1 = Label(fen_princ, text="10",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

choix2 = Label(fen_princ, text="20",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

choix3 = Label(fen_princ, text="15",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

choix1.place(x=50,y=50)

choix2.place(x=200,y=50)

choix3.place(x=350,y=50)

# Création des 3 Labels dropables

zoneDrop1 = Label(fen_princ, text="",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

zoneDrop2 = Label(fen_princ, text="",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

zoneDrop3 = Label(fen_princ, text="",fg="yellow", bg="black",font=("Helvetica", 20),height = 3, width=5)

zoneDrop1.place(x=50,y=250)

zoneDrop2.place(x=200,y=250)

zoneDrop3.place(x=350,y=250)

drag1 = DragManager(choix1,drag=True,drop=False)

drag2 = DragManager(choix2,drag=True,drop=False)

drag3 = DragManager(choix3,drag=True,drop=False)

drop1 = DragManager(zoneDrop1,drag=False,drop=True)

drop2 = DragManager(zoneDrop2,drag=False,drop=True)

drop3 = DragManager(zoneDrop3,drag=False,drop=True)

# Lancement de la surveillance sur la fenêtre

fen_princ.mainloop()

ATTENTION : Cette correction utilise x_root et y_root. Cela implique de que la fenêtre soit maximisée sur l'écran, ou du moins placée en haut à gauche.

Je vous laisse un dernier indice pour gérer mieux que cela la position : il faut utiliser les coordonnées relatives ET tenir compte de la position initiale du widget de base. Si vous ne trouvez pas comment placer votre icone, faites un dessin. Ca permet souvent de mieux visualiser le problème.