Программирование на языке 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…



Итак, мы создали заготовку шаблона документа. Но до сих пор нам не ясно, что происходит непосредственно после того, как мы выбе­рем элемент «Ореп» меню «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 позволил нам самостоятель­но сделать выводы, приведенные выше.

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



Теперь можно вернуться к CMultiDocTemplate::CMultiDocTemplate(). Обратите, пожалуйста, внимание, уважаемый читатель на второй вызов LoadTemplate():

void CMultiDocTemplate::LoadTemplate() {

CDocTemplate::LoadTemplate();

if (m_nIDResource != 0 && m_hMenuShared == NULL) {

HINSTANCE hlnst = AfxFindResourceHandle(

MAKEINTRESOURCE(m_nIDResource), RT_MENU); m_hMenuShared = ::LoadMenu(hlnst,

MAKEINTRESOURCE(m_nIDResource)); m_hAccelTable = ::LoadAccelerators(hlnst,

MAKEINTRESOURCE(m_nIDResource));

}



03.02.2010

Что здесь может быть важным для дальнейшего понимания предупреждает нас о том, что указатели на информацию времени выполнения не должны быть равными NULL. Во-вторых, класс документа должен быть унаследован от CDocument, класс фрейма – от CFrameWnd и, наконец, класс представления – от CView. В-третьих, поля m__nlDResource, m_pDocClass, m_pFrameClass и mjDViewClass инициализируются теми значениями, которые мы указали при вызове конструктора. Здесь нет ничего странного, и это мы могли предполагать. Больший интерес вызывает обращение к функции LoadTemplate():

void CDocTemplate::LoadTemplate() {

if (m_strDocStrings.IsEmpty() &&

!m_strDocStrings.LoadString(m_nIDResource) )

{

TRACE1 ("Warning: no document names irv. string for

template #%d.\n", m_nIDResource) ;

}

if (m_nIDEmbeddingResource != 0 && m_hMenuEmbedding == NULL)

{

// load menu to be used while editing an embedding // (as a server)

HINSTANCE hlnst = AfxFindResourceHandle(

MAKEINTRESOURCE(m_nIDEmbeddingResource) ,

RT_MENU);

m_hMenuEmbedding = ::LoadMenu(hlnst,

MAKEINTRESOURCE(m_nIDEmbeddingResource) ) ; m_hAccelEmbedding = ::LoadAccelerators(hlnst,

MAKEINTRESOURCE(m_nIDEmbeddingResource) ) ;

}

if (m_nIDServerResource != 0 && m_hMenuInPlaceServer == NULL)

{

// load menu to be used while editing in-place // (as a server)

HINSTANCE hlnst = AfxFindResourceHandle(

MAKEINTRESOURCE(m_nIDServerResource) , RT_MENU);

m_hMenuInPlaceServer = ::LoadMenu(hlnst,

MAKEINTRESOURCE(m_nIDServerResource) ) ;

m_hAccelInPlaceServer = ::LoadAccelerators(hlnst,

MAKEINTRESOURCE(m_nIDServerResource) ) ;

}

if (m_nIDContainerResource != 0 && m_hMenuInPlace == NULL) {

// load menu to be used while in-place editing // session (as a container)

HINSTANCE hlnst = AfxFindResourceHandle(

MAKEINTRESOURCE(m_nIDContainerResource), RT_MENU);

m_hMenuInPlace = ::LoadMenu(hlnst,

MAKEINTRESOURCE(m_nIDContainerResource)); m_hAccelInPlace = ::LoadAccelerators(hlnst,

MAKEINTRESOURCE(m nIDContainerResource));

(CMultiDocTemplate::CMultiDocTemplate)

4CMultiDocTemplate::LoadTemplate ]

Взглянем повнимательнее на текст этого метода. Во-перВых, мы заметим, что в поле m_strDocStrings загружается содержимое СТРОКИ, зафужаемой из ресурсов. И, что очень важно, идентификатор этой строки равен тому, который мы указали при вызове конструктора. Если строка по каким-то причинам в ресурсах не найдена, то нам будет выдано отладочное сообщение «Warning: no document names in string for template…» Сле­довательно, MFC подсказывает нам, что в строковом ресурсе долж­ны быть какие-то имена документов. Запомним это. Забегая вперед, скажу, что нам этот факт еще пригодится.



fcMultiDocTemplate::CMultiDocTemplatej Первым, естественно, был вызван сам конструктор. Его исходный текст находится в файле docmulti.cpp:

CMultiDocTemplate::CMultiDocTemplate(UINT nIDResource,

CRuntimeClass* pDocClass, CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass) : CDocTemplate(nIDResource, pDocClass, pFrameClass, pViewClags)

{

ASSERT(m_docList.IsEmpty()); m_hMenuShared = NULL; m_hAccelTable = NULL;

m_nUntitledCount = 0; // start at 1

// load resources in constructor if not // statically allocated if (ICDocManager::bStaticInit) LoadTemplate();

(CMultiDocTemplate-CMultiDocTemplateJ Очевидно, что аргументы,
I г~ ~ ^ с которыми производится

L-{CDocTemplate::CDocTemplate ] вызов конструктора CMultl.

DocTemplate(), передаются конструктору родительского класса CDocTemplate, чей исходный код находится в файле doctempl.cpp:

CDocTemplate::CDocTemplate(UINT nIDResource,

CRuntimeClass* pDocClass, CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass)

{

ASSERT_VALID_IDR(nIDResource); ASSERT(pDocClass == NULL ||

pDocClass-IsDerivedFrom(RUNTIME_CLASS(CDocument) ) ) ; ASSERT(pFrameClass == NULL ||

pFrameClass->IsDerivedFrom(RUNTIME_CLASS(CFrameWnd) ) ) ASSERT(pViewClass == NULL ||

pViewClass->IsDerivedFrom(RUNTIME_CLASS(CView)) ) ;

m_nIDResource = nIDResource; m_nIDServerResource = NULL; m_nIDEmbeddingResource = NULL; m nIDContainerResource = NULL;

m_pDocClass = pDocClass; m_pFrameClass = pFrameClass; m_pViewClass = pViewClass; m_p01eFrameClass = NULL; m_p01eViewClass = NULL;

m_pAttachedFactory = NULL; m_hMenuInPlace = NULL;

m_hAccelInPlace = NULL; m_hMenuEmbedding = NULL; m_hAccelEmbedding = NULL; m_hMenuInPlaceServer = NULL; m_hAccelInPlaceServer = NULL;

// add to pStaticList if constructed as static

// instead of on heap

if (CDocManager:rbStaticInit)

{

m_bAutoDelete = FALSE;

if (CDocManager::pStaticList == NULL)

CDocManager:rpStaticList = new CPtrList; if (CDocManager::pStaticDocManager == NULL)

CDocManager::pStaticDocManager = new CDocManager; CDocManager::pStaticList->AddTail(this);

else

{

m_bAutoDelete = TRUE; // usually allocated on the heap LoadTemplate();



Я постарался откомпилировать эту программу, но получил массу диагностических сообщений, из которых следовало, что класс CDocTemplate является абстрактным и для того, чтобы на­следовать от него другие классы, мне надо переопределить массу методов. Меня это не устраивало. Во-первых, я только изучаю возможности MFC, следовательно, у меня нет желания пере­писывать половину методов. Во-вторых, a priori я уверен, что разработчики MFC довели работу до логического завершения и предоставили программисту классы, унаследованные от CDocTemplate, готовые к немедленному использованию. По­копавшись в исходных текстах MFC, я обнаружил, что от CDocTemplate наследуются два класса – CSingleDocTemplate и CMultiDocTemplate. Судя по привеленным комментариям, пер­вый из этих классов предназначен для работы с однодокументным интерфейсом, а второй – для работы с MDI. Я подумал и ре­шил, что моя программа должна поддерживать многодокумент­ный интерфейс, поэтому в своей программе вместо абстрактного класса CDocTemplate я использовал производный от него класс CMultiDocTemplate. Мой расчет на то, что это не должно оказать особого влияния на понимание архитектуры «документ/пред­ставление», оказался верен. Еще одной особенностью этой программы было то, что я понятия не имел о том, идентификатор каких ресурсов я должен указывать в качестве первого аргумента конструктора и надеялся выяснить это при помощи MFC. Поэтому идентификатор ресурсов я сделал равным нулю.

После внесенных изменений я получил следующий код:

BOOL CDocViewlApp::Initlnstance() {

#ifdef _AFXDLL

Enable3dControls() ; #else

Enable3dControlsStatic(); #endif

LoadStdProfileSettings();

CMultiDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate( 0,

NULL, NULL, NULL );

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

return TRUE;

Программа откомпилировалась нормально, но, запустив про­грамму на выполнение, я немедленно получил сообщение об ошиб­ке, представленное на 11.

Заглянув в место возникновения ошибки, я узнал, что сообще­ние выдано макросом ASSERT_VALID_IDR, который находится в файле afxpriv.h:

#define ASSERT_VALID_IDR(nIDR) ASSERT((nIDR) != 0 &&

(nIDR) < 0×8000)

Значит, идентификатор ресурсов не может быть нулем или более 0×7fff. Попутно замечу, что в том же файле указано, что номера ресурсов от 0 до 0×6fff могут использоваться програм­мистами, а номера от 0×7000 до 0×7fff зарезервированы для ре­сурсов MFC и стандартных ресурсов Windows. Так… Это не бог весть что, но уже какая-то отправная информация для нас есть. Изменим значение нашего идентификатора ресурсов, скажем, на 0×6fff.. Замечательно! Программа откомпилировалась без ошибок. Я поставил точку прерывания на конструктор шаблона документа и стал смотреть, что происходит во время создания шаблона.



После приведенных изменений метод CDocView1::lnitlnstance() имел следующее содержание:

BOOL CDocViewlApp::Initlnstance() {

#ifdef _AFXDLL

Enable3dControls (); #else

Enable3dControlsStatic(); #endif

LoadStdProfileSettings(); CDocTemplate* pDocTemplate; pDocTemplate = new CDocTemplate( 0,

NULL ),

NULL,

NULL );

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

return TRUE;

}



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



Вспомним, что класс CDocManager является хранилищем указа­телей на шаблоны документов. С другой стороны, метод AddDocTem-plate() в качестве аргумента использует указатель на объект класса CDocTemplate. Следовательно, в этот момент в игру вступает объект класса CDocTemplate. Как следует из названия, он представляет со­бой именно шаблон документа. Подробное описание этого класса мы приведем позже, а сейчас заметим только, что конструктор этого класса имеет следующий вид:

protected:

CDocTemplate(UINT nIDResource,

CRuntimeClass* pDocClass, CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass);

Так… Для инициализации шаблона документа нам нужна ин­формация времени исполнения о каких-то трех составляющих. Судя по названиям этих составляющих, речь в данном случае идет о до­кументе, окне фрейма и окне представления. Значит, этот так на­зываемый шаблон документа – это не просто шаблон, а это не­кий контейнер, который соединяет в себе воедино информацию о трех важнейших составляющих архитектуры «документ/представ­ление». Тем более важно понять, как он создается, какие данные он использует, и как эти данные взаимодействуют друг с другом.



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 является списком, не так ли?



Однако что же будет записано в таблицу в том случае, если мы при помощи метода WriteObject() записываем в архив несколько объектов одного и того же класса? Неужели каждый раз нам при­дется записывать признак класса, схему, количество символов в названии, непосредственно символы? На сколько же это увели­чит размер архива! Я думаю, что читатель уже сам догадался, что дело обстоит несколько по-другому. Сделано это, на мой взгляд, дос­таточно изящно.

Пример

В таком случае в архив записываются либо слово, либо слово и двойное слово, содержащие своеобразные ссылки на индекс ранее записанной в хэш-таблицу информации о классе. Слово за­писывается тогда, когда индекс класса не превышает значения wBigObjectTag, описанного в файле arcobj.cpp следующим образом:

#define wBigObjectTag ((WORD)0×7FFF)

// 0×7FFF indicates DWORD object tag



После записи в архив информации о классе необходимо убедиться, что сохраненное в архиве число объектов не превышает максимально допустимого значения, которое в файле метод WriteClass() должен убе­диться, проверяет, не превышено ли максимальное допустимое число сохраненных классов. Это максимально допустимое число определено в файле arcobj.cpp следующим образом:

#define nMaxMapCount ((DWORD)0×3FFFFFFE)

// 0×3FFFFFFE last valid mapCount

Проверка производится при помощи метода CheckCount():

void CArchive::CheckCount() {

if (m_nMapCount >= nMaxMapCount)

AfxThrowArchiveException(CArchiveException::badlndex,

m_strFileName);

}

Честно говоря, у меня есть БОООООЛЬШИИИИИЕ сомнения, что когда-нибудь этот метод сформирует исключение. Сохранять более миллиарда объектов, это, знаете ли, достаточно серьезно! ©

Но вернемся к методу WriteClass(). После проверки числа объ­ектов в хэш-таблице происходит то, что объясняет, почему из хэш-таблицы выбирается какой-то ИНДЕКС, а не указатель. В хэш-таб­лицу с ключом, равным указателю на информацию времени вы­полнения, записывается порядковый номер добавляемого элемен­та! Так как нулевой элемент в хэш-таблицу записывается при ее создании и инициализации в методе MapObjectQ, то порядковые номера элементов будут начинаться с единицы, а не с нуля. Итак, если мы будем записывать в архив объекты разных классов, то получим хэш-таблицу, при этом элементами хэш-таблицы в грубом приближении будут п{ары «указатель на элемент – порядковый но­мер, т.е. ИНДЕКС элемента». Думаю, теперь читатель понял о ка­ком индексе идет речь.



После этого вызывается метод CRuntimeClass::Store():

void CRuntimeClass::Store(CArchive& ar) const // stores a runtime class description

{

WORD nLen = (WORD)IstrlenA(m_lpszClassName);

ar « (WORD)m_wSchema « nLen;

ar.Write(m_lpszClassName, nLen*sizeof(char));

}

Из текста метода видно, что он «сбрасывает» в архив номер схемы, длину имени класса и непосредственно имя класса сохра­няемого в архиве объекта, т. е.

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



Методу в качестве аргумента передается указатель на инфор­мацию времени исполнения объекта. Что же делает этот метод? В самом начале своей работы он проверяет номер схемы объ­екта. Если номер схемы равен -1, это означает, что объект се-риализовать нельзя. Естественно, в таком случае тут же выра­батывается исключение. Затем производится вызов метода MapObject() с аргументом NULL, т. е. инициализируется хэш-таб­лица, связанная с архивом. Вспомните, я уже говорил, что вы­зов этого метода с аргументом, равным NULL, абсолютно безо­пасен, максимум, что он делает, это создает и инициализирует хэш-таблицу. А затем начинается интересное. Метод выбирает из хэш-таблицы значение, соответствующее указателю на инфор­мацию времени исполнения объекта, а затем присваивает это значение переменной nClassIndex. Естественно, у читателя воз­никает вопрос – а почему речь зашла о каком-то индексе клас­са, а не об указателе? Но для того чтобы ответить на этот во­прос, нам нужно узнать, что записывается в хэш-таблицу. Ду­маю, мы скоро это узнаем. А пока давайте разберемся с тем, что произойдет в том случае, если элемент с ключом, равным указателю на объект, не найден в таблице. Прежде всего, это будет означать, что информация об объекте в таблицу не запи­сывалась и мы производим запись объекте, ранее в архив не записанного. В этом случае метод WriteClassQ записывает в архив значение wNewClassTag, определенное в файле arcobj.cpp следующим образом:

#define wNewClassTag ((WORD)OxFFFF)

// special tag indicating new CRuntimeClass



Но мы же хотим добавить в архив не нулевой указатель, а ука­затель на реальный объект, т. е. наш указатель на объект никак не будет нулевым, не так ли? Что происходит в нашем случае? Ме­тод WriteObject() пытается из хэш-таблицы выбрать данные с клю­чом, равным указателю на объект. В том случае, если информация об объекте ранее в хэш-таблицу не записывалась, WriteObject() принимает решение о том, что ранее в процессе записи в архив этот объект не встречался, следовательно, помимо данных об объ­екте необходимо занести в архив и информацию о классе объекта. В этом случае метод WriteObjectQ при помощи метода GetRuntimeClass() получает информацию времени выполнения объ­екта, используя которую пытается затем записать в архив данные о том, к какому классу принадлежит объект. Данные о классе в ар­хив записываются при помощи метода WriteClass():

void CArchive::WriteClass(const CRuntimeClass* pClassRef) {

ASSERT(pClassRef != NULL);

ASSERT(IsStoring()); // proper direction

if (pClassRef->m_wSchema == OxFFFF) {

TRACE1("Warning: Cannot call WriteClass/WriteObject

for %hs.\n", pClassRef->m_lpszClassName) ; AfxThrowNotSupportedException();

}

// make sure m_pStoreMap is initialized MapObject(NULL);

// write out class id of pOb, with high bit set to indicate // new object follows

II ASSUME: initialized to 0 map DWORD nClassIndex;

if ((nClassIndex = (DWORD)(*m_pStoreMap)[(void*)pClassRef])

!= 0)

{

// previously seen class, write out the index tagged // by high bit

if (nClassIndex < wBigObjectTag)

*this « (WORD)(wClassTag | nClassIndex);

else

{

*this « wBigObjectTag;

*this « (dwBigClassTag I nClassIndex);

}

}

else {

// store new class

*this « wNewClassTag; pClassRef->Store(*this);

// store new class reference in map, checking for overflow CheckCount();

(*m_pStoreMap)[(void*)pClassRef] = (void*)m_nMapCount++;

}

}



В том случае, если производится сохранение объекта и пере­менная m__pStoreMap (пока мы о ней ничего не знаем) равна NULL, то… То заводится новый объект класса CMapPtrToPtr и указатель на него присваивается переменной m__pStoreMap! Значит, поле m_pStoreMap содержит в себе указатель на объект, предназначен­ный для работы с хэш-таблицей! Следовательно, в процессе со­хранения объекта в архиве используется хэш-таблица! При этом для входа в таблицу в качестве ключа используются указатели, а в качестве значений также используются указатели. После соз­дания хэш-таблица инициализируется и (еще раз внимание, чита­тель) в нулевой элецент хэш-таблицы записывается указатель на новую ассоциацию, причем ключом этой ассоциации является зна­чение NULL, а значением – нуль. Счетчик количества ассоциаций, т. е. поле mjiMapCount увеличивается на единицу, т. е. делается равным одному. Если аргумент метода равен NULL, то больше ни­каких действий не производится. И какой вывод мы можем сделать из сказанного выше? Вывод состоит в том, что метод MapObject() с переданным ему в качестве аргумента значением NULL вызы­вать совершенно безопасно! В том случае, если обращение к мето­ду осуществляется впервые, то будет создана и проинициализиро-вана хэш-таблица, только и всего.

Но вернемся к методу WriteObject(). После инициализации хэш-таблицы в том случае, если мы хотим записать в архив нулевой указатель, то в архив записывается значение wNullTag, которое в файле appobj.cpp определено следующим образом:

#define wNullTag ((WORD)0)

// special tag indicating NULL ptrs



Что ж, в связи с важностью метода необходимо этот метод рассмотреть более подробно. Понятно, что в качестве аргумен­та методу передается указатель на записываемый в архив объ­ект. Сразу после проведения необходимых проверок вызывает­ся метод MapObject(). При этом (обратите, пожалуйста, на это внимание, читатель) методу MapObject() в качестве аргумента передается не указатель на объект, как следовало бы ожидать, a NULL. Исходный код метода MapObject() также находится в фай­ле arcobj.cpp:

void CArchive::MapObject(const CObject* pOb) {

if (IsStoring()) {

if (m_pStoreMap == NULL) {

// initialize the storage map

// (use CMapPtrToPtr because it is used for

// HANDLE maps too)

m_pStoreMap = new CMapPtrToPtr(m_nGrowSize) ; m_pStoreMap->InitHashTable(m_nHashSize); m_pStoreMap->SetAt(NULL, (void*) (DWORD)wNullTag) ; m_nMapCount = 1;

}

if (pOb != NULL) {

CheckCount(); (*m_pStoreMap)[(void*)pOb] = (void*)m_nMapCount++;

}

}

else {

if (m_pLoadArray == NULL) {

// initialize the loaded object pointer array // and set special values m_pLoadArray = new CPtrArray; m_pLoadArray->SetSize(1, m_nGrowSize); ASSERT(wNullTag == 0); m_pLoadArray->SetAt(wNullTag, NULL); m_nMapCount = 1;

}

if (pOb != NULL) {

CheckCount() ;

m_pLoadArray->InsertAt(m_nMapCount++, (void*)pOb);

}

}

}



Для того чтобы записать объект в архив, необходимо воспользо­ваться методом WriteObject().Ero исходный код находится в файле arcobj.cpp:

void CArchive::WriteObject(const CObject* pOb) {

// object can be NULL

ASSERT(IsStoringO); // proper direction DWORD nOblndex;

ASSERT(sizeof(nOblndex) == 4); ASSERT(sizeof(wNullTag) == 2); ASSERT(sizeof(wBigObjectTag) == 2); ASSERT(sizeof(wNewClassTag) == 2);

// make sure m_pStoreMap is initialized MapObject(NULL);

if (pOb == NULL) {

// save out null tag to represent NULL pointer *this « wNullTag;

}

else if ((nOblndex = (DWORD)(*m_pStoreMap)[(void*)pOb])

!= 0)

// assumes initialized to 0 map

{

// save out index of already stored object if (nOblndex < wBigObjectTag)

*this « (WORD)nOblndex; else {

*this « wBigObjectTag; *this « nOblndex;

}

else {

// write class of object first

CRuntimeClass* pClassRef = pOb->GetRuntimeClass(); WriteClass(pClassRef);

// enter in stored object table, checking for overflow CheckCount();

(*m_pStoreMap) [ (void*) pOb] = (void**) m_nMapCount++;

// cause the object to serialize itself ((CObject*)pOb)->Serialize(*this); }



Как нетрудно догадаться, метод Write() действует так же, как и метод ReadQ. Как и в случае метода ReadQ, если число записываемых байтов равно нулю, то метод, не выполняя никаких действий, сразу же возвращает нулевое значение. Но если нам нужно на самом деле записать в архив определенное число байтов, то метод производит следующие действия. Сначала подготовленными для записи данными заполняется буфер архива. Если данные удалось нормально разместить в буфере архива, то работа метода на этом прекращается и метод возвращает число записанных в буфер архива байтов. Вероятно, что все данные, которые необходимо записать в архив, полностью размещены в буфере не будут. В этом случае метод сбрасывает буфер архива в файл, используя при этом метод FlushQ, а затем осуществляет запись в файл напрямую из буфера пользователя тех частей данных, которые потребовали бы перезаписи буфера архива. Как и в случае метода ReadQ, в буфере остается только последняя часть под­готовленных данных. Все данные, которые могли бы полностью заполнить буфер, записываются в файл напрямую. Однако в этом методе есть минимум один «подводный камень». Обратите внимание на то, что последняя часть данных, оставшаяся в буфере, в файл НЕ СБРАСЫВАЕТСЯ! Если сейчас закрыть файл, то часть данных, которая находится в буфере архива, в файл сброшена не будет! Для того чтобы предотвратить потерю данных необходимо после записи всех данных в архив осуществлять вызов метода FlushQ, который произведет сброс в файл остатка данных, находящегося в бу­фере архива.

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

Что ж, уважаемый читатель, мы познакомились с некоторыми ме­тодами класса CArchive. Из того, что мы узнали про архив, можно пока сделать только один вывод – архив может использоваться для буферированного ввода и вывода данных в файл. Кроме этого, возможно, удобно бужет использовать перегруженные операторы и «»» ««» для работы сданными простых типов. Однако, как мне кажется, вы испытываете легкое разочарование. Подумаешь, стоило ли огород городить ради того, чтобы осуществлять буферированный ввод/вывод? Но не стоит торопить события. То, о чем мы говорили, является всего лишь прелюдией к тому, ради чего и создан класс CArchive. А создан он для того, чтобы можно было записывать в файл и читать из файла не только данные, но и объекты! А вот это уже интересно,верно?



03.02.2010

Естественно, раз существует метод Read(), то, наверное, дол­жен существовать и метод Write(). Действительно, такой метод су­ществует, его исходный код находится в файле агссоге.срр:

void CArchive::Write(const void* lpBuf, UINT nMax) {

ASSERT_VALID(m_pFile);

if (nMax == 0) return;

ASSERT(lpBuf !=NULL);

ASSERT(AfxIsValidAddress(lpBuf, nMax, FALSE));

// read-only access needed

ASSERT(m_bDirectBuffer || m_lpBufStart !=NULL); ASSERT(mjDDirectBuffer || m_lpBufCur !=NULL); ASSERT(m_lpBufStart == NULL ||

AfxIsValidAddress(m_lpBufStart,

m_lpBufMax – m_lpBufStart) ) ; ASSERT(m_lpBufCur == NULL ||

AfxIsValidAddress(m_lpBufCur,

m_lpBufMax – m_lpBufCur) ) ;

ASSERT(IsStoring());

// copy to buffer if possible UINT nTemp = min(nMax, (UINT)(m_lpBufMax – m_lpBufCur)); memcpy(m_lpBufCur, lpBuf, nTemp); m_lpBufCur += nTemp; lpBuf = (BYTE*)lpBuf + nTemp; nMax -= nTemp;

if (nMax > 0) {

Flush(); // flush the full buffer

// write rest of buffer size chunks

nTemp = nMax – (nMax % m_nBufSize); m_pFile->Write(lpBuf, nTemp); lpBuf = (BYTE*)lpBuf + nTemp; nMax -= nTemp;

if (m_bDirectBuffer) {

// sync up direct mode buffer to new file position

VERIFY(m_pFile->GetBufferPtr(CFile::bufferWrite,

m_nBufSize, (void**) &m__lpBufStart, (void**)&m_lpBufMax) == (UINT)m_nBufSize); ASSERT((UINT)m_nBufSize == (UINT)(m_lpBufMax -

m_lpBufStart));

m_lpBufCur = m_lpBufStart;

}

// copy remaining to active buffer ASSERT(nMax < (UINT)m_nBufSize); ASSERT(m_lpBufGur == m_lpBufStart); memcpy(m_lpBufCur, lpBuf, nMax); m_lpBufCur += nMax;

}

}

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