8. Zdarzenia użytkownika
W poprzednim rozdziale omówiliśmy pojęcie zdarzeń związanych z komponentami formularza. Teraz przyjrzymy się, jak tworzyć zdarzenia w naszych własnych klasach.
8.1. Wstępnie zdefiniowane obiekty delegujące
Pojęcie obiektu delegate pojawiło się już w poprzednim rozdziale, ale wówczas omówiono je tylko pobieżnie. Kiedy przyglądaliśmy się, w jaki sposób deklarowane są procedury obsługi zdarzeń komponentów formularza, natrafiliśmy na kod podobny do poniższego:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
gdzie buttonAfficher był komponentem typu [Button]. Klasa ta posiada pole Click zdefiniowane w następujący sposób:
![]() |
- [1]: klasa [Button]
- [2]: jej zdarzenia
- [3,4]: zdarzenie Click
- [5]: deklaracja zdarzenia [Control.Click] [4].
- EventHandler to prototyp (szablon) metody zwany delegatem.
- event to słowo kluczowe, które ogranicza funkcjonalność obiektu delegate oraz EventHandler: obiekt delegate ma bogatszy zestaw funkcji niż obiekt event.
delegate EventHandler jest zdefiniowany w następujący sposób:
![]() |
delegate EventHandler oznacza wzorzec metody:
- którego pierwszym parametrem jest typ Object
- którego drugim parametrem jest typ EventArgs
- nie zwracający żadnego wyniku
Metoda odpowiadająca modelowi zdefiniowanemu przez EventHandler mogłaby wyglądać następująco:
private void buttonAfficher_Click(object sender, EventArgs e);
Aby utworzyć obiekt typu EventHandler, należy postępować w następujący sposób:
EventHandler evtHandler=new EventHandler(méthode correspondant au prototype défini par le type EventHandler);
W ten sposób można zapisać:
Zmienna typu delegate jest w rzeczywistości listą odwołań do metod odpowiadających modelowi delegate. Aby dodać nową metodę M do powyższej zmiennej evtHandler, stosuje się następującą składnię:
Notację += można zastosować nawet wtedy, gdy evtHandler jest listą pustą.
Instrukcja:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
dodaje metodę typu EventHandler do listy metod zdarzenia buttonAfficher.Click. Gdy wystąpi zdarzenie Click w komponencie buttonAfficher, VB wykonuje instrukcję:
gdzie:
- source to komponent typu object, który wywołał zdarzenie
- evt typu EventArgs i nie zawiera żadnych informacji
Wszystkie metody podpisywania typu void M(object,EventArgs), które zostały powiązane ze zdarzeniem Click poprzez:
this.buttonAfficher.Click += new System.EventHandler(M);
zostaną wywołane z parametrami (source, evt) przekazanymi przez VB.
8.2. Definiowanie obiektów delegatów
Instrukcja
public delegate int Opération(int n1, int n2);
definiuje typ o nazwie Opération, który reprezentuje prototyp funkcji przyjmującej dwie liczby całkowite i zwracającej liczbę całkowitą. To słowo kluczowe delegate sprawia, że Opération jest definicją prototypu funkcji.
Zmienna op typu Opération będzie służyć do przechowywania listy funkcji odpowiadających prototypowi Opération:
Zapisanie metody fi w zmiennej op odbywa się za pomocą op=new Operacja(fi) lub, prościej, poprzez op=fi. Aby dodać metodę fj do listy już zapisanych funkcji, należy wpisać op+= fj. Aby usunąć już zapisaną metodę fk, należy wpisać op-=fk. Jeśli w naszym przykładzie wpiszemy n=op(n1,n2), wszystkie metody zapisane w zmiennej op zostaną wykonane z parametrami n1 i n2. Otrzymany wynik n będzie wynikiem ostatniej wykonanej metody. Nie ma możliwości uzyskania wyników wygenerowanych przez wszystkie metody. Z tego powodu, jeśli lista metod zostanie zapisana w funkcji delegowanej, metody te najczęściej zwracają wynik typu void.
Rozważmy następujący przykład:
using System;
namespace Chap6 {
class Class1 {
// definicja prototypu funkcji
// przyjmuje 2 liczby całkowite jako parametry i zwraca liczbę całkowitą
public delegate int Opération(int n1, int n2);
// dwie metody instancji odpowiadające prototypowi
public int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//dodaj
public int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//odejmowanie
// jedna metoda statyczna odpowiadająca prototypowi
public static int Augmenter(int n1, int n2) {
Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
return n1 + 2 * n2;
}//zwiększ
static void Main(string[] args) {
// definiujemy obiekt typu operacja w celu zapisania w nim funkcji
// zapisujemy funkcję statyczną „zwiększ”
Opération op = Augmenter;
// wywołujemy delegata
int n = op(4, 7);
Console.WriteLine("n=" + n);
// tworzenie obiektu c1 typu class1
Class1 c1 = new Class1();
// w delegacie rejestruje się metodę „dodaj” obiektu c1
op = c1.Ajouter;
// wykonanie obiektu delegata
n = op(2, 3);
Console.WriteLine("n=" + n);
// zapisywanie metody odejmowania obiektu c1 w delegacie
op = c1.Soustraire;
n = op(2, 3);
Console.WriteLine("n=" + n);
//zapisanie dwóch funkcji w delegacie
op = c1.Ajouter;
op += c1.Soustraire;
// wykonanie obiektu delegowanego
op(0, 0);
// usuwanie funkcji z obiektu delegowanego
op -= c1.Soustraire;
// wykonanie delegata
op(1, 1);
}
}
}
- wiersz 3: definiuje klasę Class1.
- wiersz 6: definicja delegate Opération: prototyp metod przyjmujących dwa parametry typu int i zwracających wynik typu int
- wiersze 9–12: metoda instancji Ajouter ma sygnaturę delegata Opération.
- wiersze 14–17: metoda instancji Soustraire ma sygnaturę delegata Opération.
- wiersze 20–23: metoda klasy Augmenter ma sygnaturę delegata Opération.
- wiersz 25: wywołano metodę Main
- wiersz 20: zmienna op jest typu delegate Opération. Będzie zawierała listę metod o sygnaturze typu delegate Opération. Przypisuje się do niej pierwsze odwołanie do metody – do metody statycznej Class1.Augmenter.
- wiersz 31: wykonywane są metody delegate i op: zostaną wykonane wszystkie metody, do których odwołuje się op. Zostaną one wykonane z parametrami przekazanymi do metod delegate i op.. W tym przypadku zostanie wykonana tylko metoda statyczna Class1.Augmenter.
- wiersz 35: tworzona jest instancja c1 klasy Class1.
- wiersz 37: metoda instancji c1.Ajouter jest przypisana do delegata op. Augmenter była metodą statyczną, Ajouter jest metodą instancji. Chcieliśmy pokazać, że nie ma to znaczenia.
- wiersz 39: wykonana zostaje metoda delegate op: metoda Ajouter zostanie wykonana z parametrami przekazanymi do delegate op.
- wiersz 42: postępujemy analogicznie z metodą instancji Soustraire.
- wiersze 46–47: umieszczamy metody Ajouter i Soustraire w metodach delegate i op.
- wiersz 49: uruchamiane są metody delegate i op: obie metody Ajouter i Soustraire zostaną wykonane z parametrami przekazanymi do delegate i op.
- wiersz 51: metoda Soustraire została usunięta z metod delegate i op.
- wiersz 53: uruchomiono delegate oraz op: zostanie uruchomiona pozostała metoda Ajouter.
Wyniki wykonania są następujące:
8.3. Delegaty czy interfejsy?
Pojęcia delegates i interfejsów mogą wydawać się dość zbliżone i można się zastanawiać, jakie dokładnie są różnice między tymi dwoma pojęciami. Weźmy następujący przykład, zbliżony do już omówionego:
using System;
namespace Chap6 {
class Program1 {
// zdefiniowanie prototypu funkcji
// przyjmuje 2 liczby całkowite jako parametry i zwraca liczbę całkowitą
public delegate int Opération(int n1, int n2);
// dwie metody instancji odpowiadające prototypowi
public static int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//dodaj
public static int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//odejmowanie
// Wykonanie delegata
public static int Execute(Opération op, int n1, int n2){
return op(n1, n2);
}
static void Main(string[] args) {
// wykonanie delegata Dodaj
Console.WriteLine(Execute(Ajouter, 2, 3));
// wykonanie delegata Odejmowanie
Console.WriteLine(Execute(Soustraire, 2, 3));
// wykonanie delegata multiemisji
Opération op = Ajouter;
op += Soustraire;
Console.WriteLine(Execute(op, 2, 3));
// usuwanie funkcji z delegata
op -= Soustraire;
// wykonanie delegata
Console.WriteLine(Execute(op, 2, 3));
}
}
}
W wierszu 20 metoda Execute oczekuje odwołania do obiektu typu delegata Opération zdefiniowanego w wierszu 6. Pozwala to na przekazywanie do metody Execute różnych metod (wiersze 26, 28, 32 i 36). Tę właściwość polimorfizmu można uzyskać również za pomocą interfejsu:
using System;
namespace Chap6 {
// interfejs IOperation
public interface IOperation {
int operation(int n1, int n2);
}
// klasa Dodaj
public class Ajouter : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}
}
// klasa Odejmowanie
public class Soustraire : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}
}
// klasa testowa
public static class Program2 {
// Wywołanie jedynej metody interfejsu IOperation
public static int Execute(IOperation op, int n1, int n2) {
return op.operation(n1, n2);
}
public static void Main() {
// wywołanie delegata Dodaj
Console.WriteLine(Execute(new Ajouter(), 2, 3));
// wywołanie delegata odejmowania
Console.WriteLine(Execute(new Soustraire(), 2, 3));
}
}
}
- wiersze 6–8: interfejs [IOperation] definiuje metodę operation.
- wiersze 11–16 i 19–24: klasy [Ajouter] oraz [Soustraire] implementują interfejs [IOperation].
- wiersze 29–31: metoda Execute, której pierwszy parametr jest typu interfejsu IOperation. Metoda Execute otrzyma kolejno jako pierwszy parametr instancję klasy Ajouter, a następnie instancję klasy Soustraire.
Widać tu wyraźnie polimorficzny charakter parametru typu delegate z poprzedniego przykładu. Oba przykłady pokazują jednocześnie różnice między tymi dwoma pojęciami.
Typy delegate i interface są zamienne
- , jeśli interfejs posiada tylko jedną metodę. Typ delegate jest bowiem opakowaniem dla pojedynczej metody, podczas gdy interfejs może definiować wiele metod.
- jeśli aspekt multiemisji typu delegate nie jest wykorzystywany. Pojęcie multiemisji nie występuje bowiem w interfejsie.
Jeśli oba te warunki są spełnione, wówczas dla metody Execute można wybrać jedną z dwóch następujących sygnatur:
int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)
Druga sygnatura, wykorzystująca metodę delegate, może okazać się bardziej elastyczna w użyciu. W pierwszej sygnaturze pierwszy parametr metody musi bowiem implementować interfejs IOperation. Wymaga to utworzenia klasy w celu zdefiniowania metody, która ma zostać przekazana jako pierwszy parametr do metody Execute. W przypadku drugiej sygnatury wystarczy dowolna istniejąca metoda o odpowiedniej sygnaturze. Nie ma potrzeby tworzenia żadnych dodatkowych konstrukcji.
8.4. Obsługa zdarzeń
Obiekty typu delegate mogą służyć do definiowania zdarzeń. Klasa C1 może zdefiniować zdarzenie evt w następujący sposób:
- typ delegate jest definiowany w klasie C1 lub poza nią:
- klasa C1 definiuje pole typu delegate Evt:
- gdy instancja c1 klasy C1 będzie chciała zgłosić zdarzenie, uruchomi swój delegat Evt1, przekazując mu parametry zdefiniowane przez delegata Evt. Wszystkie metody zarejestrowane w delegate Evt1 zostaną wówczas wykonane z tymi parametrami. Można powiedzieć, że zostały one powiadomione o zdarzeniu Evt1.
- jeśli obiekt c2, korzystający z obiektu c1, chce zostać powiadomiony o wystąpieniu zdarzenia Evt1 na obiekcie c1, zarejestruje jedną ze swoich metod c2.M w obiekcie delegowanym c1.Evt1 obiektu c1.. W ten sposób jego metoda c2.M będzie wykonywana za każdym razem, gdyzdarzenie Evt1 wystąpi na obiekcie c1. Będzie mógł również wyrejestrować się, gdy nie będzie już chciał otrzymywać powiadomień o tym zdarzeniu.
- Ponieważ obiekt delegowany c1.Evt1 może zarejestrować wiele metod, różne obiekty ci będą mogły zarejestrować się u delegata c1.Evt1, aby otrzymywać powiadomienia o zdarzeniu Evt1 na obiekcie c1.
W tym scenariuszu mamy:
- klasę, która zgłasza zdarzenie
- klasy, które są powiadamiane o tym zdarzeniu. Mówi się, że subskrybują one to zdarzenie lub zapisują się na nie.
- typ delegate, który definiuje sygnaturę metod, które zostaną powiadomione o zdarzeniu
Framework .NET definiuje:
- standardowy podpis zdarzenia o numerze delegate
- source: obiekt, który zgłosił zdarzenie
- evtInfo: obiekt typu EventArgs lub pochodny, zawierający informacje o zdarzeniu
- nazwa obiektu typu delegate musi kończyć się na EventHandler
- standardowy sposób deklaracji zdarzenia typu MyEventHandler w klasie:
Pole Evt1 jest typu delegate. Słowo kluczowe event służy do ograniczenia operacji, które można na nim wykonywać:
- spoza klasy C1 możliwe są wyłącznie operacje += i -=. Zapobiega to usunięciu (na przykład w wyniku pomyłki programisty) metod subskrybujących zdarzenie. Można po prostu subskrybować (+=) lub zrezygnować z subskrypcji (-=) zdarzenia.
- Tylko instancja typu C1 może wykonać wywołanie Evt1(source,evtInfo), które uruchamia metody zapisane do zdarzenia Evt1.
Framework .NET udostępnia metodę generyczną zgodną z sygnaturą delegate zdarzenia:
public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
- delegate EventHandler wykorzystuje typ generyczny TEventArgs, który jest typem jego drugiego parametru
- typ TEventArgs musi wywodzić się z typu EventsArgs (gdzie TEventArgs: EventArgs)
W przypadku tego ogólnego typu delegate deklaracja zdarzenia X w klasie C będzie zgodna z następującym zalecanym schematem:
- zdefiniować typ XEventArgs wywodzący się z EventArgs w celu zawarcia informacji o zdarzeniu X
- zdefiniować w klasie C zdarzenie typu EventHandler<XEventArgs>.
- zdefiniować w klasie C metodę chronioną
przeznaczoną do „opublikowania” zdarzenia X dla subskrybentów.
Rozważmy następujący przykład:
- klasa Emetteur zawiera wartość temperatury. Temperatura ta jest monitorowana. Gdy przekroczy ona określony próg, należy wygenerować zdarzenie. Nazwiemy to zdarzenie TemperatureTropHaute. Informacje o tym zdarzeniu zostaną zawarte w typie TemperatureTropHauteEventArgs.
- Klasa Souscripteur subskrybuje powyższe zdarzenie. Gdy zostanie powiadomiona o zdarzeniu, wyświetla komunikat na konsoli.
- Program konsolowy tworzy nadawcę i dwóch subskrybentów. Wprowadza temperatury za pomocą klawiatury i zapisuje je w instancji typu Emetteur. Jeśli temperatura jest zbyt wysoka, instancja typu Emetteur publikuje zdarzenie TemperatureTropHaute.
Aby zachować zgodność z zalecaną metodą zarządzania zdarzeniami, najpierw definiujemy typ TemperatureTropHauteEventArgs w celu hermetyzacji informacji o zdarzeniu:
using System;
namespace Chap6 {
public class TemperatureTropHauteEventArgs:EventArgs {
// temperatura podczas zdarzenia
public decimal Temperature { get; set; }
// producenci
public TemperatureTropHauteEventArgs() {
}
public TemperatureTropHauteEventArgs(decimal temperature) {
Temperature = temperature;
}
}
}
- wiersz 6: informacją zawartą w klasie TemperatureTropHauteEventArgs jest temperatura, która spowodowała zdarzenie TemperatureTropHaute.
Klasa Emetteur ma następującą postać:
using System;
namespace Chap6 {
public class Emetteur {
static decimal SEUIL = 19;
// temperatura zmierzona
private decimal temperature;
// nazwa źródła
public string Nom { get; set; }
// zgłoszone zdarzenie
public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
// odczyt/zapis temperatury
public decimal Temperature {
get {
return temperature;
}
set {
temperature = value;
if (temperature > SEUIL) {
// zgłoszenie zdarzenia do abonentów
OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
}
}
}
// zgłoszenie zdarzenia
protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
// wysyłanie zdarzenia TemperatureTropHaute do abonentów
TemperatureTropHaute(this, evt);
}
}
}
- wiersz 5: próg temperatury, powyżej którego zostanie opublikowane zdarzenie TemperatureTropHaute.
- wiersz 10: nadawca posiada nazwę umożliwiającą jego identyfikację
- wiersz 12: zdarzenie TemperatureTropHaute.
- wiersze 15–26: metoda get, która zwraca temperaturę, oraz metoda set, która ją rejestruje. To metoda set powoduje opublikowanie zdarzenia TemperatureTropHaute, jeśli temperatura, która ma zostać zarejestrowana, przekroczy próg określony w wierszu 5. Metoda ta powoduje opublikowanie zdarzenia za pomocą metody OnTemperatureTropHauteHandler z wiersza 29, przekazując jej jako parametr obiekt TemperatureTropHauteEventArgs, w którym zapisano temperaturę, która przekroczyła próg.
- wiersze 29–32: zdarzenie TemperatureTropHaute jest publikowane z pierwszym parametrem będącym samym nadawcą, a drugim parametrem obiektem TemperatureTropHauteEventArgs otrzymanym jako parametr.
Klasa Souscripteur, która będzie subskrybować zdarzenie TemperatureTropHaute, ma następującą postać:
using System;
namespace Chap6 {
public class Souscripteur {
// nazwa
public string Nom { get; set; }
// osoba zarządzająca zdarzeniem TemperatureTropHaute
public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
// wyświetlanie na konsoli operatora
Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
}
}
}
- wiersz 6: każdy subskrybent jest identyfikowany za pomocą nazwy.
- wiersze 9–12: metoda, która zostanie powiązana ze zdarzeniem TemperatureTropHaute. Ma ona sygnaturę typu delegate EventHandler<TEventArgs>, którą musi posiadać menedżer zdarzeń. Metoda wyświetla na konsoli: nazwę subskrybenta wyświetlającego komunikat, nazwę nadawcy, który zgłosił zdarzenie, oraz temperaturę, która je wywołała.
- Subskrypcja zdarzenia TemperatureTropHaute dla obiektu Emetteur nie jest realizowana w klasie Souscripteur. Zostanie ona zrealizowana przez klasę zewnętrzną.
Program [Program.cs] łączy wszystkie te elementy ze sobą:
using System;
namespace Chap6 {
class Program {
static void Main(string[] args) {
// utworzenie nadajnika zdarzeń
Emetteur e1 = new Emetteur() { Nom = "e" };
// utworzenie tablicy zawierającej 2 subskrybentów
Souscripteur[] souscripteurs = new Souscripteur[2];
for (int i = 0; i < souscripteurs.Length; i++) {
// utworzenie subskrybenta
souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
// subskrybowanie zdarzenia TemperatureTropHaute z e1
e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
}
// odczyt temperatur z klawiatury
decimal temperature;
Console.Write("Température (rien pour arrêter) : ");
string saisie = Console.ReadLine().Trim();
// dopóki wprowadzony wiersz nie jest pusty
while (saisie != "") {
// czy wprowadzona wartość jest liczbą dziesiętną?
if (decimal.TryParse(saisie, out temperature)) {
// temperatura poprawna – zapisujemy ją
e1.Temperature = temperature;
} else {
// zgłaszamy błąd
Console.WriteLine("Température incorrecte");
}
// nowe wprowadzenie
Console.Write("Température (rien pour arrêter) : ");
saisie = Console.ReadLine().Trim();
}//while
}
}
}
- wiersz 6: utworzenie nadawcy
- wiersze 8–14: utworzenie dwóch subskrybentów, których zapisano do zdarzenia TemperatureTropHaute nadawcy.
- wiersze 20–32: pętla wprowadzania temperatur za pomocą klawiatury
- wiersz 24: jeśli wprowadzona temperatura jest prawidłowa, jest ona przekazywana do obiektu Emetteur e1, który wywoła zdarzenie TemperatureTropHaute, jeśli temperatura przekracza 19 °C.
Wyniki wykonania są następujące:

