Программирование на языке MFC

Мой второй блог в серии программирования

Что мы можем здесь увидеть? То, что в описании этого класса присутствует поле rnJTemplateList класса CPtrList, только подтвер­ждает нашу догадку о том, что de facto объекты класса CDocMan­ager являются списками указателей на какие-то шаблоны. Для того чтобы убедиться, что наша догадка верна, достаточно взглянуть на исходный код методов GetFirstDocTemplatePosition() и GetNext-DocTemplate(), который находится в файле docmgr.cpp:

POSITION CDocManager::GetFirstDocTemplatePosition() const {

return m_templateList.GetHeadPosition() ;

}

CDocTemplate* CDocManager::GetNextDocTemplate(

POSITIONS pos) const

{

return (CDocTemplate*)m_templateList.GetNext(pos) ;

Теперь возникает вопрос о том, что за шаблоны включаются в список. Позвольте, уважаемый читатель, высказать предположе­ние о том, что этими шаблонами являются шаблоны ДОКУМЕН­ТОВ, с которыми работает данное приложение. Я не буду сейчас останавливаться на том, что такое шаблон документа. Это станет ясно из дальнейшего изложения.

Сейчас нам необходимо научиться создавать объект этого класса. Естественно, что для создания объекта мы восполь­зуемся конструктором. Раз конструктор класса CDocManager параметров не имеет, то создание объекта этого класса за­труднений не вызовет. Но каким-то образом нам необходимо передать нашему приложению информацию о том, с документа­ми каких типов (шаблонов) он будет иметь дело! Вспомним, что у класса CWinApp для добавления шаблона в список есть метод AddDocTemplate(), который, фактически является вызовом одно­именного метода класса CDocManager. Наверное, именно этим методом и следует воспользоваться при добавлении шаблона документа!



Наверное, из описания класса CWinApp мы не сумеем извлечь еще какую-то информацию о классе CDocManager. Настало время взглянуть на описание этого класса. Думаю, нет необходимости описывать всю внутреннюю реализацию этого класса. Достаточно будет, если мы рассмотрим назначение наиболее часто используе­мых полей и методов.

Класс CDocManager в файле afxwin.h описан следующим образом:

class CDocManager : public CObject {

DECLARE_DYNAMIC(CDocManager) public:

// Constructor CDocManager();

//Document functions

virtual void AddDocTemplate(CDocTemplate* pTemplate); virtual POSITION GetFirstDocTemplatePosition() const; virtual CDocTemplate* GetNextDocTemplate(

POSITIONS pos) const; virtual void RegisterShellFileTypes(BOOL bCompat); void UnregisterShellFileTypes();

virtual CDocument* OpenDocumentFile(LPCTSTR IpszFileName);

// open named file

virtual BOOL SaveAllModified(); // save before exit virtual void CloseAllDocuments(BOOL bEndSession);

// close documents before exiting virtual int GetOpenDocumentCount();

// helper for standard commdlg dialogs virtual BOOL DoPromptFileName(CString& fileName

UINT nIDSTitle, DWORD lFlags, BOOL bOpenFileDialog, CDocTemplate* pTemplate);

//Commands

// Advanced: process async DDE request

virtual BOOL OnDDECommand(LPTSTR IpszCommand); virtual void OnFileNew(); virtual void OnFileOpen();

// Implementation protected:

CPtrList m_templateList;

int GetDocumentCount(); // helper to count number

// of total documents

public:

static CPtrList* pStaticList;

// for static CDocTemplate objects static BOOL bStaticInit;

// TRUE during static initialization static CDocManager* pStaticDocManager;

// for static CDocTemplate objects

public:

virtual -CDocManager(); #ifdef _DEBUG

virtual void AssertValid() const;

virtual void Dump(CDumpContext& dc) const; #endif };



Значит, либо я сделал что-то не так, либо в программе не опре­делил какие-то данные. Второй вариант, конечно, более вероя­тен. Для того чтобы убедиться в этом, давайте, уважаемый чи­татель, посмотрим на то место, в котором была обнаружена ошибка. Как оказалось, на этом месте находится исходный текст метода OnFileOpenQ класса CWinApp. Ниже приведен текст это­го метода:

void CWinApp::OnFileOpen() {

ASSERT(m_pDocManager != NULL); m_pDocManager->OnFileOpen();

}

Помните, я говорил, что имеющий уши да услышит? Во-первых, судя по наименованию переменных, используемых в приведенном выше тексте метода, MFC по умолчанию считает, что наше прило­жение построено в соответствии с идеологией «Документ/представ­ление»! Другими словами, именно эта идеология считается при­оритетной при работе с MFC! Значит, именно ЭТОЙ идеологии в MFC отводится очень важная роль. Возможно, подумал я, что понимание одного из ключевых моментов MFC приблизит меня к по­ниманию идеологии всей библиотеки в целом! Но оставим «лири­ческое отступление» и продолжим нашу работу.

Во-вторых, здесь MFC сама подсказывает нам причину ошибки в программе! Из приведенного выше текста метода мы можем сделать три важных заключения. Заключение первое состоит в том, что у класса CWinApp есть член m_pDocManager, кото­рый, судя по его названию, является указателем на какой-то ме­неджер документов. Заключение второе – вероятнее всего, имен­но этот менеджер документов и отвечает за управление доку­ментами в приложении. И наконец, заключение третье. Очевид­но, что единственной причиной, которая может привести к выда­че приведенного выше сообщения об ошибке, является равен­ство нулю указателя на менеджер документов. Следовательно, причина этой ошибки состоит в том, что мы просто-напросто не создали менеджер документов!

А что следует из этих заключений? Из этих заключений следует один очень важный вывод.

Тсли наше приложение не обрабатывает команду ID_FILE_OPEN и обработку производит метод CWinApp::OnFileOpen, то перед вызовом этого метода дол­жен быть создан объект класса CDocManager и указатель на этот объект должен быть присвоен полю CWinApp::m_pDocManager.

К сожалению, искать информацию о менеджере документов в MSDN совершенно бесполезно. И в описании членов класса CWi­nApp мы также не найдем и упоминания об m_pDocManager. При­дется опять смотреть исходные коды MFC…



Я начал реализовывать этот план и какое-то время работал в этом направлении. Однако достаточно быстро я одумался и за­дал себе один вопрос: а почему, собственно говоря, мне приходит­ся делать всю черновую работу самому? Исходя из того, что я чи­тал про MFC ранее, все должно быть намного проще. Если я иду в правильном направлении, то почему мне приходится делать все самому вручную? Где же хваленые возможности MFC? Все же, на­верное, я чего-то не понимаю. И здесь мне пришла в голову мысль, результатом которой и явилось правильное решение. А не попро­бовать ли мне возложить обработку открытия файла на MFC? Ведь не зря же у класса CWinApp есть метод OnFileOpenQ, не так ли? Карту сообщений своего объекта приложения я немного изменил. Теперь обработчик команды ID__FILE__OPEN стал выглядеть сле­дующим образом:

ON_COMMAND( ID_FILE_OPEN, CWinApp::OnFileOpen )

Тем самым обработку открытия файла я возложил на MFC. Мне осталось только понять, к чему это приведет…

Программа откомпилировалась без ошибок, что меня, честно говоря, несколько удивило. Однако, когда я запустил программу на выполнение в отладочном режиме, то, выбрав элемент «Ореп» меню «File», я получил сообщение, которое приведено на.



А теперь текст непосредственно программы:

#include "stdafx.h"

// Объявляем класс приложения, его поля и методы.

class CDocViewlApp : public CWinApp

{

public:

CDocViewlApp () ; :* protected:

afx_msg void OnFileOpen(); virtual BOOL Initlnstance (); DEC LARE_ME S S AG E_MA P()

};

CDocViewlApp::CDocViewlApp()

{

}

void CDocViewlApp::OnFileOpen()

{

}

class CMainFrame : public CMDIFrameWnd {

DECLARE_DYNAMIC( CMainFrame ); public:

CMainFrame () ; «’

};

IMPLEMENT_DYNAMIC(CMainFrame, CMDIFrameWnd)

CMainFrame::CMainFrame()

{

}

BOOL CDocViewlApp::Initlnstance() {

#ifdef _AFXDLL

Enable3dControls() ; #else

Enable3dControlsStatic(); #endif

CMainFrame* pMainFrame = new CMainFrame; pMainFrame->LoadFrame( IDR_RESOURCE ); m_pMainWnd = pMainFrame; pMainFrame->ShowWindow(m_nCmdShow ); pMainFrame->UpdateWindow();

return TRUE;

}

BEGIN_MESSAGE_MAP( CDocViewlApp, CWinApp )

ON_COMMAND( ID_FILE_OPEN, OnFileOpen ) END_MESSAGE_MAP()

CDocViewlApp theApp;



Документ, естественно, рождается из файла, поэтому первым делом метод, вызывая GetFile(), открывает файл, имя которого, как я уже говорил, он получил в качестве аргумента. Затем начинается самое интересное. На основе файла создается объект класса САг-chive, т. е. АРХИВ. В архитектуре «документ/представление» счи­тается, что программист должен иметь возможность легко сохра­нять созданные в памяти структуры данных в дисковом файле, после чего он должен иметь возможность вновь считать их из фай­ла. Архив позволяет программисту читать и записывать в файл не просто некоторые объемы информации, а ОБЪЕКТЫ всевозмож­ных типов! Именно поэтому его использование в большинстве слу­чаев представляется оправданным. И дальше вызывается метод Serialize(), который работает уже не с файлом, а с архивом! По умолчанию этот метод не делает ничего. Естественно, откуда про­грамме знать, что и как программисту захотелось сохранить в ар­хиве? А для программиста здесь раздолье! Можно проверить фор­мат открытого файла, определить, при необходимости, его струк­туру, выполнить все мыслимые и немыслимые действия. Для того чтобы продемонстрировать работу метода Serialize(), давайте по­пробуем открыть файл в нашей программе и считать его в буфер. Внесем небольшие изменения в наш класс документа – добавим два поля, в которые запишем, во-первых, размер файла, а второй будет являться указателем на буфер, в который мы будем читать файл. После внесенных изменений наша программа выглядит сле­дующим образом:

#include "stdafx.h" #include <afxcview.h> #include "resource.h"

// Объявляем класс приложения, его поля и методы.

class CDoc : public CDocument {

DECLARE_DYNCREATE( CDoc ) int nFileLength; void* pMyFile; public: CDocO ;

void Serialize ( CArchive &ar ); virtual -CDoc();

}/

• IMPLEMENT_DYNCREATE( CDoc, CDocument )

CDoc::CDoc()

{

}

CDoc::-CDoc()

{

}

void CDoc::Serialize( CArchive &ar ) {

if( ar.IsStoring() )

{

}

else {

// Определяем длину файла.

nFileLength = ar.GetFile()->GetLength();’

TRACE( _T( "File length = %d.\n"), nFileLength’ );

// Выделяем буфер в памяти, в который будем считывать файл. pMyFile = new chart nFileLength ];

// Считываем файл в буфер.

ar.Read( pMyFile, nFileLength ); }

}

class CDocViewlApp : public CWinApp {

public:

CDocViewlApp(); protected:

afx_msg void OnFileOpen(); virtual BOOL Inifrlnstance(); DEC LARE_ME S S AGE_MA P()

};

CDocViewlApp::CDocViewlApp()

{

}

void CDocViewlApp::OnFileOpen()

{

}

class CMainFrame : public CMDIFrameWnd {

DECLARE_DYNAMIC( CMainFrame ); public:

CMainFrame();

>;

IMPLEMENT_DYNAMIC (CMainFrame, CMDIFrameWnd)

CMainFrame::CMainFrame()

{

}

BOOL CDocViewlApp::Initlnstance() {

#ifdef _AFXDLL

Enable3dControls(); #else

Enable3dControlsStatic(); #endif

LoadStdProfileSettings(); CDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate( IDR_DOCUMENT,

RUNTIME_CLASS( CDoc ) , RUNTIME_CLASS( CMDIChildWnd ), NULL );

AddDocTemplate( pDocTemplate ); CMainFrame* pMainFrame = new CMainFrame; pMainFrame->LoadFrame ( IDR__RESOURCE ) ; m_pMainWnd = pMainFrame; pMainFrame->ShowWindow(m_nCmdShow ); pMainFrame->UpdateWindow();

return TRUE;

}

BEGIN_MESSAGE_MAP( CDocViewlApp, CWinApp )

ON_COMMAND( ID_FILE_OPEN, CWinApp::OnFileOpen ) END_MESSAGE_MAP()

CDocViewlApp theApp;

Если мы запустим нашу программу на выполнение (внутри сре­ды разработки) и откроем какой-нибудь файл, то увидим, что на отладочном мониторе появилось сообщение о размере открытого файла. Любознательный читатель может проверить, произойдет ли считывание содержимого файла в буфер. Я абсолютно уверен, что в обычных условиях файл будет считан в буфер без каких-либо проблем.



Вносим необходимые изменения в метод InitlnstanceQ нашего приложения:

BOOL CDocViewlApp::Initlnstance() {

fifdef _AFXDLL

Enable3dControls(); #else

Enable3dControlsStatic(); #endif

LoadStdProfileSettings(); CDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate( IDR_DOCUMENT,

RUNTIME_CLASS(CDoc), NULL, NULL ); AddDocTemplate( pDocTemplate ); CMainFrame* pMainFrame = new CMainFrame; pMainFrame->LoadFrame( IDR_RESOURCE ); m_pMainWnd = pMainFrame; pMainFrame->ShowWindow(m_nCmdShow );

pMainFrame->UpdateWindow(); return TRUE;

}

Снова компилируем программу, запускаем на выполнение. ..И б-лагополучно минуем то место, на котором в прошлый раз у нас было выдано сообщение об ошибке. Однако нам необходимо по­нять, что происходит при создании документа, т. е. что происходит при работе метода CDocTemplate::CreateNewDocument().

CObject* CRuntimeClass::CreateObject() {

if (m_pfnCreateObject == NULL) {

TRACE(_T("Error: Trying to create object which is not ") _T("DECLARE_DYNCREATE \nor DECLARE_SERIAL: %hs.\n"), m_lpszClassName); return NULL;

}

CObject* pObject = NULL;

TRY

{

pObject = (*m_pfnCreateObject) () ;

}

END_TRY

return pObject;(CWJnApp::OnFJIeOpen ) Во-вторых, при помощи метода
S . CMultiDocTemplate::AddDocu-

ЦCDocManager::OnFileOpen J ment() (файл docmulti.cpp)

UcWinAppriOpenDocumentFile) только что созданный

N J документ добавля-

L-[ CDocManager::OpenDocumentFile ] ется в список

CMultiDocTemplate::OpenDocumentFjle] Документов
—’ шаблона:

CDocTemplate::CreateNewDocum^n?)

^CMultiDocTemplate::AddDocument]

void CMultiDocTemplate::AddDocument(CDocument* pDoc) {

ASSERT_VALID(pDoc);

CDocTemplate::AddDocument(pDoc);

ASSERT(m_docList.Find(pDoc, NULL) == NULL);

// must not be in list

m_docList.AddTail(pDoc);

}

CWinAPP::OnFileOpen ) A этот мет0* Фактически представ-
————————– J ляет собой вызов одноименного

ч

CDocManager::OnFileOpen ] метода базового класса

CWinApp::OpenPocumentFilQ (фаЙЛ doctemPlcPP):

^-{CDocManager^OpenDocumentFile")

L^CMultiDocTemplate::OpenDocumentFile] CDocTemplate::CreateNewDocument] CMultiDocTemplate::AddDocument] L^CDocTemplate::AddDocument ]

void CDocTemplate::AddDocument(CDocument* pDoc) {

ASSERT_VALID(pDoc);

ASSERT(pDoc->m_pDocTemplate == NULL); // no template attached yet pDoc->m_pDocTemplate = this;

Исходя из текстов этих методов, можно заметить, что не только у шаблона документа есть список документов, открытых на основе данного шаблона, но и у каждого документа есть указатель на тот шаблон, при помощи которого он был открыт. Этакая своеобраз­ная обратная связь!

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



Но взглянем на это окно повнимательнее. В основном меня все в нем устраивает, за исключением того, что мне предлагается осу­ществить выбор из всех имеющихся файлов. А если я хочу, чтобы мне был показаны только файлы с расширением «ехе» или «txt»? Могу ли я каким-нибудь образом изменить фильтр? А на этот во­прос ответить не так просто, как на первый. Здесь ответ не лежит на поверхности. Что ж, делать нечего, придется опять разбираться с исходным текстом MFC.

Поставим точку прерывания на методе CWinApp::OnFileOpen() в файле appdlg.cpp и запустим программу на выполнение. Стоп! Выполнение программы приостановилось. Исходный текст мето­да, на котором мы остановились, приведен ниже:

docmgr.cpp берем исходный код

метода CDocManager::OnFileOpen():

void CDocManager::OnFileOpen() {

// prompt the user (with all document templates) CString newName;

if (!DoPromptFileName(newName,

AFX_IDS_OPENFILE, OFN_HIDEREADONLY | FN_FILEMUS TEX1ST,

TRUE, NULL))

return; // open cancelled

AfxGetApp()->OpenDocumentFile(newName)/

// if returns NULL, the userhas already been alerted

CDocManager::OnFileOpen ]

Здесь же явно видно, что процесс открытия файла J разделен на два этапа. Этап

первый – вызов метода DoPromptFileNameO (забегая вперед, скажу, что именно эта функция обеспечивает подготов­ку и выдачу диалогового окна для выбора файла). Этап второй -непосредственно открытие файла и преобразование его в доку­мент. Наверное, логично будет, если мы изложение материала так же разделим на две части. В первой части мы должны рассмот­реть, каким образом формируется стандартное диалоговое окно для выбора открываемого файла. Кроме этого, мы должны опре­делить, каким образом мы можем повлиять на процесс формиро­вания этого окна. Во второй части нам необходимо понять, что про­исходит после того, как имя открываемого файла определено. Дру­гими словами, нам нужно понять, как из файла рождается доку­мент.



Итак, мы создали заготовку шаблона документа. Но до сих пор нам не ясно, что происходит непосредственно после того, как мы выбе­рем элемент «Ореп» меню «File». Другими словами, мы не знаем, как происходит открытие документа.

Естественно, до написания этих заметок я уже трассировал метод CWinApp::OnFileOpen(). Поэтому мне бы хотелось, чтобы чи­татель набрался терпения. Работа предстоит не очень сложная, однако достаточно кропотливая.

(cWinApp::OnFileOpen J Итак>после создания шаблона документа нам необходимо создать документ. О том, что такое документ и что нам необходимо сделать для того, чтобы создать его, мы пока не имеем ни малейшего понятия. Именно поэто­му мы не сделали НИКАКИХ изменений при вызове конструктора шаб­лона. Указатель на информацию времени выполнения нашего доку­мента при инициализации шаблона до сих пор равен NULL.

Что ж, откомпилируем нашу программу и постараемся выпол­нить ее. Так, окно отобразилось. Выбираем элемент «Ореп» меню «File»… Вот это да! В этот момент произошло то, чего я, честно говоря, никак не ожидал. На экране появилось стандартное диало­говое окно (см. 12), предлагающее мне осуществить выбор того файла, который будет открыт.

Откуда оно взялось? Я же не писал ни строчки кода для вызова этого окна! Мне так и захотелось воскликнуть: «Ай да MFC!» © Биб­лиотека оказала мне такую услугу! Как вспомню о том множестве полей, которые необходимо было заполнить соответствующими зна­чениями при подготовке стандартного диалогового окна… Ответ на вопрос о «происхождении» окна очевиден. Код, обеспечиваю­щий выдачу диалогового окна находится где-то в недрах MFC. Иначе

) tookpif^MyProjects

· sti

· the first

Fitebarne:: r[" .Files of typer

Cancel

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



03.02.2010

Здесь мы увидим кое-что интересное. Так… MFC пытается за­грузить меню и таблицу акселераторов… Но их идентификатор совпадает с идентификатором строки, которую пытается загрузить конструктор базового класса! Отсюда следующие выводы.

Вывод первый. В состав ресурсов, идентификатор которых используется при создании шаблона документа, могут входить строка, меню и таблица акселераторов. Вывод второй (обратный первому). Идентификаторы строки, меню и таблицы акселераторов, которые будут использоваться при работе программы, написанной в соответствии с архитектурой «документ/представление»,должны быть ОДИНАКОВЫМИ!

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

Метод CWinApp::AddDocTemplate(CDocTemplate* pTemplate) ни­чего интересного для нас в себе не содержит. В нем происходит инициализация списка шаблонов и добавление шаблона в этот список. Мне кажется, что более подробное рассмотрение этого вопроса ничего существенного к пониманию нами процесса созда­ния шаблона не добавит.

Пример

Ну, кажется, «охота на ресурсы» завершена успешно. Мы выяс­нили, какие ресурсы могут входить в состав приложения. Обрати­те, пожалуйста, внимание на то, что мы НИ РАЗУ не заглянули в документацию, предлагаемую MSDN, или в какие-нибудь другие пособия по MFC. Исходный код MFC позволил нам самостоятель­но сделать выводы, приведенные выше.

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



03.02.2010

Взглянув на этот текст, мы можем сделать вывод, который в зна­чительной степени облегчит нам жизнь.

В приложении 1У|ожно не создавать явным образом объект класса CDocManager. Если объект класса CDocManager не был создан вручную до момента добавления первого шаб­лона, то в момент добавления первого шаблона документа этот объект будет создан автоматически.

Давайте вспомним также, что при вызове метода CWinApp::Add-DocTemplate(CDocTemplate*pTemplate) фактически вызывается метод объекта класса CDocManager:: AddDocTemplate( CDocTemplate* pTem­plate). Из этого факта можно сделать еще один вывод.

Для того, чтобы передать нашему приложению информа­цию о том, с какими типами документов ему придется рабо­тать, нам необходимо до создания (или открытия) документа добавить в список шаблонов шаблон нашего документа. Всю остальную работу MFC, вероятно, сделает самостоя­тельно!

Зная все это, можно более подробно описдть причину, которая вызвала появление сообщения об ошибке. Ошибка, из-за которой мы полезли в глубины MFC, заключается в том, что мы не добави­ли в приложение ни одного шаблона документа, из-за чего не был проинициализирован m_pDocManager и мы получили соответствую­щее сообщение. Ура! Причину ошибки мы определили! Но ведь нам нужно не только определить причину ошибки, но и устранить ее! Другими словами, нам необходимо создать шаблон документа, а потом добавить его в список шаблонов.



Здесь мне представляется необходимым еще раз привести ис­ходный код метода CWinApp::AddDocTemplate():

void CWinApp::AddDocTemplate(CDocTemplate* pTemplate) {

if (m_pDocManager == NULL)

m_pDocManager = new CDocManager; m_pDocManager->AddDocTemplate(pTemplate);

}



03.02.2010

Строчку, которая нас интересует, мы найдем в файле afxwin.h. В описании класса CWinApp среди множества полей мы увидим:

CDocManager* m_pDocManager;

Значит, мы на правильном пути! Более того, это поле описа­но как общедоступное (public)! Но почему-то в MSDN я не нашел ни слова о классе CDocManager, за исключением нескольких упо­минаний в разделах периодики и Knowledge Base. Честно гово­ря, меня это несколько удивило. С одной стороны, член класса описан как доступный (public). А с другой, чтобы использовать его сначала нужно пролезть по исходникам MFC. Тем самым тех­нические писатели, готовившие описание MFC, разорвали связь между приложением и СИСТЕМОЙ используемых при работе архитектуры «документ/представление» классов. Но ничего не поделаешь. Из-за того, что в MSDN нет ни слова об объекте это­го класса, нам придется немного задержаться и взглянуть на описание этого объекта.

Сначала нам неплохо было бы получить хотя бы общее представ­ление о том, что представляет собой класс CDocManager. В описании класса CWinApp можно найти всего три метода, которые работают с этим полем. Этими методами являются AddDocTemplate(), Get-FirstDocTemplatePosition() и GetNextDocTemplate(). Их исходный код, который находится в файле appui2.cpp, приведен ниже:

void CWinApp::AddDocTemplate(CDocTemplate* pTemplate) {

if (m_pDocManager == NULL)

m_pDocManager = new CDocManager; m_pDocManager->AddDocTemplate(pTemplate) ;

}

POSITION CWinApp::GetFirstDocTemplatePosition() const {

if (m_pDocManager == NULL) return NULL;

return m_pDocManager->GetFirstDocTemplatePosition();

}

CDocTemplate* CWinApp::GetNextDocTemplate(POSITIONS

rPosition) const

{

ASSERT(m_pDocManager != NULL);

return m_pDocManager->GetNextDocTemplate(rPosition) ;

}

Судя по именам и исходному коду, эти методы предназначены для работы со списком каких-то шаблонов, не так ли? В таком слу­чае возможно, что в основе своей класс CDocManager является списком, не так ли?



Уважаемый читатель, сейчас нам с вами предстоит разобраться, каким образом должна быть написана простейшая программа ра­боты с MFC. В тех книгах, которые я читал ранее, этот процесс описывался очень отрывочно. Создайте класс, производный от клас­са CWinApp, перекройте метод lnitlnstance()… А почему так? Поче­му я должен перекрывать именно метод lnitlnstance(), а не какой-то другой? То есть мне предлагалось просто поверить автору на слово, а я не хочу так делать. Постараемся сейчас самостоятель­но разобраться, что к чему.

Пример

Если вы помните, уважаемый читатель, я ранее оговорился, что при описании MFC мною был принят за аксиому тот факт, что класс приложения должен быть унаследован от класса CWinApp, вклю­ченного в MFC. Что ж, напишем простейшую программу для MFC:

#include <afxwin.h>

class CEmpty : public CWinApp {

};

CEmpty theApp;



Вот с этих-то строк все и начинается. Командная строка запи­сывается в переменную IpszCommandLine, анализируется, затем готовится структура типа STARTUPINFO, после чего вызывается функция WinMain(). Обратим внимание, с какими аргументами за­пускается эта функция.

Первым аргументом этой функции должен быть хэндл текущей копии приложения. Вместо хэндла текущей копии приложения ука­зывается хэндл файла, создавшего процесс. Отметим этот момент -хэндл текущей копии приложения представляет собой ни что иное, как хэндл файла, создавшего процесс. Второй аргумент – хэндл предыдущей копии приложения – естественно, равен NULL, так как Win32 для каждого процесса выделяет свое собственное адрес­ное пространство. Третий аргумент-указатель на командную стро­ку – он только что был сформирован. И наконец, четвертый аргу­мент-флаги, определяющие, каким образом необходимо отобра­жать окно.

Функция WinMain() осуществляет вызов другой функции, JWinMain(), исходный код которой находится в файле appmodul.cpp:

extern "С" int WIUAPI

_tWinMain(HINSTANCE hlnstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)

{

// call shared/exported WinMain return AfxWinMain(hlnstance,

hPrevInstance,

lpCmdLine,

nCmdShow) ;

}

А эта функция, в свою очередь, приведет нас к функции AfxWinMain(), чей исходный код находится в файле winmain.cpp:

int AFXAPI AfxWinMain(HINSTANCE hlnstance,

HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)

{

ASSERT(hPrevInstance == NULL); int nReturnCode = -1;

CWinThread* pThread = AfxGetThread(); CWinApp* pApp = AfxGetAppO;

// AFX internal initialization if (!AfxWinInit(hlnstance, hPrevInstance, lpCmdLine, nCmdShow)) goto InitFailure;

// App global initializations (rare) if (pApp != NULL && !pApp->InitApplication() ) goto InitFailure;

// Perform specific initializations

if (!pThread->InitInstance())

{

if (pThread->m_pMainWnd != NULL) {

TRACEO("Warning: Destroying non-NULL m_pMainWnd\n") ; pThread->m_pMainWnd->DestroyWindow();

}

nReturnCode = pThread->ExitInstance(); goto InitFailure;

}

nReturnCode = pThread->Run();

InitFailure:

#ifdef _DEBUG

// Check for missing AfxLockTempMap calls

if (AfxGetModuleThreadState()->m_nTempMapLock != 0)

{

TRACE1("Warning: Temp map lock count non-zero (%ld).\n", AfxGetModuleThreadState()->m_nTempMapLock) ;

}

AfxLockTempMaps(); AfxUnlockTempMaps(-1); #endif

AfxWinTerm(); return nReturnCode;



Фактически внутри этой функции и производятся все действия, необходимые для того, чтобы программа пользователя смогла нор­мально отработать. Из того, что может нас в настоящий момент заинтересовать, заинтересовать, здесь присутствует только одна деталь. Для нас важно, что в процессе при помощи функции AfxGetApp() выбирается указатель на объект класса CWinApp, по­сле чего вызывается функция, производящая инициализацию AFX (application frameworks), – AfxWinlnit().

Ее исходный код находится в файле appinit.cpp:

BOOL AFXAPI AfxWinlnit(HINSTANCE hlnstance,

HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)

{

ASSERT(hPrevInstance == NULL);

// handle critical errors and avoid Windows message boxes SetErrorMode(SetErrorMode(0) |

SEM_FAILCRITICALERRORS|SEM_NOOPENFILEERRORBOX);

// set resource handles

AFX_MODULE_STATE* pModuleState = AfxGetModuleState(); pModuleState->m_hCurrentInstanceHandle = hlnstance; pModuleState->m_hCurrentResourceHandle = hlnstance;

// fill in the initial state for the application CWinApp* pApp = AfxGetAppO; if (pApp != NULL) {

// Windows specific initialization (not done if no CWinApp) pApp->m_hInstance = hlnstance; pApp->m_hPrevInstance = hPrevInstance; pApp->m_lpCmdLine = lpCmdLine; pApp->m_nCmdShow = nCmdShow;

pApp->SetCurrentHandles();

}

// initialize thread specific data (for main thread) if (!afxContextlsDLL) AfxInitThread()/

return TRUE;

}



Эта функция в настоящее время является устаревшей. В Windows более ранних версий, скажем, Windows 3.1, можно было запустить несколько копий о/уного и того же приложения. При этом весьма высокой была вероятность того, что все они будут использовать одни и те же данные, подготовленные при запуске первой копии приложения. Метод lnitApplication() использовался именно для под­готовки ОБЩИХ для всех копий данных при запуске первой копии приложения. В настоящее время этот метод используется для ини­циализации менеджера документов, который используется при ра­боте с архитектурой «Document/view (документ/представление)». Об этой архитектуре речь пойдет в соответствующей части книги.

Следом за методом lnitApplication() вызывается метод lnitlnstance(). Предназначение этого метода как раз и заключается в том, чтобы произвести инициализацию приложения. Исходный код этого метода находится в файле аррсоге.срр:

BOOL CWinApp::InitInstance() {

return TRUE;

}

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

Сразу после завершения работы метода lnitlnstance() вызыва­ется метод Run(). Его исходный код находится в файле аррсоге.срр:

int CWinApp::Run()

{

if (m_pMainWnd == NULL && AfxOleGetUserCtrl()) {

// Not launched /Embedding or /Automation, // but has no main window!

TRACEO("Warning: m_pMainWnd is NULL in CWinApp::Run -

quitting application.\n");

AfxPostQuitMessage(0);

}

return CWinThread::Run();

}



03.02.2010

Внимательно взглянув на текст этого метода, можно заметить, что в том случае, когда поле m_pMainWnd равно нулю и приложение было загружено системой, а не OLE, работа метода немедленно завершается. При этом в отладочное окно выдается сообщение «Warning: m_pMainWnd is NULL in CWinApp::Run – quitting applica­tion.» (Предупреждение: m_pMainWnd равно NULL в методе CWinApp::Run – приложение завершается). Возникает вопрос: что же мы сделали не так, и почему возникла ошибка? Заглянув в спи­сок полей нашего объекта-приложения, мы увидим, что поле m_pMainWnd приложение наследует от класса CWinThread. В описа­нии класса CWinThread можно найти следующую запись:

CWnd* m_pMainWnd; // main window

//(usually same AfxGetApp()->m_pMainWnd)

Другими словами, поле m_pMainWnd должно хранить указатель на объект класса CWnd, который должен быть ассоциирован с ок­ном, являющимся ГЛАВНЫМ окном приложения. Но где должен быть создан этот объект? У нас есть две возможности – метод Ini-tApplicationQ и метод lnitlnstance(). Так как метод InitApplicationQ используется MFC для других целей (инициализация менеджера документов), то, чтобы не переписывать метод lnitApplication(), луч­ше всего будет создать объект класса CWnd в переопределенном методе lnitlnstance(). Отсюда делаем еще один вывод: объект клас­са CWnd, который будет ассоциирован с главным окном прило­жения, целесообразно создавать в переопределенном методе InitlnstanceQ приложения.

А теперь давайте еще немного порассуждаем. Если наши рассуждения верны, то объект класса CWnd будет создан во вре­мя отработки метода InitlnstanceQ приложения. Но объект приложения будет создан ранее, во время отработки startup-кода! Инициализацией полей класса, в том числе и поля m_pWinMain, должен, естественно, заниматься конструктор класса. Но кон­структор класса CWinThread отрабатывает раньше конструктора класса CWinApp и, тем более, раньше метода lnitlnstance() при­ложения. Следовательно, поле m_pMainWnd после создания объекта приложения остается неинициализированным или со­держит неверную информацию. Значит, после того, как в ме­тоде InitlnstanceQ будет создано главное окно приложения, полю m_pMainWnd необходимо присвоить значение указателя на ассоциированный с окном объект. В противном случае про­грамма, увы, работать не будет!

Что ж, попробуем немного изменить нашу программу и доба­вить в нее переопределенный метод lnitlnstance(), в котором будет создан объект класса CWnd. Текст измененной программы:

#include <afxwin.h>

class CExampleApp : public CWinApp {

BOOL Initlnstance() ;

};

class CMainWnd : public CWnd {

};

BOOL CExampleApp::Initlnstance() {

CMainWnd* pMainWnd = new CMainWnd; m_pMainWnd = pMainWnd; return TRUE;

}

CExampleApp theApp;



Запускаем программу на выполнение… Что такое? Программа «зависла»… Да нет, программа не «зависла». Вспомните, уважае­мый читатель, что после метода lnitlnstance() немедленно вызыва­ется метод CWinApp::Run(). Именно в нем программа и «циклит»! Придется опять искать причину неправильной работы программы. Что же может случиться? Мы создали объект класса CWnd, запи­сали указатель на него в поле m_pMainWnd, чего же еще? Взгля­нув на исходный код метода CWinApp::Run(), можно с уверенно­стью сказать, что причину «зацикливания» нужно искать в методе CWinThread::Run(). Его исходный текст находится в файле thrdcore.cpp:

int CWinThread::Run() {

ASSERT_VALID(this);

// for tracking the idle time state BOOL bldle = TRUE; LONG HdleCount = 0;

// acquire and dispatch messages until a WM_QUIT message is received, for (;;) {

// phasel: check to see if we can do idle work while (bldle &&

! : :P eekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE)) {

// call Onldle while in bldle state

if (!0nldle (HdleCount++) )

bldle = FALSE; // assume "no idle" state

}

// phase2: pump messages while available

do

{

// pump message, but quit on WM_QUIT if (!PumpMessage())

return Exitlnstance ();

// reset "no idle" state after pumping "normal" message if (IsIdleMessage(&m_msgCur)) {

bldle = TRUE; HdleCount = 0;

}

} while (::PeekMessage(&m_msgCur,

NULL, NULL, NULL,

PM_NOREMOVE));

}

ASSERT(FALSE); // not reachable

}



Функции в качестве аргументов передаются стили окон регистри­руемого класса, хэндлы курсора, фона и иконки. В отличие от преды­дущей эта функция сама формирует имя регистрируемого класса. В том случае, если хэндлы иконки, курсора и фона равны нулю, то имя класса формируется в соответствии с вызовом функции

wsprintf(IpszName,

_T("Afx:%x:%x") , (UINT)hlnst, nClassStyle);

Примером может служить имя Afx:400000:0. Первое число в име­ни класса обозначает хэндл текущей копии приложения, второе число – стиль окон регистрируемого класса.

Если не все параметры, переданные функции, равны NULL, то формируется полное имя класса в соответствии с функцией

wsprintf(IpszName,

_Т("Afх:%х:%х:%х:%х:%х"),

(UINT)hlnst, nClassStyle,

(UINT)hCursor,

(UINT)hbrBackground,

(UINT)hlcon);

Примером такого имени может служить Afx:400000:8:14c6:0:4687. При этом первое число в имени класса означает хэндл текущей копии приложения, вггорое число – стиль окон регистрируемого класса, третье число означает хэндл курсора, четвертое – хэндл фона, и, наконец, пятое – хэндл иконки окна.

После формирования имени класса функция производит запол­нение структуры типа WNDCLASS и регистрирует класс окон, при­чем в качестве имени класса используется только что сформиро­ванное функцией имя.

Зарегистрировав класс окна, можно создавать окно. А создав окно, нужно не забыть его отобразить, верно? Таким образом, мы

«вычислили» те несколько шагов, которые ОБЯЗАТЕЛЬНО нужно вы­полнить при написании программы с использованием MFC:

1. Описываем класс приложения, производный от класса CWinApp.

2. Описываем класс, к которому будет принадлежать главное окно приложения (обычно CFrameWnd или CMDIFrameWnd).

3. Переопределяем метод lnitlnstance() класса приложения.

4. В методе lnitlnstance() создаем новый объект класса, к которо­му будет принадлежать главное окно приложения.

5. Указатель на только что созданный объект записываем в поле m__pMainWnd.

6. Регистрируем класс окон, к которому будет принадлежать глав­ное окно приложения.

7. В методе lnitlnstance() создаем непосредственно окно только что зарегистрированного класса, которое будет являться главным окном приложения.

8. Создаем объект класса приложения.

Исходный текст демонстрационной программы, написанной в соответствии с определенной нами последовательностью шагов, приведен ниже:

#include <afxwin.h>



03.02.2010

// Шаг 1. Описываем класс приложения, производный // от класса CWinApp

// Объявляем класс приложения, его поля и методы.

class CExampleApp : public CWinApp

{

BOOL Initlnstance ();

};



03.02.2010

Завершив рассмотрение метода PumpMessage(), мы тем самым завершили рассмотрение и метода Run(). Из всего сказанного мож­но сделать вывод о том, что метод Run(), включая метод Pump-Message(), реализует основной цикл обработки сообщений, опре­деляя при этом периоды «простоя» (в очереди нет сообщений) и осуществляя во время простоя вызов метода Onldle(). Тем са­мым метод позволяет осуществлять выполнение дополнительных задач в период «простоя».

Исходя из того, о чем мы говорили выше, можно сделать еще один вывод: в совокупности метод InitlnstanceQ и метод RunQ являются аналогами функции WinMainQ, а совокупность мак­росов-обработчиков событий представляет собой аналог оконной функции. Теперь наших знаний достаточно для того, что­бы создать объект-приложение, главное окно приложения и на­писать обработчики сообщений. Для примера я написал програм­му, которая выдает в центр окна слова «НеИо, World!» (надо же было когда-нибудь написать «НеИо, World!», не так ли? ©). Ис­ходный текст этой программы:

#include <afxwin.h>

// Объявляем класс приложения, его поля и методы.

class CTheFirstApp : public CWinApp

{

public:

virtual BOOL Initlnstance (); };

// Объявляем класс окна, его поля и методы.

class CMainWnd : public CWnd

{

public: -CMainWnd() ;

afx_msg void OnPaintO; DE С LARE_ME S SAGE_MA P()

};

// Деструктор класса, должен быть переопределен, // т.к. у класса CWnd деструктор виртуальный

CMainWnd::-CMainWnd()

{

}

// Инициализация класса приложения. BOOL CTheFirstApp::Initlnstance () {

CString pszWndClassName;

pszWndClassName = AfxRegisterWndClass( CS_HREDRAW |

CS_VREDRAW, LoadStandardCursor ( IDC_ARROW ), ( HBRUSH ) ::GetStockObject( WHITE_BRUSH ), LoadStandardIcon( IDI_APPLICATION ) ); CMainWnd* pMainWnd = new CMainWnd; pMainWnd->CreateEx( 0, pszWndClassName,

_T ( "Sample" ) , WS_OVERLAPPEDWINDOW, CRect (0, 0, 200, 200), NULL, NULL ); m__pMainWnd = pMainWnd; pMainWnd->ShowWindow (m__nCmdShow ) ; pMainWnd->UpdateWindow();

return TRUE;

}

// Перерисовка окна прило-// жения – обработка // сообщения WM PAINT.

CMainWnd::OnPaint()

void {

CRect Rect; CPaintDC dc( this ); GetClientRect( &Rect ); dc.DrawText( _T( "Hello,

World!" ), &Rect, DT_SINGLELINE | DT_CENTER I DT_VCENTER );

}

// Карта сообщений BEGIN_MESSAGE_MAP( CMainWnd, CWnd )

Hello, World!

ON_WM_PAINT () EN D_ME S SAGE_MAP()

CTheFirstApp theApp;

Создаваемое программой окно приведено на рис.3.

Обратите внимание, что при объявлении объекта класса CWnd был использован макрос DECLARE__MESSAGE__MAP(), а при реализации класса – макросы BEGIN__MESSAGE_MAP() и END__MESSAGE__MAP(). В связи с тем, что мне необходимо было перехватить обработку ТОЛЬКО сообщения WM_PAINT, карта сообщений состоит из одного элемента, который я определил, воспользовавшись для этого макросом ON_PAINT().



Как нетрудно заметить, макрос DECLARE_DYNCREATE() выпол­няет макрос DECLARE_DYNAMIC()f а затем добавляет к описа­нию класса метод CreateObjectQ. В реализации класса необходи­мо будет использовать макрос IMPLEMENTJDYNCREATE(), исход­ный текст которого находится в файле afx.h:

#define IMPLEMENT_DYNCREATE (class_name, base_class_name) \ CObject* PASCAL class_name::CreateObject() \

{ return new class_name; } \ IMPLEMENT_RUNTIMECLASS(class_name,

base_class_name, OxFFFF, \ class_name::CreateObject)

Очевидно, что помимо того же, что делает макрос DECLARE-_DYNAMIC(), макрос DECLARE_DYNCREATE() формирует исход­ный текст метода CreateObjectQ.

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

#include <afxwin.h>

II Объявляем класс приложения, его поля и методы.

class CRuntimelnfоАрр : public CWinApp

{

DECLARE_DYNCREATE( CRuntimelnfоАрр ); public:

CRuntimelnfoApp();

-CRuntimelnfoApp(); virtual BOOL Initlnstance ();

};

IMPLEMENT_DYNCREATE( CRuntimelnfoApp, CWinApp )

// Конструктор класса приложения. CRuntimelnfoApp::CRuntimelnfoApp() { }

// Деструктор класса приложения. CRuntimelnfoApp::-CRuntimelnfoApp() { }

// Инициализация класса приложения. BOOL CRuntimelnfoApp::Initlnstance() {

for ( CRuntimeClass* pRuntimelnfo=RUNTIME_CLASS (CRuntimelnfoApp ) ; pRuntimelnfo != NULL; pRuntimelnfo = (*pRuntimeInfo->m_pfnGetBaseClass)

0 ) {

TRACE( "Class name – %s\n

object size = %d\n schema = %08x\n

.pointer to CreateObject() = %08x\n pointer ro _GetBaseClass = %08x\n pointer to the next class = %08x\n",

pRuntimelnfo->m_lpszClassName,

pRuntimeInfo->m_nObjectSize,

pRuntimelnfo->m_wSenema,

pRuntimelnfo->m_pfnCreateObject,

pRuntimeInfo->m_pfnGetBaseClass,

pRuntimelnfo->m_pNextClass);

}

return TRUE;

}

CRuntimelnfoApp theApp;



Программа заводит объект определенного класса, а затем пе­ребирает информацию времени выполнения классов-предков до тех пор, пока не дойдет до класса, у которого нет предков. Всю информацию времени выполнения о себе и q родительских клас­сах программа выдает в окно отладки. Результат работы этой про­граммы:

Class name – CRuntimelnfoApp object size = 200 schema = OOOOffff

pointer to CreateObject() = 00401005 pointer ro _GetBaseClass = 0040101e pointer to the next class = 00000000 Class name – CWinApp object size = 200 schema = OOOOffff

pointer to CreateObject() = 00000000 pointer ro _GetBaseClass = 5f497b92 pointer to the next class = 00000000 Class name – CWinThread object size = 112 schema = OOOOffff

pointer to CreateObject() = 00000000 pointer ro _GetBaseClass = 5f496e42 pointer to the next class = 00000000 Class name – CCmdTarget object size = 32 schema = OOOOffff

pointer to CreateObject() = 00000000 pointer ro _GetBaseClass = 5f495811 pointer to the next class = 00000000 Class name – CObject object size = 4 schema = OOOOffff

pointer to CreateObject() = 00000000 pointer ro _GetBaseClass = 5f4268b0 pointer to the next class = 00000000

Warning: m_j?MainWnd is NULL in CWinApp: :Run – quitting application.

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

Кроме того, можно заметить, что только класс CRuntimelnfoApp поддерживает динамическое создание объектов [указатель на Сге-ateObject() не равен нулю], потому что при его определении я ис­пользовал макросы DECLARE_DYNCREATE() и IMPLEMENT-_DYNCREATE(). Все остальные классы не позволяют динамиче­ски создавать объекты. Естественно, это только учебный пример, однако, он демонстрирует, каким образом можно извлечь массу пользы из информации времени выполнения.



В случае обработки исключения средствами С++ или MFC мы должны использовать конструкцию

try

{

throw …; //в случае формирования

// программного исключения

}

catch(<описание исключения>)

{

}

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

#include <afxwin.h>

// Объявляем класс приложения, его поля и методы.

class CTheFirstApp : public CWinApp

{

public:

virtual BOOL Initlnstance();

};

// Инициализация класса приложения. BOOL CTheFirstApp::Initlnstance()

try

{ // <- Здесь поставить точку прерывания

throw 1;

}

catch( … )

return TRUE;

}

return TRUE;

}

CTheFirstApp theApp;

Поставим точку прерывания на фигурной скобке ПЕРЕД стро­кой, в которой находится слово «throw». После запуска программы на выполнение и останова на точке прерывания, перейдем в ре­жим дизассемблера и посмотрим, что делается в программе