События и делегаты

В Заметке о консольных и оконных (Windows Forms) приложениях мы отметили существенную разницу между ними. Консольное приложение реализует концепцию императивного (процедурного) программирования, а управление Windows-приложением основано на понятии события (event). События могут создаваться как пользователем, так и возникать в процессе выполнения приложения. Начнем с простых вещей.

Создадим оконное приложение и запустим его на выполнение. С пустой формой мы можем выполнить только несколько стандартных действий: изменить размер, свернуть, свернуть в окно/развернуть и закрыть приложение.

Для изменения размеров формы подведем указатель мыши (1 событие) к любой ее границе (указатель изменит свою форму), перетянем ее в нужном направлении (2 событие) и отпустим кнопку (3 событие). Последние три действия с формой выполняются после клика мышью (событие 4) на кнопках формы.

Ясно, что такое приложение нам не слишком интересно, тем не менее, очевидна связь между событиями и действиями.

Перенесем с Панели элементов на форму объект «Кнопка» (по умолчанию – button1 класса Button). На вкладке «Конструктор» кнопка видна, на вкладке «Код» о ней нет никакой информации. Однако раскрыв файл Form1.Designer.cs, мы увидим в описании класса Form1 поле:

   private System.Windows.Forms.Button button1;

которое задает этот объект, а в методе private void InitializeComponent() обнаружим описание его свойств (имя, местоположение, размер, надпись и т.п.).

Запустим программу снова на выполнение. Нажатие на кнопку (событие)  не влечет за собой никаких действий (кроме того, что между нажатием и отпусканием кнопки она подсвечивается).

Смотрим книгу «для чайников». В ней написано: чтобы связать это событие с каким-либо действием необходимо всего лишь выполнить двойной клик на кнопке, в окне  кода появится заготовка для метода – обработчика события Click:

private void button1_Click(object sender, EventArgs e)
{

}

Увеличим ширину кнопки примерно в три раза. Вставим в тело метода между фигурными скобками оператор:
    button1.Text = DateTime.Now.ToString();
Теперь при нажатии кнопки непосредственно на ней мы можем прочитать текущие дату и время нажатия на кнопку.

«Выдающийся» результат! Есть событие, есть реакция на него (обработка события). Как Вам такая автоматизация программирования!

Заметим, что в панели Свойства для объекта button1 на закладке События (щелчок на «желтой молнии») напротив события Click появился метод button1_Click. В окне кода добавили всего один метод с одним оператором в его теле. Что же еще изменилось? Посмотрим содержимое файла  Form1.Designer.cs. В нем добавилась загадочная строка:

this.button1.Click += new System.EventHandler(this.button1_Click);

Расшифруем ее. Ключевое слово this – это ссылка на текущий объект Form1 (забавно, что имя объекта совпадает с именем класса). Объект button1 размещен на форме Form1. А Click – очевидно это событие, клик на кнопке.  EventHandlerделегат (delegate), представляет метод, который будет обрабатывать событие, не имеющее данных (объявлен в библиотеке System). Тип события обязательно должен совпадать с типом делегата. В скобках указывается имя этого метода button1_Click.
Переведем смысл оператора на русский язык:
Объект.Событие += new Делегат(Метод_обработки);
Символ + определяет подписку обработчика события.
Очевидный вывод: Подписка на событие с использованием делегата приводит к вызову метода при возникновении события.

Возможен ли разрыв связи между событием и методом его обработки? И нет ли у вас ощущения статичности таких связей? Можно ли то же самое достичь программным путем?

Реализуем второй вариант действий:
1) поместим кнопку button1 на форме Form1;
2) в конструктор формы добавим один оператор, тогда:
public Form1()
{
InitializeComponent();
button1.Click += new System.EventHandler(button1_Click);
}
3) в описание класса добавим метод:
private void button1_Click(object sender, EventArgs e)
{
button1.Text = DateTime.Now.ToString();
}
4) запустим программу на выполнение, сравним результаты;
5) появился ли оператор подключения в файле FormDesigner.cs ?

Заметим, что этот файл Form1.Designer.cs является текстовым описанием формы и размещенных на ней элементов после запуска программы, что позволяет отобразить ее в режиме конструктора.

Далее многое можно изменять программным путем.

Итак, событие – это сообщение другим объектам программы, что произошло какое-то действие. Действие может быть инициировано пользователем (нажатие клавиши) или же в результате выполнения какого-то фрагмента программы (по условию).

Объект, который вызывает событие, называется отправителем (sender) сообщения, а объект, который сообщение получает – получателем. Роль «почтальона» выполняет делегат. Получатель сообщения имеет метод, который автоматически выполняется в ответ на исходное событие. В нашем примере отправителем и получателем сообщения является объект button1 («makes himself»).

Платформа .NET Framework поддерживает простое программирование событий, из-за чего начинающие программисты часто не вникают в работу событий.

Событием в языке C# называется сущность, предоставляющая две возможности: сообщать об изменениях, а для его пользователей — реагировать на них. В объявлениях классов визуальных компонентов мы найдем большое количество событий, которые могут быть вам полезны. Подсчитайте, сколько событий связано с формой? У меня получилось – 76. А для кнопки – 58, не мало? Если же вам необходимо создать собственное событие, то вы можете его просто объявить:

public event EventHandler myEvent;

Рассмотрим, из чего состоит объявление. Сначала идут модификаторы события, затем ключевое слово event, после него — тип события, который обязательно должен быть типом-делегатом, и идентификатор события, то есть его имя myEvent. Ключевое слово event сообщает компилятору о том, что это не публичное поле, а специальным образом раскрывающаяся конструкция, скрывающая от программиста детали реализации механизма событий (пока это замечание пропустите).

В C# разрешается формировать какие угодно разновидности событий. Но ради совместимости программных компонентов со средой .NET Framework следует придерживаться рекомендаций, которые по существу, сводятся к следующему требованию: у обработчиков событий должны быть два параметра. Первый из них — ссылка на объект, формирующий событие, второй — параметр типа EventArgs, содержащий любую дополнительную информацию о событии, которая требуется обработчику. То есть:

void обработчик(object отправитель, EventArgs е) {//…}

Как правило, отправитель — это параметр, передаваемый вызывающим кодом с помощью ключевого слова this. Параметр е типа EventArgs содержит дополнительную информацию о событии и может быть проигнорирован, если он не нужен.
Отметим, что и в первом примере с кнопкой автоматически сгенерировался заголовок метода, обеспечивающего обработку клика мышкой:
private void button1_Click(object sender, EventArgs e)
Сам класс EventArgs не содержит полей, которые могут быть использованы для передачи дополнительных данных обработчику, он служит в качестве базового класса, от которого получается производный класс, содержащий все необходимые поля. Тем не менее, в классе EventArgs имеется одно поле Empty типа static, которое представляет собой объект типа EventArgs без данных.
В среде .NET Framework предоставляется встроенный обобщенный делегат под названием EventHandler<TEventArgs>. В данном случае тип TEventArgs обозначает тип аргумента, передаваемого параметру EventArgs события.
Для обработки многих событий параметр типа EventArgs оказывается ненужным. Поэтому с целью упрощения создания кода в подобных ситуациях в среду .NET Framework и был внедрен необобщенный делегат типа EventHandler, используемый для объявления обработчиков событий, которым не требуется дополнительная информация о событиях (см. наш первый пример).

Пример использования обобщенного делегата EventHandler<TEventArgs>

Обобщенный делегат  EventHandler<MyEA> используется для
объявления события Ez:

public event EventHandler<MyEA> Ez;

Аргументы, передаваемые в метод, задаются в классе MyEA, который наследуется от класса  EventArgs.

Постановка задачи «Управление размерами и цветом формы»

Набор цветов: Red, Green, Blue, Yellow + исходный (добавляйте любые!)
Размеры: 500х150, 550×200, 600×250, 650×300

Элементы управления:
Кнопка button1 — Разрешение/Запрет изменение свойств формы
Кнопка button2 — Перекраска формы в желтый цвет без ограничений
Элемент comboBox1 — для выбора цвета: Red, Green, Blue, прежний
Метка label1 — подсказка: «Выберите цвет закраски формы» к comboBox1.

Начальная форма может выглядеть так:

Создаются два класса:
1) Класс Моих Событий Аргументы:

class MyEA : EventArgs // с полями (добавляйте любые) :
{
   public char ch; // буква цвета
   public int htw; // высота формы
}

2) Мой класс Обработка события:

class MyEH
{
   public event EventHandler<MyEA> Ez; // мое событие
   public void OnEz(MyEA c)            // и его метод
}

Далее приводится текст файла Form1.cs с комментариями:

using System;
using System.Drawing;
using System.Windows.Forms;
namespace ОбобщенныйДелегатЦвет
{
public partial class Form1 : Form
{
   public static bool flag = false; // Запретить/разрешить смену цвета 
   char []symb = {'R','G','B','X'}; // массив символов, см.comboBox1 
   int[] heigth = { 150, 200, 250, 300 };
public Form1()
{
   InitializeComponent();
}
// Переключатель флага: Разрешить/Запретить смену цвета формы
private void button1_Click(object sender, EventArgs e)
{
   if (flag)
   {
      flag = false;
      button1.Text = "Изменение цвета формы разрешить!";
   }
   else
   {
      flag = true;
      button1.Text = "Изменение цвета формы запретить!";
   }
}
// Изменение цвета (4 цвета) и размеров формы
private void ChangeBackColor(object sender, MyEA e)
{
   switch (e.ch)
   {
      case 'R':
      {
         this.BackColor = Color.Red;
         this.Height = heigth[0];
         this.Width = 350 + heigth[0];
         break;
      }
      case 'G':
      {
         this.BackColor = Color.Green;
         this.Height = heigth[1];
         this.Width = 350 + heigth[1];
         break;
      }
      case 'B':
      {
         this.BackColor = Color.Blue;
         this.Height = heigth[2];
         this.Width = 350 + heigth[2];
         break;
      }
      case 'Y':
      {
         this.BackColor = Color.Yellow;
         this.Height = heigth[3];
         this.Width = 350 + heigth[3];
         break;
     }
     default: // оставить без изменений
        break;
   }; // end switch
}
// Реакция на изменение выбора цвета формы
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
   MyEH met = new MyEH(); // объект класса Моя обработка событий
   // Получить аргументы: букву цвета и высоту формы - в объект e1:
   int i = comboBox1.SelectedIndex;// номер цвета в массиве symb[]
   MyEA e1 = new MyEA(); // объект класса Мои Аргументы Событий
   e1.ch = symb[i]; // буква цвета
   e1.htw = heigth[i]; // высота формы
   if (flag)
      met.Ez += ChangeBackColor; // Событие Ez связать с методом
   else
      met.Ez -= ChangeBackColor; // Или разорвать связь
   // вызов метода обработки события, связанного с met.Ez
   met.OnEz(e1);
}
// Покраска в желтый без условий
private void button2_Click(object sender, EventArgs e)
{
   MyEH met2 = new MyEH(); // новый объект-событие и пр.
   MyEA e2 = new MyEA(); // новый объект-аргументы:
   e2.ch = 'Y'; // первый
   e2.htw = heigth[3]; // второй
   met2.Ez += ChangeBackColor; // связывание события с методом 
                               // (его аргументы класса MyEA)
   met2.OnEz(e2); // вызов метода
}
} // end класса Form1
// Класс Моих Событий Аргументы
class MyEA : EventArgs
{
   public char ch; // буква цвета
   public int htw; // высота формы
}
// Мой класс Обработка события
class MyEH
{
   public event EventHandler<MyEA> Ez; // мое событие
   public void OnEz(MyEA c) // и его метод
   {
   if (Ez != null) // Есть ли событие?
      Ez(this, c); // Старт события
   } 
} // end класса
} // end namespace!

Вид формы после перекраски:

Другой пример применения делегатов рассмотрен в посте «Делегаты и методы».

2 мыслей о “События и делегаты”

  1. Вячеслав Рычков

    Виталий, спасибо за внимательность. Поясню, почему не получается у Вас выбрать цвет:
    1) comboBox1 — это элемент, в котором должен быть задан список. Напишите:
    Red
    Green
    Blue
    Yellow
    или если захотите по-русски:
    красный
    зеленый
    синий
    желтый.
    2) Проверьте, связано ли событие SelectedIndexChanged с обработчиком события combobox1_SelectedIndexChanged() — окно Свойства, закладка — События (Events).
    3) В этом обработчике событий изменяется объект e1 класса MyEA (через свойство comboBox1.SelectedIndex): e1.ch = symb[i]; где i — индекс выделенного элемента.
    4) А далее событие met.Ez связывается с методом ChangeBackColor().
    В примере упомянутые операторы выделены красным цветом, но сам текст мною не был изменен.
    Мне следовало бы все это описать более подробно, но я решил оставить исходный текст как есть, пометив только красным цветом некоторые операторы и написав комментарий. Запустите программу с учетом этого ответа.

Оставьте комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *