

Программирование на языке MFC
Мой второй блог в серии программирования
Что мы можем здесь увидеть? То, что в описании этого класса присутствует поле rnJTemplateList класса CPtrList, только подтверждает нашу догадку о том, что de facto объекты класса CDocManager являются списками указателей на какие-то шаблоны. Для того чтобы убедиться, что наша догадка верна, достаточно взглянуть на исходный код методов 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. Наверное, именно этим методом и следует воспользоваться при добавлении шаблона документа!
читать отзывы (0)
Наверное, из описания класса 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 совершенно бесполезно. И в описании членов класса CWinApp мы также не найдем и упоминания об 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
просто и быть не может. По мановению волшебной палочки стандартные диалоговые окна не создаются.
Здесь мы увидим кое-что интересное. Так… MFC пытается загрузить меню и таблицу акселераторов… Но их идентификатор совпадает с идентификатором строки, которую пытается загрузить конструктор базового класса! Отсюда следующие выводы.
Вывод первый. В состав ресурсов, идентификатор которых используется при создании шаблона документа, могут входить строка, меню и таблица акселераторов. Вывод второй (обратный первому). Идентификаторы строки, меню и таблицы акселераторов, которые будут использоваться при работе программы, написанной в соответствии с архитектурой «документ/представление»,должны быть ОДИНАКОВЫМИ!
Таким образом, мы в первом приближении поняли, какого рода ресурсы должны содержаться в нашем приложении. Пока нам не совсем ясно, где используются эти ресурсы. Но, надеюсь, в дальнейшем мы сможем вычислить, где используются эти ресурсы и, соответственно, создать их.
Метод CWinApp::AddDocTemplate(CDocTemplate* pTemplate) ничего интересного для нас в себе не содержит. В нем происходит инициализация списка шаблонов и добавление шаблона в этот список. Мне кажется, что более подробное рассмотрение этого вопроса ничего существенного к пониманию нами процесса создания шаблона не добавит.

Ну, кажется, «охота на ресурсы» завершена успешно. Мы выяснили, какие ресурсы могут входить в состав приложения. Обратите, пожалуйста, внимание на то, что мы НИ РАЗУ не заглянули в документацию, предлагаемую MSDN, или в какие-нибудь другие пособия по MFC. Исходный код MFC позволил нам самостоятельно сделать выводы, приведенные выше.
Теперь, можно немного отдохнуть, выпить чашку чая или кофе, а потом продолжить рассмотрение. В дальнейшем мы остановимся на процессах, происходящих при создании документа.
Взглянув на этот текст, мы можем сделать вывод, который в значительной степени облегчит нам жизнь.
В приложении 1У|ожно не создавать явным образом объект класса CDocManager. Если объект класса CDocManager не был создан вручную до момента добавления первого шаблона, то в момент добавления первого шаблона документа этот объект будет создан автоматически.
Давайте вспомним также, что при вызове метода CWinApp::Add-DocTemplate(CDocTemplate*pTemplate) фактически вызывается метод объекта класса CDocManager:: AddDocTemplate( CDocTemplate* pTemplate). Из этого факта можно сделать еще один вывод.
Для того, чтобы передать нашему приложению информацию о том, с какими типами документов ему придется работать, нам необходимо до создания (или открытия) документа добавить в список шаблонов шаблон нашего документа. Всю остальную работу MFC, вероятно, сделает самостоятельно!
Зная все это, можно более подробно описдть причину, которая вызвала появление сообщения об ошибке. Ошибка, из-за которой мы полезли в глубины MFC, заключается в том, что мы не добавили в приложение ни одного шаблона документа, из-за чего не был проинициализирован m_pDocManager и мы получили соответствующее сообщение. Ура! Причину ошибки мы определили! Но ведь нам нужно не только определить причину ошибки, но и устранить ее! Другими словами, нам необходимо создать шаблон документа, а потом добавить его в список шаблонов.
Здесь мне представляется необходимым еще раз привести исходный код метода CWinApp::AddDocTemplate():
void CWinApp::AddDocTemplate(CDocTemplate* pTemplate) {
if (m_pDocManager == NULL)
m_pDocManager = new CDocManager; m_pDocManager->AddDocTemplate(pTemplate);
}
Строчку, которая нас интересует, мы найдем в файле 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();
}
Внимательно взглянув на текст этого метода, можно заметить, что в том случае, когда поле m_pMainWnd равно нулю и приложение было загружено системой, а не OLE, работа метода немедленно завершается. При этом в отладочное окно выдается сообщение «Warning: m_pMainWnd is NULL in CWinApp::Run – quitting application.» (Предупреждение: 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 &&
! :
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>
// Шаг 1. Описываем класс приложения, производный // от класса CWinApp
// Объявляем класс приложения, его поля и методы.
class CExampleApp : public CWinApp
{
BOOL Initlnstance ();
};
Завершив рассмотрение метода 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». После запуска программы на выполнение и останова на точке прерывания, перейдем в режим дизассемблера и посмотрим, что делается в программе
