Автор Eric Shepherd

В предыдущей статье, "Программирование в BeOS, Урок 1", мы рассмотрели как создается BeOS приложение с одним единственным окном с одним представлением (view). Теперь мы расширим этот пример, добавив к нашему окну меню, и покажем как обрабатывать действия пользователя меню.

Но перед тем как начать, мы рассмотрим проблему, с которой чаще всего сталкиваются начинающие программисты - систему координат в BeOS. Допустим, у вас есть прямоугольная область:

	rect.Set(0, 0, 5, 7);

Верхний левый угол этой области имеет координаты (0, 0), а ее нижний правый угол - (5,7). Это выглядит приблизительно так:

	012345
	******0
	******1
	******2
	******3
	******4
	******5
	******6
	******7

Т.к. указанные координаты являются "включающими", следовательно, верхняя строка прямоугольника содержит точки (0,0), (1,0), (2,0), (3,0), (4,0), and (5,0). Т.е. ширина прямоугольника - 6 пикселей, а высота - 8.

Результатом вызова функции Width() на этом прямоугольнике будет 5 (5-0). Height() возвратит 7 (7-0). Это вносит некоторую путаницу. Поэтому, если вы хотите узнать настоящую ширину или высоту прямоугольника, вы должны добавить единицу к результату выполнение этих двух функций.

Теперь вернемся к нашему примеру: Menu World. Menu World базируется на примере Hello World из предыдущей статьи; если вы не читали эту статью, мы рекомендуем ее прочитать. Также советуем вам скачать исходный текст примера.

Также, перед тем, как начать, мы хотим сказать, что в этом примере используются сообщения BMessage (с помощью которых ваше приложение "узнает" обо всех действиях пользователя) без объяснения деталей того как они работают. Это будет обсуждаться в следующей статье.

Заголовочные файлы, используемые Menu World:

	#include <Application.h> 
	#include <Window.h> 
	#include <View.h> 
	#include <MenuBar.h>
	#include <Menu.h> 
	#include <MenuItem.h>
	#include <string.h>

Давайте начнем с функции main(), которая невероятно проста. Единственная причина, по которой мы включили сюда ее описание, заключается в том, что она слегка отличается от той, что мы привели в предыдущей статье -- теперь она размещает объект HelloApp в стеке, что является одним из нововведений языка C++ (мы любим прогресс). Заметьте также, что так как объект размещается в стеке, мы не должны помнить об удалении объекта, потому что это происходит автоматически, когда отрабатывает функция main().

	void main(void) {
		HelloApp theApp;    // Объект-приложение
		theApp.Run();
	}

Мы также используем следующие константы для команд BMessage для каждого пункта меню в нашем приложении Menu World:

	 const uint32 MENU_FILE_NEW = 'MFnw';
	 const uint32 MENU_FILE_OPEN = 'MFop';
	 const uint32 MENU_FILE_CLOSE = 'MFcl';
	 const uint32 MENU_FILE_SAVE = 'MFsv';
	 const uint32 MENU_FILE_SAVEAS = 'MFsa';
	 const uint32 MENU_FILE_PAGESETUP = 'MFps';
	 const uint32 MENU_FILE_PRINT = 'MFpr';
	 const uint32 MENU_FILE_QUIT = 'MFqu';
	 const uint32 MENU_OPT_HELLO = 'MOhl';

Теперь давайте посмотрим на класс HelloView, у которого теперь на один метод больше, а также добавлены кое-какие скрытые (private) данные:

	class HelloView : public BView {
	public:
		HelloView(BRect frame);
		virtual void Draw(BRect updateRect);
		void SetString(const char *s);

	private:
		char message[128];
	};

Функция SetString() была добавлена для того, чтобы вы могли сконфигурировать, какой текст должен отображать класс HelloView. Текст хранится в строке message, вот так:

	void HelloView::SetString(const char *s) {
		if (strlen(s) < 127) {
			strcpy(message, s);
		}
	}

SetString() просто убеждается, что передаваемая строка не слишком длинная, и затем копирует ее в поле message.

Функция Draw() была изменена для того, чтобы во время отрисовки использовать строку message:

	void HelloView::Draw(BRect updateRect) {
		MovePenTo(BPoint(20,75));      // Передвигаем перо
		DrawString(message);
	}

А также конструктор HelloView теперь инициализирует строку message:

	HelloView::HelloView(BRect frame)
			: BView(frame, "HelloView", B_FOLLOW_ALL_SIDES,
			B_WILL_DRAW) {
		SetString(STRING_HELLO);
	}

Все эти вещи становятся более интересными, когда мы помотрим как был изменен класс HelloWindow:

	class HelloWindow : public BWindow {
	public:
		HelloWindow(BRect frame);
		virtual bool QuitRequested();
		virtual void MessageReceived(BMessage *message);

	private:
		BMenuBar  *menubar;
		HelloView *helloview;
	};

В классе HelloWindow немного поменялась функция MessageReceived(). Отметим также, что теперь у нас есть поля для хранения указателей - объектов меню (BMenuBar) и HelloView привязанного к окну.

Давайте рассмотрим методы нашего класса более детально. В конструктор класса было добавлено создание и установка полосы меню:

	HelloWindow::HelloWindow(BRect frame)
			: BWindow(frame, "MenuWorld", B_TITLED_WINDOW,
				B_NOT_RESIZABLE | B_NOT_ZOOMABLE) {
		BRect r;
		BMenu *menu;
		BMenuItem *item;

		// Добавим представление

		r = Bounds();
		r.top = 20;
		AddChild(helloview = new HelloView(r));

		// Добавим меню

		r.top = 0;
		r.bottom = 19;
		menubar = new BMenuBar(r, "menu_bar");
		AddChild(menubar);
	}

Мы начали с получения границ окна, так же как мы делали это в Hello World. Когда мы создавали наш HelloView, мы оставили сверху 20 пикселей для полосы меню (наша полоса меню будет иметь высоту 20 пикселей и будет помещена над HelloView). Затем мы создали и поместили HelloView в окно. Заметьте, что мы сохранили указатель на представление в поле helloview нашего класса.

Затем мы создали и добавили полосу меню в наше окно. Мы установили r.top в 0 и r.bottom в 19, чтобы показать, что полоса меню должна занимять 20 пикселей окна, и создали BMenuBar названный "menu_bar". Указатель на меню мы также сохранили к поле нашего класса. После этого меню было добавлено в окно с помощью функции AddChild().

Заметьте, что теперь наше окно имеет два представления внутри: HelloView и BMenuBar (класс порожденный от BMenu, который в свою очередь порожден от BView).

Меню "File" создается вот так:

	menu = new BMenu("File");

Эта строчка просто создает пустое меню "File". Теперь пора добавить пункты в меню, например, вот так:

	menu->AddItem(new BMenuItem("New",
				   new BMessage(MENU_FILE_NEW), 'N'));

Эта строка добавляет пункт "New" в наше меню (с клавиатуры этот пункт будет вызываться по нажатию "Command+N"), создавая объект BMenuItem и добавляя его в наше меню с помощью функции AddItem().

Когда пользователь выбирает пункт меню, сообщение BMessage посылается окну, в котором содержится пункт меню (адресат сообщения может быть изменен, но не будем опережать события). Сообщение, которое посылается окну, создается путем копирования модельного сообщения, заданного при создании BMenuItem.

В нашем случае, мы создали модельное сообщение с кодом команды MENU_FILE_NEW. Мы рассмотрим систему сообщений более детально в следующей статье.

Продолжим добавив остальные пункты в меню. Заметьте, что аргумент конструктора BMenuItem, задающий "горячую клавишу", является опциональным; если он не задан, пункт меню не будет иметь "горячей клавиши". Также, мы используем функцию AddSeparatorItem() для добавления разделителей, чтобы сделать меню более легкочитаемым:

	menu->AddItem(new BMenuItem("Open" B_UTF8_ELLIPSIS,
				   new BMessage(MENU_FILE_OPEN), 'O'));
	menu->AddItem(new BMenuItem("Close",
				   new BMessage(MENU_FILE_CLOSE), 'W'));
	menu->AddSeparatorItem();
	menu->AddItem(new BMenuItem("Save",
				   new BMessage(MENU_FILE_SAVE), 'S'));
	menu->AddItem(new BMenuItem("Save as" B_UTF8_ELLIPSIS,
				   new BMessage(MENU_FILE_SAVEAS)));
	menu->AddSeparatorItem();
	menu->AddItem(new BMenuItem("Page Setup" B_UTF8_ELLIPSIS,
				   new BMessage(MENU_FILE_PAGESETUP)));
	menu->AddItem(new BMenuItem("Print" B_UTF8_ELLIPSIS,
				   new BMessage(MENU_FILE_PRINT), 'P'));
	menu->AddSeparatorItem();
	menu->AddItem(new BMenuItem("Quit",
				   new BMessage(MENU_FILE_QUIT), 'Q'));

Когда все пункты меню добавлены, мы добавляем меню (menu) к полосе меню (menu bar), используя метод AddItem() класса BMenuBar:

	menubar->AddItem(menu);

После этого мы создаем меню "Options", у которого есть только один пункт.

	menu = new BMenu("Options");
	item = new BMenuItem("Say Hello",
			new BMessage(MENU_OPT_HELLO));

Пункт меню "Say Hello" служит для переключения между двумя строками, которые будут отображаться в HelloView в нашем окне: "Hello World" и "Goodbye World". У этого пункта меню есть отметка, если пункт отмечен - отображается текст "Hello World", в противном случае отображается "Goodbye World". Давайте сделаем текст "Hello World" текстом по умолчанию и убедимся, что пункт "Say Hello" первоначально отмечен:

	item->SetMarked(true);

Передавая true в SetMarked() мы устанавливаем отметку рядом с этим пунктом. Если мы передим false, отметка будет снята.

Теперь мы добавим пункт "Say Hello" в меню и добавим полученное меню "Options" в полосу меню нашего окна.

	menu->AddItem(item);
	menubar->AddItem(menu);

Наконец, мы сделаем наше окно видимым вызвав функцию Show().

	Show();
Функция QuitRequested() остается такой же как в Hello World.

MessageReceived() это функция, в которой происходит вся тяжелая работа по обработке сообщений, получаемых когда пользователь выбирает какой-то пункт меню. Когда пользователь выберет один из пунктов, создается соответствующее сообщение BMessage и посылается окну, содержащему меню. Функция окна MessageReceived() получает это сообщение и обрабатывает его:

	void HelloWindow::MessageReceived(BMessage *message) {
		switch(message->what) {
			case MENU_OPT_HELLO:
				/* see below for the code that goes here */
				break;

			default:
				BWindow::MessageReceived(message);
				break;
		}
	}

Одно из доступных полей объекта BMessage называется "what"; оно содержит код команды, заданный при создании BMessage. Таким образом, мы можем определить что за сообщение мы получили, изучив поле "what" этого сообщения.

Сейчас единственное сообщение, которое мы обрабатываем, это MENU_OPT_HELLO, оно посылается, когда пользователь выберет пункт меню "Say Hello" в меню Options (если вы помотрите на конструктор HelloWindow, то увидите, что модельное сообщение BMessage для пункта "Say Hello" имеет именно этот код команды). Все остальные сообщения передаются BWindow::MessageReceived() для дальнейшей обработки.

Пункт меню "Say Hello" переключает текст, отображаемый в представлении, между "Hello World" и "Goodbye World". Он также добавлает отметку, если в представлении отображается "Hello World", и удаляет ее в случае "Goodbye World". Давайте помотрим на код.

	case MENU_OPT_HELLO:
	{
		BMenuItem *item;
		const char *s;
		bool mark;

		message->FindPointer("source", (void **) &item);
		if (item->IsMarked()) {
			s = STRING_GOODBYE;
			mark = false;
		}
		else {
			s = STRING_HELLO;
			mark = true;
		}
		helloview->SetString(s);
		item->SetMarked(mark);
		helloview->Invalidate();
	}
	break;

BMessage, полученный функцией MessageReceived(), когда пользователь выбрал какой-то пункт меню, является модельным сообщением, указанным при добавлении пункта меню, с тремя дополнительными полями:

Поле "when" типа B_INT64_TYPE содержит значение времени, когда пункт меню был выбран, являющееся количеством миллисекунд, прошедших с 0:00:00 1/1/1970.

Поле "source" типа B_POINTER_TYPE содержит указатель на объект BMenuItem.

Поле "index" типа B_INT32_TYPE содержит значение, являющееся порядковым номером пункта меню в меню, причем отсчет ведется с нуля.

Для того, чтобы установить отметку рядом с пунктом меню, нам необходим указатель на объект BMenuItem этого пункта. Мы, конечно, может сохранить его в нашем объекте окна, но в целях иллюстрации более интересно получить его из поля "source" BMessage, так мы и сделаем. Мы используем функцию FindPointer() для получения указателя и сохраним его в переменной "item".

Затем мы воспользуемся методом item->IsMarked(), чтобы определить отмечен ли пункт меню. Если он отмечен, IsMarked() вернет true (что означает, что текст в нашем представлении - "Hello World"). Мы установим значение строки в "Goodbye World", и установим переменную mark в false. Если IsMarked() вернет false (что означает, что сейчас отображается строка "Goodbye World"), мы установим значение строки в "Hello World" и mark в true.

Затем вызовом функции HelloView SetString() мы установим новый отображаемый текст, и вызовем item->SetMark() чтобы установить или снять отметку; если mark равна false, то отметка будет снята, в противном случае она будет установлена.

Затем мы должны отрисовать HelloView. Отрисовка осуществляется сервером приложений, который использует метод Draw() для отрисовки представлений. Это необходимо для обновления содержимого представления.

У класса BMenuItem есть еще несколько методов, с которыми стоит поэкспериментировать, например: SetEnabled() и IsEnabled(), которые позволят вым управлять состоянием пунктов меню (включен/выключен), или SetLabel() и Label(), которые позволят вам устанавливать и получать текущий текст пункта меню.

Советуем вам скачать исходный текст примера к этой статье.