Идея механизма событий библеотеки виджетов 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, но имеет свои отличия, которые я намеренно ввел.

Мне кажется, что представленная реализация более-менее эффективная с точки зрения производительности, хотя тестирования я еще не проводил. Однако, я уверен, что эту идею можно реализовать эффективно.
Re: Идея механизма событий библеотеки виджетов GUI
От: samius Япония http://sams-tricks.blogspot.com
Дата: 06.03.09 20:05
Оценка:
Здравствуйте, Inq, Вы писали:

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


Я полагаю проект настолько кишит событиями, что такая реализация механизма событий просто необходима, чтобы сэкономить время, объявляя сообщение одной строкой, вместо двух?

З.Ы. Наверное в проекте нет других проблем?

Навеяно этим топиком
Автор: Mamut
Дата: 06.03.09
.
Re[2]: Идея механизма событий библеотеки виджетов GUI
От: Inq  
Дата: 06.03.09 20:12
Оценка:
Здравствуйте, samius, Вы писали:

S>З.Ы. Наверное в проекте нет других проблем?


S>Навеяно этим топиком
Автор: Mamut
Дата: 06.03.09
.


Даже не особо важно сколько строк требуется для объявления события в классе. Просто вместе с удобной нотацией получаем разнообразные способы управления событиями, которые пока я не реализовал в том чилсе потому, что не знаю, есть ли коренные недостатки этого способа. Это эксперимент, а не реальный проект. И почему бы не существовать системам с большим количеством событий и почему такой способ не подойдет?
Re[3]: Идея механизма событий библеотеки виджетов GUI
От: samius Япония http://sams-tricks.blogspot.com
Дата: 06.03.09 20:17
Оценка:
Здравствуйте, Inq, Вы писали:

Inq>Здравствуйте, samius, Вы писали:


S>>З.Ы. Наверное в проекте нет других проблем?


S>>Навеяно этим топиком
Автор: Mamut
Дата: 06.03.09
.


Inq>Даже не особо важно сколько строк требуется для объявления события в классе. Просто вместе с удобной нотацией получаем разнообразные способы управления событиями, которые пока я не реализовал в том чилсе потому, что не знаю, есть ли коренные недостатки этого способа. Это эксперимент, а не реальный проект. И почему бы не существовать системам с большим количеством событий и почему такой способ не подойдет?


Я не утверждаю, что этот способ не подойдет. Я сомневаюсь в его необходимости для реального проекта, целью которого не является написание еще одной библиотеки виджетов ради удобной нотации записи событий.

И поверьте, к архитектуре данная идея имеет весьма далекое отношение.
Re[4]: Идея механизма событий библеотеки виджетов GUI
От: Inq  
Дата: 06.03.09 20:27
Оценка:
S>Я не утверждаю, что этот способ не подойдет. Я сомневаюсь в его необходимости для реального проекта, целью которого не является написание еще одной библиотеки виджетов ради удобной нотации записи событий.

В таком случае, пусть, целью проекта будет написание еще одной библитеки виджетов : ) Можно еще удобств придумать и может получится неплохая библиотека.

S>И поверьте, к архитектуре данная идея имеет весьма далекое отношение.


Честно говоря, тоже не был уверен с выбором раздела, но может модераторы перенаправят, если совсем неправильный выбор.
Re: Идея механизма событий библеотеки виджетов GUI
От: SergH Россия  
Дата: 07.03.09 11:29
Оценка:
Здравствуйте, Inq, Вы писали:

Inq>Здравствуйте . Прошу обсудить достоинства и недостатки такой идеи для реализации механизма событий для библиотеки виджетов.


Обработчики не имеют доступа к внутренностям класса. До тех пор, пока они выводят строчку на консоль, это не проблема, конечно....
Делай что должно, и будь что будет
Re[2]: Идея механизма событий библеотеки виджетов GUI
От: Inq  
Дата: 08.03.09 03:30
Оценка:
Здравствуйте, SergH, Вы писали:

SH>Обработчики не имеют доступа к внутренностям класса. До тех пор, пока они выводят строчку на консоль, это не проблема, конечно....


Т. к. обработчик SomeWidget::Event<SomeEvent>::On() статическая функция класса SomeWidget::Event<SomeEvent>, то узнать, через какой объект ее вызвали удасться только передав ей в качестве параметра указатель на этот объект. Например, можно подправить код так:


typedef std::map<EventID, void (*)(BaseWidget* /* другой список параметров */ )> Events;

template<class T> class Widget {
    // ...
    template<EventID id> class Event {
    public:
        // ...
        // Теперь можно узнать, кому адресованно сообщение.
        static void On(BaseWidget*);
        // ...
    };
    // ...
}

class MyWidget : public Widget<MyWidget> {
public:
    MyWidget(int i)
    :    i(i)
    {
    }
    // Доступ к закрытым данным для событий, не имеющих к ним доступ на прямую.
    int GetI() const {
        return i;
    }
    Event<Paint> OnPaint;
    Event<MyEvent> OnMyEvent;
private:
    int i;
    friend Event<Paint>; // Чтобы получить доступ к закрытым данным.
};

// ...

void MyWidget::Event<Paint>::On(BaseWidget* widget)
{
    std::cout << "MyWidget::Event<Click>::On()" << std::endl;

    // Без приведения тивов никак, но это есть даже в WindowsForms,
    // хотя в переопределенных виртуальных функциях это естественно не нужно.
    MyWidget* w = dynamic_cast<MyWidget*>(widget);

    // Получен доступ к данным объекта.
    std::cout << "i: " << w->i << std::endl;
}
void MyWidget::Event<MyEvent>::On(BaseWidget* widget)
{
    std::cout << "MyWidget::Event<MyEvent>::On()" << std::endl;

    MyWidget* w = dynamic_cast<MyWidget*>(widget);

    // Получен доступ к данным объекта, но немного подругому.
    std::cout << "i: " << w->GetI() << std::endl;
}

int main()
{
    ScrollBar a;
    Button b;

    // У класса теперь есть какие-то данные, нужные для выполнения события.
    MyWidget m(100);

    typedef std::vector<BaseWidget*> Widgets;
    Widgets widgets(3);
    widgets[0] = &a;
    widgets[1] = &b;
    widgets[2] = &m;

    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);

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

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

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

    return 0;
}
Re[3]: Идея механизма событий библеотеки виджетов GUI
От: Inq  
Дата: 08.03.09 04:04
Оценка:
Inq> // Без приведения тивов никак, но это есть даже в WindowsForms,
Inq> // хотя в переопределенных виртуальных функциях это естественно не нужно.

Что-то я напутал. В WinForms функции, которые обрабатывают сообщения являются членами класса контролов и поэтому они знаю через this, кого они обрабатывают.
Я пока не придумал, как передать информацию об обрабатываемом объекте без передачи объекта в качестве параметра события.
Re[4]: Идея механизма событий библеотеки виджетов GUI
От: Inq  
Дата: 08.03.09 07:54
Оценка:
Inq>Что-то я напутал. В WinForms функции, которые обрабатывают сообщения являются членами класса контролов и поэтому они знаю через this, кого они обрабатывают.
Inq>Я пока не придумал, как передать информацию об обрабатываемом объекте без передачи объекта в качестве параметра события.

А, нет, я был прав в начале : ) Обработчику в WinForms передается object в первом параметре, который является получателем, а сам обработчик статическая функция класса и в ней естественно нет ссылки this. Мой обработчик хоть и является нестатическим членом класса, однако создается один раз на класс.
Re: Идея механизма событий библеотеки виджетов GUI
От: Кывт Ниоткуда  
Дата: 08.03.09 18:34
Оценка: 6 (1)
Насколько я понял, идея бесперспективна.

1. В такой реализации, которую ты привел, она вообще бесполезна, так как обработчик события — один на весь класс и не имеет доступа к экземпляру класса. Если расширить ее до того, чтобы она поддерживала экземпляры классов, то идея должна как-то измениться. Как ты собираешься передавать экземпляры? Мне ведь нужно подписать конкретный обработчик на событие конкретной кнопки, а не всех кнопок в программе.

Приведи пример, как с помощью твоей идеи (уже в расширенном виде) я могу решить следующую задачу:

class Button : public Widget
{
public:

    Event<ClickedEvent> Clicked;  
};

class MyForm : public Form
{
public:

    MyForm()
    {
        button1.Clicked += ClickedEventHandler(*this, &MyForm::OnButton1Clicked);
        button2.Clicked += ClickedEventHandler(*this, &MyForm::OnButton2Clicked);
    }

private:

    Button button1;
    Button button2;

private:

    void OnButton1Clicked(Point location)
    {
        // Has to be called when button1 is clicked.
    }

    void OnButton2Clicked(Point location)
    {
        // Has to be called when button2 is clicked.
    }
};


2. А как передавать параметры? Все сообщения не могут быть без параметров.

3. Целочисленные идентификаторы сообщений. Что делать с идентификаторами сообщений при наследовании? Допустим, в базовом классе использованы идентификаторы с нуля по 5, в производном классе, значит, мы должны использовать идентификаторы начиная с 6? Да? Надо как-то это обеспечить, и сделать так, чтобы никто про это не забыл. Хорошо, а если мы решили ввести дополнительное сообщение в базовый класс? А множественное наследование? Даже если это и получится, это будет в любом случае уродливо и ненадежно.

4. При полноценной реализации эффективность окажется плохой. Сколько в интерфейсе приложения может быть объектов, произведенных от класса Widget? Десятки тысяч. Сколько в них может быть задействовано обработчиков сообщений? Сотни тысяч. При этом тебе нужно будет создать маппинг для каждого обработчика. Таким образом, каждый вызов каждого обработчика будет вызывать поиск по внушительному индексу. Насколько это замедлит быстродействие, трудно предсказать.

5. Если у тебя Event имеет хоть какие-то данные, то, в любом случае — объявлять его одной строчкой некошерно. Потому что это получаются открытые данные класса.

На самом деле, проблема-то серьезная. Похоже, что и особо хорошего решения для нее не существует — у всех есть свои недостатки.

В MFC — страшные макросы.
В QT — (короткие и простые) макросы + дополнительный препроцессор.

Я такую проблему решил с помощью делегатов на C++. Перед решением изучил изыскания Дона Клагстона. Я взял только общую идею, но реализовал гораздо проще. Внешне получилось точно как в дотнете.

Но когда реализовал, сразу понял, в чем недостаток: если сделать много event'ов, получается много неиспользуемой памяти. Допустим, неиспользованный event использует четыре байта памяти (место под указатель). У меня в контроле 33 event'а, из них используются 3. Неиспользуемой памяти 30 x 4 = 120 байт на каждый контрол. Допустим, в диалоге 3000 контролов — получаем 3000 x 120 = ~360 кбайт совершенно впустую.

Теперь думаю про другой подход. На самом деле, в итоге некий подход, подобный твоему, наверно, и будет оптимальным, только не в таком виде.
Re[2]: Идея механизма событий библеотеки виджетов GUI
От: Inq  
Дата: 09.03.09 12:40
Оценка: 2 (1)
Здравствуйте, Кывт, Вы писали:

К>1. В такой реализации, которую ты привел, она вообще бесполезна, так как обработчик события — один на весь класс и не имеет доступа к экземпляру класса. Если расширить ее до того, чтобы она поддерживала экземпляры классов, то идея должна как-то измениться. Как ты собираешься передавать экземпляры? Мне ведь нужно подписать конкретный обработчик на событие конкретной кнопки, а не всех кнопок в программе.


К>Приведи пример, как с помощью твоей идеи (уже в расширенном виде) я могу решить следующую задачу:


К>
К>class Button : public Widget
К>{
К>public:

К>    Event<ClickedEvent> Clicked;  
К>};

К>class MyForm : public Form
К>{
К>public:

К>    MyForm()
К>    {
К>        button1.Clicked += ClickedEventHandler(*this, &MyForm::OnButton1Clicked);
К>        button2.Clicked += ClickedEventHandler(*this, &MyForm::OnButton2Clicked);
К>    }

К>private:

К>    Button button1;
К>    Button button2;

К>private:

К>    void OnButton1Clicked(Point location)
К>    {
К>        // Has to be called when button1 is clicked.
К>    }

К>    void OnButton2Clicked(Point location)
К>    {
К>        // Has to be called when button2 is clicked.
К>    }
К>};
К>


Вот переделанная реализация. Задача решена как-никак.

Над остальными пунктами пока думаю. Спасибо за подробный ответ.
Re[2]: Идея механизма событий библеотеки виджетов GUI
От: Inq  
Дата: 09.03.09 13:42
Оценка:
Здравствуйте, Кывт, Вы писали:

К>Я такую проблему решил с помощью делегатов на C++. Перед решением изучил изыскания Дона Клагстона. Я взял только общую идею, но реализовал гораздо проще. Внешне получилось точно как в дотнете.


Эта статья есть переведенная на RSDN
Автор(ы): Don Clugston
Дата: 27.07.2005
В данной статье предоставлен исчерпывающий материал по указателям на функции-члены, а также приведена реализация делегатов, которые занимают всего две операции на ассемблере.
: ) Я ее читал когда-то, почитаю еще, подумаю, что от туда можно извлечь.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.