Идея механизма событий библеотеки виджетов GUI
От: Inq  
Дата: 06.03.09 19:40
Оценка:
Здравствуйте . Прошу обсудить достоинства и недостатки такой идеи для реализации механизма событий для библиотеки виджетов. Сделал примерный эскиз, того, как это будет работать. Цель была сделать, чтобы для объявления того, что некий класс виджета (ну или контрола, элемента GUI) обрабатывает определенное событие, потребовалась всего одна строка в объявлении класса. Если приглядется к решению в MFC, то там требуется объявление функции-члена в классе контрола и в карте сообщения (которая работает на макросах, что не очень красиво), т. е. требуется две строки. Гораздо лучше было бы использовать только средства языка C++.

То, что получилось работает так:

class MyWidget : public Widget<MyWidget> {
public:
    /* описание класса */

    // Объявление, что класс (т. е. все объекты этого класса) имеют сообщение с идентификатором MyEvent.
    // Название для сообщения можно выбирать тем не менее произвольное.
    Event<MyEvent> OnMyEvent;
};

MyWidget::Event<MyEvent>::On()
{
    // реализация сообщения
}


Вот код консольного приложения, который работает и его можно посмотреть в действии:

#include <map>
#include <vector>
#include <iostream>

// Виды событий. Можно использовать в принципе и просто целочисленные константы.
enum EventID { Paint, Move, Click, MyEvent };

// Алиас отображения EventID в указатели на функции
typedef std::map<EventID, void (*)()> Events;

// Через указатели на этот класс обеспечивается доступ
// к множеству событий виджетов неизвестного производного класса.
class BaseWidget {
public:
    virtual Events* GetEvents() {
        return 0; // В случае, если событий у виджета нет.
    }
    virtual ~BaseWidget() {
    }
};

// Шаблон основного класса Widget.
template<class T> class Widget : public BaseWidget {
public:
    // При создании первого экзепляра специализации шаблона
    // инициализируем указатель пустым множеством событий.
    Widget() {
        if (count_++ == 0) {
            events_ = new Events;
        }
    }
    // Аналогично, при уничтожении последнего экземпляра специализации шаблона,
    // удаляем множество событий.
    ~Widget() {
        if (--count_ == 0) {
            delete events_;
        }
    }
    // Доступ к множеству событий.
    virtual Events* GetEvents() {
        return events_;
    }
    // Шаблон класса событий для специализации шаблона виджета.
    template<EventID id> class Event {
    public:
        // При инициализации события добавляем его
        // в множество специализации класса виджета.
        Event() {
            // Инициализируем событие id один раз для класса.
            if (!added_) {
                (*Widget<T>::events_)[id] = &On;
                added_ = true;
            }
        }
        // Деструктор не нужен, т. к. Widget<T>::events_ удалится,
        // как только не останится ни одного экземпляра Widget<T>

        // Функция, которую мы специализируем для разных виджетов T и сообщений id
        // и получаем нужное поведение для каждого.
        static void On();
        // Флаг для оптимизации.
        static bool added_;
    };
    static Events* events_;
    static int count_;
};

// Инициализируем статические переменные шаблонов классов
template<class T> 
    template<EventID id>
        bool Widget<T>::Event<id>::added_(0);

template<class T>
    Events* Widget<T>::events_(0);

template<class T>
    int Widget<T>::count_(0);


// Класс скроллбара например.
class ScrollBar : public Widget<ScrollBar> {
public:
    Event<Paint> OnPaint; // Он рисуется заданным алгоритмом.
};

// Класс кнопки
class Button : public Widget<Button> {
public:
    Event<Paint> OnPaint;
    Event<Click> OnClick; // На кнопку можно нажать.
};

// Еще какой-то виджет.
class MyWidget : public Widget<MyWidget> {
public:
    Event<Paint> OnPaint;
    Event<MyEvent> OnMyEvent;
};

// Процедура рисования скроллабара.
void ScrollBar::Event<Paint>::On()
{
    std::cout << "ScrollBar::Event<Paint>::Do()" << std::endl;
}
// Процедура рисования кнопки.
void Button::Event<Paint>::On()
{
    std::cout << "Button::Event<Paint>::Do()" << std::endl;
}
// Процедура реакции кнопки на нажатие.
void Button::Event<Click>::On()
{
    std::cout << "Button::Event<Click>::Do()" << std::endl;
}

void MyWidget::Event<Paint>::On()
{
    std::cout << "MyWidget::Event<Click>::Do()" << std::endl;
}
void MyWidget::Event<MyEvent>::On()
{
    std::cout << "MyWidget::Event<MyEvent>::Do()" << std::endl;
}

int main()
{
    ScrollBar a;
    Button b;
    MyWidget m;

    typedef std::vector<BaseWidget*> Widgets;
    Widgets widgets(3);
    widgets[0] = &a;
    widgets[1] = &b;
    widgets[2] = &m;
    
    // Примерно так будет выглядеть обработка например в WndProc,
    // только доступ к виджетам будет, например
    // из static std::map<HWND, BaseWidget*> BaseWidget::widgets_
    for (Widgets::iterator I = widgets.begin(); I != widgets.end(); ++I) {
        Events::iterator i;
        Events& events = *((*I)->GetEvents());

        i = events.find(Paint);
        if ( i != events.end() ) i->second();

        i = events.find(Click);
        if ( i != events.end() ) i->second();

        i = events.find(Move);
        if ( i != events.end() ) i->second();

        i = events.find(MyEvent);
        if ( i != events.end() ) i->second();
    }

    return 0;
}


Следствие изначального требования удобства набора кода получилось таким. К классу можно добавлять сообщения во время выполнения, но можно это и запретить. Можно вообще удалить все сообщения у класса во время выполнения.

Можно добавить к шаблону Event<EventID> переменные-члены в виде указателя на обычную функцию, указателя на класс виджета T, функтор, множество вышеописанных объектов, то можно заставить класс при возникновении события выполнять более сложные действия, которые можно добавлять во время выполнения из разных точек программы.

Если добавить переменную-член в шаблон событий, которая представляет собой std::map<T*, functor_type*>, то можно ассоциировать определенное поведение при возникновении события уже с определенным экземпляром класса.

В MFC множество событий закреплена на стадии компиляции, если мне не изменяет память. Реализованный здесь механизм во многом напоминает механизм сообщений в .NET, но имеет свои отличия, которые я намеренно ввел.

Мне кажется, что представленная реализация более-менее эффективная с точки зрения производительности, хотя тестирования я еще не проводил. Однако, я уверен, что эту идею можно реализовать эффективно.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.