

Программирование на языке 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…
Итак, мы создали заготовку шаблона документа. Но до сих пор нам не ясно, что происходит непосредственно после того, как мы выберем элемент «Ореп» меню «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 позволил нам самостоятельно сделать выводы, приведенные выше.
Теперь, можно немного отдохнуть, выпить чашку чая или кофе, а потом продолжить рассмотрение. В дальнейшем мы остановимся на процессах, происходящих при создании документа.
Теперь можно вернуться к 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));
}
Что здесь может быть важным для дальнейшего понимания предупреждает нас о том, что указатели на информацию времени выполнения не должны быть равными 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);
Так… Для инициализации шаблона документа нам нужна информация времени исполнения о каких-то трех составляющих. Судя по названиям этих составляющих, речь в данном случае идет о документе, окне фрейма и окне представления. Значит, этот так называемый шаблон документа – это не просто шаблон, а это некий контейнер, который соединяет в себе воедино информацию о трех важнейших составляющих архитектуры «документ/представление». Тем более важно понять, как он создается, какие данные он использует, и как эти данные взаимодействуют друг с другом.
Взглянув на этот текст, мы можем сделать вывод, который в значительной степени облегчит нам жизнь.
В приложении 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 является списком, не так ли?
Однако что же будет записано в таблицу в том случае, если мы при помощи метода 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. А создан он для того, чтобы можно было записывать в файл и читать из файла не только данные, но и объекты! А вот это уже интересно,верно?
Естественно, раз существует метод 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;
}
}
Назначение аргументов, передаваемых методу, предельно ясно. Первый аргумент – это буфер с данными, подготовленными для записи. Второй аргумент – размер этих данных, т. е. число байтов в буфере, которые необходимо записать в архив.
