Глава 14. Сохранение и восстановление состояния объектов
Одна из задач, решаемых программистом при разработке приложений, которые могут создавать и редактировать документы различных типов (например, различные редакторы), заключается в том, чтобы предоставить пользователю возможность записать внутренне представление документа в файл и восстановить его (этот процесс называется сериализацией данных
).Процесс выполнения такой задачи частично уже рассматривался в лекциях, посвященных однооконному и многооконному приложениям. Такие приложения, подготовленные при помощи средства AppWizard, используют этот механизм с помощью методов класса
CDocument . Программисту предлагается только переопределить метод Serialize этого класса для работы с конкретными данными приложения.Программист может определить свой класс (на основе базового класса
CObject ) для работы с данными и воспользоваться рассматриваемым ниже механизмом записи и восстановления объектов.Создание класса, обеспечивающего сериализацию данных
Библиотека классов MFC определяет механизм записи и восстановления объектов (serialization), причем поддержка этого механизма осуществляется средствами класса
CObject .Классы, наследованные от
CObject , также могут обеспечивать работу механизма записи и восстановления объектов. Для этого при объявлении класса надо указать макрокоманду DECLARE_SERIAL , а при определении - макрокоманду IMPLEMENT_SERIAL .Макрокоманду DECLARE_SERIAL необходимо поместить в описании класса во включаемом файле. Непосредственно после этой макрокоманды надо указать имя класса
DECLARE_SERIAL (имя_класса )
Макрокоманду IMPLEMENT_SERIAL следует указать перед определением класса в файле исходного текста приложения. Прототип макрокоманды IMPLEMENT_SERIAL представлен ниже:
IMPLEMENT_SERIAL (имя_класса, имя_базового_класса, номер_версии)
Параметр имя_класса определяет имя класса, имя_базового_класса - имя базового класса, из которого непосредственно наследуется класс. Последний параметр номер_версии - это число типа UINT, определяющее версию программы. Если разрабатывается новая версия приложения и изменяется набор данных, которые необходимо записать в файл, нужно изменить значение параметра номер_версии
.В классе должны быть определены специальные методы для записи и восстановления состояния объектов этого класса. Обычно эти методы сохраняют и восстанавливают элементы данных из класса. Таким образом, объекты класса сами отвечают за то, как они сохраняют и восстанавливают свое состояние.
Методы, сохраняющие и восстанавливающие объектов, взаимодействуют с объектом класса
CArchive , который осуществляет непосредственную запись и чтение информации из файла на диске.Класс
CObject содержит виртуальный метод Serialize , отвечающий за запись и чтение объектов классов, наследованных от класса CObject :virtual void Serialize(CArchive& ar);
В качестве параметра ar методу передается указатель на объект класса
CArchive , используемый для записи и восстановления состояния объекта класса CObject (или наследуемого от него класса). Чтобы узнать, какую операцию должен выполнить метод Serialize , необходимо воспользоваться методами IsLoading или IsStoring класса CArchive .Итак, при создании нового класса, в котором метод
Serialize применяется для сериализации данных, необходимо:- Чтобы класс был производным от класса
Приведем шаблон (заготовку) файлов определения и реализации класса, который обеспечивает процесс сериализации данных:
// фрагмент файла определения класса class CMyDoc:public CObject { DECLARE_SERIAL(CMyDoc) protected: virtual void Serialize(CArchive& ar); protected: CMyDoc(); protected: ~CMyDoc(); // другие описания класса ........................ }; // фрагмент файла реализации класса IMPLEMENT_SERIAL(CMyDoc, CObject,1) CMyDoc::CMyDoc() { // здесь возможно динамическое создание объектов и // инициализация переменных, если это необходимо ………… } CMyDoc::~CMyDoc() { // здесь возможно выполнение специальных действий // при разрушении объектов класса, например, // освобождение памяти динамически созданных объектов ……… } void CMyDoc::Serialize(CArchive& ar) { if(ar.Storing()) { // здесь следует добавить код для записи переменных в архив ……… } else { // здесь следует добавить код для чтения переменных из архива, ……… } // здесь следует добавить вызовы методов Serialize для переменных // класса CMyDoc, являющихся объектами классов, // имеющих собственные методы Serialize } // другие методы класса .......................
Механизм записи и восстановления объектов
Рассмотрим предлагаемую MFC модель обмена данными между документом и файлом, в котором этот процесс идет через промежуточный объект-архив класса
CArchive библиотеки MFC. Этот объект передается методу Serialize класса документа как параметр. Поэтому программист посылает данные не в файл, а в архив и читает из архива.Метод Serialize класса документа вызывается объектами-архивами, когда приложение при необходимости читает или записывает состояние этого документа, вызывая методы CArchive::ReadObject и CArchive::WriteObject для объекта-архива . При этом методу
Serialize передается ссылка на объект-архив, который и вызывает методы чтения или записи.Из архива данные передаются в файл или читаются из файла. Объект-архив всегда связан с файлом (объектом класса
CFile ). Диалог при открытии и сохранении документа позволяет задать имя физического файла, хранящего данные документа. Затем создается объект класса CFile , который связывается с объектом архивом. Заметим, что создаваемый автоматически объект-архив существует на время выполнения только одной операции - чтения документа или записи документа, после чего разрушается.Рассмотрим процесс записи и восстановления документа для однооконного или многооконного приложения, создаваемого средством AppWizard. При выборе команд Open, Save, SaveAs каркас приложения
:- Выводит соответствующее диалоговое окно для получения имени файла от пользователя.
- Открывает файл, указанный пользователем, как объект класса CFile . Создает объект-архив класса CArchive , связывая его с объектом файлом. Объект-архив получает статус "store" или "load", в зависимости от того, будет ли данные документа записываться или восстанавливаться из архива.
- Вызывает метод Serialize , определенный в классе документа приложения, производном от класса CDocument , используя методы WriteObject или ReadObject для объекта-архива.
- Определенный разработчиком приложения метод Serialize класса документа запишет значения переменных документа в архив. Через буфер архива данные передаются в файл, связанный с объектом-архивом.
- Завершив сохранение документа, каркас приложения разрушит созданный объект-архив, а затем и объект-файл.
В этом процессе на разработчика приложения возлагается единственная обязанность - определить метод Serialize класса документа. Он отвечает только за то, какие переменные документа и как будут записываться в архив.
Если программист сам организует процесс сериализации данных документа, то он сам должен проделать все перечисленные выше действия. Допустим, приложение выводит окно, которое имеет меню, содержащее пункты "Store" и "Load". Класс окна тогда имеет примерно следующее описание:
// фрагмент файла определения класса class CMyWnd : public CFrameWnd { protected: CMyDoc *readDoc; // указатель на объект класса документа CMyDoc writeDoc; // объект класса документа protected: afx_msg void OnStore(); afx_msg void OnLoad(); DECLARE_MESSAGE_MAP() // другие описания класса ........................ };
При выборе пользователем из меню пункта "Store" вызывается метод-обработчик OnStore, а при выборе пункта "Load" - метод-обработчик OnLoad. Приведем примеры организации процесса сохранения и восстановления документа.
// фрагмент файла реализации класса BEGIN_MESSAGE_MAP(CMyWnd, CFrameWnd) ON_COMMAND(ID_LOAD, OnLoad) ON_COMMAND(ID_STORE, OnStore) END_MESSAGE_MAP() void CMyWnd::OnLoad() { // создание стандартной панели выбора файла Open CFileDialog DlgOpen(TRUE,NULL,NULL, OFN_HIDEREADONLY,(LPCSTR)"Все файлы (*.*)|*.*||"); // отображение стандартной панели выбора файла Open if(DlgOpen.DoModal()==IDOK) { // открытие файла для чтения CStdioFile File(DlgOpen.GetPathName(),CFile::modeRead); // связывание файла с объектом класса CArchive CArchive ar(&File,CArchive::load); // запись документа readDoc=(CMyDoc *)ar.ReadObject(RUNTIME_CLASS(CMyDoc)); // закрыть объект ar и файл, связанный с ним ar.Close(); File.Close(); // данные из переменных класса документа помещаются // в переменные класса окна ............................................. } } void CMyWnd::OnStore() { // создание стандартной панели выбора файла SaveAs CFileDialog DlgSaveAs(FALSE,NULL,NULL, OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT, (LPCSTR)"Все файлы (*.*)|*.*||"); // отображение стандартной панели выбора файла SaveAs if(DlgSaveAs.DoModal()==IDOK) { // данные из переменных класса окна помещаются // в переменные класса документа .................................................... // открытие файла для записи CFile File(DlgSaveAs.GetPathName(), CFile::modeCreate|CFile::modeWrite); // связывание файла с объектом класса CArchive CArchive ar(&File,CArchive::store); // запись документа ar.WriteObject(&writeDoc); // закрыть объект ar и файл, связанный с ним ar.Close(); File.Close(); } } // другие методы класса .......................
Методы класса
CArchiveПеред тем, как создать объект-архив класса
CArchive , необходимо создать объект класса CFile . Связывая с объектом CFile файл на диске, нужно иметь в виду, что если нужно записать данные, то файл нужно открывать на запись, а если считать - то для чтения.Конструктор класса
CArchive имеет следующий вид:CArchive(CFile* pFile, UINT nMode, int nBufSize=512, void* lpBuf=NULL);
Параметр pFile должен содержать указатель на объект класса
CFile , из которого будут считываться или записываться данные. Перед вызовом конструктора файл, связанный с объектом pFile, должен быть уже открыт.Параметр nMode определяет, будут ли данные записываться в файл или считываться из него: CArchive::load (данные будут считываться из файла на диске), CArchive::store (данные будут записываться в файл на диске).
Для операций чтения и записи в файл класс
CArchive выполняет буферизацию. Размер этого буфера определяется необязательным параметром nBufSize. По умолчанию используется буфер размером 512 байт.Конструктор класса
CArchive сам получает у операционной системы блок оперативной памяти для буфера. Однако с помощью необязательного параметра lpBuf программист может предоставить ему собственный буфер. В этом случае параметр nBufSize должен указывать размер этого буфера.Созданный объект класса
CArchive можно будет использовать только для записи или только для чтения. Если необходимо сначала считать данные, а потом записать, следует создать для этого два отдельных объекта класса CArhive.Нельзя применять методы класса
CFile для доступа к файлу во время его использования совместно с объектами класса CArchive . Запись, чтение или перемещение указателя файла может вызвать нарушения во внутренней структуре файла архива.Как уже отмечалось, метод
Serialize класса документа вызывается объектами-архивами, когда приложение при необходимости читает или записывает состояние этого документа, вызывая методы CArchive::ReadObject и CArchive::WriteObject для объекта-архива. При этом методу Serialize передается ссылка на объект-архив, который и вызывает методы чтения или записи.После использования методов
CArchive::ReadObject и CArchive::WriteObjec t для восстановления или записи объектов, необходимо закрыть используемый для этого объект класса CArchive . Для этого нужно вызвать метод CArchive::Close .После вызова этого метода нужно закрыть файл, cвязанный с объектом
CArchive , вызвав метод CFile::Close , и удалить сам объект класса CFile .Запись в архивный файл
Когда приложение желает сохранить состояние объекта в файле, оно вызывает для него метод Serialize . В качестве параметра этому методу передается указатель на объект класса CArhive , связанный с файлом, открытым на запись.
Для того, чтобы определить, предназначен ли объект-архив для записи или для чтения, можно вызвать методы CArchive::IsLoading или CArchive::IsStoring .
Метод
IsStoring возвращает ненулевое значение, если данный объект предназначен для записи в файл, и нуль в противном случае. Метод IsLoading является прямой противоположностью метода IsStoring . Можно использовать любой метод для определения режима работы с объектом-архивом.В случае записи в архивный файл реализация метода должна сохранить в файле все элементы данных, которые потом потребуется восстановить. Для этого необходимо воспользоваться оператором
<< или методами WriteString и Write , определенными в классе CArchive .Оператор << можно использовать, для записи в архивный файл переменных простых типов, например, long, int, char и объектов других классов, наследованных от класса
CObject .Для записи в архивный файл массивов удобнее использовать метод
Write класса CArchive . Он позволяет записать в файл определенное количество байт из указанного буфера памяти.Если требуется сохранить строку символов, закрытую нулем, то гораздо удобнее вместо метода
Write использовать метод WriteString . Метод WriteString записывает в архивный файл строку, ограниченную нулевым символом.Чтение из архивного файла
Опишем теперь, как восстановить записанное ранее состояние объекта из архивного файла. Когда приложение желает восстановить состояние объекта данного класса, оно вызывает для него метод
Serialize . В качестве параметра этому методу передается указатель на объект класса CArchive , связанного с файлом, открытым для чтения.Реализация метода Serialize должна восстановить из файла все элементы данных, которые в него были записаны. Для этого можно воспользоваться оператором
>> или методами ReadString и Read , определенными в классе CArchive .Данные из архивного файла должны считываться в том же порядке, в каком они были в него записаны.
Оператор >> можно использовать, для чтения из архивного файла переменных простых типов, например, long, int, char и объектов других классов, наследованных от класса CObject.
Для чтения из архивного файла массивов, записанных в него методом Write, надо использовать метод
Read класса CArchive . Он позволяет прочитать из файла определенное количество байт и записать его в указанный буфер.Если требуется прочитать из архивного файла строку, записанную в него методом
WriteString , нужно воспользоваться методом ReadString . В состав класса CArchive входят два метода ReadString , которые предназначены для записи из файла строки в объект класса CString или в обычную строку.Если надо записать прочитанную из архивного файла строку в массив символов, нужно воспользоваться другим прототипом метода
ReadString .