Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » Visual C++ - Система »

Обсудить на форуме Обсудить на форуме

Реализация обработки событий на C++

Событием (event) называется исходящий вызов. Этот термин, наверное, хорошо знаком тем, кто работает с такими языками, как Delphi, Visual Basic и т.д. При возникновении события происходит вызов всех его обработчиков. Так как объект-инициатор события может ничего не знать об обработчиках, то событие называют исходящим вызовом. Работа события происходит по принципу "от одного объекта к нескольким". Важно отметить, что событие (event) и сообщение (message) это разные понятия. Сообщением называется прямой вызов, который передаётся от объекта к объекту. То есть у сообщения имеется один обработчик.

События применяются довольно широко. Примером могут служить всевозможные библиотеки, реализующие графический интерфес пользователя. Но события при правильном применении могут оказаться ДЕЙСТВИТЕЛЬНО ПОЛЕЗНОЙ ВЕЩЬЮ К сожалению исторически сложилось так, что в C++ нет событий. Поэтому при необходимости разработчики реализуют их на уровне библиотеки. Здесь вашему вниманию представлена реализация одной такой библиотеки. В ней есть два класса: Delegate и Event.

Класс Delegate (делегат)

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

В архиве, который прилагается к статье имеется заголовочный файл Delegate.h, в котором находится объявление и реализация класса Delegate. Для того, чтобы понять как используется этот класс рассмотрим пару примеров.

Примеры использования класса Delegate

Пример №1

#include <iostream>
using namespace std;

#include "Delegate.h"

class Test
{
public:
    Test()
    {
        //Передаем делегату указатель на функцию
        _delegate = Delegate(this, &Test::SayHello);
    }

    void SayHello()
    {
        cout << "Hello!" << endl;
    }

    void SayGoodBye()
    {
        cout << "Good Bye!" << endl;
    }

    void RunTest()
    {
        //Делаем вызов делегата первый раз
        _delegate();
        //Теперь самое интересное. Лёгким движением рук подставляем
        // в делегат другую функцию
        _delegate = Delegate(this, &Test::SayGoodBye);
        //Второй раз вызываем делегат
        _delegate();
    }

private:
    Delegate _delegate;
};

int main()
{
    Test test;
    test.RunTest();

    return 0;
}

Результаты работы этой программы должны быть следующими:

Разберём программу подробнее. Вначале мы подключаем заголовочный файл класса Delegate:

    #include "Delegate.h"

Затем делаем объявление простого класса Test. Этот класс содержит в сбе частную переменную делегат. В неё мы будем подставлять функции нашего класса. Первым делом в конструкторе Test передаём делегату указатель на функцию Test::SayHello(). Как видно из реализации функции main, сразу после конструирования экземпляра Test происходит вызов функции Test::RunTest(). В ней то и заключена основная суть программы. После начала выполнения функции RunTest() делается вызов делегата как обычной функции C++

    void RunTest()
    {
        //Делаем вызов делегата первый раз
        _delegate();
        ...

Делегат передаёт этот вызов той функции, на которую он указывает. Если же делегат содержит NULL, то вызов игноррируется. Далее мы меняем функцию, на которую указывает делегат:

        ...
        _delegate = Delegate(this, &Test::SayGoodBye);
        //Второй раз вызываем делегат
        _delegate();
    }

Для демонстрации полученного эффекта повторно делаем вызов. При этом происходит вызов функции Test::SayGoodBye().

Данный пример довольно прост, но всё же он демонстрирует возможности, которыми обладают делегаты и их основные преимущества.

Пример №2: Небольшая база данных

Теперь после первого знакомства с делегатами пришло время рассмотреть более сложный приём и понять, как можно применить делегаты в своих проектах. Предположим, что Вы проектируете небольшую базу данных для какого-нибудь предприятия. Эта база данных должна содержать в себе сведения о работниках предприятия. Эти сведения включают в себя:

  • Имя работника
  • Его оклад
  • Размер начисляемой премии (в %)
  • Категорию

Размер заработной платы определяется с учётом премии. Добавим в программу класс служащего (Employe)

  • Объявление класса Employe находится в файле Employe.h
  • Реализация класса Employe находится в файле Employe.cpp

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

Следующий класс, который мы добавим в наш проект будет представлять собой базу данных предприятия по рабочим. Назовём этот класс EnterpriseDB. Этот класс будет обладать минимальной функциональностью, но всё же он будет являться центральной частью нашего проекта.

  • Объявление класса EnterpriseDB находится в файле EnterpriseDB.h
  • Реализация класса EnterpriseDB находится в файле EnterpriseDB.cpp

Класс EnterpriseDB в сущности представляет собой коллекцию указателей на объекты Employe. Имеется один метод для вставки объектов (EnterpriseDB::Add(std::string, int)). Метод принимает в качестве параметров имя работника и его категорию. Затем происходит выделение памяти под объект Employe* и вставка его в конец коллекции. В деструкторе EnterpriseDB уничтожает все указатели Employe*. С помощью метода SetupPayments(Employe *) происходит распределение окладов и премий в зависимости от категории служащего.

Но самой важной функцией в классе, а возможно и во всей программе является функция ProcessStaff(Delegate &). С помощью этого метода можно для каждого из работников, находящихся в коллекции _staff произвести вызов метода, который скрывается за делегатом. Далее в основной функции программы main производятся несколько таких вызовов.

Всякому предприятию жизненно необходим свой бухгалтер. Наше предприятие не является исключением, поэтому настало время добавить класс, который реализует простую бухгалтерию. Класс будет называться Accountant (от англ. "бухгалтер").

  • Объявление класса Accountant находится в файле Accountant.h
  • Реализация класса Accountant находится в файле Accountant.cpp

Класс бухгалтера предоставляет следующие возможности. Он позволяет вести учёт рабочего с помощью метода AddEmployeToAccount(Employe *). При выполнении этого метода счётчик работников _count увеличивается на 1, а к общей зарплате _totalWage добавляется зарплата данного работника, которая вычисляется с помощью метода Employe::CalculateWage(). Метод GetTotalWage() возвращает количество денег, которые выплачиваются всем рабочим в качестве зарплаты. GetAverageWage() вычисляет среднюю зарплату по предприятию.

Наконец пришло время написать главную функцию программы. Её реализация представлена ниже

#include <iostream>
using namespace std;

#include "Employe.h"
#include "EnterpriseDB.h"
#include "Accountant.h"
#include "Delegate.h"

class EmployeTester
{
public:

    // Распечатать информацию о работнике
    void PrintInfo(Employe *employe);

    // Нанять несколько работников
    void AddEmployes(EnterpriseDB *db);
};

int main()
{
    EmployeTester *test = new EmployeTester;
    EnterpriseDB *db = new EnterpriseDB;
    Accountant *acc = new Accountant;

    test->AddEmployes(db);

    db->ProcessStaff(Delegate(db, &EnterpriseDB::SetupPayments));
    cout << "Enterprise staff" << endl << endl;
    db->ProcessStaff(Delegate(test, &EmployeTester::PrintInfo));

    db->ProcessStaff(Delegate(acc, &Accountant::AddEmployeToAccount));
    cout << "Employes count: " << acc->GetCount() << endl;
    cout << "Total wage: $" << acc->GetTotalWage() << endl;
    cout << "Average wage: $" << acc->GetAverageWage() << endl;

    delete test;
    delete db;
    delete acc;

    return 0;
}

void EmployeTester::PrintInfo(Employe *employe)
{
    cout << "Name: " << employe->GetName() << endl
        << "Category: " << employe->GetCategory() << endl
        << "Salary: $" << employe->GetSalary() << endl
        << "Bonus: " << employe->GetBonus() * 100 << "%" << endl
        << endl;
}

void EmployeTester::AddEmployes(EnterpriseDB *db)
{
    db->Add("Gates Bill", 1);
    db->Add("Bush George", 2);
    db->Add("Payne Max", 3);
}

Результат выполнения программы приведён на рисунке ниже

Разберёмся, что же происходит в этой программе. Во-первых, следует отметить новый класс, который выполняет в основном вспомогательные функции. Этот класс называется EmployeTester. Он имеет два метода. Первый из них - PrintInfo(Employe *) распечатывает информацию о сотруднике, который передаётся ему в качестве параметра. Второй метод AddEmployes(EnterpriseDB *) просто заполняет базу данных несколькими сотрудниками.

Ну а сейчас перейдём к рассмотрению главной функции программы. Именно в ней происходят самые главные события, связанные с использованием делегатов. Сначала программа создаёт базу данных, бухгалтера и объект EmployeTester:

    EmployeTester *test = new EmployeTester;
    EnterpriseDB *db = new EnterpriseDB;
    Accountant *acc = new Accountant;

Затем в базу данных вносятся сведения о трёх работниках:

    test->AddEmployes(db);

А теперь начинается самое важное. С помощью метода EnterpriseDB::ProcessStaff происходит назначение всем работникам зарплат и премий. Для этого методу ProcessStaff передаётся в качестве параметра делегат, который указывает на функцию EnterpriseDB::SetupPayments. Соответственно происходит вызов этой функции для каждого из работников в базе данных. Так служащему правильно назначается зарплата и премия в соответствии с его категорией:

    db->ProcessStaff(Delegate(db, &EnterpriseDB::SetupPayments));

После установки начислений служащим в дело вступает объект EmployeTester. Он выводит на экран информацию о каждом из рабочих:

    db->ProcessStaff(Delegate(test, &EmployeTester::PrintInfo));

А сейчас пришло время бухгалтеру подсчитать всех рабочих и определить суммарную зарплату и среднюю зарплату по предприятию. Результаты выводятся на экран:

    db->ProcessStaff(Delegate(acc, &Accountant::AddEmployeToAccount));
    cout << "Employes count: " << acc->GetCount() << endl;
    cout << "Total wage: $" << acc->GetTotalWage() << endl;
    cout << "Average wage: $" << acc->GetAverageWage() << endl;

Подведём итоги. Делегаты удобно применять в следующих случаях:

  • Нужно вызывать только один метод
  • Вызывающему коду не нужно знать об объекте, которому принадлежит метод
  • Класс может иметь несколько реализаций одного метода

Класс Event (событие)

Событие в данном случае - это способ для класса оповестить клиентов (классов, использующих данный класс) о том, что с ним произошло что-то интересное. Наверное самый знакомый способ использования событий - в графическом пользовательском интерфейсе (GUI). Обычно классы, представляющие элементы управления (controls) в графическом интерфейсе имеют события, которые срабатывают, когда пользователь что-либо делает с элементом управления (например, нажимает на кнопку). Но события могут быть использованы не только в графическом интерфейсе. События обеспечиают действительно полезный путь для объектов сигнализировать об изменении своего состояния. Причём это изменение может быть важно для клиентов данного объекта. События - это важные строительные блоки для создания классов, которые могут быть повторно использованы во многих программах. События используются непосредственно вместе с делегатами. Важно помнить, что объекты - делегаты инкапсултруют (скрывают) метод класса и он может быть вызван анонимно. Событие - это способ вызвать методы других объектов, когда событие срабатывает. То есть сторонний объект помещает указатель на свой метод в делегат (как было показано ранее) и передаёт этот делегат событию. При срабатывании события вызывается делегат, а следовательно и тот метод, который был ему передан.

Пример использования класса Event

#include <iostream>
#include <string>
using namespace std;

#include "Event.h"

// Цвет
class Color
{
public:

    Color(unsigned short r, unsigned short g, unsigned short b,
        std::string displ)
        : red(r), green(g), blue(b), display(displ)
    {
    }

    Color(const Color &color)
        : red(color.red), green(color.green), blue(color.blue)
        , display(color.display)
    {
    }

    std::string ToString() const
    {
        return display;
    }

    static const Color Black;
    static const Color White;
    static const Color Red;
    static const Color Green;
    static const Color Blue;

private:
    unsigned short red, green, blue;
    std::string display;
};

const Color Color::Black(0, 0, 0, "Black");
const Color Color::White(255, 255, 255, "White");
const Color Color::Red(255, 0, 0, "Red");
const Color Color::Green(0, 255, 0, "Green");
const Color Color::Blue(0, 0, 255, "Blue");

// Элемент, вид - можно назвать по-разному
class View
{
public:

    // Событие, срабатывающее при смене цвета
    Event ColorChanged;

    View(): _color(Color::White)
    {
    }

    Color GetColor() const
    {
        return _color;
    }

    void SetColor(const Color &color)
    {
        _color = color;
        ColorChanged();
    }

private:
    Color _color;
};

// Производный класс от View
class NewView: public View
{
public:

    NewView()
    {
        // Добавляем функцию реакции на событие
        ColorChanged += Delegate(this, &NewView::OnColorChange);
    }

    void OnColorChange()
    {
        cout << "in NewView::OnColorChange()" << endl;
        cout << "Color has changed" << endl;
        cout << "Current color is " << GetColor().ToString() << endl
             << endl;
    }
};

// Наблюдатель за событиями
class EventListener
{
public:

    EventListener(View *view)
        : _pView(view)
    {
        // Присоединяем функцию класса EventListener
        _pView->ColorChanged += Delegate(this, &EventListener::React);
    }

    void React()
    {
        cout << "in EventListener::React()" << endl << endl;
    }

    // Отсоединить функцию
    void Detach()
    {
        _pView->ColorChanged -= Delegate(this, &EventListener::React);
        _pView = NULL;
    }

private:
    View *_pView;
};

int main()
{
    NewView nv;
    EventListener el(&nv);
    cout << "Initial color is " << nv.GetColor().ToString() << endl;

    nv.SetColor(Color::Red);
    el.Detach();
    cout << "Event listener was detached" << endl;

    nv.SetColor(Color::Green);

    return 0;
}

Результат выполнения программы приведён ниже на рисунке.

Обсудим код. В этой программе задействованы три класса. Сначала рассмотрим класс Color, который описывает цвет. В нём есть следующие компоненты:

  • Красная, зелёная и синяя составляющие цвета (RGB)
  • Строка display, предназнвченная для вывода цвета на экран

Для удобства обращения с классом объявлены несколько статических констант - White, Black, Red, Green, Blue. Использовать класс Color можно следующим образом:

    Color c1, c2;
    //Теперь с1 содержит красный цвет, а с2 - чёрный
    c1 = Color::Red;
    c2 = Color::Black;
    //strC1 содержит "Red"
    std::string strC1 = c1.ToString();

Функция ToString() преобразует значение цвета в строку.

Следующий класс называется View. Он содержит в себе одну переменную определяющую цвет. Класс предоставляет методы для получения/установки значения цвета - это соответственно GetColor/SetColor. Также в классе View есть событие ColorChanged , которое срабатывает, когда программа изменяет значение цвета, то есть вызывает метод SetColor.

NewView - является производным классом от View. Его работа заключается в том, чтобы добавить свою функцию в событие ColorChanged, которое принадлежит его предку. Делается это в конструкторе NewView:

    NewView()
    {
        // Добавляем функцию реакции на событие
        ColorChanged += Delegate(this, &NewView::OnColorChange);
    }

Как видно, для того, чтобы передать свою функцию событию нужно сначала создать для неё обёртку ввиде делегата, а затем "прибавить" делегат к событию оператором +=.

"Наблюдатель" за событиями (EventListener) фактически поступает также как и класс NewView. Он получает указатель на View и добавляет к событию ColorChanged свой метод EventListener::React():

    EventListener(View *view)
        : _pView(view)
    {
        // Присоединяем функцию класса EventListener
        _pView->ColorChanged += Delegate(this, &EventListener::React);
    }

    void React()
    {
        cout << "in EventListener::React()" << endl << endl;
    }

Метод EventListener::Detach() отсоединяет от события свой метод React:

    // Отсоединить функцию
    void Detach()
    {
        _pView->ColorChanged -= Delegate(this, &EventListener::React);
        _pView = NULL;
    }

Отсоединение метода происходит также как и присоединение, только используется оператор -=.

Рассмотрим главную функцию программы. Сначала программа создаёт объекты EventListener и NewView и выводит начальное значение цвета в объекте NewView:

    NewView nv;
    EventListener el(&nv);
    cout << "Initial color is " << nv.GetColor().ToString() << endl;

Следует обратить внимание что на этот момент к событию ColorChanged присоединены два метода - это NewView::OnColorChanged() и EventListener::React(). В этом можно убедиться, изменив значение цвета методом SetColor. В результате будут вызваны две вышеупомянутые функции. Далее объектом EventListener производится отсоединение своего метода от события. Повторно вызвав метод SetColor убеждаемся, что на событие реагирует только один метод - NewView::OnColorChange():

    el.Detach();
    cout << "Event listener was detached" << endl;

    nv.SetColor(Color::Green);

Последние замечания

Архив, который идёт вместе со статьёй содержит в себе исходный код всех разобранных примеров. Для удобства также поставляются файлы проектов для Visual Studio.Net 2002. Виду отсутствия Visual C++ 6.0 программы на этом компиляторе не тестировались.

Если Вы хотите использовать классы Delegate и Event в своих проектах, подключите к ним файлы Delegate.h, Event.h, EventBase.h и EventBase.cpp.

Скачать все примеры в архиве


Может пригодится:


Автор: Олег Сотников
Прочитано: 11812
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру