

Программирование на языке MFC
Мой второй блог в серии программирования
Архив раздела «Приложения»
Я начал реализовывать этот план и какое-то время работал в этом направлении. Однако достаточно быстро я одумался и задал себе один вопрос: а почему, собственно говоря, мне приходится делать всю черновую работу самому? Исходя из того, что я читал про MFC ранее, все должно быть намного проще. Если я иду в правильном направлении, то почему мне приходится делать все самому вручную? Где же хваленые возможности MFC? Все же, наверное, я чего-то не понимаю. И здесь мне пришла в голову мысль, результатом которой и явилось правильное решение. А не попробовать ли мне возложить обработку открытия файла на MFC? Ведь не зря же у класса CWinApp есть метод OnFileOpenQ, не так ли? Карту сообщений своего объекта приложения я немного изменил. Теперь обработчик команды ID__FILE__OPEN стал выглядеть следующим образом:
ON_COMMAND( ID_FILE_OPEN, CWinApp::OnFileOpen )
Тем самым обработку открытия файла я возложил на MFC. Мне осталось только понять, к чему это приведет…
Программа откомпилировалась без ошибок, что меня, честно говоря, несколько удивило. Однако, когда я запустил программу на выполнение в отладочном режиме, то, выбрав элемент «Ореп» меню «File», я получил сообщение, которое приведено на.
читать отзывы (0)
Характерной особенностью этой программы является то, что она практически ничего не делает. Я написал этот вариант только для того, чтобы посмотреть, как 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
Тем не менее, мы может прекратить работу архива в любое время при помощи метода 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
