Глава 13.


Создание многодокументных приложений

                                                              

Ну вот мы и подошли к следующей важной теме. Я надеюсь, что нет необходимости подробно представлять многодокументный интерфейс (MDI, Multiple Document Interface), который позволяет в одном приложении работать с несколькими окнами. MDI описан в руководстве по разработке интерфейса пользователя System Application Architecture /Common User Access/ Advanced Interface Design Guide (SAA/CUA), созданном фирмой IBM. В системе Windows интерфейс MDI используется, начиная с версии 3.0, и его типичным представителем является хорошо всем знакомый текстовый процессор Microsoft Word.

Как и все приложения Windows, MDI-приложение имеет одно главное окно, которое управляет своими окнами "MDI child".

 Примечание 

Для названия окна документа я буду придерживаться терминологии фирмы Microsoft, которая для дочерних окон MDI-приложений использует названия "MDI child frame", "MDI child window" или просто "MDI child".

Поведение окон "MDI child" напоминает поведение как дочерних, так и перекрывающихся окон. Так же, как и первые, их невозможно переместить за пределы главного окна приложения. В то же время окна "MDI child" можно сделать активными, при этом их заголовок выделяется цветом. Кроме того, каждое окно "MDI child" имеет, как правило, системное меню и кнопки изменения размера. Если свернуть такое окно, то его пиктограмма расположится в нижней части главного окна приложения. При увеличении размеров окна до максимальных пиктограмма системного меню окна "MDI child" помещается в левую часть основного меню, а в правой части располагаются кнопки изменения размера и закрытия окна.

Одним словом, можно создать аналогичное приложение с использованием обычных окон, но для этого придется затратить немало усилий. Достаточно серьезной работой будет и создание полноценного MDI-приложения с помощью функций библиотеки SDK (Software Development Kit). Чтобы в этом убедиться, достаточно посмотреть исходные тексты приложения MULTIPAD, поставляемые в составе примеров вместе с системой разработки Microsoft Developer Studio Visual C++. Совсем другая картина предстанет перед вами, когда вы возьмете исходные тексты такого же приложения, но созданного с использованием библиотеки MFC. Мы попытаемся обойти эти две крайние позиции и рассмотрим интерфейс MDI одновременно с деталями его внутренних механизмов, знание которых позволит вам создавать приложения любой сложности наиболее эффективно и с наименьшими затратами на поиск ошибок, которые зачастую возникают из-за непонимания происходящего.

Начнем с рассмотрения двух основных классов библиотеки MFC, разработанных специально для создания многодокументных приложений, — CMD1-FrameWnd к CMDlChildWnd

.

Класс CMDIFrameWnd

CMDIFrameWnd — это базовый класс для главного окна любого многодокументного приложения (рис. 13.1). Если быть более точными, то в данном случае следует говорить не об окне< а о фрейме, поскольку помимо него создается еще и специальное окно, называемое окном MDICLIENT, которое управляет рабочей областью фрейма и является, собственно, родительским для окон "MDI child".

Рис. 13.1. Место класса CMDIFrameWnd в иерархии библиотеки MFC

Рис. 13.2. Типы окон интерфейса MDI

Уточним некоторые моменты, связанные с процессом создания MDI-окна. Как обычно, сначала создается объект нашего оконного класса. Если вы сравните тексты функции Initlnstance для создания главных окон SDI- и MDI-приложений, то не увидите никакой разницы. И в том и в другом случае вызываются одни и те же функции, в частности OnCreateClient. Однако и это скрыто в функциях библиотеки, если при создании SDI-окна вызывается функция класса CFrameWnd, то при создании MDI-окна вызывается одноименная функция класса CMDIFrameWnd. В первом случае она отвечает за создание (если необходимо) присоединяемого к окну дочернего окна, а во втором — за создание специального окна MDICLIENT, которое, как мы уже говорили, полностью управляет рабочей "областью главного окна MDI-приложения и отвечает за работу с окнами "MDI child". Все перечисленные окна показаны на рис. 13.2.

 Примечание 

Разница, безусловно, есть, но явно она заметна только в случае построения приложения на основе архитектуры "документ/представление". В других же случаях она скрыта в недрах библиотеки.

Прежде чем переходить к примерам, давайте рассмотрим функциональные возможности, которые класс CMDIFrameWnd предоставляет в наше распоряжение.

void CMDIFrameWnd::MDIActivate (CWnd *pWndActivate)

Параметр pWndActivate - указатель на окно "MDI child", которое должно быть активизировано.

Эта функция посылает сообщение WM_MDIACTIVATE в два окна: сначала в то, которое активизируется, затем в то, которое теряет активность. Такое же сообщение посылается, если пользователь изменил фокус дочернего окна при помощи мыши или клавиатуры. Когда активизируется основное окно, то дочернее, которое было активно последним, посылает сообщение WM_NCACTIVATE, чтобы нарисовать его рамку и заголовок, но при этом не получает сообщения WM_MDIACTIVATE.

CMDIChildWnd* CMDIFrameWnd::MDIGetActive ( BOOL *pbMaximized =.NULL)

Возвращает указатель на текущее активное окно "MDI child". Дополнительно в параметре pbMaximized возвращается информация о том, развернуто это окно или нет.

void CMDIFrameWnd::MDIIconArrange ()

Упорядочивает все свернутые окна "MDI child", не затрагивая те, которые не свернуты.

void CMDIFrameWnd::MDMaximize (CWnd *pWnd)

Разворачивает окно "MDI child". При этом Windows изменяет его размеры таким образом, чтобы оно заполнило всю рабочую область окна фрейма. Windows также размещает системное меню дочернего окна в полосе основного меню, а его заголовок добавляется к заголовку окна фрейма.

 Примечание 

Если активное окно развернуто, и в этот момент активизируется другое окно "MDI-child", то Windows его также разворачивает.

void CMDIFrameWnd::MDINext ()

Активизирует следующее окно "MDI child".

void CMDIFrameWnd::MDIRestore (CWnd *pWnd)

Восстанавливает исходные размеры окна "MDI child", заданного параметром pWnd, если оно было развернуто или свернуто.

void CMDIFrameWnd::MDITile ()

и

void CMDIFrameWnd::MDITile(int nType)

Располагают все окна "MDI child" без перекрытия. Первая версия функции располагает окна по вертикали, а вторая в качестве параметра nТуре может принимать одно из следующих значений:

MDITILE_HORIZONTAL 

Расположить окна по горизонтали

 MDITILE_VERTICAL

 Расположить окна по вертикали

MDITILE_SKIPDISABLED 

Предотвращает отображение блокированных окон "MDI child"

void CMDIFrameWnd:rMDICascade ()

и

void CMDIFrameWnd::MDICascade(int nType)

Располагают все окна "MDI child" каскадом. Первая версия функции отображает и блокированные окна, а вторая, которая может быть вызвана только с параметром MDITILE_SKIPDISABLED, — все, кроме блокированных.

virtual HMENU CMDIFrameWnd::GetWindowMenuPopup (HMENU hMenuBar)

Позволяет получить дескриптор раскрывающегося меню для элемента меню Window (Окно) основного меню, заданного параметром hMenuBar. Реализация, заданная по умолчанию, ищет раскрывающееся меню, которое содержит стандартные команды элемента меню Window (Окно) основного меню типа ID_WINDOW_NEW и ID_WINDOW_TILE_HORZ. Если раскрывающееся меню не содержит идентификаторов стандартных команд, то эту функцию следует переопределить.

CMenu* CMDIFrameWnd::MDISetMenu (

CMenu *pFrameMenu,

CMenu *pWindowMenu)

Заменяет соответствующие текущие меню на переданные в качестве параметров, если pFrameMenu и/или pWindowMenu не равны NULL. Возвращает временный указатель на замененное меню. После вызова этой функции следует вызвать функцию CWnd::DrawMenuBar для обновления полосы меню. Если функция изменяет раскрывающееся меню, то элементы меню окна "MDI child" удаляются из предыдущего меню окна и добавляются в раскрывающееся меню. Если для управления окнами "MDI child" вы используете фрейм, то эту функцию вызывать не следует.

Как видите, всю основную работу по поддержанию работы MDI-прило-жений выполняет система Windows совместно с библиотекой MFC.

Прежде чем двигаться дальше, давайте также внимательно рассмотрим класс, который отвечает за функционирование окон "MDI child".

 

Класс CMDIChildWnd

Как вы уже поняли, основным назначением этого класса является обеспечение работоспособности дочерних окон MDI-приложений — окон "MDI child", которые, говоря несколько упрощенно, позаимствовали свои свойства как у дочерних, так и у перекрывающихся окон (рис. 13.3).

Рис. 13.3. Место класса CMDIChildWnd в иерархии библиотеки MFC

Большую часть возможностей этот класс наследует от своего базового — CFrameWnd, но имеет и свойства, присущие только ему. К ним можно отнести автоматическое отображение меню окна "MDI child" вместо основного меню MDI-приложения, а также добавление заголовка дочернего окна к заголовку его родителя.

Для того чтобы произошла автоматическая замена основного меню на меню окна "MDI child", в классе определена единственная переменная

HMENU CMDIChildWnd::m_hMenuShared

Содержит дескриптор меню, ассоциированного с окном "MDI child".

Кроме того, в классе определены несколько функций:

BOOL CMDIChildWnd::Create (

LPCTSTR IpszClassName,

LPCTSTR IpszWindowName,

DWORD dwStyle = WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW,

const RECT Srect = rectDefault,

CMDIFrameWnd *pParentWnd = NULL,

CCreateContext *pContext = NULL)

Эта функция абсолютно идентична функции CFrameWnd::Create, однако задачи, которые они решают, достаточно сильно различаются, и нельзя подменять вызов одной из них вызовом другой. Активное текущее окно "MDI child" может определять заголовок своего родительского окна, если для него установить бит стиля FWS_ADDTOTITLE. Описываемая функция вызывается MDI-окном в ответ на команду пользователя создать дочернее окно. При этом параметр pContexi позволяет правильно связать дочернее окно с приложением. Если такой необходимости нет, то для него можно использовать значение по умолчанию (NULL).

void CMDIChildWnd::MDIDestroy ()

Удаляет заголовок окна "MDI child" из MDI-окна и разрушает само окно "MDI child".

void CMDIChildWnd::MDIActivate ()

Активизирует текущее окно "MDI child". Когда MDI-окно становится активным, то активизируется и окно "MDI child", которое находилось в этом состоянии последним.

void CMDIChildWnd::MDIMaximize ()

Разворачивает текущее окно "MDI child". Если у него не сброшен бит стиля FWS_ADDTOTITLE, то его заголовок добавляется в MDI-окно.

void CMDIChildWnd::MDIRestore ()

Восстанавливает исходные размеры текущего окна "MDI child", если оно было развернуто или свернуто.

CMDIFrameWnd* CMDIChildWnd::GetMDIFrame ()

Позволяет получить указатель на родительское MDI-окно объекта класса CMDIFrameWnd. Для того чтобы получить указатель на родительское окно типа MDICLIENT, управляющее объектом класса CMDIChildWnd, необходимо вызвать функцию CWnd::GetParent.

Теперь, когда мы познакомились с обоими классами CMDIFrameWnd и CMDIChildWnd, позволяющими создавать приложения на базе интерфейса MDI, пришло время рассмотреть конкретный пример, который поможет закрепить полученные сведения и познакомиться с дополнительными возможностями библиотеки MFC.

 

Пример MDI-приложения

В этом разделе мы рассмотрим пример (рис. 13.4), на котором я продемонстрирую основные принципы построения MDI-приложений. В дальнейшем он может послужить основой для разработки многодокументных приложений любой степени сложности.

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

Рис. 13.4. Главное и дочерние окна MDI-приложения

BOOL CMDIApp::Initlnstance() 

{

// Заголовок будем брать из ресурсов

CString csCaption;

csCaption.LoadString(IDR_MAINFRAME);

// Ищем копию нашего приложения

CWnd *pWnd = CWnd::FindWindow(NULL, csCaption);

// Если таковая есть, то ...

if(pWnd != NULL) {

// раскрываем его, если оно было свернуто 

if(pWnd->Is!conic())

pWnd->ShowWindow(SW_RESTORE);

 // или выводим на передний план 

pWnd->SetForegroundWindow(); 

return FALSE; 

}

// Создаем объект главного MDI-фрейма 

CMainFrame* pMainFrame = new CMainFrame;

 // и сам фрейм

if (!pMainFrame->LoadFrame(IDR_MAINFRAME))

 return FALSE; 

m_pMainWnd = pMainFrame; 

pMainFrame->ShowWindow(m_nCmdShow);

 pMainFrame->UpdareWindow(); 

return TRUE;

}

Первое, что сразу бросается в глаза при рассмотрении приведенного фрагмента — это практически полная идентичность интерфейса и реализации нашего класса "приложение" (CMDIApp) и класса CFirstApp, описанного выше (см. главу 3). А если не считать появления специального обработчика, который мы рассмотрим дальше в этой главе, и вызова функции CWnd::FindWindow, то совпадение будет полным. На самом деле это совсем не случайно. Достаточно большая часть программ для Windows создается с использованием метода "клея и ножниц", т. е. взяв за основу некоторый шаблон (а такие заготовки имеются у всех, даже начинающих, программистов), вы выполняете несколько маленьких изменений и компонуете составные части так, как этого требует конкретная задача.

 

Поиск запущенного экземпляра приложения

Рассмотрим фрагмент кода:

CString csCaption;

csCaption.LoadString(IDR_MAINFRAME);

CWnd *pWnd = CWnd::FindWindow(NULL, csCaption);

if(pWnd != NULL)

{

if(pWnd->IsIconic())

pWnd->ShowWindow(SW_RESTORE);

 pWnd->SetForegroundWindow();

 return FALSE; 

}

He будем пока акцентировать внимание на новом для нас классе CString, а ограничимся замечанием, что в объекте этого класса будет храниться символьная строка с именем окна, загруженная из файла ресурсов.

Как создать такой ресурс, вы к этому моменту должны уже хорошо представлять, поэтому ограничусь только его "внешним видом" (рис. 13.5).

Но вернемся к фрагменту кода. Мы вставили его для того, чтобы не загружать наше приложение еще раз, если оно уже было запущено. Действительно, MDI-приложения позволяют работать с несколькими документами одновременно (в отличие от SDI-приложений), и нет никакой необходимости расходовать дополнительную память. В отличие от ранних версий Windows, для проверки того, не было ли приложение уже загружено, мы не можем воспользоваться параметром hInstance, передаваемом в командной строке, поскольку для приложений Win32 он всегда равен NULL. Поэтому мы воспользовались функцией CWnd:: Find Window, которая позволяет найти окно верхнего уровня по имени оконного класса или по заголовку окна.

 Примечание 

Функцию FlndWindow нельзя использовать для поиска дочерних окон.

Рис. 13.5. Для поиска экземпляра приложения нам понадобится строковый ресурс

static CWnd* CWnd::FindWindow (

LPCTSTR IpszClassName,

LPCTSTR IpszWindowName)

Через параметр IpszClassName передается указатель на символьную строку, определяющую имя класса искомого окна. Если этот параметр равен NULL, как в нашем случае, то поиск будет осуществляться во всех классах окон. Поскольку нам надо найти вполне определенное окно с заданным заголовком, то имя заголовка мы передаем через второй параметр IpszWindowName, который в общем случае также может быть равен NULL. Если окно найдено, то функция FindWindow вернет временный указатель на этот объект класса CWnd, в противном случае — NULL.

 Примечание 

Если назначение имени класса мы отдаем библиотеке MFC, то параметром IpszClassName пользоваться просто невозможно, т. к. имя класса генерируется каждый раз новое и имеет примерно такой вид: "Агх:400000:8:1526:0:13F7" (при следующем запуске, даже второй копии приложения, класс будет иметь имя "Afx:400000:8:1526:0:251 F" — почувствуйте разницу!).

После того как главное окно найдено, нужно проверить, не было ли оно свернуто. Для этого используется специальная функция

BOOL CWnd::Islconic ()

Возвращает TRUE, если окно свернуто.

В этом случае восстанавливаем его нормальный размер с помощью уже известной вам функции CWnd::ShowWindow.

Теперь осталось только поместить окно на передний план и активизировать. Это легко сделать при помощи функции, которая предназначена именно для подобных целей:

BOOL CWnd::SetForegroundWindow ()

Переводит поток, который создал окно, в приоритетный режим и активизирует его главное окно.

Ее дополняет функция

static CWnd* CWnd::GetForegroundWindow ()

Возвращает указатель на временный объект приоритетного окна (окна, с которым пользователь в текущий момент работает).

Для полноты картины приведем еще одну функцию (не использованную в нашем приложении), которая позволяет изменить Z-порядок окон:

void CWnd::BringWindowToTop ()

Позволяет вывести на передний план окна верхнего уровня, всплывающие или окна "MDI child". По своему действию похожа на функцию CWnd::SetWindowpPos, однако при этом не разрешает изменять стили окна.

Возвращаясь к нашему примеру, отметим, что если работающая копия приложения обнаружена, то нам остается только завершить работу текущего экземпляра. Если же работающая копия не найдена, то переопределенная функция Initlnstance приступает к инициализации приложения. Этот процесс уже был подробно описан ранее, и мы не будем к нему возвращаться.

Займемся теперь используемыми классами окон. Прежде всего обратим внимание на базовый класс нашего главного окна:

class CMainFrame : public CMDIFrameWnd 

{

...

};

Как видите, он образован из класса, специально спроектированного для окон MDI. При этом действия по созданию главного окна MDI-прило-жения осуществляются точно такие же, как и при создании любого другого окна: создание объекта "окно", регистрация оконного класса и создание окна Windows. Чтобы не повторяться, коснемся только одного момента. Строгих правил, определяющих место, где следует регистрировать оконный класс, нет. Как правило, большие классы регистрируют в отдельном модуле, а маленькие — в той же самой функции, что и основное окно. Однако это дело вкуса, и вы вправе разрабатывать или перенимать любой стиль, который вас устраивает. Поскольку мы собираемся работать со многими окнами "MDI child" одновременно, и в данном случае все они принадлежат одному оконному классу, то в качестве места регистрации их оконного класса мы выбрали обработчик сообщения WM_CREATE главного окна приложения:

int CMainFrame::OnCreate(LPCREATESTRUCT IpCreateStruct){ 

if (CMDIFrameWnd::OnCreate(IpCreateStruct) == -1)

return -1;

IpszTextClass = AfxRegisterWndClass( 

CS_HREDRAW | CS_VREDRAW, 

AfxGetApp()->LoadCursor(IDC_MDITEXT), 

(HBRUSH) (COLOR_WINDOW+1), 

AfxGetApp (} -XLoadlcon (IDI_MDITEXT) ) ; 

return 0; 

}

Обратите внимание на то, что нам не надо явно создавать специальное окно MDICLIENT, т. к. оно автоматически формируется функциями библиотеки. Вы, конечно, можете вмешаться в этот процесс, переопределив функцию класса CMDIFrameWnd— CreateClient. Однако еще раз подчеркну— чтобы не нарушать правильную работу составных частей вашего приложения, не забывайте в таких случаях вызывать переопределенную функцию базового класса.

В этом месте необходимо сделать небольшое отступление и рассмотреть вопросы, связанные с меню многодокументного интерфейса.

 

Изменение полосы меню

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

Примечание 

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

Код, создающий основное меню для нашей программы, достаточно прост:

IDR_MAINFRAME MENU PRELOAD DISCARDABLE 

BEGIN

POPUP "Файл" 

BEGIN

MENUITEM "Новое OKHO\t", ID_FILE_TEXT

MENUITEM SEPARATOR 

MENUITEM "Выход",  ID_APP_EXIT 

END

POPUP "?" 

BEGIN

MENUITEM "О программе ...", ID_APP_ABOUT 

END

END

 Примечание 

Команду меню "О программе ..." я оставил, поскольку она у нас есть. Но вопросы, которые с ней связаны, мы рассмотрим в следующей главе, когда будем знакомиться с блоками диалога.

Прежде всего мы определяем идентификатор меню — IDR_MAINFRAME, который передаем в функцию LoadFrame для регистрации оконного класса и создания окна Windows. Само тело меню заключено между операторными скобками BEGIN и END. Как видно из текста, элементы меню бывают двух видов: POPUP — целое окно (например, раскрывающийся перечень компонентов, который появляется при открытии меню File (Файл) во многих приложениях) и MENUITEM — единичный элемент меню, после объявления которого в кавычках записана символьная строка, изображаемая в строке меню. Объявляется также константа, которая ассоциирована с данным пунктом меню. (Обратите внимание, что каждый пункт меню ассоциируется с некоторой константой.) Как вы знаете из предыдущей главы, когда пользователь выбирает конкретный пункт меню, соответствующая константа и пункт меню упаковываются в параметры сообщения WM_COMMAND и посылаются в соответствующую оконную процедуру. Связав с этой константой определенный обработчик сообщений, мы можем выполнить любые необходимые нам действия. Например, для того чтобы создать новое окно "MDI child" и вывести его на экран, мы создаем обработчик сообщения Оn-Text и связываем его посредством карты сообщений с командой меню ID_FILE_TEXT аналогично тому, как были связаны между собой сообщение WM_CREATE и обработчик OnCreate:

protected:

//{{AFXJMSG(CMainFrame)

afx_msg void OnText();

afxjnsg int OnCreate(LPCREATESTRUCT IpCreateStruct);

//}}AFX_MSG

DECLARE_MESSAGE_MAP()

...

BEGIN_MESSAGE_MAP (CMainFrame, CMDIFrameWnd)

//({AFX_MSG_MAP(CMainFrame)

ON_COMMAND(ID_FILEJTEXT, OnText)

ON_WM_CREATE()

//}}AFX_MSG_MAP 

END_MESSAGE_MAP()

...

void CMainFrame::OnText() 

{

CTextWnd *pTextWnd = new CTextWnd; 

if (!pTextWnd->Create(IpszTextClass,

_TEXT("Дочернее окно \"MDI child\""), 

0, rectDefault, this))

...

return; 

}

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

Прежде всего необходимо подготовить само меню. Вот как оно выглядит у нас:

IDR_MDITEXT MENU PRELOAD DISCARDABLE

 BEGIN

POPUP "Файл"

BEGIN

MENUITEM "Новое окно",                 ID_FILE_TEXT

MENUITEM SEPARATOR

MENUITEM "Выход",                      ID_APP__EXIT

END

POPUP "Цвет"

BEGIN

MENUITEM "Черный",                     IDM_BLACK 

MENUITEM "Белый",                      IDM_WHITE 

MENUITEM "Красный",                    IDM_RED 

MENUITEM "Синий",                      IDM_BLUE

 MENUITEM "Зеленый",                   IDM_GREEN

END

POPUP "Окно"

BEGIN

MENUITEM "Каскадом",                   ID_WINDOW_CASCA.DE

 MENUITEM "По горизонтали",            ID_WINDOW_TILE_HORZ

 MENUITEM "Упорядочить пиктограммы",   ID_WINDOW_ARRANGE

END

POPUP "?"

BEGIN

MENUITEM "&O программе ...",           ID_APP_ABOUT

END 

END

Для того чтобы подключить меню окна "MDI child" к основному окну MDI-приложения, мы определили компонент m_mеnu класса CTextWnd:

class CTextWnd : public CMDIChildWnd 

{

protected:

static CMenu m_menu; // Меню для всех окон "MDI child"

// Если необходимо для каждого окна 

// использовать свое меню, ключевое слово 

// static надо убрать

...

};

Теперь остается только присвоить идентификатор меню окна "MDI child" специальному компоненту класса:

BOOL CTextWnd::PreCreateWindow(CREATESTRUCT& cs)

{

// Проверяем, не было ли меню уже загружено

 if (m__menu.m_hMenu == NULL)

 m_menu. LoadMenu (IDR__MDITEXT) ;

// Сообщаем, что теперь должно выводиться именно это меню

 m_hMenuShared = menu.m_hMenu;

}

Все остальные заботы по отслеживанию команд меню взяла на себя операционная система. Нам осталось только правильно их обработать. Если вы внимательно посмотрите на карту сообщений класса CTextWnd, например, для меню "Цвет":

BEGINJXESSAGEJMAP(CTextWnd, CMDIChildWnd)

 //({AFX_MSG_MAP(CTextWnd)

...

// Макросов много, а обработчик будет только один

ON_COMMAND(IDM_BLACK, OnColor)

ON_COMMAND(IDM_BLUE, OnColor) 

ON_COMMAND(IDM_GREEN, OnColor) 

ON_COMMAND(IDM_RED, OnColor)

ON_COMMAND(IDM_WHITE, OnColor)

ON_UPDATE_COMMAND_UI(IDM_BLACK, OnUpdateColor)

ON_UPDATE_COMMAND_UI(IDM^BLUE, OnUpdateColor)

ON_UPDATE_COMMAND__UI(IDM_GREEN, OnUpdateColor)

ON_UPDATE__COMMAND_UI (IDM__RED, OnUpdateColor)

ON_UPDATE_COMMAND_UI(IDM_WHITE,.OnUpdateColor)

//}}AFX_MSG__MAP

END_MESSAGE_MAP()

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

 Примечание 

Другой способ использования одного обработчика для нескольких команд, базирующийся на макросе ON_COMMAND_RANGE, был рассмотрен ранее в главе 5. Предложенный способ удобен тем, что позволяет переложить вспомогательную работу на ClassWizard.

Вот как выглядит его код:

// Для значений цветов создаем специальный массив

COLORREF colorArray[] =

{

RGB (О, О, 0), // черный

RGB (255, 255, 255), // белый

RGB (255, 0, 0), // красный ,

RGB (0, 0, 255), // синий

RGB (0, 255, 0) // и зеленый цвета 

};

...

void CTextWnd: :OnColor () 

{

// Получаем идентификатор команды

nIDColor = LOWORDfGetCurrentMessage()->wParam);

//По идентификатору находим значение цвета

clrText = colorArray[nIDColor - IDM^BLACK];

// Даем команду на перерисовку окна

Invalidate();

 }

 Примечание 

Здесь необходимо соблюдение обязательного условия: идентификаторы команд цветов должны строго следовать друг за другом. Иначе вы получите непредсказуемые результаты. Сбой в порядке следования идентификаторов происходит в том случае, если произошло переключение с формирования меню на ' создание какого-либо другого ресурса. В этом случае необходимо открыть файл <resource.h> и вручную присвоить последовательные числовые значения требуемым идентификаторам.

Использованная в этом коде функция CWnd::GetCurrentMessage возвращает указатель на структуру MSG, в слове wParam которой содержится идентификатор выбранного пункта меню (идентификатор команды). Вот фрагмент из файла <resource.h>:

#define IDM_BLACK          101

#define IDM_WHITE          102

#define IDM_RED            103

#define IDMJBLUE           104

#define IDM_GREEN          105

Как видите, используемые в данном фрагменте идентификаторы команд идут один за другим. Это не случайно и позволяет нам использовать их для определения индекса в массиве цветов как (nIDColor— IDM_BLACK). Попробуйте изменить числовое значение у какого-либо идентификатора — программа сразу же перестанет адекватно реагировать на эту команду.

Нам осталось рассмотреть принцип действия специальной команды обновления и связанный с ней класс CCmdUI. Поставим задачу отметить "галочкой" тот элемент меню, который является выбранным на текущий момент времени. Реализовать это очень просто — воспользуемся функцией CCmdUI::SetCheck в обработчике OnUpdateColor. Поскольку этот обработчик

каждый раз вызывается для всех элементов раскрывающегося меню, то перед перерисовкой необходимо обеспечить, чтобы "галочка" была установлена только у того элемента, который был перед этим выбран (pCmdui->m_niD == nIDcoior), а у остальных — убрана:

void CTextWnd::OnUpdateColor(CCmdUI* pCmdUI) 

{

// Поскольку текущим может быть только один цвет,

// необходимо его отслеживать

pCmdUI->SetCheck(pCmdUI->m_nID == nIDColor); 

}

Вот, собственно, и все, что я хотел рассказать о работе с меню в приложениях многооконного интерфейса.

Непосредственный процесс создания окна вам уже хорошо знаком по одно-документным приложениям. Поэтому обратим внимание только на следующую строку из функции ОnТехt.

_ТЕХТ ("Дочернее окно V'MDI child\"") .

Интерес здесь представляет макрос _ТЕХТ, содержащийся во включаемом файле <tchar.h>. Чтобы ответить на вопрос "Для чего он нужен?", сделаем еще одно небольшое отступление.

 

Стандарт Unicode

Unicode — стандарт, разработанный в 1988 году фирмами Apple и Xerox. Его полное описание содержится в The Unicode Standard: Worldwide Character Encoding, Version 1.0. Нас же будет интересовать общее понимание того, что же такое Unicode,

Поскольку система Windows становится все популярнее, то разработчикам приложений для нее стоит ориентироваться на международный рынок. Естественно, что при этом встает вопрос о поддержке самых разных языков. А ведь существуют и такие, что возможностей одного байта, позволяющего кодировать не более 256 символов, оказывается недостаточно для представления всех букв языка. Для поддержки подобных языков были созданы двухбайтовые наборы символов (Double-Byte Character Sets, DBCS). Однако работать с ними оказалось очень неудобно, т. к. для представления одних символов достаточно одного байта, а для других необходимо два.

В отличие от DBCS, всем символам в Unicode требуется ровно два байта, что позволяет кодировать около 65 000 символов — более чем достаточно для работы с любым языком. В настоящее время кодовые позиции (code points) определены для арабского, китайского, русского (кириллица), греческого, еврейского, латинского (английского), японского (кана) и некоторых других

алфавитов. Кроме того, в набор символов включено большое количество знаков препинания, математических, технических, диакритических и других символов.

И хотя пока Unicode поддерживается только в системе Windows NT и не поддерживается в Windows 95/98, для упрощения задачи по локализации программного продукта в будущем мы советуем вам уже сейчас включать поддержку Unicode в ваши программные проекты. Тем более что это Совсем несложно, ведь фирма Microsoft разработала Win32 API для Unicode так, чтобы как можно меньше повлиять на ваш код. Для переключения на Unicode надо всего лишь определить два макроса и перекомпилировать исходный текст. Естественно, это возможно при условии, что вы вставили в вашу программу файл заголовков <tchar.h> и используете макросы из этого файла вместо стандартных функций для работы со строками, такими, например, как strlen. Полный перечень макросов вы найдете в файле <tchar.h>. Но это еще не все. Для задания массива символов ANSI/Unicode следует использовать тип данных TCHAR. При этом если определен тип _UNICODE, то TCHAR объявляется как

typedef wchar_t TCHAR; 

а если не определен, то как

typedef char TCHAR;

При использовании этого типа данных строка символов объявляется как

TCHAR szString[128];

Кроме того, для корректной работы со строками необходимо использовать макрос _ТЕХТ (или _Т), определенный как

#define _TEXT(x) L##x       // для Unicode

и

#define _ТЕХТ(х) х          // для ANSI

что мы и продемонстрировали в нашей программе.

В заключение просто перечислим шаги, которые необходимо проделать, чтобы подготовить приложение к использованию Unicode:

После этого небольшого отступления можно возвращаться к нашему примеру.

Последней в обработчике OnColor вызывается функция CWnd::Invalidate. Мне представляется важным познакомиться с ней поближе.

void CWnd::Invalidate (BOOL bErase = TRUE)

Функция определяет в качестве области модификации всю рабочую область окна как требующую перерисовки. В результате приложению будет послано сообщение WM_PAINT. Это сообщение Windows посылает всякий раз, когда область модификации окна не пуста, и для этого окна нет других сообщений в очереди. Параметр, передаваемый в функцию, определяет необходимость стирания фона окна перед перерисовкой: если он имеет значение TRUE, фон стирается, если FALSE — нет.

 Примечание 

Для того чтобы отметить не всю рабочую область, а только какую-либо ее часть, в классе CWnd реализованы специальные функции InvalidateRect и InvalidateRgn. Первая из них "помечает" прямоугольную область, а вторая — произвольную.

 

Еще раз о сообщении WM_PAINT

Сообщение, которое мы здесь рассматриваем, является одним из наиболее важных, если не самым важным, поскольку приложение, как правило, выполняет всю работу по рисованию именно во время обработки этого сообщения. Оно посылается в окно всякий раз, когда требуется изменить содержимое клиентской области, т. е. того пространства окна, о котором программист заботится самостоятельно, в то время как Windows обслуживает остальные части. Например, если окно было "спрятано" под другим окном и выводится на передний план, то оно получает сообщение WM_PAINT, т. к. необходимо перерисовать область, скрытую под другим окном.

Рассмотрим обработчик этого сообщения, реализованный в нашей программе:

void CTextWnd::OnPaint() 

{

// Без определения контекста устройства

// мы ничего не сможем нарисовать

CPaintDC dc(this);

// Выбираем в контекст устройства текущий цвет

dc.SetTextColor(clrText);

//Цвет фона запрашиваем у системы

dc. SetBkColor (: :GetSysColor (COLOR_WINDOW)) ;

// Определяем прямоугольник, в котором будем рисовать

CRect rect;

GetClientRect.(rect) ;

//В данном случае "рисование" будет заключаться

//в выводе строки текста

// Отображаем текст в центре окна

dc.DrawText(_ТЕХТ("Обработка сообщения WM_PAINT"!, -1, rect,

DT_SINGLELINE | DT^CENTER | DT_VCENTER);

}

Как вы помните, любой выводив программах, написанных для Windows, осуществляется в контекст устройства, который позволяет абстрагироваться от конкретного типа внешнего устройства. Здесь еще не место и не время подробно рассматривать поддержку контекстов устройств, имеющуюся в библиотеке MFC, — этому вопросу была посвящена отдельная большая глава. Однако, ввиду важности сообщения WM_PAINT и приняв в качестве постулата необходимость наличия специального контекста для окна, рассмотрим обработчик этого сообщения достаточно подробно.

Первая же строка обработчика содержит код для создания объекта класса CPaintDC, который предоставляет контекст дисплея для обработки сообщения WM_PAINT.

Конструктор этого объекта в качестве параметра получает указатель на оконный объект, для которого запрашивается контекст, вызывает автоматически функцию CWnd::BeginPaint, обеспечивающую доступ к нему, и сохраняет структуру PAINTSTRUCT в специальной переменной m_ps. После окончания работы с контекстом устройства его необходимо вернуть в систему, чтобы избежать истощения ресурсов. Для этого в деструкторе автоматически вызывается функция CWnd::EndPaint. Между этими двумя процедурами располагается ядро обработчика сообщения WM_PAINT.

Наш обработчик демонстрирует лишь один из основных способов вывода текста в Windows-программах. Он основан на применении функции Draw-Text, которую мы уже рассматривали. Для форматирования мы выбрали центрирование по вертикали и горизонтали, а также вывод текста одной строкой. Функция DrawTex использует контекст устройства, чтобы установить цвета для фона и текста. Для того чтобы задать свои значения этих параметров, мы воспользовались специальными функциями SetTextColor и SetBkColof.

В нашем обработчике осталась еще одна функция. И поскольку она не имеет отношения к контексту устройств, опишу ее подробно:

void CWnd::GetClientRect (LPRECT IpRect)

Функция заполняет структуру (или объект) ПЕСТ, на которую указывает параметр IpRect, размерами рабочей области окна. При этом параметры top и left будут равны нулю, a right и bottom определяют ширину и высоту рабочей области.

Сама структура объявлена в файле <windef.h>:

typedef struct tagRECT{ 

LONG left;

LONG top; 

LONG right;

 LONG bottom; 

} RECT;

Определяет координаты левого верхнего (left— х-координата, top— у-коор-дината) и правого нижнего (right— х-координата, bottom— у-координата) углов некоторого прямоугольника.

Поскольку Windows большей частью работает с прямоугольными областями, в библиотеке MFC определен специальный класс CRect, который опирается на только что рассмотренную структуру RЕСТ и, кроме того, включает в себя специальные функции для манипулирования объектами класса. Мы не будем рассматривать эти функции, т. к. их использование достаточно тривиально, а перечень и описание легко можно посмотреть в Help. To же самое относится к удобным и часто используемым классам CSize и CPoint. И последнее, что хотелось бы сказать по поводу сообщения WM_PAINT. Проделайте следующий эксперимент. Добавьте в программу обработчик нажатия левой кнопки мыши (как это сделать, вы уже отлично знаете):

void CTextWnd:rOnLButtonDown(UINT nFlags, CPoint point) 

//И здесь нам нужен контекст устройства

CClientDC dc(this);

TCHAR StrText [] = "Обработчик сообщения WM_LBUTTONDOWN";

// Задаем цвета текста и фона

dc.SetTextColor(clrText);

dc.SetBkColor(::GetSysColor(COLOR_WINDOW));

// Выводим собственно текст

dc.TextOut(10, 10, strText, strlen(strText));

//He забываем передать управление библиотеке,

//ив этом нам хорошо помогает ClassWizard

CMDIChildWnd::OnLButtonDown(nFlags, point); 

}

Еще раз подчеркнем — в Windows для проведения любых операций, связанных с выводом, необходимо сначала получить контекст устройства. Как видите, здесь для получения контекста при выводе текста мы использовали другой класс — CClientDC. Кроме того, для вывода текста мы воспользовались еще одной имеющейся в библиотеке функцией — TextOut.

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

Рис. 13.6. Пример вывода текста в рабочую область окна с использованием и без использования обработчика сообщения WM_PAINT

Если вы отчетливо понимаете, что произошло, то вы действительно вникли в суть механизмов, реализованных в системе сообщений Windows. Сообщение WM_PAINT посылается в окно каждый раз, когда открывается его часть, временно закрытая другим окном. Таким образом, текст "Обработка сообщения WM_PAINT" будет виден всегда, когда видимо окно, просто потому, что Windows гарантирует посылку сообщения WM_PAINT в нужный момент. Однако любой текст, выводимый обработчиком OnLButtonDown, не будет перерисовываться вновь. Причина заключается в том, что в этот обработчик посылается только сообщение WM_LBUTTONDOWN, и он "ничего не знает" о сообщении WM_PAINT и не обращает на него внимания.

В заключение осталось сказать несколько слов о сообщениях, обрабатываемых окнами "MDI child" по умолчанию. К ним относятся: WM_MOVE, WM_SETFOCUS, WM_CHILDACTIVATE, WM_GETMINMAXINFO, WM_MENUCHAR, WM_SIZE, WM_NEXTMENU и WM_SYSCOMMAND. И если вы перехватываете какое-либо из них, то не забывайте вызывать соответствующий обработчик базового класса.