Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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.

Utilisateurclique surEnregistrerWindowsfile de messagesclicredessintoucheApplication.Runboucle de messagesbtnEnregistrer_Clickton gestionnairedéclenche Clickterminé : message suivantTant que le gestionnaire travaille, aucun autre message n'est traité :la fenêtre ne se redessine plus et ne répond plus aux clics.
La boucle de messages : un message à la fois

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 :

csharp
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 :

csharp
private void btnEnregistrer_Click(object? sender, EventArgs e)
{
    // ce qui doit se passer au clic
}
  • sender est l'objet qui a déclenché l'événement : ici, le bouton.
  • e contient les informations sur l'événement. Pour un clic, EventArgs ne 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 :

csharp
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énementsCe qu'elle contient
EventArgsClick, TextChanged, Load, SelectedIndexChangedRien
KeyEventArgsKeyDown, KeyUpKeyCode, Control, Shift, Alt, Handled, SuppressKeyPress
KeyPressEventArgsKeyPressKeyChar (le caractère tapé), Handled
MouseEventArgsMouseClick, MouseDownButton, X, Y, Clicks
CancelEventArgsValidatingCancel
FormClosingEventArgsFormClosingCancel, CloseReason
DataGridViewCellEventArgsCellClick, CellDoubleClickRowIndex, 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.

csharp
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.

csharp
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énementSe déclenche quand…
ButtonClickon clique, ou on appuie sur Entrée ou Espace quand il a le focus
TextBoxTextChangedle texte change, à chaque caractère
TextBoxKeyDown, KeyPressune touche est enfoncée
tout contrôleEnter, Leavele contrôle reçoit ou perd le focus
tout contrôleValidatingle contrôle va perdre le focus et doit être validé
ComboBox, ListBoxSelectedIndexChangedla sélection change
CheckBoxCheckedChangedla case est cochée ou décochée
DateTimePickerValueChangedla date change
DataGridViewSelectionChanged, CellDoubleClickla ligne sélectionnée change, double-clic sur une cellule
TimerTickl'intervalle (en millisecondes) est écoulé
FormLoad, Shown, FormClosing, FormClosedvoir 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énementUsage
1constructeurInitializeComponent(), réglages qui ne dépendent pas des données
2LoadJuste avant le premier affichage : charger les données, remplir les listes
3ShownUne fois la fenêtre visible : donner le focus, afficher un message
4Activated, DeactivateLa fenêtre devient active ou cesse de l'être
5FormClosingL'utilisateur demande la fermeture : on peut l'annuler
6FormClosedLa 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.

csharp
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.

csharp
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 :

csharp
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.

csharp
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) :

csharp
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 ?

  1. Remplir la liste des médecins quand la fenêtre d'ordonnance s'ouvre.
  2. Demander confirmation si l'utilisateur ferme une ordonnance non enregistrée.
  3. Afficher le détail d'une ordonnance dès qu'on sélectionne sa ligne dans la grille.
  4. Mettre le curseur dans la zone de recherche une fois la fenêtre affichée.
  5. Rafraîchir la liste des ordonnances toutes les deux minutes.
Voir la réponseMasquer la réponse de l'exercice 2
  1. Load du formulaire.
  2. FormClosing, en mettant e.Cancel à true si l'utilisateur répond Non.
  3. SelectionChanged du DataGridView (ou CurrentChanged de son BindingSource, chapitre 6).
  4. Shown : dans Load, la fenêtre n'est pas encore visible et l'appel à Focus() peut rester sans effet. ActiveControl = txtRecherche; dans le constructeur fonctionne aussi.
  5. Tick d'un Timer ré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.

csharp
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.

csharp
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 :

csharp
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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Formulaires et boîtes de dialogue : MessageBox, ShowDialogRevoir le sommaire du cours