Outils pour utilisateurs

Outils du site


issue136:python

A little while ago, I wrote a blog article about a problem with Python and Tkinter. It has to do with the Checkbox widget not responding properly to the click event, at least in my mind. Here is how it all came about. I was working on a custom widget for use with a database that implemented a scrolled checked list box. I figured that it would be helpful to be able to show a series of selected items from a list of categories on a per-record basis. As normal, I was using Page to create the GUI. I wanted the widget to raise an event whenever any of the checkboxes in the widget were clicked, then display a list of all the selected items. It didn’t really seem like a horribly daunting task at the time. However, when the event fired, and my callback code ran, the last checkbox that was clicked wasn’t on the list. To make matters worse, if I clicked on the same button again, basically unchecking the button, that button THEN showed up on the list. Now that didn’t make any sense to me. I started digging and found the problem.

Il y a quelque temps, j'ai écrit un billet de blog au sujet d'un problème avec Python et Tkinter, car le widget des cases à cocher ne répondait pas comme il fallait à l'événement d'un clic, du moins selon moi. Voici l'histoire :

Je travaillais sur un widget personnalisé qui implémentait une fenêtre avec une liste cochée défilante pour l'utiliser avec une base de données. Je pensais qu'il serait utile de pouvoir afficher une série d'éléments sélectionnés à partir d'une liste de catégories basée sur un seul enregistrement. Comme d'habitude, j'utilisais Page pour créer l'interface graphique.

Je voulais que le widget présente un événement chaque fois que n'importe laquelle des cases du widget était cochée, puis affiche une liste de tous les éléments sélectionnés. Sur le moment, cela ne semblait pas si difficile que cela. Cependant, quand l'événement était déclenché et mon code de rappel se lançait, la dernière case à cocher qui était sélectionnée n'apparaissait pas sur la liste. Pire encore, si je cliquais sur le même bouton à nouveau, décochant le bouton en fait, c'est alors que ce bouton apparaissait sur la liste. Cela ne me paraissait pas du tout logique. J'ai commencé des recherches et j'ai trouvé le problème.

As you probably know, the Checkbutton widget looks just like any GUI checkbutton. It’s an empty box that, when you click it, it shows a check in the box. If you click it again, the check goes away and the box is blank again. Under Tkinter, the Checkbutton widget provides a variable that you can monitor to show the state of the widget by using the .get() method of the widget. If it returns a “1”, then the checkbutton is checked, and if it returns a “0”, it’s unchecked. Pretty simple. I wanted to use the mouse-click event to be the trigger to check the state of the widget. Most of my work these days in under Linux, so that’s the operating system that I did the base work under. I tried various workarounds and still couldn’t get the silly thing to work the way it should. So, I went back to the basics. I created a simple GUI using Page and started exploring. It looked something like this… Nothing spectacular, but it would help with my snooping. If you have used Page before, you know that you usually bind an event (either mouse or keyboard) to a particular widget. In this case, I set the binding to the <Button-1> event (left mouse button click) for the Checkbutton widget. Then, in the callback function for the event, I printed out the state of the Checkbutton. The code (below) was pretty simple…

Comme vous le savez sans doute, le widget Checkbutton (case à cocher) ressemble à n'importe quelle case à cocher d'une interface graphique. C'est un carré vide qui, quand vous cliquez dessus, affiche une coche dans le carré. Si vous cliquez dessus à nouveau, la coche disparaît et le carré est vide à nouveau. Dans Tkinter, le widget Checkbutton fournit une variable, que vous pouvez surveiller, pour montrer l'état du widget en utilisant la méthode .get() du widget. S'il donne un « 1 », la case est cochée et s'il donne un « 0 », elle est vide. C'est simple. Je voulais me servir de l'événement du clic de la souris pour déclencher la vérification de l'état du widget.

Ces jours-ci, je travaille principalement sous Linux ; aussi, c'est le système d'exploitation que j'ai utilisé pour faire le travail de base. J'ai essayé divers contournements, sans pouvoir convaincre le truc de fonctionner comme il devrait. Je suis donc retourné aux bases. J'ai créé une interface graphique simple avec Page et commencé mes explorations.

Ça ressemblait un peu à ceci…

Rien de spectaculaire, mais elle m'aiderait dans mes investigations. Si vous avez déjà utilisé Page, vous savez que, habituellement, vous liez un événement (soit de la souris, soit du clavier) à un widget précis. Ici, pour le widget Checkbutton, j'ai réglé le lien à l'événement <Button-1> (clic sur le bouton gauche de la souris). Puis dans la fonction rappel de l'événement, j'ai imprimé l'état du Checkbutton. Le code (ci-dessous) était assez simple…

The first two lines are provided by Page as a simple notification that the user has triggered the callback function. I usually leave these in until I’m done with the majority of the early testing. The last line of the function simply shows a ‘1’ or a ‘0’ in the terminal window, to show me what is going on. The thought being that when I click to check the button, I will see a 1 in the terminal. However, what I saw when I ran the program and clicked the widget, was… chkbtntest_support.on_ChkBtnClick Now, I knew that I didn’t initialize the checkbutton, so the state was undetermined at startup, but the check showed on the widget, so the state should have been ‘1’. It wasn’t. I clicked it again and the check went away as expected, but the terminal showed: chkbtntest_support.on_ChkBtnClick 1

Les deux premières lignes sont fournies par Page comme une notification simple du fait que l'utilisateur a déclenché la fonction de rappel. En général, je les laisse jusqu'à ce que j'aie terminé la plupart des premiers tests. La dernière ligne de la fonction affiche tout simplement un « 1 » ou un « 0 » dans la fenêtre du terminal pour m'indiquer ce qui se passe. L'idée étant que, lors d'un clic pour cocher le bouton, je verrai un 1 dans le terminal.

Toutefois, quand j'ai exécuté le programme et cliqué sur le widget, ce qui j'ai vu était…

chkbtntest_support.on_ChkBtnClick

Et pourtant, je savais que je n'avais pas initialisé le checkbutton ; ainsi, l'état n'était pas connu au départ, mais la coche s'affichait sur le widget et l'état aurait dû être « 1 ». Ce n'était pas le cas. J'ai cliqué dessus à nouveau et la coche a disparu, comme je m'y attendais, mais le terminal affichait :

chkbtntest_support.on_ChkBtnClick

1

It didn’t make any sense at all. The check value is 180 degrees out of sync with the visual indicator, just like in my custom widget. I’ve used the Checkbutton widget before, many times, and used the .get() method to query the status of that widget, but always from another event like a ‘Save’ button or something like that. Frustrated to the MAX, I decided out of desperation to try the <ButtonRelease-1> event and see what happened then. I disabled the click event, and bound the other event to the widget. I wrote the same code for this event, and, low and behold, it worked properly. Everytime I let up on the mouse button, the state was queried and printed to the terminal correctly! This made me feel much better, since now I have a workaround at least. I modified the code for my custom widget, and it still worked, so I was a happy camper. I even tried the code under both Python 2.x and 3.x and it worked as I wanted. However, this elation was only to be short lived. I took my code over to a Windows 10 machine and started it up. Knowing that I had gotten it all figured out, I knew it would work just fine, since the code is all very basic (forgive the term) and nothing would go wrong.

Ce n'était pas du tout logique. La valeur n'est pas du tout synchronisée avec l'indicateur visuel, tout comme dans mon widget personnalisé. Je me suis déjà servi du widget Checkbutton, de très nombreuses fois, et j'ai utilisé la méthode .get() pour demander le status du widget, mais toujours à partir d'un autre événement, notamment d'un bouton « Enregistrer ».

Ma frustration étant MAXIMALE, j'ai décidé par désespoir d'essayer l'événement <ButtonRelease-1> pour en voir le résultat. Ayant désactivé l'événement clic, j'ai lié l'autre événement au widget. J'ai écrit le même code pour cet événement et, à ma grande surprise, il a fonctionné comme il fallait. Chaque fois que j'ai lâché le bouton de la souris, l'état fut demandé et affiché correctement sur le terminal !

Puisque j'avais au moins un contournement, je me sentais bien mieux. J'ai modifié le code de mon widget personnalisé et ça fonctionnait encore ; c'était parfait et j'étais rassuré. J'ai même essayé le code dans Python 2.x et dans Python 3.x et il fonctionnait comme je voulais. Cependant, ma joie fut de courte durée.

J'ai transféré le code sur une machine sous Windows 10 et l'ai démarré. Sachant que j'avais tout compris et rectifié, j'étais certain que cela fonctionnerait bien, puisque le code était très basique (BASIC - désolé) et rien de fâcheux ne pouvait arriver.

Much to my surprise, it couldn’t have gone worse. Not only did the capture of the widget state not work on the mouse release event, it didn’t work using the mouse down event. I could query the state from a different “standard” button AFTER I had clicked the Checkbutton, but I couldn’t get a “live update” to save my life. Nothing that I tried worked. Eventually I came up with a very “dirty” workaround for it under Windows and basically called it a day. I still wanted to find out what was going on, but other projects came up so that project was left for a while. Then, one day, I got some free time and decide to enhance the custom widget to support the scroll wheel. The scroll bar worked with the scroll wheel, but, if you tried to scroll from the center of the widget, nothing happened. That was so counterintuitive that I couldn’t let it go. After a bunch of research, I found a way to do it. Unfortunately, Linux handles the mouse scroll wheel differently than Windows did, so I was back wrapping code based on the operating system. I tried again to capture the Checkbutton state in “real time”, but I didn’t get any further. Once again, I put the code off to the side with the plan to get back to it “some day”. The other day, I get an email from Don Rozenberg, who is the author of Page. Among other things, he said “Under Linux Mint, pressing and holding button-1 on one of the checkboxes causes it to toggle, whereas under Windows the checkbox doesn't toggle until the button-1 is released.”

À ma très grande surprise, cela n'aurait pas pu être pire. La détection de l'état du widget ne fonctionnait pas sur l'événement de la relâche du bouton de la souris et elle ne fonctionnait pas non plus sur l'événement du clic. Je pouvais demander l'état à partir d'un autre bouton « standard » APRÈS a avoir cliqué sur le Checkbutton, mais je n'arrivais pas à avoir une « mise à jour en temps réel », même en cas de vie ou de mort. Rien de ce que j'ai essayé n'a fonctionné.

Ayant trouvé après maints efforts un très « sale » contournement sous Windows, j'ai décidé de m'arrêter là. Je voulais toujours savoir ce qui se passait, mais d'autres projets se sont présentés et j'ai donc laissé celui-là de côté pendant un certain temps. Un jour, ayant un peu de temps, j'ai décidé d'améliorer le widget personnalisé pour qu'il prenne en charge la molette de défilement. La barre de défilement fonctionnait avec la molette, mais, si j'essayais de faire défiler la fenêtre à partir du centre du widget, rien ne se passait. C'était tellement contre-intuitif que je ne pouvais pas l'abandonner. Après beaucoup de recherches, j'ai trouvé une façon de le faire. Malheureusement, Linux gère la molette de défilement de la souris d'une façon différente de Windows et je me retrouvais en train de créer du code basé sur le système d'exploitation. J'ai à nouveau essayé de récupérer l'état du Checkbutton en « temps réel », mais je n'ai pas du tout progressé. J'ai encore une fois mis le code de côté en pensant y retourner « un jour ».

L'autre jour j'ai reçu un mail de Don Rozenberg, l'auteur de Page. Il a notamment dit : « Sous Linux Mint, appuyer sur, et maintenir, le button-1 sur l'une des cases à cocher, le fait basculer, alors que, sous Windows, la case ne bascule pas avant que le button-1 ne soit relâché. »

I decided to go back to basics and try to figure out, once and for all, a way to monitor the check state correctly for both operating systems. I recreated my simple Checkbutton test GUI app in Page and was in the middle of doing the bindings when I realized that I had forgotten that the Checkbutton widget also has a command attribute that you can use to respond to the mouse events. The main reason that I don’t use it more is that you can’t pass the event object into the callback function, which many times is useful to have since it includes the mouse position. This time, however, it isn’t needed, so I added it to the mix. So now I had callback functions for mouse down, mouse up, and the command event. On the following page (top right) is the code. This allowed me to see what happened whenever the mouse got clicked in every way. I started under Linux, since I thought I knew what should be happening. When I ran the program, I got the following (next page, bottom right) in the terminal window…

J'ai décidé de revenir aux bases pour essayer de trouver une fois pour toute une façon de surveiller correctement l'état de la coche pour les deux systèmes d'exploitation.

J'ai recréé mon appli de test de Checkbutton en interface graphique avec Page, et j'étais en train de coder les liens quand je me suis rendu compte que j'avais oublié que le widget Checkbutton a également un attribut de commande que vous pouvez utiliser pour répondre aux événements de la souris. La raison principale pour laquelle je ne l'utilise plus est que vous ne pouvez pas passer l'objet événement dans la fonction de rappel, qui pourtant peut être extrêmement utile puisqu'il comprend la position de la souris. Cette fois-ci, cependant, il n'y en avait pas besoin et j'ai décidé donc de l'ajouter à l'ensemble. Comme ça, j'avais les fonctions de rappel pour souris enfoncée, souris lâchée et l'événement Commande. Le code se trouve à la page suivante (en haut à droite).

Cela m'a permis de voir ce qui se passait à chaque clic sur la souris, quelle que soit la façon dont c'était cliqué. Puisque je pensais savoir ce qui devrait arriver, j'ai commencé sous Linux. Quand j'ai lancé le programme, la fenêtre du terminal affichait ce qui suit (à la page suivante, en bas à droite)…

That was what I wanted to see. The mouse-down event came first, then the command callback was fired, and finally the mouse-up fired. Good. So, I bundled up my app and booted up the Windows machine. I copied the code to a folder and ran it under Python. The form came up correctly and I clicked the Checkbutton to set it to Checked. I fully expected to see pretty much the same thing, except that I wouldn’t see the correct state for the button just like I did before. Much to my surprise, this is what I got: chkbtntest_support.on_ChkBtnClick Unchecked chkbtntest_support.on_ChkBtnRelease Unchecked chkbtntest_support.on_ChkBtnCommand Checked

C'était ce que je voulais voir. L'événement appui-sur-la-souris venait en premier, puis la commande de rappel fut déclenché et, enfin, la souris fut relâchée. Bon. J'ai donc pris mon appli et démarré la machine sous Windows. J'ai copié le code vers un dossier et l'ai exécuté dans Python. Le formulaire s'est affiché correctement et j'ai cliqué sur Checkbutton pour le régler à Checked (coché).

Je m'attendais vraiment à voir à peu près la même chose, sauf que je ne verrais pas l'état correct du bouton, tout comme ce qui arrivait auparavant. À ma surprise, voici ce qui s'est affiché :

chkbtntest_support.on_ChkBtnClick Unchecked

chkbtntest_support.on_ChkBtnRelease Unchecked

chkbtntest_support.on_ChkBtnCommand Checked

Under Windows, the mouse up event fires before the command event. AND the command event can query the state of the Checkbutton. Just to make sure, I clicked it again to uncheck it and sure enough I got this: chkbtntest_support.on_ChkBtnClick Checked chkbtntest_support.on_ChkBtnRelease Checked chkbtntest_support.on_ChkBtnCommand Unchecked So now I know that under Windows, the command event happens AFTER the mouse-up event, and actually does correctly follow the state of the widget. What this means for someone who uses Python and Tkinter under the Mac operating system, I have no idea, but I am guessing that it will pretty much follow the Linux results. I intend to dig a bit further into this to see exactly where the “problem” is and then try to figure who I should report this to. Just goes to show, don’t give up if, the first hundred times, things don’t go the way you expect them to. Until next time, have a GREAT month.

Sous Windows, l'événement souris relâchée se déclenche avant l'événement commande. Et l'événement commande peut demander l'état du Checkbutton. Pour m'en assurer, j'ai cliqué dessus à nouveau pour le décocher et effectivement, ceci s'est affiché :

chkbtntest_support.on_ChkBtnClick Checked

chkbtntest_support.on_ChkBtnRelease Checked

chkbtntest_support.on_ChkBtnCommand Unchecked

Ainsi, je sais maintenant que, sous Windows, l'événement commande arrive APRÈS l'événement souris relâchée et, en fait, suit correctement l'état du widget.

Je n'ai aucune idée de ce que cela signifie pour quelqu'un qui utilise Python et Tkinter sous le système d'exploitation de Mac, mais je suppose que cela donne à peu près les mêmes résultats que sous Linux.

J'ai l'intention de faire des recherches plus approfondies pour cerner l'origine du « problème », puis essayer de trouver la personne à laquelle il faudrait que j'envoie mon rapport de bogue.

Cela montre bien qu'il faut pas abandonner si, les cent premières fois, les choses ne se passent pas comme vous y attendiez.

À la prochaine fois. Que votre mois soit GÉNIAL !

issue136/python.txt · Dernière modification : 2018/09/17 16:06 de andre_domenech