Skip to content

6. Obsługa zdarzeń

W poprzednim rozdziale omówiliśmy pojęcie zdarzeń związanych z komponentami. Teraz przyjrzymy się, jak tworzyć zdarzenia w naszych własnych klasach. Jest to trudny temat, który nie zainteresuje początkujących programistów; im zaleca się przejście bezpośrednio do następnego rozdziału.

6.1. Obiekty delegujące

Instrukcja

    Delegate Function opération(ByVal n1 As Integer, ByVal n2 As Integer) As Integer

definiuje typ o nazwie opération, który w rzeczywistości jest prototypem funkcji przyjmującej dwie liczby całkowite i zwracającej liczbę całkowitą. To właśnie słowo kluczowe delegate sprawia, że opération jest definicją prototypu funkcji. Typ [opération] można przypisać do zmiennej w następujący sposób:


        Dim op As New opération(AddressOf [fonction])

Zmienna typu [délégué] jest tworzona jako obiekt. Parametrem konstruktora jest funkcja powiązana z instancją delegata. W przypadku typu VB funkcja ta jest podana w postaci AddressOf funkcja, gdzie [fonction] to nazwa funkcji. Po utworzeniu instancji delegata można z niej korzystać jak z funkcji:


        ' wykonuje się procedurę delegowaną
        Dim n As Integer = op(4, 7)

Delegat [opération] został zdefiniowany jako przyjmujący dwa parametry typu [integer] i zwracający wynik tego samego typu. Dlatego jego instancja [op], przedstawiona powyżej, jest wykorzystywana jako taka funkcja. Rozważmy następujący przykład:


' funkcje delegowane
Imports System

Public Class delegate1
    ' definicja prototypu funkcji
    ' przyjmuje 2 liczby całkowite jako parametry i zwraca liczbę całkowitą
    Delegate Function opération(ByVal n1 As Integer, ByVal n2 As Integer) As Integer

    ' dwie metody instancji odpowiadające prototypowi
    ' dodaj
    Public Function ajouter(ByVal n1 As Integer, ByVal n2 As Integer) As Integer
        Console.Out.WriteLine(("ajouter(" & n1 & "," & n2 & ")"))
        Return n1 + n2
    End Function

    ' odejmowanie
    Public Function soustraire(ByVal n1 As Integer, ByVal n2 As Integer) As Integer
        Console.Out.WriteLine(("soustraire(" & n1 & "," & n2 & ")"))
        Return n1 - n2
    End Function

    ' jedna metoda statyczna odpowiadająca prototypowi
    Public Shared Function augmenter(ByVal n1 As Integer, ByVal n2 As Integer) As Integer
        Console.Out.WriteLine(("augmenter(" & n1 & "," & n2 & ")"))
        Return n1 + 2 * n2
    End Function

    ' program testowy
    Public Shared Sub Main()
        ' definiujemy obiekt typu operacja w celu zapisania w nim funkcji
        ' zapisujemy funkcję statyczną „zwiększ”
        Dim op As New opération(AddressOf delegate1.augmenter)
        ' wywołujemy delegat
        Dim n As Integer = op(4, 7)
        Console.Out.WriteLine(("n=" & n))
        ' tworzenie obiektu c1 typu class1
        Dim c1 As New delegate1
        ' w delegacie rejestruje się metodę „dodaj” obiektu c1
        op = New delegate1.opération(AddressOf c1.ajouter)
        ' wykonanie obiektu delegata
        n = op(2, 3)
        Console.Out.WriteLine(("n=" & n))
        ' zapisywanie metody odejmowania obiektu c1 w delegacie
        op = New delegate1.opération(AddressOf c1.soustraire)
        n = op(2, 3)
        Console.Out.WriteLine(("n=" & n))
    End Sub
End Class

Wyniki wykonania są następujące:

augmenter(4,7)
n=18
ajouter(2,3)
n=5
soustraire(2,3)
n=-1

6.2. Zarządzanie zdarzeniami

Do czego może służyć obiekt typu delegate?

Jak zobaczymy w poniższym przykładzie, służy on przede wszystkim do zarządzania zdarzeniami. Klasa C1 może generować zdarzenia typu evti. Podczas zdarzenia typu evti obiekt typu C1 uruchomi wykonanie obiektu evtiDéclenché typu delegate. Wówczas zostaną wykonane wszystkie funkcje zarejestrowane w obiekcie delegującym typu evtiDéclenché. Jeśli obiekt typu C2, korzystający z obiektu typu C1, chce zostać powiadomiony o wystąpieniu zdarzenia evti na obiekcie typu C1, zarejestruje on jedną ze swoich metod C2.f w obiekcie delegowanym C1.evtiDéclenché obiektu C1, tak aby metoda C2.f była wykonywana za każdym razem, gdyzdarzenie evti wystąpi na obiekcie C1. Ponieważ obiekt delegowany C1.evtiDéclenché może zarejestrować wiele funkcji, różne obiekty Ci będą mogły zarejestrować się u delegata C1.evtiDéclenché, aby otrzymywać powiadomienia o zdarzeniu evti na obiekcie C1.

6.2.1. Zgłoszenie zdarzenia

Zdarzenie zgłasza się w następujący sposób (1):


Delegate Sub proc(liste d'arguments)
Public Event événement as proc

lub (2)


Public Event événement(liste d'arguments)

To słowo kluczowe [Event] definiuje dane jako zdarzenie. Do zdarzenia przypisuje się sygnaturę, jaką powinny posiadać procedury obsługujące to zdarzenie. Sygnaturę tę można zdefiniować za pomocą delegata (metoda 1) lub bezpośrednio (metoda 2).

6.2.2. Definiowanie procedur obsługi zdarzenia

Gdy zdarzenie jest wyzwalane przez instrukcję [RaiseEvent], należy je przetworzyć. Procedury odpowiedzialne za przetwarzanie zdarzeń nazywane są procedurami obsługi zdarzeń. Podczas tworzenia obiektu [Event] deklarowana jest jedynie sygnatura procedur obsługi zdarzenia. Jest to sygnatura obiektu [delegate] powiązanego ze zdarzeniem. Po wykonaniu tej czynności będzie można przypisać do zdarzenia procedury obsługi. Są to procedury, które muszą mieć taką samą sygnaturę jak obiekt [delegate] powiązany ze zdarzeniem. Aby przypisać procedurę obsługi zdarzenia do zdarzenia, należy użyć instrukcji [AddHandler] o następującej składni:

AddHandler event, AddressOf eventhandler
  • event: zmienna zdarzenia, z którą powiązany jest nowy program obsługi zdarzeń
  • eventhandler: nazwa procedury obsługi zdarzenia – musi mieć sygnaturę [délégué] powiązaną ze zdarzeniem

Do jednego zdarzenia można przypisać dowolną liczbę procedur obsługi. Wszystkie zostaną wykonane, gdy zdarzenie, z którym są powiązane, zostanie wywołane.

6.2.3. Wywołanie zdarzenia

Aby wywołać zdarzenie programowo, używa się instrukcji [RaiseEvent]:

RaiseEvent eventname[( argumentlist )]
  • eventname: zmienna zdarzenia (zadeklarowana za pomocą słowa kluczowego Event)
  • argumentlist: argumenty procedury powiązanej z wydarzeniem.

Zostaną wywołane wszystkie procedury, które zostały powiązane z tym zdarzeniem za pomocą instrukcji AddHandler. Otrzymają one jako parametry wartości zdefiniowane w sygnaturze zdarzenia.

6.2.4. Przykład

Rozważmy następujący przykład:


'obsługa zdarzeń
Imports System

Public Class myEventArgs
    Inherits EventArgs
    ' klasa zdarzenia
    ' atrybut
    Private _saisie As String

    ' konstruktor
    Public Sub New(ByVal saisie As String)
        _saisie = saisie
    End Sub

    ' właściwość wprowadzana w trybie tylko do odczytu
    Public Overrides Function ToString() As String
        Return _saisie
    End Function
End Class

Public Class émetteur
    ' klasa generująca zdarzenie
    ' atrybut
    Private _nom As String    ' nom de l'émetteur

    ' konstruktor
    Public Sub New(ByVal nom As String)
        _nom = nom
    End Sub

    ' ToString
    Public Overrides Function ToString() As String
        Return _nom
    End Function

    ' prototyp funkcji odpowiedzialnych za obsługę zdarzenia
    Delegate Sub _evtHandler(ByVal sender As Object, ByVal evt As myEventArgs)

    ' pula menedżerów zdarzeń
    Public Event evtHandler As _evtHandler

    ' metoda żądania wygenerowania zdarzenia
    Public Sub envoyerEvt(ByVal evt As myEventArgs)
        ' powiadamia się wszystkich subskrybentów
        ' traktuje się to tak, jakby zdarzenie pochodziło stąd
        RaiseEvent evtHandler(Me, evt)
    End Sub
End Class

' klasa przetwarzania zdarzenia
Public Class souscripteur
    ' atrybut
    Private nom As String    ' nom du souscripteur
    Private sender As émetteur    ' l'émetteur des évts

    ' konstruktor
    Public Sub New(ByVal nom As String, ByVal e As émetteur)
        ' zapisujemy nazwę subskrybenta
        Me.nom = nom
        ' oraz nadawcę zdarzeń
        Me.sender = e
        ' rejestruje się w celu otrzymywania zdarzeń od nadawcy e
        AddHandler e.evtHandler, AddressOf traitementEvt
    End Sub

    ' menedżer zdarzeń
    Public Sub traitementEvt(ByVal sender As Object, ByVal evt As myEventArgs)
        ' wyświetlanie zdarzeń
        Console.Out.WriteLine(("L'objet [" + sender.ToString + "] a signalé la saisie erronée [" + evt.ToString + "] au souscripteur [" + nom + "]"))
    End Sub
End Class

' program testowy
Public Class test
    Public Shared Sub Main()
        ' utworzenie nadajnika zdarzeń
        Dim émetteur1 As New émetteur("émetteur1")
        ' utworzenie tablicy subskrybentów
        ' dla zdarzeń wysyłanych przez nadawcę1
        Dim souscripteurs() As souscripteur = {New souscripteur("s1", émetteur1), New souscripteur("s2", émetteur1)}
        ' odczytuje się sekwencję liczb całkowitych z klawiatury
        ' gdy tylko jedna z nich jest błędna, wysyłane jest zdarzenie
        Console.Out.Write("Nombre entier (rien pour arrêter) : ")
        Dim saisie As String = Console.In.ReadLine().Trim()
        ' dopóki wprowadzony wiersz nie jest pusty
        While saisie <> ""
            ' czy wprowadzona wartość jest liczbą całkowitą?
            Try
                Dim n As Integer = Integer.Parse(saisie)
            Catch
                ' nie jest to liczba całkowita
                ' powiadamia się wszystkich
                émetteur1.envoyerEvt(New myEventArgs(saisie))
            End Try
            ' powiadamia się wszystkich
            ' nowe wprowadzenie danych
            Console.Out.Write("Nombre entier (rien pour arrêter) : ")
            saisie = Console.In.ReadLine().Trim()
        End While
        ' koniec
        Environment.Exit(0)
    End Sub
End Class

Powyższy kod jest dość złożony. Przeanalizujmy go szczegółowo. W ramach zarządzania zdarzeniami istnieje nadawca zdarzeń (sender), który wysyła szczegóły zdarzeń (EventArgs) do subskrybentów, którzy zgłosili zainteresowanie danymi zdarzeniami. Klasa nadawcza zdarzeń wygląda w tym przypadku następująco:


Public Class émetteur
    ' klasa generująca zdarzenie
    ' atrybut
    Private _nom As String    ' nom de l'émetteur

    ' konstruktor
    Public Sub New(ByVal nom As String)
        _nom = nom
    End Sub

    ' ToString
    Public Overrides Function ToString() As String
        Return _nom
    End Function

    ' prototyp funkcji odpowiedzialnych za obsługę zdarzenia
    Delegate Sub _evtHandler(ByVal sender As Object, ByVal evt As myEventArgs)

    ' pula menedżerów zdarzeń
    Public Event evtHandler As _evtHandler

    ' metoda żądania wygenerowania zdarzenia
    Public Sub envoyerEvt(ByVal evt As myEventArgs)
        ' powiadamia się wszystkich subskrybentów
        ' traktuje się to tak, jakby zdarzenie pochodziło stąd
        RaiseEvent evtHandler(Me, evt)
    End Sub
End Class

Każdy obiekt émetteur ma obiekt nom przypisany domyślnie. Metoda ToString została przedefiniowana w taki sposób, że podczas przekształcania obiektu émetteur w string, pobierana jest właśnie jego nazwa. Klasa definiuje zdarzenie:


        ' prototyp funkcji odpowiedzialnych za obsługę zdarzenia
        Delegate Sub _evtHandler(ByVal sender As Object, ByVal evt As myEventArgs)

        ' zdarzenie
        Public Event evtHandler As _evtHandler

Pierwszym argumentem funkcji obsługującej zdarzenie będzie obiekt, który wywołuje to zdarzenie. Drugim argumentem będzie obiekt typu myEventArgs, zawierający szczegóły dotyczące zdarzenia, do którego jeszcze wrócimy. Aby móc wywołać zdarzenie spoza obiektu émetteur, dodajemy do klasy metodę envoyerEvt:


    ' metoda żądania wygenerowania zdarzenia
    Public Sub envoyerEvt(ByVal evt As myEventArgs)
        ' powiadamia się wszystkich subskrybentów
        ' traktuje się to tak, jakby zdarzenie pochodziło stąd
        RaiseEvent evtHandler(Me, evt)
    End Sub

Musimy określić, jakie rodzaje zdarzeń będą wyzwalane przez klasę émetteur. W tym celu zdefiniowaliśmy klasę myEventArgs:


Public Class myEventArgs
    ' klasa zdarzenia
    ' atrybut
    Private _saisie As String

    ' konstruktor
    Public Sub New(ByVal saisie As String)
        _saisie = saisie
    End Sub

    ' właściwość wprowadzona jako tylko do odczytu
    Public Overrides Function ToString() As String
        Return _saisie
    End Function
End Class

Zdarzenia, które nas interesują, to błędne wpisy z klawiatury. Poprosimy użytkownika o wpisanie liczb całkowitych z klawiatury, a gdy tylko wpisze ciąg znaków, który nie jest liczbą całkowitą, wywołamy zdarzenie. Jako szczegół zdarzenia wystarczy podać błędne wprowadzenie. Takie jest przeznaczenie atrybutu _saisie tej klasy. Obiekt myEventArgs jest zatem tworzony z błędnym wprowadzeniem jako parametrem. Ponadto redefiniujemy metodę ToString, aby podczas przekształcania obiektu myEventArgs w ciąg znaków uzyskano atrybut _saisie.

Zdefiniowaliśmy nadawcę zdarzeń oraz typ zdarzeń, które wysyła. Następnie definiujemy typ subskrybentów zainteresowanych tymi zdarzeniami.


    ' klasa obsługi zdarzenia
    Public Class souscripteur
        ' atrybut
        Private nom As String     ' nom du souscripteur
        Private sender As émetteur     ' l'émetteur des évts

        ' konstruktor
        Public Sub New(ByVal nom As String, ByVal e As émetteur)
            ' zapisywana jest nazwa subskrybenta
            Me.nom = nom
            ' oraz nadawcę zdarzeń
            Me.sender = e
            ' rejestruje się w celu otrzymywania zdarzeń od nadawcy e
            AddHandler e.evtHandler, AddressOf traitementEvt
        End Sub

        ' menedżer zdarzeń
        Public Sub traitementEvt(ByVal sender As Object, ByVal evt As myEventArgs)
            ' wyświetlanie zdarzeń
            Console.Out.WriteLine(("L'objet [" + sender.ToString + "] a signalé la saisie erronée [" + evt.ToString + "] au souscripteur [" + nom + "]"))
        End Sub
    End Class

Subskrybent będzie definiowany przez dwa parametry: swoją nazwę (atrybut nom) oraz obiekt émetteur, którego zdarzenia chce przetwarzać (atrybut sender). Te dwa parametry zostaną przekazane do konstruktora obiektu. W trakcie tego samego procesu tworzenia subskrybent subskrybuje zdarzenia nadawcy:


            ' rejestruje się, aby otrzymywać zdarzenia od nadawcy e
            AddHandler e.evtHandler, AddressOf traitementEvt

Funkcja zarejestrowana u nadawcy to traitementEvt. Ta metoda klasy souscripteur wyświetla dwa otrzymane argumenty (sender, evt) oraz nazwę odbiorcy (nom). Zdefiniowano typ generowanych zdarzeń, typ nadawcy tych zdarzeń oraz typ subskrybentów. Pozostaje nam tylko je zaimplementować:


    ' program testowy
    Public Class test
        Public Shared Sub Main()
            ' tworzenie źródła zdarzeń
            Dim émetteur1 As New émetteur("émetteur1")
            ' utworzenie tabeli subskrybentów
            ' dla zdarzeń wysyłanych przez nadawcę1
            Dim souscripteurs() As souscripteur = {New souscripteur("s1", émetteur1), New souscripteur("s2", émetteur1)}
            ' odczytuje się sekwencję liczb całkowitych z klawiatury
            ' gdy tylko jedna z nich jest błędna, wysyłane jest zdarzenie
            Console.Out.Write("Nombre entier (rien pour arrêter) : ")
            Dim saisie As String = Console.In.ReadLine().Trim()
            ' dopóki wprowadzony wiersz nie jest pusty
            While saisie <> ""
                ' czy wprowadzona wartość jest liczbą całkowitą?
                Try
                    Dim n As Integer = Integer.Parse(saisie)
                Catch
                    ' nie jest to liczba całkowita
                    ' powiadamia się wszystkich
                    émetteur1.envoyerEvt(New myEventArgs(saisie))
                End Try
                ' powiadamia się wszystkich
                ' nowe wprowadzenie danych
                Console.Out.Write("Nombre entier (rien pour arrêter) : ")
                saisie = Console.In.ReadLine().Trim()
            End While
            ' koniec
            Environment.Exit(0)
        End Sub

Tworzymy obiekt émetteur:


         ' utworzenie emitenta zdarzeń
        Dim émetteur1 As New émetteur("émetteur1")

Tworzymy tablicę zawierającą dwóch subskrybentów dla zdarzeń generowanych przez obiekt émetteur1:


         ' utworzenie tablicy subskrybentów
         ' dla zdarzeń wysyłanych przez nadawcę1
        Dim souscripteurs() As souscripteur = {New souscripteur("s1", émetteur1), New souscripteur("s2", émetteur1)}

Prosimy użytkownika o wpisanie liczb całkowitych za pomocą klawiatury. Gdy tylko wprowadzona wartość jest błędna, prosimy obiekt émetteur1 o wysłanie zdarzenia do swoich subskrybentów:


                 ' powiadamia się wszystkich
                émetteur1.envoyerEvt(New myEventArgs(saisie))

Wysłane zdarzenie ma typ myEventArgs i zawiera błędne dane. Obaj subskrybenci powinni otrzymać to zdarzenie i zgłosić je. Pokazuje to poniższe wykonanie.

dos>evt1
Nombre entier (rien pour arrêter) : 4
Nombre entier (rien pour arrêter) : a
L'objet [émetteur1] a signalé la saisie erronée [a] au souscripteur [s1]
L'objet [émetteur1] a signalé la saisie erronée [a] au souscripteur [s2]
Nombre entier (rien pour arrêter) : 1.6
L'objet [émetteur1] a signalé la saisie erronée [1.6] au souscripteur [s1]
L'objet [émetteur1] a signalé la saisie erronée [1.6] au souscripteur [s2]
Nombre entier (rien pour arrêter) :