Chapitre 317 minIntermédiaire
Les événements en C# WinForms : Click, TextChanged, Load
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- expliquer ce qu'est la programmation événementielle ;
- abonner un gestionnaire à un événement, dans le concepteur ou dans le code ;
- utiliser sender et les EventArgs pour savoir ce qui s'est passé ;
- connaître l'ordre des événements d'un formulaire, de Load à FormClosed ;
- corriger les erreurs classiques : gestionnaire orphelin, événement déclenché trop tôt.
4 exercices corrigés1 schéma
Un événement est un signal qu'un objet envoie quand quelque chose se produit : un clic sur un bouton, une frappe au clavier, l'ouverture d'une fenêtre. La programmation événementielle consiste à écrire des méthodes, les gestionnaires d'événements, que le programme appelle quand ces signaux arrivent. C'est le changement de logique le plus important quand on passe de la console à une application graphique.
Un programme qui attend
Un programme console déroule ses instructions de haut en bas : il demande, attend la réponse, calcule, affiche, et se termine. Une application WinForms affiche sa fenêtre, puis attend. Elle ne fait rien tant que l'utilisateur n'agit pas. Chaque action produit un événement, et ton code y répond.
Concrètement, Windows place chaque action (clic, touche, déplacement de souris, demande de redessin) dans une file de messages. La méthode Application.Run, appelée dans Program.cs, tourne en boucle : elle retire un message de la file, trouve le contrôle concerné, et celui-ci déclenche l'événement correspondant. Si tu as abonné une méthode à cet événement, elle s'exécute. Puis la boucle reprend.
Retiens la conséquence de ce schéma, elle reviendra au chapitre 7 : un seul message est traité à la fois. Tant que ton gestionnaire travaille, la fenêtre ne peut ni se redessiner ni réagir aux clics. Un gestionnaire doit donc rendre la main vite.
Écrire un gestionnaire d'événement
Il y a trois façons d'abonner une méthode à un événement.
1. Le double-clic dans le concepteur. Double-clique sur un bouton : Visual Studio crée la méthode btnEnregistrer_Click dans FrmPatients.cs et l'abonne à l'événement par défaut du contrôle (Click pour un bouton, TextChanged pour une zone de texte, Load pour un formulaire).
2. La liste des événements. Sélectionne le contrôle, clique sur l'éclair dans la fenêtre Propriétés, puis double-clique sur l'événement voulu (KeyPress, Validating, CellDoubleClick…). C'est la façon de choisir un événement qui n'est pas celui par défaut.
3. Le code. L'opérateur += abonne une méthode, -= la désabonne :
public FrmPatients()
{
InitializeComponent();
btnActualiser.Click += btnActualiser_Click;
txtRecherche.TextChanged += (sender, e) => Filtrer(); // une expression lambda
}
Dans les deux premiers cas, la ligne btnEnregistrer.Click += btnEnregistrer_Click; est écrite par le concepteur dans FrmPatients.Designer.cs. Le résultat est le même.
Un gestionnaire a toujours la même forme :
private void btnEnregistrer_Click(object? sender, EventArgs e)
{
// ce qui doit se passer au clic
}
senderest l'objet qui a déclenché l'événement : ici, le bouton.econtient les informations sur l'événement. Pour un clic,EventArgsne contient rien ; pour d'autres événements, c'est une classe dérivée plus riche.
Selon la version de Visual Studio, le concepteur écrit object sender ou object? sender ; les deux fonctionnent. Le point d'interrogation dit que sender peut être null, ce qui correspond à la définition de l'événement quand les types nullables sont activés.
Utiliser sender : un gestionnaire pour plusieurs contrôles
Quand plusieurs contrôles font presque la même chose, un seul gestionnaire suffit. sender dit lequel a été cliqué. Sur un pavé de posologie, chaque bouton ajoute son texte à la zone de saisie :
public FrmOrdonnance()
{
InitializeComponent();
foreach (Button bouton in new[] { btnMatin, btnMidi, btnSoir })
{
bouton.Click += btnMoment_Click;
}
}
private void btnMoment_Click(object? sender, EventArgs e)
{
if (sender is Button bouton)
{
txtPosologie.AppendText(bouton.Text + " ");
}
}
sender is Button bouton vérifie que l'expéditeur est bien un bouton et le range dans la variable bouton, en une seule expression.
Les EventArgs qui servent
| Classe | Événements | Ce qu'elle contient |
|---|---|---|
EventArgs | Click, TextChanged, Load, SelectedIndexChanged | Rien |
KeyEventArgs | KeyDown, KeyUp | KeyCode, Control, Shift, Alt, Handled, SuppressKeyPress |
KeyPressEventArgs | KeyPress | KeyChar (le caractère tapé), Handled |
MouseEventArgs | MouseClick, MouseDown | Button, X, Y, Clicks |
CancelEventArgs | Validating | Cancel |
FormClosingEventArgs | FormClosing | Cancel, CloseReason |
DataGridViewCellEventArgs | CellClick, CellDoubleClick | RowIndex, ColumnIndex |
Deux exemples tirés de GSB Ordonnances. Le premier n'accepte que des chiffres dans le numéro de sécurité sociale : mettre e.Handled à true empêche le caractère d'apparaître. Les touches de contrôle (retour arrière, Ctrl + V) restent permises.
private void txtSecu_KeyPress(object? sender, KeyPressEventArgs e)
{
if (!char.IsDigit(e.KeyChar) && !char.IsControl(e.KeyChar))
{
e.Handled = true;
}
}
Le second ouvre la fiche du patient par un double-clic sur sa ligne. RowIndex vaut -1 quand on double-clique sur l'en-tête de colonne : il faut l'exclure.
private void dgvPatients_CellDoubleClick(object? sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
{
btnModifier.PerformClick(); // même action que le bouton Modifier
}
}
Les événements courants
| Contrôle | Événement | Se déclenche quand… |
|---|---|---|
Button | Click | on clique, ou on appuie sur Entrée ou Espace quand il a le focus |
TextBox | TextChanged | le texte change, à chaque caractère |
TextBox | KeyDown, KeyPress | une touche est enfoncée |
| tout contrôle | Enter, Leave | le contrôle reçoit ou perd le focus |
| tout contrôle | Validating | le contrôle va perdre le focus et doit être validé |
ComboBox, ListBox | SelectedIndexChanged | la sélection change |
CheckBox | CheckedChanged | la case est cochée ou décochée |
DateTimePicker | ValueChanged | la date change |
DataGridView | SelectionChanged, CellDoubleClick | la ligne sélectionnée change, double-clic sur une cellule |
Timer | Tick | l'intervalle (en millisecondes) est écoulé |
Form | Load, Shown, FormClosing, FormClosed | voir ci-dessous |
La vie d'un formulaire
Un formulaire passe par des étapes, chacune avec son événement. Savoir lequel utiliser évite bien des bugs.
| Ordre | Événement | Usage |
|---|---|---|
| 1 | constructeur | InitializeComponent(), réglages qui ne dépendent pas des données |
| 2 | Load | Juste avant le premier affichage : charger les données, remplir les listes |
| 3 | Shown | Une fois la fenêtre visible : donner le focus, afficher un message |
| 4 | Activated, Deactivate | La fenêtre devient active ou cesse de l'être |
| 5 | FormClosing | L'utilisateur demande la fermeture : on peut l'annuler |
| 6 | FormClosed | La fenêtre est fermée : libérer ce qui doit l'être |
FormClosing sert à protéger les saisies non enregistrées. e.CloseReason dit pourquoi la fenêtre se ferme : un clic sur la croix (UserClosing), l'arrêt de Windows (WindowsShutDown), etc.
private void FrmPatientEdition_FormClosing(object? sender, FormClosingEventArgs e)
{
if (!_modifie || e.CloseReason != CloseReason.UserClosing) return;
DialogResult reponse = MessageBox.Show(
"Des modifications ne sont pas enregistrées. Fermer quand même ?",
"GSB Ordonnances", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (reponse == DialogResult.No)
{
e.Cancel = true; // la fenêtre reste ouverte
}
}
Un événement qui revient régulièrement : le Timer
Le composant Timer de System.Windows.Forms déclenche Tick à intervalle régulier, sur le thread de l'interface : tu peux modifier les contrôles sans précaution. Il sert à afficher une horloge, à rafraîchir une liste toutes les minutes, ou à fermer une session inactive.
timerHorloge.Interval = 1000; // en millisecondes
timerHorloge.Tick += (sender, e) => lblHeure.Text = DateTime.Now.ToString("HH:mm:ss");
timerHorloge.Start();
Créer ses propres événements
Les contrôles ne sont pas les seuls à déclencher des événements : tes classes peuvent en publier. Une fenêtre non modale qui doit prévenir la fenêtre principale qu'un patient a été enregistré peut déclarer un événement, que la fenêtre principale écoute :
public partial class FrmPatientEdition : Form
{
public event EventHandler<Patient>? PatientEnregistre;
private void Signaler(Patient patient)
{
PatientEnregistre?.Invoke(this, patient); // ?. : seulement si quelqu'un écoute
}
}
// dans FrmPatients
var fiche = new FrmPatientEdition();
fiche.PatientEnregistre += async (sender, patient) => await ChargerAsync();
fiche.Show(this);
C'est le même mécanisme que Click : event déclare le signal, += s'y abonne, Invoke le déclenche.
Les erreurs classiques
Le gestionnaire orphelin. Tu supprimes la méthode button1_Click dans FrmPatients.cs parce qu'elle ne sert plus. La compilation échoue avec l'erreur CS1061 : « 'FrmPatients' ne contient pas de définition pour 'button1_Click' ». La ligne d'abonnement est restée dans FrmPatients.Designer.cs. Double-clique sur l'erreur : Visual Studio ouvre le fichier sur la ligne button1.Click += button1_Click;, que tu peux supprimer. Pour éviter le problème, retire d'abord l'abonnement dans la liste des événements (éclair) de la fenêtre Propriétés, puis la méthode.
L'événement déclenché trop tôt. Remplir une ComboBox par DataSource, ou cocher une case par le code, déclenche SelectedIndexChanged ou CheckedChanged comme si l'utilisateur l'avait fait. Si ton gestionnaire recharge des données, il s'exécute pendant l'initialisation, parfois sur des données incomplètes. Deux parades : abonner le gestionnaire après le remplissage, ou poser un indicateur.
private bool _chargement;
private async void FrmOrdonnance_Load(object? sender, EventArgs e)
{
_chargement = true;
cboMedecin.DataSource = await _medecinService.ListerAsync();
_chargement = false;
}
private void cboMedecin_SelectedIndexChanged(object? sender, EventArgs e)
{
if (_chargement) return;
// réagir au choix de l'utilisateur
}
Le gestionnaire jamais appelé. Tu as écrit btnSupprimer_Click, mais rien ne se passe au clic. La méthode existe, mais personne ne l'a abonnée : vérifie l'éclair de la fenêtre Propriétés, ou cherche btnSupprimer.Click += dans le Designer. Un point d'arrêt dans la méthode le confirme : il n'est jamais atteint.
Le gestionnaire appelé deux fois. À l'inverse, abonner la même méthode dans le concepteur et dans le constructeur l'exécute deux fois par clic : deux enregistrements, deux messages.
Exercices
Exercice 1 sur 4 Le compteur de caractères Réussi À revoir
Sous la zone txtObservation (MaxLength 500), le label lblCompteur doit afficher en permanence « 37 / 500 » pendant la frappe. Quel événement utilises-tu, et quel code écris-tu ?
Voir la réponseMasquer la réponse de l'exercice 1
L'événement TextChanged de txtObservation, déclenché à chaque modification du texte (frappe, collage, effacement) :
private void txtObservation_TextChanged(object? sender, EventArgs e)
{
lblCompteur.Text = $"{txtObservation.TextLength} / {txtObservation.MaxLength}";
}
TextLength évite de construire la chaîne Text juste pour mesurer sa longueur.
Tu avais trouvé ?
Exercice 2 sur 4 Le bon événement Réussi À revoir
Dans quel événement places-tu chaque action ?
- Remplir la liste des médecins quand la fenêtre d'ordonnance s'ouvre.
- Demander confirmation si l'utilisateur ferme une ordonnance non enregistrée.
- Afficher le détail d'une ordonnance dès qu'on sélectionne sa ligne dans la grille.
- Mettre le curseur dans la zone de recherche une fois la fenêtre affichée.
- Rafraîchir la liste des ordonnances toutes les deux minutes.
Voir la réponseMasquer la réponse de l'exercice 2
Loaddu formulaire.FormClosing, en mettante.Cancelàtruesi l'utilisateur répond Non.SelectionChangedduDataGridView(ouCurrentChangedde sonBindingSource, chapitre 6).Shown: dansLoad, la fenêtre n'est pas encore visible et l'appel àFocus()peut rester sans effet.ActiveControl = txtRecherche;dans le constructeur fonctionne aussi.Tickd'unTimerréglé àInterval = 120000.
Tu avais trouvé ?
Exercice 3 sur 4 Trouve le bug Réussi À revoir
Un étudiant a écrit ce code. Le titre de la fenêtre doit indiquer le médecin choisi. Au lancement, l'application affiche une NullReferenceException dans cboMedecin_SelectedIndexChanged. Explique pourquoi, et corrige.
public FrmOrdonnance(List<Medecin> medecins)
{
InitializeComponent();
cboMedecin.SelectedIndexChanged += cboMedecin_SelectedIndexChanged;
cboMedecin.DataSource = medecins;
cboMedecin.DisplayMember = nameof(Medecin.NomComplet);
}
private void cboMedecin_SelectedIndexChanged(object? sender, EventArgs e)
{
Medecin medecin = (Medecin)cboMedecin.SelectedItem;
Text = "Ordonnance – " + medecin.NomComplet;
}
Voir la réponseMasquer la réponse de l'exercice 3
L'abonnement est fait avant le remplissage : affecter DataSource déclenche SelectedIndexChanged pendant le constructeur. Si la liste est vide, SelectedItem vaut null et medecin.NomComplet lève l'exception. Le compilateur l'avait d'ailleurs signalé : convertir un object? en Medecin non nullable produit un avertissement.
public FrmOrdonnance(List<Medecin> medecins)
{
InitializeComponent();
cboMedecin.DisplayMember = nameof(Medecin.NomComplet);
cboMedecin.DataSource = medecins;
cboMedecin.SelectedIndexChanged += cboMedecin_SelectedIndexChanged; // après le remplissage
}
private void cboMedecin_SelectedIndexChanged(object? sender, EventArgs e)
{
if (cboMedecin.SelectedItem is Medecin medecin)
{
Text = "Ordonnance – " + medecin.NomComplet;
}
}
Tu avais trouvé ?
Exercice 4 sur 4 Raccourci clavier Réussi À revoir
Dans FrmPatients, l'utilisateur veut que Ctrl + F place le curseur dans la zone de recherche, quel que soit le contrôle qui a le focus. Quel réglage du formulaire et quel gestionnaire faut-il ?
Voir la réponseMasquer la réponse de l'exercice 4
Par défaut, les touches arrivent au contrôle qui a le focus, pas au formulaire. Mets KeyPreview à true sur le formulaire pour qu'il les voie passer d'abord, puis gère KeyDown :
private void FrmPatients_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Control && e.KeyCode == Keys.F)
{
txtRecherche.Focus();
e.SuppressKeyPress = true; // la touche n'est pas transmise au contrôle actif
}
}
Avec un MenuStrip, on peut aussi créer un élément « Rechercher » avec ShortcutKeys = Ctrl + F.
Tu avais trouvé ?