L’interface graphique de Scope simplifiée | Hackaday

La dernière fois, j’ai assemblé un objet Python représentant un oscilloscope Rigol. La manipulation de l’objet communique avec l’oscilloscope via le réseau. Mais mon objectif initial était de créer une petite fenêtre GUI à côté de l’interface Web de l’oscilloscope. Si j’étais resté avec C++ ou même C, j’aurais probablement simplement choisi par défaut Qt ou peut-être FLTK. J’ai également utilisé WxWidgets, et mis à part le nombre de choses « supplémentaires » que vous souhaitez, elles sont toutes assez faciles à utiliser. Cependant, j’avais écrit le code en Python, j’ai donc dû faire un choix.

Certes, bon nombre de ces boîtes à outils ont des liaisons Python – on pense à PyQt, PySide et wxPython. Cependant, le de facto Le framework GUI pour Python est Tkinter, un wrapper autour de Tk qui est relativement simple à utiliser. J’ai donc choisi d’y aller. J’ai envisagé PySimpleGUI, qui est, comme son nom l’indique, simple. Il est attrayant car il englobe tkinter, Qt, WxPython ou Remi (une autre boîte à outils), vous n’avez donc pas besoin d’en choisir un immédiatement. Cependant, j’ai décidé de rester conservateur et de rester fidèle à Tkinter. PySimpleGUI dispose cependant d’un concepteur d’interface graphique très sophistiqué.

À propos de Tkinter

La boîte à outils Tkinter vous permet de créer des widgets (comme des boutons, par exemple) et de leur attribuer un parent, comme une fenêtre ou un cadre. Il existe une fenêtre de niveau supérieur par laquelle vous commencerez probablement. Une fois que vous avez créé un widget, vous le faites apparaître dans le widget parent en utilisant l’une des trois méthodes de mise en page :

  1. Coordonnées absolues ou relatives dans le conteneur
  2. « Emballer » en haut, en bas, à gauche ou à droite du conteneur
  3. Coordonnées des lignes et des colonnes, traitant le conteneur comme une grille

La fenêtre principale est disponible depuis la méthode Tk() :

import tkinter as tk
root=tk.Tk()
root.title('Example Program')
button=tkButton(root, text="Goodbye!", command=root.destroy)
button.pack(side='left')
root.mainloop()

C’est à peu près l’exemple le plus simple. Créez un bouton et fermez le programme lorsque vous appuyez dessus. Le mainloop call gère la boucle d’événements commune dans les programmes GUI.

Widgets plus récents

Certains widgets tkinter semblent démodés, mais des variantes plus récentes peuvent automatiquement remplacer les anciennes. Les nouveaux widgets sont regroupés sous tkinter.ttk. Ces nouveaux widgets présentent de légères différences, mais la plupart de la configuration de base reste la même. Les fonctions d’apparence sont cependant différentes. Par exemple, un bouton normal utilise fg et bg pour définir les couleurs de premier plan et d’arrière-plan. Un bouton ttk utilise un système de style plus complexe à mettre en place, mais aussi plus puissant.

Il est très simple d’utiliser les nouveaux widgets. Normalement, vous importeriez toute la bibliothèque GUI avec une importation. Vous pouvez importer ttk depuis le module tkinter, puis faire référence explicitement aux widgets (par exemple, ttk.Button). Cependant, il est courant de simplement tout importer depuis tkinter, puis d’utiliser tkinter.ttk pour remplacer tout ce qui se trouve dans la bibliothèque ttk. Par exemple:

from tkinter import *
from tkinter.ttk import *

Maintenant, chaque référence à Buttonpar exemple, se résoudra à ttk.Button. Il y a 18 « nouveaux » widgets, dont certains n’apparaissent pas dans le tkinter original, comme Combobox et Treeview.

Constructeur d’interface graphique ?

J’ai commencé par chercher un bon constructeur d’interface graphique pour tkinter et je n’ai pas vraiment trouvé grand-chose. Il existe un site Web qui ne semble pas bien fonctionner (et ne gère pas ttk), un projet qui utilise un générateur d’interface graphique payant, puis traduit sa sortie en tkinter et PAGE.

PAGE n’est effectivement pas mal mais un peu décalé. Ce que je n’ai pas aimé, c’est qu’il utilise la disposition de placement ordinaire, ce qui signifie qu’un formulaire que vous concevez peut paraître mauvais sur certaines machines en raison de la taille des polices ou d’autres facteurs. Ainsi, un bouton peut être placé, disons, à 0,034 x 0,267 du coin supérieur de son conteneur. En d’autres termes, 3,4% en général et 26,7% en baisse. Cependant, si vous passez du temps avec, cela fonctionne et génère probablement un code qui semble correct plus qu’il ne parvient à s’afficher correctement.

Cependant, j’ai finalement décidé de créer l’interface graphique manuellement. Ce n’est pas si difficile. Si vous voulez une expérience simple, consultez PySimpleGUI que j’ai mentionné plus tôt. La mise en page est une liste de listes. Chaque liste est une ligne dans l’interface graphique. C’est ça. Donc: [ [ Row_1_column_1, Row_1, column_2,...],[Row_2_column_1,....],...]. C’est très simple à gérer. Mais le faire directement dans tkinter n’est pas mal non plus.

Mise en page

J’ai utilisé une classe simple pour présenter mon interface graphique et j’ai essayé de la garder plus ou moins indépendante de la portée. Il crée un objet Scope (du dernier message) et le manipule, mais il ne comprend ni les commandes ni les communications. Si vous êtes un programmeur GUI traditionnel, l’objet scope est le modèle et la classe GUI est la vue et le contrôleur.

La plupart du travail s’effectue dans le constructeur de classe. Il y a trois parties principales :

  1. Certaines variables d’état internes comme connected et le scope objet, qui, initialement, est None.
  2. La création de widgets GUI. Cela ne montre rien ; il crée simplement les objets. Cette section crée également des styles ttk à utiliser avec le bouton Exécuter/Arrêter.
  3. La dernière section organise les widgets chez leurs parents.

Vous devez vous habituer à l’idée que vous spécifiez le widget parent à l’étape 2, mais que vous définissez la position du widget à l’étape 3. Par exemple, considérez cet extrait du code :

   self.clabel=LabelFrame(self.top,text='Control')
   self.rsbtn=Button(self.clabel,text="Run/Stop",command=self.do_rs,style="GoBtn.TButton")
   . . . # create more stuff
   self.rsbtn.pack(side="left")
   . . .  # more buttons here
   self.clabel.pack(side='top',fill='both',expand='yes')
Voici à quoi ressemble cette partie de la mise en page à l’écran.

La première ligne crée un cadre étiqueté attaché à la fenêtre supérieure. Ensuite, le code crée un bouton qui est un enfant de l’étiquette. Il contient du texte, un style et une fonction à appeler lorsque vous appuyez sur le bouton.

Placer le bouton est facile. Ensuite, l’étiquette elle-même doit être insérée dans la fenêtre principale. Dans ce cas, il remonte vers le haut et remplira l’espace disponible. Il s’agrandira également si vous redimensionnez la fenêtre.

Dans la classe principale, j’utilise uniquement le pack gestionnaire de mise en page. Cependant, j’utilise également le grid gestionnaire dans un composant personnalisé. Un petit morceau de code à la fin du constructeur capture la touche Entrée afin que vous puissiez saisir une adresse IP et appuyer sur Entrée au lieu d’appuyer sur le bouton de connexion. Le code définit également le focus sur le champ de saisie. Si vous êtes un passionné de clavier, l’ordre de tabulation, par défaut, est l’ordre dans lequel vous créez les widgets, bien que vous puissiez le modifier dans le logiciel.

Si vous recherchez un tutoriel complet sur tkinter, il en existe de nombreux. TutorialPoint en a un qui se lit rapidement.

Composants personnalisés

Le pavé directionnel est un composant personnalisé de Tkinter

Pour le contrôleur d’oscilloscope, j’avais besoin de quelques pavés directionnels. Autrement dit, quatre boutons fléchés allant dans des directions différentes et un bouton central sur lequel vous pouvez appuyer. La bibliothèque tkinter n’a rien de tel, mais ce n’est pas un problème. Vous pouvez simplement le construire vous-même. Les widgets tkinter ne sont que des classes, et vous pouvez facilement les étendre pour créer vos propres variations et widgets composites.

Tout d’abord, j’avais besoin d’un petit bouton, et par paresse, j’ai décidé de créer un composant personnalisé. J’ai simplement dérivé une nouvelle classe de Button et définissez une largeur par défaut de 1 dans le constructeur. Honnêtement, j’aurais dû simplement coder en dur la largeur. Si vous souhaitez fournir une largeur, pourquoi ne pas simplement utiliser un bouton ordinaire ? Quoi qu’il en soit, voici le code complet :

# Create a tiny button
from tkinter import *
from tkinter.ttk import *

class Button1(Button):
def __init__(self, parent,text='',command=None,width=1):
   Button.__init__(self,parent,text=text,command=command,width=width)

Comme vous pouvez le constater, créer un widget personnalisé ne doit pas nécessairement être un gros problème. Normalement, une bonne classe de base pour les widgets personnalisés est Frame. Un cadre peut contenir d’autres widgets ; par défaut, il est invisible. Juste ce dont vous avez besoin. Dans ce cas, cependant, il était plus logique de personnaliser le Button classe.

j’ai utilisé Frame comme classe de base pour le pavé directionnel. Je crée des boutons qui utilisent un lambda – une fonction anonyme en ligne – pour leurs actions. Cela permet au code d’appeler facilement un seul rappel pour tous les boutons. Le rappel par défaut décompose tout en fonctions telles que up ou down.

Au premier abord, cela peut paraître fou. Pourquoi ne pas simplement attribuer la fonction directement à la touche ? La réponse est la réutilisabilité. Il existe plusieurs manières d’utiliser le composant personnalisé :

  1. Définissez un rappel autre que celui par défaut. Il s’agit d’une fonction unique pour traiter toutes les clés.
  2. Créez une nouvelle sous-classe et remplacez le rappel par défaut. Encore une fois, il s’agit d’une fonction unique pour toutes les touches.
  3. Créez une nouvelle sous-classe et remplacez chacune des fonctions de bas niveau. Cela fournit des fonctions distinctes pour chaque touche.

La mise en page est simple, utilisant le grid appelez pour définir une ligne et une colonne :

 def __init__(self,parent,callback=None):
   Frame.__init__(self,parent)
   self.callback=callback
   self.upbtn=Button1(self,text="^",command=lambda: self.press(Dpad.UP))
   self.dnbtn=Button1(self,text="V",command=lambda: self.press(Dpad.DOWN))
   self.rtbtn=Button1(self,text=">",command=lambda: self.press(Dpad.RIGHT))
   self.lfbtn=Button1(self,text="<",command=lambda: self.press(Dpad.LEFT))
   self.exebtn=Button1(self,text="*",command=lambda: self.press(Dpad.EXEC))
   self.upbtn.grid(row=0, column=1)
   self.lfbtn.grid(row=1, column=0)
   self.rtbtn.grid(row=1, column=2)
   self.dnbtn.grid(row=2,column=1)
   self.exebtn.grid(row=1,column=1)

Désormais, le code principal peut créer deux pavés de direction différents sans problème.

L’enchilada entière

Vous pouvez trouver l’intégralité du code sur GitHub. Une fois que vous avez dépassé la disposition de l’interface graphique, la majeure partie du code appelle simplement l’objet de la dernière fois qui communique réellement avec la portée.

Il y a cependant deux choses intéressantes. Étant donné que le DHO900 ne vous permet pas d’émuler les pressions sur les touches, le programme doit comprendre un peu l’état de l’appareil. Par exemple, appuyer sur le bouton Run/Stop fonctionne différemment si l’oscilloscope est déjà en cours d’exécution ou déjà arrêté. Le programme doit donc connaître l’état actuel pour envoyer la bonne commande.

Il est bien entendu possible d’interroger le scope au moment de la commande. Cependant, je voulais que le programme suive l’état périodiquement et mette à jour certains éléments de l’interface utilisateur. Par exemple, je voulais que le bouton Exécuter/Arrêter soit rouge ou vert en fonction de ce qui se passerait si vous appuyiez sur le bouton. La zone de liste déroulante du type de déclencheur doit également refléter l’état actuel du déclencheur, même si quelqu’un le modifie manuellement.

Heureusement, tkinter fournit un moyen d’ajouter un traitement de retard à la boucle d’événements en utilisant after. Le code l’appelle sur la fenêtre supérieure avec un délai en millisecondes. Lorsque le délai d’attente expire, le tick la fonction s’exécute. Pour maintenir la fonction minuterie, tick doit aussi se réarmer en appelant after encore. Avant cela, cependant, le code interroge l’état de la portée et met à jour l’interface utilisateur ainsi que certaines variables d’état dans l’objet UI.

Le programme semble bien fonctionner et devrait maintenant être beaucoup plus facile à porter vers une portée différente. Si vous ne pouvez pas le savoir, les interfaces graphiques ne sont généralement pas mon truc, même si je les construis quand il le faut. Pour des choses simples, tkinter n’est pas si mal.

Une fois que vous pouvez contrôler votre portée et en récupérer des données, vous pouvez faire de nombreuses choses amusantes. Les choses peuvent rapidement devenir incontrôlables, mais dans le bon sens.

François Zipponi
Je suis François Zipponi, éditorialiste pour le site 10-raisons.fr. J'ai commencé ma carrière de journaliste en 2004, et j'ai travaillé pour plusieurs médias français, dont le Monde et Libération. En 2016, j'ai rejoint 10-raisons.fr, un site innovant proposant des articles sous la forme « 10 raisons de... ». En tant qu'éditorialiste, je me suis engagé à fournir un contenu original et pertinent, abordant des sujets variés tels que la politique, l'économie, les sciences, l'histoire, etc. Je m'efforce de toujours traiter les sujets de façon objective et impartiale. Mes articles sont régulièrement partagés sur les réseaux sociaux et j'interviens dans des conférences et des tables rondes autour des thèmes abordés sur 10-raisons.fr.