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

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

Архив раздела «Приложения»

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

ON_COMMAND( ID_FILE_OPEN, CWinApp::OnFileOpen )

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

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



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

Я старался рассуждать логически. Я хочу отобразить содержи­мое файла. При этом хочу сделать это в соответствии с требова­ниями архитектуры «документ/представление». Но у меня нет яс­ности по многим вопросам. Когда, в какой момент файл на диске должен превратиться в документ? Наверное, в момент открытия файла. Другими словами, при обработке команды на открытие файла, полученной от меню, я должен открыть файл и превратить его в документ. Но какой файл надо открыть? Естественно, тот файл, который я укажу. Где укажу? Конечно же, в стандартном диа­логовом окне для выбора открываемого файла! Значит, мне необ­ходимо первым делом подготовить данные для открытия стандарт­ного диалога. После того как я выберу файл, мне необходимо бу­дет его открыть, разобрать по косточкам и каким-то образом со­держимое файла превратить в документ… Н-да, работы непоча­тый край…



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

#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;



С чего я начал? Написал небольшую программу, при помощи которой хотел всего-навсего выяснить характер взаимодействия объектов раз­ных классов при работе в соответствии с идеологией архитектурой «документ/представление». Эта программа не должна делать ниче­го, кроме отображения пустого документа в (естественно!) пустом окне отображения. Впоследствии я планировал использовать текст этой программы как заготовку для других программ.

Ниже приведен текст файла ресурсов моей программы. Ком­ментарии, созданные средой Visual С++, я предварительно удалил.

#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS

#include "afxres.h"

#undef APSTUDIO_READONLY_SYMBOLS

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS)

#ifdef _WIN32

LANGUAGE LAN G__RU SSI AN, SUBLANG_DE FAULT #pragma code_page(1251) #endif //_WIN32

#ifdef APSTUDIO_INVOKED

1 TEXTINCLUDE DISCARDABLE BEGIN

"resource. h\0"

END

2 TEXTINCLUDE DISCARDABLE . BEGIN

"#include xw‘afxres.h""\r\n" "\0"

END

3 TEXTINCLUDE DISCARDABLE BEGIN

"\r\n" "\0"

END

#endif // APSTUDIO_INVOKED

IDR_RESOURCE ICON DISCARDABLE "ResWCommon.ico"

IDR_RESOURCE MENU DISCARDABLE BEGIN

ID_FILE_OPEN ID APP EXIT

POPUP "&File"

BEGIN

MENUIТЕМ "&Open\tCtrl+0", MENUIТЕМ SEPARATOR MENUIТЕМ "E&xit\tCtrl+x",

END

POPUP "SWindow", GRAYED BEGIN

MENUITEM "Tile &horizontally\tCtrl+H", ID_WINDOW_TILEHORZ, GRAYED

MENUITEM "Tile &vertically\tCtrl+V", ID_WINDOW_TILE_VERT, GRAYED

MENUIТЕМ "&Cascade\tCtrl+C", ID_WINDOW_CASCADE

END

POPUP "Help" BEGIN

MENUIТЕМ "&About", ID_APP_ABOUT

END

END

STRINGTABLE DISCARDABLE BEGIN

ID_FILE_OPEN "Open an existing document"

END

"Display program information, version number and copyright" "Quit the application"

STRINGTABLE DISCARDABLE BEGIN

ID_APP_ABOUT

ID_APP_EXIT

END

STRINGTABLE DISCARDABLE BEGIN

IDR_RESOURCE "PE-file viewer and disassembler"

END



08.02.2010

Тем не менее, мы может прекратить работу архива в любое время при помощи метода Abort(), исходный текст которого находится в файле агссоге.срр:

void CArchive::Abort() {

ASSERT(m_bDirectBuffer ||

m_lpBufStart == NULL || AfxIsValidAddress(m_lpBufStart,

m_lpBufMax – m_lpBufStart, IsStoring()));

ASSERT(m_bDirectBuffer ||

m_lpBufCur == NULL || AfxIsValidAddress(m_lpBufCur,

m_lpBufMax – m_lpBufCur, IsStoring()));

// disconnect from the file mjpFile = NULL;

if (!m_bUserBuf) {

ASSERT(!m_bDirectBuffer); delete[] m_lpBufStart; m_lpBufStart = NULL; m_lpBufCur = NULL;

}

delete m_pSchemaMap; m_pSchemaMap = NULL;

// m_pStoreMap and m_pLoadArray are unioned, // so we only need to delete one

ASSERT((CObject*)m_pStoreMap == (CObject*)m_pLoadArray); delete (CObject*)m_pLoadArray; m_pLoadArray = NULL;Посмотрим, что происходит с архивом в случае прекращения работы посредством вызова метода AbortQ. Первым делом метод «отсоединяет» архив от файла, присваивая полю m__pFile значе­ние NULL. Затем в том случае, если у архива есть ассоциированный с ним буфер, производится удаление буфера. Если в буфере остались данные, которые не были записаны в файл, то эти данные будут потеряны. Указатели на начало буфера и на текущую позицию буфера делаются равными NULL. Затем удаляется указатель на хэш-таблицу, содержащую номера схем классов, а за ней и хэш-таблица (или массив) сохраненных объектов.



Мне бы хотелось, чтобы вы вспомнили, что происходит при по­лучении окном класса CMDIChildWnd сообщения WM_CREATE. Во время работы этого метода вызываются еще много других мето­дов, в том числе CFrameWnd::CreateView(). Именно при работе этого метода и происходит вызов метода CreateObject() для объекта клас­са окна представления. Естественно, метод CreateObject() вызы­вает конструктор объекта окна представления. И, разумеется, вы­зываются строго по порядку конструкторы классов, от которых унас­ледован наш объект. Если мы в качестве окна приложения исполь­зуем объект класса CEditView, то первым вызывается конструктор CEditView::CEditView(). Исходный код этого конструктора находит­ся в файде viewedit.cpp:

// pass a NULL style because dwStyleDefault stays for // backward compatibility

CEditView::CEditView() : CCtrlView(_T("EDIT"), NULL)

m_nTabStops = 8*4; // default 8 character positions m_hPrinterFont = NULL; m_hMirrorFont = NULL; m_pShadowBuffer = NULL; m_nShadowSize = 4);

}

Перед тем, как отработает этот конструктор, управление будет передано конструктору класса CCtrlView.

Исходный текст конструктора класса CCtrlView можно найти в файле viewcore.cpp:

CCtrlView::CCtrlView(LPCTSTR IpszClass, DWORD dwStyle) {

m_strClass = IpszClass;

m_dwDefaultStyle = dwStyle;

}



Я начал писать программу и остановился, словно услышал вопрос, задаваемый читателем: «Как же так, если, к примеру, мне хочется отображать данные в виде дерева или, скажем, в виде текста, мне так и придется прорисовывать дерево или использовать TextOut()?» Что ж, вопрос вполне закономерный. Мне тоже очень не хотелось думать, что разработчики MFC остановились на полпути и не до­вели дело до конца. Помните, читатель, мое федположение о том, что класс CView является абстрактным и предоставляет только базовые возможности для объектов класса окна представления? Отправной точкой дальнейших изысканий послужило предположе­ние, что более специализированные классы окон представлений наследуются от CView. Заглянув в исходные коды MFC, я слегка обомлел. Оказывается, от CView наследуются классы CCtrlView, CScrollView. В свою очередь от CCtrlView унаследованы классы CListView, CTreeView, CEditView, CRichEditView. От CScrollView унас­ледованы CFormView, CPreviewView. От CFormView унаследованы классы CDaoRecordView, CRecordView, CHtmlView, COIeDBRecord-View. Графически это можно представить так:

Пример

(CVievT)

-[CCtrlView ]

( CListView ]

[CTreeView]

-(CEditVievT)

CRichEditView]

-(CScrollView ~)

[CFormView ]

-(CDAORecordView)

(CRecordView ]

-(CHtmlView ]

[CQIeDBRecordView ]

—[ CPreviewView]

Даже судя по названиям, уже можно сделать выводы о предна­значении этих классов. Наверное, класс окна отображения в моей программе мне нужно было наследовать не от CView, а от более специализированного класса, скажем, CEditView. Что ж, попробу­ем так и сделать. Но теперь нам нужно отследить,каким образом работают объекты этого класса. Давайте, читатель, подумаем вме­сте вами. На каком этапе начинает работать объект класса, унас­ледованного от CView? Наверное, при создание объекта окна пред­ставления. Давайте попробуем проследить, что произойдет в мо­мент создания окна представления.



Но вернемся от нашей программы к методу CDocument.On-OpenDocument(). Естественно, что после того, как мы считали со­держимое архива в поля нашего документа, архив и файл нашей программе больше не нужны и мы их можем спокойно закрыть, что и происходит. Для этого используются методы CArchive::Close() и CDocument::ReleaseFile().

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



Документ, естественно, рождается из файла, поэтому первым делом метод, вызывая 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;

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



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

И, кажется, все, что можно проверить, уже проверено. Метод ReadClassQ записывает по переданным ему ссылкам значения подготовленных схемы и ссылки на класс, после чего возвраща­ет указатель на информацию времени выполнения объекта. Не забудьте, что помимо возвращаемых значений, результатом работы метода ReadClass() является и заполненный элемент в массиве.

Итак, мы опять вернулись в метод ReadObject(). Какие возмож­ные варианты дальнейших действий необходимо рассмотреть?

Вариант первый – метод ReadClass() вернул NULL. Это означает, что из архива был считан тэг объекта.

Вариант второй – метод ReadClass вернул ненулевое значение. Это означает, что из архива был считан тэг класса.

Итак, что происходит в том случае, если считан тэг класса? Во-первых, создается объект этого класса. Согласитесь, чита­тель, мы не можем записать в архив просто класс, мы записыва­ем в архив ОБЪЕКТЫ класса, следовательно, после каждого описания класса или ссылки на класс должен следовать тэг объ­екта, верно? Во-вторых, указатель на этот объект помещается в очередной элемент массива указателей. И, в-третьих, вызы­вается метод Serialize() объекта. Метод ReadObjectQ возвраща­ет указатель на считанный из архива объект. Мы дошли до логического завершения процесса сериализации. Нерассмотрен­ным осталась небольшая деталь.

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

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

void CArchive::Close() {

ASSERT_VALID(m_pFile);

Flush(); m_pFile = NULL;

Как видно из исходного текста метода, содержимое буфера «сбра­сывается» в файл и поле m_pFile делается равным NULL, т. е. архив «отсоединяется» от файла, на основе которого он был создан.



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

Еще одна проверка – а вдруг класс, который мы считали из ар­хива, не является наследником класса, информация о котором пе­редана методу ReadClass() в качестве аргумента? Проверка осу­ществляется при помощи метода CRuntimeClass::lsDerivedFrom(). Исходный код этого метода находится в файле objcore.cpp:

BOOL CRuntimeClass::IsDerivedFrom(const

CRuntimeClass* pBaseClass) const

{

ASSERT(this !=NULL);

ASSERT(AfxIsValidAddress(this,

sizeof(CRuntimeClass), FALSE)); ASSERT(pBaseClass !=NULL); ASSERT(AfxIsValidAddress(pBaseClass,

sizeof(CRuntimeClass), FALSE));

// simple SI case

const CRuntimeClass* pClassThis = this;

while (pClassThis != NULL)

{

if (pClassThis == pBaseClass) return TRUE; #ifdef _AFXDLL

pClassThis = (*pClassThis->m_pfnGetBaseClass) (); #else

pClassThis = pClassThis->m_pBaseClass; #endif }

return FALSE; // walked to the top, no match

}



Метод производит считывание номера схемы класса и длины названия класса без учета завершающего нулевого байта, после чего в выделенный для названия класса символьный массив чита­ет название класса. Если указанное в архиве число символов в на­звании класса превышает размер выделенного для него буфера или же при считывании из архива было считано меньше, чем ука­зано, байтов, то метод немедленно возвращает NULL. После того как название класса считано, к нему дописывается нулевой байт, т. е. с этого момента название класса становится обычной строкой, завершающейся нулевым байтом. Затем метод проверяет, описан ли класс, название которого прочитано в архиве, в текущем про­цессе. Если класс не описан, то метод возвращает значение NULL. Таким образом, если: а) длина имени сохраненного класса равна 64 байтам или больше; б) если при считывании имени класса произошла какая-то ошибка и было считано меньше символов, чем указано; в) класс не описан в вызывающем модуле, то метод CRuntime::Load() возвращает значение NULL. Если же все про­верки прошли нормально, то возвращается указатель на инфор­мацию времени выполнения того класса, имя которого совпадает и именем считанного из архива класса, и управление опять пере­ходит в метод ReadClassQ. Таким образом, если метод CRuntimeClass::Load() возвратил ненулевое значение, то это яв­ляется признаком того, что в процессе, осуществляющем чте­ние из архива, класс, описание которого только что было счи­тано из архива, также описан и процесс готов работать с объ­ектами данного класса.

Если метод CRuntimeClass::Load() вернул значение NULL, это означает, что произошла ошибка, которую метод ReadClass() са­мостоятельно устранить не в состоянии. Естественный выход для метода ReadClass() в этой ситуации – выработка исключения, что он и делает. Если же загрузка прошла нормально, то начинается проверка соответствия схем (версий) классов, ведь вполне веро­ятно, что в программе используется описание более новой версии класса, чем сохраненная в архиве, верно?

Если схемы класса в вызывающем модуле и в считанной из ар­хива информации не совпали и при этом не указано, что класс мо­жет загружать информацию разных версий, то вырабатывается ис­ключение. В том случае, если схемы не совпадают, но указано, что класс может загружать информацию разных версий, для хранения несовпадающих версий заводится новая хэш-таблица, указатель на нее записывается в поле mjDSchemaMap. Ключами в этой таб­лице служат указатели на информацию времени выполнения клас­са, считанного из архива, а значения, которые принимают элемен­ты, равны номерам схем считанных из архива классов. Здесь я не поленюсь лишний раз повторить, что хэш-таблица заводится для хранения ТОЛЬКО тех схем, номера которых НЕ СОВПАДАЮТ со схемой класса, информация о котором передана методу в качестве аргумента.

Раз заведена хэш-таблица, то как же обойтись без проверки числа элементов в ней при помощи метода CheckCount()? А после проверки указатель на информацию времени исполнения класса записывается в очередной свободный элемент массива указателей, который был заведен при вызове метода MapObject(). Естественно, число элемен­тов в этом массиве увеличивается на единицу. *

Тем самым мы завершили обработку описания впервые встре­тившегося класса. А что происходит в тех случаях, когда мы счи­тываем ссылки на ранее встречавшиеся описания классов? Если индекс ранее встречавшегося класса не равен нулю (если читатель помнит, то нулевой элемент массива инициализируется значением NULL) и не превышает верхнего индекса массива (индекс в таком случае просто лишен смысла), из элемента массива с указанным индексом выбирается информация времени исполнения класса…

Так… А почему мы постоянно говорим о массиве? Дело в том, что индекс, с которым элемент был добавлен в ХЭШ-ТАБЛИЦУ при записи в архив, может использоваться как индекс МАССИВА при считывании из архива. Ведь в данном случае нам не нужно осуществлять массу проверок, верно? И для хранения данных вполне достаточно массива, доступ к элементам которого можно осуществлять по индексам, верно?



Практически сразу после вызова ReadClassQ вызывает ме­тод MapObject() с параметром NULL. Но обратите внимание, чи­татель, на то, что в случае чтения информации из архива соз­дается не хэш-таблица, а МАССИВ указателей. Указатель на массив записывается в поле m_pLoadArray, которое, кстати, опи­сано в одном объединении (union’e) с полем m_pStoreMap. После этого в архиве создается один элемент, в который записывается значение NULL. Естественно, счетчик элементов массива, т. е. значение поля m_nMapCount, тоже делается равным одному. Как и в случае сохранения информации в массиве, при аргументе, равном NULL метод больше ничего не делает. Таким образом, вызов метода MapObject() с параметром NULL при чтении из архива приводит к созданию и инициализации массива указателей.

Затем метод ReadClass() начинает работу в точном соответствии с тем списком правил, который мы сформировали в конце предыду­щего раздела. В том случае, если считан тэг объекта, то метод записывает тэг объекта по адресу, переданному ему в качестве третьего аргумента, и возвращает значение NULL. Отметим этот факт – метод ReadClassQ в том случае, если из архива считан тэг объекта, возвращает значение NULL. У объекта схемы (версии) быть не может, поэтому мы не заполняем указатель на схему. Однако если из архива считан тэг класса, нам придется немного повозиться с этим тэгом.

Давайте рассуждать. Если встречен тэг класса, то какие случаи должны быть рассмотрены при этом? Наверное, должны быть рас­смотрены два случая: 1) считан тэг ранее не встречавшегося клас­са и 2) считана ссылка на тэг ранее встречавшегося в процессе считывания класса. В том случае, если метод ReadClass() считал из архива тэг ранее не встречавшегося класса, то он должен загрузить информацию времени выполнения этого класса, после чего осуществить все необходимые проверки, верно? Информация вре­мени выполнения загружается при помощи метода CRuntimeClass::Load(), исходный текст которого можно найти в файле агссоге.срр:

CRuntimeClass* PASCAL CRuntimeClass::Load(CArchive& ar,

UINT*’pwSchemaNum)

// loads a runtime class description

{

WORD nLen;

char szClassName[64]; CRuntimeClass* pClass;

WORD wTemp;

ar >> wTemp; *pwSchemaNum = wTemp; ar >> nLen;

if (nLen >= _countof(szClassName) ||

ar.Read(szClassName, nLen*sizeof(char)) !=

nLen*sizeof(char))

{

return NULL;

}

szClassName[nLen] = Л\0′;

// search app specific classes

AFX_MODULE_STATE* pModuleState = AfxGetModuleState(); AfxLockGlobals(CRIT_RUNTIMECLASSLIST);

for (pClass = pModuleState->m_classList; pClass != NULL; pClass = pClass->m_pNextClass)

{

if (IstrcmpA(szClassName,

pClass->m_lpszClassName) == 0)

{

AfxUnlockGlobals(CRIT_RUNTIMECLASSLIST); return pClass;

}

}

AfxUnlockGlobals(CRIT_RUNTIMECLASSLIST);

#ifdef _AFXDLL

// search classes in shared DLLs AfxLockGlobals(CRITJDYNLINKLIST);

for (CDynLinkLibrary* pDLL = pModuleState->m_libraryList; pDLL != NULL; pDLL = pDLL->m_pNextDLL)

{

for (pClass = pDLL->m_classList; pClass != NULL; pClass = pClass->m_pNextClass)

{

if (IstrcmpA(szClassName,

pClass->m_lpszClassName) == 0)

{

AfxUnlockGlobals(CRIT_DYNLINKL1ST) ; return pClass;

}

}

}

AfxUnlockGlobals(CRIT_DYNLINKLIST); #endif

TRACE1 ("’Warning: Cannot load %hs from archive.

Class not defined.\n", szClassName);

return NULL; // not found

}

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



После того, что мы узнали о записи объекта в архив, мы можем догадаться, что первым делом метод ReadObject() постарается считать данные о классе объекта. Как видно из текста метод, так оно и происходит. Для считывания информации о классе использу­ется метод ReadClass(). Исходный код этого метода находится в файле afxobj.cpp:

CRuntimeClass* CArchive::ReadClass(const

CRuntimeClass* pClassRefRequested, UINT* pSchema, DWORD* pObTag)

{

ASSERT(pClassRefRequested == NULL || AfxIsValidAddress(pClassRefRequested,

sizeof(CRuntimeClass), FALSE));

ASSERT(IsLoading()); // proper direction

if (pClassRefRequested != NULL &&

pClassRefRequested->m_wSchema == OxFFFF)

{

TRACE1("Warning: Cannot call ReadClass/ReadObject

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

}

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

// read object tag – if prefixed by wBigObjectTag // then DWORD tag follows

DWORD obTag;

WORD wTag;

*this >> wTag;

if (wTag == wBigObjectTag) *this » obTag;

else

obTag = ( (wTag & wClassTag) « 16) | (wTag & -wClassTag);

// check for object tag (throw exception if // expecting class* tag)

if (!(obTag & dwBigClassTag)) {

if (pObTag == NULL)

AfxThrowArchiveException(CArchiveException::badlndex,

m_strFileName);

*pObTag = obTag; return NULL;

CRuntimeClass* pClassRef;

UINT nSchema;

if (wTag == wNewClassTag)

{

// new object follows a new class id

if ((pClassRef = CRuntimeClass::Load(*this,

&nЈchema)) == NULL) AfxThrowArchiveException(CArchiveException::badClass,

m_strFileName);

// check nSchema against the expected schema if ((pClassRef->m_wSchema &

~VERSIONABLE_SCHEMA) != nSchema)

{

if (! (pClassRef->m_wSchema & VERSIONABLE_SCHEMA) ) {

// schema doesn’t match and not marked as VERSIONABLE_SCHEMA Af xThrowArchiveException (CArchiveException: :badSchema,

m_strFileName);

}

else {

// they differ — store the schema for later retrieval if (m_pSchemaMap == NULL)

m_pSchemaMap = new CMapPtrToPtr; ASSERT_VALID(m_pSchemaMap);

m_pSchemaMap->SetAt(pClassRef, (void*)nSchema) ;

}

}

CheckCount ();

m_pLoadArray->InsertAt(m_nMapCount++, pClassRef) ;

}

else {

// existing class index in obTag followed by new object DWORD nClassIndex = (obTag & -dwBigClassTag); if (nClassIndex == 0 I I

nClassIndex > (DWORD)m_pLoadArray->GetUpperBound() ) AfxThrowArchiveException(CArchiveException::badlndex,

m_strFileName);

pClassRef =

(CRuntimeClass*)m_pLoadArray->GetAt(nClassIndex); ASSERT(pClassRef !=NULL);

// determine schema stored against objects of this type void* pTemp; BOOL bFound = FALSE; nSchema = 0;

if (m_pSchemaMap != NULL) {

bFound = m_pSchemaMap->Lookup( pClassRef, pTemp ); if (bFound)

nSchema = (UINT)pTemp;

}

if (!bFound)

nSchema = pClassRef->m_wSchema & ~VERSIONABLE_SCHEMA;

}

// check for correct derivation if (pClassRefRequested != NULL &&

!pClassRef->IsDerivedFrom(pClassRefRequested))

{

AfxThrowArchiveException(CArchiveException::badClass,

m_strFileName);

}

// store nSchema for later examination if (pSchema != NULL)

*pSchema = nSchema; else

m_nObjectSchema = nSchema;

// store obTag for later examination if (pObTag != NULL) *pObTag = obTa^;

// return the resulting CRuntimeClass* return pClassRef;

}

В качестве аргументов методу передается указатель на ин­формацию времени выполнения класса, а также ссылки на два поля, которые будут заполнены значениями, считанными из ар­хива. В поле pSchema будет записана схема (версия) класса, а в поле obTag будет записан тэг объекта, каким он был записан в архив. Смысл передачи ссылки на схему и на тэг объекта понятен. А зачем мы передаем указатель на информацию вре­мени выполнения класса? Дело обстоит очень просто. Вполне вероятно, что наша программа уже изменилась и чо в нашей программе класс, который мы готовимся считать из архива, просто не описан, верно? Или, скажем, мы по ошибке открыли архив, созданный другой программой, или… Да мало ли что может приключиться! Мы должны убедиться в том, что мы считываем из архива объект именно того класса, о котором у нас есть информация, верно?



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

CObject* CArchive::ReadObject(const CRuntimeClass*

pClassRefRequested)

{

ASSERT(pClassRefRequested == NULL || AfxIsValidAddress(pClassRefRequested,

sizeof(CRuntimeClass), FALSE));

ASSERT(IsLoading()); // proper direction ASSERT(wNullTag == 0);

ASSERT((wClassTag « 16) == dwBigClassTag); ASSERT((wNewClassTag & wClassTag) == wClassTag);

// attempt to load next stream as CRuntimeClass UINT nSchema; DWORD obTag;

CRuntimeClass* pClassRef = ReadClass(pClassRefRequested,

SnSchema, &obTag);

// check to see if tag to already loaded object CObject* pOb; if (pClassRef == NULL) {

if (obTag > (DWORD)m_pLoadArray->GetUpperBound()) {

// tag is too large for the number of objects read so far AfxThrowArchiveException(CArchiveException::badlndex,

m_strFileName);

}

pOb = (CObject*)m_pLoadArray->GetAt(obTag); if (pOb != NULL &&

pClassRefRequested != NULL && !pOb->IsKindOf(pClassRefRequested))

{

// loaded an object but of the wrong class

AfxThrowArchiveException(CArchiveException:rbadClass,

m_strFileName);

}

}

else {

// allocate a new object based on the class just acquired pOb = pClassRef->CreateObject(); if (pOb == NULL)

AfxThrowMemoryException();

// Add to mapping array BEFORE de-serializing CheckCount();

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

// Serialize the object with the schema number set // in the archive

UINT nSchemaSave = m_nObjectSchema;

m_nObjectSchema = nSchema;

pOb->Serialize(*this);

m_nObjectSchema = nSchemaSave;

ASSERT_VALID(pOb) ;

}

return pOb;

}



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

В MSDN в статье, посвященной объекту CArchive, говорится, что «an archive processes binary object data in an efficient, nonredun-dant format (архив обрабатывает данные бинарного объекта в эф­фективном неизбыточном формате)». Я думаю, что после прочте­ния этой главы вы поняли, почему в данном случае речь может идти как о неизбыточности, так и об эффективности.



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

1. Если значение считанного слова (назовем его wTag) меньше wBigObjectTag (wTag<wBigObjectTag), то, значит, мы считали тэг объекта, за которым должна следовать информация, записан­ная методом Serialize() этого объекта.

2. Если значение считанного слова wTag больше или равно (wTag >= (wClassTag + 1)), но меньше wNewClassTag (wTag< wNewClassTag), то мы считали ссылку на индекс класса. Индекс класса может быть получен путем сброса в нуль старшего разряда слова.

3. Если значение считанного слова wTag равно wNewClassTag (wTag == wNewClassTag), то, значит, мы считали признак того, что за объектом должно слеловать описание впервые встретив­шегося в архиве класса.

4. Если значение считанного слова wTag равно wBigObjectTag (wTag== wBigObjectTag), то, значит, за этим словом следует двой­ное слово (назовем его dwTag), в котором содержится индекс объекта или класса.

5. Если значение двойного слова dwTag больше dwBigClassTag (dwTag > dwBigClassTag), то, значит, мы считали индекс класса. Индекс класса может быть получен путем сброса в нуль стар­шего разряда двойного слова.

6. Если значение двойного слова dwTag меньше dwBigClassTag (dwTag < dwBigClassTag), то, значит, мы считали индекс объекта. На этом метод WriteObject() завершает свою работу. Если под­вести итог сказанному выше, то можно сделать вывод о том, что основной задачей метода WriteObjectQ является сброс в архив данных о классах сохраняемых объекта и выполнение метода Seri-alizeQ сохраняемого объекта. При этом для того, чтобы предотвра­тить дублирование данных, данные предварительно помещаются в хэш-таблицу. При необходимости повторно записать какие-то данные, в архив записываются не сами данные, а только своеобразная ссылка, которая представляет собой порядковый номер (индекс) данных в хэш-таблице.

Хотелось бы отметить, что программист может не вызывать в программе напрямую метод WriteObject(), а воспользоваться опе­ратором "«". Дело в том, что оператор для записи объекта в архив также перегружен и имеет вид:

_AFX_INLINE CArchive& AFXAPI operator«(CArchive& ar,

const CObject* pOb)

{

ar.WriteObject(pOb); return ar;

}

Другими словами, перегруженный оператор ««» представляет собой скрытый вызов метода WriteObject().



Двойное слово будет записано в архив в том случае, если ин­декс класса превышает 0×7fff. В этом случае сначала в архив запи­сывается значение wBigObjectTag, которое определено в файле arcobj.cpp следующим образом:

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

// 0×7FFF indicates DWORD object tag

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

#define dwBigClassTag ((DWORD)0×80000000)

// 0×8000000 indicates big class tag (OR’d)

На этом работа метода WriteClass() заканчивается. Пора воз­вратиться к рассмотрению метода WriteObject^). После вызова ме­тода WriteClass() метод WriteObject() проверяет при помощи мето­да CheckCount() число элементов в хэш-таблице, записывает в хэш-таблицу с ключом, равным указателю на объект, ИНДЕКС объекта (после подробного рассмотрения метода WriteClassQ читателю должно быть понятно, о каком индексе я говорю), а затем вызывает метод Serialize() объекта, записываемого в архив.

В том случае, если метод WriteObject() нашел в хэш-таблице элемент с ключом, равным указателю на записываемый в архив объект, он принимает решение о том, что информация об объекте уже записывалась в архив и действует в соответствии с принципами, которые применяются в методе WriteClass(), т. е. записывает в архив слово или слово и двойное слово. Как и в методе WriteClass(), слово записывается в том случае, если индекс объекта не превышает значения wBigObjectTag. В архив в таком случае сбрасывается слово, содержащее индекс объекта. Это слово ВСЕГДА будет МЕНЬШЕ wBigObjectTag (0×7fff). В противном случае сначала в архив сбрасывается значение wBigObjectTag (0×7fff), а затем двойное слово, содержащее индекс объекта.



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

#define wClassTag ((WORD)0×8000)

// 0×8000 indicates class tag (OR’d)

Полученное слово (обратите внимание, читатель, значение этого слова ВСЕГДА будет БОЛЬШЕ wClassTag (0×8000) и ВСЕГДА будет МЕНЬШЕ ИЛИ РАВНО wNewClassTag (Oxffff)) и будет сброшено в архив в качестве информации о классе.



Однако что же будет записано в таблицу в том случае, если мы при помощи метода 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