Programmation Tcl/Vos premiers pas en Tk

Un livre de Wikilivres.

Tk - la bibliothèque graphique de Tcl


Auteur de la version initiale : Arnaud LAPREVOTE

Tk[modifier | modifier le wikicode]

Tk est le compagnon graphique de tcl. Il contient en particulier les widgets suivants :

  • bouton
  • label
  • entrée de texte
  • menu
  • photo
  • fenêtre de sélection de fichiers, de couleurs, ...
  • ...

Ce qui n'est pas directement dans Tk peut être trouvé directement dans des extensions supplémentaires, telles que Blt (widget de tracé de courbes), les Bwidgets, HTML. On commence aussi à trouver des méga-widgets directement écrits en tk (combobox, notepad, arbre).

Premier pas en Tk[modifier | modifier le wikicode]

Pour créer une interface, on :

  • définit chaque widget et ses attributs,
  • dispose les widgets dans la fenêtre (pack ou grid),
  • définit les commandes associées aux actions.

Tk gère intégralement la boucle de suivi des événements. On ne s'en préoccupe pas.

#!/usr/bin/wish

label .nouveau_label -text "Je dis juste bonjour !"
button .say_ok -text "OK ?"

pack .nouveau_label .say_ok -side top

bind .say_ok <Button-1> { exit }
bind .say_ok <Button-2> { .nouveau_label configure -text "Vous avez appuyez sur le bouton 2" }

Ou encore plus simplement :

#!/usr/bin/wish

label .nouveau_label -text "Je dis juste bonjour !"
button .say_ok -text "OK ?" -command { exit }

pack .nouveau_label .say_ok -side top

Création d'un widget[modifier | modifier le wikicode]

Le nom d'un widget est toujours : .xxxx.yyyy.zzzz Dans ce cas on crée le widget zzzz qui se trouve dans le widget yyyy qui se trouve dans le widget xxxx. xxxx pourrait parfaitement être une nouvelle fenêtre ou un widget.

#!/usr/bin/wish
toplevel .nouvelle_fenetre

label .nouvelle_fenetre.label -text "Je dis juste bonjour !"
button .nouvelle_fenetre.say_ok -text "OK ?" -command { destroy .nouvelle_fenetre }

pack .nouvelle_fenetre.label .nouvelle_fenetre.say_ok -side top

Lors de la création d'un widget, des paramètres sont initialisés. Par exemple, dans :

label .exemple_label -text "Je dis juste bonjour !"

Lors de la création du label .exemple_label, on vient initialiser son paramètre -text à la valeur "Je dis juste bonjour !".

Il est constamment possible de changer un paramètre d'un widget avec la commande configure.

Les valeurs initialisables pour un widget varient d'un widget à l'autre. Cependant il y a un certain nombre de paramètres qui sont systématiquement initialisables pour les widgets. La liste de ces options par défaut est disponible :

man options


options(n) Tk Built-In Commands options(n)


_________________________________________________________________

NAME
       options - Standard options supported by widgets
_________________________________________________________________


DESCRIPTION
       This  manual  entry  describes  the  common  configuration
       options supported by widgets in  the  Tk  toolkit.   Every
       widget  does not necessarily support every option (see the
       manual entries for individual widgets for a  list  of  the
       standard  options supported by that widget), but if a wid-
       get does support an option with one of  the  names  listed
       below,  then  the  option has exactly the effect described
       below.

       In the descriptions below, ``Command-Line Name'' refers to
.........

Modification des paramètres d'un widget existant[modifier | modifier le wikicode]

Reprenons notre petit exemple : 2 solutions existent ici. Si vous tapez directement dans wish, alors tapez :

label .exemple_label -text "Je dis juste bonjour !"
pack .exemple_label
.exemple_label configure -text "puis au revoir"

Si vous souhaitez plutôt utiliser un programme dans un fichier, alors tapez :

#!/usr/bin/wish
set wait_var 0

label .exemple_label -text "Je dis juste bonjour !"
pack .exemple_label
# Dans une seconde je changerai la variable wait_var
after 1000 { incr wait_var }
# J'attends une modification de wait_var et je suis
# dans la boucle d'événement.
vwait wait_var
# wait_var a bougé je sors de la boucle
.exemple_label configure -text "puis au revoir"
after 1000 { incr wait_var }
vwait wait_var
# Je sors de l'application
exit

Comme vous pouvez le constater configure a permis de changer la valeur d'un paramètre d'un widget. Si vous souhaitez connaître la liste de tous les paramètres d'un widget, il vous suffit de taper : .nom_du_widget configure

Si vous souhaitez connaître la valeur d'un paramètre donné, vous pouvez utiliser la commande cget :

% label .exemple_label -text "Je dis juste bonjour !"
% pack .exemple_label
% .exemple_label cget -text
Je dis juste bonjour !

Remarque Dis tonton Arnaud, tu nous as bassiné avec le fait qu'une ligne tcl consistait toujours en :

commande argument1 argument2 ...

Or je constate ici que l'on a :

nom_du_widget commande argument1 ...

Il y a tromperie sur la marchandise, remboursez !!!!!

Réponse de tonton Arnaud Que nenni !!! Lorsque l'on crée un widget, une commande portant le nom du widget est créée et configure est un argument de cette commande comme un autre.

Mort d'un widget[modifier | modifier le wikicode]

Tout à une fin. On peut détruire un widget avec la commande :

destroy .nom_du_widget

Les options standard[modifier | modifier le wikicode]

Option Signification
-background couleur de fond du widget. Ou un mot (red, ...) ou une chaîne définissant rgb avec des valeurs hexa (noir : "#00000", blanc : "#ffffff", rouge : "#ff0000")
-activebackground couleur de fond lorsque le curseur est sur le widget
-foreground la couleur du texte quand le curseur n'est pas sur le widget
-activeforeground la couleur du texte quand le curseur est sur le widget
-text le texte du bouton
-textvariable une variable dont le contenu s'affichera dans le widget
-image une image précédemment chargée
-bitmap un bitmap qui s'affichera dans le widget
-padx espace de garde à droite et à gauche du widget
-pady espace de garde au-dessus et en dessous du widget
-anchor l'élément interne au widget (texte ou graphique) sera collé à la partie haute (nord => n) basse (sud => s) droite (est => e) ou gauche (ouest => w) ou au centre (center).
-width largeur en caractères du widget
-height hauteur en caractères du widget
-justify right, left, center. Si le widget contient un texte sur plusieurs lignes, la justification choisie sera appliquée.

À vous Cognaq-Jay[modifier | modifier le wikicode]

Créez une application contenant un label, un bouton "Quitter", un bouton "Changer". Au départ, sur le label on lit :

Je dis juste bonjour

Quand on clique sur le bouton "Changer", le label devient "Vous venez de cliquer sur le bouton Changer", si on re-clique, on revient à "Je dis juste bonjour" et ainsi de suite. Si on clique sur le bouton "Quitter", l'application se termine.

Le bouton "Quitter" a un fond rouge, quand le curseur passe dessus se fond devient rose (#ff8080).

Dans une seconde version, vous garderez cette même application, ajouterez les boutons flat, groove, sunken, ridge. Lors de l'appui sur ces boutons, la propriété -relief du label changera et prendra la valeur flat, groove, sunken, ridge.

Je paye personnellement un coca-cola (virtuel) à celui qui réussira à faire ce dernier exercice avec une boucle sur la liste [list flat groove sunken ridge].

Placement des widgets[modifier | modifier le wikicode]

3 modes de placement existent :

  • pack
  • grid
  • place

Le packer :[modifier | modifier le wikicode]

frame .top 
label .top.label -text "Name" 
entry .top.name -textvariable name 
image create photo test -file /usr/src/linux/Documentation/logo.gif 
label .bottom -image test 
pack .top.label .top.name -side left 
pack .top .bottom -side top 


Les widgets sont placés en colonnes ou lignes. Les colonnes ou les lignes sont créées les unes après les autres. Pour obtenir des placements par groupe de widget on utilise des frames qui vont contenir des widgets en horizontal ou en vertical.

Le grider :[modifier | modifier le wikicode]

label .one -text "One"
entry .one_entry -textvariable one_entry
label .two -text "BIIIIIG TWO"
entry .two_entry -textvariable two_entry
label .three -text "Un très grand commentaire"
grid .one -column 1 -row 1
grid .one_entry -column 2 -row 1
grid .two -column 1 -row 2
grid .two_entry -column 2 -row 2
grid .three -column 1 -row 3 -columnspan 2 -stick e

Les widgets sont placés sur une grille (comme dans un tableau HTML pour ceux qui connaissent). La taille des colonnes est calculée automatiquement.

Le placer permet de placer les widgets en donnant directement leurs coordonnées. Je ne l'ai jamais utilisé.

Le packer dans le détail - les options de pack[modifier | modifier le wikicode]

On peut utiliser la commande pack avec les options :

 -side [left|right|top|bottom]

On choisit l'orientation (horizontal ou vertical) ainsi que l'endroit (de gauche à droite, de droite à gauche, de haut en bas, de bas en haut) dans lequel les widget sont placés.

 -fill [x|y|both|none]

Défini si les widgets packés doivent remplir complètement l'espace disponible ou non en horizontal (x) ou en vertical (y). Par défaut l'option est none.

-expand [true|false]

Lors du redimensionnement de la fenêtre, les widgets packés avec cette commande suivront l'expansion de la taille de la fenêtre.

-padx [0-9]+
-pady [0-9]+

Le nombre de pixels à droite et à gauche du widget courant sera ajouté en horizontal (padx) ou en vertical (pady) autour du widget.

-ipadx [0-9]+
-ipady [0-9]+

Le nombre de pixels nécessaire est ajouté à l'intérieur du widget à droite et à gauche (-ipady) ou au dessus et en dessous (-ipadx).

On peut faire "oublier" un widget au packer en utilisant l'option forget. Enfin, pour pouvoir bouger un widget dans tous les sens, il faut qu'il soit défini avant un autre.

Exercice[modifier | modifier le wikicode]

Votre mission si vous l'acceptez consiste à obtenir successivement les looks suivant avec le packer et 2 labels :

img/tk_ex1.gif


img/tk_ex2.gif


img/tk_ex3.gif


img/tk_ex4.gif


img/tk_ex5.gif


img/tk_ex6.gif

Pour ce faire, vous allez lancer wish et vous allez créer les 2 widgets suivants en ligne de commande :

label .lab1 -text "------------------------------lab1---------"
label .lab2 -text "lab2" -relief ridge
pack .lab1

Afin que votre cerveau ne s’autodétruise pas au bout de 15 minutes, voici quelques conseils :

  • les 5 premiers écrans s'obtiennent uniquement en faisant bouger l'un ou l'autre des widgets avec la commande pack,
  • le dernier écran s'obtient en configurant l'option -anchor du label 2.

Bien sûr, si votre mission échoue, nous nierons tout lien avec vous et peut-être même vous livrerons au côté obscure de la force dans la salle d'à côté.

Les frames[modifier | modifier le wikicode]

Les frames sont comme le sel en cuisine, il en faut des pincées, cela reste invisible, mais sans cela le plat est infect.

Imaginons que nous souhaitions avoir l'interface suivante :

Le code correspondant est le suivant :

frame .frame1 -background "#808080" -relief groove
frame .frame1.frame2 -background "#000000" -relief groove
label .frame1.frame2.lab1 -text "Ici :"
set ent1 "Entrée de texte numéro 1"
entry .frame1.frame2.ent1 -textvariable ent1 -width 20
set ent2 "Entrée de texte numéro 2"
entry .frame1.ent2 -textvariable ent2 -width 30
button .butt1 -background red -text "OK" -command {exit} 
pack .frame1.frame2.lab1 -side left
pack .frame1.frame2.ent1 -fill x -expand true -side left
pack .frame1.frame2 -side top -ipadx 5 -ipadx 5
pack .frame1.ent2 -side top -fill x -expand true
pack .butt1 -fill both -expand true -side left
pack .frame1 -fill x -expand true -ipadx 5 -ipady 5 -side left

Comme vous le voyez, les frames permettent de regrouper des ensembles de widget par ligne horizontale et verticale. Grâce à .frame1.frame2 on a groupé les widgets .frame1.frame2.lab1 et .frame1.frame2.ent1 horizontalement, puis dans .frame1 on a ajouté .ent2 et on les a empilés verticalement. Enfin on a placé .but1 et .frame2 cote à cote.

En jouant judicieusement sur les options -expand et -fill, on arrive à obtenir un comportement sophistiqué de l'application lors des redimensionnements.

Le gridder[modifier | modifier le wikicode]

Dans certains cas, le packer est vraiment assez inadapté et oblige à utiliser un nombre incroyable de frame. Des extensions à tcl sont alors apparus qui intégraient des algorithmes de placement basés sur une grille de cases.

Une grille est définie et l'on choisi le ou les cases sur lesquels un widget (ou un ensemble de widget dans une frame) vont être placés. Le gridder est plus verbeux que le packer à l'écriture parce qu'il faut une ligne pour chaque widget. Par contre, il est trivial de générer le code automatiquement.

Les options de grid (au placement ou lors d'un configure) sont les suivantes :

-column [0-9]+
-row [0-9]+

Identification de la case où le widget sera placé

-columnspan [0-9]+
-rowspan [0-9]+

Le widget est placé sur une ou plusieurs colonnes, sur une ou plusieurs lignes.

-padx
-pady
-ipadx
-ipady

Strictement identique à ces valeurs dans le packer.

-sticky [ewns]+

La manière dont le widget est "collé" aux bords. Si l'on souhaite qu'en cas de redimensionnement le widget voit sa taille augmenté, on utilise -sticky ew ou ns ou ewns.

Il est possible de fixer les attributs d'une colonne ou d'une ligne grâce à

grid columnconfigure columnindex [-minsize [0-9]+] [-weight [0-9]+] [-pad [0-9]+]
grid rowconfigure rowindex [-minsize [0-9]+] [-weight [0-9]+] [-pad [0-9]+]

Assez étrangement, les numéros de lignes et de colonnes commencent à 1 et non pas à 0.

Événements disponibles[modifier | modifier le wikicode]

On peut associer à chaque événement d'un widget des actions. Cela se fait avec la commande bind.

label .lab1 -textvariable var
bind .lab1 <Enter> { incr var }
bind .lab1 <1> { incr var 10 }
bind .lab1 <Button-2> { incr var 20 }
bind all <Key> { set var2 "%K" }
label .lab2 -textvariable var2
pack .lab2

La syntaxe de bind est :

bind [.nom_d_un_widget|all] <événement> { script à exécuter }

La syntaxe des événements est un peu particulière. En première approximation, c'est : <type_d_evenement-evenement>

type_d_evenement peut être :

Type d'événement
ButtonPress <=> Button Expose Map
ButtonRelease FocusIn Motion
Circulate FocusOut Property
Colormap Gravity Reparent
Configure KeyPress <=> Key Unmap
Destroy KeyRelease Visibility
Enter Leave Activate
Deactivate

Button et Key sont d'utilisation très courantes. Il m'est arrivé d'utiliser aussi Enter et Leave.

evenement peut être :

  • 1 : le bouton 1 de la souris,
  • 2 : le bouton 2 de la souris,
  • 3 : le bouton 3 de la souris,
  • le nom d'une touche : abcdefghij...left, right, up, down, Control_L, Control_R, Insert, Delete, ....

En ce qui concerne les touches, attention, il peut y avoir des différences de dénomination entre OS. Utilisez l'exemple plus haut pour connaître le code.

Enfin devant tout cela (type_d_evenement-evenement), il peut y avoir un modificateur. Par exemple, on peut demander à ce que la touche Ctrl soit appuyé, Alt ou Shift, ou vouloir un double clique, ou que le bouton 1 de la souris soit activé. Les autres événements me paraissent moins utiles. La liste est la suivante :

Modificateur
Control Mod2 <=> M2
Shift Mod3 <=> M3
Lock Mod4 <=> M4
Button1 <=> B1 Mod5 <=> M5
Button2 <=> B2 Meta <=> M
Button3 <=> B3 Alt
Button4 <=> B4 Double
Button5 <=> B5 Triple
Mod1 <=> M1

Enfin, dans le script associé au binding, on peut récupérer diverses informations sur l'événement grâce à des chaines du type %x :

  • %b : numéro du bouton pour les événements ButtonPress et ButtonRelease,
  • %k : le code de touche pour l'événement KeyPress ou KeyRelease,
  • %K : le code de touche sous forme de chaîne pour les 2 événements décrits plus haut,
  • %X : coordonnée horizontal de l'événement dans le widget courant (pour ButtonPress, ButtonRelease, KeyPress, KeyRelease),
  • %Y : coordonnée vertical de l'événement dans le widget courant (pour ButtonPress, ButtonRelease, KeyPress, KeyRelease).

La page de man de bind donne toutes les indications.

Liste des widgets disponibles[modifier | modifier le wikicode]

  • button, canvas, checkbutton
  • entry, frame, label
  • listbox, menu, menubutton
  • message, radiobutton, scale
  • scrollbar, text, toplevel
  • tk_getOpenFile, tk_getSaveFile, tk_chooseColor
  • tk_dialog, tk_optionMenu, ...

Les boutons[modifier | modifier le wikicode]

Pour avoir toutes les options, n'hésitez pas à vous reporter à la page de man. Les options me semblant crucialement utiles sont :

Liste des options pour le widget button
Option Signification
-background couleur de fond du bouton
-activebackground couleur de fond lorsque le curseur est sur le bouton
-text le texte du bouton
-image une image précédemment chargée s'affiche dans le bouton
-command commande(s) exécutées lors de l'appui sur le bouton
label .lab -text "Bonjour"
button .but -text OK -command {\
 .lab configure -text "J'ai appuye sur OK" }
pack .lab .but -side top

Les labels[modifier | modifier le wikicode]

Les labels sont les widgets contenant un texte non modifiable interactivement. Évidemment le texte est modifiable via programmation.

Liste des options pour le widget
Option Signification
-text le texte du bouton
-textvariable une variable dont le contenu s'affichera dans le widget

Le widget "message" est un label multiligne.

label .lab1 -text "Ceci est un label" -background \
	"#ff00ff" -foreground "#00ff00"
label .lab2 -textvariable var
set var "Ceci est un exemple"
button .but -text OK -command {
	set var "Tiens le label 2 a changé"
}
pack .lab1 .lab2 .but

Les entrées de texte[modifier | modifier le wikicode]

Liste des options pour le widget
Option Signification
-textvariable une variable dont le contenu s'affichera dans le widget
-width le nombre de caractères par défaut du widget en largeur
label .lab -textvariable var
entry .ent -textvariable var
set var "Ceci est un exemple"
pack .lab .ent

Les cases à cocher[modifier | modifier le wikicode]

Une case à cocher permet de savoir si une option est ou non sélectionnée. Cela s'utilise de la manière suivante :

checkbutton .c1 -text "Essai" -variable check1 \
 -command { puts "c1 contient $check1" }

Les options -onvalue -offvalue permettent de forcer une valeur pour la variable selon que la case est ou non cochée.

Les boutons radio[modifier | modifier le wikicode]

Les boutons radio sont des cases à cocher dont une seule peut-être active à la fois.

radiobutton .r1 -text "Tout" \
-variable test -value "tout" -anchor w
radiobutton .r2 -text "Rien" \
-variable test -value "rien" -anchor w
.r1 select
.r2 invoke
.r2 deselect
.r1 toggle

Les menus[modifier | modifier le wikicode]

Pour créer un menu avec des listes déroulantes, on utilise simplement une frame (-relief raised), avec à l'intérieur des widget "menubutton". À chaque menubutton on associe un widget fils menu. Ce menu est composé d'entrée de type command (avec option -label, -command), de type radiobutton (option -label, -command, -variable et -value) de type checkbutton (-label -command -variable -onvalue -offvalue) ou de type cascade (option -label -cascade) enchaînant vers un autre menu.

L'option -accelerator permet d'associer une touche d'accélération pour activer l'entrée de menu correspondante.

Il existe aussi des menus d'options : tk_optionMenu

tk_optionMenu .nom_du_widget variable_global \
elt1_du_menu elt2 elt3 ...

Ainsi que des menus pop-up.

set filetype text
menubutton .file \
        -text "File" -menu .file.menu
pack .file -side left
menu .file.menu 
.file.menu add command \
        -label "Nouveau" \
        -command { puts "New" } 
.file.menu add command \
        -label "Ouvrir..." \
        -command { puts "Open..." } 
.file.menu add separator
.file.menu add radiobutton \
	-label "Graphique" -variable filetype \
	-value "graphic" -command { puts $filetype }
.file.menu add radiobutton \
	-label "Texte" -variable filetype \
	-value "text" -command { puts $filetype }
.file.menu add separator
.file.menu add checkbutton \
	-label "Fichier rw seulement" \
	-variable rwfile -onvalue on \
	-offvalue off -command { puts $rwfile }
.file.menu add separator
.file.menu add cascade \
	-label "Autre menu" -menu .file.menu.sousmenu
.file.menu add command \
        -label "Exit" \
        -command { exit }

menu .file.menu.sousmenu

.file.menu.sousmenu add command \
	-label "Action 1" \
	-command { puts "Sous-menu action 1" }

.file.menu.sousmenu add command \
	-label "Action 2" \
	-command { puts "Sous-menu action 2" }

Les images[modifier | modifier le wikicode]

Comme nous l'avons vu précédemment, il est possible d'associer un bitmap (noir et blanc) ou une photo à un widget (bouton, label).

La procédure est simple :

  • on crée l'image de type photo (couleur) ou bitmap (pixel noir ou blanc),
  • on associe la photo créée au widget.

Prenons le cas de la photo :

image create photo toto_photo -file nom_d_un_fichiergifoujpeg

Il ne reste plus qu'à associer la photo à un label

label .lab1 -image toto_photo

Il est évidemment possible de changer de photo par exemple :

toto_photo configure -file !nom_dunautrefichier.jpg

Vous pouvez vous reporter au man de la commande image pour voir les commandes disponibles.

Interface graphique contre grosse fatigue[modifier | modifier le wikicode]

Nous allons refaire un joyeux exercice, mais cette fois-ci vous allez pouvoir utiliser un builder graphique d'application. Il en existe 3 pour tk.

Le plus ancien est SpecTcl (http://wuarchive.wustl.edu/languages/tcl/SpecTcl/). À sa création par les laboratoires de Sun, s'était un produit commercial. Ils l'ont rapidement mis dans le domaine public et abandonné le développement. Le résultat est un excellent produit stable et facile à utiliser mais qui n'évolue plus (et c'est bien dommage). C'est ce que j'ai utilisé au départ.

L'inconvénient de specTcl est qu'il faut très régulièrement retoucher le code généré. À partir du moment où on touche le code généré par un builder, on ne peut plus du tout continuer à utiliser l'interface graphique et cela perd de son intérêt.

Nouveau : le développement de specTcl a repris. Il se trouve maintenant sur sourceforge.net.

tkBuilder que l'on peut trouver sur http://scp.on.ca/sawpit, est un outil moins graphique que specTcl, mais très pratique pour le programmeur. C'est un excellent support graphique à la programmation. Grâce à la possibilité de rajouter du code manuel un peu partout, il est très facile de ne pas avoir à modifier le code.

Enfin, Visual Tcl est un logiciel constamment prometteur, certainement le plus ambitieux des 3 (le plus proche de VB ?). On le trouve sur :

http://vtcl.sourceforge.com

Malheureusement, à chaque fois que je l'utilise, il me claque entre les mains. Je vous propose malgré tout de voir si la dernière version (1.6alpha) tient ou non la route.

Je vous propose de charger l'un des 3 et de faire les exercices suivants avec une interface de construction. À charge pour vous de lire le manuel.

Vous n'allez pas rigoler[modifier | modifier le wikicode]

Votre but est de créer l'application suivante :

On donne en haut le nom d'un fichier gif ou jpeg et quand on appuie sur le bouton Ouvrir cette image est affichée dans le label central. Bien sûr, on peut répéter l'opération d'ouverture à chaque fois.

On peut choisir le nom de fichier ou en le tapant dans l'entrée de texte du haut ou en cliquant sur le bouton à côté de l'entrée de texte qui permet d'appeler le widget tk_getOpenFile. On utilise cette fonction de la manière suivante :

set filename [tk_getOpenFile -filetypes \
 [list [list "Fichiers graphique" {.gif .jpg .jpeg}] \
 [list "Tous fichiers" {*}]]]

tk_getOpenFile est un widget standard de tcl ayant un manuel.

J'ai utilisé tkBuilder pour créer cette application : img/tkbuilder.gif mais n'hésitez pas à utiliser un autre builder.

Bibliographie[modifier | modifier le wikicode]

"Graphical Applications with Tcl&Tk" de Eric F. Johnson, M&T Books, ISBN 1-55851-471-6 : le livre avec lequel j'ai appris le tcl/tk. Pas complet, mais très pédagogique et agréable à lire.