Родилась мысль о том, как можно упростить реализацию GUI, конкретнее, хочется заставить систему саму отсжеливать состояния контролов. (enabled/disabled, visible, и пр)
Поясню на примере: пусть у нас есть диалог смены пароля. В этом диалоге есть поле ввода нового пароля и поле его подтверждения.
Кроме того, на диалоге есть кнопки OK и Cancel. Если поле вводе нового пароля пустое, кнопка OK должна быть в состоянии disabled. Если строка введенная в поле ввода нового пароля не равна строке в поле подтверждения, то OK также должна быть в состоянии disabed. Кроме того, пусть на диалоге будет текстовое поле с подсказкой. Если поле нового пароля пустое, подсказка будет отображать строку "пароль не может быть пустым". Если новый пароль и его подтверждение не совпадают, подсказка будет отображать строку "пароли не совпадают." Хочется описывать большую часть этого поведения декларативно, на уровне некой карты поведения диалога. (как карта сообщений в WTL или MFC.) Не очень понятно, как это сделать. Пока я думаю в сторону конечных автоматов, но возможно вы сможете подсказать что-нибудь еще?
Заранее спасибо.
PS: Есть мысль развить эту идею до уровня библиотеки. Сам я пишу на WTL, т.ч. первая версия будет для нее. В дальнейшем, можно будет сделать и для остальных библиотек (и языков).
Здравствуйте, Lonely Dog, Вы писали:
LD>Кроме того, на диалоге есть кнопки OK и Cancel. Если поле вводе нового пароля пустое, кнопка OK должна быть в состоянии disabled. Если строка введенная в поле ввода нового пароля не равна строке в поле подтверждения, то OK также должна быть в состоянии disabed. Кроме того, пусть на диалоге будет текстовое поле с подсказкой. Если поле нового пароля пустое, подсказка будет отображать строку "пароль не может быть пустым". Если новый пароль и его подтверждение не совпадают, подсказка будет отображать строку "пароли не совпадают." Хочется описывать большую часть этого поведения декларативно, на уровне некой карты поведения диалога. (как карта сообщений в WTL или MFC.) Не очень понятно, как это сделать. Пока я думаю в сторону конечных автоматов, но возможно вы сможете подсказать что-нибудь еще?
Таблицы решений, например. Для простых диалогов неплохо подходят, для сложных применять не доводилось.
Здравствуйте, Lonely Dog, Вы писали:
LD>Привет!
LD>Родилась мысль о том, как можно упростить реализацию GUI, конкретнее, хочется заставить систему саму отсжеливать состояния контролов. (enabled/disabled, visible, и пр) LD>Поясню на примере: пусть у нас есть диалог смены пароля. В этом диалоге есть поле ввода нового пароля и поле его подтверждения. LD>Кроме того, на диалоге есть кнопки OK и Cancel. Если поле вводе нового пароля пустое, кнопка OK должна быть в состоянии disabled. Если строка введенная в поле ввода нового пароля не равна строке в поле подтверждения, то OK также должна быть в состоянии disabed. Кроме того, пусть на диалоге будет текстовое поле с подсказкой. Если поле нового пароля пустое, подсказка будет отображать строку "пароль не может быть пустым". Если новый пароль и его подтверждение не совпадают, подсказка будет отображать строку "пароли не совпадают." Хочется описывать большую часть этого поведения декларативно, на уровне некой карты поведения диалога. (как карта сообщений в WTL или MFC.) Не очень понятно, как это сделать. Пока я думаю в сторону конечных автоматов, но возможно вы сможете подсказать что-нибудь еще?
LD>Заранее спасибо.
LD>PS: Есть мысль развить эту идею до уровня библиотеки. Сам я пишу на WTL, т.ч. первая версия будет для нее. В дальнейшем, можно будет сделать и для остальных библиотек (и языков).
1. Думаю, Ваш пример для автомата с одним состоянием (состояния controls я считаю внешними воздействиями).
2. Есть опасность появления циклов в графе состояний автомата. Это может привести к зависанию.
Здравствуйте, Lonely Dog, Вы писали:
LD>Родилась мысль о том, как можно упростить реализацию GUI, конкретнее, хочется заставить систему саму отсжеливать состояния контролов. (enabled/disabled, visible, и пр)
В MFC для тулбаров (в том числе с произвольными контролами) и меню эта штука уже существует — смотри ON_UPDATE_COMMAND_UI.
Теоретически, можно прикрутить его и к диалогам. (Не пробовал).
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, Lonely Dog, Вы писали:
К>В MFC для тулбаров (в том числе с произвольными контролами) и меню эта штука уже существует — смотри ON_UPDATE_COMMAND_UI. К>Теоретически, можно прикрутить его и к диалогам. (Не пробовал).
Правильно ли я понимаю, что в этом случае, в обработчике ON_UPDATE_COMMAND_UI для кнопки OK я должен написать примерно следующее:
if (strNewPassword.GetLength() && (strNewPassword == strNewPasswordConfirm))
wndOK.EnableWindow(TRUE);
else
wndOK.EnableWindow(FALSE);
Мне бы хотелось бы несколько другого поведения. Например, зачастую кнопка OK должна быть запрещена, пока не будут введены все данные. Следовательно, можно сказать, что кнопку OK мы разрешаем, когда все edit-box диалога содержат какие-то данные. По идее, можно привязать изменения всех полей ввода к установке неких флагов, а потом сказать, что если все флаги равны TRUE, то кнопку OKразрешаем. Моя идея заключалась в том, чтобы выделить те ситуации, когда некие контролы изменяют свое состояние в ответ на изменение состояние неких других контролов диалога. Возможно, для каждой ситуации можно написать некий универсальный кусок кода, который можно будет использовать в дальнейшем.
Здравствуйте, Sergey640, Вы писали:
S>1. Думаю, Ваш пример для автомата с одним состоянием (состояния controls я считаю внешними воздействиями).
Почему? Насколько я понимаю, есть три состояния:
1. пароль не введен
2. пароль введен, но не равен подтверждению.
3. пароль введен и равен подтверждению.
В первом и втором состоянии кнопка OK должна быть запрещена. В третьем разрешена.
S>2. Есть опасность появления циклов в графе состояний автомата. Это может привести к зависанию.
Да, наверное вы правы. Это надо будет как-то учитывать.
Здравствуйте, Miroff, Вы писали:
M>Таблицы решений, например. Для простых диалогов неплохо подходят, для сложных применять не доводилось.
А не могли бы вы рассказать, что это такое? Или где про это можно почитать?
Здравствуйте, Lonely Dog, Вы писали:
LD>Здравствуйте, Miroff, Вы писали:
M>>Таблицы решений, например. Для простых диалогов неплохо подходят, для сложных применять не доводилось. LD>А не могли бы вы рассказать, что это такое? Или где про это можно почитать?
В итоге все выльется в ON_UPDATE_COMMAND_UI или аналоги, которые универсальные,
но все равно требуют ручной записи условий.
Проблема в том, что условия то как раз совсем не уверсальные.
Для кнопки OK при вводе пароля нужны другие условия, чем для той же кнопки на другом диалоге.
Здравствуйте, Lonely Dog, Вы писали:
LD>Здравствуйте, Sergey640, Вы писали:
S>>1. Думаю, Ваш пример для автомата с одним состоянием (состояния controls я считаю внешними воздействиями). LD>Почему? Насколько я понимаю, есть три состояния: LD>1. пароль не введен LD>2. пароль введен, но не равен подтверждению. LD>3. пароль введен и равен подтверждению. LD>В первом и втором состоянии кнопка OK должна быть запрещена. В третьем разрешена.
Для автомата Мура — правда Ваша , для автомата Мили — моя.
Все дело в реализации. Флажок "Disabled" я склонен рассматривать как внешний, т.е. Какая-то ON_CHANGE function вызывается , проверяет состояние Controls устанавливает флажок и все. Дополнительных переменных состояния нет. Функция оценки и изменения флажка — одна для всех. Мне так кажется.
Автомат Мура будет дублировать в своих состояниях состояние флажка: добавляется переменная состояния, появляются функции переходов — это более громоздко для данного примера. Но для более сложных случаев, когда требуется помнить историю действий, это возможно и подойдет.
Правда автомат типа Мили обычно всегда меньше — я исходил из этого.
Здравствуйте, Аноним, Вы писали:
А>Здравствуйте, Lonely Dog, Вы писали:
А>В итоге все выльется в ON_UPDATE_COMMAND_UI или аналоги, которые универсальные, А>но все равно требуют ручной записи условий. А>Проблема в том, что условия то как раз совсем не уверсальные. А>Для кнопки OK при вводе пароля нужны другие условия, чем для той же кнопки на другом диалоге.
Резонно и серьезно, настолько , что может похоронить идею на корню. Лучше начать с проработки условий и способов их простого задания. Например установления связей между контролами и определения их характеристик типа "когда первый пустой — второй disabled". "когда первый равен второму — третий показывает prompt1" — Что-то сложно получается.
Здравствуйте, Lonely Dog, Вы писали:
LD>Правильно ли я понимаю, что в этом случае, в обработчике ON_UPDATE_COMMAND_UI для кнопки OK я должен написать примерно следующее:
LD>Мне бы хотелось бы несколько другого поведения. Например, зачастую кнопка OK должна быть запрещена, пока не будут введены все данные. Следовательно, можно сказать, что кнопку OK мы разрешаем, когда все edit-box диалога содержат какие-то данные. По идее, можно привязать изменения всех полей ввода к установке неких флагов, а потом сказать, что если все флаги равны TRUE, то кнопку OKразрешаем. Моя идея заключалась в том, чтобы выделить те ситуации, когда некие контролы изменяют свое состояние в ответ на изменение состояние неких других контролов диалога. Возможно, для каждой ситуации можно написать некий универсальный кусок кода, который можно будет использовать в дальнейшем.
По сути, мы имеем функцию U: (набор значений V -> набор состояний S контролов). Правильно?
Подход с UpdateCmdUI сводится к тому, что эта функция раскладывается на набор функций u[k] : (V -> состояние контрола s[k]). Эти функции реализуются в обработчиках сообщений WM_UPDATE_COMMAND_UI.
Причём моменты вычисления (и, соответственно, обновления состояний) произвольны. Можно в любой момент сказать, какое сейчас должно быть состояние, вне зависимости от истории. И в любой момент инициировать обновление.
А твоя идея (автомат переходов), как я понимаю, состоит в нахождении инкрементной функции dU: (dV -> dS), раскладываемой по изменениям значений: du[k]: (изменение одного параметра dv[k] -> смена состояний dS).
Сложность здесь в том, что
1) Операции изменения должны образовать коммутативную группу, т.е. правило ромба:
S0 -(dv1)-> S1 -(dv2)-> S12
S0 -(dv2)-> S2 -(dv1)-> S21 // исправил очепятку
S1 == S2
Отдаст ли программист этот долг — зависит от кривизны его рук. Есть риск ошибиться.
2) Одновременное изменение нескольких значений сразу (например, обновление данных по таймеру) нужно или самостоятельно обработать (предусмотрев все комбинации), причём непротиворечиво с элементарными переходами, или же разложить на последовательность элементарных переходов.
В последнем случае есть шанс для мелькания (например, кнопка видима, если включено чётное число чекбоксов).
Так что, ИМХО, проще написать функцию состояния U(V->S) и отрабатывать её в моменты переходов.
Здравствуйте, Sergey640, Вы писали:
S>Резонно и серьезно, настолько , что может похоронить идею на корню. Лучше начать с проработки условий и способов их простого задания.
Вот-вот. Именно это я и хочу сделать.
Это не принципиально. Я предпочитаю в случае сложных условий использовать if/else.
LD>>Мне бы хотелось бы несколько другого поведения. Например, зачастую кнопка OK должна быть запрещена, пока не будут введены все данные. Следовательно, можно сказать, что кнопку OK мы разрешаем, когда все edit-box диалога содержат какие-то данные. По идее, можно привязать изменения всех полей ввода к установке неких флагов, а потом сказать, что если все флаги равны TRUE, то кнопку OKразрешаем. Моя идея заключалась в том, чтобы выделить те ситуации, когда некие контролы изменяют свое состояние в ответ на изменение состояние неких других контролов диалога. Возможно, для каждой ситуации можно написать некий универсальный кусок кода, который можно будет использовать в дальнейшем.
К>По сути, мы имеем функцию U: (набор значений V -> набор состояний S контролов). Правильно?
К>Подход с UpdateCmdUI сводится к тому, что эта функция раскладывается на набор функций u[k] : (V -> состояние контрола s[k]). Эти функции реализуются в обработчиках сообщений WM_UPDATE_COMMAND_UI. К>Причём моменты вычисления (и, соответственно, обновления состояний) произвольны. Можно в любой момент сказать, какое сейчас должно быть состояние, вне зависимости от истории. И в любой момент инициировать обновление.
Да, логично.
К>А твоя идея (автомат переходов), как я понимаю, состоит в нахождении инкрементной функции dU: (dV -> dS), раскладываемой по изменениям значений: du[k]: (изменение одного параметра dv[k] -> смена состояний dS).
Класс!!!. Очень точная формулировка.
К>Сложность здесь в том, что К>1) Операции изменения должны образовать коммутативную группу, т.е. правило ромба: К>S0 -(dv1)-> S1 -(dv2)-> S12 К>S0 -(dv2)-> S2 -(dv1)-> S21 // исправил очепятку К>S1 == S2 К>Отдаст ли программист этот долг — зависит от кривизны его рук. Есть риск ошибиться.
Не очень понял, что здесь имеется ввиду. Ты имеешь ввиду, что независимо от порядка применения правил, результат должен быть один и тот же? К>2) Одновременное изменение нескольких значений сразу (например, обновление данных по таймеру) нужно или самостоятельно обработать (предусмотрев все комбинации), причём непротиворечиво с элементарными переходами, или же разложить на последовательность элементарных переходов. К>В последнем случае есть шанс для мелькания (например, кнопка видима, если включено чётное число чекбоксов).
Я что-то не помню, чтобы мне приходилось писать что-то подобное. Но конечно, в этом случае, надо будет что-то придумать.
Здравствуйте, Lonely Dog, Вы писали:
К>>Сложность здесь в том, что К>>1) Операции изменения должны образовать коммутативную группу, т.е. правило ромба: К>>S0 -(dv1)-> S1 -(dv2)-> S12 К>>S0 -(dv2)-> S2 -(dv1)-> S21 // исправил очепятку К>>S1 == S2 К>>Отдаст ли программист этот долг — зависит от кривизны его рук. Есть риск ошибиться.
LD>Не очень понял, что здесь имеется ввиду. Ты имеешь ввиду, что независимо от порядка применения правил, результат должен быть один и тот же?
Да. Результат должен быть один, даже не потому, что "группа операций бла-бла-бла", а потому, что это визивиг.
Пользователь может не помнить истории, для него состояние контролов должно непротиворечиво вытекать из того, что он прямо сейчас видит. Из чего уже и следует коммутативность...
К>>2) Одновременное изменение нескольких значений сразу (например, обновление данных по таймеру) нужно или самостоятельно обработать (предусмотрев все комбинации), причём непротиворечиво с элементарными переходами, или же разложить на последовательность элементарных переходов. К>>В последнем случае есть шанс для мелькания (например, кнопка видима, если включено чётное число чекбоксов).
LD>Я что-то не помню, чтобы мне приходилось писать что-то подобное. Но конечно, в этом случае, надо будет что-то придумать.
Родилась еще одна мысль по поводу представления карты поведения диалога. Можно вместо конечного автомата диалога указывать набор его инвариантов. Т.е., вместо постулирования "когда в текстовое поле будет что-то введено, кнопка OK должна стать Enabled.", мы говорим, что "Если в поле ничего не введено, то OK должна быть Disabled." В этом случае, можно ввести отношения "Если условие X, то контрол Y должен быть в состоянии Z" или "Контрол Y находится в состоянии Z тогда и только тогда когда условие X выполнено."
Далее останется написать только набор стандартных условий вида "Поле пусто", "В списке что-то выбрано", и набор функций, меняющих состояние объекта. Как вам такая формулировка?
Здравствуйте, Lonely Dog, Вы писали:
LD>Здравствуйте, Lonely Dog, Вы писали:
LD>Родилась еще одна мысль по поводу представления карты поведения диалога. Можно вместо конечного автомата диалога указывать набор его инвариантов. Т.е., вместо постулирования "когда в текстовое поле будет что-то введено, кнопка OK должна стать Enabled.", мы говорим, что "Если в поле ничего не введено, то OK должна быть Disabled." В этом случае, можно ввести отношения "Если условие X, то контрол Y должен быть в состоянии Z" или "Контрол Y находится в состоянии Z тогда и только тогда когда условие X выполнено." LD>Далее останется написать только набор стандартных условий вида "Поле пусто", "В списке что-то выбрано", и набор функций, меняющих состояние объекта. Как вам такая формулировка?
"Когда" — это переходы. "Если" — функции.
Ну а выбрать прямые или инверсные предикаты ("enabled если текст непуст" vs. "disabled если текст пуст") — дело хозяйское.
В одной GUI-библиотеке (Zinc) у контролов было свойство Disabled, по умолчанию false. Там удобнее было задавать условия от противного.
Здравствуйте, Lonely Dog, Вы писали:
LD>Привет!
LD>Родилась мысль о том, как можно упростить реализацию GUI, конкретнее, хочется LD>... LD>PS: Есть мысль развить эту идею до уровня библиотеки. Сам я пишу на WTL, т.ч. первая версия будет для нее. В дальнейшем, можно будет сделать и для остальных библиотек (и языков).
Здравствуйте, Lonely Dog, Вы писали:
LD>Родилась мысль о том, как можно упростить реализацию GUI, конкретнее, хочется заставить систему саму отсжеливать состояния контролов. (enabled/disabled, visible, и пр)
Похожую мысль и реализацию родил недавно...
Очень удобно разделять документ от вида.
пример использования:
class CMyDlg :
public CDialogImpl<CMyDlg>,
public CBindingImpl
{
public:
enum { IDD = IDD_MY };
DECLARE_BIND_MAP()
BEGIN_MSG_MAP(CMyDlg)
CHAIN_MSG_MAP(CBindingImpl)
END_MSG_MAP()
CIntProperty m_prop;
};
// Эта карта делает так, что IDC_TEXT1 доступен, когда чекнут IDC_RADIO1 и IDC_TEXT2 доступен, когда чекнут IDC_RADIO2
BEGIN_BIND_MAP(CMyDlg)
BIND_INT_RADIO(IDC_RADIO1, m_prop, 1)
BIND_INT_RADIO(IDC_RADIO2, m_prop, 2)
BIND_INT_ENABLED(IDC_TEXT1, m_prop, 1)
BIND_INT_ENABLED(IDC_TEXT2, m_prop, 2)
END_BIND_MAP()
// Ну и общая концепция:
/*********************************************************************************\
CProperty - базовый класс для всех пропертей
\*********************************************************************************/class CProperty
{
public:
CProperty();
CProperty(const CProperty& prop);
virtual ~CProperty();
CProperty& operator = (const CProperty& prop);
CEvent OnChange;
};
/*********************************************************************************\
CPropertyBind - базовый класс для всех биндов (связь между свойством и элементом управления)
\*********************************************************************************/class CPropertyBind
{
public:
CPropertyBind(HWND hWnd, CProperty& prop) :
m_hWnd(hWnd)
{
m_hander.init(prop.OnChange, &CPropertyBind::OnChangeProp, this);
}
void OnChangeProp(){ OnChangePropImpl(); }
virtual void OnCommand(UINT nCode) = 0;
protected:
virtual void OnChangePropImpl() = 0;
protected:
HWND m_hWnd;
CHandler m_hander;
};
/*********************************************************************************\
CIntProperty - свойство типа int
\*********************************************************************************/class CIntProperty :
public CProperty
{
public:
class CBind : public CPropertyBind
{
public:
CBind(HWND hWnd, CIntProperty& prop) :
CPropertyBind(hWnd, prop),
m_prop(prop),
m_fSelfUpdate(FALSE){}
virtual void OnCommand(UINT nCode);
protected:
virtual void OnChangePropImpl();
protected:
CIntProperty& m_prop;
BOOL m_fSelfUpdate;
};
class CBindRadio : public CPropertyBind
{
public:
CBindRadio(HWND hWnd, CIntProperty& prop, int nFlags) :
CPropertyBind(hWnd, prop),
m_prop(prop),
m_nFlags(nFlags),
m_fSelfUpdate(FALSE){}
virtual void OnCommand(UINT nCode);
protected:
virtual void OnChangePropImpl();
protected:
CIntProperty& m_prop;
int m_nFlags;
BOOL m_fSelfUpdate;
};
class CBindCheck : public CPropertyBind
{
public:
CBindCheck(HWND hWnd, CIntProperty& prop, int nFlags) :
CPropertyBind(hWnd, prop),
m_prop(prop),
m_nFlags(nFlags),
m_fSelfUpdate(FALSE){}
virtual void OnCommand(UINT nCode);
protected:
virtual void OnChangePropImpl();
protected:
CIntProperty& m_prop;
int m_nFlags;
BOOL m_fSelfUpdate;
};
class CBindEnabled : public CPropertyBind
{
public:
CBindEnabled(HWND hWnd, CIntProperty& prop, int nFlags, BOOL fAsFlag = FALSE) :
CPropertyBind(hWnd, prop),
m_prop(prop),
m_nFlags(nFlags),
m_fAsFlag(fAsFlag){}
virtual void OnCommand(UINT nCode);
protected:
virtual void OnChangePropImpl();
protected:
CIntProperty& m_prop;
int m_nFlags;
BOOL m_fAsFlag;
};
CIntProperty(int value = 0) : m_nValue(value){}
operator int() { return m_nValue; }
CIntProperty& operator = (int nValue){ SetValue(nValue); return *this; }
int GetValue()const{ return m_nValue; }
void SetValue(int value){ m_nValue = value; OnChange.raise(); }
private:
int m_nValue;
};
typedef std::multimap<UINT, CPropertyBind*> CPropertyBindMap;
typedef CPropertyBindMap::iterator CPropertyBindIter;
/*********************************************************************************\
CBindingImpl - "связыватель" свойств с контролами (от него наследовать диалог)
не забывать про CHAIN_MSG_MAP(CBindingImpl)
\*********************************************************************************/class CBindingImpl
{
public:
CBindingImpl();
virtual ~CBindingImpl();
void ClearBindings();
BEGIN_MSG_MAP(CBindingImpl)
MESSAGE_HANDLER(WM_COMMAND, OnCommand)
MESSAGE_HANDLER(WM_NOTIFY, OnNotify)
END_MSG_MAP()
private:
// Handler prototypes (uncomment arguments if needed):
// LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
// LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/);
// LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/);
LRESULT OnCommand(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
LRESULT OnNotify(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
void ProcessCommand(UINT nID, UINT nCode);
protected:
CPropertyBindMap m_mapBindProp;
};
#define DECLARE_BIND_MAP() void InitBinding();
#define BEGIN_BIND_MAP(theClass) \
void theClass::InitBinding() \
{
#define BIND_INT(id, prop) \
m_mapBindProp.insert(std::make_pair(id, new CIntProperty::CBind(GetDlgItem(id), prop))); \
prop.OnChange.raise();
#define BIND_INT_RADIO(id, prop, flags) \
m_mapBindProp.insert(std::make_pair(id, new CIntProperty::CBindRadio(GetDlgItem(id), prop, flags)));\
prop.OnChange.raise();
#define BIND_INT_CHECK(id, prop, flags) \
m_mapBindProp.insert(std::make_pair(id, new CIntProperty::CBindCheck(GetDlgItem(id), prop, flags))); \
prop.OnChange.raise();
#define BIND_INT_ENABLED(id, prop, flags) \
m_mapBindProp.insert(std::make_pair(id, new CIntProperty::CBindEnabled(GetDlgItem(id), prop, flags))); \
prop.OnChange.raise();
#define BIND_INT_ENABLED_FLAG(id, prop, flags) \
m_mapBindProp.insert(std::make_pair(id, new CIntProperty::CBindEnabled(GetDlgItem(id), prop, flags, TRUE))); \
prop.OnChange.raise();
Если интересно, могу более подробно все расписать...
Или же соорудить простенький проектик с использованием всего этого...