Ze "minimal code"

Avant de commencer il faut un code minimum. Le voici:

import tkinter as tk
 
class tkinterTuto(tk.Frame):
	def __init__(self):
		tk.Frame.__init__(self)
 
if __name__ =='__main__':
	tkinterTuto().mainloop()

L'initialisation de tkinter est subtile et ce serait vous mentir si je vous disais que je sais exactement ce que fait Python avec ce code. Je vais tout de même vous expliquer du mieux que je peut chacune des lignes de ce code minimum.

import tkinter as tk

Ici, pas de soucis, c'est la ligne qui import le module tkinter. "as tk" est un moyen d'abréger le code. Au lieu de taper "tkinter" devant chaque fonction du module, nous pourront abréger par "tk".

class tkinterTuto(tk.Frame):

C'est la déclaration de la classe principale qui va contenir l'interface. Pour le "tk.Frame" il semblerait que ce soit obligatoire et que ça fasse partie du fonctionnement de tkinter. Il s'agit surement du frame principale qui va contenir tout les autres.

def __init__(self):

Cette procédure (init()) est la procédure qui sera exécuté en premier, à l'initialisation de la classe (C'est le "constructeur").

tk.Frame.__init__(self)

Alors celle la je pige rien! Je suppose qu'il initialise la frame principal. Mais je comprends pas car il reçois déjà une frame à la création de la classe. Bref, si quelqu'un est capable de m'expliquer exactement pourquoi, au final, il semble que nous ayons deux frames (celle en argument puis celle là) je suis preneurs! :seSentCon:

if __name__ =='__main__':
	tkinterTuto().mainloop()

Ces deux lignes permettent de vérifier qu'on a lancer ce fichier (et non pas importé par exemple), et "exécute" la classe.

Nous avons donc le programme minimum:

debutTkinter001.png

Part en thèse

J'ajoute aussi des lignes qui permettent d'aligner la taille des widgets en fonction de la taille de la fenêtres. L'ajout de ses lignes n'est pas indispensable pour finir la première partie du tutorial mais elles le seront ensuite, lors ce que nous parlerons du "resizing". Vous pouvez essayer de faire sans si vous n'êtes intéressé par la seconde partie.

class tkinterTuto(tk.Frame):
	def __init__(self):
		tk.Frame.__init__(self)
		self.master.columnconfigure(0, weight=1)
		self.master.rowconfigure(0, weight=1)
		self.columnconfigure(0, weight=1)
		self.rowconfigure(0, weight=1)
		self.grid(sticky="NSEW")

Quelques mots quand même: "columnconfigure" et "rowconfigure" permettent de configurer respectivement les "rows" (lignes) et les "columns" (collones). Le premier chiffre est l'index et le second, sont "poids". (Pour l'instant, ses lignes n'ont pas d'effet). La dernière ligne indique à quoi le widget en question (ici, la fenêtre principale) va "s'accrocher". NSEW indique: Nord-Sud-Est-West. Vous indiquez donc les "poles" que vous voulez connecter au bordures (Vous n'êtes pas obligé de toute les mettre). Vous avez plusieurs façon de l'écrire suivant la façon dont est importé le module tkinter:

import tkinter
	[...]
	self.grid(sticky=tkinter.N+tkinter.S+tkinter.E+tkinter.W)
import tkinter as tk
	[...]
	self.grid(sticky=tk.N+tk.S+tk.E+tk.W)
import tkinter as *
	[...]
	self.grid(sticky=N+S+E+W)

Mais la technique qui marche tout le temps est celle que j'ai fait, directement en string.

Bon, après ses explications foirasses approximatives, attaquons nous à la parti intéressante:

Donner un titre

C'est le premier truc qui fait qu'on commence à s'approprier un programme (Ok les puristes, je sais qu'on parle de script...). Vous ne pouvez pas deviner la méthode, elle est donné dans la doc (qui, comme je l'ai écrit avant, est un peu moyenne).

self.master.title("tkinterTuto")
class tkinterTuto(tk.Frame):
	def __init__(self):
		tk.Frame.__init__(self)
		self.master.title("tkinterTuto")	#Ça, c'est du nom!
		self.master.columnconfigure(0, weight=1)
		self.master.rowconfigure(0, weight=1)
		self.columnconfigure(0, weight=1)
		self.rowconfigure(0, weight=1)
		self.grid(sticky="NSEW")

debutTkinter002.png

La procédure "qui-créé-les-choses"

Maintenant que nous avons un titre, nous allons créé une seconde procédure (createWidgets) qui va nous servir à créer les éléments de l'interface. Une fois celle ci créer, nous la lançons à partir de la précédure init:

class tkinterTuto(tk.Frame):
	def __init__(self):
		tk.Frame.__init__(self)
		[...]
		self.createWidgets()	#lance la procédure qui créer les widgets
 
	def createWidgets(self):
		mainFrame = tk.Frame(self, borderwidth=2, relief="groove")
		mainFrame.grid(column=0, row=0, sticky="NSEW")

Les explication:

mainFrame = tk.Frame(self, borderwidth=2, relief="groove")

Créé un Frame, qui a comme parent "self" (le widget principal), qui a deux pixels de bordure et un relief de type groove (Vous pouvez regarder les différents types de relief).

mainFrame.grid(column=0, row=0, sticky="NSEW")

cette ligne est importante... En effet, nous avons créer le widget mais ne lui avons donné aucun emplacement... Cette ligne s'en charge. "column" et "row" définisse les index de la grille dans laquelle nous "incrustons" les widgets. "sticky" est, comme tout à l'heure, pour accrocher le widget sur les bords de sa grille.

debutTkinter003.png

Nous avons un joli petit effet de bordure. Vous pouvez tester, la fenêtre est resizeable (C'est nos petites lignes de tout à l'heure qui font cet effet).

Dévidons le vide...

C'est pas le tout mais notre fenêtre reste bien vide... :hehe:

Nous allons la remplir avec le widget le plus simple (Je crois...), le "Label".

Le "Label" c'est quoi? Et bien c'est tout simplement un texte qui se loge dans l'interface...

def createWidgets(self):
		#creation des widgets
		mainFrame = tk.Frame(self, borderwidth=2, relief="groove")
		valeurOneLabel = tk.Label(mainFrame, text="ValeurOne")
 
		#position des widgets
		mainFrame.grid(column=0, row=0, sticky="NSEW")
		valeurOneLabel.grid(column=0, row=0, sticky="EW")
valeurOneLabel = tk.Label(mainFrame, text="ValeurOne")

Le premier argument est, comme toujours, le parent du widget (mainFrame), pour le second je vous épargne les explications...

valeurOneLabel.grid(column=0, row=0, sticky="EW")

Comme pour tout à l'heure, nous plaçons le widget dans la grille. Vous aurez remarqué qu'à priori "mainFrame" et "valeurOneLabel" sont placé au même endroit mais si vous regardez leurs parents respectif, vous verrez qu'il n'ont pas les mêmes... Et oui! Chaque frame à sa propre grille.

debutTkinter004.png

Toc! Toc!... Entry?

Nous allons maintenant ajouter notre second widget: Entry!

Entry est un champ de texte dans lequel l'utilisateur peut taper... Du texte... Mais c'est surtout un widget qui utilise la classe variable! :hehe:

def createWidgets(self):
		#creation des objets variables
		self.valeurOneVar = tk.StringVar()
		self.valeurOneVar.set("Ecrivez ici")
 
		#creation des widgets
		mainFrame = tk.Frame(self, borderwidth=2, relief="groove")
		valeurOneLabel = tk.Label(mainFrame, text="ValeurOne")
		valeurOneEntry = tk.Entry(mainFrame, textvariable=self.valeurOneVar)
 
		#position des widgets
		mainFrame.grid(column=0, row=0, sticky="NSEW")
		valeurOneLabel.grid(column=0, row=0, sticky="EW")
		valeurOneEntry.grid(column=1, row=0, sticky="EW")

Les explications:

self.valeurOneVar = tk.StringVar()

La première ligne créé un objet de type StringVar qui va stocker une variable de type... String!

self.valeurOneVar.set("Ecrivez ici")

Cette seconde ligne montre comment nous stockons les informations dans cet objet. Il faut imaginer que nous avons affaire à un objet qui contient une variable et non à une variable "classique". On y stocke avec ".set" et récupère avec ".get".

valeurOneEntry = tk.Entry(mainFrame, textvariable=self.valeurOneVar)

Vous avez remarquez? Nous avons ajouter l'argument "textvariable" et lui avons donné notre objet-variable. Ainsi quand nous modifierons "valeurOneEntry", nous modifierons directement la variable! Simple non? :sourit:

Vous remarquerez le "self" devant "valeurOneEntry". En si nous ne l'avions pas fait, "valeurOneEntry" aurait été supprimé dès la fin de la procédure "createWidgets". En ajoutant le nom du parent devant une variable (Ici, self renvois au parent de la procédure qui est la classe tout simplement), nous disons à l'interpréteur de créer cette variable au niveau de ce parent (Ainsi, notre variable sera présente dans toute la classe). Cela évite (comme en C par exemple :siffle: ) de devoir initialiser une variable (que l'on souhaite conserver) avant d'entrer dans une procédure.

debutTkinter005.png

Les python, ça me donne des boutons...

Maintenant nous allons ajouter un bouton qui réagit au clique. C'est toujours le même principe:

def createWidgets(self):
		#creation des variables
		self.valeurOneVar = tk.StringVar()
		self.valeurOneVar.set("Ecrivez ici")
 
		#creation des widgets
		mainFrame = tk.Frame(self, borderwidth=2, relief="groove")
		valeurOneLabel = tk.Label(mainFrame, text="ValeurOne")
		valeurOneEntry = tk.Entry(mainFrame, textvariable=self.valeurOneVar)
		button = tk.Button(mainFrame, text="Click Me!")
 
		#position des widgets
		mainFrame.grid(column=0, row=0, sticky="NSEW")
		valeurOneLabel.grid(column=0, row=0, sticky="EW")
		valeurOneEntry.grid(column=1, row=0, sticky="EW")
		button.grid(column=0, columnspan=2, row=1, sticky="NSEW")

Explications:

button = tk.Button(mainFrame, text="Click Me!")

Notre bouton, son parent est "mainFrame", et son texte est "Click Me!"...

button.grid(column=0, columnspan=2, row=1, sticky="NSEW")

Et on le place dans l'interface! Vous remarquerez l'argument "columnspan". Celui ci permet de "manger" un certain nombre de colonne (comme en html pour ceux qui ont eu "la chance" de faire des sites en tableau :aupoil: )

debutTkinter006.png

On vois que malgré que le bouton soit placé dans la colonne zéro, il utilise deux colonnes.

Hop hop hop! Passons à l'étape suivante! :marioCours:

Faut que ça réagisse!

Bien! Maintenant que notre interface est prête, nous allons commencer à la faire "réagire"...

Pour cela, il faut lui indiquer quoi executer... En l'occurrence, une procédure. Créons dans un premier temps une procédure toute simple:

def printSomething(self):
	print("something")

Cette procédure ne fait qu'écrire "something" dans la console...

Il faut maintenant dire au bouton de l'exécuter quand on clique dessus:

button = tk.Button(mainFrame, text="Click Me!", command=self.printSomething)

debutTkinter007.png

L'objectif maintenant est de pouvoir récupérer, dans notre procédure "printSomething" le contenu de la variable, rien de plus simple! Ça s fait grace au .get:

def printSomething(self):
		print(self.valeurOneVar.get())

debutTkinter008.png

Ça avance, ça avance!

Ce qui peut être interessant, c'est de faire intervenir cette variable dans l'interface.

La première fois où j'ai eu à faire un changement de valeur de texte dans l'interface, je ne me suis pas servi de la classe variable mais je fesai tout avec des events... Pour chaque clique je fesai une modification... :baffed:

Grace aux class variable c'est super simple! (On ne se moque pas les développeurs, ça peut sembler évident mais je n'ai pas encore une approche object-oriented très avancé et je découvre tkinter en fesant des relations entre la doc officiel des commandes tk, et la syntaxe Python)

Donc voici la méthode:

def createWidgets(self):
		#creation des variables
		self.valeurOneVar = tk.StringVar()
		self.valeurOneVar.set("Ecrivez ici")
 
		#creation des widgets
		mainFrame = tk.Frame(self, borderwidth=2, relief="groove")
		valeurOneLabel = tk.Label(mainFrame, text="ValeurOne")
		valeurOneEntry = tk.Entry(mainFrame, textvariable=self.valeurOneVar)
		button = tk.Button(mainFrame, text="Click Me!", command=self.printSomething)
		valeurTwoLabel = tk.Label(mainFrame, text="ValeurTwo", textvariable=self.valeurOneVar)
 
		#position des widgets
		mainFrame.grid(column=0, row=0, sticky="NSEW")
		valeurOneLabel.grid(column=0, row=0, sticky="EW")
		valeurOneEntry.grid(column=1, row=0, sticky="EW")
		button.grid(column=0, columnspan=2, row=1, sticky="NSEW")
		valeurTwoLabel.grid(column=0, columnspan=2, row=2, sticky="NSEW")

debutTkinter009.png

Et en temps réel!

C'est la fin de la première partie. Avec ça on peu faire pas mal de choses (comme un générateur de batch par exemple... :sauteJoie:

debutTkinter010.png

Varier la taille de la fenêtre

Si vous avez essayé de resize la fenêtre, vous verrez que les widgets ne s'adaptent pas à sa taille:

debutTkinter011.png

Le problème vient du "mainFrame" qui ne s'adapte pas à la taille de la fenetre... Vu que nous avions déjà placé les informations de "poids" de la grille plus haut, le reste est assez simple pour le coup:

def createWidgets(self):
	#creation des variables
	[...]
 
	#creation des widgets
	mainFrame = tk.Frame(self, borderwidth=2, relief="groove")
	mainFrame.columnconfigure(0, weight=0)
	mainFrame.columnconfigure(1, weight=1)
	[...]
 
	#position des widgets
	[...]

Explications:

mainFrame.columnconfigure(0, weight=0)
mainFrame.columnconfigure(1, weight=1)

Le premier argument est l'index de la colonne. weight, est le "poids" du widget.

debutTkinter012.png

Et voila! :youplaBoum:

Vous pouvez vous amuser à modifier les valeurs des poids pour voir comment le programme réagit.

En espérant que ce petit tutorial vous sera utile, passez une bonne journée.

Dorian