Как стать автором
Обновить

Описание структуры меню без использования ресурсов

Время на прочтение5 мин
Количество просмотров3.4K

Вступление


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

Обычный способ


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++. С небольшими расширениями данный метод можно применить и к десктопным приложениям с более богатой функциональностью меню.
Теги:
Хабы:
+24
Комментарии28

Публикации

Изменить настройки темы

Истории

Работа

Программист C++
128 вакансий
QT разработчик
7 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн