Вступление
Каждый из нас, конечно же, сталкивался с ситуацией, когда меню требуется сгенерировать динамически, непосредственно во время выполнения программы. Это может требоваться по разным причинам, например, из-за наличия нескольких локализаций ресурсов или из-за слишком большого разнообразия вариантов меню.
Обычный способ
Win32 API для этого предлагает функции CreateMenu(), AppendMenu() и подобные. Мало-мальски сложное меню с вложенными подменю выглядит в таком описании не очень наглядно, если не сказать нечитаемо. Даже ATL не помогает:
CMenu menu ;
menu.CreateMenu() ;
menu.AppendMenu(MF_STRING, ECmdOpen, L"Открыть") ;
menu.AppendMenu(MF_STRING, ECmdClose, L"Закрыть") ;
menu.AppendMenu(MF_SEPARATOR) ;
menu.AppendMenu(MF_STRING, ECmdTwist, L"Свинтить") ;
CMenu scratchMenu ;
scratchMenu.CreateMenu() ;
scratchMenu.AppendMenu(MF_STRING, ECmdScratchHead, L"Затылок") ;
scratchMenu.AppendMenu(MF_STRING, ECmdScratchNose, L"Нос") ;
menu.AppendMenu(MF_POPUP, scratchMenu, L"Почесать") ;
menu.AppendMenu(MF_STRING, ECmdLose, L"Потерять") ;
Большое количество повторяющегося кода утомляет взор и ввергает в уныние.
Новый способ
Хочется написать кратко и ясно:
popup(L"Меню")
[command(L"Открыть", ECmdOpen)]
[command(L"Закрыть", ECmdClose)]
[separator()]
[command(L"Свинтить", ECmdTwist)]
[popup(L"Почесать")
[command(L"Затылок", ECmdScratchHead)]
[command(L"Нос", ECmdScratchNose)]
]
[command(L"Потерять", ECmdLose)]
Так написать нам позволит наличие перегрузки операторов в языке C++. Мы перегрузим оператор «квадратные скобки», чтобы он делал некое действие с объектом и возвращал ссылку на сам объект:
struct node
{
node & operator[](...)
{
// ...
return *this ;
}
} ;
Это позволит нам сколько угодно раз вызывать у объекта этот оператор:
node n ;
n[123][456][789] ;
Меню можно представить в виде дерева, у которого есть узлы четырех типов:
enum node_type { EEmpty, ESeparator, ECommand, EPopup } ;
У узла
EPopup
может быть сколько угодно детей любого типа, у остальных узлов детей быть не может.Ясно, что если находиться им предстоит в одном дереве, то без общего предка не обойтись. Метод
append()
позволит добавлять детей к узлу, метод append_to()
нужен для составления реального меню при обходе нашего дерева.
struct node_impl_base ;
typedef std::auto_ptr<node_impl_base> node_impl_ptr ;
struct node_impl_base
{
virtual ~node_impl_base() {}
virtual node_type type() const = 0 ;
virtual void append(node & n) = 0 ;
virtual void append_to(HMENU aMenu) const = 0 ;
} ;
typedef std::auto_ptr<node_impl_base> node_impl_ptr ;
Также пригодится небольшая вспомогательная структурка, чтобы не повторяться:
template <node_type Type>
struct node_impl: public node_impl_base
{
static const node_type KType = Type ;
node_type type() const { return KType; }
void append(node & n) { _ASSERT(!"not allowed"); }
};
_ASSERT
нужен для того, чтобы случайно не вызвать метод append()
для узла, который не может иметь детей.Теперь реализуем наши узлы:
struct empty_node: node_impl<EEmpty>, boost::noncopyable
{
void append_to(HMENU aMenu) const { CMenuHandle(aMenu).AppendMenu(MF_STRING); }
};
struct separator_node: node_impl<ESeparator>, boost::noncopyable
{
void append_to(HMENU aMenu) const { CMenuHandle(aMenu).AppendMenu(MF_SEPARATOR); }
};
struct command_node: node_impl<ECommand>, boost::noncopyable
{
command_node(PCTSTR text, int id): text_(text), id_(id) {}
void append_to(HMENU aMenu) const { CMenuHandle(aMenu).AppendMenu(MF_STRING, id_, text_); }
private:
CString text_ ;
int id_ ;
} ;
struct popup_node: node_impl<EPopup>, boost::noncopyable
{
popup_node(PCTSTR text): text_(text) {}
void append(node & n) { children_.push_back(new node(n)); }
void append_to(HMENU aMenu) const
{
CMenuHandle menu ;
menu.CreatePopupMenu() ;
BOOST_FOREACH(const node & n, children_)
{
n.append_to(menu) ;
}
CMenuHandle(aMenu).AppendMenu(MF_STRING, menu, text_) ;
}
private:
boost::ptr_vector<node> children_ ;
CString text_ ;
} ;
Само дерево будет состоять из объектов одного и того же типа
node
. Используется идиома «pimpl», то есть node содержит указатель на конкретную реализацию узла. Обратите внимание на семантику копирования и присваивания:
struct node
{
friend node empty() ;
friend node separator() ;
friend node command(PCTSTR text, int id) ;
friend node popup(PCTSTR text) ;
node(): impl_(new empty_node()) {}
node(node & other): impl_(other.impl_) {} // move
node & operator=(node & other) { impl_ = other.impl_; return *this; } // move
node & operator[](node & n) { impl_->append(n); return *this; }
node_type type() const { return impl_->type(); }
void append_to(HMENU aMenu) const { impl_->append_to(aMenu); }
private:
node(node_impl_ptr impl): impl_(impl) {} // take ownership
node_impl_ptr impl_ ;
} ;
При присваивании одного
node
другому происходит передача реализации (так называемая move-семантика). Объект из правой части становится пустышкой. Так же работает и конструктор копирования. (Аналогично работает std::auto_ptr
).Так как все затевалось ради того, чтобы в виде временного объекта описать многоуровневую структуру, move-семантика экономит здесь много операций копирования.
Кстати, поскольку
node_impl_ptr
— это и есть std::auto_ptr
, можно было не определять явно node(node & other)
и operator=(node & other)
, компилятор сгенерировал бы их сам.Теперь нам остается только определить функции для создания узлов. За исключением
empty()
, они используют приватный конструктор и потому объявлены как friend
.
node empty()
{
return node() ;
}
node separator()
{
return node(node_impl_ptr(new separator_node())) ;
}
node command(PCTSTR text, int id)
{
return node(node_impl_ptr(new command_node(text, id))) ;
}
node popup(PCTSTR text)
{
return node(node_impl_ptr(new popup_node(text))) ;
}
Готово! В рабочем коде это используется примерно так:
struct menubar
{
menubar(node key1, node key2) ;
// ...
};
SetMenuBar(
menubar(
command(L"Ok", IDOK),
popup(L"Меню")
[command(L"Открыть", ECmdOpen)]
[command(L"Закрыть", ECmdClose)]
[separator()]
[command(L"Свинтить", ECmdTwist)]
[popup(L"Почесать")
[command(L"Затылок", ECmdScratchHead)]
[command(L"Нос", ECmdScratchNose)]
]
[command(L"Потерять", ECmdLose)]
)
) ;
Благодаря move-семантике node здесь не происходит копирования всей структуры, вместо этого она напрямую передается в
SetMenuBar()
.menubar
состоит из двух деревьев, потому что у приложения для Windows Mobile есть две soft key. Реализация SetMenuBar()
выходит за рамки данной статьи, и так уже получилось много текста :)Заключение
Основной целью было показать, как можно наглядно описывать многоуровневые неоднородные структуры, используя лишь синтаксис языка C++. С небольшими расширениями данный метод можно применить и к десктопным приложениям с более богатой функциональностью меню.